diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
a
|
b
|
|
234 | 234 | """ |
235 | 235 | opts = self.opts |
236 | 236 | return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission()) |
| 237 | |
| 238 | def has_view_permission(self, request, obj=None): |
| 239 | """ |
| 240 | Returns True if the given request has permission to view the given |
| 241 | Django model instance. |
| 242 | |
| 243 | If `obj` is None, this should return True if the given request has |
| 244 | permission to view *any* object of the given type. |
| 245 | """ |
| 246 | opts = self.opts |
| 247 | return request.user.has_perm(opts.app_label + '.' + opts.get_view_permission()) |
237 | 248 | |
238 | 249 | def queryset(self, request): |
239 | 250 | """ |
… |
… |
|
561 | 572 | # to determine whether a given object exists. |
562 | 573 | obj = None |
563 | 574 | |
564 | | if not self.has_change_permission(request, obj): |
| 575 | if not (self.has_change_permission(request, obj) or (not request.POST and self.has_view_permission(request, obj))): |
565 | 576 | raise PermissionDenied |
566 | 577 | |
567 | 578 | if obj is None: |
… |
… |
|
631 | 642 | from django.contrib.admin.views.main import ChangeList, ERROR_FLAG |
632 | 643 | opts = self.model._meta |
633 | 644 | app_label = opts.app_label |
634 | | if not self.has_change_permission(request, None): |
| 645 | if not (self.has_change_permission(request, None) or (not request.POST and self.has_view_permission(request, None))): |
635 | 646 | raise PermissionDenied |
636 | 647 | try: |
637 | 648 | cl = ChangeList(request, self.model, self.list_display, self.list_display_links, self.list_filter, |
diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
a
|
b
|
|
281 | 281 | 'add': model_admin.has_add_permission(request), |
282 | 282 | 'change': model_admin.has_change_permission(request), |
283 | 283 | 'delete': model_admin.has_delete_permission(request), |
| 284 | 'view': model_admin.has_view_permission(request), |
284 | 285 | } |
285 | 286 | |
286 | 287 | # Check whether user has any perm for this module. |
diff --git a/django/contrib/admin/templates/admin/index.html b/django/contrib/admin/templates/admin/index.html
a
|
b
|
|
19 | 19 | <caption><a href="{{ app.app_url }}" class="section">{% blocktrans with app.name as name %}{{ name }}{% endblocktrans %}</a></caption> |
20 | 20 | {% for model in app.models %} |
21 | 21 | <tr> |
22 | | {% if model.perms.change %} |
| 22 | {% if model.perms.change or model.perms.view %} |
23 | 23 | <th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th> |
24 | 24 | {% else %} |
25 | 25 | <th scope="row">{{ model.name }}</th> |
diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py
a
|
b
|
|
34 | 34 | not is_popup and (not save_as or context['add']), |
35 | 35 | 'show_save_and_continue': not is_popup and context['has_change_permission'], |
36 | 36 | 'is_popup': is_popup, |
37 | | 'show_save': True |
| 37 | 'show_save': change and context['has_change_permission'] or context['add'] and context['has_add_permission'] |
38 | 38 | } |
39 | 39 | submit_row = register.inclusion_tag('admin/submit_line.html', takes_context=True)(submit_row) |
diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py
a
|
b
|
|
11 | 11 | def _get_all_permissions(opts): |
12 | 12 | "Returns (codename, name) for all permissions in the given opts." |
13 | 13 | perms = [] |
14 | | for action in ('add', 'change', 'delete'): |
| 14 | for action in ('add', 'change', 'delete', 'view'): |
15 | 15 | perms.append((_get_permission_codename(action, opts), u'Can %s %s' % (action, opts.verbose_name_raw))) |
16 | 16 | return perms + list(opts.permissions) |
17 | 17 | |
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
a
|
b
|
|
64 | 64 | - The "add" permission limits the user's ability to view the "add" form and add an object. |
65 | 65 | - The "change" permission limits a user's ability to view the change list, view the "change" form and change an object. |
66 | 66 | - The "delete" permission limits the ability to delete an object. |
| 67 | - The "view" permission limits the ability to view the change list and view the object detail page. |
67 | 68 | |
68 | 69 | Permissions are set globally per type of object, not per specific object instance. It is possible to say "Mary may change news stories," but it's not currently possible to say "Mary may change news stories, but only the ones she created herself" or "Mary may only change news stories that have a certain status or publication date." |
69 | 70 | |
diff --git a/django/db/models/options.py b/django/db/models/options.py
a
|
b
|
|
331 | 331 | def get_delete_permission(self): |
332 | 332 | return 'delete_%s' % self.object_name.lower() |
333 | 333 | |
| 334 | def get_view_permission(self): |
| 335 | return 'view_%s' % self.object_name.lower() |
| 336 | |
334 | 337 | def get_all_related_objects(self, local_only=False): |
335 | 338 | try: |
336 | 339 | self._related_objects_cache |