DEV Community

Cover image for Building a Full Stack Web Application using Flask (Python Web Framework) - Part Two
Stephen Omoregie
Stephen Omoregie

Posted on

Building a Full Stack Web Application using Flask (Python Web Framework) - Part Two

Hey Pythonistas! Last time (Part One) we learnt some really interesting concepts about Flask and how we can build simple yet powerful web applications using the python micro web framework - Flask. We talked about Setting up the development environment, project structure, templates, defining routes, setting up blueprints, etc.

Today, we'll continue with the last part of the tutorial where we'll be covering the following topics:

  • Handling Form Submissions/File Uploads
  • Downloading Files
  • Connecting with a Database for Data Insertion and Retrieval using MongoDB
  • User Authentication and Authorization with Flask-Login

On your marks, get set, leggo!

Image Running GIF

Handling Form Submissions/File Uploads

Remember that we imported the request module from flask in our file? The request 'object' contains a bunch of attributes/methods that you can use to retrieve information from the request coming in from the client. When it comes to form/file handling, we'll need request.form to access form data and request.files to access files uploaded via the form.

Let's see an example of a simple form template for our project and learn some 'strange things' you might see.

# update templates/register.html
{% extends 'base.html' %}


{% block css %}
<link rel="stylesheet" href="{{url_for("static", filename="css/register.css")}}">
{% endblock css %}

{% block js %}
<script src="{{url_for("static", filename="js/register.js")}}" defer></script>
{% endblock js %}

{% block body_content %}
<main class="main-container">
    <h1 class="main-header">Register an Account with Us</h1>
    <p class="main-subtitle">Register here with us!</p>

    <form action="/register" method="post" enctype="multipart/form-data">
        <input type="text" name="username" id="username" required placeholder="Username"/>
        <input type="email" name="email" id="email" required placeholder="Email Address"/>
        <input type="password" name="password" id="password" required placeholder="Password" />
        <input type="file" name="picture" id="picture" accept=".png .jpg .jpeg" required>   
        <button>Register Now</button>
    </form>
</main>
{% endblock body_content %}
Enter fullscreen mode Exit fullscreen mode

Image Register Screen

Also, let's update the route we defined for the /register endpoint in auth_blueprint.py to handle form submission as well as file being uploaded by the user.

from flask import Blueprint, request, render_template
from flask import redirect

auth_views = Blueprint("auth", __name__)

@auth_views.route("/register", strict_slashes=False, methods=["GET", "POST"])
def register():
    # Define application logic for homepage
    if request.method == "POST":
        uploaded_file = request.files['picture']
        username = request.form.get("username")
        email = request.form.get("email")
        password = request.form.get("password")

        # Save the uploaded file to a directory on your server
        # Preferably outside the application root or as you desire
        uploaded_file.save(f"/tmp/{uploaded_file.filename}")

        # Implement database logic to register user

        return render_template("login.html")

    # Render template for GET requests
    return render_template("register.html")
Enter fullscreen mode Exit fullscreen mode

Image Fightier

Hold on! Hold on! Before you start screaming WTF! Hehe... flask has an extension for handling forms called Flask-WTF - No pun intended 😜 Let me explain what's going on in the update we just made

  • <form action="/register" method="post" enctype="multipart/form-data">: When sending a form that has file upload, it's important that you include the enctype attribute on your form tag to specify that your form also includes file upload. We've also specified the url the form would send our data to and specified post as the request method (which is the default form behaviour anyways)

  • if request.method == "POST":: Remember, we defined our route for /register to listen for both POST and GET requests, so we need a conditional statement to handle it accordingly. You can choose to separate the logic for POST and GET requests by defining them separately. e.g @auth_views.get("/register",.....) or @auth_views.post("/register", ....)

  • uploaded_file = request.files['picture']: Take note that it is important that your form child elements should have unique names as that would be needed in retrieving their values in your application. request.files is a dictionary and we're using the bracket notation to access the key based on the name we gave the file input element in our html. This dictionary holds all file uploads in your form - if multiple files exists, so you access individual ones by their name. uploaded_file becomes a FileObject which you can carry out different file operations on and in our case we're saving it to the /tmp directory in our server. You can choose to save it directly in your working directory too as you deem fit by providing the relative or absolute directory path.

  • username = request.form.get("username"): Getting all other form data follow the same method. request.form is a dictionary containing all the form fields that were sent from when your site user submitted the form. You can either use the .get() or [] to retrieve each field data by name.

