2011-03-22 05:32:52 +00:00
|
|
|
from functools import partial
|
|
|
|
|
|
|
|
from sqlalchemy.ext.associationproxy import association_proxy
|
2011-03-24 05:17:02 +00:00
|
|
|
from sqlalchemy.orm import aliased, compile_mappers, mapper, relationship, synonym
|
2011-03-22 05:32:52 +00:00
|
|
|
from sqlalchemy.orm.collections import attribute_mapped_collection
|
2011-03-30 03:15:41 +00:00
|
|
|
from sqlalchemy.orm.scoping import ScopedSession
|
2011-03-22 05:32:52 +00:00
|
|
|
from sqlalchemy.orm.session import Session, object_session
|
|
|
|
from sqlalchemy.schema import Column, ForeignKey, Table
|
2011-03-24 05:17:02 +00:00
|
|
|
from sqlalchemy.sql.expression import and_, bindparam, select
|
2011-03-22 05:32:52 +00:00
|
|
|
from sqlalchemy.types import Integer
|
|
|
|
|
|
|
|
def create_translation_table(_table_name, foreign_class, relation_name,
|
2011-03-30 01:39:37 +00:00
|
|
|
language_class, relation_lazy='select', **kwargs):
|
2011-03-22 05:32:52 +00:00
|
|
|
"""Creates a table that represents some kind of data attached to the given
|
|
|
|
foreign class, but translated across several languages. Returns the new
|
|
|
|
table's mapped class. It won't be declarative, but it will have a
|
|
|
|
`__table__` attribute so you can retrieve the Table object.
|
|
|
|
|
|
|
|
`foreign_class` must have a `__singlename__`, currently only used to create
|
|
|
|
the name of the foreign key column.
|
|
|
|
|
|
|
|
Also supports the notion of a default language, which is attached to the
|
|
|
|
session. This is English by default, for historical and practical reasons.
|
|
|
|
|
|
|
|
Usage looks like this:
|
|
|
|
|
|
|
|
class Foo(Base): ...
|
|
|
|
|
|
|
|
create_translation_table('foo_bars', Foo, 'bars',
|
|
|
|
name = Column(...),
|
|
|
|
)
|
|
|
|
|
|
|
|
# Now you can do the following:
|
|
|
|
foo.name
|
|
|
|
foo.name_map['en']
|
|
|
|
foo.foo_bars['en']
|
|
|
|
|
|
|
|
foo.name_map['en'] = "new name"
|
|
|
|
del foo.name_map['en']
|
|
|
|
|
|
|
|
q.options(joinedload(Foo.bars_local))
|
|
|
|
q.options(joinedload(Foo.bars))
|
|
|
|
|
|
|
|
The following properties are added to the passed class:
|
|
|
|
|
|
|
|
- `(relation_name)`, a relation to the new table. It uses a dict-based
|
|
|
|
collection class, where the keys are language identifiers and the values
|
|
|
|
are rows in the created tables.
|
|
|
|
- `(relation_name)_local`, a relation to the row in the new table that
|
|
|
|
matches the current default language.
|
2011-03-24 05:17:02 +00:00
|
|
|
- `(relation_name)_class`, the class created by this function.
|
2011-03-22 05:32:52 +00:00
|
|
|
|
|
|
|
Note that these are distinct relations. Even though the former necessarily
|
|
|
|
includes the latter, SQLAlchemy doesn't treat them as linked; loading one
|
|
|
|
will not load the other. Modifying both within the same transaction has
|
|
|
|
undefined behavior.
|
|
|
|
|
|
|
|
For each column provided, the following additional attributes are added to
|
|
|
|
Foo:
|
|
|
|
|
|
|
|
- `(column)_map`, an association proxy onto `foo_bars`.
|
|
|
|
- `(column)`, an association proxy onto `foo_bars_local`.
|
|
|
|
|
|
|
|
Pardon the naming disparity, but the grammar suffers otherwise.
|
|
|
|
|
|
|
|
Modifying these directly is not likely to be a good idea.
|
|
|
|
"""
|
|
|
|
# n.b.: language_class only exists for the sake of tests, which sometimes
|
|
|
|
# want to create tables entirely separate from the pokedex metadata
|
|
|
|
|
|
|
|
foreign_key_name = foreign_class.__singlename__ + '_id'
|
|
|
|
|
|
|
|
Translations = type(_table_name, (object,), {
|
2011-03-29 02:12:30 +00:00
|
|
|
'_language_identifier': association_proxy('local_language', 'identifier'),
|
2011-03-22 05:32:52 +00:00
|
|
|
})
|
2011-03-24 05:17:02 +00:00
|
|
|
|
2011-03-22 05:32:52 +00:00
|
|
|
# Create the table object
|
|
|
|
table = Table(_table_name, foreign_class.__table__.metadata,
|
|
|
|
Column(foreign_key_name, Integer, ForeignKey(foreign_class.id),
|
2011-03-29 17:44:43 +00:00
|
|
|
primary_key=True, nullable=False,
|
|
|
|
info=dict(description="ID of the %s these texts relate to" % foreign_class.__singlename__)),
|
2011-03-29 02:12:30 +00:00
|
|
|
Column('local_language_id', Integer, ForeignKey(language_class.id),
|
2011-03-29 17:44:43 +00:00
|
|
|
primary_key=True, nullable=False,
|
|
|
|
info=dict(description="Language these texts are in")),
|
2011-03-22 05:32:52 +00:00
|
|
|
)
|
|
|
|
Translations.__table__ = table
|
|
|
|
|
|
|
|
# Add ye columns
|
|
|
|
# Column objects have a _creation_order attribute in ascending order; use
|
|
|
|
# this to get the (unordered) kwargs sorted correctly
|
|
|
|
kwitems = kwargs.items()
|
|
|
|
kwitems.sort(key=lambda kv: kv[1]._creation_order)
|
|
|
|
for name, column in kwitems:
|
|
|
|
column.name = name
|
|
|
|
table.append_column(column)
|
|
|
|
|
|
|
|
# Construct ye mapper
|
|
|
|
mapper(Translations, table, properties={
|
2011-03-29 02:12:30 +00:00
|
|
|
'foreign_id': synonym(foreign_key_name),
|
|
|
|
'local_language': relationship(language_class,
|
|
|
|
primaryjoin=table.c.local_language_id == language_class.id,
|
2011-04-06 03:48:10 +00:00
|
|
|
innerjoin=True,
|
|
|
|
lazy='joined'),
|
2011-03-22 05:32:52 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
# Add full-table relations to the original class
|
2011-03-24 05:17:02 +00:00
|
|
|
# Foo.bars_table
|
|
|
|
setattr(foreign_class, relation_name + '_table', Translations)
|
2011-03-22 05:32:52 +00:00
|
|
|
# Foo.bars
|
|
|
|
setattr(foreign_class, relation_name, relationship(Translations,
|
2011-03-29 02:12:30 +00:00
|
|
|
primaryjoin=foreign_class.id == Translations.foreign_id,
|
|
|
|
collection_class=attribute_mapped_collection('local_language'),
|
2011-03-22 05:32:52 +00:00
|
|
|
))
|
|
|
|
# Foo.bars_local
|
|
|
|
# This is a bit clever; it uses bindparam() to make the join clause
|
2011-03-30 03:15:41 +00:00
|
|
|
# modifiable on the fly. db sessions know the current language and
|
|
|
|
# populate the bindparam.
|
|
|
|
# The 'dummy' value is to trick SQLA; without it, SQLA thinks this
|
|
|
|
# bindparam is just its own auto-generated clause and everything gets
|
|
|
|
# fucked up.
|
2011-03-22 05:32:52 +00:00
|
|
|
local_relation_name = relation_name + '_local'
|
|
|
|
setattr(foreign_class, local_relation_name, relationship(Translations,
|
|
|
|
primaryjoin=and_(
|
2011-03-30 03:15:41 +00:00
|
|
|
Translations.foreign_id == foreign_class.id,
|
|
|
|
Translations.local_language_id == bindparam('_default_language_id',
|
|
|
|
value='dummy', type_=Integer, required=True),
|
2011-03-22 05:32:52 +00:00
|
|
|
),
|
2011-03-30 03:15:41 +00:00
|
|
|
foreign_keys=[Translations.foreign_id, Translations.local_language_id],
|
2011-03-22 05:32:52 +00:00
|
|
|
uselist=False,
|
2011-03-30 01:39:37 +00:00
|
|
|
#innerjoin=True,
|
|
|
|
lazy=relation_lazy,
|
2011-03-22 05:32:52 +00:00
|
|
|
))
|
|
|
|
|
|
|
|
# Add per-column proxies to the original class
|
|
|
|
for name, column in kwitems:
|
|
|
|
# Class.(column) -- accessor for the default language's value
|
|
|
|
setattr(foreign_class, name,
|
|
|
|
association_proxy(local_relation_name, name))
|
|
|
|
|
|
|
|
# Class.(column)_map -- accessor for the language dict
|
|
|
|
# Need a custom creator since Translations doesn't have an init, and
|
|
|
|
# these are passed as *args anyway
|
|
|
|
def creator(language, value):
|
|
|
|
row = Translations()
|
2011-03-29 02:12:30 +00:00
|
|
|
row.local_language = language
|
2011-03-22 05:32:52 +00:00
|
|
|
setattr(row, name, value)
|
|
|
|
return row
|
|
|
|
setattr(foreign_class, name + '_map',
|
|
|
|
association_proxy(relation_name, name, creator=creator))
|
|
|
|
|
2011-03-29 16:53:16 +00:00
|
|
|
# Add to the list of translation classes
|
|
|
|
foreign_class.translation_classes.append(Translations)
|
|
|
|
|
2011-03-22 05:32:52 +00:00
|
|
|
# Done
|
|
|
|
return Translations
|
|
|
|
|
|
|
|
class MultilangSession(Session):
|
2011-04-06 04:03:41 +00:00
|
|
|
"""A tiny Session subclass that adds support for a default language.
|
|
|
|
|
|
|
|
Caller will need to assign something to `default_language` before this will
|
|
|
|
actually work.
|
|
|
|
"""
|
|
|
|
_default_language_id = 0 # Better fill this in, caller
|
2011-03-30 03:15:41 +00:00
|
|
|
|
2011-04-04 18:51:12 +00:00
|
|
|
def __init__(self, *args, **kwargs):
|
2011-04-06 04:03:41 +00:00
|
|
|
self.language_class = kwargs.pop('language_class')
|
2011-04-04 18:51:12 +00:00
|
|
|
super(MultilangSession, self).__init__(*args, **kwargs)
|
2011-03-30 03:15:41 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def default_language(self):
|
2011-04-06 04:03:41 +00:00
|
|
|
return self.query(self.language_class) \
|
|
|
|
.filter_by(id=self._default_language_id) \
|
|
|
|
.one()
|
2011-03-30 03:15:41 +00:00
|
|
|
|
|
|
|
@default_language.setter
|
|
|
|
def default_language(self, new):
|
2011-04-03 19:05:56 +00:00
|
|
|
self._default_language_id = new.id
|
2011-03-30 03:15:41 +00:00
|
|
|
|
|
|
|
@default_language.deleter
|
|
|
|
def default_language(self):
|
|
|
|
try:
|
|
|
|
del self._default_language_id
|
|
|
|
except AttributeError:
|
|
|
|
pass
|
2011-03-22 05:32:52 +00:00
|
|
|
|
|
|
|
def execute(self, clause, params=None, *args, **kwargs):
|
|
|
|
if not params:
|
|
|
|
params = {}
|
2011-03-30 03:15:41 +00:00
|
|
|
params.setdefault('_default_language_id', self._default_language_id)
|
2011-03-22 05:32:52 +00:00
|
|
|
return super(MultilangSession, self).execute(
|
|
|
|
clause, params, *args, **kwargs)
|
2011-03-30 03:15:41 +00:00
|
|
|
|
|
|
|
class MultilangScopedSession(ScopedSession):
|
|
|
|
"""Dispatches language selection to the attached Session."""
|
|
|
|
|
|
|
|
@property
|
|
|
|
def default_language(self):
|
|
|
|
return self.registry().default_language
|
|
|
|
|
|
|
|
@default_language.setter
|
|
|
|
def default_language(self, new):
|
|
|
|
self.registry().default_language = new
|