1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-03-20 06:50:23 +03:00

Replaced user interface

User interface serialization methods has been improved to:
* Allow versioning
* Posibility of change default serializer
* Now serializes to "safe yaml" instead of pickle
This commit is contained in:
Adolfo Gómez García 2022-12-11 00:20:57 +01:00
parent 0fb6b2a02d
commit 0e94fae1bf
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
8 changed files with 259 additions and 33 deletions

View File

@ -39,10 +39,53 @@ from uds.core.ui.user_interface import (
)
import time
from django.conf import settings
from ...fixtures.user_interface import TestingUserInterface, DEFAULTS
class UserinterfaceTest(UDSTestCase):
def test_userinterface(self):
# Helpers
def ensure_values_fine(self, ui: TestingUserInterface) -> None:
# Ensure that all values are fine for the ui fields
self.assertEqual(ui.str_field.value, DEFAULTS['str_field'], 'str_field')
self.assertEqual(ui.str_auto_field.value, DEFAULTS['str_auto_field'], 'str_auto_field')
self.assertEqual(ui.num_field.num(), DEFAULTS['num_field'], 'num_field')
self.assertEqual(ui.password_field.value, DEFAULTS['password_field'], 'password_field')
# Hidden field is not stored, so it's not checked
self.assertEqual(ui.choice_field.value, DEFAULTS['choice_field'], 'choice_field')
self.assertEqual(ui.multi_choice_field.value, DEFAULTS['multi_choice_field'], 'multi_choice_field')
self.assertEqual(ui.editable_list_field.value, DEFAULTS['editable_list_field'], 'editable_list_field')
self.assertEqual(ui.checkbox_field.value, DEFAULTS['checkbox_field'], 'checkbox_field')
self.assertEqual(ui.image_choice_field.value, DEFAULTS['image_choice_field'], 'image_choice_field')
self.assertEqual(ui.image_field.value, DEFAULTS['image_field'], 'image_field')
self.assertEqual(ui.date_field.value, DEFAULTS['date_field'], 'date_field')
def test_serialization(self):
pass
def test_old_serialization(self):
# This test is to ensure that old serialized data can be loaded
# This data is from a
ui = TestingUserInterface()
data = ui.oldSerializeForm()
ui2 = TestingUserInterface()
ui2.oldUnserializeForm(data)
self.assertEqual(ui, ui2)
self.ensure_values_fine(ui2)
# Now unserialize old data with new method, (will internally call oldUnserializeForm)
ui3 = TestingUserInterface()
ui3.unserializeForm(data)
self.assertEqual(ui, ui3)
self.ensure_values_fine(ui3)
def test_new_serialization(self):
# This test is to ensure that new serialized data can be loaded
ui = TestingUserInterface()
data = ui.serializeForm()
ui2 = TestingUserInterface()
ui2.unserializeForm(data)
self.assertEqual(ui, ui2)
self.ensure_values_fine(ui2)

View File

@ -26,5 +26,5 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""

View File

