Help us understand the problem. What is going on with this article?

Django実践開発入門

この記事について

Djangoを使用する際に実践開発に近いフローを簡単に再現します。
「Djangoを勉強しているけど、実務での開発はどうなっているでしょう」という方の参考になれば嬉しいです。
また本記事の内容は最善とは言えませんので、ぐれぐれもご容赦ください。
timg.jpg

本記事の環境

  • python3.7.1
  • Django 2.1.5
  • PyCharm

先ずは設計から

Explicit is better than implicit.
暗示するより明示するほうがいい。 --pythonの禅

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

今回のデモは簡単なスクール学生管理システムと設定します
キャプチャ.PNG

モデル詳細

学生(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.pyproduct.pyに移動、
例えば開発環境ではsqliteを使用します。
本番環境ではmysqlを使用する場合、下記のように記入します。

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'),
    }
}
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'}
    }
}
base.py
...
#ディレクトリ変更したためBASE_DIRを書き直します
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# DATABASESの設定を削除
# DATABASES = {
...
# }
...

最後にmanage.pywsgi.pyを修正します

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

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

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

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を作りましょう

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にアクセスしてみてください。
ログイン出来たらオーケーです。

キャプチャ.PNG
注意
Web画面に接続ができない場合、
student_sys/student_sys/urls.py内に path('admin/', admin.site.urls)がコメントアウトされている可能性があるので、コメントアウトを解除してください。

テストデータを作る

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

# manage.pyと同じディレクトリで実行してください
mkdir db_tools
cd db_tools && mkdir data
touch import_students_data.py
cd data && touch students_data.py
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_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コードを埋め込むパターンです。
プラットフォームを増やすときに手間が増えるので、今時はあまりおすすめしません。

cd student && touch forms.py
forms.py
from django import forms

from .models import Student


class StudentForm(forms.ModelForm):
    class Meta:
        model = Student
        fields = "__all__"
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)

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>

student_sys\urls.py
...
from student.views import IndexView

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

最後にpython manage.py runserverを実行して、下記の画面が表示されるはずです。
キャプチャ.PNG

Api-Views

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

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)

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形式のデータが確認できるはずです。
キャプチャ.PNG

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

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

ミドルウェア

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

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

touch middlewares.py
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の例外用テンプレートが使用されます。

base.py
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を使用する場合は以下を実施します。

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関数を一個追加します

student\models.py
...
    @property
    def sex_show(self):
        return dict(self.SEX_ITEMS)[self.sex]
...
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()
cd student && python tests.py

テストを実行すると、Terminalで下記の結果が確認できるはずです。

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

OK

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

viewテスト

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なのかを確認しています。
実行すると以下の出力が確認できます。

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

OK

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

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

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の読み込みでエラーが出たようです。

student\tests.py
...
os.environ['DJANGO_SETTINGS_MODULE'] = 'student_sys.settings.develop'
import django
django.setup()
...

上記の内容を追加すれば解決するはずです。

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

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回(予定)

Syoitu
ふしゅふしゅするハリネズミです,お友達は python Go,乗り物DjangoとVue.js,玩具はflutter
bell-face
BtoBセールスに特化したインサイドセールスシステム、ベルフェイスを開発・運営しています
https://bell-face.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした