見出し画像

djangoで作る本格的なSNSアプリケーション Part3

djangoで簡単なTodoアプリ等は作成できたけれども、オリジナルWebアプリを作る前にもう少しだけ本格的なアプリをチュートリアル形式で取り組みたい方を対象としています。

Part3では、目次の⑤ポストモデルとCRUD操作、発展的な機能の⑥ポストのお気に入り機能追加、⑦ユーザーのFollow/Unfollow機能の追加を実装していきます。Part1Part2の続きとなりますのでそれらを最初にご確認ください。

また、番外編としてallauthによる認証機能実装も紹介しておりますので、そちらもご興味あればご覧下さい。

開発の手順のおさらい

①SNSプロジェクトの開始とアプリの作成 ← Part 1
②ユーザーモデルとユーザー登録機能 ← Part 1
③ログイン、ログアウト機能 ← Part 2
④ユーザー情報の更新と一覧表示 ← Part 2
⑤ポストモデルとCRUD操作 ←今回
⑥ポストのお気に入り機能追加 ← Part4
⑦ユーザのFollow/Unfollow機能の追加 ← Part4
⑧JSによる各種機能の追加 ← Part4

Part1Part2でユーザー関連のaccountsアプリが完成しましたので、次は投稿関連のmicropostsアプリを作成していきます。

⑤ポストモデルとCRUD操作

この章では、micropostsアプリ内にPostモデルを作成しCRUD操作を実装していきます。少し長くなりますのでCreate、Read、Update、Deleteそれぞれを分けて解説していきます。

 ポストモデルの作成

まずは、microposts/models.pyを開きPostクラスを、models.Modelを継承し作成します。Post(投稿)には、投稿者(Owner)と内容(content)、作成時(created_at)をフィールドとして定義します。Postモデルのテーブル名は、postsとします。
 投稿者(Owner)とUserモデルを紐付けしたいので、外部キーForeignKeyでUserモデル('accounts.User')を指定します。ここで、ユーザーは複数の投稿を持ち、投稿者(owner)は一人としますので1対多の関係となります。verbose_nameは管理画面上の表示となります。on_delete=models.CASCADEは、Userモデルの該当ユーザーが削除された場合の処理を定義しています。もし、ichiroというユーザーが削除された場合には、ichiroがownerとなっている投稿も削除されるように設定しています。
 内容(content)は、テキストフィールドで255字以内としますのでcontent = models.TextField(max_length=255)とします。
 作成時(created_at)は、投稿ボタンを押した時間を自動的にセットしたいのでmodels.DateTimeField内にauto_now_add=Trueを指定します。

from django.db import models

class Post(models.Model):
   # Postのオーナーを設定する
   owner = models.ForeignKey('accounts.User', verbose_name='オーナー', on_delete=models.CASCADE)
   content = models.TextField(max_length=255)
   created_at = models.DateTimeField(auto_now_add=True)
   class Meta:
       db_table = 'posts'

ここまででモデル作成は完了しましたので、ターミナルからmicropostsアプリのマイグレーションを実施していきます。画像のように成功しましたら、データベースが作成されているかを確認します。それぞれのフィールドが正しく設定されているでしょうか?

python manage.py makemigrations microposts
python manage.py migrate
画像1
postテーブル作成後

 PostCreate Viewの実装(Create)

モデルが出来ましたで、次はviews.pyをCRUD操作のCreateとしてクラスベースビューで作成していきます。まずは、新規投稿画面を作成します。出来上がりは、以下の画像となるのでイメージを掴んで下さい。

画像3

microposts/views.pyPostCreateViewクラスをLoginRequiredMixin, CreateViewを継承しクラスベースビューで作成していきますので、それらをインポートします。この時点でのmicroposts/views.pyは以下となります。

from django.shortcuts import render, redirect
from django.views.generic.edit import CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Post
from .forms import PostCreateForm
from django.contrib import messages
from django.urls import reverse_lazy

# Create your views here.
class PostCreateView(LoginRequiredMixin, CreateView):
   template_name = 'microposts/create.html'
   form_class = PostCreateForm
   success_url = reverse_lazy('microposts:create')
   
   def form_valid(self, form):
       # formに問題なければ、owner id に自分のUser idを割り当てる     
       # request.userが一つのセットでAuthenticationMiddlewareでセットされている。
       form.instance.owner_id = self.request.user.id
       messages.success(self.request, '投稿が完了しました')
       return super(PostCreateView, self).form_valid(form)
       
   def form_invalid(self, form):
       messages.warning(self.request, '投稿が失敗しました')
       return redirect('microposts:create')

一つずつ解説していきます。前半は、必要なモデルやクラスベースビューなどをインポートしています。投稿機能は、クラスベースビューのCreateViewを継承します。
 ログインしているユーザーのみ投稿が出来るように制約を付けたいので、LoginRequiredMixinをインポートします。使用するモデルはPostモデルであり、投稿フォームはPostCreateFormとして後ほど作成しますので予めインポートします。投稿が成功したか、失敗したかのメッセージを表示したいのでdjango.contribからmessagesをインポートします。最後のreverse_lazyですが、CreateViewクラスが継承しているFormMixinクラスのクラス変数success_urlを指定するために必要となります。reverse関数を使うとImproperlyConfiguredというエラーが返されます。 


from django.shortcuts import render, redirect
from django.views.generic.edit import CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Post
from .forms import PostCreateForm
from django.contrib import messages
from django.urls import reverse_lazy

PostCreateViewLoginRequiredMixin,とCreateViewを継承し作成します。templateは、templates/ micropostsフォルダー内に作成していく事にします。ファイル名をcreate.htmlとしますので、template_name = 'microposts/create.html'となります。settings.pytemplatesフォルダを既に設定しているため、パスを通すときはtemplatesフォルダー以下を指定すれば良いです。
 投稿にはフォームが必要になってきますので、form_classとしてPostCreateFormを設定します。forms.pymicropostsフォルダー直下に作成し、後ほどPostCreateFormを作成します。
 新規投稿後には、同じページを読み込ませて投稿が成功したのか失敗したのかのメッセージを表示させる予定ですのでsuccess_urlreverse_lazyapp_name=micropostsの名前空間createを指定します。

class PostCreateView(LoginRequiredMixin, CreateView):
   template_name = 'microposts/create.html'
   form_class = PostCreateForm
   success_url = reverse_lazy('microposts:create')

次にフォームが正常だった場合の処理def form_valid(self, form):についてです。form.instance.owner_id = self.request.user.idの部分ですが、新規投稿フォームでいちいち投稿者が自分のユーザーIDを登録する事はしませんので、 form.instance.owner_idである投稿のowner_idフィールドに投稿者自身のユーザーIDを指定します。request.userが一つのセットでAuthenticationMiddlewareとしてdjangoで設定されています。user_idを取得したい場合はself.request.user.idとする事で取得する事ができます。
 投稿が成功した場合のメッセージは、messages.success(self.request, '投稿が完了しました')として、成功した場合のメッセージを指定します。これらの処理が完了した場合にPostCreateViewにフォームを返します。 

   def form_valid(self, form):
       # formに問題なければ、owner id に自分のUser idを割り当てる       
       # request.userが一つのセットでAuthenticationMiddlewareでセットされている。
       form.instance.owner_id = self.request.user.id
       messages.success(self.request, '投稿が完了しました')
       return super(PostCreateView, self).form_valid(form)

最後にフォームに異常があった場合処理def form_invalid(self, form):を記載していきます。投稿に失敗した場合にはワーニングメッセージを表示させるため、messages.warning(self.request, '投稿が失敗しました')として投稿失敗時のメッセージを指定します。投稿失敗後は、新規投稿画面を再度表示してメッセージを表示させたいためreturn redirect('microposts:create')として後ほどurls.pyで指定するapp_name = 'microposts'の名前空間createへリダイレクトさせます。

