Web Development Met Django

Bouw moderne web applicaties met Django. Van setup tot deployment

Waarom Django?

Django is een van de meest populaire web frameworks voor Python, en dat is niet zonder reden. Het volgt het "batteries included" principe, wat betekent dat het meeste wat je nodig hebt voor webontwikkeling al ingebouwd zit. Van user authentication tot database management - Django heeft het allemaal.

De Voordelen van Django

  • Rapid Development: Snel van idee naar werkende applicatie
  • Secure by Default: Ingebouwde beveiliging tegen veelvoorkomende aanvallen
  • Scalable: Van kleine projecten tot enterprise applicaties
  • Excellent Documentation: Uitgebreide en duidelijke documentatie
  • Active Community: Grote gemeenschap met veel packages
  • ORM (Object-Relational Mapping): Database operaties zonder SQL

Django Setup en Project Structuur

Laten we beginnen met het opzetten van je eerste Django project:

Installatie en Virtual Environment

# Virtual environment maken (aanbevolen)
python -m venv django_env

# Virtual environment activeren
# Windows:
django_env\Scripts\activate
# Mac/Linux:
source django_env/bin/activate

# Django installeren
pip install django

# Versie controleren
python -m django --version

Je Eerste Django Project

# Nieuw project maken
django-admin startproject mywebsite

# Naar project directory
cd mywebsite

# Development server starten
python manage.py runserver

# Open browser naar http://127.0.0.1:8000/

Project Structuur Begrijpen

mywebsite/
├── manage.py           # Command-line utility
├── mywebsite/          # Project package
│   ├── __init__.py
│   ├── settings.py     # Project instellingen
│   ├── urls.py         # URL routing
│   ├── wsgi.py         # WSGI deployment
│   └── asgi.py         # ASGI deployment

Django Apps en Models

Django projecten bestaan uit apps - modulaire componenten die specifieke functionaliteit bevatten:

Een App Maken

# Nieuwe app maken
python manage.py startapp blog

# App structuur
blog/
├── __init__.py
├── admin.py        # Admin interface
├── apps.py         # App configuratie
├── models.py       # Database models
├── tests.py        # Unit tests
├── views.py        # View functies
└── migrations/     # Database migraties

Models Definiëren

Models definiëren je database structuur en business logic:

# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils import timezone

class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    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)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
    content = models.TextField()
    excerpt = models.TextField(max_length=300, blank=True)
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
    featured_image = models.ImageField(upload_to='blog/', blank=True, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    published_at = models.DateTimeField(null=True, blank=True)
    
    class Meta:
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['-created_at']),
            models.Index(fields=['status']),
        ]
    
    def __str__(self):
        return self.title
    
    def get_absolute_url(self):
        return reverse('blog:post_detail', kwargs={'slug': self.slug})
    
    def save(self, *args, **kwargs):
        if self.status == 'published' and not self.published_at:
            self.published_at = timezone.now()
        super().save(*args, **kwargs)

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

Database Migraties

# App registreren in settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',  # Voeg je app toe
]

# Migraties maken
python manage.py makemigrations blog

# Migraties uitvoeren
python manage.py migrate

# Superuser maken voor admin
python manage.py createsuperuser

Views en Templates

Views bevatten de business logic en templates definiëren hoe content wordt weergegeven:

Class-Based Views

# blog/views.py
from django.shortcuts import render, get_object_or_404
from django.views.generic import ListView, DetailView, CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.core.paginator import Paginator
from .models import Post, Category, Comment
from .forms import CommentForm

