Skip to content

Commit

Permalink
feat(api) : Add datacube tenders API
Browse files Browse the repository at this point in the history
A very simple and flat API, with a basic but efficient authentication
scheme.

At the time of writing, there are about ~5000 tenders in the database.

We'll see about time filtering later as the request to retrieve the
whole list takes about 1 second.
  • Loading branch information
vperron committed Apr 15, 2024
1 parent 6f4b4bb commit d50c170
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 0 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 @@ -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.UTC)
Empty file.
90 changes: 90 additions & 0 deletions lemarche/api/datacube/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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)
company = CompanyFactory(name="Lagarde et Fils", users=[user])
tender_with_company = TenderFactory(title="Sébastien Le Lopez", author=user)

tender = TenderFactory(title="Marc Henry", presta_type=["FANFAN", "LA", "TULIPE"])

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,
"company_name": "Lagarde et Fils",
"company_slug": "lagarde-et-fils",
"created_at": "2024-06-21T14:23:34+02:00",
"kind": "QUOTE",
"presta_type": [],
"slug": "sebastien-le-lopez",
"source": "FORM",
"status": "SENT",
"title": "Sébastien Le Lopez",
"updated_at": "2024-06-21T14:23:34+02:00",
},
{
"amount": None,
"created_at": "2024-06-21T14:23:34+02:00",
"kind": "QUOTE",
"presta_type": ["FANFAN", "LA", "TULIPE"],
"slug": "marc-henry",
"source": "FORM",
"status": "SENT",
"title": "Marc Henry",
"updated_at": "2024-06-21T14:23:34+02:00",
},
],
},
)
69 changes: 69 additions & 0 deletions lemarche/api/datacube/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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)

class Meta:
model = Tender
fields = [
"created_at",
"updated_at",
"title",
"slug",
"kind",
"presta_type",
"amount",
"status",
"source",
"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)
.prefetch_related("author__company")
.order_by("-created_at")
.all()
)
serializer_class = SimpleTenderSerializer
permission_classes = []
authentication_classes = []

authentication_classes = (
DatacubeApiAuthentication,
authentication.SessionAuthentication,
)
permission_classes = (HasTokenOrIsSuperadmin,)
3 changes: 3 additions & 0 deletions lemarche/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -39,6 +40,8 @@
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

0 comments on commit d50c170

Please sign in to comment.