Permalink
Browse files

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>
  • Loading branch information...
2 people authored and timgraham committed May 2, 2018
1 parent 35b6a34 commit 825f0beda804e48e9197fcf3b0d909f9f548aa47
Showing with 579 additions and 96 deletions.
  1. +19 −8 django/contrib/admin/helpers.py
  2. +104 −33 django/contrib/admin/options.py
  3. +2 −1 django/contrib/admin/sites.py
  4. +5 −0 django/contrib/admin/static/admin/css/base.css
  5. +17 −0 django/contrib/admin/static/admin/css/forms.css
  6. +5 −1 django/contrib/admin/static/admin/css/responsive.css
  7. +1 −1 django/contrib/admin/static/admin/css/rtl.css
  8. +3 −0 django/contrib/admin/static/admin/img/icon-viewlink.svg
  9. +1 −1 django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
  10. +1 −1 django/contrib/admin/templates/admin/change_form.html
  11. +2 −2 django/contrib/admin/templates/admin/edit_inline/stacked.html
  12. +2 −2 django/contrib/admin/templates/admin/edit_inline/tabular.html
  13. +5 −1 django/contrib/admin/templates/admin/index.html
  14. +8 −2 django/contrib/admin/templates/admin/related_widget_wrapper.html
  15. +2 −1 django/contrib/admin/templates/admin/submit_line.html
  16. +15 −5 django/contrib/admin/templatetags/admin_modify.py
  17. +11 −17 django/contrib/admin/widgets.py
  18. +1 −1 django/contrib/auth/management/__init__.py
  19. +1 −1 django/db/models/options.py
  20. +3 −0 docs/ref/contrib/admin/actions.txt
  21. +18 −3 docs/ref/contrib/admin/index.txt
  22. +10 −6 docs/ref/models/options.txt
  23. +43 −0 docs/releases/2.1.txt
  24. +4 −0 docs/topics/auth/default.txt
  25. +6 −0 tests/admin_changelist/test_date_hierarchy.py
  26. +37 −2 tests/admin_changelist/tests.py
  27. +56 −1 tests/admin_filters/tests.py
  28. +4 −0 tests/admin_views/admin.py
  29. +4 −0 tests/admin_views/test_templatetags.py
  30. +144 −3 tests/admin_views/tests.py
  31. +2 −2 tests/auth_tests/test_management.py
  32. +43 −1 tests/modeladmin/tests.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,25 +238,34 @@ def __init__(self, inline, formset, fieldsets, prepopulated_fields=None,
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:
yield InlineAdminForm(
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 @@ def fields(self):
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},

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -432,7 +432,8 @@ def _build_app_dict(self, request, label=None):
'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:
@@ -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;
@@ -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 {
@@ -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;
}
@@ -35,7 +35,7 @@ th {
margin-right: 1.5em;
}
-.addlink, .changelink {
+.viewlink, .addlink, .changelink {
padding-left: 0;
padding-right: 16px;
background-position: 100% 1px;
@@ -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>
@@ -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;
}
@@ -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 %}
@@ -8,8 +8,8 @@ <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ 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 %}
@@ -25,13 +25,13 @@ <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{% 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 %}
@@ -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 %}
@@ -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 %}
@@ -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>
@@ -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
@@ -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 @@ def __init__(self, widget, rel, admin_site, can_add_related=None,
# 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 @@ def get_context(self, name, value, attrs):
'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):
@@ -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:
@@ -92,7 +92,7 @@ def __init__(self, meta, app_label=None):
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
@@ -340,6 +340,9 @@ Conditionally enabling or disabling actions
Finally, you can conditionally enable or disable actions on a per-request
(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
the values are ``(function, name, short_description)`` tuples.
Oops, something went wrong.

0 comments on commit 825f0be

Please sign in to comment.