Django
nginx
PostgreSQL
uwsgi
docker

Djangoの汎用ビューでユーザー管理用CRUDを実装する

前回までの続きです。環境、設定、手順等は以下を参照してください。
1. DockerによるDjango(1.11.2)の開発環境構築(Ubuntu:16.04 + Nginx + uWSGI + PostgreSQL)
2. Djangoでログイン認証用アプリケーションを作成
3. Djangoでユーザー登録機能を実装する1
4. Djangoでユーザー登録機能を実装する2


今回は、django.contrib.auth.forms より、UserCreationForm、UserChangeForm、PasswordChangeForm を継承したクラスを作成し、ビューでは汎用ビューを使って CRUD を実装していきます。


まず、accounts/forms.py を更新。

accounts/forms.py
import copy

from django import forms
from django.contrib.auth.forms import PasswordChangeForm, UserChangeForm, UserCreationForm
from django.utils.translation import ugettext_lazy as _

from django.contrib.auth.models import User


username = forms.RegexField(
    max_length=8,
    min_length=3,
    regex=r'^[a-z][a-zA-Z0-9]+$',
    error_messages={
        'invalid': _('先頭を小文字の半角英字から始めて、3〜8文字の半角英数字で入力してください。'),
    },
    widget=forms.TextInput(attrs={'placeholder': 'Username'}),
)

password = forms.RegexField(
    max_length=16,
    min_length=8,
    regex=r'^[a-zA-Z][a-zA-Z0-9]+$',
    error_messages={
        'invalid': _('先頭を半角英字から始めて、8〜16文字の半角英数字で入力してください。'),
    },
    widget=forms.PasswordInput,
)

email = forms.EmailField(
    required=True,
    widget=forms.TextInput(attrs={'placeholder': 'Email'}),
)

password1 = copy.deepcopy(password)
password1.widget.attrs['placeholder'] = 'Password'
password2 = copy.deepcopy(password)
password2.widget.attrs['placeholder'] = 'Password (again)'
new_password1 = copy.deepcopy(password)
new_password1.widget.attrs['placeholder'] = 'New Password'
new_password2 = copy.deepcopy(password)
new_password2.widget.attrs['placeholder'] = 'New Password (again)'
old_password = copy.deepcopy(password)
old_password.widget.attrs['placeholder'] = 'Old Password'

def email_duplicate_check(email):
    try:
        User._default_manager.get(email=email)
    except User.DoesNotExist:
        return email
    raise forms.ValidationError(
        '同じメールアドレスが既に登録済みです。'
    )


class CustomUserCreationForm(UserCreationForm):
    username = username
    password1 = password1
    password2 = password2
    email = email

    class Meta:
        model = User
        fields = (
            'username',
            'email',
        )

    def clean_email(self):
        email = self.cleaned_data['email']
        return email_duplicate_check(email)


class CustomUserChangeForm(UserChangeForm):
    username = username
    email = email

    class Meta:
        model = User
        fields = (
            'username',
            'password',
            'email',
        )

    def clean_email(self):
        user = self.instance      
        email = self.cleaned_data['email']
        if user.email == email:
            return email
        return email_duplicate_check(email)


class CustomPasswordChangeForm(PasswordChangeForm):
    new_password1 = new_password1
    new_password2 = new_password2
    old_password = old_password


accounts/views.py を更新。

汎用ビューには、ListView, DetailView、CreateView, UpdateView、DeleteView を、また、パスワード変更では django.contrib.auth.views.PasswordChangeView を使います。

accounts/views.py
from django.contrib.auth.models import User
from django.contrib.auth.views import PasswordChangeView
from django.views.generic import DetailView, ListView
from django.views.generic.edit import CreateView, DeleteView, UpdateView

from .forms import CustomPasswordChangeForm, CustomUserChangeForm, CustomUserCreationForm


# スーパーユーザーでログインすると、全ユーザーの「閲覧/更新/削除」が可能
def get_queryset_for_detail_edit_delete(self):
    if self.request.user.is_superuser:
        return User.objects.filter(pk=self.kwargs['pk'])
    else:
        return User.objects.filter(pk=self.request.user.id)


class IndexView(ListView):
    template_name = 'accounts/index.html'

    def get_queryset(self):
        if self.request.user.is_superuser:
            return User.objects.all().order_by('id')
        else:
            return User.objects.filter(pk=self.request.user.id)


class NewView(CreateView):
    model = User
    form_class = CustomUserCreationForm
    template_name = 'accounts/new.html'
    success_url = './login'


class DetailView(DetailView):
    template_name = 'accounts/detail.html'

    def get_queryset(self):
        return get_queryset_for_detail_edit_delete(self)


class EditView(UpdateView):
    form_class = CustomUserChangeForm
    template_name = 'accounts/edit.html'

    def get_queryset(self):
        return get_queryset_for_detail_edit_delete(self)

    def get_success_url(self):
        return '../' + self.kwargs['pk']


class CustomPasswordChangeView(PasswordChangeView):
    form_class = CustomPasswordChangeForm
    template_name = 'accounts/edit_password.html'

    def get_success_url(self):
        return '../' + str(self.request.user.id)


class DeleteView(DeleteView):
    template_name = 'accounts/user_confirm_delete.html'
    success_url = '../'

    def get_queryset(self):
        return get_queryset_for_detail_edit_delete(self)


テンプレートを作成。

# touch accounts/templates/accounts/{detail.html,edit.html,edit_password.html,user_confirm_delete.html}
accounts/templates/accounts/detail.html
{{ user.id }}<br>
{{ user.username }}<br>
{{ user.email }}<br>
{{ user.date_joined }}<br>
{{ user.last_login }}
accounts/templates/accounts/edit.html
<form method="post" action="./edit">
    {% csrf_token %}
    {{ form.non_field_errors }}
    {{ form.errors.username }}
    {{ form.username }}
    {{ form.errors.email }}
    {{ form.email }}
    <input type="submit" value="Update">
</form>
accounts/templates/accounts/edit_password.html
<form method="post" action="{% url 'accounts:edit_password' %}">
    {% csrf_token %}
    {{ form.non_field_errors }}
    {{ form.errors.new_password1 }}
    {{ form.new_password1 }}
    {{ form.errors.new_password2 }}
    {{ form.new_password2 }}
    {{ form.errors.old_password }}
    {{ form.old_password }}
    <input type="submit" value="Update Password">
</form>
accounts/templates/accounts/user_confirm_delete.html
<form method="post" action="./delete">
    {% csrf_token %}
    <p>Are you sure you want to delete "{{ object }}"?</p>
    <input type="submit" value="Confirm">
</form>


accounts/urls.py を更新。CustomPasswordChangeView については、スーパークラスのPasswordChangeView にて method_decorator を使って login_required が適用されているので、ここで login_required を記述する必要は無し。

accounts/urls.py
from django.conf.urls import url
from django.contrib.auth import views as auth_views
from django.contrib.auth.decorators import login_required

from . import views


app_name = 'accounts'
urlpatterns = [
    url(r'^$', login_required(views.IndexView.as_view()), name='index'),
    url(r'^new$', views.NewView.as_view(), name='new'),
    url(r'^(?P<pk>[0-9]+)$', login_required(views.DetailView.as_view()), name='detail'),
    url(r'^(?P<pk>[0-9]+)/edit$', login_required(views.EditView.as_view()), name='edit'),
    url(r'^password/edit$', views.CustomPasswordChangeView.as_view(), name='edit_password'),     # login_required は不要
    url(r'^(?P<pk>[0-9]+)/delete$', login_required(views.DeleteView.as_view()), name='delete'),
    url(r'^login$', auth_views.login, {'template_name': 'accounts/login.html'}, name='login'),
    url(r'^logout$', auth_views.logout, name='logout'),
]


uWSGI をリロード。

# touch reload.trigger


各処理のパスは以下。

パス 処理
/accounts/ ユーザー一覧表示
/accounts/new ユーザー新規登録
/accounts/:id 特定のユーザー表示
/accounts/:id/edit ユーザー編集
/accounts/password/edit パスワード変更
/accounts/:id/delete ユーザー削除

各ページ間のリンクの設定やリダイレクト先でのフラッシュメッセージなどは、適宜追加してください。

PUT や DELETE などを使い RESTful な設計にしたい場合は、Django REST framework を使うと楽に実装できます。こちらについては、別途書きます。

以上です。