Skip to content

Commit ba136ff

Browse files
committed
🔧(backend) add view to manage footer json
We added the `FRONTEND_URL_JSON_FOOTER` environment variable. It will give the possibility to generate your own footer content in the frontend. If the variable is not set, the footer will not be displayed.
1 parent 96d9d1a commit ba136ff

File tree

10 files changed

+400
-2
lines changed

10 files changed

+400
-2
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ and this project adheres to
88

99
## [Unreleased]
1010

11+
## Added
12+
13+
- 🔧(backend) add view to manage footer json #841
14+
1115
## Changed
1216

1317
- 🚨(frontend) block button when creating doc #749

env.d/development/common.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,4 @@ COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
6464

6565
# Frontend
6666
FRONTEND_THEME=default
67+
FRONTEND_URL_JSON_FOOTER=http://frontend:3000/contents/footer-demo.json

src/backend/core/api/viewsets.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
from django.db.models.expressions import RawSQL
1717
from django.db.models.functions import Left, Length
1818
from django.http import Http404, StreamingHttpResponse
19+
from django.utils.decorators import method_decorator
1920
from django.utils.text import capfirst
2021
from django.utils.translation import gettext_lazy as _
22+
from django.views.decorators.cache import cache_page
2123

2224
import requests
2325
import rest_framework as drf
@@ -30,6 +32,7 @@
3032
from core import authentication, enums, models
3133
from core.services.ai_services import AIService
3234
from core.services.collaboration_services import CollaborationService
35+
from core.services.config_services import get_footer_json
3336
from core.utils import extract_attachments, filter_descendants
3437

3538
from . import permissions, serializers, utils
@@ -1688,8 +1691,8 @@ def get(self, request):
16881691
"COLLABORATION_WS_URL",
16891692
"CRISP_WEBSITE_ID",
16901693
"ENVIRONMENT",
1691-
"FRONTEND_THEME",
16921694
"FRONTEND_CSS_URL",
1695+
"FRONTEND_THEME",
16931696
"MEDIA_BASE_URL",
16941697
"POSTHOG_KEY",
16951698
"LANGUAGES",
@@ -1702,3 +1705,22 @@ def get(self, request):
17021705
dict_settings[setting] = getattr(settings, setting)
17031706

17041707
return drf.response.Response(dict_settings)
1708+
1709+
1710+
class FooterView(drf.views.APIView):
1711+
"""API ViewSet for sharing the footer JSON."""
1712+
1713+
permission_classes = [AllowAny]
1714+
1715+
@method_decorator(cache_page(settings.FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT))
1716+
def get(self, request):
1717+
"""
1718+
GET /api/v1.0/footer/
1719+
Return the footer JSON.
1720+
"""
1721+
json_footer = (
1722+
get_footer_json(settings.FRONTEND_URL_JSON_FOOTER)
1723+
if settings.FRONTEND_URL_JSON_FOOTER
1724+
else {}
1725+
)
1726+
return drf.response.Response(json_footer)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Config services."""
2+
3+
import logging
4+
5+
import requests
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
def get_footer_json(footer_json_url: str) -> dict:
11+
"""
12+
Fetches the footer JSON from the given URL."
13+
"""
14+
try:
15+
response = requests.get(
16+
footer_json_url, timeout=5, headers={"User-Agent": "Docs-Application"}
17+
)
18+
response.raise_for_status()
19+
20+
footer_json = response.json()
21+
22+
return footer_json
23+
except (requests.RequestException, ValueError) as e:
24+
logger.error("Failed to fetch footer JSON: %s", e)
25+
return {}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Test the footer API."""
2+
3+
import responses
4+
from rest_framework.test import APIClient
5+
6+
7+
def test_api_footer_without_settings_configured(settings):
8+
"""Test the footer API without settings configured."""
9+
settings.FRONTEND_URL_JSON_FOOTER = None
10+
client = APIClient()
11+
response = client.get("/api/v1.0/footer/")
12+
assert response.status_code == 200
13+
assert response.json() == {}
14+
15+
16+
@responses.activate
17+
def test_api_footer_with_invalid_request(settings):
18+
"""Test the footer API with an invalid request."""
19+
settings.FRONTEND_URL_JSON_FOOTER = "https://invalid-request.com"
20+
21+
footer_response = responses.get(settings.FRONTEND_URL_JSON_FOOTER, status=404)
22+
23+
client = APIClient()
24+
response = client.get("/api/v1.0/footer/")
25+
assert response.status_code == 200
26+
assert response.json() == {}
27+
assert footer_response.call_count == 1
28+
29+
30+
@responses.activate
31+
def test_api_footer_with_invalid_json(settings):
32+
"""Test the footer API with an invalid JSON response."""
33+
settings.FRONTEND_URL_JSON_FOOTER = "https://valid-request.com"
34+
35+
footer_response = responses.get(
36+
settings.FRONTEND_URL_JSON_FOOTER, status=200, body="invalid json"
37+
)
38+
39+
client = APIClient()
40+
response = client.get("/api/v1.0/footer/")
41+
assert response.status_code == 200
42+
assert response.json() == {}
43+
assert footer_response.call_count == 1
44+
45+
46+
@responses.activate
47+
def test_api_footer_with_valid_json(settings):
48+
"""Test the footer API with an invalid JSON response."""
49+
settings.FRONTEND_URL_JSON_FOOTER = "https://valid-request.com"
50+
51+
footer_response = responses.get(
52+
settings.FRONTEND_URL_JSON_FOOTER, status=200, json={"foo": "bar"}
53+
)
54+
55+
client = APIClient()
56+
response = client.get("/api/v1.0/footer/")
57+
assert response.status_code == 200
58+
assert response.json() == {"foo": "bar"}
59+
assert footer_response.call_count == 1
60+
61+
62+
@responses.activate
63+
def test_api_footer_with_valid_json_and_cache(settings):
64+
"""Test the footer API with an invalid JSON response."""
65+
settings.FRONTEND_URL_JSON_FOOTER = "https://valid-request.com"
66+
67+
footer_response = responses.get(
68+
settings.FRONTEND_URL_JSON_FOOTER, status=200, json={"foo": "bar"}
69+
)
70+
71+
client = APIClient()
72+
response = client.get("/api/v1.0/footer/")
73+
assert response.status_code == 200
74+
assert response.json() == {"foo": "bar"}
75+
assert footer_response.call_count == 1
76+
77+
response = client.get("/api/v1.0/footer/")
78+
assert response.status_code == 200
79+
assert response.json() == {"foo": "bar"}
80+
# The cache should have been used
81+
assert footer_response.call_count == 1

