def test_removing_translations(session, session_cls, first, second): translator = Translator(Translation, session_cls(), 'language') translator.bind(session) instance = Model(name=first) session.add(instance) session.commit() expected_count = 1 if is_translatable_value(first) else 0 assert translator.session.query(Translation).count() == expected_count instance.name = second session.commit() expected_count = 1 if is_translatable_value(second) else 0 assert translator.session.query(Translation).count() == expected_count
def collect_translatables(manager, obj): """ Return translatables from ``obj``. Mutates ``obj`` to replace translations with placeholders. Expects translator.save_translation or translator.delete_translations to be called for each collected translatable. """ if isinstance(obj, PersistableType): # object is a type; nothing to do return [] translatables = [] descriptor = manager.type_registry.get_descriptor(type(obj)) message_id = get_message_id(manager, obj) for attr_name in iter_translatables(descriptor): attr = getattr(obj, attr_name) if is_translatable_value(attr): setattr(obj, attr_name, PLACEHOLDER) context = get_context(manager, obj, attr_name) translatable = TaalTranslatableString( context, message_id, attr) translatables.append(translatable) return translatables
def after_commit(session): """ Save any pending translations for this session """ for transaction, target, column, value in flush_log.pop(session, []): translator = get_translator(session) translatable = make_from_obj(target, column.name, value) if is_translatable_value(value): translator.save_translation(translatable, commit=True) else: # a non-translatable value in the commit log indicates a deletion translator.delete_translations(translatable) old_value = getattr(target, column.name) if is_translatable_value(old_value): # we may now have a primary key old_value.message_id = translatable.message_id # value is now saved. No need to keep around old_value.pending_value = None
def to_python(value): if not is_translatable_value(value): return value if value == PLACEHOLDER: # Before translation, return a placeholder that's more likely to # generate an error than a normal string. return PlaceholderValue raise RuntimeError( "Unexpected value found in placeholder column: '{}'".format(value))
def set_(target, value, oldvalue, initiator): """ Wrap any value in ``TranslatableString``, except None and the empty string """ if not is_translatable_value(value): return value if isinstance(value, TaalTranslatableString): return TaalTranslatableString( value.context, value.message_id, value.pending_value) translatable = make_from_obj(target, initiator.key, value) return translatable
def refresh(target, args, attrs): mapper = inspect(target.__class__) if attrs is None: attrs = mapper.columns.keys() for column_name in attrs: if column_name not in mapper.columns: continue column = mapper.columns[column_name] if isinstance(column.type, TranslatableString): value = getattr(target, column.name) if is_translatable_value(value): translatable = make_from_obj(target, column.name, value) setattr(target, column.name, translatable) return target
def process_result_value(self, value, dialect): if not is_translatable_value(value): return value if value == PLACEHOLDER: # can't prevent this from being returned to the user # in the case of a direct query for Model.field # Return something that's more likely to error early # than a string return PlaceholderValue raise RuntimeError( "Unexpected value found in placeholder column: '{}'".format(value))
def serialize(self, obj, for_db=False): if for_db or type(obj) is PersistableType: return super(Manager, self).serialize(obj) message_id = get_message_id(self, obj) data = super(Manager, self).serialize(obj) descriptor = self.type_registry.get_descriptor(type(obj)) for attr_name, attr_type in descriptor.attributes.items(): if isinstance(attr_type, TranslatableString): value = data[attr_name] if is_translatable_value(value): context = get_context(self, obj, attr_name) data[attr_name] = TaalTranslatableString( context, message_id) return data
def save(self, obj): translatables = collect_translatables(self, obj) result = super(Manager, self).save(obj) if translatables: translator = get_translator(self) for translatable in translatables: if is_translatable_value(translatable.pending_value): translator.save_translation(translatable) else: # delete all translations (in every language) if the # value is None or the empty string translator.delete_translations(translatable) return result
def load(target, context): """ Wrap columns when loading data from the db """ mapper = inspect(target.__class__) for column in mapper.columns: if isinstance(column.type, TranslatableString): value = getattr(target, column.name) if not is_translatable_value(value): continue elif value is PlaceholderValue: translatable = make_from_obj(target, column.name, value) setattr(target, column.name, translatable) elif isinstance(value, TaalTranslatableString): continue # during session.merge else: raise TypeError("Unexpected column value '{}'".format( value))
def add_to_flush_log(session, target, delete=False): cls = target.__class__ for column, attr_name in translatable_models.get(cls, {}).items(): history = get_history(target, attr_name) if not delete and not history.has_changes(): # for non-delete actions, we're only interested in changed columns continue if delete: value = None # will trigger deletion of translations else: value = getattr(target, attr_name) if is_translatable_value(value): pending_translatables.add(value) value = value.pending_value flush_log.setdefault(session, []).append( (session.transaction, target, column, value))
def process_bind_param(self, value, dialect): if not is_translatable_value(value): return value if not isinstance(value, TaalTranslatableString): # this should only happen if someone is trying to query # TODO: verify this raise RuntimeError("Cannot filter on translated fields") if value in pending_translatables: pending_translatables.remove(value) else: raise RuntimeError( "Cannot save directly to translated fields. " "Use ``save_translation`` instead " "Value was '{}'".format(value)) return PLACEHOLDER
def add_to_flush_log(session, target, delete=False): mapper = inspect(target.__class__) for column in mapper.columns: history = get_history(target, column.name) if not delete and not history.has_changes(): # for non-delete actions, we're only interested in changed columns continue if isinstance(column.type, TranslatableString): if delete: value = None # will trigger deletion of translations else: value = getattr(target, column.name) if is_translatable_value(value): pending_translatables.add(value) value = value.pending_value flush_log.setdefault(session, []).append( (session.transaction, target, column, value))
def change_instance_type(self, obj, type_id, updated_values=None): if updated_values is None: updated_values = {} updated_values = updated_values.copy() old_descriptor = self.type_registry.get_descriptor(type(obj)) new_descriptor = self.type_registry.get_descriptor_by_id(type_id) old_message_id = get_message_id(self, obj) old_translatables = {} # collect any translatable fields on the original object # also, replace any values with placeholders for the super() call for attr_name in iter_translatables(old_descriptor): attr = getattr(obj, attr_name) if is_translatable_value(attr): setattr(obj, attr_name, PLACEHOLDER) context = get_context(self, obj, attr_name) translatable = TaalTranslatableString( context, old_message_id, attr) old_translatables[attr_name] = translatable new_translatables = {} # collect any translatable fields from the new type # also, replace any values in updated_values with placeholders # for the super() call # note that we can't collect the context/message_id until after # we call super(), since they may be about to change # (context will definitely change, and message_id might, if we add or # remove unique attributes) for attr_name in iter_translatables(new_descriptor): attr = updated_values.get(attr_name) if is_translatable_value(attr): updated_values[attr_name] = PLACEHOLDER translatable = TaalTranslatableString( None, None, attr) new_translatables[attr_name] = translatable new_obj = super(Manager, self).change_instance_type( obj, type_id, updated_values) # we are now able to fill in context/message_id for the new object new_message_id = get_message_id(self, new_obj) for attr_name, translatable in new_translatables.items(): translatable.message_id = new_message_id translatable.context = get_context(self, new_obj, attr_name) to_delete = set(old_translatables) - set(new_translatables) to_rename = set(old_translatables) & set(new_translatables) to_add = set(new_translatables) - set(old_translatables) translator = get_translator(self) for key in to_delete: translatable = old_translatables[key] translator.delete_translations(translatable) for key in to_rename: old_translatable = old_translatables[key] new_translatable = new_translatables[key] translator.move_translations(old_translatable, new_translatable) if new_translatable.pending_value is not None: # updated_values contained a key for a field already existing # on the old type. save the updated translation translator.save_translation(new_translatable) for key in to_add: translatable = new_translatables[key] if translatable.pending_value is not None: translator.save_translation(translatable) return new_obj