Documentation

Welcome to the comprehensive Django documentation. Here, you'll find detailed guides and tutorials for building web applications with Django.

Module 1: Django Installation and Project Setup
Introduction to Django

Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. It follows the Model-View-Template (MVT) architectural pattern and includes many built-in features for common web development tasks.

Key features of Django:

  • Batteries Included - Authentication, admin interface, forms, ORM, and more
  • MTV Architecture - Clear separation of concerns
  • ORM - Powerful Object-Relational Mapping
  • Admin Interface - Automatic admin panel generation
  • Security - Built-in protection against common vulnerabilities
  • Scalability - Designed to handle high-traffic sites
  • Community - Large ecosystem of packages and apps
Virtual Environment

Always use a virtual environment for Django projects:


# Create virtual environment
python -m venv venv

# Activate on Windows
venv\Scripts\activate

# Activate on macOS/Linux
source venv/bin/activate

# Deactivate
deactivate
Install Django

# Install Django
pip install Django

# Install additional useful packages
pip install djangorestframework django-cors python-decouple django-environ
pip install pillow celery redis psycopg2-binary
Create Django Project

# Create new Django project
django-admin startproject myproject

# Create Django app
cd myproject
python manage.py startapp myapp

# Run migrations
python manage.py migrate

# Create superuser
python manage.py createsuperuser

# Run development server
python manage.py runserver
Verify Installation

# test_django.py
import django
print(django.get_version())
print(django.VERSION)
Django Project Structure

myproject/
├── manage.py
├── myproject/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   ├── wsgi.py
│   └── asgi.py
├── myapp/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   ├── views.py
│   ├── migrations/
│   └── templates/
│       └── myapp/
├── templates/
├── static/
│   ├── css/
│   ├── js/
│   └── images/
└── requirements.txt
Settings Configuration

# myproject/settings.py
import os
from pathlib import Path

# Build paths
BASE_DIR = Path(__file__).resolve().parent.parent

# Security settings
SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-key')
DEBUG = os.environ.get('DEBUG', 'True')

ALLOWED_HOSTS = ['localhost', '127.0.0.0', '[::1]']

# Application definition
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp',
]

# Middleware
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.ClickjackingMiddleware',
]

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

# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True

# Static files
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'

# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

# Templates
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# Logging
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': BASE_DIR / 'logs/django.log',
        },
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
        },
    },
}

# Environment-specific settings
if os.environ.get('DJANGO_ENV') == 'production':
    DEBUG = False
    ALLOWED_HOSTS = ['yourdomain.com']
    SECURE_SSL_REDIRECT = True
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SECURE = True
    SECURE_BROWSER_XSS_FILTER = True
    X_FRAME_OPTIONS = 'DENY'
Module 2: Models and Database Design
Creating Models

Models are the single, definitive source of information about your data. They contain the essential fields and behaviors of the data you're storing.


# myapp/models.py
from django.db import models
from django.contrib.auth.models import User

class Category(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        verbose_name_plural = "Categories"
        ordering = ['name']
    
    def __str__(self):
        return self.name

class Post(models.Model):
    STATUS_CHOICES = (
        ('draft', 'Draft'),
        ('published', 'Published'),
    )
    
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts')
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
    
    class Meta:
        ordering = ['-created_at']
    
    def __str__(self):
        return self.title
    
    def get_absolute_url(self):
        from django.urls import reverse
        return reverse('post_detail', args=[self.slug])

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
    name = models.CharField(max_length=80)
    email = models.EmailField()
    body = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    active = models.BooleanField(default=True)
    
    class Meta:
        ordering = ['created_at']
    
    def __str__(self):
        return f'Comment by {self.name} on {self.post}'
Model Field Types

# Common field types
class Product(models.Model):
    # String fields
    name = models.CharField(max_length=100)
    description = models.TextField()
    slug = models.SlugField()
    
    # Numeric fields
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.PositiveIntegerField()
    rating = models.FloatField()
    
    # Date and time fields
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateField(auto_now=True)
    
    # Boolean fields
    available = models.BooleanField(default=True)
    featured = models.BooleanField()
    
    # File fields
    image = models.ImageField(upload_to='products/')
    document = models.FileField(upload_to='documents/')
    
    # Relationship fields
    category = models.ForeignKey('Category', on_delete=models.CASCADE)
    tags = models.ManyToManyField('Tag')
    
    # Choice fields
    SIZE_CHOICES = (
        ('S', 'Small'),
        ('M', 'Medium'),
        ('L', 'Large'),
    )
    size = models.CharField(max_length=1, choices=SIZE_CHOICES)
Model Relationships

# myapp/models.py
from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)
    
    def __str__(self):
        return self.name

class Publisher(models.Model):
    name = models.CharField(max_length=100)
    address = models.CharField(max_length=200)
    
    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=200)
    publication_date = models.DateField()
    
    # Many-to-One relationship
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
    
    # Many-to-Many relationship
    authors = models.ManyToManyField(Author)
    
    def __str__(self):
        return self.title

# Through model for Many-to-Many with extra fields
class Student(models.Model):
    name = models.CharField(max_length=100)
    
    def __str__(self):
        return self.name

class Course(models.Model):
    name = models.CharField(max_length=100)
    students = models.ManyToManyField(Student, through='Enrollment')
    
    def __str__(self):
        return self.name

class Enrollment(models.Model):
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    date_enrolled = models.DateField()
    grade = models.CharField(max_length=2, blank=True)
    
    class Meta:
        unique_together = ('student', 'course')
Database Migrations

# Create migrations for new models
python manage.py makemigrations

# Create migrations for a specific app
python manage.py makemigrations myapp

# Create migrations with a custom name
python manage.py makemigrations myapp --name add_book_model

# Apply migrations
python manage.py migrate

# Apply migrations for a specific app
python manage.py migrate myapp

