見出し画像

【ソースコード付き】Django SNS アプリの作り方

今回はTwitterのようなSNSアプリの作り方についてハンズオン形式で解説していきます。

Djangoマガジンでは今日までビュー、モデル、テンプレート等の基礎的な内容について解説してきたので、今回は今まで解説してきた知識を使ってアプリの作り方を解説していきます。

◆本記事で得られること
・Djangoアプリケーション開発の基礎的な機能(ログイン等の認証機能、CRUD機能etc)の実装方法を学ぶことができる。
ポートフォリオを作る上で参考知識を得ることができる。

◆対象者
・Python、Djangoの基礎文法がわかる(入門書を終えたレベル)人
・これからポートフォリオを作ってみたい人

1. 今回の目標物

前述の通りSNSアプリの作り方を解説していきます。

◆主な実装機能
認証機能(新規登録、ログイン機能 etc)
CRUD機能(生成、読み取り、更新、削除)
いいね/フォロー機能(リレーション(データベースの紐付け)の実装)

以下のようなイメージの機能を実装していきます。

認証機能

登録機能

CRUD機能

CRUD機能

いいね/フォロー機能

いいねフォロー機能

2. ソースコード

ソースコード

なお、本記事で解説しない箇所(ex templates/base.html etc...)がいくつかありますので必要に応じてソースコードをご確認ください。

3. ディレクトリの構成とファイルの設定

◆ディレクトリ完成形

snsproject                        #プロジェクトディレクトリ                     
├── config                        #設定ディレクトリ
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py              
│   └── wsgi.py 
├── snsapp                        #アプリディレクトリ
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations  
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── static                        #静的ファイルディレクトリ
│   └── style.css
├── templates                     #htmlファイル用ディレクトリ
│   ├── base.html
│   └── ...
└── manage.py

今回は上記のディレクトリ構成とします。ディレクトリやファイルの基本設定については以降で簡単に解説します。

◆config(設定ディレクトリ)の設定

まずはsnsprojectディレクトリを作成し、snsprojectディレクトリ直下で、以下のコマンドを入力しconfigディレクトリを作成します。

$ django-admin startproject config .

◆snsapp(アプリディレクトリ)の設定

まずはアプリディレクトリを作成し、djangoに認識させます。

以下のコマンドでアプリディレクトリを作成します。

$ python manage.py startapp snsapp

作成したアプリを以下の通りsettings.pyに追加し、djangoにアプリを認識させます。

config/settings.py アプリの追加

INSTALLED_APPS = [
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'django.contrib.sites',
   'snsapp.apps.SnsappConfig',           #追加
]

次にconfigとsnsappのurls.pyを紐付けします。

snsappディレクトリ直下にurls.pyファイルを作成し、以下のようにpathを追記することでconfig/urls.pyとsnsapp/urls.pyを繋ぎます。※snsapp/urls.pyの記述内容は以降で解説していきます。

config/urls.py snsapp/urls.pyとの紐付け

from django.contrib import admin
from django.urls import path, include    #include追加


urlpatterns = [
   path('admin/', admin.site.urls),
   path('', include('snsapp.urls')),      #追加
]

◆staticディレクトリの設定
まずはベースディレクトリ(プロジェクトディレクトリ)直下にstaticディレクトリを作成します。

次に以下の通り、settings.pyにstaticディレクトリの位置を記載しdjangoに知らせます。

config/settings.py staticディレクトリの位置を記載

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent #BASE_DIRの位置

・・・

STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']           #追加

templatesディレクトリの設定
まずはベースディレクトリ(プロジェクトディレクトリ)直下にtemplatesディレクトリを作成します。

次に以下の通り、settings.pyにtemplatesディレクトリの位置を記載しdjangoに知らせます。

config/settings.py templatesディレクトリの位置を記載

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

・・・

TEMPLATES = [
  {
      'BACKEND': 'django.template.backends.django.DjangoTemplates',
      'DIRS': [BASE_DIR / 'templates'],   #追加
      'APP_DIRS': True,
      'OPTIONS': {
          'context_processors': [
              'django.template.context_processors.debug',           
              'django.template.context_processors.request',         
              'django.contrib.auth.context_processors.auth',         
              'django.contrib.messages.context_processors.messages', 
          ],
      },
  },
]

ディレクトリ、ファイルの基本設定は以上になりますが、デプロイする場合はsettings.pyやurls.pyにSTATIC_ROOTに関する記述が必要なのでご注意ください。STATIC_ROOT含め今回解説したディレクトリ、ファイル設定内容については以下の記事でより詳細に解説しているので、必要に応じてご確認ください。

4. 実装機能の解説

4.1 認証機能

今回はdjango-allauthというライブラリを用いて新規登録、ログイン等の認証機能を実装していきます。

