この記事について
Djangoを使用する際に実践開発に近いフローを簡単に再現します。
「Djangoを勉強しているけど、実務での開発はどうなっているでしょう」という方の参考になれば嬉しいです。
また本記事の内容は最善とは言えませんので、ぐれぐれもご容赦ください。
本記事の環境
- python3.7.1
- Django 2.1.5
- PyCharm
先ずは設計から
Explicit is better than implicit.
暗示するより明示するほうがいい。 --pythonの禅
何かを作る前に先ず頭にあるアイディアを具現化しましょう。
いかに簡単そうなものでも設計図があった方がいい。
特に会社のプロジェクト、制作途中、新しくメンバーが入ってくることがよくあります。
設計図があれば、プロジェクトを理解するための時間が短縮されます。
モデル詳細
学生(student)
- id
- name (名前)
- profession(職業)
- email(メール)
- twitter(Twitterアカウントも必須w)
- phone (ケータイ)
- status (アカウント状態)
- created_time (作成日時)
設計図が出来たら具体的な制作に入ります。
- 複数のモデルが存在する場合は関係図もあった方がいいです
プロジェクト初期化
python環境はローカルでもいいですが、出来ればプロジェクトごとに仮想環境で分けましょう。
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ファイルを作成しましょう。
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を使用する場合、下記のように記入します。
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'),
}
}
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'}
}
}
...
#ディレクトリ変更したためBASE_DIRを書き直します
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# DATABASESの設定を削除
# DATABASES = {
...
# }
...
最後にmanage.pyとwsgi.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
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
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
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を作りましょう
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
にアクセスしてみてください。
ログイン出来たらオーケーです。
テストデータを作る
データベースに直接入れてもいいですが、ここではちょっと工夫します。
# manage.pyと同じディレクトリで実行してください
mkdir db_tools
cd db_tools && mkdir data
touch import_students_data.py
cd data && touch 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"
}
]
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コードを埋め込むパターンです。
プラットフォームを増やすときに手間が増えるので、今時はあまりおすすめしません。
cd student && touch forms.py
from django import forms
from .models import Student
class StudentForm(forms.ModelForm):
class Meta:
model = Student
fields = "__all__"
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)
<!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>
...
from student.views import IndexView
urlpatterns = [
...
path('', IndexView.as_view(), name="index"),
]
最後にpython manage.py runserver
を実行して、下記の画面が表示されるはずです。
Api-Views
JSONデータだけを処理するViews、Djangoはバックエンドとしてしか機能しません。
なので、Web、iOS、Androidの各プラットホームに対応がしやすいです。
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)
...
from student.views import students_json
urlpatterns = [
...
path('index_json/', students_json, )
]
http://127.0.0.1:8000/index_json/
にアクセスすればJSON形式のデータが確認できるはずです。
またhttp://127.0.0.1:8000/index_json/?status=1
という形式でデータをフィルターすることもできます。
Djangoでより効率よくApiの開発する場合**DRF(django-rest-framework)**をおすすめします。
具体的な使い方はまた別の機会で紹介します。
ミドルウェア
今回のデモはミドルウェアの出番はないですが練習のために使ってみましょう。
index urlにアクセスし、レスポンスを返すまでの時間をミドルウェアで出力してみよう。
studentのviews.pyと同じディレクトリで
touch 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の例外用テンプレートが使用されます。
MIDDLEWARE = [
'student.middlewares.TimeItMiddleware',
...
]
以上の二点を書いておけば、ミドルウェアは動作するはずです。
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を使用する場合は以下を実施します。
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関数を一個追加します
...
@property
def sex_show(self):
return dict(self.SEX_ITEMS)[self.sex]
...
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()
cd student && python tests.py
テストを実行すると、Terminalで下記の結果が確認できるはずです。
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
ケース二つを実行し、全てokでした。
viewテスト
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なのかを確認しています。
実行すると以下の出力が確認できます。
.
----------------------------------------------------------------------
Ran 1 test in 0.047s
OK
注意
もし実行時にエラーがでたら、下記の方法を試してください。
ModuleNotFoundError: No module named 'student'
モデルが見つかりません、でてくる原因としては、実行されるファイルが作業ディレクトリ内で認識されてないかと思います。
下記の内容追加してまた実行してみてください。
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の読み込みでエラーが出たようです。
...
os.environ['DJANGO_SETTINGS_MODULE'] = 'student_sys.settings.develop'
import django
django.setup()
...
上記の内容を追加すれば解決するはずです。
AttributeError: 'StudenTestCase' object has no attribute 'runTest'
...
import unittest
...
if __name__ == "__main__":
suite = unittest.defaultTestLoader.loadTestsFromTestCase(StudenTestCase)
unittest.TextTestRunner().run(suite)
それでも解決出来なかった場合、コメントください。
最後に
If the implementation is easy to explain, it may be a good idea.
もしこの実装簡単に説明するなら、良いアイディアでしょ --pythonの禅