nikkie-ftnextの日記

イベントレポートや読書メモを発信

LT報告 | #jawsdays で機械学習を組み込んだPython製アプリのデモLTをしました

はじめに

いつも心は虹色に! nikkieです。
2/23のJAWS DAYS 2019に参加し、懇親会でLTしてきました。
デモアプリを使ったLT(いわゆるLT駆動開発)をして思うところをまとめます。

勉強会の概要

About | JAWS DAYS 2019

JAWS-UGによる全国規模の交流イベントを開催いたします!
AWS初心者から上級者までのエンジニア、経営者や人事、マーケティングエンタープライズからスタートアップ、中小企業など職種や業態・会社規模を問わず、たくさんの方に参加いただけるイベントです。

今年のテーマは「満漢全席」
海外UGのセッションも含め、多岐にわたるテクニカルセッション、ビジネスセッション、ランチセッション、OpenMic、LTも含めて「100以上のセッション」を用意します。

AWSの情報収集がしたくてJAWS DAYSに参加しました。1
今回が初めてのAWS関連のイベントです。

LT駆動開発の動機

1月くらいに「懇親会LT募集」が告知され、「Amazon MLについて話せそうだし、AWSを触ってみるLT駆動開発をやってみよう」と応募しました。
LTをすることで、

  • 聞き手(AWS経験豊富な方々):機械学習Amazon MLを使用)が選択肢に上がるようになる
  • 私(AWS初学者):Amazon ML以外のAWSサービスを触ってみる

という狙いがありました。

LT: Amazon Machine Learningで機械学習始めませんか?

懇親会を盛り上げるため、デモを中心に構成しました。(デモ→理論の順)
このレポートでは、本来想定していた講義形式(理論→デモ)で説明していきます。

伝えたかったこと

手元にたまったデータをもとに、未知のデータを予測する推論のルールを作るのが、機械学習で可能なことの一つです。
デモアプリの例では、手元にタイタニック号の乗客データがたまっています。(26歳・男性は死亡、28歳・女性は生存、といったデータが800〜900あります)
乗客データから推論のルール(=モデル)を作ることで、生存/死亡が不明のデータ(28歳・男性)についてモデルで予測できます
そして、モデルをAPIにすることで、モデルの推論結果をプログラムから利用しやすくできます。
28歳・男性という属性情報をAPIに送ると、APIは生存または死亡という予測結果を返します。

モデルの作成〜API化までをウィザード操作で可能にするのが、Amazon Machine Learning(以下Amazon MLです)
ウィザード操作ですのでコーディングは不要ですし、APIにまでしてくれるので、得意な言語から呼び出して、モデルの推論結果を利用できます。
Amazon MLで作ったモデルの使い方がイメージしてもらえるよう、「あなたがタイタニック号に乗っていたら助かるでしょうか?」というデモアプリを用意しました。

タイタニック乗客の生死の予測では、現実にアプリに機械学習を組み込む場面を想像しにくいかもしれません。
ビジネス面に即せば、例えば、優良顧客の判定といった事例が考えられます。
(年齢・性別などの顧客属性と優良顧客か否かというデータからモデルを作り、属性から優良顧客か判定する機能を実現します)

使った技術

AWSサービス

機能について補足します。
生存/死亡を予測するモデルには、入力として属性情報(年齢・性別など)が必要です。
学習した際に使ったデータと丸々同じデータをモデルに入力しなければエラーとなってしまいます。
ですので、手元のデータから作ったモデルでは、顔写真から生死を予測することはできません。

顔写真から年齢・性別情報を取り出せば、今回のモデルで生死を予測できます。
顔写真から年齢・性別情報の取り出しにRekognitionを使いました。 Rekognitionで取り出した年齢・性別情報2を手元のモデルに渡すと、生存/死亡が予測されます。

f:id:nikkie-ftnext:20190224004329p:plain

Pythonパッケージ

  • Flask
  • boto3
  • zappa

パッケージの所感はこちら:イベントレポート | #pyhack にてAmazon MLの機械学習モデルを組み込んだFlask製アプリをzappaでLambdaにデプロイしました - nikkie-ftnextの日記

顔写真から助かるかどうかを予測する機能について解説します。(boto3を使って、Amazon MLやRekognition、S3を呼び出しています)

アップロードされた顔写真を画面に表示する(imgタグ)ため、まずS3に画像をアップロードしています。

  1. S3の画像を指定してRekognitionを叩く
  2. S3の画像について制限時間付きで有効なURLを取得(imgタグに使う)

当初はアップロードされた画像を(バイナリで)Rekognitionへ送り、その後S3へアップロードし、imgタグ用のURLを取得の順で考えていました。
ところが、Rekognitionへ送った段階でファイルがクローズしてしまうようで、Rekognitionへ送るかS3へアップロードするか一方しかできないと認識しました。
S3の画像指定でRekognitionが呼び出せたので、後者を選択して上記の流れになりました。

なお、ソースコードはこちらです

フロントエンドに挑戦

サーバサイドに比べて経験が少ないのですが、デモできるくらいには見た目を整えることに挑戦しました。

懇親会ではスマホでデモアプリにアクセスしてもらう想定でしたので、Bootstrapを採用しました。
exampleを真似してモバイルでも崩れない表示にすることはできました。
Checkout example · Bootstrap

f:id:nikkie-ftnext:20190224004403p:plain