def form_invalid(self, form):
       messages.warning(self.request, '投稿が失敗しました')
       return redirect('microposts:create')

microposts/forms.pyPostCreateFormを作成していきます。forms.pyは以下のようになります。forms.pyですので、djangoからformsをインポートします。モデルも必要ですので、先ほど作成したPostモデルをインポートします。
 PostCreateFormforms.ModelFormを継承して作成していきます。Class Meta:として使用するモデル、フィールド、ウィジェットを指定します。モデルはPostモデルですので、model = Postとします。フィールドですが、ここでは投稿内容(content)だけをユーザーが入力出来るようにします。owner_idを自分自身で入力させる事も出来ますが、第一に面倒ですし正しいidを入力しない場合もありますので、PostCreateViewの中で自動で設定するようにしました。
 最後に投稿内容を入力する欄の見た目をwidgetとして設定します。投稿内容の入力欄は、Textareaとして5行30文字の領域を設定します。また、placeholderとして予め入力しておく内容を指定します。ここでは、'ここに入力してください'としています。

from django import forms
from .models import Post

class PostCreateForm(forms.ModelForm):
   class Meta:
       model = Post
       fields = (
           'content',
       )
       widgets = {
           'content': forms.Textarea(
               attrs={'rows': 5, 'cols': 30,
                      'placeholder': 'ここに入力してください'}
           ),
       }

次にurlを設定していきます。まずは、snsプロジェクトのurls.pymicropostsアプリへのパスを通します。sns/urls.pyを開き、urlpatternspath('microposts/', include('microposts.urls')),を追加します。