# Show migration status
python manage.py showmigrations

# Unapply migrations (rollback)
python manage.py migrate myapp 0001

# Create empty migration for custom data migrations
python manage.py makemigrations --empty myapp
Querying the Database

# Basic queries
from myapp.models import Post, Category

# Get all objects
all_posts = Post.objects.all()

# Get a single object
post = Post.objects.get(pk=1)
post = Post.objects.get(slug='my-first-post')

# Filter objects
published_posts = Post.objects.filter(status='published')
recent_posts = Post.objects.filter(created_at__gte='2023-01-01')

# Complex queries
from django.db.models import Q
posts = Post.objects.filter(
    Q(title__icontains='django') | Q(content__icontains='django')
)

# Ordering
posts = Post.objects.order_by('-created_at')
posts = Post.objects.order_by('title', '-created_at')

# Limiting results
latest_posts = Post.objects.order_by('-created_at')[:5]

# Related objects
category = Category.objects.get(name='Python')
posts_in_category = category.post_set.all()

# Many-to-Many relationships
post = Post.objects.get(pk=1)
authors = post.authors.all()

# Aggregation
from django.db.models import Count, Avg, Max, Min
category_counts = Category.objects.annotate(post_count=Count('post'))
avg_price = Product.objects.aggregate(avg_price=Avg('price'))

# Custom methods in models
class Post(models.Model):
    # ... fields ...
    
    def get_comment_count(self):
        return self.comments.count()
    
    def is_recent(self):
        from django.utils import timezone
        return self.created_at >= timezone.now() - datetime.timedelta(days=7)
Custom Managers

# myapp/models.py
from django.db import models

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

class Post(models.Model):
    # ... fields ...
    
    objects = models.Manager()  # Default manager
    published = PublishedManager()  # Custom manager
    
    # Usage:
    # Post.objects.all()  # All posts
    # Post.published.all()  # Only published posts
Module 3: Admin Interface
Registering Models with Admin

# myapp/admin.py
from django.contrib import admin
from .models import Post, Category, Comment

# Basic registration
admin.site.register(Post)
admin.site.register(Category)
admin.site.register(Comment)

# Custom admin class
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'author', 'status', 'created_at')
    list_filter = ('status', 'created_at', 'author')
    search_fields = ('title', 'content')
    prepopulated_fields = {'slug': ('title',)}
    raw_id_fields = ('author',)
    date_hierarchy = 'created_at'
    ordering = ('status', '-created_at')
    
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('name', 'description')
    search_fields = ('name',)

@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    list_display = ('name', 'post', 'created_at', 'active')
    list_filter = ('active', 'created_at')
    search_fields = ('name', 'email', 'body')
    actions = ['approve_comments']
    
    def approve_comments(self, request, queryset):
        queryset.update(active=True)
    approve_comments.short_description = "Mark selected comments as active"
Customizing Admin Layout

# myapp/admin.py
from django.contrib import admin
from .models import Post, Category, Comment

class CommentInline(admin.TabularInline):
    model = Comment
    extra = 0

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'author', 'status', 'created_at')
    list_filter = ('status', 'created_at', 'author')
    search_fields = ('title', 'content')
    prepopulated_fields = {'slug': ('title',)}
    raw_id_fields = ('author',)
    date_hierarchy = 'created_at'
    ordering = ('status', '-created_at')
    inlines = [CommentInline]
    
    fieldsets = (
        (None, {
            'fields': ('title', 'slug', 'author', 'category')
        }),
        ('Content', {
            'fields': ('content',),
            'classes': ('collapse',)
        }),
        ('Publishing', {
            'fields': ('status',),
        }),
    )
    
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        if request.user.is_superuser:
            return qs
        return qs.filter(author=request.user)
Admin Actions

# myapp/admin.py
from django.contrib import admin
from .models import Post

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    # ... other settings ...
    
    actions = ['make_published', 'make_draft']
    
    def make_published(self, request, queryset):
        updated = queryset.update(status='published')
        self.message_user(request, f'{updated} posts were successfully marked as published.')
    make_published.short_description = "Mark selected posts as published"
    
    def make_draft(self, request, queryset):
        updated = queryset.update(status='draft')
        self.message_user(request, f'{updated} posts were successfully marked as draft.')
    make_draft.short_description = "Mark selected posts as draft"
Custom Admin Site

# myapp/admin.py
from django.contrib.admin import AdminSite
from .models import Post

class MyAdminSite(AdminSite):
    site_header = 'My Administration'
    site_title = 'My Admin Portal'
    index_title = 'Welcome to My Admin Portal'
    
    def get_urls(self):
        from django.urls import path
        urls = super().get_urls()
        custom_urls = [
            path('my-view/', self.admin_view(my_view), name='my_view'),
        ]
        return custom_urls + urls
    
    def my_view(self, request):
        # Custom admin view logic
        pass

admin_site = MyAdminSite(name='myadmin')
admin_site.register(Post)
Module 4: Views and URL Configuration
Function-Based Views

# myapp/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponse, Http404
from django.views.generic import ListView, DetailView
from .models import Post, Category
from .forms import PostForm

def post_list(request):
    posts = Post.objects.filter(status='published')
    return render(request, 'myapp/post/list.html', {'posts': posts})

def post_detail(request, year, month, day, post):
    post = get_object_or_404(
        Post,
        slug=post,
        status='published',
        created_at__year=year,
        created_at__month=month,
        created_at__day=day
    )
    return render(request, 'myapp/post/detail.html', {'post': post})

def post_create(request):
    if request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.save()
            return redirect('myapp:post_detail', slug=post.slug)
    else:
        form = PostForm()
    return render(request, 'myapp/post/create.html', {'form': form})

