geek開発日誌

超一流のプログラマーを目指してます!作成したポートフォリオを記録していく開発日誌です。

Scrapyでニュースを取得③

こんにちは。nakatatsuです。

前回に引き続きフレームワークScrapyを使用して複数のニュースサイトから情報を取得するスクレイピングツールを作成します。今回はCrawlSpiderを使用し、Yahooニュース、gooニュースから情報を取得するコードを書き換えます。CrawlSpiderを使用すると、Webページからa要素を抽出するコードを書く代わりにたどりたいリンクの正規表現を書くだけでマッチするリンクを抽出してたどることができます。

仕様

・複数のニュースサイトから情報を取得

・記事のタイトルと要約を取得

準備

Scrapyを使用するための準備についてはこのブログの過去記事「Scrapyでニュースを取得」を参照してください。

nakatatsu-com.hatenablog.com


フォルダ構成

news
├── news
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-36.pyc
│   │   ├── items.cpython-36.pyc
│   │   ├── pipelines.cpython-36.pyc
│   │   └── settings.cpython-36.pyc
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       ├── __init__.py
│       ├── __pycache__
│       │   ├── __init__.cpython-36.pyc
│       │   └── news_crawl.cpython-36.pyc
│       └── news_crawl.py
└── scrapy.cfg

フォルダ構成はこのようになっております。

ソースコード

Itemは前回から変更なしです。

item.py

import scrapy


class Headline(scrapy.Item):
    '''
    ニュースのヘッドラインを格納するためのItem
    '''

    title = scrapy.Field()
    summary = scrapy.Field()


CrawlSpiderを作成します。

news_crawl.py

from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from news.items import Headline


class NewsCrawlSpider(CrawlSpider):
    name = 'news_crawl' # Spiderの名前
    allowed_domains = ['news.yahoo.co.jp',
        'news.goo.ne.jp'] # クロール対象とするドメインのリスト
    start_urls = ['http://news.yahoo.co.jp/',
        'http://news.goo.ne.jp/'] # クロールを開始するURLのリスト
    rules = [
        Rule(LinkExtractor(allow=r'/pickup/\d+$'),
            callback='parse_yahoo'),
        Rule(LinkExtractor(allow=r'/topstories/\w+'),
            callback='parse_goo'),
    ]

    def parse_yahoo(self, response):
        '''
        Yahooニューストピックスページからタイトルと要約を抜き出す
        '''

        item = Headline()
        item['title'] = response.css('.tpcNews_title::text').extract_first()
        item['summary'] = response.css(
            '.tpcNews_summary').xpath('string()').extract_first()
        yield item

    def parse_goo(self, response):
        '''
        gooニューストピックスページからタイトルと要約を抜き出す
        '''

        item = Headline()
        item['title'] = response.css(
            '.topics-title > a::text').extract_first()
        item['summary'] = response.css(
            '.topics-text').xpath('string()').extract_first()
        yield item

rules属性はRuleオブジェクトのリストで複数のルールを記述できます。ルールは上から順番にチェックされ、最初にマッチしたルールが使用されます。
allowには正規表現または正規表現のリストを指定します。allowに指定した正規表現にマッチするURLのみが抜き出されます。
一方denyに指定した正規表現にマッチするURLは抜き出されません。denyのほうがallowより先に判断されるため、allowとdenyの両方にマッチするURLは抜き出されません。

結果

結果です。長いので省略してます。

# Yahooニュース
2019-06-18 07:47:54 [scrapy.core.scraper] DEBUG: Scraped from <200 https://news.yahoo.co.jp/pickup/6327084>
{'summary': '\u3000'
            'ロックバンド「KANA―BOON」のベーシストの飯田(めしだ)祐馬(28)が音信不通となった騒動について、女性問題が原因ではないことが17日、分かった。関係者が「女性が関係した問題ではない」と話した。事件性はなく、トラブルに巻き込まれた様子もないことから、関係者は「メンタルの問題ではないか」とみている。',
 'title': 'KANA―BOON飯田\u3000音信不通に女性問題関係ない'}
・・・

# gooニュース
2019-06-18 07:47:46 [scrapy.core.scraper] DEBUG: Scraped from <200 https://news.goo.ne.jp/topstories/nation/188/3f6d95003d25345a77697205c38987ed.html>
{'summary': '\u3000'
            '国連人口部は17日、世界人口について、2057年に100億人を突破する一方で、日本の人口は58年に1億人を下回り、2100年には7500万人になるとする推計を発表した。国連人口部が2年ごとに発表するもので、前回の17年は日本の人口が1億人を下回るのは65年としていたが、今回は7年早くなった。\r\n'
            '\r\n',
 'title': '日本の人口、2100年に7500万人\u3000減少見通し加速'}
・・・

トピックスページからタイトルと要約を取得することができました。

まとめ

・Yahooニュース、gooニュースからCrawlSpiderを使用して情報を取得

・記事のタイトルと要約を取得

参考文献

参考文献です。

Scrapyでニュースを取得②

こんにちは。nakatatsuです。

前回に引き続きフレームワークScrapyを使用して複数のニュースサイトから情報を取得するスクレイピングツールを作成します。今回はgooニュースから情報を取得します。

仕様

完成形の仕様です。途中で変更があるかもしれません。

・複数のニュースサイトから情報を取得

・記事のタイトルと要約を取得

準備

Scrapyを使用するための準備についてはこのブログの過去記事「Scrapyでニュースを取得」を参照してください。

nakatatsu-com.hatenablog.com


フォルダ構成

news
├── news
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-36.pyc
│   │   ├── items.cpython-36.pyc
│   │   ├── pipelines.cpython-36.pyc
│   │   └── settings.cpython-36.pyc
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       ├── __init__.py
│       ├── __pycache__
│       │   ├── __init__.cpython-36.pyc
│       │   ├── goo.cpython-36.pyc
│       │   └── yahoo.cpython-36.pyc
│       ├── goo.py
│       └── yahoo.py
└── scrapy.cfg

フォルダ構成はこのようになっております。

ソースコード

Itemは前回から変更なしです。

item.py

import scrapy


class Headline(scrapy.Item):
    '''
    ニュースのヘッドラインを格納するためのItem
    '''

    title = scrapy.Field()
    summary = scrapy.Field()


Spiderを作成します。

(scraping) $ scrapy genspider goo news.goo.ne.jp

spidersディレクトリ内にgoo.pyというファイルが生成されます。これをベースに書き換えていきます。

goo.py

import scrapy
from news.items import Headline


class GooSpider(scrapy.Spider):
    name = 'goo' # Spiderの名前
    allowed_domains = ['news.goo.ne.jp'] # クロール対象とするドメインのリスト
    start_urls = ['http://news.goo.ne.jp/'] # クロールを開始するURLのリスト

    def parse(self, response):
        '''
        「トップ」>「注目」のトピックス一覧から個々のトピックスへのリンクを抜き出してたどる
        '''

        for url in response.css(
            '#topiarea_title > li > a::attr("href")').extract():
            yield scrapy.Request(response.urljoin(url), self.parse_topics)

    def parse_topics(self, response):
        '''
        トピックスページからタイトルと要約を抜き出す
        '''

        item = Headline()
        item['title'] = response.css(
            '.heading-title-topics::text').extract_first()
        item['summary'] = response.css(
            '.topics-text').xpath('string()').extract_first()
        yield item


また、Itemのtitleフィールドが正しく取得できているかを検証するためにItemPipelineを作成します。ItemPipelineはSpiderから抽出したItemに対して任意の処理を行うためのコンポーネントです。

pipelines.py

from scrapy.exceptions import DropItem


class ValidationPipeline(object):
    '''
    Itemを検証するPipeline
    '''

    def process_item(self, item, spider):
        if not item['title']: # titleフィールドが取得できていない場合は破棄
            raise DropItem('Missing title') # 破棄する理由を表すメッセージ
        return item

作成したItemPipelineはそのままでは使用できません。プロジェクトのsettings.pyに、以下の設定を追加する必要があります。

ITEM_PIPELINES = {
    'news.pipelines.ValidationPipeline': 300,
}

実際はsettings.pyにコメントとして記載されているので、コメントアウトするだけです。

結果

結果です。長いので省略してます。

{'summary': '\u3000'
            '大阪府吹田市の千里山交番で古瀬鈴之佑巡査(26)が刃物で刺され拳銃が奪われた事件で、吹田署捜査本部は17日、強盗殺人未遂容疑で、住所不定、職業不詳の飯森裕次郎容疑者(33)を逮捕した。飯森容疑者は拳銃を所持していた。大阪府箕面市で発見し確保した。\r\n'
            '\r\n',
 'title': '拳銃強奪、33歳男逮捕 1発使用か'}
・・・

トピックスページからタイトルと要約を取得することができました。

まとめ

・gooニュースサイトから情報を取得

・記事のタイトルと要約を取得

参考文献

参考文献です。

Scrapyでニュースを取得

こんにちは。nakatatsuです。

今回からフレームワークScrapyを使用して複数のニュースサイトから情報を取得するスクレイピングツールを作成します。まずはYahooニュースから情報を取得します。

仕様

完成形の仕様です。途中で変更があるかもしれません。

・複数のニュースサイトから情報を取得

・記事のタイトルと要約を取得

準備

Scrapyを使用するための準備を以下に示します。

Scrapyを使うと、どんなWebサイトでも使える共通処理をフレームワークに任せて、ユーザーは個々のWebサイトごとに異なる処理だけを書けばよくなります。以下のような機能を持っています。

●Webページからのリンクの抽出

robots.txtの取得と拒否されているページのクロール防止

XMLサイトマップの取得とリンクの抽出

ドメインごと/IPアドレスごとのクロール時間間隔の調整

●複数のクロール先の並行処理

●重複するURLのクロール防止

●エラー時の回数制限付きのリトライ

クローラーのデーモン化とジョブの管理


・Scrapyのインストール

(scraping) $ pip install scrapy

Scrapyはlxmlに依存しているので、libxml2とlibxsltのインストールも必要です。libxml2とlibxsltのインストールはこのブログの過去記事「サイトから画像を取得して保存」を参照してください。

nakatatsu-com.hatenablog.com



・プロジェクトの作成

(scraping) $ scrapy startproject news

newsというプロジェクトを作成しました。


ディレクトリの移動

(scraping) $ cd news

コマンドを実行する際は、基本的にこのディレクトリで実行します。


・設定

DOWNLOAD_DELAY = 3

settings.pyに記載されている「DOWNLOAD_DELAY」の項目をコメントアウトし、ページのダウンロード間隔を平均3秒空けます。

フォルダ構成

news
├── news
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-36.pyc
│   │   ├── items.cpython-36.pyc
│   │   └── settings.cpython-36.pyc
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       ├── __init__.py
│       ├── __pycache__
│       │   ├── __init__.cpython-36.pyc
│       │   └── yahoo.cpython-36.pyc
│       └── yahoo.py
└── scrapy.cfg

フォルダ構成はこのようになっております。

ソースコード

Itemを作成します。ItemはSpiderが抜き出したデータを格納しておくためのオブジェクトです。

item.py

import scrapy


class Headline(scrapy.Item):
    '''
    ニュースのヘッドラインを格納するためのItem
    '''

    title = scrapy.Field()
    summary = scrapy.Field()


Spiderを作成します。「scrape genspider」コマンドであらかじめ定義されているテンプレートからSpiderを作成できます。「scrape genspider」コマンドの第1引数にSpiderの名前、第2引数にドメイン名を指定して実行します。

(scraping) $ scrapy genspider yahoo news.yahoo.co.jp

spidersディレクトリ内にyahoo.pyというファイルが生成されます。これをベースに書き換えていきます。

yahoo.py

import scrapy
from news.items import Headline


class YahooSpider(scrapy.Spider):
    name = 'yahoo' # Spiderの名前
    allowed_domains = ['news.yahoo.co.jp'] # クロール対象とするドメインのリスト
    start_urls = ['http://news.yahoo.co.jp/'] # クロールを開始するURLのリスト

    def parse(self, response):
        '''
        「トップ」>「主要」のトピックス一覧から個々のトピックスへのリンクを抜き出してたどる
        '''

        for url in response.css('.topicsListItem > a::attr("href")').extract():
            yield scrapy.Request(response.urljoin(url), self.parse_topics)

    def parse_topics(self, response):
        '''
        トピックスページからタイトルと要約を抜き出す
        '''

        item = Headline()
        item['title'] = response.css('.tpcNews_title::text').extract_first()
        item['summary'] = response.css(
            '.tpcNews_summary').xpath('string()').extract_first()
        yield item


実行の流れは以下のようになっています。


f:id:nakatatsu_com:20190616030933p:plain


ScrapyEngine:他のコンポーネントを制御する実行エンジン

Scheduler:Requestをキューに溜める

Downloader:Requestが指すURLのページを実際にダウンロードする

Spider:ダウンロードしたResponseを受け取り、ページからItemや次にたどるリンクを表すRequestを抜き出す

FeedExporter:Spiderが抜き出したItemをファイルなどに保存する

ItemPipeline:Spiderが抜き出したItemに関する処理を行う

DownloaderMiddleware:Downloaderの処理を拡張する

SpiderMiddleware:Spiderへの入力となるResponseやSpiderからの出力となるItemやRequestに対しての処理を拡張する


Spiderを実行すると、最初にstart_urls属性に含まれるURLを指すRequestオブジェクトがScrapyのSchedulerに渡され、Webページの取得を待つキューに追加されます。

キューに追加されたRequestオブジェクトは順にDownloaderに渡されます。DownloaderはRequestオブジェクトに指定されたURLのページを取得し、Responseオブジェクトを作成します。Downloaderの処理が完了すると、ScrapyEngineがSpiderのコールバック関数を呼び出します。デフォルトのコールバック関数はSpiderのparse()メソッドです。コールバック関数には引数としてResponseオブジェクトが渡されるので、ここからリンクやデータを抽出します。

コールバック関数ではyield文で複数のオブジェクトを返せます。リンクを抽出して次のページをクロールしたい場合は、Requestオブジェクトをyieldします。データを抽出したい場合は、Itemオブジェクトをyieldします。Requestオブジェクトをyieldした場合、再びSchedulerのキューに追加されます。Itemオブジェクトをyieldした場合、FeedExporterに送られ、ファイルなどに保存されます。

