naritoブログ

【お知らせ】
新ブログができました。今後そちらで更新し、このサイトは更新されません(ウェブサイト自体は残しておきます)
このブログの内容に関してコメントしたい場合は、新ブログのフリースペースに書き込んでください

このブログの内容を新ブログに移行中です。このブログで見つからない記事は、新ブログにありま

Django、クラスベースビューの機能を組み合わせる

約431日前 2018年4月21日21:56
プログラミング関連
Django Python

概要


今回は、Djangoでクラスベースビューの機能を組み合わせるサンプルです。
Githubにプロジェクトを置いています。

いくつかの機能を組み合わせたい場合、関数ビューで書くほうが確実で分かりやすいのですが、保守や再利用のためにクラスにしておいたほうが便利ということも実際あります。

ListView + ListView


モデルAとモデルBがあり、それらを一覧表示させてみます。ページング処理を両方でさせると大変なので、それは考えないことにします。

models.py


記事モデルと、カテゴリモデルの例です。

from django.db import models


class Category(models.Model):
"""カテゴリー"""
name = models.CharField('カテゴリ名', max_length=255)

def __str__(self):
return self.name


class Post(models.Model):
"""記事"""
title = models.CharField('タイトル', max_length=255)
category = models.ForeignKey(Category, verbose_name='カテゴリ', on_delete=models.PROTECT)

def __str__(self):
return self.title




views.py


今回の例は簡単です。まずメインとして扱うモデルでListViewをつくります。
そして、テンプレートへ渡す各変数をつくるget_context_dataメソッド内でもう一つのモデルの一覧を取得します。今回だとカテゴリですね。

class ListAndList(generic.ListView):
model = Post
template_name = 'app/post_list_and_category_list.html'

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['category_list'] = Category.objects.all()
return context



post_list_and_category_list.html


テンプレート内にも、素直に書けます。

{% extends 'app/base.html' %}

{% block content %}
<h2>記事一覧</h2>
<table class="table">
{% for post in post_list %}
<tr>
<td>{{ post.title }}</td>
<td>{{ post.category }}</td>
</tr>
{% endfor %}
</table>

<h2>カテゴリ一覧</h2>
<table class="table">
{% for category in category_list %}
<tr>
<td>{{ category.name }}</td>
</tr>
{% endfor %}
</table>


{% endblock %}



見た目


よく表示されてますね。
記事とカテゴリを一覧で表示している様子


ListView + CreateView


一覧表示をさせながら、追加もできるようにしてみましょう。

models.py


前回使ったPostモデルを使います。

class Post(models.Model):
"""記事"""
title = models.CharField('タイトル', max_length=255)
category = models.ForeignKey(Category, verbose_name='カテゴリ', on_delete=models.PROTECT)

def __str__(self):
return self.title



views.py


複雑に見えると思いますが、実際複雑です。ModelFormMixinは、少なくともListViewと一緒に使うように設計されてはいないようです。
とりあえず動くものはできました。ページングはできるし、ちゃんと作成はできるし、フォーム内容に不備があればそれも反映されます。ページング処理を楽に実装したいのでListViewを軸にしましたが、CreateViewでPost.objects.allとするか、関数ビューの方が楽かもしれませんね。fields属性の代わりに、form_classでもOKです。

from django.urls import reverse_lazy
from django.views.generic.edit import ModelFormMixin
from django.views import generic
from .models import Post, Category


class ListAndCreate(generic.ListView, ModelFormMixin):
model = Post
fields = '__all__'
success_url = reverse_lazy('app:list_and_create') # このビュー自身!
template_name = 'app/post_list_and_post_create.html'


def get(self, request, *args, **kwargs):
self.object = None
return super().get(request, *args, **kwargs)

def post(self, request, *args, **kwargs):
self.object = None
self.object_list = self.get_queryset()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)


post_list_and_post_create.html




{% extends 'app/base.html' %}
{% block content %}
<h2>記事一覧</h2>
<table class="table">
{% for post in post_list %}
<tr>
<td>{{ post.title }}</td>
<td>{{ post.category }}</td>
</tr>
{% endfor %}
</table>

<h2>記事作成</h2>
<form action="" method="POST">
{{ form.non_field_errors }}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label_tag }}</label>
{{ field }}
{{ field.errors }}
</div>
{% endfor %}
{% csrf_token %}
<button type="submit" class="btn btn-primary">送信</button>
</form>

{% endblock %}



見た目


作成しようとすると...
記事一覧と記事作成画面が一緒にある様子

ぶじにできました。
うまく作成できました。

試しにタイトルをunique=Trueなフィールドにし、同じタイトルで作成しようとすると正しくエラーになりました。
エラーも正しく表示される


DetailView + CreateView


