Added support for related fields - ForeignKey, ManyToManyField and OneToOneField. Resolves issue 15.

This commit is contained in:
Dirk Eschler 2010-06-15 12:39:04 +00:00
parent f1aa871b75
commit 2e4a85e791
6 changed files with 107 additions and 99 deletions

View file

@ -1,3 +1,8 @@
ADDED: Support for related fields - ForeignKey, ManyToManyField and
OneToOneField.
(resolves issue 15)
v0.2
====
Date: 2010-06-15

View file

@ -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):

View file

@ -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()

View file

@ -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'))

View file

@ -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))

View file

@ -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)