diff --git a/com/api.py b/com/api.py index e46daea9..b39beac4 100644 --- a/com/api.py +++ b/com/api.py @@ -5,6 +5,8 @@ from django.http import Http404 from ninja_extra import ControllerBase, api_controller, route from com.calendar import IcsCalendar +from com.models import News +from core.auth.api_permissions import HasPerm from core.views.files import send_raw_file @@ -17,7 +19,7 @@ class CalendarController(ControllerBase): """Return the ICS file of the AE Google Calendar Because of Google's cors rules, we can't just do a request to google ics - from the frontend. Google is blocking CORS request in it's responses headers. + from the frontend. Google is blocking CORS request in its responses headers. The only way to do it from the frontend is to use Google Calendar API with an API key This is not especially desirable as your API key is going to be provided to the frontend. @@ -30,3 +32,26 @@ class CalendarController(ControllerBase): @route.get("/internal.ics", url_name="calendar_internal") def calendar_internal(self): return send_raw_file(IcsCalendar.get_internal()) + + +@api_controller("/news") +class NewsController(ControllerBase): + @route.patch( + "/{news_id}/moderate", + permissions=[HasPerm("com.moderate_news")], + url_name="moderate_news", + ) + def moderate_news(self, news_id: int): + news = self.get_object_or_exception(News, id=news_id) + if not news.is_moderated: + news.is_moderated = True + news.save() + + @route.delete( + "/{news_id}", + permissions=[HasPerm("com.delete_news")], + url_name="delete_news", + ) + def delete_news(self, news_id: int): + news = self.get_object_or_exception(News, id=news_id) + news.delete() diff --git a/com/tests/test_api.py b/com/tests/test_api.py index f131052e..1bcb4d13 100644 --- a/com/tests/test_api.py +++ b/com/tests/test_api.py @@ -6,12 +6,16 @@ from unittest.mock import MagicMock, patch import pytest from django.conf import settings +from django.contrib.auth.models import Permission from django.http import HttpResponse from django.test.client import Client from django.urls import reverse from django.utils import timezone +from model_bakery import baker from com.calendar import IcsCalendar +from com.models import News +from core.models import User @dataclass @@ -120,3 +124,58 @@ class TestInternalCalendar: out_file = accel_redirect_to_file(response) assert out_file is not None assert out_file.exists() + + +@pytest.mark.django_db +class TestModerateNews: + @pytest.mark.parametrize("news_is_moderated", [True, False]) + def test_moderation_ok(self, client: Client, news_is_moderated: bool): # noqa FBT + user = baker.make( + User, user_permissions=[Permission.objects.get(codename="moderate_news")] + ) + # The API call should work even if the news is initially moderated. + # In the latter case, the result should be a noop, rather than an error. + news = baker.make(News, is_moderated=news_is_moderated) + client.force_login(user) + response = client.patch( + reverse("api:moderate_news", kwargs={"news_id": news.id}) + ) + assert response.status_code == 200 + news.refresh_from_db() + assert news.is_moderated + + def test_moderation_forbidden(self, client: Client): + user = baker.make(User) + news = baker.make(News, is_moderated=False) + client.force_login(user) + response = client.patch( + reverse("api:moderate_news", kwargs={"news_id": news.id}) + ) + assert response.status_code == 403 + news.refresh_from_db() + assert not news.is_moderated + + +@pytest.mark.django_db +class TestDeleteNews: + def test_delete_news_ok(self, client: Client): + user = baker.make( + User, user_permissions=[Permission.objects.get(codename="delete_news")] + ) + news = baker.make(News) + client.force_login(user) + response = client.delete( + reverse("api:delete_news", kwargs={"news_id": news.id}) + ) + assert response.status_code == 200 + assert not News.objects.filter(id=news.id).exists() + + def test_delete_news_forbidden(self, client: Client): + user = baker.make(User) + news = baker.make(News) + client.force_login(user) + response = client.delete( + reverse("api:delete_news", kwargs={"news_id": news.id}) + ) + assert response.status_code == 403 + assert News.objects.filter(id=news.id).exists()