GitHubじゃ!Pythonじゃ!

GitHubからPython関係の優良リポジトリを探したかったのじゃー、でも英語は出来ないから日本語で読むのじゃー、英語社会世知辛いのじゃー

mjhea0

flaskr-tdd – Flaskr:Flask入門、テスト駆動開発(TDD)、JavaScript

投稿日:

Flaskr:Flask入門、テスト駆動開発(TDD)、JavaScript http://flaskr-tdd.herokuapp.com/

Flaskr – フラスコ入門、テスト駆動開発、およびJavaScript

ご存じのように、Flaskr(ミニブログのようなアプリ)は、公式のFlask チュートリアルのために構築したアプリです。 私はチュートリアルを通して、私が認めようと思っている以上に多くの時間を過ごしました。 とにかく、テスト駆動開発(TDD)、JavaScript、およびデプロイメントを追加することで、このチュートリアルをさらに進めたいと思っていました。 この投稿はチュートリアルです。 楽しい。

また、フラスコやウェブ開発全般について全く新しいものであれば、基本的な基本概念を理解することが重要です。

  1. GETリクエストとPOSTリクエストの違いと、アプリケーション内の関数がそれぞれどのように処理するか。
  2. 「要求」と「応答」とは何か。
  3. HTMLページのレンダリングおよび/またはエンドユーザへの返却方法。

:このチュートリアルは、 TestDriven.ioによって強化されています。 Docker、Flask、およびReactMicroservicesを購入して、このオープンソースプロジェクトをサポートしてください。Docker、Flask、およびReactによって提供されるマイクロサービスを構築、テスト、および展開する方法についてはこちらをご覧ください。

あなたが構築しているもの

ログの変更

このチュートリアルは2018年5月10日に最後に更新されました:

  • 05/10/2018 :Python 3.6.5、Flask 1.0.2、Bootstrap 4.1.1に更新されました
  • 10/16/2017 :Python 3.6.2に更新されました
  • 10/16/2017 :ブートストラップ4に更新
  • 10/10/2017 :検索機能が追加されました
  • 07/03/2017 :Python 3.6.1に更新されました
  • 01/24/2016 :Python 3に更新されました! (v3.5.1)
  • 2014年8月24日 :PEP8の更新。
  • 2014年2月25日:SQLAlchemyにアップグレードされました。
  • 2014年2月20日 :AJAXを完了しました。
  • 12/06/2013 :追加されたブートストラップ3のスタイル
  • 11/29/2013 :ユニットテストが更新されました。
  • 11/19/2013 :入力ミスが修正されました。 ユニットテストの更新。
  • 11/11/2013 :リクエストに関する情報を追加しました。

内容

  1. テスト駆動開発?
  2. Pythonをダウンロード
  3. プロジェクトセットアップ
  4. 最初のテスト
  5. Flaskrのセットアップ
  6. 2回目のテスト
  7. データベースのセットアップ
  8. テンプレートとビュー
  9. いくつかの色を追加
  10. テスト
  11. jQuery
  12. 配置
  13. テスト(もう一度!)
  14. ブートストラップ
  15. SQLAlchemy
  16. 検索ページ
  17. 結論

要件

このチュートリアルでは、次の要件を使用します。

  1. Python v3.6.5
  2. フラスコv1.0.2
  3. Flask-SQLAlchemy v2.3.2
  4. ガンコーンv19.8.1

テスト駆動開発?

テスト駆動開発(TDD)は、機能の実際の機能を記述する前に自動化されたテストを書くことを重視する反復開発サイクルです。 言い換えれば、TDDはビルディングとテストを組み合わせています。 このプロセスはコードの正確さを保証するだけでなく、プロジェクトの設計とアーキテクチャを間接的に進化させるのに役立ちます。

上記の図に示すように、TDDは通常、「赤 – 緑 – リファクタ」サイクルに従います。

  1. テストを書く
  2. テストを実行する(失敗するはずです)
  3. テストが合格するのに十分なコードを書く
  4. コードをリファクタリングして、何度も再テストします(必要な場合)

Pythonをダウンロード

開始する前に、 Python 3.6の最新バージョンがインストールされていることを確認してください 。これはhttp://www.python.org/download/からダウンロードできます

:このチュートリアルでは、Python v3.6.5を使用しています。

