Add django skills

This commit is contained in:
Peter Annabel 2026-04-15 17:11:42 -05:00
parent 32c40397ab
commit e0d961076b
10 changed files with 1751 additions and 0 deletions

37
code-quality/SKILL.md Normal file
View File

@ -0,0 +1,37 @@
---
name: code-quality
description: Run code quality checks (ruff lint, ruff format, pyright, pytest) on a directory and report findings by severity. Use when the user wants to audit code quality, check for type errors, lint issues, or run automated checks on a path. Accepts a directory path as argument. Triggers on requests like "check code quality", "run quality checks", "/code-quality apps/".
---
# Code Quality Review
Review code quality in the directory provided by the user.
## Instructions
1. **Identify files to review**:
- Find all `.py` files in the directory
- Exclude migrations, `__pycache__`, and generated files
2. **Run automated checks**:
```bash
uv run ruff check <directory>
uv run ruff format --check <directory>
uv run pyright <directory>
uv run pytest <directory> -v
```
3. **Manual review checklist**:
- [ ] No `Any` types without justification
- [ ] Proper error handling (no silent exceptions)
- [ ] N+1 queries avoided (select_related/prefetch_related)
- [ ] Forms have proper validation
- [ ] Views return correct HTTP status codes
- [ ] HTMX partials handle HX-Request header
- [ ] Celery tasks are idempotent
- [ ] Tests use factories, not raw object creation
4. **Report findings** organized by severity:
- Critical (must fix)
- Warning (should fix)
- Suggestion (could improve)

View File

@ -0,0 +1,98 @@
---
name: django-extensions
description: Django-extensions management commands for project introspection, debugging, and development. Use when exploring URLs, models, settings, database schema, running scripts, or profiling performance. Triggers on questions about Django project structure, model fields, URL routes, or requests to run development servers.
---
# Django Extensions
This project has django-extensions installed. Use these commands to understand and interact with the Django project.
## Introspection
### Show URL Routes
```bash
python manage.py show_urls
```
### List Model Information
```bash
# All models
python manage.py list_model_info
# Specific model with signatures and field classes
python manage.py list_model_info --model <app.Model> --signature --field-class
# All methods including private
python manage.py list_model_info --model <app.Model> --all-methods --signature
```
### Print Settings
```bash
# All settings
python manage.py print_settings --format=pprint
# Wildcards supported
python manage.py print_settings AUTH*
python manage.py print_settings DATABASE*
python manage.py print_settings *_DIRS
```
### Show Permissions
```bash
python manage.py show_permissions
python manage.py show_permissions <app_label>
```
### Show Template Tags
```bash
python manage.py show_template_tags
```
## Development
### Enhanced Shell (shell_plus)
```bash
python manage.py shell_plus
python manage.py shell_plus --print-sql
```
Auto-imports all models. Use `--dont-load app1` to skip apps.
### Enhanced Dev Server (runserver_plus)
```bash
python manage.py runserver_plus
python manage.py runserver_plus --print-sql
```
Includes Werkzeug debugger for interactive debugging.
## Database
### SQL Diff (Compare Models to Schema)
```bash
python manage.py sqldiff -a # SQL differences
python manage.py sqldiff -a -t # Text differences (readable)
```
## Script Execution
### Run Scripts with Django Context
```bash
python manage.py runscript <script_name>
python manage.py runscript <script_name> --script-args arg1 arg2
python manage.py runscript <script_name> --traceback
```
Scripts in `scripts/` directory must define a `run()` function.
## Profiling
### Profile Server Requests
```bash
python manage.py runprofileserver --prof-path=/tmp/profiles
python manage.py runprofileserver --use-cprofile --prof-path=/tmp/profiles
python manage.py runprofileserver --kcachegrind --prof-path=/tmp/profiles
```
## Notes
- Model notation: `app.ModelName` (e.g., `core.EmailAccount`, `metabox.Thread`)
- Settings wildcards: `AUTH*`, `*_DIRS`, `DATABASE*`
- Commands run from project root

64
django-forms/SKILL.md Normal file
View File

@ -0,0 +1,64 @@
---
name: django-forms
description: Django form handling patterns including ModelForm, validation, clean methods, and HTMX form submission. Use when building forms, implementing validation, or handling form submission.
---
# Django Forms
## Philosophy
- Prefer ModelForm for model-backed forms
- Keep validation logic in forms, not views
- Always handle and display form errors
- Use `commit=False` when you need to modify the instance before saving
## Validation
**Field-level** (`clean_<field>`):
- Validate and transform a single field
- Return the cleaned value or raise `ValidationError`
- Use for: format checks, uniqueness, normalization
**Cross-field** (`clean`):
- Call `super().clean()` first
- Access multiple fields via `cleaned_data`
- Use `self.add_error(field, message)` for field-specific errors
- Use for: password confirmation, conditional requirements
## View Integration
- Check `request.method` explicitly
- Instantiate form with `request.POST` for POST, empty for GET
- Use `form.save(commit=False)` to set additional fields (e.g., author)
- Return redirect on success, re-render with form on error
**HTMX handling:**
- Check `request.headers.get("HX-Request")` for HTMX requests
- Return partial template on success/error for HTMX
- Use `HX-Trigger` header to notify other components
## Templates
- Display `form.non_field_errors` for cross-field errors
- Display `field.errors` for each field
- Use partial templates (`_form.html`) for HTMX responses
- Include loading indicator with `hx-indicator`
## Widgets
- Override in `Meta.widgets` dict
- Set HTML attributes via `attrs` parameter
- Common: `Textarea(attrs={"rows": 5})`, `DateTimeInput(attrs={"type": "datetime-local"})`
## Formsets
- Use `inlineformset_factory` for related model collections
- Validate both form and formset: `form.is_valid() and formset.is_valid()`
- Pass `instance` for editing existing parent objects
## Pitfalls
- Validating in views instead of forms
- Silently redirecting without checking `is_valid()`
- Forgetting `commit=False` when setting related fields
- Not displaying form errors to users

141
django-models/SKILL.md Normal file
View File

