From 7cb3a4e16ec2ab00929dec76ca8f05b32c049c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Mon, 22 Jul 2024 22:06:57 +0200 Subject: [PATCH] feat/PRWLR-3996 implement /tenants endpoint and basic filters logic (#14) * chore: PRWLR-3996 add drf-jsonapi library to project * feat: PRWLR-3996 implement /tenants endpoint and basic JSON:API spec * feat: PRWLR-3996 implement new middleware to ingest metadata on API responses * chore: PRWLR-3996 annotate views with drf-spectacular * feat: PRWLR-3996 add cache-control to tenants endpoints * feat: PRWLR-3996 update views filters * chore: PRWLR-3996 remove meta middleware in favour of meta fields in serializers * test(API): PRWLR-3996 add unit tests for /tenants * chore: PRWLR-3996 turn DEBUG on on devel environment * test: PRWLR-3996 rework tenants unit tests * chore: PRWLR-3996 refactor filter classes * fix(Backend,API): PRWLR-3996 use correct filter backend --- .env.example | 2 + poetry.lock | 43 ++++- pyproject.toml | 2 + src/backend/api/filters.py | 31 +++ src/backend/api/migrations/0001_initial.py | 16 +- .../api/migrations/0002_tenantmodel.py | 33 ---- src/backend/api/models.py | 12 +- src/backend/api/serializers.py | 18 +- src/backend/api/tests.py | 3 - src/backend/api/tests/test_views.py | 179 ++++++++++++++++++ src/backend/api/urls/v1/urls.py | 22 +-- src/backend/api/views/base_views.py | 23 +++ src/backend/api/views/v1/views.py | 70 ++++++- src/backend/config/django/base.py | 32 +++- src/backend/config/django/devel.py | 12 ++ 15 files changed, 407 insertions(+), 91 deletions(-) create mode 100644 src/backend/api/filters.py delete mode 100644 src/backend/api/migrations/0002_tenantmodel.py delete mode 100644 src/backend/api/tests.py create mode 100644 src/backend/api/tests/test_views.py create mode 100644 src/backend/api/views/base_views.py diff --git a/.env.example b/.env.example index 7cda3e66d1..df80378dc9 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,8 @@ DJANGO_DEBUG=False DJANGO_SETTINGS_MODULE=config.django.[production|devel] DJANGO_LOGGING_FORMATTER=[ndjson|human_readable] DJANGO_WORKERS=4 # Defaults to the maximum available based on CPU cores if not set. +DJANGO_CACHE_MAX_AGE=3600 +DJANGO_STALE_WHILE_REVALIDATE=60 # PostgreSQL settings POSTGRES_HOST=postgres-db diff --git a/poetry.lock b/poetry.lock index bfd3e412c4..be3a6b6bad 100644 --- a/poetry.lock +++ b/poetry.lock @@ -476,6 +476,20 @@ develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "py docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] +[[package]] +name = "django-filter" +version = "24.2" +description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-filter-24.2.tar.gz", hash = "sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e"}, + {file = "django_filter-24.2-py3-none-any.whl", hash = "sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48"}, +] + +[package.dependencies] +Django = ">=4.2" + [[package]] name = "django-guid" version = "3.5.0" @@ -504,6 +518,27 @@ files = [ [package.dependencies] django = ">=4.2" +[[package]] +name = "djangorestframework-jsonapi" +version = "7.0.2" +description = "A Django REST framework API adapter for the JSON:API spec." +optional = false +python-versions = ">=3.8" +files = [ + {file = "djangorestframework-jsonapi-7.0.2.tar.gz", hash = "sha256:d6c72a2bee539f1093dd86620e862af2d1a0e60408e38a710146286dbde71d75"}, + {file = "djangorestframework_jsonapi-7.0.2-py2.py3-none-any.whl", hash = "sha256:be457adb50aac77eec8893048bf46ad6926dcd26204aa10965a1430610828d50"}, +] + +[package.dependencies] +django = ">=4.2" +djangorestframework = ">=3.14" +inflection = ">=0.5.0" + +[package.extras] +django-filter = ["django-filter (>=2.4)"] +django-polymorphic = ["django-polymorphic (>=3.0)"] +openapi = ["pyyaml (>=5.4)", "uritemplate (>=3.0.1)"] + [[package]] name = "docker" version = "7.1.0" @@ -1774,13 +1809,13 @@ files = [ [[package]] name = "sqlparse" -version = "0.5.0" +version = "0.5.1" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" files = [ - {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"}, - {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"}, + {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, + {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, ] [package.extras] @@ -1893,4 +1928,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "76aaacac73e4a835736c1c6550809e09481e405974e8ad89699cbcb52bcbe0e0" +content-hash = "17cd0c83d5aa272809743691d6b91ae3e7c3c98df8c7ce389aabc32f996b69d0" diff --git a/pyproject.toml b/pyproject.toml index 58143e0b42..664488cd33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,8 +14,10 @@ version = "1.0.0" django = "5.0.7" django-cors-headers = "4.4.0" django-environ = "0.11.2" +django-filter = "24.2" django-guid = "3.5.0" djangorestframework = "3.15.2" +djangorestframework-jsonapi = "7.0.2" drf-spectacular = "0.27.2" gunicorn = "22.0.0" psycopg2-binary = "2.9.9" diff --git a/src/backend/api/filters.py b/src/backend/api/filters.py new file mode 100644 index 0000000000..7b0b232569 --- /dev/null +++ b/src/backend/api/filters.py @@ -0,0 +1,31 @@ +from django_filters.rest_framework import FilterSet +from rest_framework_json_api.django_filters.backends import DjangoFilterBackend + +from api.models import Tenant + + +class CustomDjangoFilterBackend(DjangoFilterBackend): + def to_html(self, _request, _queryset, _view): + """Override this method to use the Browsable API in dev environments. + + This disables the HTML render for the default filter. + """ + return None + + +class BaseFilter(FilterSet): + class Meta: + model = None + fields = { + "inserted_at": ["exact", "gte", "lte"], + "updated_at": ["exact", "gte", "lte"], + } + + +class TenantFilter(BaseFilter): + class Meta(BaseFilter.Meta): + model = Tenant + fields = { + **BaseFilter.Meta.fields, + "name": ["exact", "icontains"], + } diff --git a/src/backend/api/migrations/0001_initial.py b/src/backend/api/migrations/0001_initial.py index 4fbdf753e8..6930b9b281 100644 --- a/src/backend/api/migrations/0001_initial.py +++ b/src/backend/api/migrations/0001_initial.py @@ -1,5 +1,6 @@ -# Generated by Django 5.0.6 on 2024-07-04 16:53 +# Generated by Django 5.0.6 on 2024-07-17 07:58 +import uuid from django.db import migrations, models @@ -10,18 +11,23 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="TestModel", + name="Tenant", fields=[ ( "id", - models.BigAutoField( - auto_created=True, + models.UUIDField( + default=uuid.uuid4, + editable=False, primary_key=True, serialize=False, - verbose_name="ID", ), ), + ("inserted_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), ("name", models.CharField(max_length=100)), ], + options={ + "abstract": False, + }, ), ] diff --git a/src/backend/api/migrations/0002_tenantmodel.py b/src/backend/api/migrations/0002_tenantmodel.py deleted file mode 100644 index 9ec4fafb92..0000000000 --- a/src/backend/api/migrations/0002_tenantmodel.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.0.7 on 2024-07-12 16:45 - -import uuid -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("api", "0001_initial"), - ] - - operations = [ - migrations.CreateModel( - name="Tenant", - fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ("inserted_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ("name", models.CharField(max_length=100)), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/src/backend/api/models.py b/src/backend/api/models.py index 2496dcaf19..45ec34f08d 100644 --- a/src/backend/api/models.py +++ b/src/backend/api/models.py @@ -1,6 +1,7 @@ -from django.db import models import uuid +from django.db import models + class Base(models.Model): """ @@ -22,12 +23,3 @@ class Tenant(Base): """ name = models.CharField(max_length=100) - - -class TestModel(models.Model): - """To delete. - - Use this model for development/testing purposes. - """ - - name = models.CharField(max_length=100) diff --git a/src/backend/api/serializers.py b/src/backend/api/serializers.py index 5096bd2090..df5d6a5a0f 100644 --- a/src/backend/api/serializers.py +++ b/src/backend/api/serializers.py @@ -1,20 +1,14 @@ -from rest_framework import serializers +from rest_framework_json_api import serializers -from api.models import TestModel, Tenant +from api.models import Tenant -class TestModelSerializer(serializers.ModelSerializer): - """To delete. - - Use this serializer for development/testing purposes. - """ - - class Meta: - model = TestModel - fields = "__all__" +class BaseSerializerV1(serializers.ModelSerializer): + def get_root_meta(self, _resource, _many): + return {"version": "v1"} -class TenantSerializer(serializers.ModelSerializer): +class TenantSerializer(BaseSerializerV1): """ Serializer for the Tenant model. """ diff --git a/src/backend/api/tests.py b/src/backend/api/tests.py deleted file mode 100644 index a79ca8be56..0000000000 --- a/src/backend/api/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.test import TestCase - -# Create your tests here. diff --git a/src/backend/api/tests/test_views.py b/src/backend/api/tests/test_views.py new file mode 100644 index 0000000000..815200657d --- /dev/null +++ b/src/backend/api/tests/test_views.py @@ -0,0 +1,179 @@ +import pytest +from django.urls import reverse +from rest_framework import status + +from api.models import Tenant + +API_JSON_CONTENT_TYPE = "application/vnd.api+json" + + +@pytest.mark.django_db +class TestTenantViewSet: + @pytest.fixture + def tenants(self): + tenant1 = Tenant.objects.create( + name="Tenant One", + inserted_at="2023-01-01T00:00:00Z", + updated_at="2023-01-02T00:00:00Z", + ) + tenant2 = Tenant.objects.create( + name="Tenant Two", + inserted_at="2023-01-03T00:00:00Z", + updated_at="2023-01-04T00:00:00Z", + ) + return tenant1, tenant2 + + @pytest.fixture + def valid_tenant_payload(self): + return { + "name": "Tenant Three", + "inserted_at": "2023-01-05T00:00:00Z", + "updated_at": "2023-01-06T00:00:00Z", + } + + @pytest.fixture + def invalid_tenant_payload(self): + return { + "name": "", + "inserted_at": "2023-01-05T00:00:00Z", + "updated_at": "2023-01-06T00:00:00Z", + } + + def test_tenants_list(self, client, tenants): + response = client.get(reverse("tenant-list")) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == len(tenants) + + def test_tenants_retrieve(self, client, tenants): + tenant1, _ = tenants + response = client.get(reverse("tenant-detail", kwargs={"pk": tenant1.id})) + assert response.status_code == status.HTTP_200_OK + assert response.json()["data"]["attributes"]["name"] == tenant1.name + + def test_tenants_invalid_retrieve(self, client, tenants): + tenant1, _ = tenants + response = client.get(reverse("tenant-detail", kwargs={"pk": "random_id"})) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_tenants_create(self, client, valid_tenant_payload): + response = client.post( + reverse("tenant-list"), data=valid_tenant_payload, format="json" + ) + assert response.status_code == status.HTTP_201_CREATED + assert Tenant.objects.count() == 1 + assert Tenant.objects.get().name == valid_tenant_payload["name"] + + def test_tenants_invalid_create(self, client, invalid_tenant_payload): + response = client.post( + reverse("tenant-list"), + data=invalid_tenant_payload, + format="json", + content_type=API_JSON_CONTENT_TYPE, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_tenants_partial_update(self, client, tenants): + tenant1, _ = tenants + new_name = "This is the new name" + payload = { + "data": { + "type": "Tenant", + "id": tenant1.id, + "attributes": {"name": new_name}, + }, + } + response = client.patch( + reverse("tenant-detail", kwargs={"pk": tenant1.id}), + data=payload, + content_type=API_JSON_CONTENT_TYPE, + ) + assert response.status_code == status.HTTP_200_OK + tenant1.refresh_from_db() + assert tenant1.name == new_name + + def test_tenants_partial_update_invalid_content_type(self, client, tenants): + tenant1, _ = tenants + response = client.patch( + reverse("tenant-detail", kwargs={"pk": tenant1.id}), data={} + ) + assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE + + def test_tenants_partial_update_invalid_content(self, client, tenants): + tenant1, _ = tenants + new_name = "This is the new name" + payload = {"name": new_name} + response = client.patch( + reverse("tenant-detail", kwargs={"pk": tenant1.id}), + data=payload, + content_type=API_JSON_CONTENT_TYPE, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_tenants_delete(self, client, tenants): + tenant1, _ = tenants + response = client.delete(reverse("tenant-detail", kwargs={"pk": tenant1.id})) + assert response.status_code == status.HTTP_204_NO_CONTENT + assert Tenant.objects.count() == 1 + + def test_tenants_delete_invalid(self, client): + response = client.delete(reverse("tenant-detail", kwargs={"pk": "random_id"})) + # To change if we implement RBAC + # (user might not have permissions to see if the tenant exists or not -> 200 empty) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_tenants_list_filter_search(self, client, tenants): + """Search is applied to tenants name.""" + tenant1, _ = tenants + response = client.get(reverse("tenant-list"), {"filter[search]": tenant1.name}) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["attributes"]["name"] == tenant1.name + + def test_tenants_list_query_param_name(self, client, tenants): + tenant1, _ = tenants + response = client.get(reverse("tenant-list"), {"name": tenant1.name}) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_tenants_list_invalid_query_param(self, client): + response = client.get(reverse("tenant-list"), {"random": "value"}) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_tenants_list_filter_name(self, client, tenants): + tenant1, _ = tenants + response = client.get(reverse("tenant-list"), {"filter[name]": tenant1.name}) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == 1 + assert response.json()["data"][0]["attributes"]["name"] == tenant1.name + + def test_tenants_list_filter_invalid(self, client): + response = client.get(reverse("tenant-list"), {"filter[invalid]": "whatever"}) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_tenants_list_page_size(self, client, tenants): + page_size = 1 + + response = client.get(reverse("tenant-list"), {"page[size]": page_size}) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == page_size + assert response.json()["meta"]["pagination"]["page"] == 1 + assert response.json()["meta"]["pagination"]["pages"] == len(tenants) + + def test_tenants_list_page_number(self, client, tenants): + page_size = 1 + page_number = 2 + + response = client.get( + reverse("tenant-list"), + {"page[size]": page_size, "page[number]": page_number}, + ) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == page_size + assert response.json()["meta"]["pagination"]["page"] == page_number + assert response.json()["meta"]["pagination"]["pages"] == len(tenants) + + def test_tenants_list_sort_name(self, client, tenants): + _, tenant2 = tenants + response = client.get(reverse("tenant-list"), {"sort": "-name"}) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == 2 + assert response.json()["data"][0]["attributes"]["name"] == tenant2.name diff --git a/src/backend/api/urls/v1/urls.py b/src/backend/api/urls/v1/urls.py index 7eb236526c..f10a7fd272 100644 --- a/src/backend/api/urls/v1/urls.py +++ b/src/backend/api/urls/v1/urls.py @@ -1,16 +1,16 @@ -from django.urls import path -from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView +from django.urls import path, include +from drf_spectacular.views import SpectacularRedocView +from rest_framework import routers + +from api.views.v1.views import SchemaView, TenantViewSet + +router = routers.DefaultRouter(trailing_slash=False) + +router.register(r"tenants", TenantViewSet) -from api.views.v1 import views urlpatterns = [ - path("schema", SpectacularAPIView.as_view(), name="schema"), + path("", include(router.urls)), + path("schema", SchemaView.as_view(), name="schema"), path("docs", SpectacularRedocView.as_view(url_name="schema"), name="docs"), - # To delete. Use this url for development/testing purposes - path("test", views.TestModelCreateView.as_view(), name="test-list"), - path( - "test//", - views.TestModelRetrieveUpdateDestroyView.as_view(), - name="test-detail", - ), ] diff --git a/src/backend/api/views/base_views.py b/src/backend/api/views/base_views.py new file mode 100644 index 0000000000..9211351749 --- /dev/null +++ b/src/backend/api/views/base_views.py @@ -0,0 +1,23 @@ +from rest_framework.filters import SearchFilter +from rest_framework_json_api import filters +from rest_framework_json_api.views import ModelViewSet + +from api.filters import CustomDjangoFilterBackend + + +class BaseViewSet(ModelViewSet): + filter_backends = [ + filters.QueryParameterValidationFilter, + filters.OrderingFilter, + CustomDjangoFilterBackend, + SearchFilter, + ] + + filterset_fields = [] + search_fields = [] + + ordering_fields = "__all__" + ordering = ["id"] + + def get_queryset(self): + raise NotImplementedError diff --git a/src/backend/api/views/v1/views.py b/src/backend/api/views/v1/views.py index c5c3321bf1..cfca446885 100644 --- a/src/backend/api/views/v1/views.py +++ b/src/backend/api/views/v1/views.py @@ -1,14 +1,66 @@ -from rest_framework import generics +from django.conf import settings as django_settings +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_control +from drf_spectacular.settings import spectacular_settings +from drf_spectacular.utils import extend_schema, extend_schema_view +from drf_spectacular.views import SpectacularAPIView -from api.models import TestModel -from api.serializers import TestModelSerializer +from api.filters import TenantFilter +from api.models import Tenant +from api.serializers import TenantSerializer +from api.views.base_views import BaseViewSet + +CACHE_DECORATOR = cache_control( + max_age=django_settings.CACHE_MAX_AGE, + stale_while_revalidate=django_settings.CACHE_STALE_WHILE_REVALIDATE, +) -class TestModelCreateView(generics.ListCreateAPIView): - queryset = TestModel.objects.all() - serializer_class = TestModelSerializer +@extend_schema(exclude=True) +class SchemaView(SpectacularAPIView): + serializer_class = None + + def get(self, request, *args, **kwargs): + spectacular_settings.TITLE = "Prowler RESTful API" + spectacular_settings.VERSION = "1.0.0" + spectacular_settings.DESCRIPTION = ( + "Prowler RESTful API specification.\n\nThis file is auto-generated." + ) + return super().get(request, *args, **kwargs) -class TestModelRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView): - queryset = TestModel.objects.all() - serializer_class = TestModelSerializer +@extend_schema_view( + list=extend_schema( + summary="List all tenants", + description="Retrieve a list of all tenants with options for filtering by various criteria.", + ), + retrieve=extend_schema( + summary="Retrieve data from a tenant", + description="Fetch detailed information about a specific tenant by their ID.", + ), + create=extend_schema( + summary="Create a new tenant", + description="Add a new tenant to the system by providing the required tenant details.", + ), + partial_update=extend_schema( + summary="Partially update a tenant", + description="Update certain fields of an existing tenant's information without affecting other fields.", + ), + destroy=extend_schema( + summary="Delete a tenant", + description="Remove a tenant from the system by their ID.", + ), +) +@method_decorator(CACHE_DECORATOR, name="list") +@method_decorator(CACHE_DECORATOR, name="retrieve") +class TenantViewSet(BaseViewSet): + queryset = Tenant.objects.all() + serializer_class = TenantSerializer + http_method_names = ["get", "post", "patch", "delete"] + filterset_class = TenantFilter + search_fields = ["name"] + ordering = ["inserted_at"] + ordering_fields = ["name", "inserted_at", "updated_at"] + + def get_queryset(self): + return Tenant.objects.all() diff --git a/src/backend/config/django/base.py b/src/backend/config/django/base.py index 47d7d74f10..86e3868c12 100644 --- a/src/backend/config/django/base.py +++ b/src/backend/config/django/base.py @@ -21,6 +21,7 @@ INSTALLED_APPS = [ "corsheaders", "drf_spectacular", "django_guid", + "rest_framework_json_api", ] MIDDLEWARE = [ @@ -58,13 +59,32 @@ TEMPLATES = [ REST_FRAMEWORK = { "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "PAGE_SIZE": 10, + "EXCEPTION_HANDLER": "rest_framework_json_api.exceptions.exception_handler", + "DEFAULT_PAGINATION_CLASS": "rest_framework_json_api.pagination.JsonApiPageNumberPagination", + "DEFAULT_PARSER_CLASSES": ( + "rest_framework_json_api.parsers.JSONParser", + "rest_framework.parsers.FormParser", + "rest_framework.parsers.MultiPartParser", + ), + "DEFAULT_RENDERER_CLASSES": ("rest_framework_json_api.renderers.JSONRenderer",), + "DEFAULT_METADATA_CLASS": "rest_framework_json_api.metadata.JSONAPIMetadata", + "DEFAULT_FILTER_BACKENDS": ( + "rest_framework_json_api.filters.QueryParameterValidationFilter", + "rest_framework_json_api.filters.OrderingFilter", + "rest_framework_json_api.django_filters.backends.DjangoFilterBackend", + "rest_framework.filters.SearchFilter", + ), + "SEARCH_PARAM": "filter[search]", + "TEST_REQUEST_RENDERER_CLASSES": ( + "rest_framework_json_api.renderers.JSONRenderer", + ), + "TEST_REQUEST_DEFAULT_FORMAT": "vnd.api+json", + "JSON_API_UNIFORM_EXCEPTIONS": True, } SPECTACULAR_SETTINGS = { - "TITLE": "Prowler RESTful API", - "DESCRIPTION": "Prowler RESTful API specification.\n\nThis file is auto-generated.", - "VERSION": "1.0.0", - "SERVE_INCLUDE_SCHEMA": True, + "SERVE_INCLUDE_SCHEMA": False, } WSGI_APPLICATION = "config.wsgi.application" @@ -122,3 +142,7 @@ STATIC_URL = "static/" # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Cache settings +CACHE_MAX_AGE = env.int("DJANGO_CACHE_MAX_AGE", 3600) +CACHE_STALE_WHILE_REVALIDATE = env.int("DJANGO_STALE_WHILE_REVALIDATE", 60) diff --git a/src/backend/config/django/devel.py b/src/backend/config/django/devel.py index a03daf4d0b..c53bbeb420 100644 --- a/src/backend/config/django/devel.py +++ b/src/backend/config/django/devel.py @@ -16,3 +16,15 @@ DATABASES = { "PORT": env("POSTGRES_PORT", default="5432"), } } + + +REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] = tuple( # noqa: F405 + render_class + for render_class in REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] # noqa: F405 +) + ("rest_framework_json_api.renderers.BrowsableAPIRenderer",) + +REST_FRAMEWORK["DEFAULT_FILTER_BACKENDS"] = tuple( # noqa: F405 + filter_backend + for filter_backend in REST_FRAMEWORK["DEFAULT_FILTER_BACKENDS"] # noqa: F405 + if "DjangoFilterBackend" not in filter_backend +) + ("api.filters.CustomDjangoFilterBackend",)