Open8

Djangoの学習メモ

HIR0HIR0

新規プロジェクトの作成

$ django-admin startproject app

$ cd && tree
./app
├ /app
| ├ __init__.py
| ├ asgi.py
| ├ settings.py
| ├ urls.py
| └ wsgi.py
└ manage.py

設定ファイルの書き換え

設定ファイルは./app/appの配下に存在するsetting.pyファイル。言語設定とタイムゾーンを切り替える。

setting.py

- LANGUAGE_CODE = "en-us"
+ LANGUAGE_CODE = "ja"

- TIME_ZONE = "UTC"
+ TIME_ZONE = "Asia/Tokyo"

マイグレーションコマンド実行

デフォルトではSQLite3を使うように設定されている。
実行するとapp配下にdb.sqlite3ファイルが生成される。

$ python manage.py migrate

# スーパーユーザを作成
$ python manage.py createsuperuser
admin
admin@plactice.com
hogehoge
hogehoge 

サーバー起動

$ python magane.py runsever
HIR0HIR0

新しいアプリケーションを作る

./appの配下に指定した名前のアプリケーションディレクトリが生成される。
アプリケーションは機能単位ごとで生成されていくようなイメージ。

$ python manage.py startapp placticeapp

├ /app
| └ /placticeapp
|     ├ /migrations
|     |     └ __init__.py
|     ├ __init__.py
|     ├ admin.py
|     ├ models.py
|     ├ tests.py
|     └ views.py
└ manage.py

アプリケーションの定義を設定に反映する

settings.py
# Application definition

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
+    "placticeapp.apps.PlacticeappConfig",
]
HIR0HIR0

新しいモデルを作る

モデル(Model)とは

MVTモデルの構成要素の一つであり、DBへ直接アクセスすることを防ぎ、DBのデータをクラスを使って表現、アクセス可能としたもの。

placticeapp/models.py
from django.db import models

class Person(models.Model):
   first_name = models.CharField("名", max_length=30)
   last_name = models.CharField("姓", max_length=30)
   age = models.IntegerField("年齢")
   nickname = models.CharField("ニックネーム", max_length=255, blank=True)

   def __str__(self):
       return f"{self.last_name} {self.first_name}"

class Organization(models.Model):
    name = models.CharField("組織名", max_length=255)
    person = models.ForeignKey(Person, verbose_name="人", related_name="organizations", on_delete=models.CASCADE)


    def __str__(self):
        return self.name
# マイグレーションファイル作成
$ python mangae.py makemigrations placticeapp
Migrations for 'placticeapp':
  placticeapp/migrations/0001_initial.py
    - Create model Person
    - Create model Organization

# SQLを確認
$ python manage.py sqlmigrate placticeapp 0001
BEGIN;
--
-- Create model Person
--
CREATE TABLE "placticeapp_person" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "first_name" varchar(30) NOT NULL, "last_name" varchar(30) NOT NULL, "age" integer NOT NULL, "nickname" varchar(255) NOT NULL);
--
-- Create model Organization
--
CREATE TABLE "placticeapp_organization" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(255) NOT NULL, "person_id" bigint NOT NULL REFERENCES "placticeapp_person" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "placticeapp_organization_person_id_598c497e" ON "placticeapp_organization" ("person_id");
COMMIT;

# マイグレーション実行
$ python magane.py migrate placticeapp
Operations to perform:
  Apply all migrations: placticeapp
Running migrations:
  Applying placticeapp.0001_initial... OK

管理サイトでデータを編集する

