Archivio

Posts Tagged ‘python’

Flask (tricks): download dati

12 maggio 2016 Commenti chiusi

Per poter aver disponibile un dato al download, utilizziamo

make_response

Per prima cosa creiamo una view chiamata download

from flask import flash, request, make_response

#.....

@main.route('/download/<id>')
def download(id):
    team = Team.query.get(id)
    players = team.players.all()
    data = """Team: %s\n""" % team.name
    for player in players:
        data += "\nplayer: %s - [%s]" % (player.name, player.value)
    response = make_response(data)
    response.headers["Content-Disposition"] = "attachment; filename=lineup.log"
    return response

nella template aggiungiamo un link (o un pulsante nel form) che richiami
la view corrispondente (download):

<a href="{{ url_for('main.download', id=team.id) }}">Download team lineup</a>

quando si clicchera’ sul link (o pulsante) si aprira’ il menu dove sara’ possibile
salvare il file, salvarlo con nome, ecc.

articoli successivi:

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
Flask (parte 8): registrare un nuovo user

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: ,

Flask (parte 8): registrare un nuovo user

1 settembre 2015 Commenti chiusi

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: ,

Flask (parte 7): mail

26 agosto 2015 Commenti chiusi

Per notificare un evento ad uno User, utilizzeremo il pacchetto Flask-Mail,
che integra in maniera efficiente smtplib con flask.
Per installarlo:

(venv) >pip install flask-mail

flask-mail consente di connetterci ad un SMTP server in modo da passargli
le mail e far sì che vengano spedite.
Con la configurazione di default, Flask-Mail si connette tramite la porta
25 a localhost e spedisce mail senza bisogno di autenticazione.
Le chiavi che permettono di configurare Flask-Mail sono:

MAIL_SERVER : default “localhost”
MAIL_PORT : default 25
MAIL_USE_TLS : default False
MAIL_USE_SSL : default False
MAIL_DEBUG : default app.debug
MAIL_USERNAME : default None
MAIL_PASSWORD : default None
MAIL_DEFAULT_SENDER : default None
MAIL_MAX_EMAILS : default None
MAIL_SUPPRESS_SEND : default app.testing
MAIL_ASCII_ATTACHMENTS : default False

Nel caso specifico della nostra applicazione, dovremo inizializzare Flask-Mail
in app.__init__.py, creando una istanza di Mail, che gestirà le emails:

from flask.ext.mail import Mail

...
mail = Mail()

def create_app(config_name):
    ...
    mail.init_app(app)
    ...

I valori di configurazione sono già stati settati nel file config.py e
prevedono di spedire mail tramite account di posta gmail:

...

class DevelopmentConfig(Config):
    DEBUG = True
    MAIL_SERVER = 'smtp.googlemail.com'
    MAIL_PORT = 587
    MAIL_USE_TLS = True
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    ...

i valori “MAIL_USERNAME” e “MAIL_PASSWORD” vengono settati per motivi di privacy
nel seguente modo per linux:

(venv) $ export MAIL_USERNAME=<Gmail username>
(venv) $ export MAIL_PASSWORD=<Gmail password>

e nel seguente, per windows:

(venv) $ set MAIL_USERNAME=<Gmail username>
(venv) $ set MAIL_PASSWORD=<Gmail password>

In questo modo, le nostre credenziali non appariranno nel codice, ma verranno prelevate dal dizionario
os.environ direttamente:

    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
(venv) >set [email protected]
(venv) >set MAIL_PASSWORD=pippo

(venv) >python manage.py shell

>>> import os
>>> os.environ.get("MAIL_USERNAME")
'[email protected]'
>>> os.environ.get("MAIL_PASSWORD")
'pippo'

ovviamente username e pw vanno sostituiti con qualcosa di esistente…
vediamo come mandare una mail dalla shell:

>>> import os
>>> from flask import Flask
>>> from flask_mail import Message
>>> from flask_mail import Mail
>>> app = Flask("test")
>>> mail = Mail(app)
>>> sender = os.environ.get("MAIL_USERNAME")
>>> sender
'[email protected]'
>>> recipients = [sender] # mi auto spedisco una mail!
>>> msg = Message("Test", sender=sender, recipients=recipients)
>>> msg.recipients
['[email protected]']
>>> msg.add_recipient("[email protected]") # per aggiungere destinatari...
>>> msg.recipients
['[email protected]', '[email protected]']
>>> msg.body = "text body"
>>> msg.html = "<b>HTML</b> body"
>>> with app.app_context():
...     mail.send(msg)
...

nota: il messaggio può avere un text body e/o uno html, pertanto li abbiamo aggiunti
entrambi.
Questo modo di mandare mail, non è comodissimo pertanto automatizziamo l’operazione di
spedizione mail, creando una funzione nell’apposito file app/email.py:

from threading import Thread
from flask import current_app, render_template
from flask.ext.mail import Message
from . import mail


def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)


def send_email(to, subject, template, **kwargs):
    app = current_app._get_current_object()
    msg = Message(app.config['FANTAMANAGER_MAIL_SUBJECT_PREFIX'] + ' ' + subject,
                  sender=app.config['FANTAMANAGER_MAIL_SENDER'], recipients=[to])
    msg.body = render_template(template + '.txt', **kwargs)
    msg.html = render_template(template + '.html', **kwargs)
    thr = Thread(target=send_async_email, args=[app, msg])
    thr.start()
    return thr

La funzione mail.send() blocca l’applicazione durante l’operazione di spedizione.
Per questo l’operazione stessa viene delegata ad un thread che lavora in background,
evitando che il browser e tutta l’applicazione rimangano in attesa.

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>

articoli successivi:
Flask (parte 8): registrare un nuovo user

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

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: ,

Flask (parte 6): authentication

9 luglio 2015 Commenti chiusi

Nella parte precedente abbiamo accennato alla gestione delle password utente.
Ora vediamo come gestire l’autenticazione sul sito di uno User e la User-session.
Dobbiamo installare per prima cosa l’estensione Flask-Login:

(venv) >pip install flask-login

Poi, siccome l’inserimento delle proprie credenziali da parte dello User che si connette o
si vuole registrare, necessita di Forms, installiamo anche l’estensione WTF:

(venv) >pip install flask-wtf

Ricordiamoci, dopo un’installazione, di rinfrescare i requirements con il comando:

(venv) >pip freeze > requirements.txt

Siccome abbiamo già accennato ai Blueprints e siccome vogliamo tenere i vari codici separati,
creeremo un blueprint specifico che chiameremo “auth”, che gestirà tutto quello
che concerne l’autenticazione dello User, nell afattispecie forms e views.
“auth” avrà di diritto le proprie route specifiche e verrà inizializzato e gestito,
come abbiamo fatto per il blueprint “main”.

Creiamo la dir app/auth ed il file costruttore app/auth/__init__.py

from flask import Blueprint

auth = Blueprint('auth', __name__)

from . import views

ora nella factory function dell’applicazione (file app/__init__.py)
aggiungiamo il blueprint ed inizializziamo le estensioni precedentemente installate:

from flask import Flask, render_template
from flask.ext.bootstrap import Bootstrap
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.login import LoginManager
from config import config


bootstrap = Bootstrap()
db = SQLAlchemy()

login_manager = LoginManager() # inizializzo l'estensione
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'


def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)
    bootstrap.init_app(app)
    db.init_app(app)
    login_manager.init_app(app)  

    from main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    from auth import auth as auth_blueprint # importo e registro...
    app.register_blueprint(auth_blueprint)  # ...il blueprint auth

    return app

In fase di inizializzazione dell’estensione che gestisce il login, setto
il livello di protezione della user-session a “strong”, ma è possibile
utilizzare anche i valori None e “basic”.

#...
login_manager = LoginManager()
login_manager.session_protection = 'strong'
#...

Come già accennato, utilizzando un blueprint dedicato, tutte le views e tutti i forms relativi
all’autenticazione di uno User, risiederanno nella dir app/auth, dobbiamo quindi esplicitamente
settare l’endpoint della view di login in fase di inizializzazione della estensione,
tramite l’attributo login_view.

#...
login_manager.login_view = 'auth.login'
#...

Per prima cosa creiamo il form che si occuperà del login nel file app/auth/forms.py:

from flask.ext.wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import Required, Email


class LoginForm(Form):
    email = StringField('Email', validators=[Required(), Length(1, 64),
                                             Email()])
    password = PasswordField('Password', validators=[Required()])
    remember_me = BooleanField('Keep me logged in')
    submit = SubmitField('Log In')

Come si nota, ci viene fornito tutto dall’estensione wtf precedentemente installata.
Abbiamo 2 field dove inseriamo email e pw, un booleanfield (casellina di spunta) e
il bottone submit. I primi due campi vengono validati tramite l’argomento validators.

Ora creiamo la view con la route dedicata ed importiamo in essa la form appena creata.
Il file app/auth/views.py sarà:

from flask import render_template, redirect, request, url_for, flash
from flask.ext.login import login_user
from . import auth
from ..models import User
from .forms import LoginForm  # qui importo la form di login!


@auth.route('/login', methods=['GET', 'POST'])  # route dedicata al login
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user is not None and user.verify_password(form.password.data):
            login_user(user, form.remember_me.data)
            return redirect(request.args.get('next') or url_for('main.index'))
        flash('Invalid username or password.')
    return render_template('auth/login.html', form=form)

Il funzionamento è semplicissimo:
accedendo all’url auth/login, abbiamo una richiesta (request) di tipo GET, quindi
passiamo direttamente alla rappresentazione della template auth/login.html, saltando
tutto il blocco if, che tratta solo una richiesta di tipo POST grazie al metodo
validate_on_submit() che restituisce True solo alla pressione del tasto Log-in.
In questo caso viene cercato lo user tramite query in base alle credenziali inserite e,
se trovato (not None), lo user e la sessione di competenza, vengono memorizzati e
reindirizzati nella pagina index, altrimenti protetta.
Se lo User non viene trovato appare un messaggio sulla pagina di Login grazie a flash.

La funzione login_user() mantiene l’utente loggato per tutta la sessione. Il booleanfield
remember_me mantiene l’utente loggato anche in caso di chiusura della finestra browser, in caso
di valore True, altrimenti “muore” in caso di chiusura del browser ed è necessario un nuovo login.

Per gestire le templates del blueprint auth, creiamo una sottodirectory auth in templates e creiamo
il file login.html (app/templates/auth/login.html):

{% extends "base.html" %}

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

{% block title %}Fantamanager - Login{% endblock %}

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

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

Facciamo anche un’utile modifica al file app/templates/base.html
aggiungendo i links sign-in e sign-out nella navigation bar:

{% extends "bootstrap/base.html" %}


{% block title %}Fantamanager{% endblock %}

{% block head %}

  {{ super() }}
  <link rel="shortcut icon" href="{{ url_for('static', filename = 'favicon.ico') }}"

  type="image/x-icon">
  <link rel="icon" href="{{ url_for('static', filename = 'favicon.ico') }}"

  type="image/x-icon">
  <link rel="stylesheet" 
  type="text/css" href="{{ url_for('static', filename='styles.css') }}">

{% endblock %}



{% block navbar %}

<div class="navbar navbar-inverse" role="navigation">

  <div class="container">

    <div class="navbar-header">
                 <button type="button" class="navbar-toggle"

        data-toggle="collapse" data-target=".navbar-collapse">

        <span class="sr-only">Toggle navigation</span>

        <span class="icon-bar"></span>

        <span class="icon-bar"></span>

        <span class="icon-bar"></span>
            
      </button>

      <a class="navbar-brand" href="/users">Users</a>
            
    </div>
        
    
    <div class="navbar-collapse collapse">
            
      <ul class="nav navbar-nav">

      <li><a href="/">Home</a></li>
             {% if current_user.is_authenticated() %}

        <li><a href="{{ url_for('main.user',
 username=current_user.username) }}">Profile</a></li>

      {% endif %}

      </ul>


      <ul class="nav navbar-nav navbar-right">

        {% if current_user.is_authenticated() %}
                
          <li><a href="{{ url_for('auth.logout') }}">Sign Out</a></li>

        {% else %}

          <li><a href="{{ url_for('auth.login') }}">Sign In</a></li>
                
        {% endif %}

      </ul>
        
    </div>

  </div>

</div>

{% endblock %}





{% block content %}
  
<div class="container">
  
  {% for message in get_flashed_messages() %}
    
    <div class="alert alert-warning">
      
      <button type="button" class="close" data-dismiss="alert"> &times; </button>
      
      {{ message }}
    
    </div>
  
  {% endfor %}

  
{% block page_content %}{% endblock %}
  
</div>

{% endblock %}


La variabile current_user è fornita e gestita direttamente da Flask-Login ed è pertanto
disponibile nelle views e nelle templates.
Le template specifiche poi, potranno estendere direttamente base.html invece di bootstrap/base.html
Per ottenere vantaggio da questo dobbiamo però ereditare la nostra classe User,
da quella predefinita di Flask-Login.

La classe User del file app/models.py sarà pertanto:

# ...
from flask.ext.login import UserMixin

class User(UserMixin, db.Model):
    # ...

Perchè tutto funzioni, Flask-Login richiede di settare una callback che,
dato un ID, ritorni lo User corrispondente (app/models.py):

from . import login_manager
#...

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

Ora non resta che proteggere una route. Per farlo basta utilizzare il decoratore
@login_required ad es. (app/main/views.py)

#...
from flask.ext.login import login_required

@main.route('/', methods=['GET'])
@login_required
def index():
    return render_template('index.html')

@main.route('/users', methods=['GET'])
@login_required
def users():
   #...

Abbiamo fatto tutto per gestire il login, ma manca ancora il logout.
Apriamo il file app/auth/views.py ed aggiungiamo la view logout

#...
from flask.ext.login import logout_user, login_required

#...
@auth.route('/logout')
@login_required
def logout():
    logout_user()
    flash('You have been logged out.')
    return redirect(url_for('main.index'))

la funzione logout_user, serve per resettare e rimuovere la user-session corrente.
Fino ad ora, abbiamo aggiunto Users tramite shell, quindi il prossimo passo è la
registrazione di un nuovo utente tramite form, con conseguente invio di richiesta
conferma tramite email.

articolo successivo:
Flask (parte 7): mail

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

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: ,

Flask (parte 5): password security

1 luglio 2015 Commenti chiusi

La sicurezza di un sito, passa per la autenticazione di un utente
(user authentication) e per la gestione della password collegata ad
esso.

E’ buona usanza non registrare nel database la password di un utente,
bensì l’HASH relativo ad essa.
Queste operazioni vengono gestite facilmente con Werkzeug:

nel caso non lo avessimo ancora installato

(venv) >pip install werkzeug

Werkzeug ci mette a disposizione semplicemente due funzioni:

generate_password_hash(password, method=pbkdf2:sha1, salt_length=8)
check_password_hash(hash, password)

il loro utilizzo è molto semplice, prima però di vedere come utilizzare
questo strumento, modifichiamo il model User:

from . import db
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash


class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    teams = db.relationship('Team', backref='user')
    email = db.Column(db.String(64), unique=True, index=True)
    username = db.Column(db.String(64), unique=True, index=True)
    password_hash = db.Column(db.String(128))
    
    def __repr__(self):
        return "<User %r>" % self.username

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

Abbiamo aggiunto il campo password_hash nel model User, pertanto
dobbiamo migrare e aggiornare il database:

(venv) >python manage.py db migrate -m "password_hash field added to User"
INFO  [alembic.migration] Context impl SQLiteImpl.
INFO  [alembic.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added column 'users.password_hash'
Generating >fantamanager\migrations\versions\ea1a33541f6_password_hash_field_added_to_user.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 3f1bfb116a2 -> ea1a33541f6, password_hash field added to User

facciamo ora una prova da shell:

>>> u1 = User.query.get(1)
>>> u1.password = "rosso"
>>> u1.password_hash
'pbkdf2:sha1:1000$ZDq916DS$860f7130f1e822998518f99ef078b9be11613f15'
>>> u2 = User.query.get(2)
>>> u2.password = "verde"
>>> u2.password_hash
'pbkdf2:sha1:1000$ujESM9bp$dae6906b4eb6359cf5410c469f1ae82f9664b9fb'

Nel caso dessi ad uno User la stessa password di un altro:

>>> u3 = User.query.get(3)
>>> u3.password = "verde"
>>> u3.password_hash == u2.password_hash
False

Grazie alla property, se proviamo ad accedere in lettura all’attributo password,
si solleva un’eccezione:

>>> u3.password
...
AttributeError: password is not a readable attribute

Questo meccanismo va testato, pertanto nell dir tests della nostra applicazione,
scriviamo un test in modo da tutelarci da sorprese in caso di modifiche future

Creiamo il file tests/test_user_pw_hashing.py

import unittest
from app.models import User


class UserModelTestCase(unittest.TestCase):

    def setUp(self):
        self.user_1 = User(password='rosso')
        self.user_2 = User(password='rosso')      

    def test_password_setter(self):
        self.assertTrue(self.user_1.password_hash is not None)

    def test_no_password_getter(self):
        with self.assertRaises(AttributeError):
            self.user_1.password

    def test_password_verification(self):
        self.assertTrue(self.user_1.verify_password('rosso'))
        self.assertFalse(self.user_1.verify_password('verde'))

    def test_password_salts_are_random(self):
        self.assertTrue(self.user_1.password_hash != \
                        self.user_2.password_hash)

e modifichiamo il file manage.py per lanciare comodamente i test:

#!/usr/bin/env python
...
import unittest
...

@manager.command
def test():
    """Run the unit tests."""
    tests = unittest.TestLoader().discover('tests')
    unittest.TextTestRunner(verbosity=2).run(tests)
...

ora possiamo lanciare il comando “test”:

(venv) >python manage.py test
test_no_password_getter (test_user_pw_hashing.UserModelTestCase) ... ok
test_password_salts_are_random (test_user_pw_hashing.UserModelTestCase) ... ok
test_password_setter (test_user_pw_hashing.UserModelTestCase) ... ok
test_password_verification (test_user_pw_hashing.UserModelTestCase) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.086s

OK

articolo successivo:
Flask (parte 6): authentication

articoli precedenti:
Flask (parte 1): virtualenv
Flask (parte 2): struttura progetto complesso
Flask (parte 3): database
Flask (parte 4): views e templates

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: ,

Flask (parte 4): views e templates

30 giugno 2015 Commenti chiusi

Quando un client (ad es. un web browser) manda una richiesta al web server, questi
la gira all’istanza “app” di Flask che deve riconoscere l’URL richiesta ed
eseguire il codice ad essa associato.
Questo avviene tramite funzioni dette views, che vengono associate ad un determinato url,
per mezzo del decoratore @app.route.

Se lanciassimo il server ora, con il comando:

python manage.py runserver
(venv) ...>python manage.py runserver
 * Running on http://127.0.0.1:5000/
 * Restarting with reloader

visitando la pagina indicataci http://127.0.0.1:5000/
ci troveremmo di fronte ad una bella pagina di errore

ValueError: View function did not return a response

Questo perchè non esiste ancora nessuna view associata alla route (“/”) del sito,
per fare questo, apriamo il file app/main/views.py

from flask import render_template
from . import main

@main.route('/', methods=['GET', 'POST'])
def index():
    return render_template('index.html')

creata la view, dobbiamo creare anche la template, gestite da Jinja2.
Creiamo il file app/templates/index.html

<h1>Welcome to Fantamanager!</h1>

ora lanciando il server e visitando la pagina, dovremmo vedere il nostro messaggio di benvenuto.

E se volessimo passare un argomento alla template?
la funzione render_template, accetta argomenti da passare alla template ad es. (app/main/views.py):

from flask import render_template
from . import main

@main.route('/', methods=['GET', 'POST'])
def index():
    name = "bancaldo"
    return render_template('index.html', name=name)

All’interno della template, gli argomenti si inseriscono tramite la doppia graffa:

<h1>{{ name }}, welcome to Fantamanager!</h1>

Ora creiamo una view che ci dia delle informazioni prelevate dal database, creiamo un po’ di users:

python manage.py shell
>>> for name in ("pippo", "pluto", "paperino"):
...     u = User(username=name, email="{}@example.com".format(name))
...     db.session.add(u)
...
>>> db.session.commit()
>>> User.query.all()
[<User u'pippo'>, <User u'pluto'>, <User u'paperino'>]

Ora vorrei una pagina che mi elencasse tutti gli users, quindi andiamo sul file
app/main/views.py e creiamo la views. Per prelavare dati dal database, ovviamente
abbiamo bisogno di importare l’oggetto User da models.py quindi:

from ..models import User
#...
@main.route('/users', methods=['GET', 'POST'])
def users():
    users = User.query.all()
    return render_template('users.html', users=users)

Creiamo la template relativa agli users, app/templates/users.html
Jinja2 oltre a passare argomenti, utilizza strutture di controllo e in questo caso
utilizzeremo un ciclo for:

<ul>
{% for user in users %}
	<li>{{ user.username }} mail to: {{ user.email}}</li>
{% endfor %}
</ul>

se rilanciamo il server e visitiamo la pagina http://127.0.0.1:5000/users
troviamo la nostra lista users.

Grazie a Jinja2 le template possono utilizzare l’ereditarietà ovvero è possibile
creare una template base.html:

<html>

<head>
    {% block head %}
        <title>{% block title %}{% endblock %} - Fantamanager</title>
    {% endblock %}
</head>

<body>
    {% block body %}
    {% endblock %}
</body>

</html>

ed estenderla, solo nei blocchi interessati, nelle singole templates.
In questo modo il codice verrà sempre modificato in un unico punto e non
su tutte le singole templates. Nel caso di prima, la template derivante sarà:

{% extends "base.html" %}

{% block title %}Users{% endblock %}

{% block body %}
    <ul>
    {% for user in users %}
	<li>{{ user.username }} mail to: {{ user.email}}</li>
    {% endfor %}
    </ul>
{% endblock %}

Adesso vediamo lo stile, che come ci siamo resi conto dal nostro browser,
è veramente scarno.
Per ottenere qualcosa di più appetibile visivamente, possiamo integrare il
framework Bootstrap, derivato da twitter. Essendo questa un’estensione di
Flask, sarà molto semplice installarla.
Sempre da venv, installiamo l’estensione:

C:\fantamanager>venv\Scripts\activate
(venv) C:\fantamanager>pip install flask-bootstrap
Collecting flask-bootstrap
...
Successfully installed flask-bootstrap-3.3.5.2

Ora lo registriamo nel nostro costruttore (app/__init__.py):

#...
from flask.ext.bootstrap import Bootstrap
#...


bootstrap = Bootstrap()
#...


def create_app(config_name):
    #...
    bootstrap.init_app(app)
    #...

    return app

Dopo l’installazione, sarà disponibile una template base.html di
bootstrap, dalla quale derivare tutte le altre.
Il nuovo codice della template users.html diventerà:

{% extends "bootstrap/base.html" %}

{% block title %}Users{% endblock %}

{% block navbar %}
	<div class="navbar navbar-inverse" role="navigation">
		<div class="container">
			<div class="navbar-header">
				<button type="button" class="navbar-toggle"
				data-toggle="collapse" data-target=".navbar-collapse">
					<span class="sr-only">Toggle navigation</span>
					<span class="icon-bar"></span>
					<span class="icon-bar"></span>
					<span class="icon-bar"></span>
				</button>
				<a class="navbar-brand" href="/">Home</a>
				</div>
		</div>
	</div>
{% endblock %}

{% block content %}
	<div class="container">
		<div class="page-header">
			<ul>
			{% for user in users %}
			<li>{{ user.username }} mail to: {{ user.email}}</li>
			{% endfor %}
			</ul>
		</div>
	</div>
{% endblock %}

Index diventerà:

{% extends "bootstrap/base.html" %}
{% block title %}Users{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
  <div class="container">
    <div class="navbar-header">
	<button type="button" class="navbar-toggle"
	 data-toggle="collapse" data-target=".navbar-collapse">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
	  <span class="icon-bar"></span>
	  <span class="icon-bar"></span>
	</button><a class="navbar-brand" href="/users">Users</a>
    </div>
  </div>
</div>
{% endblock %}

{% block content %}
  <h1>{{ name }}, welcome to Fantamanager!</h1>
{% endblock %}

Le navigation bar contengono già i link delle rispettive pagine di esempio alle
quali reindirizzare il browser.

Ultima cosa da sapere.
Se volessimo catturare un argomento dall’url immesso dal client e generare una vista personalizzata?
Bisogna catturare l’argomento desiderato nella vista, racchiudendolo tra <>, es.

from flask import render_template, redirect, url_for
from ..models import User
from . import main


@main.route('/', methods=['GET'])
def index():
    return render_template('index.html')

@main.route('/users', methods=['GET'])
def users():
    allusers = User.query.all()
    return render_template('users.html', users=allusers)

@main.route('/user/<name>', methods=['GET'])
def user(name):
    user = User.query.filter_by(username=name.lower()).first()
    if not user:
        return redirect(url_for('.index'))
    return render_template('user.html', user=user)

e la template user.html sarà:

{% extends "bootstrap/base.html" %}

{% block title %}Users{% endblock %}

{% block navbar %}
	<div class="navbar navbar-inverse" role="navigation">
		<div class="container">
			<div class="navbar-header">
				<button type="button" class="navbar-toggle"
				data-toggle="collapse" data-target=".navbar-collapse">
					<span class="sr-only">Toggle navigation</span>
					<span class="icon-bar"></span>
					<span class="icon-bar"></span>
					<span class="icon-bar"></span>
				</button>
				<a class="navbar-brand" href="/">Home</a>
				</div>
		</div>
	</div>
{% endblock %}

{% block content %}
  <div class="container">
    <h1>Welcome {{ user.username }}</h1>
    <div class="page-header">
      <ul>username: {{ user.username }}</ul>
      <ul>email: {{ user.email }}</ul>
    </div>
  </div>
{% endblock %}

index.html va poi rivista, poichè nell’ultmia versione importava l’argomento “name” che ora non serve più.

articolo successivo:
Flask (parte 5): password security

articoli precedenti:
Flask (parte 1): virtualenv
Flask (parte 2): struttura progetto complesso
Flask (parte 3): database

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: ,

Flask (parte 3): database

30 giugno 2015 Commenti chiusi

Cominciamo a definire il database definendo gli oggetti che rappresenteranno le entità
in relazione tra loro.
Nel file fantamanager/app/models.py cominciamo con l’importare ciò di cui abbiamo bisogno:

from . import db
from datetime import datetime

con la prima linea di codice torniamo indietro di una posizione (app) e trovando un
file __init__.py dove abbiamo definito db, lo importiamo

Ora definiamo degli oggetti molto scarni, con solo un id rappresentativo e la relazione
che lega una entità, all’altra.


class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    teams = db.relationship('Team', backref='user')


# M2M secondary tables
leagues_teams = db.Table('leagues_teams',
                         db.Column('league_id', db.Integer,
                                   db.ForeignKey('leagues.id')),
                         db.Column('team_id', db.Integer,
                                   db.ForeignKey('teams.id')))


# M2M secondary Association object
class LineupsPlayers(db.Model):
    __tablename__ = 'lineups_players'
    player_id = db.Column(db.Integer, db.ForeignKey('players.id'),
                          primary_key=True)
    lineup_id = db.Column(db.Integer, db.ForeignKey('lineups.id'),
                          primary_key=True)
    player = db.relationship('Player',
                             backref=db.backref('form_ass', lazy='dynamic'))
    position = db.Column(db.Integer)


class League(db.Model):
    __tablename__ = 'leagues'
    id = db.Column(db.Integer, primary_key=True)
    matches = db.relationship('Match', backref='league', lazy='dynamic')
    teams = db.relationship('Team', secondary=leagues_teams,
                            backref=db.backref('all_leagues', lazy='dynamic'),
                            lazy='dynamic')


class Team(db.Model):
    __tablename__ = 'teams'
    id = db.Column(db.Integer, primary_key=True)
    leagues = db.relationship('League', secondary=leagues_teams,
                              backref=db.backref('all_teams', lazy='dynamic'),
                              lazy='dynamic')
    players = db.relationship('Player', backref='team', lazy='dynamic')
    lineups = db.relationship('Lineup', backref='team', lazy='dynamic')
    trades = db.relationship('Trade', backref='team', lazy='dynamic')
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))


