Djangoメモ(25) : login_requiredデコレータでビューをログイン済みユーザーのみに制限
A Complete Beginner's Guide to Djangoのチュートリアルを参考にビューをログイン済みユーザのみに制限してみる。
login_requiredデコレータ
前回まででユーザー認証機能は実装したので、新しいTopicを作成する機能をログイン済みユーザのみに制限する。
上の画像からわかるように現時点ではログインしていないユーザーでもアクセスできるようになっている。
ログイン済みユーザのみに制限する方法は簡単で@login_required
をインポートして対象のビュー関数の前に追加すればよい。
今回の場合はboards/views.py
を以下のように編集する。
from django.contrib.auth.decorators import login_required @login_required def new_topic(request, pk): # ...
編集後にアクセスするとログインページにリダイレクトされるようになる。
リダイレクト先は前回のパスワード変更で説明したようにLOGIN_URL = 'login'
で指定する。
ログイン後のリダイレクト先
ここで上の画像のURLを見てみると?next=/boards/1/new/
というパラメータがあることが確認できる。
このパラメータの値は最初にアクセスしたページなので、templates/login.html
にname="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を作成してみるとログイン中のユーザー名で作成されていることが確認できる。
まとめ
login_required
デコレータでビューをログイン済みユーザーのみに制限?next=
パラメータを利用してログイン後のリダイレクト先として最初にアクセスしたページを指定request.user
でログイン中のユーザを取得