Djangoの機能を使って管理ページからデータを閲覧・編集することができる。
その際、編集したいデータモデルを管理サイトに紐づけておく必要がある。
紐づけができたら、(http://localhost:8000/admin)にアクセスし、事前に作成したadminユーザーでログインすると、データが見れるようになっている。

placticeapp/admin.py
from django.contrib import admin
+ from plactice.models import Person, Organization

+ class PersonAdmin(admin.ModelAdmin):
+    list_display = ("id", "first_name", "last_name", "age", "nickname")
+    list_display_links = "id"

+ admin.site.register(Person, PersonAdmin)
+ admin.site.register(Organization)
HIR0HIR0

新しくビューを作る

ビュー(View)とは

MVTモデルの構成要素の一つであり、どのページを表示させるかをリクエストを元に決定する処理を担う。またModelから取得したデータを整形して、Templateに渡す。
Djangoでは関数やクラスを使って記述することができる。

plactice/view.py
from django.shortcuts import get_object_or_404, redirect, render
from django.http import HttpResponse
from placticeapp.models import Person
from placticeapp.forms import PersonForm

def person_list(request):
    persons = Person.objects.all().order_by("id")
    return render(request, "placticeapp/person_list.html", {"persons": persons})

def person_edit(request, person_id=None):
    if person_id:
        person = get_object_or_404(Person, pk=person_id)
    else:
        person = Person()

    if request.method == "POST":
        form = PersonForm(request.POST, instance=person)
        if form.is_valid():
            person = form.save(commit=False)
            person.save()
            return redirect("placticeapp:person_list")
    else:
        form = PersonForm(instance=person)
    return render(
        request, "placticeapp/person_edit.html", dict(form=form, person_id=person_id)
    )

def person_delete(request, person_id):
    person = get_object_or_404(Person, pk=person_id)
    person.delete()
    return redirect("placticeapp:person_list")

URLのルーティングを定義する

placticeappディレクトリに新たにurls.pyを作成する。
定義したルーティングをプロジェクト全体のurlsで読み込む。

placticeapp/urls.py
from django.urls import path
from placticeapp import views

app_name = "placticeapp"
urlpatterns = [
    path("person/", views.person_list, name="person_list"),
    path("person/create/", views.person_edit, name="person_create"),
    path("person/edit/<int:person_id>/", views.person_edit, name="person_edit"),
    path("person/delete/<int:person_id>/", views.person_delete, name="person_delete"),
]
app/urls.py
from django.contrib import admin
- from django.urls import path, include
+ from django.urls import path, include

urlpatterns = [
+   path("placticeapp/", include("placticeapp.urls"))
    path("admin/", admin.site.urls),
]

入力フォームを定義する

placticeappディレクトリに新たにforms.pyを作成する。

placticeapp/forms.py
from django.forms import ModelForm
from placticeapp.models import Person

class PersonForm(ModelForm):
    class Meta:
        model = Person
        fields = ("first_name", "last_name", "age", "nickname,")
HIR0HIR0

新しくテンプレートを作る

テンプレート(Template)とは

実際のページ(HTML)となりうるもの。Viewを通じて受け取ったデータをテンプレートに当てはめてユーザーに見せる。MVCモデルのViewにあたる。

人一覧のテンプレートを作成

placticeappディレクトリにtemplates/placticeappディレクトリを作成し、そこにbase.htmlperson_list.htmlファイルを作成する。

placticeapp/templates/placticeapp/base.html
{% load i18n static %}
<!DOCTYPE html>{% get_current_language as LANGUAGE_CODE %}
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}">
    <head>
        <meta charset="utf-8">
        <meta name="view-port" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>{% block title %}Django Plactice App{% endblock %}</title>
    </head>
    <body>
        <div class="container">
            {% block content %}
                {{ content }}
            {% endblock %}
        </div>
    </body>
</html>
placticeapp/templates/placticeapp/person_list.html
{% extends "placticeapp/base.html" %}
{% block title %}人の一覧{% endblock %}
{% block content %}
    <h1>人の一覧</h1>
    <a href="{% url "placticeapp:person_create" %}" class="btn">新規作成</a>
    <table class="table">
        <thead>
            <tr>
                <th scope="col">ID</th>
                <th scope="col">名前</th>
                <th scope="col">年齢</th>
                <th scope="col">ニックネーム</th>
                <th scope="col">操作</th>
            </tr>
        </thead>
        <tbody>
            {% for person in persons %}
            <tr>
                <th scope="row">{{ person.id }}</th>
                <td>{{ person.last_name }} {{ person.first_name }}</td>
                <td>{{ person.age }}</td>
                <td>{{ person.nickname }}</td>
                <td>
                    <a href="{% url "placticeapp:person_edit" person_id=person.id %}" class="btn">編集</a>
                    <a href="{% url "placticeapp:person_delete" person_id=person.id %}" class="btn">削除</a>
                </td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
{% endblock %}

