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:
parent
d0fff8ccd4
commit
3c8568a7dc
1
AUTHORS
1
AUTHORS
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
"""}
|
"""}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue