CursorPool
← 返回首页

Django 5

Django 5.x rules, DRF patterns, scaffold skills, and anti-pattern detection for Cursor. Prevents N+1 queries, teaches idiomatic Django, and covers features LLMs miss.

cursor.directory·13
规则

django-reviewer

Reviews Django code changes for N+1 queries, missing optimizations, wrong field types, security issues, and DRF anti-patterns. Use after generating or modifying Django code.

# Django Code Reviewer

You are a Django expert reviewer. Your job is to review code changes and catch issues before they ship.

## Review Checklist

For every piece of Django code you review, check:

### N+1 Query Detection (Priority 1)
- Every loop accessing `.related_field` has a corresponding `select_related` or `prefetch_related`
- Every serializer with nested relationships has an optimized `get_queryset()`
- Templates do not trigger lazy-loaded queries
- Context processors do not run queries on every request

### Model Design
- Every model has `__str__` and `Meta` with `ordering`
- Field types are appropriate (not CharField for everything)
- No `null=True` on CharField/TextField (use `blank=True, default=""`)
- No `FloatField` for monetary values (use `DecimalField`)
- `related_name` on all ForeignKey/M2M fields
- Indexes on frequently filtered/sorted fields
- `TextChoices` for enum fields, not magic strings

### DRF Patterns
- `is_valid()` called before `save()` or accessing `validated_data`
- Explicit `fields` list in serializer Meta (never `'__all__'`)
- `read_only_fields` set for auto-generated fields
- `permission_classes` set on viewsets
- Authentication paired with authorization
- Pagination configured for list endpoints
- `perform_create` used for request-dependent fields

### Security
- `{% csrf_token %}` in all POST forms
- No hardcoded secrets in settings
- No `DEBUG = True` in production config
- `ALLOWED_HOSTS` configured
- No raw SQL with unsanitized user input
- `update_fields` on `save()` for partial updates

### Architecture
- Business logic in models, not views
- Custom managers for repeated query patterns
- Side effects deferred with `transaction.on_commit()`
- Signals registered in `AppConfig.ready()` with `dispatch_uid`

### Django 5.x Opportunities
- Can `default` be replaced with `db_default`?
- Can computed properties use `GeneratedField`?
- Should `LoginRequiredMiddleware` replace per-view `@login_required`?

## Output Format

For each issue found, report:
1. **File and location**
2. **Severity**: critical / warning / suggestion
3. **What's wrong** and **how to fix it**
4. The exact code change needed

If no issues found, confirm the code follows Django best practices.
Skill

django-api

Generate a complete DRF API endpoint from a description. Includes serializer, viewset, router registration, permissions, filtering, and pagination.

# Generate Django REST Framework API

## When to Use

Use this skill when:
- Creating a new API endpoint for an existing Django model
- Setting up DRF in a project for the first time
- The user asks for a REST API for a specific resource

## Instructions

1. Check if Django REST Framework is installed by looking for `rest_framework` in `INSTALLED_APPS`. If not present, suggest adding it.

2. Check for existing DRF configuration in `settings.py`:
   - `REST_FRAMEWORK` dict (pagination, authentication, permissions)
   - If missing, suggest adding default configuration

3. Identify the target model by reading the project's `models.py`.

4. Generate the API components:

   **Serializer:**
   - Use `ModelSerializer` with explicit `fields`
   - Set `read_only_fields` for auto-generated fields (id, timestamps)
   - If the API needs different read/write shapes, generate separate serializers
   - For nested relationships, mark them `read_only=True` unless write-through is explicitly needed

   **ViewSet or Generic View:**
   - Use `ModelViewSet` for full CRUD
   - Use `ReadOnlyModelViewSet` for read-only
   - Use specific generics (ListCreateAPIView, etc.) when not all CRUD is needed
   - Always override `get_queryset()` with `select_related`/`prefetch_related`
   - Set `permission_classes`
   - Add `filter_backends` with `DjangoFilterBackend`, `SearchFilter`, `OrderingFilter`
   - Set `filterset_fields`, `search_fields`, `ordering_fields`
   - Use `perform_create` for request-dependent field assignment

   **URL Registration:**
   - Register viewsets with `DefaultRouter`
   - Include router URLs under an `api/` prefix
   - Set `basename` when `get_queryset` is overridden without a `queryset` attribute

   **Permissions:**
   - Default to `IsAuthenticatedOrReadOnly` for public resources
   - Create custom object-level permissions when ownership matters
   - Suggest `LoginRequiredMiddleware` (Django 5.1+) if not already configured

5. If this is the project's first DRF endpoint, also generate the settings configuration:
   ```python
   REST_FRAMEWORK = {
       'DEFAULT_AUTHENTICATION_CLASSES': [
           'rest_framework.authentication.SessionAuthentication',
           'rest_framework.authentication.TokenAuthentication',
       ],
       'DEFAULT_PERMISSION_CLASSES': [
           'rest_framework.permissions.IsAuthenticatedOrReadOnly',
       ],
       'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
       'PAGE_SIZE': 20,
       'DEFAULT_FILTER_BACKENDS': [
           'django_filters.rest_framework.DjangoFilterBackend',
           'rest_framework.filters.SearchFilter',
           'rest_framework.filters.OrderingFilter',
       ],
   }
   ```

6. Consult the API patterns reference for common endpoint structures.
Skill

django-model

Scaffold a complete Django model with serializer, viewset, URL registration, admin configuration, and the migration command to run. Generates a full vertical slice, not just a model class.

# Scaffold Django Model

## When to Use

Use this skill when:
- Creating a new Django model with its full API/admin stack
- The user asks for a new resource or entity in their Django project
- You need to generate model + serializer + viewset + URL + admin in one shot

## Instructions

1. Ask or infer the model name and fields from the user's description.

2. Read the project's existing models to understand conventions:
   - Check `settings.py` for `AUTH_USER_MODEL` (use it instead of `User`)
   - Check existing models for naming patterns, base classes, common mixins
   - Check if the project uses DRF (look for `rest_framework` in `INSTALLED_APPS`)
   - Check if the project uses `django-filter` (for filter backends)

3. Generate the following files/additions, using correct Django 5.x patterns:

   **Model** (in `models.py` or `models/`):
   - Use appropriate field types (not CharField for everything)
   - Include `__str__` and `Meta` with `ordering`
   - Add `related_name` on all ForeignKey/M2M fields
   - Add `db_index=True` on frequently filtered fields
   - Use `TextChoices` for enum fields
   - Use `blank=True, default=""` on optional string fields (not `null=True`)
   - Use `db_default` for database-level defaults where appropriate

   **Serializer** (in `serializers.py`):
   - Use `ModelSerializer`
   - Explicit `fields` list (never `'__all__'`)
   - Set `read_only_fields` for auto-generated fields
   - If nested relationships exist, optimize with `select_related`/`prefetch_related` note

   **ViewSet** (in `views.py` or `viewsets.py`):
   - Use `ModelViewSet` for full CRUD, `ReadOnlyModelViewSet` for read-only
   - Override `get_queryset()` with `select_related`/`prefetch_related`
   - Set `permission_classes`
   - Use `perform_create` to set request-dependent fields (e.g., author)
   - Add filter backends if django-filter is available

   **URL registration** (in `urls.py`):
   - Register with router if using DRF viewsets
   - Use `app_name` namespace
   - Use `path()` with named patterns

   **Admin** (in `admin.py`):
   - Register with `@admin.register`
   - Set `list_display`, `list_filter`, `search_fields`
   - Set `readonly_fields` for auto-generated fields

4. At the end, output the migration command:
   ```bash
   python manage.py makemigrations <app_name>
   python manage.py migrate
   ```

5. Consult the model patterns reference for field type selection and common patterns.
Skill

django-settings

Split a monolithic Django settings.py into base/development/production configuration with environment variable extraction. Fixes hardcoded secrets and ensures production-safe defaults.

# Split Django Settings

## When to Use

Use this skill when:
- A project has a single `settings.py` with development and production config mixed
- Secrets are hardcoded in settings
- The project needs environment-specific configuration

## Instructions

1. Read the current `settings.py` completely.

2. Identify values that should vary by environment:
   - `SECRET_KEY` (must come from environment)
   - `DEBUG` (True in dev, False in prod)
   - `ALLOWED_HOSTS` (empty in dev, domain list in prod)
   - `DATABASES` (SQLite in dev, PostgreSQL in prod, or same with different credentials)
   - Email backend and credentials
   - Cache backend
   - `STATIC_ROOT`, `MEDIA_ROOT`
   - Third-party API keys
   - `SECURE_SSL_REDIRECT`, `CSRF_COOKIE_SECURE`, `SESSION_COOKIE_SECURE`

3. Create the following structure:
   ```
   config/
     settings/
       __init__.py      # Imports from DJANGO_SETTINGS_MODULE
       base.py          # Shared settings (INSTALLED_APPS, MIDDLEWARE, TEMPLATES, etc.)
       development.py   # Dev overrides (DEBUG=True, SQLite, console email)
       production.py    # Prod settings (DEBUG=False, PostgreSQL, real email, security)
   ```

4. **base.py** should contain:
   - All `INSTALLED_APPS`, `MIDDLEWARE`, `TEMPLATES`, `ROOT_URLCONF`
   - `AUTH_USER_MODEL` if custom
   - `DEFAULT_AUTO_FIELD`
   - `USE_TZ = True` (Django 5.0+ default)
   - `SECRET_KEY = os.environ['DJANGO_SECRET_KEY']`
   - Shared `REST_FRAMEWORK` config if using DRF

5. **development.py** should contain:
   ```python
   from .base import *

   DEBUG = True
   ALLOWED_HOSTS = ['localhost', '127.0.0.1']

   DATABASES = {
       'default': {
           'ENGINE': 'django.db.backends.sqlite3',
           'NAME': BASE_DIR / 'db.sqlite3',
       }
   }

   EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
   ```

6. **production.py** should contain:
   ```python
   from .base import *

   DEBUG = False
   ALLOWED_HOSTS = os.environ['DJANGO_ALLOWED_HOSTS'].split(',')

   DATABASES = {
       'default': {
           'ENGINE': 'django.db.backends.postgresql',
           'NAME': os.environ['DB_NAME'],
           'USER': os.environ['DB_USER'],
           'PASSWORD': os.environ['DB_PASSWORD'],
           'HOST': os.environ.get('DB_HOST', 'localhost'),
           'PORT': os.environ.get('DB_PORT', '5432'),
       }
   }

   # Security
   SECURE_SSL_REDIRECT = True
   SECURE_HSTS_SECONDS = 31536000
   SECURE_HSTS_INCLUDE_SUBDOMAINS = True
   SECURE_HSTS_PRELOAD = True
   SESSION_COOKIE_SECURE = True
   CSRF_COOKIE_SECURE = True
   ```

7. Update `manage.py` and `wsgi.py`/`asgi.py` to use the correct settings module:
   ```python
   os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.development')
   ```

8. Generate a `.env.example` file documenting all required environment variables.
Skill

django-validate

Scan a Django project for common anti-patterns, performance issues, and security problems. Reports N+1 queries, missing indexes, wrong field types, insecure settings, and DRF misconfigurations.

# Validate Django Project

## When to Use

Use this skill when:
- Auditing a Django project for quality issues before deployment
- Reviewing a codebase for performance problems
- Checking for common security misconfigurations
- A project is slow and the cause is unknown

## Instructions

