«Flaskr» — введення у Flask, розробка через тестування (TDD) і jQuery

Flask – це чудовий мікро веб фреймворк, заснований на Python. Flaskr – це миниблог, який описаний в офіційному керівництві Flask. Я продирався через це керівництво більше, ніж можу в цьому зізнатися. Тим не менше, я хотів би взяти це керівництво для наступного кроку, додавши в нього розроблення через тестування (test driven development) і трошки jQuery.
Якщо ви новачок у Flask і/або веб-розробки в цілому, важливо розуміти ці основні фундаментальні поняття:
  1. Різницю між Get і Post запитом, і які функції в додатку їх обробляють.
  2. Що таке request (Запит).
  3. Як HTML-сторінки відображаються і повертаються відвідувачу у браузері.
**Примітка**: цей посібник представлено проектом realpython.com. Будь ласка, підтримайте цей проект з відкритим вихідним кодом, купуючи наші курси www.realpython.com/courses з навчанням Python і веб-розробки з Django і flask!
Зміст
  1. Розробка через тестування?
  2. Завантажуємо Python
  3. Установка проекту
  4. Перший тест
  5. Установка Flaskr
  6. Другий тест
  7. Установка бази даних
  8. Шаблони і вистави(Views)
  9. Додаємо кольору
  10. Тест
  11. jQuery
  12. Розгортання
  13. Ще тест!
  14. Bootstrap
  15. SQLAlchemy
  16. Висновок
Вимоги
Керівництво використовує наступне:
  1. Python v3.5.1
  2. Flask v0.10.1
  3. Flask-SQLAlchemy v2.1
  4. gunicorn v19.4.5
Розробка через тестування?
tdd
Розробка через тестування (tdd) — це вид розробки, який передбачає написання автоматичних тестів перед написанням самої функції. Іншими словами, це комбінація випробування і написання коду. Цей процес не тільки допомагає забезпечити коректність коду, але також дозволяє розвивати дизайн і архітектуру проекту під постійним контролем.
TDD зазвичай слід схемою "Червоний-Зелений-Рефакторинг" як показано на картинці вище:
  1. Написати тест
  2. Виконати тест (і потерпіти невдачу)
  3. Написати код, щоб тест був пройдений
  4. Рефакторинг коду і повторне випробування знову і знову (при необхідності)
Завантажуємо Python
Перед початком переконайтеся, що у вас встановлена остання версія Пітон 3.5, який ви можете завантажити з http://www.python.org/download/
Примітка: це керівництво використовує Python V 3.5.1.
Разом з Python також треба поставити:
  • pip — це система управління пакетами в Python, схожа на gem або npm в Ruby або Node, відповідно.
  • pyvenv — використовується для створення ізольованою середовища в розробці. Це стандартна практика. Завжди, завжди і ще раз завжди використовуйте віртуальні середовища. Якщо ви цього не зробите, то в кінцевому підсумку зіткнетеся з проблемами сумісності між різними залежностями.
Установка проекту
  1. Створіть нову папку для збереження проекту:
    $ mkdir flaskr-tdd
    $ cd flaskr-tdd

  2. Створіть і активуйте віртуальне оточення:
    $ pyvenv-3.5 env
    $ source env/bin/activate

    Примітка:
    Коли ви перебуваєте усередині віртуального оточення, в терміналі до позначки $ показується напис (env). Для виходу з віртуального середовища використовуйте команду
    deactivate
    , а при необхідності її активувати перейдіть в потрібний каталог і запустіть команду
    source env/bin/activate
    .
  3. Установка Flask з допомогою pip:
    $ pip3 install Flask

Перший тест
Давайте почнемо з простої програми "hello, world".
  1. Створюємо тестовий файл:
    $ touch app-test.py

    Відкрийте цей файл у вашому улюбленому текстовому редакторі. Додайте у файл app-test.py наступні рядки:
    from app app import
    
    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'c Hello, World!')
    
    if __name__ == '__main__':
    unittest.main()

По суті, ми перевіряємо, чи прийде до нас відповідь з кодом "200" і відображається "hello, world" .
  1. Запустимо тест:
    $ python app-test.py

    Якщо все добре, то тест буде провалено (fail).
  2. Тепер додамо наступні рядки в файл app.py щоб успішно пройти тест.
    $ touch app.py

    Код:
    from flask import Flask
    
    app = Flask(__name__)
    
    @app.route("/")
    def hello():
    return "Hello, World!"
    
    if __name__ == "__main__":
    app.run()

  3. Запустимо наш app:
    $ python app.py

    Звернемося за адресою http://localhost:5000/. Ви побачите рядок "Hello, World!" на вашому екрані.
    Повернемося в термінал і зупинимо сервер розробки з допомогою Ctrl+C.
  4. Запустимо наш тест знову:
    $ python app-test.py
    .
    ----------------------------------------------------------------------
    Ran 1 test in 0.016 s
    
    OK

    Відмінно, то що треба.
