diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d928e5c..1e1cfb65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to ## [Unreleased] +## Added + +- 🔧(backend) add view to manage footer json #841 + ## Changed - 🚨(frontend) block button when creating doc #749 diff --git a/env.d/development/common.dist b/env.d/development/common.dist index 4b1389bf..fdd370e5 100644 --- a/env.d/development/common.dist +++ b/env.d/development/common.dist @@ -64,3 +64,4 @@ COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/ # Frontend FRONTEND_THEME=default +FRONTEND_URL_JSON_FOOTER=http://frontend:3000/contents/footer-demo.json diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index c416c224..b2f94972 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -16,8 +16,10 @@ from django.db import transaction from django.db.models.expressions import RawSQL from django.db.models.functions import Left, Length from django.http import Http404, StreamingHttpResponse +from django.utils.decorators import method_decorator from django.utils.text import capfirst from django.utils.translation import gettext_lazy as _ +from django.views.decorators.cache import cache_page import requests import rest_framework as drf @@ -30,6 +32,7 @@ from rest_framework.throttling import UserRateThrottle from core import authentication, enums, models from core.services.ai_services import AIService from core.services.collaboration_services import CollaborationService +from core.services.config_services import get_footer_json from core.utils import extract_attachments, filter_descendants from . import permissions, serializers, utils @@ -1688,8 +1691,8 @@ class ConfigView(drf.views.APIView): "COLLABORATION_WS_URL", "CRISP_WEBSITE_ID", "ENVIRONMENT", - "FRONTEND_THEME", "FRONTEND_CSS_URL", + "FRONTEND_THEME", "MEDIA_BASE_URL", "POSTHOG_KEY", "LANGUAGES", @@ -1702,3 +1705,22 @@ class ConfigView(drf.views.APIView): dict_settings[setting] = getattr(settings, setting) return drf.response.Response(dict_settings) + + +class FooterView(drf.views.APIView): + """API ViewSet for sharing the footer JSON.""" + + permission_classes = [AllowAny] + + @method_decorator(cache_page(settings.FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT)) + def get(self, request): + """ + GET /api/v1.0/footer/ + Return the footer JSON. + """ + json_footer = ( + get_footer_json(settings.FRONTEND_URL_JSON_FOOTER) + if settings.FRONTEND_URL_JSON_FOOTER + else {} + ) + return drf.response.Response(json_footer) diff --git a/src/backend/core/services/config_services.py b/src/backend/core/services/config_services.py new file mode 100644 index 00000000..f4efa946 --- /dev/null +++ b/src/backend/core/services/config_services.py @@ -0,0 +1,25 @@ +"""Config services.""" + +import logging + +import requests + +logger = logging.getLogger(__name__) + + +def get_footer_json(footer_json_url: str) -> dict: + """ + Fetches the footer JSON from the given URL." + """ + try: + response = requests.get( + footer_json_url, timeout=5, headers={"User-Agent": "Docs-Application"} + ) + response.raise_for_status() + + footer_json = response.json() + + return footer_json + except (requests.RequestException, ValueError) as e: + logger.error("Failed to fetch footer JSON: %s", e) + return {} diff --git a/src/backend/core/tests/test_api_footer.py b/src/backend/core/tests/test_api_footer.py new file mode 100644 index 00000000..8dd892cd --- /dev/null +++ b/src/backend/core/tests/test_api_footer.py @@ -0,0 +1,81 @@ +"""Test the footer API.""" + +import responses +from rest_framework.test import APIClient + + +def test_api_footer_without_settings_configured(settings): + """Test the footer API without settings configured.""" + settings.FRONTEND_URL_JSON_FOOTER = None + client = APIClient() + response = client.get("/api/v1.0/footer/") + assert response.status_code == 200 + assert response.json() == {} + + +@responses.activate +def test_api_footer_with_invalid_request(settings): + """Test the footer API with an invalid request.""" + settings.FRONTEND_URL_JSON_FOOTER = "https://invalid-request.com" + + footer_response = responses.get(settings.FRONTEND_URL_JSON_FOOTER, status=404) + + client = APIClient() + response = client.get("/api/v1.0/footer/") + assert response.status_code == 200 + assert response.json() == {} + assert footer_response.call_count == 1 + + +@responses.activate +def test_api_footer_with_invalid_json(settings): + """Test the footer API with an invalid JSON response.""" + settings.FRONTEND_URL_JSON_FOOTER = "https://valid-request.com" + + footer_response = responses.get( + settings.FRONTEND_URL_JSON_FOOTER, status=200, body="invalid json" + ) + + client = APIClient() + response = client.get("/api/v1.0/footer/") + assert response.status_code == 200 + assert response.json() == {} + assert footer_response.call_count == 1 + + +@responses.activate +def test_api_footer_with_valid_json(settings): + """Test the footer API with an invalid JSON response.""" + settings.FRONTEND_URL_JSON_FOOTER = "https://valid-request.com" + + footer_response = responses.get( + settings.FRONTEND_URL_JSON_FOOTER, status=200, json={"foo": "bar"} + ) + + client = APIClient() + response = client.get("/api/v1.0/footer/") + assert response.status_code == 200 + assert response.json() == {"foo": "bar"} + assert footer_response.call_count == 1 + + +@responses.activate +def test_api_footer_with_valid_json_and_cache(settings): + """Test the footer API with an invalid JSON response.""" + settings.FRONTEND_URL_JSON_FOOTER = "https://valid-request.com" + + footer_response = responses.get( + settings.FRONTEND_URL_JSON_FOOTER, status=200, json={"foo": "bar"} + ) + + client = APIClient() + response = client.get("/api/v1.0/footer/") + assert response.status_code == 200 + assert response.json() == {"foo": "bar"} + assert footer_response.call_count == 1 + + response = client.get("/api/v1.0/footer/") + assert response.status_code == 200 + assert response.json() == {"foo": "bar"} + # The cache should have been used + assert footer_response.call_count == 1 diff --git a/src/backend/core/urls.py b/src/backend/core/urls.py index ce89f483..fec20662 100644 --- a/src/backend/core/urls.py +++ b/src/backend/core/urls.py @@ -56,4 +56,5 @@ urlpatterns = [ ), ), path(f"api/{settings.API_VERSION}/config/", viewsets.ConfigView.as_view()), + path(f"api/{settings.API_VERSION}/footer/", viewsets.FooterView.as_view()), ] diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py index 2756ee4d..40b89ac3 100755 --- a/src/backend/impress/settings.py +++ b/src/backend/impress/settings.py @@ -410,7 +410,14 @@ class Base(Configuration): FRONTEND_THEME = values.Value( None, environ_name="FRONTEND_THEME", environ_prefix=None ) - + FRONTEND_URL_JSON_FOOTER = values.Value( + None, environ_name="FRONTEND_URL_JSON_FOOTER", environ_prefix=None + ) + FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT = values.Value( + 60 * 60 * 24, + environ_name="FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT", + environ_prefix=None, + ) FRONTEND_CSS_URL = values.Value( None, environ_name="FRONTEND_CSS_URL", environ_prefix=None ) diff --git a/src/frontend/apps/impress/public/contents/footer-demo.json b/src/frontend/apps/impress/public/contents/footer-demo.json new file mode 100644 index 00000000..77d9ed9f --- /dev/null +++ b/src/frontend/apps/impress/public/contents/footer-demo.json @@ -0,0 +1,121 @@ +{ + "default": { + "externalLinks": [ + { + "label": "Github", + "href": "https://github.com/suitenumerique/docs/" + }, + { + "label": "DINUM", + "href": "https://www.numerique.gouv.fr/dinum/" + }, + { + "label": "ZenDiS", + "href": "https://zendis.de/" + }, + { + "label": "BlockNote.js", + "href": "https://www.blocknotejs.org/" + } + ], + "bottomInformation": { + "label": "Unless otherwise stated, all content on this site is under", + "link": { + "label": "licence etalab-2.0", + "href": "https://github.com/etalab/licence-ouverte/blob/master/LO.md" + } + } + }, + "en": { + "legalLinks": [ + { + "label": "Legal Notice", + "href": "#" + }, + { + "label": "Personal data and cookies", + "href": "#" + }, + { + "label": "Accessibility", + "href": "#" + } + ], + "bottomInformation": { + "label": "Unless otherwise stated, all content on this site is under", + "link": { + "label": "licence MIT", + "href": "https://github.com/suitenumerique/docs/blob/main/LICENSE" + } + } + }, + "fr": { + "legalLinks": [ + { + "label": "Mentions légales", + "href": "#" + }, + { + "label": "Données personnelles et cookies", + "href": "#" + }, + { + "label": "Accessibilité", + "href": "#" + } + ], + "bottomInformation": { + "label": "Sauf mention contraire, tout le contenu de ce site est sous", + "link": { + "label": "licence MIT", + "href": "https://github.com/suitenumerique/docs/blob/main/LICENSE" + } + } + }, + "de": { + "legalLinks": [ + { + "label": "Impressum", + "href": "#" + }, + { + "label": "Personenbezogene Daten und Cookies", + "href": "#" + }, + { + "label": "Barrierefreiheit", + "href": "#" + } + ], + "bottomInformation": { + "label": "Sofern nicht anders angegeben, steht der gesamte Inhalt dieser Website unter", + "link": { + "label": "licence MIT", + "href": "https://github.com/suitenumerique/docs/blob/main/LICENSE" + } + } + }, + "nl": { + "legalLinks": [ + { + "label": "Wettelijke bepalingen", + "href": "#" + }, + { + "label": "Persoonlijke gegevens en cookies", + "href": "#" + }, + { + "label": "Toegankelijkheid", + "href": "#" + } + ], + "bottomInformation": { + "label": "Tenzij anders vermeld, is alle inhoud van deze site ondergebracht onder", + "link": { + "label": "licence MIT", + "href": "https://github.com/suitenumerique/docs/blob/main/LICENSE" + } + } + } +} diff --git a/src/frontend/apps/impress/public/contents/footer-dsfr.json b/src/frontend/apps/impress/public/contents/footer-dsfr.json new file mode 100644 index 00000000..fd6d8a8a --- /dev/null +++ b/src/frontend/apps/impress/public/contents/footer-dsfr.json @@ -0,0 +1,135 @@ +{ + "default": { + "externalLinks": [ + { + "label": "legifrance.gouv.fr", + "href": "https://legifrance.gouv.fr/" + }, + { + "label": "info.gouv.fr", + "href": "https://info.gouv.fr/" + }, + { + "label": "service-public.fr", + "href": "https://service-public.fr/" + }, + { + "label": "data.gouv.fr", + "href": "https://data.gouv.fr/" + } + ], + "legalLinks": [ + { + "label": "Legal Notice", + "href": "https://docs.numerique.gouv.fr/docs/4db744e3-5b47-4ed9-b9e7-d4312318bbce/" + }, + { + "label": "Personal data and cookies", + "href": "https://docs.numerique.gouv.fr/docs/eb863389-a5e5-4d18-879d-149a1122380e/" + }, + { + "label": "Accessibility", + "href": "https://docs.numerique.gouv.fr/docs/9694e570-1427-4ef7-b0a0-c3e894360e1b/" + } + ], + "bottomInformation": { + "label": "Unless otherwise stated, all content on this site is under", + "link": { + "label": "licence etalab-2.0", + "href": "https://github.com/etalab/licence-ouverte/blob/master/LO.md" + } + } + }, + "en": { + "legalLinks": [ + { + "label": "Legal Notice", + "href": "https://docs.numerique.gouv.fr/docs/4db744e3-5b47-4ed9-b9e7-d4312318bbce/" + }, + { + "label": "Personal data and cookies", + "href": "https://docs.numerique.gouv.fr/docs/eb863389-a5e5-4d18-879d-149a1122380e/" + }, + { + "label": "Accessibility", + "href": "https://docs.numerique.gouv.fr/docs/9694e570-1427-4ef7-b0a0-c3e894360e1b/" + } + ], + "bottomInformation": { + "label": "Unless otherwise stated, all content on this site is under", + "link": { + "label": "licence etalab-2.0", + "href": "https://github.com/etalab/licence-ouverte/blob/master/LO.md" + } + } + }, + "fr": { + "legalLinks": [ + { + "label": "Mentions légales", + "href": "https://docs.numerique.gouv.fr/docs/4db744e3-5b47-4ed9-b9e7-d4312318bbce/" + }, + { + "label": "Données personnelles et cookies", + "href": "https://docs.numerique.gouv.fr/docs/eb863389-a5e5-4d18-879d-149a1122380e/" + }, + { + "label": "Accessibilité", + "href": "https://docs.numerique.gouv.fr/docs/9694e570-1427-4ef7-b0a0-c3e894360e1b/" + } + ], + "bottomInformation": { + "label": "Sauf mention contraire, tout le contenu de ce site est sous", + "link": { + "label": "licence etalab-2.0", + "href": "https://github.com/etalab/licence-ouverte/blob/master/LO.md" + } + } + }, + "de": { + "legalLinks": [ + { + "label": "Impressum", + "href": "https://docs.numerique.gouv.fr/docs/4db744e3-5b47-4ed9-b9e7-d4312318bbce/" + }, + { + "label": "Personenbezogene Daten und Cookies", + "href": "https://docs.numerique.gouv.fr/docs/eb863389-a5e5-4d18-879d-149a1122380e/" + }, + { + "label": "Barrierefreiheit", + "href": "https://docs.numerique.gouv.fr/docs/9694e570-1427-4ef7-b0a0-c3e894360e1b/" + } + ], + "bottomInformation": { + "label": "Sofern nicht anders angegeben, steht der gesamte Inhalt dieser Website unter", + "link": { + "label": "licence etalab-2.0", + "href": "https://github.com/etalab/licence-ouverte/blob/master/LO.md" + } + } + }, + "nl": { + "legalLinks": [ + { + "label": "Wettelijke bepalingen", + "href": "https://docs.numerique.gouv.fr/docs/4db744e3-5b47-4ed9-b9e7-d4312318bbce/" + }, + { + "label": "Persoonlijke gegevens en cookies", + "href": "https://docs.numerique.gouv.fr/docs/eb863389-a5e5-4d18-879d-149a1122380e/" + }, + { + "label": "Toegankelijkheid", + "href": "https://docs.numerique.gouv.fr/docs/9694e570-1427-4ef7-b0a0-c3e894360e1b/" + } + ], + "bottomInformation": { + "label": "Tenzij anders vermeld, is alle inhoud van deze site ondergebracht onder", + "link": { + "label": "licence etalab-2.0", + "href": "https://github.com/etalab/licence-ouverte/blob/master/LO.md" + } + } + } +} diff --git a/src/helm/env.d/dev/values.impress.yaml.gotmpl b/src/helm/env.d/dev/values.impress.yaml.gotmpl index a06e3971..79ae5787 100644 --- a/src/helm/env.d/dev/values.impress.yaml.gotmpl +++ b/src/helm/env.d/dev/values.impress.yaml.gotmpl @@ -50,6 +50,7 @@ backend: DB_USER: dinum DB_PASSWORD: pass DB_PORT: 5432 + FRONTEND_URL_JSON_FOOTER: https://impress.127.0.0.1.nip.io/contents/footer-demo.json POSTGRES_DB: impress POSTGRES_USER: dinum POSTGRES_PASSWORD: pass