はじめてのDjango (6) サイトにユーザ登録とログイン・ログアウトの機能を組み込もう

# はじめに

このドキュメントは，もともとはラボの新メンバー向けの入門テキストとして作成したもので，はじめてDjangoにふれる人にざっとその全体像をつかんでもらうことを狙っています．Djangoの日本語での参考資料も充実してきたので今さらという気がしないでもないですが，見直しを機にQiitaに移すことにしました．万が一でもどなたかの参考になれば幸いです．

説明のための具体例として，Djangoのオフィシャルチュートリアルにある投票アプリをとりあげています（が，説明上の都合で少しコードを追加，変更しているところもあります）．素人の独学がベースで，特に前半は公式チュートリアルをやってみた感想のような記事です（今回は全7回中の6回目）．

全体のコードはまとめて[GitHub](https://github.com/j54854/introDjango)に置きました．

## 変更履歴

- [2021/04/26] Djangoのバーションを3.2に更新し，それ基づいて内容を微修正しました．

# ユーザ登録とログイン・ログアウト

ここまでは，urlさえ指定すれば誰でも閲覧できるページを作成してきました．続いて，ログインしないと閲覧できないページや，ログインしたユーザに応じてチューニングした内容が表示されるページを作成する方法をみていきましょう．

このためには，まずユーザという概念を導入する必要があります．

実は，Djangoにはユーザを扱う枠組みが標準装備されています．データベースを初期化した際に，管理者を登録したのを思い出しましょう．あれがユーザの例です．adminサイトのUsersのページをみると，その管理者のデータが格納されているはずです．

最初にユーザ登録用のページ（signupページ）を作成しましょう．希望するusernameとpasswordを取得するためのフォームを下記のように作成し，forms.pyに書き込みます．

```python
from django.contrib.auth.models import User

class SignUpForm(forms.Form):
    username = forms.CharField(widget=forms.TextInput)
    enter_password = forms.CharField(widget=forms.PasswordInput)
    retype_password = forms.CharField(widget=forms.PasswordInput)

    def clean_username(self):
        username = self.cleaned_data.get('username')
        if User.objects.filter(username=username).exists():
            raise forms.ValidationError('The username has been already taken.')
        return username

    def clean_enter_password(self):
        password = self.cleaned_data.get('enter_password')
        if len(password) < 5:
            raise forms.ValidationError('Password must contain 5 or more characters.')
        return password

    def clean(self):
        super(SignUpForm, self).clean()
        password = self.cleaned_data.get('enter_password')
        retyped = self.cleaned_data.get('retype_password')
        if password and retyped and (password != retyped):
            self.add_error('retype_password', 'This does not match with the above.')

    def save(self):
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('enter_password')
        new_user = User.objects.create_user(username = username)
        new_user.set_password(password)
        new_user.save()
```

文字列のfieldが3つ定義されていますが，それらのうち，`enter_password`と`retype_password`の2つにパスワード入力用の`widget`が指定してあります．これによって，キー入力の内容が画面に表示されないようになります．

また，`clean()`の中では，エラーを`raise`するのではなく，`add_error()`というメソッドを呼んでいます．これによって，エラーを特定のfieldに対応付けることができます．

`save()`メソッドをみると，Userクラスのインスタンスの生成方法やpasswordの設定方法が確認できます．

続いて，このフォームを処理するview関数`signup()`を作成しましょう．views.pyの末尾に下記の内容を書き足します．

```python
def signup(request):
    if request.method == 'POST':
        form = SignUpForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('polls:index')
    else:
        form = SignUpForm()

    context = {'form':form}
    return render(request, 'polls/signup.html', context)
```

これは，view関数でフォームを処理するための典型的なパターンに沿ったものになっています．対応するtemplate（signup.html）は，例えば次のように作成すればいいでしょう．

```html
<h1>Please Sign Up to Polls App.</h1>
<form action="{% url 'polls:signup' %}" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Sign Up">
</form>
<p>or <a href= "{% url 'polls:login' %}">Login</a></p>
```

最後に，urlルーティングの情報を更新します．まず，polls/urls.pyを下記のように書き換えます．

```python
from django.contrib.auth import views as auth_views
from django.urls import path
from . import views

app_name = 'polls'

urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('signup/', views.signup, name='signup'),
    path('login/', auth_views.LoginView.as_view(template_name="polls/login.html"), name='login'),
    path('logout/', auth_views.LogoutView.as_view(next_page="polls:index"), name='logout'),
]
```

ユーザ登録用のsignupページの指定が追加されており，上で作成したview関数`signup()`に対応付けられていることがわかります．これで，http://localhost:8000/polls/signup/ にアクセスするとユーザ登録用のページが表示されるはずなので，実際に登録できるか確認してみましょう．

すでに登録済みのユーザに対してはloginのページを用意しましょう．

ユーザ登録と同様にフォームとview関数を自作することもできますが，ここではDjangoに用意されている標準viewを利用してみることにします．また，あわせてlogoutの機能も実装しておきましょう．

上のpolls/urls.pyをよくみると，loginとlogoutのページの指定も追加されていることがわかります．`auth_views.LoginView.as_view()`と`auth_views.LogoutView.as_view()`で標準view（関数ではなくクラス）を呼び出しています．

まずloginページの方からみていきます．この標準viewを用いると，view関数を自作する必要はなくなります．標準viewに引数としてtemplateを渡していることからわかるように，template（login.html）は用意しなければなりません．下記はその一例です．

```html
<h1>Please Login to Polls App.</h1>
<form action="{% url 'polls:login' %}" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Login">
</form>
<p>or <a href= "{% url 'polls:signup' %}">Sign Up</a></p>
```

なお，この標準viewのデフォルトのurlはaccounts/login/なので，そのアドレスが指定された場合にpolls/loginにリダイレクトされるように仕組んでおきましょう．また，loginに成功した場合，標準ではaccounts/profile/にリダイレクトされるので，このアドレスはpolls/indexに再リダイレクトすることにしましょう．

これらの設定は，リダイレクトのための標準view（のクラス`base.RedirectView.as_view()`）を用いて，mysite/urls.pyの中に下記のように書くことができます．

```python
from django.contrib import admin
from django.urls import include, path
from django.views.generic import base

urlpatterns = [
    path('polls/', include('polls.urls')),
    path('admin/', admin.site.urls),
    path('accounts/login/', base.RedirectView.as_view(pattern_name="polls:login")),
    path('accounts/profile/', base.RedirectView.as_view(pattern_name="polls:index")),
]
```

一方，logoutの方は，特にそれ専用のページは用意せず，このアドレスがリクエストされるとlogout処理を行って，それが終わったらindexページにリダイレクトするように指定してあります（この機能の呼び出し方は下で考えることにします）．

続いて，detailページを，loginしているユーザしか利用できないように変更してみます．これは，`@login_required`というデコレータを利用すれば簡単に実装できます．view関数`detail()`を次のように書き換えましょう．

```python
from django.contrib.auth.decorators import login_required

...

@login_required
def detail(request, question_id):
    user = request.user
    question = get_object_or_404(Question, pk=question_id)

    if request.method == 'POST':
        form = VoteForm(request.POST, question=question)
        if form.is_valid():
            form.save()
            return redirect('polls:results', question_id=question.id)
    else:
        form = VoteForm(question=question)

    context = {'user':user, 'question':question, 'form':form}
    return render(request, "polls/detail.html", context)
```

`detail()`がデコレータ`@login_required`で修飾されているのがわかります．

この結果，もしloginしていないユーザがこのページにアクセスしようとした場合は，detailページは表示されず，accounts/login/にリダイレクトされるようになります（accounts/login/はさらにpolls/login/にリダイレクトされるので，上で作成したloginページが表示されます）．

また，`request.user`（loginしているユーザのUserインスタンス）を取得して`user`というキーで`context`の中に含めて`render()`を呼んでいることもみてとれます．

せっかくなので，この`user`の情報をtemplateの中で利用してみよう．具体的には，detail.htmlを次のように更新します．

```html
<h1>Hi {{ user|title }}! {{ question.question_text }}</h1>
{{ form.non_field_errors }}
<form action="{% url 'polls:detail' question.id %}" method="post">
{% csrf_token %}
<p>
    {{ form.your_choice.label }}<br>
    {{ form.your_choice }}
</p>
<p>
    {{ form.new_option.label }}<br>
    {{ form.new_option }}
</p>
<input type="submit" value="Vote">
</form>
<p>or <a href= "{% url 'polls:logout' %}">Logout</a></p>
```

最初にloginしているユーザ名を表示するようにしました．`title`は，与えられた文字列の先頭だけを大文字にするフィルタです．

また，最後にlogoutを呼ぶリンクも追加してあります．このリンクを踏むとlogoutの処理が走り，indexページにリダイレクトされるはずです．

# おわりに

以上で6回目は終了です．[最終回](https://qiita.com/j54854/items/1f0560142e39d888251c)に続きます．