これはよくありがちなケースです。記事の詳細画面にあるコメント追加フォームは、この例ですね。

models.py


記事に紐づくCommentモデルが増えました。

from django.db import models


class Category(models.Model):
"""カテゴリー"""
name = models.CharField('カテゴリ名', max_length=255)

def __str__(self):
return self.name


class Post(models.Model):
"""記事"""
title = models.CharField('タイトル', max_length=30)
category = models.ForeignKey(Category, verbose_name='カテゴリ', on_delete=models.PROTECT)

def __str__(self):
return self.title


class Comment(models.Model):
"""コメント"""
text = models.CharField('コメント内容', max_length=100)
post = models.ForeignKey(Post, verbose_name='紐づく記事', on_delete=models.PROTECT)

def __str__(self):
return self.text




forms.py


今回はフォームを使う、というよりは使わなくてはなりません。

from django import forms
from .models import Comment


class CommentCreateForm(forms.ModelForm):
class Meta:
model = Comment
fields = ('text',)
widgets = {
'text': forms.TextInput(attrs={'class': 'form-control'})
}




views.py


一部引っかかる部分(self.object = self.get_object())がありますが、それ以外は素直な実装になります。DetailViewのためにModel属性を定義しますが、作成機能のModelFormMixinでも何らかのモデルを参照させる必要があります。form_class属性があればmodel属性は参照されないので、form_classが必要でした。

from django.shortcuts import get_object_or_404, redirect
from django.views.generic.edit import ModelFormMixin
from django.views import generic
from .forms import CommentCreateForm
from .models import Post, Category


class DetailAndCreate(ModelFormMixin, generic.DetailView):
model = Post
form_class = CommentCreateForm
template_name = 'app/post_detail_and_comment_create.html'

def form_valid(self, form):
post_pk = self.kwargs['pk']
comment = form.save(commit=False)
comment.post = get_object_or_404(Post, pk=post_pk)
comment.save()
return redirect('app:detail_and_create', pk=post_pk)

def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
self.object = self.get_object()
return self.form_invalid(form)



post_detail_and_comment_create.html




{% extends 'app/base.html' %}

{% block content %}
<h1>{{ post.title }}</h1>
<p>カテゴリ:{{ post.category }}</p>
<hr>


{% for comment in post.comment_set.all %}
<p>{{ comment }}</p>
{% endfor %}
<hr>

<form action="" method="POST">
{{ form.non_field_errors }}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label_tag }}</label>
{{ field }}
{{ field.errors }}
</div>
{% endfor %}
{% csrf_token %}
<button type="submit" class="btn btn-primary">送信</button>
</form>


{% endblock %}



見た目


コメントを投稿してみます。
詳細画面にコメントフォームがある様子

ちゃんと追加されます。
ちゃんとコメント投稿できます

コメントのtextをunique=Trueなどにしてエラーをわざと起こすと、ちゃんとエラーが表示されますね。
エラーも表示されます
名無し 約428日前 2018年4月25日11:31 返信する
はじめまして、初めてコメントさせていただきました。
いつもこのサイトで勉強させていただいています。
質問なのですが、forms.ImageField()の引数にhelp_text引数を持たせて、日本語の文章を入れるとエラーが出てしまい、英語と数字のみのテキストしか持たせられません。
日本語の補足文を付けるにはどうすればいいのでしょうか?
なりと 約428日前 2018年4月25日13:32
エラー内容を教えていただけますか。
名無し 約428日前 2018年4月25日15:02 返信する
以下のエラーメッセージです。
SyntaxError: (unicode error) 'utf-8' codec can't decode byte 0x83 in position 0: invalid start byte
なりと 約428日前 2018年4月25日15:21
forms.pyを編集する際に、utf-8ではないエンコーディングでファイルを開いていませんか。
使用しているエディタのエンコーディング設定をご確認ください。
名無し 約428日前 2018年4月25日15:42 返信する
winscpで新しいファイルを作成する際、utf-8以外のコーデック(?)で開くようにデフォルト設定されてしまっていました。
ありがとうございます。
Django6ヶ月目 約84日前 2019年4月3日17:11 返信する
naritoさん

いつもお世話になっております。
色々な機能について、日々勉強させていただいております。

本記事を拝見し、「DetailView + CreateView」にチャレンジしてみました。
しかし実装をしてみると、コメントの送信をした際に405エラーになってしまいます。

405エラーではどのような原因が考えられますでしょうか?
お手すきの際にアドバイスいただけますと幸いです。

よろしくお願いいたします!
Django6ヶ月目 約81日前 2019年4月6日16:26
上記、自己解決しました。
コメントをログインユーザーに限定したく、DetailViewにlogin_urlを設定していたのが原因のようでした。