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