Установка Flaskr
  1. Додамо структуру
    Додамо пару папок, "static" і "templates", в корінь нашого проекту. Повинна вийде ось така структура:
    ├── app-test.py
    ├── app.py
    ├── static
    └── templates

  2. 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 і не вимагає установки.
Другий тест
Давайте створимо базовий файл для запуску додатку. Проте спочатку нам потрібно написати тест для нього.
  1. Просто змінимо наш app-test.py з першого тесту:
    from app app import
    
    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 з цього маршруту.
  2. Змінимо 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()

    Тут ми імпортуємо необхідні модулі, створюємо розділ конфігурації для глобальних змінних, ініціалізуємо і потім запускаємо програму.
  3. Отже, запустимо його:
    $ python app.py

    Запускаємо сервер. Ви повинні побачити повідомлення про помилку 404, при зверненні до маршруту "/", так як маршруту немає і його подання не існує. Повернемося до терміналу. Зупинимо сервер розробки. Тепер запустимо модульний тест. Він повинен пройти без помилок.
Установка бази даних
Наша мета — створити підключення до бази даних, створити базу даних на основі схеми, якщо вона ще не існує, потім закривати з'єднання кожного разу після запуску тесту.
  1. Як ми можемо перевірити існування файлу бази даних? Оновимо наш app-test.py:
    import unittest
    import os
    from app app import
    
    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()

    Запустіть його, щоб переконатися, що тест терпить невдачу, показуючи, що база даних не існує.
  2. Тепер додайте наступний код 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'c 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'c You were logged in' in rv.data
rv = self.logout)
assert b'c You were logged out' in rv.data
rv = self.login(
app.app.config['USERNAME'] + 'x',
app.app.config['PASSWORD']
)
assert b'c Invalid username' in rv.data
rv = self.login(
app.app.config['USERNAME'],
app.app.config['PASSWORD'] + 'x'
)
assert b'c 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'c No entries here so far' not in rv.data
assert b'&lt;Hello&gt;' 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 blank database is
----------------------------------------------------------------------
Traceback (most recent call last):
File "app-test.py", line 51, in test_empty_db
assert b'c 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'c 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'&lt;Hello&gt;' in rv.data
AssertionError

----------------------------------------------------------------------
Ran 5 tests in 0.088 s

FAILED (failures=4)

Давайте зробимо так, щоб тести були пройдені...
Показ записів
  1. По-перше, додамо подання для відображення записів в 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)

  2. Далі заходимо в папку "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">
    {% entry for entries in %}
    <li><h2>{{ entry.title }}</h2>{{ entry.text|safe }}</li>
    {% else %}
    <li><em>No entries yet. Add some!</em></li>
    {% endfor %}
    </ul>
    
    </div>
    
    </body>
    </html>

  3. Запустимо тест. Повинні побачити наступне:
    Ran 5 tests in 0.131 s
    
    FAILED (failures=2, errors=2)

Авторизація юзерів
  1. Додамо в файл 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 запит.
  2. Додамо в папку 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>

  3. Запустимо тест ще раз.
    Ви все одно повинні побачити деякі помилки! Розглянемо одну з помилок —
    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.070 s
    
    FAILED (failures=1, errors=2)

  4. Далі, додайте до подання функцію додавання запису:
    @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'))

  5. Повторний тест:
    Тепер ви повинні побачити це:
    ======================================================================
    FAIL: test_empty_db (__main__.FlaskrTestCase)
    Ensure blank database is
    ----------------------------------------------------------------------
    Traceback (most recent call last):
    File "app-test.py", line 49, in test_empty_db
    assert b'c No entries here so far' in rv.data
    AssertionError
    
    ----------------------------------------------------------------------
    Ran 5 tests in 0.072 s
    
    FAILED (failures=1)

    Ця помилка стверджує, що при зверненні до маршруту
    /
    повідомлення "No entries here so far" повертається. Перевірте шаблон index.html. Текст насправді говорить: "No entries yet. Add some!". Так оновіть ж тест і запустіть тест знову:
    Ran 5 tests in 0.156 s
    
    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.2 em;
}