結果

結果です。長いので省略してます。

{'summary': '\u3000'
         '【ワシントン=海谷道隆、ニューヨーク=村山誠】シャナハン米国防長官代行は14日、中東ホルムズ海峡近くのオマーン沖で起きた日本などのタンカー2隻への攻撃について、「情報の機密をさらに解除し、より多くの情報を共有したい」と述べた。イランの攻撃への関与を裏付ける情報を関係国などに提供し、米国の主張に対する国際社会の理解を広げる狙いだ。',
 'title': '「イラン関与」裏付け、米が機密開示を検討'}
・・・

トピックスページからタイトルと要約を取得することができました。

まとめ

・Yahooニュースサイトから情報を取得

・記事のタイトルと要約を取得

参考文献

参考文献です。

ドラマレビューサイト(Flask)⑩

こんにちは。nakatatsuです。

前回に引き続きドラマレビューサイトを作成します。今回でいよいよ完成です。前回の詳細についてはこのブログの過去記事「ドラマレビューサイト(Flask)」、「ドラマレビューサイト(Flask)②」、「ドラマレビューサイト(Flask)③」、「ドラマレビューサイト(Flask)④」、「ドラマレビューサイト(Flask)⑤」、「ドラマレビューサイト(Flask)⑥」、「ドラマレビューサイト(Flask)⑦」、「ドラマレビューサイト(Flask)⑧」、「ドラマレビューサイト(Flask)⑨」を参照してください。

nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com


仕様

・ドラマのレビューを閲覧、投稿できるサイトを作成

・おすすめのドラマを5個ランダムに表示

・レビューランキングを表示

・視聴率ランキングを表示

準備

Pipenv、Flask、Flask-SQLAlchemy、Flask-Scriptのインストールについてはこのブログの過去記事「ドラマレビューサイト(Flask)」、「ドラマレビューサイト(Flask)②」を参照してください。

nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com


jQueryRatyについてはドラマレビューサイト(Flask)⑥」を参照してください。

nakatatsu-com.hatenablog.com


フォルダ構成

drama
├── Pipfile
├── Pipfile.lock
├── flask_drama
│   ├── __init__.py
│   ├── config.py
│   ├── flask_drama.db
│   ├── models
│   │   ├── dramas.py
│   │   ├── posts.py
│   │   └── users.py
│   ├── scripts
│   │   └── db.py
│   ├── static
│   │   ├── coollogo_com-11407173.png
│   │   ├── jquery.raty.js
│   │   ├── responsive.css
│   │   ├── script.js
│   │   ├── star-half.png
│   │   ├── star-off.png
│   │   ├── star-on.png
│   │   ├── style.css
│   │   ├── グッドワイフ.jpg
│   │   ├── スパイラル~町工場の奇跡~.jpg
│   │   ├── なつぞら.jpg
│   │   ├── パーフェクトワールド.jpg
│   │   ├── 白い巨塔.jpg
│   │   ├── インハンド.jpg
│   │   ├── 俺のスカート、どこ行った?.jpg
│   │   ├── 特捜9 season2.jpg
│   │   ├── 科捜研の女 Season19.jpg
│   │   └── 集団左遷!!.jpg
│   │   ├── 白衣の戦士!.jpg
│   │   └── わたし、定時で帰ります。.jpg
│   ├── templates
│   │   ├── admin.html
│   │   ├── index.html
│   │   ├── layout.html
│   │   ├── login.html
│   │   ├── new.html
│   │   ├── posts_detail.html
│   │   ├── posts_index.html
│   │   └── update.html
│   └── views.py
├── manage.py
└── server.py

フォルダ構成はこのようになっております。

ソースコード

server.py(起動ファイル)

from flask_drama import app


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


__init__.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy


app = Flask(__name__)
app.config.from_object('flask_drama.config')

db = SQLAlchemy(app)

import flask_drama.views


views.py

from flask import render_template
from flask import redirect
from flask import session
from flask import flash
from flask import url_for
from flask import request
from flask_drama import app
from flask_drama import db
from flask_drama.models.users import User
from flask_drama.models.dramas import Drama
from flask_drama.models.posts import Post
from sqlalchemy.exc import IntegrityError
from random import sample


@app.route('/')
def index():
    dramas = Drama.query.all()
    dramas = sample(dramas, len(dramas))
    review_rankings = Drama.query.order_by(Drama.mean.desc()).all()
    rate_rankings = Drama.query.order_by(Drama.rate.desc()).all()
    return render_template('index.html', dramas=dramas, review_rankings=review_rankings, rate_rankings=rate_rankings)

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        if not request.form['username'] and not request.form['password']:
            flash('ユーザー名を入力してください')
            flash('パスワードを入力してください')
            return render_template('login.html', username=request.form['username'], password=request.form['password'])
        if not request.form['username']:
            flash('ユーザー名を入力してください')
            return render_template('login.html', username=request.form['username'], password=request.form['password'])
        if not request.form['password']:
            flash('パスワードを入力してください')
            return render_template('login.html', username=request.form['username'], password=request.form['password'])
        users_model = User.query.all()
        for user_model in users_model:
            if request.form['username'] == user_model.username:
                if request.form['password'] == user_model.password:
                    session['logged_in'] = True
                    flash('ログインしました')
                    if user_model.username == 'nakatatsu':
                        session['admin_name'] = True
                    return redirect(url_for('index'))
                flash('パスワードが異なります')
                return render_template('login.html', username=request.form['username'], password=request.form['password'])
        flash('ログインできません')
        return render_template('login.html', username=request.form['username'], password=request.form['password'])
    else:
        return render_template('login.html', username='', password='')

@app.route('/logout')
def logout():
    session.pop('logged_in', None)
    session.pop('admin_name', None)
    flash('ログアウトしました')
    return redirect(url_for('index'))

@app.route('/new', methods=['GET', 'POST'])
def new():
    if request.method == 'POST':
        try:
            user = User(
                username=request.form['username'],
                password=request.form['password']
            )
            if not user.username and not user.password:
                flash('ユーザー名を入力してください')
                flash('パスワードを入力してください')
                return render_template('new.html', user=user)
            if not user.username:
                flash('ユーザー名を入力してください')
                return render_template('new.html', user=user)
            if not user.password:
                flash('パスワードを入力してください')
                return render_template('new.html', user=user)
            db.session.add(user)
            db.session.commit()
            session['logged_in'] = True
            flash('登録しました')
            return redirect(url_for('index'))
        except IntegrityError:
            flash('ユーザー名が既に使用されています')
            return render_template('new.html', user=user)
    else:
        user = User()
        return render_template('new.html', user=user)

@app.route('/admin', methods=['GET', 'POST'])
def admin():
    if request.method == 'POST':
        try:
            drama = Drama(
                title=request.form['admin-title'],
                actor=request.form['actor'],
                rate=request.form['rate']
            )
            if not drama.title and not drama.actor and not drama.rate:
                flash('ドラマのタイトルを入力してください')
                flash('出演者を入力してください')
                flash('視聴率を入力してください')
                return render_template('admin.html', drama=drama)
            if not drama.title and not drama.actor:
                flash('ドラマのタイトルを入力してください')
                flash('出演者を入力してください')
                return render_template('admin.html', drama=drama)
            if not drama.title and not drama.rate:
                flash('ドラマのタイトルを入力してください')
                flash('視聴率を入力してください')
                return render_template('admin.html', drama=drama)
            if not drama.actor and not drama.rate:
                flash('出演者を入力してください')
                flash('視聴率を入力してください')
                return render_template('admin.html', drama=drama)
            if not drama.title:
                flash('ドラマのタイトルを入力してください')
                return render_template('admin.html', drama=drama)
            if not drama.actor:
                flash('出演者を入力してください')
                return render_template('admin.html', drama=drama)
            if not drama.rate:
                flash('視聴率を入力してください')
                return render_template('admin.html', drama=drama)
            db.session.add(drama)
            db.session.commit()
            flash('入力しました')
            return redirect(url_for('admin'))
        except IntegrityError:
            flash('このドラマは既に入力されています')
            return render_template('admin.html', drama=drama)
    else:
        drama = Drama()
        return render_template('admin.html', drama=drama)

@app.route('/posts')
def posts_index():
    dramas = Drama.query.order_by(Drama.title.asc()).all()
    return render_template('posts_index.html', dramas=dramas)

@app.route('/posts/<int:id>', methods=['GET', 'POST'])
def posts_detail(id):
    if request.method == 'POST':
        post = Post(
            drama_id=id,
            star=request.form['star'],
            review=request.form['review-input']
        )
        drama = Drama.query.get(id)
        if not post.star and not post.review:
            flash('評価を選択してください')
            flash('レビューを入力してください')
            return render_template('posts_detail.html', drama=drama, post=post)
        if not post.star:
            flash('評価を選択してください')
            return render_template('posts_detail.html', drama=drama, post=post)
        if not post.review:
            flash('レビューを入力してください')
            return render_template('posts_detail.html', drama=drama, post=post)
        db.session.add(post)
        db.session.commit()
        total = 0
        num = 0
        for post in drama.posts:
            total += post.star
            num += 1
        drama.mean = round((total / num), 2)
        db.session.merge(drama)
        db.session.commit()
        flash('投稿しました')
        return redirect(url_for('posts_detail', id=id))
    else:
        drama = Drama.query.get(id)
        post = Post()
        return render_template('posts_detail.html', drama=drama, post=post)

@app.route('/posts/<int:id>/delete')
def delete(id):
    post = Post.query.get(id)
    db.session.delete(post)
    db.session.commit()
    flash('削除しました')
    drama_id = post.drama_id
    return redirect(url_for('posts_detail', id=drama_id))

@app.route('/posts/<int:id>/update', methods=['GET', 'POST'])
def update(id):
    if request.method == 'POST':
        drama = Drama.query.get(id)
        if not request.form['rate']:
            flash('視聴率を入力してください')
            return render_template('update.html', drama=drama)
        drama.rate = request.form['rate']
        db.session.merge(drama)
        db.session.commit()
        flash('視聴率を変更しました')
        return redirect(url_for('posts_detail', id=drama.id))
    else:
        drama = Drama.query.get(id)
        return render_template('update.html', drama=drama)


config.py(設定情報を記載したファイル)

DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///flask_drama.db'
SQLALCHEMY_TRACK_MODIFICATIONS = True
SECRET_KEY = '\xbf,M\xe4\x89\x92C$H\xbeC\x0f\xad\xca\x04\xd5\xa5\xd00^\x8e\xee\xa0\x1c'


manage.py(モデルをデータベースに反映するための実行ファイル)

from flask_script import Manager
from flask_drama import app
from flask_drama.scripts.db import InitDB


if __name__ == '__main__':
    manager = Manager(app)
    manager.add_command('init_db', InitDB())
    manager.run()


db.py(モデルをデータベースに反映するためのスクリプトファイル)

from flask_script import Command
from flask_drama import db


class InitDB(Command):
    'create database'

    def run(self):
        db.create_all()


users.py(ユーザー情報のデータベースモデル)

from flask_drama import db


class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True)
    password = db.Column(db.String(50))

    def __init__(self, username=None, password=None):
        self.username = username
        self.password = password


posts.py(レビュー情報のデータベースモデル)

from flask_drama import db


class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer, primary_key=True)
    drama_id = db.Column(db.Integer, db.ForeignKey('dramas.id'))
    star = db.Column(db.Integer)
    review = db.Column(db.String(200))

    def __init__(self, drama_id=None, star=None, review=None):
        self.drama_id = drama_id
        self.star = star
        self.review = review


dramas.py(ドラマタイトル情報のデータベースモデル)

from flask_drama import db


class Drama(db.Model):
    __tablename__ = 'dramas'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(50), unique=True)
    actor = db.Column(db.String(100))
    mean = db.Column(db.Float)
    rate = db.Column(db.Float)
    posts = db.relationship('Post', backref='drama', lazy='dynamic')

    def __init__(self, title=None, actor=None, mean=None, rate=None):
        self.title = title
        self.actor = actor
        self.mean = mean
        self.rate = rate


layout.html(レイアウトのベーステンプレート)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>あなたにおすすめのドラマ ドラマのレビューを閲覧・投稿</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <link rel="stylesheet" href="{{ url_for('static', filename='responsive.css') }}">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.1/css/all.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="{{ url_for('static', filename='jquery.raty.js') }}"></script>
  </head>
  <body>
    <header>
      <div class="container">
        <div id="header-left">
          <a id="top" href="{{ url_for('index') }}">
            <img src="{{ url_for('static', filename='coollogo_com-11407173.png') }}">
          </a>
        </div>
        <div class="clear"></div>
        <div id="header-right">
          {% if not session.logged_in %}
            <a id="new" href="{{ url_for('new') }}">新規登録</a>
            <a id="login" href="{{ url_for('login') }}">ログイン</a>
          {% else %}
            {% if session.admin_name %}
              <a id= "admin" href="{{ url_for('admin') }}">管理者ページ</a>
            {% endif %}
            <a id="logout" href="{{ url_for('logout') }}">ログアウト</a>
          {% endif %}
        </div>
        <div class="clear"></div>
      </div>
    </header>
    <div id="main">
      <div id="title">
        <h1>あなたにおすすめのドラマ</h1>
      </div>
      {% for message in get_flashed_messages() %}
      <div id="alert">
        <p>{{ message }}</p>
      </div>
      {% endfor %}
      <div class="container">
        {% block body %}{% endblock %}
      </div>
      <div class="clear"></div>
    </div>
    <script src="{{ url_for('static', filename='script.js') }}"></script>
  </body>
</html>


index.html(トップページ)

