Skip to content

Commit

Permalink
feat: API pour le service Datacube (#1160)
Browse files Browse the repository at this point in the history
  • Loading branch information
vperron authored May 14, 2024
1 parent f685d73 commit 24868e4
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 16 deletions.
4 changes: 4 additions & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import locale
import os
import datetime

import environ
from django.contrib.messages import constants as messages
Expand Down Expand Up @@ -883,3 +884,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)
3 changes: 3 additions & 0 deletions env.docker_default.local
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,6 @@ ELASTICSEARCH_USERNAME=
ELASTICSEARCH_PASSWORD=
ELASTICSEARCH_INDEX_SIAES=
ELASTICSEARCH_MIN_SCORE=0.9

# DATACUBE API
DATACUBE_API_TOKEN=
Empty file.
28 changes: 28 additions & 0 deletions lemarche/api/datacube/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from rest_framework import serializers

from lemarche.tenders.models import Tender


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",
"amount_exact",
"status",
"source",
"author_email",
"company_name",
"company_slug",
]
95 changes: 95 additions & 0 deletions lemarche/api/datacube/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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", amount="0-42K", author=user, presta_type=["FANFAN", "LA", "TULIPE"])

# no associated company
TenderFactory(title="Marc Henry", amount_exact=697)

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": "0-42K",
"amount_exact": 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,
"amount_exact": 697,
"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",
},
],
},
)
57 changes: 57 additions & 0 deletions lemarche/api/datacube/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from drf_spectacular.utils import extend_schema
from rest_framework import authentication, exceptions, generics, permissions

from lemarche.tenders.models import Tender

from .serializers import SimpleTenderSerializer


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 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,)

@extend_schema(exclude=True)
def get(self, request, *args, **kwargs):
return super().get(self, request, *args, **kwargs)
1 change: 1 addition & 0 deletions lemarche/api/tenders/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"external_link": "",
"constraints": "string",
"amount": "0-1K",
"amount_exact": "693",
# "why_amount_is_blank": "DONT_KNOW",
"accept_share_amount": True,
"accept_cocontracting": True,
Expand Down
2 changes: 2 additions & 0 deletions lemarche/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from rest_framework import routers

from lemarche.api.datacube.views import SimpleTenderList
from lemarche.api.emails.views import InboundParsingEmailView
from lemarche.api.networks.views import NetworkViewSet
from lemarche.api.perimeters.views import PerimeterAutocompleteViewSet, PerimeterKindViewSet, PerimeterViewSet
Expand Down Expand Up @@ -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"),
Expand Down
2 changes: 1 addition & 1 deletion lemarche/utils/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ class SiaeNotMemberRequiredMixin(LoginRequiredUserPassesTestMixin):
def test_func(self):
user = self.request.user
siae_slug = self.kwargs.get("slug")
return user.is_authenticated and not (siae_slug in user.siaes.values_list("slug", flat=True))
return user.is_authenticated and siae_slug not in user.siaes.values_list("slug", flat=True)

def handle_no_permission(self):
messages.add_message(self.request, messages.WARNING, "Vous êtes déjà rattaché à cette structure.")
Expand Down
2 changes: 1 addition & 1 deletion lemarche/www/tenders/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ def clean(self):
self.kind == tender_constants.KIND_TENDER
and self.external_link
and self.cleaned_data.get("response_kind")
and not (tender_constants.RESPONSE_KIND_EXTERNAL in self.cleaned_data.get("response_kind"))
and tender_constants.RESPONSE_KIND_EXTERNAL not in self.cleaned_data.get("response_kind")
):
self.add_error("response_kind", "Appel d'offre avec lien renseigné.")

Expand Down
53 changes: 39 additions & 14 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ coverage = "^7.5.1"
django-debug-toolbar = "^3.8.1"
factory-boy = "^3.3.0"
flake8 = "^6.1.0"
freezegun = "^1.4.0"
ipdb = "^0.13.13"
isort = "^5.12.0"
poethepoet = "^0.12.3"
Expand Down

0 comments on commit 24868e4

Please sign in to comment.