【Django3.1】Djangoでsignup,login機能をgenericviewとdjango.contrib.authで最短で作成する。あとで見た目も。
なお本記事のソースコードはgithub上に公開しています。 login機能は公式documentを参考に作成していきます。 login確認の為に以下のコマンドで管理者ユーザを作成しておきます。 signup,login実装用のアプリケーションを作成しておきます。 まずベースとなる confug/urls.py accounts/urls.py このPathでレンダリングされる先はデフォルトでtemplateディレクトリ配下の デフォルトの通りregistration/login.htmlを以下のように作成します。 templates/registration/login.html たったこれだけです。 試しに先程作成した管理者ユーザでloginすると、以下のエラーになります。 実は ※一番下に追加しました。 この状態でもう一度、
http://127.0.0.1:8000/accounts/login/
にアクセスし、loginしてみましょう。 (例) loginユーザ名はどのページでも表示させたいので、一旦これをbase.htmlのbodyに入れてみます。 templates/base.html この状態で
http://127.0.0.1:8000/samplecrud/list/
にアクセスしてlogin前後を見てみましょう。 login前 無事表示出来ましたね! login出来ましたがlogoutする機能が有りません。 accounts/urls.py この状態で、
http://127.0.0.1:8000/accounts/logout/
にアクセスすると以下が表示されます。 confug/settings.py この状態で、
http://127.0.0.1:8000/accounts/logout/
にアクセスすると無事loginページにリダイレクトされました。 今の状態ではadmin site以外でユーザを作成するすべがないので、signup機能を実装します。 accounts/urls.py viewの設定 accounts/views.py レンダリング先の設定 templates/registration/signup.html 全くloginと同じでOKです。
この状態で
http://127.0.0.1:8000/accounts/signup/
にアクセスすると以下の用に表示されます。 signup,login機能は実装したものの、今の状態ではすべてのページが表示出来てしまいます。 具体的には、以下の と言っても制御は簡単で、 samplecrud/views.py この状態で
http://127.0.0.1:8000/samplecrud/list/
にアクセスするとアクセスできますが、、、 編集ボタンを押すと、login画面にリダイレクトされます。 ユーザ表示が少しかっこ悪かったので、navbarに表示させ、ドロップダウンでlogoutを表示させるようにします。 templates/base.html login前 login後 ドロップダウンでlogoutを表示します。 すこしかっこよくなりましたね。 本筋とは少しずれますが、見た目の変更をしやすくするために、base系のtemplateを変更しておきます。 templates/base_global.html それぞれ、title,head,header,body,footerを差し込めるようにし、bootstrapは全体でロード出来るようにしておきます。 そして templates/base.html 現時点で以下のフォルダ構成になっているはずです。 以下のリンクを参照するとわかるのですが、 なのでまず最初に templates/registration/login.html templates/registration/login.html labelは不要なので消し、 login画面の同様、 templates/registration/signup.html templates/registration/signup.html ちょっとはイケてる感じになりましたね。 以上、Djangoでsignup,login機能をgenericviewとdjango.contrib.auth.urlsで最短で作成する。あとで見た目も。でした。本記事で行うこと
n-guitar.hatenablog.com
なお、すでに上記のアプリケーションでBootstrap5を導入している。
GitHub - n-guitar/django-samples at feature/signup/sample.generic-viewモチベーション
環境
$ sw_vers
ProductName: macOS
ProductVersion: 11.1
BuildVersion: 20C69
$ python -V
Python 3.8.0
login機能の作成
Using the Django authentication system | Django documentation | Django事前準備 管理者用account作成
$ python manage.py createsuperuser
Username (leave blank to use 'XXXXX'): admin
Email address: test@test.com
Password:
Password (again):
Superuser created successfully.
signup,login実装用のアプリケーション作成
$ django-admin startapp accounts
settings.py
にも忘れずにアプリを追加しておきます。
confug/settings.py# <省略>
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'samplecrud',
'accounts', # 追加
]
# <省略>
accounts/urls.py
を作成
現時点でaccountディレクトリは以下のようになっている想定です。
urlの設定
confug/urls.py
からaccounts/urls.py
のrouting設定をしておきます。from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('samplecrud/', include('samplecrud.urls')),
path('accounts/', include('accounts.urls')),
]
accounts/urls.py
を作成していきます。
まずloginのURLを作成し、routing先はdjango.contrib.auth
のLoginView
を利用します。from django.urls import path
from django.contrib.auth import views as auth_views
urlpatterns = [
path('login/', auth_views.LoginView.as_view(), name='login'),
]
registration
に配下に作成したlogin.htmlファイルになります。
path('accounts/login/', auth_views.LoginView.as_view(template_name='myapp/login.html')),
とすることで任意の場所に設定することも出来ますが、デフォルトのままで行こうと思います。テンプレートの作成
<form action="" method="POST"> {% csrf_token %}
{{ form.as_p }}
<input type="submit" value="login">
</form>
この状態で
http://127.0.0.1:8000/accounts/login/
にアクセスしてみると以下のように表示されます。
デフォルトではlogin後は/accounts/profile/
が表示されるようになっています。
今回は特にprofileページは必要ないので、 login後に表示されるページを変更してみます。login後のリダイレクトページ設定
settings.py
に以下の用に記述を追加するだけです。
listはサンプルアプリに設定してあるpath('list/', ListPage.as_view(), name='list'),
としておきます。
confug/settings.py# <上記省略>
LOGIN_REDIRECT_URL = 'list'
無事login出来ました。
しかしながら本当にlogin出来ているのか分かりにくいのでloginユーザを表示してみたいと思います。login userの表示
{{ user.username }}
をtemplateに加えるだけなのですがloginしていない時表示出来ないので、条件分岐を入れます。
{% if user.is_authenticated %}
でtrueの時{{ user.username }}
を表示し、loginしていないときはloginページのリンク先を表示しておくことにします。{% if user.is_authenticated %}
{{ user.username }}
{% else %}
<a href="{% url 'login' %}">Login</a></li>
{% endif %}
{% load static %}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="{% static 'css/bootstrap.min.css' %}">
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
<title>sample app</title>
</head>
<header>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Todo Sample App</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link" href="{% url 'list' %}">List</a>
<a class="nav-link" href="{% url 'create' %}">Create</a>
</div>
</div>
</div>
</nav>
</header>
<body>
{% if user.is_authenticated %}
{{ user.username }}
{% else %}
<a href="{% url 'login' %}">Login</a></li>
{% endif %}
{% block Content %}
{% endblock Content %}
</body>
<footer>
<div class="fixed-bottom">
<p class="float-end m-2">create by haku-mai</p>
</div>
</footer>
</html>
login後
ちょっとかっこ悪いですが、見た目は後の章でなおします。logout
logout機能もdjango.contrib.auth
に用意されているため以下の用に設定します。from django.urls import path
from django.contrib.auth import views as auth_views
urlpatterns = [
path('login/', auth_views.LoginView.as_view(), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
]
公式documentとおりだとregistration/logged_out.html
にレンダリングされるはずなのですが、存在していないとadmin siteに飛ばされてしまうようです。
今回はloginした場合は再度loginページに飛ばしてほしいので以下の用に設定します。# <上記省略>
LOGIN_REDIRECT_URL = 'list'
LOGOUT_REDIRECT_URL = 'login' #追加
signup機能の作成
loginみたく、デフォルトで用意されている機能は機能はないものの、UserCreationForm
とCreateView
を組み合わせることで簡単に作成することが出来ます。
まずURLの設定from django.urls import path
from django.contrib.auth import views as auth_views
from .views import SignupPage
urlpatterns = [
path('login/', auth_views.LoginView.as_view(), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('signup/', SignupPage.as_view(), name='signup'),
]
from django.urls import reverse_lazy
from django.views import generic
from django.contrib.auth.forms import UserCreationForm
class SignupPage(generic.CreateView):
form_class = UserCreationForm
success_url = reverse_lazy('login')
template_name = 'registration/signup.html'
<form action="" method="POST"> {% csrf_token %}
{{ form.as_p }}
<input type="submit" value="signup">
</form>
簡単ですね。
ちゃんとpasswordの確認まで行ってくれています。
試しにユーザを作成してログインしてみると、ちゃんとログインできていることが分かります。
※user1と表示されていますね。アクセス制御
なので編集機能だけ、ログインしていないと操作出来ないようにしたいと思います。CreatePage
、UpdatePage
、DeletePage
が編集機能にあたるのでそれを制御します。
samplecrud/views.pyfrom django.shortcuts import render
from .models import Todo
from django.views import generic
from django.urls import reverse_lazy
class ListPage(generic.ListView):
model = Todo
template_name = 'list.html'
class CreatePage(generic.CreateView):
model = Todo
template_name = 'create.html'
fields = ('title','memo')
success_url = reverse_lazy('list')
class UpdatePage(generic.UpdateView):
model = Todo
template_name = 'update.html'
fields = ('title','memo')
success_url = reverse_lazy('list')
class DeletePage(generic.DeleteView):
model = Todo
template_name = 'delete.html'
success_url = reverse_lazy('list')
LoginRequiredMixin
を利用し、制御したいviewに継承すればいいだけです。from django.shortcuts import render
from .models import Todo
from django.views import generic
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin #追加
class ListPage(generic.ListView):
model = Todo
template_name = 'list.html'
class CreatePage(LoginRequiredMixin, generic.CreateView): #変更
model = Todo
template_name = 'create.html'
fields = ('title','memo')
success_url = reverse_lazy('list')
class UpdatePage(LoginRequiredMixin, generic.UpdateView): #変更
model = Todo
template_name = 'update.html'
fields = ('title','memo')
success_url = reverse_lazy('list')
class DeletePage(LoginRequiredMixin, generic.DeleteView): #変更
model = Todo
template_name = 'delete.html'
success_url = reverse_lazy('list')
しかも、URLがhttp://127.0.0.1:8000/accounts/login/?next=/samplecrud/update/1
になっているのですが
ログインすると、もともとアクセスしたかったページに戻ってくることが出来ます。
すごく便利ですよね。見た目の変更
ユーザ表示の変更
{% load static %}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="{% static 'css/bootstrap.min.css' %}">
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
<title>sample app</title>
</head>
<header>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Todo Sample App</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="{% url 'list' %}">List</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'create' %}">Create</a>
</li>
</ul>
{% if user.is_authenticated %}
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">{{ user.username }}</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="{% url 'logout' %}">Logout</a></li>
</ul>
{% else %}
<a class="btn btn-outline-success btn-sm" href="{% url 'login' %}">Login</a></li>
{% endif %}
</div>
</div>
</nav>
</header>
<body>
{% block Content %}
{% endblock Content %}
</body>
<footer>
<div class="fixed-bottom">
<p class="float-end m-2">create by haku-mai</p>
</div>
</footer>
</html>
base templateの修正
signup,loginの見た目をいいかんじにしたいのですが、base.htmlを継承すると、navbarまで表示してしまうので、独自で画面を作成できるように編集します。
新しくbase_global.html
を作成し、以下のように設定します。{% load static %}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="{% static 'css/bootstrap.min.css' %}">
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
{% block HeadContent %}
{% endblock HeadContent %}
<title>
{% block TitleContent %}
sample app
{% endblock TitleContent %}
</title>
</head>
<header>
{% block HeaderContent %}
{% endblock HeaderContent %}
</header>
<body>
{% block BodyContent %}
{% endblock BodyContent %}
</body>
<footer>
{% block FooterContent %}
{% endblock FooterContent %}
</footer>
</html>
base.html
を以下のように変更します。{% extends 'base_global.html' %}
{% load static %}
{% block HeaderContent %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Todo Sample App</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="{% url 'list' %}">List</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'create' %}">Create</a>
</li>
</ul>
{% if user.is_authenticated %}
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">{{ user.username }}</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="{% url 'logout' %}">Logout</a></li>
</ul>
{% else %}
<a class="btn btn-outline-success btn-sm" href="{% url 'login' %}">Login</a></li>
{% endif %}
</div>
</div>
</nav>
{% endblock HeaderContent %}
{% block BodyContent %}
{% block Content %}
{% endblock Content %}
{% endblock BodyContent %}
{% block FooterContent %}
<div class="fixed-bottom">
<p class="float-end m-2">create by haku-mai</p>
</div>
{% endblock FooterContent %}
base_head.html
から読み込み、header,footerを差し込みます。
{% block BodyContent %}
の中に{% block Content %}
を元のまま用意しておき、他のtemplateファイルには変更がないようにしておきます。
上記の変更は、今までの見た目には何も変更は有りません。
login画面の修正
{{ form.as_p }}
はいろいろな機能を持っているので、これを分解しなければなりません。
【Django3.1】Djangoで作成したアプリケーションにBootstrap5-beta1を導入し、見た目を整える。 - 米が食べたいぜぇ〜 haku-maiのブログ{{ form.as_p }}
を展開して記述します。{% if form.errors %}
<p>Please enter a correct username and password. Note that both fields may be case-sensitive.</p>
{% endif %}
<form action="" method="POST"> {% csrf_token %}
<p>
<label for="{{ form.username.id_for_label }}">{{ form.username.label }}:</label>
<input type="text" name="{{ form.username.html_name }}" autofocus=""
autocapitalize="none" autocomplete="{{ form.username.html_name }}" maxlength="150" required="" id="{{ form.username.id_for_label }}">
</p>
<p>
<label for="{{ form.password.id_for_label }}">{{ form.password.label }}:</label>
<input type="password" name="{{ form.password.html_name }}"
autocomplete="current-password" required="" id="{{ form.password.id_for_label }}">
</p>
{% if form.errors %}
{% for key,value in form.errors.items %}
{{ value }}
{% endfor %}
{% endif %}
<input type="submit" value="login">
</form>
同じ画面を表示することができ、問題なくlogin可能です。
※{% for item in form %}
でform文を作成することも出来るのですが、上記のほうが個人的にわかりやすいので、上記のママとします。
分解できたので、見た目を整えていきます。{% extends 'base_global.html' %}
{% block HeadContent %}
<style>
.login-form {
width: 340px;
margin: 100px auto;
font-size: 15px;
}
.form-group {
margin-bottom: 1rem;
}
.form-control, .btn {
min-height: 38px;
}
.btn {
font-size: 15px;
font-weight: bold;
}
.btn-block {
display: block;
width: 100%;
}
</style>
{% endblock HeadContent %}
{% block BodyContent %}
<div class="login-form">
<dev class="text-center">
<form style="max-width:300px;margin:auto" action="" method="POST"> {% csrf_token %}
<h1 class="h3 mb-3 font-weight-normal">ToDo App</h1>
<div class="form-group">
<input class="form-control" type="text" placeholder="Username"
name="{{ form.username.html_name }}" autofocus="" autocapitalize="none"
autocomplete="{{ form.username.html_name }}" maxlength="150"
required="" id="{{ form.username.id_for_label }}">
</div>
<div class="form-group">
<input class="form-control" type="password" placeholder="Password"
name="{{ form.password.html_name }}" autocomplete="current-password"
required="" id="{{ form.password.id_for_label }}">
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary btn-block">Login</button>
</div>
<p class="text-center"><a href="{% url 'signup' %}">Create an Account</a></p>
{% if form.errors %}
{% for key,value in form.errors.items %}
{{ value }}
{% endfor %}
{% endif %}
</form>
<p class="mt-5 mb-3 text-muted">© 2021 haku-mai</p>
</dev>
</div>
{% endblock BodyContent %}
class="login-form"
とclass="form-group"
でくくることで見た目を整えます。
細かい部分はstyleで修正すると少しはイケてる感じになりました。signup画面の修正
{{ form.as_p }}
を一度分解してみます。<form action="" method="POST"> {% csrf_token %}
<p>
<label for="{{ form.username.id_for_label }}">Username:</label>
<input type="text" name="{{ form.username.html_name }}" maxlength="150"
autocapitalize="none" autocomplete="{{ form.username.html_name }}"
autofocus="" required="" id="{{ form.username.id_for_label }}">
<span class="helptext">Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.</span>
</p>
<p>
<label for="{{ form.password1.id_for_label }}">Password:</label>
<input type="password" name="{{ form.password1.html_name }}"
autocomplete="new-password" required="" id="{{ form.password1.id_for_label }}">
<span class="helptext"></span>
</p>
<ul>
<li>Your password can’t be too similar to your other personal information.</li>
<li>Your password must contain at least 8 characters.</li>
<li>Your password can’t be a commonly used password.</li>
<li>Your password can’t be entirely numeric.</li>
</ul>
<p></p>
<p>
<label for="{{ form.password2.id_for_label }}">Password confirmation:</label>
<input type="password" name="{{ form.password2.html_name }}"
autocomplete="new-password" required="" id="{{ form.password2.id_for_label }}">
<span class="helptext">Enter the same password as before, for verification.</span>
</p>
{% if form.errors %}
{% for key,value in form.errors.items %}
{{ value }}
{% endfor %}
{% endif %}
<input type="submit" value="signup">
</form>
同じ画面を表示することができ、問題なくsignup可能です。
分解できたので、見た目を整えていきます。{% extends 'base_global.html' %}
{% block HeadContent %}
<style>
.signup-form {
width: 340px;
margin: 100px auto;
font-size: 15px;
}
.form-group {
margin-bottom: 1rem;
}
.form-control, .btn {
min-height: 38px;
}
.btn {
font-size: 15px;
font-weight: bold;
}
.btn-block {
display: block;
width: 100%;
}
</style>
{% endblock HeadContent %}
{% block BodyContent %}
<div class="signup-form">
<form action="" method="POST"> {% csrf_token %}
<h1 class="h3 mb-3 font-weight-normal text-center">Welcome ToDo App</h1>
<p class="text-center">Please fill in this form to create an account!</p>
<div class="form-group">
<input class="form-control" type="text" placeholder="Username" name="{{ form.username.html_name }}" maxlength="150"
autocapitalize="none" autocomplete="{{ form.username.html_name }}"
autofocus="" required="" id="{{ form.username.id_for_label }}">
</div>
<div class="form-group">
<input class="form-control" type="password" placeholder="password" name="{{ form.password1.html_name }}"
autocomplete="new-password" required="" id="{{ form.password1.id_for_label }}">
</div>
<div class="form-group">
<input class="form-control" type="password" placeholder="Password confirmation" name="{{ form.password2.html_name }}"
autocomplete="new-password" required="" id="{{ form.password2.id_for_label }}">
</div>
<div class="form-group">
<button type="submit" class="btn btn-success btn-block">Signup</button>
</div>
{% if form.errors %}
{% for key,value in form.errors.items %}
{{ value }}
{% endfor %}
{% endif %}
<p class="text-center">Already have an account? <a href="{% url 'login' %}">login</a></p>
</form>
<p class="text-center mt-5 mb-3 text-muted">© 2021 haku-mai</p>
</div>
{% endblock BodyContent %}