"""Models for arbitrary key-values storage, bound to a namespace, a user, and some other object (currently only the discussion)."""
from __future__ import absolute_import
from builtins import object
from collections import Mapping, MutableMapping
import simplejson as json
from sqlalchemy import (
Boolean,
Column,
String,
Text,
ForeignKey,
Integer,
Unicode,
DateTime,
desc,
select,
func,
UniqueConstraint,
event,
)
from sqlalchemy.schema import conv
from sqlalchemy.orm import (relationship)
from sqlalchemy.ext.declarative import declared_attr
from pyramid.httpexceptions import HTTPUnauthorized
from ..lib.abc import abstractclassmethod
from . import DiscussionBoundBase
from assembl.lib import config
from .auth import User
from ..auth.util import user_has_permission, get_permissions
from .discussion import Discussion
from .preferences import Preferences
from .idea import Idea
[docs]class AbstractNamespacedKeyValue(object):
"""Mixin class for namespace-key-value triples in a namespaced dictionaries (dict of dict)"""
# No table name, these are simply common columns
@declared_attr
def id(self):
return Column("id", Integer, primary_key=True)
@declared_attr
def namespace(self):
"""The namespace of the key-value tuple"""
return Column("namespace", String)#, index=True)
@declared_attr
def key(self):
"""The key of the key-value tuple"""
return Column("key", String)#, index=True)
@declared_attr
def value(self):
"""The value of the key-value tuple"""
return Column("value", Text)
target_name = None
target_id_name = None
target_class = None
@declared_attr
def __table_args__(cls):
schema, user = config.get('db_schema'), config.get('db_user')
return (UniqueConstraint(
getattr(cls, cls.target_id_name),
cls.namespace,
cls.key,
name=conv("%s_%s_%s_unique_constraint" % (
schema, user, cls.__tablename__))),)
@classmethod
def add_nkv(cls, target, namespace, key, value):
db = cls.default_db
args = {
"namespace": namespace,
"key": key,
cls.target_name: target
}
existing = db.query(cls).filter_by(**args).first()
if existing:
existing.value = json.dumps(value)
cls.default_db.add(cls(value=json.dumps(value), **args))
@classmethod
def delete_nk(cls, target, namespace, key):
db = cls.default_db
args = {
"namespace": namespace,
"key": key,
cls.target_name: target
}
existing = db.query(cls).filter_by(**args).first()
if existing:
existing.delete()
@classmethod
def clear_namespace(cls, target, namespace):
db = cls.default_db
args = {
"namespace": namespace,
cls.target_name: target
}
db.query(cls).filter_by(**args).delete()
[docs]class AbstractPerUserNamespacedKeyValue(
AbstractNamespacedKeyValue):
"""Mixin class for user-namespace-key-value quads in a user-local namespaced dictionaries (dict of dict)"""
# No table name, these are simply common columns
@declared_attr
def user_id(self):
"""The user of the key-value tuple"""
return Column("user_id", Integer, ForeignKey(User.id), index=True)
@declared_attr
def __table_args__(cls):
schema, user = config.get('db_schema'), config.get('db_user')
return (UniqueConstraint(
getattr(cls, cls.target_id_name),
cls.namespace,
cls.key,
cls.user_id,
name=conv("%s_%s_%s_unique_constraint" % (
schema, user, cls.__tablename__))),)
@classmethod
def add_nukv(cls, target, user, namespace, key, value):
db = cls.default_db
args = {
"user": user,
"namespace": namespace,
"key": key,
cls.target_name: target
}
existing = db.query(cls).filter_by(**args).first()
if existing:
existing.value = json.dumps(value)
cls.default_db.add(cls(value=json.dumps(value), **args))
@classmethod
def delete_nuk(cls, target, user, namespace, key):
db = cls.default_db
args = {
"user": user,
"namespace": namespace,
"key": key,
cls.target_name: target
}
existing = db.query(cls).filter_by(**args).first()
if existing:
existing.delete()
@classmethod
def clear_namespace_for_user(cls, target, user, namespace):
db = cls.default_db
args = {
"user": user,
"namespace": namespace,
cls.target_name: target
}
db.query(cls).filter_by(**args).delete()
[docs]class NamespacedUserKVCollection(MutableMapping):
"""View of the :py:class:`AbstractPerUserNamespacedKeyValue` for a given namespace as a python dict"""
def __init__(self, target, user_id, namespace):
self.target = target
self.user_id = user_id
self.namespace = namespace
def __len__(self):
ukv_cls = self.target.per_user_namespaced_kv_class
return self.target.db.query(
ukv_cls.key).filter_by(
user_id=self.user_id,
namespace=self.namespace,
**{ukv_cls.target_name: self.target}).count()
def __iter__(self):
ukv_cls = self.target.per_user_namespaced_kv_class
ns = self.target.db.query(
ukv_cls.key).filter_by(
user_id=self.user_id,
namespace=self.namespace,
**{ukv_cls.target_name: self.target})
return (x for (x,) in ns)
iterkeys = __iter__
def iteritems(self):
ukv_cls = self.target.per_user_namespaced_kv_class
kvpairs = self.target.db.query(
ukv_cls).filter_by(
user_id=self.user_id,
namespace=self.namespace,
**{ukv_cls.target_name: self.target})
return ((kvpair.key, json.loads(kvpair.value)) for kvpair in kvpairs)
def __getitem__(self, key):
ukv_cls = self.target.per_user_namespaced_kv_class
value = self.target.db.query(
ukv_cls.value).filter_by(
user_id=self.user_id,
namespace=self.namespace,
key=key,
**{ukv_cls.target_name: self.target}).first()
if not value:
raise IndexError()
(value,) = value
return json.loads(value)
def __setitem__(self, key, value):
ukv_cls = self.target.per_user_namespaced_kv_class
kvpair = self.target.db.query(
ukv_cls).filter_by(
user_id=self.user_id,
namespace=self.namespace,
key=key,
**{ukv_cls.target_name: self.target}).first()
if kvpair:
kvpair.value = json.dumps(value)
else:
self.target.db.add(ukv_cls(
user_id=self.user_id,
namespace=self.namespace,
key=key,
value=json.dumps(value),
**{ukv_cls.target_name: self.target}))
def __delitem__(self, key):
ukv_cls = self.target.per_user_namespaced_kv_class
kvpair = self.target.db.query(
ukv_cls).filter_by(
user_id=self.user_id,
namespace=self.namespace,
key=key,
**{ukv_cls.target_name: self.target}).first()
if not kvpair:
raise IndexError()
kvpair.delete()
def __contains__(self, key):
ukv_cls = self.target.per_user_namespaced_kv_class
value = self.target.db.query(
ukv_cls.id).filter_by(
user_id=self.user_id,
namespace=self.namespace,
key=key,
**{ukv_cls.target_name: self.target}).first()
return value is not None
[docs]class NamespacedKVCollection(MutableMapping):
"""View of the :py:class:`AbstractNamespacedKeyValue` for a given namespace as a python dict"""
def __init__(self, target, namespace):
self.target = target
self.namespace = namespace
def __len__(self):
kv_cls = self.target.namespaced_kv_class
return self.target.db.query(
kv_cls.key).filter_by(
namespace=self.namespace,
**{kv_cls.target_name: self.target}).count()
def __iter__(self):
kv_cls = self.target.namespaced_kv_class
ns = self.target.db.query(
kv_cls.key).filter_by(
namespace=self.namespace,
**{kv_cls.target_name: self.target})
return (x for (x,) in ns)
iterkeys = __iter__
def iteritems(self):
kv_cls = self.target.namespaced_kv_class
kvpairs = self.target.db.query(
kv_cls).filter_by(
namespace=self.namespace,
**{kv_cls.target_name: self.target})
return ((kvpair.key, json.loads(kvpair.value)) for kvpair in kvpairs)
def __getitem__(self, key):
kv_cls = self.target.namespaced_kv_class
value = self.target.db.query(
kv_cls.value).filter_by(
namespace=self.namespace,
key=key,
**{kv_cls.target_name: self.target}).first()
if not value:
raise IndexError()
(value,) = value
return json.loads(value)
def __setitem__(self, key, value):
kv_cls = self.target.namespaced_kv_class
kvpair = self.target.db.query(
kv_cls).filter_by(
namespace=self.namespace,
key=key,
**{kv_cls.target_name: self.target}).first()
if kvpair:
kvpair.value = json.dumps(value)
else:
self.target.db.add(kv_cls(
namespace=self.namespace,
key=key,
value=json.dumps(value),
**{kv_cls.target_name: self.target}))
def __delitem__(self, key):
kv_cls = self.target.namespaced_kv_class
kvpair = self.target.db.query(
kv_cls).filter_by(
namespace=self.namespace,
key=key,
**{kv_cls.target_name: self.target}).first()
if not kvpair:
raise IndexError()
kvpair.delete()
def __contains__(self, key):
kv_cls = self.target.namespaced_kv_class
value = self.target.db.query(
kv_cls.id).filter_by(
namespace=self.namespace,
key=key,
**{kv_cls.target_name: self.target}).first()
return value is not None
[docs]class UserPreferenceCollection(NamespacedUserKVCollection):
"""The 'preferences' namespace has some specific behaviour.
These are user preferences. See :py:mod:.preferences."""
PREFERENCE_NAMESPACE = "preferences"
ALLOW_OVERRIDE = "allow_user_override"
def __init__(self, user_id, discussion=None):
if discussion is None:
self.dprefs = Preferences.get_by_name()
else:
self.dprefs = discussion.preferences
self.permissions = get_permissions(user_id, discussion.id)
super(UserPreferenceCollection, self).__init__(
discussion, user_id, self.PREFERENCE_NAMESPACE)
def __len__(self):
# TODO: handle permissions
return len(self.dprefs.property_defaults)
def __setitem__(self, key, value):
if key not in Preferences.preference_data_key_set:
raise KeyError("Unknown property")
pref_data = self.dprefs.get_preference_data()
req_permission = pref_data.get(key, {}).get(
self.ALLOW_OVERRIDE, False)
if (not req_permission) or not user_has_permission(
self.target.id if self.target else None,
self.user_id, req_permission):
raise HTTPUnauthorized("Cannot edit")
self.dprefs.validate(key, value)
super(UserPreferenceCollection, self).__setitem__(key, value)
def safe_del(self, key, permissions=None):
# always safe to go back to default
del self[key]
def safe_set(self, key, value, permissions=None):
# safety built into __setitem__
self[key] = value
def __iter__(self):
# TODO: handle permissions
return self.dprefs.property_defaults.__iter__()
iterkeys = __iter__
def iteritems(self):
keys = set()
for k, v in super(UserPreferenceCollection, self).items():
keys.add(k)
yield k, v
for k, v in self.dprefs.items():
if k not in keys:
if self.dpref.can_read(k, self.permissions):
yield k, v
[docs] def items(self):
# the inherited items makes multiple requests
return list(self.iteritems())
def __getitem__(self, key):
try:
return super(UserPreferenceCollection, self).__getitem__(key)
except IndexError:
return self.dprefs.safe_get_value(key, self.permissions)
def __delitem__(self, key):
try:
return super(UserPreferenceCollection, self).__delitem__(key)
except IndexError as e:
if key not in self.dprefs:
raise e
def __contains__(self, key):
# TODO: handle permissions
return key in self.dprefs
[docs]class UserNsDict(MutableMapping):
"""The dictonary of :py:class:NamespacedUserKVCollection, indexed by namespace, as a python dict"""
def __init__(self, target, user_id):
self.target = target
self.user_id = user_id
def __len__(self):
ukv_cls = self.target.per_user_namespaced_kv_class
return self.target.db.query(
ukv_cls.namespace).filter_by(
user_id=self.user_id,
**{ukv_cls.target_name: self.target}).distinct().count()
def __iter__(self):
ukv_cls = self.target.per_user_namespaced_kv_class
ns = self.target.db.query(
ukv_cls.namespace).filter_by(
user_id=self.user_id,
**{ukv_cls.target_name: self.target}).distinct()
return (x for (x,) in ns)
iterkeys = __iter__
def iteritems(self):
ukv_cls = self.target.per_user_namespaced_kv_class
ns = self.target.db.query(
ukv_cls.namespace).filter_by(
user_id=self.user_id,
**{ukv_cls.target_name: self.target}).distinct()
return {x: NamespacedUserKVCollection(self.target, self.user_id, x)
for (x,) in ns}
def __getitem__(self, key):
return NamespacedUserKVCollection(self.target, self.user_id, key)
def __setitem__(self, key, value):
raise NotImplementedError()
def __delitem__(self, key):
ukv_cls = self.target.per_user_namespaced_kv_class
self.target.db.query(
ukv_cls).filter_by(
user_id=self.user_id,
namespace=key,
**{ukv_cls.target_name: self.target}).delete()
[docs]class NsDict(MutableMapping):
"""The dictonary of :py:class:NamespacedKVCollection, indexed by namespace, as a python dict"""
def __init__(self, target):
self.target = target
def __len__(self):
kv_cls = self.target.namespaced_kv_class
return self.target.db.query(
kv_cls.namespace).filter_by(
**{kv_cls.target_name: self.target}).distinct().count()
def __iter__(self):
kv_cls = self.target.namespaced_kv_class
ns = self.target.db.query(
kv_cls.namespace).filter_by(
**{kv_cls.target_name: self.target}).distinct()
return (x for (x,) in ns)
iterkeys = __iter__
def iteritems(self):
kv_cls = self.target.namespaced_kv_class
ns = self.target.db.query(
kv_cls.namespace).filter_by(
**{kv_cls.target_name: self.target}).distinct()
return {x: NamespacedKVCollection(self.target, x)
for (x,) in ns}
def __getitem__(self, key):
return NamespacedKVCollection(self.target, key)
def __setitem__(self, key, value):
raise NotImplementedError()
def __delitem__(self, key):
kv_cls = self.target.namespaced_kv_class
self.target.db.query(
kv_cls).filter_by(
namespace=key,
**{kv_cls.target_name: self.target}).delete()
[docs]class DiscussionPerUserNamespacedKeyValue(
DiscussionBoundBase, AbstractPerUserNamespacedKeyValue):
"""User-local namespaced dictionaries for a given discussion"""
__tablename__ = 'discussion_peruser_namespaced_key_value'
discussion_id = Column(Integer, ForeignKey(Discussion.id), index=True)
target_name = "discussion"
target_id_name = "discussion_id"
target_class = Discussion
discussion = relationship(
Discussion, backref="namespaced_peruser_key_values")
user = relationship(
User, backref="discussion_namespaced_key_values")
[docs] def get_discussion_id(self):
return self.discussion_id
[docs] def container_url(self):
return "/data/Discussion/%d/user_ns_kv" % (self.discussion_id,)
def get_default_parent_context(self, request=None, user_id=None):
return self.discussion.get_collection_context(
'user_ns_kv', request=request, user_id=user_id)
[docs] @classmethod
def get_discussion_conditions(cls, discussion_id, alias_maker=None):
return (cls.discussion_id == discussion_id, )
[docs] def unique_query(self):
query, _ = super(DiscussionPerUserNamespacedKeyValue, self
).unique_query()
query = query.filter(
AbstractNamespacedKeyValue.user_id == self.user_id,
AbstractNamespacedKeyValue.namespace == self.namespace,
AbstractNamespacedKeyValue.key == self.key)
return (query, True)
Discussion.per_user_namespaced_kv_class = DiscussionPerUserNamespacedKeyValue
[docs]class IdeaNamespacedKeyValue(
DiscussionBoundBase, AbstractNamespacedKeyValue):
"""Namespaced dictionaries for a given idea (not user-bound)"""
__tablename__ = 'idea_namespaced_key_value'
idea_id = Column(Integer, ForeignKey(Idea.id), index=True)
target_name = "idea"
target_id_name = "idea_id"
target_class = Idea
idea = relationship(
Idea, backref="namespaced_key_values")
[docs] def get_discussion_id(self):
return self.idea.discussion_id
[docs] def container_url(self):
return "/data/Discussion/%d/ideas/%d/ns_kv" % (
self.get_discussion_id(), self.idea_id)
def get_default_parent_context(self, request=None, user_id=None):
return self.idea.get_collection_context(
'ns_kv', request=request, user_id=user_id)
[docs] @classmethod
def get_discussion_conditions(cls, discussion_id, alias_maker=None):
if alias_maker is None:
idea_nskv = cls
idea_cls = Idea
else:
idea_nskv = alias_maker.alias_from_class(cls)
idea_cls = alias_maker.alias_from_relns(idea_nskv.source)
return ((idea_nskv.idea_id == idea_cls.id),
(idea_cls.discussion_id == discussion_id))
[docs] def unique_query(self):
query, _ = super(IdeaNamespacedKeyValue, self
).unique_query()
query = query.filter(
AbstractNamespacedKeyValue.namespace == self.namespace,
AbstractNamespacedKeyValue.key == self.key)
return (query, True)
Idea.namespaced_kv_class = IdeaNamespacedKeyValue