.page {
margin: 2em auto;
width: 35em;
border: 5px solid #ccc;
padding: 0.8 em;
background: white;
}

.entries {
list-style: none;
margin: 0;
padding: 0;
}

.entries li {
margin: 0.8 em 1.2 em;
}

.entries li h2 {
margin-left: -1em;
}

.add-entry {
font-size: 0.9 em;
border-bottom: 1px solid #ccc;
}

.add-entry dl {
font-weight: bold;
}

.metanav {
text-align: right;
font-size: 0.8 em;
padding: 0.3 em;
margin-bottom: 1em;
background: #fafafa;
}

.flash {
background: #CEE5F5;
padding: 0.5 em;
border: 1px solid #AACBE2;
}

.error {
background: #F0D6D6;
padding: 0.5 em;
}

Тест
Запустіть додаток, увійдіть в систему (логін/пароль = "admin"), створити який-небудь пост, і вийдете з блогу. Потім виконайте тести, щоб переконатися, що досі все працює.
jQuery
Тепер додамо трохи jQuery, щоб зробити сайт трохи більш інтерактивним.
  1. Відкрийте 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>

  2. Створимо файл 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);
    }
    }
    });
    });
    
    });

  3. Додамо нову функцію 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 entries from where id=' + post_id)
    db.commit()
    result = {'status': 1, 'message': "Post Deleted"}
    except as Exception e:
    result = {'status': 0, 'message': repr(e)}
    
    return jsonify(result)

  4. Нарешті, напишемо новий тест:
    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.132 s
    
    OK

Розгортання
Додаток в робочому стані, давайте не будемо на цьому зупинятися і розгорнемо додаток на Heroku.
  1. Для цього спочатку зареєструватися, а потім виберіть Heroku Toolbelt.
  2. Далі, встановіть веб-сервер під назвою gunicorn:
    $ pip install gunicorn

  3. Створити Procfile в корені вашого проекту:
    $ touch Procfile

    Додайте наступний код:
    web: gunicorn app:app

  4. Створіть файл requirements.txt, щоб вказати зовнішні залежності, які повинні бути встановлені для програми, щоб воно працювало:
    $ pip freeze > requirements.txt

  5. Створіть файл .gitignore:
    $ touch .gitignore

    І додайте файли і папки, які не повинні бути включені в систему контролю версій:
    env
    *.pyc
    *.DS_Store
    __pycache__

  6. Додамо локальний репозиторій:
    $ git init
    $ git add -A
    $ git commit -m "initial"

  7. Розгорнемо Heroku:
    $ heroku create
    $ git push heroku master
    $ heroku open

Тест (Знову!)
Запустимо тест в хмарі. Команда
heroku open
відкриє додаток у вашому браузері.
Bootstrap
Давайте поновимо стилі з Bootstrap 3.
  1. Видалити 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.
  2. Замініть код у файлі 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>

  3. І змінити код 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">
    {% entry for entries in %}
    <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
  1. Запустіть установку Flask-SQLAlchemy:
    $ pip install Flask-SQLAlchemy

  2. Створіть файл create_db.py і внесіть в нього такий код:
    # create_db.py
    
    from app import db
    from models import Flaskr
    
    # create the database and the table db
    db.create_all()
    
    # commit the changes
    db.session.commit()

    Цей файл буде використано для створення нової бази. Їдемо далі, видалимо старий (flaskr.db) і schema.sql
  3. Далі додамо в новий файл 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 as Exception 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)

# functions assert

def test_empty_db(self):
"""Ensure database is blank"""
rv = self.app.get('/')
self.assertIn(b'c 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'c You were logged in', rv.data)
rv = self.logout)
self.assertIn(b'c You were logged out', rv.data)
rv = self.login(app.config['USERNAME'] + 'x', app.config['PASSWORD'])
self.assertIn(b'c Invalid username', rv.data)
rv = self.login(app.config['USERNAME'], app.config['PASSWORD'] + 'x')
self.assertIn(b'c 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'c No entries here so far', rv.data)
self.assertIn(b'&lt;Hello&gt;', 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!
Висновок
  1. Де взяти цей код? Можна тут.
  2. Гляньте моє додаток на Heroku. Ура!
  3. Хочете більше від Flask? Подивіться на Real Python.
  4. Хочете ще що-небудь додати в це керівництво? Додайте питання до дерева. Ура!
* Від перекладача: Цикл статей на Хабре про Flask
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.