def post_edit(request, slug):
    post = get_object_or_404(Post, slug=slug)
    if request.method == 'POST':
        form = PostForm(request.POST, instance=post)
        if form.is_valid():
            form.save()
            return redirect('myapp:post_detail', slug=post.slug)
    else:
        form = PostForm(instance=post)
    return render(request, 'myapp/post/edit.html', {'form': form, 'post': post})
Class-Based Views

# myapp/views.py
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Post
from .forms import PostForm

class PostListView(ListView):
    model = Post
    template_name = 'myapp/post/list.html'
    context_object_name = 'posts'
    paginate_by = 3
    queryset = Post.objects.filter(status='published')

class PostDetailView(DetailView):
    model = Post
    template_name = 'myapp/post/detail.html'
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['comments'] = self.object.comments.filter(active=True)
        return context

class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    form_class = PostForm
    template_name = 'myapp/post/create.html'
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

class PostUpdateView(LoginRequiredMixin, UpdateView):
    model = Post
    form_class = PostForm
    template_name = 'myapp/post/edit.html'
    
    def dispatch(self, request, *args, **kwargs):
        obj = self.get_object()
        if obj.author != request.user:
            return HttpResponseForbidden("You don't have permission to edit this post.")
        return super().dispatch(request, *args, **kwargs)

class PostDeleteView(LoginRequiredMixin, DeleteView):
    model = Post
    template_name = 'myapp/post/delete.html'
    success_url = reverse_lazy('myapp:post_list')
    
    def dispatch(self, request, *args, **kwargs):
        obj = self.get_object()
        if obj.author != request.user:
            return HttpResponseForbidden("You don't have permission to delete this post.")
        return super().dispatch(request, *args, **kwargs)
URL Configuration

# myproject/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('myapp.urls')),
]

# myapp/urls.py
from django.urls import path
from . import views

app_name = 'myapp'

urlpatterns = [
    # Function-based views
    path('', views.post_list, name='post_list'),
    path('post//', views.post_detail, name='post_detail'),
    path('post/create/', views.post_create, name='post_create'),
    path('post//edit/', views.post_edit, name='post_edit'),
    
    # Class-based views
    # path('', views.PostListView.as_view(), name='post_list'),
    # path('post//', views.PostDetailView.as_view(), name='post_detail'),
    # path('post/create/', views.PostCreateView.as_view(), name='post_create'),
    # path('post//edit/', views.PostUpdateView.as_view(), name='post_edit'),
    
    # URL patterns with date-based routing
    path(
        'post/////',
        views.post_detail,
        name='post_detail_date'
    ),
]
Advanced URL Patterns

# myapp/urls.py
from django.urls import path, re_path
from . import views

app_name = 'myapp'

urlpatterns = [
    # Regular expression patterns
    re_path(r'^post/(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[-\w]+)/$',
           views.post_detail,
           name='post_detail_date'),
    
    # Including additional URLconf files
    path('api/', include('myapp.api.urls')),
    
    # Named URLs
    path('about/', views.about_view, name='about'),
    
    # URL with optional parameters
    path('category//', views.category_view, name='category'),
    path('category/', views.category_view, name='category_all'),
    
    # Redirects
    path('old-url/', views.redirect_view, name='old_url'),
]
URL Reverse and Named URLs

# In views.py
from django.urls import reverse
from django.http import HttpResponseRedirect

def some_view(request):
    # Using reverse with named URL
    return HttpResponseRedirect(reverse('myapp:post_list'))

def post_edit(request, slug):
    # Using reverse with parameters
    return HttpResponseRedirect(reverse('myapp:post_detail', args=[slug]))

# In templates

All Posts
{{ post.title }}

    {{ post.title }}

Module 5: Templates and Static Files
Template Structure

myproject/
├── templates/
│   ├── base.html
│   ├── 404.html
│   ├── 500.html
│   └── registration/
│       ├── login.html
│       └── signup.html
├── myapp/
│   ├── templates/
│   │   └── myapp/
│   │       ├── base.html
│   │       ├── post/
│   │       │   ├── list.html
│   │       │   ├── detail.html
│   │       │   ├── create.html
│   │       │   └── edit.html
│   │       └── category/
│   │           └── list.html
└── static/
    ├── css/
    │   ├── style.css
    │   └── bootstrap.min.css
    ├── js/
    │   ├── app.js
    │   └── jquery.min.js
    └── images/
        ├── logo.png
        └── banner.jpg
Template Inheritance





    
    
    {% block title %}My Django Site{% endblock %}
    {% load static %}
    
    {% block extra_css %}{% endblock %}


    
{% if messages %}
{% for message in messages %}
{{ message }}
{% endfor %}
{% endif %} {% block content %}{% endblock %}

© {% now "Y" %} My Django Site

{% block extra_js %}{% endblock %}
Template Context and Variables


{% extends "base.html" %}

{% block title %}{{ post.title }} - My Blog{% endblock %}

{% block content %}

{{ post.title }}

{{ post.content|linebreaks }}
{% if post.tags.all %} {% endif %}

Comments

{% for comment in post.comments.all %}

{{ comment.name }} on {{ comment.created_at|date:"F j, Y, P" }}

{{ comment.body|linebreaks }}

{% empty %}

No comments yet.

{% endfor %}

Add a comment

{% csrf_token %} {{ comment_form.as_p }}
{% endblock %}
Template Filters and Tags


{{ post.content|truncatewords:30 }}

{{ post.created_at|date:"F j, Y" }}

{{ post.title|lower }}

{{ post.content|linebreaks }}

{{ post.content|safe }}

{% if user.is_authenticated %}

Welcome, {{ user.username }}!

{% endif %} {% for post in posts %}

{{ post.title }}

{% empty %}

No posts found.

{% endfor %} {% load static %} Logo {% load custom_tags %} {% get_latest_posts 5 as latest_posts %} {% for post in latest_posts %}

{{ post.title }}

{% endfor %}
Custom Template Tags and Filters

