Mastering Flask: A Comprehensive Web Development Series for Python Enthusiasts

Mastering Flask: A Comprehensive Web Development Series for Python Enthusiasts

Article 3: Blueprint and Views

Be Sure to Complete the Series

In this Flask web development series, we'll dive deeper into Flask and cover topics that build on the basics introduced in the first article. We'll explore more advanced features of Flask, including database integration, user authentication, and RESTful APIs. We'll also show you how to use Docker to containerize your Flask application, simplify deployment, and set up CI/CD with GitHub Actions to automate testing and deployment. By following this series, you'll gain a comprehensive understanding of Flask and its ecosystem, and easily build robust, scalable web applications. So follow the entire series to get the most out of your Flask learning journey.

Learning Outcomes

After reading this article, you will have gained a strong understanding of how to develop a Flask web application that incorporates routes using Flask blueprint, a template for displaying logs. You will be able to structure your Flask application using blueprints to organize your code effectively and enhance its scalability. Additionally, you will learn how to create a template that can be used to display logs within the web application, enabling users to better understand the application's behavior. Ultimately, by mastering these skills, you will be able to develop dynamic and engaging web applications that are easy to maintain and scale.

If you feel stuck please check the github repo to get some help.

Link: https://github.com/ritwikmath/Mastering-Flask/tree/article-three

Blueprint and Views

In Flask, a view is a function that is associated with a specific route and accepts a client request, processes it, and sends a response back to the client. Views are the basic building blocks of a Flask application, and each view is responsible for handling a specific HTTP request and returning an appropriate response.

In Flask, a blueprint is a way to group related views by creating sets of routes that share a common prefix. Views, which are functions that handle HTTP requests, can be organized more efficiently and logically using blueprints. This simplifies the management of the application and makes it easier to understand the relationships between different views.

Separating Routes from app.py

You can create a new folder named "route" in the root directory and then create two distinct Python files within this folder named "logs.py" and "developers.py". These files can be used to store Flask routes and associated functions. After creating routes for the logs and developers resources, the corresponding routes in the app.py file will be removed.

from flask import Blueprint
from database.mongo import Database as Mongo
from bson import ObjectId
import json

log_bp = Blueprint('log', __name__, url_prefix='/logs')

@log_bp.get('/')
def fetchAlllogs():
    try:
        logs = list(Mongo().db.logs.find({}))
        return {'status': True, 'data': json.loads(json.dumps(logs, default=str))}
    except Exception as ex:
        return {'status': False, 'error': ex.__str__()}

@log_bp.put('/<string:doc_id>')
def softDeletLog(doc_id):
    try:
        updated_doc = Mongo().db.logs.find_one_and_update({'_id': ObjectId(doc_id)},
            {
                '$set':  {'deleted': True}
            }, 
            upsert=True, 
            new=True
        )
        return {'status': True, 'data': json.loads(json.dumps(updated_doc, default=str))}
    except Exception as ex:
        return {'status': False, 'error': ex.__str__()}

@log_bp.delete('/<string:doc_id>')
def deletLog(doc_id):
    try:
        Mongo().db.logs.delete_one({'_id': ObjectId(doc_id)})
        return {'status': True, 'data': {'deleted_id': doc_id}}
    except Exception as ex:
        return {'status': False, 'error': ex.__str__()}

This code defines a Flask blueprint named "log_bp" that groups together three routes that handle HTTP GET, PUT, and DELETE requests.

Flask blueprint named "log_bp". The first argument, "log", is the name of the blueprint, which is used to identify it in the Flask application. The second argument, name, is a special Python variable that represents the name of the current module, in this case, the module in which this line of code is written. Flask uses this information to locate resources such as templates and static files associated with the blueprint. The third argument, url_prefix='/logs', specifies a prefix that will be added to all the URLs associated with the blueprint. In this case, all the URLs will start with "/logs". Using a blueprint allows you to organize related routes and functions into a modular unit. You can register the blueprint with your Flask application using the app.register_blueprint() method. All the routes defined within the blueprint will be associated with the prefix specified in the blueprint definition.

The first route is associated with the "fetchAlllogs" function and returns a JSON object containing all the logs in the database. The second route is associated with the "softDeletLog" function and accepts a document ID as a parameter. It updates the specified log document in the database by setting its "deleted" field to True and returns the updated document as a JSON object. The third route is associated with the "deletLog" function and also accepts a document ID as a parameter. It deletes the specified log document from the database and returns the deleted document's ID as a JSON object. By using a blueprint, related routes can be grouped together for organization and modularity. This improves code readability and maintainability, making it easier to add or modify functionality in the future.

Similarly, the developers.py file will be created to register the routes through the blueprint.

from flask import Blueprint
from app import developers
from flask import request
from database.mysql import Database as MySql
from database.mongo import Database as Mongo
from sqlalchemy import insert

developer_bp = Blueprint('developer', __name__, url_prefix='/developers')

@developer_bp.post('/')
def create():
    try:
        db = MySql()
        ins = insert(developers).values(first_name = request.json.get('first_name'), 
                                last_name = request.json.get('last_name'),
                                expert = request.json.get('expert'))
        result = db.connection.execute(ins)
        Mongo().db.logs.insert_one({
            'type': 'activity',
            'url': request.url,
            'function': 'create developer'
        })
        return {'status': True, 'data': result.rowcount}
    except Exception as ex:
        Mongo().db.logs.insert_one({
            'type': 'error',
            'url': request.url,
            'message': ex.__str__()
        })
        return {'status': False, 'error': ex.__str__()}

@developer_bp.get('/')
def fetch():
    try:
        db = MySql()
        ins = developers.select()
        result = db.connection.execute(ins)
        developers_list = []
        for row in result:
            developer_dict = {'id': row.id, 'first_name': row.first_name, 'last_name': row.last_name, 'expert': row.expert}
            developers_list.append(developer_dict)
        Mongo().db.logs.insert_one({
            'type': 'activity',
            'url': request.url,
            'function': 'fetch all developers'
        })
        return {'status': True, 'data': developers_list}
    except Exception as ex:
        Mongo().db.logs.insert_one({
            'type': 'error',
            'url': request.url,
            'message': ex.__str__()
        })
        return {'status': False, 'error': ex.__str__()}

@developer_bp.patch('/<int:id>')
def update(id):
    try:
        db = MySql()
        ins = developers.update().where(developers.c.id == id).values(**request.json)
        db.connection.execute(ins)
        updated_row = db.connection.execute(developers.select().where(developers.c.id == id))
        developers_list = []
        for row in updated_row:
            developer_dict = {'id': row.id, 'first_name': row.first_name, 'last_name': row.last_name, 'expert': row.expert}
            developers_list.append(developer_dict)
        Mongo().db.logs.insert_one({
            'type': 'activity',
            'url': request.url,
            'function': 'update a developer'
        })
        return {'status': True, 'data': developers_list[0]}
    except Exception as ex:
        Mongo().db.logs.insert_one({
            'type': 'error',
            'url': request.url,
            'message': ex.__str__()
        })
        return {'status': False, 'error': ex.__str__()}

@developer_bp.delete('/<int:id>')
def delete(id):
    try:
        db = MySql()
        ins = developers.delete().where(developers.c.id == id)
        db.connection.execute(ins)
        Mongo().db.logs.insert_one({
            'type': 'activity',
            'url': request.url,
            'function': 'delete a developer'
        })
        return {'status': True, 'data': {'deleted_id': id}}
    except Exception as ex:
        Mongo().db.logs.insert_one({
            'type': 'error',
            'url': request.url,
            'message': ex.__str__()
        })
        return {'status': False, 'error': ex.__str__()}

Although the code remains the same, the routes are now registered through the blueprint instead of directly in the app. However, this approach will not work as these blueprints need to be registered to the app first. To achieve this, we need to import these blueprints in app.py and run it after initializing the database.

