# Django REST Framework — Cursor Rules
# Comprehensive rules for building APIs with Django and DRF

## Project Context
You are working on a Django application with Django REST Framework (DRF) for building
APIs. The project follows Django conventions, uses class-based views, and leverages
DRF's serialization, authentication, and permission systems. The codebase is structured
as a collection of Django apps with clear separation of concerns.

## Tech Stack
- Python 3.11+
- Django 5.0+
- Django REST Framework 3.15+
- PostgreSQL (recommended) or SQLite for development
- Celery for background tasks (with Redis broker)
- django-filter for queryset filtering
- drf-spectacular for OpenAPI schema generation
- pytest-django for testing

## Coding Style

### Naming Conventions
- Django apps: short snake_case nouns (e.g., `users`, `orders`, `payments`)
- Models: PascalCase singular (e.g., `User`, `Order`, `OrderItem`)
- Serializers: PascalCase with `Serializer` suffix (e.g., `UserSerializer`, `OrderCreateSerializer`)
- Views/ViewSets: PascalCase with `ViewSet` or `View` suffix (e.g., `UserViewSet`, `LoginView`)
- URL patterns: kebab-case (e.g., `/api/order-items/`, `/api/user-profiles/`)
- Template tags: snake_case (e.g., `{% user_avatar %}`)
- Management commands: snake_case (e.g., `sync_products`, `send_reports`)
- Signals: past tense (e.g., `order_created`, `payment_processed`)

### Project Structure
```
project/
  config/                 # Project settings
    settings/
      base.py
      local.py
      production.py
    urls.py
    wsgi.py
    celery.py
  apps/
    users/
      models.py
      serializers.py
      views.py
      urls.py
      admin.py
      signals.py
      services.py         # Business logic (not in views or models)
      tests/
        test_models.py
        test_views.py
        test_services.py
        factories.py      # Test data factories
    orders/
      ...
  common/                 # Shared utilities
    permissions.py
    pagination.py
    exceptions.py
    mixins.py
```

## Model Patterns

### Model Definition
```python
from django.db import models
from django.utils import timezone

class Order(models.Model):
    class Status(models.TextChoices):
        PENDING = "pending", "Pending"
        CONFIRMED = "confirmed", "Confirmed"
        SHIPPED = "shipped", "Shipped"
        DELIVERED = "delivered", "Delivered"
        CANCELLED = "cancelled", "Cancelled"

    user = models.ForeignKey("users.User", on_delete=models.CASCADE, related_name="orders")
    status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
    total = models.DecimalField(max_digits=10, decimal_places=2)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["user", "status"]),
            models.Index(fields=["-created_at"]),
        ]

    def __str__(self):
        return f"Order #{self.pk} - {self.user}"

    @property
    def is_cancellable(self):
        return self.status in (self.Status.PENDING, self.Status.CONFIRMED)
```

### Model Rules
- Keep models focused on data structure and simple properties
- Use `TextChoices` / `IntegerChoices` for choice fields
- Always define `__str__`, `Meta.ordering`, and relevant indexes
- Use `related_name` on all ForeignKey and M2M fields
- Move complex business logic to services, not model methods
- Use `F()` and `Q()` objects for complex queries
- Avoid `null=True` on string fields — use `blank=True` with default `""`

## Serializer Patterns

### Separate Read/Write Serializers
```python
class OrderListSerializer(serializers.ModelSerializer):
    user = UserMinimalSerializer(read_only=True)
    status_display = serializers.CharField(source="get_status_display", read_only=True)

    class Meta:
        model = Order
        fields = ["id", "user", "status", "status_display", "total", "created_at"]

class OrderCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Order
        fields = ["items", "shipping_address"]

    def validate_items(self, value):
        if not value:
            raise serializers.ValidationError("Order must have at least one item.")
        return value

    def create(self, validated_data):
        # Delegate complex creation logic to a service
        return OrderService.create_order(
            user=self.context["request"].user,
            **validated_data,
        )
```

