Djangoを使ってみる#3

#概要
[前回](https://qiita.com/ground0state/items/102da78fd13b27253664)はApacheへのデプロイを学びました。今回はWebアプリ、といいたいのですが、もっと簡単にDBの中身を表示するだけのサイトを作ろうと思います。
OSSの更新情報をスクレイピングで集めて、DBに登録、DBの中身を一覧表示という感じです。スクレイピング部分は日次バッチで動かすので、Djangoには組み込みません。DjangoはDBの中身を表示するWebサイトとして作ります。

##参考
[Djangoを最速でマスターする part1](https://qiita.com/gragragrao/items/373057783ba8856124f3)を参考にさせて頂きました。
（というかほとんど流用なので、ぜひ参考サイトをご覧いただきたく思います）


##環境
+ Windows10 home
+ Anaconda 5.0.1 (Just meでインストール)
+ Python 3.6.3 :: Anaconda, Inc.
+ Django2.0
+Apache HTTP Server2.4.29（以下Apache）

#手順

##プロジェクト作成

まずc:直下でプロジェクトを作成します。

```Django:コマンド
>django-admin startproject oss_project
```
プロジェクトができます。

```Python:ディレクトリ
oss_project/
    manage.py
    oss_project/
        __init__.py
        settings.py
        urls.py
        wsgi.py
```
この最上層のoss_projectをルートディレクトリと呼ぶことにします。
次にoss_projectをカレントディレクトリにしてアプリ（のひな型）を作成します。次のコマンドを実行します。

```Django:コマンド
>python manage.py startapp info
```
これでひな型ができました。ディレクトリは以下のようになっていると思います。

```Python:ディレクトリ
oss_project/
    manage.py
    info/
       migrations
       __init__.py
       admin.py
       apps.py
       models.py
       tests.py
       views.py
    oss_project/
        __init__.py
        settings.py
        urls.py
        wsgi.py
```
setting.pyにアプリを登録します。

```Django:oss_project/setting.py
…
# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'info'　#ここを追加
]
…
```

##データベース作成
テーブルは２つ作ります。ひとつはOSSの名前とOSSのid（識別番号）を登録します。もうひとつは、更新情報を貯めておくテーブルで、OSSのバージョン、OSSのリリース日、スクレイピングした日、OSSの番号を登録します。それぞれのテーブルはmodels.pyの中のclassで定義されます。それぞれOssクラスとInfoクラスに対応します。

```:oss_project/info/models.py
from django.db import models

# Create your models here.

class Info(models.Model):
	
	#OSSのバージョン
	version = models.CharField(max_length=128)
	
	#OSSのリリース日
	released = models.DateField()
	
	#スクレイピングした日
	registrated = models.DateTimeField()
	
	#ossの関係づけ
	oss = models.ForeignKey('Oss',on_delete=models.CASCADE)
	
	class Meta:
		unique_together = (('version', 'oss'))

class Oss(models.Model):
	
	#OSSのid
	oss_id = models.IntegerField(primary_key=True)
	
	#OSSの名前
	oss_name = models.CharField(max_length=128)

```
クラスはmodels.Modelを継承して作成し、カラムはmodels.～で定義します。主キーを定義したい場合はprimary_key=Trueを引数に持たせます。primary_key=Trueがないクラスの場合、DBをマイグレーションしたときに自動的にidカラムが生成されます。
それぞれの型の詳細は調べてほしいのですが、ForeignKeyについて説明しておきます。SQLのJoinの感覚では、InfoクラスのテーブルにもOSSのidカラムを作り、OssクラスのテーブルとJoinしたくなります。しかし、Djangoでは、外部キーとして指定して、見に行くカラムが含まれるクラスを指定します（ここではOss）。すると、見に行ったクラスの主キーがカラムになります（ここではoss_id）。ちょっと慣れが必要かと思います。また、Django2.0からon_delete=models.CASCADEのように、外部キーが削除された場合の動作を記述することが必要です。

テーブルを生成するためのクラスを定義したら、DBを作成します。

```py:コマンド
> python manage.py makemigrations
> python manage.py migrate
```
デフォルトならば、sqlite3がデータベースとして設定されており、最上層oss_projectの直下にdb.sqlite3というファイルが生成されます。

##データ登録
次のコマンドでDjangoのシェルを起動します。

```py:コマンド
> python manage.py shell
```
次のコードを張り付け実行します。

```py:コード
from datetime import timezone, datetime
import pytz
from info.models import *
from dateutil import tz

#Ossクラスのテーブルに登録
Oss.objects.create(oss_id="1", oss_name="Apache httpd")

#Infoクラスのテーブルに登録
oss_id="1"#Apache httpdのid
version="2.4.29"#バージョン
released = date(2017, 10, 23)#リリース日
now = datetime.now().replace(tzinfo=tz.tzlocal())#現在時刻(JST)
registrated = datetime(now.year, now.month, now.day, now.hour, now.minute, now.second)#タイムゾーン情報を落とす

Info.objects.create(oss_id=oss_id, version=version, released=released, registrated=registrated)#SQLでいうところのInsert
```

データベースの中身を見てみましょう。sqlite3のパスを通してあれば、次のコマンドでDBに接続します。

```sqlite3:コマンド
>sqlite3 db.sqlite3
SQLite version 3.20.1 2017-08-24 16:21:36
Enter ".help" for usage hints.
sqlite>
```
接続していない場合はsqlite3.exeを検索してbinフォルダに移動し、db.sqlite3のところをフルパスで実行してください。
テーブル一覧を表示します。

```sqlite3:コマンド
sqlite>.tables
auth_group                  django_admin_log
auth_group_permissions      django_content_type
auth_permission             django_migrations
auth_user                   django_session
auth_user_groups            info_info
auth_user_user_permissions  info_oss
```

info_info、info_ossがあると思います。infoアプリのinfoクラス、ossクラスということでしょうか。実はよくわかってないです。
selectでテーブルの中身を見てみます。

```sqlite3:コマンド
sqlite>select * from info_info;
1|2.4.29|2017-10-23|2018-02-05 00:21:53|1
```
初期状態だとヘッダーがありません。sqliteの設定は`.show`で確認できます。

```sqlite3:コマンド
sqlite> .show
        echo: off
         eqp: off
     explain: auto
     headers: off
        mode: list
   nullvalue: ""
      output: stdout
colseparator: "|"
rowseparator: "\n"
       stats: off
       width:
    filename: db.sqlite3
```
`.headers ON`でヘッダーを表示し、`.mode column`で表形式にしましょう。

```sqlite3:コマンド
sqlite> .headers ON
sqlite> .mode column
sqlite> select * from info_info;
id          version     released    registrated          oss_id
----------  ----------  ----------  -------------------  ----------
1           2.4.29      2017-10-23  2018-02-05 00:21:53  1

sqlite> select * from info_oss;
oss_id      oss_name
----------  ------------
1           Apache httpd
```
一件データが登録されているのを確認できます。同様にほかのOSSもマスタ登録しましょう。今回はTomcatとOpenSSLを登録しました。

```sqlite3:コマンド
sqlite> select * from info_oss;
oss_id      oss_name
----------  ------------
1           Apache httpd
2           Apache Tomcat
3           OpenSSL
```

##viewの作成
Djangoでは、WebページのURLをgetすると、それに対応するview.py内のクラスが呼び出されます。呼び出されたviewがDBに接続したり、その他の処理をして、htmlへと誘導します。

まずクラスを作ります。

```py:oss_project/info/views.py
from django.shortcuts import render

# Create your views here.
from django.shortcuts import render, redirect, get_object_or_404
from django.views.generic import TemplateView

from info.models import *


class InfoListView(TemplateView):
	template_name = "info/info_list.html"

	def get(self, request, *args, **kwargs):
		context = super(InfoListView, self).get_context_data(**kwargs)

		info_list = Info.objects.all().order_by("released").reverse()  # データベースからオブジェクトを取得して
		context['info_list'] = info_list  # 入れ物に入れる

		return render(self.request, self.template_name, context)
```
データベースからデータを取得し、info_listに格納、info_list.htmlへフォワードします。

URLとクラスはsetting.pyで結びつけます。

```Django:oss_project/urls.py
from django.contrib import admin
from django.urls import path
from django.conf.urls import url, include

import info.views

urlpatterns = [
    path('admin/', admin.site.urls),
    url(r'^info_list/', info.views.InfoListView.as_view())
]
```
これで、http://ホスト名/info_list/ とInfoListViewクラスが結びつきます。

##HTMLの作成
oss_project/info/templates/infoディレクトリを作成し、テンプレートとなるbase.htmlを作成します。base.htmlを継承してinfo_list.htmlを作成します。

```html:oss_project/info/templates/info/base.html
<!DOCTYPE html>
<html lang="en">
  <head>
    {% load staticfiles %}
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>{% block title %}{% endblock %}</title>

    <link href="{% static 'bootstrap/vendor/bootstrap/css/bootstrap.min.css' %}" rel="stylesheet">
    <link href="{% static 'bootstrap/vendor/metisMenu/metisMenu.min.css' %}" rel="stylesheet">
    <link href="{% static 'bootstrap/dist/css/sb-admin-2.css' %}" rel="stylesheet">
    <link href="{% static 'bootstrap/vendor/font-awesome/css/font-awesome.min.css' %}" rel="stylesheet" type="text/css">

    <link href="{% static 'info/css/structure.css' %}" rel="stylesheet" type="text/css">

    <script type="text/javascript" src="{% static 'bootstrap/vendor/jquery/jquery.min.js' %}"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.0/jquery-ui.min.js"></script>

    <script src="{% static 'bootstrap/vendor/bootstrap/js/bootstrap.min.js' %}"></script>
    <script src="{% static 'bootstrap/vendor/metisMenu/metisMenu.min.js' %}"></script>
    <script src="{% static 'bootstrap/vendor/datatables/js/jquery.dataTables.min.js' %}"></script>
    <script src="{% static 'bootstrap/vendor/datatables-plugins/dataTables.bootstrap.min.js' %}"></script>
    <script src="{% static 'bootstrap/vendor/datatables-responsive/dataTables.responsive.js' %}"></script>
    <script src="{% static 'bootstrap/dist/js/sb-admin-2.js' %}"></script>

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
        <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
        <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->
  </head>
  <body>
    <div id="wrapper">
      <nav class="navbar navbar-default navbar-static-top manager-nav no-margin" role="navigation">
        <div class="navbar-header">
          <a class="navbar-brand">OSS情報</a>
        </div>
        <div class="navbar-default sidebar" role="navigation">
          <div class="sidebar-nav navbar-collapse">
            <ul class="nav" id="side-menu">
              <li><a href="/info_list/"><i class="fa fa-bar-chart" aria-hidden="true"></i>  OSS更新情報一覧</a></li>
            </ul>
          </div>
        </div>
      </nav>
      {% block body %}
      {% endblock %}
    </div>
  </body>
</html>
```
通常、Webページのhtmlを作成していくと、ヘッダーなどの共通のパーツを作る必要があると思います。共通部分をbase.htmlとし、これを継承して各ページをつくることでhtml作成を効率化します。継承したhtml内の{% ～ %}部分がbase.htmlに埋め込まれるイメージです。

```html:oss_project/info/templates/info/info_list.html
{% extends "info/base.html" %}
{% block title %}Info List{% endblock %}
{% load staticfiles %}

      {% block body %}
      <div id="wrapper">
        <div id="page-wrapper">
          <div class="row">
            <div class="col-lg-6 full-width margin-top-20percent" >
              <div class="panel panel-default full-width">

                <div class="panel-heading">
                  Edit Help
                </div>

                <div class="panel-body full-width full-height">
                  <table id="info-list-table" class="table table-striped table-bordered table-hover dataTable no-footer dtr-inline full-width">
                    <thead>
                      <tr>
                        <th>OSS名</th>
                        <th>バージョン</th>
                        <th>リリース日</th>
                        <th>更新日</th>
                      </tr>
                    </thead>
                    <tbody>
                    {% for record in info_list %}
                      <tr>
                        <td>{{record.oss.oss_name}}</td>
                        <td>{{record.version}}</td>
                        <td>{{record.released|date:"Y/m/d"}}{{record.released|time:"H:i" }}</td>
                        <td>{{record.registrated|date:"Y/m/d"}} {{record.registrated|time:"H:i" }}</td>
                      </tr>
                    {% endfor %}
                    </tbody>
                  </table>
                </div>

              </div>
            </div>
          </div>、
        </div>
      </div>
      <script>
        $(document).ready(function() {
            $('#info-list-table').DataTable({
                responsive: true,
                // sort機能の無効化
                ordering: false,
                // ページの表示件数を変更する
                displayLength: 20,
            });
        });
      </script>
      {% endblock %}
```
ここでbootstrapを使用しています。oss_project/static/bootstrapディレクトリを作成し、https://startbootstrap.com/template-overviews/sb-admin-2/ からダウンロードしたディレクトリ内の、dist、js、vendorを格納します。
また、oss_project/static/info/cssにディレクトリを作成しstructure.cssを格納します。

```css:oss_project/static/info/css/structure.css
/* ---------------------------------------------------------- */
/* general */
/* ---------------------------------------------------------- */

.full-height {
    height: 100%;
}

.full-width {
    width: 100%;
}

.margin-top-20percent {
    margin-top: 20px;
}

.no-margin {
    margin: 0 !important;
}
```
また、{% load staticfiles %}はsetting.pyに設定されているSTATIC_URL配下のファイルを読み込む動作をします。setting.pyを以下のように修正します。

```py:oss_project/oss_project/setting.py
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/

STATIC_URL = '/static/'


# Static file settings
STATIC_ROOT = os.path.join(BASE_DIR, 'assets')

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "static"),

```
基本的に静的ファイルはSTATIC_ROOTで指定したディレクトリに格納します。このディレクトリとURLを結びつけるのがSTATIC_URLでドメイン＋STATIC_URLを指定するとSTATIC_ROOTを見に行く仕組みです。基本、STATIC_ROOTはos.path.join(BASE_DIR, 'assets')のようにルートディレクトリの直下に置き、管理しますが、アプリごとに静的ファイルを用意する場合はSTATICFILES_DIRSにパスを指定します。Djangoは指定したパスを上から順にファイルを探しに行き、先に見つかったものを使用します。

##サーバ起動
runserverでサーバを起動します。

```py:コマンド
> python manage.py runserver 8000
```
http://localhost:8000/info_list にアクセスしてDBに登録したものが表示されることを確認します。

これでDBの中身の表示はできました。次回はスクレイピングを行います。