Created
January 11, 2012 09:51
-
-
Save hetsch/1593960 to your computer and use it in GitHub Desktop.
Multilingual model for appengine ndb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import functools | |
import unittest2 | |
import os | |
from google.appengine.api import datastore_errors | |
from google.appengine.ext.ndb import model | |
from google.appengine.ext.ndb import tasklets | |
from google.appengine.ext import testbed | |
#--------------------------------------- | |
# M U L T I V A L U E | |
# This snippet was born by the following discussion | |
# https://groups.google.com/forum/#!topic/appengine-ndb-discuss/b8vUNZPzZoc | |
#--------------------------------------- | |
class InvalidKeyError(Exception): | |
pass | |
class MultiValueProperty(model.PickleProperty): | |
def __init__(self, property_instance, *args, **kwargs): | |
self.property_instance = property_instance | |
super(MultiValueProperty, self).__init__(*args, **kwargs) | |
def _set_value(self, entity, value, key=None): | |
key = key or entity.curr_key | |
curr_val = self._get_user_value(entity) | |
if curr_val is None: | |
curr_val = {} | |
curr_val[key] = value | |
super(MultiValueProperty, self)._set_value(entity, curr_val) | |
def _get_value(self, entity, key=None): | |
key = key or entity.curr_key | |
curr_val = super(MultiValueProperty, self)._get_value(entity) | |
return curr_val.get(key, None) | |
def _validate(self, value): | |
if not isinstance(value, dict): | |
raise datastore_errors.BadValueError('Expected dict, got %r' % | |
(value,)) | |
# @TODO check if this is the right place to validate the values | |
# what's the difference between _validate() and _do_validate() ? | |
for key, value in value.iteritems(): | |
if self.property_instance._repeated: | |
if not isinstance(value, (list, tuple, set, frozenset)): | |
raise datastore_errors.BadValueError('Expected list or tuple, got %r' % | |
(value,)) | |
value = [self.property_instance._do_validate(v) for v in value] | |
else: | |
value = self.property_instance._do_validate(value) | |
return super(MultiValueProperty, self)._validate(self._to_base_type(value)) | |
class MultiValueMetaModel(model.MetaModel): | |
def __init__(cls, name, bases, classdict): | |
super(MultiValueMetaModel, cls).__init__(name, bases, classdict) | |
for name in set(dir(cls)): | |
attr = getattr(cls, name, None) | |
if isinstance(attr, MultiValueProperty): | |
for key in cls.VALID_KEYS: | |
key_attr_name = '%s_%s' % (name, key) | |
# creating shorthand getters and setters for language property | |
# currying the getter and setter functions | |
# seems like we have to proxy that calls over the model instance | |
# to know the instance type | |
setattr(cls, key_attr_name, property( | |
functools.partial(cls._get_key_value, attr=attr, key=key), | |
functools.partial(cls._set_key_value, attr=attr, key=key) | |
)) | |
class MultiValueModel(model.Model): | |
__metaclass__ = MultiValueMetaModel | |
VALID_KEYS = () | |
def __init__(self, curr_key=None, *args, **kwargs): | |
self.curr_key = curr_key | |
super(MultiValueModel, self).__init__(*args, **kwargs) | |
def _get_curr_key(self): | |
return self._curr_key | |
def _set_curr_key(self, value): | |
if not value in self.VALID_KEYS: | |
raise InvalidKeyError('Key is not defined in VALID_KEYS list!') | |
self._curr_key = value | |
curr_key = property(_get_curr_key, _set_curr_key) | |
# see metaclass for more info | |
def _get_key_value(self, attr=None, key=None): | |
if attr and key: | |
return attr._get_value(self, key) | |
# see metaclass for more info | |
def _set_key_value(self, value, attr=None, key=None): | |
if attr and key: | |
attr._set_value(self, value, key) | |
#--------------------------------------- | |
# M U L T I L I N G U A L | |
#--------------------------------------- | |
class InvalidLanguageError(InvalidKeyError): | |
pass | |
class MultilingualProperty(MultiValueProperty): | |
pass | |
class MultilingualModel(MultiValueModel): | |
VALID_KEYS = ('en', 'de') | |
DEFAULT_LANGUAGE = 'en' | |
def __init__(self, curr_key=DEFAULT_LANGUAGE, *args, **kwargs): | |
super(MultilingualModel, self).__init__(curr_key, *args, **kwargs) | |
def _get_language(self): | |
return self.curr_key | |
def _set_language(self, value): | |
try: | |
self.curr_key = value | |
except InvalidKeyError, e: | |
raise InvalidLanguageError('Language is not defined in VALID_KEYS list!') | |
language = property(_get_language, _set_language) | |
#--------------------------------------- | |
# T E S T I N G | |
#--------------------------------------- | |
class MultilingualModelDummy(MultilingualModel): | |
title = MultilingualProperty(model.StringProperty()) | |
class BaseTest(unittest2.TestCase): | |
def setUp(self): | |
os.environ['HTTP_HOST'] = 'localhost' | |
self.testbed = testbed.Testbed() | |
self.testbed.activate() | |
self.testbed.init_datastore_v3_stub() | |
# Only when testing ndb. | |
self.reset_kind_map() | |
self.setup_context_cache() | |
def tearDown(self): | |
self.testbed.deactivate() | |
def reset_kind_map(self): | |
model.Model._reset_kind_map() | |
def setup_context_cache(self): | |
ctx = tasklets.get_context() | |
ctx.set_cache_policy(False) | |
ctx.set_memcache_policy(False) | |
def register_model(self, name, cls): | |
model.Model._kind_map[name] = cls | |
class MutlilingualTest(BaseTest): | |
def setUp(self): | |
super(MutlilingualTest, self).setUp() | |
self.register_model('MultilingualModelDummy', MultilingualModelDummy) | |
def testInsertEntityNoRepeat(self): | |
entity = MultilingualModelDummy() | |
entity.put() | |
self.assertEqual(1, len(MultilingualModelDummy.query().fetch(2))) | |
entity.language = 'en' | |
entity.title = 'title_en' | |
entity.put() | |
db_saved = MultilingualModelDummy.query().get() | |
db_saved.language = 'en' | |
self.assertEqual('title_en', db_saved.title) | |
entity.language = 'de' | |
entity.title = 'title_de' | |
entity.put() | |
db_saved = MultilingualModelDummy.query().get() | |
db_saved.language = 'de' | |
self.assertEqual('title_de', db_saved.title) | |
with self.assertRaises(InvalidLanguageError): | |
entity.language = 'jp' | |
with self.assertRaises(AttributeError): | |
entity.title_jp | |
with self.assertRaises(datastore_errors.BadValueError): | |
entity.title = 21 | |
with self.assertRaises(datastore_errors.BadValueError): | |
entity.title = None | |
def testInsertEntityShorthandNoRepeat(self): | |
entity = MultilingualModelDummy() | |
entity.put() | |
entity.title_de = 'title_de' | |
entity.put() | |
entity.title_en = 'title_en' | |
entity.put() | |
self.assertEqual('title_de', entity.title_de) | |
self.assertEqual('title_en', entity.title_en) | |
entity.title_de = 'aaa' | |
self.assertEqual('aaa', entity.title_de) | |
self.assertEqual('title_en', entity.title_en) | |
# Testing default language | |
entity = MultilingualModelDummy() | |
self.assertEqual('en', entity.language) | |
entity.title_de = 'title_de' | |
entity.title_en = 'title_en' | |
entity.put() | |
# Testing if the model language changed after setting shorthand | |
# attributes | |
self.assertEqual('en', entity.language) | |
# Testing if setting the model language affects the attributes in | |
# the expected manner | |
entity.language = 'de' | |
entity.title = 'aaa' | |
self.assertEqual('de', entity.language) | |
self.assertEqual('aaa', entity.title) | |
self.assertEqual('aaa', entity.title_de) | |
self.assertEqual('title_en', entity.title_en) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Please leave some comments here if you use this snippet and where...
If you have any ideas how we can make the code more stable or you wanna share some ideas you are more than welcome to comment on that gist!
Regards,
Gernot