@ -0,0 +1,141 @@
---
name: django-models
description: Django model design patterns emphasizing fat models/thin views, QuerySet optimization, and domain logic encapsulation. Use when designing models, optimizing queries, implementing business logic, or working with the ORM.
---
# Django Model Patterns
## Core Philosophy: Fat Models, Thin Views
**Business logic belongs in models and managers, not views.** Views orchestrate workflows; models implement domain behavior. This principle creates testable, reusable code that stays maintainable as complexity grows.
**Good**: Model methods handle business rules, state transitions, validation
**Bad**: Views contain if/else logic for domain rules, calculate derived values
## Model Design
### Structure Your Models Around Domain Concepts
- Use `TextChoices`/`IntegerChoices` for status fields and enums
- Add `get_absolute_url()` for canonical object URLs
- Include `__str__()` for readable representations
- Set proper `ordering` in Meta for consistent default sorting
- Add database indexes for frequently filtered/sorted fields
- Use abstract base models for shared fields (timestamps, soft deletes, etc.)
### Field Selection Guidelines
- Use `blank=True, default=""` for optional text fields (avoid null)
- Use `null=True, blank=True` for optional foreign keys
- For unique optional fields, use `null=True` to avoid collision issues
- Leverage `JSONField` for flexible metadata (avoid creating many optional fields)
- Set appropriate `max_length` based on actual data needs
### Encapsulate Business Logic in Model Methods
- State transitions: `post.publish()`, `order.cancel()`
- Permission checks: `post.is_editable_by(user)`
- Complex calculations: `invoice.calculate_total()`
- Use properties for computed read-only values
- Specify `update_fields` when saving partial changes
## QuerySet Patterns: The Power of Composition
**Custom QuerySet classes are your secret weapon.** They make queries reusable, chainable, and testable.
### Pattern: QuerySet as Manager
```
Define a QuerySet subclass with domain-specific filter methods
Attach it to your model: objects = YourQuerySet.as_manager()
Chain methods for composable queries
```
### Benefits
- Reusable query logic across views, tasks, management commands
- Chainable methods enable expressive, readable queries
- Easy to test in isolation
- Encapsulates query complexity away from views
### Common QuerySet Methods
- Filtering by status/state
- Date range queries (recent, upcoming, expired)
- User-scoped queries (owned_by, visible_to)
- Combined lookups (published_and_recent)
## Query Optimization: Avoid N+1 Queries
### The Golden Rules
1. **select_related()**: Use for ForeignKey and OneToOneField (creates SQL JOIN)
2. **prefetch_related()**: Use for ManyToManyField and reverse ForeignKeys (separate query + Python join)
3. **only()**: Load specific fields when you don't need the whole object
4. **defer()**: Exclude heavy fields (TextField, JSONField) you won't use
5. **Prefetch()** object: Customize prefetch with filters and select_related
### Efficient Counting and Existence Checks
- Use `.exists()` instead of `if queryset:` or `if len(queryset):`
- Use `.count()` instead of `len(queryset.all())`
- Both perform database-level operations without loading objects
### Aggregation and Annotation
- `annotate()`: Add computed fields to each object (Count, Sum, Avg, etc.)
- `aggregate()`: Compute values across entire queryset
- Use `F()` expressions for database-level updates (`views=F('views') + 1`)
- Combine annotate with filter for "objects with at least N related items"
## Managers vs QuerySets
**Use QuerySets for chainable query logic.**
**Use Managers for model-level operations that don't return querysets.**
Manager: Think "factory methods" - `User.objects.create_user()`
QuerySet: Think "filters and transformations" - `Post.objects.published().recent()`
Most of the time, you want a custom QuerySet, not a custom Manager.
## Signals: Use Sparingly
Signals create implicit coupling and make code harder to follow. **Prefer explicit method calls.**
### When Signals Make Sense
- Audit logging (track all changes to a model)
- Cache invalidation (clear cache when model changes)
- Decoupling apps (third-party app needs to react to your models)
### When to Avoid Signals
- Business logic that should be in model methods
- Logic tightly coupled to the calling code (just call the function directly)
- Complex workflows (use explicit service layer instead)
**Rule of thumb**: If you control both the trigger and the reaction, don't use a signal.
## Migrations
### Workflow
- Run `makemigrations` after model changes
- Review generated migration files before applying
- Run `migrate` to apply migrations
- Migrations should be reversible when possible
### Data Migrations
Create with `makemigrations --empty app_name`. Use `apps.get_model()` to access models (not direct imports). Write both forward and reverse operations.
**Use data migrations for**: Populating new fields, transforming data, migrating between fields.
## Anti-Patterns to Avoid
### Query Anti-Patterns
- Iterating over objects and accessing relations without `select_related()`/`prefetch_related()`
- Using `if queryset:` instead of `.exists()`
- Using `len()` to count instead of `.count()`
- Loading entire objects when you only need specific fields
### Design Anti-Patterns
- Business logic in views instead of models
- Views performing calculations that belong in model methods
- Overusing signals for synchronous operations
- Creating new models when JSONField would suffice
- Forgetting to add indexes for filtered/sorted fields
## Integration
Works with:
- **pytest-django-patterns**: Factory-based model testing
- **celery-patterns**: Async operations on models (pass IDs, not instances)
- **django-forms**: ModelForm validation and saving

84
django-templates/SKILL.md Normal file
View File

