ソニックの部屋

主にプログラミングに関する記事を投稿します

Djangまとめ

Django基礎

Udemy(Django3.0)

フレームワークとは

  • ウェブサイトを効率的に作るためのもの

Hello worldアプリ

  • プロジェクトを作る
django-admin startproject helloworldproject . ←.を付けることでフォルダを一つ省略する
  • ローカルサーバの立ち上げ
python manage.py runserver
  • settings.py

    • DEBUG=Falseで本番環境となる
    • ALLOWED_HOSTS=[]で本番環境の際に拒否するホストの設定ができる
    • INSTALLED_APPSはDjango内のアプリ
    • MIDDLEWAREはDjangoのセキュリティの設定
    • TEMPLATESのDIRSはHTMLファイルの保存先
  • urls.py

    • ルーティング
    • 上から順番にパスを拾っていく性質
    • オブジェクト(データ)を受け取る
    • views.pyのクラス又は関数を呼び出す
    • classを呼び出す場合は.as_view()を付ける(オブジェクトを作成して関数に変えるイメージ)
urlpatterns = [
  path('helloworld2/', HelloWorldClass.as_view())
]
  • views.py

    • コントローラー
    • function based view(古いキッチン)
    • class based view(新しいキッチン)
      • template_name=xxでビューのhtmlファイルを指定
  • アプリ

    • urls.py, views.py, models.pyの固まり
    • Djangoはプロジェクト(settings.py, urls.py)とアプリに大きく分類できる
# ①アプリの作成
python manage.py startapp helloworldapp(=アプリ名)

# ②Djangoのプロジェクトに対してアプリを作成したことを伝える
# settings.py
INSTALLED_APPS = [
    ...
    'helloworldapp.apps.HelloworldappConfig', # 一般的な書き方
]

# ③アプリとプロジェクトの繋ぎ
# urls.py
from django.urls import path, include

urlpatterns = [
    path('', include('helloworldapp.urls')), # helloworldapp/だと重複となりエラーとなるので注意
]
  • プロジェクトは統括しアプリに指示を出すもの
  • プロジェクトは全体の設定、アプリは商品のアプリ、支払いのアプリ等のイメージ
  • urls.pyはデフォルトではアプリに入っていないがアプリにも作るのが一般的

Todoアプリ

  • models.py
# TodoModelテーブル
class TodoModel(models.Model):
    title = models.CharField(max_length=100) # フィールドの設定
    memo = models.TextField()
  • migrateの実行
# ①設計図の作成
python manage.py makemigrations (アプリ名) # (アプリ名)は省略可
# ②テーブルの作成
python manage.py migrate
  • 管理画面
    • superuserの作成
python manage.py createsuperuser
# admin.py
# ①管理画面にデータの追加
from .models import TodoModel

admin.site.register(TodoModel)

# models.py
# ②データ名の変更
def __str__(self):
  # オブジェクトを作成した際に文字列を返す
  return self.title
  • CRUD

    • Create: CreateView
    • Read: ListView, DetailView
    • Update: UpdateView
    • Delete: DeleteView
  • ListVeiw

    • 一覧表示
# views.py
from django.views.generic.list import ListView
from .models import TodoModel

class TodoList(ListView):
    template_name = 'list.html'
    model = TodoModel
# list.html
<!-- object_listは指定したmodel(TodoModel)の全てのデータ -->
{% for post in object_list %}
<ul>
  <li>{{ post.title }}</li>
  <li>{{ post.memo }}</li>
</ul>
{% endfor %}
  • DetailView
    • 個別表示
# urls.py
urlpatterns = [
    path('list/', TodoList.as_view()),
    path('detail/<int:pk>', TodoDetail.as_view()), # <int:pk>がポイント
]
# detail.html
{{ object.title }}
  • 共通のビューはbase.htmlに集約する
    • 箱(block header等)を作って各々のビューで箱の中に記述していく
# base.html
{% block header %}
{% endblock %}
# list.html
{% extends 'base.html' %} ←base.htmlを読み込み
{% block header %}
this is list.
{% endblock %}
  • 中身の優先度に応じて色を替える
    • ①classにフィールド名を記述する
