Flask framework part 2

14 stycznia 2016 15:47
Jakub Januzik
python flask framework

Flask framework 2

Generowanie url - funkcja url_for

Podczas renderowania naszych template'ów bardzo często tworzone będą jakieś przekierowania (ang. redirect). Do tworzenia dynamicznie url na podstawie ścieżki zawartej w dekoratorze @route służy funkcja url_for. Przyjmuje ona nazwę funkcji jako pierwszy argument, a jako kolejne argumenty kluczowe, z których każdy odpowiada zmiennej części reguły URL. Nieznane części zmiennych są dołączane do adresu URL jako Query param.

Przykład

>>> from flask import Flask, url_for
>>> app = Flask(__name__)
>>> @app.route('/')
... def index(): pass
...
>>> @app.route('/login')
... def login(): pass
...
>>> @app.route('/user/<username>')
... def profile(username): pass
...
>>> with app.test_request_context():
...  print url_for('index')
...  print url_for('login')
...  print url_for('login', next='/')
...  print url_for('profile', username='John_Doe')
...
/
/login
/login?next=/
/user/John_Doe

WTForms

WTForms jest dodatkową biblioteką pythona wspomagającą tworzenie formularzy, walidację oraz renderowanie formularzy. Przydatna m.in. w panelach: logowania, rejestracji, wysyłania wiadomości, zamawiania produktów, * i wiele innych...

Przykładowy formularz

from wtforms import Form, BooleanField, TextField, PasswordField, validators

class RegistrationForm(Form):
    username = TextField('Username', [validators.Length(min=4, max=25)])
    email = TextField('Email Address', [validators.Length(min=6, max=35)])
    password = PasswordField('New Password', [
        validators.Required(),
        validators.EqualTo('confirm', message='Passwords must match')
    ])
    confirm = PasswordField('Repeat Password')

Aby utworzyć formularz, musimy stworzyć klasę, która dziedziczy po klasie Form, która znajduje się w paczce wtforms. Dostarczy to nam wielu użytecznych funkcji, z których będziemy korzystać.

Aby utworzyć jakieś pole formularza musimy utworzyć atrybut/pole klasy które musi być instancją którejkolwiek z klas przeznaczonych dla pól formularza w paczce wtforms. Klasy te kończą się słówkiem Field przez co bardzo łatwo poznać które to (np. TextField lub PasswordField).

Klasy pól przyjmują kilka parametrów, w większości opcjonalnych, najważniejszymi są label który w naszym przypadku jest przekazywany jako pierwszy parametr i jest to etykieta naszego pola w formularzu. Drugim argumentem jest lista tzw walidatorów czyli funkcja sprawdzająca poprawność wprowadzonych danych. Jednym z takich walidatorów jest klasa Required, która zwróci błąd w przypadku, gdy pole będzie puste.

WTForms wybór niektórych funkcji:

validate()

Zadaniem funkcji validate jest, jak sama nazwa wskazuje, walidacja danych zawartych w formularzu. Funkcja ta zwraca wartości boolowskie, czyli True/False w zależności od wyniku, dodatkowo, gdy zwróci False odpowiednie komunikaty o błędach będą dostępne w polu errors naszej zmiennej formularza.

populate_obj(object)

Kolejną bardzo ciekawą funkcją jest funkcja populate_obj, która ułatwia korzystanie z klas (jako modeli) przy zapisywaniu formularza. Jej zadaniem jest przepisanie atrybutów z formularza do pól klasowych. Może zerknijmy na przykład:

def edit_product(request):
    product = fetch_product_from_database() # Pobranie produktu z bazy danych
    form = EditProductForm(request.POST, obj=product)
    if request.POST and form.validate():
        form.populate_obj(product) # Użycie naszej funkcji
        product.save() # Zapis do bazy danych
        return redirect(url_for('product.list'))
    # Reszta kodu

Na początku pobieramy produkt i zapisujemy go do zmiennej product. Później, podczas zapisywania formularza korzystamy z funkcji populate_obj przekazując tam naszą zmienną product. Jest to równoważne z tym, że Python będzie próbował przepisać wszystkie pola formularza do pól produktu w stosunku 1 do 1. Np. jeśli produkt zawiera pola cena, opis i nazwa oraz gdy formularz zawiera takie same pola, to po wywołaniu populate_obj wartości z formularza zostaną przepisane do zmiennej product automatycznie. Na końcu zapisujemy zmieniony już produkt do bazy oraz przekierowujemy użytkownika na inną stronę.

Czas na praktykę!

