もた日記

くだらないことを真面目にやる

Djangoメモ(25) : login_requiredデコレータでビューをログイン済みユーザーのみに制限

Python 3.6.4 Django 2.0.2

A Complete Beginner's Guide to Djangoのチュートリアルを参考にビューをログイン済みユーザのみに制限してみる。


login_requiredデコレータ

前回まででユーザー認証機能は実装したので、新しいTopicを作成する機能をログイン済みユーザのみに制限する。

f:id:wonder-wall:20180325132421p:plain

上の画像からわかるように現時点ではログインしていないユーザーでもアクセスできるようになっている。

ログイン済みユーザのみに制限する方法は簡単で@login_requiredをインポートして対象のビュー関数の前に追加すればよい。
今回の場合はboards/views.pyを以下のように編集する。

from django.contrib.auth.decorators import login_required

@login_required
def new_topic(request, pk):
    # ...

編集後にアクセスするとログインページにリダイレクトされるようになる。
リダイレクト先は前回のパスワード変更で説明したようにLOGIN_URL = 'login'で指定する。

f:id:wonder-wall:20180325132701p:plain


ログイン後のリダイレクト先

ここで上の画像のURLを見てみると?next=/boards/1/new/というパラメータがあることが確認できる。 このパラメータの値は最初にアクセスしたページなので、templates/login.htmlname="next"の行を追加するとログイン後のリダイレクト先としてそのページを指定できる。

<form method="post" novalidate>
  {% csrf_token %}
  <input type="hidden" name="next" value="{{ next }}">
  {% include 'includes/form.html' %}
  <button type="submit" class="btn btn-primary btn-block">Log in</button>
</form>


login_requriedのテスト

@login_requiredのテストを追加する前に、今までtest_views.pyに書いていたテストコードを3つのファイルに分割する。 分割後のディレクトリ、ファイル構成は以下のようになる。

├── boards
│  ├── __init__.py
│  ├── admin.py
│  ├── apps.py
│  ├── forms.py
│  ├── migrations/
│  ├── models.py
│  ├── templatetags/
│  ├── tests/
│  │  ├── __init__.py
│  │  ├── test_templatetags.py
│  │  ├── test_view_board_topics.py
│  │  ├── test_view_home.py
│  │  └── test_view_new_topic.py

test_view_home.py(クリックで展開)

from django.test import TestCase
from django.urls import resolve, reverse

from ..models import Board
from ..views import home


class HomeTests(TestCase):
    def setUp(self):
        self.board = Board.objects.create(name='Django', description='Django board.')
        url = reverse('home')
        self.response = self.client.get(url)

    def test_home_view_status_code(self):
        self.assertEquals(self.response.status_code, 200)

    def test_home_url_resolves_home_view(self):
        view = resolve('/')
        self.assertEquals(view.func, home)

    def test_home_view_contains_link_to_topics_page(self):
        board_topics_url = reverse('board_topics', kwargs={'pk': self.board.pk})
        self.assertContains(self.response, 'href="{0}"'.format(board_topics_url))

test_view_board_topics.py(クリックで展開)

from django.test import TestCase
from django.urls import resolve, reverse

from ..models import Board
from ..views import board_topics


class BoardTopicsTests(TestCase):
    def setUp(self):
        Board.objects.create(name='Django', description='Django board.')

    def test_board_topics_view_success_status_code(self):
        url = reverse('board_topics', kwargs={'pk': 1})
        response = self.client.get(url)
        self.assertEquals(response.status_code, 200)

    def test_board_topics_view_not_found_status_code(self):
        url = reverse('board_topics', kwargs={'pk': 99})
        response = self.client.get(url)
        self.assertEquals(response.status_code, 404)

    def test_board_topics_url_resolves_board_topics_view(self):
        view = resolve('/boards/1/')
        self.assertEquals(view.func, board_topics)

    def test_board_topics_view_contains_navigation_links(self):
        board_topics_url = reverse('board_topics', kwargs={'pk': 1})
        homepage_url = reverse('home')
        new_topic_url = reverse('new_topic', kwargs={'pk': 1})
        response = self.client.get(board_topics_url)
        self.assertContains(response, 'href="{0}"'.format(homepage_url))
        self.assertContains(response, 'href="{0}"'.format(new_topic_url))

test_view_new_topic.py(クリックで展開)

from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import resolve, reverse

from ..forms import NewTopicForm
from ..models import Board, Post, Topic
from ..views import new_topic


