Skip to content

Commit 61e9f97

Browse files
committed
feat: better error handling with daily articles
1 parent 49f6d6a commit 61e9f97

File tree

15 files changed

+183
-59
lines changed

15 files changed

+183
-59
lines changed

backend/caviardeul/management/commands/check_next_article.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@ def handle(self, *args, **options):
1616
raise CommandError("No daily article left")
1717

1818
try:
19-
title, text = get_article_html_from_wikipedia(next_article.page_id)
19+
get_article_html_from_wikipedia(next_article.page_id)
2020
except ArticleFetchError:
2121
raise CommandError("Error when retrieving daily article")
22-
23-
if "redirectMsg" in text:
24-
raise CommandError("Next daily article has a redirect")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from ninja import Schema
2+
3+
4+
class ErrorSchema(Schema):
5+
detail: str

backend/caviardeul/services/articles.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,19 @@ def get_article_html_from_wikipedia(page_id: str) -> tuple[str, str]:
5353
},
5454
)
5555
if response.status_code != 200:
56-
raise ArticleFetchError
56+
raise ArticleFetchError(f"Unexected response from API: {response.status_code}")
5757

5858
data = response.json()
5959
if "error" in data:
60-
raise ArticleFetchError
60+
raise ArticleFetchError("Error received in API response")
6161

6262
data = data["parse"]
63-
return data["title"], data["text"]
63+
html_content = data["text"]
64+
65+
if 'class="redirectMsg"' in html_content:
66+
raise ArticleFetchError("Redirection received in article payload")
67+
68+
return data["title"], html_content
6469

6570

6671
def _prepare_article_content_from_html(page_title: str, html_content: str) -> str:
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from django.utils import timezone
2+
3+
from caviardeul.models import DailyArticle
4+
5+
6+
def get_current_daily_article_id() -> int | None:
7+
article = (
8+
DailyArticle.objects.filter(date__lt=timezone.now()).order_by("-date").first()
9+
)
10+
return article.id if article else None

backend/caviardeul/tests/conftest.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,36 @@ def inner(
4141
return inner
4242

4343

44+
@pytest.fixture()
45+
def mock_wiki_api_redirect(httpx_mock):
46+
def inner(page_id: str, title: str):
47+
httpx_mock.add_response(
48+
url=httpx.URL(
49+
"https://fr.wikipedia.org/w/api.php",
50+
params={
51+
"action": "parse",
52+
"format": "json",
53+
"prop": "text",
54+
"formatversion": 2,
55+
"origin": "*",
56+
"page": page_id,
57+
},
58+
),
59+
json={
60+
"parse": {
61+
"title": title,
62+
"text": (
63+
'<div class="redirectMsg"><p>Rediriger vers :</p>'
64+
'<ul class="redirectText"><li><a href="/wiki/foo" title="Foo">Foo</a></li></ul>'
65+
"</div>"
66+
),
67+
}
68+
},
69+
)
70+
71+
return inner
72+
73+
4474
@pytest.fixture()
4575
def mock_wiki_api_error(httpx_mock):
4676
def inner(page_id: str):

backend/caviardeul/tests/management/commands/test_check_next_article.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def test_check_next_article_has_redirect(mock_wiki_api):
5050
'<ul class="redirectText"><li><a href="/wiki/foo" title="Foo">Foo</a></li></ul></div>',
5151
)
5252

53-
with pytest.raises(CommandError, match="Next daily article has a redirect"):
53+
with pytest.raises(CommandError, match="Error when retrieving daily article"):
5454
call_command("check_next_article")
5555

5656

backend/caviardeul/tests/views/test_daily_article.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
from collections import defaultdict
23
from datetime import datetime
34
from typing import Literal
@@ -110,11 +111,25 @@ def test_get_current_article(
110111
"nbWinners": article.nb_winners,
111112
}
112113

114+
def test_error_with_article(self, client, caplog, mock_wiki_api_redirect):
115+
article = DailyArticleFactory(trait_current=True)
116+
mock_wiki_api_redirect(article.page_id, article.page_name)
117+
118+
with caplog.at_level(logging.ERROR):
119+
res = client.get("/articles/current")
120+
assert "Redirection received in article payload" in caplog.text
121+
122+
assert res.status_code == 500, res.content
123+
data = res.json()
124+
assert data["detail"]
125+
113126
def test_no_article_available(self, client):
114127
_ = DailyArticleFactory(trait_future=True)
115128

116129
res = client.get("/articles/current")
117130
assert res.status_code == 404, res.content
131+
data = res.json()
132+
assert data["detail"]
118133

119134

120135
class TestGetAchivedArticle:

backend/caviardeul/views/daily_article.py

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from datetime import timedelta
22

33
from django.contrib.auth.models import AnonymousUser
4-
from django.db.models import Avg, Count, FilteredRelation, Q
5-
from django.http import Http404, HttpRequest
4+
from django.db.models import Avg, Count, FilteredRelation, Q, QuerySet
5+
from django.http import HttpRequest
66
from django.utils import timezone
77
from ninja import Query
88
from ninja.pagination import paginate
99

