- 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,
- 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.default_translation))
- q.options(joinedload(Foo.foo_bars))
-
- In the above example, the following attributes are added to Foo:
-
- - `foo_bars`, 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.
- - `foo_bars_local`, a relation to the row in the new table that matches the
- current default language.
-
- 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'
- # 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'),
- })
-
- # 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),
- )
-
- # 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={
- # TODO change to foreign_id
- 'object_id': synonym(foreign_key_name),
- # TODO change this as appropriate
- 'language': relation(_language_class,
- primaryjoin=table.c.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
- # Class.foo_bars
- class LanguageMapping(MappedCollection):
- """Baby class that converts a language identifier key into an actual
- language object, allowing for `foo.bars['en'] = Translations(...)`.
-
- Needed for per-column association proxies to function as setters.
- """
- @collection.internally_instrumented
- def __setitem__(self, key, value, _sa_initiator=None):
- if key in self:
- raise NotImplementedError("Can't replace the whole row, sorry!")
-
- # Only do this nonsense if the value is a dangling object; if it's
- # in the db it already has its language_id
- if not object_session(value):
- # This took quite some source-diving to find, but it oughta be
- # the object that actually owns this collection.
- obj = collection_adapter(self).owner_state.obj()
- session = object_session(obj)
- value.language = session.query(_language_class) \
- .filter_by(identifier=key).one()
-
- super(LanguageMapping, self).__setitem__(key, value, _sa_initiator)
-
- setattr(foreign_class, _table_name, relation(Translations,
- primaryjoin=foreign_class.id == Translations.object_id,
- #collection_class=attribute_mapped_collection('_language_identifier'),
- collection_class=partial(LanguageMapping,
- lambda obj: obj._language_identifier),
- # TODO
- lazy='select',
- ))
- # Class.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.
- local_relation_name = _table_name + '_local'
- setattr(foreign_class, local_relation_name, relation(Translations,
- primaryjoin=and_(
- foreign_class.id == Translations.object_id,
- Translations._language_identifier ==
- bindparam('_default_language', required=True),
- ),
- uselist=False,
- # TODO MORESO HERE
- lazy='select',
- ))
-
- # Add per-column proxies to the original class
- for name, column in kwitems:
- # TODO should these proxies be mutable?
-
- # 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_code, value):
- row = Translations()
- setattr(row, name, value)
- return row
- setattr(foreign_class, name + '_map',
- association_proxy(_table_name, name, creator=creator))
-
- # Done
- return Translations
-
-def makeTextTable(foreign_table_class, table_suffix_plural, table_suffix_singular, columns, lazy, Language=Language):
- # With "Language", we'd have two language_id. So, rename one to 'lang'
- foreign_key_name = foreign_table_class.__singlename__
- if foreign_key_name == 'language':
- foreign_key_name = 'lang'
-
- table_name = foreign_table_class.__singlename__ + '_' + table_suffix_plural
-
- class TranslatedStringsTable(object):
- __tablename__ = table_name
- _attrname = table_suffix_plural
- _language_identifier = association_proxy('language', 'identifier')
-
- for column_name, column_name_plural, column in columns:
- column.name = column_name
- if not column.nullable:
- # A Python side default value, so that the strings can be set
- # one by one without the DB complaining about missing values
- column.default = ColumnDefault(u'')
-
- table = Table(table_name, foreign_table_class.__table__.metadata,
- Column(foreign_key_name + '_id', Integer, ForeignKey(foreign_table_class.id),
- primary_key=True, nullable=False),
- Column('language_id', Integer, ForeignKey(Language.id),
- primary_key=True, index=True, nullable=False),
- *(column for name, plural, column in columns)
- )
-
- mapper(TranslatedStringsTable, table,
- properties={
- "object_id": synonym(foreign_key_name + '_id'),
- "language": relation(Language,
- primaryjoin=table.c.language_id == Language.id,
- ),
- foreign_key_name: relation(foreign_table_class,
- primaryjoin=(foreign_table_class.id == table.c[foreign_key_name + "_id"]),
- backref=backref(table_suffix_plural,
- collection_class=attribute_mapped_collection('_language_identifier'),
- lazy=lazy,
- ),
- ),
- },
- )
-
- # The relation to the object
- TranslatedStringsTable.object = getattr(TranslatedStringsTable, foreign_key_name)
-
- # Link the tables themselves, so we can get them if needed
- TranslatedStringsTable.foreign_table_class = foreign_table_class
- setattr(foreign_table_class, table_suffix_singular + '_table', TranslatedStringsTable)
-
- for column_name, column_name_plural, column in columns:
- # Provide a property with all the names, and an English accessor
- # for backwards compatibility
- def text_string_creator(language_code, string):
- row = TranslatedStringsTable()
- row._language_identifier = language_code
- setattr(row, column_name, string)
- return row
-
- setattr(foreign_table_class, column_name_plural,
- association_proxy(table_suffix_plural, column_name, creator=text_string_creator))
- setattr(foreign_table_class, column_name, DefaultLangProperty(column_name_plural))