mirror of
https://github.com/veekun/pokedex.git
synced 2024-08-20 18:16:34 +00:00
Started switching to create_translation_table.
- Moved the function to its own file. - Implemented the session-based default language switching. - Migrated a couple tables.
This commit is contained in:
parent
6a9172151a
commit
68e14e663e
4 changed files with 232 additions and 192 deletions
|
@ -2,6 +2,7 @@ from sqlalchemy import MetaData, Table, engine_from_config, orm
|
||||||
|
|
||||||
from ..defaults import get_default_db_uri
|
from ..defaults import get_default_db_uri
|
||||||
from .tables import metadata
|
from .tables import metadata
|
||||||
|
from .multilang import MultilangSession
|
||||||
|
|
||||||
|
|
||||||
def connect(uri=None, session_args={}, engine_args={}, engine_prefix=''):
|
def connect(uri=None, session_args={}, engine_args={}, engine_prefix=''):
|
||||||
|
@ -40,7 +41,7 @@ def connect(uri=None, session_args={}, engine_args={}, engine_prefix=''):
|
||||||
|
|
||||||
all_session_args = dict(autoflush=True, autocommit=False, bind=engine)
|
all_session_args = dict(autoflush=True, autocommit=False, bind=engine)
|
||||||
all_session_args.update(session_args)
|
all_session_args.update(session_args)
|
||||||
sm = orm.sessionmaker(**all_session_args)
|
sm = orm.sessionmaker(class_=MultilangSession, **all_session_args)
|
||||||
session = orm.scoped_session(sm)
|
session = orm.scoped_session(sm)
|
||||||
|
|
||||||
return session
|
return session
|
||||||
|
|
168
pokedex/db/multilang.py
Normal file
168
pokedex/db/multilang.py
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
|
from sqlalchemy.orm import 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.types import Integer
|
||||||
|
|
||||||
|
def create_translation_table(_table_name, foreign_class, relation_name,
|
||||||
|
language_class, **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
|
||||||
|
`__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.
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
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={
|
||||||
|
# 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?
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add full-table relations to the original class
|
||||||
|
# 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',
|
||||||
|
))
|
||||||
|
# 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 = 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),
|
||||||
|
),
|
||||||
|
uselist=False,
|
||||||
|
# TODO MORESO HERE
|
||||||
|
lazy='select',
|
||||||
|
))
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
row.language = language
|
||||||
|
setattr(row, name, value)
|
||||||
|
return row
|
||||||
|
setattr(foreign_class, name + '_map',
|
||||||
|
association_proxy(relation_name, name, creator=creator))
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
"""
|
||||||
|
default_language = 'en'
|
||||||
|
|
||||||
|
def execute(self, clause, params=None, *args, **kwargs):
|
||||||
|
if not params:
|
||||||
|
params = {}
|
||||||
|
params.setdefault('_default_language', self.default_language)
|
||||||
|
return super(MultilangSession, self).execute(
|
||||||
|
clause, params, *args, **kwargs)
|
|
@ -47,7 +47,7 @@ from sqlalchemy.schema import ColumnDefault
|
||||||
from sqlalchemy.types import *
|
from sqlalchemy.types import *
|
||||||
from inspect import isclass
|
from inspect import isclass
|
||||||
|
|
||||||
from pokedex.db import markdown
|
from pokedex.db import markdown, multilang
|
||||||
|
|
||||||
# A list of all table classes will live in table_classes
|
# A list of all table classes will live in table_classes
|
||||||
table_classes = []
|
table_classes = []
|
||||||
|
@ -113,6 +113,30 @@ class TextColumn(LanguageSpecificColumn):
|
||||||
"""A column that will appear in the corresponding _text table"""
|
"""A column that will appear in the corresponding _text table"""
|
||||||
|
|
||||||
|
|
||||||
|
### Need Language first, to create the partial() below
|
||||||
|
|
||||||
|
class Language(TableBase):
|
||||||
|
u"""A language the Pokémon games have been transleted into
|
||||||
|
"""
|
||||||
|
__tablename__ = 'languages'
|
||||||
|
__singlename__ = 'language'
|
||||||
|
id = Column(Integer, primary_key=True, nullable=False,
|
||||||
|
info=dict(description="A numeric ID"))
|
||||||
|
iso639 = Column(Unicode(2), nullable=False,
|
||||||
|
info=dict(description="The two-letter code of the country where this language is spoken. Note that it is not unique.", format='identifier'))
|
||||||
|
iso3166 = Column(Unicode(2), nullable=False,
|
||||||
|
info=dict(description="The two-letter code of the language. Note that it is not unique.", format='identifier'))
|
||||||
|
identifier = Column(Unicode(16), nullable=False,
|
||||||
|
info=dict(description="An identifier", format='identifier'))
|
||||||
|
official = Column(Boolean, nullable=False, index=True,
|
||||||
|
info=dict(description=u"True iff games are produced in the language."))
|
||||||
|
order = Column(Integer, nullable=True,
|
||||||
|
info=dict(description=u"Order for sorting in foreign name lists."))
|
||||||
|
name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
|
||||||
|
info=dict(description="The name", format='plaintext', official=True))
|
||||||
|
|
||||||
|
create_translation_table = partial(multilang.create_translation_table, language_class=Language)
|
||||||
|
|
||||||
### The actual tables
|
### The actual tables
|
||||||
|
|
||||||
class Ability(TableBase):
|
class Ability(TableBase):
|
||||||
|
@ -126,12 +150,17 @@ class Ability(TableBase):
|
||||||
info=dict(description="An identifier", format='identifier'))
|
info=dict(description="An identifier", format='identifier'))
|
||||||
generation_id = Column(Integer, ForeignKey('generations.id'), nullable=False,
|
generation_id = Column(Integer, ForeignKey('generations.id'), nullable=False,
|
||||||
info=dict(description="The ID of the generation this ability was introduced in", detail=True))
|
info=dict(description="The ID of the generation this ability was introduced in", detail=True))
|
||||||
effect = ProseColumn(markdown.MarkdownColumn(5120), plural='effects', nullable=False,
|
|
||||||
info=dict(description="A detailed description of this ability's effect", format='markdown'))
|
create_translation_table('ability_texts', Ability, 'names',
|
||||||
short_effect = ProseColumn(markdown.MarkdownColumn(255), plural='short_effects', nullable=False,
|
name = Column(Unicode(24), nullable=False, index=True,
|
||||||
info=dict(description="A short summary of this ability's effect", format='markdown'))
|
info=dict(description="The name", format='plaintext', official=True)),
|
||||||
name = TextColumn(Unicode(24), nullable=False, index=True, plural='names',
|
)
|
||||||
info=dict(description="The name", format='plaintext', official=True))
|
create_translation_table('ability_prose', Ability, 'prose',
|
||||||
|
effect = Column(markdown.MarkdownColumn(5120), nullable=False,
|
||||||
|
info=dict(description="A detailed description of this ability's effect", format='markdown')),
|
||||||
|
short_effect = Column(markdown.MarkdownColumn(255), nullable=False,
|
||||||
|
info=dict(description="A short summary of this ability's effect", format='markdown')),
|
||||||
|
)
|
||||||
|
|
||||||
class AbilityChangelog(TableBase):
|
class AbilityChangelog(TableBase):
|
||||||
"""History of changes to abilities across main game versions."""
|
"""History of changes to abilities across main game versions."""
|
||||||
|
@ -550,26 +579,6 @@ class ItemPocket(TableBase):
|
||||||
name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
|
name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
|
||||||
info=dict(description="The name", format='plaintext', official=True))
|
info=dict(description="The name", format='plaintext', official=True))
|
||||||
|
|
||||||
class Language(TableBase):
|
|
||||||
u"""A language the Pokémon games have been transleted into
|
|
||||||
"""
|
|
||||||
__tablename__ = 'languages'
|
|
||||||
__singlename__ = 'language'
|
|
||||||
id = Column(Integer, primary_key=True, nullable=False,
|
|
||||||
info=dict(description="A numeric ID"))
|
|
||||||
iso639 = Column(Unicode(2), nullable=False,
|
|
||||||
info=dict(description="The two-letter code of the country where this language is spoken. Note that it is not unique.", format='identifier'))
|
|
||||||
iso3166 = Column(Unicode(2), nullable=False,
|
|
||||||
info=dict(description="The two-letter code of the language. Note that it is not unique.", format='identifier'))
|
|
||||||
identifier = Column(Unicode(16), nullable=False,
|
|
||||||
info=dict(description="An identifier", format='identifier'))
|
|
||||||
official = Column(Boolean, nullable=False, index=True,
|
|
||||||
info=dict(description=u"True iff games are produced in the language."))
|
|
||||||
order = Column(Integer, nullable=True,
|
|
||||||
info=dict(description=u"Order for sorting in foreign name lists."))
|
|
||||||
name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
|
|
||||||
info=dict(description="The name", format='plaintext', official=True))
|
|
||||||
|
|
||||||
class Location(TableBase):
|
class Location(TableBase):
|
||||||
u"""A place in the Pokémon world
|
u"""A place in the Pokémon world
|
||||||
"""
|
"""
|
||||||
|
@ -869,8 +878,12 @@ class Move(TableBase):
|
||||||
info=dict(description="ID of the move's Contest effect"))
|
info=dict(description="ID of the move's Contest effect"))
|
||||||
super_contest_effect_id = Column(Integer, ForeignKey('super_contest_effects.id'), nullable=True,
|
super_contest_effect_id = Column(Integer, ForeignKey('super_contest_effects.id'), nullable=True,
|
||||||
info=dict(description="ID of the move's Super Contest effect"))
|
info=dict(description="ID of the move's Super Contest effect"))
|
||||||
name = TextColumn(Unicode(24), nullable=False, index=True, plural='names',
|
|
||||||
|
create_translation_table('move_texts', Move, 'names',
|
||||||
|
name = Column(Unicode(24), nullable=False, index=True,
|
||||||
info=dict(description="The name", format='plaintext', official=True))
|
info=dict(description="The name", format='plaintext', official=True))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MoveChangelog(TableBase):
|
class MoveChangelog(TableBase):
|
||||||
"""History of changes to moves across main game versions."""
|
"""History of changes to moves across main game versions."""
|
||||||
|
@ -992,9 +1005,6 @@ class Pokemon(TableBase):
|
||||||
info=dict(description=u"The height of the Pokémon, in decimeters (tenths of a meter)"))
|
info=dict(description=u"The height of the Pokémon, in decimeters (tenths of a meter)"))
|
||||||
weight = Column(Integer, nullable=False,
|
weight = Column(Integer, nullable=False,
|
||||||
info=dict(description=u"The weight of the Pokémon, in tenths of a kilogram (decigrams)"))
|
info=dict(description=u"The weight of the Pokémon, in tenths of a kilogram (decigrams)"))
|
||||||
species = TextColumn(Unicode(16), nullable=False, plural='species_names',
|
|
||||||
info=dict(description=u'The short flavor text, such as "Seed" or "Lizard"; usually affixed with the word "Pokémon"',
|
|
||||||
official=True, format='plaintext'))
|
|
||||||
color_id = Column(Integer, ForeignKey('pokemon_colors.id'), nullable=False,
|
color_id = Column(Integer, ForeignKey('pokemon_colors.id'), nullable=False,
|
||||||
info=dict(description=u"ID of this Pokémon's Pokédex color, as used for a gimmick search function in the games."))
|
info=dict(description=u"ID of this Pokémon's Pokédex color, as used for a gimmick search function in the games."))
|
||||||
pokemon_shape_id = Column(Integer, ForeignKey('pokemon_shapes.id'), nullable=True,
|
pokemon_shape_id = Column(Integer, ForeignKey('pokemon_shapes.id'), nullable=True,
|
||||||
|
@ -1017,8 +1027,6 @@ class Pokemon(TableBase):
|
||||||
info=dict(description=u"Set iff the species exhibits enough sexual dimorphism to have separate sets of sprites in Gen IV and beyond."))
|
info=dict(description=u"Set iff the species exhibits enough sexual dimorphism to have separate sets of sprites in Gen IV and beyond."))
|
||||||
order = Column(Integer, nullable=False, index=True,
|
order = Column(Integer, nullable=False, index=True,
|
||||||
info=dict(description=u"Order for sorting. Almost national order, except families and forms are grouped together."))
|
info=dict(description=u"Order for sorting. Almost national order, except families and forms are grouped together."))
|
||||||
name = TextColumn(Unicode(20), nullable=False, index=True, plural='names',
|
|
||||||
info=dict(description="The name", format='plaintext', official=True))
|
|
||||||
|
|
||||||
### Stuff to handle alternate Pokémon forms
|
### Stuff to handle alternate Pokémon forms
|
||||||
|
|
||||||
|
@ -1102,6 +1110,14 @@ class Pokemon(TableBase):
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
create_translation_table('pokemon_texts', Pokemon, 'names',
|
||||||
|
name = Column(Unicode(20), nullable=False, index=True,
|
||||||
|
info=dict(description="The name", format='plaintext', official=True)),
|
||||||
|
species = Column(Unicode(16), nullable=False,
|
||||||
|
info=dict(description=u'The short flavor text, such as "Seed" or "Lizard"; usually affixed with the word "Pokémon"',
|
||||||
|
official=True, format='plaintext')),
|
||||||
|
)
|
||||||
|
|
||||||
class PokemonAbility(TableBase):
|
class PokemonAbility(TableBase):
|
||||||
u"""Maps an ability to a Pokémon that can have it
|
u"""Maps an ability to a Pokémon that can have it
|
||||||
"""
|
"""
|
||||||
|
@ -1524,7 +1540,7 @@ Ability.generation = relation(Generation, backref='abilities')
|
||||||
Ability.all_pokemon = relation(Pokemon,
|
Ability.all_pokemon = relation(Pokemon,
|
||||||
secondary=PokemonAbility.__table__,
|
secondary=PokemonAbility.__table__,
|
||||||
order_by=Pokemon.order,
|
order_by=Pokemon.order,
|
||||||
back_populates='all_abilities',
|
#back_populates='all_abilities',
|
||||||
)
|
)
|
||||||
Ability.pokemon = relation(Pokemon,
|
Ability.pokemon = relation(Pokemon,
|
||||||
secondary=PokemonAbility.__table__,
|
secondary=PokemonAbility.__table__,
|
||||||
|
@ -1533,7 +1549,7 @@ Ability.pokemon = relation(Pokemon,
|
||||||
PokemonAbility.is_dream == False
|
PokemonAbility.is_dream == False
|
||||||
),
|
),
|
||||||
order_by=Pokemon.order,
|
order_by=Pokemon.order,
|
||||||
back_populates='abilities',
|
#back_populates='abilities',
|
||||||
)
|
)
|
||||||
Ability.dream_pokemon = relation(Pokemon,
|
Ability.dream_pokemon = relation(Pokemon,
|
||||||
secondary=PokemonAbility.__table__,
|
secondary=PokemonAbility.__table__,
|
||||||
|
@ -1542,7 +1558,7 @@ Ability.dream_pokemon = relation(Pokemon,
|
||||||
PokemonAbility.is_dream == True
|
PokemonAbility.is_dream == True
|
||||||
),
|
),
|
||||||
order_by=Pokemon.order,
|
order_by=Pokemon.order,
|
||||||
back_populates='dream_ability',
|
#back_populates='dream_ability',
|
||||||
)
|
)
|
||||||
|
|
||||||
AbilityChangelog.changed_in = relation(VersionGroup, backref='ability_changelog')
|
AbilityChangelog.changed_in = relation(VersionGroup, backref='ability_changelog')
|
||||||
|
@ -1578,7 +1594,7 @@ EncounterSlot.version_group = relation(VersionGroup)
|
||||||
|
|
||||||
EvolutionChain.growth_rate = relation(GrowthRate, backref='evolution_chains')
|
EvolutionChain.growth_rate = relation(GrowthRate, backref='evolution_chains')
|
||||||
EvolutionChain.baby_trigger_item = relation(Item, backref='evolution_chains')
|
EvolutionChain.baby_trigger_item = relation(Item, backref='evolution_chains')
|
||||||
EvolutionChain.pokemon = relation(Pokemon, order_by=Pokemon.order, back_populates='evolution_chain')
|
EvolutionChain.pokemon = relation(Pokemon, order_by=Pokemon.order)#, back_populates='evolution_chain')
|
||||||
|
|
||||||
Experience.growth_rate = relation(GrowthRate, backref='experience_table')
|
Experience.growth_rate = relation(GrowthRate, backref='experience_table')
|
||||||
|
|
||||||
|
@ -1638,7 +1654,7 @@ Move.super_contest_effect = relation(SuperContestEffect, backref='moves')
|
||||||
Move.super_contest_combo_next = association_proxy('super_contest_combo_first', 'second')
|
Move.super_contest_combo_next = association_proxy('super_contest_combo_first', 'second')
|
||||||
Move.super_contest_combo_prev = association_proxy('super_contest_combo_second', 'first')
|
Move.super_contest_combo_prev = association_proxy('super_contest_combo_second', 'first')
|
||||||
Move.target = relation(MoveTarget, backref='moves')
|
Move.target = relation(MoveTarget, backref='moves')
|
||||||
Move.type = relation(Type, back_populates='moves')
|
Move.type = relation(Type)#, back_populates='moves')
|
||||||
|
|
||||||
MoveChangelog.changed_in = relation(VersionGroup, backref='move_changelog')
|
MoveChangelog.changed_in = relation(VersionGroup, backref='move_changelog')
|
||||||
MoveChangelog.move_effect = relation(MoveEffect, backref='move_changelog')
|
MoveChangelog.move_effect = relation(MoveEffect, backref='move_changelog')
|
||||||
|
@ -1681,7 +1697,7 @@ NatureBattleStylePreference.battle_style = relation(MoveBattleStyle, backref='na
|
||||||
NaturePokeathlonStat.pokeathlon_stat = relation(PokeathlonStat, backref='nature_effects')
|
NaturePokeathlonStat.pokeathlon_stat = relation(PokeathlonStat, backref='nature_effects')
|
||||||
|
|
||||||
Pokedex.region = relation(Region, backref='pokedexes')
|
Pokedex.region = relation(Region, backref='pokedexes')
|
||||||
Pokedex.version_groups = relation(VersionGroup, order_by=VersionGroup.id, back_populates='pokedex')
|
Pokedex.version_groups = relation(VersionGroup, order_by=VersionGroup.id)#, back_populates='pokedex')
|
||||||
|
|
||||||
Pokemon.all_abilities = relation(Ability,
|
Pokemon.all_abilities = relation(Ability,
|
||||||
secondary=PokemonAbility.__table__,
|
secondary=PokemonAbility.__table__,
|
||||||
|
@ -1709,7 +1725,7 @@ Pokemon.dex_numbers = relation(PokemonDexNumber, order_by=PokemonDexNumber.poked
|
||||||
Pokemon.egg_groups = relation(EggGroup, secondary=PokemonEggGroup.__table__,
|
Pokemon.egg_groups = relation(EggGroup, secondary=PokemonEggGroup.__table__,
|
||||||
order_by=PokemonEggGroup.egg_group_id,
|
order_by=PokemonEggGroup.egg_group_id,
|
||||||
backref=backref('pokemon', order_by=Pokemon.order))
|
backref=backref('pokemon', order_by=Pokemon.order))
|
||||||
Pokemon.evolution_chain = relation(EvolutionChain, back_populates='pokemon')
|
Pokemon.evolution_chain = relation(EvolutionChain)#, back_populates='pokemon')
|
||||||
Pokemon.child_pokemon = relation(Pokemon,
|
Pokemon.child_pokemon = relation(Pokemon,
|
||||||
primaryjoin=Pokemon.id==PokemonEvolution.from_pokemon_id,
|
primaryjoin=Pokemon.id==PokemonEvolution.from_pokemon_id,
|
||||||
secondary=PokemonEvolution.__table__,
|
secondary=PokemonEvolution.__table__,
|
||||||
|
@ -1731,7 +1747,7 @@ Pokemon.shape = relation(PokemonShape, backref='pokemon')
|
||||||
Pokemon.stats = relation(PokemonStat, backref='pokemon', order_by=PokemonStat.stat_id.asc())
|
Pokemon.stats = relation(PokemonStat, backref='pokemon', order_by=PokemonStat.stat_id.asc())
|
||||||
Pokemon.types = relation(Type, secondary=PokemonType.__table__,
|
Pokemon.types = relation(Type, secondary=PokemonType.__table__,
|
||||||
order_by=PokemonType.slot.asc(),
|
order_by=PokemonType.slot.asc(),
|
||||||
back_populates='pokemon')
|
)#back_populates='pokemon')
|
||||||
|
|
||||||
PokemonDexNumber.pokedex = relation(Pokedex)
|
PokemonDexNumber.pokedex = relation(Pokedex)
|
||||||
|
|
||||||
|
@ -1829,7 +1845,7 @@ Type.generation = relation(Generation, backref='types')
|
||||||
Type.damage_class = relation(MoveDamageClass, backref='types')
|
Type.damage_class = relation(MoveDamageClass, backref='types')
|
||||||
Type.pokemon = relation(Pokemon, secondary=PokemonType.__table__,
|
Type.pokemon = relation(Pokemon, secondary=PokemonType.__table__,
|
||||||
order_by=Pokemon.order,
|
order_by=Pokemon.order,
|
||||||
back_populates='types')
|
)#back_populates='types')
|
||||||
Type.moves = relation(Move, back_populates='type', order_by=Move.id)
|
Type.moves = relation(Move, back_populates='type', order_by=Move.id)
|
||||||
|
|
||||||
Version.version_group = relation(VersionGroup, back_populates='versions')
|
Version.version_group = relation(VersionGroup, back_populates='versions')
|
||||||
|
@ -1846,149 +1862,6 @@ VersionGroup.pokedex = relation(Pokedex, back_populates='version_groups')
|
||||||
|
|
||||||
default_lang = u'en'
|
default_lang = u'en'
|
||||||
|
|
||||||
def create_translation_table(_table_name, foreign_class,
|
|
||||||
_language_class=Language, **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.
|
|
||||||
TODO give it a __table__ or __init__?
|
|
||||||
|
|
||||||
`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.
|
|
||||||
|
|
||||||
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
|
|
||||||
setattr(foreign_class, _table_name, relation(Translations,
|
|
||||||
primaryjoin=foreign_class.id == Translations.object_id,
|
|
||||||
collection_class=attribute_mapped_collection('language'),
|
|
||||||
# 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:
|
|
||||||
# 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()
|
|
||||||
row.language = language
|
|
||||||
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):
|
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'
|
# With "Language", we'd have two language_id. So, rename one to 'lang'
|
||||||
foreign_key_name = foreign_table_class.__singlename__
|
foreign_key_name = foreign_table_class.__singlename__
|
||||||
|
|
|
@ -8,7 +8,6 @@ from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
from pokedex.db import tables, markdown
|
from pokedex.db import tables, markdown
|
||||||
from pokedex.db.multilang import create_translation_table
|
from pokedex.db.multilang import create_translation_table
|
||||||
from pokedex.db.tables import create_translation_table
|
|
||||||
|
|
||||||
def test_variable_names():
|
def test_variable_names():
|
||||||
"""We want pokedex.db.tables to export tables using the class name"""
|
"""We want pokedex.db.tables to export tables using the class name"""
|
||||||
|
@ -49,21 +48,20 @@ def test_i18n_table_creation():
|
||||||
id = Column(Integer, primary_key=True, nullable=False)
|
id = Column(Integer, primary_key=True, nullable=False)
|
||||||
|
|
||||||
FooText = create_translation_table('foo_text', Foo,
|
FooText = create_translation_table('foo_text', Foo,
|
||||||
_language_class=Language,
|
language_class=Language,
|
||||||
name = Column(String(100)),
|
name = Column(String(100)),
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO move this to the real code
|
class FauxSession(Session):
|
||||||
class DurpSession(Session):
|
|
||||||
def execute(self, clause, params=None, *args, **kwargs):
|
def execute(self, clause, params=None, *args, **kwargs):
|
||||||
if not params:
|
if not params:
|
||||||
params = {}
|
params = {}
|
||||||
params.setdefault('_default_language', 'en')
|
params.setdefault('_default_language', 'en')
|
||||||
return super(DurpSession, self).execute(clause, params, *args, **kwargs)
|
return super(FauxSession, self).execute(clause, params, *args, **kwargs)
|
||||||
|
|
||||||
# OK, create all the tables and gimme a session
|
# OK, create all the tables and gimme a session
|
||||||
Base.metadata.create_all()
|
Base.metadata.create_all()
|
||||||
sess = sessionmaker(engine, class_=DurpSession)()
|
sess = sessionmaker(engine, class_=FauxSession)()
|
||||||
|
|
||||||
# Create some languages and foos to bind together
|
# Create some languages and foos to bind together
|
||||||
lang_en = Language(identifier='en')
|
lang_en = Language(identifier='en')
|
||||||
|
|
Loading…
Reference in a new issue