Project based on a FreeCodeCamp Flask tutorial:
- YouTube: Flask Course - Python Web Application Development
- Date: 10/03/2021
- GitHub: FlaskSeries
- Technology: Python (>= 3.8), Flask, SQLite, and Bootstrap 4
Make a website where you can buy and sell stuff.
- An excellent Flask tutorial.
- A great introduction to Flask.
- Required: some basic Python knowledge.
- Even though the tutorial is only 2 years old, I had to make some changes including downgrading some packages, which I documented in the lessons below.
The final database schema:
CREATE TABLE user (
id INTEGER NOT NULL,
username VARCHAR(30) NOT NULL,
email_address VARCHAR(50) NOT NULL,
password_hash VARCHAR(60) NOT NULL,
budget INTEGER NOT NULL,
PRIMARY KEY (id),
UNIQUE (username),
UNIQUE (email_address)
);
CREATE TABLE item (
id INTEGER NOT NULL,
name VARCHAR(30) NOT NULL,
price INTEGER NOT NULL,
barcode VARCHAR(12) NOT NULL,
description VARCHAR(1024) NOT NULL,
owner INTEGER,
PRIMARY KEY (id),
UNIQUE (name),
UNIQUE (barcode),
UNIQUE (description),
FOREIGN KEY(owner) REFERENCES user (id)
);
If you want to see the code for the lesson, use the corresponding tag to switch to that version.
Using VSCode, create a virtual environment for this project. I am currently using Windows 11 with Python 3.11.2.
Install dependencies
pip install flask
Test whether Flask is installed correctly:
flask --version
Python 3.11.2
Flask 3.0.0
Werkzeug 3.0.0
To execute the script:
flask --app market run
Debug mode:
flask --app market run --debug
In debug mode you don't need to restart the server to see the changes you made.
Create a second page:
@app.route('/about')
def about_page():
return '<h1>Hello, World!</h1><h2>This Site</h2><p>Lorum ipsum ...</p><h2>The Owner</h2><p>Lorum ipsum ...</p>'
Turn add a third dynamic route/page:
@app.route('/about')
def about_page():
return f'<h1>About</h1><h2>This Site</h2><p>Lorum ipsum ...</p>'
@app.route('/about/<username>')
def about_dpage(username):
return f'<h1>About</h1><h2>{username}</h2><p>Lorum ipsum ...</p>'
Mostly an introduction to Bootstrap.
Jinja2 template
Example:
A simple example:
In market.py
:
@app.route('/market')
def market_pafe():
return render_template('market.html', item_name='Phone')
In market.html
:
<p>{{ item_name }}</p>
A less simple example:
In market.py
:
@app.route('/market')
def market_pafe():
items = [
{'id': 1, 'name': 'Phone', 'barcode': '893212299897', 'price': 500},
{'id': 2, 'name': 'Laptop', 'barcode': '123985473165', 'price': 900},
{'id': 3, 'name': 'Keyboard', 'barcode': '231985128446', 'price': 150}
]
return render_template('market.html', items=items)
In market.html
:
<table class="table table-hover table-dark">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Barcode</th>
<th scope="col">Price</th>
<th scope="col">Options</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.barcode }}</td>
<td>{{ item.price }}$</td>
<td>
<button class="btn btn-outline btn-info">More Info</button>
<button class="btn btn-outline btn-success">Purchase this Item</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
The idea is to have 1 base HTML template on which all other HTML templates are based. Less copying and pasting, easier maintenance.
Don't hardcode links in the navbar, but use the Flask function url_for()
to refer to the route:
<a class="nav-link" href="{{ url_for('market_page') }}">Market</a>
Working with a SQLite3 database.
Install SQLAlchemy
pip install flask-sqlalchemy
Creating the database:
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///market.db'
db = SQLAlchemy(app)
Creating a model that will become a table in the database:
class Item(db.Model):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(length=30), nullable=False, unique=True)
price = db.Column(db.Integer(), nullable=False)
barcode = db.Column(db.String(length=12), nullable=False, unique=True)
description = db.Column(db.String(length=1024), nullable=False, unique=True)
Creating the database and adding an item manually via a python shell. You have to open the python shell in the directory that also contains market.py
.
The method the author used, didn't work on my computer. The DB file also ended up in the instance
directory instead of the current directory.
In the python shell (this is different from the video, but with the same results):
>>> from market import app
>>> from market import db
>>> with app.app_context():
... db.create_all()
...
>>> from market import Item
>>> item1 = Item(name='iPhone 10', price=500, barcode='846154104831', description='description for the iPhone 10')
>>> with app.app_context():
... db.session.add(item1)
... db.session.commit()
...
>>> with app.app_context():
... Item.query.all()
...
[Item iPhone 10]
>>> item2 = Item(name='Laptop', price=600, barcode='321912987542', description='description for the laptop')
>>> with app.app_context():
... db.session.add(item2)
... db.session.commit()
... Item.query.all()
...
[Item iPhone 10, Item Laptop]
>>> with app.app_context():
... for item in Item.query.all():
... item.id
... item.name
... item.price
... item.barcode
... item.description
...
1
'iPhone 10'
500
'846154104831'
'description for the iPhone 10'
2
'Laptop'
600
'321912987542'
'description for the laptop'
>>> with app.app_context():
... Item.query.filter_by(price=500)
...
<flask_sqlalchemy.query.Query object at 0x0000022A1BC69850>
>>> with app.app_context():
... for item in Item.query.filter_by(price=500):
... item.name
...
'iPhone 10'
Exit from the python shell with exit()
or quit()
.
And start the app:
flask --app market run --debug
Porblem is to avoid circular imports: packages.
- Create the main application file:
run.py
- Move imports and app and db initialization from
market.py
torun.py
- Move routes from
market.py
toroutes.py
- Move
class Item
frommarket.py
tomodels.py
- Delete
market.py
- Create directory
market
- Move files
routes.py
andmodels.py
into themarket
directory - Move directory
templates
into themarket
directory - In
market
directory, create file__init__.py
- Move contents from
run.py
to__init__.py
- Add following code to
run.py
:
from market import app
# Checks if the run.py file has executed directly and not imported
if __name__ == "__main__":
app.run(debug=True)
- Running the app via
python .\run.py
, will result in page not found error - Add the line
from market import routes
to__init__.py
- Running app now will result in error:
NameError: name 'app' is not defined
- Insert the following lines at the top of the file
routes.py
:from market import app
from flask import render_template
from market.models import Item
- Insert the following lines at the top of the file
models.py
:from market import db
Contrary to the tutorial the database in my project was created in a directory instance
instead of the working directory. And I have to keep it there, I cannot move the directory and the database into the market
directory. Problem to be solved later.
Model for users:
class User (db.Model):
id = db.Column(db.Integer(), primary_key=True)
username = db.Column(db.String(length=30), nullable=False, unique=True)
email_address = db.Column(db.String(length=50), nullable=False, unique=True)
password_hash = db.Column(db.String(length=60), nullable=False)
budget = db.Column(db.Integer(), nullable=False, default=1000)
items = db.relationship('Item', backref='owned_user', lazy=True)
Modified model for items:
class Item(db.Model):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(length=30), nullable=False, unique=True)
price = db.Column(db.Integer(), nullable=False)
barcode = db.Column(db.String(length=12), nullable=False, unique=True)
description = db.Column(db.String(length=1024), nullable=False, unique=True)
owner = db.Column(db.Integer(), db.ForeignKey('user.id'))
After adding the model for users, the database had to be recreated. The code was different from the video. Open a python shell in the directory of the repository. The difference is that I have to use with app.app_context():
.
>>> from market.models import db
>>> from market.routes import app
>>> with app.app_context():
... db.drop_all()
... db.create_all()
...
>>> from market.models import User, Item
>>> u1 = User(username='jsc', password_hash='123456', email_address='jsc@jsc.com')
>>> with app.app_context():
... db.session.add(u1)
... db.session.commit()
...
>>> with app.app_context():
... User.query.all()
...
[<User 1>]
>>> i1 = Item(name='iPhone 10', description='description for the iPhone', barcode='1234567890123', price=800)
>>> i2 = Item(name='Laptop', description='description for the laptop', barcode='0123456789123', price=1000)
>>> with app.app_context():
... db.session.add(i1)
... db.session.add(i2)
... db.session.commit()
...
>>> with app.app_context():
... Item.query.all()
...
[Item iPhone 10, Item Laptop]
>>> with app.app_context():
... item1 = Item.query.filter_by(name='iPhone 10').first()
... item1.barcode
... item1.owner # empty, not yet specified
...
'1234567890123'
>>> with app.app_context():
... item1.owner = User.query.filter_by(username='jsc').first().id
... db.session.add(item1)
... db.session.commit()
... item1.owner
...
1
>>> with app.app_context():
... i = Item.query.filter_by(name='iPhone 10').first()
... i.owned_user
...
<User 1>
Install WT Forms
pip install flask-wtf
Creating a secret key for the forms. Open a python shell:
>>> import os
>>> os.urandom(12).hex()
'8e119499f4ecb2216ba55520'
Install email validator:
pip install email_validator
Validators must always be a list.
Flask also includes the validators (min and max length) in the HTML5 input element, so a first validation occurs there.
def validate_username(self, username_to_check):
Both parts of the method name are important.
validate
: The class will look for methods starting with this wordusername
: and apply them on field equal to the second word
Passwords are not stored encrypted in the database.
pip install flask_bcrypt
In file __init__.py
add:
...
from flask_bcrypt import Bcrypt
...
bcrypt = Bcrypt(app)
At the end of part 1:
pip install flask_login
Halfway through this lesson, you will get an error:
ImportError: cannot import name 'url_decode' from 'werkzeug.urls' (C:\Users\johan\Projects\fcc_flask_1\.venv\Lib\site-packages\werkzeug\urls.py)
In order for this to work you will have to downgrade Flask and WerkZeug
Flask
pip uninstall Flask
pip install Flask==2.3.0
Werkzeug
pip uninstall Werkzeug
pip install WerkZeug==2.3.0
Tip
If you want to see the code of something you imported in VS Code, for example of
UserMixin
infrom flask_login import UserMixin
.
Move cursor toUserMixin
and pressF12
.
The Font Awesome link for the coins icon doens't work. My workaround:
At the bottom of the page, replace
<script src='https://kit.fontawesome.com/a076d05399.js'></script>
with
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
In the navbar
, replace
<li class="nav-item">
<a class="nav-link" style="color:lawngreen; font-weight: bold;">
<i class="fas fa-coins"></i>
{{ current_user.budget }}
</a>
</li>
with
<li class="nav-item">
<a class="nav-link" style="color:lawngreen; font-weight: bold;">
<iconify-icon icon="la:coins" width="24" style="vertical-align: middle;"></iconify-icon>
{{ current_user.budget }}
</a>
</li>
PS: {{ current_user.budget }}
is later replaced by {{ current_user.prettier_budget }}
Note the changes that were made to routes.py
and __init__.py
:
login_manager.login_view
login_manager.login_message_category
@login_required
- For more information on modals: Bootstrap 4 - Modal
If you want to test the data you got via a form, before adding more logic (in def Market_page()
):
if purchase_form.validate_on_submit():
print(purchase_form.__dict__)
One key is of interest to us: "submit"
:
if purchase_form.validate_on_submit():
print(purchase_form.__dict__['submit'])
No comments.
Learn more about
- Jinja
- SQLAlchemy
- WTForms
- Can I specify the location of the database instead of using the default location in an
instance
directory?