diff --git a/config/settings/base.py b/config/settings/base.py index 71913e33d..79e049175 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -12,6 +12,7 @@ import locale import os +import datetime import environ from django.contrib.messages import constants as messages @@ -901,3 +902,6 @@ ELASTICSEARCH_PASSWORD = env.str("ELASTICSEARCH_PASSWORD", "") ELASTICSEARCH_INDEX_SIAES = env.str("ELASTICSEARCH_INDEX_SIAES", "") ELASTICSEARCH_MIN_SCORE = env.float("ELASTICSEARCH_MIN_SCORE", 0.9) + +DATACUBE_API_TOKEN = env.str("DATACUBE_API_TOKEN", "") +DATACUBE_API_TENDER_START_DATE = datetime.datetime(2023, 1, 1, tzinfo=datetime.timezone.utc) diff --git a/lemarche/api/datacube/__init__.py b/lemarche/api/datacube/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lemarche/api/datacube/tests.py b/lemarche/api/datacube/tests.py new file mode 100644 index 000000000..c2d9f3b79 --- /dev/null +++ b/lemarche/api/datacube/tests.py @@ -0,0 +1,93 @@ +import freezegun +from django.test import TestCase, override_settings +from django.urls import reverse + +from lemarche.companies.factories import CompanyFactory +from lemarche.tenders.factories import TenderFactory +from lemarche.users.factories import UserFactory +from lemarche.users.models import User + + +class DatacubeApiTest(TestCase): + maxDiff = None + + @override_settings(DATACUBE_API_TOKEN="bar") + def test_list_tenders_authentication(self): + url = reverse("api:datacube-tenders") + response = self.client.get(url) + self.assertEqual(response.status_code, 401) + + # an appropriate token from the settings is required + response = self.client.get(url, headers={"Authorization": "Token "}) + self.assertEqual(response.status_code, 401) + + response = self.client.get(url, headers={"Authorization": "Token foo"}) + self.assertEqual(response.status_code, 401) + + response = self.client.get(url, headers={"Authorization": "Token bar"}) + self.assertEqual(response.status_code, 200) + + # or alternatively, if you're logged in as superuser + admin = UserFactory(kind="ADMIN") + self.client.force_login(admin) + response = self.client.get(url) + self.assertEqual(response.status_code, 403) + + admin.is_superuser = True + admin.save(update_fields=["is_superuser"]) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + @freezegun.freeze_time("2024-06-21 12:23:34") + @override_settings(DATACUBE_API_TOKEN="bar") + def test_list_tenders_content(self): + url = reverse("api:datacube-tenders") + response = self.client.get(url, headers={"Authorization": "Token bar"}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"count": 0, "next": None, "previous": None, "results": []}) + + user = UserFactory(kind=User.KIND_BUYER) + CompanyFactory(name="Lagarde et Fils", users=[user]) + TenderFactory(title="Sébastien Le Lopez", author=user, presta_type=["FANFAN", "LA", "TULIPE"]) + + # no associated company + TenderFactory(title="Marc Henry") + + response = self.client.get(url, headers={"Authorization": "Token bar"}) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + { + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "amount": None, + "author_email": "email1@example.com", + "company_name": "Lagarde et Fils", + "company_slug": "lagarde-et-fils", + "created_at": "2024-06-21T14:23:34+02:00", + "kind": "QUOTE", + "presta_type": ["FANFAN", "LA", "TULIPE"], + "slug": "sebastien-le-lopez", + "source": "FORM", + "status": "SENT", + "title": "Sébastien Le Lopez", + "updated_at": "2024-06-21T14:23:34+02:00", + }, + { + "amount": None, + "author_email": "email2@example.com", + "created_at": "2024-06-21T14:23:34+02:00", + "kind": "QUOTE", + "presta_type": [], + "slug": "marc-henry", + "source": "FORM", + "status": "SENT", + "title": "Marc Henry", + "updated_at": "2024-06-21T14:23:34+02:00", + }, + ], + }, + ) diff --git a/lemarche/api/datacube/views.py b/lemarche/api/datacube/views.py new file mode 100644 index 000000000..600752bd7 --- /dev/null +++ b/lemarche/api/datacube/views.py @@ -0,0 +1,74 @@ +from django.conf import settings +from django.contrib.auth.models import AnonymousUser +from rest_framework import authentication, exceptions, generics, permissions, serializers + +from lemarche.tenders.models import Tender + + +class DatacubeApiAnonymousUser(AnonymousUser): + pass + + +class DatacubeApiAuthentication(authentication.TokenAuthentication): + def authenticate_credentials(self, key): + configured_token = settings.DATACUBE_API_TOKEN + if configured_token and key == configured_token: + return (DatacubeApiAnonymousUser(), key) + raise exceptions.AuthenticationFailed("Invalid token.") + + +class HasTokenOrIsSuperadmin(permissions.BasePermission): + def has_permission(self, request, view): + if isinstance(request.user, DatacubeApiAnonymousUser): + return True + return request.user.is_superuser + + +class SimpleTenderSerializer(serializers.ModelSerializer): + slug = serializers.CharField(read_only=True) + company_name = serializers.CharField(source="author.company.name", read_only=True) + company_slug = serializers.CharField(source="author.company.slug", read_only=True) + author_email = serializers.CharField(source="author.email", read_only=True) + + class Meta: + model = Tender + fields = [ + "created_at", + "updated_at", + "title", + "slug", + "kind", + "presta_type", + "amount", + "status", + "source", + "author_email", + "company_name", + "company_slug", + ] + + +class SimpleTenderList(generics.ListAPIView): + """Simplified list of tenders along with their listed companies. + + curl -H "Authorization: Token xxxxx" http://marche.fqdn/api/datacube-tenders/ + """ + + queryset = ( + Tender.objects.filter( + created_at__gte=settings.DATACUBE_API_TENDER_START_DATE, + ) + .exclude(author__isnull=True) + .prefetch_related("author", "author__company") + .order_by("-created_at") + .all() + ) + serializer_class = SimpleTenderSerializer + permission_classes = [] + authentication_classes = [] + + authentication_classes = ( + DatacubeApiAuthentication, + authentication.SessionAuthentication, + ) + permission_classes = (HasTokenOrIsSuperadmin,) diff --git a/lemarche/api/urls.py b/lemarche/api/urls.py index 9a891cde8..f1966ca1d 100644 --- a/lemarche/api/urls.py +++ b/lemarche/api/urls.py @@ -9,6 +9,7 @@ from lemarche.api.sectors.views import SectorViewSet from lemarche.api.siaes.views import SiaeKindViewSet, SiaePrestaTypeViewSet, SiaeViewSet from lemarche.api.tenders.views import TenderAmountViewSet, TenderKindViewSet, TenderViewSet +from lemarche.api.datacube.views import SimpleTenderList # https://docs.djangoproject.com/en/dev/topics/http/urls/#url-namespaces-and-included-urlconfs @@ -39,6 +40,7 @@ name="old_api_siae_siret", ), path("inbound-email-parsing/", InboundParsingEmailView.as_view(), name="inbound-email-parsing"), + path("datacube/tenders/", SimpleTenderList.as_view(), name="datacube-tenders"), # Swagger / OpenAPI documentation path("schema/", SpectacularAPIView.as_view(), name="schema"), path("docs/", SpectacularSwaggerView.as_view(url_name="api:schema"), name="swagger-ui"),