@ -0,0 +1,84 @@
---
name: django-templates
description: Django template patterns including inheritance, partials, tags, and filters. Use when working with templates, creating reusable components, or organizing template structure.
---
# Django Template Patterns
## Template Organization
```
templates/
├── base.html # Root template with common structure
├── partials/ # Reusable fragments (navbar, footer, pagination)
├── components/ # UI components (button, card, modal)
└── <app>/ # App-specific templates
├── list.html # Full pages extend base.html
├── detail.html
├── _list.html # HTMX partials (underscore prefix)
└── _form.html
```
**Naming conventions:**
- Full pages: `list.html`, `detail.html`, `form.html`
- HTMX partials: `_list.html`, `_card.html` (underscore prefix)
- Shared partials: `partials/_navbar.html`, `partials/_pagination.html`
- Components: `components/_button.html`, `components/_modal.html`
## Template Inheritance
Use three-level inheritance for consistent layouts:
1. **base.html** - Site-wide structure (HTML skeleton, navbar, footer)
2. **Section templates** - Optional intermediate templates for sections with shared elements
3. **Page templates** - Individual pages that extend base or section templates
**Standard blocks to define in base.html:**
- `title` - Page title (use `{{ block.super }}` to append to site name)
- `content` - Main page content
- `extra_css` - Page-specific stylesheets
- `extra_js` - Page-specific scripts
## Partials and Components
**Partials** are template fragments included in other templates. Use for:
- Repeated content (pagination, empty states)
- HTMX responses that replace portions of the page
- Keeping templates DRY
**Components** are self-contained UI elements with configurable behavior. Pass context with:
- `{% include "components/_button.html" with text="Submit" variant="primary" %}`
- Add `only` to isolate context: `{% include "_card.html" with title=post.title only %}`
## Custom Tags and Filters
**When to create custom template tags:**
- `simple_tag` - Return a string value (e.g., active link class, formatted output)
- `inclusion_tag` - Render a template fragment (e.g., pagination component, user avatar)
**When to create custom filters:**
- Transform a single value (e.g., initials from name, percentage calculation)
- Chain with other filters for composable transformations
**Location:** `apps/<app>/templatetags/<app>_tags.py` (create `__init__.py` in templatetags directory)
## Anti-Patterns
**Move logic out of templates:**
- Complex conditionals belong in views or model methods
- Use `user.can_moderate` not `user.role == "admin" or user.role == "moderator"`
**Use URL names, not hardcoded paths:**
- Use `{% url 'posts:detail' pk=post.pk %}` not `/posts/{{ post.id }}/`
**Use CSS classes, not inline styles:**
- Use `class="error-message"` not `style="color: red;"`
**Handle empty states:**
- Always include `{% empty %}` clause in loops that might have no items
## Integration with Other Skills
- **htmx-patterns** - HTMX partial templates and dynamic UI
- **django-forms** - Form rendering and validation patterns
- **pytest-django-patterns** - Testing template rendering

764
django/SKILL.md Normal file
View File