10+
from caviardeul.exceptions import ArticleFetchError
1011
from caviardeul.models import DailyArticle, User
1112
from caviardeul.serializers.daily_article import (
1213
DailyArticleListFilter,
@@ -15,12 +16,29 @@
1516
DailyArticleSchema,
1617
DailyArticlesStatsSchema,
1718
)
19+
from caviardeul.serializers.error import ErrorSchema
1820
from caviardeul.services.articles import get_article_content
1921
from caviardeul.services.authentication import OptionalAPIAuthentication
22+
from caviardeul.services.daily_article import get_current_daily_article_id
23+
from caviardeul.services.logging import logger
2024

2125
from .api import api
2226

2327

28+
@api.get(
29+
"/articles/stats",
30+
auth=OptionalAPIAuthentication(),
31+
response=DailyArticlesStatsSchema,
32+
)
33+
def get_daily_article_stats(request: HttpRequest):
34+
return _get_queryset(request.auth).aggregate(
35+
total=Count("id"),
36+
total_finished=Count("id", filter=Q(user_score__isnull=False)),
37+
average_nb_attempts=Avg("user_score__nb_attempts"),
38+
average_nb_correct=Avg("user_score__nb_correct"),
39+
)
40+
41+
2442
def _get_queryset(user: User | AnonymousUser):
2543
queryset = DailyArticle.objects.filter(date__lte=timezone.now())
2644
if not user.is_authenticated:
@@ -35,44 +53,41 @@ def _get_queryset(user: User | AnonymousUser):
3553

3654

3755
@api.get(
38-
"/articles/current", auth=OptionalAPIAuthentication(), response=DailyArticleSchema
39-
)
40-
def get_current_article(request: HttpRequest):
41-
try:
42-
article = _get_queryset(request.auth).order_by("-date")[:1].get()
43-
except DailyArticle.DoesNotExist:
44-
raise Http404("L'article n'a pas été trouvé")
45-
46-
article.content = get_article_content(article)
47-
return article
48-
49-
50-
@api.get(
51-
"/articles/stats",
56+
"/articles/current",
5257
auth=OptionalAPIAuthentication(),
53-
response=DailyArticlesStatsSchema,
58+
response={200: DailyArticleSchema, 404: ErrorSchema, 500: ErrorSchema},
5459
)
55-
def get_daily_article_stats(request: HttpRequest):
56-
return _get_queryset(request.auth).aggregate(
57-
total=Count("id"),
58-
total_finished=Count("id", filter=Q(user_score__isnull=False)),
59-
average_nb_attempts=Avg("user_score__nb_attempts"),
60-
average_nb_correct=Avg("user_score__nb_correct"),
60+
def get_current_article(request: HttpRequest):
61+
article_id = get_current_daily_article_id()
62+
return _get_daily_article_response(
63+
_get_queryset(request.auth).filter(id=article_id)
6164
)
6265

6366

6467
@api.get(
6568
"/articles/{article_id}",
6669
auth=OptionalAPIAuthentication(),
67-
response=DailyArticleSchema,
70+
response={200: DailyArticleSchema, 404: ErrorSchema, 500: ErrorSchema},
6871
)
6972
def get_archived_article(request: HttpRequest, article_id: int):
73+
return _get_daily_article_response(
74+
_get_queryset(request.auth).filter(id=article_id)
75+
)
76+
77+
78+
def _get_daily_article_response(queryset: QuerySet[DailyArticle]):
7079
try:
71-
article = _get_queryset(request.auth).get(id=article_id)
80+
article = queryset.get()
7281
except DailyArticle.DoesNotExist:
73-
raise Http404("L'article n'a pas été trouvé")
82+
return 404, {"detail": "L'article n'a pas été trouvé"}
7483

75-
article.content = get_article_content(article)
84+
try:
85+
article.content = get_article_content(article)
86+
except ArticleFetchError:
87+
logger.exception(
88+
"Error encountered with daily article", extra={"article_id": article.id}
89+
)
90+
return 500, {"detail": "Un problème a été rencontré avec cet article"}
7691
return article
7792

7893

frontend/components/utils/error.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from "react";
2+
import Error from "next/error";
3+
4+
5+
const CustomError: React.FC<{ statusCode?: number, text?: string }> = ({statusCode, text}) => {
6+
return (
7+
<main>
8+
<div className="error">
9+
<Error statusCode={statusCode ?? 500} title={text ?? "Une erreur est survenue"}/>
10+
</div>
11+
</main>
12+
)
13+
}
14+
15+
export default CustomError

frontend/lib/article.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ArticleId, EncodedArticle } from "@caviardeul/types";
22
import { API_URL } from "@caviardeul/utils/config";
3+
import {APIError} from "@caviardeul/lib/queries";
34

45
export const getEncodedArticle = async (
56
articleId?: ArticleId,
@@ -23,7 +24,7 @@ export const getEncodedArticle = async (
2324

2425
const data = await res.json();
2526
if (!res.ok) {
26-
throw data.detail;
27+
throw new APIError(res.status, data.details ?? "")
2728
}
2829

2930
return {

0 commit comments

Comments
 (0)