1. Read `settings.py` and check for:
   - Hardcoded `SECRET_KEY` (should be from environment)
   - `DEBUG = True` without environment check
   - Empty `ALLOWED_HOSTS` in production config
   - Missing `SECURE_SSL_REDIRECT`
   - Missing `CSRF_COOKIE_SECURE` and `SESSION_COOKIE_SECURE`
   - `USE_TZ = False` (should be True since Django 5.0 default)
   - Missing `DEFAULT_AUTO_FIELD` setting
   - `REST_FRAMEWORK` missing `DEFAULT_PAGINATION_CLASS` or `PAGE_SIZE`
   - Deprecated settings: `DEFAULT_FILE_STORAGE`, `STATICFILES_STORAGE` (use `STORAGES`)

2. Scan all `models.py` files for:
   - Missing `__str__` methods
   - Missing `Meta` class or `ordering`
   - `CharField` used where `EmailField`, `URLField`, `BooleanField`, etc. is appropriate
   - `null=True` on `CharField`/`TextField` (should be `blank=True, default=""`)
   - `FloatField` used for monetary values (should be `DecimalField`)
   - Missing `related_name` on ForeignKey/M2M fields
   - Missing `on_delete` on ForeignKey (syntax error in modern Django)
   - Missing `db_index` on frequently filtered fields
   - Missing indexes in `Meta.indexes`
   - `ManyToManyField` defined in both sides of a relationship

3. Scan all `views.py` and `viewsets.py` for:
   - QuerySets without `select_related`/`prefetch_related` when accessing related fields
   - N+1 query patterns: loops accessing `.related_field` without prefetch
   - Business logic in views (should be in models)
   - Missing `login_required` or permission checks
   - Missing pagination on list endpoints
   - `queryset` class attribute where `get_queryset()` should be used

4. Scan all `serializers.py` for:
   - `fields = '__all__'` (should be explicit field list)
   - Missing `read_only_fields`
   - Nested serializers without corresponding `select_related`/`prefetch_related` in viewset
   - `save()` called without prior `is_valid()`
   - Writable nested serializers without `create()`/`update()` methods

5. Scan `urls.py` for:
   - Hardcoded URLs (should use `reverse()` / `reverse_lazy()`)
   - Use of deprecated `url()` (should be `path()` or `re_path()`)
   - Missing `app_name` namespace
   - Class-based views without `.as_view()`

6. Scan templates for:
   - Forms missing `{% csrf_token %}`
   - Direct database queries in templates (accessing manager methods)
   - Complex logic that should be in views

7. Scan `admin.py` for:
   - Models registered without customization (bare `admin.site.register(Model)`)
   - Missing `list_display`, `list_filter`, `search_fields`

8. Check for Django 5.x opportunities:
   - Can any `default` fields use `db_default` instead?
   - Can computed properties use `GeneratedField`?
   - Is `LoginRequiredMiddleware` appropriate instead of per-view `@login_required`?
   - Are there manual querystring builders in templates that should use `{% querystring %}`?

9. Produce a summary report with:
   - **Issue count** by severity (critical / warning / suggestion)
   - **Issue list** grouped by file with line numbers
   - **Performance impact** estimate (N+1 queries are always critical)
   - **Recommended next steps**

10. Consult the validation checks reference for the complete checklist.
规则

Django model design rules. Auto-attaches when editing models.py. Covers field types, relationships, indexes, managers, constraints, and model methods.

Django model design rules. Auto-attaches when editing models.py. Covers field types, relationships, indexes, managers, constraints, and model methods.

# Django Model Design Rules

## Model Structure Convention

Follow this ordering within every model class:

```python
class Post(models.Model):
    # 1. Database fields
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    content = models.TextField()
    status = models.CharField(max_length=20, choices=Status, default=Status.DRAFT)
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
    tags = models.ManyToManyField(Tag, blank=True, related_name='posts')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    # 2. Custom managers (if any)
    objects = PostManager()
    published = PublishedManager()

    # 3. Meta class
    class Meta:
        ordering = ['-created_at']
        verbose_name_plural = 'posts'
        indexes = [
            models.Index(fields=['status', '-created_at']),
        ]
        constraints = [
            models.UniqueConstraint(fields=['author', 'slug'], name='unique_author_slug'),
        ]

    # 4. __str__
    def __str__(self):
        return self.title

    # 5. save() override (if needed)
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

    # 6. Other methods
    def publish(self):
        self.status = Status.PUBLISHED
        self.save(update_fields=['status'])

    # 7. Properties
    @property
    def is_published(self):
        return self.status == Status.PUBLISHED
```

## Always Set related_name on Relationships

```python
# CORRECT - explicit reverse relation name
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')

# WRONG - uses Django's default "post_set"
author = models.ForeignKey(User, on_delete=models.CASCADE)
```

`related_name` makes reverse queries readable: `user.posts.all()` vs `user.post_set.all()`.

## ManyToManyField Goes in One Model Only

```python
# CORRECT - defined once
class Post(models.Model):
    tags = models.ManyToManyField(Tag, blank=True, related_name='posts')

# WRONG - defined in both models (creates duplicate relation)
class Post(models.Model):
    tags = models.ManyToManyField(Tag)

class Tag(models.Model):
    posts = models.ManyToManyField(Post)  # DO NOT DO THIS
```

Place the M2M field in the model that is typically edited (the one with the form).

## Use through for Complex M2M Relationships

When you need extra data on the relationship:

```python
class Membership(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    role = models.CharField(max_length=50)
    joined_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['person', 'group'], name='unique_person_group'),
        ]

class Group(models.Model):
    members = models.ManyToManyField(Person, through='Membership', related_name='groups')
```

## Use Custom Managers for Reusable Queries

```python
class PublishedManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(status='published')

class PostQuerySet(models.QuerySet):
    def published(self):
        return self.filter(status='published')

    def by_author(self, user):
        return self.filter(author=user)

    def with_author(self):
        return self.select_related('author')

    def with_tags(self):
        return self.prefetch_related('tags')

class Post(models.Model):
    objects = PostQuerySet.as_manager()

# Usage: Post.objects.published().by_author(user).with_author()
```

## Use TextChoices for Enum Fields

```python
class Post(models.Model):
    class Status(models.TextChoices):
        DRAFT = 'draft', 'Draft'
        REVIEW = 'review', 'In Review'
        PUBLISHED = 'published', 'Published'

    status = models.CharField(max_length=20, choices=Status, default=Status.DRAFT)
```

Do not use magic strings: `post.status = 'published'`. Use the enum: `post.status = Post.Status.PUBLISHED`.

## Add Indexes for Frequently Queried Fields

```python
class Meta:
    indexes = [
        # Single field index (alternative to db_index=True)
        models.Index(fields=['status']),

        # Composite index for common filter + sort
        models.Index(fields=['status', '-created_at']),

        # Partial index (PostgreSQL)
        models.Index(
            fields=['created_at'],
            condition=models.Q(status='published'),
            name='published_posts_idx',
        ),
    ]
```

Fields that are filtered, ordered, or used in WHERE clauses should have indexes.

## Use Constraints for Data Integrity

```python
class Meta:
    constraints = [
        # Unique together
        models.UniqueConstraint(fields=['author', 'slug'], name='unique_author_slug'),

        # Check constraint
        # condition= is the kwarg name in Django 5.1+; use check= on Django 5.0 and earlier
        models.CheckConstraint(
            condition=models.Q(price__gte=0),
            name='price_non_negative',
        ),
    ]
```

## Use db_default for Database-Level Defaults (Django 5.0+)

```python
from django.db.models.functions import Now

class Post(models.Model):
    created_at = models.DateTimeField(db_default=Now())
    priority = models.IntegerField(db_default=0)
```

`db_default` applies even for bulk inserts and raw SQL. Use `default` for Python-computed values.

## Use GeneratedField for Computed Columns (Django 5.0+)

```python
from django.db.models import F, GeneratedField

class Product(models.Model):
    price = models.DecimalField(max_digits=10, decimal_places=2)
    tax_rate = models.DecimalField(max_digits=4, decimal_places=2)
    total = GeneratedField(
        expression=F('price') * (1 + F('tax_rate')),
        output_field=models.DecimalField(max_digits=12, decimal_places=2),
        db_persist=True,
    )
```

## Soft Delete Pattern

If the project uses soft deletes, implement via a custom manager:

```python
class SoftDeleteManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(deleted_at__isnull=True)

class Post(models.Model):
    deleted_at = models.DateTimeField(null=True, blank=True, db_index=True)

    objects = SoftDeleteManager()
    all_objects = models.Manager()  # Includes deleted

    def soft_delete(self):
        self.deleted_at = timezone.now()
        self.save(update_fields=['deleted_at'])
```
规则

Django 5.x features that LLMs typically miss. Covers db_default, GeneratedField, LoginRequiredMiddleware, querystring tag, composite primary keys, and other features from Django 5.0, 5.1, and 5.2.

Django 5.x features that LLMs typically miss. Covers db_default, GeneratedField, LoginRequiredMiddleware, querystring tag, composite primary keys, and other features from Django 5.0, 5.1, and 5.2.

# Django 5.x Features

Your training data may predate Django 5.x. These features are available now and should be used where appropriate.

## db_default - Database-Level Defaults (5.0+)

`db_default` sets defaults at the database layer, not in Python. Use it when defaults should apply even for bulk inserts or raw SQL:

```python
from django.db.models.functions import Now

class Post(models.Model):
    created_at = models.DateTimeField(db_default=Now())
    priority = models.IntegerField(db_default=0)
```

**When to use `db_default` vs `default`:**
- `db_default`: value computed by database for ALL inserts (including bulk, raw SQL)
- `default`: value set by Python ORM only

## GeneratedField - Computed Columns (5.0+)

Database-generated columns calculated from other fields. Read-only, computed automatically:

```python
from django.db.models import F, GeneratedField, Value
from django.db.models.functions import Concat

class Product(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    full_name = GeneratedField(
        expression=Concat(F('first_name'), Value(' '), F('last_name')),
        output_field=models.CharField(max_length=201),
        db_persist=True,
    )

    width = models.IntegerField()
    height = models.IntegerField()
    area = GeneratedField(
        expression=F('width') * F('height'),
        output_field=models.IntegerField(),
        db_persist=True,
    )
```

- `db_persist=True`: stored physically (materialized)
- `db_persist=False`: computed on read (virtual, not all databases support this)
- Value is read-only -- do not attempt to set it

## LoginRequiredMiddleware (5.1+)

Requires authentication for all views by default. Use `@login_not_required` for public views:

```python
# settings.py
MIDDLEWARE = [
    # ... other middleware
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.LoginRequiredMiddleware',
]

# views.py
from django.contrib.auth.decorators import login_not_required

@login_not_required
def public_homepage(request):
    return render(request, 'home.html')

@login_not_required
async def public_api(request):
    return JsonResponse({'status': 'ok'})

# All other views require login automatically
def dashboard(request):
    return render(request, 'dashboard.html')  # Redirects to login if not authenticated
```

This is preferred over decorating every protected view with `@login_required`.

## {% querystring %} Template Tag (5.1+)

Simplifies URL query parameter manipulation in templates:

```django
{# Add/modify parameters (preserves existing ones) #}
<a href="{% querystring page=page.next_page_number %}">Next</a>

{# Remove a parameter #}
<a href="{% querystring sort='' %}">Reset sort</a>

{# Multiple parameters #}
<a href="{% querystring page=1 per_page=50 %}">Show 50</a>
```

This replaces the verbose manual approach of iterating over `request.GET`.

## Enhanced Field Choices (5.0+)

Multiple new ways to define choices:

```python
# TextChoices/IntegerChoices - no .choices needed
class Status(models.TextChoices):
    DRAFT = 'draft', 'Draft'
    PUBLISHED = 'published', 'Published'

class Post(models.Model):
    status = models.CharField(max_length=20, choices=Status)  # Direct, no .choices

# Dictionary choices (grouped)
SPORT_CHOICES = {
    'Martial Arts': {'judo': 'Judo', 'karate': 'Karate'},
    'Racket': {'tennis': 'Tennis', 'badminton': 'Badminton'},
}
sport = models.CharField(max_length=20, choices=SPORT_CHOICES)

# Callable choices (dynamic)
def get_years():
    return [(y, str(y)) for y in range(2020, 2030)]

year = models.IntegerField(choices=get_years)
```

