DjangoでブログのWebアプリケーションを作成してみました



PythonのWebフレームワークであるDjangoについて、概要を勉強するために「Django Girls のチュートリアル」をやってみました。このチュートリアルでは、ブログのWebアプリケーションを作成することでDjangoの概要とWebアプリケーションの仕組みを学ぶことができます。実装する量も少なく挫折することなく簡単に実装できるので、Djangoの概要を知りたい初心者向けの内容になっています。


*Django とは

DjangoはPythonのWebアプリケーションフレームワークです。ユーザー認証や管理者用の画面、ファイルアップロードなどを簡単に実装することができるようになっています。


*参考



*環境

  • MacOS
  • Python 3.6.3
  • Django 2.0.9


*環境構築

任意の場所に作業用ディレクトリを作成して、そこに仮想環境を作成します。
$ mkdir djangogirls
$ cd djangogirls/
$ python3 -m venv myvenv
$ source myvenv/bin/activate
(myvenv)~/djangogirls$

Djangoをインストールするために、最新のpipをインストールします。
インストールが必要なパッケージリストを書くためのrequirements.txtを新規作成します。
$ python3 -m pip install --upgrade pip
$ touch requirements.txt

作成したrequirements.txtに下記を追加します。
Django~=2.0.6

requirements.txtの内容をインストールします。
pip listでインストールしたパッケージの確認ができます。
$ pip install -r requirements.txt
$ pip list

Package    Version
---------- -------
Django     2.0.9  
pip        18.1   
pytz       2018.7 
setuptools 28.8.0 


*プロジェクト作成

Djangoプロジェクトを作成します。
$ django-admin startproject mysite .
$ ls
manage.py  mysite  myvenv  requirements.txt

プロジェクトを作成すると、下記構成でファイルが作成されます。
djangogirls
├───manage.py
├───mysite
│    ├ settings.py     <-- webサイトの設定
│    ├ urls.py         <-- urlの設定
│    ├ wsgi.py
│    └ __init__.py
└───requirements.txt

hostや言語などの設定を修正します。

<settings.py>
# 修正
ALLOWED_HOSTS = ['127.0.0.1', '.pythonanywhere.com']

LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'

# 追加 (静的ファイルの追加)
STATIC_ROOT = os.path.join(BASE_DIR, 'static')


*データベースの設定

今回はサーバーを使わないでファイルベースでデータ管理をする sqlite3 を使います。(初期設定をそのまま使います)

<settings.py>
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

データベースを作成します。
$ python manage.py migrate


*Webサーバーの起動

下記コマンドを実行してWebサーバーを起動します。
$ python manage.py runserver

下記URLにアクセスするとDjangoの画面が表示されます。
http://127.0.0.1:8000/












*アプリケーションの作成

作成したプロジェクトの中に複数のプロジェクトをもつ構造になっています。
プロジェクトの中で下記コマンドを実行して、blogアプリケーションを作成します。
$ python manage.py startapp blog

作成したアプリケーションは下記構成になっています。
blog
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

設定に作成したアプリケーション名を追加します。

<settings.py>
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',         <-- 追加
]


*モデルの作成

ブログで作成した記事を保存するためのモデルを作成します。
既に作成されているmodels.py に、モデル名のクラスを追加します。
CharFieldは桁数の制限ありテキスト、TextFieldは桁数の制限なしテキストです。

<models.py>
from django.db import models  
from django.utils import timezone  
  
  
class Post(models.Model):  
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE)  
    title = models.CharField(max_length=200)  
    text = models.TextField()  
    created_date = models.DateTimeField(default=timezone.now)  
    published_date = models.DateTimeField(blank=True, null=True)  
  
    def publish(self):  
        self.published_date = timezone.now()  
        self.save()  
          
    def __str__(self):  
        return self.title

モデルのためのテーブルを作成します。
$ python manage.py makemigrations blog

Migrations for 'blog':
blog/migrations/0001_initial.py
- Create model Post

データベースにテーブルを追加します。
$ python manage.py migrate

Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
Applying blog.0001_initial... OK


*管理者用ページの作成

Djangoでは管理者用のページがデフォルトで用意されています。
既に作成されているadmin.pyに下記内容を書き込みます。

<admin.py>
from django.contrib import admin  
from .models import Post  


