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:
parent
0fb6b2a02d
commit
0e94fae1bf
@ -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)
|
2
server/src/tests/fixtures/__init__.py
vendored
2
server/src/tests/fixtures/__init__.py
vendored
@ -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
|
||||
"""
|
||||
|
163
server/src/tests/fixtures/user_interface.py
vendored
Normal file
163
server/src/tests/fixtures/user_interface.py
vendored
Normal 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
|
||||
)
|
@ -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:
|
||||
"""
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
)
|
||||
|
@ -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):
|
||||
"""
|
||||
|
Loading…
x
Reference in New Issue
Block a user