forked from shaba/openuds
updating user interface manager
This commit is contained in:
parent
937240a9fc
commit
c07c21b6a9
@ -85,7 +85,7 @@ class Images(ModelHandler):
|
|||||||
'value': '',
|
'value': '',
|
||||||
'label': gettext('Image'),
|
'label': gettext('Image'),
|
||||||
'tooltip': gettext('Image object'),
|
'tooltip': gettext('Image object'),
|
||||||
'type': gui.InputField.Types.IMAGECHOICE,
|
'type': gui.InputField.Types.IMAGE_CHOICE,
|
||||||
'order': 100, # At end
|
'order': 100, # At end
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -179,7 +179,7 @@ class MetaPools(ModelHandler):
|
|||||||
),
|
),
|
||||||
'label': gettext('Associated Image'),
|
'label': gettext('Associated Image'),
|
||||||
'tooltip': gettext('Image assocciated with this service'),
|
'tooltip': gettext('Image assocciated with this service'),
|
||||||
'type': gui.InputField.Types.IMAGECHOICE,
|
'type': gui.InputField.Types.IMAGE_CHOICE,
|
||||||
'order': 120,
|
'order': 120,
|
||||||
'tab': gui.Tab.DISPLAY,
|
'tab': gui.Tab.DISPLAY,
|
||||||
},
|
},
|
||||||
@ -196,7 +196,7 @@ class MetaPools(ModelHandler):
|
|||||||
'tooltip': gettext(
|
'tooltip': gettext(
|
||||||
'Pool group for this pool (for pool classify on display)'
|
'Pool group for this pool (for pool classify on display)'
|
||||||
),
|
),
|
||||||
'type': gui.InputField.Types.IMAGECHOICE,
|
'type': gui.InputField.Types.IMAGE_CHOICE,
|
||||||
'order': 121,
|
'order': 121,
|
||||||
'tab': gui.Tab.DISPLAY,
|
'tab': gui.Tab.DISPLAY,
|
||||||
},
|
},
|
||||||
|
@ -99,7 +99,7 @@ class ServicesPoolGroups(ModelHandler):
|
|||||||
),
|
),
|
||||||
'label': gettext('Associated Image'),
|
'label': gettext('Associated Image'),
|
||||||
'tooltip': gettext('Image assocciated with this service'),
|
'tooltip': gettext('Image assocciated with this service'),
|
||||||
'type': gui.InputField.Types.IMAGECHOICE,
|
'type': gui.InputField.Types.IMAGE_CHOICE,
|
||||||
'order': 102,
|
'order': 102,
|
||||||
}
|
}
|
||||||
]:
|
]:
|
||||||
|
@ -400,7 +400,7 @@ class ServicesPools(ModelHandler):
|
|||||||
),
|
),
|
||||||
'label': gettext('Associated Image'),
|
'label': gettext('Associated Image'),
|
||||||
'tooltip': gettext('Image assocciated with this service'),
|
'tooltip': gettext('Image assocciated with this service'),
|
||||||
'type': gui.InputField.Types.IMAGECHOICE,
|
'type': gui.InputField.Types.IMAGE_CHOICE,
|
||||||
'order': 120,
|
'order': 120,
|
||||||
'tab': gettext('Display'),
|
'tab': gettext('Display'),
|
||||||
},
|
},
|
||||||
@ -417,7 +417,7 @@ class ServicesPools(ModelHandler):
|
|||||||
'tooltip': gettext(
|
'tooltip': gettext(
|
||||||
'Pool group for this pool (for pool classify on display)'
|
'Pool group for this pool (for pool classify on display)'
|
||||||
),
|
),
|
||||||
'type': gui.InputField.Types.IMAGECHOICE,
|
'type': gui.InputField.Types.IMAGE_CHOICE,
|
||||||
'order': 121,
|
'order': 121,
|
||||||
'tab': gettext('Display'),
|
'tab': gettext('Display'),
|
||||||
},
|
},
|
||||||
|
@ -145,7 +145,7 @@ class CryptoManager(metaclass=singleton.Singleton):
|
|||||||
encoded = encryptor.update(toEncode) + encryptor.finalize()
|
encoded = encryptor.update(toEncode) + encryptor.finalize()
|
||||||
|
|
||||||
if base64:
|
if base64:
|
||||||
return codecs.encode(encoded, 'base64') # Return as binary
|
encoded = codecs.encode(encoded, 'base64') # Return as bytes
|
||||||
|
|
||||||
return encoded
|
return encoded
|
||||||
|
|
||||||
|
@ -39,10 +39,13 @@ import typing
|
|||||||
import logging
|
import logging
|
||||||
import enum
|
import enum
|
||||||
from collections import abc
|
from collections import abc
|
||||||
|
import yaml
|
||||||
|
|
||||||
from django.utils.translation import get_language, gettext as _, gettext_noop
|
from django.utils.translation import get_language, gettext as _, gettext_noop
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from numpy import isin
|
from numpy import isin
|
||||||
|
from regex import B
|
||||||
|
from yaml import safe_dump
|
||||||
|
|
||||||
from uds.core.managers import cryptoManager
|
from uds.core.managers import cryptoManager
|
||||||
from uds.core.util.decorators import deprecatedClassValue
|
from uds.core.util.decorators import deprecatedClassValue
|
||||||
@ -62,6 +65,9 @@ PASSWORD_FIELD = b'\005'
|
|||||||
FIELD_SEPARATOR = b'\002'
|
FIELD_SEPARATOR = b'\002'
|
||||||
NAME_VALUE_SEPARATOR = b'\003'
|
NAME_VALUE_SEPARATOR = b'\003'
|
||||||
|
|
||||||
|
SERIALIZATION_HEADER = b'GUIZ'
|
||||||
|
SERIALIZATION_VERSION = b'\001'
|
||||||
|
|
||||||
|
|
||||||
class gui:
|
class gui:
|
||||||
"""
|
"""
|
||||||
@ -316,7 +322,6 @@ class gui:
|
|||||||
class Types(enum.Enum):
|
class Types(enum.Enum):
|
||||||
TEXT = 'text'
|
TEXT = 'text'
|
||||||
TEXT_AUTOCOMPLETE = 'text-autocomplete'
|
TEXT_AUTOCOMPLETE = 'text-autocomplete'
|
||||||
# TEXTBOX = 'textbox'
|
|
||||||
NUMERIC = 'numeric'
|
NUMERIC = 'numeric'
|
||||||
PASSWORD = 'password' # nosec: this is not a password
|
PASSWORD = 'password' # nosec: this is not a password
|
||||||
HIDDEN = 'hidden'
|
HIDDEN = 'hidden'
|
||||||
@ -324,9 +329,10 @@ class gui:
|
|||||||
MULTI_CHOICE = 'multichoice'
|
MULTI_CHOICE = 'multichoice'
|
||||||
EDITABLE_LIST = 'editlist'
|
EDITABLE_LIST = 'editlist'
|
||||||
CHECKBOX = 'checkbox'
|
CHECKBOX = 'checkbox'
|
||||||
IMAGECHOICE = 'imgchoice'
|
IMAGE_CHOICE = 'imgchoice'
|
||||||
DATE = 'date'
|
DATE = 'date'
|
||||||
INFO = 'dummy'
|
INFO = 'dummy'
|
||||||
|
IMAGE = 'image'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.value
|
return self.value
|
||||||
@ -338,30 +344,42 @@ class gui:
|
|||||||
|
|
||||||
def __init__(self, **options) -> None:
|
def __init__(self, **options) -> None:
|
||||||
# Added defaultValue as alias for defvalue
|
# Added defaultValue as alias for defvalue
|
||||||
|
self._data = {}
|
||||||
|
if 'type' in options:
|
||||||
|
self.type = options['type'] # set type first
|
||||||
|
|
||||||
defvalue = options.get(
|
defvalue = options.get(
|
||||||
'defvalue', options.get('defaultValue', options.get('defValue', ''))
|
'defvalue', options.get('defaultValue', options.get('defValue', ''))
|
||||||
)
|
)
|
||||||
if callable(defvalue):
|
if callable(defvalue):
|
||||||
defvalue = defvalue()
|
defvalue = defvalue()
|
||||||
self._data = {
|
self._data.update(
|
||||||
'length': options.get(
|
{
|
||||||
'length', gui.InputField.DEFAULT_LENTGH
|
'length': options.get(
|
||||||
), # Length is not used on some kinds of fields, but present in all anyway
|
'length', gui.InputField.DEFAULT_LENTGH
|
||||||
'required': options.get('required', False),
|
), # Length is not used on some kinds of fields, but present in all anyway
|
||||||
'label': options.get('label', ''),
|
'required': options.get('required', False),
|
||||||
'defvalue': str(defvalue),
|
'label': options.get('label', ''),
|
||||||
'rdonly': options.get(
|
'defvalue': str(defvalue),
|
||||||
'rdonly', options.get('readOnly', options.get('readonly', False))
|
'rdonly': options.get(
|
||||||
), # This property only affects in "modify" operations
|
'rdonly',
|
||||||
'order': options.get('order', 0),
|
options.get('readOnly', options.get('readonly', False)),
|
||||||
'tooltip': options.get('tooltip', ''),
|
), # This property only affects in "modify" operations
|
||||||
'type': str(gui.InputField.Types.TEXT),
|
'order': options.get('order', 0),
|
||||||
'value': options.get('value', ''),
|
'tooltip': options.get('tooltip', ''),
|
||||||
}
|
'type': str(gui.InputField.Types.TEXT),
|
||||||
|
'value': options.get('value', ''),
|
||||||
|
}
|
||||||
|
)
|
||||||
if 'tab' in options:
|
if 'tab' in options:
|
||||||
self._data['tab'] = str(options.get('tab')) # Ensure it's a string
|
self._data['tab'] = str(options.get('tab')) # Ensure it's a string
|
||||||
|
|
||||||
def _type(self, type_: typing.Union[Types, str]) -> None:
|
@property
|
||||||
|
def type(self) -> 'Types':
|
||||||
|
return gui.InputField.Types(self._data['type'])
|
||||||
|
|
||||||
|
@type.setter
|
||||||
|
def type(self, type_: Types) -> None:
|
||||||
"""
|
"""
|
||||||
Sets the type of this field.
|
Sets the type of this field.
|
||||||
|
|
||||||
@ -483,8 +501,7 @@ class gui:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **options) -> None:
|
def __init__(self, **options) -> None:
|
||||||
super().__init__(**options)
|
super().__init__(**options, type=gui.InputField.Types.TEXT)
|
||||||
self._type(gui.InputField.Types.TEXT)
|
|
||||||
multiline = int(options.get('multiline', 0))
|
multiline = int(options.get('multiline', 0))
|
||||||
if multiline > 8:
|
if multiline > 8:
|
||||||
multiline = 8
|
multiline = 8
|
||||||
@ -501,8 +518,8 @@ class gui:
|
|||||||
|
|
||||||
def __init__(self, **options) -> None:
|
def __init__(self, **options) -> None:
|
||||||
super().__init__(**options)
|
super().__init__(**options)
|
||||||
# Change the type
|
# Update parent type
|
||||||
self._type(gui.InputField.Types.TEXT_AUTOCOMPLETE)
|
self.type = gui.InputField.Types.TEXT_AUTOCOMPLETE
|
||||||
# And store values in a list
|
# And store values in a list
|
||||||
self._data['values'] = gui.convertToChoices(options.get('values', []))
|
self._data['values'] = gui.convertToChoices(options.get('values', []))
|
||||||
|
|
||||||
@ -534,7 +551,7 @@ class gui:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **options):
|
def __init__(self, **options):
|
||||||
super().__init__(**options)
|
super().__init__(**options, type=gui.InputField.Types.NUMERIC)
|
||||||
self._data['minValue'] = int(
|
self._data['minValue'] = int(
|
||||||
options.get('minValue', options.get('minvalue', '987654321'))
|
options.get('minValue', options.get('minvalue', '987654321'))
|
||||||
)
|
)
|
||||||
@ -542,8 +559,6 @@ class gui:
|
|||||||
options.get('maxValue', options.get('maxvalue', '987654321'))
|
options.get('maxValue', options.get('maxvalue', '987654321'))
|
||||||
)
|
)
|
||||||
|
|
||||||
self._type(gui.InputField.Types.NUMERIC)
|
|
||||||
|
|
||||||
def _setValue(self, value: typing.Any):
|
def _setValue(self, value: typing.Any):
|
||||||
# Internally stores an string
|
# Internally stores an string
|
||||||
super()._setValue(str(value))
|
super()._setValue(str(value))
|
||||||
@ -601,8 +616,7 @@ class gui:
|
|||||||
for v in 'value', 'defvalue':
|
for v in 'value', 'defvalue':
|
||||||
self.processValue(v, options)
|
self.processValue(v, options)
|
||||||
|
|
||||||
super().__init__(**options)
|
super().__init__(**options, type=gui.InputField.Types.DATE)
|
||||||
self._type(gui.InputField.Types.DATE)
|
|
||||||
|
|
||||||
def date(self, min: bool = True) -> datetime.date:
|
def date(self, min: bool = True) -> datetime.date:
|
||||||
"""
|
"""
|
||||||
@ -666,8 +680,7 @@ class gui:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **options):
|
def __init__(self, **options):
|
||||||
super().__init__(**options)
|
super().__init__(**options, type=gui.InputField.Types.PASSWORD)
|
||||||
self._type(gui.InputField.Types.PASSWORD)
|
|
||||||
|
|
||||||
def cleanStr(self):
|
def cleanStr(self):
|
||||||
return str(self.value).strip()
|
return str(self.value).strip()
|
||||||
@ -705,9 +718,8 @@ class gui:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **options):
|
def __init__(self, **options):
|
||||||
super().__init__(**options)
|
super().__init__(**options, type=gui.InputField.Types.HIDDEN)
|
||||||
self._isSerializable: bool = options.get('serializable', '') != ''
|
self._isSerializable: bool = options.get('serializable', '') != ''
|
||||||
self._type(gui.InputField.Types.HIDDEN)
|
|
||||||
|
|
||||||
def isSerializable(self) -> bool:
|
def isSerializable(self) -> bool:
|
||||||
return self._isSerializable
|
return self._isSerializable
|
||||||
@ -732,8 +744,7 @@ class gui:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **options):
|
def __init__(self, **options):
|
||||||
super().__init__(**options)
|
super().__init__(**options, type=gui.InputField.Types.CHECKBOX)
|
||||||
self._type(gui.InputField.Types.CHECKBOX)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _checkTrue(val: typing.Union[str, bytes, bool]) -> bool:
|
def _checkTrue(val: typing.Union[str, bytes, bool]) -> bool:
|
||||||
@ -852,7 +863,7 @@ class gui:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **options):
|
def __init__(self, **options):
|
||||||
super().__init__(**options)
|
super().__init__(**options, type=gui.InputField.Types.CHOICE)
|
||||||
self._data['values'] = gui.convertToChoices(options.get('values'))
|
self._data['values'] = gui.convertToChoices(options.get('values'))
|
||||||
if 'fills' in options:
|
if 'fills' in options:
|
||||||
# Save fnc to register as callback
|
# Save fnc to register as callback
|
||||||
@ -861,7 +872,6 @@ class gui:
|
|||||||
fills.pop('function')
|
fills.pop('function')
|
||||||
self._data['fills'] = fills
|
self._data['fills'] = fills
|
||||||
gui.callbacks[fills['callbackName']] = fnc
|
gui.callbacks[fills['callbackName']] = fnc
|
||||||
self._type(gui.InputField.Types.CHOICE)
|
|
||||||
|
|
||||||
def setValues(self, values: typing.List['gui.ChoiceType']):
|
def setValues(self, values: typing.List['gui.ChoiceType']):
|
||||||
"""
|
"""
|
||||||
@ -871,11 +881,9 @@ class gui:
|
|||||||
|
|
||||||
class ImageChoiceField(InputField):
|
class ImageChoiceField(InputField):
|
||||||
def __init__(self, **options):
|
def __init__(self, **options):
|
||||||
super().__init__(**options)
|
super().__init__(**options, type=gui.InputField.Types.IMAGE_CHOICE)
|
||||||
self._data['values'] = options.get('values', [])
|
self._data['values'] = options.get('values', [])
|
||||||
|
|
||||||
self._type(gui.InputField.Types.IMAGECHOICE)
|
|
||||||
|
|
||||||
def setValues(self, values: typing.List[typing.Any]):
|
def setValues(self, values: typing.List[typing.Any]):
|
||||||
"""
|
"""
|
||||||
Set the values for this choice field
|
Set the values for this choice field
|
||||||
@ -917,12 +925,11 @@ class gui:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **options):
|
def __init__(self, **options):
|
||||||
super().__init__(**options)
|
super().__init__(**options, type=gui.InputField.Types.MULTI_CHOICE)
|
||||||
if options.get('values') and isinstance(options.get('values'), dict):
|
if options.get('values') and isinstance(options.get('values'), dict):
|
||||||
options['values'] = gui.convertToChoices(options['values'])
|
options['values'] = gui.convertToChoices(options['values'])
|
||||||
self._data['values'] = options.get('values', [])
|
self._data['values'] = options.get('values', [])
|
||||||
self._data['rows'] = options.get('rows', -1)
|
self._data['rows'] = options.get('rows', -1)
|
||||||
self._type(gui.InputField.Types.MULTI_CHOICE)
|
|
||||||
|
|
||||||
def setValues(self, values: typing.List[typing.Any]) -> None:
|
def setValues(self, values: typing.List[typing.Any]) -> None:
|
||||||
"""
|
"""
|
||||||
@ -961,9 +968,8 @@ class gui:
|
|||||||
SEPARATOR = '\001'
|
SEPARATOR = '\001'
|
||||||
|
|
||||||
def __init__(self, **options) -> None:
|
def __init__(self, **options) -> None:
|
||||||
super().__init__(**options)
|
super().__init__(**options, type=gui.InputField.Types.EDITABLE_LIST)
|
||||||
self._data['values'] = gui.convertToList(options.get('values', []))
|
self._data['values'] = gui.convertToList(options.get('values', []))
|
||||||
self._type(gui.InputField.Types.EDITABLE_LIST)
|
|
||||||
|
|
||||||
def _setValue(self, value):
|
def _setValue(self, value):
|
||||||
"""
|
"""
|
||||||
@ -978,8 +984,7 @@ class gui:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **options) -> None:
|
def __init__(self, **options) -> None:
|
||||||
super().__init__(**options)
|
super().__init__(**options, type=gui.InputField.Types.IMAGE)
|
||||||
self._type(gui.InputField.Types.TEXT)
|
|
||||||
|
|
||||||
class InfoField(InputField):
|
class InfoField(InputField):
|
||||||
"""
|
"""
|
||||||
@ -987,8 +992,7 @@ class gui:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **options) -> None:
|
def __init__(self, **options) -> None:
|
||||||
super().__init__(**options)
|
super().__init__(**options, type=gui.InputField.Types.INFO)
|
||||||
self._type(gui.InputField.Types.INFO)
|
|
||||||
|
|
||||||
|
|
||||||
class UserInterfaceType(type):
|
class UserInterfaceType(type):
|
||||||
@ -1030,6 +1034,7 @@ class UserInterface(metaclass=UserInterfaceType):
|
|||||||
By default, the values passed to this class constructor are used to fill
|
By default, the values passed to this class constructor are used to fill
|
||||||
the gui form fields values.
|
the gui form fields values.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Class variable that will hold the gui fields description
|
# Class variable that will hold the gui fields description
|
||||||
_base_gui: typing.ClassVar[typing.Dict[str, gui.InputField]]
|
_base_gui: typing.ClassVar[typing.Dict[str, gui.InputField]]
|
||||||
|
|
||||||
@ -1051,7 +1056,9 @@ class UserInterface(metaclass=UserInterfaceType):
|
|||||||
|
|
||||||
self._gui = copy.deepcopy(self._base_gui)
|
self._gui = copy.deepcopy(self._base_gui)
|
||||||
for key, val in self._gui.items(): # And refresh self references to them
|
for key, val in self._gui.items(): # And refresh self references to them
|
||||||
setattr(self, key, val) # val is an InputField instance, so it is a reference to self._gui[key]
|
setattr(
|
||||||
|
self, key, val
|
||||||
|
) # val is an InputField instance, so it is a reference to self._gui[key]
|
||||||
|
|
||||||
if values is not None:
|
if values is not None:
|
||||||
for k, v in self._gui.items():
|
for k, v in self._gui.items():
|
||||||
@ -1168,6 +1175,43 @@ class UserInterface(metaclass=UserInterfaceType):
|
|||||||
|
|
||||||
return codecs.encode(FIELD_SEPARATOR.join(arr), 'zip')
|
return codecs.encode(FIELD_SEPARATOR.join(arr), 'zip')
|
||||||
|
|
||||||
|
# TODO: This method is being created, not to be used yet
|
||||||
|
def serializeFormTo(
|
||||||
|
self, serializer: typing.Optional[typing.Callable[[typing.Any], str]] = None
|
||||||
|
) -> bytes:
|
||||||
|
"""New form serialization
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bytes -- serialized form (zipped)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def serialize(value: typing.Any) -> str:
|
||||||
|
if serializer:
|
||||||
|
return serializer(value)
|
||||||
|
return yaml.safe_dump(value)
|
||||||
|
|
||||||
|
converters: typing.Mapping[
|
||||||
|
gui.InfoField.Types, typing.Callable[[gui.InputField], typing.Optional[str]]
|
||||||
|
] = {
|
||||||
|
gui.InputField.Types.HIDDEN: (
|
||||||
|
lambda x: None if not x.isSerializable() else x.value
|
||||||
|
),
|
||||||
|
gui.InputField.Types.INFO: lambda x: None,
|
||||||
|
gui.InputField.Types.EDITABLE_LIST: lambda x: serialize(x.value),
|
||||||
|
gui.InputField.Types.MULTI_CHOICE: lambda x: serialize(x.value),
|
||||||
|
gui.InputField.Types.PASSWORD: lambda x: (
|
||||||
|
cryptoManager().AESCrypt(x.value.encode('utf8'), UDSK, True).decode()
|
||||||
|
),
|
||||||
|
gui.InputField.Types.NUMERIC: lambda x: str(int(x.num())),
|
||||||
|
gui.InputField.Types.CHECKBOX: lambda x: str(x.isTrue()),
|
||||||
|
}
|
||||||
|
arr = [(k, v.type, converters[v.type](v)) for k, v in self._gui.items()]
|
||||||
|
|
||||||
|
return codecs.encode(
|
||||||
|
SERIALIZATION_HEADER + SERIALIZATION_VERSION + serialize(arr).encode(),
|
||||||
|
'zip',
|
||||||
|
)
|
||||||
|
|
||||||
def unserializeForm(self, values: bytes) -> None:
|
def unserializeForm(self, values: bytes) -> None:
|
||||||
"""
|
"""
|
||||||
This method unserializes the values previously obtained using
|
This method unserializes the values previously obtained using
|
||||||
@ -1220,9 +1264,7 @@ class UserInterface(metaclass=UserInterfaceType):
|
|||||||
# Values can contain invalid characters, so we log every single char
|
# Values can contain invalid characters, so we log every single char
|
||||||
# logger.info('Invalid serialization data on {0} {1}'.format(self, values.encode('hex')))
|
# logger.info('Invalid serialization data on {0} {1}'.format(self, values.encode('hex')))
|
||||||
|
|
||||||
def guiDescription(
|
def guiDescription(self) -> typing.List[typing.MutableMapping[str, typing.Any]]:
|
||||||
self
|
|
||||||
) -> typing.List[typing.MutableMapping[str, typing.Any]]:
|
|
||||||
"""
|
"""
|
||||||
This simple method generates the theGui description needed by the
|
This simple method generates the theGui description needed by the
|
||||||
administration client, so it can
|
administration client, so it can
|
||||||
|
Loading…
Reference in New Issue
Block a user