Specification
- Skill ID
- prowler-cloud/prowler/django-drf
- Publisher
- prowler-cloud
- Repository
- prowler
- Installs
- 0
- Files
- 4
- License
- Apache-2.0
- Synced
- Sep 23, 2026
How to use it
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
prowler-cloud/prowler/django-drfInstalls these files- SKILL.md
- assets/security_patterns.py
- references/file-locations.md
- references/json-api-conventions.md
What this skill tells the agent
Critical Patterns
- ALWAYS separate serializers by operation: Read / Create / Update / Include
- ALWAYS use
filterset_classfor complex filtering (notfilterset_fields) - ALWAYS validate unknown fields in write serializers (inherit
BaseWriteSerializer) - ALWAYS use
select_related/prefetch_relatedinget_queryset()to avoid N+1 - ALWAYS handle
swagger_fake_viewinget_queryset()for schema generation - ALWAYS use
@extend_schema_fieldfor OpenAPI docs onSerializerMethodField - NEVER put business logic in serializers - use services/utils
- NEVER use auto-increment PKs - use UUIDv4 or UUIDv7
- NEVER use trailing slashes in URLs (
trailing_slash=False)
Note: swagger_fake_view is specific to drf-spectacular for OpenAPI schema generation.Implementation Checklist
When implementing a new endpoint, review these patterns in order:
| # | Pattern | Reference | Key Points |
|---|---|---|---|
| 1 | Models | api/models.py | UUID PK, inserted_at/updated_at, JSONAPIMeta.resource_name |
| 2 | ViewSets | api/base_views.py, api/v1/views.py | Inherit BaseRLSViewSet, get_queryset() with N+1 prevention |
| 3 | Serializers | api/v1/serializers.py | Separate Read/Create/Update/Include, inherit BaseWriteSerializer |
| 4 | Filters | api/filters.py | Use filterset_class, inherit base filter classes |
| 5 | Permissions | api/base_views.py | required_permissions, set_required_permissions() |
| 6 | Pagination | api/pagination.py | Custom pagination class if needed |
| 7 | URL Routing | api/v1/urls.py | trailing_slash=False, kebab-case paths |
| 8 | OpenAPI Schema | api/v1/views.py | @extend_schema_view with drf-spectacular |
| 9 | Tests | api/tests/test_views.py | JSON:API content type, fixture patterns |
Full file paths: See references/file-locations.md
Decision Trees
Which Serializer?
GET list/retrieve → <Model>Serializer
POST create → <Model>CreateSerializer
PATCH update → <Model>UpdateSerializer
?include=... → <Model>IncludeSerializerWhich Base Serializer?
Read-only serializer → BaseModelSerializerV1
Create with tenant_id → RLSSerializer + BaseWriteSerializer (auto-injects tenant_id on create)
Update with validation → BaseWriteSerializer (tenant_id already exists on object)
Non-model data → BaseSerializerV1Which Filter Base?
Direct FK to Provider → BaseProviderFilter
FK via Scan → BaseScanProviderFilter
No provider relation → FilterSetWhich Base ViewSet?
RLS-protected model → BaseRLSViewSet (most common)
Tenant operations → BaseTenantViewset
User operations → BaseUserViewset
No RLS required → BaseViewSet (rare)Resource Name Format?
Single word model → plural lowercase (Provider → providers)
Multi-word model → plural lowercase kebab (ProviderGroup → provider-groups)
Through/join model → parent-child pattern (UserRoleRelationship → user-roles)
Aggregation/overview → descriptive kebab plural (ComplianceOverview → compliance-overviews)Serializer Patterns
Base Class Hierarchy
# Read serializer (most common)
class ProviderSerializer(RLSSerializer):
class Meta:
model = Provider
fields = ["id", "provider", "uid", "alias", "connected", "inserted_at"]
# Write serializer (validates unknown fields)
class ProviderCreateSerializer(RLSSerializer, BaseWriteSerializer):
class Meta:
model = Provider
fields = ["provider", "uid", "alias"]
# Include serializer (sparse fields for ?include=)
class ProviderIncludeSerializer(RLSSerializer):
class Meta:
model = Provider
fields = ["id", "alias"] # Minimal fieldsSerializerMethodField with OpenAPI
from drf_spectacular.utils import extend_schema_field
class ProviderSerializer(RLSSerializer):
connection = serializers.SerializerMethodField(read_only=True)
@extend_schema_field({
"type": "object",
"properties": {
"connected": {"type": "boolean"},
"last_checked_at": {"type": "string", "format": "date-time"},
},
})
def get_connection(self, obj):
return {
"connected": obj.connected,
"last_checked_at": obj.connection_last_checked_at,
}Included Serializers (JSON:API)
class ScanSerializer(RLSSerializer):
included_serializers = {
"provider": "api.v1.serializers.ProviderIncludeSerializer",
}Sensitive Data Masking
def to_representation(self, instance):
data = super().to_representation(instance)
# Mask by default, expose only on explicit request
fields_param = self.context.get("request").query_params.get("fields[my-model]", "")
if "api_key" in fields_param:
data["api_key"] = instance.api_key_decoded
else:
data["api_key"] = "****" if instance.api_key else None
return dataViewSet Patterns
get_queryset() with N+1 Prevention
Always combine swagger_fake_view check with select_related/prefetch_related:
def get_queryset(self):
# REQUIRED: Return empty queryset for OpenAPI schema generation
if getattr(self, "swagger_fake_view", False):
return Provider.objects.none()
# N+1 prevention: eager load relationships
return Provider.objects.select_related(
"tenant",
).prefetch_related(
"provider_groups",
Prefetch("tags", queryset=ProviderTag.objects.filter(tenant_id=self.request.tenant_id)),
)Why swagger_fake_view? drf-spectacular introspects ViewSets to generate OpenAPI schemas. Without this check, it executes real queries and can fail without request context.
Action-Specific Serializers
def get_serializer_class(self):
if self.action == "create":
return ProviderCreateSerializer
elif self.action == "partial_update":
return ProviderUpdateSerializer
elif self.action in ["connection", "destroy"]:
return TaskSerializer
return ProviderSerializerDynamic Permissions per Action
class ProviderViewSet(BaseRLSViewSet):
required_permissions = [Permissions.MANAGE_PROVIDERS]
def set_required_permissions(self):
if self.action in ["list", "retrieve"]:
self.required_permissions = [] # Read-only = no permission
else:
self.required_permissions = [Permissions.MANAGE_PROVIDERS]Cache Decorator
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
CACHE_DECORATOR = cache_control(
max_age=django_settings.CACHE_MAX_AGE,
stale_while_revalidate=django_settings.CACHE_STALE_WHILE_REVALIDATE,
)
@method_decorator(CACHE_DECORATOR, name="list")
@method_decorator(CACHE_DECORATOR, name="retrieve")
class ProviderViewSet(BaseRLSViewSet):
passCustom Actions
# Detail action (operates on single object)
@action(detail=True, methods=["post"], url_name="connection")
def connection(self, request, pk=None):
instance = self.get_object()
# Process instance...
# List action (operates on collection)
@action(detail=False, methods=["get"], url_name="metadata")
def metadata(self, request):
queryset = self.filter_queryset(self.get_queryset())
# Aggregate over queryset...Filter Patterns
Base Filter Classes
class BaseProviderFilter(FilterSet):
"""For models with direct FK to Provider"""
provider_id = UUIDFilter(field_name="provider__id", lookup_expr="exact")
provider_id__in = UUIDInFilter(field_name="provider__id", lookup_expr="in")
provider_type = ChoiceFilter(field_name="provider__provider", choices=Provider.ProviderChoices.choices)
More from prowler-cloud
Use this skill in a project
Open any RiverX project, search the Skills panel for prowler-cloud/prowler/django-drf, and install it in one click.
