【django】ajaxに対応した、ログインをチェックするmixinの作成

ajaxに対応した、ログインチェックをするdjangoのmixinを紹介します。また、それに加えて、スーパーユーザ以外からのアクセスを弾くmixinを紹介します。

ログインチェックをするmixin

ajaxに対応した、ログインチェックをするmixinを作成します。名前はLoginRequiredMixinとします。

デコレータの作成

まずは、ログインをチェックするデコレータを作成します。この関数はLoginRequiredMixinの中で定義してもいいのですが、スーパーユーザの方でも使いまわすためにLoginRequiredMixinの外に出しています。

このmixinでは、ユーザの状態が有効かどうか(user.is_activeがTrueかどうか)もチェックしています。

import ...


def login_required(view):
    """
    ログイン済みかをチェックします。
    また、is_activeがTrueであることも確認します。
    """
    @wraps(view)
    def inner(request, *args, **kwds):
        if not request.user.is_authenticated() or not request.user.is_active:
            # ログインしていない、またはユーザが無効になっている場合
            if request.is_ajax():
                # ajaxの場合、403を返します。
                return JsonResponse({'login_required': True}, status=403)
            else:
                # ajaxではない場合、ログイン画面にリダイレクトします。
                return redirect(settings.LOGIN_URL)
        return view(request, *args, **kwds)
    return inner

mixinの作成

デコレータを作成したら、後はそれを組み込んでLoginRequiredMixinを作成するだけです。

import ...


class LoginRequiredMixin(object):
    @classmethod
    def as_view(cls, **kwds):
        """ログイン済みかをチェックします。"""
        return login_required(super().as_view(**kwds))

スーパーユーザかをチェックするmixin

スーパーユーザでログインしているかをチェックをするmixinを作成します。名前はSuRequiredMixinとします。

デコレータの作成

スーパーユーザをチェックするデコレータを作成します。SuRequiredMixinの中で定義してもいいのでえすが、外に出した方がソースがわかりやすいと思うので、外に出しておきます。

import ...


def su_required(view):
    """
    ログイン済みで、かつスーパーユーザかをチェックするデコレータです。
    """
    @wraps(view)
    def inner(request, *args, **kwds):
        if not request.user.is_superuser:
            # スーパーユーザでない時は
            if request.is_ajax():
                # ajaxの場合、403を返します。
                return JsonResponse({'su_required': True}, status=403)
            else:
                # 「member_home」にリダイレクトします。
                return redirect(reverse('member_home'))
        return view(request, *args, **kwds)
    return inner

mixinの作成

作成したsu_requiredをmixinに組み込みます。

「login_requiredのチェック」 -> 「su_requiredのチェック」となるよう、デコレータをラップする順番を考慮しています。

import ...


class SuRequiredMixin(object):
    """
    管理者権限をチェックするMixinです。
    """
    @classmethod
    def as_view(cls, **kwds):
        """ログイン済みで、かつスーパーユーザかをチェックします。"""
        view = super().as_view(**kwds)
        # スーパーユーザのチェックよりも前に、
        # ログイン済みのチェックをするようにラップします。
        return login_required(su_required(view))

テストケース

今回作成したmixinのテストケースを紹介します。

テストケースの中で使用している「テスト用URLパターンを追加する方法」は、別の記事にまとめています。

# coding: utf-8
import json
from django.test import TestCase
from django.test.client import Client
from django.views.generic import View
from django.http import HttpResponse
from django.conf import settings
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from sample1.mixins import LoginRequiredMixin, SuRequiredMixin


class TestViewBase(TestCase):
    urls = 'myapp.test_urls'
    _mixin = None

    def setUp(self):
        super().setUp()
        self.client = Client()

    @classmethod
    def as_view(cls, **initkwds):
        cls = type('TestTempClass', (cls._mixin, View), {})
        cls.get = lambda self, request, *args, **kwargs: HttpResponse()
        cls.post = lambda self, request, *args, **kwargs: HttpResponse()
        return cls.as_view()

    def get(self, *args, **kwds):
        return self.request(*args, **kwds)

    def post(self, *args, **kwds):
        return self.request(is_post=True, *args, **kwds)

    def request(self, path, is_post=False, is_ajax=False, *args, **kwds):
        if is_ajax:
            kwds['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
        if is_post:
            return self.client.post(path, *args, **kwds)
        else:
            return self.client.get(path, *args, **kwds)


class TestLoginRequiredMixin(TestViewBase):
    # テスト対象のmixinを設定します。
    _mixin = LoginRequiredMixin

    def setUp(self):
        super().setUp()
        self.path = reverse('test_sample1_loginrequiredmixin')

    def test_logged_in(self):
        """ログイン済みのテストです。"""
        # ユーザを作成し、ログインします。
        username = 'foo'
        password = 'secret'
        User.objects.create_user(username=username, password=password)
        self.client.login(username=username, password=password)

        # テスト対象を実行します。
        res = self.get(self.path)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 200)

    def test_not_logged_in(self):
        """ログインしていない場合のテストです。"""
        # テスト対象を実行します。
        res = self.get(self.path)

        # テスト結果を確認します。
        self.assertRedirects(res, settings.LOGIN_URL)

    def test_not_active(self):
        """ユーザがアクティブでない場合のテストです。"""
        username = 'foo'
        password = 'secret'
        user = User.objects.create_user(username=username, password=password)
        self.client.login(username=username, password=password)
        user.is_active = False
        user.save()

        # テスト対象を実行します。
        res = self.get(self.path)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 302)

    def test_not_logged_in_ajax(self):
        """ログインしていない場合のテストです。(ajax)"""
        # テスト対象を実行します。
        res = self.get(self.path, is_ajax=True)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 403)
        data = json.loads(res.content.decode())
        self.assertTrue(data['login_required'])

    def test_not_active_ajax(self):
        """ユーザがアクティブでない場合のテストです。(ajax)"""
        username = 'foo'
        password = 'secret'
        user = User.objects.create_user(username=username, password=password)
        self.client.login(username=username, password=password)
        user.is_active = False
        user.save()

        # テスト対象を実行します。
        res = self.get(self.path, is_ajax=True)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 403)
        data = json.loads(res.content.decode())
        self.assertTrue(data['login_required'])


class TestSuRequiredMixin(TestViewBase):
    # テスト対象のmixinを設定します。
    _mixin = SuRequiredMixin

    def setUp(self):
        super().setUp()
        self.path = reverse('test_sample1_surequiredmixin')

    def test_su_logged_in(self):
        """スーパーユーザでログイン済みのテストです。"""
        # スーパーユーザを作成し、ログインします。
        username = 'foo'
        password = 'secret'
        User.objects.create_superuser(
            username=username, password=password, email='dummy')
        self.client.login(username=username, password=password)

        # テスト対象を実行します。
        res = self.get(self.path)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 200)

    def test_logged_in(self):
        """ログイン済みのテストです。"""
        # ユーザを作成し、ログインします。
        username = 'foo'
        password = 'secret'
        User.objects.create_user(username=username, password=password)
        self.client.login(username=username, password=password)

        # テスト対象を実行します。
        res = self.get(self.path)

        # テスト結果を確認します。
        self.assertRedirects(res, reverse('member_home'))

    def test_not_logged_in(self):
        """ログインしていない場合のテストです。"""
        # テスト対象を実行します。
        res = self.get(self.path)

        # テスト結果を確認します。
        self.assertRedirects(res, settings.LOGIN_URL)

    def test_logged_in_ajax(self):
        """ログイン済みのテストです。(ajax)"""
        # ユーザを作成し、ログインします。
        username = 'foo'
        password = 'secret'
        User.objects.create_user(username=username, password=password)
        self.client.login(username=username, password=password)

        # テスト対象を実行します。
        res = self.get(self.path, is_ajax=True)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 403)
        data = json.loads(res.content.decode())
        self.assertTrue(data['su_required'])

    def test_not_logged_in_ajax(self):
        """ログインしていない場合のテストです。(ajax)"""
        # テスト対象を実行します。
        res = self.get(self.path, is_ajax=True)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 403)
        data = json.loads(res.content.decode())
        self.assertTrue(data['login_required'])

LoginRequiredMixinとSuRequiredMixinのソース全体

import文を省略していない、ソース全体は以下になります。

# coding: utf-8
from functools import wraps
from django.conf import settings
from django.http import JsonResponse
from django.shortcuts import redirect
from django.core.urlresolvers import reverse


def login_required(view):
    """
    ログイン済みかをチェックします。
    また、is_activeがTrueであることも確認します。
    """
    @wraps(view)
    def inner(request, *args, **kwds):
        if not request.user.is_authenticated() or not request.user.is_active:
            # ログインしていない、またはユーザが無効になっている場合
            if request.is_ajax():
                # ajaxの場合、403を返します。
                return JsonResponse({'login_required': True}, status=403)
            else:
                # ajaxではない場合、ログイン画面にリダイレクトします。
                return redirect(settings.LOGIN_URL)
        return view(request, *args, **kwds)
    return inner


def su_required(view):
    """
    ログイン済みで、かつスーパーユーザかをチェックするデコレータです。
    """
    @wraps(view)
    def inner(request, *args, **kwds):
        if not request.user.is_superuser:
            # スーパーユーザでない時は
            if request.is_ajax():
                # ajaxの場合、403を返します。
                return JsonResponse({'su_required': True}, status=403)
            else:
                # 「member_home」にリダイレクトします。
                return redirect(reverse('member_home'))
        return view(request, *args, **kwds)
    return inner


class LoginRequiredMixin(object):
    @classmethod
    def as_view(cls, **kwds):
        """ログイン済みかをチェックします。"""
        return login_required(super().as_view(**kwds))


class SuRequiredMixin(object):
    """
    管理者権限をチェックするMixinです。
    """
    @classmethod
    def as_view(cls, **kwds):
        """ログイン済みで、かつスーパーユーザかをチェックします。"""
        view = super().as_view(**kwds)
        # スーパーユーザのチェックよりも前に、
        # ログイン済みのチェックをするようにラップします。
        return login_required(su_required(view))

ソースコードはgithub上にも公開してあります。

この記事が役に立った場合、シェアしていただけると励みになります!!