(http://localhost:8000/placticeapp/persons)にアクセス。

人編集のテンプレートを追加

placticeapp/templates/placticeappディレクトリにperson_edit.htmlファイルを追加。

person_edit.html
{% extends "placticeapp/base.html" %}
{% block title %}人の編集{% endblock %}
{% block content %}
    <h4>人の編集</h4>
    {% if person_id %}
        <form action="{% url "placticeapp:person_edit" person_id=person_id %}" method="post">
    {% else %}
    <form action="{% url "placticeapp:person_create" %}" method="post">
    {% endif %}
        {% csrf_token %}
        <div class="form-group">
            <label for="last_name"></label>
            <input type="text" class="form-control" id="last_name" name="last_name" value="{{ person.last_name }}">
            <label for="first_name"></label>
            <input type="text" class="form-control" id="first_name" name="first_name" value="{{ person.first_name }}">
            <label for="age">年齢</label>
            <input type="number" class="form-control" id="age" name="age" value="{{ person.age }}">
            <label for="nickname">ニックネーム</label>
            <input type="text" class="form-control" id="nickname" name="nickname" value="{{ person.nickname }}">
        </div>
        <div class="form-group">
            <button type="submit" class="btn">送信</button>
        </div>
    </form>
    <a href="{% url "placticeapp:person_list" %}" class="btn">戻る</a>
{% endblock %}
HIR0HIR0

APIサーバーとして実装する

JSON形式のレスポンスを返すためのAPIサーバーとしての実装を試す。
まずはこれまでと同様にアプリケーションを新規作成する。

$ python manage.py startapp placticeapi

Viewファイルを編集

placticeapi/views.py
import json
from collections import OrderedDict
from django.http import HttpResponse
from placticeapp.models import Person


# Create your views here.
def render_json_response(request, data, status=None):
    json_str = json.dumps(data, ensure_ascii=False, indent=2)
    callback = request.GET.get("callback")
    if not callback:
        callback = request.POST.get("callback")
    if callback:
        json_str = "%s{%s}" % (callback, json_str)
        response = HttpResponse(
            json_str,
            content_type="application/javascript; charset=UTF-8",
            status=status,
        )
    else:
        response = HttpResponse(
            json_str, content_type="application/json; charset=UTF-8", status=status
        )
    return response


def person_list(request):
    persons = []
    for person in Person.objects.all().order_by("id"):
        organizations = []
        for organization in person.organizations.order_by("id"):
            organization_dict = OrderedDict(
                [
                    ("id", organization.id),
                    ("name", organization.name),
                ]
            )
            organizations.append(organization_dict)
        person_dict = OrderedDict(
            [
                ("id", person.id),
                ("first_name", person.first_name),
                ("last_name", person.last_name),
                ("age", person.age),
                ("nickname", person.nickname),
                ("organizations", organizations),
            ]
        )
        persons.append(person_dict)

    data = OrderedDict([("persons", persons)])
    return render_json_response(request, data)

各ファイルにAPIサーバー用の設定を追記

placticeapiディレクトリにurls.pyファイルを作成。

placticeapi/urls.py
from django.urls import path
from placticeapi import views

app_name = "placticeapi"
urlpatterns = [
    path("v1/persons/", views.person_list, name="person_list"),
]
placticeapi/urls.py
urlpatterns = [
    path("placticeapp/", include("placticeapp.urls")),
+   path("placticeapi/", include("placticeapi.urls")),
    path("admin/", admin.site.urls),
]
app/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "placticeapp.apps.PlacticeappConfig",
+   "placticeapi.apps.PlacticeapiConfig",
]

(http://localhost:8000/placticeapi/v1/persons/)にアクセス。

所感

APIの作成はFastAPIで実装したほうが楽だなあ

HIR0HIR0

バッチ処理を実装する

Djangoにはバッチ処理をコマンド単位で実行できる機能が存在している。公式的にはカスタムコマンドと表現されている。

https://docs.djangoproject.com/ja/1.10/howto/custom-management-commands/

特徴

  • 引数を受け取ることが可能
  • コマンドはmanagement/commands/配下に作成する
  • 1コマンドにつき1Pythonファイル
 $ python manage.py startapp placticebatch

設定ファイルapp/settings.pyにアプリケーションを追加。

app/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "placticeapp.apps.PlacticeappConfig",
    "placticeapi.apps.PlacticeapiConfig",
+   "placticebatch.apps.PlacticebatchConfig",
]

バッチ処理を認識させるためのファイルを作成

Djangoのバッチ処理はmanagement/commandsディレクトリに存在するファイルをバッチ処理用のコマンドとして認識するため、必要なディレクトリとファイルを作成する。

$ cd ./placticebatch
$ mkdir -p management/commands
$ touch management/commands/__init__.py
$ touch management/commands/command_1.py

コマンドの書き方

Djangoがコマンドを認識するためにはBaseCommandクラスを継承したクラスを定義し、クラス内でhandleメソッドを定義する。

