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
|
||||||
|
@ -62,7 +62,7 @@ class BaseFormSet(StrAndUnicode):
|
||||||
initial = {TOTAL_FORM_COUNT: self._total_form_count,
|
initial = {TOTAL_FORM_COUNT: self._total_form_count,
|
||||||
INITIAL_FORM_COUNT: self._initial_form_count}
|
INITIAL_FORM_COUNT: self._initial_form_count}
|
||||||
self.management_form = ManagementForm(initial=initial, auto_id=self.auto_id, prefix=self.prefix)
|
self.management_form = ManagementForm(initial=initial, auto_id=self.auto_id, prefix=self.prefix)
|
||||||
|
|
||||||
# construct the forms in the formset
|
# construct the forms in the formset
|
||||||
self._construct_forms()
|
self._construct_forms()
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ class BaseFormSet(StrAndUnicode):
|
||||||
self.forms = []
|
self.forms = []
|
||||||
for i in xrange(self._total_form_count):
|
for i in xrange(self._total_form_count):
|
||||||
self.forms.append(self._construct_form(i))
|
self.forms.append(self._construct_form(i))
|
||||||
|
|
||||||
def _construct_form(self, i, **kwargs):
|
def _construct_form(self, i, **kwargs):
|
||||||
"""
|
"""
|
||||||
Instantiates and returns the i-th form instance in a formset.
|
Instantiates and returns the i-th form instance in a formset.
|
||||||
|
@ -118,7 +118,7 @@ class BaseFormSet(StrAndUnicode):
|
||||||
|
|
||||||
def _get_deleted_forms(self):
|
def _get_deleted_forms(self):
|
||||||
"""
|
"""
|
||||||
Returns a list of forms that have been marked for deletion. Raises an
|
Returns a list of forms that have been marked for deletion. Raises an
|
||||||
AttributeError if deletion is not allowed.
|
AttributeError if deletion is not allowed.
|
||||||
"""
|
"""
|
||||||
if not self.is_valid() or not self.can_delete:
|
if not self.is_valid() or not self.can_delete:
|
||||||
|
@ -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
|
||||||
|
|
|
@ -249,8 +249,8 @@ class BaseModelForm(BaseForm):
|
||||||
# This is an extra field that's not on the ModelForm, ignore it
|
# This is an extra field that's not on the ModelForm, ignore it
|
||||||
continue
|
continue
|
||||||
if not isinstance(f, ModelField):
|
if not isinstance(f, ModelField):
|
||||||
# This is an extra field that happens to have a name that matches,
|
# This is an extra field that happens to have a name that matches,
|
||||||
# for example, a related object accessor for this model. So
|
# for example, a related object accessor for this model. So
|
||||||
# get_field_by_name found it, but it is not a Field so do not proceed
|
# get_field_by_name found it, but it is not a Field so do not proceed
|
||||||
# to use it as if it were.
|
# to use it as if it were.
|
||||||
continue
|
continue
|
||||||
|
@ -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>
|
||||||
"""}
|
"""}
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Article(models.Model):
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
def model_year(self):
|
def model_year(self):
|
||||||
return self.date.year
|
return self.date.year
|
||||||
model_year.admin_order_field = 'date'
|
model_year.admin_order_field = 'date'
|
||||||
|
@ -54,14 +54,14 @@ class Chapter(models.Model):
|
||||||
|
|
||||||
class ChapterXtra1(models.Model):
|
class ChapterXtra1(models.Model):
|
||||||
chap = models.OneToOneField(Chapter, verbose_name=u'¿Chap?')
|
chap = models.OneToOneField(Chapter, verbose_name=u'¿Chap?')
|
||||||
xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?')
|
xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?')
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return u'¿Xtra1: %s' % self.xtra
|
return u'¿Xtra1: %s' % self.xtra
|
||||||
|
|
||||||
class ChapterXtra2(models.Model):
|
class ChapterXtra2(models.Model):
|
||||||
chap = models.OneToOneField(Chapter, verbose_name=u'¿Chap?')
|
chap = models.OneToOneField(Chapter, verbose_name=u'¿Chap?')
|
||||||
xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?')
|
xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?')
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return u'¿Xtra2: %s' % self.xtra
|
return u'¿Xtra2: %s' % self.xtra
|
||||||
|
@ -87,7 +87,7 @@ class ArticleAdmin(admin.ModelAdmin):
|
||||||
'extra_var': 'Hello!'
|
'extra_var': 'Hello!'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def modeladmin_year(self, obj):
|
def modeladmin_year(self, obj):
|
||||||
return obj.date.year
|
return obj.date.year
|
||||||
modeladmin_year.admin_order_field = 'date'
|
modeladmin_year.admin_order_field = 'date'
|
||||||
|
@ -121,7 +121,7 @@ class ModelWithStringPrimaryKey(models.Model):
|
||||||
|
|
||||||
class Color(models.Model):
|
class Color(models.Model):
|
||||||
value = models.CharField(max_length=10)
|
value = models.CharField(max_length=10)
|
||||||
warm = models.BooleanField()
|
warm = models.BooleanField()
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
@ -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,22 +11,27 @@ 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']
|
||||||
|
|
||||||
# Store the bit of the URL where the admin is registered as a class
|
# Store the bit of the URL where the admin is registered as a class
|
||||||
# variable. That way we can test a second AdminSite just by subclassing
|
# variable. That way we can test a second AdminSite just by subclassing
|
||||||
# this test case and changing urlbit.
|
# this test case and changing urlbit.
|
||||||
urlbit = 'admin'
|
urlbit = 'admin'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client.login(username='super', password='secret')
|
self.client.login(username='super', password='secret')
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.client.logout()
|
self.client.logout()
|
||||||
|
|
||||||
def testTrailingSlashRequired(self):
|
def testTrailingSlashRequired(self):
|
||||||
"""
|
"""
|
||||||
If you leave off the trailing slash, app should redirect and add it.
|
If you leave off the trailing slash, app should redirect and add it.
|
||||||
|
@ -33,29 +40,29 @@ class AdminViewBasicTest(TestCase):
|
||||||
self.assertRedirects(request,
|
self.assertRedirects(request,
|
||||||
'/test_admin/%s/admin_views/article/add/' % self.urlbit, status_code=301
|
'/test_admin/%s/admin_views/article/add/' % self.urlbit, status_code=301
|
||||||
)
|
)
|
||||||
|
|
||||||
def testBasicAddGet(self):
|
def testBasicAddGet(self):
|
||||||
"""
|
"""
|
||||||
A smoke test to ensure GET on the add_view works.
|
A smoke test to ensure GET on the add_view works.
|
||||||
"""
|
"""
|
||||||
response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit)
|
response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit)
|
||||||
self.failUnlessEqual(response.status_code, 200)
|
self.failUnlessEqual(response.status_code, 200)
|
||||||
|
|
||||||
def testAddWithGETArgs(self):
|
def testAddWithGETArgs(self):
|
||||||
response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit, {'name': 'My Section'})
|
response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit, {'name': 'My Section'})
|
||||||
self.failUnlessEqual(response.status_code, 200)
|
self.failUnlessEqual(response.status_code, 200)
|
||||||
self.failUnless(
|
self.failUnless(
|
||||||
'value="My Section"' in response.content,
|
'value="My Section"' in response.content,
|
||||||
"Couldn't find an input with the right value in the response."
|
"Couldn't find an input with the right value in the response."
|
||||||
)
|
)
|
||||||
|
|
||||||
def testBasicEditGet(self):
|
def testBasicEditGet(self):
|
||||||
"""
|
"""
|
||||||
A smoke test to ensureGET on the change_view works.
|
A smoke test to ensureGET on the change_view works.
|
||||||
"""
|
"""
|
||||||
response = self.client.get('/test_admin/%s/admin_views/section/1/' % self.urlbit)
|
response = self.client.get('/test_admin/%s/admin_views/section/1/' % self.urlbit)
|
||||||
self.failUnlessEqual(response.status_code, 200)
|
self.failUnlessEqual(response.status_code, 200)
|
||||||
|
|
||||||
def testBasicAddPost(self):
|
def testBasicAddPost(self):
|
||||||
"""
|
"""
|
||||||
A smoke test to ensure POST on add_view works.
|
A smoke test to ensure POST on add_view works.
|
||||||
|
@ -68,7 +75,7 @@ class AdminViewBasicTest(TestCase):
|
||||||
}
|
}
|
||||||
response = self.client.post('/test_admin/%s/admin_views/section/add/' % self.urlbit, post_data)
|
response = self.client.post('/test_admin/%s/admin_views/section/add/' % self.urlbit, post_data)
|
||||||
self.failUnlessEqual(response.status_code, 302) # redirect somewhere
|
self.failUnlessEqual(response.status_code, 302) # redirect somewhere
|
||||||
|
|
||||||
def testBasicEditPost(self):
|
def testBasicEditPost(self):
|
||||||
"""
|
"""
|
||||||
A smoke test to ensure POST on edit_view works.
|
A smoke test to ensure POST on edit_view works.
|
||||||
|
@ -116,7 +123,7 @@ class AdminViewBasicTest(TestCase):
|
||||||
|
|
||||||
def testChangeListSortingCallable(self):
|
def testChangeListSortingCallable(self):
|
||||||
"""
|
"""
|
||||||
Ensure we can sort on a list_display field that is a callable
|
Ensure we can sort on a list_display field that is a callable
|
||||||
(column 2 is callable_year in ArticleAdmin)
|
(column 2 is callable_year in ArticleAdmin)
|
||||||
"""
|
"""
|
||||||
response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 2})
|
response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 2})
|
||||||
|
@ -126,10 +133,10 @@ class AdminViewBasicTest(TestCase):
|
||||||
response.content.index('Middle content') < response.content.index('Newest content'),
|
response.content.index('Middle content') < response.content.index('Newest content'),
|
||||||
"Results of sorting on callable are out of order."
|
"Results of sorting on callable are out of order."
|
||||||
)
|
)
|
||||||
|
|
||||||
def testChangeListSortingModel(self):
|
def testChangeListSortingModel(self):
|
||||||
"""
|
"""
|
||||||
Ensure we can sort on a list_display field that is a Model method
|
Ensure we can sort on a list_display field that is a Model method
|
||||||
(colunn 3 is 'model_year' in ArticleAdmin)
|
(colunn 3 is 'model_year' in ArticleAdmin)
|
||||||
"""
|
"""
|
||||||
response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'dsc', 'o': 3})
|
response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'dsc', 'o': 3})
|
||||||
|
@ -139,37 +146,37 @@ class AdminViewBasicTest(TestCase):
|
||||||
response.content.index('Middle content') < response.content.index('Oldest content'),
|
response.content.index('Middle content') < response.content.index('Oldest content'),
|
||||||
"Results of sorting on Model method are out of order."
|
"Results of sorting on Model method are out of order."
|
||||||
)
|
)
|
||||||
|
|
||||||
def testChangeListSortingModelAdmin(self):
|
def testChangeListSortingModelAdmin(self):
|
||||||
"""
|
"""
|
||||||
Ensure we can sort on a list_display field that is a ModelAdmin method
|
Ensure we can sort on a list_display field that is a ModelAdmin method
|
||||||
(colunn 4 is 'modeladmin_year' in ArticleAdmin)
|
(colunn 4 is 'modeladmin_year' in ArticleAdmin)
|
||||||
"""
|
"""
|
||||||
response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 4})
|
response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 4})
|
||||||
self.failUnlessEqual(response.status_code, 200)
|
self.failUnlessEqual(response.status_code, 200)
|
||||||
self.failUnless(
|
self.failUnless(
|
||||||
response.content.index('Oldest content') < response.content.index('Middle content') and
|
response.content.index('Oldest content') < response.content.index('Middle content') and
|
||||||
response.content.index('Middle content') < response.content.index('Newest content'),
|
response.content.index('Middle content') < response.content.index('Newest content'),
|
||||||
"Results of sorting on ModelAdmin method are out of order."
|
"Results of sorting on ModelAdmin method are out of order."
|
||||||
)
|
)
|
||||||
|
|
||||||
def testLimitedFilter(self):
|
def testLimitedFilter(self):
|
||||||
"""Ensure admin changelist filters do not contain objects excluded via limit_choices_to."""
|
"""Ensure admin changelist filters do not contain objects excluded via limit_choices_to."""
|
||||||
response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit)
|
response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit)
|
||||||
self.failUnlessEqual(response.status_code, 200)
|
self.failUnlessEqual(response.status_code, 200)
|
||||||
self.failUnless(
|
self.failUnless(
|
||||||
'<div id="changelist-filter">' in response.content,
|
'<div id="changelist-filter">' in response.content,
|
||||||
"Expected filter not found in changelist view."
|
"Expected filter not found in changelist view."
|
||||||
)
|
)
|
||||||
self.failIf(
|
self.failIf(
|
||||||
'<a href="?color__id__exact=3">Blue</a>' in response.content,
|
'<a href="?color__id__exact=3">Blue</a>' in response.content,
|
||||||
"Changelist filter not correctly limited by limit_choices_to."
|
"Changelist filter not correctly limited by limit_choices_to."
|
||||||
)
|
)
|
||||||
|
|
||||||
def testIncorrectLookupParameters(self):
|
def testIncorrectLookupParameters(self):
|
||||||
"""Ensure incorrect lookup parameters are handled gracefully."""
|
"""Ensure incorrect lookup parameters are handled gracefully."""
|
||||||
response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'notarealfield': '5'})
|
response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'notarealfield': '5'})
|
||||||
self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit)
|
self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit)
|
||||||
response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'color__id__exact': 'StringNotInteger!'})
|
response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'color__id__exact': 'StringNotInteger!'})
|
||||||
self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit)
|
self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit)
|
||||||
|
|
||||||
|
@ -186,7 +193,7 @@ class CustomModelAdminTest(AdminViewBasicTest):
|
||||||
request = self.client.get('/test_admin/admin2/')
|
request = self.client.get('/test_admin/admin2/')
|
||||||
self.assertTemplateUsed(request, 'custom_admin/index.html')
|
self.assertTemplateUsed(request, 'custom_admin/index.html')
|
||||||
self.assert_('Hello from a custom index template *bar*' in request.content)
|
self.assert_('Hello from a custom index template *bar*' in request.content)
|
||||||
|
|
||||||
def testCustomAdminSiteView(self):
|
def testCustomAdminSiteView(self):
|
||||||
self.client.login(username='super', password='secret')
|
self.client.login(username='super', password='secret')
|
||||||
response = self.client.get('/test_admin/%s/my_view/' % self.urlbit)
|
response = self.client.get('/test_admin/%s/my_view/' % self.urlbit)
|
||||||
|
@ -411,7 +418,7 @@ class AdminViewPermissionsTest(TestCase):
|
||||||
post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
|
post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
|
||||||
self.assertRedirects(post, '/test_admin/admin/admin_views/article/')
|
self.assertRedirects(post, '/test_admin/admin/admin_views/article/')
|
||||||
self.failUnlessEqual(Article.objects.get(pk=1).content, '<p>edited article</p>')
|
self.failUnlessEqual(Article.objects.get(pk=1).content, '<p>edited article</p>')
|
||||||
|
|
||||||
# one error in form should produce singular error message, multiple errors plural
|
# one error in form should produce singular error message, multiple errors plural
|
||||||
change_dict['title'] = ''
|
change_dict['title'] = ''
|
||||||
post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
|
post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
|
||||||
|
@ -422,7 +429,7 @@ class AdminViewPermissionsTest(TestCase):
|
||||||
post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
|
post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
|
||||||
self.failUnlessEqual(request.status_code, 200)
|
self.failUnlessEqual(request.status_code, 200)
|
||||||
self.failUnless('Please correct the errors below.' in post.content,
|
self.failUnless('Please correct the errors below.' in post.content,
|
||||||
'Plural error message not found in response to post with multiple errors.')
|
'Plural error message not found in response to post with multiple errors.')
|
||||||
self.client.get('/test_admin/admin/logout/')
|
self.client.get('/test_admin/admin/logout/')
|
||||||
|
|
||||||
def testCustomModelAdminTemplates(self):
|
def testCustomModelAdminTemplates(self):
|
||||||
|
@ -523,7 +530,7 @@ class AdminViewStringPrimaryKeyTest(TestCase):
|
||||||
response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/delete/' % quote(self.pk))
|
response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/delete/' % quote(self.pk))
|
||||||
should_contain = """<a href="../../%s/">%s</a>""" % (quote(self.pk), escape(self.pk))
|
should_contain = """<a href="../../%s/">%s</a>""" % (quote(self.pk), escape(self.pk))
|
||||||
self.assertContains(response, should_contain)
|
self.assertContains(response, should_contain)
|
||||||
|
|
||||||
def test_url_conflicts_with_add(self):
|
def test_url_conflicts_with_add(self):
|
||||||
"A model with a primary key that ends with add should be visible"
|
"A model with a primary key that ends with add should be visible"
|
||||||
add_model = ModelWithStringPrimaryKey(id="i have something to add")
|
add_model = ModelWithStringPrimaryKey(id="i have something to add")
|
||||||
|
@ -531,7 +538,7 @@ class AdminViewStringPrimaryKeyTest(TestCase):
|
||||||
response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(add_model.pk))
|
response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(add_model.pk))
|
||||||
should_contain = """<h1>Change model with string primary key</h1>"""
|
should_contain = """<h1>Change model with string primary key</h1>"""
|
||||||
self.assertContains(response, should_contain)
|
self.assertContains(response, should_contain)
|
||||||
|
|
||||||
def test_url_conflicts_with_delete(self):
|
def test_url_conflicts_with_delete(self):
|
||||||
"A model with a primary key that ends with delete should be visible"
|
"A model with a primary key that ends with delete should be visible"
|
||||||
delete_model = ModelWithStringPrimaryKey(id="delete")
|
delete_model = ModelWithStringPrimaryKey(id="delete")
|
||||||
|
@ -539,7 +546,7 @@ class AdminViewStringPrimaryKeyTest(TestCase):
|
||||||
response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(delete_model.pk))
|
response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(delete_model.pk))
|
||||||
should_contain = """<h1>Change model with string primary key</h1>"""
|
should_contain = """<h1>Change model with string primary key</h1>"""
|
||||||
self.assertContains(response, should_contain)
|
self.assertContains(response, should_contain)
|
||||||
|
|
||||||
def test_url_conflicts_with_history(self):
|
def test_url_conflicts_with_history(self):
|
||||||
"A model with a primary key that ends with history should be visible"
|
"A model with a primary key that ends with history should be visible"
|
||||||
history_model = ModelWithStringPrimaryKey(id="history")
|
history_model = ModelWithStringPrimaryKey(id="history")
|
||||||
|
@ -547,7 +554,7 @@ class AdminViewStringPrimaryKeyTest(TestCase):
|
||||||
response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(history_model.pk))
|
response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(history_model.pk))
|
||||||
should_contain = """<h1>Change model with string primary key</h1>"""
|
should_contain = """<h1>Change model with string primary key</h1>"""
|
||||||
self.assertContains(response, should_contain)
|
self.assertContains(response, should_contain)
|
||||||
|
|
||||||
|
|
||||||
class SecureViewTest(TestCase):
|
class SecureViewTest(TestCase):
|
||||||
fixtures = ['admin-views-users.xml']
|
fixtures = ['admin-views-users.xml']
|
||||||
|
@ -582,28 +589,28 @@ class SecureViewTest(TestCase):
|
||||||
LOGIN_FORM_KEY: 1,
|
LOGIN_FORM_KEY: 1,
|
||||||
'username': 'joepublic',
|
'username': 'joepublic',
|
||||||
'password': 'secret'}
|
'password': 'secret'}
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.client.logout()
|
self.client.logout()
|
||||||
|
|
||||||
def test_secure_view_shows_login_if_not_logged_in(self):
|
def test_secure_view_shows_login_if_not_logged_in(self):
|
||||||
"Ensure that we see the login form"
|
"Ensure that we see the login form"
|
||||||
response = self.client.get('/test_admin/admin/secure-view/' )
|
response = self.client.get('/test_admin/admin/secure-view/' )
|
||||||
self.assertTemplateUsed(response, 'admin/login.html')
|
self.assertTemplateUsed(response, 'admin/login.html')
|
||||||
|
|
||||||
def test_secure_view_login_successfully_redirects_to_original_url(self):
|
def test_secure_view_login_successfully_redirects_to_original_url(self):
|
||||||
request = self.client.get('/test_admin/admin/secure-view/')
|
request = self.client.get('/test_admin/admin/secure-view/')
|
||||||
self.failUnlessEqual(request.status_code, 200)
|
self.failUnlessEqual(request.status_code, 200)
|
||||||
query_string = "the-answer=42"
|
query_string = "the-answer=42"
|
||||||
login = self.client.post('/test_admin/admin/secure-view/', self.super_login, QUERY_STRING = query_string )
|
login = self.client.post('/test_admin/admin/secure-view/', self.super_login, QUERY_STRING = query_string )
|
||||||
self.assertRedirects(login, '/test_admin/admin/secure-view/?%s' % query_string)
|
self.assertRedirects(login, '/test_admin/admin/secure-view/?%s' % query_string)
|
||||||
|
|
||||||
def test_staff_member_required_decorator_works_as_per_admin_login(self):
|
def test_staff_member_required_decorator_works_as_per_admin_login(self):
|
||||||
"""
|
"""
|
||||||
Make sure only staff members can log in.
|
Make sure only staff members can log in.
|
||||||
|
|
||||||
Successful posts to the login page will redirect to the orignal url.
|
Successful posts to the login page will redirect to the orignal url.
|
||||||
Unsuccessfull attempts will continue to render the login page with
|
Unsuccessfull attempts will continue to render the login page with
|
||||||
a 200 status code.
|
a 200 status code.
|
||||||
"""
|
"""
|
||||||
# Super User
|
# Super User
|
||||||
|
@ -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