顔写真から予測する処理は処理時間が長く、処理中ということをユーザにフィードバックする必要がありました。(連打されて同一画像が複数回アップロードされるのは望んでいません)
jQueryフォーム送信時(formのsubmitイベント)に、ボタンをdisabledにすることにして対応しました。

やろうとしてできなかったこと

期間内に実装が終わらなかったアイデア一覧です。

  • 画像アップロード処理のスレッド化
    • Rekognition呼び出しとimgタグ指定用のURLの指定は並列化できると考えています(処理時間がどれだけ変わるかわからないですが)
    • Amazon MLで作ったモデルの呼び出しをJSで実装してみたいです
  • ローディング表示(ajax?)
  • 利用データのDB保存
    • loggingでCloudWatchに流しています3が、DynamoDB4にためて分析の余地を用意しておきたいです
  • zappaでdebug=TrueのFlaskアプリのデプロイは、ベストプラクティス?

ネタバラシ

繰り返しになりますが、今回はモデル(推論のルール)を手元にたまったタイタニック乗客データから作っています。
つまり、モデルは手元のデータの傾向を反映します。
実はタイタニックの乗客の生存と死亡には、性別と年齢が大きく関わっています。(チケットのクラスと乗船した港は寄与が小さいです)
救命ボートの数が足りず、「女性と子供優先」という方針が取られたため、生存者には女性と子供が多いです。
つまり、女性(でかつ、低い年齢)として情報を送れば、生存と予測されます。

f:id:nikkie-ftnext:20190224004444p:plain

終わりに

LT駆動開発してよかった」この一言につきます。
決して現在のアプリに満足しているわけではありません。
フロントエンドはまだまだですし、サーバサイドは試したいことが多々あります。
それでも作りたいものを時間内で実現するため、通勤時間で必死に調べ、睡眠時間を少し削って実装時間を確保し、「これでいけるだろう」という案が動いたときの高揚感!
資料を作って登壇するという経験とは別種の学びがありました。
不完全なアプリでもアウトプットするのは重要ですね。
自分のスキルアップのために、機能の追加(ブラッシュアップ)に挑戦していきます!

AWSを触ってみたところ、Lambdaへのzappaでのデプロイは非常に快適で5、Lambdaを触れたのがとりわけ大きいと思っています。

最後になりますが、初参加のJAWS DAYSで、AWS初学者にLT駆動開発の機会をくださった運営の皆さまにお礼申し上げます。
LTを聞いてくださった皆さま、デモアプリで遊んでくださった皆さま(50名超!)、Twitterでツイートしてくださった方々、誠にありがとうございました。
ここまでお読みいただき、ありがとうございました。


  1. これまでAzureメイン、k8s体験をGCPといった感じで触ってきています。Python界隈では「Azureでハマっている人」という認識もされているようです。

  2. 説明中は年齢と性別に代表させていますが、データにはチケットのクラスと乗船した港も含まれています。「ネタバラシ」で述べますが、年齢と性別が予測への寄与が大きいです

  3. loggingで書くだけでCloudWatchに記録されるのは驚きです。ref: Python の AWS Lambda 関数ログ作成 - AWS Lambda

  4. サーバレスで実装するアプリはRESTで叩けるNoSQL DBがプラクティスとのことです(初心者ハンズオンより)。RDBだとConnection Poolがないので同時に大量のConnectionが張られてしまう

  5. かつて WebAppsにデプロイは難しい というLTをしたことがあります

イベントレポート | #pyhack にてAmazon MLの機械学習モデルを組み込んだFlask製アプリをzappaでLambdaにデプロイしました

はじめに

いつも心は虹色に! nikkieです。
2/16開催 (第96回)Python mini Hack-a-thon のイベントレポートをお送りします。

勉強会の概要

(第96回)Python mini Hack-a-thon - connpass

基本的に毎月開催です。スプリントのゆるい版みたいな感じで各自自分でやりたいことを持ってきて、勝手に開発を進めています。参加費は無料です。 初めての方も常連さんもぜひご参加ください。

冬山合宿ぶりのpyhack。
参加者数が定員に収まり、こぢんまりとした雰囲気の2月回でした。
(エンジニア界隈としてはデブサミ明け、かつ、裏番組ららこんだったようです)

取り組んだこと

  1. Amazon Machine Learning(以下、Amazon ML)でタイタニック号乗客の生死を予測するモデルを作り、APIにする
  2. 1で作ったAPIをFlaskのアプリから呼び出して使う(入力フォームと結果表示ページを用意)
  3. 2のFlaskアプリをAWS Lambdaにデプロイする
  4. 入力フォーム以外に顔写真のアップロードでも生死が予測できるように機能追加(Rekognitionで顔写真から年齢・性別推定)

これは来週のJAWS DAYS 2019の懇親会LTの仕込みです。
Amazon Machine Learningで機械学習始めませんか? | JAWS DAYS 2019
エンジニア界隈デファクトAWSをどこかで触ってみたかったので、今回LT駆動開発をしています。

達成したこと

上記の1〜4は一通り達成しました。
今回のアプリは #大晦日ハッカソン の成果物をベースにしており、新規で開発する部分は少なかったためです。
(むしろ、LambdaからAmazon MLを呼び出すなど設定周りで苦心しました)

▼現状のデモアプリ(入力フォーム)
f:id:nikkie-ftnext:20190217001742p:plain
▼現状のデモアプリ(顔写真アップロードフォーム)
f:id:nikkie-ftnext:20190217001755p:plain

    return render_template(
        'predict.html',
        prediction=PREDICTION[predict_index],
        input_data=input_data
    )

他の方の取り組みから

感想

LambdaでLT用のデモアプリを作るという目的は達成できたので大満足です。
CSS設定やアップロードされた画像の表示機能(成果発表でのフィードバック)など、時間が許す限り準備していきます。(まずはスライドを作ろう)
AWSのロールまわりは脳内が整理しきれていませんが、引き続き触って理解を進めていきます。(当日猛者に質問してもいいな)

成果発表やお昼・懇親会の場で興味深い話が聞け、最近アウトプットが多かった身としては、「まだまだこんなに知らないことがあるのか!」と燃料が再チャージされた感覚です。
参加者の皆さま、1日ありがとうございました & お疲れさまでした!

イベントレポート | #azuremoku2 にてAzure ML Serviceのクイックスタート・チュートリアルを触りました

はじめに

いつも心は虹色に! nikkieです。
2/9開催の 第三十回 Azureもくもく会 のイベントレポートをお送りします。

勉強会の概要

第三十回 Azureもくもく会 @ 品川 - connpass

Azureに関する作業をもくもく行う会です。 XamarinでAzure連携を試してみる、WebAppsのGitを利用した自動デプロイをSlackに通知してみるのを試す等々、やりたかったこと研究したいことをもくもく作業できる場所です。 初心者の方から既にAzureを利用している玄人の方まで幅広くご参加いただける事を楽しみにしております。

2018年3月以来の約1年ぶりの参加でした。
先日以下のウェビナー1を聞き、Azure Machine Learning Service(以下Azure ML Serviceと略)で手を動かしてみたくなり、今回久しぶりに参加しました。

Connect(); 徹底解説シリーズ!~ AI 編 ~ 最新の環境が最短の開発だ! Azure Machine Learning update 2019年1月版 [ウェビナー]

取り組んだこと

以下3点で手を動かしました。2

達成したこと

手を動かしてわかったことを書いていきます。

前提

  • Workspaceの作成はAzure Portalを操作しています(azコマンドは今回使っていません)
  • Azure NotebookにてPythonコードを実行しています(Python3.6のノートブックを使用)3

※Azure ML ServiceはAzure Notebookからだけでなく、手元のPCのJupyter Notebookからでも扱えそうなので、今後試してみます

クイックスタート

Azure portal を使用したクイック スタート - Azure Machine Learning service | Microsoft Docs

  1. Azure PortalからWorkspaceを作成
  2. WorkspaceのリンクからAzure Notebookを開き、「Getting started」プロジェクトをクローン
  3. Azure NotebookからWorkspaceに接続し、Pythonコードを実行(後述のRun)

モンテカルロ法で円周率を推定しました。
2回実行しての結果がWorkspaceで確認できます。

f:id:nikkie-ftnext:20190209194010p:plain

現時点の理解を書き留めておきます:

  • Workspace:Azure ML Serviceの単位(コンテナレジストリやストレージがラップされている)
  • Experiment:Runを集めたもの。クイックスタートでは2つのRun(2回の推定結果)をもたせた
  • Run:Pythonのコードを実行、機械学習のタスクに対応。クイックスタートでは、1回の円周率の推定

チュートリアル1(MNIST)

イメージの分類チュートリアル:モデルをトレーニングする - Azure Machine Learning service | Microsoft Docs

ロジスティック回帰でMNIST(手書き数字の分類)に取り組みました。
円周率の推定の例との違いは、AzureにVM(CPUマシン)を立てていることです。

  • 円周率の推定:ローカル(Azure Notebook)で実行
  • MNIST:リモート(Azure Machine Learning コンピューティング=AzureのVM)で実行

リモートで実行するために、データや学習に使うPythonコードを送っています。
リモートでどのコードをどのデータに対して実行するかを指定(Runについて指定)したあとは、Azure Notebook上で状況が確認できました。
途中経過からリモートではDockerイメージのプルや途中経過の保存などが行われており、Workspaceがラップしているコンテナレジストリ、ストレージが関わってくるようです。

f:id:nikkie-ftnext:20190209194934p:plain

チュートリアル2(機械学習の自動化)

回帰モデルのチュートリアル: 自動化された ML - Azure Machine Learning service | Microsoft Docs

タクシーの料金を予測する回帰モデルを自動化して作成しました。
dataprep SDKでデータの前処理をしたあと、回帰モデルを自動で作成します。
Azure NotebookのFree CPUだと1イテレーションに1-2分かかったので、泣く泣く5回のうちから選抜しました。

f:id:nikkie-ftnext:20190209194958p:plain f:id:nikkie-ftnext:20190209195009p:plain f:id:nikkie-ftnext:20190209195020p:plain

今後調査したいこと

  • Azure NotebookのFree CPUよりスペックが高い環境で実行する(手元のPC上で実行、または、Azure NotebookからDSVMへ接続)
  • MNISTの例のようにリモート(AzureのVM)で自動化された機械学習を実行するにはどうすればいいのか?(データをまとめて送ればいい?)
  • dataprepを使った前処理が自分で書ける気がしないので、チュートリアルをやる https://docs.microsoft.com/ja-jp/python/api/overview/azure/dataprep/intro?view=azure-dataprep-py
  • または、pandasでタクシーデータを扱ってみる(dataprepがpandasライクに使えないことで、学習コストが上がってしまっている印象です😫)

他の方の取り組みから

感想

Azure ML Service、クイックスタートとチュートリアルでどんな感じのものかはつかめました。
個々のリソースの関係性を言語化するためにも、もう少し触りたいところです。
Kaggleのデータセットなどで使ってみようかな。(ログや性能の管理が楽になることを体感したい)

資格試験が刷新されたなど、Azure周りの情報収集もでき、作業環境も快適(VIP会議室で特に椅子が快適)で大満足です。
運営者、参加者の皆さま、半日ありがとうございました & お疲れさまでした。


  1. https://github.com/aldente-dev/aldente-dev.github.io/blob/master/connect2018/webiner.md より。2/13にDevOps、2/20にk8sとウェビナーは続くようです

  2. ウェビナーで教えていただいた https://docs.microsoft.com/ja-jp/azure/machine-learning/service/ からクイックスタート→チュートリアルの順で取り組んでいます。

  3. クイックスタート用のプロジェクトをクローンすると、チュートリアルソースコード込みで自分のアカウントにコピーされました

イベントレポート | #WEBエンジニア勉強会11 にていろいろな話が聞けて世界が広がりました

はじめに

いつも心は虹色に! nikkieです。
2/1開催の #WEBエンジニア勉強会 のイベントレポートをお送りします。

勉強会の概要

WEBエンジニア勉強会 #11 (東京都, 渋谷) - connpass

第11回目になる、WEBエンジニア初心者でも参加できることを心がけ・気軽に発表できるWEBエンジニアのための勉強会コミュニティです。初心者の方でも、一人でも、SIerの方も、学生の方も、みなさんお気軽にご参加ください。
今回もビアバッシュ形式の勉強会です。 ピザ・飲み物・おつまみを用意する予定です。 飲み食いしながらワイワイとやりましょう!

昨年11月の10回目に続いて2回目の参加です。
初参加の10回目が楽しかったので、今回も楽しみにしていました。
アウトプットに結びつけるために今回はブログ枠での参加です。

10回目の私の様子はこちらのツイート一覧から観察できます。
また、いわしまんさんのブログに10回目の様子がまとまっています!
【イベント】WEBエンジニア勉強会#10にお邪魔してきた話 - Rのつく財団入り口

発表振り返り

冒頭の『「みてね」を推す理由』には残念ながら間に合わず(無念)、決済の仕組みの話から参加となりました。
Web開発に限らず、開発一般に共通する話が多かった印象です。
今回はどんな発表があったかを数行でまとめます(リンクからスライドをご覧いただけます)

11回目の私のツイート一覧はこちら
発表を聞いて考えたことや手を動かしたことを今後アウトプット予定です。

感想

10回目よりも多様なテーマの話が聞けた回でした(同じ卓で「色んな話が聞けていいですね」という話になりました)。
アウトプットのネタをたくさんいただいたので、少しずつになるかと思いますが、今後消化していきます。
発表者、運営者、参加者の皆さま、ありがとうございました。

イベントレポート | #rettypy にてタイタニックハンズオンの準備に取り組みました

はじめに

明けましておめでとうございます1、nikkeです。
2019年一発目のブログはもくもく会参加レポートです。

勉強会の概要

Pythonもくもく自習室 #17 @ Rettyオフィス 2019書き初め - connpass

週末の昼下がりに、Pythonに関するやりたいことを各自持ち寄り、ゆるゆると「自習」を進めていく会です。
Python使いおよびエンジニアライフのオアシス&知見を身につける「自習室」としてゆるくご活用いただければと思います。

「技術とイイゴハン(主にランチ)を楽しむ」rettypy。
2018年9月のPyConJP前の会以来の参加となりました。
前回:イベントレポート | Pythonもくもく自習室 #13 あやめの分類器をAPIに組み込み、Dockerイメージ作成 #rettypy - nikkie-ftnextの日記

取り組んだこと

1/30に開催するKaggleタイタニックの生存予測ハンズオンの準備を進めました。

先週のpyhack合宿にて壁打ちし、100件超えのコメントをいただきました。(レビューをいただき誠にありがとうございます!)

コメントに優先度をつけた上で、

の2点を進めました。

達成したこと

時間は限られていますが、少しずつアップデートしていきます。

知ったこと

PyDataのオーガナイザの方から、PyDataでのタイタニックハンズオンの経験(分析が大事)をご共有いただき、ありがたかったです。

他の方の取り組みから

Djangoの静的ファイル関連

Django Girls Tutorialで追加したCSSファイルが効かないという質問がありました。2
CSSでカワイくしよう · Django Girls Tutorial

shinyorkeさんの回答から私の静的ファイルの理解が浅いと気づき、ドキュメントを確認しました。
静的ファイル (画像、JavaScript、CSS など) を管理する | Django documentation | Django
PythonAnywhereでcollectstaticする意味がうっすらとわかった気がします。

  • debugがTrueなら自動で静的ファイル配信(runserver)
  • 本番サーバはcollectstaticで静的ファイル配信ディレクトリに収集

runserverと本番環境とで違うことでハマりやすくなっているように感じます。
Dockerで環境用意して本番環境の設定で開発してみようと思います。

なお本番サーバでの静的ファイル配信方法も数パターンあるようです。
静的ファイルのデプロイ | Django documentation | Django

  • アプリと同じサーバから配信するケース(Apacheの設定例あり)
  • 専用サーバから配信するケース
  • クラウドサービスやCDNから配信するケース(S3の例あり)

感想

「ハンズオンのTAやってもいいですよ」と立候補してくださった方がいらっしゃって、大変嬉しかったです。
Django関係でDjango Girls Tutorialを紹介いただき、参加した方のもくもくがはかどったのでしたら、翻訳に参加した身としてこんなに嬉しいことはありません。

1日ありがとうございました。


  1. 勉強会登壇という大きなアウトプットが控えており、その準備を優先していました。だいぶ間が空いてしまったと反省しており、アウトプットの敷居を下げて習慣を取り戻そうと思います。

  2. runserverで動かす際、テンプレートやCSSを追加したときにそれが見つからない場合があります。Ctrl+Cで一度止め、runserverを再度実行することで解決します。質問者の方は再実行で解決したそうです。

Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 3/3) コメント機能 <後編>

※1本の記事として書き上げましたが、記事が長くなりすぎたため、前編・後編に分けています。

ブログからコメントを作成できるようにする(承前)

コメント入力画面へ遷移するように設定(承前)

AttributeErrorへの対応として、blog/views.pyadd_comment_to_post関数を用意します。

from django.shortcuts import render, get_object_or_404
from django.utils import timezone
from .models import Post
# importの行は以下の1行のみ変更
from .forms import PostForm, CommentForm # CommentFormのimportを追加

# これまでに作成した関数は変更なし

# 以下の行を追加
def add_comment_to_post(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == "POST":
        form = CommentForm(request.POST)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.post = post
            comment.save()
            return redirect('post_detail', pk=post.pk)
    else:
        form = CommentForm()
    return render(request, 'blog/add_comment_to_post.html', {'form': form})

Djangoフォーム · Django Girls Tutorial でやったように、comment = form.save(commit=False)で、コメント用フォームに結びつけられたデータからデータベースに保存するためのコメントのオブジェクトを生成しています。1
ForeignKeyのpostについて、comment.post = postとして、コメントが紐づく記事を設定しています。2
(記事のidを渡すのでなく、記事そのものを渡すと、Django側で処理してくれるようです。)

add_comment_to_post関数が追加されたので、記事詳細画面が表示されるようになります。(URL例:http://localhost:8000/post/1/
f:id:nikkie-ftnext:20181229182441p:plain

コメント入力フォーム追加

ところが、追加した「Add comment」ボタンをクリックすると、TemplateDoesNotExistというエラーが表示されます。
f:id:nikkie-ftnext:20181229182510p:plain

「Add comment」ボタンをクリックすると、先ほど追加したadd_comment_to_post関数が呼び出されます。
関数の中で、render(request, 'blog/add_comment_to_post.html', {'form': form})と、blog/add_comment_to_post.htmlテンプレートを表示するように設定しているのですが、そのテンプレートが見つからずエラーとなっています。
blog/add_comment_to_post.htmlテンプレートを作りましょう。

blog/templates/blog/add_comment_to_post.html

{% extends 'blog/base.html' %}

{% block content %}
  <h1>New comment</h1>
  <form method="POST" class="post-form">{% csrf_token %}
    {{ form.as_p }}
    <button type="submit" class="save btn btn-default">Send</button>
  </form>
{% endblock %}

add_comment_to_post.htmlpost_edit.htmlテンプレートと非常に似ています。
テンプレートが追加されたので、「Add comment」ボタンをクリックした後のエラーが消え、コメントが書けるようになりました。

▼ログインしていなくてもコメントが書けます f:id:nikkie-ftnext:20181229182522p:plain

▼追加されました f:id:nikkie-ftnext:20181229182533p:plain

コメントの承認機能と削除機能をつける

コメント承認機能・コメント削除機能追加

ここまででブログの記事に誰もがコメントを書け、そのコメントは誰でも見られるようになりました。
ここでは、ブログにログインしているユーザが承認したコメントに限って、誰でも見られるように修正します。

記事詳細画面の変更内容

  • ログインしているユーザは、すべてのコメントが見られる:(1)
  • ログインしているユーザは、コメントの承認または削除ができる3:(2)
  • ログインしていないユーザは、承認されたコメントのみを見られる:(3)

(1)〜(3)ができるように、記事詳細画面のテンプレートを編集します。

blog/templates/blog/post_detail.html

{% extends 'blog/base.html' %}

{% block content %}
  <div class="post">
    <!-- 省略 -->
  </div>
  <hr>
  <a class="btn btn-default" href="{% url 'add_comment_to_post' pk=post.pk %}">Add comment</a>
  {% for comment in post.comments.all %}
    {% if user.is_authenticated or comment.approved_comment %} <!-- 追加 (1)と(3)に対応 -->
    <div class="comment">
      <!-- div class="date" を変更。(2)に対応 -->
      <div class="date">
        {{ comment.created_date }}
        {% if not comment.approved_comment %}
          <a class="btn btn-default" href="{% url 'comment_remove' pk=comment.pk %}"><span class="glyphicon glyphicon-remove"></span></a>
          <a class="btn btn-default" href="{% url 'comment_approve' pk=comment.pk %}"><span class="glyphicon glyphicon-ok"></span></a>
        {% endif %}
      </div>
      <!-- div class="date" の変更 終わり -->
      <strong>{{ comment.author }}</strong>
      <p>{{ comment.text|linebreaks }}</p>
    </div>
    {% endif %} <!-- 追加 -->
  {% empty %}
    <p>No comments here yet :(</p>
  {% endfor %}
{% endblock %}

ログインしているユーザの場合、user.is_authenticatedTrueなので、すべてのコメントが表示されます。
そのうち、承認されていない各コメントには削除ボタンと承認ボタンが表示されます。
ログインしていないユーザの場合、comment.approved_commentがTrueになる(すなわち、承認済みの)コメントが表示されます。
承認済みなので、コメントに削除ボタンと承認ボタンは表示されません。4

記事詳細画面を表示すると、NoReverseMatchエラーです。 f:id:nikkie-ftnext:20181229182552p:plain
comment_remove(とcomment_approve)というURLが設定されていないために発生しているので、blog/urls.pyを編集します。

from django.urls import path
from . import views

urlpatterns = [
    # これまで作成したpath()関数に変更はなし
    # リストの末尾の要素として以下の2行を追加する
    path('comment/<int:pk>/approve/', views.comment_approve, name='comment_approve'),
    path('comment/<int:pk>/remove/', views.comment_remove, name='comment_remove'),
]

AttributeErrorにより、サーバが起動しなくなりました。
f:id:nikkie-ftnext:20181229182610p:plain

blog/views.pycomment_approve関数(とcomment_remove関数)が見つからないために発生しているので、これらの関数を追加します。

from django.shortcuts import render, get_object_or_404
from django.utils import timezone
# importの行は以下の1行のみ変更
from .models import Post, Comment # Commentのimportを追加
from .forms import PostForm, CommentForm

# これまでに作成した関数は変更なし

# 以下の行を追加
@login_required
def comment_approve(request, pk):
    comment = get_object_or_404(Comment, pk=pk)
    comment.approve()
    return redirect('post_detail', pk=comment.post.pk)

@login_required
def comment_remove(request, pk):
    comment = get_object_or_404(Comment, pk=pk)
    comment.delete()
    return redirect('post_detail', pk=comment.post.pk)

comment.post.pkでコメントが紐づく記事のpk(ID)を取得できます。
Djangoシェルでの確認

>>> from blog.models import Comment
>>> comment = Comment.objects.get(id=1)
>>> comment.post
<Post: 初投稿:dockerで環境構築>
>>> comment.post.pk
1

これでエラーは解決し、ログインするとコメントの承認/削除を行うことができます。
f:id:nikkie-ftnext:20181229182627p:plain

ログインしていないユーザには、承認したコメントだけが表示されます。
f:id:nikkie-ftnext:20181229182640p:plain

記事一覧に表示されるコメント数を承認されたコメント数に変更する

最後に、記事一覧のコメント数を承認されたコメントの数に変更します。
(ログインの有無にかかわらず、承認されたコメント数の表示となります)

変更するのはテンプレートとモデルの2箇所です。
まず、blog/templates/blog/post_list.htmlを変更します。

{% extends 'blog/base.html' %}

{% block content %}
  {% for post in posts %}
    <div class="post">
      <div class="date">
        <p>published: {{ post.published_date }}</p>
      </div>
      <h1><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h1>
      <p>{{ post.text|linebreaksbr }}</p>
      <!-- 以下の1行だけを変更しています。変更前:<a href="{% url 'post_detail' pk=post.pk %}">Comments: {{ post.comments.count }}</a> -->
      <a href="{% url 'post_detail' pk=post.pk %}">Comments: {{ post.approved_comments.count }}</a>
    </div>
  {% endfor %}
{% endblock %}

コメントの数はPostモデルのapproved_commentsメソッドで取得した数としています。
しかし、approved_commentsはまだ実装されていないため、この時点ではコメント数には何も表示されません。
そこで、blog/models.pyを変更します。
Postモデルにapproved_commentsメソッドを追加します。

from django.db import models
from django.utils import timezone


class Post(models.Model):
    # プロパティには変更がないため省略

    def publish(self):
        self.published_date = timezone.now()
        self.save()

    # 以下のメソッドを追加するだけ
    def approved_comments(self):
        return self.comments.filter(approved_comment=True)
    
    def __str__(self):
        return self.title


class Comment(models.Model):
    # 変更がないため省略

approved_commentsメソッドでは、前編冒頭のCommentモデルのForeignKey設定で、post.commentsと書けるように設定したことを利用しています。
追加したメソッドをDjangoシェルで試してみます。

>>> from blog.models import Post
>>> post = Post.objects.get(id=1)
>>> post.approved_comments()
<QuerySet [<Comment: フォームからコメントを作りました!>, <Comment: 承認のテストです>]>
>>> post.approved_comments().count()
2

記事一覧画面で承認されたコメントの数が表示されるようになりました!
f:id:nikkie-ftnext:20181229182657p:plain

終わりに

コメント機能、手を動かす部分は長かったですが、やっていることはDjango Girls Tutorialでやってきた機能追加手順と変わらないと思います。
Djangoで機能追加するときの一連の手順の練習という印象です。

nikkie流Django機能追加手順5

  1. 新規ページへ遷移するリンクを追加
  2. 新規ページのURL設定
  3. 新規ページのビュー作成
  4. 新規ページのテンプレート作成

ExtensionsのDjango2系の動作確認はこれで完了です。
PyConJPのスプリントで協力いただいた翻訳があるので、そのレビューを進めていきます。
最後までお読みいただき、ありがとうございました。

脚注


  1. https://docs.djangoproject.com/ja/2.1/topics/forms/modelforms/#the-save-method

  2. ForeignKeyの同様の書き方として post.author = request.user がありました。ref: https://tutorial.djangogirls.org/ja/django_forms/

  3. ログインしているユーザであっても、一度承認されたコメントは削除できません。(ただし、Django Adminからは削除可能です)

  4. 一度承認されたコメントを削除できるようにする場合、この箇所を修正することになりそうです。(ログインしているユーザに限って、削除ボタンと承認ボタンを表示する)

  5. Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 1/3) #モグモグDjango - nikkie-ftnextの日記Djangoの機能追加手順」より

Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 3/3) コメント機能 <前編>

※1本の記事として書き上げましたが、記事が長くなりすぎたため、前編・後編に分けています。

はじめに

いつも心は虹色に! nikkieです。
Django Girls Tutorial ExtensionsのDjango2系動作確認の第3弾(最終回)です。
第1弾はこちら:Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 1/3) #モグモグDjango - nikkie-ftnextの日記
第2弾はこちら:Django Girls Tutorial ExtensionsをDjango2系で動かす(Part 2/3) セキュリティ編 - nikkie-ftnextの日記
(#pyhack でもくもくして、このブログを作成しました)

コメント機能を実装したブログはこのような感じです。
f:id:nikkie-ftnext:20181229182225p:plain

背景

Django Girls Tutorial Extensionsでは、Django Girls Tutorialで作ったブログアプリに機能追加していきます。
ExtensionsのコードはDjango 1系で書かれており、Django Girls Tutorialの翻訳メンバーの間では「Django 2系に書き換えたいね」という話が出ていました。

Django 2系に書き換える対象は、3ページあります。

今回はコメント機能のチュートリアルを進める中で気づいたことを書きます。
Extensionsに取り組まれる方の参考になれば幸いです。

前提

  • macOS 10.13.6
  • Python 3.6.6
  • Django 2.0.9
  • venvで仮想環境を作って動かしています(スクショ中に「Dockerで動かしています」とありますが、今回は当てはまりません)

コメント機能追加の流れ

大きく分けると3ステップです。

  1. コメントを管理画面から入力できるようにし、画面に表示する
    • コメントのモデルを追加
    • コメントのモデル用のマイグレーション実施
    • 管理画面から操作できるように設定
    • ブログアプリにコメント表示(記事詳細画面、記事一覧画面)
  2. ブログからコメントを作成できるようにする
    • コメント用のフォーム追加
    • 記事詳細画面からコメント入力画面へ遷移するように設定
      • 記事詳細画面のテンプレートにボタンを追加
      • コメント入力画面のURL設定
      • コメント入力画面のビュー関数作成
    • コメント入力フォーム追加
  3. コメントの承認機能と削除機能をつける
    • 記事詳細画面のテンプレートにコメント承認ボタン・削除ボタンを追加
    • コメント承認機能、コメント削除機能用のURL設定
    • コメント承認用ビュー関数、コメント削除用ビュー関数作成
    • 記事一覧に表示されるコメント数を承認されたコメント数に変更する

それでは順に見ていきましょう。

コメントを管理画面から入力できるようにし、画面に表示する

コメント用のクラス追加

blog/models.pyCommentクラスを追加します。

# importの行とPostモデルの定義は省略

class Comment(models.Model):
    post = models.ForeignKey('blog.Post', on_delete=models.CASCADE, related_name='comments')
    author = models.CharField(max_length=200)
    text = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)
    approved_comment = models.BooleanField(default=False)

    def approve(self):
        self.approved_comment = True
        self.save()

    def __str__(self):
        return self.text

Post(記事)とComment(コメント)の関係は

  • 1つのコメントは1つの記事につく (A)
  • 1つの記事は複数のコメントを持つ (B)

です。
commentをCommentモデルのインスタンスとすると、comment.postとしてコメントの紐づく記事にアクセスできます。(A)
postをPostモデルのインスタンスとすると、post.comment_set1として記事に紐づくコメント全体にアクセスできます。2 (B)
(B)のケースで、ForeignKeyrelated_name引数を設定することで、post.<related_name引数の値>というアクセスが可能になります。
(今回のコードではpost.commentsと書けるということです)

管理画面からコメントを作成できるようにする

新しくコメントモデルを追加した後は、マイグレーションを実施します。

$ python manage.py makemigrations blog
$ python manage.py migrate blog

管理画面からコメントが作成できるように、blog/admin.pyを編集します。

from django.contrib import admin
from .models import Post, Comment # Commentを追加

admin.site.register(Post)
admin.site.register(Comment) # 追加

f:id:nikkie-ftnext:20181229182250p:plain

コメントを表示する

管理画面から作成したコメントを表示します。
記事詳細画面、記事一覧画面の順で表示するコードを追加していきます。

blog/templates/blog/post_detail.html
(ブログを書いている途中、詳細画面に<hr>タグ(記事本文とコメントを分けるための水平線を表示)を書き忘れていることに気づきました。
以下のスクショの詳細画面には水平線が表示されていませんが、本来は表示されるはずです。)

{% extends 'blog/base.html' %}

