Flask para desarrolladores de Django

@vero4ka_ru

Django Bogotá Meetup

Para empezar

«¡Vámonos!»

ПОЕХАЛИ!

*

*

¡Poyejali! (en ruso: Поехали!; se traduce como «¡Vámonos!») fue la frase que dijo Gagarin en el momento del despegue de su nave, Vostok 1

Aplicación básica

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run()
$ pip install Flask
app.py:
$ python app.py 
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

vista

URL

 

manage.py

 

from flask import Flask

def create_app():
    app = Flask(__name__, 
        static_url_path='/static')
    app.config.from_object('conf.config')
    return app
$ pip install Flask-Script
manage.py:
$ ./manage.py runserver -h HOST -p PORT
$ ./manage.py shell
In [1]:
run.py:
from flask.ext.script import Manager
from run import create_app

app = create_app()
manager = Manager(app)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == '__main__':
    manager.run()

Configuración

import sys

DEBUG = False

try:
    if 'test' in sys.argv:
        from test_config import *
    else:
        from local_config import *

except ImportError, e:
    pass
config.py:
├── conf
│   ├── config.py
│   ├── __init__.py
│   └── local_config.py
├── manage.py
└── run.py

Configuration Handling: flask.pocoo.org/docs/0.11/config/

app.config.from_object('conf.config')
run.py:

cargar la configuración

desde el objeto

Blueprints

Blueprints

├── conf
│   ├── config.py
│   ├── __init__.py
│   └── local_config.py
├── app1
│   ├── templates
│   └── views.py
├── app2
│   ├── templates
│   └── views.py
├── manage.py
└── run.py
├── conf
│   ├── config.py
│   ├── __init__.py
│   └── local_config.py
├── manage.py
└── run.py

Blueprints

from flask import Blueprint

users = Blueprint(
    'users', 
    __name__, 
    template_folder='templates')

@users.route("/")
def hello():
    return "Hello Worlds!"
Crear un blueprint.
users/views.py:
├── conf
│   ├── config.py
│   ├── __init__.py
│   └── local_config.py
├── users
│   ├── templates
│   └── views.py
├── manage.py
└── run.py
def create_app():
    app = Flask(__name__, static_url_path='/static')
    # ...

    from users.views import users
    app.register_blueprint(users, url_prefix='/users')
Registrar el blueprint.
run.py:

http://localhost:5000/users/

URLs

@users.route('/user/<name>')
def user_profile(name):
    return '<h1>Hello, {}!</h1>'.format(name)

def users_list():
    pass
users.add_url_rule('/', 'users_list', users_list)

@users.route('/user/<int:pk>')
def user_detail(pk):
    return '<h1>Hello, user #{}!</h1>'.format(pk)

@users.route('/user/<int:pk>/create', methods=('POST',))
def user_create(pk):
    pass
Django:
reverse('user_detail', args=(user_pk,))
Flask:
url_for('users.user_detail', pk=user_pk)

Para los archivos estáticos:

Nombre de la vista

Métodos permitidos

url_for('static', filename='images/profile.png')

Nombre del blueprint

Vistas

from flask import request

@users.route('/user/<int:pk>')
def user_detail(pk):
    print request
    print request.method
    print request.args
    print request.headers
    return '<h1>Hello, user #{}!</h1>'.format(pk)

http://localhost:5000/users/user/1?q=foo

GET

ImmutableMultiDict([('q', u'foo')])

Request
@users.route('/user/<int:pk>')
@login_required
def user_detail(pk):
    return '<h1>Hello, user #{}!</h1>'.format(pk)
Decoradores

Plantillas

from flask import render_template

@users.route('/user/<int:pk>')
def user_detail(pk):
    return render_template('users/detail.html', user_pk=pk)
<html>
  <body>
    <h1>Hello, user #{{ user_id }}!</h1>
  </body>
</html>

Variable de contexto

users/views.py:
users/templates/users/detail.html:

Jinja2

Jinja2

Variables:

{{ foo.bar }}
{{ foo['bar'] }}


Condicionales:

{% if user.address %}
    <p>{{ user.address }}</p>
{% endif %}