## ViewSet Patterns
```python
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend

class OrderViewSet(viewsets.ModelViewSet):
    permission_classes = [permissions.IsAuthenticated]
    filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
    filterset_fields = ["status"]
    ordering_fields = ["created_at", "total"]
    ordering = ["-created_at"]

    def get_queryset(self):
        return Order.objects.filter(user=self.request.user).select_related("user")

    def get_serializer_class(self):
        if self.action == "create":
            return OrderCreateSerializer
        return OrderListSerializer

    @action(detail=True, methods=["post"])
    def cancel(self, request, pk=None):
        order = self.get_object()
        if not order.is_cancellable:
            return Response({"error": "Order cannot be cancelled"}, status=status.HTTP_400_BAD_REQUEST)
        OrderService.cancel_order(order)
        return Response(OrderListSerializer(order).data)
```

## Service Layer Pattern
```python
# services.py — keep business logic out of views and models
class OrderService:
    @staticmethod
    def create_order(user, items, shipping_address):
        with transaction.atomic():
            order = Order.objects.create(user=user, total=0, shipping_address=shipping_address)
            total = Decimal("0")
            for item_data in items:
                product = Product.objects.select_for_update().get(id=item_data["product_id"])
                if product.stock < item_data["quantity"]:
                    raise ValidationError(f"Insufficient stock for {product.name}")
                OrderItem.objects.create(order=order, product=product, quantity=item_data["quantity"], price=product.price)
                product.stock -= item_data["quantity"]
                product.save()
                total += product.price * item_data["quantity"]
            order.total = total
            order.save()
        order_created.send(sender=Order, order=order)
        return order
```

## Error Handling
- Use DRF's built-in exception handling (`ValidationError`, `NotFound`, `PermissionDenied`)
- Create custom exception handler for consistent error format
- Use `transaction.atomic()` for operations that must be all-or-nothing
- Return structured error responses: `{"error": "message", "code": "ERROR_CODE"}`
- Log exceptions with full context in production

## Security
- Always set `permission_classes` on views — never leave them open
- Use `select_related` / `prefetch_related` to prevent N+1 (and info leaks via lazy loading)
- Filter querysets by the authenticated user — never trust URL params alone
- Use `@action(permission_classes=[...])` for custom action permissions
- Validate file uploads: size, type, and content
- Use Django's CSRF protection for session-based auth
- Throttle API endpoints with DRF's `throttle_classes`

## Testing
```python
import pytest
from rest_framework.test import APIClient
from apps.users.tests.factories import UserFactory

@pytest.fixture
def api_client():
    return APIClient()

@pytest.fixture
def authenticated_client(api_client):
    user = UserFactory()
    api_client.force_authenticate(user=user)
    return api_client, user

@pytest.mark.django_db
def test_create_order(authenticated_client):
    client, user = authenticated_client
    response = client.post("/api/orders/", {"items": [{"product_id": 1, "quantity": 2}]}, format="json")
    assert response.status_code == 201
    assert Order.objects.filter(user=user).count() == 1
```

## Performance Guidelines
- Use `select_related()` for ForeignKey joins, `prefetch_related()` for reverse/M2M
- Use `only()` / `defer()` for large models when you need few fields
- Implement cursor-based pagination for large datasets
- Cache expensive queries with Django's cache framework
- Use `bulk_create()` and `bulk_update()` for batch operations
- Run slow tasks async with Celery
- Use database indexes on filtered and ordered fields

## Common Pitfalls
- N+1 queries from accessing related objects without `select_related`/`prefetch_related`
- Not filtering querysets by user — returning other users' data
- Fat views with business logic — extract to services
- Using `ModelSerializer` for both reads and writes when they need different fields
- Forgetting `@pytest.mark.django_db` on database tests
- Not using `transaction.atomic()` for multi-step writes
- Overriding `get_queryset()` but not calling `super()` when needed
- Exposing sensitive fields (password hash, tokens) in serializers
