Skip to content
Shop

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.

ColumnTypeDescription
addresstextAddress of the user
images_pathtext (nullable)Path to user's images
videos_pathtext (nullable)Path to user's videos
system_prompttext (nullable)Custom system prompt for the user

User

Represents a user in the system.

ColumnTypeDescription
idint (auto)Unique ID for the user
emailtext (nullable)User’s email address
user_typetext (default="guest")Type of user (guest, admin, etc.)
first_nametext (nullable)User’s first name
last_nametext (nullable)User’s last name
passwordtext (nullable)User’s password (plain)
password_hashtext (nullable)Hashed password
role_idint (nullable)Role identifier
phonetext (nullable)User’s phone number
addresstext (nullable)User’s address
profile_imagetext (nullable)Path to profile image
locationtext (nullable)User’s location
universitytext (nullable)University attended
employertext (nullable)Employer name
employed_sincedatetime (nullable)Employment start date
birthdaytext (nullable)User’s birthday
ip_addresstext (nullable)IP address of the user
browsertext (nullable)Browser info of the user
forum_idint (nullable)Forum identifier
statusint (nullable)Status of the user
created_atdatetimeDate user was created
updated_atdatetimeDate user was last updated
versiontext (nullable)Version info
infoforeign key (UserInfo)Additional user info

Items

Items belonging to a user.

ColumnTypeDescription
userforeign key (User)Owner of the item
nametext (nullable)Name of the item
item_typetext (nullable)Type/category of the item

Images

Images belonging to a user.

ColumnTypeDescription
userforeign key (User)Owner of the image
titletext (nullable)Title of the image
descriptiontext (nullable)Description of the image
urltext (nullable)URL of the image
datablob (nullable)Binary image data
extensiontext (nullable)File extension

Videos

Videos belonging to a user.

ColumnTypeDescription
userforeign key (User)Owner of the video
titletext (nullable)Title of the video
descriptiontext (nullable)Description of the video
urltext (nullable)URL of the video
datablob (nullable)Binary video data
extensiontext (nullable)File extension

UserItems

Links users to items (many-to-many relationship).

ColumnTypeDescription
userforeign key (User)User linked to the item
itemforeign 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.

markdown
	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

bash
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.

python
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'
python
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)
html
<!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>
html
WELCOME
<a href="/users">GO INSIDE</a>
html
{% 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%}
html
{% 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 %}
html
{% 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 %}
html
{% 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 %}
html
<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

bash
python3 app.py

Resources