Django 環境構築

#はじめに
勉強ドキュメントです。
チームメンバーで加筆しあったWikiを元にしています。

DjangoとRuby on Railsのどちらで開発を進めるか検討しました。
詳しいメンバーがいなかったので、好みで決めることとなり、結果、Djangoを採用することに決めました。

諸々のバージョンは書いていませんでした…。
汎用的な内容が書いてあるので、ある程度通用すると思います。

ストックされる割にLGTMされないコンテンツ。
LGTMしてくれてもいいんだぜ！

#フレームワークの仕組みについて
MVCフレームワークと異なり、djangoはMTVという形態をとるそうです。
MVCと M = Model であることは共通ですが、V = View はそれぞれのフレームワークで意味が異なります。
MVCに慣れている人は特にViewの意味に注意が必要です。

| MVCの場合  | MTVの場合 | 備考 |
|------------|----------|------------|
| Model      | Model    | ここは一緒。
| View       | Template | 表示系をつかさどるのは同様。<br />呼び方だけ異なるイメージ。
| Controller | View     | 操作系をつかさどるのは同様？<br />ここも呼び方だけ異なるイメージ。

MVCだろうが、MTVだろうが、実装系をどこでやるかは個々の考え方やスキルに因るところが大きいようです。
MTVの場合の実装イメージは以下で統一するのが良いと思います。

- Model

データベースとのインターフェース。プロパティの制約や結合などの外部キーはここに実装する。

- Template

ModelのCRUDの見た目を実装。Modelが複雑になったりするとここにコードが多いと視認性とメンテナンス性が激低下しそう。
極力、表示系の実装のみに特化することが望ましい。

- View

Modelの操作を実装。ここはゴリゴリになっちゃうと思われる。

#のちのちハマらないために
「制約の実装をModelに、見た目の実装をTemplateに。多段のifはクラス化して排除する。」を念頭にプログラムの階層を浅く。
MTVそれぞれの結合度を低く。頑張る！

#開発環境構築
## Windowsの場合
Djangoで新しくプロジェクトを立ち上げるときは以下の操作で行う。
**virtualenvを使ってね！**
**PythonとVS Codeがインストールされていることが前提です！**

```text:Djangoのインストールとプロジェクトの作成
 cd D:\python\projects
 pip install django
 django-admin startproject testpj
 cd testpj
 code .
```

**VS Code起動したら、文字コードをUTF8に変更すること！**
**VS Codeでもvirtualenvの環境読み込むこと！**