@ -0,0 +1,764 @@
---
name: django
description: Guides development with Django 5.2.11. Use when writing models, views, URL routing, templates, forms, migrations, the Django admin, Django REST Framework (DRF) APIs, or pytest-django tests.
license: Internal Use Only
---
# Django 5.2.11
## Overview
Use this skill whenever working with Django — whether building views, writing models and migrations, working with templates and forms, configuring the admin, building REST APIs with DRF, or writing tests with pytest-django.
**Keywords**: django, models, views, urls, templates, forms, migrations, admin, rest framework, drf, serializers, viewsets, pytest, testing, async, orm, queryset
**Version**: Django 5.2.11 (LTS — security updates through 2028), Python 3.11, PostgreSQL
**Official docs**: https://docs.djangoproject.com/en/5.2/
---
## Views & URL Routing
### Function-Based Views
```python
from django.http import HttpResponse, Http404
from django.shortcuts import render, get_object_or_404, redirect
def article_detail(request, pk):
article = get_object_or_404(Article, pk=pk)
return render(request, "articles/detail.html", {"article": article})
```
### Async Views
Async views require an ASGI server to gain performance benefits. All handlers in a view must be either sync or async — never mixed.
```python
async def article_detail(request, pk):
article = await Article.objects.aget(pk=pk)
return render(request, "articles/detail.html", {"article": article})
```
### Class-Based Views
```python
from django.views import View
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
class ArticleListView(ListView):
model = Article
template_name = "articles/list.html"
context_object_name = "articles"
paginate_by = 20
def get_queryset(self):
return Article.objects.filter(published=True).order_by("-created_at")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["categories"] = Category.objects.all()
return context
```
**Generic CBV quick-reference:**
| Class | Purpose |
|---|---|
| `TemplateView` | Renders a template; override `get_context_data()` |
| `ListView` | List of objects; use `paginate_by` for pagination |
| `DetailView` | Single object by `pk` or `slug` |
| `CreateView` | Form to create an object; set `fields` or `form_class` |
| `UpdateView` | Form to edit an existing object |
| `DeleteView` | Confirm and delete an object |
| `RedirectView` | HTTP redirect; set `url` or `pattern_name` |
| `FormView` | Generic form handling without a model |
### Access Control Mixins & Decorators
```python
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth.decorators import login_required, permission_required
# CBV
class MyView(LoginRequiredMixin, View):
login_url = "/login/"
class AdminOnlyView(PermissionRequiredMixin, View):
permission_required = "myapp.change_article"
# FBV
@login_required
def my_view(request): ...
@permission_required("myapp.add_article")
def create_article(request): ...
```
### URL Configuration
```python
# urls.py
from django.urls import path, re_path, include
app_name = "articles" # namespace
urlpatterns = [
path("", views.ArticleListView.as_view(), name="list"),
path("<int:pk>/", views.ArticleDetailView.as_view(), name="detail"),
path("<int:pk>/edit/", views.ArticleUpdateView.as_view(), name="edit"),
path("<slug:slug>/", views.article_by_slug, name="by-slug"),
re_path(r"^archive/(?P<year>[0-9]{4})/$", views.archive, name="archive"),
]
```
**Including in the root URLconf:**
```python
urlpatterns = [
path("articles/", include("articles.urls")),
path("admin/", admin.site.urls),
path("api/", include("api.urls")),
]
```
**Reversing URLs:**
```python
from django.urls import reverse, reverse_lazy
reverse("articles:detail", args=[pk])
reverse("articles:list", query={"page": 2}) # Django 5.2 — adds ?page=2
reverse_lazy("articles:list") # safe for use in class attributes
```
**Path converters:**
| Converter | Matches |
|---|---|
| `<int:pk>` | Zero or positive integer |
| `<str:name>` | Any non-empty string without `/` |
| `<slug:slug>` | ASCII letters, numbers, hyphens, underscores |
| `<uuid:id>` | UUID string |
| `<path:rest>` | Any non-empty string including `/` |
---
## Templates & Forms
### Template Basics
All templates extend a base:
```html
{% extends "base.html" %}
{% block extra_head %}
<link rel="stylesheet" href="{% static 'myapp/style.css' %}">
{% endblock %}
{% block content %}
<h1>{{ article.title }}</h1>
{% for tag in article.tags.all %}
<span>{{ tag.name }}</span>
{% endfor %}
{% endblock %}
{% block extra_scripts %}
<script src="{% static 'myapp/app.js' %}"></script>
{% endblock %}
```
**Commonly used template tags:**
```html
{% url 'articles:detail' article.pk %}
{% static 'myapp/logo.png' %}
{% include '_sidebar.html' %}
{% with total=articles.count %}{{ total }}{% endwith %}
{% if user.is_authenticated %}...{% endif %}
{% csrf_token %}
```
**Built-in filters:**
```html
{{ value|default:"n/a" }}
{{ body|truncatewords:30 }}
{{ created_at|date:"M j, Y" }}
{{ price|floatformat:2 }}
{{ name|lower }}
```
### Forms
```python
from django import forms
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ["title", "body", "category", "published"]
widgets = {
"body": forms.Textarea(attrs={"rows": 10}),
}
def clean_title(self):
title = self.cleaned_data["title"]
if len(title) < 5:
raise forms.ValidationError("Title must be at least 5 characters.")
return title
def clean(self):
cleaned_data = super().clean()
# Cross-field validation here
return cleaned_data
```
**Handling a form in a view:**
```python
def create_article(request):
if request.method == "POST":
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save(commit=False)
article.author = request.user
article.save()
return redirect("articles:detail", pk=article.pk)
else:
form = ArticleForm()
return render(request, "articles/form.html", {"form": form})
```
**Rendering forms in templates:**
```html
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
{# Manual field rendering #}
<div class="field{% if form.title.errors %} error{% endif %}">
{{ form.title.label_tag }}
{{ form.title }}
{{ form.title.errors }}
</div>
```
**New in Django 5.2 — form widgets:**
- `forms.ColorInput``<input type="color">`
- `forms.SearchInput``<input type="search">`
- `forms.TelInput``<input type="tel">`
### Django 5.2 — `simple_block_tag`
Create template block tags that capture content:
```python
from django import template
register = template.Library()
@register.simple_block_tag
def card(content, title=""):
return f'<div class="card"><h2>{title}</h2>{content}</div>'
```
---
## Models
### Defining Models
```python
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
body = models.TextField()
author = models.ForeignKey(
"auth.User",
on_delete=models.SET_NULL,
null=True,
related_name="articles",
)
category = models.ForeignKey(
"Category",
on_delete=models.PROTECT,
related_name="articles",
)
tags = models.ManyToManyField("Tag", blank=True, related_name="articles")
published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"]
verbose_name_plural = "articles"
indexes = [models.Index(fields=["slug"])]
def __str__(self):
return self.title
def get_absolute_url(self):
from django.urls import reverse
return reverse("articles:detail", args=[self.pk])
```
**`on_delete` options:**
| Option | Behavior |
|---|---|
| `CASCADE` | Delete child when parent is deleted |
| `PROTECT` | Raise `ProtectedError` if related objects exist |
| `SET_NULL` | Set FK to NULL (requires `null=True`) |
| `SET_DEFAULT` | Set FK to field default |
| `DO_NOTHING` | Do nothing (risks integrity errors) |
### Django 5.2 — Composite Primary Keys
```python
class Release(models.Model):
pk = models.CompositePrimaryKey("version", "name")
version = models.IntegerField()
name = models.CharField(max_length=20)
```
### QuerySet Patterns
```python
# Filtering
Article.objects.filter(published=True, author__username="john")
Article.objects.exclude(created_at__year=2023)
Article.objects.filter(title__icontains="django")
# get_or_create / update_or_create
article, created = Article.objects.get_or_create(
slug="my-article",
defaults={"title": "My Article", "body": "..."},
)
# Aggregation
from django.db.models import Count, Avg, Q
Article.objects.annotate(comment_count=Count("comments"))
Article.objects.aggregate(avg_views=Avg("views"))
# Complex filters with Q
Article.objects.filter(
Q(title__icontains="django") | Q(body__icontains="django")
)
# F expressions (compare fields without fetching)
from django.db.models import F
Article.objects.filter(views__gt=F("likes") * 2)
# select_related (SQL JOIN for FK/OneToOne)
Article.objects.select_related("author", "category")
# prefetch_related (separate query for M2M/reverse FK)
Article.objects.prefetch_related("tags", "comments")
# Bulk operations
Article.objects.filter(published=False).update(published=True)
Article.objects.bulk_create([Article(...), Article(...)])
# Async ORM (for async views)
article = await Article.objects.aget(pk=pk)
articles = [a async for a in Article.objects.filter(published=True)]
```
### Migrations
```bash
python manage.py makemigrations # Generate migration files
python manage.py migrate # Apply migrations
python manage.py showmigrations # Show status
python manage.py sqlmigrate myapp 0003 # Preview SQL
python manage.py migrate myapp 0002 # Roll back to 0002
```
**Data migration:**
```python
# Create empty migration, then add:
from django.db import migrations
def populate_slugs(apps, schema_editor):
Article = apps.get_model("articles", "Article") # always use historical model
for article in Article.objects.all():
article.slug = article.title.lower().replace(" ", "-")
article.save()
class Migration(migrations.Migration):
dependencies = [("articles", "0003_article_slug")]
operations = [migrations.RunPython(populate_slugs, migrations.RunPython.noop)]
```
---
## Django Admin
### Registering Models
```python
from django.contrib import admin
from .models import Article, Category
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ["title", "author", "category", "published", "created_at"]
list_filter = ["published", "category", "created_at"]
search_fields = ["title", "body", "author__username"]
search_help_text = "Search by title, body, or author username"
readonly_fields = ["created_at", "updated_at"]
ordering = ["-created_at"]
list_per_page = 50
fieldsets = [
(None, {"fields": ["title", "slug", "author", "category"]}),
("Content", {"fields": ["body", "tags"]}),
("Publishing", {"fields": ["published"]}),
("Metadata", {"classes": ["collapse"], "fields": ["created_at", "updated_at"]}),
]
@admin.action(description="Mark selected articles as published")
def make_published(self, request, queryset):
queryset.update(published=True)
self.message_user(request, "Selected articles marked as published.")
actions = ["make_published"]
def save_model(self, request, obj, form, change):
if not change:
obj.author = request.user
super().save_model(request, obj, form, change)
```
### Inlines
```python
class CommentInline(admin.TabularInline):
model = Comment
extra = 0
readonly_fields = ["created_at"]
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
inlines = [CommentInline]
```
---
## Django REST Framework
### Setup
```python
# settings.py
INSTALLED_APPS = [..., "rest_framework", "rest_framework.authtoken"]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
"rest_framework.authentication.SessionAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}
```
### Serializers
```python
from rest_framework import serializers
from .models import Article
class ArticleSerializer(serializers.ModelSerializer):
author_username = serializers.CharField(source="author.username", read_only=True)
comment_count = serializers.SerializerMethodField()
class Meta:
model = Article
fields = ["id", "title", "slug", "body", "author_username",
"published", "created_at", "comment_count"]
read_only_fields = ["id", "slug", "created_at"]
def get_comment_count(self, obj):
return obj.comments.count()
def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError("Title must be at least 5 characters.")
return value
def validate(self, data):
# Cross-field validation
return data
def create(self, validated_data):
validated_data["author"] = self.context["request"].user
return super().create(validated_data)
```
### ViewSets & Routers
```python
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Article.objects.filter(published=True).select_related("author")
@action(detail=False, methods=["get"])
def my_articles(self, request):
qs = Article.objects.filter(author=request.user)
serializer = self.get_serializer(qs, many=True)
return Response(serializer.data)
@action(detail=True, methods=["post"])
def publish(self, request, pk=None):
article = self.get_object()
article.published = True
article.save()
return Response({"status": "published"})
```
```python
# urls.py
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r"articles", views.ArticleViewSet, basename="article")
urlpatterns = router.urls
```
**ViewSet types:**
| Class | Actions included |
|---|---|
| `ViewSet` | None — define all manually |
| `GenericViewSet` | None — mix in DRF mixins |
| `ReadOnlyModelViewSet` | `list`, `retrieve` |
| `ModelViewSet` | `list`, `create`, `retrieve`, `update`, `partial_update`, `destroy` |
### Per-View Auth/Permission Override
```python
from rest_framework.permissions import AllowAny, IsAdminUser
class ArticleViewSet(viewsets.ModelViewSet):
def get_permissions(self):
if self.action in ["list", "retrieve"]:
return [AllowAny()]
return [IsAuthenticated()]
```
### Bearer Token Authentication
For requests requiring a bearer token (e.g., from Vision-Frontend to Vision-BackendAPI):
```python
# Include in request headers:
# Authorization: Bearer <token>
class BearerTokenAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return None
token = auth_header.split(" ", 1)[1]
# Validate token against Wristband or token store
...
```
---
## Testing with pytest-django
### Configuration
```ini
# pytest.ini or pyproject.toml [tool.pytest.ini_options]
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings.test
```
### Database Access
Database access is blocked by default. Grant it via mark or fixture:
```python
import pytest
@pytest.mark.django_db
def test_article_creation():
article = Article.objects.create(title="Hello", body="World")
assert article.pk is not None
@pytest.mark.django_db(transaction=True)
def test_with_transactions():
# Needed when testing code that uses transactions explicitly
...
@pytest.mark.django_db(reset_sequences=True)
def test_with_reset_sequences():
# Needed when auto-increment IDs matter
...
```
### Key Fixtures
```python
def test_view(client):
"""Anonymous test client"""
response = client.get("/articles/")
assert response.status_code == 200
def test_authenticated(client, django_user_model):
"""Log in and test protected views"""
user = django_user_model.objects.create_user(username="tester", password="pass")
client.force_login(user)
response = client.get("/dashboard/")
assert response.status_code == 200
def test_request_factory(rf):
"""RequestFactory — builds request objects without the middleware stack"""
request = rf.get("/articles/")
request.user = AnonymousUser()
response = ArticleListView.as_view()(request)
assert response.status_code == 200
def test_email(mailoutbox):
"""Assert emails sent during a test"""
send_welcome_email("user@example.com")
assert len(mailoutbox) == 1
assert mailoutbox[0].subject == "Welcome!"
def test_with_settings(settings):
"""Override settings within a test"""
settings.DEBUG = True
settings.CACHES = {"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}}
...
```
### DRF Testing
```python
from rest_framework.test import APIClient
@pytest.fixture
def api_client():
return APIClient()
@pytest.mark.django_db
def test_article_api(api_client, django_user_model):
user = django_user_model.objects.create_user(username="tester", password="pass")
api_client.force_authenticate(user=user)
response = api_client.get("/api/articles/")
assert response.status_code == 200
assert "results" in response.data
```
### Fixtures with Database Access
```python
@pytest.fixture
def article(db):
"""Use the db fixture (not the mark) when a fixture needs DB access"""
return Article.objects.create(title="Test Article", body="Body")
@pytest.fixture
def published_article(article):
article.published = True
article.save()
return article
```
---
## Django 5.2 — Notable New Features
| Feature | Details |
|---|---|
| **Composite Primary Keys** | `CompositePrimaryKey("field1", "field2")` on the `pk` attribute |
| **Auto model imports in shell** | `manage.py shell` imports all models automatically |
| **`reverse()` `query` argument** | `reverse("name", query={"page": 2})` appends query string |
| **`HttpResponse.text`** | String representation of response content |
| **`HttpRequest.get_preferred_type()`** | Query client's preferred media type |
| **`preserve_request` on redirects** | Preserves HTTP method on `redirect()` |
| **`AlterConstraint` migration op** | Change constraint metadata without drop/recreate |
| **New form widgets** | `ColorInput`, `SearchInput`, `TelInput` |
| **`simple_block_tag` decorator** | Create block-capturing template tags |
| **Async auth methods** | `aauthenticate()`, `alogin()`, `alogout()`, `ahas_perm()` |
| **`CharField.max_length` optional** | On SQLite only; still required on PostgreSQL |
| **`QuerySet.explain()` options** | `memory` and `serialize` kwargs on PostgreSQL 17+ |
---
## Settings Reference
### Database (PostgreSQL)
```python
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": env("DB_NAME"),
"USER": env("DB_USER"),
"PASSWORD": env("DB_PASSWORD"),
"HOST": env("DB_HOST", default="127.0.0.1"),
"PORT": env("DB_PORT", default="5432"),
}
}
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
```
### Security Checklist
```python
SECRET_KEY = env("DJANGO_SECRET_KEY") # Never hardcode
DEBUG = env.bool("DEBUG", default=False)
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
# HTTPS in production
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SECURE_CONTENT_TYPE_NOSNIFF = True
```
### Static & Media Files
```python
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
```
### Logging
```python
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {"class": "logging.StreamHandler"},
},
"root": {
"handlers": ["console"],
"level": "WARNING",
},
"loggers": {
"django": {"handlers": ["console"], "level": "INFO", "propagate": False},
},
}
```