admin.site.register(Post)

Webサーバーを起動します。
$ python manage.py runserver

下記URLにアクセスすると管理者用の画面が表示されます。
http://127.0.0.1:8000/admin/












管理者にするスーパーユーザーを作成します。
名前やパスワードを聞かれるので適当に入力します。
$ python manage.py createsuperuser

Username: admin
Email address: admin@admin.com
Password:
Password (again):

Webサーバーを起動して管理者画面を表示し、作成したユーザーでログインします。
$ python manage.py runserver
http://127.0.0.1:8000/admin/










*URLを追加

初期表示用のURLを追加します。
blog.urlsをインポートするよう設定します。

<mysite/urls.py>
from django.contrib import admin  
from django.urls import path, include  
  
urlpatterns = [  
    path('admin/', admin.site.urls),  
    path('', include('blog.urls')),    <-- 追加
]

アプリケーション固有のURLでは、post_listを表示するよう設定します。

<blog/urls.py>
from django.urls import path  
from . import views  
  
urlpatterns = [  
    path('', views.post_list, name='post_list'),  
]


*HTMLの追加

画面レイアウト用のディレクトリtemplate,blogを新規作成します。
blog
└ templates    <-- 追加
  └ blog       <-- 追加

blogディレクトリ配下に、初期表示用の画面を新規作成します。

<blog/templates/blog/post_list.html>
<!DOCTYPE html>  
<html lang="en">  
    <head>  
        <meta charset="UTF-8">  
        <title>Django Girls blog</title>  
    </head>  
    <body>  
        <div>  
            <h1><a href="/">Django Girls Blog</a></h1>  
        </div>  
        <div>  
            <p>published: 14.06.2014, 12:14</p>  
            <h2><a href="">My first post</a></h2>  
            <p>Aenean eu leo quam. こんにちは</p>  
        </div>  
        <div>  
            <p>公開日: 2014/06/14, 12:14</p>  
            <h2><a href="">My Second post</a></h2>  
            <p>よろしくお願いします。</p>  
        </div>  
    </body>  
</html>

ブラウザで表示すると下記の画面になります。













*テーブルデータの操作

Djangoではクエリセットを使うことで、データベースのデータを参照したり、データを追加するといった操作を行うことができます。
下記コマンドを実行すると、Djangoのインタラクティブコードが実行します。
$ python manage.py shell

## オブジェクトの確認
>>> from blog.models import Post
>>> Post.objects.all()
<QuerySet [<Post: テスト>]>

## ユーザーの確認
>>> from django.contrib.auth.models import User
>>> User.objects.all()
<QuerySet [<User: admin>]>
>>> me = User.objects.get(username='admin')

## オブジェクトの作成
>>> Post.objects.create(author=me, title='Sample title', text='Test')
<Post: Sample title>
>>> Post.objects.all()
<QuerySet [<Post: テスト>, <Post: Sample title>]>

初期画面にデータを表示するためのメソッドを追加します。

<blog/views.py>
from django.shortcuts import render  
from django.utils import timezone 
from .models import Post
  
  
def post_list(request):  
    posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date')  
    return render(request, 'blog/post_list.html', {'posts': posts})


*テンプレートの作成

初期表示用の画面を新規作成します。

<blog/template/blog/post_list.html>
<!DOCTYPE html>  
<html lang="en">  
    <head>  
        <meta charset="UTF-8">  
        <title>Django Girls blog</title>  
    </head>  
    <body>  
        <div>  
            <h1><a href="/">Django Girls Blog</a></h1>  
        </div>  
        {% for post in posts %}  
            <div>  
                <p>published: {{ post.published_date }}</p>  
                <h1><a href="">{{ post.title }}</a></h1>  
                <p>{{ post.text|linebreaksbr }}</p>  
            </div>  
        {% endfor %}  
    </body>  
</html>


*Bootstrapを追加

画面レイアウトを美しくするためのフレームワークBootstrapを追加します。
先ほど作成したpost_list.html<link rel="stylesheet"...の2行を追加します。

