📜 ⬆️ ⬇️

Designing simple applications in Flask

This article, hosted in the Flask repository on GitHub, is the fruit of collective creativity by non-indifferent programmers, and its original author is Brice Leroy . It is quite useful for beginners material on Flask. For me personally, he became the answer to many simple questions, the main of which was “how to structure the project”.

For at least some experienced programmers, it is unlikely to be useful, many may not agree with the principles described, but for those who are at an early stage of training, it may become the impetus for development, as it became for me. That is why I made a Russian translation - this article has a very low threshold of entry and it is worth making it even lower.

The described example was tested in Python 3.5, Flask 0.10, Flask-SQLAlchemy 2.1, Flask-WTF 0.9.

Designing simple applications in Flask


This document is not included in the official Flask documentation. It is a compilation of advice received from various unofficial sources and has never been subjected to any verification. The described techniques can be very useful, but at the same time, and quite dangerous. Please do not make any changes to the original document posted on Github, as it is referenced by many of the answers to StackOverflow. You can make any corrections and notes to it, but use a personal website or blog to post.
')
This article is an attempt to describe the structure of a project that uses Flask and the basic modules SQLAlchemy and WTForms .

Installation


Flask


Installation Instructions Flask.

I recommend using virtualenv - this system is very simple and allows you to host multiple virtual environments on one system and does not require root privileges, since all libraries are installed locally.

Flask-SQLAlchemy


SQLAlchemy provides a simple and powerful interface for the interaction of your objects and any type of relational database. To install Flask-SQLAlchemy in your virtual environment, use pip:

pip install flask-sqlalchemy 

A more complete description of the Flask-SQLAlchemy package.

Flask-wtf


WTForms makes it easy to get data from the user.

 pip install Flask-WTF 

A more complete description of the Flask-WTF package.

Introduction


So, the necessary libraries are prepared. This is how the basic structure of your project should look like:

 /app/users/__init__.py /app/users/views.py /app/users/forms.py /app/users/constants.py /app/users/models.py /app/users/decorators.py 

For each module (application element) the following structure is created:

 /app/templates/404.html /app/templates/base.html /app/templates/users/login.html /app/templates/users/register.html ... 

View templates (jinja) are stored in the templates directory and the subdirectory of modules:

 /app/static/js/main.js /app/static/css/reset.css /app/static/img/header.png 

For processing unchangeable files, you must use a separate web server, but at design time you can assign this work to Flask. It automatically issues such files from the static directory, and you can use the information in this article to configure the use of another directory.

For the application in question one module will be created: users. It will provide management of registration and user login, view data of your profile.

Configuration


/run.py is used to start the web server:

  from app import app app.run(debug=True) 

/shell.py will give access to the console with the ability to execute commands. Perhaps not as convenient as debugging via pdb , but quite useful (at least when initializing the database):

  #!/usr/bin/env python import os import readline from pprint import pprint from flask import * from app import * os.environ['PYTHONINSPECT'] = 'True' 

Translator's Note:
In case you are working in Windows OS (no need to throw bricks!), The readline library is not available. In this case, you need to install the pyreadline library into your virtual or real python environment and wrap the import into the construction of the form:

 try: import readline except: import pyreadline 

In principle, you can do without this library altogether; it simply simplifies the interaction with the console by adding some bash-like elements to it.

/config.py stores the entire configuration of the application. In this example, SQLite is used as a database, since it is very convenient for development. Most likely, the /config.py file should not be included in the repository, since it will be different on test and industrial systems.

  import os _basedir = os.path.abspath(os.path.dirname(__file__)) DEBUG = False ADMINS = frozenset(['youremail@yourdomain.com']) SECRET_KEY = 'This string will be replaced with a proper key in production.' SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(_basedir, 'app.db') DATABASE_CONNECT_OPTIONS = {} THREADS_PER_PAGE = 8 WTF_CSRF_ENABLED = True WTF_CSRF_SECRET_KEY = "somethingimpossibletoguess" RECAPTCHA_USE_SSL = False RECAPTCHA_PUBLIC_KEY = '6LeYIbsSAAAAACRPIllxA7wvXjIE411PfdB2gt2J' RECAPTCHA_PRIVATE_KEY = '6LeYIbsSAAAAAJezaIq3Ft_hSTo0YtyeFG-JgRtu' RECAPTCHA_OPTIONS = {'theme': 'white'} 


Module


We configure the users module in the following order: we define models associated with constant models, then a form, and finally, a view and templates.

Model


/app/users/models.py :

  from app import db from app.users import constants as USER class User(db.Model): __tablename__ = 'users_user' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), unique=True) email = db.Column(db.String(120), unique=True) password = db.Column(db.String(120)) role = db.Column(db.SmallInteger, default=USER.USER) status = db.Column(db.SmallInteger, default=USER.NEW) def __init__(self, name=None, email=None, password=None): self.name = name self.email = email self.password = password def getStatus(self): return USER.STATUS[self.status] def getRole(self): return USER.ROLE[self.role] def __repr__(self): return '<User %r>' % (self.name) 

