Implementing Notes CRUD - Creating and Reading

Length: 00:18:55

Lesson Summary:

One of the most common things to implement in any web application is the CRUD functionality around a specific type of data: Create, Read, Update, and Delete. In this lesson, we'll implement the C and R portions of CRUD for our Note class.

Documentation For This Video

Populating user Based on the Session

Before we create our first note, we need to add some code to our application to make working with a logged in user nicer:

  1. We want to automatically populate g.user if there is user information in the session.
  2. We want to create a decorator that can wrap views that require a logged in User to work.

For the first one, we'll use a different decorator on app to state that the specified function should run before the request is processed by a view, using app.before_request. To handle the second, we'll use functools.wraps to create a decorator that redirects to /log_in if g.user is not set. Let's implement these now:

notes/__init__.py:

import os
import functools

from flask import Flask, render_template, redirect, url_for, request, session, flash, g
from flask_migrate import Migrate
from werkzeug.security import generate_password_hash, check_password_hash

def create_app(test_config=None):
    # Ommit initial setup

    db.init_app(app)
    migrate = Migrate(app, db)

    def require_login(view):
        @functools.wraps(view)
        def wrapped_view(**kwargs):
            if not g.user:
                return redirect(url_for('log_in'))
            return view(**kwargs)
        return wrapped_view

    @app.before_request
    def load_user():
        user_id = session.get('user_id')
        if user_id:
            g.user = User.query.get(user_id)
        else:
            g.user = None

    # Remaining code omitted

We needed to import both functools and the g object from Flask itself. From there, we created the require_login function that does a few things:

  1. Takes in a view to decorate
  2. Defines the wrapped_view that decorates the view, redirecting if there is no user and calling the original view if there is

The used of functools.wraps allows the wrapped_view that we return to copy some of the contents that are implicitly designated for a function, such as __name__. So the wrapped_view.__name__ == view.__name__ would be true.

The load_user function doesn't do anything but assign g.user if there is a user_id in the session. Without adding any more code, we can actually see changes to our navigation now.

Creating a Note

We need to create a few different routes for our Notes CRUD:

  • /notes - The notes index that will list out the currently logged in User's notes
  • /notes/new - The note creation page that will have a form for creating a new note
  • /notes/<note_id>/delete - This route will be used to delete a note by handling using either a GET or DELETE method. If we receive that request, then we'll delete the note with the given note_id. The most accurate HTTP method to use is DELETE, but browsers mostly utilize GET and POST so we'll support GET to avoid writing JavaScript code to execute our request.
  • /notes/<note_id>/edit - The note edit page to allow us to make modifications to an existing note

To limit the amount of time that we need to spend writing HTML, we can copy over the contents of /tmp/contents-intro-to-python-development/note_templates into our project's templates directory:

(notes) $ cp /tmp/content-intro-to-python-development/note_templates/* ~/projects/notes/templates/

We can't really do anything else in our system before we have notes to render, so we'll start by adding some skeleton views and then moving from there:

notes/__init__.py:

# Imports omitted

def create_app(test_config=None):
    # Initial setup omitted

    from .models import db, User, Note

    db.init_app(app)
    migrate = Migrate(app, db)

    # Earlier views omitted

    @app.route('/notes')
    @require_login
    def note_index():
        return 'Note Index'

    @app.route('/notes/new', methods=('GET', 'POST'))
    @require_login
    def note_create():
        if request.method == 'POST':
            title = request.form['title']
            body = request.form['body']
            error = None

            if not title:
                error = 'Title is required.'

            if not error:
                note = Note(author=g.user, title=title, body=body)
                db.session.add(note)
                db.session.commit()
                flash(f"Successfully created note: '{title}'", 'success')
                return redirect(url_for('note_index'))

            flash(error, 'error')

        return render_template('note_create.html')

    return app

When we're dealing with the 'C' in CRUD it usually goes the same way. We check if the request is a POST request, validate the information, create the object, and then redirect OR render the original page with errors to display.

Looking at our note_create.html template, the only new thing is that we're now setting the value for our inputs to match the corresponding item in request.form. If there is an error, we'll re-render the page with the content that the user created.

notes/templates/note_create.html:

{% extends 'base.html' %}

{% block content %}

  <h1 class="is-size-3">New Note</h1>

  <form action="{{ url_for('note_create') }}" method="post">
    <div class="field">
      <label class="label" for="title">Title</label>
      <div class="control">
        <input name="title" value="{{ request.form['title'] }}" class="input"></input>
      </div>
    </div>

    <div class="field">
      <label class="label" for="body">Body (Supports Markdown)</label>
      <div class="control">
        <textarea name="body" class="textarea has-text-monospaced">{{ request.form['body'] }}</textarea>
      </div>
    </div>

    <div class="field is-grouped">
      <div class="control">
        <input type="submit" value="Create Note" class="button is-primary" />
      </div>
      <div class="control">
        <a href="{{ url_for('note_index') }}" class="button is-text">Cancel</a>
      </div>
    </div>
  </form>

{% endblock %}

Now that we have a note, we can implement the 'R' portion of CRUD by implementing the note index page and the note show page.

Parsing Markdown

Parsing markdown and displaying it is something that's outside the scope of this course, but this is a great situation where we can rely on the open source community and pull in another dependency. We're going to use on the mistune markdown parsing library, so let's install that now:

(notes) $ pipenv install mistune
...

There are a few ways that we can use this, but in general, we need to use a mistune.Markdown object or the mistune.markdown function to take the text that we store in Note.body. We'll add a new calculated property to our Note class called body_html. Here's what it looks like:

notes/models.py:

from flask_sqlalchemy import SQLAlchemy
from mistune import markdown

# db and User omitted

class Note(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200))
    body = db.Column(db.Text)
    created_at = db.Column(db.DateTime, server_default=db.func.now())
    updated_at = db.Column(db.DateTime, server_default=db.func.now(), server_onupdate=db.func.now())
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

    @property
    def body_html(self):
        return markdown(self.body)

Now we're able to use note.body_html in our templates to render out the HTML.

Rendering Notes

Now that we can convert the Note.body into HTML, let's implement the note_index view so that we can see our notes from the UI.

notes/__init__.py:

# Imports omitted

def create_app(test_config=None):
    # Earlier code omitted

    @app.route('/notes')
    @require_login
    def note_index():
        return render_template('note_index.html', notes=g.user.notes)

    # Remaining views omitted

    return app

Because we're using require_login, we know that we'll never make it into the note_index view if there is not a g.user object. From there, we're able to simply pass the currently logged in user's notes to the template as the variable "notes", by using the corresponding keyword argument in the template we're rendering.

Let's take a look at the template that will render these notes:

notes/templates/note_index.html:

{% extends 'base.html' %}

{% block content %}
  {% if not notes %}
    <div class="content">
      <p>You haven't created any notes! <a href="{{ url_for('note_create') }}">Create your first note.</a>
    </div>
  {% endif %}

  {% for note in notes %}
    <article class="message">
      <div class="message-header">
        <p>{{ note.title }}</p>
        <div>
          <a class="button is-primary is-small has-text-weight-bold" href="#">
            Edit Note
          </a>
          <a class="button is-danger is-small has-text-weight-bold" href="#">
            Delete Note
          </a>
        </div>
      </div>
      <div class="message-body content">
        {{ note.body_html|safe }}
      </div>
    </article>
  {% endfor %}
{% endblock %}

For the most part, we've seen everything in this template already. The exception being |safe when we're rendering out the note.body_html. This is a Jinja filter, and the safe filter will tell Jinja that it should not escape the value coming from Python. Instead, we've already declared it to be "safe" to render.

Now if we log in and head to /notes, we can see the notes that we've created. And if we used fenced code blocks, we'll even see them rendered with syntax highlighting, thanks to some JavaScript that we packaged with the static assets.


This lesson is only available to Linux Academy members.

Sign Up To View This Lesson

Or Log In

Looking For Team Training?

Learn More