実装方法については以下にわかりやすくまとめられているのでご参照ください。

今回django-allauthに関連するコードは以下になります。

config/urls.py

from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static

urlpatterns = [
   path('admin/', admin.site.urls),
   path('accounts/', include('allauth.urls')),    #追加
   path('', include('snsapp.urls')), 
]

config/settings.py

INSTALLED_APPS = [
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'django.contrib.sites',
   'snsapp.apps.SnsappConfig',
   'allauth',                      #追加
   'allauth.account',              #追加
   'allauth.socialaccount',        #追加
]

...(以下末尾に追記)

AUTHENTICATION_BACKENDS = (
  'django.contrib.auth.backends.ModelBackend',
  'allauth.account.auth_backends.AuthenticationBackend',  
)

ACCOUNT_AUTHENTICATION_METHOD = 'username'
ACCOUNT_USERNAME_REQUIRED = True 

ACCOUNT_EMAIL_VERIFICATION = 'none'
ACCOUNT_EMAIL_REQUIRED = True 

SITE_ID = 1 

LOGIN_REDIRECT_URL = 'home'      #リダイレクト先をhomeページに設定。詳細後述          
ACCOUNT_LOGOUT_REDIRECT_URL = '/accounts/login/'

4.2 CRUD機能

CRUDとは、「Create(生成)」「Read(読み取り)」「Update(更新)」「Delete(削除)」の頭文字を並べた用語であり、WEBアプリでかなり頻出度の高い機能です。

Djangoには、CRUD機能を簡単に実装するための汎用ビューがあるので、そちらを使いながら実装していきます。

4.2.1 Read(読み取り)

説明の都合上、Read(読み取り)の部分から解説していきます。

今回以下の2つのRead(読み取り)機能について解説していきます。
①投稿記事の"一覧"ページ表示機能
②投稿記事の"詳細"ページ表示機能

早速①より解説していきます。

4.2.1 ①投稿記事の"一覧"ページ表示機能

「HOME(自分以外のユーザーの投稿内容)」「自分の投稿」の2種類の一覧ページを表示していきます。

◆Modelの設定

snsapp/models.py

rom django.db import models
from django.contrib.auth.models import User


class Post(models.Model):
   title = models.CharField(max_length=100)
   content = models.TextField()
   user = models.ForeignKey(User, on_delete=models.CASCADE)
   created_at = models.DateTimeField(auto_now_add=True)

   def __str__(self):
       return self.title

   class Meta:
       ordering = ["-created_at"]     #投稿順にクエリを取得

タイトル(title)、本文(content)、投稿者(user)、投稿日時(created_at)に関するフィールドを設定してテーブルを作成します。

投稿者(user)はForeignKeyを用いてユーザーモデルと紐付けします。

補足ですが、認証機能はallauthを使って実装していますが、Userモデルをインポートすれば問題なくユーザー情報を使用できます。

◆Urlの設定

snsapp/urls.py

from django.urls import path
from .views import Home, MyPost                       #Home, MyPost追加


urlpatterns = [
   path('', Home.as_view(), name='home'),             #追加
   path('mypost/', MyPost.as_view(), name='mypost'),  #追加
]

'Home'と'MyPost'というビューを以降で作成していきます。

◆Viewの設定

snsapp/views.py

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView

from .models import Post


class Home(LoginRequiredMixin, ListView):
   """HOMEページで、自分以外のユーザー投稿をリスト表示"""
   model = Post
   template_name = 'list.html'

   def get_queryset(self):
       #リクエストユーザーのみ除外
       return Post.objects.exclude(user=self.request.user)
      
   
class MyPost(LoginRequiredMixin, ListView):
   """自分の投稿のみ表示"""
   model = Post
   template_name = 'list.html'

   def get_queryset(self):
      #自分の投稿に限定
       return Post.objects.filter(user=self.request.user)

一覧の表示は汎用ビューであるListViewで簡単に実装できます。

ListViewの使い方として'model'と' template_name'を設定すれば簡単に使用できます。Homeクラス、MyPostクラスどちらも共通のmodelとtemplate_nameを使用します。

また今回どちらのクラスにもLoginRequiredMixinを継承させています。このクラスを継承することで、ログインしていなければ一覧ページを参照することができません。参照しようとするとログイン画面にリダイレクトします。

Homeクラス、MyPostクラスの違いである"自分以外の投稿""自分の投稿"については、取得するクエリセットに条件をつけることができるget_querysetメソッドで実装します。

自分以外の投稿の場合は...exclude(user=self.request.user)でリクエストユーザー(自分)を除外し、自分の投稿の場合は...filter(user=self.request.user)としリクエストユーザーの投稿のみを取得します。

◆Templateの設定

