diff --git a/code-quality/SKILL.md b/code-quality/SKILL.md new file mode 100644 index 0000000..63e6a43 --- /dev/null +++ b/code-quality/SKILL.md @@ -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 + uv run ruff format --check + uv run pyright + uv run pytest -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) diff --git a/django-extensions/SKILL.md b/django-extensions/SKILL.md new file mode 100644 index 0000000..d659594 --- /dev/null +++ b/django-extensions/SKILL.md @@ -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 --signature --field-class + +# All methods including private +python manage.py list_model_info --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 +``` + +### 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 +python manage.py runscript --script-args arg1 arg2 +python manage.py runscript --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 diff --git a/django-forms/SKILL.md b/django-forms/SKILL.md new file mode 100644 index 0000000..4dd70ac --- /dev/null +++ b/django-forms/SKILL.md @@ -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_`): +- 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 diff --git a/django-models/SKILL.md b/django-models/SKILL.md new file mode 100644 index 0000000..fccfd35 --- /dev/null +++ b/django-models/SKILL.md @@ -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 diff --git a/django-templates/SKILL.md b/django-templates/SKILL.md new file mode 100644 index 0000000..426e5df --- /dev/null +++ b/django-templates/SKILL.md @@ -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-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//templatetags/_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 diff --git a/django/SKILL.md b/django/SKILL.md new file mode 100644 index 0000000..69b33b7 --- /dev/null +++ b/django/SKILL.md @@ -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("/", views.ArticleDetailView.as_view(), name="detail"), + path("/edit/", views.ArticleUpdateView.as_view(), name="edit"), + path("/", views.article_by_slug, name="by-slug"), + re_path(r"^archive/(?P[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 | +|---|---| +| `` | Zero or positive integer | +| `` | Any non-empty string without `/` | +| `` | ASCII letters, numbers, hyphens, underscores | +| `` | UUID string | +| `` | Any non-empty string including `/` | + +--- + +## Templates & Forms + +### Template Basics + +All templates extend a base: +```html +{% extends "base.html" %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} +

{{ article.title }}

+ {% for tag in article.tags.all %} + {{ tag.name }} + {% endfor %} +{% endblock %} + +{% block extra_scripts %} + +{% 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 +
+ {% csrf_token %} + {{ form.as_p }} + +
+ +{# Manual field rendering #} +
+ {{ form.title.label_tag }} + {{ form.title }} + {{ form.title.errors }} +
+``` + +**New in Django 5.2 — form widgets:** +- `forms.ColorInput` → `` +- `forms.SearchInput` → `` +- `forms.TelInput` → `` + +### 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'

{title}

{content}
' +``` + +--- + +## 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 + +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}, + }, +} +``` diff --git a/docs-sync/SKILL.md b/docs-sync/SKILL.md new file mode 100644 index 0000000..f4abe7d --- /dev/null +++ b/docs-sync/SKILL.md @@ -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 diff --git a/htmx-patterns/SKILL.md b/htmx-patterns/SKILL.md new file mode 100644 index 0000000..4707ea5 --- /dev/null +++ b/htmx-patterns/SKILL.md @@ -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: `` + +**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 `` - 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: `
` + +### 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 diff --git a/pytest-django-patterns/SKILL.md b/pytest-django-patterns/SKILL.md new file mode 100644 index 0000000..9e479af --- /dev/null +++ b/pytest-django-patterns/SKILL.md @@ -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__` +- 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 diff --git a/systematic-debugging/SKILL.md b/systematic-debugging/SKILL.md new file mode 100644 index 0000000..b66bab9 --- /dev/null +++ b/systematic-debugging/SKILL.md @@ -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 + +
+ {% csrf_token %} +
+``` + +### 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 + +``` + +```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