# list.html
{% block content %}
<div class="container">
  {% for item in object_list %}
  <div class="alert alert-{{ item.priority }}" role="alert"> ←{{ item.priority }}がポイント
    <p>{{ item.title }}</p>
    <a href="#" class="btn btn-info" tabindex="-1" role="button" aria-disabled="true">編集画面へ</a>
    <a href="#" class="btn btn-success" tabindex="-1" role="button" aria-disabled="true">削除画面へ</a>
    <a href="#" class="btn btn-primary" tabindex="-1" role="button" aria-disabled="true">詳細画面へ</a>
  </div>
  {% endfor %}
</div>
{% endblock %}
  • ②モデルの中でユーザーが選択できるフィールドを追加する
# 右のデータは管理画面の表示名、左のデータはBootstrapより
CHOICE = (('danger', 'high'), ('warning', 'normal'), ('primary', 'low'))

# TodoModelテーブル
class TodoModel(models.Model):

    priority = models.CharField(
        max_length=50,
        choices=CHOICE, # 管理画面上で選択肢を与える
        )
    duedate = models.DateField() # 時間の表示(本件には関係ない) 
  • マイグレーションファイルの作成とマイグレートによるテーブルの作成

    • 何らかのデータを入れる警告が出る(nullはNG)→timezone.now, dangerを入力
  • CreateView

    • ①ルーティングの設定
# urls.py
from .views import TodoList, TodoDetail, TodoCreate

urlpatterns = [
    path('list/', TodoList.as_view(), name='list'),  # nameを記述することにより画面遷移が可能となる
    path('detail/<int:pk>', TodoDetail.as_view(), name='detail'),
    path('create/', TodoCreate.as_view(), name='create'),
]
  • ②コントローラーの設定
    • reverseは名前からビューを呼び出す
# views.py
from django.views.generic import ListView, DetailView, CreateView
from django.urls import reverse_lazy

class TodoCreate(CreateView):
    template_name = 'create.html'
    model = TodoModel
    fields = ('title', 'memo', 'priority', 'duedate') # フォームの設定
    success_url = reverse_lazy('list') # データ登録後listに画面遷移
  • ③ビューの設定
# create.html
{% extends 'base.html' %}

{% block content %}
<form action="" method="POST">{% csrf_token %} # {% csrf_token %}はセキュリティ対策となりここに書くのが一般的
{{ form.as_p}} # フォームの設定(詳細はviews.pyで設定)
<input type="submit" value="create">
</form>
{% endblock %}
  • DeleteView, UpdateView

    • CreateViewを基本とする 
  • urlタグの設定

    • reverseと同じイメージ
# list.html
{% block content %}
<div class="container">
  {% for item in object_list %}
  <div class="alert alert-{{ item.priority }}" role="alert">
    <p>{{ item.title }}</p>
    # {% url 'update' item.pk %}がポイントとなりupdate画面へ遷移する
    <a href="{% url 'update' item.pk %}" class="btn btn-info" tabindex="-1" role="button" aria-disabled="true">編集画面へ</a>
  </div>
  {% endfor %}
</div>
{% endblock %}

社内SNSアプリ

  • render
# views.py
from django.shortcuts import render

def signupfunc(request):
    # httpresponseオブジェクトの作成
    # {}はモデルの情報
    return render(request, 'signup.html', {})
  • ユーザーの作成と取得→Userモデルを使う
    • djangoのデフォルトのUserモデルを使う方法
# views.py
# ①ユーザーモデルのインポートとユーザーの取得
from django.contrib.auth.models import User

def signupfunc(request):
    object = User.objects.get(username='s') # User.objects.all()で全てのユーザーを取得
# ②migrateの実行
# models.pyを修正していないためmigrationfileの作成は不要
python manage.py migrate
# ③ユーザーの作成
python manage.py createsuperuser
  • フォームからのユーザーの作成→POSTを使う
# signup.html
# ①フォームをPOSTにする
{% extends 'base.html' %}

{% block content %}
<body class="text-center">
    
  <main class="form-signin">
    <!-- 書く場所注意 -->
    <form method="POST">{% csrf_token %}
      <h1 class="h3 mb-3 fw-normal">Please sign in</h1>
      <div class="form-floating">
        <input type="text" class="form-control" id="floatingInput" placeholder="username" name="username">
        <label for="floatingInput">username</label>
      </div>
      <div class="form-floating">
        <input type="password" class="form-control" id="floatingPassword" placeholder="Password" name="password">
        <label for="floatingPassword">Password</label>
      </div>
      <button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
      <p class="mt-5 mb-3 text-muted">&copy; 2017–2023</p>
    </form>
  </main>
