Fixed #10271, #10281 -- Fixed the handling multiple inline models that share a common base class and have the link to the inline parent on the base class. Includes modifications that allow the equivalent handling for GenericFields. Thanks to Idan Gazit, Antti Kaihola (akaihola), and Alex Gaynor for their work on this patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@10017 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2009-03-10 11:19:26 +00:00
parent d0fff8ccd4
commit 3c8568a7dc
8 changed files with 228 additions and 52 deletions

View File

@ -161,6 +161,7 @@ answer newbie questions, and generally made Django that much better:
Marc Garcia <marc.garcia@accopensys.com> Marc Garcia <marc.garcia@accopensys.com>
Alex Gaynor <alex.gaynor@gmail.com> Alex Gaynor <alex.gaynor@gmail.com>
Andy Gayton <andy-django@thecablelounge.com> Andy Gayton <andy-django@thecablelounge.com>
Idan Gazit
Baishampayan Ghose Baishampayan Ghose
Dimitris Glezos <dimitris@glezos.com> Dimitris Glezos <dimitris@glezos.com>
glin@seznam.cz glin@seznam.cz

View File

@ -522,10 +522,16 @@ class ModelAdmin(BaseModelAdmin):
else: else:
form_validated = False form_validated = False
new_object = self.model() new_object = self.model()
prefixes = {}
for FormSet in self.get_formsets(request): for FormSet in self.get_formsets(request):
prefix = FormSet.get_default_prefix()
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1:
prefix = "%s-%s" % (prefix, prefixes[prefix])
formset = FormSet(data=request.POST, files=request.FILES, formset = FormSet(data=request.POST, files=request.FILES,
instance=new_object, instance=new_object,
save_as_new=request.POST.has_key("_saveasnew")) save_as_new=request.POST.has_key("_saveasnew"),
prefix=prefix)
formsets.append(formset) formsets.append(formset)
if all_valid(formsets) and form_validated: if all_valid(formsets) and form_validated:
self.save_model(request, new_object, form, change=False) self.save_model(request, new_object, form, change=False)
@ -547,8 +553,13 @@ class ModelAdmin(BaseModelAdmin):
if isinstance(f, models.ManyToManyField): if isinstance(f, models.ManyToManyField):
initial[k] = initial[k].split(",") initial[k] = initial[k].split(",")
form = ModelForm(initial=initial) form = ModelForm(initial=initial)
prefixes = {}
for FormSet in self.get_formsets(request): for FormSet in self.get_formsets(request):
formset = FormSet(instance=self.model()) prefix = FormSet.get_default_prefix()
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1:
prefix = "%s-%s" % (prefix, prefixes[prefix])
formset = FormSet(instance=self.model(), prefix=prefix)
formsets.append(formset) formsets.append(formset)
adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields) adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
@ -608,9 +619,14 @@ class ModelAdmin(BaseModelAdmin):
else: else:
form_validated = False form_validated = False
new_object = obj new_object = obj
prefixes = {}
for FormSet in self.get_formsets(request, new_object): for FormSet in self.get_formsets(request, new_object):
prefix = FormSet.get_default_prefix()
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1:
prefix = "%s-%s" % (prefix, prefixes[prefix])
formset = FormSet(request.POST, request.FILES, formset = FormSet(request.POST, request.FILES,
instance=new_object) instance=new_object, prefix=prefix)
formsets.append(formset) formsets.append(formset)
if all_valid(formsets) and form_validated: if all_valid(formsets) and form_validated:
@ -625,8 +641,13 @@ class ModelAdmin(BaseModelAdmin):
else: else:
form = ModelForm(instance=obj) form = ModelForm(instance=obj)
prefixes = {}
for FormSet in self.get_formsets(request, obj): for FormSet in self.get_formsets(request, obj):
formset = FormSet(instance=obj) prefix = FormSet.get_default_prefix()
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1:
prefix = "%s-%s" % (prefix, prefixes[prefix])
formset = FormSet(instance=obj, prefix=prefix)
formsets.append(formset) formsets.append(formset)
adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields) adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)

