diff options
Diffstat (limited to 'django/contrib')
67 files changed, 2678 insertions, 1515 deletions
diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py index e69de29bb2..56b64faacb 100644 --- a/django/contrib/admin/__init__.py +++ b/django/contrib/admin/__init__.py @@ -0,0 +1,16 @@ +from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL +from django.contrib.admin.options import StackedInline, TabularInline +from django.contrib.admin.sites import AdminSite, site + +def autodiscover(): + """ + Auto-discover INSTALLED_APPS admin.py modules and fail silently when + not present. This forces an import on them to register any admin bits they + may want. + """ + from django.conf import settings + for app in settings.INSTALLED_APPS: + try: + __import__("%s.admin" % app) + except ImportError: + pass diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py index a4f92c986a..d6a4a0bc48 100644 --- a/django/contrib/admin/filterspecs.py +++ b/django/contrib/admin/filterspecs.py @@ -15,7 +15,7 @@ import datetime class FilterSpec(object): filter_specs = [] - def __init__(self, f, request, params, model): + def __init__(self, f, request, params, model, model_admin): self.field = f self.params = params @@ -23,10 +23,10 @@ class FilterSpec(object): cls.filter_specs.append((test, factory)) register = classmethod(register) - def create(cls, f, request, params, model): + def create(cls, f, request, params, model, model_admin): for test, factory in cls.filter_specs: if test(f): - return factory(f, request, params, model) + return factory(f, request, params, model, model_admin) create = classmethod(create) def has_output(self): @@ -52,8 +52,8 @@ class FilterSpec(object): return mark_safe("".join(t)) class RelatedFilterSpec(FilterSpec): - def __init__(self, f, request, params, model): - super(RelatedFilterSpec, self).__init__(f, request, params, model) + def __init__(self, f, request, params, model, model_admin): + super(RelatedFilterSpec, self).__init__(f, request, params, model, model_admin) if isinstance(f, models.ManyToManyField): self.lookup_title = f.rel.to._meta.verbose_name else: @@ -81,8 +81,8 @@ class RelatedFilterSpec(FilterSpec): FilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec) class ChoicesFilterSpec(FilterSpec): - def __init__(self, f, request, params, model): - super(ChoicesFilterSpec, self).__init__(f, request, params, model) + def __init__(self, f, request, params, model, model_admin): + super(ChoicesFilterSpec, self).__init__(f, request, params, model, model_admin) self.lookup_kwarg = '%s__exact' % f.name self.lookup_val = request.GET.get(self.lookup_kwarg, None) @@ -98,8 +98,8 @@ class ChoicesFilterSpec(FilterSpec): FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec) class DateFieldFilterSpec(FilterSpec): - def __init__(self, f, request, params, model): - super(DateFieldFilterSpec, self).__init__(f, request, params, model) + def __init__(self, f, request, params, model, model_admin): + super(DateFieldFilterSpec, self).__init__(f, request, params, model, model_admin) self.field_generic = '%s__' % self.field.name @@ -133,8 +133,8 @@ class DateFieldFilterSpec(FilterSpec): FilterSpec.register(lambda f: isinstance(f, models.DateField), DateFieldFilterSpec) class BooleanFieldFilterSpec(FilterSpec): - def __init__(self, f, request, params, model): - super(BooleanFieldFilterSpec, self).__init__(f, request, params, model) + def __init__(self, f, request, params, model, model_admin): + super(BooleanFieldFilterSpec, self).__init__(f, request, params, model, model_admin) self.lookup_kwarg = '%s__exact' % f.name self.lookup_kwarg2 = '%s__isnull' % f.name self.lookup_val = request.GET.get(self.lookup_kwarg, None) @@ -159,10 +159,10 @@ FilterSpec.register(lambda f: isinstance(f, models.BooleanField) or isinstance(f # if a field is eligible to use the BooleanFieldFilterSpec, that'd be much # more appropriate, and the AllValuesFilterSpec won't get used for it. class AllValuesFilterSpec(FilterSpec): - def __init__(self, f, request, params, model): - super(AllValuesFilterSpec, self).__init__(f, request, params, model) + def __init__(self, f, request, params, model, model_admin): + super(AllValuesFilterSpec, self).__init__(f, request, params, model, model_admin) self.lookup_val = request.GET.get(f.name, None) - self.lookup_choices = model._meta.admin.manager.distinct().order_by(f.name).values(f.name) + self.lookup_choices = model_admin.queryset(request).distinct().order_by(f.name).values(f.name) def title(self): return self.field.verbose_name diff --git a/django/contrib/admin/media/css/forms.css b/django/contrib/admin/media/css/forms.css index 72e57501e9..2a1a0995a0 100644 --- a/django/contrib/admin/media/css/forms.css +++ b/django/contrib/admin/media/css/forms.css @@ -58,3 +58,24 @@ fieldset.monospace textarea { font-family:"Bitstream Vera Sans Mono",Monaco,"Cou .vLargeTextField, .vXMLLargeTextField { width:48em; } .flatpages-flatpage #id_content { height:40.2em; } .module table .vPositiveSmallIntegerField { width:2.2em; } + +/* x unsorted */ +.inline-group {padding:10px; padding-bottom:5px; background:#eee; margin:10px 0;} +.inline-group h3.header {margin:-5px -10px 5px -10px; background:#bbb; color:#fff; padding:2px 5px 3px 5px; font-size:11px} +.inline-related {margin-bottom:15px; position:relative;} +.last-related {margin-bottom:0px;} +.inline-related h2 { margin:0; padding:2px 5px 3px 5px; font-size:11px; text-align:left; font-weight:bold; color:#888; } +.inline-related h2 b {font-weight:normal; color:#aaa;} +.inline-related h2 span.delete {padding-left:20px; position:absolute; top:0px; right:5px;} +.inline-related h2 span.delete label {margin-left:2px; padding-top:1px;} +.inline-related fieldset {background:#fbfbfb;} +.inline-related fieldset.module h2 { margin:0; padding:2px 5px 3px 5px; font-size:11px; text-align:left; font-weight:bold; background:#bcd; color:#fff; } +.inline-related.tabular fieldset.module table {width:100%;} + +.inline-group .tabular tr.has_original td {padding-top:2em;} +.inline-group .tabular tr td.original { padding:2px 0 0 0; width:0; _position:relative; } +.inline-group .tabular th.original {width:0px; padding:0;} +.inline-group .tabular td.original p {position:absolute; left:0; height:1.1em; padding:2px 7px; overflow:hidden; font-size:9px; font-weight:bold; color:#666; _width:700px; } +.inline-group ul.tools {padding:0; margin: 0; list-style:none;} +.inline-group ul.tools li {display:inline; padding:0 5px;} +.inline-group ul.tools a.add {background:url(../img/admin/icon_addlink.gif) 0 50% no-repeat; padding-left:14px;}
\ No newline at end of file diff --git a/django/contrib/admin/media/js/SelectFilter.js b/django/contrib/admin/media/js/SelectFilter.js deleted file mode 100644 index 0501920608..0000000000 --- a/django/contrib/admin/media/js/SelectFilter.js +++ /dev/null @@ -1,81 +0,0 @@ -/* -SelectFilter - Turns a multiple-select box into a filter interface. - -Requires SelectBox.js and addevent.js. -*/ - -function findForm(node) { - // returns the node of the form containing the given node - if (node.tagName.toLowerCase() != 'form') { - return findForm(node.parentNode); - } - return node; -} - -var SelectFilter = { - init: function(field_id) { - var from_box = document.getElementById(field_id); - from_box.id += '_from'; // change its ID - // Create the INPUT input box - var input_box = document.createElement('input'); - input_box.id = field_id + '_input'; - input_box.setAttribute('type', 'text'); - from_box.parentNode.insertBefore(input_box, from_box); - from_box.parentNode.insertBefore(document.createElement('br'), input_box.nextSibling); - // Create the TO box - var to_box = document.createElement('select'); - to_box.id = field_id + '_to'; - to_box.setAttribute('multiple', 'multiple'); - to_box.setAttribute('size', from_box.size); - from_box.parentNode.insertBefore(to_box, from_box.nextSibling); - to_box.setAttribute('name', from_box.getAttribute('name')); - from_box.setAttribute('name', from_box.getAttribute('name') + '_old'); - // Give the filters a CSS hook - from_box.setAttribute('class', 'filtered'); - to_box.setAttribute('class', 'filtered'); - // Set up the JavaScript event handlers for the select box filter interface - addEvent(input_box, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); }); - addEvent(input_box, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); }); - addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); }); - addEvent(from_box, 'focus', function() { input_box.focus(); }); - addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); }); - addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); }); - SelectBox.init(field_id + '_from'); - SelectBox.init(field_id + '_to'); - // Move selected from_box options to to_box - SelectBox.move(field_id + '_from', field_id + '_to'); - }, - filter_key_up: function(event, field_id) { - from = document.getElementById(field_id + '_from'); - // don't submit form if user pressed Enter - if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) { - from.selectedIndex = 0; - SelectBox.move(field_id + '_from', field_id + '_to'); - from.selectedIndex = 0; - return false; - } - var temp = from.selectedIndex; - SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value); - from.selectedIndex = temp; - return true; - }, - filter_key_down: function(event, field_id) { - from = document.getElementById(field_id + '_from'); - // right arrow -- move across - if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) { - var old_index = from.selectedIndex; - SelectBox.move(field_id + '_from', field_id + '_to'); - from.selectedIndex = (old_index == from.length) ? from.length - 1 : old_index; - return false; - } - // down arrow -- wrap around - if ((event.which && event.which == 40) || (event.keyCode && event.keyCode == 40)) { - from.selectedIndex = (from.length == from.selectedIndex + 1) ? 0 : from.selectedIndex + 1; - } - // up arrow -- wrap around - if ((event.which && event.which == 38) || (event.keyCode && event.keyCode == 38)) { - from.selectedIndex = (from.selectedIndex == 0) ? from.length - 1 : from.selectedIndex - 1; - } - return true; - } -} diff --git a/django/contrib/admin/media/js/admin/CollapsedFieldsets.js b/django/contrib/admin/media/js/admin/CollapsedFieldsets.js index c8426db228..d66bec0d97 100644 --- a/django/contrib/admin/media/js/admin/CollapsedFieldsets.js +++ b/django/contrib/admin/media/js/admin/CollapsedFieldsets.js @@ -47,7 +47,7 @@ var CollapsedFieldsets = { // Returns true if any fields in the fieldset have validation errors. var divs = fs.getElementsByTagName('div'); for (var i=0; i<divs.length; i++) { - if (divs[i].className.match(/\berror\b/)) { + if (divs[i].className.match(/\berrors\b/)) { return true; } } diff --git a/django/contrib/admin/media/js/admin/RelatedObjectLookups.js b/django/contrib/admin/media/js/admin/RelatedObjectLookups.js index f6a39ca091..ca578cc28a 100644 --- a/django/contrib/admin/media/js/admin/RelatedObjectLookups.js +++ b/django/contrib/admin/media/js/admin/RelatedObjectLookups.js @@ -1,4 +1,4 @@ -// Handles related-objects functionality: lookup link for raw_id_admin=True +// Handles related-objects functionality: lookup link for raw_id_fields // and Add Another links. function html_unescape(text) { @@ -29,7 +29,7 @@ function showRelatedObjectLookupPopup(triggeringLink) { function dismissRelatedLookupPopup(win, chosenId) { var name = win.name.replace(/___/g, '.'); var elem = document.getElementById(name); - if (elem.className.indexOf('vRawIdAdminField') != -1 && elem.value) { + if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) { elem.value += ',' + chosenId; } else { document.getElementById(name).value = chosenId; diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py index 23c8661336..259884faba 100644 --- a/django/contrib/admin/models.py +++ b/django/contrib/admin/models.py @@ -1,6 +1,7 @@ from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User +from django.contrib.admin.util import quote from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_unicode from django.utils.safestring import mark_safe @@ -50,4 +51,4 @@ class LogEntry(models.Model): Returns the admin URL to edit the object represented by this log entry. This is relative to the Django admin index page. """ - return mark_safe(u"%s/%s/%s/" % (self.content_type.app_label, self.content_type.model, self.object_id)) + return mark_safe(u"%s/%s/%s/" % (self.content_type.app_label, self.content_type.model, quote(self.object_id))) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py new file mode 100644 index 0000000000..3b26f7b262 --- /dev/null +++ b/django/contrib/admin/options.py @@ -0,0 +1,795 @@ +from django import oldforms, template +from django import newforms as forms +from django.newforms.formsets import all_valid +from django.newforms.models import modelform_factory, inlineformset_factory +from django.newforms.models import BaseInlineFormset +from django.contrib.contenttypes.models import ContentType +from django.contrib.admin import widgets +from django.contrib.admin.util import quote, unquote, get_deleted_objects +from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.db import models, transaction +from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404, render_to_response +from django.utils.html import escape +from django.utils.safestring import mark_safe +from django.utils.text import capfirst, get_text_list +from django.utils.translation import ugettext as _ +from django.utils.encoding import force_unicode +import sets + +HORIZONTAL, VERTICAL = 1, 2 +# returns the <ul> class for a given radio_admin field +get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '') + +class IncorrectLookupParameters(Exception): + pass + +def flatten_fieldsets(fieldsets): + """Returns a list of field names from an admin fieldsets structure.""" + field_names = [] + for name, opts in fieldsets: + for field in opts['fields']: + # type checking feels dirty, but it seems like the best way here + if type(field) == tuple: + field_names.extend(field) + else: + field_names.append(field) + return field_names + +class AdminForm(object): + def __init__(self, form, fieldsets, prepopulated_fields): + self.form, self.fieldsets = form, fieldsets + self.prepopulated_fields = [{ + 'field': form[field_name], + 'dependencies': [form[f] for f in dependencies] + } for field_name, dependencies in prepopulated_fields.items()] + + def __iter__(self): + for name, options in self.fieldsets: + yield Fieldset(self.form, name, **options) + + def first_field(self): + for bf in self.form: + return bf + + def _media(self): + media = self.form.media + for fs in self: + media = media + fs.media + return media + media = property(_media) + +class Fieldset(object): + def __init__(self, form, name=None, fields=(), classes=(), description=None): + self.form = form + self.name, self.fields = name, fields + self.classes = u' '.join(classes) + self.description = description + + def _media(self): + from django.conf import settings + if 'collapse' in self.classes: + return forms.Media(js=['%sjs/admin/CollapsedFieldsets.js' % settings.ADMIN_MEDIA_PREFIX]) + return forms.Media() + media = property(_media) + + def __iter__(self): + for field in self.fields: + yield Fieldline(self.form, field) + +class Fieldline(object): + def __init__(self, form, field): + self.form = form # A django.forms.Form instance + if isinstance(field, basestring): + self.fields = [field] + else: + self.fields = field + + def __iter__(self): + for i, field in enumerate(self.fields): + yield AdminField(self.form, field, is_first=(i == 0)) + + def errors(self): + return mark_safe(u'\n'.join([self.form[f].errors.as_ul() for f in self.fields])) + +class AdminField(object): + def __init__(self, form, field, is_first): + self.field = form[field] # A django.forms.BoundField instance + self.is_first = is_first # Whether this field is first on the line + self.is_checkbox = isinstance(self.field.field.widget, forms.CheckboxInput) + + def label_tag(self): + classes = [] + if self.is_checkbox: + classes.append(u'vCheckboxLabel') + contents = escape(self.field.label) + else: + contents = force_unicode(escape(self.field.label)) + u':' + if self.field.field.required: + classes.append(u'required') + if not self.is_first: + classes.append(u'inline') + attrs = classes and {'class': u' '.join(classes)} or {} + return self.field.label_tag(contents=contents, attrs=attrs) + +class BaseModelAdmin(object): + """Functionality common to both ModelAdmin and InlineAdmin.""" + raw_id_fields = () + fields = None + fieldsets = None + form = forms.ModelForm + filter_vertical = () + filter_horizontal = () + radio_fields = {} + prepopulated_fields = {} + + def formfield_for_dbfield(self, db_field, **kwargs): + """ + Hook for specifying the form Field instance for a given database Field + instance. + + If kwargs are given, they're passed to the form Field's constructor. + """ + # For DateTimeFields, use a special field and widget. + if isinstance(db_field, models.DateTimeField): + kwargs['form_class'] = forms.SplitDateTimeField + kwargs['widget'] = widgets.AdminSplitDateTime() + return db_field.formfield(**kwargs) + + # For DateFields, add a custom CSS class. + if isinstance(db_field, models.DateField): + kwargs['widget'] = widgets.AdminDateWidget + return db_field.formfield(**kwargs) + + # For TimeFields, add a custom CSS class. + if isinstance(db_field, models.TimeField): + kwargs['widget'] = widgets.AdminTimeWidget + return db_field.formfield(**kwargs) + + # For FileFields and ImageFields add a link to the current file. + if isinstance(db_field, models.ImageField) or isinstance(db_field, models.FileField): + kwargs['widget'] = widgets.AdminFileWidget + return db_field.formfield(**kwargs) + + # For ForeignKey or ManyToManyFields, use a special widget. + if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)): + if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields: + kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel) + elif isinstance(db_field, models.ForeignKey) and db_field.name in self.radio_fields: + kwargs['widget'] = widgets.AdminRadioSelect(attrs={ + 'class': get_ul_class(self.radio_fields[db_field.name]), + }) + kwargs['empty_label'] = db_field.blank and _('None') or None + else: + if isinstance(db_field, models.ManyToManyField): + if db_field.name in self.raw_id_fields: + kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel) + kwargs['help_text'] = '' + elif db_field.name in (self.filter_vertical + self.filter_horizontal): + kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical)) + # Wrap the widget's render() method with a method that adds + # extra HTML to the end of the rendered output. + formfield = db_field.formfield(**kwargs) + # Don't wrap raw_id fields. Their add function is in the popup window. + if not db_field.name in self.raw_id_fields: + formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site) + return formfield + + if db_field.choices and db_field.name in self.radio_fields: + kwargs['widget'] = widgets.AdminRadioSelect( + choices=db_field.get_choices(include_blank=db_field.blank, + blank_choice=[('', _('None'))]), + attrs={ + 'class': get_ul_class(self.radio_fields[db_field.name]), + } + ) + + # For any other type of field, just call its formfield() method. + return db_field.formfield(**kwargs) + + def _declared_fieldsets(self): + if self.fieldsets: + return self.fieldsets + elif self.fields: + return [(None, {'fields': self.fields})] + return None + declared_fieldsets = property(_declared_fieldsets) + +class ModelAdmin(BaseModelAdmin): + "Encapsulates all admin options and functionality for a given model." + __metaclass__ = forms.MediaDefiningClass + + list_display = ('__str__',) + list_display_links = () + list_filter = () + list_select_related = False + list_per_page = 100 + search_fields = () + date_hierarchy = None + save_as = False + save_on_top = False + ordering = None + inlines = [] + + # Custom templates (designed to be over-ridden in subclasses) + change_form_template = None + change_list_template = None + delete_confirmation_template = None + object_history_template = None + + def __init__(self, model, admin_site): + self.model = model + self.opts = model._meta + self.admin_site = admin_site + self.inline_instances = [] + for inline_class in self.inlines: + inline_instance = inline_class(self.model, self.admin_site) + self.inline_instances.append(inline_instance) + super(ModelAdmin, self).__init__() + + def __call__(self, request, url): + # Check that LogEntry, ContentType and the auth context processor are installed. + from django.conf import settings + if settings.DEBUG: + from django.contrib.admin.models import LogEntry + if not LogEntry._meta.installed: + raise ImproperlyConfigured("Put 'django.contrib.admin' in your INSTALLED_APPS setting in order to use the admin application.") + if not ContentType._meta.installed: + raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in your INSTALLED_APPS setting in order to use the admin application.") + if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS: + raise ImproperlyConfigured("Put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting in order to use the admin application.") + + # Delegate to the appropriate method, based on the URL. + if url is None: + return self.changelist_view(request) + elif url.endswith('add'): + return self.add_view(request) + elif url.endswith('history'): + return self.history_view(request, unquote(url[:-8])) + elif url.endswith('delete'): + return self.delete_view(request, unquote(url[:-7])) + else: + return self.change_view(request, unquote(url)) + + def _media(self): + from django.conf import settings + + js = ['js/core.js', 'js/admin/RelatedObjectLookups.js'] + if self.prepopulated_fields: + js.append('js/urlify.js') + if self.opts.get_ordered_objects(): + js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js']) + if self.filter_vertical or self.filter_horizontal: + js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js']) + + return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js]) + media = property(_media) + + def has_add_permission(self, request): + "Returns True if the given request has permission to add an object." + opts = self.opts + return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission()) + + def has_change_permission(self, request, obj=None): + """ + Returns True if the given request has permission to change the given + Django model instance. + + If `obj` is None, this should return True if the given request has + permission to change *any* object of the given type. + """ + opts = self.opts + return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission()) + + def has_delete_permission(self, request, obj=None): + """ + Returns True if the given request has permission to change the given + Django model instance. + + If `obj` is None, this should return True if the given request has + permission to delete *any* object of the given type. + """ + opts = self.opts + return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission()) + + def queryset(self, request): + """ + Returns a QuerySet of all model instances that can be edited by the + admin site. This is used by changelist_view. + """ + qs = self.model._default_manager.get_query_set() + # TODO: this should be handled by some parameter to the ChangeList. + ordering = self.ordering or () # otherwise we might try to *None, which is bad ;) + if ordering: + qs = qs.order_by(*ordering) + return qs + + def get_fieldsets(self, request, obj=None): + "Hook for specifying fieldsets for the add form." + if self.declared_fieldsets: + return self.declared_fieldsets + form = self.get_form(request) + return [(None, {'fields': form.base_fields.keys()})] + + def get_form(self, request, obj=None): + """ + Returns a Form class for use in the admin add view. This is used by + add_view and change_view. + """ + if self.declared_fieldsets: + fields = flatten_fieldsets(self.declared_fieldsets) + else: + fields = None + return modelform_factory(self.model, form=self.form, fields=fields, formfield_callback=self.formfield_for_dbfield) + + def get_formsets(self, request, obj=None): + for inline in self.inline_instances: + yield inline.get_formset(request, obj) + + def save_add(self, request, form, formsets, post_url_continue): + """ + Saves the object in the "add" stage and returns an HttpResponseRedirect. + + `form` is a bound Form instance that's verified to be valid. + """ + from django.contrib.admin.models import LogEntry, ADDITION + opts = self.model._meta + new_object = form.save(commit=True) + + if formsets: + for formset in formsets: + # HACK: it seems like the parent obejct should be passed into + # a method of something, not just set as an attribute + formset.instance = new_object + formset.save() + + pk_value = new_object._get_pk_val() + LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(self.model).id, pk_value, force_unicode(new_object), ADDITION) + msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': opts.verbose_name, 'obj': new_object} + # Here, we distinguish between different save types by checking for + # the presence of keys in request.POST. + if request.POST.has_key("_continue"): + request.user.message_set.create(message=msg + ' ' + _("You may edit it again below.")) + if request.POST.has_key("_popup"): + post_url_continue += "?_popup=1" + return HttpResponseRedirect(post_url_continue % pk_value) + + if request.POST.has_key("_popup"): + return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \ + # escape() calls force_unicode. + (escape(pk_value), escape(new_object))) + elif request.POST.has_key("_addanother"): + request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % opts.verbose_name)) + return HttpResponseRedirect(request.path) + else: + request.user.message_set.create(message=msg) + # Figure out where to redirect. If the user has change permission, + # redirect to the change-list page for this object. Otherwise, + # redirect to the admin index. + if self.has_change_permission(request, None): + post_url = '../' + else: + post_url = '../../../' + return HttpResponseRedirect(post_url) + save_add = transaction.commit_on_success(save_add) + + def save_change(self, request, form, formsets=None): + """ + Saves the object in the "change" stage and returns an HttpResponseRedirect. + + `form` is a bound Form instance that's verified to be valid. + + `formsets` is a sequence of InlineFormSet instances that are verified to be valid. + """ + from django.contrib.admin.models import LogEntry, CHANGE + opts = self.model._meta + new_object = form.save(commit=True) + pk_value = new_object._get_pk_val() + + if formsets: + for formset in formsets: + formset.save() + + # Construct the change message. + change_message = [] + if form.changed_data: + change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and'))) + + if formsets: + for formset in formsets: + for added_object in formset.new_objects: + change_message.append(_('Added %(name)s "%(object)s".') + % {'name': added_object._meta.verbose_name, + 'object': added_object}) + for changed_object, changed_fields in formset.changed_objects: + change_message.append(_('Changed %(list)s for %(name)s "%(object)s".') + % {'list': get_text_list(changed_fields, _('and')), + 'name': changed_object._meta.verbose_name, + 'object': changed_object}) + for deleted_object in formset.deleted_objects: + change_message.append(_('Deleted %(name)s "%(object)s".') + % {'name': deleted_object._meta.verbose_name, + 'object': deleted_object}) + change_message = ' '.join(change_message) + if not change_message: + change_message = _('No fields changed.') + LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(self.model).id, pk_value, force_unicode(new_object), CHANGE, change_message) + + msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': opts.verbose_name, 'obj': new_object} + if request.POST.has_key("_continue"): + request.user.message_set.create(message=msg + ' ' + _("You may edit it again below.")) + if request.REQUEST.has_key('_popup'): + return HttpResponseRedirect(request.path + "?_popup=1") + else: + return HttpResponseRedirect(request.path) + elif request.POST.has_key("_saveasnew"): + request.user.message_set.create(message=_('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': opts.verbose_name, 'obj': new_object}) + return HttpResponseRedirect("../%s/" % pk_value) + elif request.POST.has_key("_addanother"): + request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % opts.verbose_name)) + return HttpResponseRedirect("../add/") + else: + request.user.message_set.create(message=msg) + return HttpResponseRedirect("../") + save_change = transaction.commit_on_success(save_change) + + def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): + opts = self.model._meta + app_label = opts.app_label + ordered_objects = opts.get_ordered_objects() + context.update({ + 'add': add, + 'change': change, + 'has_add_permission': self.has_add_permission(request), + 'has_change_permission': self.has_change_permission(request, obj), + 'has_delete_permission': self.has_delete_permission(request, obj), + 'has_file_field': True, # FIXME - this should check if form or formsets have a FileField, + 'has_absolute_url': hasattr(self.model, 'get_absolute_url'), + 'ordered_objects': ordered_objects, + 'form_url': mark_safe(form_url), + 'opts': opts, + 'content_type_id': ContentType.objects.get_for_model(self.model).id, + 'save_as': self.save_as, + 'save_on_top': self.save_on_top, + 'root_path': self.admin_site.root_path, + }) + return render_to_response(self.change_form_template or [ + "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()), + "admin/%s/change_form.html" % app_label, + "admin/change_form.html" + ], context, context_instance=template.RequestContext(request)) + + def add_view(self, request, form_url='', extra_context=None): + "The 'add' admin view for this model." + model = self.model + opts = model._meta + app_label = opts.app_label + + if not self.has_add_permission(request): + raise PermissionDenied + + if self.has_change_permission(request, None): + # redirect to list view + post_url = '../' + else: + # Object list will give 'Permission Denied', so go back to admin home + post_url = '../../../' + + ModelForm = self.get_form(request) + inline_formsets = [] + obj = self.model() + if request.method == 'POST': + form = ModelForm(request.POST, request.FILES) + for FormSet in self.get_formsets(request): + inline_formset = FormSet(data=request.POST, files=request.FILES, + instance=obj, save_as_new=request.POST.has_key("_saveasnew")) + inline_formsets.append(inline_formset) + if all_valid(inline_formsets) and form.is_valid(): + return self.save_add(request, form, inline_formsets, '../%s/') + else: + form = ModelForm(initial=dict(request.GET.items())) + for FormSet in self.get_formsets(request): + inline_formset = FormSet(instance=obj) + inline_formsets.append(inline_formset) + + adminForm = AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields) + media = self.media + adminForm.media + for fs in inline_formsets: + media = media + fs.media + + inline_admin_formsets = [] + for inline, formset in zip(self.inline_instances, inline_formsets): + fieldsets = list(inline.get_fieldsets(request)) + inline_admin_formset = InlineAdminFormSet(inline, formset, fieldsets) + inline_admin_formsets.append(inline_admin_formset) + + context = { + 'title': _('Add %s') % opts.verbose_name, + 'adminform': adminForm, + 'is_popup': request.REQUEST.has_key('_popup'), + 'show_delete': False, + 'media': mark_safe(media), + 'inline_admin_formsets': inline_admin_formsets, + 'errors': AdminErrorList(form, inline_formsets), + 'root_path': self.admin_site.root_path, + } + context.update(extra_context or {}) + return self.render_change_form(request, context, add=True) + + def change_view(self, request, object_id, extra_context=None): + "The 'change' admin view for this model." + model = self.model + opts = model._meta + app_label = opts.app_label + + try: + obj = model._default_manager.get(pk=object_id) + except model.DoesNotExist: + # Don't raise Http404 just yet, because we haven't checked + # permissions yet. We don't want an unauthenticated user to be able + # to determine whether a given object exists. + obj = None + + if not self.has_change_permission(request, obj): + raise PermissionDenied + + if obj is None: + raise Http404('%s object with primary key %r does not exist.' % (opts.verbose_name, escape(object_id))) + + if request.POST and request.POST.has_key("_saveasnew"): + return self.add_view(request, form_url='../../add/') + + ModelForm = self.get_form(request, obj) + inline_formsets = [] + if request.method == 'POST': + form = ModelForm(request.POST, request.FILES, instance=obj) + for FormSet in self.get_formsets(request, obj): + inline_formset = FormSet(request.POST, request.FILES, instance=obj) + inline_formsets.append(inline_formset) + + if all_valid(inline_formsets) and form.is_valid(): + return self.save_change(request, form, inline_formsets) + else: + form = ModelForm(instance=obj) + for FormSet in self.get_formsets(request, obj): + inline_formset = FormSet(instance=obj) + inline_formsets.append(inline_formset) + + adminForm = AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields) + media = self.media + adminForm.media + for fs in inline_formsets: + media = media + fs.media + + inline_admin_formsets = [] + for inline, formset in zip(self.inline_instances, inline_formsets): + fieldsets = list(inline.get_fieldsets(request, obj)) + inline_admin_formset = InlineAdminFormSet(inline, formset, fieldsets) + inline_admin_formsets.append(inline_admin_formset) + + context = { + 'title': _('Change %s') % opts.verbose_name, + 'adminform': adminForm, + 'object_id': object_id, + 'original': obj, + 'is_popup': request.REQUEST.has_key('_popup'), + 'media': mark_safe(media), + 'inline_admin_formsets': inline_admin_formsets, + 'errors': AdminErrorList(form, inline_formsets), + 'root_path': self.admin_site.root_path, + } + context.update(extra_context or {}) + return self.render_change_form(request, context, change=True, obj=obj) + + def changelist_view(self, request, extra_context=None): + "The 'change list' admin view for this model." + from django.contrib.admin.views.main import ChangeList, ERROR_FLAG + opts = self.model._meta + app_label = opts.app_label + if not self.has_change_permission(request, None): + raise PermissionDenied + try: + cl = ChangeList(request, self.model, self.list_display, self.list_display_links, self.list_filter, + self.date_hierarchy, self.search_fields, self.list_select_related, self.list_per_page, self) + except IncorrectLookupParameters: + # Wacky lookup parameters were given, so redirect to the main + # changelist page, without parameters, and pass an 'invalid=1' + # parameter via the query string. If wacky parameters were given and + # the 'invalid=1' parameter was already in the query string, something + # is screwed up with the database, so display an error page. + if ERROR_FLAG in request.GET.keys(): + return render_to_response('admin/invalid_setup.html', {'title': _('Database error')}) + return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1') + + context = { + 'title': cl.title, + 'is_popup': cl.is_popup, + 'cl': cl, + 'has_add_permission': self.has_add_permission(request), + 'root_path': self.admin_site.root_path, + } + context.update(extra_context or {}) + return render_to_response(self.change_list_template or [ + 'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()), + 'admin/%s/change_list.html' % app_label, + 'admin/change_list.html' + ], context, context_instance=template.RequestContext(request)) + + def delete_view(self, request, object_id, extra_context=None): + "The 'delete' admin view for this model." + from django.contrib.admin.models import LogEntry, DELETION + opts = self.model._meta + app_label = opts.app_label + + try: + obj = self.model._default_manager.get(pk=object_id) + except self.model.DoesNotExist: + # Don't raise Http404 just yet, because we haven't checked + # permissions yet. We don't want an unauthenticated user to be able + # to determine whether a given object exists. + obj = None + + if not self.has_delete_permission(request, obj): + raise PermissionDenied + + if obj is None: + raise Http404('%s object with primary key %r does not exist.' % (opts.verbose_name, escape(object_id))) + + # Populate deleted_objects, a data structure of all related objects that + # will also be deleted. + deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), quote(object_id), escape(obj))), []] + perms_needed = sets.Set() + get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site) + + if request.POST: # The user has already confirmed the deletion. + if perms_needed: + raise PermissionDenied + obj_display = str(obj) + obj.delete() + LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(self.model).id, object_id, obj_display, DELETION) + request.user.message_set.create(message=_('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj_display)}) + if not self.has_change_permission(request, None): + return HttpResponseRedirect("../../../../") + return HttpResponseRedirect("../../") + + context = { + "title": _("Are you sure?"), + "object_name": opts.verbose_name, + "object": obj, + "deleted_objects": deleted_objects, + "perms_lacking": perms_needed, + "opts": opts, + "root_path": self.admin_site.root_path, + } + context.update(extra_context or {}) + return render_to_response(self.delete_confirmation_template or [ + "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()), + "admin/%s/delete_confirmation.html" % app_label, + "admin/delete_confirmation.html" + ], context, context_instance=template.RequestContext(request)) + + def history_view(self, request, object_id, extra_context=None): + "The 'history' admin view for this model." + from django.contrib.admin.models import LogEntry + model = self.model + opts = model._meta + action_list = LogEntry.objects.filter( + object_id = object_id, + content_type__id__exact = ContentType.objects.get_for_model(model).id + ).select_related().order_by('action_time') + # If no history was found, see whether this object even exists. + obj = get_object_or_404(model, pk=object_id) + context = { + 'title': _('Change history: %s') % force_unicode(obj), + 'action_list': action_list, + 'module_name': capfirst(opts.verbose_name_plural), + 'object': obj, + 'root_path': self.admin_site.root_path, + } + context.update(extra_context or {}) + return render_to_response(self.object_history_template or [ + "admin/%s/%s/object_history.html" % (opts.app_label, opts.object_name.lower()), + "admin/%s/object_history.html" % opts.app_label, + "admin/object_history.html" + ], context, context_instance=template.RequestContext(request)) + +class InlineModelAdmin(BaseModelAdmin): + """ + Options for inline editing of ``model`` instances. + + Provide ``name`` to specify the attribute name of the ``ForeignKey`` from + ``model`` to its parent. This is required if ``model`` has more than one + ``ForeignKey`` to its parent. + """ + model = None + fk_name = None + formset = BaseInlineFormset + extra = 3 + max_num = 0 + template = None + verbose_name = None + verbose_name_plural = None + + def __init__(self, parent_model, admin_site): + self.admin_site = admin_site + self.parent_model = parent_model + self.opts = self.model._meta + super(InlineModelAdmin, self).__init__() + if self.verbose_name is None: + self.verbose_name = self.model._meta.verbose_name + if self.verbose_name_plural is None: + self.verbose_name_plural = self.model._meta.verbose_name_plural + + def get_formset(self, request, obj=None): + """Returns a BaseInlineFormSet class for use in admin add/change views.""" + if self.declared_fieldsets: + fields = flatten_fieldsets(self.declared_fieldsets) + else: + fields = None + return inlineformset_factory(self.parent_model, self.model, + form=self.form, formset=self.formset, fk_name=self.fk_name, + fields=fields, formfield_callback=self.formfield_for_dbfield, + extra=self.extra, max_num=self.max_num) + + def get_fieldsets(self, request, obj=None): + if self.declared_fieldsets: + return self.declared_fieldsets + form = self.get_formset(request).form + return [(None, {'fields': form.base_fields.keys()})] + +class StackedInline(InlineModelAdmin): + template = 'admin/edit_inline/stacked.html' + +class TabularInline(InlineModelAdmin): + template = 'admin/edit_inline/tabular.html' + +class InlineAdminFormSet(object): + """ + A wrapper around an inline formset for use in the admin system. + """ + def __init__(self, inline, formset, fieldsets): + self.opts = inline + self.formset = formset + self.fieldsets = fieldsets + + def __iter__(self): + for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()): + yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original) + for form in self.formset.extra_forms: + yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None) + + def fields(self): + for field_name in flatten_fieldsets(self.fieldsets): + yield self.formset.form.base_fields[field_name] + +class InlineAdminForm(AdminForm): + """ + A wrapper around an inline form for use in the admin system. + """ + def __init__(self, formset, form, fieldsets, prepopulated_fields, original): + self.formset = formset + self.original = original + self.show_url = original and hasattr(original, 'get_absolute_url') + super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields) + + def pk_field(self): + return AdminField(self.form, self.formset._pk_field_name, False) + + def deletion_field(self): + from django.newforms.formsets import DELETION_FIELD_NAME + return AdminField(self.form, DELETION_FIELD_NAME, False) + + def ordering_field(self): + from django.newforms.formsets import ORDERING_FIELD_NAME + return AdminField(self.form, ORDERING_FIELD_NAME, False) + +class AdminErrorList(forms.util.ErrorList): + """ + Stores all errors for the form/formsets in an add/change stage view. + """ + def __init__(self, form, inline_formsets): + if form.is_bound: + self.extend(form.errors.values()) + for inline_formset in inline_formsets: + self.extend(inline_formset.non_form_errors()) + for errors_in_inline_form in inline_formset.errors: + self.extend(errors_in_inline_form.values()) diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py new file mode 100644 index 0000000000..bb4dc58ece --- /dev/null +++ b/django/contrib/admin/sites.py @@ -0,0 +1,349 @@ +from django import http, template +from django.contrib.admin import ModelAdmin +from django.contrib.auth import authenticate, login +from django.db.models.base import ModelBase +from django.shortcuts import render_to_response +from django.utils.safestring import mark_safe +from django.utils.text import capfirst +from django.utils.translation import ugettext_lazy, ugettext as _ +from django.views.decorators.cache import never_cache +from django.conf import settings +import base64 +import cPickle as pickle +import datetime +import md5 +import re + +ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.") +LOGIN_FORM_KEY = 'this_is_the_login_form' + +USER_CHANGE_PASSWORD_URL_RE = re.compile('auth/user/(\d+)/password') + +class AlreadyRegistered(Exception): + pass + +class NotRegistered(Exception): + pass + +def _encode_post_data(post_data): + from django.conf import settings + pickled = pickle.dumps(post_data) + pickled_md5 = md5.new(pickled + settings.SECRET_KEY).hexdigest() + return base64.encodestring(pickled + pickled_md5) + +def _decode_post_data(encoded_data): + from django.conf import settings + encoded_data = base64.decodestring(encoded_data) + pickled, tamper_check = encoded_data[:-32], encoded_data[-32:] + if md5.new(pickled + settings.SECRET_KEY).hexdigest() != tamper_check: + from django.core.exceptions import SuspiciousOperation + raise SuspiciousOperation, "User may have tampered with session cookie." + return pickle.loads(pickled) + +class AdminSite(object): + """ + An AdminSite object encapsulates an instance of the Django admin application, ready + to be hooked in to your URLConf. Models are registered with the AdminSite using the + register() method, and the root() method can then be used as a Django view function + that presents a full admin interface for the collection of registered models. + """ + + index_template = None + login_template = None + + def __init__(self): + self._registry = {} # model_class class -> admin_class instance + + def register(self, model_or_iterable, admin_class=None, **options): + """ + Registers the given model(s) with the given admin class. + + The model(s) should be Model classes, not instances. + + If an admin class isn't given, it will use ModelAdmin (the default + admin options). If keyword arguments are given -- e.g., list_display -- + they'll be applied as options to the admin class. + + If a model is already registered, this will raise AlreadyRegistered. + """ + do_validate = admin_class and settings.DEBUG + if do_validate: + # don't import the humongous validation code unless required + from django.contrib.admin.validation import validate + admin_class = admin_class or ModelAdmin + # TODO: Handle options + if isinstance(model_or_iterable, ModelBase): + model_or_iterable = [model_or_iterable] + for model in model_or_iterable: + if model in self._registry: + raise AlreadyRegistered('The model %s is already registered' % model.__name__) + if do_validate: + validate(admin_class, model) + self._registry[model] = admin_class(model, self) + + def unregister(self, model_or_iterable): + """ + Unregisters the given model(s). + + If a model isn't already registered, this will raise NotRegistered. + """ + if isinstance(model_or_iterable, ModelBase): + model_or_iterable = [model_or_iterable] + for model in model_or_iterable: + if model not in self._registry: + raise NotRegistered('The model %s is not registered' % model.__name__) + del self._registry[model] + + def has_permission(self, request): + """ + Returns True if the given HttpRequest has permission to view + *at least one* page in the admin site. + """ + return request.user.is_authenticated() and request.user.is_staff + + def root(self, request, url): + """ + Handles main URL routing for the admin app. + + `url` is the remainder of the URL -- e.g. 'comments/comment/'. + """ + if request.method == 'GET' and not request.path.endswith('/'): + return http.HttpResponseRedirect(request.path + '/') + + # Figure out the admin base URL path and stash it for later use + self.root_path = re.sub(re.escape(url) + '$', '', request.path) + + url = url.rstrip('/') # Trim trailing slash, if it exists. + + # The 'logout' view doesn't require that the person is logged in. + if url == 'logout': + return self.logout(request) + + # Check permission to continue or display login form. + if not self.has_permission(request): + return self.login(request) + + if url == '': + return self.index(request) + elif url == 'password_change': + return self.password_change(request) + elif url == 'password_change/done': + return self.password_change_done(request) + elif url == 'jsi18n': + return self.i18n_javascript(request) + # urls starting with 'r/' are for the "show in web" links + elif url.startswith('r/'): + from django.views.defaults import shortcut + return shortcut(request, *url.split('/')[1:]) + else: + match = USER_CHANGE_PASSWORD_URL_RE.match(url) + if match: + return self.user_change_password(request, match.group(1)) + + if '/' in url: + return self.model_page(request, *url.split('/', 2)) + + raise http.Http404('The requested admin page does not exist.') + + def model_page(self, request, app_label, model_name, rest_of_url=None): + """ + Handles the model-specific functionality of the admin site, delegating + to the appropriate ModelAdmin class. + """ + from django.db import models + model = models.get_model(app_label, model_name) + if model is None: + raise http.Http404("App %r, model %r, not found." % (app_label, model_name)) + try: + admin_obj = self._registry[model] + except KeyError: + raise http.Http404("This model exists but has not been registered with the admin site.") + return admin_obj(request, rest_of_url) + model_page = never_cache(model_page) + + def password_change(self, request): + """ + Handles the "change password" task -- both form display and validation. + """ + from django.contrib.auth.views import password_change + return password_change(request) + + def password_change_done(self, request): + """ + Displays the "success" page after a password change. + """ + from django.contrib.auth.views import password_change_done + return password_change_done(request) + + def user_change_password(self, request, id): + """ + Handles the "user change password" task + """ + from django.contrib.auth.views import user_change_password + return user_change_password(request, id) + + def i18n_javascript(self, request): + """ + Displays the i18n JavaScript that the Django admin requires. + + This takes into account the USE_I18N setting. If it's set to False, the + generated JavaScript will be leaner and faster. + """ + from django.conf import settings + if settings.USE_I18N: + from django.views.i18n import javascript_catalog + else: + from django.views.i18n import null_javascript_catalog as javascript_catalog + return javascript_catalog(request, packages='django.conf') + + def logout(self, request): + """ + Logs out the user for the given HttpRequest. + + This should *not* assume the user is already logged in. + """ + from django.contrib.auth.views import logout + return logout(request) + logout = never_cache(logout) + + def login(self, request): + """ + Displays the login form for the given HttpRequest. + """ + from django.contrib.auth.models import User + + # If this isn't already the login page, display it. + if not request.POST.has_key(LOGIN_FORM_KEY): + if request.POST: + message = _("Please log in again, because your session has expired. Don't worry: Your submission has been saved.") + else: + message = "" + return self.display_login_form(request, message) + + # Check that the user accepts cookies. + if not request.session.test_cookie_worked(): + message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.") + return self.display_login_form(request, message) + + # Check the password. + username = request.POST.get('username', None) + password = request.POST.get('password', None) + user = authenticate(username=username, password=password) + if user is None: + message = ERROR_MESSAGE + if u'@' in username: + # Mistakenly entered e-mail address instead of username? Look it up. + try: + user = User.objects.get(email=username) + except (User.DoesNotExist, User.MultipleObjectsReturned): + message = _("Usernames cannot contain the '@' character.") + else: + if user.check_password(password): + message = _("Your e-mail address is not your username." + " Try '%s' instead.") % user.username + else: + message = _("Usernames cannot contain the '@' character.") + return self.display_login_form(request, message) + + # The user data is correct; log in the user in and continue. + else: + if user.is_active and user.is_staff: + login(request, user) + # TODO: set last_login with an event. + user.last_login = datetime.datetime.now() + user.save() + if request.POST.has_key('post_data'): + post_data = _decode_post_data(request.POST['post_data']) + if post_data and not post_data.has_key(LOGIN_FORM_KEY): + # overwrite request.POST with the saved post_data, and continue + request.POST = post_data + request.user = user + return self.root(request, request.path.split(self.root_path)[-1]) + else: + request.session.delete_test_cookie() + return http.HttpResponseRedirect(request.path) + else: + return self.display_login_form(request, ERROR_MESSAGE) + login = never_cache(login) + + def index(self, request, extra_context=None): + """ + Displays the main admin index page, which lists all of the installed + apps that have been registered in this site. + """ + app_dict = {} + user = request.user + for model, model_admin in self._registry.items(): + app_label = model._meta.app_label + has_module_perms = user.has_module_perms(app_label) + + if has_module_perms: + perms = { + 'add': model_admin.has_add_permission(request), + 'change': model_admin.has_change_permission(request), + 'delete': model_admin.has_delete_permission(request), + } + + # Check whether user has any perm for this module. + # If so, add the module to the model_list. + if True in perms.values(): + model_dict = { + 'name': capfirst(model._meta.verbose_name_plural), + 'admin_url': mark_safe('%s/%s/' % (app_label, model.__name__.lower())), + 'perms': perms, + } + if app_label in app_dict: + app_dict[app_label]['models'].append(model_dict) + else: + app_dict[app_label] = { + 'name': app_label.title(), + 'has_module_perms': has_module_perms, + 'models': [model_dict], + } + + # Sort the apps alphabetically. + app_list = app_dict.values() + app_list.sort(lambda x, y: cmp(x['name'], y['name'])) + + # Sort the models alphabetically within each app. + for app in app_list: + app['models'].sort(lambda x, y: cmp(x['name'], y['name'])) + + context = { + 'title': _('Site administration'), + 'app_list': app_list, + 'root_path': self.root_path, + } + context.update(extra_context or {}) + return render_to_response(self.index_template or 'admin/index.html', context, + context_instance=template.RequestContext(request) + ) + index = never_cache(index) + + def display_login_form(self, request, error_message='', extra_context=None): + request.session.set_test_cookie() + if request.POST and request.POST.has_key('post_data'): + # User has failed login BUT has previously saved post data. + post_data = request.POST['post_data'] + elif request.POST: + # User's session must have expired; save their post data. + post_data = _encode_post_data(request.POST) + else: + post_data = _encode_post_data({}) + + context = { + 'title': _('Log in'), + 'app_path': request.path, + 'post_data': post_data, + 'error_message': error_message, + 'root_path': self.root_path, + } + context.update(extra_context or {}) + return render_to_response(self.login_template or 'admin/login.html', context, + context_instance=template.RequestContext(request) + ) + + +# This global object represents the default admin site, for the common case. +# You can instantiate AdminSite in your own code to create a custom admin site. +site = AdminSite() diff --git a/django/contrib/admin/templates/admin/auth/user/add_form.html b/django/contrib/admin/templates/admin/auth/user/add_form.html index 139fa6a75e..65824a6b7d 100644 --- a/django/contrib/admin/templates/admin/auth/user/add_form.html +++ b/django/contrib/admin/templates/admin/auth/user/add_form.html @@ -8,21 +8,26 @@ <fieldset class="module aligned"> <div class="form-row"> - {{ form.username.html_error_list }} + {{ form.username.errors }} + {# TODO: get required class on label_tag #} <label for="id_username" class="required">{% trans 'Username' %}:</label> {{ form.username }} - <p class="help">{{ username_help_text }}</p> + <p class="help">{{ form.username.help_text }}</p> </div> <div class="form-row"> - {{ form.password1.html_error_list }} + {{ form.password1.errors }} + {# TODO: get required class on label_tag #} <label for="id_password1" class="required">{% trans 'Password' %}:</label> {{ form.password1 }} </div> <div class="form-row"> - {{ form.password2.html_error_list }} + {{ form.password2.errors }} + {# TODO: get required class on label_tag #} <label for="id_password2" class="required">{% trans 'Password (again)' %}:</label> {{ form.password2 }} <p class="help">{% trans 'Enter the same password as above, for verification.' %}</p> </div> +<script type="text/javascript">document.getElementById("id_username").focus();</script> + </fieldset> {% endblock %} diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html index 28e342da86..f1c4a8d34a 100644 --- a/django/contrib/admin/templates/admin/auth/user/change_password.html +++ b/django/contrib/admin/templates/admin/auth/user/change_password.html @@ -2,7 +2,6 @@ {% load i18n admin_modify adminmedia %} {% block extrahead %}{{ block.super }} <script type="text/javascript" src="../../../../jsi18n/"></script> -{% for js in javascript_imports %}{% include_admin_script js %}{% endfor %} {% endblock %} {% block stylesheet %}{% admin_media_prefix %}css/forms.css{% endblock %} {% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %} @@ -18,9 +17,9 @@ <form action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% block form_top %}{% endblock %} <div> {% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %} -{% if form.error_dict %} +{% if form.errors %} <p class="errornote"> - {% blocktrans count form.error_dict.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} + {% blocktrans count form.errors.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} </p> {% endif %} @@ -29,12 +28,14 @@ <fieldset class="module aligned"> <div class="form-row"> - {{ form.password1.html_error_list }} + {{ form.password1.errors }} + {# TODO: get required class on label_tag #} <label for="id_password1" class="required">{% trans 'Password' %}:</label> {{ form.password1 }} </div> <div class="form-row"> - {{ form.password2.html_error_list }} + {{ form.password2.errors }} + {# TODO: get required class on label_tag #} <label for="id_password2" class="required">{% trans 'Password (again)' %}:</label> {{ form.password2 }} <p class="help">{% trans 'Enter the same password as above, for verification.' %}</p> </div> @@ -45,7 +46,7 @@ <input type="submit" value="{% trans 'Change password' %}" class="default" /> </div> -<script type="text/javascript">document.getElementById("{{ first_form_field_id }}").focus();</script> +<script type="text/javascript">document.getElementById("id_password1").focus();</script> </div> </form></div> {% endblock %} diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index cdd3561ab9..479e18b2ee 100644 --- a/django/contrib/admin/templates/admin/base.html +++ b/django/contrib/admin/templates/admin/base.html @@ -22,14 +22,7 @@ {% block branding %}{% endblock %} </div> {% if user.is_authenticated and user.is_staff %} - <div id="user-tools"> - {% trans 'Welcome,' %} <strong>{% if user.first_name %}{{ user.first_name|escape }}{% else %}{{ user.username }}{% endif %}</strong>. - {% block userlinks %} - <a href="{% url django.contrib.admin.views.doc.doc_index %}">{% trans 'Documentation' %}</a> - / <a href="{% url django.contrib.auth.views.password_change %}">{% trans 'Change password' %}</a> - / <a href="{% url django.contrib.auth.views.logout %}">{% trans 'Log out' %}</a> - {% endblock %} - </div> + <div id="user-tools">{% trans 'Welcome,' %} <strong>{% if user.first_name %}{{ user.first_name|escape }}{% else %}{{ user.username }}{% endif %}</strong>. {% block userlinks %}<a href="{{ root_path }}doc/">{% trans 'Documentation' %}</a> / <a href="{{ root_path }}password_change/">{% trans 'Change password' %}</a> / <a href="{{ root_path }}logout/">{% trans 'Log out' %}</a>{% endblock %}</div> {% endif %} {% block nav-global %}{% endblock %} </div> diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index d540cf6e79..e8df6b99f5 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -1,12 +1,17 @@ {% extends "admin/base_site.html" %} {% load i18n admin_modify adminmedia %} + {% block extrahead %}{{ block.super }} <script type="text/javascript" src="../../../jsi18n/"></script> -{% for js in javascript_imports %}{% include_admin_script js %}{% endfor %} +{{ media }} {% endblock %} + {% block stylesheet %}{% admin_media_prefix %}css/forms.css{% endblock %} + {% block coltype %}{% if ordered_objects %}colMS{% else %}colM{% endif %}{% endblock %} + {% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %} + {% block breadcrumbs %}{% if not is_popup %} <div class="breadcrumbs"> <a href="../../../">{% trans "Home" %}</a> › @@ -14,6 +19,7 @@ {% if add %}{% trans "Add" %} {{ opts.verbose_name }}{% else %}{{ original|truncatewords:"18" }}{% endif %} </div> {% endif %}{% endblock %} + {% block content %}<div id="content-main"> {% block object-tools %} {% if change %}{% if not is_popup %} @@ -25,45 +31,48 @@ <form {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% block form_top %}{% endblock %} <div> {% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %} -{% if opts.admin.save_on_top %}{% submit_row %}{% endif %} -{% if form.error_dict %} +{% if save_on_top %}{% submit_row %}{% endif %} +{% if errors %} <p class="errornote"> - {% blocktrans count form.error_dict.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} + {% blocktrans count errors.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} </p> + <ul class="errorlist">{% for error in adminform.form.non_field_errors %}<li>{{ error }}</li>{% endfor %}</ul> {% endif %} -{% for bound_field_set in bound_field_sets %} - <fieldset class="module aligned {{ bound_field_set.classes }}"> - {% if bound_field_set.name %}<h2>{{ bound_field_set.name }}</h2>{% endif %} - {% if bound_field_set.description %}<div class="description">{{ bound_field_set.description|safe }}</div>{% endif %} - {% for bound_field_line in bound_field_set %} - {% admin_field_line bound_field_line %} - {% for bound_field in bound_field_line %} - {% filter_interface_script_maybe bound_field %} - {% endfor %} - {% endfor %} - </fieldset> + +{% for fieldset in adminform %} + {% include "admin/includes/fieldset.html" %} {% endfor %} + {% block after_field_sets %}{% endblock %} -{% if change %} - {% if ordered_objects %} - <fieldset class="module"><h2>{% trans "Ordering" %}</h2> - <div class="form-row{% if form.order_.errors %} error{% endif %} "> - {% if form.order_.errors %}{{ form.order_.html_error_list }}{% endif %} - <p><label for="id_order_">{% trans "Order:" %}</label> {{ form.order_ }}</p> - </div></fieldset> - {% endif %} -{% endif %} -{% for related_object in inline_related_objects %}{% edit_inline related_object %}{% endfor %} + +{% for inline_admin_formset in inline_admin_formsets %} + {% include inline_admin_formset.opts.template %} +{% endfor %} + {% block after_related_objects %}{% endblock %} + {% submit_row %} + {% if add %} - <script type="text/javascript">document.getElementById("{{ first_form_field_id }}").focus();</script> + <script type="text/javascript">document.getElementById("{{ adminform.first_field.auto_id }}").focus();</script> {% endif %} -{% if auto_populated_fields %} - <script type="text/javascript"> - {% auto_populated_field_script auto_populated_fields change %} - </script> + +{# JavaScript for prepopulated fields #} + +{% if add %} +<script type="text/javascript"> +{% for field in adminform.prepopulated_fields %} + document.getElementById("{{ field.field.auto_id }}").onchange = function() { this._changed = true; }; + {% for dependency in field.dependencies %} + document.getElementById("{{ dependency.auto_id }}").onkeyup = function() { + var e = document.getElementById("{{ field.field.auto_id }}"); + if (!e._changed) { e.value = URLify({% for innerdep in field.dependencies %}document.getElementById("{{ innerdep.auto_id }}").value{% if not forloop.last %} + ' ' + {% endif %}{% endfor %}, {{ field.field.field.max_length }}); } + } + {% endfor %} +{% endfor %} +</script> {% endif %} + </div> </form></div> {% endblock %} diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index 611c5ff8fc..24286a51a7 100644 --- a/django/contrib/admin/templates/admin/change_list.html +++ b/django/contrib/admin/templates/admin/change_list.html @@ -1,9 +1,14 @@ {% extends "admin/base_site.html" %} {% load adminmedia admin_list i18n %} + {% block stylesheet %}{% admin_media_prefix %}css/changelists.css{% endblock %} + {% block bodyclass %}change-list{% endblock %} + {% if not is_popup %}{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans "Home" %}</a> › {{ cl.opts.verbose_name_plural|capfirst|escape }}</div>{% endblock %}{% endif %} + {% block coltype %}flex{% endblock %} + {% block content %} <div id="content-main"> {% block object-tools %} @@ -14,7 +19,18 @@ <div class="module{% if cl.has_filters %} filtered{% endif %}" id="changelist"> {% block search %}{% search_form cl %}{% endblock %} {% block date_hierarchy %}{% date_hierarchy cl %}{% endblock %} -{% block filters %}{% filters cl %}{% endblock %} + +{% block filters %} +{% if cl.has_filters %} +<div id="changelist-filter"> +<h2>{% trans 'Filter' %}</h2> +{% for spec in cl.filter_specs %} + {% admin_list_filter cl spec %} +{% endfor %} +</div> +{% endif %} +{% endblock %} + {% block result_list %}{% result_list cl %}{% endblock %} {% block pagination %}{% pagination cl %}{% endblock %} </div> diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html index f2126882fa..386e134b96 100644 --- a/django/contrib/admin/templates/admin/delete_confirmation.html +++ b/django/contrib/admin/templates/admin/delete_confirmation.html @@ -1,5 +1,6 @@ {% extends "admin/base_site.html" %} {% load i18n %} + {% block breadcrumbs %} <div class="breadcrumbs"> <a href="../../../../">{% trans "Home" %}</a> › @@ -8,6 +9,7 @@ {% trans 'Delete' %} </div> {% endblock %} + {% block content %} {% if perms_lacking %} <p>{% blocktrans with object|escape as escaped_object %}Deleting the {{ object_name }} '{{ escaped_object }}' would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}</p> diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html new file mode 100644 index 0000000000..c726b0fcda --- /dev/null +++ b/django/contrib/admin/templates/admin/edit_inline/stacked.html @@ -0,0 +1,26 @@ +{% load i18n %} +<div class="inline-group"> +{{ inline_admin_formset.formset.management_form }} +{# <h3 class="header">{{ inline_admin_formset.opts.verbose_name_plural|title }}</h3> #} +{{ inline_admin_formset.formset.non_form_errors }} + +{% for inline_admin_form in inline_admin_formset %} +<div class="inline-related {% if forloop.last %}last-related{% endif %}"> + <h2><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b> {% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %} #{{ forloop.counter }}{% endif %} + {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %} + </h2> + {% if inline_admin_form.show_url %} + <p><a href="/r/{{ inline_admin_form.original.content_type_id }}/{{ inline_admin_form.original.id }}/">View on site</a></p> + {% endif %} + + {% for fieldset in inline_admin_form %} + {% include "admin/includes/fieldset.html" %} + {% endfor %} + {{ inline_admin_form.pk_field.field }} +</div> +{% endfor %} + +{# <ul class="tools"> #} +{# <li><a class="add" href="">Add another {{ inline_admin_formset.opts.verbose_name|title }}</a></li> #} +{# </ul> #} +</div> diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html new file mode 100644 index 0000000000..f6332fafbc --- /dev/null +++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html @@ -0,0 +1,64 @@ +{% load i18n %} +<div class="inline-group"> + <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}"> +{{ inline_admin_formset.formset.management_form }} +<fieldset class="module"> + <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst|escape }}</h2> + {{ inline_admin_formset.formset.non_form_errors }} + <table> + <thead><tr> + {% for field in inline_admin_formset.fields %} + {% if not field.is_hidden %} + <th {% if forloop.first %}colspan="2"{% endif %}>{{ field.label|capfirst|escape }}</th> + {% endif %} + {% endfor %} + {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete" %}?</th>{% endif %} + </tr></thead> + + {% for inline_admin_form in inline_admin_formset %} + + <tr class="{% cycle row1,row2 %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}"> + + <td class="original">{% if inline_admin_form.original or inline_admin_form.show_url %}<p> + {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %} + {% if inline_admin_form.show_url %}<a href="/r/{{ inline_admin_form.original.content_type_id }}/{{ inline_admin_form.original.id }}/">View on site</a>{% endif %} + </p>{% endif %} + {{ inline_admin_form.pk_field.field }} + {% spaceless %} + {% for fieldset in inline_admin_form %} + {% for line in fieldset %} + {% for field in line %} + {% if field.is_hidden %} {{ field.field }} {% endif %} + {% endfor %} + {% endfor %} + {% endfor %} + {% endspaceless %} + </td> + + {% for fieldset in inline_admin_form %} + {% for line in fieldset %} + {% for field in line %} + <td class="{{ field.field.name }}"> + {{ field.field.errors.as_ul }} + {{ field.field }} + </td> + {% endfor %} + {% endfor %} + {% endfor %} + + {% if inline_admin_formset.formset.can_delete %}<td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>{% endif %} + + </tr> + + {% endfor %} + + </table> + +</fieldset> + </div> + + {# <ul class="tools"> #} + {# <li><a class="add" href="">Add another {{ inline_admin_formset.opts.verbose_name|title }}</a></li> #} + {# </ul> #} + +</div> diff --git a/django/contrib/admin/templates/admin/edit_inline_stacked.html b/django/contrib/admin/templates/admin/edit_inline_stacked.html deleted file mode 100644 index 45aa0a4f58..0000000000 --- a/django/contrib/admin/templates/admin/edit_inline_stacked.html +++ /dev/null @@ -1,16 +0,0 @@ -{% load admin_modify %} -<fieldset class="module aligned"> - {% for fcw in bound_related_object.form_field_collection_wrappers %} - <h2>{{ bound_related_object.relation.opts.verbose_name|capfirst }} #{{ forloop.counter }}</h2> - {% if bound_related_object.show_url %}{% if fcw.obj.original %} - <p><a href="/r/{{ fcw.obj.original.content_type_id }}/{{ fcw.obj.original.id }}/">View on site</a></p> - {% endif %}{% endif %} - {% for bound_field in fcw.bound_fields %} - {% if bound_field.hidden %} - {% field_widget bound_field %} - {% else %} - {% admin_field_line bound_field %} - {% endif %} - {% endfor %} - {% endfor %} -</fieldset> diff --git a/django/contrib/admin/templates/admin/edit_inline_tabular.html b/django/contrib/admin/templates/admin/edit_inline_tabular.html deleted file mode 100644 index e2dbcbaed2..0000000000 --- a/django/contrib/admin/templates/admin/edit_inline_tabular.html +++ /dev/null @@ -1,44 +0,0 @@ -{% load admin_modify %} -<fieldset class="module"> - <h2>{{ bound_related_object.relation.opts.verbose_name_plural|capfirst }}</h2><table> - <thead><tr> - {% for fw in bound_related_object.field_wrapper_list %} - {% if fw.needs_header %} - <th{{ fw.header_class_attribute }}>{{ fw.field.verbose_name|capfirst }}</th> - {% endif %} - {% endfor %} - </tr></thead> - {% for fcw in bound_related_object.form_field_collection_wrappers %} - {% if change %}{% if original_row_needed %} - {% if fcw.obj.original %} - <tr class="row-label {% cycle row1,row2 %}"><td colspan="{{ num_headers }}"><strong>{{ fcw.obj.original }}</strong></tr> - {% endif %} - {% endif %}{% endif %} - {% if fcw.obj.errors %} - <tr class="errorlist"><td colspan="{{ num_headers }}"> - {{ fcw.obj.html_combined_error_list }} - </tr> - {% endif %} - <tr class="{% cycle row1,row2 %}"> - {% for bound_field in fcw.bound_fields %} - {% if not bound_field.hidden %} - <td {{ bound_field.cell_class_attribute }}> - {% field_widget bound_field %} - </td> - {% endif %} - {% endfor %} - {% if bound_related_object.show_url %}<td> - {% if fcw.obj.original %}<a href="/r/{{ fcw.obj.original.content_type_id }}/{{ fcw.obj.original.id }}/">View on site</a>{% endif %} - </td>{% endif %} - </tr> - - {% endfor %} </table> - - {% for fcw in bound_related_object.form_field_collection_wrappers %} - {% for bound_field in fcw.bound_fields %} - {% if bound_field.hidden %} - {% field_widget bound_field %} - {% endif %} - {% endfor %} - {% endfor %} -</fieldset> diff --git a/django/contrib/admin/templates/admin/field_line.html b/django/contrib/admin/templates/admin/field_line.html deleted file mode 100644 index f4b53fff67..0000000000 --- a/django/contrib/admin/templates/admin/field_line.html +++ /dev/null @@ -1,10 +0,0 @@ -{% load admin_modify %} -<div class="{{ class_names }}" > -{% for bound_field in bound_fields %}{{ bound_field.html_error_list }}{% endfor %} -{% for bound_field in bound_fields %} - {% if bound_field.has_label_first %}{% field_label bound_field %}{% endif %} - {% field_widget bound_field %} - {% if not bound_field.has_label_first %}{% field_label bound_field %}{% endif %} - {% if bound_field.field.help_text %}<p class="help">{{ bound_field.field.help_text|safe }}</p>{% endif %} -{% endfor %} -</div> diff --git a/django/contrib/admin/templates/admin/filters.html b/django/contrib/admin/templates/admin/filters.html deleted file mode 100644 index 3ca763cce3..0000000000 --- a/django/contrib/admin/templates/admin/filters.html +++ /dev/null @@ -1,7 +0,0 @@ -{% load admin_list %} -{% load i18n %} -{% if cl.has_filters %}<div id="changelist-filter"> -<h2>{% trans 'Filter' %}</h2> -{% for spec in cl.filter_specs %} - {% filter cl spec %} -{% endfor %}</div>{% endif %} diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html new file mode 100644 index 0000000000..a61795cfe4 --- /dev/null +++ b/django/contrib/admin/templates/admin/includes/fieldset.html @@ -0,0 +1,17 @@ +<fieldset class="module aligned {{ fieldset.classes }}"> + {% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %} + {% if fieldset.description %}<div class="description">{{ fieldset.description }}</div>{% endif %} + {% for line in fieldset %} + <div class="form-row{% if line.errors %} errors{% endif %} {% for field in line %}{{ field.field.name }} {% endfor %} "> + {{ line.errors }} + {% for field in line %} + {% if field.is_checkbox %} + {{ field.field }}{{ field.label_tag }} + {% else %} + {{ field.label_tag }}{{ field.field }} + {% endif %} + {% if field.field.field.help_text %}<p class="help">{{ field.field.field.help_text|safe }}</p>{% endif %} + {% endfor %} + </div> + {% endfor %} +</fieldset>
\ No newline at end of file diff --git a/django/contrib/admin/templates/admin/index.html b/django/contrib/admin/templates/admin/index.html index 2f406a1754..074be03208 100644 --- a/django/contrib/admin/templates/admin/index.html +++ b/django/contrib/admin/templates/admin/index.html @@ -2,15 +2,16 @@ {% load i18n %} {% block stylesheet %}{% load adminmedia %}{% admin_media_prefix %}css/dashboard.css{% endblock %} + {% block coltype %}colMS{% endblock %} + {% block bodyclass %}dashboard{% endblock %} + {% block breadcrumbs %}{% endblock %} + {% block content %} <div id="content-main"> -{% load adminapplist %} - -{% get_admin_app_list as app_list %} {% if app_list %} {% for app in app_list %} <div class="module"> diff --git a/django/contrib/admin/templates/admin/invalid_setup.html b/django/contrib/admin/templates/admin/invalid_setup.html index 1fa0d32358..f09b316b06 100644 --- a/django/contrib/admin/templates/admin/invalid_setup.html +++ b/django/contrib/admin/templates/admin/invalid_setup.html @@ -4,7 +4,5 @@ {% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans 'Home' %}</a> › {{ title }}</div>{% endblock %} {% block content %} - <p>{% trans "Something's wrong with your database installation. Make sure the appropriate database tables have been created, and make sure the database is readable by the appropriate user." %}</p> - {% endblock %} diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html index 0773132cec..5dd953bc23 100644 --- a/django/contrib/admin/templates/admin/login.html +++ b/django/contrib/admin/templates/admin/login.html @@ -2,12 +2,14 @@ {% load i18n %} {% block stylesheet %}{% load adminmedia %}{% admin_media_prefix %}css/login.css{% endblock %} + {% block bodyclass %}login{% endblock %} + {% block content_title %}{% endblock %} + {% block breadcrumbs %}{% endblock %} {% block content %} - {% if error_message %} <p class="errornote">{{ error_message }}</p> {% endif %} diff --git a/django/contrib/admin/templates/admin/object_history.html b/django/contrib/admin/templates/admin/object_history.html index 00e47259bf..19c037cda9 100644 --- a/django/contrib/admin/templates/admin/object_history.html +++ b/django/contrib/admin/templates/admin/object_history.html @@ -1,16 +1,15 @@ {% extends "admin/base_site.html" %} {% load i18n %} + {% block breadcrumbs %} <div class="breadcrumbs"><a href="../../../../">{% trans 'Home' %}</a> › <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 %} diff --git a/django/contrib/admin/templates/admin/search_form.html b/django/contrib/admin/templates/admin/search_form.html index 445cca3089..b232aa917d 100644 --- a/django/contrib/admin/templates/admin/search_form.html +++ b/django/contrib/admin/templates/admin/search_form.html @@ -1,6 +1,6 @@ {% load adminmedia %} {% load i18n %} -{% if cl.lookup_opts.admin.search_fields %} +{% if cl.search_fields %} <div id="toolbar"><form id="changelist-search" action="" method="get"> <div><!-- DIV needed for valid HTML --> <label for="searchbar"><img src="{% admin_media_prefix %}img/admin/icon_searchbox.png" alt="Search" /></label> diff --git a/django/contrib/admin/templates/admin_doc/index.html b/django/contrib/admin/templates/admin_doc/index.html index 750dd2f5ac..242fc7339a 100644 --- a/django/contrib/admin/templates/admin_doc/index.html +++ b/django/contrib/admin/templates/admin_doc/index.html @@ -25,3 +25,4 @@ </div> {% endblock %} + diff --git a/django/contrib/admin/templates/admin_doc/view_index.html b/django/contrib/admin/templates/admin_doc/view_index.html index 716e2d1a91..4099005828 100644 --- a/django/contrib/admin/templates/admin_doc/view_index.html +++ b/django/contrib/admin/templates/admin_doc/view_index.html @@ -40,3 +40,4 @@ </div> {% endblock %} + diff --git a/django/contrib/admin/templates/registration/password_change_done.html b/django/contrib/admin/templates/registration/password_change_done.html index 4498c63a37..252572001d 100644 --- a/django/contrib/admin/templates/registration/password_change_done.html +++ b/django/contrib/admin/templates/registration/password_change_done.html @@ -1,5 +1,6 @@ {% extends "admin/base_site.html" %} {% load i18n %} +{% block userlinks %}<a href="../../doc/">{% trans 'Documentation' %}</a> / {% trans 'Change password' %} / <a href="../../logout/">{% trans 'Log out' %}</a>{% endblock %} {% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans 'Home' %}</a> › {% trans 'Password change' %}</div>{% endblock %} {% block title %}{% trans 'Password change successful' %}{% endblock %} diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html index cd2b1e0c8a..036d56212c 100644 --- a/django/contrib/admin/templates/registration/password_change_form.html +++ b/django/contrib/admin/templates/registration/password_change_form.html @@ -1,5 +1,6 @@ {% extends "admin/base_site.html" %} {% load i18n %} +{% block userlinks %}<a href="../doc/">{% trans 'Documentation' %}</a> / {% trans 'Change password' %} / <a href="../logout/">{% trans 'Log out' %}</a>{% endblock %} {% block breadcrumbs %}<div class="breadcrumbs"><a href="../">{% trans 'Home' %}</a> › {% trans 'Password change' %}</div>{% endblock %} {% block title %}{% trans 'Password change' %}{% endblock %} diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html index 423821ba60..d8c7d03f93 100644 --- a/django/contrib/admin/templates/registration/password_reset_form.html +++ b/django/contrib/admin/templates/registration/password_reset_form.html @@ -12,7 +12,7 @@ <p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll reset your password and e-mail the new one to you." %}</p> <form action="" method="post"> -{% if form.email.errors %}{{ form.email.html_error_list }}{% endif %} +{% if form.email.errors %}{{ form.email.errors }}{% endif %} <p><label for="id_email">{% trans 'E-mail address:' %}</label> {{ form.email }} <input type="submit" value="{% trans 'Reset my password' %}" /></p> </form> diff --git a/django/contrib/admin/templates/widget/date_time.html b/django/contrib/admin/templates/widget/date_time.html deleted file mode 100644 index cbd4a2e1c6..0000000000 --- a/django/contrib/admin/templates/widget/date_time.html +++ /dev/null @@ -1,5 +0,0 @@ -{% load i18n %} -<p class="datetime"> - {% trans "Date:" %} {{ bound_field.form_fields.0 }}<br /> - {% trans "Time:" %} {{ bound_field.form_fields.1 }} -</p> diff --git a/django/contrib/admin/templates/widget/default.html b/django/contrib/admin/templates/widget/default.html deleted file mode 100644 index 0af231ddcb..0000000000 --- a/django/contrib/admin/templates/widget/default.html +++ /dev/null @@ -1 +0,0 @@ -{% load admin_modify %}{% output_all bound_field.form_fields %} diff --git a/django/contrib/admin/templates/widget/file.html b/django/contrib/admin/templates/widget/file.html deleted file mode 100644 index e584abf956..0000000000 --- a/django/contrib/admin/templates/widget/file.html +++ /dev/null @@ -1,4 +0,0 @@ -{% load admin_modify i18n %}{% if bound_field.original_value %} -{% trans "Currently:" %} <a href="{{ bound_field.original_url }}" > {{ bound_field.original_value|escape }} </a><br /> -{% trans "Change:" %}{% output_all bound_field.form_fields %} -{% else %} {% output_all bound_field.form_fields %} {% endif %} diff --git a/django/contrib/admin/templates/widget/foreign.html b/django/contrib/admin/templates/widget/foreign.html deleted file mode 100644 index 6b43d044bd..0000000000 --- a/django/contrib/admin/templates/widget/foreign.html +++ /dev/null @@ -1,20 +0,0 @@ -{% load admin_modify adminmedia %} -{% output_all bound_field.form_fields %} -{% if bound_field.raw_id_admin %} - {% if bound_field.field.rel.limit_choices_to %} - <a href="{{ bound_field.related_url }}?{% for limit_choice in bound_field.field.rel.limit_choices_to.items %}{% if not forloop.first %}&{% 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 %} diff --git a/django/contrib/admin/templates/widget/many_to_many.html b/django/contrib/admin/templates/widget/many_to_many.html deleted file mode 100644 index a93aa65f73..0000000000 --- a/django/contrib/admin/templates/widget/many_to_many.html +++ /dev/null @@ -1 +0,0 @@ -{% include "widget/foreign.html" %} diff --git a/django/contrib/admin/templates/widget/one_to_one.html b/django/contrib/admin/templates/widget/one_to_one.html deleted file mode 100644 index a79a12314f..0000000000 --- a/django/contrib/admin/templates/widget/one_to_one.html +++ /dev/null @@ -1,2 +0,0 @@ -{% if add %}{% include "widget/foreign.html" %}{% endif %} -{% if change %}{% if bound_field.existing_display %} <strong>{{ bound_field.existing_display|truncatewords:"14" }}</strong>{% endif %}{% endif %} diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 6db59ea338..87fad70ec3 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -71,7 +71,7 @@ pagination = register.inclusion_tag('admin/pagination.html')(pagination) def result_headers(cl): lookup_opts = cl.lookup_opts - for i, field_name in enumerate(lookup_opts.admin.list_display): + for i, field_name in enumerate(cl.list_display): try: f = lookup_opts.get_field(field_name) admin_order_field = None @@ -123,7 +123,7 @@ def _boolean_icon(field_val): def items_for_result(cl, result): first = True pk = cl.lookup_opts.pk.attname - for field_name in cl.lookup_opts.admin.list_display: + for field_name in cl.list_display: row_class = '' try: f = cl.lookup_opts.get_field(field_name) @@ -189,7 +189,7 @@ def items_for_result(cl, result): if force_unicode(result_repr) == '': result_repr = mark_safe(' ') # If list_display_links not defined, add the link tag to the first field - if (first and not cl.lookup_opts.admin.list_display_links) or field_name in cl.lookup_opts.admin.list_display_links: + if (first and not cl.list_display_links) or field_name in cl.list_display_links: table_tag = {True:'th', False:'td'}[first] first = False url = cl.url_for_result(result) @@ -212,8 +212,8 @@ def result_list(cl): result_list = register.inclusion_tag("admin/change_list_results.html")(result_list) def date_hierarchy(cl): - if cl.lookup_opts.admin.date_hierarchy: - field_name = cl.lookup_opts.admin.date_hierarchy + if cl.date_hierarchy: + field_name = cl.date_hierarchy year_field = '%s__year' % field_name month_field = '%s__month' % field_name day_field = '%s__day' % field_name @@ -280,10 +280,6 @@ def search_form(cl): } search_form = register.inclusion_tag('admin/search_form.html')(search_form) -def filter(cl, spec): +def admin_list_filter(cl, spec): return {'title': spec.title(), 'choices' : list(spec.choices(cl))} -filter = register.inclusion_tag('admin/filter.html')(filter) - -def filters(cl): - return {'cl': cl} -filters = register.inclusion_tag('admin/filters.html')(filters) +admin_list_filter = register.inclusion_tag('admin/filter.html')(admin_list_filter) diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index ef33bb33b0..25d2d6774a 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -1,253 +1,21 @@ from django import template -from django.contrib.admin.views.main import AdminBoundField -from django.template import loader -from django.utils.text import capfirst -from django.utils.encoding import force_unicode -from django.utils.safestring import mark_safe -from django.utils.html import escape -from django.db import models -from django.db.models.fields import Field -from django.db.models.related import BoundRelatedObject -from django.conf import settings -import re register = template.Library() -word_re = re.compile('[A-Z][a-z]+') -absolute_url_re = re.compile(r'^(?:http(?:s)?:/)?/', re.IGNORECASE) - -def class_name_to_underscored(name): - return u'_'.join([s.lower() for s in word_re.findall(name)[:-1]]) - -def include_admin_script(script_path): - """ - Returns an HTML script element for including a script from the admin - media url (or other location if an absolute url is given). - - Example usage:: - - {% include_admin_script "js/calendar.js" %} - - could return:: - - <script type="text/javascript" src="/media/admin/js/calendar.js"> - """ - if not absolute_url_re.match(script_path): - script_path = '%s%s' % (settings.ADMIN_MEDIA_PREFIX, script_path) - return mark_safe(u'<script type="text/javascript" src="%s"></script>' - % script_path) -include_admin_script = register.simple_tag(include_admin_script) - def submit_row(context): opts = context['opts'] change = context['change'] is_popup = context['is_popup'] + save_as = context['save_as'] return { 'onclick_attrib': (opts.get_ordered_objects() and change and 'onclick="submitOrderForm();"' or ''), 'show_delete_link': (not is_popup and context['has_delete_permission'] and (change or context['show_delete'])), - 'show_save_as_new': not is_popup and change and opts.admin.save_as, - 'show_save_and_add_another': not is_popup and (not opts.admin.save_as or context['add']), + 'show_save_as_new': not is_popup and change and save_as, + 'show_save_and_add_another': context['has_add_permission'] and + not is_popup and (not save_as or context['add']), 'show_save_and_continue': not is_popup and context['has_change_permission'], 'show_save': True } submit_row = register.inclusion_tag('admin/submit_line.html', takes_context=True)(submit_row) - -def field_label(bound_field): - class_names = [] - if isinstance(bound_field.field, models.BooleanField): - class_names.append("vCheckboxLabel") - colon = "" - else: - if not bound_field.field.blank: - class_names.append('required') - if not bound_field.first: - class_names.append('inline') - colon = ":" - class_str = class_names and u' class="%s"' % u' '.join(class_names) or u'' - return mark_safe(u'<label for="%s"%s>%s%s</label> ' % - (bound_field.element_id, class_str, - escape(force_unicode(capfirst(bound_field.field.verbose_name))), - colon)) -field_label = register.simple_tag(field_label) - -class FieldWidgetNode(template.Node): - nodelists = {} - default = None - - def __init__(self, bound_field_var): - self.bound_field_var = template.Variable(bound_field_var) - - def get_nodelist(cls, klass): - if klass not in cls.nodelists: - try: - field_class_name = klass.__name__ - template_name = u"widget/%s.html" % class_name_to_underscored(field_class_name) - nodelist = loader.get_template(template_name).nodelist - except template.TemplateDoesNotExist: - super_klass = bool(klass.__bases__) and klass.__bases__[0] or None - if super_klass and super_klass != Field: - nodelist = cls.get_nodelist(super_klass) - else: - if not cls.default: - cls.default = loader.get_template("widget/default.html").nodelist - nodelist = cls.default - - cls.nodelists[klass] = nodelist - return nodelist - else: - return cls.nodelists[klass] - get_nodelist = classmethod(get_nodelist) - - def render(self, context): - bound_field = self.bound_field_var.resolve(context) - - context.push() - context['bound_field'] = bound_field - - output = self.get_nodelist(bound_field.field.__class__).render(context) - context.pop() - return output - -class FieldWrapper(object): - def __init__(self, field ): - self.field = field - - def needs_header(self): - return not isinstance(self.field, models.AutoField) - - def header_class_attribute(self): - return self.field.blank and mark_safe(' class="optional"') or '' - - def use_raw_id_admin(self): - return isinstance(self.field.rel, (models.ManyToOneRel, models.ManyToManyRel)) \ - and self.field.rel.raw_id_admin - -class FormFieldCollectionWrapper(object): - def __init__(self, field_mapping, fields, index): - self.field_mapping = field_mapping - self.fields = fields - self.bound_fields = [AdminBoundField(field, self.field_mapping, field_mapping['original']) - for field in self.fields] - self.index = index - -class TabularBoundRelatedObject(BoundRelatedObject): - def __init__(self, related_object, field_mapping, original): - super(TabularBoundRelatedObject, self).__init__(related_object, field_mapping, original) - self.field_wrapper_list = [FieldWrapper(field) for field in self.relation.editable_fields()] - - fields = self.relation.editable_fields() - - self.form_field_collection_wrappers = [FormFieldCollectionWrapper(field_mapping, fields, i) - for (i,field_mapping) in self.field_mappings.items() ] - self.original_row_needed = max([fw.use_raw_id_admin() for fw in self.field_wrapper_list]) - self.show_url = original and hasattr(self.relation.opts, 'get_absolute_url') - - def template_name(self): - return "admin/edit_inline_tabular.html" - -class StackedBoundRelatedObject(BoundRelatedObject): - def __init__(self, related_object, field_mapping, original): - super(StackedBoundRelatedObject, self).__init__(related_object, field_mapping, original) - fields = self.relation.editable_fields() - self.field_mappings.fill() - self.form_field_collection_wrappers = [FormFieldCollectionWrapper(field_mapping ,fields, i) - for (i,field_mapping) in self.field_mappings.items()] - self.show_url = original and hasattr(self.relation.opts, 'get_absolute_url') - - def template_name(self): - return "admin/edit_inline_stacked.html" - -class EditInlineNode(template.Node): - def __init__(self, rel_var): - self.rel_var = template.Variable(rel_var) - - def render(self, context): - relation = self.rel_var.resolve(context) - context.push() - if relation.field.rel.edit_inline == models.TABULAR: - bound_related_object_class = TabularBoundRelatedObject - elif relation.field.rel.edit_inline == models.STACKED: - bound_related_object_class = StackedBoundRelatedObject - else: - bound_related_object_class = relation.field.rel.edit_inline - original = context.get('original', None) - bound_related_object = relation.bind(context['form'], original, bound_related_object_class) - context['bound_related_object'] = bound_related_object - t = loader.get_template(bound_related_object.template_name()) - output = t.render(context) - context.pop() - return output - -def output_all(form_fields): - return u''.join([force_unicode(f) for f in form_fields]) -output_all = register.simple_tag(output_all) - -def auto_populated_field_script(auto_pop_fields, change = False): - t = [] - for field in auto_pop_fields: - if change: - t.append(u'document.getElementById("id_%s")._changed = true;' % field.name) - else: - t.append(u'document.getElementById("id_%s").onchange = function() { this._changed = true; };' % field.name) - - add_values = u' + " " + '.join([u'document.getElementById("id_%s").value' % g for g in field.prepopulate_from]) - for f in field.prepopulate_from: - t.append(u'document.getElementById("id_%s").onkeyup = function() {' \ - ' var e = document.getElementById("id_%s");' \ - ' if(!e._changed) { e.value = URLify(%s, %s);} }; ' % ( - f, field.name, add_values, field.max_length)) - return mark_safe(u''.join(t)) -auto_populated_field_script = register.simple_tag(auto_populated_field_script) - -def filter_interface_script_maybe(bound_field): - f = bound_field.field - if f.rel and isinstance(f.rel, models.ManyToManyRel) and f.rel.filter_interface: - return mark_safe(u'<script type="text/javascript">addEvent(window, "load", function(e) {' \ - ' SelectFilter.init("id_%s", "%s", %s, "%s"); });</script>\n' % ( - f.name, escape(f.verbose_name.replace('"', '\\"')), f.rel.filter_interface-1, settings.ADMIN_MEDIA_PREFIX)) - else: - return '' -filter_interface_script_maybe = register.simple_tag(filter_interface_script_maybe) - -def field_widget(parser, token): - bits = token.contents.split() - if len(bits) != 2: - raise template.TemplateSyntaxError, "%s takes 1 argument" % bits[0] - return FieldWidgetNode(bits[1]) -field_widget = register.tag(field_widget) - -def edit_inline(parser, token): - bits = token.contents.split() - if len(bits) != 2: - raise template.TemplateSyntaxError, "%s takes 1 argument" % bits[0] - return EditInlineNode(bits[1]) -edit_inline = register.tag(edit_inline) - -def admin_field_line(context, argument_val): - if isinstance(argument_val, AdminBoundField): - bound_fields = [argument_val] - else: - bound_fields = [bf for bf in argument_val] - add = context['add'] - change = context['change'] - - class_names = ['form-row'] - for bound_field in bound_fields: - for f in bound_field.form_fields: - if f.errors(): - class_names.append('errors') - break - - # Assumes BooleanFields won't be stacked next to each other! - if isinstance(bound_fields[0].field, models.BooleanField): - class_names.append('checkbox-row') - - return { - 'add': context['add'], - 'change': context['change'], - 'bound_fields': bound_fields, - 'class_names': " ".join(class_names), - } -admin_field_line = register.inclusion_tag('admin/field_line.html', takes_context=True)(admin_field_line) diff --git a/django/contrib/admin/templatetags/adminapplist.py b/django/contrib/admin/templatetags/adminapplist.py deleted file mode 100644 index bf394e2099..0000000000 --- a/django/contrib/admin/templatetags/adminapplist.py +++ /dev/null @@ -1,81 +0,0 @@ -from django import template -from django.db.models import get_models -from django.utils.encoding import force_unicode -from django.utils.safestring import mark_safe - -register = template.Library() - -class AdminApplistNode(template.Node): - def __init__(self, varname): - self.varname = varname - - def render(self, context): - from django.db import models - from django.utils.text import capfirst - app_list = [] - user = context['user'] - - for app in models.get_apps(): - # Determine the app_label. - app_models = get_models(app) - if not app_models: - continue - app_label = app_models[0]._meta.app_label - - has_module_perms = user.has_module_perms(app_label) - - if has_module_perms: - model_list = [] - for m in app_models: - if m._meta.admin: - perms = { - 'add': user.has_perm("%s.%s" % (app_label, m._meta.get_add_permission())), - 'change': user.has_perm("%s.%s" % (app_label, m._meta.get_change_permission())), - 'delete': user.has_perm("%s.%s" % (app_label, m._meta.get_delete_permission())), - } - - # Check whether user has any perm for this module. - # If so, add the module to the model_list. - if True in perms.values(): - model_list.append({ - 'name': force_unicode(capfirst(m._meta.verbose_name_plural)), - 'admin_url': mark_safe(u'%s/%s/' % (force_unicode(app_label), m.__name__.lower())), - 'perms': perms, - }) - - if model_list: - # Sort using verbose decorate-sort-undecorate pattern - # instead of key argument to sort() for python 2.3 compatibility - decorated = [(x['name'], x) for x in model_list] - decorated.sort() - model_list = [x for key, x in decorated] - - app_list.append({ - 'name': app_label.title(), - 'has_module_perms': has_module_perms, - 'models': model_list, - }) - context[self.varname] = app_list - return '' - -def get_admin_app_list(parser, token): - """ - Returns a list of installed applications and models for which the current user - has at least one permission. - - Syntax:: - - {% get_admin_app_list as [context_var_containing_app_list] %} - - Example usage:: - - {% get_admin_app_list as admin_app_list %} - """ - tokens = token.contents.split() - if len(tokens) < 3: - raise template.TemplateSyntaxError, "'%s' tag requires two arguments" % tokens[0] - if tokens[1] != 'as': - raise template.TemplateSyntaxError, "First argument to '%s' tag must be 'as'" % tokens[0] - return AdminApplistNode(tokens[2]) - -register.tag('get_admin_app_list', get_admin_app_list) diff --git a/django/contrib/admin/urls.py b/django/contrib/admin/urls.py deleted file mode 100644 index 436e9b7b23..0000000000 --- a/django/contrib/admin/urls.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.conf import settings -from django.conf.urls.defaults import * - -if settings.USE_I18N: - i18n_view = 'django.views.i18n.javascript_catalog' -else: - i18n_view = 'django.views.i18n.null_javascript_catalog' - -urlpatterns = patterns('', - ('^$', 'django.contrib.admin.views.main.index'), - ('^r/', include('django.conf.urls.shortcut')), - ('^jsi18n/$', i18n_view, {'packages': 'django.conf'}), - ('^logout/$', 'django.contrib.auth.views.logout'), - ('^password_change/$', 'django.contrib.auth.views.password_change'), - ('^password_change/done/$', 'django.contrib.auth.views.password_change_done'), - ('^template_validator/$', 'django.contrib.admin.views.template.template_validator'), - - # Documentation - ('^doc/$', 'django.contrib.admin.views.doc.doc_index'), - ('^doc/bookmarklets/$', 'django.contrib.admin.views.doc.bookmarklets'), - ('^doc/tags/$', 'django.contrib.admin.views.doc.template_tag_index'), - ('^doc/filters/$', 'django.contrib.admin.views.doc.template_filter_index'), - ('^doc/views/$', 'django.contrib.admin.views.doc.view_index'), - ('^doc/views/(?P<view>[^/]+)/$', 'django.contrib.admin.views.doc.view_detail'), - ('^doc/models/$', 'django.contrib.admin.views.doc.model_index'), - ('^doc/models/(?P<app_label>[^\.]+)\.(?P<model_name>[^/]+)/$', 'django.contrib.admin.views.doc.model_detail'), -# ('^doc/templates/$', 'django.views.admin.doc.template_index'), - ('^doc/templates/(?P<template>.*)/$', 'django.contrib.admin.views.doc.template_detail'), - - # "Add user" -- a special-case view - ('^auth/user/add/$', 'django.contrib.admin.views.auth.user_add_stage'), - # "Change user password" -- another special-case view - ('^auth/user/(\d+)/password/$', 'django.contrib.admin.views.auth.user_change_password'), - - # Add/change/delete/history - ('^([^/]+)/([^/]+)/$', 'django.contrib.admin.views.main.change_list'), - ('^([^/]+)/([^/]+)/add/$', 'django.contrib.admin.views.main.add_stage'), - ('^([^/]+)/([^/]+)/(.+)/history/$', 'django.contrib.admin.views.main.history'), - ('^([^/]+)/([^/]+)/(.+)/delete/$', 'django.contrib.admin.views.main.delete_stage'), - ('^([^/]+)/([^/]+)/(.+)/$', 'django.contrib.admin.views.main.change_stage'), -) - -del i18n_view diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py new file mode 100644 index 0000000000..6a585b2919 --- /dev/null +++ b/django/contrib/admin/util.py @@ -0,0 +1,139 @@ +from django.core.exceptions import ObjectDoesNotExist +from django.db import models +from django.utils.html import escape +from django.utils.safestring import mark_safe +from django.utils.text import capfirst +from django.utils.encoding import force_unicode +from django.utils.translation import ugettext as _ + + +def quote(s): + """ + Ensure that primary key values do not confuse the admin URLs by escaping + any '/', '_' and ':' characters. Similar to urllib.quote, except that the + quoting is slightly different so that it doesn't get automatically + unquoted by the Web browser. + """ + if not isinstance(s, basestring): + return s + res = list(s) + for i in range(len(res)): + c = res[i] + if c in """:/_#?;@&=+$,"<>%\\""": + res[i] = '_%02X' % ord(c) + return ''.join(res) + +def unquote(s): + """ + Undo the effects of quote(). Based heavily on urllib.unquote(). + """ + mychr = chr + myatoi = int + list = s.split('_') + res = [list[0]] + myappend = res.append + del list[0] + for item in list: + if item[1:2]: + try: + myappend(mychr(myatoi(item[:2], 16)) + item[2:]) + except ValueError: + myappend('_' + item) + else: + myappend('_' + item) + return "".join(res) + +def _nest_help(obj, depth, val): + current = obj + for i in range(depth): + current = current[-1] + current.append(val) + +def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth, admin_site): + "Helper function that recursively populates deleted_objects." + nh = _nest_help # Bind to local variable for performance + if current_depth > 16: + return # Avoid recursing too deep. + opts_seen = [] + for related in opts.get_all_related_objects(): + has_admin = related.model in admin_site._registry + if related.opts in opts_seen: + continue + opts_seen.append(related.opts) + rel_opts_name = related.get_accessor_name() + if isinstance(related.field.rel, models.OneToOneRel): + try: + sub_obj = getattr(obj, rel_opts_name) + except ObjectDoesNotExist: + pass + else: + if has_admin: + p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission()) + if not user.has_perm(p): + perms_needed.add(related.opts.verbose_name) + # We don't care about populating deleted_objects now. + continue + if related.field.rel.edit_inline or not has_admin: + # Don't display link to edit, because it either has no + # admin or is edited inline. + nh(deleted_objects, current_depth, [mark_safe(u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), sub_obj)), []]) + else: + # Display a link to the admin page. + nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % + (escape(force_unicode(capfirst(related.opts.verbose_name))), + related.opts.app_label, + related.opts.object_name.lower(), + sub_obj._get_pk_val(), sub_obj)), []]) + get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site) + else: + has_related_objs = False + for sub_obj in getattr(obj, rel_opts_name).all(): + has_related_objs = True + if related.field.rel.edit_inline or not has_admin: + # Don't display link to edit, because it either has no + # admin or is edited inline. + nh(deleted_objects, current_depth, [u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), escape(sub_obj)), []]) + else: + # Display a link to the admin page. + nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % \ + (escape(force_unicode(capfirst(related.opts.verbose_name))), related.opts.app_label, related.opts.object_name.lower(), sub_obj._get_pk_val(), escape(sub_obj))), []]) + get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site) + # If there were related objects, and the user doesn't have + # permission to delete them, add the missing perm to perms_needed. + if has_admin and has_related_objs: + p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission()) + if not user.has_perm(p): + perms_needed.add(related.opts.verbose_name) + for related in opts.get_all_related_many_to_many_objects(): + has_admin = related.model in admin_site._registry + if related.opts in opts_seen: + continue + opts_seen.append(related.opts) + rel_opts_name = related.get_accessor_name() + has_related_objs = False + + # related.get_accessor_name() could return None for symmetrical relationships + if rel_opts_name: + rel_objs = getattr(obj, rel_opts_name, None) + if rel_objs: + has_related_objs = True + + if has_related_objs: + for sub_obj in rel_objs.all(): + if related.field.rel.edit_inline or not has_admin: + # Don't display link to edit, because it either has no + # admin or is edited inline. + nh(deleted_objects, current_depth, [_('One or more %(fieldname)s in %(name)s: %(obj)s') % \ + {'fieldname': force_unicode(related.field.verbose_name), 'name': force_unicode(related.opts.verbose_name), 'obj': escape(sub_obj)}, []]) + else: + # Display a link to the admin page. + nh(deleted_objects, current_depth, [ + mark_safe((_('One or more %(fieldname)s in %(name)s:') % {'fieldname': escape(force_unicode(related.field.verbose_name)), 'name': escape(force_unicode(related.opts.verbose_name))}) + \ + (u' <a href="../../../../%s/%s/%s/">%s</a>' % \ + (related.opts.app_label, related.opts.module_name, sub_obj._get_pk_val(), escape(sub_obj)))), []]) + # If there were related objects, and the user doesn't have + # permission to change them, add the missing perm to perms_needed. + if has_admin and has_related_objs: + p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission()) + if not user.has_perm(p): + perms_needed.add(related.opts.verbose_name) diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py new file mode 100644 index 0000000000..f640cc5100 --- /dev/null +++ b/django/contrib/admin/validation.py @@ -0,0 +1,280 @@ + +from django.core.exceptions import ImproperlyConfigured +from django.db import models +from django.newforms.models import BaseModelForm, BaseModelFormSet, fields_for_model +from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin +from django.contrib.admin.options import HORIZONTAL, VERTICAL + +def validate(cls, model): + """ + Does basic ModelAdmin option validation. Calls custom validation + classmethod in the end if it is provided in cls. The signature of the + custom validation classmethod should be: def validate(cls, model). + """ + opts = model._meta + _validate_base(cls, model) + + # currying is expensive, use wrappers instead + def _check_istuplew(label, obj): + _check_istuple(cls, label, obj) + + def _check_isdictw(label, obj): + _check_isdict(cls, label, obj) + + def _check_field_existsw(label, field): + return _check_field_exists(cls, model, opts, label, field) + + def _check_attr_existsw(label, field): + return _check_attr_exists(cls, model, opts, label, field) + + # list_display + if hasattr(cls, 'list_display'): + _check_istuplew('list_display', cls.list_display) + for idx, field in enumerate(cls.list_display): + f = _check_attr_existsw("list_display[%d]" % idx, field) + if isinstance(f, models.ManyToManyField): + raise ImproperlyConfigured("`%s.list_display[%d]`, `%s` is a " + "ManyToManyField which is not supported." + % (cls.__name__, idx, field)) + + # list_display_links + if hasattr(cls, 'list_display_links'): + _check_istuplew('list_display_links', cls.list_display_links) + for idx, field in enumerate(cls.list_display_links): + _check_attr_existsw('list_display_links[%d]' % idx, field) + if field not in cls.list_display: + raise ImproperlyConfigured("`%s.list_display_links[%d]`" + "refers to `%s` which is not defined in `list_display`." + % (cls.__name__, idx, field)) + + # list_filter + if hasattr(cls, 'list_filter'): + _check_istuplew('list_filter', cls.list_filter) + for idx, field in enumerate(cls.list_filter): + _check_field_existsw('list_filter[%d]' % idx, field) + + # list_per_page = 100 + if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int): + raise ImproperlyConfigured("`%s.list_per_page` should be a integer." + % cls.__name__) + + # search_fields = () + if hasattr(cls, 'search_fields'): + _check_istuplew('search_fields', cls.search_fields) + + # date_hierarchy = None + if cls.date_hierarchy: + f = _check_field_existsw('date_hierarchy', cls.date_hierarchy) + if not isinstance(f, (models.DateField, models.DateTimeField)): + raise ImproperlyConfigured("`%s.date_hierarchy is " + "neither an instance of DateField nor DateTimeField." + % cls.__name__) + + # ordering = None + if cls.ordering: + _check_istuplew('ordering', cls.ordering) + for idx, field in enumerate(cls.ordering): + if field == '?' and len(cls.ordering) != 1: + raise ImproperlyConfigured("`%s.ordering` has the random " + "ordering marker `?`, but contains other fields as " + "well. Please either remove `?` or the other fields." + % cls.__name__) + if field == '?': + continue + if field.startswith('-'): + field = field[1:] + # Skip ordering in the format field1__field2 (FIXME: checking + # this format would be nice, but it's a little fiddly). + if '__' in field: + continue + _check_field_existsw('ordering[%d]' % idx, field) + + # list_select_related = False + # save_as = False + # save_on_top = False + for attr in ('list_select_related', 'save_as', 'save_on_top'): + if not isinstance(getattr(cls, attr), bool): + raise ImproperlyConfigured("`%s.%s` should be a boolean." + % (cls.__name__, attr)) + + # inlines = [] + if hasattr(cls, 'inlines'): + _check_istuplew('inlines', cls.inlines) + for idx, inline in enumerate(cls.inlines): + if not issubclass(inline, BaseModelAdmin): + raise ImproperlyConfigured("`%s.inlines[%d]` does not inherit " + "from BaseModelAdmin." % (cls.__name__, idx)) + if not inline.model: + raise ImproperlyConfigured("`model` is a required attribute " + "of `%s.inlines[%d]`." % (cls.__name__, idx)) + if not issubclass(inline.model, models.Model): + raise ImproperlyConfigured("`%s.inlines[%d].model` does not " + "inherit from models.Model." % (cls.__name__, idx)) + _validate_base(inline, inline.model) + _validate_inline(inline) + +def _validate_inline(cls): + # model is already verified to exist and be a Model + if cls.fk_name: # default value is None + f = _check_field_exists(cls, cls.model, cls.model._meta, + 'fk_name', cls.fk_name) + if not isinstance(f, models.ForeignKey): + raise ImproperlyConfigured("`%s.fk_name is not an instance of " + "models.ForeignKey." % cls.__name__) + # extra = 3 + # max_num = 0 + for attr in ('extra', 'max_num'): + if not isinstance(getattr(cls, attr), int): + raise ImproperlyConfigured("`%s.%s` should be a integer." + % (cls.__name__, attr)) + + # formset + if hasattr(cls, 'formset') and not issubclass(cls.formset, BaseModelFormSet): + raise ImproperlyConfigured("`%s.formset` does not inherit from " + "BaseModelFormSet." % cls.__name__) + +def _validate_base(cls, model): + opts = model._meta + # currying is expensive, use wrappers instead + def _check_istuplew(label, obj): + _check_istuple(cls, label, obj) + + def _check_isdictw(label, obj): + _check_isdict(cls, label, obj) + + def _check_field_existsw(label, field): + return _check_field_exists(cls, model, opts, label, field) + + def _check_form_field_existsw(label, field): + return _check_form_field_exists(cls, model, opts, label, field) + + # raw_id_fields + if hasattr(cls, 'raw_id_fields'): + _check_istuplew('raw_id_fields', cls.raw_id_fields) + for idx, field in enumerate(cls.raw_id_fields): + f = _check_field_existsw('raw_id_fields', field) + if not isinstance(f, (models.ForeignKey, models.ManyToManyField)): + raise ImproperlyConfigured("`%s.raw_id_fields[%d]`, `%s` must " + "be either a ForeignKey or ManyToManyField." + % (cls.__name__, idx, field)) + + # fields + if cls.fields: # default value is None + _check_istuplew('fields', cls.fields) + for field in cls.fields: + _check_form_field_existsw('fields', field) + if cls.fieldsets: + raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__) + + # fieldsets + if cls.fieldsets: # default value is None + _check_istuplew('fieldsets', cls.fieldsets) + for idx, fieldset in enumerate(cls.fieldsets): + _check_istuplew('fieldsets[%d]' % idx, fieldset) + if len(fieldset) != 2: + raise ImproperlyConfigured("`%s.fieldsets[%d]` does not " + "have exactly two elements." % (cls.__name__, idx)) + _check_isdictw('fieldsets[%d][1]' % idx, fieldset[1]) + if 'fields' not in fieldset[1]: + raise ImproperlyConfigured("`fields` key is required in " + "%s.fieldsets[%d][1] field options dict." + % (cls.__name__, idx)) + for field in flatten_fieldsets(cls.fieldsets): + _check_form_field_existsw("fieldsets[%d][1]['fields']" % idx, field) + + # form + if hasattr(cls, 'form') and not issubclass(cls.form, BaseModelForm): + raise ImproperlyConfigured("%s.form does not inherit from " + "BaseModelForm." % cls.__name__) + + # filter_vertical + if hasattr(cls, 'filter_vertical'): + _check_istuplew('filter_vertical', cls.filter_vertical) + for idx, field in enumerate(cls.filter_vertical): + f = _check_field_existsw('filter_vertical', field) + if not isinstance(f, models.ManyToManyField): + raise ImproperlyConfigured("`%s.filter_vertical[%d]` must be " + "a ManyToManyField." % (cls.__name__, idx)) + + # filter_horizontal + if hasattr(cls, 'filter_horizontal'): + _check_istuplew('filter_horizontal', cls.filter_horizontal) + for idx, field in enumerate(cls.filter_horizontal): + f = _check_field_existsw('filter_horizontal', field) + if not isinstance(f, models.ManyToManyField): + raise ImproperlyConfigured("`%s.filter_horizontal[%d]` must be " + "a ManyToManyField." % (cls.__name__, idx)) + + # radio_fields + if hasattr(cls, 'radio_fields'): + _check_isdictw('radio_fields', cls.radio_fields) + for field, val in cls.radio_fields.items(): + f = _check_field_existsw('radio_fields', field) + if not (isinstance(f, models.ForeignKey) or f.choices): + raise ImproperlyConfigured("`%s.radio_fields['%s']` " + "is neither an instance of ForeignKey nor does " + "have choices set." % (cls.__name__, field)) + if not val in (HORIZONTAL, VERTICAL): + raise ImproperlyConfigured("`%s.radio_fields['%s']` " + "is neither admin.HORIZONTAL nor admin.VERTICAL." + % (cls.__name__, field)) + + # prepopulated_fields + if hasattr(cls, 'prepopulated_fields'): + _check_isdictw('prepopulated_fields', cls.prepopulated_fields) + for field, val in cls.prepopulated_fields.items(): + f = _check_field_existsw('prepopulated_fields', field) + if isinstance(f, (models.DateTimeField, models.ForeignKey, + models.ManyToManyField)): + raise ImproperlyConfigured("`%s.prepopulated_fields['%s']` " + "is either a DateTimeField, ForeignKey or " + "ManyToManyField. This isn't allowed." + % (cls.__name__, field)) + _check_istuplew("prepopulated_fields['%s']" % field, val) + for idx, f in enumerate(val): + _check_field_existsw("prepopulated_fields['%s'][%d]" + % (f, idx), f) + +def _check_istuple(cls, label, obj): + if not isinstance(obj, (list, tuple)): + raise ImproperlyConfigured("`%s.%s` must be a " + "list or tuple." % (cls.__name__, label)) + +def _check_isdict(cls, label, obj): + if not isinstance(obj, dict): + raise ImproperlyConfigured("`%s.%s` must be a dictionary." + % (cls.__name__, label)) + +def _check_field_exists(cls, model, opts, label, field): + try: + return opts.get_field(field) + except models.FieldDoesNotExist: + raise ImproperlyConfigured("`%s.%s` refers to " + "field `%s` that is missing from model `%s`." + % (cls.__name__, label, field, model.__name__)) + +def _check_form_field_exists(cls, model, opts, label, field): + if hasattr(cls.form, 'base_fields'): + try: + cls.form.base_fields[field] + except KeyError: + raise ImproperlyConfigured("`%s.%s` refers to field `%s` that " + "is missing from the form." % (cls.__name__, label, field)) + else: + fields = fields_for_model(model) + try: + fields[field] + except KeyError: + raise ImproperlyConfigured("`%s.%s` refers to field `%s` that " + "is missing from the form." % (cls.__name__, label, field)) + +def _check_attr_exists(cls, model, opts, label, field): + try: + return opts.get_field(field) + except models.FieldDoesNotExist: + if not hasattr(model, field): + raise ImproperlyConfigured("`%s.%s` refers to " + "`%s` that is neither a field, method or property " + "of model `%s`." + % (cls.__name__, label, field, model.__name__)) + return getattr(model, field) diff --git a/django/contrib/admin/views/auth.py b/django/contrib/admin/views/auth.py deleted file mode 100644 index 0c8104831b..0000000000 --- a/django/contrib/admin/views/auth.py +++ /dev/null @@ -1,78 +0,0 @@ -from django.contrib.admin.views.decorators import staff_member_required -from django.contrib.auth.forms import UserCreationForm, AdminPasswordChangeForm -from django.contrib.auth.models import User -from django.core.exceptions import PermissionDenied -from django import oldforms, template -from django.shortcuts import render_to_response, get_object_or_404 -from django.http import HttpResponseRedirect -from django.utils.html import escape -from django.utils.translation import ugettext as _ - -def user_add_stage(request): - if not request.user.has_perm('auth.change_user'): - raise PermissionDenied - manipulator = UserCreationForm() - if request.method == 'POST': - new_data = request.POST.copy() - errors = manipulator.get_validation_errors(new_data) - if not errors: - new_user = manipulator.save(new_data) - msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': 'user', 'obj': new_user} - if "_addanother" in request.POST: - request.user.message_set.create(message=msg) - return HttpResponseRedirect(request.path) - else: - request.user.message_set.create(message=msg + ' ' + _("You may edit it again below.")) - return HttpResponseRedirect('../%s/' % new_user.id) - else: - errors = new_data = {} - form = oldforms.FormWrapper(manipulator, new_data, errors) - return render_to_response('admin/auth/user/add_form.html', { - 'title': _('Add user'), - 'form': form, - 'is_popup': '_popup' in request.REQUEST, - 'add': True, - 'change': False, - 'has_delete_permission': False, - 'has_change_permission': True, - 'has_file_field': False, - 'has_absolute_url': False, - 'auto_populated_fields': (), - 'bound_field_sets': (), - 'first_form_field_id': 'id_username', - 'opts': User._meta, - 'username_help_text': User._meta.get_field('username').help_text, - }, context_instance=template.RequestContext(request)) -user_add_stage = staff_member_required(user_add_stage) - -def user_change_password(request, id): - if not request.user.has_perm('auth.change_user'): - raise PermissionDenied - user = get_object_or_404(User, pk=id) - manipulator = AdminPasswordChangeForm(user) - if request.method == 'POST': - new_data = request.POST.copy() - errors = manipulator.get_validation_errors(new_data) - if not errors: - new_user = manipulator.save(new_data) - msg = _('Password changed successfully.') - request.user.message_set.create(message=msg) - return HttpResponseRedirect('..') - else: - errors = new_data = {} - form = oldforms.FormWrapper(manipulator, new_data, errors) - return render_to_response('admin/auth/user/change_password.html', { - 'title': _('Change password: %s') % escape(user.username), - 'form': form, - 'is_popup': '_popup' in request.REQUEST, - 'add': True, - 'change': False, - 'has_delete_permission': False, - 'has_change_permission': True, - 'has_absolute_url': False, - 'first_form_field_id': 'id_password1', - 'opts': User._meta, - 'original': user, - 'show_save': True, - }, context_instance=template.RequestContext(request)) -user_change_password = staff_member_required(user_change_password) diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py index c27b2e7427..57517cc821 100644 --- a/django/contrib/admin/views/decorators.py +++ b/django/contrib/admin/views/decorators.py @@ -12,7 +12,6 @@ from django.contrib.auth.models import User from django.contrib.auth import authenticate, login from django.shortcuts import render_to_response from django.utils.translation import ugettext_lazy, ugettext as _ -from django.utils.safestring import mark_safe ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.") LOGIN_FORM_KEY = 'this_is_the_login_form' diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index f1f620ed9e..926270cc68 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,20 +1,13 @@ -from django import oldforms, template -from django.conf import settings from django.contrib.admin.filterspecs import FilterSpec -from django.contrib.admin.views.decorators import staff_member_required -from django.views.decorators.cache import never_cache -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied +from django.contrib.admin.options import IncorrectLookupParameters +from django.contrib.admin.util import quote from django.core.paginator import Paginator, InvalidPage -from django.shortcuts import get_object_or_404, render_to_response from django.db import models from django.db.models.query import QuerySet -from django.http import Http404, HttpResponse, HttpResponseRedirect -from django.utils.html import escape -from django.utils.text import capfirst, get_text_list from django.utils.encoding import force_unicode, smart_str -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext from django.utils.safestring import mark_safe +from django.utils.http import urlencode import operator try: @@ -22,13 +15,6 @@ try: except NameError: from sets import Set as set # Python 2.3 fallback -from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION -if not LogEntry._meta.installed: - raise ImproperlyConfigured, "You'll need to put 'django.contrib.admin' in your INSTALLED_APPS setting before you can use the admin application." - -if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS: - raise ImproperlyConfigured, "You'll need to put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting before you can use the admin application." - # The system will display a "Show all" link on the change list only if the # total result count is less than or equal to this setting. MAX_SHOW_ALL_ALLOWED = 200 @@ -45,523 +31,20 @@ ERROR_FLAG = 'e' # Text to display within change-list table cells if the value is blank. EMPTY_CHANGELIST_VALUE = '(None)' -use_raw_id_admin = lambda field: isinstance(field.rel, (models.ManyToOneRel, models.ManyToManyRel)) and field.rel.raw_id_admin - -class IncorrectLookupParameters(Exception): - pass - -def quote(s): - """ - Ensure that primary key values do not confuse the admin URLs by escaping - any '/', '_' and ':' characters. Similar to urllib.quote, except that the - quoting is slightly different so that it doesn't get automatically - unquoted by the Web browser. - """ - if type(s) != type(''): - return s - res = list(s) - for i in range(len(res)): - c = res[i] - if c in ':/_': - res[i] = '_%02X' % ord(c) - return ''.join(res) - -def unquote(s): - """ - Undo the effects of quote(). Based heavily on urllib.unquote(). - """ - mychr = chr - myatoi = int - list = s.split('_') - res = [list[0]] - myappend = res.append - del list[0] - for item in list: - if item[1:2]: - try: - myappend(mychr(myatoi(item[:2], 16)) + item[2:]) - except ValueError: - myappend('_' + item) - else: - myappend('_' + item) - return "".join(res) - -def get_javascript_imports(opts, auto_populated_fields, field_sets): -# Put in any necessary JavaScript imports. - js = ['js/core.js', 'js/admin/RelatedObjectLookups.js'] - if auto_populated_fields: - js.append('js/urlify.js') - if opts.has_field_type(models.DateTimeField) or opts.has_field_type(models.TimeField) or opts.has_field_type(models.DateField): - js.extend(['js/calendar.js', 'js/admin/DateTimeShortcuts.js']) - if opts.get_ordered_objects(): - js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js']) - if opts.admin.js: - js.extend(opts.admin.js) - seen_collapse = False - for field_set in field_sets: - if not seen_collapse and 'collapse' in field_set.classes: - seen_collapse = True - js.append('js/admin/CollapsedFieldsets.js') - - for field_line in field_set: - try: - for f in field_line: - if f.rel and isinstance(f, models.ManyToManyField) and f.rel.filter_interface: - js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js']) - raise StopIteration - except StopIteration: - break - return js - -class AdminBoundField(object): - def __init__(self, field, field_mapping, original): - self.field = field - self.original = original - self.form_fields = [field_mapping[name] for name in self.field.get_manipulator_field_names('')] - self.element_id = self.form_fields[0].get_id() - self.has_label_first = not isinstance(self.field, models.BooleanField) - self.raw_id_admin = use_raw_id_admin(field) - self.is_date_time = isinstance(field, models.DateTimeField) - self.is_file_field = isinstance(field, models.FileField) - self.needs_add_label = field.rel and (isinstance(field.rel, models.ManyToOneRel) or isinstance(field.rel, models.ManyToManyRel)) and field.rel.to._meta.admin - self.hidden = isinstance(self.field, models.AutoField) - self.first = False - - classes = [] - if self.raw_id_admin: - classes.append('nowrap') - if max([bool(f.errors()) for f in self.form_fields]): - classes.append('error') - if classes: - self.cell_class_attribute = u' class="%s" ' % ' '.join(classes) - self._repr_filled = False - - if field.rel: - self.related_url = mark_safe(u'../../../%s/%s/' - % (field.rel.to._meta.app_label, - field.rel.to._meta.object_name.lower())) - - def original_value(self): - if self.original: - return self.original.__dict__[self.field.attname] - - def existing_display(self): - try: - return self._display - except AttributeError: - if isinstance(self.field.rel, models.ManyToOneRel): - self._display = force_unicode(getattr(self.original, self.field.name), strings_only=True) - elif isinstance(self.field.rel, models.ManyToManyRel): - self._display = u", ".join([force_unicode(obj) for obj in getattr(self.original, self.field.name).all()]) - return self._display - - def __repr__(self): - return repr(self.__dict__) - - def html_error_list(self): - return mark_safe(" ".join([form_field.html_error_list() for form_field in self.form_fields if form_field.errors])) - - def original_url(self): - if self.is_file_field and self.original and self.field.attname: - url_method = getattr(self.original, 'get_%s_url' % self.field.attname) - if callable(url_method): - return url_method() - return '' - -class AdminBoundFieldLine(object): - def __init__(self, field_line, field_mapping, original): - self.bound_fields = [field.bind(field_mapping, original, AdminBoundField) for field in field_line] - for bound_field in self: - bound_field.first = True - break - - def __iter__(self): - for bound_field in self.bound_fields: - yield bound_field - - def __len__(self): - return len(self.bound_fields) - -class AdminBoundFieldSet(object): - def __init__(self, field_set, field_mapping, original): - self.name = field_set.name - self.classes = field_set.classes - self.description = field_set.description - self.bound_field_lines = [field_line.bind(field_mapping, original, AdminBoundFieldLine) for field_line in field_set] - - def __iter__(self): - for bound_field_line in self.bound_field_lines: - yield bound_field_line - - def __len__(self): - return len(self.bound_field_lines) - -def render_change_form(model, manipulator, context, add=False, change=False, form_url=''): - opts = model._meta - app_label = opts.app_label - auto_populated_fields = [f for f in opts.fields if f.prepopulate_from] - field_sets = opts.admin.get_field_sets(opts) - original = getattr(manipulator, 'original_object', None) - bound_field_sets = [field_set.bind(context['form'], original, AdminBoundFieldSet) for field_set in field_sets] - first_form_field_id = bound_field_sets[0].bound_field_lines[0].bound_fields[0].form_fields[0].get_id(); - ordered_objects = opts.get_ordered_objects() - inline_related_objects = opts.get_followed_related_objects(manipulator.follow) - extra_context = { - 'add': add, - 'change': change, - 'has_delete_permission': context['perms'][app_label][opts.get_delete_permission()], - 'has_change_permission': context['perms'][app_label][opts.get_change_permission()], - 'has_file_field': opts.has_field_type(models.FileField), - 'has_absolute_url': hasattr(model, 'get_absolute_url'), - 'auto_populated_fields': auto_populated_fields, - 'bound_field_sets': bound_field_sets, - 'first_form_field_id': first_form_field_id, - 'javascript_imports': get_javascript_imports(opts, auto_populated_fields, field_sets), - 'ordered_objects': ordered_objects, - 'inline_related_objects': inline_related_objects, - 'form_url': mark_safe(form_url), - 'opts': opts, - 'content_type_id': ContentType.objects.get_for_model(model).id, - } - context.update(extra_context) - return render_to_response([ - "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()), - "admin/%s/change_form.html" % app_label, - "admin/change_form.html"], context_instance=context) - -def index(request): - return render_to_response('admin/index.html', {'title': _('Site administration')}, context_instance=template.RequestContext(request)) -index = staff_member_required(never_cache(index)) - -def add_stage(request, app_label, model_name, show_delete=False, form_url='', post_url=None, post_url_continue='../%s/', object_id_override=None): - model = models.get_model(app_label, model_name) - if model is None: - raise Http404("App %r, model %r, not found" % (app_label, model_name)) - opts = model._meta - - if not request.user.has_perm(app_label + '.' + opts.get_add_permission()): - raise PermissionDenied - - if post_url is None: - if request.user.has_perm(app_label + '.' + opts.get_change_permission()): - # redirect to list view - post_url = '../' - else: - # Object list will give 'Permission Denied', so go back to admin home - post_url = '../../../' - - manipulator = model.AddManipulator() - if request.POST: - new_data = request.POST.copy() - - if opts.has_field_type(models.FileField): - new_data.update(request.FILES) - - errors = manipulator.get_validation_errors(new_data) - manipulator.do_html2python(new_data) - - if not errors: - new_object = manipulator.save(new_data) - pk_value = new_object._get_pk_val() - LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, force_unicode(new_object), ADDITION) - msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(new_object)} - # Here, we distinguish between different save types by checking for - # the presence of keys in request.POST. - if "_continue" in request.POST: - request.user.message_set.create(message=msg + ' ' + _("You may edit it again below.")) - if "_popup" in request.POST: - post_url_continue += "?_popup=1" - return HttpResponseRedirect(post_url_continue % pk_value) - if "_popup" in request.POST: - return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \ - # escape() calls force_unicode. - (escape(pk_value), escape(new_object))) - elif "_addanother" in request.POST: - request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name))) - return HttpResponseRedirect(request.path) - else: - request.user.message_set.create(message=msg) - return HttpResponseRedirect(post_url) - else: - # Add default data. - new_data = manipulator.flatten_data() - - # Override the defaults with GET params, if they exist. - new_data.update(dict(request.GET.items())) - - errors = {} - - # Populate the FormWrapper. - form = oldforms.FormWrapper(manipulator, new_data, errors) - - c = template.RequestContext(request, { - 'title': _('Add %s') % force_unicode(opts.verbose_name), - 'form': form, - 'is_popup': '_popup' in request.REQUEST, - 'show_delete': show_delete, - }) - - if object_id_override is not None: - c['object_id'] = object_id_override - - return render_change_form(model, manipulator, c, add=True) -add_stage = staff_member_required(never_cache(add_stage)) - -def change_stage(request, app_label, model_name, object_id): - model = models.get_model(app_label, model_name) - object_id = unquote(object_id) - if model is None: - raise Http404("App %r, model %r, not found" % (app_label, model_name)) - opts = model._meta - - if not request.user.has_perm(app_label + '.' + opts.get_change_permission()): - raise PermissionDenied - - if request.POST and "_saveasnew" in request.POST: - return add_stage(request, app_label, model_name, form_url='../../add/') - - try: - manipulator = model.ChangeManipulator(object_id) - except model.DoesNotExist: - raise Http404('%s object with primary key %r does not exist' % (model_name, escape(object_id))) - - if request.POST: - new_data = request.POST.copy() - - if opts.has_field_type(models.FileField): - new_data.update(request.FILES) - - errors = manipulator.get_validation_errors(new_data) - manipulator.do_html2python(new_data) - - if not errors: - new_object = manipulator.save(new_data) - pk_value = new_object._get_pk_val() - - # Construct the change message. - change_message = [] - if manipulator.fields_added: - change_message.append(_('Added %s.') % get_text_list(manipulator.fields_added, _('and'))) - if manipulator.fields_changed: - change_message.append(_('Changed %s.') % get_text_list(manipulator.fields_changed, _('and'))) - if manipulator.fields_deleted: - change_message.append(_('Deleted %s.') % get_text_list(manipulator.fields_deleted, _('and'))) - change_message = ' '.join(change_message) - if not change_message: - change_message = _('No fields changed.') - LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, force_unicode(new_object), CHANGE, change_message) - - msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(new_object)} - if "_continue" in request.POST: - request.user.message_set.create(message=msg + ' ' + _("You may edit it again below.")) - if '_popup' in request.REQUEST: - return HttpResponseRedirect(request.path + "?_popup=1") - else: - return HttpResponseRedirect(request.path) - elif "_saveasnew" in request.POST: - request.user.message_set.create(message=_('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(new_object)}) - return HttpResponseRedirect("../%s/" % pk_value) - elif "_addanother" in request.POST: - request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name))) - return HttpResponseRedirect("../add/") - else: - request.user.message_set.create(message=msg) - return HttpResponseRedirect("../") - else: - # Populate new_data with a "flattened" version of the current data. - new_data = manipulator.flatten_data() - - # TODO: do this in flatten_data... - # If the object has ordered objects on its admin page, get the existing - # order and flatten it into a comma-separated list of IDs. - - id_order_list = [] - for rel_obj in opts.get_ordered_objects(): - id_order_list.extend(getattr(manipulator.original_object, 'get_%s_order' % rel_obj.object_name.lower())()) - if id_order_list: - new_data['order_'] = ','.join(map(str, id_order_list)) - errors = {} - - # Populate the FormWrapper. - form = oldforms.FormWrapper(manipulator, new_data, errors) - form.original = manipulator.original_object - form.order_objects = [] - - #TODO Should be done in flatten_data / FormWrapper construction - for related in opts.get_followed_related_objects(): - wrt = related.opts.order_with_respect_to - if wrt and wrt.rel and wrt.rel.to == opts: - func = getattr(manipulator.original_object, 'get_%s_list' % - related.get_accessor_name()) - orig_list = func() - form.order_objects.extend(orig_list) - - c = template.RequestContext(request, { - 'title': _('Change %s') % force_unicode(opts.verbose_name), - 'form': form, - 'object_id': object_id, - 'original': manipulator.original_object, - 'is_popup': '_popup' in request.REQUEST, - }) - return render_change_form(model, manipulator, c, change=True) -change_stage = staff_member_required(never_cache(change_stage)) - -def _nest_help(obj, depth, val): - current = obj - for i in range(depth): - current = current[-1] - current.append(val) - -def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth): - "Helper function that recursively populates deleted_objects." - nh = _nest_help # Bind to local variable for performance - if current_depth > 16: - return # Avoid recursing too deep. - opts_seen = [] - for related in opts.get_all_related_objects(): - if related.opts in opts_seen: - continue - opts_seen.append(related.opts) - rel_opts_name = related.get_accessor_name() - if isinstance(related.field.rel, models.OneToOneRel): - try: - sub_obj = getattr(obj, rel_opts_name) - except ObjectDoesNotExist: - pass - else: - if related.opts.admin: - p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission()) - if not user.has_perm(p): - perms_needed.add(related.opts.verbose_name) - # We don't care about populating deleted_objects now. - continue - if related.field.rel.edit_inline or not related.opts.admin: - # Don't display link to edit, because it either has no - # admin or is edited inline. - nh(deleted_objects, current_depth, [mark_safe(u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), sub_obj)), []]) - else: - # Display a link to the admin page. - nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % - (escape(force_unicode(capfirst(related.opts.verbose_name))), - related.opts.app_label, - related.opts.object_name.lower(), - sub_obj._get_pk_val(), sub_obj)), []]) - _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2) - else: - has_related_objs = False - for sub_obj in getattr(obj, rel_opts_name).all(): - has_related_objs = True - if related.field.rel.edit_inline or not related.opts.admin: - # Don't display link to edit, because it either has no - # admin or is edited inline. - nh(deleted_objects, current_depth, [u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), escape(sub_obj)), []]) - else: - # Display a link to the admin page. - nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % \ - (escape(force_unicode(capfirst(related.opts.verbose_name))), related.opts.app_label, related.opts.object_name.lower(), sub_obj._get_pk_val(), escape(sub_obj))), []]) - _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2) - # If there were related objects, and the user doesn't have - # permission to delete them, add the missing perm to perms_needed. - if related.opts.admin and has_related_objs: - p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission()) - if not user.has_perm(p): - perms_needed.add(related.opts.verbose_name) - for related in opts.get_all_related_many_to_many_objects(): - if related.opts in opts_seen: - continue - opts_seen.append(related.opts) - rel_opts_name = related.get_accessor_name() - has_related_objs = False - - # related.get_accessor_name() could return None for symmetrical relationships - if rel_opts_name: - rel_objs = getattr(obj, rel_opts_name, None) - if rel_objs: - has_related_objs = True - - if has_related_objs: - for sub_obj in rel_objs.all(): - if related.field.rel.edit_inline or not related.opts.admin: - # Don't display link to edit, because it either has no - # admin or is edited inline. - nh(deleted_objects, current_depth, [_('One or more %(fieldname)s in %(name)s: %(obj)s') % \ - {'fieldname': force_unicode(related.field.verbose_name), 'name': force_unicode(related.opts.verbose_name), 'obj': escape(sub_obj)}, []]) - else: - # Display a link to the admin page. - nh(deleted_objects, current_depth, [ - mark_safe((_('One or more %(fieldname)s in %(name)s:') % {'fieldname': escape(force_unicode(related.field.verbose_name)), 'name': escape(force_unicode(related.opts.verbose_name))}) + \ - (u' <a href="../../../../%s/%s/%s/">%s</a>' % \ - (related.opts.app_label, related.opts.module_name, sub_obj._get_pk_val(), escape(sub_obj)))), []]) - # If there were related objects, and the user doesn't have - # permission to change them, add the missing perm to perms_needed. - if related.opts.admin and has_related_objs: - p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission()) - if not user.has_perm(p): - perms_needed.add(related.opts.verbose_name) - -def delete_stage(request, app_label, model_name, object_id): - model = models.get_model(app_label, model_name) - object_id = unquote(object_id) - if model is None: - raise Http404("App %r, model %r, not found" % (app_label, model_name)) - opts = model._meta - if not request.user.has_perm(app_label + '.' + opts.get_delete_permission()): - raise PermissionDenied - obj = get_object_or_404(model, pk=object_id) - - # Populate deleted_objects, a data structure of all related objects that - # will also be deleted. - deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), force_unicode(object_id), escape(obj))), []] - perms_needed = set() - _get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1) - - if request.POST: # The user has already confirmed the deletion. - if perms_needed: - raise PermissionDenied - obj_display = force_unicode(obj) - obj.delete() - LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, object_id, obj_display, DELETION) - request.user.message_set.create(message=_('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': obj_display}) - return HttpResponseRedirect("../../") - extra_context = { - "title": _("Are you sure?"), - "object_name": force_unicode(opts.verbose_name), - "object": obj, - "deleted_objects": deleted_objects, - "perms_lacking": perms_needed, - "opts": model._meta, - } - return render_to_response(["admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower() ), - "admin/%s/delete_confirmation.html" % app_label , - "admin/delete_confirmation.html"], extra_context, context_instance=template.RequestContext(request)) -delete_stage = staff_member_required(never_cache(delete_stage)) - -def history(request, app_label, model_name, object_id): - model = models.get_model(app_label, model_name) - object_id = unquote(object_id) - if model is None: - raise Http404("App %r, model %r, not found" % (app_label, model_name)) - action_list = LogEntry.objects.filter(object_id=object_id, - content_type__id__exact=ContentType.objects.get_for_model(model).id).select_related().order_by('action_time') - # If no history was found, see whether this object even exists. - obj = get_object_or_404(model, pk=object_id) - extra_context = { - 'title': _('Change history: %s') % obj, - 'action_list': action_list, - 'module_name': force_unicode(capfirst(model._meta.verbose_name_plural)), - 'object': obj, - } - return render_to_response(["admin/%s/%s/object_history.html" % (app_label, model._meta.object_name.lower()), - "admin/%s/object_history.html" % app_label , - "admin/object_history.html"], extra_context, context_instance=template.RequestContext(request)) -history = staff_member_required(never_cache(history)) - class ChangeList(object): - def __init__(self, request, model): + def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, model_admin): self.model = model self.opts = model._meta self.lookup_opts = self.opts - self.manager = self.opts.admin.manager + self.root_query_set = model_admin.queryset(request) + self.list_display = list_display + self.list_display_links = list_display_links + self.list_filter = list_filter + self.date_hierarchy = date_hierarchy + self.search_fields = search_fields + self.list_select_related = list_select_related + self.list_per_page = list_per_page + self.model_admin = model_admin # Get search parameters from the query string. try: @@ -580,17 +63,16 @@ class ChangeList(object): self.query = request.GET.get(SEARCH_VAR, '') self.query_set = self.get_query_set() self.get_results(request) - self.title = (self.is_popup and _('Select %s') % force_unicode(self.opts.verbose_name) or _('Select %s to change') % force_unicode(self.opts.verbose_name)) + self.title = (self.is_popup and ugettext('Select %s') % force_unicode(self.opts.verbose_name) or ugettext('Select %s to change') % force_unicode(self.opts.verbose_name)) self.filter_specs, self.has_filters = self.get_filters(request) self.pk_attname = self.lookup_opts.pk.attname def get_filters(self, request): filter_specs = [] - if self.lookup_opts.admin.list_filter and not self.opts.one_to_one_field: - filter_fields = [self.lookup_opts.get_field(field_name) \ - for field_name in self.lookup_opts.admin.list_filter] + if self.list_filter and not self.opts.one_to_one_field: + filter_fields = [self.lookup_opts.get_field(field_name) for field_name in self.list_filter] for f in filter_fields: - spec = FilterSpec.create(f, request, self.params, self.model) + spec = FilterSpec.create(f, request, self.params, self.model, self.model_admin) if spec and spec.has_output(): filter_specs.append(spec) return filter_specs, bool(filter_specs) @@ -604,15 +86,15 @@ class ChangeList(object): if k.startswith(r): del p[k] for k, v in new_params.items(): - if k in p and v is None: - del p[k] - elif v is not None: + if v is None: + if k in p: + del p[k] + else: p[k] = v - return mark_safe('?' + '&'.join([u'%s=%s' % (k, v) for k, v in p.items()]).replace(' ', '%20')) + return '?%s' % urlencode(p) def get_results(self, request): - paginator = Paginator(self.query_set, self.lookup_opts.admin.list_per_page) - + paginator = Paginator(self.query_set, self.list_per_page) # Get the number of objects, with admin filters applied. try: result_count = paginator.count @@ -630,10 +112,10 @@ class ChangeList(object): if not self.query_set.query.where: full_result_count = result_count else: - full_result_count = self.manager.count() + full_result_count = self.root_query_set.count() can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED - multi_page = result_count > self.lookup_opts.admin.list_per_page + multi_page = result_count > self.list_per_page # Get the list of objects to display on this page. if (self.show_all and can_show_all) or not multi_page: @@ -657,7 +139,7 @@ class ChangeList(object): # options, then check the object's default ordering. If neither of # those exist, order descending by ID by default. Finally, look for # manually-specified ordering from the query string. - ordering = lookup_opts.admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name] + ordering = self.model_admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name] if ordering[0].startswith('-'): order_field, order_type = ordering[0][1:], 'desc' @@ -665,14 +147,14 @@ class ChangeList(object): order_field, order_type = ordering[0], 'asc' if ORDER_VAR in params: try: - field_name = lookup_opts.admin.list_display[int(params[ORDER_VAR])] + field_name = self.list_display[int(params[ORDER_VAR])] try: f = lookup_opts.get_field(field_name) except models.FieldDoesNotExist: - # see if field_name is a name of a non-field - # that allows sorting + # See whether field_name is a name of a non-field + # that allows sorting. try: - attr = getattr(lookup_opts.admin.manager.model, field_name) + attr = getattr(self.model, field_name) order_field = attr.admin_order_field except AttributeError: pass @@ -686,7 +168,7 @@ class ChangeList(object): return order_field, order_type def get_query_set(self): - qs = self.manager.get_query_set() + qs = self.root_query_set lookup_params = self.params.copy() # a dictionary of the query string for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR): if i in lookup_params: @@ -703,10 +185,10 @@ class ChangeList(object): # Use select_related() if one of the list_display options is a field # with a relationship. - if self.lookup_opts.admin.list_select_related: + if self.list_select_related: qs = qs.select_related() else: - for field_name in self.lookup_opts.admin.list_display: + for field_name in self.list_display: try: f = self.lookup_opts.get_field(field_name) except models.FieldDoesNotExist: @@ -731,13 +213,17 @@ class ChangeList(object): else: return "%s__icontains" % field_name - if self.lookup_opts.admin.search_fields and self.query: + if self.search_fields and self.query: for bit in self.query.split(): - or_queries = [models.Q(**{construct_search(field_name): bit}) for field_name in self.lookup_opts.admin.search_fields] + or_queries = [models.Q(**{construct_search(field_name): bit}) for field_name in self.search_fields] other_qs = QuerySet(self.model) other_qs.dup_select_related(qs) other_qs = other_qs.filter(reduce(operator.or_, or_queries)) qs = qs & other_qs + for field_name in self.search_fields: + if '__' in field_name: + qs = qs.distinct() + break if self.opts.one_to_one_field: qs = qs.complex_filter(self.opts.one_to_one_field.rel.limit_choices_to) @@ -746,31 +232,3 @@ class ChangeList(object): def url_for_result(self, result): return "%s/" % quote(getattr(result, self.pk_attname)) - -def change_list(request, app_label, model_name): - model = models.get_model(app_label, model_name) - if model is None: - raise Http404("App %r, model %r, not found" % (app_label, model_name)) - if not request.user.has_perm(app_label + '.' + model._meta.get_change_permission()): - raise PermissionDenied - try: - cl = ChangeList(request, model) - except IncorrectLookupParameters: - # Wacky lookup parameters were given, so redirect to the main - # changelist page, without parameters, and pass an 'invalid=1' - # parameter via the query string. If wacky parameters were given and - # the 'invalid=1' parameter was already in the query string, something - # is screwed up with the database, so display an error page. - if ERROR_FLAG in request.GET.keys(): - return render_to_response('admin/invalid_setup.html', {'title': _('Database error')}) - return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1') - c = template.RequestContext(request, { - 'title': cl.title, - 'is_popup': cl.is_popup, - 'cl': cl, - }) - c.update({'has_add_permission': c['perms'][app_label][cl.opts.get_add_permission()]}), - return render_to_response(['admin/%s/%s/change_list.html' % (app_label, cl.opts.object_name.lower()), - 'admin/%s/change_list.html' % app_label, - 'admin/change_list.html'], context_instance=c) -change_list = staff_member_required(never_cache(change_list)) diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py new file mode 100644 index 0000000000..4ae8889ac4 --- /dev/null +++ b/django/contrib/admin/widgets.py @@ -0,0 +1,215 @@ +""" +Form Widget classes specific to the Django admin site. +""" + +import copy + +from django import newforms as forms +from django.newforms.widgets import RadioFieldRenderer +from django.newforms.util import flatatt +from django.utils.datastructures import MultiValueDict +from django.utils.text import capfirst, truncate_words +from django.utils.translation import ugettext as _ +from django.utils.safestring import mark_safe +from django.utils.encoding import force_unicode +from django.conf import settings + +class FilteredSelectMultiple(forms.SelectMultiple): + """ + A SelectMultiple with a JavaScript filter interface. + + Note that the resulting JavaScript assumes that the SelectFilter2.js + library and its dependencies have been loaded in the HTML page. + """ + def __init__(self, verbose_name, is_stacked, attrs=None, choices=()): + self.verbose_name = verbose_name + self.is_stacked = is_stacked + super(FilteredSelectMultiple, self).__init__(attrs, choices) + + def render(self, name, value, attrs=None, choices=()): + from django.conf import settings + output = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)] + output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {') + # TODO: "id_" is hard-coded here. This should instead use the correct + # API to determine the ID dynamically. + output.append(u'SelectFilter.init("id_%s", "%s", %s, "%s"); });</script>\n' % \ + (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), settings.ADMIN_MEDIA_PREFIX)) + return mark_safe(u''.join(output)) + +class AdminDateWidget(forms.TextInput): + class Media: + js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", + settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js") + + def __init__(self, attrs={}): + super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'}) + +class AdminTimeWidget(forms.TextInput): + class Media: + js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", + settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js") + + def __init__(self, attrs={}): + super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'}) + +class AdminSplitDateTime(forms.SplitDateTimeWidget): + """ + A SplitDateTime Widget that has some admin-specific styling. + """ + def __init__(self, attrs=None): + widgets = [AdminDateWidget, AdminTimeWidget] + # Note that we're calling MultiWidget, not SplitDateTimeWidget, because + # we want to define widgets. + forms.MultiWidget.__init__(self, widgets, attrs) + + def format_output(self, rendered_widgets): + return mark_safe(u'<p class="datetime">%s %s<br />%s %s</p>' % \ + (_('Date:'), rendered_widgets[0], _('Time:'), rendered_widgets[1])) + +class AdminRadioFieldRenderer(RadioFieldRenderer): + def render(self): + """Outputs a <ul> for this set of radio fields.""" + return mark_safe(u'<ul%s>\n%s\n</ul>' % ( + flatatt(self.attrs), + u'\n'.join([u'<li>%s</li>' % force_unicode(w) for w in self])) + ) + +class AdminRadioSelect(forms.RadioSelect): + renderer = AdminRadioFieldRenderer + +class AdminFileWidget(forms.FileInput): + """ + A FileField Widget that shows its current value if it has one. + """ + def __init__(self, attrs={}): + super(AdminFileWidget, self).__init__(attrs) + + def render(self, name, value, attrs=None): + from django.conf import settings + output = [] + if value: + output.append('%s <a target="_blank" href="%s%s">%s</a> <br />%s ' % \ + (_('Currently:'), settings.MEDIA_URL, value, value, _('Change:'))) + output.append(super(AdminFileWidget, self).render(name, value, attrs)) + return mark_safe(u''.join(output)) + +class ForeignKeyRawIdWidget(forms.TextInput): + """ + A Widget for displaying ForeignKeys in the "raw_id" interface rather than + in a <select> box. + """ + def __init__(self, rel, attrs=None): + self.rel = rel + super(ForeignKeyRawIdWidget, self).__init__(attrs) + + def render(self, name, value, attrs=None): + from django.conf import settings + related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower()) + if self.rel.limit_choices_to: + url = '?' + '&'.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_) diff --git a/django/contrib/admindocs/__init__.py b/django/contrib/admindocs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/contrib/admindocs/__init__.py diff --git a/django/contrib/admindocs/urls.py b/django/contrib/admindocs/urls.py new file mode 100644 index 0000000000..e7baa76fc0 --- /dev/null +++ b/django/contrib/admindocs/urls.py @@ -0,0 +1,15 @@ +from django.conf.urls.defaults import * +from django.contrib.admindocs import views + +urlpatterns = patterns('', + ('^$', views.doc_index), + ('^bookmarklets/$', views.bookmarklets), + ('^tags/$', views.template_tag_index), + ('^filters/$', views.template_filter_index), + ('^views/$', views.view_index), + ('^views/(?P<view>[^/]+)/$', views.view_detail), + ('^models/$', views.model_index), + ('^models/(?P<app_label>[^\.]+)\.(?P<model_name>[^/]+)/$', views.model_detail), +# ('^templates/$', views.template_index), + ('^templates/(?P<template>.*)/$', views.template_detail), +) diff --git a/django/contrib/admin/utils.py b/django/contrib/admindocs/utils.py index 4a45a622b2..4a45a622b2 100644 --- a/django/contrib/admin/utils.py +++ b/django/contrib/admindocs/utils.py diff --git a/django/contrib/admin/views/doc.py b/django/contrib/admindocs/views.py index 44a27d6cc3..e81293adcb 100644 --- a/django/contrib/admin/views/doc.py +++ b/django/contrib/admindocs/views.py @@ -5,9 +5,9 @@ from django.contrib.admin.views.decorators import staff_member_required from django.db import models from django.shortcuts import render_to_response from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist -from django.http import Http404 +from django.http import Http404, get_host from django.core import urlresolvers -from django.contrib.admin import utils +from django.contrib.admindocs import utils from django.contrib.sites.models import Site from django.utils.translation import ugettext as _ from django.utils.safestring import mark_safe @@ -23,13 +23,18 @@ class GenericSite(object): def doc_index(request): if not utils.docutils_is_available: return missing_docutils_page(request) - return render_to_response('admin_doc/index.html', context_instance=RequestContext(request)) + root_path = re.sub(re.escape('doc/') + '$', '', request.path) + return render_to_response('admin_doc/index.html', { + 'root_path': root_path, + }, context_instance=RequestContext(request)) doc_index = staff_member_required(doc_index) def bookmarklets(request): # Hack! This couples this view to the URL it lives at. admin_root = request.path[:-len('doc/bookmarklets/')] + root_path = re.sub(re.escape('doc/bookmarklets/') + '$', '', request.path) return render_to_response('admin_doc/bookmarklets.html', { + 'root_path': root_path, 'admin_url': mark_safe("%s://%s%s" % (request.is_secure() and 'https' or 'http', request.get_host(), admin_root)), }, context_instance=RequestContext(request)) bookmarklets = staff_member_required(bookmarklets) @@ -61,8 +66,11 @@ def template_tag_index(request): 'meta': metadata, 'library': tag_library, }) - - return render_to_response('admin_doc/template_tag_index.html', {'tags': tags}, context_instance=RequestContext(request)) + root_path = re.sub(re.escape('doc/tags/') + '$', '', request.path) + return render_to_response('admin_doc/template_tag_index.html', { + 'root_path': root_path, + 'tags': tags + }, context_instance=RequestContext(request)) template_tag_index = staff_member_required(template_tag_index) def template_filter_index(request): @@ -92,7 +100,11 @@ def template_filter_index(request): 'meta': metadata, 'library': tag_library, }) - return render_to_response('admin_doc/template_filter_index.html', {'filters': filters}, context_instance=RequestContext(request)) + root_path = re.sub(re.escape('doc/filters/') + '$', '', request.path) + return render_to_response('admin_doc/template_filter_index.html', { + 'root_path': root_path, + 'filters': filters + }, context_instance=RequestContext(request)) template_filter_index = staff_member_required(template_filter_index) def view_index(request): @@ -120,7 +132,11 @@ def view_index(request): 'site': site_obj, 'url': simplify_regex(regex), }) - return render_to_response('admin_doc/view_index.html', {'views': views}, context_instance=RequestContext(request)) + root_path = re.sub(re.escape('doc/views/') + '$', '', request.path) + return render_to_response('admin_doc/view_index.html', { + 'root_path': root_path, + 'views': views + }, context_instance=RequestContext(request)) view_index = staff_member_required(view_index) def view_detail(request, view): @@ -139,7 +155,9 @@ def view_detail(request, view): body = utils.parse_rst(body, 'view', _('view:') + view) for key in metadata: metadata[key] = utils.parse_rst(metadata[key], 'model', _('view:') + view) + root_path = re.sub(re.escape('doc/views/%s/' % view) + '$', '', request.path) return render_to_response('admin_doc/view_detail.html', { + 'root_path': root_path, 'name': view, 'summary': title, 'body': body, @@ -150,15 +168,18 @@ view_detail = staff_member_required(view_detail) def model_index(request): if not utils.docutils_is_available: return missing_docutils_page(request) - m_list = [m._meta for m in models.get_models()] - return render_to_response('admin_doc/model_index.html', {'models': m_list}, context_instance=RequestContext(request)) + root_path = re.sub(re.escape('doc/models/') + '$', '', request.path) + return render_to_response('admin_doc/model_index.html', { + 'root_path': root_path, + 'models': m_list + }, context_instance=RequestContext(request)) model_index = staff_member_required(model_index) def model_detail(request, app_label, model_name): if not utils.docutils_is_available: return missing_docutils_page(request) - + # Get the model class. try: app_mod = models.get_app(app_label) @@ -170,7 +191,7 @@ def model_detail(request, app_label, model_name): model = m break if model is None: - raise Http404, _("Model %(name)r not found in app %(label)r") % {'name': model_name, 'label': app_label} + raise Http404, _("Model %(model_name)r not found in app %(app_label)r") % {'model_name': model_name, 'app_label': app_label} opts = model._meta @@ -182,7 +203,7 @@ def model_detail(request, app_label, model_name): if isinstance(field, models.ForeignKey): data_type = related_object_name = field.rel.to.__name__ app_label = field.rel.to._meta.app_label - verbose = utils.parse_rst((_("the related `%(label)s.%(type)s` object") % {'label': app_label, 'type': data_type}), 'model', _('model:') + data_type) + verbose = utils.parse_rst((_("the related `%(app_label)s.%(data_type)s` object") % {'app_label': app_label, 'data_type': data_type}), 'model', _('model:') + data_type) else: data_type = get_readable_field_data_type(field) verbose = field.verbose_name @@ -213,7 +234,7 @@ def model_detail(request, app_label, model_name): # Gather related objects for rel in opts.get_all_related_objects(): - verbose = _("related `%(label)s.%(name)s` objects") % {'label': rel.opts.app_label, 'name': rel.opts.object_name} + verbose = _("related `%(app_label)s.%(object_name)s` objects") % {'app_label': rel.opts.app_label, 'object_name': rel.opts.object_name} accessor = rel.get_accessor_name() fields.append({ 'name' : "%s.all" % accessor, @@ -225,8 +246,9 @@ def model_detail(request, app_label, model_name): 'data_type' : 'Integer', 'verbose' : utils.parse_rst(_("number of %s") % verbose , 'model', _('model:') + opts.module_name), }) - + root_path = re.sub(re.escape('doc/models/%s.%s/' % (app_label, model_name)) + '$', '', request.path) return render_to_response('admin_doc/model_detail.html', { + 'root_path': root_path, 'name': '%s.%s' % (opts.app_label, opts.object_name), 'summary': _("Fields on %s objects") % opts.object_name, 'description': model.__doc__, @@ -252,7 +274,9 @@ def template_detail(request, template): 'site': site_obj, 'order': list(settings_mod.TEMPLATE_DIRS).index(dir), }) + root_path = re.sub(re.escape('doc/templates/%s/' % template) + '$', '', request.path) return render_to_response('admin_doc/template_detail.html', { + 'root_path': root_path, 'name': template, 'templates': templates, }, context_instance=RequestContext(request)) diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py new file mode 100644 index 0000000000..998692a6cb --- /dev/null +++ b/django/contrib/auth/admin.py @@ -0,0 +1,66 @@ +from django.contrib.auth.models import User, Group +from django.core.exceptions import PermissionDenied +from django import oldforms, template +from django.shortcuts import render_to_response +from django.http import HttpResponseRedirect +from django.utils.translation import ugettext, ugettext_lazy as _ +from django.contrib import admin + +class GroupAdmin(admin.ModelAdmin): + search_fields = ('name',) + ordering = ('name',) + filter_horizontal = ('permissions',) + +class UserAdmin(admin.ModelAdmin): + fieldsets = ( + (None, {'fields': ('username', 'password')}), + (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}), + (_('Permissions'), {'fields': ('is_staff', 'is_active', 'is_superuser', 'user_permissions')}), + (_('Important dates'), {'fields': ('last_login', 'date_joined')}), + (_('Groups'), {'fields': ('groups',)}), + ) + list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') + list_filter = ('is_staff', 'is_superuser') + search_fields = ('username', 'first_name', 'last_name', 'email') + ordering = ('username',) + filter_horizontal = ('user_permissions',) + + def add_view(self, request): + # avoid a circular import. see #6718. + from django.contrib.auth.forms import UserCreationForm + if not self.has_change_permission(request): + raise PermissionDenied + if request.method == 'POST': + form = UserCreationForm(request.POST) + if form.is_valid(): + new_user = form.save() + msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': 'user', 'obj': new_user} + if "_addanother" in request.POST: + request.user.message_set.create(message=msg) + return HttpResponseRedirect(request.path) + else: + request.user.message_set.create(message=msg + ' ' + ugettext("You may edit it again below.")) + return HttpResponseRedirect('../%s/' % new_user.id) + else: + form = UserCreationForm() + return render_to_response('admin/auth/user/add_form.html', { + 'title': _('Add user'), + 'form': form, + 'is_popup': '_popup' in request.REQUEST, + 'add': True, + 'change': False, + 'has_add_permission': True, + 'has_delete_permission': False, + 'has_change_permission': True, + 'has_file_field': False, + 'has_absolute_url': False, + 'auto_populated_fields': (), + 'opts': User._meta, + 'save_as': False, + 'username_help_text': User._meta.get_field('username').help_text, + 'root_path': self.admin_site.root_path, + }, context_instance=template.RequestContext(request)) + +admin.site.register(Group, GroupAdmin) +admin.site.register(User, UserAdmin) + diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 47a974cacd..f63dc7b854 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -3,88 +3,106 @@ from django.contrib.auth import authenticate from django.contrib.sites.models import Site from django.template import Context, loader from django.core import validators -from django import oldforms -from django.utils.translation import ugettext as _ +from django import newforms as forms +from django.utils.translation import ugettext_lazy as _ -class UserCreationForm(oldforms.Manipulator): - "A form that creates a user, with no privileges, from the given username and password." - def __init__(self): - self.fields = ( - oldforms.TextField(field_name='username', length=30, max_length=30, is_required=True, - validator_list=[validators.isAlphaNumeric, self.isValidUsername]), - oldforms.PasswordField(field_name='password1', length=30, max_length=60, is_required=True), - oldforms.PasswordField(field_name='password2', length=30, max_length=60, is_required=True, - validator_list=[validators.AlwaysMatchesOtherField('password1', _("The two password fields didn't match."))]), - ) - - def isValidUsername(self, field_data, all_data): +class UserCreationForm(forms.ModelForm): + """ + A form that creates a user, with no privileges, from the given username and password. + """ + username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^\w+$', + help_text = _("Required. 30 characters or fewer. Alphanumeric characters only (letters, digits and underscores)."), + error_message = _("This value must contain only letters, numbers and underscores.")) + password1 = forms.CharField(label=_("Password"), max_length=60, widget=forms.PasswordInput) + password2 = forms.CharField(label=_("Password confirmation"), max_length=60, widget=forms.PasswordInput) + + class Meta: + model = User + fields = ("username",) + + def clean_username(self): + username = self.cleaned_data["username"] try: - User.objects.get(username=field_data) + User.objects.get(username=username) except User.DoesNotExist: - return - raise validators.ValidationError, _('A user with that username already exists.') - - def save(self, new_data): - "Creates the user." - return User.objects.create_user(new_data['username'], '', new_data['password1']) + return username + raise forms.ValidationError(_("A user with that username already exists.")) + + def clean_password2(self): + password1 = self.cleaned_data["password1"] + password2 = self.cleaned_data["password2"] + if password1 != password2: + raise forms.ValidationError(_("The two password fields didn't match.")) + return password2 + + def save(self, commit=True): + user = super(UserCreationForm, self).save(commit=False) + user.set_password(self.cleaned_data["password1"]) + if commit: + user.save() + return user -class AuthenticationForm(oldforms.Manipulator): +class AuthenticationForm(forms.Form): """ Base class for authenticating users. Extend this to get a form that accepts username/password logins. """ - def __init__(self, request=None): + username = forms.CharField(label=_("Username"), max_length=30) + password = forms.CharField(label=_("Password"), max_length=30, widget=forms.PasswordInput) + + def __init__(self, request=None, *args, **kwargs): """ - If request is passed in, the manipulator will validate that cookies are + If request is passed in, the form will validate that cookies are enabled. Note that the request (a HttpRequest object) must have set a cookie with the key TEST_COOKIE_NAME and value TEST_COOKIE_VALUE before - running this validator. + running this validation. """ self.request = request - self.fields = [ - oldforms.TextField(field_name="username", length=15, max_length=30, is_required=True, - validator_list=[self.isValidUser, self.hasCookiesEnabled]), - oldforms.PasswordField(field_name="password", length=15, max_length=30, is_required=True), - ] self.user_cache = None - - def hasCookiesEnabled(self, field_data, all_data): - if self.request and not self.request.session.test_cookie_worked(): - raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.") - - def isValidUser(self, field_data, all_data): - username = field_data - password = all_data.get('password', None) - self.user_cache = authenticate(username=username, password=password) - if self.user_cache is None: - raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.") - elif not self.user_cache.is_active: - raise validators.ValidationError, _("This account is inactive.") - + super(AuthenticationForm, self).__init__(*args, **kwargs) + + def clean(self): + username = self.cleaned_data.get('username') + password = self.cleaned_data.get('password') + + if username and password: + self.user_cache = authenticate(username=username, password=password) + if self.user_cache is None: + raise forms.ValidationError(_("Please enter a correct username and password. Note that both fields are case-sensitive.")) + elif not self.user_cache.is_active: + raise forms.ValidationError(_("This account is inactive.")) + + # TODO: determine whether this should move to its own method. + if self.request: + if not self.request.session.test_cookie_worked(): + raise forms.ValidationError(_("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.")) + + return self.cleaned_data + def get_user_id(self): if self.user_cache: return self.user_cache.id return None - + def get_user(self): return self.user_cache -class PasswordResetForm(oldforms.Manipulator): - "A form that lets a user request a password reset" - def __init__(self): - self.fields = ( - oldforms.EmailField(field_name="email", length=40, is_required=True, - validator_list=[self.isValidUserEmail]), - ) - - def isValidUserEmail(self, new_data, all_data): - "Validates that a user exists with the given e-mail address" - self.users_cache = list(User.objects.filter(email__iexact=new_data)) +class PasswordResetForm(forms.Form): + email = forms.EmailField(label=_("E-mail"), max_length=40) + + def clean_email(self): + """ + Validates that a user exists with the given e-mail address. + """ + email = self.cleaned_data["email"] + self.users_cache = User.objects.filter(email__iexact=email) if len(self.users_cache) == 0: - raise validators.ValidationError, _("That e-mail address doesn't have an associated user account. Are you sure you've registered?") - + raise forms.ValidationError(_("That e-mail address doesn't have an associated user account. Are you sure you've registered?")) + def save(self, domain_override=None, email_template_name='registration/password_reset_email.html'): - "Calculates a new password randomly and sends it to the user" + """ + Calculates a new password randomly and sends it to the user. + """ from django.core.mail import send_mail for user in self.users_cache: new_pass = User.objects.make_random_password() @@ -103,42 +121,69 @@ class PasswordResetForm(oldforms.Manipulator): 'domain': domain, 'site_name': site_name, 'user': user, - } - send_mail(_('Password reset on %s') % site_name, t.render(Context(c)), None, [user.email]) + } + send_mail(_("Password reset on %s") % site_name, + t.render(Context(c)), None, [user.email]) -class PasswordChangeForm(oldforms.Manipulator): - "A form that lets a user change his password." - def __init__(self, user): +class PasswordChangeForm(forms.Form): + """ + A form that lets a user change his/her password. + """ + old_password = forms.CharField(label=_("Old password"), max_length=30, widget=forms.PasswordInput) + new_password1 = forms.CharField(label=_("New password"), max_length=30, widget=forms.PasswordInput) + new_password2 = forms.CharField(label=_("New password confirmation"), max_length=30, widget=forms.PasswordInput) + + def __init__(self, user, *args, **kwargs): self.user = user - self.fields = ( - oldforms.PasswordField(field_name="old_password", length=30, max_length=30, is_required=True, - validator_list=[self.isValidOldPassword]), - oldforms.PasswordField(field_name="new_password1", length=30, max_length=30, is_required=True, - validator_list=[validators.AlwaysMatchesOtherField('new_password2', _("The two 'new password' fields didn't match."))]), - oldforms.PasswordField(field_name="new_password2", length=30, max_length=30, is_required=True), - ) - - def isValidOldPassword(self, new_data, all_data): - "Validates that the old_password field is correct." - if not self.user.check_password(new_data): - raise validators.ValidationError, _("Your old password was entered incorrectly. Please enter it again.") - - def save(self, new_data): - "Saves the new password." - self.user.set_password(new_data['new_password1']) - self.user.save() + super(PasswordChangeForm, self).__init__(*args, **kwargs) + + def clean_old_password(self): + """ + Validates that the old_password field is correct. + """ + old_password = self.cleaned_data["old_password"] + if not self.user.check_password(old_password): + raise forms.ValidationError(_("Your old password was entered incorrectly. Please enter it again.")) + return old_password + + def clean_new_password2(self): + password1 = self.cleaned_data.get('new_password1') + password2 = self.cleaned_data.get('new_password2') + if password1 and password2: + if password1 != password2: + raise forms.ValidationError(_("The two password fields didn't match.")) + return password2 + + def save(self, commit=True): + self.user.set_password(self.cleaned_data['new_password1']) + if commit: + self.user.save() + return self.user -class AdminPasswordChangeForm(oldforms.Manipulator): - "A form used to change the password of a user in the admin interface." - def __init__(self, user): +class AdminPasswordChangeForm(forms.Form): + """ + A form used to change the password of a user in the admin interface. + """ + password1 = forms.CharField(label=_("Password"), max_length=60, widget=forms.PasswordInput) + password2 = forms.CharField(label=_("Password (again)"), max_length=60, widget=forms.PasswordInput) + + def __init__(self, user, *args, **kwargs): self.user = user - self.fields = ( - oldforms.PasswordField(field_name='password1', length=30, max_length=60, is_required=True), - oldforms.PasswordField(field_name='password2', length=30, max_length=60, is_required=True, - validator_list=[validators.AlwaysMatchesOtherField('password1', _("The two password fields didn't match."))]), - ) - - def save(self, new_data): - "Saves the new password." - self.user.set_password(new_data['password1']) - self.user.save() + super(AdminPasswordChangeForm, self).__init__(*args, **kwargs) + + def clean_password2(self): + password1 = self.cleaned_data.get('password1') + password2 = self.cleaned_data.get('password2') + if password1 and password2: + if password1 != password2: + raise forms.ValidationError(_("The two password fields didn't match.")) + return password2 + + def save(self, commit=True): + """ + Saves the new password. + """ + self.user.set_password(self.cleaned_data["password1"]) + if commit: + self.user.save() + return self.user diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 379a9f4c64..a0ed4f366f 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -91,16 +91,12 @@ class Group(models.Model): Beyond permissions, groups are a convenient way to categorize users to apply some label, or extended functionality, to them. For example, you could create a group 'Special users', and you could write code that would do special things to those users -- such as giving them access to a members-only portion of your site, or sending them members-only e-mail messages. """ name = models.CharField(_('name'), max_length=80, unique=True) - permissions = models.ManyToManyField(Permission, verbose_name=_('permissions'), blank=True, filter_interface=models.HORIZONTAL) + permissions = models.ManyToManyField(Permission, verbose_name=_('permissions'), blank=True) class Meta: verbose_name = _('group') verbose_name_plural = _('groups') - - class Admin: - search_fields = ('name',) - ordering = ('name',) - + def __unicode__(self): return self.name @@ -147,26 +143,13 @@ class User(models.Model): date_joined = models.DateTimeField(_('date joined'), default=datetime.datetime.now) groups = models.ManyToManyField(Group, verbose_name=_('groups'), blank=True, help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in.")) - user_permissions = models.ManyToManyField(Permission, verbose_name=_('user permissions'), blank=True, filter_interface=models.HORIZONTAL) + user_permissions = models.ManyToManyField(Permission, verbose_name=_('user permissions'), blank=True) objects = UserManager() class Meta: verbose_name = _('user') verbose_name_plural = _('users') - - class Admin: - fields = ( - (None, {'fields': ('username', 'password')}), - (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}), - (_('Permissions'), {'fields': ('is_staff', 'is_active', 'is_superuser', 'user_permissions')}), - (_('Important dates'), {'fields': ('last_login', 'date_joined')}), - (_('Groups'), {'fields': ('groups',)}), - ) - list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') - list_filter = ('is_staff', 'is_superuser') - search_fields = ('username', 'first_name', 'last_name', 'email') - ordering = ('username',) - + def __unicode__(self): return self.username diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py new file mode 100644 index 0000000000..6242303f46 --- /dev/null +++ b/django/contrib/auth/tests/__init__.py @@ -0,0 +1,8 @@ +from django.contrib.auth.tests.basic import BASIC_TESTS, PasswordResetTest +from django.contrib.auth.tests.forms import FORM_TESTS + +__test__ = { + 'BASIC_TESTS': BASIC_TESTS, + 'PASSWORDRESET_TESTS': PasswordResetTest, + 'FORM_TESTS': FORM_TESTS, +} diff --git a/django/contrib/auth/tests.py b/django/contrib/auth/tests/basic.py index ea1ac26c21..76dbdc9cb9 100644 --- a/django/contrib/auth/tests.py +++ b/django/contrib/auth/tests/basic.py @@ -1,4 +1,5 @@ -""" + +BASIC_TESTS = """ >>> from django.contrib.auth.models import User, AnonymousUser >>> u = User.objects.create_user('testuser', 'test@example.com', 'testpw') >>> u.has_usable_password() @@ -60,14 +61,15 @@ from django.core import mail class PasswordResetTest(TestCase): fixtures = ['authtestdata.json'] urls = 'django.contrib.auth.urls' + def test_email_not_found(self): "Error is raised if the provided email address isn't currently registered" response = self.client.get('/password_reset/') self.assertEquals(response.status_code, 200) response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'}) - self.assertContains(response, "That e-mail address doesn't have an associated user account") + self.assertContains(response, "That e-mail address doesn't have an associated user account") self.assertEquals(len(mail.outbox), 0) - + def test_email_found(self): "Email is sent if a valid email address is provided for password reset" response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py new file mode 100644 index 0000000000..1e1e0a95d4 --- /dev/null +++ b/django/contrib/auth/tests/forms.py @@ -0,0 +1,135 @@ + +FORM_TESTS = """ +>>> from django.contrib.auth.models import User +>>> from django.contrib.auth.forms import UserCreationForm, AuthenticationForm +>>> from django.contrib.auth.forms import PasswordChangeForm + +The user already exists. + +>>> user = User.objects.create_user("jsmith", "jsmith@example.com", "test123") +>>> data = { +... 'username': 'jsmith', +... 'password1': 'test123', +... 'password2': 'test123', +... } +>>> form = UserCreationForm(data) +>>> form.is_valid() +False +>>> form["username"].errors +[u'A user with that username already exists.'] + +The username contains invalid data. + +>>> data = { +... 'username': 'jsmith@example.com', +... 'password1': 'test123', +... 'password2': 'test123', +... } +>>> form = UserCreationForm(data) +>>> form.is_valid() +False +>>> form["username"].errors +[u'This value must contain only letters, numbers and underscores.'] + +The verification password is incorrect. + +>>> data = { +... 'username': 'jsmith2', +... 'password1': 'test123', +... 'password2': 'test', +... } +>>> form = UserCreationForm(data) +>>> form.is_valid() +False +>>> form["password2"].errors +[u"The two password fields didn't match."] + +The success case. + +>>> data = { +... 'username': 'jsmith2', +... 'password1': 'test123', +... 'password2': 'test123', +... } +>>> form = UserCreationForm(data) +>>> form.is_valid() +True +>>> form.save() +<User: jsmith2> + +The user submits an invalid username. + +>>> data = { +... 'username': 'jsmith_does_not_exist', +... 'password': 'test123', +... } + +>>> form = AuthenticationForm(None, data) +>>> form.is_valid() +False +>>> form.non_field_errors() +[u'Please enter a correct username and password. Note that both fields are case-sensitive.'] + +The user is inactive. + +>>> data = { +... 'username': 'jsmith', +... 'password': 'test123', +... } +>>> user.is_active = False +>>> user.save() +>>> form = AuthenticationForm(None, data) +>>> form.is_valid() +False +>>> form.non_field_errors() +[u'This account is inactive.'] + +>>> user.is_active = True +>>> user.save() + +The success case + +>>> form = AuthenticationForm(None, data) +>>> form.is_valid() +True +>>> form.non_field_errors() +[] + +The old password is incorrect. + +>>> data = { +... 'old_password': 'test', +... 'new_password1': 'abc123', +... 'new_password2': 'abc123', +... } +>>> form = PasswordChangeForm(user, data) +>>> form.is_valid() +False +>>> form["old_password"].errors +[u'Your old password was entered incorrectly. Please enter it again.'] + +The two new passwords do not match. + +>>> data = { +... 'old_password': 'test123', +... 'new_password1': 'abc123', +... 'new_password2': 'abc', +... } +>>> form = PasswordChangeForm(user, data) +>>> form.is_valid() +False +>>> form["new_password2"].errors +[u"The two password fields didn't match."] + +The success case. + +>>> data = { +... 'old_password': 'test123', +... 'new_password1': 'abc123', +... 'new_password2': 'abc123', +... } +>>> form = PasswordChangeForm(user, data) +>>> form.is_valid() +True + +""" diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 524710327a..0a52240631 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -1,42 +1,42 @@ -from django import oldforms from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import AuthenticationForm -from django.contrib.auth.forms import PasswordResetForm, PasswordChangeForm +from django.contrib.auth.forms import PasswordResetForm, PasswordChangeForm, AdminPasswordChangeForm +from django.core.exceptions import PermissionDenied +from django.shortcuts import render_to_response, get_object_or_404 from django.contrib.sites.models import Site, RequestSite from django.http import HttpResponseRedirect -from django.shortcuts import render_to_response from django.template import RequestContext from django.utils.http import urlquote +from django.utils.html import escape from django.utils.translation import ugettext as _ +from django.contrib.auth.models import User +import re def login(request, template_name='registration/login.html', redirect_field_name=REDIRECT_FIELD_NAME): "Displays the login form and handles the login action." - manipulator = AuthenticationForm() redirect_to = request.REQUEST.get(redirect_field_name, '') - if request.POST: - errors = manipulator.get_validation_errors(request.POST) - if not errors: + if request.method == "POST": + form = AuthenticationForm(data=request.POST) + if form.is_valid(): # Light security check -- make sure redirect_to isn't garbage. if not redirect_to or '//' in redirect_to or ' ' in redirect_to: from django.conf import settings redirect_to = settings.LOGIN_REDIRECT_URL from django.contrib.auth import login - login(request, manipulator.get_user()) + login(request, form.get_user()) if request.session.test_cookie_worked(): request.session.delete_test_cookie() return HttpResponseRedirect(redirect_to) else: - errors = {} + form = AuthenticationForm(request) request.session.set_test_cookie() - if Site._meta.installed: current_site = Site.objects.get_current() else: current_site = RequestSite(request) - return render_to_response(template_name, { - 'form': oldforms.FormWrapper(manipulator, request.POST, errors), + 'form': form, redirect_field_name: redirect_to, 'site_name': current_site.name, }, context_instance=RequestContext(request)) @@ -66,13 +66,11 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N return HttpResponseRedirect('%s?%s=%s' % (login_url, urlquote(redirect_field_name), urlquote(next))) def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html', - email_template_name='registration/password_reset_email.html'): - new_data, errors = {}, {} - form = PasswordResetForm() - if request.POST: - new_data = request.POST.copy() - errors = form.get_validation_errors(new_data) - if not errors: + email_template_name='registration/password_reset_email.html', + password_reset_form=PasswordResetForm): + if request.method == "POST": + form = password_reset_form(request.POST) + if form.is_valid(): if is_admin_site: form.save(domain_override=request.META['HTTP_HOST']) else: @@ -81,24 +79,57 @@ def password_reset(request, is_admin_site=False, template_name='registration/pas else: form.save(domain_override=RequestSite(request).domain, email_template_name=email_template_name) return HttpResponseRedirect('%sdone/' % request.path) - return render_to_response(template_name, {'form': oldforms.FormWrapper(form, new_data, errors)}, - context_instance=RequestContext(request)) + else: + form = password_reset_form() + return render_to_response(template_name, { + 'form': form, + }, context_instance=RequestContext(request)) def password_reset_done(request, template_name='registration/password_reset_done.html'): return render_to_response(template_name, context_instance=RequestContext(request)) def password_change(request, template_name='registration/password_change_form.html'): - new_data, errors = {}, {} - form = PasswordChangeForm(request.user) - if request.POST: - new_data = request.POST.copy() - errors = form.get_validation_errors(new_data) - if not errors: - form.save(new_data) + if request.method == "POST": + form = PasswordChangeForm(request.user, request.POST) + if form.is_valid(): + form.save() return HttpResponseRedirect('%sdone/' % request.path) - return render_to_response(template_name, {'form': oldforms.FormWrapper(form, new_data, errors)}, - context_instance=RequestContext(request)) + else: + form = PasswordChangeForm(request.user) + return render_to_response(template_name, { + 'form': form, + }, context_instance=RequestContext(request)) password_change = login_required(password_change) def password_change_done(request, template_name='registration/password_change_done.html'): return render_to_response(template_name, context_instance=RequestContext(request)) + +# TODO: move to admin.py in the ModelAdmin +def user_change_password(request, id): + if not request.user.has_perm('auth.change_user'): + raise PermissionDenied + user = get_object_or_404(User, pk=id) + if request.method == 'POST': + form = AdminPasswordChangeForm(user, request.POST) + if form.is_valid(): + new_user = form.save() + msg = _('Password changed successfully.') + request.user.message_set.create(message=msg) + return HttpResponseRedirect('..') + else: + form = AdminPasswordChangeForm(user) + return render_to_response('admin/auth/user/change_password.html', { + 'title': _('Change password: %s') % escape(user.username), + 'form': form, + 'is_popup': '_popup' in request.REQUEST, + 'add': True, + 'change': False, + 'has_delete_permission': False, + 'has_change_permission': True, + 'has_absolute_url': False, + 'opts': User._meta, + 'original': user, + 'save_as': False, + 'show_save': True, + 'root_path': re.sub('auth/user/(\d+)/password/$', '', request.path), + }, context_instance=RequestContext(request)) diff --git a/django/contrib/comments/admin.py b/django/contrib/comments/admin.py new file mode 100644 index 0000000000..81ecc699c7 --- /dev/null +++ b/django/contrib/comments/admin.py @@ -0,0 +1,30 @@ +from django.contrib import admin +from django.contrib.comments.models import Comment, FreeComment + + +class CommentAdmin(admin.ModelAdmin): + fieldsets = ( + (None, {'fields': ('content_type', 'object_id', 'site')}), + ('Content', {'fields': ('user', 'headline', 'comment')}), + ('Ratings', {'fields': ('rating1', 'rating2', 'rating3', 'rating4', 'rating5', 'rating6', 'rating7', 'rating8', 'valid_rating')}), + ('Meta', {'fields': ('is_public', 'is_removed', 'ip_address')}), + ) + list_display = ('user', 'submit_date', 'content_type', 'get_content_object') + list_filter = ('submit_date',) + date_hierarchy = 'submit_date' + search_fields = ('comment', 'user__username') + raw_id_fields = ('user',) + +class FreeCommentAdmin(admin.ModelAdmin): + fieldsets = ( + (None, {'fields': ('content_type', 'object_id', 'site')}), + ('Content', {'fields': ('person_name', 'comment')}), + ('Meta', {'fields': ('is_public', 'ip_address', 'approved')}), + ) + list_display = ('person_name', 'submit_date', 'content_type', 'get_content_object') + list_filter = ('submit_date',) + date_hierarchy = 'submit_date' + search_fields = ('comment', 'person_name') + +admin.site.register(Comment, CommentAdmin) +admin.site.register(FreeComment, FreeCommentAdmin)
\ No newline at end of file diff --git a/django/contrib/comments/models.py b/django/contrib/comments/models.py index d0c54b85cb..a13fec9e6e 100644 --- a/django/contrib/comments/models.py +++ b/django/contrib/comments/models.py @@ -66,7 +66,7 @@ class CommentManager(models.Manager): class Comment(models.Model): """A comment by a registered user.""" - user = models.ForeignKey(User, raw_id_admin=True) + user = models.ForeignKey(User) content_type = models.ForeignKey(ContentType) object_id = models.IntegerField(_('object ID')) headline = models.CharField(_('headline'), max_length=255, blank=True) @@ -96,18 +96,6 @@ class Comment(models.Model): verbose_name_plural = _('comments') ordering = ('-submit_date',) - class Admin: - fields = ( - (None, {'fields': ('content_type', 'object_id', 'site')}), - ('Content', {'fields': ('user', 'headline', 'comment')}), - ('Ratings', {'fields': ('rating1', 'rating2', 'rating3', 'rating4', 'rating5', 'rating6', 'rating7', 'rating8', 'valid_rating')}), - ('Meta', {'fields': ('is_public', 'is_removed', 'ip_address')}), - ) - list_display = ('user', 'submit_date', 'content_type', 'get_content_object') - list_filter = ('submit_date',) - date_hierarchy = 'submit_date' - search_fields = ('comment', 'user__username') - def __unicode__(self): return "%s: %s..." % (self.user.username, self.comment[:100]) @@ -188,17 +176,6 @@ class FreeComment(models.Model): verbose_name_plural = _('free comments') ordering = ('-submit_date',) - class Admin: - fields = ( - (None, {'fields': ('content_type', 'object_id', 'site')}), - ('Content', {'fields': ('person_name', 'comment')}), - ('Meta', {'fields': ('submit_date', 'is_public', 'ip_address', 'approved')}), - ) - list_display = ('person_name', 'submit_date', 'content_type', 'get_content_object') - list_filter = ('submit_date',) - date_hierarchy = 'submit_date' - search_fields = ('comment', 'person_name') - def __unicode__(self): return "%s: %s..." % (self.person_name, self.comment[:100]) @@ -306,3 +283,4 @@ class ModeratorDeletion(models.Model): def __unicode__(self): return _("Moderator deletion by %r") % self.user +
\ No newline at end of file diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index 67da5759ac..ba59cbafc9 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -1,3 +1,6 @@ +import base64 +import datetime + from django.core import validators from django import oldforms from django.core.mail import mail_admins, mail_managers @@ -7,16 +10,61 @@ from django.shortcuts import render_to_response from django.template import RequestContext from django.contrib.comments.models import Comment, FreeComment, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC from django.contrib.contenttypes.models import ContentType -from django.contrib.auth.forms import AuthenticationForm +from django.contrib.auth import authenticate from django.http import HttpResponseRedirect from django.utils.text import normalize_newlines from django.conf import settings from django.utils.translation import ungettext, ugettext as _ from django.utils.encoding import smart_unicode -import base64, datetime COMMENTS_PER_PAGE = 20 +# TODO: This is a copy of the manipulator-based form that used to live in +# contrib.auth.forms. It should be replaced with the newforms version that +# has now been added to contrib.auth.forms when the comments app gets updated +# for newforms. + +class AuthenticationForm(oldforms.Manipulator): + """ + Base class for authenticating users. Extend this to get a form that accepts + username/password logins. + """ + def __init__(self, request=None): + """ + If request is passed in, the manipulator will validate that cookies are + enabled. Note that the request (a HttpRequest object) must have set a + cookie with the key TEST_COOKIE_NAME and value TEST_COOKIE_VALUE before + running this validator. + """ + self.request = request + self.fields = [ + oldforms.TextField(field_name="username", length=15, max_length=30, is_required=True, + validator_list=[self.isValidUser, self.hasCookiesEnabled]), + oldforms.PasswordField(field_name="password", length=15, max_length=30, is_required=True), + ] + self.user_cache = None + + def hasCookiesEnabled(self, field_data, all_data): + if self.request and not self.request.session.test_cookie_worked(): + raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.") + + def isValidUser(self, field_data, all_data): + username = field_data + password = all_data.get('password', None) + self.user_cache = authenticate(username=username, password=password) + if self.user_cache is None: + raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.") + elif not self.user_cache.is_active: + raise validators.ValidationError, _("This account is inactive.") + + def get_user_id(self): + if self.user_cache: + return self.user_cache.id + return None + + def get_user(self): + return self.user_cache + class PublicCommentManipulator(AuthenticationForm): "Manipulator that handles public registered comments" def __init__(self, user, ratings_required, ratings_range, num_rating_choices): diff --git a/django/contrib/flatpages/admin.py b/django/contrib/flatpages/admin.py new file mode 100644 index 0000000000..02bbaf6b1a --- /dev/null +++ b/django/contrib/flatpages/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin +from django.contrib.flatpages.models import FlatPage +from django.utils.translation import ugettext_lazy as _ + + +class FlatPageAdmin(admin.ModelAdmin): + fieldsets = ( + (None, {'fields': ('url', 'title', 'content', 'sites')}), + (_('Advanced options'), {'classes': ('collapse',), 'fields': ('enable_comments', 'registration_required', 'template_name')}), + ) + list_display = ('url', 'title') + list_filter = ('sites', 'enable_comments', 'registration_required') + search_fields = ('url', 'title') + +admin.site.register(FlatPage, FlatPageAdmin)
\ No newline at end of file diff --git a/django/contrib/flatpages/models.py b/django/contrib/flatpages/models.py index d61e9a3b1c..466425cb46 100644 --- a/django/contrib/flatpages/models.py +++ b/django/contrib/flatpages/models.py @@ -20,16 +20,7 @@ class FlatPage(models.Model): verbose_name = _('flat page') verbose_name_plural = _('flat pages') ordering = ('url',) - - class Admin: - fields = ( - (None, {'fields': ('url', 'title', 'content', 'sites')}), - (_('Advanced options'), {'classes': 'collapse', 'fields': ('enable_comments', 'registration_required', 'template_name')}), - ) - list_display = ('url', 'title') - list_filter = ('sites', 'enable_comments', 'registration_required') - search_fields = ('url', 'title') - + def __unicode__(self): return u"%s -- %s" % (self.url, self.title) diff --git a/django/contrib/redirects/models.py b/django/contrib/redirects/models.py index 1720f33466..991423268d 100644 --- a/django/contrib/redirects/models.py +++ b/django/contrib/redirects/models.py @@ -3,7 +3,7 @@ from django.contrib.sites.models import Site from django.utils.translation import ugettext_lazy as _ class Redirect(models.Model): - site = models.ForeignKey(Site, radio_admin=models.VERTICAL) + site = models.ForeignKey(Site) old_path = models.CharField(_('redirect from'), max_length=200, db_index=True, help_text=_("This should be an absolute path, excluding the domain name. Example: '/events/search/'.")) new_path = models.CharField(_('redirect to'), max_length=200, blank=True, @@ -15,11 +15,21 @@ class Redirect(models.Model): db_table = 'django_redirect' unique_together=(('site', 'old_path'),) ordering = ('old_path',) + + def __unicode__(self): + return "%s ---> %s" % (self.old_path, self.new_path) - class Admin: - list_display = ('old_path', 'new_path') - list_filter = ('site',) - search_fields = ('old_path', 'new_path') +# Register the admin options for these models. +# TODO: Maybe this should live in a separate module admin.py, but how would we +# ensure that module was loaded? + +from django.contrib import admin + +class RedirectAdmin(admin.ModelAdmin): + list_display = ('old_path', 'new_path') + list_filter = ('site',) + search_fields = ('old_path', 'new_path') + radio_fields = {'site': admin.VERTICAL} + +admin.site.register(Redirect, RedirectAdmin) - def __unicode__(self): - return u"%s ---> %s" % (self.old_path, self.new_path) diff --git a/django/contrib/sites/admin.py b/django/contrib/sites/admin.py new file mode 100644 index 0000000000..2442c24292 --- /dev/null +++ b/django/contrib/sites/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from django.contrib.sites.models import Site + + +class SiteAdmin(admin.ModelAdmin): + list_display = ('domain', 'name') + search_fields = ('domain', 'name') + +admin.site.register(Site, SiteAdmin)
\ No newline at end of file diff --git a/django/contrib/sites/models.py b/django/contrib/sites/models.py index c928a4e852..c44e5ce11f 100644 --- a/django/contrib/sites/models.py +++ b/django/contrib/sites/models.py @@ -32,18 +32,16 @@ class Site(models.Model): domain = models.CharField(_('domain name'), max_length=100) name = models.CharField(_('display name'), max_length=50) objects = SiteManager() + class Meta: db_table = 'django_site' verbose_name = _('site') verbose_name_plural = _('sites') ordering = ('domain',) - class Admin: - list_display = ('domain', 'name') - search_fields = ('domain', 'name') def __unicode__(self): return self.domain - + def delete(self): pk = self.pk super(Site, self).delete() @@ -51,7 +49,6 @@ class Site(models.Model): del(SITE_CACHE[pk]) except KeyError: pass - class RequestSite(object): """ |
