Django実践開発入門

# この記事について
Djangoを使用する際に実践開発に近いフローを簡単に再現します。
「Djangoを勉強しているけど、実務での開発はどうなっているでしょう」という方の参考になれば嬉しいです。
また本記事の内容は最善とは言えませんので、ぐれぐれもご容赦ください。
![timg.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/258c4031-0f15-ecc6-d712-146be27eae61.jpeg)

#### 本記事の環境
- python3.7.1
- Django 2.1.5
- PyCharm

# 先ずは設計から
*Explicit is better than implicit.*
暗示するより明示するほうがいい。 --pythonの禅

何かを作る前に先ず頭にあるアイディアを具現化しましょう。
いかに簡単そうなものでも設計図があった方がいい。
特に会社のプロジェクト、制作途中、新しくメンバーが入ってくることがよくあります。
設計図があれば、プロジェクトを理解するための時間が短縮されます。

**今回のデモは簡単なスクール学生管理システムと設定します**
<img width="623" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/b4de7ccd-94c5-35eb-a5b6-ac503c66e876.png">


##### モデル詳細
 学生(student)
    - id
    - name          (名前)
    - profession(職業)
    - email(メール)
    - twitter(Twitterアカウントも必須w)
    - phone         (ケータイ)
    - status        (アカウント状態)
    - created_time  (作成日時)
    
設計図が出来たら具体的な制作に入ります。

* *複数のモデルが存在する場合は関係図もあった方がいいです*

# プロジェクト初期化

python環境はローカルでもいいですが、出来ればプロジェクトごとに**仮想環境**で分けましょう。

