Repository: maykinmedia/referentielijsten
Pull request #77: :sparkles: [#72] add link from table to items in admin
Author: login: TimdeBeer1
Created: 2025-05-09T10:28:02Z | Merged: 2025-05-26T12:43:30Z

Fixes #72

**Changes**

Added a column showing a link that points to the list view of items.



Linked issue #72: Add link from table to items in admin table list view
### Thema / Theme

Admin

### Omschrijving / Description

Add a column "actions" showing link "Show items in item list view" that points to the list view of items (the admin item list view, filtered by the table name).

### Toegevoegde waarde / Added value

_No response_

### Aanvullende opmerkingen / Additional context

_No response_
Linked issue comments: user: login: stevenbal
created_at: 2025-05-12T07:21:39Z
body: @alextreme I'm assuming this is owner: maykin?
author_association: COLLABORATOR
user: login: alextreme
created_at: 2025-05-12T14:53:10Z
body: @stevenbal indeed
author_association: MEMBER

Diff:
diff --git a/src/referentielijsten/api/admin.py b/src/referentielijsten/api/admin.py
index ba57b81..f6b5314 100644
--- a/src/referentielijsten/api/admin.py
+++ b/src/referentielijsten/api/admin.py
@@ -1,5 +1,9 @@
+from urllib.parse import urlencode
+
 from django.contrib import admin
+from django.urls import reverse
 from django.utils import timezone
+from django.utils.html import format_html
 from django.utils.translation import gettext_lazy as _
 
 from import_export import resources
