Inleiding tot Python Web Development

Python is een van de meest populaire talen voor web development, dankzij zijn eenvoud, leesbaarheid en krachtige frameworks. In dit uitgebreide artikel duiken we diep in de wereld van Python web development met de twee meest gebruikte frameworks: Django en Flask.

We behandelen alles van basis concepten tot geavanceerde technieken, zodat u aan het einde zelfstandig moderne, schaalbare webapplicaties kunt bouwen die klaar zijn voor productie.

Django vs Flask: De Grote Vergelijking

Voordat we beginnen met coderen, is het belangrijk om te begrijpen wanneer je welk framework gebruikt:

Django - "The Web Framework for Perfectionists with Deadlines"

  • Batteries included: Alles wat je nodig hebt zit erbij
  • Admin interface: Automatische admin panel generatie
  • ORM: Krachtige database abstraction layer
  • Authenticatie: Ingebouwde user management
  • Best voor: Grote, complexe applicaties

Flask - "Micro Framework"

  • Minimalistisch: Klein en flexibel
  • Modular: Voeg alleen toe wat je nodig hebt
  • Flexibel: Meer controle over de architectuur
  • Leerslimte: Gemakkelijker om te beginnen
  • Best voor: Kleine tot middelgrote projecten, APIs

Flask: Van Eenvoudig tot Geavanceerd

Laten we beginnen met Flask vanwege zijn eenvoud en flexibiliteit.

Flask Basics: Je Eerste Web App

# app.py
from flask import Flask, render_template, request, redirect, url_for, session, flash
import sqlite3
from datetime import datetime

app = Flask(__name__)
app.secret_key = 'jouw-geheime-sleutel-hier'

# Database setup
def init_db():
    conn = sqlite3.connect('cursisten.db')
    c = conn.cursor()
    c.execute('''
        CREATE TABLE IF NOT EXISTS cursisten (
            id INTEGER PRIMARY KEY,
            naam TEXT NOT NULL,
            email TEXT UNIQUE NOT NULL,
            cursus TEXT NOT NULL,
            aanmelddatum TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    ''')
    conn.commit()
    conn.close()

# Home route
@app.route('/')
def home():
    return render_template('home.html')

# Cursussen overzicht
@app.route('/cursussen')
def cursussen():
    cursus_data = [
        {
            'naam': 'Python Fundamentals',
            'duur': '8 weken',
            'prijs': '€399',
            'beschrijving': 'Leer de basis van Python programmering'
        },
        {
            'naam': 'Data Science met Python',
            'duur': '12 weken', 
            'prijs': '€699',
            'beschrijving': 'Verdiep je in data analyse en machine learning'
        },
        {
            'naam': 'Web Development',
            'duur': '10 weken',
            'prijs': '€599',
            'beschrijving': 'Bouw moderne webapplicaties met Python'
        }
    ]
    return render_template('cursussen.html', cursussen=cursus_data)

if __name__ == '__main__':
    init_db()
    app.run(debug=True, host='0.0.0.0', port=5000)

Flask Templates met Jinja2





    
    
    {% block title %}Accidaccia Cursussen{% endblock %}
    


    

    
{% with messages = get_flashed_messages() %} {% if messages %} {% for message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %} {% block content %}{% endblock %}
{% extends "base.html" %} {% block title %}Cursussen - Accidaccia{% endblock %} {% block content %}

Onze Python Cursussen

{% for cursus in cursussen %}
{{ cursus.naam }}

{{ cursus.beschrijving }}

  • Duur: {{ cursus.duur }}
  • Prijs: {{ cursus.prijs }}
{% endfor %}
{% endblock %}

Flask Forms en Database Integratie

# Uitbreiding van app.py
from flask_wtf import FlaskForm
from wtforms import StringField, EmailField, SelectField, SubmitField
from wtforms.validators import DataRequired, Email

