diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 6e93da41c9..e2b30917b2 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -321,6 +321,12 @@ class AttributeHandler(object): return ret if len(key) > 1 else default return ret[0] if len(ret)==1 else ret + def batch_add(self, keys, values, categories=None, lockstrings=None, + stratts=None, accessing_obj=None, default_access=True): + """ + Batch version supporting the addition of more than one + """ + def add(self, key, value, category=None, lockstring="", strattr=False, accessing_obj=None, default_access=True): """ @@ -332,6 +338,12 @@ class AttributeHandler(object): 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. + + The method also accepts multiple attributes (this is a faster way + to add attributes since it allows for some optimizations). + If so, key and value (or strvalue) must be iterables of the same length. + All batch-added Attributes will use the same category and lockstring. + """ if accessing_obj and not self.obj.access(accessing_obj, self._attrcreate, default=default_access): @@ -341,28 +353,41 @@ class AttributeHandler(object): self._recache() if not key: return - key = key.strip().lower() + + 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 - cachekey = "%s-%s" % (key, category) - attr_obj = self._cache.get(cachekey) - if not attr_obj: - # no old attr available; create new. - attr_obj = Attribute(db_key=key, db_category=category, - db_model=self._model, db_attrtype=self._attrtype) - attr_obj.save() # important - getattr(self.obj, self._m2m_fieldname).add(attr_obj) - self._cache[cachekey] = attr_obj - if lockstring: - attr_obj.locks.add(lockstring) - # we shouldn't need to fear stale objects, the field signalling - # should catch all cases - if strattr: - # store as a simple string - attr_obj.db_strvalue = value - attr_obj.save(update_fields=["db_strvalue"]) - else: - # pickle arbitrary data - attr_obj.value = value + 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_model" : self._model, "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): @@ -581,8 +606,10 @@ class TagHandler(object): self.obj, self._m2m_fieldname).filter( db_model=self._model).filter(tagtype)) - def add(self, tag, category=None, data=None): + 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 diff --git a/src/utils/create.py b/src/utils/create.py index bce3c76f62..13052d207f 100644 --- a/src/utils/create.py +++ b/src/utils/create.py @@ -59,7 +59,12 @@ def handle_dbref(inp, objclass, raise_errors=True): objects. """ if not (isinstance(inp, basestring) and inp.startswith("#")): - return inp + try: + return inp.dbobj + except AttributeError: + return inp + + # a string, analyze it inp = inp.lstrip('#') try: if int(inp) < 0: @@ -67,7 +72,7 @@ def handle_dbref(inp, objclass, raise_errors=True): except ValueError: return None - # if we get to this point, inp is an integer dbref + # if we get to this point, inp is an integer dbref; get the matching object try: return objclass.objects.get(id=inp) except Exception: @@ -98,7 +103,7 @@ def create_object(typeclass=None, key=None, location=None, containing the error message. If set, this method will return None upon errors. nohome - this allows the creation of objects without a default home location; - this only used when creating default location itself or during unittests + this only used when creating the default location itself or during unittests """ global _Object, _ObjectDB if not _Object: @@ -122,7 +127,6 @@ def create_object(typeclass=None, key=None, location=None, location = handle_dbref(location, _ObjectDB) destination = handle_dbref(destination, _ObjectDB) - report_to = handle_dbref(report_to, _ObjectDB) home = handle_dbref(home, _ObjectDB) if not home: try: @@ -136,9 +140,8 @@ def create_object(typeclass=None, key=None, location=None, db_destination=destination, db_home=home, db_typeclass_path=typeclass) - # the name/key is often set later in the typeclass. This - # is set here as a failsafe. if not key: + # the object should always have a key, so if not set we give a default new_db_object.key = "#%i" % new_db_object.dbid # this will either load the typeclass or the default one (will also save object) @@ -149,6 +152,7 @@ def create_object(typeclass=None, key=None, location=None, # gave us a default SharedMemoryModel.delete(new_db_object) if report_to: + report_to = handle_dbref(report_to, _ObjectDB) _GA(report_to, "msg")("Error creating %s (%s).\n%s" % (new_db_object.key, typeclass, _GA(new_db_object, "typeclass_last_errmsg"))) return None @@ -162,10 +166,21 @@ def create_object(typeclass=None, key=None, location=None, # customization happens as the typeclass stores custom # things on its database object. - # note - this will override eventual custom keys, locations etc! + # note - this may override input keys, locations etc! new_object.basetype_setup() # setup the basics of Exits, Characters etc. new_object.at_object_creation() + # we want the input to override that set in the hooks, so + # we re-apply those if needed + if new_object.key != key: + new_object.key = key + if new_object.location != location: + new_object.location = location + if new_object.home != home: + new_object.home = home + if new_object.destination != destination: + new_object.destination = destination + # custom-given perms/locks do overwrite hooks if permissions: new_object.permissions.add(permissions) @@ -174,7 +189,7 @@ def create_object(typeclass=None, key=None, location=None, if aliases: new_object.aliases.add(aliases) - # trigger relevant move_to hooks in order to display eventual messages. + # trigger relevant move_to hooks in order to display messages. if location: new_object.at_object_receive(new_object, None) new_object.at_after_move(new_object) diff --git a/src/utils/picklefield.py b/src/utils/picklefield.py index 3c59ad31f9..49da9f0ebe 100644 --- a/src/utils/picklefield.py +++ b/src/utils/picklefield.py @@ -43,6 +43,8 @@ from django.forms import CharField, Textarea from django.forms.util import flatatt from django.utils.html import format_html +from src.utils.dbserialize import from_pickle, to_pickle + try: from django.utils.encoding import force_text except ImportError: