summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorolivierdalang <olivier.dalang@gmail.com>2018-05-02 20:39:12 +1200
committerTim Graham <timograham@gmail.com>2018-05-16 06:44:55 -0400
commit825f0beda804e48e9197fcf3b0d909f9f548aa47 (patch)
treebe5036c256efa1cd06a72b3265ed97884afc39cb /django
parent35b6a348dea6b019679fe35fd443be875bdb028e (diff)
downloaddjango-825f0beda804e48e9197fcf3b0d909f9f548aa47.tar.gz
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>
Diffstat (limited to 'django')
-rw-r--r--django/contrib/admin/helpers.py27
-rw-r--r--django/contrib/admin/options.py137
-rw-r--r--django/contrib/admin/sites.py3
-rw-r--r--django/contrib/admin/static/admin/css/base.css5
-rw-r--r--django/contrib/admin/static/admin/css/forms.css17
-rw-r--r--django/contrib/admin/static/admin/css/responsive.css6
-rw-r--r--django/contrib/admin/static/admin/css/rtl.css2
-rw-r--r--django/contrib/admin/static/admin/img/icon-viewlink.svg3
-rw-r--r--django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js2
-rw-r--r--django/contrib/admin/templates/admin/change_form.html2
-rw-r--r--django/contrib/admin/templates/admin/edit_inline/stacked.html4
-rw-r--r--django/contrib/admin/templates/admin/edit_inline/tabular.html4
-rw-r--r--django/contrib/admin/templates/admin/index.html6
-rw-r--r--django/contrib/admin/templates/admin/related_widget_wrapper.html10
-rw-r--r--django/contrib/admin/templates/admin/submit_line.html3
-rw-r--r--django/contrib/admin/templatetags/admin_modify.py20
-rw-r--r--django/contrib/admin/widgets.py28
-rw-r--r--django/contrib/auth/management/__init__.py2
-rw-r--r--django/db/models/options.py2
19 files changed, 205 insertions, 78 deletions
diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
index f010ebb63e..8bb3df7c43 100644
--- a/django/contrib/admin/helpers.py
+++ b/django/contrib/admin/helpers.py
@@ -224,7 +224,9 @@ class InlineAdminFormSet:
A wrapper around an inline formset for use in the admin system.
"""
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.formset = formset
self.fieldsets = fieldsets
@@ -236,13 +238,21 @@ class InlineAdminFormSet:
prepopulated_fields = {}
self.prepopulated_fields = prepopulated_fields
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):
+ 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()):
view_on_site_url = self.opts.get_view_on_site_url(original)
yield InlineAdminForm(
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,
)
for form in self.formset.extra_forms:
@@ -250,11 +260,12 @@ class InlineAdminFormSet:
self.formset, form, self.fieldsets, self.prepopulated_fields,
None, self.readonly_fields, model_admin=self.opts,
)
- yield InlineAdminForm(
- self.formset, self.formset.empty_form,
- self.fieldsets, self.prepopulated_fields, None,
- self.readonly_fields, model_admin=self.opts,
- )
+ if self.has_add_permission:
+ yield InlineAdminForm(
+ self.formset, self.formset.empty_form,
+ self.fieldsets, self.prepopulated_fields, None,
+ self.readonly_fields, model_admin=self.opts,
+ )
def fields(self):
fk = getattr(self.formset, "fk", None)
@@ -264,7 +275,7 @@ class InlineAdminFormSet:
for i, field_name in enumerate(flatten_fieldsets(self.fieldsets)):
if fk and fk.name == field_name:
continue
- if field_name in self.readonly_fields:
+ if not self.has_change_permission or field_name in self.readonly_fields:
yield {
'label': meta_labels.get(field_name) or label_for_field(field_name, self.opts.model, self.opts),
'widget': {'is_hidden': False},
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 7cbfde4452..e78e99f9fb 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -167,6 +167,7 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
can_add_related=related_modeladmin.has_add_permission(request),
can_change_related=related_modeladmin.has_change_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, db_field.remote_field, self.admin_site, **wrapper_kwargs
@@ -497,6 +498,25 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
codename = get_permission_codename('delete', opts)
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):
"""
Return True if the given request has any permission in the given
@@ -567,7 +587,8 @@ class ModelAdmin(BaseModelAdmin):
else:
inline_has_add_permission = inline.has_add_permission(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_delete_permission(request, obj)):
continue
@@ -624,19 +645,20 @@ class ModelAdmin(BaseModelAdmin):
def get_model_perms(self, request):
"""
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
- of those actions.
+ ``add``, ``change``, ``delete``, and ``view`` mapping to the True/False
+ for each of those actions.
"""
return {
'add': self.has_add_permission(request),
'change': self.has_change_permission(request),
'delete': self.has_delete_permission(request),
+ 'view': self.has_view_permission(request),
}
def _get_form_for_get_fields(self, request, obj):
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
add_view and change_view.
@@ -649,6 +671,10 @@ class ModelAdmin(BaseModelAdmin):
exclude = [] if excluded is None else list(excluded)
readonly_fields = self.get_readonly_fields(request, obj)
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:
# Take the custom ModelForm's Meta.exclude into account only if the
# ModelAdmin doesn't define its own.
@@ -834,6 +860,9 @@ class ModelAdmin(BaseModelAdmin):
# want *any* actions enabled on this page.
if self.actions is None or IS_POPUP_VAR in request.GET:
return OrderedDict()
+ # The change permission is required to use actions.
+ if not self.has_change_permission(request):
+ return OrderedDict()
actions = []
@@ -1082,12 +1111,19 @@ class ModelAdmin(BaseModelAdmin):
preserved_filters = self.get_preserved_filters(request)
form_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, form_url)
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({
'add': add,
'change': change,
+ 'has_view_permission': self.has_view_permission(request, obj),
'has_add_permission': self.has_add_permission(request),
'has_change_permission': self.has_change_permission(request, obj),
'has_delete_permission': self.has_delete_permission(request, obj),
+ 'has_editable_inline_admin_formsets': has_editable_inline_admin_formsets,
'has_file_field': context['adminform'].form.is_multipart() or any(
admin_formset.formset.form().is_multipart()
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
self.has_change_permission(request, obj)
):
- msg = format_html(
- _('The {name} "{obj}" was added successfully. You may edit it again below.'),
- **msg_dict
- )
- self.message_user(request, msg, messages.SUCCESS)
+ msg = _('The {name} "{obj}" was added successfully.')
+ if self.has_change_permission(request, obj):
+ msg += ' ' + _('You may edit it again below.')
+ self.message_user(request, format_html(msg, **msg_dict), messages.SUCCESS)
if post_url_continue is None:
post_url_continue = obj_url
post_url_continue = add_preserved_filters(
@@ -1438,10 +1473,15 @@ class ModelAdmin(BaseModelAdmin):
for inline, formset in zip(inline_instances, formsets):
fieldsets = list(inline.get_fieldsets(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))
inline_admin_formset = helpers.InlineAdminFormSet(
- inline, formset, fieldsets, prepopulated, readonly,
- model_admin=self,
+ inline, formset, fieldsets, prepopulated, readonly, 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)
return inline_admin_formsets
@@ -1500,13 +1540,13 @@ class ModelAdmin(BaseModelAdmin):
else:
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
if obj is None:
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':
form = ModelForm(request.POST, request.FILES, instance=obj)
form_validated = form.is_valid()
@@ -1536,11 +1576,15 @@ class ModelAdmin(BaseModelAdmin):
form = ModelForm(instance=obj)
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(
form,
list(self.get_fieldsets(request, obj)),
self.get_prepopulated_fields(request, obj),
- self.get_readonly_fields(request, obj),
+ readonly_fields,
model_admin=self)
media = self.media + adminForm.media
@@ -1591,7 +1635,7 @@ class ModelAdmin(BaseModelAdmin):
from django.contrib.admin.views.main import ERROR_FLAG
opts = self.model._meta
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
try:
@@ -1620,6 +1664,8 @@ class ModelAdmin(BaseModelAdmin):
# Actions with no confirmation
if (actions and request.method == 'POST' and
'index' in request.POST and '_save' not in request.POST):
+ if not self.has_change_permission(request):
+ raise PermissionDenied
if selected:
response = self.response_action(request, queryset=cl.get_queryset(request))
if response:
@@ -1636,6 +1682,8 @@ class ModelAdmin(BaseModelAdmin):
if (actions and request.method == 'POST' and
helpers.ACTION_CHECKBOX_NAME in request.POST and
'index' not in request.POST and '_save' not in request.POST):
+ if not self.has_change_permission(request):
+ raise PermissionDenied
if selected:
response = self.response_action(request, queryset=cl.get_queryset(request))
if response:
@@ -1656,6 +1704,8 @@ class ModelAdmin(BaseModelAdmin):
# Handle POSTed bulk-edit data.
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 = cl.formset = FormSet(request.POST, request.FILES, queryset=self.get_queryset(request))
if formset.is_valid():
@@ -1683,7 +1733,7 @@ class ModelAdmin(BaseModelAdmin):
return HttpResponseRedirect(request.get_full_path())
# 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 = cl.formset = FormSet(queryset=cl.result_list)
@@ -1814,7 +1864,7 @@ class ModelAdmin(BaseModelAdmin):
if obj is None:
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
# Then get the history for this object.
@@ -1961,8 +2011,17 @@ class InlineModelAdmin(BaseModelAdmin):
}
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):
+ 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):
"""
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):
using = router.db_for_write(self._meta.model)
collector = NestedObjects(using=using)
- if self.instance.pk is None:
+ if self.instance._state.adding:
return
collector.collect([self.instance])
if collector.protected:
@@ -2010,7 +2069,7 @@ class InlineModelAdmin(BaseModelAdmin):
def get_queryset(self, 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()
return queryset
@@ -2018,32 +2077,44 @@ class InlineModelAdmin(BaseModelAdmin):
if self.opts.auto_created:
# We're checking the rights to an auto-created intermediate model,
# 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.
- return self.has_change_permission(request, obj)
+ return self.has_view_permission(request, obj)
return super().has_add_permission(request)
def has_change_permission(self, request, obj=None):
- opts = self.opts
- if opts.auto_created:
- # The model was auto-created as intermediary for a
- # ManyToMany-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
- codename = get_permission_codename('change', opts)
- return request.user.has_perm("%s.%s" % (opts.app_label, codename))
+ if self.opts.auto_created:
+ # We're checking the rights to an auto-created intermediate model,
+ # which doesn't have its own individual permissions. The user needs
+ # to have the view permission for the related model in order to
+ # be able to do anything with the intermediate model.
+ return self.has_view_permission(request, obj)
+ return super().has_change_permission(request)
def has_delete_permission(self, request, obj=None):
if self.opts.auto_created:
# We're checking the rights to an auto-created intermediate model,
# 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.
- return self.has_change_permission(request, obj)
+ return self.has_view_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):
template = 'admin/edit_inline/stacked.html'
diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
index f7d0ac0fbc..0dafe9766b 100644
--- a/django/contrib/admin/sites.py
+++ b/django/contrib/admin/sites.py
@@ -432,7 +432,8 @@ class AdminSite:
'object_name': model._meta.object_name,
'perms': perms,
}
- if perms.get('change'):
+ if perms.get('change') or perms.get('view'):
+ model_dict['view_only'] = not perms.get('change')
try:
model_dict['admin_url'] = reverse('admin:%s_%s_changelist' % info, current_app=self.name)
except NoReverseMatch:
diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css
index 5dfeaffe81..6551e232a2 100644
--- a/django/contrib/admin/static/admin/css/base.css
+++ b/django/contrib/admin/static/admin/css/base.css
@@ -662,6 +662,11 @@ div.breadcrumbs a:focus, div.breadcrumbs a:hover {
/* ACTION ICONS */
+.viewlink, .inlineviewlink {
+ padding-left: 16px;
+ background: url(../img/icon-viewlink.svg) 0 1px no-repeat;
+}
+
.addlink {
padding-left: 16px;
background: url(../img/icon-addlink.svg) 0 1px no-repeat;
diff --git a/django/contrib/admin/static/admin/css/forms.css b/django/contrib/admin/static/admin/css/forms.css
index 82930a0cd6..5db927d6cf 100644
--- a/django/contrib/admin/static/admin/css/forms.css
+++ b/django/contrib/admin/static/admin/css/forms.css
@@ -291,12 +291,29 @@ body.popup .submit-row {
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:hover,
.submit-row a.deletelink:active {
background: #a41515;
}
+.submit-row a.closelink:focus,
+.submit-row a.closelink:hover,
+.submit-row a.closelink:active {
+ background: #aaaaaa;
+}
+
/* CUSTOM FORM FIELDS */
.vSelectMultipleField {
diff --git a/django/contrib/admin/static/admin/css/responsive.css b/django/contrib/admin/static/admin/css/responsive.css
index 2a4b2bbd40..05fd2c5123 100644
--- a/django/contrib/admin/static/admin/css/responsive.css
+++ b/django/contrib/admin/static/admin/css/responsive.css
@@ -810,12 +810,16 @@ input[type="submit"], button {
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;
margin: 0 0 10px;
text-align: center;
}
+ .submit-row a.closelink {
+ padding: 10px 0;
+ }
+
.submit-row p.deletelink-box {
order: 4;
}
diff --git a/django/contrib/admin/static/admin/css/rtl.css b/django/contrib/admin/static/admin/css/rtl.css
index f7514a5d38..d998e7ce0a 100644
--- a/django/contrib/admin/static/admin/css/rtl.css
+++ b/django/contrib/admin/static/admin/css/rtl.css
@@ -35,7 +35,7 @@ th {
margin-right: 1.5em;
}
-.addlink, .changelink {
+.viewlink, .addlink, .changelink {
padding-left: 0;
padding-right: 16px;
background-position: 100% 1px;
diff --git a/django/contrib/admin/static/admin/img/icon-viewlink.svg b/django/contrib/admin/static/admin/img/icon-viewlink.svg
new file mode 100644
index 0000000000..a1ca1d3f4e
--- /dev/null
+++ b/django/contrib/admin/static/admin/img/icon-viewlink.svg
@@ -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>
diff --git a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
index e6118be668..f4c57c40e5 100644
--- a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
+++ b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
@@ -58,7 +58,7 @@
function updateRelatedObjectLinks(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) {
return;
}
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
index 604747e6d9..1d749f25d3 100644
--- a/django/contrib/admin/templates/admin/change_form.html
+++ b/django/contrib/admin/templates/admin/change_form.html
@@ -17,7 +17,7 @@
<div class="breadcrumbs">
<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; {% 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 %}
</div>
{% endblock %}
diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html
index 65af259a21..507f69bc56 100644
--- a/django/contrib/admin/templates/admin/edit_inline/stacked.html
+++ b/django/contrib/admin/templates/admin/edit_inline/stacked.html
@@ -8,8 +8,8 @@
{{ inline_admin_formset.formset.management_form }}
{{ 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 %}">
- <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 %}
+{% 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="{% 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>
{% 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 %}
diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html
index 2f449d67af..2584745ef9 100644
--- a/django/contrib/admin/templates/admin/edit_inline/tabular.html
+++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html
@@ -25,13 +25,13 @@
{% 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>
{% 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 %}">
<td class="original">
{% if inline_admin_form.original or inline_admin_form.show_url %}<p>
{% 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 %}
+ {% 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 %}
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% endif %}
</p>{% endif %}
diff --git a/django/contrib/admin/templates/admin/index.html b/django/contrib/admin/templates/admin/index.html
index 03383db8ea..2b50015886 100644
--- a/django/contrib/admin/templates/admin/index.html
+++ b/django/contrib/admin/templates/admin/index.html
@@ -34,7 +34,11 @@
{% endif %}
{% 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>
+ {% endif %}
{% else %}
<td>&nbsp;</td>
{% endif %}
@@ -44,7 +48,7 @@
</div>
{% endfor %}
{% 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 %}
</div>
{% endblock %}
diff --git a/django/contrib/admin/templates/admin/related_widget_wrapper.html b/django/contrib/admin/templates/admin/related_widget_wrapper.html
index 7b0a809392..658a7b547b 100644
--- a/django/contrib/admin/templates/admin/related_widget_wrapper.html
+++ b/django/contrib/admin/templates/admin/related_widget_wrapper.html
@@ -3,11 +3,17 @@
{{ widget }}
{% block links %}
{% spaceless %}
- {% if can_change_related %}
- <a class="related-widget-wrapper-link change-related" id="change_id_{{ name }}"
+ {% if can_change_related or can_view_related %}
+ <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 }}"
+ {% if can_change_related %}
title="{% blocktrans %}Change selected {{ model }}{% endblocktrans %}">
<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>
{% endif %}
{% if can_add_related %}
diff --git a/django/contrib/admin/templates/admin/submit_line.html b/django/contrib/admin/templates/admin/submit_line.html
index 26f3920ffa..b9467e82b7 100644
--- a/django/contrib/admin/templates/admin/submit_line.html
+++ b/django/contrib/admin/templates/admin/submit_line.html
@@ -8,6 +8,7 @@
{% 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_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 %}
</div>
diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py
index 82bb6c9be2..60bc560df0 100644
--- a/django/contrib/admin/templatetags/admin_modify.py
+++ b/django/contrib/admin/templatetags/admin_modify.py
@@ -49,24 +49,34 @@ def submit_row(context):
"""
Display the row of buttons for delete and save.
"""
+ add = context['add']
change = context['change']
is_popup = context['is_popup']
save_as = context['save_as']
show_save = context.get('show_save', 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.update({
+ 'can_change': can_change,
'show_delete_link': (
not is_popup and context['has_delete_permission'] and
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': (
- context['has_add_permission'] and not is_popup and
- (not save_as or context['add'])
+ has_add_permission and not is_popup and
+ (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': show_save,
+ 'show_save_and_continue': can_save_and_continue,
+ 'show_save': show_save and can_save,
+ 'show_close': not(show_save and can_save)
})
return ctx
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index 5af187a5c3..4ce3e053f6 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -239,7 +239,8 @@ class RelatedFieldWidgetWrapper(forms.Widget):
template_name = 'admin/widgets/related_widget_wrapper.html'
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.attrs = widget.attrs
self.choices = widget.choices
@@ -256,6 +257,7 @@ class RelatedFieldWidgetWrapper(forms.Widget):
# XXX: The deletion UX can be confusing when dealing with cascading deletion.
cascade = getattr(rel, 'on_delete', None) is CASCADE
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
self.admin_site = admin_site
@@ -292,25 +294,17 @@ class RelatedFieldWidgetWrapper(forms.Widget):
'name': name,
'url_params': url_params,
'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:
- add_related_url = self.get_related_url(info, 'add')
- context.update(
- can_add_related=True,
- add_related_url=add_related_url,
- )
+ context['add_related_url'] = self.get_related_url(info, 'add')
if self.can_delete_related:
- delete_related_template_url = self.get_related_url(info, 'delete', '__fk__')
- context.update(
- can_delete_related=True,
- delete_related_template_url=delete_related_template_url,
- )
+ context['delete_related_template_url'] = self.get_related_url(info, 'delete', '__fk__')
+ if self.can_view_related or self.can_change_related:
+ context['change_related_template_url'] = self.get_related_url(info, 'change', '__fk__')
return context
def value_from_datadict(self, data, files, name):
diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py
index 36a2a7038f..cdf7203a9d 100644
--- a/django/contrib/auth/management/__init__.py
+++ b/django/contrib/auth/management/__init__.py
@@ -22,7 +22,7 @@ def _get_all_permissions(opts):
def _get_builtin_permissions(opts):
"""
Return (codename, name) for all autogenerated permissions.
- By default, this is ('add', 'change', 'delete')
+ By default, this is ('add', 'change', 'delete', 'view')
"""
perms = []
for action in opts.default_permissions:
diff --git a/django/db/models/options.py b/django/db/models/options.py
index 5364383076..c0c925375f 100644
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -92,7 +92,7 @@ class Options:
self.unique_together = []
self.index_together = []
self.select_on_save = False
- self.default_permissions = ('add', 'change', 'delete')
+ self.default_permissions = ('add', 'change', 'delete', 'view')
self.permissions = []
self.object_name = None
self.app_label = app_label