Flask + Peewee ORM
August 19, 2024
You will learn how to make a Flask CRUD app using Flask, Peewee ORM, Tailwind and HTMX!
Note
This tutorial only includes the CRUD with no styles.
What you will need:
- Flask - Python web framework
- Peewee ORM - 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 one-to-many
and many-to-many
relationships.
UserInfo
Additional information about the user.
Column | Type | Description |
---|---|---|
address | text | Address of the user |
images_path | text (nullable) | Path to user's images |
videos_path | text (nullable) | Path to user's videos |
system_prompt | text (nullable) | Custom system prompt for the user |
User
Represents a user in the system.
Column | Type | Description |
---|---|---|
id | int (auto) | Unique ID for the user |
text (nullable) | User’s email address | |
user_type | text (default="guest") | Type of user (guest, admin, etc.) |
first_name | text (nullable) | User’s first name |
last_name | text (nullable) | User’s last name |
password | text (nullable) | User’s password (plain) |
password_hash | text (nullable) | Hashed password |
role_id | int (nullable) | Role identifier |
phone | text (nullable) | User’s phone number |
address | text (nullable) | User’s address |
profile_image | text (nullable) | Path to profile image |
location | text (nullable) | User’s location |
university | text (nullable) | University attended |
employer | text (nullable) | Employer name |
employed_since | datetime (nullable) | Employment start date |
birthday | text (nullable) | User’s birthday |
ip_address | text (nullable) | IP address of the user |
browser | text (nullable) | Browser info of the user |
forum_id | int (nullable) | Forum identifier |
status | int (nullable) | Status of the user |
created_at | datetime | Date user was created |
updated_at | datetime | Date user was last updated |
version | text (nullable) | Version info |
info | foreign key (UserInfo) | Additional user info |
Items
Items belonging to a user.
Column | Type | Description |
---|---|---|
user | foreign key (User) | Owner of the item |
name | text (nullable) | Name of the item |
item_type | text (nullable) | Type/category of the item |
Images
Images belonging to a user.
Column | Type | Description |
---|---|---|
user | foreign key (User) | Owner of the image |
title | text (nullable) | Title of the image |
description | text (nullable) | Description of the image |
url | text (nullable) | URL of the image |
data | blob (nullable) | Binary image data |
extension | text (nullable) | File extension |
Videos
Videos belonging to a user.
Column | Type | Description |
---|---|---|
user | foreign key (User) | Owner of the video |
title | text (nullable) | Title of the video |
description | text (nullable) | Description of the video |
url | text (nullable) | URL of the video |
data | blob (nullable) | Binary video data |
extension | text (nullable) | File extension |
UserItems
Links users to items (many-to-many relationship).
Column | Type | Description |
---|---|---|
user | foreign key (User) | User linked to the item |
item | foreign key (Items) | Item linked to the user |
UI/UX
None.
Requirements
Functional Requirements
- CRUD Users
Non Functional Requirements
- Beautiful, Basic UI
Technical Requirements
- Desktop
- Responsive
Business Rules
None.
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
.
This is the folder structure of the app.
app_name/
templates/
users/
index.html
show.html
edit.html
new.html
shared/
layout.html
nav.html
index.html
static/
script.js
style.css
app.py
models.py
Install dependencies
Create a virtual environment and install flask
and peewee
python3 -m venv .venv
source .venv/bin/activate
pip3 install flask
pip3 install peewee
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 peewee import *
from datetime import datetime
database = SqliteDatabase('database.db')
class UnknownField(object):
def __init__(self, *_, **__): pass
class BaseModel(Model):
class Meta:
database = database
# One-To-One
class UserInfo(BaseModel):
address = CharField()
images_path = CharField(null=True)
videos_path = CharField(null=True)
system_prompt = CharField(null=True)
class Meta:
table_name = 'user_info'
class User(BaseModel):
id = AutoField(null=True)
email = CharField(null=True,max_length=60)
user_type = CharField(null=True,default="guest")
first_name = CharField(null=True,max_length=50)
last_name = CharField(null=True,max_length=50)
password = CharField(null=True,max_length=80)
password_hash = CharField(null=True,max_length=128)
role_id = IntegerField(null=True)
phone = CharField(null=True)
address = CharField(null=True)
profile_image =CharField(null=True)
location = CharField(null=True)
university = CharField(null=True)
employer = CharField(null=True)
employed_since = DateTimeField(null=True)
birthday = CharField(null=True)
ip_address = CharField(null=True)
browser = CharField(null=True)
forum_id = IntegerField(null=True)
status = IntegerField(null=True)
created_at = DateTimeField(default=datetime.now,null=True)
updated_at = DateTimeField(default=datetime.now,null=True)
version = CharField(null=True)
info = ForeignKeyField(UserInfo, backref='user',null=True)
class Meta:
table_name = 'users'
primary_key = False
# One-To-Many
class Items(BaseModel):
user = ForeignKeyField(User, backref='items',null=True)
name = CharField(null=True)
item_type = CharField(null=True)
class Meta:
table_name = 'items'
class Images(BaseModel):
user = ForeignKeyField(User, backref='images',null=True)
title = CharField(null=True)
description = TextField(null=True)
url = CharField(null=True)
data = BlobField(null=True)
extension = CharField(null=True)
class Meta:
table_name = 'images'
class Videos(BaseModel):
user = ForeignKeyField(User, backref='videos',null=True)
title = CharField(null=True)
description = TextField(null=True)
url = CharField(null=True)
data = BlobField(null=True)
extension = CharField(null=True)
class Meta:
table_name = 'videos'
# Many-To-Many
class UserItems(BaseModel):
user = ForeignKeyField(User, backref='items',null=True)
item = ForeignKeyField(Items, backref='items',null=True)
class Meta:
table_name = 'user_items'
from flask import (Flask,flash,redirect,url_for,g,render_template,request
)
from models import database, User, Items, UserItems, UserInfo, Images, Videos
from datetime import date
from werkzeug.utils import secure_filename
import os
app = Flask(__name__)
database.create_tables([UserInfo, User, Items, UserItems,Images,Videos])
UPLOAD_FOLDER = './static/uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
# DATABASE LIFECYCLE
@app.before_request
def _db_connect():
if database.is_closed():
database.connect()
@app.teardown_request
def _db_close(exc):
if not database.is_closed():
database.close()
# ROUTES
@app.route("/")
def index():
users = User.select()
return render_template('index.html',users=users)
# ROUTES
@app.route("/users")
def all_users():
users = User.select()
return render_template('users/index.html',users=users)
# CREATE
@app.route("/users/new", methods=["GET", "POST"], endpoint="new")
def create_user():
if request.method == "POST":
information = UserInfo(address="123 safe street")
information.save()
first_name = request.form["first_name"]
last_name = request.form["last_name"]
profile_image = request.form["profile_image"]
user = User(
first_name=first_name,
last_name=last_name,
profile_image=profile_image,
birthday=date(1960, 1, 15),
info=information,
)
user.save()
# flash('User created')
return redirect('/')
# Otherwise
return render_template('users/new.html')
# RETRIEVE
@app.route('/users/<id>')
def show_user(id):
if not id:
redirect('/')
user = User.get_by_id(id)
return render_template('users/show.html',user=user)
# UPDATE
@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':
firstname = request.form['first_name']
lastname = request.form['last_name']
profile_image = request.form['profile_image']
user.first_name = firstname
user.last_name = lastname
user.profile_image = profile_image
details = "Some new address"
files = request.files.getlist('files')
uploadFilesToUser(user,files)
try:
user.info.address = details
except:
details = UserInfo(address=details)
user.info = details
user.save()
# flash('User details updated')
return redirect(url_for('show_user', id=user.id))
# Otherwise
return render_template('users/edit.html', user=user)
# DELETE
@app.route("/users/<int:user_id>/delete")
def delete_user(user_id):
user = User.select().where(User.id == user_id).get()
user.delete_instance()
# flash("User Deleted")
return redirect(url_for("all_users"))
@app.route('/images/<int:image_id>')
def get_image(image_id):
img = Images.get_or_none(Images.id == image_id)
if img and img.data:
mime_type = f"image/{img.extension or 'jpeg'}"
return img.data, 200, {"Content-Type": mime_type}
return "Image not found", 404
@app.route('/videos/<int:video_id>')
def get_video(video_id):
video = Videos.get_or_none(Videos.id == video_id)
if video and video.data:
mime_type = f"image/{video.extension or 'mov'}"
return video.data, 200, {"Content-Type": mime_type}
return "Video not found", 404
def uploadFilesToUser(user,files):
uploaded_files = []
upload_folder = app.config['UPLOAD_FOLDER']
if not os.path.exists(upload_folder):
os.makedirs(upload_folder)
for file in files:
# UPLOAD TO FOLDER
if not file.filename:
continue
filename = secure_filename(file.filename)
filepath = os.path.join(upload_folder, filename)
file.seek(0)
file_bytes = file.read()
file.save(filepath) #file bytes has to come before save because pointer will be EOF
# CREATE IN DATABASE
ext = os.path.splitext(filename)[1].lstrip(".")
if ext in ['png','jpg','jpeg','PNG','JPG','JPEG']:
img = Images.create(
user=user,
title=filename,
description="Uploaded user image",
url=filepath,
extension=ext,
data=file_bytes
)
uploaded_files.append(img)
else:
vdo = Videos.create(
user=user,
title=filename,
description="Uploaded user video",
url=filepath,
extension=ext,
data=file_bytes
)
uploaded_files.append(vdo)
# if __name__ == "__main__":
# app.run(debug=True, port=2000)
# Accessible from other devices if you know your IP.
if __name__ == '__main__':
# app.run(debug=True,port=4001)
app.run(host="0.0.0.0", port=4002, debug=True)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{url_for('static', filename='css/style.css')}}">
<script src="https://unpkg.com/htmx.org@2.0.2"
integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ"
crossorigin="anonymous"></script>
<title>Flamingo AI</title>
</head>
<body>
{%include './shared/nav.html'%}
<div class="p-8">
{% block content %}
{% endblock %}
</div>
<script src="https://cdn.tailwindcss.com"></script>
<script src="{{url_for('static', filename='script.js')}}"></script>
</body>
</html>
WELCOME
<a href="/users">GO INSIDE</a>
{% extends 'shared/layout.html'%}
{% block content %}
<a href="/users/new">Add User</a>
<div>
{% for user in users %}
<a href="/users/{{user.id}}">{{user.first_name}}</a><br />
<img loading="lazy" class="brightness-50 h-[320px] w-[224px] rounded-md object-cover" src="{{user.profile_image}}">
{% endfor %}
</div>
{% endblock%}
{% extends 'shared/layout.html' %}
{% block content %}
<form action="/users/new" method="POST" id="form">
<input name="first_name" placeholder="Enter your name">
<input name="last_name" placeholder="Enter your name">
<input name="profile_image" placeholder="Enter your profile image">
<button>Create</button>
</form>
{% endblock %}
{% extends 'shared/layout.html' %}
{% block content %}
<a href="/users/{{user.id}}/edit">Edit</a>
<div>
{{user.first_name}} {{user.last_name}}<br>
<img class="h-8 w-8" src="{{user.profile_image}}">
</div>
<a href="/users/{{user.id}}/delete">Delete</a>
{% if user.images %}
{% for image in user.images %}
{{image.title}}
{% if image %}
<div class="empty">
<img src="{{ url_for('get_image', image_id=image.id) }}" alt="{{ image.title }}">
</div>
{% endif %}
{% endfor %}
{% endif %}
{% if user.videos %}
{% for video in user.videos %}
{%if video%}
<div class="empty">
<video controls src="{{ url_for('get_video', video_id=video.id) }}" alt="{{ video.title }}"></video>
</div>
{% endif %}
{% endfor %}
{% endif %}
{% endblock %}
{% extends 'shared/layout.html' %}
{% block content %}
<form action="{{url_for('update_user', user_id=user.id)}}" method="POST" enctype="multipart/form-data">
<input value="{{user.first_name}}" name="first_name" placeholder="Enter your name">
<input value="{{user.last_name}}" name="last_name" placeholder="Enter your name">
<input value="{{user.profile_image}}" name="profile_image" placeholder="Enter your profile image">
<input type="file" id="files" name="files" multiple>
<button>Update</button>
</form>
{% endblock %}
<nav class="w-full bg-gray-100 z-100 h-16 flex justify-center items-center">
<h1 class="text-center text-xl">Navigation</h1>
</nav>
Run the App
python3 app.py