C.R.U.D. tutorial

In this tutorial we are going to add the concept of a notepad (title and description) to our application. The logical steps are detailed as a first approach to the development of uvlhub .

Table of contents

  1. Create a new module
    1. Dynamic loading of modules
  2. Model design
  3. Inclusion of dependencies
  4. Default route: list all my notepads
    1. Define the route in routes.py
    2. Define the template notepad/templates/notepad/index.html
    3. Add new function in NotepadService
    4. Add new function in NotepadRepository
  5. Migrations
    1. Create a new migration
    2. Apply the new migration
  6. Design form
  7. Complete C.R.U.D.
    1. Create a notepad
      1. Route in routes.py
      2. Template notepad/templates/notepad/create.html
    2. Read a notepad
      1. Route in routes.py
      2. Template notepad/templates/notepad/show.html
    3. Edit a notepad
      1. Route in routes.py
      2. Template notepad/templates/notepad/edit.html
    4. Delete a notepad
      1. Route in routes.py

Create a new module

We are going to create the notepad module. To do this, we are going to use the Rosemary CLI:

rosemary make:module notepad

This creates a folder in app/modules/notepad with several files inside. Take some time to examine each file and understand how they are related.

Dynamic loading of modules

If we would like to check if the module is already listed by the system, we apply:

rosemary module:list

Reboot required!

However, even if we see the module listed, Flask may not yet allow navigation in the routes of that module. This is because Flask has a particular way of loading files and modules in its initial stage. We have to reboot our Flask server (or Docker container). After that, our module should appear in the list.

We can also list the current routes of our module with:

rosemary route:list notepad

We should see something like this:

notepad.scripts         GET         /notepad/script.js  
notepad.index           GET         /notepad  

Model design

Let’s make the Notepad model a bit more interesting. Let’s add two fields and add an owner user.

The app/modules/notepad/models.py file would look like this:

from app import db