class AanmeldForm(FlaskForm):
    naam = StringField('Naam', validators=[DataRequired()])
    email = EmailField('E-mail', validators=[DataRequired(), Email()])
    cursus = SelectField('Cursus', choices=[
        ('Python Fundamentals', 'Python Fundamentals'),
        ('Data Science met Python', 'Data Science met Python'),
        ('Web Development', 'Web Development')
    ], validators=[DataRequired()])
    submit = SubmitField('Aanmelden')

@app.route('/aanmelden/', methods=['GET', 'POST'])
def aanmelden(cursus):
    form = AanmeldForm()
    form.cursus.data = cursus  # Pre-fill cursus
    
    if form.validate_on_submit():
        try:
            conn = sqlite3.connect('cursisten.db')
            c = conn.cursor()
            c.execute(
                'INSERT INTO cursisten (naam, email, cursus) VALUES (?, ?, ?)',
                (form.naam.data, form.email.data, form.cursus.data)
            )
            conn.commit()
            conn.close()
            
            flash(f'Bedankt {form.naam.data}! Je bent aangemeld voor {form.cursus.data}.')
            return redirect(url_for('home'))
        except sqlite3.IntegrityError:
            flash('Dit e-mailadres is al geregistreerd.')
    
    return render_template('aanmelden.html', form=form, cursus=cursus)

@app.route('/cursisten')
def cursisten_overzicht():
    conn = sqlite3.connect('cursisten.db')
    c = conn.cursor()
    c.execute('SELECT naam, email, cursus, aanmelddatum FROM cursisten ORDER BY aanmelddatum DESC')
    cursisten = c.fetchall()
    conn.close()
    
    return render_template('cursisten.html', cursisten=cursisten)

# API endpoint
@app.route('/api/cursisten')
def api_cursisten():
    from flask import jsonify
    
    conn = sqlite3.connect('cursisten.db')
    c = conn.cursor()
    c.execute('SELECT naam, email, cursus, aanmelddatum FROM cursisten')
    cursisten = c.fetchall()
    conn.close()
    
    cursisten_data = []
    for cursist in cursisten:
        cursisten_data.append({
            'naam': cursist[0],
            'email': cursist[1],
            'cursus': cursist[2],
            'aanmelddatum': cursist[3]
        })
    
    return jsonify(cursisten_data)

Flask Security en Authenticatie

# Beveiligde Flask app
from werkzeug.security import generate_password_hash, check_password_hash
from functools import wraps

# User model
def create_user_table():
    conn = sqlite3.connect('cursisten.db')
    c = conn.cursor()
    c.execute('''
        CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY,
            username TEXT UNIQUE NOT NULL,
            email TEXT UNIQUE NOT NULL,
            password_hash TEXT NOT NULL,
            role TEXT DEFAULT 'user'
        )
    ''')
    conn.commit()
    conn.close()

# Authentication decorator
def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'user_id' not in session:
            flash('Je moet ingelogd zijn om deze pagina te bekijken.')
            return redirect(url_for('login'))
        return f(*args, **kwargs)
    return decorated_function

def admin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'user_id' not in session:
            return redirect(url_for('login'))
        
        conn = sqlite3.connect('cursisten.db')
        c = conn.cursor()
        c.execute('SELECT role FROM users WHERE id = ?', (session['user_id'],))
        user = c.fetchone()
        conn.close()
        
        if not user or user[0] != 'admin':
            flash('Je hebt geen toegang tot deze pagina.')
            return redirect(url_for('home'))
        
        return f(*args, **kwargs)
    return decorated_function

