【Django入門】簡単な単語帳を作る

## はじめに
　インターン先でDjangoに触れる機会があったので、どうせなら普段使いできるようなものをと思い、勉強がてら簡単な単語帳を作成したので、今回はそれについてまとめます。



## 概要

　暗記系科目のテスト勉強では中高生時代はオレンジボールペン＋赤シートで、学部時代はパワーポイントで問題スライドと答えスライドを順番につくることで簡単な単語帳を作っていました。今回はこれらに代わる単語帳をDjangoで作成していきます。求める機能は以下の２点です。

- 問題に対して答えを見るボタンを押すと答えが表示される
- 指定した問題カテゴリ(科目)の問題のみが表示される



## 作成物

**問題ページ**

![1571011401753.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481315/de5800c3-0195-561a-1d81-09775ddeb84f.png)


**答えページ**
![1571011441015.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481315/01142f3c-3893-17dc-24db-0f4aaea7c5c9.png)


問題ページの`答えを見る`ボタンを押すと答えページに移動し`問題に戻る`ボタンを押すと元の問題ページに戻ります。また上の問題カテゴリを指定すると指定されたカテゴリの問題のみが表示される仕様です。



## 実行環境

```
windows10
python 3.7.3
django 2.0.3
```



## Djangoで作成

### Djangoプロジェクトの作成

Djangoではアプリケーションの作成環境をプロジェクトと呼ばれる単位で管理します。ターミナル上でDjangoプロジェクトを作成したいディレクトリに移動して以下のように実行します。

```
$ python -m django startproject mybook
```

カレントディレクトリ上に`mybook`というプロジェクトのディレクトリが以下のような構造で作成されます。

```
mybook/
    manage.py
    mybook/
        __init__.py
        settings.py
        urls.py
        wsgi.py
```

`mybook`プロジェクトのディレクトリの中には、さらに同じ名前のディレクトリが１つ作成されています。これには、このプロジェクトで使うファイル類がまとめられています。Djangoプロジェクトでは、「プロジェクト名と同じ名前のディレクトリ」にプロジェクト全体で使うファイルが保存されいます。各ファイルについて簡単にまとめます。

| ファイル名      | 説明                                                         |
| ------------ | ------------------------------------------------------------ |
| \__init__.py  | Djangoプロジェクトを実行するときの初期化処理を行うスクリプトファイル |
| settings.py   | プロジェクトの設定情報を記述したファイル                     |
| urls.py      | プロジェクトで使うURLを管理するファイル                      |
| wsgi.py      | Webアプリケーションのメインプログラムとなる部分              |

**manage.py** : プロジェクトで実行する様々な機能に関するプログラム



### 言語とタイムゾーンの設定

`mybook/settings.py`を以下のように変更します。

```python
# LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'ja'

# TIME_ZONE = 'UTC'
TIME_ZONE = 'Asia/Tokyo'
```



### サーバの起動

　ここまでで一度プロジェクトが動くかどうかを確認するために以下のコマンドを実行します。

```
mybook$ python manage.py runserver
```

実行したらhttp://127.0.0.1:8000 にアクセスして以下のような画面になれば成功です。

![1571018767761.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481315/5401445a-5afd-1d27-5ca9-ab2477cbb3b9.png)


サーバを終了したいときは、`Control+C`で終了させることができます。



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

　Djangoでは、プロジェクトの中にサブディレクトリとしてアプリケーションを作成します。こうすることで、１つのプロジェクトで複数のアプリケーションを管理できるようになっています。

　アプリケーションを作成するために以下のコマンドを実行します。

```
mybook$ python manage.py startapp vocaapp
```

mybookプロジェクトのディレクトリの中で以下のように作成されました。

```
mybook/
    manage.py
    mybook/
        __init__.py
        settings.py
        urls.py
        wsgi.py
    vocaapp/
        __init__.py
        admin.py
        apps.py
        migrations/
            __init__.py
        models.py
        tests.py
        views.py  
```