class PostListView(ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    paginate_by = 6
    
    def get_queryset(self):
        queryset = Post.objects.filter(status='published').select_related('author', 'category')
        
        # Search functionaliteit
        search_query = self.request.GET.get('search')
        if search_query:
            queryset = queryset.filter(
                Q(title__icontains=search_query) |
                Q(content__icontains=search_query) |
                Q(excerpt__icontains=search_query)
            )
        
        # Category filter
        category_slug = self.request.GET.get('category')
        if category_slug:
            queryset = queryset.filter(category__slug=category_slug)
            
        return queryset
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['categories'] = Category.objects.all()
        context['search_query'] = self.request.GET.get('search', '')
        return context

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'
    slug_field = 'slug'
    slug_url_kwarg = 'slug'
    
    def get_queryset(self):
        return Post.objects.filter(status='published').select_related('author', 'category')
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['comments'] = self.object.comments.filter(approved=True)
        context['comment_form'] = CommentForm()
        context['related_posts'] = Post.objects.filter(
            category=self.object.category,
            status='published'
        ).exclude(id=self.object.id)[:3]
        return context

def add_comment(request, slug):
    post = get_object_or_404(Post, slug=slug, status='published')
    
    if request.method == 'POST':
        form = CommentForm(request.POST)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.post = post
            comment.save()
            
            messages.success(request, 'Je comment is toegevoegd en wacht op goedkeuring.')
            return redirect('blog:post_detail', slug=slug)
    
    return redirect('blog:post_detail', slug=slug)

Templates Maken





    
    
    {% block title %}My Blog{% endblock %}
    
    {% load static %}
    


    

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

© 2024 My Blog. Alle rechten voorbehouden.


{% extends 'blog/base.html' %}

{% block title %}Blog Posts{% endblock %}

{% block content %}

Laatste Blog Posts

{% for post in posts %}
{% if post.featured_image %} {{ post.title }} {% endif %}
{{ post.title }}

{{ post.excerpt|truncatewords:20 }}

Door {{ post.author.get_full_name|default:post.author.username }} op {{ post.published_at|date:"d F Y" }}
{% empty %}

Geen posts gevonden.

{% endfor %}
{% if is_paginated %} {% endif %}
Categorieën
{% for category in categories %} {{ category.name }} {% endfor %}
{% endblock %}

URL Routing

Django's URL systeem verbindt URLs met views:

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

app_name = 'blog'

urlpatterns = [
    path('', views.PostListView.as_view(), name='post_list'),
    path('post/<slug:slug>/', views.PostDetailView.as_view(), name='post_detail'),
    path('post/<slug:slug>/comment/', views.add_comment, name='add_comment'),
]

# mywebsite/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

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

# Serve media files in development
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Forms en User Input

Django forms maken het gemakkelijk om user input te valideren en verwerken:

# blog/forms.py
from django import forms
from .models import Comment

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['name', 'email', 'content']
        widgets = {
            'name': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'Je naam'
            }),
            'email': forms.EmailInput(attrs={
                'class': 'form-control',
                'placeholder': 'Je email'
            }),
            'content': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 4,
                'placeholder': 'Je comment...'
            }),
        }
    
    def clean_content(self):
        content = self.cleaned_data['content']
        if len(content) < 10:
            raise forms.ValidationError('Comment moet minstens 10 karakters bevatten.')
        return content

# Geavanceerde form met custom validatie
class ContactForm(forms.Form):
    name = forms.CharField(
        max_length=100,
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': 'Je naam'
        })
    )
    email = forms.EmailField(
        widget=forms.EmailInput(attrs={
            'class': 'form-control',
            'placeholder': 'Je email'
        })
    )
    subject = forms.CharField(
        max_length=200,
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': 'Onderwerp'
        })
    )
    message = forms.CharField(
        widget=forms.Textarea(attrs={
            'class': 'form-control',
            'rows': 5,
            'placeholder': 'Je bericht...'
        })
    )
    
    def clean_email(self):
        email = self.cleaned_data['email']
        if not email.endswith('@example.com'):
            raise forms.ValidationError('Alleen example.com emails zijn toegestaan.')
        return email

Admin Interface

Django's admin interface geeft je een krachtige backend voor content management:

# blog/admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import Post, Category, Comment

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ['name', 'description', 'created_at']
    search_fields = ['name']
    prepopulated_fields = {'slug': ('name',)}

class CommentInline(admin.TabularInline):
    model = Comment
    extra = 0
    readonly_fields = ['created_at']

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'category', 'status', 'created_at', 'view_count']
    list_filter = ['status', 'category', 'created_at', 'author']
    search_fields = ['title', 'content']
    prepopulated_fields = {'slug': ('title',)}
    date_hierarchy = 'created_at'
    ordering = ['-created_at']
    inlines = [CommentInline]
    
    fieldsets = (
        ('Basis Informatie', {
            'fields': ('title', 'slug', 'author', 'category')
        }),
        ('Content', {
            'fields': ('excerpt', 'content', 'featured_image')
        }),
        ('Publishing', {
            'fields': ('status', 'published_at'),
            'classes': ('collapse',)
        }),
    )
    
    def view_count(self, obj):
        return f"{obj.comments.count()} comments"
    view_count.short_description = 'Engagement'

@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    list_display = ['name', 'post', 'created_at', 'approved', 'content_preview']
    list_filter = ['approved', 'created_at']
    search_fields = ['name', 'email', 'content']
    actions = ['approve_comments', 'disapprove_comments']
    
    def content_preview(self, obj):
        return obj.content[:50] + '...' if len(obj.content) > 50 else obj.content
    content_preview.short_description = 'Content Preview'
    
    def approve_comments(self, request, queryset):
        queryset.update(approved=True)
    approve_comments.short_description = 'Approve selected comments'
    
    def disapprove_comments(self, request, queryset):
        queryset.update(approved=False)
    disapprove_comments.short_description = 'Disapprove selected comments'

Static Files en Media

Configuratie voor CSS, JavaScript en uploads:

# settings.py
import os
from pathlib import Path

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

# Static files (CSS, JavaScript, Images)
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),
]

# Media files (User uploads)
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

# Static files directory structure
static/
├── blog/
│   ├── css/
│   │   └── style.css
│   ├── js/
│   │   └── script.js
│   └── images/
└── admin/
    └── custom_admin.css

