naritoブログ

このブログはPython/DjangoとBootstrap4で作成されました
Githubのソースコード

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

約109日前 2018年4月29日15:32
プログラミング関連
Django Bootstrap3 Python

概要


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

見た目


ナビバーに、閲覧と更新が増えました。
メニューに、ユーザー情報閲覧と更新が増えました

閲覧ページではユーザー情報の確認ができ...
閲覧ページは、ユーザー情報が表示されます。

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

自分以外のユーザーページにアクセスしようとすると、エラーです。
自分じゃないユーザーのページに行くと、403エラーです。

ソースコードと解説



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


増えたビューは3つです。うち1つはMixinです。
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.http import Http404
from django.shortcuts import redirect, resolve_url
from django.template.loader import get_template
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
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を作成するための関数はreverse、reverse_lazy、resolve_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 %}
名無し 約109日前 2018年4月29日17:40 返信する
このブログのおかげでdjango初心者の私にも、user登録関連のカスタマイズの仕方が少しずつわかってきました。ありがとうございます。
現在私は地図アプリを作成していて、そのアプリの中にMarkerというモデルを作りました。
このモデルは「緯度、経度、地名」などの情報を持っているのですが、さらにここに、訪れたユーザーの名前一覧を持たせたいと思っています。
ユーザーがその場所を訪れて何らかのボタンを押すとユーザー名が一覧に追加され、そうすることで、そのユーザーが一度訪れた場所かどうか判別できるような機能を付けたいのです。
最初空のリストをモデルに持たせて実現しようと思ったのですが、それではなんだか汚らしい気もします。(まずその方法でデータベースに値を格納できるのかもわかりません)
そこで質問なのですが、上記の機能を実装するために何かいい方法はあるでしょうか?

ちなみに地図上に配置されたマーカーは様々な観光スポットを表しており、ユーザーではなく管理者が管理画面から登録するものです。
なりと 約108日前 2018年4月30日12:04
Markerモデルに、
from django.conf import settings
...
...
class Marker(models.Model):
users = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True)

のように、Userと紐づくフィールドを作成するのがよさそうです。settings.AUTH_USER_MODELは、Userモデルを直接指定するよりも柔軟で、デフォルトのUserでもカスタムしたUserでも動作するようになります。

ビューでは、
marker = Marker.objects.get(pk=1)
marker.users.add(current_user)
のようにするとMakerにユーザーを追加できます。

また、そのユーザーが訪れたことのあるMarkerすべてを取得する場合は
user = self.request.user
user.maker_set.all()
のように書けます。
名無し 約107日前 2018年5月1日0:33 返信する
お忙しい中返信ありがとうございます。
実装の方向性が理解できました。
地図アプリでは、googlemapsAPIを使って、マーカーに設置されたボタンを押すと自分の現在位置とマーカーまでの距離を計算し、100m以内であれば「到達」したことになるようにしようと考えています。
ですがボタンが押されると、ページ遷移することなく「距離の計算」、「到達していれば名簿への登録」、「到達していなければポップアップの表示」といった処理が行われるようにするためには、どうすればいいのでしょうか?
自分では「マーカーの詳細ウィンドウの中にhtmlのボタンを設置」、「ボタンを押すとjavascriptの関数が実行される」、「なんとかしてjavascriptで算出した距離に応じて、真偽値をdjango側に渡す(???)」、「名簿への追加処理をpythonコードで実行」みたいなやり方を考えていますが、javascriptの知識もなく、もしこんな方法しか無いのであれば大変だなと辟易しています。

p.s.
なりとさんはudemyで講師もされているのですね、驚きました。
なりと 約107日前 2018年5月1日13:46 返信する
Markerやユーザー情報をDjango側で管理しているので、できるだけDjango側で処理をさせる機会を多くするのが管理としては楽になるように思います。
Ajaxを使えば、Markerの情報(pk等)や現在の位置情報をDjango側に渡すことができ、Django側で距離の計算や到達していれば名簿への登録を行えます。到達しているかどうか等の結果もJavaScriptで受け取ることができるので、それに応じてポップアップを表示できます。

処理の流れのイメージとしては以下のようになります。
マーカーの詳細ウィンドウの中にhtmlのボタンを設置

ボタンを押すとjavascriptの関数が実行される

AjaxでDjangoに処理を投げる

JavaScriptで結果を受け取り、ポップアップを表示したりする

JavaScriptのコードは必要になりますが、Ajaxを使ったコードはどれも似たようなコードになるので、そこまで複雑にはならないと思います。
名無し 約105日前 2018年5月3日15:14 返信する
前回のコメントをいただいてから、すぐにajaxについて調べて、見よう見真似ではありますがコードを書いてみました。
その中で2つ不明な点があったので質問させていただいてもいいでしょうか?

1つは、私はjavascriptを使ってマーカーを地図上に表示する際、マーカー一覧が記述されたjsonファイルを読み込ませています。そのjsonファイルは、マーカーが管理画面で追加されるごとに手動で更新しており、かなりの手間です。
Markerのsaveメソッドが実行されると同時に、「jsonファイルの末尾に新たな情報を追加する」などの処理も行うようにすることは可能でしょうか?

2つ目は、ユーザーがボタンを押した際、一つ一つのマーカーの緯度と経度を取り出す必要があるのですが、javascriptのforループでjsonからマーカーを生成して表示しているので、個別の情報をユーザーのタイミングで取り出すことができません。
マーカーから情報を取り出すには、マーカーを識別する値(例えばpk)などをjsonの情報に付け加えて、htmlのタグのidなどに書いておけば良いのでしょうか?
なりと 約103日前 2018年5月5日0:00
Django側でjsonを作成し、それを返すことができます。
from django.http import JsonResponse
response = JsonResponse(ここに、Makerから情報を取り出して入れる)
return response
といったビューを作成することができます。2番目の質問的に、ここでMakerのpkも一緒に格納させておくのが良さそうです。
名無し 約103日前 2018年5月5日6:39 返信する
親身に相談に乗ってくださり、有難うございました。
これからもブログの1読者として応援しています。
生徒 約103日前 2018年5月5日15:31 返信する
いつも楽しく拝見しています。
質問させて頂きます。
narito先生のviews.pyのコードを拝見すると、importされたclassが()で囲まれているものがあります。
例:from django.contrib.auth.forms import (
AuthenticationForm, UserCreationForm
)
この書き方を初めてみるのですが、この()にはどのような意味があるのでしょうか。
ご教授頂ければ嬉しく思います。
なりと 約103日前 2018年5月5日15:54
from a import A, B, C, D, E, F...のようにimportが長くなってしまう場合があります。そのままだと大変見づらいので、importを複数行に分けるための書き方になります。
生徒 約103日前 2018年5月5日21:25
このような書き方ができるとは知りませんでした。
ご回答ありがとうございました!
生徒 約72日前 2018年6月5日16:53 返信する
こんにちは。いつも楽しく拝見しています。
上記の記事のclass UserUpdateの改造を試みているのですが、
分からないことが出てきてしまいましたので、教えてください。

ユーザー情報をアップデートしたときに、djangoのmessage機能を使って、”アップデートに成功しました”というメッセージを表示させたいと考えています。
この思いを実現するために、以下のコードを書きました。

def get_success_url(self):
messages.success(request,'アップデートに成功しました。')
return resolve_url('authapp:user_detail',pk=self.kwargs['pk'])

しかし、上記のコードでは
add_message() argument must be an HttpRequest object, not 'module'.

というメッセージが表示されてしまいます。
関数の第2引数にrequestを加えてみたり、stackoverflowを見てrequestの部分を、self.request._requestなどに変えてみたのですが、うまくいきませんでした。
何かヒントだけでもよいので、解決策をご教示頂けませんでしょうか。
誠に恐れ入りますが、何卒宜しくお願い致します。
なりと 約72日前 2018年6月5日18:44
messages.success(self.request, 'アップデートに成功しました。')
とするとどうですか。
生徒 約71日前 2018年6月5日23:25
narito先生、大変申し訳ございません。
実装できました。
self.requestは試していませんでした。
こんな簡単に事に気づかないなんてとても恥ずかしいです。
誠に有難うございました。