Pythonと並んで、これはまた、

  • pip – Pythonのパッケージ管理システム。RubyとNodeのgemやnpmに似ています。
  • venv – 開発用の独立した環境を作成するために使用されます。 これは標準的な方法です。 常に、常に、常に仮想環境を利用しています。 そうしないと、最終的に依存関係の競合に関する問題が発生します。

プロジェクトセットアップ

  1. プロジェクトを保存するための新しいディレクトリを作成します。

    $ mkdir flaskr-tdd
    $ cd flaskr-tdd
  2. 仮想環境を作成して有効にする:

    $ python3 -m venv env
    $ source env/bin/activate

    :仮想環境では、 “env”が端末の$(env)$の前に表示されていることを知っています。 仮想環境を終了するには、 deactivateコマンドをdeactivateます。 プロジェクトディレクトリに戻り、 source env/bin/activateを実行して、再度source env/bin/activateます。

  3. pipと一緒にFlaskをインストールしてください:

    (env)$ pip install flask==1.0.2

最初のテスト

シンプルな「こんにちは、世界」アプリから始めましょう。

  1. テストファイルを作成する:

    (env)$ touch app-test.py

    このファイルをお気に入りのテキストエディタで開きます。 (私はSublimeを使用します)。次のコードを追加します。

    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!」であるかどうかをテストしています。 表示されています。

  1. テストを実行します。

    (env)$ python app-test.py

    すべてうまくいけば、これは失敗します:

    ModuleNotFoundError: No module named 'app'
  2. これでパスするコードを追加します。

    (env)$ touch app.py

    コード:

    from flask import Flask
    
    app = Flask(__name__)
    
    
    @app.route('/')
    def hello():
        return 'Hello, World!'
    
    
    if __name__ == '__main__':
        app.run()
  3. アプリを実行する:

    (env)$ python app.py

    次に、選択したブラウザでhttp:// localhost:5000 /に移動します。 「こんにちは、世界!」と表示されます。 あなたの画面に。

    ターミナルに戻ります。 Ctrl + Cでサーバーを終了します。

  4. もう一度テストを実行します。

    (env)$ python app-test.py
    .
    ----------------------------------------------------------------------
    Ran 1 test in 0.010s
    
    OK

    ニース。

Flaskrのセットアップ

  1. 構造を追加する

    プロジェクトルートに「静的」と「テンプレート」の2つのフォルダを追加します。 ファイル構造は次のようになります。

    ├── 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”の3つのフィールドを持つ単一のテーブルを設定します。 標準のPythonライブラリに組み込まれているため、SQLiteはRDMSに使用され、設定は不要です。

2回目のテスト

アプリケーションを実行するための基本ファイルを作成しましょう。 その前に、まずテストを書く必要があります。

  1. 単に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エラーが予想されます。 テストを実行します。 これは失敗します。 どうして? 私たちは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. それを実行します:

    (env)$ python app.py

    サーバーを起動します。 ルートまたはビューが設定されていないため、404エラーが表示されます。 ターミナルに戻ります。 サーバーを停止します。 今すぐテストを実行します。 それは成功するはずです。

データベースのセットアップ

基本的には、データベース接続を開き、スキーマに基づいてデータベースを作成していない場合は作成し、テストが実行されるたびに接続を終了します。

  1. ファイルの存在をどのようにテストするのですか? 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()

    データベースが存在しないことを示す、失敗したことを確認するために実行します。

  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()の一番下にinit_db()関数を追加して、 init_db()新しいデータベースでサーバを起動するようにしてください:

    if __name__ == '__main__':
        init_db()
        app.run()

    これで、Pythonシェルを起動し、 init_db()関数をインポートして呼び出すことで、データベースを作成することができます:

    >>> from app import init_db
    >>> init_db()

    シェルを閉じてから、もう一度テストを実行してください。 それは合格ですか? これで、データベースが作成されたことがわかりました。

テンプレートとビュー

次に、経路を定義するテンプレートと関連するビューを設定する必要があります。 ユーザーの立場から考えてみましょう。

  1. ユーザーはログインとログアウトができる必要があります。
  2. ログインしたら、ユーザーは投稿できる必要があります。
  3. 最後に、ユーザーは投稿を閲覧できる必要があります。

最初にいくつかのテストを書いてください。

