Djangoで、ユーザー情報閲覧・更新ページ

2018-10-21 / PythonDjangoBootstrap4

概要

Djangoで、会員登録機能を自作するシリーズの1つです。前回、登録機能を作りました。今回はユーザー情報の閲覧、更新ページです。閲覧と更新ページには、自分とスーパーユーザー以外はアクセスできないようにもします。

ナビバーに、閲覧と更新が増えました。
ナビバーに、閲覧と更新が

閲覧ページではユーザー情報の確認ができ...
閲覧ページではユーザー情報の確認

更新ページでは、ユーザー情報の更新ができます。メールアドレスは別のページで変更します。
更新ページでは、ユーザー情報の更新

自分以外のユーザーページにアクセスしようとすると、エラーです。
エラー

URL定義の追加

2つ増えました。urls.pyを更新しましょう。

    path('user_detail/<int:pk>/', views.UserDetail.as_view(), name='user_detail'),
    path('user_update/<int:pk>/', views.UserUpdate.as_view(), name='user_update'),

ユーザー情報更新フォーム

forms.pyです。更新フォームは、単純にモデルフォームをそのまま使います。

class UserUpdateForm(forms.ModelForm):
    """ユーザー情報更新フォーム"""

    class Meta:
        model = User
        fields = ('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'

ビュー

views.pyです。importがいくつか増え、ビューが3つ増えました。うち1つはMixinです。

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.views import (
    LoginView, LogoutView
)
from django.contrib.sites.shortcuts import get_current_site
from django.core.signing import BadSignature, SignatureExpired, loads, dumps
from django.http import HttpResponseBadRequest
from django.shortcuts import redirect, resolve_url
from django.template.loader import render_to_string
from django.views import generic
from .forms import (
    LoginForm, UserCreateForm, UserUpdateForm
)


User = get_user_model()
...
...
...
class OnlyYouMixin(UserPassesTestMixin):
    raise_exception = True

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


class UserDetail(OnlyYouMixin, generic.DetailView):
    model = User
    template_name = 'register/user_detail.html'


class UserUpdate(OnlyYouMixin, generic.UpdateView):
    model = User
    form_class = UserUpdateForm
    template_name = 'register/user_form.html'

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

ユーザー情報閲覧・更新ページは、自分以外アクセスできないようにするのが良さそうです。こういった場合に使えるのがDjangoに用意されているUserPassesTestMixinです。クラス属性raise_exceptionは、条件を満たさない場合に403ページに移動させるかどうかのフラグです。Falseなら、ログインページに移動させます。 test_funcメソッド内に、条件となる処理を書くだけです。

class OnlyYouMixin(UserPassesTestMixin):
    raise_exception = True

    def test_func(self):
        # 今ログインしてるユーザーのpkと、そのユーザー情報ページのpkが同じか、又はスーパーユーザーなら許可
        user = self.request.user
        return user.pk == self.kwargs['pk'] or user.is_superuser

ユーザー情報の詳細ページは、非常に簡単ですね。Mixinの適用も簡単です。

class UserDetail(OnlyYouMixin, generic.DetailView):
    model = User

更新ページも殆ど通常のUpdateViewですが、メソッドを上書きしています。 get_success_url()では、リダイレクト先のURLを文字列で返します。今回は更新した後に詳細ページに移動させたかったのですが、このようにリダイレクト先が動的に変わる場合はクラスの属性ではなく、メソッドを上書きします。 私は処理によってはform_valid内でリダイレクトさせることも多いのですが、行儀よく書くならget_success_urlメソッドという専用のメソッドを上書きするべきですね。

class UserUpdate(OnlyYouMixin, generic.UpdateView):
    model = User
    form_class = UserUpdateForm

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

このメソッドは、form_valid()の最後でHttpResponseRedirect(self.get_success_url()) のように呼び出されます。なので、URLを返す必要があるのです。URLを作成するための関数はreversereverse_lazyresolve_url等があるのですが、一番使いやすいのがresolve_urlです。resolve_urlが使えない場合...よくあるのはクラスの属性にsuccess_url = reverse_lazy('app:index')のように書く場合ですが、こういったときだけreverse_lazyを使っておけば間違いありません。(これは、この段階ではurls.pyが読み込まれていないため、app:indexを逆引きできないためです。lazyの名のとおり、遅延評価します)

ベーステンプレート更新

base.htmlです。ナビバーにリンクが増えます。

    <!-- ナビバー -->
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
      <a class="navbar-brand" href="{% url 'register:top' %}">ホームページ名</a>
      <button class="navbar-toggler" type="button" data-toggle="collapse" data-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 mr-auto">
          {% if user.is_authenticated %}
          <li>
            <a class="nav-item nav-link" href="{% url 'register:user_detail' user.pk %}">ユーザー情報閲覧</a>
          </li>
          <li>
            <a class="nav-item nav-link" href="{% url 'register:user_update' user.pk %}">ユーザー情報更新</a>
          </li>
          <li>
            <a class="nav-item nav-link" href="{% url 'register:logout' %}">ログアウト</a>
          </li>
          {% else %}
          <li class="nav-item">
            <a class="nav-item nav-link" href="{% url 'register:login' %}">
            ようこそ、ゲスト!ログインはこちら
          </a>
          </li>
          {% endif %}
        </ul>
      </div>
    </nav>

ユーザー詳細テンプレート作成

user_detail.htmlとして作成します。

{% extends "register/base.html" %}
{% block content %}
<table class="table">
    <tbody>
        <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>
{% endblock %}

ユーザー情報更新テンプレート作成

user_form.htmlとして作成します。ユーザー情報更新ページです。

{% extends "register/base.html" %}
{% block content %}
<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 }} {{ field.errors }}</td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
    {% csrf_token %}
    <button type="submit" class="btn btn-success btn-lg" >送信</button>
