クラスベースビューについて（自分用メモ）

## Django自分用メモになります
今回はクラスベースビューについてまとめます


## 開発環境
OS:mac
エディタ：vscode
python:3.10.9
django:4.1.0


### 公式ページ
[公式](https://docs.djangoproject.com/ja/3.1/ref/class-based-views/)

### View
GET,POSTなどリクエストに応じて実行する処理を変えたい場合にはget(),post()をメゾットとして定義する
```python
class MyView(View):
    def get(self,request,**kwargs):
        pass #GETの場合の処理
    def post(self,request,**kwargs):
        pass #POSTの場合の処理
```

### クラスベースビューに触れてみる
Viewを継承したクラスの中でgetメゾットを使ってページを表示する
```python:views.py
from django.shortcuts import render
from django.views.generic.base import(
    View,
)

class IndexView(View):
    #getメゾットが呼び出されてページを呼び出す
    def get(self,request,*args,**kwargs):
        return render(request,'index.html')
```
```python:urls.py
from django.urls import path
from .views import IndexView
app_name = 'store'

urlpatterns = [
    path('index/',IndexView.as_view(),name='index')
]
```
IndexView.as_view()とすることで呼び出し可

### get,postメゾットを定義してそれぞれで処理を変える
１、単純にそのフォームのページを呼び出す場合。(get)
2、フォームで送信した場合。(post)
で処理を変える。

まずモデルの作成と、ModelFormを継承したフォームの作成とsaveメゾットのカスタマイズを復習
```python:models.py
from django.db import models

class BaseModel(models.Model):
    create_at = models.DateTimeField()
    update_at = models.DateTimeField()
    
    class Meta:
        abstract = True #抽象的なモデルとして作成
        
class Books(BaseModel): #create_atとupdate_atを作成しなくても引き継ぐことが可能
    name = models.CharField(max_length=255)
    description = models.CharField(max_length=1000)
    price = models.IntegerField()
    
    class Meta:
        db_table = 'books'
```
::: note 
abstract=Trueと抽象クラスで設定する。
それを継承することで複数のモデルで共通したカラムを持たせることができる
:::

```python:forms.py
from django import forms
from .models import Books
from datetime import datetime

class BookForm(forms.ModelForm):
    
    class Meta:
        model = Books
        fields = ['name','description','price']
        
    #saveメゾットのカスタマイズ(create_atとupdate_atを保存する処理を加えたい。Formの記事でも書いてあるので要チェック)
    def save(self,*args,**kwargs):
        obj = super(BookForm,self).save(commit=False) #saveを実行して、Booksクラスのインスタンスがobjに入る
        obj.create_at = datetime.now()
        obj.update_at = datetime.now()
        obj.save()
        return obj
```
:::note
saveメゾットのカスタマイズは[formの記事](https://qiita.com/Ryo_Kitamoto/items/62f377265e62e70024bc)でも書いたようにかなりつかう記述
:::

getの場合は単純にforms.pyのフォームをコンテキストとして渡してテンプレートで表示
postの場合はフォームの値を受け取って、バリデーションをして保存するという処理にする
```python:views.py
from django.shortcuts import render
from django.views.generic.base import(
    View,
)
from . import forms

class IndexView(View):
    
    #getメゾットが呼び出されてページを呼び出す
    def get(self,request,*args,**kwargs):
        #フォームをコンテキストでページに渡してあげる
        book_form = forms.BookForm()
        return render(request,'index.html',context={
            'book_form':book_form,
        })
    
    #ここでpostメゾットが定義されていないとフォームで送られてきた際にどういった処理をするのかがわからないから定義してあげる必要がある
    def post(self,request,*args,**kwargs):
        book_form = forms.BookForm(request.POST or None)
        if book_form.is_valid():
            book_form.save()
        return render(request,'index.html',context={
            'book_form':book_form,
        })
```

### TemplateView
TemplateViewではこれまでcontextで値を渡していたやり方を
get_context_dataで渡すようにする

### TemplateViewの呼び出し方(urls.pyからとviews.py)
urls.pyからとviews.pyから呼び出す２種類がある
```python:urls.py
from django.urls import path
from django.views.generic.base import TemplateView
app_name = 'store'

urlpatterns = [
    #urls.pyからTemplateViewを呼び出す方法
    path('home/',TemplateView.as_view(template_name='home.html'),name='home'),
]
```
またはviews.pyから呼び出す
```python:views.py
from django.views.generic.base import(
    TemplateView
)
class HomeView(TemplateView):
    template_name = 'home.html'
```
```python:urls.py
from .views import HomeView
urlpatterns = [
    path('home/',HomeView.as_view(),name='home'),
]
```

### get_context_dataで値を渡す
```python:views.py
class HomeView(TemplateView):
    template_name = 'home.html'
    
    def get_context_data(self,**kwargs): #kwargsとは何を受け取るのかはのちに解説
        context = super().get_context_data(**kwargs)#get_context_dataをオーバライドする
        context['time'] = datetime.now()
        return context
```
渡したコンテキストは{{time}}で表示可能
```html:home.html
<h1>Home画面</h1>
<p>現在時刻は{{time}}です。</p>
```

### get_context_dataで値を渡す その２（urlパラメータを受け取って動的にページを変更）

urls.pyでパラメータを受け取るように設定
```python:urls.py
urlpatterns = [
    path('home/<name>',HomeView.as_view(),name='home'),
]
```
get_context_dataの引数の**kwargsはパラメータのnameを受け取る
```python:views.py
class HomeView(TemplateView):
    template_name = 'home.html'
    
    def get_context_data(self,**kwargs):
        context = super().get_context_data(**kwargs)
        print(kwargs)
        context['name']=kwargs.get('name') #nameはkwargsに入ってくるのでパラメータのnameを取得してコンテキストに渡す
        context['time'] = datetime.now()
        return context
```
表示する
```html:home.html
<p>{{name}}さん。現在時刻は{{time}}です。</p>
```

### get_context_dataで値を渡す その３（フォームをつくって渡す）演習
```python:views.py
#フォームをつくってわたす
    def get_context_data(self,**kwargs):
        context = super().get_context_data(**kwargs)
        picture_form = forms.PictureUploadForm
        #8.本の一覧を取得する処理を書くが、models.pyでマネージャーを用意してそれを使って取得する
        pictures = Pictures.objects.filter_by_book(book=self.object)
        context['pictures']=pictures
        context['picture_form'] =picture_form
        return context

#ファイルが上げられた際の保存処理をする
    def post(self,request,*args,**kwargs):
        #2.こちらで画像のアップロードする処理を書く
        picture_form = forms.PictureUploadForm(request.POST or None,request.FILES or None)
        if picture_form.is_valid() and request.FILES:
            #3.いまBookUpdateViewで更新している本がどの本なのかをおしえてあげる
            book = self.get_object()
            picture_form.save(book=book) #4.Pictureモデルで定義したbookというカラムにその本を渡す
            #5.forms.pyでsaveメゾットをカスマイズする必要がある
        
        #1.本の名前や説明、値段などの更新処理はこちらで行うので
        return super(BookUpdateView,self).post(request,*args,**kwargs)

```
forms.pyでフォームを定義
```python:forms.py
class PictureUploadForm(forms.ModelForm):
    picture =forms.FileField(required=False)
    
    class Meta:
        model=Pictures
        fields = ['picture',]

    #6.saveメゾットのカスタマイズ  
    def save(self,*args,**kwargs):
        obj = super(PictureUploadForm,self).save(commit=False)
        obj.create_at = datetime.now()
        obj.update_at = datetime.now()
        obj.book = kwargs['book']
        obj.save()
        return obj
```
:::note
コンテキストで渡しているのでhtml側では    {{picture_form.as_p}}で表示ができるが、
画像を処理するので
form のenctype="multipart/form-data"とすることを忘れずに

コンテキストでpicturesと画像一覧を渡しているのでfor文で回して
picture.picture.urlで取り出せる
```python:html
{%for picture in pictures %}
    <img width='100px' height = '100px' src="{{picture.picture.url}}">
{% endfor %}
```
:::

:::note
画像を表示するならプロジェクトのurls.pyにその設定をすることを忘れない
```python:プロジェクトのurls.py
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('store/',include('store.urls')),
]

if settings.DEBUG:
    urlpatterns +=static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)
```

:::

### インスタンス名.モデル名_set.all(インスタンスから逆参照する)
```python:book_list.html
{%for object in object_list%}
{# object.pictures_set.allとはインスタンス名.モデル名_set.all()とすることでインスタンスの方からモデルのデータに逆参照できる #}
    {% for picture in object.pictures_set.all %} 
        <img width="20px" height="20px" src={{picture.picture.url}}>
    {% endfor %}
{% endfor %}
```
:::note
object.pictures_set.allでインスタンスからモデルに含まれる画像をすべてとりだしfor文でループさせて表示するといったことができる
[参考にさせてもらったページ](https://brhk.me/programing/django-foreignkey/)
:::

### DB上からも画像データを削除（シグナル復習）
やりかたは２通り。views.pyで関数として定義するか、シグナルのpost_deleteでモデルが削除された時に起動するコードを書くか。
1.views.pyの場合
```python:views.py
#画像削除用の関数
def delete_picture(request,pk):
    picture = get_object_or_404(Pictures,pk=pk)
    picture.delete()
    #上記だとテーブルから削除しただけでDBのフォルダ上には残るのでそちらも削除したい場合に下記を記述
    #もしくはモデルに操作が加わった場合に操作を実行したいのでシグナルを使う。
    import os
    if os.path.isfile(picture.picture.path):
        os.remove(picture.picture.path)
    
    messages.success(request,'画像を削除しました')
    return redirect('store:edit_book',pk=picture.book.id)

    #delete_picture関数はhtml側でimgのa href='{%url 'store:delete_picture' pk=picture.id%}'として設定する
```
2.models.pyでシグナルを使う場合
```python:models.py
from django.db.models.signals import post_delete
from django.dispatch import receiver
import os

#画像の削除が行われたらDBからも削除する
@receiver(post_delete,sender=Pictures)
def delete_picture(sender,instance,**kwargs):
    #instanceの中に削除されたpictureのデータが入ってるので
    if instance.picture:
        if os.path.isfile(instance.picture.path):
            os.remove(instance.picture.path)
```
models.pyはこんなかんじ
```python:models.py
class BaseModel(models.Model):
    create_at = models.DateTimeField()
    update_at = models.DateTimeField()
    
    class Meta:
        abstract = True #抽象的なモデルとして作成
        
class Books(BaseModel): #create_atとupdate_atを作成しなくても引き継ぐことが可能
    name = models.CharField(max_length=255)
    description = models.CharField(max_length=1000)
    price = models.IntegerField()
    
    class Meta:
        db_table = 'books'
      
    #BookCreateViewで作成した際に変移先を設定する  
    def get_absolute_url(self):
        return reverse_lazy('store:detail_book',kwargs={'pk':self.pk})
    
    
#7.画像を表示するためにPicturesモデルのマネージャーを用意  
class PicturesManager(models.Manager):
    def filter_by_book(self,book):
        return self.filter(book=book).all()
    
class Pictures(BaseModel):
    picture = models.FileField(upload_to='picture/',blank=True)
    book = models.ForeignKey(
        'Books',on_delete=models.CASCADE
    )
    
    objects = PicturesManager()
    
    class Meta:
        db_table='pictures'
```

### DetailView
データの詳細を表示するのに使う
テンプレート側はobjectにデータが格納されているのでそれを使う
```python:views.py
from django.views.generic.detail import DetailView
from .models import Books

class BookDetailView(DetailView):
    model = Books
    template_name = 'book.html'

    #おまけ
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        print(context)
        #詳細を表示すると共にフォームを渡してpostメゾットを記述して更新するなんかもできる
        context['form'] = forms.BookForm()
        return context
```
int:pkとしてそのプライマリキーを持つ要素を取得する

:::note
DetailViewの便利なメゾットself.get_object()
```python
class ItemDetailView(DetailView):
    model = Items
    template_name = 'item_detail.html'
    
    def get_context_data(self,**kwargs):
        context =super().get_context_data(**kwargs)
        #その品物のお気に入り数を取得
        item = self.get_object() #ここ
        favorite_count = item.favorited_by.count()
        context['favorite_count']=favorite_count
        
        return context
```
URLパラメータで取得してきたインスタンスを簡単に取得できて操作できるのがself.get_object()
:::
```python:urls.py
from .views import BookDetailView
app_name = 'store'

urlpatterns = [
    path('detail_book/<int:pk>',BookDetailView.as_view(),name='detail_book'),
]
```
テンプレート側の表示はobjectが全て持っているのでそれを使う
```html:book.html
{{object.name}}<br>
{{object.description}}<br>
{{object.price}}円<br>
```
ListViewのあるリンクからDetailViewに移動させたいときのurl記法(例)
```python:html
#ListViewはobject_listにはいってくるのでそれをobjectという変数にいれて<int:pk>に渡す
#ここでのitem_idはプライマリキーに設定したカラム
{% for object in object_list%}
    <td>商品名</td>
    <td><a href="{% url 'boards:item_detail' object.item_id %}">{{object.name}}</a></td>
{% endfor %}
```

### ListView
挿入したいデータの一覧を表示したい場合に用いる
def get_queryset(self)でデータを取得する際のSQLを定義できるので
並び替えたりフィルターをかけて取り出す、なんかもできる
テンプレート側はobject_listにデータが格納されているのでそれを使う

```python:views.py
from django.views.generic.list import ListView

class BookListView(ListView):
    model = Books
    template_name='book_list.html'

    #SQLを発行して表示項目を並び替えたり絞り込む
    def get_queryset(self):
        qs = super(BookListView,self).get_queryset()#オーバーライド
        qs = qs.filter(name__startswith='Book') #nameがBookで始まるもの
        qs = qs.order_by('-id')　#idを逆順に
        print(qs)
        return qs

    #例　演習　inputに入力された内容でProductsモデルからフィルターする inputのname属性はproduct_type_name
    def get_queryset(self):
        qs=super(ProductListView,self).get_queryset()
        product_type_name=self.request.GET.get('product_type_name',None) #inputタグから取り出す。なにもなければNoneをいれる
        product_name=self.request.GET.get('product_name',None) #inputタグから取り出す。なにもなければNoneをいれる
        if product_type_name:
            #Productsモデルのproduct_typeの外部キーでnameにアクセス
            qs = qs.filter(product_type__name=product_type_name)
        if product_name:
            #Productsモデルのnameにアクセス
            qs = qs.filter(name=product_name)
        #昇順降順で並び替える(選択されていない時は0をいれる)
        order_by_price=self.request.GET.get('order_by_price',0)
        if order_by_price =='1':
            qs = qs.order_by('price')
        elif order_by_price =='2':
            qs = qs.order_by('-price') 
            
        return qs
    
    #フィルターしたときにテキストボックスの中にフィルターした文字列を渡して表示する
    def get_context_data(self,**kwargs):
        context=super().get_context_data(**kwargs)
        #contextの['product_type_name']という場所にinputの'product_type_name'属性の値を渡し、なければ空白を渡す
        context['product_type_name']=self.request.GET.get('product_type_name','')
        context['product_name']=self.request.GET.get('product_name','')

        #昇順降順で並び替えた時にチェックを維持する
        order_by_price=self.request.GET.get('order_by_price',0)
        if order_by_price == '1':    
            context['ascending']=True
        elif order_by_price =='2':
            context['descending']=True
        return context
        #htmlのinputのvalue属性に{{product_type_name}}と{{product_name}}を渡すことを忘れずに
        #<p>値段で並び替える：
        #昇順<input type ="radio" name="order_by_price" value="1" {%if ascending %}checked{%endif%}>
        #降順<input type ="radio" name="order_by_price" value="2" {%if descending %}checked{%endif%}>
        #</p>
```
urls.pyも記述する
```python:urls.py
path('list_books/',BookListView.as_view(),name='list_books'),
#urlにパラメータを指定してkwargsで受け取ってそれをフィルターにかけることもできる
path('list_books/<name>',BookListView.as_view(),name='list_books'),
```
パラメータからフィルターする
```python:views.py
def get_queryset(self):
        qs = super(BookListView,self).get_queryset()
        if 'name' in self.kwargs:
            qs = qs.filter(name__startswith=self.kwargs['name'])
        return qs
```
for文で一つ一つ取り出して表示
```python:book_list.html
<ul>
    {%for object in object_list%}
        <li>名前：{{object.name}}</li>
        <li>説明：{{object.description}}</li>
        <hr>
    {%endfor%}
</ul>
```

### CreateView
テーブルにデータを挿入したい時に用いる
from django.views.generic.edit import CreateView
入力するデータの指定、成功時の変移先の指定、送信時に処理を加える、初期値を設定できるなど
いろんな設定ができる

:::note
CreateViewのメゾット一覧
[【Django】CreateViewの使い方（クラスベースビューでの新規登録ページの実現）](https://daeudaeu.com/django-createview/#CreateView-6)
:::
```python
from django.urls import reverse_lazy

class MyCreateView(CreateView):
    template_name=''
    model = モデル
    field = ['',''] #入力するデータ
    #もしくは
    form_class = ''#利用したいForm

    success_url =reverse_lazy('app_name:name') #Create成功時の変移先を定義 ないとエラーに
    #もしくは
    get_absolute_url = '' #対象のモデルにこちらを定義

    def get_success_url() #成功時の変移先を定義
    def form_valid(self,form) #formの送信時の処理をカスタマイズする
    def get_initial(self,**kwargs): #初期値の設定
```
実際に書く
```python:views.py
class BookCreateView(CreateView):
    model = Books
    fields = ['name','description','price']
    template_name = 'add_book.html'
    success_url =reverse_lazy('store:list_books')
    #またはmodels.pyにdef get_absolute_urlを定義する(両方に書いてある場合はこちらが優先される)
    
    
    def form_valid(self,form): #送信時の処理をカスタマイズする（create_atがnullだと保存できないのでそれを追加する）
        form.instance.create_at = datetime.now()
        form.instance.update_at = datetime.now()
        return super(BookCreateView,self).form_valid(form)
    
    #初期値の設定
    def get_initial(self,**kwargs):
        initial = super(BookCreateView,self).get_initial(**kwargs)
        initial['name'] = 'sample'
        return initial
```
models.pyに変移先を設定もできる
```python:models.py
#class Books内
#BookCreateViewで作成した際に変移先を設定する  
    def get_absolute_url(self):
        return reverse_lazy('store:detail_book',kwargs={'pk':self.pk})
```

### UpdateView(CreateViewと似ている)
```python:views.py
from django.views.generic.edit import CreateView,UpdateView,DeleteView #全て同じところからimportできる

class BookUpdateView(UpdateView):
    template_name='update_book.html'
    model=Books
    form_class = forms.BookUpdateForm
    #models.pyでdef get_absolute_urlを設定しているので自動的にページ遷移する
    
    #こちらを設定すればこちらが優先される
    def get_success_url(self):
        print(self.object)
        return reverse_lazy('store:edit_book',kwargs={'pk':self.object.id})
```
forms.pyでupdateで使うフォームを作成する
IndexViewでつくったフォームをコピーして一部編集。
変更箇所はsuperのあとをBookUpdateFormにすることと、更新時に使うフォームなのでcreate_atは削除する
```python:forms.py
class BookUpdateForm(forms.ModelForm):
    
    class Meta:
        model = Books
        fields = ['name','description','price']
        labels={
            '〇〇':'◻️◻️',
            '△△':'⚪︎⚪︎',
        } #labelを一括で指定することもできる
        
    #saveメゾットのカスタマイズ(create_atとupdate_atを保存する処理を加えたい。Formの記事でも書いてあるので要チェック)
    def save(self,*args,**kwargs):
        obj = super(BookUpdateForm,self).save(commit=False) #saveを実行して、Booksクラスのインスタンスがobjに入る
        obj.update_at = datetime.now()
        obj.save()
        return obj
```
パスの設定。pkを受け取る
```python:urls.py
    path('edit_book/<int:pk>',BookUpdateView.as_view(),name='edit_book'),
```
### DeleteView
```python:views.py
class BookDeleteView(DeleteView):
    model = Books
    template_name='delete_book.html'
    success_url =reverse_lazy('store:list_books') #削除後一覧ページに遷移
```
urls.pyはpkを受け取る
```python:urls.py
    path('delete_book/<int:pk>',BookDeleteView.as_view(),name='delete_book'),

```

### 一覧画面から各画面へ遷移させる
object_listをfor文で一つ一つをobjectでとりだすと{%url ""%}のあとにobject.idをつけるだけで
パラメータにわたすことができる
a href="{{object.get_absolute_url}}"としてもurlを渡すことができる
```python:book_list.html
<p><a href="{% url 'store:add_book'%}">データ追加</a></p>
<table>
    <tbody>
    {%for object in object_list%}
    <tr>
        <td>名前：<a href="{{object.get_absolute_url}}">{{object.name}}</a></td>
        #<td>名前：<a href="{%url 'store:detail_book' object.id%}">{{object.name}}</a></td> こちらでも可
        <td>説明：{{object.description}}</td>
        <td><a href = "{% url 'store:edit_book' object.id%}">編集</a></td>
        <td><a href = "{% url 'store:delete_book' object.id%}">削除</a></td>
    </tr>
    {%endfor%}
    <tbody>
</table>
```

### FormView
フォームを使う際はこちらが便利らしい
```python:views.py
from django.views.generic.edit import CreateView,UpdateView,DeleteView,FormView

class BookFormView(FormView):
    template_name='form_book.html'
    form_class = forms.BookForm #利用するフォームを定義
    success_url = reverse_lazy('store:list_books')
    
    #POSTリクエストを投げた場合に保存処理する記述をする(これがなければ送信しても何も実行されない)
    def form_valid(self,form):
        if form.is_valid():
            form.save()
        return super(BookFormView,self).form_valid(form)
    
    #フォームの初期値を設定
    def get_initial(self):
        initial = super(BookFormView,self).get_initial()
        initial['name']='form sample'
        return initial
```

### RedirectView(別のView,別のページにリダイレクトしたい時に用いる)
やりかたは２種類

:::note
urls.pyに直接定義する
:::

```python:urls.py
from django.views.generic.base import RedirectView
from .views import BookRedirectView

urlpatterns=[
    path('/search/<term>/',RedirectView.as_view(url='http://google.co.jp/?q=%(term)s')),
    #例
    path('google/',RedirectView.as_view(url='https://google.co.jp')),
    path('book_redirect_view',BookRedirectView.as_view(),name='book_redirect_view'),
    path('book_redirect_view/<int:pk>',BookRedirectView.as_view(),name='book_redirect_view'),
]
```
:::note
RedirectViewを継承してクラスを作成する
:::
```python:views.py
class SearchRedirectView(RedirectView):
    url = 'https://google.co.jp' #静的に定義

class SearchRedirectView(RedirectView):
    def get_redirect_url(self, *args, **kwargs):
        return redirect(〇〇) #動的に定義
#例(静的)
class BookRedirectView(RedirectView):
    url = 'https://google.co.jp'
#例（動的）
class BookRedirectView(RedirectView):

    #DBからデータを取得してそのpkをつかって編集画面へ遷移するといった動的に動かすこともできる
    def get_redirect_url(self,*args,**kwargs):
        book = Books.objects.first()
        if 'pk' in kwargs: #urlパラメータがある場合はこちら
            return reverse_lazy('store:detail_book',kwargs={'pk':kwargs['pk']})

        return reverse_lazy('store:edit_book',kwargs={'pk':book.pk})
```

### 更新時削除時にメッセージを表示する(SuccessMessageMixin)
:::note
SuccessMessageMixinをUpdateViewなんかと一緒に多重継承させる
:::
```python
from django.contrib.messages.views import SuccessMessageMixin

class BookUpdateView(SuccessMessageMixin,UpdateView):
    model=Books
    success_message='成功しました' #成功した場合のメッセージを定義（静的）
    def get_success_message(self,cleaned_data): #成功した場合のメッセージを定義（動的）
```
html側ではmessagesから表示させる
```python:update_book.html
{% if messages %}
{% for message in messages %}
{{ message.message }}
{% endfor %}
{% endif %}
```
def get_success_messageの例
print(cleaned_data)をみてみると
{'name': 'Book3', 'description': 'Book3の説明メッセージ再更新', 'price': 3000}
このようになっているのでcleaned_data.get('name')でBook３をとりだすことができる
```python:views.py
#成功時のメッセージを動的に
    def get_success_message(self, cleaned_data):
        print(cleaned_data)
        return cleaned_data.get('name') + 'を更新しました'
```