Skip to content

Commit

Permalink
Added language method onto queryset/manager
Browse files Browse the repository at this point in the history
Implemented filter on queryset/manager
Implemented order_by on queryset/manager
Fixed the double_normal fixture
Changed the tests to use the new language() API
  • Loading branch information
Jonas Obrist committed Feb 15, 2011
1 parent a5b191e commit e71c870
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 69 deletions.
173 changes: 113 additions & 60 deletions nani/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ class FieldTranslator(dict):
"""
def __init__(self, manager):
self.manager = manager
self.shared_fields = tuple(self.manager.model._meta.get_all_field_names())
self.translated_fields = tuple(self.manager.translations_model._meta.get_all_field_names())
self.shared_fields = tuple(self.manager.shared_model._meta.get_all_field_names()) + ('pk',)
self.translated_fields = tuple(self.manager.model._meta.get_all_field_names())
super(FieldTranslator, self).__init__()

def get(self, key):
Expand All @@ -26,40 +26,32 @@ def build(self, key):
else:
return key


class TranslationManager(models.Manager):
"""
Manager class for models with translated fields
"""
def __init__(self):

class TranslationMixin(QuerySet):
def __init__(self, model=None, query=None, using=None, real=None):
self._local_field_names = None
self._field_translator = None
super(TranslationManager, self).__init__()
self._real_manager = real
self._language_code = None
super(TranslationMixin, self).__init__(model=model, query=query, using=using)

#===========================================================================
# Helpers and properties
# Helpers and properties (ITNERNAL!)
#===========================================================================

@property
def translations_manager(self):
"""
Get manager of translations model
"""
return self.translations_model.objects

@property
def translations_accessor(self):
"""
Get the name of the reverse FK from the shared model
Get the (real) manager of translations model
"""
return self.model._meta.translations_accessor
return self.model.objects

@property
def translations_model(self):
def shared_model(self):
"""
Get the translations model class
Get the shared model class
"""
return self.model._meta.translations_model
return self._real_manager.model

@property
def field_translator(self):
Expand All @@ -71,10 +63,29 @@ def field_translator(self):
return self._field_translator

@property
def local_field_names(self):
def shared_local_field_names(self):
if self._local_field_names is None:
self._local_field_names = self.model._meta.get_all_field_names()
self._local_field_names = self.shared_model._meta.get_all_field_names()
return self._local_field_names

def _translate(self, *args, **kwargs):
# Translated kwargs from '<shared_field>' to 'master__<shared_field>'
# where necessary.
newkwargs = {}
for key, value in kwargs.items():
newkwargs[self.field_translator.get(key)] = value
# Translate args (Q objects) from '<shared_field>' to
# 'master_<shared_field>' where necessary.
newargs = []
for q in args:
newargs.append(self._recurse_q(q))
return newargs, newkwargs

def _translate_fieldnames(self, fieldnames):
newnames = []
for name in fieldnames:
newnames.append(self.field_translator.get(name))
return newnames

def _recurse_q(self, q):
"""
Expand All @@ -99,15 +110,11 @@ def _recurse_q(self, q):
# Queryset/Manager API
#===========================================================================

def get_queryset(self):
"""
Make sure that querysets inherit the methods on this manager (chaining)
"""
qs = super(TranslationManager, self).get_queryset()
bases = [QuerySet, TranslationManager]
new_queryset_cls = type('TranslationManagerQueryset', tuple(bases), {})
qs.__class__ = new_queryset_cls
return qs
def language(self, language_code=None):
if not language_code:
language_code = get_language()
self._language_code = language_code
return self.filter(language_code=language_code)

def create(self, **kwargs):
"""
Expand All @@ -121,11 +128,14 @@ def create(self, **kwargs):
"""
tkwargs = {}
for key in kwargs.keys():
if not key in self.local_field_names:
if not key in self.shared_local_field_names:
tkwargs[key] = kwargs.pop(key)
# Enforce the language_code kwarg
# enforce a language_code
if 'language_code' not in tkwargs:
tkwargs['language_code'] = get_language()
if self._language_code:
tkwargs['language_code'] = self._language_code
else:
tkwargs['language_code'] = get_language()
# Allow a pre-existing master to be passed, but only if no shared fields
# are given.
if 'master' in tkwargs:
Expand All @@ -135,10 +145,10 @@ def create(self, **kwargs):
)
else:
# create shared instance
shared = super(TranslationManager, self).create(**kwargs)
shared = self._real_manager.create(**kwargs)
tkwargs['master'] = shared
# create translated instance
trans = self.translations_model.objects.create(**tkwargs)
trans = self.translations_manager.create(**tkwargs)
# return combined instance
return combine(trans)

