First rework of tag category handling by new fields db_model and db_tagtype.

This commit is contained in:
Griatch 2014-02-16 13:19:31 +01:00
parent a9ad82d005
commit 17123bf7ed
2 changed files with 60 additions and 51 deletions

View file

@ -6,6 +6,7 @@ all Attributes and TypedObjects).
from functools import update_wrapper from functools import update_wrapper
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.contrib.contenttypes.models import ContentType
from src.utils import idmapper from src.utils import idmapper
from src.utils.utils import make_iter from src.utils.utils import make_iter
from src.utils.dbserialize import to_pickle from src.utils.dbserialize import to_pickle
@ -113,60 +114,61 @@ class TagManager(models.Manager):
tags = tags.filter(db_category__iexact=category.lower().strip()) tags = tags.filter(db_category__iexact=category.lower().strip())
return list(tags) return list(tags)
def get_tag(self, key=None, category=None): def get_tag(self, key=None, category=None, model="objects.objectdb", tagtype=None):
""" """
Search and return all tags matching any combination of Search and return all tags matching any combination of
the search criteria. the search criteria.
search_key (string) - the tag identifier search_key (string) - the tag identifier
category (string) - the tag category category (string) - the tag category
model - the type of object tagged, on naturalkey form, like "objects.objectdb"
tagtype - None, alias or permission
Returns a single Tag (or None) if both key and category is given, Returns a single Tag (or None) if both key and category is given,
otherwise it will return a list. otherwise it will return a list.
""" """
key_cands = Q(db_key__iexact=key.lower().strip()) if key is not None else Q() key_cands = Q(db_key__iexact=key.lower().strip()) if key is not None else Q()
cat_cands = Q(db_category__iexact=category.lower().strip()) if category is not None else Q() cat_cands = Q(db_category__iexact=category.lower().strip()) if category is not None else Q()
tags = self.filter(key_cands & cat_cands) tags = self.filter(db_model=model, db_tagtype=tagtype).filter(key_cands & cat_cands)
if key and category: if key and category:
return tags[0] if tags else None return tags[0] if tags else None
else: else:
return list(tags) return list(tags)
def get_objs_with_tag(self, key=None, category=None, objclass=None): def get_objs_with_tag(self, key=None, category=None, model="objects.objectdb", tagtype=None):
""" """
Search and return all objects of objclass that has tags matching Search and return all objects of objclass that has tags matching
the given search criteria. the given search criteria.
key (string) - the tag identifier key (string) - the tag identifier
category (string) - the tag category category (string) - the tag category
model (string) - tag model name. Defaults to "ObjectDB"
tagtype (string) - None, alias or permission
objclass (dbmodel) - the object class to search. If not given, use ObjectDB. objclass (dbmodel) - the object class to search. If not given, use ObjectDB.
""" """
global _ObjectDB objclass = ContentType.objects.get_by_natural_key(*model.split(".", 1)).model_class()
if not objclass:
if not _ObjectDB:
from src.objects.models import ObjectDB as _ObjectDB
objclass = _ObjectDB
key_cands = Q(db_tags__db_key__iexact=key.lower().strip()) if key is not None else Q() key_cands = Q(db_tags__db_key__iexact=key.lower().strip()) if key is not None else Q()
cat_cands = Q(db_tags__db_category__iexact=category.lower().strip()) if category is not None else Q() cat_cands = Q(db_tags__db_category__iexact=category.lower().strip()) if category is not None else Q()
return objclass.objects.filter(key_cands & cat_cands) return objclass.objects.filter(db_model=model, db_tagtype=tagtype).filter(key_cands & cat_cands)
def create_tag(self, key=None, category=None, data=None): def create_tag(self, key=None, category=None, data=None, model="objects.objectdb", tagtype=None):
""" """
Create a tag. This makes sure the create case-insensitive tags. Create a tag. This makes sure the create case-insensitive tags.
Note that if the exact same tag configuration (key+category) Note that if the exact same tag configuration (key+category+model+tagtype)
exists, it will be re-used. A data keyword will overwrite existing exists, it will be re-used. A data keyword will overwrite existing
data on a tag (it is not part of what makes the tag unique). data on a tag (it is not part of what makes the tag unique).
""" """
data = str(data) if data is not None else None data = str(data) if data is not None else None
tag = self.get_tag(key=key, category=category) tag = self.get_tag(key=key, category=category, model=model, tagtype=tagtype)
if tag and data is not None: if tag and data is not None:
tag.db_data = data tag.db_data = data
tag.save() tag.save()
elif not tag: elif not tag:
tag = self.create(db_key=key.lower().strip() if key is not None else None, tag = self.create(db_key=key.lower().strip() if key is not None else None,
db_category=category.lower().strip() db_category=category.lower().strip() if category and key is not None else None,
if category and key is not None else None, db_data=str(data) if data is not None else None,
db_data=str(data) if data is not None else None) db_model=model,
db_tagtype=tagtype)
tag.save() tag.save()
return make_iter(tag)[0] return make_iter(tag)[0]