And its constants in the /app/users/constants.py file:

  # User role ADMIN = 0 STAFF = 1 USER = 2 ROLE = { ADMIN: 'admin', STAFF: 'staff', USER: 'user', } # user status INACTIVE = 0 NEW = 1 ACTIVE = 2 STATUS = { INACTIVE: 'inactive', NEW: 'new', ACTIVE: 'active', } 

Speaking of constants: I like it when constants are stored in a separate file inside the module. Constants are most likely to be used in models, forms, and views, so that you get conveniently organized data that is easy to find. In addition, importing constants under the name of a module in upper case (for example, USERS for users.constants ) will help to avoid name conflicts.

The form


When the model of the desired object is created, it is necessary to construct a form for working with it.

The registration form will ask for the username, email address and password, validators will be used to verify the user input, and the Recaptcha field will protect against registering bots. In case you need to implement a user agreement, the BooleanField field with the name accept_tos is also added. This field is marked as required , that is, the user will be required to mark the checkbox generated by the form. The login form is supplied with email and password fields with similar validators.

Forms are described in the /app/users/forms.py file:

  from flask.ext.wtf import Form, RecaptchaField from wtforms import TextField, PasswordField, BooleanField from wtforms.validators import Required, EqualTo, Email class LoginForm(Form): email = TextField('Email address', [Required(), Email()]) password = PasswordField('Password', [Required()]) class RegisterForm(Form): name = TextField('NickName', [Required()]) email = TextField('Email address', [Required(), Email()]) password = PasswordField('Password', [Required()]) confirm = PasswordField('Repeat Password', [ Required(), EqualTo('password', message='Passwords must match') ]) accept_tos = BooleanField('I accept the TOS', [Required()]) recaptcha = RecaptchaField() 

The first parameter for each field is its label; for example, the name field in the form is set to the NickName label. For the password input fields, the EqualTo validator is used , which compares the data in the two fields.

More information about the capabilities of WTForms can be found at this link .

Representation


In the view, Blueprint is declared - the object of the module's schema, the properties of which indicate url_prefix , which will be inserted at the beginning of any URL specified in the route . The view also uses the form method form.validate_on_submit , which returns the truth for the HTTP POST method and the valid form. After successful login, the user is redirected to the profile page ( / users / me ). To prevent unauthorized users from accessing, a special decorator is created in the /app/users/decorators.py file:

  from functools import wraps from flask import g, flash, redirect, url_for, request def requires_login(f): @wraps(f) def decorated_function(*args, **kwargs): if g.user is None: flash(u'You need to be signed in for this page.') return redirect(url_for('users.login', next=request.path)) return f(*args, **kwargs) return decorated_function 

This decorator checks for data in the g.user variable. In case the variable is not set, the user is not authenticated, then an informational message is set and the login is redirected to the login view. The data in the g.user variable is placed in the before_request function. When you get a large amount of data from a user profile (historical data, friends, messages, actions), a serious slowdown in accessing the database is possible, so caching user data can solve this problem (but only while you modify objects centrally and clear the cache at each update). Below is the presentation code /app/users/views.py :

  from flask import Blueprint, request, render_template, flash, g, session, redirect, url_for from werkzeug import check_password_hash, generate_password_hash from app import db from app.users.forms import RegisterForm, LoginForm from app.users.models import User from app.users.decorators import requires_login mod = Blueprint('users', __name__, url_prefix='/users') @mod.route('/me/') @requires_login def home(): return render_template("users/profile.html", user=g.user) @mod.before_request def before_request(): """ pull user's profile from the database before every request are treated """ g.user = None if 'user_id' in session: g.user = User.query.get(session['user_id']) @mod.route('/login/', methods=['GET', 'POST']) def login(): """ Login form """ form = LoginForm(request.form) # make sure data are valid, but doesn't validate password is right if form.validate_on_submit(): user = User.query.filter_by(email=form.email.data).first() # we use werzeug to validate user's password if user and check_password_hash(user.password, form.password.data): # the session can't be modified as it's signed, # it's a safe place to store the user id session['user_id'] = user.id flash('Welcome %s' % user.name) return redirect(url_for('users.home')) flash('Wrong email or password', 'error-message') return render_template("users/login.html", form=form) @mod.route('/register/', methods=['GET', 'POST']) def register(): """ Registration Form """ form = RegisterForm(request.form) if form.validate_on_submit(): # create an user instance not yet stored in the database user = User(name=form.name.data, email=form.email.data, \ password=generate_password_hash(form.password.data)) # Insert the record in our database and commit it db.session.add(user) db.session.commit() # Log the user in, as he now has an id session['user_id'] = user.id # flash will display a message to the user flash('Thanks for registering') # redirect user to the 'home' method of the user module. return redirect(url_for('users.home')) return render_template("users/register.html", form=form) 

