import json
from asyncio import iscoroutinefunction
from pyplanet.apps.core.pyplanet.models.setting import Setting as SettingModel
from pyplanet.contrib.setting.exceptions import TypeUnknownException, SerializationException, SettingException
[docs]class Setting:
"""
The setting class is for defining a setting for the end-user.
This setting can be changed with /settings and //settings.
With this class you can define or manage your setting that is going to be public for all other apps and end-user.
You can get notified of changes with the ``change_target`` in the init of this class. Point this to a method (async or sync)
with the following params: ``old_value`` and ``new_value``.
Example:
.. code-block:: python
my_setting = Setting(
'dedimania_code', 'Dedimania Server Code', Setting.CAT_KEYS, type=str,
description='The secret dedimania code. Get one at $lhttp://dedimania.net/tm2stats/?do=register',
default=None
)
my_other_setting = Setting(
'sample_boolean', 'Booleans for the win!', Setting.CAT_BEHAVIOUR, type=bool, description='Example',
)
"""
CAT_GENERAL = 'General'
CAT_KEYS = 'Keys'
CAT_DESIGN = 'Design'
CAT_POSITION = 'Position'
CAT_BEHAVIOUR = 'Behaviour'
CAT_FEATURES = 'Features'
CAT_OTHER = 'Other'
ALL_CATEGORIES = [CAT_GENERAL, CAT_DESIGN, CAT_POSITION, CAT_BEHAVIOUR, CAT_FEATURES, CAT_KEYS, CAT_OTHER]
[docs] def __init__(
self, key: str, name: str, category: str, type=str, description: str = None, choices=None, default=None,
change_target=None
):
"""
Create setting with properties.
:param key: Key of setting, this is mainly only used for the backend and for referencing the setting.
You should keep this unique in your app!
:param name: Name of the setting that will be displayed as a small label to the player.
:param category: Category from Categories.*. Must be provided!
:param type: Type of value to expect, use python types here. str by default.
:param description: Description to provide help and instructions to the player.
:param choices: List or tuple with choices, only when wanting to restrict values to selected options.
:param default: Default value if not provided from database. This will be returned. Defaults to None.
:param change_target: Target method to call when the setting value has been changed.
"""
if category not in self.ALL_CATEGORIES:
raise SettingException('Invalid category. Must be an category in the Categories static class.')
# Prepare property for app specific setting. Will be injected by the register command.
self.app_label = None
self.key = key
self.name = name
self.description = description
self.category = category
self.default = default
self.type = type
self.choices = choices
self.change_target = change_target
# Prepare the model instance here. This will be filled once it's fetched for the first time (or inited).
self._instance = None
self._value = (False, None)
[docs] async def initiate_setting(self):
"""
Initiate database record for setting.
"""
return await SettingModel.get_or_create_from_info(
key=self.key, app=self.app_label, category=self.category, name=self.name, description=self.description,
value=None
)
[docs] def unserialize_value(self, value):
"""
Unserialize the datastorage value to the python value, based on the type of the setting.
:param value: Value from database.
:return: Python value.
:raise pyplanet.contrib.setting.exceptions.SerializationException: SerializationException
"""
if value is None:
return self.default
try:
if self.type == str:
return str(value)
elif self.type == int:
return int(value)
elif self.type == float:
return float(value)
elif self.type == bool:
return bool(value)
elif self.type == list or self.type == set or self.type == dict:
return json.loads(value)
else:
raise TypeUnknownException('The type \'{}\' is unknown!'.format(self.type))
except TypeUnknownException:
raise
except Exception as e:
raise SerializationException('Error with unserialization of the setting \'{}\''.format(str(self))) from e
[docs] def serialize_value(self, value):
"""
Serialize the python value to the data store value, based on the type of the setting.
:param value: Python Value.
:return: Database Value
"""
# Always set to Null, so we get the default value back.
if value is None:
return value
# Empty value, set the default.
if value == '':
return self.default
if self.choices and value not in self.choices:
raise SerializationException('Value given is not in the predefined choices!')
try:
if self.type == int:
value = int(value)
elif self.type == float:
value = float(value)
elif self.type == bool:
if value == '1' or value == 1 or value == '0' or value == 0:
value = bool(int(value))
except:
pass
if type(value) != self.type:
raise SerializationException(
'Your given value is not of the type you specified! \'{}\' != \'{}\''.format(type(value), self.type)
)
if self.type == list or self.type == set or self.type == dict:
return json.dumps(value)
if self.type == bool:
return value
return str(value)
@property
def type_name(self):
"""
Get the name of the specified type in string format, suited for displaying to end-user.
:return: User friendly name of type.
"""
if self.type == str:
return 'string'
elif self.type == int:
return 'integer'
elif self.type == float:
return 'float'
elif self.type == bool:
return 'boolean'
elif self.type == list or self.type == set:
return 'list'
elif self.type == dict:
return 'dict'
else:
return 'unknown'
[docs] async def get_value(self, refresh=False):
"""
Get the value or the default value for the setting model.
:param refresh: Force a refresh of the value.
:return: Value in the desired type and unserialized from database/storage.
:raise: NotFound / SerializationException
"""
if not self._value[0] or refresh is True:
model = await self.get_model()
self._value = (True, self.unserialize_value(model.value))
return self._value[1]
[docs] async def set_value(self, value):
"""
Set the value, this will serialize and save the setting to the data storage.
:param value: Python value input.
:raise: NotFound / SerializationException
"""
old_value = self._value[0] if self._value and len(self._value) > 0 else None
model = await self.get_model()
model.value = self.serialize_value(value)
self._value = (True, model.value)
await model.save()
# Call the change target.
if self.change_target and callable(self.change_target):
if iscoroutinefunction(self.change_target):
await self.change_target(old_value, model.value)
else:
self.change_target(old_value, model.value)
[docs] async def clear(self):
"""
Clear the value in the data storage. This will set the value to None, and will return the default value on
request of data.
:raise: NotFound / SerializationException
"""
return await self.set_value(None)
[docs] async def get_model(self):
"""
Get the model for the setting. This will return the model instance or raise an exception when not found.
:return: Model instance
:raise: NotFound
"""
return await SettingModel.get(key=self.key, app=self.app_label)
[docs] def __str__(self):
return self.name