- [Python仮想環境とDjango](https://qiita.com/Syoitu/items/54d6d3e57482c1e8ab17)


```shell
 cd [好きなディレクトリ]
 mkdir student_house
 cd student_house && django-admin startproject student_sys
 cd student_sys && python manage.py startapp student
```
実行完了後、プロジェクトは以下のディレクトリになります

   
├─  manage.py
├─  student
│   ├─  __init__.py
│   ├─  admin.py
│   ├─  apps.py
│   ├─  migrations
│   │   ├─  `__init__.py `
│   ├─ models.py
│   ├─ tests.py
│   ├─ views.py
├─   student_sys
│   ├─ `__init__.py`
│   ├─ setting.py
│   ├─ url.py
│   ├─ wsgi.py     

# settingsを複数の環境に合わせる
  開発環境と本番環境を分けましょう。
settings.pyの中でif..elseを書くのはあんまりおすすめできません。
**settings.pyの内容を一旦コピーします**
settings.pyを削除し、settingsディレクトリを作成し、配下にpythonファイルを作成しましょう。

```shell
 rm -rf settings.py 
 mkdir settings && cd settings
 touch __init__.py && touch base.py && touch develop.py && touch product.py
```
実行後、以下のディレクトリになります

├─   student_sys
│   ├─ setting
│   │  ├─ `__init__.py`
│   │  ├─ base.py
│   │  ├─ develop.py
│   │  ├─ product.py
│   ├─ `__init__.py`
│   ├─ url.py
│   ├─ wsgi.py 

元の**settings.py**の内容を**base.py**に**ペースト**します。
環境によって変化する部分をそれぞれ**develop.py**と**product.py**に移動、
例えば開発環境では**sqlite**を使用します。
本番環境では**mysql**を使用する場合、下記のように記入します。

```python:develop.py
from .base import *
import os

DEBUG = True
TEMPLATE_DEBUG = DEBUG
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
```

```python:product.py
 from .base import *

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'students',
        'USER': 'root',
        'PASSWORD': 'root',
        'HOST': 'xxx.x.x.x',
        'OPTIONS': {'init_command': 'SET storage_engine=INNODB'}
    }
}
```
```python:base.py
...
#ディレクトリ変更したためBASE_DIRを書き直します
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# DATABASESの設定を削除
# DATABASES = {
...
# }
...
```
最後に**manage.py**と**wsgi.py**を修正します

```python:manage.py
#!/usr/bin/env python
import os
import sys

if __name__ == '__main__':
    # 環境切り替え用のprofile 変数を追加
    profile = os.environ.get("PROJECT_PROFILE", 'develop')
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', f'student_sys.settings.{profile}')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)
```
**wsgi.py**でも同じ修正を加えればsettingの修正は完成しました。
ここで一旦サーバーを起動してエラーがないことを確認することをオススメします。
`python manage.py runserver`

# メインロジカルを書いていく
### model

```python:student_sys/student/models.py
from django.db import models


class Student(models.Model):
    SEX_ITEMS = (
        (1, "男性"),
        (2, "女性"),
        (3, "未入力"),
    )
    STATUS_ITEMS = (
        (0, "申請中"),
        (1, "承認"),
        (2, "拒否"),
        (3, "使用停止")
    )
    name = models.CharField(max_length=128, null=True, blank=True, verbose_name="名前")
    sex = models.IntegerField(choices=SEX_ITEMS, default=3, verbose_name="性別")
    profession = models.CharField(max_length=128, null=True, blank=True, verbose_name="職業")
    email = models.CharField(max_length=128, verbose_name="メールアドレス")
    twitter = models.CharField(max_length=128, null=True, blank=True, verbose_name="TwitterId")
    phone = models.CharField(max_length=128, null=True, blank=True, verbose_name="電話番号")
    status = models.IntegerField(choices=STATUS_ITEMS, default=0, verbose_name="アカウント状態")
    created_time = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="作成日時")

    def __str__(self):
        return f"Student:{self.name}"

    class Meta:
        verbose_name = "学生データ"
        verbose_name_plural = verbose_name
```

### admin

```python:student_sys/student/admin.py

from django.contrib import admin

from .models import Student


class StudentAdmin(admin.ModelAdmin):
    list_display = ("id", "name", "sex", "profession", "email", "twitter", "phone", "status", "created_time")
    list_filter = ('sex', "status", 'created_time')
    search_fields = ("name", "profession")
    fieldsets = (
        (None, {
            "fields": (
                "name",
                ("sex", "profession"),
                ("email", "twitter", "phone"),
                "status",
            )
        }),
    )

admin.site.register(Student, StudentAdmin)
```
### setting

```python:student_sys/settings/base.py
INSTALLED_APPS = [
    # studentを追加
    'student',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
  
]

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True

```
これでバックエンドは完了しました。
**migrate**を実行してから、**createsuperuser**を作りましょう

```shell
cd student_sys/student_sys/
python manage.py makemigrations 
python manage.py migrate
python manage.py createsuperuser
```
完了後 `python manage.py runserver` でサーバーを立ち上げ、
`http://127.0.0.1:8000/admin`にアクセスしてみてください。
ログイン出来たらオーケーです。

<img width="921" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/623d1042-b648-3b47-70be-617a3178fea2.png">
**注意**
Web画面に接続ができない場合、
student_sys/student_sys/urls.py内に *path('admin/', admin.site.urls)*がコメントアウトされている可能性があるので、コメントアウトを解除してください。

### テストデータを作る

データベースに直接入れてもいいですが、ここではちょっと工夫します。

```shell
# manage.pyと同じディレクトリで実行してください
mkdir db_tools
cd db_tools && mkdir data
touch import_students_data.py
cd data && touch students_data.py
```
```python:students_data.py
row_data = [
    {
        "name": "test1",
        "sex": "1",
        "profession": "ニート",
        "email": "neat@gamil.com",
        "twitter": "neat123",
        "phone": "09012344321",
        "status": "1"
    },
    {
        "name": "test2",
        "sex": "1",
        "profession": "ニート",
        "email": "neat@gamil.com",
        "twitter": "neat123",
        "phone": "09012344321",
        "status": "1"
    },
    {
        "name": "test3",
        "sex": "1",
        "profession": "ニート",
        "email": "neat@gamil.com",
        "twitter": "neat123",
        "phone": "09012344321",
        "status": "1"
    }
]

```
```python:import_students_data.py
import sys
import os

pwd = os.path.dirname(os.path.realpath(__file__))
x = sys.path.append(pwd + "../")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "student_sys.settings.develop")

import django
django.setup()
from student.models import Student

from db_tools.data.students_data import row_data

for info in row_data:
    student_info = Student()
    student_info.name = info['name']
    student_info.sex = info['sex']
    student_info.profession = info["profession"]
    student_info.email = info['email']
    student_info.twitter = info['twitter']
    student_info.phone = info['phone']
    student_info.status = info['status']
    student_info.save()

```
 import_students_data.pyを実行すれば、テスト用のデータがデータベースに挿入されるはずです。


### views

#### template-views
HTMLの中にpythonコードを埋め込むパターンです。
プラットフォームを増やすときに手間が増えるので、今時はあまりおすすめしません。

```shell
cd student && touch forms.py
```

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

from .models import Student


class StudentForm(forms.ModelForm):
    class Meta:
        model = Student
        fields = "__all__"
```

```python:views.py

from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from django.views import View

from .forms import StudentForm
from .models import Student

class IndexView(View):
    template_name = "index.html"

    def get_context(self):
        students = Student.objects.all()
        context = {
            "students": students
        }
        return context

    def get(self, request):
        context = self.get_context()
        form = StudentForm()
        context.update({
            "form": form
        })
        return render(request, self.template_name, context=context)

    def post(self, request):
        form = StudentForm(request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse("index"))
        context = self.get_context()
        context.update({
            "form": form
        })
        return render(request, self.template_name, context=context)

```

```html:templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>学生管理システム</title>
</head>
<body>
        <h3><a href="/admin/">Admin</a></h3>
        <ul>
            {% for student in students %}
            <li>{{ student.name }}-{{ student.get_status_display }}</li>
            {% endfor %}
        </ul>
        <form action="/" method="post">
            {% csrf_token %}
            {{ form }}
            <input type="submit" value="Submit">
        </form>
</body>
</html>

```

```python:student_sys\urls.py
...
from student.views import IndexView

urlpatterns = [
    ...
    path('', IndexView.as_view(), name="index"),
]

```
最後に`python manage.py runserver`を実行して、下記の画面が表示されるはずです。
<img width="612" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/994015ef-4044-df58-d496-b67bd335a7de.png">



#### Api-Views
JSONデータだけを処理するViews、Djangoはバックエンドとしてしか機能しません。
なので、Web、iOS、Androidの各プラットホームに対応がしやすいです。

```python:views.py
import json

from django.core import serializers
from django.http import JsonResponse

from .models import Student

def students_json(request):
    if request.method == "GET":
        status = request.GET.get("status")
        if status is None or status > 3:
            students = Student.objects.all()
        else:
            students = Student.objects.filter(status=status)
        json_data = serializers.serialize("json", students)
        json_data = json.loads(json_data)
        return JsonResponse(json_data, safe=False)
    else:
        json_data = [{"data": "Invalid Method"}]
        return JsonResponse(json_data, status=405, safe=False)

```

```python:student_sys\urls.py
...
from student.views import students_json

urlpatterns = [
    ...
    path('index_json/', students_json, )
]

```

`http://127.0.0.1:8000/index_json/` にアクセスすればJSON形式のデータが確認できるはずです。
<img width="285" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/5d004fc9-f7d2-55a9-876d-95f56b42796e.png">

また`http://127.0.0.1:8000/index_json/?status=1`という形式でデータをフィルターすることもできます。

Djangoでより効率よくApiの開発する場合**DRF(django-rest-framework)**をおすすめします。
具体的な使い方はまた別の機会で紹介します。

# ミドルウェア
今回のデモはミドルウェアの出番はないですが練習のために使ってみましょう。
**index urlにアクセスし、レスポンスを返すまでの時間をミドルウェアで出力してみよう。**

studentのviews.pyと同じディレクトリで

```shell
touch middlewares.py
```

```python:middlewares.py
import time

from django.urls import reverse
from django.utils.deprecation import MiddlewareMixin


class TimeItMiddleware(MiddlewareMixin):
    def process_request(self, request):
        self.start_time = time.time()
        return

    def process_view(self, request, func, *args, **kwargs):
        if request.path != reverse('index'):
            return None
        start = time.time()
        response = func(request)
        costed = time.time() - start
        print('process view:{:.2f}s'.format(costed))
        return response

    def process_exception(self, request, exception):
        pass

    def process_template_response(self, request, response):
        return response

    def process_response(self, request, response):
        costed = time.time() - self.start_time
        print('request to response cose:{:.2f}s'.format(costed))
        return response
```


>process_request
>> リクエストがミドルウェアに入って来てから最初に実行します。
リクエストの検証に使用することが多い。
NoneまたはHttpResponseを返します。
もしHttpResponseを返す場合、`process_response`のみが実行される。

>process_view
>> `process_response`の後に実行されます。
パラメータ`func`は実行されるview関数です。
NoneまたはHttpResponseを返します。
もしNoneを返す場合、view関数が実行されます。

>process_template_response
>>以上の関数実行後、もしテンプレートビュー(return render(request,'index.html',context={}))を使用する場合実行されます。
Content-Typeの設定やheaderの修正ができます。

>process_response
>>最後に実行されます。
使用方法は`process_template_response`と同じです。

>process_exception
>>viewを呼び出す段階で例外がraiseされた時に実行されます。
もし`process_view`で自主的に`func`を使用する場合は実行されません。
例外処理のために使用することが多いがNoneを返す場合、Djangoの例外用テンプレートが使用されます。

```python:base.py
MIDDLEWARE = [
    'student.middlewares.TimeItMiddleware',
    ...
]
```
以上の二点を書いておけば、ミドルウェアは動作するはずです。

```shell
process view:0.02s
request to response cose:0.02s
[04/Aug/2019 16:45:53] "GET / HTTP/1.1" 200 2115

```

# ユニットテスト

テストに関しては、非常に大事な一環ですが、飛ばされがちです。
品質担保するためにも、絶対書きましょう。

**SQLite**使用してる場合、Djangoはメモリにテスト用のDBを作ってくれます。
なので特に何もしなくても大丈夫です。

**MySQL**を使用する場合は以下を実施します。

```python:develop
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'students',
        'USER': 'root',
        'PASSWORD': 'root',
        'HOST': 'xxx.x.x.x',
        'OPTIONS': {'init_command': 'SET storage_engine=INNODB'},
        'TEST': {
          'NAME': 'mytestdatabase', #追加
        } 
    }
}
```
Djangoは`TestCase`というBaseClassを提供しています。
それを継承してテストケースを書きましょう。
その前に`TestCase`でよく使用する関数を見てみます。

| 関数       |       用途   |
|:-----------------|------------------------------------:|
| setUp           |                 テストケース実行前のデータ初期化などに使用ことが多い                     |
| test_xxxx           |            テストケース、全てのテストケースは独立してます |
| tearDown             | テストケース実行後の後処理、Djangoでは気にする必要はありません      |

### modelテスト
テストするためにmodelにproperty関数を一個追加します

```python:student\models.py
...
    @property
    def sex_show(self):
        return dict(self.SEX_ITEMS)[self.sex]
...
```

```python:student\tests.py
from django.test import TestCase
from student.models import Student

class StudenTestCase(TestCase):
    def setUp(self) -> None:
        Student.objects.create(
            name='the5fire',
            sex=1,
            email='nobody@gmail.com',
            profession='エンジニア',
            twitter='twitter',
            phone="3222"
        )

    def test_create_and_sex_show(self):
        student = Student.objects.create(
            name='huyang',
            sex=1,
            email='nobody@gmail.com',
            profession='エンジニア',
            twitter='twitter',
            phone="3222"
        )
        self.assertEqual(student.sex_show, '男性', 'sexカラムの値が一致しません')

    def test_filter(self):
        Student.objects.create(
            name='huyang',
            sex=1,
            email='nobody@gmail.com',
            profession='エンジニア',
            twitter='twitter',
            phone="3222"
        )
        name = 'the5fire'
        students = Student.objects.filter(name=name)
        self.assertEqual(students.count(), 1, '名前は{}の記録は一つのはず'.format(name))

if __name__ == "__main__":
    s = StudenTestCase()
    s.run()
```

```shell:
cd student && python tests.py
```
テストを実行すると、Terminalで下記の結果が確認できるはずです。

```shell
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
```
ケース二つを実行し、全てokでした。

### viewテスト

```python:student\tests.py
from django.test import TestCase, Client
from student.models import Student

class StudenTestCase(TestCase):
    def test_get_index(self):
        client = Client()
        response = client.get('/')
        self.assertEqual(response.status_code, 200, 'status code must be 200!')

```

**test_get_index** : ホームページにアクセスして、status codeが200なのかを確認しています。
実行すると以下の出力が確認できます。

```shell
.
----------------------------------------------------------------------
Ran 1 test in 0.047s

OK

```
**注意**
もし実行時にエラーがでたら、下記の方法を試してください。

` ModuleNotFoundError: No module named 'student' `
モデルが見つかりません、でてくる原因としては、実行されるファイルが作業ディレクトリ内で認識されてないかと思います。
下記の内容追加してまた実行してみてください。

```python:students\tests.py
import sys
import os
curPath = os.path.abspath(os.path.dirname(__file__))
rootPath = os.path.split(curPath)[0]
sys.path.append(rootPath)
```

`django.core.exceptions.ImproperlyConfigured: Requested setting INSTALLED_APPS, but settings are not configured. You must either define the environment variable DJANGO_SETTING
S_MODULE or call settings.configure() before accessing settings.
`
settingsの読み込みでエラーが出たようです。

```python:student\tests.py
...
os.environ['DJANGO_SETTINGS_MODULE'] = 'student_sys.settings.develop'
import django
django.setup()
...
```
上記の内容を追加すれば解決するはずです。

`AttributeError: 'StudenTestCase' object has no attribute 'runTest'
`

```python:student\tests.py
...
import unittest
...
if __name__ == "__main__":
    suite = unittest.defaultTestLoader.loadTestsFromTestCase(StudenTestCase)
    unittest.TextTestRunner().run(suite)
```
それでも解決出来なかった場合、コメントください。:raising_hand:

# 最後に
*If the implementation is easy to explain, it may be a good idea.*
もしこの実装簡単に説明するなら、良いアイディアでしょ　--pythonの禅
### 続編
[Django REST framework + Vue.js「SEIYU風のECサイトを作りましょう」全6回（予定）](https://qiita.com/Syoitu/items/62e6ec1437d98d6b9d28)