Praktyka czyni mistrza, także czas na małe zadanie:

Front-end developer dostarczył gotowy front-end dla aplikacji PyPiła-kwejk. Naszym zadaniem jest napisać współpracujący backend oraz uruchomić aplikację.

Pobieramy aplikację z GITa

W katalogu development

$ git clone git@gitlab.com:pypila/pypila_flask.git
# Dla wszystkich mających problem alternatywny link:
$ git clone https://gitlab.com/pypila/pypila_flask.git
$ cd pypila_flask
$ git checkout zadanie

Instalujemy paczki

$ mkvirtualenv pypila_zadanie
$ pip install -r requirements.txt

Wymagania:

Napisać niżej wymienione funkcję zgodnie z dokumentajcą w kodzie. Doprowadzić aplikację do stanu używalności.

  1. List item
  2. index
  3. browse
  4. register
  5. login
  6. logout
  7. validate_password
  8. user_exists
  9. get_images
  10. page_not_found
  11. internal_server_error
  12. add

Podpowiedzi/Odpowiedzi:

Są to przypadkowe odpowiedzi, Wasze mogą się różnić.

@app.route('/')
def index():
    """
        if user is logged:
            render template index.html with user name
            in "name" parameter
        else:
            just render template index.html
    """
    if 'username' in session:
        return render_template(
            'index.html',
            name=session['username']
        )
    return render_template('index.html')

@app.route('/browse')
def browse():
    """
        render template with get_images function 
        in "images" parameter
    """
    return render_template(
        'browse.html',
        images=get_images()
    )

@app.route('/register', methods=['GET', 'POST'])
def register():
    form = forms.RegistrationForm(request.form)
    if request.method == 'POST' and form.validate():
        if not user_exists(form.username.data):
            user = '{}:{}'.format(
                form.username.data,
                form.password.data
            )
            with open('users.txt', 'a') as users:
                users.write(user)
                users.write('\n')
                flash("User have been created succesfully")
            return redirect(url_for('login'))
        flash(
            "Sorry but user with given username 
            already exists"
        )
        return redirect(url_for('register'))
    return render_template('register.html', form=form)


@app.route('/login', methods=['GET', 'POST'])
def login():
    form = forms.LoginForm(request.form)
    if request.method == 'POST' and form.validate():
        if validate_password(
            form.username.data,
            form.password.data
        ):
            session['username'] = form.username.data
            flash("You have been logged succesfully")
            return redirect(url_for('index'))
    return render_template('login.html', form=form)


@app.route('/logout')
def logout():
    """
        Pop user from session, send flash message:
        "You have been logged out succesfully"
        and redirect to index page
    """
    session.pop('username')
    flash("You have been logged out succesfully")
    return redirect(url_for('index'))


def validate_password(username, password):
    """
        Helper function for login().
        Checks if username and password combination 
        match to data in users.txt
        @param username: user name
        @param password: user password
        @return: True or False
    """
    with open('users.txt', 'r') as users:
        for line in users.read().splitlines():
            if str(username) == line.split(':')[0]:
                if str(password) == line.split(':')[1]:
                    return True
    return False


def user_exists(username):
    """
        Helper function for register().
        Checks if user exists in users.txt
        @param username: user name
        @return: True or False
    """
    with open('users.txt', 'r') as users:
        for line in users.read().splitlines():
            if str(username) == line.split(':')[0]:
                return True
    return False


def get_images():
    """
        Helper function for browse()
        Creates reversed list with urls to images
        taken from images.txt file
    """
    photos = []
    with open('images.txt', 'r') as images:
        for line in images.read().splitlines():
            photos.append(line)
    return reversed(photos)


@app.errorhandler(404)
def page_not_found(e):
    """
        render template 404.html and send 404 http status
    """
    return render_template('404.html'), 404


@app.errorhandler(500)
def internal_server_error(e):
    """
        render template 500.html and send 500 http status
    """
    return render_template('500.html'), 500


@app.route('/add', methods=['GET', 'POST'])
def add():
    """
        Render add.html with forms.AdditionForm(request.form)
        as "form" param
        if form is correct open images.txt file and
        add url to next line, then
        flash message: 'Photo added succesfully'
        and redirect to browse.html
    """
    form = forms.AdditionForm(request.form)
    if request.method == 'POST' and form.validate():
        with open('images.txt', 'a') as photos:
            photos.write(str(form.url.data))
            photos.write('\n')
            flash('Photo added succesfully')
        return redirect(url_for('browse'))
    return render_template('add.html', form=form)

Related pages

Comments