Bucles:

{% for user in users %}
  <li>{{ user.name }}</li>
{% else %}
  <li>No hay usuarios</li>
{% endfor %}

Bloques:

{% block title %}Usuarios{% endblock %}
Extender una plantilla base:

{% extends "layouts/main.html" %}

Comentario:

{# Texto #}

Incluir otra plantilla:

{% with rating=user.rating %}
    {% include 'includes/_rating.html' %}
{% endwith %}

Asignar valor a una variable local:

{% set permissions = user.get_permission() %}
{{ permissions }}

Filtros:

{{ user.address|default('N/A') }}

Procesadores de contexto

Variables estándares de contexto:

config  - Objeto de configuración (flask.config)
        {{ config.DEBUG }}
request - Objeto de la petición actual (flask.request)
        {{ request.path }}
session - Objeto de sesión (flask.session)
g       - Variables globales

Pasar variables a todas las plantillas:

def create_app():
    app = Flask(__name__, static_url_path='/static')
    app.config.from_object('conf.config')

    @app.context_processor
    def constants_processor():
        return {
            'say_hello': 'Hola', 
        }

    return app
run.py:

Modelos

SQLAlchemy

$ pip install SQLAlchemy
$ pip install Flask-SQLAlchemy
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

def create_app():
    app = Flask(__name__, static_url_path='/static')
    app.config.from_object('conf.config')

    db.init_app(app)
    return app
run.py:
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/test.db'
config.py:

Modelos

from run import db

class User(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True)
    email = db.Column(db.String(120), unique=True)
    city_id = db.Column(db.ForeignKey(u'cities.id'), nullable=False, index=True)
    created_at = db.Column(db.DateTime, nullable=False, default=db.func.now())
    updated_at = db.Column(db.DateTime, nullable=False, default=db.func.now(),
                        onupdate=db.func.now())

    def __repr__(self):
        return '<Usuario %r>' % self.username


class City(db.Model):
    __tablename__ = 'cities'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255))

    users = db.relationship('User', backref='city', lazy='dynamic')
users/models.py:

CRUD

Select, Insert, Delete: flask-sqlalchemy.pocoo.org/2.1/queries/

Crear
user = User(email='john@example.com', username='')
db.session.add(user)
db.session.commit()

Obtener todos usuarios
users = User.query.all()  # [<Usuario john'>, <Usuario u'admin'>]

Obtener el primer usuario
john = User.query.first()  # <Usuario 'john'>

