def populate_translation_fields(sender, kwargs): """ When models are created or loaded from fixtures, replicates values provided for translatable fields to some / all empty translation fields, according to the current population mode. Population is performed only on keys (field names) present in kwargs. Nothing is returned, but passed kwargs dictionary is altered. With ``mode`` set to: -- ``all``: fills all translation fields, skipping just those for which a translated value is also provided; -- ``default``: fills only the default translation (unless it is additionally provided); -- ``required``: like ``default``, but only if the original field is non-nullable; At least the ``required`` mode should be used when loading untranslated fixtures to keep the database consistent (note that Django management commands are normally forced to run with hardcoded ``en-us`` language active). The ``default`` mode is useful if you need to ensure fallback values are available, and ``all`` if you need to have all translations defined (for example to make lookups / filtering without resorting to query fallbacks). """ populate = mt_settings.AUTO_POPULATE if not populate: return if populate is True: # What was meant by ``True`` is now called ``all``. populate = 'all' opts = translator.get_options_for_model(sender) for key, val in list(kwargs.items()): if key in opts.fields: if populate == 'all': # Set the value for every language. for translation_field in opts.fields[key]: kwargs.setdefault(translation_field.name, val) elif populate == 'default': default = build_localized_fieldname( key, mt_settings.DEFAULT_LANGUAGE) kwargs.setdefault(default, val) elif populate == 'required': default = build_localized_fieldname( key, mt_settings.DEFAULT_LANGUAGE) if not sender._meta.get_field(key).null: kwargs.setdefault(default, val) else: raise AttributeError("Unknown population mode '%s'." % populate)
def populate_translation_fields(sender, kwargs): """ When models are created or loaded from fixtures, replicates values provided for translatable fields to some / all empty translation fields, according to the current population mode. Population is performed only on keys (field names) present in kwargs. Nothing is returned, but passed kwargs dictionary is altered. With ``mode`` set to: -- ``all``: fills all translation fields, skipping just those for which a translated value is also provided; -- ``default``: fills only the default translation (unless it is additionally provided); -- ``required``: like ``default``, but only if the original field is non-nullable; At least the ``required`` mode should be used when loading untranslated fixtures to keep the database consistent (note that Django management commands are normally forced to run with hardcoded ``en-us`` language active). The ``default`` mode is useful if you need to ensure fallback values are available, and ``all`` if you need to have all translations defined (for example to make lookups / filtering without resorting to query fallbacks). """ populate = mt_settings.AUTO_POPULATE if not populate: return if populate is True: # What was meant by ``True`` is now called ``all``. populate = 'all' opts = translator.get_options_for_model(sender) for key, val in list(kwargs.items()): if key in opts.fields: if populate == 'all': # Set the value for every language. for translation_field in opts.fields[key]: kwargs.setdefault(translation_field.name, val) elif populate == 'default': default = build_localized_fieldname(key, mt_settings.DEFAULT_LANGUAGE) kwargs.setdefault(default, val) elif populate == 'required': default = build_localized_fieldname(key, mt_settings.DEFAULT_LANGUAGE) if not sender._meta.get_field(key).null: kwargs.setdefault(default, val) else: raise AttributeError("Unknown population mode '%s'." % populate)
def __get__(self, instance, owner): """ Returns value from the translation field for the current language, or value for some another language according to fallback languages, or the custom fallback value, or field's default value. """ if instance is None: return self default = NONE undefined = self.fallback_undefined if undefined is NONE: default = self.field.get_default() undefined = default langs = resolution_order(get_language(), self.fallback_languages) for lang in langs: loc_field_name = build_localized_fieldname(self.field.name, lang) val = getattr(instance, loc_field_name, None) if self.meaningful_value(val, undefined): return val if mt_settings.ENABLE_FALLBACKS and self.fallback_value is not NONE: return self.fallback_value else: if default is NONE: default = self.field.get_default() # Some fields like FileField behave strange, as their get_default() doesn't return # instance of attr_class, but rather None or ''. # Normally this case is handled in the descriptor, but since we have overridden it, we # must mock it up. if (isinstance(self.field, fields.files.FileField) and not isinstance(default, self.field.attr_class)): return self.field.attr_class(instance, self.field, default) return default
def add_translation_fields(model, opts): """ Monkey patches the original model class to provide additional fields for every language. Adds newly created translation fields to the given translation options. """ model_empty_values = getattr(opts, 'empty_values', NONE) for field_name in opts.local_fields.keys(): field_empty_value = parse_field(model_empty_values, field_name, NONE) for l in mt_settings.AVAILABLE_LANGUAGES: # Create a dynamic translation field translation_field = create_translation_field( model=model, field_name=field_name, lang=l, empty_value=field_empty_value) # Construct the name for the localized field localized_field_name = build_localized_fieldname(field_name, l) # Check if the model already has a field by that name if hasattr(model, localized_field_name): raise ValueError( "Error adding translation field. Model '%s' already contains a field named" "'%s'." % (model._meta.object_name, localized_field_name)) # This approach implements the translation fields as full valid # django model fields and therefore adds them via add_to_class model.add_to_class(localized_field_name, translation_field) opts.add_translation_field(field_name, translation_field) # Rebuild information about parents fields. If there are opts.local_fields, field cache would be # invalidated (by model._meta.add_field() function). Otherwise, we need to do it manually. if len(opts.local_fields) == 0: try: model._meta._fill_fields_cache() except AttributeError: # Django 1.8 removed _fill_fields_cache model._meta._expire_cache() model._meta.get_fields()
def handle_noargs(self, **options): verbosity = int(options['verbosity']) if verbosity > 0: self.stdout.write("Using default language: %s\n" % DEFAULT_LANGUAGE) models = translator.get_registered_models(abstract=False) for model in models: if verbosity > 0: self.stdout.write("Updating data of model '%s'\n" % model) opts = translator.get_options_for_model(model) for field_name in opts.fields.keys(): def_lang_fieldname = build_localized_fieldname( field_name, DEFAULT_LANGUAGE) # We'll only update fields which do not have an existing value q = Q(**{def_lang_fieldname: None}) field = model._meta.get_field(field_name) if field.empty_strings_allowed: q |= Q(**{def_lang_fieldname: ''}) if issubclass(model, Page): for obj in model._default_manager.filter(q): # Get table description in order to know if field is # in child or parent table # TODO: Tested only on PostgreSQL engine db_table = model._meta.db_table db_table_desc = connection.introspection.\ get_table_description( connection.cursor(), db_table) # original field in child class if field_name in [x.name for x in db_table_desc]: raw = model._default_manager.raw( 'SELECT *, %s AS original_field FROM %s \ WHERE page_ptr_id=%d LIMIT 1' % (field_name, db_table, obj.page_ptr_id))[0] setattr(obj, def_lang_fieldname, raw.original_field) # field is a foreign key elif (field_name + '_id') in\ [x.name for x in db_table_desc]: raw = model._default_manager.raw( 'SELECT *, %s AS original_field FROM %s \ WHERE page_ptr_id=%d LIMIT 1' % (field_name + '_id', db_table, obj.page_ptr_id))[0] setattr(obj, def_lang_fieldname + '_id', raw.original_field) # original field parent class else: raw = Page._default_manager.raw( 'SELECT *, %s AS original_field FROM \ wagtailcore_page WHERE id=%d LIMIT 1' % (field_name, obj.page_ptr_id))[0] setattr(obj, def_lang_fieldname, raw.original_field) obj.save(update_fields=[def_lang_fieldname]) else: model._default_manager.filter(q).rewrite(False).update( **{def_lang_fieldname: F(field_name)})
def __set__(self, instance, value): lang = get_language() loc_field_name = build_localized_fieldname(self.field_name, lang) # Localized field name with '_id' loc_attname = instance._meta.get_field(loc_field_name).get_attname() setattr(instance, loc_attname, value) base_attname = instance._meta.get_field(self.field_name).get_attname() instance.__dict__[base_attname] = value
def get_missing_languages(self, field_name, db_table): """ Gets only missings fields. """ db_table_fields = self.get_table_fields(db_table) for lang_code in AVAILABLE_LANGUAGES: if build_localized_fieldname(field_name, lang_code) not in db_table_fields: yield lang_code
def __set__(self, instance, value): """ Updates the translation field for the current language. """ if getattr(instance, '_mt_init', False): # When assignment takes place in model instance constructor, don't set value. # This is essential for only/defer to work, but I think it's sensible anyway. return loc_field_name = build_localized_fieldname(self.field.name, get_language()) setattr(instance, loc_field_name, value)
def __get__(self, instance, owner): if instance is None: return self langs = resolution_order(get_language(), self.fallback_languages) for lang in langs: loc_field_name = build_localized_fieldname(self.field_name, lang) # Localized field name with '_id' loc_attname = instance._meta.get_field(loc_field_name).get_attname() val = getattr(instance, loc_attname, None) if val is not None: return val return None
def handle_noargs(self, **options): verbosity = int(options['verbosity']) if verbosity > 0: self.stdout.write( "Using default language: %s\n" % DEFAULT_LANGUAGE) models = translator.get_registered_models(abstract=False) for model in models: if verbosity > 0: self.stdout.write("Updating data of model '%s'\n" % model) opts = translator.get_options_for_model(model) for field_name in opts.fields.keys(): def_lang_fieldname = build_localized_fieldname( field_name, DEFAULT_LANGUAGE) # We'll only update fields which do not have an existing value q = Q(**{def_lang_fieldname: None}) field = model._meta.get_field(field_name) if field.empty_strings_allowed: q |= Q(**{def_lang_fieldname: ''}) if issubclass(model, Page): for obj in model._default_manager.filter(q): # Get table description in order to know if field is # in child or parent table # TODO: Tested only on PostgreSQL engine db_table = model._meta.db_table db_table_desc = connection.introspection.\ get_table_description( connection.cursor(), db_table) # original field in child class if field_name in [x.name for x in db_table_desc]: raw = model._default_manager.raw( 'SELECT *, %s AS original_field FROM %s \ WHERE page_ptr_id=%d LIMIT 1' % ( field_name, db_table, obj.page_ptr_id))[0] setattr( obj, def_lang_fieldname, raw.original_field) # original field parent class else: raw = Page._default_manager.raw( 'SELECT *, %s AS original_field FROM \ wagtailcore_page WHERE id=%d LIMIT 1' % ( field_name, obj.page_ptr_id))[0] setattr( obj, def_lang_fieldname, raw.original_field) obj.save(update_fields=[def_lang_fieldname]) else: model._default_manager.filter(q).rewrite(False).update( **{def_lang_fieldname: F(field_name)})
def append_fallback(model, fields): """ If translated field is encountered, add also all its fallback fields. Returns tuple: (set_of_new_fields_to_use, set_of_translated_field_names) """ fields = set(fields) trans = set() from wagtail_modeltranslation.translator import translator opts = translator.get_options_for_model(model) for key, _ in opts.fields.items(): if key in fields: langs = resolution_order(get_language(), getattr(model, key).fallback_languages) fields = fields.union(build_localized_fieldname(key, lang) for lang in langs) fields.remove(key) trans.add(key) return fields, trans
def __set__(self, instance, value): """ Updates the translation field for the current language. """ # In order for deferred fields to work, we also need to set the base value instance.__dict__[self.field.name] = value if isinstance(self.field, fields.related.ForeignKey): instance.__dict__[ self.field.get_attname()] = None if value is None else value.pk if getattr(instance, '_mt_init', False): # When assignment takes place in model instance constructor, don't set value. # This is essential for only/defer to work, but I think it's sensible anyway. return loc_field_name = build_localized_fieldname(self.field.name, get_language()) setattr(instance, loc_field_name, value)
def get_sync_sql(self, field_name, missing_langs, model): """ Returns SQL needed for sync schema for a new translatable field. """ qn = connection.ops.quote_name style = no_style() sql_output = [] db_table = model._meta.db_table for lang in missing_langs: new_field = build_localized_fieldname(field_name, lang) f = model._meta.get_field(new_field) col_type = f.db_type(connection=connection) field_sql = [style.SQL_FIELD(qn(f.column)), style.SQL_COLTYPE(col_type)] # column creation stmt = "ALTER TABLE %s ADD COLUMN %s" % (qn(db_table), ' '.join(field_sql)) if not f.null: stmt += " " + style.SQL_KEYWORD('NOT NULL') sql_output.append(stmt + ";") return sql_output
def rewrite_lookup_key(model, lookup_key): pieces = lookup_key.split('__', 1) original_key = pieces[0] translatable_fields = get_translatable_fields_for_model(model) if translatable_fields is not None: # If we are doing a lookup on a translatable field, # we want to rewrite it to the actual field name # For example, we want to rewrite "name__startswith" to "name_fr__startswith" if pieces[0] in translatable_fields: pieces[0] = build_localized_fieldname(pieces[0], get_language()) if len(pieces) > 1: # Check if we are doing a lookup to a related trans model fields_to_trans_models = get_fields_to_translatable_models(model) # Check ``original key``, as pieces[0] may have been already rewritten. if original_key in fields_to_trans_models: transmodel = fields_to_trans_models[original_key] pieces[1] = rewrite_lookup_key(transmodel, pieces[1]) return '__'.join(pieces)
def handle_noargs(self, **options): verbosity = int(options['verbosity']) if verbosity > 0: self.stdout.write("Using default language: %s\n" % DEFAULT_LANGUAGE) models = translator.get_registered_models(abstract=False) for model in models: if verbosity > 0: self.stdout.write("Updating data of model '%s'\n" % model) opts = translator.get_options_for_model(model) for field_name in opts.fields.keys(): def_lang_fieldname = build_localized_fieldname(field_name, DEFAULT_LANGUAGE) # We'll only update fields which do not have an existing value q = Q(**{def_lang_fieldname: None}) field = model._meta.get_field(field_name) if field.empty_strings_allowed: q |= Q(**{def_lang_fieldname: ""}) model._default_manager.filter(q).rewrite(False).update( **{def_lang_fieldname: F(field_name)})
def add_translation_fields(model, opts): """ Monkey patches the original model class to provide additional fields for every language. Adds newly created translation fields to the given translation options. """ model_empty_values = getattr(opts, 'empty_values', NONE) for field_name in opts.local_fields.keys(): field_empty_value = parse_field(model_empty_values, field_name, NONE) for l in mt_settings.AVAILABLE_LANGUAGES: # Create a dynamic translation field translation_field = create_translation_field( model=model, field_name=field_name, lang=l, empty_value=field_empty_value) # Construct the name for the localized field localized_field_name = build_localized_fieldname(field_name, l) # Check if the model already has a field by that name if hasattr(model, localized_field_name): # Check if are not dealing with abstract field inherited. for cls in model.__mro__: if hasattr(cls, '_meta') and cls.__dict__.get(localized_field_name, None): cls_opts = translator._get_options_for_model(cls) if not cls._meta.abstract or field_name not in cls_opts.local_fields: raise ValueError("Error adding translation field. Model '%s' already" " contains a field named '%s'." % (model._meta.object_name, localized_field_name)) # This approach implements the translation fields as full valid # django model fields and therefore adds them via add_to_class model.add_to_class(localized_field_name, translation_field) opts.add_translation_field(field_name, translation_field) # Rebuild information about parents fields. If there are opts.local_fields, field cache would be # invalidated (by model._meta.add_field() function). Otherwise, we need to do it manually. if len(opts.local_fields) == 0: try: model._meta._fill_fields_cache() except AttributeError: # Django 1.8 removed _fill_fields_cache model._meta._expire_cache() model._meta.get_fields()
def handle_noargs(self, **options): verbosity = int(options['verbosity']) if verbosity > 0: self.stdout.write("Using default language: %s\n" % DEFAULT_LANGUAGE) models = translator.get_registered_models(abstract=False) for model in models: if verbosity > 0: self.stdout.write("Updating data of model '%s'\n" % model) opts = translator.get_options_for_model(model) for field_name in opts.fields.keys(): def_lang_fieldname = build_localized_fieldname( field_name, DEFAULT_LANGUAGE) # We'll only update fields which do not have an existing value q = Q(**{def_lang_fieldname: None}) field = model._meta.get_field(field_name) if field.empty_strings_allowed: q |= Q(**{def_lang_fieldname: ""}) model._default_manager.filter(q).rewrite(False).update( **{def_lang_fieldname: F(field_name)})
def get_sync_sql(self, field_name, missing_langs, model): """ Returns SQL needed for sync schema for a new translatable field. """ qn = connection.ops.quote_name style = no_style() sql_output = [] db_table = model._meta.db_table for lang in missing_langs: new_field = build_localized_fieldname(field_name, lang) f = model._meta.get_field(new_field) col_type = f.db_type(connection=connection) field_sql = [ style.SQL_FIELD(qn(f.column)), style.SQL_COLTYPE(col_type) ] # column creation stmt = "ALTER TABLE %s ADD COLUMN %s" % (qn(db_table), ' '.join(field_sql)) if not f.null: stmt += " " + style.SQL_KEYWORD('NOT NULL') sql_output.append(stmt + ";") return sql_output
def cache_name(self): lang = get_language() cache = build_localized_fieldname(self.accessor, lang) return "_%s_cache" % cache
def __init__(self, translated_field, language, empty_value, *args, **kwargs): from wagtail_modeltranslation.translator import translator # Update the dict of this field with the content of the original one # This might be a bit radical?! Seems to work though... self.__dict__.update(translated_field.__dict__) # Store the originally wrapped field for later self.translated_field = translated_field self.language = language self.empty_value = empty_value if empty_value is NONE: self.empty_value = None if translated_field.null else '' # Default behaviour is that all translations are optional if not isinstance(self, fields.BooleanField): # TODO: Do we really want to enforce null *at all*? Shouldn't this # better honour the null setting of the translated field? self.null = True self.blank = True # Take required_languages translation option into account trans_opts = translator.get_options_for_model(self.model) if trans_opts.required_languages: required_languages = trans_opts.required_languages if isinstance(trans_opts.required_languages, (tuple, list)): # All fields if self.language in required_languages: # self.null = False self.blank = False else: # Certain fields only # Try current language - if not present, try 'default' key try: req_fields = required_languages[self.language] except KeyError: req_fields = required_languages.get('default', ()) if self.name in req_fields: # TODO: We might have to handle the whole thing through the # FieldsAggregationMetaClass, as fields can be inherited. # self.null = False self.blank = False # Adjust the name of this field to reflect the language self.attname = build_localized_fieldname(self.translated_field.name, language) self.name = self.attname if self.translated_field.db_column: self.db_column = build_localized_fieldname( self.translated_field.db_column, language) self.column = self.db_column # Copy the verbose name and append a language suffix # (will show up e.g. in the admin). self.verbose_name = build_localized_verbose_name( translated_field.verbose_name, language) # ForeignKey support - rewrite related_name if not NEW_RELATED_API and self.rel and self.related and not self.rel.is_hidden( ): import copy current = self.related.get_accessor_name() self.rel = copy.copy( self.rel) # Since fields cannot share the same rel object. # self.related doesn't need to be copied, as it will be recreated in # ``RelatedField.do_related_class`` if self.rel.related_name is None: # For implicit related_name use different query field name loc_related_query_name = build_localized_fieldname( self.related_query_name(), self.language) self.related_query_name = lambda: loc_related_query_name self.rel.related_name = build_localized_fieldname( current, self.language) self.rel.field = self # Django 1.6 if hasattr(self.rel.to._meta, '_related_objects_cache'): del self.rel.to._meta._related_objects_cache elif NEW_RELATED_API and self.remote_field and not self.remote_field.is_hidden( ): import copy current = self.remote_field.get_accessor_name() # Since fields cannot share the same rel object: self.remote_field = copy.copy(self.remote_field) if self.remote_field.related_name is None: # For implicit related_name use different query field name loc_related_query_name = build_localized_fieldname( self.related_query_name(), self.language) self.related_query_name = lambda: loc_related_query_name self.remote_field.related_name = build_localized_fieldname( current, self.language) self.remote_field.field = self # Django 1.6 if hasattr(self.remote_field.to._meta, '_related_objects_cache'): del self.remote_field.to._meta._related_objects_cache
def __set__(self, instance, value): lang = get_language() loc_field_name = build_localized_fieldname(self.field_name, lang) # Localized field name with '_id' loc_attname = instance._meta.get_field(loc_field_name).get_attname() setattr(instance, loc_attname, value)
def __init__(self, translated_field, language, empty_value, *args, **kwargs): from wagtail_modeltranslation.translator import translator # Update the dict of this field with the content of the original one # This might be a bit radical?! Seems to work though... self.__dict__.update(translated_field.__dict__) # Store the originally wrapped field for later self.translated_field = translated_field self.language = language self.empty_value = empty_value if empty_value is NONE: self.empty_value = None if translated_field.null else '' # Default behaviour is that all translations are optional if not isinstance(self, fields.BooleanField): # TODO: Do we really want to enforce null *at all*? Shouldn't this # better honour the null setting of the translated field? self.null = True self.blank = True # Take required_languages translation option into account trans_opts = translator.get_options_for_model(self.model) if trans_opts.required_languages: required_languages = trans_opts.required_languages if isinstance(trans_opts.required_languages, (tuple, list)): # All fields if self.language in required_languages: # self.null = False self.blank = False else: # Certain fields only # Try current language - if not present, try 'default' key try: req_fields = required_languages[self.language] except KeyError: req_fields = required_languages.get('default', ()) if self.name in req_fields: # TODO: We might have to handle the whole thing through the # FieldsAggregationMetaClass, as fields can be inherited. # self.null = False self.blank = False # Adjust the name of this field to reflect the language self.attname = build_localized_fieldname(self.translated_field.name, language) self.name = self.attname if self.translated_field.db_column: self.db_column = build_localized_fieldname(self.translated_field.db_column, language) self.column = self.db_column # Copy the verbose name and append a language suffix # (will show up e.g. in the admin). self.verbose_name = build_localized_verbose_name(translated_field.verbose_name, language) # ForeignKey support - rewrite related_name if not NEW_RELATED_API and self.rel and self.related and not self.rel.is_hidden(): import copy current = self.related.get_accessor_name() self.rel = copy.copy(self.rel) # Since fields cannot share the same rel object. # self.related doesn't need to be copied, as it will be recreated in # ``RelatedField.do_related_class`` if self.rel.related_name is None: # For implicit related_name use different query field name loc_related_query_name = build_localized_fieldname( self.related_query_name(), self.language) self.related_query_name = lambda: loc_related_query_name self.rel.related_name = build_localized_fieldname(current, self.language) self.rel.field = self # Django 1.6 if hasattr(self.rel.to._meta, '_related_objects_cache'): del self.rel.to._meta._related_objects_cache elif NEW_RELATED_API and self.remote_field and not self.remote_field.is_hidden(): import copy current = self.remote_field.get_accessor_name() # Since fields cannot share the same rel object: self.remote_field = copy.copy(self.remote_field) if self.remote_field.related_name is None: # For implicit related_name use different query field name loc_related_query_name = build_localized_fieldname( self.related_query_name(), self.language) self.related_query_name = lambda: loc_related_query_name self.remote_field.related_name = build_localized_fieldname(current, self.language) self.remote_field.field = self # Django 1.6 if hasattr(self.remote_field.to._meta, '_related_objects_cache'): del self.remote_field.to._meta._related_objects_cache