# myapp/templatetags/custom_tags.py
from django import template
from ..models import Post

register = template.Library()

@register.simple_tag
def get_latest_posts(count=5):
    return Post.objects.filter(status='published').order_by('-created_at')[:count]

@register.inclusion_tag('myapp/tags/latest_posts.html')
def show_latest_posts(count=5):
    latest_posts = Post.objects.filter(status='published').order_by('-created_at')[:count]
    return {'latest_posts': latest_posts}

@register.filter
def truncate_chars(value, arg):
    """
    Truncates a string after a certain number of characters.
    """
    try:
        length = int(arg)
    except ValueError:
        return value
    
    if len(value) > length:
        return value[:length] + '...'
    return value


Latest Posts

{% for post in latest_posts %}

{{ post.title }}

{{ post.content|truncatechars:100 }}

{% endfor %}
Static Files


{% load static %}










Logo
Banner






Media Files


{% if post.image %}
    {{ post.title }}
{% endif %}

{% if user.profile.avatar %}
    {{ user.username }}'s avatar
{% endif %}


{% csrf_token %} {{ form.as_p }}
Module 6: Forms and Validation
Creating Forms

# myapp/forms.py
from django import forms
from .models import Post, Comment

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ('title', 'slug', 'content', 'status', 'category')
        widgets = {
            'title': forms.TextInput(attrs={'class': 'form-control'}),
            'slug': forms.TextInput(attrs={'class': 'form-control'}),
            'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
            'status': forms.Select(attrs={'class': 'form-control'}),
            'category': forms.Select(attrs={'class': 'form-control'}),
        }
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['title'].label = "Post Title"
        self.fields['content'].label = "Content"
        self.fields['slug'].required = False

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ('name', 'email', 'body')
        widgets = {
            'name': forms.TextInput(attrs={'class': 'form-control'}),
            'email': forms.EmailInput(attrs={'class': 'form-control'}),
            'body': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
        }

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100, widget=forms.TextInput(attrs={'class': 'form-control'}))
    email = forms.EmailField(widget=forms.EmailInput(attrs={'class': 'form-control'}))
    subject = forms.CharField(max_length=100, widget=forms.TextInput(attrs={'class': 'form-control'}))
    message = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 5}))
    
    def clean_email(self):
        email = self.cleaned_data.get('email')
        # Custom validation logic
        if 'example.com' in email:
            raise forms.ValidationError("Please use a real email address.")
        return email
Form Fields and Widgets

# myapp/forms.py
from django import forms

class AdvancedForm(forms.Form):
    # Basic fields
    text_input = forms.CharField()
    password_input = forms.CharField(widget=forms.PasswordInput)
    number_input = forms.IntegerField()
    email_input = forms.EmailField()
    url_input = forms.URLField()
    
    # Choice fields
    choice_field = forms.ChoiceField(choices=[
        ('option1', 'Option 1'),
        ('option2', 'Option 2'),
        ('option3', 'Option 3'),
    ])
    
    # Multiple choice
    multiple_choice = forms.MultipleChoiceField(choices=[
        ('option1', 'Option 1'),
        ('option2', 'Option 2'),
        ('option3', 'Option 3'),
    ], widget=forms.CheckboxSelectMultiple)
    
    # Date and time fields
    date_field = forms.DateField(widget=forms.DateInput(attrs={'type': 'date'}))
    datetime_field = forms.DateTimeField(widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}))
    
    # File fields
    file_field = forms.FileField()
    image_field = forms.ImageField()
    
    # Boolean field
    agree_terms = forms.BooleanField(required=True)
    
    # Custom widget attributes
    custom_field = forms.CharField(
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': 'Enter your text here',
            'data-toggle': 'tooltip',
            'title': 'This is a tooltip',
        })
    )
Form Validation

# myapp/forms.py
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

class RegistrationForm(forms.Form):
    username = forms.CharField(max_length=150)
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)
    confirm_password = forms.CharField(widget=forms.PasswordInput)
    
    def clean_username(self):
        username = self.cleaned_data.get('username')
        # Check if username already exists
        if User.objects.filter(username=username).exists():
            raise ValidationError(_("Username already exists."))
        return username
    
    def clean_email(self):
        email = self.cleaned_data.get('email')
        # Check if email already exists
        if User.objects.filter(email=email).exists():
            raise ValidationError(_("Email already exists."))
        return email
    
    def clean(self):
        cleaned_data = super().clean()
        password = cleaned_data.get('password')
        confirm_password = cleaned_data.get('confirm_password')
        
        if password and confirm_password and password != confirm_password:
            raise ValidationError(_("Passwords don't match."))
        
        return cleaned_data

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ('title', 'content', 'status')
    
    def clean_title(self):
        title = self.cleaned_data.get('title')
        # Custom validation
        if len(title) < 5:
            raise ValidationError(_("Title must be at least 5 characters long."))
        return title
    
    def clean_content(self):
        content = self.cleaned_data.get('content')
        # Check for forbidden words
        forbidden_words = ['spam', 'advertisement']
        for word in forbidden_words:
            if word in content.lower():
                raise ValidationError(_("Content contains forbidden words."))
        return content
Working with Forms in Views

# myapp/views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import PostForm, ContactForm

def post_create(request):
    if request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():
            # Form is valid, process the data
            post = form.save(commit=False)
            post.author = request.user
            post.save()
            messages.success(request, 'Post created successfully!')
            return redirect('myapp:post_detail', slug=post.slug)
    else:
        form = PostForm()
    
    return render(request, 'myapp/post/create.html', {'form': form})