vocaappアプリケーションのディレクトリに含まれるファイルについて簡単にまとめます。

| ファイル名                 | 説明                                               |
| -------------------------- | -------------------------------------------------- |
| 「migrations」ディレクトリ | データベース関係の機能のファイルがまとめられている |
| \__init__.py               | アプリケーションの初期化のためのもの               |
| admin.py                   | 管理者ツールのためのもの                           |
| apps.py                    | アプリケーション本体の処理をまとめる               |
| models.py                  | モデルに関する処理を記述するもの                   |
| tests.py                   | プログラムテストに関するもの                       |
| views.py                   | 画面表示に関するもの                               |



### アプリケーションの登録

　アプリケーションを作成をしたら、Djangoに「このプロジェクトでは、こういうアプリケーションがある」ということを知らせる必要があるのでアプリケーションの登録をします。

　`mybook/settings.py`の「INSTASLLED_APPS」という変数の値を設定している部分に以下のように追記します。

```python
INSTALLED_APPS = [
    'vocaapp.apps.VocaappConfig',  # 追記
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
```



### モデルの作成

データベースに定義したいデータを`vocaapp/models.py`に定義します。「モデル＝テーブルの定義」と考えることもできます。

`vocaapp/models.py`を以下のように書き換えます。

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


class Category(models.Model):
    """問題カテゴリ"""
    name = models.CharField('カテゴリ名', max_length=255)
    created_at = models.DateTimeField('作成日', default=timezone.now)

    def __str__(self):
        """テキストの値を返す"""
        return self.name


class Post(models.Model):
    """問題本文と答え"""
    question = models.TextField('問題文')
    answer = models.TextField('答え')
    created_at = models.DateTimeField('作成日', default=timezone.now)
    category = models.ForeignKey(Category, verbose_name='カテゴリ', on_delete=models.PROTECT)

    def __str__(self):
        """テキストの値を返す"""
        return self.question

```



### マイグレーション

　モデルが用意出来たら、次にマイグレーションと呼ばれる作業を行います。マイグレーションとはデータベースの移行を行うための機能です。あるデータベースから別のデータベースに移行する時、必要なテーブルを作成したりしてスムーズに移行できるようにするのがマイグレーションです。マイグレーションは以下の２つの作業からなります。

* マイグレーションファイルの作成
* マイグレーションの適用

それぞれ以下のコマンドで実行できます。

**マイグレーションファイルの作成**

```
mybook$ python manage.py makemigrations vocaapp
```

　これを実行すると、作成したモデルの情報などを元に、マイグレーションファイルを作成します。

**マイグレーション**

```
mybook$ python manage.py migrate
```

　これを実行することで、作成したマイグレーションファイルを適用してデータベースを更新する処理が行われます。



### 管理サイトを有効にする

　まずは管理サイトにログインするために管理者を登録しておく必要があります。以下のコマンドを実行することで登録することができます。

```
mybook$ python manage.py createsuperuser
```

* Username: 管理者名
* Email address：メールアドレス
* Password：パスワード
* Password(Again)：パスワード(確認)

これらが順に尋ねられるのでそれぞれに答えます。



### モデルの登録

　次に先ほど作成したモデルをadmin上で編集できるように`vocaapp/admin.py`を以下のように編集します。

```python
from django.contrib import admin
from .models import Post, Category