テスト

最終的なコードを見てください。 説明のためにドキュメントストリングを追加しました。

import unittest
import os
import tempfile

import app


class BasicTestCase(unittest.TestCase):

    def test_index(self):
        """Initial test: Ensure flask was set up correctly."""
        tester = app.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.assertEqual(tester, True)


class FlaskrTestCase(unittest.TestCase):

    def setUp(self):
        """Set up a blank temp database before each test."""
        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):
        """Destroy blank temp database after each test."""
        os.close(self.db_fd)
        os.unlink(app.app.config['DATABASE'])

    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('/')
        assert b'No entries here so far' in rv.data

    def test_login_logout(self):
        """Test login and logout using helper functions."""
        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):
        """Ensure that a user can post messages."""
        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'&lt;Hello&gt;' in rv.data
        assert b'<strong>HTML</strong> allowed here' in rv.data


if __name__ == '__main__':
    unittest.main()

今すぐテストを実行する場合:

(env)$ python app-test.py

test_database()を除いて、すべてが失敗します:

.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'&lt;Hello&gt;' in rv.data
AssertionError

----------------------------------------------------------------------
Ran 5 tests in 0.027s

FAILED (failures=4)

これらを一度に1つずつグリーンにしましょう…

エントリーを表示する

  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. 次に、 index.htmlテンプレートを「templates」フォルダに追加します。

    <!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>
  3. 今すぐテストを実行してください。 君は見るべきだ:

    Ran 5 tests in 0.048s
    
    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要求のいずれかを受け入れることができることを示します。 簡単に言えば、リクエストはエンドユーザが/login URLにアクセスするときに開始され/login これらの要求の違いは簡単です.GETはWebページにアクセスするために使用され、POSTは情報がサーバーに送信されるときに使用されます。 したがって、ユーザーが/login urlにアクセスするとGET要求が使用されますが、ログインしようとするとPOST要求が使用されます。

  2. テンプレートを追加する – 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. もう一度テストを実行します。

    あなたはまだいくつかのエラーが表示されるはずです! エラーの1つを見てください – werkzeug.routing.BuildError: Could not build url for endpoint 'index'. Did you mean 'login' instead? werkzeug.routing.BuildError: Could not build url for endpoint 'index'. Did you mean 'login' instead?

    基本的に、存在しないindex()関数にリダイレクトしようとしています。 show_entries()関数の名前をindex()変更してから 、再度テストします。

    Ran 5 tests in 0.048s
    
    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 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
    
    ----------------------------------------------------------------------
    Ran 5 tests in 0.068s
    
    FAILED (failures=1)

    このエラーは、ルート/がヒットしたときに、「ここまでのエントリがありません」というメッセージが返されることを示しています。 index.htmlテンプレートを確認してください。 メッセージは実際には「まだエントリがありません。いくつか追加してください!」と表示されます。 したがって、テストを更新して再テストしてください:

    Ran 5 tests in 0.055s
    
    OK

    完璧。

いくつかの色を追加

次のスタイルを “static”フォルダ内のstyle.cssという新しいファイルに保存します。

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;
}

テスト

あなたのアプリを実行し、ログインする(username / password = “admin”)、投稿を追加する、ログアウトする。

JavaScript

次に、サイトを少しだけインタラクティブにするJavaScriptを追加しましょう。

  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-2.2.4.min.js"></script>
    <script src="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
    <script type="text/javascript" src="{{url_for('static', filename='main.js') }}"></script>
  2. “static”ディレクトリにmain.jsファイルを作成し、次のコードを追加します:

    $(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 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)
  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インポートしてください。

    サーバーを実行し、2つの新しいエントリを追加して、これを手動でテストします。 そのうちの1つをクリックします。 これは、DOMとデータベースの両方から削除する必要があります。 これをもう一度チェックしてください。

    次に、自動テストスイートを実行します。 それは渡す必要があります:

    ......
    ----------------------------------------------------------------------
    Ran 6 tests in 0.062s
    
    OK

配置

