Fixed #8936 -- Added a view permission and a read-only admin.

Co-authored-by: Petr Dlouhy <petr.dlouhy@email.cz>
Co-authored-by: Olivier Dalang <olivier.dalang@gmail.com>
This commit is contained in:
olivierdalang 2018-05-02 20:39:12 +12:00 committed by Tim Graham
parent 35b6a348de
commit 825f0beda8
32 changed files with 579 additions and 96 deletions

View File

@ -224,7 +224,9 @@ class InlineAdminFormSet:
A wrapper around an inline formset for use in the admin system. A wrapper around an inline formset for use in the admin system.
""" """
def __init__(self, inline, formset, fieldsets, prepopulated_fields=None, def __init__(self, inline, formset, fieldsets, prepopulated_fields=None,
readonly_fields=None, model_admin=None): readonly_fields=None, model_admin=None, has_add_permission=True,
has_change_permission=True, has_delete_permission=True,
has_view_permission=True):
self.opts = inline self.opts = inline
self.formset = formset self.formset = formset
self.fieldsets = fieldsets self.fieldsets = fieldsets
@ -236,13 +238,21 @@ class InlineAdminFormSet:
prepopulated_fields = {} prepopulated_fields = {}
self.prepopulated_fields = prepopulated_fields self.prepopulated_fields = prepopulated_fields
self.classes = ' '.join(inline.classes) if inline.classes else '' self.classes = ' '.join(inline.classes) if inline.classes else ''
self.has_add_permission = has_add_permission
self.has_change_permission = has_change_permission
self.has_delete_permission = has_delete_permission
self.has_view_permission = has_view_permission
def __iter__(self): def __iter__(self):
readonly_fields_for_editing = self.readonly_fields
if not self.has_change_permission:
readonly_fields_for_editing += flatten_fieldsets(self.fieldsets)
for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()): for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
view_on_site_url = self.opts.get_view_on_site_url(original) view_on_site_url = self.opts.get_view_on_site_url(original)
yield InlineAdminForm( yield InlineAdminForm(
self.formset, form, self.fieldsets, self.prepopulated_fields, self.formset, form, self.fieldsets, self.prepopulated_fields,
original, self.readonly_fields, model_admin=self.opts, original, readonly_fields_for_editing, model_admin=self.opts,
view_on_site_url=view_on_site_url, view_on_site_url=view_on_site_url,
) )
for form in self.formset.extra_forms: for form in self.formset.extra_forms:
@ -250,11 +260,12 @@ class InlineAdminFormSet:
self.formset, form, self.fieldsets, self.prepopulated_fields, self.formset, form, self.fieldsets, self.prepopulated_fields,
None, self.readonly_fields, model_admin=self.opts, None, self.readonly_fields, model_admin=self.opts,
) )
yield InlineAdminForm( if self.has_add_permission:
self.formset, self.formset.empty_form, yield InlineAdminForm(
self.fieldsets, self.prepopulated_fields, None, self.formset, self.formset.empty_form,
self.readonly_fields, model_admin=self.opts, self.fieldsets, self.prepopulated_fields, None,
) self.readonly_fields, model_admin=self.opts,
)
def fields(self): def fields(self):
fk = getattr(self.formset, "fk", None) fk = getattr(self.formset, "fk", None)
@ -264,7 +275,7 @@ class InlineAdminFormSet:
for i, field_name in enumerate(flatten_fieldsets(self.fieldsets)): for i, field_name in enumerate(flatten_fieldsets(self.fieldsets)):
if fk and fk.name == field_name: if fk and fk.name == field_name:
continue continue
if field_name in self.readonly_fields: if not self.has_change_permission or field_name in self.readonly_fields:
yield { yield {
'label': meta_labels.get(field_name) or label_for_field(field_name, self.opts.model, self.opts), 'label': meta_labels.get(field_name) or label_for_field(field_name, self.opts.model, self.opts),
'widget': {'is_hidden': False}, 'widget': {'is_hidden': False},

View File

@ -167,6 +167,7 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
can_add_related=related_modeladmin.has_add_permission(request), can_add_related=related_modeladmin.has_add_permission(request),
can_change_related=related_modeladmin.has_change_permission(request), can_change_related=related_modeladmin.has_change_permission(request),
can_delete_related=related_modeladmin.has_delete_permission(request), can_delete_related=related_modeladmin.has_delete_permission(request),
can_view_related=related_modeladmin.has_view_permission(request),
) )
formfield.widget = widgets.RelatedFieldWidgetWrapper( formfield.widget = widgets.RelatedFieldWidgetWrapper(
formfield.widget, db_field.remote_field, self.admin_site, **wrapper_kwargs formfield.widget, db_field.remote_field, self.admin_site, **wrapper_kwargs
@ -497,6 +498,25 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
codename = get_permission_codename('delete', opts) codename = get_permission_codename('delete', opts)
return request.user.has_perm("%s.%s" % (opts.app_label, codename)) return request.user.has_perm("%s.%s" % (opts.app_label, codename))
def has_view_permission(self, request, obj=None):
"""
Return True if the given request has permission to view the given
Django model instance. The default implementation doesn't examine the
`obj` parameter.
If overridden by the user in subclasses, it should return True if the
given request has permission to view the `obj` model instance. If `obj`
is None, it should return True if the request has permission to view
any object of the given type.
"""
opts = self.opts
codename_view = get_permission_codename('view', opts)
codename_change = get_permission_codename('change', opts)
return (
request.user.has_perm('%s.%s' % (opts.app_label, codename_view)) or
request.user.has_perm('%s.%s' % (opts.app_label, codename_change))
)
def has_module_permission(self, request): def has_module_permission(self, request):
""" """
Return True if the given request has any permission in the given Return True if the given request has any permission in the given
@ -567,7 +587,8 @@ class ModelAdmin(BaseModelAdmin):
else: else:
inline_has_add_permission = inline.has_add_permission(request) inline_has_add_permission = inline.has_add_permission(request)
if request: if request:
if not (inline_has_add_permission or if not (inline.has_view_permission(request, obj) or
inline_has_add_permission or
inline.has_change_permission(request, obj) or inline.has_change_permission(request, obj) or
inline.has_delete_permission(request, obj)): inline.has_delete_permission(request, obj)):
continue continue
@ -624,19 +645,20 @@ class ModelAdmin(BaseModelAdmin):
def get_model_perms(self, request): def get_model_perms(self, request):
""" """
Return a dict of all perms for this model. This dict has the keys Return a dict of all perms for this model. This dict has the keys
``add``, ``change``, and ``delete`` mapping to the True/False for each ``add``, ``change``, ``delete``, and ``view`` mapping to the True/False
of those actions. for each of those actions.
""" """
return { return {
'add': self.has_add_permission(request), 'add': self.has_add_permission(request),
'change': self.has_change_permission(request), 'change': self.has_change_permission(request),
'delete': self.has_delete_permission(request), 'delete': self.has_delete_permission(request),
'view': self.has_view_permission(request),
} }
def _get_form_for_get_fields(self, request, obj): def _get_form_for_get_fields(self, request, obj):
return self.get_form(request, obj, fields=None) return self.get_form(request, obj, fields=None)
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, change=False, **kwargs):
""" """
Return a Form class for use in the admin add view. This is used by Return a Form class for use in the admin add view. This is used by
add_view and change_view. add_view and change_view.
@ -649,6 +671,10 @@ class ModelAdmin(BaseModelAdmin):
exclude = [] if excluded is None else list(excluded) exclude = [] if excluded is None else list(excluded)
readonly_fields = self.get_readonly_fields(request, obj) readonly_fields = self.get_readonly_fields(request, obj)
exclude.extend(readonly_fields) exclude.extend(readonly_fields)
# Exclude all fields if it's a change form and the user doesn't have
# the change permission.
if change and hasattr(request, 'user') and not self.has_change_permission(request, obj):
exclude.extend(fields)
if excluded is None and hasattr(self.form, '_meta') and self.form._meta.exclude: if excluded is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
# Take the custom ModelForm's Meta.exclude into account only if the # Take the custom ModelForm's Meta.exclude into account only if the
# ModelAdmin doesn't define its own. # ModelAdmin doesn't define its own.
@ -834,6 +860,9 @@ class ModelAdmin(BaseModelAdmin):
# want *any* actions enabled on this page. # want *any* actions enabled on this page.
if self.actions is None or IS_POPUP_VAR in request.GET: if self.actions is None or IS_POPUP_VAR in request.GET:
return OrderedDict() return OrderedDict()
# The change permission is required to use actions.
if not self.has_change_permission(request):
return OrderedDict()
actions = [] actions = []
@ -1082,12 +1111,19 @@ class ModelAdmin(BaseModelAdmin):
preserved_filters = self.get_preserved_filters(request) preserved_filters = self.get_preserved_filters(request)
form_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, form_url) form_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, form_url)
view_on_site_url = self.get_view_on_site_url(obj) view_on_site_url = self.get_view_on_site_url(obj)
has_editable_inline_admin_formsets = False
for inline in context['inline_admin_formsets']:
if inline.has_add_permission or inline.has_change_permission or inline.has_delete_permission:
has_editable_inline_admin_formsets = True
break
context.update({ context.update({
'add': add, 'add': add,
'change': change, 'change': change,
'has_view_permission': self.has_view_permission(request, obj),
'has_add_permission': self.has_add_permission(request), 'has_add_permission': self.has_add_permission(request),
'has_change_permission': self.has_change_permission(request, obj), 'has_change_permission': self.has_change_permission(request, obj),
'has_delete_permission': self.has_delete_permission(request, obj), 'has_delete_permission': self.has_delete_permission(request, obj),
'has_editable_inline_admin_formsets': has_editable_inline_admin_formsets,
'has_file_field': context['adminform'].form.is_multipart() or any( 'has_file_field': context['adminform'].form.is_multipart() or any(
admin_formset.formset.form().is_multipart() admin_formset.formset.form().is_multipart()
for admin_formset in context['inline_admin_formsets'] for admin_formset in context['inline_admin_formsets']
@ -1163,11 +1199,10 @@ class ModelAdmin(BaseModelAdmin):
"_saveasnew" in request.POST and self.save_as_continue and "_saveasnew" in request.POST and self.save_as_continue and
self.has_change_permission(request, obj) self.has_change_permission(request, obj)
): ):
msg = format_html( msg = _('The {name} "{obj}" was added successfully.')
_('The {name} "{obj}" was added successfully. You may edit it again below.'), if self.has_change_permission(request, obj):
**msg_dict msg += ' ' + _('You may edit it again below.')
) self.message_user(request, format_html(msg, **msg_dict), messages.SUCCESS)
self.message_user(request, msg, messages.SUCCESS)
if post_url_continue is None: if post_url_continue is None:
post_url_continue = obj_url post_url_continue = obj_url
post_url_continue = add_preserved_filters( post_url_continue = add_preserved_filters(
@ -1438,10 +1473,15 @@ class ModelAdmin(BaseModelAdmin):
for inline, formset in zip(inline_instances, formsets): for inline, formset in zip(inline_instances, formsets):
fieldsets = list(inline.get_fieldsets(request, obj)) fieldsets = list(inline.get_fieldsets(request, obj))
readonly = list(inline.get_readonly_fields(request, obj)) readonly = list(inline.get_readonly_fields(request, obj))
has_add_permission = inline.has_add_permission(request, obj)
has_change_permission = inline.has_change_permission(request, obj)
has_delete_permission = inline.has_delete_permission(request, obj)
has_view_permission = inline.has_view_permission(request, obj)
prepopulated = dict(inline.get_prepopulated_fields(request, obj)) prepopulated = dict(inline.get_prepopulated_fields(request, obj))
inline_admin_formset = helpers.InlineAdminFormSet( inline_admin_formset = helpers.InlineAdminFormSet(
inline, formset, fieldsets, prepopulated, readonly, inline, formset, fieldsets, prepopulated, readonly, model_admin=self,
model_admin=self, has_add_permission=has_add_permission, has_change_permission=has_change_permission,
has_delete_permission=has_delete_permission, has_view_permission=has_view_permission,
) )
inline_admin_formsets.append(inline_admin_formset) inline_admin_formsets.append(inline_admin_formset)
return inline_admin_formsets return inline_admin_formsets
@ -1500,13 +1540,13 @@ class ModelAdmin(BaseModelAdmin):
else: else:
obj = self.get_object(request, unquote(object_id), to_field) obj = self.get_object(request, unquote(object_id), to_field)
if not self.has_change_permission(request, obj): if not self.has_view_permission(request, obj) and not self.has_change_permission(request, obj):
raise PermissionDenied raise PermissionDenied
if obj is None: if obj is None:
return self._get_obj_does_not_exist_redirect(request, opts, object_id) return self._get_obj_does_not_exist_redirect(request, opts, object_id)
ModelForm = self.get_form(request, obj) ModelForm = self.get_form(request, obj, change=not add)
if request.method == 'POST': if request.method == 'POST':
form = ModelForm(request.POST, request.FILES, instance=obj) form = ModelForm(request.POST, request.FILES, instance=obj)
form_validated = form.is_valid() form_validated = form.is_valid()
@ -1536,11 +1576,15 @@ class ModelAdmin(BaseModelAdmin):
form = ModelForm(instance=obj) form = ModelForm(instance=obj)
formsets, inline_instances = self._create_formsets(request, obj, change=True) formsets, inline_instances = self._create_formsets(request, obj, change=True)
if not add and not self.has_change_permission(request):
readonly_fields = flatten_fieldsets(self.get_fieldsets(request, obj))
else:
readonly_fields = self.get_readonly_fields(request, obj)
adminForm = helpers.AdminForm( adminForm = helpers.AdminForm(
form, form,
list(self.get_fieldsets(request, obj)), list(self.get_fieldsets(request, obj)),
self.get_prepopulated_fields(request, obj), self.get_prepopulated_fields(request, obj),
self.get_readonly_fields(request, obj), readonly_fields,
model_admin=self) model_admin=self)
media = self.media + adminForm.media media = self.media + adminForm.media
@ -1591,7 +1635,7 @@ class ModelAdmin(BaseModelAdmin):
from django.contrib.admin.views.main import ERROR_FLAG from django.contrib.admin.views.main import ERROR_FLAG
opts = self.model._meta opts = self.model._meta
app_label = opts.app_label app_label = opts.app_label
if not self.has_change_permission(request, None): if not self.has_view_permission(request) and not self.has_change_permission(request):
raise PermissionDenied raise PermissionDenied
try: try:
@ -1620,6 +1664,8 @@ class ModelAdmin(BaseModelAdmin):
# Actions with no confirmation # Actions with no confirmation
if (actions and request.method == 'POST' and if (actions and request.method == 'POST' and
'index' in request.POST and '_save' not in request.POST): 'index' in request.POST and '_save' not in request.POST):
if not self.has_change_permission(request):
raise PermissionDenied
if selected: if selected:
response = self.response_action(request, queryset=cl.get_queryset(request)) response = self.response_action(request, queryset=cl.get_queryset(request))
if response: if response:
@ -1636,6 +1682,8 @@ class ModelAdmin(BaseModelAdmin):
if (actions and request.method == 'POST' and if (actions and request.method == 'POST' and
helpers.ACTION_CHECKBOX_NAME in request.POST and helpers.ACTION_CHECKBOX_NAME in request.POST and
'index' not in request.POST and '_save' not in request.POST): 'index' not in request.POST and '_save' not in request.POST):
if not self.has_change_permission(request):
raise PermissionDenied
if selected: if selected:
response = self.response_action(request, queryset=cl.get_queryset(request)) response = self.response_action(request, queryset=cl.get_queryset(request))
if response: if response:
@ -1656,6 +1704,8 @@ class ModelAdmin(BaseModelAdmin):
# Handle POSTed bulk-edit data. # Handle POSTed bulk-edit data.
if request.method == 'POST' and cl.list_editable and '_save' in request.POST: if request.method == 'POST' and cl.list_editable and '_save' in request.POST:
if not self.has_change_permission(request):
raise PermissionDenied
FormSet = self.get_changelist_formset(request) FormSet = self.get_changelist_formset(request)
formset = cl.formset = FormSet(request.POST, request.FILES, queryset=self.get_queryset(request)) formset = cl.formset = FormSet(request.POST, request.FILES, queryset=self.get_queryset(request))
if formset.is_valid(): if formset.is_valid():
@ -1683,7 +1733,7 @@ class ModelAdmin(BaseModelAdmin):
return HttpResponseRedirect(request.get_full_path()) return HttpResponseRedirect(request.get_full_path())
# Handle GET -- construct a formset for display. # Handle GET -- construct a formset for display.
elif cl.list_editable: elif cl.list_editable and self.has_change_permission(request):
FormSet = self.get_changelist_formset(request) FormSet = self.get_changelist_formset(request)
formset = cl.formset = FormSet(queryset=cl.result_list) formset = cl.formset = FormSet(queryset=cl.result_list)
@ -1814,7 +1864,7 @@ class ModelAdmin(BaseModelAdmin):
if obj is None: if obj is None:
return self._get_obj_does_not_exist_redirect(request, model._meta, object_id) return self._get_obj_does_not_exist_redirect(request, model._meta, object_id)
if not self.has_change_permission(request, obj): if not self.has_view_permission(request, obj) and not self.has_change_permission(request, obj):
raise PermissionDenied raise PermissionDenied
# Then get the history for this object. # Then get the history for this object.
@ -1961,8 +2011,17 @@ class InlineModelAdmin(BaseModelAdmin):
} }
base_model_form = defaults['form'] base_model_form = defaults['form']
can_change = self.has_change_permission(request, obj) if request else True
can_add = self.has_add_permission(request, obj) if request else True
class DeleteProtectedModelForm(base_model_form): class DeleteProtectedModelForm(base_model_form):
def __init__(self, *args, **kwargs):
super(DeleteProtectedModelForm, self).__init__(*args, **kwargs)
if not can_change and not self.instance._state.adding:
self.fields = {}
if not can_add and self.instance._state.adding:
self.fields = {}
def hand_clean_DELETE(self): def hand_clean_DELETE(self):
""" """
We don't validate the 'DELETE' field itself because on We don't validate the 'DELETE' field itself because on
@ -1972,7 +2031,7 @@ class InlineModelAdmin(BaseModelAdmin):
if self.cleaned_data.get(DELETION_FIELD_NAME, False): if self.cleaned_data.get(DELETION_FIELD_NAME, False):
using = router.db_for_write(self._meta.model) using = router.db_for_write(self._meta.model)
collector = NestedObjects(using=using) collector = NestedObjects(using=using)
if self.instance.pk is None: if self.instance._state.adding:
return return
collector.collect([self.instance]) collector.collect([self.instance])
if collector.protected: if collector.protected:
@ -2010,7 +2069,7 @@ class InlineModelAdmin(BaseModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
queryset = super().get_queryset(request) queryset = super().get_queryset(request)
if not self.has_change_permission(request): if not self.has_change_permission(request) and not self.has_view_permission(request):
queryset = queryset.none() queryset = queryset.none()
return queryset return queryset
@ -2018,32 +2077,44 @@ class InlineModelAdmin(BaseModelAdmin):
if self.opts.auto_created: if self.opts.auto_created:
# We're checking the rights to an auto-created intermediate model, # We're checking the rights to an auto-created intermediate model,
# which doesn't have its own individual permissions. The user needs # which doesn't have its own individual permissions. The user needs
# to have the change permission for the related model in order to # to have the view permission for the related model in order to
# be able to do anything with the intermediate model. # be able to do anything with the intermediate model.
return self.has_change_permission(request, obj) return self.has_view_permission(request, obj)
return super().has_add_permission(request) return super().has_add_permission(request)
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
opts = self.opts if self.opts.auto_created:
if opts.auto_created: # We're checking the rights to an auto-created intermediate model,
# The model was auto-created as intermediary for a # which doesn't have its own individual permissions. The user needs
# ManyToMany-relationship, find the target model # to have the view permission for the related model in order to
for field in opts.fields: # be able to do anything with the intermediate model.
if field.remote_field and field.remote_field.model != self.parent_model: return self.has_view_permission(request, obj)
opts = field.remote_field.model._meta return super().has_change_permission(request)
break
codename = get_permission_codename('change', opts)
return request.user.has_perm("%s.%s" % (opts.app_label, codename))
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
if self.opts.auto_created: if self.opts.auto_created:
# We're checking the rights to an auto-created intermediate model, # We're checking the rights to an auto-created intermediate model,
# which doesn't have its own individual permissions. The user needs # which doesn't have its own individual permissions. The user needs
# to have the change permission for the related model in order to # to have the view permission for the related model in order to
# be able to do anything with the intermediate model. # be able to do anything with the intermediate model.
return self.has_change_permission(request, obj) return self.has_view_permission(request, obj)
return super().has_delete_permission(request, obj) return super().has_delete_permission(request, obj)
def has_view_permission(self, request, obj=None):
if self.opts.auto_created:
opts = self.opts
# The model was auto-created as intermediary for a many-to-many
# Many-relationship; find the target model.
for field in opts.fields:
if field.remote_field and field.remote_field.model != self.parent_model:
opts = field.remote_field.model._meta
break
return (
request.user.has_perm('%s.%s' % (opts.app_label, get_permission_codename('view', opts))) or
request.user.has_perm('%s.%s' % (opts.app_label, get_permission_codename('change', opts)))
)
return super().has_view_permission(request)
class StackedInline(InlineModelAdmin): class StackedInline(InlineModelAdmin):
template = 'admin/edit_inline/stacked.html' template = 'admin/edit_inline/stacked.html'

View File

@ -432,7 +432,8 @@ class AdminSite:
'object_name': model._meta.object_name, 'object_name': model._meta.object_name,
'perms': perms, 'perms': perms,
} }
if perms.get('change'): if perms.get('change') or perms.get('view'):
model_dict['view_only'] = not perms.get('change')
try: try:
model_dict['admin_url'] = reverse('admin:%s_%s_changelist' % info, current_app=self.name) model_dict['admin_url'] = reverse('admin:%s_%s_changelist' % info, current_app=self.name)
except NoReverseMatch: except NoReverseMatch:

View File

@ -662,6 +662,11 @@ div.breadcrumbs a:focus, div.breadcrumbs a:hover {
/* ACTION ICONS */ /* ACTION ICONS */
.viewlink, .inlineviewlink {
padding-left: 16px;
background: url(../img/icon-viewlink.svg) 0 1px no-repeat;
}
.addlink { .addlink {
padding-left: 16px; padding-left: 16px;
background: url(../img/icon-addlink.svg) 0 1px no-repeat; background: url(../img/icon-addlink.svg) 0 1px no-repeat;

View File

@ -291,12 +291,29 @@ body.popup .submit-row {
color: #fff; color: #fff;
} }
.submit-row a.closelink {
display: inline-block;
background: #bbbbbb;
border-radius: 4px;
padding: 10px 15px;
height: 15px;
line-height: 15px;
margin: 0 0 0 5px;
color: #fff;
}
.submit-row a.deletelink:focus, .submit-row a.deletelink:focus,
.submit-row a.deletelink:hover, .submit-row a.deletelink:hover,
.submit-row a.deletelink:active { .submit-row a.deletelink:active {
background: #a41515; background: #a41515;
} }
.submit-row a.closelink:focus,
.submit-row a.closelink:hover,
.submit-row a.closelink:active {
background: #aaaaaa;
}
/* CUSTOM FORM FIELDS */ /* CUSTOM FORM FIELDS */
.vSelectMultipleField { .vSelectMultipleField {

View File

@ -810,12 +810,16 @@ input[type="submit"], button {
width: 100%; width: 100%;
} }
.submit-row input, .submit-row input.default, .submit-row a { .submit-row input, .submit-row input.default, .submit-row a, .submit-row a.closelink {
float: none; float: none;
margin: 0 0 10px; margin: 0 0 10px;
text-align: center; text-align: center;
} }
.submit-row a.closelink {
padding: 10px 0;
}
.submit-row p.deletelink-box { .submit-row p.deletelink-box {
order: 4; order: 4;
} }

View File

@ -35,7 +35,7 @@ th {
margin-right: 1.5em; margin-right: 1.5em;
} }
.addlink, .changelink { .viewlink, .addlink, .changelink {
padding-left: 0; padding-left: 0;
padding-right: 16px; padding-right: 16px;
background-position: 100% 1px; background-position: 100% 1px;

View File

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
<path fill="#2b70bf" d="M1664 960q-152-236-381-353 61 104 61 225 0 185-131.5 316.5t-316.5 131.5-316.5-131.5-131.5-316.5q0-121 61-225-229 117-381 353 133 205 333.5 326.5t434.5 121.5 434.5-121.5 333.5-326.5zm-720-384q0-20-14-34t-34-14q-125 0-214.5 89.5t-89.5 214.5q0 20 14 34t34 14 34-14 14-34q0-86 61-147t147-61q20 0 34-14t14-34zm848 384q0 34-20 69-140 230-376.5 368.5t-499.5 138.5-499.5-139-376.5-368q-20-35-20-69t20-69q140-229 376.5-368t499.5-139 499.5 139 376.5 368q20 35 20 69z"/>
</svg>

After

Width:  |  Height:  |  Size: 581 B

View File

@ -58,7 +58,7 @@
function updateRelatedObjectLinks(triggeringLink) { function updateRelatedObjectLinks(triggeringLink) {
var $this = $(triggeringLink); var $this = $(triggeringLink);
var siblings = $this.nextAll('.change-related, .delete-related'); var siblings = $this.nextAll('.view-related, .change-related, .delete-related');
if (!siblings.length) { if (!siblings.length) {
return; return;
} }

View File

@ -17,7 +17,7 @@
<div class="breadcrumbs"> <div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a> <a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a> &rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; {% if has_change_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %} &rsaquo; {% if has_view_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}
&rsaquo; {% if add %}{% blocktrans with name=opts.verbose_name %}Add {{ name }}{% endblocktrans %}{% else %}{{ original|truncatewords:"18" }}{% endif %} &rsaquo; {% if add %}{% blocktrans with name=opts.verbose_name %}Add {{ name }}{% endblocktrans %}{% else %}{{ original|truncatewords:"18" }}{% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -8,8 +8,8 @@
{{ inline_admin_formset.formset.management_form }} {{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }} {{ inline_admin_formset.formset.non_form_errors }}
{% for inline_admin_form in inline_admin_formset %}<div class="inline-related{% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if forloop.last %} empty-form last-related{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}"> {% for inline_admin_form in inline_admin_formset %}<div class="inline-related{% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if forloop.last and inline_admin_formset.has_add_permission %} empty-form last-related{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
<h3><b>{{ inline_admin_formset.opts.verbose_name|capfirst }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} <a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="inlinechangelink">{% trans "Change" %}</a>{% endif %} <h3><b>{{ inline_admin_formset.opts.verbose_name|capfirst }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} <a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="{% if inline_admin_formset.has_change_permission %}inlinechangelink{% else %}inlineviewlink{% endif %}">{% if inline_admin_formset.has_change_permission %}{% trans "Change" %}{% else %}{% trans "View" %}{% endif %}</a>{% endif %}
{% else %}#{{ forloop.counter }}{% endif %}</span> {% else %}#{{ forloop.counter }}{% endif %}</span>
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% endif %} {% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% endif %}
{% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %} {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}

View File

@ -25,13 +25,13 @@
{% if inline_admin_form.form.non_field_errors %} {% if inline_admin_form.form.non_field_errors %}
<tr><td colspan="{{ inline_admin_form|cell_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr> <tr><td colspan="{{ inline_admin_form|cell_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr>
{% endif %} {% endif %}
<tr class="form-row {% cycle "row1" "row2" %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last %} empty-form{% endif %}" <tr class="form-row {% cycle "row1" "row2" %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last and inline_admin_formset.has_add_permission %} empty-form{% endif %}"
id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}"> id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
<td class="original"> <td class="original">
{% if inline_admin_form.original or inline_admin_form.show_url %}<p> {% if inline_admin_form.original or inline_admin_form.show_url %}<p>
{% if inline_admin_form.original %} {% if inline_admin_form.original %}
{{ inline_admin_form.original }} {{ inline_admin_form.original }}
{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}<a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="inlinechangelink">{% trans "Change" %}</a>{% endif %} {% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}<a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="{% if inline_admin_formset.has_change_permission %}inlinechangelink{% else %}inlineviewlink{% endif %}">{% if inline_admin_formset.has_change_permission %}{% trans "Change" %}{% else %}{% trans "View" %}{% endif %}</a>{% endif %}
{% endif %} {% endif %}
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% endif %} {% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% endif %}
</p>{% endif %} </p>{% endif %}

View File

@ -34,7 +34,11 @@
{% endif %} {% endif %}
{% if model.admin_url %} {% if model.admin_url %}
{% if model.view_only %}
<td><a href="{{ model.admin_url }}" class="viewlink">{% trans 'View' %}</a></td>
{% else %}
<td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td> <td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
{% endif %}
{% else %} {% else %}
<td>&nbsp;</td> <td>&nbsp;</td>
{% endif %} {% endif %}
@ -44,7 +48,7 @@
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}
<p>{% trans "You don't have permission to edit anything." %}</p> <p>{% trans "You don't have permission to view or edit anything." %}</p>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -3,11 +3,17 @@
{{ widget }} {{ widget }}
{% block links %} {% block links %}
{% spaceless %} {% spaceless %}
{% if can_change_related %} {% if can_change_related or can_view_related %}
<a class="related-widget-wrapper-link change-related" id="change_id_{{ name }}" <a class="related-widget-wrapper-link {% if can_change_related %}change-related{% else %}view-related{% endif %}"
id="change_id_{{ name }}"
data-href-template="{{ change_related_template_url }}?{{ url_params }}" data-href-template="{{ change_related_template_url }}?{{ url_params }}"
{% if can_change_related %}
title="{% blocktrans %}Change selected {{ model }}{% endblocktrans %}"> title="{% blocktrans %}Change selected {{ model }}{% endblocktrans %}">
<img src="{% static 'admin/img/icon-changelink.svg' %}" alt="{% trans 'Change' %}"> <img src="{% static 'admin/img/icon-changelink.svg' %}" alt="{% trans 'Change' %}">
{% else %}
title="{% blocktrans %}View selected {{ model }}{% endblocktrans %}">
<img src="{% static 'admin/img/icon-viewlink.svg' %}" alt="{% trans 'View' %}">
{% endif %}
</a> </a>
{% endif %} {% endif %}
{% if can_add_related %} {% if can_add_related %}

View File

@ -8,6 +8,7 @@
{% endif %} {% endif %}
{% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew">{% endif %} {% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew">{% endif %}
{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother">{% endif %} {% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother">{% endif %}
{% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue">{% endif %} {% if show_save_and_continue %}<input type="submit" value="{% if can_change %}{% trans 'Save and continue editing' %}{% else %}{% trans 'Save and view' %}{% endif %}" name="_continue">{% endif %}
{% if show_close %}<a href="{% url opts|admin_urlname:'changelist' %}" class="closelink">{% trans 'Close' %}</a>{% endif %}
{% endblock %} {% endblock %}
</div> </div>

View File

@ -49,24 +49,34 @@ def submit_row(context):
""" """
Display the row of buttons for delete and save. Display the row of buttons for delete and save.
""" """
add = context['add']
change = context['change'] change = context['change']
is_popup = context['is_popup'] is_popup = context['is_popup']
save_as = context['save_as'] save_as = context['save_as']
show_save = context.get('show_save', True) show_save = context.get('show_save', True)
show_save_and_continue = context.get('show_save_and_continue', True) show_save_and_continue = context.get('show_save_and_continue', True)
has_add_permission = context['has_add_permission']
has_change_permission = context['has_change_permission']
has_view_permission = context['has_view_permission']
has_editable_inline_admin_formsets = context['has_editable_inline_admin_formsets']
can_save = (has_change_permission and change) or (has_add_permission and add) or has_editable_inline_admin_formsets
can_save_and_continue = not is_popup and can_save and has_view_permission and show_save_and_continue
can_change = has_change_permission or has_editable_inline_admin_formsets
ctx = Context(context) ctx = Context(context)
ctx.update({ ctx.update({
'can_change': can_change,
'show_delete_link': ( 'show_delete_link': (
not is_popup and context['has_delete_permission'] and not is_popup and context['has_delete_permission'] and
change and context.get('show_delete', True) change and context.get('show_delete', True)
), ),
'show_save_as_new': not is_popup and change and save_as, 'show_save_as_new': not is_popup and has_change_permission and change and save_as,
'show_save_and_add_another': ( 'show_save_and_add_another': (
context['has_add_permission'] and not is_popup and has_add_permission and not is_popup and
(not save_as or context['add']) (not save_as or add) and can_save
), ),
'show_save_and_continue': not is_popup and context['has_change_permission'] and show_save_and_continue, 'show_save_and_continue': can_save_and_continue,
'show_save': show_save, 'show_save': show_save and can_save,
'show_close': not(show_save and can_save)
}) })
return ctx return ctx

View File

@ -239,7 +239,8 @@ class RelatedFieldWidgetWrapper(forms.Widget):
template_name = 'admin/widgets/related_widget_wrapper.html' template_name = 'admin/widgets/related_widget_wrapper.html'
def __init__(self, widget, rel, admin_site, can_add_related=None, def __init__(self, widget, rel, admin_site, can_add_related=None,
can_change_related=False, can_delete_related=False): can_change_related=False, can_delete_related=False,
can_view_related=False):
self.needs_multipart_form = widget.needs_multipart_form self.needs_multipart_form = widget.needs_multipart_form
self.attrs = widget.attrs self.attrs = widget.attrs
self.choices = widget.choices self.choices = widget.choices
@ -256,6 +257,7 @@ class RelatedFieldWidgetWrapper(forms.Widget):
# XXX: The deletion UX can be confusing when dealing with cascading deletion. # XXX: The deletion UX can be confusing when dealing with cascading deletion.
cascade = getattr(rel, 'on_delete', None) is CASCADE cascade = getattr(rel, 'on_delete', None) is CASCADE
self.can_delete_related = not multiple and not cascade and can_delete_related self.can_delete_related = not multiple and not cascade and can_delete_related
self.can_view_related = not multiple and can_view_related
# so we can check if the related object is registered with this AdminSite # so we can check if the related object is registered with this AdminSite
self.admin_site = admin_site self.admin_site = admin_site
@ -292,25 +294,17 @@ class RelatedFieldWidgetWrapper(forms.Widget):
'name': name, 'name': name,
'url_params': url_params, 'url_params': url_params,
'model': rel_opts.verbose_name, 'model': rel_opts.verbose_name,
'can_add_related': self.can_add_related,
'can_change_related': self.can_change_related,
'can_delete_related': self.can_delete_related,
'can_view_related': self.can_view_related,
} }
if self.can_change_related:
change_related_template_url = self.get_related_url(info, 'change', '__fk__')
context.update(
can_change_related=True,
change_related_template_url=change_related_template_url,
)
if self.can_add_related: if self.can_add_related:
add_related_url = self.get_related_url(info, 'add') context['add_related_url'] = self.get_related_url(info, 'add')
context.update(
can_add_related=True,
add_related_url=add_related_url,
)
if self.can_delete_related: if self.can_delete_related:
delete_related_template_url = self.get_related_url(info, 'delete', '__fk__') context['delete_related_template_url'] = self.get_related_url(info, 'delete', '__fk__')
context.update( if self.can_view_related or self.can_change_related:
can_delete_related=True, context['change_related_template_url'] = self.get_related_url(info, 'change', '__fk__')
delete_related_template_url=delete_related_template_url,
)
return context return context
def value_from_datadict(self, data, files, name): def value_from_datadict(self, data, files, name):

View File

@ -22,7 +22,7 @@ def _get_all_permissions(opts):
def _get_builtin_permissions(opts): def _get_builtin_permissions(opts):
""" """
Return (codename, name) for all autogenerated permissions. Return (codename, name) for all autogenerated permissions.
By default, this is ('add', 'change', 'delete') By default, this is ('add', 'change', 'delete', 'view')
""" """
perms = [] perms = []
for action in opts.default_permissions: for action in opts.default_permissions:

View File

@ -92,7 +92,7 @@ class Options:
self.unique_together = [] self.unique_together = []
self.index_together = [] self.index_together = []
self.select_on_save = False self.select_on_save = False
self.default_permissions = ('add', 'change', 'delete') self.default_permissions = ('add', 'change', 'delete', 'view')
self.permissions = [] self.permissions = []
self.object_name = None self.object_name = None
self.app_label = app_label self.app_label = app_label

View File

@ -340,6 +340,9 @@ Conditionally enabling or disabling actions
Finally, you can conditionally enable or disable actions on a per-request Finally, you can conditionally enable or disable actions on a per-request
(and hence per-user basis) by overriding :meth:`ModelAdmin.get_actions`. (and hence per-user basis) by overriding :meth:`ModelAdmin.get_actions`.
This doesn't return any actions if the user doesn't have the "change"
permission for the model.
This returns a dictionary of actions allowed. The keys are action names, and This returns a dictionary of actions allowed. The keys are action names, and
the values are ``(function, name, short_description)`` tuples. the values are ``(function, name, short_description)`` tuples.

View File

@ -1623,7 +1623,7 @@ templates used by the :class:`ModelAdmin` views:
a ``list`` or ``tuple`` of :class:`~django.contrib.admin.InlineModelAdmin` a ``list`` or ``tuple`` of :class:`~django.contrib.admin.InlineModelAdmin`
objects, as described below in the :class:`~django.contrib.admin.InlineModelAdmin` objects, as described below in the :class:`~django.contrib.admin.InlineModelAdmin`
section. For example, the following would return inlines without the default section. For example, the following would return inlines without the default
filtering based on add, change, and delete permissions:: filtering based on add, change, delete, and view permissions::
class MyModelAdmin(admin.ModelAdmin): class MyModelAdmin(admin.ModelAdmin):
inlines = (MyInline,) inlines = (MyInline,)
@ -1887,6 +1887,19 @@ templates used by the :class:`ModelAdmin` views:
Override this method to customize the lookups permitted for your Override this method to customize the lookups permitted for your
:class:`~django.contrib.admin.ModelAdmin` subclass. :class:`~django.contrib.admin.ModelAdmin` subclass.
.. method:: ModelAdmin.has_view_permission(request, obj=None)
.. versionadded:: 2.1
Should return ``True`` if viewing ``obj`` is permitted, ``False`` otherwise.
If obj is ``None``, should return ``True`` or ``False`` to indicate whether
viewing of objects of this type is permitted in general (e.g., ``False``
will be interpreted as meaning that the current user is not permitted to
view any object of this type).
The default implementation returns ``True`` if the user has either the
"change" or "view" permission.
.. method:: ModelAdmin.has_add_permission(request) .. method:: ModelAdmin.has_add_permission(request)
Should return ``True`` if adding an object is permitted, ``False`` Should return ``True`` if adding an object is permitted, ``False``
@ -1914,7 +1927,8 @@ templates used by the :class:`ModelAdmin` views:
accessing the module's index page is permitted, ``False`` otherwise. accessing the module's index page is permitted, ``False`` otherwise.
Uses :meth:`User.has_module_perms() Uses :meth:`User.has_module_perms()
<django.contrib.auth.models.User.has_module_perms>` by default. Overriding <django.contrib.auth.models.User.has_module_perms>` by default. Overriding
it does not restrict access to the add, change or delete views, it does not restrict access to the view, add, change, or delete views,
:meth:`~ModelAdmin.has_view_permission`,
:meth:`~ModelAdmin.has_add_permission`, :meth:`~ModelAdmin.has_add_permission`,
:meth:`~ModelAdmin.has_change_permission`, and :meth:`~ModelAdmin.has_change_permission`, and
:meth:`~ModelAdmin.has_delete_permission` should be used for that. :meth:`~ModelAdmin.has_delete_permission` should be used for that.
@ -2862,7 +2876,8 @@ Templates can override or extend base admin templates as described in
* ``object_name``: class name of the model * ``object_name``: class name of the model
* ``name``: plural name of the model * ``name``: plural name of the model
* ``perms``: a ``dict`` tracking ``add``, ``change``, and ``delete`` permissions * ``perms``: a ``dict`` tracking ``add``, ``change``, ``delete``, and
``view`` permissions
* ``admin_url``: admin changelist URL for the model * ``admin_url``: admin changelist URL for the model
* ``add_url``: admin URL to add a new model instance * ``add_url``: admin URL to add a new model instance

View File

@ -313,7 +313,7 @@ Django quotes column and table names behind the scenes.
.. attribute:: Options.permissions .. attribute:: Options.permissions
Extra permissions to enter into the permissions table when creating this object. Extra permissions to enter into the permissions table when creating this object.
Add, delete and change permissions are automatically created for each Add, change, delete, and view permissions are automatically created for each
model. This example specifies an extra permission, ``can_deliver_pizzas``:: model. This example specifies an extra permission, ``can_deliver_pizzas``::
permissions = (("can_deliver_pizzas", "Can deliver pizzas"),) permissions = (("can_deliver_pizzas", "Can deliver pizzas"),)
@ -326,11 +326,15 @@ Django quotes column and table names behind the scenes.
.. attribute:: Options.default_permissions .. attribute:: Options.default_permissions
Defaults to ``('add', 'change', 'delete')``. You may customize this list, Defaults to ``('add', 'change', 'delete', 'view')``. You may customize this
for example, by setting this to an empty list if your app doesn't require list, for example, by setting this to an empty list if your app doesn't
any of the default permissions. It must be specified on the model before require any of the default permissions. It must be specified on the model
the model is created by :djadmin:`migrate` in order to prevent any omitted before the model is created by :djadmin:`migrate` in order to prevent any
permissions from being created. omitted permissions from being created.
.. versionchanged:: 2.1
The ``view`` permission was added.
``proxy`` ``proxy``
--------- ---------

View File

@ -26,6 +26,21 @@ latest release of each series.
What's new in Django 2.1 What's new in Django 2.1
======================== ========================
Model "view" permission
-----------------------
A "view" permission is added to the model :attr:`Meta.default_permissions
<django.db.models.Options.default_permissions>`. The new permissions will be
create automatically when running :djadmin:`migrate`.
This allows giving users read-only access to models in the admin.
:meth:`.ModelAdmin.has_view_permission` is new. The implementation is backwards
compatible in that there isn't a need to assign the "view" permission to allow
users who have the "change" permission to edit objects.
There are a couple of :ref:`backwards incompatible considerations
<view_permission_backwards_incompatible>`.
Minor features Minor features
-------------- --------------
@ -372,6 +387,34 @@ cross-origin requests. If you rely on the old behavior, set the
:setting:`SESSION_COOKIE_SAMESITE` and/or :setting:`CSRF_COOKIE_SAMESITE` :setting:`SESSION_COOKIE_SAMESITE` and/or :setting:`CSRF_COOKIE_SAMESITE`
setting to ``None``. setting to ``None``.
.. _view_permission_backwards_incompatible:
Considerations for the new model "view" permission
--------------------------------------------------
Custom admin forms need to take the view-only case into account
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
With the new "view" permission, existing custom admin forms may raise errors
when a user doesn't have the change permission because the form might access
nonexistent fields. Fix this by overriding :meth:`.ModelAdmin.get_form` and
checking if the user has the "change" permissions and returning the default
form if not::
class MyAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if not self.has_change_permission(request, obj):
return super().get_form(request, obj, **kwargs)
return CustomForm
New default view permission could allow unwanted access to admin views
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you have a custom permission with a codename of the form
``can_view_<modelname>``, the new view permission handling in the admin will
allow view access to the changelist and detail pages for those models. If this
is unwanted, you must change your custom permission codename.
Miscellaneous Miscellaneous
------------- -------------

View File

@ -158,6 +158,8 @@ code.
The Django admin site uses permissions as follows: The Django admin site uses permissions as follows:
* Access to view objects is limited to users with the "view" or "change"
permission for that type of object.
* Access to view the "add" form and add an object is limited to users with * Access to view the "add" form and add an object is limited to users with
the "add" permission for that type of object. the "add" permission for that type of object.
* Access to view the change list, view the "change" form and change an * Access to view the change list, view the "change" form and change an
@ -168,6 +170,7 @@ The Django admin site uses permissions as follows:
Permissions can be set not only per type of object, but also per specific Permissions can be set not only per type of object, but also per specific
object instance. By using the object instance. By using the
:meth:`~django.contrib.admin.ModelAdmin.has_view_permission`,
:meth:`~django.contrib.admin.ModelAdmin.has_add_permission`, :meth:`~django.contrib.admin.ModelAdmin.has_add_permission`,
:meth:`~django.contrib.admin.ModelAdmin.has_change_permission` and :meth:`~django.contrib.admin.ModelAdmin.has_change_permission` and
:meth:`~django.contrib.admin.ModelAdmin.has_delete_permission` methods provided :meth:`~django.contrib.admin.ModelAdmin.has_delete_permission` methods provided
@ -213,6 +216,7 @@ to test for basic permissions you should use:
* add: ``user.has_perm('foo.add_bar')`` * add: ``user.has_perm('foo.add_bar')``
* change: ``user.has_perm('foo.change_bar')`` * change: ``user.has_perm('foo.change_bar')``
* delete: ``user.has_perm('foo.delete_bar')`` * delete: ``user.has_perm('foo.delete_bar')``
* view: ``user.has_perm('foo.view_bar')``
The :class:`~django.contrib.auth.models.Permission` model is rarely accessed The :class:`~django.contrib.auth.models.Permission` model is rarely accessed
directly. directly.

View File

@ -1,6 +1,7 @@
from datetime import datetime from datetime import datetime
from django.contrib.admin.options import IncorrectLookupParameters from django.contrib.admin.options import IncorrectLookupParameters
from django.contrib.auth.models import User
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
@ -11,9 +12,14 @@ from .models import Event
class DateHierarchyTests(TestCase): class DateHierarchyTests(TestCase):
factory = RequestFactory() factory = RequestFactory()
@classmethod
def setUpTestData(cls):
cls.superuser = User.objects.create_superuser(username='super', email='a@b.com', password='xxx')
def assertDateParams(self, query, expected_from_date, expected_to_date): def assertDateParams(self, query, expected_from_date, expected_to_date):
query = {'date__%s' % field: val for field, val in query.items()} query = {'date__%s' % field: val for field, val in query.items()}
request = self.factory.get('/', query) request = self.factory.get('/', query)
request.user = self.superuser
changelist = EventAdmin(Event, custom_site).get_changelist_instance(request) changelist = EventAdmin(Event, custom_site).get_changelist_instance(request)
_, _, lookup_params, _ = changelist.get_filters(request) _, _, lookup_params, _ = changelist.get_filters(request)
self.assertEqual(lookup_params['date__gte'], expected_from_date) self.assertEqual(lookup_params['date__gte'], expected_from_date)

View File

@ -50,6 +50,7 @@ class ChangeListTests(TestCase):
def setUp(self): def setUp(self):
self.factory = RequestFactory() self.factory = RequestFactory()
self.superuser = User.objects.create_superuser(username='super', email='a@b.com', password='xxx')
def _create_superuser(self, username): def _create_superuser(self, username):
return User.objects.create_superuser(username=username, email='a@b.com', password='xxx') return User.objects.create_superuser(username=username, email='a@b.com', password='xxx')
@ -70,6 +71,7 @@ class ChangeListTests(TestCase):
m = OrderedByFBandAdmin(Band, custom_site) m = OrderedByFBandAdmin(Band, custom_site)
request = self.factory.get('/band/') request = self.factory.get('/band/')
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
self.assertEqual(cl.get_ordering_field_columns(), {3: 'desc', 2: 'asc'}) self.assertEqual(cl.get_ordering_field_columns(), {3: 'desc', 2: 'asc'})
@ -80,12 +82,14 @@ class ChangeListTests(TestCase):
""" """
m = ChildAdmin(Child, custom_site) m = ChildAdmin(Child, custom_site)
request = self.factory.get('/child/') request = self.factory.get('/child/')
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
self.assertEqual(cl.queryset.query.select_related, {'parent': {}}) self.assertEqual(cl.queryset.query.select_related, {'parent': {}})
def test_select_related_as_tuple(self): def test_select_related_as_tuple(self):
ia = InvitationAdmin(Invitation, custom_site) ia = InvitationAdmin(Invitation, custom_site)
request = self.factory.get('/invitation/') request = self.factory.get('/invitation/')
request.user = self.superuser
cl = ia.get_changelist_instance(request) cl = ia.get_changelist_instance(request)
self.assertEqual(cl.queryset.query.select_related, {'player': {}}) self.assertEqual(cl.queryset.query.select_related, {'player': {}})
@ -93,6 +97,7 @@ class ChangeListTests(TestCase):
ia = InvitationAdmin(Invitation, custom_site) ia = InvitationAdmin(Invitation, custom_site)
ia.list_select_related = () ia.list_select_related = ()
request = self.factory.get('/invitation/') request = self.factory.get('/invitation/')
request.user = self.superuser
cl = ia.get_changelist_instance(request) cl = ia.get_changelist_instance(request)
self.assertIs(cl.queryset.query.select_related, False) self.assertIs(cl.queryset.query.select_related, False)
@ -105,6 +110,7 @@ class ChangeListTests(TestCase):
ia = GetListSelectRelatedAdmin(Invitation, custom_site) ia = GetListSelectRelatedAdmin(Invitation, custom_site)
request = self.factory.get('/invitation/') request = self.factory.get('/invitation/')
request.user = self.superuser
cl = ia.get_changelist_instance(request) cl = ia.get_changelist_instance(request)
self.assertEqual(cl.queryset.query.select_related, {'player': {}, 'band': {}}) self.assertEqual(cl.queryset.query.select_related, {'player': {}, 'band': {}})
@ -115,6 +121,7 @@ class ChangeListTests(TestCase):
""" """
new_child = Child.objects.create(name='name', parent=None) new_child = Child.objects.create(name='name', parent=None)
request = self.factory.get('/child/') request = self.factory.get('/child/')
request.user = self.superuser
m = ChildAdmin(Child, custom_site) m = ChildAdmin(Child, custom_site)
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
cl.formset = None cl.formset = None
@ -131,6 +138,7 @@ class ChangeListTests(TestCase):
""" """
new_child = Child.objects.create(name='name', parent=None) new_child = Child.objects.create(name='name', parent=None)
request = self.factory.get('/child/') request = self.factory.get('/child/')
request.user = self.superuser
# Set a new empty display value on AdminSite. # Set a new empty display value on AdminSite.
admin.site.empty_value_display = '???' admin.site.empty_value_display = '???'
m = ChildAdmin(Child, admin.site) m = ChildAdmin(Child, admin.site)
@ -149,6 +157,7 @@ class ChangeListTests(TestCase):
""" """
new_child = Child.objects.create(name='name', parent=None) new_child = Child.objects.create(name='name', parent=None)
request = self.factory.get('/child/') request = self.factory.get('/child/')
request.user = self.superuser
m = EmptyValueChildAdmin(Child, admin.site) m = EmptyValueChildAdmin(Child, admin.site)
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
cl.formset = None cl.formset = None
@ -172,6 +181,7 @@ class ChangeListTests(TestCase):
new_parent = Parent.objects.create(name='parent') new_parent = Parent.objects.create(name='parent')
new_child = Child.objects.create(name='name', parent=new_parent) new_child = Child.objects.create(name='name', parent=new_parent)
request = self.factory.get('/child/') request = self.factory.get('/child/')
request.user = self.superuser
m = ChildAdmin(Child, custom_site) m = ChildAdmin(Child, custom_site)
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
cl.formset = None cl.formset = None
@ -194,6 +204,7 @@ class ChangeListTests(TestCase):
new_parent = Parent.objects.create(name='parent') new_parent = Parent.objects.create(name='parent')
new_child = Child.objects.create(name='name', parent=new_parent) new_child = Child.objects.create(name='name', parent=new_parent)
request = self.factory.get('/child/') request = self.factory.get('/child/')
request.user = self.superuser
m = ChildAdmin(Child, custom_site) m = ChildAdmin(Child, custom_site)
# Test with list_editable fields # Test with list_editable fields
@ -233,6 +244,7 @@ class ChangeListTests(TestCase):
for i in range(200): for i in range(200):
Child.objects.create(name='name %s' % i, parent=new_parent) Child.objects.create(name='name %s' % i, parent=new_parent)
request = self.factory.get('/child/', data={'p': -1}) # Anything outside range request = self.factory.get('/child/', data={'p': -1}) # Anything outside range
request.user = self.superuser
m = ChildAdmin(Child, custom_site) m = ChildAdmin(Child, custom_site)
# Test with list_editable fields # Test with list_editable fields
@ -248,6 +260,7 @@ class ChangeListTests(TestCase):
Child.objects.create(name='name %s' % i, parent=new_parent) Child.objects.create(name='name %s' % i, parent=new_parent)
request = self.factory.get('/child/') request = self.factory.get('/child/')
request.user = self.superuser
m = CustomPaginationAdmin(Child, custom_site) m = CustomPaginationAdmin(Child, custom_site)
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
@ -267,6 +280,7 @@ class ChangeListTests(TestCase):
m = BandAdmin(Band, custom_site) m = BandAdmin(Band, custom_site)
request = self.factory.get('/band/', data={'genres': blues.pk}) request = self.factory.get('/band/', data={'genres': blues.pk})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
cl.get_results(request) cl.get_results(request)
@ -286,6 +300,7 @@ class ChangeListTests(TestCase):
m = GroupAdmin(Group, custom_site) m = GroupAdmin(Group, custom_site)
request = self.factory.get('/group/', data={'members': lead.pk}) request = self.factory.get('/group/', data={'members': lead.pk})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
cl.get_results(request) cl.get_results(request)
@ -307,6 +322,7 @@ class ChangeListTests(TestCase):
m = ConcertAdmin(Concert, custom_site) m = ConcertAdmin(Concert, custom_site)
request = self.factory.get('/concert/', data={'group__members': lead.pk}) request = self.factory.get('/concert/', data={'group__members': lead.pk})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
cl.get_results(request) cl.get_results(request)
@ -327,6 +343,7 @@ class ChangeListTests(TestCase):
m = QuartetAdmin(Quartet, custom_site) m = QuartetAdmin(Quartet, custom_site)
request = self.factory.get('/quartet/', data={'members': lead.pk}) request = self.factory.get('/quartet/', data={'members': lead.pk})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
cl.get_results(request) cl.get_results(request)
@ -347,6 +364,7 @@ class ChangeListTests(TestCase):
m = ChordsBandAdmin(ChordsBand, custom_site) m = ChordsBandAdmin(ChordsBand, custom_site)
request = self.factory.get('/chordsband/', data={'members': lead.pk}) request = self.factory.get('/chordsband/', data={'members': lead.pk})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
cl.get_results(request) cl.get_results(request)
@ -366,6 +384,7 @@ class ChangeListTests(TestCase):
m = ParentAdmin(Parent, custom_site) m = ParentAdmin(Parent, custom_site)
request = self.factory.get('/parent/', data={'child__name': 'Daniel'}) request = self.factory.get('/parent/', data={'child__name': 'Daniel'})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
# Make sure distinct() was called # Make sure distinct() was called
@ -382,6 +401,7 @@ class ChangeListTests(TestCase):
m = ParentAdmin(Parent, custom_site) m = ParentAdmin(Parent, custom_site)
request = self.factory.get('/parent/', data={SEARCH_VAR: 'daniel'}) request = self.factory.get('/parent/', data={SEARCH_VAR: 'daniel'})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
# Make sure distinct() was called # Make sure distinct() was called
@ -401,6 +421,7 @@ class ChangeListTests(TestCase):
m = ConcertAdmin(Concert, custom_site) m = ConcertAdmin(Concert, custom_site)
request = self.factory.get('/concert/', data={SEARCH_VAR: 'vox'}) request = self.factory.get('/concert/', data={SEARCH_VAR: 'vox'})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
# There's only one Concert instance # There's only one Concert instance
@ -414,10 +435,12 @@ class ChangeListTests(TestCase):
m.search_fields = ['group__pk'] m.search_fields = ['group__pk']
request = self.factory.get('/concert/', data={SEARCH_VAR: band.pk}) request = self.factory.get('/concert/', data={SEARCH_VAR: band.pk})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
self.assertEqual(cl.queryset.count(), 1) self.assertEqual(cl.queryset.count(), 1)
request = self.factory.get('/concert/', data={SEARCH_VAR: band.pk + 5}) request = self.factory.get('/concert/', data={SEARCH_VAR: band.pk + 5})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
self.assertEqual(cl.queryset.count(), 0) self.assertEqual(cl.queryset.count(), 0)
@ -429,10 +452,12 @@ class ChangeListTests(TestCase):
m.search_fields = ['name__iexact'] m.search_fields = ['name__iexact']
request = self.factory.get('/', data={SEARCH_VAR: 'woodstock'}) request = self.factory.get('/', data={SEARCH_VAR: 'woodstock'})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [concert]) self.assertCountEqual(cl.queryset, [concert])
request = self.factory.get('/', data={SEARCH_VAR: 'wood'}) request = self.factory.get('/', data={SEARCH_VAR: 'wood'})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, []) self.assertCountEqual(cl.queryset, [])
@ -445,10 +470,12 @@ class ChangeListTests(TestCase):
Field.register_lookup(Contains, 'cc') Field.register_lookup(Contains, 'cc')
try: try:
request = self.factory.get('/', data={SEARCH_VAR: 'Hype'}) request = self.factory.get('/', data={SEARCH_VAR: 'Hype'})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [concert]) self.assertCountEqual(cl.queryset, [concert])
request = self.factory.get('/', data={SEARCH_VAR: 'Woodstock'}) request = self.factory.get('/', data={SEARCH_VAR: 'Woodstock'})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, []) self.assertCountEqual(cl.queryset, [])
finally: finally:
@ -467,10 +494,12 @@ class ChangeListTests(TestCase):
m.search_fields = ['group__members__age__exactly'] m.search_fields = ['group__members__age__exactly']
request = self.factory.get('/', data={SEARCH_VAR: '20'}) request = self.factory.get('/', data={SEARCH_VAR: '20'})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [concert]) self.assertCountEqual(cl.queryset, [concert])
request = self.factory.get('/', data={SEARCH_VAR: '21'}) request = self.factory.get('/', data={SEARCH_VAR: '21'})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, []) self.assertCountEqual(cl.queryset, [])
finally: finally:
@ -486,10 +515,12 @@ class ChangeListTests(TestCase):
m.search_fields = ['pk__exact'] m.search_fields = ['pk__exact']
request = self.factory.get('/', data={SEARCH_VAR: 'abc'}) request = self.factory.get('/', data={SEARCH_VAR: 'abc'})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [abc]) self.assertCountEqual(cl.queryset, [abc])
request = self.factory.get('/', data={SEARCH_VAR: 'abcd'}) request = self.factory.get('/', data={SEARCH_VAR: 'abcd'})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [abcd]) self.assertCountEqual(cl.queryset, [abcd])
@ -501,11 +532,13 @@ class ChangeListTests(TestCase):
m = BandAdmin(Band, custom_site) m = BandAdmin(Band, custom_site)
for lookup_params in ({}, {'name': 'test'}): for lookup_params in ({}, {'name': 'test'}):
request = self.factory.get('/band/', lookup_params) request = self.factory.get('/band/', lookup_params)
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
self.assertFalse(cl.queryset.query.distinct) self.assertFalse(cl.queryset.query.distinct)
# A ManyToManyField in params does have distinct applied. # A ManyToManyField in params does have distinct applied.
request = self.factory.get('/band/', {'genres': '0'}) request = self.factory.get('/band/', {'genres': '0'})
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
self.assertTrue(cl.queryset.query.distinct) self.assertTrue(cl.queryset.query.distinct)
@ -520,6 +553,7 @@ class ChangeListTests(TestCase):
Child.objects.create(name='filtered %s' % i, parent=parent) Child.objects.create(name='filtered %s' % i, parent=parent)
request = self.factory.get('/child/') request = self.factory.get('/child/')
request.user = self.superuser
# Test default queryset # Test default queryset
m = ChildAdmin(Child, custom_site) m = ChildAdmin(Child, custom_site)
@ -540,8 +574,7 @@ class ChangeListTests(TestCase):
Regression test for #13196: output of functions should be localized Regression test for #13196: output of functions should be localized
in the changelist. in the changelist.
""" """
superuser = User.objects.create_superuser(username='super', email='super@localhost', password='secret') self.client.force_login(self.superuser)
self.client.force_login(superuser)
event = Event.objects.create(date=datetime.date.today()) event = Event.objects.create(date=datetime.date.today())
response = self.client.get(reverse('admin:admin_changelist_event_changelist')) response = self.client.get(reverse('admin:admin_changelist_event_changelist'))
self.assertContains(response, formats.localize(event.date)) self.assertContains(response, formats.localize(event.date))
@ -597,6 +630,7 @@ class ChangeListTests(TestCase):
# Add "show all" parameter to request # Add "show all" parameter to request
request = self.factory.get('/child/', data={ALL_VAR: ''}) request = self.factory.get('/child/', data={ALL_VAR: ''})
request.user = self.superuser
# Test valid "show all" request (number of total objects is under max) # Test valid "show all" request (number of total objects is under max)
m = ChildAdmin(Child, custom_site) m = ChildAdmin(Child, custom_site)
@ -856,6 +890,7 @@ class ChangeListTests(TestCase):
# instantiating and setting up ChangeList object # instantiating and setting up ChangeList object
m = GroupAdmin(Group, custom_site) m = GroupAdmin(Group, custom_site)
request = self.factory.get('/group/') request = self.factory.get('/group/')
request.user = self.superuser
cl = m.get_changelist_instance(request) cl = m.get_changelist_instance(request)
per_page = cl.list_per_page = 10 per_page = cl.list_per_page = 10

View File

@ -263,7 +263,7 @@ class ListFiltersTests(TestCase):
self.request_factory = RequestFactory() self.request_factory = RequestFactory()
# Users # Users
self.alfred = User.objects.create_user('alfred', 'alfred@example.com') self.alfred = User.objects.create_superuser('alfred', 'alfred@example.com', 'password')
self.bob = User.objects.create_user('bob', 'bob@example.com') self.bob = User.objects.create_user('bob', 'bob@example.com')
self.lisa = User.objects.create_user('lisa', 'lisa@example.com') self.lisa = User.objects.create_user('lisa', 'lisa@example.com')
@ -308,6 +308,7 @@ class ListFiltersTests(TestCase):
modeladmin = BookmarkChoicesAdmin(Bookmark, site) modeladmin = BookmarkChoicesAdmin(Bookmark, site)
request = self.request_factory.get('/', {}) request = self.request_factory.get('/', {})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
filterspec = changelist.get_filters(request)[0][0] filterspec = changelist.get_filters(request)[0][0]
choices = list(filterspec.choices(changelist)) choices = list(filterspec.choices(changelist))
@ -318,10 +319,12 @@ class ListFiltersTests(TestCase):
modeladmin = BookAdmin(Book, site) modeladmin = BookAdmin(Book, site)
request = self.request_factory.get('/') request = self.request_factory.get('/')
request.user = self.alfred
changelist = modeladmin.get_changelist(request) changelist = modeladmin.get_changelist(request)
request = self.request_factory.get('/', {'date_registered__gte': self.today, request = self.request_factory.get('/', {'date_registered__gte': self.today,
'date_registered__lt': self.tomorrow}) 'date_registered__lt': self.tomorrow})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -343,6 +346,7 @@ class ListFiltersTests(TestCase):
request = self.request_factory.get('/', {'date_registered__gte': self.today.replace(day=1), request = self.request_factory.get('/', {'date_registered__gte': self.today.replace(day=1),
'date_registered__lt': self.next_month}) 'date_registered__lt': self.next_month})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -368,6 +372,7 @@ class ListFiltersTests(TestCase):
request = self.request_factory.get('/', {'date_registered__gte': self.today.replace(month=1, day=1), request = self.request_factory.get('/', {'date_registered__gte': self.today.replace(month=1, day=1),
'date_registered__lt': self.next_year}) 'date_registered__lt': self.next_year})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -395,6 +400,7 @@ class ListFiltersTests(TestCase):
'date_registered__gte': str(self.one_week_ago), 'date_registered__gte': str(self.one_week_ago),
'date_registered__lt': str(self.tomorrow), 'date_registered__lt': str(self.tomorrow),
}) })
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -416,6 +422,7 @@ class ListFiltersTests(TestCase):
# Null/not null queries # Null/not null queries
request = self.request_factory.get('/', {'date_registered__isnull': 'True'}) request = self.request_factory.get('/', {'date_registered__isnull': 'True'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -431,6 +438,7 @@ class ListFiltersTests(TestCase):
self.assertEqual(choice['query_string'], '?date_registered__isnull=True') self.assertEqual(choice['query_string'], '?date_registered__isnull=True')
request = self.request_factory.get('/', {'date_registered__isnull': 'False'}) request = self.request_factory.get('/', {'date_registered__isnull': 'False'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -459,6 +467,7 @@ class ListFiltersTests(TestCase):
modeladmin = BookAdmin(Book, site) modeladmin = BookAdmin(Book, site)
request = self.request_factory.get('/', {'year__isnull': 'True'}) request = self.request_factory.get('/', {'year__isnull': 'True'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -473,6 +482,7 @@ class ListFiltersTests(TestCase):
self.assertEqual(choices[-1]['query_string'], '?year__isnull=True') self.assertEqual(choices[-1]['query_string'], '?year__isnull=True')
request = self.request_factory.get('/', {'year': '2002'}) request = self.request_factory.get('/', {'year': '2002'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct choice is selected # Make sure the correct choice is selected
@ -486,6 +496,7 @@ class ListFiltersTests(TestCase):
# Make sure that correct filters are returned with custom querysets # Make sure that correct filters are returned with custom querysets
modeladmin = BookAdminWithCustomQueryset(self.alfred, Book, site) modeladmin = BookAdminWithCustomQueryset(self.alfred, Book, site)
request = self.request_factory.get('/') request = self.request_factory.get('/')
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
filterspec = changelist.get_filters(request)[0][0] filterspec = changelist.get_filters(request)[0][0]
@ -502,6 +513,7 @@ class ListFiltersTests(TestCase):
modeladmin = BookAdmin(Book, site) modeladmin = BookAdmin(Book, site)
request = self.request_factory.get('/') request = self.request_factory.get('/')
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure that all users are present in the author's list filter # Make sure that all users are present in the author's list filter
@ -510,6 +522,7 @@ class ListFiltersTests(TestCase):
self.assertEqual(sorted(filterspec.lookup_choices), sorted(expected)) self.assertEqual(sorted(filterspec.lookup_choices), sorted(expected))
request = self.request_factory.get('/', {'author__isnull': 'True'}) request = self.request_factory.get('/', {'author__isnull': 'True'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -524,6 +537,7 @@ class ListFiltersTests(TestCase):
self.assertEqual(choices[-1]['query_string'], '?author__isnull=True') self.assertEqual(choices[-1]['query_string'], '?author__isnull=True')
request = self.request_factory.get('/', {'author__id__exact': self.alfred.pk}) request = self.request_factory.get('/', {'author__id__exact': self.alfred.pk})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct choice is selected # Make sure the correct choice is selected
@ -538,6 +552,7 @@ class ListFiltersTests(TestCase):
modeladmin = BookAdmin(Book, site) modeladmin = BookAdmin(Book, site)
request = self.request_factory.get('/') request = self.request_factory.get('/')
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure that all users are present in the contrib's list filter # Make sure that all users are present in the contrib's list filter
@ -546,6 +561,7 @@ class ListFiltersTests(TestCase):
self.assertEqual(sorted(filterspec.lookup_choices), sorted(expected)) self.assertEqual(sorted(filterspec.lookup_choices), sorted(expected))
request = self.request_factory.get('/', {'contributors__isnull': 'True'}) request = self.request_factory.get('/', {'contributors__isnull': 'True'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -560,6 +576,7 @@ class ListFiltersTests(TestCase):
self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True') self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True')
request = self.request_factory.get('/', {'contributors__id__exact': self.bob.pk}) request = self.request_factory.get('/', {'contributors__id__exact': self.bob.pk})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct choice is selected # Make sure the correct choice is selected
@ -574,6 +591,7 @@ class ListFiltersTests(TestCase):
# FK relationship ----- # FK relationship -----
request = self.request_factory.get('/', {'books_authored__isnull': 'True'}) request = self.request_factory.get('/', {'books_authored__isnull': 'True'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -588,6 +606,7 @@ class ListFiltersTests(TestCase):
self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True') self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True')
request = self.request_factory.get('/', {'books_authored__id__exact': self.bio_book.pk}) request = self.request_factory.get('/', {'books_authored__id__exact': self.bio_book.pk})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct choice is selected # Make sure the correct choice is selected
@ -599,6 +618,7 @@ class ListFiltersTests(TestCase):
# M2M relationship ----- # M2M relationship -----
request = self.request_factory.get('/', {'books_contributed__isnull': 'True'}) request = self.request_factory.get('/', {'books_contributed__isnull': 'True'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -613,6 +633,7 @@ class ListFiltersTests(TestCase):
self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True') self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True')
request = self.request_factory.get('/', {'books_contributed__id__exact': self.django_book.pk}) request = self.request_factory.get('/', {'books_contributed__id__exact': self.django_book.pk})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct choice is selected # Make sure the correct choice is selected
@ -636,6 +657,7 @@ class ListFiltersTests(TestCase):
modeladmin = BookAdminRelatedOnlyFilter(Book, site) modeladmin = BookAdminRelatedOnlyFilter(Book, site)
request = self.request_factory.get('/') request = self.request_factory.get('/')
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure that only actual authors are present in author's list filter # Make sure that only actual authors are present in author's list filter
@ -652,6 +674,7 @@ class ListFiltersTests(TestCase):
modeladmin = BookAdminRelatedOnlyFilter(Book, site) modeladmin = BookAdminRelatedOnlyFilter(Book, site)
request = self.request_factory.get('/') request = self.request_factory.get('/')
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Only actual departments should be present in employee__department's # Only actual departments should be present in employee__department's
@ -667,6 +690,7 @@ class ListFiltersTests(TestCase):
modeladmin = BookAdminRelatedOnlyFilter(Book, site) modeladmin = BookAdminRelatedOnlyFilter(Book, site)
request = self.request_factory.get('/') request = self.request_factory.get('/')
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure that only actual contributors are present in contrib's list filter # Make sure that only actual contributors are present in contrib's list filter
@ -686,6 +710,7 @@ class ListFiltersTests(TestCase):
modeladmin = BookmarkAdminGenericRelation(Bookmark, site) modeladmin = BookmarkAdminGenericRelation(Bookmark, site)
request = self.request_factory.get('/', {'tags__tag': 'python'}) request = self.request_factory.get('/', {'tags__tag': 'python'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
queryset = changelist.get_queryset(request) queryset = changelist.get_queryset(request)
@ -702,9 +727,11 @@ class ListFiltersTests(TestCase):
def verify_booleanfieldlistfilter(self, modeladmin): def verify_booleanfieldlistfilter(self, modeladmin):
request = self.request_factory.get('/') request = self.request_factory.get('/')
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
request = self.request_factory.get('/', {'is_best_seller__exact': 0}) request = self.request_factory.get('/', {'is_best_seller__exact': 0})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -719,6 +746,7 @@ class ListFiltersTests(TestCase):
self.assertEqual(choice['query_string'], '?is_best_seller__exact=0') self.assertEqual(choice['query_string'], '?is_best_seller__exact=0')
request = self.request_factory.get('/', {'is_best_seller__exact': 1}) request = self.request_factory.get('/', {'is_best_seller__exact': 1})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -733,6 +761,7 @@ class ListFiltersTests(TestCase):
self.assertEqual(choice['query_string'], '?is_best_seller__exact=1') self.assertEqual(choice['query_string'], '?is_best_seller__exact=1')
request = self.request_factory.get('/', {'is_best_seller__isnull': 'True'}) request = self.request_factory.get('/', {'is_best_seller__isnull': 'True'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -750,9 +779,11 @@ class ListFiltersTests(TestCase):
modeladmin = BookAdmin2(Book, site) modeladmin = BookAdmin2(Book, site)
request = self.request_factory.get('/') request = self.request_factory.get('/')
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
request = self.request_factory.get('/', {'is_best_seller2__exact': 0}) request = self.request_factory.get('/', {'is_best_seller2__exact': 0})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -767,6 +798,7 @@ class ListFiltersTests(TestCase):
self.assertEqual(choice['query_string'], '?is_best_seller2__exact=0') self.assertEqual(choice['query_string'], '?is_best_seller2__exact=0')
request = self.request_factory.get('/', {'is_best_seller2__exact': 1}) request = self.request_factory.get('/', {'is_best_seller2__exact': 1})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -781,6 +813,7 @@ class ListFiltersTests(TestCase):
self.assertEqual(choice['query_string'], '?is_best_seller2__exact=1') self.assertEqual(choice['query_string'], '?is_best_seller2__exact=1')
request = self.request_factory.get('/', {'is_best_seller2__isnull': 'True'}) request = self.request_factory.get('/', {'is_best_seller2__isnull': 'True'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -801,9 +834,11 @@ class ListFiltersTests(TestCase):
""" """
modeladmin = BookAdminWithUnderscoreLookupAndTuple(Book, site) modeladmin = BookAdminWithUnderscoreLookupAndTuple(Book, site)
request = self.request_factory.get('/') request = self.request_factory.get('/')
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
request = self.request_factory.get('/', {'author__email': 'alfred@example.com'}) request = self.request_factory.get('/', {'author__email': 'alfred@example.com'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -814,6 +849,7 @@ class ListFiltersTests(TestCase):
"""Filtering by an invalid value.""" """Filtering by an invalid value."""
modeladmin = BookAdmin(Book, site) modeladmin = BookAdmin(Book, site)
request = self.request_factory.get('/', {'author__id__exact': 'StringNotInteger!'}) request = self.request_factory.get('/', {'author__id__exact': 'StringNotInteger!'})
request.user = self.alfred
with self.assertRaises(IncorrectLookupParameters): with self.assertRaises(IncorrectLookupParameters):
modeladmin.get_changelist_instance(request) modeladmin.get_changelist_instance(request)
@ -822,6 +858,7 @@ class ListFiltersTests(TestCase):
# Make sure that the first option is 'All' --------------------------- # Make sure that the first option is 'All' ---------------------------
request = self.request_factory.get('/', {}) request = self.request_factory.get('/', {})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -838,6 +875,7 @@ class ListFiltersTests(TestCase):
# Look for books in the 1980s ---------------------------------------- # Look for books in the 1980s ----------------------------------------
request = self.request_factory.get('/', {'publication-decade': 'the 80s'}) request = self.request_factory.get('/', {'publication-decade': 'the 80s'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -854,6 +892,7 @@ class ListFiltersTests(TestCase):
# Look for books in the 1990s ---------------------------------------- # Look for books in the 1990s ----------------------------------------
request = self.request_factory.get('/', {'publication-decade': 'the 90s'}) request = self.request_factory.get('/', {'publication-decade': 'the 90s'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -870,6 +909,7 @@ class ListFiltersTests(TestCase):
# Look for books in the 2000s ---------------------------------------- # Look for books in the 2000s ----------------------------------------
request = self.request_factory.get('/', {'publication-decade': 'the 00s'}) request = self.request_factory.get('/', {'publication-decade': 'the 00s'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -886,6 +926,7 @@ class ListFiltersTests(TestCase):
# Combine multiple filters ------------------------------------------- # Combine multiple filters -------------------------------------------
request = self.request_factory.get('/', {'publication-decade': 'the 00s', 'author__id__exact': self.alfred.pk}) request = self.request_factory.get('/', {'publication-decade': 'the 00s', 'author__id__exact': self.alfred.pk})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -915,6 +956,7 @@ class ListFiltersTests(TestCase):
""" """
modeladmin = DecadeFilterBookAdminWithoutTitle(Book, site) modeladmin = DecadeFilterBookAdminWithoutTitle(Book, site)
request = self.request_factory.get('/', {}) request = self.request_factory.get('/', {})
request.user = self.alfred
msg = "The list filter 'DecadeListFilterWithoutTitle' does not specify a 'title'." msg = "The list filter 'DecadeListFilterWithoutTitle' does not specify a 'title'."
with self.assertRaisesMessage(ImproperlyConfigured, msg): with self.assertRaisesMessage(ImproperlyConfigured, msg):
modeladmin.get_changelist_instance(request) modeladmin.get_changelist_instance(request)
@ -925,6 +967,7 @@ class ListFiltersTests(TestCase):
""" """
modeladmin = DecadeFilterBookAdminWithoutParameter(Book, site) modeladmin = DecadeFilterBookAdminWithoutParameter(Book, site)
request = self.request_factory.get('/', {}) request = self.request_factory.get('/', {})
request.user = self.alfred
msg = "The list filter 'DecadeListFilterWithoutParameter' does not specify a 'parameter_name'." msg = "The list filter 'DecadeListFilterWithoutParameter' does not specify a 'parameter_name'."
with self.assertRaisesMessage(ImproperlyConfigured, msg): with self.assertRaisesMessage(ImproperlyConfigured, msg):
modeladmin.get_changelist_instance(request) modeladmin.get_changelist_instance(request)
@ -936,6 +979,7 @@ class ListFiltersTests(TestCase):
""" """
modeladmin = DecadeFilterBookAdminWithNoneReturningLookups(Book, site) modeladmin = DecadeFilterBookAdminWithNoneReturningLookups(Book, site)
request = self.request_factory.get('/', {}) request = self.request_factory.get('/', {})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
filterspec = changelist.get_filters(request)[0] filterspec = changelist.get_filters(request)[0]
self.assertEqual(len(filterspec), 0) self.assertEqual(len(filterspec), 0)
@ -947,12 +991,14 @@ class ListFiltersTests(TestCase):
""" """
modeladmin = DecadeFilterBookAdminWithFailingQueryset(Book, site) modeladmin = DecadeFilterBookAdminWithFailingQueryset(Book, site)
request = self.request_factory.get('/', {}) request = self.request_factory.get('/', {})
request.user = self.alfred
with self.assertRaises(ZeroDivisionError): with self.assertRaises(ZeroDivisionError):
modeladmin.get_changelist_instance(request) modeladmin.get_changelist_instance(request)
def test_simplelistfilter_with_queryset_based_lookups(self): def test_simplelistfilter_with_queryset_based_lookups(self):
modeladmin = DecadeFilterBookAdminWithQuerysetBasedLookups(Book, site) modeladmin = DecadeFilterBookAdminWithQuerysetBasedLookups(Book, site)
request = self.request_factory.get('/', {}) request = self.request_factory.get('/', {})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
filterspec = changelist.get_filters(request)[0][0] filterspec = changelist.get_filters(request)[0][0]
@ -978,6 +1024,7 @@ class ListFiltersTests(TestCase):
""" """
modeladmin = BookAdmin(Book, site) modeladmin = BookAdmin(Book, site)
request = self.request_factory.get('/', {'no': '207'}) request = self.request_factory.get('/', {'no': '207'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -998,6 +1045,7 @@ class ListFiltersTests(TestCase):
# When it ends with '__in' ----------------------------------------- # When it ends with '__in' -----------------------------------------
modeladmin = DecadeFilterBookAdminParameterEndsWith__In(Book, site) modeladmin = DecadeFilterBookAdminParameterEndsWith__In(Book, site)
request = self.request_factory.get('/', {'decade__in': 'the 90s'}) request = self.request_factory.get('/', {'decade__in': 'the 90s'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -1015,6 +1063,7 @@ class ListFiltersTests(TestCase):
# When it ends with '__isnull' --------------------------------------- # When it ends with '__isnull' ---------------------------------------
modeladmin = DecadeFilterBookAdminParameterEndsWith__Isnull(Book, site) modeladmin = DecadeFilterBookAdminParameterEndsWith__Isnull(Book, site)
request = self.request_factory.get('/', {'decade__isnull': 'the 90s'}) request = self.request_factory.get('/', {'decade__isnull': 'the 90s'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -1036,6 +1085,7 @@ class ListFiltersTests(TestCase):
""" """
modeladmin = DepartmentFilterEmployeeAdmin(Employee, site) modeladmin = DepartmentFilterEmployeeAdmin(Employee, site)
request = self.request_factory.get('/', {'department': self.john.department.pk}) request = self.request_factory.get('/', {'department': self.john.department.pk})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
queryset = changelist.get_queryset(request) queryset = changelist.get_queryset(request)
@ -1056,6 +1106,7 @@ class ListFiltersTests(TestCase):
""" """
modeladmin = DepartmentFilterUnderscoredEmployeeAdmin(Employee, site) modeladmin = DepartmentFilterUnderscoredEmployeeAdmin(Employee, site)
request = self.request_factory.get('/', {'department__whatever': self.john.department.pk}) request = self.request_factory.get('/', {'department__whatever': self.john.department.pk})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
queryset = changelist.get_queryset(request) queryset = changelist.get_queryset(request)
@ -1076,6 +1127,7 @@ class ListFiltersTests(TestCase):
modeladmin = EmployeeAdmin(Employee, site) modeladmin = EmployeeAdmin(Employee, site)
request = self.request_factory.get('/', {}) request = self.request_factory.get('/', {})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -1101,6 +1153,7 @@ class ListFiltersTests(TestCase):
# Filter by Department=='Development' -------------------------------- # Filter by Department=='Development' --------------------------------
request = self.request_factory.get('/', {'department__code__exact': 'DEV'}) request = self.request_factory.get('/', {'department__code__exact': 'DEV'})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
@ -1130,6 +1183,7 @@ class ListFiltersTests(TestCase):
modeladmin = DepartmentFilterDynamicValueBookAdmin(Book, site) modeladmin = DepartmentFilterDynamicValueBookAdmin(Book, site)
def _test_choices(request, expected_displays): def _test_choices(request, expected_displays):
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
filterspec = changelist.get_filters(request)[0][0] filterspec = changelist.get_filters(request)[0][0]
self.assertEqual(filterspec.title, 'publication decade') self.assertEqual(filterspec.title, 'publication decade')
@ -1152,6 +1206,7 @@ class ListFiltersTests(TestCase):
""" """
modeladmin = NotNinetiesListFilterAdmin(Book, site) modeladmin = NotNinetiesListFilterAdmin(Book, site)
request = self.request_factory.get('/', {}) request = self.request_factory.get('/', {})
request.user = self.alfred
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
changelist.get_results(request) changelist.get_results(request)
self.assertEqual(changelist.full_result_count, 4) self.assertEqual(changelist.full_result_count, 4)

View File

@ -156,6 +156,10 @@ class RowLevelChangePermissionModelAdmin(admin.ModelAdmin):
""" Only allow changing objects with even id number """ """ Only allow changing objects with even id number """
return request.user.is_staff and (obj is not None) and (obj.id % 2 == 0) return request.user.is_staff and (obj is not None) and (obj.id % 2 == 0)
def has_view_permission(self, request, obj=None):
"""Only allow viewing objects if id is a multiple of 3."""
return request.user.is_staff and obj is not None and obj.id % 3 == 0
class CustomArticleAdmin(admin.ModelAdmin): class CustomArticleAdmin(admin.ModelAdmin):
""" """

View File

@ -72,6 +72,9 @@ class AdminTemplateTagsTest(AdminViewBasicTestCase):
class DateHierarchyTests(TestCase): class DateHierarchyTests(TestCase):
factory = RequestFactory() factory = RequestFactory()
def setUp(self):
self.superuser = User.objects.create_superuser(username='super', password='secret', email='super@example.com')
def test_choice_links(self): def test_choice_links(self):
modeladmin = ModelAdmin(Question, site) modeladmin = ModelAdmin(Question, site)
modeladmin.date_hierarchy = 'posted' modeladmin.date_hierarchy = 'posted'
@ -97,6 +100,7 @@ class DateHierarchyTests(TestCase):
with self.subTest(query=query): with self.subTest(query=query):
query = {'posted__%s' % q: val for q, val in query.items()} query = {'posted__%s' % q: val for q, val in query.items()}
request = self.factory.get('/', query) request = self.factory.get('/', query)
request.user = self.superuser
changelist = modeladmin.get_changelist_instance(request) changelist = modeladmin.get_changelist_instance(request)
spec = date_hierarchy(changelist) spec = date_hierarchy(changelist)
choices = [choice['link'] for choice in spec['choices']] choices = [choice['link'] for choice in spec['choices']]

View File

@ -1394,6 +1394,7 @@ class AdminViewPermissionsTest(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.superuser = User.objects.create_superuser(username='super', password='secret', email='super@example.com') cls.superuser = User.objects.create_superuser(username='super', password='secret', email='super@example.com')
cls.viewuser = User.objects.create_user(username='viewuser', password='secret', is_staff=True)
cls.adduser = User.objects.create_user(username='adduser', password='secret', is_staff=True) cls.adduser = User.objects.create_user(username='adduser', password='secret', is_staff=True)
cls.changeuser = User.objects.create_user(username='changeuser', password='secret', is_staff=True) cls.changeuser = User.objects.create_user(username='changeuser', password='secret', is_staff=True)
cls.deleteuser = User.objects.create_user(username='deleteuser', password='secret', is_staff=True) cls.deleteuser = User.objects.create_user(username='deleteuser', password='secret', is_staff=True)
@ -1415,6 +1416,8 @@ class AdminViewPermissionsTest(TestCase):
# Setup permissions, for our users who can add, change, and delete. # Setup permissions, for our users who can add, change, and delete.
opts = Article._meta opts = Article._meta
# User who can view Articles
cls.viewuser.user_permissions.add(get_perm(Article, get_permission_codename('view', opts)))
# User who can add Articles # User who can add Articles
cls.adduser.user_permissions.add(get_perm(Article, get_permission_codename('add', opts))) cls.adduser.user_permissions.add(get_perm(Article, get_permission_codename('add', opts)))
# User who can change Articles # User who can change Articles
@ -1467,6 +1470,11 @@ class AdminViewPermissionsTest(TestCase):
'username': 'joepublic', 'username': 'joepublic',
'password': 'secret', 'password': 'secret',
} }
cls.viewuser_login = {
REDIRECT_FIELD_NAME: cls.index_url,
'username': 'viewuser',
'password': 'secret',
}
cls.no_username_login = { cls.no_username_login = {
REDIRECT_FIELD_NAME: cls.index_url, REDIRECT_FIELD_NAME: cls.index_url,
'password': 'secret', 'password': 'secret',
@ -1503,6 +1511,14 @@ class AdminViewPermissionsTest(TestCase):
login = self.client.post(login_url, self.super_email_login) login = self.client.post(login_url, self.super_email_login)
self.assertContains(login, ERROR_MESSAGE) self.assertContains(login, ERROR_MESSAGE)
# View User
response = self.client.get(self.index_url)
self.assertEqual(response.status_code, 302)
login = self.client.post(login_url, self.viewuser_login)
self.assertRedirects(login, self.index_url)
self.assertFalse(login.context)
self.client.get(reverse('admin:logout'))
# Add User # Add User
response = self.client.get(self.index_url) response = self.client.get(self.index_url)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
@ -1657,6 +1673,27 @@ class AdminViewPermissionsTest(TestCase):
self.assertEqual(Article.objects.count(), 3) self.assertEqual(Article.objects.count(), 3)
self.client.get(reverse('admin:logout')) self.client.get(reverse('admin:logout'))
# View User should not have access to add articles
self.client.force_login(self.viewuser)
response = self.client.get(reverse('admin:admin_views_article_add'))
self.assertEqual(response.status_code, 403)
# Try POST just to make sure
post = self.client.post(reverse('admin:admin_views_article_add'), add_dict)
self.assertEqual(post.status_code, 403)
self.assertEqual(Article.objects.count(), 3)
# Now give the user permission to add but not change.
self.viewuser.user_permissions.add(get_perm(Article, get_permission_codename('add', Article._meta)))
response = self.client.get(reverse('admin:admin_views_article_add'))
self.assertContains(response, '<input type="submit" value="Save and view" name="_continue">')
post = self.client.post(reverse('admin:admin_views_article_add'), add_dict, follow=False)
self.assertEqual(post.status_code, 302)
self.assertEqual(Article.objects.count(), 4)
article = Article.objects.latest('pk')
response = self.client.get(reverse('admin:admin_views_article_change', args=(article.pk,)))
self.assertContains(response, '<li class="success">The article "Døm ikke" was added successfully.</li>')
article.delete()
self.client.get(reverse('admin:logout'))
# Add user may login and POST to add view, then redirect to admin root # Add user may login and POST to add view, then redirect to admin root
self.client.force_login(self.adduser) self.client.force_login(self.adduser)
addpage = self.client.get(reverse('admin:admin_views_article_add')) addpage = self.client.get(reverse('admin:admin_views_article_add'))
@ -1668,7 +1705,7 @@ class AdminViewPermissionsTest(TestCase):
post = self.client.post(reverse('admin:admin_views_article_add'), add_dict) post = self.client.post(reverse('admin:admin_views_article_add'), add_dict)
self.assertRedirects(post, self.index_url) self.assertRedirects(post, self.index_url)
self.assertEqual(Article.objects.count(), 4) self.assertEqual(Article.objects.count(), 4)
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 2)
self.assertEqual(mail.outbox[0].subject, 'Greetings from a created object') self.assertEqual(mail.outbox[0].subject, 'Greetings from a created object')
self.client.get(reverse('admin:logout')) self.client.get(reverse('admin:logout'))
@ -1722,6 +1759,19 @@ class AdminViewPermissionsTest(TestCase):
self.assertEqual(post.status_code, 403) self.assertEqual(post.status_code, 403)
self.client.get(reverse('admin:logout')) self.client.get(reverse('admin:logout'))
# view user should be able to view the article but not change any of them
# (the POST can be sent, but no modification occures)
self.client.force_login(self.viewuser)
response = self.client.get(article_changelist_url)
self.assertEqual(response.status_code, 200)
response = self.client.get(article_change_url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<a href="/test_admin/admin/admin_views/article/" class="closelink">Close</a>')
post = self.client.post(article_change_url, change_dict)
self.assertEqual(post.status_code, 302)
self.assertEqual(Article.objects.get(pk=self.a1.pk).content, '<p>Middle content</p>')
self.client.get(reverse('admin:logout'))
# change user can view all items and edit them # change user can view all items and edit them
self.client.force_login(self.changeuser) self.client.force_login(self.changeuser)
response = self.client.get(article_changelist_url) response = self.client.get(article_changelist_url)
@ -1751,9 +1801,14 @@ class AdminViewPermissionsTest(TestCase):
# Test redirection when using row-level change permissions. Refs #11513. # Test redirection when using row-level change permissions. Refs #11513.
r1 = RowLevelChangePermissionModel.objects.create(id=1, name="odd id") r1 = RowLevelChangePermissionModel.objects.create(id=1, name="odd id")
r2 = RowLevelChangePermissionModel.objects.create(id=2, name="even id") r2 = RowLevelChangePermissionModel.objects.create(id=2, name="even id")
r3 = RowLevelChangePermissionModel.objects.create(id=3, name='odd id mult 3')
r6 = RowLevelChangePermissionModel.objects.create(id=6, name='even id mult 3')
change_url_1 = reverse('admin:admin_views_rowlevelchangepermissionmodel_change', args=(r1.pk,)) change_url_1 = reverse('admin:admin_views_rowlevelchangepermissionmodel_change', args=(r1.pk,))
change_url_2 = reverse('admin:admin_views_rowlevelchangepermissionmodel_change', args=(r2.pk,)) change_url_2 = reverse('admin:admin_views_rowlevelchangepermissionmodel_change', args=(r2.pk,))
for login_user in [self.superuser, self.adduser, self.changeuser, self.deleteuser]: change_url_3 = reverse('admin:admin_views_rowlevelchangepermissionmodel_change', args=(r3.pk,))
change_url_6 = reverse('admin:admin_views_rowlevelchangepermissionmodel_change', args=(r6.pk,))
logins = [self.superuser, self.viewuser, self.adduser, self.changeuser, self.deleteuser]
for login_user in logins:
self.client.force_login(login_user) self.client.force_login(login_user)
response = self.client.get(change_url_1) response = self.client.get(change_url_1)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
@ -1765,6 +1820,18 @@ class AdminViewPermissionsTest(TestCase):
response = self.client.post(change_url_2, {'name': 'changed'}) response = self.client.post(change_url_2, {'name': 'changed'})
self.assertEqual(RowLevelChangePermissionModel.objects.get(id=2).name, 'changed') self.assertEqual(RowLevelChangePermissionModel.objects.get(id=2).name, 'changed')
self.assertRedirects(response, self.index_url) self.assertRedirects(response, self.index_url)
response = self.client.get(change_url_3)
self.assertEqual(response.status_code, 200)
response = self.client.post(change_url_3, {'name': 'changed'})
self.assertEqual(response.status_code, 302)
self.assertRedirects(response, self.index_url)
self.assertEqual(RowLevelChangePermissionModel.objects.get(id=3).name, 'odd id mult 3')
response = self.client.get(change_url_6)
self.assertEqual(response.status_code, 200)
response = self.client.post(change_url_6, {'name': 'changed'})
self.assertEqual(RowLevelChangePermissionModel.objects.get(id=6).name, 'changed')
self.assertRedirects(response, self.index_url)
self.client.get(reverse('admin:logout')) self.client.get(reverse('admin:logout'))
for login_user in [self.joepublicuser, self.nostaffuser]: for login_user in [self.joepublicuser, self.nostaffuser]:
@ -1833,6 +1900,15 @@ class AdminViewPermissionsTest(TestCase):
self.assertEqual(Article.objects.count(), 3) self.assertEqual(Article.objects.count(), 3)
self.client.logout() self.client.logout()
# view user should not be able to delete articles
self.client.force_login(self.viewuser)
response = self.client.get(delete_url)
self.assertEqual(response.status_code, 403)
post = self.client.post(delete_url, delete_dict)
self.assertEqual(post.status_code, 403)
self.assertEqual(Article.objects.count(), 3)
self.client.logout()
# Delete user can delete # Delete user can delete
self.client.force_login(self.deleteuser) self.client.force_login(self.deleteuser)
response = self.client.get(reverse('admin:admin_views_section_delete', args=(self.s1.pk,))) response = self.client.get(reverse('admin:admin_views_section_delete', args=(self.s1.pk,)))
@ -1890,6 +1966,12 @@ class AdminViewPermissionsTest(TestCase):
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
self.client.get(reverse('admin:logout')) self.client.get(reverse('admin:logout'))
# view user can view all items
self.client.force_login(self.viewuser)
response = self.client.get(reverse('admin:admin_views_article_history', args=(self.a1.pk,)))
self.assertEqual(response.status_code, 200)
self.client.get(reverse('admin:logout'))
# change user can view all items and edit them # change user can view all items and edit them
self.client.force_login(self.changeuser) self.client.force_login(self.changeuser)
response = self.client.get(reverse('admin:admin_views_article_history', args=(self.a1.pk,))) response = self.client.get(reverse('admin:admin_views_article_history', args=(self.a1.pk,)))
@ -1898,7 +1980,8 @@ class AdminViewPermissionsTest(TestCase):
# Test redirection when using row-level change permissions. Refs #11513. # Test redirection when using row-level change permissions. Refs #11513.
rl1 = RowLevelChangePermissionModel.objects.create(name="odd id") rl1 = RowLevelChangePermissionModel.objects.create(name="odd id")
rl2 = RowLevelChangePermissionModel.objects.create(name="even id") rl2 = RowLevelChangePermissionModel.objects.create(name="even id")
for login_user in [self.superuser, self.adduser, self.changeuser, self.deleteuser]: logins = [self.superuser, self.viewuser, self.adduser, self.changeuser, self.deleteuser]
for login_user in logins:
self.client.force_login(login_user) self.client.force_login(login_user)
url = reverse('admin:admin_views_rowlevelchangepermissionmodel_history', args=(rl1.pk,)) url = reverse('admin:admin_views_rowlevelchangepermissionmodel_history', args=(rl1.pk,))
response = self.client.get(url) response = self.client.get(url)
@ -2072,6 +2155,12 @@ class AdminViewPermissionsTest(TestCase):
self.assertContains(response, 'Articles') self.assertContains(response, 'Articles')
self.client.logout() self.client.logout()
self.client.force_login(self.viewuser)
response = self.client.get(self.index_url)
self.assertContains(response, 'admin_views')
self.assertContains(response, 'Articles')
self.client.logout()
self.client.force_login(self.adduser) self.client.force_login(self.adduser)
response = self.client.get(self.index_url) response = self.client.get(self.index_url)
self.assertContains(response, 'admin_views') self.assertContains(response, 'admin_views')
@ -2104,6 +2193,12 @@ class AdminViewPermissionsTest(TestCase):
self.assertNotContains(response, articles) self.assertNotContains(response, articles)
self.client.logout() self.client.logout()
self.client.force_login(self.viewuser)
response = self.client.get(index_url)
self.assertNotContains(response, 'admin_views')
self.assertNotContains(response, articles)
self.client.logout()
self.client.force_login(self.adduser) self.client.force_login(self.adduser)
response = self.client.get(index_url) response = self.client.get(index_url)
self.assertNotContains(response, 'admin_views') self.assertNotContains(response, 'admin_views')
@ -3745,6 +3840,52 @@ class AdminInlineTests(TestCase):
self.assertEqual(Widget.objects.count(), 1) self.assertEqual(Widget.objects.count(), 1)
self.assertEqual(Widget.objects.all()[0].name, "Widget 1 Updated") self.assertEqual(Widget.objects.all()[0].name, "Widget 1 Updated")
def test_simple_inline_permissions(self):
"""
Changes aren't allowed without change permissions for the inline object.
"""
# User who can view Articles
permissionuser = User.objects.create_user(
username='permissionuser', password='secret',
email='vuser@example.com', is_staff=True,
)
permissionuser.user_permissions.add(get_perm(Collector, get_permission_codename('view', Collector._meta)))
permissionuser.user_permissions.add(get_perm(Widget, get_permission_codename('view', Widget._meta)))
self.client.force_login(permissionuser)
# Without add permission, a new inline can't be added.
self.post_data['widget_set-0-name'] = 'Widget 1'
collector_url = reverse('admin:admin_views_collector_change', args=(self.collector.pk,))
response = self.client.post(collector_url, self.post_data)
self.assertEqual(response.status_code, 302)
self.assertEqual(Widget.objects.count(), 0)
# But after adding the permisson it can.
permissionuser.user_permissions.add(get_perm(Widget, get_permission_codename('add', Widget._meta)))
self.post_data['widget_set-0-name'] = "Widget 1"
collector_url = reverse('admin:admin_views_collector_change', args=(self.collector.pk,))
response = self.client.post(collector_url, self.post_data)
self.assertEqual(response.status_code, 302)
self.assertEqual(Widget.objects.count(), 1)
self.assertEqual(Widget.objects.first().name, 'Widget 1')
widget_id = Widget.objects.first().id
# Without the change permission, a POST doesn't change the object.
self.post_data['widget_set-INITIAL_FORMS'] = '1'
self.post_data['widget_set-0-id'] = str(widget_id)
self.post_data['widget_set-0-name'] = 'Widget 1 Updated'
response = self.client.post(collector_url, self.post_data)
self.assertEqual(response.status_code, 302)
self.assertEqual(Widget.objects.count(), 1)
self.assertEqual(Widget.objects.first().name, 'Widget 1')
# Now adding the change permission and editing works.
permissionuser.user_permissions.remove(get_perm(Widget, get_permission_codename('add', Widget._meta)))
permissionuser.user_permissions.add(get_perm(Widget, get_permission_codename('change', Widget._meta)))
self.post_data['widget_set-INITIAL_FORMS'] = '1'
self.post_data['widget_set-0-id'] = str(widget_id)
self.post_data['widget_set-0-name'] = 'Widget 1 Updated'
response = self.client.post(collector_url, self.post_data)
self.assertEqual(response.status_code, 302)
self.assertEqual(Widget.objects.count(), 1)
self.assertEqual(Widget.objects.first().name, 'Widget 1 Updated')
def test_explicit_autofield_inline(self): def test_explicit_autofield_inline(self):
"A model with an explicit autofield primary key can be saved as inlines. Regression for #8093" "A model with an explicit autofield primary key can be saved as inlines. Regression for #8093"
# First add a new inline # First add a new inline

View File

@ -767,10 +767,10 @@ class CreatePermissionsTests(TestCase):
] ]
create_permissions(self.app_config, verbosity=0) create_permissions(self.app_config, verbosity=0)
# add/change/delete permission by default + custom permission # view/add/change/delete permission by default + custom permission
self.assertEqual(Permission.objects.filter( self.assertEqual(Permission.objects.filter(
content_type=permission_content_type, content_type=permission_content_type,
).count(), 4) ).count(), 5)
Permission.objects.filter(content_type=permission_content_type).delete() Permission.objects.filter(content_type=permission_content_type).delete()
Permission._meta.default_permissions = [] Permission._meta.default_permissions = []

View File

@ -11,7 +11,7 @@ from django.contrib.admin.widgets import (
AdminDateWidget, AdminRadioSelect, AutocompleteSelect, AdminDateWidget, AdminRadioSelect, AutocompleteSelect,
AutocompleteSelectMultiple, AutocompleteSelectMultiple,
) )
from django.contrib.auth.models import User from django.contrib.auth.models import Permission, User
from django.db import models from django.db import models
from django.forms.widgets import Select from django.forms.widgets import Select
from django.test import SimpleTestCase, TestCase from django.test import SimpleTestCase, TestCase
@ -676,6 +676,18 @@ class ModelAdminTests(TestCase):
self.assertEqual(perms_needed, set()) self.assertEqual(perms_needed, set())
self.assertEqual(protected, []) self.assertEqual(protected, [])
def test_get_actions_requires_change_perm(self):
user = User.objects.create_user(username='bob', email='bob@test.com', password='test')
mock_request = MockRequest()
mock_request.user = user
mock_request.GET = {}
ma = ModelAdmin(Band, self.site)
self.assertEqual(list(ma.get_actions(mock_request).keys()), [])
p = Permission.objects.get(codename='change_band', content_type=get_content_type_for_model(Band()))
user.user_permissions.add(p)
mock_request.user = User.objects.get(pk=user.pk)
self.assertEqual(list(ma.get_actions(mock_request).keys()), ['delete_selected'])
class ModelAdminPermissionTests(SimpleTestCase): class ModelAdminPermissionTests(SimpleTestCase):
@ -683,6 +695,10 @@ class ModelAdminPermissionTests(SimpleTestCase):
def has_module_perms(self, app_label): def has_module_perms(self, app_label):
return app_label == 'modeladmin' return app_label == 'modeladmin'
class MockViewUser(MockUser):
def has_perm(self, perm):
return perm == 'modeladmin.view_band'
class MockAddUser(MockUser): class MockAddUser(MockUser):
def has_perm(self, perm): def has_perm(self, perm):
return perm == 'modeladmin.add_band' return perm == 'modeladmin.add_band'
@ -695,6 +711,22 @@ class ModelAdminPermissionTests(SimpleTestCase):
def has_perm(self, perm): def has_perm(self, perm):
return perm == 'modeladmin.delete_band' return perm == 'modeladmin.delete_band'
def test_has_view_permission(self):
"""
has_view_permission() returns True for users who can view objects and
False for users who can't.
"""
ma = ModelAdmin(Band, AdminSite())
request = MockRequest()
request.user = self.MockViewUser()
self.assertIs(ma.has_view_permission(request), True)
request.user = self.MockAddUser()
self.assertIs(ma.has_view_permission(request), False)
request.user = self.MockChangeUser()
self.assertIs(ma.has_view_permission(request), True)
request.user = self.MockDeleteUser()
self.assertIs(ma.has_view_permission(request), False)
def test_has_add_permission(self): def test_has_add_permission(self):
""" """
has_add_permission returns True for users who can add objects and has_add_permission returns True for users who can add objects and
@ -702,6 +734,8 @@ class ModelAdminPermissionTests(SimpleTestCase):
""" """
ma = ModelAdmin(Band, AdminSite()) ma = ModelAdmin(Band, AdminSite())
request = MockRequest() request = MockRequest()
request.user = self.MockViewUser()
self.assertFalse(ma.has_add_permission(request))
request.user = self.MockAddUser() request.user = self.MockAddUser()
self.assertTrue(ma.has_add_permission(request)) self.assertTrue(ma.has_add_permission(request))
request.user = self.MockChangeUser() request.user = self.MockChangeUser()
@ -735,6 +769,8 @@ class ModelAdminPermissionTests(SimpleTestCase):
""" """
ma = ModelAdmin(Band, AdminSite()) ma = ModelAdmin(Band, AdminSite())
request = MockRequest() request = MockRequest()
request.user = self.MockViewUser()
self.assertIs(ma.has_change_permission(request), False)
request.user = self.MockAddUser() request.user = self.MockAddUser()
self.assertFalse(ma.has_change_permission(request)) self.assertFalse(ma.has_change_permission(request))
request.user = self.MockChangeUser() request.user = self.MockChangeUser()
@ -749,6 +785,8 @@ class ModelAdminPermissionTests(SimpleTestCase):
""" """
ma = ModelAdmin(Band, AdminSite()) ma = ModelAdmin(Band, AdminSite())
request = MockRequest() request = MockRequest()
request.user = self.MockViewUser()
self.assertIs(ma.has_delete_permission(request), False)
request.user = self.MockAddUser() request.user = self.MockAddUser()
self.assertFalse(ma.has_delete_permission(request)) self.assertFalse(ma.has_delete_permission(request))
request.user = self.MockChangeUser() request.user = self.MockChangeUser()
@ -763,6 +801,8 @@ class ModelAdminPermissionTests(SimpleTestCase):
""" """
ma = ModelAdmin(Band, AdminSite()) ma = ModelAdmin(Band, AdminSite())
request = MockRequest() request = MockRequest()
request.user = self.MockViewUser()
self.assertIs(ma.has_module_permission(request), True)
request.user = self.MockAddUser() request.user = self.MockAddUser()
self.assertTrue(ma.has_module_permission(request)) self.assertTrue(ma.has_module_permission(request))
request.user = self.MockChangeUser() request.user = self.MockChangeUser()
@ -773,6 +813,8 @@ class ModelAdminPermissionTests(SimpleTestCase):
original_app_label = ma.opts.app_label original_app_label = ma.opts.app_label
ma.opts.app_label = 'anotherapp' ma.opts.app_label = 'anotherapp'
try: try:
request.user = self.MockViewUser()
self.assertIs(ma.has_module_permission(request), False)
request.user = self.MockAddUser() request.user = self.MockAddUser()
self.assertFalse(ma.has_module_permission(request)) self.assertFalse(ma.has_module_permission(request))
request.user = self.MockChangeUser() request.user = self.MockChangeUser()