Make a Flask CRUD App
September 18, 2024
You will learn how to make a Flask CRUD app using Flask, Tailwind and HTMX!
Note
This tutorial only includes the CRUD with no styles. You can purchase the full app with the UI
on the Dev Shop
What you will need:
- Flask - Python web framework
- Flask SQLAlchemy - for persistent relational DB storage
- Tailwind CDN - for fast CSS styling
- HTMX - for a more dynamic frontend (not heavily used)
Data Models
This app will have one-to-one
and many-to-many
relationships.
User
A task to complete
Column | Type | Description |
---|---|---|
id | int | unique id for the user |
username | text | unique username for the user |
password | text | password for the user |
details_id | More details about the user |
Item
A generic item. Edit these fields to your needs
Column | Type | Description |
---|---|---|
id | int | unique id for the user |
name | text | unique username for the user |
description | text | password for the user |
image_url | text | url to image for item |
numeral | int | a number |
numeral_name | text | the name of the numeral |
created_at | text | date item was created |
updated_at | text | date item was updated |
... | ... | ... |
UserItems
A user has many items and an item belongs to many users.
Column | Type | Description |
---|---|---|
id | int | unique id for the user detail |
user_id | text | id of the user |
item_id | text | id of the item |
User Details
More details about a user. Edit these fields to your needs
Column | Type | Description |
---|---|---|
id | int | unique id for the user detail |
user_id | text | id of the user |
... | ... | ... |
UI/UX
This is the final UI. I left the other components outside of the index.html
and show.html
files blank so that you can update them to look however you'd like.
Mockup By ilnicki
Requirements
Functional Requirements
- CRUD Users
- Save to JSON
Non Functional Requirements
- Beautiful - Nice looking UI
- Useful - could be used for real work
- Stable - No bugs
Technical Requirements
- Desktop
- Responsive
Business Rules
These business rules were generated by Google Gemini. These are only ideas for future features and are not included in this project (TBA):
Create a new Flask App
Create the folders for your flask app. You should be able to copy the content below and paste it into your terminal. You can also create the folders manually using your mouse and keyboard. the app_name/
folder can be called whatever the name of your app is. For example you can do mkdir inventory_app
instead of mkdir app_name
.
mkdir app_name
cd app_name
touch app.py
touch config.py
mkdir templates
touch templates/base.html
touch templates/index.html
touch templates/_nav.html
touch templates/_footer.html
mkdir templates/users
touch templates/users/index.html
touch templates/users/show.html
touch templates/users/new.html
touch templates/users/edit.html
mkdir templates/items
touch templates/items/edit_item.html
touch templates/items/new_item.html
mkdir static
touch static/style.css
touch static/script.js
This is the folder structure of the app.
app_name/
templates/
users/
index.html
show.html
edit.html
new.html
items/
edit_item.html
new_item.html
base.html
index.html
_nav.html
_footer.html
static/
script.js
style.css
app.py
config.py
Install dependencies
Create a virtual environment and install flask
and flask-sqlalchemy
python3 -m venv .venv
source .venv/bin/activate
pip3 install flask
pip3 install flask-sqlalchemy
Configuration
Set up some configuration.
Optional Folder Structure
app_name/
templates/
alerts/
_error_messages.html
_success.html
_error.html
components/
_nav.html
_footer.html
auth/
login.html
register.html
items/
edit_item.html
new_item.html
users/
index.html
show.html
edit.html
new.html
base.html
index.html
static/
script.js
style.css
app.py
config.py
Optional Configuration
You will need to add your own logo to the nav.html
component and replace the logo image.
Other components such as footer.html
login.html
and register.html
haven't been implemented and are there for your convenience.
<html>
<head>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="p-10">
{% if request.endpoint != 'new'%}
{% include 'components/_nav.html' %}
{%endif%}
{% include 'alerts/_error_messages.html' %}
{% block content %}
{% endblock %}
{% include 'components/_footer.html' %}
</div>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@2.0.2" integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ" crossorigin="anonymous"></script>
<script src="{{url_for('static', filename='script.js')}}"></script>
</body>
</html>
<!-- ErrorMessages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages%}
{% for category, message in messages %}
{% if category == 'success' %}
{% include 'alerts/_success.html' %}
{% elif category == 'error' %}
{% include 'alerts/_error.html' %}
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}
<!-- /ErrorMessages -->
<div id="alert" class="bg-red-500 text-white p-2">
{{category}}
{{message}}
</div>
<div id="alert" class="bg-green-500 text-white p-2">
{{category}}
{{message}}
</div>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/users">Users</a></li>
</ul>
</nav>
<footer>
<ul>
<li>Footer</li>
</ul>
</footer>
The Code
All components live in /templates/
. CSS and Javascript files live in /static/
. Copy the code in the files below to the appropriate files in your project.
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret_key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
class User(db.Model):
__tablename__='users'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(50), unique=True)
password = db.Column(db.String(128))
details_id = db.Column(db.Integer, db.ForeignKey('user_details.id')) #one-to-one relationship
items = db.relationship('Item', secondary="users_items", back_populates="users")
def __repr__(self):
return f'<User {self.username}>'
@classmethod
def get_by_id(cls, id):
return cls.query.get_or_404(id)
@classmethod
def all(cls):
return cls.query.all()
class Item(db.Model):
__tablename__='items'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
description = db.Column(db.String(200))
image_url = db.Column(db.String(800))
numeral = db.Column(db.Integer)
numeral_name = db.Column(db.String(200)) #cost, count, price
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now)
users = db.relationship('User', secondary="users_items", back_populates="items")
@classmethod
def get_by_id(cls, id):
return cls.query.get_or_404(id)
@classmethod
def all(cls):
return cls.query.all()
# JOIN TABLE or many-to-one relationship
class UserItems(db.Model):
__tablename__ = "users_items"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column('item_id', db.Integer, db.ForeignKey('items.id'))
user_id = db.Column('user_id', db.Integer, db.ForeignKey('users.id'))
item = db.relationship('Item', backref= db.backref('items', cascade="all, delete-orphan"))
user = db.relationship('User', backref= db.backref('users', cascade="all, delete-orphan"))
db.UniqueConstraint('item_id', 'user_id', name='UC_item_id_user_id')
# one-to-one relationship
class UserDetails(db.Model):
__tablename__='user_details'
id = db.Column(db.Integer, primary_key=True)
profile_photo = db.Column(db.String(200))
gallery = db.Column(db.String(200))
bio = db.Column(db.String(200))
first_name = db.Column(db.String(200))
last_name = db.Column(db.String(200))
occupation = db.Column(db.String(200))
address = db.Column(db.String(200))
state = db.Column(db.String(200))
city = db.Column(db.String(200))
family_members = db.Column(db.String(200))
friends = db.Column(db.String(200))
images = db.Column(db.String(800))
website = db.Column(db.String(800))
birth_date = db.Column(db.String(800))
weight = db.Column(db.String(200))
height = db.Column(db.String(200))
user = db.relationship("User", backref="details") #one-to-one relationship
@app.route('/')
def index():
items = Item.all()
return render_template('index.html',items=items)
@app.route('/users')
def all_users():
users = User.all()
return render_template('users/index.html', users=users)
@app.route('/users/new', methods=['GET', 'POST'], endpoint='new')
def create_user():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = User(username=username,password=password)
db.session.add(user)
db.session.commit()
flash('User created')
return redirect(url_for('all_users'))
return render_template('users/new.html')
@app.route('/users/<int:user_id>')
def retrieve_user(user_id):
user = User.get_by_id(user_id)
return render_template('users/show.html', user=user)
@app.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
def update_user(user_id):
user = User.get_by_id(user_id)
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
details = request.form['bio']
user.username = username
user.password = password
try:
user.details.bio = details
except:
details = UserDetails(bio=details)
user.details = details
db.session.commit()
flash('User details updated')
return redirect(url_for('retrieve_user', user_id=user.id))
return render_template('users/edit.html', user=user)
@app.route('/users/<int:user_id>/delete')
def delete_user(user_id):
user = User.get_by_id(user_id)
db.session.delete(user)
db.session.commit()
flash('User Deleted')
return redirect(url_for('all_users'))
# ITEMS
@app.route('/items', defaults={'id': None},methods=['GET','POST'])
@app.route('/items/<id>', methods=['GET','POST', 'PUT', 'DELETE'])
def items(id):
if not id:
if request.method == 'GET':
return render_template('items/new_item.html')
if request.method == 'POST':
new_item = Item(**request.form)
db.session.add(new_item)
db.session.commit()
return redirect(url_for('index'))
item = Item.get_by_id(id)
if request.method == 'GET':
return render_template('items/edit_item.html',item=item)
if request.method == 'POST':
for key, value in request.form.items():
setattr(item, key, value)
db.session.commit()
return redirect(url_for('index'))
if request.method == 'DELETE':
db.session.delete(item)
db.session.commit()
flash('Item Deleted')
items = Item.all()
return render_template('index.html',items=items)
with app.app_context():
db.create_all()
if __name__ == '__main__':
# db.create_all()
app.run(debug=True,port=4000)
<html>
<head>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
{% include '_nav.html' %}
{% block content %}
{% endblock %}
{% include '_footer.html' %}
</div>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@2.0.2" integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ" crossorigin="anonymous"></script>
<script src="{{url_for('static', filename='script.js')}}"></script>
</body>
</html>
{% extends 'base.html' %}
{% block content %}
<h1>WELCOME</h1>
<a href='/users'>USERS</a>
<div id="all">
<a class="ml-2" href="/items">NEW ITEM</a>
{%for item in items%}
<div class="flex">
{{item.name}}<br/>
<a class="ml-2" href="/items/{{item.id}}">Edit</a>
<a class="ml-2" hx-delete="/items/{{item.id}}" hx-target="body">DELETE</a>
</div>
{%endfor%}
</div>
{% endblock %}
{% extends 'base.html' %}
{% block content %}
<h1>Users</h1>
<a href="users/new">New User</a><br/>
{% for user in users%}
<a href="/users/{{user.id}}">{{ user }}</a>
<a href="/users/{{user.id}}/delete" onclick="return confirm('Are you sure?')">Delete</a>
{% endfor %}
{% endblock %}
{% extends 'base.html' %}
{% block content %}
<form method="POST" action="/users/new">
<input name="username">
<input name="password">
<input type="submit" value="Submit">
</form>
{% endblock %}
{% extends 'base.html' %}
{% block content %}
{{user.username}}
{{user.password}}
<a href="/users/{{user.id}}/edit">Edit User</a>
<h2>
<p>Bio</p>
{{user.details.bio}}
</h2>
<hr>
{%for item in user.items%}
{{item.name}}<br/>
{{item.numeral_name}}
{{item.numeral}}
{%endfor%}
{% endblock %}
{% extends 'base.html' %}
{% block content %}
<form method="POST" action="/users/{{user.id}}/edit">
<input name="username" value="{{user.username}}">
<input name="password" value="{{user.password}}">
<div>
<label class="w-full" for="bio">Bio</label>
<input required name="bio" id="bio" class="text-black h-10 p-2" value="{{user.details.bio}}">
<input type="submit" value="Submit">
</form>
{% endblock %}
{% extends 'base.html' %}
{% block content %}
<form method="POST" action="/items/{{item.id}}">
<input name="name" value="{{item.name}}">
<input name="description" value="{{item.description}}">
<input type="submit" value="Submit">
</form>
{% endblock %}
{% extends 'base.html' %}
{% block content %}
<form method="POST" action="/items">
<input name="name">
<input name="description">
<input type="submit" value="Submit">
</form>
{% endblock %}
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/users">Users</a></li>
</ul>
</nav>
<footer>
<ul>
<li>Footer</li>
</ul>
</footer>
Run the App
python3 app.py