Ideally, after collecting the form data, you will carry out your database operation for creating a new user or trying to authenticate the user. More on that shortly.

Sending Files as Download from your Application

I figured it would make sense to learn how easy it is to create a download route in your application if your project demands such. You can have dynamic download routes where you receive the filename as path of the route and dynamically send that file to the user or have a static download file download (example for a resume on your portfolio website.

Update the main_blueprint.py file to include the download route function. Make sure to import this: from flask import send_from_directory

@main_views.get("/download/<str:filename>", strict_slashes=False)
def download(filename):
    # Using the send_from_directory function you can send a file
    # By providing the absolute path to the file
    # Note the /tmp/ is just for practice purpose.
    # You decide which directory you want to store your files at
    return send_from_directory(f"/tmp/Tutorial/{filename}")

Enter fullscreen mode Exit fullscreen mode

Side note: Flask allows you use extensions like Flask-WTF for more robust form handling and validation capabilities. But then again, there's no harm in doing your own form validation. Hehehe...yes write some bugs!!!!!

Image Happy_minions

Connecting with a Database (MongoDB)

Thing is, if you intend to persist data in your application, for serving dynamic data, you need a database. Let's do a little exploring on how to connect to your MongoDB database, a popular NoSQL database of choice for web applications.

  • Installation and Setup: First off, let's install the PyMongo library, which is the official python driver for connecting to a MongoDB Database. To make things easier, just create an account/database on MongoDB Atlas and get your connection string. You don't have to install mongodb on your local machine.
pip3 install pymongo
Enter fullscreen mode Exit fullscreen mode

Make sure to save your Connection string as an environment variable and use the python-dotenv library to load it to your application as it contains sensitive data. But for the purpose of this project, I'll just put dummy string so you can see what and what goes where.

Create a file database.py in the models directory and paste the code below into it. Let's explain what's going on! Leggo.

# models/database.py
from pymongo.mongo_client import MongoClient
from pymongo.server_api import ServerApi

client = MongoClient("mongodb+srv://[USERNAME]:[PASSWORD]@[YOUR_HOST:PORT]/?retryWrites=true&w=majority", server_api=ServerApi('1'))

# Access a database from the client 
db = client["YOUR_DATABASE_NAME"]
user = db["COLLECTION_TO_ACCESS"] # Example Users

# Define Class to use to map Mongodb Data for user login
class User:
    """Class Definition to handle Flask login for user"""
    def __init__(self, username, id):
        self.username = username
        self.id = id

    @staticmethod 
    def is_authenticated():
        return True

    @staticmethod
    def is_active():
        return True

    @staticmethod
    def is_anonymous():
        return False

    def get_id(self):
        return self.id

Enter fullscreen mode Exit fullscreen mode
  • from pymongo.mongo_client import MongoClient: First we import the MongoClient class from pymongo.mongo_client. as well as the ServerApi class which we'll pass when instantiating the MongoClient

  • client = MongoClient(connection_string, server_api=ServerApi('1')): from this client returned by the MongoClient connection, you can access a specific database as well as Collection (e.g User)

  • An important part of this file is the definition of the User class (it can be named anything). The reason for doing this is that our Authentication extension Flask-Login cannot work directly with MongoDB, it needs you to create a model for your user with which to help you manage authentication and authorization on your application. While you can inherit from the UserMixin class from flask_login, let's just keep things simple by creating our own simple class. This class you're creating must have all the methods as seen in our class definition above. They're used by Flask-Login to manage user authentication for you.

Configuring Flask Login on your App
We're almost done with the hard part. Now we just need to configure flask_login to wrap around our application. Let's update the app.py in the root of our project directory.

# app.py

from flask import Flask
from blueprints.main_blueprint import main_views
from blueprints.auth_blueprint import auth_views

from flask_login import LoginManager
from models.database import db, user, User
from bson import ObjectId

# create an instance of the flask application
app = Flask(__name__)

app.register_blueprint(main_views)
app.register_blueprint(auth_views)

login = LoginManager(app)
login.login_view = "/login"

# setup the login user loader
@login.user_loader #3
def load_user(id):
    """Confirm user exists in database then use else return None"""
    cur_user = user.find_one({"_id": ObjectId(id)})

    if cur_user is None:
        return None

    # Create a user instance from the retrieved user
    return User(cur_user.get("username"), str(cur_user.get("_id")))

Enter fullscreen mode Exit fullscreen mode

Here's some explanation about what's going on with the addition to our code.

login = LoginManager(app)
You import LoginManager from flask_login and pass your app to instantiate the LoginManager and assign it to any variable, but let's call ours login.

login.login_view = "/login"
On this line, you define the route/url you want the app to redirect users to when they try to access a protected route that they are not authorized to visit. In our example, when users try to access protected routes, they will be redirected to the login page.

@login.user_loader
We need to create a login decorator for a function that will be called when our flask-login manager needs to load a user. This function will retrieve the user's id from the class we defined earlier using the user's id. If it does not exist, it returns none and the user is redirected to a login page. Make sure to create a secret key for your app. Flask-login uses cookies to sign each session, so it needs a secret key to sign the cookies We import the user collection we defined as user in the models/database.py

Since we're retrieving the user from the database using the id retrieved from cookies, we need to create an ObjectId from the id string as that's the type MongoDb is expecting to match the filter. We therefore import ObjectId from bson module.

If a match is found from the database, then we Create an instance of the User class we defined earlier that Flask-Login will be using and return the class instance from the function. Don't sweat it, that's all you need to know.

Take note that the variable user refers to our connection to the 'Users' Collection in MongoDB, while User refers to the class we defined for Flask-Login to be able to verify authenticated/authorized users.

Next Step, we need to implement the register and login logic on our /login and /registerroute in the auth_blueprints.py file. Update the file in blueprints/auth_blueprints.py with the code below (specifically the /login route). I've documented the code to help you understand what's going on and why.

# Update file blueprints/auth_blueprints
from flask import Blueprint, request, render_template, redirect, flash
from flask_login import login_required, logout_user, current_user, login_user
from werkzeug.security import generate_password_hash, check_password_hash


auth_views = Blueprint("auth", __name__)

# Create routes on this blueprint instance
@auth_views.route("/register", strict_slashes=False, methods=["GET", "POST"])
def register():
    # Define application logic for homepage
    if request.method == "POST":
        uploaded_file = request.files['picture']
        username = request.form.get("username")
        email = request.form.get("email")
        password = request.form.get("password")

        # Save the uploaded file to a directory on your server
        # Preferably in a folder you define in your app directory
        # example in static/profile_pic/
        uploaded_file.save(f"/static/profile_pic/{uploaded_file.filename}")

        # Implement database logic to register user
        # Create a dictionary of your user details to insert into the MongoDB Collection for a new User 
        new_user = {
            "username": username,
            "email": email,
            "password": generate_password_hash(password),
            "profile_pic": f"/static/profile_pic/{uploaded_file.filename}"
        }
        try:
            # Retrieve the user collection from database as defined in models/database.py
            from models.database import user

            # Check if the email/username already exists in db
            check_email = user.find_one({"email": request.form.get("email")})
            check_username = user.find_one({"username": request.form.get("username")})

            if check_email or check_username:
                flash("Credentials Already in use!", "error")
                return redirect("/register")

            new_user = user.insert_one(new_user)
            return redirect("/login")

        # If any error occurs, we can catch it
        except Exception as e:
            print(e)
            flash("Error occured during registration. Try again!", "error")
            return redirect("/register"),

    # When it's a GET request we sent the html form
    return render_template("register.html")


@auth_views.route("/login", strict_slashes=False, methods=["GET", "POST"])
def login():
    # Define application logic for profile page
    # If a user alredy exists and tries to be funny by
    # manually entering the /login route, they should be 
    # redirected to the index page 
    if current_user.is_authenticated:
        return redirect("/")

    if request.method == "POST":
        # Enter logic for processing registration
        from models.database import user, User

        # Get username and password from the form
        username = request.form.get("username")
        user_password = request.form.get("password")

        # Retrieve user from the database with username
        find_user = user.find_one({"username": username})

        # Return an error if user not in database
        if find_user == None:
            flash("Invalid Login Credentials!", "error")
            return redirect("/login")

        # Compare the user's password with the password returned from db

        is_valid_password = check_password_hash(find_user.get("password"), user_password)

        # If password does not match, redirect user to login again
        if not is_valid_password:
            flash("Invalid Login Credentials!", "error")
            return redirect("/login")

        # At this point all is well; so instantiate the User class 
        # This is to enable the Flask-Login Extension kick in
        log_user = User(find_user.get("username"), str(find_user.get("_id")))

        # use the login_user function imported from flask_login
        login_user(log_user)

        # Then return the user to the index page after sucess
        return redirect("/")

        # Make sure to do proper error handling with try/except
        # I don't want to make the code too bulky

    # for Get request to the route, we sent the html form
    return render_template("login.html")


# Create Sign Out Route which we'll create a button for
@auth_views.route("/logout", strict_slashes=False)
@login_required
def logout():
    # We wrap the logout function with @login_required decorator
    # So that only logged in users should be able to 'log out'
    logout_user()
    return redirect("/")

Enter fullscreen mode Exit fullscreen mode

Cool, that's a lot of code! Let's quickly explain some of the things not properly covered in the documentation within the code.

  1. The new functions we've imported
from ..., flash
from flask_login import login_required, logout_user, current_user, login_user
from werkzeug.security import generate_password_hash, check_password_hash
Enter fullscreen mode Exit fullscreen mode
  • flash: This function allows us to send a message from one page to the next page. This allows us to do things like sending a notification to our page when maybe a registration/login attempt fails or is successful. You can access the message (string) within your template by using the function:
{% with messages = get_flashed_messages(with_categories=true) %}
    {% if messages %}
        <ul class=flashes>
        {% for category, message in messages %}
            {% if loop.first %}
                <li class="{{ category }}" style="color: white;">{{ message }}</li>
            {% endif %}
        {% endfor %}
        </ul>
    {% endif %}
{% endwith %}

Enter fullscreen mode Exit fullscreen mode
  • login_required, logout_user, current_user, login_user:
  1. login_required: You use it as a decorator for the routes you want to protect i.e routes that can only be accessed or used by logged in users. Just as we did for the /logout and /profile routes.
  2. logout_user: You can call this function within your designated logout route. This will clear the cookies used to verify the user on every request by flask-login.
  3. current_user: After a user logs in, the User instance created by the user is loaded here so you can use if for conditional rendering in your templates or within your route.
  4. login_user: This function is used to login the user. It receives an instance of the User Class earlier defined.

  • from werkzeug.security import generate_password_hash, check_password_hash: Werkzeug module has tons of utility functions, classes and stuff that you can use within your flask application. We're using two functions here, to hash our password before saving it to the database. Since we're storing a hashed value to the database, we also need to compare what the user enters with the hashed value stored in database to see if they match. If it does, then you login the user.

Conditionally rendering elements within your templates.

Currently, visiting any page, you would find Sign In, Sign Up, Profile and Sign Out links all visible. But we need to make sure that Sign In should be shown to users who are not logged in, while the other three (excluding sign in) are visible only to users who are logged in.

So let's update our header tag in the templates/base.html file. Update only the Header Section as in our example for this project:

<!-- Preceeding parts remain the same -->

<header class="header-container">
    <a href="/"><h2 class="header-title">Flask Project</h2></a>
    <nav class="navigation-container">
        <ul>
            <a href="/login"><li class="link">Sign In</li></a>
            {% if current_user.is_authenticated %}
            <a href="/register"><li class="link">Sign Up</li></a>
            <a href="/profile"><li class="link">Profile</li></a>
            <a href="/logout"><li class="link">Sign Out</li></a>
            {% endif %}
        </ul>
    </nav>
</header>

<!-- Other parts remain the same -->
Enter fullscreen mode Exit fullscreen mode
  • {% if current_user.is_authenticated %}: This block is used to conditionally render portions of the page to the user. you don't have to import current_user into the template, as it is globally available to all your templates. You check if it is authenticated, remember, the is_authenticated() method defined in your class returns True for authenticated users. Make sure to close the block with {% endif %}

Conclusion

Phew! This has been a very long, and truly exciting writing project. with a touch of weird GIFs haha. I hope you enjoy it and learn something from it too.

Remember, this project is just a skeleton project to give you the surface comfort with using the Flask Microframework to build your full stack applications and integration with MongoDB. Enjoy and have fun! Happy Coding from Stephen!

Image Final dancing

Additional Resources:

Top comments (4)

Collapse
 
kasambalaji profile image
KasamBalaji

Enjoyed this!

Collapse
 
cre8stevedev profile image
Stephen Omoregie

Thank you! Happy coding

Collapse
 
stepheweffie profile image
stepheweffie

I'm into it.

Collapse
 
cre8stevedev profile image
Stephen Omoregie

Awesome!