## Form Field Groups - as_field_group() (5.0+)

Render a form field with its label, errors, and help text in one call:

```django
{# Before (verbose) #}
{{ form.name.label_tag }}
{{ form.name }}
{{ form.name.errors }}

{# After (clean) #}
{{ form.name.as_field_group }}
```

## Composite Primary Keys (5.2+)

Support for multi-column primary keys:

```python
class OrderItem(models.Model):
    pk = models.CompositePrimaryKey('order_id', 'product_id')
    order = models.ForeignKey(Order, on_delete=models.CASCADE)
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    quantity = models.IntegerField(default=1)
```

## reverse() with Query Parameters (5.2+)

`reverse()` and `reverse_lazy()` now support query parameters and fragments:

```python
from django.urls import reverse

url = reverse('post-detail', kwargs={'pk': 1}, query={'highlight': 'true'})
# /posts/1/?highlight=true

url = reverse('post-detail', kwargs={'pk': 1}, fragment='comments')
# /posts/1/#comments
```

## PostgreSQL Connection Pools (5.1+)

Built-in connection pooling:

```python
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'OPTIONS': {
            'pool': {
                'min_size': 2,
                'max_size': 4,
                'timeout': 10,
            }
        },
    },
}
```

## Async Support Expansion (5.0+)

Nearly everything has async variants now:

```python
# Auth
user = await aauthenticate(request, username='...', password='...')
await alogin(request, user)

# Shortcuts
obj = await aget_object_or_404(Model, pk=1)

# ORM
async for item in qs.prefetch_related('related').aiterator():
    pass

# Signals
await some_signal.asend(sender=Model, instance=obj)

# Sessions (5.1+)
value = await request.session.aget('key')
```

## New Widgets (5.2+)

```python
from django.forms import ColorInput, SearchInput, TelInput

class MyForm(forms.Form):
    color = forms.CharField(widget=ColorInput())    # <input type="color">
    query = forms.CharField(widget=SearchInput())   # <input type="search">
    phone = forms.CharField(widget=TelInput())      # <input type="tel">
```

## Key Deprecations and Removals

- `index_together` removed (use `Meta.indexes`)
- `DEFAULT_FILE_STORAGE` / `STATICFILES_STORAGE` removed (use `STORAGES`)
- `USE_TZ` defaults to `True` (runtime default changed in Django 4.0; project template updated in 5.0)
- SHA1/MD5 password hashers removed
- `url()` removed (use `path()` or `re_path()`)
- `CICharField`, `CIEmailField`, `CITextField` removed
规则

Django anti-pattern detection. Prevents the most common AI-generated mistakes: N+1 queries, wrong field types, missing optimizations, insecure patterns, and architectural errors.

Django anti-pattern detection. Prevents the most common AI-generated mistakes: N+1 queries, wrong field types, missing optimizations, insecure patterns, and architectural errors.

# Django Anti-Patterns

These are the patterns AI models generate most frequently when writing Django code. Every one of them causes real problems. If you catch yourself generating any of these, stop and correct immediately.

## Anti-Pattern 1: N+1 Queries (Most Common AI Mistake)

When accessing related objects in a loop, template, or serializer without `select_related`/`prefetch_related`:

```python
# WRONG - triggers 1 + N queries
posts = Post.objects.all()
for post in posts:
    print(post.author.username)  # Each access = 1 query

# WRONG - same problem in a serializer
class PostSerializer(serializers.ModelSerializer):
    author_name = serializers.CharField(source='author.username')
    class Meta:
        model = Post
        fields = ['title', 'author_name']

class PostListView(generics.ListAPIView):
    queryset = Post.objects.all()  # N+1 when author_name serialized

# CORRECT
class PostListView(generics.ListAPIView):
    serializer_class = PostSerializer
    def get_queryset(self):
        return Post.objects.select_related('author')
```

**Rule:** Every time you access a related field (ForeignKey, OneToOne, ManyToMany), there MUST be a `select_related` or `prefetch_related` in the queryset that feeds it.

## Anti-Pattern 2: CharField for Everything

```python
# WRONG
class UserProfile(models.Model):
    email = models.CharField(max_length=254)      # Use EmailField
    website = models.CharField(max_length=200)     # Use URLField
    is_active = models.CharField(max_length=5)     # Use BooleanField
    birth_date = models.CharField(max_length=10)   # Use DateField
    balance = models.FloatField()                   # Use DecimalField for money
    biography = models.CharField(max_length=5000)  # Use TextField

# CORRECT
class UserProfile(models.Model):
    email = models.EmailField()
    website = models.URLField(blank=True, default="")
    is_active = models.BooleanField(default=True)
    birth_date = models.DateField(null=True, blank=True)
    balance = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    biography = models.TextField(blank=True, default="")
```

## Anti-Pattern 3: Missing __str__ on Models

Every model must have `__str__`. Without it, the admin and shell show `<ModelName object (1)>`:

```python
# WRONG
class Post(models.Model):
    title = models.CharField(max_length=200)
    # No __str__ - admin shows "Post object (1)"

# CORRECT
class Post(models.Model):
    title = models.CharField(max_length=200)

    def __str__(self):
        return self.title
```

## Anti-Pattern 4: null=True on String Fields

```python
# WRONG - creates two "empty" states: NULL and ""
name = models.CharField(max_length=100, null=True, blank=True)

# CORRECT - one "empty" state: ""
name = models.CharField(max_length=100, blank=True, default="")
```

Exception: `null=True` is correct when the field has `unique=True` (multiple NULLs allowed, multiple empty strings not).

## Anti-Pattern 5: Fat Views, Thin Models

