diff --git a/django/contrib/admin/templates/admin/index.html b/django/contrib/admin/templates/admin/index.html
index 899727a4ba..dd97e79cb3 100644
--- a/django/contrib/admin/templates/admin/index.html
+++ b/django/contrib/admin/templates/admin/index.html
@@ -31,10 +31,10 @@
{% for entry in admin_log %}
{% if entry.is_addition %}{% translate 'Added:' %}{% elif entry.is_change %}{% translate 'Changed:' %}{% elif entry.is_deletion %}{% translate 'Deleted:' %}{% endif %}
- {% if entry.is_deletion or not entry.get_admin_url %}
+ {% if entry.is_deletion or not entry.url %}
{{ entry.object_repr }}
{% else %}
- {{ entry.object_repr }}
+ {{ entry.object_repr }}
{% endif %}
{% if entry.content_type %}
diff --git a/django/contrib/admin/templatetags/log.py b/django/contrib/admin/templatetags/log.py
index 55b2c46fa5..856e8f0eeb 100644
--- a/django/contrib/admin/templatetags/log.py
+++ b/django/contrib/admin/templatetags/log.py
@@ -19,7 +19,22 @@ class AdminLogNode(template.Node):
if not user_id.isdigit():
user_id = context[self.user].pk
entries = entries.filter(user__pk=user_id)
- context[self.varname] = entries[: int(self.limit)]
+ filtered_context = entries.select_related("content_type", "user")[
+ : int(self.limit)
+ ]
+ for entry in filtered_context:
+ content_type = entry.content_type
+ if not content_type:
+ entry.url = entry.get_admin_url()
+ continue
+ user = entry.user
+ view_permission_name = f"{content_type.app_label}.view_{content_type.model}"
+ user_has_permission = user.has_perm(view_permission_name)
+ if user_has_permission:
+ entry.url = entry.get_admin_url()
+ else:
+ entry.url = None
+ context[self.varname] = filtered_context
return ""
diff --git a/tests/admin_utils/test_logentry.py b/tests/admin_utils/test_logentry.py
index 20bbcccb1c..a64adc1d56 100644
--- a/tests/admin_utils/test_logentry.py
+++ b/tests/admin_utils/test_logentry.py
@@ -3,7 +3,7 @@ from datetime import datetime
from django.contrib.admin.models import ADDITION, CHANGE, DELETION, LogEntry
from django.contrib.admin.utils import quote
-from django.contrib.auth.models import User
+from django.contrib.auth.models import User, Permission
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase, override_settings
from django.urls import reverse
@@ -351,6 +351,32 @@ class LogEntryTests(TestCase):
counted_presence_after = response.content.count(should_contain)
self.assertEqual(counted_presence_before - 1, counted_presence_after)
+ def test_recent_actions_user_lacks_view_permission(self):
+ # No permission to view articles. Log is a span with no link.
+ response = self.client.get(reverse("admin:index"))
+ should_contain = """Article"""
+ self.assertContains(response, should_contain)
+
+ # Add permission to view articles. Log is a link.
+ logentry = LogEntry.objects.get(content_type__model__iexact="article")
+ view_permission_name = f"{logentry.content_type.app_label}.view_{logentry.content_type.model}"
+ view_article_permission = Permission.objects.create(
+ name=f"Can view {logentry.content_type.model}",
+ content_type=logentry.content_type,
+ codename=view_permission_name,
+ )
+ self.user.user_permissions.add(view_article_permission)
+
+ response = self.client.get(reverse("admin:index"))
+ expected_log_entry_link = reverse(
+ "admin:admin_utils_article_change", args=(quote(self.a1.pk),)
+ )
+ should_contain = """%s""" % (
+ escape(expected_log_entry_link),
+ escape(repr(self.a1)),
+ )
+ self.assertContains(response, should_contain)
+
def test_proxy_model_content_type_is_used_for_log_entries(self):
"""
Log entries for proxy models should have the proxy model's contenttype