X-Git-Url: http://git.veekun.com/zzz-pokedex.git/blobdiff_plain/ec65b5ae366174912cedf0e263d425378220d70b..8f47c0412b68c1c8499a08623eea2f58377bbab8:/pokedex/db/multilang.py diff --git a/pokedex/db/multilang.py b/pokedex/db/multilang.py index e2d9009..d58f6df 100644 --- a/pokedex/db/multilang.py +++ b/pokedex/db/multilang.py @@ -1,15 +1,16 @@ 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.scoping import ScopedSession 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, - language_class, **kwargs): + language_class, relation_lazy='select', **kwargs): """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 @@ -17,7 +18,6 @@ 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. @@ -48,6 +48,7 @@ TODO remove this requirement 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 @@ -68,22 +69,19 @@ TODO remove this requirement # 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), - primary_key=True, nullable=False), + primary_key=True, nullable=False, + info=dict(description="ID of the %s these texts relate to" % foreign_class.__singlename__)), + Column('local_language_id', Integer, ForeignKey(language_class.id), + primary_key=True, nullable=False, + info=dict(description="Language these texts are in")), ) Translations.__table__ = table @@ -98,38 +96,39 @@ TODO remove this requirement # 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, - lazy='joined', - innerjoin=True), - # TODO does this need to join to the original table? + 'foreign_id': synonym(foreign_key_name), + 'local_language': relationship(language_class, + primaryjoin=table.c.local_language_id == language_class.id, + innerjoin=True, + lazy='joined'), }) # 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'), - # TODO - lazy='select', + primaryjoin=foreign_class.id == Translations.foreign_id, + collection_class=attribute_mapped_collection('local_language'), )) # 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. + # 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. local_relation_name = relation_name + '_local' setattr(foreign_class, local_relation_name, relationship(Translations, primaryjoin=and_( - foreign_class.id == Translations.object_id, - Translations._language_identifier == - bindparam('_default_language', required=True), + Translations.foreign_id == foreign_class.id, + Translations.local_language_id == bindparam('_default_language_id', + value='dummy', type_=Integer, required=True), ), + foreign_keys=[Translations.foreign_id, Translations.local_language_id], uselist=False, - # TODO MORESO HERE - lazy='select', + #innerjoin=True, + lazy=relation_lazy, )) # Add per-column proxies to the original class @@ -143,26 +142,61 @@ TODO remove this requirement # 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', association_proxy(relation_name, name, creator=creator)) + # Add to the list of translation classes + foreign_class.translation_classes.append(Translations) + # Done 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. + Caller will need to assign something to `default_language` before this will + actually work. """ - default_language = 'en' + _default_language_id = 0 # Better fill this in, caller + + def __init__(self, *args, **kwargs): + self.language_class = kwargs.pop('language_class') + super(MultilangSession, self).__init__(*args, **kwargs) + + @property + def default_language(self): + return self.query(self.language_class) \ + .filter_by(id=self._default_language_id) \ + .one() + + @default_language.setter + def default_language(self, new): + self._default_language_id = new.id + + @default_language.deleter + def default_language(self): + try: + del self._default_language_id + except AttributeError: + pass def execute(self, clause, params=None, *args, **kwargs): if not params: params = {} - params.setdefault('_default_language', self.default_language) + params.setdefault('_default_language_id', self._default_language_id) return super(MultilangSession, self).execute( clause, params, *args, **kwargs) + +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