Django 関数ベースビュー演習（自分用メモ）

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



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


##その他新しく学ぶこと
・Modelのシグナル機能、Modelマネージャー
・Messegesライブラリでの画面上でのメッセージの表示
・AJAXを用いたUIの作成
・キャッシュを用いたメモリ上へのデータの保存


### ユーザ登録をした際にis_activeをTrueにする（初め）
```python:models.py
class UserActivateTokens(models.Model):
    token = models.UUIDField(db_index=True) #tokenを起点に検索するのでdb_indexをTrueにしておく
    expired_at =models.DateTimeField()
    user = models.ForeignKey(
        'Users',on_delete=models.CASCADE
    )
    class Meta:
        db_table='user_activate_tokens'
```
tokenを受け取ってviews.pyの関数を動かす
```python:urls.py
urlpatterns=[
    path('activate_user/<uuid:token>',views.activate_user,name='activate_user'),
]
```
```python:views.py
def activate_user(request,token):# 引数にtokenを受け取ることを忘れない
    pass

```

### シグナル機能について
::: note
シグナルとは特定の処理を実行した時に自動的に呼び出される処理を定義すること
post_save:保存処理の後に実行
pre_save:保存処理の前に実行
post_delete:削除処理の後に実行
pre_delete:削除処理の前に実行
インポートは
from django.contrib.auth.models import モデル名
from django.db.models.signals import post_save

または関数の前に@receiverを関数の前に付与
インポートは
from django.dispatch import receive
from django.contrib.auth.models import モデル名
from django.db.models.signals import post_save
@receiver(post_save,sender=モデル名)
def 関数名(sender,instance,**kwargs)
:::

関数の中で
post_save.connect(関数A,sender=モデル名)とすると
Userクラスで新たにオブジェクトが追加されるたびに関数Aが呼び出される。

```python:models.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from uuid import uuid4
from datetime import datetime,timedelta

#シグナル機能を使う(Usersモデルに保存処理が行われたらUserActivateTokensモデルにデータを作成する)      
@receiver(post_save,sender=Users)
def publish_token(sender,instance,**kwargs):
    print(str(uuid4()))
    print(datetime.now()+timedelta(days=1))
    user_activate_token=UserActivateTokens.objects.create(
        user=instance,token=str(uuid4()),expired_at=datetime.now()+timedelta(days=1)
    )
    #本当はメールでURLを送る方がいい
    print(f'http://127.0.0.1:8000/accounts/activate_user/{user_activate_token.token}')

#別演習でのシグナル
#画像の削除が行われたら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)
```

### Modelマネージャー(データ挿入取り出し用のクラス)
テーブルの定義を記述するクラスとテーブルのデータ挿入、取り出しをするクラスと分けることもある
テーブルのデータ挿入、取り出しをするクラスはmodels.Managerを継承して作成する

### ユーザ登録をした際にis_activeをTrueにする(実際に書いてみる)

その前にややこしいので頭の中整理

1.シグナル機能を使ってユーザが登録された際に、
UserActivateTokensというモデルに新たにデータを作成する。その際にトークンを発行してそのユーザに対して外部キーで紐付けがされている状態にする

2.その前にurls.pyでactivate_user/<発行したトークン>のURL先にいくと
views.pyで定義したactivate_user関数が実行されるようにする。
で、そのacitivate_userはテーブルのデータ挿入や取り出しを行えるmodels.Managerを継承したクラスを(UserActivateTokensManager)を動かせるようになっている
（動かせる理由はUserActivateTokens（テーブルの定義だけのクラス）とobjectsで紐付けしているから）

3.UserActivateTokensManagerはtokenを引数で受け取ってそのtokenをもつユーザーのデータをfilterでとってきてuser.is_active=Trueとしてuser.save()する関数をもっていることでユーザを有効化するという流れ。