# Login/Register routes
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        
        conn = sqlite3.connect('cursisten.db')
        c = conn.cursor()
        c.execute('SELECT id, password_hash FROM users WHERE username = ?', (username,))
        user = c.fetchone()
        conn.close()
        
        if user and check_password_hash(user[1], password):
            session['user_id'] = user[0]
            flash('Succesvol ingelogd!')
            return redirect(url_for('dashboard'))
        else:
            flash('Ongeldige inloggegevens.')
    
    return render_template('login.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        email = request.form['email']
        password = request.form['password']
        
        password_hash = generate_password_hash(password)
        
        try:
            conn = sqlite3.connect('cursisten.db')
            c = conn.cursor()
            c.execute(
                'INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)',
                (username, email, password_hash)
            )
            conn.commit()
            conn.close()
            
            flash('Account succesvol aangemaakt! Je kunt nu inloggen.')
            return redirect(url_for('login'))
        except sqlite3.IntegrityError:
            flash('Gebruikersnaam of e-mail al in gebruik.')
    
    return render_template('register.html')

@app.route('/dashboard')
@login_required
def dashboard():
    return render_template('dashboard.html')

@app.route('/admin')
@admin_required
def admin():
    conn = sqlite3.connect('cursisten.db')
    c = conn.cursor()
    c.execute('SELECT COUNT(*) FROM cursisten')
    total_cursisten = c.fetchone()[0]
    
    c.execute('SELECT cursus, COUNT(*) FROM cursisten GROUP BY cursus')
    cursus_stats = c.fetchall()
    conn.close()
    
    return render_template('admin.html', 
                         total_cursisten=total_cursisten,
                         cursus_stats=cursus_stats)

Django: Full-Featured Web Development

Nu gaan we over naar Django, Python's meest uitgebreide web framework.

Django Project Setup

# Terminal commands
# django-admin startproject accidaccia_site
# cd accidaccia_site
# python manage.py startapp cursussen

# settings.py (belangrijkste instellingen)
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'cursussen',  # onze app
]

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

LANGUAGE_CODE = 'nl-nl'
TIME_ZONE = 'Europe/Brussels'
USE_I18N = True
USE_TZ = True

# Voor productie
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'accidaccia.com']
DEBUG = False  # In productie altijd False!

Django Models: Database Design

# cursussen/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 Cursus(models.Model):
    NIVEAU_CHOICES = [
        ('beginner', 'Beginner'),
        ('intermediate', 'Intermediate'),
        ('advanced', 'Gevorderd'),
    ]
    
    naam = models.CharField(max_length=200)
    beschrijving = models.TextField()
    prijs = models.DecimalField(max_digits=6, decimal_places=2)
    duur_weken = models.IntegerField()
    niveau = models.CharField(max_length=20, choices=NIVEAU_CHOICES)
    max_deelnemers = models.IntegerField(default=12)
    actief = models.BooleanField(default=True)
    aangemaakt_op = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        verbose_name_plural = "Cursussen"
        ordering = ['naam']
    
    def __str__(self):
        return self.naam
    
    def get_absolute_url(self):
        return reverse('cursus_detail', kwargs={'pk': self.pk})
    
    @property
    def aantal_aanmeldingen(self):
        return self.aanmelding_set.count()
    
    @property
    def is_vol(self):
        return self.aantal_aanmeldingen >= self.max_deelnemers