アプリが稼動状態になったら、歯車をシフトさせてアプリをHerokuに導入しましょう。

  1. これを行うには、まずサインアップし、 Heroku CLIをインストールします

  2. 次に、 gunicornと呼ばれるプロダクショングレードのWebサーバーをインストールします。

    (env)$ pip install gunicorn==19.8.1
  3. プロジェクトルートにプロジェクトファイルを作成する:

    (env)$ touch Procfile

    そして次のコードを追加します:

    web: gunicorn app:app
  4. アプリケーションが動作するためにインストールする必要のある外部依存関係を指定するためのrequirements.txtファイルを作成します

    (env)$ pip freeze > requirements.txt
  5. プロジェクトルートに.gitignoreファイルを作成します:

    (env)$ touch .gitignore

    また、以下のファイルとフォルダをインクルードしてください(バージョンコントロールには含まれていません)。

    env
    *.pyc
    *.DS_Store
    __pycache__
  6. ローカルのGitリポジトリを追加する:

    (env)$ git init
    (env)$ git add -A
    (env)$ git commit -m "initial"
  7. 正しいPythonランタイムを指定するには、 runtime.txtというプロジェクトルートに新しいファイルを追加します。

    python-3.6.5
    
  8. Herokuに展開する:

    (env)$ heroku create
    (env)$ git push heroku master

テスト(もう一度!)

これを雲の中でテストしましょう。 heroku openてブラウザでアプリを開きます。

ブートストラップ

ブートストラップ4でスタイルを更新しましょう。

  1. まず、 index.htmllogin.htmlの両方からstyle.cssスタイルシートを削除します。 次に、このスタイルシートを両方のファイルに追加します。

    <link rel="stylesheet" type="text/css" href="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css">

    これで、すべてのBootstrapヘルパークラスにフルアクセスできました。

  2. login.htmlのコードを次のように置き換えます。

    <!DOCTYPE html>
    <html>
    <head>
      <title>Flaskr-TDD | Login</title>
      <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css">
    </head>
    <body>
    
      <div class="container">
    
        <h1>Flaskr</h1>
    
        <br><br>
    
        {% for message in get_flashed_messages() %}
          <div class="flash alert alert-success col-sm-4" role="success">{{ message }}</div>
        {% endfor %}
    
        <h3>Login</h3>
    
        {% if error %}<p class="alert alert-danger col-sm-4" role="danger"><strong>Error:</strong> {{ error }}{% endif %}</p>
        <form action="{{ url_for('login') }}" method="post" class="form-group">
          <dl>
            <dt>Username:</dt>
            <dd><input type="text" name="username" class="form-control col-sm-4"></dd>
            <dt>Password:</dt>
            <dd><input type="password" name="password" class="form-control col-sm-4"></dd>
            <br><br>
            <dd><input type="submit" class="btn btn-primary" value="Login"></dd>
            <span>Use "admin" for username and password</span>
          </dl>
        </form>
    
      </div>
    
      <script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
      <script src="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/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="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css">
    </head>
    <body>
    
      <div class="container">
    
        <h1>Flaskr-TDD</h1>
    
        {% if not session.logged_in %}
          <a class="btn btn-success" role="button" href="{{ url_for('login') }}">log in</a>
        {% else %}
          <a class="btn btn-warning" role="button" href="{{ url_for('logout') }}">log out</a>
        {% endif %}
    
        <br><br>
    
        {% for message in get_flashed_messages() %}
          <div class="flash alert alert-success col-sm-4" role="success">{{ message }}</div>
        {% endfor %}
    
        {% if session.logged_in %}
          <form action="{{ url_for('add_entry') }}" method="post" class="add-entry form-group">
            <dl>
              <dt>Title:</dt>
              <dd><input type="text" size="30" name="title" class="form-control col-sm-4"></dd>
              <dt>Text:</dt>
              <dd><textarea name="text" rows="5" cols="40" class="form-control col-sm-4"></textarea></dd>
              <br><br>
              <dd><input type="submit" class="btn btn-primary" 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-2.2.4.min.js"></script>
      <script src="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
      <script type="text/javascript" src="{{url_for('static', filename='main.js') }}"></script>
    
    </body>
    </html>
  4. アプリをローカルで実行する:

    (env)$ python app.py

    ブラウザで変更をチェックしてください!

SQLAlchemy

データベースをより良く管理するために、 Flask-SQLAlchemyにアップグレードしましょう。

SQLAlchemyのセットアップ

  1. Flask-SQLAlchemyをインストールします。

    $ pip install Flask-SQLAlchemy==2.3.2
  2. 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()

    このファイルは、新しいデータベースの作成に使用されます。 古い.dbflaskr.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
