- 28
- 08月
My last post about creating websites with Flask covered the steps to create a simple application. What happens when it grows bigger?
In this post I will take as example a common use case for a web app:
- a public section (homepage, tour, signup, login)
- a member only section (the app, user settings)
- an api
Each member will have its own subdomain (ie: if my username is maximebf, I get the maximebf.example.com subdomain). This app will need input validation and background processing (eg: resizing the user's profile picture).
I'll assume the same file organization as I described in my previous post.
Contents:
[TOC]
Getting modular with Blueprints
Flask provides a feature called Blueprints which let your organize your app as modules. As always, the documentation about Blueprints is pretty comprehensive.
I like to create a modules
folder in my application directory where all
my module files will be stored. I create one python file per module.
example/
modules/
__init__.py
public.py
member.py
api.py
To create a module, you initializes a Blueprint
object which acts in the
same way as the Flask
object. Most importantly you can register handlers
using the same route()
decorator.
In the example/modules/public.py
file:
from flask import Blueprint
blueprint = Blueprint('public', __name__)
@blueprint.route('/')
def home():
return render_template('public/home.html')
Each blueprint can have its own templates folder. However, I don't like
to do it that way and prefer to keep a single templates folder. I find it
easier to have all my template files in the same directory. Thus, the
public/home.html
file would be located in example/templates/
.
I use the same logic for static files. Anyway, as I use webassets to manage them, using the static file feature from blueprints wouldn't be practical.
Don't forget to add the module name when building urls using
url_for()
. For example, the name of the url for our home()
function
would be public.home
.
Blueprints need to be registered against a Flask
object. The great thing
about this is that you can specify at which endpoint using url_prefix
and subdomain
. In the example/init.py
file:
from modules import public, member, api
app.register_blueprint(public)
app.register_blueprint(member, subdomain='<subdomain>')
app.register_blueprint(api, subdomain='api')
Note that there shouldn't be any handlers left in the __init__.py
file.
I think once you start using modules, all handlers should be located in
modules.
Using this scheme, the public module is accessible from example.com/
,
the member module from *.example.com/
and the api module from
api.example.com/
.
Wildcard subdomains
As you may have noted, when I registered the member module against the
Flask
object, I used a dynamic parameter for the subdomain
(the same
way as with URLs). We can use this parameter to fetch the associated user
using a combination of url processors and before request callbacks.
Here is a handy function to add subdomain support for any Flask
or
Blueprint
object.
def add_subdomain_to_global(endpoint, values):
g.subdomain = values.pop('subdomain', None)
def add_subdomain_to_url_params(endpoint, values):
if not 'subdomain' in values:
values['subdomain'] = g.subdomain
def add_subdomain_support(app):
app.url_value_preprocessor(add_subdomain_to_global)
app.url_defaults(add_subdomain_to_url_params)
You can then use a before_request
callback to process the subdomain:
add_subdomain_support(blueprint)
@blueprint.before_request
def add_user_to_global():
g.user = None
if g.subdomain:
g.user = User.query.filter_by(username=g.subdomain).first_or_404()
Handling html forms
WTForms is one of the best Python solution to manage html forms. The Flask documentation has a great section about integrating and using WTForms with your app. I like to define my forms at the top of the module files.
If you are using the Bootstrap CSS framework, here is a Jinja2 macro to generate Bootstrap compatible form fields:
{% macro form_field(field) -%}
{% set with_label = kwargs.pop('with_label', False) %}
{% set placeholder = '' %}
{% if not with_label %}
{% set placeholder = field.label.text %}
{% endif %}
<div class="control-group {% if field.errors %}error{% endif %}">
{% if with_label %}
<label for="{{ field.id }}" class="control-label">
{{ field.label.text }}{% if field.flags.required %} *{% endif %}:
</label>
{% endif %}
<div class="controls">
{% set class_ = kwargs.pop('class_', '') %}
{% if field.flags.required %}
{% set class_ = class_ + ' required' %}
{% endif %}
{% if field.type == 'BooleanField' %}
<label class="checkbox">
{{ field(class_=class_, **kwargs) }}
{{ field.label.text|safe }}
</label>
{% else %}
{% if field.type in ('TextField', 'TextAreaField', 'PasswordField') %}
{% set class_ = class_ + ' input-xlarge' %}
{% elif field.type == 'FileField' %}
{% set class_ = class_ + ' input-file' %}
{% endif %}
{{ field(class_=class_, placeholder=placeholder, **kwargs) }}
{% endif %}
{% if field.errors %}
<span class="error help-inline">{{ field.errors|join(', ') }}</span>
{% endif %}
{% if field.description %}
<p class="help-block">{{ field.description|safe }}</p>
{% endif %}
</div>
</div>
{%- endmacro %}
I like to have a helpers.html
file in my templates/
folder with some
useful macros (like the one above). To import the macros use:
{% from "helpers.html" import form_field %}
Background processing with Celery
Any task which needs a bit of processing should be queued and processed as a background job. Celery is a great Python library which makes launching background tasks really easy.
I create a tasks.py
file in the application directory where Celery will
be initialized and all my delayed functions will be located.
from example import app
from celery import Celery
celery = Celery('example.tasks')
celery.conf.update(app.config)
@celery.task(ignore_result=True)
def resize_uploaded_image(filename, w, h):
# ...
I find the easiest way to get Celery running is using Redis as the
backend. In the settings.py
file:
class Config(object):
# ...
BROKER_URL = 'redis://localhost:6379/0'
Launching a background job is done as described in the celery doc:
from example.tasks import resize_uploaded_image
# ...
@blueprint.route('/upload', methods=['POST'])
def upload_image():
# ...
resize_uploaded_image.delay(filename, 100, 100)
Finally, you'll need to start the worker using the celery
CLI utility:
celery worker -A example.tasks -l info
This is where supervisor comes handy (see previous post) as we can add a new entry to automatically start the celery process:
[program:example_worker]
directory=/path/to/my/app
command=celery worker -A example.tasks -l info
autostart=true
autorestart=true
Origin: http://maximebf.com/blog/2012/11/getting-bigger-with-flask/