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