31
docs-sync/SKILL.md Normal file
View File

@ -0,0 +1,31 @@
---
name: docs-sync
description: Check if documentation is in sync with code. Use when the user wants to verify that documentation matches current code, find outdated docs, or audit documentation accuracy. Triggers on requests like "check docs", "sync documentation", "are the docs up to date", "/docs-sync".
---
# Documentation Sync
Check if documentation matches the current code state.
## Instructions
1. **Find recent code changes**:
```bash
git log --since="30 days ago" --name-only --pretty=format: -- "*.py" "*.ts" "*.tsx" | sort -u
```
2. **Find related documentation**:
- Search `/docs/` for files mentioning changed code
- Check README files near changed code
- Look for docstrings in changed files
3. **Verify documentation accuracy**:
- Do code examples still work?
- Are API signatures correct?
- Are field types up to date?
4. **Report only actual problems**:
- Only flag things that are WRONG, not missing
- Don't suggest documentation for documentation's sake
5. **Output a checklist** of documentation that needs updating

154
htmx-patterns/SKILL.md Normal file
View File

@ -0,0 +1,154 @@
---
name: htmx-patterns
description: HTMX patterns for Django including partial templates, hx-* attributes, and dynamic UI without JavaScript. Use when building interactive UI, handling AJAX requests, or creating dynamic components.
---
# HTMX Patterns for Django
## Core Philosophy
- Server renders HTML, not JSON - HTMX requests return HTML fragments, not data
- Partial templates for dynamic updates - separate `_partial.html` files for HTMX responses
- Progressive enhancement - pages work without JavaScript, HTMX enhances UX
- Minimal client-side complexity - let the server do the heavy lifting
## Critical Hints & Reminders
### UX Best Practices
**Always include loading indicators**
- Use `hx-indicator` to show loading states during requests
- Users should never wonder if their action worked
- Example: `<button hx-get="/data/" hx-indicator="#spinner">Load</button>`
**Always provide user feedback**
- Use Django messages framework for success/error feedback
- Return error messages in HTMX responses, not silent failures
- Show what happened after an action completes
**Handle errors gracefully**
- Return proper HTTP status codes (400 for validation errors, 500 for server errors)
- Render form errors in partial templates
- Don't swallow exceptions - log and show user-friendly messages
### Django-Specific Patterns
**Always detect HTMX requests**
- Check `request.headers.get("HX-Request")` to detect HTMX requests
- Return partial templates for HTMX, full page templates otherwise
- Pattern: `if request.headers.get("HX-Request"): return render(request, "_partial.html", context)`
**Always return partials for HTMX**
- HTMX requests should return `_partial.html` templates, not full pages with `base.html`
- Full page responses to HTMX requests break the UX and send duplicate HTML
- Partials should be self-contained HTML fragments
**Always validate request.method**
- Check `request.method == "POST"` before processing form data
- Return proper status codes (405 Method Not Allowed for wrong methods)
**CSRF is already configured globally**
- The base template has `hx-headers` on `<body>` - no need to add CSRF tokens to individual forms
- All HTMX requests automatically include the CSRF token
### Template Organization
**Naming convention**
- Partials: `_partial.html` (underscore prefix)
- Full pages: `page.html` (no prefix)
- Example: `posts/list.html` (full page) includes `posts/_list.html` (partial)
**Structure**
- Full page template extends `base.html` and includes partial
- Partial contains only the dynamic HTML fragment
- HTMX targets the partial's container div
**Keep partials focused**
- Each partial should represent one logical UI component
- Avoid partials that are too large or do too much
- Compose larger UIs from multiple smaller partials
## Django View Patterns
### HTMX Detection
Check the `HX-Request` header to detect HTMX requests:
```python
def my_view(request):
context = {...}
if request.headers.get("HX-Request"):
return render(request, "app/_partial.html", context)
return render(request, "app/full_page.html", context)
```
### Form Handling Pattern
Key points:
- Validate form normally
- On success: return partial with new data OR trigger client-side event
- On error: return partial with form errors
- Always handle both HTMX and non-HTMX cases
```python
def create_view(request):
if request.method == "POST":
form = MyForm(request.POST)
if form.is_valid():
obj = form.save()
if request.headers.get("HX-Request"):
return render(request, "app/_item.html", {"item": obj})
return redirect("app:list")
# Return form with errors
if request.headers.get("HX-Request"):
return render(request, "app/_form.html", {"form": form})
else:
form = MyForm()
return render(request, "app/create.html", {"form": form})
```
## Response Headers Reference
HTMX respects special response headers for client-side behavior:
### HX-Trigger
Trigger client-side events after response
- Use case: Update other parts of page after action
- Example: `response["HX-Trigger"] = "itemCreated"`
- Template listens: `<div hx-get="/count/" hx-trigger="itemCreated from:body">`
### HX-Redirect
Client-side redirect
- Use case: Redirect after successful action
- Example: `response["HX-Redirect"] = reverse("app:detail", args=[obj.pk])`
### HX-Retarget / HX-Reswap
Override hx-target and hx-swap from server
- Use case: Different targets for success vs error
- Success: `response["HX-Retarget"] = "#main"`
- Error: Return partial without changing target (targets the form)
### HX-Refresh
Force full page refresh
- Use case: Major state change that affects whole page
- Example: `response["HX-Refresh"] = "true"`
## Common Pitfalls
- **Missing loading indicators**: Always use `hx-indicator` - users click multiple times without feedback
- **Full pages in HTMX responses**: Return `_partial.html`, not full pages with `base.html` - check `HX-Request` header
- **Not handling form errors**: Always return the form with errors on validation failure, not just the success case
- **Not disabling buttons**: Use `hx-disabled-elt="this"` to prevent duplicate submissions
- **N+1 queries**: HTMX views need `select_related()`/`prefetch_related()` just like regular views
## Integration with Other Skills
- **django-templates**: Partial template organization and inheritance patterns
- **django-forms**: HTMX form submission and validation
- **django-extensions**: Use `show_urls` to verify HTMX endpoints
- **pytest-django-patterns**: Testing HTMX endpoints and headers
- **systematic-debugging**: Debug HTMX request/response issues

