Merge with develop and fix merge conflicts
This commit is contained in:
commit
72f4fedcbe
148 changed files with 20005 additions and 2718 deletions
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
59
evennia/typeclasses/tests.py
Normal file
59
evennia/typeclasses/tests.py
Normal 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"]), [])
|
||||
Loading…
Add table
Add a link
Reference in a new issue