3-2. ユーザープロフィール画面を作成する

今回のテーマは「ユーザープロフィール画面を作成する」です。ユーザー情報に関する画面はログインユーザーしかアクセスできない仕様を想定しています。ここではログインユーザーのみアクセスできるビューについて解説していきます。

※本ページは「ユーザー登録画面を作成する」まで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


LoginRequiredMixinとlogin_requiredデコレータ

Djangoにはログインユーザーしかアクセスできないする仕掛けとしてlogin_requiredデコレータとLoginRequiredMixinがあります。login_requiredデコレータは関数ベースビューに用います。LoginRequiredMixinはクラスベースビューで使用します。複数のクラスを継承する際にはLoginRequiredMixinは一番先に記載するように注意してください。login_requiredもしくはLoginRequiredMixinが付与されているビューにアクセスしようとするとログイン画面に飛ばされるようになっています。

LOGIN_URLについて

LoginRequiredMixinを継承したクラス(正確にはAccessMixin)を継承したクラスはlogin_urlを設定することが出来ます。これはログイン画面のURLです。もしこれが設定されていない場合はsettings.pyのLOGIN_URLが用いられます。デフォルトでは’/accounts/login/’です。
ちなみにlogin_requiredデコレータでは引数でlogin_urlを渡します。

ユーザープロフィール画面を作成する

まずはテンプレートを新規作成します。
templates/registration/profile.html



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a class="active section">ユーザー情報</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>ユーザー情報</h3></div>
                <div class="ui divided bulleted list">
                    <div class="item">ログインID:{{user.username}}</div>
                    <div class="item">E-mail:{% if user.email %}{{user.email}}{% else %}未設定{% endif %}</div>
                    <div class="item">名字:{% if user.last_name %}{{user.last_name}}{% else %}未設定{% endif %}</div>
                    <div class="item">名前:{% if user.first_name %}{{user.first_name}}{% else %}未設定{% endif %}</div>
                </div>
            </div>
        </div>
        <a class="ui button" href="">登録情報変更</a>
        <a class="ui button" href="">パスワード変更</a>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

次にビューを作成していきます。
accounts/views.py(一部抜粋)


from django.contrib.auth.mixins import LoginRequiredMixin

class UserProfileView(LoginRequiredMixin, TemplateView):
    template_name = 'registration/profile.html'
    def get_queryset(self):
        return User.objects.get(id=self.request.user.id)

先程説明したLoginRequiredMixinが使われていますね。ここではlogin_urlは設定していないので、もしログインしない状態で、このページにアクセスすると/accounts/login/にアクセスしようとしてエラーとなります。(現段階ではログイン画面作成していないため)

では作成したプロフィール画面にアクセスするためのURLを設定しましょう。
accounts/urls.py(一部抜粋)


  urlpatterns = [
      # path('', include('django.contrib.auth.urls')),
      path('create/', views.UserCreateView.as_view(), name="create"),
+     path('profile/', views.UserProfileView.as_view(), name="profile"),
  ]

ヘッダーのURL修正

画面のヘッダーの「ユーザーの情報」にURLを設定しましょう。



  {% load static %}
  <!DOCTYPE html>
  <head>
      <meta charset="UTF-8">
      <meta http-equiv="content-language" content="ja">
      <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
      {% block meta_tag %}{% endblock %}
      <link href="{% static 'css/semantic.css' %}" rel="stylesheet">
      {% block css %}{% endblock %}
      <title>
          {% block title %}IT学習ちゃんねる{% endblock %}
      </title>
  </head>
  <body>
      <div class="ui stackable inverted menu">
          <a href="{% url 'base:top' %}" class="header item">
              IT学習ちゃんねる
          </a>
          <a href="{% url 'base:about' %}" class="item">
              このサイトはなに?
          </a>
          <a class="item" href="{% url 'thread:create_topic' %}">
              トピック作成
          </a>
          <div class="right menu">
              {% if user.is_authenticated %}
-             <a class="item" href="">ユーザー情報</a>
+             <a class="item" href="{% url 'accounts:profile' %}">ユーザー情報</a>
              <a class="item" href="">ログアウト</a>
              {% else %}
              <a class="item" href="">ログイン</a>
              <a class="item" href="{% url 'accounts:create' %}">ユーザー登録</a>
              {% endif %}
          </div>
      </div>
      
      <div class="ui container" style="min-height:100vh;">
          {% block content %}
          {% endblock %}
      </div>
      <div class="ui inverted stackable footer segment">
          <div class="ui container center aligned">
              <div class="ui horizontal inverted small divided link list">
                  <a href="{% url 'base:top' %}" class="item">© 2019 IT学習ちゃんねる(仮)</a>
                  <a href="{% url 'base:terms' %}" class="item">利用規約</a>
                  <a href="{% url 'base:policy' %}" class="item">プライバシーポリシー</a>
              </div>
          </div>
      </div>
      <script src="https://code.jquery.com/jquery-3.1.1.js"></script>
      <script type="text/javascript" src="{% static 'js/semantic.js' %}"></script>
      {% block js %}{% endblock %}
  </body>

ユーザー登録後の遷移先を変更する

現在はユーザー登録後にトップページに遷移するようになっています。これを登録後にユーザープロフィール画面に遷移するように変更しましょう。

accounts/views.py(一部抜粋)


  class UserCreateView(FormView):
      form_class = UserCreationForm
      template_name = 'registration/create.html'
-     success_url = reverse_lazy('base:top')
+     success_url = reverse_lazy('accounts:profile')
      def form_valid(self, form):
          print(self.request.POST['next'])
          if self.request.POST['next'] == 'back':
              return render(self.request, 'registration/create.html', {'form': form})
          elif self.request.POST['next'] == 'confirm':
              return render(self.request, 'registration/create_confirm.html', {'form': form})
          elif self.request.POST['next'] == 'regist':
              form.save()
              # 認証
              user = authenticate(
                  username=form.cleaned_data['username'],
                  password=form.cleaned_data['password1'],
              )
              # ログイン
              login(self.request, user)
              return super().form_valid(form)
          else:
              # 通常このルートは通らない
              return redirect(reverse_lazy('base:top'))

では確認してみましょう。ユーザーを登録すると無事にプロフィール画面に遷移すればOKです。(現時点ではログインページがないのでログインしない状態でアクセスするとエラーとなります。)

最後に

今回は認証されたユーザーのみがアクセス出来るページについて見てきました。次回はログイン処理を実装してきます。

Sponsored Link


3-4. LogoutViewを使用してログアウト機能を実装する

今回のテーマは「LogoutViewを使用してログアウト機能を実装する」です。前回扱ったログイン処理と対となるログアウト処理を扱います。今回もDjangoに組み込まれたLogoutViewを使用することでコーディングの手間をかけずに実装していきます。

※本ページは「LoginViewを使用してログイン画面を作成する」まで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります


LogoutViewについて

django.contrib.auth.views.LogoutViewには以下のクラス変数があり、この値を継承クラスでオーバライドしたり、as_viewメソッドで設定することで制御することが出来ます。

  • next_page: ログアウト後のリダイレクト先URL。デフォルトはNone
  • redirect_field_name: POSTやGETでリダイレクト先を指定するときのキー。デフォルトはnext
  • template_name: テンプレート名。デフォルトはregistration/logged_out.html
  • extra_context: 追加コンテキスト。デフォルトはNone

今回はすべてデフォルトの状態で使ってみます。認証機能のカスタマイズは本章の後半で扱うので、それまでお待ち下さい。

ログアウト画面用のテンプレート作成

ではログアウト画面用のテンプレートを作成していきます。django.contrib.auth.views.LogoutViewのデフォルトテンプレートは’registration/logged_out.html’ですので、その名前で作成していきます。

templates/registration/logged_out.html



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a class="active section">ログアウト完了</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>ログアウトしました。</h3></div>
                <p><a href="{% url 'base:top' %}">TOPへ戻る</a></p>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

URLの確認

LoginViewを使用してログイン画面を作成するの「django.contrib.auth.urlsをインクルードする」は必ず設定しておいて下さい。

前回設定しているのでURLに関しては設定不要ですが、本ページから見た読者のためにmysite.pyを表示しておきます。

mysite/urls.py(一部抜粋)


urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('django.contrib.auth.urls')),
    path('accounts/', include('accounts.urls')),
    path('', include('base.urls')),
    path('thread/', include('thread.urls')),
    path('api/', include('api.urls')),
    path('search/', include('search.urls')),
    path('sitemap.xml', sitemap, {'sitemaps': sitemaps}),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

動作確認

では動作を確認してみましょう。
ログインした状態でヘッダーの「ログアウト」を押してログアウト画面に遷移すればOKをです。

ヘッダーのログアウトボタン押下

ログアウト画面に遷移

最後に

さてログイン、ログアウトについては実装出来ましたね。次回はパスワード変更機能を実装していきますよ。

Sponsored Link


3-3. LoginViewを使用してログイン画面を作成する

今回のテーマは「LoginViewを使用してログイン画面を作成する」です。前回まではビューを自作していましたが、今回はDjangoに組み込まれたLoginViewを用いてログイン機能を構築していきます。3章の初めでも書きましたが、ログイン機能はテンプレートを作るだけでスピーディに実現できます。

※本ページは「ユーザープロフィール画面を作成する」まで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


django.contrib.auth.urlsをインクルードする

Djangoの認証用のビューを使用する際のURLの設定で最も簡単なのはプロジェクト用のurls.py(今回のケースだとmysite/urls.py)にdjango.contrib.auth.urlsのurlpatternsをインクルードすることです。具体的にはmysite/urls.pyを以下のように変更します。


  urlpatterns = [
      path('admin/', admin.site.urls),
+     path('accounts/', include('django.contrib.auth.urls')),
      path('accounts/', include('accounts.urls')),
      path('', include('base.urls')),
      path('thread/', include('thread.urls')),
      path('api/', include('api.urls')),
      path('search/', include('search.urls')),
      path('sitemap.xml', sitemap, {'sitemaps': sitemaps}),
  ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

ではここでdjango.contrib.auth.urlsのurlpatternsの中身を見てみましょう。

django/contrib/auth/urls.py(一部抜粋)


from django.contrib.auth import views
from django.urls import path

urlpatterns = [
    path('login/', views.LoginView.as_view(), name='login'),
    path('logout/', views.LogoutView.as_view(), name='logout'),

    path('password_change/', views.PasswordChangeView.as_view(), name='password_change'),
    path('password_change/done/', views.PasswordChangeDoneView.as_view(), name='password_change_done'),

    path('password_reset/', views.PasswordResetView.as_view(), name='password_reset'),
    path('password_reset/done/', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
    path('reset///', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
    path('reset/done/', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]

このurlspatternsをmysite/urls.pyにインクルードすることで以下のように各URLで各ビューにアクセスすることにあります。

/accounts/login/ -> LoginView ログイン機能
/accounts/logout/ -> LogoutView ログアウト機能
/accounts/password_change/ -> PasswordChangeView パスワード変更機能
/accounts/password_reset/ -> PasswordResetView パスワードリセット機能
※他は省略

テンプレートの作成

今回はビューに関しては組込みのLoginViewを使用するのでテンプレートを作成しましょう。registration/login.htmlを作成します。さて、ここで何故templatesディレクトリ内にaccountsディレクトリではなくregistrationディレクトリを作成したか説明します。LoginViewのクラス変数であるtemplate_nameのデフォルト値は’registration/login.html’なのです。他のLogoutViewのテンプレートのデフォルト値は’registration/logout.html’であり、他も同様です。よって統一するためにURLが/accounts/でアクセスするビューに関してはregistrationに統一しました。

templates/registration/login.html



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a class="active section">ログイン</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>ログイン</h3></div>
                <form class="ui form" action="" method="POST">
                    {% csrf_token %}
                    {{form.as_p}}
                    <button class="ui orange button" type="submit">ログイン</button>
                </form>
            </div>
        </div>
        <a class="ui item" href="{% url 'accounts:create' %}">ユーザー登録</a>/
        <a class="ui item" href="{% url 'password_reset' %}">パスワードを忘れた場合</a>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

それでは画面のヘッダーのURLも修正しておきましょう。
templates/registration/login.html



    <div class="ui stackable inverted menu">
        <a href="{% url 'base:top' %}" class="header item">
            IT学習ちゃんねる
        </a>
        <a href="{% url 'base:about' %}" class="item">
            このサイトはなに?
        </a>
        <a class="item" href="{% url 'thread:create_topic' %}">
            トピック作成
        </a>
        <div class="right menu">
            {% if user.is_authenticated %}
            <a class="item" href="{% url 'accounts:profile' %}">ユーザー情報</a>
            <a class="item" href="">ログアウト</a>
            {% else %}
-           <a class="item" href="">ログイン</a>
+           <a class="item" href="{% url 'login' %}">ログイン</a>
            <a class="item" href="{% url 'accounts:create' %}">ユーザー登録</a>
            {% endif %}
        </div>
    </div>


ログイン画面へのURLに関して”{% url ‘login’ %}”ですね。’accounts:login’ではないことに注意して下さい。django/contrib/auth/urls.pyではapp_nameを設定していないので、このURLのショートカットは’login’となります。

では掲示板にアクセスして確認してみましょう。localhost:8080/accounts/login/にアクセスしてログイン画面から作成したユーザーでログインできればOKです。

ログイン後の遷移先は?

さて、無事ユーザー情報ページに遷移しましたか?ところで1つの疑問が湧きます。なぜユーザー情報ページに遷移したのでしょうか?LoginViewはGETもしはPOSTで遷移先のURLが与えられた場合はそのURLに遷移し、特に指定がなければsettings.LOGIN_REDIRECT_URLに遷移します。このLOGIN_REDIRECT_URLのデフォルト値は’/accounts/profile/’なのです。よって先に作成しておいたユーザー情報ページに遷移シました。尚、GET/POSTでURLを与える時はパラメータのキーはデフォルトで”next”です。これはLoginViewのクラス変数であるredirect_field_nameで変更できます。

最後に

今回はLoginViewを中心に見てきました。次回はログアウト処理を見ていきます。

Sponsored Link


3-1. ユーザー登録画面を作成する

今回のテーマは「ユーザー登録画面を作成する」です。まずはユーザー登録画面を作成しましょう。これらの画面(ビュー)はDjangoでは用意されていませんので自作する必要があります。もうDjangoに慣れてきた皆さんなら簡単ですよね?

※本ページは第一章、第二章の続きとして書かれています。省略されたソースコードやプロジェクトの説明がある可能性があります。ご了承下さい。


accountsアプリケーションの作成

まずはユーザー情報を扱うaccountsアプリケーションを作成しましょう。


(venv)$ ./manage.py startapp accounts

例によってaccounts/urls.pyも作成しておきます。

accounts/urls.py


from django.urls import path, include
from . import views

app_name = 'accounts'

urlpatterns = [
]

ひとまずurlpatternsはから配列で作成しておきます。

また、mysite/settings.pyとmysite/urls.pyにも追記をしておきます。
mysite/settings.py(一部抜粋)


  INSTALLED_APPS = [
      'django.contrib.admin',
      'django.contrib.auth',
      'django.contrib.contenttypes',
      'django.contrib.sessions',
      'django.contrib.messages',
      'django.contrib.staticfiles',
      'django.contrib.sites',
      'django.contrib.sitemaps',
      'debug_toolbar',
      'base',
      'thread',
      'api',
      'search',
+     'accounts',
  ]

mysite/settings.py(一部抜粋)


  urlpatterns = [
      path('admin/', admin.site.urls),
+     path('accounts/', include('accounts.urls')),
      path('', include('base.urls')),
      path('thread/', include('thread.urls')),
      path('api/', include('api.urls')),
      path('search/', include('search.urls')),
      path('sitemap.xml', sitemap, {'sitemaps': sitemaps}),
  ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

UserCreationFormについて

今回はDjangoに備わっているUserCreationFormを使ってユーザー登録画面を作成していきます。このUserCreationFormはDjango標準の認証機能として搭載されているUserモデルの作成フォームです。Userをカスタマイズする場合はUserCreationFormもカスタマイズする必要が出てきますのでご注意下さい。

UserCreationFormに関しては公式ドキュメントのDjangoの認証システムを使用するをご覧ください。

ユーザー登録画面の作成

まずはユーザー登録画面を作成しましょう。特に難しいことはありません。FormViewを継承したクラスを作り、トピック登録画面と同様に確認画面付きの画面を作成していきます。ただし、今回は単にユーザーを登録するだけではなく、登録と同時に認証してログインする処理も行うことにします。まずテンプレートを用意しましょう。

template/registration/create.html



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a class="active section">ユーザー作成</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>ユーザー作成</h3></div>
                <form class="ui form" action="" method="POST">
                    {% csrf_token %}
                    {{form.as_p}}
                    <button class="ui orange button" name="next" value="confirm" type="submit">確認</button>
                </form>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

template/registration/create_confirm.html



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a class="active section">ユーザー作成</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>ユーザー作成</h3></div>
                <table  class="ui celled table table table-hover" >
                    <tr><td>お名前</td><td>{{form.cleaned_data.username}}</td></tr>
                    <tr><td>パスワード</td><td>********</td></tr>
                </table>
                <form class="ui form" action="" method="POST">
                    {% csrf_token %}
                    {% for field in form %}
                    {{field.as_hidden}}
                    {% endfor %}
                    <button class="ui button" name="next" value="back" type="submit">修正</button>
                    <button class="ui orange button" name="next" value="regist" type="submit">登録</button>
                </form>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

ここまでは特に問題ないと思います。ではビューを作っていきましょう。

accounts/views.py


from django.shortcuts import render, redirect
from django.views.generic import TemplateView, FormView
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.contrib.auth import login, authenticate

# Create your views here.

class UserCreateView(FormView):
    form_class = UserCreationForm
    template_name = 'registration/create.html'
    success_url = reverse_lazy('base:top')
    def form_valid(self, form):
        print(self.request.POST['next'])
        if self.request.POST['next'] == 'back':
            return render(self.request, 'registration/create.html', {'form': form})
        elif self.request.POST['next'] == 'confirm':
            return render(self.request, 'registration/create_confirm.html', {'form': form})
        elif self.request.POST['next'] == 'regist':
            form.save()
            # 認証
            user = authenticate(
                username=form.cleaned_data['username'],
                password=form.cleaned_data['password1'],
            )
            # ログイン
            login(self.request, user)
            return super().form_valid(form)
        else:
            # 通常このルートは通らない
            return redirect(reverse_lazy('base:top'))

簡単に解説しますと、POSTで受ける’next’パラメータの値によって挙動がコントロールされているのは問題ないと思います。この仕組みはトピック登録画面と同様です。問題はform_classで設定されているUserCreationFormですね。これはDjangoで用意されたフォームでユーザー登録用のフォームです。今回はこのフォームクラスをそのまま使いました。もし、カスタマイズが必要な場合はUserCreationFormを継承したクラスを作成すれば良いということになります。次に、登録処理のところですね。form.save()メソッドを呼んでユーザー情報を登録してUser情報を登録しています。次にauthenticate関数を呼んで認証処理を行っています。authenticate関数が呼ばれると認証用のバックエンドのリストが呼び出されて順番に認証バックエンドで認証できるかが試されていきます。無事に認証が通れば認証バックエンドと紐づけされたユーザーが返される仕組みです。このユーザーをlogin関数で処理することでセッションにユーザーデータをもたせてログイン処理を行っています。

ではこのビューにアクセスするURLを作成しましょう。
accounts/urls.py


from django.urls import path, include
from . import views

app_name = 'accounts'

urlpatterns = [
    path('create/', views.UserCreateView.as_view(), name="create"),
]

早速確認したいところですが、ここでユーザー登録してログインした際にヘッダー部分の表示が変わるように変更しておきましょう。templates/base/base.htmlを修正します。



  {% load static %}
  <!DOCTYPE html>
  <head>
      <meta charset="UTF-8">
      <meta http-equiv="content-language" content="ja">
      <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
      {% block meta_tag %}{% endblock %}
      <link href="{% static 'css/semantic.css' %}" rel="stylesheet">
      {% block css %}{% endblock %}
      <title>
          {% block title %}IT学習ちゃんねる{% endblock %}
      </title>
  </head>
  <body>
      <div class="ui stackable inverted menu">
          <a href="{% url 'base:top' %}" class="header item">
              IT学習ちゃんねる
          </a>
          <a href="{% url 'base:about' %}" class="item">
              このサイトはなに?
          </a>
          <a class="item" href="{% url 'thread:create_topic' %}">
              トピック作成
          </a>
-         <div class="right menu">
-             <a class="item" href="">ログイン</a>
-             <a class="item" href="">ユーザー登録</a>
-         </div>
+         <div class="right menu">
+             {% if user.is_authenticated %}
+             <a class="item" href="">ユーザー情報</a>
+             <a class="item" href="">ログアウト</a>
+             {% else %}
+             <a class="item" href="">ログイン</a>
+             <a class="item" href="{% url 'accounts:create' %}">ユーザー登録</a>
+             {% endif %}
+         </div>
      </div>
      
      <div class="ui container" style="min-height:100vh;">
          {% block content %}
          {% endblock %}
      </div>
      <div class="ui inverted stackable footer segment">
          <div class="ui container center aligned">
              <div class="ui horizontal inverted small divided link list">
                  <a href="{% url 'base:top' %}" class="item">© 2019 IT学習ちゃんねる(仮)</a>
                  <a href="{% url 'base:terms' %}" class="item">利用規約</a>
                  <a href="{% url 'base:policy' %}" class="item">プライバシーポリシー</a>
              </div>
          </div>
      </div>
      <script src="https://code.jquery.com/jquery-3.1.1.js"></script>
      <script type="text/javascript" src="{% static 'js/semantic.js' %}"></script>
      {% block js %}{% endblock %}
  </body>

ポイントは条件分岐で使用されているuser.is_authenticated関数ですねこれはユーザーが認証されているかを判定する関数です。ユーザーがログインしてに称された状態であれば「ユーザー情報」と「ログアウト」を表示するようにしています。ユーザー登録だけは画面が作成されていますのでURLを設定します。

それでは確認してみましょう。ユーザーを登録してTOPページに遷移してヘッダーの表示が変化すればOKです。
まずはヘッダーの「ユーザー登録」からアクセス

ユーザーを作成

完了するとヘッダーの表示が変化する。(ログイン済み)

最後に

今回は会員サイトを作る上で必須となる会員登録ページの作り方でした。UserCreationFormを使うことで余分な手間なく出来たと思います。次回は登録したユーザーの情報を表示するページを作っていきます。

Sponsored Link


2-12. タイムゾーンと日時オブジェクトを扱う

今回のテーマは「タイムゾーンと日時オブジェクトを扱う」です。いよいよ第二章も最終回です。もうDjnagoの特徴や使い方が大分見えてきましたよね。今回はデータベースに保存されている日時を利用して現在時刻との差分を計算していきます。今回扱う範囲は公式ドキュメントのタイムゾーンに詳細説明があります。


日時オブジェクトのnativeとawareについて

settings.pyにてUSE_TZ = Trueにしている場合タイムゾーンがサポートされます。この状態ではDjangoはタイムゾーンを認識する日時オブジェクト(awareな日時オブジェクト)を使用します。本サイトのようにstartprojectコマンドによってプロジェクトを生成した場合は初期設定としてUSE_TZ=Trueの設定になっています。よって現状ではawareな日時オブジェクトを使用してきました。タイムゾーンが有効となっている場合、データベースにはUTCで日時を保存しています。Djangoはテンプレートやフォーム等で表示する際に、設定されたタイムゾーンで変換をしています。

現在時刻の取得に関して

pythonで現在時刻を取得する際にdatetime.datetime.now()と覚えている方も多いのではないでしょうか?この方法ではnativeの日時オブジェクトが取得されるため、awareな日時オブジェクトとの比較は出来ません。データベースから取得した日時オブジェクトと現在時刻を比較するにはどうしたら良いのでしょうか?Djangoにはdjango.utils.timezoneモジュールがあります。このモジュールのnow関数を使うことで現時刻の日時オブジェクトを適当なモードで取得することが出来ます。now関数の中身を見てみましょう。

django/utils/timezone.py


def now():
    """
    Return an aware or naive datetime.datetime, depending on settings.USE_TZ.
    """
    if settings.USE_TZ:
        # timeit shows that datetime.now(tz=utc) is 24% slower
        return datetime.utcnow().replace(tzinfo=utc)
    else:
        return datetime.now()

このようにUSE_TZによって返すオブジェクトを変えています。タイムゾーンが無効(USE_TZ=False)の場合にはnativeの日時オブジェクトが返されるのが理解できると思います。

よってDjango内で現在時刻を取得する際にはtimezoneモジュールをインポートしてnow関数を呼び出すのが良いでしょう。

データベース

簡単にデータベースについて触れておきたいと思います。筆者としてはタイムゾーンは常に有効にして使用した方が良いと考えていますが、タイムゾーンの有効・無効を切り替える場合もあると思います。PostgreSQLはタイムゾーン情報をデータベースに保存しているために、タイムゾーンの有効・無効は自由に切り替えられます。しかしそれ以外のデータベースに関してはタイムゾーンがを無効に切り替えた場合にはUTCからネイティブなdatetimeに変換する必要があります。(参考:公式ドキュメント

現在時刻との差分を計算してNEWラベルをつける

さて、今回は演習として一時間以内に新しく作成されたトピックはトップページにNEWラベルを表示するようにしましょう。トップページを表示しているビューはbase/views.pyのTopicListViewクラスですので、ここに手を加えていきます。

base/views.py(一部抜粋)


from django.utils import timezone

class TopicListView(ListView):
    template_name = 'base/top.html'
    # model = Topic
    queryset = Topic.objects.order_by('-created')
    context_object_name = 'topic_list'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.new_list = []

    def get_queryset(self):
        topic_list = Topic.objects.order_by('-created')
        self.new_list = self._make_new_list(topic_list)
        return topic_list

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['new_list'] = self.new_list
        return ctx

    def _make_new_list(self, topic_list):
        def pickup_topic(topic):
            now = timezone.now()
            diff = (now - topic.created).total_seconds() / (60 * 60)
            if diff > 1:
                return False
            else:
                return True
        return list(map(lambda x : x.id ,filter(pickup_topic, topic_list)))

解説は不要かと思いますが、現在時刻をtimezone.now()で取得してデータベースに格納されているtopic.createdと差を求めて秒を時間に変換しています。この時間が1時間以下のトピックのIDのみのリストを作成して返していますね。コンテキストにnew_listを渡すためnew_listをインスタンス変数として渡しています。

ラベルを表示するようにテンプレートも変更しましょう。

templates/base/top.html(一部抜粋)



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a class="active section">TOP</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>新着スレッド</h3></div>
                <div class="ui divided items">
                    {% for topic in topic_list %}
                    <div class="item">
                        <div class="content">
                            <div class="header">
                                <a href="{% url 'thread:topic' pk=topic.id %}">
                                    <h4>
                                        {% if topic.id in new_list %}
                                        <div class="ui violet horizontal label">new</div>
                                        {% endif %}
                                        {{topic.title}}
                                    </h4>
                                </a>
                            </div>
                            <div class="meta">
                                <span class="name">{{topic.user_name}}</span>
                                <span class="date">{{topic.created}}</span>
                            </div>
                        </div>
                    </div>
                    {% endfor %}
                </div>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

全て掲載しましたが、修正点はトピックタイトルの部分のみです。

では確認してみましょう。新規にトピックを作成するとNEWラベルがつきますね。

最後に

ここまで、Djangoの機能をつまみ食いしながら紹介してきました。少々無理のある機能もあり、掲示板というお題で始めてしまってよかったのか悩む場面もありましたが、何とかそれなりの機能を有した掲示板になってきたのではないでしょうか?次章からはDjangoの認証機能を利用して活きます。ここまで作成した掲示板を会員サイトに修正していく予定です。好ご期待!

Sponsored Link


2-11. 画像アップロード機能を付与する

今回のテーマは「画像アップロード機能を付与する」です。掲示板に画像のアップロード機能をつけることでファイルの扱いについて見ていきましょう。全てのファイルの種類を扱うのは難しいので画像データのアップロードという点に焦点を当てて見ていきたいと思います。

※本ページはCookieへのデータの保存と読み出しまで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


MEDIA_ROOTの設定

Djangoでファイルを扱うにはMEDIA_ROOT変数でファイルを取り扱うディレクトリを指定する必要があります。このディレクトリはDjangoの実行ユーザーの書き込み権限がある必要があります。今回はmysiteプロジェクト内にmediaディレクトリを作成することにします。少々ややこしいのでディレクトリ構成を図示します。


mysite
│└mysite
│─base
│─thread
│─search
│─static
└─media

(venv)$ cd mysite #プロジェクトディレクトリ
(venv)$ mkdir -p media

このmediaディレクトリを認識させるためにmysite/settings.pyに以下を追記します。

mysite/settings.py(一部抜粋)


+ MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
+ MEDIA_URL = '/media/'

MEDIA_URL変数はWEBアプリでファイルを指し示すURLのルートとなります。今回は’/media/’としましたが、他のフレーズでもOKです。
次にmysite/urls.pyの設定をします。urlpatterns変数を以下の様に変更します。

mysite/urls.py(一部抜粋)


+ from django.conf.urls.static import static
  urlpatterns = [
      path('admin/', admin.site.urls),
      path('accounts/', include('django.contrib.auth.urls')),
      path('', include('base.urls')),
      path('thread/', include('thread.urls')),
      path('api/', include('api.urls')),
      path('search/', include('search.urls')),
      path('sitemap.xml', sitemap, {'sitemaps': sitemaps}),
- ]
+ ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

モデルの作成

今回はFileFieldを継承したImageFieldを使っていきます。ImageFieldを使う場合はPillowが必要になるためインストールします。


(venv)$ pip install pillow

コメント投稿時に画像をアップロードできるようにするためthread/models.pyを修正していきます。
thread/models.py(Commentクラス関連部分のみ)


class CommentManager(models.Manager):
    # Comment操作に関する処理を追加
    def create_comment(self, user_name, message, topic_id, image=None):
        comment = self.model(
            user_name=user_name,
            message=message,
            image=image
        )
        comment.topic = Topic.objects.get(id=topic_id)
        comment.no = self.filter(topic_id=topic_id).count() + 1
        comment.save()

class Comment(models.Model):
    id = models.BigAutoField(
        primary_key=True,
    )
    no = models.IntegerField(
        default=0,
    )
    user_name = models.CharField(
        'お名前',
        max_length=30,
        null=True,
        blank=False,
    )
    topic = models.ForeignKey(
        Topic,
        on_delete=models.PROTECT,
    )
    message = models.TextField(
        verbose_name='投稿内容'
    )
    image = models.ImageField(
        verbose_name='投稿画像',
        validators=[FileExtensionValidator(['jpg', 'png'])],
        upload_to='images/%Y/%m/%d/',
        null=True,
        blank=True,
    )
    pub_flg = models.BooleanField(
        default=True,
    )
    created = models.DateTimeField(
        auto_now_add=True,
    )
    objects = CommentManager()

    def __str__(self):
        return '{}-{}'.format(self.topic.title, self.no)

併せてビューも修正しておきます。

thread/views.py(一部抜粋)


  class TopicViewAndCommentCreateView(FormView):
      template_name = 'thread/detail_topic.html'
      form_class = CommentModelForm
      
      def form_valid(self, form):
          Comment.objects.create_comment(
              user_name=form.cleaned_data['user_name'],
              message=form.cleaned_data['message'],
              topic_id=self.kwargs['pk'],
+             image=form.cleaned_data['image']
          )
          response = super().form_valid(form)
          return response
  
      def get_success_url(self):
          return reverse_lazy('thread:topic', kwargs={'pk': self.kwargs['pk']})
      
      def get_context_data(self):
          ctx = super().get_context_data()
          ctx['topic'] = Topic.objects.get(id=self.kwargs['pk'])
          ctx['comment_list'] = Comment.objects.filter(
                  topic_id=self.kwargs['pk']).annotate(vote_count=Count('vote')).order_by('no')
          return ctx

ではテンプレートを修正していきましょう。templates/thread/detail_topic.htmlは以下のように修正されます。validatorsで拡張子によるバリデーション処理を行うように指定しています。特定の拡張子しか受け付けたくないときには便利です。upload_toにはmediaディレクトリ内のアップロードファイルを指定します。%Yのような指定子を用いることで日付や時間をディレクトリ名とすることも出来ます。今回は画像なしでもコメント投稿できるようにするためにnull,blankはTrueとしています。

templates/thread/detail_topic.html



  {% extends 'base/base.html' %}
  {% block title %}{{topic.title}} - {{ block.super }}{% endblock %}
  {% block content %}
  {% load threadfilters %}
  {% load static %}
  <div class="ui grid stackable">
      <div class="eleven wide column">
          <div class="ui breadcrumb">
              <a href="{% url 'base:top' %}" class="section">TOP</a>
              <i class="right angle icon divider"></i>
              <a href="{% url 'thread:category' url_code=topic.category.url_code %}" class="section">{{topic.category.name}}</a>
              <i class="right angle icon divider"></i>
              <a class="active section">{{topic.title}}</a>
          </div>
          <div class="ui segment">
              <div class="content">
                  <div class="header"><h3>{{topic.title}}</h3></div>
                  <p>{{topic.user_name}} - {{topic.created}}</p>
                  <div class="ui segment">
                      <p><pre>{{topic.message}}</pre></p>
                  </div>
              </div>
          </div>
          <!--コメント表示-->
          <div class="ui segment">
              {% if comment_list %}
              {% for comment in comment_list %}
              <div class="ui segment secondary">
                  <p>{{comment.no}}. {{comment.user_name}}<br>{{comment.created}}</p>
                  {% if comment.pub_flg %}
                  <p>{{comment.message | comment_filter | safe}}</p>
+                     {% if comment.image %}
+                     <a href="{{comment.image.url}}" target="_blank" rel="noopener noreferrer"><img src="{{comment.image.url}}" width=200px></a>
+                     {% endif %}
                  <div class="vote_button ui right aligned vertical segment" style="cursor: pointer;"
                      data-comment-id="{{comment.id}}" data-count="{{comment.vote_count}}">
                      <i class="heart outline icon"></i>
                      <span class="vote_counter">
                          {% if comment.vote_count > 0 %}{{comment.vote_count}}{% endif %}
                      </span>
                  </div>
                  {% else %}
                  <p style="color: grey">コメントは非表示とされました</p>
                  {% endif %}
              </div>
              {% endfor %}
              {% else %}
              <div class="ui warning message"><p>まだコメントはありません</p></div>
              {% endif %}
          </div>
          <!--//コメント表示-->
          <!--コメント投稿-->
          <h4>コメント投稿</h4>
          <div class="ui segment">
-             <form class="ui form" action="" method="POST">
+             <form class="ui form" action="" method="POST" enctype="multipart/form-data">
                  {% csrf_token %}
                  {{form.as_p}}
                  <button class="ui button orange" type="submit">コメント投稿</button>
              </form>
          </div>
          <!--//コメント投稿-->
      </div>
      {% include 'base/sidebar.html' %}
  </div>
  {% endblock %}
  {% block js %}
  <script src="{% static 'js/vote.js' %}" type='text/javascript'></script>
  {% endblock %}

まず、投稿したファイルをimgタグで呼び出しています。ファイルのpathはimageのurl属性で取得することができます。MEDIA_ROOTでしたいした’/media/’を含むpathが返されます。

注意点ですが、formタグのenctypeを”multipart/form-data”としないとファイルが送信出来ませんので忘れず修正して下さい。

実は画像をDjangoで扱う場合には上記だけでは不十分なことが多く、画像表示が重たくならないようにサムネイルやサイズごとの画像を用意する等の工夫がされることが多いのですが、サードパーティの機能の解説になることもあり、別の機会にできればと考えています。

では、コメント投稿時に画像が投稿できるか確かめてみましょう。コメント投稿欄の「投稿画像」部分のボタンを押すと画像選択用のウィンドウが開きます。(ブラウザに酔って挙動は異なります)

そのまま投稿するとコメント欄に画像が表示されます。

最後に

今回は基本的なファイルの扱いについて見てきました。実際はdjango-imagekitライブラリ等を使用することが多いと思いますが、それについては別の機会にしたいと思います。

Sponsored Link


2-10. Cookieへのデータの保存と読み出し

今回のテーマは「Cookieへのデータの保存と読み出し」です。前回セッションでのデータの扱いを見てきました。今回はCookieのデータ保存とデータの読み出しを見ていきます。

※本ページはセッションへのデータの保存と読み出しまで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


Cookieにデータを保管することについて

Djangoに限らない話ですがクッキーはユーザーサイドでデータの操作が可能である点、クッキーが盗難にあう可能性があることを考慮し、セキュリティ重要なデータや個人情報などを保存してはいけません。あくまでページを跨ぐ情報の一時的な保管場所として利用するのが望ましいかと考えています。

Cookieへ値をセットと取得

HttpResposeクラスのset_cookieメソッドを利用してセットします。ここでは例としてトピック作成した際にカテゴリーIDを保存してみます。あまりいい例が思いうかばず申し訳ないです。次回のトピック作成時には前回作成したカテゴリーが予め選択されているようにします。thread/views.pyの先程作成したTocicCreateViewBySessionクラスを修正します。
thread/views.py(一部抜粋)


  class TocicCreateViewBySession(FormView):
      template_name = 'thread/create_topic.html'
      form_class = TopicModelForm
  
      def post(self, request, *args, **kwargs):
          ctx = {}
          if request.POST.get('next', '') == 'back':
              if 'input_data' in self.request.session:
                  input_data = self.request.session['input_data']
                  form = TopicModelForm(input_data)
                  ctx['form'] = form
              return render(request, self.template_name, ctx)
          elif request.POST.get('next', '') == 'create':
              if 'input_data' in request.session:
                  Topic.objects.create_topic(
                      title=request.session['input_data']['title'],
                      user_name=request.session['input_data']['user_name'],
                      category_id=request.session['input_data']['category'],
                      message=request.session['input_data']['message']
                  )
                  # メール送信処理は省略
+                 response = redirect(reverse_lazy('base:top'))
+                 response.set_cookie('categ_id', request.session['input_data']['category'])
+                 request.session.pop('input_data') # セッションに保管した情報の削除
+                 return response
          elif request.POST.get('next', '') == 'confirm':
              form = TopicModelForm(request.POST)
              if form.is_valid():
                  ctx = {'form': form}
                  # セッションにデータを保存
                  input_data = {
                      'title': form.cleaned_data['title'],
                      'user_name': form.cleaned_data['user_name'],
                      'message': form.cleaned_data['message'],
                      'category': form.cleaned_data['category'].id,
                  }
                  request.session['input_data'] = input_data
                  ctx['category'] = form.cleaned_data['category']
                  return render(request, 'thread/confirm_topic.html', ctx)
              else:
                  return render(request, self.template_name, {'form': form})
          
+     def get_context_data(self):
+         ctx = super().get_context_data()
+         if 'categ_id' in self.request.COOKIES:
+             form = ctx['form']
+             form['category'].field.initial = self.request.COOKIES['categ_id']
+             ctx['form'] = form
+         return ctx

簡単に解説します。set_cookieメソッドはHttpResposeの属性です。なのでredirect関数で生成されたHttpResponseオブジェクトresponseからset_cookieメソッドを呼びます。引数にキーとバリューを入れて設定しています。有効期限を引数に入れることで有効期限の設定も出来ます。

セットした値を取得するにはrequest.COOKIESにアクセスすることで取得できます。今回はget_context_data関数の中でCookieの値を取得してカテゴリーの初期値を設定する操作を行っています。

最後に

ちょっと強引な例となってしまいましたが、Cookieの使用方法を紹介しました。次回はファイルを扱う方法を見ていきます。

Sponsored Link


2-9. セッションへのデータの保存と読み出し

今回のテーマは「セッションへのデータの保存と読み出し」です。掲示板アプリもそれっぽくなってきましたね。あと少し頑張りましょう。

今回はセッションの使い方を学ぶための実装を例示しますが、学習用に用意したため掲示板の機能にはあまり影響がありません。予めご了承下さい。

※本ページはDjangoでメールを送信するまで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


セッション

セッションについては公式ドキュメントも参照してください。Djangoのセッションは完全にクッキーベースでありセッションIDをURLとして渡す等の手法は用いません。また、セッションとはブラウザが起動中のみ有効でブラウザと閉じると切れるものと考えている方もいるかも知れませんが、Djangoの標準設定ではセッションはブラウザを閉じても生き続ける永続的なもので、意図的に削除されるか、有効期限(標準設定では2週間)となるまで削除されません。もちろん、ブラウザを閉じたらセッションも切れるようにも設定出来ます。

セッションを使ったトピック作成画面

確認画面付きのトピック作成画面を作るで作成したトピック作成画面をセッションを用いる方式に修正してみましょう。トピック作成画面はユーザー作成画面→確認画面→(トピック作成処理)→TOP画面と遷移していきます。現行処理は確認画面にhiddenのフォームを入れることでユーザー入力画面の情報をトピック作成処理に渡すことにしていました。今回はセッションを使ってこの処理を実装してみます。

まずthread/views.pyに新しいクラスを作りましょう。以下のようなクラスを生成します。
thread/views.py(一部抜粋)


class TocicCreateViewBySession(FormView):
    template_name = 'thread/create_topic.html'
    form_class = TopicModelForm

    def post(self, request, *args, **kwargs):
        ctx = {}
        if request.POST.get('next', '') == 'back':
            if 'input_data' in self.request.session:
                input_data = self.request.session['input_data']
                form = TopicModelForm(input_data)
                ctx['form'] = form
            return render(request, self.template_name, ctx)
        elif request.POST.get('next', '') == 'create':
            if 'input_data' in request.session:
                Topic.objects.create_topic(
                    title=request.session['input_data']['title'],
                    user_name=request.session['input_data']['user_name'],
                    category_id=request.session['input_data']['category'],
                    message=request.session['input_data']['message']
                )
                request.session.pop('input_data') # セッションに保管した情報の削除
                # メール送信処理は省略
                return redirect(reverse_lazy('base:top'))
        elif request.POST.get('next', '') == 'confirm':
            form = TopicModelForm(request.POST)
            if form.is_valid():
                ctx = {'form': form}
                # セッションにデータを保存
                input_data = {
                    'title': form.cleaned_data['title'],
                    'user_name': form.cleaned_data['user_name'],
                    'message': form.cleaned_data['message'],
                    'category': form.cleaned_data['category'].id,
                }
                request.session['input_data'] = input_data
                ctx['category'] = form.cleaned_data['category']
                return render(request, 'thread/confirm_topic.html', ctx)
            else:
                return render(request, self.template_name, {'form': form})

templates/thread/confirm_topic.html内でhiddenのインプットタグを削除します。
templates/thread/confirm_topic.html(差分のみ)



{% extends 'base/base.html' %}
{% block title %}トピック作成 - {{ block.super }}{% endblock %}
{% block content %}
  <div class="ui grid stackable">
      <div class="eleven wide column">
          <div class="ui breadcrumb">
              <a href="{% url 'base:top' %}" class="section">TOP</a>
              <i class="right angle icon divider"></i>
              <a class="active section">トピック作成</a>
          </div>
          <div class="ui segment">
              <div class="content">
                  <div class="header"><h3>トピック作成</h3></div>
                  <p>内容を確認してください</p>
                  <table  class="ui celled table table table-hover" >
                      <tr><td>タイトル</td><td>{{form.title.value}}</td></tr>
                      <tr><td>お名前</td><td>{{form.user_name.value}}</td></tr>
                      <tr><td>カテゴリー</td><td>{{category}}</td></tr>
                      <tr><td>本文</td><td><pre>{{form.message.value}}</pre></td></tr>
                  </table>
                  <form class="ui form" action="{% url 'thread:create_topic' %}" method="POST">
                      {% csrf_token %}
-                     {% for field in form %}
-                         {{field.as_hidden}}
-                     {% endfor %}
                      <button class="ui button grey" type="submit" name="next" value="back">戻る</button>
                      <button class="ui button orange" type="submit" name="next" value="create">作成</button>
                  </form>
              </div>
          </div>
      </div>
      {% include 'base/sidebar.html' %}
  </div>
{% endblock %}

thread/urls.pyも変更します。
thread/urls.py(一部抜粋)


+ path('create_topic/', views.TocicCreateViewBySession.as_view(), name='create_topic'),
- path('create_topic/', views.TocicCreateView.as_view(), name='create_topic'),

これで最初に作成した確認画面付きのトピック作成画面と同等の機能をセッションを使って実装することが出来ました。セッションは基本的にはビュー内部で扱うことが多く、request.sessionに対してキーと値を持たせることで保存します。セッションエンジンにはDB,ファイル, キャッシュがありますが、今回はデフォルト設定されているDBを用いました。変更したい場合には公式ドキュメントのセッションエンジンを設定するを参考にして下さい。

セッションをビュー外部から使用することも出来ます。その場合はビューの外でセッションを使うを参考にして下さい。

最後に

セッションは会員制のサイトなどでユーザー情報を保持しておく際によく用いられます。次回はCookieの扱いについて見ていきたいと思います。

Sponsored Link


2-8. Djangoでメールを送信する

今回のテーマは「Djangoでメールを送信する」です。ウェブアプリケーションではメールの送信は良くあるアクションの1つですね。今回はDjangoの機能を用いてメール送信する処理を見ていこうと思います。

公式ドキュメントの該当箇所としてはメールを送信するに該当します。

※本ページはサイトマップを作成するまで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


メールバックエンドについて

メールの送信処理はメールバックエンドによって処理されます。メールバックエンドにはいくつか種類があり、目的に応じて使わけます。まず、開発時に試験的に(実際にメールを送信せず)送信テストをしたい場合はコンソールバックエンドの使用をオススメします。これはメールをコンソールに出力するのみで、送信しません。またファイルバックエンドもファイルに出力するのみで実際には送信しません。メールバックエンドは独自のものを使用することもできますが多くの場合はSMTPバックエンドを使用する機会が多いと思いますので、今回は開発環境ではコンソールバックエンド、送信用にはSMTPバックエンドを使用していきます。

メールバックエンドの設定

まずはmysite/settings.pyにて設定を行います。

mysite/settings.py(一部抜粋)


+ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

まずは開発用にコンソールバックエンドを指定しました。ではトピックの追加された場合にメールを送信する処理を行いましょう。

thread/views.py(一部抜粋)


+ from django.core.mail import send_mail, EmailMessage

  class TopicCreateView(CreateView):
      template_name = 'thread/create_topic.html'
      form_class = TopicModelForm
      model = Topic
      success_url = reverse_lazy('base:top')
  
      def form_valid(self, form):
          ctx = {'form': form}
          if self.request.POST.get('next', '') == 'confirm':
              ctx['category'] = form.cleaned_data['category']
              return render(self.request, 'thread/confirm_topic.html', ctx)
          elif self.request.POST.get('next', '') == 'back':
              return render(self.request, 'thread/create_topic.html', ctx)
          elif self.request.POST.get('next', '') == 'create':
+             # メール送信処理
+             send_mail(
+                 subject='トピック作成: ' + form.creaned_data['title'],
+                 message='トピックが生成されました。',
+                 from_email='hogehoge@example.com',
+                 recipient_list = [
+                     'admin@example.com',
+                 ]
+             )
              return super().form_valid(form)
          else:
              # 正常動作ではここは通らない。エラーページへの遷移でも良い
              return redirect(reverse_lazy('base:top'))

ここで用いたsend_mail関数はEmailMessageのラッパーで、単順なメール送信では重宝します。タイトル、本文、送信アドレス、受診アドレスを設定します。ではトピック作成してメールがコンソールに出力されるか見てみましょう。

出力例


Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
Subject:
 =?utf-8?b?44OI44OU44OD44Kv5L2c5oiQOiDntKDmlbXjgarjg4vjg6Pjg7PjgrPjga7kuJY=?=
 =?utf-8?b?55WM?=
From: hogehoge@example.com
To: admin@example.com
Date: Wed, 20 Mar 2019 07:58:17 -0000
Message-ID: <155306869746.10086.12768691481838639735@arch.localdomain>

トピックが生成されました。
-------------------------------------------------------------------------------

このように出力されます。

テンプレートを使う
多くのウェブサービスではメール用のテンプレートを用意しておいて変数化された部分のみ変更してメール送信する処理も多いと思います。直接のメール機能というわけではないですが、紹介しておきたいと思います。まずはテンプレートファイルを生成します。

templates/thread/mail/topic_mail.html


以下のトピックが登録されました。

---------------------
タイトル: {{title}}
ユーザー名: {{user_name}}
本文:
{{message}}

thread/views.py(一部抜粋)


+ from django.core.mail import send_mail, EmailMessage
+ from django.template.loader import get_template

  class TopicCreateView(CreateView):
      template_name = 'thread/create_topic.html'
      form_class = TopicModelForm
      model = Topic
      success_url = reverse_lazy('base:top')
  
      def form_valid(self, form):
          ctx = {'form': form}
          if self.request.POST.get('next', '') == 'confirm':
              ctx['category'] = form.cleaned_data['category']
              return render(self.request, 'thread/confirm_topic.html', ctx)
          elif self.request.POST.get('next', '') == 'back':
              return render(self.request, 'thread/create_topic.html', ctx)
          elif self.request.POST.get('next', '') == 'create':
+             # メール送信処理
+             template = get_template('thread/mail/topic_mail.html')
+             mail_ctx={
+                 'title': form.cleaned_data['title'],
+                 'user_name': form.cleaned_data['user_name'],
+                 'message': form.cleaned_data['message'],
+             }
+             send_mail(
+                 subject='トピック作成: ' + form.cleaned_data['title'],
+                 message=template.render(mail_ctx),
+                 from_email='hogehoge@example.com',
+                 recipient_list = [
+                     'admin@example.com',
+                 ]
+             )
              return super().form_valid(form)
          else:
              # 正常動作ではここは通らない。エラーページへの遷移でも良い
              return redirect(reverse_lazy('base:top'))

このようにテンプレートのレンダリングを用いることでコンテキストをテンプレートに渡してメール本文を作成することができます。
[出力例]


Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
Subject:
 =?utf-8?b?44OI44OU44OD44Kv5L2c5oiQOiDntKDmlbXjgarjg6/jg7PjgrPjga7kuJbnlYw=?=
From: hogehoge@example.com
To: admin@example.com
Date: Wed, 20 Mar 2019 08:09:26 -0000
Message-ID: <155306936665.10237.4864318700173116195@arch.localdomain>

以下のトピックが登録されました。

---------------------
タイトル: 素敵なワンコの世界
ユーザー名: 名無し
本文:
ようこそ。ワンコの世界へ
-------------------------------------------------------------------------------

EmeilMessageオブジェクトを使用してメールを送信する

冒頭で書いた通りsend_mail関数はEmailMessageのラッパーです。CCやBCCを使う等の複雑な処理はEmailMessageオブジェクトを使用します。
thread/views.py(一部抜粋)


+ from django.core.mail import send_mail, EmailMessage

  class TopicCreateView(CreateView):
      template_name = 'thread/create_topic.html'
      form_class = TopicModelForm
      model = Topic
      success_url = reverse_lazy('base:top')
  
      def form_valid(self, form):
          ctx = {'form': form}
          if self.request.POST.get('next', '') == 'confirm':
              ctx['category'] = form.cleaned_data['category']
              return render(self.request, 'thread/confirm_topic.html', ctx)
          elif self.request.POST.get('next', '') == 'back':
              return render(self.request, 'thread/create_topic.html', ctx)
          elif self.request.POST.get('next', '') == 'create':
+             # メール送信処理
+             template = get_template('thread/mail/topic_mail.html')
+             mail_ctx={
+                 'title': form.cleaned_data['title'],
+                 'user_name': form.cleaned_data['user_name'],
+                 'message': form.cleaned_data['message'],
+             }
+             EmailMessage(
+                 subject='トピック作成: ' + form.cleaned_data['title'],
+                 body=template.render(mail_ctx),
+                 from_email='hogehoge@example.com',
+                 to=['admin@example.com'],
+                 cc=['admin2@example.com'],
+                 bcc=['admin3@example.com'],
+             ).send()
              return super().form_valid(form)
          else:
              # 正常動作ではここは通らない。エラーページへの遷移でも良い
              return redirect(reverse_lazy('base:top'))

トピックを追加してみましょう。

[出力例]


Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
Subject:
 =?utf-8?b?44OI44OU44OD44Kv5L2c5oiQOiDntKDmmbTjgonjgZfjgY1weXRob27jga7kuJY=?=
 =?utf-8?b?55WM?=
From: hogehoge@example.com
To: admin@example.com
Cc: admin2@example.com
Date: Wed, 20 Mar 2019 08:29:19 -0000
Message-ID: <155307055904.10506.3851232136327226257@arch.localdomain>

以下のトピックが登録されました。

---------------------
タイトル: 素晴らしきpythonの世界
ユーザー名: 名無し
本文:
ようこそ。pythonの世界へ
-------------------------------------------------------------------------------

SMTPバックエンドでメールを送信する

ではSMTPバックエンドによるメール送信を見ていきたいと思います。まず、メールサーバーの設定をmysite/settings.pyに追加します。今回は擬似的にMailCatcherを用いてメールの受信を体験します。MailCatcherの設定についてはDebian 9にMailCatcherを導入するを参考にして下さい。

mysite/settings.py(一部抜粋)


- EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+ EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
+ 
+ EMAL_USE_TLS = False
+ EMAIL_PORT = 1025
+ EMAIL_HOST = '192.168.0.10'
+ EMAIL_HOST_USER = ''
+ EMAIL_HOST_PASS = ''

これでトピックを追加してみます。MailCatcherにメールが送られました。

最後に

いかがだったでしょうか。SMTPバックエンドを使用すればGmail経由でもメールを送ることができ、個人のウェブサービスでもメールを手軽に使うことが出来ると思います。是非活用してみて下さい。

Sponsored Link


2-7. サイトマップを作成する

今回のテーマは「サイトマップを自動作成する」です。SEO対策として検索エンジンにウェブサイトの更新を通知するためサイトマップを用意したいケースも多いと思います。今回はDjangoを用いてサイトマップを動的に生成する方法を見ていきます。

今回の内容は公式ドキュメントではThe sitemap frameworkの部分に相当します。

※本ページはページネーションを使うまで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。


準備

まずsettings.pyを変更する必要があります。
mysite/settings.py


  INSTALLED_APPS = [
      'django.contrib.admin',
      'django.contrib.auth',
      'django.contrib.contenttypes',
      'django.contrib.sessions',
      'django.contrib.messages',
      'django.contrib.staticfiles',
+     'django.contrib.sites',
+     'django.contrib.sitemaps',
      'debug_toolbar',
      'base',
      'thread',
      'api',
      'search',
  ]
  
+ SITE_ID = 1

次にデータベースのマイグレーションをします。


(venv)$ ./manage.py migrate

管理画面にアクセスしてドメイン名と表示名を変更します。今回はmydomain.comに修正しました。このサイトオブジェクトのIDが先程settings.pyで設定したSITE_IDと一致している必要があります。

Sitemap継承クラスを作る

ではサイトマップを作っていきましょう。DjangoにおいてサイトマップはSitemapクラスを継承したクラスにて作成します。thread/sitemaps.pyを作成します。尚、sitemaps.pyはDjangoで決まっているファイルではありませんので別の名前でもいいですし、他のファイルに書いても構いません。

thread/sitemaps.py


from django.contrib.sitemaps import Sitemap
from django.shortcuts import resolve_url

from . models import Topic, Category

class TopicSitemap(Sitemap):
    priority = 0.5
    changefreq = 'always'
    
    def items(self):
        return Topic.objects.all()

    def location(self, obj):
        return resolve_url('thread:topic', pk=obj.id)
    
class CategorySitemap(Sitemap):
    priority = 0.5
    changefreq = 'never'

    def items(self):
        return Category.objects.all()

    def location(self, obj):
        return resolve_url('thread:category', url_code=obj.url_code)

Sitemapクラスを継承したクラスを作成しています。このクラスではサイトマップを作成に必要な情報をクラスの属性として与えます。この際、静的な情報はクラス変数、動的な情報は関数を使って指定出来るようになっています。

指定できる属性は以下です。

items [必須]ページのオブジェクト。イテレータブルなオブジェクトを指定する
location [オプション]itemsのURL
lastmod [オプション]ページ更新日時
changefreq [オプション]ページの更新頻度
priority [オプション]ページの重要度0〜1
limit [オプション]ページネーションの区切り。デフォルトは50000
protocol [オプション]httpプロトコル。デフォルトはhttp
i18n [オプション]URL表示に設定言語を適用するか True/False

まず、items()メソッドでトピックのリストを与えています。この関数が返すイテレータブルなitemsの1つ1つに対してサイトマップが生成されます。ではこのitemsの要素であるitemのURLのを指定するためにlocation関数でURLを指定しましょう。トピックのURLは動的に変更するのでlocationは関数で与えます。このようにitemsで指定したリストの1要素を仮引数とする関数を作ることで動的なサイトマップを生成できます。

カテゴリー用のサイトマップも同様です。CategorySitemapクラスを作成して要素を指定しています。カテゴリーの場合はurl_codeを指定することに注意して下さい。

では、次に静的なページである利用規約やプライバシーポリシーについて見ていきましょう。これらのページは動的要素がないために非常に簡単です。base/sitemaps.pyを生成しましょう。

base/sitemaps.py


from django.contrib.sitemaps import Sitemap
from django.shortcuts import resolve_url

class BaseSitemap(Sitemap):

    def items(self):
        items = [
            'base:top',
            'base:policy',
            'base:terms',
        ]
        return items

    def location(self, obj):
        return resolve_url(obj)

    def changefreq(self, obj):
        if obj == 'base:top':
            return 'always'
        return 'never'

    def priority(self, obj):
        if obj == 'base:top':
            return 0.8
        return 0.1

基本的な考え方はthread/sitemaps.pyと同じです。ただitemsで’base:policy’のようにページのショートカット名でリストを生成することで効率的にサイトマップを生成することができます。上記でクラス変数として指定した属性についても関数で指定しています。両者を比べると理解がより進むのではないでしょうか。

URLの設定

では、作成したサイトマップを表示してみましょう。ここから先はmysite/urls.pyを変更していきます。
mysite/urls.py


  from django.contrib import admin, auth
  from django.urls import path, include
  from django.conf import settings
  from django.contrib.sitemaps.views import sitemap
  
+ from thread.sitemaps import TopicSitemap, CategorySitemap
+ from base.sitemaps import BaseSitemap
  
+ sitemaps = {
+     'topic': TopicSitemap,
+     'cateogry': CategorySitemap,
+     'base': BaseSitemap,
+ }
  
  urlpatterns = [
      path('admin/', admin.site.urls),
      path('accounts/', include('django.contrib.auth.urls')),
      path('', include('base.urls')),
      path('thread/', include('thread.urls')),
      path('api/', include('api.urls')),
      path('search/', include('search.urls')),
+     path('sitemap.xml', sitemap, {'sitemaps': sitemaps}),
  ]
  
  if settings.DEBUG:
      import debug_toolbar
      urlpatterns = [
          path('__debug__/', include(debug_toolbar.urls)),
      ] + urlpatterns

このように各アプリケーションでSitemap継承クラスを作成し、urls.pyでまとめるというのはDjangoのルールではないですが、分かりやすいので筆者は気に入っています。

では確認してみましょう。localhost:8080/sitemap.xmlにアクセスします。

最後に

Djangoのサイトマップ生成機能を使うと簡単に動的なサイトマップを作成することができますね。今回は基礎的な部分に着目したので、テンプレートをカスタマイズしたいという場合には公式ドキュメントを呼んでみて下さい。

Sponsored Link