</body>
{% endblock %}
# views.py
# ②ユーザーの作成
# request.POSTでフォームからのデータを取得するのがポイント
def signupfunc(request):
    # userの作成
    if request.method == "POST":
        username = request.POST["username"]
        password = request.POST["password"]
        user = User.objects.create_user(username, "", password)
  • 登録データの重複を防ぐ→try exceptを使う
# views.py
from django.db import IntegrityError

def signupfunc(request):
    if request.method == "POST":
        username = request.POST["username"]
        password = request.POST["password"]
        try:
            user = User.objects.create_user(username, "", password)
        except IntegrityError:
            return render(request, 'signup.html', {"error": "このユーザーは既に登録されています"})
  • ログイン機能→authenticate, loginを使う
# ①signupと同じような画面を作成する
# login.html
# ②authenticateで認証
# views
from django.contrib.auth import authenticate, login

def loginfunc(request):
    if request.method == "POST":
        username = request.POST["username"]
        password = request.POST["password"]
        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)
            return render(request, 'login.html', {"context": "logged in"})
        else:
            return render(request, 'login.html', {"context": "not logged in"})
    return render(request, 'login.html', {"context": "get method"})
  • renderとredirect

    • renderは違うveiwsを呼び出さない(データを組み合わせてレスポンスを返す)
    • redirectは違うveiwsを呼び出す
    • 使い分けは複数のcontextを使う場合はrender、処理が終わり違う所に移す場合はredirectを使う
  • modelsの記載例

# models.py
from django.db import models

