Flask Forms, Templates & CRUD OperationsΒΆ
IntroductionΒΆ
Learn how to handle forms, use Jinja2 templates effectively, and implement CRUD (Create, Read, Update, Delete) operations to build real web applications! πβ¨
Note
CRUD operations are the bread and butter of web apps! Almost every app needs to Create, Read, Update, and Delete data. Letβs make it fun! π―
Jinja2 Template BasicsΒΆ
Variables and Expressions:
<!-- templates/home.html -->
<!DOCTYPE html>
<html>
<head>
<title>{{ page_title }} π¨</title>
</head>
<body>
<h1>Hello, {{ username }}! {{ emoji }}</h1>
<p>You have {{ points }} points! β</p>
<p>Double points: {{ points * 2 }}</p>
</body>
</html>
app.py:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
return render_template('home.html',
page_title='My App',
username='Alice',
emoji='π',
points=100)
if __name__ == '__main__':
app.run(debug=True)
Loops in TemplatesΒΆ
<!-- templates/students.html -->
<!DOCTYPE html>
<html>
<head>
<title>Student List π</title>
<style>
body { font-family: Arial; padding: 20px; }
.student {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px;
margin: 10px 0;
border-radius: 8px;
}
</style>
</head>
<body>
<h1>Students π</h1>
{% for student in students %}
<div class="student">
{{ loop.index }}. {{ student.name }} - Grade: {{ student.grade }}
{% if student.grade >= 75 %}β{% else %}π{% endif %}
</div>
{% endfor %}
</body>
</html>
app.py:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/students')
def students():
student_list = [
{'name': 'Alice', 'grade': 85},
{'name': 'Bob', 'grade': 72},
{'name': 'Charlie', 'grade': 90}
]
return render_template('students.html', students=student_list)
if __name__ == '__main__':
app.run(debug=True)
Note
{% for %} loops through lists. loop.index gives the current iteration number (1-based). Use {% if %} for conditionals! π
Template Inheritance (DRY Principle)ΒΆ
base.html (Parent Template):
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My App{% endblock %} π</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background: #f5f5f5;
}
nav {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 15px;
color: white;
}
nav a {
color: white;
margin: 0 15px;
text-decoration: none;
}
.container {
max-width: 900px;
margin: 30px auto;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<nav>
<a href="/">π Home</a>
<a href="/about">βΉοΈ About</a>
<a href="/contact">π§ Contact</a>
</nav>
<div class="container">
{% block content %}{% endblock %}
</div>
</body>
</html>
home.html (Child Template):
<!-- templates/home.html -->
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block content %}
<h1>Welcome Home! π </h1>
<p>This page inherits from base.html! π</p>
{% endblock %}
Handling Forms (GET & POST)ΒΆ
Simple Form Example:
from flask import Flask, render_template, request, redirect, url_for
app = Flask(__name__)
# In-memory storage (resets when app restarts)
confessions = []
@app.route('/')
def home():
return render_template('confessions.html', confessions=confessions)
@app.route('/submit', methods=['GET', 'POST'])
def submit():
if request.method == 'POST':
category = request.form.get('category')
confession = request.form.get('confession')
emoji = request.form.get('emoji', 'π
')
confessions.append({
'category': category,
'confession': confession,
'emoji': emoji
})
return redirect(url_for('home'))
return render_template('submit_form.html')
if __name__ == '__main__':
app.run(debug=True)
templates/submit_form.html:
<!DOCTYPE html>
<html>
<head>
<title>Student Confession Booth π€</title>
<style>
body {
font-family: Arial;
max-width: 600px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.form-container {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
h1 { color: #667eea; text-align: center; }
input, select, textarea {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
button {
width: 100%;
padding: 15px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
font-size: 18px;
cursor: pointer;
}
button:hover {
background: #764ba2;
}
</style>
</head>
<body>
<div class="form-container">
<h1>π€ Anonymous Confession Booth</h1>
<p style="text-align: center; color: #666;">
Share your college struggles anonymously! π
</p>
<form method="POST" action="/submit">
<label>Category:</label>
<select name="category" required>
<option value="">Select a category...</option>
<option value="Academic Disasters">π Academic Disasters</option>
<option value="Social Awkwardness">π Social Awkwardness</option>
<option value="Career Confusion">πΌ Career Confusion</option>
<option value="General Life Failures">π€· General Life Failures</option>
</select>
<label>Your Confession:</label>
<textarea name="confession" rows="5"
placeholder="Tell us what's on your mind..."
required></textarea>
<label>How do you feel?</label>
<select name="emoji">
<option value="π
">π
Embarrassed</option>
<option value="π’">π’ Sad</option>
<option value="π°">π° Stressed</option>
<option value="π€·">π€· Confused</option>
<option value="πͺ">πͺ Ready to Change</option>
</select>
<button type="submit">Submit Confession π</button>
</form>
<p style="text-align: center; margin-top: 20px;">
<a href="/" style="color: #667eea;">β View All Confessions</a>
</p>
</div>
</body>
</html>
templates/confessions.html:
<!DOCTYPE html>
<html>
<head>
<title>Confession Wall π</title>
<style>
body {
font-family: Arial;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background: #f5f5f5;
}
h1 { text-align: center; color: #667eea; }
.confession {
background: white;
padding: 20px;
margin: 15px 0;
border-radius: 10px;
border-left: 5px solid #667eea;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.category {
background: #667eea;
color: white;
padding: 5px 15px;
border-radius: 20px;
display: inline-block;
font-size: 14px;
margin-bottom: 10px;
}
.add-btn {
display: block;
width: 200px;
margin: 20px auto;
padding: 15px;
background: #667eea;
color: white;
text-align: center;
text-decoration: none;
border-radius: 8px;
font-weight: bold;
}
.add-btn:hover {
background: #764ba2;
}
</style>
</head>
<body>
<h1>π€ Student Confession Wall</h1>
<p style="text-align: center; color: #666;">
Anonymous confessions from fellow students π
</p>
<a href="/submit" class="add-btn">+ Add Your Confession</a>
{% if confessions %}
{% for confession in confessions %}
<div class="confession">
<span class="category">{{ confession.category }}</span>
<p style="font-size: 18px; margin: 10px 0;">
{{ confession.emoji }} {{ confession.confession }}
</p>
</div>
{% endfor %}
{% else %}
<p style="text-align: center; color: #999; padding: 50px;">
No confessions yet... be the first! π€
</p>
{% endif %}
</body>
</html>
Note
redirect(url_for('home')) redirects to a route by function name. Use url_for() instead of hardcoding URLs! β¨
CRUD Operations: Learning Journey TrackerΒΆ
Letβs build a complete CRUD application! π
app.py:
from flask import Flask, render_template, request, redirect, url_for
from datetime import datetime
app = Flask(__name__)
# In-memory database (list of dictionaries)
skills = [
{'id': 1, 'name': 'Python Basics', 'status': 'Completed',
'confidence': 80, 'emoji': 'π', 'date': '2025-01-15'},
{'id': 2, 'name': 'Web Development', 'status': 'In Progress',
'confidence': 50, 'emoji': 'π', 'date': '2025-02-01'},
{'id': 3, 'name': 'Machine Learning', 'status': 'Started',
'confidence': 20, 'emoji': 'π€', 'date': '2025-02-20'},
]
# Helper to get next ID
def get_next_id():
return max([s['id'] for s in skills], default=0) + 1
# CREATE - Add new skill
@app.route('/create', methods=['GET', 'POST'])
def create():
if request.method == 'POST':
new_skill = {
'id': get_next_id(),
'name': request.form.get('name'),
'status': request.form.get('status', 'Started'),
'confidence': int(request.form.get('confidence', 0)),
'emoji': request.form.get('emoji', 'π'),
'date': datetime.now().strftime('%Y-%m-%d')
}
skills.append(new_skill)
return redirect(url_for('read_all'))
return render_template('create.html')
# READ - View all skills
@app.route('/')
def read_all():
# Calculate stats
total = len(skills)
completed = len([s for s in skills if s['status'] == 'Completed'])
in_progress = len([s for s in skills if s['status'] == 'In Progress'])
stats = {
'total': total,
'completed': completed,
'in_progress': in_progress,
'completion_rate': (completed / total * 100) if total > 0 else 0
}
return render_template('dashboard.html', skills=skills, stats=stats)
# READ - View single skill
@app.route('/skill/<int:skill_id>')
def read_one(skill_id):
skill = next((s for s in skills if s['id'] == skill_id), None)
if skill:
return render_template('detail.html', skill=skill)
return "Skill not found! π€·", 404
# UPDATE - Edit skill
@app.route('/update/<int:skill_id>', methods=['GET', 'POST'])
def update(skill_id):
skill = next((s for s in skills if s['id'] == skill_id), None)
if not skill:
return "Skill not found! π€·", 404
if request.method == 'POST':
skill['name'] = request.form.get('name')
skill['status'] = request.form.get('status')
skill['confidence'] = int(request.form.get('confidence'))
skill['emoji'] = request.form.get('emoji')
return redirect(url_for('read_all'))
return render_template('update.html', skill=skill)
# DELETE - Remove skill (with farewell ceremony π)
@app.route('/delete/<int:skill_id>', methods=['POST'])
def delete(skill_id):
global skills
skill = next((s for s in skills if s['id'] == skill_id), None)
if skill:
skills = [s for s in skills if s['id'] != skill_id]
return render_template('farewell.html', skill=skill)
return "Skill not found! π€·", 404
if __name__ == '__main__':
app.run(debug=True)
templates/dashboard.html:
<!DOCTYPE html>
<html>
<head>
<title>Learning Journey Tracker π</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.header {
background: white;
padding: 30px;
border-radius: 15px;
text-align: center;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 10px;
text-align: center;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.stat-number {
font-size: 36px;
font-weight: bold;
color: #667eea;
}
.skill-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.skill-card {
background: white;
padding: 25px;
border-radius: 15px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: transform 0.2s;
}
.skill-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 12px rgba(0,0,0,0.2);
}
.skill-emoji {
font-size: 48px;
display: block;
margin-bottom: 15px;
}
.status {
display: inline-block;
padding: 5px 15px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
margin: 10px 0;
}
.status-completed { background: #2ecc71; color: white; }
.status-inprogress { background: #f39c12; color: white; }
.status-started { background: #3498db; color: white; }
.confidence-bar {
background: #ecf0f1;
height: 20px;
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
}
.confidence-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
transition: width 0.3s;
}
.btn {
display: inline-block;
padding: 10px 20px;
margin: 5px;
border-radius: 5px;
text-decoration: none;
font-weight: bold;
transition: all 0.2s;
}
.btn-primary { background: #667eea; color: white; }
.btn-secondary { background: #95a5a6; color: white; }
.btn-danger { background: #e74c3c; color: white; }
.btn:hover { opacity: 0.9; transform: scale(1.05); }
.add-btn {
position: fixed;
bottom: 30px;
right: 30px;
width: 60px;
height: 60px;
border-radius: 50%;
background: #2ecc71;
color: white;
font-size: 32px;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
transition: all 0.3s;
}
.add-btn:hover {
transform: scale(1.1) rotate(90deg);
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>π My Learning Journey Tracker</h1>
<p style="color: #666; margin-top: 10px;">
Track your skills, celebrate progress, and say goodbye to abandoned dreams! π
</p>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-number">{{ stats.total }}</div>
<div>Total Skills</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ stats.completed }}</div>
<div>β
Completed</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ stats.in_progress }}</div>
<div>β³ In Progress</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ "%.0f"|format(stats.completion_rate) }}%</div>
<div>Completion Rate</div>
</div>
</div>
<div class="skill-grid">
{% for skill in skills %}
<div class="skill-card">
<span class="skill-emoji">{{ skill.emoji }}</span>
<h3>{{ skill.name }}</h3>
{% if skill.status == 'Completed' %}
<span class="status status-completed">β
{{ skill.status }}</span>
{% elif skill.status == 'In Progress' %}
<span class="status status-inprogress">β³ {{ skill.status }}</span>
{% else %}
<span class="status status-started">π {{ skill.status }}</span>
{% endif %}
<div style="margin: 15px 0;">
<small style="color: #666;">Confidence Level</small>
<div class="confidence-bar">
<div class="confidence-fill"
style="width: {{ skill.confidence }}%"></div>
</div>
<small style="color: #666;">{{ skill.confidence }}%</small>
</div>
<p style="color: #999; font-size: 12px;">
Started: {{ skill.date }}
</p>
<div style="margin-top: 15px;">
<a href="/skill/{{ skill.id }}" class="btn btn-primary">
ποΈ View
</a>
<a href="/update/{{ skill.id }}" class="btn btn-secondary">
βοΈ Edit
</a>
<form action="/delete/{{ skill.id }}" method="POST"
style="display: inline;"
onsubmit="return confirm('Say goodbye to {{ skill.name }}? π')">
<button type="submit" class="btn btn-danger"
style="border: none; cursor: pointer;">
ποΈ Delete
</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% if stats.total == 0 %}
<div style="text-align: center; color: white; padding: 50px;">
<h2>No skills yet! π±</h2>
<p>Start your learning journey by adding your first skill! π</p>
</div>
{% endif %}
</div>
<a href="/create" class="add-btn" title="Add New Skill">+</a>
</body>
</html>
templates/create.html:
<!DOCTYPE html>
<html>
<head>
<title>Add New Skill π±</title>
<style>
body {
font-family: Arial;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.form-container {
background: white;
padding: 40px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
max-width: 500px;
width: 100%;
}
h1 { color: #667eea; text-align: center; margin-bottom: 30px; }
label {
display: block;
margin-top: 15px;
color: #333;
font-weight: bold;
}
input, select {
width: 100%;
padding: 12px;
margin-top: 5px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 15px;
margin-top: 20px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #764ba2;
}
.back-link {
display: block;
text-align: center;
margin-top: 20px;
color: #667eea;
text-decoration: none;
}
</style>
</head>
<body>
<div class="form-container">
<h1>π± Add New Skill</h1>
<form method="POST">
<label>Skill Name:</label>
<input type="text" name="name"
placeholder="e.g., Machine Learning" required>
<label>Emoji:</label>
<input type="text" name="emoji"
placeholder="π€" maxlength="2" required>
<label>Status:</label>
<select name="status" required>
<option value="Started">π Started</option>
<option value="In Progress">β³ In Progress</option>
<option value="Completed">β
Completed</option>
</select>
<label>Confidence Level (0-100):</label>
<input type="range" name="confidence"
min="0" max="100" value="50"
oninput="this.nextElementSibling.value = this.value + '%'">
<output style="display: block; text-align: center;
color: #667eea; font-weight: bold;">50%</output>
<button type="submit">π Add Skill</button>
</form>
<a href="/" class="back-link">β Back to Dashboard</a>
</div>
</body>
</html>
templates/farewell.html:
<!DOCTYPE html>
<html>
<head>
<title>Farewell Ceremony π</title>
<style>
body {
font-family: Arial;
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: white;
text-align: center;
}
.ceremony {
max-width: 600px;
animation: fadeIn 1s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
.emoji {
font-size: 100px;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px); }
}
h1 { font-size: 48px; margin: 20px 0; }
p { font-size: 20px; line-height: 1.6; }
.btn {
display: inline-block;
margin-top: 30px;
padding: 15px 30px;
background: white;
color: #e74c3c;
text-decoration: none;
border-radius: 8px;
font-weight: bold;
transition: transform 0.2s;
}
.btn:hover {
transform: scale(1.05);
}
</style>
</head>
<body>
<div class="ceremony">
<div class="emoji">π</div>
<h1>Farewell, {{ skill.name }}!</h1>
<p>
{{ skill.emoji }} We bid farewell to "{{ skill.name }}".<br>
You served us well with {{ skill.confidence }}% confidence.<br>
May you rest in peace in the graveyard of abandoned skills. πͺ¦
</p>
<p style="font-style: italic; opacity: 0.8;">
"It's not you, it's me." - Every developer ever π
</p>
<a href="/" class="btn">Return to Dashboard π </a>
</div>
</body>
</html>
Note
This complete CRUD application demonstrates all four operations! Notice the beautiful styling, animations, and the humorous βfarewell ceremonyβ for deleted skills! πβ¨
Flash MessagesΒΆ
Show feedback messages after actions:
from flask import Flask, render_template, flash, redirect, url_for
app = Flask(__name__)
app.secret_key = 'your-secret-key-here' # Required for flash messages
@app.route('/submit', methods=['POST'])
def submit():
# Process form...
flash('β
Success! Your data was saved!', 'success')
flash('β οΈ Warning: This is a demo!', 'warning')
return redirect(url_for('home'))
@app.route('/')
def home():
return render_template('home.html')
In template:
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
TasksΒΆ
Task 1: Student Confession Booth
Build a confession submission system with Jinja2 templates. Create: (1) Form page with confession categories (Academic, Social, Career, Life), (2) Display page showing all confessions with appropriate styling per category, (3) Use template inheritance with a base template. Add emojis and CSS styling! π€
Hint: Store confessions in a list. Use {% extends "base.html" %} for inheritance. Style each category differently with CSS classes.
Task 2: Personal Blog with CRUD
Create a complete blog system: (1) Homepage listing all posts, (2) Create post form, (3) View individual post, (4) Edit post, (5) Delete post with confirmation. Use colorful CSS, emojis for post types (π Article, π‘ Idea, π Achievement). Include post count dashboard.
Hint: Use list of dictionaries for posts. Implement all CRUD routes. Use redirect(url_for('route_name')) after create/update/delete.
Task 3: Mood Journal with Templates
Build a mood tracking app: Form to log mood (emoji selector), note, and activities. Display mood history with different colors per mood. Show statistics: most common mood, total entries, mood distribution. Use Jinja2 loops and conditionals for display. Make it beautiful! π
Hint: Store moods with timestamps. Calculate statistics in route before passing to template. Use {% for %} to display history.
Task 4: Recipe Manager
Create a recipe management system: Add recipe (name, ingredients list, instructions, cooking time, emoji). List all recipes. View recipe details. Edit recipe. Delete recipe. Use template inheritance. Add search functionality using form GET request. Style with food-themed colors! π
Hint: Store ingredients as comma-separated string, split in template with {{ ingredients.split(',') }}. Implement search by filtering list in route.
Task 5: Goal Tracker with Progress
Build a goal tracking app: Create goals with title, description, target date, progress (0-100%). Dashboard showing all goals with progress bars. Update progress. Mark as complete. Delete with farewell message. Calculate statistics: total goals, completed, in progress, average progress. Use animations for progress bars! π―
Hint: Use <input type="range"> for progress. Style progress bars with width: {{ progress }}%. Use JavaScript oninput to update display value as user drags slider.
SummaryΒΆ
Jinja2 is Flaskβs templating engine π¨
Use
{{ variable }}for variables,{% for %}for loops,{% if %}for conditionsTemplate inheritance:
{% extends %}and{% block %}enable DRY codeForms: Use
methods=['GET', 'POST']to handle both display and submissionCRUD Operations: - Create: POST route to add new data - Read: GET route to display data - Update: GET to show form, POST to save changes - Delete: POST to remove data
Use
request.form.get()for form dataredirect(url_for('function_name'))redirects to routesflash()shows one-time messages (requiressecret_key)Always use
url_for()instead of hardcoding URLsAdd emojis and styling to make apps fun! π