root / env / lib / python2.7 / site-packages / django / contrib / admin / options.py @ 1a305335
History | View | Annotate | Download (62 KB)
1 | 1a305335 | officers | from functools import update_wrapper, partial |
---|---|---|---|
2 | from django import forms |
||
3 | from django.conf import settings |
||
4 | from django.forms.formsets import all_valid |
||
5 | from django.forms.models import (modelform_factory, modelformset_factory, |
||
6 | inlineformset_factory, BaseInlineFormSet) |
||
7 | from django.contrib.contenttypes.models import ContentType |
||
8 | from django.contrib.admin import widgets, helpers |
||
9 | from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict |
||
10 | from django.contrib.admin.templatetags.admin_static import static |
||
11 | from django.contrib import messages |
||
12 | from django.views.decorators.csrf import csrf_protect |
||
13 | from django.core.exceptions import PermissionDenied, ValidationError |
||
14 | from django.core.paginator import Paginator |
||
15 | from django.core.urlresolvers import reverse |
||
16 | from django.db import models, transaction, router |
||
17 | from django.db.models.related import RelatedObject |
||
18 | from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist |
||
19 | from django.db.models.sql.constants import LOOKUP_SEP, QUERY_TERMS |
||
20 | from django.http import Http404, HttpResponse, HttpResponseRedirect |
||
21 | from django.shortcuts import get_object_or_404 |
||
22 | from django.template.response import SimpleTemplateResponse, TemplateResponse |
||
23 | from django.utils.decorators import method_decorator |
||
24 | from django.utils.datastructures import SortedDict |
||
25 | from django.utils.html import escape, escapejs |
||
26 | from django.utils.safestring import mark_safe |
||
27 | from django.utils.text import capfirst, get_text_list |
||
28 | from django.utils.translation import ugettext as _ |
||
29 | from django.utils.translation import ungettext |
||
30 | from django.utils.encoding import force_unicode |
||
31 | |||
32 | HORIZONTAL, VERTICAL = 1, 2 |
||
33 | # returns the <ul> class for a given radio_admin field
|
||
34 | get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '') |
||
35 | |||
36 | class IncorrectLookupParameters(Exception): |
||
37 | pass
|
||
38 | |||
39 | # Defaults for formfield_overrides. ModelAdmin subclasses can change this
|
||
40 | # by adding to ModelAdmin.formfield_overrides.
|
||
41 | |||
42 | FORMFIELD_FOR_DBFIELD_DEFAULTS = { |
||
43 | models.DateTimeField: { |
||
44 | 'form_class': forms.SplitDateTimeField,
|
||
45 | 'widget': widgets.AdminSplitDateTime
|
||
46 | }, |
||
47 | models.DateField: {'widget': widgets.AdminDateWidget},
|
||
48 | models.TimeField: {'widget': widgets.AdminTimeWidget},
|
||
49 | models.TextField: {'widget': widgets.AdminTextareaWidget},
|
||
50 | models.URLField: {'widget': widgets.AdminURLFieldWidget},
|
||
51 | models.IntegerField: {'widget': widgets.AdminIntegerFieldWidget},
|
||
52 | models.BigIntegerField: {'widget': widgets.AdminIntegerFieldWidget},
|
||
53 | models.CharField: {'widget': widgets.AdminTextInputWidget},
|
||
54 | models.ImageField: {'widget': widgets.AdminFileWidget},
|
||
55 | models.FileField: {'widget': widgets.AdminFileWidget},
|
||
56 | } |
||
57 | |||
58 | csrf_protect_m = method_decorator(csrf_protect) |
||
59 | |||
60 | class BaseModelAdmin(object): |
||
61 | """Functionality common to both ModelAdmin and InlineAdmin."""
|
||
62 | __metaclass__ = forms.MediaDefiningClass |
||
63 | |||
64 | raw_id_fields = () |
||
65 | fields = None
|
||
66 | exclude = None
|
||
67 | fieldsets = None
|
||
68 | form = forms.ModelForm |
||
69 | filter_vertical = () |
||
70 | filter_horizontal = () |
||
71 | radio_fields = {} |
||
72 | prepopulated_fields = {} |
||
73 | formfield_overrides = {} |
||
74 | readonly_fields = () |
||
75 | ordering = None
|
||
76 | |||
77 | def __init__(self): |
||
78 | overrides = FORMFIELD_FOR_DBFIELD_DEFAULTS.copy() |
||
79 | overrides.update(self.formfield_overrides)
|
||
80 | self.formfield_overrides = overrides
|
||
81 | |||
82 | def formfield_for_dbfield(self, db_field, **kwargs): |
||
83 | """
|
||
84 | Hook for specifying the form Field instance for a given database Field
|
||
85 | instance.
|
||
86 |
|
||
87 | If kwargs are given, they're passed to the form Field's constructor.
|
||
88 | """
|
||
89 | request = kwargs.pop("request", None) |
||
90 | |||
91 | # If the field specifies choices, we don't need to look for special
|
||
92 | # admin widgets - we just need to use a select widget of some kind.
|
||
93 | if db_field.choices:
|
||
94 | return self.formfield_for_choice_field(db_field, request, **kwargs) |
||
95 | |||
96 | # ForeignKey or ManyToManyFields
|
||
97 | if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)): |
||
98 | # Combine the field kwargs with any options for formfield_overrides.
|
||
99 | # Make sure the passed in **kwargs override anything in
|
||
100 | # formfield_overrides because **kwargs is more specific, and should
|
||
101 | # always win.
|
||
102 | if db_field.__class__ in self.formfield_overrides: |
||
103 | kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs) |
||
104 | |||
105 | # Get the correct formfield.
|
||
106 | if isinstance(db_field, models.ForeignKey): |
||
107 | formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
|
||
108 | elif isinstance(db_field, models.ManyToManyField): |
||
109 | formfield = self.formfield_for_manytomany(db_field, request, **kwargs)
|
||
110 | |||
111 | # For non-raw_id fields, wrap the widget with a wrapper that adds
|
||
112 | # extra HTML -- the "add other" interface -- to the end of the
|
||
113 | # rendered output. formfield can be None if it came from a
|
||
114 | # OneToOneField with parent_link=True or a M2M intermediary.
|
||
115 | if formfield and db_field.name not in self.raw_id_fields: |
||
116 | related_modeladmin = self.admin_site._registry.get(
|
||
117 | db_field.rel.to) |
||
118 | can_add_related = bool(related_modeladmin and |
||
119 | related_modeladmin.has_add_permission(request)) |
||
120 | formfield.widget = widgets.RelatedFieldWidgetWrapper( |
||
121 | formfield.widget, db_field.rel, self.admin_site,
|
||
122 | can_add_related=can_add_related) |
||
123 | |||
124 | return formfield
|
||
125 | |||
126 | # If we've got overrides for the formfield defined, use 'em. **kwargs
|
||
127 | # passed to formfield_for_dbfield override the defaults.
|
||
128 | for klass in db_field.__class__.mro(): |
||
129 | if klass in self.formfield_overrides: |
||
130 | kwargs = dict(self.formfield_overrides[klass], **kwargs) |
||
131 | return db_field.formfield(**kwargs)
|
||
132 | |||
133 | # For any other type of field, just call its formfield() method.
|
||
134 | return db_field.formfield(**kwargs)
|
||
135 | |||
136 | def formfield_for_choice_field(self, db_field, request=None, **kwargs): |
||
137 | """
|
||
138 | Get a form Field for a database Field that has declared choices.
|
||
139 | """
|
||
140 | # If the field is named as a radio_field, use a RadioSelect
|
||
141 | if db_field.name in self.radio_fields: |
||
142 | # Avoid stomping on custom widget/choices arguments.
|
||
143 | if 'widget' not in kwargs: |
||
144 | kwargs['widget'] = widgets.AdminRadioSelect(attrs={
|
||
145 | 'class': get_ul_class(self.radio_fields[db_field.name]), |
||
146 | }) |
||
147 | if 'choices' not in kwargs: |
||
148 | kwargs['choices'] = db_field.get_choices(
|
||
149 | include_blank = db_field.blank, |
||
150 | blank_choice=[('', _('None'))] |
||
151 | ) |
||
152 | return db_field.formfield(**kwargs)
|
||
153 | |||
154 | def formfield_for_foreignkey(self, db_field, request=None, **kwargs): |
||
155 | """
|
||
156 | Get a form Field for a ForeignKey.
|
||
157 | """
|
||
158 | db = kwargs.get('using')
|
||
159 | if db_field.name in self.raw_id_fields: |
||
160 | kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel,
|
||
161 | self.admin_site, using=db)
|
||
162 | elif db_field.name in self.radio_fields: |
||
163 | kwargs['widget'] = widgets.AdminRadioSelect(attrs={
|
||
164 | 'class': get_ul_class(self.radio_fields[db_field.name]), |
||
165 | }) |
||
166 | kwargs['empty_label'] = db_field.blank and _('None') or None |
||
167 | |||
168 | return db_field.formfield(**kwargs)
|
||
169 | |||
170 | def formfield_for_manytomany(self, db_field, request=None, **kwargs): |
||
171 | """
|
||
172 | Get a form Field for a ManyToManyField.
|
||
173 | """
|
||
174 | # If it uses an intermediary model that isn't auto created, don't show
|
||
175 | # a field in admin.
|
||
176 | if not db_field.rel.through._meta.auto_created: |
||
177 | return None |
||
178 | db = kwargs.get('using')
|
||
179 | |||
180 | if db_field.name in self.raw_id_fields: |
||
181 | kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel,
|
||
182 | self.admin_site, using=db)
|
||
183 | kwargs['help_text'] = '' |
||
184 | elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)): |
||
185 | kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical)) |
||
186 | |||
187 | return db_field.formfield(**kwargs)
|
||
188 | |||
189 | def _declared_fieldsets(self): |
||
190 | if self.fieldsets: |
||
191 | return self.fieldsets |
||
192 | elif self.fields: |
||
193 | return [(None, {'fields': self.fields})] |
||
194 | return None |
||
195 | declared_fieldsets = property(_declared_fieldsets)
|
||
196 | |||
197 | def get_ordering(self, request): |
||
198 | """
|
||
199 | Hook for specifying field ordering.
|
||
200 | """
|
||
201 | return self.ordering or () # otherwise we might try to *None, which is bad ;) |
||
202 | |||
203 | def get_readonly_fields(self, request, obj=None): |
||
204 | """
|
||
205 | Hook for specifying custom readonly fields.
|
||
206 | """
|
||
207 | return self.readonly_fields |
||
208 | |||
209 | def get_prepopulated_fields(self, request, obj=None): |
||
210 | """
|
||
211 | Hook for specifying custom prepopulated fields.
|
||
212 | """
|
||
213 | return self.prepopulated_fields |
||
214 | |||
215 | def queryset(self, request): |
||
216 | """
|
||
217 | Returns a QuerySet of all model instances that can be edited by the
|
||
218 | admin site. This is used by changelist_view.
|
||
219 | """
|
||
220 | qs = self.model._default_manager.get_query_set()
|
||
221 | # TODO: this should be handled by some parameter to the ChangeList.
|
||
222 | ordering = self.get_ordering(request)
|
||
223 | if ordering:
|
||
224 | qs = qs.order_by(*ordering) |
||
225 | return qs
|
||
226 | |||
227 | def lookup_allowed(self, lookup, value): |
||
228 | model = self.model
|
||
229 | # Check FKey lookups that are allowed, so that popups produced by
|
||
230 | # ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
|
||
231 | # are allowed to work.
|
||
232 | for l in model._meta.related_fkey_lookups: |
||
233 | for k, v in widgets.url_params_from_lookup_dict(l).items(): |
||
234 | if k == lookup and v == value: |
||
235 | return True |
||
236 | |||
237 | parts = lookup.split(LOOKUP_SEP) |
||
238 | |||
239 | # Last term in lookup is a query term (__exact, __startswith etc)
|
||
240 | # This term can be ignored.
|
||
241 | if len(parts) > 1 and parts[-1] in QUERY_TERMS: |
||
242 | parts.pop() |
||
243 | |||
244 | # Special case -- foo__id__exact and foo__id queries are implied
|
||
245 | # if foo has been specificially included in the lookup list; so
|
||
246 | # drop __id if it is the last part. However, first we need to find
|
||
247 | # the pk attribute name.
|
||
248 | rel_name = None
|
||
249 | for part in parts[:-1]: |
||
250 | try:
|
||
251 | field, _, _, _ = model._meta.get_field_by_name(part) |
||
252 | except FieldDoesNotExist:
|
||
253 | # Lookups on non-existants fields are ok, since they're ignored
|
||
254 | # later.
|
||
255 | return True |
||
256 | if hasattr(field, 'rel'): |
||
257 | model = field.rel.to |
||
258 | rel_name = field.rel.get_related_field().name |
||
259 | elif isinstance(field, RelatedObject): |
||
260 | model = field.model |
||
261 | rel_name = model._meta.pk.name |
||
262 | else:
|
||
263 | rel_name = None
|
||
264 | if rel_name and len(parts) > 1 and parts[-1] == rel_name: |
||
265 | parts.pop() |
||
266 | |||
267 | if len(parts) == 1: |
||
268 | return True |
||
269 | clean_lookup = LOOKUP_SEP.join(parts) |
||
270 | return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy |
||
271 | |||
272 | def has_add_permission(self, request): |
||
273 | """
|
||
274 | Returns True if the given request has permission to add an object.
|
||
275 | Can be overriden by the user in subclasses.
|
||
276 | """
|
||
277 | opts = self.opts
|
||
278 | return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission()) |
||
279 | |||
280 | def has_change_permission(self, request, obj=None): |
||
281 | """
|
||
282 | Returns True if the given request has permission to change the given
|
||
283 | Django model instance, the default implementation doesn't examine the
|
||
284 | `obj` parameter.
|
||
285 |
|
||
286 | Can be overriden by the user in subclasses. In such case it should
|
||
287 | return True if the given request has permission to change the `obj`
|
||
288 | model instance. If `obj` is None, this should return True if the given
|
||
289 | request has permission to change *any* object of the given type.
|
||
290 | """
|
||
291 | opts = self.opts
|
||
292 | return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission()) |
||
293 | |||
294 | def has_delete_permission(self, request, obj=None): |
||
295 | """
|
||
296 | Returns True if the given request has permission to change the given
|
||
297 | Django model instance, the default implementation doesn't examine the
|
||
298 | `obj` parameter.
|
||
299 |
|
||
300 | Can be overriden by the user in subclasses. In such case it should
|
||
301 | return True if the given request has permission to delete the `obj`
|
||
302 | model instance. If `obj` is None, this should return True if the given
|
||
303 | request has permission to delete *any* object of the given type.
|
||
304 | """
|
||
305 | opts = self.opts
|
||
306 | return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission()) |
||
307 | |||
308 | class ModelAdmin(BaseModelAdmin): |
||
309 | "Encapsulates all admin options and functionality for a given model."
|
||
310 | |||
311 | list_display = ('__str__',)
|
||
312 | list_display_links = () |
||
313 | list_filter = () |
||
314 | list_select_related = False
|
||
315 | list_per_page = 100
|
||
316 | list_max_show_all = 200
|
||
317 | list_editable = () |
||
318 | search_fields = () |
||
319 | date_hierarchy = None
|
||
320 | save_as = False
|
||
321 | save_on_top = False
|
||
322 | paginator = Paginator |
||
323 | inlines = [] |
||
324 | |||
325 | # Custom templates (designed to be over-ridden in subclasses)
|
||
326 | add_form_template = None
|
||
327 | change_form_template = None
|
||
328 | change_list_template = None
|
||
329 | delete_confirmation_template = None
|
||
330 | delete_selected_confirmation_template = None
|
||
331 | object_history_template = None
|
||
332 | |||
333 | # Actions
|
||
334 | actions = [] |
||
335 | action_form = helpers.ActionForm |
||
336 | actions_on_top = True
|
||
337 | actions_on_bottom = False
|
||
338 | actions_selection_counter = True
|
||
339 | |||
340 | def __init__(self, model, admin_site): |
||
341 | self.model = model
|
||
342 | self.opts = model._meta
|
||
343 | self.admin_site = admin_site
|
||
344 | super(ModelAdmin, self).__init__() |
||
345 | |||
346 | def get_inline_instances(self, request): |
||
347 | inline_instances = [] |
||
348 | for inline_class in self.inlines: |
||
349 | inline = inline_class(self.model, self.admin_site) |
||
350 | if request:
|
||
351 | if not (inline.has_add_permission(request) or |
||
352 | inline.has_change_permission(request) or
|
||
353 | inline.has_delete_permission(request)): |
||
354 | continue
|
||
355 | if not inline.has_add_permission(request): |
||
356 | inline.max_num = 0
|
||
357 | inline_instances.append(inline) |
||
358 | |||
359 | return inline_instances
|
||
360 | |||
361 | def get_urls(self): |
||
362 | from django.conf.urls import patterns, url |
||
363 | |||
364 | def wrap(view): |
||
365 | def wrapper(*args, **kwargs): |
||
366 | return self.admin_site.admin_view(view)(*args, **kwargs) |
||
367 | return update_wrapper(wrapper, view)
|
||
368 | |||
369 | info = self.model._meta.app_label, self.model._meta.module_name |
||
370 | |||
371 | urlpatterns = patterns('',
|
||
372 | url(r'^$',
|
||
373 | wrap(self.changelist_view),
|
||
374 | name='%s_%s_changelist' % info),
|
||
375 | url(r'^add/$',
|
||
376 | wrap(self.add_view),
|
||
377 | name='%s_%s_add' % info),
|
||
378 | url(r'^(.+)/history/$',
|
||
379 | wrap(self.history_view),
|
||
380 | name='%s_%s_history' % info),
|
||
381 | url(r'^(.+)/delete/$',
|
||
382 | wrap(self.delete_view),
|
||
383 | name='%s_%s_delete' % info),
|
||
384 | url(r'^(.+)/$',
|
||
385 | wrap(self.change_view),
|
||
386 | name='%s_%s_change' % info),
|
||
387 | ) |
||
388 | return urlpatterns
|
||
389 | |||
390 | def urls(self): |
||
391 | return self.get_urls() |
||
392 | urls = property(urls)
|
||
393 | |||
394 | @property
|
||
395 | def media(self): |
||
396 | extra = '' if settings.DEBUG else '.min' |
||
397 | js = [ |
||
398 | 'core.js',
|
||
399 | 'admin/RelatedObjectLookups.js',
|
||
400 | 'jquery%s.js' % extra,
|
||
401 | 'jquery.init.js'
|
||
402 | ] |
||
403 | if self.actions is not None: |
||
404 | js.append('actions%s.js' % extra)
|
||
405 | if self.prepopulated_fields: |
||
406 | js.extend(['urlify.js', 'prepopulate%s.js' % extra]) |
||
407 | if self.opts.get_ordered_objects(): |
||
408 | js.extend(['getElementsBySelector.js', 'dom-drag.js' , 'admin/ordering.js']) |
||
409 | return forms.Media(js=[static('admin/js/%s' % url) for url in js]) |
||
410 | |||
411 | def get_model_perms(self, request): |
||
412 | """
|
||
413 | Returns a dict of all perms for this model. This dict has the keys
|
||
414 | ``add``, ``change``, and ``delete`` mapping to the True/False for each
|
||
415 | of those actions.
|
||
416 | """
|
||
417 | return {
|
||
418 | 'add': self.has_add_permission(request), |
||
419 | 'change': self.has_change_permission(request), |
||
420 | 'delete': self.has_delete_permission(request), |
||
421 | } |
||
422 | |||
423 | def get_fieldsets(self, request, obj=None): |
||
424 | "Hook for specifying fieldsets for the add form."
|
||
425 | if self.declared_fieldsets: |
||
426 | return self.declared_fieldsets |
||
427 | form = self.get_form(request, obj)
|
||
428 | fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj)) |
||
429 | return [(None, {'fields': fields})] |
||
430 | |||
431 | def get_form(self, request, obj=None, **kwargs): |
||
432 | """
|
||
433 | Returns a Form class for use in the admin add view. This is used by
|
||
434 | add_view and change_view.
|
||
435 | """
|
||
436 | if self.declared_fieldsets: |
||
437 | fields = flatten_fieldsets(self.declared_fieldsets)
|
||
438 | else:
|
||
439 | fields = None
|
||
440 | if self.exclude is None: |
||
441 | exclude = [] |
||
442 | else:
|
||
443 | exclude = list(self.exclude) |
||
444 | exclude.extend(self.get_readonly_fields(request, obj))
|
||
445 | if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude: |
||
446 | # Take the custom ModelForm's Meta.exclude into account only if the
|
||
447 | # ModelAdmin doesn't define its own.
|
||
448 | exclude.extend(self.form._meta.exclude)
|
||
449 | # if exclude is an empty list we pass None to be consistant with the
|
||
450 | # default on modelform_factory
|
||
451 | exclude = exclude or None |
||
452 | defaults = { |
||
453 | "form": self.form, |
||
454 | "fields": fields,
|
||
455 | "exclude": exclude,
|
||
456 | "formfield_callback": partial(self.formfield_for_dbfield, request=request), |
||
457 | } |
||
458 | defaults.update(kwargs) |
||
459 | return modelform_factory(self.model, **defaults) |
||
460 | |||
461 | def get_changelist(self, request, **kwargs): |
||
462 | """
|
||
463 | Returns the ChangeList class for use on the changelist page.
|
||
464 | """
|
||
465 | from django.contrib.admin.views.main import ChangeList |
||
466 | return ChangeList
|
||
467 | |||
468 | def get_object(self, request, object_id): |
||
469 | """
|
||
470 | Returns an instance matching the primary key provided. ``None`` is
|
||
471 | returned if no match is found (or the object_id failed validation
|
||
472 | against the primary key field).
|
||
473 | """
|
||
474 | queryset = self.queryset(request)
|
||
475 | model = queryset.model |
||
476 | try:
|
||
477 | object_id = model._meta.pk.to_python(object_id) |
||
478 | return queryset.get(pk=object_id)
|
||
479 | except (model.DoesNotExist, ValidationError):
|
||
480 | return None |
||
481 | |||
482 | def get_changelist_form(self, request, **kwargs): |
||
483 | """
|
||
484 | Returns a Form class for use in the Formset on the changelist page.
|
||
485 | """
|
||
486 | defaults = { |
||
487 | "formfield_callback": partial(self.formfield_for_dbfield, request=request), |
||
488 | } |
||
489 | defaults.update(kwargs) |
||
490 | return modelform_factory(self.model, **defaults) |
||
491 | |||
492 | def get_changelist_formset(self, request, **kwargs): |
||
493 | """
|
||
494 | Returns a FormSet class for use on the changelist page if list_editable
|
||
495 | is used.
|
||
496 | """
|
||
497 | defaults = { |
||
498 | "formfield_callback": partial(self.formfield_for_dbfield, request=request), |
||
499 | } |
||
500 | defaults.update(kwargs) |
||
501 | return modelformset_factory(self.model, |
||
502 | self.get_changelist_form(request), extra=0, |
||
503 | fields=self.list_editable, **defaults)
|
||
504 | |||
505 | def get_formsets(self, request, obj=None): |
||
506 | for inline in self.get_inline_instances(request): |
||
507 | yield inline.get_formset(request, obj)
|
||
508 | |||
509 | def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True): |
||
510 | return self.paginator(queryset, per_page, orphans, allow_empty_first_page) |
||
511 | |||
512 | def log_addition(self, request, object): |
||
513 | """
|
||
514 | Log that an object has been successfully added.
|
||
515 |
|
||
516 | The default implementation creates an admin LogEntry object.
|
||
517 | """
|
||
518 | from django.contrib.admin.models import LogEntry, ADDITION |
||
519 | LogEntry.objects.log_action( |
||
520 | user_id = request.user.pk, |
||
521 | content_type_id = ContentType.objects.get_for_model(object).pk,
|
||
522 | object_id = object.pk,
|
||
523 | object_repr = force_unicode(object),
|
||
524 | action_flag = ADDITION |
||
525 | ) |
||
526 | |||
527 | def log_change(self, request, object, message): |
||
528 | """
|
||
529 | Log that an object has been successfully changed.
|
||
530 |
|
||
531 | The default implementation creates an admin LogEntry object.
|
||
532 | """
|
||
533 | from django.contrib.admin.models import LogEntry, CHANGE |
||
534 | LogEntry.objects.log_action( |
||
535 | user_id = request.user.pk, |
||
536 | content_type_id = ContentType.objects.get_for_model(object).pk,
|
||
537 | object_id = object.pk,
|
||
538 | object_repr = force_unicode(object),
|
||
539 | action_flag = CHANGE, |
||
540 | change_message = message |
||
541 | ) |
||
542 | |||
543 | def log_deletion(self, request, object, object_repr): |
||
544 | """
|
||
545 | Log that an object will be deleted. Note that this method is called
|
||
546 | before the deletion.
|
||
547 |
|
||
548 | The default implementation creates an admin LogEntry object.
|
||
549 | """
|
||
550 | from django.contrib.admin.models import LogEntry, DELETION |
||
551 | LogEntry.objects.log_action( |
||
552 | user_id = request.user.id, |
||
553 | content_type_id = ContentType.objects.get_for_model(self.model).pk,
|
||
554 | object_id = object.pk,
|
||
555 | object_repr = object_repr, |
||
556 | action_flag = DELETION |
||
557 | ) |
||
558 | |||
559 | def action_checkbox(self, obj): |
||
560 | """
|
||
561 | A list_display column containing a checkbox widget.
|
||
562 | """
|
||
563 | return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_unicode(obj.pk))
|
||
564 | action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle" />')
|
||
565 | action_checkbox.allow_tags = True
|
||
566 | |||
567 | def get_actions(self, request): |
||
568 | """
|
||
569 | Return a dictionary mapping the names of all actions for this
|
||
570 | ModelAdmin to a tuple of (callable, name, description) for each action.
|
||
571 | """
|
||
572 | # If self.actions is explicitally set to None that means that we don't
|
||
573 | # want *any* actions enabled on this page.
|
||
574 | from django.contrib.admin.views.main import IS_POPUP_VAR |
||
575 | if self.actions is None or IS_POPUP_VAR in request.GET: |
||
576 | return SortedDict()
|
||
577 | |||
578 | actions = [] |
||
579 | |||
580 | # Gather actions from the admin site first
|
||
581 | for (name, func) in self.admin_site.actions: |
||
582 | description = getattr(func, 'short_description', name.replace('_', ' ')) |
||
583 | actions.append((func, name, description)) |
||
584 | |||
585 | # Then gather them from the model admin and all parent classes,
|
||
586 | # starting with self and working back up.
|
||
587 | for klass in self.__class__.mro()[::-1]: |
||
588 | class_actions = getattr(klass, 'actions', []) |
||
589 | # Avoid trying to iterate over None
|
||
590 | if not class_actions: |
||
591 | continue
|
||
592 | actions.extend([self.get_action(action) for action in class_actions]) |
||
593 | |||
594 | # get_action might have returned None, so filter any of those out.
|
||
595 | actions = filter(None, actions) |
||
596 | |||
597 | # Convert the actions into a SortedDict keyed by name.
|
||
598 | actions = SortedDict([ |
||
599 | (name, (func, name, desc)) |
||
600 | for func, name, desc in actions |
||
601 | ]) |
||
602 | |||
603 | return actions
|
||
604 | |||
605 | def get_action_choices(self, request, default_choices=BLANK_CHOICE_DASH): |
||
606 | """
|
||
607 | Return a list of choices for use in a form object. Each choice is a
|
||
608 | tuple (name, description).
|
||
609 | """
|
||
610 | choices = [] + default_choices |
||
611 | for func, name, description in self.get_actions(request).itervalues(): |
||
612 | choice = (name, description % model_format_dict(self.opts))
|
||
613 | choices.append(choice) |
||
614 | return choices
|
||
615 | |||
616 | def get_action(self, action): |
||
617 | """
|
||
618 | Return a given action from a parameter, which can either be a callable,
|
||
619 | or the name of a method on the ModelAdmin. Return is a tuple of
|
||
620 | (callable, name, description).
|
||
621 | """
|
||
622 | # If the action is a callable, just use it.
|
||
623 | if callable(action): |
||
624 | func = action |
||
625 | action = action.__name__ |
||
626 | |||
627 | # Next, look for a method. Grab it off self.__class__ to get an unbound
|
||
628 | # method instead of a bound one; this ensures that the calling
|
||
629 | # conventions are the same for functions and methods.
|
||
630 | elif hasattr(self.__class__, action): |
||
631 | func = getattr(self.__class__, action) |
||
632 | |||
633 | # Finally, look for a named method on the admin site
|
||
634 | else:
|
||
635 | try:
|
||
636 | func = self.admin_site.get_action(action)
|
||
637 | except KeyError: |
||
638 | return None |
||
639 | |||
640 | if hasattr(func, 'short_description'): |
||
641 | description = func.short_description |
||
642 | else:
|
||
643 | description = capfirst(action.replace('_', ' ')) |
||
644 | return func, action, description
|
||
645 | |||
646 | def get_list_display(self, request): |
||
647 | """
|
||
648 | Return a sequence containing the fields to be displayed on the
|
||
649 | changelist.
|
||
650 | """
|
||
651 | return self.list_display |
||
652 | |||
653 | def get_list_display_links(self, request, list_display): |
||
654 | """
|
||
655 | Return a sequence containing the fields to be displayed as links
|
||
656 | on the changelist. The list_display parameter is the list of fields
|
||
657 | returned by get_list_display().
|
||
658 | """
|
||
659 | if self.list_display_links or not list_display: |
||
660 | return self.list_display_links |
||
661 | else:
|
||
662 | # Use only the first item in list_display as link
|
||
663 | return list(list_display)[:1] |
||
664 | |||
665 | def construct_change_message(self, request, form, formsets): |
||
666 | """
|
||
667 | Construct a change message from a changed object.
|
||
668 | """
|
||
669 | change_message = [] |
||
670 | if form.changed_data:
|
||
671 | change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and'))) |
||
672 | |||
673 | if formsets:
|
||
674 | for formset in formsets: |
||
675 | for added_object in formset.new_objects: |
||
676 | change_message.append(_('Added %(name)s "%(object)s".')
|
||
677 | % {'name': force_unicode(added_object._meta.verbose_name),
|
||
678 | 'object': force_unicode(added_object)})
|
||
679 | for changed_object, changed_fields in formset.changed_objects: |
||
680 | change_message.append(_('Changed %(list)s for %(name)s "%(object)s".')
|
||
681 | % {'list': get_text_list(changed_fields, _('and')), |
||
682 | 'name': force_unicode(changed_object._meta.verbose_name),
|
||
683 | 'object': force_unicode(changed_object)})
|
||
684 | for deleted_object in formset.deleted_objects: |
||
685 | change_message.append(_('Deleted %(name)s "%(object)s".')
|
||
686 | % {'name': force_unicode(deleted_object._meta.verbose_name),
|
||
687 | 'object': force_unicode(deleted_object)})
|
||
688 | change_message = ' '.join(change_message)
|
||
689 | return change_message or _('No fields changed.') |
||
690 | |||
691 | def message_user(self, request, message): |
||
692 | """
|
||
693 | Send a message to the user. The default implementation
|
||
694 | posts a message using the django.contrib.messages backend.
|
||
695 | """
|
||
696 | messages.info(request, message) |
||
697 | |||
698 | def save_form(self, request, form, change): |
||
699 | """
|
||
700 | Given a ModelForm return an unsaved instance. ``change`` is True if
|
||
701 | the object is being changed, and False if it's being added.
|
||
702 | """
|
||
703 | return form.save(commit=False) |
||
704 | |||
705 | def save_model(self, request, obj, form, change): |
||
706 | """
|
||
707 | Given a model instance save it to the database.
|
||
708 | """
|
||
709 | obj.save() |
||
710 | |||
711 | def delete_model(self, request, obj): |
||
712 | """
|
||
713 | Given a model instance delete it from the database.
|
||
714 | """
|
||
715 | obj.delete() |
||
716 | |||
717 | def save_formset(self, request, form, formset, change): |
||
718 | """
|
||
719 | Given an inline formset save it to the database.
|
||
720 | """
|
||
721 | formset.save() |
||
722 | |||
723 | def save_related(self, request, form, formsets, change): |
||
724 | """
|
||
725 | Given the ``HttpRequest``, the parent ``ModelForm`` instance, the
|
||
726 | list of inline formsets and a boolean value based on whether the
|
||
727 | parent is being added or changed, save the related objects to the
|
||
728 | database. Note that at this point save_form() and save_model() have
|
||
729 | already been called.
|
||
730 | """
|
||
731 | form.save_m2m() |
||
732 | for formset in formsets: |
||
733 | self.save_formset(request, form, formset, change=change)
|
||
734 | |||
735 | def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): |
||
736 | opts = self.model._meta
|
||
737 | app_label = opts.app_label |
||
738 | ordered_objects = opts.get_ordered_objects() |
||
739 | context.update({ |
||
740 | 'add': add,
|
||
741 | 'change': change,
|
||
742 | 'has_add_permission': self.has_add_permission(request), |
||
743 | 'has_change_permission': self.has_change_permission(request, obj), |
||
744 | 'has_delete_permission': self.has_delete_permission(request, obj), |
||
745 | 'has_file_field': True, # FIXME - this should check if form or formsets have a FileField, |
||
746 | 'has_absolute_url': hasattr(self.model, 'get_absolute_url'), |
||
747 | 'ordered_objects': ordered_objects,
|
||
748 | 'form_url': mark_safe(form_url),
|
||
749 | 'opts': opts,
|
||
750 | 'content_type_id': ContentType.objects.get_for_model(self.model).id, |
||
751 | 'save_as': self.save_as, |
||
752 | 'save_on_top': self.save_on_top, |
||
753 | }) |
||
754 | if add and self.add_form_template is not None: |
||
755 | form_template = self.add_form_template
|
||
756 | else:
|
||
757 | form_template = self.change_form_template
|
||
758 | |||
759 | return TemplateResponse(request, form_template or [ |
||
760 | "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
|
||
761 | "admin/%s/change_form.html" % app_label,
|
||
762 | "admin/change_form.html"
|
||
763 | ], context, current_app=self.admin_site.name)
|
||
764 | |||
765 | def response_add(self, request, obj, post_url_continue='../%s/'): |
||
766 | """
|
||
767 | Determines the HttpResponse for the add_view stage.
|
||
768 | """
|
||
769 | opts = obj._meta |
||
770 | pk_value = obj._get_pk_val() |
||
771 | |||
772 | msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)} |
||
773 | # Here, we distinguish between different save types by checking for
|
||
774 | # the presence of keys in request.POST.
|
||
775 | if "_continue" in request.POST: |
||
776 | self.message_user(request, msg + ' ' + _("You may edit it again below.")) |
||
777 | if "_popup" in request.POST: |
||
778 | post_url_continue += "?_popup=1"
|
||
779 | return HttpResponseRedirect(post_url_continue % pk_value)
|
||
780 | |||
781 | if "_popup" in request.POST: |
||
782 | return HttpResponse(
|
||
783 | '<!DOCTYPE html><html><head><title></title></head><body>'
|
||
784 | '<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script></body></html>' % \
|
||
785 | # escape() calls force_unicode.
|
||
786 | (escape(pk_value), escapejs(obj))) |
||
787 | elif "_addanother" in request.POST: |
||
788 | self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name))) |
||
789 | return HttpResponseRedirect(request.path)
|
||
790 | else:
|
||
791 | self.message_user(request, msg)
|
||
792 | |||
793 | # Figure out where to redirect. If the user has change permission,
|
||
794 | # redirect to the change-list page for this object. Otherwise,
|
||
795 | # redirect to the admin index.
|
||
796 | if self.has_change_permission(request, None): |
||
797 | post_url = reverse('admin:%s_%s_changelist' %
|
||
798 | (opts.app_label, opts.module_name), |
||
799 | current_app=self.admin_site.name)
|
||
800 | else:
|
||
801 | post_url = reverse('admin:index',
|
||
802 | current_app=self.admin_site.name)
|
||
803 | return HttpResponseRedirect(post_url)
|
||
804 | |||
805 | def response_change(self, request, obj): |
||
806 | """
|
||
807 | Determines the HttpResponse for the change_view stage.
|
||
808 | """
|
||
809 | opts = obj._meta |
||
810 | |||
811 | # Handle proxy models automatically created by .only() or .defer().
|
||
812 | # Refs #14529
|
||
813 | verbose_name = opts.verbose_name |
||
814 | module_name = opts.module_name |
||
815 | if obj._deferred:
|
||
816 | opts_ = opts.proxy_for_model._meta |
||
817 | verbose_name = opts_.verbose_name |
||
818 | module_name = opts_.module_name |
||
819 | |||
820 | pk_value = obj._get_pk_val() |
||
821 | |||
822 | msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(verbose_name), 'obj': force_unicode(obj)} |
||
823 | if "_continue" in request.POST: |
||
824 | self.message_user(request, msg + ' ' + _("You may edit it again below.")) |
||
825 | if "_popup" in request.REQUEST: |
||
826 | return HttpResponseRedirect(request.path + "?_popup=1") |
||
827 | else:
|
||
828 | return HttpResponseRedirect(request.path)
|
||
829 | elif "_saveasnew" in request.POST: |
||
830 | msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(verbose_name), 'obj': obj} |
||
831 | self.message_user(request, msg)
|
||
832 | return HttpResponseRedirect(reverse('admin:%s_%s_change' % |
||
833 | (opts.app_label, module_name), |
||
834 | args=(pk_value,), |
||
835 | current_app=self.admin_site.name))
|
||
836 | elif "_addanother" in request.POST: |
||
837 | self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(verbose_name))) |
||
838 | return HttpResponseRedirect(reverse('admin:%s_%s_add' % |
||
839 | (opts.app_label, module_name), |
||
840 | current_app=self.admin_site.name))
|
||
841 | else:
|
||
842 | self.message_user(request, msg)
|
||
843 | # Figure out where to redirect. If the user has change permission,
|
||
844 | # redirect to the change-list page for this object. Otherwise,
|
||
845 | # redirect to the admin index.
|
||
846 | if self.has_change_permission(request, None): |
||
847 | post_url = reverse('admin:%s_%s_changelist' %
|
||
848 | (opts.app_label, module_name), |
||
849 | current_app=self.admin_site.name)
|
||
850 | else:
|
||
851 | post_url = reverse('admin:index',
|
||
852 | current_app=self.admin_site.name)
|
||
853 | return HttpResponseRedirect(post_url)
|
||
854 | |||
855 | def response_action(self, request, queryset): |
||
856 | """
|
||
857 | Handle an admin action. This is called if a request is POSTed to the
|
||
858 | changelist; it returns an HttpResponse if the action was handled, and
|
||
859 | None otherwise.
|
||
860 | """
|
||
861 | |||
862 | # There can be multiple action forms on the page (at the top
|
||
863 | # and bottom of the change list, for example). Get the action
|
||
864 | # whose button was pushed.
|
||
865 | try:
|
||
866 | action_index = int(request.POST.get('index', 0)) |
||
867 | except ValueError: |
||
868 | action_index = 0
|
||
869 | |||
870 | # Construct the action form.
|
||
871 | data = request.POST.copy() |
||
872 | data.pop(helpers.ACTION_CHECKBOX_NAME, None)
|
||
873 | data.pop("index", None) |
||
874 | |||
875 | # Use the action whose button was pushed
|
||
876 | try:
|
||
877 | data.update({'action': data.getlist('action')[action_index]}) |
||
878 | except IndexError: |
||
879 | # If we didn't get an action from the chosen form that's invalid
|
||
880 | # POST data, so by deleting action it'll fail the validation check
|
||
881 | # below. So no need to do anything here
|
||
882 | pass
|
||
883 | |||
884 | action_form = self.action_form(data, auto_id=None) |
||
885 | action_form.fields['action'].choices = self.get_action_choices(request) |
||
886 | |||
887 | # If the form's valid we can handle the action.
|
||
888 | if action_form.is_valid():
|
||
889 | action = action_form.cleaned_data['action']
|
||
890 | select_across = action_form.cleaned_data['select_across']
|
||
891 | func, name, description = self.get_actions(request)[action]
|
||
892 | |||
893 | # Get the list of selected PKs. If nothing's selected, we can't
|
||
894 | # perform an action on it, so bail. Except we want to perform
|
||
895 | # the action explicitly on all objects.
|
||
896 | selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME) |
||
897 | if not selected and not select_across: |
||
898 | # Reminder that something needs to be selected or nothing will happen
|
||
899 | msg = _("Items must be selected in order to perform "
|
||
900 | "actions on them. No items have been changed.")
|
||
901 | self.message_user(request, msg)
|
||
902 | return None |
||
903 | |||
904 | if not select_across: |
||
905 | # Perform the action only on the selected objects
|
||
906 | queryset = queryset.filter(pk__in=selected) |
||
907 | |||
908 | response = func(self, request, queryset)
|
||
909 | |||
910 | # Actions may return an HttpResponse, which will be used as the
|
||
911 | # response from the POST. If not, we'll be a good little HTTP
|
||
912 | # citizen and redirect back to the changelist page.
|
||
913 | if isinstance(response, HttpResponse): |
||
914 | return response
|
||
915 | else:
|
||
916 | return HttpResponseRedirect(request.get_full_path())
|
||
917 | else:
|
||
918 | msg = _("No action selected.")
|
||
919 | self.message_user(request, msg)
|
||
920 | return None |
||
921 | |||
922 | @csrf_protect_m
|
||
923 | @transaction.commit_on_success
|
||
924 | def add_view(self, request, form_url='', extra_context=None): |
||
925 | "The 'add' admin view for this model."
|
||
926 | model = self.model
|
||
927 | opts = model._meta |
||
928 | |||
929 | if not self.has_add_permission(request): |
||
930 | raise PermissionDenied
|
||
931 | |||
932 | ModelForm = self.get_form(request)
|
||
933 | formsets = [] |
||
934 | inline_instances = self.get_inline_instances(request)
|
||
935 | if request.method == 'POST': |
||
936 | form = ModelForm(request.POST, request.FILES) |
||
937 | if form.is_valid():
|
||
938 | new_object = self.save_form(request, form, change=False) |
||
939 | form_validated = True
|
||
940 | else:
|
||
941 | form_validated = False
|
||
942 | new_object = self.model()
|
||
943 | prefixes = {} |
||
944 | for FormSet, inline in zip(self.get_formsets(request), inline_instances): |
||
945 | prefix = FormSet.get_default_prefix() |
||
946 | prefixes[prefix] = prefixes.get(prefix, 0) + 1 |
||
947 | if prefixes[prefix] != 1 or not prefix: |
||
948 | prefix = "%s-%s" % (prefix, prefixes[prefix])
|
||
949 | formset = FormSet(data=request.POST, files=request.FILES, |
||
950 | instance=new_object, |
||
951 | save_as_new="_saveasnew" in request.POST, |
||
952 | prefix=prefix, queryset=inline.queryset(request)) |
||
953 | formsets.append(formset) |
||
954 | if all_valid(formsets) and form_validated: |
||
955 | self.save_model(request, new_object, form, False) |
||
956 | self.save_related(request, form, formsets, False) |
||
957 | self.log_addition(request, new_object)
|
||
958 | return self.response_add(request, new_object) |
||
959 | else:
|
||
960 | # Prepare the dict of initial data from the request.
|
||
961 | # We have to special-case M2Ms as a list of comma-separated PKs.
|
||
962 | initial = dict(request.GET.items())
|
||
963 | for k in initial: |
||
964 | try:
|
||
965 | f = opts.get_field(k) |
||
966 | except models.FieldDoesNotExist:
|
||
967 | continue
|
||
968 | if isinstance(f, models.ManyToManyField): |
||
969 | initial[k] = initial[k].split(",")
|
||
970 | form = ModelForm(initial=initial) |
||
971 | prefixes = {} |
||
972 | for FormSet, inline in zip(self.get_formsets(request), inline_instances): |
||
973 | prefix = FormSet.get_default_prefix() |
||
974 | prefixes[prefix] = prefixes.get(prefix, 0) + 1 |
||
975 | if prefixes[prefix] != 1 or not prefix: |
||
976 | prefix = "%s-%s" % (prefix, prefixes[prefix])
|
||
977 | formset = FormSet(instance=self.model(), prefix=prefix,
|
||
978 | queryset=inline.queryset(request)) |
||
979 | formsets.append(formset) |
||
980 | |||
981 | adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), |
||
982 | self.get_prepopulated_fields(request),
|
||
983 | self.get_readonly_fields(request),
|
||
984 | model_admin=self)
|
||
985 | media = self.media + adminForm.media
|
||
986 | |||
987 | inline_admin_formsets = [] |
||
988 | for inline, formset in zip(inline_instances, formsets): |
||
989 | fieldsets = list(inline.get_fieldsets(request))
|
||
990 | readonly = list(inline.get_readonly_fields(request))
|
||
991 | prepopulated = dict(inline.get_prepopulated_fields(request))
|
||
992 | inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, |
||
993 | fieldsets, prepopulated, readonly, model_admin=self)
|
||
994 | inline_admin_formsets.append(inline_admin_formset) |
||
995 | media = media + inline_admin_formset.media |
||
996 | |||
997 | context = { |
||
998 | 'title': _('Add %s') % force_unicode(opts.verbose_name), |
||
999 | 'adminform': adminForm,
|
||
1000 | 'is_popup': "_popup" in request.REQUEST, |
||
1001 | 'show_delete': False, |
||
1002 | 'media': media,
|
||
1003 | 'inline_admin_formsets': inline_admin_formsets,
|
||
1004 | 'errors': helpers.AdminErrorList(form, formsets),
|
||
1005 | 'app_label': opts.app_label,
|
||
1006 | } |
||
1007 | context.update(extra_context or {})
|
||
1008 | return self.render_change_form(request, context, form_url=form_url, add=True) |
||
1009 | |||
1010 | @csrf_protect_m
|
||
1011 | @transaction.commit_on_success
|
||
1012 | def change_view(self, request, object_id, form_url='', extra_context=None): |
||
1013 | "The 'change' admin view for this model."
|
||
1014 | model = self.model
|
||
1015 | opts = model._meta |
||
1016 | |||
1017 | obj = self.get_object(request, unquote(object_id))
|
||
1018 | |||
1019 | if not self.has_change_permission(request, obj): |
||
1020 | raise PermissionDenied
|
||
1021 | |||
1022 | if obj is None: |
||
1023 | raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)}) |
||
1024 | |||
1025 | if request.method == 'POST' and "_saveasnew" in request.POST: |
||
1026 | return self.add_view(request, form_url=reverse('admin:%s_%s_add' % |
||
1027 | (opts.app_label, opts.module_name), |
||
1028 | current_app=self.admin_site.name))
|
||
1029 | |||
1030 | ModelForm = self.get_form(request, obj)
|
||
1031 | formsets = [] |
||
1032 | inline_instances = self.get_inline_instances(request)
|
||
1033 | if request.method == 'POST': |
||
1034 | form = ModelForm(request.POST, request.FILES, instance=obj) |
||
1035 | if form.is_valid():
|
||
1036 | form_validated = True
|
||
1037 | new_object = self.save_form(request, form, change=True) |
||
1038 | else:
|
||
1039 | form_validated = False
|
||
1040 | new_object = obj |
||
1041 | prefixes = {} |
||
1042 | for FormSet, inline in zip(self.get_formsets(request, new_object), inline_instances): |
||
1043 | prefix = FormSet.get_default_prefix() |
||
1044 | prefixes[prefix] = prefixes.get(prefix, 0) + 1 |
||
1045 | if prefixes[prefix] != 1 or not prefix: |
||
1046 | prefix = "%s-%s" % (prefix, prefixes[prefix])
|
||
1047 | formset = FormSet(request.POST, request.FILES, |
||
1048 | instance=new_object, prefix=prefix, |
||
1049 | queryset=inline.queryset(request)) |
||
1050 | |||
1051 | formsets.append(formset) |
||
1052 | |||
1053 | if all_valid(formsets) and form_validated: |
||
1054 | self.save_model(request, new_object, form, True) |
||
1055 | self.save_related(request, form, formsets, True) |
||
1056 | change_message = self.construct_change_message(request, form, formsets)
|
||
1057 | self.log_change(request, new_object, change_message)
|
||
1058 | return self.response_change(request, new_object) |
||
1059 | |||
1060 | else:
|
||
1061 | form = ModelForm(instance=obj) |
||
1062 | prefixes = {} |
||
1063 | for FormSet, inline in zip(self.get_formsets(request, obj), inline_instances): |
||
1064 | prefix = FormSet.get_default_prefix() |
||
1065 | prefixes[prefix] = prefixes.get(prefix, 0) + 1 |
||
1066 | if prefixes[prefix] != 1 or not prefix: |
||
1067 | prefix = "%s-%s" % (prefix, prefixes[prefix])
|
||
1068 | formset = FormSet(instance=obj, prefix=prefix, |
||
1069 | queryset=inline.queryset(request)) |
||
1070 | formsets.append(formset) |
||
1071 | |||
1072 | adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
|
||
1073 | self.get_prepopulated_fields(request, obj),
|
||
1074 | self.get_readonly_fields(request, obj),
|
||
1075 | model_admin=self)
|
||
1076 | media = self.media + adminForm.media
|
||
1077 | |||
1078 | inline_admin_formsets = [] |
||
1079 | for inline, formset in zip(inline_instances, formsets): |
||
1080 | fieldsets = list(inline.get_fieldsets(request, obj))
|
||
1081 | readonly = list(inline.get_readonly_fields(request, obj))
|
||
1082 | prepopulated = dict(inline.get_prepopulated_fields(request, obj))
|
||
1083 | inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, |
||
1084 | fieldsets, prepopulated, readonly, model_admin=self)
|
||
1085 | inline_admin_formsets.append(inline_admin_formset) |
||
1086 | media = media + inline_admin_formset.media |
||
1087 | |||
1088 | context = { |
||
1089 | 'title': _('Change %s') % force_unicode(opts.verbose_name), |
||
1090 | 'adminform': adminForm,
|
||
1091 | 'object_id': object_id,
|
||
1092 | 'original': obj,
|
||
1093 | 'is_popup': "_popup" in request.REQUEST, |
||
1094 | 'media': media,
|
||
1095 | 'inline_admin_formsets': inline_admin_formsets,
|
||
1096 | 'errors': helpers.AdminErrorList(form, formsets),
|
||
1097 | 'app_label': opts.app_label,
|
||
1098 | } |
||
1099 | context.update(extra_context or {})
|
||
1100 | return self.render_change_form(request, context, change=True, obj=obj, form_url=form_url) |
||
1101 | |||
1102 | @csrf_protect_m
|
||
1103 | def changelist_view(self, request, extra_context=None): |
||
1104 | """
|
||
1105 | The 'change list' admin view for this model.
|
||
1106 | """
|
||
1107 | from django.contrib.admin.views.main import ERROR_FLAG |
||
1108 | opts = self.model._meta
|
||
1109 | app_label = opts.app_label |
||
1110 | if not self.has_change_permission(request, None): |
||
1111 | raise PermissionDenied
|
||
1112 | |||
1113 | list_display = self.get_list_display(request)
|
||
1114 | list_display_links = self.get_list_display_links(request, list_display)
|
||
1115 | |||
1116 | # Check actions to see if any are available on this changelist
|
||
1117 | actions = self.get_actions(request)
|
||
1118 | if actions:
|
||
1119 | # Add the action checkboxes if there are any actions available.
|
||
1120 | list_display = ['action_checkbox'] + list(list_display) |
||
1121 | |||
1122 | ChangeList = self.get_changelist(request)
|
||
1123 | try:
|
||
1124 | cl = ChangeList(request, self.model, list_display,
|
||
1125 | list_display_links, self.list_filter, self.date_hierarchy, |
||
1126 | self.search_fields, self.list_select_related, |
||
1127 | self.list_per_page, self.list_max_show_all, self.list_editable, |
||
1128 | self)
|
||
1129 | except IncorrectLookupParameters:
|
||
1130 | # Wacky lookup parameters were given, so redirect to the main
|
||
1131 | # changelist page, without parameters, and pass an 'invalid=1'
|
||
1132 | # parameter via the query string. If wacky parameters were given
|
||
1133 | # and the 'invalid=1' parameter was already in the query string,
|
||
1134 | # something is screwed up with the database, so display an error
|
||
1135 | # page.
|
||
1136 | if ERROR_FLAG in request.GET.keys(): |
||
1137 | return SimpleTemplateResponse('admin/invalid_setup.html', { |
||
1138 | 'title': _('Database error'), |
||
1139 | }) |
||
1140 | return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1') |
||
1141 | |||
1142 | # If the request was POSTed, this might be a bulk action or a bulk
|
||
1143 | # edit. Try to look up an action or confirmation first, but if this
|
||
1144 | # isn't an action the POST will fall through to the bulk edit check,
|
||
1145 | # below.
|
||
1146 | action_failed = False
|
||
1147 | selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME) |
||
1148 | |||
1149 | # Actions with no confirmation
|
||
1150 | if (actions and request.method == 'POST' and |
||
1151 | 'index' in request.POST and '_save' not in request.POST): |
||
1152 | if selected:
|
||
1153 | response = self.response_action(request, queryset=cl.get_query_set(request))
|
||
1154 | if response:
|
||
1155 | return response
|
||
1156 | else:
|
||
1157 | action_failed = True
|
||
1158 | else:
|
||
1159 | msg = _("Items must be selected in order to perform "
|
||
1160 | "actions on them. No items have been changed.")
|
||
1161 | self.message_user(request, msg)
|
||
1162 | action_failed = True
|
||
1163 | |||
1164 | # Actions with confirmation
|
||
1165 | if (actions and request.method == 'POST' and |
||
1166 | helpers.ACTION_CHECKBOX_NAME in request.POST and |
||
1167 | 'index' not in request.POST and '_save' not in request.POST): |
||
1168 | if selected:
|
||
1169 | response = self.response_action(request, queryset=cl.get_query_set(request))
|
||
1170 | if response:
|
||
1171 | return response
|
||
1172 | else:
|
||
1173 | action_failed = True
|
||
1174 | |||
1175 | # If we're allowing changelist editing, we need to construct a formset
|
||
1176 | # for the changelist given all the fields to be edited. Then we'll
|
||
1177 | # use the formset to validate/process POSTed data.
|
||
1178 | formset = cl.formset = None
|
||
1179 | |||
1180 | # Handle POSTed bulk-edit data.
|
||
1181 | if (request.method == "POST" and cl.list_editable and |
||
1182 | '_save' in request.POST and not action_failed): |
||
1183 | FormSet = self.get_changelist_formset(request)
|
||
1184 | formset = cl.formset = FormSet(request.POST, request.FILES, queryset=cl.result_list) |
||
1185 | if formset.is_valid():
|
||
1186 | changecount = 0
|
||
1187 | for form in formset.forms: |
||
1188 | if form.has_changed():
|
||
1189 | obj = self.save_form(request, form, change=True) |
||
1190 | self.save_model(request, obj, form, change=True) |
||
1191 | self.save_related(request, form, formsets=[], change=True) |
||
1192 | change_msg = self.construct_change_message(request, form, None) |
||
1193 | self.log_change(request, obj, change_msg)
|
||
1194 | changecount += 1
|
||
1195 | |||
1196 | if changecount:
|
||
1197 | if changecount == 1: |
||
1198 | name = force_unicode(opts.verbose_name) |
||
1199 | else:
|
||
1200 | name = force_unicode(opts.verbose_name_plural) |
||
1201 | msg = ungettext("%(count)s %(name)s was changed successfully.",
|
||
1202 | "%(count)s %(name)s were changed successfully.",
|
||
1203 | changecount) % {'count': changecount,
|
||
1204 | 'name': name,
|
||
1205 | 'obj': force_unicode(obj)}
|
||
1206 | self.message_user(request, msg)
|
||
1207 | |||
1208 | return HttpResponseRedirect(request.get_full_path())
|
||
1209 | |||
1210 | # Handle GET -- construct a formset for display.
|
||
1211 | elif cl.list_editable:
|
||
1212 | FormSet = self.get_changelist_formset(request)
|
||
1213 | formset = cl.formset = FormSet(queryset=cl.result_list) |
||
1214 | |||
1215 | # Build the list of media to be used by the formset.
|
||
1216 | if formset:
|
||
1217 | media = self.media + formset.media
|
||
1218 | else:
|
||
1219 | media = self.media
|
||
1220 | |||
1221 | # Build the action form and populate it with available actions.
|
||
1222 | if actions:
|
||
1223 | action_form = self.action_form(auto_id=None) |
||
1224 | action_form.fields['action'].choices = self.get_action_choices(request) |
||
1225 | else:
|
||
1226 | action_form = None
|
||
1227 | |||
1228 | selection_note_all = ungettext('%(total_count)s selected',
|
||
1229 | 'All %(total_count)s selected', cl.result_count)
|
||
1230 | |||
1231 | context = { |
||
1232 | 'module_name': force_unicode(opts.verbose_name_plural),
|
||
1233 | 'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(cl.result_list)}, |
||
1234 | 'selection_note_all': selection_note_all % {'total_count': cl.result_count}, |
||
1235 | 'title': cl.title,
|
||
1236 | 'is_popup': cl.is_popup,
|
||
1237 | 'cl': cl,
|
||
1238 | 'media': media,
|
||
1239 | 'has_add_permission': self.has_add_permission(request), |
||
1240 | 'app_label': app_label,
|
||
1241 | 'action_form': action_form,
|
||
1242 | 'actions_on_top': self.actions_on_top, |
||
1243 | 'actions_on_bottom': self.actions_on_bottom, |
||
1244 | 'actions_selection_counter': self.actions_selection_counter, |
||
1245 | } |
||
1246 | context.update(extra_context or {})
|
||
1247 | |||
1248 | return TemplateResponse(request, self.change_list_template or [ |
||
1249 | 'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()),
|
||
1250 | 'admin/%s/change_list.html' % app_label,
|
||
1251 | 'admin/change_list.html'
|
||
1252 | ], context, current_app=self.admin_site.name)
|
||
1253 | |||
1254 | @csrf_protect_m
|
||
1255 | @transaction.commit_on_success
|
||
1256 | def delete_view(self, request, object_id, extra_context=None): |
||
1257 | "The 'delete' admin view for this model."
|
||
1258 | opts = self.model._meta
|
||
1259 | app_label = opts.app_label |
||
1260 | |||
1261 | obj = self.get_object(request, unquote(object_id))
|
||
1262 | |||
1263 | if not self.has_delete_permission(request, obj): |
||
1264 | raise PermissionDenied
|
||
1265 | |||
1266 | if obj is None: |
||
1267 | raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)}) |
||
1268 | |||
1269 | using = router.db_for_write(self.model)
|
||
1270 | |||
1271 | # Populate deleted_objects, a data structure of all related objects that
|
||
1272 | # will also be deleted.
|
||
1273 | (deleted_objects, perms_needed, protected) = get_deleted_objects( |
||
1274 | [obj], opts, request.user, self.admin_site, using)
|
||
1275 | |||
1276 | if request.POST: # The user has already confirmed the deletion. |
||
1277 | if perms_needed:
|
||
1278 | raise PermissionDenied
|
||
1279 | obj_display = force_unicode(obj) |
||
1280 | self.log_deletion(request, obj, obj_display)
|
||
1281 | self.delete_model(request, obj)
|
||
1282 | |||
1283 | self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj_display)}) |
||
1284 | |||
1285 | if not self.has_change_permission(request, None): |
||
1286 | return HttpResponseRedirect(reverse('admin:index', |
||
1287 | current_app=self.admin_site.name))
|
||
1288 | return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % |
||
1289 | (opts.app_label, opts.module_name), |
||
1290 | current_app=self.admin_site.name))
|
||
1291 | |||
1292 | object_name = force_unicode(opts.verbose_name) |
||
1293 | |||
1294 | if perms_needed or protected: |
||
1295 | title = _("Cannot delete %(name)s") % {"name": object_name} |
||
1296 | else:
|
||
1297 | title = _("Are you sure?")
|
||
1298 | |||
1299 | context = { |
||
1300 | "title": title,
|
||
1301 | "object_name": object_name,
|
||
1302 | "object": obj,
|
||
1303 | "deleted_objects": deleted_objects,
|
||
1304 | "perms_lacking": perms_needed,
|
||
1305 | "protected": protected,
|
||
1306 | "opts": opts,
|
||
1307 | "app_label": app_label,
|
||
1308 | } |
||
1309 | context.update(extra_context or {})
|
||
1310 | |||
1311 | return TemplateResponse(request, self.delete_confirmation_template or [ |
||
1312 | "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()),
|
||
1313 | "admin/%s/delete_confirmation.html" % app_label,
|
||
1314 | "admin/delete_confirmation.html"
|
||
1315 | ], context, current_app=self.admin_site.name)
|
||
1316 | |||
1317 | def history_view(self, request, object_id, extra_context=None): |
||
1318 | "The 'history' admin view for this model."
|
||
1319 | from django.contrib.admin.models import LogEntry |
||
1320 | model = self.model
|
||
1321 | opts = model._meta |
||
1322 | app_label = opts.app_label |
||
1323 | action_list = LogEntry.objects.filter( |
||
1324 | object_id = object_id, |
||
1325 | content_type__id__exact = ContentType.objects.get_for_model(model).id |
||
1326 | ).select_related().order_by('action_time')
|
||
1327 | # If no history was found, see whether this object even exists.
|
||
1328 | obj = get_object_or_404(model, pk=unquote(object_id)) |
||
1329 | context = { |
||
1330 | 'title': _('Change history: %s') % force_unicode(obj), |
||
1331 | 'action_list': action_list,
|
||
1332 | 'module_name': capfirst(force_unicode(opts.verbose_name_plural)),
|
||
1333 | 'object': obj,
|
||
1334 | 'app_label': app_label,
|
||
1335 | 'opts': opts,
|
||
1336 | } |
||
1337 | context.update(extra_context or {})
|
||
1338 | return TemplateResponse(request, self.object_history_template or [ |
||
1339 | "admin/%s/%s/object_history.html" % (app_label, opts.object_name.lower()),
|
||
1340 | "admin/%s/object_history.html" % app_label,
|
||
1341 | "admin/object_history.html"
|
||
1342 | ], context, current_app=self.admin_site.name)
|
||
1343 | |||
1344 | class InlineModelAdmin(BaseModelAdmin): |
||
1345 | """
|
||
1346 | Options for inline editing of ``model`` instances.
|
||
1347 |
|
||
1348 | Provide ``name`` to specify the attribute name of the ``ForeignKey`` from
|
||
1349 | ``model`` to its parent. This is required if ``model`` has more than one
|
||
1350 | ``ForeignKey`` to its parent.
|
||
1351 | """
|
||
1352 | model = None
|
||
1353 | fk_name = None
|
||
1354 | formset = BaseInlineFormSet |
||
1355 | extra = 3
|
||
1356 | max_num = None
|
||
1357 | template = None
|
||
1358 | verbose_name = None
|
||
1359 | verbose_name_plural = None
|
||
1360 | can_delete = True
|
||
1361 | |||
1362 | def __init__(self, parent_model, admin_site): |
||
1363 | self.admin_site = admin_site
|
||
1364 | self.parent_model = parent_model
|
||
1365 | self.opts = self.model._meta |
||
1366 | super(InlineModelAdmin, self).__init__() |
||
1367 | if self.verbose_name is None: |
||
1368 | self.verbose_name = self.model._meta.verbose_name |
||
1369 | if self.verbose_name_plural is None: |
||
1370 | self.verbose_name_plural = self.model._meta.verbose_name_plural |
||
1371 | |||
1372 | @property
|
||
1373 | def media(self): |
||
1374 | extra = '' if settings.DEBUG else '.min' |
||
1375 | js = ['jquery%s.js' % extra, 'jquery.init.js', 'inlines%s.js' % extra] |
||
1376 | if self.prepopulated_fields: |
||
1377 | js.extend(['urlify.js', 'prepopulate%s.js' % extra]) |
||
1378 | if self.filter_vertical or self.filter_horizontal: |
||
1379 | js.extend(['SelectBox.js', 'SelectFilter2.js']) |
||
1380 | return forms.Media(js=[static('admin/js/%s' % url) for url in js]) |
||
1381 | |||
1382 | def get_formset(self, request, obj=None, **kwargs): |
||
1383 | """Returns a BaseInlineFormSet class for use in admin add/change views."""
|
||
1384 | if self.declared_fieldsets: |
||
1385 | fields = flatten_fieldsets(self.declared_fieldsets)
|
||
1386 | else:
|
||
1387 | fields = None
|
||
1388 | if self.exclude is None: |
||
1389 | exclude = [] |
||
1390 | else:
|
||
1391 | exclude = list(self.exclude) |
||
1392 | exclude.extend(self.get_readonly_fields(request, obj))
|
||
1393 | if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude: |
||
1394 | # Take the custom ModelForm's Meta.exclude into account only if the
|
||
1395 | # InlineModelAdmin doesn't define its own.
|
||
1396 | exclude.extend(self.form._meta.exclude)
|
||
1397 | # if exclude is an empty list we use None, since that's the actual
|
||
1398 | # default
|
||
1399 | exclude = exclude or None |
||
1400 | can_delete = self.can_delete and self.has_delete_permission(request, obj) |
||
1401 | defaults = { |
||
1402 | "form": self.form, |
||
1403 | "formset": self.formset, |
||
1404 | "fk_name": self.fk_name, |
||
1405 | "fields": fields,
|
||
1406 | "exclude": exclude,
|
||
1407 | "formfield_callback": partial(self.formfield_for_dbfield, request=request), |
||
1408 | "extra": self.extra, |
||
1409 | "max_num": self.max_num, |
||
1410 | "can_delete": can_delete,
|
||
1411 | } |
||
1412 | defaults.update(kwargs) |
||
1413 | return inlineformset_factory(self.parent_model, self.model, **defaults) |
||
1414 | |||
1415 | def get_fieldsets(self, request, obj=None): |
||
1416 | if self.declared_fieldsets: |
||
1417 | return self.declared_fieldsets |
||
1418 | form = self.get_formset(request, obj).form
|
||
1419 | fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj)) |
||
1420 | return [(None, {'fields': fields})] |
||
1421 | |||
1422 | def queryset(self, request): |
||
1423 | queryset = super(InlineModelAdmin, self).queryset(request) |
||
1424 | if not self.has_change_permission(request): |
||
1425 | queryset = queryset.none() |
||
1426 | return queryset
|
||
1427 | |||
1428 | def has_add_permission(self, request): |
||
1429 | if self.opts.auto_created: |
||
1430 | # We're checking the rights to an auto-created intermediate model,
|
||
1431 | # which doesn't have its own individual permissions. The user needs
|
||
1432 | # to have the change permission for the related model in order to
|
||
1433 | # be able to do anything with the intermediate model.
|
||
1434 | return self.has_change_permission(request) |
||
1435 | return request.user.has_perm(
|
||
1436 | self.opts.app_label + '.' + self.opts.get_add_permission()) |
||
1437 | |||
1438 | def has_change_permission(self, request, obj=None): |
||
1439 | opts = self.opts
|
||
1440 | if opts.auto_created:
|
||
1441 | # The model was auto-created as intermediary for a
|
||
1442 | # ManyToMany-relationship, find the target model
|
||
1443 | for field in opts.fields: |
||
1444 | if field.rel and field.rel.to != self.parent_model: |
||
1445 | opts = field.rel.to._meta |
||
1446 | break
|
||
1447 | return request.user.has_perm(
|
||
1448 | opts.app_label + '.' + opts.get_change_permission())
|
||
1449 | |||
1450 | def has_delete_permission(self, request, obj=None): |
||
1451 | if self.opts.auto_created: |
||
1452 | # We're checking the rights to an auto-created intermediate model,
|
||
1453 | # which doesn't have its own individual permissions. The user needs
|
||
1454 | # to have the change permission for the related model in order to
|
||
1455 | # be able to do anything with the intermediate model.
|
||
1456 | return self.has_change_permission(request, obj) |
||
1457 | return request.user.has_perm(
|
||
1458 | self.opts.app_label + '.' + self.opts.get_delete_permission()) |
||
1459 | |||
1460 | class StackedInline(InlineModelAdmin): |
||
1461 | template = 'admin/edit_inline/stacked.html'
|
||
1462 | |||
1463 | class TabularInline(InlineModelAdmin): |
||
1464 | template = 'admin/edit_inline/tabular.html' |