<blog/template/blog/post_list.html>
<!DOCTYPE html>  
<html lang="en">  
    <head>  
        <meta charset="UTF-8">  
        <!-- 追加 start -->
  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">  
  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
        <!-- 追加 end -->
        <title>Django Girls blog</title>  
    </head>  
    <body>  
        <div>  
            <h1><a href="/">Django Girls Blog</a></h1>  
        </div>  
        {% for post in posts %}  
            <div>  
                <p>published: {{ post.published_date }}</p>  
                <h1><a href="">{{ post.title }}</a></h1>  
                <p>{{ post.text|linebreaksbr }}</p>  
            </div>  
        {% endfor %}  
    </body>  
</html>




*静的CSSファイルの作成

Djangoは自動的にstaticフォルダを参照するようになっているので、プロジェクトに依存しないCSSファイルなどはstaticディレクトリに置きます。
今回はblogディレクトリ配下にstaticディレクトリを追加します。
blog
├ migrations
├ static  <-- 追加
└ templates

cssディレクトリを新規作成し、その中にblog.cssを作成して下記を書き込みます。

<blog/static/css/blog.css>
h1 a {  
    color: #FCA205;  
    font-family: 'Lobster';  
}  
body {  
    padding-left: 15px;  
}  
.page-header {  
    background-color: #ff9400;  
    margin-top: 0;  
    padding: 20px 20px 20px 40px;  
}  
.page-header h1, .page-header h1 a, .page-header h1 a:visited, .page-header h1 a:active {  
    color: #ffffff;  
    font-size: 36px;  
    text-decoration: none;  
}  
.content {  
    margin-left: 40px;  
}  
h1, h2, h3, h4 {  
    font-family: 'Lobster', cursive;  
}  
.date {  
    color: #828282;  
}  
.save {  
    float: right;  
}  
.post-form textarea, .post-form input {  
    width: 100%;  
}  
.top-menu, .top-menu:hover, .top-menu:visited {  
    color: #ffffff;  
    float: right;  
    font-size: 26px;  
    margin-right: 20px;  
}  
.post {  
    margin-bottom: 70px;  
}  
.post h1 a, .post h1 a:visited {  
    color: #000000;  
}

post_list.htmlでCSSを読み込むための設定をします。
staticディレクトリを読み込むために{% load static %}をファイルの最初に追加します。
また、<link rel="stylesheet"...blog.cssを読み込むための設定を追加します。

<post_list.html>
<!DOCTYPE html>  
{% load static %}  
<html lang="en">  
    <head>  
        <meta charset="UTF-8">  
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">  
        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">  
        <link href="//fonts.googleapis.com/css?family=Lobster&subset=latin,latin-ext" rel="stylesheet" type="text/css">  
        <link rel="stylesheet" href="{% static 'css/blog.css' %}">    <-- 追加
        <title>Django Girls blog</title>  
    </head>  
    <body>  
        <div class="page-header">  
            <h1><a href="/">Django Girls Blog</a></h1>  
        </div>  
        <div class="content container">  
            <div class="row">  
                <div class="col-md-8">  
                    {% for post in posts %}  
                    <div class="post">  
                        <p>published: {{ post.published_date }}</p>  
                        <h1><a href="">{{ post.title }}</a></h1>  
                        <p>{{ post.text|linebreaksbr }}</p>  
                    </div>  
                    {% endfor %}  
                </div>  
            </div>  
        </div>  
    </body>  
</html>




*基本テンプレートを作成

各画面に共通する部分は基本テンプレートを使い、異なる部分だけ別で拡張するようにします。
template/blog配下にbase.htmlを新規作成して下記を書き込みます。

<blog/templates/blog/base.html>
<!DOCTYPE html>  
{% load static %}  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">  
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">  
    <link href="//fonts.googleapis.com/css?family=Lobster&subset=latin,latin-ext" rel="stylesheet" type="text/css">  
    <link rel="stylesheet" href="{% static 'css/blog.css' %}">  
    <title>Django Girls blog</title>  
</head>  
<body>  
<div class="page-header">  
    <h1><a href="/">Django Girls Blog</a></h1>  
</div>  
<div class="content container">  
    <div class="row">  
        <div class="col-md-8">  
            {% block content %}  
            {% endblock %}  
        </div>  
    </div>  
</div>  
</body>  
</html>

初期表示用の画面でbase.htmlを読み込む設定を追記し、base.htmlに書かれている内容を削除します。

<blog/templates/blog/post_list.html>
{% extends 'blog/base.html' %}  
  
