diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 5271e02..2825c99 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -11,6 +11,7 @@ from django.contrib import messages from django.views.decorators.csrf import csrf_protect from django.core.exceptions import PermissionDenied, ValidationError from django.core.paginator import Paginator +from django.core.urlresolvers import reverse from django.db import models, transaction, router from django.db.models.related import RelatedObject from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist @@ -776,9 +777,12 @@ class ModelAdmin(BaseModelAdmin): # redirect to the change-list page for this object. Otherwise, # redirect to the admin index. if self.has_change_permission(request, None): - post_url = '../' + post_url = reverse('admin:%s_%s_changelist' % + (opts.app_label, opts.module_name), + current_app=self.admin_site.name) else: - post_url = '../../../' + post_url = reverse('admin:index', + current_app=self.admin_site.name) return HttpResponseRedirect(post_url) def response_change(self, request, obj): @@ -787,11 +791,14 @@ class ModelAdmin(BaseModelAdmin): """ opts = obj._meta - # Handle proxy models automatically created by .only() or .defer() + # Handle proxy models automatically created by .only() or .defer(). + # Refs #14529 verbose_name = opts.verbose_name + module_name = opts.module_name if obj._deferred: opts_ = opts.proxy_for_model._meta verbose_name = opts_.verbose_name + module_name = opts_.module_name pk_value = obj._get_pk_val() @@ -805,19 +812,28 @@ class ModelAdmin(BaseModelAdmin): elif "_saveasnew" in request.POST: msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(verbose_name), 'obj': obj} self.message_user(request, msg) - return HttpResponseRedirect("../%s/" % pk_value) + return HttpResponseRedirect(reverse('admin:%s_%s_change' % + (opts.app_label, module_name), + args=(pk_value,), + current_app=self.admin_site.name)) elif "_addanother" in request.POST: self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(verbose_name))) - return HttpResponseRedirect("../add/") + return HttpResponseRedirect(reverse('admin:%s_%s_add' % + (opts.app_label, module_name), + current_app=self.admin_site.name)) else: self.message_user(request, msg) # Figure out where to redirect. If the user has change permission, # redirect to the change-list page for this object. Otherwise, # redirect to the admin index. if self.has_change_permission(request, None): - return HttpResponseRedirect('../') + post_url = reverse('admin:%s_%s_changelist' % + (opts.app_label, module_name), + current_app=self.admin_site.name) else: - return HttpResponseRedirect('../../../') + post_url = reverse('admin:index', + current_app=self.admin_site.name) + return HttpResponseRedirect(post_url) def response_action(self, request, queryset): """ @@ -990,7 +1006,9 @@ class ModelAdmin(BaseModelAdmin): raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)}) if request.method == 'POST' and "_saveasnew" in request.POST: - return self.add_view(request, form_url='../add/') + return self.add_view(request, form_url=reverse('admin:%s_%s_add' % + (opts.app_label, opts.module_name), + current_app=self.admin_site.name)) ModelForm = self.get_form(request, obj) formsets = [] @@ -1246,8 +1264,11 @@ class ModelAdmin(BaseModelAdmin): self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj_display)}) if not self.has_change_permission(request, None): - return HttpResponseRedirect("../../../../") - return HttpResponseRedirect("../../") + return HttpResponseRedirect(reverse('admin:index', + current_app=self.admin_site.name)) + return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % + (opts.app_label, opts.module_name), + current_app=self.admin_site.name)) object_name = force_unicode(opts.verbose_name) @@ -1292,6 +1313,7 @@ class ModelAdmin(BaseModelAdmin): 'module_name': capfirst(force_unicode(opts.verbose_name_plural)), 'object': obj, 'app_label': app_label, + 'opts': opts, } context.update(extra_context or {}) return TemplateResponse(request, self.object_history_template or [ diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 9bb9f4a..3a5c12b 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -339,9 +339,11 @@ class AdminSite(object): # Check whether user has any perm for this module. # If so, add the module to the model_list. if True in perms.values(): + info = (app_label, model._meta.module_name) model_dict = { 'name': capfirst(model._meta.verbose_name_plural), - 'admin_url': mark_safe('%s/%s/' % (app_label, model.__name__.lower())), + 'admin_url': reverse('admin:%s_%s_changelist' % info, current_app=self.name), + 'add_url': reverse('admin:%s_%s_add' % info, current_app=self.name), 'perms': perms, } if app_label in app_dict: @@ -349,7 +351,7 @@ class AdminSite(object): else: app_dict[app_label] = { 'name': app_label.title(), - 'app_url': app_label + '/', + 'app_url': reverse('admin:app_list', kwargs={'app_label': app_label}, current_app=self.name), 'has_module_perms': has_module_perms, 'models': [model_dict], } @@ -383,9 +385,11 @@ class AdminSite(object): # Check whether user has any perm for this module. # If so, add the module to the model_list. if True in perms.values(): + info = (app_label, model._meta.module_name) model_dict = { 'name': capfirst(model._meta.verbose_name_plural), - 'admin_url': '%s/' % model.__name__.lower(), + 'admin_url': reverse('admin:%s_%s_changelist' % info, current_app=self.name), + 'add_url': reverse('admin:%s_%s_add' % info, current_app=self.name), 'perms': perms, } if app_dict: diff --git a/django/contrib/admin/templates/admin/500.html b/django/contrib/admin/templates/admin/500.html index b30e431..eeb9e8d 100644 --- a/django/contrib/admin/templates/admin/500.html +++ b/django/contrib/admin/templates/admin/500.html @@ -1,7 +1,13 @@ {% extends "admin/base_site.html" %} {% load i18n %} +{% load url from future %} -{% block breadcrumbs %}
{% endblock %} +{% block breadcrumbs %} + +{% endblock %} {% block title %}{% trans 'Server error (500)' %}{% endblock %} diff --git a/django/contrib/admin/templates/admin/app_index.html b/django/contrib/admin/templates/admin/app_index.html index 120433d..4616b16 100644 --- a/django/contrib/admin/templates/admin/app_index.html +++ b/django/contrib/admin/templates/admin/app_index.html @@ -1,15 +1,17 @@ -{% extends "admin/index.html" %} -{% load i18n %} +{% extends "admin/index.html" %} +{% load i18n %} +{% load url from future %} {% if not is_popup %} - {% block breadcrumbs %} - +{% endblock %} +{% endif %} -{% block sidebar %}{% endblock %} \ No newline at end of file +{% block sidebar %}{% endblock %} diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html index b18b0aa..32dbcd9 100644 --- a/django/contrib/admin/templates/admin/auth/user/change_password.html +++ b/django/contrib/admin/templates/admin/auth/user/change_password.html @@ -1,21 +1,24 @@ {% extends "admin/base_site.html" %} {% load i18n admin_static admin_modify %} {% load url from future %} +{% load admin_urls %} + {% block extrahead %}{{ block.super }} {% url 'admin:jsi18n' as jsi18nurl %} {% endblock %} {% block extrastyle %}{{ block.super }}{% endblock %} {% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %} -{% block breadcrumbs %}{% if not is_popup %} - -{% endif %}{% endblock %} +{% endblock %} +{% endif %} {% block content %}{% trans "Something's wrong with your database installation. Make sure the appropriate database tables have been created, and make sure the database is readable by the appropriate user." %}
diff --git a/django/contrib/admin/templates/admin/object_history.html b/django/contrib/admin/templates/admin/object_history.html index 5ae7847..e39471b 100644 --- a/django/contrib/admin/templates/admin/object_history.html +++ b/django/contrib/admin/templates/admin/object_history.html @@ -1,13 +1,15 @@ {% extends "admin/base_site.html" %} {% load i18n %} +{% load url from future %} +{% load admin_urls %} {% block breadcrumbs %} {% endblock %} diff --git a/django/contrib/admin/templates/registration/logged_out.html b/django/contrib/admin/templates/registration/logged_out.html index d339ef0..e95d864 100644 --- a/django/contrib/admin/templates/registration/logged_out.html +++ b/django/contrib/admin/templates/registration/logged_out.html @@ -1,12 +1,13 @@ {% extends "admin/base_site.html" %} {% load i18n %} +{% load url from future %} -{% block breadcrumbs %} {% endblock %} +{% block breadcrumbs %} {% endblock %} {% block content %}{% trans "Thanks for spending some quality time with the Web site today." %}
- + {% endblock %} diff --git a/django/contrib/admin/templates/registration/password_change_done.html b/django/contrib/admin/templates/registration/password_change_done.html index 0c0690d..863fc96 100644 --- a/django/contrib/admin/templates/registration/password_change_done.html +++ b/django/contrib/admin/templates/registration/password_change_done.html @@ -1,8 +1,13 @@ {% extends "admin/base_site.html" %} {% load i18n %} {% load url from future %} -{% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}{% trans 'Documentation' %} / {% endif %}{% trans 'Change password' %} / {% trans 'Log out' %}{% endblock %} -{% block breadcrumbs %} {% endblock %} +{% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}{% trans 'Documentation' %} / {% endif %}{% trans 'Change password' %} / {% trans 'Log out' %}{% endblock %} +{% block breadcrumbs %} + +{% endblock %} {% block title %}{% trans 'Password change successful' %}{% endblock %} diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html index f76692d..0e4ea53 100644 --- a/django/contrib/admin/templates/registration/password_change_form.html +++ b/django/contrib/admin/templates/registration/password_change_form.html @@ -2,8 +2,13 @@ {% load i18n static %} {% load url from future %} {% block extrastyle %}{{ block.super }}{% endblock %} -{% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}{% trans 'Documentation' %} / {% endif %} {% trans 'Change password' %} / {% trans 'Log out' %}{% endblock %} -{% block breadcrumbs %} {% endblock %} +{% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}{% trans 'Documentation' %} / {% endif %} {% trans 'Change password' %} / {% trans 'Log out' %}{% endblock %} +{% block breadcrumbs %} + +{% endblock %} {% block title %}{% trans 'Password change' %}{% endblock %} diff --git a/django/contrib/admin/templates/registration/password_reset_complete.html b/django/contrib/admin/templates/registration/password_reset_complete.html index fceb167..7c07707 100644 --- a/django/contrib/admin/templates/registration/password_reset_complete.html +++ b/django/contrib/admin/templates/registration/password_reset_complete.html @@ -1,7 +1,13 @@ {% extends "admin/base_site.html" %} {% load i18n %} +{% load url from future %} -{% block breadcrumbs %} {% endblock %} +{% block breadcrumbs %} + +{% endblock %} {% block title %}{% trans 'Password reset complete' %}{% endblock %} diff --git a/django/contrib/admin/templates/registration/password_reset_confirm.html b/django/contrib/admin/templates/registration/password_reset_confirm.html index df9cf1b..d5299eb 100644 --- a/django/contrib/admin/templates/registration/password_reset_confirm.html +++ b/django/contrib/admin/templates/registration/password_reset_confirm.html @@ -1,7 +1,13 @@ {% extends "admin/base_site.html" %} {% load i18n %} +{% load url from future %} -{% block breadcrumbs %} {% endblock %} +{% block breadcrumbs %} + +{% endblock %} {% block title %}{% trans 'Password reset' %}{% endblock %} diff --git a/django/contrib/admin/templates/registration/password_reset_done.html b/django/contrib/admin/templates/registration/password_reset_done.html index e223bdb..fd73af8 100644 --- a/django/contrib/admin/templates/registration/password_reset_done.html +++ b/django/contrib/admin/templates/registration/password_reset_done.html @@ -1,7 +1,13 @@ {% extends "admin/base_site.html" %} {% load i18n %} +{% load url from future %} -{% block breadcrumbs %} {% endblock %} +{% block breadcrumbs %} + +{% endblock %} {% block title %}{% trans 'Password reset successful' %}{% endblock %} diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html index d3a1284..2b591fe 100644 --- a/django/contrib/admin/templates/registration/password_reset_form.html +++ b/django/contrib/admin/templates/registration/password_reset_form.html @@ -1,7 +1,13 @@ {% extends "admin/base_site.html" %} {% load i18n %} +{% load url from future %} -{% block breadcrumbs %} {% endblock %} +{% block breadcrumbs %} + +{% endblock %} {% block title %}{% trans "Password reset" %}{% endblock %} diff --git a/django/contrib/admin/templatetags/admin_urls.py b/django/contrib/admin/templatetags/admin_urls.py new file mode 100644 index 0000000..15e4649 --- /dev/null +++ b/django/contrib/admin/templatetags/admin_urls.py @@ -0,0 +1,8 @@ +from django.core.urlresolvers import reverse, NoReverseMatch +from django import template + +register = template.Library() + +@register.filter +def admin_url(value, arg): + return 'admin:%s_%s_%s' % (value.app_label, value.module_name, arg) diff --git a/django/contrib/admindocs/templates/admin_doc/bookmarklets.html b/django/contrib/admindocs/templates/admin_doc/bookmarklets.html index 6447529..baa717c 100644 --- a/django/contrib/admindocs/templates/admin_doc/bookmarklets.html +++ b/django/contrib/admindocs/templates/admin_doc/bookmarklets.html @@ -1,6 +1,14 @@ {% extends "admin/base_site.html" %} - -{% block breadcrumbs %}{% load i18n %} {% endblock %} +{% load i18n %} +{% load url from future %} + +{% block breadcrumbs %} + +{% endblock %} {% block title %}{% trans "Documentation bookmarklets" %}{% endblock %} {% block content %} diff --git a/django/contrib/admindocs/templates/admin_doc/index.html b/django/contrib/admindocs/templates/admin_doc/index.html index a8b21c3..652fbfd 100644 --- a/django/contrib/admindocs/templates/admin_doc/index.html +++ b/django/contrib/admindocs/templates/admin_doc/index.html @@ -1,6 +1,13 @@ {% extends "admin/base_site.html" %} {% load i18n %} -{% block breadcrumbs %} {% endblock %} +{% load url from future %} + +{% block breadcrumbs %} + +{% endblock %} {% block title %}Documentation{% endblock %} {% block content %} diff --git a/django/contrib/admindocs/templates/admin_doc/missing_docutils.html b/django/contrib/admindocs/templates/admin_doc/missing_docutils.html index 97c9d47..34c18ff 100644 --- a/django/contrib/admindocs/templates/admin_doc/missing_docutils.html +++ b/django/contrib/admindocs/templates/admin_doc/missing_docutils.html @@ -1,6 +1,12 @@ {% extends "admin/base_site.html" %} {% load i18n %} -{% block breadcrumbs %} {% endblock %} +{% load url from future %} + +{% block breadcrumbs %} + {% block title %}Please install docutils{% endblock %} {% block content %} diff --git a/django/contrib/admindocs/templates/admin_doc/model_detail.html b/django/contrib/admindocs/templates/admin_doc/model_detail.html index 828df18..82cb405 100644 --- a/django/contrib/admindocs/templates/admin_doc/model_detail.html +++ b/django/contrib/admindocs/templates/admin_doc/model_detail.html @@ -1,5 +1,7 @@ {% extends "admin/base_site.html" %} {% load i18n %} +{% load url from future %} + {% block extrahead %} {{ block.super }} {% endblock %} -{% block breadcrumbs %} {% endblock %} +{% block breadcrumbs %} + +{% endblock %} {% block title %}Model: {{ name }}{% endblock %} @@ -41,6 +50,6 @@‹ Back to Models Documentation
+‹ Back to Models Documentation
{% endblock %} diff --git a/django/contrib/admindocs/templates/admin_doc/model_index.html b/django/contrib/admindocs/templates/admin_doc/model_index.html index 47c94c0..4eb3cf6 100644 --- a/django/contrib/admindocs/templates/admin_doc/model_index.html +++ b/django/contrib/admindocs/templates/admin_doc/model_index.html @@ -1,7 +1,16 @@ {% extends "admin/base_site.html" %} {% load i18n %} +{% load url from future %} + {% block coltype %}colSM{% endblock %} -{% block breadcrumbs %} {% endblock %} + +{% block breadcrumbs %} + +{% endblock %} {% block title %}Models{% endblock %} @@ -19,7 +28,7 @@{{ model.object_name }} | +{{ model.object_name }} |
---|
{{ meta.Templates }}
{% endif %} - + {% endblock %} diff --git a/django/contrib/admindocs/templates/admin_doc/view_index.html b/django/contrib/admindocs/templates/admin_doc/view_index.html index 6b10fa6..e508c84 100644 --- a/django/contrib/admindocs/templates/admin_doc/view_index.html +++ b/django/contrib/admindocs/templates/admin_doc/view_index.html @@ -1,7 +1,15 @@ {% extends "admin/base_site.html" %} {% load i18n %} +{% load url from future %} + {% block coltype %}colSM{% endblock %} -{% block breadcrumbs %} {% endblock %} +{% block breadcrumbs %} + +{% endblock %} {% block title %}Views{% endblock %} {% block content %} @@ -29,8 +37,8 @@ {% for view in site_views.list|dictsort:"url" %} {% ifchanged %} -View function: {{ view.module }}.{{ view.name }}
+View function: {{ view.full_name }}
{{ view.title }}
Awesome stacked help text is awesome.
', 4) self.assertContains(response, '', 1) class TestInlineMedia(TestCase): + urls = "regressiontests.admin_inlines.urls" fixtures = ['admin-views-users.xml'] def setUp(self): @@ -128,7 +131,7 @@ class TestInlineMedia(TestCase): holder = Holder(dummy=13) holder.save() Inner(dummy=42, holder=holder).save() - change_url = '/test_admin/admin/admin_inlines/holder/%i/' % holder.id + change_url = '/admin/admin_inlines/holder/%i/' % holder.id response = self.client.get(change_url) self.assertContains(response, 'my_awesome_admin_scripts.js') @@ -136,7 +139,7 @@ class TestInlineMedia(TestCase): holder = Holder3(dummy=13) holder.save() Inner3(dummy=42, holder=holder).save() - change_url = '/test_admin/admin/admin_inlines/holder3/%i/' % holder.id + change_url = '/admin/admin_inlines/holder3/%i/' % holder.id response = self.client.get(change_url) self.assertContains(response, 'my_awesome_inline_scripts.js') @@ -144,12 +147,13 @@ class TestInlineMedia(TestCase): holder = Holder2(dummy=13) holder.save() Inner2(dummy=42, holder=holder).save() - change_url = '/test_admin/admin/admin_inlines/holder2/%i/' % holder.id + change_url = '/admin/admin_inlines/holder2/%i/' % holder.id response = self.client.get(change_url) self.assertContains(response, 'my_awesome_admin_scripts.js') self.assertContains(response, 'my_awesome_inline_scripts.js') class TestInlineAdminForm(TestCase): + urls = "regressiontests.admin_inlines.urls" def test_immutable_content_type(self): """Regression for #9362 diff --git a/tests/regressiontests/admin_inlines/urls.py b/tests/regressiontests/admin_inlines/urls.py new file mode 100644 index 0000000..1cbb496 --- /dev/null +++ b/tests/regressiontests/admin_inlines/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls.defaults import * + +import admin + +urlpatterns = patterns('', + (r'^admin/', include(admin.site.urls)), +) diff --git a/tests/regressiontests/admin_views/admin.py b/tests/regressiontests/admin_views/admin.py new file mode 100644 index 0000000..11b0b9e --- /dev/null +++ b/tests/regressiontests/admin_views/admin.py @@ -0,0 +1,526 @@ +# -*- coding: utf-8 -*- +import datetime +import tempfile +import os + +from django.contrib import admin +from django.contrib.admin.views.main import ChangeList +from django.forms.models import BaseModelFormSet +from django.core.mail import EmailMessage + +from models import * + + +def callable_year(dt_value): + return dt_value.year +callable_year.admin_order_field = 'date' + + +class ArticleInline(admin.TabularInline): + model = Article + + +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') + + 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') + + +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 = u'foo' + + +class BarAccount(Account): + """A service-specific account of type Bar.""" + servicename = u'bar' + + +class FooAccountAdmin(admin.StackedInline): + model = FooAccount + extra = 1 + + +class BarAccountAdmin(admin.StackedInline): + model = BarAccount + extra = 1 + + +class PersonaAdmin(admin.ModelAdmin): + inlines = ( + FooAccountAdmin, + BarAccountAdmin + ) + + +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()) +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",) + + +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', 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 + 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 PaperAdmin(admin.ModelAdmin): + """ + A ModelAdin with a custom queryset() method that uses only(), to test + verbose_name display in messages shown after adding Paper instances. + """ + + def queryset(self, request): + return super(PaperAdmin, self).queryset(request).only('title') + + +class CoverLetterAdmin(admin.ModelAdmin): + """ + A ModelAdin with a custom queryset() method that uses only(), to test + verbose_name display in messages shown after adding CoverLetter instances. + Note that the CoverLetter model defines a __unicode__ method. + """ + + def queryset(self, request): + return super(CoverLetterAdmin, self).queryset(request).defer('date_written') + + +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 '%s' % ('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',) + + +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(Story, StoryAdmin) +site.register(OtherStory, OtherStoryAdmin) + +# 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_unicode 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) + +# 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/regressiontests/admin_views/customadmin.py b/tests/regressiontests/admin_views/customadmin.py index 760e93f..a696e9f 100644 --- a/tests/regressiontests/admin_views/customadmin.py +++ b/tests/regressiontests/admin_views/customadmin.py @@ -5,7 +5,7 @@ from django.conf.urls import patterns from django.contrib import admin from django.http import HttpResponse -import models, forms +import models, forms, admin as base_admin class Admin2(admin.AdminSite): login_form = forms.CustomAdminAuthenticationForm @@ -29,8 +29,8 @@ class Admin2(admin.AdminSite): site = Admin2(name="admin2") -site.register(models.Article, models.ArticleAdmin) -site.register(models.Section, inlines=[models.ArticleInline]) -site.register(models.Thing, models.ThingAdmin) -site.register(models.Fabric, models.FabricAdmin) -site.register(models.ChapterXtra1, models.ChapterXtra1Admin) +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) diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index 8dc61e3..bb8d026 100644 --- a/tests/regressiontests/admin_views/models.py +++ b/tests/regressiontests/admin_views/models.py @@ -3,17 +3,14 @@ import datetime import tempfile import os -from django.contrib import admin from django.core.files.storage import FileSystemStorage -from django.contrib.admin.views.main import ChangeList -from django.core.mail import EmailMessage from django.db import models from django import forms -from django.forms.models import BaseModelFormSet from django.contrib.auth.models import User from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType + class Section(models.Model): """ A simple section that links to articles, to test linking to related items @@ -21,6 +18,7 @@ class Section(models.Model): """ name = models.CharField(max_length=100) + class Article(models.Model): """ A simple article to test admin views. Test backwards compatibility. @@ -38,6 +36,7 @@ class Article(models.Model): model_year.admin_order_field = 'date' model_year.short_description = '' + class Book(models.Model): """ A simple book that has chapters. @@ -47,6 +46,7 @@ class Book(models.Model): def __unicode__(self): return self.name + class Promo(models.Model): name = models.CharField(max_length=100, verbose_name=u'¿Name?') book = models.ForeignKey(Book) @@ -54,6 +54,7 @@ class Promo(models.Model): def __unicode__(self): return self.name + class Chapter(models.Model): title = models.CharField(max_length=100, verbose_name=u'¿Title?') content = models.TextField() @@ -66,6 +67,7 @@ class Chapter(models.Model): # Use a utf-8 bytestring to ensure it works (see #11710) verbose_name = '¿Chapter?' + class ChapterXtra1(models.Model): chap = models.OneToOneField(Chapter, verbose_name=u'¿Chap?') xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?') @@ -73,6 +75,7 @@ class ChapterXtra1(models.Model): def __unicode__(self): return u'¿Xtra1: %s' % self.xtra + class ChapterXtra2(models.Model): chap = models.OneToOneField(Chapter, verbose_name=u'¿Chap?') xtra = models.CharField(max_length=100, verbose_name=u'¿Xtra?') @@ -80,89 +83,15 @@ class ChapterXtra2(models.Model): def __unicode__(self): return u'¿Xtra2: %s' % self.xtra -def callable_year(dt_value): - return dt_value.year -callable_year.admin_order_field = 'date' - -class ArticleInline(admin.TabularInline): - model = Article - -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') - - 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 RowLevelChangePermissionModel(models.Model): name = models.CharField(max_length=100, blank=True) -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 CustomArticle(models.Model): content = models.TextField() date = models.DateTimeField() -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 ModelWithStringPrimaryKey(models.Model): id = models.CharField(max_length=255, primary_key=True) @@ -170,20 +99,20 @@ class ModelWithStringPrimaryKey(models.Model): def __unicode__(self): return self.id + class Color(models.Model): value = models.CharField(max_length=10) warm = models.BooleanField() def __unicode__(self): return self.value + class Thing(models.Model): title = models.CharField(max_length=20) color = models.ForeignKey(Color, limit_choices_to={'warm': True}) def __unicode__(self): return self.title -class ThingAdmin(admin.ModelAdmin): - list_filter = ('color__warm', 'color__value') class Actor(models.Model): name = models.CharField(max_length=50) @@ -191,6 +120,7 @@ class Actor(models.Model): def __unicode__(self): return self.name + class Inquisition(models.Model): expected = models.BooleanField() leader = models.ForeignKey(Actor) @@ -199,8 +129,6 @@ class Inquisition(models.Model): def __unicode__(self): return u"by %s from %s" % (self.leader, self.country) -class InquisitionAdmin(admin.ModelAdmin): - list_display = ('leader', 'country', 'expected') class Sketch(models.Model): title = models.CharField(max_length=100) @@ -212,8 +140,6 @@ class Sketch(models.Model): def __unicode__(self): return self.title -class SketchAdmin(admin.ModelAdmin): - raw_id_fields = ('inquisition',) class Fabric(models.Model): NG_CHOICES = ( @@ -226,9 +152,6 @@ class Fabric(models.Model): ) surface = models.CharField(max_length=20, choices=NG_CHOICES) -class FabricAdmin(admin.ModelAdmin): - list_display = ('surface',) - list_filter = ('surface',) class Person(models.Model): GENDER_CHOICES = ( @@ -243,30 +166,6 @@ class Person(models.Model): def __unicode__(self): return self.name -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 Persona(models.Model): """ @@ -277,6 +176,7 @@ class Persona(models.Model): def __unicode__(self): return self.name + class Account(models.Model): """ A simple, generic account encapsulating the information shared by all @@ -289,27 +189,16 @@ class Account(models.Model): def __unicode__(self): return "%s: %s" % (self.servicename, self.username) + class FooAccount(Account): """A service-specific account of type Foo.""" servicename = u'foo' + class BarAccount(Account): """A service-specific account of type Bar.""" servicename = u'bar' -class FooAccountAdmin(admin.StackedInline): - model = FooAccount - extra = 1 - -class BarAccountAdmin(admin.StackedInline): - model = BarAccount - extra = 1 - -class PersonaAdmin(admin.ModelAdmin): - inlines = ( - FooAccountAdmin, - BarAccountAdmin - ) class Subscriber(models.Model): name = models.CharField(blank=False, max_length=80) @@ -318,120 +207,58 @@ class Subscriber(models.Model): def __unicode__(self): return "%s (%s)" % (self.name, self.email) -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() class ExternalSubscriber(Subscriber): pass + class OldSubscriber(Subscriber): pass -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 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 PodcastAdmin(admin.ModelAdmin): - list_display = ('name', 'release_date') - list_editable = ('release_date',) - date_hierarchy = 'release_date' - ordering = ('name',) class Vodcast(Media): media = models.OneToOneField(Media, primary_key=True, parent_link=True) released = models.BooleanField(default=False) -class VodcastAdmin(admin.ModelAdmin): - list_display = ('name', 'released') - list_editable = ('released',) - - ordering = ('name',) 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) -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 EmptyModel(models.Model): def __unicode__(self): return "Primary key = %s" % self.id -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()) 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 PictureInline(admin.TabularInline): - model = Picture - extra = 1 - -class GalleryAdmin(admin.ModelAdmin): - inlines = [PictureInline] - -class PictureAdmin(admin.ModelAdmin): - pass class Language(models.Model): iso = models.CharField(max_length=5, primary_key=True) @@ -442,70 +269,60 @@ class Language(models.Model): class Meta: ordering = ('iso',) -class LanguageAdmin(admin.ModelAdmin): - list_display = ['iso', 'shortlist', 'english_name', 'name'] - list_editable = ['shortlist'] # 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 RecommendationAdmin(admin.ModelAdmin): - search_fields = ('=titletranslation__text', '=recommender__titletranslation__text',) 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) -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 Category(models.Model): collector = models.ForeignKey(Collector) @@ -517,18 +334,6 @@ class Category(models.Model): def __unicode__(self): return u'%s:o%s' % (self.id, self.order) -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 Link(models.Model): posted = models.DateField( @@ -538,57 +343,17 @@ class Link(models.Model): post = models.ForeignKey("Post") -class LinkInline(admin.TabularInline): - model = Link - extra = 1 - - readonly_fields = ("posted",) - - 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 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 Post(models.Model): title = models.CharField(max_length=100, help_text="Some help text for the title (with unicode ŠĐĆŽćžšđ)") @@ -602,23 +367,6 @@ class Post(models.Model): def awesomeness_level(self): return "Very awesome." -class PostAdmin(admin.ModelAdmin): - list_display = ['title', 'public'] - readonly_fields = ('posted', 'awesomeness_level', 'coolness', 'value', 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 - value.short_description = 'Value in $US' class Gadget(models.Model): name = models.CharField(max_length=100) @@ -626,13 +374,6 @@ class Gadget(models.Model): def __unicode__(self): return self.name -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 Villain(models.Model): name = models.CharField(max_length=100) @@ -640,9 +381,11 @@ class Villain(models.Model): def __unicode__(self): return self.name + class SuperVillain(Villain): pass + class FunkyTag(models.Model): "Because we all know there's only one real use case for GFKs." name = models.CharField(max_length=25) @@ -653,6 +396,7 @@ class FunkyTag(models.Model): def __unicode__(self): return self.name + class Plot(models.Model): name = models.CharField(max_length=100) team_leader = models.ForeignKey(Villain, related_name='lead_plots') @@ -662,6 +406,7 @@ class Plot(models.Model): def __unicode__(self): return self.name + class PlotDetails(models.Model): details = models.CharField(max_length=100) plot = models.OneToOneField(Plot) @@ -669,6 +414,7 @@ class PlotDetails(models.Model): def __unicode__(self): return self.details + class SecretHideout(models.Model): """ Secret! Not registered with the admin! """ location = models.CharField(max_length=100) @@ -677,6 +423,7 @@ class SecretHideout(models.Model): def __unicode__(self): return self.location + class SuperSecretHideout(models.Model): """ Secret! Not registered with the admin! """ location = models.CharField(max_length=100) @@ -685,6 +432,7 @@ class SuperSecretHideout(models.Model): def __unicode__(self): return self.location + class CyclicOne(models.Model): name = models.CharField(max_length=25) two = models.ForeignKey('CyclicTwo') @@ -692,6 +440,7 @@ class CyclicOne(models.Model): def __unicode__(self): return self.name + class CyclicTwo(models.Model): name = models.CharField(max_length=25) one = models.ForeignKey(CyclicOne) @@ -699,37 +448,34 @@ class CyclicTwo(models.Model): def __unicode__(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 PizzaAdmin(admin.ModelAdmin): - readonly_fields = ('toppings',) class Album(models.Model): owner = models.ForeignKey(User) title = models.CharField(max_length=30) -class AlbumAdmin(admin.ModelAdmin): - list_filter = ['title'] class Employee(Person): code = models.CharField(max_length=20) + class WorkHour(models.Model): datum = models.DateField() employee = models.ForeignKey(Employee) -class WorkHourAdmin(admin.ModelAdmin): - list_display = ('datum', 'employee') - list_filter = ('employee',) class Question(models.Model): question = models.CharField(max_length=20) + class Answer(models.Model): question = models.ForeignKey(Question, on_delete=models.PROTECT) answer = models.CharField(max_length=20) @@ -737,6 +483,7 @@ class Answer(models.Model): def __unicode__(self): return self.answer + class Reservation(models.Model): start_date = models.DateTimeField() price = models.IntegerField() @@ -753,6 +500,7 @@ RESTAURANT_CHOICES = ( (u'pizza', u'Pizza Mama'), ) + class FoodDelivery(models.Model): reference = models.CharField(max_length=100) driver = models.CharField(max_length=100, choices=DRIVER_CHOICES, blank=True) @@ -761,14 +509,12 @@ class FoodDelivery(models.Model): class Meta: unique_together = (("driver", "restaurant"),) -class FoodDeliveryAdmin(admin.ModelAdmin): - list_display=('reference', 'driver', 'restaurant') - list_editable = ('driver', 'restaurant') class Paper(models.Model): title = models.CharField(max_length=30) author = models.CharField(max_length=30, blank=True, null=True) + class CoverLetter(models.Model): author = models.CharField(max_length=30) date_written = models.DateField(null=True, blank=True) @@ -776,123 +522,19 @@ class CoverLetter(models.Model): def __unicode__(self): return self.author -class PaperAdmin(admin.ModelAdmin): - """ - A ModelAdin with a custom queryset() method that uses only(), to test - verbose_name display in messages shown after adding Paper instances. - """ - - def queryset(self, request): - return super(PaperAdmin, self).queryset(request).only('title') - -class CoverLetterAdmin(admin.ModelAdmin): - """ - A ModelAdin with a custom queryset() method that uses only(), to test - verbose_name display in messages shown after adding CoverLetter instances. - Note that the CoverLetter model defines a __unicode__ method. - """ - - def queryset(self, request): - return super(CoverLetterAdmin, self).queryset(request).defer('date_written') class Story(models.Model): title = models.CharField(max_length=100) content = models.TextField() -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 OtherStory(models.Model): title = models.CharField(max_length=100) content = models.TextField() -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 ComplexSortedPerson(models.Model): name = models.CharField(max_length=100) age = models.PositiveIntegerField() is_employee = models.NullBooleanField() -class ComplexSortedPersonAdmin(admin.ModelAdmin): - list_display = ('name', 'age', 'is_employee', 'colored_name') - ordering = ('name',) - - def colored_name(self, obj): - return '%s' % ('ff00ff', obj.name) - colored_name.allow_tags = True - colored_name.admin_order_field = 'name' - -admin.site.register(Article, ArticleAdmin) -admin.site.register(CustomArticle, CustomArticleAdmin) -admin.site.register(Section, save_as=True, inlines=[ArticleInline]) -admin.site.register(ModelWithStringPrimaryKey) -admin.site.register(Color) -admin.site.register(Thing, ThingAdmin) -admin.site.register(Actor) -admin.site.register(Inquisition, InquisitionAdmin) -admin.site.register(Sketch, SketchAdmin) -admin.site.register(Person, PersonAdmin) -admin.site.register(Persona, PersonaAdmin) -admin.site.register(Subscriber, SubscriberAdmin) -admin.site.register(ExternalSubscriber, ExternalSubscriberAdmin) -admin.site.register(OldSubscriber, OldSubscriberAdmin) -admin.site.register(Podcast, PodcastAdmin) -admin.site.register(Vodcast, VodcastAdmin) -admin.site.register(Parent, ParentAdmin) -admin.site.register(EmptyModel, EmptyModelAdmin) -admin.site.register(Fabric, FabricAdmin) -admin.site.register(Gallery, GalleryAdmin) -admin.site.register(Picture, PictureAdmin) -admin.site.register(Language, LanguageAdmin) -admin.site.register(Recommendation, RecommendationAdmin) -admin.site.register(Recommender) -admin.site.register(Collector, CollectorAdmin) -admin.site.register(Category, CategoryAdmin) -admin.site.register(Post, PostAdmin) -admin.site.register(Gadget, GadgetAdmin) -admin.site.register(Villain) -admin.site.register(SuperVillain) -admin.site.register(Plot) -admin.site.register(PlotDetails) -admin.site.register(CyclicOne) -admin.site.register(CyclicTwo) -admin.site.register(WorkHour, WorkHourAdmin) -admin.site.register(Reservation) -admin.site.register(FoodDelivery, FoodDeliveryAdmin) -admin.site.register(RowLevelChangePermissionModel, RowLevelChangePermissionModelAdmin) -admin.site.register(Paper, PaperAdmin) -admin.site.register(CoverLetter, CoverLetterAdmin) -admin.site.register(Story, StoryAdmin) -admin.site.register(OtherStory, OtherStoryAdmin) - -# 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_unicode to avoid problems on Python 2.3) paths through -# contrib.admin.util's get_deleted_objects function. -admin.site.register(Book, inlines=[ChapterInline]) -admin.site.register(Promo) -admin.site.register(ChapterXtra1, ChapterXtra1Admin) -admin.site.register(Pizza, PizzaAdmin) -admin.site.register(Topping) -admin.site.register(Album, AlbumAdmin) -admin.site.register(Question) -admin.site.register(Answer) -admin.site.register(PrePopulatedPost, PrePopulatedPostAdmin) -admin.site.register(ComplexSortedPerson, ComplexSortedPersonAdmin) diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 22b65a6..d4b2715 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -32,7 +32,7 @@ from django.utils import unittest # local test models from models import (Article, BarAccount, CustomArticle, EmptyModel, - FooAccount, Gallery, PersonAdmin, ModelWithStringPrimaryKey, + 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, @@ -40,6 +40,8 @@ from models import (Article, BarAccount, CustomArticle, EmptyModel, RowLevelChangePermissionModel, Paper, CoverLetter, Story, OtherStory, ComplexSortedPerson, Parent, Child) +from admin import PersonAdmin + class AdminViewBasicTest(TestCase): fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', @@ -50,6 +52,8 @@ class AdminViewBasicTest(TestCase): # 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 @@ -542,6 +546,8 @@ class AdminViewBasicTest(TestCase): self.fail("Filters should be allowed if they are defined on a ForeignKey pointing to this model") class AdminJavaScriptTest(AdminViewBasicTest): + urls = "regressiontests.admin_views.urls" + def testSingleWidgetFirsFieldFocus(self): """ JavaScript-assisted auto-focus on first field. @@ -565,6 +571,7 @@ class AdminJavaScriptTest(AdminViewBasicTest): class SaveAsTests(TestCase): + urls = "regressiontests.admin_views.urls" fixtures = ['admin-views-users.xml','admin-views-person.xml'] def setUp(self): @@ -590,9 +597,10 @@ class SaveAsTests(TestCase): 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'], '../add/') + 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): @@ -654,6 +662,7 @@ def get_perm(Model, perm): class AdminViewPermissionsTest(TestCase): """Tests for Admin Views Permissions.""" + urls = "regressiontests.admin_views.urls" fixtures = ['admin-views-users.xml'] def setUp(self): @@ -835,7 +844,7 @@ class AdminViewPermissionsTest(TestCase): self.client.post('/test_admin/admin/', self.adduser_login) addpage = self.client.get('/test_admin/admin/admin_views/article/add/') self.assertEqual(addpage.status_code, 200) - change_list_link = 'Articles ›' + change_list_link = '› Articles' self.assertFalse(change_list_link in addpage.content, '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) @@ -1055,6 +1064,7 @@ class AdminViewPermissionsTest(TestCase): class AdminViewDeletedObjectsTest(TestCase): + urls = "regressiontests.admin_views.urls" fixtures = ['admin-views-users.xml', 'deleted-objects.xml'] def setUp(self): @@ -1170,6 +1180,7 @@ class AdminViewDeletedObjectsTest(TestCase): self.assertContains(response, should_contain) class AdminViewStringPrimaryKeyTest(TestCase): + urls = "regressiontests.admin_views.urls" fixtures = ['admin-views-users.xml', 'string-primary-key.xml'] def __init__(self, *args): @@ -1261,6 +1272,7 @@ class AdminViewStringPrimaryKeyTest(TestCase): class SecureViewTests(TestCase): + urls = "regressiontests.admin_views.urls" fixtures = ['admin-views-users.xml'] def setUp(self): @@ -1418,6 +1430,7 @@ class SecureViewTests(TestCase): self.assertEqual(response['Location'], 'http://example.com/users/super/') class AdminViewUnicodeTest(TestCase): + urls = "regressiontests.admin_views.urls" fixtures = ['admin-views-unicode.xml'] def setUp(self): @@ -1471,6 +1484,7 @@ class AdminViewUnicodeTest(TestCase): class AdminViewListEditable(TestCase): + urls = "regressiontests.admin_views.urls" fixtures = ['admin-views-users.xml', 'admin-views-person.xml'] def setUp(self): @@ -1827,6 +1841,7 @@ class AdminViewListEditable(TestCase): class AdminSearchTest(TestCase): + urls = "regressiontests.admin_views.urls" fixtures = ['admin-views-users', 'multiple-child-classes', 'admin-views-person'] @@ -1873,6 +1888,7 @@ class AdminSearchTest(TestCase): class AdminInheritedInlinesTest(TestCase): + urls = "regressiontests.admin_views.urls" fixtures = ['admin-views-users.xml',] def setUp(self): @@ -1958,6 +1974,7 @@ class AdminInheritedInlinesTest(TestCase): self.assertEqual(Persona.objects.all()[0].accounts.count(), 2) class AdminActionsTest(TestCase): + urls = "regressiontests.admin_views.urls" fixtures = ['admin-views-users.xml', 'admin-views-actions.xml'] def setUp(self): @@ -2179,6 +2196,7 @@ class AdminActionsTest(TestCase): class TestCustomChangeList(TestCase): + urls = "regressiontests.admin_views.urls" fixtures = ['admin-views-users.xml'] urlbit = 'admin' @@ -2206,6 +2224,7 @@ class TestCustomChangeList(TestCase): class TestInlineNotEditable(TestCase): + urls = "regressiontests.admin_views.urls" fixtures = ['admin-views-users.xml'] def setUp(self): @@ -2223,6 +2242,7 @@ class TestInlineNotEditable(TestCase): self.assertEqual(response.status_code, 200) class AdminCustomQuerysetTest(TestCase): + urls = "regressiontests.admin_views.urls" fixtures = ['admin-views-users.xml'] def setUp(self): @@ -2277,6 +2297,7 @@ class AdminCustomQuerysetTest(TestCase): self.assertContains(response, '