Database Optimalisatie

Best practices voor database performance:

# Efficiënte database queries
from django.db import models
from django.db.models import Prefetch, Count, Q

# Select related voor foreign keys
posts = Post.objects.select_related('author', 'category').filter(status='published')

# Prefetch related voor many-to-many en reverse foreign keys
posts = Post.objects.prefetch_related('comments').filter(status='published')

# Complex prefetch
approved_comments = Prefetch('comments', queryset=Comment.objects.filter(approved=True))
posts = Post.objects.prefetch_related(approved_comments)

# Aggregatie en annotatie
categories_with_counts = Category.objects.annotate(
    post_count=Count('post', filter=Q(post__status='published'))
)

# Database indexen in models
class Post(models.Model):
    # ... andere velden
    
    class Meta:
        indexes = [
            models.Index(fields=['status', '-created_at']),
            models.Index(fields=['category', 'status']),
            models.Index(fields=['author', '-created_at']),
        ]

Security Best Practices

Django heeft ingebouwde beveiliging, maar je moet het wel correct configureren:

# settings.py - Production security
import os

# Secret key (gebruik environment variables)
SECRET_KEY = os.environ.get('SECRET_KEY')

# Debug moet False zijn in productie
DEBUG = False

# Allowed hosts
ALLOWED_HOSTS = ['jouwdomein.com', 'www.jouwdomein.com']

# Security 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.XFrameOptionsMiddleware',
]

# Security settings
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'

# HTTPS settings (voor productie)
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

# Database security
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'),
    }
}

Testing

Automated testing is cruciaal voor betrouwbare applicaties:

# blog/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, Comment

class PostModelTest(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username='testuser',
            email='[email protected]',
            password='testpass123'
        )
        self.category = Category.objects.create(
            name='Test Category',
            description='Test description'
        )
    
    def test_post_creation(self):
        post = Post.objects.create(
            title='Test Post',
            slug='test-post',
            author=self.user,
            category=self.category,
            content='Test content',
            status='published'
        )
        self.assertEqual(post.title, 'Test Post')
        self.assertEqual(str(post), 'Test Post')
        self.assertTrue(post.published_at)
    
    def test_post_absolute_url(self):
        post = Post.objects.create(
            title='Test Post',
            slug='test-post',
            author=self.user,
            content='Test content'
        )
        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='testpass123'
        )
        self.category = Category.objects.create(name='Test Category')
        self.post = Post.objects.create(
            title='Test Post',
            slug='test-post',
            author=self.user,
            category=self.category,
            content='Test content',
            status='published'
        )
    
    def test_post_list_view(self):
        response = self.client.get(reverse('blog:post_list'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Test Post')
    
    def test_post_detail_view(self):
        response = self.client.get(
            reverse('blog:post_detail', kwargs={'slug': 'test-post'})
        )
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Test Post')
    
    def test_post_search(self):
        response = self.client.get(reverse('blog:post_list') + '?search=Test')
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Test Post')

# Run tests
# python manage.py test

Deployment

Van development naar productie:

# requirements.txt
Django==4.2.7
Pillow==10.0.1
psycopg2-binary==2.9.7
gunicorn==21.2.0
whitenoise==6.6.0

# Production settings
# settings/production.py
from .base import *
import os

DEBUG = False
ALLOWED_HOSTS = ['jouwdomein.com']

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

# Static files
MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware')
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

# Deployment script
#!/bin/bash
# deploy.sh

# Pull latest code
git pull origin main

# Install dependencies
pip install -r requirements.txt

# Run migrations
python manage.py migrate

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

# Restart gunicorn
sudo systemctl restart gunicorn

Performance Optimalisatie

  • Database indexen: Voeg indexen toe voor veelgebruikte queries
  • Query optimalisatie: Gebruik select_related en prefetch_related
  • Caching: Redis of Memcached voor frequent accessed data
  • Static files: CDN voor static content
  • Database connection pooling: Voor high-traffic sites

Volgende Stappen

Nu je de basis van Django kent, kun je verder met:

  • Django REST Framework: API development
  • Celery: Background tasks
  • Channels: WebSocket support
  • Docker: Containerized deployment
  • AWS/Digital Ocean: Cloud deployment

Conclusie

Django is een krachtig framework dat je in staat stelt om snel moderne web applicaties te bouwen. Het volgt best practices voor security, performance en maintainability out-of-the-box. De leercurve kan steil zijn, maar de productiviteitswinst is enorm.

Het geheim van succesvol Django development ligt in het begrijpen van het MTV (Model-Template-View) pattern en het benutten van Django's "batteries included" filosofie. Begin klein, bouw incrementeel, en test alles.

Klaar om Full-Stack Developer te worden?

In onze Web Development cursus leer je niet alleen Django, maar ook frontend technologieën, databases, deployment, en best practices voor professionele webontwikkeling.

Meer informatie