def contact_view(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            # Process the form data
            name = form.cleaned_data['name']
            email = form.cleaned_data['email']
            subject = form.cleaned_data['subject']
            message = form.cleaned_data['message']
            
            # Send email or save to database
            # ...
            
            messages.success(request, 'Your message has been sent!')
            return redirect('myapp:contact_success')
    else:
        form = ContactForm()
    
    return render(request, 'myapp/contact.html', {'form': form})
Rendering Forms in Templates


{% extends "base.html" %}

{% block title %}Create Post - My Blog{% endblock %}

{% block content %}

Create Post

{% csrf_token %} {% if form.non_field_errors %}
{% for error in form.non_field_errors %} {{ error }} {% endfor %}
{% endif %}
{{ form.title }} {% if form.title.errors %}
{% for error in form.title.errors %} {{ error }} {% endfor %}
{% endif %}
{{ form.content }} {% if form.content.errors %}
{% for error in form.content.errors %} {{ error }} {% endfor %}
{% endif %}
{{ form.as_p }} {{ form.as_table }}
    {{ form.as_ul }}
{% endblock %}
Model Formsets

# myapp/views.py
from django.shortcuts import render, redirect
from django.forms import modelformset_factory
from .models import Image

def image_gallery(request):
    ImageFormSet = modelformset_factory(
        Image, 
        fields=('image', 'caption'),
        extra=3
    )
    
    if request.method == 'POST':
        formset = ImageFormSet(request.POST, request.FILES)
        if formset.is_valid():
            instances = formset.save(commit=False)
            for instance in instances:
                instance.gallery = request.user.gallery
                instance.save()
            return redirect('myapp:gallery_detail')
    else:
        formset = ImageFormSet(queryset=Image.objects.none())
    
    return render(request, 'myapp/gallery/upload.html', {'formset': formset})


{% extends "base.html" %}

{% block content %}

Upload Images

{% csrf_token %} {{ formset.management_form }} {% for form in formset %}
{{ form.id }}
{{ form.image.label_tag }} {{ form.image }}
{{ form.caption.label_tag }} {{ form.caption }}
{% endfor %}
{% endblock %}
Module 7: User Authentication and Authorization
User Authentication Setup

# settings.py
INSTALLED_APPS = [
    # ...
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # ...
]

# Authentication backends
AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
]

# Login URLs
LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = 'profile'
LOGOUT_REDIRECT_URL = 'home'

# Password validation
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]
User Registration

# myapp/forms.py
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm

class CustomUserCreationForm(UserCreationForm):
    email = forms.EmailField(required=True)
    first_name = forms.CharField(max_length=30)
    last_name = forms.CharField(max_length=30)
    
    class Meta:
        model = User
        fields = ("username", "first_name", "last_name", "email", "password1", "password2")
    
    def save(self, commit=True):
        user = super().save(commit=False)
        user.email = self.cleaned_data["email"]
        user.first_name = self.cleaned_data["first_name"]
        user.last_name = self.cleaned_data["last_name"]
        if commit:
            user.save()
        return user

# myapp/views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import CustomUserCreationForm

def register(request):
    if request.method == 'POST':
        form = CustomUserCreationForm(request.POST)
        if form.is_valid():
            form.save()
            username = form.cleaned_data.get('username')
            messages.success(request, f'Account created for {username}!')
            return redirect('login')
    else:
        form = CustomUserCreationForm()
    return render(request, 'registration/register.html', {'form': form})
Login and Logout Views

# myapp/urls.py
from django.urls import path
from django.contrib.auth import views as auth_views

urlpatterns = [
    # ...
    path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
    path('password_change/', auth_views.PasswordChangeView.as_view(template_name='registration/password_change.html'), name='password_change'),
    path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(template_name='registration/password_change_done.html'), name='password_change_done'),
    path('password_reset/', auth_views.PasswordResetView.as_view(template_name='registration/password_reset.html'), name='password_reset'),
    path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(template_name='registration/password_reset_done.html'), name='password_reset_done'),
    path('reset///', auth_views.PasswordResetConfirmView.as_view(template_name='registration/password_reset_confirm.html'), name='password_reset_confirm'),
    path('reset/done/', auth_views.PasswordResetCompleteView.as_view(template_name='registration/password_reset_complete.html'), name='password_reset_complete'),
]

# myapp/views.py
from django.contrib.auth.decorators import login_required
from django.contrib.auth import authenticate, login, logout

def login_view(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        
        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)
            return redirect('profile')
        else:
            messages.error(request, 'Invalid username or password.')
    
    return render(request, 'registration/login.html')

def logout_view(request):
    logout(request)
    return redirect('home')

@login_required
def profile_view(request):
    return render(request, 'registration/profile.html')
User Profiles

# myapp/models.py
from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)
    avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
    
    def __str__(self):
        return f'{self.user.username} Profile'

# myapp/signals.py
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.dispatch import receiver
from .models import Profile

@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
    instance.profile.save()

# myapp/apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'myapp'
    
    def ready(self):
        import myapp.signals
Custom Authentication Backend

# myapp/authentication.py
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.models import User