from flask import Flask, request, url_for, current_app
from bson import ObjectId
from database.mysql import Database as MySql
from database.mongo import Database as Mongo
from sqlalchemy import Table, Column, Integer, String, MetaData, insert
import json

app = Flask(__name__)

meta = MetaData()

developers = Table(
   'developers', meta, 
   Column('id', Integer, primary_key = True), 
   Column('first_name', String(255)), 
   Column('last_name', String(255)), 
   Column('expert', String(255)), 
)

@app.before_request
def logRequest():
    Mongo().db.logs.insert_one({
        'type': 'request',
        'client_addr': request.remote_addr,
        'url': request.url,
        'http_method': request.method,
        'body': request.method in ['POST', 'PATCH'] and request.get_json() or None
    })

if __name__ == '__main__':
    db = MySql()
    db.connect()
    meta.create_all(db.engine)
    Mongo().connect()
    from route.logs import log_bp
    from route.developers import developer_bp
    app.register_blueprint(log_bp)
    app.register_blueprint(developer_bp)
    app.run(debug=True)

Add a new key, client_addr, to store the remote address of the client. Storing the client's remote address in request logs can help with troubleshooting, security, and auditing. It can provide valuable information about who is accessing your application and from where, and can aid in identifying and resolving issues or suspicious activity.

Log Viewing Templates

To display logs on a browser, first make a template folder and create a dashboard.html file within it.

Within the HTML, display logs in cards.

<!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">
    <title>Document</title>
    <style>
        h1 {
            text-align: center;
        }

        .card-container {
            width: 80%;
            margin: 2rem auto;
            display: grid;
            grid-template-columns: 1fr 1fr 1fr;
            grid-auto-rows: auto;
            grid-gap: 1rem;
        }

        .card {
            background-color: rgb(210, 241, 231);
            padding: 1rem;
            border-radius: 1rem;
        }
    </style>
</head>
<body>
    <h1>Dashboard</h1>
    <div class="card-container">
        {% for log in logs %}
            <div class="card">
                {% for key, value in log.items() %}
                    <h4>{{key}}: </h4>
                    <p>{{value}}</p>
                {% endfor %}
            </div>
        {% endfor %}
    </div>
</body>
</html>

Then, create a route by making a home.py file in the route folder.

Make a blueprint and use no prefix to access the home routes from the base URL. Create a dashboard view and get all log details from Mongo. Render the template by passing the log details, and before passing the data, convert the object ID to a string using json.loads(json.dumps(logs, default=str)). The code "json.loads(json.dumps(logs, default=str))" is used to convert the object ID to a string before passing the data. The "json.dumps" method converts the logs object to a JSON formatted string with the default string representation of the object ID, and then "json.loads" method converts the JSON formatted string back to a Python object with the object ID converted to a string. This is necessary because some data types, like object IDs, cannot be serialized directly to JSON.

from flask import Blueprint, render_template
from database.mongo import Database as Mongo
import json

home_bp = Blueprint('home', __name__)

@home_bp.get('/')
def dashboard():
    logs = list(Mongo().db.logs.find({}))
    return render_template('dashboard.html', logs=json.loads(json.dumps(logs, default=str)))

308 Permanent Redirect

developer_bp = Blueprint('developer', __name__, url_prefix='/developers')

@developer_bp.post('/')
def create():

When a Flask route includes a trailing slash and an API request is made without the trailing slash, such as 'http://127.0.0.1:5000/developers', Flask will automatically redirect the request to 'http://127.0.0.1:5000/developers/' using the 308 HTTP protocol. However, if the trailing slash is not included in the original request, attempting to add it to the URL will result in a 404 Not Found error.

The app was creating a problem because it was saving two request logs for each call - one for the client request and another for the 308 permanent redirect caused by the trailing slash. As a solution, the trailing slash was removed from the routes.

Currently, the logs are displayed in the browser using this method.