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

# はじめに
[前回（ユーザ認証編）](https://qiita.com/kuma3_it/items/8038fac36c41b139e285)で使用したプロジェクトを引き継いで作成する。

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

# ユーザ情報
## 表示と編集
ログインしているユーザの情報を表示するページと，変更できるページを作成する

### URL設定
ユーザ情報の表示と変更のURLを`accounts/urls.py`に設定する

```python:urls.py
urlpatterns = [
                        .
                        .
                        .
    path('profile/<int:pk>/', views.Profile.as_view(), name='profile'),   # 追加
    path('profile/<int:pk>/update/', views.ProfileUpdate.as_view(), name='profile_update'),   # 追加
]
```

ユーザ情報更新用のフォームを作成

```python:forms.py

""" アカウント更新フォーム """
class AccountsUpdateForm(forms.ModelForm):

    class Meta:
        model = User
        fields = (
            'username', 'email',
            'last_name', 'first_name',
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
```
'fields'によって更新できる値を指定できる

`views.py`を編集

```python:views.py
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.shortcuts import redirect, resolve_url
from .forms import LoginForm, RegistrationForm, AccountsUpdateForm


""" ユーザ限定クラス """
class UserOnlyMixin(UserPassesTestMixin):
    raise_exception = True

    def test_func(self):
        user = self.request.user
        return user.pk == self.kwargs['pk'] or user.is_superuser


""" ユーザ情報表示ページ """
class Profile(UserOnlyMixin, generic.DetailView):
    model = User
    template_name = 'profile.html'


""" ユーザ情報更新ページ """
class ProfileUpdate(UserOnlyMixin, generic.UpdateView):
    model = User
    form_class = AccountsUpdateForm
    template_name = 'profile_update.html'

    def get_success_url(self):
        return resolve_url('accounts:profile', pk=self.kwargs['pk'])

```

`UserPassesTestMixin`を継承したMixinクラスによって，自分以外のユーザがそのページを見れないようにする事が出来るので，
各ページには作成したMixinクラスを継承させる

あとはHTMLを作れば画面に表示される


### HTML作成
ユーザ情報へ移動するボタンを作成するので'base.html'の`<body></body>`内を以下のように修正する

```html:base.html
<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="btn-group">
                <div class="btn dropdown-toggle naviDrop" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                        {% if user.is_authenticated %}
                            User : {{ user.get_username }}
                        {% else %}
                            User
                        {% endif %}
                </div>
                {% if user.is_authenticated %}
                    <div class="dropdown-menu dropdown-menu-right" >
                    <a class="dropdown-item" href="{% url 'accounts:profile' user.pk %}">ユーザ情報</a>
                    <div class="dropdown-divider"></div>
                    <a class="dropdown-item" href="/logout/">ログアウト</a>
                    </div>
                {% endif %}
            </div>
        </nav>
    
        {% block body %}{% endblock %}

    </body>
```

また，CSSを追加する

```css:adjustment.css
.naviDrop {
    color:white;
    margin-right:20px;
    width:200px;

}
```

ユーザ情報ページ（`profile.html`）とユーザ情報更新ページ（`profile_update.html`）を作成する

```html:profile.html
{% extends "base.html" %}
{% block title %} TaskMan {% endblock %}
{% block mainT %} アカウント情報 {% endblock %}
{% block body %}
<div style="margin:20px">
    <table class="table">
        <tbody>
            <tr>
                <th>ユーザー名</th>
                <td>{{ user.username }}</td>
            </tr>
            <tr>
                <th>メールアドレス</th>
                <td>{{ user.email }}</td>
            </tr>
            <tr>
                <th>姓</th>
                <td>{{ user.last_name }}</td>
            </tr>
            <tr>
                <th>名</th>
                <td>{{ user.first_name }}</td>
            </tr>
        </tbody>
    </table>
    <a href="{% url 'accounts:profile_update' user.pk %}" class="btn btn-primary ">変更</a>
</div>
{% endblock %}
```

```html:profile_update.html
{% extends "base.html" %}
{% block title %} TaskMan {% endblock %}
{% block mainT %} アカウント情報 更新 {% endblock %}
{% block body %}
<div style="margin:20px">
    <form action="" method="POST">
        {{ form.non_field_errors }}
        <table class="table">
            <tbody>
                {% for field in form %}
                    <tr>
                        <th><label for="{{ field.id_for_label }}">{{ field.label }}</label></th>
                        <td>{{ field }} <span style="color:red">{{ field.errors }}<span></td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
        {% csrf_token %}
        <button type="submit" class="btn btn-success" >更新</button>
    </form>
</div>
{% endblock %}
```

ユーザ情報画面とユーザ情報更新画面の完成


### 画面確認
作成した画面を確認すると次のようになる

・トップページ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/36dd41b2-d9b6-ce0b-169e-fcb6f22bbb5f.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/675a07eb-d931-18e1-2366-63d116b3a856.png)

・ユーザ情報
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/f0e2992e-7913-3685-e0df-d306f334265f.png)

・ユーザ情報更新
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/a75d545d-cd2c-0099-c131-6803507da862.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/550eac26-d4fe-99f5-49f2-0f1679a9cb55.png)

・異なるユーザでログインし同じURLを開こうとする
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/c91f9290-3f39-ddf1-dda2-49a757e34859.png)
**※superuserだと見れるので注意**

期待している動きを確認することができた


## パスワード変更

### URL設定

パスワード変更用のURLを`accounts/urls.py`設定する

```python:urls.py
urlpatterns = [
              .
              .
              .
    path('profile/<int:pk>/password', views.PasswordChange.as_view(), name='password'),  # 追記
    path('profile/<int:pk>/password/complete', views.PasswordChangeComplete.as_view(), name='password_complete'),  # 追記
]
```

`views.py`の設定も行う

```python:views.py
from django.contrib.auth.views import LoginView, LogoutView, PasswordChangeView, PasswordChangeDoneView


""" パスワード変更ページ """
class PasswordChange(UserOnlyMixin, PasswordChangeView):
    model = User
    template_name = 'password_change.html'

    def get_success_url(self):
        return resolve_url('accounts:password_complete', pk=self.kwargs['pk'])


""" パスワード変更完了 """
class PasswordChangeComplete(UserOnlyMixin, PasswordChangeDoneView):
    template_name = 'password_change_complete.html'
```


### HTML作成
ユーザ情報からパスワード変更画面に飛びたいので，`profile.html'を修正する

```html:profile.html
     .
     .
     .
    </table>
    <a href="{% url 'accounts:profile_update' user.pk %}" class="btn btn-primary ">変更</a>
    <a href="{% url 'accounts:password' user.pk %}" class="btn btn-primary ">パスワード変更</a>  <!-- 追加 -->
</div>
{% endblock %}
```

パスワード変更画面（`password_change.html`）と変更完了画面（`password_change_complete.html`）を作成する

```html:password_change.html
{% extends "base.html" %}
{% block title %} TaskMan {% endblock %}
{% block mainT %} パスワード変更 {% endblock %}
{% block body %}
<div style="margin:20px">
    <form action="" method="POST">
        {{ form.non_field_errors }}
        {% for field in form %}
        <div class="form-group">
            <label class="col-6" for="{{ field.id_for_label }}">{{ field.label_tag }}</label>
            {{ field }}
            <span style="color:red">{{ field.errors }}<span>
        </div>
        {% endfor %}
        {% csrf_token %}
        <div class="form-group row">
            <div class="authElement">
                <button type="submit" class="btn btn-success">変更</button>
            </div>
            <div class="authElement">
                <a href="{% url 'accounts:profile' user.pk %}" class="btn btn-primary">戻る</a>
            </div>
        </div>
    </form>
    
</div>
{% endblock %}
```

```html:password_change_complete.html
{% extends "base.html" %}
{% block title %} TaskMan {% endblock %}
{% block mainT %} パスワード変更 {% endblock %}
{% block body %}
<div style="text-align: center; padding-top: 50px;">
    <h2 >パスワードを変更しました</h2>
    <div class="authElement">
        <a href="{% url 'accounts:profile' user.pk %}" class="btn btn-primary">ユーザ情報</a>
    </div>
</div>
{% endblock %}
```

### 画面確認

・ユーザ情報画面
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/8d549f21-9780-5873-4b89-a565010223f3.png)

・パスワード変更画面
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/d8c58748-62ac-f809-ad8a-0a29f188317f.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/aff4c7b2-c843-bfe9-6342-4a5daab4f63c.png)

・パスワード変更完了画面
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/a48c748d-487c-c73b-ae59-251019717b9b.png)

次回ログイン時，変更したパスワードでログインできれば確認完了


## ユーザアイコン設定
今後，チャット機能を追加する予定なのであらかじめユーザアイコンを準備しておく

### 準備
画像を保存するためにはPIL (Pillow)が必要なのでインストールする

```powershell:PowerShell
> python -m pip install pillow
```
画像を保存しておく`media`フォルダをmanage.pyと同じ階層に作成し，`setting.py`にその場所を記録する

```python:setting.py
# media root setting
MEDIA_URL = '/mdeia/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 
```

画像用のURLをプロジェクトのurls.py(`taskMan/urls.py`)に設定する

```python:urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings  # 追加
from django.conf.urls.static import static  # 追加

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

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root = settings.MEDIA_ROOT)  # 追加
```

### Userモデルの変更
ユーザモデルを以下のように変更する

```python:models.py
class User(AbstractUser, models.Model):
    icon = models.ImageField(
        upload_to='img/',
        verbose_name='アイコン',
        blank=True,
    )
```

更新用のフォームにアイコンを追加する

```python:forms.py
fields = (
            'icon',  # 追加
            'username', 'email',
            'last_name', 'first_name',
        )
```

`urls.py`と`views.py`は変更しない

### HTML修正

ユーザ情報画面のテーブルの上に以下のコードを追加する

```html:profile.html
    <div class="icon">
        {% if user.icon.url != "" %}
            <img src="{{ user.icon.url }}" style="height: 100px; width: 100px;">
        {% else %}
            <img src="/static/img/c0063_6_2.png" style="height: 50px; width: 50px;">
        {% endif%}
    </div>
```
`/static/img/c0063_6_2.png`は画像を登録していないときのデフォルト画像
`static`フォルダに`img`フォルダを作り，そこに画像を準備する

更新する際に画像をアップロードできるように`<form>`を変更する

```html:profile_update.html
  <form action="" method="POST" enctype ="multipart/form-data">
```

また，ユーザ名の横にアイコンを表示できるように`base.html`も変更する

```html:base.html
<div class="btn dropdown-toggle naviDrop" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
   {% if user.is_authenticated %}
       User : {{ user.get_username }}
       {% if user.icon.url != "" %}
           <img src="{{ user.icon.url }}" style="height: 20px; width: 20px;">
       {% else %}
           <img src="/static/img/c0063_6_2.png" style="height: 20px; width: 20px;">
       {% endif%}
   {% else %}
        User
   {% endif %}
</div>
```

アイコン調整用のCSSを作成する

```CSS:adjustment.css
.icon {
    text-align: center;
    margin-bottom: 20px;
}
```

### 画面確認

・ユーザ情報画面
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/fdfccfac-b47d-a659-a257-0f2c8b01e774.png)
デフォルトアイコンが表示されている

・ユーザ情報更新画面
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/31bc9de1-63e0-9fbb-370a-dbb45a8e44ee.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/47fafb05-b1ee-98ec-6bff-f77b3281b49b.png)

画像を選択して更新すると
![image.png](https://qiita-image-store.s3.amazonaws.com/0/300075/cab7f57a-22d0-b927-c10f-95722b041545.png)

アイコン画像が更新されたのが確認できた

# 今回のまとめ
情報更新やパスワード変更は比較的に早く完成したが，アイコン設定に時間がかかってしまった。本当は画像が更新された際に前の画像を削除したかったが，画像が更新されなくても前の（つまり今の）画像が削除されてしまったので諦めた。しかし，最終的には導入しなくてはいけないと思っているので，もう少しPythonやDjangoになれてから再挑戦しようと思う

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