下記はコード
```python:models.py
#UserActivateTokensクラスのデータを挿入、取り出す用のクラス
#取り出してuserのis_activateをTrueにする関数 
class UserActivateTokensManager(models.Manager):
    def activate_user_by_token(self,token):
        #tokenが引数で渡されたtokenで現在時刻より大きいもののデータを取ってくる
        user_activate_token=self.filter(
            token=token,
            expired_at__gte=datetime.now()
        ).first()
        user = user_activate_token.user
        user.is_active=True
        user.save()
    
class UserActivateTokens(models.Model):
    token = models.UUIDField(db_index=True) #tokenを起点に検索するのでdb_indexをTrueにしておく
    expired_at =models.DateTimeField()
    user = models.ForeignKey(
        'Users',on_delete=models.CASCADE
    )
    
    #データを取り出すクラスとの紐付け。またはviews.pyでそのマネージャの中の関数を使えるようにする
    objects = UserActivateTokensManager()
    
    class Meta:
        db_table='user_activate_tokens'

#シグナル機能を使う(Usersモデルに保存処理が行われたらUserActivateTokensモデルにデータを作成する)      
@receiver(post_save,sender=Users)
def publish_token(sender,instance,**kwargs):
    print(str(uuid4()))
    print(datetime.now()+timedelta(days=1))
    user_activate_token=UserActivateTokens.objects.create(
        user=instance,token=str(uuid4()),expired_at=datetime.now()+timedelta(days=1)
    )
    #メールでURLを送る方がいい
    print(f'http://127.0.0.1:8000/accounts/activate_user/{user_activate_token.token}')
```

```python:views.py
def activate_user(request,token):
    #objectsで呼び出すにはmodels.pyでobjects=models.Managerを継承したクラスとする必要がある
    user_activate_token =UserActivateTokens.objects.activate_user_by_token(token)
    return render(request,'accounts/activate_user.html')
```

要するにモデルの中身を取り出してなにか変化を与えて登録したい時はこのようにmodels.Managerを継承したクラスをviews.pyで使ってモデルのテーブルの中身を書き換えるという流れをつくればいいのかな？


### ログイン、ログアウト、メッセージ
ログインを実行するにはカスタマイズしたUserにdjango.contrib.auth.models.UserManagerかUserManagerを継承したクラスを指定したobjectsを定義する必要がある？

:::note warn
Userクラス（AbstractBaseUserとPermissionsMixinを継承）のcreate_userやcreate_superuserを定義したUserManagerとはまた別？よくわかってない。
:::

遷移前にメッセージを格納して遷移先の画面上にメッセージを表示するにはdjango.contrib.messagesを使う。
messages.debug(reqest,'メッセージ')
messages.info(reqest,'メッセージ')
messages.success(reqest,'メッセージ')
messages.warning(reqest,'メッセージ')
messages.error(reqest,'メッセージ')と渡してあげると遷移先でmessages変数に格納されて

```python:html
{%for message in messages %}
    {{message.message}}
{% endfor %}
```
で表示することができる

またsettings.pyにでメッセージの表示レベルの変更ができる（デフォルトはINFO）
```python:settings.py
from django.contrib.meesages import constants as message_constants
MESSAGE_LEVEL = message_constants.INFO
```

```python:models.py
from django.contrib.auth.models import UserManager
class Users(AbstractBaseUser,PermissionsMixin):
    (省略)
    objects=UserManager()
```

```python:forms.py
class LoginForm(forms.Form):
    email = forms.CharField(label='メールアドレス')
    password = forms.CharField(label='パスワード',widget=forms.PasswordInput())
```

```python:views.py
from .forms import LoginForm
from django.contrib.auth import authenticate,login,logout
from django.contrib import messages
from django.contrib.auth.decorators import login_required

def user_login(request):
    login_form = LoginForm(request.POST or None)
    if login_form.is_valid():
        email = login_form.cleaned_data.get('email')
        password = login_form.cleaned_data.get('password')
        #認証して、ユーザが存在する場合はそのユーザを取ってこれる
        user = authenticate(email=email,password=password)
        if user:
            if user.is_active:
               login(request,user)
               messages.success(request,'ログイン完了しました')
               return redirect('accounts:home') 
            else:
                messages.warning(request,'ユーザがアクティブではありません')
        else:
            messages.warning(request,'ユーザかパスワードが間違っています')
    return render(request,'accounts/user_login.html',context={
        'login_form':login_form,
    })  
    
@login_required #ログインしていないと実行できないようにデコレータを設定
def user_logout(request):
    logout(request)
    messages.success(request,'ログアウトしました')
    return redirect('accounts:home')
```
urls.pyとhtmlを設定するとログインログアウト処理ができるようになっている。


### ModelFormのデータの更新をする（パスワードの更新）
Form作成時にinstance=として更新したいレコードを指定する
```python:
forms.ModelForm(request.POST or None, instance=<更新したいモデル>
```

ログインをするとログインユーザとシステムの間でセッションが張られるが、パスワードを変更する場合にはユーザのセッションを更新することが必要である。

::: note warn
セッションとは？ こちらがわかりやすい[セッションについて](https://wa3.i-3-i.info/word1791.html)
:::

このセッションを更新するには
```python:
django.contrib.auth.update_session_auth_hash
```
を使う。
```
update_session_auth_user(request,instance)
```
としてリクエストとインスタンスを指定する
```python:
request.user :#ログインしているユーザインスタンスを取得できる
```

手順としてはユーザ情報を編集するフォーム(forms.ModelFormを継承)とviews.pyでログインしているユーザをrequest.userで受け取って、フォームが有効であればsaveする関数を作成する
```python:forms.py
#ユーザを編集するフォーム  
class UserEditForm(forms.ModelForm):
    username = forms.CharField(label='名前')
    age = forms.IntegerField(label='年齢',min_value=0)
    email = forms.EmailField(label='メールアドレス')
    picture = forms.FileField(label='写真',required=False)
    
    class Meta:
        model = Users
        #パスワードは別の画面で更新する
        fields=('username','age','email','picture')

#パスワードを編集するフォーム
class PasswordChangeForm(forms.ModelForm):
    password=forms.CharField(label='パスワード',widget=forms.PasswordInput)
    confirm_password = forms.CharField(label='パスワード再入力',widget=forms.PasswordInput)
    
    class Meta:
        model=Users
        fields=('password',) #タプル型として定義
        
    def clean(self):
        cleaned_data = super().clean()
        password = cleaned_data.get('password')
        confirm_password = cleaned_data.get('confirm_password')
        if password != confirm_password:
            raise ValidationError('パスワードが一致しません')
        
    def save(self,commit=False):
        user = super().save(commit=False)
        validate_password(self.cleaned_data.get('password'),user)
        user.set_password(self.cleaned_data.get('password'))
        user.save()
        return user
    
```
```python:views.py
#ユーザ情報を変更する関数
@login_required
def user_edit(request):
    #request.userはログインしているユーザのことで、そちらを取得して編集する
    user_edit_form = UserEditForm(request.POST or None,request.FILES or None, instance=request.user)
    if user_edit_form.is_valid():
        messages.success(request,'更新完了しました。')
        user_edit_form.save()
    return render(request,'accounts/user_edit.html',context={
        'user_edit_form':user_edit_form
    })
    
#パスワードを更新する関数
@login_required
def change_password(request):
    password_change_form = PasswordChangeForm(request.POST or None,instance=request.user)
    if password_change_form.is_valid():
        try:
            password_change_form.save()
            messages.success(request,'パスワード更新完了しました')
            #パスワードを更新したらセッションを貼り直す必要がある
            update_session_auth_hash(request,request.user)
        except ValidationError as e:
            password_change_form.add_error('password',e)
    return render(request,'accounts/change_password.html',context={
        'password_change_form':password_change_form
    })
```
ちなみにログインしている状態としていない状態でリンクの表示を変えるには
{% if user.is_authenticated %}を使う. 
```html:base.html
<a class='navbar-brand' href="{%url 'accounts:home'%}">ホーム</a>
{%if user.is_authenticated %}
<a class = 'navbar-brand' href="{%url 'accounts:user_logout'%}">ログアウト</a>
<a class = 'navbar-brand' href="{%url 'accounts:user_edit'%}">ユーザ情報更新</a>
{%else%}
<a class = 'navbar-brand' href="{%url 'accounts:regist'%}">ユーザー登録</a>
<a class = 'navbar-brand' href="{%url 'accounts:user_login'%}">ログイン</a>
{%endif%}
```

### 掲示板機能を作成する(Themeを作るフォームの表示と保存)
アプリを区切って新たなboardsというアプリを作成し、models.pyにThemesとCommentsというクラスを作成
```python:boards/models.py
from django.db import models

class Themes(models.Model):
    title=models.CharField(max_length=255)
    user = models.ForeignKey(
        'accounts.Users',on_delete=models.CASCADE
    )
    
    class Meta:
        db_table ='themes'
        
class Comments(models.Model):
    comment=models.CharField(max_length=1000)
    user = models.ForeignKey(
        'accounts.Users',on_delete=models.CASCADE
    )
    theme=models.ForeignKey(
        'Themes',on_delete=models.CASCADE
    )
    
    class Meta:
        db_table='comments'
```
themeを作成するviewを設定と共にurls.pyとforms.pyも作成
```python:urls.py
from django.urls import path
from . import views

app_name='boards'

urlpatterns = [
    path('create_theme',views.create_theme,name='create_theme'),
]
```
views.pyでのポイントは
create_theme_formがis_valid()であれば
Themeモデルは外部キーでuserを持っているのだからそれをログインしているユーザとして渡してあげるところ
```python:views.py
from django.shortcuts import render
from . import forms
from django.contrib import messages

def create_theme(request):
    create_theme_form=forms.CreateThemeForm(request.POST or None)
    if create_theme_form.is_valid():
        #Themeは外部キーとしてuserをもっているのでそれはログインしているユーザだよと教えてあげる
        create_theme_form.instance.user=request.user
        #そしてsave
        create_theme_form.save()
        messages.success(request,'掲示板を作成しました')
        return redirect('accounts:home')
    return render(request,'boards/create_theme.html',context={
        'create_theme_form':create_theme_form
    })
```

```python:forms.py
from django import forms
from .models import Themes,Comments

class CreateThemeForm(forms.ModelForm):
    title = forms.CharField(label='タイトル')
    
    class Meta:
        model = Themes
        fields=('title',)
```

### Theme一覧画面の作成(ModelManagerをつかってDBから取りだす)
Modelマネージャーを使ってDBの内容を取り出すため、models.Managerを継承したクラスとその操作（データを返す処理）　とThemesモデルにobjectsで紐づける設定をしてviews.pyからobjectsを介して操作できるようにする
```python:models.py
#Themesからデータを取り出すためのModelマネージャー #追加
class ThemesManager(models.Manager):
    def fetch_all_theme(self):
        return self.order_by('id').all()


class Themes(models.Model):
    title=models.CharField(max_length=255)
    user = models.ForeignKey(
        'accounts.Users',on_delete=models.CASCADE
    )
    
    #ThemeManagerと紐づけるためにobjectsを設定 #追加
    objects = ThemesManager()
    
    class Meta:
        db_table ='themes'
        
class Comments(models.Model):
    comment=models.CharField(max_length=1000)
    user = models.ForeignKey(
        'accounts.Users',on_delete=models.CASCADE
    )
    theme=models.ForeignKey(
        'Themes',on_delete=models.CASCADE
    )
    
    class Meta:
        db_table='comments'
```
views.pyからModelマネージャーを操作する
```python:views.py
#作成したThemeの一覧を表示する関数
def list_themes(request):
    #Themesでobjectsで紐付けたModelマネージャーの関数を呼び出す
    themes=Themes.objects.fetch_all_themes()
    return render(request,'boards/list_themes.html',context={
        'themes':themes
    })
```
表示するテンプレート
```html:list_themes.html
{%extends 'base.html'%}
{%block content%}
{% if messages %}
{%for message in messages%}
    <div>{{message.message}}</div>
{%endfor%}
{%endif%}
<h3>掲示板一覧画面</h3>
<p><a class = 'navbar-brand' href="{%url 'boards:create_theme'%}">掲示板作成画面</a></p>
<table class = 'table table-striped table-hover'>
    <thead>
        <tr>
            <th>#</th>
            <th>タイトル</th>
            <th>作成者</th>
        <tr>
    </thead>
    <tbody>
    {%for theme in themes%}
        <tr>
            <td>{{forloop.counter}}</td>
            <td>{{theme.title}}</td>
            <td>{{theme.user}}</td>
        </tr>
    {%endfor%}
    </tbody>
</table>

{%endblock%}
```

### 一覧画面から自分の記事は編集や削除ができるようにする
手順
urls.pyでedit_theme/\<int:id\>として遷移する。
views.pyで引数にrequestとidを受け取る。
get_object_or_404をつかってThemeモデルからそのidでデータを取得。
forms.pyで定義したフォームをつかってそれに取得したthemeをinstance=themeの形で渡して、
バリデーションしてsave処理を行う
```python:views.py

#urls.py
urlpatterns = [
    path('edit_theme/<int:id>',views.edit_theme,name='edit_theme'),
    path('delete_theme/<int:id>',views.delete_theme,name='delete_theme'),
]
###

#一覧から編集できるようにする
def edit_theme(request,id):
    theme = get_object_or_404(Themes,id=id)
    if theme.user.id !=request.user.id: #ログインしているユーザとthemeの持ち主が違う場合にエラーを発生させる
        #URLから直接idを打ってアクセスするのを防ぐ
        raise Http404
    edit_theme_form = forms.CreateThemeForm(request.POST or None,instance=theme)
    if edit_theme_form.is_valid():
        edit_theme_form.save()
        messages.success(request,'掲示板を更新しました')
        return redirect('boards:list_themes')
    return render(request,'boards/edit_theme.html',context={
        'edit_theme_form':edit_theme_form,
        'id':id #のちに編集画面で削除する他mにidを渡すのでこちらも渡しておく
    })

#編集画面から削除する
def delete_theme(request,id):
    theme = get_object_or_404(Themes, id=id)
    if theme.user.id != request.user.id:
        raise Http404
    delete_theme_form =forms.DeleteThemeForm(request.POST or None)
    if delete_theme_form.is_valid(): #入力は何もないけどcsrf_tokenのチェックを行う
        theme.delete()
        messages.success(request,'掲示板を削除しました')
        return redirect('boards:list_themes')
    return render(request,'boards/delete_theme.html',context={
            'delete_theme_form':delete_theme_form
        }
    )

#edit_theme.html一部抜粋
<p><a class = 'navbar-brand' href="{%url 'boards:delete_theme' id=id%}">削除する</a></p>
#編集画面から遷移できるようにする
```

### コメント機能を持たせる
urls.pyでpost_comments/\<int:theme_id>で遷移する
views.pyでrequestとtheme_idを引数にとる関数を作成して
themeをget_object_or_404でThemeモデルのidをtheme_idで指定して取ってくる。
forms.pyで作成したコメント投稿用のフォームを使ってそのフォームがis_validであれば
post_comment_form.instance　フォームのインスタンス、つまりCommentsモデルのthemeフィールド、userフィールドにそれぞれをいれて保存する
```python:urls.py
urlpatterns = [path('post_comments/<int:theme_id>',views.post_comments,name='post_comments'),
]
```

```python:views.py
#コメント機能を持たせる
def post_comments(request,theme_id):
    theme = get_object_or_404(Themes,id=theme_id)
    post_comment_form =forms.PostCommentForm(request.POST or None)
    #どのテーマに対するコメントなのか、だれがコメントしたのかをいれる
    if post_comment_form.is_valid():
        #models.pyで定義したCommentsモデルのフィールドにそれぞれをいれる
        post_comment_form.instance.theme = theme
        post_comment_form.instance.user = request.user
        post_comment_form.save()
        messages.success(request,'コメント送信完了')
        #コメントしたらコメントするページに飛ばす。theme_idを持たせることでその詳細ページへ
        return redirect('boards:post_comments',theme_id=theme_id)
    return render(request,'boards/post_comments.html',context={
        'post_comment_form':post_comment_form,
        'theme':theme #遷移先でまた使うためtheme情報を持たせる
    })
```
タイトルにリンクを作ってそちらから飛ぶ
```html:list_themes.html
<a href="{%url 'boards:post_comments' theme_id=theme.id%}">{{theme.title}}</a>
```

### コメント機能を持たせる その２
コメントを送信された内容をDBからとってきて表示させたい。
DBからデータをもってくるにはモデルマネージャーを使う
models.pyでobjectsを設定し、モデルマネージャーと紐付けをする
views.pyでモデルマネージャーで定義した関数を使う。
```python:views.py
#コメント機能を持たせる
def post_comments(request,theme_id):
    theme = get_object_or_404(Themes,id=theme_id)
    post_comment_form =forms.PostCommentForm(request.POST or None)
    #コメントされた内容を表示(モデルマネージャーで操作)
    comments=Comments.objects.fetch_by_theme_id(theme_id)
    #どのテーマに対するコメントなのか、だれがコメントしたのかをいれる
    if post_comment_form.is_valid():
        #ユーザが認証されていない場合にはエラーを発生させる
        if not request.user.is_authenticated:
            raise Http404
        #models.pyで定義したCommentsモデルのフィールドにそれぞれをいれる
        post_comment_form.instance.theme = theme
        post_comment_form.instance.user = request.user
        post_comment_form.save()
        messages.success(request,'コメント送信完了')
        #コメントしたらコメントするページに飛ばす。theme_idを持たせることでその詳細ページへ
        return redirect('boards:post_comments',theme_id=theme_id)
    return render(request,'boards/post_comments.html',context={
        'post_comment_form':post_comment_form,
        'theme':theme,#遷移先でまた使うためtheme情報を持たせる
        'comments':comments #commentsから遷移先でユーザの写真といった情報を扱うためcontextで渡す
    })
```

```html:post_comments.html
{%extends 'base.html'%}
{%block content%}
<h3>{{theme.title}}</h3>
{#コメントの表示#}
{%for comment in comments %}
    <div class = "col-1 offset-1">
        {%if comment.user.picture%}
            <img style="float:left;" width='50px' height='50px' src="{{comment.user.picture.url}}">
        {%endif%}
    </div>
    <div class = "col-8 offset-2">
        <p>名前：{{comment.user.username}}</p>
        {#改行を改行として認識させるためにフィルターのlinebreaksを使う#}
        <p>{{comment.comment|linebreaks}}</p>
    </div>
    <div class = "col-10 offset-1">
    <hr>
    </div>
{%endfor%}
{# ログインしているユーザのみコメントできるようにする#}
{% if user.is_authenticated %}
<div class ="col-4 offset-7"> 
<form method ="POST">
{% csrf_token %}
{{post_comment_form.as_p}}
<input type ='submit' value ='コメント送信'>
</form>
</div>
{%endif%}
{% if messages %}
    {%for message in messages%}
        <div>{{message.message}}</div>
    {%endfor%}
{%endif%}

{%endblock%}
```

またこのままでは画像が表示されない。プロジェクトの方のurls.pyに設定を追加
```python:プロジェクトのurls.py
#画像を表示するモードへ
from django.conf import settings
from django.conf.urls.static import static

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

if settings.DEBUG: #settings.pyのDEBUGがTrueのとき
    urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)
    
```

### AJAX(非同期通信),キャッシュ（一時保存）とは
非同期通信をおこなって画面遷移なくサーバとの情報のやり取りを行う
クライアント側ではJQuery
サーバ側ではJsonを返す処理が行われている
Ajaxこちらが勉強になる[Ajax(非同期通信)の基礎知識
](https://qiita.com/mdgwshk2021/items/83f88dd786fa03c87e6c#:~:text=%E5%BF%85%E8%A6%81%E3%81%A8%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82-,%E3%82%A8%E3%83%B3%E3%83%89%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88,%E3%82%A8%E3%83%B3%E3%83%89%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E3%81%A8%E3%81%84%E3%81%84%E3%81%BE%E3%81%99%E3%80%82)

キャッシュの設定はsettings.pyに記述する
[キャッシュ公式](https://docs.djangoproject.com/ja/3.1/topics/cache)

base.htmlにjQueryを読み込む[公式](https://releases.jquery.com/)
```python:base.html
<head>
<script src="https://code.jquery.com/jquery-3.7.0.js" integrity="sha256-JlqSTELeR4TLqP0OG9dxM7yDPqX1ox/HfgiSLBj8+kM=" crossorigin="anonymous"></script>
</head>

jsonを実行するためのblockも用意
{%block content%}
{%block javascript%}{%endblock%}
{%endblock%}
```
一時保存ボタンを押すとjQueryが実行される記述
```html:post_comments.html
#フォーム内省略
<input type='button' value='一時保存' id = "save_comment">

{#一時保存ボタンを押した時に実行されるスクリプト#}
{# #id_commentとはtextareaのidのこと。デベロッパーツールで見ればわかる #}
{%block javascript%}
<script>
$("#save_comment").click(function(){
    var comment =$("#id_comment").val();
    $.ajax({
        url:"{% url 'boards:save_comment' %}",
        type:"GET",
        data:{comment:comment,theme_id:"{{theme.id}}"},
        dataType:"json",
        success:function(json){
            if (json.message){
                alert(json.message);
            }
        },
        #エラーの場合はこちらを表示するといったこともできる（演習にて）
        error:function(error){
            alert(error.responseJSON.message);
        }
    });
});
</script>
```
if request.us_ajaxはDjango４系では使えなかったので代わりに
if request.headers.get('x-requested-with') == 'XMLHttpRequest':
を使うと上手く作動した

```python:views.py
from django.core.cache import cache
from django.http import JsonResponse

#キャッシュで一時保存する
def save_comment(request):
    # if request.is_ajax: #Django4系統では使えない
    if request.headers.get('x-requested-with') == 'XMLHttpRequest':
        comment=request.GET.get('comment')
        theme_id =request.GET.get('theme_id')
        if comment and theme_id:
            #存在する場合にキャッシュにデータを入れる
            cache.set(f'saved_comment-theme_id={theme_id}-user_id={request.user.id}',comment)
            return JsonResponse({'message':'一時保存しました'})
```
またこのままではキャッシュに保存しただけなので、
保存してまた同じページにきた時に初期から表示されるようにするのと、投稿が完了したらキャッシュを削除する機能を実装する
```python:views.py
#変更点のみ記述
def post_comments(request,theme_id):
    #!キャッシュに保存があればこちらを表示する.何もない場合は空白を取り出す
    saved_comment =cache.get(f'saved_comment-theme_id={theme_id}-user_id={request.user.id}','')

#初期値をフォームに与える
post_comment_form =forms.PostCommentForm(request.POST or None,initial={'comment':saved_comment})


#if post_comment_form.is_valid()内
post_comment_form.save()
        #!投稿した時にはキャッシュを削除する
        cache.delete(f'saved_comment-theme_id={theme_id}-user_id={request.user.id}')

#演習例
#get_context_dataでキャッシュを取得してそれらをコンテキストでフォームの中に初期値でいれるという方法もとれる
#InputAddressView(CreateView)内
#キャッシュに住所の保存があればそれを取得
    def get_context_data(self, **kwargs):
        context=super().get_context_data(**kwargs)
        address=cache.get(f'address_user_{self.request.user.id}')
        if address:
            context['form'].fields['zip_code'].initial=address.zip_code
            context['form'].fields['prefecture'].initial=address.prefecture
            context['form'].fields['address'].initial=address.address
        return context
```
urlでsave_commentに飛ぶわけでもないのにこのように書くのは疑問。
```python:urls.py
path('save_comment/',views.save_comment,name='save_comment'),
```

### キャッシュの設定と操作について
キャッシュの設定はsettings.pyに以下のように記述する
```python:settings.py
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': '127.0.0.1:11211',
    }
}
```
キャッシュの操作
```python
from django.core.cache import cache
cache.set('my_key','hello,world') #my_keyにhello,worldをキャッシュする
cache.get('my_key','default') #my_keyに該当する値をキャッシュから取り出して、存在しない時はdefaultを返す。（空白も可）
cache.clear() #キャッシュを全て削除する
cache.delete('a') #キャッシュからaに該当するものを削除する。
```

### 404エラーページの作成
accountsフォルダのviews.pyに記述
```python:accounts/views.py
#Http404エラーの表示
def show_error_page(request,exception):
    return render(request,'404.html',status=404)
```
プロジェクトのurls.py
```python:
#エラーハンドリング
from accounts.views import show_error_page
handler404 = show_error_page
```