{% extends 'layout.html' %}
{% block body %}
<div id="reviews">
  <h2>おすすめのドラマ</h2>
  {% for drama in dramas %}
    {% if loop.index > 5 %}
    {% else %}
      <div class="review">
        <div class="drama-title">
          <h3>{{ drama.title }}</h3>
          <p>出演:{{ drama.actor }}</p>
          <p>視聴率:{{ drama.rate }}%</p>
          <div class="stars-review">
            {% if drama.mean %}
              <p class="stars" data-score="{{ drama.mean }}"></p>
              <div class="stars-value">
                <p>{{ drama.mean }}</p>
              </div>
            {% endif %}
            <a class="all detail-all" href="{{ url_for('posts_detail', id=drama.id) }}"><i class="fab fa-sistrix"></i><span>ドラマの詳細ページへ</span></a>
          </div>
        </div>
        <div class="drama-contents">
          {% if drama.posts[0] %}
            {% for post in drama.posts[::-1] %}
              {% if loop.index > 3 %}
              {% else %}
                <div class="drama-content">
                  <p class="stars" data-score="{{ post.star }}"></p>
                  <p>{{ post.review }}</p>
                </div>
              {% endif %}
            {% endfor %}
          {% else %}
            <div class="review-nothing">
              <p>投稿はありません</p>
            </div>
          {% endif %}
        </div>
      </div>
    {% endif %}
  {% endfor %}
</div>
<div id="main-right">
  <div id="review-ranking">
    <h3>ドラマレビューランキングTOP10</h3>
    <div id="review-ranking-list">
      <ol>
        {% for review_ranking in review_rankings %}
          {% if loop.index > 10 %}
          {% else %}
            {% if review_ranking.mean %}
              <li>{{ review_ranking.title }}<span id="review-value">{{ review_ranking.mean }}</span></li>
            {% endif %}
          {% endif %}
        {% endfor %}
      </ol>
      <a class="all" href="{{ url_for('posts_index') }}"><i class="fas fa-list"></i><span>ドラマの一覧ページへ</span></a>
    </div>
  </div>
  <div id="rate-ranking">
    <h3>視聴率ランキングTOP10</h3>
    <div id="rate-ranking-list">
      <ol>
        {% for rate_ranking in rate_rankings %}
          {% if loop.index > 10 %}
          {% else %}
            <li>{{ rate_ranking.title }}<span id="review-value">{{ rate_ranking.rate }}%</span></li>
          {% endif %}
        {% endfor %}
      </ol>
    </div>
  </div>
</div>
{% endblock %}


new.html(新規登録ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="form">
  <div id="form-title">
    <h2>新規登録</h2>
  </div>
  <div id="form-group">
    <form action="{{ url_for('new') }}" method=post>
      <label for="username">ユーザー名</label><br>
      {% if user.username %}
        <input id="username" type="text" name="username" value="{{ user.username }}"><br>
      {% else %}
        <input id="username" type="text" name="username" value=""><br>
      {% endif %}
      <label for="password">パスワード</label><br>
      {% if user.password %}
        <input id="password" type="password" name="password" value="{{ user.password }}"><br>
      {% else %}
        <input id="password" type="password" name="password" value=""><br>
      {% endif %}
      <input id="btn" type="submit" value="新規登録">
    </form>
  </div>
</div>
{% endblock %}


login.html(ログインページ)

{% extends 'layout.html' %}
{% block body %}
<div id="form">
  <div id="form-title">
    <h2>ログイン</h2>
  </div>
  <div id="form-group">
    <form action="{{ url_for('login') }}" method=post>
      <label for="username">ユーザー名</label><br>
      <input id="username" type="text" name="username" value="{{ username }}"><br>
      <label for="password">パスワード</label><br>
      <input id="password" type="password" name="password" value="{{ password }}"><br>
      <input id="btn" type="submit" value="ログイン">
    </form>
  </div>
</div>
{% endblock %}


admin.html(管理者ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="form">
  <div id="form-title">
    <h2>管理者ページ</h2>
  </div>
  <div id="form-group">
    <form action="{{ url_for('admin') }}" method=post>
      <label for="admin-title">ドラマのタイトル</label><br>
      {% if drama.title %}
        <input id="admin-title" type="text" name="admin-title" value="{{ drama.title }}"><br>
      {% else %}
        <input id="admin-title" type="text" name="admin-title" value=""><br>
      {% endif %}
      <label for="actor">出演者</label><br>
      {% if drama.actor %}
        <input id="actor" type="text" name="actor" value="{{ drama.actor }}"><br>
      {% else %}
        <input id="actor" type="text" name="actor" value=""><br>
      {% endif %}
      <input id="btn" type="submit" value="ドラマを追加">
    </form>
  </div>
</div>
{% endblock %}


posts_index.html(ドラマの一覧ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="index-title">
  <h2>ドラマの一覧</h2>
</div>
  {% for drama in dramas %}
    <div class="drama-index">
      <a href="{{ url_for('posts_detail', id=drama.id) }}">
        <ul>
          <li>
            {{ drama.title }}
          </li>
        </ul>
      </a>
    </div>
  {% endfor %}
{% endblock %}


posts_detail.html(ドラマの詳細ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="detail">
  <div id="drama-img">
    <img src="../static/{{ drama.title }}.jpg">
  </div>
  <div class="drama-title">
    <h3>{{ drama.title }}</h3>
    <p>出演:{{ drama.actor }}</p>
    {% if session.admin_name %}
      <a id="update" href="{{ url_for('update', id=drama.id) }}"><p>視聴率:{{ drama.rate }}%</p></a>
    {% else %}
      <p>視聴率:{{ drama.rate }}%</p>
    {% endif %}
    <div class="stars-review">
      {% if drama.mean %}
        <p class="stars" data-score="{{ drama.mean }}"></p>
        <div class="stars-value">
          <p>{{ drama.mean }}</p>
        </div>
      {% endif %}
      <a class="all detail-all" href="{{ url_for('posts_index') }}"><i class="fas fa-list"></i><span>ドラマの一覧ページへ</span></a>
    </div>
  </div>
  <div class="drama-contents">
    {% if drama.posts[0] %}
      {% for post in drama.posts[::-1] %}
        <div class="drama-content">
          <p class="stars" data-score="{{ post.star }}"></p>
          <p>{{ post.review }}</p>
          {% if session.admin_name %}
            <a id="delete" href="{{ url_for('delete', id=post.id) }}"><i class="far fa-trash-alt"></i><span>削除</span></a>
          {% endif %}
        </div>
      {% endfor %}
    {% else %}
      <div class="review-nothing">
        <p>投稿はありません</p>
      </div>
    {% endif %}
  </div>
  {% if session.logged_in %}
    <div id="form">
      <div id="form-title">
        <h2>新規投稿</h2>
      </div>
      <div id="form-group">
        <form action="{{ url_for('posts_detail', id=drama.id) }}" method=post>
          <label for="star">評価</label><br>
          <select id="star" name="star">
            <option value="" {% if not post.star %}selected{% endif %}>選択してください</option>
            <option value="1" {% if post.star == '1' %}selected{% endif %}>星1つ</option>
            <option value="2" {% if post.star == '2' %}selected{% endif %}>星2つ</option>
            <option value="3" {% if post.star == '3' %}selected{% endif %}>星3つ</option>
            <option value="4" {% if post.star == '4' %}selected{% endif %}>星4つ</option>
            <option value="5" {% if post.star == '5' %}selected{% endif %}>星5つ</option>
          </select>
          <label for="review-input">レビュー</label><br>
          {% if post.review %}
            <textarea id="review-input" name="review-input" value="{{ post.review }}"></textarea><br>
          {% else %}
            <textarea id="review-input" name="review-input" value=""></textarea><br>
          {% endif %}
          <input id="btn" type="submit" value="新規投稿">
        </form>
      </div>
    </div>
  {% endif %}
</div>
{% endblock %}


update.html(視聴率変更ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="detail">
  <div id="drama-img">
    <img src="../../static/{{ drama.title }}.jpg">
  </div>
  <div class="drama-title">
    <h3>{{ drama.title }}</h3>
    <p>出演:{{ drama.actor }}</p>
    <p>視聴率:{{ drama.rate }}%</p>
    <div class="stars-review">
      {% if drama.mean %}
        <p class="stars" data-score="{{ drama.mean }}"></p>
        <div class="stars-value">
          <p>{{ drama.mean }}</p>
        </div>
      {% endif %}
      <a class="all detail-all" href="{{ url_for('posts_index') }}"><i class="fas fa-list"></i><span>ドラマの一覧ページへ</span></a>
    </div>
  </div>
  <div id="form">
    <div id="form-title">
      <h2>視聴率変更ページ</h2>
    </div>
    <div id="form-group">
      <form action="{{ url_for('update', id=drama.id) }}" method=post>
        <label for="rate">視聴率</label><br>
        {% if drama.rate %}
          <input id="rate" type="text" name="rate" value="{{ drama.rate }}"><br>
        {% else %}
          <input id="rate" type="text" name="rate" value=""><br>
        {% endif %}
        <input id="btn" type="submit" value="視聴率を変更">
      </form>
    </div>
  </div>
</div>
{% endblock %}


style.css

* {
  box-sizing: border-box;
  margin: 0;
}

.container {
  width: 100%;
  padding: 0 20px;
}

a {
  text-decoration: none;
}

span {
  margin-left: 5px;
}

#alert {
  color: #ff0000;
  padding-left: 20px;
  text-align: center;
}

header {
  border-bottom: solid 1px #c0c0c0;
  box-shadow: 0 2px 4px #c0c0c0;
  width: 100%;
  height: 60px;
  position: fixed;
  top: 0px;
  z-index: 10;
  background-color: #ffffff;
}

.clear {
  clear: left;
  clear: right;
}

#header-left img {
  float: left;
  height: 40px;
  margin-top: 10px;
}

#header-right {
  float: right;
}

#header-right a {
  display: inline-block;
  line-height: 60px;
  transition: all 0.5s;
  padding: 0 10px;
}

#header-right a, #header-right a:visited {
  color: #000000;
}

#header-right a:hover {
  background-color: rgba(192, 192, 192, 0.2);
}

#new, #login, #admin, #logout {
  margin: 0 10px;
}

#main {
  padding: 100px 0 20px 0;
}

#title {
  padding-bottom: 20px;
  text-align: center;
  text-shadow: 4px 4px 1px #c0c0c0;
}

#reviews {
  float: left;
  width: 70%;
}

#reviews h2 {
  padding-bottom: 20px;
}

.review {
  border: solid 1px #c0c0c0;
  margin-bottom: 20px;
  border-radius: 5px;
  padding: 10px;
  box-shadow: 0 0 3px #c0c0c0;
}

.drama-title h3 {
  display: inline;
}

.drama-title p {
  margin-left: 10px;
  color: #6b6b6b;
  display: inline;
}

.drama-title {
  height: 70px;
  margin-bottom: 20px;
  border-bottom: solid 1px #c0c0c0;
}

.drama-content {
  padding-bottom: 20px;
}

#main-right {
  float: right;
  width: 30%;
}

#main-right h3 {
  border-bottom: solid 1px #c0c0c0;
  padding: 20px 0;
  margin-bottom: 20px;
  font-size: 18px;
  color: #6b6b6b;
}

#review-ranking, #rate-ranking {
  border: solid 1px #f5f5f5;
  border-radius: 5px;
  padding: 10px;
  background-color: #f5f5f5;
  margin-left: 20px;
  box-shadow: 0 0 3px #c0c0c0;
}

#review-ranking {
  margin-top: 50px;
  margin-bottom: 30px;
}

#review-ranking-list ol {
  padding-bottom: 10px;
}

#review-value {
  margin-left: 15px;
  color: #6b6b6b;
}

.all {
  display: inline-block;
  color: #4387e9;
}

.all:visited {
  color: #4387e9;
}

.all:hover {
  text-decoration: underline;
}

#form-title {
  color: #6b6b6b;
  text-align: center;
  margin-top: 10px;
  margin-bottom: 20px;
}

#form-group {
  border: solid 1px #c0c0c0;
  padding: 20px;
  width: 40%;
  margin: 0 auto;
  box-shadow: 0 0 3px #c0c0c0;
  border-radius: 5px;
}

label {
  font-size: 20px;
}

#username, #password, #admin-title, #actor, #rate {
  width: 100%;
  height: 35px;
  margin-bottom: 20px;
  font-size: 20px;
  border: solid 1px #c0c0c0;
}

select {
  -webkit-appearance: none;
	-moz-appearance: none;
	appearance: none;
  width: 100%;
  height: 35px;
  margin-bottom: 20px;
  font-size: 20px;
  border: solid 1px #c0c0c0;
  border-radius: 0;
  background-color: #ffffff;
  cursor: pointer;
}

textarea {
  border: solid 1px #c0c0c0
}

#review-input {
  width: 100%;
  height: 200px;
  margin-bottom: 20px;
  font-size: 20px;
  resize: none;
}

#btn {
  color: #ffffff;
  background-color: #4387e9;
  padding: 5px 10px;
  opacity: 1;
  cursor: pointer;
  font-size: 20px;
  border-radius: 5px;
}

#btn:hover {
  opacity: 0.8;
}

#index-title {
  padding-bottom: 20px;
}

.drama-index {
  width: 25%;
  height: 50px;
  float: left;
}

.drama-index ul li {
  list-style-type: none;
}

.drama-index a {
  color: #000000;
  font-size: 18px;
}

.drama-index a:visited {
  color: #000000;
}

.drama-index a:hover {
  text-decoration: underline;
}

.stars-review {
  height: 40px;
  line-height: 40px;
}

.stars-value {
  display: inline;
}

.stars-value p {
  font-size: 20px;
  color: #000000;
}

.detail-all {
  float: right;
}

#drama-img {
  text-align: center;
  margin-bottom: 20px;
}

#delete, #delete:visited {
  color: #4387e9;
}

#delete:hover {
  text-decoration: underline;
}

#update p, #update p:visited {
  color: #4387e9;
}

#update p:hover {
  text-decoration: underline;
}


responsive.css

@media (max-width: 1000px) {
  #reviews {
    width: 100%;
  }

  #main-right {
    display: none;
  }

  .drama-index {
    width: 50%;
  }

  #form-group {
    width: 100%;
  }
}

@media (max-width: 670px) {
  .drama-title p {
    display: none;
  }

  .drama-index {
    width: 100%;
  }
}


script.js

$(function() {
  $('.stars').raty({
    readOnly: true,
    score: function() {
      return $(this).attr('data-score');
    },
    path: '../static',
    path: '../../static'
  });
});


起動ファイルであるserver.pyを実行するとアプリケーションが起動します。今回の改良点は以下のようになっています。

・Dramaモデルにrateプロパティを追加し、視聴率を入力できるようにしました

・視聴率変更ページを作成し、管理者のみ視聴率を変更できるようにしました

・レスポンシブデザインを適用し、画面のサイズに合わせて最適なレイアウトを組めるようにしました