templates/list.html

{% extends 'base.html' %}
{% load static %}

...

{% block content %}
<div class="container mt-3">
   {% for item in object_list %}
   <div class="alert alert-success" role="alert">
       <p>タイトル:<a href="#">{{item.title}}</a></p>
       <p>投稿者:{{item.user.username}}</p>    
   </div>
   {% endfor %}
</div>
{% endblock content %}

ListViewのテンプレートの記載ではよくfor文を使用します。

ListViewのデフォルトのコンテキストは"object_list"であり、object_listの中にはクエリセット (複数のオブジェクト)情報が含まれていますので、for文で個別の情報を取り出してあげます。

◆一覧ページの確認

Home 自分以外の投稿

スクリーンショット 2021-05-18 12.04.36

MyPost 自分の投稿

スクリーンショット 2021-05-18 12.07.34

なおオブジェクトの作成については、「Create(生成)」の部分で後述しますが、管理画面でも作成可能ですので必要に応じて使ってみてください。

4.2.1 ②投稿記事の"詳細"ページ表示機能

◆Model
「4.2.1 ①投稿記事の"一覧"ページ表示機能」のモデルと同一です。

◆Urlの設定

snsapp/urls.py

from django.urls import path
from .views import Home, MyPost, DetailPost                  #DetailPost追加


urlpatterns = [
  path('', Home.as_view(), name='home'),
  path('mypost/', MyPost.as_view(), name='mypost'),  
  path('detail/<int:pk>', DetailPost.as_view(), name='detail'), #追加
]

詳細ページではオブジェクトを特定する必要があります。

特定の方法はurl内の「detail/pk or slug」のpk(id)またはslugとオブジェクト内のpk(id)またはslugが一致するオブジェクトを取得します。

今回は「detail/<int:pk>」としpk情報をもとにオブジェクトを取得します。

◆Viewの設定

snsapp/views.py

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import DetailView

from .models import Post


class DetailPost(LoginRequiredMixin, DetailView):
   """投稿詳細ページ"""
   model = Post
   template_name = 'detail.html'

詳細ページの表示は汎用ビューであるDetailViewで簡単に実装できます。

使い方はとてもシンプルでmodelとtemplate_nameを設定すればOKです。

url情報のpkやslugをもとにオブジェクトを特定すると上述しましたが、DetailViewはその処理を裏側で行ってくれるためとても短いコード量で実装できます。

◆Templateの設定

詳細ページ一覧ページから詳細ページへのリンクを設定していきます。

templates/detail.html 詳細ページ

{% extends 'base.html' %}
{% load static %}

...

{% block content %}
<div class="container">
   <div class="alert alert-success" role="alert">
       <p>タイトル:{{object.title}}</p>
       <p>投稿者:{{object.user}}</p>
       <p>コメント:{{object.content}}</p>
   </div>
</div>
{% endblock content %}

本文、投稿者、コメントを表示する詳細ページを作成していきます。

DetailViewではデフォルトのコンテクストとして"object"が使えます。

templates/list.html 詳細ページへのリンク

{% extends 'base.html' %}
{% load static %}

...

{% block content %}
<div class="container mt-3">
  {% for item in object_list %}
  <div class="alert alert-success" role="alert">
      #詳細ページへのリンク設定
      <p>タイトル:<a href="{% url 'detail' item.pk %}">{{item.title}}</a></p>
      <p>投稿者:{{item.user.username}}</p>  
  </div>
  {% endfor %}
</div>
{% endblock content %}

◆詳細ページの確認

詳細機能

4.2.2 Create(生成) 記事の投稿機能

ユーザーが記事を投稿できる機能について解説していきます。

◆Model
「4.2.1 ①投稿記事の"一覧"ページ表示機能」のモデルと同一です。

◆Urlの設定

snsapp/urls.py

from django.urls import path
from .views import Home, MyPost, DetailPost, CreatePost      #CreatePost追加

urlpatterns = [
  path('', Home.as_view(), name='home'),
  path('mypost/', MyPost.as_view(), name='mypost'),  
  path('detail/<int:pk>', DetailPost.as_view(), name='detail'),
  path('create/', CreatePost.as_view(), name='create'),      #追加     
]

◆Viewの設定

snsapp/views.py

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreatelView
from django.urls import reverse_lazy

from .models import Post


class CreatePost(LoginRequiredMixin, CreateView):
   """投稿フォーム"""
   model = Post
   template_name = 'create.html'
   fields = ['title', 'content']
   success_url = reverse_lazy('mypost')

   def form_valid(self, form):
       """投稿ユーザーをリクエストユーザーと紐付け"""
       form.instance.user = self.request.user
       return super().form_valid(form)

記事の投稿は汎用ビューであるCreateViewで簡単に実装できます。