class EmailBackend(BaseBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        try:
            user = User.objects.get(email=username)
            if user.check_password(password):
                return user
        except User.DoesNotExist:
            return None
    
    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

# settings.py
AUTHENTICATION_BACKENDS = [
    'myapp.authentication.EmailBackend',
    'django.contrib.auth.backends.ModelBackend',
]
Permissions and Authorization

# myapp/models.py
from django.db import models
from django.contrib.auth.models import User

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    
    class Meta:
        permissions = [
            ("can_publish_post", "Can publish post"),
            ("can_edit_any_post", "Can edit any post"),
        ]

# myapp/views.py
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views.generic import UpdateView

@login_required
def my_view(request):
    # Only logged-in users can access this view
    return render(request, 'myapp/my_template.html')

@permission_required('myapp.can_publish_post')
def publish_post(request, post_id):
    # Only users with the 'can_publish_post' permission can access this view
    pass

class PostUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
    model = Post
    fields = ['title', 'content']
    template_name = 'myapp/post_edit.html'
    permission_required = 'myapp.can_edit_any_post'
    
    def get_success_url(self):
        return reverse('myapp:post_detail', kwargs={'pk': self.object.pk})
Groups and Permissions

# myapp/views.py
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from .models import Post

def create_groups(request):
    # Create groups
    editors_group, created = Group.objects.get_or_create(name='Editors')
    publishers_group, created = Group.objects.get_or_create(name='Publishers')
    
    # Get permissions
    post_content_type = ContentType.objects.get_for_model(Post)
    can_publish = Permission.objects.get(
        codename='can_publish_post',
        content_type=post_content_type
    )
    can_edit = Permission.objects.get(
        codename='change_post',
        content_type=post_content_type
    )
    
    # Assign permissions to groups
    editors_group.permissions.add(can_edit)
    publishers_group.permissions.add(can_publish, can_edit)
    
    return HttpResponse("Groups created successfully")

# In a view or management command
def assign_user_to_group(user):
    editors_group = Group.objects.get(name='Editors')
    user.groups.add(editors_group)
    user.save()
Module 8: REST APIs with Django REST Framework
Django REST Framework Setup

# settings.py
INSTALLED_APPS = [
    # ...
    'rest_framework',
    'myapp',
]

# REST Framework configuration
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
    ],
}
Serializers

# myapp/serializers.py
from rest_framework import serializers
from .models import Post, Category, Comment

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ['id', 'name', 'description']

class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ['id', 'name', 'email', 'body', 'created_at']
        read_only_fields = ['created_at']

class PostSerializer(serializers.ModelSerializer):
    author = serializers.ReadOnlyField(source='author.username')
    category_name = serializers.ReadOnlyField(source='category.name')
    comments = CommentSerializer(many=True, read_only=True)
    
    class Meta:
        model = Post
        fields = ['id', 'title', 'slug', 'content', 'author', 'category', 
                  'category_name', 'status', 'created_at', 'updated_at', 'comments']
        read_only_fields = ['author', 'created_at', 'updated_at']

class PostDetailSerializer(PostSerializer):
    # More detailed serializer for individual post view
    comments = CommentSerializer(many=True, read_only=True)
    
    class Meta(PostSerializer.Meta):
        fields = PostSerializer.Meta.fields

class PostCreateSerializer(serializers.ModelSerializer):
    # Serializer for creating posts
    class Meta:
        model = Post
        fields = ['title', 'slug', 'content', 'status', 'category']
    
    def create(self, validated_data):
        # Set the author to the current user
        validated_data['author'] = self.context['request'].user
        return super().create(validated_data)
API Views

# myapp/views.py
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Post, Category
from .serializers import PostSerializer, CategorySerializer, PostDetailSerializer

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    
    def get_serializer_class(self):
        if self.action == 'retrieve':
            return PostDetailSerializer
        return self.serializer_class
    
    def get_queryset(self):
        queryset = Post.objects.all()
        status_filter = self.request.query_params.get('status', None)
        if status_filter is not None:
            queryset = queryset.filter(status=status_filter)
        return queryset
    
    @action(detail=True, methods=['post'])
    def publish(self, request, pk=None):
        post = self.get_object()
        post.status = 'published'
        post.save()
        serializer = self.get_serializer(post)
        return Response(serializer.data)
    
    @action(detail=False)
    def published(self, request):
        published_posts = Post.objects.filter(status='published')
        page = self.paginate_queryset(published_posts)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)
        serializer = self.get_serializer(published_posts, many=True)
        return Response(serializer.data)

class CategoryViewSet(viewsets.ModelViewSet):
    queryset = Category.objects.all()
    serializer_class = CategorySerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

class PostLikeAPIView(APIView):
    permission_classes = [permissions.IsAuthenticated]
    
    def post(self, request, pk):
        post = Post.objects.get(pk=pk)
        # Like logic here
        return Response({"message": "Post liked successfully"}, status=status.HTTP_200_OK)
URL Configuration for APIs

# myapp/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

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

urlpatterns = [
    path('api/', include(router.urls)),
    path('api/posts//like/', views.PostLikeAPIView.as_view(), name='post-like'),
]
Authentication for APIs

# myapp/views.py
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response

class CustomAuthToken(ObtainAuthToken):
    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data,
                                           context={'request': request})
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data['user']
        token, created = Token.objects.get_or_create(user=user)
        return Response({
            'token': token.key,
            'user_id': user.pk,
            'email': user.email
        })

# myapp/urls.py
from django.urls import path
from . import views

urlpatterns = [
    # ...
    path('api/auth/login/', views.CustomAuthToken.as_view(), name='api_token_auth'),
]
Custom Permissions

# myapp/permissions.py
from rest_framework import permissions

class IsAuthorOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow authors of an object to edit it.
    """
    
    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True
        
        # Write permissions are only allowed to the author of the post.
        return obj.author == request.user

# myapp/views.py
from .permissions import IsAuthorOrReadOnly

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
API Versioning

# myapp/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import v1_views, v2_views

router_v1 = DefaultRouter()
router_v1.register(r'posts', v1_views.PostViewSet)

router_v2 = DefaultRouter()
router_v2.register(r'posts', v2_views.PostViewSet)

urlpatterns = [
    path('api/v1/', include(router_v1.urls)),
    path('api/v2/', include(router_v2.urls)),
]

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
    'DEFAULT_VERSION': 'v1',
    'ALLOWED_VERSIONS': ['v1', 'v2'],
    'VERSION_PARAM': 'version',
}
Module 9: Testing and Debugging
Writing Tests

# myapp/tests.py
from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.urls import reverse
from .models import Post, Category

class PostModelTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Set up non-modified objects used by all test methods
        user = User.objects.create_user(username='testuser', password='testpass')
        category = Category.objects.create(name='Test Category')
        Post.objects.create(
            title='Test Post',
            slug='test-post',
            content='This is a test post.',
            author=user,
            category=category,
            status='published'
        )
    
    def test_title_content(self):
        post = Post.objects.get(id=1)
        expected_object_name = f'{post.title}'
        self.assertEqual(expected_object_name, 'Test Post')
    
    def test_post_get_absolute_url(self):
        post = Post.objects.get(id=1)
        self.assertEqual(post.get_absolute_url(), '/post/test-post/')

class PostViewTest(TestCase):
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create_user(username='testuser', password='testpass')
        self.category = Category.objects.create(name='Test Category')
        self.post = Post.objects.create(
            title='Test Post',
            slug='test-post',
            content='This is a test post.',
            author=self.user,
            category=self.category,
            status='published'
        )
    
    def test_post_list_view(self):
        response = self.client.get(reverse('myapp:post_list'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Test Post')
        self.assertTemplateUsed(response, 'myapp/post/list.html')
    
    def test_post_detail_view(self):
        response = self.client.get(reverse('myapp:post_detail', kwargs={'slug': 'test-post'}))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Test Post')
        self.assertTemplateUsed(response, 'myapp/post/detail.html')
    
    def test_post_create_view(self):
        self.client.login(username='testuser', password='testpass')
        response = self.client.get(reverse('myapp:post_create'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'myapp/post/create.html')
        
        response = self.client.post(reverse('myapp:post_create'), {
            'title': 'New Test Post',
            'slug': 'new-test-post',
            'content': 'This is a new test post.',
            'status': 'published',
            'category': self.category.id
        })
        self.assertEqual(response.status_code, 302)
        self.assertTrue(Post.objects.filter(slug='new-test-post').exists())
Testing APIs

# myapp/tests.py
from django.test import TestCase
from django.contrib.auth.models import User
from rest_framework.test import APIClient
from rest_framework import status
from .models import Post

class PostAPITestCase(TestCase):
    def setUp(self):
        self.client = APIClient()
        self.user = User.objects.create_user(username='testuser', password='testpass')
        self.post = Post.objects.create(
            title='Test Post',
            slug='test-post',
            content='This is a test post.',
            author=self.user,
            status='published'
        )
    
    def test_get_posts(self):
        response = self.client.get('/api/posts/')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 1)
        self.assertEqual(response.data[0]['title'], 'Test Post')
    
    def test_create_post_unauthorized(self):
        response = self.client.post('/api/posts/', {
            'title': 'New Post',
            'content': 'This is a new post.',
            'status': 'published'
        })
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
    
    def test_create_post_authorized(self):
        self.client.force_authenticate(user=self.user)
        response = self.client.post('/api/posts/', {
            'title': 'New Post',
            'content': 'This is a new post.',
            'status': 'published'
        })
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertTrue(Post.objects.filter(title='New Post').exists())
    
    def test_update_post(self):
        self.client.force_authenticate(user=self.user)
        response = self.client.put(f'/api/posts/{self.post.id}/', {
            'title': 'Updated Post',
            'content': 'This is an updated post.',
            'status': 'published'
        })
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.post.refresh_from_db()
        self.assertEqual(self.post.title, 'Updated Post')
    
    def test_delete_post(self):
        self.client.force_authenticate(user=self.user)
        response = self.client.delete(f'/api/posts/{self.post.id}/')
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        self.assertFalse(Post.objects.filter(id=self.post.id).exists())
Test Fixtures

# myapp/fixtures/test_data.json
[
    {
        "model": "auth.user",
        "pk": 1,
        "fields": {
            "username": "testuser",
            "email": "test@example.com",
            "password": "pbkdf2_sha256$260000$..."
        }
    },
    {
        "model": "myapp.category",
        "pk": 1,
        "fields": {
            "name": "Test Category",
            "description": "A test category"
        }
    },
    {
        "model": "myapp.post",
        "pk": 1,
        "fields": {
            "title": "Test Post",
            "slug": "test-post",
            "content": "This is a test post.",
            "author": 1,
            "category": 1,
            "status": "published",
            "created_at": "2023-01-01T00:00:00Z"
        }
    }
]

# myapp/tests.py
from django.test import TestCase

class PostTestCase(TestCase):
    fixtures = ['test_data.json']
    
    def test_post_count(self):
        self.assertEqual(Post.objects.count(), 1)
    
    def test_post_title(self):
        post = Post.objects.get(pk=1)
        self.assertEqual(post.title, 'Test Post')
Debugging Tools

# Using Django Debug Toolbar
# settings.py
if DEBUG:
    INSTALLED_APPS += [
        'debug_toolbar',
    ]
    MIDDLEWARE += [
        'debug_toolbar.middleware.DebugToolbarMiddleware',
    ]
    INTERNAL_IPS = [
        '127.0.0.1',
    ]

# myproject/urls.py
if settings.DEBUG:
    import debug_toolbar
    urlpatterns += [
        path('__debug__/', include(debug_toolbar.urls)),
    ]

# Using Django Extensions
# settings.py
INSTALLED_APPS += [
    'django_extensions',
]

# Commands
python manage.py shell_plus
python manage.py runserver_plus
python manage.py print_user_for_session 
python manage.py describe_form 
python manage.py dumpscript  > script.py

# Using pdb for debugging
def my_view(request):
    import pdb; pdb.set_trace()
    # Code execution will stop here and you can debug
    post = get_object_or_404(Post, pk=id)
    return render(request, 'template.html', {'post': post})
Logging Configuration

# settings.py
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
            'style': '{',
        },
        'simple': {
            'format': '{levelname} {message}',
            'style': '{',
        },
    },
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': BASE_DIR / 'logs/django.log',
            'formatter': 'verbose',
        },
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'simple',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file', 'console'],
            'level': 'INFO',
            'propagate': True,
        },
        'myapp': {
            'handlers': ['file', 'console'],
            'level': 'DEBUG',
            'propagate': True,
        },
    },
}

# Using logging in views
import logging

logger = logging.getLogger(__name__)

def my_view(request):
    logger.debug('Debug message')
    logger.info('Info message')
    logger.warning('Warning message')
    logger.error('Error message')
    logger.critical('Critical message')
    
    try:
        # Some code that might raise an exception
        result = 1 / 0
    except Exception as e:
        logger.exception(f'An error occurred: {e}')
    
    return render(request, 'template.html')
Module 10: Deployment and Scaling
Production Settings

# settings/production.py
from .base import *

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']

# Security settings
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

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

# Static files
STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_ROOT = BASE_DIR / 'mediafiles'

# Email
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.environ.get('EMAIL_HOST')
EMAIL_PORT = os.environ.get('EMAIL_PORT', 587)
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL')

# Cache
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': os.environ.get('REDIS_URL', 'redis://127.0.0.1:6379/1'),
    }
}

# Logging
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
            'style': '{',
        },
    },
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': BASE_DIR / 'logs/django.log',
            'formatter': 'verbose',
        },
    },
    'root': {
        'handlers': ['file'],
        'level': 'INFO',
    },
}
Environment Variables

# .env file (never commit to version control)
SECRET_KEY=your-secret-key-here
DEBUG=False
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com

# Database
DB_NAME=mydatabase
DB_USER=myuser
DB_PASSWORD=mypassword
DB_HOST=localhost
DB_PORT=5432

# Email
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
DEFAULT_FROM_EMAIL=your-email@gmail.com

# Redis
REDIS_URL=redis://127.0.0.1:6379/1

# settings/base.py
import os
from pathlib import Path
from dotenv import load_dotenv

load_dotenv()

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = os.environ.get('DEBUG', 'False').lower() in ('true', '1', 't')
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
Gunicorn Deployment

# Install Gunicorn
pip install gunicorn

# Run with Gunicorn
gunicorn myproject.wsgi:application

# With specific settings
gunicorn myproject.wsgi:application --env DJANGO_SETTINGS_MODULE=myproject.settings.production

# With workers
gunicorn myproject.wsgi:application --workers 3 --bind 0.0.0.0:8000

# gunicorn.conf.py
bind = "0.0.0.0:8000"
workers = 3
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
max_requests = 1000
max_requests_jitter = 100
preload_app = True
user = "www-data"
group = "www-data"
tmp_upload_dir = None
errorlog = "/var/log/gunicorn/error.log"
accesslog = "/var/log/gunicorn/access.log"
loglevel = "info"
Systemd Service

# /etc/systemd/system/gunicorn.service
[Unit]
Description=gunicorn daemon for myproject
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/path/to/myproject
ExecStart=/path/to/venv/bin/gunicorn --config gunicorn.conf.py myproject.wsgi:application
Restart=always

[Install]
WantedBy=multi-user.target

# Enable and start the service
sudo systemctl enable gunicorn
sudo systemctl start gunicorn
sudo systemctl status gunicorn
sudo journalctl -u gunicorn
Nginx Configuration

# /etc/nginx/sites-available/myproject
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    ssl_certificate /path/to/ssl/cert.pem;
    ssl_certificate_key /path/to/ssl/private.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    location = /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        root /path/to/myproject;
    }
    
    location /media/ {
        root /path/to/myproject;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }
}
Docker Deployment

# Dockerfile
FROM python:3.9-slim

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# Set work directory
WORKDIR /app

# Install system dependencies
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        postgresql-client \
        build-essential \
    && rm -rf /var/lib/apt/lists/*

# Install Python dependencies
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt

# Copy project
COPY . /app/

# Collect static files
RUN python manage.py collectstatic --noinput

# Expose port
EXPOSE 8000

# Run the application
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myproject.wsgi:application"]

# docker-compose.yml
version: '3.8'

services:
  db:
    image: postgres:13
    environment:
      POSTGRES_DB: mydatabase
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:6-alpine
    ports:
      - "6379:6379"

  web:
    build: .
    command: gunicorn --bind 0.0.0.0:8000 myproject.wsgi:application
    volumes:
      - .:/app
      - static_volume:/app/staticfiles
      - media_volume:/app/mediafiles
    ports:
      - "8000:8000"
    depends_on:
      - db
      - redis
    environment:
      - DEBUG=False
      - DB_HOST=db
      - DB_NAME=mydatabase
      - DB_USER=myuser
      - DB_PASSWORD=mypassword
      - REDIS_URL=redis://redis:6379/1

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - static_volume:/app/staticfiles
      - media_volume:/app/mediafiles
      - ./ssl:/etc/ssl/certs
    depends_on:
      - web

volumes:
  postgres_data:
  static_volume:
  media_volume:
Scaling with Celery

# myproject/celery.py
import os
from celery import Celery

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings.production')

app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()

# settings/production.py
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'UTC'

# myapp/tasks.py
from celery import shared_task
from django.core.mail import send_mail
from .models import Post

@shared_task
def send_new_post_email(post_id):
    try:
        post = Post.objects.get(id=post_id)
        send_mail(
            subject=f'New Post: {post.title}',
            message=post.content,
            from_email='noreply@example.com',
            recipient_list=['user@example.com'],
            fail_silently=False,
        )
        return f"Email sent for post {post.title}"
    except Post.DoesNotExist:
        return f"Post with id {post_id} does not exist"

# myapp/views.py
from .tasks import send_new_post_email

def post_create(request):
    # ... form processing ...
    post = form.save()
    send_new_post_email.delay(post.id)
    return redirect('myapp:post_detail', slug=post.slug)

# Run Celery worker
celery -A myproject worker -l info

# Run Celery beat (scheduler)
celery -A myproject beat -l info

# Run Celery flower (monitoring)
pip install flower
celery -A myproject flower