```python
# WRONG - business logic in view
def cancel_order(request, pk):
    order = get_object_or_404(Order, pk=pk)
    order.status = 'cancelled'
    order.cancelled_at = timezone.now()
    order.save()
    for item in order.items.all():
        item.product.stock += item.quantity
        item.product.save()
    send_cancellation_email(order)
    return redirect('order-list')

# CORRECT - business logic in model
class Order(models.Model):
    def cancel(self):
        self.status = 'cancelled'
        self.cancelled_at = timezone.now()
        self.save(update_fields=['status', 'cancelled_at'])
        for item in self.items.select_related('product').all():
            item.product.stock += item.quantity
            item.product.save(update_fields=['stock'])
        send_cancellation_email(self)

# View is thin:
def cancel_order(request, pk):
    order = get_object_or_404(Order, pk=pk)
    order.cancel()
    return redirect('order-list')
```

## Anti-Pattern 6: Hardcoded Secrets

```python
# WRONG
SECRET_KEY = 'django-insecure-abc123xyz'
DATABASES = {
    'default': {
        'PASSWORD': 'mypassword123',
    }
}
API_KEY = 'sk-abc123'

# CORRECT
import os
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
DATABASES = {
    'default': {
        'PASSWORD': os.environ['DB_PASSWORD'],
    }
}
API_KEY = os.environ['API_KEY']
```

## Anti-Pattern 7: Missing Pagination on List Endpoints

```python
# WRONG - returns entire table
class PostListView(generics.ListAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    # No pagination - can return millions of rows

# CORRECT
class PostListView(generics.ListAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    pagination_class = PageNumberPagination
```

Also set it globally in settings:
```python
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
}
```

## Anti-Pattern 8: Serializer Data Without Validation

```python
# WRONG - will raise AssertionError
serializer = PostSerializer(data=request.data)
post = serializer.save()  # Crash: must call is_valid() first

# CORRECT
serializer = PostSerializer(data=request.data)
if serializer.is_valid():
    post = serializer.save()
    return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)

# OR use raise_exception for cleaner code
serializer = PostSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
post = serializer.save()
```

## Anti-Pattern 9: Queries in Context Processors

```python
# WRONG - runs on EVERY request, including error pages
def site_settings(request):
    return {
        'settings': SiteSettings.objects.first(),  # Query on every page load
        'categories': Category.objects.all(),       # Another query on every page
    }

# CORRECT - use caching or lazy evaluation
from django.utils.functional import SimpleLazyObject

def site_settings(request):
    return {
        'settings': SimpleLazyObject(lambda: SiteSettings.objects.first()),
    }
```

## Anti-Pattern 10: Missing Database Indexes

```python
# WRONG - filtering on unindexed fields
class Post(models.Model):
    slug = models.SlugField(unique=True)  # unique=True adds index, good
    status = models.CharField(max_length=20)  # No index, filtered frequently
    author = models.ForeignKey(User, on_delete=models.CASCADE)  # FK auto-indexed
    created_at = models.DateTimeField(auto_now_add=True)  # No index, ordered frequently

# CORRECT - add indexes for filtered/ordered fields
class Post(models.Model):
    slug = models.SlugField(unique=True)
    status = models.CharField(max_length=20, db_index=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)

    class Meta:
        indexes = [
            models.Index(fields=['status', 'created_at']),  # Composite index
        ]
```

## Anti-Pattern 11: Missing CSRF Protection

```html
<!-- WRONG - form without CSRF token -->
<form method="post">
    <input type="text" name="title" />
    <button type="submit">Submit</button>
</form>

<!-- CORRECT -->
<form method="post">
    {% csrf_token %}
    <input type="text" name="title" />
    <button type="submit">Submit</button>
</form>
```

## Anti-Pattern 12: Using .save() Without update_fields

When updating specific fields, always pass `update_fields` to avoid race conditions and unnecessary writes:

```python
# WRONG - overwrites all fields, possible race condition
post.title = 'New Title'
post.save()

# CORRECT - only updates the specified fields
post.title = 'New Title'
post.save(update_fields=['title'])
```

## Anti-Pattern 13: Transactions with External Side Effects

```python
# WRONG - email sent even if transaction rolls back
from django.db import transaction

with transaction.atomic():
    order = Order.objects.create(total=100)
    send_confirmation_email(order)  # Sent, but order might roll back!
    payment = Payment.objects.create(order=order, amount=100)  # Might fail

# CORRECT - defer side effects until after commit
from django.db import transaction

with transaction.atomic():
    order = Order.objects.create(total=100)
    payment = Payment.objects.create(order=order, amount=100)

# Only runs if the transaction commits successfully
transaction.on_commit(lambda: send_confirmation_email(order))
```

## Anti-Pattern 14: Signals Without dispatch_uid

```python
# WRONG - can register multiple times, especially in tests
@receiver(post_save, sender=Post)
def notify_subscribers(sender, instance, created, **kwargs):
    if created:
        send_notifications(instance)

# CORRECT - dispatch_uid prevents duplicate registration
@receiver(post_save, sender=Post, dispatch_uid='post_notify_subscribers')
def notify_subscribers(sender, instance, created, **kwargs):
    if created:
        send_notifications(instance)
```

Also, register signals in `AppConfig.ready()`, not at module level:

```python
# apps.py
class BlogConfig(AppConfig):
    name = 'blog'

    def ready(self):
        import blog.signals  # Import signals here
```
规则

Core Django development rules. Enforces idiomatic Django patterns for ORM queries, views, URLs, templates, and project structure.

Core Django development rules. Enforces idiomatic Django patterns for ORM queries, views, URLs, templates, and project structure.

# Django Core Rules

You are working with **Django 5.x**. Follow these rules for every Django project.

## ORM: Always Optimize Related Object Access

When accessing related objects, use `select_related` (ForeignKey, OneToOne) or `prefetch_related` (ManyToMany, reverse FK) to prevent N+1 queries:

