Flaskを用いたウェブアプリケーション作成③

# 9. CRUD

1. **Create** : 記事の新規作成機能
2. **Read** : 記事一覧表示機能、記事の詳細表示機能
3. **Update** : 記事更新機能
4. **Delete** : 記事削除機能
#### この**CRUD**を満たすことで一通りの機能を作成しました。
## 10. views.pyファイル分割
CRUDの機能を追加していく前に、ソースを整理するためviews.pyを**cafe.py**, **loging.py**, **review.py**の3つに分割しました。
**Blueprint**を用いて分割したファイルをそれぞれアプリケーション化しました。

```python:./cafe_site/views/cafe.py
from flask import Blueprint

cafe = Blueprint('cafe', __name__)
```
```python:./cafe_site/views/loging.py
from flask import Blueprint

loging = Blueprint('loging', __name__)
```
```python:./cafe_site/views/review.py
from flask import Blueprint

review = Blueprint('review', __name__)
```
アプリケーション名をappではなく**cafe**, **loging**, **review**に変更し、ルーティングも`@cafe.route()` `@loging.route()` `@review.route()`に変更しました。
次に、./cafe_site/\_\_init__.pyファイルの、viewsをインポートしている部分を変更しました。

```python:./__init__.py
from cafe_site.views.cafe import cafe
app.register_blueprint(cafe)

from cafe_site.views.loging import loging
app.register_blueprint(loging)

from cafe_site.views.reviews import review
app.register_blueprint(review, url_prefix='/users')
```
これで今後の記事に関連するCRUD機能はreviews.pyを、ログインに関連する機能はloging.pyを、cafeホームページに関連する機能はcafe.pyをアップデートすればよくなりました。
## 11. ログイン認証のデコレータ作成
最後に、login認証を簡略化するログイン認証デコレータを作成してからCRUDの機能追加に移りたいと思います。

```python:./cafe_site/views/loging.py
from functools import wraps

# ログイン認証デコレータ
def login_required(loging):
    @wraps(loging)
    def inner(*args, **kwargs):
        if not session.get('logged_in'):
            return redirect(url_for('loging.login'))
        return loging(*args, **kwargs)
    return inner
```
ログインが必要な処理については、ビューメソッドの直前に`@login_required`の１文を加えるだけでログイン認証ができます。



# 12. 記事投稿機能作成(Create)
[Flaskを用いてのウェブアプリケーション作成③]()で作成したデータベース機能を使って、記事投稿機能を作成しました。
## 12-1. 新規投稿リンク追加
theme.htmlを編集して新規投稿するための「review」リンクを追加しました。

```html:./cafe_site/templates/theme.html
<ul class="main-nav">
　　　<!-- ログオフ状態であれば新規投稿リンク表示　-->
    {% if not session.logged_in %}

    {% else %}
     <li><a href="{{ url_for('review.new_review') }}">review</a></li>

    {% endif %}
</ul>
```
ファイル全体は以下のようになっています。

```html:./cafe_site/templates/theme.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        {% block head %}
        <meta charset="utf-8">
        <title>{% block title %}{% endblock %}</title>
        <meta name="description" content="ワーキングスペースを提供するカフェ">
        <link rel="icon" type="image/png" href="/static/images/favicon.png">
        <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSS -->
        <link rel="stylesheet" href="https://unpkg.com/ress/dist/ress.min.css">
        <link href="https://fonts.googleapis.com/css?family=Philosopher" rel="stylesheet">
        <link href="/static/style.css" rel="stylesheet">
        {% endblock %}
    </head>

    <body>
        <div id="{{ id }}" class="big-bg">
            <header class="page-header wrapper">
                <h1><a href="/"><img class="logo" src="/static/images/logo.svg" alt="Coffee House ホーム"></a></h1>
                <nav>
                    <ul class="main-nav">
　　　　　　　　　　　　　<!-- ログオフ状態に表示するリンク　-->
                        {% if not session.logged_in %}
                        <li><a href="{{ url_for('cafe.news') }}">News</a></li>
                        <li><a href="{{ url_for('cafe.menu') }}">Menu</a></li>
                        <li><a href="{{ url_for('cafe.contact') }}">Contact</a></li>
                        <li><a href="{{ url_for('loging.login') }}">login</a></li>
　　　　　　　　　　　　　<!-- ログイン状態に表示するリンク　-->
                        {% else %}
                        <li><a href="{{ url_for('cafe.news') }}">News</a></li>
                        <li><a href="{{ url_for('cafe.menu') }}">Menu</a></li>
                        <li><a href="{{ url_for('cafe.contact') }}">Contact</a></li>
                        <li><a href="{{ url_for('review.new_review') }}">review</a></li>
                        <li><a href="{{ url_for('review.show_reviews') }}">board</a></li>
                        <li><a href="{{ url_for('loging.logout') }}">logout</a></li>
                        {% endif %}
                    </ul>
                </nav>
            </header>
            {% for message in get_flashed_messages() %}
                <div class="alert">
                  <font color="red">
                    <p>※{{ message }}</p>
                  </font>
                </div>
            {% endfor %}
            {% block content %}
            {% endblock %}
        </div>
        {% block content2 %}
        {% endblock %}
        {% block footer %}
        {% endblock %}
    </body>
</html>
```
新規投稿リンクにアクセスした時、投稿フォームを返すよう処理を追加します。