{% block content %}
  <div class="post">
    <!-- 省略 -->
  </div>
  <!-- 追加 -->
  <hr>
  {% for comment in post.comments.all %}
    <div class="comment">
      <div class="date">{{ comment.created_date }}</div>
      <strong>{{ comment.author }}</strong>
      <p>{{ comment.text|linebreaks }}</p>
    </div>
  {% empty %}
    <p>No comments here yet :(</p>
  {% endfor %}
  <!-- 追加 終わり -->
{% endblock %}

{% empty %}というタグが使われていますが、これは{% for %}タグで「ループさせようとした配列が空、または存在しなかった場合に表示する文字列を指定」するものだそうです。3

コメントの本文の表示にlinebreaksというフィルタが使われています。
記事の本文の表示ではlinebreaksbrでした。<p>{{ post.text|linebreaksbr }}</p> 4
2つの挙動の違いは以下の通りです。

  • linebreaksbrは、改行を<br>タグに置き換えます5
    • Joel\nis a\n\nslugを渡すと、Joel<br>is a<br><br>slugとなりました
  • linebreaksタグは空行が続く改行を<p>タグに置き換え、改行を<br>タグに置き換えます6
    • Joel\nis a\n\nslugを渡すと、<p>Joel<br>is a</p><p>slug</p>となりました

記事詳細画面にコメントが表示されるようになりました!
f:id:nikkie-ftnext:20181229182308p:plain

コメントにスタイルを設定します。

blog/static/css/blog.css

/* これまでのスタイル設定の下に追加 */
.comment {
  margin: 20px 0px 20px 20px;
}

スタイルを設定するとコメントが右に寄ります。
f:id:nikkie-ftnext:20181229182337p:plain

続いて、記事一覧画面に、各記事に紐づくコメントの数を表示します。

blog/templates/blog/post_list.html

{% extends 'blog/base.html' %}

{% block content %}
  {% for post in posts %}
    <div class="post">
      <div class="date">
        <p>published: {{ post.published_date }}</p>
      </div>
      <h1><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h1>
      <p>{{ post.text|linebreaksbr }}</p>
      <!-- 以下の1行を追加するだけです -->
      <a href="{% url 'post_detail' pk=post.pk %}">Comments: {{ post.comments.count }}</a>
    </div>
  {% endfor %}
{% endblock %}

post.comments.countで各記事のコメント件数が取得されます。
Djangoシェルで試してみました。

>>> from blog.models import Post
>>> post = Post.objects.get(pk=1)
>>> post.comments.count()
2

f:id:nikkie-ftnext:20181229182405p:plain

ブログからコメントを作成できるようにする

コメント用のフォームを追加

blog/forms.pyにコメント用のフォーム(CommentForm)を追加します。

from django import forms

from .models import Post, Comment # Commentのimportを追加

class PostForm(forms.ModelForm):
    # 変更はないため省略

# 以下を追加する
class CommentForm(forms.ModelForm):

    class Meta:
        model = Comment
        fields = ('author', 'text',)

コメント入力画面へ遷移するように設定

記事詳細画面からコメント入力画面へ遷移するように設定していきます。
Django URL · Django Girls Tutorial 以降で経験したように、エラーを出しながら1つ1つ解決していきます。

記事詳細画面のテンプレートに、コメント入力画面へ遷移するボタン(aタグ)を追加します。
blog/templates/blog/post_detail.html

{% extends 'blog/base.html' %}

{% block content %}
  <div class="post">
    <!-- 省略 -->
  </div>
  <hr>
  <!-- 追加 -->
  <a class="btn btn-default" href="{% url 'add_comment_to_post' pk=post.pk %}">Add comment</a>
  <!-- 追加 終わり -->
  {% for comment in post.comments.all %}
    <div class="comment">
      <div class="date">{{ comment.created_date }}</div>
      <strong>{{ comment.author }}</strong>
      <p>{{ comment.text|linebreaks }}</p>
    </div>
  {% empty %}
    <p>No comments here yet :(</p>
  {% endfor %}
{% endblock %}

記事詳細画面にアクセスすると(URL例:http://localhost:8000/post/1/)、NoReverseMatchというエラーが出ます。(図はExtensions参照)
これは、追加した箇所のhref="{% url 'add_comment_to_post' pk=post.pk %}が原因です。
add_comment_to_postというURLが未設定のために発生しています。
では、blog/urls.pyで設定しましょう。

from django.urls import path
from . import views

urlpatterns = [
    # これまで作成したpath()関数に変更はなし
    # リストの末尾の要素として以下の1行を追加する
    path('post/<int:pk>/comment/', views.add_comment_to_post, name='add_comment_to_post'),
]

すると、別のエラーAttributeErrorが現れ、runserverに失敗します。(NoReverseMatchというエラーを解決し、先に進んだと考えましょう)

f:id:nikkie-ftnext:20181229182426p:plain

blog/urls.pyに追加した行で、http://localhost:8000/post/1/comment/のようなURLに対して、blog/views.pyadd_comment_to_post関数を実行するように設定しています。
ところが、現時点ではblog/views.pyadd_comment_to_post関数が見つからないため、エラーとなっています。
では、blog/views.pyadd_comment_to_post関数を用意しましょう。

この解決は後編の記事に続きます。

脚注


  1. 「ForeignKey 定義内の related_name パラメータをセットして FOO_set の名前をオーバーライドできます」ref: https://docs.djangoproject.com/ja/2.1/topics/db/queries/#backwards-related-objects

  2. Djangoモデル の章でPostモデルにauthor = models.ForeignKey('auth.User', on_delete=models.CASCADE)と定義しています。これにより、me = User.objects.get(username='ftnext')がauthorとなる記事はme.post_set.all()で取得できます。(Djangoシェルで確認する場合は、まずfrom django.contrib.auth.models import Userを実行してください)

  3. https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#for-empty

  4. https://tutorial.djangogirls.org/ja/template_extending/

  5. https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#linebreaksbr

  6. https://docs.djangoproject.com/ja/2.1/ref/templates/builtins/#linebreaks