gergelypolonkai-web-jekyll/_posts/2017-03-14-sqlalchemy-i18n-...

158 lines
5.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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, its actually not a problem. Well, not until
Celery comes into play.
#### --Chemical X-- Celery
Up to this point I havent used Celery for this app. When I set that up,
due to some import order madness (which I could overcome, but doesnt worth
it), my models got imported before my Celery tasks and the Celery app, so my
well-crafted solution failed miserably.
Fortunately, SQLAlchemy-i18n doesnt 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, Im not particularly happy with this solution. It works nice
with the current version of SQLAlchemy-i18n, but as soon as they switch
their implementation, Im screwed (but thats where testing comes in,
right?)