class Player(db.Model):
    __tablename__ = 'players'
    id = db.Column(db.Integer, primary_key=True)
    team_id = db.Column(db.Integer, db.ForeignKey('teams.id'),)
    evaluations = db.relationship('Evaluation', backref='player',
                                  lazy='dynamic')


class Match (db.Model):
    __tablename__ = 'matches'
    id = db.Column(db.Integer, primary_key=True)
    league_id = db.Column(db.Integer, db.ForeignKey('leagues.id'))
    home_id = db.Column(db.Integer, db.ForeignKey('teams.id'),)
    visit_id = db.Column(db.Integer, db.ForeignKey('teams.id'),)
    home = db.relationship('Team', foreign_keys='Match.home_id',
                           primaryjoin="Match.home_id==Team.id")
    visit = db.relationship('Team', foreign_keys='Match.visit_id',
                            primaryjoin="Match.visit_id==Team.id")


class Lineup (db.Model):
    __tablename__ = 'lineups'
    id = db.Column(db.Integer, primary_key=True)
    team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
    players = db.relationship('Player', secondary='lineups_players',
                              backref=db.backref('lineups', lazy='dynamic'))

    def get_players_by_position(self):
        lu_players = LineupsPlayers.query.filter_by(lineup_id=self.id).order_by(
                        LineupsPlayers.position).all()
        if lu_players:
            return [rec.player for rec in lu_players]


