diff options
author | olivierdalang <olivier.dalang@gmail.com> | 2018-05-02 20:39:12 +1200 |
---|---|---|
committer | Tim Graham <timograham@gmail.com> | 2018-05-16 06:44:55 -0400 |
commit | 825f0beda804e48e9197fcf3b0d909f9f548aa47 (patch) | |
tree | be5036c256efa1cd06a72b3265ed97884afc39cb /django | |
parent | 35b6a348dea6b019679fe35fd443be875bdb028e (diff) | |
download | django-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')
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> › <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a> -› {% if has_change_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %} +› {% if has_view_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %} › {% 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> <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> <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> </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 |