# 管理サイトでmodels.pyで定義したものを操作できるようにする
admin.site.register(Post)
admin.site.register(Category)
```

　このようにできたら管理サイトにアクセスしてみます。

```
mybook$ python manage.py runserver
```

サーバを起動したらhttp://127.0.0.1:8000/adminこちらにアクセスします。すると以下のようなログインが面が出てきます。


![1571025856062.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481315/572d7474-4c8a-1d46-1341-1e931c17b6c6.png)

　

先ほど設定したユーザー名とパスワードを入れてログインに成功すると次のような画面に移動します。



Postsから問題カテゴリーと問題文と答えをそれぞれ登録できます。

Posts→POSTを追加で以下のような画面に移動します。
![1571026471039.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481315/3ae464ce-a3f7-188c-e489-b35e30e5c6a9.png)

以下のように追加しておきます。

| カテゴリ | 問題             | 答え    |
| -------- | ---------------- | ------- |
| 英熟語   | apple            | りんご  |
| 英単語   | be surprised at~ | ~に驚く |

adminサイトから簡単にモデルに追加したテーブルの一覧、登録、修正、削除を簡単にすることができます。


### ビューの作成

　データベースからデータを受け取り問題を表示するための問題ページと答えページの部分を作成していきます。

まず`vocaapp/views.py`を以下のようにします。

```python
from django.views import generic
from .models import Post, Category


class IndexView(generic.ListView):
    model = Post
    paginate_by = 10

    def get_queryset(self):
        return Post.objects.order_by('-created_at')


class CategoryView(generic.ListView):
    model = Post
    paginate_by = 10

    def get_queryset(self):
        """
        category = get_object_or_404(Category, pk=self.kwargs['pk'])
        queryset = Post.objects.order_by('created_at').filter(category=category)
        """
        category_pk = self.kwargs['pk']
        queryset = Post.objects.order_by('-created_at').filter(category__pk=category_pk)
        return queryset


class DetailView(generic.DetailView):
    model = Post

```



### vocaappアプリケーションディレクトリ内にurls.pyを作成する

　アプリのアドレスをアプリ内で管理させるようにします。vocaappアプリ内にURLを管理するファイルを作成して、vocaappのアドレスはそこですべて管理します。vocaapp内に新しく`urls.py`を作成し、以下のように書きます。

```python
from django.urls import path
from . import views

app_name = 'vocaapp'

urlpatterns = [
    path('', views.IndexView.as_view(), name='index'), 
    path('category/<int:pk>/', views.CategoryView.as_view(), name='category'),
    path('detail/<int:pk>/', views.DetailView.as_view(), name='detail'),
]
```

続いて、プロジェクトのurls.py(`mybook/urls.py`)を以下のように修正します。

```python
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('vocaapp.urls'))
]
```



### テンプレートの作成

　表示される各画面用のHTMLファイルを保存するテンプレートディレクトリを作成する必要があります。Djangoでは、テンプレートは、各アプリケーションごとに「templates」という名前のディレクトリを用意し、その中に保管するようになっています。まずmybookプロジェクトの「vocaapp」ディレクトリの中に新しく「temlpates」ディレクトリを作成します。そしてさらにこの「temlpates」ディレクトリの中に「vocaapp」というディレクトリを作成し、そこでhtmlファイルを管理します。

ではこの`vocaapp/templates/vocaapp`に以下の４つのhtmlを保存します。

**base.html**

```html
<!doctype html>
<html lang="ja" xmlns:display="http://www.w3.org/1999/xhtml">
  <head>
    <title>単語帳</title>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
  </head>
  <body>
  <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <a class="navbar-brand" href="{% url 'vocaapp:index' %}" >VOCABULARY BOOK</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active" >
        <a class="nav-link" style="color: #00fa9a;">問題カテゴリ</a>
        {% for category in category_list %}
          <a class="nav-link" href="{% url 'vocaapp:category' category.pk %}" style="display: inline;">{{ category }}</a>
        {% endfor %}
      {% if user.is_superuser %}
      <li class="nav-item">
        <a class="nav-link" href="{% url 'admin:index' %}">Admin</a>
      </li>
      {% endif %}
    </ul>
  </div>
　　</nav>
    <div class="container">
      <div class="row">
        <div class="col-md-12">
        {% block content %}
        {% endblock %}
      </div>
     </div>
    </div>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
  </body>
