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.
This commit is contained in:
@@ -53,6 +53,7 @@ _NAV_SECTIONS = {
|
|||||||
"news_create": "news_list",
|
"news_create": "news_list",
|
||||||
"news_detail": "news_list",
|
"news_detail": "news_list",
|
||||||
"news_update": "news_list",
|
"news_update": "news_list",
|
||||||
|
"news_delete": "news_list",
|
||||||
"news_publish": "news_list",
|
"news_publish": "news_list",
|
||||||
"news_unpublish": "news_list",
|
"news_unpublish": "news_list",
|
||||||
"news_photo_upload": "news_list",
|
"news_photo_upload": "news_list",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
{% block actions %}
|
{% block actions %}
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
<a class="btn btn-outline btn-neutral gap-2" href="{% url 'management:news_update' news_item.pk %}">{% lucide "pencil" size=16 %} {% trans "Edit" %}</a>
|
<a class="btn btn-outline btn-neutral gap-2" href="{% url 'management:news_update' news_item.pk %}">{% lucide "pencil" size=16 %} {% trans "Edit" %}</a>
|
||||||
|
<button class="btn btn-outline btn-error gap-2" type="button" onclick="document.getElementById('delete_news_modal').showModal()">{% lucide "trash-2" size=16 %} {% trans "Delete" %}</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if can_publish %}
|
{% if can_publish %}
|
||||||
{% if news_item.status == "draft" %}
|
{% if news_item.status == "draft" %}
|
||||||
@@ -84,6 +85,12 @@
|
|||||||
{% trans "Add photos" as add_photos_label %}
|
{% trans "Add photos" as add_photos_label %}
|
||||||
{% url 'management:news_photo_upload' news_item.pk as add_photos_url %}
|
{% 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" %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
{% if can_publish %}
|
{% if can_publish %}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "management/base.html" %}
|
{% extends "management/base.html" %}
|
||||||
{% load i18n lucide %}
|
{% load i18n lucide ui %}
|
||||||
|
|
||||||
{% block heading %}{% trans "News" %}{% endblock heading %}
|
{% block heading %}{% trans "News" %}{% endblock heading %}
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
<th>{% trans "Teams" %}</th>
|
<th>{% trans "Teams" %}</th>
|
||||||
<th>{% trans "Visibility" %}</th>
|
<th>{% trans "Visibility" %}</th>
|
||||||
<th>{% trans "Status" %}</th>
|
<th>{% trans "Status" %}</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -43,10 +44,18 @@
|
|||||||
<span class="badge badge-success badge-sm">{% trans "Published" %}</span>
|
<span class="badge badge-success badge-sm">{% trans "Published" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
{% if news_item.can_edit %}
|
||||||
|
<div class="flex justify-end gap-1">
|
||||||
|
<a class="btn btn-sm btn-outline btn-neutral" href="{% url 'management:news_detail' news_item.pk %}" aria-label="{% trans 'Edit' %}">{% lucide "pencil" size=14 %} {% trans "Edit" %}</a>
|
||||||
|
<button class="btn btn-sm btn-outline btn-error" type="button" onclick="document.getElementById('{{ news_item.pk|dom_id:"news_delete_modal" }}').showModal()" aria-label="{% trans 'Delete' %}">{% lucide "trash-2" size=14 %} {% trans "Delete" %}</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="text-center opacity-60">{% trans "No news items yet." %}</td>
|
<td colspan="5" class="text-center opacity-60">{% trans "No news items yet." %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -54,4 +63,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% 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 %}
|
{% endblock panel %}
|
||||||
|
|||||||
@@ -1877,3 +1877,49 @@ class NewsManagementTests(ManagementTestBase):
|
|||||||
self.club_post("news_photo_delete", {}, item.pk, photo.pk)
|
self.club_post("news_photo_delete", {}, item.pk, photo.pk)
|
||||||
|
|
||||||
self.assertFalse(NewsPhoto.objects.filter(pk=photo.pk).exists())
|
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]))
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ urlpatterns = [
|
|||||||
path("news/new/", views.NewsCreateView.as_view(), name="news_create"),
|
path("news/new/", views.NewsCreateView.as_view(), name="news_create"),
|
||||||
path("news/<uuid:pk>/", views.NewsDetailView.as_view(), name="news_detail"),
|
path("news/<uuid:pk>/", views.NewsDetailView.as_view(), name="news_detail"),
|
||||||
path("news/<uuid:pk>/edit/", views.NewsUpdateView.as_view(), name="news_update"),
|
path("news/<uuid:pk>/edit/", views.NewsUpdateView.as_view(), name="news_update"),
|
||||||
|
path("news/<uuid:pk>/delete/", views.NewsDeleteView.as_view(), name="news_delete"),
|
||||||
path("news/<uuid:pk>/publish/", views.NewsPublishView.as_view(), name="news_publish"),
|
path("news/<uuid:pk>/publish/", views.NewsPublishView.as_view(), name="news_publish"),
|
||||||
path("news/<uuid:pk>/unpublish/", views.NewsUnpublishView.as_view(), name="news_unpublish"),
|
path("news/<uuid:pk>/unpublish/", views.NewsUnpublishView.as_view(), name="news_unpublish"),
|
||||||
path("news/<uuid:pk>/photos/", views.NewsPhotoUploadView.as_view(), name="news_photo_upload"),
|
path("news/<uuid:pk>/photos/", views.NewsPhotoUploadView.as_view(), name="news_photo_upload"),
|
||||||
|
|||||||
@@ -966,6 +966,11 @@ class NewsListView(ClubStaffRequiredMixin, ListView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return News.objects.filter(club=self.request.club).prefetch_related("teams")
|
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):
|
class NewsCreateView(NewsAuthorRequiredMixin, CreateView):
|
||||||
model = News
|
model = News
|
||||||
@@ -1031,6 +1036,20 @@ class NewsUpdateView(NewsEditRequiredMixin, UpdateView):
|
|||||||
return super().get_context_data(update_view=True, **kwargs)
|
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):
|
class NewsPublishView(NewsPublisherRequiredMixin, RedirectOnInvalidMixin, FormView):
|
||||||
form_class = NewsPublishForm
|
form_class = NewsPublishForm
|
||||||
http_method_names = ["post"]
|
http_method_names = ["post"]
|
||||||
|
|||||||
Reference in New Issue
Block a user