management/commands/command_1.py
from django.core.management.base import BaseCommand

class Command(BaseCommand):
    help = "コマンドの説明"

    def handle(self, *args, **options):
        print("コマンドの処理")

コマンドの実行

# コマンドがDjangoに認識されているか確認
$ python manage.py help

# コマンド実行
$ python manage.py command_1

コマンドライン引数を定義

add_argumentsメソッドを定義することで、コマンドライン引数を実装することができる。

management/commands/command_1.py
class Command(BaseCommand):
    help = "コマンドの説明"

    def handle(self, *args, **options):
        print(f"渡された引数は{options['name']}")

+   def add_arguments(self, parser):
        parser.add_arguments("--name", nargs="1", default="", type=str)
$ python manage.py command_1 --name テスト
渡された引数はテスト
HIR0HIR0

ロギング設定

Djangoのロギング設定はsettings.pyファイル内に記述する。
ロギングの基本的な仕様はloggingモジュールに準じており、Djangoだからといって特に特別な作り込みはされていないっぽい。

ログレベル

INFOのログレベルを持ったloggerやhandlerはINFO以上のログを出力するため、DEBUGやNOTSETで指定されたログは出力しない。

レベル 概要
NOTSET 全てのログ
DEBUG デバッグ用のログ
INFO 正常に動作しているときのログ
WARNING 警告のログ
ERROR エラーなどの問題が発生したときのログ
CRITICAL 致命的な問題が発生したときのログ

formatter

出力するログのフォーマットを指定する。
フォーマットにはそれぞれ名前を指定することが可能で、ここで作成したformatterを次に紹介するhandlersに渡す。

filter

ログ出力をフィルタリングするためのもの。
デバッグ設定のON/OFFによってログをフィルタリングさせるなどができる。

handler

ログの出力方式を指定する。
出力方式はコンソールやファイルとなり、ファイルの場合はファイル名もここで指定する。
また出力時のフォーマットは前述したformatterのから選択する。

logger

ログ出力を行うオブジェクトの設定。
どのhandlerを使用するかやログレベルを指定する。ここで設定したオブジェクトをgetLogger(logger名)でインスタンス化して、実際にログ出力を行う。
ここの設定でpropagate: Trueとするとログ出力が親のロガーにも伝搬する。

loggerの名前空間

loggerには名前空間を持つことができ、getLogger()でインスタンス化しようとした際、対象のloggerがなかった場合にはより上位のloggerを取得する。

getLogger("aaa.bbb.ccc")とした際に、"aaa.bbb.ccc"loggerが定義されていない場合は、"aaa.bbb"logger→"aaa"logger→"root"loggerの順に取得を試みる。

Django定義済みのロガー
logger名 役割
django Djangoの内部処理ログ
django.server サーバーアクセスログ

settings.py

app/settings.py
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "filters": {
        "require_debug_false": {
            "()": "django.utils.log.RequireDebugFalse",
        },
        "require_debug_true": {
            "()": "django.utils.log.RequireDebugTrue",
        },
    },
    "formatters": {
        "django.server": {
            "()": "django.utils.log.ServerFormatter",
            "format": "[%(server_time)s] %(message)s",
        },
        "debug_formatter": {
            "format": "[%(levelname)s]:%(asctime)s [%(pathname)s:%(lineno)d] %(message)s",
        },
        "default": {
            "format": "[%(levelname)s]:%(asctime)s %(message)s",
        },
    },
    "handlers": {
        "console": {
            "level": "DEBUG",
            "class": "logging.StreamHandler",
            "formatter": "debug_formatter",
        },
        "file": {
            "level": "DEBUG",
            "class": "logging.handlers.TimedRotatingFileHandler",
            "filename": "./logs/debug.log",
            "when": "D",
            "interval": 1,
            "backupCount": 7,
            "formatter": "debug_formatter",
        },
        "django": {
            "level": "INFO",
            "class": "logging.StreamHandler",
            "formatter": "default",
        },
        "django.server": {
            "level": "INFO",
            "class": "logging.StreamHandler",
            "formatter": "django.server",
        },
    },
    "loggers": {
        "batch": {
            "handlers": ["console", "file"],
            "level": "DEBUG",
            "propagate": False,
        },
        "django": {
            "handlers": ["django"],
            "level": "INFO",
            "propagate": False,
        },
        "django.server": {
            "handlers": ["django.server"],
            "level": "INFO",
            "propagate": False,
        },
    },
}