class Notepad(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(256), nullable=False)
    body = db.Column(db.Text, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

    user = db.relationship('User', backref='notepads', lazy=True)

    def __repr__(self):
        return f'Notepad<{self.id}, Title={self.title}, Author={self.user.username}>'

Inclusion of dependencies

Since this is your first time developing this project, it can be a bit confusing to manage dependencies.

Before you continue, make sure that at the beginning of the routes.py file you have the following content:

from flask import render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user

from app.modules.notepad.forms import NotepadForm
from app.modules.notepad import notepad_bp
from app.modules.notepad.services import NotepadService

notepad_service = NotepadService()

Default route: list all my notepads

It’s a bit boring to work only with code and not see anything, so let’s do something interesting! Let’s re-define the /notepad route to list the notepads created by me (even if we don’t have any yet).

Define the route in routes.py

'''
READ ALL
'''
@notepad_bp.route('/notepad', methods=['GET'])
@login_required
def index():
    form = NotepadForm()
    notepads = notepad_service.get_all_by_user(current_user.id)
    return render_template('notepad/index.html', notepads=notepads, form=form)

Define the template notepad/templates/notepad/index.html

{% extends "base_template.html" %}

{% block title %}View my notepads{% endblock %}

{% block content %}

{% if notepads %}
    <ul>
    {% for notepad in notepads %}
        <li>
            <strong><a href="{{ url_for('notepad.edit_notepad', notepad_id=notepad.id) }}">{{ notepad.title }}</a></strong> - {{ notepad.body }}
            <a href="{{ url_for('notepad.edit_notepad', notepad_id=notepad.id) }}">Edit</a>
            <form method="POST" action="{{ url_for('notepad.delete_notepad', notepad_id=notepad.id) }}">
                {{ form.hidden_tag() }}
                <button type="submit">Delete</button>
            </form>
        </li>
    {% endfor %}
    </ul>
{% else %}
    <p>You have no notepads.</p>
{% endif %}

{% endblock %}

{% block scripts %}
    <script src="{{ url_for('notepad.scripts') }}"></script>
{% endblock %}

Add new function in NotepadService

The notepad/services.py file should look like this:

from app.modules.notepad.repositories import NotepadRepository
from core.services.BaseService import BaseService

class NotepadService(BaseService):
    def __init__(self):
        super().__init__(NotepadRepository())

    def get_all_by_user(self, user_id):
        return self.repository.get_all_by_user(user_id)

Add new function in NotepadRepository

The notepad/repositories.py file should look like this:

from app.modules.notepad.models import Notepad
from core.repositories.BaseRepository import BaseRepository

class NotepadRepository(BaseRepository):
    def __init__(self):
        super().__init__(Notepad)

    def get_all_by_user(self, user_id):
        return Notepad.query.filter_by(user_id=user_id).all()

We go to the /notepad route in the browser. Since we use the middleware @login_required, it is necessary to log in using a test user:

User: user1@example.com
Pass: 1234

If we access /notepad we notice that it gives error. Why do you think it gives error?

Migrations

Even if you define a model, it does not automatically exist in the database. You need to update the database, but don’t even think of creating a table by hand! No, that’s what migrations are for.

Concept of a migration

A migration is a software artefact that details how a database evolves, i.e. how it migrates from one state to another.

Create a new migration

Since we have a new entity in our model, in this case Notepad, it is necessary to create a new migration:

flask db migrate -m "create_notepad_model"

This creates a file in migrations/versions/XXXXXXXXX_create_notepad_model with XXXXXXXXX being a unique alphanumeric string generated via the timestamp. Take your time to parse this file.

Let’s go back to the /notepad route and see that it still gives an error. Why do you think it happens, if we have already created a new migration?

Apply the new migration

It is important to understand that the above command has only created the migration file, but we have not executed it yet. To run new migrations:

flask db upgrade

We go to the /notepad route and see that it no longer gives an error. Excellent!

Design form

We are going to design a form thanks to the Flask-WTForms package.

Flask-WTForms

Flask-WTForms is a Flask extension that allows you to manage and validate forms in an efficient and structured way within Flask web applications. It combines the simplicity of HTML forms with the advantages of server-side data validation, all in a simple and reusable way.

The notepad/forms.py file must have this content:

from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length

class NotepadForm(FlaskForm):
    title = StringField('Title', validators=[DataRequired(), Length(max=256)])
    body = TextAreaField('Body', validators=[DataRequired()])
    submit = SubmitField('Save notepad')

Complete C.R.U.D.

With all that we have learned and thanks to the form, we are ready to design a complete C.R.U.D.

Create a notepad

Route in routes.py

'''
CREATE
'''
@notepad_bp.route('/notepad/create', methods=['GET', 'POST'])
@login_required
def create_notepad():
    form = NotepadForm()
    if form.validate_on_submit():
        result = notepad_service.create(title=form.title.data, body=form.body.data, user_id=current_user.id)
        return notepad_service.handle_service_response(
            result=result,
            errors=form.errors,
            success_url_redirect='notepad.index',
            success_msg='Notepad created successfully!',
            error_template='notepad/create.html',
            form=form
        )
    return render_template('notepad/create.html', form=form)

Template notepad/templates/notepad/create.html

{% extends "base_template.html" %}

{% block title %}Create notepad{% endblock %}

{% block content %}

<form method="POST" action="{{ url_for('notepad.create_notepad') }}">
    {{ form.hidden_tag() }}
    <div>
        {{ form.title.label }}<br>
        {{ form.title(size=32) }}
    </div>
    <div>
        {{ form.body.label }}<br>
        {{ form.body(rows=5) }}
    </div>
    <div>
        {{ form.submit() }}
    </div>
</form>


{% endblock %}

{% block scripts %}
    <script src="{{ url_for('notepad.scripts') }}"></script>
{% endblock %}

Read a notepad

Route in routes.py

'''
READ BY ID
'''
@notepad_bp.route('/notepad/<int:notepad_id>', methods=['GET'])
@login_required
def get_notepad(notepad_id):
    notepad = notepad_service.get_or_404(notepad_id)
    
    if notepad.user_id != current_user.id:
        flash('You are not authorized to view this notepad', 'error')
        return redirect(url_for('notepad.index'))

    return render_template('notepad/show.html', notepad=notepad)

Template notepad/templates/notepad/show.html

{% extends "base_template.html" %}

{% block title %}Notepad details{% endblock %}

{% block content %}

<h1>{{ notepad.title }}</h1>
<p>{{ notepad.body }}</p>
<a href="{{ url_for('notepad.index') }}">Back to Notepads</a>
{% endblock %}

{% block scripts %}
    <script src="{{ url_for('notepad.scripts') }}"></script>
{% endblock %}

Edit a notepad

Route in routes.py

'''
EDIT
'''
@notepad_bp.route('/notepad/edit/<int:notepad_id>', methods=['GET', 'POST'])
@login_required
def edit_notepad(notepad_id):
    notepad = notepad_service.get_or_404(notepad_id)
    if notepad.user_id != current_user.id:
        flash('You are not authorized to edit this notepad', 'error')
        return redirect(url_for('notepad.index'))

    form = NotepadForm(obj=notepad)
    if form.validate_on_submit():
        result = notepad_service.update(
            notepad_id,
            title=form.title.data,
            body=form.body.data
        )
        return notepad_service.handle_service_response(
            result=result,
            errors=form.errors,
            success_url_redirect='notepad.index',
            success_msg='Notepad updated successfully!',
            error_template='notepad/edit.html',
            form=form
        )
    return render_template('notepad/edit.html', form=form, notepad=notepad)

Template notepad/templates/notepad/edit.html

{% extends "base_template.html" %}

{% block title %}View notepad{% endblock %}

{% block content %}

<form method="POST" action="{{ url_for('notepad.edit_notepad', notepad_id=notepad.id) }}">
    {{ form.hidden_tag() }}
    <div>
        {{ form.title.label }}<br>
        {{ form.title(size=32) }}
    </div>
    <div>
        {{ form.body.label }}<br>
        {{ form.body(rows=5) }}
    </div>
    <div>
        {{ form.submit() }}
    </div>
</form>

{% endblock %}

{% block scripts %}
    <script src="{{ url_for('notepad.scripts') }}"></script>
{% endblock %}

Delete a notepad

Route in routes.py

'''
DELETE
'''
@notepad_bp.route('/notepad/delete/<int:notepad_id>', methods=['POST'])
@login_required
def delete_notepad(notepad_id):
    notepad = notepad_service.get_or_404(notepad_id)
    if notepad.user_id != current_user.id:
        flash('You are not authorized to delete this notepad', 'error')
        return redirect(url_for('notepad.index'))

    result = notepad_service.delete(notepad_id)
    if result:
        flash('Notepad deleted successfully!', 'success')
    else:
        flash('Error deleting notepad', 'error')
    
    return redirect(url_for('notepad.index'))

Take the time to check that everything is working properly. Try creating a notepad in the /notepad/create route.

You can list the routes again to see that the log has been updated:

rosemary route:list notepad

Happy development!