結果

最後の回なので全てをページを表示します。
トップページです。

f:id:nakatatsu_com:20190614220123p:plain


新規登録ページです。

f:id:nakatatsu_com:20190614220301p:plain


ログインページです。

f:id:nakatatsu_com:20190614220348p:plain


管理者ページです。

f:id:nakatatsu_com:20190614220513p:plain


ドラマの一覧ページです。

f:id:nakatatsu_com:20190614220554p:plain


ドラマの詳細ページです。

f:id:nakatatsu_com:20190614220639p:plain


視聴率変更ページです。

f:id:nakatatsu_com:20190614221320p:plain

まとめ

全10回で仕様の通り作成することができました。

・ドラマのレビューを閲覧、投稿できるサイトを作成した

・おすすめのドラマを5個ランダムに表示した

・レビューランキングを表示した

・視聴率ランキングを表示した

参考文献

参考文献です。

ドラマレビューサイト(Flask)⑨

こんにちは。nakatatsuです。

前回に引き続きドラマレビューサイトを作成します。前回の詳細についてはこのブログの過去記事「ドラマレビューサイト(Flask)」、「ドラマレビューサイト(Flask)②」、「ドラマレビューサイト(Flask)③」、「ドラマレビューサイト(Flask)④」、「ドラマレビューサイト(Flask)⑤」、「ドラマレビューサイト(Flask)⑥」、「ドラマレビューサイト(Flask)⑦」、「ドラマレビューサイト(Flask)⑧」を参照してください。

nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com


仕様

完成形の仕様です。途中で変更があるかもしれません。

・ドラマのレビューを閲覧、投稿できるサイトを作成

・おすすめのドラマを5個ランダムに表示

・レビューランキングを表示

・視聴率ランキングを表示

準備

Pipenv、Flask、Flask-SQLAlchemy、Flask-Scriptのインストールについてはこのブログの過去記事「ドラマレビューサイト(Flask)」、「ドラマレビューサイト(Flask)②」を参照してください。

nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com


jQueryRatyについてはドラマレビューサイト(Flask)⑥」を参照してください。

nakatatsu-com.hatenablog.com


フォルダ構成

drama
├── Pipfile
├── Pipfile.lock
├── flask_drama
│   ├── __init__.py
│   ├── config.py
│   ├── flask_drama.db
│   ├── models
│   │   ├── dramas.py
│   │   ├── posts.py
│   │   └── users.py
│   ├── scripts
│   │   └── db.py
│   ├── static
│   │   ├── coollogo_com-11407173.png
│   │   ├── jquery.raty.js
│   │   ├── script.js
│   │   ├── star-half.png
│   │   ├── star-off.png
│   │   ├── star-on.png
│   │   ├── style.css
│   │   ├── グッドワイフ.jpg
│   │   ├── スパイラル~町工場の奇跡~.jpg
│   │   ├── なつぞら.jpg
│   │   ├── パーフェクトワールド.jpg
│   │   ├── 白い巨塔.jpg
│   │   ├── インハンド.jpg
│   │   ├── 俺のスカート、どこ行った?.jpg
│   │   ├── 特捜9 season2.jpg
│   │   ├── 科捜研の女 Season19.jpg
│   │   └── 集団左遷!!.jpg
│   ├── templates
│   │   ├── admin.html
│   │   ├── index.html
│   │   ├── layout.html
│   │   ├── login.html
│   │   ├── new.html
│   │   ├── posts_detail.html
│   │   └── posts_index.html
│   └── views.py
├── manage.py
└── server.py

フォルダ構成はこのようになっております。

ソースコード

server.py(起動ファイル)

from flask_drama import app


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


__init__.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy


app = Flask(__name__)
app.config.from_object('flask_drama.config')

db = SQLAlchemy(app)

import flask_drama.views


views.py

from flask import render_template
from flask import redirect
from flask import session
from flask import flash
from flask import url_for
from flask import request
from flask_drama import app
from flask_drama import db
from flask_drama.models.users import User
from flask_drama.models.dramas import Drama
from flask_drama.models.posts import Post
from sqlalchemy.exc import IntegrityError
from random import sample


@app.route('/')
def index():
    dramas = Drama.query.all()
    dramas = sample(dramas, len(dramas))
    rankings = Drama.query.order_by(Drama.mean.desc()).all()
    return render_template('index.html', dramas=dramas, rankings=rankings)

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        if not request.form['username'] and not request.form['password']:
            flash('ユーザー名を入力してください')
            flash('パスワードを入力してください')
            return render_template('login.html', username=request.form['username'], password=request.form['password'])
        if not request.form['username']:
            flash('ユーザー名を入力してください')
            return render_template('login.html', username=request.form['username'], password=request.form['password'])
        if not request.form['password']:
            flash('パスワードを入力してください')
            return render_template('login.html', username=request.form['username'], password=request.form['password'])
        users_model = User.query.all()
        for user_model in users_model:
            if request.form['username'] == user_model.username:
                if request.form['password'] == user_model.password:
                    session['logged_in'] = True
                    flash('ログインしました')
                    if user_model.username == 'nakatatsu':
                        session['admin_name'] = True
                    return redirect(url_for('index'))
                flash('パスワードが異なります')
                return render_template('login.html', username=request.form['username'], password=request.form['password'])
        flash('ログインできません')
        return render_template('login.html', username=request.form['username'], password=request.form['password'])
    else:
        return render_template('login.html', username='', password='')

@app.route('/logout')
def logout():
    session.pop('logged_in', None)
    session.pop('admin_name', None)
    flash('ログアウトしました')
    return redirect(url_for('index'))

@app.route('/new', methods=['GET', 'POST'])
def new():
    if request.method == 'POST':
        try:
            user = User(
                username=request.form['username'],
                password=request.form['password']
            )
            if not user.username and not user.password:
                flash('ユーザー名を入力してください')
                flash('パスワードを入力してください')
                return render_template('new.html', user=user)
            if not user.username:
                flash('ユーザー名を入力してください')
                return render_template('new.html', user=user)
            if not user.password:
                flash('パスワードを入力してください')
                return render_template('new.html', user=user)
            db.session.add(user)
            db.session.commit()
            session['logged_in'] = True
            flash('登録しました')
            return redirect(url_for('index'))
        except IntegrityError:
            flash('ユーザー名が既に使用されています')
            return render_template('new.html', user=user)
    else:
        user = User()
        return render_template('new.html', user=user)

@app.route('/admin', methods=['GET', 'POST'])
def admin():
    if request.method == 'POST':
        try:
            drama = Drama(
                title=request.form['admin-title'],
                actor=request.form['actor']
            )
            if not drama.title and not drama.actor:
                flash('ドラマのタイトルを入力してください')
                flash('出演者を入力してください')
                return render_template('admin.html', drama=drama)
            if not drama.title:
                flash('ドラマのタイトルを入力してください')
                return render_template('admin.html', drama=drama)
            if not drama.actor:
                flash('出演者を入力してください')
                return render_template('admin.html', drama=drama)
            db.session.add(drama)
            db.session.commit()
            flash('入力しました')
            return redirect(url_for('admin'))
        except IntegrityError:
            flash('このドラマは既に入力されています')
            return render_template('admin.html', drama=drama)
    else:
        drama = Drama()
        return render_template('admin.html', drama=drama)

@app.route('/posts')
def posts_index():
    dramas = Drama.query.order_by(Drama.title.asc()).all()
    return render_template('posts_index.html', dramas=dramas)

@app.route('/posts/<int:id>', methods=['GET', 'POST'])
def posts_detail(id):
    if request.method == 'POST':
        post = Post(
            drama_id=id,
            star=request.form['star'],
            review=request.form['review-input']
        )
        drama = Drama.query.get(id)
        if not post.star and not post.review:
            flash('評価を選択してください')
            flash('レビューを入力してください')
            return render_template('posts_detail.html', drama=drama, post=post)
        if not post.star:
            flash('評価を選択してください')
            return render_template('posts_detail.html', drama=drama, post=post)
        if not post.review:
            flash('レビューを入力してください')
            return render_template('posts_detail.html', drama=drama, post=post)
        db.session.add(post)
        db.session.commit()
        total = 0
        num = 0
        for post in drama.posts:
            total += post.star
            num += 1
        drama.mean = round((total / num), 2)
        db.session.add(drama)
        db.session.commit()
        flash('投稿しました')
        return redirect(url_for('posts_detail', id=id))
    else:
        drama = Drama.query.get(id)
        post = Post()
        return render_template('posts_detail.html', drama=drama, post=post)

@app.route('/posts/<int:id>/delete')
def delete(id):
    post = Post.query.get(id)
    db.session.delete(post)
    db.session.commit()
    flash('削除しました')
    drama_id = post.drama_id
    return redirect(url_for('posts_detail', id=drama_id))


config.py(設定情報を記載したファイル)

DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///flask_drama.db'
SQLALCHEMY_TRACK_MODIFICATIONS = True
SECRET_KEY = '\xbf,M\xe4\x89\x92C$H\xbeC\x0f\xad\xca\x04\xd5\xa5\xd00^\x8e\xee\xa0\x1c'


manage.py(モデルをデータベースに反映するための実行ファイル)

from flask_script import Manager
from flask_drama import app
from flask_drama.scripts.db import InitDB


if __name__ == '__main__':
    manager = Manager(app)
    manager.add_command('init_db', InitDB())
    manager.run()


db.py(モデルをデータベースに反映するためのスクリプトファイル)

from flask_script import Command
from flask_drama import db


class InitDB(Command):
    'create database'

    def run(self):
        db.create_all()


users.py(ユーザー情報のデータベースモデル)

from flask_drama import db


class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True)
    password = db.Column(db.String(50))

    def __init__(self, username=None, password=None):
        self.username = username
        self.password = password


posts.py(レビュー情報のデータベースモデル)

from flask_drama import db


class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer, primary_key=True)
    drama_id = db.Column(db.Integer, db.ForeignKey('dramas.id'))
    star = db.Column(db.Integer)
    review = db.Column(db.String(200))

    def __init__(self, drama_id=None, star=None, review=None):
        self.drama_id = drama_id
        self.star = star
        self.review = review


dramas.py(ドラマタイトル情報のデータベースモデル)

from flask_drama import db


class Drama(db.Model):
    __tablename__ = 'dramas'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(50), unique=True)
    actor = db.Column(db.String(100))
    mean = db.Column(db.Float)
    posts = db.relationship('Post', backref='drama', lazy='dynamic')

    def __init__(self, title=None, actor=None, mean=None):
        self.title = title
        self.actor = actor
        self.mean = mean


layout.html(レイアウトのベーステンプレート)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>あなたにおすすめのドラマ ドラマのレビューを閲覧・投稿</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.1/css/all.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="{{ url_for('static', filename='jquery.raty.js') }}"></script>
  </head>
  <body>
    <header>
      <div class="container">
        <div id="header-left">
          <a id="top" href="{{ url_for('index') }}">
            <img src="{{ url_for('static', filename='coollogo_com-11407173.png') }}">
          </a>
        </div>
        <div class="clear"></div>
        <div id="header-right">
          {% if not session.logged_in %}
            <a id="new" href="{{ url_for('new') }}">新規登録</a>
            <a id="login" href="{{ url_for('login') }}">ログイン</a>
          {% else %}
            {% if session.admin_name %}
              <a id= "admin" href="{{ url_for('admin') }}">管理者ページ</a>
            {% endif %}
            <a id="logout" href="{{ url_for('logout') }}">ログアウト</a>
          {% endif %}
        </div>
        <div class="clear"></div>
      </div>
    </header>
    <div id="main">
      <div id="title">
        <h1>あなたにおすすめのドラマ</h1>
      </div>
      {% for message in get_flashed_messages() %}
      <div id="alert">
        <p>{{ message }}</p>
      </div>
      {% endfor %}
      <div class="container">
        {% block body %}{% endblock %}
      </div>
      <div class="clear"></div>
    </div>
    <script src="{{ url_for('static', filename='script.js') }}"></script>
  </body>
</html>


index.html(トップページ)

{% extends 'layout.html' %}
{% block body %}
<div id="reviews">
  <h2>おすすめのドラマ</h2>
  {% for drama in dramas %}
    {% if loop.index > 5 %}
    {% else %}
      <div class="review">
        <div class="drama-title">
          <h3>{{ drama.title }}</h3>
          <p>出演:{{ drama.actor }}</p>
          <div class="stars-review">
            {% if drama.mean %}
              <p class="stars" data-score="{{ drama.mean }}"></p>
              <div class="stars-value">
                <p>{{ drama.mean }}</p>
              </div>
            {% endif %}
            <a class="all detail-all" href="{{ url_for('posts_detail', id=drama.id) }}"><i class="fab fa-sistrix"></i><span>ドラマの詳細ページへ</span></a>
          </div>
        </div>
        <div class="drama-contents">
          {% if drama.posts[0] %}
            {% for post in drama.posts[::-1] %}
              {% if loop.index > 3 %}
              {% else %}
                <div class="drama-content">
                  <p class="stars" data-score="{{ post.star }}"></p>
                  <p>{{ post.review }}</p>
                </div>
              {% endif %}
            {% endfor %}
          {% else %}
            <div class="review-nothing">
              <p>投稿はありません</p>
            </div>
          {% endif %}
        </div>
      </div>
    {% endif %}
  {% endfor %}
</div>
<div id="main-right">
  <div id="review-ranking">
    <h3>ドラマレビューランキングTOP10</h3>
    <div id="review-ranking-list">
      <ol>
        {% for ranking in rankings %}
          <li>{{ ranking.title }}<span id="review-value">{{ ranking.mean }}</span></li>
        {% endfor %}
      </ol>
      <a class="all" href="{{ url_for('posts_index') }}"><i class="fas fa-list"></i><span>ドラマの一覧ページへ</span></a>
    </div>
  </div>
  <div id="rate-ranking">
    <h3>視聴率ランキングTOP10</h3>
    <div id="rate-ranking-list">
      <ol>
        <li>緊急取調室 第3シリーズ</li>
        <li>特捜9 season2</li>
        <li>ラジエーションハウス</li>
        <li>科捜研の女 Season19</li>
        <li>集団左遷!!</li>
        <li>いだてん~東京オリムピック噺~</li>
        <li>インハンド</li>
        <li>わたし、定時で帰ります。</li>
        <li>俺のスカート、どこ行った?</li>
        <li>白衣の戦士!</li>
      </ol>
    </div>
  </div>