from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static  
from . import settings  
urlpatterns = [
   path('admin/', admin.site.urls),
   path('accounts/', include('accounts.urls')), 
   path('microposts/', include('microposts.urls')), # 追加
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

次は、アプリ側のurls.pyを設定します。microposts/urls.pyを開き、先ほど作成したPostCreateViewをインポートします。 app_namemicropostsとします。
  urlpatternsPostCreateViewのurlとして'create/'を設定し、名前空間を'create'とします。これで、microposts/createへアクセすればPostCreateViewで指定したcreate.htmlを画面に表示出来るようになります。

from django.urls import path
from .views import (
   PostCreateView
)
app_name = 'microposts'
urlpatterns = [
   path('create/', PostCreateView.as_view(), name='create'),
]

パスを通しましたので、対応するtemplateを作成します。ここでは、投稿画面をcreate.htmlとして作成していきます。コードは以下のようになります。

{% extends 'base.html' %}
{% block content %}
   <div class="content-wrapper">
       <div class="container-fluid">
           <div class="row">
               <div class="col-sm-6 offset-sm-3">
                   {#  views.pyからmessagesが渡された場合にのみメッセージを表示させる。#}
                   {% if messages %}
                       {% for message in messages %}
                           {#   message/tagsはSettings.pyで設定している#}
                           <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
                               {{ message }}
                           </li>
                       {% endfor %}
                   {% endif %}
                   <div class="card">
                       <div class="card-header">
                           <h4><b>新規投稿</b></h4>
                       </div>
                       <div class="card-body">
                           <form method="post">
                               {% csrf_token %}
                               {% bootstrap_form form %}
                               <button type="submit" class="btn btn-outline-primary btn-block">
                                   投稿する
                               </button>
                           </form>
                       </div>
                   </div>
               </div>
           </div>
       </div>
   </div>
{% endblock %}

以下の部分は、お決まりの部分ですので詳細の説明を省きます。

{% extends 'base.html' %}
{% block content %}
   <div class="content-wrapper">
       <div class="container-fluid">
           <div class="row">
               <div class="col-sm-6 offset-sm-3">
                ・・・省略・・・
               </div>
           </div>
       </div>
   </div>
{% endblock %}

新規投稿が成功もしくは失敗した時にはメッセージを表示したいので、{% if messages %}〜{% endif %}として、PostCreateViewからmessagesが渡された場合の処理を記載します。sns/settings.pyでメッセージタグを設定していました。メッセージのタグに応じて、 {{ message }}に適用するbootstrap4のクラスを以下のように指定します。
 <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>

{#  views.pyからmessagesが渡された場合にのみメッセージを表示させる。#}
{% if messages %}
    {% for message in messages %}
    {#   message/tagsはSettings.pyで設定している#}
     <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
    {{ message }}
     </li>
    {% endfor %}
{% endif %}

念の為、sns/settings.pyでメッセージタグを指定した部分を掲載します。

MESSAGE_TAGS = {
   messages.ERROR: 'alert alert-danger',
   messages.WARNING: 'alert alert-warning',
   messages.SUCCESS: 'alert alert-success',
   messages.INFO: 'alert alert-info'
}

create.htmlの説明に戻ります。ここでも、bootstrapのカードを使用します。card-headerとして、新規投稿を設定します。 <div class="card-body">〜</div>にフォームを設定していきます。投稿しますので、最初のフォームメソッド指定は<form method="post">となります。djangoでは、postの場合はcsrf_tokenを設定する決まりがありますので{% csrf_token %}となります。
 django-bootstrapでbootstrapを適用したフォームを表示するためには、{% bootstrap_form form %}とします。postsubmitするボタンをbootstrapを適用して、<button type="submit" class="btn btn-outline-primary btn-block">投稿する</button>とします。

<div class="card">
    <div class="card-header">
    <h4><b>新規投稿</b></h4>
    </div>
    <div class="card-body">
        <form method="post">
        {% csrf_token %}
        {% bootstrap_form form %}
        <button type="submit" class="btn btn-outline-primary btn-block">
           投稿する
        </button>
        </form>
    </div>
</div>

以上で投稿画面は完成です。ターミナルからサーバーを立ち上げて確認していきましょう。http://127.0.0.1:8000/microposts/create/へアクセスすると以下のように投稿画面が表示されています。

投稿画面作りたて

入力欄に何か入力して投稿してみましょう。ここでは、最初の投稿として投稿してみます。

最初の投稿

 投稿が成功した場合にメッセージが表示される事も確認して下さい。

投稿成功

投稿した内容が正しくpostテーブルに保存されているかも確認します。contentsには、最初の投稿が入力されています。created_atには投稿した日時、そしてowner_idは2が正しく保存されています。今回は、ichiroでログインして投稿しました。ichiroのユーザーidは2でしたので正しくowner_id=user.idがpostに反映されている事が確認できました。以上でCre

postテーブルコンテンツ

 PostListViewの実装(Read)

 次はPostListViewをCRUD操作のReadの例も兼ねてクラスベースビューで作成していきます。出来上がりは、以下の画像となるのでイメージを掴んで下さい。

画像8

microposts/views.pyPost ListViewクラスをLoginRequiredMixin, ListViewを継承しクラスベースビューで作成していきますので、それらをインポートします。この時点でのmicroposts/views.pyは以下となります。

from django.shortcuts import render, redirect
from django.views.generic.edit import CreateView
from django.views.generic import ListView # 追加
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Post
from .forms import PostCreateForm
from django.contrib import messages
from django.urls import reverse_lazy


# Create your views here.
class PostCreateView(LoginRequiredMixin, CreateView):
   template_name = 'microposts/create.html'
   form_class = PostCreateForm
   success_url = reverse_lazy('microposts:create')

   def form_valid(self, form):
       # formに問題なければ、owner id に自分のUser idを割り当てる        
       # request.userが一つのセットでAuthenticationMiddlewareでセットされている。
       form.instance.owner_id = self.request.user.id
       messages.success(self.request, '投稿が完了しました')
       return super(PostCreateView, self).form_valid(form)

   def form_invalid(self, form):
       messages.warning(self.request, '投稿が失敗しました')
       return redirect('microposts:create')

class PostListView(LoginRequiredMixin, ListView):   # 追加
   # テンプレートを指定
   template_name = 'microposts/list.html'
   # 利用するモデルを指定
   model = Post
   # ページネーションの表示件数
   paginate_by = 3

   # Postsテーブルの全データを取得するメソッド定義
   # テンプレートでは、object_listとしてreturnの値が渡される
   def get_queryset(self):
       return Post.objects.all()  

今回は、ListViewを継承して使いますので3行目でインポートしています。 PostListViewLoginRequiredMixin,とListViewを継承し作成します。
 templateは、templates/micropostsフォルダー内に作成する事にしていました。投稿一覧画面は、postlist.htmlとしますので、template_name = 'microposts/postlist.html'となります。modelは、Postモデルを指定します。
 投稿数が増えてくるとページネーションがあった方が表示が見易くなりますのでページネーションの表示件数をpaginate_by = 3と設定します。ここでは、取り敢えず3件としていますが好みに合わせて5件や10件と変更してみて下さい。
 テーブルからデータを取得しますので、get_querysetPostモデルからobjectsを全件取得するクエリセットを設定し、returnで返してtemplateへデータを渡します。これで、template側でデータを受け取る準備が出来ました。

from django.shortcuts import render, redirect
from django.views.generic.edit import CreateView

from django.views.generic import ListView # 追加

・・・省略・・・

class PostListView(LoginRequiredMixin, ListView):   # 追加
   # テンプレートを指定
   template_name = 'microposts/postlist.html'
   # 利用するモデルを指定
   model = Post
   # ページネーションの表示件数
   paginate_by = 3

   # Postsテーブルの全データを取得するメソッド定義
   # テンプレートでは、object_listとしてreturnの値が渡される
   def get_queryset(self):
       return Post.objects.all()  

次にパスを通します。microposts/urls.pyを開いて下さい。まずは、PostListViewをインポートしましょう。その後は、urlpatternsPostListViewのurlとして'postlist/'を設定し、名前空間を'postlist'とします。これで、microposts/postlistにアクセするとPostListViewで指定したmicroposts/postlist.htmlを画面に表示する事が出来ます。

from django.urls import path
from .views import (
   PostCreateView, PostListView, # 追加
)
app_name = 'microposts'
urlpatterns = [
   path('create/', PostCreateView.as_view(), name='create'),
   path('postlist/', PostListView.as_view(), name='postlist'), # 追加
]

対応するtemplateを作成します。postlist.htmlの作成がまだでしたので、ここで作成します。

postlist.htmlの場所

postlist.htmlのコードは以下の通りです。userlist.htmlと非常に似ていますのでコピーして必要な部分を変更する事にします。

{% extends 'base.html' %}
{% block content %}
   <div class="content-wrapper">
       <div class="container-fluid">
           <!--ページタイトル-->
           <div class="card mb-3">
               <div class="card-header">
                   <h4><b>投稿一覧</b></h4>
               </div>
               <div class="card-body">
                   <!-- テーブル表の定義 -->
                   <div class="table table-responsive">
                       <table id='post_list' class="table table-striped table-bordered table-hover">
                            <!-- 表の列の定義-->
                           <thead>
                           <tr>
                               <th class="text-center">投稿内容</th>
                               <th class="text-center">お気に入り</th>
                           </tr>
                           </thead>
                            <!-- ここまでが表の列の定義-->
                            <!-- 表のデータ部分の表示-->
                           <tbody>
                           {# デフォルトはobject_listとなるが、view.pyでcontextとして指定#}
                           {% for item in object_list %}
                               <tr class="text">
                                   <td class="text" style="width: 80%">
                                       <h6>{{ item.owner }}@{{ item.created_at}}</h6>{{ item.content }}</td>
                                   <td class="text-center align-middle" style="width: 20%">
                                      ・・・省略・・・
                                   </td>                                  
                               </tr>
                           {% endfor %}
                           </tbody>
                            <!-- ここまでが表のデータ部分の表示-->
                       </table>
                       <!-- ここまでがテーブル表の定義 -->
                   </div>
                   {#   django-bootstrap4のページネーション#}
                   {% bootstrap_pagination page_obj extra=request.GET.urlencode %}
               </div>
           </div>
       </div>
   </div>
{% endblock %}

{% extends 'base.html' %}でbase.htmlを読み込み{% block content %} 〜 {% endblock %}内に投稿一覧画面のためのコードを記載していきます。<div class="content-wrapper"> 〜 </div>、<div class="container-fluid">〜 </div>、<div class="card mb-3">〜 </div>、<div class="card mb-3">〜 </div>は先ほどと同じでbootstrap4のカードを使用しています<div class="card-header"> 〜 </div>内にカードヘッダーに表示する文字列を入力します。ここでは投稿一覧としています。<div class="card-body">〜 </div>に投稿一覧を表示する表をコーディングしていきます。

{% extends 'base.html' %}
{% block content %}
   <div class="content-wrapper">
       <div class="container-fluid">
           <!--ページタイトル-->
           <div class="card mb-3">
               <div class="card-header">
                   <h4><b>投稿一覧</b></h4> # 変更
               </div>
               <div class="card-body">
              ・・・省略・・・
               </div>
           </div>
       </div>
   </div>
{% endblock %}

<div class="table table-responsive">〜 </div>でbootstrapのレスポンシブなテーブルを適用します。<!-- 表の列の定義-->〜<!-- ここまでが表の列の定義-->内に表の列を指定します。今表示しようとしているコンテンツは、投稿内容だけですが、後々にお気に入りボタンも設置予定ですので予め列を用意しておきます。
 <!-- 表のデータ部分の表示-->〜<!-- ここまでが表のデータ部分の表示-->内に投稿内容を表示していきます。ここもuserlist.htmlの時とほぼ同じです。
 {% for item in object_list %} 〜 {% endfor %}では、object_listに入っているデータをitemに取り出してループさせています。object_listには、PostListView内でget_querysetで取得したPost .objects .all()が渡されます。
 投稿内容の列幅を<td class="text" style="width: 80%">で80%に指定します。投稿内容列には、投稿者名(owner)、投稿された日時(created_at)、投稿内容(content)を表示させます。<h6>{{ item.owner }}@{{ item.created_at}}</h6>{{ item.content }}
 お気に入り列は、取り敢えず ・・・省略・・・ という文字列を入力しておきます。ここには、後の章でお気に入りボタンを設置します。
 最後の行がdjango-bootstrap4を用いたページネーションです。ページネーションの表示も非常に簡単です。たった1行追加するだけです。詳細は、公式ページのリンクを参照下さい。
 {% bootstrap_pagination page_obj extra=request.GET.urlencode %}

<div class="card-body">
   <!-- テーブル表の定義 -->
   <div class="table table-responsive">
       <table id='post_list' width="100%" class="table table-striped table-bordered table-hover">
       <!-- 表の列の定義-->
       <thead>
         <tr>
           <th class="text-center">投稿内容</th>
           <th class="text-center">お気に入り</th>
         </tr>
       </thead>
       <!-- ここまでが表の列の定義-->
       <!-- 表のデータ部分の表示-->
       <tbody>
       {# デフォルトはobject_listとなるが、view.pyでcontextとして指定することも出来る#}
       {% for item in object_list %}
           <tr class="text">
              <td class="text" style="width: 80%">
              <h6>{{ item.owner }}@{{ item.created_at}}</h6>{{ item.content }}</td>
              <td class="text-center align-middle" style="width: 20%">
              ・・・省略・・・
              </td>                                  
          </tr>
       {% endfor %}
       </tbody>
       <!-- ここまでが表のデータ部分の表示-->
   </table>
   <!-- ここまでがテーブル表の定義 -->
</div>
{#   django-bootstrap4のページネーション#}
{% bootstrap_pagination page_obj extra=request.GET.urlencode %}

ターミナルからサーバーを立ち上げて下さい。今回は、ページネーションが正しく動作するかも確認しますので、投稿を4件以上追加しておいて下さい。
 投稿内容の列には、投稿者名、投稿日時、投稿内容が反映されています。また、投稿件数が4件以上ありますのでページネーションも設定されています。ページ2をクリックして次のページに飛んでみましょう。別のユーザー(taro)が登録した内容もしっかり反映されている事が確認できました。以上で投稿一覧画面の作成は完了です。

画像10
画像11

 PostUpdateViewの実装(Update)

次は、CRUD操作のUpdateであるPostUpdateViewを実装していきます。出来上がりのイメージを以下の画像から掴んで下さい。

画像12

microposts/views.pyPostUpdateViewクラスをLoginRequiredMixin, UpdateViewを継承しクラスベースビューで作成していきますので、それらをインポートします。この時点でのmicroposts/views.pyは以下となります。

from django.shortcuts import render, redirect
from django.views.generic.edit import CreateView, UpdateView # 追加
from django.views.generic import ListView 
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Post
from .forms import PostCreateForm, PostUpdateForm
from django.contrib import messages
from django.urls import reverse_lazy

# Create your views here.
class PostCreateView(LoginRequiredMixin, CreateView):
   template_name = 'microposts/create.html'
   form_class = PostCreateForm
   success_url = reverse_lazy('microposts:create')
   def form_valid(self, form):
       # formに問題なければ、owner id に自分のUser idを割り当てる        
       # request.userが一つのセットでAuthenticationMiddlewareでセットされている。
       form.instance.owner_id = self.request.user.id
       messages.success(self.request, '投稿が完了しました')
       return super(PostCreateView, self).form_valid(form)
   def form_invalid(self, form):
       messages.warning(self.request, '投稿が失敗しました')
       return redirect('microposts:create')
       
class PostListView(LoginRequiredMixin, ListView):   
   # テンプレートを指定
   template_name = 'microposts/postlist.html'
   # 利用するモデルを指定
   model = Post
   # ページネーションの表示件数
   paginate_by = 3
   # Postsテーブルの全データを取得するメソッド定義
   # テンプレートでは、object_listとしてreturnの値が渡される
   def get_queryset(self):
       return Post.objects.all()  
       
class PostUpdateView(LoginRequiredMixin, UpdateView): # 追加
   model = Post
   form_class = PostUpdateForm
   template_name = 'microposts/update.html'
   def form_valid(self, form):
       messages.success(self.request, '更新が完了しました')
       return super(PostUpdateView, self).form_valid(form)
       
   def get_success_url(self):
       return reverse_lazy('microposts:update', kwargs={'pk': self.object.id})  
        
   def form_invalid(self, form):
       messages.warning(self.request, '更新が失敗しました')
       return reverse_lazy('microposts:update', kwargs={'pk': self.object.id})
      

 今回は、UpdateViewを継承して使いますので2行目でインポートしています。 PostUpdateViewLoginRequiredMixin,とUpdateViewを継承し作成します。投稿の更新フォームはPost UpdateFormとして後ほど作成しますので予めインポートします。 
 modelは、Postモデルを指定します。投稿にはフォームが必要になってきますので、form_classとしてPostUpdateFormを設定します。templateは、templates/micropostsフォルダー内に作成する事にしていました。投稿の更新画面は、update.htmlとしますので、template_name = 'microposts/update.html'となります。
 次にフォームが正常だった場合の処理def form_valid(self, form):についてです。投稿が成功した場合のメッセージは、messages.success(self.request, '更新が完了しました')として、成功した場合のメッセージを指定します。これらの処理が完了した場合にPostUpdateViewにフォームを返します。 
 また、def get_success_url(self):で更新が成功した時のurlを指定します。ここは少し特殊です。これまでのように、redirect('microposts:update')とする事は出来ません。更新には、その投稿を識別するid(pk=プライマリーキー)を指定する必要があります。reverse_lazy’リダイレクト先’, kwargs = 'pk':'リダイレクト先のpk')として指定します。更新成功後も同じ記事に留まりメッセージを表示させたいため、リダイレクト先は'microposts:update'、記事のpkはself.object.idとして記事のidをpkに指定します。 
 また、フォームに異常があった場合処理def form_invalid(self, form):を記載していきます。投稿に失敗した場合にはワーニングメッセージを表示させるため、messages.warning(self.request, '更新が失敗しました')として投稿失敗時のメッセージを指定します。投稿失敗後は、新規投稿画面を再度表示してメッセージを表示させたいため先ほどと同じようにreverse_lazyで指定します。

from django.shortcuts import render, redirect
from django.views.generic.edit import CreateView, UpdateView # 追加
from django.views.generic import ListView 
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Post
from .forms import PostCreateForm, PostUpdateForm

・・・省略・・・

class PostUpdateView(LoginRequiredMixin, UpdateView): # 追加
   model = Post
   form_class = PostUpdateForm
   template_name = 'microposts/update.html'
   
   def form_valid(self, form):
       messages.success(self.request, '更新が完了しました')
       return super(PostUpdateView, self).form_valid(form)
    
   def get_success_url(self):
       return reverse_lazy('microposts:update', kwargs={'pk': self.object.id})
       
   def form_invalid(self, form):
       messages.warning(self.request, '更新が失敗しました')
       return reverse_lazy('microposts:update', kwargs={'pk': self.object.id})      
   

microposts/forms.pyPostUpdateFormを作成していきます。forms.pyは以下のようになります。追加する部分は、PostUpdateFormの部分になります。PostCreateViewとほとんど同じになりますので、コピーして使いましょう。
 変更した部分は、widgetsattrs=の部分だけになります。PostCreateFormでは、新規作成ですのでまだ文字列が入力されていませんでしたのでplaceholderを設定しました。しかし、今回は更新ですので既にcontentが存在しますのでplaceholderが不要になります。 

from django import forms
from .models import Post

class PostCreateForm(forms.ModelForm):
   class Meta:
       model = Post
       fields = (
           'content',
       )
       widgets = {
           'content': forms.Textarea(
               attrs={'rows': 5, 'cols': 30,
                      'placeholder': 'ここに入力してください'}
           ),
       }

class PostUpdateForm(forms.ModelForm): #追加
   class Meta:
       model = Post
       fields = (
           'content',
       )
       widgets = {
           'content': forms.Textarea(
               attrs={'rows': 5, 'cols': 30} #変更部分
           ),
       }

次にパスを通します。microposts/urls.pyを開いて下さい。まずは、Post UpdatetViewをインポートしましょう。その後は、urlpatternsPost UpdateViewのurlとして'update/<int:pk>'を設定し、名前空間を'update'とします。投稿を一意に指定するためには、pkを指定する必要がありますので'update/<int:pk>'で指定しています。これで、microposts/update/pkにアクセするとPostUpdateViewで指定したmicroposts/postlist.htmlを画面に表示する事が出来ます。

from django.urls import path
from .views import (
   PostCreateView, PostListView, 
   PostUpdateView, # 追加
)
app_name = 'microposts'
urlpatterns = [
   path('create/', PostCreateView.as_view(), name='create'),
   path('postlist/', PostListView.as_view(), name='postlist'), 
   path('update/<int:pk>', PostUpdateView.as_view(), name='update'),# 追加
]

対応するtemplateを作成します。update.htmlの作成がまだでしたので、ここで作成します。update.htmlのコードは以下の通りです。create.htmlと非常に似ていますのでコピーして必要な部分を変更する事にします。

{% extends 'base.html' %}
{% block content %}
   <div class="content-wrapper">
       <div class="container-fluid">
           <div class="row">
               <div class="col-sm-6 offset-sm-3">
                   {#  views.pyからmessagesが渡された場合にのみメッセージを表示させる。#}
                   {% if messages %}
                       {% for message in messages %}
                           {#   message/tagsはSettings.pyで設定している#}
                           <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
                               {{ message }}
                           </li>
                       {% endfor %}
                   {% endif %}
                   <div class="card">
                       <div class="card-header">
                           <h4><b>投稿の更新</b></h4>
                       </div>
                       {#  投稿したユーザーしか削除できない制約を追加#}
                       {% if object.owner_id == user.id %}
                           <div class="card-body">
                           <form method="post">
                               {% csrf_token %}
                               {% bootstrap_form form %}
                               <button type="submit" class="btn btn-outline-success btn-block">
                                   更新する
                               </button>
                           </form>
                       {% else %}
                           <p class='font-weight-bold text-danger text-center'>
                               {{ user.get_username }}さんの投稿ではありません。
                           </p>
                       {% endif %}
                       </div>
                   </div>
               </div>
           </div>
       </div>
   </div>
{% endblock %}

create.htmlと異なる部分である{# 投稿したユーザーしか削除できない制約を追加#}以降を説明します。投稿は、投稿した本人しか投稿できないような制約をつける必要がありますのでif文でその制約を入れます。
 {% if object.owner_id == user.id %}ですが、記事のowner_idと今ログインしているユーザのID(user.id)が同じ場合にのみ投稿できるように<form method="post">〜</form>内に更新フォームを設置します。
 それ以外の場合は、{% else %}として{{ user.get_username }}さんの投稿ではありません。という警告文を表示させるようにしています。{{ user.get_username }}とする事でログインしているユーザーのusernameを取得し表示する事ができます。これ以外はほぼ全てcreate.htmlと同じになります。

{#  投稿したユーザーしか削除できない制約を追加#}
{% if object.owner_id == user.id %}
<div class="card-body">
    <form method="post">
        {% csrf_token %}
        {% bootstrap_form form %}
        <button type="submit" class="btn btn-outline-success btn-block">
            更新する
        </button>
    </form>
{% else %}
    <p class='font-weight-bold text-danger text-center'>
        {{ user.get_username }}さんの投稿ではありません。
    </p>
{% endif %}
</div>

ターミナルからサーバーを立ち上げて下さい。まだ更新ページへのリンクボタンを設置していませんので、ブラウザのアドレスバーに直接http://127.0.0.1:8000/microposts/update/1を入力してみましょう。

スクリーンショット 2021-06-13 午後23.55.08 午後

正しく表示されているでしょうか?1番目の記事は今ログインしているichiroの記事でしたので、投稿の更新画面が表示されました。それでは、ichiro以外の人の記事にアクセスするとどうなるかを試してみます。
 ポストテーブル内を確認しichiro以外の投稿であるid=6にアクセスしてみます。アドレスバーにhttp://127.0.0.1:8000/microposts/update/6を入力します。

ポストテーブル内容

ichiroさんの投稿ではありません。と赤字で表示され更新が出来ないようになっています。先ほどif文で設定した条件分岐が正しく作動することが確認できました。

画像15

最後にhttp://127.0.0.1:8000/microposts/update/1へ再度アクセスして投稿を更新してみましょう。更新が成功すれば、以下のように更新が完了しましたというメッセージが表示されます。以上で投稿の更新画面の作成は完了です。

更新成功

 PostDeleteViewの実装(Delete)

次は、CRUD操作の最後DeleteであるPost DeleteViewを実装していきます。出来上がりのイメージを以下の画像から掴んで下さい。

投稿を削除しますか

microposts/views.pyPostDeleteViewクラスをLoginRequiredMixin, DeleteViewを継承しクラスベースビューで作成していきますので、それらをインポートします。この時点でのmicroposts/views.pyは以下となります。

from django.shortcuts import render, redirect
from django.views.generic.edit import CreateView, UpdateView 
from django.views.generic import ListView , DeleteView # 追加
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Post
from .forms import PostCreateForm, PostUpdateForm
from django.contrib import messages
from django.urls import reverse_lazy

# Create your views here.
class PostCreateView(LoginRequiredMixin, CreateView):
   template_name = 'microposts/create.html'
   form_class = PostCreateForm
   success_url = reverse_lazy('microposts:create')
   def form_valid(self, form):
       # formに問題なければ、owner id に自分のUser idを割り当てる        
       # request.userが一つのセットでAuthenticationMiddlewareでセットされている。
       form.instance.owner_id = self.request.user.id
       messages.success(self.request, '投稿が完了しました')
       return super(PostCreateView, self).form_valid(form)
   def form_invalid(self, form):
       messages.warning(self.request, '投稿が失敗しました')
       return redirect('microposts:create')

class PostListView(LoginRequiredMixin, ListView):   
   # テンプレートを指定
   template_name = 'microposts/postlist.html'
   # 利用するモデルを指定
   model = Post
   # ページネーションの表示件数
   paginate_by = 3
   # Postsテーブルの全データを取得するメソッド定義
   # テンプレートでは、object_listとしてreturnの値が渡される
   def get_queryset(self):
       return Post.objects.all()  

class PostUpdateView(LoginRequiredMixin, UpdateView): 
   model = Post
   form_class = PostUpdateForm
   template_name = 'microposts/update.html'
   def form_valid(self, form):
       messages.success(self.request, '更新が完了しました')
       return super(PostUpdateView, self).form_valid(form)
   def get_success_url(self):
       return reverse_lazy('microposts:update', kwargs={'pk': self.object.id})
   def form_invalid(self, form):
       messages.warning(self.request, '更新が失敗しました')
       return reverse_lazy('microposts:update', kwargs={'pk': self.object.id})

class PostDeleteView(LoginRequiredMixin, DeleteView):# 追加
   model = Post
   template_name = 'microposts/delete.html'
   
   # deleteviewでは、SuccessMessageMixinが使われないので設定する必要あり
   success_url = reverse_lazy('microposts:myposts')
   success_message = "投稿は削除されました。"
   # 削除された際にメッセージが表示されるようにする。
   def delete(self, request, *args, **kwargs):
       messages.success(self.request, self.success_message)
       return super(PostDeleteView, self).delete(request, *args, **kwargs)

 今回は、DeleteViewを継承して使いますので3行目でインポートしています。 PostDeleteViewLoginRequiredMixin,とDeleteViewを継承し作成します。 
 modelは、Postモデルを指定します。投稿の更新画面は、delete.htmlとしますので、template_name = 'microposts/delete.html'となります。
 ここでdeleteviewでは、SuccessMessageMixinが使われないので設定する必要あります。投稿削除に成功した時のリダイレクト先をsuccess_url = reverse_lazy('microposts:create')として設定します。その時のメッセージをsuccess_messageとして 投稿は削除されました。を設定します。
 次は、削除が実行された時の処理def delete(self, request, *args, **kwargs):です。messages.success(self.request, self.success_message)とすることで、先ほど設定した 投稿は削除されました。というメッセージを表示させまてreturn super(PostDeleteView, self).delete(request, *args, **kwargs)PostDeleteViewに返します。

from django.shortcuts import render, redirect
from django.views.generic.edit import CreateView, UpdateView 
from django.views.generic import ListView , DeleteView # 追加

・・・省略・・・

class PostDeleteView(LoginRequiredMixin, DeleteView):# 追加
   model = Post
   template_name = 'microposts/delete.html'
   
   # deleteviewでは、SuccessMessageMixinが使われないので設定する必要あり
   success_url = reverse_lazy('microposts:myposts')
   success_message = "投稿は削除されました。"
   
   # 削除された際にメッセージが表示されるようにする。
   def delete(self, request, *args, **kwargs):
       messages.success(self.request, self.success_message)
       return super(PostDeleteView, self).delete(request, *args, **kwargs)

次にパスを通します。microposts/urls.pyを開いて下さい。まずは、Pos DeleteViewをインポートしましょう。その後は、urlpatternsPostDeleteViewのurlとして'delete/<int:pk>'を設定し、名前空間を'delete'とします。更新の時と同じように投稿を一意に指定するためには、pkを指定する必要がありますので'delete/<int:pk>'で指定しています。これで、microposts/delete/pkにアクセスするとPost DeleteViewで指定したmicroposts/delete.htmlを画面に表示する事が出来ます。

from django.urls import path
from .views import (
   PostCreateView, PostListView, 
   PostUpdateView, PostDeleteView # 追加
)
app_name = 'microposts'
urlpatterns = [
   path('create/', PostCreateView.as_view(), name='create'),
   path('postlist/', PostListView.as_view(), name='postlist'), 
   path('update/<int:pk>', PostUpdateView.as_view(), name='update'),
   path('delete/<int:pk>', PostDeleteView.as_view(), name='delete'),# 追加
]

対応するtemplateを作成します。delete.htmlの作成がまだでしたので、ここで作成します。delete.htmlのコードは以下の通りです。削除するかどうかの確認をするだけのシンプルなページとしています。
 ここでも、投稿したユーザーしか削除できな制約をif文で追加しています。ここは、update.htmlと同じですので詳細な説明は省きます。

{% extends 'base.html' %}
{% block content %}
{#    投稿したユーザーしか削除できない制約を追加#}
   {% if object.owner_id == user.id %}
       <form method="POST">
       {% csrf_token %}
       <p>「{{ object.content }}」を削除しますか?</p>
       <button type="submit" class="btn btn-default btn-danger">
           削除する
       </button>
   </form>
       {% else %}
       <h1>{{ user.get_username }}さんの投稿ではありません。</h1>
   {% endif %}
{% endblock %}

ターミナルからサーバーを立ち上げて下さい。まだ削除ページへのリンクボタンを設置していませんので、ブラウザのアドレスバーに直接http://127.0.0.1:8000/microposts/delete/1を入力してみましょう。削除するコメントが表示され削除するかを確認します。 削除するボタンを押す事で記事が削除され、新規投稿ページへ成功メッセージ(投稿は削除されました)付きでリダイレクトされます。

スクリーンショット 2021-06-14 午後00.33.26 午前
投稿は削除されました

データベースのpostテーブルからも投稿が削除されていることを確認します。今回は、id=1の投稿を削除しましたので、下の画像でも確かに記事が削除されていることが確認できます。

投稿を削除後のポストテーブル

また、本人の記事以外を削除しようとすると、以下のようにichiroさんの投稿ではありませんと表示されて記事が削除されません。

あなたの投稿ではありません

少し野暮ったいところもありますが、最低限の表示は出来るようにしておりますので後は好みで表示を変更してみましょう。以上でmicropostsのCRUD操作全てが実装できました。このCRUD操作が出来れば他のアプリへ応用が出来ます。
 次はリストビューを継承させて、自分の投稿一覧ページを作成していきます。また、そのページにはプロフィール画像も表示させてプロフィール変更画面へのリンクも付けることにします。

 自分の投稿一覧画面

自分の投稿一覧画面をこれから作っていきます。自分のフォロワー(followers)やフォロー相手(followings)などについても確認出来て、プロフィール画像も表示できるよにしていきます。言葉では分かり難いと思いますので、以下の画像で出来上がりイメージを掴んで下さい。先ほど作成した投稿の更新/削除へのリンクも記事の横に設置することにします。

画像22

microposts/views.pyMyPostsViewクラスをLoginRequiredMixin, ListViewを継承しクラスベースビューで作成していきます。特に新たにインポートする必要はありません。この時点でのmicroposts/views.pyは以下となります。今回追加したのは、MyPostsViewクラスだけになります。

from django.shortcuts import render, redirect
from django.views.generic.edit import CreateView, UpdateView 
from django.views.generic import ListView , DeleteView 
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Post
from .forms import PostCreateForm, PostUpdateForm
from django.contrib import messages
from django.urls import reverse_lazy

# Create your views here.
class PostCreateView(LoginRequiredMixin, CreateView):
   template_name = 'microposts/create.html'
   form_class = PostCreateForm
   success_url = reverse_lazy('microposts:create')
   def form_valid(self, form):
       # formに問題なければ、owner id に自分のUser idを割り当てる        
       # request.userが一つのセットでAuthenticationMiddlewareでセットされている。
       form.instance.owner_id = self.request.user.id
       messages.success(self.request, '投稿が完了しました')
       return super(PostCreateView, self).form_valid(form)
   def form_invalid(self, form):
       messages.warning(self.request, '投稿が失敗しました')
       return redirect('microposts:create')
class PostListView(LoginRequiredMixin, ListView):   
   # テンプレートを指定
   template_name = 'microposts/postlist.html'
   # 利用するモデルを指定
   model = Post
   # ページネーションの表示件数
   paginate_by = 3
   # Postsテーブルの全データを取得するメソッド定義
   # テンプレートでは、object_listとしてreturnの値が渡される
   def get_queryset(self):
       return Post.objects.all()  
class PostUpdateView(LoginRequiredMixin, UpdateView): 
   model = Post
   form_class = PostUpdateForm
   template_name = 'microposts/update.html'
   def form_valid(self, form):
       messages.success(self.request, '更新が完了しました')
       return super(PostUpdateView, self).form_valid(form)
   def get_success_url(self):
       return reverse_lazy('microposts:update', kwargs={'pk': self.object.id})
   def form_invalid(self, form):
       messages.warning(self.request, '更新が失敗しました')
       return reverse_lazy('microposts:update', kwargs={'pk': self.object.id})
class PostDeleteView(LoginRequiredMixin, DeleteView):
   model = Post
   template_name = 'microposts/delete.html'
   # deleteviewでは、SuccessMessageMixinが使われないので設定する必要あり
   success_url = reverse_lazy('microposts:create')
   success_message = "投稿は削除されました。"
   # 削除された際にメッセージが表示されるようにする。
   def delete(self, request, *args, **kwargs):
       messages.success(self.request, self.success_message)
       return super(PostDeleteView, self).delete(request, *args, **kwargs)

class MyPostsView(LoginRequiredMixin, ListView): # 追加
   # テンプレートを指定
   template_name = 'microposts/myposts.html'
   # 利用するモデルを指定
   model = Post
   # ページネーションの表示件数
   paginate_by = 3
   
   # Postsテーブルのowner_idが自分自身の全データを取得するメソッド定義
   def get_queryset(self):  # 自分の投稿オブジェクトを返す。
       return Post.objects.filter(owner_id=self.request.user)
       
   def get_context_data(self, **kwargs):
       context = super().get_context_data(**kwargs)
       
       # Postsテーブルの自分の投稿数をmy_posts_countへ格納
       context['my_posts_count'] = Post.objects.filter(owner_id=self.request.user).count()
       return context

MyPostsViewは、LoginRequiredMixin,とListViewを継承して作成します。
自分の投稿一覧画面は、myposts.htmlとしますので、template_name = 'microposts/myposts.html'となります。modelは、Postモデルを指定します。
 投稿数が増えてくるとページネーションがあった方が表示が見易くなりますのでページネーションの表示件数をpaginate_by = 3と設定します。これまでは、templateへは一セットのデータをobjects_listとして渡していましたが、複数のデータを渡したい場合が出てきます。そのような場合には、get_queryset()メソッドではなく、get_context_data()メソッドを使用します。
 少し助長になりますが、get_queryset()メソッドとget_context_data()メソッドの両方を使用してみます。get_queryset()メソッドにて、テーブルからデータを取得しますが、今回は自分の投稿のみを取得しますのでフィルターを使用します。Post .objects .all()とすると、ポストテーブルの全件が取得できました。フィルターで条件をつけたい場合は、objectsの後をfilterとして条件を指定します。
 qs = Post.objects.filter(owner_id=self.request.user)
 ここでは、Postモデルのowner_idself.request.user(自分自身)という指定をしていますので、自分の投稿だけが返されるクエリをqsとして生成します。return qs として、templateqsを渡します。template側ではobject_listとしてデータを受け取ることが出来ます。
 次にget_context_data()メソッドの説明です。引数は、self, **kwargsとなります。**kwargsですが、複数のキーワード引数を辞書として受け取ることができます。自分の投稿数も分かるようにしたいため、my_posts_countという名前のコンテキストをcontext['my_posts_count']として設定します。自分の投稿数は、先ほどのqsに対してcountメソッドを使うことで取得する事が可能です。contextは名前を指定し、 return contextとする事で複数のデータをtemplateに渡す事が出来ます。

class MyPostsView(LoginRequiredMixin, ListView): # 追加
   # テンプレートを指定
   template_name = 'microposts/myposts.html'
   # 利用するモデルを指定
   model = Post
   # ページネーションの表示件数
   paginate_by = 3
   
   # Postsテーブルのowner_idが自分自身の全データを取得するメソッド定義
   def get_queryset(self):  # 自分の投稿オブジェクトを返す。
       qs = Post.objects.filter(owner_id=self.request.user)
       return qs
       
   def get_context_data(self, **kwargs):
       context = super().get_context_data(**kwargs)      
       
       qs = Post.objects.filter(owner_id=self.request.user)
       # qsのレコード数をmy_posts_countというコンテキストとして設定
       context['my_posts_count'] = qs.count()
       return context

次にパスを通します。microposts/urls.pyを開いて下さい。まずは、MyPostsViewをインポートしましょう。その後は、urlpatternsMyPostsViewのurlとして'myposts/'を設定し、名前空間を'myposts'とします。これで、microposts/mypostsにアクセするとMyPostsViewで指定したmicroposts/myposts.htmlを画面に表示する事が出来ます。

from django.urls import path
from .views import (
   PostCreateView, PostListView, 
   PostUpdateView, PostDeleteView,
   MyPostsView # 追加
)
app_name = 'microposts'
urlpatterns = [
   path('create/', PostCreateView.as_view(), name='create'),
   path('postlist/', PostListView.as_view(), name='postlist'), 
   path('update/<int:pk>', PostUpdateView.as_view(), name='update'),
   path('delete/<int:pk>', PostDeleteView.as_view(), name='delete'),
   path('myposts/', MyPostsView.as_view(), name='myposts'),# 追加
]

対応するtemplateであるmyposts.htmltemplates/micropostsフォルダー内に作成します。myposts.htmlのコードは以下の通りです。これまでよりも複雑になります。まずは、出来上がり画面を再度確認しイメージを掴みましょう。

{% extends 'base.html' %}
{% load static %}
{% block content %}
   <div class="content-wrapper">
       <div class="container-fluid">
           <div class="row">
               <!--ページタイトル-->
               <aside class="col-sm-4">
                   <div class="card">
                       <div class="card-header">
                           <h3 class="card-title text-center">{{ user.get_username }}</h3>
                       </div>
                       <div class="card-body">
                           {# ユーザーがプロフィール画像を持っている場合#}
                           {% if user.avatar %}
                               <img class="rounded img-fluid mx-auto d-block"
                                   src="{{ user.avatar.url }}" id="avatar-image" alt="avatar_image">
                               {# ユーザーがプロフィール画像を持っている場合はデフォルト画像を表示#}
                           {% else %}
                               <img class="rounded img-fluid mx-auto d-block"
                                   src="{% static 'images/avator_default.png' %}" id="avatar-image" alt="avatar_image">
                           {% endif %}
                       </div>
                       <a class="btn btn-outline-secondary btn-sm"
                           href="{% url 'accounts:edit_profile' %}" role="button">プロフィール更新</a>
                   </div>
               </aside>
               <div class="col-sm-8">
                   <ul class="nav nav-tabs nav-justified mb-3">
                       <li class="nav-item"><a href="{% url 'microposts:myposts' %}" class="nav-link active">MyPosts一覧
                           {#  object_listのlengthを取得することで投稿数をカウントできる。#}
                           <span class="badge badge-secondary">{{ my_posts_count }}</span></a></li>
                       <li class="nav-item"><a href="#" class="nav-link">Followings
                           {#  フォロワー数を表示#}
                           <span class="badge badge-secondary">"#"</span></a></li>
                       <li class="nav-item"><a href="#" class="nav-link">Followers
                           {#  フォロワー数を表示#}
                           <span class="badge badge-secondary">"#"</span></a></li>
                   </ul>
                   <!-- テーブル表の定義 -->
                   <div class="table table-responsive">
                       <table id='post_list'
                              class="table table-striped table-bordered table-hover">
                            <!-- 表の列の定義-->
                           <thead>
                           <tr>
                               <th class="text-center" style="width: 80%">投稿内容</th>
                               <th class="text-center" style="width: 20%">更新/削除</th>
                           </tr>
                           </thead>
                            <!-- ここまでが表の列の定義-->
                            <!-- 表のデータ部分の表示-->
                           <tbody>
                           {% for item in object_list %}
                               <tr class="text">
                                   <td class="text"><h6>@{{ item.created_at }}</h6>{{ item.content }}</td>
                                   <td class="text-center align-middle">
                                       {#  pkを指定する際は、urlタグはやや特殊な書き方となる↓以下参照#}
                                       <a class="btn btn-outline-success btn-sm"
                                          href="{% url 'microposts:update' item.pk %}"
                                          role="button">更新</a>
                                       <a class="btn btn-outline-danger btn-sm"
                                          href="{% url 'microposts:delete' item.pk %}"
                                          role="button">削除</a>
                                   </td>
                               </tr>
                           {% endfor %}
                           </tbody>
                            <!-- ここまでが表のデータ部分の表示-->
                       </table>
                       <!-- ここまでがテーブル表の定義 -->
                   </div>
                   {#   django-bootstrap4のページネーション#}
                   {% bootstrap_pagination page_obj extra=request.GET.urlencode %}
               </div>
           </div>
       </div>
   </div>
{% endblock %}
画像23

画面の左側は、bootstrapのカードを使用しています。そのカード内にプロフィール画像を表示させ、さらにその下にプロフィール更新画面へのリンクボタンを設置しています。
 画面の右側はbootstrapのnav-tabsを適用してタブを作成しています。詳細説明は、公式のリンクから確認ください。タブは3つ用意して、Myposts一覧FollowingsFollowersと切り替えられるようにしていきます。それらの横には、bootstrapのbadgeで投稿数やフォロワー数を表示します。 また、タブの下に表を表示させて、一番下にはページネーションも設置します。
 以下の部分は、これまで何度か説明しておりますので詳細の説明を省きます。

{% extends 'base.html' %}
{% load static %}
{% block content %}
   <div class="content-wrapper">
       <div class="container-fluid">
           <div class="row">
           
           ・・・省略・・・
           
           </div> 
       </div>
   </div>
{% endblock %}

画面の左側部分ですが、card-headerのところにはユーザー名を表示することにしたいので、{{ user.get_username }}としてユーザー名を取得します。
 次にcard-bodyの部分ですが、ユーザーがプロフィール画像を持っている場合と持っていない場合をif文で条件分岐させます。先ほどのuserlist.htmlと同じ処理になります。{% if user.avatar %}でプロフィール画像を持っている場合の処理を
<img class="rounded img-fluid mx-auto d-block"
src="{{ user.avatar.url }}" id="avatar-image" alt="avatar_image">

としますが、サムネイルと同じ画像サイズだと小さすぎますのでCSSで画像サイズを指定するためにid="avatar-image"を設定します。
 {% else %}では、プロフィール画像が登録されていない場合の処理を記述します。<img class="rounded img-fluid mx-auto d-block" alt="プロフィール更新から画像が登録できます">
 </div>でcard-bodyを閉じて、その下にプロフィール更新ページへのリンクボタンを設置します。<a>タグでbootstrapのボタンを設置し、href属性でhref="{% url 'accounts:edit_profile' %}"としてurlを指定します。

<!--ページタイトル-->
<aside class="col-sm-4">
    <div class="card">
        <div class="card-header">
            <h3 class="card-title text-center">{{ user.get_username }}</h3>
        </div>
        <div class="card-body">
            { # ユーザーがプロフィール画像を持っている場合#}
            {% if user.avatar %}
                 <img class="rounded img-fluid mx-auto d-block"
                   src="{{ user.avatar.url }}" id="avatar-image" alt="avatar_image">
            {# ユーザーがプロフィール画像を持っている場合はデフォルト画像を表示#}
            {% else %}
                <img class="rounded img-fluid mx-auto d-block" alt="プロフィール更新から画像が登録できます">
            {% endif %}
            </div>
         <a class="btn btn-outline-secondary btn-sm"
        href="{% url 'accounts:edit_profile' %}"
        role="button">プロフィール更新</a>
    </div>
</aside>
mypost左

画面の右側部分も、bootstrapのnav-tabとbadge以外は見覚えがあるコードが多くあります。<ul class="nav nav-tabs nav-justified mb-3">はbootstrapのnav-tabになります。<li class="nav-item">〜</li>内に<a>タグでhref属性としてリンクを設定します。また、class="nav-link active"とするとタブがアクティブ状態であることを示す事が出来ます。
 bootstrapのbadgeを使用します。<span class="badge badge-secondary">〜</span>の〜の部分に投稿数を表示するようにします。先ほどのMyPostsViewでは、my_posts_count というcontext名でテンプレートへ値を渡しており、{{ my_posts_count }}とすると投稿数が表示できます。
 FollowingsFollowersのタブも同様に作成しておきます。ここで、これらのタブはアクティブではありませんので、class="nav-link"とします。

<div class="col-sm-8">
    <ul class="nav nav-tabs nav-justified mb-3">
        <li class="nav-item"><a href="{% url 'microposts:myposts' %}" class="nav-link active">MyPosts一覧
            {#  object_listのlengthを取得することで投稿数をカウントできる。#}
            span class="badge badge-secondary">{{ my_posts_count }}</span></a></li>
        
        {#  Followingsの表示#}
        <li class="nav-item"><a href="#" class="nav-link">Following            
            <span class="badge badge-secondary">"#"</span></a></li>
         {#  Followersを表示#}   
        <li class="nav-item"><a href="#" class="nav-link">Followers        
            <span class="badge badge-secondary">"#"</span></a></li>
            
    </ul>

  ・・・省略・・・

</div>

テーブルを作成していきます。table-idは後々、CSSで変更を加えられるようにmy_post_listと設定します(今回は変更を加えません)。表の列は、投稿内容更新/削除の2列としてそれぞれの幅を80%と20%で設定します。
 <th class="text-center" style="width: 80%">投稿内容</th>
 <th class="text-center" style="width: 20%">更新/削除</th>
 表のデータの部分を<tbody>〜</tbody>内にコードを書いていきます。MyPostsViewで、get_queryset()メソッドで使ってobject_listとしてデータをテンプレ渡していました。受け取るデータは、自分の全ての投稿になります。
 {% for item in object_list %}で、for文でobject_list内の自分の投稿をitemとして一つずつ取り出して表のレコードを作成していきます。最初の<td>タグで表の1列目には投稿時間と投稿内容を表示させます。itemの各フィールドを表示したい場合は、{{ item.フィールド名 }}とすれば値を表示できますので投稿日時は{{ item.created_at }}、投稿の内容は{{ item.content }}となります。
 次の<td>タグで表の2列目に更新ボタンと削除ボタンを設置します。更新と削除には、pkを指定する必要がありました。{{ item.pk }}とすれば投稿のidが取得できます。更新ボタンには、href属性で投稿の更新ページと投稿のidを指定しますのでhref="{% url 'microposts:update' item.pk %}"とします。
 削除ボタンも考え方は同じです。href属性で投稿の削除ページと投稿のidをhref="{% url 'microposts:delete' item.pk %}"と指定します。

    <!-- テーブル表の定義 -->
    <div class="table table-responsive">
        <table id='my_post_list'
            class="table table-striped table-bordered table-hover">
        <!-- 表の列の定義-->
            <thead>
            <tr>
                <th class="text-center" style="width: 80%">投稿内容</th>
                <th class="text-center" style="width: 20%">更新/削除</th>
            </tr>
            </thead>
        <!-- ここまでが表の列の定義-->
        <!-- 表のデータ部分の表示-->
            <tbody>
            {% for item in object_list %}
                <tr class="text">
                <td class="text"><h6>@{{ item.created_at }}</h6>{{ item.content }}</td>
                
                <td class="text-center align-middle">                
                    {#  pkを指定する際は、urlタグはやや特殊な書き方となる↓以下参照#}
                    <a class="btn btn-outline-success btn-sm"
                    href="{% url 'microposts:update' item.pk %}"
                     role="button">更新</a>
                     
                     <a class="btn btn-outline-danger btn-sm"
                     href="{% url 'microposts:delete' item.pk %}"
                    role="button">削除</a>
                </td>
                </tr>
            {% endfor %}
            </tbody>
        <!-- ここまでが表のデータ部分の表示-->
        </table>
    <!-- ここまでがテーブル表の定義 -->
mypost右

最後は、ページネーションの設定です。こちらもpostlist.htmlの時と全く同じですのでコピーして使用します。django-bootstrap4へのリンクです。

{#   django-bootstrap4のページネーション#}
{% bootstrap_pagination page_obj extra=request.GET.urlencode %}

 ターミナルからサーバーを立ち上げて下さい。ブラウザのアドレスバーに直接http://127.0.0.1:8000/microposts/mypostsを入力してみましょう。投稿内容が正しく表示されているでしょうか?プロフィール更新ボタン、投稿の更新ボタン、投稿の削除ボタンが、ページネーションがそれぞれ正しく作動するかを確認してください。Nav -tabの投稿数も反映されていますでしょうか?

スクリーンショット 2021-06-17 午後06.44.42 午前

それでは、基本の全てのページ作成が完了しましたのでbase.htmlのナビバーにリンクを設定していきます。base.htmlを開きナビバーのhref属性を以下のように設定してください。

{% if user.is_authenticated %}
    <li class="nav-item">
        <a class="nav-link" href="{% url 'microposts:postlist' %}">All Posts</a>
    </li>
    <li class="nav-item">
        <a class="nav-link" href="{% url 'accounts:userlist' %}">All Users</a>
    </li>
    <li class="nav-item">
        <a class="nav-link" href="{% url 'microposts:create' %}">New post</a>
    </li>
    <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" 
        data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
        {{ user.username }}'s detail </a>
        <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
            <a class="dropdown-item" href="{% url 'microposts:myposts' %}">My posts</a>
            <a class="dropdown-item" href="">Following</a>
            <a class="dropdown-item" href="#">Follower</a>
        </div>
    </li>
{% endif %}

以上でPost モデルとCRUD操作までが完了しました。一通りの機能を実装を通してデータベースからのデータ取得方法やテンプレートへの渡し方などが理解できたかと思います。

次からは、発展編としてユーザのFollow/Unfollow機能やポストのお気に入り機能追加を通じて多対多の関係について説明していきます。また、JavaScriptと各種プラグインを追加することによって写真のドラッグ&ドロップ機能及び拡大表示機能を追加します。Javascriptを使えるようになるとWebアプリの幅が広がります!


この記事が気に入ったらサポートをしてみませんか?