summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorBrian Rosner <brosner@gmail.com>2008-07-18 23:54:34 +0000
committerBrian Rosner <brosner@gmail.com>2008-07-18 23:54:34 +0000
commita19ed8aea395e8e07164ff7d85bd7dff2f24edca (patch)
treeec5fd01c30abc5fa22c1f02159bf68cfe89313cc /django
parentdc375fb0f3b7fbae740e8cfcd791b8bccb8a4e66 (diff)
Merged the newforms-admin branch into trunk.
This is a backward incompatible change. The admin contrib app has been refactored. The newforms module has several improvements including FormSets and Media definitions. git-svn-id: http://code.djangoproject.com/svn/django/trunk@7967 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django')
-rw-r--r--django/__init__.py2
-rw-r--r--django/conf/project_template/urls.py8
-rw-r--r--django/contrib/admin/__init__.py16
-rw-r--r--django/contrib/admin/filterspecs.py28
-rw-r--r--django/contrib/admin/media/css/forms.css21
-rw-r--r--django/contrib/admin/media/js/SelectFilter.js81
-rw-r--r--django/contrib/admin/media/js/admin/CollapsedFieldsets.js2
-rw-r--r--django/contrib/admin/media/js/admin/RelatedObjectLookups.js4
-rw-r--r--django/contrib/admin/models.py3
-rw-r--r--django/contrib/admin/options.py795
-rw-r--r--django/contrib/admin/sites.py349
-rw-r--r--django/contrib/admin/templates/admin/auth/user/add_form.html13
-rw-r--r--django/contrib/admin/templates/admin/auth/user/change_password.html13
-rw-r--r--django/contrib/admin/templates/admin/base.html9
-rw-r--r--django/contrib/admin/templates/admin/change_form.html69
-rw-r--r--django/contrib/admin/templates/admin/change_list.html18
-rw-r--r--django/contrib/admin/templates/admin/delete_confirmation.html2
-rw-r--r--django/contrib/admin/templates/admin/edit_inline/stacked.html26
-rw-r--r--django/contrib/admin/templates/admin/edit_inline/tabular.html64
-rw-r--r--django/contrib/admin/templates/admin/edit_inline_stacked.html16
-rw-r--r--django/contrib/admin/templates/admin/edit_inline_tabular.html44
-rw-r--r--django/contrib/admin/templates/admin/field_line.html10
-rw-r--r--django/contrib/admin/templates/admin/filters.html7
-rw-r--r--django/contrib/admin/templates/admin/includes/fieldset.html17
-rw-r--r--django/contrib/admin/templates/admin/index.html7
-rw-r--r--django/contrib/admin/templates/admin/invalid_setup.html2
-rw-r--r--django/contrib/admin/templates/admin/login.html4
-rw-r--r--django/contrib/admin/templates/admin/object_history.html8
-rw-r--r--django/contrib/admin/templates/admin/search_form.html2
-rw-r--r--django/contrib/admin/templates/admin_doc/index.html1
-rw-r--r--django/contrib/admin/templates/admin_doc/view_index.html1
-rw-r--r--django/contrib/admin/templates/registration/password_change_done.html1
-rw-r--r--django/contrib/admin/templates/registration/password_change_form.html1
-rw-r--r--django/contrib/admin/templates/registration/password_reset_form.html2
-rw-r--r--django/contrib/admin/templates/widget/date_time.html5
-rw-r--r--django/contrib/admin/templates/widget/default.html1
-rw-r--r--django/contrib/admin/templates/widget/file.html4
-rw-r--r--django/contrib/admin/templates/widget/foreign.html20
-rw-r--r--django/contrib/admin/templates/widget/many_to_many.html1
-rw-r--r--django/contrib/admin/templates/widget/one_to_one.html2
-rw-r--r--django/contrib/admin/templatetags/admin_list.py18
-rw-r--r--django/contrib/admin/templatetags/admin_modify.py240
-rw-r--r--django/contrib/admin/templatetags/adminapplist.py81
-rw-r--r--django/contrib/admin/urls.py43
-rw-r--r--django/contrib/admin/util.py139
-rw-r--r--django/contrib/admin/validation.py280
-rw-r--r--django/contrib/admin/views/auth.py78
-rw-r--r--django/contrib/admin/views/decorators.py1
-rw-r--r--django/contrib/admin/views/main.py622
-rw-r--r--django/contrib/admin/widgets.py215
-rw-r--r--django/contrib/admindocs/__init__.py0
-rw-r--r--django/contrib/admindocs/urls.py15
-rw-r--r--django/contrib/admindocs/utils.py (renamed from django/contrib/admin/utils.py)0
-rw-r--r--django/contrib/admindocs/views.py (renamed from django/contrib/admin/views/doc.py)52
-rw-r--r--django/contrib/auth/admin.py66
-rw-r--r--django/contrib/auth/forms.py233
-rw-r--r--django/contrib/auth/models.py25
-rw-r--r--django/contrib/auth/tests/__init__.py8
-rw-r--r--django/contrib/auth/tests/basic.py (renamed from django/contrib/auth/tests.py)8
-rw-r--r--django/contrib/auth/tests/forms.py135
-rw-r--r--django/contrib/auth/views.py91
-rw-r--r--django/contrib/comments/admin.py30
-rw-r--r--django/contrib/comments/models.py26
-rw-r--r--django/contrib/comments/views/comments.py52
-rw-r--r--django/contrib/flatpages/admin.py15
-rw-r--r--django/contrib/flatpages/models.py11
-rw-r--r--django/contrib/redirects/models.py24
-rw-r--r--django/contrib/sites/admin.py9
-rw-r--r--django/contrib/sites/models.py7
-rw-r--r--django/core/management/validation.py62
-rw-r--r--django/db/models/__init__.py2
-rw-r--r--django/db/models/base.py5
-rw-r--r--django/db/models/fields/__init__.py33
-rw-r--r--django/db/models/fields/related.py77
-rw-r--r--django/db/models/manipulators.py7
-rw-r--r--django/db/models/options.py75
-rw-r--r--django/newforms/__init__.py1
-rw-r--r--django/newforms/forms.py51
-rw-r--r--django/newforms/formsets.py292
-rw-r--r--django/newforms/models.py222
-rw-r--r--django/newforms/widgets.py170
81 files changed, 3445 insertions, 1755 deletions
diff --git a/django/__init__.py b/django/__init__.py
index de473fa4e9..9c5fda133d 100644
--- a/django/__init__.py
+++ b/django/__init__.py
@@ -1,4 +1,4 @@
-VERSION = (0, 97, 'pre')
+VERSION = (0, 97, 'newforms-admin')
def get_version():
"Returns the version as a human-format string."
diff --git a/django/conf/project_template/urls.py b/django/conf/project_template/urls.py
index 402dd6536b..98335a151c 100644
--- a/django/conf/project_template/urls.py
+++ b/django/conf/project_template/urls.py
@@ -1,9 +1,15 @@
from django.conf.urls.defaults import *
+# Uncomment this for admin:
+#from django.contrib import admin
+
urlpatterns = patterns('',
# Example:
# (r'^{{ project_name }}/', include('{{ project_name }}.foo.urls')),
+ # Uncomment this for admin docs:
+ #(r'^admin/doc/', include('django.contrib.admindocs.urls')),
+
# Uncomment this for admin:
-# (r'^admin/', include('django.contrib.admin.urls')),
+ #('^admin/(.*)', admin.site.root),
)
diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py
index e69de29bb2..56b64faacb 100644
--- a/django/contrib/admin/__init__.py
+++ b/django/contrib/admin/__init__.py
@@ -0,0 +1,16 @@
+from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
+from django.contrib.admin.options import StackedInline, TabularInline
+from django.contrib.admin.sites import AdminSite, site
+
+def autodiscover():
+ """
+ Auto-discover INSTALLED_APPS admin.py modules and fail silently when
+ not present. This forces an import on them to register any admin bits they
+ may want.
+ """
+ from django.conf import settings
+ for app in settings.INSTALLED_APPS:
+ try:
+ __import__("%s.admin" % app)
+ except ImportError:
+ pass
diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py
index a4f92c986a..d6a4a0bc48 100644
--- a/django/contrib/admin/filterspecs.py
+++ b/django/contrib/admin/filterspecs.py
@@ -15,7 +15,7 @@ import datetime
class FilterSpec(object):
filter_specs = []
- def __init__(self, f, request, params, model):
+ def __init__(self, f, request, params, model, model_admin):
self.field = f
self.params = params
@@ -23,10 +23,10 @@ class FilterSpec(object):
cls.filter_specs.append((test, factory))
register = classmethod(register)
- def create(cls, f, request, params, model):
+ def create(cls, f, request, params, model, model_admin):
for test, factory in cls.filter_specs:
if test(f):
- return factory(f, request, params, model)
+ return factory(f, request, params, model, model_admin)
create = classmethod(create)
def has_output(self):
@@ -52,8 +52,8 @@ class FilterSpec(object):
return mark_safe("".join(t))
class RelatedFilterSpec(FilterSpec):
- def __init__(self, f, request, params, model):
- super(RelatedFilterSpec, self).__init__(f, request, params, model)
+ def __init__(self, f, request, params, model, model_admin):
+ super(RelatedFilterSpec, self).__init__(f, request, params, model, model_admin)
if isinstance(f, models.ManyToManyField):
self.lookup_title = f.rel.to._meta.verbose_name
else:
@@ -81,8 +81,8 @@ class RelatedFilterSpec(FilterSpec):
FilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec)
class ChoicesFilterSpec(FilterSpec):
- def __init__(self, f, request, params, model):
- super(ChoicesFilterSpec, self).__init__(f, request, params, model)
+ def __init__(self, f, request, params, model, model_admin):
+ super(ChoicesFilterSpec, self).__init__(f, request, params, model, model_admin)
self.lookup_kwarg = '%s__exact' % f.name
self.lookup_val = request.GET.get(self.lookup_kwarg, None)
@@ -98,8 +98,8 @@ class ChoicesFilterSpec(FilterSpec):
FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)
class DateFieldFilterSpec(FilterSpec):
- def __init__(self, f, request, params, model):
- super(DateFieldFilterSpec, self).__init__(f, request, params, model)
+ def __init__(self, f, request, params, model, model_admin):
+ super(DateFieldFilterSpec, self).__init__(f, request, params, model, model_admin)
self.field_generic = '%s__' % self.field.name
@@ -133,8 +133,8 @@ class DateFieldFilterSpec(FilterSpec):
FilterSpec.register(lambda f: isinstance(f, models.DateField), DateFieldFilterSpec)
class BooleanFieldFilterSpec(FilterSpec):
- def __init__(self, f, request, params, model):
- super(BooleanFieldFilterSpec, self).__init__(f, request, params, model)
+ def __init__(self, f, request, params, model, model_admin):
+ super(BooleanFieldFilterSpec, self).__init__(f, request, params, model, model_admin)
self.lookup_kwarg = '%s__exact' % f.name
self.lookup_kwarg2 = '%s__isnull' % f.name
self.lookup_val = request.GET.get(self.lookup_kwarg, None)
@@ -159,10 +159,10 @@ FilterSpec.register(lambda f: isinstance(f, models.BooleanField) or isinstance(f
# if a field is eligible to use the BooleanFieldFilterSpec, that'd be much
# more appropriate, and the AllValuesFilterSpec won't get used for it.
class AllValuesFilterSpec(FilterSpec):
- def __init__(self, f, request, params, model):
- super(AllValuesFilterSpec, self).__init__(f, request, params, model)
+ def __init__(self, f, request, params, model, model_admin):
+ super(AllValuesFilterSpec, self).__init__(f, request, params, model, model_admin)
self.lookup_val = request.GET.get(f.name, None)
- self.lookup_choices = model._meta.admin.manager.distinct().order_by(f.name).values(f.name)
+ self.lookup_choices = model_admin.queryset(request).distinct().order_by(f.name).values(f.name)
def title(self):
return self.field.verbose_name
diff --git a/django/contrib/admin/media/css/forms.css b/django/contrib/admin/media/css/forms.css
index 72e57501e9..2a1a0995a0 100644
--- a/django/contrib/admin/media/css/forms.css
+++ b/django/contrib/admin/media/css/forms.css
@@ -58,3 +58,24 @@ fieldset.monospace textarea { font-family:"Bitstream Vera Sans Mono",Monaco,"Cou
.vLargeTextField, .vXMLLargeTextField { width:48em; }
.flatpages-flatpage #id_content { height:40.2em; }
.module table .vPositiveSmallIntegerField { width:2.2em; }
+
+/* x unsorted */
+.inline-group {padding:10px; padding-bottom:5px; background:#eee; margin:10px 0;}
+.inline-group h3.header {margin:-5px -10px 5px -10px; background:#bbb; color:#fff; padding:2px 5px 3px 5px; font-size:11px}
+.inline-related {margin-bottom:15px; position:relative;}
+.last-related {margin-bottom:0px;}
+.inline-related h2 { margin:0; padding:2px 5px 3px 5px; font-size:11px; text-align:left; font-weight:bold; color:#888; }
+.inline-related h2 b {font-weight:normal; color:#aaa;}
+.inline-related h2 span.delete {padding-left:20px; position:absolute; top:0px; right:5px;}
+.inline-related h2 span.delete label {margin-left:2px; padding-top:1px;}
+.inline-related fieldset {background:#fbfbfb;}
+.inline-related fieldset.module h2 { margin:0; padding:2px 5px 3px 5px; font-size:11px; text-align:left; font-weight:bold; background:#bcd; color:#fff; }
+.inline-related.tabular fieldset.module table {width:100%;}
+
+.inline-group .tabular tr.has_original td {padding-top:2em;}
+.inline-group .tabular tr td.original { padding:2px 0 0 0; width:0; _position:relative; }
+.inline-group .tabular th.original {width:0px; padding:0;}
+.inline-group .tabular td.original p {position:absolute; left:0; height:1.1em; padding:2px 7px; overflow:hidden; font-size:9px; font-weight:bold; color:#666; _width:700px; }
+.inline-group ul.tools {padding:0; margin: 0; list-style:none;}
+.inline-group ul.tools li {display:inline; padding:0 5px;}
+.inline-group ul.tools a.add {background:url(../img/admin/icon_addlink.gif) 0 50% no-repeat; padding-left:14px;} \ No newline at end of file
diff --git a/django/contrib/admin/media/js/SelectFilter.js b/django/contrib/admin/media/js/SelectFilter.js
deleted file mode 100644
index 0501920608..0000000000
--- a/django/contrib/admin/media/js/SelectFilter.js
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
-SelectFilter - Turns a multiple-select box into a filter interface.
-
-Requires SelectBox.js and addevent.js.
-*/
-
-function findForm(node) {
- // returns the node of the form containing the given node
- if (node.tagName.toLowerCase() != 'form') {
- return findForm(node.parentNode);
- }
- return node;
-}
-
-var SelectFilter = {
- init: function(field_id) {
- var from_box = document.getElementById(field_id);
- from_box.id += '_from'; // change its ID
- // Create the INPUT input box
- var input_box = document.createElement('input');
- input_box.id = field_id + '_input';
- input_box.setAttribute('type', 'text');
- from_box.parentNode.insertBefore(input_box, from_box);
- from_box.parentNode.insertBefore(document.createElement('br'), input_box.nextSibling);
- // Create the TO box
- var to_box = document.createElement('select');
- to_box.id = field_id + '_to';
- to_box.setAttribute('multiple', 'multiple');
- to_box.setAttribute('size', from_box.size);
- from_box.parentNode.insertBefore(to_box, from_box.nextSibling);
- to_box.setAttribute('name', from_box.getAttribute('name'));
- from_box.setAttribute('name', from_box.getAttribute('name') + '_old');
- // Give the filters a CSS hook
- from_box.setAttribute('class', 'filtered');
- to_box.setAttribute('class', 'filtered');
- // Set up the JavaScript event handlers for the select box filter interface
- addEvent(input_box, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); });
- addEvent(input_box, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); });
- addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); });
- addEvent(from_box, 'focus', function() { input_box.focus(); });
- addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); });
- addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); });
- SelectBox.init(field_id + '_from');
- SelectBox.init(field_id + '_to');
- // Move selected from_box options to to_box
- SelectBox.move(field_id + '_from', field_id + '_to');
- },
- filter_key_up: function(event, field_id) {
- from = document.getElementById(field_id + '_from');
- // don't submit form if user pressed Enter
- if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) {
- from.selectedIndex = 0;
- SelectBox.move(field_id + '_from', field_id + '_to');
- from.selectedIndex = 0;
- return false;
- }
- var temp = from.selectedIndex;
- SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value);
- from.selectedIndex = temp;
- return true;
- },
- filter_key_down: function(event, field_id) {
- from = document.getElementById(field_id + '_from');
- // right arrow -- move across
- if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) {
- var old_index = from.selectedIndex;
- SelectBox.move(field_id + '_from', field_id + '_to');
- from.selectedIndex = (old_index == from.length) ? from.length - 1 : old_index;
- return false;
- }
- // down arrow -- wrap around
- if ((event.which && event.which == 40) || (event.keyCode && event.keyCode == 40)) {
- from.selectedIndex = (from.length == from.selectedIndex + 1) ? 0 : from.selectedIndex + 1;
- }
- // up arrow -- wrap around
- if ((event.which && event.which == 38) || (event.keyCode && event.keyCode == 38)) {
- from.selectedIndex = (from.selectedIndex == 0) ? from.length - 1 : from.selectedIndex - 1;
- }
- return true;
- }
-}
diff --git a/django/contrib/admin/media/js/admin/CollapsedFieldsets.js b/django/contrib/admin/media/js/admin/CollapsedFieldsets.js
index c8426db228..d66bec0d97 100644
--- a/django/contrib/admin/media/js/admin/CollapsedFieldsets.js
+++ b/django/contrib/admin/media/js/admin/CollapsedFieldsets.js
@@ -47,7 +47,7 @@ var CollapsedFieldsets = {
// Returns true if any fields in the fieldset have validation errors.
var divs = fs.getElementsByTagName('div');
for (var i=0; i<divs.length; i++) {
- if (divs[i].className.match(/\berror\b/)) {
+ if (divs[i].className.match(/\berrors\b/)) {
return true;
}
}
diff --git a/django/contrib/admin/media/js/admin/RelatedObjectLookups.js b/django/contrib/admin/media/js/admin/RelatedObjectLookups.js
index f6a39ca091..ca578cc28a 100644
--- a/django/contrib/admin/media/js/admin/RelatedObjectLookups.js
+++ b/django/contrib/admin/media/js/admin/RelatedObjectLookups.js
@@ -1,4 +1,4 @@
-// Handles related-objects functionality: lookup link for raw_id_admin=True
+// Handles related-objects functionality: lookup link for raw_id_fields
// and Add Another links.
function html_unescape(text) {
@@ -29,7 +29,7 @@ function showRelatedObjectLookupPopup(triggeringLink) {
function dismissRelatedLookupPopup(win, chosenId) {
var name = win.name.replace(/___/g, '.');
var elem = document.getElementById(name);
- if (elem.className.indexOf('vRawIdAdminField') != -1 && elem.value) {
+ if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) {
elem.value += ',' + chosenId;
} else {
document.getElementById(name).value = chosenId;
diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py
index 23c8661336..259884faba 100644
--- a/django/contrib/admin/models.py
+++ b/django/contrib/admin/models.py
@@ -1,6 +1,7 @@
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
+from django.contrib.admin.util import quote
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode
from django.utils.safestring import mark_safe
@@ -50,4 +51,4 @@ class LogEntry(models.Model):
Returns the admin URL to edit the object represented by this log entry.
This is relative to the Django admin index page.
"""
- return mark_safe(u"%s/%s/%s/" % (self.content_type.app_label, self.content_type.model, self.object_id))
+ return mark_safe(u"%s/%s/%s/" % (self.content_type.app_label, self.content_type.model, quote(self.object_id)))
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
new file mode 100644
index 0000000000..3b26f7b262
--- /dev/null
+++ b/django/contrib/admin/options.py
@@ -0,0 +1,795 @@
+from django import oldforms, template
+from django import newforms as forms
+from django.newforms.formsets import all_valid
+from django.newforms.models import modelform_factory, inlineformset_factory
+from django.newforms.models import BaseInlineFormset
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.admin import widgets
+from django.contrib.admin.util import quote, unquote, get_deleted_objects
+from django.core.exceptions import ImproperlyConfigured, PermissionDenied
+from django.db import models, transaction
+from django.http import Http404, HttpResponse, HttpResponseRedirect
+from django.shortcuts import get_object_or_404, render_to_response
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+from django.utils.text import capfirst, get_text_list
+from django.utils.translation import ugettext as _
+from django.utils.encoding import force_unicode
+import sets
+
+HORIZONTAL, VERTICAL = 1, 2
+# returns the <ul> class for a given radio_admin field
+get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
+
+class IncorrectLookupParameters(Exception):
+ pass
+
+def flatten_fieldsets(fieldsets):
+ """Returns a list of field names from an admin fieldsets structure."""
+ field_names = []
+ for name, opts in fieldsets:
+ for field in opts['fields']:
+ # type checking feels dirty, but it seems like the best way here
+ if type(field) == tuple:
+ field_names.extend(field)
+ else:
+ field_names.append(field)
+ return field_names
+
+class AdminForm(object):
+ def __init__(self, form, fieldsets, prepopulated_fields):
+ self.form, self.fieldsets = form, fieldsets
+ self.prepopulated_fields = [{
+ 'field': form[field_name],
+ 'dependencies': [form[f] for f in dependencies]
+ } for field_name, dependencies in prepopulated_fields.items()]
+
+ def __iter__(self):
+ for name, options in self.fieldsets:
+ yield Fieldset(self.form, name, **options)
+
+ def first_field(self):
+ for bf in self.form:
+ return bf
+
+ def _media(self):
+ media = self.form.media
+ for fs in self:
+ media = media + fs.media
+ return media
+ media = property(_media)
+
+class Fieldset(object):
+ def __init__(self, form, name=None, fields=(), classes=(), description=None):
+ self.form = form
+ self.name, self.fields = name, fields
+ self.classes = u' '.join(classes)
+ self.description = description
+
+ def _media(self):
+ from django.conf import settings
+ if 'collapse' in self.classes:
+ return forms.Media(js=['%sjs/admin/CollapsedFieldsets.js' % settings.ADMIN_MEDIA_PREFIX])
+ return forms.Media()
+ media = property(_media)
+
+ def __iter__(self):
+ for field in self.fields:
+ yield Fieldline(self.form, field)
+
+class Fieldline(object):
+ def __init__(self, form, field):
+ self.form = form # A django.forms.Form instance
+ if isinstance(field, basestring):
+ self.fields = [field]
+ else:
+ self.fields = field
+
+ def __iter__(self):
+ for i, field in enumerate(self.fields):
+ yield AdminField(self.form, field, is_first=(i == 0))
+
+ def errors(self):
+ return mark_safe(u'\n'.join([self.form[f].errors.as_ul() for f in self.fields]))
+
+class AdminField(object):
+ def __init__(self, form, field, is_first):
+ self.field = form[field] # A django.forms.BoundField instance
+ self.is_first = is_first # Whether this field is first on the line
+ self.is_checkbox = isinstance(self.field.field.widget, forms.CheckboxInput)
+
+ def label_tag(self):
+ classes = []
+ if self.is_checkbox:
+ classes.append(u'vCheckboxLabel')
+ contents = escape(self.field.label)
+ else:
+ contents = force_unicode(escape(self.field.label)) + u':'
+ if self.field.field.required:
+ classes.append(u'required')
+ if not self.is_first:
+ classes.append(u'inline')
+ attrs = classes and {'class': u' '.join(classes)} or {}
+ return self.field.label_tag(contents=contents, attrs=attrs)
+
+class BaseModelAdmin(object):
+ """Functionality common to both ModelAdmin and InlineAdmin."""
+ raw_id_fields = ()
+ fields = None
+ fieldsets = None
+ form = forms.ModelForm
+ filter_vertical = ()
+ filter_horizontal = ()
+ radio_fields = {}
+ prepopulated_fields = {}
+
+ def formfield_for_dbfield(self, db_field, **kwargs):
+ """
+ Hook for specifying the form Field instance for a given database Field
+ instance.
+
+ If kwargs are given, they're passed to the form Field's constructor.
+ """
+ # For DateTimeFields, use a special field and widget.
+ if isinstance(db_field, models.DateTimeField):
+ kwargs['form_class'] = forms.SplitDateTimeField
+ kwargs['widget'] = widgets.AdminSplitDateTime()
+ return db_field.formfield(**kwargs)
+
+ # For DateFields, add a custom CSS class.
+ if isinstance(db_field, models.DateField):
+ kwargs['widget'] = widgets.AdminDateWidget
+ return db_field.formfield(**kwargs)
+
+ # For TimeFields, add a custom CSS class.
+ if isinstance(db_field, models.TimeField):
+ kwargs['widget'] = widgets.AdminTimeWidget
+ return db_field.formfield(**kwargs)
+
+ # For FileFields and ImageFields add a link to the current file.
+ if isinstance(db_field, models.ImageField) or isinstance(db_field, models.FileField):
+ kwargs['widget'] = widgets.AdminFileWidget
+ return db_field.formfield(**kwargs)
+
+ # For ForeignKey or ManyToManyFields, use a special widget.
+ if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
+ if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields:
+ kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel)
+ elif isinstance(db_field, models.ForeignKey) and db_field.name in self.radio_fields:
+ kwargs['widget'] = widgets.AdminRadioSelect(attrs={
+ 'class': get_ul_class(self.radio_fields[db_field.name]),
+ })
+ kwargs['empty_label'] = db_field.blank and _('None') or None
+ else:
+ if isinstance(db_field, models.ManyToManyField):
+ if db_field.name in self.raw_id_fields:
+ kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
+ kwargs['help_text'] = ''
+ elif db_field.name in (self.filter_vertical + self.filter_horizontal):
+ kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
+ # Wrap the widget's render() method with a method that adds
+ # extra HTML to the end of the rendered output.
+ formfield = db_field.formfield(**kwargs)
+ # Don't wrap raw_id fields. Their add function is in the popup window.
+ if not db_field.name in self.raw_id_fields:
+ formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site)
+ return formfield
+
+ if db_field.choices and db_field.name in self.radio_fields:
+ kwargs['widget'] = widgets.AdminRadioSelect(
+ choices=db_field.get_choices(include_blank=db_field.blank,
+ blank_choice=[('', _('None'))]),
+ attrs={
+ 'class': get_ul_class(self.radio_fields[db_field.name]),
+ }
+ )
+
+ # For any other type of field, just call its formfield() method.
+ return db_field.formfield(**kwargs)
+
+ def _declared_fieldsets(self):
+ if self.fieldsets:
+ return self.fieldsets
+ elif self.fields:
+ return [(None, {'fields': self.fields})]
+ return None
+ declared_fieldsets = property(_declared_fieldsets)
+
+class ModelAdmin(BaseModelAdmin):
+ "Encapsulates all admin options and functionality for a given model."
+ __metaclass__ = forms.MediaDefiningClass
+
+ list_display = ('__str__',)
+ list_display_links = ()
+ list_filter = ()
+ list_select_related = False
+ list_per_page = 100
+ search_fields = ()
+ date_hierarchy = None
+ save_as = False
+ save_on_top = False
+ ordering = None
+ inlines = []
+
+ # Custom templates (designed to be over-ridden in subclasses)
+ change_form_template = None
+ change_list_template = None
+ delete_confirmation_template = None
+ object_history_template = None
+
+ def __init__(self, model, admin_site):
+ self.model = model
+ self.opts = model._meta
+ self.admin_site = admin_site
+ self.inline_instances = []
+ for inline_class in self.inlines:
+ inline_instance = inline_class(self.model, self.admin_site)
+ self.inline_instances.append(inline_instance)
+ super(ModelAdmin, self).__init__()
+
+ def __call__(self, request, url):
+ # Check that LogEntry, ContentType and the auth context processor are installed.
+ from django.conf import settings
+ if settings.DEBUG:
+ from django.contrib.admin.models import LogEntry
+ if not LogEntry._meta.installed:
+ raise ImproperlyConfigured("Put 'django.contrib.admin' in your INSTALLED_APPS setting in order to use the admin application.")
+ if not ContentType._meta.installed:
+ raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in your INSTALLED_APPS setting in order to use the admin application.")
+ if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS:
+ raise ImproperlyConfigured("Put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting in order to use the admin application.")
+
+ # Delegate to the appropriate method, based on the URL.
+ if url is None:
+ return self.changelist_view(request)
+ elif url.endswith('add'):
+ return self.add_view(request)
+ elif url.endswith('history'):
+ return self.history_view(request, unquote(url[:-8]))
+ elif url.endswith('delete'):
+ return self.delete_view(request, unquote(url[:-7]))
+ else:
+ return self.change_view(request, unquote(url))
+
+ def _media(self):
+ from django.conf import settings
+
+ js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
+ if self.prepopulated_fields:
+ js.append('js/urlify.js')
+ if self.opts.get_ordered_objects():
+ js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js'])
+ if self.filter_vertical or self.filter_horizontal:
+ js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
+
+ return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
+ media = property(_media)
+
+ def has_add_permission(self, request):
+ "Returns True if the given request has permission to add an object."
+ opts = self.opts
+ return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
+
+ def has_change_permission(self, request, obj=None):
+ """
+ Returns True if the given request has permission to change the given
+ Django model instance.
+
+ If `obj` is None, this should return True if the given request has
+ permission to change *any* object of the given type.
+ """
+ opts = self.opts
+ return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
+
+ def has_delete_permission(self, request, obj=None):
+ """
+ Returns True if the given request has permission to change the given
+ Django model instance.
+
+ If `obj` is None, this should return True if the given request has
+ permission to delete *any* object of the given type.
+ """
+ opts = self.opts
+ return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
+
+ def queryset(self, request):
+ """
+ Returns a QuerySet of all model instances that can be edited by the
+ admin site. This is used by changelist_view.
+ """
+ qs = self.model._default_manager.get_query_set()
+ # TODO: this should be handled by some parameter to the ChangeList.
+ ordering = self.ordering or () # otherwise we might try to *None, which is bad ;)
+ if ordering:
+ qs = qs.order_by(*ordering)
+ return qs
+
+ def get_fieldsets(self, request, obj=None):
+ "Hook for specifying fieldsets for the add form."
+ if self.declared_fieldsets:
+ return self.declared_fieldsets
+ form = self.get_form(request)
+ return [(None, {'fields': form.base_fields.keys()})]
+
+ def get_form(self, request, obj=None):
+ """
+ Returns a Form class for use in the admin add view. This is used by
+ add_view and change_view.
+ """
+ if self.declared_fieldsets:
+ fields = flatten_fieldsets(self.declared_fieldsets)
+ else:
+ fields = None
+ return modelform_factory(self.model, form=self.form, fields=fields, formfield_callback=self.formfield_for_dbfield)
+
+ def get_formsets(self, request, obj=None):
+ for inline in self.inline_instances:
+ yield inline.get_formset(request, obj)
+
+ def save_add(self, request, form, formsets, post_url_continue):
+ """
+ Saves the object in the "add" stage and returns an HttpResponseRedirect.
+
+ `form` is a bound Form instance that's verified to be valid.
+ """
+ from django.contrib.admin.models import LogEntry, ADDITION
+ opts = self.model._meta
+ new_object = form.save(commit=True)
+
+ if formsets:
+ for formset in formsets:
+ # HACK: it seems like the parent obejct should be passed into
+ # a method of something, not just set as an attribute
+ formset.instance = new_object
+ formset.save()
+
+ pk_value = new_object._get_pk_val()
+ LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(self.model).id, pk_value, force_unicode(new_object), ADDITION)
+ msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': opts.verbose_name, 'obj': new_object}
+ # Here, we distinguish between different save types by checking for
+ # the presence of keys in request.POST.
+ if request.POST.has_key("_continue"):
+ request.user.message_set.create(message=msg + ' ' + _("You may edit it again below."))
+ if request.POST.has_key("_popup"):
+ post_url_continue += "?_popup=1"
+ return HttpResponseRedirect(post_url_continue % pk_value)
+
+ if request.POST.has_key("_popup"):
+ return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
+ # escape() calls force_unicode.
+ (escape(pk_value), escape(new_object)))
+ elif request.POST.has_key("_addanother"):
+ request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % opts.verbose_name))
+ return HttpResponseRedirect(request.path)
+ else:
+ request.user.message_set.create(message=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):
+ post_url = '../'
+ else:
+ post_url = '../../../'
+ return HttpResponseRedirect(post_url)
+ save_add = transaction.commit_on_success(save_add)
+
+ def save_change(self, request, form, formsets=None):
+ """
+ Saves the object in the "change" stage and returns an HttpResponseRedirect.
+
+ `form` is a bound Form instance that's verified to be valid.
+
+ `formsets` is a sequence of InlineFormSet instances that are verified to be valid.
+ """
+ from django.contrib.admin.models import LogEntry, CHANGE
+ opts = self.model._meta
+ new_object = form.save(commit=True)
+ pk_value = new_object._get_pk_val()
+
+ if formsets:
+ for formset in formsets:
+ formset.save()
+
+ # Construct the change message.
+ change_message = []
+ if form.changed_data:
+ change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
+
+ if formsets:
+ for formset in formsets:
+ for added_object in formset.new_objects:
+ change_message.append(_('Added %(name)s "%(object)s".')
+ % {'name': added_object._meta.verbose_name,
+ 'object': added_object})
+ for changed_object, changed_fields in formset.changed_objects:
+ change_message.append(_('Changed %(list)s for %(name)s "%(object)s".')
+ % {'list': get_text_list(changed_fields, _('and')),
+ 'name': changed_object._meta.verbose_name,
+ 'object': changed_object})
+ for deleted_object in formset.deleted_objects:
+ change_message.append(_('Deleted %(name)s "%(object)s".')
+ % {'name': deleted_object._meta.verbose_name,
+ 'object': deleted_object})
+ change_message = ' '.join(change_message)
+ if not change_message:
+ change_message = _('No fields changed.')
+ LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(self.model).id, pk_value, force_unicode(new_object), CHANGE, change_message)
+
+ msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': opts.verbose_name, 'obj': new_object}
+ if request.POST.has_key("_continue"):
+ request.user.message_set.create(message=msg + ' ' + _("You may edit it again below."))
+ if request.REQUEST.has_key('_popup'):
+ return HttpResponseRedirect(request.path + "?_popup=1")
+ else:
+ return HttpResponseRedirect(request.path)
+ elif request.POST.has_key("_saveasnew"):
+ request.user.message_set.create(message=_('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': opts.verbose_name, 'obj': new_object})
+ return HttpResponseRedirect("../%s/" % pk_value)
+ elif request.POST.has_key("_addanother"):
+ request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % opts.verbose_name))
+ return HttpResponseRedirect("../add/")
+ else:
+ request.user.message_set.create(message=msg)
+ return HttpResponseRedirect("../")
+ save_change = transaction.commit_on_success(save_change)
+
+ def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
+ opts = self.model._meta
+ app_label = opts.app_label
+ ordered_objects = opts.get_ordered_objects()
+ context.update({
+ 'add': add,
+ 'change': change,
+ 'has_add_permission': self.has_add_permission(request),
+ 'has_change_permission': self.has_change_permission(request, obj),
+ 'has_delete_permission': self.has_delete_permission(request, obj),
+ 'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
+ 'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
+ 'ordered_objects': ordered_objects,
+ 'form_url': mark_safe(form_url),
+ 'opts': opts,
+ 'content_type_id': ContentType.objects.get_for_model(self.model).id,
+ 'save_as': self.save_as,
+ 'save_on_top': self.save_on_top,
+ 'root_path': self.admin_site.root_path,
+ })
+ return render_to_response(self.change_form_template or [
+ "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
+ "admin/%s/change_form.html" % app_label,
+ "admin/change_form.html"
+ ], context, context_instance=template.RequestContext(request))
+
+ def add_view(self, request, form_url='', extra_context=None):
+ "The 'add' admin view for this model."
+ model = self.model
+ opts = model._meta
+ app_label = opts.app_label
+
+ if not self.has_add_permission(request):
+ raise PermissionDenied
+
+ if self.has_change_permission(request, None):
+ # redirect to list view
+ post_url = '../'
+ else:
+ # Object list will give 'Permission Denied', so go back to admin home
+ post_url = '../../../'
+
+ ModelForm = self.get_form(request)
+ inline_formsets = []
+ obj = self.model()
+ if request.method == 'POST':
+ form = ModelForm(request.POST, request.FILES)
+ for FormSet in self.get_formsets(request):
+ inline_formset = FormSet(data=request.POST, files=request.FILES,
+ instance=obj, save_as_new=request.POST.has_key("_saveasnew"))
+ inline_formsets.append(inline_formset)
+ if all_valid(inline_formsets) and form.is_valid():
+ return self.save_add(request, form, inline_formsets, '../%s/')
+ else:
+ form = ModelForm(initial=dict(request.GET.items()))
+ for FormSet in self.get_formsets(request):
+ inline_formset = FormSet(instance=obj)
+ inline_formsets.append(inline_formset)
+
+ adminForm = AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
+ media = self.media + adminForm.media
+ for fs in inline_formsets:
+ media = media + fs.media
+
+ inline_admin_formsets = []
+ for inline, formset in zip(self.inline_instances, inline_formsets):
+ fieldsets = list(inline.get_fieldsets(request))
+ inline_admin_formset = InlineAdminFormSet(inline, formset, fieldsets)
+ inline_admin_formsets.append(inline_admin_formset)
+
+ context = {
+ 'title': _('Add %s') % opts.verbose_name,
+ 'adminform': adminForm,
+ 'is_popup': request.REQUEST.has_key('_popup'),
+ 'show_delete': False,
+ 'media': mark_safe(media),
+ 'inline_admin_formsets': inline_admin_formsets,
+ 'errors': AdminErrorList(form, inline_formsets),
+ 'root_path': self.admin_site.root_path,
+ }
+ context.update(extra_context or {})
+ return self.render_change_form(request, context, add=True)
+
+ def change_view(self, request, object_id, extra_context=None):
+ "The 'change' admin view for this model."
+ model = self.model
+ opts = model._meta
+ app_label = opts.app_label
+
+ try:
+ obj = model._default_manager.get(pk=object_id)
+ except model.DoesNotExist:
+ # Don't raise Http404 just yet, because we haven't checked
+ # permissions yet. We don't want an unauthenticated user to be able
+ # to determine whether a given object exists.
+ obj = None
+
+ if not self.has_change_permission(request, obj):
+ raise PermissionDenied
+
+ if obj is None:
+ raise Http404('%s object with primary key %r does not exist.' % (opts.verbose_name, escape(object_id)))
+
+ if request.POST and request.POST.has_key("_saveasnew"):
+ return self.add_view(request, form_url='../../add/')
+
+ ModelForm = self.get_form(request, obj)
+ inline_formsets = []
+ if request.method == 'POST':
+ form = ModelForm(request.POST, request.FILES, instance=obj)
+ for FormSet in self.get_formsets(request, obj):
+ inline_formset = FormSet(request.POST, request.FILES, instance=obj)
+ inline_formsets.append(inline_formset)
+
+ if all_valid(inline_formsets) and form.is_valid():
+ return self.save_change(request, form, inline_formsets)
+ else:
+ form = ModelForm(instance=obj)
+ for FormSet in self.get_formsets(request, obj):
+ inline_formset = FormSet(instance=obj)
+ inline_formsets.append(inline_formset)
+
+ adminForm = AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
+ media = self.media + adminForm.media
+ for fs in inline_formsets:
+ media = media + fs.media
+
+ inline_admin_formsets = []
+ for inline, formset in zip(self.inline_instances, inline_formsets):
+ fieldsets = list(inline.get_fieldsets(request, obj))
+ inline_admin_formset = InlineAdminFormSet(inline, formset, fieldsets)
+ inline_admin_formsets.append(inline_admin_formset)
+
+ context = {
+ 'title': _('Change %s') % opts.verbose_name,
+ 'adminform': adminForm,
+ 'object_id': object_id,
+ 'original': obj,
+ 'is_popup': request.REQUEST.has_key('_popup'),
+ 'media': mark_safe(media),
+ 'inline_admin_formsets': inline_admin_formsets,
+ 'errors': AdminErrorList(form, inline_formsets),
+ 'root_path': self.admin_site.root_path,
+ }
+ context.update(extra_context or {})
+ return self.render_change_form(request, context, change=True, obj=obj)
+
+ def changelist_view(self, request, extra_context=None):
+ "The 'change list' admin view for this model."
+ from django.contrib.admin.views.main import ChangeList, ERROR_FLAG
+ opts = self.model._meta
+ app_label = opts.app_label
+ if not self.has_change_permission(request, None):
+ raise PermissionDenied
+ try:
+ cl = ChangeList(request, self.model, self.list_display, self.list_display_links, self.list_filter,
+ self.date_hierarchy, self.search_fields, self.list_select_related, self.list_per_page, self)
+ except IncorrectLookupParameters:
+ # Wacky lookup parameters were given, so redirect to the main
+ # changelist page, without parameters, and pass an 'invalid=1'
+ # parameter via the query string. If wacky parameters were given and
+ # the 'invalid=1' parameter was already in the query string, something
+ # is screwed up with the database, so display an error page.
+ if ERROR_FLAG in request.GET.keys():
+ return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
+ return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
+
+ context = {
+ 'title': cl.title,
+ 'is_popup': cl.is_popup,
+ 'cl': cl,
+ 'has_add_permission': self.has_add_permission(request),
+ 'root_path': self.admin_site.root_path,
+ }
+ context.update(extra_context or {})
+ return render_to_response(self.change_list_template or [
+ 'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()),
+ 'admin/%s/change_list.html' % app_label,
+ 'admin/change_list.html'
+ ], context, context_instance=template.RequestContext(request))
+
+ def delete_view(self, request, object_id, extra_context=None):
+ "The 'delete' admin view for this model."
+ from django.contrib.admin.models import LogEntry, DELETION
+ opts = self.model._meta
+ app_label = opts.app_label
+
+ try:
+ obj = self.model._default_manager.get(pk=object_id)
+ except self.model.DoesNotExist:
+ # Don't raise Http404 just yet, because we haven't checked
+ # permissions yet. We don't want an unauthenticated user to be able
+ # to determine whether a given object exists.
+ obj = None
+
+ if not self.has_delete_permission(request, obj):
+ raise PermissionDenied
+
+ if obj is None:
+ raise Http404('%s object with primary key %r does not exist.' % (opts.verbose_name, escape(object_id)))
+
+ # Populate deleted_objects, a data structure of all related objects that
+ # will also be deleted.
+ deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), quote(object_id), escape(obj))), []]
+ perms_needed = sets.Set()
+ get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
+
+ if request.POST: # The user has already confirmed the deletion.
+ if perms_needed:
+ raise PermissionDenied
+ obj_display = str(obj)
+ obj.delete()
+ LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(self.model).id, object_id, obj_display, DELETION)
+ request.user.message_set.create(message=_('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("../../")
+
+ context = {
+ "title": _("Are you sure?"),
+ "object_name": opts.verbose_name,
+ "object": obj,
+ "deleted_objects": deleted_objects,
+ "perms_lacking": perms_needed,
+ "opts": opts,
+ "root_path": self.admin_site.root_path,
+ }
+ context.update(extra_context or {})
+ return render_to_response(self.delete_confirmation_template or [
+ "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()),
+ "admin/%s/delete_confirmation.html" % app_label,
+ "admin/delete_confirmation.html"
+ ], context, context_instance=template.RequestContext(request))
+
+ def history_view(self, request, object_id, extra_context=None):
+ "The 'history' admin view for this model."
+ from django.contrib.admin.models import LogEntry
+ model = self.model
+ opts = model._meta
+ action_list = LogEntry.objects.filter(
+ object_id = object_id,
+ content_type__id__exact = ContentType.objects.get_for_model(model).id
+ ).select_related().order_by('action_time')
+ # If no history was found, see whether this object even exists.
+ obj = get_object_or_404(model, pk=object_id)
+ context = {
+ 'title': _('Change history: %s') % force_unicode(obj),
+ 'action_list': action_list,
+ 'module_name': capfirst(opts.verbose_name_plural),
+ 'object': obj,
+ 'root_path': self.admin_site.root_path,
+ }
+ context.update(extra_context or {})
+ return render_to_response(self.object_history_template or [
+ "admin/%s/%s/object_history.html" % (opts.app_label, opts.object_name.lower()),
+ "admin/%s/object_history.html" % opts.app_label,
+ "admin/object_history.html"
+ ], context, context_instance=template.RequestContext(request))
+
+class InlineModelAdmin(BaseModelAdmin):
+ """
+ Options for inline editing of ``model`` instances.
+
+ Provide ``name`` to specify the attribute name of the ``ForeignKey`` from
+ ``model`` to its parent. This is required if ``model`` has more than one
+ ``ForeignKey`` to its parent.
+ """
+ model = None
+ fk_name = None
+ formset = BaseInlineFormset
+ extra = 3
+ max_num = 0
+ template = None
+ verbose_name = None
+ verbose_name_plural = None
+
+ def __init__(self, parent_model, admin_site):
+ self.admin_site = admin_site
+ self.parent_model = parent_model
+ self.opts = self.model._meta
+ super(InlineModelAdmin, self).__init__()
+ if self.verbose_name is None:
+ self.verbose_name = self.model._meta.verbose_name
+ if self.verbose_name_plural is None:
+ self.verbose_name_plural = self.model._meta.verbose_name_plural
+
+ def get_formset(self, request, obj=None):
+ """Returns a BaseInlineFormSet class for use in admin add/change views."""
+ if self.declared_fieldsets:
+ fields = flatten_fieldsets(self.declared_fieldsets)
+ else:
+ fields = None
+ return inlineformset_factory(self.parent_model, self.model,
+ form=self.form, formset=self.formset, fk_name=self.fk_name,
+ fields=fields, formfield_callback=self.formfield_for_dbfield,
+ extra=self.extra, max_num=self.max_num)
+
+ def get_fieldsets(self, request, obj=None):
+ if self.declared_fieldsets:
+ return self.declared_fieldsets
+ form = self.get_formset(request).form
+ return [(None, {'fields': form.base_fields.keys()})]
+
+class StackedInline(InlineModelAdmin):
+ template = 'admin/edit_inline/stacked.html'
+
+class TabularInline(InlineModelAdmin):
+ template = 'admin/edit_inline/tabular.html'
+
+class InlineAdminFormSet(object):
+ """
+ A wrapper around an inline formset for use in the admin system.
+ """
+ def __init__(self, inline, formset, fieldsets):
+ self.opts = inline
+ self.formset = formset
+ self.fieldsets = fieldsets
+
+ def __iter__(self):
+ for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
+ yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original)
+ for form in self.formset.extra_forms:
+ yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None)
+
+ def fields(self):
+ for field_name in flatten_fieldsets(self.fieldsets):
+ yield self.formset.form.base_fields[field_name]
+
+class InlineAdminForm(AdminForm):
+ """
+ A wrapper around an inline form for use in the admin system.
+ """
+ def __init__(self, formset, form, fieldsets, prepopulated_fields, original):
+ self.formset = formset
+ self.original = original
+ self.show_url = original and hasattr(original, 'get_absolute_url')
+ super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields)
+
+ def pk_field(self):
+ return AdminField(self.form, self.formset._pk_field_name, False)
+
+ def deletion_field(self):
+ from django.newforms.formsets import DELETION_FIELD_NAME
+ return AdminField(self.form, DELETION_FIELD_NAME, False)
+
+ def ordering_field(self):
+ from django.newforms.formsets import ORDERING_FIELD_NAME
+ return AdminField(self.form, ORDERING_FIELD_NAME, False)
+
+class AdminErrorList(forms.util.ErrorList):
+ """
+ Stores all errors for the form/formsets in an add/change stage view.
+ """
+ def __init__(self, form, inline_formsets):
+ if form.is_bound:
+ self.extend(form.errors.values())
+ for inline_formset in inline_formsets:
+ self.extend(inline_formset.non_form_errors())
+ for errors_in_inline_form in inline_formset.errors:
+ self.extend(errors_in_inline_form.values())
diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
new file mode 100644
index 0000000000..bb4dc58ece
--- /dev/null
+++ b/django/contrib/admin/sites.py
@@ -0,0 +1,349 @@
+from django import http, template
+from django.contrib.admin import ModelAdmin
+from django.contrib.auth import authenticate, login
+from django.db.models.base import ModelBase
+from django.shortcuts import render_to_response
+from django.utils.safestring import mark_safe
+from django.utils.text import capfirst
+from django.utils.translation import ugettext_lazy, ugettext as _
+from django.views.decorators.cache import never_cache
+from django.conf import settings
+import base64
+import cPickle as pickle
+import datetime
+import md5
+import re
+
+ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
+LOGIN_FORM_KEY = 'this_is_the_login_form'
+
+USER_CHANGE_PASSWORD_URL_RE = re.compile('auth/user/(\d+)/password')
+
+class AlreadyRegistered(Exception):
+ pass
+
+class NotRegistered(Exception):
+ pass
+
+def _encode_post_data(post_data):
+ from django.conf import settings
+ pickled = pickle.dumps(post_data)
+ pickled_md5 = md5.new(pickled + settings.SECRET_KEY).hexdigest()
+ return base64.encodestring(pickled + pickled_md5)
+
+def _decode_post_data(encoded_data):
+ from django.conf import settings
+ encoded_data = base64.decodestring(encoded_data)
+ pickled, tamper_check = encoded_data[:-32], encoded_data[-32:]
+ if md5.new(pickled + settings.SECRET_KEY).hexdigest() != tamper_check:
+ from django.core.exceptions import SuspiciousOperation
+ raise SuspiciousOperation, "User may have tampered with session cookie."
+ return pickle.loads(pickled)
+
+class AdminSite(object):
+ """
+ An AdminSite object encapsulates an instance of the Django admin application, ready
+ to be hooked in to your URLConf. Models are registered with the AdminSite using the
+ register() method, and the root() method can then be used as a Django view function
+ that presents a full admin interface for the collection of registered models.
+ """
+
+ index_template = None
+ login_template = None
+
+ def __init__(self):
+ self._registry = {} # model_class class -> admin_class instance
+
+ def register(self, model_or_iterable, admin_class=None, **options):
+ """
+ Registers the given model(s) with the given admin class.
+
+ The model(s) should be Model classes, not instances.
+
+ If an admin class isn't given, it will use ModelAdmin (the default
+ admin options). If keyword arguments are given -- e.g., list_display --
+ they'll be applied as options to the admin class.
+
+ If a model is already registered, this will raise AlreadyRegistered.
+ """
+ do_validate = admin_class and settings.DEBUG
+ if do_validate:
+ # don't import the humongous validation code unless required
+ from django.contrib.admin.validation import validate
+ admin_class = admin_class or ModelAdmin
+ # TODO: Handle options
+ if isinstance(model_or_iterable, ModelBase):
+ model_or_iterable = [model_or_iterable]
+ for model in model_or_iterable:
+ if model in self._registry:
+ raise AlreadyRegistered('The model %s is already registered' % model.__name__)
+ if do_validate:
+ validate(admin_class, model)
+ self._registry[model] = admin_class(model, self)
+
+ def unregister(self, model_or_iterable):
+ """
+ Unregisters the given model(s).
+
+ If a model isn't already registered, this will raise NotRegistered.
+ """
+ if isinstance(model_or_iterable, ModelBase):
+ model_or_iterable = [model_or_iterable]
+ for model in model_or_iterable:
+ if model not in self._registry:
+ raise NotRegistered('The model %s is not registered' % model.__name__)
+ del self._registry[model]
+
+ def has_permission(self, request):
+ """
+ Returns True if the given HttpRequest has permission to view
+ *at least one* page in the admin site.
+ """
+ return request.user.is_authenticated() and request.user.is_staff
+
+ def root(self, request, url):
+ """
+ Handles main URL routing for the admin app.
+
+ `url` is the remainder of the URL -- e.g. 'comments/comment/'.
+ """
+ if request.method == 'GET' and not request.path.endswith('/'):
+ return http.HttpResponseRedirect(request.path + '/')
+
+ # Figure out the admin base URL path and stash it for later use
+ self.root_path = re.sub(re.escape(url) + '$', '', request.path)
+
+ url = url.rstrip('/') # Trim trailing slash, if it exists.
+
+ # The 'logout' view doesn't require that the person is logged in.
+ if url == 'logout':
+ return self.logout(request)
+
+ # Check permission to continue or display login form.
+ if not self.has_permission(request):
+ return self.login(request)
+
+ if url == '':
+ return self.index(request)
+ elif url == 'password_change':
+ return self.password_change(request)
+ elif url == 'password_change/done':
+ return self.password_change_done(request)
+ elif url == 'jsi18n':
+ return self.i18n_javascript(request)
+ # urls starting with 'r/' are for the "show in web" links
+ elif url.startswith('r/'):
+ from django.views.defaults import shortcut
+ return shortcut(request, *url.split('/')[1:])
+ else:
+ match = USER_CHANGE_PASSWORD_URL_RE.match(url)
+ if match:
+ return self.user_change_password(request, match.group(1))
+
+ if '/' in url:
+ return self.model_page(request, *url.split('/', 2))
+
+ raise http.Http404('The requested admin page does not exist.')
+
+ def model_page(self, request, app_label, model_name, rest_of_url=None):
+ """
+ Handles the model-specific functionality of the admin site, delegating
+ to the appropriate ModelAdmin class.
+ """
+ from django.db import models
+ model = models.get_model(app_label, model_name)
+ if model is None:
+ raise http.Http404("App %r, model %r, not found." % (app_label, model_name))
+ try:
+ admin_obj = self._registry[model]
+ except KeyError:
+ raise http.Http404("This model exists but has not been registered with the admin site.")
+ return admin_obj(request, rest_of_url)
+ model_page = never_cache(model_page)
+
+ def password_change(self, request):
+ """
+ Handles the "change password" task -- both form display and validation.
+ """
+ from django.contrib.auth.views import password_change
+ return password_change(request)
+
+ def password_change_done(self, request):
+ """
+ Displays the "success" page after a password change.
+ """
+ from django.contrib.auth.views import password_change_done
+ return password_change_done(request)
+
+ def user_change_password(self, request, id):
+ """
+ Handles the "user change password" task
+ """
+ from django.contrib.auth.views import user_change_password
+ return user_change_password(request, id)
+
+ def i18n_javascript(self, request):
+ """
+ Displays the i18n JavaScript that the Django admin requires.
+
+ This takes into account the USE_I18N setting. If it's set to False, the
+ generated JavaScript will be leaner and faster.
+ """
+ from django.conf import settings
+ if settings.USE_I18N:
+ from django.views.i18n import javascript_catalog
+ else:
+ from django.views.i18n import null_javascript_catalog as javascript_catalog
+ return javascript_catalog(request, packages='django.conf')
+
+ def logout(self, request):
+ """
+ Logs out the user for the given HttpRequest.
+
+ This should *not* assume the user is already logged in.
+ """
+ from django.contrib.auth.views import logout
+ return logout(request)
+ logout = never_cache(logout)
+
+ def login(self, request):
+ """
+ Displays the login form for the given HttpRequest.
+ """
+ from django.contrib.auth.models import User
+
+ # If this isn't already the login page, display it.
+ if not request.POST.has_key(LOGIN_FORM_KEY):
+ if request.POST:
+ message = _("Please log in again, because your session has expired. Don't worry: Your submission has been saved.")
+ else:
+ message = ""
+ return self.display_login_form(request, message)
+
+ # Check that the user accepts cookies.
+ if not request.session.test_cookie_worked():
+ message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
+ return self.display_login_form(request, message)
+
+ # Check the password.
+ username = request.POST.get('username', None)
+ password = request.POST.get('password', None)
+ user = authenticate(username=username, password=password)
+ if user is None:
+ message = ERROR_MESSAGE
+ if u'@' in username:
+ # Mistakenly entered e-mail address instead of username? Look it up.
+ try:
+ user = User.objects.get(email=username)
+ except (User.DoesNotExist, User.MultipleObjectsReturned):
+ message = _("Usernames cannot contain the '@' character.")
+ else:
+ if user.check_password(password):
+ message = _("Your e-mail address is not your username."
+ " Try '%s' instead.") % user.username
+ else:
+ message = _("Usernames cannot contain the '@' character.")
+ return self.display_login_form(request, message)
+
+ # The user data is correct; log in the user in and continue.
+ else:
+ if user.is_active and user.is_staff:
+ login(request, user)
+ # TODO: set last_login with an event.
+ user.last_login = datetime.datetime.now()
+ user.save()
+ if request.POST.has_key('post_data'):
+ post_data = _decode_post_data(request.POST['post_data'])
+ if post_data and not post_data.has_key(LOGIN_FORM_KEY):
+ # overwrite request.POST with the saved post_data, and continue
+ request.POST = post_data
+ request.user = user
+ return self.root(request, request.path.split(self.root_path)[-1])
+ else:
+ request.session.delete_test_cookie()
+ return http.HttpResponseRedirect(request.path)
+ else:
+ return self.display_login_form(request, ERROR_MESSAGE)
+ login = never_cache(login)
+
+ def index(self, request, extra_context=None):
+ """
+ Displays the main admin index page, which lists all of the installed
+ apps that have been registered in this site.
+ """
+ app_dict = {}
+ user = request.user
+ for model, model_admin in self._registry.items():
+ app_label = model._meta.app_label
+ has_module_perms = user.has_module_perms(app_label)
+
+ if has_module_perms:
+ perms = {
+ 'add': model_admin.has_add_permission(request),
+ 'change': model_admin.has_change_permission(request),
+ 'delete': model_admin.has_delete_permission(request),
+ }
+
+ # Check whether user has any perm for this module.
+ # If so, add the module to the model_list.
+ if True in perms.values():
+ model_dict = {
+ 'name': capfirst(model._meta.verbose_name_plural),
+ 'admin_url': mark_safe('%s/%s/' % (app_label, model.__name__.lower())),
+ 'perms': perms,
+ }
+ if app_label in app_dict:
+ app_dict[app_label]['models'].append(model_dict)
+ else:
+ app_dict[app_label] = {
+ 'name': app_label.title(),
+ 'has_module_perms': has_module_perms,
+ 'models': [model_dict],
+ }
+
+ # Sort the apps alphabetically.
+ app_list = app_dict.values()
+ app_list.sort(lambda x, y: cmp(x['name'], y['name']))
+
+ # Sort the models alphabetically within each app.
+ for app in app_list:
+ app['models'].sort(lambda x, y: cmp(x['name'], y['name']))
+
+ context = {
+ 'title': _('Site administration'),
+ 'app_list': app_list,
+ 'root_path': self.root_path,
+ }
+ context.update(extra_context or {})
+ return render_to_response(self.index_template or 'admin/index.html', context,
+ context_instance=template.RequestContext(request)
+ )
+ index = never_cache(index)
+
+ def display_login_form(self, request, error_message='', extra_context=None):
+ request.session.set_test_cookie()
+ if request.POST and request.POST.has_key('post_data'):
+ # User has failed login BUT has previously saved post data.
+ post_data = request.POST['post_data']
+ elif request.POST:
+ # User's session must have expired; save their post data.
+ post_data = _encode_post_data(request.POST)
+ else:
+ post_data = _encode_post_data({})
+
+ context = {
+ 'title': _('Log in'),
+ 'app_path': request.path,
+ 'post_data': post_data,
+ 'error_message': error_message,
+ 'root_path': self.root_path,
+ }
+ context.update(extra_context or {})
+ return render_to_response(self.login_template or 'admin/login.html', context,
+ context_instance=template.RequestContext(request)
+ )
+
+
+# This global object represents the default admin site, for the common case.
+# You can instantiate AdminSite in your own code to create a custom admin site.
+site = AdminSite()
diff --git a/django/contrib/admin/templates/admin/auth/user/add_form.html b/django/contrib/admin/templates/admin/auth/user/add_form.html
index 139fa6a75e..65824a6b7d 100644
--- a/django/contrib/admin/templates/admin/auth/user/add_form.html
+++ b/django/contrib/admin/templates/admin/auth/user/add_form.html
@@ -8,21 +8,26 @@
<fieldset class="module aligned">
<div class="form-row">
- {{ form.username.html_error_list }}
+ {{ form.username.errors }}
+ {# TODO: get required class on label_tag #}
<label for="id_username" class="required">{% trans 'Username' %}:</label> {{ form.username }}
- <p class="help">{{ username_help_text }}</p>
+ <p class="help">{{ form.username.help_text }}</p>
</div>
<div class="form-row">
- {{ form.password1.html_error_list }}
+ {{ form.password1.errors }}
+ {# TODO: get required class on label_tag #}
<label for="id_password1" class="required">{% trans 'Password' %}:</label> {{ form.password1 }}
</div>
<div class="form-row">
- {{ form.password2.html_error_list }}
+ {{ form.password2.errors }}
+ {# TODO: get required class on label_tag #}
<label for="id_password2" class="required">{% trans 'Password (again)' %}:</label> {{ form.password2 }}
<p class="help">{% trans 'Enter the same password as above, for verification.' %}</p>
</div>
+<script type="text/javascript">document.getElementById("id_username").focus();</script>
+
</fieldset>
{% 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 28e342da86..f1c4a8d34a 100644
--- a/django/contrib/admin/templates/admin/auth/user/change_password.html
+++ b/django/contrib/admin/templates/admin/auth/user/change_password.html
@@ -2,7 +2,6 @@
{% load i18n admin_modify adminmedia %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="../../../../jsi18n/"></script>
-{% for js in javascript_imports %}{% include_admin_script js %}{% endfor %}
{% endblock %}
{% block stylesheet %}{% admin_media_prefix %}css/forms.css{% endblock %}
{% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %}
@@ -18,9 +17,9 @@
<form action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% block form_top %}{% endblock %}
<div>
{% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %}
-{% if form.error_dict %}
+{% if form.errors %}
<p class="errornote">
- {% blocktrans count form.error_dict.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
+ {% blocktrans count form.errors.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
</p>
{% endif %}
@@ -29,12 +28,14 @@
<fieldset class="module aligned">
<div class="form-row">
- {{ form.password1.html_error_list }}
+ {{ form.password1.errors }}
+ {# TODO: get required class on label_tag #}
<label for="id_password1" class="required">{% trans 'Password' %}:</label> {{ form.password1 }}
</div>
<div class="form-row">
- {{ form.password2.html_error_list }}
+ {{ form.password2.errors }}
+ {# TODO: get required class on label_tag #}
<label for="id_password2" class="required">{% trans 'Password (again)' %}:</label> {{ form.password2 }}
<p class="help">{% trans 'Enter the same password as above, for verification.' %}</p>
</div>
@@ -45,7 +46,7 @@
<input type="submit" value="{% trans 'Change password' %}" class="default" />
</div>
-<script type="text/javascript">document.getElementById("{{ first_form_field_id }}").focus();</script>
+<script type="text/javascript">document.getElementById("id_password1").focus();</script>
</div>
</form></div>
{% endblock %}
diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html
index cdd3561ab9..479e18b2ee 100644
--- a/django/contrib/admin/templates/admin/base.html
+++ b/django/contrib/admin/templates/admin/base.html
@@ -22,14 +22,7 @@
{% block branding %}{% endblock %}
</div>
{% if user.is_authenticated and user.is_staff %}
- <div id="user-tools">
- {% trans 'Welcome,' %} <strong>{% if user.first_name %}{{ user.first_name|escape }}{% else %}{{ user.username }}{% endif %}</strong>.
- {% block userlinks %}
- <a href="{% url django.contrib.admin.views.doc.doc_index %}">{% trans 'Documentation' %}</a>
- / <a href="{% url django.contrib.auth.views.password_change %}">{% trans 'Change password' %}</a>
- / <a href="{% url django.contrib.auth.views.logout %}">{% trans 'Log out' %}</a>
- {% endblock %}
- </div>
+ <div id="user-tools">{% trans 'Welcome,' %} <strong>{% if user.first_name %}{{ user.first_name|escape }}{% else %}{{ user.username }}{% endif %}</strong>. {% block userlinks %}<a href="{{ root_path }}doc/">{% trans 'Documentation' %}</a> / <a href="{{ root_path }}password_change/">{% trans 'Change password' %}</a> / <a href="{{ root_path }}logout/">{% trans 'Log out' %}</a>{% endblock %}</div>
{% endif %}
{% block nav-global %}{% endblock %}
</div>
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
index d540cf6e79..e8df6b99f5 100644
--- a/django/contrib/admin/templates/admin/change_form.html
+++ b/django/contrib/admin/templates/admin/change_form.html
@@ -1,12 +1,17 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_modify adminmedia %}
+
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="../../../jsi18n/"></script>
-{% for js in javascript_imports %}{% include_admin_script js %}{% endfor %}
+{{ media }}
{% endblock %}
+
{% block stylesheet %}{% admin_media_prefix %}css/forms.css{% endblock %}
+
{% block coltype %}{% if ordered_objects %}colMS{% else %}colM{% endif %}{% endblock %}
+
{% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %}
+
{% block breadcrumbs %}{% if not is_popup %}
<div class="breadcrumbs">
<a href="../../../">{% trans "Home" %}</a> &rsaquo;
@@ -14,6 +19,7 @@
{% if add %}{% trans "Add" %} {{ opts.verbose_name }}{% else %}{{ original|truncatewords:"18" }}{% endif %}
</div>
{% endif %}{% endblock %}
+
{% block content %}<div id="content-main">
{% block object-tools %}
{% if change %}{% if not is_popup %}
@@ -25,45 +31,48 @@
<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% block form_top %}{% endblock %}
<div>
{% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %}
-{% if opts.admin.save_on_top %}{% submit_row %}{% endif %}
-{% if form.error_dict %}
+{% if save_on_top %}{% submit_row %}{% endif %}
+{% if errors %}
<p class="errornote">
- {% blocktrans count form.error_dict.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
+ {% blocktrans count errors.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
</p>
+ <ul class="errorlist">{% for error in adminform.form.non_field_errors %}<li>{{ error }}</li>{% endfor %}</ul>
{% endif %}
-{% for bound_field_set in bound_field_sets %}
- <fieldset class="module aligned {{ bound_field_set.classes }}">
- {% if bound_field_set.name %}<h2>{{ bound_field_set.name }}</h2>{% endif %}
- {% if bound_field_set.description %}<div class="description">{{ bound_field_set.description|safe }}</div>{% endif %}
- {% for bound_field_line in bound_field_set %}
- {% admin_field_line bound_field_line %}
- {% for bound_field in bound_field_line %}
- {% filter_interface_script_maybe bound_field %}
- {% endfor %}
- {% endfor %}
- </fieldset>
+
+{% for fieldset in adminform %}
+ {% include "admin/includes/fieldset.html" %}
{% endfor %}
+
{% block after_field_sets %}{% endblock %}
-{% if change %}
- {% if ordered_objects %}
- <fieldset class="module"><h2>{% trans "Ordering" %}</h2>
- <div class="form-row{% if form.order_.errors %} error{% endif %} ">
- {% if form.order_.errors %}{{ form.order_.html_error_list }}{% endif %}
- <p><label for="id_order_">{% trans "Order:" %}</label> {{ form.order_ }}</p>
- </div></fieldset>
- {% endif %}
-{% endif %}
-{% for related_object in inline_related_objects %}{% edit_inline related_object %}{% endfor %}
+
+{% for inline_admin_formset in inline_admin_formsets %}
+ {% include inline_admin_formset.opts.template %}
+{% endfor %}
+
{% block after_related_objects %}{% endblock %}
+
{% submit_row %}
+
{% if add %}
- <script type="text/javascript">document.getElementById("{{ first_form_field_id }}").focus();</script>
+ <script type="text/javascript">document.getElementById("{{ adminform.first_field.auto_id }}").focus();</script>
{% endif %}
-{% if auto_populated_fields %}
- <script type="text/javascript">
- {% auto_populated_field_script auto_populated_fields change %}
- </script>
+
+{# JavaScript for prepopulated fields #}
+
+{% if add %}
+<script type="text/javascript">
+{% for field in adminform.prepopulated_fields %}
+ document.getElementById("{{ field.field.auto_id }}").onchange = function() { this._changed = true; };
+ {% for dependency in field.dependencies %}
+ document.getElementById("{{ dependency.auto_id }}").onkeyup = function() {
+ var e = document.getElementById("{{ field.field.auto_id }}");
+ if (!e._changed) { e.value = URLify({% for innerdep in field.dependencies %}document.getElementById("{{ innerdep.auto_id }}").value{% if not forloop.last %} + ' ' + {% endif %}{% endfor %}, {{ field.field.field.max_length }}); }
+ }
+ {% endfor %}
+{% endfor %}
+</script>
{% endif %}
+
</div>
</form></div>
{% endblock %}
diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html
index 611c5ff8fc..24286a51a7 100644
--- a/django/contrib/admin/templates/admin/change_list.html
+++ b/django/contrib/admin/templates/admin/change_list.html
@@ -1,9 +1,14 @@
{% extends "admin/base_site.html" %}
{% load adminmedia admin_list i18n %}
+
{% block stylesheet %}{% admin_media_prefix %}css/changelists.css{% endblock %}
+
{% block bodyclass %}change-list{% endblock %}
+
{% if not is_popup %}{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans "Home" %}</a> &rsaquo; {{ cl.opts.verbose_name_plural|capfirst|escape }}</div>{% endblock %}{% endif %}
+
{% block coltype %}flex{% endblock %}
+
{% block content %}
<div id="content-main">
{% block object-tools %}
@@ -14,7 +19,18 @@
<div class="module{% if cl.has_filters %} filtered{% endif %}" id="changelist">
{% block search %}{% search_form cl %}{% endblock %}
{% block date_hierarchy %}{% date_hierarchy cl %}{% endblock %}
-{% block filters %}{% filters cl %}{% endblock %}
+
+{% block filters %}
+{% if cl.has_filters %}
+<div id="changelist-filter">
+<h2>{% trans 'Filter' %}</h2>
+{% for spec in cl.filter_specs %}
+ {% admin_list_filter cl spec %}
+{% endfor %}
+</div>
+{% endif %}
+{% endblock %}
+
{% block result_list %}{% result_list cl %}{% endblock %}
{% block pagination %}{% pagination cl %}{% endblock %}
</div>
diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html
index f2126882fa..386e134b96 100644
--- a/django/contrib/admin/templates/admin/delete_confirmation.html
+++ b/django/contrib/admin/templates/admin/delete_confirmation.html
@@ -1,5 +1,6 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
+
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="../../../../">{% trans "Home" %}</a> &rsaquo;
@@ -8,6 +9,7 @@
{% trans 'Delete' %}
</div>
{% endblock %}
+
{% block content %}
{% if perms_lacking %}
<p>{% blocktrans with object|escape as escaped_object %}Deleting the {{ object_name }} '{{ escaped_object }}' would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}</p>
diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html
new file mode 100644
index 0000000000..c726b0fcda
--- /dev/null
+++ b/django/contrib/admin/templates/admin/edit_inline/stacked.html
@@ -0,0 +1,26 @@
+{% load i18n %}
+<div class="inline-group">
+{{ inline_admin_formset.formset.management_form }}
+{# <h3 class="header">{{ inline_admin_formset.opts.verbose_name_plural|title }}</h3> #}
+{{ inline_admin_formset.formset.non_form_errors }}
+
+{% for inline_admin_form in inline_admin_formset %}
+<div class="inline-related {% if forloop.last %}last-related{% endif %}">
+ <h2><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>&nbsp;{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %} #{{ forloop.counter }}{% endif %}
+ {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
+ </h2>
+ {% if inline_admin_form.show_url %}
+ <p><a href="/r/{{ inline_admin_form.original.content_type_id }}/{{ inline_admin_form.original.id }}/">View on site</a></p>
+ {% endif %}
+
+ {% for fieldset in inline_admin_form %}
+ {% include "admin/includes/fieldset.html" %}
+ {% endfor %}
+ {{ inline_admin_form.pk_field.field }}
+</div>
+{% endfor %}
+
+{# <ul class="tools"> #}
+{# <li><a class="add" href="">Add another {{ inline_admin_formset.opts.verbose_name|title }}</a></li> #}
+{# </ul> #}
+</div>
diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html
new file mode 100644
index 0000000000..f6332fafbc
--- /dev/null
+++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html
@@ -0,0 +1,64 @@
+{% load i18n %}
+<div class="inline-group">
+ <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
+{{ inline_admin_formset.formset.management_form }}
+<fieldset class="module">
+ <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst|escape }}</h2>
+ {{ inline_admin_formset.formset.non_form_errors }}
+ <table>
+ <thead><tr>
+ {% for field in inline_admin_formset.fields %}
+ {% if not field.is_hidden %}
+ <th {% if forloop.first %}colspan="2"{% endif %}>{{ field.label|capfirst|escape }}</th>
+ {% endif %}
+ {% endfor %}
+ {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete" %}?</th>{% endif %}
+ </tr></thead>
+
+ {% for inline_admin_form in inline_admin_formset %}
+
+ <tr class="{% cycle row1,row2 %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}">
+
+ <td class="original">{% if inline_admin_form.original or inline_admin_form.show_url %}<p>
+ {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %}
+ {% if inline_admin_form.show_url %}<a href="/r/{{ inline_admin_form.original.content_type_id }}/{{ inline_admin_form.original.id }}/">View on site</a>{% endif %}
+ </p>{% endif %}
+ {{ inline_admin_form.pk_field.field }}
+ {% spaceless %}
+ {% for fieldset in inline_admin_form %}
+ {% for line in fieldset %}
+ {% for field in line %}
+ {% if field.is_hidden %} {{ field.field }} {% endif %}
+ {% endfor %}
+ {% endfor %}
+ {% endfor %}
+ {% endspaceless %}
+ </td>
+
+ {% for fieldset in inline_admin_form %}
+ {% for line in fieldset %}
+ {% for field in line %}
+ <td class="{{ field.field.name }}">
+ {{ field.field.errors.as_ul }}
+ {{ field.field }}
+ </td>
+ {% endfor %}
+ {% endfor %}
+ {% endfor %}
+
+ {% if inline_admin_formset.formset.can_delete %}<td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>{% endif %}
+
+ </tr>
+
+ {% endfor %}
+
+ </table>
+
+</fieldset>
+ </div>
+
+ {# <ul class="tools"> #}
+ {# <li><a class="add" href="">Add another {{ inline_admin_formset.opts.verbose_name|title }}</a></li> #}
+ {# </ul> #}
+
+</div>
diff --git a/django/contrib/admin/templates/admin/edit_inline_stacked.html b/django/contrib/admin/templates/admin/edit_inline_stacked.html
deleted file mode 100644
index 45aa0a4f58..0000000000
--- a/django/contrib/admin/templates/admin/edit_inline_stacked.html
+++ /dev/null
@@ -1,16 +0,0 @@
-{% load admin_modify %}
-<fieldset class="module aligned">
- {% for fcw in bound_related_object.form_field_collection_wrappers %}
- <h2>{{ bound_related_object.relation.opts.verbose_name|capfirst }}&nbsp;#{{ forloop.counter }}</h2>
- {% if bound_related_object.show_url %}{% if fcw.obj.original %}
- <p><a href="/r/{{ fcw.obj.original.content_type_id }}/{{ fcw.obj.original.id }}/">View on site</a></p>
- {% endif %}{% endif %}
- {% for bound_field in fcw.bound_fields %}
- {% if bound_field.hidden %}
- {% field_widget bound_field %}
- {% else %}
- {% admin_field_line bound_field %}
- {% endif %}
- {% endfor %}
- {% endfor %}
-</fieldset>
diff --git a/django/contrib/admin/templates/admin/edit_inline_tabular.html b/django/contrib/admin/templates/admin/edit_inline_tabular.html
deleted file mode 100644
index e2dbcbaed2..0000000000
--- a/django/contrib/admin/templates/admin/edit_inline_tabular.html
+++ /dev/null
@@ -1,44 +0,0 @@
-{% load admin_modify %}
-<fieldset class="module">
- <h2>{{ bound_related_object.relation.opts.verbose_name_plural|capfirst }}</h2><table>
- <thead><tr>
- {% for fw in bound_related_object.field_wrapper_list %}
- {% if fw.needs_header %}
- <th{{ fw.header_class_attribute }}>{{ fw.field.verbose_name|capfirst }}</th>
- {% endif %}
- {% endfor %}
- </tr></thead>
- {% for fcw in bound_related_object.form_field_collection_wrappers %}
- {% if change %}{% if original_row_needed %}
- {% if fcw.obj.original %}
- <tr class="row-label {% cycle row1,row2 %}"><td colspan="{{ num_headers }}"><strong>{{ fcw.obj.original }}</strong></tr>
- {% endif %}
- {% endif %}{% endif %}
- {% if fcw.obj.errors %}
- <tr class="errorlist"><td colspan="{{ num_headers }}">
- {{ fcw.obj.html_combined_error_list }}
- </tr>
- {% endif %}
- <tr class="{% cycle row1,row2 %}">
- {% for bound_field in fcw.bound_fields %}
- {% if not bound_field.hidden %}
- <td {{ bound_field.cell_class_attribute }}>
- {% field_widget bound_field %}
- </td>
- {% endif %}
- {% endfor %}
- {% if bound_related_object.show_url %}<td>
- {% if fcw.obj.original %}<a href="/r/{{ fcw.obj.original.content_type_id }}/{{ fcw.obj.original.id }}/">View on site</a>{% endif %}
- </td>{% endif %}
- </tr>
-
- {% endfor %} </table>
-
- {% for fcw in bound_related_object.form_field_collection_wrappers %}
- {% for bound_field in fcw.bound_fields %}
- {% if bound_field.hidden %}
- {% field_widget bound_field %}
- {% endif %}
- {% endfor %}
- {% endfor %}
-</fieldset>
diff --git a/django/contrib/admin/templates/admin/field_line.html b/django/contrib/admin/templates/admin/field_line.html
deleted file mode 100644
index f4b53fff67..0000000000
--- a/django/contrib/admin/templates/admin/field_line.html
+++ /dev/null
@@ -1,10 +0,0 @@
-{% load admin_modify %}
-<div class="{{ class_names }}" >
-{% for bound_field in bound_fields %}{{ bound_field.html_error_list }}{% endfor %}
-{% for bound_field in bound_fields %}
- {% if bound_field.has_label_first %}{% field_label bound_field %}{% endif %}
- {% field_widget bound_field %}
- {% if not bound_field.has_label_first %}{% field_label bound_field %}{% endif %}
- {% if bound_field.field.help_text %}<p class="help">{{ bound_field.field.help_text|safe }}</p>{% endif %}
-{% endfor %}
-</div>
diff --git a/django/contrib/admin/templates/admin/filters.html b/django/contrib/admin/templates/admin/filters.html
deleted file mode 100644
index 3ca763cce3..0000000000
--- a/django/contrib/admin/templates/admin/filters.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{% load admin_list %}
-{% load i18n %}
-{% if cl.has_filters %}<div id="changelist-filter">
-<h2>{% trans 'Filter' %}</h2>
-{% for spec in cl.filter_specs %}
- {% filter cl spec %}
-{% endfor %}</div>{% endif %}
diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html
new file mode 100644
index 0000000000..a61795cfe4
--- /dev/null
+++ b/django/contrib/admin/templates/admin/includes/fieldset.html
@@ -0,0 +1,17 @@
+<fieldset class="module aligned {{ fieldset.classes }}">
+ {% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
+ {% if fieldset.description %}<div class="description">{{ fieldset.description }}</div>{% endif %}
+ {% for line in fieldset %}
+ <div class="form-row{% if line.errors %} errors{% endif %} {% for field in line %}{{ field.field.name }} {% endfor %} ">
+ {{ line.errors }}
+ {% for field in line %}
+ {% if field.is_checkbox %}
+ {{ field.field }}{{ field.label_tag }}
+ {% else %}
+ {{ field.label_tag }}{{ field.field }}
+ {% endif %}
+ {% if field.field.field.help_text %}<p class="help">{{ field.field.field.help_text|safe }}</p>{% endif %}
+ {% endfor %}
+ </div>
+ {% endfor %}
+</fieldset> \ No newline at end of file
diff --git a/django/contrib/admin/templates/admin/index.html b/django/contrib/admin/templates/admin/index.html
index 2f406a1754..074be03208 100644
--- a/django/contrib/admin/templates/admin/index.html
+++ b/django/contrib/admin/templates/admin/index.html
@@ -2,15 +2,16 @@
{% load i18n %}
{% block stylesheet %}{% load adminmedia %}{% admin_media_prefix %}css/dashboard.css{% endblock %}
+
{% block coltype %}colMS{% endblock %}
+
{% block bodyclass %}dashboard{% endblock %}
+
{% block breadcrumbs %}{% endblock %}
+
{% block content %}
<div id="content-main">
-{% load adminapplist %}
-
-{% get_admin_app_list as app_list %}
{% if app_list %}
{% for app in app_list %}
<div class="module">
diff --git a/django/contrib/admin/templates/admin/invalid_setup.html b/django/contrib/admin/templates/admin/invalid_setup.html
index 1fa0d32358..f09b316b06 100644
--- a/django/contrib/admin/templates/admin/invalid_setup.html
+++ b/django/contrib/admin/templates/admin/invalid_setup.html
@@ -4,7 +4,5 @@
{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans 'Home' %}</a> &rsaquo; {{ title }}</div>{% endblock %}
{% block content %}
-
<p>{% 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." %}</p>
-
{% endblock %}
diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html
index 0773132cec..5dd953bc23 100644
--- a/django/contrib/admin/templates/admin/login.html
+++ b/django/contrib/admin/templates/admin/login.html
@@ -2,12 +2,14 @@
{% load i18n %}
{% block stylesheet %}{% load adminmedia %}{% admin_media_prefix %}css/login.css{% endblock %}
+
{% block bodyclass %}login{% endblock %}
+
{% block content_title %}{% endblock %}
+
{% block breadcrumbs %}{% endblock %}
{% block content %}
-
{% if error_message %}
<p class="errornote">{{ error_message }}</p>
{% endif %}
diff --git a/django/contrib/admin/templates/admin/object_history.html b/django/contrib/admin/templates/admin/object_history.html
index 00e47259bf..19c037cda9 100644
--- a/django/contrib/admin/templates/admin/object_history.html
+++ b/django/contrib/admin/templates/admin/object_history.html
@@ -1,16 +1,15 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
+
{% block breadcrumbs %}
<div class="breadcrumbs"><a href="../../../../">{% trans 'Home' %}</a> &rsaquo; <a href="../../">{{ module_name }}</a> &rsaquo; <a href="../">{{ object|truncatewords:"18" }}</a> &rsaquo; {% trans 'History' %}</div>
{% endblock %}
{% block content %}
-
<div id="content-main">
<div class="module">
{% if action_list %}
-
<table id="change-history">
<thead>
<tr>
@@ -29,14 +28,9 @@
{% endfor %}
</tbody>
</table>
-
{% else %}
-
<p>{% trans "This object doesn't have a change history. It probably wasn't added via this admin site." %}</p>
-
{% endif %}
-
</div>
</div>
-
{% endblock %}
diff --git a/django/contrib/admin/templates/admin/search_form.html b/django/contrib/admin/templates/admin/search_form.html
index 445cca3089..b232aa917d 100644
--- a/django/contrib/admin/templates/admin/search_form.html
+++ b/django/contrib/admin/templates/admin/search_form.html
@@ -1,6 +1,6 @@
{% load adminmedia %}
{% load i18n %}
-{% if cl.lookup_opts.admin.search_fields %}
+{% if cl.search_fields %}
<div id="toolbar"><form id="changelist-search" action="" method="get">
<div><!-- DIV needed for valid HTML -->
<label for="searchbar"><img src="{% admin_media_prefix %}img/admin/icon_searchbox.png" alt="Search" /></label>
diff --git a/django/contrib/admin/templates/admin_doc/index.html b/django/contrib/admin/templates/admin_doc/index.html
index 750dd2f5ac..242fc7339a 100644
--- a/django/contrib/admin/templates/admin_doc/index.html
+++ b/django/contrib/admin/templates/admin_doc/index.html
@@ -25,3 +25,4 @@
</div>
{% endblock %}
+
diff --git a/django/contrib/admin/templates/admin_doc/view_index.html b/django/contrib/admin/templates/admin_doc/view_index.html
index 716e2d1a91..4099005828 100644
--- a/django/contrib/admin/templates/admin_doc/view_index.html
+++ b/django/contrib/admin/templates/admin_doc/view_index.html
@@ -40,3 +40,4 @@
</div>
{% endblock %}
+
diff --git a/django/contrib/admin/templates/registration/password_change_done.html b/django/contrib/admin/templates/registration/password_change_done.html
index 4498c63a37..252572001d 100644
--- a/django/contrib/admin/templates/registration/password_change_done.html
+++ b/django/contrib/admin/templates/registration/password_change_done.html
@@ -1,5 +1,6 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
+{% block userlinks %}<a href="../../doc/">{% trans 'Documentation' %}</a> / {% trans 'Change password' %} / <a href="../../logout/">{% trans 'Log out' %}</a>{% endblock %}
{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans 'Home' %}</a> &rsaquo; {% trans 'Password change' %}</div>{% 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 cd2b1e0c8a..036d56212c 100644
--- a/django/contrib/admin/templates/registration/password_change_form.html
+++ b/django/contrib/admin/templates/registration/password_change_form.html
@@ -1,5 +1,6 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
+{% block userlinks %}<a href="../doc/">{% trans 'Documentation' %}</a> / {% trans 'Change password' %} / <a href="../logout/">{% trans 'Log out' %}</a>{% endblock %}
{% block breadcrumbs %}<div class="breadcrumbs"><a href="../">{% trans 'Home' %}</a> &rsaquo; {% trans 'Password change' %}</div>{% endblock %}
{% block title %}{% trans 'Password change' %}{% endblock %}
diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html
index 423821ba60..d8c7d03f93 100644
--- a/django/contrib/admin/templates/registration/password_reset_form.html
+++ b/django/contrib/admin/templates/registration/password_reset_form.html
@@ -12,7 +12,7 @@
<p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll reset your password and e-mail the new one to you." %}</p>
<form action="" method="post">
-{% if form.email.errors %}{{ form.email.html_error_list }}{% endif %}
+{% if form.email.errors %}{{ form.email.errors }}{% endif %}
<p><label for="id_email">{% trans 'E-mail address:' %}</label> {{ form.email }} <input type="submit" value="{% trans 'Reset my password' %}" /></p>
</form>
diff --git a/django/contrib/admin/templates/widget/date_time.html b/django/contrib/admin/templates/widget/date_time.html
deleted file mode 100644
index cbd4a2e1c6..0000000000
--- a/django/contrib/admin/templates/widget/date_time.html
+++ /dev/null
@@ -1,5 +0,0 @@
-{% load i18n %}
-<p class="datetime">
- {% trans "Date:" %} {{ bound_field.form_fields.0 }}<br />
- {% trans "Time:" %} {{ bound_field.form_fields.1 }}
-</p>
diff --git a/django/contrib/admin/templates/widget/default.html b/django/contrib/admin/templates/widget/default.html
deleted file mode 100644
index 0af231ddcb..0000000000
--- a/django/contrib/admin/templates/widget/default.html
+++ /dev/null
@@ -1 +0,0 @@
-{% load admin_modify %}{% output_all bound_field.form_fields %}
diff --git a/django/contrib/admin/templates/widget/file.html b/django/contrib/admin/templates/widget/file.html
deleted file mode 100644
index e584abf956..0000000000
--- a/django/contrib/admin/templates/widget/file.html
+++ /dev/null
@@ -1,4 +0,0 @@
-{% load admin_modify i18n %}{% if bound_field.original_value %}
-{% trans "Currently:" %} <a href="{{ bound_field.original_url }}" > {{ bound_field.original_value|escape }} </a><br />
-{% trans "Change:" %}{% output_all bound_field.form_fields %}
-{% else %} {% output_all bound_field.form_fields %} {% endif %}
diff --git a/django/contrib/admin/templates/widget/foreign.html b/django/contrib/admin/templates/widget/foreign.html
deleted file mode 100644
index 6b43d044bd..0000000000
--- a/django/contrib/admin/templates/widget/foreign.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% load admin_modify adminmedia %}
-{% output_all bound_field.form_fields %}
-{% if bound_field.raw_id_admin %}
- {% if bound_field.field.rel.limit_choices_to %}
- <a href="{{ bound_field.related_url }}?{% for limit_choice in bound_field.field.rel.limit_choices_to.items %}{% if not forloop.first %}&amp;{% endif %}{{ limit_choice|join:"=" }}{% endfor %}" class="related-lookup" id="lookup_{{ bound_field.element_id }}" onclick="return showRelatedObjectLookupPopup(this);"> <img src="{% admin_media_prefix %}img/admin/selector-search.gif" width="16" height="16" alt="Lookup"></a>
- {% else %}
- <a href="{{ bound_field.related_url }}" class="related-lookup" id="lookup_{{ bound_field.element_id }}" onclick="return showRelatedObjectLookupPopup(this);"> <img src="{% admin_media_prefix %}img/admin/selector-search.gif" width="16" height="16" alt="Lookup"></a>
- {% endif %}
-{% else %}
-{% if bound_field.needs_add_label %}
- <a href="{{ bound_field.related_url }}add/" class="add-another" id="add_{{ bound_field.element_id }}" onclick="return showAddAnotherPopup(this);"> <img src="{% admin_media_prefix %}img/admin/icon_addlink.gif" width="10" height="10" alt="Add Another"/></a>
-{% endif %}{% endif %}
-{% if change %}
- {% if bound_field.field.primary_key %}
- {{ bound_field.original_value }}
- {% endif %}
- {% if bound_field.raw_id_admin %}
- {% if bound_field.existing_display %}&nbsp;<strong>{{ bound_field.existing_display|truncatewords:"14" }}</strong>{% endif %}
- {% endif %}
-{% endif %}
diff --git a/django/contrib/admin/templates/widget/many_to_many.html b/django/contrib/admin/templates/widget/many_to_many.html
deleted file mode 100644
index a93aa65f73..0000000000
--- a/django/contrib/admin/templates/widget/many_to_many.html
+++ /dev/null
@@ -1 +0,0 @@
-{% include "widget/foreign.html" %}
diff --git a/django/contrib/admin/templates/widget/one_to_one.html b/django/contrib/admin/templates/widget/one_to_one.html
deleted file mode 100644
index a79a12314f..0000000000
--- a/django/contrib/admin/templates/widget/one_to_one.html
+++ /dev/null
@@ -1,2 +0,0 @@
-{% if add %}{% include "widget/foreign.html" %}{% endif %}
-{% if change %}{% if bound_field.existing_display %}&nbsp;<strong>{{ bound_field.existing_display|truncatewords:"14" }}</strong>{% endif %}{% endif %}
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index 6db59ea338..87fad70ec3 100644
--- a/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -71,7 +71,7 @@ pagination = register.inclusion_tag('admin/pagination.html')(pagination)
def result_headers(cl):
lookup_opts = cl.lookup_opts
- for i, field_name in enumerate(lookup_opts.admin.list_display):
+ for i, field_name in enumerate(cl.list_display):
try:
f = lookup_opts.get_field(field_name)
admin_order_field = None
@@ -123,7 +123,7 @@ def _boolean_icon(field_val):
def items_for_result(cl, result):
first = True
pk = cl.lookup_opts.pk.attname
- for field_name in cl.lookup_opts.admin.list_display:
+ for field_name in cl.list_display:
row_class = ''
try:
f = cl.lookup_opts.get_field(field_name)
@@ -189,7 +189,7 @@ def items_for_result(cl, result):
if force_unicode(result_repr) == '':
result_repr = mark_safe('&nbsp;')
# If list_display_links not defined, add the link tag to the first field
- if (first and not cl.lookup_opts.admin.list_display_links) or field_name in cl.lookup_opts.admin.list_display_links:
+ if (first and not cl.list_display_links) or field_name in cl.list_display_links:
table_tag = {True:'th', False:'td'}[first]
first = False
url = cl.url_for_result(result)
@@ -212,8 +212,8 @@ def result_list(cl):
result_list = register.inclusion_tag("admin/change_list_results.html")(result_list)
def date_hierarchy(cl):
- if cl.lookup_opts.admin.date_hierarchy:
- field_name = cl.lookup_opts.admin.date_hierarchy
+ if cl.date_hierarchy:
+ field_name = cl.date_hierarchy
year_field = '%s__year' % field_name
month_field = '%s__month' % field_name
day_field = '%s__day' % field_name
@@ -280,10 +280,6 @@ def search_form(cl):
}
search_form = register.inclusion_tag('admin/search_form.html')(search_form)
-def filter(cl, spec):
+def admin_list_filter(cl, spec):
return {'title': spec.title(), 'choices' : list(spec.choices(cl))}
-filter = register.inclusion_tag('admin/filter.html')(filter)
-
-def filters(cl):
- return {'cl': cl}
-filters = register.inclusion_tag('admin/filters.html')(filters)
+admin_list_filter = register.inclusion_tag('admin/filter.html')(admin_list_filter)
diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py
index ef33bb33b0..25d2d6774a 100644
--- a/django/contrib/admin/templatetags/admin_modify.py
+++ b/django/contrib/admin/templatetags/admin_modify.py
@@ -1,253 +1,21 @@
from django import template
-from django.contrib.admin.views.main import AdminBoundField
-from django.template import loader
-from django.utils.text import capfirst
-from django.utils.encoding import force_unicode
-from django.utils.safestring import mark_safe
-from django.utils.html import escape
-from django.db import models
-from django.db.models.fields import Field
-from django.db.models.related import BoundRelatedObject
-from django.conf import settings
-import re
register = template.Library()
-word_re = re.compile('[A-Z][a-z]+')
-absolute_url_re = re.compile(r'^(?:http(?:s)?:/)?/', re.IGNORECASE)
-
-def class_name_to_underscored(name):
- return u'_'.join([s.lower() for s in word_re.findall(name)[:-1]])
-
-def include_admin_script(script_path):
- """
- Returns an HTML script element for including a script from the admin
- media url (or other location if an absolute url is given).
-
- Example usage::
-
- {% include_admin_script "js/calendar.js" %}
-
- could return::
-
- <script type="text/javascript" src="/media/admin/js/calendar.js">
- """
- if not absolute_url_re.match(script_path):
- script_path = '%s%s' % (settings.ADMIN_MEDIA_PREFIX, script_path)
- return mark_safe(u'<script type="text/javascript" src="%s"></script>'
- % script_path)
-include_admin_script = register.simple_tag(include_admin_script)
-
def submit_row(context):
opts = context['opts']
change = context['change']
is_popup = context['is_popup']
+ save_as = context['save_as']
return {
'onclick_attrib': (opts.get_ordered_objects() and change
and 'onclick="submitOrderForm();"' or ''),
'show_delete_link': (not is_popup and context['has_delete_permission']
and (change or context['show_delete'])),
- 'show_save_as_new': not is_popup and change and opts.admin.save_as,
- 'show_save_and_add_another': not is_popup and (not opts.admin.save_as or context['add']),
+ 'show_save_as_new': not is_popup and change and save_as,
+ 'show_save_and_add_another': context['has_add_permission'] and
+ not is_popup and (not save_as or context['add']),
'show_save_and_continue': not is_popup and context['has_change_permission'],
'show_save': True
}
submit_row = register.inclusion_tag('admin/submit_line.html', takes_context=True)(submit_row)
-
-def field_label(bound_field):
- class_names = []
- if isinstance(bound_field.field, models.BooleanField):
- class_names.append("vCheckboxLabel")
- colon = ""
- else:
- if not bound_field.field.blank:
- class_names.append('required')
- if not bound_field.first:
- class_names.append('inline')
- colon = ":"
- class_str = class_names and u' class="%s"' % u' '.join(class_names) or u''
- return mark_safe(u'<label for="%s"%s>%s%s</label> ' %
- (bound_field.element_id, class_str,
- escape(force_unicode(capfirst(bound_field.field.verbose_name))),
- colon))
-field_label = register.simple_tag(field_label)
-
-class FieldWidgetNode(template.Node):
- nodelists = {}
- default = None
-
- def __init__(self, bound_field_var):
- self.bound_field_var = template.Variable(bound_field_var)
-
- def get_nodelist(cls, klass):
- if klass not in cls.nodelists:
- try:
- field_class_name = klass.__name__
- template_name = u"widget/%s.html" % class_name_to_underscored(field_class_name)
- nodelist = loader.get_template(template_name).nodelist
- except template.TemplateDoesNotExist:
- super_klass = bool(klass.__bases__) and klass.__bases__[0] or None
- if super_klass and super_klass != Field:
- nodelist = cls.get_nodelist(super_klass)
- else:
- if not cls.default:
- cls.default = loader.get_template("widget/default.html").nodelist
- nodelist = cls.default
-
- cls.nodelists[klass] = nodelist
- return nodelist
- else:
- return cls.nodelists[klass]
- get_nodelist = classmethod(get_nodelist)
-
- def render(self, context):
- bound_field = self.bound_field_var.resolve(context)
-
- context.push()
- context['bound_field'] = bound_field
-
- output = self.get_nodelist(bound_field.field.__class__).render(context)
- context.pop()
- return output
-
-class FieldWrapper(object):
- def __init__(self, field ):
- self.field = field
-
- def needs_header(self):
- return not isinstance(self.field, models.AutoField)
-
- def header_class_attribute(self):
- return self.field.blank and mark_safe(' class="optional"') or ''
-
- def use_raw_id_admin(self):
- return isinstance(self.field.rel, (models.ManyToOneRel, models.ManyToManyRel)) \
- and self.field.rel.raw_id_admin
-
-class FormFieldCollectionWrapper(object):
- def __init__(self, field_mapping, fields, index):
- self.field_mapping = field_mapping
- self.fields = fields
- self.bound_fields = [AdminBoundField(field, self.field_mapping, field_mapping['original'])
- for field in self.fields]
- self.index = index
-
-class TabularBoundRelatedObject(BoundRelatedObject):
- def __init__(self, related_object, field_mapping, original):
- super(TabularBoundRelatedObject, self).__init__(related_object, field_mapping, original)
- self.field_wrapper_list = [FieldWrapper(field) for field in self.relation.editable_fields()]
-
- fields = self.relation.editable_fields()
-
- self.form_field_collection_wrappers = [FormFieldCollectionWrapper(field_mapping, fields, i)
- for (i,field_mapping) in self.field_mappings.items() ]
- self.original_row_needed = max([fw.use_raw_id_admin() for fw in self.field_wrapper_list])
- self.show_url = original and hasattr(self.relation.opts, 'get_absolute_url')
-
- def template_name(self):
- return "admin/edit_inline_tabular.html"
-
-class StackedBoundRelatedObject(BoundRelatedObject):
- def __init__(self, related_object, field_mapping, original):
- super(StackedBoundRelatedObject, self).__init__(related_object, field_mapping, original)
- fields = self.relation.editable_fields()
- self.field_mappings.fill()
- self.form_field_collection_wrappers = [FormFieldCollectionWrapper(field_mapping ,fields, i)
- for (i,field_mapping) in self.field_mappings.items()]
- self.show_url = original and hasattr(self.relation.opts, 'get_absolute_url')
-
- def template_name(self):
- return "admin/edit_inline_stacked.html"
-
-class EditInlineNode(template.Node):
- def __init__(self, rel_var):
- self.rel_var = template.Variable(rel_var)
-
- def render(self, context):
- relation = self.rel_var.resolve(context)
- context.push()
- if relation.field.rel.edit_inline == models.TABULAR:
- bound_related_object_class = TabularBoundRelatedObject
- elif relation.field.rel.edit_inline == models.STACKED:
- bound_related_object_class = StackedBoundRelatedObject
- else:
- bound_related_object_class = relation.field.rel.edit_inline
- original = context.get('original', None)
- bound_related_object = relation.bind(context['form'], original, bound_related_object_class)
- context['bound_related_object'] = bound_related_object
- t = loader.get_template(bound_related_object.template_name())
- output = t.render(context)
- context.pop()
- return output
-
-def output_all(form_fields):
- return u''.join([force_unicode(f) for f in form_fields])
-output_all = register.simple_tag(output_all)
-
-def auto_populated_field_script(auto_pop_fields, change = False):
- t = []
- for field in auto_pop_fields:
- if change:
- t.append(u'document.getElementById("id_%s")._changed = true;' % field.name)
- else:
- t.append(u'document.getElementById("id_%s").onchange = function() { this._changed = true; };' % field.name)
-
- add_values = u' + " " + '.join([u'document.getElementById("id_%s").value' % g for g in field.prepopulate_from])
- for f in field.prepopulate_from:
- t.append(u'document.getElementById("id_%s").onkeyup = function() {' \
- ' var e = document.getElementById("id_%s");' \
- ' if(!e._changed) { e.value = URLify(%s, %s);} }; ' % (
- f, field.name, add_values, field.max_length))
- return mark_safe(u''.join(t))
-auto_populated_field_script = register.simple_tag(auto_populated_field_script)
-
-def filter_interface_script_maybe(bound_field):
- f = bound_field.field
- if f.rel and isinstance(f.rel, models.ManyToManyRel) and f.rel.filter_interface:
- return mark_safe(u'<script type="text/javascript">addEvent(window, "load", function(e) {' \
- ' SelectFilter.init("id_%s", "%s", %s, "%s"); });</script>\n' % (
- f.name, escape(f.verbose_name.replace('"', '\\"')), f.rel.filter_interface-1, settings.ADMIN_MEDIA_PREFIX))
- else:
- return ''
-filter_interface_script_maybe = register.simple_tag(filter_interface_script_maybe)
-
-def field_widget(parser, token):
- bits = token.contents.split()
- if len(bits) != 2:
- raise template.TemplateSyntaxError, "%s takes 1 argument" % bits[0]
- return FieldWidgetNode(bits[1])
-field_widget = register.tag(field_widget)
-
-def edit_inline(parser, token):
- bits = token.contents.split()
- if len(bits) != 2:
- raise template.TemplateSyntaxError, "%s takes 1 argument" % bits[0]
- return EditInlineNode(bits[1])
-edit_inline = register.tag(edit_inline)
-
-def admin_field_line(context, argument_val):
- if isinstance(argument_val, AdminBoundField):
- bound_fields = [argument_val]
- else:
- bound_fields = [bf for bf in argument_val]
- add = context['add']
- change = context['change']
-
- class_names = ['form-row']
- for bound_field in bound_fields:
- for f in bound_field.form_fields:
- if f.errors():
- class_names.append('errors')
- break
-
- # Assumes BooleanFields won't be stacked next to each other!
- if isinstance(bound_fields[0].field, models.BooleanField):
- class_names.append('checkbox-row')
-
- return {
- 'add': context['add'],
- 'change': context['change'],
- 'bound_fields': bound_fields,
- 'class_names': " ".join(class_names),
- }
-admin_field_line = register.inclusion_tag('admin/field_line.html', takes_context=True)(admin_field_line)
diff --git a/django/contrib/admin/templatetags/adminapplist.py b/django/contrib/admin/templatetags/adminapplist.py
deleted file mode 100644
index bf394e2099..0000000000
--- a/django/contrib/admin/templatetags/adminapplist.py
+++ /dev/null
@@ -1,81 +0,0 @@
-from django import template
-from django.db.models import get_models
-from django.utils.encoding import force_unicode
-from django.utils.safestring import mark_safe
-
-register = template.Library()
-
-class AdminApplistNode(template.Node):
- def __init__(self, varname):
- self.varname = varname
-
- def render(self, context):
- from django.db import models
- from django.utils.text import capfirst
- app_list = []
- user = context['user']
-
- for app in models.get_apps():
- # Determine the app_label.
- app_models = get_models(app)
- if not app_models:
- continue
- app_label = app_models[0]._meta.app_label
-
- has_module_perms = user.has_module_perms(app_label)
-
- if has_module_perms:
- model_list = []
- for m in app_models:
- if m._meta.admin:
- perms = {
- 'add': user.has_perm("%s.%s" % (app_label, m._meta.get_add_permission())),
- 'change': user.has_perm("%s.%s" % (app_label, m._meta.get_change_permission())),
- 'delete': user.has_perm("%s.%s" % (app_label, m._meta.get_delete_permission())),
- }
-
- # Check whether user has any perm for this module.
- # If so, add the module to the model_list.
- if True in perms.values():
- model_list.append({
- 'name': force_unicode(capfirst(m._meta.verbose_name_plural)),
- 'admin_url': mark_safe(u'%s/%s/' % (force_unicode(app_label), m.__name__.lower())),
- 'perms': perms,
- })
-
- if model_list:
- # Sort using verbose decorate-sort-undecorate pattern
- # instead of key argument to sort() for python 2.3 compatibility
- decorated = [(x['name'], x) for x in model_list]
- decorated.sort()
- model_list = [x for key, x in decorated]
-
- app_list.append({
- 'name': app_label.title(),
- 'has_module_perms': has_module_perms,
- 'models': model_list,
- })
- context[self.varname] = app_list
- return ''
-
-def get_admin_app_list(parser, token):
- """
- Returns a list of installed applications and models for which the current user
- has at least one permission.
-
- Syntax::
-
- {% get_admin_app_list as [context_var_containing_app_list] %}
-
- Example usage::
-
- {% get_admin_app_list as admin_app_list %}
- """
- tokens = token.contents.split()
- if len(tokens) < 3:
- raise template.TemplateSyntaxError, "'%s' tag requires two arguments" % tokens[0]
- if tokens[1] != 'as':
- raise template.TemplateSyntaxError, "First argument to '%s' tag must be 'as'" % tokens[0]
- return AdminApplistNode(tokens[2])
-
-register.tag('get_admin_app_list', get_admin_app_list)
diff --git a/django/contrib/admin/urls.py b/django/contrib/admin/urls.py
deleted file mode 100644
index 436e9b7b23..0000000000
--- a/django/contrib/admin/urls.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from django.conf import settings
-from django.conf.urls.defaults import *
-
-if settings.USE_I18N:
- i18n_view = 'django.views.i18n.javascript_catalog'
-else:
- i18n_view = 'django.views.i18n.null_javascript_catalog'
-
-urlpatterns = patterns('',
- ('^$', 'django.contrib.admin.views.main.index'),
- ('^r/', include('django.conf.urls.shortcut')),
- ('^jsi18n/$', i18n_view, {'packages': 'django.conf'}),
- ('^logout/$', 'django.contrib.auth.views.logout'),
- ('^password_change/$', 'django.contrib.auth.views.password_change'),
- ('^password_change/done/$', 'django.contrib.auth.views.password_change_done'),
- ('^template_validator/$', 'django.contrib.admin.views.template.template_validator'),
-
- # Documentation
- ('^doc/$', 'django.contrib.admin.views.doc.doc_index'),
- ('^doc/bookmarklets/$', 'django.contrib.admin.views.doc.bookmarklets'),
- ('^doc/tags/$', 'django.contrib.admin.views.doc.template_tag_index'),
- ('^doc/filters/$', 'django.contrib.admin.views.doc.template_filter_index'),
- ('^doc/views/$', 'django.contrib.admin.views.doc.view_index'),
- ('^doc/views/(?P<view>[^/]+)/$', 'django.contrib.admin.views.doc.view_detail'),
- ('^doc/models/$', 'django.contrib.admin.views.doc.model_index'),
- ('^doc/models/(?P<app_label>[^\.]+)\.(?P<model_name>[^/]+)/$', 'django.contrib.admin.views.doc.model_detail'),
-# ('^doc/templates/$', 'django.views.admin.doc.template_index'),
- ('^doc/templates/(?P<template>.*)/$', 'django.contrib.admin.views.doc.template_detail'),
-
- # "Add user" -- a special-case view
- ('^auth/user/add/$', 'django.contrib.admin.views.auth.user_add_stage'),
- # "Change user password" -- another special-case view
- ('^auth/user/(\d+)/password/$', 'django.contrib.admin.views.auth.user_change_password'),
-
- # Add/change/delete/history
- ('^([^/]+)/([^/]+)/$', 'django.contrib.admin.views.main.change_list'),
- ('^([^/]+)/([^/]+)/add/$', 'django.contrib.admin.views.main.add_stage'),
- ('^([^/]+)/([^/]+)/(.+)/history/$', 'django.contrib.admin.views.main.history'),
- ('^([^/]+)/([^/]+)/(.+)/delete/$', 'django.contrib.admin.views.main.delete_stage'),
- ('^([^/]+)/([^/]+)/(.+)/$', 'django.contrib.admin.views.main.change_stage'),
-)
-
-del i18n_view
diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
new file mode 100644
index 0000000000..6a585b2919
--- /dev/null
+++ b/django/contrib/admin/util.py
@@ -0,0 +1,139 @@
+from django.core.exceptions import ObjectDoesNotExist
+from django.db import models
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+from django.utils.text import capfirst
+from django.utils.encoding import force_unicode
+from django.utils.translation import ugettext as _
+
+
+def quote(s):
+ """
+ Ensure that primary key values do not confuse the admin URLs by escaping
+ any '/', '_' and ':' characters. Similar to urllib.quote, except that the
+ quoting is slightly different so that it doesn't get automatically
+ unquoted by the Web browser.
+ """
+ if not isinstance(s, basestring):
+ return s
+ res = list(s)
+ for i in range(len(res)):
+ c = res[i]
+ if c in """:/_#?;@&=+$,"<>%\\""":
+ res[i] = '_%02X' % ord(c)
+ return ''.join(res)
+
+def unquote(s):
+ """
+ Undo the effects of quote(). Based heavily on urllib.unquote().
+ """
+ mychr = chr
+ myatoi = int
+ list = s.split('_')
+ res = [list[0]]
+ myappend = res.append
+ del list[0]
+ for item in list:
+ if item[1:2]:
+ try:
+ myappend(mychr(myatoi(item[:2], 16)) + item[2:])
+ except ValueError:
+ myappend('_' + item)
+ else:
+ myappend('_' + item)
+ return "".join(res)
+
+def _nest_help(obj, depth, val):
+ current = obj
+ for i in range(depth):
+ current = current[-1]
+ current.append(val)
+
+def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth, admin_site):
+ "Helper function that recursively populates deleted_objects."
+ nh = _nest_help # Bind to local variable for performance
+ if current_depth > 16:
+ return # Avoid recursing too deep.
+ opts_seen = []
+ for related in opts.get_all_related_objects():
+ has_admin = related.model in admin_site._registry
+ if related.opts in opts_seen:
+ continue
+ opts_seen.append(related.opts)
+ rel_opts_name = related.get_accessor_name()
+ if isinstance(related.field.rel, models.OneToOneRel):
+ try:
+ sub_obj = getattr(obj, rel_opts_name)
+ except ObjectDoesNotExist:
+ pass
+ else:
+ if has_admin:
+ p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
+ if not user.has_perm(p):
+ perms_needed.add(related.opts.verbose_name)
+ # We don't care about populating deleted_objects now.
+ continue
+ if related.field.rel.edit_inline or not has_admin:
+ # Don't display link to edit, because it either has no
+ # admin or is edited inline.
+ nh(deleted_objects, current_depth, [mark_safe(u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), sub_obj)), []])
+ else:
+ # Display a link to the admin page.
+ nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' %
+ (escape(force_unicode(capfirst(related.opts.verbose_name))),
+ related.opts.app_label,
+ related.opts.object_name.lower(),
+ sub_obj._get_pk_val(), sub_obj)), []])
+ get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site)
+ else:
+ has_related_objs = False
+ for sub_obj in getattr(obj, rel_opts_name).all():
+ has_related_objs = True
+ if related.field.rel.edit_inline or not has_admin:
+ # Don't display link to edit, because it either has no
+ # admin or is edited inline.
+ nh(deleted_objects, current_depth, [u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), escape(sub_obj)), []])
+ else:
+ # Display a link to the admin page.
+ nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % \
+ (escape(force_unicode(capfirst(related.opts.verbose_name))), related.opts.app_label, related.opts.object_name.lower(), sub_obj._get_pk_val(), escape(sub_obj))), []])
+ get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site)
+ # If there were related objects, and the user doesn't have
+ # permission to delete them, add the missing perm to perms_needed.
+ if has_admin and has_related_objs:
+ p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
+ if not user.has_perm(p):
+ perms_needed.add(related.opts.verbose_name)
+ for related in opts.get_all_related_many_to_many_objects():
+ has_admin = related.model in admin_site._registry
+ if related.opts in opts_seen:
+ continue
+ opts_seen.append(related.opts)
+ rel_opts_name = related.get_accessor_name()
+ has_related_objs = False
+
+ # related.get_accessor_name() could return None for symmetrical relationships
+ if rel_opts_name:
+ rel_objs = getattr(obj, rel_opts_name, None)
+ if rel_objs:
+ has_related_objs = True
+
+ if has_related_objs:
+ for sub_obj in rel_objs.all():
+ if related.field.rel.edit_inline or not has_admin:
+ # Don't display link to edit, because it either has no
+ # admin or is edited inline.
+ nh(deleted_objects, current_depth, [_('One or more %(fieldname)s in %(name)s: %(obj)s') % \
+ {'fieldname': force_unicode(related.field.verbose_name), 'name': force_unicode(related.opts.verbose_name), 'obj': escape(sub_obj)}, []])
+ else:
+ # Display a link to the admin page.
+ nh(deleted_objects, current_depth, [
+ mark_safe((_('One or more %(fieldname)s in %(name)s:') % {'fieldname': escape(force_unicode(related.field.verbose_name)), 'name': escape(force_unicode(related.opts.verbose_name))}) + \
+ (u' <a href="../../../../%s/%s/%s/">%s</a>' % \
+ (related.opts.app_label, related.opts.module_name, sub_obj._get_pk_val(), escape(sub_obj)))), []])
+ # If there were related objects, and the user doesn't have
+ # permission to change them, add the missing perm to perms_needed.
+ if has_admin and has_related_objs:
+ p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission())
+ if not user.has_perm(p):
+ perms_needed.add(related.opts.verbose_name)
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
new file mode 100644
index 0000000000..f640cc5100
--- /dev/null
+++ b/django/contrib/admin/validation.py
@@ -0,0 +1,280 @@
+
+from django.core.exceptions import ImproperlyConfigured
+from django.db import models
+from django.newforms.models import BaseModelForm, BaseModelFormSet, fields_for_model
+from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin
+from django.contrib.admin.options import HORIZONTAL, VERTICAL
+
+def validate(cls, model):
+ """
+ Does basic ModelAdmin option validation. Calls custom validation
+ classmethod in the end if it is provided in cls. The signature of the
+ custom validation classmethod should be: def validate(cls, model).
+ """
+ opts = model._meta
+ _validate_base(cls, model)
+
+ # currying is expensive, use wrappers instead
+ def _check_istuplew(label, obj):
+ _check_istuple(cls, label, obj)
+
+ def _check_isdictw(label, obj):
+ _check_isdict(cls, label, obj)
+
+ def _check_field_existsw(label, field):
+ return _check_field_exists(cls, model, opts, label, field)
+
+ def _check_attr_existsw(label, field):
+ return _check_attr_exists(cls, model, opts, label, field)
+
+ # list_display
+ if hasattr(cls, 'list_display'):
+ _check_istuplew('list_display', cls.list_display)
+ for idx, field in enumerate(cls.list_display):
+ f = _check_attr_existsw("list_display[%d]" % idx, field)
+ if isinstance(f, models.ManyToManyField):
+ raise ImproperlyConfigured("`%s.list_display[%d]`, `%s` is a "
+ "ManyToManyField which is not supported."
+ % (cls.__name__, idx, field))
+
+ # list_display_links
+ if hasattr(cls, 'list_display_links'):
+ _check_istuplew('list_display_links', cls.list_display_links)
+ for idx, field in enumerate(cls.list_display_links):
+ _check_attr_existsw('list_display_links[%d]' % idx, field)
+ if field not in cls.list_display:
+ raise ImproperlyConfigured("`%s.list_display_links[%d]`"
+ "refers to `%s` which is not defined in `list_display`."
+ % (cls.__name__, idx, field))
+
+ # list_filter
+ if hasattr(cls, 'list_filter'):
+ _check_istuplew('list_filter', cls.list_filter)
+ for idx, field in enumerate(cls.list_filter):
+ _check_field_existsw('list_filter[%d]' % idx, field)
+
+ # list_per_page = 100
+ if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int):
+ raise ImproperlyConfigured("`%s.list_per_page` should be a integer."
+ % cls.__name__)
+
+ # search_fields = ()
+ if hasattr(cls, 'search_fields'):
+ _check_istuplew('search_fields', cls.search_fields)
+
+ # date_hierarchy = None
+ if cls.date_hierarchy:
+ f = _check_field_existsw('date_hierarchy', cls.date_hierarchy)
+ if not isinstance(f, (models.DateField, models.DateTimeField)):
+ raise ImproperlyConfigured("`%s.date_hierarchy is "
+ "neither an instance of DateField nor DateTimeField."
+ % cls.__name__)
+
+ # ordering = None
+ if cls.ordering:
+ _check_istuplew('ordering', cls.ordering)
+ for idx, field in enumerate(cls.ordering):
+ if field == '?' and len(cls.ordering) != 1:
+ raise ImproperlyConfigured("`%s.ordering` has the random "
+ "ordering marker `?`, but contains other fields as "
+ "well. Please either remove `?` or the other fields."
+ % cls.__name__)
+ if field == '?':
+ continue
+ if field.startswith('-'):
+ field = field[1:]
+ # Skip ordering in the format field1__field2 (FIXME: checking
+ # this format would be nice, but it's a little fiddly).
+ if '__' in field:
+ continue
+ _check_field_existsw('ordering[%d]' % idx, field)
+
+ # list_select_related = False
+ # save_as = False
+ # save_on_top = False
+ for attr in ('list_select_related', 'save_as', 'save_on_top'):
+ if not isinstance(getattr(cls, attr), bool):
+ raise ImproperlyConfigured("`%s.%s` should be a boolean."
+ % (cls.__name__, attr))
+
+ # inlines = []
+ if hasattr(cls, 'inlines'):
+ _check_istuplew('inlines', cls.inlines)
+ for idx, inline in enumerate(cls.inlines):
+ if not issubclass(inline, BaseModelAdmin):
+ raise ImproperlyConfigured("`%s.inlines[%d]` does not inherit "
+ "from BaseModelAdmin." % (cls.__name__, idx))
+ if not inline.model:
+ raise ImproperlyConfigured("`model` is a required attribute "
+ "of `%s.inlines[%d]`." % (cls.__name__, idx))
+ if not issubclass(inline.model, models.Model):
+ raise ImproperlyConfigured("`%s.inlines[%d].model` does not "
+ "inherit from models.Model." % (cls.__name__, idx))
+ _validate_base(inline, inline.model)
+ _validate_inline(inline)
+
+def _validate_inline(cls):
+ # model is already verified to exist and be a Model
+ if cls.fk_name: # default value is None
+ f = _check_field_exists(cls, cls.model, cls.model._meta,
+ 'fk_name', cls.fk_name)
+ if not isinstance(f, models.ForeignKey):
+ raise ImproperlyConfigured("`%s.fk_name is not an instance of "
+ "models.ForeignKey." % cls.__name__)
+ # extra = 3
+ # max_num = 0
+ for attr in ('extra', 'max_num'):
+ if not isinstance(getattr(cls, attr), int):
+ raise ImproperlyConfigured("`%s.%s` should be a integer."
+ % (cls.__name__, attr))
+
+ # formset
+ if hasattr(cls, 'formset') and not issubclass(cls.formset, BaseModelFormSet):
+ raise ImproperlyConfigured("`%s.formset` does not inherit from "
+ "BaseModelFormSet." % cls.__name__)
+
+def _validate_base(cls, model):
+ opts = model._meta
+ # currying is expensive, use wrappers instead
+ def _check_istuplew(label, obj):
+ _check_istuple(cls, label, obj)
+
+ def _check_isdictw(label, obj):
+ _check_isdict(cls, label, obj)
+
+ def _check_field_existsw(label, field):
+ return _check_field_exists(cls, model, opts, label, field)
+
+ def _check_form_field_existsw(label, field):
+ return _check_form_field_exists(cls, model, opts, label, field)
+
+ # raw_id_fields
+ if hasattr(cls, 'raw_id_fields'):
+ _check_istuplew('raw_id_fields', cls.raw_id_fields)
+ for idx, field in enumerate(cls.raw_id_fields):
+ f = _check_field_existsw('raw_id_fields', field)
+ if not isinstance(f, (models.ForeignKey, models.ManyToManyField)):
+ raise ImproperlyConfigured("`%s.raw_id_fields[%d]`, `%s` must "
+ "be either a ForeignKey or ManyToManyField."
+ % (cls.__name__, idx, field))
+
+ # fields
+ if cls.fields: # default value is None
+ _check_istuplew('fields', cls.fields)
+ for field in cls.fields:
+ _check_form_field_existsw('fields', field)
+ if cls.fieldsets:
+ raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__)
+
+ # fieldsets
+ if cls.fieldsets: # default value is None
+ _check_istuplew('fieldsets', cls.fieldsets)
+ for idx, fieldset in enumerate(cls.fieldsets):
+ _check_istuplew('fieldsets[%d]' % idx, fieldset)
+ if len(fieldset) != 2:
+ raise ImproperlyConfigured("`%s.fieldsets[%d]` does not "
+ "have exactly two elements." % (cls.__name__, idx))
+ _check_isdictw('fieldsets[%d][1]' % idx, fieldset[1])
+ if 'fields' not in fieldset[1]:
+ raise ImproperlyConfigured("`fields` key is required in "
+ "%s.fieldsets[%d][1] field options dict."
+ % (cls.__name__, idx))
+ for field in flatten_fieldsets(cls.fieldsets):
+ _check_form_field_existsw("fieldsets[%d][1]['fields']" % idx, field)
+
+ # form
+ if hasattr(cls, 'form') and not issubclass(cls.form, BaseModelForm):
+ raise ImproperlyConfigured("%s.form does not inherit from "
+ "BaseModelForm." % cls.__name__)
+
+ # filter_vertical
+ if hasattr(cls, 'filter_vertical'):
+ _check_istuplew('filter_vertical', cls.filter_vertical)
+ for idx, field in enumerate(cls.filter_vertical):
+ f = _check_field_existsw('filter_vertical', field)
+ if not isinstance(f, models.ManyToManyField):
+ raise ImproperlyConfigured("`%s.filter_vertical[%d]` must be "
+ "a ManyToManyField." % (cls.__name__, idx))
+
+ # filter_horizontal
+ if hasattr(cls, 'filter_horizontal'):
+ _check_istuplew('filter_horizontal', cls.filter_horizontal)
+ for idx, field in enumerate(cls.filter_horizontal):
+ f = _check_field_existsw('filter_horizontal', field)
+ if not isinstance(f, models.ManyToManyField):
+ raise ImproperlyConfigured("`%s.filter_horizontal[%d]` must be "
+ "a ManyToManyField." % (cls.__name__, idx))
+
+ # radio_fields
+ if hasattr(cls, 'radio_fields'):
+ _check_isdictw('radio_fields', cls.radio_fields)
+ for field, val in cls.radio_fields.items():
+ f = _check_field_existsw('radio_fields', field)
+ if not (isinstance(f, models.ForeignKey) or f.choices):
+ raise ImproperlyConfigured("`%s.radio_fields['%s']` "
+ "is neither an instance of ForeignKey nor does "
+ "have choices set." % (cls.__name__, field))
+ if not val in (HORIZONTAL, VERTICAL):
+ raise ImproperlyConfigured("`%s.radio_fields['%s']` "
+ "is neither admin.HORIZONTAL nor admin.VERTICAL."
+ % (cls.__name__, field))
+
+ # prepopulated_fields
+ if hasattr(cls, 'prepopulated_fields'):
+ _check_isdictw('prepopulated_fields', cls.prepopulated_fields)
+ for field, val in cls.prepopulated_fields.items():
+ f = _check_field_existsw('prepopulated_fields', field)
+ if isinstance(f, (models.DateTimeField, models.ForeignKey,
+ models.ManyToManyField)):
+ raise ImproperlyConfigured("`%s.prepopulated_fields['%s']` "
+ "is either a DateTimeField, ForeignKey or "
+ "ManyToManyField. This isn't allowed."
+ % (cls.__name__, field))
+ _check_istuplew("prepopulated_fields['%s']" % field, val)
+ for idx, f in enumerate(val):
+ _check_field_existsw("prepopulated_fields['%s'][%d]"
+ % (f, idx), f)
+
+def _check_istuple(cls, label, obj):
+ if not isinstance(obj, (list, tuple)):
+ raise ImproperlyConfigured("`%s.%s` must be a "
+ "list or tuple." % (cls.__name__, label))
+
+def _check_isdict(cls, label, obj):
+ if not isinstance(obj, dict):
+ raise ImproperlyConfigured("`%s.%s` must be a dictionary."
+ % (cls.__name__, label))
+
+def _check_field_exists(cls, model, opts, label, field):
+ try:
+ return opts.get_field(field)
+ except models.FieldDoesNotExist:
+ raise ImproperlyConfigured("`%s.%s` refers to "
+ "field `%s` that is missing from model `%s`."
+ % (cls.__name__, label, field, model.__name__))
+
+def _check_form_field_exists(cls, model, opts, label, field):
+ if hasattr(cls.form, 'base_fields'):
+ try:
+ cls.form.base_fields[field]
+ except KeyError:
+ raise ImproperlyConfigured("`%s.%s` refers to field `%s` that "
+ "is missing from the form." % (cls.__name__, label, field))
+ else:
+ fields = fields_for_model(model)
+ try:
+ fields[field]
+ except KeyError:
+ raise ImproperlyConfigured("`%s.%s` refers to field `%s` that "
+ "is missing from the form." % (cls.__name__, label, field))
+
+def _check_attr_exists(cls, model, opts, label, field):
+ try:
+ return opts.get_field(field)
+ except models.FieldDoesNotExist:
+ if not hasattr(model, field):
+ raise ImproperlyConfigured("`%s.%s` refers to "
+ "`%s` that is neither a field, method or property "
+ "of model `%s`."
+ % (cls.__name__, label, field, model.__name__))
+ return getattr(model, field)
diff --git a/django/contrib/admin/views/auth.py b/django/contrib/admin/views/auth.py
deleted file mode 100644
index 0c8104831b..0000000000
--- a/django/contrib/admin/views/auth.py
+++ /dev/null
@@ -1,78 +0,0 @@
-from django.contrib.admin.views.decorators import staff_member_required
-from django.contrib.auth.forms import UserCreationForm, AdminPasswordChangeForm
-from django.contrib.auth.models import User
-from django.core.exceptions import PermissionDenied
-from django import oldforms, template
-from django.shortcuts import render_to_response, get_object_or_404
-from django.http import HttpResponseRedirect
-from django.utils.html import escape
-from django.utils.translation import ugettext as _
-
-def user_add_stage(request):
- if not request.user.has_perm('auth.change_user'):
- raise PermissionDenied
- manipulator = UserCreationForm()
- if request.method == 'POST':
- new_data = request.POST.copy()
- errors = manipulator.get_validation_errors(new_data)
- if not errors:
- new_user = manipulator.save(new_data)
- msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': 'user', 'obj': new_user}
- if "_addanother" in request.POST:
- request.user.message_set.create(message=msg)
- return HttpResponseRedirect(request.path)
- else:
- request.user.message_set.create(message=msg + ' ' + _("You may edit it again below."))
- return HttpResponseRedirect('../%s/' % new_user.id)
- else:
- errors = new_data = {}
- form = oldforms.FormWrapper(manipulator, new_data, errors)
- return render_to_response('admin/auth/user/add_form.html', {
- 'title': _('Add user'),
- 'form': form,
- 'is_popup': '_popup' in request.REQUEST,
- 'add': True,
- 'change': False,
- 'has_delete_permission': False,
- 'has_change_permission': True,
- 'has_file_field': False,
- 'has_absolute_url': False,
- 'auto_populated_fields': (),
- 'bound_field_sets': (),
- 'first_form_field_id': 'id_username',
- 'opts': User._meta,
- 'username_help_text': User._meta.get_field('username').help_text,
- }, context_instance=template.RequestContext(request))
-user_add_stage = staff_member_required(user_add_stage)
-
-def user_change_password(request, id):
- if not request.user.has_perm('auth.change_user'):
- raise PermissionDenied
- user = get_object_or_404(User, pk=id)
- manipulator = AdminPasswordChangeForm(user)
- if request.method == 'POST':
- new_data = request.POST.copy()
- errors = manipulator.get_validation_errors(new_data)
- if not errors:
- new_user = manipulator.save(new_data)
- msg = _('Password changed successfully.')
- request.user.message_set.create(message=msg)
- return HttpResponseRedirect('..')
- else:
- errors = new_data = {}
- form = oldforms.FormWrapper(manipulator, new_data, errors)
- return render_to_response('admin/auth/user/change_password.html', {
- 'title': _('Change password: %s') % escape(user.username),
- 'form': form,
- 'is_popup': '_popup' in request.REQUEST,
- 'add': True,
- 'change': False,
- 'has_delete_permission': False,
- 'has_change_permission': True,
- 'has_absolute_url': False,
- 'first_form_field_id': 'id_password1',
- 'opts': User._meta,
- 'original': user,
- 'show_save': True,
- }, context_instance=template.RequestContext(request))
-user_change_password = staff_member_required(user_change_password)
diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py
index c27b2e7427..57517cc821 100644
--- a/django/contrib/admin/views/decorators.py
+++ b/django/contrib/admin/views/decorators.py
@@ -12,7 +12,6 @@ from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login
from django.shortcuts import render_to_response
from django.utils.translation import ugettext_lazy, ugettext as _
-from django.utils.safestring import mark_safe
ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
LOGIN_FORM_KEY = 'this_is_the_login_form'
diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index f1f620ed9e..926270cc68 100644
--- a/django/contrib/admin/views/main.py
+++ b/django/contrib/admin/views/main.py
@@ -1,20 +1,13 @@
-from django import oldforms, template
-from django.conf import settings
from django.contrib.admin.filterspecs import FilterSpec
-from django.contrib.admin.views.decorators import staff_member_required
-from django.views.decorators.cache import never_cache
-from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied
+from django.contrib.admin.options import IncorrectLookupParameters
+from django.contrib.admin.util import quote
from django.core.paginator import Paginator, InvalidPage
-from django.shortcuts import get_object_or_404, render_to_response
from django.db import models
from django.db.models.query import QuerySet
-from django.http import Http404, HttpResponse, HttpResponseRedirect
-from django.utils.html import escape
-from django.utils.text import capfirst, get_text_list
from django.utils.encoding import force_unicode, smart_str
-from django.utils.translation import ugettext as _
+from django.utils.translation import ugettext
from django.utils.safestring import mark_safe
+from django.utils.http import urlencode
import operator
try:
@@ -22,13 +15,6 @@ try:
except NameError:
from sets import Set as set # Python 2.3 fallback
-from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
-if not LogEntry._meta.installed:
- raise ImproperlyConfigured, "You'll need to put 'django.contrib.admin' in your INSTALLED_APPS setting before you can use the admin application."
-
-if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS:
- raise ImproperlyConfigured, "You'll need to put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting before you can use the admin application."
-
# The system will display a "Show all" link on the change list only if the
# total result count is less than or equal to this setting.
MAX_SHOW_ALL_ALLOWED = 200
@@ -45,523 +31,20 @@ ERROR_FLAG = 'e'
# Text to display within change-list table cells if the value is blank.
EMPTY_CHANGELIST_VALUE = '(None)'
-use_raw_id_admin = lambda field: isinstance(field.rel, (models.ManyToOneRel, models.ManyToManyRel)) and field.rel.raw_id_admin
-
-class IncorrectLookupParameters(Exception):
- pass
-
-def quote(s):
- """
- Ensure that primary key values do not confuse the admin URLs by escaping
- any '/', '_' and ':' characters. Similar to urllib.quote, except that the
- quoting is slightly different so that it doesn't get automatically
- unquoted by the Web browser.
- """
- if type(s) != type(''):
- return s
- res = list(s)
- for i in range(len(res)):
- c = res[i]
- if c in ':/_':
- res[i] = '_%02X' % ord(c)
- return ''.join(res)
-
-def unquote(s):
- """
- Undo the effects of quote(). Based heavily on urllib.unquote().
- """
- mychr = chr
- myatoi = int
- list = s.split('_')
- res = [list[0]]
- myappend = res.append
- del list[0]
- for item in list:
- if item[1:2]:
- try:
- myappend(mychr(myatoi(item[:2], 16)) + item[2:])
- except ValueError:
- myappend('_' + item)
- else:
- myappend('_' + item)
- return "".join(res)
-
-def get_javascript_imports(opts, auto_populated_fields, field_sets):
-# Put in any necessary JavaScript imports.
- js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
- if auto_populated_fields:
- js.append('js/urlify.js')
- if opts.has_field_type(models.DateTimeField) or opts.has_field_type(models.TimeField) or opts.has_field_type(models.DateField):
- js.extend(['js/calendar.js', 'js/admin/DateTimeShortcuts.js'])
- if opts.get_ordered_objects():
- js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js'])
- if opts.admin.js:
- js.extend(opts.admin.js)
- seen_collapse = False
- for field_set in field_sets:
- if not seen_collapse and 'collapse' in field_set.classes:
- seen_collapse = True
- js.append('js/admin/CollapsedFieldsets.js')
-
- for field_line in field_set:
- try:
- for f in field_line:
- if f.rel and isinstance(f, models.ManyToManyField) and f.rel.filter_interface:
- js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
- raise StopIteration
- except StopIteration:
- break
- return js
-
-class AdminBoundField(object):
- def __init__(self, field, field_mapping, original):
- self.field = field
- self.original = original
- self.form_fields = [field_mapping[name] for name in self.field.get_manipulator_field_names('')]
- self.element_id = self.form_fields[0].get_id()
- self.has_label_first = not isinstance(self.field, models.BooleanField)
- self.raw_id_admin = use_raw_id_admin(field)
- self.is_date_time = isinstance(field, models.DateTimeField)
- self.is_file_field = isinstance(field, models.FileField)
- self.needs_add_label = field.rel and (isinstance(field.rel, models.ManyToOneRel) or isinstance(field.rel, models.ManyToManyRel)) and field.rel.to._meta.admin
- self.hidden = isinstance(self.field, models.AutoField)
- self.first = False
-
- classes = []
- if self.raw_id_admin:
- classes.append('nowrap')
- if max([bool(f.errors()) for f in self.form_fields]):
- classes.append('error')
- if classes:
- self.cell_class_attribute = u' class="%s" ' % ' '.join(classes)
- self._repr_filled = False
-
- if field.rel:
- self.related_url = mark_safe(u'../../../%s/%s/'
- % (field.rel.to._meta.app_label,
- field.rel.to._meta.object_name.lower()))
-
- def original_value(self):
- if self.original:
- return self.original.__dict__[self.field.attname]
-
- def existing_display(self):
- try:
- return self._display
- except AttributeError:
- if isinstance(self.field.rel, models.ManyToOneRel):
- self._display = force_unicode(getattr(self.original, self.field.name), strings_only=True)
- elif isinstance(self.field.rel, models.ManyToManyRel):
- self._display = u", ".join([force_unicode(obj) for obj in getattr(self.original, self.field.name).all()])
- return self._display
-
- def __repr__(self):
- return repr(self.__dict__)
-
- def html_error_list(self):
- return mark_safe(" ".join([form_field.html_error_list() for form_field in self.form_fields if form_field.errors]))
-
- def original_url(self):
- if self.is_file_field and self.original and self.field.attname:
- url_method = getattr(self.original, 'get_%s_url' % self.field.attname)
- if callable(url_method):
- return url_method()
- return ''
-
-class AdminBoundFieldLine(object):
- def __init__(self, field_line, field_mapping, original):
- self.bound_fields = [field.bind(field_mapping, original, AdminBoundField) for field in field_line]
- for bound_field in self:
- bound_field.first = True
- break
-
- def __iter__(self):
- for bound_field in self.bound_fields:
- yield bound_field
-
- def __len__(self):
- return len(self.bound_fields)
-
-class AdminBoundFieldSet(object):
- def __init__(self, field_set, field_mapping, original):
- self.name = field_set.name
- self.classes = field_set.classes
- self.description = field_set.description
- self.bound_field_lines = [field_line.bind(field_mapping, original, AdminBoundFieldLine) for field_line in field_set]
-
- def __iter__(self):
- for bound_field_line in self.bound_field_lines:
- yield bound_field_line
-
- def __len__(self):
- return len(self.bound_field_lines)
-
-def render_change_form(model, manipulator, context, add=False, change=False, form_url=''):
- opts = model._meta
- app_label = opts.app_label
- auto_populated_fields = [f for f in opts.fields if f.prepopulate_from]
- field_sets = opts.admin.get_field_sets(opts)
- original = getattr(manipulator, 'original_object', None)
- bound_field_sets = [field_set.bind(context['form'], original, AdminBoundFieldSet) for field_set in field_sets]
- first_form_field_id = bound_field_sets[0].bound_field_lines[0].bound_fields[0].form_fields[0].get_id();
- ordered_objects = opts.get_ordered_objects()
- inline_related_objects = opts.get_followed_related_objects(manipulator.follow)
- extra_context = {
- 'add': add,
- 'change': change,
- 'has_delete_permission': context['perms'][app_label][opts.get_delete_permission()],
- 'has_change_permission': context['perms'][app_label][opts.get_change_permission()],
- 'has_file_field': opts.has_field_type(models.FileField),
- 'has_absolute_url': hasattr(model, 'get_absolute_url'),
- 'auto_populated_fields': auto_populated_fields,
- 'bound_field_sets': bound_field_sets,
- 'first_form_field_id': first_form_field_id,
- 'javascript_imports': get_javascript_imports(opts, auto_populated_fields, field_sets),
- 'ordered_objects': ordered_objects,
- 'inline_related_objects': inline_related_objects,
- 'form_url': mark_safe(form_url),
- 'opts': opts,
- 'content_type_id': ContentType.objects.get_for_model(model).id,
- }
- context.update(extra_context)
- return render_to_response([
- "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
- "admin/%s/change_form.html" % app_label,
- "admin/change_form.html"], context_instance=context)
-
-def index(request):
- return render_to_response('admin/index.html', {'title': _('Site administration')}, context_instance=template.RequestContext(request))
-index = staff_member_required(never_cache(index))
-
-def add_stage(request, app_label, model_name, show_delete=False, form_url='', post_url=None, post_url_continue='../%s/', object_id_override=None):
- model = models.get_model(app_label, model_name)
- if model is None:
- raise Http404("App %r, model %r, not found" % (app_label, model_name))
- opts = model._meta
-
- if not request.user.has_perm(app_label + '.' + opts.get_add_permission()):
- raise PermissionDenied
-
- if post_url is None:
- if request.user.has_perm(app_label + '.' + opts.get_change_permission()):
- # redirect to list view
- post_url = '../'
- else:
- # Object list will give 'Permission Denied', so go back to admin home
- post_url = '../../../'
-
- manipulator = model.AddManipulator()
- if request.POST:
- new_data = request.POST.copy()
-
- if opts.has_field_type(models.FileField):
- new_data.update(request.FILES)
-
- errors = manipulator.get_validation_errors(new_data)
- manipulator.do_html2python(new_data)
-
- if not errors:
- new_object = manipulator.save(new_data)
- pk_value = new_object._get_pk_val()
- LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, force_unicode(new_object), ADDITION)
- msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(new_object)}
- # Here, we distinguish between different save types by checking for
- # the presence of keys in request.POST.
- if "_continue" in request.POST:
- request.user.message_set.create(message=msg + ' ' + _("You may edit it again below."))
- if "_popup" in request.POST:
- post_url_continue += "?_popup=1"
- return HttpResponseRedirect(post_url_continue % pk_value)
- if "_popup" in request.POST:
- return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
- # escape() calls force_unicode.
- (escape(pk_value), escape(new_object)))
- elif "_addanother" in request.POST:
- request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
- return HttpResponseRedirect(request.path)
- else:
- request.user.message_set.create(message=msg)
- return HttpResponseRedirect(post_url)
- else:
- # Add default data.
- new_data = manipulator.flatten_data()
-
- # Override the defaults with GET params, if they exist.
- new_data.update(dict(request.GET.items()))
-
- errors = {}
-
- # Populate the FormWrapper.
- form = oldforms.FormWrapper(manipulator, new_data, errors)
-
- c = template.RequestContext(request, {
- 'title': _('Add %s') % force_unicode(opts.verbose_name),
- 'form': form,
- 'is_popup': '_popup' in request.REQUEST,
- 'show_delete': show_delete,
- })
-
- if object_id_override is not None:
- c['object_id'] = object_id_override
-
- return render_change_form(model, manipulator, c, add=True)
-add_stage = staff_member_required(never_cache(add_stage))
-
-def change_stage(request, app_label, model_name, object_id):
- model = models.get_model(app_label, model_name)
- object_id = unquote(object_id)
- if model is None:
- raise Http404("App %r, model %r, not found" % (app_label, model_name))
- opts = model._meta
-
- if not request.user.has_perm(app_label + '.' + opts.get_change_permission()):
- raise PermissionDenied
-
- if request.POST and "_saveasnew" in request.POST:
- return add_stage(request, app_label, model_name, form_url='../../add/')
-
- try:
- manipulator = model.ChangeManipulator(object_id)
- except model.DoesNotExist:
- raise Http404('%s object with primary key %r does not exist' % (model_name, escape(object_id)))
-
- if request.POST:
- new_data = request.POST.copy()
-
- if opts.has_field_type(models.FileField):
- new_data.update(request.FILES)
-
- errors = manipulator.get_validation_errors(new_data)
- manipulator.do_html2python(new_data)
-
- if not errors:
- new_object = manipulator.save(new_data)
- pk_value = new_object._get_pk_val()
-
- # Construct the change message.
- change_message = []
- if manipulator.fields_added:
- change_message.append(_('Added %s.') % get_text_list(manipulator.fields_added, _('and')))
- if manipulator.fields_changed:
- change_message.append(_('Changed %s.') % get_text_list(manipulator.fields_changed, _('and')))
- if manipulator.fields_deleted:
- change_message.append(_('Deleted %s.') % get_text_list(manipulator.fields_deleted, _('and')))
- change_message = ' '.join(change_message)
- if not change_message:
- change_message = _('No fields changed.')
- LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, force_unicode(new_object), CHANGE, change_message)
-
- msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(new_object)}
- if "_continue" in request.POST:
- request.user.message_set.create(message=msg + ' ' + _("You may edit it again below."))
- if '_popup' in request.REQUEST:
- return HttpResponseRedirect(request.path + "?_popup=1")
- else:
- return HttpResponseRedirect(request.path)
- elif "_saveasnew" in request.POST:
- request.user.message_set.create(message=_('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(new_object)})
- return HttpResponseRedirect("../%s/" % pk_value)
- elif "_addanother" in request.POST:
- request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
- return HttpResponseRedirect("../add/")
- else:
- request.user.message_set.create(message=msg)
- return HttpResponseRedirect("../")
- else:
- # Populate new_data with a "flattened" version of the current data.
- new_data = manipulator.flatten_data()
-
- # TODO: do this in flatten_data...
- # If the object has ordered objects on its admin page, get the existing
- # order and flatten it into a comma-separated list of IDs.
-
- id_order_list = []
- for rel_obj in opts.get_ordered_objects():
- id_order_list.extend(getattr(manipulator.original_object, 'get_%s_order' % rel_obj.object_name.lower())())
- if id_order_list:
- new_data['order_'] = ','.join(map(str, id_order_list))
- errors = {}
-
- # Populate the FormWrapper.
- form = oldforms.FormWrapper(manipulator, new_data, errors)
- form.original = manipulator.original_object
- form.order_objects = []
-
- #TODO Should be done in flatten_data / FormWrapper construction
- for related in opts.get_followed_related_objects():
- wrt = related.opts.order_with_respect_to
- if wrt and wrt.rel and wrt.rel.to == opts:
- func = getattr(manipulator.original_object, 'get_%s_list' %
- related.get_accessor_name())
- orig_list = func()
- form.order_objects.extend(orig_list)
-
- c = template.RequestContext(request, {
- 'title': _('Change %s') % force_unicode(opts.verbose_name),
- 'form': form,
- 'object_id': object_id,
- 'original': manipulator.original_object,
- 'is_popup': '_popup' in request.REQUEST,
- })
- return render_change_form(model, manipulator, c, change=True)
-change_stage = staff_member_required(never_cache(change_stage))
-
-def _nest_help(obj, depth, val):
- current = obj
- for i in range(depth):
- current = current[-1]
- current.append(val)
-
-def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth):
- "Helper function that recursively populates deleted_objects."
- nh = _nest_help # Bind to local variable for performance
- if current_depth > 16:
- return # Avoid recursing too deep.
- opts_seen = []
- for related in opts.get_all_related_objects():
- if related.opts in opts_seen:
- continue
- opts_seen.append(related.opts)
- rel_opts_name = related.get_accessor_name()
- if isinstance(related.field.rel, models.OneToOneRel):
- try:
- sub_obj = getattr(obj, rel_opts_name)
- except ObjectDoesNotExist:
- pass
- else:
- if related.opts.admin:
- p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
- if not user.has_perm(p):
- perms_needed.add(related.opts.verbose_name)
- # We don't care about populating deleted_objects now.
- continue
- if related.field.rel.edit_inline or not related.opts.admin:
- # Don't display link to edit, because it either has no
- # admin or is edited inline.
- nh(deleted_objects, current_depth, [mark_safe(u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), sub_obj)), []])
- else:
- # Display a link to the admin page.
- nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' %
- (escape(force_unicode(capfirst(related.opts.verbose_name))),
- related.opts.app_label,
- related.opts.object_name.lower(),
- sub_obj._get_pk_val(), sub_obj)), []])
- _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2)
- else:
- has_related_objs = False
- for sub_obj in getattr(obj, rel_opts_name).all():
- has_related_objs = True
- if related.field.rel.edit_inline or not related.opts.admin:
- # Don't display link to edit, because it either has no
- # admin or is edited inline.
- nh(deleted_objects, current_depth, [u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), escape(sub_obj)), []])
- else:
- # Display a link to the admin page.
- nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % \
- (escape(force_unicode(capfirst(related.opts.verbose_name))), related.opts.app_label, related.opts.object_name.lower(), sub_obj._get_pk_val(), escape(sub_obj))), []])
- _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2)
- # If there were related objects, and the user doesn't have
- # permission to delete them, add the missing perm to perms_needed.
- if related.opts.admin and has_related_objs:
- p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
- if not user.has_perm(p):
- perms_needed.add(related.opts.verbose_name)
- for related in opts.get_all_related_many_to_many_objects():
- if related.opts in opts_seen:
- continue
- opts_seen.append(related.opts)
- rel_opts_name = related.get_accessor_name()
- has_related_objs = False
-
- # related.get_accessor_name() could return None for symmetrical relationships
- if rel_opts_name:
- rel_objs = getattr(obj, rel_opts_name, None)
- if rel_objs:
- has_related_objs = True
-
- if has_related_objs:
- for sub_obj in rel_objs.all():
- if related.field.rel.edit_inline or not related.opts.admin:
- # Don't display link to edit, because it either has no
- # admin or is edited inline.
- nh(deleted_objects, current_depth, [_('One or more %(fieldname)s in %(name)s: %(obj)s') % \
- {'fieldname': force_unicode(related.field.verbose_name), 'name': force_unicode(related.opts.verbose_name), 'obj': escape(sub_obj)}, []])
- else:
- # Display a link to the admin page.
- nh(deleted_objects, current_depth, [
- mark_safe((_('One or more %(fieldname)s in %(name)s:') % {'fieldname': escape(force_unicode(related.field.verbose_name)), 'name': escape(force_unicode(related.opts.verbose_name))}) + \
- (u' <a href="../../../../%s/%s/%s/">%s</a>' % \
- (related.opts.app_label, related.opts.module_name, sub_obj._get_pk_val(), escape(sub_obj)))), []])
- # If there were related objects, and the user doesn't have
- # permission to change them, add the missing perm to perms_needed.
- if related.opts.admin and has_related_objs:
- p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission())
- if not user.has_perm(p):
- perms_needed.add(related.opts.verbose_name)
-
-def delete_stage(request, app_label, model_name, object_id):
- model = models.get_model(app_label, model_name)
- object_id = unquote(object_id)
- if model is None:
- raise Http404("App %r, model %r, not found" % (app_label, model_name))
- opts = model._meta
- if not request.user.has_perm(app_label + '.' + opts.get_delete_permission()):
- raise PermissionDenied
- obj = get_object_or_404(model, pk=object_id)
-
- # Populate deleted_objects, a data structure of all related objects that
- # will also be deleted.
- deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), force_unicode(object_id), escape(obj))), []]
- perms_needed = set()
- _get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1)
-
- if request.POST: # The user has already confirmed the deletion.
- if perms_needed:
- raise PermissionDenied
- obj_display = force_unicode(obj)
- obj.delete()
- LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, object_id, obj_display, DELETION)
- request.user.message_set.create(message=_('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': obj_display})
- return HttpResponseRedirect("../../")
- extra_context = {
- "title": _("Are you sure?"),
- "object_name": force_unicode(opts.verbose_name),
- "object": obj,
- "deleted_objects": deleted_objects,
- "perms_lacking": perms_needed,
- "opts": model._meta,
- }
- return render_to_response(["admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower() ),
- "admin/%s/delete_confirmation.html" % app_label ,
- "admin/delete_confirmation.html"], extra_context, context_instance=template.RequestContext(request))
-delete_stage = staff_member_required(never_cache(delete_stage))
-
-def history(request, app_label, model_name, object_id):
- model = models.get_model(app_label, model_name)
- object_id = unquote(object_id)
- if model is None:
- raise Http404("App %r, model %r, not found" % (app_label, model_name))
- action_list = LogEntry.objects.filter(object_id=object_id,
- content_type__id__exact=ContentType.objects.get_for_model(model).id).select_related().order_by('action_time')
- # If no history was found, see whether this object even exists.
- obj = get_object_or_404(model, pk=object_id)
- extra_context = {
- 'title': _('Change history: %s') % obj,
- 'action_list': action_list,
- 'module_name': force_unicode(capfirst(model._meta.verbose_name_plural)),
- 'object': obj,
- }
- return render_to_response(["admin/%s/%s/object_history.html" % (app_label, model._meta.object_name.lower()),
- "admin/%s/object_history.html" % app_label ,
- "admin/object_history.html"], extra_context, context_instance=template.RequestContext(request))
-history = staff_member_required(never_cache(history))
-
class ChangeList(object):
- def __init__(self, request, model):
+ def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, model_admin):
self.model = model
self.opts = model._meta
self.lookup_opts = self.opts
- self.manager = self.opts.admin.manager
+ self.root_query_set = model_admin.queryset(request)
+ self.list_display = list_display
+ self.list_display_links = list_display_links
+ self.list_filter = list_filter
+ self.date_hierarchy = date_hierarchy
+ self.search_fields = search_fields
+ self.list_select_related = list_select_related
+ self.list_per_page = list_per_page
+ self.model_admin = model_admin
# Get search parameters from the query string.
try:
@@ -580,17 +63,16 @@ class ChangeList(object):
self.query = request.GET.get(SEARCH_VAR, '')
self.query_set = self.get_query_set()
self.get_results(request)
- self.title = (self.is_popup and _('Select %s') % force_unicode(self.opts.verbose_name) or _('Select %s to change') % force_unicode(self.opts.verbose_name))
+ self.title = (self.is_popup and ugettext('Select %s') % force_unicode(self.opts.verbose_name) or ugettext('Select %s to change') % force_unicode(self.opts.verbose_name))
self.filter_specs, self.has_filters = self.get_filters(request)
self.pk_attname = self.lookup_opts.pk.attname
def get_filters(self, request):
filter_specs = []
- if self.lookup_opts.admin.list_filter and not self.opts.one_to_one_field:
- filter_fields = [self.lookup_opts.get_field(field_name) \
- for field_name in self.lookup_opts.admin.list_filter]
+ if self.list_filter and not self.opts.one_to_one_field:
+ filter_fields = [self.lookup_opts.get_field(field_name) for field_name in self.list_filter]
for f in filter_fields:
- spec = FilterSpec.create(f, request, self.params, self.model)
+ spec = FilterSpec.create(f, request, self.params, self.model, self.model_admin)
if spec and spec.has_output():
filter_specs.append(spec)
return filter_specs, bool(filter_specs)
@@ -604,15 +86,15 @@ class ChangeList(object):
if k.startswith(r):
del p[k]
for k, v in new_params.items():
- if k in p and v is None:
- del p[k]
- elif v is not None:
+ if v is None:
+ if k in p:
+ del p[k]
+ else:
p[k] = v
- return mark_safe('?' + '&amp;'.join([u'%s=%s' % (k, v) for k, v in p.items()]).replace(' ', '%20'))
+ return '?%s' % urlencode(p)
def get_results(self, request):
- paginator = Paginator(self.query_set, self.lookup_opts.admin.list_per_page)
-
+ paginator = Paginator(self.query_set, self.list_per_page)
# Get the number of objects, with admin filters applied.
try:
result_count = paginator.count
@@ -630,10 +112,10 @@ class ChangeList(object):
if not self.query_set.query.where:
full_result_count = result_count
else:
- full_result_count = self.manager.count()
+ full_result_count = self.root_query_set.count()
can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED
- multi_page = result_count > self.lookup_opts.admin.list_per_page
+ multi_page = result_count > self.list_per_page
# Get the list of objects to display on this page.
if (self.show_all and can_show_all) or not multi_page:
@@ -657,7 +139,7 @@ class ChangeList(object):
# options, then check the object's default ordering. If neither of
# those exist, order descending by ID by default. Finally, look for
# manually-specified ordering from the query string.
- ordering = lookup_opts.admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name]
+ ordering = self.model_admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name]
if ordering[0].startswith('-'):
order_field, order_type = ordering[0][1:], 'desc'
@@ -665,14 +147,14 @@ class ChangeList(object):
order_field, order_type = ordering[0], 'asc'
if ORDER_VAR in params:
try:
- field_name = lookup_opts.admin.list_display[int(params[ORDER_VAR])]
+ field_name = self.list_display[int(params[ORDER_VAR])]
try:
f = lookup_opts.get_field(field_name)
except models.FieldDoesNotExist:
- # see if field_name is a name of a non-field
- # that allows sorting
+ # See whether field_name is a name of a non-field
+ # that allows sorting.
try:
- attr = getattr(lookup_opts.admin.manager.model, field_name)
+ attr = getattr(self.model, field_name)
order_field = attr.admin_order_field
except AttributeError:
pass
@@ -686,7 +168,7 @@ class ChangeList(object):
return order_field, order_type
def get_query_set(self):
- qs = self.manager.get_query_set()
+ qs = self.root_query_set
lookup_params = self.params.copy() # a dictionary of the query string
for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR):
if i in lookup_params:
@@ -703,10 +185,10 @@ class ChangeList(object):
# Use select_related() if one of the list_display options is a field
# with a relationship.
- if self.lookup_opts.admin.list_select_related:
+ if self.list_select_related:
qs = qs.select_related()
else:
- for field_name in self.lookup_opts.admin.list_display:
+ for field_name in self.list_display:
try:
f = self.lookup_opts.get_field(field_name)
except models.FieldDoesNotExist:
@@ -731,13 +213,17 @@ class ChangeList(object):
else:
return "%s__icontains" % field_name
- if self.lookup_opts.admin.search_fields and self.query:
+ if self.search_fields and self.query:
for bit in self.query.split():
- or_queries = [models.Q(**{construct_search(field_name): bit}) for field_name in self.lookup_opts.admin.search_fields]
+ or_queries = [models.Q(**{construct_search(field_name): bit}) for field_name in self.search_fields]
other_qs = QuerySet(self.model)
other_qs.dup_select_related(qs)
other_qs = other_qs.filter(reduce(operator.or_, or_queries))
qs = qs & other_qs
+ for field_name in self.search_fields:
+ if '__' in field_name:
+ qs = qs.distinct()
+ break
if self.opts.one_to_one_field:
qs = qs.complex_filter(self.opts.one_to_one_field.rel.limit_choices_to)
@@ -746,31 +232,3 @@ class ChangeList(object):
def url_for_result(self, result):
return "%s/" % quote(getattr(result, self.pk_attname))
-
-def change_list(request, app_label, model_name):
- model = models.get_model(app_label, model_name)
- if model is None:
- raise Http404("App %r, model %r, not found" % (app_label, model_name))
- if not request.user.has_perm(app_label + '.' + model._meta.get_change_permission()):
- raise PermissionDenied
- try:
- cl = ChangeList(request, model)
- except IncorrectLookupParameters:
- # Wacky lookup parameters were given, so redirect to the main
- # changelist page, without parameters, and pass an 'invalid=1'
- # parameter via the query string. If wacky parameters were given and
- # the 'invalid=1' parameter was already in the query string, something
- # is screwed up with the database, so display an error page.
- if ERROR_FLAG in request.GET.keys():
- return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
- return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
- c = template.RequestContext(request, {
- 'title': cl.title,
- 'is_popup': cl.is_popup,
- 'cl': cl,
- })
- c.update({'has_add_permission': c['perms'][app_label][cl.opts.get_add_permission()]}),
- return render_to_response(['admin/%s/%s/change_list.html' % (app_label, cl.opts.object_name.lower()),
- 'admin/%s/change_list.html' % app_label,
- 'admin/change_list.html'], context_instance=c)
-change_list = staff_member_required(never_cache(change_list))
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
new file mode 100644
index 0000000000..4ae8889ac4
--- /dev/null
+++ b/django/contrib/admin/widgets.py
@@ -0,0 +1,215 @@
+"""
+Form Widget classes specific to the Django admin site.
+"""
+
+import copy
+
+from django import newforms as forms
+from django.newforms.widgets import RadioFieldRenderer
+from django.newforms.util import flatatt
+from django.utils.datastructures import MultiValueDict
+from django.utils.text import capfirst, truncate_words
+from django.utils.translation import ugettext as _
+from django.utils.safestring import mark_safe
+from django.utils.encoding import force_unicode
+from django.conf import settings
+
+class FilteredSelectMultiple(forms.SelectMultiple):
+ """
+ A SelectMultiple with a JavaScript filter interface.
+
+ Note that the resulting JavaScript assumes that the SelectFilter2.js
+ library and its dependencies have been loaded in the HTML page.
+ """
+ def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
+ self.verbose_name = verbose_name
+ self.is_stacked = is_stacked
+ super(FilteredSelectMultiple, self).__init__(attrs, choices)
+
+ def render(self, name, value, attrs=None, choices=()):
+ from django.conf import settings
+ output = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)]
+ output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {')
+ # TODO: "id_" is hard-coded here. This should instead use the correct
+ # API to determine the ID dynamically.
+ output.append(u'SelectFilter.init("id_%s", "%s", %s, "%s"); });</script>\n' % \
+ (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), settings.ADMIN_MEDIA_PREFIX))
+ return mark_safe(u''.join(output))
+
+class AdminDateWidget(forms.TextInput):
+ class Media:
+ js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js",
+ settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js")
+
+ def __init__(self, attrs={}):
+ super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'})
+
+class AdminTimeWidget(forms.TextInput):
+ class Media:
+ js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js",
+ settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js")
+
+ def __init__(self, attrs={}):
+ super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'})
+
+class AdminSplitDateTime(forms.SplitDateTimeWidget):
+ """
+ A SplitDateTime Widget that has some admin-specific styling.
+ """
+ def __init__(self, attrs=None):
+ widgets = [AdminDateWidget, AdminTimeWidget]
+ # Note that we're calling MultiWidget, not SplitDateTimeWidget, because
+ # we want to define widgets.
+ forms.MultiWidget.__init__(self, widgets, attrs)
+
+ def format_output(self, rendered_widgets):
+ return mark_safe(u'<p class="datetime">%s %s<br />%s %s</p>' % \
+ (_('Date:'), rendered_widgets[0], _('Time:'), rendered_widgets[1]))
+
+class AdminRadioFieldRenderer(RadioFieldRenderer):
+ def render(self):
+ """Outputs a <ul> for this set of radio fields."""
+ return mark_safe(u'<ul%s>\n%s\n</ul>' % (
+ flatatt(self.attrs),
+ u'\n'.join([u'<li>%s</li>' % force_unicode(w) for w in self]))
+ )
+
+class AdminRadioSelect(forms.RadioSelect):
+ renderer = AdminRadioFieldRenderer
+
+class AdminFileWidget(forms.FileInput):
+ """
+ A FileField Widget that shows its current value if it has one.
+ """
+ def __init__(self, attrs={}):
+ super(AdminFileWidget, self).__init__(attrs)
+
+ def render(self, name, value, attrs=None):
+ from django.conf import settings
+ output = []
+ if value:
+ output.append('%s <a target="_blank" href="%s%s">%s</a> <br />%s ' % \
+ (_('Currently:'), settings.MEDIA_URL, value, value, _('Change:')))
+ output.append(super(AdminFileWidget, self).render(name, value, attrs))
+ return mark_safe(u''.join(output))
+
+class ForeignKeyRawIdWidget(forms.TextInput):
+ """
+ A Widget for displaying ForeignKeys in the "raw_id" interface rather than
+ in a <select> box.
+ """
+ def __init__(self, rel, attrs=None):
+ self.rel = rel
+ super(ForeignKeyRawIdWidget, self).__init__(attrs)
+
+ def render(self, name, value, attrs=None):
+ from django.conf import settings
+ related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower())
+ if self.rel.limit_choices_to:
+ url = '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in self.rel.limit_choices_to.items()])
+ else:
+ url = ''
+ if not attrs.has_key('class'):
+ attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript looks for this hook.
+ output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)]
+ # TODO: "id_" is hard-coded here. This should instead use the correct
+ # API to determine the ID dynamically.
+ output.append('<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' % \
+ (related_url, url, name))
+ output.append('<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a>' % settings.ADMIN_MEDIA_PREFIX)
+ if value:
+ output.append(self.label_for_value(value))
+ return mark_safe(u''.join(output))
+
+ def label_for_value(self, value):
+ return '&nbsp;<strong>%s</strong>' % \
+ truncate_words(self.rel.to.objects.get(pk=value), 14)
+
+class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
+ """
+ A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
+ in a <select multiple> box.
+ """
+ def __init__(self, rel, attrs=None):
+ super(ManyToManyRawIdWidget, self).__init__(rel, attrs)
+
+ def render(self, name, value, attrs=None):
+ attrs['class'] = 'vManyToManyRawIdAdminField'
+ if value:
+ value = ','.join([str(v) for v in value])
+ else:
+ value = ''
+ return super(ManyToManyRawIdWidget, self).render(name, value, attrs)
+
+ def label_for_value(self, value):
+ return ''
+
+ def value_from_datadict(self, data, files, name):
+ value = data.get(name, None)
+ if value and ',' in value:
+ return data[name].split(',')
+ if value:
+ return [value]
+ return None
+
+ def _has_changed(self, initial, data):
+ if initial is None:
+ initial = []
+ if data is None:
+ data = []
+ if len(initial) != len(data):
+ return True
+ for pk1, pk2 in zip(initial, data):
+ if force_unicode(pk1) != force_unicode(pk2):
+ return True
+ return False
+
+class RelatedFieldWidgetWrapper(forms.Widget):
+ """
+ This class is a wrapper to a given widget to add the add icon for the
+ admin interface.
+ """
+ def __init__(self, widget, rel, admin_site):
+ self.is_hidden = widget.is_hidden
+ self.needs_multipart_form = widget.needs_multipart_form
+ self.attrs = widget.attrs
+ self.choices = widget.choices
+ self.widget = widget
+ self.rel = rel
+ # so we can check if the related object is registered with this AdminSite
+ self.admin_site = admin_site
+
+ def __deepcopy__(self, memo):
+ obj = copy.copy(self)
+ obj.widget = copy.deepcopy(self.widget, memo)
+ obj.attrs = self.widget.attrs
+ memo[id(self)] = obj
+ return obj
+
+ def render(self, name, value, *args, **kwargs):
+ from django.conf import settings
+ rel_to = self.rel.to
+ related_url = '../../../%s/%s/' % (rel_to._meta.app_label, rel_to._meta.object_name.lower())
+ self.widget.choices = self.choices
+ output = [self.widget.render(name, value, *args, **kwargs)]
+ if rel_to in self.admin_site._registry: # If the related object has an admin interface:
+ # TODO: "id_" is hard-coded here. This should instead use the correct
+ # API to determine the ID dynamically.
+ output.append(u'<a href="%sadd/" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> ' % \
+ (related_url, name))
+ output.append(u'<img src="%simg/admin/icon_addlink.gif" width="10" height="10" alt="Add Another"/></a>' % settings.ADMIN_MEDIA_PREFIX)
+ return mark_safe(u''.join(output))
+
+ def build_attrs(self, extra_attrs=None, **kwargs):
+ "Helper function for building an attribute dictionary."
+ self.attrs = self.widget.build_attrs(extra_attrs=None, **kwargs)
+ return self.attrs
+
+ def value_from_datadict(self, data, files, name):
+ return self.widget.value_from_datadict(data, files, name)
+
+ def _has_changed(self, initial, data):
+ return self.widget._has_changed(initial, data)
+
+ def id_for_label(self, id_):
+ return self.widget.id_for_label(id_)
diff --git a/django/contrib/admindocs/__init__.py b/django/contrib/admindocs/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/contrib/admindocs/__init__.py
diff --git a/django/contrib/admindocs/urls.py b/django/contrib/admindocs/urls.py
new file mode 100644
index 0000000000..e7baa76fc0
--- /dev/null
+++ b/django/contrib/admindocs/urls.py
@@ -0,0 +1,15 @@
+from django.conf.urls.defaults import *
+from django.contrib.admindocs import views
+
+urlpatterns = patterns('',
+ ('^$', views.doc_index),
+ ('^bookmarklets/$', views.bookmarklets),
+ ('^tags/$', views.template_tag_index),
+ ('^filters/$', views.template_filter_index),
+ ('^views/$', views.view_index),
+ ('^views/(?P<view>[^/]+)/$', views.view_detail),
+ ('^models/$', views.model_index),
+ ('^models/(?P<app_label>[^\.]+)\.(?P<model_name>[^/]+)/$', views.model_detail),
+# ('^templates/$', views.template_index),
+ ('^templates/(?P<template>.*)/$', views.template_detail),
+)
diff --git a/django/contrib/admin/utils.py b/django/contrib/admindocs/utils.py
index 4a45a622b2..4a45a622b2 100644
--- a/django/contrib/admin/utils.py
+++ b/django/contrib/admindocs/utils.py
diff --git a/django/contrib/admin/views/doc.py b/django/contrib/admindocs/views.py
index 44a27d6cc3..e81293adcb 100644
--- a/django/contrib/admin/views/doc.py
+++ b/django/contrib/admindocs/views.py
@@ -5,9 +5,9 @@ from django.contrib.admin.views.decorators import staff_member_required
from django.db import models
from django.shortcuts import render_to_response
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
-from django.http import Http404
+from django.http import Http404, get_host
from django.core import urlresolvers
-from django.contrib.admin import utils
+from django.contrib.admindocs import utils
from django.contrib.sites.models import Site
from django.utils.translation import ugettext as _
from django.utils.safestring import mark_safe
@@ -23,13 +23,18 @@ class GenericSite(object):
def doc_index(request):
if not utils.docutils_is_available:
return missing_docutils_page(request)
- return render_to_response('admin_doc/index.html', context_instance=RequestContext(request))
+ root_path = re.sub(re.escape('doc/') + '$', '', request.path)
+ return render_to_response('admin_doc/index.html', {
+ 'root_path': root_path,
+ }, context_instance=RequestContext(request))
doc_index = staff_member_required(doc_index)
def bookmarklets(request):
# Hack! This couples this view to the URL it lives at.
admin_root = request.path[:-len('doc/bookmarklets/')]
+ root_path = re.sub(re.escape('doc/bookmarklets/') + '$', '', request.path)
return render_to_response('admin_doc/bookmarklets.html', {
+ 'root_path': root_path,
'admin_url': mark_safe("%s://%s%s" % (request.is_secure() and 'https' or 'http', request.get_host(), admin_root)),
}, context_instance=RequestContext(request))
bookmarklets = staff_member_required(bookmarklets)
@@ -61,8 +66,11 @@ def template_tag_index(request):
'meta': metadata,
'library': tag_library,
})
-
- return render_to_response('admin_doc/template_tag_index.html', {'tags': tags}, context_instance=RequestContext(request))
+ root_path = re.sub(re.escape('doc/tags/') + '$', '', request.path)
+ return render_to_response('admin_doc/template_tag_index.html', {
+ 'root_path': root_path,
+ 'tags': tags
+ }, context_instance=RequestContext(request))
template_tag_index = staff_member_required(template_tag_index)
def template_filter_index(request):
@@ -92,7 +100,11 @@ def template_filter_index(request):
'meta': metadata,
'library': tag_library,
})
- return render_to_response('admin_doc/template_filter_index.html', {'filters': filters}, context_instance=RequestContext(request))
+ root_path = re.sub(re.escape('doc/filters/') + '$', '', request.path)
+ return render_to_response('admin_doc/template_filter_index.html', {
+ 'root_path': root_path,
+ 'filters': filters
+ }, context_instance=RequestContext(request))
template_filter_index = staff_member_required(template_filter_index)
def view_index(request):
@@ -120,7 +132,11 @@ def view_index(request):
'site': site_obj,
'url': simplify_regex(regex),
})
- return render_to_response('admin_doc/view_index.html', {'views': views}, context_instance=RequestContext(request))
+ root_path = re.sub(re.escape('doc/views/') + '$', '', request.path)
+ return render_to_response('admin_doc/view_index.html', {
+ 'root_path': root_path,
+ 'views': views
+ }, context_instance=RequestContext(request))
view_index = staff_member_required(view_index)
def view_detail(request, view):
@@ -139,7 +155,9 @@ def view_detail(request, view):
body = utils.parse_rst(body, 'view', _('view:') + view)
for key in metadata:
metadata[key] = utils.parse_rst(metadata[key], 'model', _('view:') + view)
+ root_path = re.sub(re.escape('doc/views/%s/' % view) + '$', '', request.path)
return render_to_response('admin_doc/view_detail.html', {
+ 'root_path': root_path,
'name': view,
'summary': title,
'body': body,
@@ -150,15 +168,18 @@ view_detail = staff_member_required(view_detail)
def model_index(request):
if not utils.docutils_is_available:
return missing_docutils_page(request)
-
m_list = [m._meta for m in models.get_models()]
- return render_to_response('admin_doc/model_index.html', {'models': m_list}, context_instance=RequestContext(request))
+ root_path = re.sub(re.escape('doc/models/') + '$', '', request.path)
+ return render_to_response('admin_doc/model_index.html', {
+ 'root_path': root_path,
+ 'models': m_list
+ }, context_instance=RequestContext(request))
model_index = staff_member_required(model_index)
def model_detail(request, app_label, model_name):
if not utils.docutils_is_available:
return missing_docutils_page(request)
-
+
# Get the model class.
try:
app_mod = models.get_app(app_label)
@@ -170,7 +191,7 @@ def model_detail(request, app_label, model_name):
model = m
break
if model is None:
- raise Http404, _("Model %(name)r not found in app %(label)r") % {'name': model_name, 'label': app_label}
+ raise Http404, _("Model %(model_name)r not found in app %(app_label)r") % {'model_name': model_name, 'app_label': app_label}
opts = model._meta
@@ -182,7 +203,7 @@ def model_detail(request, app_label, model_name):
if isinstance(field, models.ForeignKey):
data_type = related_object_name = field.rel.to.__name__
app_label = field.rel.to._meta.app_label
- verbose = utils.parse_rst((_("the related `%(label)s.%(type)s` object") % {'label': app_label, 'type': data_type}), 'model', _('model:') + data_type)
+ verbose = utils.parse_rst((_("the related `%(app_label)s.%(data_type)s` object") % {'app_label': app_label, 'data_type': data_type}), 'model', _('model:') + data_type)
else:
data_type = get_readable_field_data_type(field)
verbose = field.verbose_name
@@ -213,7 +234,7 @@ def model_detail(request, app_label, model_name):
# Gather related objects
for rel in opts.get_all_related_objects():
- verbose = _("related `%(label)s.%(name)s` objects") % {'label': rel.opts.app_label, 'name': rel.opts.object_name}
+ verbose = _("related `%(app_label)s.%(object_name)s` objects") % {'app_label': rel.opts.app_label, 'object_name': rel.opts.object_name}
accessor = rel.get_accessor_name()
fields.append({
'name' : "%s.all" % accessor,
@@ -225,8 +246,9 @@ def model_detail(request, app_label, model_name):
'data_type' : 'Integer',
'verbose' : utils.parse_rst(_("number of %s") % verbose , 'model', _('model:') + opts.module_name),
})
-
+ root_path = re.sub(re.escape('doc/models/%s.%s/' % (app_label, model_name)) + '$', '', request.path)
return render_to_response('admin_doc/model_detail.html', {
+ 'root_path': root_path,
'name': '%s.%s' % (opts.app_label, opts.object_name),
'summary': _("Fields on %s objects") % opts.object_name,
'description': model.__doc__,
@@ -252,7 +274,9 @@ def template_detail(request, template):
'site': site_obj,
'order': list(settings_mod.TEMPLATE_DIRS).index(dir),
})
+ root_path = re.sub(re.escape('doc/templates/%s/' % template) + '$', '', request.path)
return render_to_response('admin_doc/template_detail.html', {
+ 'root_path': root_path,
'name': template,
'templates': templates,
}, context_instance=RequestContext(request))
diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py
new file mode 100644
index 0000000000..998692a6cb
--- /dev/null
+++ b/django/contrib/auth/admin.py
@@ -0,0 +1,66 @@
+from django.contrib.auth.models import User, Group
+from django.core.exceptions import PermissionDenied
+from django import oldforms, template
+from django.shortcuts import render_to_response
+from django.http import HttpResponseRedirect
+from django.utils.translation import ugettext, ugettext_lazy as _
+from django.contrib import admin
+
+class GroupAdmin(admin.ModelAdmin):
+ search_fields = ('name',)
+ ordering = ('name',)
+ filter_horizontal = ('permissions',)
+
+class UserAdmin(admin.ModelAdmin):
+ fieldsets = (
+ (None, {'fields': ('username', 'password')}),
+ (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
+ (_('Permissions'), {'fields': ('is_staff', 'is_active', 'is_superuser', 'user_permissions')}),
+ (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
+ (_('Groups'), {'fields': ('groups',)}),
+ )
+ list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
+ list_filter = ('is_staff', 'is_superuser')
+ search_fields = ('username', 'first_name', 'last_name', 'email')
+ ordering = ('username',)
+ filter_horizontal = ('user_permissions',)
+
+ def add_view(self, request):
+ # avoid a circular import. see #6718.
+ from django.contrib.auth.forms import UserCreationForm
+ if not self.has_change_permission(request):
+ raise PermissionDenied
+ if request.method == 'POST':
+ form = UserCreationForm(request.POST)
+ if form.is_valid():
+ new_user = form.save()
+ msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': 'user', 'obj': new_user}
+ if "_addanother" in request.POST:
+ request.user.message_set.create(message=msg)
+ return HttpResponseRedirect(request.path)
+ else:
+ request.user.message_set.create(message=msg + ' ' + ugettext("You may edit it again below."))
+ return HttpResponseRedirect('../%s/' % new_user.id)
+ else:
+ form = UserCreationForm()
+ return render_to_response('admin/auth/user/add_form.html', {
+ 'title': _('Add user'),
+ 'form': form,
+ 'is_popup': '_popup' in request.REQUEST,
+ 'add': True,
+ 'change': False,
+ 'has_add_permission': True,
+ 'has_delete_permission': False,
+ 'has_change_permission': True,
+ 'has_file_field': False,
+ 'has_absolute_url': False,
+ 'auto_populated_fields': (),
+ 'opts': User._meta,
+ 'save_as': False,
+ 'username_help_text': User._meta.get_field('username').help_text,
+ 'root_path': self.admin_site.root_path,
+ }, context_instance=template.RequestContext(request))
+
+admin.site.register(Group, GroupAdmin)
+admin.site.register(User, UserAdmin)
+
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 47a974cacd..f63dc7b854 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -3,88 +3,106 @@ from django.contrib.auth import authenticate
from django.contrib.sites.models import Site
from django.template import Context, loader
from django.core import validators
-from django import oldforms
-from django.utils.translation import ugettext as _
+from django import newforms as forms
+from django.utils.translation import ugettext_lazy as _
-class UserCreationForm(oldforms.Manipulator):
- "A form that creates a user, with no privileges, from the given username and password."
- def __init__(self):
- self.fields = (
- oldforms.TextField(field_name='username', length=30, max_length=30, is_required=True,
- validator_list=[validators.isAlphaNumeric, self.isValidUsername]),
- oldforms.PasswordField(field_name='password1', length=30, max_length=60, is_required=True),
- oldforms.PasswordField(field_name='password2', length=30, max_length=60, is_required=True,
- validator_list=[validators.AlwaysMatchesOtherField('password1', _("The two password fields didn't match."))]),
- )
-
- def isValidUsername(self, field_data, all_data):
+class UserCreationForm(forms.ModelForm):
+ """
+ A form that creates a user, with no privileges, from the given username and password.
+ """
+ username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^\w+$',
+ help_text = _("Required. 30 characters or fewer. Alphanumeric characters only (letters, digits and underscores)."),
+ error_message = _("This value must contain only letters, numbers and underscores."))
+ password1 = forms.CharField(label=_("Password"), max_length=60, widget=forms.PasswordInput)
+ password2 = forms.CharField(label=_("Password confirmation"), max_length=60, widget=forms.PasswordInput)
+
+ class Meta:
+ model = User
+ fields = ("username",)
+
+ def clean_username(self):
+ username = self.cleaned_data["username"]
try:
- User.objects.get(username=field_data)
+ User.objects.get(username=username)
except User.DoesNotExist:
- return
- raise validators.ValidationError, _('A user with that username already exists.')
-
- def save(self, new_data):
- "Creates the user."
- return User.objects.create_user(new_data['username'], '', new_data['password1'])
+ return username
+ raise forms.ValidationError(_("A user with that username already exists."))
+
+ def clean_password2(self):
+ password1 = self.cleaned_data["password1"]
+ password2 = self.cleaned_data["password2"]
+ if password1 != password2:
+ raise forms.ValidationError(_("The two password fields didn't match."))
+ return password2
+
+ def save(self, commit=True):
+ user = super(UserCreationForm, self).save(commit=False)
+ user.set_password(self.cleaned_data["password1"])
+ if commit:
+ user.save()
+ return user
-class AuthenticationForm(oldforms.Manipulator):
+class AuthenticationForm(forms.Form):
"""
Base class for authenticating users. Extend this to get a form that accepts
username/password logins.
"""
- def __init__(self, request=None):
+ username = forms.CharField(label=_("Username"), max_length=30)
+ password = forms.CharField(label=_("Password"), max_length=30, widget=forms.PasswordInput)
+
+ def __init__(self, request=None, *args, **kwargs):
"""
- If request is passed in, the manipulator will validate that cookies are
+ If request is passed in, the form will validate that cookies are
enabled. Note that the request (a HttpRequest object) must have set a
cookie with the key TEST_COOKIE_NAME and value TEST_COOKIE_VALUE before
- running this validator.
+ running this validation.
"""
self.request = request
- self.fields = [
- oldforms.TextField(field_name="username", length=15, max_length=30, is_required=True,
- validator_list=[self.isValidUser, self.hasCookiesEnabled]),
- oldforms.PasswordField(field_name="password", length=15, max_length=30, is_required=True),
- ]
self.user_cache = None
-
- def hasCookiesEnabled(self, field_data, all_data):
- if self.request and not self.request.session.test_cookie_worked():
- raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.")
-
- def isValidUser(self, field_data, all_data):
- username = field_data
- password = all_data.get('password', None)
- self.user_cache = authenticate(username=username, password=password)
- if self.user_cache is None:
- raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
- elif not self.user_cache.is_active:
- raise validators.ValidationError, _("This account is inactive.")
-
+ super(AuthenticationForm, self).__init__(*args, **kwargs)
+
+ def clean(self):
+ username = self.cleaned_data.get('username')
+ password = self.cleaned_data.get('password')
+
+ if username and password:
+ self.user_cache = authenticate(username=username, password=password)
+ if self.user_cache is None:
+ raise forms.ValidationError(_("Please enter a correct username and password. Note that both fields are case-sensitive."))
+ elif not self.user_cache.is_active:
+ raise forms.ValidationError(_("This account is inactive."))
+
+ # TODO: determine whether this should move to its own method.
+ if self.request:
+ if not self.request.session.test_cookie_worked():
+ raise forms.ValidationError(_("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in."))
+
+ return self.cleaned_data
+
def get_user_id(self):
if self.user_cache:
return self.user_cache.id
return None
-
+
def get_user(self):
return self.user_cache
-class PasswordResetForm(oldforms.Manipulator):
- "A form that lets a user request a password reset"
- def __init__(self):
- self.fields = (
- oldforms.EmailField(field_name="email", length=40, is_required=True,
- validator_list=[self.isValidUserEmail]),
- )
-
- def isValidUserEmail(self, new_data, all_data):
- "Validates that a user exists with the given e-mail address"
- self.users_cache = list(User.objects.filter(email__iexact=new_data))
+class PasswordResetForm(forms.Form):
+ email = forms.EmailField(label=_("E-mail"), max_length=40)
+
+ def clean_email(self):
+ """
+ Validates that a user exists with the given e-mail address.
+ """
+ email = self.cleaned_data["email"]
+ self.users_cache = User.objects.filter(email__iexact=email)
if len(self.users_cache) == 0:
- raise validators.ValidationError, _("That e-mail address doesn't have an associated user account. Are you sure you've registered?")
-
+ raise forms.ValidationError(_("That e-mail address doesn't have an associated user account. Are you sure you've registered?"))
+
def save(self, domain_override=None, email_template_name='registration/password_reset_email.html'):
- "Calculates a new password randomly and sends it to the user"
+ """
+ Calculates a new password randomly and sends it to the user.
+ """
from django.core.mail import send_mail
for user in self.users_cache:
new_pass = User.objects.make_random_password()
@@ -103,42 +121,69 @@ class PasswordResetForm(oldforms.Manipulator):
'domain': domain,
'site_name': site_name,
'user': user,
- }
- send_mail(_('Password reset on %s') % site_name, t.render(Context(c)), None, [user.email])
+ }
+ send_mail(_("Password reset on %s") % site_name,
+ t.render(Context(c)), None, [user.email])
-class PasswordChangeForm(oldforms.Manipulator):
- "A form that lets a user change his password."
- def __init__(self, user):
+class PasswordChangeForm(forms.Form):
+ """
+ A form that lets a user change his/her password.
+ """
+ old_password = forms.CharField(label=_("Old password"), max_length=30, widget=forms.PasswordInput)
+ new_password1 = forms.CharField(label=_("New password"), max_length=30, widget=forms.PasswordInput)
+ new_password2 = forms.CharField(label=_("New password confirmation"), max_length=30, widget=forms.PasswordInput)
+
+ def __init__(self, user, *args, **kwargs):
self.user = user
- self.fields = (
- oldforms.PasswordField(field_name="old_password", length=30, max_length=30, is_required=True,
- validator_list=[self.isValidOldPassword]),
- oldforms.PasswordField(field_name="new_password1", length=30, max_length=30, is_required=True,
- validator_list=[validators.AlwaysMatchesOtherField('new_password2', _("The two 'new password' fields didn't match."))]),
- oldforms.PasswordField(field_name="new_password2", length=30, max_length=30, is_required=True),
- )
-
- def isValidOldPassword(self, new_data, all_data):
- "Validates that the old_password field is correct."
- if not self.user.check_password(new_data):
- raise validators.ValidationError, _("Your old password was entered incorrectly. Please enter it again.")
-
- def save(self, new_data):
- "Saves the new password."
- self.user.set_password(new_data['new_password1'])
- self.user.save()
+ super(PasswordChangeForm, self).__init__(*args, **kwargs)
+
+ def clean_old_password(self):
+ """
+ Validates that the old_password field is correct.
+ """
+ old_password = self.cleaned_data["old_password"]
+ if not self.user.check_password(old_password):
+ raise forms.ValidationError(_("Your old password was entered incorrectly. Please enter it again."))
+ return old_password
+
+ def clean_new_password2(self):
+ password1 = self.cleaned_data.get('new_password1')
+ password2 = self.cleaned_data.get('new_password2')
+ if password1 and password2:
+ if password1 != password2:
+ raise forms.ValidationError(_("The two password fields didn't match."))
+ return password2
+
+ def save(self, commit=True):
+ self.user.set_password(self.cleaned_data['new_password1'])
+ if commit:
+ self.user.save()
+ return self.user
-class AdminPasswordChangeForm(oldforms.Manipulator):
- "A form used to change the password of a user in the admin interface."
- def __init__(self, user):
+class AdminPasswordChangeForm(forms.Form):
+ """
+ A form used to change the password of a user in the admin interface.
+ """
+ password1 = forms.CharField(label=_("Password"), max_length=60, widget=forms.PasswordInput)
+ password2 = forms.CharField(label=_("Password (again)"), max_length=60, widget=forms.PasswordInput)
+
+ def __init__(self, user, *args, **kwargs):
self.user = user
- self.fields = (
- oldforms.PasswordField(field_name='password1', length=30, max_length=60, is_required=True),
- oldforms.PasswordField(field_name='password2', length=30, max_length=60, is_required=True,
- validator_list=[validators.AlwaysMatchesOtherField('password1', _("The two password fields didn't match."))]),
- )
-
- def save(self, new_data):
- "Saves the new password."
- self.user.set_password(new_data['password1'])
- self.user.save()
+ super(AdminPasswordChangeForm, self).__init__(*args, **kwargs)
+
+ def clean_password2(self):
+ password1 = self.cleaned_data.get('password1')
+ password2 = self.cleaned_data.get('password2')
+ if password1 and password2:
+ if password1 != password2:
+ raise forms.ValidationError(_("The two password fields didn't match."))
+ return password2
+
+ def save(self, commit=True):
+ """
+ Saves the new password.
+ """
+ self.user.set_password(self.cleaned_data["password1"])
+ if commit:
+ self.user.save()
+ return self.user
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
index 379a9f4c64..a0ed4f366f 100644
--- a/django/contrib/auth/models.py
+++ b/django/contrib/auth/models.py
@@ -91,16 +91,12 @@ class Group(models.Model):
Beyond permissions, groups are a convenient way to categorize users to apply some label, or extended functionality, to them. For example, you could create a group 'Special users', and you could write code that would do special things to those users -- such as giving them access to a members-only portion of your site, or sending them members-only e-mail messages.
"""
name = models.CharField(_('name'), max_length=80, unique=True)
- permissions = models.ManyToManyField(Permission, verbose_name=_('permissions'), blank=True, filter_interface=models.HORIZONTAL)
+ permissions = models.ManyToManyField(Permission, verbose_name=_('permissions'), blank=True)
class Meta:
verbose_name = _('group')
verbose_name_plural = _('groups')
-
- class Admin:
- search_fields = ('name',)
- ordering = ('name',)
-
+
def __unicode__(self):
return self.name
@@ -147,26 +143,13 @@ class User(models.Model):
date_joined = models.DateTimeField(_('date joined'), default=datetime.datetime.now)
groups = models.ManyToManyField(Group, verbose_name=_('groups'), blank=True,
help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in."))
- user_permissions = models.ManyToManyField(Permission, verbose_name=_('user permissions'), blank=True, filter_interface=models.HORIZONTAL)
+ user_permissions = models.ManyToManyField(Permission, verbose_name=_('user permissions'), blank=True)
objects = UserManager()
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
-
- class Admin:
- fields = (
- (None, {'fields': ('username', 'password')}),
- (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
- (_('Permissions'), {'fields': ('is_staff', 'is_active', 'is_superuser', 'user_permissions')}),
- (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
- (_('Groups'), {'fields': ('groups',)}),
- )
- list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
- list_filter = ('is_staff', 'is_superuser')
- search_fields = ('username', 'first_name', 'last_name', 'email')
- ordering = ('username',)
-
+
def __unicode__(self):
return self.username
diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py
new file mode 100644
index 0000000000..6242303f46
--- /dev/null
+++ b/django/contrib/auth/tests/__init__.py
@@ -0,0 +1,8 @@
+from django.contrib.auth.tests.basic import BASIC_TESTS, PasswordResetTest
+from django.contrib.auth.tests.forms import FORM_TESTS
+
+__test__ = {
+ 'BASIC_TESTS': BASIC_TESTS,
+ 'PASSWORDRESET_TESTS': PasswordResetTest,
+ 'FORM_TESTS': FORM_TESTS,
+}
diff --git a/django/contrib/auth/tests.py b/django/contrib/auth/tests/basic.py
index ea1ac26c21..76dbdc9cb9 100644
--- a/django/contrib/auth/tests.py
+++ b/django/contrib/auth/tests/basic.py
@@ -1,4 +1,5 @@
-"""
+
+BASIC_TESTS = """
>>> from django.contrib.auth.models import User, AnonymousUser
>>> u = User.objects.create_user('testuser', 'test@example.com', 'testpw')
>>> u.has_usable_password()
@@ -60,14 +61,15 @@ from django.core import mail
class PasswordResetTest(TestCase):
fixtures = ['authtestdata.json']
urls = 'django.contrib.auth.urls'
+
def test_email_not_found(self):
"Error is raised if the provided email address isn't currently registered"
response = self.client.get('/password_reset/')
self.assertEquals(response.status_code, 200)
response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
- self.assertContains(response, "That e-mail address doesn&#39;t have an associated user account")
+ self.assertContains(response, "That e-mail address doesn't have an associated user account")
self.assertEquals(len(mail.outbox), 0)
-
+
def test_email_found(self):
"Email is sent if a valid email address is provided for password reset"
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py
new file mode 100644
index 0000000000..1e1e0a95d4
--- /dev/null
+++ b/django/contrib/auth/tests/forms.py
@@ -0,0 +1,135 @@
+
+FORM_TESTS = """
+>>> from django.contrib.auth.models import User
+>>> from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
+>>> from django.contrib.auth.forms import PasswordChangeForm
+
+The user already exists.
+
+>>> user = User.objects.create_user("jsmith", "jsmith@example.com", "test123")
+>>> data = {
+... 'username': 'jsmith',
+... 'password1': 'test123',
+... 'password2': 'test123',
+... }
+>>> form = UserCreationForm(data)
+>>> form.is_valid()
+False
+>>> form["username"].errors
+[u'A user with that username already exists.']
+
+The username contains invalid data.
+
+>>> data = {
+... 'username': 'jsmith@example.com',
+... 'password1': 'test123',
+... 'password2': 'test123',
+... }
+>>> form = UserCreationForm(data)
+>>> form.is_valid()
+False
+>>> form["username"].errors
+[u'This value must contain only letters, numbers and underscores.']
+
+The verification password is incorrect.
+
+>>> data = {
+... 'username': 'jsmith2',
+... 'password1': 'test123',
+... 'password2': 'test',
+... }
+>>> form = UserCreationForm(data)
+>>> form.is_valid()
+False
+>>> form["password2"].errors
+[u"The two password fields didn't match."]
+
+The success case.
+
+>>> data = {
+... 'username': 'jsmith2',
+... 'password1': 'test123',
+... 'password2': 'test123',
+... }
+>>> form = UserCreationForm(data)
+>>> form.is_valid()
+True
+>>> form.save()
+<User: jsmith2>
+
+The user submits an invalid username.
+
+>>> data = {
+... 'username': 'jsmith_does_not_exist',
+... 'password': 'test123',
+... }
+
+>>> form = AuthenticationForm(None, data)
+>>> form.is_valid()
+False
+>>> form.non_field_errors()
+[u'Please enter a correct username and password. Note that both fields are case-sensitive.']
+
+The user is inactive.
+
+>>> data = {
+... 'username': 'jsmith',
+... 'password': 'test123',
+... }
+>>> user.is_active = False
+>>> user.save()
+>>> form = AuthenticationForm(None, data)
+>>> form.is_valid()
+False
+>>> form.non_field_errors()
+[u'This account is inactive.']
+
+>>> user.is_active = True
+>>> user.save()
+
+The success case
+
+>>> form = AuthenticationForm(None, data)
+>>> form.is_valid()
+True
+>>> form.non_field_errors()
+[]
+
+The old password is incorrect.
+
+>>> data = {
+... 'old_password': 'test',
+... 'new_password1': 'abc123',
+... 'new_password2': 'abc123',
+... }
+>>> form = PasswordChangeForm(user, data)
+>>> form.is_valid()
+False
+>>> form["old_password"].errors
+[u'Your old password was entered incorrectly. Please enter it again.']
+
+The two new passwords do not match.
+
+>>> data = {
+... 'old_password': 'test123',
+... 'new_password1': 'abc123',
+... 'new_password2': 'abc',
+... }
+>>> form = PasswordChangeForm(user, data)
+>>> form.is_valid()
+False
+>>> form["new_password2"].errors
+[u"The two password fields didn't match."]
+
+The success case.
+
+>>> data = {
+... 'old_password': 'test123',
+... 'new_password1': 'abc123',
+... 'new_password2': 'abc123',
+... }
+>>> form = PasswordChangeForm(user, data)
+>>> form.is_valid()
+True
+
+"""
diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
index 524710327a..0a52240631 100644
--- a/django/contrib/auth/views.py
+++ b/django/contrib/auth/views.py
@@ -1,42 +1,42 @@
-from django import oldforms
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm
-from django.contrib.auth.forms import PasswordResetForm, PasswordChangeForm
+from django.contrib.auth.forms import PasswordResetForm, PasswordChangeForm, AdminPasswordChangeForm
+from django.core.exceptions import PermissionDenied
+from django.shortcuts import render_to_response, get_object_or_404
from django.contrib.sites.models import Site, RequestSite
from django.http import HttpResponseRedirect
-from django.shortcuts import render_to_response
from django.template import RequestContext
from django.utils.http import urlquote
+from django.utils.html import escape
from django.utils.translation import ugettext as _
+from django.contrib.auth.models import User
+import re
def login(request, template_name='registration/login.html', redirect_field_name=REDIRECT_FIELD_NAME):
"Displays the login form and handles the login action."
- manipulator = AuthenticationForm()
redirect_to = request.REQUEST.get(redirect_field_name, '')
- if request.POST:
- errors = manipulator.get_validation_errors(request.POST)
- if not errors:
+ if request.method == "POST":
+ form = AuthenticationForm(data=request.POST)
+ if form.is_valid():
# Light security check -- make sure redirect_to isn't garbage.
if not redirect_to or '//' in redirect_to or ' ' in redirect_to:
from django.conf import settings
redirect_to = settings.LOGIN_REDIRECT_URL
from django.contrib.auth import login
- login(request, manipulator.get_user())
+ login(request, form.get_user())
if request.session.test_cookie_worked():
request.session.delete_test_cookie()
return HttpResponseRedirect(redirect_to)
else:
- errors = {}
+ form = AuthenticationForm(request)
request.session.set_test_cookie()
-
if Site._meta.installed:
current_site = Site.objects.get_current()
else:
current_site = RequestSite(request)
-
return render_to_response(template_name, {
- 'form': oldforms.FormWrapper(manipulator, request.POST, errors),
+ 'form': form,
redirect_field_name: redirect_to,
'site_name': current_site.name,
}, context_instance=RequestContext(request))
@@ -66,13 +66,11 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N
return HttpResponseRedirect('%s?%s=%s' % (login_url, urlquote(redirect_field_name), urlquote(next)))
def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html',
- email_template_name='registration/password_reset_email.html'):
- new_data, errors = {}, {}
- form = PasswordResetForm()
- if request.POST:
- new_data = request.POST.copy()
- errors = form.get_validation_errors(new_data)
- if not errors:
+ email_template_name='registration/password_reset_email.html',
+ password_reset_form=PasswordResetForm):
+ if request.method == "POST":
+ form = password_reset_form(request.POST)
+ if form.is_valid():
if is_admin_site:
form.save(domain_override=request.META['HTTP_HOST'])
else:
@@ -81,24 +79,57 @@ def password_reset(request, is_admin_site=False, template_name='registration/pas
else:
form.save(domain_override=RequestSite(request).domain, email_template_name=email_template_name)
return HttpResponseRedirect('%sdone/' % request.path)
- return render_to_response(template_name, {'form': oldforms.FormWrapper(form, new_data, errors)},
- context_instance=RequestContext(request))
+ else:
+ form = password_reset_form()
+ return render_to_response(template_name, {
+ 'form': form,
+ }, context_instance=RequestContext(request))
def password_reset_done(request, template_name='registration/password_reset_done.html'):
return render_to_response(template_name, context_instance=RequestContext(request))
def password_change(request, template_name='registration/password_change_form.html'):
- new_data, errors = {}, {}
- form = PasswordChangeForm(request.user)
- if request.POST:
- new_data = request.POST.copy()
- errors = form.get_validation_errors(new_data)
- if not errors:
- form.save(new_data)
+ if request.method == "POST":
+ form = PasswordChangeForm(request.user, request.POST)
+ if form.is_valid():
+ form.save()
return HttpResponseRedirect('%sdone/' % request.path)
- return render_to_response(template_name, {'form': oldforms.FormWrapper(form, new_data, errors)},
- context_instance=RequestContext(request))
+ else:
+ form = PasswordChangeForm(request.user)
+ return render_to_response(template_name, {
+ 'form': form,
+ }, context_instance=RequestContext(request))
password_change = login_required(password_change)
def password_change_done(request, template_name='registration/password_change_done.html'):
return render_to_response(template_name, context_instance=RequestContext(request))
+
+# TODO: move to admin.py in the ModelAdmin
+def user_change_password(request, id):
+ if not request.user.has_perm('auth.change_user'):
+ raise PermissionDenied
+ user = get_object_or_404(User, pk=id)
+ if request.method == 'POST':
+ form = AdminPasswordChangeForm(user, request.POST)
+ if form.is_valid():
+ new_user = form.save()
+ msg = _('Password changed successfully.')
+ request.user.message_set.create(message=msg)
+ return HttpResponseRedirect('..')
+ else:
+ form = AdminPasswordChangeForm(user)
+ return render_to_response('admin/auth/user/change_password.html', {
+ 'title': _('Change password: %s') % escape(user.username),
+ 'form': form,
+ 'is_popup': '_popup' in request.REQUEST,
+ 'add': True,
+ 'change': False,
+ 'has_delete_permission': False,
+ 'has_change_permission': True,
+ 'has_absolute_url': False,
+ 'opts': User._meta,
+ 'original': user,
+ 'save_as': False,
+ 'show_save': True,
+ 'root_path': re.sub('auth/user/(\d+)/password/$', '', request.path),
+ }, context_instance=RequestContext(request))
diff --git a/django/contrib/comments/admin.py b/django/contrib/comments/admin.py
new file mode 100644
index 0000000000..81ecc699c7
--- /dev/null
+++ b/django/contrib/comments/admin.py
@@ -0,0 +1,30 @@
+from django.contrib import admin
+from django.contrib.comments.models import Comment, FreeComment
+
+
+class CommentAdmin(admin.ModelAdmin):
+ fieldsets = (
+ (None, {'fields': ('content_type', 'object_id', 'site')}),
+ ('Content', {'fields': ('user', 'headline', 'comment')}),
+ ('Ratings', {'fields': ('rating1', 'rating2', 'rating3', 'rating4', 'rating5', 'rating6', 'rating7', 'rating8', 'valid_rating')}),
+ ('Meta', {'fields': ('is_public', 'is_removed', 'ip_address')}),
+ )
+ list_display = ('user', 'submit_date', 'content_type', 'get_content_object')
+ list_filter = ('submit_date',)
+ date_hierarchy = 'submit_date'
+ search_fields = ('comment', 'user__username')
+ raw_id_fields = ('user',)
+
+class FreeCommentAdmin(admin.ModelAdmin):
+ fieldsets = (
+ (None, {'fields': ('content_type', 'object_id', 'site')}),
+ ('Content', {'fields': ('person_name', 'comment')}),
+ ('Meta', {'fields': ('is_public', 'ip_address', 'approved')}),
+ )
+ list_display = ('person_name', 'submit_date', 'content_type', 'get_content_object')
+ list_filter = ('submit_date',)
+ date_hierarchy = 'submit_date'
+ search_fields = ('comment', 'person_name')
+
+admin.site.register(Comment, CommentAdmin)
+admin.site.register(FreeComment, FreeCommentAdmin) \ No newline at end of file
diff --git a/django/contrib/comments/models.py b/django/contrib/comments/models.py
index d0c54b85cb..a13fec9e6e 100644
--- a/django/contrib/comments/models.py
+++ b/django/contrib/comments/models.py
@@ -66,7 +66,7 @@ class CommentManager(models.Manager):
class Comment(models.Model):
"""A comment by a registered user."""
- user = models.ForeignKey(User, raw_id_admin=True)
+ user = models.ForeignKey(User)
content_type = models.ForeignKey(ContentType)
object_id = models.IntegerField(_('object ID'))
headline = models.CharField(_('headline'), max_length=255, blank=True)
@@ -96,18 +96,6 @@ class Comment(models.Model):
verbose_name_plural = _('comments')
ordering = ('-submit_date',)
- class Admin:
- fields = (
- (None, {'fields': ('content_type', 'object_id', 'site')}),
- ('Content', {'fields': ('user', 'headline', 'comment')}),
- ('Ratings', {'fields': ('rating1', 'rating2', 'rating3', 'rating4', 'rating5', 'rating6', 'rating7', 'rating8', 'valid_rating')}),
- ('Meta', {'fields': ('is_public', 'is_removed', 'ip_address')}),
- )
- list_display = ('user', 'submit_date', 'content_type', 'get_content_object')
- list_filter = ('submit_date',)
- date_hierarchy = 'submit_date'
- search_fields = ('comment', 'user__username')
-
def __unicode__(self):
return "%s: %s..." % (self.user.username, self.comment[:100])
@@ -188,17 +176,6 @@ class FreeComment(models.Model):
verbose_name_plural = _('free comments')
ordering = ('-submit_date',)
- class Admin:
- fields = (
- (None, {'fields': ('content_type', 'object_id', 'site')}),
- ('Content', {'fields': ('person_name', 'comment')}),
- ('Meta', {'fields': ('submit_date', 'is_public', 'ip_address', 'approved')}),
- )
- list_display = ('person_name', 'submit_date', 'content_type', 'get_content_object')
- list_filter = ('submit_date',)
- date_hierarchy = 'submit_date'
- search_fields = ('comment', 'person_name')
-
def __unicode__(self):
return "%s: %s..." % (self.person_name, self.comment[:100])
@@ -306,3 +283,4 @@ class ModeratorDeletion(models.Model):
def __unicode__(self):
return _("Moderator deletion by %r") % self.user
+ \ No newline at end of file
diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py
index 67da5759ac..ba59cbafc9 100644
--- a/django/contrib/comments/views/comments.py
+++ b/django/contrib/comments/views/comments.py
@@ -1,3 +1,6 @@
+import base64
+import datetime
+
from django.core import validators
from django import oldforms
from django.core.mail import mail_admins, mail_managers
@@ -7,16 +10,61 @@ from django.shortcuts import render_to_response
from django.template import RequestContext
from django.contrib.comments.models import Comment, FreeComment, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC
from django.contrib.contenttypes.models import ContentType
-from django.contrib.auth.forms import AuthenticationForm
+from django.contrib.auth import authenticate
from django.http import HttpResponseRedirect
from django.utils.text import normalize_newlines
from django.conf import settings
from django.utils.translation import ungettext, ugettext as _
from django.utils.encoding import smart_unicode
-import base64, datetime
COMMENTS_PER_PAGE = 20
+# TODO: This is a copy of the manipulator-based form that used to live in
+# contrib.auth.forms. It should be replaced with the newforms version that
+# has now been added to contrib.auth.forms when the comments app gets updated
+# for newforms.
+
+class AuthenticationForm(oldforms.Manipulator):
+ """
+ Base class for authenticating users. Extend this to get a form that accepts
+ username/password logins.
+ """
+ def __init__(self, request=None):
+ """
+ If request is passed in, the manipulator will validate that cookies are
+ enabled. Note that the request (a HttpRequest object) must have set a
+ cookie with the key TEST_COOKIE_NAME and value TEST_COOKIE_VALUE before
+ running this validator.
+ """
+ self.request = request
+ self.fields = [
+ oldforms.TextField(field_name="username", length=15, max_length=30, is_required=True,
+ validator_list=[self.isValidUser, self.hasCookiesEnabled]),
+ oldforms.PasswordField(field_name="password", length=15, max_length=30, is_required=True),
+ ]
+ self.user_cache = None
+
+ def hasCookiesEnabled(self, field_data, all_data):
+ if self.request and not self.request.session.test_cookie_worked():
+ raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.")
+
+ def isValidUser(self, field_data, all_data):
+ username = field_data
+ password = all_data.get('password', None)
+ self.user_cache = authenticate(username=username, password=password)
+ if self.user_cache is None:
+ raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
+ elif not self.user_cache.is_active:
+ raise validators.ValidationError, _("This account is inactive.")
+
+ def get_user_id(self):
+ if self.user_cache:
+ return self.user_cache.id
+ return None
+
+ def get_user(self):
+ return self.user_cache
+
class PublicCommentManipulator(AuthenticationForm):
"Manipulator that handles public registered comments"
def __init__(self, user, ratings_required, ratings_range, num_rating_choices):
diff --git a/django/contrib/flatpages/admin.py b/django/contrib/flatpages/admin.py
new file mode 100644
index 0000000000..02bbaf6b1a
--- /dev/null
+++ b/django/contrib/flatpages/admin.py
@@ -0,0 +1,15 @@
+from django.contrib import admin
+from django.contrib.flatpages.models import FlatPage
+from django.utils.translation import ugettext_lazy as _
+
+
+class FlatPageAdmin(admin.ModelAdmin):
+ fieldsets = (
+ (None, {'fields': ('url', 'title', 'content', 'sites')}),
+ (_('Advanced options'), {'classes': ('collapse',), 'fields': ('enable_comments', 'registration_required', 'template_name')}),
+ )
+ list_display = ('url', 'title')
+ list_filter = ('sites', 'enable_comments', 'registration_required')
+ search_fields = ('url', 'title')
+
+admin.site.register(FlatPage, FlatPageAdmin) \ No newline at end of file
diff --git a/django/contrib/flatpages/models.py b/django/contrib/flatpages/models.py
index d61e9a3b1c..466425cb46 100644
--- a/django/contrib/flatpages/models.py
+++ b/django/contrib/flatpages/models.py
@@ -20,16 +20,7 @@ class FlatPage(models.Model):
verbose_name = _('flat page')
verbose_name_plural = _('flat pages')
ordering = ('url',)
-
- class Admin:
- fields = (
- (None, {'fields': ('url', 'title', 'content', 'sites')}),
- (_('Advanced options'), {'classes': 'collapse', 'fields': ('enable_comments', 'registration_required', 'template_name')}),
- )
- list_display = ('url', 'title')
- list_filter = ('sites', 'enable_comments', 'registration_required')
- search_fields = ('url', 'title')
-
+
def __unicode__(self):
return u"%s -- %s" % (self.url, self.title)
diff --git a/django/contrib/redirects/models.py b/django/contrib/redirects/models.py
index 1720f33466..991423268d 100644
--- a/django/contrib/redirects/models.py
+++ b/django/contrib/redirects/models.py
@@ -3,7 +3,7 @@ from django.contrib.sites.models import Site
from django.utils.translation import ugettext_lazy as _
class Redirect(models.Model):
- site = models.ForeignKey(Site, radio_admin=models.VERTICAL)
+ site = models.ForeignKey(Site)
old_path = models.CharField(_('redirect from'), max_length=200, db_index=True,
help_text=_("This should be an absolute path, excluding the domain name. Example: '/events/search/'."))
new_path = models.CharField(_('redirect to'), max_length=200, blank=True,
@@ -15,11 +15,21 @@ class Redirect(models.Model):
db_table = 'django_redirect'
unique_together=(('site', 'old_path'),)
ordering = ('old_path',)
+
+ def __unicode__(self):
+ return "%s ---> %s" % (self.old_path, self.new_path)
- class Admin:
- list_display = ('old_path', 'new_path')
- list_filter = ('site',)
- search_fields = ('old_path', 'new_path')
+# Register the admin options for these models.
+# TODO: Maybe this should live in a separate module admin.py, but how would we
+# ensure that module was loaded?
+
+from django.contrib import admin
+
+class RedirectAdmin(admin.ModelAdmin):
+ list_display = ('old_path', 'new_path')
+ list_filter = ('site',)
+ search_fields = ('old_path', 'new_path')
+ radio_fields = {'site': admin.VERTICAL}
+
+admin.site.register(Redirect, RedirectAdmin)
- def __unicode__(self):
- return u"%s ---> %s" % (self.old_path, self.new_path)
diff --git a/django/contrib/sites/admin.py b/django/contrib/sites/admin.py
new file mode 100644
index 0000000000..2442c24292
--- /dev/null
+++ b/django/contrib/sites/admin.py
@@ -0,0 +1,9 @@
+from django.contrib import admin
+from django.contrib.sites.models import Site
+
+
+class SiteAdmin(admin.ModelAdmin):
+ list_display = ('domain', 'name')
+ search_fields = ('domain', 'name')
+
+admin.site.register(Site, SiteAdmin) \ No newline at end of file
diff --git a/django/contrib/sites/models.py b/django/contrib/sites/models.py
index c928a4e852..c44e5ce11f 100644
--- a/django/contrib/sites/models.py
+++ b/django/contrib/sites/models.py
@@ -32,18 +32,16 @@ class Site(models.Model):
domain = models.CharField(_('domain name'), max_length=100)
name = models.CharField(_('display name'), max_length=50)
objects = SiteManager()
+
class Meta:
db_table = 'django_site'
verbose_name = _('site')
verbose_name_plural = _('sites')
ordering = ('domain',)
- class Admin:
- list_display = ('domain', 'name')
- search_fields = ('domain', 'name')
def __unicode__(self):
return self.domain
-
+
def delete(self):
pk = self.pk
super(Site, self).delete()
@@ -51,7 +49,6 @@ class Site(models.Model):
del(SITE_CACHE[pk])
except KeyError:
pass
-
class RequestSite(object):
"""
diff --git a/django/core/management/validation.py b/django/core/management/validation.py
index cd1f84f34b..e17409ae5d 100644
--- a/django/core/management/validation.py
+++ b/django/core/management/validation.py
@@ -51,8 +51,6 @@ def get_validation_errors(outfile, app=None):
from PIL import Image
except ImportError:
e.add(opts, '"%s": To use ImageFields, you need to install the Python Imaging Library. Get it at http://www.pythonware.com/products/pil/ .' % f.name)
- if f.prepopulate_from is not None and type(f.prepopulate_from) not in (list, tuple):
- e.add(opts, '"%s": prepopulate_from should be a list or tuple.' % f.name)
if f.choices:
if isinstance(f.choices, basestring) or not is_iterable(f.choices):
e.add(opts, '"%s": "choices" should be iterable (e.g., a tuple or list).' % f.name)
@@ -145,54 +143,6 @@ def get_validation_errors(outfile, app=None):
if r.get_accessor_name() == rel_query_name:
e.add(opts, "Reverse query name for m2m field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
- # Check admin attribute.
- if opts.admin is not None:
- if not isinstance(opts.admin, models.AdminOptions):
- e.add(opts, '"admin" attribute, if given, must be set to a models.AdminOptions() instance.')
- else:
- # list_display
- if not isinstance(opts.admin.list_display, (list, tuple)):
- e.add(opts, '"admin.list_display", if given, must be set to a list or tuple.')
- else:
- for fn in opts.admin.list_display:
- try:
- f = opts.get_field(fn)
- except models.FieldDoesNotExist:
- if not hasattr(cls, fn):
- e.add(opts, '"admin.list_display" refers to %r, which isn\'t an attribute, method or property.' % fn)
- else:
- if isinstance(f, models.ManyToManyField):
- e.add(opts, '"admin.list_display" doesn\'t support ManyToManyFields (%r).' % fn)
- # list_display_links
- if opts.admin.list_display_links and not opts.admin.list_display:
- e.add(opts, '"admin.list_display" must be defined for "admin.list_display_links" to be used.')
- if not isinstance(opts.admin.list_display_links, (list, tuple)):
- e.add(opts, '"admin.list_display_links", if given, must be set to a list or tuple.')
- else:
- for fn in opts.admin.list_display_links:
- try:
- f = opts.get_field(fn)
- except models.FieldDoesNotExist:
- if not hasattr(cls, fn):
- e.add(opts, '"admin.list_display_links" refers to %r, which isn\'t an attribute, method or property.' % fn)
- if fn not in opts.admin.list_display:
- e.add(opts, '"admin.list_display_links" refers to %r, which is not defined in "admin.list_display".' % fn)
- # list_filter
- if not isinstance(opts.admin.list_filter, (list, tuple)):
- e.add(opts, '"admin.list_filter", if given, must be set to a list or tuple.')
- else:
- for fn in opts.admin.list_filter:
- try:
- f = opts.get_field(fn)
- except models.FieldDoesNotExist:
- e.add(opts, '"admin.list_filter" refers to %r, which isn\'t a field.' % fn)
- # date_hierarchy
- if opts.admin.date_hierarchy:
- try:
- f = opts.get_field(opts.admin.date_hierarchy)
- except models.FieldDoesNotExist:
- e.add(opts, '"admin.date_hierarchy" refers to %r, which isn\'t a field.' % opts.admin.date_hierarchy)
-
# Check ordering attribute.
if opts.ordering:
for field_name in opts.ordering:
@@ -210,18 +160,6 @@ def get_validation_errors(outfile, app=None):
except models.FieldDoesNotExist:
e.add(opts, '"ordering" refers to "%s", a field that doesn\'t exist.' % field_name)
- # Check core=True, if needed.
- for related in opts.get_followed_related_objects():
- if not related.edit_inline:
- continue
- try:
- for f in related.opts.fields:
- if f.core:
- raise StopIteration
- e.add(related.opts, "At least one field in %s should have core=True, because it's being edited inline by %s.%s." % (related.opts.object_name, opts.module_name, opts.object_name))
- except StopIteration:
- pass
-
# Check unique_together.
for ut in opts.unique_together:
for field_name in ut:
diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py
index 86763d99f9..bd6cc3542d 100644
--- a/django/db/models/__init__.py
+++ b/django/db/models/__init__.py
@@ -5,7 +5,7 @@ from django.db import connection
from django.db.models.loading import get_apps, get_app, get_models, get_model, register_models
from django.db.models.query import Q
from django.db.models.manager import Manager
-from django.db.models.base import Model, AdminOptions
+from django.db.models.base import Model
from django.db.models.fields import *
from django.db.models.fields.subclassing import SubfieldBase
from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel, TABULAR, STACKED
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 757f2378ce..e2ba49ee8c 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -15,7 +15,7 @@ from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned,
from django.db.models.fields import AutoField, ImageField, FieldDoesNotExist
from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
from django.db.models.query import delete_objects, Q, CollectedObjects
-from django.db.models.options import Options, AdminOptions
+from django.db.models.options import Options
from django.db import connection, transaction
from django.db.models import signals
from django.db.models.loading import register_models, get_model
@@ -137,9 +137,6 @@ class ModelBase(type):
return get_model(new_class._meta.app_label, name, False)
def add_to_class(cls, name, value):
- if name == 'Admin':
- assert type(value) == types.ClassType, "%r attribute of %s model must be a class, not a %s object" % (name, cls.__name__, type(value))
- value = AdminOptions(**dict([(k, v) for k, v in value.__dict__.items() if not k.startswith('_')]))
if hasattr(value, 'contribute_to_class'):
value.contribute_to_class(cls, name)
else:
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 713343cc15..879807d2d2 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -28,16 +28,10 @@ from django.utils import datetime_safe
class NOT_PROVIDED:
pass
-# Values for filter_interface.
-HORIZONTAL, VERTICAL = 1, 2
-
# The values to use for "blank" in SelectFields. Will be appended to the start of most "choices" lists.
BLANK_CHOICE_DASH = [("", "---------")]
BLANK_CHOICE_NONE = [("", "None")]
-# returns the <ul> class for a given radio_admin value
-get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
-
class FieldDoesNotExist(Exception):
pass
@@ -85,10 +79,10 @@ class Field(object):
def __init__(self, verbose_name=None, name=None, primary_key=False,
max_length=None, unique=False, blank=False, null=False,
db_index=False, core=False, rel=None, default=NOT_PROVIDED,
- editable=True, serialize=True, prepopulate_from=None,
- unique_for_date=None, unique_for_month=None, unique_for_year=None,
- validator_list=None, choices=None, radio_admin=None, help_text='',
- db_column=None, db_tablespace=None, auto_created=False):
+ editable=True, serialize=True, unique_for_date=None,
+ unique_for_month=None, unique_for_year=None, validator_list=None,
+ choices=None, help_text='', db_column=None, db_tablespace=None,
+ auto_created=False):
self.name = name
self.verbose_name = verbose_name
self.primary_key = primary_key
@@ -102,11 +96,9 @@ class Field(object):
self.editable = editable
self.serialize = serialize
self.validator_list = validator_list or []
- self.prepopulate_from = prepopulate_from
self.unique_for_date, self.unique_for_month = unique_for_date, unique_for_month
self.unique_for_year = unique_for_year
self._choices = choices or []
- self.radio_admin = radio_admin
self.help_text = help_text
self.db_column = db_column
self.db_tablespace = db_tablespace or settings.DEFAULT_INDEX_TABLESPACE
@@ -294,11 +286,7 @@ class Field(object):
params['max_length'] = self.max_length
if self.choices:
- if self.radio_admin:
- field_objs = [oldforms.RadioSelectField]
- params['ul_class'] = get_ul_class(self.radio_admin)
- else:
- field_objs = [oldforms.SelectField]
+ field_objs = [oldforms.SelectField]
params['choices'] = self.get_choices_default()
else:
@@ -386,10 +374,7 @@ class Field(object):
return first_choice + lst
def get_choices_default(self):
- if self.radio_admin:
- return self.get_choices(include_blank=self.blank, blank_choice=BLANK_CHOICE_NONE)
- else:
- return self.get_choices()
+ return self.get_choices()
def _get_val_from_obj(self, obj):
if obj:
@@ -1012,7 +997,11 @@ class NullBooleanField(Field):
return [oldforms.NullBooleanField]
def formfield(self, **kwargs):
- defaults = {'form_class': forms.NullBooleanField}
+ defaults = {
+ 'form_class': forms.NullBooleanField,
+ 'required': not self.blank,
+ 'label': capfirst(self.verbose_name),
+ 'help_text': self.help_text}
defaults.update(kwargs)
return super(NullBooleanField, self).formfield(**defaults)
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index a1977c07b4..594236b4c6 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -1,6 +1,6 @@
from django.db import connection, transaction
from django.db.models import signals, get_model
-from django.db.models.fields import AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, get_ul_class, FieldDoesNotExist
+from django.db.models.fields import AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist
from django.db.models.related import RelatedObject
from django.db.models.query_utils import QueryWrapper
from django.utils.text import capfirst
@@ -541,7 +541,7 @@ class ManyToOneRel(object):
def __init__(self, to, field_name, num_in_admin=3, min_num_in_admin=None,
max_num_in_admin=None, num_extra_on_change=1, edit_inline=False,
related_name=None, limit_choices_to=None, lookup_overrides=None,
- raw_id_admin=False, parent_link=False):
+ parent_link=False):
try:
to._meta
except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
@@ -554,7 +554,6 @@ class ManyToOneRel(object):
limit_choices_to = {}
self.limit_choices_to = limit_choices_to
self.lookup_overrides = lookup_overrides or {}
- self.raw_id_admin = raw_id_admin
self.multiple = True
self.parent_link = parent_link
@@ -573,34 +572,29 @@ class OneToOneRel(ManyToOneRel):
def __init__(self, to, field_name, num_in_admin=0, min_num_in_admin=None,
max_num_in_admin=None, num_extra_on_change=None, edit_inline=False,
related_name=None, limit_choices_to=None, lookup_overrides=None,
- raw_id_admin=False, parent_link=False):
+ parent_link=False):
# NOTE: *_num_in_admin and num_extra_on_change are intentionally
# ignored here. We accept them as parameters only to match the calling
# signature of ManyToOneRel.__init__().
super(OneToOneRel, self).__init__(to, field_name, num_in_admin,
edit_inline=edit_inline, related_name=related_name,
limit_choices_to=limit_choices_to,
- lookup_overrides=lookup_overrides, raw_id_admin=raw_id_admin,
- parent_link=parent_link)
+ lookup_overrides=lookup_overrides, parent_link=parent_link)
self.multiple = False
class ManyToManyRel(object):
def __init__(self, to, num_in_admin=0, related_name=None,
- filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True):
+ limit_choices_to=None, symmetrical=True):
self.to = to
self.num_in_admin = num_in_admin
self.related_name = related_name
- self.filter_interface = filter_interface
if limit_choices_to is None:
limit_choices_to = {}
self.limit_choices_to = limit_choices_to
self.edit_inline = False
- self.raw_id_admin = raw_id_admin
self.symmetrical = symmetrical
self.multiple = True
- assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface"
-
class ForeignKey(RelatedField, Field):
empty_strings_allowed = False
def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
@@ -626,7 +620,6 @@ class ForeignKey(RelatedField, Field):
related_name=kwargs.pop('related_name', None),
limit_choices_to=kwargs.pop('limit_choices_to', None),
lookup_overrides=kwargs.pop('lookup_overrides', None),
- raw_id_admin=kwargs.pop('raw_id_admin', False),
parent_link=kwargs.pop('parent_link', False))
Field.__init__(self, **kwargs)
@@ -640,19 +633,11 @@ class ForeignKey(RelatedField, Field):
def prepare_field_objs_and_params(self, manipulator, name_prefix):
params = {'validator_list': self.validator_list[:], 'member_name': name_prefix + self.attname}
- if self.rel.raw_id_admin:
- field_objs = self.get_manipulator_field_objs()
- params['validator_list'].append(curry(manipulator_valid_rel_key, self, manipulator))
+ if self.null:
+ field_objs = [oldforms.NullSelectField]
else:
- if self.radio_admin:
- field_objs = [oldforms.RadioSelectField]
- params['ul_class'] = get_ul_class(self.radio_admin)
- else:
- if self.null:
- field_objs = [oldforms.NullSelectField]
- else:
- field_objs = [oldforms.SelectField]
- params['choices'] = self.get_choices_default()
+ field_objs = [oldforms.SelectField]
+ params['choices'] = self.get_choices_default()
return field_objs, params
def get_default(self):
@@ -664,10 +649,7 @@ class ForeignKey(RelatedField, Field):
def get_manipulator_field_objs(self):
rel_field = self.rel.get_related_field()
- if self.rel.raw_id_admin and not isinstance(rel_field, AutoField):
- return rel_field.get_manipulator_field_objs()
- else:
- return [oldforms.IntegerField]
+ return [oldforms.IntegerField]
def get_db_prep_save(self, value):
if value == '' or value == None:
@@ -679,15 +661,11 @@ class ForeignKey(RelatedField, Field):
if not obj:
# In required many-to-one fields with only one available choice,
# select that one available choice. Note: For SelectFields
- # (radio_admin=False), we have to check that the length of choices
- # is *2*, not 1, because SelectFields always have an initial
- # "blank" value. Otherwise (radio_admin=True), we check that the
- # length is 1.
- if not self.blank and (not self.rel.raw_id_admin or self.choices):
+ # we have to check that the length of choices is *2*, not 1,
+ # because SelectFields always have an initial "blank" value.
+ if not self.blank and self.choices:
choice_list = self.get_choices_default()
- if self.radio_admin and len(choice_list) == 1:
- return {self.attname: choice_list[0][0]}
- if not self.radio_admin and len(choice_list) == 2:
+ if len(choice_list) == 2:
return {self.attname: choice_list[1][0]}
return Field.flatten_data(self, follow, obj)
@@ -704,7 +682,7 @@ class ForeignKey(RelatedField, Field):
setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
def formfield(self, **kwargs):
- defaults = {'form_class': forms.ModelChoiceField, 'queryset': self.rel.to._default_manager.all()}
+ defaults = {'form_class': forms.ModelChoiceField, 'queryset': self.rel.to._default_manager.complex_filter(self.rel.limit_choices_to)}
defaults.update(kwargs)
return super(ForeignKey, self).formfield(**defaults)
@@ -743,27 +721,17 @@ class ManyToManyField(RelatedField, Field):
kwargs['rel'] = ManyToManyRel(to,
num_in_admin=kwargs.pop('num_in_admin', 0),
related_name=kwargs.pop('related_name', None),
- filter_interface=kwargs.pop('filter_interface', None),
limit_choices_to=kwargs.pop('limit_choices_to', None),
- raw_id_admin=kwargs.pop('raw_id_admin', False),
symmetrical=kwargs.pop('symmetrical', True))
self.db_table = kwargs.pop('db_table', None)
- if kwargs["rel"].raw_id_admin:
- kwargs.setdefault("validator_list", []).append(self.isValidIDList)
Field.__init__(self, **kwargs)
- if self.rel.raw_id_admin:
- msg = ugettext_lazy('Separate multiple IDs with commas.')
- else:
- msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
+ msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
self.help_text = string_concat(self.help_text, ' ', msg)
def get_manipulator_field_objs(self):
- if self.rel.raw_id_admin:
- return [oldforms.RawIdAdminField]
- else:
- choices = self.get_choices_default()
- return [curry(oldforms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)]
+ choices = self.get_choices_default()
+ return [curry(oldforms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)]
def get_choices_default(self):
return Field.get_choices(self, include_blank=False)
@@ -812,14 +780,11 @@ class ManyToManyField(RelatedField, Field):
new_data = {}
if obj:
instance_ids = [instance._get_pk_val() for instance in getattr(obj, self.name).all()]
- if self.rel.raw_id_admin:
- new_data[self.name] = u",".join([smart_unicode(id) for id in instance_ids])
- else:
- new_data[self.name] = instance_ids
+ new_data[self.name] = instance_ids
else:
# In required many-to-many fields with only one available choice,
# select that one available choice.
- if not self.blank and not self.rel.edit_inline and not self.rel.raw_id_admin:
+ if not self.blank and not self.rel.edit_inline:
choices_list = self.get_choices_default()
if len(choices_list) == 1:
new_data[self.name] = [choices_list[0][0]]
@@ -861,7 +826,7 @@ class ManyToManyField(RelatedField, Field):
setattr(instance, self.attname, data)
def formfield(self, **kwargs):
- defaults = {'form_class': forms.ModelMultipleChoiceField, 'queryset': self.rel.to._default_manager.all()}
+ defaults = {'form_class': forms.ModelMultipleChoiceField, 'queryset': self.rel.to._default_manager.complex_filter(self.rel.limit_choices_to)}
defaults.update(kwargs)
# If initial is passed in, it's a list of related objects, but the
# MultipleChoiceField takes a list of IDs.
diff --git a/django/db/models/manipulators.py b/django/db/models/manipulators.py
index 2d953260fb..4e6ddca26e 100644
--- a/django/db/models/manipulators.py
+++ b/django/db/models/manipulators.py
@@ -120,10 +120,7 @@ class AutomaticManipulator(oldforms.Manipulator):
for f in self.opts.many_to_many:
if self.follow.get(f.name, None):
if not f.rel.edit_inline:
- if f.rel.raw_id_admin:
- new_vals = new_data.get(f.name, ())
- else:
- new_vals = new_data.getlist(f.name)
+ new_vals = new_data.getlist(f.name)
# First, clear the existing values.
rel_manager = getattr(new_object, f.name)
rel_manager.clear()
@@ -220,8 +217,6 @@ class AutomaticManipulator(oldforms.Manipulator):
for f in related.opts.many_to_many:
if child_follow.get(f.name, None) and not f.rel.edit_inline:
new_value = rel_new_data[f.attname]
- if f.rel.raw_id_admin:
- new_value = new_value[0]
setattr(new_rel_obj, f.name, f.rel.to.objects.filter(pk__in=new_value))
if self.change:
self.fields_changed.append('%s for %s "%s"' % (f.verbose_name, related.opts.verbose_name, new_rel_obj))
diff --git a/django/db/models/options.py b/django/db/models/options.py
index a81a34d722..ffea6d5082 100644
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -11,7 +11,6 @@ from django.db.models.fields.related import ManyToManyRel
from django.db.models.fields import AutoField, FieldDoesNotExist
from django.db.models.fields.proxy import OrderWrt
from django.db.models.loading import get_models, app_cache_ready
-from django.db.models import Manager
from django.utils.translation import activate, deactivate_all, get_language, string_concat
from django.utils.encoding import force_unicode, smart_str
from django.utils.datastructures import SortedDict
@@ -485,77 +484,3 @@ class Options(object):
else:
self._field_types[field_type] = False
return self._field_types[field_type]
-
-class AdminOptions(object):
- def __init__(self, fields=None, js=None, list_display=None, list_display_links=None, list_filter=None,
- date_hierarchy=None, save_as=False, ordering=None, search_fields=None,
- save_on_top=False, list_select_related=False, manager=None, list_per_page=100):
- self.fields = fields
- self.js = js or []
- self.list_display = list_display or ['__str__']
- self.list_display_links = list_display_links or []
- self.list_filter = list_filter or []
- self.date_hierarchy = date_hierarchy
- self.save_as, self.ordering = save_as, ordering
- self.search_fields = search_fields or []
- self.save_on_top = save_on_top
- self.list_select_related = list_select_related
- self.list_per_page = list_per_page
- self.manager = manager or Manager()
-
- def get_field_sets(self, opts):
- "Returns a list of AdminFieldSet objects for this AdminOptions object."
- if self.fields is None:
- field_struct = ((None, {'fields': [f.name for f in opts.fields + opts.many_to_many if f.editable and not isinstance(f, AutoField)]}),)
- else:
- field_struct = self.fields
- new_fieldset_list = []
- for fieldset in field_struct:
- fs_options = fieldset[1]
- classes = fs_options.get('classes', ())
- description = fs_options.get('description', '')
- new_fieldset_list.append(AdminFieldSet(fieldset[0], classes,
- opts.get_field, fs_options['fields'], description))
- return new_fieldset_list
-
- def contribute_to_class(self, cls, name):
- cls._meta.admin = self
- # Make sure the admin manager has access to the model
- self.manager.model = cls
-
-class AdminFieldSet(object):
- def __init__(self, name, classes, field_locator_func, line_specs, description):
- self.name = name
- self.field_lines = [AdminFieldLine(field_locator_func, line_spec) for line_spec in line_specs]
- self.classes = classes
- self.description = description
-
- def __repr__(self):
- return "FieldSet: (%s, %s)" % (self.name, self.field_lines)
-
- def bind(self, field_mapping, original, bound_field_set_class):
- return bound_field_set_class(self, field_mapping, original)
-
- def __iter__(self):
- for field_line in self.field_lines:
- yield field_line
-
- def __len__(self):
- return len(self.field_lines)
-
-class AdminFieldLine(object):
- def __init__(self, field_locator_func, linespec):
- if isinstance(linespec, basestring):
- self.fields = [field_locator_func(linespec)]
- else:
- self.fields = [field_locator_func(field_name) for field_name in linespec]
-
- def bind(self, field_mapping, original, bound_field_line_class):
- return bound_field_line_class(self, field_mapping, original)
-
- def __iter__(self):
- for field in self.fields:
- yield field
-
- def __len__(self):
- return len(self.fields)
diff --git a/django/newforms/__init__.py b/django/newforms/__init__.py
index 0d9c68f9e0..99631e4e8f 100644
--- a/django/newforms/__init__.py
+++ b/django/newforms/__init__.py
@@ -15,3 +15,4 @@ from widgets import *
from fields import *
from forms import *
from models import *
+from formsets import * \ No newline at end of file
diff --git a/django/newforms/forms.py b/django/newforms/forms.py
index fc203f36b5..753ee254bc 100644
--- a/django/newforms/forms.py
+++ b/django/newforms/forms.py
@@ -10,7 +10,7 @@ from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode
from django.utils.safestring import mark_safe
from fields import Field, FileField
-from widgets import TextInput, Textarea
+from widgets import Media, media_property, TextInput, Textarea
from util import flatatt, ErrorDict, ErrorList, ValidationError
__all__ = ('BaseForm', 'Form')
@@ -31,6 +31,7 @@ def get_declared_fields(bases, attrs, with_base_fields=True):
If 'with_base_fields' is True, all fields from the bases are used.
Otherwise, only fields in the 'declared_fields' attribute on the bases are
used. The distinction is useful in ModelForm subclassing.
+ Also integrates any additional media definitions
"""
fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
@@ -56,8 +57,11 @@ class DeclarativeFieldsMetaclass(type):
"""
def __new__(cls, name, bases, attrs):
attrs['base_fields'] = get_declared_fields(bases, attrs)
- return super(DeclarativeFieldsMetaclass,
+ new_class = super(DeclarativeFieldsMetaclass,
cls).__new__(cls, name, bases, attrs)
+ if 'media' not in attrs:
+ new_class.media = media_property(new_class)
+ return new_class
class BaseForm(StrAndUnicode):
# This is the main implementation of all the Form logic. Note that this
@@ -65,7 +69,8 @@ class BaseForm(StrAndUnicode):
# information. Any improvements to the form API should be made to *this*
# class, not to the Form class.
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
- initial=None, error_class=ErrorList, label_suffix=':'):
+ initial=None, error_class=ErrorList, label_suffix=':',
+ empty_permitted=False):
self.is_bound = data is not None or files is not None
self.data = data or {}
self.files = files or {}
@@ -74,7 +79,9 @@ class BaseForm(StrAndUnicode):
self.initial = initial or {}
self.error_class = error_class
self.label_suffix = label_suffix
+ self.empty_permitted = empty_permitted
self._errors = None # Stores the errors after clean() has been called.
+ self._changed_data = None
# The base_fields class attribute is the *class-wide* definition of
# fields. Because a particular *instance* of the class might want to
@@ -194,6 +201,10 @@ class BaseForm(StrAndUnicode):
if not self.is_bound: # Stop further processing.
return
self.cleaned_data = {}
+ # If the form is permitted to be empty, and none of the form data has
+ # changed from the initial data, short circuit any validation.
+ if self.empty_permitted and not self.has_changed():
+ return
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
@@ -229,6 +240,40 @@ class BaseForm(StrAndUnicode):
"""
return self.cleaned_data
+ def has_changed(self):
+ """
+ Returns True if data differs from initial.
+ """
+ return bool(self.changed_data)
+
+ def _get_changed_data(self):
+ if self._changed_data is None:
+ self._changed_data = []
+ # XXX: For now we're asking the individual widgets whether or not the
+ # data has changed. It would probably be more efficient to hash the
+ # initial data, store it in a hidden field, and compare a hash of the
+ # submitted data, but we'd need a way to easily get the string value
+ # for a given field. Right now, that logic is embedded in the render
+ # method of each widget.
+ for name, field in self.fields.items():
+ prefixed_name = self.add_prefix(name)
+ data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
+ initial_value = self.initial.get(name, field.initial)
+ if field.widget._has_changed(initial_value, data_value):
+ self._changed_data.append(name)
+ return self._changed_data
+ changed_data = property(_get_changed_data)
+
+ def _get_media(self):
+ """
+ Provide a description of all media required to render the widgets on this form
+ """
+ media = Media()
+ for field in self.fields.values():
+ media = media + field.widget.media
+ return media
+ media = property(_get_media)
+
def is_multipart(self):
"""
Returns True if the form needs to be multipart-encrypted, i.e. it has
diff --git a/django/newforms/formsets.py b/django/newforms/formsets.py
new file mode 100644
index 0000000000..1ae27bf58c
--- /dev/null
+++ b/django/newforms/formsets.py
@@ -0,0 +1,292 @@
+from forms import Form
+from django.utils.encoding import StrAndUnicode
+from django.utils.safestring import mark_safe
+from fields import IntegerField, BooleanField
+from widgets import Media, HiddenInput, TextInput
+from util import ErrorList, ValidationError
+
+__all__ = ('BaseFormSet', 'all_valid')
+
+# special field names
+TOTAL_FORM_COUNT = 'TOTAL_FORMS'
+INITIAL_FORM_COUNT = 'INITIAL_FORMS'
+MAX_FORM_COUNT = 'MAX_FORMS'
+ORDERING_FIELD_NAME = 'ORDER'
+DELETION_FIELD_NAME = 'DELETE'
+
+class ManagementForm(Form):
+ """
+ ``ManagementForm`` is used to keep track of how many form instances
+ are displayed on the page. If adding new forms via javascript, you should
+ increment the count field of this form as well.
+ """
+ def __init__(self, *args, **kwargs):
+ self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
+ self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
+ self.base_fields[MAX_FORM_COUNT] = IntegerField(widget=HiddenInput)
+ super(ManagementForm, self).__init__(*args, **kwargs)
+
+class BaseFormSet(StrAndUnicode):
+ """
+ A collection of instances of the same Form class.
+ """
+ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
+ initial=None, error_class=ErrorList):
+ self.is_bound = data is not None or files is not None
+ self.prefix = prefix or 'form'
+ self.auto_id = auto_id
+ self.data = data
+ self.files = files
+ self.initial = initial
+ self.error_class = error_class
+ self._errors = None
+ self._non_form_errors = None
+ # initialization is different depending on whether we recieved data, initial, or nothing
+ if data or files:
+ self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix)
+ if self.management_form.is_valid():
+ self._total_form_count = self.management_form.cleaned_data[TOTAL_FORM_COUNT]
+ self._initial_form_count = self.management_form.cleaned_data[INITIAL_FORM_COUNT]
+ self._max_form_count = self.management_form.cleaned_data[MAX_FORM_COUNT]
+ else:
+ raise ValidationError('ManagementForm data is missing or has been tampered with')
+ else:
+ if initial:
+ self._initial_form_count = len(initial)
+ if self._initial_form_count > self._max_form_count and self._max_form_count > 0:
+ self._initial_form_count = self._max_form_count
+ self._total_form_count = self._initial_form_count + self.extra
+ else:
+ self._initial_form_count = 0
+ self._total_form_count = self.extra
+ if self._total_form_count > self._max_form_count and self._max_form_count > 0:
+ self._total_form_count = self._max_form_count
+ initial = {TOTAL_FORM_COUNT: self._total_form_count,
+ INITIAL_FORM_COUNT: self._initial_form_count,
+ MAX_FORM_COUNT: self._max_form_count}
+ self.management_form = ManagementForm(initial=initial, auto_id=self.auto_id, prefix=self.prefix)
+
+ # construct the forms in the formset
+ self._construct_forms()
+
+ def __unicode__(self):
+ return self.as_table()
+
+ def _construct_forms(self):
+ # instantiate all the forms and put them in self.forms
+ self.forms = []
+ for i in xrange(self._total_form_count):
+ self.forms.append(self._construct_form(i))
+
+ def _construct_form(self, i, **kwargs):
+ """
+ Instantiates and returns the i-th form instance in a formset.
+ """
+ defaults = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
+ if self.data or self.files:
+ defaults['data'] = self.data
+ defaults['files'] = self.files
+ if self.initial:
+ try:
+ defaults['initial'] = self.initial[i]
+ except IndexError:
+ pass
+ # Allow extra forms to be empty.
+ if i >= self._initial_form_count:
+ defaults['empty_permitted'] = True
+ defaults.update(kwargs)
+ form = self.form(**defaults)
+ self.add_fields(form, i)
+ return form
+
+ def _get_initial_forms(self):
+ """Return a list of all the intial forms in this formset."""
+ return self.forms[:self._initial_form_count]
+ initial_forms = property(_get_initial_forms)
+
+ def _get_extra_forms(self):
+ """Return a list of all the extra forms in this formset."""
+ return self.forms[self._initial_form_count:]
+ extra_forms = property(_get_extra_forms)
+
+ # Maybe this should just go away?
+ def _get_cleaned_data(self):
+ """
+ Returns a list of form.cleaned_data dicts for every form in self.forms.
+ """
+ if not self.is_valid():
+ raise AttributeError("'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__)
+ return [form.cleaned_data for form in self.forms]
+ cleaned_data = property(_get_cleaned_data)
+
+ def _get_deleted_forms(self):
+ """
+ Returns a list of forms that have been marked for deletion. Raises an
+ AttributeError is deletion is not allowed.
+ """
+ if not self.is_valid() or not self.can_delete:
+ raise AttributeError("'%s' object has no attribute 'deleted_forms'" % self.__class__.__name__)
+ # construct _deleted_form_indexes which is just a list of form indexes
+ # that have had their deletion widget set to True
+ if not hasattr(self, '_deleted_form_indexes'):
+ self._deleted_form_indexes = []
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ # if this is an extra form and hasn't changed, don't consider it
+ if i >= self._initial_form_count and not form.has_changed():
+ continue
+ if form.cleaned_data[DELETION_FIELD_NAME]:
+ self._deleted_form_indexes.append(i)
+ return [self.forms[i] for i in self._deleted_form_indexes]
+ deleted_forms = property(_get_deleted_forms)
+
+ def _get_ordered_forms(self):
+ """
+ Returns a list of form in the order specified by the incoming data.
+ Raises an AttributeError is deletion is not allowed.
+ """
+ if not self.is_valid() or not self.can_order:
+ raise AttributeError("'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__)
+ # Construct _ordering, which is a list of (form_index, order_field_value)
+ # tuples. After constructing this list, we'll sort it by order_field_value
+ # so we have a way to get to the form indexes in the order specified
+ # by the form data.
+ if not hasattr(self, '_ordering'):
+ self._ordering = []
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ # if this is an extra form and hasn't changed, don't consider it
+ if i >= self._initial_form_count and not form.has_changed():
+ continue
+ # don't add data marked for deletion to self.ordered_data
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
+ continue
+ # A sort function to order things numerically ascending, but
+ # None should be sorted below anything else. Allowing None as
+ # a comparison value makes it so we can leave ordering fields
+ # blamk.
+ def compare_ordering_values(x, y):
+ if x[1] is None:
+ return 1
+ if y[1] is None:
+ return -1
+ return x[1] - y[1]
+ self._ordering.append((i, form.cleaned_data[ORDERING_FIELD_NAME]))
+ # After we're done populating self._ordering, sort it.
+ self._ordering.sort(compare_ordering_values)
+ # Return a list of form.cleaned_data dicts in the order spcified by
+ # the form data.
+ return [self.forms[i[0]] for i in self._ordering]
+ ordered_forms = property(_get_ordered_forms)
+
+ def non_form_errors(self):
+ """
+ Returns an ErrorList of errors that aren't associated with a particular
+ form -- i.e., from formset.clean(). Returns an empty ErrorList if there
+ are none.
+ """
+ if self._non_form_errors is not None:
+ return self._non_form_errors
+ return self.error_class()
+
+ def _get_errors(self):
+ """
+ Returns a list of form.errors for every form in self.forms.
+ """
+ if self._errors is None:
+ self.full_clean()
+ return self._errors
+ errors = property(_get_errors)
+
+ def is_valid(self):
+ """
+ Returns True if form.errors is empty for every form in self.forms.
+ """
+ if not self.is_bound:
+ return False
+ # We loop over every form.errors here rather than short circuiting on the
+ # first failure to make sure validation gets triggered for every form.
+ forms_valid = True
+ for errors in self.errors:
+ if bool(errors):
+ forms_valid = False
+ return forms_valid and not bool(self.non_form_errors())
+
+ def full_clean(self):
+ """
+ Cleans all of self.data and populates self._errors.
+ """
+ self._errors = []
+ if not self.is_bound: # Stop further processing.
+ return
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ self._errors.append(form.errors)
+ # Give self.clean() a chance to do cross-form validation.
+ try:
+ self.clean()
+ except ValidationError, e:
+ self._non_form_errors = e.messages
+
+ def clean(self):
+ """
+ Hook for doing any extra formset-wide cleaning after Form.clean() has
+ been called on every form. Any ValidationError raised by this method
+ will not be associated with a particular form; it will be accesible
+ via formset.non_form_errors()
+ """
+ pass
+
+ def add_fields(self, form, index):
+ """A hook for adding extra fields on to each form instance."""
+ if self.can_order:
+ # Only pre-fill the ordering field for initial forms.
+ if index < self._initial_form_count:
+ form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1, required=False)
+ else:
+ form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', required=False)
+ if self.can_delete:
+ form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False)
+
+ def add_prefix(self, index):
+ return '%s-%s' % (self.prefix, index)
+
+ def is_multipart(self):
+ """
+ Returns True if the formset needs to be multipart-encrypted, i.e. it
+ has FileInput. Otherwise, False.
+ """
+ return self.forms[0].is_multipart()
+
+ def _get_media(self):
+ # All the forms on a FormSet are the same, so you only need to
+ # interrogate the first form for media.
+ if self.forms:
+ return self.forms[0].media
+ else:
+ return Media()
+ media = property(_get_media)
+
+ def as_table(self):
+ "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>."
+ # XXX: there is no semantic division between forms here, there
+ # probably should be. It might make sense to render each form as a
+ # table row with each field as a td.
+ forms = u' '.join([form.as_table() for form in self.forms])
+ return mark_safe(u'\n'.join([unicode(self.management_form), forms]))
+
+def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
+ can_delete=False, max_num=0):
+ """Return a FormSet for the given form class."""
+ attrs = {'form': form, 'extra': extra,
+ 'can_order': can_order, 'can_delete': can_delete,
+ '_max_form_count': max_num}
+ return type(form.__name__ + 'FormSet', (formset,), attrs)
+
+def all_valid(formsets):
+ """Returns true if every formset in formsets is valid."""
+ valid = True
+ for formset in formsets:
+ if not formset.is_valid():
+ valid = False
+ return valid
diff --git a/django/newforms/models.py b/django/newforms/models.py
index c3938d9ae7..43e2978ba8 100644
--- a/django/newforms/models.py
+++ b/django/newforms/models.py
@@ -12,13 +12,15 @@ from django.core.exceptions import ImproperlyConfigured
from util import ValidationError, ErrorList
from forms import BaseForm, get_declared_fields
-from fields import Field, ChoiceField, EMPTY_VALUES
-from widgets import Select, SelectMultiple, MultipleHiddenInput
+from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
+from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
+from widgets import media_property
+from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME
__all__ = (
'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
- 'ModelChoiceField', 'ModelMultipleChoiceField'
+ 'ModelChoiceField', 'ModelMultipleChoiceField',
)
def save_instance(form, instance, fields=None, fail_message='saved',
@@ -30,7 +32,7 @@ def save_instance(form, instance, fields=None, fail_message='saved',
database. Returns ``instance``.
"""
from django.db import models
- opts = instance.__class__._meta
+ opts = instance._meta
if form.errors:
raise ValueError("The %s could not be %s because the data didn't"
" validate." % (opts.object_name, fail_message))
@@ -44,7 +46,7 @@ def save_instance(form, instance, fields=None, fail_message='saved',
f.save_form_data(instance, cleaned_data[f.name])
# Wrap up the saving of m2m data as a function.
def save_m2m():
- opts = instance.__class__._meta
+ opts = instance._meta
cleaned_data = form.cleaned_data
for f in opts.many_to_many:
if fields and f.name not in fields:
@@ -226,6 +228,8 @@ class ModelFormMetaclass(type):
if not parents:
return new_class
+ if 'media' not in attrs:
+ new_class.media = media_property(new_class)
declared_fields = get_declared_fields(bases, attrs, False)
opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
if opts.model:
@@ -244,7 +248,7 @@ class ModelFormMetaclass(type):
class BaseModelForm(BaseForm):
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=':',
- instance=None):
+ empty_permitted=False, instance=None):
opts = self._meta
if instance is None:
# if we didn't get an instance, instantiate a new one
@@ -256,7 +260,8 @@ class BaseModelForm(BaseForm):
# if initial was provided, it should override the values from instance
if initial is not None:
object_data.update(initial)
- BaseForm.__init__(self, data, files, auto_id, prefix, object_data, error_class, label_suffix)
+ BaseForm.__init__(self, data, files, auto_id, prefix, object_data,
+ error_class, label_suffix, empty_permitted)
def save(self, commit=True):
"""
@@ -275,6 +280,209 @@ class BaseModelForm(BaseForm):
class ModelForm(BaseModelForm):
__metaclass__ = ModelFormMetaclass
+def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
+ formfield_callback=lambda f: f.formfield()):
+ # HACK: we should be able to construct a ModelForm without creating
+ # and passing in a temporary inner class
+ class Meta:
+ pass
+ setattr(Meta, 'model', model)
+ setattr(Meta, 'fields', fields)
+ setattr(Meta, 'exclude', exclude)
+ class_name = model.__name__ + 'Form'
+ return ModelFormMetaclass(class_name, (form,), {'Meta': Meta,
+ 'formfield_callback': formfield_callback})
+
+
+# ModelFormSets ##############################################################
+
+class BaseModelFormSet(BaseFormSet):
+ """
+ A ``FormSet`` for editing a queryset and/or adding new objects to it.
+ """
+ model = None
+
+ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
+ queryset=None, **kwargs):
+ self.queryset = queryset
+ defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
+ if self._max_form_count > 0:
+ qs = self.get_queryset()[:self._max_form_count]
+ else:
+ qs = self.get_queryset()
+ defaults['initial'] = [model_to_dict(obj) for obj in qs]
+ defaults.update(kwargs)
+ super(BaseModelFormSet, self).__init__(**defaults)
+
+ def get_queryset(self):
+ if self.queryset is not None:
+ return self.queryset
+ return self.model._default_manager.get_query_set()
+
+ def save_new(self, form, commit=True):
+ """Saves and returns a new model instance for the given form."""
+ return save_instance(form, self.model(), commit=commit)
+
+ def save_existing(self, form, instance, commit=True):
+ """Saves and returns an existing model instance for the given form."""
+ return save_instance(form, instance, commit=commit)
+
+ def save(self, commit=True):
+ """Saves model instances for every form, adding and changing instances
+ as necessary, and returns the list of instances.
+ """
+ if not commit:
+ self.saved_forms = []
+ def save_m2m():
+ for form in self.saved_forms:
+ form.save_m2m()
+ self.save_m2m = save_m2m
+ return self.save_existing_objects(commit) + self.save_new_objects(commit)
+
+ def save_existing_objects(self, commit=True):
+ self.changed_objects = []
+ self.deleted_objects = []
+ if not self.get_queryset():
+ return []
+
+ # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
+ existing_objects = {}
+ for obj in self.get_queryset():
+ existing_objects[obj.pk] = obj
+ saved_instances = []
+ for form in self.initial_forms:
+ obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
+ self.deleted_objects.append(obj)
+ obj.delete()
+ else:
+ if form.changed_data:
+ self.changed_objects.append((obj, form.changed_data))
+ saved_instances.append(self.save_existing(form, obj, commit=commit))
+ if not commit:
+ self.saved_forms.append(form)
+ return saved_instances
+
+ def save_new_objects(self, commit=True):
+ self.new_objects = []
+ for form in self.extra_forms:
+ if not form.has_changed():
+ continue
+ # If someone has marked an add form for deletion, don't save the
+ # object.
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
+ continue
+ self.new_objects.append(self.save_new(form, commit=commit))
+ if not commit:
+ self.saved_forms.append(form)
+ return self.new_objects
+
+ def add_fields(self, form, index):
+ """Add a hidden field for the object's primary key."""
+ self._pk_field_name = self.model._meta.pk.attname
+ form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
+ super(BaseModelFormSet, self).add_fields(form, index)
+
+def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(),
+ formset=BaseModelFormSet,
+ extra=1, can_delete=False, can_order=False,
+ max_num=0, fields=None, exclude=None):
+ """
+ Returns a FormSet class for the given Django model class.
+ """
+ form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
+ formfield_callback=formfield_callback)
+ FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
+ can_order=can_order, can_delete=can_delete)
+ FormSet.model = model
+ return FormSet
+
+
+# InlineFormSets #############################################################
+
+class BaseInlineFormset(BaseModelFormSet):
+ """A formset for child objects related to a parent."""
+ def __init__(self, data=None, files=None, instance=None, save_as_new=False):
+ from django.db.models.fields.related import RelatedObject
+ self.instance = instance
+ self.save_as_new = save_as_new
+ # is there a better way to get the object descriptor?
+ self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
+ super(BaseInlineFormset, self).__init__(data, files, prefix=self.rel_name)
+
+ def _construct_forms(self):
+ if self.save_as_new:
+ self._total_form_count = self._initial_form_count
+ self._initial_form_count = 0
+ super(BaseInlineFormset, self)._construct_forms()
+
+ def get_queryset(self):
+ """
+ Returns this FormSet's queryset, but restricted to children of
+ self.instance
+ """
+ kwargs = {self.fk.name: self.instance}
+ return self.model._default_manager.filter(**kwargs)
+
+ def save_new(self, form, commit=True):
+ kwargs = {self.fk.get_attname(): self.instance.pk}
+ new_obj = self.model(**kwargs)
+ return save_instance(form, new_obj, commit=commit)
+
+def _get_foreign_key(parent_model, model, fk_name=None):
+ """
+ Finds and returns the ForeignKey from model to parent if there is one.
+ If fk_name is provided, assume it is the name of the ForeignKey field.
+ """
+ # avoid circular import
+ from django.db.models import ForeignKey
+ opts = model._meta
+ if fk_name:
+ fks_to_parent = [f for f in opts.fields if f.name == fk_name]
+ if len(fks_to_parent) == 1:
+ fk = fks_to_parent[0]
+ if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model:
+ raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
+ elif len(fks_to_parent) == 0:
+ raise Exception("%s has no field named '%s'" % (model, fk_name))
+ else:
+ # Try to discover what the ForeignKey from model to parent_model is
+ fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
+ if len(fks_to_parent) == 1:
+ fk = fks_to_parent[0]
+ elif len(fks_to_parent) == 0:
+ raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
+ else:
+ raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
+ return fk
+
+
+def inlineformset_factory(parent_model, model, form=ModelForm,
+ formset=BaseInlineFormset, fk_name=None,
+ fields=None, exclude=None,
+ extra=3, can_order=False, can_delete=True, max_num=0,
+ formfield_callback=lambda f: f.formfield()):
+ """
+ Returns an ``InlineFormset`` for the given kwargs.
+
+ You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
+ to ``parent_model``.
+ """
+ fk = _get_foreign_key(parent_model, model, fk_name=fk_name)
+ # let the formset handle object deletion by default
+
+ if exclude is not None:
+ exclude.append(fk.name)
+ else:
+ exclude = [fk.name]
+ FormSet = modelformset_factory(model, form=form,
+ formfield_callback=formfield_callback,
+ formset=formset,
+ extra=extra, can_delete=can_delete, can_order=can_order,
+ fields=fields, exclude=exclude, max_num=max_num)
+ FormSet.fk = fk
+ return FormSet
+
# Fields #####################################################################
diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py
index dc36530b93..2c9f3c2eba 100644
--- a/django/newforms/widgets.py
+++ b/django/newforms/widgets.py
@@ -9,7 +9,7 @@ except NameError:
import copy
from itertools import chain
-
+from django.conf import settings
from django.utils.datastructures import MultiValueDict
from django.utils.html import escape, conditional_escape
from django.utils.translation import ugettext
@@ -17,16 +17,118 @@ from django.utils.encoding import StrAndUnicode, force_unicode
from django.utils.safestring import mark_safe
from django.utils import datetime_safe
from util import flatatt
+from urlparse import urljoin
__all__ = (
- 'Widget', 'TextInput', 'PasswordInput',
+ 'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'PasswordInput',
'HiddenInput', 'MultipleHiddenInput',
'FileInput', 'DateTimeInput', 'Textarea', 'CheckboxInput',
'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget',
)
+MEDIA_TYPES = ('css','js')
+
+class Media(StrAndUnicode):
+ def __init__(self, media=None, **kwargs):
+ if media:
+ media_attrs = media.__dict__
+ else:
+ media_attrs = kwargs
+
+ self._css = {}
+ self._js = []
+
+ for name in MEDIA_TYPES:
+ getattr(self, 'add_' + name)(media_attrs.get(name, None))
+
+ # Any leftover attributes must be invalid.
+ # if media_attrs != {}:
+ # raise TypeError, "'class Media' has invalid attribute(s): %s" % ','.join(media_attrs.keys())
+
+ def __unicode__(self):
+ return self.render()
+
+ def render(self):
+ return u'\n'.join(chain(*[getattr(self, 'render_' + name)() for name in MEDIA_TYPES]))
+
+ def render_js(self):
+ return [u'<script type="text/javascript" src="%s"></script>' % self.absolute_path(path) for path in self._js]
+
+ def render_css(self):
+ # To keep rendering order consistent, we can't just iterate over items().
+ # We need to sort the keys, and iterate over the sorted list.
+ media = self._css.keys()
+ media.sort()
+ return chain(*[
+ [u'<link href="%s" type="text/css" media="%s" rel="stylesheet" />' % (self.absolute_path(path), medium)
+ for path in self._css[medium]]
+ for medium in media])
+
+ def absolute_path(self, path):
+ if path.startswith(u'http://') or path.startswith(u'https://') or path.startswith(u'/'):
+ return path
+ return urljoin(settings.MEDIA_URL,path)
+
+ def __getitem__(self, name):
+ "Returns a Media object that only contains media of the given type"
+ if name in MEDIA_TYPES:
+ return Media(**{name: getattr(self, '_' + name)})
+ raise KeyError('Unknown media type "%s"' % name)
+
+ def add_js(self, data):
+ if data:
+ self._js.extend([path for path in data if path not in self._js])
+
+ def add_css(self, data):
+ if data:
+ for medium, paths in data.items():
+ self._css.setdefault(medium, []).extend([path for path in paths if path not in self._css[medium]])
+
+ def __add__(self, other):
+ combined = Media()
+ for name in MEDIA_TYPES:
+ getattr(combined, 'add_' + name)(getattr(self, '_' + name, None))
+ getattr(combined, 'add_' + name)(getattr(other, '_' + name, None))
+ return combined
+
+def media_property(cls):
+ def _media(self):
+ # Get the media property of the superclass, if it exists
+ if hasattr(super(cls, self), 'media'):
+ base = super(cls, self).media
+ else:
+ base = Media()
+
+ # Get the media definition for this class
+ definition = getattr(cls, 'Media', None)
+ if definition:
+ extend = getattr(definition, 'extend', True)
+ if extend:
+ if extend == True:
+ m = base
+ else:
+ m = Media()
+ for medium in extend:
+ m = m + base[medium]
+ return m + Media(definition)
+ else:
+ return Media(definition)
+ else:
+ return base
+ return property(_media)
+
+class MediaDefiningClass(type):
+ "Metaclass for classes that can have media definitions"
+ def __new__(cls, name, bases, attrs):
+ new_class = super(MediaDefiningClass, cls).__new__(cls, name, bases,
+ attrs)
+ if 'media' not in attrs:
+ new_class.media = media_property(new_class)
+ return new_class
+
class Widget(object):
+ __metaclass__ = MediaDefiningClass
is_hidden = False # Determines whether this corresponds to an <input type="hidden">.
needs_multipart_form = False # Determines does this widget need multipart-encrypted form
@@ -65,6 +167,25 @@ class Widget(object):
"""
return data.get(name, None)
+ def _has_changed(self, initial, data):
+ """
+ Return True if data differs from initial.
+ """
+ # For purposes of seeing whether something has changed, None is
+ # the same as an empty string, if the data or inital value we get
+ # is None, replace it w/ u''.
+ if data is None:
+ data_value = u''
+ else:
+ data_value = data
+ if initial is None:
+ initial_value = u''
+ else:
+ initial_value = initial
+ if force_unicode(initial_value) != force_unicode(data_value):
+ return True
+ return False
+
def id_for_label(self, id_):
"""
Returns the HTML ID attribute of this Widget for use by a <label>,
@@ -143,6 +264,11 @@ class FileInput(Input):
def value_from_datadict(self, data, files, name):
"File widgets take data from FILES, not POST"
return files.get(name, None)
+
+ def _has_changed(self, initial, data):
+ if data is None:
+ return False
+ return True
class Textarea(Widget):
def __init__(self, attrs=None):
@@ -202,6 +328,11 @@ class CheckboxInput(Widget):
return False
return super(CheckboxInput, self).value_from_datadict(data, files, name)
+ def _has_changed(self, initial, data):
+ # Sometimes data or initial could be None or u'' which should be the
+ # same thing as False.
+ return bool(initial) != bool(data)
+
class Select(Widget):
def __init__(self, attrs=None, choices=()):
super(Select, self).__init__(attrs)
@@ -244,6 +375,11 @@ class NullBooleanSelect(Select):
value = data.get(name, None)
return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
+ def _has_changed(self, initial, data):
+ # Sometimes data or initial could be None or u'' which should be the
+ # same thing as False.
+ return bool(initial) != bool(data)
+
class SelectMultiple(Widget):
def __init__(self, attrs=None, choices=()):
super(SelectMultiple, self).__init__(attrs)
@@ -268,6 +404,18 @@ class SelectMultiple(Widget):
if isinstance(data, MultiValueDict):
return data.getlist(name)
return data.get(name, None)
+
+ def _has_changed(self, initial, data):
+ if initial is None:
+ initial = []
+ if data is None:
+ data = []
+ if len(initial) != len(data):
+ return True
+ for value1, value2 in zip(initial, data):
+ if force_unicode(value1) != force_unicode(value2):
+ return True
+ return False
class RadioInput(StrAndUnicode):
"""
@@ -447,6 +595,16 @@ class MultiWidget(Widget):
def value_from_datadict(self, data, files, name):
return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
+
+ def _has_changed(self, initial, data):
+ if initial is None:
+ initial = [u'' for x in range(0, len(data))]
+ else:
+ initial = self.decompress(initial)
+ for widget, initial, data in zip(self.widgets, initial, data):
+ if widget._has_changed(initial, data):
+ return True
+ return False
def format_output(self, rendered_widgets):
"""
@@ -466,6 +624,14 @@ class MultiWidget(Widget):
"""
raise NotImplementedError('Subclasses must implement this method.')
+ def _get_media(self):
+ "Media for a multiwidget is the combination of all media of the subwidgets"
+ media = Media()
+ for w in self.widgets:
+ media = media + w.media
+ return media
+ media = property(_get_media)
+
class SplitDateTimeWidget(MultiWidget):
"""
A Widget that splits datetime input into two <input type="text"> boxes.