from functools import partial
from sqlalchemy.ext.associationproxy import association_proxy
-from sqlalchemy.orm import compile_mappers, mapper, relationship, synonym
+from sqlalchemy.orm import aliased, compile_mappers, mapper, relationship, synonym
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.orm.session import Session, object_session
from sqlalchemy.schema import Column, ForeignKey, Table
-from sqlalchemy.sql.expression import and_, bindparam
+from sqlalchemy.sql.expression import and_, bindparam, select
from sqlalchemy.types import Integer
def create_translation_table(_table_name, foreign_class, relation_name,
`foreign_class` must have a `__singlename__`, currently only used to create
the name of the foreign key column.
-TODO remove this requirement
Also supports the notion of a default language, which is attached to the
session. This is English by default, for historical and practical reasons.
are rows in the created tables.
- `(relation_name)_local`, a relation to the row in the new table that
matches the current default language.
+ - `(relation_name)_class`, the class created by this function.
Note that these are distinct relations. Even though the former necessarily
includes the latter, SQLAlchemy doesn't treat them as linked; loading one
# want to create tables entirely separate from the pokedex metadata
foreign_key_name = foreign_class.__singlename__ + '_id'
- # A foreign key "language_id" will clash with the language_id we naturally
- # put in every table. Rename it something else
- if foreign_key_name == 'language_id':
- # TODO change language_id below instead and rename this
- foreign_key_name = 'lang_id'
Translations = type(_table_name, (object,), {
- '_language_identifier': association_proxy('language', 'identifier'),
+ '_language_identifier': association_proxy('local_language', 'identifier'),
})
-
+
# Create the table object
table = Table(_table_name, foreign_class.__table__.metadata,
Column(foreign_key_name, Integer, ForeignKey(foreign_class.id),
primary_key=True, nullable=False),
- Column('language_id', Integer, ForeignKey(language_class.id),
+ Column('local_language_id', Integer, ForeignKey(language_class.id),
primary_key=True, nullable=False),
)
Translations.__table__ = table
# Construct ye mapper
mapper(Translations, table, properties={
- # TODO change to foreign_id
- 'object_id': synonym(foreign_key_name),
- # TODO change this as appropriate
- 'language': relationship(language_class,
- primaryjoin=table.c.language_id == language_class.id,
+ 'foreign_id': synonym(foreign_key_name),
+ 'local_language': relationship(language_class,
+ primaryjoin=table.c.local_language_id == language_class.id,
lazy='joined',
innerjoin=True),
- # TODO does this need to join to the original table?
})
# Add full-table relations to the original class
+ # Foo.bars_table
+ setattr(foreign_class, relation_name + '_table', Translations)
# Foo.bars
setattr(foreign_class, relation_name, relationship(Translations,
- primaryjoin=foreign_class.id == Translations.object_id,
- collection_class=attribute_mapped_collection('language'),
+ primaryjoin=foreign_class.id == Translations.foreign_id,
+ collection_class=attribute_mapped_collection('local_language'),
# TODO
lazy='select',
))
# Foo.bars_local
# This is a bit clever; it uses bindparam() to make the join clause
# modifiable on the fly. db sessions know the current language identifier
- # populates the bindparam.
+ # populates the bindparam. The manual alias and join are (a) to make the
+ # condition nice (sqla prefers an EXISTS) and to make the columns play nice
+ # when foreign_class == language_class.
local_relation_name = relation_name + '_local'
+ language_class_a = aliased(language_class)
setattr(foreign_class, local_relation_name, relationship(Translations,
primaryjoin=and_(
- foreign_class.id == Translations.object_id,
- Translations._language_identifier ==
- bindparam('_default_language', required=True),
+ foreign_class.id == Translations.foreign_id,
+ Translations.local_language_id == select(
+ [language_class_a.id],
+ language_class_a.identifier ==
+ bindparam('_default_language', required=True),
+ ),
),
uselist=False,
# TODO MORESO HERE
# these are passed as *args anyway
def creator(language, value):
row = Translations()
- row.language = language
+ row.local_language = language
setattr(row, name, value)
return row
setattr(foreign_class, name + '_map',
return Translations
class MultilangSession(Session):
- """A tiny Session subclass that adds support for a default language.
-
- Change the default_language attribute to whatever language's IDENTIFIER you
- would like to be the default.
- """
+ """A tiny Session subclass that adds support for a default language."""
default_language = 'en'
def execute(self, clause, params=None, *args, **kwargs):