Skip to content

Technical Paper

Elias Khattar edited this page May 16, 2019 · 30 revisions

MeowNotes by Elias Khattar

MeowNotes is a simple note-taking app for cat lovers.

Contents

Architecture

The source code for MeowNotes is found in the meownotes/ folder.

Overview

MeowNotes app Figure 1: MeowNotes app structure

The MeowNotes Flask app is created in meownotes/__init__.py based on the blueprint defined in meownotes/pawprint.py.

Backend

The blueprint makes use of all the backend logic in meownotes/dbquery.py as well as some utils from meownotes/utils.py.

The backend interacts with the SQLite database meownotes/meownotes.db using SQL queries that are prepared from predefined templates (e.g., INSERT INTO TABLE (COLS) VALUES (VALS)). These queries are generated given a table name and inputs using the function prepare_query() in meownotes/dbquery.py.

For conditional SQL SELECT, INSERT, and DELETE, the function supports multiple match types:

  • exact matches (e.g., ... WHERE col='val')
  • partial matches (e.g., ... WHERE col LIKE '%val')
  • multiple column matches (e.g., ...WHERE 'val' in (col, col2))

Multiple conditions can also be specified in order to form more precise queries, e.g., ... WHERE ... AND .... I made the decision to generate SQL queries in this form as I wanted to create something that was easily reusable and would support different use cases, e.g., particular search conditions.

For MeowNotes, there are two main objects of interest: users and notes (each have a database table). In order to interact with these objects, I defined additional helper functions in dbquery.py that would provide certain interactions such as creating, retrieving, or deleting a user (e.g., get_user_by_name(), get_user_by_id()) or note.

In dbquery.py, the raw database results are parsed with parse_user() and parse_note() to return dicts of the respective formats using the database columns:

user = {
    "uid": db_result[0],
    "username": db_result[1],
    "password": db_result[2]
}

note = {
    "note_id": db_note[0],
    "uid": db_note[1],
    "date_created": db_note[2],
    "ui_date": dateutil.parser.parse(db_note[2]).strftime('%b %d, %H:%m'),
    "title": db_note[3],
    "tags": db_note[4].split(","),
    "content": db_note[5],
}

Frontend

Based on the routes defined in the blueprint with the decorators @MEOW_BP.route("/..."), MeowNotes returns either one of the frontend templates under the templates/... folder or redirects to a different route based on the logic defined.

The templates are all based on the parent template templates/meownotes.html and inherit its content. The parent template contains the simple menu navigation and static imports for the content in meownotes/static/css and meownotes/static/js. There are a total of 6 child templates for MeowNotes that represent the distinct views/features of the app.

Screenshots of the different MeowNotes views possible are shown in the readme screenshots section.

Once retrieved, the database data in the format above (see previous section) is returned to the frontend as defined in the blueprint to be displayed directly in the HTML templates like in the example shown below. Here, data is passed from Flask during render_template("TEMPLATE_NAME.html", data=RETRIEVED_DATA).

      <div class="col my-auto">
        <div class="note-metadata">
          <h1>{{ data['title'] }}</h1>
          <h5>date created: {{ data['ui_date'] }}</h5>
          <p>tags: {{ data['tags'] }}</p>
        </div>
      </div>

Other templates such as the dashboard and the search results iterate through the retrieved database notes and for each one generate the specified HTML, like shown below.

{% for note in data %}
<div>
  ...
</div>
{% endfor %}

This is an advantage of using Flask and its Jinja templates, as I could generate the necessary HTML programmatically based on the number of results.

I also used conditional logic in the templates in other cases, such as determining whether the menu shows "login" or "logout" to users.

        {% if menu_item == 'logout' %}
        <form method="POST" action="{{ url_for('pawprint.search') }}">
            <div class="input-group">
                <input type="text" id="search" name="search" class="form-control mr-1 meownotes-input"
                    placeholder="search" required="">
                <span class="input-group-btn">
                    <button class="meownotes-button btn btn-med btn-primary btn-block" type="submit" data-toggle="tooltip" title="search for a note"><i class="fas fa-search"></i>
                    </button>
                </span>
            </div>
        </form>
        {% endif %}

Note that for the static files, I made the decision to include Bootstrap CSS & JS directly instead of using a CDN together with its dependencies (e.g., jQuery). This made it easier for local development by not needing to rely on internet access.

Routing

There are 12 routes defined in the pawprint blueprint, along with the default Flask route for static content /static.

Below, the endpoints are shown indicating which ones also accept POST requests with data (and not just the default GET). These are used when form data needed to be sent, e.g., for login/sign up, creating notes, updating notes. For deleting notes, a POST request was used as the "delete" button was implemented as a form with a hidden input field containing the note id that was generated as part of the Jinja template for each note card on the dashboard as well as on the single note view.

What happens after browser navigation to each route is described in more detail in the readme routes section as well as in comments directly in the code.

Routes Figure 2: Flask routes

What is notable in MeowNotes is that the login functionality is combined with the sign up. This means that for a new user, they simply must enter a username and password that is not used in the landing page form.

  • if the username exists and the password is does not match the one stored, the user is returned to the landing page where they see an error message below the form
  • if the username exists and the password is correct, they then login to see the MeowNotes dashboard
  • if the username does not yet exist, then a new account is created for the user and the hashed password is stored in the users table for that user

This combined sign up and login was an intentional decision I made to simplify the app.

Note that the raw password is never stored in the database, instead check_password_hash() and generate_password_hash from Werkzeug are used to compare and store the passwords.

At first, I had been storing the passwords in raw form, but decided to instead use hashing to have better security.

Complete landscape

To setup and install MeowNotes, the utility make can be used for the tasks defined in the Makefile. These include the following tasks:

  • create the virtualenv and install dependencies (flask, python-dateutil, uwsgi, pylint, pytest)
  • (re)create the SQLite database (deletes data and recreates tables based on the schema meownotes/meownotes-schema.sql)
  • test using pytest
    • I decided to implement some basic smoke tests in order to have an easy indicator when I broke something
  • lint using pylint with the config from the .pylintrc file
    • I used static code checking to help catch syntax errors and ensure better code quality
  • run MeowNotes in 3 modes (debug on port 5000, prod with Werkzeug on port 8000, and prod with uWSGI on port 8000 - see readme)
    • the debug mode shows additional output and live reload on change, thus making for easier development and debugging

I chose to use make in order to simplify the process and make shortcuts for myself and others. This made local development and debugging easier.

MeowNotes can be run locally using either Flask's built-in server Werkzeug or the uWSGI server, whose entrypoint is wsgi.py.

The uWSGI environment is the same as for the MeowNotes app deployed and hosted from pythonanywhere. This is why I decided to also have the uWSGI server option locally because anyways I needed to make the wsgi.py file and I could make sure it would run on pythonanywhere, which uses uWSGI.

Overview Figure 3: Overview

The entire landscape is shown above in the figure.

For the deployment to pythonanywhere, I cloned this github repo and created a virtualenv with python 3.6 (from /usr/bin/python3.6), as that's the version I have locally installed (whereas pythonanywhere has up to python 3.7).

I configured MeowNotes to use the virtualenv created and replaced the wsgi.py file content in the pythonanywhere settings with the content of my wsgi.py file.

MeowNotes

MeowNotes

Clone this wiki locally