Filtrar usuarios y ordenar
User.query.filter_by(username='john').all()
User.query.filter(
    User.created_at >= (datetime.datetime.utcnow() - datetime.timedelta(days=3)
).order_by(User.created_at.desc()).limit(10).all()

Borrar
db.session.delete(user)
db.session.commit()

Formularios

Formularios

import wtforms
from flask_wtf import Form

class UserForm(Form):
    email = wtforms.StringField(
        validators=[
            wtforms.validators.Email(),
            wtforms.validators.DataRequired(),
        ],
    )
    username = wtforms.StringField(
        validators=[wtforms.validators.DataRequired()],
    )
    submit = wtforms.SubmitField('Save')
users/forms.py:
$ pip install Flask-WTF
$ pip install Flask-Bootstrap
StringField
TextAreaField
BooleanField
DateField
DateTimeField
DecimalField
FloatField
IntegerField
SelectField
SelectMultipleField
FileField
HiddenField
PasswordField
SubmitField

Campos:

Formularios (plantilla)

{% extends "layouts/main_layout.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block content %}
  {{ wtf.quick_form(form) }}
{% endblock %}
users/templates/users/form.html:
{{ wtf.form_field(form.email) }}
{{ wtf.form_field(form.submit) }}

Referirse a un campo específico:

~ crispy-forms en Django

Formularios (vista)

@users.route('/update/<int:pk>/', methods=('GET', 'POST'))
def user_update(pk):
    user = User.query.filter_by(id=pk).first_or_404()

    form = UserForm(user, request.form)

    if request.method == 'POST' and form.validate():
        form.populate_obj(user)

        db.session.add(user)
        db.session.commit()

        flash('Usuario fue editado exitosamente', 'success')
        return redirect(url_for('users.user_detail', pk=user.id))

    return render_template(
        'users/form.html',
        form=form,
    )
users/views.py:

Procesar el formulario en la vista:

Obtener formulario
del request

form.validate()  # True/False

Validación:

Autenticación

from flask_login import LoginManager


login_manager = LoginManager()
login_manager.login_view = 'users.login'

def create_app():
    app = Flask(__name__, static_url_path='/static')
    app.config.from_object('conf.config')
    app.permanent_session_lifetime = datetime.timedelta(days=365)

    login_manager.init_app(app)
users/run.py:
$ pip install Flask-Login
$ pip install Flask-OAuth

Autenticación

from flask_login import UserMixin
from run import db

class User(db.Model, UserMixin):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    # ...
users/models.py:
@main.route('/login')
def login():
    callback = url_for('main.authorized', _external=True)
    return google.authorize(callback=callback)
users/views.py:
from flask_login import current_user

Múltiples idiomas

# Extraer los textos para traducción 
# (se corre sólo una vez al principio)
$ pybabel extract -F babel.cfg -o messages.pot .


# Generar un catálogo para español
$ pybabel init -i messages.pot -d translations -l es

# Se crea el directorio translations/es
# Por dentro hay otro directorio llamado LC_MESSAGES que tiene  
# un archivo messages.po. 

# Después de traducir los textos y guardarlos en messages.po,
# compilamos el archivo y publicamos los textos:

$ pybabel compile -d translations

# Para actualizar las traducciones a diario:

$ pybabel extract -F babel.cfg -o messages.pot .
$ pybabel update -i messages.pot -d translations
$ pip install Flask-Babel

Múltiples idiomas

En una vista:

from flask_babel import gettext as _

_('Invalid authentication token')

En una plantilla:

<button class="btn btn-default" title="{{ _('Help') }}">
    <i class="fa fa-question"></i>
</button>

Mensajes

En Django:

from django.contrib import messages

messages.add_message(request, messages.INFO, 'Hello world.')

En Flask:

from flask import flash

flash('Hello world.', 'success')
{% for category, message in get_flashed_messages(with_categories=true) %}
<div class="alert alert-{{ category|replace('message', 'info') }}">
  <button type="button" class="close" data-dismiss="alert">×</button>
  {{ message }}
</div>
{% endfor %}

Cache

from flask_cache import Cache


cache = Cache()

def create_app():
    app = Flask(__name__, static_url_path='/static')
    app.config.from_object('conf.config')

    cache.init_app(app)
users/run.py:
$ pip install Flask-Cache

Cache

En una vista:

from run import cache

@users.route('/list')
@cache.cached(timeout=60*60, key_prefix='user_list')
def user_list():
    pass

En una plantilla:

{% cache 60*60, 'dashboard_menu_user' + current_user.id|string %}
  {% include 'layouts/_menu.html' %}
{% endcache %}

pip freeze

# Framework
Flask==0.11

# Libs
celery==3.1.20
coverage==4.0.3
factory-boy==2.6.1
SQLAlchemy==1.0.12
SQLAlchemy-Utils==0.31.6

# Flask libs
Flask-And-Redis==0.6
Flask-Babel==0.9               # Múltiples idiomas y zonas horarias
Flask-Bootstrap==3.3.5.7       # ~ django-crispy-forms para Bootstrap
Flask-Cache==0.13.1
Flask-DebugToolbar==0.10.0     # En Django: django-debug-toolbar
Flask-fillin==0.2              # Diligenciar formularios en pruebas
Flask-Login==0.3.2
Flask-Migrate==1.8.0
Flask-Moment==0.5.1            # Integración con moment.js
Flask-OAuth==0.12              # ~ python-social-auth
Flask-Script==2.0.5            # manage.py, shell
Flask-SQLAlchemy==2.1
Flask-WTF==0.12                # Formularios con protección CSRF
WTForms-Alchemy==0.15.0        # Para crear formularios basados en modelos
WTForms-Components==0.10.0     # Campos adicionales para los formularios de Flask

Gracias por su atención