diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 236d097..6a8bf63 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ + ADDED: Support for related fields - ForeignKey, ManyToManyField and + OneToOneField. + (resolves issue 15) + + v0.2 ==== Date: 2010-06-15 diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py index fa53aa7..950eeb1 100644 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from copy import deepcopy +from copy import copy from django import forms, template from django.conf import settings @@ -41,7 +41,7 @@ class TranslationAdminBase(object): field.required = True field.blank = False - field.widget = deepcopy(orig_formfield.widget) + field.widget = copy(orig_formfield.widget) class TranslationAdmin(admin.ModelAdmin, TranslationAdminBase): diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index 42974dc..2a9884c 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from django.conf import settings from django.db.models.fields import Field, CharField +from django.db.models.fields.related import (ForeignKey, OneToOneField, + ManyToManyField) from modeltranslation.utils import (get_language, build_localized_fieldname, build_localized_verbose_name) @@ -25,22 +27,27 @@ class TranslationField(Field): that needs to be specified when the field is created. """ def __init__(self, translated_field, language, *args, **kwargs): - # Store the originally wrapped field for later - self.translated_field = translated_field - self.language = language - # 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__) + # Common init + self._post_init(translated_field, language) + + def _post_init(self, translated_field, language): + """Common init for subclasses of TranslationField.""" + # Store the originally wrapped field for later + self.translated_field = translated_field + self.language = language + # Translation are always optional (for now - maybe add some parameters # to the translation options for configuring this) self.null = True self.blank = True # Adjust the name of this field to reflect the language - self.attname = build_localized_fieldname(translated_field.name, - language) + self.attname = build_localized_fieldname(self.translated_field.name, + self.language) self.name = self.attname # Copy the verbose name and append a language suffix @@ -66,11 +73,6 @@ class TranslationField(Field): def get_internal_type(self): return self.translated_field.get_internal_type() - #def contribute_to_class(self, cls, name): - #super(TranslationField, self).contribute_to_class(cls, name) - ##setattr(cls, 'get_%s_display' % self.name, - ##curry(cls._get_FIELD_display, field=self)) - def south_field_triple(self): """Returns a suitable description of this field for South.""" # We'll just introspect the _actual_ field. @@ -89,26 +91,59 @@ class TranslationField(Field): return super(TranslationField, self).formfield(*args, **defaults) -#class CurrentLanguageField(CharField): - #def __init__(self, **kwargs): - #super(CurrentLanguageField, self).__init__(null=True, max_length=5, - #**kwargs) +class RelatedTranslationField(object): + """ + Mixin class which handles shared init of a translated relation field. + """ + def _related_pre_init(self, translated_field, language, *args, **kwargs): + self.translated_field = translated_field + self.language = language - #def contribute_to_class(self, cls, name): - #super(CurrentLanguageField, self).contribute_to_class(cls, name) - #registry = CurrentLanguageFieldRegistry() - #registry.add_field(cls, self) + self.field_name = self.translated_field.name + self.translated_field_name = \ + build_localized_fieldname(self.translated_field.name, + self.language) + + # Dynamically add a related_name to the original field + translated_field.rel.related_name = \ + '%s%s' % (self.translated_field.model._meta.module_name, + self.field_name) + + TranslationField.__init__(self, self.translated_field, self.language, + *args, **kwargs) + + def _related_post_init(self): + # Dynamically add a related_name to the translation fields + self.rel.related_name = \ + '%s%s' % (self.translated_field.model._meta.module_name, + self.translated_field_name) + + # ForeignKey's init overrides some essential values from + # TranslationField, they have to be reassigned. + TranslationField._post_init(self, self.translated_field, self.language) -#class CurrentLanguageFieldRegistry(object): - #_registry = {} +class ForeignKeyTranslationField(ForeignKey, TranslationField, + RelatedTranslationField): + def __init__(self, translated_field, language, to, to_field=None, *args, + **kwargs): + self._related_pre_init(translated_field, language, *args, **kwargs) + ForeignKey.__init__(self, to, to_field, **kwargs) + self._related_post_init() - #def add_field(self, model, field): - #reg = self.__class__._registry.setdefault(model, []) - #reg.append(field) - #def get_fields(self, model): - #return self.__class__._registry.get(model, []) +class OneToOneTranslationField(OneToOneField, TranslationField, + RelatedTranslationField): + def __init__(self, translated_field, language, to, to_field=None, *args, + **kwargs): + self._related_pre_init(translated_field, language, *args, **kwargs) + OneToOneField.__init__(self, to, to_field, **kwargs) + self._related_post_init() - #def __contains__(self, model): - #return model in self.__class__._registry + +class ManyToManyTranslationField(ManyToManyField, TranslationField, + RelatedTranslationField): + def __init__(self, translated_field, language, to, *args, **kwargs): + self._related_pre_init(translated_field, language, *args, **kwargs) + ManyToManyField.__init__(self, to, **kwargs) + self._related_post_init() diff --git a/modeltranslation/tests.py b/modeltranslation/tests.py index b8fafcf..69db259 100644 --- a/modeltranslation/tests.py +++ b/modeltranslation/tests.py @@ -11,7 +11,7 @@ from django.utils.translation import ugettext_lazy from modeltranslation import translator -# TODO: tests for TranslationAdmin +# TODO: Tests for TranslationAdmin, RelatedTranslationField and subclasses settings.LANGUAGES = (('de', 'Deutsch'), ('en', 'English')) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 0a55362..2a33c9f 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -6,11 +6,17 @@ from django.db.models import signals from django.db.models.base import ModelBase from django.utils.functional import curry -from modeltranslation.fields import TranslationField +from modeltranslation.fields import (TranslationField, + ForeignKeyTranslationField, + OneToOneTranslationField, + ManyToManyTranslationField) from modeltranslation.utils import (TranslationFieldDescriptor, build_localized_fieldname) +RELATED_CLASSES = ('ManyToOneRel', 'OneToOneRel', 'ManyToManyRel',) + + class AlreadyRegistered(Exception): pass @@ -60,9 +66,26 @@ def add_localized_fields(model): # This approach implements the translation fields as full valid # django model fields and therefore adds them via add_to_class - localized_field = model.add_to_class( \ - localized_field_name, - TranslationField(model._meta.get_field(field_name), l[0])) + field = model._meta.get_field(field_name) + field_class_name = field.rel.__class__.__name__ + if field_class_name in RELATED_CLASSES: + to = field.rel.to._meta.object_name + if field_class_name == 'ManyToOneRel': + translation_field = ForeignKeyTranslationField(\ + translated_field=field, + language=l[0], to=to) + elif field_class_name == 'OneToOneRel': + translation_field = OneToOneTranslationField(\ + translated_field=field, + language=l[0], to=to) + elif field_class_name == 'ManyToManyRel': + translation_field = ManyToManyTranslationField(\ + translated_field=field, + language=l[0], to=to) + else: + translation_field = TranslationField(field, l[0]) + localized_field = model.add_to_class(localized_field_name, + translation_field) localized_fields[field_name].append(localized_field_name) return localized_fields @@ -147,14 +170,14 @@ class Translator(object): # Create a reverse dict mapping the localized_fieldnames to the # original fieldname rev_dict = dict() - for orig_name, loc_names in \ + for orig_name, loc_names in\ translation_opts.localized_fieldnames.items(): for ln in loc_names: rev_dict[ln] = orig_name translation_opts.localized_fieldnames_rev = rev_dict - # print "Applying descriptor field for model %s" % model + #print "Applying descriptor field for model %s" % model for field_name in translation_opts.fields: setattr(model, field_name, TranslationFieldDescriptor(field_name)) diff --git a/modeltranslation/utils.py b/modeltranslation/utils.py index 8f7713c..c1030dc 100644 --- a/modeltranslation/utils.py +++ b/modeltranslation/utils.py @@ -11,7 +11,6 @@ def get_language(): """ Return an active language code that is guaranteed to be in settings.LANGUAGES (Django does not seem to guarantee this for us.) - """ lang = _get_language() available_languages = [l[0] for l in settings.LANGUAGES] @@ -65,65 +64,11 @@ class TranslationFieldDescriptor(object): if hasattr(instance, loc_field_name): return getattr(instance, loc_field_name) or \ instance.__dict__[self.name] - return instance.__dict__[self.name] + #return instance.__dict__[self.name] + # FIXME: KeyError raised for ForeignKeyTanslationField + # in admin list view + try: + return instance.__dict__[self.name] + except KeyError: + return None - -#def create_model(name, fields=None, app_label='', module='', options=None, - #admin_opts=None): - #""" - #Create specified model. - #This is taken from http://code.djangoproject.com/wiki/DynamicModels - #""" - #class Meta: - ## Using type('Meta', ...) gives a dictproxy error during model - ## creation - #pass - - #if app_label: - ## app_label must be set using the Meta inner class - #setattr(Meta, 'app_label', app_label) - - ## Update Meta with any options that were provided - #if options is not None: - #for key, value in options.iteritems(): - #setattr(Meta, key, value) - - ## Set up a dictionary to simulate declarations within a class - #attrs = {'__module__': module, 'Meta': Meta} - - ## Add in any fields that were provided - #if fields: - #attrs.update(fields) - - ## Create the class, which automatically triggers ModelBase processing - #model = type(name, (models.Model,), attrs) - - ## Create an Admin class if admin options were provided - #if admin_opts is not None: - #class Admin(admin.ModelAdmin): - #pass - #for key, value in admin_opts: - #setattr(Admin, key, value) - #admin.site.register(model, Admin) - - #return model - - -def copy_field(field): - """ - Instantiate a new field, with all of the values from the old one, except - the to and to_field in the case of related fields. - - This taken from http://www.djangosnippets.org/snippets/442/ - """ - base_kw = dict([(n, getattr(field, n, '_null')) for n in \ - models.fields.Field.__init__.im_func.func_code.co_varnames]) - if isinstance(field, models.fields.related.RelatedField): - rel = base_kw.get('rel') - rel_kw = dict([(n, getattr(rel, n, '_null')) for n in \ - rel.__init__.im_func.func_code.co_varnames]) - if isinstance(field, models.fields.related.ForeignKey): - base_kw['to_field'] = rel_kw.pop('field_name') - base_kw.update(rel_kw) - base_kw.pop('self') - return field.__class__(**base_kw)