mirror of https://github.com/django/django.git
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
This commit is contained in:
parent
dc375fb0f3
commit
a19ed8aea3
10
AUTHORS
10
AUTHORS
|
@ -74,6 +74,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Arvis Bickovskis <viestards.lists@gmail.com>
|
||||
Paul Bissex <http://e-scribe.com/>
|
||||
Simon Blanchard
|
||||
David Blewett <david@dawninglight.net>
|
||||
Matt Boersma <ogghead@gmail.com>
|
||||
boobsd@gmail.com
|
||||
Andrew Brehaut <http://brehaut.net/blog>
|
||||
|
@ -172,6 +173,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Espen Grindhaug <http://grindhaug.org/>
|
||||
Thomas Güttler <hv@tbz-pariv.de>
|
||||
dAniel hAhler
|
||||
hambaloney
|
||||
Brian Harring <ferringb@gmail.com>
|
||||
Brant Harris
|
||||
Hawkeye
|
||||
|
@ -194,6 +196,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Baurzhan Ismagulov <ibr@radix50.net>
|
||||
james_027@yahoo.com
|
||||
jcrasta@gmail.com
|
||||
jdetaeye
|
||||
Zak Johnson <zakj@nox.cx>
|
||||
Nis Jørgensen <nis@superlativ.dk>
|
||||
Michael Josephson <http://www.sdjournal.com/>
|
||||
|
@ -241,11 +244,13 @@ answer newbie questions, and generally made Django that much better:
|
|||
Waylan Limberg <waylan@gmail.com>
|
||||
limodou
|
||||
Philip Lindborg <philip.lindborg@gmail.com>
|
||||
Simon Litchfield <simon@quo.com.au>
|
||||
Daniel Lindsley <polarcowz@gmail.com>
|
||||
Trey Long <trey@ktrl.com>
|
||||
msaelices <msaelices@gmail.com>
|
||||
Matt McClanahan <http://mmcc.cx/>
|
||||
Martin Maney <http://www.chipy.org/Martin_Maney>
|
||||
Petr Marhoun <petr.marhoun@gmail.com>
|
||||
masonsimon+django@gmail.com
|
||||
Manuzhai
|
||||
Petr Marhoun <petr.marhoun@gmail.com>
|
||||
|
@ -258,6 +263,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
mattycakes@gmail.com
|
||||
Jason McBrayer <http://www.carcosa.net/jason/>
|
||||
mccutchen@gmail.com
|
||||
Christian Metts
|
||||
michael.mcewan@gmail.com
|
||||
michal@plovarna.cz
|
||||
Slawek Mikula <slawek dot mikula at gmail dot com>
|
||||
|
@ -270,6 +276,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Eric Moritz <http://eric.themoritzfamily.com/>
|
||||
mrmachine <real.human@mrmachine.net>
|
||||
Robin Munn <http://www.geekforgod.com/>
|
||||
msundstr
|
||||
Robert Myers <myer0052@gmail.com>
|
||||
Nebojša Dorđević
|
||||
Doug Napoleone <doug@dougma.com>
|
||||
|
@ -290,6 +297,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
peter@mymart.com
|
||||
pgross@thoughtworks.com
|
||||
phaedo <http://phaedo.cx/>
|
||||
Julien Phalip <http://www.julienphalip.com>
|
||||
phil@produxion.net
|
||||
phil.h.smith@gmail.com
|
||||
Gustavo Picon
|
||||
|
@ -298,6 +306,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Mihai Preda <mihai_preda@yahoo.com>
|
||||
Daniel Poelzleithner <http://poelzi.org/>
|
||||
polpak@yahoo.com
|
||||
Matthias Pronk <django@masida.nl>
|
||||
Jyrki Pulliainen <jyrki.pulliainen@gmail.com>
|
||||
Johann Queuniet <johann.queuniet@adh.naellia.eu>
|
||||
Jan Rademaker
|
||||
|
@ -314,6 +323,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Matt Riggott
|
||||
Henrique Romano <onaiort@gmail.com>
|
||||
Armin Ronacher
|
||||
Daniel Roseman <http://roseman.org.uk/>
|
||||
Brian Rosner <brosner@gmail.com>
|
||||
Oliver Rutherfurd <http://rutherfurd.net/>
|
||||
ryankanno
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
VERSION = (0, 97, 'pre')
|
||||
VERSION = (0, 97, 'newforms-admin')
|
||||
|
||||
def get_version():
|
||||
"Returns the version as a human-format string."
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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;}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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())
|
|
@ -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()
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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> ›
|
||||
|
@ -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 %}
|
||||
|
|
|
@ -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> › {{ 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>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="../../../../">{% trans "Home" %}</a> ›
|
||||
|
@ -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>
|
||||
|
|
|
@ -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> {% 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>
|
|
@ -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>
|
|
@ -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 }} #{{ 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
|
@ -4,7 +4,5 @@
|
|||
{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans 'Home' %}</a> › {{ 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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs"><a href="../../../../">{% trans 'Home' %}</a> › <a href="../../">{{ module_name }}</a> › <a href="../">{{ object|truncatewords:"18" }}</a> › {% 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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -25,3 +25,4 @@
|
|||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -40,3 +40,4 @@
|
|||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
|
@ -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> › {% trans 'Password change' %}</div>{% endblock %}
|
||||
|
||||
{% block title %}{% trans 'Password change successful' %}{% endblock %}
|
||||
|
|
|
@ -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> › {% trans 'Password change' %}</div>{% endblock %}
|
||||
|
||||
{% block title %}{% trans 'Password change' %}{% endblock %}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -1 +0,0 @@
|
|||
{% load admin_modify %}{% output_all bound_field.form_fields %}
|
|
@ -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 %}
|
|
@ -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 %}&{% 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 %} <strong>{{ bound_field.existing_display|truncatewords:"14" }}</strong>{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
|
@ -1 +0,0 @@
|
|||
{% include "widget/foreign.html" %}
|
|
@ -1,2 +0,0 @@
|
|||
{% if add %}{% include "widget/foreign.html" %}{% endif %}
|
||||
{% if change %}{% if bound_field.existing_display %} <strong>{{ bound_field.existing_display|truncatewords:"14" }}</strong>{% endif %}{% endif %}
|
|
@ -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(' ')
|
||||
# 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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'
|
||||
|
|
|
@ -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('?' + '&'.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))
|
||||
|
|
|
@ -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 = '?' + '&'.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 ' <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_)
|
|
@ -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),
|
||||
)
|
|
@ -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))
|
|
@ -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)
|
||||
|
|
@ -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.')
|
||||
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
|
||||
|
||||
def save(self, new_data):
|
||||
"Creates the user."
|
||||
return User.objects.create_user(new_data['username'], '', new_data['password1'])
|
||||
|
||||
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),
|
||||
)
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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'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'})
|
|
@ -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
|
||||
|
||||
"""
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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',)
|
||||
|
||||
class Admin:
|
||||
list_display = ('old_path', 'new_path')
|
||||
list_filter = ('site',)
|
||||
search_fields = ('old_path', 'new_path')
|
||||
|
||||
|
||||
def __unicode__(self):
|
||||
return u"%s ---> %s" % (self.old_path, self.new_path)
|
||||
return "%s ---> %s" % (self.old_path, self.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)
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -15,3 +15,4 @@ from widgets import *
|
|||
from fields import *
|
||||
from forms import *
|
||||
from models import *
|
||||
from formsets import *
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 #####################################################################
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,678 @@
|
|||
=====================
|
||||
The Django admin site
|
||||
=====================
|
||||
|
||||
One of the most powerful parts of Django is the automatic admin interface. It
|
||||
reads metadata in your model to provide a powerful and production-ready
|
||||
interface that content producers can immediately use to start adding content to
|
||||
the site. In this document, we discuss how to activate, use and customize
|
||||
Django's admin interface.
|
||||
|
||||
.. admonition:: Note
|
||||
|
||||
The admin site has been refactored significantly since Django 0.96. This
|
||||
document describes the newest version of the admin site, which allows for
|
||||
much richer customization. If you follow the development of Django itself,
|
||||
you may have heard this described as "newforms-admin."
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
There are four steps in activating the Django admin site:
|
||||
|
||||
1. Determine which of your application's models should be editable in the
|
||||
admin interface.
|
||||
|
||||
2. For each of those models, optionally create a ``ModelAdmin`` class that
|
||||
encapsulates the customized admin functionality and options for that
|
||||
particular model.
|
||||
|
||||
3. Instantiate an ``AdminSite`` and tell it about each of your models and
|
||||
``ModelAdmin`` classes.
|
||||
|
||||
4. Hook the ``AdminSite`` instance into your URLconf.
|
||||
|
||||
``ModelAdmin`` objects
|
||||
======================
|
||||
|
||||
The ``ModelAdmin`` class is the representation of a model in the admin
|
||||
interface. These are stored in a file named ``admin.py`` in your application.
|
||||
Let's take a look at a very simple example the ``ModelAdmin``::
|
||||
|
||||
from django.contrib import admin
|
||||
from myproject.myapp.models import Author
|
||||
|
||||
class AuthorAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
admin.site.register(Author, AuthorAdmin)
|
||||
|
||||
``ModelAdmin`` Options
|
||||
----------------------
|
||||
|
||||
The ``ModelAdmin`` is very flexible. It has several options for dealing with
|
||||
customizing the interface. All options are defined on the ``ModelAdmin``
|
||||
subclass::
|
||||
|
||||
class AuthorAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = 'pub_date'
|
||||
|
||||
``date_hierarchy``
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set ``date_hierarchy`` to the name of a ``DateField`` or ``DateTimeField`` in
|
||||
your model, and the change list page will include a date-based drilldown
|
||||
navigation by that field.
|
||||
|
||||
Example::
|
||||
|
||||
date_hierarchy = 'pub_date'
|
||||
|
||||
``fieldsets``
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Set ``fieldsets`` to control the layout of admin "add" and "change" pages.
|
||||
|
||||
``fieldsets`` is a list of two-tuples, in which each two-tuple represents a
|
||||
``<fieldset>`` on the admin form page. (A ``<fieldset>`` is a "section" of the
|
||||
form.)
|
||||
|
||||
The two-tuples are in the format ``(name, field_options)``, where ``name`` is a
|
||||
string representing the title of the fieldset and ``field_options`` is a
|
||||
dictionary of information about the fieldset, including a list of fields to be
|
||||
displayed in it.
|
||||
|
||||
A full example, taken from the ``django.contrib.flatpages.FlatPage`` model::
|
||||
|
||||
class FlatPageAdmin(admin.ModelAdmin):
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('url', 'title', 'content', 'sites')
|
||||
}),
|
||||
('Advanced options', {
|
||||
'classes': ('collapse',),
|
||||
'fields': ('enable_comments', 'registration_required', 'template_name')
|
||||
}),
|
||||
)
|
||||
|
||||
This results in an admin page that looks like:
|
||||
|
||||
.. image:: http://media.djangoproject.com/img/doc/flatfiles_admin.png
|
||||
|
||||
If ``fieldsets`` isn't given, Django will default to displaying each field
|
||||
that isn't an ``AutoField`` and has ``editable=True``, in a single fieldset,
|
||||
in the same order as the fields are defined in the model.
|
||||
|
||||
The ``field_options`` dictionary can have the following keys:
|
||||
|
||||
``fields``
|
||||
A tuple of field names to display in this fieldset. This key is required.
|
||||
|
||||
Example::
|
||||
|
||||
{
|
||||
'fields': ('first_name', 'last_name', 'address', 'city', 'state'),
|
||||
}
|
||||
|
||||
To display multiple fields on the same line, wrap those fields in their own
|
||||
tuple. In this example, the ``first_name`` and ``last_name`` fields will
|
||||
display on the same line::
|
||||
|
||||
{
|
||||
'fields': (('first_name', 'last_name'), 'address', 'city', 'state'),
|
||||
}
|
||||
|
||||
``classes``
|
||||
A string containing extra CSS classes to apply to the fieldset.
|
||||
|
||||
Example::
|
||||
|
||||
{
|
||||
'classes': 'wide',
|
||||
}
|
||||
|
||||
Apply multiple classes by separating them with spaces. Example::
|
||||
|
||||
{
|
||||
'classes': 'wide extrapretty',
|
||||
}
|
||||
|
||||
Two useful classes defined by the default admin-site stylesheet are
|
||||
``collapse`` and ``wide``. Fieldsets with the ``collapse`` style will be
|
||||
initially collapsed in the admin and replaced with a small "click to expand"
|
||||
link. Fieldsets with the ``wide`` style will be given extra horizontal space.
|
||||
|
||||
``description``
|
||||
A string of optional extra text to be displayed at the top of each fieldset,
|
||||
under the heading of the fieldset. It's used verbatim, so you can use any HTML
|
||||
and you must escape any special HTML characters (such as ampersands) yourself.
|
||||
|
||||
``filter_horizontal``
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Use a nifty unobtrusive Javascript "filter" interface instead of the
|
||||
usability-challenged ``<select multiple>`` in the admin form. The value is a
|
||||
list of fields that should be displayed as a horizontal filter interface. See
|
||||
``filter_vertical`` to use a vertical interface.
|
||||
|
||||
``filter_vertical``
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Same as ``filter_horizontal``, but is a vertical display of the filter
|
||||
interface.
|
||||
|
||||
``list_display``
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Set ``list_display`` to control which fields are displayed on the change list
|
||||
page of the admin.
|
||||
|
||||
Example::
|
||||
|
||||
list_display = ('first_name', 'last_name')
|
||||
|
||||
If you don't set ``list_display``, the admin site will display a single column
|
||||
that displays the ``__unicode__()`` representation of each object.
|
||||
|
||||
A few special cases to note about ``list_display``:
|
||||
|
||||
* If the field is a ``ForeignKey``, Django will display the
|
||||
``__unicode__()`` of the related object.
|
||||
|
||||
* ``ManyToManyField`` fields aren't supported, because that would entail
|
||||
executing a separate SQL statement for each row in the table. If you
|
||||
want to do this nonetheless, give your model a custom method, and add
|
||||
that method's name to ``list_display``. (See below for more on custom
|
||||
methods in ``list_display``.)
|
||||
|
||||
* If the field is a ``BooleanField`` or ``NullBooleanField``, Django will
|
||||
display a pretty "on" or "off" icon instead of ``True`` or ``False``.
|
||||
|
||||
* If the string given is a method of the model, Django will call it and
|
||||
display the output. This method should have a ``short_description``
|
||||
function attribute, for use as the header for the field.
|
||||
|
||||
Here's a full example model::
|
||||
|
||||
class Person(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
birthday = models.DateField()
|
||||
|
||||
def decade_born_in(self):
|
||||
return self.birthday.strftime('%Y')[:3] + "0's"
|
||||
decade_born_in.short_description = 'Birth decade'
|
||||
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'decade_born_in')
|
||||
|
||||
* If the string given is a method of the model, Django will HTML-escape the
|
||||
output by default. If you'd rather not escape the output of the method,
|
||||
give the method an ``allow_tags`` attribute whose value is ``True``.
|
||||
|
||||
Here's a full example model::
|
||||
|
||||
class Person(models.Model):
|
||||
first_name = models.CharField(max_length=50)
|
||||
last_name = models.CharField(max_length=50)
|
||||
color_code = models.CharField(max_length=6)
|
||||
|
||||
def colored_name(self):
|
||||
return '<span style="color: #%s;">%s %s</span>' % (self.color_code, self.first_name, self.last_name)
|
||||
colored_name.allow_tags = True
|
||||
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
list_display = ('first_name', 'last_name', 'colored_name')
|
||||
|
||||
* If the string given is a method of the model that returns True or False
|
||||
Django will display a pretty "on" or "off" icon if you give the method a
|
||||
``boolean`` attribute whose value is ``True``.
|
||||
|
||||
Here's a full example model::
|
||||
|
||||
class Person(models.Model):
|
||||
first_name = models.CharField(max_length=50)
|
||||
birthday = models.DateField()
|
||||
|
||||
def born_in_fifties(self):
|
||||
return self.birthday.strftime('%Y')[:3] == 5
|
||||
born_in_fifties.boolean = True
|
||||
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'born_in_fifties')
|
||||
|
||||
|
||||
* The ``__str__()`` and ``__unicode__()`` methods are just as valid in
|
||||
``list_display`` as any other model method, so it's perfectly OK to do
|
||||
this::
|
||||
|
||||
list_display = ('__unicode__', 'some_other_field')
|
||||
|
||||
* Usually, elements of ``list_display`` that aren't actual database fields
|
||||
can't be used in sorting (because Django does all the sorting at the
|
||||
database level).
|
||||
|
||||
However, if an element of ``list_display`` represents a certain database
|
||||
field, you can indicate this fact by setting the ``admin_order_field``
|
||||
attribute of the item.
|
||||
|
||||
For example::
|
||||
|
||||
class Person(models.Model):
|
||||
first_name = models.CharField(max_length=50)
|
||||
color_code = models.CharField(max_length=6)
|
||||
|
||||
def colored_first_name(self):
|
||||
return '<span style="color: #%s;">%s</span>' % (self.color_code, self.first_name)
|
||||
colored_first_name.allow_tags = True
|
||||
colored_first_name.admin_order_field = 'first_name'
|
||||
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
list_display = ('first_name', 'colored_first_name')
|
||||
|
||||
The above will tell Django to order by the ``first_name`` field when
|
||||
trying to sort by ``colored_first_name`` in the admin.
|
||||
|
||||
``list_display_links``
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set ``list_display_links`` to control which fields in ``list_display`` should
|
||||
be linked to the "change" page for an object.
|
||||
|
||||
By default, the change list page will link the first column -- the first field
|
||||
specified in ``list_display`` -- to the change page for each item. But
|
||||
``list_display_links`` lets you change which columns are linked. Set
|
||||
``list_display_links`` to a list or tuple of field names (in the same format as
|
||||
``list_display``) to link.
|
||||
|
||||
``list_display_links`` can specify one or many field names. As long as the
|
||||
field names appear in ``list_display``, Django doesn't care how many (or how
|
||||
few) fields are linked. The only requirement is: If you want to use
|
||||
``list_display_links``, you must define ``list_display``.
|
||||
|
||||
In this example, the ``first_name`` and ``last_name`` fields will be linked on
|
||||
the change list page::
|
||||
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
list_display = ('first_name', 'last_name', 'birthday')
|
||||
list_display_links = ('first_name', 'last_name')
|
||||
|
||||
Finally, note that in order to use ``list_display_links``, you must define
|
||||
``list_display``, too.
|
||||
|
||||
``list_filter``
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Set ``list_filter`` to activate filters in the right sidebar of the change list
|
||||
page of the admin. This should be a list of field names, and each specified
|
||||
field should be either a ``BooleanField``, ``CharField``, ``DateField``,
|
||||
``DateTimeField``, ``IntegerField`` or ``ForeignKey``.
|
||||
|
||||
This example, taken from the ``django.contrib.auth.models.User`` model, shows
|
||||
how both ``list_display`` and ``list_filter`` work::
|
||||
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
|
||||
list_filter = ('is_staff', 'is_superuser')
|
||||
|
||||
The above code results in an admin change list page that looks like this:
|
||||
|
||||
.. image:: http://media.djangoproject.com/img/doc/users_changelist.png
|
||||
|
||||
(This example also has ``search_fields`` defined. See below.)
|
||||
|
||||
``list_per_page``
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set ``list_per_page`` to control how many items appear on each paginated admin
|
||||
change list page. By default, this is set to ``100``.
|
||||
|
||||
``list_select_related``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set ``list_select_related`` to tell Django to use ``select_related()`` in
|
||||
retrieving the list of objects on the admin change list page. This can save you
|
||||
a bunch of database queries.
|
||||
|
||||
The value should be either ``True`` or ``False``. Default is ``False``.
|
||||
|
||||
Note that Django will use ``select_related()``, regardless of this setting,
|
||||
if one of the ``list_display`` fields is a ``ForeignKey``.
|
||||
|
||||
For more on ``select_related()``, see `the select_related() docs`_.
|
||||
|
||||
.. _the select_related() docs: ../db-api/#select-related
|
||||
|
||||
``inlines``
|
||||
~~~~~~~~~~~
|
||||
|
||||
See ``InlineModelAdmin`` objects below.
|
||||
|
||||
``ordering``
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Set ``ordering`` to specify how objects on the admin change list page should be
|
||||
ordered. This should be a list or tuple in the same format as a model's
|
||||
``ordering`` parameter.
|
||||
|
||||
If this isn't provided, the Django admin will use the model's default ordering.
|
||||
|
||||
``prepopulated_fields``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set ``prepopulated_fields`` to a dictionary mapping field names to the fields
|
||||
it should prepopulate from::
|
||||
|
||||
class ArticleAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {"slug": ("title",)}
|
||||
|
||||
When set the given fields will use a bit of Javascript to populate from the
|
||||
fields assigned.
|
||||
|
||||
``prepopulated_fields`` doesn't accept DateTimeFields, ForeignKeys nor
|
||||
ManyToManyFields.
|
||||
|
||||
``radio_fields``
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
By default, Django's admin uses a select-box interface (<select>) for
|
||||
fields that are ``ForeignKey`` or have ``choices`` set. If a field is present
|
||||
in ``radio_fields``, Django will use a radio-button interface instead.
|
||||
Assuming ``group`` is a ``ForeignKey`` on the ``Person`` model::
|
||||
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
radio_fields = {"group": admin.VERTICAL}
|
||||
|
||||
You have the choice of using ``HORIZONTAL`` or ``VERTICAL`` from the
|
||||
``django.contrib.admin`` module.
|
||||
|
||||
Don't include a field in ``radio_fields`` unless it's a ``ForeignKey`` or has
|
||||
``choices`` set.
|
||||
|
||||
``raw_id_fields``
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default, Django's admin uses a select-box interface (<select>) for
|
||||
fields that are ``ForeignKey``. Sometimes you don't want to incur the
|
||||
overhead of having to select all the related instances to display in the
|
||||
drop-down.
|
||||
|
||||
``raw_id_fields`` is a list of fields you would like to change
|
||||
into a ``Input`` widget for the primary key.
|
||||
|
||||
``save_as``
|
||||
~~~~~~~~~~~
|
||||
|
||||
Set ``save_as`` to enable a "save as" feature on admin change forms.
|
||||
|
||||
Normally, objects have three save options: "Save", "Save and continue editing"
|
||||
and "Save and add another". If ``save_as`` is ``True``, "Save and add another"
|
||||
will be replaced by a "Save as" button.
|
||||
|
||||
"Save as" means the object will be saved as a new object (with a new ID),
|
||||
rather than the old object.
|
||||
|
||||
By default, ``save_as`` is set to ``False``.
|
||||
|
||||
``save_on_top``
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Set ``save_on_top`` to add save buttons across the top of your admin change
|
||||
forms.
|
||||
|
||||
Normally, the save buttons appear only at the bottom of the forms. If you set
|
||||
``save_on_top``, the buttons will appear both on the top and the bottom.
|
||||
|
||||
By default, ``save_on_top`` is set to ``False``.
|
||||
|
||||
``search_fields``
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set ``search_fields`` to enable a search box on the admin change list page.
|
||||
This should be set to a list of field names that will be searched whenever
|
||||
somebody submits a search query in that text box.
|
||||
|
||||
These fields should be some kind of text field, such as ``CharField`` or
|
||||
``TextField``. You can also perform a related lookup on a ``ForeignKey`` with
|
||||
the lookup API "follow" notation::
|
||||
|
||||
search_fields = ['foreign_key__related_fieldname']
|
||||
|
||||
When somebody does a search in the admin search box, Django splits the search
|
||||
query into words and returns all objects that contain each of the words, case
|
||||
insensitive, where each word must be in at least one of ``search_fields``. For
|
||||
example, if ``search_fields`` is set to ``['first_name', 'last_name']`` and a
|
||||
user searches for ``john lennon``, Django will do the equivalent of this SQL
|
||||
``WHERE`` clause::
|
||||
|
||||
WHERE (first_name ILIKE '%john%' OR last_name ILIKE '%john%')
|
||||
AND (first_name ILIKE '%lennon%' OR last_name ILIKE '%lennon%')
|
||||
|
||||
For faster and/or more restrictive searches, prefix the field name
|
||||
with an operator:
|
||||
|
||||
``^``
|
||||
Matches the beginning of the field. For example, if ``search_fields`` is
|
||||
set to ``['^first_name', '^last_name']`` and a user searches for
|
||||
``john lennon``, Django will do the equivalent of this SQL ``WHERE``
|
||||
clause::
|
||||
|
||||
WHERE (first_name ILIKE 'john%' OR last_name ILIKE 'john%')
|
||||
AND (first_name ILIKE 'lennon%' OR last_name ILIKE 'lennon%')
|
||||
|
||||
This query is more efficient than the normal ``'%john%'`` query, because
|
||||
the database only needs to check the beginning of a column's data, rather
|
||||
than seeking through the entire column's data. Plus, if the column has an
|
||||
index on it, some databases may be able to use the index for this query,
|
||||
even though it's a ``LIKE`` query.
|
||||
|
||||
``=``
|
||||
Matches exactly, case-insensitive. For example, if
|
||||
``search_fields`` is set to ``['=first_name', '=last_name']`` and
|
||||
a user searches for ``john lennon``, Django will do the equivalent
|
||||
of this SQL ``WHERE`` clause::
|
||||
|
||||
WHERE (first_name ILIKE 'john' OR last_name ILIKE 'john')
|
||||
AND (first_name ILIKE 'lennon' OR last_name ILIKE 'lennon')
|
||||
|
||||
Note that the query input is split by spaces, so, following this example,
|
||||
it's currently not possible to search for all records in which
|
||||
``first_name`` is exactly ``'john winston'`` (containing a space).
|
||||
|
||||
``@``
|
||||
Performs a full-text match. This is like the default search method but uses
|
||||
an index. Currently this is only available for MySQL.
|
||||
|
||||
``ModelAdmin`` media definitions
|
||||
--------------------------------
|
||||
|
||||
There are times where you would like add a bit of CSS and/or Javascript to
|
||||
the add/change views. This can be accomplished by using a Media inner class
|
||||
on your ``ModelAdmin``::
|
||||
|
||||
class ArticleAdmin(admin.ModelAdmin):
|
||||
class Media:
|
||||
css = {
|
||||
"all": ("my_styles.css",)
|
||||
}
|
||||
js = ("my_code.js",)
|
||||
|
||||
Keep in mind that this will be prepended with ``MEDIA_URL``. The same rules
|
||||
apply as `regular media definitions on forms`_.
|
||||
|
||||
.. _regular media definitions on forms: ../newforms/#media
|
||||
|
||||
``InlineModelAdmin`` objects
|
||||
============================
|
||||
|
||||
The admin interface has the ability to edit models on the same page as a
|
||||
parent model. These are called inlines. You can add them a model being
|
||||
specifing them in a ``ModelAdmin.inlines`` attribute::
|
||||
|
||||
class BookInline(admin.TabularInline):
|
||||
model = Book
|
||||
|
||||
class AuthorAdmin(admin.ModelAdmin):
|
||||
inlines = [
|
||||
BookInline,
|
||||
]
|
||||
|
||||
Django provides two subclasses of ``InlineModelAdmin`` and they are::
|
||||
|
||||
* ``TabularInline``
|
||||
* ``StackedInline``
|
||||
|
||||
The difference between these two is merely the template used to render them.
|
||||
|
||||
``InlineModelAdmin`` options
|
||||
-----------------------------
|
||||
|
||||
The ``InlineModelAdmin`` class is a subclass of ``ModelAdmin`` so it inherits
|
||||
all the same functionality as well as some of its own:
|
||||
|
||||
``model``
|
||||
~~~~~~~~~
|
||||
|
||||
The model in which the inline is using. This is required.
|
||||
|
||||
``fk_name``
|
||||
~~~~~~~~~~~
|
||||
|
||||
The name of the foreign key on the model. In most cases this will be dealt
|
||||
with automatically, but ``fk_name`` must be specified explicitly if there are
|
||||
more than one foreign key to the same parent model.
|
||||
|
||||
``formset``
|
||||
~~~~~~~~~~~
|
||||
|
||||
This defaults to ``BaseInlineFormset``. Using your own formset can give you
|
||||
many possibilities of customization. Inlines are built around
|
||||
`model formsets`_.
|
||||
|
||||
.. _model formsets: ../modelforms/#model-formsets
|
||||
|
||||
``form``
|
||||
~~~~~~~~
|
||||
|
||||
The value for ``form`` is inherited from ``ModelAdmin``. This is what is
|
||||
passed through to ``formset_factory`` when creating the formset for this
|
||||
inline.
|
||||
|
||||
``extra``
|
||||
~~~~~~~~~
|
||||
|
||||
This controls the number of extra forms the formset will display in addition
|
||||
to the initial forms. See the `formsets documentation`_ for more information.
|
||||
|
||||
.. _formsets documentation: ../newforms/#formsets
|
||||
|
||||
``max_num``
|
||||
~~~~~~~~~~~
|
||||
|
||||
This controls the maximum number of forms to show in the inline. This doesn't
|
||||
directly corrolate to the number of objects, but can if the value is small
|
||||
enough. See `max_num in formsets`_ for more information.
|
||||
|
||||
.. _max_num in formsets: ../modelforms/#limiting-the-number-of-objects-editable
|
||||
|
||||
``template``
|
||||
~~~~~~~~~~~~
|
||||
|
||||
The template used to render the inline on the page.
|
||||
|
||||
``verbose_name``
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
An override to the ``verbose_name`` found in the model's inner ``Meta`` class.
|
||||
|
||||
``verbose_name_plural``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An override to the ``verbose_name_plural`` found in the model's inner ``Meta``
|
||||
class.
|
||||
|
||||
Working with a model with two or more foreign keys to the same parent model
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
It is sometimes possible to have more than one foreign key to the same model.
|
||||
Take this model for instance::
|
||||
|
||||
class Friendship(models.Model):
|
||||
to_person = models.ForeignKey(Person, related_name="friends")
|
||||
from_person = models.ForeignKey(Person, related_name="from_friends")
|
||||
|
||||
If you wanted to display an inline on the ``Person`` admin add/change pages
|
||||
you need to explicitly define the foreign key since it is unable to do so
|
||||
automatically::
|
||||
|
||||
class FriendshipInline(admin.TabularInline):
|
||||
model = Friendship
|
||||
fk_name = "to_person"
|
||||
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
inlines = [
|
||||
FriendshipInline,
|
||||
]
|
||||
|
||||
``AdminSite`` objects
|
||||
=====================
|
||||
|
||||
Hooking ``AdminSite`` instances into your URLconf
|
||||
-------------------------------------------------
|
||||
|
||||
The last step in setting up the Django admin is to hook your ``AdminSite``
|
||||
instance into your URLconf. Do this by pointing a given URL at the
|
||||
``AdminSite.root`` method.
|
||||
|
||||
In this example, we register the default ``AdminSite`` instance
|
||||
``django.contrib.admin.site`` at the URL ``/admin/`` ::
|
||||
|
||||
# urls.py
|
||||
from django.conf.urls.defaults import *
|
||||
from django.contrib import admin
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns('',
|
||||
('^admin/(.*)', admin.site.root),
|
||||
)
|
||||
|
||||
Above we used ``admin.autodiscover()`` to automatically load the
|
||||
``INSTALLED_APPS`` admin.py modules.
|
||||
|
||||
In this example, we register the ``AdminSite`` instance
|
||||
``myproject.admin.admin_site`` at the URL ``/myadmin/`` ::
|
||||
|
||||
# urls.py
|
||||
from django.conf.urls.defaults import *
|
||||
from myproject.admin import admin_site
|
||||
|
||||
urlpatterns = patterns('',
|
||||
('^myadmin/(.*)', admin_site.root),
|
||||
)
|
||||
|
||||
There is really no need to use autodiscover when using your own ``AdminSite``
|
||||
instance since you will likely be importing all the per-app admin.py modules
|
||||
in your ``myproject.admin`` module.
|
||||
|
||||
Note that the regular expression in the URLpattern *must* group everything in
|
||||
the URL that comes after the URL root -- hence the ``(.*)`` in these examples.
|
||||
|
||||
Multiple admin sites in the same URLconf
|
||||
----------------------------------------
|
||||
|
||||
It's easy to create multiple instances of the admin site on the same
|
||||
Django-powered Web site. Just create multiple instances of ``AdminSite`` and
|
||||
root each one at a different URL.
|
||||
|
||||
In this example, the URLs ``/basic-admin/`` and ``/advanced-admin/`` feature
|
||||
separate versions of the admin site -- using the ``AdminSite`` instances
|
||||
``myproject.admin.basic_site`` and ``myproject.admin.advanced_site``,
|
||||
respectively::
|
||||
|
||||
# urls.py
|
||||
from django.conf.urls.defaults import *
|
||||
from myproject.admin import basic_site, advanced_site
|
||||
|
||||
urlpatterns = patterns('',
|
||||
('^basic-admin/(.*)', basic_site.root),
|
||||
('^advanced-admin/(.*)', advanced_site.root),
|
||||
)
|
|
@ -516,8 +516,8 @@ It's your responsibility to provide the login form in a template called
|
|||
``registration/login.html`` by default. This template gets passed three
|
||||
template context variables:
|
||||
|
||||
* ``form``: A ``FormWrapper`` object representing the login form. See the
|
||||
`forms documentation`_ for more on ``FormWrapper`` objects.
|
||||
* ``form``: A ``Form`` object representing the login form. See the
|
||||
`newforms documentation`_ for more on ``Form`` objects.
|
||||
* ``next``: The URL to redirect to after successful login. This may contain
|
||||
a query string, too.
|
||||
* ``site_name``: The name of the current ``Site``, according to the
|
||||
|
@ -541,14 +541,14 @@ block::
|
|||
|
||||
{% block content %}
|
||||
|
||||
{% if form.has_errors %}
|
||||
{% if form.errors %}
|
||||
<p>Your username and password didn't match. Please try again.</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action=".">
|
||||
<table>
|
||||
<tr><td><label for="id_username">Username:</label></td><td>{{ form.username }}</td></tr>
|
||||
<tr><td><label for="id_password">Password:</label></td><td>{{ form.password }}</td></tr>
|
||||
<tr><td>{{ form.username.label_tag }}</td><td>{{ form.username }}</td></tr>
|
||||
<tr><td>{{ form.password.label_tag }}</td><td>{{ form.password }}</td></tr>
|
||||
</table>
|
||||
|
||||
<input type="submit" value="login" />
|
||||
|
@ -557,7 +557,7 @@ block::
|
|||
|
||||
{% endblock %}
|
||||
|
||||
.. _forms documentation: ../forms/
|
||||
.. _newforms documentation: ../newforms/
|
||||
.. _site framework docs: ../sites/
|
||||
|
||||
Other built-in views
|
||||
|
@ -677,29 +677,29 @@ successful login.
|
|||
* ``login_url``: The URL of the login page to redirect to. This
|
||||
will default to ``settings.LOGIN_URL`` if not supplied.
|
||||
|
||||
Built-in manipulators
|
||||
---------------------
|
||||
Built-in forms
|
||||
--------------
|
||||
|
||||
**New in Django development version.**
|
||||
|
||||
If you don't want to use the built-in views, but want the convenience
|
||||
of not having to write manipulators for this functionality, the
|
||||
authentication system provides several built-in manipulators:
|
||||
of not having to write forms for this functionality, the authentication
|
||||
system provides several built-in forms:
|
||||
|
||||
* ``django.contrib.auth.forms.AdminPasswordChangeForm``: A
|
||||
manipulator used in the admin interface to change a user's
|
||||
password.
|
||||
* ``django.contrib.auth.forms.AdminPasswordChangeForm``: A form used in
|
||||
the admin interface to change a user's password.
|
||||
|
||||
* ``django.contrib.auth.forms.AuthenticationForm``: A manipulator
|
||||
for logging a user in.
|
||||
* ``django.contrib.auth.forms.AuthenticationForm``: A form for logging a
|
||||
user in.
|
||||
|
||||
* ``django.contrib.auth.forms.PasswordChangeForm``: A manipulator
|
||||
for allowing a user to change their password.
|
||||
* ``django.contrib.auth.forms.PasswordChangeForm``: A form for allowing a
|
||||
user to change their password.
|
||||
|
||||
* ``django.contrib.auth.forms.PasswordResetForm``: A manipulator
|
||||
for resetting a user's password and emailing the new password to
|
||||
them.
|
||||
* ``django.contrib.auth.forms.PasswordResetForm``: A form for resetting a
|
||||
user's password and emailing the new password to them.
|
||||
|
||||
* ``django.contrib.auth.forms.UserCreationForm``: A manipulator
|
||||
for creating a new user.
|
||||
* ``django.contrib.auth.forms.UserCreationForm``: A form for creating a
|
||||
new user.
|
||||
|
||||
Limiting access to logged-in users that pass a test
|
||||
---------------------------------------------------
|
||||
|
|
|
@ -204,7 +204,6 @@ order:
|
|||
* ``unique_for_year``
|
||||
* ``validator_list``
|
||||
* ``choices``
|
||||
* ``radio_admin``
|
||||
* ``help_text``
|
||||
* ``db_column``
|
||||
* ``db_tablespace``: Currently only used with the Oracle backend and only
|
||||
|
|
|
@ -20,10 +20,10 @@ For example, here's how you can create a form with a field representing a
|
|||
French telephone number::
|
||||
|
||||
from django import newforms as forms
|
||||
from django.contrib.localflavor.fr.forms import FRPhoneNumberField
|
||||
from django.contrib.localflavor import fr
|
||||
|
||||
class MyForm(forms.Form):
|
||||
my_french_phone_no = FRPhoneNumberField()
|
||||
my_french_phone_no = fr.forms.FRPhoneNumberField()
|
||||
|
||||
Supported countries
|
||||
===================
|
||||
|
|
|
@ -12,8 +12,6 @@ The basics:
|
|||
* Each attribute of the model represents a database field.
|
||||
* Model metadata (non-field information) goes in an inner class named
|
||||
``Meta``.
|
||||
* Metadata used for Django's admin site goes into an inner class named
|
||||
``Admin``.
|
||||
* With all of this, Django gives you an automatically-generated
|
||||
database-access API, which is explained in the `Database API reference`_.
|
||||
|
||||
|
@ -425,18 +423,6 @@ not specified, Django will use a default length of 50.
|
|||
|
||||
Implies ``db_index=True``.
|
||||
|
||||
Accepts an extra option, ``prepopulate_from``, which is a list of fields
|
||||
from which to auto-populate the slug, via JavaScript, in the object's admin
|
||||
form::
|
||||
|
||||
models.SlugField(prepopulate_from=("pre_name", "name"))
|
||||
|
||||
``prepopulate_from`` doesn't accept DateTimeFields, ForeignKeys nor
|
||||
ManyToManyFields.
|
||||
|
||||
The admin represents ``SlugField`` as an ``<input type="text">`` (a
|
||||
single-line input).
|
||||
|
||||
``SmallIntegerField``
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -665,16 +651,6 @@ unless you want to override the default primary-key behavior.
|
|||
``primary_key=True`` implies ``null=False`` and ``unique=True``. Only
|
||||
one primary key is allowed on an object.
|
||||
|
||||
``radio_admin``
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
By default, Django's admin uses a select-box interface (<select>) for
|
||||
fields that are ``ForeignKey`` or have ``choices`` set. If ``radio_admin``
|
||||
is set to ``True``, Django will use a radio-button interface instead.
|
||||
|
||||
Don't use this for a field unless it's a ``ForeignKey`` or has ``choices``
|
||||
set.
|
||||
|
||||
``unique``
|
||||
~~~~~~~~~~
|
||||
|
||||
|
@ -822,14 +798,6 @@ relationship should work. All are optional:
|
|||
======================= ============================================================
|
||||
Argument Description
|
||||
======================= ============================================================
|
||||
``edit_inline`` If ``True``, this related object is edited
|
||||
"inline" on the related object's page. This means
|
||||
that the object will not have its own admin
|
||||
interface. Use either ``models.TABULAR`` or ``models.STACKED``,
|
||||
which, respectively, designate whether the inline-editable
|
||||
objects are displayed as a table or as a "stack" of
|
||||
fieldsets.
|
||||
|
||||
``limit_choices_to`` A dictionary of lookup arguments and values (see
|
||||
the `Database API reference`_) that limit the
|
||||
available admin choices for this object. Use this
|
||||
|
@ -848,39 +816,6 @@ relationship should work. All are optional:
|
|||
|
||||
Not compatible with ``edit_inline``.
|
||||
|
||||
``max_num_in_admin`` For inline-edited objects, this is the maximum
|
||||
number of related objects to display in the admin.
|
||||
Thus, if a pizza could only have up to 10
|
||||
toppings, ``max_num_in_admin=10`` would ensure
|
||||
that a user never enters more than 10 toppings.
|
||||
|
||||
Note that this doesn't ensure more than 10 related
|
||||
toppings ever get created. It simply controls the
|
||||
admin interface; it doesn't enforce things at the
|
||||
Python API level or database level.
|
||||
|
||||
``min_num_in_admin`` The minimum number of related objects displayed in
|
||||
the admin. Normally, at the creation stage,
|
||||
``num_in_admin`` inline objects are shown, and at
|
||||
the edit stage ``num_extra_on_change`` blank
|
||||
objects are shown in addition to all pre-existing
|
||||
related objects. However, no fewer than
|
||||
``min_num_in_admin`` related objects will ever be
|
||||
displayed.
|
||||
|
||||
``num_extra_on_change`` The number of extra blank related-object fields to
|
||||
show at the change stage.
|
||||
|
||||
``num_in_admin`` The default number of inline objects to display
|
||||
on the object page at the add stage.
|
||||
|
||||
``raw_id_admin`` Only display a field for the integer to be entered
|
||||
instead of a drop-down menu. This is useful when
|
||||
related to an object type that will have too many
|
||||
rows to make a select box practical.
|
||||
|
||||
Not used with ``edit_inline``.
|
||||
|
||||
``related_name`` The name to use for the relation from the related
|
||||
object back to this one. See the
|
||||
`related objects documentation`_ for a full
|
||||
|
@ -957,13 +892,6 @@ the relationship should work. All are optional:
|
|||
======================= ============================================================
|
||||
``related_name`` See the description under ``ForeignKey`` above.
|
||||
|
||||
``filter_interface`` Use a nifty unobtrusive Javascript "filter" interface
|
||||
instead of the usability-challenged ``<select multiple>``
|
||||
in the admin form for this object. The value should be
|
||||
``models.HORIZONTAL`` or ``models.VERTICAL`` (i.e.
|
||||
should the interface be stacked horizontally or
|
||||
vertically).
|
||||
|
||||
``limit_choices_to`` See the description under ``ForeignKey`` above.
|
||||
|
||||
``symmetrical`` Only used in the definition of ManyToManyFields on self.
|
||||
|
@ -1255,412 +1183,6 @@ attribute is the primary key field for the model. You can read and set this
|
|||
value, just as you would for any other attribute, and it will update the
|
||||
correct field in the model.
|
||||
|
||||
Admin options
|
||||
=============
|
||||
|
||||
If you want your model to be visible to Django's admin site, give your model an
|
||||
inner ``"class Admin"``, like so::
|
||||
|
||||
class Person(models.Model):
|
||||
first_name = models.CharField(max_length=30)
|
||||
last_name = models.CharField(max_length=30)
|
||||
|
||||
class Admin:
|
||||
# Admin options go here
|
||||
pass
|
||||
|
||||
The ``Admin`` class tells Django how to display the model in the admin site.
|
||||
|
||||
Here's a list of all possible ``Admin`` options. None of these options are
|
||||
required. To use an admin interface without specifying any options, use
|
||||
``pass``, like so::
|
||||
|
||||
class Admin:
|
||||
pass
|
||||
|
||||
Adding ``class Admin`` to a model is completely optional.
|
||||
|
||||
``date_hierarchy``
|
||||
------------------
|
||||
|
||||
Set ``date_hierarchy`` to the name of a ``DateField`` or ``DateTimeField`` in
|
||||
your model, and the change list page will include a date-based drilldown
|
||||
navigation by that field.
|
||||
|
||||
Example::
|
||||
|
||||
date_hierarchy = 'pub_date'
|
||||
|
||||
``fields``
|
||||
----------
|
||||
|
||||
Set ``fields`` to control the layout of admin "add" and "change" pages.
|
||||
|
||||
``fields`` is a list of two-tuples, in which each two-tuple represents a
|
||||
``<fieldset>`` on the admin form page. (A ``<fieldset>`` is a "section" of the
|
||||
form.)
|
||||
|
||||
The two-tuples are in the format ``(name, field_options)``, where ``name`` is a
|
||||
string representing the title of the fieldset and ``field_options`` is a
|
||||
dictionary of information about the fieldset, including a list of fields to be
|
||||
displayed in it.
|
||||
|
||||
A full example, taken from the ``django.contrib.flatpages.FlatPage`` model::
|
||||
|
||||
class Admin:
|
||||
fields = (
|
||||
(None, {
|
||||
'fields': ('url', 'title', 'content', 'sites')
|
||||
}),
|
||||
('Advanced options', {
|
||||
'classes': 'collapse',
|
||||
'fields' : ('enable_comments', 'registration_required', 'template_name')
|
||||
}),
|
||||
)
|
||||
|
||||
This results in an admin page that looks like:
|
||||
|
||||
.. image:: http://media.djangoproject.com/img/doc/flatfiles_admin.png
|
||||
|
||||
If ``fields`` isn't given, Django will default to displaying each field that
|
||||
isn't an ``AutoField`` and has ``editable=True``, in a single fieldset, in
|
||||
the same order as the fields are defined in the model.
|
||||
|
||||
The ``field_options`` dictionary can have the following keys:
|
||||
|
||||
``fields``
|
||||
~~~~~~~~~~
|
||||
|
||||
A tuple of field names to display in this fieldset. This key is required.
|
||||
|
||||
Example::
|
||||
|
||||
{
|
||||
'fields': ('first_name', 'last_name', 'address', 'city', 'state'),
|
||||
}
|
||||
|
||||
To display multiple fields on the same line, wrap those fields in their own
|
||||
tuple. In this example, the ``first_name`` and ``last_name`` fields will
|
||||
display on the same line::
|
||||
|
||||
{
|
||||
'fields': (('first_name', 'last_name'), 'address', 'city', 'state'),
|
||||
}
|
||||
|
||||
``classes``
|
||||
~~~~~~~~~~~
|
||||
|
||||
A string containing extra CSS classes to apply to the fieldset.
|
||||
|
||||
Example::
|
||||
|
||||
{
|
||||
'classes': 'wide',
|
||||
}
|
||||
|
||||
Apply multiple classes by separating them with spaces. Example::
|
||||
|
||||
{
|
||||
'classes': 'wide extrapretty',
|
||||
}
|
||||
|
||||
Two useful classes defined by the default admin-site stylesheet are
|
||||
``collapse`` and ``wide``. Fieldsets with the ``collapse`` style will be
|
||||
initially collapsed in the admin and replaced with a small "click to expand"
|
||||
link. Fieldsets with the ``wide`` style will be given extra horizontal space.
|
||||
|
||||
``description``
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
A string of optional extra text to be displayed at the top of each fieldset,
|
||||
under the heading of the fieldset. It's used verbatim, so you can use any HTML
|
||||
and you must escape any special HTML characters (such as ampersands) yourself.
|
||||
|
||||
``js``
|
||||
------
|
||||
|
||||
A list of strings representing URLs of JavaScript files to link into the admin
|
||||
screen via ``<script src="">`` tags. This can be used to tweak a given type of
|
||||
admin page in JavaScript or to provide "quick links" to fill in default values
|
||||
for certain fields.
|
||||
|
||||
If you use relative URLs -- URLs that don't start with ``http://`` or ``/`` --
|
||||
then the admin site will automatically prefix these links with
|
||||
``settings.ADMIN_MEDIA_PREFIX``.
|
||||
|
||||
``list_display``
|
||||
----------------
|
||||
|
||||
Set ``list_display`` to control which fields are displayed on the change list
|
||||
page of the admin.
|
||||
|
||||
Example::
|
||||
|
||||
list_display = ('first_name', 'last_name')
|
||||
|
||||
If you don't set ``list_display``, the admin site will display a single column
|
||||
that displays the ``__str__()`` representation of each object.
|
||||
|
||||
A few special cases to note about ``list_display``:
|
||||
|
||||
* If the field is a ``ForeignKey``, Django will display the
|
||||
``__unicode__()`` of the related object.
|
||||
|
||||
* ``ManyToManyField`` fields aren't supported, because that would entail
|
||||
executing a separate SQL statement for each row in the table. If you
|
||||
want to do this nonetheless, give your model a custom method, and add
|
||||
that method's name to ``list_display``. (See below for more on custom
|
||||
methods in ``list_display``.)
|
||||
|
||||
* If the field is a ``BooleanField`` or ``NullBooleanField``, Django will
|
||||
display a pretty "on" or "off" icon instead of ``True`` or ``False``.
|
||||
|
||||
* If the string given is a method of the model, Django will call it and
|
||||
display the output. This method should have a ``short_description``
|
||||
function attribute, for use as the header for the field.
|
||||
|
||||
Here's a full example model::
|
||||
|
||||
class Person(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
birthday = models.DateField()
|
||||
|
||||
class Admin:
|
||||
list_display = ('name', 'decade_born_in')
|
||||
|
||||
def decade_born_in(self):
|
||||
return self.birthday.strftime('%Y')[:3] + "0's"
|
||||
decade_born_in.short_description = 'Birth decade'
|
||||
|
||||
* If the string given is a method of the model, Django will HTML-escape the
|
||||
output by default. If you'd rather not escape the output of the method,
|
||||
give the method an ``allow_tags`` attribute whose value is ``True``.
|
||||
|
||||
Here's a full example model::
|
||||
|
||||
class Person(models.Model):
|
||||
first_name = models.CharField(max_length=50)
|
||||
last_name = models.CharField(max_length=50)
|
||||
color_code = models.CharField(max_length=6)
|
||||
|
||||
class Admin:
|
||||
list_display = ('first_name', 'last_name', 'colored_name')
|
||||
|
||||
def colored_name(self):
|
||||
return '<span style="color: #%s;">%s %s</span>' % (self.color_code, self.first_name, self.last_name)
|
||||
colored_name.allow_tags = True
|
||||
|
||||
* If the string given is a method of the model that returns True or False
|
||||
Django will display a pretty "on" or "off" icon if you give the method a
|
||||
``boolean`` attribute whose value is ``True``.
|
||||
|
||||
Here's a full example model::
|
||||
|
||||
class Person(models.Model):
|
||||
first_name = models.CharField(max_length=50)
|
||||
birthday = models.DateField()
|
||||
|
||||
class Admin:
|
||||
list_display = ('name', 'born_in_fifties')
|
||||
|
||||
def born_in_fifties(self):
|
||||
return self.birthday.strftime('%Y')[:3] == 5
|
||||
born_in_fifties.boolean = True
|
||||
|
||||
|
||||
* The ``__str__()`` and ``__unicode__()`` methods are just as valid in
|
||||
``list_display`` as any other model method, so it's perfectly OK to do
|
||||
this::
|
||||
|
||||
list_display = ('__unicode__', 'some_other_field')
|
||||
|
||||
* Usually, elements of ``list_display`` that aren't actual database fields
|
||||
can't be used in sorting (because Django does all the sorting at the
|
||||
database level).
|
||||
|
||||
However, if an element of ``list_display`` represents a certain database
|
||||
field, you can indicate this fact by setting the ``admin_order_field``
|
||||
attribute of the item.
|
||||
|
||||
For example::
|
||||
|
||||
class Person(models.Model):
|
||||
first_name = models.CharField(max_length=50)
|
||||
color_code = models.CharField(max_length=6)
|
||||
|
||||
class Admin:
|
||||
list_display = ('first_name', 'colored_first_name')
|
||||
|
||||
def colored_first_name(self):
|
||||
return '<span style="color: #%s;">%s</span>' % (self.color_code, self.first_name)
|
||||
colored_first_name.allow_tags = True
|
||||
colored_first_name.admin_order_field = 'first_name'
|
||||
|
||||
The above will tell Django to order by the ``first_name`` field when
|
||||
trying to sort by ``colored_first_name`` in the admin.
|
||||
|
||||
``list_display_links``
|
||||
----------------------
|
||||
|
||||
Set ``list_display_links`` to control which fields in ``list_display`` should
|
||||
be linked to the "change" page for an object.
|
||||
|
||||
By default, the change list page will link the first column -- the first field
|
||||
specified in ``list_display`` -- to the change page for each item. But
|
||||
``list_display_links`` lets you change which columns are linked. Set
|
||||
``list_display_links`` to a list or tuple of field names (in the same format as
|
||||
``list_display``) to link.
|
||||
|
||||
``list_display_links`` can specify one or many field names. As long as the
|
||||
field names appear in ``list_display``, Django doesn't care how many (or how
|
||||
few) fields are linked. The only requirement is: If you want to use
|
||||
``list_display_links``, you must define ``list_display``.
|
||||
|
||||
In this example, the ``first_name`` and ``last_name`` fields will be linked on
|
||||
the change list page::
|
||||
|
||||
class Admin:
|
||||
list_display = ('first_name', 'last_name', 'birthday')
|
||||
list_display_links = ('first_name', 'last_name')
|
||||
|
||||
Finally, note that in order to use ``list_display_links``, you must define
|
||||
``list_display``, too.
|
||||
|
||||
``list_filter``
|
||||
---------------
|
||||
|
||||
Set ``list_filter`` to activate filters in the right sidebar of the change list
|
||||
page of the admin. This should be a list of field names, and each specified
|
||||
field should be either a ``BooleanField``, ``CharField``, ``DateField``,
|
||||
``DateTimeField``, ``IntegerField`` or ``ForeignKey``.
|
||||
|
||||
This example, taken from the ``django.contrib.auth.models.User`` model, shows
|
||||
how both ``list_display`` and ``list_filter`` work::
|
||||
|
||||
class Admin:
|
||||
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
|
||||
list_filter = ('is_staff', 'is_superuser')
|
||||
|
||||
The above code results in an admin change list page that looks like this:
|
||||
|
||||
.. image:: http://media.djangoproject.com/img/doc/users_changelist.png
|
||||
|
||||
(This example also has ``search_fields`` defined. See below.)
|
||||
|
||||
``list_per_page``
|
||||
-----------------
|
||||
|
||||
Set ``list_per_page`` to control how many items appear on each paginated admin
|
||||
change list page. By default, this is set to ``100``.
|
||||
|
||||
``list_select_related``
|
||||
-----------------------
|
||||
|
||||
Set ``list_select_related`` to tell Django to use ``select_related()`` in
|
||||
retrieving the list of objects on the admin change list page. This can save you
|
||||
a bunch of database queries.
|
||||
|
||||
The value should be either ``True`` or ``False``. Default is ``False``.
|
||||
|
||||
Note that Django will use ``select_related()``, regardless of this setting,
|
||||
if one of the ``list_display`` fields is a ``ForeignKey``.
|
||||
|
||||
For more on ``select_related()``, see `the select_related() docs`_.
|
||||
|
||||
.. _the select_related() docs: ../db-api/#select-related
|
||||
|
||||
``ordering``
|
||||
------------
|
||||
|
||||
Set ``ordering`` to specify how objects on the admin change list page should be
|
||||
ordered. This should be a list or tuple in the same format as a model's
|
||||
``ordering`` parameter.
|
||||
|
||||
If this isn't provided, the Django admin will use the model's default ordering.
|
||||
|
||||
``save_as``
|
||||
-----------
|
||||
|
||||
Set ``save_as`` to enable a "save as" feature on admin change forms.
|
||||
|
||||
Normally, objects have three save options: "Save", "Save and continue editing"
|
||||
and "Save and add another". If ``save_as`` is ``True``, "Save and add another"
|
||||
will be replaced by a "Save as" button.
|
||||
|
||||
"Save as" means the object will be saved as a new object (with a new ID),
|
||||
rather than the old object.
|
||||
|
||||
By default, ``save_as`` is set to ``False``.
|
||||
|
||||
``save_on_top``
|
||||
---------------
|
||||
|
||||
Set ``save_on_top`` to add save buttons across the top of your admin change
|
||||
forms.
|
||||
|
||||
Normally, the save buttons appear only at the bottom of the forms. If you set
|
||||
``save_on_top``, the buttons will appear both on the top and the bottom.
|
||||
|
||||
By default, ``save_on_top`` is set to ``False``.
|
||||
|
||||
``search_fields``
|
||||
-----------------
|
||||
|
||||
Set ``search_fields`` to enable a search box on the admin change list page.
|
||||
This should be set to a list of field names that will be searched whenever
|
||||
somebody submits a search query in that text box.
|
||||
|
||||
These fields should be some kind of text field, such as ``CharField`` or
|
||||
``TextField``. You can also perform a related lookup on a ``ForeignKey`` with
|
||||
the lookup API "follow" notation::
|
||||
|
||||
search_fields = ['foreign_key__related_fieldname']
|
||||
|
||||
When somebody does a search in the admin search box, Django splits the search
|
||||
query into words and returns all objects that contain each of the words, case
|
||||
insensitive, where each word must be in at least one of ``search_fields``. For
|
||||
example, if ``search_fields`` is set to ``['first_name', 'last_name']`` and a
|
||||
user searches for ``john lennon``, Django will do the equivalent of this SQL
|
||||
``WHERE`` clause::
|
||||
|
||||
WHERE (first_name ILIKE '%john%' OR last_name ILIKE '%john%')
|
||||
AND (first_name ILIKE '%lennon%' OR last_name ILIKE '%lennon%')
|
||||
|
||||
For faster and/or more restrictive searches, prefix the field name
|
||||
with an operator:
|
||||
|
||||
``^``
|
||||
Matches the beginning of the field. For example, if ``search_fields`` is
|
||||
set to ``['^first_name', '^last_name']`` and a user searches for
|
||||
``john lennon``, Django will do the equivalent of this SQL ``WHERE``
|
||||
clause::
|
||||
|
||||
WHERE (first_name ILIKE 'john%' OR last_name ILIKE 'john%')
|
||||
AND (first_name ILIKE 'lennon%' OR last_name ILIKE 'lennon%')
|
||||
|
||||
This query is more efficient than the normal ``'%john%'`` query, because
|
||||
the database only needs to check the beginning of a column's data, rather
|
||||
than seeking through the entire column's data. Plus, if the column has an
|
||||
index on it, some databases may be able to use the index for this query,
|
||||
even though it's a ``LIKE`` query.
|
||||
|
||||
``=``
|
||||
Matches exactly, case-insensitive. For example, if
|
||||
``search_fields`` is set to ``['=first_name', '=last_name']`` and
|
||||
a user searches for ``john lennon``, Django will do the equivalent
|
||||
of this SQL ``WHERE`` clause::
|
||||
|
||||
WHERE (first_name ILIKE 'john' OR last_name ILIKE 'john')
|
||||
AND (first_name ILIKE 'lennon' OR last_name ILIKE 'lennon')
|
||||
|
||||
Note that the query input is split by spaces, so, following this example,
|
||||
it's currently not possible to search for all records in which
|
||||
``first_name`` is exactly ``'john winston'`` (containing a space).
|
||||
|
||||
``@``
|
||||
Performs a full-text match. This is like the default search method but uses
|
||||
an index. Currently this is only available for MySQL.
|
||||
|
||||
Managers
|
||||
========
|
||||
|
||||
|
|
|
@ -376,3 +376,125 @@ There are a couple of things to note, however.
|
|||
|
||||
Chances are these notes won't affect you unless you're trying to do something
|
||||
tricky with subclassing.
|
||||
|
||||
Model Formsets
|
||||
==============
|
||||
|
||||
Similar to regular formsets there are a couple enhanced formset classes that
|
||||
provide all the right things to work with your models. Lets reuse the
|
||||
``Author`` model from above::
|
||||
|
||||
>>> from django.newforms.models import modelformset_factory
|
||||
>>> AuthorFormSet = modelformset_factory(Author)
|
||||
|
||||
This will create a formset that is capable of working with the data associated
|
||||
to the ``Author`` model. It works just like a regular formset::
|
||||
|
||||
>>> formset = AuthorFormSet()
|
||||
>>> print formset
|
||||
<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS" /><input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS" />
|
||||
<tr><th><label for="id_form-0-name">Name:</label></th><td><input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /></td></tr>
|
||||
<tr><th><label for="id_form-0-title">Title:</label></th><td><select name="form-0-title" id="id_form-0-title">
|
||||
<option value="" selected="selected">---------</option>
|
||||
<option value="MR">Mr.</option>
|
||||
<option value="MRS">Mrs.</option>
|
||||
<option value="MS">Ms.</option>
|
||||
</select></td></tr>
|
||||
<tr><th><label for="id_form-0-birth_date">Birth date:</label></th><td><input type="text" name="form-0-birth_date" id="id_form-0-birth_date" /><input type="hidden" name="form-0-id" id="id_form-0-id" /></td></tr>
|
||||
|
||||
.. note::
|
||||
One thing to note is that ``modelformset_factory`` uses ``formset_factory``
|
||||
and by default uses ``can_delete=True``.
|
||||
|
||||
Changing the queryset
|
||||
---------------------
|
||||
|
||||
By default when you create a formset from a model the queryset will be all
|
||||
objects in the model. This is best shown as ``Author.objects.all()``. This is
|
||||
configurable::
|
||||
|
||||
>>> formset = AuthorFormSet(queryset=Author.objects.filter(name__startswith='O'))
|
||||
|
||||
Alternatively, you can use a subclassing based approach::
|
||||
|
||||
from django.newforms.models import BaseModelFormSet
|
||||
|
||||
class BaseAuthorFormSet(BaseModelFormSet):
|
||||
def get_queryset(self):
|
||||
return super(BaseAuthorFormSet, self).get_queryset().filter(name__startswith='O')
|
||||
|
||||
Then your ``BaseAuthorFormSet`` would be passed into the factory function to
|
||||
be used as a base::
|
||||
|
||||
>>> AuthorFormSet = modelformset_factory(Author, formset=BaseAuthorFormSet)
|
||||
|
||||
Saving objects in the formset
|
||||
-----------------------------
|
||||
|
||||
Similar to a ``ModelForm`` you can save the data into the model. This is done
|
||||
with the ``save()`` method on the formset::
|
||||
|
||||
# create a formset instance with POST data.
|
||||
>>> formset = AuthorFormSet(request.POST)
|
||||
|
||||
# assuming all is valid, save the data
|
||||
>>> instances = formset.save()
|
||||
|
||||
The ``save()`` method will return the instances that have been saved to the
|
||||
database. If an instance did not change in the bound data it will not be
|
||||
saved to the database and not found in ``instances`` in the above example.
|
||||
|
||||
You can optionally pass in ``commit=False`` to ``save()`` to only return the
|
||||
model instances without any database interaction::
|
||||
|
||||
# don't save to the database
|
||||
>>> instances = formset.save(commit=False)
|
||||
>>> for instance in instances:
|
||||
... # do something with instance
|
||||
... instance.save()
|
||||
|
||||
This gives you the ability to attach data to the instances before saving them
|
||||
to the database. If your formset contains a ``ManyToManyField`` you will also
|
||||
need to make a call to ``formset.save_m2m()`` to ensure the many-to-many
|
||||
relationships are saved properly.
|
||||
|
||||
Limiting the number of objects editable
|
||||
---------------------------------------
|
||||
|
||||
Similar to regular formsets you can use the ``max_num`` parameter to
|
||||
``modelformset_factory`` to limit the number of forms displayed. With
|
||||
model formsets this will properly limit the query to only select the maximum
|
||||
number of objects needed::
|
||||
|
||||
>>> Author.objects.order_by('name')
|
||||
[<Author: Charles Baudelaire>, <Author: Paul Verlaine>, <Author: Walt Whitman>]
|
||||
|
||||
>>> AuthorFormSet = modelformset_factory(Author, max_num=2, extra=1)
|
||||
>>> formset = AuthorFormSet(queryset=Author.objects.order_by('name'))
|
||||
>>> formset.initial
|
||||
[{'id': 1, 'name': u'Charles Baudelaire'}, {'id': 3, 'name': u'Paul Verlaine'}]
|
||||
|
||||
If the value of ``max_num`` is less than the total objects returned it will
|
||||
fill the rest with extra forms::
|
||||
|
||||
>>> AuthorFormSet = modelformset_factory(Author, max_num=4, extra=1)
|
||||
>>> formset = AuthorFormSet(queryset=Author.objects.order_by('name'))
|
||||
>>> for form in formset.forms:
|
||||
... print form.as_table()
|
||||
<tr><th><label for="id_form-0-name">Name:</label></th><td><input id="id_form-0-name" type="text" name="form-0-name" value="Charles Baudelaire" maxlength="100" /><input type="hidden" name="form-0-id" value="1" id="id_form-0-id" /></td></tr>
|
||||
<tr><th><label for="id_form-1-name">Name:</label></th><td><input id="id_form-1-name" type="text" name="form-1-name" value="Paul Verlaine" maxlength="100" /><input type="hidden" name="form-1-id" value="3" id="id_form-1-id" /></td></tr>
|
||||
<tr><th><label for="id_form-2-name">Name:</label></th><td><input id="id_form-2-name" type="text" name="form-2-name" value="Walt Whitman" maxlength="100" /><input type="hidden" name="form-2-id" value="2" id="id_form-2-id" /></td></tr>
|
||||
<tr><th><label for="id_form-3-name">Name:</label></th><td><input id="id_form-3-name" type="text" name="form-3-name" maxlength="100" /><input type="hidden" name="form-3-id" id="id_form-3-id" /></td></tr>
|
||||
|
||||
Using ``inlineformset_factory``
|
||||
-------------------------------
|
||||
|
||||
The ``inlineformset_factory`` is a helper to a common usage pattern of working
|
||||
with related objects through a foreign key. Suppose you have two models
|
||||
``Author`` and ``Book``. You want to create a formset that works with the
|
||||
books of a specific author. Here is how you could accomplish this::
|
||||
|
||||
>>> from django.newforms.models import inlineformset_factory
|
||||
>>> BookFormSet = inlineformset_factory(Author, Book)
|
||||
>>> author = Author.objects.get(name=u'Orson Scott Card')
|
||||
>>> formset = BookFormSet(instance=author)
|
||||
|
|
|
@ -76,6 +76,9 @@ The library deals with these concepts:
|
|||
* **Form** -- A collection of fields that knows how to validate itself and
|
||||
display itself as HTML.
|
||||
|
||||
* **Media** -- A definition of the CSS and JavaScript resources that are
|
||||
required to render a form.
|
||||
|
||||
The library is decoupled from the other Django components, such as the database
|
||||
layer, views and templates. It relies only on Django settings, a couple of
|
||||
``django.utils`` helper functions and Django's internationalization hooks (but
|
||||
|
@ -1864,6 +1867,643 @@ They've been deprecated, but you can still `view the documentation`_.
|
|||
.. _ModelForms documentation: ../modelforms/
|
||||
.. _view the documentation: ../form_for_model/
|
||||
|
||||
Media
|
||||
=====
|
||||
|
||||
Rendering an attractive and easy-to-use web form requires more than just
|
||||
HTML - it also requires CSS stylesheets, and if you want to use fancy
|
||||
"Web2.0" widgets, you may also need to include some JavaScript on each
|
||||
page. The exact combination of CSS and JavaScript that is required for
|
||||
any given page will depend upon the widgets that are in use on that page.
|
||||
|
||||
This is where Django media definitions come in. Django allows you to
|
||||
associate different media files with the forms and widgets that require
|
||||
that media. For example, if you want to use a calendar to render DateFields,
|
||||
you can define a custom Calendar widget. This widget can then be associated
|
||||
with the CSS and Javascript that is required to render the calendar. When
|
||||
the Calendar widget is used on a form, Django is able to identify the CSS and
|
||||
JavaScript files that are required, and provide the list of file names
|
||||
in a form suitable for easy inclusion on your web page.
|
||||
|
||||
.. admonition:: Media and Django Admin
|
||||
|
||||
The Django Admin application defines a number of customized widgets
|
||||
for calendars, filtered selections, and so on. These widgets define
|
||||
media requirements, and the Django Admin uses the custom widgets
|
||||
in place of the Django defaults. The Admin templates will only include
|
||||
those media files that are required to render the widgets on any
|
||||
given page.
|
||||
|
||||
If you like the widgets that the Django Admin application uses,
|
||||
feel free to use them in your own application! They're all stored
|
||||
in ``django.contrib.admin.widgets``.
|
||||
|
||||
.. admonition:: Which JavaScript toolkit?
|
||||
|
||||
Many JavaScript toolkits exist, and many of them include widgets (such
|
||||
as calendar widgets) that can be used to enhance your application.
|
||||
Django has deliberately avoided blessing any one JavaScript toolkit.
|
||||
Each toolkit has its own relative strengths and weaknesses - use
|
||||
whichever toolkit suits your requirements. Django is able to integrate
|
||||
with any JavaScript toolkit.
|
||||
|
||||
Media as a static definition
|
||||
----------------------------
|
||||
|
||||
The easiest way to define media is as a static definition. Using this method,
|
||||
the media declaration is an inner class. The properties of the inner class
|
||||
define the media requirements.
|
||||
|
||||
Here's a simple example::
|
||||
|
||||
class CalendarWidget(forms.TextInput):
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('pretty.css',)
|
||||
}
|
||||
js = ('animations.js', 'actions.js')
|
||||
|
||||
This code defines a ``CalendarWidget``, which will be based on ``TextInput``.
|
||||
Every time the CalendarWidget is used on a form, that form will be directed
|
||||
to include the CSS file ``pretty.css``, and the JavaScript files
|
||||
``animations.js`` and ``actions.js``.
|
||||
|
||||
This static media definition is converted at runtime into a widget property
|
||||
named ``media``. The media for a CalendarWidget instance can be retrieved
|
||||
through this property::
|
||||
|
||||
>>> w = CalendarWidget()
|
||||
>>> print w.media
|
||||
<link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" />
|
||||
<script type="text/javascript" src="http://media.example.com/animations.js"></script>
|
||||
<script type="text/javascript" src="http://media.example.com/actions.js"></script>
|
||||
|
||||
Here's a list of all possible ``Media`` options. There are no required options.
|
||||
|
||||
``css``
|
||||
~~~~~~~
|
||||
|
||||
A dictionary describing the CSS files required for various forms of output
|
||||
media.
|
||||
|
||||
The values in the dictionary should be a tuple/list of file names. See
|
||||
`the section on media paths`_ for details of how to specify paths to media
|
||||
files.
|
||||
|
||||
.. _the section on media paths: `Paths in media definitions`_
|
||||
|
||||
The keys in the dictionary are the output media types. These are the same
|
||||
types accepted by CSS files in media declarations: 'all', 'aural', 'braille',
|
||||
'embossed', 'handheld', 'print', 'projection', 'screen', 'tty' and 'tv'. If
|
||||
you need to have different stylesheets for different media types, provide
|
||||
a list of CSS files for each output medium. The following example would
|
||||
provide two CSS options -- one for the screen, and one for print::
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'screen': ('pretty.css',),
|
||||
'print': ('newspaper.css',)
|
||||
}
|
||||
|
||||
If a group of CSS files are appropriate for multiple output media types,
|
||||
the dictionary key can be a comma separated list of output media types.
|
||||
In the following example, TV's and projectors will have the same media
|
||||
requirements::
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'screen': ('pretty.css',),
|
||||
'tv,projector': ('lo_res.css',),
|
||||
'print': ('newspaper.css',)
|
||||
}
|
||||
|
||||
If this last CSS definition were to be rendered, it would become the following HTML::
|
||||
|
||||
<link href="http://media.example.com/pretty.css" type="text/css" media="screen" rel="stylesheet" />
|
||||
<link href="http://media.example.com/lo_res.css" type="text/css" media="tv,projector" rel="stylesheet" />
|
||||
<link href="http://media.example.com/newspaper.css" type="text/css" media="print" rel="stylesheet" />
|
||||
|
||||
``js``
|
||||
~~~~~~
|
||||
|
||||
A tuple describing the required javascript files. See
|
||||
`the section on media paths`_ for details of how to specify paths to media
|
||||
files.
|
||||
|
||||
``extend``
|
||||
~~~~~~~~~~
|
||||
|
||||
A boolean defining inheritance behavior for media declarations.
|
||||
|
||||
By default, any object using a static media definition will inherit all the
|
||||
media associated with the parent widget. This occurs regardless of how the
|
||||
parent defines its media requirements. For example, if we were to extend our
|
||||
basic Calendar widget from the example above::
|
||||
|
||||
class FancyCalendarWidget(CalendarWidget):
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('fancy.css',)
|
||||
}
|
||||
js = ('whizbang.js',)
|
||||
|
||||
>>> w = FancyCalendarWidget()
|
||||
>>> print w.media
|
||||
<link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" />
|
||||
<link href="http://media.example.com/fancy.css" type="text/css" media="all" rel="stylesheet" />
|
||||
<script type="text/javascript" src="http://media.example.com/animations.js"></script>
|
||||
<script type="text/javascript" src="http://media.example.com/actions.js"></script>
|
||||
<script type="text/javascript" src="http://media.example.com/whizbang.js"></script>
|
||||
|
||||
The FancyCalendar widget inherits all the media from it's parent widget. If
|
||||
you don't want media to be inherited in this way, add an ``extend=False``
|
||||
declaration to the media declaration::
|
||||
|
||||
class FancyCalendar(Calendar):
|
||||
class Media:
|
||||
extend = False
|
||||
css = {
|
||||
'all': ('fancy.css',)
|
||||
}
|
||||
js = ('whizbang.js',)
|
||||
|
||||
>>> w = FancyCalendarWidget()
|
||||
>>> print w.media
|
||||
<link href="http://media.example.com/fancy.css" type="text/css" media="all" rel="stylesheet" />
|
||||
<script type="text/javascript" src="http://media.example.com/whizbang.js"></script>
|
||||
|
||||
If you require even more control over media inheritance, define your media
|
||||
using a `dynamic property`_. Dynamic properties give you complete control over
|
||||
which media files are inherited, and which are not.
|
||||
|
||||
.. _dynamic property: `Media as a dynamic property`_
|
||||
|
||||
Media as a dynamic property
|
||||
---------------------------
|
||||
|
||||
If you need to perform some more sophisticated manipulation of media
|
||||
requirements, you can define the media property directly. This is done
|
||||
by defining a model property that returns an instance of ``forms.Media``.
|
||||
The constructor for ``forms.Media`` accepts ``css`` and ``js`` keyword
|
||||
arguments in the same format as that used in a static media definition.
|
||||
|
||||
For example, the static media definition for our Calendar Widget could
|
||||
also be defined in a dynamic fashion::
|
||||
|
||||
class CalendarWidget(forms.TextInput):
|
||||
def _media(self):
|
||||
return forms.Media(css={'all': ('pretty.css',)},
|
||||
js=('animations.js', 'actions.js'))
|
||||
media = property(_media)
|
||||
|
||||
See the section on `Media objects`_ for more details on how to construct
|
||||
return values for dynamic media properties.
|
||||
|
||||
Paths in media definitions
|
||||
--------------------------
|
||||
|
||||
Paths used to specify media can be either relative or absolute. If a path
|
||||
starts with '/', 'http://' or 'https://', it will be interpreted as an absolute
|
||||
path, and left as-is. All other paths will be prepended with the value of
|
||||
``settings.MEDIA_URL``. For example, if the MEDIA_URL for your site was
|
||||
``http://media.example.com/``::
|
||||
|
||||
class CalendarWidget(forms.TextInput):
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('/css/pretty.css',),
|
||||
}
|
||||
js = ('animations.js', 'http://othersite.com/actions.js')
|
||||
|
||||
>>> w = CalendarWidget()
|
||||
>>> print w.media
|
||||
<link href="/css/pretty.css" type="text/css" media="all" rel="stylesheet" />
|
||||
<script type="text/javascript" src="http://media.example.com/animations.js"></script>
|
||||
<script type="text/javascript" src="http://othersite.com/actions.js"></script>
|
||||
|
||||
Media objects
|
||||
-------------
|
||||
|
||||
When you interrogate the media attribute of a widget or form, the value that
|
||||
is returned is a ``forms.Media`` object. As we have already seen, the string
|
||||
representation of a Media object is the HTML required to include media
|
||||
in the ``<head>`` block of your HTML page.
|
||||
|
||||
However, Media objects have some other interesting properties.
|
||||
|
||||
Media subsets
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
If you only want media of a particular type, you can use the subscript operator
|
||||
to filter out a medium of interest. For example::
|
||||
|
||||
>>> w = CalendarWidget()
|
||||
>>> print w.media
|
||||
<link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" />
|
||||
<script type="text/javascript" src="http://media.example.com/animations.js"></script>
|
||||
<script type="text/javascript" src="http://media.example.com/actions.js"></script>
|
||||
|
||||
>>> print w.media['css']
|
||||
<link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" />
|
||||
|
||||
When you use the subscript operator, the value that is returned is a new
|
||||
Media object -- but one that only contains the media of interest.
|
||||
|
||||
Combining media objects
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Media objects can also be added together. When two media objects are added,
|
||||
the resulting Media object contains the union of the media from both files::
|
||||
|
||||
class CalendarWidget(forms.TextInput):
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('pretty.css',)
|
||||
}
|
||||
js = ('animations.js', 'actions.js')
|
||||
|
||||
class OtherWidget(forms.TextInput):
|
||||
class Media:
|
||||
js = ('whizbang.js',)
|
||||
|
||||
>>> w1 = CalendarWidget()
|
||||
>>> w2 = OtherWidget()
|
||||
>>> print w1+w2
|
||||
<link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" />
|
||||
<script type="text/javascript" src="http://media.example.com/animations.js"></script>
|
||||
<script type="text/javascript" src="http://media.example.com/actions.js"></script>
|
||||
<script type="text/javascript" src="http://media.example.com/whizbang.js"></script>
|
||||
|
||||
Media on Forms
|
||||
--------------
|
||||
|
||||
Widgets aren't the only objects that can have media definitions -- forms
|
||||
can also define media. The rules for media definitions on forms are the
|
||||
same as the rules for widgets: declarations can be static or dynamic;
|
||||
path and inheritance rules for those declarations are exactly the same.
|
||||
|
||||
Regardless of whether you define a media declaration, *all* Form objects
|
||||
have a media property. The default value for this property is the result
|
||||
of adding the media definitions for all widgets that are part of the form::
|
||||
|
||||
class ContactForm(forms.Form):
|
||||
date = DateField(widget=CalendarWidget)
|
||||
name = CharField(max_length=40, widget=OtherWidget)
|
||||
|
||||
>>> f = ContactForm()
|
||||
>>> f.media
|
||||
<link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" />
|
||||
<script type="text/javascript" src="http://media.example.com/animations.js"></script>
|
||||
<script type="text/javascript" src="http://media.example.com/actions.js"></script>
|
||||
<script type="text/javascript" src="http://media.example.com/whizbang.js"></script>
|
||||
|
||||
If you want to associate additional media with a form -- for example, CSS for form
|
||||
layout -- simply add a media declaration to the form::
|
||||
|
||||
class ContactForm(forms.Form):
|
||||
date = DateField(widget=CalendarWidget)
|
||||
name = CharField(max_length=40, widget=OtherWidget)
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('layout.css',)
|
||||
}
|
||||
|
||||
>>> f = ContactForm()
|
||||
>>> f.media
|
||||
<link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" />
|
||||
<link href="http://media.example.com/layout.css" type="text/css" media="all" rel="stylesheet" />
|
||||
<script type="text/javascript" src="http://media.example.com/animations.js"></script>
|
||||
<script type="text/javascript" src="http://media.example.com/actions.js"></script>
|
||||
<script type="text/javascript" src="http://media.example.com/whizbang.js"></script>
|
||||
|
||||
Formsets
|
||||
========
|
||||
|
||||
A formset is a layer of abstraction to working with multiple forms on the same
|
||||
page. It can be best compared to a data grid. Let's say you have the following
|
||||
form::
|
||||
|
||||
>>> from django import newforms as forms
|
||||
>>> class ArticleForm(forms.Form):
|
||||
... title = forms.CharField()
|
||||
... pub_date = forms.DateField()
|
||||
|
||||
You might want to allow the user to create several articles at once. To create
|
||||
a formset of ``ArticleForm``s you would do::
|
||||
|
||||
>>> from django.newforms.formsets import formset_factory
|
||||
>>> ArticleFormSet = formset_factory(ArticleForm)
|
||||
|
||||
You now have created a formset named ``ArticleFormSet``. The formset gives you
|
||||
the ability to iterate over the forms in the formset and display them as you
|
||||
would with a regular form::
|
||||
|
||||
>>> formset = ArticleFormSet()
|
||||
>>> for form in formset.forms:
|
||||
... print form.as_table()
|
||||
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
|
||||
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
|
||||
|
||||
As you can see it only displayed one form. This is because by default the
|
||||
``formset_factory`` defines one extra form. This can be controlled with the
|
||||
``extra`` parameter::
|
||||
|
||||
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
|
||||
|
||||
Using initial data with a formset
|
||||
---------------------------------
|
||||
|
||||
Initial data is what drives the main usability of a formset. As shown above
|
||||
you can define the number of extra forms. What this means is that you are
|
||||
telling the formset how many additional forms to show in addition to the
|
||||
number of forms it generates from the initial data. Lets take a look at an
|
||||
example::
|
||||
|
||||
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
|
||||
>>> formset = ArticleFormSet(initial=[
|
||||
... {'title': u'Django is now open source',
|
||||
... 'pub_date': datetime.date.today()},
|
||||
... ])
|
||||
|
||||
>>> for form in formset.forms:
|
||||
... print form.as_table()
|
||||
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Django is now open source" id="id_form-0-title" /></td></tr>
|
||||
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-12" id="id_form-0-pub_date" /></td></tr>
|
||||
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" id="id_form-1-title" /></td></tr>
|
||||
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" id="id_form-1-pub_date" /></td></tr>
|
||||
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
|
||||
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
|
||||
|
||||
There are now a total of three forms showing above. One for the initial data
|
||||
that was passed in and two extra forms. Also note that we are passing in a
|
||||
list of dictionaries as the initial data.
|
||||
|
||||
Limiting the maximum number of forms
|
||||
------------------------------------
|
||||
|
||||
The ``max_num`` parameter to ``formset_factory`` gives you the ability to
|
||||
force the maximum number of forms the formset will display::
|
||||
|
||||
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1)
|
||||
>>> formset = ArticleFormset()
|
||||
>>> for form in formset.forms:
|
||||
... print form.as_table()
|
||||
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
|
||||
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
|
||||
|
||||
The default value of ``max_num`` is ``0`` which is the same as saying put no
|
||||
limit on the number forms displayed.
|
||||
|
||||
Formset validation
|
||||
------------------
|
||||
|
||||
Validation with a formset is about identical to a regular ``Form``. There is
|
||||
an ``is_valid`` method on the formset to provide a convenient way to validate
|
||||
each form in the formset::
|
||||
|
||||
>>> ArticleFormSet = formset_factory(ArticleForm)
|
||||
>>> formset = ArticleFormSet({})
|
||||
>>> formset.is_valid()
|
||||
True
|
||||
|
||||
We passed in no data to the formset which is resulting in a valid form. The
|
||||
formset is smart enough to ignore extra forms that were not changed. If we
|
||||
attempt to provide an article, but fail to do so::
|
||||
|
||||
>>> data = {
|
||||
... 'form-TOTAL_FORMS': u'1',
|
||||
... 'form-INITIAL_FORMS': u'1',
|
||||
... 'form-0-title': u'Test',
|
||||
... 'form-0-pub_date': u'',
|
||||
... }
|
||||
>>> formset = ArticleFormSet(data)
|
||||
>>> formset.is_valid()
|
||||
False
|
||||
>>> formset.errors
|
||||
[{'pub_date': [u'This field is required.']}]
|
||||
|
||||
As we can see the formset properly performed validation and gave us the
|
||||
expected errors.
|
||||
|
||||
Understanding the ManagementForm
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You may have noticed the additional data that was required in the formset's
|
||||
data above. This data is coming from the ``ManagementForm``. This form is
|
||||
dealt with internally to the formset. If you don't use it, it will result in
|
||||
an exception::
|
||||
|
||||
>>> data = {
|
||||
... 'form-0-title': u'Test',
|
||||
... 'form-0-pub_date': u'',
|
||||
... }
|
||||
>>> formset = ArticleFormSet(data)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
django.newforms.util.ValidationError: [u'ManagementForm data is missing or has been tampered with']
|
||||
|
||||
It is used to keep track of how many form instances are being displayed. If
|
||||
you are adding new forms via javascript, you should increment the count fields
|
||||
in this form as well.
|
||||
|
||||
Custom formset validation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A formset has a ``clean`` method similar to the one on a ``Form`` class. This
|
||||
is where you define your own validation that deals at the formset level::
|
||||
|
||||
>>> from django.newforms.formsets import BaseFormSet
|
||||
|
||||
>>> class BaseArticleFormSet(BaseFormSet):
|
||||
... def clean(self):
|
||||
... raise forms.ValidationError, u'An error occured.'
|
||||
|
||||
>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
|
||||
>>> formset = ArticleFormSet({})
|
||||
>>> formset.is_valid()
|
||||
False
|
||||
>>> formset.non_form_errors()
|
||||
[u'An error occured.']
|
||||
|
||||
The formset ``clean`` method is called after all the ``Form.clean`` methods
|
||||
have been called. The errors will be found using the ``non_form_errors()``
|
||||
method on the formset.
|
||||
|
||||
Dealing with ordering and deletion of forms
|
||||
-------------------------------------------
|
||||
|
||||
Common use cases with a formset is dealing with ordering and deletion of the
|
||||
form instances. This has been dealt with for you. The ``formset_factory``
|
||||
provides two optional parameters ``can_order`` and ``can_delete`` that will do
|
||||
the extra work of adding the extra fields and providing simpler ways of
|
||||
getting to that data.
|
||||
|
||||
``can_order``
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Default: ``False``
|
||||
|
||||
Lets create a formset with the ability to order::
|
||||
|
||||
>>> ArticleFormSet = formset_factory(ArticleForm, can_order=True)
|
||||
>>> formset = ArticleFormSet(initial=[
|
||||
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
|
||||
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
|
||||
... ])
|
||||
>>> for form in formset.forms:
|
||||
... print form.as_table()
|
||||
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title" /></td></tr>
|
||||
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date" /></td></tr>
|
||||
<tr><th><label for="id_form-0-ORDER">Order:</label></th><td><input type="text" name="form-0-ORDER" value="1" id="id_form-0-ORDER" /></td></tr>
|
||||
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title" /></td></tr>
|
||||
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date" /></td></tr>
|
||||
<tr><th><label for="id_form-1-ORDER">Order:</label></th><td><input type="text" name="form-1-ORDER" value="2" id="id_form-1-ORDER" /></td></tr>
|
||||
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
|
||||
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
|
||||
<tr><th><label for="id_form-2-ORDER">Order:</label></th><td><input type="text" name="form-2-ORDER" id="id_form-2-ORDER" /></td></tr>
|
||||
|
||||
This adds an additional field to each form. This new field is named ``ORDER``
|
||||
and is an ``forms.IntegerField``. For the forms that came from the initial
|
||||
data it automatically assigned them a numeric value. Lets look at what will
|
||||
happen when the user changes these values::
|
||||
|
||||
>>> data = {
|
||||
... 'form-TOTAL_FORMS': u'3',
|
||||
... 'form-INITIAL_FORMS': u'2',
|
||||
... 'form-0-title': u'Article #1',
|
||||
... 'form-0-pub_date': u'2008-05-10',
|
||||
... 'form-0-ORDER': u'2',
|
||||
... 'form-1-title': u'Article #2',
|
||||
... 'form-1-pub_date': u'2008-05-11',
|
||||
... 'form-1-ORDER': u'1',
|
||||
... 'form-2-title': u'Article #3',
|
||||
... 'form-2-pub_date': u'2008-05-01',
|
||||
... 'form-2-ORDER': u'0',
|
||||
... }
|
||||
|
||||
>>> formset = ArticleFormSet(data, initial=[
|
||||
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
|
||||
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
|
||||
... ])
|
||||
>>> formset.is_valid()
|
||||
True
|
||||
>>> for form in formset.ordered_forms:
|
||||
... print form.cleaned_data
|
||||
{'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0, 'title': u'Article #3'}
|
||||
{'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1, 'title': u'Article #2'}
|
||||
{'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2, 'title': u'Article #1'}
|
||||
|
||||
``can_delete``
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Default: ``False``
|
||||
|
||||
Lets create a formset with the ability to delete::
|
||||
|
||||
>>> ArticleFormSet = formset_factory(ArticleForm, can_delete=True)
|
||||
>>> formset = ArticleFormSet(initial=[
|
||||
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
|
||||
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
|
||||
... ])
|
||||
>>> for form in formset.forms:
|
||||
.... print form.as_table()
|
||||
<input type="hidden" name="form-TOTAL_FORMS" value="3" id="id_form-TOTAL_FORMS" /><input type="hidden" name="form-INITIAL_FORMS" value="2" id="id_form-INITIAL_FORMS" />
|
||||
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title" /></td></tr>
|
||||
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date" /></td></tr>
|
||||
<tr><th><label for="id_form-0-DELETE">Delete:</label></th><td><input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE" /></td></tr>
|
||||
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title" /></td></tr>
|
||||
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date" /></td></tr>
|
||||
<tr><th><label for="id_form-1-DELETE">Delete:</label></th><td><input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE" /></td></tr>
|
||||
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
|
||||
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
|
||||
<tr><th><label for="id_form-2-DELETE">Delete:</label></th><td><input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE" /></td></tr>
|
||||
|
||||
Similar to ``can_order`` this adds a new field to each form named ``DELETE``
|
||||
and is a ``forms.BooleanField``. When data comes through marking any of the
|
||||
delete fields you can access them with ``deleted_forms``::
|
||||
|
||||
>>> data = {
|
||||
... 'form-TOTAL_FORMS': u'3',
|
||||
... 'form-INITIAL_FORMS': u'2',
|
||||
... 'form-0-title': u'Article #1',
|
||||
... 'form-0-pub_date': u'2008-05-10',
|
||||
... 'form-0-DELETE': u'on',
|
||||
... 'form-1-title': u'Article #2',
|
||||
... 'form-1-pub_date': u'2008-05-11',
|
||||
... 'form-1-DELETE': u'',
|
||||
... 'form-2-title': u'',
|
||||
... 'form-2-pub_date': u'',
|
||||
... 'form-2-DELETE': u'',
|
||||
... }
|
||||
|
||||
>>> formset = ArticleFormSet(data, initial=[
|
||||
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
|
||||
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
|
||||
... ])
|
||||
>>> [form.cleaned_data for form in formset.deleted_forms]
|
||||
[{'DELETE': True, 'pub_date': datetime.date(2008, 5, 10), 'title': u'Article #1'}]
|
||||
|
||||
Adding additional fields to a formset
|
||||
-------------------------------------
|
||||
|
||||
If you need to add additional fields to the formset this can be easily
|
||||
accomplished. The formset base class provides an ``add_fields`` method. You
|
||||
can simply override this method to add your own fields or even redefine the
|
||||
default fields/attributes of the order and deletion fields::
|
||||
|
||||
>>> class BaseArticleFormSet(BaseFormSet):
|
||||
... def add_fields(self, form, index):
|
||||
... super(BaseArticleFormSet, self).add_fields(form, index)
|
||||
... form.fields["my_field"] = forms.CharField()
|
||||
|
||||
>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
|
||||
>>> formset = ArticleFormSet()
|
||||
>>> for form in formset.forms:
|
||||
... print form.as_table()
|
||||
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
|
||||
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
|
||||
<tr><th><label for="id_form-0-my_field">My field:</label></th><td><input type="text" name="form-0-my_field" id="id_form-0-my_field" /></td></tr>
|
||||
|
||||
Using a formsets in views and templates
|
||||
---------------------------------------
|
||||
|
||||
Using a formset inside a view is as easy as using a regular ``Form`` class.
|
||||
The only thing you will want to be aware of is making sure to use the
|
||||
management form inside the template. Lets look at a sample view::
|
||||
|
||||
def manage_articles(request):
|
||||
ArticleFormSet = formset_factory(ArticleForm)
|
||||
if request.method == 'POST':
|
||||
formset = ArticleFormSet(request.POST, request.FILES)
|
||||
if formset.is_valid():
|
||||
# do something with the formset.cleaned_data
|
||||
else:
|
||||
formset = ArticleFormSet()
|
||||
return render_to_response('manage_articles.html', {'formset': formset})
|
||||
|
||||
The ``manage_articles.html`` template might look like this::
|
||||
|
||||
<form method="POST" action="">
|
||||
{{ formset.management_form }}
|
||||
<table>
|
||||
{% for form in formset.forms %}
|
||||
{{ form }}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</form>
|
||||
|
||||
However the above can be slightly shortcutted and let the formset itself deal
|
||||
with the management form::
|
||||
|
||||
<form method="POST" action="">
|
||||
<table>
|
||||
{{ formset }}
|
||||
</table>
|
||||
</form>
|
||||
|
||||
The above ends up calling the ``as_table`` method on the formset class.
|
||||
|
||||
More coming soon
|
||||
================
|
||||
|
||||
|
|
|
@ -31,10 +31,10 @@ activate the admin site for your installation, do these three things:
|
|||
* Add ``"django.contrib.admin"`` to your ``INSTALLED_APPS`` setting.
|
||||
* Run ``python manage.py syncdb``. Since you have added a new application
|
||||
to ``INSTALLED_APPS``, the database tables need to be updated.
|
||||
* Edit your ``mysite/urls.py`` file and uncomment the line below
|
||||
"Uncomment this for admin:". This file is a URLconf; we'll dig into
|
||||
URLconfs in the next tutorial. For now, all you need to know is that it
|
||||
maps URL roots to applications.
|
||||
* Edit your ``mysite/urls.py`` file and uncomment the lines below the
|
||||
"Uncomment this for admin:" comments. This file is a URLconf; we'll dig
|
||||
into URLconfs in the next tutorial. For now, all you need to know is that
|
||||
it maps URL roots to applications.
|
||||
|
||||
Start the development server
|
||||
============================
|
||||
|
@ -71,19 +71,13 @@ Make the poll app modifiable in the admin
|
|||
|
||||
But where's our poll app? It's not displayed on the admin index page.
|
||||
|
||||
Just one thing to do: We need to specify in the ``Poll`` model that ``Poll``
|
||||
Just one thing to do: We need to tell the admin that ``Poll``
|
||||
objects have an admin interface. Edit the ``mysite/polls/models.py`` file and
|
||||
make the following change to add an inner ``Admin`` class::
|
||||
add the following to the bottom of the file::
|
||||
|
||||
class Poll(models.Model):
|
||||
# ...
|
||||
class Admin:
|
||||
pass
|
||||
|
||||
The ``class Admin`` will contain all the settings that control how this model
|
||||
appears in the Django admin. All the settings are optional, however, so
|
||||
creating an empty class means "give this object an admin interface using
|
||||
all the default options."
|
||||
from django.contrib import admin
|
||||
|
||||
admin.site.register(Poll)
|
||||
|
||||
Now reload the Django admin page to see your changes. Note that you don't have
|
||||
to restart the development server -- the server will auto-reload your project,
|
||||
|
@ -92,8 +86,8 @@ so any modifications code will be seen immediately in your browser.
|
|||
Explore the free admin functionality
|
||||
====================================
|
||||
|
||||
Now that ``Poll`` has the inner ``Admin`` class, Django knows that it should be
|
||||
displayed on the admin index page:
|
||||
Now that we've registered ``Poll``, Django knows that it should be displayed on
|
||||
the admin index page:
|
||||
|
||||
.. image:: http://media.djangoproject.com/img/doc/tutorial-trunk/admin03t.png
|
||||
:alt: Django admin index page, now with polls displayed
|
||||
|
@ -145,17 +139,26 @@ with the timestamp and username of the person who made the change:
|
|||
Customize the admin form
|
||||
========================
|
||||
|
||||
Take a few minutes to marvel at all the code you didn't have to write.
|
||||
Take a few minutes to marvel at all the code you didn't have to write. When you
|
||||
call ``admin.site.register(Poll)``, Django just lets you edit the object and
|
||||
"guess" at how to display it within the admin. Often you'll want to control how
|
||||
the admin looks and works. You'll do this by telling Django about the options
|
||||
you want when you register the object.
|
||||
|
||||
Let's customize this a bit. We can reorder the fields by explicitly adding a
|
||||
``fields`` parameter to ``Admin``::
|
||||
Let's see how this works by reordering the fields on the edit form. Replace the
|
||||
``admin.site.register(Poll)`` line with::
|
||||
|
||||
class Admin:
|
||||
fields = (
|
||||
(None, {'fields': ('pub_date', 'question')}),
|
||||
)
|
||||
class PollAdmin(admin.ModelAdmin):
|
||||
fields = ['pub_date', 'question']
|
||||
|
||||
admin.site.register(Poll, PollAdmin)
|
||||
|
||||
That made the "Publication date" show up first instead of second:
|
||||
You'll follow this pattern -- create a model admin object, then pass it as the
|
||||
second argument to ``admin.site.register()`` -- any time you need to change the
|
||||
admin options for an object.
|
||||
|
||||
This particular change above makes the "Publication date" come before the
|
||||
"Question" field:
|
||||
|
||||
.. image:: http://media.djangoproject.com/img/doc/tutorial-trunk/admin07.png
|
||||
:alt: Fields have been reordered
|
||||
|
@ -166,13 +169,15 @@ of fields, choosing an intuitive order is an important usability detail.
|
|||
And speaking of forms with dozens of fields, you might want to split the form
|
||||
up into fieldsets::
|
||||
|
||||
class Admin:
|
||||
fields = (
|
||||
(None, {'fields': ('question',)}),
|
||||
('Date information', {'fields': ('pub_date',)}),
|
||||
)
|
||||
class PollAdmin(admin.ModelAdmin):
|
||||
fieldsets = [
|
||||
(None, {'fields': ['question']}),
|
||||
('Date information', {'fields': ['pub_date']}),
|
||||
]
|
||||
|
||||
admin.site.register(Poll, PollAdmin)
|
||||
|
||||
The first element of each tuple in ``fields`` is the title of the fieldset.
|
||||
The first element of each tuple in ``fieldsets`` is the title of the fieldset.
|
||||
Here's what our form looks like now:
|
||||
|
||||
.. image:: http://media.djangoproject.com/img/doc/tutorial-trunk/admin08t.png
|
||||
|
@ -184,11 +189,11 @@ You can assign arbitrary HTML classes to each fieldset. Django provides a
|
|||
This is useful when you have a long form that contains a number of fields that
|
||||
aren't commonly used::
|
||||
|
||||
class Admin:
|
||||
fields = (
|
||||
(None, {'fields': ('question',)}),
|
||||
('Date information', {'fields': ('pub_date',), 'classes': 'collapse'}),
|
||||
)
|
||||
class PollAdmin(admin.ModelAdmin):
|
||||
fieldsets = [
|
||||
(None, {'fields': ['question']}),
|
||||
('Date information', {'fields': ['pub_date'], 'classes': 'pub_date'}),
|
||||
]
|
||||
|
||||
.. image:: http://media.djangoproject.com/img/doc/tutorial-trunk/admin09.png
|
||||
:alt: Fieldset is initially collapsed
|
||||
|
@ -201,14 +206,10 @@ the admin page doesn't display choices.
|
|||
|
||||
Yet.
|
||||
|
||||
There are two ways to solve this problem. The first is to give the ``Choice``
|
||||
model its own inner ``Admin`` class, just as we did with ``Poll``. Here's what
|
||||
that would look like::
|
||||
There are two ways to solve this problem. The first register ``Choice`` with the
|
||||
admin just as we did with ``Poll``. That's easy::
|
||||
|
||||
class Choice(models.Model):
|
||||
# ...
|
||||
class Admin:
|
||||
pass
|
||||
admin.site.register(Choice)
|
||||
|
||||
Now "Choices" is an available option in the Django admin. The "Add choice" form
|
||||
looks like this:
|
||||
|
@ -220,33 +221,35 @@ In that form, the "Poll" field is a select box containing every poll in the
|
|||
database. Django knows that a ``ForeignKey`` should be represented in the admin
|
||||
as a ``<select>`` box. In our case, only one poll exists at this point.
|
||||
|
||||
Also note the "Add Another" link next to "Poll." Every object with a ForeignKey
|
||||
relationship to another gets this for free. When you click "Add Another," you'll
|
||||
get a popup window with the "Add poll" form. If you add a poll in that window
|
||||
and click "Save," Django will save the poll to the database and dynamically add
|
||||
it as the selected choice on the "Add choice" form you're looking at.
|
||||
Also note the "Add Another" link next to "Poll." Every object with a
|
||||
``ForeignKey`` relationship to another gets this for free. When you click "Add
|
||||
Another," you'll get a popup window with the "Add poll" form. If you add a poll
|
||||
in that window and click "Save," Django will save the poll to the database and
|
||||
dynamically add it as the selected choice on the "Add choice" form you're
|
||||
looking at.
|
||||
|
||||
But, really, this is an inefficient way of adding Choice objects to the system.
|
||||
It'd be better if you could add a bunch of Choices directly when you create the
|
||||
Poll object. Let's make that happen.
|
||||
|
||||
Remove the ``Admin`` for the Choice model. Then, edit the ``ForeignKey(Poll)``
|
||||
field like so::
|
||||
Remove the ``register()`` call for the Choice model. Then, edit the ``Poll``
|
||||
registration code to read::
|
||||
|
||||
poll = models.ForeignKey(Poll, edit_inline=models.STACKED, num_in_admin=3)
|
||||
class ChoiceInline(admin.StackedInline):
|
||||
model = Choice
|
||||
extra = 3
|
||||
|
||||
class PollAdmin(admin.ModelAdmin):
|
||||
fieldsets = [
|
||||
(None, {'fields': ['question']}),
|
||||
('Date information', {'fields': ['pub_date'], 'classes': 'pub_date'}),
|
||||
]
|
||||
inlines = [ChoiceInline]
|
||||
|
||||
admin.site.register(Poll, PollAdmin)
|
||||
|
||||
This tells Django: "Choice objects are edited on the Poll admin page. By
|
||||
default, provide enough fields for 3 Choices."
|
||||
|
||||
Then change the other fields in ``Choice`` to give them ``core=True``::
|
||||
|
||||
choice = models.CharField(max_length=200, core=True)
|
||||
votes = models.IntegerField(core=True)
|
||||
|
||||
This tells Django: "When you edit a Choice on the Poll admin page, the 'choice'
|
||||
and 'votes' fields are required. The presence of at least one of them signifies
|
||||
the addition of a new Choice object, and clearing both of them signifies the
|
||||
deletion of that existing Choice object."
|
||||
default, provide enough fields for 3 choices."
|
||||
|
||||
Load the "Add poll" page to see how that looks:
|
||||
|
||||
|
@ -255,19 +258,18 @@ Load the "Add poll" page to see how that looks:
|
|||
:target: http://media.djangoproject.com/img/doc/tutorial-trunk/admin11.png
|
||||
|
||||
It works like this: There are three slots for related Choices -- as specified
|
||||
by ``num_in_admin`` -- but each time you come back to the "Change" page for an
|
||||
already-created object, you get one extra slot. (This means there's no
|
||||
hard-coded limit on how many related objects can be added.) If you wanted space
|
||||
for three extra Choices each time you changed the poll, you'd use
|
||||
``num_extra_on_change=3``.
|
||||
by ``extra`` -- and each time you come back to the "Change" page for an
|
||||
already-created object, you get another three extra slots.
|
||||
|
||||
One small problem, though. It takes a lot of screen space to display all the
|
||||
fields for entering related Choice objects. For that reason, Django offers an
|
||||
alternate way of displaying inline related objects::
|
||||
tabular way of displaying inline related objects; you just need to change
|
||||
the ``ChoiceInline`` declaration to read::
|
||||
|
||||
poll = models.ForeignKey(Poll, edit_inline=models.TABULAR, num_in_admin=3)
|
||||
class ChoiceInline(admin.TabularInline):
|
||||
#...
|
||||
|
||||
With that ``edit_inline=models.TABULAR`` (instead of ``models.STACKED``), the
|
||||
With that ``TabularInline`` (instead of ``StackedInline``), the
|
||||
related objects are displayed in a more compact, table-based format:
|
||||
|
||||
.. image:: http://media.djangoproject.com/img/doc/tutorial-trunk/admin12.png
|
||||
|
@ -285,21 +287,21 @@ Here's what it looks like at this point:
|
|||
:alt: Polls change list page
|
||||
:target: http://media.djangoproject.com/img/doc/tutorial-trunk/admin04.png
|
||||
|
||||
By default, Django displays the ``str()`` of each object. But sometimes it'd
|
||||
be more helpful if we could display individual fields. To do that, use the
|
||||
``list_display`` option, which is a tuple of field names to display, as columns,
|
||||
on the change list page for the object::
|
||||
By default, Django displays the ``str()`` of each object. But sometimes it'd be
|
||||
more helpful if we could display individual fields. To do that, use the
|
||||
``list_display`` admin option, which is a tuple of field names to display, as
|
||||
columns, on the change list page for the object::
|
||||
|
||||
class Poll(models.Model):
|
||||
class PollAdmin(admin.ModelAdmin):
|
||||
# ...
|
||||
class Admin:
|
||||
# ...
|
||||
list_display = ('question', 'pub_date')
|
||||
list_display = ('question', 'pub_date')
|
||||
|
||||
Just for good measure, let's also include the ``was_published_today`` custom
|
||||
method from Tutorial 1::
|
||||
|
||||
list_display = ('question', 'pub_date', 'was_published_today')
|
||||
class PollAdmin(admin.ModelAdmin):
|
||||
# ...
|
||||
list_display = ('question', 'pub_date', 'was_published_today')
|
||||
|
||||
Now the poll change list page looks like this:
|
||||
|
||||
|
@ -318,9 +320,8 @@ method a ``short_description`` attribute::
|
|||
return self.pub_date.date() == datetime.date.today()
|
||||
was_published_today.short_description = 'Published today?'
|
||||
|
||||
|
||||
Let's add another improvement to the Poll change list page: Filters. Add the
|
||||
following line to ``Poll.Admin``::
|
||||
following line to ``PollAdmin``::
|
||||
|
||||
list_filter = ['pub_date']
|
||||
|
||||
|
|
|
@ -5,12 +5,11 @@ This example exists purely to point out errors in models.
|
|||
"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
model_errors = ""
|
||||
class FieldErrors(models.Model):
|
||||
charfield = models.CharField()
|
||||
decimalfield = models.DecimalField()
|
||||
filefield = models.FileField()
|
||||
prepopulate = models.CharField(max_length=10, prepopulate_from='bad')
|
||||
choices = models.CharField(max_length=10, choices='bad')
|
||||
choices2 = models.CharField(max_length=10, choices=[(1,2,3),(1,2,3)])
|
||||
index = models.CharField(max_length=10, db_index='bad')
|
||||
|
@ -116,7 +115,6 @@ model_errors = """invalid_models.fielderrors: "charfield": CharFields require a
|
|||
invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
|
||||
invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute.
|
||||
invalid_models.fielderrors: "filefield": FileFields require an "upload_to" attribute.
|
||||
invalid_models.fielderrors: "prepopulate": prepopulate_from should be a list or tuple.
|
||||
invalid_models.fielderrors: "choices": "choices" should be iterable (e.g., a tuple or list).
|
||||
invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples.
|
||||
invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples.
|
||||
|
|
|
@ -992,4 +992,22 @@ True
|
|||
u'...test3.png'
|
||||
>>> instance.delete()
|
||||
|
||||
# Media on a ModelForm ########################################################
|
||||
|
||||
# Similar to a regular Form class you can define custom media to be used on
|
||||
# the ModelForm.
|
||||
|
||||
>>> class ModelFormWithMedia(ModelForm):
|
||||
... class Media:
|
||||
... js = ('/some/form/javascript',)
|
||||
... css = {
|
||||
... 'all': ('/some/form/css',)
|
||||
... }
|
||||
... class Meta:
|
||||
... model = PhoneNumber
|
||||
>>> f = ModelFormWithMedia()
|
||||
>>> print f.media
|
||||
<link href="/some/form/css" type="text/css" media="all" rel="stylesheet" />
|
||||
<script type="text/javascript" src="/some/form/javascript"></script>
|
||||
|
||||
"""}
|
||||
|
|
|
@ -0,0 +1,324 @@
|
|||
from django.db import models
|
||||
|
||||
class Author(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class Book(models.Model):
|
||||
author = models.ForeignKey(Author)
|
||||
title = models.CharField(max_length=100)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.title
|
||||
|
||||
class AuthorMeeting(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
authors = models.ManyToManyField(Author)
|
||||
created = models.DateField(editable=False)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
__test__ = {'API_TESTS': """
|
||||
|
||||
>>> from datetime import date
|
||||
|
||||
>>> from django.newforms.models import modelformset_factory
|
||||
|
||||
>>> qs = Author.objects.all()
|
||||
>>> AuthorFormSet = modelformset_factory(Author, extra=3)
|
||||
|
||||
>>> formset = AuthorFormSet(queryset=qs)
|
||||
>>> for form in formset.forms:
|
||||
... print form.as_p()
|
||||
<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /><input type="hidden" name="form-0-id" id="id_form-0-id" /></p>
|
||||
<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" maxlength="100" /><input type="hidden" name="form-1-id" id="id_form-1-id" /></p>
|
||||
<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /><input type="hidden" name="form-2-id" id="id_form-2-id" /></p>
|
||||
|
||||
>>> data = {
|
||||
... 'form-TOTAL_FORMS': '3', # the number of forms rendered
|
||||
... 'form-INITIAL_FORMS': '0', # the number of forms with initial data
|
||||
... 'form-MAX_FORMS': '0', # the max number of forms
|
||||
... 'form-0-name': 'Charles Baudelaire',
|
||||
... 'form-1-name': 'Arthur Rimbaud',
|
||||
... 'form-2-name': '',
|
||||
... }
|
||||
|
||||
>>> formset = AuthorFormSet(data=data, queryset=qs)
|
||||
>>> formset.is_valid()
|
||||
True
|
||||
|
||||
>>> formset.save()
|
||||
[<Author: Charles Baudelaire>, <Author: Arthur Rimbaud>]
|
||||
|
||||
>>> for author in Author.objects.order_by('name'):
|
||||
... print author.name
|
||||
Arthur Rimbaud
|
||||
Charles Baudelaire
|
||||
|
||||
|
||||
Gah! We forgot Paul Verlaine. Let's create a formset to edit the existing
|
||||
authors with an extra form to add him. We *could* pass in a queryset to
|
||||
restrict the Author objects we edit, but in this case we'll use it to display
|
||||
them in alphabetical order by name.
|
||||
|
||||
>>> qs = Author.objects.order_by('name')
|
||||
>>> AuthorFormSet = modelformset_factory(Author, extra=1, can_delete=False)
|
||||
|
||||
>>> formset = AuthorFormSet(queryset=qs)
|
||||
>>> for form in formset.forms:
|
||||
... print form.as_p()
|
||||
<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></p>
|
||||
<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" value="Charles Baudelaire" maxlength="100" /><input type="hidden" name="form-1-id" value="1" id="id_form-1-id" /></p>
|
||||
<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /><input type="hidden" name="form-2-id" id="id_form-2-id" /></p>
|
||||
|
||||
|
||||
>>> data = {
|
||||
... 'form-TOTAL_FORMS': '3', # the number of forms rendered
|
||||
... 'form-INITIAL_FORMS': '2', # the number of forms with initial data
|
||||
... 'form-MAX_FORMS': '0', # the max number of forms
|
||||
... 'form-0-id': '2',
|
||||
... 'form-0-name': 'Arthur Rimbaud',
|
||||
... 'form-1-id': '1',
|
||||
... 'form-1-name': 'Charles Baudelaire',
|
||||
... 'form-2-name': 'Paul Verlaine',
|
||||
... }
|
||||
|
||||
>>> formset = AuthorFormSet(data=data, queryset=qs)
|
||||
>>> formset.is_valid()
|
||||
True
|
||||
|
||||
# Only changed or new objects are returned from formset.save()
|
||||
>>> formset.save()
|
||||
[<Author: Paul Verlaine>]
|
||||
|
||||
>>> for author in Author.objects.order_by('name'):
|
||||
... print author.name
|
||||
Arthur Rimbaud
|
||||
Charles Baudelaire
|
||||
Paul Verlaine
|
||||
|
||||
|
||||
This probably shouldn't happen, but it will. If an add form was marked for
|
||||
deltetion, make sure we don't save that form.
|
||||
|
||||
>>> qs = Author.objects.order_by('name')
|
||||
>>> AuthorFormSet = modelformset_factory(Author, extra=1, can_delete=True)
|
||||
|
||||
>>> formset = AuthorFormSet(queryset=qs)
|
||||
>>> for form in formset.forms:
|
||||
... print form.as_p()
|
||||
<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /></p>
|
||||
<p><label for="id_form-0-DELETE">Delete:</label> <input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE" /><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></p>
|
||||
<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" value="Charles Baudelaire" maxlength="100" /></p>
|
||||
<p><label for="id_form-1-DELETE">Delete:</label> <input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE" /><input type="hidden" name="form-1-id" value="1" id="id_form-1-id" /></p>
|
||||
<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" value="Paul Verlaine" maxlength="100" /></p>
|
||||
<p><label for="id_form-2-DELETE">Delete:</label> <input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE" /><input type="hidden" name="form-2-id" value="3" id="id_form-2-id" /></p>
|
||||
<p><label for="id_form-3-name">Name:</label> <input id="id_form-3-name" type="text" name="form-3-name" maxlength="100" /></p>
|
||||
<p><label for="id_form-3-DELETE">Delete:</label> <input type="checkbox" name="form-3-DELETE" id="id_form-3-DELETE" /><input type="hidden" name="form-3-id" id="id_form-3-id" /></p>
|
||||
|
||||
>>> data = {
|
||||
... 'form-TOTAL_FORMS': '4', # the number of forms rendered
|
||||
... 'form-INITIAL_FORMS': '3', # the number of forms with initial data
|
||||
... 'form-MAX_FORMS': '0', # the max number of forms
|
||||
... 'form-0-id': '2',
|
||||
... 'form-0-name': 'Arthur Rimbaud',
|
||||
... 'form-1-id': '1',
|
||||
... 'form-1-name': 'Charles Baudelaire',
|
||||
... 'form-2-id': '3',
|
||||
... 'form-2-name': 'Paul Verlaine',
|
||||
... 'form-3-name': 'Walt Whitman',
|
||||
... 'form-3-DELETE': 'on',
|
||||
... }
|
||||
|
||||
>>> formset = AuthorFormSet(data=data, queryset=qs)
|
||||
>>> formset.is_valid()
|
||||
True
|
||||
|
||||
# No objects were changed or saved so nothing will come back.
|
||||
>>> formset.save()
|
||||
[]
|
||||
|
||||
>>> for author in Author.objects.order_by('name'):
|
||||
... print author.name
|
||||
Arthur Rimbaud
|
||||
Charles Baudelaire
|
||||
Paul Verlaine
|
||||
|
||||
Let's edit a record to ensure save only returns that one record.
|
||||
|
||||
>>> data = {
|
||||
... 'form-TOTAL_FORMS': '4', # the number of forms rendered
|
||||
... 'form-INITIAL_FORMS': '3', # the number of forms with initial data
|
||||
... 'form-MAX_FORMS': '0', # the max number of forms
|
||||
... 'form-0-id': '2',
|
||||
... 'form-0-name': 'Walt Whitman',
|
||||
... 'form-1-id': '1',
|
||||
... 'form-1-name': 'Charles Baudelaire',
|
||||
... 'form-2-id': '3',
|
||||
... 'form-2-name': 'Paul Verlaine',
|
||||
... 'form-3-name': '',
|
||||
... 'form-3-DELETE': '',
|
||||
... }
|
||||
|
||||
>>> formset = AuthorFormSet(data=data, queryset=qs)
|
||||
>>> formset.is_valid()
|
||||
True
|
||||
|
||||
# One record has changed.
|
||||
>>> formset.save()
|
||||
[<Author: Walt Whitman>]
|
||||
|
||||
Test the behavior of commit=False and save_m2m
|
||||
|
||||
>>> meeting = AuthorMeeting.objects.create(created=date.today())
|
||||
>>> meeting.authors = Author.objects.all()
|
||||
|
||||
# create an Author instance to add to the meeting.
|
||||
>>> new_author = Author.objects.create(name=u'John Steinbeck')
|
||||
|
||||
>>> AuthorMeetingFormSet = modelformset_factory(AuthorMeeting, extra=1, can_delete=True)
|
||||
>>> data = {
|
||||
... 'form-TOTAL_FORMS': '2', # the number of forms rendered
|
||||
... 'form-INITIAL_FORMS': '1', # the number of forms with initial data
|
||||
... 'form-MAX_FORMS': '0', # the max number of forms
|
||||
... 'form-0-id': '1',
|
||||
... 'form-0-name': '2nd Tuesday of the Week Meeting',
|
||||
... 'form-0-authors': [2, 1, 3, 4],
|
||||
... 'form-1-name': '',
|
||||
... 'form-1-authors': '',
|
||||
... 'form-1-DELETE': '',
|
||||
... }
|
||||
>>> formset = AuthorMeetingFormSet(data=data, queryset=AuthorMeeting.objects.all())
|
||||
>>> formset.is_valid()
|
||||
True
|
||||
>>> instances = formset.save(commit=False)
|
||||
>>> for instance in instances:
|
||||
... instance.created = date.today()
|
||||
... instance.save()
|
||||
>>> formset.save_m2m()
|
||||
>>> instances[0].authors.all()
|
||||
[<Author: Charles Baudelaire>, <Author: Walt Whitman>, <Author: Paul Verlaine>, <Author: John Steinbeck>]
|
||||
|
||||
# delete the author we created to allow later tests to continue working.
|
||||
>>> new_author.delete()
|
||||
|
||||
Test the behavior of max_num with model formsets. It should properly limit
|
||||
the queryset to reduce the amount of objects being pulled in when not being
|
||||
used.
|
||||
|
||||
>>> qs = Author.objects.order_by('name')
|
||||
|
||||
>>> AuthorFormSet = modelformset_factory(Author, max_num=2)
|
||||
>>> formset = AuthorFormSet(queryset=qs)
|
||||
>>> formset.initial
|
||||
[{'id': 1, 'name': u'Charles Baudelaire'}, {'id': 3, 'name': u'Paul Verlaine'}]
|
||||
|
||||
>>> AuthorFormSet = modelformset_factory(Author, max_num=3)
|
||||
>>> formset = AuthorFormSet(queryset=qs)
|
||||
>>> formset.initial
|
||||
[{'id': 1, 'name': u'Charles Baudelaire'}, {'id': 3, 'name': u'Paul Verlaine'}, {'id': 2, 'name': u'Walt Whitman'}]
|
||||
|
||||
# Inline Formsets ############################################################
|
||||
|
||||
We can also create a formset that is tied to a parent model. This is how the
|
||||
admin system's edit inline functionality works.
|
||||
|
||||
>>> from django.newforms.models import inlineformset_factory
|
||||
|
||||
>>> AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=3)
|
||||
>>> author = Author.objects.get(name='Charles Baudelaire')
|
||||
|
||||
>>> formset = AuthorBooksFormSet(instance=author)
|
||||
>>> for form in formset.forms:
|
||||
... print form.as_p()
|
||||
<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p>
|
||||
<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
|
||||
<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
|
||||
|
||||
>>> data = {
|
||||
... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered
|
||||
... 'book_set-INITIAL_FORMS': '0', # the number of forms with initial data
|
||||
... 'book_set-MAX_FORMS': '0', # the max number of forms
|
||||
... 'book_set-0-title': 'Les Fleurs du Mal',
|
||||
... 'book_set-1-title': '',
|
||||
... 'book_set-2-title': '',
|
||||
... }
|
||||
|
||||
>>> formset = AuthorBooksFormSet(data, instance=author)
|
||||
>>> formset.is_valid()
|
||||
True
|
||||
|
||||
>>> formset.save()
|
||||
[<Book: Les Fleurs du Mal>]
|
||||
|
||||
>>> for book in author.book_set.all():
|
||||
... print book.title
|
||||
Les Fleurs du Mal
|
||||
|
||||
|
||||
Now that we've added a book to Charles Baudelaire, let's try adding another
|
||||
one. This time though, an edit form will be available for every existing
|
||||
book.
|
||||
|
||||
>>> AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2)
|
||||
>>> author = Author.objects.get(name='Charles Baudelaire')
|
||||
|
||||
>>> formset = AuthorBooksFormSet(instance=author)
|
||||
>>> for form in formset.forms:
|
||||
... print form.as_p()
|
||||
<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p>
|
||||
<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
|
||||
<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
|
||||
|
||||
>>> data = {
|
||||
... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered
|
||||
... 'book_set-INITIAL_FORMS': '1', # the number of forms with initial data
|
||||
... 'book_set-MAX_FORMS': '0', # the max number of forms
|
||||
... 'book_set-0-id': '1',
|
||||
... 'book_set-0-title': 'Les Fleurs du Mal',
|
||||
... 'book_set-1-title': 'Le Spleen de Paris',
|
||||
... 'book_set-2-title': '',
|
||||
... }
|
||||
|
||||
>>> formset = AuthorBooksFormSet(data, instance=author)
|
||||
>>> formset.is_valid()
|
||||
True
|
||||
|
||||
>>> formset.save()
|
||||
[<Book: Le Spleen de Paris>]
|
||||
|
||||
As you can see, 'Le Spleen de Paris' is now a book belonging to Charles Baudelaire.
|
||||
|
||||
>>> for book in author.book_set.order_by('title'):
|
||||
... print book.title
|
||||
Le Spleen de Paris
|
||||
Les Fleurs du Mal
|
||||
|
||||
The save_as_new parameter lets you re-associate the data to a new instance.
|
||||
This is used in the admin for save_as functionality.
|
||||
|
||||
>>> data = {
|
||||
... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered
|
||||
... 'book_set-INITIAL_FORMS': '2', # the number of forms with initial data
|
||||
... 'book_set-MAX_FORMS': '0', # the max number of forms
|
||||
... 'book_set-0-id': '1',
|
||||
... 'book_set-0-title': 'Les Fleurs du Mal',
|
||||
... 'book_set-1-id': '2',
|
||||
... 'book_set-1-title': 'Le Spleen de Paris',
|
||||
... 'book_set-2-title': '',
|
||||
... }
|
||||
|
||||
>>> formset = AuthorBooksFormSet(data, instance=Author(), save_as_new=True)
|
||||
>>> formset.is_valid()
|
||||
True
|
||||
|
||||
>>> new_author = Author.objects.create(name='Charles Baudelaire')
|
||||
>>> formset.instance = new_author
|
||||
>>> [book for book in formset.save() if book.author.pk == new_author.pk]
|
||||
[<Book: Les Fleurs du Mal>, <Book: Le Spleen de Paris>]
|
||||
|
||||
"""}
|
|
@ -0,0 +1,46 @@
|
|||
# coding: utf-8
|
||||
from django.db import models
|
||||
|
||||
class Band(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
bio = models.TextField()
|
||||
rank = models.IntegerField()
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
|
||||
__test__ = {'API_TESTS': """
|
||||
|
||||
Let's make sure that ModelAdmin.queryset uses the ordering we define in
|
||||
ModelAdmin rather that ordering defined in the model's inner Meta
|
||||
class.
|
||||
|
||||
>>> from django.contrib.admin.options import ModelAdmin
|
||||
|
||||
>>> b1 = Band(name='Aerosmith', bio='', rank=3)
|
||||
>>> b1.save()
|
||||
>>> b2 = Band(name='Radiohead', bio='', rank=1)
|
||||
>>> b2.save()
|
||||
>>> b3 = Band(name='Van Halen', bio='', rank=2)
|
||||
>>> b3.save()
|
||||
|
||||
The default ordering should be by name, as specified in the inner Meta class.
|
||||
|
||||
>>> ma = ModelAdmin(Band, None)
|
||||
>>> [b.name for b in ma.queryset(None)]
|
||||
[u'Aerosmith', u'Radiohead', u'Van Halen']
|
||||
|
||||
|
||||
Let's use a custom ModelAdmin that changes the ordering, and make sure it
|
||||
actually changes.
|
||||
|
||||
>>> class BandAdmin(ModelAdmin):
|
||||
... ordering = ('rank',) # default ordering is ('name',)
|
||||
...
|
||||
|
||||
>>> ma = BandAdmin(Band, None)
|
||||
>>> [b.name for b in ma.queryset(None)]
|
||||
[u'Radiohead', u'Van Halen', u'Aerosmith']
|
||||
|
||||
"""
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<django-objects version="1.0">
|
||||
<object pk="100" model="auth.user">
|
||||
<field type="CharField" name="username">super</field>
|
||||
<field type="CharField" name="first_name">Super</field>
|
||||
<field type="CharField" name="last_name">User</field>
|
||||
<field type="CharField" name="email">super@example.com</field>
|
||||
<field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
|
||||
<field type="BooleanField" name="is_staff">True</field>
|
||||
<field type="BooleanField" name="is_active">True</field>
|
||||
<field type="BooleanField" name="is_superuser">True</field>
|
||||
<field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
|
||||
<field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
|
||||
<field to="auth.group" name="groups" rel="ManyToManyRel"></field>
|
||||
<field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
|
||||
</object>
|
||||
<object pk="101" model="auth.user">
|
||||
<field type="CharField" name="username">adduser</field>
|
||||
<field type="CharField" name="first_name">Add</field>
|
||||
<field type="CharField" name="last_name">User</field>
|
||||
<field type="CharField" name="email">auser@example.com</field>
|
||||
<field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
|
||||
<field type="BooleanField" name="is_staff">True</field>
|
||||
<field type="BooleanField" name="is_active">True</field>
|
||||
<field type="BooleanField" name="is_superuser">False</field>
|
||||
<field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
|
||||
<field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
|
||||
<field to="auth.group" name="groups" rel="ManyToManyRel"></field>
|
||||
<field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
|
||||
</object>
|
||||
<object pk="102" model="auth.user">
|
||||
<field type="CharField" name="username">changeuser</field>
|
||||
<field type="CharField" name="first_name">Change</field>
|
||||
<field type="CharField" name="last_name">User</field>
|
||||
<field type="CharField" name="email">cuser@example.com</field>
|
||||
<field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
|
||||
<field type="BooleanField" name="is_staff">True</field>
|
||||
<field type="BooleanField" name="is_active">True</field>
|
||||
<field type="BooleanField" name="is_superuser">False</field>
|
||||
<field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
|
||||
<field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
|
||||
<field to="auth.group" name="groups" rel="ManyToManyRel"></field>
|
||||
<field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
|
||||
</object>
|
||||
<object pk="103" model="auth.user">
|
||||
<field type="CharField" name="username">deleteuser</field>
|
||||
<field type="CharField" name="first_name">Delete</field>
|
||||
<field type="CharField" name="last_name">User</field>
|
||||
<field type="CharField" name="email">duser@example.com</field>
|
||||
<field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
|
||||
<field type="BooleanField" name="is_staff">True</field>
|
||||
<field type="BooleanField" name="is_active">True</field>
|
||||
<field type="BooleanField" name="is_superuser">False</field>
|
||||
<field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
|
||||
<field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
|
||||
<field to="auth.group" name="groups" rel="ManyToManyRel"></field>
|
||||
<field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
|
||||
</object>
|
||||
<object pk="104" model="auth.user">
|
||||
<field type="CharField" name="username">joepublic</field>
|
||||
<field type="CharField" name="first_name">Joe</field>
|
||||
<field type="CharField" name="last_name">Public</field>
|
||||
<field type="CharField" name="email">joepublic@example.com</field>
|
||||
<field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
|
||||
<field type="BooleanField" name="is_staff">False</field>
|
||||
<field type="BooleanField" name="is_active">True</field>
|
||||
<field type="BooleanField" name="is_superuser">False</field>
|
||||
<field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
|
||||
<field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
|
||||
<field to="auth.group" name="groups" rel="ManyToManyRel"></field>
|
||||
<field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
|
||||
</object>
|
||||
<object pk="1" model="admin_views.section">
|
||||
<field type="CharField" name="name">Test section</field>
|
||||
</object>
|
||||
<object pk="1" model="admin_views.article">
|
||||
<field type="TextField" name="content"><p>test content</p></field>
|
||||
<field type="DateTimeField" name="date">2008-03-18 11:54:58</field>
|
||||
<field to="admin_views.section" name="section" rel="ManyToOneRel">1</field>
|
||||
</object>
|
||||
</django-objects>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<django-objects version="1.0">
|
||||
<object pk="1" model="admin_views.modelwithstringprimarykey">
|
||||
<field type="CharField" name="id"><![CDATA[abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 -_.!~*'() ;/?:@&=+$, <>#%" {}|\^[]`]]></field>
|
||||
</object>
|
||||
</django-objects>
|
|
@ -0,0 +1,61 @@
|
|||
from django.db import models
|
||||
from django.contrib import admin
|
||||
|
||||
class Section(models.Model):
|
||||
"""
|
||||
A simple section that links to articles, to test linking to related items
|
||||
in admin views.
|
||||
"""
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Article(models.Model):
|
||||
"""
|
||||
A simple article to test admin views. Test backwards compatibility.
|
||||
"""
|
||||
content = models.TextField()
|
||||
date = models.DateTimeField()
|
||||
section = models.ForeignKey(Section)
|
||||
|
||||
class ArticleAdmin(admin.ModelAdmin):
|
||||
list_display = ('content', 'date')
|
||||
list_filter = ('date',)
|
||||
|
||||
def changelist_view(self, request):
|
||||
"Test that extra_context works"
|
||||
return super(ArticleAdmin, self).changelist_view(
|
||||
request, extra_context={
|
||||
'extra_var': 'Hello!'
|
||||
}
|
||||
)
|
||||
|
||||
class CustomArticle(models.Model):
|
||||
content = models.TextField()
|
||||
date = models.DateTimeField()
|
||||
|
||||
class CustomArticleAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Tests various hooks for using custom templates and contexts.
|
||||
"""
|
||||
change_list_template = 'custom_admin/change_list.html'
|
||||
change_form_template = 'custom_admin/change_form.html'
|
||||
object_history_template = 'custom_admin/object_history.html'
|
||||
delete_confirmation_template = 'custom_admin/delete_confirmation.html'
|
||||
|
||||
def changelist_view(self, request):
|
||||
"Test that extra_context works"
|
||||
return super(CustomArticleAdmin, self).changelist_view(
|
||||
request, extra_context={
|
||||
'extra_var': 'Hello!'
|
||||
}
|
||||
)
|
||||
|
||||
class ModelWithStringPrimaryKey(models.Model):
|
||||
id = models.CharField(max_length=255, primary_key=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.id
|
||||
|
||||
admin.site.register(Article, ArticleAdmin)
|
||||
admin.site.register(CustomArticle, CustomArticleAdmin)
|
||||
admin.site.register(Section)
|
||||
admin.site.register(ModelWithStringPrimaryKey)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue