Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions django/contrib/admin/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,38 +224,44 @@ class InlineAdminFormSet(object):
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, uneditable_fields=None, has_add_permission=True,
model_admin=None):
self.opts = inline
self.formset = formset
self.fieldsets = fieldsets
self.model_admin = model_admin
if readonly_fields is None:
readonly_fields = ()
self.readonly_fields = readonly_fields
if uneditable_fields is None:
uneditable_fields = ()
self.uneditable_fields = uneditable_fields
if prepopulated_fields is None:
prepopulated_fields = {}
self.prepopulated_fields = prepopulated_fields
self.has_add_permission = has_add_permission

def __iter__(self):
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,
self.prepopulated_fields, original, self.uneditable_fields,
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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi,

I'm trying to do the rebase on 1.11 in another PR (#6734), but I am having a little trouble understanding why you had to add this condition.

I don't see how adding the view permission requires to add a check on the add permission for the inlines.

With this change, I had some strange problems (the last object in the inline got hidden because it gets the empty-form class). Removing the condition leads to an exception which looks pretty low level, so I guess there was a good reason.

I could fix it (I think) with this commit :
olivierdalang@061a2cb

But I'm not sure of the fix at all, since I don't get why the change was made in the first place.

Can you explain it to me ?

Thanks again for the great work !!

(besides, I see you have a 1.9.x backport branch for this, so do I, as I'll be needing this feature soon on 1.9.x, check it out if you're still working on this)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose, this condition is there because I had to change the behaviour of empty fields on the end of Inline formset. I You don't have add permissions and only view permissions You shouldn't be able to add anything. Before it was easy, because without change permissions, the inlines was not displayed at all, but with view permissions You have to display inlines in read only way.

BTW, can you please manage to add me to be one of the authors of the read-permissions commit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I didn't think this through, I assumed that one could add inlines with the add permission.

Of course for the author. If you like I can also withdraw my PR to django's repo and do the PR to you branch, so you keep control on this (you did all the work, and you're obviously more experienced than me in this, I just thought you may have no more time to spend on this).

Also, I'm willing to backport this patch to 1.9 (or 1.10, but can't wait for 1.11 to use it in my projects). I see you have a 1.9 backport branch too, we may want to share efforts on this. My backport branch isn't in production yet, but will probably be in the two or three weeks. It seems to work well for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might have more time for this now and during summer.

If I would do any changes, I would pull it from you PR and propagate to mine, but if not, I think, that your PR is OK.

I have the backport branch in my repository. It might be needed to update 1.9's security updates, which I don't expect to be much work. That branch is not pure backport, because I have one minor change about memcached's record size there also.

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)
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 field_name in self.uneditable_fields:
yield {
'label': label_for_field(field_name, self.opts.model, self.opts),
'widget': {
Expand Down
166 changes: 118 additions & 48 deletions django/contrib/admin/options.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion django/contrib/admin/sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ 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'):
try:
model_dict['admin_url'] = reverse('admin:%s_%s_changelist' % info, current_app=self.name)
except NoReverseMatch:
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/admin/templates/admin/change_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}{% trans 'Add' %} {{ opts.verbose_name }}{% else %}{{ original|truncatewords:"18" }}{% endif %}
</div>
{% endblock %}
Expand Down
15 changes: 10 additions & 5 deletions django/contrib/admin/templatetags/admin_modify.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,30 @@ def submit_row(context):
Displays the row of buttons for delete and save.
"""
opts = context['opts']
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_change_permission = context['has_change_permission']
has_add_permission = context['has_add_permission']
has_inline_admin_formsets = context['has_inline_admin_formsets']
can_save = (has_change_permission and change) or (has_add_permission and add) or has_inline_admin_formsets
ctx = {
'opts': opts,
'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_and_continue': not is_popup and can_save and show_save_and_continue,
'is_popup': is_popup,
'show_save': show_save,
'show_save': show_save and can_save,
'preserved_filters': context.get('preserved_filters'),
}
if context.get('original') is not None:
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/contenttypes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def get_formset(self, request, obj=None, **kwargs):
exclude = []
else:
exclude = list(self.exclude)
exclude.extend(self.get_readonly_fields(request, obj))
exclude.extend(self.get_uneditable_fields(request, obj))
if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
# Take the custom ModelForm's Meta.exclude into account only if the
# GenericInlineModelAdmin doesn't define its own.
Expand Down
2 changes: 1 addition & 1 deletion django/db/models/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,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 = ('view', 'add', 'change', 'delete')
self.permissions = []
self.object_name = None
self.app_label = app_label
Expand Down
10 changes: 10 additions & 0 deletions docs/ref/contrib/admin/actions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ models. For example, here's the user module from Django's built-in
For more background on bulk deletion, see the documentation on :ref:`object
deletion <topics-db-queries-delete>`.

.. note::

.. versionadded:: 1.10

``Change`` permission is needed for actions to accessible.

The ``change`` permission is newly needed to trigger actions by user
(previously they couldn't be even accessed in admin without ``change``
permission).

Read on to find out how to add your own actions to this list.

Writing actions
Expand Down
18 changes: 16 additions & 2 deletions docs/ref/contrib/admin/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1469,7 +1469,7 @@ templates used by the :class:`ModelAdmin` views:
a ``list`` or ``tuple`` of :class:`~django.contrib.admin.InlineModelAdmin`
objects, as described below in the :class:`~django.contrib.admin.InlineModelAdmin`
section. For example, the following would return inlines without the default
filtering based on add, change, and delete permissions::
filtering based on view, add, change, and delete permissions::

class MyModelAdmin(admin.ModelAdmin):
inlines = (MyInline,)
Expand Down Expand Up @@ -1712,6 +1712,18 @@ templates used by the :class:`ModelAdmin` views:
Should return ``True`` if adding an object is permitted, ``False``
otherwise.

.. method:: ModelAdmin.has_view_permission(request, obj=None)

.. versionadded:: 1.10

Should return ``True`` if viewing obj is permitted, ``False`` otherwise.
If obj is ``None``, should return ``True`` or ``False`` to indicate whether
viewing of objects of this type is permitted in general (e.g., ``False``
will be interpreted as meaning that the current user is not permitted to
view any object of this type).
The ``view`` permission has been made with backward compatibility in mind, so
``change`` permission imply that user can also view objects.

.. method:: ModelAdmin.has_change_permission(request, obj=None)

Should return ``True`` if editing obj is permitted, ``False`` otherwise.
Expand All @@ -1734,7 +1746,8 @@ templates used by the :class:`ModelAdmin` views:
accessing the module's index page is permitted, ``False`` otherwise.
Uses :meth:`User.has_module_perms()
<django.contrib.auth.models.User.has_module_perms>` by default. Overriding
it does not restrict access to the add, change or delete views,
it does not restrict access to the view, add, change, or delete views,
:meth:`~ModelAdmin.has_view_permission`,
:meth:`~ModelAdmin.has_add_permission`,
:meth:`~ModelAdmin.has_change_permission`, and
:meth:`~ModelAdmin.has_delete_permission` should be used for that.
Expand Down Expand Up @@ -2023,6 +2036,7 @@ adds some of its own (the shared features are actually defined in the
- :meth:`~ModelAdmin.formfield_for_choice_field`
- :meth:`~ModelAdmin.formfield_for_foreignkey`
- :meth:`~ModelAdmin.formfield_for_manytomany`
- :meth:`~ModelAdmin.has_view_permission`
- :meth:`~ModelAdmin.has_add_permission`
- :meth:`~ModelAdmin.has_change_permission`
- :meth:`~ModelAdmin.has_delete_permission`
Expand Down
8 changes: 6 additions & 2 deletions docs/ref/models/options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ Django quotes column and table names behind the scenes.
.. attribute:: Options.permissions

Extra permissions to enter into the permissions table when creating this object.
Add, delete and change permissions are automatically created for each
View, add, delete and change permissions are automatically created for each
model. This example specifies an extra permission, ``can_deliver_pizzas``::

permissions = (("can_deliver_pizzas", "Can deliver pizzas"),)
Expand All @@ -281,12 +281,16 @@ Django quotes column and table names behind the scenes.

.. attribute:: Options.default_permissions

Defaults to ``('add', 'change', 'delete')``. You may customize this list,
Defaults to ``('view', 'add', 'change', 'delete')``. You may customize this list,
for example, by setting this to an empty list if your app doesn't require
any of the default permissions. It must be specified on the model before
the model is created by :djadmin:`migrate` in order to prevent any omitted
permissions from being created.

.. versionchanged:: 1.10

The ``view`` permission was added.

``proxy``
---------

Expand Down
16 changes: 16 additions & 0 deletions docs/releases/1.10.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ Minor features
link <django.contrib.admin.AdminSite.site_url>` at the top of each admin page
will now point to ``request.META['SCRIPT_NAME']`` if set, instead of ``/``.

* Added ``view`` permission to the
:attr:`~django.db.models.Options.default_permissions`.
This allows to define read only access to the models and admin pages.
The :meth:`ModelAdmin.has_view_permission()
<django.contrib.admin.ModelAdmin.has_view_permission>` function was introduced.
The ``view`` permission has been made with backward compatibility in mind, so
``change`` permission imply that user can also view objects.

The ``change`` permission is newly needed to trigger :doc:`/ref/contrib/admin/actions`
by user.

To use list_editable option, the ``change`` permission is also needed.

This works also for inlines. To display the change view of the base object,
at least ``view`` permission is needed.

* The success message that appears after adding or editing an object now
contains a link to the object's change form.

Expand Down
1 change: 1 addition & 0 deletions docs/topics/auth/default.txt
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ The Django admin site uses permissions as follows:

Permissions can be set not only per type of object, but also per specific
object instance. By using the
:meth:`~django.contrib.admin.ModelAdmin.has_view_permission`,
:meth:`~django.contrib.admin.ModelAdmin.has_add_permission`,
:meth:`~django.contrib.admin.ModelAdmin.has_change_permission` and
:meth:`~django.contrib.admin.ModelAdmin.has_delete_permission` methods provided
Expand Down
4 changes: 4 additions & 0 deletions tests/admin_views/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ def has_change_permission(self, request, obj=None):
""" Only allow changing objects with even id number """
return request.user.is_staff and (obj is not None) and (obj.id % 2 == 0)

def has_view_permission(self, request, obj=None):
""" Only allow viewing objects with even id number """
return request.user.is_staff and (obj is not None) and (obj.id % 2 == 0)


class CustomArticleAdmin(admin.ModelAdmin):
"""
Expand Down
Loading