class Docent(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    expertise = models.CharField(max_length=200)
    jaren_ervaring = models.IntegerField()
    linkedin_url = models.URLField(blank=True)
    
    def __str__(self):
        return f"{self.user.first_name} {self.user.last_name}"

class Aanmelding(models.Model):
    STATUS_CHOICES = [
        ('pending', 'In Behandeling'),
        ('confirmed', 'Bevestigd'),
        ('cancelled', 'Geannuleerd'),
        ('completed', 'Voltooid'),
    ]
    
    cursus = models.ForeignKey(Cursus, on_delete=models.CASCADE)
    naam = models.CharField(max_length=100)
    email = models.EmailField()
    telefoon = models.CharField(max_length=20)
    ervaring = models.TextField(help_text="Vertel over je huidige programmeerervaring")
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
    aangemeld_op = models.DateTimeField(auto_now_add=True)
    bevestigd_op = models.DateTimeField(null=True, blank=True)
    
    class Meta:
        verbose_name_plural = "Aanmeldingen"
        ordering = ['-aangemeld_op']
        unique_together = ['cursus', 'email']  # Voorkom dubbele aanmeldingen
    
    def __str__(self):
        return f"{self.naam} - {self.cursus.naam}"
    
    def save(self, *args, **kwargs):
        if self.status == 'confirmed' and not self.bevestigd_op:
            self.bevestigd_op = timezone.now()
        super().save(*args, **kwargs)

class Review(models.Model):
    cursus = models.ForeignKey(Cursus, on_delete=models.CASCADE)
    naam = models.CharField(max_length=100)
    email = models.EmailField()
    rating = models.IntegerField(choices=[(i, i) for i in range(1, 6)])
    commentaar = models.TextField()
    aangemaakt_op = models.DateTimeField(auto_now_add=True)
    goedgekeurd = models.BooleanField(default=False)
    
    class Meta:
        ordering = ['-aangemaakt_op']
    
    def __str__(self):
        return f"Review van {self.naam} voor {self.cursus.naam}"

Django Views: Business Logic

# cursussen/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.views.generic import ListView, DetailView, CreateView
from django.contrib import messages
from django.core.mail import send_mail
from django.db.models import Q, Avg
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
from django.contrib.admin.views.decorators import staff_member_required
from .models import Cursus, Aanmelding, Review
from .forms import AanmeldingForm, ReviewForm

class CursusListView(ListView):
    model = Cursus
    template_name = 'cursussen/lijst.html'
    context_object_name = 'cursussen'
    paginate_by = 6
    
    def get_queryset(self):
        queryset = Cursus.objects.filter(actief=True)
        
        # Search functionality
        query = self.request.GET.get('q')
        if query:
            queryset = queryset.filter(
                Q(naam__icontains=query) | Q(beschrijving__icontains=query)
            )
        
        # Filter op niveau
        niveau = self.request.GET.get('niveau')
        if niveau:
            queryset = queryset.filter(niveau=niveau)
        
        return queryset.annotate(
            gemiddelde_rating=Avg('review__rating')
        )

class CursusDetailView(DetailView):
    model = Cursus
    template_name = 'cursussen/detail.html'
    context_object_name = 'cursus'
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['aanmelding_form'] = AanmeldingForm()
        context['review_form'] = ReviewForm()
        context['reviews'] = self.object.review_set.filter(goedgekeurd=True)[:5]
        context['gemiddelde_rating'] = self.object.review_set.filter(
            goedgekeurd=True
        ).aggregate(Avg('rating'))['rating__avg']
        return context

def aanmelden_cursus(request, cursus_id):
    cursus = get_object_or_404(Cursus, id=cursus_id)
    
    if cursus.is_vol:
        messages.error(request, 'Deze cursus is helaas vol.')
        return redirect('cursus_detail', pk=cursus_id)
    
    if request.method == 'POST':
        form = AanmeldingForm(request.POST)
        if form.is_valid():
            aanmelding = form.save(commit=False)
            aanmelding.cursus = cursus
            
            try:
                aanmelding.save()
                
                # Verstuur bevestigingsmail
                send_mail(
                    subject=f'Aanmelding bevestigd: {cursus.naam}',
                    message=f'Beste {aanmelding.naam},\n\n'
                           f'Uw aanmelding voor {cursus.naam} is ontvangen.\n'
                           f'Wij nemen binnen 24 uur contact met u op.\n\n'
                           f'Met vriendelijke groet,\nTeam Accidaccia',
                    from_email='[email protected]',
                    recipient_list=[aanmelding.email],
                    fail_silently=False,
                )
                
                messages.success(
                    request, 
                    f'Bedankt {aanmelding.naam}! Uw aanmelding is ontvangen.'
                )
                return redirect('aanmelding_succes')
                
            except Exception as e:
                messages.error(request, 'Er is een fout opgetreden. Probeer het opnieuw.')
    
    return redirect('cursus_detail', pk=cursus_id)

@staff_member_required
def aanmeldingen_dashboard(request):
    aanmeldingen = Aanmelding.objects.all().order_by('-aangemeld_op')
    
    # Filter opties
    status_filter = request.GET.get('status')
    if status_filter:
        aanmeldingen = aanmeldingen.filter(status=status_filter)
    
    cursus_filter = request.GET.get('cursus')
    if cursus_filter:
        aanmeldingen = aanmeldingen.filter(cursus_id=cursus_filter)
    
    context = {
        'aanmeldingen': aanmeldingen,
        'cursussen': Cursus.objects.all(),
        'status_choices': Aanmelding.STATUS_CHOICES,
    }
    
    return render(request, 'admin/aanmeldingen_dashboard.html', context)

# API Views
def api_cursussen(request):
    cursussen = Cursus.objects.filter(actief=True).values(
        'id', 'naam', 'prijs', 'duur_weken', 'niveau'
    )
    return JsonResponse(list(cursussen), safe=False)

def api_cursus_detail(request, cursus_id):
    try:
        cursus = Cursus.objects.get(id=cursus_id, actief=True)
        data = {
            'id': cursus.id,
            'naam': cursus.naam,
            'beschrijving': cursus.beschrijving,
            'prijs': float(cursus.prijs),
            'duur_weken': cursus.duur_weken,
            'niveau': cursus.niveau,
            'aantal_aanmeldingen': cursus.aantal_aanmeldingen,
            'is_vol': cursus.is_vol,
        }
        return JsonResponse(data)
    except Cursus.DoesNotExist:
        return JsonResponse({'error': 'Cursus niet gevonden'}, status=404)

Django Forms: User Input Handling

# cursussen/forms.py
from django import forms
from django.core.validators import RegexValidator
from .models import Aanmelding, Review

class AanmeldingForm(forms.ModelForm):
    telefoon = forms.CharField(
        validators=[RegexValidator(r'^\d{10,15}$', 'Voer een geldig telefoonnummer in.')],
        widget=forms.TextInput(attrs={
            'placeholder': '+32 123 456 789',
            'class': 'form-control'
        })
    )
    
    ervaring = forms.CharField(
        widget=forms.Textarea(attrs={
            'rows': 4,
            'placeholder': 'Vertel ons over uw programmeerervaring...',
            'class': 'form-control'
        })
    )
    
    privacybeleid = forms.BooleanField(
        required=True,
        label='Ik ga akkoord met het privacybeleid',
        widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
    )
    
    class Meta:
        model = Aanmelding
        fields = ['naam', 'email', 'telefoon', 'ervaring']
        widgets = {
            'naam': forms.TextInput(attrs={'class': 'form-control'}),
            'email': forms.EmailInput(attrs={'class': 'form-control'}),
        }
    
    def clean_email(self):
        email = self.cleaned_data['email']
        # Check of email al bestaat voor deze cursus
        if hasattr(self, 'cursus'):  # Set in view
            if Aanmelding.objects.filter(cursus=self.cursus, email=email).exists():
                raise forms.ValidationError(
                    'U bent al aangemeld voor deze cursus met dit e-mailadres.'
                )
        return email

class ReviewForm(forms.ModelForm):
    class Meta:
        model = Review
        fields = ['naam', 'email', 'rating', 'commentaar']
        widgets = {
            'naam': forms.TextInput(attrs={'class': 'form-control'}),
            'email': forms.EmailInput(attrs={'class': 'form-control'}),
            'rating': forms.Select(attrs={'class': 'form-select'}),
            'commentaar': forms.Textarea(attrs={
                'rows': 4,
                'class': 'form-control',
                'placeholder': 'Deel uw ervaring met deze cursus...'
            }),
        }

class CursusZoekForm(forms.Form):
    q = forms.CharField(
        max_length=100,
        required=False,
        widget=forms.TextInput(attrs={
            'placeholder': 'Zoek cursussen...',
            'class': 'form-control'
        })
    )
    
    niveau = forms.ChoiceField(
        choices=[('', 'Alle niveaus')] + Cursus.NIVEAU_CHOICES,
        required=False,
        widget=forms.Select(attrs={'class': 'form-select'})
    )
    
    max_prijs = forms.DecimalField(
        max_digits=6,
        decimal_places=2,
        required=False,
        widget=forms.NumberInput(attrs={
            'class': 'form-control',
            'placeholder': 'Max prijs'
        })
    )

Django Admin: Automatische Backend

# cursussen/admin.py
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils.safestring import mark_safe
from .models import Cursus, Docent, Aanmelding, Review

@admin.register(Cursus)
class CursusAdmin(admin.ModelAdmin):
    list_display = ['naam', 'prijs', 'duur_weken', 'niveau', 'aantal_aanmeldingen_display', 'actief']
    list_filter = ['niveau', 'actief', 'aangemaakt_op']
    search_fields = ['naam', 'beschrijving']
    ordering = ['naam']
    list_editable = ['prijs', 'actief']
    
    fieldsets = (
        ('Basis Informatie', {
            'fields': ('naam', 'beschrijving', 'niveau')
        }),
        ('Cursus Details', {
            'fields': ('duur_weken', 'prijs', 'max_deelnemers')
        }),
        ('Status', {
            'fields': ('actief',)
        }),
    )
    
    def aantal_aanmeldingen_display(self, obj):
        count = obj.aantal_aanmeldingen
        url = reverse('admin:cursussen_aanmelding_changelist') + f'?cursus__id={obj.id}'
        return format_html('{} aanmeldingen', url, count)
    
    aantal_aanmeldingen_display.short_description = 'Aanmeldingen'

@admin.register(Aanmelding)
class AanmeldingAdmin(admin.ModelAdmin):
    list_display = ['naam', 'email', 'cursus', 'status', 'aangemeld_op']
    list_filter = ['status', 'cursus', 'aangemeld_op']
    search_fields = ['naam', 'email', 'cursus__naam']
    ordering = ['-aangemeld_op']
    list_editable = ['status']
    readonly_fields = ['aangemeld_op']
    
    fieldsets = (
        ('Persoonlijke Gegevens', {
            'fields': ('naam', 'email', 'telefoon')
        }),
        ('Cursus Informatie', {
            'fields': ('cursus', 'ervaring')
        }),
        ('Status', {
            'fields': ('status', 'aangemeld_op', 'bevestigd_op')
        }),
    )
    
    actions = ['bevestig_aanmeldingen', 'annuleer_aanmeldingen']
    
    def bevestig_aanmeldingen(self, request, queryset):
        updated = queryset.update(status='confirmed')
        self.message_user(request, f'{updated} aanmeldingen bevestigd.')
    bevestig_aanmeldingen.short_description = "Bevestig geselecteerde aanmeldingen"
    
    def annuleer_aanmeldingen(self, request, queryset):
        updated = queryset.update(status='cancelled')
        self.message_user(request, f'{updated} aanmeldingen geannuleerd.')
    annuleer_aanmeldingen.short_description = "Annuleer geselecteerde aanmeldingen"

@admin.register(Review)
class ReviewAdmin(admin.ModelAdmin):
    list_display = ['naam', 'cursus', 'rating', 'goedgekeurd', 'aangemaakt_op']
    list_filter = ['rating', 'goedgekeurd', 'cursus']
    search_fields = ['naam', 'commentaar', 'cursus__naam']
    ordering = ['-aangemaakt_op']
    list_editable = ['goedgekeurd']
    
    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        if not request.user.is_superuser:
            # Normale staff ziet alleen goedgekeurde reviews
            queryset = queryset.filter(goedgekeurd=True)
        return queryset

@admin.register(Docent)
class DocentAdmin(admin.ModelAdmin):
    list_display = ['user', 'expertise', 'jaren_ervaring']
    search_fields = ['user__first_name', 'user__last_name', 'expertise']

RESTful APIs met Django REST Framework

# Installeer django-rest-framework
# pip install djangorestframework

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

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20
}

# cursussen/serializers.py
from rest_framework import serializers
from .models import Cursus, Aanmelding, Review

class CursusSerializer(serializers.ModelSerializer):
    aantal_aanmeldingen = serializers.ReadOnlyField()
    gemiddelde_rating = serializers.SerializerMethodField()
    
    class Meta:
        model = Cursus
        fields = ['id', 'naam', 'beschrijving', 'prijs', 'duur_weken', 
                 'niveau', 'max_deelnemers', 'aantal_aanmeldingen', 
                 'gemiddelde_rating', 'actief']
    
    def get_gemiddelde_rating(self, obj):
        reviews = obj.review_set.filter(goedgekeurd=True)
        if reviews.exists():
            return reviews.aggregate(models.Avg('rating'))['rating__avg']
        return None

class AanmeldingSerializer(serializers.ModelSerializer):
    cursus_naam = serializers.CharField(source='cursus.naam', read_only=True)
    
    class Meta:
        model = Aanmelding
        fields = ['id', 'cursus', 'cursus_naam', 'naam', 'email', 
                 'telefoon', 'ervaring', 'status', 'aangemeld_op']
        read_only_fields = ['aangemeld_op']

# cursussen/api_views.py
from rest_framework import generics, permissions, status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from .models import Cursus, Aanmelding
from .serializers import CursusSerializer, AanmeldingSerializer

class CursusListAPIView(generics.ListAPIView):
    queryset = Cursus.objects.filter(actief=True)
    serializer_class = CursusSerializer
    permission_classes = [permissions.AllowAny]

class CursusDetailAPIView(generics.RetrieveAPIView):
    queryset = Cursus.objects.filter(actief=True)
    serializer_class = CursusSerializer
    permission_classes = [permissions.AllowAny]

class AanmeldingCreateAPIView(generics.CreateAPIView):
    queryset = Aanmelding.objects.all()
    serializer_class = AanmeldingSerializer
    permission_classes = [permissions.AllowAny]
    
    def perform_create(self, serializer):
        cursus = serializer.validated_data['cursus']
        if cursus.is_vol:
            raise serializers.ValidationError("Deze cursus is vol.")
        serializer.save()

@api_view(['GET'])
@permission_classes([permissions.AllowAny])
def cursus_statistieken(request):
    """API endpoint voor cursus statistieken"""
    total_cursussen = Cursus.objects.filter(actief=True).count()
    total_aanmeldingen = Aanmelding.objects.count()
    
    niveau_stats = {}
    for niveau_code, niveau_naam in Cursus.NIVEAU_CHOICES:
        count = Cursus.objects.filter(niveau=niveau_code, actief=True).count()
        niveau_stats[niveau_naam] = count
    
    data = {
        'total_cursussen': total_cursussen,
        'total_aanmeldingen': total_aanmeldingen,
        'niveau_verdeling': niveau_stats,
    }
    
    return Response(data)

Deployment en Production

Production Settings

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

DEBUG = False
ALLOWED_HOSTS = ['accidaccia.com', 'www.accidaccia.com']

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

# Security
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'

# Static files
STATIC_ROOT = '/var/www/accidaccia/static'
MEDIA_ROOT = '/var/www/accidaccia/media'

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

# Logging
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': '/var/log/django/accidaccia.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'INFO',
            'propagate': True,
        },
    },
}

Docker Setup

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "accidaccia_site.wsgi:application"]

# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DEBUG=False
      - DB_NAME=accidaccia_db
      - DB_USER=postgres
      - DB_PASSWORD=password
      - DB_HOST=db
    depends_on:
      - db
    volumes:
      - static_volume:/app/static
      - media_volume:/app/media

  db:
    image: postgres:13
    environment:
      POSTGRES_DB: accidaccia_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data/

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - static_volume:/app/static
      - media_volume:/app/media
    depends_on:
      - web

volumes:
  postgres_data:
  static_volume:
  media_volume:

Testing en Quality Assurance

# cursussen/tests.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from .models import Cursus, Aanmelding

class CursusModelTest(TestCase):
    def setUp(self):
        self.cursus = Cursus.objects.create(
            naam="Test Cursus",
            beschrijving="Test beschrijving",
            prijs=299.99,
            duur_weken=8,
            niveau="beginner",
            max_deelnemers=12
        )
    
    def test_cursus_creation(self):
        self.assertEqual(self.cursus.naam, "Test Cursus")
        self.assertFalse(self.cursus.is_vol)
        self.assertEqual(self.cursus.aantal_aanmeldingen, 0)
    
    def test_cursus_vol(self):
        # Maak 12 aanmeldingen
        for i in range(12):
            Aanmelding.objects.create(
                cursus=self.cursus,
                naam=f"Student {i}",
                email=f"student{i}@test.com",
                telefoon="1234567890",
                ervaring="Test ervaring"
            )
        
        self.assertTrue(self.cursus.is_vol)

