初めてDjangoでブログアプリを作ったときの話

以前ブログアプリを Django で作ったときの話。Webフレームワークからのアプリ開発は初めてだったのですが、仕事終わってからと土日の1週間で「とりあえず動くもの」を一通り作ることが出来ました。未知の領域だったので非常に勉強になり楽しかったです。
いろんなつまづきポイントがありましたが、仕事で日常的に使わないテクノロジーはすぐに忘れてしまうので、自分に対する備忘の意味でも作成ステップを書いておこうと思います。

私と同じ様なスキル・環境の人がこの投稿を読んでくれて、何か解決の一助となれば幸いです。

※開発バージョン: Python 3.x, Django 2.0
※リリースした Django アプリは現在停止しています。あくまでゼロからリリースするまでの話で、運用面についてのノウハウは、本投稿に一切記載がないことはご了承ください。

1. チュートリアルで全体構成をつかむ

まず最初に Django がどんな仕組みで動いているのか全く分からなかったので、公式の Django Ver. 2.0 チュートリアルでフレームワークの基本構造をインプットしました。
とても分かりやすいチュートリアルで、初めての私でも概要をざっくりと理解することが出来ました。自分でメモを取りながら内容を整理しながら進めるのが良いと思います。

また、個人的にはあまり参照しませんでしたが、他によく引き合いに出されるのが、Django Girls(日本語アリ) というチュートリアルです。パッと見た感じ、説明の仕方がフランクかつ丁寧で公式よりも分かりやすいと感じる方も多いかと思います。

Python 自体初めてという方は「__init__.pyの役割」とか、基本的な部分で疑問が出てくると思いますが、公式ドキュメント入門書 を参考にして、一つひとつ解決していきましょう。ただし、「まずは動くものを作る」という目的を見失わないためにも、なんとなく分かったらOKと、割り切る気持ちも大切です。

一通り公式チュートリアルをやり終えると、次のような項目を理解している状態になりました。

  • プロジェクト、アプリ、ユーザーの作り方とそれぞれの位置付け
  • ModelをMigrateしてデータベースと管理画面を定義する感覚
  • URLを引数として、Viewが処理し、戻り値をTemplateが表示する一連のプロセス
  • CSSやJavaScriptといったリソース ファイルの置き場所と読み込み方

ここでぼんやりと「自由にWebアプリケーションが作れそうだぞ!」という感覚になっていました。はやる気持ちを持て余しつつ、次のステップに進みます。

2. 画面デザインを考える

実際に作る画面の大まかな方向性をイメージをするために、目標となる「画面イメージ」を絵に描いてみました。紙に書いた方が良いかは人によるかもしれませんが、画面インターフェイスを持つアプリで必要な機能・要素が何かを具体的に確認することが出来ます。

以下は私が書いたメモですが、こんな感じでざっくりとした画面要件を忘れないようにメモしておきました。

3. DB構成を決める

画面イメージの絵が描ければ、必要なDB構成要素(ここでは例えば「タイトル」、「本文」、「日付」、「カテゴリー」、「タグ」とか)を思い浮かべることが出来ます。

個人的な情報発信が目的のブログなので、DB構成(ER図)は以下のようにシンプルな構成とします。

(コメント機能実装も脳裏をよぎりましたが、めんどくさいので最初のフェーズではやめておきました。)

実際のModel.pyは以下のように書きました。

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


class Category(models.Model):
name = models.CharField('Category', max_length=50)
description = models.TextField('Description', blank=True)
createdAt = models.DateTimeField('Created At', default=timezone.now)

def __str__(self):
    return self.name

class Meta:
    verbose_name_plural = "Categories" #管理画面で"Categorys"とスペルミスされないよう追記


class Tag(models.Model):
name = models.CharField('Tag', max_length=50)
description = models.TextField('Description', blank=True)
createdAt = models.DateTimeField('Created At', default=timezone.now)

def __str__(self):
    return self.name


class Post(models.Model):
author = models.ForeignKey('auth.User', on_delete = models.CASCADE)
title  = models.CharField(max_length = 255)
headContent = models.TextField()
content   = models.TextField()
category = models.ForeignKey(Category, on_delete = models.CASCADE) #1対1のリレーション
tags = models.ManyToManyField(Tag, default=None, blank=True) #多対多のリレーション
createdAt = models.DateTimeField(default = timezone.now)
isPublic = models.BooleanField('Go Publish', default = False) #記事の公開・非公開を決定するチェックボックス

def __str__(self):
    return self.title


class Image(models.Model):
filename = models.CharField(max_length = 255)
image = models.ImageField(upload_to = 'post/',) #画像ファイルをアプリ内フォルダーにアップロードする

def __str__(self):
    return self.filename

DBにMigrateすると、ここでのClass名がテーブル名として、各モデル変数名が列名として作成されるんだなぁということは、チュートリアルの説明で理解できていました。

しかし、そこで説明の無かったimageフィールドはちょっと苦労しました。要するにブログポストに表示させる画像を投稿管理したいのですが、ベストプラクティスが分からず時間を使ってしまいました。

こちらを検討する際に参考にさせていただいた情報を列挙しておきます。

ローカル環境での開発段階では、settings.pyには以下のような記述となります。

...

STATIC_URL = '/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)

MEDIA_URL = '/media/'
MEDIA_ROOT = (
os.path.join(BASE_DIR, 'media')
)

ここで、「STATICなんちゃらとMEDIAなんちゃらって何が違うねん」という疑問が出てきました。

ローカル開発のタイミングではあまり深追いせずに、 こちら にあるようなコミュニティ回答を参考にしつつ、シンプルに「STATICは静的なアプリケーションリソース」、「MEDIAはユーザーがアップロードしたファイル」をそれぞれ保管する場所という簡易的な理解にとどめました。

明確にこれらの違いを理解できたのはサーバーにデプロイするタイミングになるかと思います。詳しくは以下で説明しますが、簡単にイメージだけお伝えすると、STATIC系はサーバーにリソースをPUSHする役割を担い、MEDIA系はアップロード先となるストレージのAPIをsettings.pyで明示することで、アプリとの懸け橋となる役割を担うことになります。「6. Google Cloud にデプロイする」で後述します。

4. 機能を実装する

このあたりから、チュートリアルの説明だけでは出来ないことが多くなってきます。Google で調べながら取り掛かりましたが、特に Narito Takizawa氏のブログ, naritoブログVitor Freitas氏のブログ, simpleisbetterthancomplex からは多くのことを参考にさせていただきました。

私の場合、主な「つまづきポイント」は以下3つでした。

  • I. DBの本文テキストで書いたHTMLをテンプレートの表示でレンダリングしたい
  • II. 記事一覧ページでは5件ずつ表示し、追加読み込みは”Load More”ボタンで実装するには?
  • III. 記事検索ボックスはどのような方法で実装する?

I. DBの本文テキストで書いたHTMLをテンプレートの表示でレンダリングしたい

HTMLが書ける人にとって、これ以上に自由度の高いWebの記述方法はないはずです。ブログを書く際にプレーンテキストではなく、自由に編集できることの方がメリットが大きかったので、テキストフィールドの本文にタグを書いてしまい、それをブラウザにレンダリングさせたい、というのを要件として定義しました。
調べてみると、解決方法はあっさりとしていて、テンプレートのデータ呼び出し記述を

{{ data }}

ではなく

{{ data|safe }}

というようにsafeフィルターを付与すれば良いだけでした。

参考: “Rendering a template variable as HTML”, stackoverflow

II. 記事一覧ページでは5件ずつ表示し、追加読み込みは”Load More”ボタンで実装するには?

記事一覧を表示させる際には対象の記事毎にfor文処理が走るわけですが、いくら記事が増えてもとにかく、すべてを表示させるような処理ではUI的にも分かりづらく、ネットワークの負担も大きくて困りますね。

最新記事の5件ずつ読み込んでくれるのが理想だったので、それを要件に盛り込みました。

解決策は様々あり、どのように実現するかは人の好きずきですが、例えば「Pagination(ページ番号表示)」や「Infinite Scroll(無限スクロール)」という実装も考えられます。

※それぞれの実装方法に詳しいおすすめリンクは以下です。

私の場合は、縦長のモバイルからも操作しやすく、フッターをデザインしたかったので、「Load More ボタン」を採用することにしました。

Google で調べてみたところ、
こちらのやり方がスタンダードなやり方のようだったので採用しました。以下のように対象ファイルを配置して、

project/
├── manage.py
└── app/
        ├── migrations/
        ├── static/
        │           └── app/
        │               ├── css/
        │               ├── img/
        │               └── js/
        │                       └── jquery-3.3.1.min.js    <- jQuery
        ├── templates/
        ├── templatetags/
        │           ├── __init__.py
        │           └── pagemore.py    <- "Load More"ボタンの機能
        ├── __init__.py
        ├── admin.py
        ├── apps.py
        ├── models.py
        ├── tests.py
        ├── urls.py
        └── views.py

テンプレートとなるHTMLを次のように記述します。 記事表示タグを pagenator.objects で囲んでいます。

    <head>

        ...

       <script type="text/javascript" src="{% static 'blog/js/jquery-3.3.1.min.js' %}"></script>
       <script type="text/javascript" src="{% static 'blog/js/pagemore.js' %}"></script>

        ...

    </head>

    ...

    <div class = "content">
        {% more_paginator Posts per_page=5 ordered_by="-createdAt" as paginator %}
        {% for Post in paginator.objects %}
            {% if forloop.first %}
            <div class="pagemore-container">
                    {% endif %}
                    <div class = "post">

                            ... ここに記事

                    </div>
                    {% if forloop.last %}
                    </div>
                {% if paginator.has_more %}
                <a class="pagemore-paginator" href="?{{paginator.next_query}}">Load More</a>
                {% endif %}
            {% endif %}
        {% endfor %}
        </div>

    ...

III. 記事検索ボックスはどのような方法で実装する?

本文中のコンテンツを探して一覧表示してくれるようにSearchボタンとview.pyを組み合わせて実装することにしました。まずはテンプレート側の検索ボックスについて、以下のように記述します。

<form id = "searchbox" type="GET" action="/search/" style="margin: 0">
<input  id="search_box" type="text" name="keyword", placeholder = "Keyword...">
<button id="search_submit" type="submit" >Search</button>
</form>

次にview.pyで処理コントロールを書いていきます。django.db.models から Q というキーワード検索のためのオブジェクトをインポートして利用します。該当箇所は以下のようになります。

from .models import Post, Category, Tag
from django.db.models import Q
from django.shortcuts import render

...

def searchFilter(request):
categoriesQuerySet = Category.objects.all()
tagsQuerySet = Tag.objects.all()
archiveSet = Post.objects.filter(isPublic = True).dates('createdAt', 'month', order='DESC')
queryset = Post.objects.filter(isPublic = True).order_by('-createdAt')
if request.method == 'GET': #フォームがsubmitされたとき
    search_query = request.GET['keyword']
    if search_query:
        for keyword in search_query.split():
            queryset = queryset.filter(
                Q(title__icontains=keyword) | Q(content__icontains=keyword))
return render(request, 'blog/index.html', {'Posts': queryset, 'Archive': archiveSet, 'Categs': categoriesQuerySet, 'Tags': tagsQuerySet,})

先ほどテンプレート側に記述した検索ボックスの値を’keyword’として受け取ると、Qオブジェクトに記述したフィルター条件(’keyword’をPost.objectのtitleまたはcontentに含む)をqyerysetに適用しています。

querysetはPostsというオブジェクト名でテンプレートから表示対象となります。

5. テンプレートを実装する

テンプレート実装は、一般的なHTMLとCSSの基本知識で問題ないはずです。Django 特有の部分はview.pyから受け取ったクエリ結果の表示方法の部分くらいですね。

CSSに関して、得意な人はすんなりと思い通りのデザインに出来るかと思いますが、私の場合、知識が無いくせに凝り性なところがあって、思ったよりも時間がかかってしまいました。レスポンシブ対応させて、キリの良いところでサクッと終了。

※このとき、ブラウザのエミュレーターを使いこなせていればと後悔しました。というのも、デプロイした後にモバイル端末でデザインの当たり具合を確認すると、修正点が沢山出てきて、CSSを直してはデプロイ、直してはデプロイ、というやり方で、ちょっと効率が悪かったと思います。特に、staticファイルをホストしてくれている Google Cloud Storage には処理の高速化のためのキャッシュ機能が備わっており( 詳細: Shingo Ishimura氏 GPC Edge Cache, Qiita )、すぐに変更点が反映されないのでオロオロしました。

6. Google Cloud にデプロイする

機能実装とテンプレート作成(画面デザイン)が完了し、実際に動いているアプリが手元(ローカル)にある状態になりました。
あたかもゴールはすぐそこにあるかのように見えていますが、デプロイ作業を進めたところ、それが錯覚であったと気づかされました。。(PaaSは各社毎のクセがすごい。)

サーバー運用スキルの獲得は今回の目的ではないのでクラウドサービスを選びます。個人的に、仕事では Azure、趣味で AWS をちょろちょろっといじったことがある程度だったのですが、今回初めて Google Cloud Platform を選択してみました。選んだ理由は、

  1. I. 初めてアカウントを作るので無料期間がたっぷりあること
  2. II .他社サービスとの比較をしてみたかったこと
  3. III. Django のデプロイ方法についての 公式ドキュメント がしっかりしていたこと

の3つです。

3点目の公式ドキュメントはかなり詳細な説明があり、何かエラーが起きたら必ずここを読み返して解決のための糸口を探していました。

