From 2261f86596cccc7256dffa87b99fb43a9d897e26 Mon Sep 17 00:00:00 2001 From: Bernard Siebens Date: Mon, 3 Aug 2026 21:38:47 +0200 Subject: [PATCH] Add edit/delete actions to the news list and detail pages Both gated the same way editing already was: broad while a draft, editor/admin-only once published. The list's Edit link goes to the detail page rather than straight to the edit form, matching the member list's convention. --- management/context_processors.py | 1 + .../templates/management/news_detail.html | 7 +++ .../templates/management/news_list.html | 23 +++++++++- management/tests.py | 46 +++++++++++++++++++ management/urls.py | 1 + management/views.py | 19 ++++++++ 6 files changed, 95 insertions(+), 2 deletions(-) diff --git a/management/context_processors.py b/management/context_processors.py index 5ed3126..00e9d7a 100644 --- a/management/context_processors.py +++ b/management/context_processors.py @@ -53,6 +53,7 @@ _NAV_SECTIONS = { "news_create": "news_list", "news_detail": "news_list", "news_update": "news_list", + "news_delete": "news_list", "news_publish": "news_list", "news_unpublish": "news_list", "news_photo_upload": "news_list", diff --git a/management/templates/management/news_detail.html b/management/templates/management/news_detail.html index aafa40c..3e2bcb5 100644 --- a/management/templates/management/news_detail.html +++ b/management/templates/management/news_detail.html @@ -16,6 +16,7 @@ {% block actions %} {% if can_edit %} {% lucide "pencil" size=16 %} {% trans "Edit" %} + {% endif %} {% if can_publish %} {% if news_item.status == "draft" %} @@ -84,6 +85,12 @@ {% trans "Add photos" as add_photos_label %} {% url 'management:news_photo_upload' news_item.pk as add_photos_url %} {% include "controlpanel/_modal_form.html" with modal_id="add_photos_modal" title=add_photos_label form=photo_upload_form action_url=add_photos_url submit_label=add_photos_label submit_icon="image-plus" %} + + {% trans "Delete news item" as delete_news_title %} + {% blocktrans asvar delete_news_body %}Delete “{{ news_item }}”? Its photos go with it. This cannot be undone.{% endblocktrans %} + {% trans "Delete" as delete_label %} + {% url 'management:news_delete' news_item.pk as delete_news_url %} + {% include "controlpanel/_confirm_modal.html" with modal_id="delete_news_modal" title=delete_news_title body=delete_news_body action_url=delete_news_url submit_label=delete_label %} {% endif %} {% if can_publish %} diff --git a/management/templates/management/news_list.html b/management/templates/management/news_list.html index 6590fc1..4264348 100644 --- a/management/templates/management/news_list.html +++ b/management/templates/management/news_list.html @@ -1,5 +1,5 @@ {% extends "management/base.html" %} -{% load i18n lucide %} +{% load i18n lucide ui %} {% block heading %}{% trans "News" %}{% endblock heading %} @@ -20,6 +20,7 @@ {% trans "Teams" %} {% trans "Visibility" %} {% trans "Status" %} + @@ -43,10 +44,18 @@ {% trans "Published" %} {% endif %} + + {% if news_item.can_edit %} +
+ {% lucide "pencil" size=14 %} {% trans "Edit" %} + +
+ {% endif %} + {% empty %} - {% trans "No news items yet." %} + {% trans "No news items yet." %} {% endfor %} @@ -54,4 +63,14 @@ + + {% trans "Delete news item" as delete_news_title %} + {% trans "Delete" as delete_label %} + {% for news_item in news_items %} + {% if news_item.can_edit %} + {% url 'management:news_delete' news_item.pk as news_delete_url %} + {% blocktrans with title=news_item.title asvar delete_news_body %}Delete “{{ title }}”? Its photos go with it. This cannot be undone.{% endblocktrans %} + {% include "controlpanel/_confirm_modal.html" with modal_id=news_item.pk|dom_id:"news_delete_modal" title=delete_news_title body=delete_news_body action_url=news_delete_url submit_label=delete_label %} + {% endif %} + {% endfor %} {% endblock panel %} diff --git a/management/tests.py b/management/tests.py index 2513dcb..8c45b17 100644 --- a/management/tests.py +++ b/management/tests.py @@ -1877,3 +1877,49 @@ class NewsManagementTests(ManagementTestBase): self.club_post("news_photo_delete", {}, item.pk, photo.pk) self.assertFalse(NewsPhoto.objects.filter(pk=photo.pk).exists()) + + def test_a_coach_manager_can_delete_a_draft(self): + item = News.objects.create(club=self.club, title="Draft item", body="Body.") + self.client.force_login(self.make_coach_manager()) + + response = self.club_post("news_delete", {}, item.pk) + + self.assertRedirects(response, reverse("management:news_list")) + self.assertFalse(News.objects.filter(pk=item.pk).exists()) + + def test_a_coach_manager_cannot_delete_once_published(self): + item = News.objects.create(club=self.club, title="Live item", body="Body.") + item.publish() + self.client.force_login(self.make_coach_manager()) + + response = self.club_post("news_delete", {}, item.pk) + + self.assertEqual(response.status_code, 403) + self.assertTrue(News.objects.filter(pk=item.pk).exists()) + + def test_an_editor_can_delete_once_published(self): + item = News.objects.create(club=self.club, title="Live item", body="Body.") + item.publish() + self.client.force_login(self.make_editor()) + + self.club_post("news_delete", {}, item.pk) + + self.assertFalse(News.objects.filter(pk=item.pk).exists()) + + def test_deleting_a_news_item_removes_its_photos(self): + item = News.objects.create(club=self.club, title="Match report", body="Body.") + photo = NewsPhoto.objects.create(news_item=item, image=SimpleUploadedFile("one.jpg", b"one", content_type="image/jpeg")) + self.client.force_login(self.make_coach_manager()) + + self.club_post("news_delete", {}, item.pk) + + self.assertFalse(NewsPhoto.objects.filter(pk=photo.pk).exists()) + + def test_the_edit_and_delete_buttons_are_hidden_once_published_for_a_coach_manager(self): + item = News.objects.create(club=self.club, title="Live item", body="Body.") + item.publish() + self.client.force_login(self.make_coach_manager()) + + response = self.club_get("news_list") + + self.assertNotContains(response, reverse("management:news_update", args=[item.pk])) diff --git a/management/urls.py b/management/urls.py index 98babd9..6a1e2a5 100644 --- a/management/urls.py +++ b/management/urls.py @@ -47,6 +47,7 @@ urlpatterns = [ path("news/new/", views.NewsCreateView.as_view(), name="news_create"), path("news//", views.NewsDetailView.as_view(), name="news_detail"), path("news//edit/", views.NewsUpdateView.as_view(), name="news_update"), + path("news//delete/", views.NewsDeleteView.as_view(), name="news_delete"), path("news//publish/", views.NewsPublishView.as_view(), name="news_publish"), path("news//unpublish/", views.NewsUnpublishView.as_view(), name="news_unpublish"), path("news//photos/", views.NewsPhotoUploadView.as_view(), name="news_photo_upload"), diff --git a/management/views.py b/management/views.py index 9cae475..e000c25 100644 --- a/management/views.py +++ b/management/views.py @@ -966,6 +966,11 @@ class NewsListView(ClubStaffRequiredMixin, ListView): def get_queryset(self): return News.objects.filter(club=self.request.club).prefetch_related("teams") + def get_context_data(self, **kwargs): + for news_item in self.object_list: + news_item.can_edit = can_edit_news(self.request.user, news_item) + return super().get_context_data(**kwargs) + class NewsCreateView(NewsAuthorRequiredMixin, CreateView): model = News @@ -1031,6 +1036,20 @@ class NewsUpdateView(NewsEditRequiredMixin, UpdateView): return super().get_context_data(update_view=True, **kwargs) +class NewsDeleteView(NewsEditRequiredMixin, View): + def get_news_item(self): + return get_object_or_404(News.objects.filter(club=self.request.club), pk=self.kwargs["pk"]) + + def post(self, request, pk): + news_item = self.get_news_item() + title = str(news_item) + news_item.delete() + + body = _("“%(news)s” has been deleted.") % {"news": title} + notify(request, f"w|{_('News item deleted')}|{body}") + return redirect("management:news_list") + + class NewsPublishView(NewsPublisherRequiredMixin, RedirectOnInvalidMixin, FormView): form_class = NewsPublishForm http_method_names = ["post"]