View file

@ -41,7 +41,7 @@ from django.contrib.contenttypes.models import ContentType
from src.utils.idmapper.models import SharedMemoryModel from src.utils.idmapper.models import SharedMemoryModel
from src.server.caches import get_prop_cache, set_prop_cache from src.server.caches import get_prop_cache, set_prop_cache
from src.server.caches import get_attr_cache, set_attr_cache from src.server.caches import set_attr_cache
#from src.server.caches import call_ndb_hooks #from src.server.caches import call_ndb_hooks
from src.server.models import ServerConfig from src.server.models import ServerConfig
@ -462,6 +462,10 @@ class Tag(models.Model):
help_text="tag category", db_index=True) help_text="tag category", db_index=True)
db_data = models.TextField('data', null=True, blank=True, db_data = models.TextField('data', null=True, blank=True,
help_text="optional data field with extra information. This is not searched for.") help_text="optional data field with extra information. This is not searched for.")
# this is "objects.objectdb" etc
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)
objects = managers.TagManager() objects = managers.TagManager()
@ -488,62 +492,64 @@ class TagHandler(object):
Generic tag-handler. Accessed via TypedObject.tags. Generic tag-handler. Accessed via TypedObject.tags.
""" """
_m2m_fieldname = "db_tags" _m2m_fieldname = "db_tags"
_base_category = "" _tagtype = None
def __init__(self, obj, category_prefix=""): def __init__(self, obj):
""" """
Tags are stored internally in the TypedObject.db_tags m2m field Tags are stored internally in the TypedObject.db_tags m2m field
using the category <category_prefix><tag_category> 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.obj = obj
self.prefix = "%s%s" % (category_prefix.strip(" _").lower() self._model = "%s.%s" % ContentType.objects.get_for_model(obj).natural_key()
if category_prefix else "", self._base_category)
self._cache = None self._cache = None
def _recache(self): def _recache(self):
self._cache = dict((to_str(p.db_key), p) for p in _GA(self.obj, self._m2m_fieldname).filter( "Update cache from database field"
db_category__startswith=self.prefix)) self._cache = dict(("%s-%s" % (p.db_key, p.db_category), p)
for p in _GA(self.obj, self._m2m_fieldname).filter(
db_model=self._model, db_tagtype=self._tagtype))
def add(self, tag, category=None, data=None): def add(self, tag, category=None, data=None):
"Add a new tag to the handler. Tag is a string or a list of strings." "Add a new tag to the handler. Tag is a string or a list of strings."
for tagstr in make_iter(tag): for tagstr in make_iter(tag):
tagstr = tagstr.strip().lower() if tagstr is not None else None tagstr = tagstr.strip().lower() if tagstr is not None else None
categ = "%s%s" % (self.prefix, category.strip().lower() if category is not None else "") category = category().lower() if category is not None else None
data = str(data) if data 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 # this will only create tag if no matches existed beforehand (it
# will overload data on an existing tag since that is not # will overload data on an existing tag since that is not
# considered part of making the tag unique) # considered part of making the tag unique)
tagobj = Tag.objects.create_tag(key=tagstr, category=categ, data=data) tagobj = Tag.objects.create_tag(key=tagstr, category=category, data=data,
model=self._model, tagtype=self._tagtype)
_GA(self.obj, self._m2m_fieldname).add(tagobj) _GA(self.obj, self._m2m_fieldname).add(tagobj)
if self._cache is None: if self._cache is None:
self._recache() self._recache()
self._cache[tagstr] = tagobj cachestring = "%s-%s" % (tagstr, category)
self._cache[cachestring] = tagobj
def get(self, key, category="", return_data=False): def get(self, key, category="", return_tagobj=False):
""" """
Get the tag for the given key or list of tags. If Get the tag for the given key or list of tags. If
return_data=True, return the matching Tag objects instead. 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: if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
self._recache() self._recache()
ret = [] ret = []
category = "%s%s" % (self.prefix, category.strip().lower() category = category.strip().lower() if category is not None else None
if category is not None else "") 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.strip().lower()) ret = [val for val in (self._cache.get(searchkey) for keystr in key) if val]
for keystr in make_iter(key)) if val] ret = [to_str(tag.db_data) for tag in ret] if return_tagobj else ret
ret = [to_str(tag.db_data) for tag in ret] if return_data else ret
return ret[0] if len(ret) == 1 else ret return ret[0] if len(ret) == 1 else ret
def remove(self, tag, category=None): def remove(self, key, category=None):
"Remove a tag from the handler, where tag is the key of the tag to remove" "Remove a tag from the handler based ond key and category."
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
self._recache()
for tag in make_iter(tag): for tag in make_iter(tag):
if not (tag or tag.strip()): # we don't allow empty tags if not (tag or tag.strip()): # we don't allow empty tags
continue continue
tagstr = tag.strip().lower() if tag is not None else None tagstr = tag.strip().lower() if tag is not None else None
category = "%s%s" % (self.prefix, category.strip().lower() category = category.strip().lower() if tag is not None else None
if category is not None else "")
# This does not delete the tag object itself. Maybe it should do # This does not delete the tag object itself. Maybe it should do
# that when no objects reference the tag anymore (how to check)? # that when no objects reference the tag anymore (how to check)?
@ -554,7 +560,7 @@ class TagHandler(object):
def clear(self): def clear(self):
"Remove all tags from the handler" "Remove all tags from the handler"
for tag in _GA(self.obj, self._m2m_fieldname).filter(db_category__startswith=self.prefix): for tag in _GA(self.obj, self._m2m_fieldname).filter(db_model=self._model, db_tagtype=self._tagtype):
_GA(self.obj, self._m2m_fieldname).remove(tag) _GA(self.obj, self._m2m_fieldname).remove(tag)
self._recache() self._recache()
@ -563,21 +569,22 @@ class TagHandler(object):
Get all tags in this handler. Get all tags in this handler.
If category is given, return only Tags with this category. If If category is given, return only Tags with this category. If
return_keys_and_categories is set, return a list of tuples [(key, category), ...] return_keys_and_categories is set, return a list of tuples [(key, category), ...]
where the category is stripped of the category prefix (this is ignored
if category keyword is given).
""" """
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
self._recache() self._recache()
if category: if category:
category = "%s%s" % (self.prefix, category.strip().lower() category = category.strip().lower() if category is not None else None
if category is not None else "") matches = _GA(self.obj, self._m2m_fieldname).filter(db_category=category,
return [to_str(p[0]) for p in _GA(self.obj, self._m2m_fieldname).filter( db_tagtype=self._tagtype,
db_category=category).values_list("db_key") if p[0]] db_model=self._model).values_list("db_key")
elif return_key_and_category:
# return tuple (key, category)
return [(to_str(p.db_key), to_str(p.db_category).lstrip(self.prefix)) for p in self._cache.values()]
else: else:
return self._cache.keys() matches = self._cache.values()
if matches:
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 [to_str(p[0]) for p in _GA(self.obj, self._m2m_fieldname).filter(db_category__startswith=self.prefix).values_list("db_key") if p[0]] #return [to_str(p[0]) for p in _GA(self.obj, self._m2m_fieldname).filter(db_category__startswith=self.prefix).values_list("db_key") if p[0]]
@ -589,11 +596,11 @@ class TagHandler(object):
class AliasHandler(TagHandler): class AliasHandler(TagHandler):
_base_category = "alias" _tagtype = "alias"
class PermissionHandler(TagHandler): class PermissionHandler(TagHandler):
_base_category = "permission" _tagtype = "permission"
#------------------------------------------------------------ #------------------------------------------------------------