Flask – это замечательный микро веб фреймворк, основанный на Python. Flaskr – это миниблог, который описан в официальном руководстве по Flask. Я продирался через это руководство больше раз, чем могу в этом признаться. Тем не менее, я хотел бы взять это руководство для следующего шага, добавив в него разработку через тестирование (test driven development) и немножко jQuery.
Если вы новичок во Flask и/или в веб-разработке в целом, важно понимать эти основные фундаментальные понятия:
- Разницу между Get и Post запросом, и какие функции в приложении их обрабатывают.
- Что такое request (Запрос).
- Как HTML-страницы отображаются и возвращаются посетителю в браузере.
**Примечание**: этот руководство представлено проектом https://realpython.com. Пожалуйста, поддержите этот проект с открытым исходным кодом, приобретая наши курсы http://www.realpython.com/courses с обучением Python и веб-разработке с Django и flask!
Содержание
- Разработка через тестирование?
- Скачиваем Python
- Установка проекта
- Первый тест
- Установка Flaskr
- Второй тест
- Установка базы данных
- Шаблоны и представления(Views)
- Добавляем цвета
- Тест
- jQuery
- Развёртывание
- Еще тест!
- Bootstrap
- SQLAlchemy
- Заключение
Требования
Руководство использует следующее ПО:
- Python v3.5.1
- Flask v0.10.1
- Flask-SQLAlchemy v2.1
- gunicorn v19.4.5
Разработка через тестирование?
Разработка через тестирование (tdd) — это вид разработки, который предусматривает написание автоматических тестов перед написанием самой функции. Иными словами, это комбинация испытания и написания кода. Этот процесс не только помогает обеспечить корректность кода, но также позволяет развивать дизайн и архитектуру проекта под постоянным контролем.
TDD обычно следует схеме "Красный-Зеленый-Рефакторинг" как показано на картинке выше:
- Написать тест
- Выполнить тест (и потерпеть неудачу)
- Написать код, чтобы тест был пройден
- Рефакторинг кода и повторное испытания снова и снова (при необходимости)
Скачиваем Python
Перед началом убедитесь, что у вас установлена последняя версия Питон 3.5, который вы можете скачать с http://www.python.org/download/
Примечание: это руководство использует Python V 3.5.1.
Вместе с Python также надо поставить:
- pip — это система управления пакетами в Python, похожая на gem или npm в Ruby или Node, соответственно.
- pyvenv — используется для создания изолированной среды в разработке. Это стандартная практика. Всегда, всегда и еще раз всегда используйте виртуальные среды. Если вы этого не сделаете, то в конечном итоге столкнетесь с проблемами совместимости между различными зависимостями.
Установка проекта
Создайте новую папку для сохранения проекта:
$ mkdir flaskr-tdd $ cd flaskr-tdd
Создайте и активируйте виртуальное окружение:
$ pyvenv-3.5 env $ source env/bin/activate
Примечание:
Когда вы находитесь внутри виртуального окружения, в терминале до значка $ показывается надпись (env). Для выхода из виртуальной среды используйте командуdeactivate
, а при необходимости ее активировать перейдите в нужный каталог и запустите командуsource env/bin/activate
.
Установка Flask с помощью pip:
$ pip3 install Flask
Первый тест
Давайте начнем с простой программы "hello, world".
Создаем тестовый файл:
$ touch app-test.py
Откройте этот файл в вашем любимом текстовом редакторе. И добавьте в файл app-test.py следующие строки:
from app import app import unittest class BasicTestCase(unittest.TestCase): def test_index(self): tester = app.test_client(self) response = tester.get('/', content_type='html/text') self.assertEqual(response.status_code, 200) self.assertEqual(response.data, b'Hello, World!') if __name__ == '__main__': unittest.main()
По сути, мы проверяем, придет ли к нам ответ с кодом "200" и отображается ли "hello, world" .
Запустим тест:
$ python app-test.py
Если все хорошо, то тест будет провален (fail).
Теперь добавим следующие строки в файл app.py, чтобы успешно пройти тест.
$ touch app.py
Код:
from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Hello, World!" if __name__ == "__main__": app.run()
Запустим наш app:
$ python app.py
Обратимся по адресу http://localhost:5000/. Вы увидите строку "Hello, World!" на вашем экране.
Вернемся в терминал и остановим сервер разработки с помощью Ctrl+C.
Запустим наш тест вновь:
$ python app-test.py . ---------------------------------------------------------------------- Ran 1 test in 0.016s OK
Отлично, то что надо.
Установка Flaskr
Добавим структуру
Добавим пару папок, "static" и "templates", в корень нашего проекта. Должна получится вот такая структура:
+-- app-test.py +-- app.py +-- static L-- templates
SQL схема
Создайте новый файл с именем "schema.sql" и добавьте в него следующий код:
drop table if exists entries; create table entries ( id integer primary key autoincrement, title text not null, text text not null );
Это позволит создать таблицу с тремя полями — "id", "title" и "text". SQLite будет использоваться для наших СУБД, поскольку SQLite встроен в стандартную библиотеку Python и не требует настройки.
Второй тест
Давайте создадим базовый файл для запуска нашего приложения. Однако сначала нам нужно написать тест для него.
Просто изменим наш app-test.py из первого теста:
from app import app import unittest class BasicTestCase(unittest.TestCase): def test_index(self): tester = app.test_client(self) response = tester.get('/', content_type='html/text') self.assertEqual(response.status_code, 404) if __name__ == '__main__': unittest.main()
Итак, мы ожидаем получить код 404 (error). Запустите тест. Тест не прошел. Почему тест был провален? Все просто. Мы ожидали 404, но на самом деле мы получаем назад код 200 с этого маршрута.
Изменим app.py:
# imports import sqlite3 from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash, jsonify # configuration DATABASE = 'flaskr.db' DEBUG = True SECRET_KEY = 'my_precious' USERNAME = 'admin' PASSWORD = 'admin' # create and initialize app app = Flask(__name__) app.config.from_object(__name__) if __name__ == '__main__': app.run()
Здесь мы импортируем необходимые модули, создаем раздел конфигурации для глобальных переменных, инициализируем и затем запускаем приложение.
Итак, запустим его:
$ python app.py
Запускаем сервер. Вы должны увидеть сообщение об ошибке 404, при обращении к маршруту "/", так как маршрута нет и его представления не существует. Вернемся к терминалу. Остановим сервер разработки. Теперь запустим модульный тест. Он должен пройти без ошибок.
Установка базы данных
Наша цель — создать подключение к базе данных, создать базу данных на основе схемы, если она еще не существует, затем закрывать соединение каждый раз после запуска теста.
Как мы можем проверить существование файла базы данных? Обновим наш app-test.py:
import unittest import os from app import app class BasicTestCase(unittest.TestCase): def test_index(self): tester = app.test_client(self) response = tester.get('/', content_type='html/text') self.assertEqual(response.status_code, 404) def test_database(self): tester = os.path.exists("flaskr.db") self.assertTrue(tester) if __name__ == '__main__': unittest.main()
Запустите его, чтобы убедиться, что тест терпит неудачу, показывая, что база данных не существует.
Теперь добавьте следующий код в app.py:
# connect to database def connect_db(): """Connects to the database.""" rv = sqlite3.connect(app.config['DATABASE']) rv.row_factory = sqlite3.Row return rv # create the database def init_db(): with app.app_context(): db = get_db() with app.open_resource('schema.sql', mode='r') as f: db.cursor().executescript(f.read()) db.commit() # open database connection def get_db(): if not hasattr(g, 'sqlite_db'): g.sqlite_db = connect_db() return g.sqlite_db # close database connection @app.teardown_appcontext def close_db(error): if hasattr(g, 'sqlite_db'): g.sqlite_db.close()
И добавьте функцию
init_db ()
в конецapp.py
, чтобы быть уверенным в том, что мы будем запускать сервер каждый раз с новой базой данных:
if __name__ == '__main__': init_db() app.run()
Теперь можно создать базу данных с помощью Python Shell и импорта и вызова
init_db()
:
>>> from app import init_db >>> init_db()
Закроем оболочку и запустим тест еще раз. Тест пройден? Теперь мы убедились, что база данных была создана.
Шаблоны и представления (Templates and Views )
Далее нам нужно настроить шаблоны и соответствующие представления, которые определяют маршруты. Подумайте об этом с точки зрения пользователя. Нам нужно, чтобы пользователи могли иметь возможность войти в блог и выйти из него. После входа в систему, пользователь должен иметь возможность создать пост. И наконец, мы должны иметь возможность показывать эти посты.
В первую очередь напишем несколько тестов для этого.
Юнит тестирование
Взгляните на окончательный код. Я добавил комментарии для объяснений.
import unittest
import os
import tempfile
import app
class BasicTestCase(unittest.TestCase):
def test_index(self):
"""Начальный тест. Убедимся, что фласк установлен корректно"""
tester = app.app.test_client(self)
response = tester.get('/', content_type='html/text')
self.assertEqual(response.status_code, 200)
def test_database(self):
"""Начальный тест, убеждаемся, что база данных существует"""
tester = os.path.exists("flaskr.db")
self.assertEqual(tester, True)
class FlaskrTestCase(unittest.TestCase):
def setUp(self):
"""Создаем пустую тестовую базу данных"""
self.db_fd, app.app.config['DATABASE'] = tempfile.mkstemp()
app.app.config['TESTING'] = True
self.app = app.app.test_client()
app.init_db()
def tearDown(self):
"""Уничтожаем базу данных после всех тестов"""
os.close(self.db_fd)
os.unlink(app.app.config['DATABASE'])
def login(self, username, password):
"""Вспомогательная функция авторизации"""
return self.app.post('/login', data=dict(
username=username,
password=password
), follow_redirects=True)
def logout(self):
"""Вспомогательная функция выхода из блога"""
return self.app.get('/logout', follow_redirects=True)
# Функции с утверждениями (assert)
def test_empty_db(self):
"""Убедимся, что база данных пуста"""
rv = self.app.get('/')
assert b'No entries here so far' in rv.data
def test_login_logout(self):
"""Протестируем вход и выход юзера"""
rv = self.login(
app.app.config['USERNAME'],
app.app.config['PASSWORD']
)
assert b'You were logged in' in rv.data
rv = self.logout()
assert b'You were logged out' in rv.data
rv = self.login(
app.app.config['USERNAME'] + 'x',
app.app.config['PASSWORD']
)
assert b'Invalid username' in rv.data
rv = self.login(
app.app.config['USERNAME'],
app.app.config['PASSWORD'] + 'x'
)
assert b'Invalid password' in rv.data
def test_messages(self):
"""Убеждаемся, что юзер может оставить сообщение в блоге"""
self.login(
app.app.config['USERNAME'],
app.app.config['PASSWORD']
)
rv = self.app.post('/add', data=dict(
title='<Hello>',
text='<strong>HTML</strong> allowed here'
), follow_redirects=True)
assert b'No entries here so far' not in rv.data
assert b'<Hello>' in rv.data
assert b'<strong>HTML</strong> allowed here' in rv.data
if __name__ == '__main__':
unittest.main()
Если сейчас запустить тесты, все рухнет кроме test_database()`:
python app-test.py
.FFFF
======================================================================
FAIL: test_index (__main__.BasicTestCase)
initial test. ensure flask was set up correctly
----------------------------------------------------------------------
Traceback (most recent call last):
File "app-test.py", line 13, in test_index
self.assertEqual(response.status_code, 200)
AssertionError: 404 != 200
======================================================================
FAIL: test_empty_db (__main__.FlaskrTestCase)
Ensure database is blank
----------------------------------------------------------------------
Traceback (most recent call last):
File "app-test.py", line 51, in test_empty_db
assert b'No entries here so far' in rv.data
AssertionError
======================================================================
FAIL: test_login_logout (__main__.FlaskrTestCase)
Test login and logout using helper functions
----------------------------------------------------------------------
Traceback (most recent call last):
File "app-test.py", line 59, in test_login_logout
assert b'You were logged in' in rv.data
AssertionError
======================================================================
FAIL: test_messages (__main__.FlaskrTestCase)
Ensure that user can post messages
----------------------------------------------------------------------
Traceback (most recent call last):
File "app-test.py", line 84, in test_messages
assert b'<Hello>' in rv.data
AssertionError
----------------------------------------------------------------------
Ran 5 tests in 0.088s
FAILED (failures=4)
Давайте сделаем так, чтобы тесты были пройдены...
Показ записей
Во-первых, добавим представление для отображения записей в app.py:
@app.route('/') def show_entries(): """Searches the database for entries, then displays them.""" db = get_db() cur = db.execute('select * from entries order by id desc') entries = cur.fetchall() return render_template('index.html', entries=entries)
Далее зайдем в папку "templates" и добавим в нее файл index.html такого содержания:
<!DOCTYPE html> <html> <head> <title>Flaskr</title> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}"> </head> <body> <div class="page"> <h1>Flaskr-TDD</h1> <div class="metanav"> {% if not session.logged_in %} <a href="{{ url_for('login') }}">log in</a> {% else %} <a href="{{ url_for('logout') }}">log out</a> {% endif %} </div> {% for message in get_flashed_messages() %} <div class="flash">{{ message }}</div> {% endfor %} {% block body %}{% endblock %} {% if session.logged_in %} <form action="{{ url_for('add_entry') }}" method="post" class="add-entry"> <dl> <dt>Title:</dt> <dd><input type="text" size="30" name="title"></dd> <dt>Text:</dt> <dd><textarea name="text" rows="5" cols="40"></textarea></dd> <dd><input type="submit" value="Share"></dd> </dl> </form> {% endif %} <ul class="entries"> {% for entry in entries %} <li><h2>{{ entry.title }}</h2>{{ entry.text|safe }}</li> {% else %} <li><em>No entries yet. Add some!</em></li> {% endfor %} </ul> </div> </body> </html>
Запустим тест. Должны увидеть следующее:
Ran 5 tests in 0.131s FAILED (failures=2, errors=2)
Авторизация юзеров
Добавим в файл app.py:
@app.route('/login', methods=['GET', 'POST']) def login(): """User login/authentication/session management.""" error = None if request.method == 'POST': if request.form['username'] != app.config['USERNAME']: error = 'Invalid username' elif request.form['password'] != app.config['PASSWORD']: error = 'Invalid password' else: session['logged_in'] = True flash('You were logged in') return redirect(url_for('index')) return render_template('login.html', error=error) @app.route('/logout') def logout(): """User logout/authentication/session management.""" session.pop('logged_in', None) flash('You were logged out') return redirect(url_for('index'))
В приведенной выше функции
login()
есть декоратор, который указывает на то, что маршрут может принять или Get или Post запрос. Проще говоря, запрос на авторизацию начинается от пользователя при доступе к url/login
. Разница между этими типами запросов проста: Get используется для доступа к сайту, а POST используется для отправки информации на сервер. Таким образом, когда пользователь просто обращается к/login
, он использует Get-запрос, но при попытке входа в систему используется Post-запрос.
Добавим в папку template файл "login.html":
<!DOCTYPE html> <html> <head> <title>Flaskr-TDD | Login</title> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}"> </head> <body> <div class="page"> <h1>Flaskr</h1> <div class="metanav"> {% if not session.logged_in %} <a href="{{ url_for('login') }}">log in</a> {% else %} <a href="{{ url_for('logout') }}">log out</a> {% endif %} </div> {% for message in get_flashed_messages() %} <div class="flash">{{ message }}</div> {% endfor %} {% block body %}{% endblock %} <h2>Login</h2> {% if error %} <p class="error"><strong>Error:</strong> {{ error }}</p> {% endif %} <form action="{{ url_for('login') }}" method="post"> <dl> <dt>Username:</dt> <dd><input type="text" name="username"></dd> <dt>Password:</dt> <dd><input type="password" name="password"></dd> <dd><input type="submit" value="Login"></dd> </dl> </form> </div> </body> </html>
Запустим тест еще раз.
Вы все равно должны увидеть некоторые ошибки! Рассмотрим одну из ошибок —
werkzeug.routing.BuildError: Could not build url for endpoint 'index'. Did you mean 'login' instead?
По сути, мы пытаемся обратится к функции
index()
, которая не существует. Переименуйте функциюshow_entries()
в функциюindex()
в файле app.py и запустите тест по новой:
Ran 5 tests in 0.070s FAILED (failures=1, errors=2)
Далее, добавьте в представление функцию добавления записи:
@app.route('/add', methods=['POST']) def add_entry(): """Add new post to database.""" if not session.get('logged_in'): abort(401) db = get_db() db.execute( 'insert into entries (title, text) values (?, ?)', [request.form['title'], request.form['text']] ) db.commit() flash('New entry was successfully posted') return redirect(url_for('index'))
Повторный тест:
Теперь вы должны увидеть это:
====================================================================== FAIL: test_empty_db (__main__.FlaskrTestCase) Ensure database is blank ---------------------------------------------------------------------- Traceback (most recent call last): File "app-test.py", line 49, in test_empty_db assert b'No entries here so far' in rv.data AssertionError ---------------------------------------------------------------------- Ran 5 tests in 0.072s FAILED (failures=1)
Эта ошибка утверждает, что при обращении к маршруту
/
сообщение "No entries here so far" возвращается. Проверьте шаблон index.html. Текст на самом деле гласит: "No entries yet. Add some!". Так обновите же тест и запустите тест вновь:
Ran 5 tests in 0.156s OK
Великолепно.
Добавляем цвета
Сохраните следующие стили в новый файл с именем style.css в папку "static":
body {
font-family: sans-serif;
background: #eee;
}
a, h1, h2 {
color: #377BA8;
}
h1, h2 {
font-family: 'Georgia', serif;
margin: 0;
}
h1 {
border-bottom: 2px solid #eee;
}
h2 {
font-size: 1.2em;
}
.page {
margin: 2em auto;
width: 35em;
border: 5px solid #ccc;
padding: 0.8em;
background: white;
}
.entries {
list-style: none;
margin: 0;
padding: 0;
}
.entries li {
margin: 0.8em 1.2em;
}
.entries li h2 {
margin-left: -1em;
}
.add-entry {
font-size: 0.9em;
border-bottom: 1px solid #ccc;
}
.add-entry dl {
font-weight: bold;
}
.metanav {
text-align: right;
font-size: 0.8em;
padding: 0.3em;
margin-bottom: 1em;
background: #fafafa;
}
.flash {
background: #CEE5F5;
padding: 0.5em;
border: 1px solid #AACBE2;
}
.error {
background: #F0D6D6;
padding: 0.5em;
}
Тест
Запустите приложение, войдите в систему (логин/пароль = "admin"), создайте какой-нибудь пост, и выйдете из блога. Затем выполните тесты, чтобы убедиться, что до сих пор все работает.
jQuery
Теперь добавим немного jQuery, чтобы сделать сайт немного более интерактивным.
Откройте index.html и измените первый
<li
> как-то так:
<li class="entry"><h2 id={{ entry.id }}>{{ entry.title }}</h2>{{ entry.text|safe }}</li>
Теперь мы можем использовать jQuery для каждого
<li>
. Во-первых, нам нужно добавить следующий скрипт в документ непосредственно перед закрывающим тегом Body:
<script src="//code.jquery.com/jquery-1.10.2.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> <script type="text/javascript" src="{{url_for('static', filename='main.js') }}"></script>
Создадим файл main.js в директории "static" и запишем в него следующий код:
$(function() { console.log( "ready!" ); // sanity check $('.entry').on('click', function() { var entry = this; var post_id = $(this).find('h2').attr('id'); $.ajax({ type:'GET', url: '/delete' + '/' + post_id, context: entry, success:function(result) { if(result.status === 1) { $(this).remove(); console.log(result); } } }); }); });
Добавим новую функцию в app.py, чтобы иметь возможность удалять сообщения из базы данных:
@app.route('/delete/<post_id>', methods=['GET']) def delete_entry(post_id): '''Delete post from database''' result = {'status': 0, 'message': 'Error'} try: db = get_db() db.execute('delete from entries where id=' + post_id) db.commit() result = {'status': 1, 'message': "Post Deleted"} except Exception as e: result = {'status': 0, 'message': repr(e)} return jsonify(result)
Наконец, напишем новый тест:
def test_delete_message(self): """Ensure the messages are being deleted""" rv = self.app.get('/delete/1') data = json.loads((rv.data).decode('utf-8')) self.assertEqual(data['status'], 1)
Убедитесь, что вы добавили импорт
import json
Проверьте это вручную, запустив сервер и добавив две новые записи. Нажмите на одну из них. Запись должна быть удалена из dom, а также из базы данных. Дважды проверьте это.
Затем запустите тестирование. Результат тестов должен быть таким:
$ python app-test.py ...... ---------------------------------------------------------------------- Ran 6 tests in 0.132s OK
Развёртывание
Приложение в рабочем состоянии, давайте не будем на этом останавливаться и развернем приложение на Heroku.
Для этого сначала зарегистрируйтесь, а затем установите Heroku Toolbelt.
Далее, установите веб-сервер под названием gunicorn:
$ pip install gunicorn
Создайте Procfile в корне вашего проекта:
$ touch Procfile
Добавьте следующий код:
web: gunicorn app:app
Создайте файл requirements.txt, чтобы указать внешние зависимости, которые должны быть установлены для приложения, чтобы оно работало:
$ pip freeze > requirements.txt
Создайте файл .gitignore:
$ touch .gitignore
И добавьте файлы и папки, которые не должны быть включены в систему контроля версиями:
env *.pyc *.DS_Store __pycache__
Добавим локальный репозитарий:
$ git init $ git add -A $ git commit -m "initial"
Развернем Heroku:
$ heroku create $ git push heroku master $ heroku open
Тест (Опять!)
Запустим тест в облаке. Команда heroku open
откроет приложение в вашем браузере.
Bootstrap
Давайте обновим стили из Bootstrap 3.
Удалите style.css и ссылку на него в index.html и login.html.Затем добавьте этот стиль в обоих файлах
<link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css">
Теперь мы имеем полный доступ ко всем вспомогательным классам Bootstrap.
Замените код в файле login.html на:
<!DOCTYPE html> <html> <head> <title>Flaskr-TDD | Login</title> <link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css"> </head> <body> <div class="container"> <h1>Flaskr</h1> {% for message in get_flashed_messages() %} <div class="flash">{{ message }}</div> {% endfor %} <h3>Login</h3> {% if error %}<p class="error"><strong>Error:</strong> {{ error }}{% endif %}</p> <form action="{{ url_for('login') }}" method="post"> <dl> <dt>Username:</dt> <dd><input type="text" name="username"></dd> <dt>Password:</dt> <dd><input type="password" name="password"></dd> <br><br> <dd><input type="submit" class="btn btn-default" value="Login"></dd> <span>Use "admin" for username and password</span> </dl> </form> </div> <script src="//code.jquery.com/jquery-1.10.2.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> <script type="text/javascript" src="{{url_for('static', filename='main.js') }}"></script> </body> </html>
И измените код в index.html:
<!DOCTYPE html> <html> <head> <title>Flaskr</title> <link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css"> </head> <body> <div class="container"> <h1>Flaskr-TDD</h1> {% if not session.logged_in %} <a href="{{ url_for('login') }}">log in</a> {% else %} <a href="{{ url_for('logout') }}">log out</a> {% endif %} {% for message in get_flashed_messages() %} <div class="flash">{{ message }}</div> {% endfor %} {% if session.logged_in %} <form action="{{ url_for('add_entry') }}" method="post" class="add-entry"> <dl> <dt>Title:</dt> <dd><input type="text" size="30" name="title"></dd> <dt>Text:</dt> <dd><textarea name="text" rows="5" cols="40"></textarea></dd> <br><br> <dd><input type="submit" class="btn btn-default" value="Share"></dd> </dl> </form> {% endif %} <br> <ul class="entries"> {% for entry in entries %} <li class="entry"><h2 id={{ entry.id }}>{{ entry.title }}</h2>{{ entry.text|safe }}</li> {% else %} <li><em>No entries yet. Add some!</em></li> {% endfor %} </ul> </div> <script src="//code.jquery.com/jquery-1.10.2.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> <script type="text/javascript" src="{{url_for('static', filename='main.js') }}"></script> </body> </html>
Проверьте внесенные вами изменения!
SQLAlchemy
Давайте попробуем Flask-SQLAlchemy для лучшего управления нашей базой данных
Установка SQLAlchemy
Запустите установку Flask-SQLAlchemy:
$ pip install Flask-SQLAlchemy
Создайте файл create_db.py и внесите в него нижеследующий код:
# create_db.py from app import db from models import Flaskr # create the database and the db table db.create_all() # commit the changes db.session.commit()
Этот файл будет использован для создания нашей новой базы. Едем дальше, удалим старый (flaskr.db) и schema.sql
Далее добавим в новый файл models.py следующее содержимое, которое генерирует новую схему :
from app import db class Flaskr(db.Model): __tablename__ = "flaskr" post_id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String, nullable=False) text = db.Column(db.String, nullable=False) def __init__(self, title, text): self.title = title self.text = text def __repr__(self): return '<title {}>'.format(self.body)
Обновим app.py
# imports
from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash, jsonify
from flask.ext.sqlalchemy import SQLAlchemy
import os
# grabs the folder where the script runs
basedir = os.path.abspath(os.path.dirname(__file__))
# configuration
DATABASE = 'flaskr.db'
DEBUG = True
SECRET_KEY = 'my_precious'
USERNAME = 'admin'
PASSWORD = 'admin'
# defines the full path for the database
DATABASE_PATH = os.path.join(basedir, DATABASE)
# the database uri
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + DATABASE_PATH
# create app
app = Flask(__name__)
app.config.from_object(__name__)
db = SQLAlchemy(app)
import models
@app.route('/')
def index():
"""Searches the database for entries, then displays them."""
entries = db.session.query(models.Flaskr)
return render_template('index.html', entries=entries)
@app.route('/add', methods=['POST'])
def add_entry():
"""Adds new post to the database."""
if not session.get('logged_in'):
abort(401)
new_entry = models.Flaskr(request.form['title'], request.form['text'])
db.session.add(new_entry)
db.session.commit()
flash('New entry was successfully posted')
return redirect(url_for('index'))
@app.route('/login', methods=['GET', 'POST'])
def login():
"""User login/authentication/session management."""
error = None
if request.method == 'POST':
if request.form['username'] != app.config['USERNAME']:
error = 'Invalid username'
elif request.form['password'] != app.config['PASSWORD']:
error = 'Invalid password'
else:
session['logged_in'] = True
flash('You were logged in')
return redirect(url_for('index'))
return render_template('login.html', error=error)
@app.route('/logout')
def logout():
"""User logout/authentication/session management."""
session.pop('logged_in', None)
flash('You were logged out')
return redirect(url_for('index'))
@app.route('/delete/<int:post_id>', methods=['GET'])
def delete_entry(post_id):
"""Deletes post from database"""
result = {'status': 0, 'message': 'Error'}
try:
new_id = post_id
db.session.query(models.Flaskr).filter_by(post_id=new_id).delete()
db.session.commit()
result = {'status': 1, 'message': "Post Deleted"}
flash('The entry was deleted.')
except Exception as e:
result = {'status': 0, 'message': repr(e)}
return jsonify(result)
if __name__ == '__main__':
app.run()
Обратите внимание на изменения в конфиге в верхней части, а также на средства, через которые мы теперь имеем доступ и управляем базой данных в каждой функции представления — через с SQLAlchemy вместо встроенного SQL.
Создаем базу данных
Запустим команду для создания и инициализации базы данных:
$ python create_db.py
Обновим index.html
Обновим эту строку:
<li class="entry"><h2 id={{ entry.post_id }}>{{ entry.title }}</h2>{{ entry.text|safe }}</li>
Обратите внимание на post_id
. Проверьте базу данных, чтобы убедиться в наличии соответствующего поля.
Тесты
Наконец, обновим наши тесты:
import unittest
import os
from flask import json
from app import app, db
TEST_DB = 'test.db'
class BasicTestCase(unittest.TestCase):
def test_index(self):
"""initial test. ensure flask was set up correctly"""
tester = app.test_client(self)
response = tester.get('/', content_type='html/text')
self.assertEqual(response.status_code, 200)
def test_database(self):
"""initial test. ensure that the database exists"""
tester = os.path.exists("flaskr.db")
self.assertTrue(tester)
class FlaskrTestCase(unittest.TestCase):
def setUp(self):
"""Set up a blank temp database before each test"""
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, TEST_DB)
self.app = app.test_client()
db.create_all()
def tearDown(self):
"""Destroy blank temp database after each test"""
db.drop_all()
def login(self, username, password):
"""Login helper function"""
return self.app.post('/login', data=dict(
username=username,
password=password
), follow_redirects=True)
def logout(self):
"""Logout helper function"""
return self.app.get('/logout', follow_redirects=True)
# assert functions
def test_empty_db(self):
"""Ensure database is blank"""
rv = self.app.get('/')
self.assertIn(b'No entries yet. Add some!', rv.data)
def test_login_logout(self):
"""Test login and logout using helper functions"""
rv = self.login(app.config['USERNAME'], app.config['PASSWORD'])
self.assertIn(b'You were logged in', rv.data)
rv = self.logout()
self.assertIn(b'You were logged out', rv.data)
rv = self.login(app.config['USERNAME'] + 'x', app.config['PASSWORD'])
self.assertIn(b'Invalid username', rv.data)
rv = self.login(app.config['USERNAME'], app.config['PASSWORD'] + 'x')
self.assertIn(b'Invalid password', rv.data)
def test_messages(self):
"""Ensure that user can post messages"""
self.login(app.config['USERNAME'], app.config['PASSWORD'])
rv = self.app.post('/add', data=dict(
title='<Hello>',
text='<strong>HTML</strong> allowed here'
), follow_redirects=True)
self.assertNotIn(b'No entries here so far', rv.data)
self.assertIn(b'<Hello>', rv.data)
self.assertIn(b'<strong>HTML</strong> allowed here', rv.data)
def test_delete_message(self):
"""Ensure the messages are being deleted"""
rv = self.app.get('/delete/1')
data = json.loads(rv.data)
self.assertEqual(data['status'], 1)
if __name__ == '__main__':
unittest.main()
Мы в основном просто обновили setUp()
и tearDown()
методы.
Запустите тесты, а потом проверьте вручную путем запуска сервера и входа и выхода, добавления новых записей и удаления старых записей.
Если все хорошо, обновите требования командой (pip freeze > requirements.txt
), закоммитьте ваш код и затем отправьте новую версию на heroku!
Заключение
- Где взять данный код? Можно здесь.
- Гляньте мое приложение на Heroku. Ура!
- Хотите больше от Flask? Посмотрите на Real Python.
- Хотите что-нибудь еще добавить в это руководство? Добавьте вопрос к репозиторию. Ура!
* От переводчика: Цикл статей на Хабре о Flask
Комментарии (19)
gabirx
10.10.2016 15:35Первый тест, выдает ошибку "ImportError: No module named 'app'
Я так понимаю, до самого теста дело даже не дошло.pcdesign
10.10.2016 15:39Все правильно.
Еще даже приложения app нет с нашим «здравствуй, миром»… И файла app.py нет. Поэтому тест провален.
В статье так об этом и написано, цитата:
If all goes well, this will fail. Если все хорошо, то тест будет провален (fail).
То есть сначала написали тест к несуществующему файлу(функции) и только потом уже пишется сама функция.
herrjemand
10.10.2016 17:59+3Совет для тех кто начинает с Flask. Этот туториал если честно очень паршивый в плане структуры приложения, но хорошо объясняет TDD. По этому мой совет:
Пройти Flask Mega Tutorial — он уже довольно устарел, но части 1я по 4ю очень хорошо описывают как правильно строить приложение. Дальше я бы не стал делать, так как все содержимое после ужасно устарело
EN http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world
RU https://habrahabr.ru/post/193242/
Потом собственно сделать TDD
И потом написать свое приложение со структурой Мега Туториала, но с TDD
bosha
18.10.2016 09:22Я не так давно описывал как структурировать приложения на Flask. Информация свежая, новичкам будет полезно почитать. :)
Akichi
11.10.2016 11:54+1Как раз решил начать проект на Flask. У DigitalOcean есть статья «How To Structure Large Flask Applications», хотел бы задать вопрос к тем, кто тесно работал с этим фреймворком, хорошая ли структура предложения там предлагается и актуальна ли сама информация?
herrjemand
13.10.2016 03:04Такое ощущение что автор статьи «How To Structure Large Flask Applications» никогда до этого не работал с virtualenvironment. Да и в целом там нету ничего нового если смотреть на мега туториаль и тдд.
pcdesign
13.10.2016 20:48herrjemand, что думаете на счет blueprint, который рекомендуют в статье «How To Structure Large Flask Applications»?
herrjemand
14.10.2016 02:24Blueprint это отличная идея. Очень похоже на плагин систему django. Советую почитать официальные доки
http://flask.pocoo.org/docs/0.11/blueprints/pcdesign
14.10.2016 10:05Да, не я знаю что такое blueprint. Просто вижу тьму народу, который с ним мучается, в вопросах на stackoverflow.com и прочих местах, хотя возможно они «не умеют его готовить».
Да и сам как-то побаиваюсь использовать его в продакшене.
ArtVol
11.10.2016 17:58Вопрос сравнения с Джанго уже был. Скажите чем Flask отличается от Bottle, в чем схожесть и различия?
CJay
12.10.2016 22:46мне казалось, что тдд хорошо для модульных тестов, а тут какие то интеграционных тесты получаются. действительно ли необходимы такие тесты, не являются ли они хрупкими?
prostofilya
Есть ли случаи, когда нужно использовать Flask, но нежелательно использовать Django? и наоборот. Чем один хуже/лучше другого?
herrjemand
Flask это Микрофреймворк. Там нету ничего кроме роутера, и шаблонизатора. Бери декоратор и пили свой обработчик. Просто отлично для RESTful api приложений, и в целом для чистой бизнес логики. Но так как это микрофреймворк, тем как авторизация, бд, пейджинг и прочих фишек там нету(но есть плагины). Так что берешь напильник и пилишь.
Django это полноценный фремворк. Там уже есть авторизация, плагин система, пейджинг. Это фактически вордпресс, только больше возможности и больше допиливать.
prostofilya
спасибо!
herrjemand
del