</div>
{% endblock %}


new.html(新規登録ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="form">
  <div id="form-title">
    <h2>新規登録</h2>
  </div>
  <div id="form-group">
    <form action="{{ url_for('new') }}" method=post>
      <label for="username">ユーザー名</label><br>
      {% if user.username %}
        <input id="username" type="text" name="username" value="{{ user.username }}"><br>
      {% else %}
        <input id="username" type="text" name="username" value=""><br>
      {% endif %}
      <label for="password">パスワード</label><br>
      {% if user.password %}
        <input id="password" type="password" name="password" value="{{ user.password }}"><br>
      {% else %}
        <input id="password" type="password" name="password" value=""><br>
      {% endif %}
      <input id="btn" type="submit" value="新規登録">
    </form>
  </div>
</div>
{% endblock %}


login.html(ログインページ)

{% extends 'layout.html' %}
{% block body %}
<div id="form">
  <div id="form-title">
    <h2>ログイン</h2>
  </div>
  <div id="form-group">
    <form action="{{ url_for('login') }}" method=post>
      <label for="username">ユーザー名</label><br>
      <input id="username" type="text" name="username" value="{{ username }}"><br>
      <label for="password">パスワード</label><br>
      <input id="password" type="password" name="password" value="{{ password }}"><br>
      <input id="btn" type="submit" value="ログイン">
    </form>
  </div>
</div>
{% endblock %}


admin.html(管理者ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="form">
  <div id="form-title">
    <h2>管理者ページ</h2>
  </div>
  <div id="form-group">
    <form action="{{ url_for('admin') }}" method=post>
      <label for="admin-title">ドラマのタイトル</label><br>
      {% if drama.title %}
        <input id="admin-title" type="text" name="admin-title" value="{{ drama.title }}"><br>
      {% else %}
        <input id="admin-title" type="text" name="admin-title" value=""><br>
      {% endif %}
      <label for="actor">出演者</label><br>
      {% if drama.actor %}
        <input id="actor" type="text" name="actor" value="{{ drama.actor }}"><br>
      {% else %}
        <input id="actor" type="text" name="actor" value=""><br>
      {% endif %}
      <input id="btn" type="submit" value="ドラマを追加">
    </form>
  </div>
</div>
{% endblock %}


posts_index.html(ドラマの一覧ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="index-title">
  <h2>ドラマの一覧</h2>
</div>
  {% for drama in dramas %}
    <div class="drama-index">
      <a href="{{ url_for('posts_detail', id=drama.id) }}">
        <ul>
          <li>
            {{ drama.title }}
          </li>
        </ul>
      </a>
    </div>
  {% endfor %}
{% endblock %}


posts_detail.html(ドラマの詳細ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="detail">
  <div id="drama-img">
    <img src="../static/{{ drama.title }}.jpg">
  </div>
  <div class="drama-title">
    <h3>{{ drama.title }}</h3>
    <p>出演:{{ drama.actor }}</p>
    <div class="stars-review">
      {% if drama.mean %}
        <p class="stars" data-score="{{ drama.mean }}"></p>
        <div class="stars-value">
          <p>{{ drama.mean }}</p>
        </div>
      {% endif %}
      <a class="all detail-all" href="{{ url_for('posts_index') }}"><i class="fas fa-list"></i><span>ドラマの一覧ページへ</span></a>
    </div>
  </div>
  <div class="drama-contents">
    {% if drama.posts[0] %}
      {% for post in drama.posts[::-1] %}
        <div class="drama-content">
          <p class="stars" data-score="{{ post.star }}"></p>
          <p>{{ post.review }}</p>
          {% if session.admin_name %}
            <a id="delete" href="{{ url_for('delete', id=post.id) }}"><i class="far fa-trash-alt"></i><span>削除</span></a>
          {% endif %}
        </div>
      {% endfor %}
    {% else %}
      <div class="review-nothing">
        <p>投稿はありません</p>
      </div>
    {% endif %}
  </div>
  {% if session.logged_in %}
    <div id="form">
      <div id="form-title">
        <h2>新規投稿</h2>
      </div>
      <div id="form-group">
        <form action="{{ url_for('posts_detail', id=drama.id) }}" method=post>
          <label for="star">評価</label><br>
          <select id="star" name="star">
            <option value="" {% if not post.star %}selected{% endif %}>選択してください</option>
            <option value="1" {% if post.star == '1' %}selected{% endif %}>星1つ</option>
            <option value="2" {% if post.star == '2' %}selected{% endif %}>星2つ</option>
            <option value="3" {% if post.star == '3' %}selected{% endif %}>星3つ</option>
            <option value="4" {% if post.star == '4' %}selected{% endif %}>星4つ</option>
            <option value="5" {% if post.star == '5' %}selected{% endif %}>星5つ</option>
          </select>
          <label for="review-input">レビュー</label><br>
          {% if post.review %}
            <textarea id="review-input" name="review-input" value="{{ post.review }}"></textarea><br>
          {% else %}
            <textarea id="review-input" name="review-input" value=""></textarea><br>
          {% endif %}
          <input id="btn" type="submit" value="新規投稿">
        </form>
      </div>
    </div>
  {% endif %}
</div>
{% endblock %}


style.css

* {
  box-sizing: border-box;
  margin: 0;
}

.container {
  width: 100%;
  padding: 0 20px;
}

a {
  text-decoration: none;
}

span {
  margin-left: 5px;
}

#alert {
  color: #ff0000;
  padding-left: 20px;
  text-align: center;
}

header {
  border-bottom: solid 1px #c0c0c0;
  box-shadow: 0 2px 4px #c0c0c0;
  width: 100%;
  height: 60px;
  position: fixed;
  top: 0px;
  z-index: 10;
  background-color: #ffffff;
}

.clear {
  clear: left;
  clear: right;
}

#header-left img {
  float: left;
  height: 40px;
  margin-top: 10px;
}

#header-right {
  float: right;
}

#header-right a {
  display: inline-block;
  line-height: 60px;
  transition: all 0.5s;
  padding: 0 10px;
}

#header-right a, #header-right a:visited {
  color: #000000;
}

#header-right a:hover {
  background-color: rgba(192, 192, 192, 0.2);
}

#new, #login, #admin, #logout {
  margin: 0 10px;
}

#main {
  padding: 100px 0 20px 0;
}

#title {
  padding-bottom: 20px;
  text-align: center;
  text-shadow: 4px 4px 1px #c0c0c0;
}

#reviews {
  float: left;
  width: 70%;
}

#reviews h2 {
  padding-bottom: 20px;
}

.review {
  border: solid 1px #c0c0c0;
  margin-bottom: 20px;
  border-radius: 5px;
  padding: 10px;
  box-shadow: 0 0 3px #c0c0c0;
}

.drama-title h3 {
  display: inline;
}

.drama-title p {
  margin-left: 10px;
  color: #6b6b6b;
  display: inline;
}

.drama-title {
  height: 70px;
  margin-bottom: 20px;
  border-bottom: solid 1px #c0c0c0;
}

.drama-content {
  padding-bottom: 20px;
}

#main-right {
  float: right;
  width: 30%;
}

#main-right h3 {
  border-bottom: solid 1px #c0c0c0;
  padding: 20px 0;
  margin-bottom: 20px;
  font-size: 18px;
  color: #6b6b6b;
}

#review-ranking, #rate-ranking {
  border: solid 1px #f5f5f5;
  border-radius: 5px;
  padding: 10px;
  background-color: #f5f5f5;
  margin-left: 20px;
  box-shadow: 0 0 3px #c0c0c0;
}

#review-ranking {
  margin-top: 50px;
  margin-bottom: 30px;
}

#review-ranking-list ol {
  padding-bottom: 10px;
}

#review-value {
  margin-left: 15px;
  color: #6b6b6b;
}

.all {
  display: inline-block;
  color: #4387e9;
}

.all:visited {
  color: #4387e9;
}

.all:hover {
  text-decoration: underline;
}

#form-title {
  color: #6b6b6b;
  text-align: center;
  margin-top: 10px;
  margin-bottom: 20px;
}

#form-group {
  border: solid 1px #c0c0c0;
  padding: 20px;
  width: 40%;
  margin: 0 auto;
  box-shadow: 0 0 3px #c0c0c0;
  border-radius: 5px;
}

label {
  font-size: 20px;
}

#username, #password, #admin-title, #actor {
  width: 100%;
  height: 35px;
  margin-bottom: 20px;
  font-size: 20px;
  border: solid 1px #c0c0c0;
}

select {
  -webkit-appearance: none;
	-moz-appearance: none;
	appearance: none;
  width: 100%;
  height: 35px;
  margin-bottom: 20px;
  font-size: 20px;
  border: solid 1px #c0c0c0;
  border-radius: 0;
  background-color: #ffffff;
  cursor: pointer;
}

textarea {
  border: solid 1px #c0c0c0
}

#review-input {
  width: 100%;
  height: 200px;
  margin-bottom: 20px;
  font-size: 20px;
  resize: none;
}

#btn {
  color: #ffffff;
  background-color: #4387e9;
  padding: 5px 10px;
  opacity: 1;
  cursor: pointer;
  font-size: 20px;
  border-radius: 5px;
}

#btn:hover {
  opacity: 0.8;
}

#index-title {
  padding-bottom: 20px;
}

.drama-index {
  width: 25%;
  height: 50px;
  float: left;
}

.drama-index ul li {
  list-style-type: none;
}

.drama-index a {
  color: #000000;
  font-size: 18px;
}

.drama-index a:visited {
  color: #000000;
}

.drama-index a:hover {
  text-decoration: underline;
}

.stars-review {
  height: 40px;
  line-height: 40px;
}

.stars-value {
  display: inline;
}

.stars-value p {
  font-size: 20px;
  color: #000000;
}

.detail-all {
  float: right;
}

#drama-img {
  text-align: center;
  margin-bottom: 20px;
}

#delete, #delete:visited {
  color: #4387e9;
}

#delete:hover {
  text-decoration: underline;
}


script.js

$(function() {
  $('.stars').raty({
    readOnly: true,
    score: function() {
      return $(this).attr('data-score');
    },
    path: '../static'
  });
});


起動ファイルであるserver.pyを実行するとアプリケーションが起動します。今回の改良点は以下のようになっています。

・トップページでおすすめのドラマを5個ランダムに表示するようにしました

・トップページでドラマレビューを投稿日時が新しい順から3個表示するようにしました

・トップページでレビューランキングトップ10を表示するようにしました

・ドラマの一覧ページのUIを変更しました

結果

トップページです。おすすめのドラマが5個、ドラマレビューが3個表示されています。レビューランキングトップ10が表示されています。

f:id:nakatatsu_com:20190614020755p:plain

ドラマの一覧ページです。

f:id:nakatatsu_com:20190614020703p:plain

まとめ

・ドラマのレビューを閲覧、投稿できるサイトを作成した

・トップページでおすすめのドラマを5個、ドラマレビューを3個表示するようにした

・トップページでレビューランキングトップ10を表示するようにした

・ドラマの一覧ページのUIを変更した

参考文献

参考文献です。

ドラマレビューサイト(Flask)⑧

こんにちは。nakatatsuです。

前回に引き続きドラマレビューサイトを作成します。前回の詳細についてはこのブログの過去記事「ドラマレビューサイト(Flask)」、「ドラマレビューサイト(Flask)②」、「ドラマレビューサイト(Flask)③」、「ドラマレビューサイト(Flask)④」、「ドラマレビューサイト(Flask)⑤」、「ドラマレビューサイト(Flask)⑥」、「ドラマレビューサイト(Flask)⑦」を参照してください。

nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com


仕様

完成形の仕様です。途中で変更があるかもしれません。

・ドラマのレビューを閲覧、投稿できるサイトを作成

・おすすめのドラマを5個ランダムに表示

・レビューランキングを表示

・視聴率ランキングを表示

準備

Pipenv、Flask、Flask-SQLAlchemy、Flask-Scriptのインストールについてはこのブログの過去記事「ドラマレビューサイト(Flask)」、「ドラマレビューサイト(Flask)②」を参照してください。

nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com


jQueryRatyについてはドラマレビューサイト(Flask)⑥」を参照してください。

nakatatsu-com.hatenablog.com


フォルダ構成

drama
├── Pipfile
├── Pipfile.lock
├── flask_drama
│   ├── __init__.py
│   ├── config.py
│   ├── flask_drama.db
│   ├── models
│   │   ├── dramas.py
│   │   ├── posts.py
│   │   └── users.py
│   ├── scripts
│   │   └── db.py
│   ├── static
│   │   ├── coollogo_com-11407173.png
│   │   ├── jquery.raty.js
│   │   ├── script.js
│   │   ├── star-half.png
│   │   ├── star-off.png
│   │   ├── star-on.png
│   │   ├── style.css
│   │   ├── グッドワイフ.jpg
│   │   ├── スパイラル~町工場の奇跡~.jpg
│   │   ├── なつぞら.jpg
│   │   ├── パーフェクトワールド.jpg
│   │   ├── 白い巨塔.jpg
│   │   ├── インハンド.jpg
│   │   ├── 俺のスカート、どこ行った?.jpg
│   │   ├── 特捜9 season2.jpg
│   │   ├── 科捜研の女 Season19.jpg
│   │   └── 集団左遷!!.jpg
│   ├── templates
│   │   ├── admin.html
│   │   ├── index.html
│   │   ├── layout.html
│   │   ├── login.html
│   │   ├── new.html
│   │   ├── posts_detail.html
│   │   └── posts_index.html
│   └── views.py
├── manage.py
└── server.py

フォルダ構成はこのようになっております。

ソースコード

server.py(起動ファイル)

from flask_drama import app


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


__init__.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy


app = Flask(__name__)
app.config.from_object('flask_drama.config')

db = SQLAlchemy(app)

import flask_drama.views


views.py

from flask import render_template
from flask import redirect
from flask import session
from flask import flash
from flask import url_for
from flask import request
from flask_drama import app
from flask_drama import db
from flask_drama.models.users import User
from flask_drama.models.dramas import Drama
from flask_drama.models.posts import Post
from sqlalchemy.exc import IntegrityError