{% block content %}  
    {% for post in posts %}  
    <div class="post">  
        <p>published: {{ post.published_date }}</p>  
        <h1><a href="">{{ post.title }}</a></h1>  
        <p>{{ post.text|linebreaksbr }}</p>  
    </div>  
    {% endfor %}  
{% endblock %}


*詳細ページへのリンクを追加

タイトルをクリックすると詳細ページに遷移するようにします。{% url 'post_detail' pk=post.pk %}"views.pypost_detail()を呼び出すようにしていますが、ここはこのあと実装します。

<blog/templates/blog/post_list.html>
{% extends 'blog/base.html' %}  
  
{% block content %}  
    {% for post in posts %}  
    <div class="post">  
        <p>published: {{ post.published_date }}</p> 
        <!-- 追加 start --> 
        <h1><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h1>  
        <!-- 追加 end --> 
        <p>{{ post.text|linebreaksbr }}</p>  
    </div>  
    {% endfor %}  
{% endblock %}

views.pypost_detail()を呼び出したいため、urls.pyに設定を追加します。

<blog/urls.py>
from django.urls import path  
from . import views    <-- 追加
  
urlpatterns = [  
    path('', views.post_list, name='post_list'),  
    path('post/<int:pk>/', views.post_detail, name='post_detail'),     <-- 追加
]

詳細画面を表示するためのメソッドpost_detail()を追加します。

<blog/views.py>
from django.shortcuts import render, get_object_or_404  
from django.utils import timezone  
from .models import Post  
  
  
def post_list(request):  
    posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date')  
    return render(request, 'blog/post_list.html', {'posts': posts})  

  
# 追加  
def post_detail(request, pk):  
    post = get_object_or_404(Post, pk=pk)  
    return render(request, 'blog/post_detail.html', {'post': post})

詳細を表示するための画面を新規作成します。

<blog/templates/blog/post_detail.html>
{% extends 'blog/base.html' %}  
  
{% block content %}  
    <div class="post">  
        {% if post.published_date %}  
            <div class="date">  
                {{ post.published_date }}  
            </div>  
        {% endif %}  
        <h1>{{ post.title }}</h1>  
        <p>{{ post.text|linebreaksbr }}</p>  
    </div>  
{% endblock %}


*新規追加画面の作成

記事を新規で投稿するためのフォーム画面を作成します。
forms.pyを新規作成して下記を書き込みます。

<blog/forms.py>
from django import forms
from .models import Post


class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ('title', 'text',)

初期表示画面に、追加用のプラス記号のボタンを追加します。

<blog/template/blog/base.html>
<!DOCTYPE html>  
{% load static %}  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">  
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">  
    <link href="//fonts.googleapis.com/css?family=Lobster&subset=latin,latin-ext" rel="stylesheet" type="text/css">  
    <link rel="stylesheet" href="{% static 'css/blog.css' %}">  
    <title>Django Girls blog</title>  
</head>  
<body>  
<div class="page-header">  
    <!-- 追加 start -->
    <a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>  
    <!-- 追加 end -->
    <h1><a href="/">Django Girls Blog</a></h1>  
</div>  
<div class="content container">  
    <div class="row">  
        <div class="col-md-8">  
            {% block content %}  
            {% endblock %}  
        </div>  
    </div>  
</div>  
</body>  
</html>

フォーム画面用のリンクの設定を追加します。

<blog/urls.py>
from django.urls import path  
from . import views  
  
urlpatterns = [  
    path('', views.post_list, name='post_list'),  
    path('post/<int:pk>/', views.post_detail, name='post_detail'),  
    path('post/new/', views.post_new, name='post_new'),   <-- 追加
]

views.pyに新規追加用のメソッドpost_new()を追加します。

<blog/views.py>
from django.shortcuts import render, get_object_or_404  
from django.utils import timezone  
from .models import Post  
from .forms import PostForm  
from django.shortcuts import redirect  
  
  
def post_list(request):  
    posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date')  
    return render(request, 'blog/post_list.html', {'posts': posts})  
  
  
def post_detail(request, pk):  
    post = get_object_or_404(Post, pk=pk)  
    return render(request, 'blog/post_detail.html', {'post': post})  
  