```python:./cafe_site/views/reviews.py
# 投稿フォーム
@review.route('/reviews/new', methods=['GET'])
# ログイン認証デコレータ
@login_required
def new_review():
    return render_template('reviews/review.html', id="review")
```
## 12-2. 記事投稿フォーム作成
最後に[Flaskを用いてのウェブアプリケーション作成①](https://qiita.com/syu-hei/items/ac7ed49f1f383aa1dd7c)で作成したreview.htmlを少し編集しました。

```html:./cafe_site/templates/views/reviews.html
{% extends "theme.html" %}
{% block title %}Coffee House{% endblock %}
{% block head %}
  {{ super() }}
  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
{% endblock %}
{% block content %}
    <div class="form-content wrapper">
        <h2 class="page-title">Review</h2>
　　　　<!-- 記事の投稿先　-->
        <form method="POST" action="{{ url_for('review.add_review') }}">

            <div class="star-rating">
                <div class="star-rating__wrap">
                  <input class="star-rating__input" id="star-rating-5" type="radio" name="star" value=5>
                  <label class="star-rating__ico fa fa-star-o fa-lg" for="star-rating-5" title="5 out of 5 stars"></label>
                  <input class="star-rating__input" id="star-rating-4" type="radio" name="star" value=4>
                  <label class="star-rating__ico fa fa-star-o fa-lg" for="star-rating-4" title="4 out of 5 stars"></label>
                  <input class="star-rating__input" id="star-rating-3" type="radio" name="star" value=3>
                  <label class="star-rating__ico fa fa-star-o fa-lg" for="star-rating-3" title="3 out of 5 stars"></label>
                  <input class="star-rating__input" id="star-rating-2" type="radio" name="star" value=2>
                  <label class="star-rating__ico fa fa-star-o fa-lg" for="star-rating-2" title="2 out of 5 stars"></label>
                  <input class="star-rating__input" id="star-rating-1" type="radio" name="star" value=1>
                  <label class="star-rating__ico fa fa-star-o fa-lg" for="star-rating-1" title="1 out of 5 stars"></label>
                </div>
            </div>
            <div>
                <label for="title">タイトル</label>
                <input placeholder="title" name="title" type="text"/>
            </div>
            <div>
                <label for="message">メッセージ</label>
                <textarea id="message" placeholder="message" name="text"></textarea>
            </div>
            <input type="submit" class="button" value="投稿">
        </form>
    </div>
{% endblock %}
{% block footer %}
    <footer>
        <div class="wrapper">
            <p><small>&copy; Coffee House</small></p>
        </div>
    </footer>
{% endblock %}
```
## 12-3. 投稿機能
ここまでで、投稿フォームを作成しました。次に、投稿フォームから投稿された記事をデータベースに保存するための機能を作成しました。
## 12-4. 投稿機能追加
review.htmlでは、フォームの投稿先は、
`action="{{url_for('review.add_review')}}"`としています。**review.add_review**を作成し、投稿内容を受信してデータベースに保存する処理を追加します。views/reviews.pyにadd_reviewを追加します。

```python:./cafe_site/views/reviews.py
@review.route('/reviews', methods=['POST'])
@login_required
# 記事投稿フォーム
def add_review():
    review = Review(
        star=request.form['star'],
        title=request.form['title'],
        text=request.form['text']
    )
# reviewの情報をDBに追加
    db.session.add(review)
# 上記の内容をコミット
    db.session.commit()
    flash('新しく記事が作成されました')
    return redirect(url_for('review.show_reviews'))
```
ログインフォーム作成のときと同様、データの送信の際にはPOSTメソッドを指定しました。
`@login_required`でログインしているかどうかの処理を行っています。
ログインしていれば、前章で作成したReviewモデルを使って、送られてきた記事タイトルと内容についてのモデルインスタンスを作成します。

```python:./cafe_site/views/reviews.py
review = Review(
        star=request.form['star'],
        title=request.form['title'],
        text=request.form['text']
    )
```
それから作成したモデルインスタンスに対して、以下の処理を行うことで新しい記事内容をデータベースに保存するようにしました。

```python:./cafe_site/views/reviews.py
db.session.add(review)
db.session.commit()
```
`db.session.add()`で新しい内容を追加し、`db.session.commit()`でデータベースにデータが書き込まれます。
# 13. 記事詳細機能作成(Read)
次に、記事本文など詳細が表示されるようブログ詳細機能を追加しました。
## 13-1. 「続きを読む」リンク追加
一覧画面での各タイトルに加え、「続きを読む」のリンクを追加しました。 

```html:./cafe_site/templates/views/index.html
{% extends "theme.html" %}
{% block title %}Coffee House{% endblock %}
{% block head %}
  {{ super() }}
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
  <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
{% endblock %}
{% block content %}
<div class="wrapper">
  <h2 class="page-title">board</h2>
  {% for review in reviews %}
  <div class="card bg-transparent">
    <div class="card-body">
      {% if review.star == 1 %}
      <h5 class="card-title">rating : <img src="/static/images/star.png" width="15" height="15"></h5>
      {% endif %}
      {% if review.star  == 2 %}
      <h5 class="card-title">rating : <img src="/static/images/star.png" width="15" height="15"><img src="/static/images/star.png" width="15" height="15"></h5>
      {% endif %}
      {% if review.star == 3 %}
      <h5 class="card-title">rating : <img src="/static/images/star.png" width="15" height="15"><img src="/static/images/star.png" width="15" height="15"><img src="/static/images/star.png" width="15" height="15"></h5>
      {% endif %}
      {% if review.star == 4 %}
      <h5 class="card-title">rating : <img src="/static/images/star.png" width="15" height="15"><img src="/static/images/star.png" width="15" height="15"><img src="/static/images/star.png" width="15" height="15"><img src="/static/images/star.png" width="15" height="15"></h5>
      {% endif %}
      {% if review.star == 5 %}
      <h5 class="card-title">rating : <img src="/static/images/star.png" width="15" height="15"><img src="/static/images/star.png" width="15" height="15"><img src="/static/images/star.png" width="15" height="15"><img src="/static/images/star.png" width="15" height="15"><img src="/static/images/star.png" width="15" height="15"></h5>
      {% endif %}
      <h5 class="card-subtitle">{{ review.title }}</h5>
      <!-- review.show_reviewの引数にidを渡す　-->
      <a class="card-link" href="{{ url_for('review.show_review', id=review.id) }}" >続きを読む</a>
    </div>
  </div>
      {% else %}
        投稿がありません
      {% endfor %}
</div>
{% endblock %}
{% block footer %}
    <footer>
        <div class="wrapper">
            <p><small>&copy; Coffee House</small></p>
        </div>
    </footer>
{% endblock %}
```
ここで、`{{url_for('review.show_review', id=review.id)}}`とurl_forに引数を渡すことで、show_review()が呼び出されたときに、クリックした記事のidが渡されるようになりました。
## 13-2. 閲覧機能追加
views/reviews.pyに閲覧機能を新しく追加しました。

```python:./cafe_site/views/reviews.py
@review.route('/board')
@login_required
# ユーザーの投稿記事一覧を表示
def show_reviews():
# idの降順で記事を並べる
    reviews = Review.query.order_by(Review.id.desc()).all()
    return render_template('reviews/index.html', reviews=reviews, id="board")

@review.route('/reviews/<int:id>', methods=['GET'])
@login_required
# ユーザーidを取得
def show_review(id):
    review = Review.query.get(id)
    return render_template('reviews/show.html', review=review, id="show")
```
`reviews = Review.query.order_by(Review.id.desc()).all()`で、url_forで渡された変数*id*を指定します。
加えて`show_review(id)`と引数名を追加し、変数*id*を参照できるようにしました。
> show_reviewメソッド内では、`review = Review.query.get(id)`とし、渡されたidの記事をデータベースから取得できるようにしました。

## 13-3. 記事内容の詳細表示
最後に、`show.html`ファイルを作成し、記事内容の詳細を表示します。

```html:./cafe_site/templates/views/show.html
{% extends "theme.html" %}
{% block title %}Coffee House{% endblock %}
{% block head %}
  {{ super() }}
{% endblock %}
{% block content %}
<div class="show-content wrapper">
  <!-- 記事のタイトル　-->
  <h2>{{ review.title }}</h2>

    <!-- 記事の評価　-->
    {% if review.star == 1 %}
      <h2>rating : <img src="/static/images/star.png" width="40" height="40"></h2>
    {% endif %}
    {% if review.star  == 2 %}
      <h2>rating : <img src="/static/images/star.png" width="40" height="40"><img src="/static/images/star.png" width="40" height="40"></h2>
    {% endif %}
    {% if review.star == 3 %}
      <h2>rating : <img src="/static/images/star.png" width="40" height="40"><img src="/static/images/star.png" width="40" height="40"><img src="/static/images/star.png" width="40" height="40"></h2>
    {% endif %}
    {% if review.star == 4 %}
      <h2>rating : <img src="/static/images/star.png" width="40" height="40"><img src="/static/images/star.png" width="40" height="40"><img src="/static/images/star.png" width="40" height="40"><img src="/static/images/star.png" width="40" height="40"></h2>
    {% endif %}
    {% if review.star == 5 %}
      <h2>rating : <img src="/static/images/star.png" width="40" height="40"><img src="/static/images/star.png" width="40" height="40"><img src="/static/images/star.png" width="40" height="40"><img src="/static/images/star.png" width="40" height="40"><img src="/static/images/star.png" width="40" height="40"></h2>
    {% endif %}
　　<!-- 記事の内容　-->
    <br> {{ review.text|safe }}

    <br>
　　<!-- 記事の投稿日時　-->
    <br> 投稿日時 {{ review.created_at }}
</div>
{% endblock %}
```
# 14. 記事編集機能追加(Update)
投稿した記事が編集できるよう、記事編集機能を追加しました。
## 14-1. 編集ボタン追加
記事詳細画面に、「編集」ボタンを追加しました。

```html:./cafe_site/templates/views/show.html
<form action="{{ url_for('review.edit_review', id=review.id) }}" method="GET">
    <input type="submit" class="button" value="編集">
</form>
```
## 14-2. edit_review追加
編集ボタンをクリックした時に編集画面を返すように、views/reviews.pyにedit_reviewを追加しました。

```python:./cafe_site/views/reviews.py
@review.route('/reviews/<int:id>/edit', methods=['GET'])
@login_required
# 記事編集フォーム
def edit_review(id):
    review = Review.query.get(id)
    return render_template('reviews/edit.html', review=review)
```
## 14-3. 編集画面作成
edit.htmlを新しく作成し、編集画面を作りました。

```html:./cafe_site/templates/views/edit.html
{% extends "theme.html" %}
{% block title %}Coffee House{% endblock %}
{% block head %}
  {{ super() }}
  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
{% endblock %}
{% block content %}
<div class="edit wrapper">
  <h2 class="page-title">review</h2>
  <form action="{{ url_for('review.update_review', id=review.id) }}" method=post class=add-review>

    <div class="star-rating">
      <div class="star-rating__wrap">
        <input class="star-rating__input" id="star-rating-5" type="radio" name="star" value=5>
        <label class="star-rating__ico fa fa-star-o fa-lg" for="star-rating-5" title="5 out of 5 stars"></label>
        <input class="star-rating__input" id="star-rating-4" type="radio" name="star" value=4>
        <label class="star-rating__ico fa fa-star-o fa-lg" for="star-rating-4" title="4 out of 5 stars"></label>
        <input class="star-rating__input" id="star-rating-3" type="radio" name="star" value=3>
        <label class="star-rating__ico fa fa-star-o fa-lg" for="star-rating-3" title="3 out of 5 stars"></label>
        <input class="star-rating__input" id="star-rating-2" type="radio" name="star" value=2>
        <label class="star-rating__ico fa fa-star-o fa-lg" for="star-rating-2" title="2 out of 5 stars"></label>
        <input class="star-rating__input" id="star-rating-1" type="radio" name="star" value=1>
        <label class="star-rating__ico fa fa-star-o fa-lg" for="star-rating-1" title="1 out of 5 stars"></label>
      </div>
    </div>
      <div>
        <label for="name">タイトル</label>
        <input placeholder="タイトル" name="title" type="text" value={{ review.title }}/>
      </div>
      <div class="form-group">
        <label for="message">メッセージ</label>
        <textarea id="message" placeholder="レビュー" name="text">{{ review.text | safe }}</textarea>
      </div>
      <input type="submit" class="button" value="更新">
  </form>
{% endblock %}
{% block footer %}
    <footer>
        <div class="wrapper">
            <p><small>&copy; Coffee House</small></p>
        </div>
    </footer>
{% endblock %}
```
## 14-4. update_review作成
reviews.pyにupdate_reviewを作成し、フォームに入力された編集内容を受け取り、データベースを更新する処理を追加しました。

```python:./cafe_site/views/reviews.py
@review.route('/reviews/<int:id>/update', methods=['POST'])
@login_required
# 編集フォーム
def update_review(id):
    review = Review.query.get(id)
    review.star = request.form['star']
    review.title = request.form['title']
    review.text = request.form['text']
    # レコード編集
    db.session.merge(review)
    db.session.commit()
    flash('記事が更新されました')
    return redirect(url_for('review.show_reviews'))
```
`Review.query.get(id)`で取得したreviewに対し、**star**, **title**, **text**それぞれを更新しています。
作成のときは**add**ですが、更新の場合は`db.session.merge(review)`とし、最後に`db.session.commit()`としデータベースを更新します。
# 15. 記事削除機能の追加(Delete)
最後に、記事を削除する機能を追加します。
## 15-1. 削除ボタンの追加
```html:./cafe_site/templates/views/show.html
<form action="{{ url_for('review.delete_review', id=review.id) }}" method="post">
    <input type="submit" class="button" value="削除">
</form>
```
## 15-2. delete_review追加
views/reviews.pyにdelete_reviewを追加し、削除ボタンが押されたときに該当の記事を削除する処理を追加します。

```python:./cafe_site/views/reviews.py
@review.route('/reviews/<int:id>/delete', methods=['POST'])
@login_required
# 記事削除
def delete_review(id):
    review = Review.query.get(id)
    # レコード削除
    db.session.delete(review)
    db.session.commit()
    flash('投稿が削除されました')
    return redirect(url_for('review.show_reviews'))
```
データベースの内容を削除するときは、`db.session.delete(review)`と指定します。
これで、CRUDの機能を全て追加することができました。
最後にreviews.pyの全体像を確認します。

```python:./cafe_site/views/reviews.py
from flask import request, redirect, url_for, render_template, flash, session
from cafe_site import db
from cafe_site import app
from cafe_site.models.reviews import Review
from cafe_site.views.loging import login_required
from flask import Blueprint

review = Blueprint('review', __name__)

@review.route('/board')
@login_required
def show_reviews():
    reviews = Review.query.order_by(Review.id.desc()).all()
    return render_template('reviews/index.html', reviews=reviews, id="board")

@review.route('/reviews', methods=['POST'])
@login_required
def add_review():
    review = Review(
        star=request.form['star'],
        title=request.form['title'],
        text=request.form['text']
    )
    db.session.add(review)
    db.session.commit()
    flash('新しく記事が作成されました')
    return redirect(url_for('review.show_reviews'))

@review.route('/reviews/new', methods=['GET'])
@login_required
def new_review():
    return render_template('reviews/review.html', id="review")

@review.route('/reviews/<int:id>', methods=['GET'])
@login_required
def show_review(id):
    review = Review.query.get(id)
    return render_template('reviews/show.html', review=review, id="show")

@review.route('/reviews/<int:id>/edit', methods=['GET'])
@login_required
def edit_review(id):
    review = Review.query.get(id)
    return render_template('reviews/edit.html', review=review)

@review.route('/reviews/<int:id>/update', methods=['POST'])
@login_required
def update_review(id):
    review = Review.query.get(id)
    review.star = request.form['star']
    review.title = request.form['title']
    review.text = request.form['text']
    db.session.merge(review)
    db.session.commit()
    flash('記事が更新されました')
    return redirect(url_for('review.show_reviews'))

@review.route('/reviews/<int:id>/delete', methods=['POST'])
@login_required
def delete_review(id):
    review = Review.query.get(id)
    db.session.delete(review)
    db.session.commit()
    flash('投稿が削除されました')
    return redirect(url_for('review.show_reviews'))
```
> 続きは[Flaskを用いたウェブアプリケーション作成④](https://qiita.com/syu-hei/items/dcaaff7ce360743298be)で説明いたします。