class Evaluation(db.Model):
    __tablename__ = 'evaluations'
    id = db.Column(db.Integer, primary_key=True)
    player_id = db.Column(db.Integer, db.ForeignKey('players.id'))


class Trade(db.Model):
    __tablename__ = 'trades'
    id = db.Column(db.Integer, primary_key=True)
    team_id = db.Column(db.Integer, db.ForeignKey('teams.id'),)
    player_id = db.Column(db.Integer, db.ForeignKey('players.id'),)
    player = db.relationship('Player', foreign_keys='Trade.player_id',
                             primaryjoin="Trade.player_id==Player.id")

Sommariamente:
User è in relazione “uno a molti” con Team poichè come utente posso
partecipare con più squadre. mentre una squadra non può essere di più utenti.
League è in relazione “molti a molti” con Team, poichè si potrebbe, parallelamente
alla League, partecipare anche ad una coppa con la stessa squadra.
Team è in relazione “uno a molti” con Player, poichè un giocatore non può
far parte di più squadre. Qui si acuista un giocatore tramite asta.
E così via per le altre entità.

Cominciamo a creare il database poi, per gradi, aggiungeremo i campi che
ci servono grazie alle migrations.
Portiamoci al livello di fantamanager/manage.py e se abbiamo ancora la dir migrations,
rappresentativa della struttura di un progetto complesso, rimuoviamola.
Ora lanciamo il comando di inizializzazione del database:

python manage.py db init

(su ubuntu si può omettere il comando python, avendo utilizzato il shebang)

>python manage.py db init
Creating directory ...\fantamanager\migrations ... done
Creating directory ...\fantamanager\migrations\versions ... done
...

il db non è ancora stato creato, ma possiamo notare che la dir migration è stata creata.
Procediamo con il comando che genererà il primo script di migration:

python manage.py db migrate -m "first migration"

e il comando che rende la modifica permanente:

python manage.py db upgrade
>python manage.py db upgrade
INFO  [alembic.migration] Context impl SQLiteImpl.
INFO  [alembic.migration] Will assume non-transactional DDL.

Ora apriamo il file manage.py e rendiamo disponibili per la shell tutti gli
oggetti, in modo da non reimportarli ogni volta.
La funzione make_shell_context sarà:

def make_shell_context():
    return dict(app=app, db=db,
                User=User, LineupsPlayers=LineupsPlayers,
                League=League, Team=Team, Player=Player,
                Match=Match, Lineup=Lineup,
                Evaluation=Evaluation, Trade=Trade)

Ora sistemiamo i nostri oggetti nel file models.py inquanto non
abbiamo inserito nessun campo, ma solo le relazioni tra gli oggetti stessi

es di User, League e Team migliorati:

#...

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    teams = db.relationship('Team', backref='user')
    email = db.Column(db.String(64), unique=True, index=True)
    username = db.Column(db.String(64), unique=True, index=True)

#...

class League(db.Model):
    __tablename__ = 'leagues'
    id = db.Column(db.Integer, primary_key=True)
    matches = db.relationship('Match', backref='league', lazy='dynamic')
    teams = db.relationship('Team', secondary=leagues_teams,
                            backref=db.backref('all_leagues', lazy='dynamic'),
                            lazy='dynamic')
    name = db.Column(db.String(40), unique=True)
    budget = db.Column(db.Integer)
    max_trades = db.Column(db.Integer)
    max_goalkeepers = db.Column(db.Integer)
    max_defenders = db.Column(db.Integer)
    max_midfielders = db.Column(db.Integer)
    max_forwards = db.Column(db.Integer)
    rounds = db.Column(db.Integer)
    offset = db.Column(db.Integer)

#...

class Team(db.Model):
    __tablename__ = 'teams'
    id = db.Column(db.Integer, primary_key=True)
    leagues = db.relationship('League', secondary=leagues_teams,
                              backref=db.backref('all_teams', lazy='dynamic'),
                              lazy='dynamic')
    players = db.relationship('Player', backref='team', lazy='dynamic')
    lineups = db.relationship('Lineup', backref='team', lazy='dynamic')
    trades = db.relationship('Trade', backref='team', lazy='dynamic')
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    name = db.Column(db.String(50), unique=True)
    budget = db.Column(db.Integer())
    max_trades = db.Column(db.Integer())


#...

creiamo la seconda migration con il comando:

python manage.py db migrate -m "user, team, league fields added"
>python manage.py db migrate -m "user, team, league fields added"
INFO  [alembic.migration] Context impl SQLiteImpl.
INFO  [alembic.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'users'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_users_email' on '['email']'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_users_username' on '['username']'
INFO  [alembic.autogenerate.compare] Detected added table 'leagues'
INFO  [alembic.autogenerate.compare] Detected added table 'matches'
INFO  [alembic.autogenerate.compare] Detected added table 'teams'
INFO  [alembic.autogenerate.compare] Detected added table 'trades'
INFO  [alembic.autogenerate.compare] Detected added table 'leagues_teams'
INFO  [alembic.autogenerate.compare] Detected added table 'players'
INFO  [alembic.autogenerate.compare] Detected added table 'lineups'
INFO  [alembic.autogenerate.compare] Detected added table 'lineups_players'
INFO  [alembic.autogenerate.compare] Detected added table 'evaluations'
Generating C:\fantamanager\migrations\versions\3ebec3a59744_user_team_league_fields_added.py ... done

lo script di migration è generato, per renderlo effettivo facciamo l’upgrade:

python manage.py db upgrade

Nel caso si volesse tornare indietro, basterà al posto di “upgrade” utilizzare “downgrade”

>python manage.py db upgrade
INFO  [alembic.migration] Context impl SQLiteImpl.
INFO  [alembic.migration] Will assume non-transactional DDL.
INFO  [alembic.migration] Running upgrade  -> 3ebec3a59744, user, team, league fields added

Ora possiamo entrare nella shell furba con il comando:

python manage.py shell

Creiamo un paio di Users senza salvarli su db:

>>> u1 = User(username="user_1", email="[email protected]")
>>> u2 = User(username="user_2", email="[email protected]")

i dati senza il comando “add” e “commit” non sono salvati, infatti:

>>> User.query.all()
[]

se richiamiamo le istanze di User create, ci accorgiamo che non
sono proprio riconoscibili infatti:

>>> u1
<app.models.User object at 0x039A9E30>
>>> u2
<app.models.User object at 0x039A9270>

Per renderle più amichevoli, dobbiamo utilizzare il metodo __repr__

>>> def myrepr(self):
...     return "<User %r>" % self.username
...
>>> User.__repr__ = myrepr
>>> u1
<User 'user_1'>
>>> u2
<User 'user_2'>

Ok, visto che ci piace, rendiamo effettiva questa modifica nel file models.py
aggiungendo il metodo __repr__ in ogni classe, come più ci piace.
Ora che abbiamo visto come lavorare sul database, modificandolo, focalizziamoci
sulle singole classi per introdurre anche le view e le template.

articolo successivo:
Flask (parte 4): views e templates

articoli precedenti:
Flask (parte 1): virtualenv
Flask (parte 2): struttura progetto complesso

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: ,

Flask (parte 2): struttura progetto complesso

26 giugno 2015 Commenti chiusi

Come si può notare nella maggiorparte delle guide presenti sul web, molto spesso gli esempi sono codificati
all’interno di un unico script, eccezion fatta per le templates che devono risiedere in una sottodirectory “templates”.
Utilizzando però un unico modulo, al crescere della complessità dell’applicazione, cresce anche la difficoltà
di gestione del codice stesso.
Flask non dà direttive in merito pertanto lo sviluppatore di turno può agire come crede.
Una buona soluzione, guarda caso, la si trova al capitolo 7 del libro di Grinberg e ne
mostrerò qui la struttura:

|-fantamanager
  |-app/
    |-templates/
    |-static/
    |-main/
      |-__init__.py
      |-errors.py
      |-forms.py
      |-views.py
    |-__init__.py
    |-email.py
    |-models.py
  |-migrations/
  |-tests/
    |-__init__.py
    |-test*.py
  |-venv/
  |-requirements.txt
  |-config.py
  |-manage.py

Questa struttura è composta da 4 directories principali…

app: il package dove risiede la Flask-application
migrations: dove risiedono i migrations-scripts che vedremo dopo
tests: il package dove risiedono gli Unit tests
venv: l’ambiente virtuale trattato nella parte 1

…e da 3 file fondamentali:

requirements.txt: contenente le dipendenze della nostra applicazione (parte 1)
config.py: contenente tutte le configurazioni e i settaggi della app
manage.py: lo script che lancia la nostra applicazione.

Da qui in avanti cominceremo a mettere il codice al posto giusto e lo analizzeremo passo
passo.

Per questa parte sono necessari 3 moduli che andremo ad installare:

(venv) >pip install flask-script flask-sqlalchemy flask-migrate

poi periodicamente o alla fine, è bene rinfrescare il file requirements.txt con il comando:

(venv) >pip freeze>requirements.txt

Il file Config

Di requirements.txt abbiamo già parlato, vediamo config.py:

import os
basedir = os.path.abspath(os.path.dirname(__file__))


class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
    SQLALCHEMY_COMMIT_ON_TEARDOWN = True
    FANTAMANAGER_MAIL_SUBJECT_PREFIX = '[FANTAMANAGER]'
    FANTAMANAGER_MAIL_SENDER = 'FANTAMANAGER Admin <[email protected]>'
    FANTAMANAGER_ADMIN = os.environ.get('FANTAMANAGER_ADMIN')

    @staticmethod
    def init_app(app):
        pass


class DevelopmentConfig(Config):
    DEBUG = True
    MAIL_SERVER = 'smtp.googlemail.com'
    MAIL_PORT = 587
    MAIL_USE_TLS = True
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')


class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')


class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data.sqlite')


config = {'development': DevelopmentConfig,
          'testing': TestingConfig,
          'production': ProductionConfig,
          'default': DevelopmentConfig}

La classe Config contiene i settaggi comuni alle altre configurazioni, le altre classi invece
si distinguono tra loro in base ai diversi settaggi ed ognuna subclassa la prima.
Tutte queste classi vengono registrate in un dizionario “config”.

Tutti i valori di tali variabili, sono raggiungibili tramite il dizionario os.environ.
In questo modo possono essere settate in maniera invisibile da shell e poi recuperate (password, email, username, ecc),
senza la necessità di mostrarle nel codice sorgente.

(venv) C:\fantamanager>set SECRET_VAR=secretvalue

(venv) C:\fantamanager>python
Python 2.7.8 (default, Jun 30 2014, 16:03:49) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.environ.get("SECRET_VAR")
'secretvalue'

Nella superclass Config viene definito anche un metodo di classe init_app, che accetta come
argomento app e permette, in caso di necessità, nelle varie sottoclassi di configurazione, di specializzare
l’inizializzazione della app stessa.

Per capire il funzionamento dell’applicazione, parleremo ora di application package e application factory.
Prima un passo indietro.
Quando negli esempi classici (unico file) viene creata l’applicazione, questo avviene a livello globale (global scope)

from flask import Flask

app = Flask(__name__)
#...

il problema è che, una volta lanciato lo script, viene creata l’istanza di app e non è più possibile
apportare cambiamenti dinamicamente.

Per ovviare a questo spostiamo tutta l’inizializzazione dell’applicazione all’interno del costruttore __init__.py,
dell’application package, in esso importiamo tutte le estensioni che ci servono ma non passiamo nessun parametro in fase
di inizializzazione delle stesse, questo perchè l’istanza app non è ancora stata creata.
Questo avverrà infatti all’interno della factory function, nella quale provvederemo al completamento delle inizializzazioni
precedenti:

from flask import Flask, render_template
from flask.ext.sqlalchemy import SQLAlchemy
from config import config # il dizionario config!

db = SQLAlchemy()

def create_app(config_name): # la factory function
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)
    db.init_app(app)
    return app

come si nota la factory function, quando chiamata (quando vogliamo e anche più volte) ritorna l’istanza app
debitamente inizializzata.

Ora che abbiamo introdotto l’application factory sorge il problema delle route, che nello script singolo venivano
definite tramite il decoratore app.route a livello globale come la stessa app, ora invece il decoratore che deve definirle
viene utilizzato prima che la app sia creata dalla factory function.
Per questo Flask mette a disposizione i Blueprints. I bluebprint sono simili ad una applicazione, possono cioè
registrare le route con i decoratori appositi. Queste route rimangono “dormienti” fintanto che il Blueprint non viene
registrato sulla applicazione app, a quel punto, essendo il blueprint definito a livello globale, le route diventano disponibili.
Per migliorare la flessibilità e la leggibilità della struttura del progetto, dentro app verranno create tante subdir,
quanti sono i blueprints necessari.
Il blueprint principale verrà chiamato “main” (come si nota dalla struttura precedente).

|-fantamanager
  |-app/
    |-main/
      |-__init__.py
      |-errors.py
      |-forms.py
      |-views.py

Il costruttore __init__.py del blueprint “main” sarà:

from flask import Blueprint

main = Blueprint('main', __name__)

from . import views, errors

main è il blueprint che importeremo nei moduli errors.py e views.py specifici.
Per evitare riferimenti circolari negli imports, i due moduli che faranno uso di “main” (vedi es. app/main/errors.py),
vengono importati per ultimi.

from flask import render_template
from . import main # ecco il blueprint!

@main.app_errorhandler(404) # route specifica
def page_not_found(e):
    return render_template('404.html'), 404

@main.app_errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

es. app/main/views.py

from flask import render_template, session, redirect, url_for
from . import main

@main.route('/', methods=['GET', 'POST'])
def index():
    # ...

Ora il blueprint va registrato nella factory function del costruttore della app (app/_init_.py):

def create_app(config_name):
    # ...
    
    from main import main as main_blueprint
    app.register_blueprint(main_blueprint)
    
    return app

Il file manage.py

Come ultimo fondamentale settaggio, il file manage.py che altri non è che il nostro launch script:

#!/usr/bin/env python
import os
from app import create_app, db
# from app.models import ... qui importiamo gli oggetti del models 
from flask.ext.script import Manager, Shell
from flask.ext.migrate import Migrate, MigrateCommand

app = create_app(os.getenv('FLASK_CONFIG') or 'default') # ecco la factory function
manager = Manager(app)
migrate = Migrate(app, db)

def make_shell_context():
    return dict(app=app, db=db) # qui aggiungeremo gli oggetti dei models

manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

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

Segnalo la funzione make_shell_context che ritorna un dizionario con tutti gli oggetti
che vogliamo avere disponibili lanciando una shell, senza doverli importare ogni volta.
quando lanceremo il comando

python manage.py shell

Shell dell’estensione script ci renderà disponibili senza importarli, tutti gli oggetti forniti al
dizionario ritornato da make_shell_context.

quando invece lanceremo il comando

python manage.py db <command>

utilizzeremo l’estensione migrate, che vedremo più avanti.