@app.route('/')
def index():
    dramas = Drama.query.order_by(Drama.title.asc()).all()
    return render_template('index.html', dramas=dramas)

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        if not request.form['username'] and not request.form['password']:
            flash('ユーザー名を入力してください')
            flash('パスワードを入力してください')
            return render_template('login.html', username=request.form['username'], password=request.form['password'])
        if not request.form['username']:
            flash('ユーザー名を入力してください')
            return render_template('login.html', username=request.form['username'], password=request.form['password'])
        if not request.form['password']:
            flash('パスワードを入力してください')
            return render_template('login.html', username=request.form['username'], password=request.form['password'])
        users_model = User.query.all()
        for user_model in users_model:
            if request.form['username'] == user_model.username:
                if request.form['password'] == user_model.password:
                    session['logged_in'] = True
                    flash('ログインしました')
                    if user_model.username == 'nakatatsu':
                        session['admin_name'] = True
                    return redirect(url_for('index'))
                flash('パスワードが異なります')
                return render_template('login.html', username=request.form['username'], password=request.form['password'])
        flash('ログインできません')
        return render_template('login.html', username=request.form['username'], password=request.form['password'])
    else:
        return render_template('login.html', username='', password='')

@app.route('/logout')
def logout():
    session.pop('logged_in', None)
    session.pop('admin_name', None)
    flash('ログアウトしました')
    return redirect(url_for('index'))

@app.route('/new', methods=['GET', 'POST'])
def new():
    if request.method == 'POST':
        try:
            user = User(
                username=request.form['username'],
                password=request.form['password']
            )
            if not user.username and not user.password:
                flash('ユーザー名を入力してください')
                flash('パスワードを入力してください')
                return render_template('new.html', user=user)
            if not user.username:
                flash('ユーザー名を入力してください')
                return render_template('new.html', user=user)
            if not user.password:
                flash('パスワードを入力してください')
                return render_template('new.html', user=user)
            db.session.add(user)
            db.session.commit()
            session['logged_in'] = True
            flash('登録しました')
            return redirect(url_for('index'))
        except IntegrityError:
            flash('ユーザー名が既に使用されています')
            return render_template('new.html', user=user)
    else:
        user = User()
        return render_template('new.html', user=user)

@app.route('/admin', methods=['GET', 'POST'])
def admin():
    if request.method == 'POST':
        try:
            drama = Drama(
                title=request.form['admin-title'],
                actor=request.form['actor']
            )
            if not drama.title and not drama.actor:
                flash('ドラマのタイトルを入力してください')
                flash('出演者を入力してください')
                return render_template('admin.html', drama=drama)
            if not drama.title:
                flash('ドラマのタイトルを入力してください')
                return render_template('admin.html', drama=drama)
            if not drama.actor:
                flash('出演者を入力してください')
                return render_template('admin.html', drama=drama)
            db.session.add(drama)
            db.session.commit()
            flash('入力しました')
            return redirect(url_for('admin'))
        except IntegrityError:
            flash('このドラマは既に入力されています')
            return render_template('admin.html', drama=drama)
    else:
        drama = Drama()
        return render_template('admin.html', drama=drama)

@app.route('/posts/')
def posts_index():
    dramas = Drama.query.order_by(Drama.title.asc()).all()
    return render_template('posts_index.html', dramas=dramas)

@app.route('/posts/<int:id>', methods=['GET', 'POST'])
def posts_detail(id):
    if request.method == 'POST':
        post = Post(
            drama_id=id,
            star=request.form['star'],
            review=request.form['review-input']
        )
        drama = Drama.query.get(id)
        if not post.star and not post.review:
            flash('評価を選択してください')
            flash('レビューを入力してください')
            return render_template('posts_detail.html', drama=drama, post=post)
        if not post.star:
            flash('評価を選択してください')
            return render_template('posts_detail.html', drama=drama, post=post)
        if not post.review:
            flash('レビューを入力してください')
            return render_template('posts_detail.html', drama=drama, post=post)
        db.session.add(post)
        db.session.commit()
        total = 0
        num = 0
        for post in drama.posts:
            total += post.star
            num += 1
        drama.mean = round((total / num), 2)
        db.session.add(drama)
        db.session.commit()
        flash('投稿しました')
        return redirect(url_for('posts_detail', id=id))
    else:
        drama = Drama.query.get(id)
        post = Post()
        return render_template('posts_detail.html', drama=drama, post=post)


config.py(設定情報を記載したファイル)

DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///flask_drama.db'
SQLALCHEMY_TRACK_MODIFICATIONS = True
SECRET_KEY = '\xbf,M\xe4\x89\x92C$H\xbeC\x0f\xad\xca\x04\xd5\xa5\xd00^\x8e\xee\xa0\x1c'


manage.py(モデルをデータベースに反映するための実行ファイル)

from flask_script import Manager
from flask_drama import app
from flask_drama.scripts.db import InitDB


if __name__ == '__main__':
    manager = Manager(app)
    manager.add_command('init_db', InitDB())
    manager.run()


db.py(モデルをデータベースに反映するためのスクリプトファイル)

from flask_script import Command
from flask_drama import db


class InitDB(Command):
    'create database'

    def run(self):
        db.create_all()


users.py(ユーザー情報のデータベースモデル)

from flask_drama import db


class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True)
    password = db.Column(db.String(50))

    def __init__(self, username=None, password=None):
        self.username = username
        self.password = password


posts.py(レビュー情報のデータベースモデル)

from flask_drama import db


class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer, primary_key=True)
    drama_id = db.Column(db.Integer, db.ForeignKey('dramas.id'))
    star = db.Column(db.Integer)
    review = db.Column(db.String(200))

    def __init__(self, drama_id=None, star=None, review=None):
        self.drama_id = drama_id
        self.star = star
        self.review = review


dramas.py(ドラマタイトル情報のデータベースモデル)

from flask_drama import db


class Drama(db.Model):
    __tablename__ = 'dramas'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(50), unique=True)
    actor = db.Column(db.String(100))
    mean = db.Column(db.Float)
    posts = db.relationship('Post', backref='drama', lazy='dynamic')

    def __init__(self, title=None, actor=None, mean=None):
        self.title = title
        self.actor = actor
        self.mean = mean


layout.html(レイアウトのベーステンプレート)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>あなたにおすすめのドラマ ドラマのレビューを閲覧・投稿</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.1/css/all.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="{{ url_for('static', filename='jquery.raty.js') }}"></script>
  </head>
  <body>
    <header>
      <div class="container">
        <div id="header-left">
          <a id="top" href="{{ url_for('index') }}">
            <img src="{{ url_for('static', filename='coollogo_com-11407173.png') }}">
          </a>
        </div>
        <div class="clear"></div>
        <div id="header-right">
          {% if not session.logged_in %}
            <a id="new" href="{{ url_for('new') }}">新規登録</a>
            <a id="login" href="{{ url_for('login') }}">ログイン</a>
          {% else %}
            {% if session.admin_name %}
              <a id= "admin" href="{{ url_for('admin') }}">管理者ページ</a>
            {% endif %}
            <a id="logout" href="{{ url_for('logout') }}">ログアウト</a>
          {% endif %}
        </div>
        <div class="clear"></div>
      </div>
    </header>
    <div id="main">
      <div id="title">
        <h1>あなたにおすすめのドラマ</h1>
      </div>
      {% for message in get_flashed_messages() %}
      <div id="alert">
        <p>{{ message }}</p>
      </div>
      {% endfor %}
      <div class="container">
        {% block body %}{% endblock %}
      </div>
      <div class="clear"></div>
    </div>
    <script src="{{ url_for('static', filename='script.js') }}"></script>
  </body>
</html>


index.html(トップページ)

{% extends 'layout.html' %}
{% block body %}
<div id="reviews">
  <h2>おすすめのドラマ</h2>
  {% for drama in dramas %}
    <div class="review">
      <div class="drama-title">
        <h3>{{ drama.title }}</h3>
        <p>出演:{{ drama.actor }}</p>
        <div class="stars-review">
          {% if drama.mean %}
            <p class="stars" data-score="{{ drama.mean }}"></p>
            <div class="stars-value">
              <p>{{ drama.mean }}</p>
            </div>
          {% endif %}
          <a class="all detail-all" href="{{ url_for('posts_detail', id=drama.id) }}"><i class="fab fa-sistrix"></i><span>ドラマの詳細ページへ</span></a>
        </div>
      </div>
      <div class="drama-contents">
        {% if drama.posts[0] %}
          {% for post in drama.posts %}
            <div class="drama-content">
              <p class="stars" data-score="{{ post.star }}"></p>
              <p>{{ post.review }}</p>
            </div>
          {% endfor %}
        {% else %}
          <div class="review-nothing">
            <p>投稿はありません</p>
          </div>
        {% endif %}
      </div>
    </div>
  {% endfor %}
</div>
<div id="main-right">
  <div id="review-ranking">
    <h3>ドラマレビューランキングTOP10</h3>
    <div id="review-ranking-list">
      <ol>
        <li>坂の途中の家</li>
        <li>のの湯</li>
        <li>きのう何食べた?</li>
        <li>デジタル・タトゥー</li>
        <li>インハンド</li>
        <li>向かいのバズる家族</li>
        <li>わたし、定時で帰ります。</li>
        <li>賭ケグルイ season 2</li>
        <li>やじ×きた 元祖・東海道中膝栗毛</li>
        <li>大草原の小さな家</li>
      </ol>
      <a class="all" href="{{ url_for('posts_index') }}"><i class="fas fa-list"></i><span>ドラマの一覧ページへ</span></a>
    </div>
  </div>
  <div id="rate-ranking">
    <h3>視聴率ランキングTOP10</h3>
    <div id="rate-ranking-list">
      <ol>
        <li>緊急取調室 第3シリーズ</li>
        <li>特捜9 season2</li>
        <li>ラジエーションハウス</li>
        <li>科捜研の女 Season19</li>
        <li>集団左遷!!</li>
        <li>いだてん~東京オリムピック噺~</li>
        <li>インハンド</li>
        <li>わたし、定時で帰ります。</li>
        <li>俺のスカート、どこ行った?</li>
        <li>白衣の戦士!</li>
      </ol>
    </div>
  </div>
</div>
{% endblock %}


new.html(新規登録ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="form">
  <div id="form-title">
    <h2>新規登録</h2>
  </div>
  <div id="form-group">
    <form action="{{ url_for('new') }}" method=post>
      <label for="username">ユーザー名</label><br>
      {% if user.username %}
        <input id="username" type="text" name="username" value="{{ user.username }}"><br>
      {% else %}
        <input id="username" type="text" name="username" value=""><br>
      {% endif %}
      <label for="password">パスワード</label><br>
      {% if user.password %}
        <input id="password" type="password" name="password" value="{{ user.password }}"><br>
      {% else %}
        <input id="password" type="password" name="password" value=""><br>
      {% endif %}
      <input id="btn" type="submit" value="新規登録">
    </form>
  </div>
</div>
{% endblock %}


login.html(ログインページ)

{% extends 'layout.html' %}
{% block body %}
<div id="form">
  <div id="form-title">
    <h2>ログイン</h2>
  </div>
  <div id="form-group">
    <form action="{{ url_for('login') }}" method=post>
      <label for="username">ユーザー名</label><br>
      <input id="username" type="text" name="username" value="{{ username }}"><br>
      <label for="password">パスワード</label><br>
      <input id="password" type="password" name="password" value="{{ password }}"><br>
      <input id="btn" type="submit" value="ログイン">
    </form>
  </div>
</div>
{% endblock %}


admin.html(管理者ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="form">
  <div id="form-title">
    <h2>管理者ページ</h2>
  </div>
  <div id="form-group">
    <form action="{{ url_for('admin') }}" method=post>
      <label for="admin-title">ドラマのタイトル</label><br>
      {% if drama.title %}
        <input id="admin-title" type="text" name="admin-title" value="{{ drama.title }}"><br>
      {% else %}
        <input id="admin-title" type="text" name="admin-title" value=""><br>
      {% endif %}
      <label for="actor">出演者</label><br>
      {% if drama.actor %}
        <input id="actor" type="text" name="actor" value="{{ drama.actor }}"><br>
      {% else %}
        <input id="actor" type="text" name="actor" value=""><br>
      {% endif %}
      <input id="btn" type="submit" value="ドラマを追加">
    </form>
  </div>
</div>
{% endblock %}


posts_index.html(ドラマの一覧ページ)

{% extends 'layout.html' %}
{% block body %}
{% for drama in dramas %}
  <div class="drama-index">
    <a href="{{ url_for('posts_detail', id=drama.id) }}">{{ drama.title }}</a>
  </div>
{% endfor %}
{% endblock %}


posts_detail.html(ドラマの詳細ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="detail">
  <div id="drama-img">
    <img src="../static/{{ drama.title }}.jpg">
  </div>
  <div class="drama-title">
    <h3>{{ drama.title }}</h3>
    <p>出演:{{ drama.actor }}</p>
    <div class="stars-review">
      {% if drama.mean %}
        <p class="stars" data-score="{{ drama.mean }}"></p>
        <div class="stars-value">
          <p>{{ drama.mean }}</p>
        </div>
      {% endif %}
      <a class="all detail-all" href="{{ url_for('posts_index') }}"><i class="fas fa-list"></i><span>ドラマの一覧ページへ</span></a>
    </div>
  </div>
  <div class="drama-contents">
    {% if drama.posts[0] %}
      {% for post in drama.posts %}
        <div class="drama-content">
          <p class="stars" data-score="{{ post.star }}"></p>
          <p>{{ post.review }}</p>
        </div>
      {% endfor %}
    {% else %}
      <div class="review-nothing">
        <p>投稿はありません</p>
      </div>
    {% endif %}
  </div>
  {% if session.logged_in %}
    <div id="form">
      <div id="form-title">
        <h2>新規投稿</h2>
      </div>
      <div id="form-group">
        <form action="{{ url_for('posts_detail', id=drama.id) }}" method=post>
          <label for="star">評価</label><br>
          <select id="star" name="star">
            <option value="" {% if not post.star %}selected{% endif %}>選択してください</option>
            <option value="1" {% if post.star == '1' %}selected{% endif %}>星1つ</option>
            <option value="2" {% if post.star == '2' %}selected{% endif %}>星2つ</option>
            <option value="3" {% if post.star == '3' %}selected{% endif %}>星3つ</option>
            <option value="4" {% if post.star == '4' %}selected{% endif %}>星4つ</option>
            <option value="5" {% if post.star == '5' %}selected{% endif %}>星5つ</option>
          </select>
          <label for="review-input">レビュー</label><br>
          {% if post.review %}
            <textarea id="review-input" name="review-input" value="{{ post.review }}"></textarea><br>
          {% else %}
            <textarea id="review-input" name="review-input" value=""></textarea><br>
          {% endif %}
          <input id="btn" type="submit" value="新規投稿">
        </form>
      </div>
    </div>
  {% endif %}
</div>
{% endblock %}


style.css

* {
  box-sizing: border-box;
  margin: 0;
}

.container {
  width: 100%;
  padding: 0 20px;
}

a {
  text-decoration: none;
}

span {
  margin-left: 5px;
}

#alert {
  color: #ff0000;
  padding-left: 20px;
  text-align: center;
}

header {
  border-bottom: solid 1px #c0c0c0;
  box-shadow: 0 2px 4px #c0c0c0;
  width: 100%;
  height: 60px;
  position: fixed;
  top: 0px;
  z-index: 10;
  background-color: #ffffff;
}

.clear {
  clear: left;
  clear: right;
}

#header-left img {
  float: left;
  height: 40px;
  margin-top: 10px;
}

#header-right {
  float: right;
}