@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2022 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import typing
import datetime
from uds.core.ui.user_interface import UserInterface, gui
DEFAULTS = {
'str_field': 'Default value text',
'str_auto_field': 'Default value auto',
'num_field': 50,
'password_field': 'Default value password',
'hidden_field': 'Default value hidden',
'choice_field': 'Default value choice',
'multi_choice_field': ['Default value multi choice 1', 'Default value multi choice 2'],
'editable_list_field': ['Default value editable list 1', 'Default value editable list 2'],
'checkbox_field': True,
'image_choice_field': 'Default value image choice',
'image_field': 'Default value image',
'date_field': '2009-12-09',
'info_field': 'Default value info',
}
class TestingUserInterface(UserInterface):
str_field = gui.TextField(
label='Text Field',
order=0,
tooltip='This is a text field',
required=True,
value=DEFAULTS['str_field'],
)
str_auto_field = gui.TextAutocompleteField(
label='Text Autocomplete Field',
order=1,
tooltip='This is a text autocomplete field',
required=True,
defvalue=DEFAULTS['str_auto_field'],
values=['Value 1', 'Value 2', 'Value 3'],
)
num_field = gui.NumericField(
label='Numeric Field',
order=1,
tooltip='This is a numeric field',
required=True,
defvalue=DEFAULTS['num_field'],
minValue=0,
maxValue=100,
)
password_field = gui.PasswordField(
label='Password Field',
order=2,
tooltip='This is a password field',
required=True,
defvalue=DEFAULTS['password_field'],
)
hidden_field = gui.HiddenField(
label='Hidden Field',
order=3,
tooltip='This is a hidden field',
required=True,
defvalue=DEFAULTS['hidden_field'],
)
choice_field = gui.ChoiceField(
label='Choice Field',
order=4,
tooltip='This is a choice field',
required=True,
value=DEFAULTS['choice_field'],
values=['Value 1', 'Value 2', 'Value 3'],
)
multi_choice_field = gui.MultiChoiceField(
label='Multi Choice Field',
order=5,
tooltip='This is a multi choice field',
required=True,
defvalue=DEFAULTS['multi_choice_field'],
values=['Value 1', 'Value 2', 'Value 3'],
)
editable_list_field = gui.EditableListField(
label='Editable List Field',
order=6,
tooltip='This is a editable list field',
required=True,
defvalue=DEFAULTS['editable_list_field'],
)
checkbox_field = gui.CheckBoxField(
label='Checkbox Field',
order=7,
tooltip='This is a checkbox field',
required=True,
defvalue=DEFAULTS['checkbox_field'],
)
image_choice_field = gui.ImageChoiceField(
label='Image Choice Field',
order=8,
tooltip='This is a image choice field',
required=True,
defvalue=DEFAULTS['image_choice_field'],
values=['Value 1', 'Value 2', 'Value 3'],
)
image_field = gui.ImageField(
label='Image Field',
order=9,
tooltip='This is a image field',
required=True,
defvalue=DEFAULTS['image_field'],
)
date_field = gui.DateField(
label='Date Field',
order=10,
tooltip='This is a date field',
required=True,
defvalue=DEFAULTS['date_field'],
)
# Equals operator, to speed up tests writing
def __eq__(self, other: typing.Any) -> bool:
if not isinstance(other, TestingUserInterface):
return False
return (
self.str_field.value == other.str_field.value
and self.str_auto_field.value == other.str_auto_field.value
and self.num_field.num() == other.num_field.num()
and self.password_field.value == other.password_field.value
# Hidden field is not compared, because it is not serialized
and self.choice_field.value == other.choice_field.value
and self.multi_choice_field.value == other.multi_choice_field.value
and self.editable_list_field.value == other.editable_list_field.value
and self.checkbox_field.value == other.checkbox_field.value
and self.image_choice_field.value == other.image_choice_field.value
and self.image_field.value == other.image_field.value
and self.date_field.value == other.date_field.value
# Info field is not compared, because it is not serialized
)

View File

@ -118,7 +118,7 @@ class SampleAuth(auths.Authenticator):
# : We will define a simple form where we will use a simple
# : list editor to allow entering a few group names
groups = gui.EditableList(label=_('Groups'), values=['Gods', 'Daemons', 'Mortals'])
groups = gui.EditableListField(label=_('Groups'), values=['Gods', 'Daemons', 'Mortals'])
def initialize(self, values: typing.Optional[typing.Dict[str, typing.Any]]) -> None:
"""

View File

@ -145,7 +145,7 @@ class CryptoManager(metaclass=singleton.Singleton):
encoded = encryptor.update(toEncode) + encryptor.finalize()
if base64:
encoded = codecs.encode(encoded, 'base64') # Return as bytes
encoded = codecs.encode(encoded, 'base64').strip() # Return as bytes
return encoded

View File

@ -39,13 +39,10 @@ import typing
import logging
import enum
from collections import abc
import yaml
import yaml
from django.utils.translation import get_language, gettext as _, gettext_noop
from django.conf import settings
from numpy import isin
from regex import B
from yaml import safe_dump
from uds.core.managers import cryptoManager
from uds.core.util.decorators import deprecatedClassValue
@ -367,7 +364,7 @@ class gui:
), # This property only affects in "modify" operations
'order': options.get('order', 0),
'tooltip': options.get('tooltip', ''),
'value': options.get('value', ''),
'value': options.get('value', defvalue),
}
)
if 'tab' in options:
@ -594,19 +591,30 @@ class gui:
def processValue(
self, valueName: str, options: typing.Dict[str, typing.Any]
) -> None:
val = options.get(valueName, datetime.date.today())
try:
val = options.get(valueName, None)
if not val and valueName == 'defvalue':
val = datetime.date.today()
elif val == datetime.date.min:
val = datetime.date(2000, 1, 1)
elif val == datetime.date.max:
# val = datetime.date(2099, 12, 31)
if not val and valueName == 'defvalue':
val = datetime.date.today()
elif isinstance(val, str):
val = datetime.datetime.strptime(val, '%Y-%m-%d').date()
elif val == datetime.date.min:
val = datetime.date(2000, 1, 1)
elif val == datetime.date.max:
# val = datetime.date(2099, 12, 31)
val = datetime.date.today()
elif not isinstance(val, datetime.date):
val = datetime.date.today()
# Any error, use today
except Exception:
val = datetime.date.today()
options[valueName] = val.strftime('%Y-%m-%d')
def __init__(self, **options):
if 'value' not in options:
options['value'] = options['defvalue']
for v in 'value', 'defvalue':
self.processValue(v, options)
@ -932,7 +940,7 @@ class gui:
"""
self._data['values'] = gui.convertToChoices(values)
class EditableList(InputField):
class EditableListField(InputField):
"""
Editables list are lists of editable elements (i.e., a list of IPs, macs,
names, etcc) treated as simple strings with no id
@ -1120,7 +1128,7 @@ class UserInterface(metaclass=UserInterfaceType):
logger.debug('Values Dict: %s', dic)
return dic
def serializeForm(self) -> bytes:
def oldSerializeForm(self) -> bytes:
"""
All values stored at form fields are serialized and returned as a single
string
@ -1170,8 +1178,7 @@ class UserInterface(metaclass=UserInterfaceType):
return codecs.encode(FIELD_SEPARATOR.join(arr), 'zip')
# TODO: This method is being created, not to be used yet
def serializeFormTo(
def serializeForm(
self, serializer: typing.Optional[typing.Callable[[typing.Any], str]] = None
) -> bytes:
"""New form serialization
@ -1207,14 +1214,14 @@ class UserInterface(metaclass=UserInterfaceType):
gui.InputField.Types.INFO: lambda x: None,
}
# Any unexpected type will raise an exception
arr = [(k, v.type, fw_converters[v.type](v)) for k, v in self._gui.items() if fw_converters[v.type](v) is not None]
arr = [(k, v.type.name, fw_converters[v.type](v)) for k, v in self._gui.items() if fw_converters[v.type](v) is not None]
return codecs.encode(
SERIALIZATION_HEADER + SERIALIZATION_VERSION + serialize(arr).encode(),
'zip',
)
def unserializeFormFrom(
def unserializeForm(
self, values: bytes, serializer: typing.Optional[typing.Callable[[str], typing.Any]] = None
) -> None:
"""New form unserialization
@ -1234,19 +1241,32 @@ class UserInterface(metaclass=UserInterfaceType):
if not values:
return
values = codecs.decode(values, 'zip')
if not values:
tmp_values = codecs.decode(values, 'zip')
if not tmp_values:
return
if not values.startswith(SERIALIZATION_HEADER):
if not tmp_values.startswith(SERIALIZATION_HEADER):
# Unserialize with old method
self.unserializeForm(values)
self.oldUnserializeForm(values)
return
values = tmp_values
version = values[len(SERIALIZATION_HEADER) : len(SERIALIZATION_HEADER) + len(SERIALIZATION_VERSION)]
# Currently, only 1 version is available, ignore it
values = values[len(SERIALIZATION_HEADER) + len(SERIALIZATION_VERSION) :]
arr = unserialize(values.decode())
# Set all values to defaults ones
for k in self._gui:
if (
self._gui[k].isType(gui.InputField.Types.HIDDEN)
and self._gui[k].isSerializable() is False
):
# logger.debug('Field {0} is not unserializable'.format(k))
continue
self._gui[k].value = self._gui[k].defValue
converters: typing.Mapping[
gui.InfoField.Types, typing.Callable[[str], typing.Any]
] = {
@ -1271,12 +1291,12 @@ class UserInterface(metaclass=UserInterfaceType):
if k not in self._gui:
logger.warning('Field %s not found in form', k)
continue
if t != self._gui[k].type:
if t != self._gui[k].type.name:
logger.warning('Field %s has different type than expected', k)
continue
self._gui[k].value = converters[t](v)
self._gui[k].value = converters[self._gui[k].type](v)
def unserializeForm(self, values: bytes) -> None:
def oldUnserializeForm(self, values: bytes) -> None:
"""
This method unserializes the values previously obtained using
:py:meth:`serializeForm`, and stores

View File

@ -68,7 +68,7 @@ class IPMachinesService(IPServiceBase):
rdonly=False,
)
ipList = gui.EditableList(
ipList = gui.EditableListField(
label=_('List of servers'),
tooltip=_('List of servers available for this service'),
)

View File

@ -226,7 +226,7 @@ class ServiceTwo(services.Service):
deployedType = SampleUserDeploymentTwo
# Gui, we will use here the EditableList field
names = gui.EditableList(label=_('List of names'))
names = gui.EditableListField(label=_('List of names'))
def __init__(self, environment, parent, values=None):
"""