Merge with develop and fix merge conflicts

This commit is contained in:
Griatch 2018-10-01 20:58:16 +02:00
commit 72f4fedcbe
148 changed files with 20005 additions and 2718 deletions

View file

@ -2,7 +2,7 @@ from django.contrib import admin
from evennia.typeclasses.models import Tag
from django import forms
from evennia.utils.picklefield import PickledFormField
from evennia.utils.dbserialize import from_pickle
from evennia.utils.dbserialize import from_pickle, _SaverSet
import traceback
@ -164,12 +164,12 @@ class AttributeForm(forms.ModelForm):
attr_category = forms.CharField(label="Category",
help_text="type of attribute, for sorting",
required=False,
max_length=4)
max_length=128)
attr_value = PickledFormField(label="Value", help_text="Value to pickle/save", required=False)
attr_type = forms.CharField(label="Type",
help_text="Internal use. Either unset (normal Attribute) or \"nick\"",
required=False,
max_length=4)
max_length=16)
attr_strvalue = forms.CharField(label="String Value",
help_text="Only set when using the Attribute as a string-only store",
required=False,
@ -213,6 +213,9 @@ class AttributeForm(forms.ModelForm):
self.instance.attr_key = attr_key
self.instance.attr_category = attr_category
self.instance.attr_value = attr_value
# prevent set from being transformed to unicode
if isinstance(attr_value, set) or isinstance(attr_value, _SaverSet):
self.fields['attr_value'].disabled = True
self.instance.deserialized_value = from_pickle(attr_value)
self.instance.attr_strvalue = attr_strvalue
self.instance.attr_type = attr_type
@ -237,6 +240,17 @@ class AttributeForm(forms.ModelForm):
instance.attr_lockstring = self.cleaned_data['attr_lockstring']
return instance
def clean_attr_value(self):
"""
Prevent Sets from being cleaned due to literal_eval failing on them. Otherwise they will be turned into
unicode.
"""
data = self.cleaned_data['attr_value']
initial = self.instance.attr_value
if isinstance(initial, set) or isinstance(initial, _SaverSet):
return initial
return data
class AttributeFormSet(forms.BaseInlineFormSet):
"""

View file

@ -245,7 +245,7 @@ class AttributeHandler(object):
found from cache or database.
Notes:
When given a category only, a search for all objects
of that cateogory is done and a the category *name* is is
of that cateogory is done and the category *name* is
stored. This tells the system on subsequent calls that the
list of cached attributes of this category is up-to-date
and that the cache can be queried for category matches
@ -282,6 +282,8 @@ class AttributeHandler(object):
"attribute__db_attrtype": self._attrtype,
"attribute__db_key__iexact": key.lower(),
"attribute__db_category__iexact": category.lower() if category else None}
if not self.obj.pk:
return []
conn = getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)
if conn:
attr = conn[0].attribute
@ -433,6 +435,7 @@ class AttributeHandler(object):
def __init__(self):
self.key = None
self.value = default
self.category = None
self.strvalue = str(default) if default is not None else None
ret = []
@ -528,8 +531,8 @@ class AttributeHandler(object):
repeat-calling add when having many Attributes to add.
Args:
indata (tuple): Tuples of varying length representing the
Attribute to add to this object.
indata (list): List of tuples of varying length representing the
Attribute to add to this object. Supported tuples are
- `(key, value)`
- `(key, value, category)`
- `(key, value, category, lockstring)`
@ -561,7 +564,7 @@ class AttributeHandler(object):
ntup = len(tup)
keystr = str(tup[0]).strip().lower()
new_value = tup[1]
category = str(tup[2]).strip().lower() if ntup > 2 else None
category = str(tup[2]).strip().lower() if ntup > 2 and tup[2] is not None else None
lockstring = tup[3] if ntup > 3 else ""
attr_objs = self._getcache(keystr, category)
@ -570,7 +573,7 @@ class AttributeHandler(object):
attr_obj = attr_objs[0]
# update an existing attribute object
attr_obj.db_category = category
attr_obj.db_lock_storage = lockstring
attr_obj.db_lock_storage = lockstring or ''
attr_obj.save(update_fields=["db_category", "db_lock_storage"])
if strattr:
# store as a simple string (will not notify OOB handlers)
@ -587,7 +590,7 @@ class AttributeHandler(object):
"db_attrtype": self._attrtype,
"db_value": None if strattr else to_pickle(new_value),
"db_strvalue": new_value if strattr else None,
"db_lock_storage": lockstring}
"db_lock_storage": lockstring or ''}
new_attr = Attribute(**kwargs)
new_attr.save()
new_attrobjs.append(new_attr)

View file

@ -8,6 +8,8 @@ import shlex
from django.db.models import Q
from evennia.utils import idmapper
from evennia.utils.utils import make_iter, variable_from_module
from evennia.typeclasses.attributes import Attribute
from evennia.typeclasses.tags import Tag
__all__ = ("TypedObjectManager", )
_GA = object.__getattribute__
@ -56,17 +58,19 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
dbmodel = self.model.__dbclass__.__name__.lower()
query = [("attribute__db_attrtype", attrtype), ("attribute__db_model", dbmodel)]
if obj:
query.append(("%s__id" % self.model.__name__.lower(), obj.id))
query.append(("%s__id" % self.model.__dbclass__.__name__.lower(), obj.id))
if key:
query.append(("attribute__db_key", key))
if category:
query.append(("attribute__db_category", category))
if strvalue:
query.append(("attribute__db_strvalue", strvalue))
elif value:
# strvalue and value are mutually exclusive
if value:
# no reason to make strvalue/value mutually exclusive at this level
query.append(("attribute__db_value", value))
return [th.attribute for th in self.model.db_attributes.through.objects.filter(**dict(query))]
return Attribute.objects.filter(
pk__in=self.model.db_attributes.through.objects.filter(
**dict(query)).values_list("attribute_id", flat=True))
def get_nick(self, key=None, category=None, value=None, strvalue=None, obj=None):
"""
@ -145,6 +149,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
# Tag manager methods
def get_tag(self, key=None, category=None, obj=None, tagtype=None, global_search=False):
"""
Return Tag objects by key, by category, by object (it is
@ -188,7 +193,9 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
query.append(("tag__db_key", key))
if category:
query.append(("tag__db_category", category))
return [th.tag for th in self.model.db_tags.through.objects.filter(**dict(query))]
return Tag.objects.filter(
pk__in=self.model.db_tags.through.objects.filter(
**dict(query)).values_list("tag_id", flat=True))
def get_permission(self, key=None, category=None, obj=None):
"""
@ -222,25 +229,58 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
def get_by_tag(self, key=None, category=None, tagtype=None):
"""
Return objects having tags with a given key or category or
combination of the two.
Return objects having tags with a given key or category or combination of the two.
Also accepts multiple tags/category/tagtype
Args:
key (str, optional): Tag key. Not case sensitive.
category (str, optional): Tag category. Not case sensitive.
tagtype (str or None, optional): 'type' of Tag, by default
key (str or list, optional): Tag key or list of keys. Not case sensitive.
category (str or list, optional): Tag category. Not case sensitive. If `key` is
a list, a single category can either apply to all keys in that list or this
must be a list matching the `key` list element by element. If no `key` is given,
all objects with tags of this category are returned.
tagtype (str, optional): 'type' of Tag, by default
this is either `None` (a normal Tag), `alias` or
`permission`.
`permission`. This always apply to all queried tags.
Returns:
objects (list): Objects with matching tag.
Raises:
IndexError: If `key` and `category` are both lists and `category` is shorter
than `key`.
"""
if not (key or category):
return []
keys = make_iter(key) if key else []
categories = make_iter(category) if category else []
n_keys = len(keys)
n_categories = len(categories)
dbmodel = self.model.__dbclass__.__name__.lower()
query = [("db_tags__db_tagtype", tagtype), ("db_tags__db_model", dbmodel)]
if key:
query.append(("db_tags__db_key", key.lower()))
if category:
query.append(("db_tags__db_category", category.lower()))
return self.filter(**dict(query))
query = self.filter(db_tags__db_tagtype__iexact=tagtype,
db_tags__db_model__iexact=dbmodel).distinct()
if n_keys > 0:
# keys and/or categories given
if n_categories == 0:
categories = [None for _ in range(n_keys)]
elif n_categories == 1 and n_keys > 1:
cat = categories[0]
categories = [cat for _ in range(n_keys)]
elif 1 < n_categories < n_keys:
raise IndexError("get_by_tag needs a single category or a list of categories "
"the same length as the list of tags.")
for ikey, key in enumerate(keys):
query = query.filter(db_tags__db_key__iexact=key,
db_tags__db_category__iexact=categories[ikey])
else:
# only one or more categories given
for category in categories:
query = query.filter(db_tags__db_category__iexact=category)
return query
def get_by_permission(self, key=None, category=None):
"""
@ -613,6 +653,42 @@ class TypeclassManager(TypedObjectManager):
"""
return super().filter(db_typeclass_path=self.model.path).count()
def annotate(self, *args, **kwargs):
"""
Overload annotate method to filter on typeclass before annotating.
Args:
*args (any): Positional arguments passed along to queryset annotate method.
**kwargs (any): Keyword arguments passed along to queryset annotate method.
Returns:
Annotated queryset.
"""
return super(TypeclassManager, self).filter(db_typeclass_path=self.model.path).annotate(*args, **kwargs)
def values(self, *args, **kwargs):
"""
Overload values method to filter on typeclass first.
Args:
*args (any): Positional arguments passed along to values method.
**kwargs (any): Keyword arguments passed along to values method.
Returns:
Queryset of values dictionaries, just filtered by typeclass first.
"""
return super(TypeclassManager, self).filter(db_typeclass_path=self.model.path).values(*args, **kwargs)
def values_list(self, *args, **kwargs):
"""
Overload values method to filter on typeclass first.
Args:
*args (any): Positional arguments passed along to values_list method.
**kwargs (any): Keyword arguments passed along to values_list method.
Returns:
Queryset of value_list tuples, just filtered by typeclass first.
"""
return super(TypeclassManager, self).filter(db_typeclass_path=self.model.path).values_list(*args, **kwargs)
def _get_subclasses(self, cls):
"""
Recursively get all subclasses to a class.

View file

@ -272,14 +272,15 @@ class TagHandler(object):
def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False):
"""
Get the tag for the given key or list of tags.
Get the tag for the given key, category or combination of the two.
Args:
key (str or list): The tag or tags to retrieve.
key (str or list, optional): The tag or tags to retrieve.
default (any, optional): The value to return in case of no match.
category (str, optional): The Tag category to limit the
request to. Note that `None` is the valid, default
category.
category. If no `key` is given, all tags of this category will be
returned.
return_tagobj (bool, optional): Return the Tag object itself
instead of a string representation of the Tag.
return_list (bool, optional): Always return a list, regardless

View file

@ -0,0 +1,59 @@
"""
Unit tests for typeclass base system
"""
from evennia.utils.test_resources import EvenniaTest
# ------------------------------------------------------------
# Manager tests
# ------------------------------------------------------------
class TestTypedObjectManager(EvenniaTest):
def _manager(self, methodname, *args, **kwargs):
return list(getattr(self.obj1.__class__.objects, methodname)(*args, **kwargs))
def test_get_by_tag_no_category(self):
self.obj1.tags.add("tag1")
self.obj1.tags.add("tag2")
self.obj1.tags.add("tag2c")
self.obj2.tags.add("tag2")
self.obj2.tags.add("tag2a")
self.obj2.tags.add("tag2b")
self.obj2.tags.add("tag3 with spaces")
self.obj2.tags.add("tag4")
self.obj2.tags.add("tag2c")
self.assertEquals(self._manager("get_by_tag", "tag1"), [self.obj1])
self.assertEquals(self._manager("get_by_tag", "tag2"), [self.obj1, self.obj2])
self.assertEquals(self._manager("get_by_tag", "tag2a"), [self.obj2])
self.assertEquals(self._manager("get_by_tag", "tag3 with spaces"), [self.obj2])
self.assertEquals(self._manager("get_by_tag", ["tag2a", "tag2b"]), [self.obj2])
self.assertEquals(self._manager("get_by_tag", ["tag2a", "tag1"]), [])
self.assertEquals(self._manager("get_by_tag", ["tag2a", "tag4", "tag2c"]), [self.obj2])
def test_get_by_tag_and_category(self):
self.obj1.tags.add("tag5", "category1")
self.obj1.tags.add("tag6", )
self.obj1.tags.add("tag7", "category1")
self.obj1.tags.add("tag6", "category3")
self.obj1.tags.add("tag7", "category4")
self.obj2.tags.add("tag5", "category1")
self.obj2.tags.add("tag5", "category2")
self.obj2.tags.add("tag6", "category3")
self.obj2.tags.add("tag7", "category1")
self.obj2.tags.add("tag7", "category5")
self.assertEquals(self._manager("get_by_tag", "tag5", "category1"), [self.obj1, self.obj2])
self.assertEquals(self._manager("get_by_tag", "tag6", "category1"), [])
self.assertEquals(self._manager("get_by_tag", "tag6", "category3"), [self.obj1, self.obj2])
self.assertEquals(self._manager("get_by_tag", ["tag5", "tag6"],
["category1", "category3"]), [self.obj1, self.obj2])
self.assertEquals(self._manager("get_by_tag", ["tag5", "tag7"],
"category1"), [self.obj1, self.obj2])
self.assertEquals(self._manager("get_by_tag", category="category1"), [self.obj1, self.obj2])
self.assertEquals(self._manager("get_by_tag", category="category2"), [self.obj2])
self.assertEquals(self._manager("get_by_tag", category=["category1", "category3"]),
[self.obj1, self.obj2])
self.assertEquals(self._manager("get_by_tag", category=["category1", "category2"]),
[self.obj2])
self.assertEquals(self._manager("get_by_tag", category=["category5", "category4"]), [])