765 lines
21 KiB
Markdown
765 lines
21 KiB
Markdown
---
|
|
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},
|
|
},
|
|
}
|
|
```
|