mirror of
https://github.com/evennia/evennia.git
synced 2026-04-05 23:47:16 +02:00
Reshuffling the Evennia package into the new template paradigm.
This commit is contained in:
parent
2846e64833
commit
2b3a32e447
371 changed files with 17250 additions and 304 deletions
1
lib/typeclasses/__init__.py
Normal file
1
lib/typeclasses/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
68
lib/typeclasses/admin.py
Normal file
68
lib/typeclasses/admin.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.admin import ModelAdmin
|
||||
from django.core.urlresolvers import reverse
|
||||
from src.typeclasses.models import Attribute, Tag
|
||||
|
||||
|
||||
class TagAdmin(admin.ModelAdmin):
|
||||
fields = ('db_key', 'db_category', 'db_data')
|
||||
|
||||
|
||||
class TagInline(admin.TabularInline):
|
||||
# Set this to the through model of your desired M2M when subclassing.
|
||||
model = None
|
||||
raw_id_fields = ('tag',)
|
||||
extra = 0
|
||||
|
||||
|
||||
class AttributeInline(admin.TabularInline):
|
||||
"""
|
||||
Inline creation of player attributes
|
||||
"""
|
||||
# Set this to the through model of your desired M2M when subclassing.
|
||||
model = None
|
||||
extra = 1
|
||||
#form = AttributeForm
|
||||
fields = ('attribute', 'key', 'value', 'strvalue')
|
||||
raw_id_fields = ('attribute',)
|
||||
readonly_fields = ('key', 'value', 'strvalue')
|
||||
|
||||
def key(self, instance):
|
||||
if not instance.id:
|
||||
return "Not yet set or saved."
|
||||
return '<strong><a href="%s">%s</a></strong>' % (
|
||||
reverse("admin:typeclasses_attribute_change",
|
||||
args=[instance.attribute.id]),
|
||||
instance.attribute.db_key)
|
||||
|
||||
key.allow_tags = True
|
||||
|
||||
def value(self, instance):
|
||||
if not instance.id:
|
||||
return "Not yet set or saved."
|
||||
return instance.attribute.db_value
|
||||
|
||||
def strvalue(self, instance):
|
||||
if not instance.id:
|
||||
return "Not yet set or saved."
|
||||
return instance.attribute.db_strvalue
|
||||
|
||||
|
||||
class AttributeAdmin(ModelAdmin):
|
||||
"""
|
||||
Defines how to display the attributes
|
||||
"""
|
||||
search_fields = ('db_key', 'db_strvalue', 'db_value')
|
||||
list_display = ('db_key', 'db_strvalue', 'db_value')
|
||||
permitted_types = ('str', 'int', 'float', 'NoneType', 'bool')
|
||||
|
||||
fields = ('db_key', 'db_value', 'db_strvalue', 'db_category',
|
||||
'db_lock_storage', 'db_model', 'db_attrtype')
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
if obj.db_value.__class__.__name__ not in self.permitted_types:
|
||||
return ['db_value']
|
||||
return []
|
||||
|
||||
admin.site.register(Attribute, AttributeAdmin)
|
||||
admin.site.register(Tag, TagAdmin)
|
||||
515
lib/typeclasses/attributes.py
Normal file
515
lib/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
lib/typeclasses/django_new_patch.py
Normal file
257
lib/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
|
||||
344
lib/typeclasses/managers.py
Normal file
344
lib/typeclasses/managers.py
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
"""
|
||||
This implements the common managers that are used by the
|
||||
abstract models in dbobjects.py (and which are thus shared by
|
||||
all Attributes and TypedObjects).
|
||||
"""
|
||||
from functools import update_wrapper
|
||||
from django.db.models import Q
|
||||
from src.utils import idmapper
|
||||
from src.utils.utils import make_iter, variable_from_module
|
||||
|
||||
__all__ = ("TypedObjectManager", )
|
||||
_GA = object.__getattribute__
|
||||
_Tag = None
|
||||
|
||||
#
|
||||
# Decorators
|
||||
#
|
||||
|
||||
def returns_typeclass_list(method):
|
||||
"""
|
||||
Decorator: Always returns a list, even
|
||||
if it is empty.
|
||||
"""
|
||||
def func(self, *args, **kwargs):
|
||||
self.__doc__ = method.__doc__
|
||||
return list(method(self, *args, **kwargs))
|
||||
return update_wrapper(func, method)
|
||||
|
||||
|
||||
def returns_typeclass(method):
|
||||
"""
|
||||
Decorator: Returns a single match or None
|
||||
"""
|
||||
def func(self, *args, **kwargs):
|
||||
self.__doc__ = method.__doc__
|
||||
query = method(self, *args, **kwargs)
|
||||
return query
|
||||
return update_wrapper(func, method)
|
||||
|
||||
# Managers
|
||||
|
||||
class TypedObjectManager(idmapper.manager.SharedMemoryManager):
|
||||
"""
|
||||
Common ObjectManager for all dbobjects.
|
||||
"""
|
||||
# common methods for all typed managers. These are used
|
||||
# in other methods. Returns querysets.
|
||||
|
||||
|
||||
# Attribute manager methods
|
||||
def get_attribute(self, key=None, category=None, value=None, strvalue=None, obj=None, attrtype=None):
|
||||
"""
|
||||
Return Attribute objects by key, by category, by value, by
|
||||
strvalue, by object (it is stored on) or with a combination of
|
||||
those criteria.
|
||||
|
||||
attrtype - one of None (normal Attributes) or "nick"
|
||||
"""
|
||||
query = [("attribute__db_attrtype", attrtype)]
|
||||
if obj:
|
||||
query.append(("%s__id" % self.model.__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", value))
|
||||
elif value:
|
||||
# strvalue and value are mutually exclusive
|
||||
query.append(("attribute__db_value", value))
|
||||
return [th.attribute for th in self.model.db_attributes.through.objects.filter(**dict(query))]
|
||||
|
||||
def get_nick(self, key=None, category=None, value=None, strvalue=None, obj=None):
|
||||
return self.get_attribute(key=key, category=category, value=value, strvalue=strvalue, obj=obj)
|
||||
|
||||
@returns_typeclass_list
|
||||
def get_by_attribute(self, key=None, category=None, value=None, strvalue=None, attrtype=None):
|
||||
"""
|
||||
Return objects having attributes with the given key, category, value,
|
||||
strvalue or combination of those criteria.
|
||||
"""
|
||||
query = [("db_attributes__db_attrtype", attrtype)]
|
||||
if key:
|
||||
query.append(("db_attributes__db_key", key))
|
||||
if category:
|
||||
query.append(("db_attributes__db_category", category))
|
||||
if strvalue:
|
||||
query.append(("db_attributes__db_strvalue", value))
|
||||
elif value:
|
||||
# strvalue and value are mutually exclusive
|
||||
query.append(("db_attributes__db_value", value))
|
||||
return self.filter(**dict(query))
|
||||
|
||||
def get_by_nick(self, key=None, nick=None, category="inputline"):
|
||||
"Get object based on its key or nick."
|
||||
return self.get_by_attribute(key=key, category=category, strvalue=nick, attrtype="nick")
|
||||
|
||||
# Tag manager methods
|
||||
|
||||
def get_tag(self, key=None, category=None, obj=None, tagtype=None):
|
||||
"""
|
||||
Return Tag objects by key, by category, by object (it is
|
||||
stored on) or with a combination of those criteria.
|
||||
|
||||
tagtype - one of None (normal tags), "alias" or "permission"
|
||||
"""
|
||||
query = [("tag__db_tagtype", tagtype)]
|
||||
if obj:
|
||||
query.append(("%s__id" % self.model.__name__.lower(), obj.id))
|
||||
if key:
|
||||
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))]
|
||||
|
||||
def get_permission(self, key=None, category=None, obj=None):
|
||||
return self.get_tag(key=key, category=category, obj=obj, tagtype="permission")
|
||||
|
||||
def get_alias(self, key=None, category=None, obj=None):
|
||||
return self.get_tag(key=key, category=category, obj=obj, tagtype="alias")
|
||||
|
||||
@returns_typeclass_list
|
||||
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.
|
||||
|
||||
tagtype = None, alias or permission
|
||||
"""
|
||||
query = [("db_tags__db_tagtype", tagtype)]
|
||||
if key:
|
||||
query.append(("db_tags__db_key", key))
|
||||
if category:
|
||||
query.append(("db_tags__db_category", category))
|
||||
return self.filter(**dict(query))
|
||||
|
||||
def get_by_permission(self, key=None, category=None):
|
||||
return self.get_by_tag(key=key, category=category, tagtype="permission")
|
||||
|
||||
def get_by_alias(self, key=None, category=None):
|
||||
return self.get_by_tag(key=key, category=category, tagtype="alias")
|
||||
|
||||
def create_tag(self, key=None, category=None, data=None, tagtype=None):
|
||||
"""
|
||||
Create a new Tag of the base type associated with this typedobject.
|
||||
This makes sure to create case-insensitive tags. If the exact same
|
||||
tag configuration (key+category+tagtype) exists on the model, a
|
||||
new tag will not be created, but an old one returned. A data
|
||||
keyword is not part of the uniqueness of the tag and setting one
|
||||
on an existing tag will overwrite the old data field.
|
||||
"""
|
||||
data = str(data) if data is not None else None
|
||||
# try to get old tag
|
||||
tag = self.get_tag(key=key, category=category, tagtype=tagtype)
|
||||
if tag and data is not None:
|
||||
# overload data on tag
|
||||
tag.db_data = data
|
||||
tag.save()
|
||||
elif not tag:
|
||||
# create a new tag
|
||||
global _Tag
|
||||
if not _Tag:
|
||||
from src.typeclasses.models import Tag as _Tag
|
||||
tag = _Tag.objects.create(
|
||||
db_key=key.strip().lower() if key is not None else None,
|
||||
db_category=category.strip().lower() if category and key is not None else None,
|
||||
db_data=data,
|
||||
db_tagtype=tagtype.strip().lower() if tagtype is not None else None)
|
||||
tag.save()
|
||||
return make_iter(tag)[0]
|
||||
|
||||
# object-manager methods
|
||||
|
||||
def dbref(self, dbref, reqhash=True):
|
||||
"""
|
||||
Valid forms of dbref (database reference number)
|
||||
are either a string '#N' or an integer N.
|
||||
Output is the integer part.
|
||||
reqhash - require input to be on form "#N" to be
|
||||
identified as a dbref
|
||||
"""
|
||||
if reqhash and not (isinstance(dbref, basestring) and dbref.startswith("#")):
|
||||
return None
|
||||
if isinstance(dbref, basestring):
|
||||
dbref = dbref.lstrip('#')
|
||||
try:
|
||||
if int(dbref) < 0:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
return dbref
|
||||
|
||||
@returns_typeclass
|
||||
def get_id(self, dbref):
|
||||
"""
|
||||
Find object with given dbref
|
||||
"""
|
||||
dbref = self.dbref(dbref, reqhash=False)
|
||||
try:
|
||||
return self.get(id=dbref)
|
||||
except self.model.DoesNotExist:
|
||||
pass
|
||||
return None
|
||||
|
||||
def dbref_search(self, dbref):
|
||||
"""
|
||||
Alias to get_id
|
||||
"""
|
||||
return self.get_id(dbref)
|
||||
|
||||
@returns_typeclass_list
|
||||
def get_dbref_range(self, min_dbref=None, max_dbref=None):
|
||||
"""
|
||||
Return all objects inside and including the
|
||||
given boundaries.
|
||||
"""
|
||||
retval = super(TypedObjectManager, self).all()
|
||||
if min_dbref is not None:
|
||||
retval = retval.filter(id__gte=self.dbref(min_dbref, reqhash=False))
|
||||
if max_dbref is not None:
|
||||
retval = retval.filter(id__lte=self.dbref(max_dbref, reqhash=False))
|
||||
return retval
|
||||
|
||||
def object_totals(self):
|
||||
"""
|
||||
Returns a dictionary with all the typeclasses active in-game
|
||||
as well as the number of such objects defined (i.e. the number
|
||||
of database object having that typeclass set on themselves).
|
||||
"""
|
||||
dbtotals = {}
|
||||
typeclass_paths = set(self.values_list('db_typeclass_path', flat=True))
|
||||
for typeclass_path in typeclass_paths:
|
||||
dbtotals[typeclass_path] = \
|
||||
self.filter(db_typeclass_path=typeclass_path).count()
|
||||
return dbtotals
|
||||
|
||||
@returns_typeclass_list
|
||||
def typeclass_search(self, typeclass, include_children=False, include_parents=False):
|
||||
"""
|
||||
Searches through all objects returning those which has a
|
||||
certain typeclass. If location is set, limit search to objects
|
||||
in that location.
|
||||
|
||||
typeclass - a typeclass class or a python path to a typeclass
|
||||
include_children - return objects with given typeclass and all
|
||||
children inheriting from this typeclass.
|
||||
include_parents - return objects with given typeclass and all
|
||||
parents to this typeclass
|
||||
The include_children/parents keywords are mutually exclusive.
|
||||
"""
|
||||
|
||||
if callable(typeclass):
|
||||
cls = typeclass.__class__
|
||||
typeclass = "%s.%s" % (cls.__module__, cls.__name__)
|
||||
elif not isinstance(typeclass, basestring) and hasattr(typeclass, "path"):
|
||||
typeclass = typeclass.path
|
||||
|
||||
# query objects of exact typeclass
|
||||
query = Q(db_typeclass_path__exact=typeclass)
|
||||
|
||||
if include_children:
|
||||
# build requests for child typeclass objects
|
||||
clsmodule, clsname = typeclass.rsplit(".", 1)
|
||||
cls = variable_from_module(clsmodule, clsname)
|
||||
subclasses = cls.__subclasses__()
|
||||
if subclasses:
|
||||
for child in (child for child in subclasses if hasattr(child, "path")):
|
||||
query = query | Q(db_typeclass_path__exact=child.path)
|
||||
elif include_parents:
|
||||
# build requests for parent typeclass objects
|
||||
clsmodule, clsname = typeclass.rsplit(".", 1)
|
||||
cls = variable_from_module(clsmodule, clsname)
|
||||
parents = cls.__mro__
|
||||
if parents:
|
||||
for parent in (parent for parent in parents if hasattr(parent, "path")):
|
||||
query = query | Q(db_typeclass_path__exact=parent.path)
|
||||
# actually query the database
|
||||
return self.filter(query)
|
||||
|
||||
|
||||
class TypeclassManager(TypedObjectManager):
|
||||
|
||||
def get(self, **kwargs):
|
||||
"""
|
||||
Overload the standard get. This will limit itself to only
|
||||
return the current typeclass.
|
||||
"""
|
||||
kwargs.update({"db_typeclass_path":self.model.path})
|
||||
return super(TypedObjectManager, self).get(**kwargs)
|
||||
|
||||
def filter(self, **kwargs):
|
||||
"""
|
||||
Overload of the standard filter function. This filter will
|
||||
limit itself to only the current typeclass.
|
||||
"""
|
||||
kwargs.update({"db_typeclass_path":self.model.path})
|
||||
return super(TypedObjectManager, self).filter(**kwargs)
|
||||
|
||||
def all(self, **kwargs):
|
||||
"""
|
||||
Overload method to return all matches, filtering for typeclass
|
||||
"""
|
||||
return super(TypedObjectManager, self).all(**kwargs).filter(db_typeclass_path=self.model.path)
|
||||
|
||||
def _get_subclasses(self, cls):
|
||||
"""
|
||||
Recursively get all subclasses to a class
|
||||
"""
|
||||
all_subclasses = cls.__subclasses__()
|
||||
for subclass in all_subclasses:
|
||||
all_subclasses.extend(self._get_subclasses(subclass))
|
||||
return all_subclasses
|
||||
|
||||
def get_family(self, **kwargs):
|
||||
"""
|
||||
Variation of get that not only returns the current
|
||||
typeclass but also all subclasses of that typeclass.
|
||||
"""
|
||||
paths = [self.model.path] + ["%s.%s" % (cls.__module__, cls.__name__)
|
||||
for cls in self._get_subclasses(self.model)]
|
||||
kwargs.update({"db_typeclass_path__in":paths})
|
||||
return super(TypedObjectManager, self).get(**kwargs)
|
||||
|
||||
def filter_family(self, **kwargs):
|
||||
"""
|
||||
Variation of filter that allows results both from typeclass
|
||||
and from subclasses of typeclass
|
||||
"""
|
||||
# query, including all subclasses
|
||||
paths = [self.model.path] + ["%s.%s" % (cls.__module__, cls.__name__)
|
||||
for cls in self._get_subclasses(self.model)]
|
||||
kwargs.update({"db_typeclass_path__in":paths})
|
||||
return super(TypedObjectManager, self).filter(**kwargs)
|
||||
|
||||
def all_family(self, **kwargs):
|
||||
"""
|
||||
Return all matches, allowing matches from all subclasses of
|
||||
the typeclass.
|
||||
"""
|
||||
paths = [self.model.path] + ["%s.%s" % (cls.__module__, cls.__name__)
|
||||
for cls in self._get_subclasses(self.model)]
|
||||
return super(TypedObjectManager, self).all(**kwargs).filter(db_typeclass_path__in=paths)
|
||||
|
||||
|
||||
55
lib/typeclasses/migrations/0001_initial.py
Normal file
55
lib/typeclasses/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import src.utils.picklefield
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Attribute',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('db_key', models.CharField(max_length=255, verbose_name=b'key', db_index=True)),
|
||||
('db_value', src.utils.picklefield.PickledObjectField(help_text=b'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.', null=True, verbose_name=b'value')),
|
||||
('db_strvalue', models.TextField(help_text=b'String-specific storage for quick look-up', null=True, verbose_name=b'strvalue', blank=True)),
|
||||
('db_category', models.CharField(max_length=128, blank=True, help_text=b'Optional categorization of attribute.', null=True, verbose_name=b'category', db_index=True)),
|
||||
('db_lock_storage', models.TextField(help_text=b'Lockstrings for this object are stored here.', verbose_name=b'locks', blank=True)),
|
||||
('db_model', models.CharField(max_length=32, blank=True, help_text=b'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.', null=True, verbose_name=b'model', db_index=True)),
|
||||
('db_attrtype', models.CharField(max_length=16, blank=True, help_text=b'Subclass of Attribute (None or nick)', null=True, verbose_name=b'attrtype', db_index=True)),
|
||||
('db_date_created', models.DateTimeField(auto_now_add=True, verbose_name=b'date_created')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Evennia Attribute',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tag',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('db_key', models.CharField(help_text=b'tag identifier', max_length=255, null=True, verbose_name=b'key', db_index=True)),
|
||||
('db_category', models.CharField(help_text=b'tag category', max_length=64, null=True, verbose_name=b'category', db_index=True)),
|
||||
('db_data', models.TextField(help_text=b'optional data field with extra information. This is not searched for.', null=True, verbose_name=b'data', blank=True)),
|
||||
('db_model', models.CharField(help_text=b'database model to Tag', max_length=32, null=True, verbose_name=b'model', db_index=True)),
|
||||
('db_tagtype', models.CharField(help_text=b'overall type of Tag', max_length=16, null=True, verbose_name=b'tagtype', db_index=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Tag',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='tag',
|
||||
unique_together=set([('db_key', 'db_category', 'db_tagtype')]),
|
||||
),
|
||||
migrations.AlterIndexTogether(
|
||||
name='tag',
|
||||
index_together=set([('db_key', 'db_category', 'db_tagtype')]),
|
||||
),
|
||||
]
|
||||
1
lib/typeclasses/migrations/__init__.py
Normal file
1
lib/typeclasses/migrations/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
589
lib/typeclasses/models.py
Normal file
589
lib/typeclasses/models.py
Normal file
|
|
@ -0,0 +1,589 @@
|
|||
"""
|
||||
This is the *abstract* django models for many of the database objects
|
||||
in Evennia. A django abstract (obs, not the same as a Python metaclass!) is
|
||||
a model which is not actually created in the database, but which only exists
|
||||
for other models to inherit from, to avoid code duplication. Any model can
|
||||
import and inherit from these classes.
|
||||
|
||||
Attributes are database objects stored on other objects. The implementing
|
||||
class needs to supply a ForeignKey field attr_object pointing to the kind
|
||||
of object being mapped. Attributes storing iterables actually store special
|
||||
types of iterables named PackedList/PackedDict respectively. These make
|
||||
sure to save changes to them to database - this is criticial in order to
|
||||
allow for obj.db.mylist[2] = data. Also, all dbobjects are saved as
|
||||
dbrefs but are also aggressively cached.
|
||||
|
||||
TypedObjects are objects 'decorated' with a typeclass - that is, the typeclass
|
||||
(which is a normal Python class implementing some special tricks with its
|
||||
get/set attribute methods, allows for the creation of all sorts of different
|
||||
objects all with the same database object underneath. Usually attributes are
|
||||
used to permanently store things not hard-coded as field on the database object.
|
||||
The admin should usually not have to deal directly with the database object
|
||||
layer.
|
||||
|
||||
This module also contains the Managers for the respective models; inherit from
|
||||
these to create custom managers.
|
||||
|
||||
"""
|
||||
|
||||
from django.db.models import signals
|
||||
|
||||
from django.db import models
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.conf import settings
|
||||
from django.utils.encoding import smart_str
|
||||
|
||||
from src.typeclasses.attributes import Attribute, AttributeHandler, NAttributeHandler
|
||||
from src.typeclasses.tags import Tag, TagHandler, AliasHandler, PermissionHandler
|
||||
|
||||
from src.utils.idmapper.models import SharedMemoryModel
|
||||
from src.utils.idmapper.base import SharedMemoryModelBase
|
||||
from src.server.caches import get_prop_cache, set_prop_cache
|
||||
#from src.server.caches import set_attr_cache
|
||||
|
||||
#from src.server.caches import call_ndb_hooks
|
||||
#from src.server.models import ServerConfig
|
||||
from src.typeclasses import managers
|
||||
from src.locks.lockhandler import LockHandler
|
||||
from src.utils.utils import (
|
||||
is_iter, inherits_from, lazy_property,
|
||||
class_from_module)
|
||||
from src.typeclasses.django_new_patch import patched_new
|
||||
|
||||
__all__ = ("Attribute", "TypeNick", "TypedObject")
|
||||
|
||||
TICKER_HANDLER = None
|
||||
|
||||
_PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
|
||||
_TYPECLASS_AGGRESSIVE_CACHE = settings.TYPECLASS_AGGRESSIVE_CACHE
|
||||
_GA = object.__getattribute__
|
||||
_SA = object.__setattr__
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Typed Objects
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
|
||||
#
|
||||
# Meta class for typeclasses
|
||||
#
|
||||
|
||||
|
||||
# signal receivers. Assigned in __new__
|
||||
def post_save(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Receives a signal just after the object is saved.
|
||||
"""
|
||||
if created:
|
||||
instance.at_first_save()
|
||||
#TODO - put OOB handler here?
|
||||
|
||||
|
||||
class TypeclassBase(SharedMemoryModelBase):
|
||||
"""
|
||||
Metaclass which should be set for the root of model proxies
|
||||
that don't define any new fields, like Object, Script etc.
|
||||
"""
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
"""
|
||||
We must define our Typeclasses as proxies. We also store the
|
||||
path directly on the class, this is required by managers.
|
||||
"""
|
||||
|
||||
# storage of stats
|
||||
attrs["typename"] = name#cls.__name__
|
||||
attrs["path"] = "%s.%s" % (attrs["__module__"], name)
|
||||
|
||||
# typeclass proxy setup
|
||||
if not "Meta" in attrs:
|
||||
class Meta:
|
||||
proxy = True
|
||||
attrs["Meta"] = Meta
|
||||
attrs["Meta"].proxy = True
|
||||
|
||||
# patch for django proxy multi-inheritance
|
||||
# this is a copy of django.db.models.base.__new__
|
||||
# with a few lines changed as per
|
||||
# https://code.djangoproject.com/ticket/11560
|
||||
new_class = patched_new(cls, name, bases, attrs)
|
||||
|
||||
# attach signal
|
||||
signals.post_save.connect(post_save, sender=new_class)
|
||||
|
||||
return new_class
|
||||
|
||||
|
||||
#
|
||||
# Main TypedObject abstraction
|
||||
#
|
||||
|
||||
|
||||
class TypedObject(SharedMemoryModel):
|
||||
"""
|
||||
Abstract Django model.
|
||||
|
||||
This is the basis for a typed object. It also contains all the
|
||||
mechanics for managing connected attributes.
|
||||
|
||||
The TypedObject has the following properties:
|
||||
key - main name
|
||||
name - alias for key
|
||||
typeclass_path - the path to the decorating typeclass
|
||||
typeclass - auto-linked typeclass
|
||||
date_created - time stamp of object creation
|
||||
permissions - perm strings
|
||||
dbref - #id of object
|
||||
db - persistent attribute storage
|
||||
ndb - non-persistent attribute storage
|
||||
|
||||
"""
|
||||
|
||||
#
|
||||
# TypedObject Database Model setup
|
||||
#
|
||||
#
|
||||
# These databse fields are all accessed and set using their corresponding
|
||||
# properties, named same as the field, but without the db_* prefix
|
||||
# (no separate save() call is needed)
|
||||
|
||||
# Main identifier of the object, for searching. Is accessed with self.key
|
||||
# or self.name
|
||||
db_key = models.CharField('key', max_length=255, db_index=True)
|
||||
# This is the python path to the type class this object is tied to. The
|
||||
# typeclass is what defines what kind of Object this is)
|
||||
db_typeclass_path = models.CharField('typeclass', max_length=255, null=True,
|
||||
help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.")
|
||||
# Creation date. This is not changed once the object is created.
|
||||
db_date_created = models.DateTimeField('creation date', editable=False, auto_now_add=True)
|
||||
# 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.")
|
||||
# many2many relationships
|
||||
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).')
|
||||
db_tags = models.ManyToManyField(Tag, null=True,
|
||||
help_text='tags on this object. Tags are simple string markers to identify, group and alias objects.')
|
||||
|
||||
# Database manager
|
||||
objects = managers.TypedObjectManager()
|
||||
|
||||
# quick on-object typeclass cache for speed
|
||||
_cached_typeclass = None
|
||||
|
||||
# typeclass mechanism
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
This is the main function of the typeclass system -
|
||||
to dynamically re-apply a class based on the
|
||||
db_typeclass_path rather than use the one in the model.
|
||||
"""
|
||||
typeclass_path = kwargs.pop("typeclass", None)
|
||||
super(TypedObject, self).__init__(*args, **kwargs)
|
||||
if typeclass_path:
|
||||
self.__class__ = class_from_module(typeclass_path)
|
||||
self.db_typclass_path = typeclass_path
|
||||
elif self.db_typeclass_path:
|
||||
self.__class__ = class_from_module(self.db_typeclass_path)
|
||||
else:
|
||||
self.db_typeclass_path = "%s.%s" % (self.__module__, self.__class__.__name__)
|
||||
# important to put this at the end since _meta is based on the set __class__
|
||||
self.__dbclass__ = self._meta.proxy_for_model or self.__class__
|
||||
|
||||
# initialize all handlers in a lazy fashion
|
||||
@lazy_property
|
||||
def attributes(self):
|
||||
return AttributeHandler(self)
|
||||
|
||||
@lazy_property
|
||||
def locks(self):
|
||||
return LockHandler(self)
|
||||
|
||||
@lazy_property
|
||||
def tags(self):
|
||||
return TagHandler(self)
|
||||
|
||||
@lazy_property
|
||||
def aliases(self):
|
||||
return AliasHandler(self)
|
||||
|
||||
@lazy_property
|
||||
def permissions(self):
|
||||
return PermissionHandler(self)
|
||||
|
||||
@lazy_property
|
||||
def nattributes(self):
|
||||
return NAttributeHandler(self)
|
||||
|
||||
|
||||
class Meta:
|
||||
"""
|
||||
Django setup info.
|
||||
"""
|
||||
abstract = True
|
||||
verbose_name = "Evennia Database Object"
|
||||
ordering = ['-db_date_created', 'id', 'db_typeclass_path', 'db_key']
|
||||
|
||||
# wrapper
|
||||
# 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).
|
||||
|
||||
# name property (alias to self.key)
|
||||
def __name_get(self):
|
||||
return self.key
|
||||
|
||||
def __name_set(self, value):
|
||||
self.key = value
|
||||
|
||||
def __name_del(self):
|
||||
raise Exception("Cannot delete name")
|
||||
name = property(__name_get, __name_set, __name_del)
|
||||
|
||||
#
|
||||
#
|
||||
# TypedObject main class methods and properties
|
||||
#
|
||||
#
|
||||
|
||||
def __eq__(self, other):
|
||||
return other and hasattr(other, 'dbid') and self.dbid == other.dbid
|
||||
|
||||
def __str__(self):
|
||||
return smart_str("%s" % self.db_key)
|
||||
|
||||
def __unicode__(self):
|
||||
return u"%s" % self.db_key
|
||||
|
||||
#@property
|
||||
def __dbid_get(self):
|
||||
"""
|
||||
Caches and returns the unique id of the object.
|
||||
Use this instead of self.id, which is not cached.
|
||||
"""
|
||||
dbid = get_prop_cache(self, "_dbid")
|
||||
if not dbid:
|
||||
dbid = self.id
|
||||
set_prop_cache(self, "_dbid", dbid)
|
||||
return dbid
|
||||
|
||||
def __dbid_set(self, value):
|
||||
raise Exception("dbid cannot be set!")
|
||||
|
||||
def __dbid_del(self):
|
||||
raise Exception("dbid cannot be deleted!")
|
||||
dbid = property(__dbid_get, __dbid_set, __dbid_del)
|
||||
|
||||
#@property
|
||||
def __dbref_get(self):
|
||||
"""
|
||||
Returns the object's dbref on the form #NN.
|
||||
"""
|
||||
return "#%s" % self.dbid
|
||||
|
||||
def __dbref_set(self):
|
||||
raise Exception("dbref cannot be set!")
|
||||
|
||||
def __dbref_del(self):
|
||||
raise Exception("dbref cannot be deleted!")
|
||||
dbref = property(__dbref_get, __dbref_set, __dbref_del)
|
||||
|
||||
#
|
||||
# Object manipulation methods
|
||||
#
|
||||
|
||||
def is_typeclass(self, typeclass, exact=True):
|
||||
"""
|
||||
Returns true if this object has this type OR has a typeclass
|
||||
which is an subclass of the given typeclass. This operates on
|
||||
the actually loaded typeclass (this is important since a
|
||||
failing typeclass may instead have its default currently
|
||||
loaded) typeclass - can be a class object or the python path
|
||||
to such an object to match against.
|
||||
|
||||
typeclass - a class or the full python path to the class
|
||||
exact - returns true only
|
||||
if the object's type is exactly this typeclass, ignoring
|
||||
parents.
|
||||
"""
|
||||
if not isinstance(typeclass, basestring):
|
||||
typeclass = typeclass.path
|
||||
|
||||
if exact:
|
||||
return typeclass == self.path
|
||||
else:
|
||||
# check parent chain
|
||||
selfpath = self.path
|
||||
return any(cls for cls in self.__class__.mro() if cls.path == selfpath)
|
||||
|
||||
def swap_typeclass(self, new_typeclass, clean_attributes=False,
|
||||
run_start_hooks=True, no_default=True):
|
||||
"""
|
||||
This performs an in-situ swap of the typeclass. This means
|
||||
that in-game, this object will suddenly be something else.
|
||||
Player will not be affected. To 'move' a player to a different
|
||||
object entirely (while retaining this object's type), use
|
||||
self.player.swap_object().
|
||||
|
||||
Note that this might be an error prone operation if the
|
||||
old/new typeclass was heavily customized - your code
|
||||
might expect one and not the other, so be careful to
|
||||
bug test your code if using this feature! Often its easiest
|
||||
to create a new object and just swap the player over to
|
||||
that one instead.
|
||||
|
||||
Arguments:
|
||||
new_typeclass (path/classobj) - type to switch to
|
||||
clean_attributes (bool/list) - will delete all attributes
|
||||
stored on this object (but not any
|
||||
of the database fields such as name or
|
||||
location). You can't get attributes back,
|
||||
but this is often the safest bet to make
|
||||
sure nothing in the new typeclass clashes
|
||||
with the old one. If you supply a list,
|
||||
only those named attributes will be cleared.
|
||||
run_start_hooks - trigger the start hooks of the object, as if
|
||||
it was created for the first time.
|
||||
no_default - if this is active, the swapper will not allow for
|
||||
swapping to a default typeclass in case the given
|
||||
one fails for some reason. Instead the old one
|
||||
will be preserved.
|
||||
Returns:
|
||||
boolean True/False depending on if the swap worked or not.
|
||||
|
||||
"""
|
||||
|
||||
if not callable(new_typeclass):
|
||||
# this is an actual class object - build the path
|
||||
new_typeclass = class_from_module(new_typeclass)
|
||||
|
||||
# if we get to this point, the class is ok.
|
||||
|
||||
|
||||
if inherits_from(self, "src.scripts.models.ScriptDB"):
|
||||
if self.interval > 0:
|
||||
raise RuntimeError("Cannot use swap_typeclass on time-dependent " \
|
||||
"Script '%s'.\nStop and start a new Script of the " \
|
||||
"right type instead." % self.key)
|
||||
|
||||
self.typeclass_path = new_typeclass.path
|
||||
self.__class__ = new_typeclass
|
||||
|
||||
if clean_attributes:
|
||||
# Clean out old attributes
|
||||
if is_iter(clean_attributes):
|
||||
for attr in clean_attributes:
|
||||
self.attributes.remove(attr)
|
||||
for nattr in clean_attributes:
|
||||
if hasattr(self.ndb, nattr):
|
||||
self.nattributes.remove(nattr)
|
||||
else:
|
||||
#print "deleting attrs ..."
|
||||
self.attributes.clear()
|
||||
self.nattributes.clear()
|
||||
|
||||
if run_start_hooks:
|
||||
# fake this call to mimic the first save
|
||||
self.at_first_save()
|
||||
|
||||
#
|
||||
# Lock / permission methods
|
||||
#
|
||||
|
||||
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 - this is ignored, but is there to make the api consistent with the
|
||||
object-typeclass method access, which use it to feed to its hook methods.
|
||||
"""
|
||||
return self.locks.check(accessing_obj, access_type=access_type, default=default)
|
||||
|
||||
def check_permstring(self, permstring):
|
||||
"""
|
||||
This explicitly checks if we hold particular permission without
|
||||
involving any locks. It does -not- trigger the at_access hook.
|
||||
"""
|
||||
if hasattr(self, "player"):
|
||||
if self.player and self.player.is_superuser:
|
||||
return True
|
||||
else:
|
||||
if self.is_superuser:
|
||||
return True
|
||||
|
||||
if not permstring:
|
||||
return False
|
||||
perm = permstring.lower()
|
||||
perms = [p.lower() for p in self.permissions.all()]
|
||||
if perm in perms:
|
||||
# simplest case - we have a direct match
|
||||
return True
|
||||
if perm in _PERMISSION_HIERARCHY:
|
||||
# check if we have a higher hierarchy position
|
||||
ppos = _PERMISSION_HIERARCHY.index(perm)
|
||||
return any(True for hpos, hperm in enumerate(_PERMISSION_HIERARCHY)
|
||||
if hperm in perms and hpos > ppos)
|
||||
return False
|
||||
|
||||
#
|
||||
# Deletion methods
|
||||
#
|
||||
|
||||
def _deleted(self, *args, **kwargs):
|
||||
"Scrambling method for already deleted objects"
|
||||
raise ObjectDoesNotExist("This object was already deleted!")
|
||||
|
||||
_is_deleted = False # this is checked by db_* wrappers
|
||||
|
||||
def delete(self):
|
||||
"Cleaning up handlers on the typeclass level"
|
||||
global TICKER_HANDLER
|
||||
if not TICKER_HANDLER:
|
||||
from src.scripts.tickerhandler import TICKER_HANDLER
|
||||
TICKER_HANDLER.remove(self) # removes objects' all ticker subscriptions
|
||||
self.permissions.clear()
|
||||
self.attributes.clear()
|
||||
self.aliases.clear()
|
||||
if hasattr(self, "nicks"):
|
||||
self.nicks.clear()
|
||||
self.flush_from_cache()
|
||||
|
||||
# scrambling properties
|
||||
self.delete = self._deleted
|
||||
self._is_deleted = True
|
||||
super(TypedObject, self).delete()
|
||||
|
||||
#
|
||||
# Memory management
|
||||
#
|
||||
|
||||
def flush_from_cache(self):
|
||||
"""
|
||||
Flush this object instance from cache, forcing an object reload.
|
||||
Note that this will kill all temporary attributes on this object
|
||||
since it will be recreated as a new Typeclass instance.
|
||||
"""
|
||||
self.__class__.flush_cached_instance(self)
|
||||
|
||||
#
|
||||
# Attribute storage
|
||||
#
|
||||
|
||||
#@property db
|
||||
def __db_get(self):
|
||||
"""
|
||||
Attribute handler wrapper. Allows for the syntax
|
||||
obj.db.attrname = value
|
||||
and
|
||||
value = obj.db.attrname
|
||||
and
|
||||
del obj.db.attrname
|
||||
and
|
||||
all_attr = obj.db.all() (unless there is an attribute
|
||||
named 'all', in which case that will be returned instead).
|
||||
"""
|
||||
try:
|
||||
return self._db_holder
|
||||
except AttributeError:
|
||||
class DbHolder(object):
|
||||
"Holder for allowing property access of attributes"
|
||||
def __init__(self, obj):
|
||||
_SA(self, "attrhandler", obj.attributes)
|
||||
|
||||
def __getattribute__(self, attrname):
|
||||
if attrname == 'all':
|
||||
# we allow to overload our default .all
|
||||
attr = _GA(self, "attrhandler").get("all")
|
||||
if attr:
|
||||
return attr
|
||||
return self.all
|
||||
return _GA(self, "attrhandler").get(attrname)
|
||||
|
||||
def __setattr__(self, attrname, value):
|
||||
_GA(self, "attrhandler").add(attrname, value)
|
||||
|
||||
def __delattr__(self, attrname):
|
||||
_GA(self, "attrhandler").remove(attrname)
|
||||
|
||||
def get_all(self):
|
||||
return _GA(self, "attrhandler").all()
|
||||
all = property(get_all)
|
||||
self._db_holder = DbHolder(self)
|
||||
return self._db_holder
|
||||
|
||||
#@db.setter
|
||||
def __db_set(self, value):
|
||||
"Stop accidentally replacing the db object"
|
||||
string = "Cannot assign directly to db object! "
|
||||
string += "Use db.attr=value instead."
|
||||
raise Exception(string)
|
||||
|
||||
#@db.deleter
|
||||
def __db_del(self):
|
||||
"Stop accidental deletion."
|
||||
raise Exception("Cannot delete the db object!")
|
||||
db = property(__db_get, __db_set, __db_del)
|
||||
|
||||
#
|
||||
# Non-persistent (ndb) storage
|
||||
#
|
||||
|
||||
#@property ndb
|
||||
def __ndb_get(self):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
try:
|
||||
return self._ndb_holder
|
||||
except AttributeError:
|
||||
class NDbHolder(object):
|
||||
"Holder for allowing property access of attributes"
|
||||
def __init__(self, obj):
|
||||
_SA(self, "nattrhandler", obj.nattributes)
|
||||
|
||||
def __getattribute__(self, attrname):
|
||||
if attrname == 'all':
|
||||
# we allow to overload our default .all
|
||||
attr = _GA(self, "nattrhandler").get('all')
|
||||
if attr:
|
||||
return attr
|
||||
return self.all
|
||||
return _GA(self, "nattrhandler").get(attrname)
|
||||
|
||||
def __setattr__(self, attrname, value):
|
||||
_GA(self, "nattrhandler").add(attrname, value)
|
||||
|
||||
def __delattr__(self, attrname):
|
||||
_GA(self, "nattrhandler").remove(attrname)
|
||||
|
||||
def get_all(self):
|
||||
return _GA(self, "nattrhandler").all()
|
||||
all = property(get_all)
|
||||
self._ndb_holder = NDbHolder(self)
|
||||
return self._ndb_holder
|
||||
|
||||
#@db.setter
|
||||
def __ndb_set(self, value):
|
||||
"Stop accidentally replacing the ndb object"
|
||||
string = "Cannot assign directly to ndb object! "
|
||||
string += "Use ndb.attr=value instead."
|
||||
raise Exception(string)
|
||||
|
||||
#@db.deleter
|
||||
def __ndb_del(self):
|
||||
"Stop accidental deletion."
|
||||
raise Exception("Cannot delete the ndb object!")
|
||||
ndb = property(__ndb_get, __ndb_set, __ndb_del)
|
||||
|
||||
193
lib/typeclasses/tags.py
Normal file
193
lib/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