jmares / fcc_flask_1

Project based on a FreeCodeCamp Flask tutorial

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

FreeCodeCamp - Flask Tutorial 1

Introduction

Project based on a FreeCodeCamp Flask tutorial:

Make a website where you can buy and sell stuff.

Review

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

Database

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)
);

Lessons

If you want to see the code for the lesson, use the corresponding tag to switch to that version.

Lesson 01 - Introduction

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>'

Lesson 02 - Styling and Templates

Mostly an introduction to Bootstrap.

Lesson 3 - Sending Data to Templates

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>

Lesson 04 - Template Inheritance

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>

Lesson 05 - Models and Databases

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

Lesson 06 - Project Restructure

Porblem is to avoid circular imports: packages.

  • Create the main application file: run.py
  • Move imports and app and db initialization from market.py to run.py
  • Move routes from market.py to routes.py
  • Move class Item from market.py to models.py
  • Delete market.py
  • Create directory market
  • Move files routes.py and models.py into the market directory
  • Move directory templates into the market 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.

Lesson 07 - Model Relationships

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>

Lesson 08 - Flask Forms

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'

Lesson 09 - Flask Validation

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.

Lesson 10 - Flash Messages and Advanced Validations

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 word
  • username: and apply them on field equal to the second word

Lesson 11 - User Authentication Part 1

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

Lesson 12 - User Authentication Part 2

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 in from flask_login import UserMixin.
Move cursor to UserMixin and press F12.

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 }}

Lesson 13 - Logout and Customizations

Note the changes that were made to routes.py and __init__.py:

  • login_manager.login_view
  • login_manager.login_message_category
  • @login_required

Lesson 14 - Item Purchasing Part 1

Lesson 15 - Item Purchasing Part 2

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'])

Lesson 16 - Item Selling

No comments.

To Study

Learn more about

  • Jinja
  • SQLAlchemy
  • WTForms
  • Can I specify the location of the database instead of using the default location in an instance directory?

About

Project based on a FreeCodeCamp Flask tutorial

License:MIT License


Languages

Language:HTML 63.4%Language:Python 36.6%