Fix ancient bug with Pursuit description. #569
[zzz-pokedex.git] / pokedex / db / multilang.py
index e2d9009..8ac1bfc 100644 (file)
@@ -1,11 +1,11 @@
 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,
@@ -17,7 +17,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 +47,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,21 +68,16 @@ 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),
+        Column('local_language_id', Integer, ForeignKey(language_class.id),
             primary_key=True, nullable=False),
     )
     Translations.__table__ = table
@@ -98,34 +93,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,
+        '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
@@ -143,7 +143,7 @@ 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',
@@ -153,11 +153,7 @@ TODO remove this requirement
     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):