ポイントですが、Tsuyoshi Chujo氏, “Google App Engine にDjangoアプリケーションをデプロイする(Python3)”, Qiita にも書かれている通り、”いきなり自分のアプリケーションをデプロイしようとせずに、… まずは手順に記載の通りサンプルのプロジェクトを … 一度GAEのフレキシブル環境へデプロイしてみる” というのがかなり大事な教訓です。

私はまさしく「いきなり自分のアプリケーションをデプロイしてやろう」と試みた輩で、結局何が何だか分からなくなりました(笑)

最初は手間に感じていたのですが、ドキュメントに書かれている通りのサンプルプロジェクトを試してみることで、そもそもの仕様を理解することができ、次に問題が発生したときの原因究明が出来るようになる、というのを実感することができました。

とはいえ、ここでもいくつか「つまづきポイント」がありました。

  • I. Cloud SQL Proxy によるDBとの接続するには?
  • II. app.yamlファイルの設定漏れ
  • III. Google Cloud Storage のAPI設定方法?

I. Cloud SQL Proxy によるDBとの接続するには?

実際にクラウドに作成したDBに対してローカルにある Django プロジェクトからMigrateを行うために接続が必要ですが、ここでCloud SQL Proxy が必要となります。

これはこれで腰を据えて動きを確認する必要があるシロモノで、公式ドキュメント を参考にしてきちんと仕様を理解するのに結構時間を使いました。

II. app.yamlファイルの設定漏れ

Cloud SQL Proxy が利用できるようになり、順当にチュートリアルを進めていくと、プロジェクト名やインスタンスIDなど、接続情報を書き換える箇所がいくつか出てきます。その中で、app.yamlファイルの以下<django-project-name>を書き換え忘れていました。

runtime: python
env: flex
entrypoint: gunicorn -b :$PORT <django-project-name>.wsgi

beta_settings:
cloud_sql_instances: <your-cloudsql-connection-string>

runtime_config:
python_version: 3

単純なミスですが、意外と書き忘れる方いらっしゃるんじゃないかと思います。公式チュートリアルの手順では、何気なく「mysite」というタイトルの Django プロジェクトで話が進んでいきますが、ファイル内の書き換え箇所としての説明が明記されていないので注意が必要です。

ちなみにここが抜けていると、デプロイのタイミングでエラーとなります。

III. Google Cloud Storage のAPI設定方法?

「3. DB構成を決める」の部分で書きましたが、「ImageField」の本番展開にも苦労しました。ローカル開発環境では何も考えずとも、同じプロジェクト内のフォルダーに格納されていきますが、本番環境では、それを公開してくれるストレージに格納していく必要があり、格納するにはAPIを利用することになります。

参照: django-storages

手順ですが、まず開発環境(仮想env)にpipで以下をインストールします。

pip install django-storages

本番環境にも反映されるように

pip freeze > requirements.txt

を忘れずに実行しましょう。

次に、接続情報としてsettings.pyに以下を記述します。

  
...      
DEFAULT_FILE_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage'
GS_BUCKET_NAME = '<ファイルをアップロードしたい Google Cloud Storage のバケット名>'
...

この設定をして初めて Django 管理画面のImage登録フォームから選択、保存したファイルが Google Cloud Storage の指定したバケットにアップロードできるようになります。

ネットで探してもあまり説明がなく、なかなか苦労しました。今後ここらへんの細かい情報も充実していくといいんですけどねぇ。

7. デバッグする

デプロイが完了し、インターネットできちんと動いているように見えますが、あれやこれやといろいろなケースを試すと修正点が出てくるはずです。
Django に限らずシステム開発全般で言えることですが、私の経験上、開発直後のデプロイでは100%確実にどこかボロが出ます。落ち着いて一つひとつパズルを解くようにデバッグしましょう。
全部出来た!というタイミングになったら settings.py の

DEBUG = True

DEBUG = False

に変更しておしまいです。

以上、初めて Django アプリを作ってみた一連の流れでした。思い出せる範囲でどどどーっと書いた文章になってしまいました。

実際に作ったブログを使ってみると、「もっとここをああしたい」みたいな欲が出てきます。何度もデプロイ作業をしているうちに Django に詳しくなったり、
サーバーのクセや操作の勘所が分かってくるかと思いますので、とても勉強になりました。

最後に、十分な参考文献や頼る人がいない中で、個人的な趣味として Django アプリ開発を楽しく進めることが出来たのも、こういったブログを残してくださっている方のおかげだということを強く感じました。
本投稿は初歩的で些細な内容までですが、私のようにどこかの誰かがこの記事を読んで、少しでも問題解決の糸口になれば幸いです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です