Django基礎
Udemy(Django3.0)
フレームワークとは
- ウェブサイトを効率的に作るためのもの
Hello worldアプリ
- プロジェクトを作る
django-admin startproject helloworldproject . ←.を付けることでフォルダを一つ省略する
- ローカルサーバの立ち上げ
python manage.py runserver
settings.py
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
- DBとDjangoの仲介役
# 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
-
- 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">© 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 }}" />
# 画像の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
- 各ファイルの作成
- Procfile
- gunicornのインストール
- runtime.txt
- DjangoアプリをHerokuにデプロイしました。
- Heroku Dev Center
- Heroku スターターガイド (Python)
- Procfile
- settings.pyの設定
- whitenoise
- デバッグ環境と本番環境
- python-getting-started
- アプリの作成(+名前付け)
heroku apps:create ecapp
- herokuの環境変数の設定
- Settings/Config VarsでSECRET_KEYとDATABASE_URLの設定が必要
- DATABASE_URLは必ずこの名前で設定が必要
- DATABASE_URLはResources/Add-onsでHeroku Postgresより発行
environの理解
- herokuの環境変数、.envファイルの順に読み込む
- Django python-environ を使って設定を分離する方法
デプロイ
git push heroku xxxx:main # 強制プッシュ git push heroku xxxx:main --force
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
- 画像の表示
- Cloudinaryを使う(無料)。S3も可能
- 【Django】herokuでCloudinaryを使い画像をアップロードする
- エラー画面の表示
- 所定のコードを追記
- Herokuの500エラーへの対応(Django)
flake8
docker compose exec web flake8 --max-line-length 400
応用
- classbasedviewのカスタマイズ
- メソッドのオーバーライドによりカスタマイズ
- 【Django入門14】クラスベースビューの基本
# 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のテンプレートでは、変数の宣言や算術演算を直接行うことはできない。代わりに、ビューで必要な計算を行い、計算された結果をテンプレートに渡すのが一般的な方法
- フラッシュメッセージ
- messagesフレームワークを使用
- フラッシュメッセージを実装してみる
- 中間テーブル
# 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
- カスタムコマンド
- management, commandsフォルダの中にカスタムファイルを作成する
- [Django]カスタムコマンドを作る
- bulk_create, bulk_updateで効率よくオブジェクトを作成・更新する
- カスタムユーザー作成時のmigrateエラー
- デフォルトのadminユーザのModelとカスタムユーザのModelが衝突してしまうために起こるエラー
- Migration admin.0001_initial is applied before its dependencyが出た時の対処法
- ソーシャル連携
- ページネーション
- デフォルトの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}'
- アップロードボタン
- request.FILES['image']がポイント
- Djangoを使って画像アップロード、閲覧を実装してみる
- ビューではrequest.userでログインユーザーのユーザーオブジェクトが取得可能
- モデルに列挙型choicesを設定することでフィールドの選択肢を柔軟に変更可能
DBリセット
- 方法①ロールバック
- 1.migrate履歴の確認
docker compose exec web python manage.py showmigrations
- 2.ロールバック
docker compose exec web python manage.py migrate twapp 0001_initial←戻したいファイル名
- 3.戻した以後のmigrationファイルを削除
- 4.migrateの実行
方法②リセット
- 1.migrationsフォルダから init.py を除く全てのマイグレーションファイルを削除
- 2.コンテナ削除後にDB削除
docker compose down docker volume rm django_tw_db-data
- 3.makemigrationsの実行
- 4.migrateの事項
テスト
- UIテストはseleniumを使用
- Selenium + Python環境をDockerで簡単に構築して自動ブラウンジング!
- Django, Docker, SeleniumのUIテストではまった所
- SeleniumサーバーからブラウザでDjangoアプリに接続する際、「http://"services名":3001/」と接続できるが、そうするとテスト環境のDBではなく開発環境のDBを使用してしまい、データ検索等のテストに支障が出る
- クラス変数にhost="services名"と定義し、StaticLiveServerTestCaseのlive_server_urlを使用すると、テスト環境のDBが使用できる
- Running django tests with selenium in docker
参考文献
基礎
・株式会社CODOR (大橋亮太), 【徹底的に解説!】Djangoの基礎をマスターして、3つのアプリを作ろう!(Django2版 / 3版を同時公開中です)
・公式チュートリアル
・pyhaya’s diary
応用
・上記の各リンク参照