@@ -44,6 +48,7 @@ class ItemAdmin(ImportExportModelAdmin):
     list_filter = (
         GeldigListFilter,
         ("tabel__naam", filter_title("tabel naam")),
+        ("tabel__code", filter_title("tabel code")),
     )
     fields = (
         "tabel",
@@ -83,7 +88,7 @@ def get_import_resource_kwargs(self, request, form=None):
 
 @admin.register(Tabel)
 class TabelAdmin(admin.ModelAdmin):
-    list_display = ("naam", "code", "is_geldig")
+    list_display = ("naam", "code", "is_geldig", "items_link")
     list_filter = (GeldigListFilter,)
     inlines = [ItemInline]
 
@@ -117,3 +122,15 @@ def is_geldig(self, obj):
             return True
 
         return False
+
+    @admin.display(description=_("Actions"))
+    def items_link(self, obj):
+        url = (
+            # TODO replace with reverse(..., query=...) once Django is upgraded to 5.2
+            reverse("admin:api_item_changelist")
+            + "?"
+            + urlencode({"tabel__code": obj.code})
+        )
+        return format_html(
+            '<a href="{}">{}</a>', url, _("Show items in item list view")
+        )
diff --git a/src/referentielijsten/api/tests/test_tabellen_admin.py b/src/referentielijsten/api/tests/test_tabellen_admin.py
new file mode 100644
index 0000000..7a24318
--- /dev/null
+++ b/src/referentielijsten/api/tests/test_tabellen_admin.py
@@ -0,0 +1,52 @@
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+
+from django_webtest import WebTest
+from maykin_2fa.test import disable_admin_mfa
+
+from referentielijsten.accounts.tests.factories import UserFactory
+
+from .factories import ItemFactory, TabelFactory
+
+
+@disable_admin_mfa()
+class TabellenAdminTests(WebTest):
+    @classmethod
+    def setUpTestData(cls):
+        super().setUpTestData()
+
+        cls.superuser = UserFactory.create(superuser=True)
+
+    def test_items_link_returns_correct_admin_url(self):
+        tabel = TabelFactory.create()
+
+        # TODO replace with reverse(..., query=...) once Django is upgraded to 5.2
+        expected_url = (
+            reverse("admin:api_item_changelist") + f"?tabel__code={tabel.code}"
+        )
+        expected_html = (
+            f'<a href="{expected_url}">{_("Show items in item list view")}</a>'
+        )
+
+        response = self.app.get(
+            reverse("admin:api_tabel_changelist"), user=self.superuser
+        )
+        self.assertIn(expected_html, response.content.decode())
+
+    def test_items_link_target_url_is_accessible(self):
+        tabel = TabelFactory.create(code="matching")
+        tabel1 = TabelFactory.create(code="not")
+
+        item = ItemFactory.create(tabel=tabel)
+        item1 = ItemFactory.create(tabel=tabel1)
+
+        url = reverse("admin:api_item_changelist") + f"?tabel__code={tabel.code}"
+
+        response = self.app.get(url, user=self.superuser)
+        self.assertEqual(response.status_code, 200)
+
+        matching_item_change_url = reverse("admin:api_item_change", args=[item.id])
+        self.assertIn(matching_item_change_url, response.text)
+
+        other_item_change_url = reverse("admin:api_item_change", args=[item1.id])
+        self.assertNotIn(other_item_change_url, response.text)


Reviews:
user: login: stevenbal
body: Looks good overall, minor suggestions
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T08:15:19Z
user: login: stevenbal
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T08:29:19Z
user: login: stevenbal
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T14:29:51Z
user: login: stevenbal
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-26T12:43:25Z

Review comments:
diff_hunk: @@ -71,3 +75,16 @@ def test_tabellen_is_not_geldig(self):
         self.assertEqual(response_data["count"], 1)
         self.assertEqual(response_data["results"][0]["code"], "2")
         self.assertEqual(response_data["results"][0]["naam"], "ongeldig")
+
+    def test_items_link_returns_correct_admin_url(self):
path: src/referentielijsten/api/tests/test_tabellen.py
user: login: stevenbal
body: This test file is specifically for API tests for tabellen, so I think it'd be be better to create a separate file `test_tabellen_admin.py` with a testcase that inherits from `WebTest`. With webtest you can do something like `self.app.get(reverse(expected_url), user=user)` (see `src/referentielijsten/accounts/tests/test_permission_limit.py` for example)
created_at: 2025-05-12T08:06:47Z
author_association: COLLABORATOR
diff_hunk: @@ -82,3 +86,14 @@ def is_geldig(self, obj):
             return True
 
         return False
+
+    @admin.display(description=_("Actions"))
+    def items_link(self, obj):
+        url = (
+            reverse("admin:api_item_changelist")
path: src/referentielijsten/api/admin.py
user: login: stevenbal
body: In django 5.2 you could use `reverse(..., query={...})` here, but Referentielijsten is still on django 4.2. Could you add a comment here to modify this once we're on 5.2? https://docs.djangoproject.com/en/5.2/ref/urlresolvers/

`# TODO replace with reverse(..., query=...) once Django is upgraded to 5.2`
created_at: 2025-05-12T08:12:29Z
author_association: COLLABORATOR
diff_hunk: @@ -71,3 +75,16 @@ def test_tabellen_is_not_geldig(self):
         self.assertEqual(response_data["count"], 1)
         self.assertEqual(response_data["results"][0]["code"], "2")
         self.assertEqual(response_data["results"][0]["naam"], "ongeldig")
+
+    def test_items_link_returns_correct_admin_url(self):
+        site = AdminSite()
+        admin = TabelAdmin(Tabel, site)
+        tabel = TabelFactory.create()
+
+        link = admin.items_link(tabel)
+        expected_url = reverse("admin:api_item_changelist") + f"?tabel__id={tabel.id}"
path: src/referentielijsten/api/tests/test_tabellen.py
user: login: stevenbal
body: Could you add the same TODO comment I mentioned in the other file here?
created_at: 2025-05-12T08:13:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,48 @@
+from django.contrib.admin.sites import AdminSite
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+
+from django_webtest import WebTest
+from maykin_2fa.test import disable_admin_mfa
+
+from referentielijsten.accounts.tests.factories import UserFactory
+
+from ..admin import TabelAdmin
+from ..models import Tabel
+from .factories import TabelFactory
+
+
+@disable_admin_mfa()
+class TabellenAdminTests(WebTest):
+
+    @classmethod
+    def setUpTestData(cls):
+        super().setUpTestData()
+
+        cls.superuser = UserFactory.create(superuser=True)
+
+    def test_items_link_returns_correct_admin_url(self):
+        site = AdminSite()
+        admin = TabelAdmin(Tabel, site)
+        tabel = TabelFactory.create()
+
+        link = admin.items_link(tabel)
+
+        # TODO replace with reverse(..., query=...) once Django is upgraded to 5.2
+        expected_url = reverse("admin:api_item_changelist") + f"?tabel__id={tabel.id}"
+        expected_html = (
+            f'<a href="{expected_url}">{_("Show items in item list view")}</a>'
+        )
+
+        self.assertHTMLEqual(link, expected_html)
+
path: src/referentielijsten/api/tests/test_tabellen_admin.py
user: login: stevenbal
body: Instead of manually setting up the admin, you can use webtest here to simulate what a user would do by making a request to the tabel changelist (not 100% sure if the assertion I do here is correct, but something like this):

```suggestion
    # TODO replace with reverse(..., query=...) once Django is upgraded to 5.2
    expected_url = reverse("admin:api_item_changelist") + f"?tabel__id={tabel.id}"
    expected_html = (
        f'<a href="{expected_url}">{_("Show items in item list view")}</a>'
    )
    
    response = self.app.get(
        reverse("admin:api_tabel_changelist", user=self.superuser)
    )

    self.assertIn(expected_html, response.content)
```

created_at: 2025-05-13T08:29:11Z
author_association: COLLABORATOR
diff_hunk: @@ -82,3 +86,15 @@ def is_geldig(self, obj):
             return True
 
         return False
+
+    @admin.display(description=_("Actions"))
+    def items_link(self, obj):
+        url = (
+            # TODO replace with reverse(..., query=...) once Django is upgraded to 5.2
+            reverse("admin:api_item_changelist")
+            + "?"
+            + urlencode({"tabel__id": obj.id})
path: src/referentielijsten/api/admin.py
user: login: stevenbal
body: Didn't notice this during the first review, I checked the admin views and right now there's no filter option in the sidebar for id. Let's change this to use `code` instead since that's also unique and a bit cleaner to use than the `id`

![afbeelding](https://github.com/user-attachments/assets/ea227a62-75d1-4ddd-897e-855e9422b2d2)

```suggestion
            + urlencode({"tabel__code": obj.code})
```

Could you also add `tabel__code` to the `ItemAdmin.list_filter`? That way it shows the filters accordingly if you navigate to the changelist via this link:

![afbeelding](https://github.com/user-attachments/assets/1198861c-fd05-4b05-b9dc-6d9f505f4c9a)

created_at: 2025-05-13T14:26:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,44 @@
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+
+from django_webtest import WebTest
+from maykin_2fa.test import disable_admin_mfa
+
+from referentielijsten.accounts.tests.factories import UserFactory
+
+from .factories import TabelFactory
+
+
+@disable_admin_mfa()
+class TabellenAdminTests(WebTest):
+
+    @classmethod
+    def setUpTestData(cls):
+        super().setUpTestData()
+
+        cls.superuser = UserFactory.create(superuser=True)
+
+    def test_items_link_returns_correct_admin_url(self):
+        tabel = TabelFactory.create()
+
+        # TODO replace with reverse(..., query=...) once Django is upgraded to 5.2
+        expected_url = reverse("admin:api_item_changelist") + f"?tabel__id={tabel.id}"
+        expected_html = (
+            f'<a href="{expected_url}">{_("Show items in item list view")}</a>'
+        )
+
+        response = self.app.get(
+            reverse("admin:api_tabel_changelist"), user=self.superuser
+        )
+        self.assertIn(expected_html, response.content.decode())
+
+    def test_items_link_target_url_is_accessible(self):
+        tabel = TabelFactory.create()
+
+        # TODO replace with reverse(..., query=...) once Django is upgraded to 5.2
+        url = reverse("admin:api_item_changelist") + f"?tabel__id={tabel.id}"
+
+        response = self.app.get(url, user=self.superuser)
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(str(tabel.id), response.text)
path: src/referentielijsten/api/tests/test_tabellen_admin.py
user: login: stevenbal
body: I think this test would be a bit more robust if you:
- assert that the changeview URL of the table is in `response.text`, currently it just checks if the `id` (which is just a number) is in the page
- create a different tabel with the factory and explicitly assert that the URL for that table is not in `response.text`, that way we know the filter is working as expected
created_at: 2025-05-13T14:29:33Z
author_association: COLLABORATOR

PR comments:
user: login: codecov-commenter
created_at: 2025-05-09T10:29:29Z
body: ## [Codecov](https://app.codecov.io/gh/maykinmedia/referentielijsten/pull/77?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maykinmedia) Report
:white_check_mark: All modified and coverable lines are covered by tests.
:white_check_mark: Project coverage is 94.16%. Comparing base ([`d8928af`](https://app.codecov.io/gh/maykinmedia/referentielijsten/commit/d8928afb4acfb1eda9d221ccb0b44b36d9f9defd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maykinmedia)) to head ([`7a8ad63`](https://app.codecov.io/gh/maykinmedia/referentielijsten/commit/7a8ad63516b56a487f012e698f1c2eec99d0da6a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maykinmedia)).
:warning: Report is 144 commits behind head on master.

<details><summary>Additional details and impacted files</summary>



```diff
@@            Coverage Diff             @@
##           master      #77      +/-   ##
==========================================
+ Coverage   92.89%   94.16%   +1.27%     
==========================================
  Files          56       57       +1     
  Lines        1112     1149      +37     
==========================================
+ Hits         1033     1082      +49     
+ Misses         79       67      -12     
```
</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/maykinmedia/referentielijsten/pull/77?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maykinmedia).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=maykinmedia).
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: stevenbal
created_at: 2025-05-23T08:20:56Z
body: @TimdeBeer1 rebasing on master should fix the failing code quality job
author_association: COLLABORATOR