X-Git-Url: http://git.veekun.com/zzz-pokedex.git/blobdiff_plain/1d07bbc3950b1d59094d22dc75973ef9a24592a8..8df292954aad77e7934d6bbf523fcff626cfd33c:/pokedex/db/tables.py diff --git a/pokedex/db/tables.py b/pokedex/db/tables.py index 89a0efa..76bd753 100644 --- a/pokedex/db/tables.py +++ b/pokedex/db/tables.py @@ -15,10 +15,18 @@ Columns have a info dictionary with these keys: - identifier: A fan-made identifier in the [-_a-z0-9]* format. Not intended for translation. - latex: A formula in LaTeX syntax. + +A localizable text column is visible as two properties: +The plural-name property (e.g. Pokemon.names) is a language-to-name dictionary: + bulbasaur.names['en'] == "Bulbasaur" and bulbasaur.names['de'] == "Bisasam". + You can use Pokemon.names['en'] to filter a query. +The singular-name property returns the name in the default language, English. + For example bulbasaur.name == "Bulbasaur" + Setting pokedex.db.tables.default_lang changes the default language. """ # XXX: Check if "gametext" is set correctly everywhere -import operator +import collections from sqlalchemy import Column, ForeignKey, MetaData, PrimaryKeyConstraint, Table from sqlalchemy.ext.declarative import ( @@ -28,9 +36,12 @@ from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm import ( backref, eagerload_all, relation, class_mapper, synonym, mapper, ) -from sqlalchemy.orm.session import Session +from sqlalchemy.orm.session import Session, object_session from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.sql import and_ +from sqlalchemy.sql.expression import ColumnOperators +from sqlalchemy.schema import ColumnDefault from sqlalchemy.types import * from inspect import isclass @@ -671,9 +682,9 @@ class MoveEffectChangelog(TableBase): __singlename__ = 'move_effect_changelog' id = Column(Integer, primary_key=True, nullable=False, info=dict(description="A numeric ID")) - effect_id = Column(Integer, ForeignKey('move_effects.id'), primary_key=True, nullable=False, + effect_id = Column(Integer, ForeignKey('move_effects.id'), nullable=False, info=dict(description="The ID of the effect that changed")) - changed_in_version_group_id = Column(Integer, ForeignKey('version_groups.id'), primary_key=True, nullable=False, + changed_in_version_group_id = Column(Integer, ForeignKey('version_groups.id'), nullable=False, info=dict(description="The ID of the version group in which the effect changed")) effect = ProseColumn(markdown.MarkdownColumn(512), plural='effects', nullable=False, info=dict(description="A description of the old behavior", format='markdown')) @@ -1355,7 +1366,7 @@ class StatHint(TableBase): info=dict(description=u"ID of the highest stat")) gene_mod_5 = Column(Integer, nullable=False, index=True, info=dict(description=u"Value of the highest stat modulo 5")) - text = TextColumn(Unicode(24), plural='texts', nullable=False, index=True, unique=True, + message = TextColumn(Unicode(24), plural='messages', nullable=False, index=True, unique=True, info=dict(description=u"The text displayed", official=True, format='plaintext')) class SuperContestCombo(TableBase): @@ -1397,7 +1408,7 @@ class Type(TableBase, OfficiallyNamed): __singlename__ = 'type' id = Column(Integer, primary_key=True, nullable=False, info=dict(description=u"A unique ID for this type.")) - identifier = Column(Unicode(8), nullable=False, + identifier = Column(Unicode(12), nullable=False, info=dict(description=u"An identifier", format='identifier')) generation_id = Column(Integer, ForeignKey('generations.id'), nullable=False, info=dict(description=u"The ID of the generation this type first appeared in.")) @@ -1562,20 +1573,10 @@ Move.super_contest_combo_prev = association_proxy('super_contest_combo_second', Move.target = relation(MoveTarget, backref='moves') Move.type = relation(Type, back_populates='moves') -Move.effect = markdown.MoveEffectProperty('effect') -Move.effects = markdown.MoveEffectsProperty('effect') -Move.short_effect = markdown.MoveEffectProperty('short_effect') -Move.short_effects = markdown.MoveEffectsProperty('short_effect') - MoveChangelog.changed_in = relation(VersionGroup, backref='move_changelog') MoveChangelog.move_effect = relation(MoveEffect, backref='move_changelog') MoveChangelog.type = relation(Type, backref='move_changelog') -MoveChangelog.effect = markdown.MoveEffectProperty('effect') -MoveChangelog.effects = markdown.MoveEffectsProperty('effect') -MoveChangelog.short_effect = markdown.MoveEffectProperty('short_effect') -MoveChangelog.short_effects = markdown.MoveEffectsProperty('short_effect') - MoveEffect.category_map = relation(MoveEffectCategoryMap) MoveEffect.categories = association_proxy('category_map', 'category') MoveEffect.changelog = relation(MoveEffectChangelog, @@ -1785,10 +1786,12 @@ for table in list(table_classes): else: continue table.name = cls(Unicode(class_mapper(table).c.identifier.type.length), - plural='names', nullable=False, info=info) + plural='names', index=True, nullable=False, info=info) ### Add text/prose tables +default_lang = u'en' + def makeTextTable(object_table, name_plural, name_singular, columns, lazy): # With "Language", we'd have two language_id. So, rename one to 'lang' safe_name = object_table.__singlename__ @@ -1801,15 +1804,21 @@ def makeTextTable(object_table, name_plural, name_singular, columns, lazy): class Strings(object): __tablename__ = tablename __singlename__ = singlename + _attrname = name_plural + _language_identifier = association_proxy('language', 'identifier') for name, plural, column in columns: column.name = 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(tablename, metadata, Column(safe_name + '_id', Integer, ForeignKey(object_table.id), primary_key=True, nullable=False), Column('language_id', Integer, ForeignKey(Language.id), - primary_key=True, nullable=False), + primary_key=True, index=True, nullable=False), *(column for name, plural, column in columns) ) @@ -1833,35 +1842,115 @@ def makeTextTable(object_table, name_plural, name_singular, columns, lazy): )) Strings.object = getattr(Strings, safe_name) - # Link the tables themselves, so we can get to them + # Link the tables themselves, so we can get them if needed Strings.object_table = object_table setattr(object_table, name_singular + '_table', Strings) for colname, pluralname, column in columns: - # Provide a relation with all the names, and an English accessor + # Provide a property with all the names, and an English accessor # for backwards compatibility - def scope(colname, pluralname, column): - def get_strings(self): - return dict( - (l, getattr(t, colname)) - for l, t in getattr(self, name_plural).items() - ) - - def get_english_string(self): - try: - return get_strings(self)['en'] - except KeyError: - raise AttributeError(colname) - - setattr(object_table, pluralname, property(get_strings)) - setattr(object_table, colname, property(get_english_string)) - scope(colname, pluralname, column) + setattr(object_table, pluralname, StringProperty( + object_table, Strings, colname, + )) + setattr(object_table, colname, DefaultLangProperty(pluralname)) if colname == 'name': object_table.name_table = Strings return Strings +class StringProperty(object): + def __init__(self, cls, stringclass, colname): + self.cls = cls + self.colname = colname + self.stringclass = stringclass + + def __get__(self, instance, cls): + if instance: + return StringMapping(instance, self) + else: + return self + + def __getitem__(self, lang): + return StringExpression(self, lang) + + def __str__(self): + return '' % (self.cls, self.colname) + +class StringMapping(collections.MutableMapping): + def __init__(self, instance, prop): + self.stringclass = prop.stringclass + self.instance = instance + self.strings = getattr(instance, prop.stringclass._attrname) + self.colname = prop.colname + + def __len__(self): + return len(self.strings) + + def __iter__(self): + return iter(self.strings) + + def __contains__(self, lang): + return lang in self.strings + + def __getitem__(self, lang): + return getattr(self.strings[lang], self.colname) + + def __setitem__(self, lang, value): + try: + # Modifying an existing row + row = self.strings[lang] + except KeyError: + # We need do add a whole row for the language + row = self.stringclass() + row.object_id = self.instance.id + session = object_session(self.instance) + if isinstance(lang, basestring): + lang = session.query(Language).filter_by( + identifier=lang).one() + row.language = lang + self.strings[lang] = row + session.add(row) + return setattr(row, self.colname, value) + + def __delitem__(self, lang): + raise NotImplementedError('Cannot delete a single string. ' + 'Perhaps you wan to delete all of %s.%s?' % + (self.instance, self.stringclass._attrname) + ) + +class StringExpression(ColumnOperators): + def __init__(self, prop, lang): + self.prop = prop + self.column = getattr(prop.stringclass, prop.colname) + self.lang_column = prop.stringclass._language_identifier + if isinstance(lang, basestring): + self.lang = lang + else: + self.lang = lang.identifier + + def operate(self, op, *values, **kwargs): + return getattr(self.prop.cls, self.prop.stringclass._attrname).any(and_( + self.lang_column == self.lang, + op(self.column, *values, **kwargs), + )) + +class DefaultLangProperty(object): + def __init__(self, colname): + self.colname = colname + + def __get__(self, instance, cls): + if instance: + return getattr(instance, self.colname)[default_lang] + else: + return getattr(cls, self.colname)[default_lang] + + def __set__(self, instance, value): + getattr(instance, self.colname)[default_lang] = value + + def __delete__(self, instance): + del getattr(instance, self.colname)[default_lang] + for table in list(table_classes): # Find all the language-specific columns, keeping them in the order they # were defined @@ -1894,3 +1983,13 @@ for table in list(table_classes): for table in list(table_classes): if issubclass(table, LanguageSpecific): table.language = relation(Language, primaryjoin=table.language_id == Language.id) + +Move.effect = DefaultLangProperty('effects') +Move.effects = markdown.MoveEffectsProperty('effect') +Move.short_effect = DefaultLangProperty('short_effects') +Move.short_effects = markdown.MoveEffectsProperty('short_effect') + +MoveChangelog.effect = DefaultLangProperty('effects') +MoveChangelog.effects = markdown.MoveEffectsProperty('effect') +MoveChangelog.short_effect = DefaultLangProperty('short_effects') +MoveChangelog.short_effects = markdown.MoveEffectsProperty('short_effect')