# 追加
def post_new(request):  
    if request.method == 'POST':  
        form = PostForm(request.POST)  
        if form.is_valid():  
            post = form.save(commit=False)  
            post.author = request.user  
            post.published_date = timezone.now()  
            post.save()  
            return redirect('post_detail', pk=post.pk)  
    else:  
        form = PostForm()  
    return render(request, 'blog/post_edit.html', {'form': form})  















*編集画面の追加

詳細画面から登録済みのデータを編集できるようにします。
編集画面用のテンプレートを新規作成します。

<blog/template/blog_edit.html>
{% extends 'blog/base.html' %}  
  
{% block content %}  
    <h1>New post</h1>  
    <form method="POST" class="post-form">{% csrf_token %}  
        {{ form.as_p }}  
        <button type="submit" class="save btn btn-default">Save</button>  
    </form>  
{% endblock %}

詳細画面に編集ボタンを追加します。

<blog/template/blog/post_detail.html>
{% extends 'blog/base.html' %}  
  
{% block content %}  
    <div class="post">  
        {% if post.published_date %}  
            <div class="date">  
                {{ post.published_date }}  
            </div>  
        {% endif %}  
        <!-- 追加 start -->
        <a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}">  
            <span class="glyphicon glyphicon-pencil"></span>  
        </a>  
        <!-- 追加 end -->
        <h1>{{ post.title }}</h1>  
        <p>{{ post.text|linebreaksbr }}</p>  
    </div>  
{% endblock %}


編集用画面用のリンクの設定を追加します。

<blog/urls.py>
from django.urls import path  
from . import views  
  
urlpatterns = [  
    path('', views.post_list, name='post_list'),  
    path('post/<int:pk>/', views.post_detail, name='post_detail'),  
    path('post/new/', views.post_new, name='post_new'),  
    path('post/<int:pk>/edit/', views.post_edit, name='post_edit'),    <-- 追加
]

データを更新するためのメソッドpost_edit()を追加します。

<blog/views.py>
from django.shortcuts import render, get_object_or_404  
from django.utils import timezone  
from .models import Post  
from .forms import PostForm  
from django.shortcuts import redirect  
  
  
def post_list(request):  
    posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date')  
    return render(request, 'blog/post_list.html', {'posts': posts})  
  
  
def post_detail(request, pk):  
    post = get_object_or_404(Post, pk=pk)  
    return render(request, 'blog/post_detail.html', {'post': post})  
  
  
def post_new(request):  
    if request.method == 'POST':  
        form = PostForm(request.POST)  
        if form.is_valid():  
            post = form.save(commit=False)  
            post.author = request.user  
            post.published_date = timezone.now()  
            post.save()  
            return redirect('post_detail', pk=post.pk)  
    else:  
        form = PostForm()  
    return render(request, 'blog/post_edit.html', {'form': form})  
  

# 追加
def post_edit(request, pk):  
    post = get_object_or_404(Post, pk=pk)  
    if request.method == 'POST':  
        form = PostForm(request.POST, instance=post)  
        if form.is_valid():  
            post = form.save(commit=False)  
            post.author = request.user  
            post.publish_date = timezone.now()  
            post.save()  
            return redirect('post_detail', pk=post.pk)  
    else:  
        form = PostForm(instance=post)  
    return render(request, 'blog/post_edit.html', {'form': form})
























*ブログをアップロード

こちらはやらなくてもいいですが、PythonAnywhereを使うとクラウド上のサーバーで作成したアプリケーションを公開することができます。
https://www.pythonanywhere.com

bashエディタで下記コマンドを実行すると、ローカルでWebサーバーを起動したときと同様の画面を表示することができます。
$ pip3.6 install --user pythonanywhere

$ pa_autoconfigure_django.py https://github.com/<your-github-username>/my-first-blog.git


*所感

このチュートリアルをやったことで、Djangoの基本的な操作を学ぶことができました。わざとエラーにさせて、ログから原因を特定する方法や、Bootstrapの適用方法など書いてあるので、Webアプリケーション開発の初心者にとって非常にわかりやすい内容になっていて、学びが多かったです。今まで何となくしか理解できていなかったことも丁寧に書かれていました。
PythonAnywhereというサービスを初めて知りましたが、無料で使うことができて便利そうなので、今後機会がありましたら使ってみようと思います。

Previous
Next Post »

人気の投稿