使い方はmodelとtemplate_nameの設定に加えて、「投稿フォームの入力項目を決めるfields」「投稿後の遷移先を決めるsuccess_url(またはget_success_urlメソッド)」を設定する必要があります。

今回は解説しませんが、modelやfileldsを設定する代わりにアプリディレクトリ直下にforms.pyを作成してform_classをビュー内に設定してもOKです。

基本的には以上の設定で完了なのですが、このままでフォーム入力の無い投稿者(user)をデータベースに追加することができません。

form_validメソッドを使って、ユーザーが記事投稿した瞬間にデータベースに投稿者情報を自動追加できるようにします。

form.instance.(フィールド名)でデータベースのテーブル情報にアクセスできるので「form.instance.user = self.request.user」とします。

◆Templateの設定

templates/create.html

{% block content %}
<form method="post">{% csrf_token %}
   {{ form.as_p }}
   <input type="submit" value="投稿">
</form>

{% endblock content %}

テンプレートにformを設定します。入力項目については{{ form.as_p }}とすることで先ほどビューのfieldsで設定した項目(title, content)をpタグで囲って表示することができます。

◆投稿機能の確認

投稿機能

4.2.3 Update(更新) 記事の編集機能

ユーザーが記事を編集できる機能について解説していきます。

◆Model
「4.2.1 ①投稿記事の"一覧"ページ表示機能」のモデルと同一です。

◆Urlの設定

snsapp/urls.py

from django.urls import path
from .views import ... , UpdatePost       #UpdatePost追加

urlpatterns = [
  path('', Home.as_view(), name='home'),
  path('mypost/', MyPost.as_view(), name='mypost'),  
  path('detail/<int:pk>', DetailPost.as_view(), name='detail'),
  path('detail/<int:pk>/update', UpdatePost.as_view(), name='update'), #追加
  path('create/', CreatePost.as_view(), name='create'),   
]

どのページを編集するか指定する必要があるため、詳細ページ同様pkを設定します。詳細ページのurl末尾に/updateを付け加えるとわかりやすいurlになります。

◆Viewの設定

snsapp/views.py

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import UpdatelView
from django.urls import reverse_lazy

from .models import Post