</html>
```

**page.html**

```html
{% load mytag %}
<nav aria-label="Page navigation">
  <ul class="pagination">

      {% if page_obj.has_previous %}
        <li class="page-item">
        <a class="page-link" href="?{% url_replace request 'page' page_obj.previous_page_number %}" aria-label="Previous">
          <span aria-hidden="true">&laquo;</span>
        </a>
      </li>
      {% endif %}

      {% for link_page in page_obj.paginator.page_range %}
        {% if link_page == page_obj.number %}
          <li class="page-item active">
            <a class="page-link" href="?{% url_replace request 'page' link_page %}">
              {{ link_page }}
            </a>
          </li>
        {% else %}
          <li class="page-item">
            <a class="page-link" href="?{% url_replace request 'page' link_page %}">
              {{ link_page }}
            </a>
          </li>
        {% endif %}
      {% endfor %}

      {% if page_obj.has_next %}
        <li class="page-item">
        <a class="page-link" href="?{% url_replace request 'page' page_obj.next_page_number %}" aria-label="Next">
          <span aria-hidden="true">&raquo;</span>
        </a>
      </li>
      {% endif %}
  </ul>
</nav>
```

**post_detail.html**

```html
{% extends 'vocaapp/base.html' %}

{% block content %}
<div class="card mb-3 mt-4">
  <h4 class="card-header bg-dark text-white">{{ post.question }}</h4>
  <div class="card-body">
      <h3>{{ post.answer | linebreaksbr | urlize}}</h3>
      <br /><br />
      <a href="{{request.META.HTTP_REFERER}}" class="btn btn-dark">問題に戻る</a>
  </div>
</div>

{% endblock %}
```

**post_list.html**

```html
{% extends 'vocaapp/base.html' %}

{% block content %}
{% for post in post_list %}
<div class="card mb-4 mt-4">
  <h4 class="card-header bg-dark text-white">問題</h4>
  <div class="card-body">
      <h3>{{post.question}}</h3>
   <br /><br />
    <a href="{% url 'vocaapp:detail' post.pk %}" class="btn btn-dark">答えを見る</a>
  </div>
</div>
{% endfor %}

{% include 'vocaapp/page.html' %}

{% endblock %}
```



### 各機能の追加

問題カテゴリを渡すためにvocaapp内に`context_processors.py`を新しく作成し以下のように書きます。

```python
from .models import Category


def common(request):
    """テンプレートに毎回渡すデータ"""
    context = {
        'category_list': Category.objects.all(),
    }
    return context

```



また、vocaapp内に新しくtemplatetagsディレクトリを作成しその中に`mytag.py`を作成し以下のようにします。

```python
from django import template

register = template.Library()


@register.simple_tag
def url_replace(request, field, value):
    """GETパラメータを一部を置き換える."""
    url_dict = request.GET.copy()
    url_dict[field] = value
    return url_dict.urlencode()
```



最後に`mybook/settings.py`のTEMPLATESの部分に以下のように追記して完成となります。

```python
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'vocaapp.context_processors.common',
            ],
        },
    },
]

```



サーバを起動します。

```
mybook$ python manage.py runserver
```

実行したらhttp://127.0.0.1:8000/ にアクセスして完成していることを確認します。

![1571037471193.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481315/9cf05bfd-00da-dbff-835e-b49f2dc68012.png)




## 最終的なディレクトリ構造

```
mybook/
    manage.py
    db.sqlite3
    mybook/
        __init__.py
        settings.py
        urls.py
        wsgi.py
    vocaapp/
        migrations/
        	0001_initial.py
        	__init__.py
        templates/
        	vocaapp/
        		base.html
        		page.html
        		post_detail.html
        		post_list.html
        templatetags/
        	mytag.py
        __init__.py
        admin.py
        apps.py
        context_processors.py
        models.py
        tests.py
        urls.py
        views.py 
```





## 最後に

　今回はDjangoで簡単な単語帳を作成しました。今後は暗記済みのものにチェックがつけられるようにしたり、問題文や答えに数式が書けるように改良していこうと思います。