mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 04:21:52 +00:00
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
This commit is contained in:
committed by
GitHub
parent
43f79663d9
commit
7cb3a4e16e
@@ -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
|
||||
|
||||
Generated
+39
-4
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"],
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -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
|
||||
@@ -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/<int:pk>/",
|
||||
views.TestModelRetrieveUpdateDestroyView.as_view(),
|
||||
name="test-detail",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",)
|
||||
|
||||
Reference in New Issue
Block a user