Refactored the typeclass base, moved Attributes/Tags into separate modules and the django-proxy patch to its on module too. Lots of other cleanups.
This commit is contained in:
parent
24764743ff
commit
302f5bdd81
11 changed files with 1018 additions and 984 deletions
|
|
@ -8,7 +8,7 @@ src.comms.channelmanager.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from src.comms.models import *
|
#from src.comms.models import *
|
||||||
|
|
||||||
msgmanager = Msg.objects
|
#msgmanager = Msg.objects
|
||||||
channelmanager = ChannelDB.objects
|
#channelmanager = ChannelDB.objects
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ Also, the initiated object manager is available as src.help.manager.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from src.help.models import *
|
#from src.help.models import *
|
||||||
|
#
|
||||||
manager = HelpEntry.objects
|
#manager = HelpEntry.objects
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ they control by simply linking to a new object's user property.
|
||||||
import traceback
|
import traceback
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from src.typeclasses.models import TypeclassBase, NickHandler
|
from src.typeclasses.models import TypeclassBase
|
||||||
|
from src.typeclasses.attributes import NickHandler
|
||||||
from src.objects.manager import ObjectManager
|
from src.objects.manager import ObjectManager
|
||||||
from src.objects.models import ObjectDB
|
from src.objects.models import ObjectDB
|
||||||
from src.scripts.scripthandler import ScriptHandler
|
from src.scripts.scripthandler import ScriptHandler
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@ Also, the initiated object manager is available as src.players.manager.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#from src.players.player import *
|
#from src.players.player import *
|
||||||
from src.players.models import PlayerDB
|
#from src.players.models import PlayerDB
|
||||||
|
#
|
||||||
manager = PlayerDB.objects
|
#manager = PlayerDB.objects
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ from django.utils.encoding import smart_str
|
||||||
|
|
||||||
from src.players.manager import PlayerDBManager
|
from src.players.manager import PlayerDBManager
|
||||||
from src.scripts.models import ScriptDB
|
from src.scripts.models import ScriptDB
|
||||||
from src.typeclasses.models import (TypedObject, NickHandler)
|
from src.typeclasses.models import TypedObject
|
||||||
|
from src.typeclasses.attributes import NickHandler
|
||||||
from src.scripts.scripthandler import ScriptHandler
|
from src.scripts.scripthandler import ScriptHandler
|
||||||
from src.commands.cmdsethandler import CmdSetHandler
|
from src.commands.cmdsethandler import CmdSetHandler
|
||||||
from src.commands import cmdhandler
|
from src.commands import cmdhandler
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,6 @@ Also, the initiated object manager is available as src.scripts.manager.
|
||||||
# Note - we MUST NOT import src.scripts.scripts here, or
|
# Note - we MUST NOT import src.scripts.scripts here, or
|
||||||
# proxy models will fall under Django migrations.
|
# proxy models will fall under Django migrations.
|
||||||
#from src.scripts.scripts import *
|
#from src.scripts.scripts import *
|
||||||
from src.scripts.models import ScriptDB
|
#from src.scripts.models import ScriptDB
|
||||||
|
|
||||||
manager = ScriptDB.objects
|
#manager = ScriptDB.objects
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,6 @@ def create_objects():
|
||||||
|
|
||||||
god_character.save()
|
god_character.save()
|
||||||
god_player.attributes.add("_first_login", True)
|
god_player.attributes.add("_first_login", True)
|
||||||
print god_character
|
|
||||||
god_player.attributes.add("_last_puppet", god_character)
|
god_player.attributes.add("_last_puppet", god_character)
|
||||||
god_player.db._playable_characters.append(god_character)
|
god_player.db._playable_characters.append(god_character)
|
||||||
|
|
||||||
|
|
|
||||||
515
src/typeclasses/attributes.py
Normal file
515
src/typeclasses/attributes.py
Normal file
|
|
@ -0,0 +1,515 @@
|
||||||
|
"""
|
||||||
|
Attributes are arbitrary data stored on objects. Attributes supports
|
||||||
|
both pure-string values and pickled arbitrary data.
|
||||||
|
|
||||||
|
Attributes are also used to implement Nicks. This module also contains
|
||||||
|
the Attribute- and NickHandlers as well as the NAttributeHandler,
|
||||||
|
which is a non-db version of Attributes.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import weakref
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.encoding import smart_str
|
||||||
|
|
||||||
|
from src.locks.lockhandler import LockHandler
|
||||||
|
from src.utils.idmapper.models import SharedMemoryModel
|
||||||
|
from src.utils.dbserialize import to_pickle, from_pickle
|
||||||
|
from src.utils.picklefield import PickledObjectField
|
||||||
|
from src.utils.utils import lazy_property, to_str, make_iter
|
||||||
|
|
||||||
|
_TYPECLASS_AGGRESSIVE_CACHE = settings.TYPECLASS_AGGRESSIVE_CACHE
|
||||||
|
|
||||||
|
#------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Attributes
|
||||||
|
#
|
||||||
|
#------------------------------------------------------------
|
||||||
|
|
||||||
|
class Attribute(SharedMemoryModel):
|
||||||
|
"""
|
||||||
|
Attributes are things that are specific to different types of objects. For
|
||||||
|
example, a drink container needs to store its fill level, whereas an exit
|
||||||
|
needs to store its open/closed/locked/unlocked state. These are done via
|
||||||
|
attributes, rather than making different classes for each object type and
|
||||||
|
storing them directly. The added benefit is that we can add/remove
|
||||||
|
attributes on the fly as we like.
|
||||||
|
The Attribute class defines the following properties:
|
||||||
|
key - primary identifier
|
||||||
|
lock_storage - perm strings
|
||||||
|
obj - which object the attribute is defined on
|
||||||
|
date_created - when the attribute was created.
|
||||||
|
value - the data stored in the attribute, in pickled form
|
||||||
|
using wrappers to be able to store/retrieve models.
|
||||||
|
strvalue - string-only data. This data is not pickled and is
|
||||||
|
thus faster to search for in the database.
|
||||||
|
category - optional character string for grouping the Attribute
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
#
|
||||||
|
# Attribute Database Model setup
|
||||||
|
#
|
||||||
|
# These database fields are all set using their corresponding properties,
|
||||||
|
# named same as the field, but withtout the db_* prefix.
|
||||||
|
db_key = models.CharField('key', max_length=255, db_index=True)
|
||||||
|
db_value = PickledObjectField(
|
||||||
|
'value', null=True,
|
||||||
|
help_text="The data returned when the attribute is accessed. Must be "
|
||||||
|
"written as a Python literal if editing through the admin "
|
||||||
|
"interface. Attribute values which are not Python literals "
|
||||||
|
"cannot be edited through the admin interface.")
|
||||||
|
db_strvalue = models.TextField(
|
||||||
|
'strvalue', null=True, blank=True,
|
||||||
|
help_text="String-specific storage for quick look-up")
|
||||||
|
db_category = models.CharField(
|
||||||
|
'category', max_length=128, db_index=True, blank=True, null=True,
|
||||||
|
help_text="Optional categorization of attribute.")
|
||||||
|
# Lock storage
|
||||||
|
db_lock_storage = models.TextField(
|
||||||
|
'locks', blank=True,
|
||||||
|
help_text="Lockstrings for this object are stored here.")
|
||||||
|
db_model = models.CharField(
|
||||||
|
'model', max_length=32, db_index=True, blank=True, null=True,
|
||||||
|
help_text="Which model of object this attribute is attached to (A "
|
||||||
|
"natural key like objects.dbobject). You should not change "
|
||||||
|
"this value unless you know what you are doing.")
|
||||||
|
# subclass of Attribute (None or nick)
|
||||||
|
db_attrtype = models.CharField(
|
||||||
|
'attrtype', max_length=16, db_index=True, blank=True, null=True,
|
||||||
|
help_text="Subclass of Attribute (None or nick)")
|
||||||
|
# time stamp
|
||||||
|
db_date_created = models.DateTimeField(
|
||||||
|
'date_created', editable=False, auto_now_add=True)
|
||||||
|
|
||||||
|
# Database manager
|
||||||
|
#objects = managers.AttributeManager()
|
||||||
|
|
||||||
|
@lazy_property
|
||||||
|
def locks(self):
|
||||||
|
return LockHandler(self)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"Define Django meta options"
|
||||||
|
verbose_name = "Evennia Attribute"
|
||||||
|
|
||||||
|
# read-only wrappers
|
||||||
|
key = property(lambda self: self.db_key)
|
||||||
|
strvalue = property(lambda self: self.db_strvalue)
|
||||||
|
category = property(lambda self: self.db_category)
|
||||||
|
model = property(lambda self: self.db_model)
|
||||||
|
attrtype = property(lambda self: self.db_attrtype)
|
||||||
|
date_created = property(lambda self: self.db_date_created)
|
||||||
|
|
||||||
|
def __lock_storage_get(self):
|
||||||
|
return self.db_lock_storage
|
||||||
|
def __lock_storage_set(self, value):
|
||||||
|
self.db_lock_storage = value
|
||||||
|
self.save(update_fields=["db_lock_storage"])
|
||||||
|
def __lock_storage_del(self):
|
||||||
|
self.db_lock_storage = ""
|
||||||
|
self.save(update_fields=["db_lock_storage"])
|
||||||
|
lock_storage = property(__lock_storage_get, __lock_storage_set, __lock_storage_del)
|
||||||
|
|
||||||
|
# Wrapper properties to easily set database fields. These are
|
||||||
|
# @property decorators that allows to access these fields using
|
||||||
|
# normal python operations (without having to remember to save()
|
||||||
|
# etc). So e.g. a property 'attr' has a get/set/del decorator
|
||||||
|
# defined that allows the user to do self.attr = value,
|
||||||
|
# value = self.attr and del self.attr respectively (where self
|
||||||
|
# is the object in question).
|
||||||
|
|
||||||
|
# value property (wraps db_value)
|
||||||
|
#@property
|
||||||
|
def __value_get(self):
|
||||||
|
"""
|
||||||
|
Getter. Allows for value = self.value.
|
||||||
|
We cannot cache here since it makes certain cases (such
|
||||||
|
as storing a dbobj which is then deleted elsewhere) out-of-sync.
|
||||||
|
The overhead of unpickling seems hard to avoid.
|
||||||
|
"""
|
||||||
|
return from_pickle(self.db_value, db_obj=self)
|
||||||
|
|
||||||
|
#@value.setter
|
||||||
|
def __value_set(self, new_value):
|
||||||
|
"""
|
||||||
|
Setter. Allows for self.value = value. We cannot cache here,
|
||||||
|
see self.__value_get.
|
||||||
|
"""
|
||||||
|
self.db_value = to_pickle(new_value)
|
||||||
|
self.save(update_fields=["db_value"])
|
||||||
|
|
||||||
|
#@value.deleter
|
||||||
|
def __value_del(self):
|
||||||
|
"Deleter. Allows for del attr.value. This removes the entire attribute."
|
||||||
|
self.delete()
|
||||||
|
value = property(__value_get, __value_set, __value_del)
|
||||||
|
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Attribute methods
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return smart_str("%s(%s)" % (self.db_key, self.id))
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"%s(%s)" % (self.db_key,self.id)
|
||||||
|
|
||||||
|
def access(self, accessing_obj, access_type='read', default=False, **kwargs):
|
||||||
|
"""
|
||||||
|
Determines if another object has permission to access.
|
||||||
|
accessing_obj - object trying to access this one
|
||||||
|
access_type - type of access sought
|
||||||
|
default - what to return if no lock of access_type was found
|
||||||
|
**kwargs - passed to at_access hook along with result.
|
||||||
|
"""
|
||||||
|
result = self.locks.check(accessing_obj, access_type=access_type, default=default)
|
||||||
|
#self.at_access(result, **kwargs)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Handlers making use of the Attribute model
|
||||||
|
#
|
||||||
|
|
||||||
|
class AttributeHandler(object):
|
||||||
|
"""
|
||||||
|
Handler for adding Attributes to the object.
|
||||||
|
"""
|
||||||
|
_m2m_fieldname = "db_attributes"
|
||||||
|
_attrcreate = "attrcreate"
|
||||||
|
_attredit = "attredit"
|
||||||
|
_attrread = "attrread"
|
||||||
|
_attrtype = None
|
||||||
|
|
||||||
|
def __init__(self, obj):
|
||||||
|
"Initialize handler"
|
||||||
|
self.obj = obj
|
||||||
|
self._objid = obj.id
|
||||||
|
self._model = to_str(obj.__dbclass__.__name__.lower())
|
||||||
|
self._cache = None
|
||||||
|
|
||||||
|
def _recache(self):
|
||||||
|
"Cache all attributes of this object"
|
||||||
|
query = {"%s__id" % self._model : self._objid,
|
||||||
|
"attribute__db_attrtype" : self._attrtype}
|
||||||
|
attrs = [conn.attribute for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)]
|
||||||
|
self._cache = dict(("%s-%s" % (to_str(attr.db_key).lower(),
|
||||||
|
attr.db_category.lower() if conn.attribute.db_category else None),
|
||||||
|
attr) for attr in attrs)
|
||||||
|
|
||||||
|
def has(self, key, category=None):
|
||||||
|
"""
|
||||||
|
Checks if the given Attribute (or list of Attributes) exists on
|
||||||
|
the object.
|
||||||
|
|
||||||
|
If an iterable is given, returns list of booleans.
|
||||||
|
"""
|
||||||
|
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
|
||||||
|
self._recache()
|
||||||
|
key = [k.strip().lower() for k in make_iter(key) if k]
|
||||||
|
category = category.strip().lower() if category is not None else None
|
||||||
|
searchkeys = ["%s-%s" % (k, category) for k in make_iter(key)]
|
||||||
|
ret = [self._cache.get(skey) for skey in searchkeys if skey in self._cache]
|
||||||
|
return ret[0] if len(ret) == 1 else ret
|
||||||
|
|
||||||
|
def get(self, key=None, category=None, default=None, return_obj=False,
|
||||||
|
strattr=False, raise_exception=False, accessing_obj=None,
|
||||||
|
default_access=True, not_found_none=False):
|
||||||
|
"""
|
||||||
|
Returns the value of the given Attribute or list of Attributes.
|
||||||
|
strattr will cause the string-only value field instead of the normal
|
||||||
|
pickled field data. Use to get back values from Attributes added with
|
||||||
|
the strattr keyword.
|
||||||
|
If return_obj=True, return the matching Attribute object
|
||||||
|
instead. Returns default if no matches (or [ ] if key was a list
|
||||||
|
with no matches). If raise_exception=True, failure to find a
|
||||||
|
match will raise AttributeError instead.
|
||||||
|
|
||||||
|
If accessing_obj is given, its "attrread" permission lock will be
|
||||||
|
checked before displaying each looked-after Attribute. If no
|
||||||
|
accessing_obj is given, no check will be done.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class RetDefault(object):
|
||||||
|
"Holds default values"
|
||||||
|
def __init__(self):
|
||||||
|
self.value = default
|
||||||
|
self.strvalue = str(default) if default is not None else None
|
||||||
|
|
||||||
|
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
|
||||||
|
self._recache()
|
||||||
|
ret = []
|
||||||
|
key = [k.strip().lower() for k in make_iter(key) if k]
|
||||||
|
category = category.strip().lower() if category is not None else None
|
||||||
|
#print "cache:", self._cache.keys(), key
|
||||||
|
if not key:
|
||||||
|
# return all with matching category (or no category)
|
||||||
|
catkey = "-%s" % category if category is not None else None
|
||||||
|
ret = [attr for key, attr in self._cache.items() if key and key.endswith(catkey)]
|
||||||
|
else:
|
||||||
|
for searchkey in ("%s-%s" % (k, category) for k in key):
|
||||||
|
attr_obj = self._cache.get(searchkey)
|
||||||
|
if attr_obj:
|
||||||
|
ret.append(attr_obj)
|
||||||
|
else:
|
||||||
|
if raise_exception:
|
||||||
|
raise AttributeError
|
||||||
|
else:
|
||||||
|
ret.append(RetDefault())
|
||||||
|
if accessing_obj:
|
||||||
|
# check 'attrread' locks
|
||||||
|
ret = [attr for attr in ret if attr.access(accessing_obj, self._attrread, default=default_access)]
|
||||||
|
if strattr:
|
||||||
|
ret = ret if return_obj else [attr.strvalue for attr in ret if attr]
|
||||||
|
else:
|
||||||
|
ret = ret if return_obj else [attr.value for attr in ret if attr]
|
||||||
|
if not ret:
|
||||||
|
return ret if len(key) > 1 else default
|
||||||
|
return ret[0] if len(ret)==1 else ret
|
||||||
|
|
||||||
|
|
||||||
|
def add(self, key, value, category=None, lockstring="",
|
||||||
|
strattr=False, accessing_obj=None, default_access=True):
|
||||||
|
"""
|
||||||
|
Add attribute to object, with optional lockstring.
|
||||||
|
|
||||||
|
If strattr is set, the db_strvalue field will be used (no pickling).
|
||||||
|
Use the get() method with the strattr keyword to get it back.
|
||||||
|
|
||||||
|
If accessing_obj is given, self.obj's 'attrcreate' lock access
|
||||||
|
will be checked against it. If no accessing_obj is given, no check
|
||||||
|
will be done.
|
||||||
|
"""
|
||||||
|
if accessing_obj and not self.obj.access(accessing_obj,
|
||||||
|
self._attrcreate, default=default_access):
|
||||||
|
# check create access
|
||||||
|
return
|
||||||
|
if self._cache is None:
|
||||||
|
self._recache()
|
||||||
|
if not key:
|
||||||
|
return
|
||||||
|
|
||||||
|
category = category.strip().lower() if category is not None else None
|
||||||
|
keystr = key.strip().lower()
|
||||||
|
cachekey = "%s-%s" % (keystr, category)
|
||||||
|
attr_obj = self._cache.get(cachekey)
|
||||||
|
|
||||||
|
if attr_obj:
|
||||||
|
# update an existing attribute object
|
||||||
|
if strattr:
|
||||||
|
# store as a simple string (will not notify OOB handlers)
|
||||||
|
attr_obj.db_strvalue = value
|
||||||
|
attr_obj.save(update_fields=["db_strvalue"])
|
||||||
|
else:
|
||||||
|
# store normally (this will also notify OOB handlers)
|
||||||
|
attr_obj.value = value
|
||||||
|
else:
|
||||||
|
# create a new Attribute (no OOB handlers can be notified)
|
||||||
|
kwargs = {"db_key" : keystr, "db_category" : category,
|
||||||
|
"db_model" : self._model, "db_attrtype" : self._attrtype,
|
||||||
|
"db_value" : None if strattr else to_pickle(value),
|
||||||
|
"db_strvalue" : value if strattr else None}
|
||||||
|
new_attr = Attribute(**kwargs)
|
||||||
|
new_attr.save()
|
||||||
|
getattr(self.obj, self._m2m_fieldname).add(new_attr)
|
||||||
|
self._cache[cachekey] = new_attr
|
||||||
|
|
||||||
|
|
||||||
|
def batch_add(self, key, value, category=None, lockstring="",
|
||||||
|
strattr=False, accessing_obj=None, default_access=True):
|
||||||
|
"""
|
||||||
|
Batch-version of add(). This is more efficient than
|
||||||
|
repeat-calling add.
|
||||||
|
|
||||||
|
key and value must be sequences of the same length, each
|
||||||
|
representing a key-value pair.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if accessing_obj and not self.obj.access(accessing_obj,
|
||||||
|
self._attrcreate, default=default_access):
|
||||||
|
# check create access
|
||||||
|
return
|
||||||
|
if self._cache is None:
|
||||||
|
self._recache()
|
||||||
|
if not key:
|
||||||
|
return
|
||||||
|
|
||||||
|
keys, values= make_iter(key), make_iter(value)
|
||||||
|
|
||||||
|
if len(keys) != len(values):
|
||||||
|
raise RuntimeError("AttributeHandler.add(): key and value of different length: %s vs %s" % key, value)
|
||||||
|
category = category.strip().lower() if category is not None else None
|
||||||
|
new_attrobjs = []
|
||||||
|
for ikey, keystr in enumerate(keys):
|
||||||
|
keystr = keystr.strip().lower()
|
||||||
|
new_value = values[ikey]
|
||||||
|
cachekey = "%s-%s" % (keystr, category)
|
||||||
|
attr_obj = self._cache.get(cachekey)
|
||||||
|
|
||||||
|
if attr_obj:
|
||||||
|
# update an existing attribute object
|
||||||
|
if strattr:
|
||||||
|
# store as a simple string (will not notify OOB handlers)
|
||||||
|
attr_obj.db_strvalue = new_value
|
||||||
|
attr_obj.save(update_fields=["db_strvalue"])
|
||||||
|
else:
|
||||||
|
# store normally (this will also notify OOB handlers)
|
||||||
|
attr_obj.value = new_value
|
||||||
|
else:
|
||||||
|
# create a new Attribute (no OOB handlers can be notified)
|
||||||
|
kwargs = {"db_key" : keystr, "db_category" : category,
|
||||||
|
"db_attrtype" : self._attrtype,
|
||||||
|
"db_value" : None if strattr else to_pickle(new_value),
|
||||||
|
"db_strvalue" : value if strattr else None}
|
||||||
|
new_attr = Attribute(**kwargs)
|
||||||
|
new_attr.save()
|
||||||
|
new_attrobjs.append(new_attr)
|
||||||
|
if new_attrobjs:
|
||||||
|
# Add new objects to m2m field all at once
|
||||||
|
getattr(self.obj, self._m2m_fieldname).add(*new_attrobjs)
|
||||||
|
self._recache()
|
||||||
|
|
||||||
|
|
||||||
|
def remove(self, key, raise_exception=False, category=None,
|
||||||
|
accessing_obj=None, default_access=True):
|
||||||
|
"""Remove attribute or a list of attributes from object.
|
||||||
|
|
||||||
|
If accessing_obj is given, will check against the 'attredit' lock.
|
||||||
|
If not given, this check is skipped.
|
||||||
|
"""
|
||||||
|
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
|
||||||
|
self._recache()
|
||||||
|
key = [k.strip().lower() for k in make_iter(key) if k]
|
||||||
|
category = category.strip().lower() if category is not None else None
|
||||||
|
for searchstr in ("%s-%s" % (k, category) for k in key):
|
||||||
|
attr_obj = self._cache.get(searchstr)
|
||||||
|
if attr_obj:
|
||||||
|
if not (accessing_obj and not attr_obj.access(accessing_obj,
|
||||||
|
self._attredit, default=default_access)):
|
||||||
|
attr_obj.delete()
|
||||||
|
elif not attr_obj and raise_exception:
|
||||||
|
raise AttributeError
|
||||||
|
self._recache()
|
||||||
|
|
||||||
|
def clear(self, category=None, accessing_obj=None, default_access=True):
|
||||||
|
"""
|
||||||
|
Remove all Attributes on this object. If accessing_obj is
|
||||||
|
given, check the 'attredit' lock on each Attribute before
|
||||||
|
continuing. If not given, skip check.
|
||||||
|
"""
|
||||||
|
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
|
||||||
|
self._recache()
|
||||||
|
if accessing_obj:
|
||||||
|
[attr.delete() for attr in self._cache.values()
|
||||||
|
if attr.access(accessing_obj, self._attredit, default=default_access)]
|
||||||
|
else:
|
||||||
|
[attr.delete() for attr in self._cache.values()]
|
||||||
|
self._recache()
|
||||||
|
|
||||||
|
def all(self, accessing_obj=None, default_access=True):
|
||||||
|
"""
|
||||||
|
Return all Attribute objects on this object.
|
||||||
|
|
||||||
|
If accessing_obj is given, check the "attrread" lock on
|
||||||
|
each attribute before returning them. If not given, this
|
||||||
|
check is skipped.
|
||||||
|
"""
|
||||||
|
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
|
||||||
|
self._recache()
|
||||||
|
attrs = sorted(self._cache.values(), key=lambda o: o.id)
|
||||||
|
if accessing_obj:
|
||||||
|
return [attr for attr in attrs
|
||||||
|
if attr.access(accessing_obj, self._attredit, default=default_access)]
|
||||||
|
else:
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class NickHandler(AttributeHandler):
|
||||||
|
"""
|
||||||
|
Handles the addition and removal of Nicks
|
||||||
|
(uses Attributes' strvalue and category fields)
|
||||||
|
|
||||||
|
Nicks are stored as Attributes
|
||||||
|
with categories nick_<nicktype>
|
||||||
|
"""
|
||||||
|
_attrtype = "nick"
|
||||||
|
|
||||||
|
def has(self, key, category="inputline"):
|
||||||
|
return super(NickHandler, self).has(key, category=category)
|
||||||
|
|
||||||
|
def get(self, key=None, category="inputline", **kwargs):
|
||||||
|
"Get the replacement value matching the given key and category"
|
||||||
|
return super(NickHandler, self).get(key=key, category=category, strattr=True, **kwargs)
|
||||||
|
|
||||||
|
def add(self, key, replacement, category="inputline", **kwargs):
|
||||||
|
"Add a new nick"
|
||||||
|
super(NickHandler, self).add(key, replacement, category=category, strattr=True, **kwargs)
|
||||||
|
|
||||||
|
def remove(self, key, category="inputline", **kwargs):
|
||||||
|
"Remove Nick with matching category"
|
||||||
|
super(NickHandler, self).remove(key, category=category, **kwargs)
|
||||||
|
|
||||||
|
def nickreplace(self, raw_string, categories=("inputline", "channel"), include_player=True):
|
||||||
|
"Replace entries in raw_string with nick replacement"
|
||||||
|
raw_string
|
||||||
|
obj_nicks, player_nicks = [], []
|
||||||
|
for category in make_iter(categories):
|
||||||
|
obj_nicks.extend([n for n in make_iter(self.get(category=category, return_obj=True)) if n])
|
||||||
|
if include_player and self.obj.has_player:
|
||||||
|
for category in make_iter(categories):
|
||||||
|
player_nicks.extend([n for n in make_iter(self.obj.player.nicks.get(category=category, return_obj=True)) if n])
|
||||||
|
for nick in obj_nicks + player_nicks:
|
||||||
|
# make a case-insensitive match here
|
||||||
|
match = re.match(re.escape(nick.db_key), raw_string, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
raw_string = raw_string.replace(match.group(), nick.db_strvalue, 1)
|
||||||
|
break
|
||||||
|
return raw_string
|
||||||
|
|
||||||
|
|
||||||
|
class NAttributeHandler(object):
|
||||||
|
"""
|
||||||
|
This stand-alone handler manages non-database saving.
|
||||||
|
It is similar to AttributeHandler and is used
|
||||||
|
by the .ndb handler in the same way as .db does
|
||||||
|
for the AttributeHandler.
|
||||||
|
"""
|
||||||
|
def __init__(self, obj):
|
||||||
|
"initialized on the object"
|
||||||
|
self._store = {}
|
||||||
|
self.obj = weakref.proxy(obj)
|
||||||
|
|
||||||
|
def has(self, key):
|
||||||
|
"Check if object has this attribute or not"
|
||||||
|
return key in self._store
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
"Returns named key value"
|
||||||
|
return self._store.get(key, None)
|
||||||
|
|
||||||
|
def add(self, key, value):
|
||||||
|
"Add new key and value"
|
||||||
|
self._store[key] = value
|
||||||
|
self.obj.set_recache_protection()
|
||||||
|
|
||||||
|
def remove(self, key):
|
||||||
|
"Remove key from storage"
|
||||||
|
if key in self._store:
|
||||||
|
del self._store[key]
|
||||||
|
self.obj.set_recache_protection(self._store)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"Remove all nattributes from handler"
|
||||||
|
self._store = {}
|
||||||
|
|
||||||
|
def all(self, return_tuples=False):
|
||||||
|
"List all keys or (keys, values) stored, except _keys"
|
||||||
|
if return_tuples:
|
||||||
|
return [(key, value) for (key, value) in self._store.items() if not key.startswith("_")]
|
||||||
|
return [key for key in self._store if not key.startswith("_")]
|
||||||
257
src/typeclasses/django_new_patch.py
Normal file
257
src/typeclasses/django_new_patch.py
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
"""
|
||||||
|
This is a patch of django.db.models.base.py:__new__, to allow for the
|
||||||
|
proxy system to allow multiple inheritance when both parents are of
|
||||||
|
the same base model.
|
||||||
|
|
||||||
|
This patch is implemented as per
|
||||||
|
https://code.djangoproject.com/ticket/11560 and will hopefully be
|
||||||
|
possibe to remove as it gets adde to django's main branch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# django patch imports
|
||||||
|
import sys
|
||||||
|
import copy
|
||||||
|
import warnings
|
||||||
|
from django.apps import apps
|
||||||
|
from django.db.models.base import ModelBase
|
||||||
|
from django.db.models.base import subclass_exception
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db.models.options import Options
|
||||||
|
from django.utils.deprecation import RemovedInDjango19Warning
|
||||||
|
from django.core.exceptions import MultipleObjectsReturned, FieldError
|
||||||
|
from django.apps.config import MODELS_MODULE_NAME
|
||||||
|
from django.db.models.fields.related import OneToOneField
|
||||||
|
#/ django patch imports
|
||||||
|
|
||||||
|
def patched_new(cls, name, bases, attrs):
|
||||||
|
"Patched version of __new__"
|
||||||
|
|
||||||
|
super_new = super(ModelBase, cls).__new__
|
||||||
|
|
||||||
|
# Also ensure initialization is only performed for subclasses of Model
|
||||||
|
# (excluding Model class itself).
|
||||||
|
parents = [b for b in bases if isinstance(b, ModelBase)]
|
||||||
|
if not parents:
|
||||||
|
return super_new(cls, name, bases, attrs)
|
||||||
|
|
||||||
|
# Create the class.
|
||||||
|
module = attrs.pop('__module__')
|
||||||
|
new_class = super_new(cls, name, bases, {'__module__': module})
|
||||||
|
attr_meta = attrs.pop('Meta', None)
|
||||||
|
abstract = getattr(attr_meta, 'abstract', False)
|
||||||
|
if not attr_meta:
|
||||||
|
meta = getattr(new_class, 'Meta', None)
|
||||||
|
else:
|
||||||
|
meta = attr_meta
|
||||||
|
base_meta = getattr(new_class, '_meta', None)
|
||||||
|
|
||||||
|
# Look for an application configuration to attach the model to.
|
||||||
|
app_config = apps.get_containing_app_config(module)
|
||||||
|
|
||||||
|
if getattr(meta, 'app_label', None) is None:
|
||||||
|
|
||||||
|
if app_config is None:
|
||||||
|
# If the model is imported before the configuration for its
|
||||||
|
# application is created (#21719), or isn't in an installed
|
||||||
|
# application (#21680), use the legacy logic to figure out the
|
||||||
|
# app_label by looking one level up from the package or module
|
||||||
|
# named 'models'. If no such package or module exists, fall
|
||||||
|
# back to looking one level up from the module this model is
|
||||||
|
# defined in.
|
||||||
|
|
||||||
|
# For 'django.contrib.sites.models', this would be 'sites'.
|
||||||
|
# For 'geo.models.places' this would be 'geo'.
|
||||||
|
|
||||||
|
msg = (
|
||||||
|
"Model class %s.%s doesn't declare an explicit app_label "
|
||||||
|
"and either isn't in an application in INSTALLED_APPS or "
|
||||||
|
"else was imported before its application was loaded. " %
|
||||||
|
(module, name))
|
||||||
|
if abstract:
|
||||||
|
msg += "Its app_label will be set to None in Django 1.9."
|
||||||
|
else:
|
||||||
|
msg += "This will no longer be supported in Django 1.9."
|
||||||
|
warnings.warn(msg, RemovedInDjango19Warning, stacklevel=2)
|
||||||
|
|
||||||
|
model_module = sys.modules[new_class.__module__]
|
||||||
|
package_components = model_module.__name__.split('.')
|
||||||
|
package_components.reverse() # find the last occurrence of 'models'
|
||||||
|
try:
|
||||||
|
app_label_index = package_components.index(MODELS_MODULE_NAME) + 1
|
||||||
|
except ValueError:
|
||||||
|
app_label_index = 1
|
||||||
|
kwargs = {"app_label": package_components[app_label_index]}
|
||||||
|
|
||||||
|
else:
|
||||||
|
kwargs = {"app_label": app_config.label}
|
||||||
|
|
||||||
|
else:
|
||||||
|
kwargs = {}
|
||||||
|
|
||||||
|
new_class.add_to_class('_meta', Options(meta, **kwargs))
|
||||||
|
if not abstract:
|
||||||
|
new_class.add_to_class(
|
||||||
|
'DoesNotExist',
|
||||||
|
subclass_exception(
|
||||||
|
str('DoesNotExist'),
|
||||||
|
tuple(x.DoesNotExist for x in parents if hasattr(x, '_meta') and not x._meta.abstract) or (ObjectDoesNotExist,),
|
||||||
|
module,
|
||||||
|
attached_to=new_class))
|
||||||
|
new_class.add_to_class(
|
||||||
|
'MultipleObjectsReturned',
|
||||||
|
subclass_exception(
|
||||||
|
str('MultipleObjectsReturned'),
|
||||||
|
tuple(x.MultipleObjectsReturned for x in parents if hasattr(x, '_meta') and not x._meta.abstract) or (MultipleObjectsReturned,),
|
||||||
|
module,
|
||||||
|
attached_to=new_class))
|
||||||
|
if base_meta and not base_meta.abstract:
|
||||||
|
# Non-abstract child classes inherit some attributes from their
|
||||||
|
# non-abstract parent (unless an ABC comes before it in the
|
||||||
|
# method resolution order).
|
||||||
|
if not hasattr(meta, 'ordering'):
|
||||||
|
new_class._meta.ordering = base_meta.ordering
|
||||||
|
if not hasattr(meta, 'get_latest_by'):
|
||||||
|
new_class._meta.get_latest_by = base_meta.get_latest_by
|
||||||
|
|
||||||
|
is_proxy = new_class._meta.proxy
|
||||||
|
|
||||||
|
# If the model is a proxy, ensure that the base class
|
||||||
|
# hasn't been swapped out.
|
||||||
|
if is_proxy and base_meta and base_meta.swapped:
|
||||||
|
raise TypeError("%s cannot proxy the swapped model '%s'." % (name, base_meta.swapped))
|
||||||
|
|
||||||
|
if getattr(new_class, '_default_manager', None):
|
||||||
|
if not is_proxy:
|
||||||
|
# Multi-table inheritance doesn't inherit default manager from
|
||||||
|
# parents.
|
||||||
|
new_class._default_manager = None
|
||||||
|
new_class._base_manager = None
|
||||||
|
else:
|
||||||
|
# Proxy classes do inherit parent's default manager, if none is
|
||||||
|
# set explicitly.
|
||||||
|
new_class._default_manager = new_class._default_manager._copy_to_model(new_class)
|
||||||
|
new_class._base_manager = new_class._base_manager._copy_to_model(new_class)
|
||||||
|
|
||||||
|
# Add all attributes to the class.
|
||||||
|
for obj_name, obj in attrs.items():
|
||||||
|
new_class.add_to_class(obj_name, obj)
|
||||||
|
|
||||||
|
# All the fields of any type declared on this model
|
||||||
|
new_fields = (
|
||||||
|
new_class._meta.local_fields +
|
||||||
|
new_class._meta.local_many_to_many +
|
||||||
|
new_class._meta.virtual_fields
|
||||||
|
)
|
||||||
|
field_names = set(f.name for f in new_fields)
|
||||||
|
|
||||||
|
# Basic setup for proxy models.
|
||||||
|
if is_proxy:
|
||||||
|
base = None
|
||||||
|
for parent in [kls for kls in parents if hasattr(kls, '_meta')]:
|
||||||
|
if parent._meta.abstract:
|
||||||
|
if parent._meta.fields:
|
||||||
|
raise TypeError("Abstract base class containing model fields not permitted for proxy model '%s'." % name)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
#if base is not None: # patch
|
||||||
|
while parent._meta.proxy: # patch
|
||||||
|
parent = parent._meta.proxy_for_model # patch
|
||||||
|
if base is not None and base is not parent: # patch
|
||||||
|
raise TypeError("Proxy model '%s' has more than one non-abstract model base class." % name)
|
||||||
|
else:
|
||||||
|
base = parent
|
||||||
|
if base is None:
|
||||||
|
raise TypeError("Proxy model '%s' has no non-abstract model base class." % name)
|
||||||
|
new_class._meta.setup_proxy(base)
|
||||||
|
new_class._meta.concrete_model = base._meta.concrete_model
|
||||||
|
else:
|
||||||
|
new_class._meta.concrete_model = new_class
|
||||||
|
|
||||||
|
# Collect the parent links for multi-table inheritance.
|
||||||
|
parent_links = {}
|
||||||
|
for base in reversed([new_class] + parents):
|
||||||
|
# Conceptually equivalent to `if base is Model`.
|
||||||
|
if not hasattr(base, '_meta'):
|
||||||
|
continue
|
||||||
|
# Skip concrete parent classes.
|
||||||
|
if base != new_class and not base._meta.abstract:
|
||||||
|
continue
|
||||||
|
# Locate OneToOneField instances.
|
||||||
|
for field in base._meta.local_fields:
|
||||||
|
if isinstance(field, OneToOneField):
|
||||||
|
parent_links[field.rel.to] = field
|
||||||
|
|
||||||
|
# Do the appropriate setup for any model parents.
|
||||||
|
for base in parents:
|
||||||
|
original_base = base
|
||||||
|
if not hasattr(base, '_meta'):
|
||||||
|
# Things without _meta aren't functional models, so they're
|
||||||
|
# uninteresting parents.
|
||||||
|
continue
|
||||||
|
|
||||||
|
parent_fields = base._meta.local_fields + base._meta.local_many_to_many
|
||||||
|
# Check for clashes between locally declared fields and those
|
||||||
|
# on the base classes (we cannot handle shadowed fields at the
|
||||||
|
# moment).
|
||||||
|
for field in parent_fields:
|
||||||
|
if field.name in field_names:
|
||||||
|
raise FieldError(
|
||||||
|
'Local field %r in class %r clashes '
|
||||||
|
'with field of similar name from '
|
||||||
|
'base class %r' % (field.name, name, base.__name__)
|
||||||
|
)
|
||||||
|
if not base._meta.abstract:
|
||||||
|
# Concrete classes...
|
||||||
|
base = base._meta.concrete_model
|
||||||
|
if base in parent_links:
|
||||||
|
field = parent_links[base]
|
||||||
|
elif not is_proxy:
|
||||||
|
attr_name = '%s_ptr' % base._meta.model_name
|
||||||
|
field = OneToOneField(base, name=attr_name,
|
||||||
|
auto_created=True, parent_link=True)
|
||||||
|
# Only add the ptr field if it's not already present;
|
||||||
|
# e.g. migrations will already have it specified
|
||||||
|
if not hasattr(new_class, attr_name):
|
||||||
|
new_class.add_to_class(attr_name, field)
|
||||||
|
else:
|
||||||
|
field = None
|
||||||
|
new_class._meta.parents[base] = field
|
||||||
|
else:
|
||||||
|
# .. and abstract ones.
|
||||||
|
for field in parent_fields:
|
||||||
|
new_class.add_to_class(field.name, copy.deepcopy(field))
|
||||||
|
|
||||||
|
# Pass any non-abstract parent classes onto child.
|
||||||
|
new_class._meta.parents.update(base._meta.parents)
|
||||||
|
|
||||||
|
# Inherit managers from the abstract base classes.
|
||||||
|
new_class.copy_managers(base._meta.abstract_managers)
|
||||||
|
|
||||||
|
# Proxy models inherit the non-abstract managers from their base,
|
||||||
|
# unless they have redefined any of them.
|
||||||
|
if is_proxy:
|
||||||
|
new_class.copy_managers(original_base._meta.concrete_managers)
|
||||||
|
|
||||||
|
# Inherit virtual fields (like GenericForeignKey) from the parent
|
||||||
|
# class
|
||||||
|
for field in base._meta.virtual_fields:
|
||||||
|
if base._meta.abstract and field.name in field_names:
|
||||||
|
raise FieldError(
|
||||||
|
'Local field %r in class %r clashes '
|
||||||
|
'with field of similar name from '
|
||||||
|
'abstract base class %r' % (field.name, name, base.__name__)
|
||||||
|
)
|
||||||
|
new_class.add_to_class(field.name, copy.deepcopy(field))
|
||||||
|
|
||||||
|
if abstract:
|
||||||
|
# Abstract base models can't be instantiated and don't appear in
|
||||||
|
# the list of models for an app. We do the final setup for them a
|
||||||
|
# little differently from normal models.
|
||||||
|
attr_meta.abstract = False
|
||||||
|
new_class.Meta = attr_meta
|
||||||
|
return new_class
|
||||||
|
|
||||||
|
new_class._prepare()
|
||||||
|
new_class._meta.apps.register_model(new_class._meta.app_label, new_class)
|
||||||
|
|
||||||
|
return new_class
|
||||||
File diff suppressed because it is too large
Load diff
193
src/typeclasses/tags.py
Normal file
193
src/typeclasses/tags.py
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
"""
|
||||||
|
Tags are entities that are attached to objects like Attributes but
|
||||||
|
which are unique to an individual object - any number of objects
|
||||||
|
can have the same Tag attached to them.
|
||||||
|
|
||||||
|
Tags are used for tagging, obviously, but the data structure
|
||||||
|
is also used for storing Aliases and Permissions. This module
|
||||||
|
contains the respective handlers.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from src.utils.utils import to_str, make_iter
|
||||||
|
|
||||||
|
|
||||||
|
_TYPECLASS_AGGRESSIVE_CACHE = settings.TYPECLASS_AGGRESSIVE_CACHE
|
||||||
|
|
||||||
|
#------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Tags
|
||||||
|
#
|
||||||
|
#------------------------------------------------------------
|
||||||
|
|
||||||
|
class Tag(models.Model):
|
||||||
|
"""
|
||||||
|
Tags are quick markers for objects in-game. An typeobject
|
||||||
|
can have any number of tags, stored via its db_tags property.
|
||||||
|
Tagging similar objects will make it easier to quickly locate the
|
||||||
|
group later (such as when implementing zones). The main advantage
|
||||||
|
of tagging as opposed to using Attributes is speed; a tag is very
|
||||||
|
limited in what data it can hold, and the tag key+category is
|
||||||
|
indexed for efficient lookup in the database. Tags are shared between
|
||||||
|
objects - a new tag is only created if the key+category combination
|
||||||
|
did not previously exist, making them unsuitable for storing
|
||||||
|
object-related data (for this a full Attribute
|
||||||
|
should be used).
|
||||||
|
The 'db_data' field is intended as a documentation
|
||||||
|
field for the tag itself, such as to document what this tag+category
|
||||||
|
stands for and display that in a web interface or similar.
|
||||||
|
|
||||||
|
The main default use for Tags is to implement Aliases for objects.
|
||||||
|
this uses the 'aliases' tag category, which is also checked by the
|
||||||
|
default search functions of Evennia to allow quick searches by alias.
|
||||||
|
"""
|
||||||
|
db_key = models.CharField('key', max_length=255, null=True,
|
||||||
|
help_text="tag identifier", db_index=True)
|
||||||
|
db_category = models.CharField('category', max_length=64, null=True,
|
||||||
|
help_text="tag category", db_index=True)
|
||||||
|
db_data = models.TextField('data', null=True, blank=True,
|
||||||
|
help_text="optional data field with extra information. This is not searched for.")
|
||||||
|
# this is "objectdb" etc. Required behind the scenes
|
||||||
|
db_model = models.CharField('model', max_length=32, null=True, help_text="database model to Tag", db_index=True)
|
||||||
|
# this is None, alias or permission
|
||||||
|
db_tagtype = models.CharField('tagtype', max_length=16, null=True, help_text="overall type of Tag", db_index=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"Define Django meta options"
|
||||||
|
verbose_name = "Tag"
|
||||||
|
unique_together = (('db_key', 'db_category', 'db_tagtype'),)
|
||||||
|
index_together = (('db_key', 'db_category', 'db_tagtype'),)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"%s" % self.db_key
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.db_key)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Handlers making use of the Tags model
|
||||||
|
#
|
||||||
|
|
||||||
|
class TagHandler(object):
|
||||||
|
"""
|
||||||
|
Generic tag-handler. Accessed via TypedObject.tags.
|
||||||
|
"""
|
||||||
|
_m2m_fieldname = "db_tags"
|
||||||
|
_tagtype = None
|
||||||
|
|
||||||
|
def __init__(self, obj):
|
||||||
|
"""
|
||||||
|
Tags are stored internally in the TypedObject.db_tags m2m field
|
||||||
|
with an tag.db_model based on the obj the taghandler is stored on
|
||||||
|
and with a tagtype given by self.handlertype
|
||||||
|
"""
|
||||||
|
self.obj = obj
|
||||||
|
self._objid = obj.id
|
||||||
|
self._model = obj.__dbclass__.__name__.lower()
|
||||||
|
self._cache = None
|
||||||
|
|
||||||
|
def _recache(self):
|
||||||
|
"Cache all tags of this object"
|
||||||
|
query = {"%s__id" % self._model : self._objid,
|
||||||
|
"tag__db_tagtype" : self._tagtype}
|
||||||
|
tagobjs = [conn.tag for conn in getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)]
|
||||||
|
self._cache = dict(("%s-%s" % (to_str(tagobj.db_key).lower(),
|
||||||
|
tagobj.db_category.lower() if tagobj.db_category else None),
|
||||||
|
tagobj) for tagobj in tagobjs)
|
||||||
|
|
||||||
|
def add(self, tag=None, category=None, data=None):
|
||||||
|
"Add a new tag to the handler. Tag is a string or a list of strings."
|
||||||
|
if not tag:
|
||||||
|
return
|
||||||
|
for tagstr in make_iter(tag):
|
||||||
|
if not tagstr:
|
||||||
|
continue
|
||||||
|
tagstr = tagstr.strip().lower()
|
||||||
|
category = category.strip().lower() if category is not None else None
|
||||||
|
data = str(data) if data is not None else None
|
||||||
|
# this will only create tag if no matches existed beforehand (it
|
||||||
|
# will overload data on an existing tag since that is not
|
||||||
|
# considered part of making the tag unique)
|
||||||
|
tagobj = self.obj.__class__.objects.create_tag(key=tagstr, category=category, data=data,
|
||||||
|
tagtype=self._tagtype)
|
||||||
|
getattr(self.obj, self._m2m_fieldname).add(tagobj)
|
||||||
|
if self._cache is None:
|
||||||
|
self._recache()
|
||||||
|
cachestring = "%s-%s" % (tagstr, category)
|
||||||
|
self._cache[cachestring] = tagobj
|
||||||
|
|
||||||
|
def get(self, key, category=None, return_tagobj=False):
|
||||||
|
"""
|
||||||
|
Get the tag for the given key or list of tags. If
|
||||||
|
return_data=True, return the matching Tag objects instead.
|
||||||
|
Returns a single tag if a unique match, otherwise a list
|
||||||
|
"""
|
||||||
|
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
|
||||||
|
self._recache()
|
||||||
|
ret = []
|
||||||
|
category = category.strip().lower() if category is not None else None
|
||||||
|
searchkey = ["%s-%s" % (key.strip().lower(), category) if key is not None else None for key in make_iter(key)]
|
||||||
|
ret = [val for val in (self._cache.get(keystr) for keystr in searchkey) if val]
|
||||||
|
ret = [to_str(tag.db_data) for tag in ret] if return_tagobj else ret
|
||||||
|
return ret[0] if len(ret) == 1 else ret
|
||||||
|
|
||||||
|
def remove(self, key, category=None):
|
||||||
|
"Remove a tag from the handler based ond key and category."
|
||||||
|
for key in make_iter(key):
|
||||||
|
if not (key or key.strip()): # we don't allow empty tags
|
||||||
|
continue
|
||||||
|
tagstr = key.strip().lower()
|
||||||
|
category = category.strip().lower() if category is not None else None
|
||||||
|
|
||||||
|
# This does not delete the tag object itself. Maybe it should do
|
||||||
|
# that when no objects reference the tag anymore (how to check)?
|
||||||
|
tagobj = self.obj.db_tags.filter(db_key=tagstr, db_category=category)
|
||||||
|
if tagobj:
|
||||||
|
getattr(self.obj, self._m2m_fieldname).remove(tagobj[0])
|
||||||
|
self._recache()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"Remove all tags from the handler"
|
||||||
|
getattr(self.obj, self._m2m_fieldname).clear()
|
||||||
|
self._recache()
|
||||||
|
|
||||||
|
def all(self, category=None, return_key_and_category=False):
|
||||||
|
"""
|
||||||
|
Get all tags in this handler.
|
||||||
|
If category is given, return only Tags with this category. If
|
||||||
|
return_keys_and_categories is set, return a list of tuples [(key, category), ...]
|
||||||
|
"""
|
||||||
|
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
|
||||||
|
self._recache()
|
||||||
|
if category:
|
||||||
|
category = category.strip().lower() if category is not None else None
|
||||||
|
matches = [tag for tag in self._cache.values() if tag.db_category == category]
|
||||||
|
else:
|
||||||
|
matches = self._cache.values()
|
||||||
|
|
||||||
|
if matches:
|
||||||
|
matches = sorted(matches, key=lambda o: o.id)
|
||||||
|
if return_key_and_category:
|
||||||
|
# return tuple (key, category)
|
||||||
|
return [(to_str(p.db_key), to_str(p.db_category)) for p in matches]
|
||||||
|
else:
|
||||||
|
return [to_str(p.db_key) for p in matches]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ",".join(self.all())
|
||||||
|
|
||||||
|
def __unicode(self):
|
||||||
|
return u",".join(self.all())
|
||||||
|
|
||||||
|
|
||||||
|
class AliasHandler(TagHandler):
|
||||||
|
_tagtype = "alias"
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionHandler(TagHandler):
|
||||||
|
_tagtype = "permission"
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue