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

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

Python - Django
2018年11月22日21:14に更新(約21日前)
2018年10月21日9:53に作成(約53日前)

旧ブログ移行記事です。

概要

Djangoで、会員登録機能を自作するシリーズの1つです。

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

見た目

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

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

更新ページでは、ユーザー情報の更新ができます。

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

ソースコードと解説

urls.py

2つ増えました。

    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

更新フォームは、単純にモデルフォームが使えます。通常のUserを使う場合に備え、少し処理があります。

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

    class Meta:
        model = User
        if User.USERNAME_FIELD == 'email':
            fields = ('email', 'first_name', 'last_name')
        else:
            fields = ('username', 'email', 'first_name', 'last_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 get_template
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

詳細ページは、シンプルです。デフォルトユーザーを使う場合に備え、ifが1つあるぐらいです。

{% extends "register/base.html" %}
{% block content %}
<table class="table">
    <tbody>
        <tr>
            <th>ユーザー名</th>
            <td>{{ user.username }}</td>
        </tr>
        {% if user.username != user.email %}
        <tr>
            <th>ユーザー名</th>
            <td>{{ user.email }}</td>
        </tr>
        {% endif %}
        <tr>
            <th>姓</th>
            <td>{{ user.last_name }}</td>
        </tr>
        <tr>
            <th>名</th>
            <td>{{ user.first_name }}</td>
        </tr>
    </tbody>
</table>
{% endblock %}

user_form.html

ユーザー情報更新ページです。tableタグを使っているぐらいですね。

{% 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 %}
Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする

名無し
2018年11月2日19:48にコメント(約41日前)

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

質問があるのですが、どのようにしたら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文で回してテンプレートで表示できるようなのですが、試してもうまくいきません。

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

コメントに返信する

なりと
2018年11月3日23:51に返信(約40日前)

この場合はテンプレートで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....)