```python
# CORRECT - 1 query with JOIN
posts = Post.objects.select_related('author').all()
for post in posts:
    print(post.author.username)  # No extra query

# CORRECT - 2 queries (1 for posts, 1 for tags)
posts = Post.objects.prefetch_related('tags').all()
for post in posts:
    print(post.tags.all())  # No extra query

# WRONG - N+1 queries (1 + N for authors)
posts = Post.objects.all()
for post in posts:
    print(post.author.username)  # Query per post!
```

**Rule of thumb:**
- `select_related` for ForeignKey and OneToOneField (SQL JOIN)
- `prefetch_related` for ManyToManyField and reverse relations (separate query + Python join)
- Chain them: `Post.objects.select_related('author').prefetch_related('tags', 'comments__author')`

## ORM: Use .iterator() for Large Datasets

When processing large querysets, use `.iterator()` to avoid caching all results in memory:

```python
# CORRECT - streams results in chunks
for post in Post.objects.all().iterator(chunk_size=2000):
    process(post)

# WRONG - loads entire queryset into memory
for post in Post.objects.all():
    process(post)
```

## Models: Always Include __str__ and Meta

Every model must have `__str__` and a `Meta` class:

```python
class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']
        verbose_name_plural = 'posts'

    def __str__(self):
        return self.title
```

## Models: Use Appropriate Field Types

Do not use `CharField` for everything:

| Data | Correct field | Wrong |
|------|--------------|-------|
| Email | `EmailField` | `CharField` |
| URL | `URLField` | `CharField` |
| IP address | `GenericIPAddressField` | `CharField` |
| UUID | `UUIDField` | `CharField` |
| File path | `FilePathField` | `CharField` |
| Boolean | `BooleanField` | `IntegerField` or `CharField` |
| Date | `DateField` | `CharField` |
| Price/money | `DecimalField` | `FloatField` |
| Long text | `TextField` | `CharField(max_length=10000)` |
| Enum/choices | `CharField` with `TextChoices` | `CharField` with magic strings |

## Models: ForeignKey Always Needs on_delete

Every `ForeignKey` and `OneToOneField` must specify `on_delete`:

```python
# Common on_delete choices
author = models.ForeignKey(User, on_delete=models.CASCADE)       # Delete post when user deleted
category = models.ForeignKey(Category, on_delete=models.PROTECT)  # Prevent deletion if posts exist
editor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)  # Set to NULL
```

## Models: Do Not Use null=True on String Fields

For `CharField` and `TextField`, use `blank=True` with `default=""` instead of `null=True`. Having both NULL and empty string as "no data" creates ambiguity:

```python
# CORRECT
bio = models.TextField(blank=True, default="")

# WRONG - two possible "empty" values: NULL and ""
bio = models.TextField(null=True, blank=True)
```

Exception: `null=True` is correct for `unique=True` string fields (NULL != NULL in SQL).

## Views: Fat Models, Thin Views

Put business logic in models or managers, not in views:

```python
# CORRECT - logic in model
class Post(models.Model):
    def publish(self):
        self.status = 'published'
        self.published_at = timezone.now()
        self.save(update_fields=['status', 'published_at'])

# In view, just call:
post.publish()

# WRONG - logic in view
def publish_post(request, pk):
    post = get_object_or_404(Post, pk=pk)
    post.status = 'published'
    post.published_at = timezone.now()
    post.save()
```

## Views: Use get_queryset() for Request-Dependent Filtering

Override `get_queryset()` instead of setting `queryset` when filtering depends on the request:

```python
# CORRECT
class UserPostListView(ListView):
    def get_queryset(self):
        return Post.objects.filter(author=self.request.user).select_related('author')

# WRONG - queryset is evaluated once at class definition
class UserPostListView(ListView):
    queryset = Post.objects.filter(author=request.user)  # Error: request not available
```

## URLs: Use reverse() and reverse_lazy()

Never hardcode URLs. Use `reverse()` in functions and `reverse_lazy()` in class attributes:

```python
# CORRECT
from django.urls import reverse, reverse_lazy

class PostCreateView(CreateView):
    success_url = reverse_lazy('post-list')  # Class attribute: reverse_lazy

def redirect_to_post(request, pk):
    return redirect(reverse('post-detail', kwargs={'pk': pk}))  # Function: reverse

# WRONG
success_url = '/posts/'
return redirect(f'/posts/{pk}/')
```

## URLs: Use path() with Named Patterns

```python
# CORRECT
from django.urls import path

app_name = 'blog'
urlpatterns = [
    path('', views.PostListView.as_view(), name='post-list'),
    path('<int:pk>/', views.PostDetailView.as_view(), name='post-detail'),
    path('create/', views.PostCreateView.as_view(), name='post-create'),
]

# WRONG - url() is removed in Django 4.0+
from django.conf.urls import url
urlpatterns = [
    url(r'^posts/$', views.post_list),
]
```

## Templates: Do Not Run Queries in Templates

All database work should happen in views. Templates should receive pre-fetched data:

```python
# CORRECT - prefetch in view
def post_list(request):
    posts = Post.objects.select_related('author').prefetch_related('tags')
    return render(request, 'posts/list.html', {'posts': posts})

# WRONG - template triggers queries
# In template: {% for tag in post.tags.all %}  <- query per post!
```

## Settings: Use Environment Variables

Never hardcode secrets. Use `os.environ` or `django-environ`:

```python
import os

SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True'

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME', 'myapp'),
        'USER': os.environ.get('DB_USER', 'postgres'),
        'PASSWORD': os.environ['DB_PASSWORD'],
        'HOST': os.environ.get('DB_HOST', 'localhost'),
        'PORT': os.environ.get('DB_PORT', '5432'),
    }
}
```

## Migrations: Always Run After Model Changes

After any model change:
1. `python manage.py makemigrations`
2. Review the generated migration file
3. `python manage.py migrate`

Never edit migrations manually unless you know exactly what you're doing. Never delete migrations and re-create from scratch in production.

## Security Basics

