Witaj ponownie, @name! Darwin tutaj, gotowy pokazać Ci profesjonalne formularze z Flask-WTF! 📝✅
Do tej pory obsługiwałeś/aś formularze ręcznie przez
request.form. To działa, ale nie ma walidacji, nie ma ochrony CSRF, i jest podatne na błędy! 😰Analogia Safari: Ręczna obsługa formularzy to jak ręczne sprawdzanie każdego turysty bez listy kontrolnej - łatwo coś pominąć! Flask-WTF to jak profesjonalny formularz zgłoszeniowy z automatyczną walidacją - wszystko jest sprawdzone! 📋✅
1@app.route('/add-species', methods=['POST'])
2def add_species():
3 name = request.form.get('name')
4 population = request.form.get('population')
5
6 # ❌ Brak walidacji - co jeśli name jest puste?
7 # ❌ Brak walidacji typu - co jeśli population to tekst?
8 # ❌ Brak ochrony CSRF
9 # ❌ Trzeba ręcznie sprawdzać wszystko
10
11 if not name:
12 return "Błąd: Nazwa jest wymagana", 400
13
14 try:
15 population = int(population)
16 except ValueError:
17 return "Błąd: Populacja musi być liczbą", 400
18
19 # Dodaj do bazy...Problemy: ❌ Długi kod walidacji ❌ Brak ochrony CSRF (Cross-Site Request Forgery) ❌ Trzeba powtarzać walidację dla każdego formularza ❌ Brak automatycznego wyświetlania błędów ❌ Trudne w utrzymaniu
Flask-WTF to rozszerzenie integrujące WTForms (library do formularzy) z Flask.
Co daje Flask-WTF? ✅ Automatyczna walidacja - nie musisz sprawdzać ręcznie ✅ Ochrona CSRF - bezpieczeństwo out-of-the-box ✅ Klasy formularzy - czytelny, DRY kod ✅ Renderowanie w templates - łatwe wyświetlanie ✅ Komunikaty błędów - automatyczne wyświetlanie
1pip install Flask-WTFFlask-WTF automatycznie instaluje WTForms (core library).
1pip freeze > requirements.txtFlask-WTF wymaga secret key do ochrony CSRF:
1# app.py
2from flask import Flask
3from flask_wtf import FlaskForm
4
5app = Flask(__name__)
6
7# SECRET_KEY jest wymagany dla CSRF protection
8app.config['SECRET_KEY'] = 'twój-super-tajny-klucz-12345' # Zmień w produkcji!
9
10# Alternatywnie - użyj os.urandom()
11import os
12app.config['SECRET_KEY'] = os.urandom(24)⚠️ WAŻNE: W produkcji używaj zmiennych środowiskowych zamiast hardcoded key!
1import os
2app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY') or 'dev-key-123'Formularz to klasa dziedzicząca z
FlaskForm:1# forms.py
2from flask_wtf import FlaskForm
3from wtforms import StringField, IntegerField, TextAreaField, SelectField, SubmitField
4from wtforms.validators import DataRequired, Length, NumberRange, Optional
5
6class AddSpeciesForm(FlaskForm):
7 name = StringField('Nazwa gatunku',
8 validators=[DataRequired(message='Nazwa jest wymagana'),
9 Length(min=2, max=100, message='Nazwa musi mieć 2-100 znaków')])
10
11 scientific_name = StringField('Nazwa naukowa',
12 validators=[DataRequired(),
13 Length(min=2, max=150)])
14
15 population = IntegerField('Populacja',
16 validators=[DataRequired(),
17 NumberRange(min=0, message='Populacja nie może być ujemna')])
18
19 habitat = SelectField('Siedlisko',
20 choices=[('savanna', 'Sawanna'),
21 ('forest', 'Las'),
22 ('desert', 'Pustynia'),
23 ('wetland', 'Mokradło')],
24 validators=[DataRequired()])
25
26 notes = TextAreaField('Notatki',
27 validators=[Optional(), Length(max=500)])
28
29 submit = SubmitField('Dodaj gatunek')Pola formularza:
StringField - pole tekstowe (input type="text")IntegerField - liczba całkowita (input type="number")TextAreaField - wieloliniowy tekst (textarea)SelectField - lista rozwijana (select)SubmitField - przycisk submit (button type="submit")Inne typy pól:
EmailField - email z walidacjąPasswordField - hasło (input type="password")BooleanField - checkboxDateField - dataFileField - upload plikuRadioField - radio buttonsDecimalField - liczba zmiennoprzecinkowaWTForms ma wbudowane validatory:
1from wtforms.validators import (
2 DataRequired, # Pole jest wymagane
3 Optional, # Pole opcjonalne
4 Length, # Długość tekstu
5 Email, # Walidacja email
6 EqualTo, # Równe innemu polu (hasła)
7 NumberRange, # Zakres liczb
8 Regexp, # Regex
9 URL, # Walidacja URL
10 AnyOf, # Jedna z wartości
11 NoneOf # Żadna z wartości
12)Przykłady:
1from wtforms import StringField, IntegerField, PasswordField
2from wtforms.validators import DataRequired, Length, Email, EqualTo, NumberRange
3
4class RegistrationForm(FlaskForm):
5 username = StringField('Username',
6 validators=[DataRequired(),
7 Length(min=4, max=20)])
8
9 email = EmailField('Email',
10 validators=[DataRequired(), Email()])
11
12 password = PasswordField('Hasło',
13 validators=[DataRequired(),
14 Length(min=8)])
15
16 confirm_password = PasswordField('Potwierdź hasło',
17 validators=[DataRequired(),
18 EqualTo('password', message='Hasła muszą być identyczne')])
19
20 age = IntegerField('Wiek',
21 validators=[NumberRange(min=13, max=120,
22 message='Wiek musi być między 13 a 120')])1# app.py
2from flask import Flask, render_template, redirect, url_for, flash
3from forms import AddSpeciesForm
4
5app = Flask(__name__)
6app.config['SECRET_KEY'] = 'your-secret-key-123'
7
8species_db = {}
9next_id = 1
10
11@app.route('/species/add', methods=['GET', 'POST'])
12def add_species():
13 form = AddSpeciesForm()
14
15 # POST request + walidacja przeszła
16 if form.validate_on_submit():
17 global next_id
18
19 # Pobierz dane z formularza
20 new_species = {
21 'id': next_id,
22 'name': form.name.data,
23 'scientific_name': form.scientific_name.data,
24 'population': form.population.data,
25 'habitat': form.habitat.data,
26 'notes': form.notes.data
27 }
28
29 species_db[next_id] = new_species
30 next_id += 1
31
32 # Flash message - komunikat sukcesu
33 flash(f'Gatunek {new_species["name"]} został dodany!', 'success')
34
35 return redirect(url_for('list_species'))
36
37 # GET request lub walidacja nie przeszła
38 return render_template('add_species.html', form=form)Jak to działa?
form.validate_on_submit() sprawdza:form.nazwa_pola.data)1{% extends 'base.html' %}
2
3{% block title %}Dodaj gatunek - Safari Database{% endblock %}
4
5{% block content %}
6 <h2>➕ Dodaj nowy gatunek</h2>
7
8 <form method="POST" novalidate>
9 {{ form.hidden_tag() }} <!-- CSRF token -->
10
11 <div style="margin: 15px 0;">
12 {{ form.name.label }}
13 {{ form.name(size=40) }}
14
15 {% if form.name.errors %}
16 <ul style="color: red; margin: 5px 0;">
17 {% for error in form.name.errors %}
18 <li>{{ error }}</li>
19 {% endfor %}
20 </ul>
21 {% endif %}
22 </div>
23
24 <div style="margin: 15px 0;">
25 {{ form.scientific_name.label }}
26 {{ form.scientific_name(size=40) }}
27
28 {% if form.scientific_name.errors %}
29 <ul style="color: red; margin: 5px 0;">
30 {% for error in form.scientific_name.errors %}
31 <li>{{ error }}</li>
32 {% endfor %}
33 </ul>
34 {% endif %}
35 </div>
36
37 <div style="margin: 15px 0;">
38 {{ form.population.label }}
39 {{ form.population() }}
40
41 {% if form.population.errors %}
42 <ul style="color: red; margin: 5px 0;">
43 {% for error in form.population.errors %}
44 <li>{{ error }}</li>
45 {% endfor %}
46 </ul>
47 {% endif %}
48 </div>
49
50 <div style="margin: 15px 0;">
51 {{ form.habitat.label }}
52 {{ form.habitat() }}
53
54 {% if form.habitat.errors %}
55 <ul style="color: red; margin: 5px 0;">
56 {% for error in form.habitat.errors %}
57 <li>{{ error }}</li>
58 {% endfor %}
59 </ul>
60 {% endif %}
61 </div>
62
63 <div style="margin: 15px 0;">
64 {{ form.notes.label }}
65 {{ form.notes(rows=5, cols=40) }}
66
67 {% if form.notes.errors %}
68 <ul style="color: red; margin: 5px 0;">
69 {% for error in form.notes.errors %}
70 <li>{{ error }}</li>
71 {% endfor %}
72 </ul>
73 {% endif %}
74 </div>
75
76 <div style="margin-top: 20px;">
77 {{ form.submit(style='padding: 10px 20px; background: #2c5f2d; color: white; border: none; border-radius: 4px; cursor: pointer;') }}
78 </div>
79 </form>
80
81 <p style="margin-top: 20px;">
82 <a href="{{ url_for('list_species') }}">← Powrót do listy</a>
83 </p>
84{% endblock %}Kluczowe elementy:
{{ form.hidden_tag() }} - CSRF token (wymagane!){{ form.field_name.label }} - label pola{{ form.field_name() }} - pole formularza{% if form.field_name.errors %} - wyświetl błędyFlask ma wbudowany system flash messages do wyświetlania komunikatów:
1from flask import flash
2
3@app.route('/species/add', methods=['POST'])
4def add_species():
5 form = AddSpeciesForm()
6
7 if form.validate_on_submit():
8 # ... dodaj gatunek ...
9
10 flash('Gatunek został dodany!', 'success')
11 return redirect(url_for('list_species'))
12
13 # Błąd walidacji
14 flash('Popraw błędy w formularzu.', 'error')
15 return render_template('add_species.html', form=form)Kategorie flash:
'success' - sukces (zielony)'error' - błąd (czerwony)'warning' - ostrzeżenie (pomarańczowy)'info' - informacja (niebieski)W
base.html dodaj:1<!DOCTYPE html>
2<html>
3<head>...</head>
4<body>
5 <header>...</header>
6
7 <!-- Flash messages -->
8 {% with messages = get_flashed_messages(with_categories=true) %}
9 {% if messages %}
10 <div style="max-width: 900px; margin: 20px auto;">
11 {% for category, message in messages %}
12 <div style="padding: 15px; margin: 10px 0; border-radius: 4px;
13 {% if category == 'success' %}background: #d4edda; color: #155724; border: 1px solid #c3e6cb;
14 {% elif category == 'error' %}background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;
15 {% elif category == 'warning' %}background: #fff3cd; color: #856404; border: 1px solid #ffeaa7;
16 {% else %}background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb;{% endif %}">
17 {{ message }}
18 </div>
19 {% endfor %}
20 </div>
21 {% endif %}
22 {% endwith %}
23
24 <main>{% block content %}{% endblock %}</main>
25</body>
26</html>Zamiast powtarzać kod dla każdego pola, stwórz makro:
1{# templates/macros.html #}
2{% macro render_field(field) %}
3 <div style="margin: 15px 0;">
4 {{ field.label }}
5 {{ field(size=40, **kwargs) }}
6
7 {% if field.errors %}
8 <ul style="color: #d62828; margin: 5px 0; padding-left: 20px;">
9 {% for error in field.errors %}
10 <li>{{ error }}</li>
11 {% endfor %}
12 </ul>
13 {% endif %}
14 </div>
15{% endmacro %}
16
17{# Użycie w formularzu #}
18{% from 'macros.html' import render_field %}
19
20<form method="POST" novalidate>
21 {{ form.hidden_tag() }}
22
23 {{ render_field(form.name) }}
24 {{ render_field(form.scientific_name) }}
25 {{ render_field(form.population) }}
26 {{ render_field(form.habitat) }}
27 {{ render_field(form.notes) }}
28
29 <div style="margin-top: 20px;">
30 {{ form.submit(style='...') }}
31 </div>
32</form>1# forms.py
2class EditSpeciesForm(FlaskForm):
3 name = StringField('Nazwa', validators=[DataRequired(), Length(min=2, max=100)])
4 scientific_name = StringField('Nazwa naukowa', validators=[DataRequired()])
5 population = IntegerField('Populacja', validators=[DataRequired(), NumberRange(min=0)])
6 habitat = SelectField('Siedlisko',
7 choices=[('savanna', 'Sawanna'), ('forest', 'Las'),
8 ('desert', 'Pustynia'), ('wetland', 'Mokradło')])
9 notes = TextAreaField('Notatki', validators=[Optional(), Length(max=500)])
10 submit = SubmitField('Zapisz zmiany')
11
12# app.py
13@app.route('/species/<int:species_id>/edit', methods=['GET', 'POST'])
14def edit_species(species_id):
15 species = species_db.get(species_id)
16
17 if not species:
18 flash('Gatunek nie znaleziony.', 'error')
19 return redirect(url_for('list_species'))
20
21 form = EditSpeciesForm()
22
23 if form.validate_on_submit():
24 # Aktualizuj dane
25 species['name'] = form.name.data
26 species['scientific_name'] = form.scientific_name.data
27 species['population'] = form.population.data
28 species['habitat'] = form.habitat.data
29 species['notes'] = form.notes.data
30
31 flash(f'Gatunek {species["name"]} został zaktualizowany!', 'success')
32 return redirect(url_for('show_species', species_id=species_id))
33
34 elif request.method == 'GET':
35 # Wypełnij formularz aktualnymi danymi
36 form.name.data = species['name']
37 form.scientific_name.data = species['scientific_name']
38 form.population.data = species['population']
39 form.habitat.data = species['habitat']
40 form.notes.data = species.get('notes', '')
41
42 return render_template('edit_species.html', form=form, species=species)Logika:
Możesz stworzyć własne validatory:
1from wtforms.validators import ValidationError
2
3class AddSpeciesForm(FlaskForm):
4 name = StringField('Nazwa', validators=[DataRequired()])
5 scientific_name = StringField('Nazwa naukowa', validators=[DataRequired()])
6 population = IntegerField('Populacja', validators=[DataRequired()])
7
8 # Custom validator - metoda validate_<field_name>
9 def validate_name(self, field):
10 # Sprawdź, czy gatunek już istnieje
11 for species in species_db.values():
12 if species['name'].lower() == field.data.lower():
13 raise ValidationError('Gatunek o tej nazwie już istnieje!')
14
15 def validate_scientific_name(self, field):
16 # Sprawdź format nazwy naukowej (Genus species)
17 parts = field.data.split()
18 if len(parts) != 2:
19 raise ValidationError('Nazwa naukowa musi mieć format: Genus species')
20
21 # Sprawdź, czy zaczyna się wielką literą
22 if not parts[0][0].isupper() or not parts[1][0].islower():
23 raise ValidationError('Format: pierwsza wielka, druga mała litera')Custom validator function:
1def validate_population_range(form, field):
2 if field.data < 10:
3 raise ValidationError('Populacja musi wynosić co najmniej 10 osobników')
4 if field.data > 1000000:
5 raise ValidationError('Populacja przekracza realistyczny zakres')
6
7class AddSpeciesForm(FlaskForm):
8 population = IntegerField('Populacja',
9 validators=[DataRequired(), validate_population_range])1from flask_wtf import FlaskForm
2from wtforms import StringField, IntegerField, TextAreaField, SelectField, SubmitField
3from wtforms.validators import DataRequired, Length, NumberRange, Optional, ValidationError
4
5class AddSpeciesForm(FlaskForm):
6 name = StringField('Nazwa gatunku',
7 validators=[DataRequired(message='Nazwa jest wymagana'),
8 Length(min=2, max=100)])
9
10 scientific_name = StringField('Nazwa naukowa',
11 validators=[DataRequired(), Length(min=5, max=150)])
12
13 population = IntegerField('Populacja',
14 validators=[DataRequired(), NumberRange(min=0)])
15
16 habitat = SelectField('Siedlisko',
17 choices=[('savanna', 'Sawanna'), ('forest', 'Las'),
18 ('desert', 'Pustynia'), ('wetland', 'Mokradło')],
19 validators=[DataRequired()])
20
21 notes = TextAreaField('Notatki', validators=[Optional(), Length(max=500)])
22
23 submit = SubmitField('Dodaj gatunek')
24
25 def validate_scientific_name(self, field):
26 parts = field.data.split()
27 if len(parts) < 2:
28 raise ValidationError('Nazwa naukowa musi mieć format: Genus species')1from flask import Flask, render_template, redirect, url_for, flash
2from forms import AddSpeciesForm
3
4app = Flask(__name__)
5app.config['SECRET_KEY'] = 'your-secret-key-here'
6
7species_db = {
8 1: {'id': 1, 'name': 'Lew', 'scientific_name': 'Panthera leo',
9 'population': 120, 'habitat': 'savanna', 'notes': ''},
10}
11next_id = 2
12
13@app.route('/')
14def home():
15 return render_template('home.html', species_count=len(species_db))
16
17@app.route('/species')
18def list_species():
19 return render_template('species_list.html', species=list(species_db.values()))
20
21@app.route('/species/add', methods=['GET', 'POST'])
22def add_species():
23 form = AddSpeciesForm()
24
25 if form.validate_on_submit():
26 global next_id
27
28 new_species = {
29 'id': next_id,
30 'name': form.name.data,
31 'scientific_name': form.scientific_name.data,
32 'population': form.population.data,
33 'habitat': form.habitat.data,
34 'notes': form.notes.data
35 }
36
37 species_db[next_id] = new_species
38 next_id += 1
39
40 flash(f'✅ Gatunek {new_species["name"]} został dodany!', 'success')
41 return redirect(url_for('show_species', species_id=new_species['id']))
42
43 return render_template('add_species.html', form=form)
44
45@app.route('/species/<int:species_id>')
46def show_species(species_id):
47 species = species_db.get(species_id)
48 return render_template('species_detail.html', species=species)
49
50if __name__ == '__main__':
51 app.run(debug=True)Analogia Safari finalna: Flask-WTF to jak profesjonalny formularz zgłoszeniowy safari z automatyczną walidacją - sprawdza wiek turysty (NumberRange), wymaga podpisu (DataRequired), weryfikuje email (Email), i chroni przed fałszowaniem dokumentów (CSRF)! Nie musisz ręcznie sprawdzać każdego pola! 📋✅🦁
Następna lekcja: Darwin pokaże Ci Flask-Login - autentykację użytkowników (logowanie, rejestracja, ochrona routes)! 🔐👤