summaryrefslogtreecommitdiff
path: root/tests/admin_views
diff options
context:
space:
mode:
authorFlorian Apolloner <florian@apolloner.eu>2013-02-26 09:53:47 +0100
committerFlorian Apolloner <florian@apolloner.eu>2013-02-26 14:36:57 +0100
commit89f40e36246100df6a11316c31a76712ebc6c501 (patch)
tree6e65639683ddaf2027908d1ecb1739e0e2ff853b /tests/admin_views
parentb3d2ccb5bfbaf6e7fe1f98843baaa48c35a70950 (diff)
Merged regressiontests and modeltests into the test root.
Diffstat (limited to 'tests/admin_views')
-rw-r--r--tests/admin_views/__init__.py0
-rw-r--r--tests/admin_views/admin.py750
-rw-r--r--tests/admin_views/customadmin.py59
-rw-r--r--tests/admin_views/fixtures/admin-views-actions.xml15
-rw-r--r--tests/admin_views/fixtures/admin-views-books.xml45
-rw-r--r--tests/admin_views/fixtures/admin-views-colors.xml19
-rw-r--r--tests/admin_views/fixtures/admin-views-fabrics.xml12
-rw-r--r--tests/admin_views/fixtures/admin-views-person.xml18
-rw-r--r--tests/admin_views/fixtures/admin-views-unicode.xml63
-rw-r--r--tests/admin_views/fixtures/admin-views-users.xml96
-rw-r--r--tests/admin_views/fixtures/deleted-objects.xml53
-rw-r--r--tests/admin_views/fixtures/multiple-child-classes.json107
-rw-r--r--tests/admin_views/fixtures/string-primary-key.xml6
-rw-r--r--tests/admin_views/forms.py11
-rw-r--r--tests/admin_views/models.py680
-rw-r--r--tests/admin_views/templates/custom_filter_template.html7
-rw-r--r--tests/admin_views/tests.py4017
-rw-r--r--tests/admin_views/urls.py15
-rw-r--r--tests/admin_views/views.py6
19 files changed, 5979 insertions, 0 deletions
diff --git a/tests/admin_views/__init__.py b/tests/admin_views/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/admin_views/__init__.py
diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py
new file mode 100644
index 0000000000..d4348968e0
--- /dev/null
+++ b/tests/admin_views/admin.py
@@ -0,0 +1,750 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
+import tempfile
+import os
+
+from django import forms
+from django.contrib import admin
+from django.contrib.admin.views.main import ChangeList
+from django.core.files.storage import FileSystemStorage
+from django.core.mail import EmailMessage
+from django.conf.urls import patterns, url
+from django.db import models
+from django.forms.models import BaseModelFormSet
+from django.http import HttpResponse
+from django.contrib.admin import BooleanFieldListFilter
+
+from .models import (Article, Chapter, Account, Media, Child, Parent, Picture,
+ Widget, DooHickey, Grommet, Whatsit, FancyDoodad, Category, Link,
+ PrePopulatedPost, PrePopulatedSubPost, CustomArticle, Section,
+ ModelWithStringPrimaryKey, Color, Thing, Actor, Inquisition, Sketch, Person,
+ Persona, Subscriber, ExternalSubscriber, OldSubscriber, Vodcast, EmptyModel,
+ Fabric, Gallery, Language, Recommendation, Recommender, Collector, Post,
+ Gadget, Villain, SuperVillain, Plot, PlotDetails, CyclicOne, CyclicTwo,
+ WorkHour, Reservation, FoodDelivery, RowLevelChangePermissionModel, Paper,
+ CoverLetter, Story, OtherStory, Book, Promo, ChapterXtra1, Pizza, Topping,
+ Album, Question, Answer, ComplexSortedPerson, PrePopulatedPostLargeSlug,
+ AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod,
+ AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated,
+ RelatedPrepopulated, UndeletableObject, UserMessenger, Simple, Choice,
+ ShortMessage, Telegram)
+
+
+def callable_year(dt_value):
+ try:
+ return dt_value.year
+ except AttributeError:
+ return None
+callable_year.admin_order_field = 'date'
+
+
+class ArticleInline(admin.TabularInline):
+ model = Article
+ prepopulated_fields = {
+ 'title' : ('content',)
+ }
+ fieldsets=(
+ ('Some fields', {
+ 'classes': ('collapse',),
+ 'fields': ('title', 'content')
+ }),
+ ('Some other fields', {
+ 'classes': ('wide',),
+ 'fields': ('date', 'section')
+ })
+ )
+
+class ChapterInline(admin.TabularInline):
+ model = Chapter
+
+
+class ChapterXtra1Admin(admin.ModelAdmin):
+ list_filter = ('chap',
+ 'chap__title',
+ 'chap__book',
+ 'chap__book__name',
+ 'chap__book__promo',
+ 'chap__book__promo__name',)
+
+
+class ArticleAdmin(admin.ModelAdmin):
+ list_display = ('content', 'date', callable_year, 'model_year', 'modeladmin_year')
+ list_filter = ('date', 'section')
+ fieldsets=(
+ ('Some fields', {
+ 'classes': ('collapse',),
+ 'fields': ('title', 'content')
+ }),
+ ('Some other fields', {
+ 'classes': ('wide',),
+ 'fields': ('date', 'section')
+ })
+ )
+
+ def changelist_view(self, request):
+ "Test that extra_context works"
+ return super(ArticleAdmin, self).changelist_view(
+ request, extra_context={
+ 'extra_var': 'Hello!'
+ }
+ )
+
+ def modeladmin_year(self, obj):
+ return obj.date.year
+ modeladmin_year.admin_order_field = 'date'
+ modeladmin_year.short_description = None
+
+ def delete_model(self, request, obj):
+ EmailMessage(
+ 'Greetings from a deleted object',
+ 'I hereby inform you that some user deleted me',
+ 'from@example.com',
+ ['to@example.com']
+ ).send()
+ return super(ArticleAdmin, self).delete_model(request, obj)
+
+ def save_model(self, request, obj, form, change=True):
+ EmailMessage(
+ 'Greetings from a created object',
+ 'I hereby inform you that some user created me',
+ 'from@example.com',
+ ['to@example.com']
+ ).send()
+ return super(ArticleAdmin, self).save_model(request, obj, form, change)
+
+
+class RowLevelChangePermissionModelAdmin(admin.ModelAdmin):
+ def has_change_permission(self, request, obj=None):
+ """ Only allow changing objects with even id number """
+ return request.user.is_staff and (obj is not None) and (obj.id % 2 == 0)
+
+
+class CustomArticleAdmin(admin.ModelAdmin):
+ """
+ Tests various hooks for using custom templates and contexts.
+ """
+ change_list_template = 'custom_admin/change_list.html'
+ change_form_template = 'custom_admin/change_form.html'
+ add_form_template = 'custom_admin/add_form.html'
+ object_history_template = 'custom_admin/object_history.html'
+ delete_confirmation_template = 'custom_admin/delete_confirmation.html'
+ delete_selected_confirmation_template = 'custom_admin/delete_selected_confirmation.html'
+
+ def changelist_view(self, request):
+ "Test that extra_context works"
+ return super(CustomArticleAdmin, self).changelist_view(
+ request, extra_context={
+ 'extra_var': 'Hello!'
+ }
+ )
+
+
+class ThingAdmin(admin.ModelAdmin):
+ list_filter = ('color__warm', 'color__value', 'pub_date',)
+
+
+class InquisitionAdmin(admin.ModelAdmin):
+ list_display = ('leader', 'country', 'expected')
+
+
+class SketchAdmin(admin.ModelAdmin):
+ raw_id_fields = ('inquisition',)
+
+
+class FabricAdmin(admin.ModelAdmin):
+ list_display = ('surface',)
+ list_filter = ('surface',)
+
+
+class BasePersonModelFormSet(BaseModelFormSet):
+ def clean(self):
+ for person_dict in self.cleaned_data:
+ person = person_dict.get('id')
+ alive = person_dict.get('alive')
+ if person and alive and person.name == "Grace Hopper":
+ raise forms.ValidationError("Grace is not a Zombie")
+
+
+class PersonAdmin(admin.ModelAdmin):
+ list_display = ('name', 'gender', 'alive')
+ list_editable = ('gender', 'alive')
+ list_filter = ('gender',)
+ search_fields = ('^name',)
+ save_as = True
+
+ def get_changelist_formset(self, request, **kwargs):
+ return super(PersonAdmin, self).get_changelist_formset(request,
+ formset=BasePersonModelFormSet, **kwargs)
+
+ def queryset(self, request):
+ # Order by a field that isn't in list display, to be able to test
+ # whether ordering is preserved.
+ return super(PersonAdmin, self).queryset(request).order_by('age')
+
+
+class FooAccount(Account):
+ """A service-specific account of type Foo."""
+ servicename = 'foo'
+
+
+class BarAccount(Account):
+ """A service-specific account of type Bar."""
+ servicename = 'bar'
+
+
+class FooAccountAdmin(admin.StackedInline):
+ model = FooAccount
+ extra = 1
+
+
+class BarAccountAdmin(admin.StackedInline):
+ model = BarAccount
+ extra = 1
+
+
+class PersonaAdmin(admin.ModelAdmin):
+ inlines = (
+ FooAccountAdmin,
+ BarAccountAdmin
+ )
+
+
+class SubscriberAdmin(admin.ModelAdmin):
+ actions = ['mail_admin']
+
+ def mail_admin(self, request, selected):
+ EmailMessage(
+ 'Greetings from a ModelAdmin action',
+ 'This is the test email from a admin action',
+ 'from@example.com',
+ ['to@example.com']
+ ).send()
+
+
+def external_mail(modeladmin, request, selected):
+ EmailMessage(
+ 'Greetings from a function action',
+ 'This is the test email from a function action',
+ 'from@example.com',
+ ['to@example.com']
+ ).send()
+external_mail.short_description = 'External mail (Another awesome action)'
+
+
+def redirect_to(modeladmin, request, selected):
+ from django.http import HttpResponseRedirect
+ return HttpResponseRedirect('/some-where-else/')
+redirect_to.short_description = 'Redirect to (Awesome action)'
+
+
+class ExternalSubscriberAdmin(admin.ModelAdmin):
+ actions = [redirect_to, external_mail]
+
+
+class Podcast(Media):
+ release_date = models.DateField()
+
+ class Meta:
+ ordering = ('release_date',) # overridden in PodcastAdmin
+
+
+class PodcastAdmin(admin.ModelAdmin):
+ list_display = ('name', 'release_date')
+ list_editable = ('release_date',)
+ date_hierarchy = 'release_date'
+ ordering = ('name',)
+
+
+class VodcastAdmin(admin.ModelAdmin):
+ list_display = ('name', 'released')
+ list_editable = ('released',)
+
+ ordering = ('name',)
+
+
+class ChildInline(admin.StackedInline):
+ model = Child
+
+
+class ParentAdmin(admin.ModelAdmin):
+ model = Parent
+ inlines = [ChildInline]
+
+ list_editable = ('name',)
+
+ def save_related(self, request, form, formsets, change):
+ super(ParentAdmin, self).save_related(request, form, formsets, change)
+ first_name, last_name = form.instance.name.split()
+ for child in form.instance.child_set.all():
+ if len(child.name.split()) < 2:
+ child.name = child.name + ' ' + last_name
+ child.save()
+
+
+class EmptyModelAdmin(admin.ModelAdmin):
+ def queryset(self, request):
+ return super(EmptyModelAdmin, self).queryset(request).filter(pk__gt=1)
+
+
+class OldSubscriberAdmin(admin.ModelAdmin):
+ actions = None
+
+
+temp_storage = FileSystemStorage(tempfile.mkdtemp(dir=os.environ['DJANGO_TEST_TEMP_DIR']))
+UPLOAD_TO = os.path.join(temp_storage.location, 'test_upload')
+
+
+class PictureInline(admin.TabularInline):
+ model = Picture
+ extra = 1
+
+
+class GalleryAdmin(admin.ModelAdmin):
+ inlines = [PictureInline]
+
+
+class PictureAdmin(admin.ModelAdmin):
+ pass
+
+
+class LanguageAdmin(admin.ModelAdmin):
+ list_display = ['iso', 'shortlist', 'english_name', 'name']
+ list_editable = ['shortlist']
+
+
+class RecommendationAdmin(admin.ModelAdmin):
+ search_fields = ('=titletranslation__text', '=recommender__titletranslation__text',)
+
+
+class WidgetInline(admin.StackedInline):
+ model = Widget
+
+
+class DooHickeyInline(admin.StackedInline):
+ model = DooHickey
+
+
+class GrommetInline(admin.StackedInline):
+ model = Grommet
+
+
+class WhatsitInline(admin.StackedInline):
+ model = Whatsit
+
+
+class FancyDoodadInline(admin.StackedInline):
+ model = FancyDoodad
+
+
+class CategoryAdmin(admin.ModelAdmin):
+ list_display = ('id', 'collector', 'order')
+ list_editable = ('order',)
+
+
+class CategoryInline(admin.StackedInline):
+ model = Category
+
+
+class CollectorAdmin(admin.ModelAdmin):
+ inlines = [
+ WidgetInline, DooHickeyInline, GrommetInline, WhatsitInline,
+ FancyDoodadInline, CategoryInline
+ ]
+
+
+class LinkInline(admin.TabularInline):
+ model = Link
+ extra = 1
+
+ readonly_fields = ("posted", "multiline")
+
+ def multiline(self, instance):
+ return "InlineMultiline\ntest\nstring"
+
+
+class SubPostInline(admin.TabularInline):
+ model = PrePopulatedSubPost
+
+ prepopulated_fields = {
+ 'subslug' : ('subtitle',)
+ }
+
+ def get_readonly_fields(self, request, obj=None):
+ if obj and obj.published:
+ return ('subslug',)
+ return self.readonly_fields
+
+ def get_prepopulated_fields(self, request, obj=None):
+ if obj and obj.published:
+ return {}
+ return self.prepopulated_fields
+
+
+class PrePopulatedPostAdmin(admin.ModelAdmin):
+ list_display = ['title', 'slug']
+ prepopulated_fields = {
+ 'slug' : ('title',)
+ }
+
+ inlines = [SubPostInline]
+
+ def get_readonly_fields(self, request, obj=None):
+ if obj and obj.published:
+ return ('slug',)
+ return self.readonly_fields
+
+ def get_prepopulated_fields(self, request, obj=None):
+ if obj and obj.published:
+ return {}
+ return self.prepopulated_fields
+
+
+class PostAdmin(admin.ModelAdmin):
+ list_display = ['title', 'public']
+ readonly_fields = (
+ 'posted', 'awesomeness_level', 'coolness', 'value', 'multiline',
+ lambda obj: "foo"
+ )
+
+ inlines = [
+ LinkInline
+ ]
+
+ def coolness(self, instance):
+ if instance.pk:
+ return "%d amount of cool." % instance.pk
+ else:
+ return "Unkown coolness."
+
+ def value(self, instance):
+ return 1000
+
+ def multiline(self, instance):
+ return "Multiline\ntest\nstring"
+
+ value.short_description = 'Value in $US'
+
+
+class CustomChangeList(ChangeList):
+ def get_query_set(self, request):
+ return self.root_query_set.filter(pk=9999) # Does not exist
+
+
+class GadgetAdmin(admin.ModelAdmin):
+ def get_changelist(self, request, **kwargs):
+ return CustomChangeList
+
+
+class PizzaAdmin(admin.ModelAdmin):
+ readonly_fields = ('toppings',)
+
+
+class WorkHourAdmin(admin.ModelAdmin):
+ list_display = ('datum', 'employee')
+ list_filter = ('employee',)
+
+
+class FoodDeliveryAdmin(admin.ModelAdmin):
+ list_display=('reference', 'driver', 'restaurant')
+ list_editable = ('driver', 'restaurant')
+
+
+class CoverLetterAdmin(admin.ModelAdmin):
+ """
+ A ModelAdmin with a custom queryset() method that uses defer(), to test
+ verbose_name display in messages shown after adding/editing CoverLetter
+ instances.
+ Note that the CoverLetter model defines a __unicode__ method.
+ For testing fix for ticket #14529.
+ """
+
+ def queryset(self, request):
+ return super(CoverLetterAdmin, self).queryset(request).defer('date_written')
+
+
+class PaperAdmin(admin.ModelAdmin):
+ """
+ A ModelAdmin with a custom queryset() method that uses only(), to test
+ verbose_name display in messages shown after adding/editing Paper
+ instances.
+ For testing fix for ticket #14529.
+ """
+
+ def queryset(self, request):
+ return super(PaperAdmin, self).queryset(request).only('title')
+
+
+class ShortMessageAdmin(admin.ModelAdmin):
+ """
+ A ModelAdmin with a custom queryset() method that uses defer(), to test
+ verbose_name display in messages shown after adding/editing ShortMessage
+ instances.
+ For testing fix for ticket #14529.
+ """
+
+ def queryset(self, request):
+ return super(ShortMessageAdmin, self).queryset(request).defer('timestamp')
+
+
+class TelegramAdmin(admin.ModelAdmin):
+ """
+ A ModelAdmin with a custom queryset() method that uses only(), to test
+ verbose_name display in messages shown after adding/editing Telegram
+ instances.
+ Note that the Telegram model defines a __unicode__ method.
+ For testing fix for ticket #14529.
+ """
+
+ def queryset(self, request):
+ return super(TelegramAdmin, self).queryset(request).only('title')
+
+
+class StoryForm(forms.ModelForm):
+ class Meta:
+ widgets = {'title': forms.HiddenInput}
+
+
+class StoryAdmin(admin.ModelAdmin):
+ list_display = ('id', 'title', 'content')
+ list_display_links = ('title',) # 'id' not in list_display_links
+ list_editable = ('content', )
+ form = StoryForm
+ ordering = ["-pk"]
+
+
+class OtherStoryAdmin(admin.ModelAdmin):
+ list_display = ('id', 'title', 'content')
+ list_display_links = ('title', 'id') # 'id' in list_display_links
+ list_editable = ('content', )
+ ordering = ["-pk"]
+
+
+class ComplexSortedPersonAdmin(admin.ModelAdmin):
+ list_display = ('name', 'age', 'is_employee', 'colored_name')
+ ordering = ('name',)
+
+ def colored_name(self, obj):
+ return '<span style="color: #%s;">%s</span>' % ('ff00ff', obj.name)
+ colored_name.allow_tags = True
+ colored_name.admin_order_field = 'name'
+
+
+class AlbumAdmin(admin.ModelAdmin):
+ list_filter = ['title']
+
+
+class WorkHourAdmin(admin.ModelAdmin):
+ list_display = ('datum', 'employee')
+ list_filter = ('employee',)
+
+
+class PrePopulatedPostLargeSlugAdmin(admin.ModelAdmin):
+ prepopulated_fields = {
+ 'slug' : ('title',)
+ }
+
+
+class AdminOrderedFieldAdmin(admin.ModelAdmin):
+ ordering = ('order',)
+ list_display = ('stuff', 'order')
+
+class AdminOrderedModelMethodAdmin(admin.ModelAdmin):
+ ordering = ('order',)
+ list_display = ('stuff', 'some_order')
+
+class AdminOrderedAdminMethodAdmin(admin.ModelAdmin):
+ def some_admin_order(self, obj):
+ return obj.order
+ some_admin_order.admin_order_field = 'order'
+ ordering = ('order',)
+ list_display = ('stuff', 'some_admin_order')
+
+def admin_ordered_callable(obj):
+ return obj.order
+admin_ordered_callable.admin_order_field = 'order'
+class AdminOrderedCallableAdmin(admin.ModelAdmin):
+ ordering = ('order',)
+ list_display = ('stuff', admin_ordered_callable)
+
+class ReportAdmin(admin.ModelAdmin):
+ def extra(self, request):
+ return HttpResponse()
+
+ def get_urls(self):
+ # Corner case: Don't call parent implementation
+ return patterns('',
+ url(r'^extra/$',
+ self.extra,
+ name='cable_extra'),
+ )
+
+
+class CustomTemplateBooleanFieldListFilter(BooleanFieldListFilter):
+ template = 'custom_filter_template.html'
+
+class CustomTemplateFilterColorAdmin(admin.ModelAdmin):
+ list_filter = (('warm', CustomTemplateBooleanFieldListFilter),)
+
+
+# For Selenium Prepopulated tests -------------------------------------
+class RelatedPrepopulatedInline1(admin.StackedInline):
+ fieldsets = (
+ (None, {
+ 'fields': (('pubdate', 'status'), ('name', 'slug1', 'slug2',),)
+ }),
+ )
+ model = RelatedPrepopulated
+ extra = 1
+ prepopulated_fields = {'slug1': ['name', 'pubdate'],
+ 'slug2': ['status', 'name']}
+
+class RelatedPrepopulatedInline2(admin.TabularInline):
+ model = RelatedPrepopulated
+ extra = 1
+ prepopulated_fields = {'slug1': ['name', 'pubdate'],
+ 'slug2': ['status', 'name']}
+
+class MainPrepopulatedAdmin(admin.ModelAdmin):
+ inlines = [RelatedPrepopulatedInline1, RelatedPrepopulatedInline2]
+ fieldsets = (
+ (None, {
+ 'fields': (('pubdate', 'status'), ('name', 'slug1', 'slug2',),)
+ }),
+ )
+ prepopulated_fields = {'slug1': ['name', 'pubdate'],
+ 'slug2': ['status', 'name']}
+
+
+class UnorderedObjectAdmin(admin.ModelAdmin):
+ list_display = ['name']
+ list_editable = ['name']
+ list_per_page = 2
+
+
+class UndeletableObjectAdmin(admin.ModelAdmin):
+ def change_view(self, *args, **kwargs):
+ kwargs['extra_context'] = {'show_delete': False}
+ return super(UndeletableObjectAdmin, self).change_view(*args, **kwargs)
+
+
+def callable_on_unknown(obj):
+ return obj.unknown
+
+
+class AttributeErrorRaisingAdmin(admin.ModelAdmin):
+ list_display = [callable_on_unknown, ]
+
+class MessageTestingAdmin(admin.ModelAdmin):
+ actions = ["message_debug", "message_info", "message_success",
+ "message_warning", "message_error", "message_extra_tags"]
+
+ def message_debug(self, request, selected):
+ self.message_user(request, "Test debug", level="debug")
+
+ def message_info(self, request, selected):
+ self.message_user(request, "Test info", level="info")
+
+ def message_success(self, request, selected):
+ self.message_user(request, "Test success", level="success")
+
+ def message_warning(self, request, selected):
+ self.message_user(request, "Test warning", level="warning")
+
+ def message_error(self, request, selected):
+ self.message_user(request, "Test error", level="error")
+
+ def message_extra_tags(self, request, selected):
+ self.message_user(request, "Test tags", extra_tags="extra_tag")
+
+
+class ChoiceList(admin.ModelAdmin):
+ list_display = ['choice']
+ readonly_fields = ['choice']
+ fields = ['choice']
+
+
+site = admin.AdminSite(name="admin")
+site.register(Article, ArticleAdmin)
+site.register(CustomArticle, CustomArticleAdmin)
+site.register(Section, save_as=True, inlines=[ArticleInline])
+site.register(ModelWithStringPrimaryKey)
+site.register(Color)
+site.register(Thing, ThingAdmin)
+site.register(Actor)
+site.register(Inquisition, InquisitionAdmin)
+site.register(Sketch, SketchAdmin)
+site.register(Person, PersonAdmin)
+site.register(Persona, PersonaAdmin)
+site.register(Subscriber, SubscriberAdmin)
+site.register(ExternalSubscriber, ExternalSubscriberAdmin)
+site.register(OldSubscriber, OldSubscriberAdmin)
+site.register(Podcast, PodcastAdmin)
+site.register(Vodcast, VodcastAdmin)
+site.register(Parent, ParentAdmin)
+site.register(EmptyModel, EmptyModelAdmin)
+site.register(Fabric, FabricAdmin)
+site.register(Gallery, GalleryAdmin)
+site.register(Picture, PictureAdmin)
+site.register(Language, LanguageAdmin)
+site.register(Recommendation, RecommendationAdmin)
+site.register(Recommender)
+site.register(Collector, CollectorAdmin)
+site.register(Category, CategoryAdmin)
+site.register(Post, PostAdmin)
+site.register(Gadget, GadgetAdmin)
+site.register(Villain)
+site.register(SuperVillain)
+site.register(Plot)
+site.register(PlotDetails)
+site.register(CyclicOne)
+site.register(CyclicTwo)
+site.register(WorkHour, WorkHourAdmin)
+site.register(Reservation)
+site.register(FoodDelivery, FoodDeliveryAdmin)
+site.register(RowLevelChangePermissionModel, RowLevelChangePermissionModelAdmin)
+site.register(Paper, PaperAdmin)
+site.register(CoverLetter, CoverLetterAdmin)
+site.register(ShortMessage, ShortMessageAdmin)
+site.register(Telegram, TelegramAdmin)
+site.register(Story, StoryAdmin)
+site.register(OtherStory, OtherStoryAdmin)
+site.register(Report, ReportAdmin)
+site.register(MainPrepopulated, MainPrepopulatedAdmin)
+site.register(UnorderedObject, UnorderedObjectAdmin)
+site.register(UndeletableObject, UndeletableObjectAdmin)
+
+# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
+# That way we cover all four cases:
+# related ForeignKey object registered in admin
+# related ForeignKey object not registered in admin
+# related OneToOne object registered in admin
+# related OneToOne object not registered in admin
+# when deleting Book so as exercise all four troublesome (w.r.t escaping
+# and calling force_text to avoid problems on Python 2.3) paths through
+# contrib.admin.util's get_deleted_objects function.
+site.register(Book, inlines=[ChapterInline])
+site.register(Promo)
+site.register(ChapterXtra1, ChapterXtra1Admin)
+site.register(Pizza, PizzaAdmin)
+site.register(Topping)
+site.register(Album, AlbumAdmin)
+site.register(Question)
+site.register(Answer)
+site.register(PrePopulatedPost, PrePopulatedPostAdmin)
+site.register(ComplexSortedPerson, ComplexSortedPersonAdmin)
+site.register(PrePopulatedPostLargeSlug, PrePopulatedPostLargeSlugAdmin)
+site.register(AdminOrderedField, AdminOrderedFieldAdmin)
+site.register(AdminOrderedModelMethod, AdminOrderedModelMethodAdmin)
+site.register(AdminOrderedAdminMethod, AdminOrderedAdminMethodAdmin)
+site.register(AdminOrderedCallable, AdminOrderedCallableAdmin)
+site.register(Color2, CustomTemplateFilterColorAdmin)
+site.register(Simple, AttributeErrorRaisingAdmin)
+site.register(UserMessenger, MessageTestingAdmin)
+site.register(Choice, ChoiceList)
+
+# Register core models we need in our tests
+from django.contrib.auth.models import User, Group
+from django.contrib.auth.admin import UserAdmin, GroupAdmin
+site.register(User, UserAdmin)
+site.register(Group, GroupAdmin)
diff --git a/tests/admin_views/customadmin.py b/tests/admin_views/customadmin.py
new file mode 100644
index 0000000000..d69d690af0
--- /dev/null
+++ b/tests/admin_views/customadmin.py
@@ -0,0 +1,59 @@
+"""
+A second, custom AdminSite -- see tests.CustomAdminSiteTests.
+"""
+from __future__ import absolute_import
+
+from django.conf.urls import patterns
+from django.contrib import admin
+from django.http import HttpResponse
+from django.contrib.auth.models import User
+from django.contrib.auth.admin import UserAdmin
+
+from . import models, forms, admin as base_admin
+
+
+class Admin2(admin.AdminSite):
+ login_form = forms.CustomAdminAuthenticationForm
+ login_template = 'custom_admin/login.html'
+ logout_template = 'custom_admin/logout.html'
+ index_template = ['custom_admin/index.html'] # a list, to test fix for #18697
+ password_change_template = 'custom_admin/password_change_form.html'
+ password_change_done_template = 'custom_admin/password_change_done.html'
+
+ # A custom index view.
+ def index(self, request, extra_context=None):
+ return super(Admin2, self).index(request, {'foo': '*bar*'})
+
+ def get_urls(self):
+ return patterns('',
+ (r'^my_view/$', self.admin_view(self.my_view)),
+ ) + super(Admin2, self).get_urls()
+
+ def my_view(self, request):
+ return HttpResponse("Django is a magical pony!")
+
+
+class UserLimitedAdmin(UserAdmin):
+ # used for testing password change on a user not in queryset
+ def queryset(self, request):
+ qs = super(UserLimitedAdmin, self).queryset(request)
+ return qs.filter(is_superuser=False)
+
+
+class CustomPwdTemplateUserAdmin(UserAdmin):
+ change_user_password_template = ['admin/auth/user/change_password.html'] # a list, to test fix for #18697
+
+
+site = Admin2(name="admin2")
+
+site.register(models.Article, base_admin.ArticleAdmin)
+site.register(models.Section, inlines=[base_admin.ArticleInline])
+site.register(models.Thing, base_admin.ThingAdmin)
+site.register(models.Fabric, base_admin.FabricAdmin)
+site.register(models.ChapterXtra1, base_admin.ChapterXtra1Admin)
+site.register(User, UserLimitedAdmin)
+site.register(models.UndeletableObject, base_admin.UndeletableObjectAdmin)
+site.register(models.Simple, base_admin.AttributeErrorRaisingAdmin)
+
+simple_site = Admin2(name='admin4')
+simple_site.register(User, CustomPwdTemplateUserAdmin)
diff --git a/tests/admin_views/fixtures/admin-views-actions.xml b/tests/admin_views/fixtures/admin-views-actions.xml
new file mode 100644
index 0000000000..316e750577
--- /dev/null
+++ b/tests/admin_views/fixtures/admin-views-actions.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="1" model="admin_views.subscriber">
+ <field type="CharField" name="name">John Doe</field>
+ <field type="CharField" name="email">john@example.org</field>
+ </object>
+ <object pk="2" model="admin_views.subscriber">
+ <field type="CharField" name="name">Max Mustermann</field>
+ <field type="CharField" name="email">max@example.org</field>
+ </object>
+ <object pk="1" model="admin_views.externalsubscriber">
+ <field type="CharField" name="name">John Doe</field>
+ <field type="CharField" name="email">john@example.org</field>
+ </object>
+</django-objects>
diff --git a/tests/admin_views/fixtures/admin-views-books.xml b/tests/admin_views/fixtures/admin-views-books.xml
new file mode 100644
index 0000000000..0517ed8d8a
--- /dev/null
+++ b/tests/admin_views/fixtures/admin-views-books.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="1" model="admin_views.book">
+ <field type="CharField" name="name">Book 1</field>
+ </object>
+ <object pk="2" model="admin_views.book">
+ <field type="CharField" name="name">Book 2</field>
+ </object>
+ <object pk="1" model="admin_views.promo">
+ <field type="CharField" name="name">Promo 1</field>
+ <field type="ForeignKey" name="book">1</field>
+ </object>
+ <object pk="2" model="admin_views.promo">
+ <field type="CharField" name="name">Promo 2</field>
+ <field type="ForeignKey" name="book">2</field>
+ </object>
+ <object pk="1" model="admin_views.chapter">
+ <field type="CharField" name="title">Chapter 1</field>
+ <field type="TextField" name="content">[ insert contents here ]</field>
+ <field type="ForeignKey" name="book">1</field>
+ </object>
+ <object pk="2" model="admin_views.chapter">
+ <field type="CharField" name="title">Chapter 2</field>
+ <field type="TextField" name="content">[ insert contents here ]</field>
+ <field type="ForeignKey" name="book">1</field>
+ </object>
+ <object pk="3" model="admin_views.chapter">
+ <field type="CharField" name="title">Chapter 1</field>
+ <field type="TextField" name="content">[ insert contents here ]</field>
+ <field type="ForeignKey" name="book">2</field>
+ </object>
+ <object pk="4" model="admin_views.chapter">
+ <field type="CharField" name="title">Chapter 2</field>
+ <field type="TextField" name="content">[ insert contents here ]</field>
+ <field type="ForeignKey" name="book">2</field>
+ </object>
+ <object pk="1" model="admin_views.chapterxtra1">
+ <field type="CharField" name="xtra">ChapterXtra1 1</field>
+ <field type="ForeignKey" name="chap">1</field>
+ </object>
+ <object pk="2" model="admin_views.chapterxtra1">
+ <field type="CharField" name="xtra">ChapterXtra1 2</field>
+ <field type="ForeignKey" name="chap">3</field>
+ </object>
+</django-objects>
diff --git a/tests/admin_views/fixtures/admin-views-colors.xml b/tests/admin_views/fixtures/admin-views-colors.xml
new file mode 100644
index 0000000000..e1213561b9
--- /dev/null
+++ b/tests/admin_views/fixtures/admin-views-colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="1" model="admin_views.color">
+ <field type="CharField" name="value">Red</field>
+ <field type="BooleanField" name="warm">1</field>
+ </object>
+ <object pk="2" model="admin_views.color">
+ <field type="CharField" name="value">Orange</field>
+ <field type="BooleanField" name="warm">1</field>
+ </object>
+ <object pk="3" model="admin_views.color">
+ <field type="CharField" name="value">Blue</field>
+ <field type="BooleanField" name="warm">0</field>
+ </object>
+ <object pk="4" model="admin_views.color">
+ <field type="CharField" name="value">Green</field>
+ <field type="BooleanField" name="warm">0</field>
+ </object>
+</django-objects>
diff --git a/tests/admin_views/fixtures/admin-views-fabrics.xml b/tests/admin_views/fixtures/admin-views-fabrics.xml
new file mode 100644
index 0000000000..485bb27c2a
--- /dev/null
+++ b/tests/admin_views/fixtures/admin-views-fabrics.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="1" model="admin_views.fabric">
+ <field type="CharField" name="surface">x</field>
+ </object>
+ <object pk="2" model="admin_views.fabric">
+ <field type="CharField" name="surface">y</field>
+ </object>
+ <object pk="3" model="admin_views.fabric">
+ <field type="CharField" name="surface">plain</field>
+ </object>
+</django-objects>
diff --git a/tests/admin_views/fixtures/admin-views-person.xml b/tests/admin_views/fixtures/admin-views-person.xml
new file mode 100644
index 0000000000..ff00fd2169
--- /dev/null
+++ b/tests/admin_views/fixtures/admin-views-person.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="1" model="admin_views.person">
+ <field type="CharField" name="name">John Mauchly</field>
+ <field type="IntegerField" name="gender">1</field>
+ <field type="BooleanField" name="alive">True</field>
+ </object>
+ <object pk="2" model="admin_views.person">
+ <field type="CharField" name="name">Grace Hopper</field>
+ <field type="IntegerField" name="gender">1</field>
+ <field type="BooleanField" name="alive">False</field>
+ </object>
+ <object pk="3" model="admin_views.person">
+ <field type="CharField" name="name">Guido van Rossum</field>
+ <field type="IntegerField" name="gender">1</field>
+ <field type="BooleanField" name="alive">True</field>
+ </object>
+</django-objects>
diff --git a/tests/admin_views/fixtures/admin-views-unicode.xml b/tests/admin_views/fixtures/admin-views-unicode.xml
new file mode 100644
index 0000000000..5652aa1881
--- /dev/null
+++ b/tests/admin_views/fixtures/admin-views-unicode.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="100" model="auth.user">
+ <field type="CharField" name="username">super</field>
+ <field type="CharField" name="first_name">Super</field>
+ <field type="CharField" name="last_name">User</field>
+ <field type="CharField" name="email">super@example.com</field>
+ <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
+ <field type="BooleanField" name="is_staff">True</field>
+ <field type="BooleanField" name="is_active">True</field>
+ <field type="BooleanField" name="is_superuser">True</field>
+ <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
+ <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
+ <field to="auth.group" name="groups" rel="ManyToManyRel"></field>
+ <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
+ </object>
+ <object pk="1" model="admin_views.book">
+ <field type="CharField" name="name">Lærdommer</field>
+ </object>
+ <object pk="1" model="admin_views.promo">
+ <field type="CharField" name="name">&lt;Promo for Lærdommer&gt;</field>
+ <field to="admin_views.book" name="book" rel="ManyToOneRel">1</field>
+ </object>
+ <object pk="1" model="admin_views.chapter">
+ <field type="CharField" name="title">Norske bostaver æøå skaper problemer</field>
+ <field type="TextField" name="content">&lt;p&gt;Svært frustrerende med UnicodeDecodeErro&lt;/p&gt;</field>
+ <field to="admin_views.book" name="book" rel="ManyToOneRel">1</field>
+ </object>
+ <object pk="2" model="admin_views.chapter">
+ <field type="CharField" name="title">Kjærlighet</field>
+ <field type="TextField" name="content">&lt;p&gt;La kjærligheten til de lidende seire.&lt;/p&gt;</field>
+ <field to="admin_views.book" name="book" rel="ManyToOneRel">1</field>
+ </object>
+ <object pk="3" model="admin_views.chapter">
+ <field type="CharField" name="title">Kjærlighet</field>
+ <field type="TextField" name="content">&lt;p&gt;Noe innhold&lt;/p&gt;</field>
+ <field to="admin_views.book" name="book" rel="ManyToOneRel">1</field>
+ </object>
+ <object pk="1" model="admin_views.chapterxtra1">
+ <field type="CharField" name="xtra">&lt;Xtra(1) Norske bostaver æøå skaper problemer&gt;</field>
+ <field to="admin_views.chapter" name="chap" rel="OneToOneRel">1</field>
+ </object>
+ <object pk="2" model="admin_views.chapterxtra1">
+ <field type="CharField" name="xtra">&lt;Xtra(1) Kjærlighet&gt;</field>
+ <field to="admin_views.chapter" name="chap" rel="OneToOneRel">2</field>
+ </object>
+ <object pk="3" model="admin_views.chapterxtra1">
+ <field type="CharField" name="xtra">&lt;Xtra(1) Kjærlighet&gt;</field>
+ <field to="admin_views.chapter" name="chap" rel="OneToOneRel">3</field>
+ </object>
+ <object pk="1" model="admin_views.chapterxtra2">
+ <field type="CharField" name="xtra">&lt;Xtra(2) Norske bostaver æøå skaper problemer&gt;</field>
+ <field to="admin_views.chapter" name="chap" rel="OneToOneRel">1</field>
+ </object>
+ <object pk="2" model="admin_views.chapterxtra2">
+ <field type="CharField" name="xtra">&lt;Xtra(2) Kjærlighet&gt;</field>
+ <field to="admin_views.chapter" name="chap" rel="OneToOneRel">2</field>
+ </object>
+ <object pk="3" model="admin_views.chapterxtra2">
+ <field type="CharField" name="xtra">&lt;Xtra(2) Kjærlighet&gt;</field>
+ <field to="admin_views.chapter" name="chap" rel="OneToOneRel">3</field>
+ </object>
+</django-objects>
diff --git a/tests/admin_views/fixtures/admin-views-users.xml b/tests/admin_views/fixtures/admin-views-users.xml
new file mode 100644
index 0000000000..1c85e1c909
--- /dev/null
+++ b/tests/admin_views/fixtures/admin-views-users.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="100" model="auth.user">
+ <field type="CharField" name="username">super</field>
+ <field type="CharField" name="first_name">Super</field>
+ <field type="CharField" name="last_name">User</field>
+ <field type="CharField" name="email">super@example.com</field>
+ <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
+ <field type="BooleanField" name="is_staff">True</field>
+ <field type="BooleanField" name="is_active">True</field>
+ <field type="BooleanField" name="is_superuser">True</field>
+ <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
+ <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
+ <field to="auth.group" name="groups" rel="ManyToManyRel"></field>
+ <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
+ </object>
+ <object pk="101" model="auth.user">
+ <field type="CharField" name="username">adduser</field>
+ <field type="CharField" name="first_name">Add</field>
+ <field type="CharField" name="last_name">User</field>
+ <field type="CharField" name="email">auser@example.com</field>
+ <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
+ <field type="BooleanField" name="is_staff">True</field>
+ <field type="BooleanField" name="is_active">True</field>
+ <field type="BooleanField" name="is_superuser">False</field>
+ <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
+ <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
+ <field to="auth.group" name="groups" rel="ManyToManyRel"></field>
+ <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
+ </object>
+ <object pk="102" model="auth.user">
+ <field type="CharField" name="username">changeuser</field>
+ <field type="CharField" name="first_name">Change</field>
+ <field type="CharField" name="last_name">User</field>
+ <field type="CharField" name="email">cuser@example.com</field>
+ <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
+ <field type="BooleanField" name="is_staff">True</field>
+ <field type="BooleanField" name="is_active">True</field>
+ <field type="BooleanField" name="is_superuser">False</field>
+ <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
+ <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
+ <field to="auth.group" name="groups" rel="ManyToManyRel"></field>
+ <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
+ </object>
+ <object pk="103" model="auth.user">
+ <field type="CharField" name="username">deleteuser</field>
+ <field type="CharField" name="first_name">Delete</field>
+ <field type="CharField" name="last_name">User</field>
+ <field type="CharField" name="email">duser@example.com</field>
+ <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
+ <field type="BooleanField" name="is_staff">True</field>
+ <field type="BooleanField" name="is_active">True</field>
+ <field type="BooleanField" name="is_superuser">False</field>
+ <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
+ <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
+ <field to="auth.group" name="groups" rel="ManyToManyRel"></field>
+ <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
+ </object>
+ <object pk="104" model="auth.user">
+ <field type="CharField" name="username">joepublic</field>
+ <field type="CharField" name="first_name">Joe</field>
+ <field type="CharField" name="last_name">Public</field>
+ <field type="CharField" name="email">joepublic@example.com</field>
+ <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
+ <field type="BooleanField" name="is_staff">False</field>
+ <field type="BooleanField" name="is_active">True</field>
+ <field type="BooleanField" name="is_superuser">False</field>
+ <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
+ <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
+ <field to="auth.group" name="groups" rel="ManyToManyRel"></field>
+ <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
+ </object>
+ <object pk="1" model="admin_views.section">
+ <field type="CharField" name="name">Test section</field>
+ </object>
+ <object pk="1" model="admin_views.article">
+ <field type="TextField" name="content">&lt;p&gt;Middle content&lt;/p&gt;</field>
+ <field type="DateTimeField" name="date">2008-03-18 11:54:58</field>
+ <field to="admin_views.section" name="section" rel="ManyToOneRel">1</field>
+ </object>
+ <object pk="2" model="admin_views.article">
+ <field type="TextField" name="content">&lt;p&gt;Oldest content&lt;/p&gt;</field>
+ <field type="DateTimeField" name="date">2000-03-18 11:54:58</field>
+ <field to="admin_views.section" name="section" rel="ManyToOneRel">1</field>
+ </object>
+ <object pk="3" model="admin_views.article">
+ <field type="TextField" name="content">&lt;p&gt;Newest content&lt;/p&gt;</field>
+ <field type="DateTimeField" name="date">2009-03-18 11:54:58</field>
+ <field to="admin_views.section" name="section" rel="ManyToOneRel">1</field>
+ </object>
+ <object pk="1" model="admin_views.prepopulatedpost">
+ <field type="TextField" name="title">A Long Title</field>
+ <field type="BooleanField" name="published">True</field>
+ <field type="SlugField" name="slug">a-long-title</field>
+ </object>
+</django-objects>
diff --git a/tests/admin_views/fixtures/deleted-objects.xml b/tests/admin_views/fixtures/deleted-objects.xml
new file mode 100644
index 0000000000..92e43dba1c
--- /dev/null
+++ b/tests/admin_views/fixtures/deleted-objects.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="1" model="admin_views.villain">
+ <field type="CharField" name="name">Adam</field>
+ </object>
+ <object pk="2" model="admin_views.villain">
+ <field type="CharField" name="name">Sue</field>
+ </object>
+ <object pk="3" model="admin_views.villain">
+ <field type="CharField" name="name">Bob</field>
+ </object>
+ <object pk="3" model="admin_views.supervillain">
+ </object>
+ <object pk="1" model="admin_views.plot">
+ <field type="CharField" name="name">World Domination</field>
+ <field type="ForeignKey" name="team_leader">1</field>
+ <field type="ForeignKey" name="contact">2</field>
+ </object>
+ <object pk="2" model="admin_views.plot">
+ <field type="CharField" name="name">World Peace</field>
+ <field type="ForeignKey" name="team_leader">2</field>
+ <field type="ForeignKey" name="contact">2</field>
+ </object>
+ <object pk="1" model="admin_views.plotdetails">
+ <field type="CharField" name="details">almost finished</field>
+ <field type="ForeignKey" name="plot">1</field>
+ </object>
+ <object pk="1" model="admin_views.secrethideout">
+ <field type="CharField" name="location">underground bunker</field>
+ <field type="ForeignKey" name="villain">1</field>
+ </object>
+ <object pk="2" model="admin_views.secrethideout">
+ <field type="CharField" name="location">floating castle</field>
+ <field type="ForeignKey" name="villain">3</field>
+ </object>
+ <object pk="1" model="admin_views.supersecrethideout">
+ <field type="CharField" name="location">super floating castle!</field>
+ <field type="ForeignKey" name="supervillain">3</field>
+ </object>
+ <object pk="1" model="admin_views.cyclicone">
+ <field type="CharField" name="name">I am recursive</field>
+ <field type="ForeignKey" name="two">1</field>
+ </object>
+ <object pk="1" model="admin_views.cyclictwo">
+ <field type="CharField" name="name">I am recursive too</field>
+ <field type="ForeignKey" name="one">1</field>
+ </object>
+ <object pk="3" model="admin_views.plot">
+ <field type="CharField" name="name">Corn Conspiracy</field>
+ <field type="ForeignKey" name="team_leader">1</field>
+ <field type="ForeignKey" name="contact">1</field>
+ </object>
+</django-objects>
diff --git a/tests/admin_views/fixtures/multiple-child-classes.json b/tests/admin_views/fixtures/multiple-child-classes.json
new file mode 100644
index 0000000000..5cadf4c1c5
--- /dev/null
+++ b/tests/admin_views/fixtures/multiple-child-classes.json
@@ -0,0 +1,107 @@
+[
+ {
+ "pk": 1,
+ "model": "admin_views.title",
+ "fields":
+ {
+ }
+ },
+
+ {
+ "pk": 2,
+ "model": "admin_views.title",
+ "fields":
+ {
+ }
+ },
+
+ {
+ "pk": 3,
+ "model": "admin_views.title",
+ "fields":
+ {
+ }
+ },
+
+ {
+ "pk": 4,
+ "model": "admin_views.title",
+ "fields":
+ {
+ }
+ },
+
+ {
+ "pk": 1,
+ "model": "admin_views.titletranslation",
+ "fields":
+ {
+ "text": "Bar",
+ "title": 1
+ }
+ },
+
+ {
+ "pk": 2,
+ "model": "admin_views.titletranslation",
+ "fields":
+ {
+ "text": "Foo",
+ "title": 2
+ }
+ },
+
+ {
+ "pk": 3,
+ "model": "admin_views.titletranslation",
+ "fields":
+ {
+ "text": "Few",
+ "title": 3
+ }
+ },
+
+ {
+ "pk": 4,
+ "model": "admin_views.titletranslation",
+ "fields":
+ {
+ "text": "Bas",
+ "title": 4
+ }
+ },
+
+ {
+ "pk": 1,
+ "model": "admin_views.recommender",
+ "fields":
+ {
+ }
+ },
+
+ {
+ "pk": 4,
+ "model": "admin_views.recommender",
+ "fields":
+ {
+ }
+ },
+
+ {
+ "pk": 2,
+ "model": "admin_views.recommendation",
+ "fields":
+ {
+ "recommender": 1
+ }
+ },
+
+ {
+ "pk": 3,
+ "model": "admin_views.recommendation",
+ "fields":
+ {
+ "recommender": 4
+ }
+ }
+] \ No newline at end of file
diff --git a/tests/admin_views/fixtures/string-primary-key.xml b/tests/admin_views/fixtures/string-primary-key.xml
new file mode 100644
index 0000000000..1792cb2d7e
--- /dev/null
+++ b/tests/admin_views/fixtures/string-primary-key.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="1" model="admin_views.modelwithstringprimarykey">
+ <field type="CharField" name="string_pk"><![CDATA[abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 -_.!~*'() ;/?:@&=+$, <>#%" {}|\^[]`]]></field>
+ </object>
+</django-objects>
diff --git a/tests/admin_views/forms.py b/tests/admin_views/forms.py
new file mode 100644
index 0000000000..e8493df95b
--- /dev/null
+++ b/tests/admin_views/forms.py
@@ -0,0 +1,11 @@
+from django import forms
+from django.contrib.admin.forms import AdminAuthenticationForm
+
+
+class CustomAdminAuthenticationForm(AdminAuthenticationForm):
+
+ def clean_username(self):
+ username = self.cleaned_data.get('username')
+ if username == 'customform':
+ raise forms.ValidationError('custom form error')
+ return username
diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py
new file mode 100644
index 0000000000..5e25f0d542
--- /dev/null
+++ b/tests/admin_views/models.py
@@ -0,0 +1,680 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import datetime
+import tempfile
+import os
+
+from django.contrib.auth.models import User
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.core.files.storage import FileSystemStorage
+from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
+
+
+class Section(models.Model):
+ """
+ A simple section that links to articles, to test linking to related items
+ in admin views.
+ """
+ name = models.CharField(max_length=100)
+
+
+@python_2_unicode_compatible
+class Article(models.Model):
+ """
+ A simple article to test admin views. Test backwards compatibility.
+ """
+ title = models.CharField(max_length=100)
+ content = models.TextField()
+ date = models.DateTimeField()
+ section = models.ForeignKey(Section, null=True, blank=True)
+
+ def __str__(self):
+ return self.title
+
+ def model_year(self):
+ return self.date.year
+ model_year.admin_order_field = 'date'
+ model_year.short_description = ''
+
+
+@python_2_unicode_compatible
+class Book(models.Model):
+ """
+ A simple book that has chapters.
+ """
+ name = models.CharField(max_length=100, verbose_name='¿Name?')
+
+ def __str__(self):
+ return self.name
+
+
+@python_2_unicode_compatible
+class Promo(models.Model):
+ name = models.CharField(max_length=100, verbose_name='¿Name?')
+ book = models.ForeignKey(Book)
+
+ def __str__(self):
+ return self.name
+
+
+@python_2_unicode_compatible
+class Chapter(models.Model):
+ title = models.CharField(max_length=100, verbose_name='¿Title?')
+ content = models.TextField()
+ book = models.ForeignKey(Book)
+
+ def __str__(self):
+ return self.title
+
+ class Meta:
+ # Use a utf-8 bytestring to ensure it works (see #11710)
+ verbose_name = '¿Chapter?'
+
+
+@python_2_unicode_compatible
+class ChapterXtra1(models.Model):
+ chap = models.OneToOneField(Chapter, verbose_name='¿Chap?')
+ xtra = models.CharField(max_length=100, verbose_name='¿Xtra?')
+
+ def __str__(self):
+ return '¿Xtra1: %s' % self.xtra
+
+
+@python_2_unicode_compatible
+class ChapterXtra2(models.Model):
+ chap = models.OneToOneField(Chapter, verbose_name='¿Chap?')
+ xtra = models.CharField(max_length=100, verbose_name='¿Xtra?')
+
+ def __str__(self):
+ return '¿Xtra2: %s' % self.xtra
+
+
+class RowLevelChangePermissionModel(models.Model):
+ name = models.CharField(max_length=100, blank=True)
+
+
+class CustomArticle(models.Model):
+ content = models.TextField()
+ date = models.DateTimeField()
+
+
+@python_2_unicode_compatible
+class ModelWithStringPrimaryKey(models.Model):
+ string_pk = models.CharField(max_length=255, primary_key=True)
+
+ def __str__(self):
+ return self.string_pk
+
+ def get_absolute_url(self):
+ return '/dummy/%s/' % self.string_pk
+
+
+@python_2_unicode_compatible
+class Color(models.Model):
+ value = models.CharField(max_length=10)
+ warm = models.BooleanField()
+ def __str__(self):
+ return self.value
+
+# we replicate Color to register with another ModelAdmin
+class Color2(Color):
+ class Meta:
+ proxy = True
+
+@python_2_unicode_compatible
+class Thing(models.Model):
+ title = models.CharField(max_length=20)
+ color = models.ForeignKey(Color, limit_choices_to={'warm': True})
+ pub_date = models.DateField(blank=True, null=True)
+ def __str__(self):
+ return self.title
+
+
+@python_2_unicode_compatible
+class Actor(models.Model):
+ name = models.CharField(max_length=50)
+ age = models.IntegerField()
+ def __str__(self):
+ return self.name
+
+
+@python_2_unicode_compatible
+class Inquisition(models.Model):
+ expected = models.BooleanField()
+ leader = models.ForeignKey(Actor)
+ country = models.CharField(max_length=20)
+
+ def __str__(self):
+ return "by %s from %s" % (self.leader, self.country)
+
+
+@python_2_unicode_compatible
+class Sketch(models.Model):
+ title = models.CharField(max_length=100)
+ inquisition = models.ForeignKey(Inquisition, limit_choices_to={'leader__name': 'Palin',
+ 'leader__age': 27,
+ 'expected': False,
+ })
+
+ def __str__(self):
+ return self.title
+
+
+class Fabric(models.Model):
+ NG_CHOICES = (
+ ('Textured', (
+ ('x', 'Horizontal'),
+ ('y', 'Vertical'),
+ )
+ ),
+ ('plain', 'Smooth'),
+ )
+ surface = models.CharField(max_length=20, choices=NG_CHOICES)
+
+
+@python_2_unicode_compatible
+class Person(models.Model):
+ GENDER_CHOICES = (
+ (1, "Male"),
+ (2, "Female"),
+ )
+ name = models.CharField(max_length=100)
+ gender = models.IntegerField(choices=GENDER_CHOICES)
+ age = models.IntegerField(default=21)
+ alive = models.BooleanField()
+
+ def __str__(self):
+ return self.name
+
+
+@python_2_unicode_compatible
+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 __str__(self):
+ return self.name
+
+
+@python_2_unicode_compatible
+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 = 'generic service'
+
+ def __str__(self):
+ return "%s: %s" % (self.servicename, self.username)
+
+
+class FooAccount(Account):
+ """A service-specific account of type Foo."""
+ servicename = 'foo'
+
+
+class BarAccount(Account):
+ """A service-specific account of type Bar."""
+ servicename = 'bar'
+
+
+@python_2_unicode_compatible
+class Subscriber(models.Model):
+ name = models.CharField(blank=False, max_length=80)
+ email = models.EmailField(blank=False, max_length=175)
+
+ def __str__(self):
+ return "%s (%s)" % (self.name, self.email)
+
+
+class ExternalSubscriber(Subscriber):
+ pass
+
+
+class OldSubscriber(Subscriber):
+ pass
+
+
+class Media(models.Model):
+ name = models.CharField(max_length=60)
+
+
+class Podcast(Media):
+ release_date = models.DateField()
+
+ class Meta:
+ ordering = ('release_date',) # overridden in PodcastAdmin
+
+
+class Vodcast(Media):
+ media = models.OneToOneField(Media, primary_key=True, parent_link=True)
+ released = models.BooleanField(default=False)
+
+
+class Parent(models.Model):
+ name = models.CharField(max_length=128)
+
+
+class Child(models.Model):
+ parent = models.ForeignKey(Parent, editable=False)
+ name = models.CharField(max_length=30, blank=True)
+
+
+@python_2_unicode_compatible
+class EmptyModel(models.Model):
+ def __str__(self):
+ return "Primary key = %s" % self.id
+
+
+temp_storage = FileSystemStorage(tempfile.mkdtemp(dir=os.environ['DJANGO_TEST_TEMP_DIR']))
+UPLOAD_TO = os.path.join(temp_storage.location, 'test_upload')
+
+
+class Gallery(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class Picture(models.Model):
+ name = models.CharField(max_length=100)
+ image = models.FileField(storage=temp_storage, upload_to='test_upload')
+ gallery = models.ForeignKey(Gallery, related_name="pictures")
+
+
+class Language(models.Model):
+ iso = models.CharField(max_length=5, primary_key=True)
+ name = models.CharField(max_length=50)
+ english_name = models.CharField(max_length=50)
+ shortlist = models.BooleanField(default=False)
+
+ class Meta:
+ ordering = ('iso',)
+
+
+# a base class for Recommender and Recommendation
+class Title(models.Model):
+ pass
+
+
+class TitleTranslation(models.Model):
+ title = models.ForeignKey(Title)
+ text = models.CharField(max_length=100)
+
+
+class Recommender(Title):
+ pass
+
+
+class Recommendation(Title):
+ recommender = models.ForeignKey(Recommender)
+
+
+class Collector(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class Widget(models.Model):
+ owner = models.ForeignKey(Collector)
+ name = models.CharField(max_length=100)
+
+
+class DooHickey(models.Model):
+ code = models.CharField(max_length=10, primary_key=True)
+ owner = models.ForeignKey(Collector)
+ name = models.CharField(max_length=100)
+
+
+class Grommet(models.Model):
+ code = models.AutoField(primary_key=True)
+ owner = models.ForeignKey(Collector)
+ name = models.CharField(max_length=100)
+
+
+class Whatsit(models.Model):
+ index = models.IntegerField(primary_key=True)
+ owner = models.ForeignKey(Collector)
+ name = models.CharField(max_length=100)
+
+
+class Doodad(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class FancyDoodad(Doodad):
+ owner = models.ForeignKey(Collector)
+ expensive = models.BooleanField(default=True)
+
+
+@python_2_unicode_compatible
+class Category(models.Model):
+ collector = models.ForeignKey(Collector)
+ order = models.PositiveIntegerField()
+
+ class Meta:
+ ordering = ('order',)
+
+ def __str__(self):
+ return '%s:o%s' % (self.id, self.order)
+
+
+class Link(models.Model):
+ posted = models.DateField(
+ default=lambda: datetime.date.today() - datetime.timedelta(days=7)
+ )
+ url = models.URLField()
+ post = models.ForeignKey("Post")
+
+
+class PrePopulatedPost(models.Model):
+ title = models.CharField(max_length=100)
+ published = models.BooleanField()
+ slug = models.SlugField()
+
+
+class PrePopulatedSubPost(models.Model):
+ post = models.ForeignKey(PrePopulatedPost)
+ subtitle = models.CharField(max_length=100)
+ subslug = models.SlugField()
+
+
+class Post(models.Model):
+ title = models.CharField(max_length=100, help_text="Some help text for the title (with unicode ŠĐĆŽćžšđ)")
+ content = models.TextField(help_text="Some help text for the content (with unicode ŠĐĆŽćžšđ)")
+ posted = models.DateField(
+ default=datetime.date.today,
+ help_text="Some help text for the date (with unicode ŠĐĆŽćžšđ)"
+ )
+ public = models.NullBooleanField()
+
+ def awesomeness_level(self):
+ return "Very awesome."
+
+
+@python_2_unicode_compatible
+class Gadget(models.Model):
+ name = models.CharField(max_length=100)
+
+ def __str__(self):
+ return self.name
+
+
+@python_2_unicode_compatible
+class Villain(models.Model):
+ name = models.CharField(max_length=100)
+
+ def __str__(self):
+ return self.name
+
+
+class SuperVillain(Villain):
+ pass
+
+
+@python_2_unicode_compatible
+class FunkyTag(models.Model):
+ "Because we all know there's only one real use case for GFKs."
+ name = models.CharField(max_length=25)
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ content_object = generic.GenericForeignKey('content_type', 'object_id')
+
+ def __str__(self):
+ return self.name
+
+
+@python_2_unicode_compatible
+class Plot(models.Model):
+ name = models.CharField(max_length=100)
+ team_leader = models.ForeignKey(Villain, related_name='lead_plots')
+ contact = models.ForeignKey(Villain, related_name='contact_plots')
+ tags = generic.GenericRelation(FunkyTag)
+
+ def __str__(self):
+ return self.name
+
+
+@python_2_unicode_compatible
+class PlotDetails(models.Model):
+ details = models.CharField(max_length=100)
+ plot = models.OneToOneField(Plot)
+
+ def __str__(self):
+ return self.details
+
+
+@python_2_unicode_compatible
+class SecretHideout(models.Model):
+ """ Secret! Not registered with the admin! """
+ location = models.CharField(max_length=100)
+ villain = models.ForeignKey(Villain)
+
+ def __str__(self):
+ return self.location
+
+
+@python_2_unicode_compatible
+class SuperSecretHideout(models.Model):
+ """ Secret! Not registered with the admin! """
+ location = models.CharField(max_length=100)
+ supervillain = models.ForeignKey(SuperVillain)
+
+ def __str__(self):
+ return self.location
+
+
+@python_2_unicode_compatible
+class CyclicOne(models.Model):
+ name = models.CharField(max_length=25)
+ two = models.ForeignKey('CyclicTwo')
+
+ def __str__(self):
+ return self.name
+
+
+@python_2_unicode_compatible
+class CyclicTwo(models.Model):
+ name = models.CharField(max_length=25)
+ one = models.ForeignKey(CyclicOne)
+
+ def __str__(self):
+ return self.name
+
+
+class Topping(models.Model):
+ name = models.CharField(max_length=20)
+
+
+class Pizza(models.Model):
+ name = models.CharField(max_length=20)
+ toppings = models.ManyToManyField('Topping')
+
+
+class Album(models.Model):
+ owner = models.ForeignKey(User)
+ title = models.CharField(max_length=30)
+
+
+class Employee(Person):
+ code = models.CharField(max_length=20)
+
+
+class WorkHour(models.Model):
+ datum = models.DateField()
+ employee = models.ForeignKey(Employee)
+
+
+class Question(models.Model):
+ question = models.CharField(max_length=20)
+
+
+@python_2_unicode_compatible
+class Answer(models.Model):
+ question = models.ForeignKey(Question, on_delete=models.PROTECT)
+ answer = models.CharField(max_length=20)
+
+ def __str__(self):
+ return self.answer
+
+
+class Reservation(models.Model):
+ start_date = models.DateTimeField()
+ price = models.IntegerField()
+
+
+DRIVER_CHOICES = (
+ ('bill', 'Bill G'),
+ ('steve', 'Steve J'),
+)
+
+RESTAURANT_CHOICES = (
+ ('indian', 'A Taste of India'),
+ ('thai', 'Thai Pography'),
+ ('pizza', 'Pizza Mama'),
+)
+
+
+class FoodDelivery(models.Model):
+ reference = models.CharField(max_length=100)
+ driver = models.CharField(max_length=100, choices=DRIVER_CHOICES, blank=True)
+ restaurant = models.CharField(max_length=100, choices=RESTAURANT_CHOICES, blank=True)
+
+ class Meta:
+ unique_together = (("driver", "restaurant"),)
+
+
+@python_2_unicode_compatible
+class CoverLetter(models.Model):
+ author = models.CharField(max_length=30)
+ date_written = models.DateField(null=True, blank=True)
+
+ def __str__(self):
+ return self.author
+
+
+class Paper(models.Model):
+ title = models.CharField(max_length=30)
+ author = models.CharField(max_length=30, blank=True, null=True)
+
+
+class ShortMessage(models.Model):
+ content = models.CharField(max_length=140)
+ timestamp = models.DateTimeField(null=True, blank=True)
+
+
+@python_2_unicode_compatible
+class Telegram(models.Model):
+ title = models.CharField(max_length=30)
+ date_sent = models.DateField(null=True, blank=True)
+
+ def __str__(self):
+ return self.title
+
+
+class Story(models.Model):
+ title = models.CharField(max_length=100)
+ content = models.TextField()
+
+
+class OtherStory(models.Model):
+ title = models.CharField(max_length=100)
+ content = models.TextField()
+
+
+class ComplexSortedPerson(models.Model):
+ name = models.CharField(max_length=100)
+ age = models.PositiveIntegerField()
+ is_employee = models.NullBooleanField()
+
+class PrePopulatedPostLargeSlug(models.Model):
+ """
+ Regression test for #15938: a large max_length for the slugfield must not
+ be localized in prepopulated_fields_js.html or it might end up breaking
+ the javascript (ie, using THOUSAND_SEPARATOR ends up with maxLength=1,000)
+ """
+ title = models.CharField(max_length=100)
+ published = models.BooleanField()
+ slug = models.SlugField(max_length=1000)
+
+class AdminOrderedField(models.Model):
+ order = models.IntegerField()
+ stuff = models.CharField(max_length=200)
+
+class AdminOrderedModelMethod(models.Model):
+ order = models.IntegerField()
+ stuff = models.CharField(max_length=200)
+ def some_order(self):
+ return self.order
+ some_order.admin_order_field = 'order'
+
+class AdminOrderedAdminMethod(models.Model):
+ order = models.IntegerField()
+ stuff = models.CharField(max_length=200)
+
+class AdminOrderedCallable(models.Model):
+ order = models.IntegerField()
+ stuff = models.CharField(max_length=200)
+
+@python_2_unicode_compatible
+class Report(models.Model):
+ title = models.CharField(max_length=100)
+
+ def __str__(self):
+ return self.title
+
+
+class MainPrepopulated(models.Model):
+ name = models.CharField(max_length=100)
+ pubdate = models.DateField()
+ status = models.CharField(
+ max_length=20,
+ choices=(('option one', 'Option One'),
+ ('option two', 'Option Two')))
+ slug1 = models.SlugField()
+ slug2 = models.SlugField()
+
+class RelatedPrepopulated(models.Model):
+ parent = models.ForeignKey(MainPrepopulated)
+ name = models.CharField(max_length=75)
+ pubdate = models.DateField()
+ status = models.CharField(
+ max_length=20,
+ choices=(('option one', 'Option One'),
+ ('option two', 'Option Two')))
+ slug1 = models.SlugField(max_length=50)
+ slug2 = models.SlugField(max_length=60)
+
+
+class UnorderedObject(models.Model):
+ """
+ Model without any defined `Meta.ordering`.
+ Refs #16819.
+ """
+ name = models.CharField(max_length=255)
+ bool = models.BooleanField(default=True)
+
+class UndeletableObject(models.Model):
+ """
+ Model whose show_delete in admin change_view has been disabled
+ Refs #10057.
+ """
+ name = models.CharField(max_length=255)
+
+class UserMessenger(models.Model):
+ """
+ Dummy class for testing message_user functions on ModelAdmin
+ """
+
+class Simple(models.Model):
+ """
+ Simple model with nothing on it for use in testing
+ """
+
+class Choice(models.Model):
+ choice = models.IntegerField(blank=True, null=True,
+ choices=((1, 'Yes'), (0, 'No'), (None, 'No opinion')))
diff --git a/tests/admin_views/templates/custom_filter_template.html b/tests/admin_views/templates/custom_filter_template.html
new file mode 100644
index 0000000000..e5c9a8e7d8
--- /dev/null
+++ b/tests/admin_views/templates/custom_filter_template.html
@@ -0,0 +1,7 @@
+<h3>By {{ filter_title }} (custom)</h3>
+<ul>
+{% for choice in choices %}
+ <li{% if choice.selected %} class="selected"{% endif %}>
+ <a href="{{ choice.query_string|iriencode }}">{{ choice.display }}</a></li>
+{% endfor %}
+</ul>
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
new file mode 100644
index 0000000000..d3cfaa3e24
--- /dev/null
+++ b/tests/admin_views/tests.py
@@ -0,0 +1,4017 @@
+# coding: utf-8
+from __future__ import absolute_import, unicode_literals
+
+import os
+import re
+import datetime
+try:
+ from urllib.parse import urljoin
+except ImportError: # Python 2
+ from urlparse import urljoin
+
+from django.conf import settings, global_settings
+from django.core import mail
+from django.core.exceptions import SuspiciousOperation
+from django.core.files import temp as tempfile
+from django.core.urlresolvers import reverse
+# Register auth models with the admin.
+from django.contrib import admin
+from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
+from django.contrib.admin.models import LogEntry, DELETION
+from django.contrib.admin.sites import LOGIN_FORM_KEY
+from django.contrib.admin.util import quote
+from django.contrib.admin.views.main import IS_POPUP_VAR
+from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
+from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.contrib.auth.models import Group, User, Permission, UNUSABLE_PASSWORD
+from django.contrib.contenttypes.models import ContentType
+from django.forms.util import ErrorList
+from django.template.response import TemplateResponse
+from django.test import TestCase
+from django.utils import formats, translation, unittest
+from django.utils.cache import get_max_age
+from django.utils.encoding import iri_to_uri, force_bytes
+from django.utils.html import escape
+from django.utils.http import urlencode
+from django.utils._os import upath
+from django.utils import six
+from django.test.utils import override_settings
+
+# local test models
+from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount,
+ Gallery, ModelWithStringPrimaryKey, Person, Persona, Picture, Podcast,
+ Section, Subscriber, Vodcast, Language, Collector, Widget, Grommet,
+ DooHickey, FancyDoodad, Whatsit, Category, Post, Plot, FunkyTag, Chapter,
+ Book, Promo, WorkHour, Employee, Question, Answer, Inquisition, Actor,
+ FoodDelivery, RowLevelChangePermissionModel, Paper, CoverLetter, Story,
+ OtherStory, ComplexSortedPerson, Parent, Child, AdminOrderedField,
+ AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable,
+ Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject,
+ Simple, UndeletableObject, Choice, ShortMessage, Telegram)
+
+
+ERROR_MESSAGE = "Please enter the correct username and password \
+for a staff account. Note that both fields may be case-sensitive."
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminViewBasicTest(TestCase):
+ fixtures = ['admin-views-users.xml', 'admin-views-colors.xml',
+ 'admin-views-fabrics.xml', 'admin-views-books.xml']
+
+ # 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
+ # this test case and changing urlbit.
+ urlbit = 'admin'
+
+ urls = "regressiontests.admin_views.urls"
+
+ def setUp(self):
+ self.old_USE_I18N = settings.USE_I18N
+ self.old_USE_L10N = settings.USE_L10N
+ self.old_LANGUAGE_CODE = settings.LANGUAGE_CODE
+ self.client.login(username='super', password='secret')
+ settings.USE_I18N = True
+
+ def tearDown(self):
+ settings.USE_I18N = self.old_USE_I18N
+ settings.USE_L10N = self.old_USE_L10N
+ settings.LANGUAGE_CODE = self.old_LANGUAGE_CODE
+ self.client.logout()
+ formats.reset_format_cache()
+
+ def assertContentBefore(self, response, text1, text2, failing_msg=None):
+ """
+ Testing utility asserting that text1 appears before text2 in response
+ content.
+ """
+ self.assertEqual(response.status_code, 200)
+ self.assertTrue(response.content.index(force_bytes(text1)) < response.content.index(force_bytes(text2)),
+ failing_msg
+ )
+
+ def testTrailingSlashRequired(self):
+ """
+ If you leave off the trailing slash, app should redirect and add it.
+ """
+ response = self.client.get('/test_admin/%s/admin_views/article/add' % self.urlbit)
+ self.assertRedirects(response,
+ '/test_admin/%s/admin_views/article/add/' % self.urlbit, status_code=301
+ )
+
+ def testBasicAddGet(self):
+ """
+ A smoke test to ensure GET on the add_view works.
+ """
+ response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit)
+ self.assertIsInstance(response, TemplateResponse)
+ self.assertEqual(response.status_code, 200)
+
+ def testAddWithGETArgs(self):
+ response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit, {'name': 'My Section'})
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'value="My Section"',
+ msg_prefix="Couldn't find an input with the right value in the response"
+ )
+
+ def testBasicEditGet(self):
+ """
+ A smoke test to ensure GET on the change_view works.
+ """
+ response = self.client.get('/test_admin/%s/admin_views/section/1/' % self.urlbit)
+ self.assertIsInstance(response, TemplateResponse)
+ self.assertEqual(response.status_code, 200)
+
+ def testBasicEditGetStringPK(self):
+ """
+ A smoke test to ensure GET on the change_view works (returns an HTTP
+ 404 error, see #11191) when passing a string as the PK argument for a
+ model with an integer PK field.
+ """
+ response = self.client.get('/test_admin/%s/admin_views/section/abc/' % self.urlbit)
+ self.assertEqual(response.status_code, 404)
+
+ def testBasicAddPost(self):
+ """
+ A smoke test to ensure POST on add_view works.
+ """
+ post_data = {
+ "name": "Another Section",
+ # inline data
+ "article_set-TOTAL_FORMS": "3",
+ "article_set-INITIAL_FORMS": "0",
+ "article_set-MAX_NUM_FORMS": "0",
+ }
+ response = self.client.post('/test_admin/%s/admin_views/section/add/' % self.urlbit, post_data)
+ self.assertEqual(response.status_code, 302) # redirect somewhere
+
+ def testPopupAddPost(self):
+ """
+ Ensure http response from a popup is properly escaped.
+ """
+ post_data = {
+ '_popup': '1',
+ 'title': 'title with a new\nline',
+ 'content': 'some content',
+ 'date_0': '2010-09-10',
+ 'date_1': '14:55:39',
+ }
+ response = self.client.post('/test_admin/%s/admin_views/article/add/' % self.urlbit, post_data)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'dismissAddAnotherPopup')
+ self.assertContains(response, 'title with a new\\u000Aline')
+
+ # Post data for edit inline
+ inline_post_data = {
+ "name": "Test section",
+ # inline data
+ "article_set-TOTAL_FORMS": "6",
+ "article_set-INITIAL_FORMS": "3",
+ "article_set-MAX_NUM_FORMS": "0",
+ "article_set-0-id": "1",
+ # there is no title in database, give one here or formset will fail.
+ "article_set-0-title": "Norske bostaver æøå skaper problemer",
+ "article_set-0-content": "&lt;p&gt;Middle content&lt;/p&gt;",
+ "article_set-0-date_0": "2008-03-18",
+ "article_set-0-date_1": "11:54:58",
+ "article_set-0-section": "1",
+ "article_set-1-id": "2",
+ "article_set-1-title": "Need a title.",
+ "article_set-1-content": "&lt;p&gt;Oldest content&lt;/p&gt;",
+ "article_set-1-date_0": "2000-03-18",
+ "article_set-1-date_1": "11:54:58",
+ "article_set-2-id": "3",
+ "article_set-2-title": "Need a title.",
+ "article_set-2-content": "&lt;p&gt;Newest content&lt;/p&gt;",
+ "article_set-2-date_0": "2009-03-18",
+ "article_set-2-date_1": "11:54:58",
+ "article_set-3-id": "",
+ "article_set-3-title": "",
+ "article_set-3-content": "",
+ "article_set-3-date_0": "",
+ "article_set-3-date_1": "",
+ "article_set-4-id": "",
+ "article_set-4-title": "",
+ "article_set-4-content": "",
+ "article_set-4-date_0": "",
+ "article_set-4-date_1": "",
+ "article_set-5-id": "",
+ "article_set-5-title": "",
+ "article_set-5-content": "",
+ "article_set-5-date_0": "",
+ "article_set-5-date_1": "",
+ }
+
+ def testBasicEditPost(self):
+ """
+ A smoke test to ensure POST on edit_view works.
+ """
+ response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, self.inline_post_data)
+ self.assertEqual(response.status_code, 302) # redirect somewhere
+
+ def testEditSaveAs(self):
+ """
+ Test "save as".
+ """
+ post_data = self.inline_post_data.copy()
+ post_data.update({
+ '_saveasnew': 'Save+as+new',
+ "article_set-1-section": "1",
+ "article_set-2-section": "1",
+ "article_set-3-section": "1",
+ "article_set-4-section": "1",
+ "article_set-5-section": "1",
+ })
+ response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, post_data)
+ self.assertEqual(response.status_code, 302) # redirect somewhere
+
+ def testChangeListSortingCallable(self):
+ """
+ Ensure we can sort on a list_display field that is a callable
+ (column 2 is callable_year in ArticleAdmin)
+ """
+ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'o': 2})
+ self.assertContentBefore(response, 'Oldest content', 'Middle content',
+ "Results of sorting on callable are out of order.")
+ self.assertContentBefore(response, 'Middle content', 'Newest content',
+ "Results of sorting on callable are out of order.")
+
+ def testChangeListSortingModel(self):
+ """
+ Ensure we can sort on a list_display field that is a Model method
+ (colunn 3 is 'model_year' in ArticleAdmin)
+ """
+ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'o': '-3'})
+ self.assertContentBefore(response, 'Newest content', 'Middle content',
+ "Results of sorting on Model method are out of order.")
+ self.assertContentBefore(response, 'Middle content', 'Oldest content',
+ "Results of sorting on Model method are out of order.")
+
+ def testChangeListSortingModelAdmin(self):
+ """
+ Ensure we can sort on a list_display field that is a ModelAdmin method
+ (colunn 4 is 'modeladmin_year' in ArticleAdmin)
+ """
+ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'o': '4'})
+ self.assertContentBefore(response, 'Oldest content', 'Middle content',
+ "Results of sorting on ModelAdmin method are out of order.")
+ self.assertContentBefore(response, 'Middle content', 'Newest content',
+ "Results of sorting on ModelAdmin method are out of order.")
+
+ def testChangeListSortingMultiple(self):
+ p1 = Person.objects.create(name="Chris", gender=1, alive=True)
+ p2 = Person.objects.create(name="Chris", gender=2, alive=True)
+ p3 = Person.objects.create(name="Bob", gender=1, alive=True)
+ link1 = reverse('admin:admin_views_person_change', args=(p1.pk,))
+ link2 = reverse('admin:admin_views_person_change', args=(p2.pk,))
+ link3 = reverse('admin:admin_views_person_change', args=(p3.pk,))
+
+ # Sort by name, gender
+ # This hard-codes the URL because it'll fail if it runs against the
+ # 'admin2' custom admin (which doesn't have the Person model).
+ response = self.client.get('/test_admin/admin/admin_views/person/', {'o': '1.2'})
+ self.assertContentBefore(response, link3, link1)
+ self.assertContentBefore(response, link1, link2)
+
+ # Sort by gender descending, name
+ response = self.client.get('/test_admin/admin/admin_views/person/', {'o': '-2.1'})
+ self.assertContentBefore(response, link2, link3)
+ self.assertContentBefore(response, link3, link1)
+
+ def testChangeListSortingPreserveQuerySetOrdering(self):
+ """
+ If no ordering is defined in `ModelAdmin.ordering` or in the query
+ string, then the underlying order of the queryset should not be
+ changed, even if it is defined in `Modeladmin.queryset()`.
+ Refs #11868, #7309.
+ """
+ p1 = Person.objects.create(name="Amy", gender=1, alive=True, age=80)
+ p2 = Person.objects.create(name="Bob", gender=1, alive=True, age=70)
+ p3 = Person.objects.create(name="Chris", gender=2, alive=False, age=60)
+ link1 = reverse('admin:admin_views_person_change', args=(p1.pk,))
+ link2 = reverse('admin:admin_views_person_change', args=(p2.pk,))
+ link3 = reverse('admin:admin_views_person_change', args=(p3.pk,))
+
+ # This hard-codes the URL because it'll fail if it runs against the
+ # 'admin2' custom admin (which doesn't have the Person model).
+ response = self.client.get('/test_admin/admin/admin_views/person/', {})
+ self.assertContentBefore(response, link3, link2)
+ self.assertContentBefore(response, link2, link1)
+
+ def testChangeListSortingModelMeta(self):
+ # Test ordering on Model Meta is respected
+
+ l1 = Language.objects.create(iso='ur', name='Urdu')
+ l2 = Language.objects.create(iso='ar', name='Arabic')
+ link1 = reverse('admin:admin_views_language_change', args=(quote(l1.pk),))
+ link2 = reverse('admin:admin_views_language_change', args=(quote(l2.pk),))
+
+ response = self.client.get('/test_admin/admin/admin_views/language/', {})
+ self.assertContentBefore(response, link2, link1)
+
+ # Test we can override with query string
+ response = self.client.get('/test_admin/admin/admin_views/language/', {'o': '-1'})
+ self.assertContentBefore(response, link1, link2)
+
+ def testChangeListSortingOverrideModelAdmin(self):
+ # Test ordering on Model Admin is respected, and overrides Model Meta
+ dt = datetime.datetime.now()
+ p1 = Podcast.objects.create(name="A", release_date=dt)
+ p2 = Podcast.objects.create(name="B", release_date=dt - datetime.timedelta(10))
+ link1 = reverse('admin:admin_views_podcast_change', args=(p1.pk,))
+ link2 = reverse('admin:admin_views_podcast_change', args=(p2.pk,))
+
+ response = self.client.get('/test_admin/admin/admin_views/podcast/', {})
+ self.assertContentBefore(response, link1, link2)
+
+ def testMultipleSortSameField(self):
+ # Check that we get the columns we expect if we have two columns
+ # that correspond to the same ordering field
+ dt = datetime.datetime.now()
+ p1 = Podcast.objects.create(name="A", release_date=dt)
+ p2 = Podcast.objects.create(name="B", release_date=dt - datetime.timedelta(10))
+ link1 = reverse('admin:admin_views_podcast_change', args=(quote(p1.pk),))
+ link2 = reverse('admin:admin_views_podcast_change', args=(quote(p2.pk),))
+
+ response = self.client.get('/test_admin/admin/admin_views/podcast/', {})
+ self.assertContentBefore(response, link1, link2)
+
+ p1 = ComplexSortedPerson.objects.create(name="Bob", age=10)
+ p2 = ComplexSortedPerson.objects.create(name="Amy", age=20)
+ link1 = reverse('admin:admin_views_complexsortedperson_change', args=(p1.pk,))
+ link2 = reverse('admin:admin_views_complexsortedperson_change', args=(p2.pk,))
+
+ response = self.client.get('/test_admin/admin/admin_views/complexsortedperson/', {})
+ # Should have 5 columns (including action checkbox col)
+ self.assertContains(response, '<th scope="col"', count=5)
+
+ self.assertContains(response, 'Name')
+ self.assertContains(response, 'Colored name')
+
+ # Check order
+ self.assertContentBefore(response, 'Name', 'Colored name')
+
+ # Check sorting - should be by name
+ self.assertContentBefore(response, link2, link1)
+
+ def testSortIndicatorsAdminOrder(self):
+ """
+ Ensures that the admin shows default sort indicators for all
+ kinds of 'ordering' fields: field names, method on the model
+ admin and model itself, and other callables. See #17252.
+ """
+ models = [(AdminOrderedField, 'adminorderedfield'),
+ (AdminOrderedModelMethod, 'adminorderedmodelmethod'),
+ (AdminOrderedAdminMethod, 'adminorderedadminmethod'),
+ (AdminOrderedCallable, 'adminorderedcallable')]
+ for model, url in models:
+ a1 = model.objects.create(stuff='The Last Item', order=3)
+ a2 = model.objects.create(stuff='The First Item', order=1)
+ a3 = model.objects.create(stuff='The Middle Item', order=2)
+ response = self.client.get('/test_admin/admin/admin_views/%s/' % url, {})
+ self.assertEqual(response.status_code, 200)
+ # Should have 3 columns including action checkbox col.
+ self.assertContains(response, '<th scope="col"', count=3, msg_prefix=url)
+ # Check if the correct column was selected. 2 is the index of the
+ # 'order' column in the model admin's 'list_display' with 0 being
+ # the implicit 'action_checkbox' and 1 being the column 'stuff'.
+ self.assertEqual(response.context['cl'].get_ordering_field_columns(), {2: 'asc'})
+ # Check order of records.
+ self.assertContentBefore(response, 'The First Item', 'The Middle Item')
+ self.assertContentBefore(response, 'The Middle Item', 'The Last Item')
+
+ def testLimitedFilter(self):
+ """Ensure admin changelist filters do not contain objects excluded via limit_choices_to.
+ This also tests relation-spanning filters (e.g. 'color__value').
+ """
+ response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, '<div id="changelist-filter">',
+ msg_prefix="Expected filter not found in changelist view"
+ )
+ self.assertNotContains(response, '<a href="?color__id__exact=3">Blue</a>',
+ msg_prefix="Changelist filter not correctly limited by limit_choices_to"
+ )
+
+ def testRelationSpanningFilters(self):
+ response = self.client.get('/test_admin/%s/admin_views/chapterxtra1/' %
+ self.urlbit)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, '<div id="changelist-filter">')
+ filters = {
+ 'chap__id__exact': dict(
+ values=[c.id for c in Chapter.objects.all()],
+ test=lambda obj, value: obj.chap.id == value),
+ 'chap__title': dict(
+ values=[c.title for c in Chapter.objects.all()],
+ test=lambda obj, value: obj.chap.title == value),
+ 'chap__book__id__exact': dict(
+ values=[b.id for b in Book.objects.all()],
+ test=lambda obj, value: obj.chap.book.id == value),
+ 'chap__book__name': dict(
+ values=[b.name for b in Book.objects.all()],
+ test=lambda obj, value: obj.chap.book.name == value),
+ 'chap__book__promo__id__exact': dict(
+ values=[p.id for p in Promo.objects.all()],
+ test=lambda obj, value:
+ obj.chap.book.promo_set.filter(id=value).exists()),
+ 'chap__book__promo__name': dict(
+ values=[p.name for p in Promo.objects.all()],
+ test=lambda obj, value:
+ obj.chap.book.promo_set.filter(name=value).exists()),
+ }
+ for filter_path, params in filters.items():
+ for value in params['values']:
+ query_string = urlencode({filter_path: value})
+ # ensure filter link exists
+ self.assertContains(response, '<a href="?%s">' % query_string)
+ # ensure link works
+ filtered_response = self.client.get(
+ '/test_admin/%s/admin_views/chapterxtra1/?%s' % (
+ self.urlbit, query_string))
+ self.assertEqual(filtered_response.status_code, 200)
+ # ensure changelist contains only valid objects
+ for obj in filtered_response.context['cl'].query_set.all():
+ self.assertTrue(params['test'](obj, value))
+
+ def testIncorrectLookupParameters(self):
+ """Ensure incorrect lookup parameters are handled gracefully."""
+ 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)
+
+ # Spanning relationships through an inexistant related object (Refs #16716)
+ response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'notarealfield__whatever': '5'})
+ 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!'})
+ self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit)
+
+ # Regression test for #18530
+ response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'pub_date__gte': 'foo'})
+ self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit)
+
+ def testIsNullLookups(self):
+ """Ensure is_null is handled correctly."""
+ Article.objects.create(title="I Could Go Anywhere", content="Versatile", date=datetime.datetime.now())
+ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit)
+ self.assertContains(response, '4 articles')
+ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'section__isnull': 'false'})
+ self.assertContains(response, '3 articles')
+ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'section__isnull': 'true'})
+ self.assertContains(response, '1 article')
+
+ def testLogoutAndPasswordChangeURLs(self):
+ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit)
+ self.assertContains(response, '<a href="/test_admin/%s/logout/">' % self.urlbit)
+ self.assertContains(response, '<a href="/test_admin/%s/password_change/">' % self.urlbit)
+
+ def testNamedGroupFieldChoicesChangeList(self):
+ """
+ Ensures the admin changelist shows correct values in the relevant column
+ for rows corresponding to instances of a model in which a named group
+ has been used in the choices option of a field.
+ """
+ link1 = reverse('admin:admin_views_fabric_change', args=(1,), current_app=self.urlbit)
+ link2 = reverse('admin:admin_views_fabric_change', args=(2,), current_app=self.urlbit)
+ response = self.client.get('/test_admin/%s/admin_views/fabric/' % self.urlbit)
+ fail_msg = "Changelist table isn't showing the right human-readable values set by a model field 'choices' option named group."
+ self.assertContains(response, '<a href="%s">Horizontal</a>' % link1, msg_prefix=fail_msg, html=True)
+ self.assertContains(response, '<a href="%s">Vertical</a>' % link2, msg_prefix=fail_msg, html=True)
+
+ def testNamedGroupFieldChoicesFilter(self):
+ """
+ Ensures the filter UI shows correctly when at least one named group has
+ been used in the choices option of a model field.
+ """
+ response = self.client.get('/test_admin/%s/admin_views/fabric/' % self.urlbit)
+ fail_msg = "Changelist filter isn't showing options contained inside a model field 'choices' option named group."
+ self.assertContains(response, '<div id="changelist-filter">')
+ self.assertContains(response,
+ '<a href="?surface__exact=x">Horizontal</a>', msg_prefix=fail_msg, html=True)
+ self.assertContains(response,
+ '<a href="?surface__exact=y">Vertical</a>', msg_prefix=fail_msg, html=True)
+
+ def testChangeListNullBooleanDisplay(self):
+ Post.objects.create(public=None)
+ # This hard-codes the URl because it'll fail if it runs
+ # against the 'admin2' custom admin (which doesn't have the
+ # Post model).
+ response = self.client.get("/test_admin/admin/admin_views/post/")
+ self.assertContains(response, 'icon-unknown.gif')
+
+ def testI18NLanguageNonEnglishDefault(self):
+ """
+ Check if the JavaScript i18n view returns an empty language catalog
+ if the default language is non-English but the selected language
+ is English. See #13388 and #3594 for more details.
+ """
+ with self.settings(LANGUAGE_CODE='fr'):
+ with translation.override('en-us'):
+ response = self.client.get('/test_admin/admin/jsi18n/')
+ self.assertNotContains(response, 'Choisir une heure')
+
+ def testI18NLanguageNonEnglishFallback(self):
+ """
+ Makes sure that the fallback language is still working properly
+ in cases where the selected language cannot be found.
+ """
+ with self.settings(LANGUAGE_CODE='fr'):
+ with translation.override('none'):
+ response = self.client.get('/test_admin/admin/jsi18n/')
+ self.assertContains(response, 'Choisir une heure')
+
+ def testL10NDeactivated(self):
+ """
+ Check if L10N is deactivated, the JavaScript i18n view doesn't
+ return localized date/time formats. Refs #14824.
+ """
+ with self.settings(LANGUAGE_CODE='ru', USE_L10N=False):
+ with translation.override('none'):
+ response = self.client.get('/test_admin/admin/jsi18n/')
+ self.assertNotContains(response, '%d.%m.%Y %H:%M:%S')
+ self.assertContains(response, '%Y-%m-%d %H:%M:%S')
+
+ def test_disallowed_filtering(self):
+ self.assertRaises(SuspiciousOperation,
+ self.client.get, "/test_admin/admin/admin_views/album/?owner__email__startswith=fuzzy"
+ )
+
+ try:
+ self.client.get("/test_admin/admin/admin_views/thing/?color__value__startswith=red")
+ self.client.get("/test_admin/admin/admin_views/thing/?color__value=red")
+ except SuspiciousOperation:
+ self.fail("Filters are allowed if explicitly included in list_filter")
+
+ try:
+ self.client.get("/test_admin/admin/admin_views/person/?age__gt=30")
+ except SuspiciousOperation:
+ self.fail("Filters should be allowed if they involve a local field without the need to whitelist them in list_filter or date_hierarchy.")
+
+ e1 = Employee.objects.create(name='Anonymous', gender=1, age=22, alive=True, code='123')
+ e2 = Employee.objects.create(name='Visitor', gender=2, age=19, alive=True, code='124')
+ WorkHour.objects.create(datum=datetime.datetime.now(), employee=e1)
+ WorkHour.objects.create(datum=datetime.datetime.now(), employee=e2)
+ response = self.client.get("/test_admin/admin/admin_views/workhour/")
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'employee__person_ptr__exact')
+ response = self.client.get("/test_admin/admin/admin_views/workhour/?employee__person_ptr__exact=%d" % e1.pk)
+ self.assertEqual(response.status_code, 200)
+
+ def test_allowed_filtering_15103(self):
+ """
+ Regressions test for ticket 15103 - filtering on fields defined in a
+ ForeignKey 'limit_choices_to' should be allowed, otherwise raw_id_fields
+ can break.
+ """
+ try:
+ self.client.get("/test_admin/admin/admin_views/inquisition/?leader__name=Palin&leader__age=27")
+ except SuspiciousOperation:
+ self.fail("Filters should be allowed if they are defined on a ForeignKey pointing to this model")
+
+ def test_hide_change_password(self):
+ """
+ Tests if the "change password" link in the admin is hidden if the User
+ does not have a usable password set.
+ (against 9bea85795705d015cdadc82c68b99196a8554f5c)
+ """
+ user = User.objects.get(username='super')
+ password = user.password
+ user.set_unusable_password()
+ user.save()
+
+ response = self.client.get('/test_admin/admin/')
+ self.assertNotContains(response, reverse('admin:password_change'),
+ msg_prefix='The "change password" link should not be displayed if a user does not have a usable password.')
+
+ def test_change_view_with_show_delete_extra_context(self):
+ """
+ Ensured that the 'show_delete' context variable in the admin's change
+ view actually controls the display of the delete button.
+ Refs #10057.
+ """
+ instance = UndeletableObject.objects.create(name='foo')
+ response = self.client.get('/test_admin/%s/admin_views/undeletableobject/%d/' %
+ (self.urlbit, instance.pk))
+ self.assertNotContains(response, 'deletelink')
+
+ def test_allows_attributeerror_to_bubble_up(self):
+ """
+ Ensure that AttributeErrors are allowed to bubble when raised inside
+ a change list view.
+
+ Requires a model to be created so there's something to be displayed
+
+ Refs: #16655, #18593, and #18747
+ """
+ Simple.objects.create()
+ with self.assertRaises(AttributeError):
+ self.client.get('/test_admin/%s/admin_views/simple/' % self.urlbit)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminViewFormUrlTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ["admin-views-users.xml"]
+ urlbit = "admin3"
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def testChangeFormUrlHasCorrectValue(self):
+ """
+ Tests whether change_view has form_url in response.context
+ """
+ response = self.client.get('/test_admin/%s/admin_views/section/1/' % self.urlbit)
+ self.assertTrue('form_url' in response.context, msg='form_url not present in response.context')
+ self.assertEqual(response.context['form_url'], 'pony')
+
+ def test_filter_with_custom_template(self):
+ """
+ Ensure that one can use a custom template to render an admin filter.
+ Refs #17515.
+ """
+ template_dirs = settings.TEMPLATE_DIRS + (
+ os.path.join(os.path.dirname(upath(__file__)), 'templates'),)
+ with self.settings(TEMPLATE_DIRS=template_dirs):
+ response = self.client.get("/test_admin/admin/admin_views/color2/")
+ self.assertTrue('custom_filter_template.html' in [t.name for t in response.templates])
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminJavaScriptTest(TestCase):
+ fixtures = ['admin-views-users.xml']
+
+ urls = "regressiontests.admin_views.urls"
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def testSingleWidgetFirsFieldFocus(self):
+ """
+ JavaScript-assisted auto-focus on first field.
+ """
+ response = self.client.get('/test_admin/%s/admin_views/picture/add/' % 'admin')
+ self.assertContains(
+ response,
+ '<script type="text/javascript">document.getElementById("id_name").focus();</script>'
+ )
+
+ def testMultiWidgetFirsFieldFocus(self):
+ """
+ JavaScript-assisted auto-focus should work if a model/ModelAdmin setup
+ is such that the first form field has a MultiWidget.
+ """
+ response = self.client.get('/test_admin/%s/admin_views/reservation/add/' % 'admin')
+ self.assertContains(
+ response,
+ '<script type="text/javascript">document.getElementById("id_start_date_0").focus();</script>'
+ )
+
+ def test_js_minified_only_if_debug_is_false(self):
+ """
+ Ensure that the minified versions of the JS files are only used when
+ DEBUG is False.
+ Refs #17521.
+ """
+ with override_settings(DEBUG=False):
+ response = self.client.get(
+ '/test_admin/%s/admin_views/section/add/' % 'admin')
+ self.assertNotContains(response, 'jquery.js')
+ self.assertContains(response, 'jquery.min.js')
+ self.assertNotContains(response, 'prepopulate.js')
+ self.assertContains(response, 'prepopulate.min.js')
+ self.assertNotContains(response, 'actions.js')
+ self.assertContains(response, 'actions.min.js')
+ self.assertNotContains(response, 'collapse.js')
+ self.assertContains(response, 'collapse.min.js')
+ self.assertNotContains(response, 'inlines.js')
+ self.assertContains(response, 'inlines.min.js')
+ with override_settings(DEBUG=True):
+ response = self.client.get(
+ '/test_admin/%s/admin_views/section/add/' % 'admin')
+ self.assertContains(response, 'jquery.js')
+ self.assertNotContains(response, 'jquery.min.js')
+ self.assertContains(response, 'prepopulate.js')
+ self.assertNotContains(response, 'prepopulate.min.js')
+ self.assertContains(response, 'actions.js')
+ self.assertNotContains(response, 'actions.min.js')
+ self.assertContains(response, 'collapse.js')
+ self.assertNotContains(response, 'collapse.min.js')
+ self.assertContains(response, 'inlines.js')
+ self.assertNotContains(response, 'inlines.min.js')
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class SaveAsTests(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml', 'admin-views-person.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_save_as_duplication(self):
+ """Ensure save as actually creates a new person"""
+ post_data = {'_saveasnew': '', 'name': 'John M', 'gender': 1, 'age': 42}
+ response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data)
+ self.assertEqual(len(Person.objects.filter(name='John M')), 1)
+ self.assertEqual(len(Person.objects.filter(id=1)), 1)
+
+ def test_save_as_display(self):
+ """
+ Ensure that 'save as' is displayed when activated and after submitting
+ invalid data aside save_as_new will not show us a form to overwrite the
+ initial model.
+ """
+ response = self.client.get('/test_admin/admin/admin_views/person/1/')
+ self.assertTrue(response.context['save_as'])
+ post_data = {'_saveasnew': '', 'name': 'John M', 'gender': 3, 'alive': 'checked'}
+ response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data)
+ self.assertEqual(response.context['form_url'], '/test_admin/admin/admin_views/person/add/')
+
+
+class CustomModelAdminTest(AdminViewBasicTest):
+ urls = "regressiontests.admin_views.urls"
+ urlbit = "admin2"
+
+ def testCustomAdminSiteLoginForm(self):
+ self.client.logout()
+ response = self.client.get('/test_admin/admin2/')
+ self.assertIsInstance(response, TemplateResponse)
+ self.assertEqual(response.status_code, 200)
+ login = self.client.post('/test_admin/admin2/', {
+ REDIRECT_FIELD_NAME: '/test_admin/admin2/',
+ LOGIN_FORM_KEY: 1,
+ 'username': 'customform',
+ 'password': 'secret',
+ })
+ self.assertIsInstance(login, TemplateResponse)
+ self.assertEqual(login.status_code, 200)
+ self.assertContains(login, 'custom form error')
+
+ def testCustomAdminSiteLoginTemplate(self):
+ self.client.logout()
+ response = self.client.get('/test_admin/admin2/')
+ self.assertIsInstance(response, TemplateResponse)
+ self.assertTemplateUsed(response, 'custom_admin/login.html')
+ self.assertContains(response, 'Hello from a custom login template')
+
+ def testCustomAdminSiteLogoutTemplate(self):
+ response = self.client.get('/test_admin/admin2/logout/')
+ self.assertIsInstance(response, TemplateResponse)
+ self.assertTemplateUsed(response, 'custom_admin/logout.html')
+ self.assertContains(response, 'Hello from a custom logout template')
+
+ def testCustomAdminSiteIndexViewAndTemplate(self):
+ try:
+ response = self.client.get('/test_admin/admin2/')
+ except TypeError:
+ self.fail('AdminSite.index_template should accept a list of template paths')
+ self.assertIsInstance(response, TemplateResponse)
+ self.assertTemplateUsed(response, 'custom_admin/index.html')
+ self.assertContains(response, 'Hello from a custom index template *bar*')
+
+ def testCustomAdminSitePasswordChangeTemplate(self):
+ response = self.client.get('/test_admin/admin2/password_change/')
+ self.assertIsInstance(response, TemplateResponse)
+ self.assertTemplateUsed(response, 'custom_admin/password_change_form.html')
+ self.assertContains(response, 'Hello from a custom password change form template')
+
+ def testCustomAdminSitePasswordChangeDoneTemplate(self):
+ response = self.client.get('/test_admin/admin2/password_change/done/')
+ self.assertIsInstance(response, TemplateResponse)
+ self.assertTemplateUsed(response, 'custom_admin/password_change_done.html')
+ self.assertContains(response, 'Hello from a custom password change done template')
+
+ def testCustomAdminSiteView(self):
+ self.client.login(username='super', password='secret')
+ response = self.client.get('/test_admin/%s/my_view/' % self.urlbit)
+ self.assertEqual(response.content, b"Django is a magical pony!")
+
+ def test_pwd_change_custom_template(self):
+ self.client.login(username='super', password='secret')
+ su = User.objects.get(username='super')
+ try:
+ response = self.client.get('/test_admin/admin4/auth/user/%s/password/' % su.pk)
+ except TypeError:
+ self.fail('ModelAdmin.change_user_password_template should accept a list of template paths')
+ self.assertEqual(response.status_code, 200)
+
+
+def get_perm(Model, perm):
+ """Return the permission object, for the Model"""
+ ct = ContentType.objects.get_for_model(Model)
+ return Permission.objects.get(content_type=ct, codename=perm)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminViewPermissionsTest(TestCase):
+ """Tests for Admin Views Permissions."""
+
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ """Test setup."""
+ # Setup permissions, for our users who can add, change, and delete.
+ # We can't put this into the fixture, because the content type id
+ # and the permission id could be different on each run of the test.
+
+ opts = Article._meta
+
+ # User who can add Articles
+ add_user = User.objects.get(username='adduser')
+ add_user.user_permissions.add(get_perm(Article,
+ opts.get_add_permission()))
+
+ # User who can change Articles
+ change_user = User.objects.get(username='changeuser')
+ change_user.user_permissions.add(get_perm(Article,
+ opts.get_change_permission()))
+
+ # User who can delete Articles
+ delete_user = User.objects.get(username='deleteuser')
+ delete_user.user_permissions.add(get_perm(Article,
+ opts.get_delete_permission()))
+
+ delete_user.user_permissions.add(get_perm(Section,
+ Section._meta.get_delete_permission()))
+
+ # login POST dicts
+ self.super_login = {
+ REDIRECT_FIELD_NAME: '/test_admin/admin/',
+ LOGIN_FORM_KEY: 1,
+ 'username': 'super',
+ 'password': 'secret',
+ }
+ self.super_email_login = {
+ REDIRECT_FIELD_NAME: '/test_admin/admin/',
+ LOGIN_FORM_KEY: 1,
+ 'username': 'super@example.com',
+ 'password': 'secret',
+ }
+ self.super_email_bad_login = {
+ REDIRECT_FIELD_NAME: '/test_admin/admin/',
+ LOGIN_FORM_KEY: 1,
+ 'username': 'super@example.com',
+ 'password': 'notsecret',
+ }
+ self.adduser_login = {
+ REDIRECT_FIELD_NAME: '/test_admin/admin/',
+ LOGIN_FORM_KEY: 1,
+ 'username': 'adduser',
+ 'password': 'secret',
+ }
+ self.changeuser_login = {
+ REDIRECT_FIELD_NAME: '/test_admin/admin/',
+ LOGIN_FORM_KEY: 1,
+ 'username': 'changeuser',
+ 'password': 'secret',
+ }
+ self.deleteuser_login = {
+ REDIRECT_FIELD_NAME: '/test_admin/admin/',
+ LOGIN_FORM_KEY: 1,
+ 'username': 'deleteuser',
+ 'password': 'secret',
+ }
+ self.joepublic_login = {
+ REDIRECT_FIELD_NAME: '/test_admin/admin/',
+ LOGIN_FORM_KEY: 1,
+ 'username': 'joepublic',
+ 'password': 'secret',
+ }
+ self.no_username_login = {
+ REDIRECT_FIELD_NAME: '/test_admin/admin/',
+ LOGIN_FORM_KEY: 1,
+ 'password': 'secret',
+ }
+
+ def testLogin(self):
+ """
+ Make sure only staff members can log in.
+
+ Successful posts to the login page will redirect to the orignal url.
+ Unsuccessfull attempts will continue to render the login page with
+ a 200 status code.
+ """
+ # Super User
+ response = self.client.get('/test_admin/admin/')
+ self.assertEqual(response.status_code, 200)
+ login = self.client.post('/test_admin/admin/', self.super_login)
+ self.assertRedirects(login, '/test_admin/admin/')
+ self.assertFalse(login.context)
+ self.client.get('/test_admin/admin/logout/')
+
+ # Test if user enters email address
+ response = self.client.get('/test_admin/admin/')
+ self.assertEqual(response.status_code, 200)
+ login = self.client.post('/test_admin/admin/', self.super_email_login)
+ self.assertContains(login, ERROR_MESSAGE)
+ # only correct passwords get a username hint
+ login = self.client.post('/test_admin/admin/', self.super_email_bad_login)
+ self.assertContains(login, ERROR_MESSAGE)
+ new_user = User(username='jondoe', password='secret', email='super@example.com')
+ new_user.save()
+ # check to ensure if there are multiple email addresses a user doesn't get a 500
+ login = self.client.post('/test_admin/admin/', self.super_email_login)
+ self.assertContains(login, ERROR_MESSAGE)
+
+ # Add User
+ response = self.client.get('/test_admin/admin/')
+ self.assertEqual(response.status_code, 200)
+ login = self.client.post('/test_admin/admin/', self.adduser_login)
+ self.assertRedirects(login, '/test_admin/admin/')
+ self.assertFalse(login.context)
+ self.client.get('/test_admin/admin/logout/')
+
+ # Change User
+ response = self.client.get('/test_admin/admin/')
+ self.assertEqual(response.status_code, 200)
+ login = self.client.post('/test_admin/admin/', self.changeuser_login)
+ self.assertRedirects(login, '/test_admin/admin/')
+ self.assertFalse(login.context)
+ self.client.get('/test_admin/admin/logout/')
+
+ # Delete User
+ response = self.client.get('/test_admin/admin/')
+ self.assertEqual(response.status_code, 200)
+ login = self.client.post('/test_admin/admin/', self.deleteuser_login)
+ self.assertRedirects(login, '/test_admin/admin/')
+ self.assertFalse(login.context)
+ self.client.get('/test_admin/admin/logout/')
+
+ # Regular User should not be able to login.
+ response = self.client.get('/test_admin/admin/')
+ self.assertEqual(response.status_code, 200)
+ login = self.client.post('/test_admin/admin/', self.joepublic_login)
+ self.assertEqual(login.status_code, 200)
+ self.assertContains(login, ERROR_MESSAGE)
+
+ # Requests without username should not return 500 errors.
+ response = self.client.get('/test_admin/admin/')
+ self.assertEqual(response.status_code, 200)
+ login = self.client.post('/test_admin/admin/', self.no_username_login)
+ self.assertEqual(login.status_code, 200)
+ form = login.context[0].get('form')
+ self.assertEqual(form.errors['username'][0], 'This field is required.')
+
+ def testLoginSuccessfullyRedirectsToOriginalUrl(self):
+ response = self.client.get('/test_admin/admin/')
+ self.assertEqual(response.status_code, 200)
+ query_string = 'the-answer=42'
+ redirect_url = '/test_admin/admin/?%s' % query_string
+ new_next = {REDIRECT_FIELD_NAME: redirect_url}
+ login = self.client.post('/test_admin/admin/', dict(self.super_login, **new_next), QUERY_STRING=query_string)
+ self.assertRedirects(login, redirect_url)
+
+ def testAddView(self):
+ """Test add view restricts access and actually adds items."""
+
+ add_dict = {'title': 'Døm ikke',
+ 'content': '<p>great article</p>',
+ 'date_0': '2008-03-18', 'date_1': '10:54:39',
+ 'section': 1}
+
+ # Change User should not have access to add articles
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.changeuser_login)
+ # make sure the view removes test cookie
+ self.assertEqual(self.client.session.test_cookie_worked(), False)
+ response = self.client.get('/test_admin/admin/admin_views/article/add/')
+ self.assertEqual(response.status_code, 403)
+ # Try POST just to make sure
+ post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict)
+ self.assertEqual(post.status_code, 403)
+ self.assertEqual(Article.objects.all().count(), 3)
+ self.client.get('/test_admin/admin/logout/')
+
+ # Add user may login and POST to add view, then redirect to admin root
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.adduser_login)
+ addpage = self.client.get('/test_admin/admin/admin_views/article/add/')
+ change_list_link = '&rsaquo; <a href="/test_admin/admin/admin_views/article/">Articles</a>'
+ self.assertNotContains(addpage, change_list_link,
+ msg_prefix='User restricted to add permission is given link to change list view in breadcrumbs.')
+ post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict)
+ self.assertRedirects(post, '/test_admin/admin/')
+ self.assertEqual(Article.objects.all().count(), 4)
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(mail.outbox[0].subject, 'Greetings from a created object')
+ self.client.get('/test_admin/admin/logout/')
+
+ # Super can add too, but is redirected to the change list view
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.super_login)
+ addpage = self.client.get('/test_admin/admin/admin_views/article/add/')
+ self.assertContains(addpage, change_list_link,
+ msg_prefix='Unrestricted user is not given link to change list view in breadcrumbs.')
+ post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict)
+ self.assertRedirects(post, '/test_admin/admin/admin_views/article/')
+ self.assertEqual(Article.objects.all().count(), 5)
+ self.client.get('/test_admin/admin/logout/')
+
+ # 8509 - if a normal user is already logged in, it is possible
+ # to change user into the superuser without error
+ login = self.client.login(username='joepublic', password='secret')
+ # Check and make sure that if user expires, data still persists
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.super_login)
+ # make sure the view removes test cookie
+ self.assertEqual(self.client.session.test_cookie_worked(), False)
+
+ def testChangeView(self):
+ """Change view should restrict access and allow users to edit items."""
+
+ change_dict = {'title': 'Ikke fordømt',
+ 'content': '<p>edited article</p>',
+ 'date_0': '2008-03-18', 'date_1': '10:54:39',
+ 'section': 1}
+
+ # add user shoud not be able to view the list of article or change any of them
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.adduser_login)
+ response = self.client.get('/test_admin/admin/admin_views/article/')
+ self.assertEqual(response.status_code, 403)
+ response = self.client.get('/test_admin/admin/admin_views/article/1/')
+ self.assertEqual(response.status_code, 403)
+ post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
+ self.assertEqual(post.status_code, 403)
+ self.client.get('/test_admin/admin/logout/')
+
+ # change user can view all items and edit them
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.changeuser_login)
+ response = self.client.get('/test_admin/admin/admin_views/article/')
+ self.assertEqual(response.status_code, 200)
+ response = self.client.get('/test_admin/admin/admin_views/article/1/')
+ self.assertEqual(response.status_code, 200)
+ post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
+ self.assertRedirects(post, '/test_admin/admin/admin_views/article/')
+ self.assertEqual(Article.objects.get(pk=1).content, '<p>edited article</p>')
+
+ # one error in form should produce singular error message, multiple errors plural
+ change_dict['title'] = ''
+ post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
+ self.assertContains(post, 'Please correct the error below.',
+ msg_prefix='Singular error message not found in response to post with one error')
+
+ change_dict['content'] = ''
+ post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
+ self.assertContains(post, 'Please correct the errors below.',
+ msg_prefix='Plural error message not found in response to post with multiple errors')
+ self.client.get('/test_admin/admin/logout/')
+
+ # Test redirection when using row-level change permissions. Refs #11513.
+ RowLevelChangePermissionModel.objects.create(id=1, name="odd id")
+ RowLevelChangePermissionModel.objects.create(id=2, name="even id")
+ for login_dict in [self.super_login, self.changeuser_login, self.adduser_login, self.deleteuser_login]:
+ self.client.post('/test_admin/admin/', login_dict)
+ response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/')
+ self.assertEqual(response.status_code, 403)
+ response = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/', {'name': 'changed'})
+ self.assertEqual(RowLevelChangePermissionModel.objects.get(id=1).name, 'odd id')
+ self.assertEqual(response.status_code, 403)
+ response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/')
+ self.assertEqual(response.status_code, 200)
+ response = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/', {'name': 'changed'})
+ self.assertEqual(RowLevelChangePermissionModel.objects.get(id=2).name, 'changed')
+ self.assertRedirects(response, '/test_admin/admin/')
+ self.client.get('/test_admin/admin/logout/')
+ for login_dict in [self.joepublic_login, self.no_username_login]:
+ self.client.post('/test_admin/admin/', login_dict)
+ response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'login-form')
+ response = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/', {'name': 'changed'})
+ self.assertEqual(RowLevelChangePermissionModel.objects.get(id=1).name, 'odd id')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'login-form')
+ response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'login-form')
+ response = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/', {'name': 'changed again'})
+ self.assertEqual(RowLevelChangePermissionModel.objects.get(id=2).name, 'changed')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'login-form')
+ self.client.get('/test_admin/admin/logout/')
+
+ def testHistoryView(self):
+ """History view should restrict access."""
+
+ # add user shoud not be able to view the list of article or change any of them
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.adduser_login)
+ response = self.client.get('/test_admin/admin/admin_views/article/1/history/')
+ self.assertEqual(response.status_code, 403)
+ self.client.get('/test_admin/admin/logout/')
+
+ # change user can view all items and edit them
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.changeuser_login)
+ response = self.client.get('/test_admin/admin/admin_views/article/1/history/')
+ self.assertEqual(response.status_code, 200)
+
+ # Test redirection when using row-level change permissions. Refs #11513.
+ RowLevelChangePermissionModel.objects.create(id=1, name="odd id")
+ RowLevelChangePermissionModel.objects.create(id=2, name="even id")
+ for login_dict in [self.super_login, self.changeuser_login, self.adduser_login, self.deleteuser_login]:
+ self.client.post('/test_admin/admin/', login_dict)
+ response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/history/')
+ self.assertEqual(response.status_code, 403)
+
+ response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/history/')
+ self.assertEqual(response.status_code, 200)
+
+ self.client.get('/test_admin/admin/logout/')
+
+ for login_dict in [self.joepublic_login, self.no_username_login]:
+ self.client.post('/test_admin/admin/', login_dict)
+ response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/history/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'login-form')
+ response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/history/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'login-form')
+
+ self.client.get('/test_admin/admin/logout/')
+
+ def testConditionallyShowAddSectionLink(self):
+ """
+ The foreign key widget should only show the "add related" button if the
+ user has permission to add that related item.
+ """
+ # Set up and log in user.
+ url = '/test_admin/admin/admin_views/article/add/'
+ add_link_text = ' class="add-another"'
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.adduser_login)
+ # The add user can't add sections yet, so they shouldn't see the "add
+ # section" link.
+ response = self.client.get(url)
+ self.assertNotContains(response, add_link_text)
+ # Allow the add user to add sections too. Now they can see the "add
+ # section" link.
+ add_user = User.objects.get(username='adduser')
+ perm = get_perm(Section, Section._meta.get_add_permission())
+ add_user.user_permissions.add(perm)
+ response = self.client.get(url)
+ self.assertContains(response, add_link_text)
+
+ def testCustomModelAdminTemplates(self):
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.super_login)
+
+ # Test custom change list template with custom extra context
+ response = self.client.get('/test_admin/admin/admin_views/customarticle/')
+ self.assertContains(response, "var hello = 'Hello!';")
+ self.assertTemplateUsed(response, 'custom_admin/change_list.html')
+
+ # Test custom add form template
+ response = self.client.get('/test_admin/admin/admin_views/customarticle/add/')
+ self.assertTemplateUsed(response, 'custom_admin/add_form.html')
+
+ # Add an article so we can test delete, change, and history views
+ post = self.client.post('/test_admin/admin/admin_views/customarticle/add/', {
+ 'content': '<p>great article</p>',
+ 'date_0': '2008-03-18',
+ 'date_1': '10:54:39'
+ })
+ self.assertRedirects(post, '/test_admin/admin/admin_views/customarticle/')
+ self.assertEqual(CustomArticle.objects.all().count(), 1)
+ article_pk = CustomArticle.objects.all()[0].pk
+
+ # Test custom delete, change, and object history templates
+ # Test custom change form template
+ response = self.client.get('/test_admin/admin/admin_views/customarticle/%d/' % article_pk)
+ self.assertTemplateUsed(response, 'custom_admin/change_form.html')
+ response = self.client.get('/test_admin/admin/admin_views/customarticle/%d/delete/' % article_pk)
+ self.assertTemplateUsed(response, 'custom_admin/delete_confirmation.html')
+ response = self.client.post('/test_admin/admin/admin_views/customarticle/', data={
+ 'index': 0,
+ 'action': ['delete_selected'],
+ '_selected_action': ['1'],
+ })
+ self.assertTemplateUsed(response, 'custom_admin/delete_selected_confirmation.html')
+ response = self.client.get('/test_admin/admin/admin_views/customarticle/%d/history/' % article_pk)
+ self.assertTemplateUsed(response, 'custom_admin/object_history.html')
+
+ self.client.get('/test_admin/admin/logout/')
+
+ def testDeleteView(self):
+ """Delete view should restrict access and actually delete items."""
+
+ delete_dict = {'post': 'yes'}
+
+ # add user shoud not be able to delete articles
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.adduser_login)
+ response = self.client.get('/test_admin/admin/admin_views/article/1/delete/')
+ self.assertEqual(response.status_code, 403)
+ post = self.client.post('/test_admin/admin/admin_views/article/1/delete/', delete_dict)
+ self.assertEqual(post.status_code, 403)
+ self.assertEqual(Article.objects.all().count(), 3)
+ self.client.get('/test_admin/admin/logout/')
+
+ # Delete user can delete
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.deleteuser_login)
+ response = self.client.get('/test_admin/admin/admin_views/section/1/delete/')
+ # test response contains link to related Article
+ self.assertContains(response, "admin_views/article/1/")
+
+ response = self.client.get('/test_admin/admin/admin_views/article/1/delete/')
+ self.assertEqual(response.status_code, 200)
+ post = self.client.post('/test_admin/admin/admin_views/article/1/delete/', delete_dict)
+ self.assertRedirects(post, '/test_admin/admin/')
+ self.assertEqual(Article.objects.all().count(), 2)
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(mail.outbox[0].subject, 'Greetings from a deleted object')
+ article_ct = ContentType.objects.get_for_model(Article)
+ logged = LogEntry.objects.get(content_type=article_ct, action_flag=DELETION)
+ self.assertEqual(logged.object_id, '1')
+ self.client.get('/test_admin/admin/logout/')
+
+ def testDisabledPermissionsWhenLoggedIn(self):
+ self.client.login(username='super', password='secret')
+ superuser = User.objects.get(username='super')
+ superuser.is_active = False
+ superuser.save()
+
+ response = self.client.get('/test_admin/admin/')
+ self.assertContains(response, 'id="login-form"')
+ self.assertNotContains(response, 'Log out')
+
+ response = self.client.get('/test_admin/admin/secure-view/')
+ self.assertContains(response, 'id="login-form"')
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminViewsNoUrlTest(TestCase):
+ """Regression test for #17333"""
+
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ opts = Report._meta
+ # User who can change Reports
+ change_user = User.objects.get(username='changeuser')
+ change_user.user_permissions.add(get_perm(Report,
+ opts.get_change_permission()))
+
+ # login POST dict
+ self.changeuser_login = {
+ REDIRECT_FIELD_NAME: '/test_admin/admin/',
+ LOGIN_FORM_KEY: 1,
+ 'username': 'changeuser',
+ 'password': 'secret',
+ }
+
+ def test_no_standard_modeladmin_urls(self):
+ """Admin index views don't break when user's ModelAdmin removes standard urls"""
+ self.client.get('/test_admin/admin/')
+ self.client.post('/test_admin/admin/', self.changeuser_login)
+ r = self.client.get('/test_admin/admin/')
+ # we shouldn' get an 500 error caused by a NoReverseMatch
+ self.assertEqual(r.status_code, 200)
+ self.client.get('/test_admin/admin/logout/')
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminViewDeletedObjectsTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml', 'deleted-objects.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_nesting(self):
+ """
+ Objects should be nested to display the relationships that
+ cause them to be scheduled for deletion.
+ """
+ pattern = re.compile(br"""<li>Plot: <a href=".+/admin_views/plot/1/">World Domination</a>\s*<ul>\s*<li>Plot details: <a href=".+/admin_views/plotdetails/1/">almost finished</a>""")
+ response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
+ six.assertRegex(self, response.content, pattern)
+
+ def test_cyclic(self):
+ """
+ Cyclic relationships should still cause each object to only be
+ listed once.
+
+ """
+ one = """<li>Cyclic one: <a href="/test_admin/admin/admin_views/cyclicone/1/">I am recursive</a>"""
+ two = """<li>Cyclic two: <a href="/test_admin/admin/admin_views/cyclictwo/1/">I am recursive too</a>"""
+ response = self.client.get('/test_admin/admin/admin_views/cyclicone/%s/delete/' % quote(1))
+
+ self.assertContains(response, one, 1)
+ self.assertContains(response, two, 1)
+
+ def test_perms_needed(self):
+ self.client.logout()
+ delete_user = User.objects.get(username='deleteuser')
+ delete_user.user_permissions.add(get_perm(Plot,
+ Plot._meta.get_delete_permission()))
+
+ self.assertTrue(self.client.login(username='deleteuser',
+ password='secret'))
+
+ response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(1))
+ self.assertContains(response, "your account doesn't have permission to delete the following types of objects")
+ self.assertContains(response, "<li>plot details</li>")
+
+ def test_protected(self):
+ q = Question.objects.create(question="Why?")
+ a1 = Answer.objects.create(question=q, answer="Because.")
+ a2 = Answer.objects.create(question=q, answer="Yes.")
+
+ response = self.client.get("/test_admin/admin/admin_views/question/%s/delete/" % quote(q.pk))
+ self.assertContains(response, "would require deleting the following protected related objects")
+ self.assertContains(response, '<li>Answer: <a href="/test_admin/admin/admin_views/answer/%s/">Because.</a></li>' % a1.pk)
+ self.assertContains(response, '<li>Answer: <a href="/test_admin/admin/admin_views/answer/%s/">Yes.</a></li>' % a2.pk)
+
+ def test_not_registered(self):
+ should_contain = """<li>Secret hideout: underground bunker"""
+ response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
+ self.assertContains(response, should_contain, 1)
+
+ def test_multiple_fkeys_to_same_model(self):
+ """
+ If a deleted object has two relationships from another model,
+ both of those should be followed in looking for related
+ objects to delete.
+
+ """
+ should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/1/">World Domination</a>"""
+ response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
+ self.assertContains(response, should_contain)
+ response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2))
+ self.assertContains(response, should_contain)
+
+ def test_multiple_fkeys_to_same_instance(self):
+ """
+ If a deleted object has two relationships pointing to it from
+ another object, the other object should still only be listed
+ once.
+
+ """
+ should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/2/">World Peace</a></li>"""
+ response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2))
+ self.assertContains(response, should_contain, 1)
+
+ def test_inheritance(self):
+ """
+ In the case of an inherited model, if either the child or
+ parent-model instance is deleted, both instances are listed
+ for deletion, as well as any relationships they have.
+
+ """
+ should_contain = [
+ """<li>Villain: <a href="/test_admin/admin/admin_views/villain/3/">Bob</a>""",
+ """<li>Super villain: <a href="/test_admin/admin/admin_views/supervillain/3/">Bob</a>""",
+ """<li>Secret hideout: floating castle""",
+ """<li>Super secret hideout: super floating castle!"""
+ ]
+ response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(3))
+ for should in should_contain:
+ self.assertContains(response, should, 1)
+ response = self.client.get('/test_admin/admin/admin_views/supervillain/%s/delete/' % quote(3))
+ for should in should_contain:
+ self.assertContains(response, should, 1)
+
+ def test_generic_relations(self):
+ """
+ If a deleted object has GenericForeignKeys pointing to it,
+ those objects should be listed for deletion.
+
+ """
+ plot = Plot.objects.get(pk=3)
+ tag = FunkyTag.objects.create(content_object=plot, name='hott')
+ should_contain = """<li>Funky tag: hott"""
+ response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(3))
+ self.assertContains(response, should_contain)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminViewStringPrimaryKeyTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml', 'string-primary-key.xml']
+
+ def __init__(self, *args):
+ super(AdminViewStringPrimaryKeyTest, self).__init__(*args)
+ self.pk = """abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 -_.!~*'() ;/?:@&=+$, <>#%" {}|\^[]`"""
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+ content_type_pk = ContentType.objects.get_for_model(ModelWithStringPrimaryKey).pk
+ LogEntry.objects.log_action(100, content_type_pk, self.pk, self.pk, 2, change_message='Changed something')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_get_history_view(self):
+ """
+ Retrieving the history for an object using urlencoded form of primary
+ key should work.
+ Refs #12349, #18550.
+ """
+ response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/history/' % quote(self.pk))
+ self.assertContains(response, escape(self.pk))
+ self.assertContains(response, 'Changed something')
+ self.assertEqual(response.status_code, 200)
+
+ def test_get_change_view(self):
+ "Retrieving the object using urlencoded form of primary key should work"
+ response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(self.pk))
+ self.assertContains(response, escape(self.pk))
+ self.assertEqual(response.status_code, 200)
+
+ def test_changelist_to_changeform_link(self):
+ "Link to the changeform of the object in changelist should use reverse() and be quoted -- #18072"
+ prefix = '/test_admin/admin/admin_views/modelwithstringprimarykey/'
+ response = self.client.get(prefix)
+ # this URL now comes through reverse(), thus iri_to_uri encoding
+ pk_final_url = escape(iri_to_uri(quote(self.pk)))
+ should_contain = """<th><a href="%s%s/">%s</a></th>""" % (prefix, pk_final_url, escape(self.pk))
+ self.assertContains(response, should_contain)
+
+ def test_recentactions_link(self):
+ "The link from the recent actions list referring to the changeform of the object should be quoted"
+ response = self.client.get('/test_admin/admin/')
+ should_contain = """<a href="admin_views/modelwithstringprimarykey/%s/">%s</a>""" % (escape(quote(self.pk)), escape(self.pk))
+ self.assertContains(response, should_contain)
+
+ def test_recentactions_without_content_type(self):
+ "If a LogEntry is missing content_type it will not display it in span tag under the hyperlink."
+ response = self.client.get('/test_admin/admin/')
+ should_contain = """<a href="admin_views/modelwithstringprimarykey/%s/">%s</a>""" % (escape(quote(self.pk)), escape(self.pk))
+ self.assertContains(response, should_contain)
+ should_contain = "Model with string primary key" # capitalized in Recent Actions
+ self.assertContains(response, should_contain)
+ logentry = LogEntry.objects.get(content_type__name__iexact=should_contain)
+ # http://code.djangoproject.com/ticket/10275
+ # if the log entry doesn't have a content type it should still be
+ # possible to view the Recent Actions part
+ logentry.content_type = None
+ logentry.save()
+
+ counted_presence_before = response.content.count(force_bytes(should_contain))
+ response = self.client.get('/test_admin/admin/')
+ counted_presence_after = response.content.count(force_bytes(should_contain))
+ self.assertEqual(counted_presence_before - 1,
+ counted_presence_after)
+
+ def test_deleteconfirmation_link(self):
+ "The link from the delete confirmation page referring back to the changeform of the object should be quoted"
+ response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/delete/' % quote(self.pk))
+ # this URL now comes through reverse(), thus iri_to_uri encoding
+ should_contain = """/%s/">%s</a>""" % (escape(iri_to_uri(quote(self.pk))), escape(self.pk))
+ self.assertContains(response, should_contain)
+
+ def test_url_conflicts_with_add(self):
+ "A model with a primary key that ends with add should be visible"
+ add_model = ModelWithStringPrimaryKey(pk="i have something to add")
+ add_model.save()
+ 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>"""
+ self.assertContains(response, should_contain)
+
+ def test_url_conflicts_with_delete(self):
+ "A model with a primary key that ends with delete should be visible"
+ delete_model = ModelWithStringPrimaryKey(pk="delete")
+ delete_model.save()
+ 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>"""
+ self.assertContains(response, should_contain)
+
+ def test_url_conflicts_with_history(self):
+ "A model with a primary key that ends with history should be visible"
+ history_model = ModelWithStringPrimaryKey(pk="history")
+ history_model.save()
+ 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>"""
+ self.assertContains(response, should_contain)
+
+ def test_shortcut_view_with_escaping(self):
+ "'View on site should' work properly with char fields"
+ model = ModelWithStringPrimaryKey(pk='abc_123')
+ model.save()
+ response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(model.pk))
+ should_contain = '/%s/" class="viewsitelink">' % model.pk
+ self.assertContains(response, should_contain)
+
+ def test_change_view_history_link(self):
+ """Object history button link should work and contain the pk value quoted."""
+ url = reverse('admin:%s_modelwithstringprimarykey_change' %
+ ModelWithStringPrimaryKey._meta.app_label,
+ args=(quote(self.pk),))
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ expected_link = reverse('admin:%s_modelwithstringprimarykey_history' %
+ ModelWithStringPrimaryKey._meta.app_label,
+ args=(quote(self.pk),))
+ self.assertContains(response, '<a href="%s" class="historylink"' % expected_link)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class SecureViewTests(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ # login POST dicts
+ self.super_login = {
+ LOGIN_FORM_KEY: 1,
+ REDIRECT_FIELD_NAME: '/test_admin/admin/secure-view/',
+ 'username': 'super',
+ 'password': 'secret',
+ }
+ self.super_email_login = {
+ LOGIN_FORM_KEY: 1,
+ REDIRECT_FIELD_NAME: '/test_admin/admin/secure-view/',
+ 'username': 'super@example.com',
+ 'password': 'secret',
+ }
+ self.super_email_bad_login = {
+ LOGIN_FORM_KEY: 1,
+ REDIRECT_FIELD_NAME: '/test_admin/admin/secure-view/',
+ 'username': 'super@example.com',
+ 'password': 'notsecret',
+ }
+ self.adduser_login = {
+ LOGIN_FORM_KEY: 1,
+ REDIRECT_FIELD_NAME: '/test_admin/admin/secure-view/',
+ 'username': 'adduser',
+ 'password': 'secret',
+ }
+ self.changeuser_login = {
+ LOGIN_FORM_KEY: 1,
+ REDIRECT_FIELD_NAME: '/test_admin/admin/secure-view/',
+ 'username': 'changeuser',
+ 'password': 'secret',
+ }
+ self.deleteuser_login = {
+ LOGIN_FORM_KEY: 1,
+ REDIRECT_FIELD_NAME: '/test_admin/admin/secure-view/',
+ 'username': 'deleteuser',
+ 'password': 'secret',
+ }
+ self.joepublic_login = {
+ LOGIN_FORM_KEY: 1,
+ REDIRECT_FIELD_NAME: '/test_admin/admin/secure-view/',
+ 'username': 'joepublic',
+ 'password': 'secret',
+ }
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_secure_view_shows_login_if_not_logged_in(self):
+ "Ensure that we see the login form"
+ response = self.client.get('/test_admin/admin/secure-view/')
+ self.assertTemplateUsed(response, 'admin/login.html')
+
+ def test_secure_view_login_successfully_redirects_to_original_url(self):
+ response = self.client.get('/test_admin/admin/secure-view/')
+ self.assertEqual(response.status_code, 200)
+ query_string = 'the-answer=42'
+ redirect_url = '/test_admin/admin/secure-view/?%s' % query_string
+ new_next = {REDIRECT_FIELD_NAME: redirect_url}
+ login = self.client.post('/test_admin/admin/secure-view/', dict(self.super_login, **new_next), QUERY_STRING=query_string)
+ self.assertRedirects(login, redirect_url)
+
+ def test_staff_member_required_decorator_works_as_per_admin_login(self):
+ """
+ Make sure only staff members can log in.
+
+ Successful posts to the login page will redirect to the orignal url.
+ Unsuccessfull attempts will continue to render the login page with
+ a 200 status code.
+ """
+ # Super User
+ response = self.client.get('/test_admin/admin/secure-view/')
+ self.assertEqual(response.status_code, 200)
+ login = self.client.post('/test_admin/admin/secure-view/', self.super_login)
+ self.assertRedirects(login, '/test_admin/admin/secure-view/')
+ self.assertFalse(login.context)
+ self.client.get('/test_admin/admin/logout/')
+ # make sure the view removes test cookie
+ self.assertEqual(self.client.session.test_cookie_worked(), False)
+
+ # Test if user enters email address
+ response = self.client.get('/test_admin/admin/secure-view/')
+ self.assertEqual(response.status_code, 200)
+ login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login)
+ self.assertContains(login, ERROR_MESSAGE)
+ # only correct passwords get a username hint
+ login = self.client.post('/test_admin/admin/secure-view/', self.super_email_bad_login)
+ self.assertContains(login, ERROR_MESSAGE)
+ new_user = User(username='jondoe', password='secret', email='super@example.com')
+ new_user.save()
+ # check to ensure if there are multiple email addresses a user doesn't get a 500
+ login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login)
+ self.assertContains(login, ERROR_MESSAGE)
+
+ # Add User
+ response = self.client.get('/test_admin/admin/secure-view/')
+ self.assertEqual(response.status_code, 200)
+ login = self.client.post('/test_admin/admin/secure-view/', self.adduser_login)
+ self.assertRedirects(login, '/test_admin/admin/secure-view/')
+ self.assertFalse(login.context)
+ self.client.get('/test_admin/admin/logout/')
+
+ # Change User
+ response = self.client.get('/test_admin/admin/secure-view/')
+ self.assertEqual(response.status_code, 200)
+ login = self.client.post('/test_admin/admin/secure-view/', self.changeuser_login)
+ self.assertRedirects(login, '/test_admin/admin/secure-view/')
+ self.assertFalse(login.context)
+ self.client.get('/test_admin/admin/logout/')
+
+ # Delete User
+ response = self.client.get('/test_admin/admin/secure-view/')
+ self.assertEqual(response.status_code, 200)
+ login = self.client.post('/test_admin/admin/secure-view/', self.deleteuser_login)
+ self.assertRedirects(login, '/test_admin/admin/secure-view/')
+ self.assertFalse(login.context)
+ self.client.get('/test_admin/admin/logout/')
+
+ # Regular User should not be able to login.
+ response = self.client.get('/test_admin/admin/secure-view/')
+ self.assertEqual(response.status_code, 200)
+ login = self.client.post('/test_admin/admin/secure-view/', self.joepublic_login)
+ self.assertEqual(login.status_code, 200)
+ # Login.context is a list of context dicts we just need to check the first one.
+ self.assertContains(login, ERROR_MESSAGE)
+
+ # 8509 - if a normal user is already logged in, it is possible
+ # to change user into the superuser without error
+ login = self.client.login(username='joepublic', password='secret')
+ # Check and make sure that if user expires, data still persists
+ self.client.get('/test_admin/admin/secure-view/')
+ self.client.post('/test_admin/admin/secure-view/', self.super_login)
+ # make sure the view removes test cookie
+ self.assertEqual(self.client.session.test_cookie_worked(), False)
+
+ def test_shortcut_view_only_available_to_staff(self):
+ """
+ Only admin users should be able to use the admin shortcut view.
+ """
+ user_ctype = ContentType.objects.get_for_model(User)
+ user = User.objects.get(username='super')
+ shortcut_url = "/test_admin/admin/r/%s/%s/" % (user_ctype.pk, user.pk)
+
+ # Not logged in: we should see the login page.
+ response = self.client.get(shortcut_url, follow=False)
+ self.assertTemplateUsed(response, 'admin/login.html')
+
+ # Logged in? Redirect.
+ self.client.login(username='super', password='secret')
+ response = self.client.get(shortcut_url, follow=False)
+ # Can't use self.assertRedirects() because User.get_absolute_url() is silly.
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, 'http://example.com/users/super/')
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminViewUnicodeTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-unicode.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def testUnicodeEdit(self):
+ """
+ A test to ensure that POST on edit_view handles non-ascii characters.
+ """
+ post_data = {
+ "name": "Test lærdommer",
+ # inline data
+ "chapter_set-TOTAL_FORMS": "6",
+ "chapter_set-INITIAL_FORMS": "3",
+ "chapter_set-MAX_NUM_FORMS": "0",
+ "chapter_set-0-id": "1",
+ "chapter_set-0-title": "Norske bostaver æøå skaper problemer",
+ "chapter_set-0-content": "&lt;p&gt;Svært frustrerende med UnicodeDecodeError&lt;/p&gt;",
+ "chapter_set-1-id": "2",
+ "chapter_set-1-title": "Kjærlighet.",
+ "chapter_set-1-content": "&lt;p&gt;La kjærligheten til de lidende seire.&lt;/p&gt;",
+ "chapter_set-2-id": "3",
+ "chapter_set-2-title": "Need a title.",
+ "chapter_set-2-content": "&lt;p&gt;Newest content&lt;/p&gt;",
+ "chapter_set-3-id": "",
+ "chapter_set-3-title": "",
+ "chapter_set-3-content": "",
+ "chapter_set-4-id": "",
+ "chapter_set-4-title": "",
+ "chapter_set-4-content": "",
+ "chapter_set-5-id": "",
+ "chapter_set-5-title": "",
+ "chapter_set-5-content": "",
+ }
+
+ response = self.client.post('/test_admin/admin/admin_views/book/1/', post_data)
+ self.assertEqual(response.status_code, 302) # redirect somewhere
+
+ def testUnicodeDelete(self):
+ """
+ Ensure that the delete_view handles non-ascii characters
+ """
+ delete_dict = {'post': 'yes'}
+ response = self.client.get('/test_admin/admin/admin_views/book/1/delete/')
+ self.assertEqual(response.status_code, 200)
+ response = self.client.post('/test_admin/admin/admin_views/book/1/delete/', delete_dict)
+ self.assertRedirects(response, '/test_admin/admin/admin_views/book/')
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminViewListEditable(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml', 'admin-views-person.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_inheritance(self):
+ Podcast.objects.create(name="This Week in Django",
+ release_date=datetime.date.today())
+ response = self.client.get('/test_admin/admin/admin_views/podcast/')
+ self.assertEqual(response.status_code, 200)
+
+ def test_inheritance_2(self):
+ Vodcast.objects.create(name="This Week in Django", released=True)
+ response = self.client.get('/test_admin/admin/admin_views/vodcast/')
+ self.assertEqual(response.status_code, 200)
+
+ def test_custom_pk(self):
+ Language.objects.create(iso='en', name='English', english_name='English')
+ response = self.client.get('/test_admin/admin/admin_views/language/')
+ self.assertEqual(response.status_code, 200)
+
+ def test_changelist_input_html(self):
+ response = self.client.get('/test_admin/admin/admin_views/person/')
+ # 2 inputs per object(the field and the hidden id field) = 6
+ # 3 management hidden fields = 3
+ # 4 action inputs (3 regular checkboxes, 1 checkbox to select all)
+ # main form submit button = 1
+ # search field and search submit button = 2
+ # CSRF field = 1
+ # field to track 'select all' across paginated views = 1
+ # 6 + 3 + 4 + 1 + 2 + 1 + 1 = 18 inputs
+ self.assertContains(response, "<input", count=18)
+ # 1 select per object = 3 selects
+ self.assertContains(response, "<select", count=4)
+
+ def test_post_messages(self):
+ # Ticket 12707: Saving inline editable should not show admin
+ # action warnings
+ data = {
+ "form-TOTAL_FORMS": "3",
+ "form-INITIAL_FORMS": "3",
+ "form-MAX_NUM_FORMS": "0",
+
+ "form-0-gender": "1",
+ "form-0-id": "1",
+
+ "form-1-gender": "2",
+ "form-1-id": "2",
+
+ "form-2-alive": "checked",
+ "form-2-gender": "1",
+ "form-2-id": "3",
+
+ "_save": "Save",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/person/',
+ data, follow=True)
+ self.assertEqual(len(response.context['messages']), 1)
+
+ def test_post_submission(self):
+ data = {
+ "form-TOTAL_FORMS": "3",
+ "form-INITIAL_FORMS": "3",
+ "form-MAX_NUM_FORMS": "0",
+
+ "form-0-gender": "1",
+ "form-0-id": "1",
+
+ "form-1-gender": "2",
+ "form-1-id": "2",
+
+ "form-2-alive": "checked",
+ "form-2-gender": "1",
+ "form-2-id": "3",
+
+ "_save": "Save",
+ }
+ self.client.post('/test_admin/admin/admin_views/person/', data)
+
+ self.assertEqual(Person.objects.get(name="John Mauchly").alive, False)
+ self.assertEqual(Person.objects.get(name="Grace Hopper").gender, 2)
+
+ # test a filtered page
+ data = {
+ "form-TOTAL_FORMS": "2",
+ "form-INITIAL_FORMS": "2",
+ "form-MAX_NUM_FORMS": "0",
+
+ "form-0-id": "1",
+ "form-0-gender": "1",
+ "form-0-alive": "checked",
+
+ "form-1-id": "3",
+ "form-1-gender": "1",
+ "form-1-alive": "checked",
+
+ "_save": "Save",
+ }
+ self.client.post('/test_admin/admin/admin_views/person/?gender__exact=1', data)
+
+ self.assertEqual(Person.objects.get(name="John Mauchly").alive, True)
+
+ # test a searched page
+ data = {
+ "form-TOTAL_FORMS": "1",
+ "form-INITIAL_FORMS": "1",
+ "form-MAX_NUM_FORMS": "0",
+
+ "form-0-id": "1",
+ "form-0-gender": "1",
+
+ "_save": "Save",
+ }
+ self.client.post('/test_admin/admin/admin_views/person/?q=john', data)
+
+ self.assertEqual(Person.objects.get(name="John Mauchly").alive, False)
+
+ def test_non_field_errors(self):
+ ''' Ensure that non field errors are displayed for each of the
+ forms in the changelist's formset. Refs #13126.
+ '''
+ fd1 = FoodDelivery.objects.create(reference='123', driver='bill', restaurant='thai')
+ fd2 = FoodDelivery.objects.create(reference='456', driver='bill', restaurant='india')
+ fd3 = FoodDelivery.objects.create(reference='789', driver='bill', restaurant='pizza')
+
+ data = {
+ "form-TOTAL_FORMS": "3",
+ "form-INITIAL_FORMS": "3",
+ "form-MAX_NUM_FORMS": "0",
+
+ "form-0-id": str(fd1.id),
+ "form-0-reference": "123",
+ "form-0-driver": "bill",
+ "form-0-restaurant": "thai",
+
+ # Same data as above: Forbidden because of unique_together!
+ "form-1-id": str(fd2.id),
+ "form-1-reference": "456",
+ "form-1-driver": "bill",
+ "form-1-restaurant": "thai",
+
+ "form-2-id": str(fd3.id),
+ "form-2-reference": "789",
+ "form-2-driver": "bill",
+ "form-2-restaurant": "pizza",
+
+ "_save": "Save",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/fooddelivery/', data)
+ self.assertContains(response, '<tr><td colspan="4"><ul class="errorlist"><li>Food delivery with this Driver and Restaurant already exists.</li></ul></td></tr>', 1, html=True)
+
+ data = {
+ "form-TOTAL_FORMS": "3",
+ "form-INITIAL_FORMS": "3",
+ "form-MAX_NUM_FORMS": "0",
+
+ "form-0-id": str(fd1.id),
+ "form-0-reference": "123",
+ "form-0-driver": "bill",
+ "form-0-restaurant": "thai",
+
+ # Same data as above: Forbidden because of unique_together!
+ "form-1-id": str(fd2.id),
+ "form-1-reference": "456",
+ "form-1-driver": "bill",
+ "form-1-restaurant": "thai",
+
+ # Same data also.
+ "form-2-id": str(fd3.id),
+ "form-2-reference": "789",
+ "form-2-driver": "bill",
+ "form-2-restaurant": "thai",
+
+ "_save": "Save",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/fooddelivery/', data)
+ self.assertContains(response, '<tr><td colspan="4"><ul class="errorlist"><li>Food delivery with this Driver and Restaurant already exists.</li></ul></td></tr>', 2, html=True)
+
+ def test_non_form_errors(self):
+ # test if non-form errors are handled; ticket #12716
+ data = {
+ "form-TOTAL_FORMS": "1",
+ "form-INITIAL_FORMS": "1",
+ "form-MAX_NUM_FORMS": "0",
+
+ "form-0-id": "2",
+ "form-0-alive": "1",
+ "form-0-gender": "2",
+
+ # Ensure that the form processing understands this as a list_editable "Save"
+ # and not an action "Go".
+ "_save": "Save",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/person/', data)
+ self.assertContains(response, "Grace is not a Zombie")
+
+ def test_non_form_errors_is_errorlist(self):
+ # test if non-form errors are correctly handled; ticket #12878
+ data = {
+ "form-TOTAL_FORMS": "1",
+ "form-INITIAL_FORMS": "1",
+ "form-MAX_NUM_FORMS": "0",
+
+ "form-0-id": "2",
+ "form-0-alive": "1",
+ "form-0-gender": "2",
+
+ "_save": "Save",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/person/', data)
+ non_form_errors = response.context['cl'].formset.non_form_errors()
+ self.assertTrue(isinstance(non_form_errors, ErrorList))
+ self.assertEqual(str(non_form_errors), str(ErrorList(["Grace is not a Zombie"])))
+
+ def test_list_editable_ordering(self):
+ collector = Collector.objects.create(id=1, name="Frederick Clegg")
+
+ Category.objects.create(id=1, order=1, collector=collector)
+ Category.objects.create(id=2, order=2, collector=collector)
+ Category.objects.create(id=3, order=0, collector=collector)
+ Category.objects.create(id=4, order=0, collector=collector)
+
+ # NB: The order values must be changed so that the items are reordered.
+ data = {
+ "form-TOTAL_FORMS": "4",
+ "form-INITIAL_FORMS": "4",
+ "form-MAX_NUM_FORMS": "0",
+
+ "form-0-order": "14",
+ "form-0-id": "1",
+ "form-0-collector": "1",
+
+ "form-1-order": "13",
+ "form-1-id": "2",
+ "form-1-collector": "1",
+
+ "form-2-order": "1",
+ "form-2-id": "3",
+ "form-2-collector": "1",
+
+ "form-3-order": "0",
+ "form-3-id": "4",
+ "form-3-collector": "1",
+
+ # Ensure that the form processing understands this as a list_editable "Save"
+ # and not an action "Go".
+ "_save": "Save",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/category/', data)
+ # Successful post will redirect
+ self.assertEqual(response.status_code, 302)
+
+ # Check that the order values have been applied to the right objects
+ self.assertEqual(Category.objects.get(id=1).order, 14)
+ self.assertEqual(Category.objects.get(id=2).order, 13)
+ self.assertEqual(Category.objects.get(id=3).order, 1)
+ self.assertEqual(Category.objects.get(id=4).order, 0)
+
+ def test_list_editable_pagination(self):
+ """
+ Ensure that pagination works for list_editable items.
+ Refs #16819.
+ """
+ UnorderedObject.objects.create(id=1, name='Unordered object #1')
+ UnorderedObject.objects.create(id=2, name='Unordered object #2')
+ UnorderedObject.objects.create(id=3, name='Unordered object #3')
+ response = self.client.get('/test_admin/admin/admin_views/unorderedobject/')
+ self.assertContains(response, 'Unordered object #3')
+ self.assertContains(response, 'Unordered object #2')
+ self.assertNotContains(response, 'Unordered object #1')
+ response = self.client.get('/test_admin/admin/admin_views/unorderedobject/?p=1')
+ self.assertNotContains(response, 'Unordered object #3')
+ self.assertNotContains(response, 'Unordered object #2')
+ self.assertContains(response, 'Unordered object #1')
+
+ def test_list_editable_action_submit(self):
+ # List editable changes should not be executed if the action "Go" button is
+ # used to submit the form.
+ data = {
+ "form-TOTAL_FORMS": "3",
+ "form-INITIAL_FORMS": "3",
+ "form-MAX_NUM_FORMS": "0",
+
+ "form-0-gender": "1",
+ "form-0-id": "1",
+
+ "form-1-gender": "2",
+ "form-1-id": "2",
+
+ "form-2-alive": "checked",
+ "form-2-gender": "1",
+ "form-2-id": "3",
+
+ "index": "0",
+ "_selected_action": ['3'],
+ "action": ['', 'delete_selected'],
+ }
+ self.client.post('/test_admin/admin/admin_views/person/', data)
+
+ self.assertEqual(Person.objects.get(name="John Mauchly").alive, True)
+ self.assertEqual(Person.objects.get(name="Grace Hopper").gender, 1)
+
+ def test_list_editable_action_choices(self):
+ # List editable changes should be executed if the "Save" button is
+ # used to submit the form - any action choices should be ignored.
+ data = {
+ "form-TOTAL_FORMS": "3",
+ "form-INITIAL_FORMS": "3",
+ "form-MAX_NUM_FORMS": "0",
+
+ "form-0-gender": "1",
+ "form-0-id": "1",
+
+ "form-1-gender": "2",
+ "form-1-id": "2",
+
+ "form-2-alive": "checked",
+ "form-2-gender": "1",
+ "form-2-id": "3",
+
+ "_save": "Save",
+ "_selected_action": ['1'],
+ "action": ['', 'delete_selected'],
+ }
+ self.client.post('/test_admin/admin/admin_views/person/', data)
+
+ self.assertEqual(Person.objects.get(name="John Mauchly").alive, False)
+ self.assertEqual(Person.objects.get(name="Grace Hopper").gender, 2)
+
+ def test_list_editable_popup(self):
+ """
+ Fields should not be list-editable in popups.
+ """
+ response = self.client.get('/test_admin/admin/admin_views/person/')
+ self.assertNotEqual(response.context['cl'].list_editable, ())
+ response = self.client.get('/test_admin/admin/admin_views/person/?%s' % IS_POPUP_VAR)
+ self.assertEqual(response.context['cl'].list_editable, ())
+
+ def test_pk_hidden_fields(self):
+ """ Ensure that hidden pk fields aren't displayed in the table body and
+ that their corresponding human-readable value is displayed instead.
+ Note that the hidden pk fields are in fact be displayed but
+ separately (not in the table), and only once.
+ Refs #12475.
+ """
+ story1 = Story.objects.create(title='The adventures of Guido', content='Once upon a time in Djangoland...')
+ story2 = Story.objects.create(title='Crouching Tiger, Hidden Python', content='The Python was sneaking into...')
+ response = self.client.get('/test_admin/admin/admin_views/story/')
+ self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table.
+ self.assertContains(response, 'id="id_form-1-id"', 1)
+ self.assertContains(response, '<div class="hiddenfields">\n<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id" /><input type="hidden" name="form-1-id" value="%d" id="id_form-1-id" />\n</div>' % (story2.id, story1.id), html=True)
+ self.assertContains(response, '<td>%d</td>' % story1.id, 1)
+ self.assertContains(response, '<td>%d</td>' % story2.id, 1)
+
+ def test_pk_hidden_fields_with_list_display_links(self):
+ """ Similarly as test_pk_hidden_fields, but when the hidden pk fields are
+ referenced in list_display_links.
+ Refs #12475.
+ """
+ story1 = OtherStory.objects.create(title='The adventures of Guido', content='Once upon a time in Djangoland...')
+ story2 = OtherStory.objects.create(title='Crouching Tiger, Hidden Python', content='The Python was sneaking into...')
+ link1 = reverse('admin:admin_views_otherstory_change', args=(story1.pk,))
+ link2 = reverse('admin:admin_views_otherstory_change', args=(story2.pk,))
+ response = self.client.get('/test_admin/admin/admin_views/otherstory/')
+ self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table.
+ self.assertContains(response, 'id="id_form-1-id"', 1)
+ self.assertContains(response, '<div class="hiddenfields">\n<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id" /><input type="hidden" name="form-1-id" value="%d" id="id_form-1-id" />\n</div>' % (story2.id, story1.id), html=True)
+ self.assertContains(response, '<th><a href="%s">%d</a></th>' % (link1, story1.id), 1)
+ self.assertContains(response, '<th><a href="%s">%d</a></th>' % (link2, story2.id), 1)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminSearchTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users', 'multiple-child-classes',
+ 'admin-views-person']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_search_on_sibling_models(self):
+ "Check that a search that mentions sibling models"
+ response = self.client.get('/test_admin/admin/admin_views/recommendation/?q=bar')
+ # confirm the search returned 1 object
+ self.assertContains(response, "\n1 recommendation\n")
+
+ def test_with_fk_to_field(self):
+ """Ensure that the to_field GET parameter is preserved when a search
+ is performed. Refs #10918.
+ """
+ from django.contrib.admin.views.main import TO_FIELD_VAR
+ response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=username' % TO_FIELD_VAR)
+ self.assertContains(response, "\n1 user\n")
+ self.assertContains(response, '<input type="hidden" name="t" value="username"/>', html=True)
+
+ def test_exact_matches(self):
+ response = self.client.get('/test_admin/admin/admin_views/recommendation/?q=bar')
+ # confirm the search returned one object
+ self.assertContains(response, "\n1 recommendation\n")
+
+ response = self.client.get('/test_admin/admin/admin_views/recommendation/?q=ba')
+ # confirm the search returned zero objects
+ self.assertContains(response, "\n0 recommendations\n")
+
+ def test_beginning_matches(self):
+ response = self.client.get('/test_admin/admin/admin_views/person/?q=Gui')
+ # confirm the search returned one object
+ self.assertContains(response, "\n1 person\n")
+ self.assertContains(response, "Guido")
+
+ response = self.client.get('/test_admin/admin/admin_views/person/?q=uido')
+ # confirm the search returned zero objects
+ self.assertContains(response, "\n0 persons\n")
+ self.assertNotContains(response, "Guido")
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminInheritedInlinesTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ 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 = "foo username"
+ bar_user = "bar username"
+
+ name_re = re.compile(b'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.assertEqual(len(names), len(set(names)))
+
+ # test the add case
+ post_data = {
+ "name": "Test Name",
+ # inline data
+ "accounts-TOTAL_FORMS": "1",
+ "accounts-INITIAL_FORMS": "0",
+ "accounts-MAX_NUM_FORMS": "0",
+ "accounts-0-username": foo_user,
+ "accounts-2-TOTAL_FORMS": "1",
+ "accounts-2-INITIAL_FORMS": "0",
+ "accounts-2-MAX_NUM_FORMS": "0",
+ "accounts-2-0-username": bar_user,
+ }
+
+ response = self.client.post('/test_admin/admin/admin_views/persona/add/', post_data)
+ self.assertEqual(response.status_code, 302) # redirect somewhere
+ self.assertEqual(Persona.objects.count(), 1)
+ self.assertEqual(FooAccount.objects.count(), 1)
+ self.assertEqual(BarAccount.objects.count(), 1)
+ self.assertEqual(FooAccount.objects.all()[0].username, foo_user)
+ self.assertEqual(BarAccount.objects.all()[0].username, bar_user)
+ self.assertEqual(Persona.objects.all()[0].accounts.count(), 2)
+
+ persona_id = Persona.objects.all()[0].id
+ foo_id = FooAccount.objects.all()[0].id
+ bar_id = BarAccount.objects.all()[0].id
+
+ # test the edit case
+
+ response = self.client.get('/test_admin/admin/admin_views/persona/%d/' % persona_id)
+ names = name_re.findall(response.content)
+ # make sure we have no duplicate HTML names
+ self.assertEqual(len(names), len(set(names)))
+
+ post_data = {
+ "name": "Test Name",
+
+ "accounts-TOTAL_FORMS": "2",
+ "accounts-INITIAL_FORMS": "1",
+ "accounts-MAX_NUM_FORMS": "0",
+
+ "accounts-0-username": "%s-1" % foo_user,
+ "accounts-0-account_ptr": str(foo_id),
+ "accounts-0-persona": str(persona_id),
+
+ "accounts-2-TOTAL_FORMS": "2",
+ "accounts-2-INITIAL_FORMS": "1",
+ "accounts-2-MAX_NUM_FORMS": "0",
+
+ "accounts-2-0-username": "%s-1" % bar_user,
+ "accounts-2-0-account_ptr": str(bar_id),
+ "accounts-2-0-persona": str(persona_id),
+ }
+ response = self.client.post('/test_admin/admin/admin_views/persona/%d/' % persona_id, post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(Persona.objects.count(), 1)
+ self.assertEqual(FooAccount.objects.count(), 1)
+ self.assertEqual(BarAccount.objects.count(), 1)
+ self.assertEqual(FooAccount.objects.all()[0].username, "%s-1" % foo_user)
+ self.assertEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user)
+ self.assertEqual(Persona.objects.all()[0].accounts.count(), 2)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminActionsTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml', 'admin-views-actions.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_model_admin_custom_action(self):
+ "Tests a custom action defined in a ModelAdmin method"
+ action_data = {
+ ACTION_CHECKBOX_NAME: [1],
+ 'action': 'mail_admin',
+ 'index': 0,
+ }
+ response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data)
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(mail.outbox[0].subject, 'Greetings from a ModelAdmin action')
+
+ def test_model_admin_default_delete_action(self):
+ "Tests the default delete action defined as a ModelAdmin method"
+ action_data = {
+ ACTION_CHECKBOX_NAME: [1, 2],
+ 'action': 'delete_selected',
+ 'index': 0,
+ }
+ delete_confirmation_data = {
+ ACTION_CHECKBOX_NAME: [1, 2],
+ 'action': 'delete_selected',
+ 'post': 'yes',
+ }
+ confirmation = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data)
+ self.assertIsInstance(confirmation, TemplateResponse)
+ self.assertContains(confirmation, "Are you sure you want to delete the selected subscribers?")
+ self.assertContains(confirmation, ACTION_CHECKBOX_NAME, count=2)
+ response = self.client.post('/test_admin/admin/admin_views/subscriber/', delete_confirmation_data)
+ self.assertEqual(Subscriber.objects.count(), 0)
+
+ def test_non_localized_pk(self):
+ """If USE_THOUSAND_SEPARATOR is set, make sure that the ids for
+ the objects selected for deletion are rendered without separators.
+ Refs #14895.
+ """
+ self.old_USE_THOUSAND_SEPARATOR = settings.USE_THOUSAND_SEPARATOR
+ self.old_USE_L10N = settings.USE_L10N
+ settings.USE_THOUSAND_SEPARATOR = True
+ settings.USE_L10N = True
+ subscriber = Subscriber.objects.get(id=1)
+ subscriber.id = 9999
+ subscriber.save()
+ action_data = {
+ ACTION_CHECKBOX_NAME: [9999, 2],
+ 'action': 'delete_selected',
+ 'index': 0,
+ }
+ response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data)
+ self.assertTemplateUsed(response, 'admin/delete_selected_confirmation.html')
+ self.assertContains(response, 'value="9999"') # Instead of 9,999
+ self.assertContains(response, 'value="2"')
+ settings.USE_THOUSAND_SEPARATOR = self.old_USE_THOUSAND_SEPARATOR
+ settings.USE_L10N = self.old_USE_L10N
+
+ def test_model_admin_default_delete_action_protected(self):
+ """
+ Tests the default delete action defined as a ModelAdmin method in the
+ case where some related objects are protected from deletion.
+ """
+ q1 = Question.objects.create(question="Why?")
+ a1 = Answer.objects.create(question=q1, answer="Because.")
+ a2 = Answer.objects.create(question=q1, answer="Yes.")
+ q2 = Question.objects.create(question="Wherefore?")
+
+ action_data = {
+ ACTION_CHECKBOX_NAME: [q1.pk, q2.pk],
+ 'action': 'delete_selected',
+ 'index': 0,
+ }
+
+ response = self.client.post("/test_admin/admin/admin_views/question/", action_data)
+
+ self.assertContains(response, "would require deleting the following protected related objects")
+ self.assertContains(response, '<li>Answer: <a href="/test_admin/admin/admin_views/answer/%s/">Because.</a></li>' % a1.pk, html=True)
+ self.assertContains(response, '<li>Answer: <a href="/test_admin/admin/admin_views/answer/%s/">Yes.</a></li>' % a2.pk, html=True)
+
+ def test_custom_function_mail_action(self):
+ "Tests a custom action defined in a function"
+ action_data = {
+ ACTION_CHECKBOX_NAME: [1],
+ 'action': 'external_mail',
+ 'index': 0,
+ }
+ response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data)
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(mail.outbox[0].subject, 'Greetings from a function action')
+
+ def test_custom_function_action_with_redirect(self):
+ "Tests a custom action defined in a function"
+ action_data = {
+ ACTION_CHECKBOX_NAME: [1],
+ 'action': 'redirect_to',
+ 'index': 0,
+ }
+ response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data)
+ self.assertEqual(response.status_code, 302)
+
+ def test_default_redirect(self):
+ """
+ Test that actions which don't return an HttpResponse are redirected to
+ the same page, retaining the querystring (which may contain changelist
+ information).
+ """
+ action_data = {
+ ACTION_CHECKBOX_NAME: [1],
+ 'action': 'external_mail',
+ 'index': 0,
+ }
+ url = '/test_admin/admin/admin_views/externalsubscriber/?o=1'
+ response = self.client.post(url, action_data)
+ self.assertRedirects(response, url)
+
+ def test_actions_ordering(self):
+ """
+ Ensure that actions are ordered as expected.
+ Refs #15964.
+ """
+ response = self.client.get('/test_admin/admin/admin_views/externalsubscriber/')
+ self.assertContains(response, '''<label>Action: <select name="action">
+<option value="" selected="selected">---------</option>
+<option value="delete_selected">Delete selected external subscribers</option>
+<option value="redirect_to">Redirect to (Awesome action)</option>
+<option value="external_mail">External mail (Another awesome action)</option>
+</select>''', html=True)
+
+ def test_model_without_action(self):
+ "Tests a ModelAdmin without any action"
+ response = self.client.get('/test_admin/admin/admin_views/oldsubscriber/')
+ self.assertEqual(response.context["action_form"], None)
+ self.assertNotContains(response, '<input type="checkbox" class="action-select"',
+ msg_prefix="Found an unexpected action toggle checkboxbox in response")
+ self.assertNotContains(response, '<input type="checkbox" class="action-select"')
+
+ def test_model_without_action_still_has_jquery(self):
+ "Tests that a ModelAdmin without any actions still gets jQuery included in page"
+ response = self.client.get('/test_admin/admin/admin_views/oldsubscriber/')
+ self.assertEqual(response.context["action_form"], None)
+ self.assertContains(response, 'jquery.min.js',
+ msg_prefix="jQuery missing from admin pages for model with no admin actions"
+ )
+
+ def test_action_column_class(self):
+ "Tests that the checkbox column class is present in the response"
+ response = self.client.get('/test_admin/admin/admin_views/subscriber/')
+ self.assertNotEqual(response.context["action_form"], None)
+ self.assertContains(response, 'action-checkbox-column')
+
+ def test_multiple_actions_form(self):
+ """
+ Test that actions come from the form whose submit button was pressed (#10618).
+ """
+ action_data = {
+ ACTION_CHECKBOX_NAME: [1],
+ # Two different actions selected on the two forms...
+ 'action': ['external_mail', 'delete_selected'],
+ # ...but we clicked "go" on the top form.
+ 'index': 0
+ }
+ response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data)
+
+ # Send mail, don't delete.
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(mail.outbox[0].subject, 'Greetings from a function action')
+
+ def test_user_message_on_none_selected(self):
+ """
+ User should see a warning when 'Go' is pressed and no items are selected.
+ """
+ action_data = {
+ ACTION_CHECKBOX_NAME: [],
+ 'action': 'delete_selected',
+ 'index': 0,
+ }
+ response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data)
+ msg = """Items must be selected in order to perform actions on them. No items have been changed."""
+ self.assertContains(response, msg)
+ self.assertEqual(Subscriber.objects.count(), 2)
+
+ def test_user_message_on_no_action(self):
+ """
+ User should see a warning when 'Go' is pressed and no action is selected.
+ """
+ action_data = {
+ ACTION_CHECKBOX_NAME: [1, 2],
+ 'action': '',
+ 'index': 0,
+ }
+ response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data)
+ msg = """No action selected."""
+ self.assertContains(response, msg)
+ self.assertEqual(Subscriber.objects.count(), 2)
+
+ def test_selection_counter(self):
+ """
+ Check if the selection counter is there.
+ """
+ response = self.client.get('/test_admin/admin/admin_views/subscriber/')
+ self.assertContains(response, '0 of 2 selected')
+
+ def test_popup_actions(self):
+ """ Actions should not be shown in popups. """
+ response = self.client.get('/test_admin/admin/admin_views/subscriber/')
+ self.assertNotEqual(response.context["action_form"], None)
+ response = self.client.get(
+ '/test_admin/admin/admin_views/subscriber/?%s' % IS_POPUP_VAR)
+ self.assertEqual(response.context["action_form"], None)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class TestCustomChangeList(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+ urlbit = 'admin'
+
+ def setUp(self):
+ result = self.client.login(username='super', password='secret')
+ self.assertEqual(result, True)
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_custom_changelist(self):
+ """
+ Validate that a custom ChangeList class can be used (#9749)
+ """
+ # Insert some data
+ post_data = {"name": "First Gadget"}
+ response = self.client.post('/test_admin/%s/admin_views/gadget/add/' % self.urlbit, post_data)
+ self.assertEqual(response.status_code, 302) # redirect somewhere
+ # Hit the page once to get messages out of the queue message list
+ response = self.client.get('/test_admin/%s/admin_views/gadget/' % self.urlbit)
+ # Ensure that data is still not visible on the page
+ response = self.client.get('/test_admin/%s/admin_views/gadget/' % self.urlbit)
+ self.assertEqual(response.status_code, 200)
+ self.assertNotContains(response, 'First Gadget')
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class TestInlineNotEditable(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ result = self.client.login(username='super', password='secret')
+ self.assertEqual(result, True)
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test(self):
+ """
+ InlineModelAdmin broken?
+ """
+ response = self.client.get('/test_admin/admin/admin_views/parent/add/')
+ self.assertEqual(response.status_code, 200)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminCustomQuerysetTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+ self.pks = [EmptyModel.objects.create().id for i in range(3)]
+
+ def test_changelist_view(self):
+ response = self.client.get('/test_admin/admin/admin_views/emptymodel/')
+ for i in self.pks:
+ if i > 1:
+ self.assertContains(response, 'Primary key = %s' % i)
+ else:
+ self.assertNotContains(response, 'Primary key = %s' % i)
+
+ def test_changelist_view_count_queries(self):
+ #create 2 Person objects
+ Person.objects.create(name='person1', gender=1)
+ Person.objects.create(name='person2', gender=2)
+
+ # 4 queries are expected: 1 for the session, 1 for the user,
+ # 1 for the count and 1 for the objects on the page
+ with self.assertNumQueries(4):
+ resp = self.client.get('/test_admin/admin/admin_views/person/')
+ self.assertEqual(resp.context['selection_note'], '0 of 2 selected')
+ self.assertEqual(resp.context['selection_note_all'], 'All 2 selected')
+ with self.assertNumQueries(4):
+ extra = {'q': 'not_in_name'}
+ resp = self.client.get('/test_admin/admin/admin_views/person/', extra)
+ self.assertEqual(resp.context['selection_note'], '0 of 0 selected')
+ self.assertEqual(resp.context['selection_note_all'], 'All 0 selected')
+ with self.assertNumQueries(4):
+ extra = {'q': 'person'}
+ resp = self.client.get('/test_admin/admin/admin_views/person/', extra)
+ self.assertEqual(resp.context['selection_note'], '0 of 2 selected')
+ self.assertEqual(resp.context['selection_note_all'], 'All 2 selected')
+ # here one more count(*) query will run, because filters were applied
+ with self.assertNumQueries(5):
+ extra = {'gender__exact': '1'}
+ resp = self.client.get('/test_admin/admin/admin_views/person/', extra)
+ self.assertEqual(resp.context['selection_note'], '0 of 1 selected')
+ self.assertEqual(resp.context['selection_note_all'], '1 selected')
+
+ def test_change_view(self):
+ for i in self.pks:
+ response = self.client.get('/test_admin/admin/admin_views/emptymodel/%s/' % i)
+ if i > 1:
+ self.assertEqual(response.status_code, 200)
+ else:
+ self.assertEqual(response.status_code, 404)
+
+ def test_add_model_modeladmin_defer_qs(self):
+ # Test for #14529. defer() is used in ModelAdmin.queryset()
+
+ # model has __unicode__ method
+ self.assertEqual(CoverLetter.objects.count(), 0)
+ # Emulate model instance creation via the admin
+ post_data = {
+ "author": "Candidate, Best",
+ "_save": "Save",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/coverletter/add/',
+ post_data, follow=True)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(CoverLetter.objects.count(), 1)
+ # Message should contain non-ugly model verbose name
+ self.assertContains(
+ response,
+ '<li class="info">The cover letter &quot;Candidate, Best&quot; was added successfully.</li>',
+ html=True
+ )
+
+ # model has no __unicode__ method
+ self.assertEqual(ShortMessage.objects.count(), 0)
+ # Emulate model instance creation via the admin
+ post_data = {
+ "content": "What's this SMS thing?",
+ "_save": "Save",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/shortmessage/add/',
+ post_data, follow=True)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(ShortMessage.objects.count(), 1)
+ # Message should contain non-ugly model verbose name
+ self.assertContains(
+ response,
+ '<li class="info">The short message &quot;ShortMessage object&quot; was added successfully.</li>',
+ html=True
+ )
+
+ def test_add_model_modeladmin_only_qs(self):
+ # Test for #14529. only() is used in ModelAdmin.queryset()
+
+ # model has __unicode__ method
+ self.assertEqual(Telegram.objects.count(), 0)
+ # Emulate model instance creation via the admin
+ post_data = {
+ "title": "Urgent telegram",
+ "_save": "Save",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/telegram/add/',
+ post_data, follow=True)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(Telegram.objects.count(), 1)
+ # Message should contain non-ugly model verbose name
+ self.assertContains(
+ response,
+ '<li class="info">The telegram &quot;Urgent telegram&quot; was added successfully.</li>',
+ html=True
+ )
+
+ # model has no __unicode__ method
+ self.assertEqual(Paper.objects.count(), 0)
+ # Emulate model instance creation via the admin
+ post_data = {
+ "title": "My Modified Paper Title",
+ "_save": "Save",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/paper/add/',
+ post_data, follow=True)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(Paper.objects.count(), 1)
+ # Message should contain non-ugly model verbose name
+ self.assertContains(
+ response,
+ '<li class="info">The paper &quot;Paper object&quot; was added successfully.</li>',
+ html=True
+ )
+
+ def test_edit_model_modeladmin_defer_qs(self):
+ # Test for #14529. defer() is used in ModelAdmin.queryset()
+
+ # model has __unicode__ method
+ cl = CoverLetter.objects.create(author="John Doe")
+ self.assertEqual(CoverLetter.objects.count(), 1)
+ response = self.client.get('/test_admin/admin/admin_views/coverletter/%s/' % cl.pk)
+ self.assertEqual(response.status_code, 200)
+ # Emulate model instance edit via the admin
+ post_data = {
+ "author": "John Doe II",
+ "_save": "Save",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/coverletter/%s/' % cl.pk,
+ post_data, follow=True)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(CoverLetter.objects.count(), 1)
+ # Message should contain non-ugly model verbose name. Instance
+ # representation is set by model's __unicode__()
+ self.assertContains(
+ response,
+ '<li class="info">The cover letter &quot;John Doe II&quot; was changed successfully.</li>',
+ html=True
+ )
+
+ # model has no __unicode__ method
+ sm = ShortMessage.objects.create(content="This is expensive")
+ self.assertEqual(ShortMessage.objects.count(), 1)
+ response = self.client.get('/test_admin/admin/admin_views/shortmessage/%s/' % sm.pk)
+ self.assertEqual(response.status_code, 200)
+ # Emulate model instance edit via the admin
+ post_data = {
+ "content": "Too expensive",
+ "_save": "Save",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/shortmessage/%s/' % sm.pk,
+ post_data, follow=True)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(ShortMessage.objects.count(), 1)
+ # Message should contain non-ugly model verbose name. The ugly(!)
+ # instance representation is set by six.text_type()
+ self.assertContains(
+ response,
+ '<li class="info">The short message &quot;ShortMessage_Deferred_timestamp object&quot; was changed successfully.</li>',
+ html=True
+ )
+
+ def test_edit_model_modeladmin_only_qs(self):
+ # Test for #14529. only() is used in ModelAdmin.queryset()
+
+ # model has __unicode__ method
+ t = Telegram.objects.create(title="Frist Telegram")
+ self.assertEqual(Telegram.objects.count(), 1)
+ response = self.client.get('/test_admin/admin/admin_views/telegram/%s/' % t.pk)
+ self.assertEqual(response.status_code, 200)
+ # Emulate model instance edit via the admin
+ post_data = {
+ "title": "Telegram without typo",
+ "_save": "Save",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/telegram/%s/' % t.pk,
+ post_data, follow=True)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(Telegram.objects.count(), 1)
+ # Message should contain non-ugly model verbose name. The instance
+ # representation is set by model's __unicode__()
+ self.assertContains(
+ response,
+ '<li class="info">The telegram &quot;Telegram without typo&quot; was changed successfully.</li>',
+ html=True
+ )
+
+ # model has no __unicode__ method
+ p = Paper.objects.create(title="My Paper Title")
+ self.assertEqual(Paper.objects.count(), 1)
+ response = self.client.get('/test_admin/admin/admin_views/paper/%s/' % p.pk)
+ self.assertEqual(response.status_code, 200)
+ # Emulate model instance edit via the admin
+ post_data = {
+ "title": "My Modified Paper Title",
+ "_save": "Save",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/paper/%s/' % p.pk,
+ post_data, follow=True)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(Paper.objects.count(), 1)
+ # Message should contain non-ugly model verbose name. The ugly(!)
+ # instance representation is set by six.text_type()
+ self.assertContains(
+ response,
+ '<li class="info">The paper &quot;Paper_Deferred_author object&quot; was changed successfully.</li>',
+ html=True
+ )
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminInlineFileUploadTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml', 'admin-views-actions.xml']
+ urlbit = 'admin'
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ # Set up test Picture and Gallery.
+ # These must be set up here instead of in fixtures in order to allow Picture
+ # to use a NamedTemporaryFile.
+ tdir = tempfile.gettempdir()
+ file1 = tempfile.NamedTemporaryFile(suffix=".file1", dir=tdir)
+ file1.write(b'a' * (2 ** 21))
+ filename = file1.name
+ file1.close()
+ self.gallery = Gallery(name="Test Gallery")
+ self.gallery.save()
+ self.picture = Picture(name="Test Picture", image=filename, gallery=self.gallery)
+ self.picture.save()
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_inline_file_upload_edit_validation_error_post(self):
+ """
+ Test that inline file uploads correctly display prior data (#10002).
+ """
+ post_data = {
+ "name": "Test Gallery",
+ "pictures-TOTAL_FORMS": "2",
+ "pictures-INITIAL_FORMS": "1",
+ "pictures-MAX_NUM_FORMS": "0",
+ "pictures-0-id": six.text_type(self.picture.id),
+ "pictures-0-gallery": six.text_type(self.gallery.id),
+ "pictures-0-name": "Test Picture",
+ "pictures-0-image": "",
+ "pictures-1-id": "",
+ "pictures-1-gallery": str(self.gallery.id),
+ "pictures-1-name": "Test Picture 2",
+ "pictures-1-image": "",
+ }
+ response = self.client.post('/test_admin/%s/admin_views/gallery/%d/' % (self.urlbit, self.gallery.id), post_data)
+ self.assertTrue(response._container[0].find("Currently:") > -1)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminInlineTests(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.post_data = {
+ "name": "Test Name",
+
+ "widget_set-TOTAL_FORMS": "3",
+ "widget_set-INITIAL_FORMS": "0",
+ "widget_set-MAX_NUM_FORMS": "0",
+ "widget_set-0-id": "",
+ "widget_set-0-owner": "1",
+ "widget_set-0-name": "",
+ "widget_set-1-id": "",
+ "widget_set-1-owner": "1",
+ "widget_set-1-name": "",
+ "widget_set-2-id": "",
+ "widget_set-2-owner": "1",
+ "widget_set-2-name": "",
+
+ "doohickey_set-TOTAL_FORMS": "3",
+ "doohickey_set-INITIAL_FORMS": "0",
+ "doohickey_set-MAX_NUM_FORMS": "0",
+ "doohickey_set-0-owner": "1",
+ "doohickey_set-0-code": "",
+ "doohickey_set-0-name": "",
+ "doohickey_set-1-owner": "1",
+ "doohickey_set-1-code": "",
+ "doohickey_set-1-name": "",
+ "doohickey_set-2-owner": "1",
+ "doohickey_set-2-code": "",
+ "doohickey_set-2-name": "",
+
+ "grommet_set-TOTAL_FORMS": "3",
+ "grommet_set-INITIAL_FORMS": "0",
+ "grommet_set-MAX_NUM_FORMS": "0",
+ "grommet_set-0-code": "",
+ "grommet_set-0-owner": "1",
+ "grommet_set-0-name": "",
+ "grommet_set-1-code": "",
+ "grommet_set-1-owner": "1",
+ "grommet_set-1-name": "",
+ "grommet_set-2-code": "",
+ "grommet_set-2-owner": "1",
+ "grommet_set-2-name": "",
+
+ "whatsit_set-TOTAL_FORMS": "3",
+ "whatsit_set-INITIAL_FORMS": "0",
+ "whatsit_set-MAX_NUM_FORMS": "0",
+ "whatsit_set-0-owner": "1",
+ "whatsit_set-0-index": "",
+ "whatsit_set-0-name": "",
+ "whatsit_set-1-owner": "1",
+ "whatsit_set-1-index": "",
+ "whatsit_set-1-name": "",
+ "whatsit_set-2-owner": "1",
+ "whatsit_set-2-index": "",
+ "whatsit_set-2-name": "",
+
+ "fancydoodad_set-TOTAL_FORMS": "3",
+ "fancydoodad_set-INITIAL_FORMS": "0",
+ "fancydoodad_set-MAX_NUM_FORMS": "0",
+ "fancydoodad_set-0-doodad_ptr": "",
+ "fancydoodad_set-0-owner": "1",
+ "fancydoodad_set-0-name": "",
+ "fancydoodad_set-0-expensive": "on",
+ "fancydoodad_set-1-doodad_ptr": "",
+ "fancydoodad_set-1-owner": "1",
+ "fancydoodad_set-1-name": "",
+ "fancydoodad_set-1-expensive": "on",
+ "fancydoodad_set-2-doodad_ptr": "",
+ "fancydoodad_set-2-owner": "1",
+ "fancydoodad_set-2-name": "",
+ "fancydoodad_set-2-expensive": "on",
+
+ "category_set-TOTAL_FORMS": "3",
+ "category_set-INITIAL_FORMS": "0",
+ "category_set-MAX_NUM_FORMS": "0",
+ "category_set-0-order": "",
+ "category_set-0-id": "",
+ "category_set-0-collector": "1",
+ "category_set-1-order": "",
+ "category_set-1-id": "",
+ "category_set-1-collector": "1",
+ "category_set-2-order": "",
+ "category_set-2-id": "",
+ "category_set-2-collector": "1",
+ }
+
+ result = self.client.login(username='super', password='secret')
+ self.assertEqual(result, True)
+ self.collector = Collector(pk=1, name='John Fowles')
+ self.collector.save()
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_simple_inline(self):
+ "A simple model can be saved as inlines"
+ # First add a new inline
+ self.post_data['widget_set-0-name'] = "Widget 1"
+ collector_url = '/test_admin/admin/admin_views/collector/%d/' % self.collector.pk
+ response = self.client.post(collector_url, self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(Widget.objects.count(), 1)
+ self.assertEqual(Widget.objects.all()[0].name, "Widget 1")
+ widget_id = Widget.objects.all()[0].id
+
+ # Check that the PK link exists on the rendered form
+ response = self.client.get(collector_url)
+ self.assertContains(response, 'name="widget_set-0-id"')
+
+ # Now resave that inline
+ self.post_data['widget_set-INITIAL_FORMS'] = "1"
+ self.post_data['widget_set-0-id'] = str(widget_id)
+ self.post_data['widget_set-0-name'] = "Widget 1"
+ response = self.client.post(collector_url, self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(Widget.objects.count(), 1)
+ self.assertEqual(Widget.objects.all()[0].name, "Widget 1")
+
+ # Now modify that inline
+ self.post_data['widget_set-INITIAL_FORMS'] = "1"
+ self.post_data['widget_set-0-id'] = str(widget_id)
+ self.post_data['widget_set-0-name'] = "Widget 1 Updated"
+ response = self.client.post(collector_url, self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(Widget.objects.count(), 1)
+ self.assertEqual(Widget.objects.all()[0].name, "Widget 1 Updated")
+
+ def test_explicit_autofield_inline(self):
+ "A model with an explicit autofield primary key can be saved as inlines. Regression for #8093"
+ # First add a new inline
+ self.post_data['grommet_set-0-name'] = "Grommet 1"
+ collector_url = '/test_admin/admin/admin_views/collector/%d/' % self.collector.pk
+ response = self.client.post(collector_url, self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(Grommet.objects.count(), 1)
+ self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1")
+
+ # Check that the PK link exists on the rendered form
+ response = self.client.get(collector_url)
+ self.assertContains(response, 'name="grommet_set-0-code"')
+
+ # Now resave that inline
+ self.post_data['grommet_set-INITIAL_FORMS'] = "1"
+ self.post_data['grommet_set-0-code'] = str(Grommet.objects.all()[0].code)
+ self.post_data['grommet_set-0-name'] = "Grommet 1"
+ response = self.client.post(collector_url, self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(Grommet.objects.count(), 1)
+ self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1")
+
+ # Now modify that inline
+ self.post_data['grommet_set-INITIAL_FORMS'] = "1"
+ self.post_data['grommet_set-0-code'] = str(Grommet.objects.all()[0].code)
+ self.post_data['grommet_set-0-name'] = "Grommet 1 Updated"
+ response = self.client.post(collector_url, self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(Grommet.objects.count(), 1)
+ self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1 Updated")
+
+ def test_char_pk_inline(self):
+ "A model with a character PK can be saved as inlines. Regression for #10992"
+ # First add a new inline
+ self.post_data['doohickey_set-0-code'] = "DH1"
+ self.post_data['doohickey_set-0-name'] = "Doohickey 1"
+ collector_url = '/test_admin/admin/admin_views/collector/%d/' % self.collector.pk
+ response = self.client.post(collector_url, self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(DooHickey.objects.count(), 1)
+ self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1")
+
+ # Check that the PK link exists on the rendered form
+ response = self.client.get(collector_url)
+ self.assertContains(response, 'name="doohickey_set-0-code"')
+
+ # Now resave that inline
+ self.post_data['doohickey_set-INITIAL_FORMS'] = "1"
+ self.post_data['doohickey_set-0-code'] = "DH1"
+ self.post_data['doohickey_set-0-name'] = "Doohickey 1"
+ response = self.client.post(collector_url, self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(DooHickey.objects.count(), 1)
+ self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1")
+
+ # Now modify that inline
+ self.post_data['doohickey_set-INITIAL_FORMS'] = "1"
+ self.post_data['doohickey_set-0-code'] = "DH1"
+ self.post_data['doohickey_set-0-name'] = "Doohickey 1 Updated"
+ response = self.client.post(collector_url, self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(DooHickey.objects.count(), 1)
+ self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1 Updated")
+
+ def test_integer_pk_inline(self):
+ "A model with an integer PK can be saved as inlines. Regression for #10992"
+ # First add a new inline
+ self.post_data['whatsit_set-0-index'] = "42"
+ self.post_data['whatsit_set-0-name'] = "Whatsit 1"
+ response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(Whatsit.objects.count(), 1)
+ self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1")
+
+ # Check that the PK link exists on the rendered form
+ response = self.client.get('/test_admin/admin/admin_views/collector/1/')
+ self.assertContains(response, 'name="whatsit_set-0-index"')
+
+ # Now resave that inline
+ self.post_data['whatsit_set-INITIAL_FORMS'] = "1"
+ self.post_data['whatsit_set-0-index'] = "42"
+ self.post_data['whatsit_set-0-name'] = "Whatsit 1"
+ response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(Whatsit.objects.count(), 1)
+ self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1")
+
+ # Now modify that inline
+ self.post_data['whatsit_set-INITIAL_FORMS'] = "1"
+ self.post_data['whatsit_set-0-index'] = "42"
+ self.post_data['whatsit_set-0-name'] = "Whatsit 1 Updated"
+ response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(Whatsit.objects.count(), 1)
+ self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1 Updated")
+
+ def test_inherited_inline(self):
+ "An inherited model can be saved as inlines. Regression for #11042"
+ # First add a new inline
+ self.post_data['fancydoodad_set-0-name'] = "Fancy Doodad 1"
+ collector_url = '/test_admin/admin/admin_views/collector/%d/' % self.collector.pk
+ response = self.client.post(collector_url, self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(FancyDoodad.objects.count(), 1)
+ self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1")
+ doodad_pk = FancyDoodad.objects.all()[0].pk
+
+ # Check that the PK link exists on the rendered form
+ response = self.client.get(collector_url)
+ self.assertContains(response, 'name="fancydoodad_set-0-doodad_ptr"')
+
+ # Now resave that inline
+ self.post_data['fancydoodad_set-INITIAL_FORMS'] = "1"
+ self.post_data['fancydoodad_set-0-doodad_ptr'] = str(doodad_pk)
+ self.post_data['fancydoodad_set-0-name'] = "Fancy Doodad 1"
+ response = self.client.post(collector_url, self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(FancyDoodad.objects.count(), 1)
+ self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1")
+
+ # Now modify that inline
+ self.post_data['fancydoodad_set-INITIAL_FORMS'] = "1"
+ self.post_data['fancydoodad_set-0-doodad_ptr'] = str(doodad_pk)
+ self.post_data['fancydoodad_set-0-name'] = "Fancy Doodad 1 Updated"
+ response = self.client.post(collector_url, self.post_data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(FancyDoodad.objects.count(), 1)
+ self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1 Updated")
+
+ def test_ordered_inline(self):
+ """Check that an inline with an editable ordering fields is
+ updated correctly. Regression for #10922"""
+ # Create some objects with an initial ordering
+ Category.objects.create(id=1, order=1, collector=self.collector)
+ Category.objects.create(id=2, order=2, collector=self.collector)
+ Category.objects.create(id=3, order=0, collector=self.collector)
+ Category.objects.create(id=4, order=0, collector=self.collector)
+
+ # NB: The order values must be changed so that the items are reordered.
+ self.post_data.update({
+ "name": "Frederick Clegg",
+
+ "category_set-TOTAL_FORMS": "7",
+ "category_set-INITIAL_FORMS": "4",
+ "category_set-MAX_NUM_FORMS": "0",
+
+ "category_set-0-order": "14",
+ "category_set-0-id": "1",
+ "category_set-0-collector": "1",
+
+ "category_set-1-order": "13",
+ "category_set-1-id": "2",
+ "category_set-1-collector": "1",
+
+ "category_set-2-order": "1",
+ "category_set-2-id": "3",
+ "category_set-2-collector": "1",
+
+ "category_set-3-order": "0",
+ "category_set-3-id": "4",
+ "category_set-3-collector": "1",
+
+ "category_set-4-order": "",
+ "category_set-4-id": "",
+ "category_set-4-collector": "1",
+
+ "category_set-5-order": "",
+ "category_set-5-id": "",
+ "category_set-5-collector": "1",
+
+ "category_set-6-order": "",
+ "category_set-6-id": "",
+ "category_set-6-collector": "1",
+ })
+ response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data)
+ # Successful post will redirect
+ self.assertEqual(response.status_code, 302)
+
+ # Check that the order values have been applied to the right objects
+ self.assertEqual(self.collector.category_set.count(), 4)
+ self.assertEqual(Category.objects.get(id=1).order, 14)
+ self.assertEqual(Category.objects.get(id=2).order, 13)
+ self.assertEqual(Category.objects.get(id=3).order, 1)
+ self.assertEqual(Category.objects.get(id=4).order, 0)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class NeverCacheTests(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def testAdminIndex(self):
+ "Check the never-cache status of the main index"
+ response = self.client.get('/test_admin/admin/')
+ self.assertEqual(get_max_age(response), 0)
+
+ def testAppIndex(self):
+ "Check the never-cache status of an application index"
+ response = self.client.get('/test_admin/admin/admin_views/')
+ self.assertEqual(get_max_age(response), 0)
+
+ def testModelIndex(self):
+ "Check the never-cache status of a model index"
+ response = self.client.get('/test_admin/admin/admin_views/fabric/')
+ self.assertEqual(get_max_age(response), 0)
+
+ def testModelAdd(self):
+ "Check the never-cache status of a model add page"
+ response = self.client.get('/test_admin/admin/admin_views/fabric/add/')
+ self.assertEqual(get_max_age(response), 0)
+
+ def testModelView(self):
+ "Check the never-cache status of a model edit page"
+ response = self.client.get('/test_admin/admin/admin_views/section/1/')
+ self.assertEqual(get_max_age(response), 0)
+
+ def testModelHistory(self):
+ "Check the never-cache status of a model history page"
+ response = self.client.get('/test_admin/admin/admin_views/section/1/history/')
+ self.assertEqual(get_max_age(response), 0)
+
+ def testModelDelete(self):
+ "Check the never-cache status of a model delete page"
+ response = self.client.get('/test_admin/admin/admin_views/section/1/delete/')
+ self.assertEqual(get_max_age(response), 0)
+
+ def testLogin(self):
+ "Check the never-cache status of login views"
+ self.client.logout()
+ response = self.client.get('/test_admin/admin/')
+ self.assertEqual(get_max_age(response), 0)
+
+ def testLogout(self):
+ "Check the never-cache status of logout view"
+ response = self.client.get('/test_admin/admin/logout/')
+ self.assertEqual(get_max_age(response), 0)
+
+ def testPasswordChange(self):
+ "Check the never-cache status of the password change view"
+ self.client.logout()
+ response = self.client.get('/test_admin/password_change/')
+ self.assertEqual(get_max_age(response), None)
+
+ def testPasswordChangeDone(self):
+ "Check the never-cache status of the password change done view"
+ response = self.client.get('/test_admin/admin/password_change/done/')
+ self.assertEqual(get_max_age(response), None)
+
+ def testJsi18n(self):
+ "Check the never-cache status of the JavaScript i18n view"
+ response = self.client.get('/test_admin/admin/jsi18n/')
+ self.assertEqual(get_max_age(response), None)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class PrePopulatedTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_prepopulated_on(self):
+ response = self.client.get('/test_admin/admin/admin_views/prepopulatedpost/add/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "id: '#id_slug',")
+ self.assertContains(response, "field['dependency_ids'].push('#id_title');")
+ self.assertContains(response, "id: '#id_prepopulatedsubpost_set-0-subslug',")
+
+ def test_prepopulated_off(self):
+ response = self.client.get('/test_admin/admin/admin_views/prepopulatedpost/1/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "A Long Title")
+ self.assertNotContains(response, "id: '#id_slug'")
+ self.assertNotContains(response, "field['dependency_ids'].push('#id_title');")
+ self.assertNotContains(response, "id: '#id_prepopulatedsubpost_set-0-subslug',")
+
+ @override_settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=True)
+ def test_prepopulated_maxlength_localized(self):
+ """
+ Regression test for #15938: if USE_THOUSAND_SEPARATOR is set, make sure
+ that maxLength (in the JavaScript) is rendered without separators.
+ """
+ response = self.client.get('/test_admin/admin/admin_views/prepopulatedpostlargeslug/add/')
+ self.assertContains(response, "maxLength: 1000") # instead of 1,000
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class SeleniumAdminViewsFirefoxTests(AdminSeleniumWebDriverTestCase):
+ webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def test_prepopulated_fields(self):
+ """
+ Ensure that the JavaScript-automated prepopulated fields work with the
+ main form and with stacked and tabular inlines.
+ Refs #13068, #9264, #9983, #9784.
+ """
+ from selenium.common.exceptions import TimeoutException
+ self.admin_login(username='super', password='secret', login_url='/test_admin/admin/')
+ self.selenium.get('%s%s' % (self.live_server_url,
+ '/test_admin/admin/admin_views/mainprepopulated/add/'))
+
+ # Main form ----------------------------------------------------------
+ self.selenium.find_element_by_css_selector('#id_pubdate').send_keys('2012-02-18')
+ self.get_select_option('#id_status', 'option two').click()
+ self.selenium.find_element_by_css_selector('#id_name').send_keys(' this is the mAin nÀMë and it\'s awεšome')
+ slug1 = self.selenium.find_element_by_css_selector('#id_slug1').get_attribute('value')
+ slug2 = self.selenium.find_element_by_css_selector('#id_slug2').get_attribute('value')
+ self.assertEqual(slug1, 'main-name-and-its-awesome-2012-02-18')
+ self.assertEqual(slug2, 'option-two-main-name-and-its-awesome')
+
+ # Stacked inlines ----------------------------------------------------
+ # Initial inline
+ self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-0-pubdate').send_keys('2011-12-17')
+ self.get_select_option('#id_relatedprepopulated_set-0-status', 'option one').click()
+ self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-0-name').send_keys(' here is a sŤāÇkeð inline ! ')
+ slug1 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-0-slug1').get_attribute('value')
+ slug2 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-0-slug2').get_attribute('value')
+ self.assertEqual(slug1, 'here-stacked-inline-2011-12-17')
+ self.assertEqual(slug2, 'option-one-here-stacked-inline')
+
+ # Add an inline
+ self.selenium.find_elements_by_link_text('Add another Related Prepopulated')[0].click()
+ self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-pubdate').send_keys('1999-01-25')
+ self.get_select_option('#id_relatedprepopulated_set-1-status', 'option two').click()
+ self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-name').send_keys(' now you haVe anöther sŤāÇkeð inline with a very ... loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooog text... ')
+ slug1 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-slug1').get_attribute('value')
+ slug2 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-slug2').get_attribute('value')
+ self.assertEqual(slug1, 'now-you-have-another-stacked-inline-very-loooooooo') # 50 characters maximum for slug1 field
+ self.assertEqual(slug2, 'option-two-now-you-have-another-stacked-inline-very-looooooo') # 60 characters maximum for slug2 field
+
+ # Tabular inlines ----------------------------------------------------
+ # Initial inline
+ self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-2-0-pubdate').send_keys('1234-12-07')
+ self.get_select_option('#id_relatedprepopulated_set-2-0-status', 'option two').click()
+ self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-2-0-name').send_keys('And now, with a tÃbűlaŘ inline !!!')
+ slug1 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-2-0-slug1').get_attribute('value')
+ slug2 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-2-0-slug2').get_attribute('value')
+ self.assertEqual(slug1, 'and-now-tabular-inline-1234-12-07')
+ self.assertEqual(slug2, 'option-two-and-now-tabular-inline')
+
+ # Add an inline
+ self.selenium.find_elements_by_link_text('Add another Related Prepopulated')[1].click()
+ self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-2-1-pubdate').send_keys('1981-08-22')
+ self.get_select_option('#id_relatedprepopulated_set-2-1-status', 'option one').click()
+ self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-2-1-name').send_keys('a tÃbűlaŘ inline with ignored ;"&*^\%$#@-/`~ characters')
+ slug1 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-2-1-slug1').get_attribute('value')
+ slug2 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-2-1-slug2').get_attribute('value')
+ self.assertEqual(slug1, 'tabular-inline-ignored-characters-1981-08-22')
+ self.assertEqual(slug2, 'option-one-tabular-inline-ignored-characters')
+
+ # Save and check that everything is properly stored in the database
+ self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
+ self.wait_page_loaded()
+ self.assertEqual(MainPrepopulated.objects.all().count(), 1)
+ MainPrepopulated.objects.get(
+ name=' this is the mAin nÀMë and it\'s awεšome',
+ pubdate='2012-02-18',
+ status='option two',
+ slug1='main-name-and-its-awesome-2012-02-18',
+ slug2='option-two-main-name-and-its-awesome',
+ )
+ self.assertEqual(RelatedPrepopulated.objects.all().count(), 4)
+ RelatedPrepopulated.objects.get(
+ name=' here is a sŤāÇkeð inline ! ',
+ pubdate='2011-12-17',
+ status='option one',
+ slug1='here-stacked-inline-2011-12-17',
+ slug2='option-one-here-stacked-inline',
+ )
+ RelatedPrepopulated.objects.get(
+ name=' now you haVe anöther sŤāÇkeð inline with a very ... loooooooooooooooooo', # 75 characters in name field
+ pubdate='1999-01-25',
+ status='option two',
+ slug1='now-you-have-another-stacked-inline-very-loooooooo',
+ slug2='option-two-now-you-have-another-stacked-inline-very-looooooo',
+ )
+ RelatedPrepopulated.objects.get(
+ name='And now, with a tÃbűlaŘ inline !!!',
+ pubdate='1234-12-07',
+ status='option two',
+ slug1='and-now-tabular-inline-1234-12-07',
+ slug2='option-two-and-now-tabular-inline',
+ )
+ RelatedPrepopulated.objects.get(
+ name='a tÃbűlaŘ inline with ignored ;"&*^\%$#@-/`~ characters',
+ pubdate='1981-08-22',
+ status='option one',
+ slug1='tabular-inline-ignored-characters-1981-08-22',
+ slug2='option-one-tabular-inline-ignored-characters',
+ )
+
+ def test_collapsible_fieldset(self):
+ """
+ Test that the 'collapse' class in fieldsets definition allows to
+ show/hide the appropriate field section.
+ """
+ self.admin_login(username='super', password='secret', login_url='/test_admin/admin/')
+ self.selenium.get('%s%s' % (self.live_server_url,
+ '/test_admin/admin/admin_views/article/add/'))
+ self.assertFalse(self.selenium.find_element_by_id('id_title').is_displayed())
+ self.selenium.find_elements_by_link_text('Show')[0].click()
+ self.assertTrue(self.selenium.find_element_by_id('id_title').is_displayed())
+ self.assertEqual(
+ self.selenium.find_element_by_id('fieldsetcollapser0').text,
+ "Hide"
+ )
+
+
+class SeleniumAdminViewsChromeTests(SeleniumAdminViewsFirefoxTests):
+ webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver'
+
+
+class SeleniumAdminViewsIETests(SeleniumAdminViewsFirefoxTests):
+ webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver'
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class ReadonlyTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_readonly_get(self):
+ response = self.client.get('/test_admin/admin/admin_views/post/add/')
+ self.assertEqual(response.status_code, 200)
+ self.assertNotContains(response, 'name="posted"')
+ # 3 fields + 2 submit buttons + 4 inline management form fields, + 2
+ # hidden fields for inlines + 1 field for the inline + 2 empty form
+ self.assertContains(response, "<input", count=14)
+ self.assertContains(response, formats.localize(datetime.date.today()))
+ self.assertContains(response,
+ "<label>Awesomeness level:</label>")
+ self.assertContains(response, "Very awesome.")
+ self.assertContains(response, "Unkown coolness.")
+ self.assertContains(response, "foo")
+
+ # Checks that multiline text in a readonly field gets <br /> tags
+ self.assertContains(response, "Multiline<br />test<br />string")
+ self.assertContains(response, "InlineMultiline<br />test<br />string")
+
+ self.assertContains(response,
+ formats.localize(datetime.date.today() - datetime.timedelta(days=7))
+ )
+
+ self.assertContains(response, '<div class="form-row field-coolness">')
+ self.assertContains(response, '<div class="form-row field-awesomeness_level">')
+ self.assertContains(response, '<div class="form-row field-posted">')
+ self.assertContains(response, '<div class="form-row field-value">')
+ self.assertContains(response, '<div class="form-row">')
+ self.assertContains(response, '<p class="help">', 3)
+ self.assertContains(response, '<p class="help">Some help text for the title (with unicode ŠĐĆŽćžšđ)</p>', html=True)
+ self.assertContains(response, '<p class="help">Some help text for the content (with unicode ŠĐĆŽćžšđ)</p>', html=True)
+ self.assertContains(response, '<p class="help">Some help text for the date (with unicode ŠĐĆŽćžšđ)</p>', html=True)
+
+ p = Post.objects.create(title="I worked on readonly_fields", content="Its good stuff")
+ response = self.client.get('/test_admin/admin/admin_views/post/%d/' % p.pk)
+ self.assertContains(response, "%d amount of cool" % p.pk)
+
+ def test_readonly_post(self):
+ data = {
+ "title": "Django Got Readonly Fields",
+ "content": "This is an incredible development.",
+ "link_set-TOTAL_FORMS": "1",
+ "link_set-INITIAL_FORMS": "0",
+ "link_set-MAX_NUM_FORMS": "0",
+ }
+ response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(Post.objects.count(), 1)
+ p = Post.objects.get()
+ self.assertEqual(p.posted, datetime.date.today())
+
+ data["posted"] = "10-8-1990" # some date that's not today
+ response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(Post.objects.count(), 2)
+ p = Post.objects.order_by('-id')[0]
+ self.assertEqual(p.posted, datetime.date.today())
+
+ def test_readonly_manytomany(self):
+ "Regression test for #13004"
+ response = self.client.get('/test_admin/admin/admin_views/pizza/add/')
+ self.assertEqual(response.status_code, 200)
+
+ def test_user_password_change_limited_queryset(self):
+ su = User.objects.filter(is_superuser=True)[0]
+ response = self.client.get('/test_admin/admin2/auth/user/%s/password/' % su.pk)
+ self.assertEqual(response.status_code, 404)
+
+ def test_change_form_renders_correct_null_choice_value(self):
+ """
+ Regression test for #17911.
+ """
+ choice = Choice.objects.create(choice=None)
+ response = self.client.get('/test_admin/admin/admin_views/choice/%s/' % choice.pk)
+ self.assertContains(response, '<p>No opinion</p>', html=True)
+ self.assertNotContains(response, '<p>(None)</p>')
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class RawIdFieldsTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_limit_choices_to(self):
+ """Regression test for 14880"""
+ # This includes tests integers, strings and booleans in the lookup query string
+ actor = Actor.objects.create(name="Palin", age=27)
+ inquisition1 = Inquisition.objects.create(expected=True,
+ leader=actor,
+ country="England")
+ inquisition2 = Inquisition.objects.create(expected=False,
+ leader=actor,
+ country="Spain")
+ response = self.client.get('/test_admin/admin/admin_views/sketch/add/')
+ # Find the link
+ m = re.search(br'<a href="([^"]*)"[^>]* id="lookup_id_inquisition"', response.content)
+ self.assertTrue(m) # Got a match
+ popup_url = m.groups()[0].decode().replace("&amp;", "&")
+
+ # Handle relative links
+ popup_url = urljoin(response.request['PATH_INFO'], popup_url)
+ # Get the popup
+ response2 = self.client.get(popup_url)
+ self.assertContains(response2, "Spain")
+ self.assertNotContains(response2, "England")
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class UserAdminTest(TestCase):
+ """
+ Tests user CRUD functionality.
+ """
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_save_button(self):
+ user_count = User.objects.count()
+ response = self.client.post('/test_admin/admin/auth/user/add/', {
+ 'username': 'newuser',
+ 'password1': 'newpassword',
+ 'password2': 'newpassword',
+ })
+ new_user = User.objects.order_by('-id')[0]
+ self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk)
+ self.assertEqual(User.objects.count(), user_count + 1)
+ self.assertNotEqual(new_user.password, UNUSABLE_PASSWORD)
+
+ def test_save_continue_editing_button(self):
+ user_count = User.objects.count()
+ response = self.client.post('/test_admin/admin/auth/user/add/', {
+ 'username': 'newuser',
+ 'password1': 'newpassword',
+ 'password2': 'newpassword',
+ '_continue': '1',
+ })
+ new_user = User.objects.order_by('-id')[0]
+ self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk)
+ self.assertEqual(User.objects.count(), user_count + 1)
+ self.assertNotEqual(new_user.password, UNUSABLE_PASSWORD)
+
+ def test_password_mismatch(self):
+ response = self.client.post('/test_admin/admin/auth/user/add/', {
+ 'username': 'newuser',
+ 'password1': 'newpassword',
+ 'password2': 'mismatch',
+ })
+ self.assertEqual(response.status_code, 200)
+ adminform = response.context['adminform']
+ self.assertTrue('password' not in adminform.form.errors)
+ self.assertEqual(adminform.form.errors['password2'],
+ ["The two password fields didn't match."])
+
+ def test_user_fk_popup(self):
+ """Quick user addition in a FK popup shouldn't invoke view for further user customization"""
+ response = self.client.get('/test_admin/admin/admin_views/album/add/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, '/test_admin/admin/auth/user/add')
+ self.assertContains(response, 'class="add-another" id="add_id_owner" onclick="return showAddAnotherPopup(this);"')
+ response = self.client.get('/test_admin/admin/auth/user/add/?_popup=1')
+ self.assertEqual(response.status_code, 200)
+ self.assertNotContains(response, 'name="_continue"')
+ self.assertNotContains(response, 'name="_addanother"')
+ data = {
+ 'username': 'newuser',
+ 'password1': 'newpassword',
+ 'password2': 'newpassword',
+ '_popup': '1',
+ '_save': '1',
+ }
+ response = self.client.post('/test_admin/admin/auth/user/add/?_popup=1', data, follow=True)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'dismissAddAnotherPopup')
+
+ def test_save_add_another_button(self):
+ user_count = User.objects.count()
+ response = self.client.post('/test_admin/admin/auth/user/add/', {
+ 'username': 'newuser',
+ 'password1': 'newpassword',
+ 'password2': 'newpassword',
+ '_addanother': '1',
+ })
+ new_user = User.objects.order_by('-id')[0]
+ self.assertRedirects(response, '/test_admin/admin/auth/user/add/')
+ self.assertEqual(User.objects.count(), user_count + 1)
+ self.assertNotEqual(new_user.password, UNUSABLE_PASSWORD)
+
+ def test_user_permission_performance(self):
+ u = User.objects.all()[0]
+
+ # Don't depend on a warm cache, see #17377.
+ ContentType.objects.clear_cache()
+ with self.assertNumQueries(8):
+ response = self.client.get('/test_admin/admin/auth/user/%s/' % u.pk)
+ self.assertEqual(response.status_code, 200)
+
+ def test_form_url_present_in_context(self):
+ u = User.objects.all()[0]
+ response = self.client.get('/test_admin/admin3/auth/user/%s/password/' % u.pk)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['form_url'], 'pony')
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class GroupAdminTest(TestCase):
+ """
+ Tests group CRUD functionality.
+ """
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_save_button(self):
+ group_count = Group.objects.count()
+ response = self.client.post('/test_admin/admin/auth/group/add/', {
+ 'name': 'newgroup',
+ })
+
+ new_group = Group.objects.order_by('-id')[0]
+ self.assertRedirects(response, '/test_admin/admin/auth/group/')
+ self.assertEqual(Group.objects.count(), group_count + 1)
+
+ def test_group_permission_performance(self):
+ g = Group.objects.create(name="test_group")
+
+ with self.assertNumQueries(6): # instead of 259!
+ response = self.client.get('/test_admin/admin/auth/group/%s/' % g.pk)
+ self.assertEqual(response.status_code, 200)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class CSSTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_field_prefix_css_classes(self):
+ """
+ Ensure that fields have a CSS class name with a 'field-' prefix.
+ Refs #16371.
+ """
+ response = self.client.get('/test_admin/admin/admin_views/post/add/')
+
+ # The main form
+ self.assertContains(response, 'class="form-row field-title"')
+ self.assertContains(response, 'class="form-row field-content"')
+ self.assertContains(response, 'class="form-row field-public"')
+ self.assertContains(response, 'class="form-row field-awesomeness_level"')
+ self.assertContains(response, 'class="form-row field-coolness"')
+ self.assertContains(response, 'class="form-row field-value"')
+ self.assertContains(response, 'class="form-row"') # The lambda function
+
+ # The tabular inline
+ self.assertContains(response, '<td class="field-url">')
+ self.assertContains(response, '<td class="field-posted">')
+
+ def test_index_css_classes(self):
+ """
+ Ensure that CSS class names are used for each app and model on the
+ admin index pages.
+ Refs #17050.
+ """
+ # General index page
+ response = self.client.get("/test_admin/admin/")
+ self.assertContains(response, '<div class="app-admin_views module">')
+ self.assertContains(response, '<tr class="model-actor">')
+ self.assertContains(response, '<tr class="model-album">')
+
+ # App index page
+ response = self.client.get("/test_admin/admin/admin_views/")
+ self.assertContains(response, '<div class="app-admin_views module">')
+ self.assertContains(response, '<tr class="model-actor">')
+ self.assertContains(response, '<tr class="model-album">')
+
+try:
+ import docutils
+except ImportError:
+ docutils = None
+
+
+@unittest.skipUnless(docutils, "no docutils installed.")
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminDocsTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_tags(self):
+ response = self.client.get('/test_admin/admin/doc/tags/')
+
+ # The builtin tag group exists
+ self.assertContains(response, "<h2>Built-in tags</h2>", count=2, html=True)
+
+ # A builtin tag exists in both the index and detail
+ self.assertContains(response, '<h3 id="built_in-autoescape">autoescape</h3>', html=True)
+ self.assertContains(response, '<li><a href="#built_in-autoescape">autoescape</a></li>', html=True)
+
+ # An app tag exists in both the index and detail
+ self.assertContains(response, '<h3 id="flatpages-get_flatpages">get_flatpages</h3>', html=True)
+ self.assertContains(response, '<li><a href="#flatpages-get_flatpages">get_flatpages</a></li>', html=True)
+
+ # The admin list tag group exists
+ self.assertContains(response, "<h2>admin_list</h2>", count=2, html=True)
+
+ # An admin list tag exists in both the index and detail
+ self.assertContains(response, '<h3 id="admin_list-admin_actions">admin_actions</h3>', html=True)
+ self.assertContains(response, '<li><a href="#admin_list-admin_actions">admin_actions</a></li>', html=True)
+
+ def test_filters(self):
+ response = self.client.get('/test_admin/admin/doc/filters/')
+
+ # The builtin filter group exists
+ self.assertContains(response, "<h2>Built-in filters</h2>", count=2, html=True)
+
+ # A builtin filter exists in both the index and detail
+ self.assertContains(response, '<h3 id="built_in-add">add</h3>', html=True)
+ self.assertContains(response, '<li><a href="#built_in-add">add</a></li>', html=True)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class ValidXHTMLTests(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+ urlbit = 'admin'
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ @override_settings(
+ TEMPLATE_CONTEXT_PROCESSORS=filter(
+ lambda t: t != 'django.core.context_processors.i18n',
+ global_settings.TEMPLATE_CONTEXT_PROCESSORS),
+ USE_I18N=False,
+ )
+ def testLangNamePresent(self):
+ response = self.client.get('/test_admin/%s/admin_views/' % self.urlbit)
+ self.assertNotContains(response, ' lang=""')
+ self.assertNotContains(response, ' xml:lang=""')
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class DateHierarchyTests(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+ self.old_USE_THOUSAND_SEPARATOR = settings.USE_THOUSAND_SEPARATOR
+ self.old_USE_L10N = settings.USE_L10N
+ settings.USE_THOUSAND_SEPARATOR = True
+ settings.USE_L10N = True
+
+ def tearDown(self):
+ settings.USE_THOUSAND_SEPARATOR = self.old_USE_THOUSAND_SEPARATOR
+ settings.USE_L10N = self.old_USE_L10N
+ formats.reset_format_cache()
+
+ def assert_non_localized_year(self, response, year):
+ """Ensure that the year is not localized with
+ USE_THOUSAND_SEPARATOR. Refs #15234.
+ """
+ self.assertNotContains(response, formats.number_format(year))
+
+ def assert_contains_year_link(self, response, date):
+ self.assertContains(response, '?release_date__year=%d"' % (date.year,))
+
+ def assert_contains_month_link(self, response, date):
+ self.assertContains(
+ response, '?release_date__month=%d&amp;release_date__year=%d"' % (
+ date.month, date.year))
+
+ def assert_contains_day_link(self, response, date):
+ self.assertContains(
+ response, '?release_date__day=%d&amp;'
+ 'release_date__month=%d&amp;release_date__year=%d"' % (
+ date.day, date.month, date.year))
+
+ def test_empty(self):
+ """
+ Ensure that no date hierarchy links display with empty changelist.
+ """
+ response = self.client.get(
+ reverse('admin:admin_views_podcast_changelist'))
+ self.assertNotContains(response, 'release_date__year=')
+ self.assertNotContains(response, 'release_date__month=')
+ self.assertNotContains(response, 'release_date__day=')
+
+ def test_single(self):
+ """
+ Ensure that single day-level date hierarchy appears for single object.
+ """
+ DATE = datetime.date(2000, 6, 30)
+ Podcast.objects.create(release_date=DATE)
+ url = reverse('admin:admin_views_podcast_changelist')
+ response = self.client.get(url)
+ self.assert_contains_day_link(response, DATE)
+ self.assert_non_localized_year(response, 2000)
+
+ def test_within_month(self):
+ """
+ Ensure that day-level links appear for changelist within single month.
+ """
+ DATES = (datetime.date(2000, 6, 30),
+ datetime.date(2000, 6, 15),
+ datetime.date(2000, 6, 3))
+ for date in DATES:
+ Podcast.objects.create(release_date=date)
+ url = reverse('admin:admin_views_podcast_changelist')
+ response = self.client.get(url)
+ for date in DATES:
+ self.assert_contains_day_link(response, date)
+ self.assert_non_localized_year(response, 2000)
+
+ def test_within_year(self):
+ """
+ Ensure that month-level links appear for changelist within single year.
+ """
+ DATES = (datetime.date(2000, 1, 30),
+ datetime.date(2000, 3, 15),
+ datetime.date(2000, 5, 3))
+ for date in DATES:
+ Podcast.objects.create(release_date=date)
+ url = reverse('admin:admin_views_podcast_changelist')
+ response = self.client.get(url)
+ # no day-level links
+ self.assertNotContains(response, 'release_date__day=')
+ for date in DATES:
+ self.assert_contains_month_link(response, date)
+ self.assert_non_localized_year(response, 2000)
+
+ def test_multiple_years(self):
+ """
+ Ensure that year-level links appear for year-spanning changelist.
+ """
+ DATES = (datetime.date(2001, 1, 30),
+ datetime.date(2003, 3, 15),
+ datetime.date(2005, 5, 3))
+ for date in DATES:
+ Podcast.objects.create(release_date=date)
+ response = self.client.get(
+ reverse('admin:admin_views_podcast_changelist'))
+ # no day/month-level links
+ self.assertNotContains(response, 'release_date__day=')
+ self.assertNotContains(response, 'release_date__month=')
+ for date in DATES:
+ self.assert_contains_year_link(response, date)
+
+ # and make sure GET parameters still behave correctly
+ for date in DATES:
+ url = '%s?release_date__year=%d' % (
+ reverse('admin:admin_views_podcast_changelist'),
+ date.year)
+ response = self.client.get(url)
+ self.assert_contains_month_link(response, date)
+ self.assert_non_localized_year(response, 2000)
+ self.assert_non_localized_year(response, 2003)
+ self.assert_non_localized_year(response, 2005)
+
+ url = '%s?release_date__year=%d&release_date__month=%d' % (
+ reverse('admin:admin_views_podcast_changelist'),
+ date.year, date.month)
+ response = self.client.get(url)
+ self.assert_contains_day_link(response, date)
+ self.assert_non_localized_year(response, 2000)
+ self.assert_non_localized_year(response, 2003)
+ self.assert_non_localized_year(response, 2005)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminCustomSaveRelatedTests(TestCase):
+ """
+ Ensure that one can easily customize the way related objects are saved.
+ Refs #16115.
+ """
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def test_should_be_able_to_edit_related_objects_on_add_view(self):
+ post = {
+ 'child_set-TOTAL_FORMS': '3',
+ 'child_set-INITIAL_FORMS': '0',
+ 'name': 'Josh Stone',
+ 'child_set-0-name': 'Paul',
+ 'child_set-1-name': 'Catherine',
+ }
+ response = self.client.post('/test_admin/admin/admin_views/parent/add/', post)
+ self.assertEqual(1, Parent.objects.count())
+ self.assertEqual(2, Child.objects.count())
+
+ children_names = list(Child.objects.order_by('name').values_list('name', flat=True))
+
+ self.assertEqual('Josh Stone', Parent.objects.latest('id').name)
+ self.assertEqual(['Catherine Stone', 'Paul Stone'], children_names)
+
+ def test_should_be_able_to_edit_related_objects_on_change_view(self):
+ parent = Parent.objects.create(name='Josh Stone')
+ paul = Child.objects.create(parent=parent, name='Paul')
+ catherine = Child.objects.create(parent=parent, name='Catherine')
+ post = {
+ 'child_set-TOTAL_FORMS': '5',
+ 'child_set-INITIAL_FORMS': '2',
+ 'name': 'Josh Stone',
+ 'child_set-0-name': 'Paul',
+ 'child_set-0-id': paul.id,
+ 'child_set-1-name': 'Catherine',
+ 'child_set-1-id': catherine.id,
+ }
+ response = self.client.post('/test_admin/admin/admin_views/parent/%s/' % parent.id, post)
+
+ children_names = list(Child.objects.order_by('name').values_list('name', flat=True))
+
+ self.assertEqual('Josh Stone', Parent.objects.latest('id').name)
+ self.assertEqual(['Catherine Stone', 'Paul Stone'], children_names)
+
+ def test_should_be_able_to_edit_related_objects_on_changelist_view(self):
+ parent = Parent.objects.create(name='Josh Rock')
+ paul = Child.objects.create(parent=parent, name='Paul')
+ catherine = Child.objects.create(parent=parent, name='Catherine')
+ post = {
+ 'form-TOTAL_FORMS': '1',
+ 'form-INITIAL_FORMS': '1',
+ 'form-MAX_NUM_FORMS': '0',
+ 'form-0-id': parent.id,
+ 'form-0-name': 'Josh Stone',
+ '_save': 'Save'
+ }
+
+ response = self.client.post('/test_admin/admin/admin_views/parent/', post)
+ children_names = list(Child.objects.order_by('name').values_list('name', flat=True))
+
+ self.assertEqual('Josh Stone', Parent.objects.latest('id').name)
+ self.assertEqual(['Catherine Stone', 'Paul Stone'], children_names)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminViewLogoutTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_client_logout_url_can_be_used_to_login(self):
+ response = self.client.get('/test_admin/admin/logout/')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.template_name, 'registration/logged_out.html')
+ self.assertEqual(response.request['PATH_INFO'], '/test_admin/admin/logout/')
+
+ # we are now logged out
+ response = self.client.get('/test_admin/admin/logout/')
+ self.assertEqual(response.status_code, 302) # we should be redirected to the login page.
+
+ # follow the redirect and test results.
+ response = self.client.get('/test_admin/admin/logout/', follow=True)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.template_name, 'admin/login.html')
+ self.assertEqual(response.request['PATH_INFO'], '/test_admin/admin/')
+ self.assertContains(response, '<input type="hidden" name="next" value="/test_admin/admin/" />')
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminUserMessageTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def send_message(self, level):
+ """
+ Helper that sends a post to the dummy test methods and asserts that a
+ message with the level has appeared in the response.
+ """
+ action_data = {
+ ACTION_CHECKBOX_NAME: [1],
+ 'action': 'message_%s' % level,
+ 'index': 0,
+ }
+
+ response = self.client.post('/test_admin/admin/admin_views/usermessenger/',
+ action_data, follow=True)
+ self.assertContains(response,
+ '<li class="%s">Test %s</li>' % (level, level),
+ html=True)
+
+ @override_settings(MESSAGE_LEVEL=10) # Set to DEBUG for this request
+ def test_message_debug(self):
+ self.send_message('debug')
+
+ def test_message_info(self):
+ self.send_message('info')
+
+ def test_message_success(self):
+ self.send_message('success')
+
+ def test_message_warning(self):
+ self.send_message('warning')
+
+ def test_message_error(self):
+ self.send_message('error')
+
+ def test_message_extra_tags(self):
+ action_data = {
+ ACTION_CHECKBOX_NAME: [1],
+ 'action': 'message_extra_tags',
+ 'index': 0,
+ }
+
+ response = self.client.post('/test_admin/admin/admin_views/usermessenger/',
+ action_data, follow=True)
+ self.assertContains(response,
+ '<li class="extra_tag info">Test tags</li>',
+ html=True)
diff --git a/tests/admin_views/urls.py b/tests/admin_views/urls.py
new file mode 100644
index 0000000000..441834cd2a
--- /dev/null
+++ b/tests/admin_views/urls.py
@@ -0,0 +1,15 @@
+from __future__ import absolute_import
+
+from django.conf.urls import patterns, include
+
+from . import views, customadmin, admin
+
+
+urlpatterns = patterns('',
+ (r'^test_admin/admin/doc/', include('django.contrib.admindocs.urls')),
+ (r'^test_admin/admin/secure-view/$', views.secure_view),
+ (r'^test_admin/admin/', include(admin.site.urls)),
+ (r'^test_admin/admin2/', include(customadmin.site.urls)),
+ (r'^test_admin/admin3/', include(admin.site.urls), dict(form_url='pony')),
+ (r'^test_admin/admin4/', include(customadmin.simple_site.urls)),
+)
diff --git a/tests/admin_views/views.py b/tests/admin_views/views.py
new file mode 100644
index 0000000000..bb5f24ebfe
--- /dev/null
+++ b/tests/admin_views/views.py
@@ -0,0 +1,6 @@
+from django.contrib.admin.views.decorators import staff_member_required
+from django.http import HttpResponse
+
+@staff_member_required
+def secure_view(request):
+ return HttpResponse('%s' % request.POST)