From aca0cc49be8f626073d781f570db4f1bfd1efe42 Mon Sep 17 00:00:00 2001 From: Gergely Polonkai Date: Fri, 3 Aug 2018 06:42:13 +0200 Subject: [PATCH] Add post about SQLAlchemy-i18n --- ...alchemy-i18n-application-factory-celery.md | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 _posts/2017-03-14-sqlalchemy-i18n-application-factory-celery.md diff --git a/_posts/2017-03-14-sqlalchemy-i18n-application-factory-celery.md b/_posts/2017-03-14-sqlalchemy-i18n-application-factory-celery.md new file mode 100644 index 0000000..bb7bdf1 --- /dev/null +++ b/_posts/2017-03-14-sqlalchemy-i18n-application-factory-celery.md @@ -0,0 +1,157 @@ +--- +layout: post +title: "SQLAlchemy-i18n with application factory, dynamic locales, and Celery" +date: 2017-03-14 00:25:52+01:00 +tags: [development, python, flask] +published: true +author: + name: Gergely Polonkai + email: gergely@polonkai.eu +--- + +[SQLAlchemy-i18n](https://sqlalchemy-i18n.readthedocs.io/) is an awesome +project if you need to make some of your model fields translatable. +However, it is only usable if your application class can be direectly +instantiated; if you use +[application factories](http://flask.pocoo.org/docs/0.12/patterns/appfactories/) and +you have the available locales in your config, you are on your own. + +#### SQLAlchemy-i18n basics + +According to the QuickStart section of the SQLAlchemy-i18n documentation, +you have to initialize the module like this: + +```python +from flask_babel import get_locale +from sqlalchemy_i18n import make_translatable, translation_base, Translatable +import sqlalchemy_utils + + +make_translatable(options={'locales': ['fi', 'en']}) + + +class Article(Translatable, Base): + __tablename__ = 'articles' + __translatable__ = {'locales': ['fi', 'en']} # Available locales + locale = 'en' # Default locale + + id = sa.Column(sa.Integer, primary_key=True) + author = sa.Column(sa.Unicode(255)) + + +class ArticleTranslation(translation_base(Article)): + __tablename__ = 'article_translations' + + name = sa.Column(sa.Unicode(255)) + content = sa.Column(sa.UnicodeText) +``` + +#### Add application factories to the mixture + +In one of my current projects, I have the following config (excerpt): + +```python +from flask_babel import lazy_gettext as _ + +class Config: + AVAILABLE_LOCALES = { + 'en': _('English') + 'hu': _('Hungarian') + } + DEFAULT_LOCALE = 'en' +``` + +And the following application factory (again, excerpt): + +```python +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +from app import models + +def create_app(config_name): + app = Flask(__name__) + db.init_app(app) +``` + +Unfortunately, adding the `make_translatable` call to `create_app` and +leaving the `__translatable__` definition (assuming the options of the +`make_translatable` are the default) is not working (its value is not a +default for models), so here is what I came up with: + +```python +from sqlalchemy_i18n import Translatable, translation_base + +from app import db + + +class Article(Translatable, db.Model): + __tablename__ = 'articles' + __translatable__ = { + 'locales': current_app.config['AVAILABLE_LOCALES'], + } + locale = current_app.config['DEFAULT_LOCALE'] + + id = db.Column(sa.Integer, primary_key=True) + author = sa.Column(sa.Unicode(255)) +``` + +This brings up a problem: my models have to be initialized with an active +app context. Here is a solution for that: + + from config import config + # I have removed the models import from here + + def create_app(config_name): + app = Flask(__name__) + app.config.from_object(config[config_name]) + + make_translatable(options={ + 'locales': list(app.config['AVAILABLE_LOCALES'].keys()), + }) + + with app.app_context(): + # Import models so they are registered with SQLAlchemy + from app import models + +The only drawback I found is that I cannot import my models without creating +the app first; however, given I need to instantiate the app to have a +working database connection, it’s actually not a problem. Well, not until +Celery comes into play. + +#### --Chemical X-- Celery + +Up to this point I haven’t used Celery for this app. When I set that up, +due to some import order madness (which I could overcome, but doesn’t worth +it), my models got imported before my Celery tasks and the Celery app, so my +well-crafted solution failed miserably. + +Fortunately, SQLAlchemy-i18n doesn’t tamper with metaclasses neither does +monkey patching on my model classes. I only have to extend `Translatable`, +and the magic happens on its own. There are two properties `Translatable` +needs: `__translatable__`, a dictionary which can, among other things, +define the available locales (languages) for the model, and `locale`, which +sets the default locale. As there are no tricky metaclasses are involved, I +could easily set these properties during app initialization time: + + from app import models + + + def create_app(config): + app = Flask(__name__) + + # Iterate over everything in the models module + for _, member in models.__dict__.items(): + # We look for subclasses of Translatable, but not Translatable + # itself + if isinstance(member, type) and \ + issubclass(member, Translatable) and \ + member != Translatable: + member.__translatable__['locales'] = list(app.config['AVAILABLE_LOCALES'].keys()) + member.locale = app.config['DEFAULT_LOCALE'] + +Honestly, I’m not particularly happy with this solution. It works nice +with the current version of SQLAlchemy-i18n, but as soon as they switch +their implementation, I’m screwed (but that’s where testing comes in, +right?)