Repository: wagtail/wagtail

Issue:
ModelAdmin title-column always links to "edit" page
### Issue Summary

Even if a user does not have the "change" permission, the `ModelAdmin` still renders a link to the "edit"-view, which then results in a permission error for this user. For example, we often use ModelAdmins which use `inspect_view_enabled = True` and only allow inspect and delete.

This worked before https://github.com/wagtail/wagtail/pull/7408 (wagtail < 2.15), because the title column has not been linked and the "Edit"-button did not appear if the user didn't have the permission.

I don't know, what's the best way to solve it. Falling back to the inspect view only works, if it has been enabled. Skipping the link if the permission is missing maybe re-creates the issue wagtail/wagtail#7333 in some cases, or at least needs some extra work to improve the html for screen readers?


### Steps to Reproduce

1. Start a new project with `wagtail start myproject`
2. Create a simple model and a `ModelAdmin` for it
3. Create a user in the "Moderators" group and only add the "view" permission for the new model to the group.
4. Create an instance with a superuser and find the linked item with the newly created moderator.

### Technical details

* Wagtail version: >= 2.15



Discussion:
After looking at the code again, I would propose the following solution:

Instead of only using the "edit"-URL here
https://github.com/wagtail/wagtail/blob/820c27fca9abec82406dad0ca6cbbf66c7e9ed7d/wagtail/contrib/modeladmin/templatetags/modeladmin_tags.py#L74-L84
I would instead use the `ModelAdmin`s `button_helper.get_buttons_for_obj()`, and use the URL of the first button, which defines a non-empty "url" (so it won't use buttons, which only define classes for JS-side actions).
With that the developer can easily change the "default" action by reordering the buttons returned by the button helper.

If that's ok, I could make a PR for that.

Resolving patch:
diff --git a/wagtail/contrib/modeladmin/helpers/button.py b/wagtail/contrib/modeladmin/helpers/button.py
--- a/wagtail/contrib/modeladmin/helpers/button.py
+++ b/wagtail/contrib/modeladmin/helpers/button.py
@@ -107,6 +107,15 @@ def get_buttons_for_obj(
             btns.append(self.delete_button(pk, classnames_add, classnames_exclude))
         return btns
 
+    def get_primary_button(self, obj):
+        ph = self.permission_helper
+        usr = self.request.user
+        pk = getattr(obj, self.opts.pk.attname)
+        if ph.user_can_edit_obj(usr, obj):
+            return self.edit_button(pk)
+        if ph.user_can_inspect_obj(usr, obj):
+            return self.inspect_button(pk)
+
 
 class PageButtonHelper(ButtonHelper):
 
diff --git a/wagtail/contrib/modeladmin/templatetags/modeladmin_tags.py b/wagtail/contrib/modeladmin/templatetags/modeladmin_tags.py
--- a/wagtail/contrib/modeladmin/templatetags/modeladmin_tags.py
+++ b/wagtail/contrib/modeladmin/templatetags/modeladmin_tags.py
@@ -71,13 +71,15 @@ def items_for_result(view, result, request):
         row_attrs = modeladmin.get_extra_attrs_for_field_col(result, field_name)
         row_attrs["class"] = " ".join(row_classes)
         row_attrs_flat = flatatt(row_attrs)
+        primary_button = None
         if field_name == modeladmin.get_list_display_add_buttons(request):
-            edit_url = view.url_helper.get_action_url("edit", result.pk)
+            primary_button = view.button_helper.get_primary_button(result)
+        if primary_button is not None and primary_button.get("url"):
             yield format_html(
                 '<td{}><div class="title-wrapper"><a href="{}" title="{}">{}</a></div></td>',
                 row_attrs_flat,
-                edit_url,
-                _("Edit this %s") % view.verbose_name,
+                primary_button["url"],
+                primary_button.get("title", ""),
                 result_repr,
             )
         else:


Test patch:
diff --git a/wagtail/contrib/modeladmin/tests/test_simple_modeladmin.py b/wagtail/contrib/modeladmin/tests/test_simple_modeladmin.py
--- a/wagtail/contrib/modeladmin/tests/test_simple_modeladmin.py
+++ b/wagtail/contrib/modeladmin/tests/test_simple_modeladmin.py
@@ -265,6 +265,39 @@ def test_col_extra_attributes(self):
         """
         self.assertContains(response, test_html, html=True)
 
+    def test_title_column_links_to_edit_view_by_default(self):
+        response = self.get()
+        self.assertEqual(response.status_code, 200)
+        test_html = """
+            <div class="title-wrapper"><a href="/admin/modeladmintest/author/edit/1/" title="Edit this author">J. R. R. Tolkien</a></div>
+        """
+        self.assertContains(response, test_html, html=True)
+
+    @mock.patch(
+        "wagtail.contrib.modeladmin.helpers.permission.PermissionHelper.user_can_edit_obj",
+        return_value=False,
+    )
+    def test_title_column_links_to_inspect_view_when_user_cannot_edit(self, *mocks):
+        response = self.get()
+        self.assertEqual(response.status_code, 200)
+        test_html = """
+            <div class="title-wrapper"><a href="/admin/modeladmintest/author/inspect/1/" title="Inspect this author">J. R. R. Tolkien</a></div>
+        """
+        self.assertContains(response, test_html, html=True)
+
+    @mock.patch(
+        "wagtail.contrib.modeladmin.helpers.permission.PermissionHelper.user_can_inspect_obj",
+        return_value=False,
+    )
+    @mock.patch(
+        "wagtail.contrib.modeladmin.helpers.permission.PermissionHelper.user_can_edit_obj",
+        return_value=False,
+    )
+    def test_title_column_is_not_linked_when_user_cannot_edit_or_inspect(self, *mocks):
+        response = self.get()
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, '<td class="field-name title">J. R. R. Tolkien')
+
 
 @override_settings(WAGTAIL_I18N_ENABLED=True)
 class TestTranslatableBookIndexView(TestCase, WagtailTestUtils):