class UpdatePost(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
   """投稿編集ページ"""
   model = Post
   template_name = 'update.html'
   fields = ['title', 'content']

   def get_success_url(self,  **kwargs):
       """編集完了後の遷移先"""
       pk = self.kwargs["pk"]
       return reverse_lazy('detail', kwargs={"pk": pk})
   
   def test_func(self, **kwargs):
       """アクセスできるユーザーを制限"""
       pk = self.kwargs["pk"]
       post = Post.objects.get(pk=pk)
       return (post.user == self.request.user) 

記事の編集機能は汎用ビューであるUpdateViewで簡単に実装できます。

使い方はCreateViewとほぼ同じでmodel、template_name、fields(編集項目)、編集後の遷移先を設定する必要があります。

編集後の遷移先はget_success_urlメソッド内で記述します。先ほどのCreateViewで説明したクラス変数success_urlと違う点"pk"等の変数を扱える点です。

今回編集後の遷移先は詳細ページ(detail)に設定しているためpkが必要となります。したがってクラス変数success_urlではなくpk使用可能なget_success_urlメソッドを使用します。

また今回クラスの引数に"UserPassesTestMixin"を使用しています。

これはアクセスできるユーザーを制限することができるミックスインであり、具体的にどのように制限するかはtest_funcメソッド内で記述します。

今回投稿者とリクエストユーザーが同じ場合のみ編集できるように設定しています。returnでの戻り値がTrueの場合のみアクセス可能であり、Falseの場合は403エラー(閲覧禁止)となります。

したがって以下の前提でリンクを踏んだ場合、リクエストユーザーと投稿者が異なるため画面上に「403 Forbidden」と表示されます。

リクエストユーザー:shogosaito
投稿者:saito

アクセス制限

◆Templateの設定

templates/create.html 編集フォーム

「4.2.1 ①投稿記事の"一覧"ページ表示機能」のテンプレートと同一です。

templates/detail.html 編集フォームへのリンク

{% block content %}
<div class="container">
   <div class="alert alert-success" role="alert">
       <p>タイトル:{{object.title}}</p>
       <p>投稿者:{{object.user}}</p>
       <p>コメント:{{object.content}}</p>
        
       #以下追加
       {% if object.user == request.user %} #リクエストユーザーと投稿者が等しい場合のみリンク表示
       <a href="{% url 'update' pk %}" class="btn btn-primary ms-3" tabindex="-1" role="button" aria-disabled="true">編集</a>
       {% endif %}
   </div>
</div>
{% endblock content %}

{% if object.user == request.user %}以下を追加し、編集フォームへのリンクを作成しています。if文を使っている理由はリクエストユーザーと投稿者が等しい場合のみ編集リンクを表示するためです。

◆編集機能の確認

編集機能

4.2.4 Delete(削除) 記事の削除機能

ユーザーが記事を削除できる機能について解説していきます。

◆Model
「4.2.1 ①投稿記事の"一覧"ページ表示機能」のモデルと同一です。

◆Urlの設定

snsapp/urls.py

from django.urls import path
from .views import ... , DeletePost       #DeletePost追加

urlpatterns = [
 path('', Home.as_view(), name='home'),
 path('mypost/', MyPost.as_view(), name='mypost'),  
 path('detail/<int:pk>', DetailPost.as_view(), name='detail'),
 path('detail/<int:pk>/update', UpdatePost.as_view(), name='update'),
 path('detail/<int:pk>/delete', DeletePost.as_view(), name='delete'), #追加
 path('create/', CreatePost.as_view(), name='create'),   
]

どのページを編集するか指定する必要があるため、詳細ページ同様pkを設定します。詳細ページのurl末尾に/deleteを付け加えるとわかりやすいurlになります。

◆Viewの設定

snsapp/views.py

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import DeleteView
from django.urls import reverse_lazy

from .models import Post


class DeletePost(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
   """投稿編集ページ"""
   model = Post
   template_name = 'delete.html'
   success_url = reverse_lazy('mypost')

   def test_func(self, **kwargs):
       """アクセスできるユーザーを制限"""
       pk = self.kwargs["pk"]
       post = Post.objects.get(pk=pk)
       return (post.user == self.request.user) 

削除機能は汎用ビューであるDeleteViewで簡単に実装できます。

使い方は使い方はCreateViewとほぼ同じでmodel、template_name、success_url(削除後の遷移先)を設定する必要があります。

「記事の編集機能」で解説した内容と同様にUserPassesTestMixinを使いアクセスできるユーザーを制限します。

◆Templateの設定

templates/delete.html 削除ページ

{% block content %}
<form method="post">{% csrf_token %}
   <p>本当に削除してもよろしいですか?</p>
   <input type="submit" value="削除する">
</form>
{% endblock content %}

templates/detail.html 削除ページへのリンク

{% block content %}
<div class="container">
  <div class="alert alert-success" role="alert">
      <p>タイトル:{{object.title}}</p>
      <p>投稿者:{{object.user}}</p>
      <p>コメント:{{object.content}}</p>
       
      {% if object.user == request.user %} 
      <a href="{% url 'update' pk %}" class="btn btn-primary ms-3" tabindex="-1" role="button" aria-disabled="true">編集</a>
      #以下追加
      <a href="{% url 'delete' pk %}" class="btn btn-danger ms-3" tabindex="-1" role="button" aria-disabled="true">削除</a>
      {% endif %}
  </div>
</div>
{% endblock content %}

◆削除機能の確認

削除機能

4.3 いいね/フォロー機能

SNSで頻出の機能、いいね/フォロー機能について解説していきます。

いいね/フォロー機能ではManyToManyFieldを用いて実装していきます。

いいね/フォロー機能を実装してみたい方、ManyToManyField等のモデルのリレーションについて学びたい方はぜひ最後まで読んでください。

4.3.1 いいね機能

◆Model

モデルの設計が肝と思いますので、詳細に解説していきます。

まずTwitterのいいね機能をイメージしてみてください。「あるツイートに対して」「複数人が」いいねをすることができます。

したがって今回の実施ではある投稿記事に対して複数人がいいねできるように、Postモデル(記事)ManyToManyField(複数人のユーザー)を追加していきます。

snsapp/models.py

from django.db import models
from django.contrib.auth.models import User

class Post(models.Model):
   title = models.CharField(max_length=100)
   content = models.TextField()
   user = models.ForeignKey(User, on_delete=models.CASCADE)
   
   #like追加
   like = models.ManyToManyField(User, related_name='related_post', blank=True)
   created_at = models.DateTimeField(auto_now_add=True)

   def __str__(self):
       return self.title

   class Meta:
       ordering = ["-created_at"]

上記コードのようにlikeフィールドを追加し、ManyToManyFieldでユーザーと紐付けします。

フィールドオプションで疑問に思った方もいるかもしれません。通常「blank=True, null=Ture」をセットで使用しますが、今回はblank=Trueのみです。

少し難しい話になりますが、ManyToManyFieldは以下の通り中間テーブルを自動作成することで多対多を実装しています。したがって通常の数値、文字列等が入ったカラムではなくテーブルと紐づいているためカラムをnull(空)にすることができません。したがってblank=Trueのみのオプション設定になっています。

無題のプレゼンテーション (3)

管理画面での確認

スクリーンショット 2021-05-18 12.39.11

いいねするとManyToManyFieldのテーブル内にユーザーが追加され、いいねを解除するとユーザーがテーブルから削除されるようなイメージで実装していきます。

◆Urlの設定

snsapp/urls.py

from django.urls import path
from .views import ... , LikeHome, LikeDetail       #LikeHome、LikeDetail追加

urlpatterns = [
   path('like-home/<int:pk>', LikeHome.as_view(), name='like-home'),
   path('like-detail/<int:pk>', LikeDetail.as_view(), name='like-detail'),  
]

like-home...とlike-detail...の二種類のurlを準備します。

二種類準備する理由としては、一覧ページからいいねした場合詳細ページからいいねした場合いいね後のリダイレクト先を変えるためです。

またurl上のpkはいいねする記事を特定するために設定します。

理想は...

いいねした時にリダイレクトさせずに同じページにとどまることだと思います。これを実装するにはjavascript等を使って非同期化させる必要があります。今回はjavascriptを用いた非同期化は実装しませんが余力のある方はぜひチャレンジしてみてください。

◆Viewの設定

snsapp/views.py

from django.shortcuts import redirect
from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User

from .models import Post


class LikeBase(LoginRequiredMixin, View):
   """いいねのベース。リダイレクト先を以降で継承先で設定"""
   def get(self, request, *args, **kwargs):
       #記事の特定
       pk = self.kwargs['pk']
       related_post = Post.objects.get(pk=pk)         
       
       #いいねテーブル内にすでにユーザーが存在する場合   
       if self.request.user in related_post.like.all(): 
           #テーブルからユーザーを削除 
           obj = related_post.like.remove(self.request.user) 
       #いいねテーブル内にすでにユーザーが存在しない場合
       else:
           #テーブルにユーザーを追加                           
           obj = related_post.like.add(self.request.user)  
       return obj


class LikeHome(LikeBase):
   """HOMEページでいいねした場合"""
   def get(self, request, *args, **kwargs):
       #LikeBaseでリターンしたobj情報を継承
       super().get(request, *args, **kwargs)
       #homeにリダイレクト
       return redirect('home')


class LikeDetail(LikeBase):
   """詳細ページでいいねした場合"""
   def get(self, request, *args, **kwargs):
       #LikeBaseでリターンしたobj情報を継承
       super().get(request, *args, **kwargs)
       pk = self.kwargs['pk'] 
       #detailにリダイレクト
       return redirect('detail', pk)

Viewの方針としては、LikeBaseでいいねした際にデータベースとやりとりするベースの処理を記載し、LikeHomeとLikeDetailでいいねした場所に応じてリダイレクト先を変更します。

まずはLikeBaseから解説していきます。

Likeボタンがクリックされた際に、まずpkを用いて記事を特定します。次に特定した記事のlikeテーブル内クリックしたユーザーがすでに存在する場合は、テーブル内からユーザーを削除しいいね解除します。逆にlikeテーブル内にクリックしたユーザーが存在しない場合は、テーブル内にそのユーザーを追加しいいねします。

LikeHomeとLikeDetailではLikeBaseで処理したオブジェクトを継承しいいねしたページに応じてリダイレクト先を設定します。

◆Templateの設定

templates/list.html

{% block content %}
<div class="container mt-3">
   {% for item in object_list %}
   <div class="alert alert-success" role="alert">
    ...
       #追加
       {% if request.user in item.like.all %}
       <a href="{% url 'like-home' item.pk %}" class="like-btn add-color" tabindex="-1" role="button" aria-disabled="true"><i class="fas fa-heart"></i></a>{{item.like.count}}
       {% else %}
       <a href="{% url 'like-home' item.pk %}" class="like-btn" tabindex="-1" role="button" aria-disabled="true"><i class="far fa-heart"></i></a>{{item.like.count}}
       {% endif %}
    ...
   </div>
   {% endfor %}
</div>
{% endblock content %}

templates/detail.html

{% block content %}
<div class="container">
   <div class="alert alert-success" role="alert">
   ...

       #追加
       {% if request.user in object.like.all %}
       <a href="{% url 'like-detail' object.pk %}" class="like-btn add-color" tabindex="-1" role="button" aria-disabled="true"><i class="fas fa-heart"></i></a>{{object.like.count}}
       {% else %}
       <a href="{% url 'like-detail' object.pk %}" class="like-btn" tabindex="-1" role="button" aria-disabled="true"><i class="far fa-heart"></i></a>{{object.like.count}}
       {% endif %}

    ・・・
   </div>
</div>
{% endblock content %}

if文を使っているのはテーブル内にリクエストユーザーが存在する場合とそうではない場合、つまりいいね済みの場合とされていない場合で表示内容を変更するためです。

{{ ....like.count }}とすることでいいね数をカウントし表示させています。

◆いいね機能の確認

いいね機能

4.3.2 フォロー機能

◆Model

いいね機能同様、フォローボタンをクリックするとユーザーがManyToManyFieldで作成されたテーブルの中に追加される仕様でフォロー機能を実装していきます。

いいねは投稿記事に紐づいていましたが、フォロー機能は特定のユーザーと紐付ける必要があります。

新たにConnectionというモデルを設定して実装していきます。

snsapp/models.py

from django.db import models
from django.contrib.auth.models import User

class Connection(models.Model):
   user = models.OneToOneField(User, on_delete=models.CASCADE)
   following = models.ManyToManyField(User, related_name='following', blank=True)

   def __str__(self):
       return self.user.username

Connectionモデルの全体としては、特定のユーザーをuserとし、特定のユーザーがフォローしているユーザー情報をfollowingとします。

userは一対一であるOneToOneFieldで設定し、followingをManyToManyFieldで設定します。

◆Urlの設定

snsapp/urls.py

from django.urls import path
from .views import ... , FollowHome, FollowDetail, FollowList


urlpatterns = [
   path('follow-home/<int:pk>', FollowHome.as_view(), name='follow-home'),
   path('follow-detail/<int:pk>', FollowDetail.as_view(), name='follow-detail'),
   path('follow-list/', FollowList.as_view(), name='follow-list'),
]

いいね同様、フォローするページに応じてリダイレクト先を変えるため'follow-home/...'と'follow-detail/...'を設定します。

'follow-list/'はフォローしたユーザーの投稿一覧を表示するためのurlです。

◆Viewの設定

「フォロー機能」「フォローしたユーザーの投稿一覧表示機能」に分けて解説していきます。

snsapp/views.py フォロー機能

from django.shortcuts import redirect
from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User

from .models import Post, Connection


class FollowBase(LoginRequiredMixin, View):
   """フォローのベース。リダイレクト先を以降で継承先で設定"""
   def get(self, request, *args, **kwargs):
       #ユーザーの特定
       pk = self.kwargs['pk']
       target_user = Post.objects.get(pk=pk).user
       
       #ユーザー情報よりコネクション情報を取得。存在しなければ作成
       my_connection = Connection.objects.get_or_create(user=self.request.user)
       
      #フォローテーブル内にすでにユーザーが存在する場合
       if target_user in my_connection[0].following.all():
           #テーブルからユーザーを削除
           obj = my_connection[0].following.remove(target_user)
       #フォローテーブル内にすでにユーザーが存在しない場合
       else:
           #テーブルにユーザーを追加
           obj = my_connection[0].following.add(target_user)
       return obj

class FollowHome(FollowBase):
   """HOMEページでフォローした場合"""
   def get(self, request, *args, **kwargs):
       #FollowBaseでリターンしたobj情報を継承
       super().get(request, *args, **kwargs)
       #homeにリダイレクト
       return redirect('home')

class FollowDetail(FollowBase):
   """詳細ページでフォローした場合"""
   def get(self, request, *args, **kwargs):
       #FollowBaseでリターンしたobj情報を継承
       super().get(request, *args, **kwargs)
       pk = self.kwargs['pk'] 
       #detailにリダイレクト
       return redirect('detail', pk)

フォロー機能のビュー記載内容は上述のいいね機能とほぼ同じため解説MINで進めていきます。

1点異なる点はフォロー機能の場合getメソッドではなく、get_or_createメソッドを使ってことです。

get_or_createメソッドは、「オブジェクトを取得、なければオブジェクトを作成」するメソッドです。なぜ今回get_or_createメソッドを使うかというと、Connectionモデルでは投稿機能のようにオブジェクトを事前に生成する機能を有していないので、getメソッドを使うとオブジェクトの欠損でエラーになる可能性があります。

ただしget_or_createメソッドを使う上で注意点があります。

>>> book = Book.objects.create(title="Ulysses")
>>> book.chapters.get_or_create(title="Telemachus")
(<Chapter: Telemachus>, True)   #オブジェクトを生成(Trueはcreate)
>>> book.chapters.get_or_create(title="Telemachus")
(<Chapter: Telemachus>, False) #オブジェクトを取得(Falseはget)
>>> book.chapters.get(title="Telemachus")
<Chapter: Telemachus>  #オブジェクトを取得

get_or_createメソッドの戻り値はタプル型であり、インデックス0番目にオブジェクトインデックス1番目にTrue/Falseが格納されています。

したがってオブジェクトのみを取得する場合はインデックス番号を指定する必要あるので、ビューにmy_connection[0]...と記載しています。

snsapp/views.py フォローしたユーザーの投稿一覧表示機能

from django.shortcuts import redirect
from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User

from .models import Post, Connection


class FollowList(LoginRequiredMixin, ListView):
  """フォローしたユーザーの投稿をリスト表示"""
  model = Post
  template_name = 'list.html'

  def get_queryset(self):
      """フォローリスト内にユーザーが含まれている場合のみクエリセット返す"""
      my_connection = Connection.objects.get_or_create(user=self.request.user)
      all_follow = my_connection[0].following.all()
      #投稿ユーザーがフォローしているユーザーに含まれている場合オブジェクトを返す。
      return Post.objects.filter(user__in=all_follow)

  def get_context_data(self, *args, **kwargs):
      """コネクションに関するオブジェクト情報をコンテクストに追加"""
      context = super().get_context_data(*args, **kwargs)
      #コンテクストに追加
      context['connection'] = Connection.objects.get_or_create(user=self.request.user)
      return context

get_querysetメソッドフォローリスト内にユーザーが含まれている場合のみクエリセット返すために使います。手順としてはまず自分のフォローしているユーザー情報"all_follow"を取得します。次にfilterメソッドを使い、投稿ユーザーがフォローしているユーザーに含まれている条件(user__in=all_follow)でオブジェクトを限定します。

get_context_dataメソッドテンプレートでフォローボタンを表示するために使用します。モデルには"Post"と設定しているためConnectionモデルに関する情報をコンテクストに渡す場合はget_context_dataメソッドで設定する必要があります。

またフォローボタンを全ての投稿関連ページで表示するためHome、MyPost、DetailPostにも同様にget_context_dataメソッドの内容を追加してください。詳細はソースコードのsnsapp/views.pyをご確認ください。

◆Templateの設定

templates/list.html

{% block content %}
<div class="container mt-3">
   {% for item in object_list %}
   <div class="alert alert-success" role="alert">
    ...
       #追加
       {% if item.user in connection.0.following.all %}
       <a href="{% url 'follow-home' item.id %}" class="btn btn-danger ms-3" tabindex="-1" role="button" aria-disabled="true">フォロー解除</a>
       {% else %}
       <a href="{% url 'follow-home' item.id %}" class="btn btn-primary ms-3" tabindex="-1" role="button" aria-disabled="true">フォロー</a>
       {% endif %}
    ...
   </div>
   {% endfor %}
</div>
{% endblock content %}

templates/detail.html

{% block content %}
<div class="container">
   <div class="alert alert-success" role="alert">
   ...
       #追加
       {% if object.user in connection.0.following.all %}
       <a href="{% url 'follow-detail' object.pk %}" class="btn btn-danger ms-3" tabindex="-1" role="button" aria-disabled="true">フォロー解除</a>
       {% else %}
       <a href="{% url 'follow-detail' object.pk %}" class="btn btn-primary ms-3" tabindex="-1" role="button" aria-disabled="true">フォロー</a>
       {% endif %}
    ・・・
   </div>
</div>
{% endblock content %}

if文を使っているのはテーブル内にリクエストユーザーが存在する場合とそうではない場合、つまりフォロー済みの場合とされていない場合で表示内容を変更するためです。

またconnection.0...の0の部分はインデックス番号であり、ビューにてget_or_createメソッドを使用しているのでインデックス番号を指定する必要があります。

viewではconnection[0]のように記載しますが、templateではconnection.0と記載するので覚えておきましょう。「5. 関連記事」にテンプレートの参考記事を紹介しているのでテンプレートについてもっと勉強したい方はご確認ください。

◆フォロー機能の確認

フォロー機能

5. 関連記事

今回のアプリで使用した技術の参考記事を以下にまとめます。

↓Django ポートフォリを作る上で基本的な考え方が学べます。

↓ディレクトリ/ファイル設定を学ぶことができます。

↓django-allauthの実装方法を学ぶことができます。

↓viewの基礎知識〜LoginRequiredMixin、UserPassesTestMixin等のミックスインまでview全般幅広く学ぶことがきます。

↓モデルの基本フィールド、モデル間のリレーション(ManyToManyField)について学ぶことができます。

↓データベースからオブジェクトを取得する方法を学ぶことができます。

↓テンプレートの基本的な使い方を学ぶことができます。

6. 最後に

いかがだったでしょうか。

Djangoを学ぶ上で何かアプリを作ってみることがとても重要です。

本記事で紹介したアプリを真似てみるもよし、一部変更してオリジナルのものを作ってみてもよし、みなさまの学習の手助けになればと思います!

もう少し難易度をあげたアプリの実装方法についても解説しているので、「より作り込んだポートフォリオ」を作りたいという方はぜひ以下記事をご確認ください。



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