以下を実行後、開発用サーバーが起動するか確認すること。
ブラウザで[こちら](http://127.0.0.1:8000/)にアクセスし、"**Congratulations!**"と表示されればOK! 

```python:開発用サーバーの起動
 python manage.py runserver
```

管理者ユーザーを作成する。

```python:管理者ユーザーの作成
 python manage.py createsuperuser
```

**migrateしろってエラー出たら、migrateしてからcreatesuperuserする。**

```python:migrateする
 python manage.py migrate
 Operations to perform:
   Apply all migrations: admin, auth, contenttypes, sessions
 :
 :
 python manage.py createsuperuser
 Username (leave blank to use 'administrator'): administrator
 Email address: administrator@example.com
 Password:
 Password (again):
 Superuser created successfully.
```

Djangoのソースファイルの保存場所は以下のコマンドを実行すれば分かる。

```python:Djangoのソースファイル確認
 python -c "import django; print(django.__path__)"
```

#本番環境構築
##インフラ環境
本番環境はLinuxが良いよねってことでCentOS7で準備しました。
CentOS7、Apache、mod_wsgi を使用します。

## Djangoの実行ユーザを作成する 
単純でOK。

```shell:ユーザー作成
useradd django
```

Djangoのプロジェクトごとにユーザを変更する場合は、各ユーザを同一グループに設定する。

##Python3.xをインストールする
Pythonは標準でyumから導入できない。ソースからコンパイルすることとなる。
pythonの仮想環境にはvenvを用いる、pyenvは使用しない。
CentOS7は元々Python2.4がインストールされているので、混在してしまう。
環境変数の設定に注意。

#開発
##アプリケーションの作成
プロジェクト内の機能という位置づけでアプリケーションを作成するらしい。

```python:アプリケーションの作成
 python manage.py startapp <app_name>
```

**アプリケーション名には _ (アンダースコア、アンダーバー)は使用しないこと。**
**djangoの動きを見ていると、テーブル名などに _ による区切りを使用しているので、競合する可能性が高いです。**

アプリケーションを作成したら、プロジェクトの settings.py の INSTALLED_APPS に追記する。

```python:settings.py
INSTALLED_APPS = [
   'django.contrib.admin',
   :
   :
   '<app_name>',
]
```

アプリケーションをdjango adminから編集させるには、アプリケーションディレクトリ内の admin.py に追記する。

```python:<app>/admin.py(DjangoAdminの有効化)
from .models import <model name>
admin.site.register(<model name>)
```

##Modelの定義
 アプリケーションを作成したら、アプリケーションディレクトリ内のmodels.pyを編集する。

### modelを作成するときの注意点

**models.ForeignKey を使ったフィールドを作成する場合、models.pyの中で参照先となるクラスを先に定義する必要がある。**
**(上から順に読まれるので、最初に書けってこと。)**

### modelをいじったらやること
modelをいじったら、その変更をDBへ反映してあげる必要があります。

```python:migrate
python manage.py check
python manage.py makemigrations <appname>
python manage.py migrate
```

**create文など実行されるクエリは以下のコマンドで確認する。**

```python:クエリの確認
 python manage.py sqlmigrate <appname> <number>
```

### modelの動作確認
対話型shellを使って動作確認できます。

```python:modelの動作確認
python manage.py shell
from Computers.models import Maker
m = Maker(name = "hp", comment = "ヒューレットパッカード")
m.save()
m = Maker(name = "NEC", comment = "NEC")
m.save()
Maker.objects.all()
<QuerySet [<Maker: hp>, <Maker: NEC>]>
```
**Maker.objects.all()の結果は、modelの__str__メソッドの結果が出力される。**

### DBを初期化する

**基本的にやっちゃダメ！**
**どうしてもっていう人用**

DBの初期化ですので、検証中や作成中のプロジェクトでのみ使用するように。
本番プロジェクトで実行したときには…目も当てられません。

modelをちまちまと変更しながらDBへマイグレーションしていると、DBに不整合が発生したりする。
特にUNIQUE、NOT NULL制約やForeignKeyを後から追加した場合など。
こうなると、DBへ手動で手を入れてテーブルの不整合などを直してあげる必要がある。
が、あれこれ考えるより、DBを初期化できる状況ならそれをやったほうが早そう。
…という意見もあったので執筆されています。

ちなみに、本例はsqlite3を利用している場合。

```shell:DB初期化
 rm -d -r db.sqlite3
 rm -d -r <appname>/migrations/*
 python manage.py makemigrations <appname>
 python manage.py migrate
```

その後、管理ユーザーがいなくなるので、再作成する。

```shell:管理ユーザー作成
 python manage.py createsuperuser
```

### アプリケーションを跨いで外部キーを設定する
以下のような感じで、別アプリケーションのmodelを参照するように外部キー制約を設定できる。

```python:外部キー設定
from <model名> import models as <別名>
 
class TestClass(models.Model):
    user_id = models.ForeignKey(<別名>.user_id, on_delete=models.CASCADE,verbose_name='ユーザID')
```

**ただし、Djangoではアプリケーションの再利用性を高くするために、このような設計は出来るだけ使わない方が良いとされている。**
**これを使いたくなったら、アプリケーションやモデルの設計に間違いがないか検討しましょう。**

##Templateの定義
djangoは urls.py というファイルでURLとアプリケーションのルーティング(マッピング)を行います。
ベストプラクティスだと思われる実装を記載します。

###フォルダーの階層構造

- Project Root
  - Project Config
    - settings.py
    - urls.py
  - Applications
    - migrations
    - templates
      - Application Name
        - Class_detail.html
        - Class_list.html
    - admin.py
    - apps.py
    - models.py
    - tests.py
    - urls.py
    - views.py

##URLとviews.pyとtemplatesフォルダーの関連付けについて
ブラウザから入力されるURLと、views.py内のクラスとの関連付けはurls.pyで行います。
まずは、Project Configフォルダー内にある urls.py ファイルが参照されることにご注意ください。
例えば、Project Config/urls.pyが以下のようになっていたとします。

```python:config/urls.py
from django.contrib import admin
from django.urls import path, include

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

すると、ブラウザから、http://…/ComputerAdmin/とアクセスされると、ComputerAdminアプリケーションフォルダー内のurls.pyを参照するようになります。
続いて./ComputerAdmin/urls.pyが以下のようになっているとします。

```python:ComputerAdmin/urls.py
 from django.urls import path
 from . import views
 
 urlpatterns = [
     path('makers/', views.IndexView.as_view()),
     path('makers/<pk>', views.DetailView.as_view()),
 ]
```

views.pyの中で定義するカスタムクラスにdjango標準で準備されているview関連のクラスのうち、どのクラスを継承させるかで、マッピングされるURLが決まります。
例えば、views.pyの中で以下のように定義したとします。

```python:views.py
from django.shortcuts import render
from django.views import generic

from . import models

class IndexView(generic.ListView):
    model = models.Maker
```

すると、./templates/Application_Name/Class_list.html にマッピングされます。
template_nameプロパティなどで、マッピングされるテンプレートをオーバーライドすることもできますが、マッピングルールを暗黙的に理解している人にとっては、コードを追わないといけなくなるので、特別な事情がない限り避けたほうが良いでしょう。

## フォームの実装
あるModelのCRUDを実装するときに、Djangoのフォームを使うと楽できます。

ここでは、ModelFormを利用する例で解説します。まずコード。

```python:Models.py
from django.db import models

class PC(models.Model):
    serial_num = models.CharField(primary_key=True,verbose_name='シリアルナンバー',max_length=20)
    pc_id = models.CharField(verbose_name='管理番号',max_length=8,unique=True,db_index=True)
    pc_model_name = models.ForeignKey(PCModel, on_delete=models.CASCADE,verbose_name='モデル名')
    Purchase_date = models.DateField('購入日')
    status = models.ForeignKey(Status, on_delete=models.CASCADE,verbose_name='ステータス')
    lend_user = models.ForeignKey(User, on_delete=models.SET_NULL,related_name='lend_user',verbose_name='貸出予定ユーザ',blank=True,null=True)
    remarks = models.CharField(verbose_name='備考',max_length=1000,blank=True,null=True)

    def __str__(self):
        return self.pc_id
```

上記の例では、PCというModelが定義しています。(他にも定義してますが、省略しています。)

```python:forms.py
from django import forms
from .models import PC

class PCForm(forms.ModelForm):
    class Meta:
        model = PC
        fields = ('serial_num','pc_id','pc_model_name','Purchase_date','status','lend_user','remarks')
```

定義した「PC」ModelをForms.pyで読み込んでいます。(importしている)
さらに、「PCForm」という名前の「ModelForm」を定義しています。
「PC」Modelの形をしたフォームを定義して、「PC」Modelのフィールドの中で取り扱いたいものをチョイスしている感じです。
なんで、「class Meta:」で定義しないといけないのかは、分かりません…。

フォームを定義したら、viewの方でフォームを読み込ませてあげます。これで、簡単にModelに対応したフォームが作成できます。
PCを新規登録するviewを定義する場合はこんな感じ。もちろん、UpdateやDeleteも同じ要領で行けます。

```python:views.py
from .models import User,PC
from .forms import PCForm
from django.views.generic import CreateView,UpdateView,ListView,DeleteView

class PCCreateView(CreateView):
    model = PC
    form_class = PCForm
    template_name = "management/form_pc.html"

class PCUpdateView(UpdateView):
    model = PC
    form_class = PCForm
    template_name = "management/form_pc.html"
```

「template_name = "management/form_pc.html"」のように、htmlはチコチコ用意する必要があるので、そこは少々面倒かもしれません。
ただ、サイトを作りこんでいくと、フレームワークが用意してくれたもので満足できなくなると思われるので、まあ良いかという感じです。

```html:form_pc.html
{% extends "base.html" %}
{% block content %}
<form method="post">
  {% csrf_token %}
  <table>
  {{ form.as_table }}
  </table>
  <input type="submit" value="保存">
</form>
{% endblock %} 
```

「form」というのがオブジェクト変数みたいに使えます。
この例では「form.as_table」としてますが、「form.as_list」とかもできました。見た目の話なのでお好みで。

urls.pyには以下のパターンを追記します。

```python:urls.py
 url(r'^create_pc$', PCCreateView.as_view()),
```

modelでForeignKeyの制約を掛けていると、勝手に入力フィールドがドロップダウンリストになったりしています。
このフォームは定義したModelに合わせてバリデーションチェックも勝手にやってくれます。
必須項目チェックとかデータ型とか。便利です。

### ModelFormでフィールドの定義をオーバーライドする
ModelFormを利用した場合、外部キー制約を持たせているフィールドは、勝手にドロップダウンリストから選択する形式にします。
基本的には問題ないのですが、ドロップダウンリストの候補数が増えた場合に残念なUIとなるケースがあります。
そんな場合は、フォームのフィールドを定義をオーバーライドすることで解決します。
以下のケースでは、lend_userをオーバーライドし、テキストフィールドにした場合です。
もちろん、外部キー制約は有効なので制約違反の場合は勝手にバリデーションチェックで弾いてくれます。

```python:forms.py
from django.forms import ModelForm,TextInput
from .models import PC

class PCForm(forms.ModelForm):
    class Meta:
        model = PC
        fields = ('serial_num','pc_id','pc_model_name','Purchase_date','status','lend_user','remarks')
        widgets = {
          'lend_user':TextInput(),
        }
```

### フォームでフィルタ機能を実装する
django-filterを用いるのがお手軽で良かった。

#### 導入手順 
pipでインストールする。

```shell:django-filterのインストール
 pip install django-filter
```

setting.pyに以下を追記する。

```python:setting.py
INSTALLED_APPS = [
  ...
  'django_filters',
]
```

#### 正規表現でフィルタするパターン
アプリケーション名：management

```python:models.py
 from django.db import models
 
 class PC(models.Model):
     serial_num = models.CharField(primary_key=True,verbose_name='シリアルナンバー',max_length=20)
     pc_id = models.CharField(verbose_name='PC管理番号',max_length=8,unique=True)
     pc_model_name = models.ForeignKey(PCModel, on_delete=models.CASCADE,verbose_name='モデル名')
     Purchase_date = models.DateField('購入日')
     status = models.ForeignKey(Status, on_delete=models.CASCADE,verbose_name='ステータス')
     lend_user = models.ForeignKey(User, on_delete=models.SET_NULL,related_name='lend_user',verbose_name='貸出予定ユーザ',blank=True,null=True)
     remarks = models.CharField(verbose_name='備考',max_length=1000,blank=True,null=True)
 
     def __str__(self):
         return self.pc_id
```

```python:forms.py
from django import forms
from .models import PC
import django_filters

class PCForm(forms.ModelForm):

    class Meta:
        model = PC
        fields = ('serial_num','pc_id','pc_model_name','Purchase_date','status','lend_user','remarks')
        widgets = {
        }

class PCFilter(django_filters.FilterSet):
    #iregex：大文字小文字を区別しない正規表現
    pc_id = django_filters.CharFilter(label=("PC管理番号"),lookup_expr='iregex')
    serial_num = django_filters.CharFilter(label=("シリアルナンバー"),lookup_expr='iregex')
    remarks = django_filters.CharFilter(label=("備考"),lookup_expr='iregex')

    class Meta:
        model = PC
        fields = ('serial_num','pc_id','pc_model_name','Purchase_date','status','lend_user','remarks')
```

```python:urls.py
from django.urls import path
from django.conf.urls import url
from . import views
from management.views import PCListView
from django_filters.views import FilterView
from .models import PC

urlpatterns = [
  url(r'^list_filter_pc/$', views.PCListView),
]
```

```python:views.py(没例)
from django.shortcuts import render,get_object_or_404,redirect
from django.http import HttpResponse
from .models import PC
from .forms import PCForm,PCFilter
from django_filters.views import FilterView

def PCListView(request):
  f = PCFilter(request.GET, queryset=PC.objects.all())
  return render(request, 'management/list_filter_pc.html', {'filter': f})
```

上記例では、ものすごくクエリが遅くなったので、以下のようにして改善した。

```python:改善例
f = PCFilter(request.GET, queryset=PC.objects.select_related('pc_id','user_name','os_name','configurator','domain_status').all())
```

```html:templates/management/list_filter_pc.html
{% extends "base.html" %}
                              
{% block content %}

<h1>PC管理台帳 一覧表示</h1>
<h2>フィルタメニュー</h2>
<form action="" method="get">
  {{ filter.form.as_ul }}
  <input type="submit" />
</form>

<ul>
  <table border="1">
    <thead>
      <tr>
        <th>シリアルNo</th>
        <th>PC管理番号</th>
        <th>型番</th>
        <th>購入日</th>
        <th>ステータス</th>
        <th>貸与予定者</th>
        <th>備考</th>
      </tr>
    </thead>
    <tbody>
      {% for pc in filter.qs %}
      <tr>
        <td>{{ pc.serial_num }}</a></td>
        <td>{{ pc.pc_id }}</td>
        <td>{{ pc.pc_model_name }}</td>
        <td>{{ pc.Purchase_date }}</td>
        <td>{{ pc.status }}</td>
        <td>{{ pc.lend_user }}</td>
        <td>{{ pc.remarks }}</td>
      </tr>
      {% endfor %}
    </tbody>
  </table>
</ul>

{% endblock %}
```

## WebAPIの実装

### 実装 
以下のようなmodelを定義している例で解説する。

```python:models.py
class User(models.Model):
  user_id = models.CharField(verbose_name='ユーザーID',max_length = 10,unique=True)
  user_name = models.CharField(verbose_name='ユーザー氏名',max_length=20)
```

まず、Django REST frameworkをインストールする。

```shell:djangorestframeworkのインストール
 pip install djangorestframework
```

settings.pyに以下を追記。

```python:settings.py
INSTALLED_APPS = [
  'rest_framework',
  :
```

REST APIを作成するには最低限以下の3つを定義する必要がある

- Serializer (モデルを API のデータに変換したり、逆に API のデータをモデルに変換したりする)
- ViewSet
- URL pattern

これらをAPI化したいModelに対してそれぞれ定義する。

```python:monitomng/serializer.py
 from rest_framework import serializers
 from .models import User
 
 class UserSerializer(serializers.ModelSerializer):
     class Meta:
         model = User
         fields = ('user_id', 'user_name')
```

```python:monitomng/views.py
 :
 from rest_framework import viewsets, filters
 from .serializer import UserSerializer
 
 class UserViewSet(viewsets.ModelViewSet):
     queryset = User.objects.all()
     serializer_class = UserSerializer
```

```python:monitomng/urls.py
:
from monitormng.views import UserViewSet,MonitorViewSet
from rest_framework import routers

router = routers.DefaultRouter()
router.register(r'users', UserViewSet)

urlpatterns = [
  :
]
```

```python:urls.py
:
from monitormng.urls import router as monitor_router

admin.autodiscover()

urlpatterns = [
  path('',            include('management.urls')),
  path('monitormng/', include('monitormng.urls')),
  path('admin/',      admin.site.urls),
  url(r'^api/',       include(monitor_router.urls)),
]
```

### PowerShellからWebAPIを使って、HTTP Requestでデータをいじる
以下のようなmodelを定義している例で解説する。

```python:models.py
class User(models.Model):
  user_id = models.CharField(verbose_name='ユーザーID',max_length = 10,unique=True)
  user_name = models.CharField(verbose_name='ユーザー氏名',max_length=20)
```

ここでは、PowerShellで解説するが、Linuxであればcurlを使うと良い。
調べている限りcurlを使った例は非常に多くヒットするので、迷うことはなさそう。

#### データの取得

##### オブジェクトをリストで取得

```powershell:Web-API-GET-1
Invoke-RestMethod -Uri "http://localhost:8000/api/users/" -Method GET
```

##### オブジェクトを個別に取得

```powershell:Web-API-GET-2
Invoke-RestMethod -Uri "http://localhost:8000/api/users/1" -Method GET
```

#### データの追加

```powershell:Web-API-POST-1
 $postdata = @{ user_id=10; user_name="あいつ" }
 Invoke-RestMethod -Uri "http://localhost:8000/api/users/" -Method POST -Body $postdata
```

日付型や真偽型なども""でくくること。こんな感じ。

```powershell:Web-API-POST-2
$postdata = @{
  monitor_id=1010;
  model_name=1;
  status=1;
  repair_period=3;
  repair_info="";
  onsite_repair="False";
  purchase_date="2018-05-20";
  remarks=""
}
```

#### データの削除

```powershell:Web-API-DELETE
Invoke-RestMethod -Uri "http://localhost:8000/api/users/1" -Method DELETE
```

#### データの変更

```powershell:Web-API-UPDATE
$hash = @{
  user_id = 1;
  user_name = "テストユーザさん";
}
$json = $hash | convertto-json
invoke-restmethod -uri "http://localhost:8000/api/users/1/" -body $json -method PUT -contenttype "application"
```