An Advanced Flask App: Shopyo


flask

Shopyo is an Open Source Flask-based, Python-powered inventory solution and upcoming point of sales. It’s aim is to help small business owners get a nice Python-based product with essential amenities for their businesses. An empty Shopyo project at the very least makes a great Flask base. Shopyo also makes use of advanced flask concepts.

The Tech Stack

A peek at Shopyo’s requirements.txt gives

flask
flask_sqlalchemy
marshmallow_sqlalchemy
flask_marshmallow
flask_migrate
flask_script
flask_login
Flask-WTF
requests

  • flask_sqlalchemy

flask_sqlalchemy is a wrapper around SQLAlchemy, a popular Python ORM. An ORM allows you to define your SQL tables without the need to write SQL codes. You just define models and the table is created for you. A typical model looks like this:

class Settings(db.Model):
    __tablename__ = 'settings'
    setting = db.Column(db.String(100), primary_key=True)
    value = db.Column(db.String(100))

  • flask_migrate

flask_migrate is used to migrate your models. If you change your models by adding a new field, the change is not reflected in your database. That’s why you need to apply migrations. It is a wrapper around Alembic, a popular migration package.

  • flask_marshmallow
  • marshmallow_sqlalchemy

Marshmallow is a project that allows you to create REST Apis. flask_marshmallow allows the easy integration of marshmallow with Flask and marshmallow_sqlalchemy is a required package that goes along.

  • flask_script

Though deprecated by Flask’s official command line utitlity, flask_script allows you to create scripts to manage your Flask app easily.

  • flask_login

Flask login allows you to add authentication to your app.

  • Flask-WTF

A package that allows you to create forms. We use it to prevent CSRF (pronounced sea surf) attacks. It basically ensures that you don’t tamper with a webpage then try to send it to someone else expecting it to work

  • requests

A package to make web requests and pull in urls easily. You can view a tutorial about it here Terms Explained ===============

model


A model defines your table

class Settings(db.Model):
    __tablename__ = 'settings'
    setting = db.Column(db.String(100), primary_key=True)
    value = db.Column(db.String(100))

creates a table named settings with fields named setting and value

template

