Djangoでタスク管理アプリをつくりたい！ ユーザ認証編

# はじめに
[前回（環境構築編）](https://qiita.com/kuma3_it/items/d5adba5fae6d6cefa512)で使用したプロジェクトを引き継いで作成する。

# 開発環境
・Windows10 professional 64bit
・Python 3.7.1
・Django 2.1.2
・MySQL 8.0.12

# ユーザ認証
## ログイン
・ログインをしトップページを表示する機能を作成する。
　
### URL設定
プロジェクトで画面のURLを設定する

```python:urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('accounts.urls')),
]
```

アプリ内でもURLを設定するために，accounts直下に`urls.py`を作成

```python:urls.py
from django.urls import path
from . import views

app_name = 'accounts'

urlpatterns = [
    path('', views.Login.as_view(), name='login'),
    path('top/', views.Top.as_view(), name='top'),
    path('logout/', views.Logout.as_view(), name='logout'),
]
```


ログインフォームを作成するので，accounts直下に`forms.py`を作成

```python:forms.py
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
```
表示先を指定する`views.py`を編集

```python:views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import (
    LoginView, LogoutView
)
from django.views import generic
from django.contrib.auth.decorators import login_required
from .forms import LoginForm


"""ログインページ"""
class Login(LoginView):
    template_name = 'login.html'
    form_class = LoginForm


""" トップページ """
class Top(LoginRequiredMixin, generic.TemplateView):
    template_name = 'top.html'
    redirect_field_name = 'redirect_to'


"""ログアウトページ"""
class Logout(LoginRequiredMixin, LogoutView):
    template_name = 'logout.html'
```
`LoginRequiredMixin`を継承することによって，ログインをしていない場合にログインページにリダイレクトされる

accounts直下に`templates`フォルダを作成しtemplateの接続先とログインページの設定を`setting.py`で指定する

```python:setting.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(BASE_DIR, 'templates')     # 追記
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]



# login setting
LOGIN_ERROR_URL = '/login/'
LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '../'
```

ここまでの設定でプロジェクトから表示するページまでのプログラム上の繋がりが完成した

### HTML作成
#### bootstrapをダウンロード
・見た目を整えるためにbootstrapを使うので，[bootstrap4](http://getbootstrap.com/docs/4.0/examples/)のソースコードをダウンロードする
解凍してその中身をそのまま`taskMan/static/bootstrap`直下にコピーする
この際`static/bootstrap`フォルダを作成する

```:tree
taskMan
 |---accounts
 |---static
 | |---bootstrap
 | 
 |---taskMan
 |---manage.py
```

そして，staticフォルダがどこにあるのかをプロジェクトの`setting.py`に記載する

```python:setting.py
# Static file settings
STATIC_ROOT = os.path.join(BASE_DIR, 'assets')

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "static"),
)
```

#### ベースHTMLの作成
毎回linkタグやmetaタグなどのヘッダー情報を書くのは大変なので，雛型となるhtmlを`templates`に作成する

```html:base.html
<!DOCTYPE html>
<html lang='ja'>
    <head>
        {% load staticfiles %}
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="">
        <meta name="author" content="">

        <title>{% block title %}{% endblock %}</title>

        <link href="{% static 'bootstrap/dist/css/bootstrap.css' %}" rel="stylesheet">
        <link href="{% static 'bootstrap/dist/css/bootstrap.min.css' %}" rel="stylesheet">
        <link href="{% static 'bootstrap/dist/css/bootstrap-grid.css' %}" rel="stylesheet">
        <link href="{% static 'bootstrap/dist/css/bootstrap-grid.min.css' %}" rel="stylesheet">
        <link href="{% static 'bootstrap/dist/css/bootstrap-reboot.css' %}" rel="stylesheet">
        <link href="{% static 'bootstrap/dist/css/bootstrap-reboot.min.css' %}" rel="stylesheet">
        <link href="{% static 'bootstrap/docs/4.0/examples/dashboard/dashboard.css' %}" rel="stylesheet">

        <script type="text/javascript" src="{% static 'bootstrap/assets/js/vendor/jquery-slim.min.js' %}"></script>

        <script src="{% static 'bootstrap/assets/js/vendor/anchor.min.js' %}"></script>
        <script src="{% static 'bootstrap/assets/js/vendor/clipboard.min.js' %}"></script>
        <script src="{% static 'bootstrap/assets/js/vendor/holder.min.js' %}"></script>
        <script src="{% static 'bootstrap/assets/js/vendor/popper.min.js' %}"></script>
        <script src="{% static 'bootstrap/dist/js/bootstrap.bundle.js' %}"></script>
        <script src="{% static 'bootstrap/dist/js/bootstrap.bundle.min.js' %}"></script>
        <script src="{% static 'bootstrap/dist/js/bootstrap.js' %}"></script>
        <script src="{% static 'bootstrap/dist/js/bootstrap.min.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/alert.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/button.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/carousel.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/collapse.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/dropdown.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/index.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/modal.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/scrollspy.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/tab.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/tooltip.js' %}"></script>
        <script src="{% static 'bootstrap/js/dist/util.js' %}"></script>
    </head>

    <body>
        <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0">
            <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="/">TaskMan</a>
            <div class="form-control form-control-dark w-100">{% block mainT %}{% endblock %}</div> 
            <div class="form-control form-control-dark w-100" style="text-align: right;">{% block mainL %}{% endblock %}</div> 
            <ul class="navbar-nav px-3">
                <li class="nav-item text-nowrap">
                {% block auth %}{% endblock %}
                </li>
            </ul>
        </nav>
    
        {% block body %}{% endblock %}

    </body>
</html>
```

トップページ，ログインページ，ログアウトページを`templates`にそれぞれ作成する

```html:top.html
{% extends "base.html" %}
{% block title %} TaskMan {% endblock %}
{% block mainT %} トップメニュー {% endblock %}
{% block mainL %}UserID : {{ user.get_username }}{% endblock %}
{% block auth %}<a class="nav-link" href="../logout/">ログアウト</a>{% endblock %}
{% block body %}
    <h2 style="text-align: center;">Hello World!</h2>
{% endblock %}
```

```html:login.html
{% extends "base.html" %}
{% block title %} TaskMan ログイン {% endblock %}
{% block mainT %} ログイン {% endblock %}
{% block mainL %}UserID : {{ user.get_username }}{% endblock %}
{% block auth %}{% endblock %}
{% block body %}
<div style='padding-top: 50px;'>
    <form action="" method="POST">
        <div class="col-md-6 offset-md-3">
            <div class="card">
                <div class="card-body">
                    {{ form.non_field_errors }}
                    {% for field in form %}
                        {{ field }}
                        {{ field.errors }}
                        <hr>
                    {% endfor %}
                    <button type="submit" class="btn btn-lg btn-primary btn-block" >ログイン</button>
                    <input type="hidden" name="next" value="{{ next }}" />
                    {% csrf_token %}
                </div>
            </div>
        </div>
    </form>
</div>
{% endblock %}
```

```html:lgout.html
{% extends "base.html" %}
{% block title %} TaskMan ログアウト {% endblock %}
{% block mainT %} ログアウト {% endblock %}
{% block mainL %}UserID : {{ user.get_username }}{% endblock %}
{% block auth %}{% endblock %}
{% block body %}
    <div style="text-align: center; padding-top: 50px;">
    <h2 >お疲れ様でした</h2>
    <a class="nav-link" href="../login/">ログイン</a>
    </div>
{% endblock %}
```

HTMLも完成したので，[localhost](http://localhost:8000)にアクセスするとログイン画面が表示される
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/763a1215-dc49-585f-2729-c85a34bc1aae.png)

前回作成したアカウントでログインすると
トップページが表示される
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/079f56f8-86bb-73eb-160d-8f8c12bf0acc.png)


ログアウトをして，トップページに移ろうとするとログインページにリダイレクトする


## アカウント登録

### URL設定
アカウント登録するためのURLを`accounts/urls.py`に追加

```python:urls.py
urlpatterns = [
    path('', views.Top.as_view(), name='top'),
    path('login/', views.Login.as_view(), name='login'),
    path('logout/', views.Logout.as_view(), name='logout'),
    path('registration/', views.Registration.as_view(), name='registration'),  # 追加
    path('registration/complete', views.RegistrationComp.as_view(), name='registration_complete'),  # 追加
]
```

アカウント登録用のフォームを作成するので，`accounts/forms.py`に以下を追加

```python:forms.py:
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model

User = get_user_model()


"""アカウント登録フォーム"""
class RegistrationForm(UserCreationForm):

    class Meta:
        model = User
        fields = (
            'username', 'email',
            'password1', 'password2',
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
```

`get_user_model()`によって，現在アクティブになっているユーザモデルを使用できる
なので，カスタムユーザモデルならカスタムユーザモデルで，そうでないならデフォルトのユーザモデルを使用する
また，Meta情報に`fields'を書き込まないとフォームが使えないので注意する（自分はここでしばらく悩んだ）

Viewに以下を追加する

```python:views.py
from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.auth import get_user_model
from .forms import RegistrationForm

User = get_user_model()


"""アカウント登録ページ"""
class Registration(generic.CreateView):
    model = User
    template_name = 'registration.html'
    form_class = RegistrationForm
    success_url ='/registration/complete'


"""アカウント登録完了"""
class RegistrationComp(generic.TemplateView):
    template_name = 'registration_complete.html'
```

アカウント登録フォームを表示するページと，登録完了用のページを準備する。


### HTML作成
アカウント登録用の`registration.html'と登録完了用の`registration_complete.html'を作成し，アカウント登録画面への入り口を`login.html`に作成する

```python:login.html
{% extends "base.html" %}
{% block title %} TaskMan ログイン {% endblock %}
{% block mainT %} ログイン {% endblock %}
{% block mainL %}User : {{ user.get_username }}{% endblock %}
{% block auth %}
{% endblock %}
{% block body %}
<div style='padding-top: 50px;'>
    <form action="" method="POST">
        <div class="col-md-6 offset-md-3">
            <div class="card">
                <div class="card-body">
                    {{ form.non_field_errors }}
                    {% for field in form %}
                        {{ field }}
                        {{ field.errors }}
                        <hr>
                    {% endfor %}
                    <button type="submit" class="btn btn-lg btn-primary btn-block" >ログイン</button>
                    <input type="hidden" name="next" value="{{ next }}" />
                    {% csrf_token %}
                </div>
            </div>
            <div class="authElement">    # 追加
                <a class="btn btn-outline-secondary" href="/registration/">アカウント登録</a>    # 追加
            </div>    # 追加
        </div>
    </form>
</div>
{% endblock %}
```

この時に，見た目を整えるためにCSSを作り適用させる
新しく`static/css/adjustment.css`を作成する

```css:adjustment.css
.authElement {
    margin:20px;
    text-align: center;
}
```

このCSSを適用するために，`base.html`のヘッダに追記する

```html:base.html
<link href="{% static 'css/adjustment.css' %}" rel="stylesheet" type="text/css">
```

次に登録画面を作成する

```html:registration.html
<div style="padding:20px 200px 0px 200px;">
    <form action="" method="POST">
        {{ form.non_field_errors }}
        {% for field in form %}
        <div class="form-group">
            <label for="{{ field.id_for_label }}">{{ field.label_tag }}</label>
            {{ field }}
            <span style="color:red">{{ field.errors }}<span>
        </div>
        {% endfor %}
        {% csrf_token %}
        <button type="submit" class="btn btn-primary btn-lg">登録</button>
    </form>
</div>
```

そして、登録完了画面を作成

```python:registration_complete.py
    <div style="text-align: center; padding-top: 50px;">
        <h2 >登録が完了しました</h2>
        <a class="nav-link" href="/login/">ログイン</a>
    </div>
```
登録処理が完成

画面で確認するとこうなる

ログイン画面
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/ab4f6452-b151-7f16-19ae-5e6feb350b11.png)


登録画面
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/d2c609b0-e035-7614-dc04-d2397c8bd3ea.png)

項目を入力し登録をする
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/71a73cdd-425e-7034-3816-cab452030ee0.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/344fb69c-efed-a98b-f531-3b5faff067e4.png)

登録したアカウントでログインできることを確認する
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/a3f26e07-2a95-6d84-7b99-23160cc8a70a.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/e729ed50-6616-6132-702e-9fe73707ceef.png)

アカウント登録が正常にできていることが確認できた

# 今回のまとめ
今回は導入したかったログイン関係とアカウント登録の作成ができた。しかし，パスワード変更ができないなどまだ課題は残っているので次回はそこら辺を解決していきたい。

# シリーズ
・[Djangoでタスク管理アプリをつくりたい! 環境構築編](https://qiita.com/kuma3_it/items/d5adba5fae6d6cefa512)
・Djangoでタスク管理アプリをつくりたい! ユーザ認証編
・[Djangoでタスク管理アプリをつくりたい! ユーザ情報編](https://qiita.com/kuma3_it/items/32edb3854c5154ca6ba6)
・[Djangoでタスク管理アプリをつくりたい！ プロジェクト管理編](https://qiita.com/kuma3_it/items/bbca1565a62aac0d0a63)
・[Djangoでタスク管理アプリをつくりたい！ タスク管理編１](https://qiita.com/kuma3_it/items/7cb0f838fa4f99a74441)

# 追記
(2018/11/12)
 @khskさん 誤字の指摘・修正ありがとうございます。