</form>
{% endblock %}

この記事の関連記事

Djangoで会員登録機能を自作するシリーズ

2018-10-19 / PythonDjangoシリーズ・まとめ

- Djangoで会員登録機能を自作していきます。メールアドレスをユーザー名として使うようにし、ログイン画面、仮登録、メールクリックで本登録、ユーザー情報変更ページ、パスワード変更ページ、パスワードを忘れた際の再設定...などなど、よくある一連の機能を実装します。

コメント欄

記事にコメントする

名無し

はじめまして、初めてコメントさせていただきました。 いつもこのサイトで勉強させていただいています。

質問があるのですが、どのようにしたら1つのviewに複数のmodelからオブジェクトを表示させられますでしょうか。

具体的には、一覧表示画面にListViewで複数のモデルのオブジェクトを表示させたいのですが、model= で指定したモデルのオブジェクトしか反映されません。

ネットで調べたところ、

views.py

class BlogListView(ListView):

model = MyBlog
template_name = "blog_list.html"
def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    context.update({
        'tag_list': Tag.objects.order_by('name'),
        'more_context': Tag.objects.all(),
    })
    blog_list = MyBlog.objects.all().order_by('-publishing_date')[:5]
    return context
def get_queryset(self):
    return MyBlog.objects.order_by('publishing_date')

のようにすることで、tag_listとblog_listをfor文で回してテンプレートで表示できるようなのですが、試してもうまくいきません。

何か他の手段はありますでしょうか。

コメントに返信する

なりと

この場合はテンプレートでmyblog_listtag_listmore_contextをそれぞれ{% for %} で取り出せます。

mymodel_listではなくblog_listという名前で使いたい場合は以下のようにするか...

def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    context.update({
        'tag_list': Tag.objects.order_by('name'),
        'blog_list': MyBlog.objects.all().order_by('-publishing_date')[:5]
    })
    return context

もしくは、以下のようになります。

model = MyBlog
template_name = "blog_list.html"
context_object_name = 'blog_list'

def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    context.update({
        'tag_list': Tag.objects.order_by('name'),
    })
    return context

def get_queryset(self):
    return MyBlog.objects.all().order_by('-publishing_date')[:5]

素直に関数のビューで書いてしまうほうが簡単かもしれません。

関数ビューで書く場合

def blog_list_view(request):
    context = {
        'blog_list': MyBlog.objects.all().order_by('-publishing_date')[:5],
        'tag_list': Tag.objects.order_by('name'),
    }
    return render(request....)
タム

Udemyからこちらのブログで勉強させていただいております。 OneToOneで子モデルを作成するところまではできたのですが、更新処理がうまくいきません。 色々検索をして、インラインフォームなどを試したのですが手詰まりな状況です。 ざっくりとした内容で申し訳ないのですが、どうかよろしくお願いします。

コメントに返信する

なりと

もう少し詳しく教えていただけますか。

名無し

(※)すみません、自己解決しています。 OneToOneを使った親モデル(User)、子モデル(Profile)において、親モデルの更新はこちらのブログで紹介されていたので理解することができました。 ただ子モデルを更新したいと思った時に、ビュー関数やgeneric.UpdateViewを使っても親モデル、子モデル両方を更新するのが難しかったのです。 ビュー関数ですと、pkとの兼ね合いが、generic.UpdateViewだとclass Meta内のmodelフィールドが一つしか選べないのでどうしようかな、と思っていていました。 現在はforms.inlineformset_factoryを使って解決できています。