class BoardModel(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    author = models.CharField(max_length=50)
    # settings.pyで設定されたデフォルトの場所に保存するならブランクでよい
    sns_image = models.ImageField(upload_to="")
    good = models.IntegerField()
    read = models.IntegerField()
    readtext = models.TextField()
  • viewsの記載例
# views.py
from .models import BoardModel

def listfunc(request):
    object_list = BoardModel.objects.all()
    # キーはバリューと同じ名前が一般的
    # list.htmlに{}書きでモデルのデータを渡す
    return render(request, "list.html", {"object_list": object_list})
  • list.htmlの記載例
# list.html
{% block content %}
<div class="container">
  {% for item in object_list %} # object_listでviewsより受け取る
  <div class="alert alert-success" role="alert">
    <p>タイトル:{{ item.title }}</p> # {{}}でデータを扱う
    <p>投稿者:{{ item.author }}</p>
    <a href="#" class="btn btn-primary" tabindex="-1" role="button" aria-disabled="true">Primary link</a>
    <a href="#" class="btn btn-secondary" tabindex="-1" role="button" aria-disabled="true">Link</a>
  </div>
  {% endfor %} # ここはendfor
</div>
{% endblock %}
  • imageファイル
    • 開発時の取り扱い
    • ユーザーがアップロードした画像→MEDIAを使う
# settings.py
# 画像のURLとなり最後スラッシュを入れる
MEDIA_URL = "medi/"

# 画像の保存先(開発環境用)
MEDIA_ROOT = BASE_DIR / "media"
# (プロジェクトの)urls.py
from django.conf.urls.static import static

urlpatterns = [
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
  • モデルにイメージフィールドを追加しpillowをインストールする
# models.py
class ItemModel(models.Model):
    ...
    item_image = models.ImageField(upload_to="")
pip install pillow
  • 画像の表示方法
# list.html
<img class="card-img-top" src="{{ item.item_image.url }}" alt="{{ item.name }}" />
  • cssファイル
    • 開発者が扱う画像やCSSファイル→STATICを使う
# 画像のURLとなり最後スラッシュを入れる
STATIC_URL = 'static/'

# 画像の保存先(本番環境用となり本番環境はSTATICFILES_DIRSからここに一つにまとめる)
STATIC_ROOT = BASE_DIR / "staticfiles"

# 画像の保存先(開発環境用となり複数の保存先を指定可)
STATICFILES_DIRS = [str(BASE_DIR / "static")]
from django.conf.urls.static import static

urlpatterns = [
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \
  + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
  • cssファイルをstaticフォルダに保存して読み込ませる
# base.html
<head>
    {% block customcss %}
    {% endblock %}
</head>
# signup.html
{% extends 'base.html' %}
<!-- staticfilesdirsの場所を読み込む -->
{% load static %}

{% block customcss %}
<link href="{% static 'style.css' %}" rel="stylesheet"> # staticフォルダのcssを読み込む
{% endblock %}
  • ログイン状態の判定
    • @login_requiredを使う方法
# settings.py
# ulrs.pyのname=loginを参照する(reverseのイメージ)
LOGIN_URL = "login"
# views.py
# 関数呼び出し前に処理を加える(リストの表示前にログイン画面に飛ばす)
from django.contrib.auth.decorators import login_required

@login_required
def listfunc(request):
    xxx
  • テンプレートの中にif文を書く方法
# list.html
{% if user.authenticated %}
  # タイトルや投稿者
{% else %}
please login
{% endif %}
  • ログアウト機能の実装
# ulrs.py
from .views import signupfunc, loginfunc, listfunc, logoutfunc

urlpatterns = [
    path('logout/', logoutfunc, name="logout"),
]
# views.py
from django.contrib.auth import authenticate, login, logout

def logoutfunc(request):
    logout(request)
    return redirect("login")
# list.html
...
  {% endfor %}
  <a href="{% url 'logout' %}">logout</a>
</div>
  • DetailView
# urls.py
from .views import signupfunc, loginfunc, listfunc, logoutfunc, detailfunc

urlpatterns = [
    path('detail/<int:pk>', detailfunc, name="detail"),
]
# views.py
from django.shortcuts import render, redirect, get_object_or_404

# 引数pkがポイント
def detailfunc(request, pk):
    # object = BoardModel.object.get(pk=pk)
    # 上記と同じくデータを取得しかつオブジェクトがなければ例外を返す
    object = get_object_or_404(BoardModel, pk=pk)
    return render(request, "detail.html", {"object": object})
# detail.html
...
    <p>タイトル:{{ object.title }}</p>
    <p>投稿者:{{ object.author }}</p>
    # 画像の表示方法
    <p><img src="{{ object.snsimage.url }}" width=300></p>
...
  • リストから詳細への飛び方
# list.html
# item.pkを渡すのがポイント
...
    <p>タイトル:<a href="{% url 'detail' item.pk %}">{{ item.title }}</a></p>
...
  • いいね機能
# urls.py
path('good/<int:pk>', goodfunc, name="good"),
# views.py
def goodfunc(request, pk):
    object = get_object_or_404(BoardModel, pk=pk)
    object.good = object.good + 1
    object.save() # ポイント
    return redirect("list")
# detail.html
<a href="{% url 'good' object.pk %}" class="btn btn-primary" tabindex="-1" role="button" aria-disabled="true">いいねする</a>

# list.html
# disabledでクリック出来ないようにする(好み)
<a href="#" class="btn btn-primary disabled" tabindex="-1" role="button" aria-disabled="true">いいね{{ item.good }}件</a>
  • 既読機能
    • 本番環境ではユーザー名の実装が弱いためNGの実装(理解のための実装)
# urls.py
from .views import signupfunc, loginfunc, listfunc, logoutfunc, detailfunc, goodfunc, readfunc

urlpatterns = [
    path('read/<int:pk>', readfunc, name="read"),
]
# views.py
def readfunc(request, pk):
    object = get_object_or_404(BoardModel, pk=pk)
    username = request.user.get_username()
    if username in object.readtext:
        return redirect("list")
    else:
        # 既読回数を増やす
        object.read = object.read + 1
        # 既読ユーザーを入れる
        object.readtext = object.readtext + ' ' + username
        object.save()
        return redirect("list")
# detail.html
<a href="{% url 'read' object.pk %}" class="btn btn-secondary" tabindex="-1" role="button" aria-disabled="true">既読にする</a>
  • CreateView→classbasedviewを使う→ファイルの扱いが容易なため
# urls.py
from .views import signupfunc, loginfunc, listfunc, logoutfunc, detailfunc, goodfunc, readfunc, \
                   BoardCreate

urlpatterns = [
    path('create/', BoardCreate.as_view(), name="create")
]
# views.py
from django.views.generic import CreateView
from django.urls import reverse_lazy

class BoardCreate(CreateView):
    template_name = "create.html"
    model = BoardModel
    fields = ("title", "content", "author", "snsimage")
    success_url = reverse_lazy("list")
  • @login_requiredはクラスでは指定不可→テンプレートの中にif文を書く
# create.html
{% extends "base.html" %}
{% block content %}
{% if user.is_authenticated %}
<!-- fileを扱う場合はenctypeの指定かつ複数のデータを扱う場合はmultipartを使う -->
<form method="POST" enctype="multipart/form-data">{% csrf_token %}
  <!-- {{ form.as_p }} -->
  <p>title:<input type="text" name="title"></p>
  <p>content:<input type="text" name="content"></p>
  <p><input type="file" name="snsimage"></p>
  <!-- フロント側は隠す -->
  <input type="hidden" name="author" value="{{ user.username }}">
  <input type="submit" value="create">
</form>
{% else %}
please login
{% endif %}
{% endblock %}
# models.py
from django.db import models
# 画像をアップできるようにバリデーションを外すのがポイント
class BoardModel(models.Model):
    # nullとblankを許容する(セットで設定:nullはDB関係、blanckはform関係)
    good = models.IntegerField(null=True, blank=True, default=1)
    read = models.IntegerField(null=True, blank=True, default=1)
    readtext = models.TextField(null=True, blank=True, default="a")

公式チュートリアル(Django4.0)

Udemyとの差分を主に記述する

  • テーブル間のリレーションシップの貼り方
# models.py
class Question(models.Model):
    question_text = models.CharField(max_length=200)

class Choice(models.Model):
    # リレーションシップの定義(各々のChoiceが一つのQuestionに関連付けられる)
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
# <テーブル1><テーブル2>_set.allでテーブル1に紐付くテーブル2のデータを全て取得する
question.choice_set.all
  • オブジェクトをソートして取得
# views.py
def index(request):
    # オブジェクトをソートして取得
    latest_question_list = Question.objects.order_by("-pub_date")[:5]
# 名前空間により他のアプリと区別する
app_name = "polls"
urlpatterns = [
    path("index/", views.index, name="index"),
# index.html
{% for question in latest_question_list %}
    <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
{% endfor %}
  • index, detailなどの汎用化できるviewsはclassbasedviewを使う
  • テストはアプリフォルダの中のtests.pyに記述していく
    • テストは冗長になってもOK
  • 静的ファイル(画像、JavaScript, CSS等)はstaticフォルダに入れる
  • adminのUIを変更にするにはadmin.pyから
  • Django Debug Toolbar
python -m pip install django-debug-toolbar

Django応用

Heroku

heroku apps:create ecapp
git push heroku xxxx:main
# 強制プッシュ
git push heroku xxxx:main --force
  • アプリ消去時に再設定が必要な項目
    • Heroku上の環境変数の設定
    • GithubのOAuthに関するHerokuへのリダイレクトURLの再設定が必要
    • settings.pyのCLOUD_NAMEの設定
    CLOUDINARY_STORAGE = {
        'CLOUD_NAME': 'drsbhpxje', # アプリ消去時の再設定忘れない
        'API_KEY': env('CLOUDINARY_API_KEY'),
        'API_SECRET': env('CLOUDINARY_API_SECRET')
    }
  • DBリセット
heroku pg:reset DATABASE_URL
// heroku run python manage.py makemigrations twapp
heroku run python manage.py migrate -a twapp
  • superuserの作成
heroku run python manage.py createsuperuser

flake8

docker compose exec web flake8 --max-line-length 400

応用

# views.py
class ItemList(ListView):
    template_name = 'list.html'
    model = ItemModel
    # メソッドをオーバーライドし商品をid順に取得する
    def get_queryset(self):
        return ItemModel.objects.all().order_by('id')

class ItemDetail(DetailView):
    template_name = 'detail.html'
    model = ItemModel

    # メソッドをオーバーライドしURLから取得されるオブジェクトに加えて最新のDBデータを取得する
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['latest_item'] = ItemModel.objects.latest('id')
# views.py
def add_to_cart_from_list_func(request, pk):
    # セッションのカートを取得、なければ新しく作成
    cart = request.session.get('cart', [])
    cart.append(pk)
    # カートをセッションに保存
    request.session['cart'] = cart
    return redirect("list")
  • Djangoのテンプレートでは、変数の宣言や算術演算を直接行うことはできない。代わりに、ビューで必要な計算を行い、計算された結果をテンプレートに渡すのが一般的な方法
  • フラッシュメッセージ
  • 中間テーブル
    • カートテーブルと商品テーブルの間には多:多の関係があるため中間テーブルを設けて1:1の関係にする
    • cart_idをsessionに保存するようにする
    • 外部キーは各オブジェクトを設定、cart_itemsを各テーブルから中間テーブルへのアクセスポイントとする 
# models.py
class CartItemModel(models.Model):
    # 中間テーブルの作成
    cart = models.ForeignKey(CartModel, on_delete=models.CASCADE, related_name='cart_items')
    item = models.ForeignKey(ItemModel, on_delete=models.CASCADE, related_name='item_carts')
  • 各オブジェクトを用いて中間オブジェクトを作成、cart_itemsを用いて中間テーブルへアクセス
# views.py
# 中間オブジェクトを作成
cart_item_object= CartItemModel.objects.create(cart=cart_object, item=item_object)
cart_item_object.save()

# カートアイテムの数を取得
cart_item_count = cart_object.cart_items.all().count()
  • item.priceで中間テーブルにリンクされているリンク先のテーブルデータにアクセス
# カートアイテムを取得
cart_items = cart_object.cart_items.all()

# カートの商品数と合計金額を算定
    for cart_item in cart_items:
        total_price += cart_item.item.price
  • item__idで中間テーブルにリンクされているリンク先のテーブルデータにアクセス
# カートアイテムからpkに一致する最初のデータを削除
    cart_object.cart_items.filter(item__id=pk).first().delete()
  • DBデータの削除時はsaveは不要
# カートアイテムの削除
cart_object.cart_items.filter(item__id=pk).delete()
# cart_object.save() # 不要
  • Basic認証
  • メール送信
    • Gmailを使用
    • パスワードはGmailパスワードではなく アプリ固有のパスワードとすること
    • Code for Django
  • カスタムコマンド
  • bulk_create, bulk_updateで効率よくオブジェクトを作成・更新する
  • カスタムユーザー作成時のmigrateエラー
  • ソーシャル連携
    • GitHubのOAuth設定のherokuについてはhttps://とsを忘れない(ローカル上はhttpでよい)
  • ページネーション
    • デフォルトのPaginatorを使う
    • 公式
  • クエリパラメータについて
    • ChatGPTより:{% url %} タグでクエリパラメータを指定する場合、それはURLのパス部分ではないため、urls.pyで<int:id> のようなパスパラメータを指定する必要はありません。クエリパラメータはビュー内で request.GET からアクセスでき、URLのパス自体には影響を与えません。
  • テンプレートでのクエリパラメーターの渡し方
{% url 'twapp:tweet_good' tweet.id %}?param={{ param_value }}
  • ビューでのクエリパラメータの受け取り方
  • URLの生成はreverseを使う、idはkwargsで渡す
# クエリパラメータの受け取り
param_value = request.GET["param"]
if param_value in ["all", "follow"]:
  # URLのパス部分を生成
  url = reverse('twapp:tweet_list')
else:
  url = reverse('twapp:tweet_list_id', kwargs={'id': tweetmodel.customusermodel_id})
# クエリパラメータを追加
redirect_url = f'{url}?param={param_value}'

DBリセット

docker compose exec web python manage.py showmigrations
docker compose exec web python manage.py migrate twapp 0001_initial←戻したいファイル名
docker compose down
docker volume rm django_tw_db-data  
  • 3.makemigrationsの実行
  • 4.migrateの事項

テスト

参考文献
基礎
・株式会社CODOR (大橋亮太), 【徹底的に解説!】Djangoの基礎をマスターして、3つのアプリを作ろう!(Django2版 / 3版を同時公開中です)
公式チュートリアル
pyhaya’s diary
応用
・上記の各リンク参照