Djangoメモ(30) : クラスベース汎用ビューのUpdateViewで編集機能作成
A Complete Beginner's Guide to Djangoのチュートリアルを参考にクラスベースビューを使用してみる。
ビューの種類
これまでのチュートリアルでは関数ベースビューを使用してきたが、ビューにはその他にクラスベースのビューがある。
以降、それぞれについて説明する。
- 関数ベースビュー(Function-based views)
- クラスベースビュー(Class-based views)
- クラスベース汎用ビュー(Class-based generic views)
関数ベースビュー
関数ベースビューはシンプルなビューでHttpRequest
を受け取りHttpResponse
を返す。
以下のようにわかりやすいのでチュートリアルなどではこれを使用している場合が多い。
urls.py
urlpatterns = [ path('new_post/', views.new_post, name='new_post'), ]
views.py
def new_post(request): if request.method == 'POST': form = PostForm(request.POST) if form.is_valid(): form.save() return redirect('post_list') else: form = PostForm() return render(request, 'new_post.html', {'form': form})
クラスベースビュー
クラスベースビューはクラスとして定義され、全てのクラスはdjango.views.generic.base.View
を継承している。ビューを構造化し、継承とミックスインを利用してコードを再利用しやすいのが利点。
クラスベースビューではget
、post
などのHTTPメソッドを関数として取り扱うため、関数ベースビューのような条件分岐がない。
また、urls.py
ではクラスメソッドのas_view()
により設定する。
urls.py
urlpatterns = [ path('new_post/', views.NewPostView.as_view(), name='new_post'), ]
views.py
from django.views.generic import View class NewPostView(View): def render(self, request): return render(request, 'new_post.html', {'form': self.form}) def post(self, request): self.form = PostForm(request.POST) if self.form.is_valid(): self.form.save() return redirect('post_list') return self.render(request) def get(self, request): self.form = PostForm() return self.render(request)
クラスベース汎用ビュー
クラスベース汎用ビューはリスト表示やCRUD操作などのよく使われる機能をクラスベースビューとして定義したもので、一覧はドキュメントを参照。
代表的なクラスベース汎用ビューとしては以下のものがある。
- シンプルな汎用ビュー
- View
- TemplateView
- RedirectView
- 詳細のビュー
- Detail Views
- リストのビュー
- List Views
- 編集するためのビュー
- FormView
- CreateView
- UpdateView
- DeleteView
urls.py
urlpatterns = [ path('new_post/', views.NewPostView.as_view(), name='new_post'), ]
views.py
from django.views.generic import CreateView class NewPostView(CreateView): model = Post form_class = PostForm success_url = reverse_lazy('post_list') template_name = 'new_post.html'
UpdateViewを使って編集機能を作成
それでは実際にクラスベース汎用ビューのUpdateViewを使ってPost(返信)の編集機能を作成してみる。
myproject/urls.py
from django.conf.urls import url from boards import views urlpatterns = [ # ... path('boards/<int:pk>/topics/<int:topic_pk>/posts/<int:post_pk>/edit/', views.PostUpdateView.as_view(), name='edit_post'), ]
boards/views.py
from django.contrib.auth.decorators import login_required from django.shortcuts import redirect from django.views.generic import UpdateView from django.utils import timezone from django.utils.decorators import method_decorator from .models import Post @method_decorator(login_required, name='dispatch') class PostUpdateView(UpdateView): model = Post fields = ('message', ) template_name = 'edit_post.html' pk_url_kwarg = 'post_pk' context_object_name = 'post' def get_queryset(self): queryset = super().get_queryset() return queryset.filter(created_by=self.request.user) def form_valid(self, form): post = form.save(commit=False) post.updated_by = self.request.user post.updated_at = timezone.now() post.save() return redirect('topic_posts', pk=post.topic.board.pk, topic_pk=post.topic.pk)
コードの意味は以下の通り。
@method_decorator(login_required, name='dispatch')
- クラスベースビューでの
@login_required
の指定方法。クラスベースビューではdispatch
メソッドを指定する。
- クラスベースビューでの
model
- モデルを指定する。
fields
- 編集するフィールドを指定する。
('message')
だとstrになるので('message', )
でtupleにしている。['message']
としてもよい。('message', 'created_by')
のようにすれば編集可能なフィールドが増える。
- 編集するフィールドを指定する。
template_name
- テンプレートファイル名を指定する。
pk_url_kwarg
- プライマリキーを含むURLConfキーワード引数の名前を指定する。
context_object_name
- テンプレート側で参照するオブジェクト名を指定する。
def get_queryset(self)
- ビューが表示するオブジェクトを取得するために使われるクエリセットを返す。デフォルトは
all()
なのでfilter()
でログインユーザーが作成したPostのみに限定している。
- ビューが表示するオブジェクトを取得するために使われるクエリセットを返す。デフォルトは
def form_valid(self, form)
- 適切なデータが送信された場合にインスタンスを保存する。例ではオーバーライドして
updated_at
などのフィールドにもデータをセットしている。
- 適切なデータが送信された場合にインスタンスを保存する。例ではオーバーライドして
次にtemplates/edit_post.html
テンプレートを作成する。
{% extends 'base.html' %} {% block title %}Edit post{% endblock %} {% block breadcrumb %} <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li> <li class="breadcrumb-item"><a href="{% url 'board_topics' post.topic.board.pk %}">{{ post.topic.board.name }}</a></li> <li class="breadcrumb-item"><a href="{% url 'topic_posts' post.topic.board.pk post.topic.pk %}">{{ post.topic.subject }}</a></li> <li class="breadcrumb-item active">Edit post</li> {% endblock %} {% block content %} <form method="post" class="mb-4" novalidate> {% csrf_token %} {% include 'includes/form.html' %} <button type="submit" class="btn btn-success">Save changes</button> <a href="{% url 'topic_posts' post.topic.board.pk post.topic.pk %}" class="btn btn-outline-secondary" role="button">Cancel</a> </form> {% endblock %}
最後に編集画面に遷移できるようにtemplates/topic_posts.html
のEditボタンのリンクを修正する。
{% if post.created_by == user %} <div class="mt-3"> <a href="{% url 'edit_post' post.topic.board.pk post.topic.pk post.pk %}" class="btn btn-primary btn-sm" role="button">Edit</a> </div> {% endif %}
これで図のような編集が画面が表示される。
もちろん"Message"を変更して"Save changes"を押せば編集できる("Hi"から"UpdateView Test"に変更)。
このように使い方さえわかっていればクラスベース汎用ビューを使うことで簡単に機能を追加することができるので便利。
UpdateViewのテスト
UpdateViewのテストについては下記コードを参照。
boards/tests/test_view_edit_post.py
(クリックで展開)from django.forms import ModelForm
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import resolve, reverse
from ..models import Board, Post, Topic
from ..views import PostUpdateView
class PostUpdateViewTestCase(TestCase):
'''
Base test case to be used in all `PostUpdateView` view tests
'''
def setUp(self):
self.board = Board.objects.create(name='Django', description='Django board.')
self.username = 'john'
self.password = '123'
user = User.objects.create_user(username=self.username, email='john@doe.com', password=self.password)
self.topic = Topic.objects.create(subject='Hello, world', board=self.board, starter=user)
self.post = Post.objects.create(message='Lorem ipsum dolor sit amet', topic=self.topic, created_by=user)
self.url = reverse('edit_post', kwargs={
'pk': self.board.pk,
'topic_pk': self.topic.pk,
'post_pk': self.post.pk
})
class LoginRequiredPostUpdateViewTests(PostUpdateViewTestCase):
def test_redirection(self):
login_url = reverse('login')
response = self.client.get(self.url)
self.assertRedirects(response, '{login_url}?next={url}'.format(login_url=login_url, url=self.url))
class UnauthorizedPostUpdateViewTests(PostUpdateViewTestCase):
def setUp(self):
super().setUp()
username = 'jane'
password = '321'
user = User.objects.create_user(username=username, email='jane@doe.com', password=password)
self.client.login(username=username, password=password)
self.response = self.client.get(self.url)
def test_status_code(self):
'''
A topic should be edited only by the owner.
Unauthorized users should get a 404 response (Page Not Found)
'''
self.assertEquals(self.response.status_code, 404)
class PostUpdateViewTests(PostUpdateViewTestCase):
def setUp(self):
super().setUp()
self.client.login(username=self.username, password=self.password)
self.response = self.client.get(self.url)
def test_status_code(self):
self.assertEquals(self.response.status_code, 200)
def test_view_class(self):
view = resolve('/boards/1/topics/1/posts/1/edit/')
self.assertEquals(view.func.view_class, PostUpdateView)
def test_csrf(self):
self.assertContains(self.response, 'csrfmiddlewaretoken')
def test_contains_form(self):
form = self.response.context.get('form')
self.assertIsInstance(form, ModelForm)
def test_form_inputs(self):
'''
The view must contain two inputs: csrf, message textarea
'''
self.assertContains(self.response, '<input', 1)
self.assertContains(self.response, '<textarea', 1)
class SuccessfulPostUpdateViewTests(PostUpdateViewTestCase):
def setUp(self):
super().setUp()
self.client.login(username=self.username, password=self.password)
self.response = self.client.post(self.url, {'message': 'edited message'})
def test_redirection(self):
'''
A valid form submission should redirect the user
'''
topic_posts_url = reverse('topic_posts', kwargs={'pk': self.board.pk, 'topic_pk': self.topic.pk})
self.assertRedirects(self.response, topic_posts_url)
def test_post_changed(self):
self.post.refresh_from_db()
self.assertEquals(self.post.message, 'edited message')
class InvalidPostUpdateViewTests(PostUpdateViewTestCase):
def setUp(self):
'''
Submit an empty dictionary to the `reply_topic` view
'''
super().setUp()
self.client.login(username=self.username, password=self.password)
self.response = self.client.post(self.url, {})
def test_status_code(self):
'''
An invalid form submission should return to the same page
'''
self.assertEquals(self.response.status_code, 200)
def test_form_errors(self):
form = self.response.context.get('form')
self.assertTrue(form.errors)
テストが通ることを確認する。
@method_decorator(login_required, name='dispatch')
を付けないとテストに失敗するのでいろいろと試してみてもよいかも(なお、チュートリアルではテストを作成してから機能を実装している)。
$ python manage.py test boards.tests.test_view_edit_post Creating test database for alias 'default'... System check identified no issues (0 silenced). ........... ---------------------------------------------------------------------- Ran 11 tests in 3.421s OK Destroying test database for alias 'default'...
まとめ
- 関数ベースビュー、クラスベースビュー、クラスベース汎用ビューがある
- クラスベースビューでは継承を使えるのでコードを再利用しやすい
- クラスベース汎用ビューを使うとリスト表示やCRUD操作などのよく使われる機能が簡単に実装できる
- 編集機能にはUpdateViewを使用する