View File

@ -0,0 +1,173 @@
---
name: pytest-django-patterns
description: pytest-django testing patterns, Factory Boy, fixtures, and TDD workflow. Use when writing tests, creating test factories, or following TDD red-green-refactor cycle.
---
# pytest-django Testing Patterns
## TDD Workflow (RED-GREEN-REFACTOR)
**Always follow this cycle:**
1. **RED**: Write a failing test first that describes desired behavior
2. **GREEN**: Write minimal code to make the test pass
3. **REFACTOR**: Clean up code while keeping tests green
4. **REPEAT**: Never write production code without a failing test
**Critical rule**: If implementing a feature or fixing a bug, write the test BEFORE touching production code.
## Essential pytest-django Patterns
### Database Access
- Use `@pytest.mark.django_db` on any test touching the database
- Apply to entire module: `pytestmark = pytest.mark.django_db`
- Transactions roll back automatically after each test
### Fixtures for Test Data
**Use Factory Boy for models, pytest fixtures for setup:**
- **Factories**: Create model instances with realistic data (`UserFactory()`)
- Use `factory.Sequence()` for unique fields
- Use `factory.Faker()` for realistic fake data
- Use `factory.SubFactory()` for foreign keys
- Use `@factory.post_generation` for M2M relationships
- **Fixtures**: Setup clients, auth state, or shared resources
- `client` fixture: Django test client
- Create `auth_client` fixture: `client.force_login(user)` for authenticated requests
- Define in `conftest.py` for reuse across test files
### Test Organization
**Structure tests to mirror app structure:**
```
tests/
├── apps/
│ └── posts/
│ ├── test_models.py
│ ├── test_views.py
│ └── test_forms.py
├── factories.py
└── conftest.py
```
**Group related tests in classes:**
- Name classes `TestComponentName` (e.g., `TestPostListView`)
- Name test methods descriptively: `test_<action>_<expected_outcome>`
- Use `@pytest.mark.parametrize` for testing multiple scenarios
## What to Test
### Views
- **Status codes**: Correct HTTP responses (200, 404, 302)
- **Authentication**: Authenticated vs anonymous behavior
- **Authorization**: User can only access their own data
- **Context data**: Correct objects passed to template
- **Side effects**: Database changes, emails sent, tasks queued
- **HTMX**: Check `HTTP_HX_REQUEST` header returns partial template
### Forms
- **Validation**: Valid data passes, invalid data fails with correct errors
- **Edge cases**: Empty fields, max lengths, unique constraints
- **Clean methods**: Custom validation logic works
- **Save behavior**: Objects created/updated correctly
### Models
- **Methods**: `__str__`, custom methods return expected values
- **Managers/QuerySets**: Custom filtering works correctly
- **Constraints**: Database-level validation enforced
- **Signals**: Pre/post save hooks execute correctly
### Celery Tasks
- **Mock external calls**: Patch HTTP requests, email sending, etc.
- **Test logic only**: Don't test actual async execution
- **Idempotency**: Running task multiple times is safe
## Django-Specific Testing Patterns
### Testing HTMX Responses
Check partial template rendered when `HX-Request` header present:
- Pass `HTTP_HX_REQUEST="true"` to client request
- Assert `response.templates` contains partial template name
### Testing Permissions
Create authenticated vs anonymous client fixtures:
- Test redirect/403 for unauthorized access
- Test success for authorized access
### Testing QuerySets
Verify efficient queries:
- Create test data with factories
- Execute query
- Assert correct objects returned/excluded
- Verify related objects loaded with `select_related()`/`prefetch_related()`
### Testing Forms with Model Instances
Pass instance to form for updates:
- `form = MyForm(data=new_data, instance=existing_obj)`
- Verify `form.save()` updates, doesn't create
## Common Patterns
**Parametrize multiple scenarios:**
Use `@pytest.mark.parametrize("input,expected", [...])` for testing various inputs
**Mock external services:**
Use `mocker.patch()` to avoid actual HTTP calls, emails, file operations
**Check database changes:**
- Assert `Model.objects.filter(...).exists()` after creation
- Assert `Model.objects.count() == expected` for deletions
- Use `refresh_from_db()` to verify updates
**Test error handling:**
- Invalid form data produces correct errors
- Failed operations return error responses
- User sees appropriate error messages
## Running Tests
```bash
uv run pytest # All tests
uv run pytest -x # Stop on first failure
uv run pytest --lf # Run last failed
uv run pytest -x --lf # Stop first, last failed only
uv run pytest -k "test_name" # Run tests matching pattern
uv run pytest tests/apps/posts/ # Specific directory
uv run pytest --cov=apps # With coverage report
```
## Common Pitfalls
- **Forgetting `@pytest.mark.django_db`**: Results in "Database access not allowed" errors
- **Not using factories**: Creating instances manually is verbose and brittle
- **Testing implementation**: Test behavior and outcomes, not internal implementation details
- **Skipping TDD**: Writing tests after code means tests follow implementation, missing edge cases
- **Over-mocking**: Mock external dependencies, not your own code
- **Testing framework code**: Don't test Django's ORM, form validation, etc. Test YOUR logic
## Setup Requirements
**In `pyproject.toml`:**
```toml
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings.test"
python_files = ["test_*.py"]
addopts = ["--reuse-db", "-ra"]
```
**In `conftest.py`:**
Define shared fixtures (auth_client, common factories, etc.)
## Integration with Other Skills
- **systematic-debugging**: When fixing bugs, write failing test first to reproduce
- **django-models**: Test custom managers, QuerySets, and model methods
- **django-forms**: Test form validation, clean methods, and save behavior
- **celery-patterns**: Test task logic with mocked external dependencies