src/backend/core/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,5 @@
5656
),
5757
),
5858
path(f"api/{settings.API_VERSION}/config/", viewsets.ConfigView.as_view()),
59+
path(f"api/{settings.API_VERSION}/footer/", viewsets.FooterView.as_view()),
5960
]

src/backend/impress/settings.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,14 @@ class Base(Configuration):
410410
FRONTEND_THEME = values.Value(
411411
None, environ_name="FRONTEND_THEME", environ_prefix=None
412412
)
413-
413+
FRONTEND_URL_JSON_FOOTER = values.Value(
414+
None, environ_name="FRONTEND_URL_JSON_FOOTER", environ_prefix=None
415+
)
416+
FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT = values.Value(
417+
60 * 60 * 24,
418+
environ_name="FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT",
419+
environ_prefix=None,
420+
)
414421
FRONTEND_CSS_URL = values.Value(
415422
None, environ_name="FRONTEND_CSS_URL", environ_prefix=None
416423
)
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
{
2+
"default": {
3+
"externalLinks": [
4+
{
5+
"label": "Github",
6+
"href": "https://github.com/suitenumerique/docs/"
7+
},
8+
{
9+
"label": "DINUM",
10+
"href": "https://www.numerique.gouv.fr/dinum/"
11+
},
12+
{
13+
"label": "ZenDiS",
14+
"href": "https://zendis.de/"
15+
},
16+
{
17+
"label": "BlockNote.js",
18+
"href": "https://www.blocknotejs.org/"
19+
}
20+
],
21+
"bottomInformation": {
22+
"label": "Unless otherwise stated, all content on this site is under",
23+
"link": {
24+
"label": "licence etalab-2.0",
25+
"href": "https://github.com/etalab/licence-ouverte/blob/master/LO.md"
26+
}
27+
}
28+
},
29+
"en": {
30+
"legalLinks": [
31+
{
32+
"label": "Legal Notice",
33+
"href": "#"
34+
},
35+
{
36+
"label": "Personal data and cookies",
37+
"href": "#"
38+
},
39+
{
40+
"label": "Accessibility",
41+
"href": "#"
42+
}
43+
],
44+
"bottomInformation": {
45+
"label": "Unless otherwise stated, all content on this site is under",
46+
"link": {
47+
"label": "licence MIT",
48+
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
49+
}
50+
}
51+
},
52+
"fr": {
53+
"legalLinks": [
54+
{
55+
"label": "Mentions légales",
56+
"href": "#"
57+
},
58+
{
59+
"label": "Données personnelles et cookies",
60+
"href": "#"
61+
},
62+
{
63+
"label": "Accessibilité",
64+
"href": "#"
65+
}
66+
],
67+
"bottomInformation": {
68+
"label": "Sauf mention contraire, tout le contenu de ce site est sous",
69+
"link": {
70+
"label": "licence MIT",
71+
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
72+
}
73+
}
74+
},
75+
"de": {
76+
"legalLinks": [
77+
{
78+
"label": "Impressum",
79+
"href": "#"
80+
},
81+
{
82+
"label": "Personenbezogene Daten und Cookies",
83+
"href": "#"
84+
},
85+
{
86+
"label": "Barrierefreiheit",
87+
"href": "#"
88+
}
89+
],
90+
"bottomInformation": {
91+
"label": "Sofern nicht anders angegeben, steht der gesamte Inhalt dieser Website unter",
92+
"link": {
93+
"label": "licence MIT",
94+
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
95+
}
96+
}
97+
},
98+
"nl": {
99+
"legalLinks": [
100+
{
101+
"label": "Wettelijke bepalingen",
102+
"href": "#"
103+
},
104+
{
105+
"label": "Persoonlijke gegevens en cookies",
106+
"href": "#"
107+
},
108+
{
109+
"label": "Toegankelijkheid",
110+
"href": "#"
111+
}
112+
],
113+
"bottomInformation": {
114+
"label": "Tenzij anders vermeld, is alle inhoud van deze site ondergebracht onder",
115+
"link": {
116+
"label": "licence MIT",
117+
"href": "https://github.com/suitenumerique/docs/blob/main/LICENSE"
118+
}
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)