この記事では、Djangoのプロジェクト、アプリを作成しているという前提のもと、アプリ開発における逆引きを紹介する。
- プロジェクトの作成は以下の記事を参考にする
ルーティングの設定
includeを追加する
以後、プロジェクト.urls.pyを触ることわない
from django.contrib import admin
from django.urls import path, include #include追加
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('アプリ名.urls')), # 追加
]
プロジェクト.urls.pyの中身をコピペする
ここに、実行していくルーティングを書いていく
from django.urls import path
from .views import Index
app_name = 'アプリ名'
urlpatterns = [
path('', Index.as_view(), name='index'),
]
開発サーバの停止
新たにディレクトリを追加した場合
Quit the server with CONTROL-C.
- tmplatesなど、新たにdirを作成した場合など
- 開発サーバーを再起動する必要がある(再起動:
control
+c
)
- 開発サーバーを再起動する必要がある(再起動:
マイグレーション
モデルを新規作成、変更した場合
$ python3 manage.py makemigrations
$ python3 manage.py migrate
- モデルを作成、変更した場合は、上記のコマンドを入力してマイグレーションを行う必要がある必要がある
-
python3 manage.py makemigrations
:マイグレーションファイルにモデルの作成、変更内容を書き込む -
python3 manage.py migrate
:マイグレーションファイルを元にマイグレーションを行う。これによって、モデルをDB内のテーブルとして扱い、設定内容を反映する
Git管理
一旦pushしたい場合
$ git add .
$ git commit -m "コミットメッセージ"
$ git status
$ git push origin master
- manage.pyを含むdirに移動してから上記のコマンドを入力
モデル
抽象基底クラスでモデル定義の効率化
from django.db import models
from model_utils.models import TimeStampedModel
class Post(TimeStampedModel):
# created = models.DateTimeField(auto_now_add=True) 新規作成
# modified = models.DateTimeField(auto_now=True) 更新
title = models.CharField(max_length=255)
body = models.TextField()
def __str__(self):
return self.title
- TimeStampedModelクラス:他のクラスで共通的に利用するcreated(作成時刻)とmodified(更新時刻)のフィールドが定義されている
- Metaクラス以降のabstract = Trueが定義されているため、TimeStampedModelクラスは抽象基底クラスとなる
- from model_utils.models import TimeStampedModelをインポートすることで、TimeStampedModelをそのまま使用することができる
class TimeStampedModel(models.Model):
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
作成したモデルをadminサイトに表示
from django.contrib import admin
from .models import Post
admin.site.register(Post) # 作成したPostモデルを登録
ログイン・ログアウト機能の実装
アプリ作成(ログイン・ログアウト) ※任意
$ python3 manage.py startapp accounts
ルーティング
- accounts/urls.pyを作成
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('report.urls')),
path('accounts/', include('accounts.urls')), # 追加
]
from django.urls import path
from .views import Login, Logout
app_name = 'accounts'
urlpatterns = [
path('login/', Login.as_view(), name='login'),
path('logout/', Logout.as_view(), name='logout'),
]
settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'アプリ名',
'acounts', # 追加
]
LOGIN_URL = 'accounts:login'
LOGIN_REDIRECT_URL = '/' # TOPページへリダイレクト
LOGOUT_REDIRECT_URL= 'accounts:login'
環境変数 | 説明 |
---|---|
LOGIN_URL | ログイン必須のページにログインしていないユーザがアクセスした場合のリダイレクト先URL、ルーティング名を指定 |
LOGIN_REDIRECT_URL | ログイン後のリダイレクト先URL、ルーティング名を指定 |
LOGOUT_REDIRECT_URL | ログアウト後のリダイレクト先URL、ルーティング名を指定 |
ユーザ認証フォームの作成(ログインフォーム)
- accounts/forms.pyを作成
- Django標準のAuthenticationFormクラスを継承して、ログインフォームを実装
from django.contrib.auth.forms import AuthenticationForm
class LoginForm(AuthenticationForm):
"""ログオンフォーム"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs['class'] = 'form-control' # ①
field.widget.attrs['placeholder'] = field.label # ②
※①:全てのフォームの部品のclass属性に「form-control」を指定(bootstrapのフォームデザインを利用するため)
※②:全てのフォームの部品にpaceholderを定義して、入力フォームにフォーム名が表示されるように指定。
ログイン・ログアウトViewの実装
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import LoginView, LogoutView
from .forms import LoginForm
class Login(LoginView):
"""ログインページ"""
form_class = LoginForm
template_name = 'accounts/login.html'
class Logout(LoginRequiredMixin, LogoutView):
"""ログアウトページ"""
template_name = 'accounts/login.html'
# ログアウト後は、LOGOUT_REDIRECT_URL = 'products:login'にリダイレクトするため、ログアウト用のHTMLは空でもいい
ユーザ認証用ビュークラス | 説明 |
---|---|
LoginRequiredMixin | ログイン済みユーザのみに制限してレスポンスを返す |
LoginView | ログイン機能 |
LogoutView | ログアウト機能 |
form_classに先ほどforms.pyで定義したフォームクラス「LoginForm」を指定することでログイン処理時にLoginFormで定義したフォームデザインが利用されるようになります。
複数のクラスを承継する場合は、LoginRequiredMixinを一番最初に指定するようにしましょう。
一番最初に指定しないとうまく動作しなくなりますので注意しましょう。
- Logoutのtemplate_nameに指定するhtmlファイルは空でも可。template_nameを指定しないと、Django管理サイトのものが使用される。
テンプレート
- accounts/templates/accounts/を作成
- accounts/templates/accounts/login.htmlを作成
※詳細は、GitHubを参照
<div class="card-body">
<form action="" method="POST">{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
{{ field }}
{{ field.errors }}
<br>
{% endfor %}
<button type="submit" class="btn btn-primary" >ログイン</button>
<input type="hidden" name="next" value="{{ next }}" />
</form>
</div>
- フィールドの表示:
- for文によって、フォームオブジェクト(form)からフォームクラスのフィールド(入力値)を1つずつ取り出して
{{field}}
で表示している。
- for文によって、フォームオブジェクト(form)からフォームクラスのフィールド(入力値)を1つずつ取り出して
- フィールドのエラー表示:
- 認証系のエラーは
form.non_field_errors
に情報が格納されるため、for文で{{ field.errors }}
としてテンプレートに表示できる。
- 認証系のエラーは
- POSTボタンの作成(ログイン用ボタン):
- フォームオブジェクトを表示するだけでは、POST用のボタンは作成されないため、ボタンを作成する。
ログイン中のユーザ情報を表示
userオブジェクトはリクエスト(request)オブジェクト内に格納されている。
ビューのget_context_data()によって、これらはテンプレートに渡されるため、以下のようにuser変数に対して、ユーザモデルのフィールドを指定することで、ユーザに関する情報をテンプレートに表示することができる。
- user.is_authenticated ・・・ユーザが認証済みの場合Trueを返す。
- user.id・・・認証済みユーザのID番号
- user.username・・・認証済みユーザのユーザ名 (user.get_usernameも同じ)
サインアップ機能の実装
- カスタムユーザモデルを作成しておく
- ルーティング設定
- サインアップ用の汎用ビューがないのでCreateViewで作成
- サインアップ用のフォームクラス(UserCreationForm)を用いてサインアップフォームを作成
- サインアップ、サインアップ後のテンプレートを作成
ルーティング設定
from .views import Login, Logout, Signup, SignupDone # 一部追加
urlpatterns = [
# 省略
path('signup/', Signup.as_view(), name='signup'), # サインアップ
path('signup_done/', SignupDone.as_view(), name='signup_success'), # サインアップ完了
]
ビューの作成
- サインアップ用の汎用ビューがないのでCreateViewで作成
#signup
from django.views.generic import TemplateView
from django.views.generic.edit import CreateView
from .forms import LoginForm, SignupForm
from django.shortcuts import redirect
from django.urls import reverse_lazy
'''サインアップ'''
class Signup(CreateView):
template_name = 'accounts/signup.html'
form_class =SignupForm
success_url = reverse_lazy('accounts:signup_success')
# templateに表示するデータを追加するためオーバーライド
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["process_name"] = "Sign up"
return context
'''定義されているためオーバーライド不要
def form_valid(self, form):
user = form.save() # formの情報を保存
self.object = user
return super().form_valid(form)
'''
'''サインアップ完了'''
class SignupDone(TemplateView):
template_name = 'accounts/signup_success.html'
フォームの作成
- サインアップ用のフォームクラス(UserCreationForm)を用いてサインアップフォームを作成
- UserCreationFormフォームクラスには、パスワードとパスワード確認用のフィールドが定義されている
- Metaクラス内のfieldsで指定したCustomUserモデルクラスのフィールドとは別に、UserCreationFormのフィールドもフォームに表示される
- フィールドの表示優先度は、フォームフィールド<モデルフィールド
#signup
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from .models import CustomUser
'''サインアップ用フォーム'''
class SignupForm(UserCreationForm):
'''UserCreationFormのフィールド'''
# CustomUserモデルのフィールドとは別でフォームに表示される
# password1 = forms.~
# password2 = forms.~ (確認用)
class Meta:
model = CustomUser
fields = ('username', 'email')
サインアップ、サインアップ後のテンプレートを作成
- base.htmlのナビゲーションバーにsignupを追加
- accounts/templates/signup.htmlを作成
- accounts/templates/signup_success.htmlを作成
※詳細は、GitHubを参照
マイページの作成、変更(アクセス制限)
他のユーザはアクセスできず、自分のみアクセスできるサイト(マイページ)を作成する。
また、マイページから自分のユーザ情報を変更する機能も追加する。
- カスタムユーザモデルを作成しておく
- ルーティング設定
- 独自の制限を設定したマイページビューとマイページ更新ビューの作成
- マイページ更新用のフォームを作成
- マイページ、マイページ更新用のテンプレートを作成
ルーティング設定
from .views import Login, Logout, Signup, SignupDone, MyPage,UserUpdate # 一部追加
urlpatterns = [
# 省略
path('my_page/<int:pk>/', MyPage.as_view(), name='my_page'),
path('user_update/<int:pk>', UserUpdate.as_view(), name='user_update'), # 登録情報の更新
独自の制限を設定したマイページビューとマイページ更新ビューの作成
- 自分以外のユーザがアクセスできない用に、UserPassesTestMixinクラスを使って独自のアクセス制限を設定する。
# my_page
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import UserPassesTestMixin
from django.views.generic import DetailView
from .models import CustomUser
'''自分しかアクセスできないようにするMixin(共通化させるため)'''
# 詳細はQiita:https://qiita.com/kkk777/items/6f467a14b0f9616a0a79
class OnlyYouMixin(UserPassesTestMixin):
def test_func(self):
# 今ログインしてるユーザーのpkと、そのマイページのpkが同じならアクセスを許可
user = self.request.user
return user.pk == self.kwargs['pk']
# test_func関数がFalseの場合のリダイレクト先を指定
def handle_no_permission(self):
return redirect("report:index")
'''マイページ'''
class MyPage(OnlyYouMixin, DetailView):
model = CustomUser
template_name = 'accounts/my_page.html'
'''ユーザー登録情報の更新'''
class UserUpdate(OnlyYouMixin, UpdateView):
model = CustomUser
form_class = UserUpdateForm
template_name = 'accounts/signup.html'
def get_success_url(self):
return resolve_url('accounts:my_page', pk=self.kwargs['pk'])
# contextデータ作成
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["process_name"] = "Update"
return context
マイページ更新用のフォームを作成
- accounts/forms.pyに以下のコードを追加
'''ユーザー情報更新用フォーム'''
class UserUpdateForm(forms.ModelForm):
class Meta:
model = CustomUser
fields = ('username', 'email')
# bootstrap4対応
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs['class'] = 'form-control'
field.widget.attrs['required'] = '' # 全フィールドを入力必須
マイページ、マイページ更新用のテンプレートを作成
base.htmlのナビゲーションバーにmypageを追加
accounts/templates/my_page.htmlを作成
※詳細は、GitHubを参照
ユーザのパスワード変更機能
マイページから自分(ユーザ)のパスワードを変更できる機能を追加する。
- 上記のマイページ作成の続きから
- ルーティング設定
- ユーザパスワード変更機能と変更後の処理
- パスワード変更用のフォームを作成
- パスワード変更用、変更後のテンプレートを作成
ルーティング設定
from .views import Login, Logout, Signup, SignupDone, MyPage,UserUpdate, PasswordChange, PasswordChangeDone # 一部追加
urlpatterns = [
# 省略
# password
path('password_change/', PasswordChange.as_view(), name='password_change'), # パスワード変更
path('password_change_done/', PasswordChangeDone.as_view(), name='password_change_done'), # パスワード変更完了name='user_update'), # 登録情報の更新
ユーザパスワード変更機能と変更後の処理
- PasswordChangeViewクラスを継承してパスワード変更を処理を行うビューを定義
- get_context_data()をオーバーライドすることで、テンプレートに表示する値を追加
- PasswordChangeDoneViewクラスを継承してパスワード変更後の処理を行うビューを定義
- パスワード変更後にリダイレクトするテンプレートを指定する
# password
from .forms import LoginForm, SignupForm, UserUpdateForm, MyPasswordChangeForm
from django.contrib.auth.views import LoginView, LogoutView, PasswordChangeView, PasswordChangeDoneView
from django.urls import reverse_lazy
'''パスワード変更'''
class PasswordChange(PasswordChangeView):
form_class = MyPasswordChangeForm
success_url = reverse_lazy('accounts:password_change_done')
template_name = 'accounts/signup.html'
# contextデータ作成
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["process_name"] = "Change Password"
return context
'''パスワード変更完了'''
class PasswordChangeDone(PasswordChangeDoneView):
template_name = 'accounts/password_change_done.html'
パスワード変更用のフォームを作成
- accounts/forms.pyに以下のコードを追加
- パスワード変更用のフォームを作成するPasswordChangeForm クラスを継承してフォームクラスを定義する
'''パスワード変更フォーム'''
class MyPasswordChangeForm(PasswordChangeForm):
# bootstrap4対応で、classを指定
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs['class'] = 'form-control'
パスワード変更用、変更後のテンプレートを作成
accounts/templates/my_page.htmlにリンクを追加
accounts/templates/password_change_done.htmlを作成
※パスワード更新用のテンプレートは、サインインとユーザー登録情報の更新用のテンプレートと同じものを使用
※詳細は、GitHubを参照
デプロイ(本番環境)
表示するモデルのデータをユーザごとに切り替える
-
表示するモデルのデータをユーザごとに切り替える
- ログイン機能を実装しただけではデータベースから表示するデータが全ユーザーで共有されてしまう。
- 特定のユーザーが登録した情報だけを表示する処理を実装する。
- ListViewのget_queryset()をオーバーライドする。
ページネーションの作成
表示するデータ数の指定
- ListView内で、paginate_by変数をオーバーライドする
class Index(ListView):
template_name = 'report/index.html'
paginate_by = 2
- paginate_by変数には、1ページに表示するデータ数を指定する
ページネーションを作成
- ListViewのtemplate_nameに指定しているテンプレートの最後に以下のコードを追加する
<div class="row mx-auto" style="width: 600px;">
<ul class="pagination">
<!-- 前へ の部分 -->
{% if page_obj.has_previous %}
<li><a class="page-link text-primary d-inline-block" href="?page={{ page_obj.previous_page_number }}"><</a></li>
{% endif %}
<!-- 数字の部分 -->
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="disabled"><div class="page-link text-secondary d-inline-block disabled">{{ num }}</div></li>
{% else %}
<li><a class="page-link text-primary d-inline-block" href="?page={{ num }}">{{ num }}</a></li>
{% endif %}
{% endfor %}
<!-- 次へ の部分 -->
{% if page_obj.has_next %}
<li><a class="page-link text-primary d-inline-block" href="?page={{ page_obj.next_page_number }}">></a></li>
{% endif %}
<li class="p-md-2">
{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}を表示
</li>
</ul>
</div>