Template


The Jinja template engine is built into Flask . One of its advantages is the possibility of inheritance and built-in logic (dependencies, cycles, contextual changes). Create a template /app/templates/base.html , from which other templates will be inherited. It is possible to set more than one inheritance (for example, inheritance from the twocolumn.html template, which in turn is inherited from main.html ). The basic template also simplifies the display of informational (flash) messages from the variable get_flashed_messages in each inheriting template.

Now there is no need to set the basic structure of the page and every change of base.html will affect the inheriting templates. It is recommended to name the templates according to their calling views, this is how the template /app/templates/users/register.html is named:

  <html> <head> <title>{% block title %}My Site{% endblock %}</title> {% block css %} <link rel="stylesheet" href="/static/css/reset-min.css" /> <link rel="stylesheet" href="/static/css/main.css" /> {% endblock %} {% block script %} <script src="/static/js/main.js" type="text/javascript"></script> {% endblock %} </head> <body> <div id="header">{% block header %}{% endblock %}</div> <div id="messages-wrap"> <div id="messages"> {% for category, msg in get_flashed_messages(with_categories=true) %} <p class="message flash-{{ category }}">{{ msg }}</p> {% endfor %} </div> </div> <div id="content">{% block content %}{% endblock %}</div> <div id="footer">{% block footer %}{% endblock %}</div> </body> </html> 

And the /app/templates/users/login.html template:

  {% extends "base.html" %} {% block content %} {% from "forms/macros.html" import render_field %} <form method="POST" action="." class="form"> {{ form.csrf_token }} {{ render_field(form.email, class="input text") }} {{ render_field(form.password, class="input text") }} <input type="submit" value="Login" class="button green"> </form> <a href="{{ url_for('users.register') }}">Register</a> {% endblock %} 

Created templates use macros to automate the creation of html fields. Since this macro will be used in various modules, it is placed in a separate file /app/templates/forms/macros.html :

  {% macro render_field(field) %} <div class="form_field"> {{ field.label(class="label") }} {% if field.errors %} {% set css_class = 'has_error ' + kwargs.pop('class', '') %} {{ field(class=css_class, **kwargs) }} <ul class="errors">{% for error in field.errors %}<li>{{ error|e }}</li>{% endfor %}</ul> {% else %} {{ field(**kwargs) }} {% endif %} </div> {% endmacro %} 

Finally, a primitive template created /app/templates/users/profile.html :

  {% extends "base.html" %} {% block content %} Hi {{ user.name }}! {% endblock %} 

Application Initialization


As it is easy to guess, the application is initialized in the /app/init.py file:

  import os import sys from flask import Flask, render_template from flask.ext.sqlalchemy import SQLAlchemy app = Flask(__name__) app.config.from_object('config') db = SQLAlchemy(app) ######################## # Configure Secret Key # ######################## def install_secret_key(app, filename='secret_key'): """Configure the SECRET_KEY from a file in the instance directory. If the file does not exist, print instructions to create it from a shell with a random key, then exit. """ filename = os.path.join(app.instance_path, filename) try: app.config['SECRET_KEY'] = open(filename, 'rb').read() except IOError: print('Error: No secret key. Create it with:') full_path = os.path.dirname(filename) if not os.path.isdir(full_path): print('mkdir -p {filename}'.format(filename=full_path)) print('head -c 24 /dev/urandom > {filename}'.format(filename=filename)) sys.exit(1) if not app.config['DEBUG']: install_secret_key(app) @app.errorhandler(404) def not_found(error): return render_template('404.html'), 404 from app.users.views import mod as usersModule app.register_blueprint(usersModule) # Later on you'll import the other blueprints the same way: #from app.comments.views import mod as commentsModule #from app.posts.views import mod as postsModule #app.register_blueprint(commentsModule) #app.register_blueprint(postsModule) 

The SQLAlchemy DB instance and the Users model are in two different files; you need to import both of them into the common namespace using the from app.users.views import mod as usersModule line . Otherwise, the db.create_all () command will not produce a result.

Activate the virtual environment virtualenv and initialize the database:

 user@Machine:~/Projects/dev$ . env/bin/activate (env)user@Machine:~/Projects/dev$ python shell.py >>> from app import db >>> db.create_all() >>> exit() 

Now you can run the python run.py command and get a message like this:

 (env)user@Machine:~/Projects/dev$ python run.py * Running on http://127.0.0.1:5000/ * Restarting with reloader 

When you open the browser address http : //127.0.0.1nu00/users/me/, you will be redirected to the login page and you will see a link to the registration page.

Source: https://habr.com/ru/post/275099/


All Articles