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.
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 settings 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. Rener 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 add to this 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 is called the App Factory pattern
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