View File

@ -0,0 +1,205 @@
---
name: systematic-debugging
description: Four-phase debugging methodology with root cause analysis for Django. Use when investigating bugs, fixing test failures, or troubleshooting unexpected behavior. Emphasizes NO FIXES WITHOUT ROOT CAUSE FIRST.
---
# Systematic Debugging for Django
## Core Principle
**NO FIXES WITHOUT ROOT CAUSE FIRST**
Never apply patches that mask underlying problems. Understand WHY something fails before attempting to fix it.
## Four-Phase Framework
### Phase 1: Reproduce and Investigate
Before touching any code:
1. **Write a failing test** - Captures the bug behavior
2. **Read error messages thoroughly** - Every word matters
3. **Examine recent changes** - `git diff`, `git log`
4. **Trace data flow** - Follow the call chain to find where bad values originate
```python
# Write a failing test first
@pytest.mark.django_db
def test_bug_reproduction():
"""Reproduces issue #123."""
user = UserFactory()
response = Client().post("/profile/", {"bio": "New"})
assert response.status_code == 200 # Currently failing
```
### Phase 2: Isolate
Narrow down the problem:
```python
# Add strategic logging
import logging
logger = logging.getLogger(__name__)
def problematic_view(request):
logger.debug(f"Method: {request.method}")
logger.debug(f"POST: {request.POST}")
logger.debug(f"User: {request.user}")
form = MyForm(request.POST)
logger.debug(f"Valid: {form.is_valid()}")
logger.debug(f"Errors: {form.errors}")
```
### Phase 3: Identify Root Cause
- Read the full stack trace
- Use debugger to inspect state
- Check what assumptions are violated
### Phase 4: Fix and Verify
1. Implement fix at the root cause
2. Run reproduction test (should pass)
3. Run full test suite
4. Verify manually if needed
## Django Debug Tools
### Django Debug Toolbar
```python
# settings/dev.py
INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]
INTERNAL_IPS = ["127.0.0.1"]
```
Check SQL panel for N+1 queries, slow queries > 10ms.
### Python Debugger
```python
def problematic_view(request):
breakpoint() # Execution stops here
# Commands: n(ext), s(tep), c(ontinue), p var, q(uit)
```
```bash
# Drop into debugger on test failure
uv run pytest --pdb -x
```
### Query Debugging
```python
# Log all SQL queries
LOGGING = {
"loggers": {
"django.db.backends": {"level": "DEBUG", "handlers": ["console"]},
},
}
# Count queries in tests
from django.test.utils import CaptureQueriesContext
from django.db import connection
def test_no_n_plus_one():
with CaptureQueriesContext(connection) as ctx:
list(Post.objects.select_related("author"))
assert len(ctx) <= 2
```
## Common Django Issues
### N+1 Queries
```python
# Problem
for post in Post.objects.all():
print(post.author.email) # Query per post!
# Fix
for post in Post.objects.select_related("author"):
print(post.author.email) # Single query
```
### Form Not Saving
```python
# Check these:
# 1. form.is_valid() returns True?
# 2. form.save() called?
# 3. If commit=False, did you call .save() on instance?
def debug_form(request):
form = MyForm(request.POST)
print(f"Valid: {form.is_valid()}")
print(f"Errors: {form.errors}")
```
### CSRF 403 Errors
```html
<!-- Check: csrf_token in form -->
<form method="post">
{% csrf_token %}
</form>
```
### Migration Issues
```bash
uv run python manage.py showmigrations
uv run python manage.py migrate app_name 0001 --fake
```
## Debugging Celery
```python
# Run synchronously for debugging
my_task(arg) # Direct call, not .delay()
# Or set in settings
CELERY_TASK_ALWAYS_EAGER = True
```
## Debugging HTMX
```html
<script>htmx.logAll();</script>
```
```python
def view(request):
print(f"HTMX: {request.headers.get('HX-Request')}")
```
## Checklist
Before claiming fixed:
- [ ] Root cause identified
- [ ] Reproduction test passes
- [ ] Full test suite passes (`uv run pytest`)
- [ ] No type errors (`uv run pyright`)
- [ ] No lint errors (`uv run ruff check .`)
## Red Flags
Stop if you're thinking:
- "Quick fix now, investigate later"
- "One more attempt" (after 3+ failures)
- "This should work" (without understanding why)
Three consecutive failed fixes = architectural problem. Stop and discuss.
## Integration with Other Skills
- **pytest-django-patterns**: Write reproduction tests
- **django-models**: Debug QuerySet issues
- **celery-patterns**: Debug async task failures
- **htmx-alpine-patterns**: Debug HTMX requests
- **django-extensions**: Use `show_urls`, `list_model_info`, and `shell_plus` for project introspection
- **skill-creator**: Create debugging-specific skills for recurring issues