diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d83759 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.pyc +.*swp +doc/build +dist +versiontools* +build* +*.egg* diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..bd32dcd --- /dev/null +++ b/README.rst @@ -0,0 +1,112 @@ +================ +djorm-ext-hstore +================ + +Hstore module of django orm extensions package (collection of third party plugins build in one unified package). Is the library which integrates the `hstore`_ extension of PostgreSQL into Django, + +Limitations and notes +--------------------- + +- PostgreSQL's implementation of hstore has no concept of type; it stores a mapping of string keys to + string values. This library makes no attempt to coerce keys or values to strings. + + +Classes +------- + +The library provides three principal classes: + +``djorm_hstore.fields.DictionaryField`` + An ORM field which stores a mapping of string key/value pairs in an hstore column. +``djorm_hstore.fields.ReferencesField`` + An ORM field which builds on DictionaryField to store a mapping of string keys to + django object references, much like ForeignKey. +``djorm_hstor.models.HStoreManager`` + An ORM manager which provides much of the query functionality of the library. + +**NOTE**: the predefined hstore manager inherits all functionality of djorm-ext-expressions module (which is part of django orm extensions package) + + +Usage +----- + +Initially define some sample model: + +.. code-block:: python + + from django.db import models + from djorm_hstore.fields import DictionaryField + from djorm_hstore.models import HStoreManager + + class Something(models.Model): + name = models.CharField(max_length=32) + data = DictionaryField(db_index=True) + objects = HStoreManager() + + def __unicode__(self): + return self.name + + +You then treat the ``data`` field as simply a dictionary of string pairs: + +.. code-block:: python + + instance = Something.objects.create(name='something', data={'a': '1', 'b': '2'}) + assert instance.data['a'] == '1' + + empty = Something.objects.create(name='empty') + assert empty.data == {} + + empty.data['a'] = '1' + empty.save() + assert Something.objects.get(name='something').data['a'] == '1' + + +You can issue indexed queries against hstore fields: + + +.. code-block:: python + + from djorm_hstore.expressions import HstoreExpression as HE + + # equivalence + Something.objects.filter(data={'a': '1', 'b': '2'}) + + # subset by key/value mapping + Something.objects.where(HE("data").contains({'a':'1'})) + + # subset by list of keys + Something.objects.where(HE("data").contains(['a', 'b'])) + + # subset by single key + Something.objects.where(HE("data").contains("a")) + + +You can also take advantage of some db-side functionality by using the manager: + +.. code-block:: python + + # identify the keys present in an hstore field + >>> Something.objects.filter(id=1).hkeys(attr='data') + ['a', 'b'] + + # peek at a a named value within an hstore field + >>> Something.objects.filter(id=1).hpeek(attr='data', key='a') + '1' + + # remove a key/value pair from an hstore field + >>> Something.objects.filter(name='something').hremove('data', 'b') + + +In addition to filters and specific methods to retrieve keys or hstore field values, +we can also use annotations, and then we can filter for them. + +.. code-block:: python + + from djorm_hstore.functions import HstoreSlice, HstorePeek, HstoreKeys + + queryset = SomeModel.objects.annotate_functions( + sliced = HstoreSlice("hstorefield", ['v']), + peeked = HstorePeek("hstorefield", "v"), + keys = HstoreKeys("hstorefield"), + ) diff --git a/djorm_hstore/__init__.py b/djorm_hstore/__init__.py new file mode 100644 index 0000000..0256fcd --- /dev/null +++ b/djorm_hstore/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +__version__ = (4, 0, 0, 'final', 0) diff --git a/djorm_hstore/expressions.py b/djorm_hstore/expressions.py new file mode 100644 index 0000000..31f66c8 --- /dev/null +++ b/djorm_hstore/expressions.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +from djorm_expressions.base import SqlExpression + +class HstoreExpression(object): + def __init__(self, field): + self.field = field + + def contains(self, value): + if isinstance(value, dict): + expression = SqlExpression( + self.field, "@>", value + ) + elif isinstance(value, (list,tuple)): + expression = SqlExpression( + self.field, "?&", value + ) + elif isinstance(value, basestring): + expression = SqlExpression( + self.field, "?", value + ) + else: + raise ValueError("Invalid value") + return expression + + def exact(self, value): + return SqlExpression( + self.field, "=", value + ) + + def as_sql(self, qn, queryset): + raise NotImplementedError diff --git a/djorm_hstore/fields.py b/djorm_hstore/fields.py new file mode 100644 index 0000000..96b97ac --- /dev/null +++ b/djorm_hstore/fields.py @@ -0,0 +1,86 @@ + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from . import forms, util + +class HStoreDictionary(dict): + """ + A dictionary subclass which implements hstore support. + """ + def __init__(self, value=None, field=None, instance=None, **params): + super(HStoreDictionary, self).__init__(value, **params) + self.field = field + self.instance = instance + + def remove(self, keys): + """ + Removes the specified keys from this dictionary. + """ + queryset = self.instance._base_manager.get_query_set() + queryset.filter(pk=self.instance.pk).hremove(self.field.name, keys) + + +class HStoreDescriptor(models.fields.subclassing.Creator): + def __set__(self, obj, value): + value = self.field.to_python(value) + + if not isinstance(value, HStoreDictionary): + value = self.field._attribute_class(value, self.field, obj) + + obj.__dict__[self.field.name] = value + + +class HStoreField(models.Field): + _attribute_class = HStoreDictionary + _descriptor_class = HStoreDescriptor + + def contribute_to_class(self, cls, name): + super(HStoreField, self).contribute_to_class(cls, name) + setattr(cls, self.name, self._descriptor_class(self)) + + def db_type(self, connection=None): + return 'hstore' + + +class DictionaryField(HStoreField): + description = _("A python dictionary in a postgresql hstore field.") + + def formfield(self, **params): + params['form_class'] = forms.DictionaryField + return super(DictionaryField, self).formfield(**params) + + def get_prep_lookup(self, lookup, value): + return value + + def to_python(self, value): + return value or {} + + def _value_to_python(self, value): + return value + + +class ReferencesField(HStoreField): + description = _("A python dictionary of references to model instances in an hstore field.") + + def formfield(self, **params): + params['form_class'] = forms.ReferencesField + return super(ReferencesField, self).formfield(**params) + + def get_prep_lookup(self, lookup, value): + return util.serialize_references(value) if isinstance(value, dict) else value + + def get_prep_value(self, value): + return util.serialize_references(value) if value else {} + + def to_python(self, value): + return util.unserialize_references(value) if value else {} + + def _value_to_python(self, value): + return util.acquire_reference(value) if value else None + +try: + from south.modelsinspector import add_introspection_rules + add_introspection_rules(rules=[], patterns=['djorm_hstore.fields\.DictionaryField']) +except ImportError: + pass diff --git a/djorm_hstore/forms.py b/djorm_hstore/forms.py new file mode 100644 index 0000000..3cf78ad --- /dev/null +++ b/djorm_hstore/forms.py @@ -0,0 +1,45 @@ +from django.forms import Field +from django.utils import simplejson as json +from django.contrib.admin.widgets import AdminTextareaWidget + +from . import util + +class JsonMixin(object): + def to_python(self, value): + return json.loads(value) + + def render(self, name, value, attrs=None): + value = json.dumps(value, sort_keys=True, indent=2) + return super(JsonMixin, self).render(name, value, attrs) + + +class DictionaryFieldWidget(JsonMixin, AdminTextareaWidget): + pass + + +class ReferencesFieldWidget(JsonMixin, AdminTextareaWidget): + def render(self, name, value, attrs=None): + value = util.serialize_references(value) + return super(ReferencesFieldWidget, self).render(name, value, attrs) + + +class DictionaryField(JsonMixin, Field): + """ + A dictionary form field. + """ + def __init__(self, **params): + params['widget'] = DictionaryFieldWidget + super(DictionaryField, self).__init__(**params) + + +class ReferencesField(JsonMixin, Field): + """ + A references form field. + """ + def __init__(self, **params): + params['widget'] = ReferencesFieldWidget + super(ReferencesField, self).__init__(**params) + + def to_python(self, value): + value = super(ReferencesField, self).to_python(value) + return util.unserialize_references(value) diff --git a/djorm_hstore/functions.py b/djorm_hstore/functions.py new file mode 100644 index 0000000..c12935e --- /dev/null +++ b/djorm_hstore/functions.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +from djorm_expressions.base import SqlFunction + + +class HstoreSlice(SqlFunction): + """ + Obtain dictionary with only selected keys. + + Usage example:: + + queryset = SomeModel.objects\ + .inline_annotate(sliced=HstoreSlice("data").as_aggregate(['v'])) + """ + + sql_template = '%(function)s(%(field)s, %%s)' + sql_function = 'slice' + + +class HstorePeek(SqlFunction): + """ + Obtain values from hstore field. + Usage example:: + + queryset = SomeModel.objects\ + .inline_annotate(peeked=HstorePeek("data").as_aggregate("v")) + """ + + sql_template = '%(field)s -> %%s' + + +class HstoreKeys(SqlFunction): + """ + Obtain keys from hstore fields. + Usage:: + + queryset = SomeModel.objects\ + .inline_annotate(keys=HstoreKeys("somefield").as_aggregate()) + """ + + sql_function = 'akeys' diff --git a/djorm_hstore/models.py b/djorm_hstore/models.py new file mode 100644 index 0000000..cd05de1 --- /dev/null +++ b/djorm_hstore/models.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +from django.db.models.sql.constants import SINGLE +from django.db.models.query_utils import QueryWrapper +from django.db.models.query import QuerySet +from django.db import models + +from djorm_expressions.models import ExpressionQuerySetMixin, ExpressionManagerMixin + +from .query_utils import select_query, update_query + + +class HStoreQuerysetMixin(object): + @select_query + def hkeys(self, query, attr): + """ + Enumerates the keys in the specified hstore. + """ + query.add_extra({'_': 'akeys("%s")' % attr}, None, None, None, None, None) + result = query.get_compiler(self.db).execute_sql(SINGLE) + return (result[0] if result else []) + + @select_query + def hpeek(self, query, attr, key): + """ + Peeks at a value of the specified key. + """ + query.add_extra({'_': '%s -> %%s' % attr}, [key], None, None, None, None) + result = query.get_compiler(self.db).execute_sql(SINGLE) + if result and result[0]: + field = self.model._meta.get_field_by_name(attr)[0] + return field._value_to_python(result[0]) + + @select_query + def hslice(self, query, attr, keys): + """ + Slices the specified key/value pairs. + """ + query.add_extra({'_': 'slice("%s", %%s)' % attr}, [keys], None, None, None, None) + result = query.get_compiler(self.db).execute_sql(SINGLE) + if result and result[0]: + field = self.model._meta.get_field_by_name(attr)[0] + return dict((key, field._value_to_python(value)) for key, value in result[0].iteritems()) + return {} + + @update_query + def hremove(self, query, attr, keys): + """ + Removes the specified keys in the specified hstore. + """ + value = QueryWrapper('delete("%s", %%s)' % attr, [keys]) + field, model, direct, m2m = self.model._meta.get_field_by_name(attr) + query.add_update_fields([(field, None, value)]) + return query + + @update_query + def hupdate(self, query, attr, updates): + """ + Updates the specified hstore. + """ + value = QueryWrapper('"%s" || %%s' % attr, [updates]) + field, model, direct, m2m = self.model._meta.get_field_by_name(attr) + query.add_update_fields([(field, None, value)]) + return query + + +class HStoreQueryset(HStoreQuerysetMixin, ExpressionQuerySetMixin, QuerySet): + pass + + +class HStoreManagerMixin(object): + """ + Object manager which enables hstore features. + """ + use_for_related_fields = True + + def hkeys(self, attr): + return self.get_query_set().hkeys(attr) + + def hpeek(self, attr, key): + return self.get_query_set().hpeek(attr, key) + + def hslice(self, attr, keys, **params): + return self.get_query_set().hslice(attr, keys) + + +class HStoreManager(HStoreManagerMixin, ExpressionManagerMixin, models.Manager): + def get_query_set(self): + return HStoreQueryset(self.model, using=self._db) + + +# Signal attaching +from psycopg2.extras import register_hstore + +def register_hstore_handler(connection, **kwargs): + register_hstore(connection.cursor(), globally=True, unicode=True) + +from djorm_core.models import connection_handler +connection_handler.attach_handler(register_hstore_handler, vendor="postgresql", unique=True) diff --git a/djorm_hstore/query_utils.py b/djorm_hstore/query_utils.py new file mode 100644 index 0000000..c5f2df2 --- /dev/null +++ b/djorm_hstore/query_utils.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +from django.db import transaction +from django.db.models.sql.subqueries import UpdateQuery + +def select_query(method): + def selector(self, *args, **params): + query = self.query.clone() + query.default_cols = False + query.clear_select_fields() + return method(self, query, *args, **params) + return selector + + +def update_query(method): + def updater(self, *args, **params): + self._for_write = True + temporal_update_query = self.query.clone(UpdateQuery) + query = method(self, temporal_update_query, *args, **params) + + forced_managed = False + if not transaction.is_managed(using=self.db): + transaction.enter_transaction_management(using=self.db) + forced_managed = True + + try: + rows = query.get_compiler(self.db).execute_sql(None) + if forced_managed: + transaction.commit(using=self.db) + else: + transaction.commit_unless_managed(using=self.db) + finally: + if forced_managed: + transaction.leave_transaction_management(using=self.db) + + self._result_cache = None + return rows + + updater.alters_data = True + return updater diff --git a/djorm_hstore/tests/__init__.py b/djorm_hstore/tests/__init__.py new file mode 100644 index 0000000..18f738f --- /dev/null +++ b/djorm_hstore/tests/__init__.py @@ -0,0 +1,326 @@ +# -*- coding: utf-8 -*- + +from django.db import connections +from django.db.models.aggregates import Count +from django.utils.unittest import TestCase + +from ..functions import HstoreKeys, HstoreSlice, HstorePeek +from ..expressions import HstoreExpression + +from .models import DataBag, Ref, RefsBag + +class TestDictionaryField(TestCase): + def setUp(self): + DataBag.objects.all().delete() + + def _create_bags(self): + alpha = DataBag.objects.create(name='alpha', data={'v': '1', 'v2': '3'}) + beta = DataBag.objects.create(name='beta', data={'v': '2', 'v2': '4'}) + return alpha, beta + + def _create_bitfield_bags(self): + # create dictionaries with bits as dictionary keys (i.e. bag5 = { 'b0':'1', 'b2':'1'}) + for i in xrange(10): + DataBag.objects.create(name='bag%d' % (i,), + data=dict(('b%d' % (bit,), '1') for bit in xrange(4) if (1 << bit) & i)) + + def test_regression_handler(self): + self._create_bags() + from django.db import connection + connection.close() + + obj = DataBag.objects.get(name="alpha") + + + def test_empty_instantiation(self): + bag = DataBag.objects.create(name='bag') + self.assertTrue(isinstance(bag.data, dict)) + self.assertEqual(bag.data, {}) + + def test_named_querying(self): + alpha, beta = self._create_bags() + + instance = DataBag.objects.get(name='alpha') + self.assertEqual(instance, alpha) + + instance = DataBag.objects.filter(name='beta')[0] + self.assertEqual(instance, beta) + + def test_annotations(self): + self._create_bitfield_bags() + queryset = DataBag.objects\ + .annotate(num_id=Count('id'))\ + .filter(num_id=1) + + self.assertEqual(queryset[0].num_id, 1) + + def test_unicode_processing(self): + greets = { + u'de': u'Gr\xfc\xdfe, Welt', + u'en': u'hello, world', + u'es': u'hola, ma\xf1ana', + u'he': u'\u05e9\u05dc\u05d5\u05dd, \u05e2\u05d5\u05dc\u05dd', + u'jp': u'\u3053\u3093\u306b\u3061\u306f\u3001\u4e16\u754c', + u'zh': u'\u4f60\u597d\uff0c\u4e16\u754c', + } + DataBag.objects.create(name='multilang', data=greets) + + instance = DataBag.objects.get(name='multilang') + self.assertEqual(greets, instance.data) + + def test_query_escaping(self): + me = self + def readwrite(s): + # try create and query with potentially illegal characters in the field and dictionary key/value + o = DataBag.objects.create(name=s, data={ s: s }) + me.assertEqual(o, DataBag.objects.get(name=s, data={ s: s })) + + readwrite('\' select') + readwrite('% select') + readwrite('\\\' select') + readwrite('-- select') + readwrite('\n select') + readwrite('\r select') + readwrite('* select') + + def test_replace_full_dictionary(self): + DataBag.objects.create(name='foo', data={ 'change': 'old value', 'remove': 'baz'}) + + replacement = { 'change': 'new value', 'added': 'new'} + DataBag.objects.filter(name='foo').update(data=replacement) + + instance = DataBag.objects.get(name='foo') + self.assertEqual(replacement, instance.data) + + def test_equivalence_querying(self): + alpha, beta = self._create_bags() + + for bag in (alpha, beta): + data = {'v': bag.data['v'], 'v2': bag.data['v2']} + + instance = DataBag.objects.get(data=data) + self.assertEqual(instance, bag) + + r = DataBag.objects.filter(data=data) + self.assertEqual(len(r), 1) + self.assertEqual(r[0], bag) + + def test_hkeys(self): + alpha, beta = self._create_bags() + + instance = DataBag.objects.filter(id=alpha.id) + self.assertEqual(instance.hkeys('data'), ['v', 'v2']) + + instance = DataBag.objects.filter(id=beta.id) + self.assertEqual(instance.hkeys('data'), ['v', 'v2']) + + def test_hkeys_annotation(self): + alpha, beta = self._create_bags() + queryset = DataBag.objects.annotate_functions(keys=HstoreKeys("data")) + self.assertEqual(queryset[0].keys, ['v', 'v2']) + self.assertEqual(queryset[1].keys, ['v', 'v2']) + + def test_hpeek(self): + alpha, beta = self._create_bags() + + queryset = DataBag.objects.filter(id=alpha.id) + self.assertEqual(queryset.hpeek(attr='data', key='v'), '1') + self.assertEqual(queryset.hpeek(attr='data', key='invalid'), None) + + def test_hpeek_annotation(self): + alpha, beta = self._create_bags() + queryset = DataBag.objects.annotate_functions(peeked=HstorePeek("data", "v")) + self.assertEqual(queryset[0].peeked, "1") + self.assertEqual(queryset[1].peeked, "2") + + def test_hremove(self): + alpha, beta = self._create_bags() + + instance = DataBag.objects.get(name='alpha') + self.assertEqual(instance.data, alpha.data) + + DataBag.objects.filter(name='alpha').hremove('data', 'v2') + instance = DataBag.objects.get(name='alpha') + self.assertEqual(instance.data, {'v': '1'}) + + instance = DataBag.objects.get(name='beta') + self.assertEqual(instance.data, beta.data) + + DataBag.objects.filter(name='beta').hremove('data', ['v', 'v2']) + instance = DataBag.objects.get(name='beta') + self.assertEqual(instance.data, {}) + + def test_hslice(self): + alpha, beta = self._create_bags() + + queryset = DataBag.objects.filter(id=alpha.id) + self.assertEqual(queryset.hslice(attr='data', keys=['v']), {'v': '1'}) + self.assertEqual(queryset.hslice(attr='data', keys=['invalid']), {}) + + def test_hslice_annotation(self): + alpha, beta = self._create_bags() + queryset = DataBag.objects.annotate_functions(sliced=HstoreSlice("data", ['v'])) + + self.assertEqual(queryset.count(), 2) + self.assertEqual(queryset[0].sliced, {'v': '1'}) + + def test_hupdate(self): + alpha, beta = self._create_bags() + self.assertEqual(DataBag.objects.get(name='alpha').data, alpha.data) + DataBag.objects.filter(name='alpha').hupdate('data', {'v2': '10', 'v3': '20'}) + self.assertEqual(DataBag.objects.get(name='alpha').data, {'v': '1', 'v2': '10', 'v3': '20'}) + + def test_key_value_subset_querying(self): + alpha, beta = self._create_bags() + + for bag in (alpha, beta): + qs = DataBag.objects.where( + HstoreExpression("data").contains({'v': bag.data['v']}) + ) + + self.assertEqual(len(qs), 1) + self.assertEqual(qs[0], bag) + + qs = DataBag.objects.where( + HstoreExpression("data").contains({'v': bag.data['v'], 'v2': bag.data['v2']}) + ) + self.assertEqual(len(qs), 1) + self.assertEqual(qs[0], bag) + + def test_multiple_key_subset_querying(self): + alpha, beta = self._create_bags() + + for keys in (['v'], ['v', 'v2']): + qs = DataBag.objects.where( + HstoreExpression("data").contains(keys) + ) + self.assertEqual(qs.count(), 2) + + for keys in (['v', 'nv'], ['n1', 'n2']): + qs = DataBag.objects.where( + HstoreExpression("data").contains(keys) + ) + self.assertEqual(qs.count(), 0) + + def test_single_key_querying(self): + alpha, beta = self._create_bags() + for key in ('v', 'v2'): + qs = DataBag.objects.where(HstoreExpression("data").contains(key)) + self.assertEqual(qs.count(), 2) + + for key in ('n1', 'n2'): + qs = DataBag.objects.where(HstoreExpression("data").contains(key)) + self.assertEqual(qs.count(), 0) + + def test_nested_filtering(self): + self._create_bitfield_bags() + + # Test cumulative successive filters for both dictionaries and other fields + qs = DataBag.objects.all() + self.assertEqual(10, qs.count()) + + qs = qs.where(HstoreExpression("data").contains({'b0':'1'})) + self.assertEqual(5, qs.count()) + + qs = qs.where(HstoreExpression("data").contains({'b1':'1'})) + self.assertEqual(2, qs.count()) + + qs = qs.filter(name='bag3') + self.assertEqual(1, qs.count()) + + def test_aggregates(self): + self._create_bitfield_bags() + res = DataBag.objects.where(HstoreExpression("data").contains({'b0':'1'}))\ + .aggregate(Count('id')) + + self.assertEqual(res['id__count'], 5) + + def test_empty_querying(self): + bag = DataBag.objects.create(name='bag') + self.assertTrue(DataBag.objects.get(data={})) + self.assertTrue(DataBag.objects.filter(data={})) + self.assertTrue(DataBag.objects.where(HstoreExpression("data").contains({}))) + + +class TestReferencesField(TestCase): + def setUp(self): + Ref.objects.all().delete() + RefsBag.objects.all().delete() + + def _create_bags(self): + refs = [Ref.objects.create(name=str(i)) for i in range(4)] + alpha = RefsBag.objects.create(name='alpha', refs={'0': refs[0], '1': refs[1]}) + beta = RefsBag.objects.create(name='beta', refs={'0': refs[2], '1': refs[3]}) + return alpha, beta, refs + + def test_empty_instantiation(self): + bag = RefsBag.objects.create(name='bag') + self.assertTrue(isinstance(bag.refs, dict)) + self.assertEqual(bag.refs, {}) + + def test_equivalence_querying(self): + alpha, beta, refs = self._create_bags() + for bag in (alpha, beta): + refs = {'0': bag.refs['0'], '1': bag.refs['1']} + self.assertEqual(RefsBag.objects.get(refs=refs), bag) + r = RefsBag.objects.filter(refs=refs) + self.assertEqual(len(r), 1) + self.assertEqual(r[0], bag) + + def test_hkeys(self): + alpha, beta, refs = self._create_bags() + self.assertEqual(RefsBag.objects.filter(id=alpha.id).hkeys(attr='refs'), ['0', '1']) + + def test_hpeek(self): + alpha, beta, refs = self._create_bags() + self.assertEqual(RefsBag.objects.filter(id=alpha.id).hpeek(attr='refs', key='0'), refs[0]) + self.assertEqual(RefsBag.objects.filter(id=alpha.id).hpeek(attr='refs', key='invalid'), None) + + def test_hslice(self): + alpha, beta, refs = self._create_bags() + self.assertEqual(RefsBag.objects.filter(id=alpha.id).hslice(attr='refs', keys=['0']), {'0': refs[0]}) + self.assertEqual(RefsBag.objects.filter(id=alpha.id).hslice(attr='refs', keys=['invalid']), {}) + + def test_empty_querying(self): + bag = RefsBag.objects.create(name='bag') + self.assertTrue(RefsBag.objects.get(refs={})) + self.assertTrue(RefsBag.objects.filter(refs={})) + + # TODO: fix this test + #def test_key_value_subset_querying(self): + # alpha, beta, refs = self._create_bags() + # for bag in (alpha, beta): + # qs = RefsBag.objects.where( + # HstoreExpression("refs").contains({'0': bag.refs['0']}) + # ) + # self.assertEqual(len(qs), 1) + # self.assertEqual(qs[0], bag) + + # qs = RefsBag.objects.where( + # HstoreExpression("refs").contains({'0': bag.refs['0'], '1': bag.refs['1']}) + # ) + + # self.assertEqual(len(qs), 1) + # self.assertEqual(qs[0], bag) + + def test_multiple_key_subset_querying(self): + alpha, beta, refs = self._create_bags() + + for keys in (['0'], ['0', '1']): + qs = RefsBag.objects.where(HstoreExpression("refs").contains(keys)) + self.assertEqual(qs.count(), 2) + + for keys in (['0', 'nv'], ['n1', 'n2']): + qs = RefsBag.objects.where(HstoreExpression("refs").contains(keys)) + self.assertEqual(qs.count(), 0) + + def test_single_key_querying(self): + alpha, beta, refs = self._create_bags() + for key in ('0', '1'): + qs = RefsBag.objects.where(HstoreExpression("refs").contains(key)) + self.assertEqual(qs.count(), 2) + + for key in ('n1', 'n2'): + qs = RefsBag.objects.where(HstoreExpression("refs").contains(key)) + self.assertEqual(qs.count(), 0) + diff --git a/djorm_hstore/tests/models.py b/djorm_hstore/tests/models.py new file mode 100644 index 0000000..769fec8 --- /dev/null +++ b/djorm_hstore/tests/models.py @@ -0,0 +1,42 @@ + +from django.db import models + +from ..fields import DictionaryField, ReferencesField +from ..models import HStoreManager + +class Ref(models.Model): + name = models.CharField(max_length=32) + + def __unicode__(self): + return self.name + + _options = { + 'manager': False, + } + +class DataBag(models.Model): + name = models.CharField(max_length=32) + data = DictionaryField(db_index=True) + + objects = HStoreManager() + + _options = { + 'manager': False + } + + def __unicode__(self): + return self.name + +class RefsBag(models.Model): + name = models.CharField(max_length=32) + refs = ReferencesField(db_index=True) + + objects = HStoreManager() + + _options = { + 'manager': False + } + + def __unicode__(self): + return self.name + diff --git a/djorm_hstore/util.py b/djorm_hstore/util.py new file mode 100644 index 0000000..a645b03 --- /dev/null +++ b/djorm_hstore/util.py @@ -0,0 +1,36 @@ +from django.core.exceptions import ObjectDoesNotExist + +def acquire_reference(reference): + try: + implementation, identifier = reference.split(':') + module, sep, attr = implementation.rpartition('.') + implementation = getattr(__import__(module, fromlist=(attr,)), attr) + return implementation.objects.get(pk=identifier) + except ObjectDoesNotExist: + return None + except Exception: + raise ValueError + +def identify_instance(instance): + implementation = type(instance) + return '%s.%s:%s' % (implementation.__module__, implementation.__name__, instance.pk) + +def serialize_references(references): + refs = {} + for key, instance in references.iteritems(): + if not isinstance(instance, basestring): + refs[key] = identify_instance(instance) + else: + refs[key] = instance + else: + return refs + +def unserialize_references(references): + refs = {} + for key, reference in references.iteritems(): + if isinstance(reference, basestring): + refs[key] = acquire_reference(reference) + else: + refs[key] = reference + else: + return refs diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e371025 --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +from setuptools import setup, find_packages + +description=""" +Hstore module of django orm extensions package (collection of third party plugins build in one unified package). +""" + +setup( + name = "djorm-ext-hstore", + version = ':versiontools:djorm_expressions:', + url = 'https://github.com/niwibe/djorm-ext-hstore', + license = 'BSD', + platforms = ['OS Independent'], + description = description.strip(), + author = 'Andrey Antukh', + author_email = 'niwi@niwi.be', + maintainer = 'Andrey Antukh', + maintainer_email = 'niwi@niwi.be', + packages = find_packages(), + include_package_data = False, + install_requires = [ + 'djorm-ext-core >= 4.0', + 'djorm-ext-expressions >= 4.0', + ], + setup_requires = [ + 'versiontools >= 1.9', + ], + zip_safe = False, + classifiers = [ + 'Development Status :: 4 - Beta', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP', + ] +) diff --git a/testing/runtests.py b/testing/runtests.py new file mode 100644 index 0000000..04be09a --- /dev/null +++ b/testing/runtests.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +import os, sys +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") + +from django.core.management import call_command + +if __name__ == "__main__": + args = sys.argv[1:] + call_command("test", *args, verbosity=2) diff --git a/testing/settings.py b/testing/settings.py new file mode 100644 index 0000000..e30268b --- /dev/null +++ b/testing/settings.py @@ -0,0 +1,32 @@ +import os, sys + +sys.path.insert(0, '/home/niwi/devel/djorm-ext-core') +sys.path.insert(0, '/home/niwi/devel/djorm-ext-expressions') + +PROJECT_ROOT = os.path.dirname(__file__) +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'test', + 'USER': '', + 'PASSWORD': '', + 'HOST': 'localhost', + 'PORT': '', + } +} + +TIME_ZONE = 'America/Chicago' +LANGUAGE_CODE = 'en-us' +ADMIN_MEDIA_PREFIX = '/static/admin/' +STATICFILES_DIRS = () + +SECRET_KEY = 'di!n($kqa3)nd%ikad#kcjpkd^uw*h%*kj=*pm7$vbo6ir7h=l' +INSTALLED_APPS = ( + 'djorm_core', + 'djorm_expressions', + 'djorm_hstore', + 'djorm_hstore.tests', +)