Home > flask, python > Flask (parte 8): registrare un nuovo user

Flask (parte 8): registrare un nuovo user

1 Settembre 2015

Nella parte precedente abbiamo accennato alla authentication ed ai
forms di competenza, considerando utenti inseriti a database, tramite shell.
Vediamo come fare quando un nuovo utente decide di registrarsi in autonomia.
Il Blueprint per l’authentication è stato creato ed attivato in precedenza,
quello che dobbiamo fare è creare un form per la registrazione.
Editiamo quindi il file app/auth/forms.py
ed aggiungiamo il suddetto form:

from flask.ext.wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo
from wtforms import ValidationError
from ..models import User


class LoginForm(Form):
    ...

class RegistrationForm(Form):
    email = StringField('Email', 
                        validators=[DataRequired(), Length(1, 64), Email()])
    username = StringField('Username', 
                           validators=[DataRequired(), Length(1, 64),
                                    Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
                                          'Usernames must have only letters, '
                                          'numbers, dots or underscores')])
    password = PasswordField('Password', 
                             validators=[DataRequired(), EqualTo('password2',
                                            message='Passwords must match.')])
    password2 = PasswordField('Confirm password', validators=[DataRequired()])
    submit = SubmitField('Register')

    def validate_email(self, field):
        if User.query.filter_by(email=field.data).first():
            raise ValidationError('Email already registered.')

    def validate_username(self, field):
        if User.query.filter_by(username=field.data).first():
            raise ValidationError('Username already in use.')

Il validator usato nel campo “username”, si assicura che vengano utilizzati solo
lettere, numeri, punti e underscores altrimenti ritorna il messaggio di errore,
passato come argomento a Regexp.
Nel campo password abbiamo un altro validator (EqualTo) che si assicura che le
password immesse (password e conferma della stessa) siano uguali.
I due metodi validate_email() e validate_username() si assicurano che i valori
immessi nei campi non siano già presenti a database, sollevando in caso affermativo,
l’eccezione di competenza.

La template che rappresenterà il form di registrazione sarà /templates/auth/register.html:

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

{% block title %} Registration {% endblock %}

{% block page_content %}
    <div class="page-header">
        <h1>New User Registration</h1>
    </div>

    <div class="col-md-4">
        {{ wtf.quick_form(form) }}
    </div>
{% endblock %}

Solitamente la pagina di registrazione, va linkata da quella di Login, in modo che un
nuovo utente, trovandosi di fornte il form di accesso, possa facilmente raggiungere
quello di registrazione.

Apriamo quindi la template /templates/auth/login.html e aggiungiamo il link:

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

{% block title %}Login{% endblock %}
{% block page_content %}
    <div class="page-header">
        <h1>Login</h1>
    </div>

    <div class="col-md-4">
        {{ wtf.quick_form(form) }}
    </div>

<p>
New user?
    <a href="{{ url_for('auth.register') }}">
        Click here to register
    </a>
</p>
{% endblock %}

La funzione (view) che gestisce l’operazione di registrazione va inserita in
app/auth/views.py:

from flask import render_template, redirect, request, url_for, flash
from flask.ext.login import login_user
from flask.ext.login import logout_user, login_required
from . import auth
from ..models import User
from .forms import LoginForm, RegistrationForm
from .. import db

...

@auth.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(email=form.email.data,
                    username=form.username.data,
                    password=form.password.data)
        db.session.add(user)
        flash('You can now login.')
        return redirect(url_for('auth.login'))
    return render_template('auth/register.html', form=form)

molto semplice in fase di richiesta visualizzazione form di registrazione (GET),
viene visualizzata la template register.html, in fase di immissione dati (POST),
viene eseguita la parte di codice dentro l’if e, registrato il nuovo user, si
viene reindirizzati alla pagina di login.

Conferma di registrazione – Token

Con la gestione delle registrazioni appena accennata, potremmo però registrare decine
di nuovi user senza problemi. Dobbiamo quindi verificare la mail immessa, mandando ad essa,
una email di conferma, in modo che il nuovo utente possa confermare la propria registrazione.
Per fare questo e per motivi di sicurezza, utilizziamo i token.
Per gestirli si utilizza il pacchetto itsdangerous.

come funziona?

brevemente…apriamo una shell:

python manage.py shell
>>> from manage import app
>>> from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
>>> s = Serializer(app.config['SECRET_KEY'], expires_in = 3600)
>>> token = s.dumps({ 'confirm': 10 })
>>> token
'eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ0MDQxMzkyNCwiaWF0IjoxNDQwNDEwMzI0fQ.eyJjb25maXJtIjoxMH0.Bzoa8GFakRN3WkfvSt5Vkk9RjPC2ffa6Il1aEhFa8tc'
>>> data = s.loads(token)
>>> data
{u'confirm': 10}

L’oggetto Serialiezer fornisce due metodi:
dumps() crea una signature crittografata ed expires_in indicano i secondi di validità della signature.
load() decodifica la stessa.

Cominciamo a gestire la cosa a livello di user e modifichiamo il file app/models.py:

...
from flask import current_app
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer

...

class User(UserMixin, db.Model):
    ...
    confirmed = db.Column(db.Boolean, default=False)

    ...
    def generate_confirmation_token(self, expiration=3600):
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps({'confirm': self.id})

    def confirm(self, token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token)
        except:
            return False
        if data.get('confirm') != self.id:
            return False
        self.confirmed = True
        db.session.add(self)
        return True

Siccome abbiamo aggiunto il campo “confirmed” nella tabella “users”, dobbiamo
allineare il database con una migration:

(venv) >python manage.py db migrate -m "confirmed field added to User class"
INFO  [alembic.migration] Context impl SQLiteImpl.
INFO  [alembic.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added column 'users.confirmed'
Generating ...\migrations\versions\59952a5eeded_confirmed_field_added_to_user_class.py ... done

(venv) >python manage.py db upgrade
INFO  [alembic.migration] Context impl SQLiteImpl.
INFO  [alembic.migration] Will assume non-transactional DDL.
INFO  [alembic.migration] Running upgrade ea1a33541f6 -> 59952a5eeded, confirmed field added to User class

Ora modifichiamo la view che gestisce la registrazione (app/auth/views.py), in modo da mandare la mail di
conferma, prima che l’utente venga redirezionato.

@auth.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(email=form.email.data,
                    username=form.username.data,
                    password=form.password.data)
        db.session.add(user)
        db.session.commit()
        token = user.generate_confirmation_token()
        send_email(user.email, 'Confirm Your Account', 'auth/email/confirm',
                   user=user, token=token)
        flash('A confirmation email has been sent to you by email.')
        return redirect(url_for('main.index'))
    return render_template('auth/register.html', form=form)

Nota:
nel caso nelle impostazioni del database avessimo settato il commit automatico, è necessario
fare il commit manuale, una volta creato il nuovo user, in modo da avere disponibile subito
l’id, necessario nella conferma del token.
La template che si occupa della mail risiede in app/auth/templates/auth/email/confirm.txt

Dear {{ user.username }},
Welcome to Fantamanager!

To confirm your account please click on the following link:
{{ url_for('auth.confirm', token=token, _external=True) }}

Sincerely,
The Fantamanager Admin
Note: replies to this email address are not monitored.

mentre il file html sarà:

<p>Dear {{ user.username }},</p>
<p>Welcome to <b>Fantamanager</b>!</p>

<p>To confirm your account please <a href="{{ url_for('auth.confirm', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.confirm', token=token, _external=True) }}</p>
<p>Sincerely,</p>
<p>The Fantamanager Team</p>

<p><small>Note: replies to this email address are not monitored.</small></p>

ora vediamo la view che si occupa della conferma (app/auth/views.py):

from flask.ext.login import current_user

...

@auth.route('/confirm/<token>')
@login_required
def confirm(token):
    if current_user.confirmed:
        return redirect(url_for('main.index'))
    if current_user.confirm(token):
        flash('You have confirmed your account. Thanks!')
    else:
        flash('The confirmation link is invalid or has expired.')
        return redirect(url_for('main.index'))

articoli successivi:
Flask (tricks): download dati

articoli precedenti:
Flask (parte 1): virtualenv
Flask (parte 2): struttura progetto complesso
Flask (parte 3): database
Flask (parte 4): views e templates
Flask (parte 5): password security
Flask (parte 6): authentication
Flask (parte 7): mail

link utili:
“Flask Web Development” di Miguel Grinberg, fondamentale per calarsi nel mondo di Flask.
Altri link fondamentali:
Flask sito ufficiale
il blog dell’autore (Flask mega tutorial)

Categorie:flask, python Tag: ,
I commenti sono chiusi.