Sharing models, classes, Blueprints and templates between apps with Flask DispatcherMiddleWare
By avoiding duplication your code becomes smaller and better maintainable.
This site is running Flask. It uses DispatcherMiddleWare to run the frontend app and the admin app. The Flask documents state that the Flask applications in this case are entirely isolated from each other. That is true but often there is a lot of code we want to share between these apps.
Things we want to share
Both apps use the same database meaning we want to share the models.py file. Then we have certain classes we wrote ourselves. For example I wrote classes like MailMessage and FormValidation. They should be used by both apps.
I am also using Blueprints that should be shared, for example the 'auth' Blueprint handling the authentication functions like log in, create account, reset password. The templates used by these Blueprints should also be shared. There are also other templates that should be shared, like macros to put forms on a page, macros that put buttons in tables.
Adding a shared directory
In a previous post I wrote about using DispatcherMiddleware and presented a basic directory structure. Now it is time to add a shared directory, see below.
|
|-- project
| |-- alembic
| | `--
| |
| |-- app_admin
| | |-- __init__.py
| | |-- blueprints
| | | |-- content_item
| | | | |-- forms.py
| | | | |-- __init__.py
| | | | `-- views.py
| | | |-- user
| | | | |-- forms.py
| | | | |-- __init__.py
| | | | `-- views.py
| | |-- templates
| | | |-- content_item
| | | | |-- content_items_list.html
| | | | `--
| | | |-- user
| | | | |-- users_list.html
| | | | `--
| | | |-- base.html
| | `-- translations
| | `-- es_ES
| | `-- LC_MESSAGES
| | |-- messages.mo
| | `-- messages.po
| |
| |-- app_frontend
| | |-- __init__.py
| | |-- blueprints
| | | |-- comments
| | | | |-- forms.py
| | | | |-- __init__.py
| | | | `-- views.py
| | | `-- demo_crud_view_uuid
| | | |-- forms.py
| | | |-- __init__.py
| | | `-- views.py
| | |-- templates
| | | |-- comments
| | | | |-- comment_form.html
| | | | `--
| | | `-- base.html
| | `-- translations
| | `-- es_ES
| | `-- LC_MESSAGES
| | |-- messages.mo
| | `-- messages.po
| |
| |-- shared
| | |-- blueprints
| | | |-- account
| | | | |-- forms.py
| | | | |-- __init__.py
| | | | `-- views.py
| | | `-- auth
| | | |-- forms.py
| | | |-- __init__.py
| | | `-- views.py
| | |-- static
| | | |-- blog
| | | |-- css
| | | |-- js
| | | |-- vendor
| | | `-- robots.txt
| | |-- templates
| | | |-- account
| | | | `-- overview.html
| | | |-- auth
| | | | `-- login.html
| | | |-- macros
| | | | `--
| | | `-- boxed.html
| | |
| | |-- constants.py
| | |-- class_app_mail.py
| | |-- class_input_validation.py
Changes to the static folder
I already have an environment variable that holds the static directory. The reason is that the static directory is at a different location on the production system. In the create_app() of both the app_frontend and app_admin we create the Flask app with the same static folder:
flask_static_folder = os.getenv('FLASK_STATIC_FOLDER')
app = Flask(
__name__,
static_folder=flask_static_folder,
)
Changes to imports
Of course we must make changes to the inital imports. For example, in app_frontend Python files we change the imports of Python files from:
# register blueprints
# authentication (shared)
from .blueprints.auth.views import auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/<lang_code>/auth')
to:
# register blueprints
# authentication (shared)
from shared.blueprints.auth.views import auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/<lang_code>/auth')
Changes to template_folders
This is a bit magical. I did not specify a template_folder when creating the Flask app meaning Flask uses the default 'templates' folder. But how can we access the shared templates? Fortunately we can specify a 'templates' directory when we create a Blueprint. If you specify:
auth_blueprint = Blueprint('auth', __name__, template_folder='templates')
then you tell Flask that there is a directory 'templates' in the Blueprint 'auth' directory. This directory is relative (!) to the Blueprint 'auth' directory. The directory structure then should be like this:
| |-- shared
| | |-- blueprints
| | | `-- auth
| | | |-- forms.py
| | | |-- __init__.py
| | | |-- views.py
| | | `-- templates
| | | `-- auth
| | | |-- login.html
| | | `--
Note that there is an additonal directory 'auth' in the templates directory because we do not want to change the views.py file. This file contains view functions ending with:
...
return render_template(
'auth/login.html',
...
If we would proceed this way we get a template directory for every shared Blueprint. not really what we want. The shared templates directory must have the same structure as the app_frontend and app_admin template directories, a single directory with subdirectories for every Blueprint. To achieve this we change the Blueprint template_folder to point to shared/templates:
auth_blueprint = Blueprint('auth', __name__, template_folder='../../templates')
We do this for all the shared Blueprints and we are done. What is magic about this is that you only have to do this for a single shared Blueprint. It looks like Flask is building a list of template search directories and once we the template_folder for the 'auth' Blueprint has been processed, the path is added to this list and the other shared Blueprints also find their templates. From the Flask documentation: 'The (Blueprint) template folder is added to the search path of templates but with a lower priority than the actual application's template folder'.
This works because in this case we refer to single shared templates directory, but I would prefer to be able to specify a template search path list at application level. There is information available how you can do this, see links below.
Translations
When you have a multilanguage site and use Babel you must make sure that the app_frontend translations are not only generated from the directory app_frontend but also from the shared directory. The same applies to app_admin. To achive this we add the directory to the file babel_app_frontend.cfg:
[python: app_frontend/**.py]
[python: shared/**.py]
[jinja2: app_frontend/templates/**.html]
[jinja2: shared/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
encoding = utf-8
Summary
Sharing files between Flask DispatcherMiddleWare apps makes your work far more easy, no duplicates. I struggled a bit with the shared templates. You really must take some time to understand this. In the end I created a test and followed the flow in the Jinja code.
Sharing files appeared not that difficult, it starts with a solid directory structure. Again this proves the capabilities of Flask. No hacking necessary, it is all there already.
Links / credits
Application Dispatching
https://flask.palletsprojects.com/en/1.1.x/patterns/appdispatch/
flask blueprint template folder
https://stackoverflow.com/questions/7974771/flask-blueprint-template-folder
Modular Applications with Blueprints
https://flask.palletsprojects.com/en/1.1.x/blueprints/#modular-applications-with-blueprints
Two Flask apps, frontend and admin, on one domain using DispatcherMiddleware
http://127.0.0.1:8000/en/blog/two-flask-apps-frontend-and-admin-on-one-domain-using-dispatchermiddleware
Read more
DispatcherMiddleWare Flask
Recent
- Hiding database UUID primary keys of your web application
- Don't Repeat Yourself (DRY) with Jinja2
- SQLAlchemy, PostgreSQL, maximum number of rows per user
- Show the values in SQLAlchemy dynamic filters
- Secure data transfer with Public Key encryption and pyNaCl
- rqlite: a high-availability and distributed SQLite alternative
Most viewed
- Using Python's pyOpenSSL to verify SSL certificates downloaded from a host
- Using UUIDs instead of Integer Autoincrement Primary Keys with SQLAlchemy and MariaDb
- Connect to a service on a Docker host from a Docker container
- Using PyInstaller and Cython to create a Python executable
- SQLAlchemy: Using Cascade Deletes to delete related objects
- Flask RESTful API request parameter validation with Marshmallow schemas