A template is an html file with spaces left for values. {% and {{ have special meaning. {{1 + 1}} will display 2 when rendered. Similarly {{x + 1}} will evaluate the expression before rendering.

{% extends "base/main_base.html" %}

{% set active_page ='settings' %}

{% block pagehead %}
<title>Settings</title>
<style>
  .hidden{
    display: none;
  }
  .show{
    display: inline-block;
  }
</style>
{% endblock %}

{% block content %}
<script type="text/javascript">
$(function() {

  });
</script>

<table class="table">
  <thead>
    <tr>
      <th scope="col">Settings</th>
      <th scope="col">Value </th>
      <th scope="col"></th>
    </tr>
  </thead>
  <tbody>
{% for setting in settings %}
    <tr>
      <td>{{setting.setting}}</td>
      <td>{{setting.value}} </td>
      <td><a href="/settings/edit/{{setting.setting}}" class="btn btn-info" role="button"><i class="fas fa-pencil-alt"></i></a></td>
    </tr>
{%endfor%}
  </tbody>
</table>

{% endblock %}


let’s take this snippet

{% extends "base/main_base.html" %}

Tells that we are inheriting from base.html

This eases our life by not copying whole <link> or <script> codes for example or not recopying the header/footer

{% block pagehead %}
...
{% endblock %}


{% block content %}
...
{% endblock %}

allows us to define our head and body respectively.

Views and blueprint

A view file has lots of routes and what happens when such a route is found. This is the settings view file for example

from flask import (
    Blueprint, render_template, request, redirect, url_for, jsonify
    )

from addon import db
from views.settings.models import Settings
from flask_marshmallow import Marshmallow
from flask_login import login_required, current_user

from project_api import base_context


settings_blueprint = Blueprint('settings', __name__, url_prefix='/settings')

@settings_blueprint.route("/")
@login_required
def settings_main():
    context = base_context()

    settings =  Settings.query.all()

    context['settings'] = settings
    return render_template('settings/index.html', **context)

...

Here is a breaking down:

from flask import (
    Blueprint, render_template, request, redirect, jsonify
    )

  • Blueprint

Allows us to create blueprints. More on that further on

  • render_template

return render_template(filename) returns the rendered html

  • request

refers to the incoming web request by which we can check for GET or POST methods

  • redirect

redirects to another url

  • jsonify

returns a string or dictionary as JSON response

from addon import db

addon contains the following:

from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from flask_login import LoginManager

db = SQLAlchemy()
ma = Marshmallow()
login_manager = LoginManager()

from views.settings.models import Settings

Tells us to go to views folder -> settings folder -> models file

...
from project_api import base_context

project_api.py contains base_context which returns just a dictionary

def base_context():
    base_context = {
        'APP_NAME': get_setting('APP_NAME'),
        'SECTION_NAME': get_setting('SECTION_NAME'),
        'SECTION_ITEMS': get_setting('SECTION_ITEMS')
    }
    return base_context.copy()

We copy so as not to let additions be global. Render templates accepts the variables to be rendered as keywords. But those 3 variables are rendered everywhere so, instead of copy paste each time, we just added them beforehand to the dictionary.

settings_blueprint = Blueprint('settings', __name__, url_prefix='/settings')

This tells that whatever urls starts with /settings will be dealt with in this file

@settings_blueprint.route("/abc")

is actually for the url /settings/abc

@settings_blueprint.route("/")
@login_required
def settings_main():
    context = base_context()

    settings =  Settings.query.all()

    context['settings'] = settings
    return render_template('settings/index.html', **context)

Context is just a dictionary

context['settings'] = settings

passes the settings variable which the for loop in the template makes use of

{% for setting in settings %}
    <tr>
      <td>{{setting.setting}}</td>
      <td>{{setting.value}} </td>
      <td><a href="/settings/edit/{{setting.setting}}" class="btn btn-info" role="button"><i class="fas fa-pencil-alt"></i></a></td>
    </tr>
{%endfor%}

By the way,

context['settings'] = settings
return render_template('settings/index.html', **context)

is the same as

return render_template('settings/index.html', 
    settings=settings,
    APP_NAME=get_setting('APP_NAME'),
    SECTION_NAME=get_setting('SECTION_NAME'),
    SECTION_ITEMS=get_setting('SECTION_ITEMS'))

Register blueprints

in app.py you will see

    from views.settings.settings_modif import settings_blueprint

    app.register_blueprint(settings_blueprint)

which adds the views of the settings folder to the app.

Configuration management

in config.py we see

class Config:
    """Parent configuration class."""
    DEBUG = False
    SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SECRET_KEY = 'qow32ijjdkc756osk5dmck'  # Need a generator
    APP_NAME = 'Demo'
    SECTION_NAME = 'Manufacturer'
    SECTION_ITEMS = 'Products'
    HOMEPAGE_URL = '/manufac/'


class DevelopmentConfig(Config):
    """Configurations for development"""
    ENV = 'development'
    DEBUG = True


app_config = {
    'development': DevelopmentConfig,
    'production': Config,
}

then in app.py

def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(app_config[config_name])
    ...


then further down

app = create_app('development')

so, if we put in ‘production’ it will load the production configs.

As info, creating apps this way (app = …) is called the App Factory pattern

manage.py

manage.py

migrate = Migrate(app, db, compare_type=True)
manager = Manager(app)

manager.add_command('db', MigrateCommand)

allows us to pass in normal Alembic migration commands

normal alembic commands run like:

alembic db init
alembic db migrate
alembic db upgrade

but using the above code, we automatically get

python manage.py db init
python manage.py db migrate
python manage.py db upgrate


similarly this:

@manager.command
def runserver():
    app.run()


allows us to have

python manage.py runserver

Conclusion

This was written as part of the Shopyo docs but makes a nice Flask post by the way!

If you did not understand something, please ping me at

arj.python at gmail dot com