Questi erano la struttura ed i settaggi di base per la creazione di una applicazione.

Articoli successivi:
Flask (parte 3): database

Articoli precedenti:
Flask (parte 1): virtualenv

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: ,

Flask (parte 1): virtualenv

26 giugno 2015 Commenti chiusi

Molte informazioni di questa guida prendono spunto o sono tradotte dal magnifico libro
“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)

Il primo passo è installare un ambiente virtuale (ovviamente dopo aver installato Python).

Utilizzeremo virtualenv, un tool che ci permette di fare una copia dell’interprete Python
ed installare su di essa tutti i pacchetti necessari allo sviluppo della nostra applicazione.
Questo è molto comodo dal momento che l’interprete python “principale” installato sul sistema
non verrà mai toccato e rimarrà nel suo stato nativo e “pulito”.
Ogni applicazione avrà quindi al suo interno una copia dell’interprete Python di sistema e tutti
i pacchetti necessari installati.

Controlliamo di non aver già installato questo tool:

$ virtualenv --version

se otteniamo un errore, procediamo con l’installazione.

Su ubuntu da terminale:

$ sudo apt-get install python-virtualenv

Su windows, scarichiamo lo script ez_setup.py dal sito ufficiale
e lo lanciamo con:

>python ez_setup.py
Downloading https://pypi.python.org/packages/source/s/setuptools/setuptools-18.0.1.zip
#...
Installing Setuptools
#...
Installing easy_install-2.7.exe script to C:\Python27\Scripts
#...
Finished processing dependencies for setuptools==18.0.1

In caso di problemi con la modifica delle variabili di sistema è possibile
lanciare easy_install dal path evidenziato: C:\Python27\Scripts

>\Python27\Scripts\easy_install.exe virtualenv
Searching for virtualenv
Reading https://pypi.python.org/simple/virtualenv/
Best match: virtualenv 13.0.3
Downloading https://pypi.python.org/packages/source/v/virtualenv/virtualenv-13.0.3.tar.gz#md5=cd2043ea72448d65dcc54d33744c2214
#...
Installing virtualenv-2.7.exe script to C:\Python27\Scripts
#...
Finished processing dependencies for virtualenv

Ora creiamo la dir principale della nostra futura applicazione, nel mio caso “fantamanager” e
ci portiamo all’interno di essa creando il clone dell’interprete Python:

da ubuntu:

$ virtualenv venv

o da win:

C:\fantamanager>\Python27\Scripts\virtualenv.exe venv
New python executable in venv\Scripts\python.exe
Installing setuptools, pip, wheel...done.

Ora, all’interno della dir abbiamo una sottodirectory venv con all’interno una
copia “pulita” dell’interprete python.

Ora dobbiamo abilitare venv con il seguente comando:

da ubuntu:

$ source venv/bin/activate

o da win:

C:\fantamanager>venv\Scripts\activate
(venv) C:\fantamanager>

ora è possibile installare i pacchetti necessari.
Il primo fondamentale è flask, quindi:

(venv) C:\fantamanager>pip install flask
#...
Collecting flask
#...
  Downloading Flask-0.10.1.tar.gz (544kB)
    100% |################################| 544kB 401kB/s
Collecting Werkzeug>=0.7 (from flask)
#...
Successfully installed Jinja2-2.7.3 Werkzeug-0.10.4 flask-0.10.1 itsdangerous-0.24 markupsafe-0.23

Una nota va spesa per i pacchetti che vengono installati.
Per non fare impazzire gli utenti che prenderanno a mano il nostro codice,
rubando loro tempo per installare tutti i pacchetti necessari al funzionamento dell’applicazione,
dobbiamo creare un file “requirements.txt” (chiamatelo come vi pare)
con elencate tutte le dipendenze necessarie.
Per fare questo, finito lo sviluppo dell’applicazione, tramite il tool “pip”:

(venv) $ pip freeze >requirements.txt

Se andiamo ad analizzarlo troveremo:

Flask==0.10.1
itsdangerous==0.24
Jinja2==2.7.3
MarkupSafe==0.23
Werkzeug==0.10.4
wheel==0.24.0

Quando un utente prenderà a mano la nostra applicazione e troverà il file requirements,
dovrà solamente installare le dipendenze all’interno del suo venv (per non intaccare
l’interprete di sistema) con:

(venv) $ pip install -r requirements.txt

e sarà allineato.

Abbiamo detto come attivare virtualenv, ma non come disattivarlo:

(venv) C:\fantamanager>deactivate
C:\fantamanager>

Il prossimo passo sarà quello di suddividere l’applicazione in più file;
purtroppo su molti testi, gli esempi fanno sempre riferimento a singoli file
che all’interno contengono impostazioni, view, models, diventando di difficile
gestione, aumentando di complessità.

articoli successivi:
Flask (parte 2): struttura progetto complesso

Categorie:flask, python Tag: ,

Python: creazione calendario Fantalega

28 aprile 2010 Commenti chiusi

Il modulo che regola la creazione del calendario secondo il metodo del cingolo a scorrimento è:

# calendar.py


class Calendar(object):
    def __init__(self, teams, rounds=2):
        self.teams = teams
        self.max_rounds = rounds
        self.team1_home = True
        self.team2_home = True
        self.matrix = []
        self.first_round_calendar = []
        self.season = []

    def generate_matrix(self):
        """
        generate_matrix() -> list

        create a matrix to fill with teams
        First init matrix with all None fields.
        The matrix first row is a copy of input iterable (i.e. teams).
        The matrix second row is the reversed one except the last iterable value
        that is excluded.
        """
        # init matrix
        self.matrix = [[None]*len(self.teams) for team in self.teams]
        # first matrix row with all teams (header) i.e. [1, 2, 3, 4]
        self.matrix[0] = self.teams
        # reversed header without the last team i.e. [3, 2, 1, None]
        row2 = self.teams[:][:-1]
        row2.reverse()
        self.matrix[1][0:(len(self.teams) - 1)] = row2[0:(len(self.teams) - 1)]

    def matrix_rotation(self):
        """
        matrix_rotation() -> list

        This is the first step of matrix composition.
        if the first row is [3, 2, 1, None], the second one will be the last value
        except None of the first row at postion 1 and then the other values rotated:
        [1, 3, 2, None]
        The last row will be [2, 1, 3, None]
        """
        i = 1
        while i < len(self.teams):
            k = 1
            for item in self.matrix[i]:
                try:
                    self.matrix[i + 1][k] = item
                    self.matrix[i + 1][0] = self.matrix[i + 1][(
                        len(self.teams) - 1)]
                    self.matrix[i + 1][(len(self.teams) - 1)] = None
                    k += 1
                except IndexError:
                    break
            i += 1

    def matrix_substitution(self):
        """
        matrix_substitution() -> list

        This is the second step of matrix composition.
        If the matrix header and a matrix row for the same index, have the same value
        the row index moves his value in place of 'None' and gets the excluded value
        of the previous matrix generation (4):
        [1, 2, 3, 4]
        [3, 2, 1, None] --> 2 has same index of header
        [3, 4, 1, 2]  4 take place of 2 and 2 substitutes None
        """
        row_m = 1
        while row_m < len(self.teams):
            for item_a in self.matrix[0]:
                for item_b in self.matrix[row_m]:
                    if self.matrix[0].index(item_a) == \
                       self.matrix[row_m].index(item_b):
                        # when a value has the same index than header 
                        if item_a == item_b:
                            # the value is substituted by the excluded one (e.g. 4)
                            self.matrix[row_m][self.matrix[row_m].index(item_b)] = \
                                self.teams[-1]  
                            self.matrix[row_m][(len(self.teams) - 1)] = item_b
            row_m += 1

    def match_composition(self):
        """
        match_composition() -> list

        Match composition: third step
        coupling the first team of header with the matrix first-row team
        with same index. The second team of header is coupled with the
        second team of the matrix first row.
        We get teams only if the team are not chosen yet, to avoid duplicated match
        if header team or n-row one is duplicate, we pass to the other column
        I.e. At day 1 we couple header team with same index first-row team,
        at day 2 we couple header team with same index second-row team and so on.
        """
        cal = []
        day = 1
        while day < len(self.teams):
            first_round = []
            for team1 in self.matrix[0]:
                index = self.matrix[0].index(team1)
                team2 = self.matrix[day][index]
                if team2 not in first_round or team1 not in first_round:
                    if self.team1_home is True:
                        first_round.append(team1)
                        first_round.append(team2)
                        cal.append((day, team1, team2))
                        self.team1_home = False
                        self.team2_home = True
                    else:
                        first_round.append(team2)
                        first_round.append(team1)
                        cal.append((day, team2, team1))
                        self.team1_home = True
                        self.team2_home = False
            day += 1
        return cal


    def generate_2nd_round(self):
        """
        generate_2nd_round() -> list

        Create the second round for half calendar
        Tuples have "day, team_a, team_b" format
        """
        return [(int(n) + len(self.teams) - 1, team2, team1)
                for n, team1, team2 in self.first_round_calendar]


    def build_season(self):
        """
        build_season() -> list

        generate a match calendar iterable.
        Tuples have "day, team_a, team_b" format
        """
        n_round = 1
        self.generate_matrix()
        self.matrix_rotation()
        self.matrix_substitution()
       
        self.first_round_calendar = self.match_composition()
        while n_round < self.max_rounds:
            second_round = self.generate_2nd_round()
            self.season = self.first_round_calendar + second_round
            n_round += 1
        return self.season