#header-right a {
  display: inline-block;
  line-height: 60px;
  transition: all 0.5s;
  padding: 0 10px;
}

#header-right a, #header-right a:visited {
  color: #000000;
}

#header-right a:hover {
  background-color: rgba(192, 192, 192, 0.2);
}

#new, #login, #admin, #logout {
  margin: 0 10px;
}

#main {
  padding: 100px 0 20px 0;
}

#title {
  padding-bottom: 20px;
  text-align: center;
  text-shadow: 4px 4px 1px #c0c0c0;
}

#reviews {
  float: left;
  width: 70%;
}

#reviews h2 {
  padding-bottom: 20px;
}

.review {
  border: solid 1px #c0c0c0;
  margin-bottom: 20px;
  border-radius: 5px;
  padding: 10px;
  box-shadow: 0 0 3px #c0c0c0;
}

.drama-title h3 {
  display: inline;
}

.drama-title p {
  margin-left: 10px;
  color: #6b6b6b;
  display: inline;
}

.drama-title {
  height: 70px;
  margin-bottom: 20px;
  border-bottom: solid 1px #c0c0c0;
}

.drama-content {
  padding-bottom: 20px;
}

#main-right {
  float: right;
  width: 30%;
}

#main-right h3 {
  border-bottom: solid 1px #c0c0c0;
  padding: 20px 0;
  margin-bottom: 20px;
  font-size: 18px;
  color: #6b6b6b;
}

#review-ranking, #rate-ranking {
  border: solid 1px #f5f5f5;
  border-radius: 5px;
  padding: 10px;
  background-color: #f5f5f5;
  margin-left: 20px;
  box-shadow: 0 0 3px #c0c0c0;
}

#review-ranking {
  margin-top: 50px;
  margin-bottom: 30px;
}

#review-ranking-list ol {
  padding-bottom: 10px;
}

.all {
  display: inline-block;
  color: #4387e9;
}

.all:visited {
  color: #4387e9;
}

.all:hover {
  text-decoration: underline;
}

#form-title {
  color: #6b6b6b;
  text-align: center;
  margin-top: 30px;
  margin-bottom: 20px;
}

#form-group {
  border: solid 1px #c0c0c0;
  padding: 20px;
  width: 40%;
  margin: 0 auto;
  box-shadow: 0 0 3px #c0c0c0;
  border-radius: 5px;
}

label {
  font-size: 20px;
}

#username, #password, #admin-title, #actor {
  width: 100%;
  height: 35px;
  margin-bottom: 20px;
  font-size: 20px;
  border: solid 1px #c0c0c0;
}

select {
  -webkit-appearance: none;
	-moz-appearance: none;
	appearance: none;
  width: 100%;
  height: 35px;
  margin-bottom: 20px;
  font-size: 20px;
  border: solid 1px #c0c0c0;
  border-radius: 0;
  background-color: #ffffff;
  cursor: pointer;
}

textarea {
  border: solid 1px #c0c0c0
}

#review-input {
  width: 100%;
  height: 200px;
  margin-bottom: 20px;
  font-size: 20px;
  resize: none;
}

#btn {
  color: #ffffff;
  background-color: #4387e9;
  padding: 5px 10px;
  opacity: 1;
  cursor: pointer;
  font-size: 20px;
  border-radius: 5px;
}

#btn:hover {
  opacity: 0.8;
}

.drama-index a {
  line-height: 30px;
  color: #000000;
}

.drama-index a:visited {
  color: #000000;
}

.drama-index a:hover {
  text-decoration: underline;
}

.stars-review {
  height: 40px;
  line-height: 40px;
}

.stars-value {
  display: inline;
}

.stars-value p {
  font-size: 20px;
  color: #000000;
}

.detail-all {
  float: right;
}

#drama-img {
  text-align: center;
  margin-bottom: 20px;
}


script.js

$(function() {
  $('.stars').raty({
    readOnly: true,
    score: function() {
      return $(this).attr('data-score');
    },
    path: '../static'
  });
});


起動ファイルであるserver.pyを実行するとアプリケーションが起動します。今回の改良点は以下のようになっています。

・ドラマの詳細ページにメインポスタービジュアルを挿入しました

結果

ドラマの詳細ページです。メインポスタービジュアルが表示されています。

f:id:nakatatsu_com:20190612225733p:plain

まとめ

・ドラマのレビューを閲覧、投稿できるサイトを作成した

・ドラマの詳細ページにメインポスタービジュアルを挿入した

参考文献

参考文献です。

ドラマレビューサイト(Flask)⑦

こんにちは。nakatatsuです。

前回に引き続きドラマレビューサイトを作成します。前回の詳細についてはこのブログの過去記事「ドラマレビューサイト(Flask)」、「ドラマレビューサイト(Flask)②」、「ドラマレビューサイト(Flask)③」、「ドラマレビューサイト(Flask)④」、「ドラマレビューサイト(Flask)⑤」、「ドラマレビューサイト(Flask)⑥」を参照してください。

nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com


仕様

完成形の仕様です。途中で変更があるかもしれません。

・ドラマのレビューを閲覧、投稿できるサイトを作成

・おすすめのドラマを5個ランダムに表示

・レビューランキングを表示

・視聴率ランキングを表示

準備

Pipenv、Flask、Flask-SQLAlchemy、Flask-Scriptのインストールについてはこのブログの過去記事「ドラマレビューサイト(Flask)」、「ドラマレビューサイト(Flask)②」を参照してください。

nakatatsu-com.hatenablog.com
nakatatsu-com.hatenablog.com


jQueryRatyについてはドラマレビューサイト(Flask)⑥」を参照してください。

nakatatsu-com.hatenablog.com


フォルダ構成

drama
├── Pipfile
├── Pipfile.lock
├── flask_drama
│   ├── __init__.py
│   ├── config.py
│   ├── flask_drama.db
│   ├── models
│   │   ├── dramas.py
│   │   ├── posts.py
│   │   └── users.py
│   ├── scripts
│   │   └── db.py
│   ├── static
│   │   ├── coollogo_com-11407173.png
│   │   ├── jquery.raty.js
│   │   ├── script.js
│   │   ├── star-half.png
│   │   ├── star-off.png
│   │   ├── star-on.png
│   │   └── style.css
│   ├── templates
│   │   ├── admin.html
│   │   ├── index.html
│   │   ├── layout.html
│   │   ├── login.html
│   │   ├── new.html
│   │   ├── posts_detail.html
│   │   └── posts_index.html
│   └── views.py
├── manage.py
└── server.py

フォルダ構成はこのようになっております。

ソースコード

server.py(起動ファイル)

from flask_drama import app


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


__init__.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy


app = Flask(__name__)
app.config.from_object('flask_drama.config')

db = SQLAlchemy(app)

import flask_drama.views


views.py

from flask import render_template
from flask import redirect
from flask import session
from flask import flash
from flask import url_for
from flask import request
from flask_drama import app
from flask_drama import db
from flask_drama.models.users import User
from flask_drama.models.dramas import Drama
from flask_drama.models.posts import Post
from sqlalchemy.exc import IntegrityError


@app.route('/')
def index():
    dramas = Drama.query.order_by(Drama.title.asc()).all()
    return render_template('index.html', dramas=dramas)

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        if not request.form['username'] and not request.form['password']:
            flash('ユーザー名を入力してください')
            flash('パスワードを入力してください')
            return render_template('login.html', username=request.form['username'], password=request.form['password'])
        if not request.form['username']:
            flash('ユーザー名を入力してください')
            return render_template('login.html', username=request.form['username'], password=request.form['password'])
        if not request.form['password']:
            flash('パスワードを入力してください')
            return render_template('login.html', username=request.form['username'], password=request.form['password'])
        users_model = User.query.all()
        for user_model in users_model:
            if request.form['username'] == user_model.username:
                if request.form['password'] == user_model.password:
                    session['logged_in'] = True
                    flash('ログインしました')
                    if user_model.username == 'nakatatsu':
                        session['admin_name'] = True
                    return redirect(url_for('index'))
                flash('パスワードが異なります')
                return render_template('login.html', username=request.form['username'], password=request.form['password'])
        flash('ログインできません')
        return render_template('login.html', username=request.form['username'], password=request.form['password'])
    else:
        return render_template('login.html', username='', password='')

@app.route('/logout')
def logout():
    session.pop('logged_in', None)
    session.pop('admin_name', None)
    flash('ログアウトしました')
    return redirect(url_for('index'))

@app.route('/new', methods=['GET', 'POST'])
def new():
    if request.method == 'POST':
        try:
            user = User(
                username=request.form['username'],
                password=request.form['password']
            )
            if not user.username and not user.password:
                flash('ユーザー名を入力してください')
                flash('パスワードを入力してください')
                return render_template('new.html', user=user)
            if not user.username:
                flash('ユーザー名を入力してください')
                return render_template('new.html', user=user)
            if not user.password:
                flash('パスワードを入力してください')
                return render_template('new.html', user=user)
            db.session.add(user)
            db.session.commit()
            session['logged_in'] = True
            flash('登録しました')
            return redirect(url_for('index'))
        except IntegrityError:
            flash('ユーザー名が既に使用されています')
            return render_template('new.html', user=user)
    else:
        user = User()
        return render_template('new.html', user=user)

@app.route('/admin', methods=['GET', 'POST'])
def admin():
    if request.method == 'POST':
        try:
            drama = Drama(
                title=request.form['admin-title'],
                actor=request.form['actor']
            )
            if not drama.title and not drama.actor:
                flash('ドラマのタイトルを入力してください')
                flash('出演者を入力してください')
                return render_template('admin.html', drama=drama)
            if not drama.title:
                flash('ドラマのタイトルを入力してください')
                return render_template('admin.html', drama=drama)
            if not drama.actor:
                flash('出演者を入力してください')
                return render_template('admin.html', drama=drama)
            db.session.add(drama)
            db.session.commit()
            flash('入力しました')
            return redirect(url_for('admin'))
        except IntegrityError:
            flash('このドラマは既に入力されています')
            return render_template('admin.html', drama=drama)
    else:
        drama = Drama()
        return render_template('admin.html', drama=drama)

@app.route('/posts/')
def posts_index():
    dramas = Drama.query.order_by(Drama.title.asc()).all()
    return render_template('posts_index.html', dramas=dramas)

@app.route('/posts/<int:id>', methods=['GET', 'POST'])
def posts_detail(id):
    if request.method == 'POST':
        post = Post(
            drama_id=id,
            star=request.form['star'],
            review=request.form['review-input']
        )
        drama = Drama.query.get(id)
        if not post.star and not post.review:
            flash('評価を選択してください')
            flash('レビューを入力してください')
            return render_template('posts_detail.html', drama=drama, post=post)
        if not post.star:
            flash('評価を選択してください')
            return render_template('posts_detail.html', drama=drama, post=post)
        if not post.review:
            flash('レビューを入力してください')
            return render_template('posts_detail.html', drama=drama, post=post)
        db.session.add(post)
        db.session.commit()
        total = 0
        num = 0
        for post in drama.posts:
            total += post.star
            num += 1
        drama.mean = round((total / num), 2)
        db.session.add(drama)
        db.session.commit()
        flash('投稿しました')
        return redirect(url_for('posts_detail', id=id))
    else:
        drama = Drama.query.get(id)
        post = Post()
        return render_template('posts_detail.html', drama=drama, post=post)


config.py(設定情報を記載したファイル)

DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///flask_drama.db'
SQLALCHEMY_TRACK_MODIFICATIONS = True
SECRET_KEY = '\xbf,M\xe4\x89\x92C$H\xbeC\x0f\xad\xca\x04\xd5\xa5\xd00^\x8e\xee\xa0\x1c'


manage.py(モデルをデータベースに反映するための実行ファイル)

from flask_script import Manager
from flask_drama import app
from flask_drama.scripts.db import InitDB


if __name__ == '__main__':
    manager = Manager(app)
    manager.add_command('init_db', InitDB())
    manager.run()


db.py(モデルをデータベースに反映するためのスクリプトファイル)

from flask_script import Command
from flask_drama import db


class InitDB(Command):
    'create database'

    def run(self):
        db.create_all()


users.py(ユーザー情報のデータベースモデル)

from flask_drama import db


class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True)
    password = db.Column(db.String(50))

    def __init__(self, username=None, password=None):
        self.username = username
        self.password = password


posts.py(レビュー情報のデータベースモデル)

from flask_drama import db


class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer, primary_key=True)
    drama_id = db.Column(db.Integer, db.ForeignKey('dramas.id'))
    star = db.Column(db.Integer)
    review = db.Column(db.String(200))

    def __init__(self, drama_id=None, star=None, review=None):
        self.drama_id = drama_id
        self.star = star
        self.review = review


dramas.py(ドラマタイトル情報のデータベースモデル)

from flask_drama import db


