From ce9110b7ca6d24ef09a9468f98e7071d6270eae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20G=C3=B3mez=20Garc=C3=ADa?= Date: Sat, 26 Jul 2025 02:07:58 +0200 Subject: [PATCH] Refactor GUI field handling to replace add_default_fields with default_fields for consistency --- server/src/uds/REST/methods/accounts.py | 2 +- server/src/uds/REST/methods/authenticators.py | 11 +- server/src/uds/REST/methods/calendars.py | 2 +- server/src/uds/REST/methods/images.py | 2 +- server/src/uds/REST/methods/meta_pools.py | 2 +- server/src/uds/REST/methods/mfas.py | 2 +- server/src/uds/REST/methods/networks.py | 2 +- server/src/uds/REST/methods/notifiers.py | 2 +- server/src/uds/REST/methods/osmanagers.py | 2 +- server/src/uds/REST/methods/providers.py | 2 +- .../uds/REST/methods/servers_management.py | 2 +- server/src/uds/REST/methods/services.py | 2 +- .../uds/REST/methods/services_pool_groups.py | 2 +- server/src/uds/REST/methods/services_pools.py | 2 +- server/src/uds/REST/methods/transports.py | 2 +- .../uds/REST/methods/tunnels_management.py | 2 +- server/src/uds/REST/model/base.py | 199 ++++++++++-------- server/src/uds/core/types/ui.py | 26 ++- server/src/uds/core/ui/user_interface.py | 25 ++- 19 files changed, 173 insertions(+), 118 deletions(-) diff --git a/server/src/uds/REST/methods/accounts.py b/server/src/uds/REST/methods/accounts.py index ff4aca88e..277bdd2f7 100644 --- a/server/src/uds/REST/methods/accounts.py +++ b/server/src/uds/REST/methods/accounts.py @@ -94,7 +94,7 @@ class Accounts(ModelHandler[AccountItem]): } def get_gui(self, type_: str) -> list[typing.Any]: - return self.add_default_fields([], ['name', 'comments', 'tags']) + return self.default_fields([], ['name', 'comments', 'tags']) def timemark(self, item: 'Model') -> typing.Any: """ diff --git a/server/src/uds/REST/methods/authenticators.py b/server/src/uds/REST/methods/authenticators.py index 965b20308..40c0dc575 100644 --- a/server/src/uds/REST/methods/authenticators.py +++ b/server/src/uds/REST/methods/authenticators.py @@ -121,19 +121,20 @@ class Authenticators(ModelHandler[AuthenticatorItem]): # Not of my type return None - def get_gui(self, type_: str) -> list[typing.Any]: + def get_gui(self, type_: str) -> list[types.ui.GuiElement]: try: auth_type = auths.factory().lookup(type_) if auth_type: # Create a new instance of the authenticator to access to its GUI with Environment.temporary_environment() as env: auth_instance = auth_type(env, None) - field = self.add_default_fields( + fields = self.default_fields( auth_instance.gui_description(), ['name', 'comments', 'tags', 'priority', 'small_name', 'networks'], ) + self.add_field( - field, + fields, { 'name': 'state', 'value': consts.auth.VISIBLE, @@ -154,7 +155,7 @@ class Authenticators(ModelHandler[AuthenticatorItem]): # If supports mfa, add MFA provider selector field if auth_type.provides_mfa(): self.add_field( - field, + fields, { 'name': 'mfa_id', 'choices': [gui.choice_item('', str(_('None')))] @@ -168,7 +169,7 @@ class Authenticators(ModelHandler[AuthenticatorItem]): 'tab': types.ui.Tab.MFA, }, ) - return field + return fields raise Exception() # Not found except Exception as e: logger.info('Type not found: %s', e) diff --git a/server/src/uds/REST/methods/calendars.py b/server/src/uds/REST/methods/calendars.py index 67bfdef36..91cb9b42a 100644 --- a/server/src/uds/REST/methods/calendars.py +++ b/server/src/uds/REST/methods/calendars.py @@ -103,4 +103,4 @@ class Calendars(ModelHandler[CalendarItem]): } def get_gui(self, type_: str) -> list[typing.Any]: - return self.add_default_fields([], ['name', 'comments', 'tags']) + return self.default_fields([], ['name', 'comments', 'tags']) diff --git a/server/src/uds/REST/methods/images.py b/server/src/uds/REST/methods/images.py index 4f993b94d..d66399d34 100644 --- a/server/src/uds/REST/methods/images.py +++ b/server/src/uds/REST/methods/images.py @@ -93,7 +93,7 @@ class Images(ModelHandler[ImageItem]): def get_gui(self, type_: str) -> list[typing.Any]: return self.add_field( - self.add_default_fields([], ['name']), + self.default_fields([], ['name']), { 'name': 'data', 'value': '', diff --git a/server/src/uds/REST/methods/meta_pools.py b/server/src/uds/REST/methods/meta_pools.py index 2f56ea616..772684f74 100644 --- a/server/src/uds/REST/methods/meta_pools.py +++ b/server/src/uds/REST/methods/meta_pools.py @@ -180,7 +180,7 @@ class MetaPools(ModelHandler[MetaPoolItem]): # Gui related def get_gui(self, type_: str) -> list[typing.Any]: - local_gui = self.add_default_fields([], ['name', 'comments', 'tags']) + local_gui = self.default_fields([], ['name', 'comments', 'tags']) for field in [ { diff --git a/server/src/uds/REST/methods/mfas.py b/server/src/uds/REST/methods/mfas.py index 79ae309ab..436f136ce 100644 --- a/server/src/uds/REST/methods/mfas.py +++ b/server/src/uds/REST/methods/mfas.py @@ -89,7 +89,7 @@ class MFA(ModelHandler[MFAItem]): with Environment.temporary_environment() as env: mfa = mfa_type(env, None) - local_gui = self.add_default_fields(mfa.gui_description(), ['name', 'comments', 'tags']) + local_gui = self.default_fields(mfa.gui_description(), ['name', 'comments', 'tags']) self.add_field( local_gui, { diff --git a/server/src/uds/REST/methods/networks.py b/server/src/uds/REST/methods/networks.py index 95a7afe85..df6171efa 100644 --- a/server/src/uds/REST/methods/networks.py +++ b/server/src/uds/REST/methods/networks.py @@ -97,7 +97,7 @@ class Networks(ModelHandler[NetworkItem]): def get_gui(self, type_: str) -> list[typing.Any]: return self.add_field( - self.add_default_fields([], ['name', 'tags']), + self.default_fields([], ['name', 'tags']), { 'name': 'net_string', 'value': '', diff --git a/server/src/uds/REST/methods/notifiers.py b/server/src/uds/REST/methods/notifiers.py index ea932f2eb..942994a6e 100644 --- a/server/src/uds/REST/methods/notifiers.py +++ b/server/src/uds/REST/methods/notifiers.py @@ -99,7 +99,7 @@ class Notifiers(ModelHandler[NotifierItem]): with Environment.temporary_environment() as env: notifier = notifier_type(env, None) - local_gui = self.add_default_fields(notifier.gui_description(), ['name', 'comments', 'tags']) + local_gui = self.default_fields(notifier.gui_description(), ['name', 'comments', 'tags']) for field in [ { diff --git a/server/src/uds/REST/methods/osmanagers.py b/server/src/uds/REST/methods/osmanagers.py index bf90ce1cc..50f89789a 100644 --- a/server/src/uds/REST/methods/osmanagers.py +++ b/server/src/uds/REST/methods/osmanagers.py @@ -117,7 +117,7 @@ class OsManagers(ModelHandler[OsManagerItem]): with Environment.temporary_environment() as env: osmanager = osmanager_type(env, None) - return self.add_default_fields( + return self.default_fields( osmanager.gui_description(), ['name', 'comments', 'tags'], ) diff --git a/server/src/uds/REST/methods/providers.py b/server/src/uds/REST/methods/providers.py index 887fbaa7a..434f0735b 100644 --- a/server/src/uds/REST/methods/providers.py +++ b/server/src/uds/REST/methods/providers.py @@ -149,7 +149,7 @@ class Providers(ModelHandler[ProviderItem]): if provider_type: with Environment.temporary_environment() as env: provider = provider_type(env, None) - return self.add_default_fields(provider.gui_description(), ['name', 'comments', 'tags']) + return self.default_fields(provider.gui_description(), ['name', 'comments', 'tags']) raise exceptions.rest.NotFound('Type not found!') def allservices(self) -> typing.Generator[types.rest.ItemDictType, None, None]: diff --git a/server/src/uds/REST/methods/servers_management.py b/server/src/uds/REST/methods/servers_management.py index 4b2b3d904..f5dbeec0b 100644 --- a/server/src/uds/REST/methods/servers_management.py +++ b/server/src/uds/REST/methods/servers_management.py @@ -498,7 +498,7 @@ class ServersGroups(ModelHandler[GroupItem ]): kind = _('Unmanaged') title = _('of type') + f' {subkind.upper()} {kind}' return self.add_field( - self.add_default_fields( + self.default_fields( [], ['name', 'comments', 'tags'], ), diff --git a/server/src/uds/REST/methods/services.py b/server/src/uds/REST/methods/services.py index cf49817da..a691390bd 100644 --- a/server/src/uds/REST/methods/services.py +++ b/server/src/uds/REST/methods/services.py @@ -337,7 +337,7 @@ class Services(DetailHandler[ServiceItem]): # pylint: disable=too-many-public-m service = service_type( env, parent_instance ) # Instantiate it so it has the opportunity to alter gui description based on parent - local_gui = self.add_default_fields(service.gui_description(), ['name', 'comments', 'tags']) + local_gui = self.default_fields(service.gui_description(), ['name', 'comments', 'tags']) self.add_field( local_gui, { diff --git a/server/src/uds/REST/methods/services_pool_groups.py b/server/src/uds/REST/methods/services_pool_groups.py index 007758b9b..733247257 100644 --- a/server/src/uds/REST/methods/services_pool_groups.py +++ b/server/src/uds/REST/methods/services_pool_groups.py @@ -96,7 +96,7 @@ class ServicesPoolGroups(ModelHandler[ServicePoolGroupItem]): # Gui related def get_gui(self, type_: str) -> list[typing.Any]: - local_gui = self.add_default_fields([], ['name', 'comments', 'priority']) + local_gui = self.default_fields([], ['name', 'comments', 'priority']) for field in [ { diff --git a/server/src/uds/REST/methods/services_pools.py b/server/src/uds/REST/methods/services_pools.py index 082022d9c..8a2003d84 100644 --- a/server/src/uds/REST/methods/services_pools.py +++ b/server/src/uds/REST/methods/services_pools.py @@ -322,7 +322,7 @@ class ServicesPools(ModelHandler[ServicePoolItem]): gettext('Create at least a service before creating a new service pool') ) - g = self.add_default_fields([], ['name', 'comments', 'tags']) + g = self.default_fields([], ['name', 'comments', 'tags']) for f in [ { diff --git a/server/src/uds/REST/methods/transports.py b/server/src/uds/REST/methods/transports.py index 0127c3c6f..4fdb1faee 100644 --- a/server/src/uds/REST/methods/transports.py +++ b/server/src/uds/REST/methods/transports.py @@ -110,7 +110,7 @@ class Transports(ModelHandler[TransportItem]): with Environment.temporary_environment() as env: transport = transport_type(env, None) - field = self.add_default_fields( + field = self.default_fields( transport.gui_description(), ['name', 'comments', 'tags', 'priority', 'networks'] ) field = self.add_field( diff --git a/server/src/uds/REST/methods/tunnels_management.py b/server/src/uds/REST/methods/tunnels_management.py index 365d6d3b5..e07ef9a56 100644 --- a/server/src/uds/REST/methods/tunnels_management.py +++ b/server/src/uds/REST/methods/tunnels_management.py @@ -180,7 +180,7 @@ class Tunnels(ModelHandler): def get_gui(self, type_: str) -> list[typing.Any]: return self.add_field( - self.add_default_fields( + self.default_fields( [], ['name', 'comments', 'tags'], ), diff --git a/server/src/uds/REST/model/base.py b/server/src/uds/REST/model/base.py index 4ba2a1df6..5f775c2b0 100644 --- a/server/src/uds/REST/model/base.py +++ b/server/src/uds/REST/model/base.py @@ -133,113 +133,136 @@ class BaseModelHandler(Handler, typing.Generic[types.rest.T_Item]): gui.append(v) return gui - def add_default_fields(self, gui: list[typing.Any], flds: list[str]) -> list[typing.Any]: + def append_field( + self, gui_list: list[types.ui.GuiElement], field: types.ui.GuiElement + ) -> list[types.ui.GuiElement]: + """ + Appends a field to the gui description + + Args: + gui_list: List of GuiElement to append the field to + field: Field to append + + Returns: + The updated gui list with the new field appended + """ + gui_list.append(field) + return gui_list + + def default_fields(self, gui: list[types.ui.GuiElement], flds: list[str]) -> list[types.ui.GuiElement]: """ Adds default fields (based in a list) to a "gui" description :param gui: Gui list where the "default" fielsds will be added :param flds: List of fields names requested to be added. Valid values are 'name', 'comments', 'priority' and 'small_name', 'short_name', 'tags' """ - if 'tags' in flds: - self.add_field( - gui, + TRANS_FLDS: dict[str, list[types.ui.GuiElement]] = { + 'tags': [ { 'name': 'tags', - 'label': _('Tags'), - 'type': 'taglist', - 'tooltip': _('Tags for this element'), - 'order': 0 - 105, - }, - ) - if 'name' in flds: - self.add_field( - gui, + 'gui': { + 'label': _('Tags'), + 'type': 'taglist', + 'tooltip': _('Tags for this element'), + 'order': 0 - 105, + }, + } + ], + 'name': [ { 'name': 'name', - 'type': 'text', - 'required': True, - 'label': _('Name'), - 'length': 128, - 'tooltip': _('Name of this element'), - 'order': 0 - 100, - }, - ) - if 'comments' in flds: - self.add_field( - gui, + 'gui': { + 'type': 'text', + 'required': True, + 'label': _('Name'), + 'length': 128, + 'tooltip': _('Name of this element'), + 'order': 0 - 100, + }, + } + ], + 'comments': [ { 'name': 'comments', - 'label': _('Comments'), - 'type': 'text', - 'lines': 3, - 'tooltip': _('Comments for this element'), - 'length': 256, - 'order': 0 - 90, - }, - ) - if 'priority' in flds: - self.add_field( - gui, + 'gui': { + 'label': _('Comments'), + 'type': 'text', + 'lines': 3, + 'tooltip': _('Comments for this element'), + 'length': 256, + 'order': 0 - 90, + }, + } + ], + 'priority': [ { 'name': 'priority', - 'type': 'numeric', - 'label': _('Priority'), - 'tooltip': _('Selects the priority of this element (lower number means higher priority)'), - 'required': True, - 'value': 1, - 'length': 4, - 'order': 0 - 85, - }, - ) - if 'small_name' in flds: - self.add_field( - gui, + 'gui': { + 'label': _('Priority'), + 'type': 'numeric', + 'required': True, + 'default': 1, + 'length': 4, + 'tooltip': _( + 'Selects the priority of this element (lower number means higher priority)' + ), + 'order': 0 - 85, + }, + } + ], + 'small_name': [ { 'name': 'small_name', - 'type': 'text', - 'label': _('Label'), - 'tooltip': _('Label for this element'), - 'required': True, - 'length': 128, - 'order': 0 - 80, - }, - ) - if 'networks' in flds: - self.add_field( - gui, - { - 'name': 'net_filtering', - 'value': 'n', - 'choices': [ - {'id': 'n', 'text': _('No filtering')}, - {'id': 'a', 'text': _('Allow selected networks')}, - {'id': 'd', 'text': _('Deny selected networks')}, - ], - 'label': _('Network Filtering'), - 'tooltip': _( - 'Type of network filtering. Use "Disabled" to disable origin check, "Allow" to only enable for selected networks or "Deny" to deny from selected networks' - ), - 'type': 'choice', - 'order': 100, # At end - 'tab': types.ui.Tab.ADVANCED, - }, - ) - self.add_field( - gui, + 'gui': { + 'label': _('Label'), + 'type': 'text', + 'required': True, + 'length': 128, + 'tooltip': _('Label for this element'), + 'order': 0 - 80, + }, + } + ], + 'networks': [ { 'name': 'networks', - 'value': [], - 'choices': sorted( - [{'id': x.uuid, 'text': x.name} for x in Network.objects.all()], - key=lambda x: x['text'].lower(), - ), - 'label': _('Networks'), - 'tooltip': _('Networks associated. If No network selected, will mean "all networks"'), - 'type': 'multichoice', - 'order': 101, - 'tab': types.ui.Tab.ADVANCED, + 'gui': { + 'label': _('Networks'), + 'type': 'multichoice', + 'tooltip': _('Networks associated. If No network selected, will mean "all networks"'), + 'choices': sorted( + [{'id': x.uuid, 'text': x.name} for x in Network.objects.all()], + key=lambda x: x['text'].lower(), + ), + 'order': 101, + 'tab': types.ui.Tab.ADVANCED, + }, }, - ) + { + 'name': 'net_filtering', + 'gui': { + 'label': _('Network Filtering'), + 'type': 'choice', # Type of network filtering + 'default': 'n', + 'choices': [ + {'id': 'n', 'text': _('No filtering')}, + {'id': 'a', 'text': _('Allow selected networks')}, + {'id': 'd', 'text': _('Deny selected networks')}, + ], + 'tooltip': _( + 'Type of network filtering. Use "Disabled" to disable origin check, "Allow" to only enable for selected networks or "Deny" to deny from selected networks' + ), + 'order': 100, # At end + 'tab': types.ui.Tab.ADVANCED, + }, + }, + ], + } + + for i in flds: + if i in TRANS_FLDS: + for field in TRANS_FLDS[i]: + gui = self.append_field(gui, field) return gui diff --git a/server/src/uds/core/types/ui.py b/server/src/uds/core/types/ui.py index d2e2b44a4..d16081f10 100644 --- a/server/src/uds/core/types/ui.py +++ b/server/src/uds/core/types/ui.py @@ -167,11 +167,33 @@ class FieldInfo: """Returns a dict with all fields that are not None""" return {k: v for k, v in dataclasses.asdict(self).items() if v is not None} +class GuiDescription(typing.TypedDict): + """ + GuiDescription is a dictionary that describes a GUI element. + It contains the name of the element, the GUI description, and the value. + """ + label: str + tooltip: str + order: int + type: str + readonly: typing.NotRequired[bool] + default: typing.NotRequired[str|int|float|bool] + required: typing.NotRequired[bool] + length: typing.NotRequired[int] + lines: typing.NotRequired[int] + pattern: typing.NotRequired[str] + tab: typing.NotRequired[str] + choices: typing.NotRequired[list[ChoiceItem]] + min_value: typing.NotRequired[int] + max_value: typing.NotRequired[int] + fills: typing.NotRequired[Filler] + rows: typing.NotRequired[int] + class GuiElement(typing.TypedDict): name: str - gui: dict[str, list[dict[str, typing.Any]]] - value: typing.Any + value: typing.NotRequired[typing.Any] + gui: GuiDescription # Row styles diff --git a/server/src/uds/core/ui/user_interface.py b/server/src/uds/core/ui/user_interface.py index 07725af70..8e4eee883 100644 --- a/server/src/uds/core/ui/user_interface.py +++ b/server/src/uds/core/ui/user_interface.py @@ -325,7 +325,7 @@ class gui: value=value, tab=tab, ) - + @property def field_name(self) -> str: """ @@ -389,7 +389,7 @@ class gui: """ self._field_info.value = value - def gui_description(self) -> dict[str, typing.Any]: + def gui_description(self) -> types.ui.GuiDescription: """ Returns the dictionary with the description of this item. We copy it, cause we need to translate the label and tooltip fields @@ -400,12 +400,17 @@ class gui: for i in ('value', 'old_field_name'): if i in data: del data[i] # We don't want to send some values on gui_description + # Translate label and tooltip data['label'] = gettext(data['label']) if data['label'] else '' data['tooltip'] = gettext(data['tooltip']) if data['tooltip'] else '' + + # And, if tab is set, translate it too if 'tab' in data: data['tab'] = gettext(data['tab']) # Translates tab name - data['default'] = self.default # We need to translate default value - return data + + data['default'] = self.default + + return typing.cast(types.ui.GuiDescription, data) @property def default(self) -> typing.Any: @@ -799,7 +804,7 @@ class gui: def value(self, value: datetime.date | str) -> None: self._set_value(value) - def gui_description(self) -> dict[str, typing.Any]: + def gui_description(self) -> types.ui.GuiDescription: fldgui = super().gui_description() # Convert if needed value and default to string (YYYY-MM-DD) if 'default' in fldgui: @@ -1653,10 +1658,13 @@ class UserInterface(metaclass=UserInterfaceType): if internal_field_type not in FIELD_DECODERS: logger.warning('Field %s has no decoder', field_name) continue - + if field_type != internal_field_type.name: # Especial case for text fields converted to password fields - if not (internal_field_type == types.ui.FieldType.PASSWORD and field_type == types.ui.FieldType.TEXT.name): + if not ( + internal_field_type == types.ui.FieldType.PASSWORD + and field_type == types.ui.FieldType.TEXT.name + ): logger.warning( 'Field %s has different type than expected: %s != %s', field_name, @@ -1791,12 +1799,13 @@ def password_compat_field_decoder(value: str) -> str: """ Compatibility function to decode text fields converted to password fields """ - try: + try: value = CryptoManager.manager().aes_decrypt(value.encode('utf8'), UDSK, True).decode() except Exception: pass return value + # Dictionaries used to encode/decode fields to be stored on database FIELDS_ENCODERS: typing.Final[ collections.abc.Mapping[