class CursusViewTest(TestCase):
    def setUp(self):
        self.client = Client()
        self.cursus = Cursus.objects.create(
            naam="Test Cursus",
            beschrijving="Test beschrijving",
            prijs=299.99,
            duur_weken=8,
            niveau="beginner"
        )
    
    def test_cursus_list_view(self):
        response = self.client.get(reverse('cursus_list'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Test Cursus")
    
    def test_cursus_detail_view(self):
        response = self.client.get(
            reverse('cursus_detail', kwargs={'pk': self.cursus.pk})
        )
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, self.cursus.naam)
    
    def test_aanmelding_form_valid(self):
        response = self.client.post(
            reverse('aanmelden_cursus', kwargs={'cursus_id': self.cursus.id}),
            {
                'naam': 'Test Student',
                'email': '[email protected]',
                'telefoon': '1234567890',
                'ervaring': 'Geen ervaring',
                'privacybeleid': True
            }
        )
        
        self.assertEqual(Aanmelding.objects.count(), 1)
        aanmelding = Aanmelding.objects.first()
        self.assertEqual(aanmelding.naam, 'Test Student')

# Performance tests
from django.test.utils import override_settings
from django.test import TransactionTestCase
import time

class PerformanceTest(TransactionTestCase):
    def test_cursus_list_performance(self):
        # Maak veel cursussen
        for i in range(100):
            Cursus.objects.create(
                naam=f"Cursus {i}",
                beschrijving="Test",
                prijs=299.99,
                duur_weken=8,
                niveau="beginner"
            )
        
        start_time = time.time()
        response = self.client.get(reverse('cursus_list'))
        end_time = time.time()
        
        self.assertEqual(response.status_code, 200)
        self.assertLess(end_time - start_time, 1.0)  # Moet onder 1 seconde

Best Practices voor Python Web Development

Security

  • Input validatie: Valideer alle user input
  • SQL Injection: Gebruik altijd ORM queries
  • XSS Protection: Escape user content in templates
  • CSRF Protection: Gebruik CSRF tokens in forms
  • Authentication: Implementeer sterke authenticatie

Performance

  • Database queries: Gebruik select_related en prefetch_related
  • Caching: Implementeer Redis/Memcached voor caching
  • Static files: Gebruik CDN voor static content
  • Database indexing: Index frequently queried fields
  • Lazy loading: Load data on demand

Code Quality

  • Testing: Schrijf unit tests en integration tests
  • Code style: Gebruik PEP 8 en tools zoals Black
  • Documentation: Documenteer je code en API
  • Version control: Gebruik Git met meaningful commits
  • Code reviews: Implementeer peer reviews

Volgende Stappen

Nu u de basis van Python web development beheerst, kunt u zich verder specialiseren in:

  • Frontend frameworks: React, Vue.js integratie
  • GraphQL: Moderne API architectuur
  • Microservices: Distributed architecture
  • Cloud deployment: AWS, GCP, Azure
  • DevOps: CI/CD pipelines, monitoring

Conclusie

Python web development biedt ongelooflijke mogelijkheden voor het bouwen van moderne, schaalbare webapplicaties. Of u nu kiest voor Flask's flexibiliteit of Django's uitgebreide feature set, beide frameworks stellen u in staat om professionele webapplicaties te ontwikkelen.

De sleutel tot succes ligt in het begrijpen van de fundamentele concepten, het toepassen van best practices, en het voortdurend leren van nieuwe technieken en tools. Met de kennis uit dit artikel bent u goed voorbereid om uw eigen web development projecten te starten.

Klaar om te Beginnen met Web Development?

Onze Web Development met Python cursus behandelt alle aspecten van moderne webontwikkeling met hands-on projecten.

Neem Contact Op Bekijk Cursussen