View File

@ -291,7 +291,7 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
ct_field_name = "content_type" ct_field_name = "content_type"
ct_fk_field_name = "object_id" ct_fk_field_name = "object_id"
def __init__(self, data=None, files=None, instance=None, save_as_new=None): def __init__(self, data=None, files=None, instance=None, save_as_new=None, prefix=None):
opts = self.model._meta opts = self.model._meta
self.instance = instance self.instance = instance
self.rel_name = '-'.join(( self.rel_name = '-'.join((
@ -300,9 +300,17 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
)) ))
super(BaseGenericInlineFormSet, self).__init__( super(BaseGenericInlineFormSet, self).__init__(
queryset=self.get_queryset(), data=data, files=files, queryset=self.get_queryset(), data=data, files=files,
prefix=self.rel_name prefix=prefix
) )
#@classmethod
def get_default_prefix(cls):
opts = cls.model._meta
return '-'.join((opts.app_label, opts.object_name.lower(),
cls.ct_field.name, cls.ct_fk_field.name,
))
get_default_prefix = classmethod(get_default_prefix)
def get_queryset(self): def get_queryset(self):
# Avoid a circular import. # Avoid a circular import.
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType

View File

@ -32,7 +32,7 @@ class BaseFormSet(StrAndUnicode):
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList): initial=None, error_class=ErrorList):
self.is_bound = data is not None or files is not None self.is_bound = data is not None or files is not None
self.prefix = prefix or 'form' self.prefix = prefix or self.get_default_prefix()
self.auto_id = auto_id self.auto_id = auto_id
self.data = data self.data = data
self.files = files self.files = files
@ -176,6 +176,11 @@ class BaseFormSet(StrAndUnicode):
return [self.forms[i[0]] for i in self._ordering] return [self.forms[i[0]] for i in self._ordering]
ordered_forms = property(_get_ordered_forms) ordered_forms = property(_get_ordered_forms)
#@classmethod
def get_default_prefix(cls):
return 'form'
get_default_prefix = classmethod(get_default_prefix)
def non_form_errors(self): def non_form_errors(self):
""" """
Returns an ErrorList of errors that aren't associated with a particular Returns an ErrorList of errors that aren't associated with a particular

View File

@ -472,7 +472,7 @@ class BaseInlineFormSet(BaseModelFormSet):
# is there a better way to get the object descriptor? # is there a better way to get the object descriptor?
self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name() self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
qs = self.model._default_manager.filter(**{self.fk.name: self.instance}) qs = self.model._default_manager.filter(**{self.fk.name: self.instance})
super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix or self.rel_name, super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix,
queryset=qs) queryset=qs)
def _construct_forms(self): def _construct_forms(self):
@ -489,6 +489,12 @@ class BaseInlineFormSet(BaseModelFormSet):
form.data[form.add_prefix(self._pk_field.name)] = None form.data[form.add_prefix(self._pk_field.name)] = None
return form return form
#@classmethod
def get_default_prefix(cls):
from django.db.models.fields.related import RelatedObject
return RelatedObject(cls.fk.rel.to, cls.model, cls.fk).get_accessor_name()
get_default_prefix = classmethod(get_default_prefix)
def save_new(self, form, commit=True): def save_new(self, form, commit=True):
fk_attname = self.fk.get_attname() fk_attname = self.fk.get_attname()
kwargs = {fk_attname: self.instance.pk} kwargs = {fk_attname: self.instance.pk}

View File