import os

from flask import Flask, request, session, g, redirect, url_for, \
     abort, render_template, flash, jsonify
from flask_sqlalchemy import SQLAlchemy


# get the folder where this file runs
basedir = os.path.abspath(os.path.dirname(__file__))

# configuration
DATABASE = 'flaskr.db'
DEBUG = True
SECRET_KEY = 'my_precious'
USERNAME = 'admin'
PASSWORD = 'admin'

# define the full path for the database
DATABASE_PATH = os.path.join(basedir, DATABASE)

# database config
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + DATABASE_PATH
SQLALCHEMY_TRACK_MODIFICATIONS = False

# 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()

上のconfigの変更と、各ビュー関数でデータベースにアクセスして操作する手段(バニラSQLの代わりにSQLAlchemyを使用)を確認してください。

DBを作成する

次のコマンドを実行して、初期データベースを作成します。

(env)$ python create_db.py

index.htmlを更新

この行を更新してください:

<li class="entry"><h2 id="{{ entry.post_id }}">{{ entry.title }}</h2>{{ entry.text|safe }}</li>

post_id注意してpost_id データベースをチェックして、一致するフィールドがあることを確認します。

テスト

最後に、テストを更新します。

import unittest
import os
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'&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()

We’ve mostly just updated the setUp() and tearDown() methods.

Run the tests, and then manually test it by running the server and logging in and out, adding new entries, and deleting old entries.

If all is well, update your requirements ( pip freeze > requirements.txt ) commit your code, then PUSH the new version to Heroku!

検索ページ

Let’s add a search page to our blog. It will be a nice feature that will come in handy after we have a number of blog posts.

Update app.py

@app.route('/search/', methods=['GET'])
def search():
    query = request.args.get("query")
    entries = db.session.query(models.Flaskr)
    if query:
        return render_template('search.html', entries=entries, query=query)
    return render_template('search.html')

NOTE : Be sure to write a test for this on your own!

Add search.html

In the “templates” folder create a new file called search.html :

(env)$ touch search.html

Now add the following code to search.html :

<!DOCTYPE html>
<html>
<head>
  <title>Flaskr</title>
  <link rel="stylesheet" type="text/css" href="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css">
</head>
<body>

  <div class="container">

    <h1>Flaskr-TDD</h1>

    <a class="btn btn-primary" role="button" href="{{ url_for('index') }}"> Home </a>

    {% if not session.logged_in %}
      <a class="btn btn-success" role="button" href="{{ url_for('login') }}">log in</a>
    {% else %}
      <a class="btn btn-warning" role="button" href="{{ url_for('logout') }}">log out</a>
    {% endif %}

    <br><br>

    {% for message in get_flashed_messages() %}
      <div class="flash alert alert-success col-sm-4" role="success">{{ message }}</div>
    {% endfor %}

    <form action="{{ url_for('search') }}" method="get" class="from-group">
      <dl>
        <dt>Search:</dt>
        <dd><input type="text" name="query" class="form-control col-sm-4" ></dd>
        <br>
        <dd><input type="submit" class="btn btn-info" value="Search" ></dd>
      </dl>
    </form>

    <ul class="entries">
      {% for entry in entries %}
        {% if query.lower() in entry.title.lower() or query.lower() in entry.text.lower() %}
        <li class="entry"><h2 id="{{ entry.post_id }}">{{ entry.title }}</h2>{{ entry.text|safe }}</li>
        {% endif %}
      {% endfor %}
    </ul>


  </div>

  <script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
  <script src="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
  <script type="text/javascript" src="{{url_for('static', filename='main.js') }}"></script>

</body>
</html>

Update index.html

Add a search button for better navigation just below <h1>Flaskr-TDD</h1> :

<a class="btn btn-info" role="button" href="{{ url_for('search') }}">Search</a>

Test it out locally. If all is well, commit your code and update the version on Heroku.

結論

  1. Want my code? ここにそれをつかみなさい。
  2. View my app on Heroku . 乾杯!
  3. Want more Flask fun? Check out Microservices with Docker, Flask, and React .
  4. Want something else added to this tutorial? Add an issue to the repo. 乾杯!







-mjhea0

執筆者: