torinaブログ

DjangoとBootstrap4で作成したブログ
Python, Django, Kivy, Bootstrap, Apache等のメモです
ソースコード

Djangoで、ユーザ登録機能(汎用ビュー)

Python Django Bootstrap4 Django
2016年9月16日7:46
https://torina.top/main/271/
の内容を汎用ビューで書き直し、Bootstrap4を使ったサンプルです。

Django1.10
Python3.5

プロジェクト名は「testproject」
アプリケーション名は「testapp」

Django1.9で追加された機能なども使っており、少し古いのだと動かないです。

まずこのような画面


ログイン画面です。登録ページにも遷移できます。


ログインすると、少しナビバーの内容が変わりますね。


こちらはミニマイページ


会員登録画面はこんな感じに



testproject/testproject/settings.py
デフォルトの状態から、LOGIN_URLと、LOGIN_REDIRECT_URLと、APPSに足したぐらいです。
"""
Django settings for tes project.

Generated by 'django-admin startproject' using Django 1.10.

For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'aosk9amwy65*bhbmw#h63u(s!%--y#k3o7!$vvvo9o^wmaaap('

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ["*"]


# Application definition

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'testapp',
)

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'testproject.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'testproject.wsgi.application'


# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/

STATIC_URL = '/static/'

LOGIN_URL = "/login/"  # ログインするページ。デフォルトにするなら"/admin/login/"等も
LOGIN_REDIRECT_URL = '/'  # ログインページに直接飛んだとき、ログイン完了後のリダイレクト先



testproject/testproject/urls.py
いつもの
from django.conf.urls import include, url
from django.contrib import admin


urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^', include('testapp.urls', namespace='testapp')),
]


testproject/testapp/models.py
testproject/testapp/admin.py
特に書くものはありません。

testproject/testapp/urls.py
トップページ、マイページ、会員登録、ログイン、ログアウトの順です。
from django.conf.urls import url
from . import views


urlpatterns = [
    url(r'^$', views.TopPageView.as_view(), name='index'),
    url(r'^mypage/$', views.MyPageView.as_view(), name='mypage'),
    url(r'^create/$', views.CreateUserView.as_view(), name='create'),
    url(r'^login/$', views.login, name='login'),
    url(r'^logout/$', views.logout, name='logout'),
]


testproject/testapp/forms.py
以前とだいたい同じです。
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm


class RegisterForm(UserCreationForm):

    class Meta:
        model = User
        fields = (
            "username", "email", "password1", "password2",
            "first_name", "last_name",
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['username'].widget.attrs['class'] = 'form-control'
        self.fields['username'].widget.attrs['placeholder'] = 'ユーザ名'

        self.fields['email'].widget.attrs['class'] = 'form-control'
        self.fields['email'].widget.attrs['placeholder'] = 'メールアドレス'

        self.fields['first_name'].widget.attrs['class'] = 'form-control'
        self.fields['first_name'].widget.attrs['placeholder'] = '姓'

        self.fields['last_name'].widget.attrs['class'] = 'form-control'
        self.fields['last_name'].widget.attrs['placeholder'] = '名'

        self.fields['password1'].widget.attrs['class'] = 'form-control'
        self.fields['password1'].widget.attrs['placeholder'] = 'パスワード'

        self.fields['password2'].widget.attrs['class'] = 'form-control'
        self.fields['password2'].widget.attrs['placeholder'] = 'パスワード(確認)'


class LoginForm(AuthenticationForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['username'].widget.attrs['class'] = 'form-control'
        self.fields['username'].widget.attrs['placeholder'] = 'ユーザ名'

        self.fields['password'].widget.attrs['class'] = 'form-control'
        self.fields['password'].widget.attrs['placeholder'] = 'パスワード'



testproject/testapp/views.py
汎用ビューにしたことで、かなりコード量が減りました。
from django.contrib.auth import views as auth_views
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse_lazy
from django.views import generic

from .forms import RegisterForm, LoginForm


class TopPageView(generic.TemplateView):
    template_name = "testapp/index.html"


class MyPageView(LoginRequiredMixin, generic.TemplateView):
    template_name = "testapp/info.html"


class CreateUserView(generic.CreateView):
    template_name = 'testapp/create.html'
    form_class = RegisterForm
    success_url = reverse_lazy('testapp:index')


def login(request):
    context = {
        'template_name': 'testapp/login.html',
        'authentication_form': LoginForm
    }
    return auth_views.login(request, **context)


def logout(request):
    context = {
        'template_name': 'testapp/index.html',
    }
    returnauth_views.logout(request, **context)


viewsでimportできるようにしていますが、viewsだとちょっと紛らわしくなるので
auth_viewsという別名にしています。django.contrib.auth.viewsからとってるので、auth_viewsですね。
# これはちょっと紛らわしい...紛らわしくない?
from django.contrib.auth import views

# 別名にする
from django.contrib.auth import views as auth_views


実際にdjango.contrib.auth.viewsから使うのはloginとlogout関数なのですが、これを直接importするとこのモジュール内のloginとlogout関数と衝突します。
なので、auth_views.login等と使えるようにしているわけですね。
# これでもいいけど、今回はモジュール内のlogin、logout関数と衝突する
from django.contrib.auth.views import login, logout


これは単純にtestapp/index.htmlを見せるだけのViewです。
class TopPageView(generic.TemplateView):
    template_name = "testapp/index.html"


まずですが、LoginRequiredMixinを使っています。これはDjango1.9で追加されたもので、今回のように使うと
ログインしてなきゃ見せないよ!とすることができます。やり方は他にもあるのですが、今回はこの方法を使いました。
PermissionRequiredMixinというユーザの権限チェックなんかもあります。
class MyPageView(LoginRequiredMixin, generic.TemplateView):
    template_name = "testapp/info.html"


MyPageではシンプルなユーザ情報を表示するのですが、TemplateViewをそのまま使っています。
templateでは{{ user }}でログイン中のユーザオブジェクトにアクセスできるため、特に何もしなくてよいわけです。

CreateViewです。これはレコードを作成する際に使用します。
class CreateUserView(generic.CreateView):
    template_name = 'testapp/create.html'
    form_class = RegisterForm
    success_url = reverse_lazy('testapp:index')


これは、無事にデータを作成した後の遷移先です。
reverse_lazyは、URLの逆引きを行ってくれます。今回の例だと、"/"と書くのと同じ意味になります。
reverse_lazyを使うことで、urls.pyを変更してもこちらのコードをいじる必要はなくなります。
django.core.urlresolvers にreverseというのもあるのですが、reverseを使うとこの例はエラーになります。
プロジェクトの URLConf がロードされる前に、 URL 逆引きを使う必要がある 場合は、reverse_lazyを使います。とりあえずreverse_lazyを使っていれば問題ないです。
success_url = reverse_lazy('testapp:index')


今回のようにform_classにModelFormを渡すか、以下のようにModelとfieldsを指定するのが一般的な使い方です。
今回はformを色々カスタマイズしたので、フォームを渡していますが、場合によってはModelFormすら作らずにレコードが作成できます。
class CreateUserView(generic.CreateView):
    template_name = 'testapp/create.html'
    model = User
    fields = ("username", )
    success_url = reverse_lazy('testapp:index') 


ModelFormのように、全てのフィールドを指定する場合は__all__とします。
class CreateUserView(generic.CreateView):
    template_name = 'testapp/create.html'
    model = User
    fields = "__all__"
    success_url = reverse_lazy('testapp:index') 



汎用ビューはメソッドを適切にオーバーライドすることで、ふるまいを細かく変更できます。
今回のCreateViewの例であれば、例えばUserモデルのis_staffをTrueにしたい、ということであれば以下のようになります。
class CreateUserView(generic.CreateView):
    template_name = 'testapp/create.html'
    form_class = RegisterForm
    success_url = reverse_lazy('testapp:index') 

    def form_valid(self, form):
        user = form.save(commit=False)
        user.is_staff = True
        user.save()
        return super(CreateUserView, self).form_valid(form)


ログインと、ログアウトです。別名なのでauth_views.login等となっていますね。
https://torina.top/main/279/
でも似たようなことを書きました。
def login(request):
    context = {
        'template_name': 'testapp/login.html',
        'authentication_form': LoginForm
    }
    return auth_views.login(request, **context)


def logout(request):
    context = {
        'template_name': 'testapp/index.html',
    }
    returnauth_views.logout(request, **context)


urls.pyにて以下のようにしていると、viewを書く必要はなかったりします。
汎用ビューも、似たように書ける場合があります。
    url(
        '^login/$',
        auth_views.login,
        {'template_name': 'testapp/login.html', 'authentication_form': LoginForm}
    ),

    url(
        '^logout/$',
        auth_views.logout,
        {'template_name': 'testapp/index.html'}
    ),



testproject/testapp/templates/testapp/base.html
Bootstrap4を使っています。navbarに色の指定なども簡単にできるようになりました。
{% load static %}
<!DOCTYPE html>
<html lang="ja">
  <head>
    <!-- Required meta tags always come first -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="x-ua-compatible" content="ie=edge">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/css/bootstrap.min.css" integrity="2hfp1SzUoho7/TsGGGDaFdsuuDL0LX2hnUp6VkX3CUQ2K4K+xjboZdsXyp4oUHZj" crossorigin="anonymous">

    <!-- Custom CSS -->
    <link rel="stylesheet" href="{% static 'testapp/css/main.css' %}">

  </head>
  <body>
    <nav class="navbar navbar-dark bg-primary">
      <div class="container">
        <div class="nav navbar-nav">
          <a class="nav-item nav-link" href="{% url 'testapp:index' %}">トップ</a>
          
          {% if user.is_authenticated %}
          <div class="pull-sm-right">
            <a class="nav-item nav-link" href="{% url 'testapp:mypage' %}">
              ようこそ、{{ user.username }}!マイページへ
            </a>
            <a class="nav-item nav-link" href="{% url 'testapp:logout' %}">ログアウト</a>
          </div>
          
          {% else %}
          <a class="nav-item nav-link pull-sm-right" href="{% url 'testapp:login' %}">
            ようこそ、ゲスト!ログインはこちら
          </a>
          
          {% endif %}
        </div><!-- /.nav -->
      </div><!-- /.container -->
    </nav>

    {% block content %} {% endblock %}

    <!-- jQuery first, then Tether, then Bootstrap JS. -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js" integrity="sha384-THPy051/pYDQGanwU6poAc/hOdQxjnOEXzbT+OuUAFqNqFjL+4IGLBgCJC3ZOShY" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.2.0/js/tether.min.js" integrity="sha384-Plbmg8JY28KFelvJVai01l8WyZzrYWG825m+cZ0eDDS1f7d/js6ikvy1+X+guPIB" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.4/js/bootstrap.min.js" integrity="VjEeINv9OSwtWFLAtmc4JCtEJXXBub00gtSnszmspDLCtC0I4z4nqz7rEFbIZLLU" crossorigin="anonymous"></script>

    <!-- Custom JS -->
    <script src="{% static 'testapp/js/main.js' %}"></script>

  </body>
</html>


testproject/testapp/static/testapp/css/main.css
.center {
  padding: 3rem 1.5rem;
  text-align: center;
}

.errorlist li {
  list-style-type: none;
}
 
.errorlist {
  color: red;
  margin-left: 0;
  padding-left: 0;
}



testproject/testapp/templates/testapp/index.html
{% extends "testapp/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>トップページ</h1>
    <p class="lead">Bootstrap4を使っています。</p>
  </div>
</div><!-- /.container -->
{% endblock %}



testproject/testapp/templates/testapp/info.html
マイページ。{% verbatim %}{% endverbatim %}タグ内では、テンプレートエンジンによる解釈を行いません。
今回は{{ }}をHTMLに文字として出力したいので、使っています。
{% extends "testapp/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>ユーザ情報</h1>
    <p class="lead">ログイン中のユーザデータを表示します</p>
  </div>

  <table class="table">
    <thead class="thead-inverse">
      <tr>
        <th>名前・説明</th>
        <th>templateでの呼び出し方</th>
        <th>ログインユーザのデータ</th>
        <th>備考</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>ユーザ名</td>
        <td>{% verbatim %} {{ user.username }} {% endverbatim %} </td>
        <td>{{ user.username }}</td>
        <td>必須</td>
      </tr>
      <tr>
        <td>メール</td>
        <td>{% verbatim %} {{ user.email }} {% endverbatim %} </td>
        <td>{{ user.email }}</td>
        <td>デフォルトは空欄でもOK</td>
      </tr>
      <tr>
        <td>姓</td>
        <td>{% verbatim %} {{ user.first_name }} {% endverbatim %} </td>
        <td>{{ user.first_name }}</td>
        <td>デフォルトは空欄でもOK</td>
      </tr>
      <tr>
        <td>名</td>
        <td>{% verbatim %} {{ user.last_name }} {% endverbatim %} </td>
        <td>{{ user.last_name }}</td>
        <td>デフォルトは空欄でもOK</td>
      </tr>
      <tr>
        <td>adminサイトにアクセスできるか</td>
        <td>{% verbatim %} {{ user.is_staff }} {% endverbatim %} </td>
        <td>{{ user.is_staff }}</td>
        <td>デフォルトはFalse</td>
      </tr>
      <tr>
        <td>アカウントが有効か(ログインできるか)</td>
        <td>{% verbatim %} {{ user.is_active }} {% endverbatim %} </td>
        <td>{{ user.is_active }}</td>
        <td>デフォルトはTrue</td>
      </tr>
    </tbody>
  </table>

</div><!-- /.container -->
{% endblock %}




testproject/testapp/templates/testapp/login.html
panelはなくなりましたが、もっと汎用的に使えるcardというものが追加されています。便利っすよ
{% extends "testapp/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>ログインページ</h1>
  </div>

  <div class="card-group">
    <div class="card card-outline-primary">
      <div class="card-block">
        <form action="{% url 'testapp:login' %}" method="POST">
  		  <p>{{ form.non_field_errors }}</p>
	      {{ form.username }}
	      {{ form.password }}
	      {% csrf_token %}
	  	  <input type="hidden" name="next" value={{ next }} />
	      <button type="submit" class="btn btn-lg btn-block btn-outline-primary">
	        ログイン
	      </button>
	    </form>
      </div>
    </div>
    <div class="card card-outline-primary">
      <div class="card-block">
        <a class="btn btn-lg btn-block btn-outline-primary" href="{% url 'testapp:create' %}">
          会員登録
        </a>
      </div>
    </div>

  </div><!-- /.card-group -->
  
</div><!-- /.container -->
{% endblock %}




testproject/testapp/templates/testapp/create.html
{% extends "testapp/base.html" %}
{% block content %}
<div class="container">
  <div class="center">
    <h1>ユーザ登録</h1>
  </div>

  <form action="{% url 'testapp:create' %}" method="POST">
	<div class="row">
	  <div class="col-sm-10 offset-sm-1">
        <div class="card">
          <div class="card-block">

  			<div class="row">
  			  <div class="col-xs-12 col-sm-6">
  			    <div class="form-group">
  			  	  {{ form.username }}
  			  	  {{ form.username.errors }}
  			  	</div>
  			  </div>
   			  <div class="col-xs-12 col-sm-6">
   			    <div class="form-group">
  			  	  {{ form.email }}
  			  	</div>
  			  </div>
  			</div>
  			<div class="row">
  			  <div class="col-xs-12 col-sm-6">
  			    <div class="form-group">
  			  	  {{ form.password1 }}
  			  	  {{ form.password1.errors }}
  			  	</div>
  			  </div>
   			  <div class="col-xs-12 col-sm-6">
   			    <div class="form-group">
  			  	  {{ form.password2 }}
  			  	  {{ form.password2.errors }}
  			  	</div>
  			  </div>
  			</div>
    		<div class="row">
  			  <div class="col-xs-12 col-sm-6">
  			    <div class="form-group">
  			  	  {{ form.first_name }}
  			  	</div>
  			  </div>
   			  <div class="col-xs-12 col-sm-6">
   			    <div class="form-group">
  			  	  {{ form.last_name }}
  			  	</div>
  			  </div>
  			</div>
	        {% csrf_token %}
	        <button type="submit" class="btn btn-lg btn-block btn-outline-primary">会員登録</button>
          </div>
        </div>
      </div>
    </div>
  </form>

</div>
{% endblock %}


Bootstrap4は、
https://hackerthemes.com/bootstrap-cheatsheet/#pull-xs-right
のチートセットや、

公式
http://v4-alpha.getbootstrap.com/
のExsampleなんかを見て勉強してます。