Source code for assembl.models.langstrings

"""Classes for multilingual strings, using automatic or manual translation"""
from builtins import next
from builtins import filter
from collections import defaultdict
from datetime import datetime

from future.utils import as_native_str, string_types
from sqlalchemy import (
    Column, ForeignKey, Integer, Boolean, String, SmallInteger,
    UnicodeText, UniqueConstraint, event, inspect, Sequence, events,
    literal)
from sqlalchemy.sql.expression import case
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import (
    relationship, backref, subqueryload, joinedload, aliased,
    attributes, remote, foreign)
from sqlalchemy.orm.query import Query
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property
from sqlalchemy.ext.declarative import declared_attr
import simplejson as json

from ..lib.sqla_types import CoerceUnicode
from . import Base, TombstonableMixin, CrudOperation
from .import_records import ImportRecord
from ..lib import config
from ..lib.abc import classproperty
from ..lib.locale import (
    locale_ancestry, create_mt_code, locale_compatible, split_mt_code)
from ..auth import CrudPermissions, P_READ, P_ADMIN_DISC, P_SYSADMIN


[docs]class LocaleLabel(Base): "Allows to obtain the name of locales (in any target locale, incl. itself)" __tablename__ = "locale_label" __table_args__ = (UniqueConstraint('named_locale', 'locale_of_label'), ) id = Column(Integer, primary_key=True) named_locale = Column(String(11), nullable=False, index=True) locale_of_label = Column(String(11), nullable=False, index=True) name = Column(CoerceUnicode) UNDEFINED = "und" NON_LINGUISTIC = "zxx" MULTILINGUAL = "mul" SPECIAL_LOCALES = [UNDEFINED, NON_LINGUISTIC, MULTILINGUAL] """Note: The name of locales follow Posix locale conventions: lang(_Script)(_COUNTRY), (eg zh_Hant_HK, but script can be elided (eg fr_CA) if only one script for language, as per http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry """ @classmethod def names_in_locale(cls, locale): locale_chain = locale_ancestry(locale) locale_labels = locale.db.query(cls).filter( cls.locale_of_label.in_(locale_chain)).all() by_target = defaultdict(list) for ln in locale_labels: by_target[ln.locale_of_label].append(ln) result = dict() locale_chain.reverse() for locale in locale_chain: result.update({ lname.named_locale: lname.name for lname in by_target[locale]}) return result @classmethod def names_of_locales_in_locale(cls, loc_codes, target_locale): target_locs = locale_ancestry(target_locale) locale_labels = cls.default_db.query(cls).filter( cls.locale_of_label.in_(target_locs), cls.named_locale.in_(loc_codes)).all() by_target = defaultdict(list) for ln in locale_labels: by_target[ln.locale_of_label].append(ln) result = dict() target_locs.reverse() for loc in target_locs: result.update({ lname.named_locale: lname.name for lname in by_target[loc]}) return result @classmethod def names_in_self(cls): return { lname.named_locale: lname.name for lname in cls.default_db.query(cls).filter( cls.locale_of_label == cls.named_locale)} @classmethod def load_names(cls, db=None): from os.path import dirname, join db = db or cls.default_db fname = join(dirname(dirname(__file__)), 'nlp/data/language-names.json') with open(fname, encoding='utf-8') as f: names = json.load(f, encoding="utf-8") locales = {x[0] for x in names}.union({x[1] for x in names}) count_locales = db.query(cls.named_locale.label('loc')).union(db.query(cls.locale_of_label.label('loc'))).distinct().count() count_names = db.query(cls.id).count() if count_names == len(names) and count_locales == len(locales): # shortcut return existing = set(db.query(cls.named_locale, cls.locale_of_label).all()) missing = [] for (lcode, tcode, name) in names: if (lcode, tcode) not in existing: missing.append(cls( named_locale=lcode, locale_of_label=tcode, name=name)) db.bulk_save_objects(missing) db.flush() @classmethod def populate_db(cls, db=None): db.execute("lock table %s in exclusive mode" % cls.__table__.name) cls.load_names(db) crud_permissions = CrudPermissions(P_READ, P_ADMIN_DISC)
[docs]class LangString(Base): """A multilingual string, composed of many :py:class:`LangStringEntry`""" __tablename__ = "langstring" @classmethod def subqueryload_option(cls, reln): return subqueryload(reln).joinedload(cls.entries) @classmethod def joinedload_option(cls, reln): return joinedload(reln).joinedload(cls.entries) id_sequence_name = "langstring_idsequence" @classproperty def id_sequence(cls): return Sequence(cls.id_sequence_name, schema=cls.metadata.schema) id = Column(Integer, primary_key=True) @declared_attr def import_record(cls): return relationship( ImportRecord, uselist=False, primaryjoin=(remote(ImportRecord.target_id)==foreign(cls.id)) & (ImportRecord.target_table == cls.__tablename__)) def __bool__(self): for entry in self.entries: if entry: return True return False def as_jsonld(self, default_lang=None, use_map=False): entries = [e.as_jsonld(default_lang) for e in self.entries] if len(self.entries) == 1: return self.entries[0].as_jsonld(default_lang) elif use_map: return {e.locale: e.value for e in self.entries} else: return [e.as_jsonld(default_lang) for e in self.entries]
[docs] def add_entry(self, entry, allow_replacement=True): """Add a LangStringEntry to the langstring. Previous versions with the same language will be tombstoned, and translations based on such a version will be suppressed.""" if entry and isinstance(entry, LangStringEntry): entry_locale = entry.locale for ex_entry in self.entries: # Loop on the entries means that repeated calls to # add_entry will be O(n^2). If this becomes an issue, # create an add_entries method. if ex_entry is entry: continue if ex_entry.value == entry.value: if entry in self.entries: self.entries.remove(entry) return ex_entry ex_locale = ex_entry.locale if entry_locale == ex_locale: if ex_entry.is_machine_translated: self.entries.remove(ex_entry) else: if not allow_replacement: return None ex_entry.is_tombstone = True self.remove_translations_of(ex_entry) if inspect(self).persistent: self.db.expire(self, ["entries"]) entry.langstring = self return entry
[docs] def remove_translations_of(self, entry): """Remove all translations based on this code.""" if entry.id is None: return for trans in self.entries[:]: if trans.mt_trans_of_id == entry.id: trans.delete()
@as_native_str() def __repr__(self): return 'LangString (%d): %s\n' % ( self.id or -1, "\n".join((repr(x) for x in self.entries)))
[docs] @classmethod def create(cls, value, locale_code=LocaleLabel.UNDEFINED): ls = cls() lse = LangStringEntry( langstring=ls, value=value, locale=locale_code) return ls
def add_value(self, value, locale_code=LocaleLabel.UNDEFINED, allow_replacement=True): return self.add_entry(LangStringEntry( langstring=self, value=value, locale=locale_code), allow_replacement=allow_replacement)
[docs] @classmethod def create_localized_langstring( cls, trans_string, desired_locales=None, known_translations=None): """Construct a langstring from a localized string. Call with a TranslationString.""" inst = cls.create(trans_string, 'en') known_translations = known_translations or {} for loc in desired_locales or (): if loc == 'en': continue elif loc in known_translations: inst.add_value(known_translations[loc], loc) else: from pyramid.i18n import make_localizer from os.path import dirname, join loc_dir = join(dirname(dirname(__file__)), 'locale') localizer = make_localizer(loc, loc_dir) inst.add_value(localizer.translate(trans_string), loc) return inst
@property def entries_as_dict(self): return {e.locale: e for e in self.entries} @hybrid_method def non_mt_entries(self): return [e for e in self.entries if not e.is_machine_translated] @non_mt_entries.expression def non_mt_entries(self): return self.db.query(LangStringEntry).filter( ~LangStringEntry.is_machine_translated).subquery() def first_original(self): return next(iter(self.non_mt_entries())) @classmethod def EMPTY(cls, db=None): ls = LangString() e = LangStringEntry( langstring=ls, locale=LocaleLabel.NON_LINGUISTIC) if db is not None: db.add(e) db.add(ls) return ls @classmethod def reset_cache(cls): pass # Which object owns this? owner_object = None _owning_relns = [] @classmethod def setup_ownership_load_event(cls, owner_class, relns): def load_owner_object(target, context): for reln in relns: ls = getattr(target, reln, None) if ls is not None: ls.owner_object = target event.listen(owner_class, "load", load_owner_object, propagate=True) event.listens_for(owner_class, "refresh", load_owner_object, propagate=True) def set_owner_object(target, value, old_value, initiator): if old_value is not None: old_value.owner_object = None if value is not None: value.owner_object = target for reln in relns: cls._owning_relns.append((owner_class, reln)) event.listen(getattr(owner_class, reln), "set", set_owner_object, propagate=True) def get_owner_object(self): if self.owner_object is None and inspect(self).persistent: self.owner_object = self.owner_object_from_query() return self.owner_object def owner_object_query(self): queries = [] for owning_class, reln_name in self._owning_relns: backref_name = owning_class.__mapper__.relationships[reln_name].backref[0] query = getattr(self, backref_name) query = query.with_entities(owning_class.id, literal(owning_class.__name__).label('classname')) queries.append(query) query = queries[0].union(*queries[1:]) return query def owner_object_from_query(self): query = self.owner_object_query() data = query.first() if data: id, cls_name = data cls = [cls for (cls, _) in self._owning_relns if cls.__name__ == cls_name][0] return self.db.query(cls).filter_by(id=id).first() @classmethod def orphans_query(cls, db=None): db = db or cls.default_db queries = [] for owning_class, reln_name in cls._owning_relns: reln = getattr(owning_class, reln_name) col = next(iter(reln.impl.parent_token.local_columns)) query = db.query(col.label('id')) queries.append(query) subquery = queries[0].union(*queries[1:]) return db.query(cls.id.label('id')).except_(subquery)
[docs] def send_to_changes(self, connection, operation=CrudOperation.DELETE, discussion_id=None, view_def="changes"): owner_object = self.get_owner_object() if owner_object is not None: owner_object.send_to_changes( connection, operation, discussion_id, view_def) else: super(LangString, self).send_to_changes( connection, operation, discussion_id, view_def)
[docs] def user_can(self, user_id, operation, permissions): owner_object = self.get_owner_object() if owner_object is not None: return owner_object.user_can(user_id, operation, permissions) return super(LangString, self).user_can(user_id, operation, permissions)
@classmethod def _do_create_from_json( cls, json, parse_def, context, duplicate_handling=None, object_importer=None): # Special case for JSON-LD added = False ls = cls() def guess_lang(value): from .discussion import Discussion discussion = context.get_instance_of_class(Discussion) if discussion: tr_service = discussion.translation_service() lang, _ = tr_service.identify(value) return LocaleLabel.UNDEFINED if isinstance(json, list): for entry_record in json: value = entry_record['@value'] if value: added = True lang = entry_record.get('@language', None) or guess_lang(value) ls.add_value(value, lang) elif isinstance(json, dict): if '@id' in json or '@type' in json: return super(LangString, cls)._do_create_from_json( json, parse_def, context, duplicate_handling, object_importer) elif '@value' in json: value = json['@value'] if value: added = True lang = json.get('@language', None) or guess_lang(value) ls.add_value(value, lang) else: for lang, value in json.items(): if value: added = True ls.add_value(value, lang) elif isinstance(json, string_types): if json: added = True lang = guess_lang(json) ls.add_value(json, lang) else: raise ValueError("Not a valid langstring: " + json) i_context = ls.get_instance_context(context) if added: cls.default_db.add(ls) else: i_context._instance = None return i_context def _do_update_from_json( self, json, parse_def, context, duplicate_handling=None, object_importer=None): # Special case for JSON-LD if isinstance(json, list): for entry_record in json: lang = entry_record.get('@language', LocaleLabel.UNDEFINED) value = entry_record['@value'] entry = self.entries_as_dict.get(lang, None) if entry: entry.set_value(value) elif value: self.add_value(value, lang) elif isinstance(json, dict): if '@id' in json or '@type' in json: return super(LangString, self)._do_update_from_json( json, parse_def, context, duplicate_handling, object_importer) elif '@value' in json: value = json['@value'] if value: lang = json.get('@language', LocaleLabel.UNDEFINED) entry = self.entries_as_dict.get(lang, None) if entry: entry.set_value(value) elif value: self.add_value(value, lang) else: for lang, value in json.items(): entry = self.entries_as_dict.get(lang, None) if entry: entry.set_value(value) elif value: self.add_value(value, lang) elif isinstance(json, string_types): from .discussion import Discussion lang = LocaleLabel.UNDEFINED discussion = context.get_instance_of_class(Discussion) if discussion: tr_service = discussion.translation_service() lang, _ = tr_service.identify(json) entry = self.entries_as_dict.get(lang, None) if entry: entry.set_value(json) elif json: self.add_value(json, lang) else: raise ValueError("Not a valid langstring: " + json) return self # TODO: Reinstate when the javascript can handle empty body/subject. # def generic_json( # self, view_def_name='default', user_id=None, # permissions=(P_READ, ), base_uri='local:'): # if self.id == self.EMPTY_ID: # return None # return super(LangString, self).generic_json( # view_def_name=view_def_name, user_id=user_id, # permissions=permissions, base_uri=base_uri) @property def undefined_entry(self): und = LocaleLabel.UNDEFINED for x in self.entries: if x.locale == und: return x @hybrid_method def best_lang_old(self, locale_codes): # based on a simple ordered list of locale_codes locale_collection = Locale.locale_collection locale_collection_subsets = Locale.locale_collection_subsets available = self.entries_as_dict if len(available) == 0: return LangStringEntry.EMPTY() if len(available) == 1: # optimize for common case return available[0] for locale_code in locale_codes: # is the locale there? if locale_code in available: return available[locale_code] # is the base locale there? root_locale = Locale.extract_root_locale(locale_code) if root_locale not in locale_codes: locale_id = locale_collection.get(root_locale, None) if locale_id and locale_id in available: return available[locale_id] # is another variant there? mt_variants = list() for sublocale in locale_collection_subsets[root_locale]: if sublocale in locale_codes: continue if sublocale == root_locale: continue if Locale.locale_is_machine_translated(sublocale): mt_variants.append(sublocale) continue locale_id = locale_collection.get(sublocale, None) if locale_id and locale_id in available: return available # We found nothing, look at MT variants. for sublocale in mt_variants: locale_id = locale_collection.get(sublocale, None) if locale_id and locale_id in available: return available[locale_id] # TODO: Look at other languages in the country? # Give up and give nothing, or give first? @best_lang_old.expression def best_lang_old(self, locale_codes): # Construct an expression that will find the best locale according to list. scores = {} current_score = 1 locale_collection = Locale.locale_collection locale_collection_subsets = Locale.locale_collection_subsets for locale_code in locale_codes: # is the locale there? locale_id = locale_collection.get(locale_code, None) if locale_id: scores[locale_id] = current_score current_score += 1 # is the base locale there? root_locale = Locale.extract_root_locale(locale_code) if root_locale not in locale_codes: locale_id = locale_collection.get(root_locale, None) if locale_id: scores[locale_id] = current_score current_score += 1 # is another variant there? mt_variants = list() found = False for sublocale in locale_collection_subsets[root_locale]: if sublocale in locale_codes: continue if sublocale == root_locale: continue if Locale.locale_is_machine_translated(sublocale): mt_variants.append(sublocale) continue locale_id = locale_collection.get(sublocale, None) if locale_id: scores[locale_id] = current_score found = True if found: current_score += 1 # Put MT variants as last resort. for sublocale in mt_variants: locale_id = locale_collection.get(sublocale, None) if locale_id: scores[locale_id] = current_score # Assume each mt variant to have a lower score. current_score += 1 c = case(scores, value=LangStringEntry.locale_id, else_=current_score) q = Query(LangStringEntry).order_by(c).limit(1).subquery() return aliased(LangStringEntry, q) def best_lang(self, user_prefs=None, allow_errors=True): from .auth import LanguagePreferenceCollection # Get the best langStringEntry among those available using user prefs. # 1. Look at available original languages: get corresponding pref. # 2. Sort prefs (same order as original list.) # 3. take first applicable w/o trans or whose translation is available. # 4. if none, look at available translations and repeat. # Logic is painful, but most of the time (single original) # will be trivial in practice. if len(self.entries) == 1: return self.entries[0] if user_prefs: if not isinstance(user_prefs, LanguagePreferenceCollection): # Often worth doing upstream user_prefs = LanguagePreferenceCollection.getCurrent() for use_originals in (True, False): entries = [e for e in self.entries if e.is_machine_translated != use_originals] if not allow_errors: entries = [e for e in entries if not e.error_code] if not entries: continue candidates = [] entriesByLocale = {} for entry in entries: pref = user_prefs.find_locale(entry.locale) if pref: candidates.append(pref) entriesByLocale[pref.locale] = entry elif use_originals: # No pref for original, just return the original entry return entry if candidates: candidates.sort() entries = list(self.entries) if not allow_errors: entries = [e for e in entries if not e.error_code] for pref in candidates: if pref.translate: best = self.closest_entry(pref.translate) if best: return best else: return entriesByLocale[pref.locale] # give up and give first original entries = self.non_mt_entries() if entries: return entries[0] # or first entry return self.entries[0] def safe_best_entry_in_request(self): from .auth import LanguagePreferenceCollection from pyramid.threadlocal import get_current_request req = get_current_request() # Use only when a request is in context, eg view_def if req: return self.best_lang( LanguagePreferenceCollection.getCurrent(), False) else: return self.first_original() def best_entry_in_request(self): from .auth import LanguagePreferenceCollection # Use only when a request is in context, eg view_def return self.best_lang( LanguagePreferenceCollection.getCurrent(), False) def best_entries_in_request_with_originals(self): from .auth import LanguagePreferenceCollection "Give both best and original (for view_def); avoids a roundtrip" # Use only when a request is in context, eg view_def prefs = LanguagePreferenceCollection.getCurrent() lang = self.best_lang(prefs) entries = [lang] # Punt this. # if lang.error_code: # # Wasteful to call twice, but should be rare. # entries.append(self.best_lang(prefs, False)) if all((e.is_machine_translated for e in entries)): entries.extend(self.non_mt_entries()) return entries def closest_entry(self, target_locale): def common_len(e): return locale_compatible(target_locale, e.locale) entries = [(common_len(e), id(e), e) for e in self.entries if not e.error_code] if entries: entries.sort(reverse=True) if entries[0][0]: return entries[0][2] def remove_translations(self, forget_identification=True): for entry in list(self.entries): if entry.is_machine_translated: entry.delete() elif forget_identification: entry.forget_identification(True) if inspect(self).persistent: self.db.expire(self, ["entries"]) def clone(self, db=None, tombstone=None): if tombstone is True: tombstone = datetime.utcnow() clone = self.__class__() db = db or clone.db for e in self.entries: e = e.clone(clone, db=db, tombstone=tombstone) db.add(clone) return clone # Those permissions are for an ownerless object. Accept Create before ownership. crud_permissions = CrudPermissions(P_READ, P_SYSADMIN, P_SYSADMIN, P_SYSADMIN)
[docs]class LangStringEntry(TombstonableMixin, Base): """A string bound to a given locale. Many of those form a :py:class:`LangString`""" __tablename__ = "langstring_entry" __table_args__ = ( UniqueConstraint("langstring_id", "locale", "tombstone_date"), ) def __init__(self, session=None, *args, **kwargs): """ in the kwargs, you can specify locale using locale or @language""" if ("locale" not in kwargs and '@language' in kwargs): # Create locale on demand. kwargs["locale"] = kwargs.get("@language", "und") del kwargs["@language"] super(LangStringEntry, self).__init__(*args, **kwargs) id = Column(Integer, primary_key=True) langstring_id = Column( Integer, ForeignKey(LangString.id, ondelete="CASCADE"), nullable=False, index=True) langstring = relationship( LangString, primaryjoin="LangString.id==LangStringEntry.langstring_id", backref=backref( "entries", primaryjoin="and_(LangString.id==LangStringEntry.langstring_id, " "LangStringEntry.tombstone_date == None)", lazy="subquery", cascade="all, delete-orphan")) # Should we allow locale-less LangStringEntry? (for unknown...) locale = Column(String(11), index=True) mt_trans_of_id = Column(Integer, ForeignKey( 'langstring_entry.id', ondelete='CASCADE', onupdate='CASCADE')) mt_trans_as = relationship( "LangStringEntry", foreign_keys=[mt_trans_of_id], backref=backref("mt_trans_of", remote_side=[id])) locale_identification_data = Column(String) locale_confirmed = Column( Boolean, server_default="0", doc="Locale inferred from discussion agrees with identification_data") error_count = Column( Integer, default=0, doc="Errors from the translation server") error_code = Column( SmallInteger, default=None, doc="Type of error from the translation server") # tombstone_date = Column(DateTime) implicit from Tombstonable mixin value = Column(UnicodeText) def __bool__(self): return bool(self.value) @declared_attr def import_record(cls): return relationship( ImportRecord, uselist=False, primaryjoin=(remote(ImportRecord.target_id)==foreign(cls.id)) & (ImportRecord.target_table == cls.__tablename__)) def as_jsonld(self, default_lang=None): if self.locale in (default_lang, LocaleLabel.UNDEFINED): return self.value else: return {"@language": self.locale, "@value": self.value} def set_value(self, value, clone=True): target = self if value != self.value: if value: if clone: target = self.clone(self.langstring, tombstone=True) target.value = value else: target.is_tombstone = True return target def clone(self, langstring, db=None, tombstone=None): if tombstone is True: tombstone = datetime.utcnow() clone = self.__class__( langstring=langstring, locale=self.locale, value=self.value, tombstone_date = self.tombstone_date or ( tombstone if tombstone else None), locale_identification_data=self.locale_identification_data, locale_confirmed = self.locale_confirmed, error_code=self.error_code, error_count=self.error_count) db = db or self.db db.add(clone) return clone
[docs] def populate_from_context(self, context): if not(self.langstring or self.langstring_id): self.langstring = context.get_instance_of_class(LangString) super(LangStringEntry, self).populate_from_context(context)
def find_best_sibling(self, parent, siblings): # self is a non-persistent object created from json, # and a corresponding persistent object may already exist in parent for sibling in siblings: if sibling.locale == self.locale: return sibling if self.locale == LocaleLabel.UNDEFINED: for sibling in siblings: if sibling.value == self.value: return sibling @as_native_str() def __repr__(self): value = self.value or '' if len(value) > 50: value = value[:50]+'...' if self.error_code: return (u'%d: [%s, ERROR %d] "%s"' % ( self.id or -1, self.locale or "missing", self.error_code, value)) return (u'%d: [%s] "%s"' % ( self.id or -1, self.locale or "missing", value)) @property def mt_trans_of_locale(self): if self.mt_trans_of_id: return self.mt_trans_of.locale @property def locale_code(self): if self.mt_trans_of_id: return create_mt_code(self.mt_trans_of_locale, self.locale) else: return self.locale def set_locale_code(self, locale): base, trans = split_mt_code(locale) my_trans = self.mt_trans_of_locale if trans: assert trans == my_trans elif my_trans: # clearing translation status self.mt_trans_of_id = None self.error_code = None self.error_count = 0 self.locale = base @property def locale_identification_data_json(self): return json.loads(self.locale_identification_data)\ if self.locale_identification_data else {} @locale_identification_data_json.setter def locale_identification_data_json(self, data): self.locale_identification_data = json.dumps(data) if data else None @hybrid_property def is_machine_translated(self): return self.mt_trans_of_id != None @is_machine_translated.expression def is_machine_translated(cls): # Only works if the Locale is part of the join return cls.mt_trans_of_id != None def identify_locale(self, locale_code, data, certainty=False): # A translation service proposes a data identification. # the information is deemed confirmed if it fits the initial # hypothesis given at LSE creation. changed = False old_locale_code = self.locale langstring = self.langstring or ( LangString.get(self.langstring_id) if self.langstring_id else None) if self.is_machine_translated: raise RuntimeError("Why identify a machine-translated locale?") data = data or {} original = self.locale_identification_data_json.get("original", None) if not locale_code or locale_code == LocaleLabel.UNDEFINED: if not self.locale or self.locale == LocaleLabel.UNDEFINED: # replace id data with new one. if original: data['original'] = original self.locale_identification_data_json = data return False elif original and locale_code == original: if locale_code != old_locale_code: self.locale = locale_code changed = True self.locale_identification_data_json = data self.locale_confirmed = True elif locale_code != old_locale_code: if self.locale_confirmed: if certainty: raise RuntimeError("Conflict of certainty") # keep the old confirming data return False # compare data? replacing with new for now. if not original and self.locale_identification_data: original = LocaleLabel.UNDEFINED original = original or old_locale_code if original != locale_code and original != LocaleLabel.UNDEFINED: data["original"] = original self.locale = locale_code changed = True self.locale_identification_data_json = data self.locale_confirmed = certainty else: if original and original != locale_code: data['original'] = original self.locale_identification_data_json = data self.locale_confirmed = certainty or locale_code == original if changed: if langstring: langstring.remove_translations_of(self) # Re-adding to verify there's no conflict added = langstring.add_entry(self, certainty) if added is None: # We identified an entry with something that existed # as a known original. Not sure what to do now, # reverting just in case. self.locale_code = old_locale_code changed = False return changed def forget_identification(self, force=False): if force: self.locale = LocaleLabel.UNDEFINED self.locale_confirmed = False elif not self.locale_confirmed: data = self.locale_identification_data_json orig = data.get("original", None) if orig and orig != self.locale: self.locale = orig self.locale_identification_data = None self.error_code = None self.error_count = 0
[docs] def send_to_changes(self, connection, operation=CrudOperation.DELETE, discussion_id=None, view_def="changes"): if self.langstring is not None: self.langstring.send_to_changes( connection, operation, discussion_id, view_def) else: super(LangStringEntry, self).send_to_changes( connection, operation, discussion_id, view_def)
[docs] def user_can(self, user_id, operation, permissions): if self.langstring is not None: return self.langstring.user_can(user_id, operation, permissions) return super(LangStringEntry, self).user_can(user_id, operation, permissions)
# Those permissions are for an ownerless object. Accept Create before ownership. crud_permissions = CrudPermissions(P_READ, P_SYSADMIN, P_SYSADMIN, P_SYSADMIN)
# class TranslationStamp(Base): # "For future reference. Not yet created." # __tablename__ = "translation_stamp" # id = Column(Integer, primary_key=True) # source = Column(Integer, ForeignKey(LangStringEntry.id)) # dest = Column(Integer, ForeignKey(LangStringEntry.id)) # translator = Column(Integer, ForeignKey(User.id)) # created = Column(DateTime, server_default="now()") # crud_permissions = CrudPermissions( # P_TRANSLATE, P_READ, P_SYSADMIN, P_SYSADMIN, # P_TRANSLATE, P_TRANSLATE)