diff --git a/src/objects/models.py b/src/objects/models.py index 907feb5945..49cb8609ec 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -17,13 +17,13 @@ transparently through the decorating TypeClass. import traceback from django.db import models from django.conf import settings -from django.db.models.signals import post_init, pre_delete +from django.db.models.signals import m2m_changed from src.utils.idmapper.models import SharedMemoryModel from src.typeclasses.models import Attribute, TypedObject, TypeNick, TypeNickHandler from src.server.caches import get_field_cache, set_field_cache, del_field_cache from src.server.caches import get_prop_cache, set_prop_cache, del_prop_cache -from src.server.caches import attr_post_init, attr_pre_delete + from src.typeclasses.typeclass import TypeClass from src.players.models import PlayerNick from src.objects.manager import ObjectManager @@ -36,7 +36,7 @@ from src.utils.utils import make_iter, to_unicode, variable_from_module, inherit from django.utils.translation import ugettext as _ -#__all__ = ("ObjAttribute", "Alias", "ObjectNick", "ObjectDB") +#__all__ = ("Alias", "ObjectNick", "ObjectDB") _ScriptDB = None _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) @@ -66,8 +66,8 @@ class ObjAttribute(Attribute): verbose_name_plural = "Object Attributes" # attach the cache handlers -post_init.connect(attr_post_init, sender=ObjAttribute, dispatch_uid="objattrcache") -pre_delete.connect(attr_pre_delete, sender=ObjAttribute, dispatch_uid="objattrcache") +#post_init.connect(attr_post_init, sender=ObjAttribute, dispatch_uid="objattrcache") +#pre_delete.connect(attr_pre_delete, sender=ObjAttribute, dispatch_uid="objattrcache") #------------------------------------------------------------ # @@ -518,7 +518,7 @@ class ObjectDB(TypedObject): # this is required to properly handle attributes and typeclass loading. _typeclass_paths = settings.OBJECT_TYPECLASS_PATHS - _attribute_class = ObjAttribute + #_attribute_class = ObjAttribute _db_model_name = "objectdb" # used by attributes to safely store objects _default_typeclass_path = settings.BASE_OBJECT_TYPECLASS or "src.objects.objects.Object" diff --git a/src/players/models.py b/src/players/models.py index 37e639af18..54cbac5214 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -31,7 +31,6 @@ from django.db.models.signals import post_init, pre_delete from src.server.caches import get_field_cache, set_field_cache, del_field_cache from src.server.caches import get_prop_cache, set_prop_cache, del_prop_cache -from src.server.caches import attr_post_init, attr_pre_delete from src.players import manager from src.scripts.models import ScriptDB @@ -44,7 +43,7 @@ from src.utils.utils import inherits_from, make_iter from django.utils.translation import ugettext as _ -__all__ = ("PlayerAttribute", "PlayerNick", "PlayerDB") +__all__ = ("PlayerNick", "PlayerDB") _ME = _("me") _SELF = _("self") @@ -77,8 +76,8 @@ class PlayerAttribute(Attribute): "Define Django meta options" verbose_name = "Player Attribute" -post_init.connect(attr_post_init, sender=PlayerAttribute, dispatch_uid="playerattrcache") -pre_delete.connect(attr_pre_delete, sender=PlayerAttribute, dispatch_uid="playerattrcache") +#post_init.connect(attr_post_init, sender=PlayerAttribute, dispatch_uid="playerattrcache") +#pre_delete.connect(attr_pre_delete, sender=PlayerAttribute, dispatch_uid="playerattrcache") #------------------------------------------------------------ # @@ -252,7 +251,7 @@ class PlayerDB(TypedObject): # this is required to properly handle attributes and typeclass loading _typeclass_paths = settings.PLAYER_TYPECLASS_PATHS - _attribute_class = PlayerAttribute + #_attribute_class = PlayerAttribute _db_model_name = "playerdb" # used by attributes to safely store objects _default_typeclass_path = settings.BASE_PLAYER_TYPECLASS or "src.players.player.Player" diff --git a/src/scripts/models.py b/src/scripts/models.py index ae79794a9a..e7f1b0efb9 100644 --- a/src/scripts/models.py +++ b/src/scripts/models.py @@ -28,12 +28,11 @@ from django.conf import settings from django.db import models from django.db.models.signals import post_init, pre_delete -from src.server.caches import attr_post_init, attr_pre_delete from src.typeclasses.models import Attribute, TypedObject from django.contrib.contenttypes.models import ContentType from src.scripts.manager import ScriptManager -__all__ = ("ScriptAttribute", "ScriptDB") +__all__ = ("ScriptDB",) #------------------------------------------------------------ # @@ -50,9 +49,9 @@ class ScriptAttribute(Attribute): verbose_name = "Script Attribute" verbose_name_plural = "Script Attributes" -# attach cache handlers for attribute lookup -post_init.connect(attr_post_init, sender=ScriptAttribute, dispatch_uid="scriptattrcache") -pre_delete.connect(attr_pre_delete, sender=ScriptAttribute, dispatch_uid="scriptattrcache") +## attach cache handlers for attribute lookup +#post_init.connect(attr_post_init, sender=ScriptAttribute, dispatch_uid="scriptattrcache") +#pre_delete.connect(attr_pre_delete, sender=ScriptAttribute, dispatch_uid="scriptattrcache") #------------------------------------------------------------ # @@ -254,7 +253,7 @@ class ScriptDB(TypedObject): # this is required to properly handle attributes and typeclass loading _typeclass_paths = settings.SCRIPT_TYPECLASS_PATHS - _attribute_class = ScriptAttribute + #_attribute_class = ScriptAttribute _db_model_name = "scriptdb" # used by attributes to safely store objects _default_typeclass_path = settings.BASE_SCRIPT_TYPECLASS or "src.scripts.scripts.DoNothing" diff --git a/src/server/caches.py b/src/server/caches.py index a3d123b2bd..4bb790d098 100644 --- a/src/server/caches.py +++ b/src/server/caches.py @@ -141,26 +141,34 @@ def field_pre_save(sender, instance=None, update_fields=None, raw=False, **kwarg # to any property). #------------------------------------------------------------ -# connected to post_init signal (connected in respective Attribute model) -def attr_post_init(sender, instance=None, **kwargs): - "Called when attribute is created or retrieved in connection with obj." - #print "attr_post_init:", instance, instance.db_obj, instance.db_key - hid = hashid(_GA(instance, "db_obj"), "-%s" % _GA(instance, "db_key")) - if hid: - global _ATTR_CACHE - _ATTR_CACHE[hid] = sender - #_ATTR_CACHE.set(hid, sender) - -# connected to pre_delete signal (connected in respective Attribute model) -def attr_pre_delete(sender, instance=None, **kwargs): - "Called when attribute is deleted (del_attribute)" - #print "attr_pre_delete:", instance, instance.db_obj, instance.db_key - hid = hashid(_GA(instance, "db_obj"), "-%s" % _GA(instance, "db_key")) - if hid: - #print "attr_pre_delete:", _GA(instance, "db_key") - global _ATTR_CACHE - del _ATTR_CACHE[hid] - #_ATTR_CACHE.delete(hid) +# connected to m2m_changed signal in respective model class +def update_attr_cache(sender, **kwargs): + "Called when the many2many relation changes some way" + obj = kwargs['instance'] + model = kwargs['model'] + action = kwargs['action'] + if kwargs['reverse']: + # the reverse relation changed (the Attribute itself was acted on) + pass + else: + # forward relation changed (the Object holding the Attribute m2m field) + if action == "post_add": + # cache all added objects + for attr_id in kwargs["pk_set"]: + attr_obj = model.objects.get(pk=attr_id) + set_attr_cache(obj, _GA(attr_obj, "db_key"), attr_obj) + elif action == "post_remove": + # obj.db_attributes.remove(attr) was called + for attr_id in kwargs["pk_set"]: + attr_obj = model.objects.get(pk=attr_id) + del_attr_cache(obj, _GA(attr_obj, "db_key")) + attr_obj.delete() + elif action == "post_clear": + # obj.db_attributes.clear() was called + for attr_id in kwargs["pk_set"]: + attr_obj = model.objects.get(pk=attr_id) + del_attr_cache(obj, _GA(attr_obj, "db_key")) + attr_obj.delete() # access methods @@ -169,15 +177,23 @@ def get_attr_cache(obj, attrname): hid = hashid(obj, "-%s" % attrname) return hid and _ATTR_CACHE.get(hid, None) or None -def set_attr_cache(attrobj): +def set_attr_cache(obj, attrname, attrobj): "Set the attr cache manually; this can be used to update" - attr_post_init(None, instance=attrobj) + global _ATTR_CACHE + hid = hashid(obj, "-%s" % attrname) + _ATTR_CACHE[hid] = attrobj + +def del_attr_cache(obj, attrname): + "Del attribute cache" + global _ATTR_CACHE + hid = hashid(obj, "-%s" % attrname) + if hid in _ATTR_CACHE: + del _ATTR_CACHE[hid] def flush_attr_cache(): "Clear attribute cache" global _ATTR_CACHE _ATTR_CACHE = {} - #_ATTR_CACHE.clear() #------------------------------------------------------------ # Property cache - this is a generic cache for properties stored on models. diff --git a/src/typeclasses/managers.py b/src/typeclasses/managers.py index e3630efe47..28ddbe61cb 100644 --- a/src/typeclasses/managers.py +++ b/src/typeclasses/managers.py @@ -5,11 +5,13 @@ all Attributes and TypedObjects). """ from functools import update_wrapper from django.db import models +from django.contrib.contenttypes.models import ContentType from src.utils import idmapper from src.utils.utils import make_iter from src.utils.dbserialize import to_pickle __all__ = ("AttributeManager", "TypedObjectManager") +_GA = object.__getattribute__ # Managers @@ -50,26 +52,33 @@ class AttributeManager(models.Manager): def attr_namesearch(self, searchstr, obj, exact_match=True): """ - Searches the object's attributes for name matches. + Searches the object's attributes for attribute key matches. searchstr: (str) A string to search for. """ # Retrieve the list of attributes for this object. + if exact_match: - return self.filter(db_obj=obj).filter( - db_key__iexact=searchstr) + return _GA("obj", "db_attributes").filter(db_key__iexact=searchstr) else: - return self.filter(db_obj=obj).filter( - db_key__icontains=searchstr) + return _GA("obj", "db_attributes").filter(db_key__icontains=searchstr) def attr_valuesearch(self, searchstr, obj=None): """ - Searches for Attributes with a given value on obj + Searches obj for Attributes with a given value. + searchstr - value to search for. This may be any suitable object. + obj - limit to a given object instance + + If no restraint is given, all Attributes on all types of objects + will be searched. It's highly recommended to at least + supply the objclass argument (DBObject, DBScript or DBPlayer) + to restrict this lookup. """ if obj: - return self.filter(db_obj=obj, db_value=searchstr) + return _GA(obj, "db_attributes").filter(db_value=searchstr) return self.filter(db_value=searchstr) + # # helper functions for the TypedObjectManager. # diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index ada441f89b..94c76420ff 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -34,15 +34,18 @@ import sys import traceback #from collections import defaultdict -from django.db import models, IntegrityError +from django.db import models from django.conf import settings from django.utils.encoding import smart_str from django.contrib.contenttypes.models import ContentType -from django.db.models.fields import AutoField, FieldDoesNotExist from src.utils.idmapper.models import SharedMemoryModel from src.server.caches import get_field_cache, set_field_cache, del_field_cache from src.server.caches import get_attr_cache, set_attr_cache from src.server.caches import get_prop_cache, set_prop_cache, del_prop_cache, flush_attr_cache + +from django.db.models.signals import m2m_changed +from src.server.caches import update_attr_cache + #from src.server.caches import call_ndb_hooks from src.server.models import ServerConfig from src.typeclasses import managers @@ -61,6 +64,7 @@ _GA = object.__getattribute__ _SA = object.__setattr__ _DA = object.__delattr__ + #------------------------------------------------------------ # # Attributes @@ -106,12 +110,12 @@ class Attribute(SharedMemoryModel): db_key = models.CharField('key', max_length=255, db_index=True) # access through the value property - db_value = PickledObjectField('value2', null=True) + db_value = PickledObjectField('value', null=True) # Lock storage db_lock_storage = models.TextField('locks', blank=True) # references the object the attribute is linked to (this is set # by each child class to this abstract class) - db_obj = None # models.ForeignKey("RefencedObject") + db_obj = None # models.ForeignKey("RefencedObject") #TODO-remove # time stamp db_date_created = models.DateTimeField('date_created', editable=False, auto_now_add=True) @@ -128,7 +132,7 @@ class Attribute(SharedMemoryModel): class Meta: "Define Django meta options" - abstract = True + #abstract = True verbose_name = "Evennia Attribute" # Wrapper properties to easily set database fields. These are @@ -426,7 +430,8 @@ class TypedObject(SharedMemoryModel): # Lock storage db_lock_storage = models.TextField('locks', blank=True, help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.") - #db_attributes = models.ManyToManyField(Attribute, related_name="%(app_label)s_%(class)s_related") + # attribute store + db_attributes = models.ManyToManyField(Attribute, null=True, help_text='attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).') # Database manager objects = managers.TypedObjectManager() @@ -923,10 +928,10 @@ class TypedObject(SharedMemoryModel): # # - # Fully persistent attributes. You usually access these + # Fully attr_obj attributes. You usually access these # through the obj.db.attrname method. - # Helper methods for persistent attributes + # Helper methods for attr_obj attributes def has_attribute(self, attribute_name): """ @@ -935,10 +940,9 @@ class TypedObject(SharedMemoryModel): attribute_name: (str) The attribute's name. """ if not get_attr_cache(self, attribute_name): - attrib_obj = _GA(self, "_attribute_class").objects.filter( - db_obj=self, db_key__iexact=attribute_name) - if attrib_obj: - set_attr_cache(attrib_obj[0]) + attr_obj = _GA(self, "db_attributes").filter(db_key__iexact=attribute_name) + if attr_obj: + set_attr_cache(self, attribute_name, attr_obj[0]) else: return False return True @@ -956,46 +960,38 @@ class TypedObject(SharedMemoryModel): below to perform access-checked modification of attributes. Lock types checked by secureattr are 'attrread','attredit','attrcreate'. """ - attrib_obj = get_attr_cache(self, attribute_name) - if not attrib_obj: - attrclass = _GA(self, "_attribute_class") - # check if attribute already exists. - attrib_obj = attrclass.objects.filter( - db_obj=self, db_key__iexact=attribute_name) - if attrib_obj: - # use old attribute - attrib_obj = attrib_obj[0] - set_attr_cache(attrib_obj) # renew cache + attr_obj = get_attr_cache(self, attribute_name) + if not attr_obj: + # check if attribute already exists + attr_obj = _GA(self, "db_attributes").filter(db_key__iexact=attribute_name) + if attr_obj: + # re-use old attribute object + attr_obj = attr_obj[0] + set_attr_cache(self, attribute_name, attr_obj) # renew cache else: - # no match; create new attribute (this will cache automatically) - attrib_obj = attrclass(db_key=attribute_name, db_obj=self) + # no old attr available; create new (caches automatically) + attr_obj = Attribute(db_key=attribute_name) + attr_obj.save() # important + _GA(self, "db_attributes").add(attr_obj) if lockstring: - attrib_obj.locks.add(lockstring) - # re-set an old attribute value - try: - attrib_obj.value = new_value - except IntegrityError: - # this can happen if the cache was stale and the database object is - # missing. If so we need to clean self.hashid from the cache - flush_attr_cache(self) - self.delete() - raise IntegrityError("Attribute could not be saved - object %s was deleted from database." % self.key) + attr_obj.locks.add(lockstring) + # we shouldn't need to fear stale objects, the signalling should catch all cases + attr_obj.value = new_value def get_attribute_obj(self, attribute_name, default=None): """ Get the actual attribute object named attribute_name """ - attrib_obj = get_attr_cache(self, attribute_name) - if not attrib_obj: - attrib_obj = _GA(self, "_attribute_class").objects.filter( - db_obj=self, db_key__iexact=attribute_name) - if not attrib_obj: + attr_obj = get_attr_cache(self, attribute_name) + if not attr_obj: + attr_obj = _GA(self, "db_attributes").filter(db_key__iexact=attribute_name) + if not attr_obj: return default - set_attr_cache(attrib_obj[0]) #query is first evaluated here - return attrib_obj[0] - return attrib_obj + attr_obj = attr_obj[0] # query evaluated here + set_attr_cache(self, attribute_name, attr_obj) + return attr_obj - def get_attribute(self, attribute_name, default=None): + def get_attribute(self, attribute_name, default=None, raise_exception=False): """ Returns the value of an attribute on an object. You may need to type cast the returned value from this function since the attribute @@ -1003,73 +999,76 @@ class TypedObject(SharedMemoryModel): attribute_name: (str) The attribute's name. default: What to return if no attribute is found + raise_exception (bool) - raise an eception if no object exists instead of returning default. """ - attrib_obj = get_attr_cache(self, attribute_name) - if not attrib_obj: - attrib_obj = _GA(self, "_attribute_class").objects.filter( - db_obj=self, db_key__iexact=attribute_name) - if not attrib_obj: + attr_obj = get_attr_cache(self, attribute_name) + if not attr_obj: + attr_obj = _GA(self, "db_atttributes").filter(db_key__iexact=attribute_name) + if not attr_obj: + if raise_exception: + raise AttributeError return default - set_attr_cache(attrib_obj[0]) #query is first evaluated here - return attrib_obj[0].value - return attrib_obj.value + attr_obj = attr_obj[0] # query is evaluated here + set_attr_cache(self, attribute_name, attr_obj) + return attr_obj.value - def get_attribute_raise(self, attribute_name): - """ - Returns value of an attribute. Raises AttributeError - if no match is found. +# def get_attribute_raise(self, attribute_name): +# """ +# Returns value of an attribute. Raises AttributeError +# if no match is found. +# +# attribute_name: (str) The attribute's name. +# """ +# attr_obj = get_attr_cache(self, attribute_name) +# if not attr_obj: +# attr_obj = _GA(self, "attributes").filter(db_key__iexact=attribute_name) +# if not attr_obj: +# raise AttributeError +# attr_obj = attrib_obj[0] # query is evaluated here +# set_attr_cache(self, attribute_name, attr_obj[0]) +# return attr_obj.value - attribute_name: (str) The attribute's name. - """ - attrib_obj = get_attr_cache(self, attribute_name) - if not attrib_obj: - attrib_obj = _GA(self, "_attribute_class").objects.filter( - db_obj=self, db_key__iexact=attribute_name) - if not attrib_obj: - raise AttributeError - set_attr_cache(attrib_obj[0]) #query is first evaluated here - return attrib_obj[0].value - return attrib_obj.value - - def del_attribute(self, attribute_name): + def del_attribute(self, attribute_name, raise_exception=False): """ Removes an attribute entirely. attribute_name: (str) The attribute's name. + raise_exception (bool) - raise exception if attribute to delete + could not be found """ attr_obj = get_attr_cache(self, attribute_name) if attr_obj: attr_obj.delete() # this will clear attr cache automatically else: - try: - _GA(self, "_attribute_class").objects.filter( - db_obj=self, db_key__iexact=attribute_name)[0].delete() - except IndexError: - pass + attr_obj = _GA(self, "db_attributes").filter(db_key__iexact=attribute_name) + if attr_obj: + attr_obj[0].delete() + elif raise_exception: + raise AttributeError - def del_attribute_raise(self, attribute_name): - """ - Removes and attribute. Raises AttributeError if - attribute is not found. - - attribute_name: (str) The attribute's name. - """ - attr_obj = get_attr_cache(self, attribute_name) - if attr_obj: - attr_obj.delete() # this will clear attr cache automatically - else: - try: - _GA(self, "_attribute_class").objects.filter( - db_obj=self, db_key__iexact=attribute_name)[0].delete() - except IndexError: - pass - raise AttributeError +# def del_attribute_raise(self, attribute_name): +# """ +# Removes and attribute. Raises AttributeError if +# attribute is not found. +# +# attribute_name: (str) The attribute's name. +# """ +# attr_obj = get_attr_cache(self, attribute_name) +# if attr_obj: +# attr_obj.delete() # this will clear attr cache automatically +# else: +# try: +# _GA(self, "_attribute_class").objects.filter( +# db_obj=self, db_key__iexact=attribute_name)[0].delete() +# except IndexError: +# pass +# raise AttributeError def get_all_attributes(self): """ Returns all attributes defined on the object. """ - return list(_GA(self,"_attribute_class").objects.filter(db_obj=self)) + return list(_GA(self, "db_attributes").all()) def attr(self, attribute_name=None, value=None, delete=False): """ @@ -1195,12 +1194,12 @@ class TypedObject(SharedMemoryModel): db = property(__db_get, __db_set, __db_del) # - # NON-PERSISTENT storage methods + # NON-attr_obj storage methods # def nattr(self, attribute_name=None, value=None, delete=False): """ - This is the equivalence of self.attr but for non-persistent + This is the equivalence of self.attr but for non-attr_obj stores. Will not raise error but return None. """ if attribute_name == None: @@ -1226,7 +1225,7 @@ class TypedObject(SharedMemoryModel): #@property def __ndb_get(self): """ - A non-persistent store (ndb: NonDataBase). Everything stored + A non-attr_obj store (ndb: NonDataBase). Everything stored to this is guaranteed to be cleared when a server is shutdown. Syntax is same as for the _get_db_holder() method and property, e.g. obj.ndb.attr = value etc. @@ -1235,7 +1234,7 @@ class TypedObject(SharedMemoryModel): return self._ndb_holder except AttributeError: class NdbHolder(object): - "Holder for storing non-persistent attributes." + "Holder for storing non-attr_obj attributes." def get_all(self): return [val for val in self.__dict__.keys() if not val.startswith('_')] @@ -1313,3 +1312,7 @@ class TypedObject(SharedMemoryModel): as a new Typeclass instance. """ self.__class__.flush_cached_instance(self) + + +# connect to signal +m2m_changed.connect(update_attr_cache, sender=TypedObject.db_attributes.through) diff --git a/src/typeclasses/typeclass.py b/src/typeclasses/typeclass.py index 13640d1414..8bd1c2854b 100644 --- a/src/typeclasses/typeclass.py +++ b/src/typeclasses/typeclass.py @@ -168,7 +168,7 @@ class TypeClass(object): log_trace("This is probably due to an unsafe reload.") return # ignore delete try: - dbobj.del_attribute_raise(propname) + dbobj.del_attribute(propname, raise_exception=True) except AttributeError: string = "Object: '%s' not found on %s(#%s), nor on its typeclass %s." raise AttributeError(string % (propname, dbobj,