mirror of
https://github.com/ae-utbm/sith.git
synced 2025-02-27 09:57:09 +00:00
API route to fetch news dates
This commit is contained in:
parent
fc3b82c35c
commit
86c2ea7fd9
@ -7,3 +7,17 @@ class ClubSchema(ModelSchema):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Club
|
model = Club
|
||||||
fields = ["id", "name"]
|
fields = ["id", "name"]
|
||||||
|
|
||||||
|
|
||||||
|
class ClubProfileSchema(ModelSchema):
|
||||||
|
"""The infos needed to display a simple club profile."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Club
|
||||||
|
fields = ["id", "name", "logo"]
|
||||||
|
|
||||||
|
url: str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_url(obj: Club) -> str:
|
||||||
|
return obj.get_absolute_url()
|
||||||
|
30
com/api.py
30
com/api.py
@ -1,11 +1,16 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from ninja_extra import ControllerBase, api_controller, route
|
from ninja import Query
|
||||||
|
from ninja_extra import ControllerBase, api_controller, paginate, route
|
||||||
|
from ninja_extra.pagination import PageNumberPaginationExtra
|
||||||
|
from ninja_extra.schemas import PaginatedResponseSchema
|
||||||
|
|
||||||
from com.calendar import IcsCalendar
|
from com.calendar import IcsCalendar
|
||||||
from com.models import News
|
from com.models import News, NewsDate
|
||||||
|
from com.schemas import NewsDateFilterSchema, NewsDateSchema
|
||||||
from core.auth.api_permissions import HasPerm
|
from core.auth.api_permissions import HasPerm
|
||||||
from core.views.files import send_raw_file
|
from core.views.files import send_raw_file
|
||||||
|
|
||||||
@ -37,7 +42,7 @@ class CalendarController(ControllerBase):
|
|||||||
@api_controller("/news")
|
@api_controller("/news")
|
||||||
class NewsController(ControllerBase):
|
class NewsController(ControllerBase):
|
||||||
@route.patch(
|
@route.patch(
|
||||||
"/{news_id}/moderate",
|
"/{int:news_id}/moderate",
|
||||||
permissions=[HasPerm("com.moderate_news")],
|
permissions=[HasPerm("com.moderate_news")],
|
||||||
url_name="moderate_news",
|
url_name="moderate_news",
|
||||||
)
|
)
|
||||||
@ -49,10 +54,27 @@ class NewsController(ControllerBase):
|
|||||||
news.save()
|
news.save()
|
||||||
|
|
||||||
@route.delete(
|
@route.delete(
|
||||||
"/{news_id}",
|
"/{int:news_id}",
|
||||||
permissions=[HasPerm("com.delete_news")],
|
permissions=[HasPerm("com.delete_news")],
|
||||||
url_name="delete_news",
|
url_name="delete_news",
|
||||||
)
|
)
|
||||||
def delete_news(self, news_id: int):
|
def delete_news(self, news_id: int):
|
||||||
news = self.get_object_or_exception(News, id=news_id)
|
news = self.get_object_or_exception(News, id=news_id)
|
||||||
news.delete()
|
news.delete()
|
||||||
|
|
||||||
|
@route.get(
|
||||||
|
"/date",
|
||||||
|
url_name="fetch_news_dates",
|
||||||
|
response=PaginatedResponseSchema[NewsDateSchema],
|
||||||
|
)
|
||||||
|
@paginate(PageNumberPaginationExtra, page_size=50)
|
||||||
|
def fetch_news_dates(
|
||||||
|
self,
|
||||||
|
filters: Query[NewsDateFilterSchema],
|
||||||
|
text_format: Literal["md", "html"] = "md",
|
||||||
|
):
|
||||||
|
return filters.filter(
|
||||||
|
NewsDate.objects.viewable_by(self.context.request.user)
|
||||||
|
.order_by("start_date")
|
||||||
|
.select_related("news", "news__club")
|
||||||
|
)
|
||||||
|
58
com/schemas.py
Normal file
58
com/schemas.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from ninja import FilterSchema, ModelSchema
|
||||||
|
from ninja_extra import service_resolver
|
||||||
|
from ninja_extra.controllers import RouteContext
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from club.schemas import ClubProfileSchema
|
||||||
|
from com.models import News, NewsDate
|
||||||
|
from core.markdown import markdown
|
||||||
|
|
||||||
|
|
||||||
|
class NewsDateFilterSchema(FilterSchema):
|
||||||
|
before: datetime | None = Field(None, q="end_date__lt")
|
||||||
|
after: datetime | None = Field(None, q="start_date__gt")
|
||||||
|
club_id: int | None = Field(None, q="news__club_id")
|
||||||
|
news_id: int | None = None
|
||||||
|
is_moderated: bool | None = Field(None, q="news__is_moderated")
|
||||||
|
title: str | None = Field(None, q="news__title__icontains")
|
||||||
|
|
||||||
|
|
||||||
|
class NewsSchema(ModelSchema):
|
||||||
|
class Meta:
|
||||||
|
model = News
|
||||||
|
fields = ["id", "title", "summary", "is_moderated"]
|
||||||
|
|
||||||
|
club: ClubProfileSchema
|
||||||
|
url: str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_summary(obj: News) -> str:
|
||||||
|
# if this is returned from a route that allows the
|
||||||
|
# user to choose the text format (md or html)
|
||||||
|
# and the user chose "html", convert the markdown to html
|
||||||
|
context: RouteContext = service_resolver(RouteContext)
|
||||||
|
if context.kwargs.get("text_format", "") == "html":
|
||||||
|
return markdown(obj.summary)
|
||||||
|
return obj.summary
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_url(obj: News) -> str:
|
||||||
|
return obj.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
|
class NewsDateSchema(ModelSchema):
|
||||||
|
"""Basic infos about an event occurrence.
|
||||||
|
|
||||||
|
Warning:
|
||||||
|
This uses [NewsSchema][], which itself
|
||||||
|
uses [ClubProfileSchema][club.schemas.ClubProfileSchema].
|
||||||
|
Don't forget the appropriated `select_related`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = NewsDate
|
||||||
|
fields = ["id", "start_date", "end_date"]
|
||||||
|
|
||||||
|
news: NewsSchema
|
@ -3,18 +3,22 @@ from datetime import datetime, timedelta
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.test.client import Client
|
from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from model_bakery import baker
|
from django.utils.timezone import now
|
||||||
|
from model_bakery import baker, seq
|
||||||
|
from pytest_django.asserts import assertNumQueries
|
||||||
|
|
||||||
from com.calendar import IcsCalendar
|
from com.calendar import IcsCalendar
|
||||||
from com.models import News
|
from com.models import News, NewsDate
|
||||||
|
from core.markdown import markdown
|
||||||
from core.models import User
|
from core.models import User
|
||||||
|
|
||||||
|
|
||||||
@ -184,3 +188,63 @@ class TestDeleteNews:
|
|||||||
)
|
)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
assert News.objects.filter(id=news.id).exists()
|
assert News.objects.filter(id=news.id).exists()
|
||||||
|
|
||||||
|
|
||||||
|
class TestFetchNewsDates(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
News.objects.all().delete()
|
||||||
|
cls.dates = baker.make(
|
||||||
|
NewsDate,
|
||||||
|
_quantity=5,
|
||||||
|
_bulk_create=True,
|
||||||
|
start_date=seq(value=now(), increment_by=timedelta(days=1)),
|
||||||
|
end_date=seq(
|
||||||
|
value=now() + timedelta(hours=2), increment_by=timedelta(days=1)
|
||||||
|
),
|
||||||
|
news=iter(
|
||||||
|
baker.make(News, is_moderated=True, _quantity=5, _bulk_create=True)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
cls.dates.append(
|
||||||
|
baker.make(
|
||||||
|
NewsDate,
|
||||||
|
start_date=now() + timedelta(days=2, hours=1),
|
||||||
|
end_date=now() + timedelta(days=2, hours=5),
|
||||||
|
news=baker.make(News, is_moderated=True),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cls.dates.sort(key=lambda d: d.start_date)
|
||||||
|
|
||||||
|
def test_num_queries(self):
|
||||||
|
with assertNumQueries(2):
|
||||||
|
self.client.get(reverse("api:fetch_news_dates"))
|
||||||
|
|
||||||
|
def test_html_format(self):
|
||||||
|
"""Test that when the summary is asked in html, the summary is in html."""
|
||||||
|
summary_1 = "# First event\nThere is something happening.\n"
|
||||||
|
self.dates[0].news.summary = summary_1
|
||||||
|
self.dates[0].news.save()
|
||||||
|
summary_2 = (
|
||||||
|
"# Second event\n"
|
||||||
|
"There is something happening **for real**.\n"
|
||||||
|
"Everything is [here](https://youtu.be/dQw4w9WgXcQ)\n"
|
||||||
|
)
|
||||||
|
self.dates[1].news.summary = summary_2
|
||||||
|
self.dates[1].news.save()
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("api:fetch_news_dates") + "?page_size=2&text_format=html"
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
dates = response.json()["results"]
|
||||||
|
assert dates[0]["news"]["summary"] == markdown(summary_1)
|
||||||
|
assert dates[1]["news"]["summary"] == markdown(summary_2)
|
||||||
|
|
||||||
|
def test_fetch(self):
|
||||||
|
after = quote((now() + timedelta(days=1)).isoformat())
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("api:fetch_news_dates") + f"?page_size=3&after={after}"
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
dates = response.json()["results"]
|
||||||
|
assert [d["id"] for d in dates] == [d.id for d in self.dates[1:4]]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user