Expand All @@ -148,25 +158,30 @@ def get(self, *args, **kwargs):
combined instance.
"""
# Enforce a language_code to be used
if not 'language_code' in kwargs:
kwargs['language_code'] = get_language()
# Translated kwargs from '<shared_field>' to 'master__<shared_field>'
# where necessary.
newkwargs = {}
for key, value in kwargs.items():
newkwargs[self.field_translator.get(key)] = value
# Translate args (Q objects) from '<shared_field>' to
# 'master_<shared_field>' where necessary.
newargs = []
for q in args:
newargs.append(self._recurse_q(q))
# Enforce 'select related' onto 'mastser'
qs = self.translations_manager.select_related('master')
newargs, newkwargs = self._translate(*args, **kwargs)
# Enforce 'select related' onto 'master'
qs = self.select_related('master')
# Get the translated instance
trans = qs.get(*newargs, **newkwargs)
found = False
if 'language_code' in newkwargs:
language_code = newkwargs.pop('language_code')
qs = qs.language(language_code)
else:
for where in qs.query.where.children:
if where.children:
for child in where.children:
if child[0].field.name == 'language_code':
found = True
if not found:
qs = qs.language()
trans = QuerySet.get(qs, *newargs, **newkwargs)
# Return a combined instance
return combine(trans)

def filter(self, *args, **kwargs):
newargs, newkwargs = self._translate(*args, **kwargs)
return super(TranslationMixin, self).filter(*newargs, **newkwargs)

def aggregate(self, *args, **kwargs):
raise NotImplementedError()

Expand All @@ -193,9 +208,6 @@ def values_list(self, *fields, **kwargs):
def dates(self, field_name, kind, order='ASC'):
raise NotImplementedError()

def filter(self, *args, **kwargs):
raise NotImplementedError()

def exclude(self, *args, **kwargs):
raise NotImplementedError()

Expand All @@ -206,8 +218,12 @@ def annotate(self, *args, **kwargs):
raise NotImplementedError()

def order_by(self, *field_names):
raise NotImplementedError()

"""
Returns a new QuerySet instance with the ordering changed.
"""
fieldnames = self._translate_fieldnames(field_names)
return super(TranslationMixin, self).order_by(*fieldnames)

def reverse(self):
raise NotImplementedError()

Expand All @@ -217,6 +233,43 @@ def defer(self, *fields):
def only(self, *fields):
raise NotImplementedError()

def _clone(self, klass=None, setup=False, **kwargs):
cloned = super(TranslationMixin, self)._clone(self.__class__, setup, **kwargs)
cloned._local_field_names = self._local_field_names
cloned._field_translator = self._field_translator
cloned._language_code = self._language_code
cloned._real_manager = self._real_manager
return cloned

def __getitem__(self, item):
return super(TranslationMixin, self).__getitem__(item)

def __iter__(self):
for obj in super(TranslationManager, self):
yield combine(obj)
for obj in super(TranslationMixin, self).__iter__():
yield combine(obj)


class TranslationManager(models.Manager):
"""
Manager class for models with translated fields
"""
def language(self, language_code=None):
return self.get_query_set().language(language_code)

@property
def translations_model(self):
"""
Get the translations model class
"""
return self.model._meta.translations_model

def get_query_set(self):
"""
Make sure that querysets inherit the methods on this manager (chaining)
"""
return TranslationMixin(self.translations_model, using=self.db, real=self._real_manager)

def contribute_to_class(self, model, name):
super(TranslationManager, self).contribute_to_class(model, name)
self._real_manager = models.Manager()
self._real_manager.contribute_to_class(self.model, '_%s' % name)
2 changes: 1 addition & 1 deletion nani/test_utils/testcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,4 @@ class SingleNormalTestCase(NaniTestCase):
fixtures = ['single_normal.json']

def get_obj(self):
return Normal.objects.get(pk=1, language_code='en')
return Normal.objects.language('en').get(pk=1)
5 changes: 2 additions & 3 deletions nani/tests/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ def test_options(self):
class CreateTest(NaniTestCase):
def test_create(self):
with self.assertNumQueries(2):
en = Normal.objects.create(
en = Normal.objects.language('en').create(
shared_field="shared",
translated_field='English',
language_code='en'
)
self.assertEqual(en.shared_field, "shared")
self.assertEqual(en.translated_field, "English")
Expand Down Expand Up @@ -73,7 +72,7 @@ class BasicQueryTest(SingleNormalTestCase):
def test_basic(self):
en = self.get_obj()
with self.assertNumQueries(1):
queried = Normal.objects.get(pk=en.pk, language_code='en')
queried = Normal.objects.language('en').get(pk=en.pk)
self.assertEqual(queried.shared_field, en.shared_field)
self.assertEqual(queried.translated_field, en.translated_field)
self.assertEqual(queried.language_code, en.language_code)
Expand Down
8 changes: 4 additions & 4 deletions nani/tests/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ class FilterTests(NaniTestCase):
}

def test_simple_filter(self):
qs = Normal.objects.filter(shared_field__contains='2', language_code='en')
qs = Normal.objects.language('en').filter(shared_field__contains='2')
self.assertEqual(qs.count(), 1)
obj = qs[0]
self.assertEqual(obj.shared_field, self.data[2]['shared_field'])
self.assertEqual(obj.translated_field, self.data[2]['translated_field_en'])
qs = Normal.objects.filter(shared_field__contains='1', language_code='ja')
qs = Normal.objects.language('ja').filter(shared_field__contains='1')
self.assertEqual(qs.count(), 1)
obj = qs[0]
self.assertEqual(obj.shared_field, self.data[1]['shared_field'])
Expand Down Expand Up @@ -62,14 +62,14 @@ def test_simple_iter(self):
with LanguageOverride('en'):
with self.assertNumQueries(1):
index = 0
for obj in Normal.objects.all():
for obj in Normal.objects.language().all():
index += 1
self.assertEqual(obj.shared_field, self.data[index]['shared_field'])
self.assertEqual(obj.translated_field, self.data[index]['translated_field_en'])
with LanguageOverride('ja'):
with self.assertNumQueries(1):
index = 0
for obj in Normal.objects.all():
for obj in Normal.objects.language().all():
index += 1
self.assertEqual(obj.shared_field, self.data[index]['shared_field'])
self.assertEqual(obj.translated_field, self.data[index]['translated_field_ja'])
2 changes: 1 addition & 1 deletion testproject/fixtures/double_normal.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[{"pk": 1, "model": "app.normaltranslations", "fields": {"language_code": "en", "master": 1, "translated_field": "English1"}}, {"pk": 2, "model": "app.normaltranslations", "fields": {"language_code": "en", "master": 2, "translated_field": "English2"}}, {"pk": 3, "model": "app.normaltranslations", "fields": {"language_code": "ja", "master": 1, "translated_field": "\u00e6\u0097\u00a5\u00e6\u009c\u00ac\u00e8\u00aa\u009e\u00e4\u00b8\u0080"}}, {"pk": 4, "model": "app.normaltranslations", "fields": {"language_code": "ja", "master": 2, "translated_field": "\u00e6\u0097\u00a5\u00e6\u009c\u00ac\u00e8\u00aa\u009e\u00e4\u00ba\u008c"}}, {"pk": 1, "model": "app.normal", "fields": {"shared_field": "Shared1"}}, {"pk": 2, "model": "app.normal", "fields": {"shared_field": "Shared2"}}]
[{"pk": 1, "model": "app.normaltranslations", "fields": {"language_code": "en", "master": 1, "translated_field": "English1"}}, {"pk": 2, "model": "app.normaltranslations", "fields": {"language_code": "en", "master": 2, "translated_field": "English2"}}, {"pk": 3, "model": "app.normaltranslations", "fields": {"language_code": "ja", "master": 1, "translated_field": "\u65e5\u672c\u8a9e\u4e00"}}, {"pk": 4, "model": "app.normaltranslations", "fields": {"language_code": "ja", "master": 2, "translated_field": "\u65e5\u672c\u8a9e\u4e8c"}}, {"pk": 1, "model": "app.normal", "fields": {"shared_field": "Shared1"}}, {"pk": 2, "model": "app.normal", "fields": {"shared_field": "Shared2"}}, {"pk": 1, "model": "app.normal", "fields": {"shared_field": "Shared1"}}, {"pk": 2, "model": "app.normal", "fields": {"shared_field": "Shared2"}}]
Empty file modified testproject/manage.py
100644 → 100755
Empty file.

0 comments on commit e71c870

Please sign in to comment.