Una piccola interfaccia per l’inserimento delle squadre:

# main.py

import wx
from random import shuffle
from calendar import Calendar


def get_teams():
    app = wx.App()
    app.MainLoop()
    teams = []
    dlg = wx.TextEntryDialog(None, "number of teams?", 'INFO', '')
    if dlg.ShowModal() == wx.ID_OK:
        try:
            nteams  = int(dlg.GetValue())
        except ValueError:
            print "ERROR: Need a number!"
        else:
            # exit if number of teams is < 2
            if nteams < 2:
                print "ERROR: number of teams must be >= 2"
            else:
                
                team_no = 1
                while team_no <= nteams:
                    dlgt = wx.TextEntryDialog(None, "team name n. %d?"  % (team_no),
                                              'INFO', '')
                    if dlgt.ShowModal() == wx.ID_OK:
                        name = dlgt.GetValue()
                        if name in teams:
                            print "ERROR: team <%s> already exists" % name
                            dlgt.SetValue('')
                        else:
                            teams.append(name)
                            dlgt.SetValue('')
                            team_no += 1
                # Add fake team if number of teams is odd
                if nteams % 2 != 0: 
                    teams.append('Fake Team')
        return teams


if __name__ == '__main__':
    teams = get_teams()
    # shuffle(teams)
    if teams:
        cal = Calendar(teams=teams)
        season = cal.build_season()
        for match in season:
            print match
    else:
        print "WARNING: no teams saved"

Un piccolo abbozzo di tests:

import unittest
from calendar import Calendar


class CalendarInitTestCase(unittest.TestCase):
    def setUp(self):
        self.teams = ['Inter', 'Milan', 'Juve', 'Napoli']
        self.cal = Calendar(self.teams)

    def tearDown(self):
        self.teams = []

    def test_default_rounds(self):
        self.assertEqual(self.cal.max_rounds, 2)

    def test_default_teams(self):
        self.assertEqual(self.cal.teams, self.teams)


class CalendarMatrixTestCase(unittest.TestCase):
    def setUp(self):
        self.teams = ['Inter', 'Milan', 'Juve', 'Napoli']
        self.cal = Calendar(self.teams)
        self.cal.generate_matrix()

    def tearDown(self):
        self.cal.teams = []
        self.cal.matrix = []

    def test_matrix_header(self):
        self.assertEqual(self.cal.matrix[0], self.teams)

    def test_matrix_first_row_before_rotation(self):
        self.assertEqual(self.cal.matrix[1], ['Juve', 'Milan', 'Inter', None])

    def test_matrix_second_row_before_rotation(self):
        self.assertEqual(self.cal.matrix[2], [None, None, None, None])

    def test_matrix_third_row_before_rotation(self):
        self.assertEqual(self.cal.matrix[3], [None, None, None, None])

    def test_matrix_second_row_after_rotation(self):
        self.cal.matrix_rotation()
        self.assertEqual(self.cal.matrix[2], ['Inter', 'Juve', 'Milan', None])

    def test_matrix_third_row_after_rotation(self):
        self.cal.matrix_rotation()
        self.assertEqual(self.cal.matrix[3], ['Milan', 'Inter', 'Juve', None])

    def test_matrix_first_row_after_substitution(self):
        self.cal.matrix_rotation()
        self.cal.matrix_substitution()
        self.assertEqual(self.cal.matrix[1], ['Juve', 'Napoli', 'Inter', 'Milan'])

    def test_matrix_second_row_after_substitution(self):
        self.cal.matrix_rotation()
        self.cal.matrix_substitution()
        self.assertEqual(self.cal.matrix[2], ['Napoli', 'Juve', 'Milan', 'Inter'])

    def test_matrix_third_row_after_substitution(self):
        self.cal.matrix_rotation()
        self.cal.matrix_substitution()
        self.assertEqual(self.cal.matrix[3], ['Milan', 'Inter', 'Napoli', 'Juve'])


class CalendarSeasonTestCase(unittest.TestCase):
    def setUp(self):
        self.teams = ['Inter', 'Milan', 'Juve', 'Napoli']
        self.cal = Calendar(self.teams)
        self.cal.generate_matrix()
        self.cal.matrix_rotation()
        self.cal.matrix_substitution()
        self.season = [(1, 'Inter', 'Juve'), (1, 'Napoli', 'Milan'),
                       (2, 'Inter', 'Napoli'), (2, 'Juve', 'Milan'),
                       (3, 'Inter', 'Milan'), (3, 'Napoli', 'Juve'),
                       (4, u'Juve', u'Inter'), (4, u'Milan', u'Napoli'),
                       (5, u'Napoli', u'Inter'), (5, u'Milan', u'Juve'),
                       (6, u'Milan', u'Inter'), (6, u'Juve', u'Napoli')]
    def tearDown(self):
        self.cal.teams = []
        self.cal.matrix = []

    def test_1st_round_season(self):
        self.cal.build_season()
        self.assertEqual(self.cal.first_round_calendar,
                         self.season[:len(self.season)/2])

    def test_complete_season(self):
        self.cal.build_season()
        self.assertEqual(self.cal.season, self.season)
        
        
    


if __name__ == '__main__':
    suite1 = unittest.TestLoader().loadTestsFromTestCase(CalendarInitTestCase)
    suite2 = unittest.TestLoader().loadTestsFromTestCase(CalendarMatrixTestCase)
    suite3 = unittest.TestLoader().loadTestsFromTestCase(CalendarSeasonTestCase)
    alltests = unittest.TestSuite([suite1, suite2, suite3])
    unittest.TextTestRunner(verbosity=2).run(alltests)
Categorie:python, wxpython Tag: ,