class Drama(db.Model):
    __tablename__ = 'dramas'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(50), unique=True)
    actor = db.Column(db.String(100))
    mean = db.Column(db.Float)
    posts = db.relationship('Post', backref='drama', lazy='dynamic')

    def __init__(self, title=None, actor=None, mean=None):
        self.title = title
        self.actor = actor
        self.mean = mean


layout.html(レイアウトのベーステンプレート)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>あなたにおすすめのドラマ ドラマのレビューを閲覧・投稿</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.1/css/all.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="{{ url_for('static', filename='jquery.raty.js') }}"></script>
  </head>
  <body>
    <header>
      <div class="container">
        <div id="header-left">
          <a id="top" href="{{ url_for('index') }}">
            <img src="{{ url_for('static', filename='coollogo_com-11407173.png') }}">
          </a>
        </div>
        <div class="clear"></div>
        <div id="header-right">
          {% if not session.logged_in %}
            <a id="new" href="{{ url_for('new') }}">新規登録</a>
            <a id="login" href="{{ url_for('login') }}">ログイン</a>
          {% else %}
            {% if session.admin_name %}
              <a id= "admin" href="{{ url_for('admin') }}">管理者ページ</a>
            {% endif %}
            <a id="logout" href="{{ url_for('logout') }}">ログアウト</a>
          {% endif %}
        </div>
        <div class="clear"></div>
      </div>
    </header>
    <div id="main">
      <div id="title">
        <h1>あなたにおすすめのドラマ</h1>
      </div>
      {% for message in get_flashed_messages() %}
      <div id="alert">
        <p>{{ message }}</p>
      </div>
      {% endfor %}
      <div class="container">
        {% block body %}{% endblock %}
      </div>
      <div class="clear"></div>
    </div>
    <script src="{{ url_for('static', filename='script.js') }}"></script>
  </body>
</html>


index.html(トップページ)

{% extends 'layout.html' %}
{% block body %}
<div id="reviews">
  <h2>おすすめのドラマ</h2>
  {% for drama in dramas %}
    <div class="review">
      <div class="drama-title">
        <h3>{{ drama.title }}</h3>
        <p>出演:{{ drama.actor }}</p>
        <div class="stars-review">
          {% if drama.mean %}
            <p class="stars" data-score="{{ drama.mean }}"></p>
            <div class="stars-value">
              <p>{{ drama.mean }}</p>
            </div>
          {% endif %}
          <a class="all detail-all" href="{{ url_for('posts_detail', id=drama.id) }}"><i class="fab fa-sistrix"></i><span>ドラマの詳細ページへ</span></a>
        </div>
      </div>
      <div class="drama-contents">
        {% if drama.posts[0] %}
          {% for post in drama.posts %}
            <div class="drama-content">
              <p class="stars" data-score="{{ post.star }}"></p>
              <p>{{ post.review }}</p>
            </div>
          {% endfor %}
        {% else %}
          <div class="review-nothing">
            <p>投稿はありません</p>
          </div>
        {% endif %}
      </div>
    </div>
  {% endfor %}
</div>
<div id="main-right">
  <div id="review-ranking">
    <h3>ドラマレビューランキングTOP10</h3>
    <div id="review-ranking-list">
      <ol>
        <li>坂の途中の家</li>
        <li>のの湯</li>
        <li>きのう何食べた?</li>
        <li>デジタル・タトゥー</li>
        <li>インハンド</li>
        <li>向かいのバズる家族</li>
        <li>わたし、定時で帰ります。</li>
        <li>賭ケグルイ season 2</li>
        <li>やじ×きた 元祖・東海道中膝栗毛</li>
        <li>大草原の小さな家</li>
      </ol>
      <a class="all" href="{{ url_for('posts_index') }}"><i class="fas fa-list"></i><span>ドラマの一覧ページへ</span></a>
    </div>
  </div>
  <div id="rate-ranking">
    <h3>視聴率ランキングTOP10</h3>
    <div id="rate-ranking-list">
      <ol>
        <li>緊急取調室 第3シリーズ</li>
        <li>特捜9 season2</li>
        <li>ラジエーションハウス</li>
        <li>科捜研の女 Season19</li>
        <li>集団左遷!!</li>
        <li>いだてん~東京オリムピック噺~</li>
        <li>インハンド</li>
        <li>わたし、定時で帰ります。</li>
        <li>俺のスカート、どこ行った?</li>
        <li>白衣の戦士!</li>
      </ol>
    </div>
  </div>
</div>
{% endblock %}


new.html(新規登録ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="form">
  <div id="form-title">
    <h2>新規登録</h2>
  </div>
  <div id="form-group">
    <form action="{{ url_for('new') }}" method=post>
      <label for="username">ユーザー名</label><br>
      {% if user.username %}
        <input id="username" type="text" name="username" value="{{ user.username }}"><br>
      {% else %}
        <input id="username" type="text" name="username" value=""><br>
      {% endif %}
      <label for="password">パスワード</label><br>
      {% if user.password %}
        <input id="password" type="password" name="password" value="{{ user.password }}"><br>
      {% else %}
        <input id="password" type="password" name="password" value=""><br>
      {% endif %}
      <input id="btn" type="submit" value="新規登録">
    </form>
  </div>
</div>
{% endblock %}


login.html(ログインページ)

{% extends 'layout.html' %}
{% block body %}
<div id="form">
  <div id="form-title">
    <h2>ログイン</h2>
  </div>
  <div id="form-group">
    <form action="{{ url_for('login') }}" method=post>
      <label for="username">ユーザー名</label><br>
      <input id="username" type="text" name="username" value="{{ username }}"><br>
      <label for="password">パスワード</label><br>
      <input id="password" type="password" name="password" value="{{ password }}"><br>
      <input id="btn" type="submit" value="ログイン">
    </form>
  </div>
</div>
{% endblock %}


admin.html(管理者ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="form">
  <div id="form-title">
    <h2>管理者ページ</h2>
  </div>
  <div id="form-group">
    <form action="{{ url_for('admin') }}" method=post>
      <label for="admin-title">ドラマのタイトル</label><br>
      {% if drama.title %}
        <input id="admin-title" type="text" name="admin-title" value="{{ drama.title }}"><br>
      {% else %}
        <input id="admin-title" type="text" name="admin-title" value=""><br>
      {% endif %}
      <label for="actor">出演者</label><br>
      {% if drama.actor %}
        <input id="actor" type="text" name="actor" value="{{ drama.actor }}"><br>
      {% else %}
        <input id="actor" type="text" name="actor" value=""><br>
      {% endif %}
      <input id="btn" type="submit" value="ドラマを追加">
    </form>
  </div>
</div>
{% endblock %}


posts_index.html(ドラマの一覧ページ)

{% extends 'layout.html' %}
{% block body %}
{% for drama in dramas %}
  <div class="drama-index">
    <a href="{{ url_for('posts_detail', id=drama.id) }}">{{ drama.title }}</a>
  </div>
{% endfor %}
{% endblock %}


posts_detail.html(ドラマの詳細ページ)

{% extends 'layout.html' %}
{% block body %}
<div id="detail">
  <div class="drama-title">
    <h3>{{ drama.title }}</h3>
    <p>出演:{{ drama.actor }}</p>
    <div class="stars-review">
      {% if drama.mean %}
        <p class="stars" data-score="{{ drama.mean }}"></p>
        <div class="stars-value">
          <p>{{ drama.mean }}</p>
        </div>
      {% endif %}
      <a class="all detail-all" href="{{ url_for('posts_index') }}"><i class="fas fa-list"></i><span>ドラマの一覧ページへ</span></a>
    </div>
  </div>
  <div class="drama-contents">
    {% if drama.posts[0] %}
      {% for post in drama.posts %}
        <div class="drama-content">
          <p class="stars" data-score="{{ post.star }}"></p>
          <p>{{ post.review }}</p>
        </div>
      {% endfor %}
    {% else %}
      <div class="review-nothing">
        <p>投稿はありません</p>
      </div>
    {% endif %}
  </div>
  {% if session.logged_in %}
    <div id="form">
      <div id="form-title">
        <h2>新規投稿</h2>
      </div>
      <div id="form-group">
        <form action="{{ url_for('posts_detail', id=drama.id) }}" method=post>
          <label for="star">評価</label><br>
          <select id="star" name="star">
            <option value="" {% if not post.star %}selected{% endif %}>選択してください</option>
            <option value="1" {% if post.star == '1' %}selected{% endif %}>星1つ</option>
            <option value="2" {% if post.star == '2' %}selected{% endif %}>星2つ</option>
            <option value="3" {% if post.star == '3' %}selected{% endif %}>星3つ</option>
            <option value="4" {% if post.star == '4' %}selected{% endif %}>星4つ</option>
            <option value="5" {% if post.star == '5' %}selected{% endif %}>星5つ</option>
          </select>
          <label for="review-input">レビュー</label><br>
          {% if post.review %}
            <textarea id="review-input" name="review-input" value="{{ post.review }}"></textarea><br>
          {% else %}
            <textarea id="review-input" name="review-input" value=""></textarea><br>
          {% endif %}
          <input id="btn" type="submit" value="新規投稿">
        </form>
      </div>
    </div>
  {% endif %}
</div>
{% endblock %}


style.css

* {
  box-sizing: border-box;
  margin: 0;
}

.container {
  width: 100%;
  padding: 0 20px;
}

a {
  text-decoration: none;
}

span {
  margin-left: 5px;
}

#alert {
  color: #ff0000;
  padding-left: 20px;
  text-align: center;
}

header {
  border-bottom: solid 1px #c0c0c0;
  box-shadow: 0 2px 4px #c0c0c0;
  width: 100%;
  height: 60px;
  position: fixed;
  top: 0px;
  z-index: 10;
  background-color: #ffffff;
}

.clear {
  clear: left;
  clear: right;
}

#header-left img {
  float: left;
  height: 40px;
  margin-top: 10px;
}

#header-right {
  float: right;
}

#header-right a {
  display: inline-block;
  line-height: 60px;
  transition: all 0.5s;
  padding: 0 10px;
}

#header-right a, #header-right a:visited {
  color: #000000;
}

#header-right a:hover {
  background-color: rgba(192, 192, 192, 0.2);
}

#new, #login, #admin, #logout {
  margin: 0 10px;
}

#main {
  padding: 100px 0 20px 0;
}

#title {
  padding-bottom: 20px;
  text-align: center;
  text-shadow: 4px 4px 1px #c0c0c0;
}

#reviews {
  float: left;
  width: 70%;
}

#reviews h2 {
  padding-bottom: 20px;
}

.review {
  border: solid 1px #c0c0c0;
  margin-bottom: 20px;
  border-radius: 5px;
  padding: 10px;
  box-shadow: 0 0 3px #c0c0c0;
}

.drama-title h3 {
  display: inline;
}

.drama-title p {
  margin-left: 10px;
  color: #6b6b6b;
  display: inline;
}

.drama-title {
  height: 70px;
  margin-bottom: 20px;
  border-bottom: solid 1px #c0c0c0;
}

.drama-content {
  padding-bottom: 20px;
}

#main-right {
  float: right;
  width: 30%;
}

#main-right h3 {
  border-bottom: solid 1px #c0c0c0;
  padding: 20px 0;
  margin-bottom: 20px;
  font-size: 18px;
  color: #6b6b6b;
}

#review-ranking, #rate-ranking {
  border: solid 1px #f5f5f5;
  border-radius: 5px;
  padding: 10px;
  background-color: #f5f5f5;
  margin-left: 20px;
  box-shadow: 0 0 3px #c0c0c0;
}

#review-ranking {
  margin-top: 50px;
  margin-bottom: 30px;
}

#review-ranking-list ol {
  padding-bottom: 10px;
}

.all {
  display: inline-block;
  color: #4387e9;
}

.all:visited {
  color: #4387e9;
}

.all:hover {
  text-decoration: underline;
}

#form-title {
  color: #6b6b6b;
  text-align: center;
  margin-top: 30px;
  margin-bottom: 20px;
}

#form-group {
  border: solid 1px #c0c0c0;
  padding: 20px;
  width: 40%;
  margin: 0 auto;
  box-shadow: 0 0 3px #c0c0c0;
  border-radius: 5px;
}

label {
  font-size: 20px;
}

#username, #password, #admin-title, #actor {
  width: 100%;
  height: 35px;
  margin-bottom: 20px;
  font-size: 20px;
  border: solid 1px #c0c0c0;
}

select {
  -webkit-appearance: none;
	-moz-appearance: none;
	appearance: none;
  width: 100%;
  height: 35px;
  margin-bottom: 20px;
  font-size: 20px;
  border: solid 1px #c0c0c0;
  border-radius: 0;
  background-color: #ffffff;
  cursor: pointer;
}

textarea {
  border: solid 1px #c0c0c0
}

#review-input {
  width: 100%;
  height: 200px;
  margin-bottom: 20px;
  font-size: 20px;
  resize: none;
}

#btn {
  color: #ffffff;
  background-color: #4387e9;
  padding: 5px 10px;
  opacity: 1;
  cursor: pointer;
  font-size: 20px;
  border-radius: 5px;
}

#btn:hover {
  opacity: 0.8;
}

.drama-index a {
  line-height: 30px;
  color: #000000;
}

.drama-index a:visited {
  color: #000000;
}

.drama-index a:hover {
  text-decoration: underline;
}

.stars-review {
  height: 40px;
  line-height: 40px;
}

.stars-value {
  display: inline;
}

.stars-value p {
  font-size: 20px;
  color: #000000;
}

.detail-all {
  float: right;
}


script.js

$(function() {
  $('.stars').raty({
    readOnly: true,
    score: function() {
      return $(this).attr('data-score');
    },
    path: '../static'
  });
});


起動ファイルであるserver.pyを実行するとアプリケーションが起動します。今回の改良点は以下のようになっています。

・Dramaモデルにレビューの平均を表すmeanプロパティを追加しました

・トップページからドラマの一覧ページ、詳細ページに遷移することができるリンクを作成しました

・ドラマの詳細ページからドラマの一覧ページに遷移することができるリンクを作成しました

結果

トップページです。総合評価はスター表示と数字で表しています。

f:id:nakatatsu_com:20190612013750p:plain

まとめ

・ドラマのレビューを閲覧、投稿できるサイトを作成した

・ドラマの総合評価をスター表示と数字で表した

参考文献

参考文献です。