class NewTopicTests(TestCase):
    def setUp(self):
        Board.objects.create(name='Django', description='Django board.')
        User.objects.create_user(username='john', email='john@doe.com', password='123')
        self.client.login(username='john', password='123')

    def test_new_topic_view_success_status_code(self):
        url = reverse('new_topic', kwargs={'pk': 1})
        response = self.client.get(url)
        self.assertEquals(response.status_code, 200)

    def test_new_topic_view_not_found_status_code(self):
        url = reverse('new_topic', kwargs={'pk': 99})
        response = self.client.get(url)
        self.assertEquals(response.status_code, 404)

    def test_new_topic_url_resolves_new_topic_view(self):
        view = resolve('/boards/1/new/')
        self.assertEquals(view.func, new_topic)

    def test_new_topic_view_contains_link_back_to_board_topics_view(self):
        new_topic_url = reverse('new_topic', kwargs={'pk': 1})
        board_topics_url = reverse('board_topics', kwargs={'pk': 1})
        response = self.client.get(new_topic_url)
        self.assertContains(response, 'href="{0}"'.format(board_topics_url))

    def test_csrf(self):
        url = reverse('new_topic', kwargs={'pk': 1})
        response = self.client.get(url)
        self.assertContains(response, 'csrfmiddlewaretoken')

    def test_contains_form(self):
        url = reverse('new_topic', kwargs={'pk': 1})
        response = self.client.get(url)
        form = response.context.get('form')
        self.assertIsInstance(form, NewTopicForm)

    def test_new_topic_valid_post_data(self):
        url = reverse('new_topic', kwargs={'pk': 1})
        data = {
            'subject': 'Test title',
            'message': 'Lorem ipsum dolor sit amet'
        }
        self.client.post(url, data)
        self.assertTrue(Topic.objects.exists())
        self.assertTrue(Post.objects.exists())

    def test_new_topic_invalid_post_data(self):
        '''
        Invalid post data should not redirect
        The expected behavior is to show the form again with validation errors
        '''
        url = reverse('new_topic', kwargs={'pk': 1})
        response = self.client.post(url, {})
        form = response.context.get('form')
        self.assertEquals(response.status_code, 200)
        self.assertTrue(form.errors)

    def test_new_topic_invalid_post_data_empty_fields(self):
        '''
        Invalid post data should not redirect
        The expected behavior is to show the form again with validation errors
        '''
        url = reverse('new_topic', kwargs={'pk': 1})
        data = {
            'subject': '',
            'message': ''
        }
        response = self.client.post(url, data)
        self.assertEquals(response.status_code, 200)
        self.assertFalse(Topic.objects.exists())
        self.assertFalse(Post.objects.exists())


コードを分割したらtest_view_new_topic.py@login_requiredのテストを追加する。

from django.test import TestCase
from django.urls import reverse
from ..models import Board

class LoginRequiredNewTopicTests(TestCase):
    def setUp(self):
        Board.objects.create(name='Django', description='Django board.')
        self.url = reverse('new_topic', kwargs={'pk': 1})
        self.response = self.client.get(self.url)

    def test_redirection(self):
        login_url = reverse('login')
        self.assertRedirects(self.response, '{login_url}?next={url}'.format(login_url=login_url, url=self.url))

このテストではログインしていない場合にログインページにリダイレクトされることを確認している。
最後にテストを実行して通ることを確認しておく。

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....................................................................
----------------------------------------------------------------------
Ran 68 tests in 11.278s

OK
Destroying test database for alias 'default'...


Topic作成者としてログインユーザーを指定

これまではユーザー認証機能がなかったので最初のUserオブジェクト(adminユーザー)を取得していたが、ユーザー認証機能を実装したのでTopic作成者としてログインユーザーを指定する。
boards/views.pyを以下のように編集(# <- hereの部分)。

from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render

from .forms import NewTopicForm
from .models import Board, Post

@login_required
def new_topic(request, pk):
    board = get_object_or_404(Board, pk=pk)
    if request.method == 'POST':
        form = NewTopicForm(request.POST)
        if form.is_valid():
            topic = form.save(commit=False)
            topic.board = board
            topic.starter = request.user  # <- here
            topic.save()
            Post.objects.create(
                message=form.cleaned_data.get('message'),
                topic=topic,
                created_by=request.user  # <- and here
            )
            return redirect('board_topics', pk=board.pk)  # TODO: redirect to the created topic page
    else:
        form = NewTopicForm()
    return render(request, 'new_topic.html', {'board': board, 'form': form})

新規Topicを作成してみるとログイン中のユーザー名で作成されていることが確認できる。

f:id:wonder-wall:20180325141352p:plain


まとめ

  • login_requiredデコレータでビューをログイン済みユーザーのみに制限
  • ?next=パラメータを利用してログイン後のリダイレクト先として最初にアクセスしたページを指定
  • request.userでログイン中のユーザを取得