@ -238,4 +238,9 @@ __test__ = {'API_TESTS':"""
<p><label for="id_generic_relations-taggeditem-content_type-object_id-1-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-1-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-1-tag" maxlength="50" /></p> <p><label for="id_generic_relations-taggeditem-content_type-object_id-1-tag">Tag:</label> <input id="id_generic_relations-taggeditem-content_type-object_id-1-tag" type="text" name="generic_relations-taggeditem-content_type-object_id-1-tag" maxlength="50" /></p>
<p><label for="id_generic_relations-taggeditem-content_type-object_id-1-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-1-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-1-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-1-id" id="id_generic_relations-taggeditem-content_type-object_id-1-id" /></p> <p><label for="id_generic_relations-taggeditem-content_type-object_id-1-DELETE">Delete:</label> <input type="checkbox" name="generic_relations-taggeditem-content_type-object_id-1-DELETE" id="id_generic_relations-taggeditem-content_type-object_id-1-DELETE" /><input type="hidden" name="generic_relations-taggeditem-content_type-object_id-1-id" id="id_generic_relations-taggeditem-content_type-object_id-1-id" /></p>
>>> formset = GenericFormSet(instance=lion, prefix='x')
>>> for form in formset.forms:
... print form.as_p()
<p><label for="id_x-0-tag">Tag:</label> <input id="id_x-0-tag" type="text" name="x-0-tag" maxlength="50" /></p>
<p><label for="id_x-0-DELETE">Delete:</label> <input type="checkbox" name="x-0-DELETE" id="id_x-0-DELETE" /><input type="hidden" name="x-0-id" id="id_x-0-id" /></p>
"""} """}

View File

@ -134,12 +134,56 @@ class Thing(models.Model):
class ThingAdmin(admin.ModelAdmin): class ThingAdmin(admin.ModelAdmin):
list_filter = ('color',) list_filter = ('color',)
class Persona(models.Model):
"""
A simple persona associated with accounts, to test inlining of related
accounts which inherit from a common accounts class.
"""
name = models.CharField(blank=False, max_length=80)
def __unicode__(self):
return self.name
class Account(models.Model):
"""
A simple, generic account encapsulating the information shared by all
types of accounts.
"""
username = models.CharField(blank=False, max_length=80)
persona = models.ForeignKey(Persona, related_name="accounts")
servicename = u'generic service'
def __unicode__(self):
return "%s: %s" % (self.servicename, self.username)
class FooAccount(Account):
"""A service-specific account of type Foo."""
servicename = u'foo'
class BarAccount(Account):
"""A service-specific account of type Bar."""
servicename = u'bar'
class FooAccountAdmin(admin.StackedInline):
model = FooAccount
extra = 1
class BarAccountAdmin(admin.StackedInline):
model = BarAccount
extra = 1
class PersonaAdmin(admin.ModelAdmin):
inlines = (
FooAccountAdmin,
BarAccountAdmin
)
admin.site.register(Article, ArticleAdmin) admin.site.register(Article, ArticleAdmin)
admin.site.register(CustomArticle, CustomArticleAdmin) admin.site.register(CustomArticle, CustomArticleAdmin)
admin.site.register(Section, inlines=[ArticleInline]) admin.site.register(Section, inlines=[ArticleInline])
admin.site.register(ModelWithStringPrimaryKey) admin.site.register(ModelWithStringPrimaryKey)
admin.site.register(Color) admin.site.register(Color)
admin.site.register(Thing, ThingAdmin) admin.site.register(Thing, ThingAdmin)
admin.site.register(Persona, PersonaAdmin)
# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
# That way we cover all four cases: # That way we cover all four cases:
@ -153,3 +197,5 @@ admin.site.register(Thing, ThingAdmin)
admin.site.register(Book, inlines=[ChapterInline]) admin.site.register(Book, inlines=[ChapterInline])
admin.site.register(Promo) admin.site.register(Promo)
admin.site.register(ChapterXtra1) admin.site.register(ChapterXtra1)

View File

@ -1,5 +1,7 @@
# coding: utf-8 # coding: utf-8
import re
from django.test import TestCase from django.test import TestCase
from django.contrib.auth.models import User, Permission from django.contrib.auth.models import User, Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -9,7 +11,12 @@ from django.contrib.admin.util import quote
from django.utils.html import escape from django.utils.html import escape
# local test models # local test models
from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey, Persona, FooAccount, BarAccount
try:
set
except NameError:
from sets import Set as set
class AdminViewBasicTest(TestCase): class AdminViewBasicTest(TestCase):
fixtures = ['admin-views-users.xml', 'admin-views-colors.xml'] fixtures = ['admin-views-users.xml', 'admin-views-colors.xml']
@ -721,3 +728,80 @@ class AdminViewUnicodeTest(TestCase):
self.failUnlessEqual(response.status_code, 200) self.failUnlessEqual(response.status_code, 200)
response = self.client.post('/test_admin/admin/admin_views/book/1/delete/', delete_dict) response = self.client.post('/test_admin/admin/admin_views/book/1/delete/', delete_dict)
self.assertRedirects(response, '/test_admin/admin/admin_views/book/') self.assertRedirects(response, '/test_admin/admin/admin_views/book/')
class AdminInheritedInlinesTest(TestCase):
fixtures = ['admin-views-users.xml',]
def setUp(self):
self.client.login(username='super', password='secret')
def tearDown(self):
self.client.logout()
def testInline(self):
"Ensure that inline models which inherit from a common parent are correctly handled by admin."
foo_user = u"foo username"
bar_user = u"bar username"
name_re = re.compile('name="(.*?)"')
# test the add case
response = self.client.get('/test_admin/admin/admin_views/persona/add/')
names = name_re.findall(response.content)
# make sure we have no duplicate HTML names
self.failUnlessEqual(len(names), len(set(names)))
# test the add case
post_data = {
"name": u"Test Name",
# inline data
"accounts-TOTAL_FORMS": u"1",
"accounts-INITIAL_FORMS": u"0",
"accounts-0-username": foo_user,
"accounts-2-TOTAL_FORMS": u"1",
"accounts-2-INITIAL_FORMS": u"0",
"accounts-2-0-username": bar_user,
}
response = self.client.post('/test_admin/admin/admin_views/persona/add/', post_data)
self.failUnlessEqual(response.status_code, 302) # redirect somewhere
self.failUnlessEqual(Persona.objects.count(), 1)
self.failUnlessEqual(FooAccount.objects.count(), 1)
self.failUnlessEqual(BarAccount.objects.count(), 1)
self.failUnlessEqual(FooAccount.objects.all()[0].username, foo_user)
self.failUnlessEqual(BarAccount.objects.all()[0].username, bar_user)
self.failUnlessEqual(Persona.objects.all()[0].accounts.count(), 2)
# test the edit case
response = self.client.get('/test_admin/admin/admin_views/persona/1/')
names = name_re.findall(response.content)
# make sure we have no duplicate HTML names
self.failUnlessEqual(len(names), len(set(names)))
post_data = {
"name": u"Test Name",
"accounts-TOTAL_FORMS": "2",
"accounts-INITIAL_FORMS": u"1",
"accounts-0-username": "%s-1" % foo_user,
"accounts-0-account_ptr": "1",
"accounts-0-persona": "1",
"accounts-2-TOTAL_FORMS": u"2",
"accounts-2-INITIAL_FORMS": u"1",
"accounts-2-0-username": "%s-1" % bar_user,
"accounts-2-0-account_ptr": "2",
"accounts-2-0-persona": "1",
}
response = self.client.post('/test_admin/admin/admin_views/persona/1/', post_data)
self.failUnlessEqual(response.status_code, 302)
self.failUnlessEqual(Persona.objects.count(), 1)
self.failUnlessEqual(FooAccount.objects.count(), 1)
self.failUnlessEqual(BarAccount.objects.count(), 1)
self.failUnlessEqual(FooAccount.objects.all()[0].username, "%s-1" % foo_user)
self.failUnlessEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user)
self.failUnlessEqual(Persona.objects.all()[0].accounts.count(), 2)