- Always use `{% csrf_token %}` in POST forms
- Always validate and sanitize user input
- Use Django's ORM for queries (prevents SQL injection)
- Set `ALLOWED_HOSTS` in production
- Set `SECURE_SSL_REDIRECT = True` in production
- Use `@login_required` or `LoginRequiredMiddleware` (Django 5.1+)
规则

Django REST Framework rules for serializers, viewsets, permissions, pagination, and filtering. Auto-attaches when editing DRF-related Python files.

Django REST Framework rules for serializers, viewsets, permissions, pagination, and filtering. Auto-attaches when editing DRF-related Python files.

# Django REST Framework Rules

## Serializer Validation Is Mandatory

Always call `is_valid()` before accessing `validated_data` or calling `save()`:

```python
# CORRECT
serializer = PostSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
post = serializer.save(author=request.user)

# WRONG - raises AssertionError
serializer = PostSerializer(data=request.data)
post = serializer.save()
```

## Use ModelSerializer for Model-Backed APIs

```python
# CORRECT - auto-generates fields, validators, create/update
class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'author', 'created_at']
        read_only_fields = ['id', 'author', 'created_at']
```

Use plain `Serializer` only for non-model data (search results, aggregations, external API responses).

## Always Specify fields in Meta

Never use `fields = '__all__'` in production. It exposes all model fields including sensitive ones:

```python
# WRONG - exposes everything
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = '__all__'  # Includes password hash, is_superuser, etc.

# CORRECT - explicit fields
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'date_joined']
```

## Optimize Querysets for Serializers

Every serializer that accesses related objects needs an optimized queryset:

```python
class PostSerializer(serializers.ModelSerializer):
    author = UserSerializer(read_only=True)
    tags = TagSerializer(many=True, read_only=True)

    class Meta:
        model = Post
        fields = ['id', 'title', 'author', 'tags']

class PostViewSet(viewsets.ModelViewSet):
    serializer_class = PostSerializer

    def get_queryset(self):
        return Post.objects.select_related('author').prefetch_related('tags')
```

## Writable Nested Serializers Need Explicit create/update

DRF does not auto-handle writable nested data:

```python
class PostSerializer(serializers.ModelSerializer):
    tags = TagSerializer(many=True)

    def create(self, validated_data):
        tags_data = validated_data.pop('tags')
        post = Post.objects.create(**validated_data)
        for tag_data in tags_data:
            tag, _ = Tag.objects.get_or_create(**tag_data)
            post.tags.add(tag)
        return post

    def update(self, instance, validated_data):
        tags_data = validated_data.pop('tags', None)
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.save()
        if tags_data is not None:
            instance.tags.clear()
            for tag_data in tags_data:
                tag, _ = Tag.objects.get_or_create(**tag_data)
                instance.tags.add(tag)
        return instance

    class Meta:
        model = Post
        fields = ['title', 'content', 'tags']
```

## Use the Most Specific View Class

| Need | Use | Not |
|------|-----|-----|
| Full CRUD | `ModelViewSet` | manual `ViewSet` |
| Read-only list + detail | `ReadOnlyModelViewSet` | `ModelViewSet` |
| List + create | `ListCreateAPIView` | `ModelViewSet` |
| Single custom endpoint | `APIView` | `ViewSet` |

```python
# CORRECT - read-only, use ReadOnlyModelViewSet
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Category.objects.all()
    serializer_class = CategorySerializer

# WRONG - full CRUD when only reads needed
class CategoryViewSet(viewsets.ModelViewSet):
    queryset = Category.objects.all()
    serializer_class = CategorySerializer
```

## Permissions: Always Pair Authentication with Authorization

```python
# WRONG - authenticates but doesn't authorize
class PostViewSet(viewsets.ModelViewSet):
    authentication_classes = [TokenAuthentication]
    # Missing permission_classes - any token holder can do anything

# CORRECT
class PostViewSet(viewsets.ModelViewSet):
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]
```

Object-level permissions need `has_object_permission`:

```python
class IsAuthorOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.author == request.user
```

## Pagination Is Required on List Endpoints

Set it globally:
```python
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
}
```

Both `DEFAULT_PAGINATION_CLASS` and `PAGE_SIZE` must be set. One without the other does not work.

## Use Filter Backends for Filtering

```python
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]

    filterset_fields = ['status', 'author']           # ?status=published&author=1
    search_fields = ['title', 'content']              # ?search=django
    ordering_fields = ['created_at', 'title']         # ?ordering=-created_at
    ordering = ['-created_at']                        # Default ordering
```

Do NOT manually parse `request.query_params` for filtering -- use filter backends.

## Use perform_create for Setting Request-Dependent Fields

```python
class PostViewSet(viewsets.ModelViewSet):
    serializer_class = PostSerializer
    permission_classes = [IsAuthenticated]

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)
```

## Use Different Serializers per Action

```python
class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()

    def get_serializer_class(self):
        if self.action == 'list':
            return PostListSerializer    # Fewer fields, no nested
        if self.action == 'create':
            return PostCreateSerializer  # Write fields only
        return PostDetailSerializer      # Full detail with nested
```

## Custom Actions Use @action

```python
from rest_framework.decorators import action

class PostViewSet(viewsets.ModelViewSet):
    @action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
    def publish(self, request, pk=None):
        post = self.get_object()
        post.publish()
        return Response({'status': 'published'})

    @action(detail=False, methods=['get'])
    def recent(self, request):
        recent = self.get_queryset().order_by('-created_at')[:10]
        page = self.paginate_queryset(recent)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)
        serializer = self.get_serializer(recent, many=True)
        return Response(serializer.data)
```

## Router Registration

```python
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'posts', PostViewSet)
router.register(r'categories', CategoryViewSet)

# basename required when no queryset attribute
router.register(r'my-posts', UserPostViewSet, basename='user-post')

urlpatterns = [
    path('api/', include(router.urls)),
]
```

来源:https://github.com/RoninForge/roninforge-django