DjangoでSNSアプリを作る – その2

テンプレートの配置

テンプレートが無いと実感がわかないと思うので「Argon Dashboard」を少し改造したものを用意しました。

ダウンロードして使って下さい。(ZIPでダウンロードする

create-social-media-mainというZIPがダウンロードされるので解凍します。

「part2」の中にあるbase.htmlとindex.htmlをtemplates/sns内に入れます。(前回作ったindexファイルは削除)

さらにmanage.pyがあるプロジェクトディレクトリ「sampleapp」の中に、ダウンロードしたstaticフォルダを入れて下さい。

仮想サーバーを起動しhttp://127.0.0.1:8000/にアクセスしてみましょう。

$ python manage.py runserver

Twitter風の画面が表示されたと思うのでこれで準備完了です。

base.htmlは全ページの元(共通部分)となり、{% block contents %}{% endblock %}の部分にindex.htmlの内容が入りページを形成します。

index.htmlの一行目にある{% extends “sns/base.html” %}はbase.htmlを元に拡張することを意味します。


ツイート機能

  • Tweetモデルの作成
  • ツイートフォームの作成
  • ツイートを保存するコードの追加
  • index.htmlの編集

Tweetモデルの作成

models.pyでTweetのモデルを作成します。

class Tweet(models.Model):
    myuser = models.ForeignKey(MyUser, on_delete=models.CASCADE, verbose_name='投稿者')
    text = models.TextField(verbose_name='内容', max_length=140)
    tweet_image = models.ImageField(verbose_name='ツイート画像', upload_to="twimage", blank=True, null=True)
    created_at = models.DateTimeField('作成日', default=timezone.now)

    def __str__(self):
        return f"{self.text}"
$ python manage.py makemigrations
$ python manage.py migrate

ツイートフォームの作成

Djangoでは、コードをスマートにするために入力フォームをforms.pyで管理します。

snsディレクトリにforms.pyを作成し次のように編集してください。

from django import forms
# Tweetモデルを読み込む
from .models import Tweet

#Tweetフォームを定義
class TweetForm(forms.ModelForm):
    class Meta:
        # 保存先のモデルを指定
        model  = Tweet
        # テンプレートに表示するフィールドを指定
        fields = ('text', 'tweet_image')
        # ツイートの入力欄を二行しておく
        widgets = {
            'text': forms.Textarea(attrs={'rows':2}),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 見た目を良くするためにform-controlをクラスに指定
        self.fields['text'].widget.attrs['class'] = 'form-control'
        # javascriptで動きを出したいのでidを指定
        self.fields['tweet_image'].widget.attrs['id'] = 'uploadimage'

TweetFormはviews.pyで呼び出して使います。

ツイートを保存する処理

views.pyを次のように編集して下さい。

from django.shortcuts import render, redirect
# TweetFormを呼び出す
from .forms import TweetForm

def index(request):
    # 今ログインしている本人をuserに代入
    user = request.user
    
    # 通常は入力フォームを表示するだけ
    form = TweetForm()
    # method postが実行されたら
    if request.method == 'POST':
        # TweetFormにpostされた内容を入れる
        form = TweetForm(request.POST, request.FILES)
        # 成功したら
        if form.is_valid():
            # 保存する前に内容を取得
            tweet = form.save(commit=False)
            # tweetのmyuserのフィールドにrequest.userを入れる
            tweet.myuser = request.user
            # データベースにツイートが保存され、画像はmediaに保存される。
            tweet.save()
            # 保存できたらindexページにリダイレクト
            return redirect("sns:index")

    # contextにuserとformを追加し、テンプレートで使えるようにする。
    context = {
        'user': user, 'form':form,
    }
    return render(request, 'sns/index.html', context)

注意が必要なのはrequest.FILESで、ファイルや画像がある場合はこれが書かれていないと保存されません。

また、テンプレート側でuserを選択させたくないので、view側でform.save(commit=False)してからrequest.userを保存する必要があります。

index.htmlの編集

ツイートの入力部分(10行目辺りから)を次のように変えて下さい。

<!-- ツイートする -->
<div class="card mb-4">
    <div class="card-body">
        <div class="d-flex align-items-top">
            <div class="me-3">
                <!-- userの画像のurlを取得 -->
                <img class="avatar avatar-md" src="{{ user.image.url }}" alt="">
            </div>
            <div class="w-100">
                <!-- ツイートボタンが押されたらindex関数のPOSTmethodを実行 -->
                <form action="{% url 'sns:index' %}" method="POST" enctype="multipart/form-data">
                    <!-- form内に必須 -->
                    {% csrf_token %}
                    <!-- TweetFormのtextフィールドを表示 -->
                    {{ form.text }}
                    <!-- ① 画像が選択されたらプレビューを表示する場所 -->
                    <img id="img-preview" class="img-fluid rounded mt-3">
                    <div class="d-flex align-items-center justify-content-between mt-3">
                        <div>
                            <!-- ②「ファイルを選択する」をボタンにする -->
                            <span class="btn btn-link p-0 m-0"><i id="uplaoricon" class="fa-regular fa-image fa-xl"></i></span>
                            <!-- ③「ファイルを選択する」を非表示にするためforms.pyでidを設定 -->
                            {{ form.tweet_image }}
                            <!-- ①②③に関するjavascript -->
                            <script>
                                document.getElementById("img-preview").style.display ="none";
                                document.querySelector("#uplaoricon").addEventListener("click", () => {
                                    document.querySelector("#uploadimage").click();
                                });
                                document.getElementById('uploadimage').addEventListener('change', function (e) {
                                    var file = e.target.files[0];
                                    var blobUrl = window.URL.createObjectURL(file);
                                    var img = document.getElementById('img-preview');
                                    img.src = blobUrl;
                                    const imgp = document.getElementById("img-preview");
                                    imgp.style.display ="block";
                                });
                            </script>
                        </div>
                        <div>
                            <button class="btn btn-sm btn-primary shadow-none m-0" type="submit">ツイート</button>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>

javascriptについては詳しく説明しませんが、プレビュー機能、imgの非表示、iconクリックでファイル選択のコードが書いてあります。

試しにいくつかツイートし、adminページで保存されているか確認してみて下さい。


ツイート一覧を表示

  • admin.pyにTweetモデル追加
  • index viewでツイート一覧を取得
  • django-debug-toolbarの追加
  • index.htmlの編集
  • lazyload機能の追加

まだユーザーが自分しか居ないので、test1ユーザーとtest2ユーザーを作成します。

自分のアカウントのfollowの項目で、作ったtest1、test2ユーザーを追加しておいて下さい。

admin.pyにTweetモデル追加

adminページにTweetのモデルが表示されていないのでadmin.pyで登録しましょう。

# Tweetを追加
from .models import MyUser, Tweet

# Tweetを登録
admin.site.register(Tweet)

adminページでtest1とtest2のツイートをそれぞれ作成し保存します。

ツイート一覧を取得

views.pyのindex関数を次のように修正して下さい。

from django.shortcuts import render, redirect
from django.db.models import Q

from .models import MyUser, Tweet
from .forms import TweetForm

def index(request):
    user = request.user
    # 自分がフォローしているユーザーを取得
    follow_list = user.follow.all()
    # 自分とフォローしている人のツイートを取得
    tweet_list = Tweet.objects.filter(Q(myuser__in=follow_list)|Q(myuser=user)).select_related('myuser').order_by('-created_at')

    form = TweetForm()
    if request.method == 'POST':
        form = TweetForm(request.POST, request.FILES)
        if form.is_valid():
            tweet = form.save(commit=False)
            tweet.myuser = user
            tweet.save()
            return redirect("sns:index")

    context = {
        'user': user, 'form':form, 
        'follow_list': follow_list, 'tweet_list': tweet_list, # 追加
    }
    return render(request, 'sns/index.html', context)

Qオブジェクトで、followしているユーザーと自分のツイートを取得しています。Qは「or」検索する時に必要なので形を覚えておきましょう。

さらに、select_related(‘myuser’)で、ツイートに紐付いているmyuserのオブジェクトも同時に取得しorder_byで新しいツイート順にしています。

リレーションを張っているフィールドは、for文などで出力すると余分なSQL(重複したSQL)が発行されてしまう場合があります。

今回の場合、ツイートに関連するユーザー情報を取得しているので、for文で回すたびにSQLを発行(ユーザー情報を取得)する形となっていて処理が遅くなってしまいます。

これを回避する為のメソッドがselect_relatedで、myuserフィールドを最初に取得しておき処理軽くしています。他にも「prefetch_related」というのもあるので覚えておきましょう!

どれくらいSQLが発行されているか分かりにくいと思うのでdjango-debug-toolbarでSQL数を可視化します。

django-debug-toolbarはDjangoで良く使われるパッケージです。

django-debug-toolbarの追加

django-debug-toolbarをインストールします(仮想サーバーが起動している場合はcontrol + Cで終了)

$ pip install django-debug-toolbar

settings.pyの下の方に設定を記入

# django-debug-toolbar
if DEBUG:
    INTERNAL_IPS = ['127.0.0.1']
    INSTALLED_APPS += ['debug_toolbar',]
    MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware',)

sampleappのurls.pyを修正

# もしDEBUGがTrueなら
if settings.DEBUG:
    import debug_toolbar #追加
    urlpatterns += [path('__debug__/', include(debug_toolbar.urls))] # 追加
    from django.conf.urls.static import static
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

仮想サーバーを起動させhttp://127.0.0.1:8000/にアクセスすると、右上に「DjDT」と表示されているのでクリック、さらに「SQL」の項目をクリックするとSQLの発行回数が確認できると思います。

viewを編集し、select_relatedの有無で数値がどれくらい変わるかチェックしてみると実感が湧くはずです が、ツイート数が少ないと差は小さくなります。

index.htmlの編集

index.htmlのツイート一覧を以下のように修正してください。

<!-- ツイート一覧 -->
<div class="card mb-4">
    <!-- for文で一つずつ表示させる。 -->
    {% for tweet in tweet_list %}
    <div class="card-header pb-0">
        <div class="d-flex align-items-center justify-content-between">
            <div class="d-flex align-items-center">
                <!-- プロフアイコン -->
                <div class="me-3">
                    <!-- classにlazyloadを追加  srcをdata-srcに変更 ユーザーの画像が無ければデフォルトの画像を表示 -->
                    {% if tweet.myuser.image %}
                    <img alt="" class="avatar avatar-md lazyload" data-src="{{ tweet.myuser.image.url }}">
                    {% else %}
                    <img alt="" class="avatar avatar-md lazyload" data-src="{% static 'assets/img/default-user.png' %}">
                    {% endif %}
                </div>
                <!-- ユーザー情報 -->
                <div>
                    <div class="nav nav-divider">
                        <h6 class="nav-item card-title mb-0"> <a href="#!"> {{ tweet.myuser.nickname }} <span class="ms-2 text-secondary">@{{ tweet.myuser.username }}</span> </a></h6>
                    </div>
                    <p class="mb-0 small">{{ tweet.created_at }}</p>
                </div>
            </div>
        </div>
    </div>
    <!-- 投稿 -->
    <div class="card-body pb-0">
        <!-- 投稿内容 -->
        <p>{{ tweet.text }}</p>
        <!-- 投稿画像 -->
        <!-- classにlazyloadを追加  srcをdata-srcに変更  画像があれば表示 -->
        {% if tweet.tweet_image %}
        <img class="lazyload card-img" data-src="{{ tweet.tweet_image.url }}" alt="">
        {% endif %}
        <!-- アクション -->
        <ul class="nav nav-stack py-1">
            <li class="nav-item">
                <a class="nav-link" href="#!"><i class="fa-regular fa-comment fa-md fa-fw me-2"></i>56</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="#!"><i class="fa-solid fa-retweet fa-md fa-fw me-2"></i>12</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="#!"><i class="fa-solid fa-heart fa-md fa-fw me-2"></i>32</a>
            </li>
            <!-- シェアアクション -->
            <li class="nav-item dropdown ms-sm-auto">
                <a class="nav-link mb-0" href="#" id="cardShareAction" data-bs-toggle="dropdown" aria-expanded="false">
                    <i class="fa-solid fa-arrow-up-from-bracket fa-md fa-fw"></i>
                </a>
                <!-- Card share action dropdown menu -->
                <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="cardShareAction">
                    <li><a class="dropdown-item" href="#"> <i class="bi bi-envelope fa-fw pe-2"></i>メッセージ</a></li>
                    <li><a class="dropdown-item" href="#"> <i class="bi bi-bookmark-check fa-fw pe-2"></i>ブックマーク</a></li>
                    <li><a class="dropdown-item" href="#"> <i class="bi bi-link fa-fw pe-2"></i>コピーリンク</a></li>
                    <li><a class="dropdown-item" href="#"> <i class="bi bi-share fa-fw pe-2"></i>シェア</a></li>
                </ul>
            </li>
        </ul>
    </div>
    <hr class="horizontal dark">
    {% endfor %}
</div>

lazyload機能の追加

画像を遅れて読み込ませるためにlazyload機能を追加します。

sns/base.htmlのheadに次のコードを追加。

<!-- lazyload -->
<script src="https://cdn.jsdelivr.net/npm/lazyload@2.0.0-rc.2/lazyload.js"></script>

/body直前の<script>の中にlazyload();を追加。(既存のscriptに追加する形にしました)

<script>
    lazyload(); <!-- これを追加 -->
    var win = navigator.platform.indexOf('Win') > -1;
    if (win && document.querySelector('#sidenav-scrollbar')) {
    var options = {
        damping: '0.5'
    }
    Scrollbar.init(document.querySelector('#sidenav-scrollbar'), options);
    }
</script>

これでツイートした内容が全て表示されるはずです。


フォロー機能の実装

  • ユーザーの候補を表示
  • follow viewの作成
  • urls.pyにfollow viewを追加
  • index.htmlの編集

ユーザーの候補を表示

右の「You should follow」の部分でフォローできるようにします。

まずindex関数でユーザー4人をランダムで取得してみましょう。

def index(request):
    user = request.user
    follow_list = user.follow.all()
    tweet_list = Tweet.objects.filter(Q(myuser__in=follow_list)|Q(myuser=user)).select_related('myuser').order_by('-created_at')
    # 自分を除外
    user_list = MyUser.objects.exclude(id=user.id).order_by("?")[:4]

    form = TweetForm()
    if request.method == 'POST':
        form = TweetForm(request.POST, request.FILES)
        if form.is_valid():
            tweet = form.save(commit=False)
            tweet.myuser = user
            tweet.save()
            return redirect("sns:index")

    context = {
        'user': user, 'form':form, 
        'follow_list': follow_list, 'tweet_list': tweet_list,
        'user_list': user_list,  # 追加
    }
    return render(request, 'sns/index.html', context)

user_listでは、exclude(id=user.id)で自分を除外し、order_by(“?”)[:4]で4件のオブジェクトをランダムに取得しています。

follow viewとurls.pyの追加

index関数の下にfollow関数を追加します。

def follow(request, id):
    user = request.user
    add_user = get_object_or_404(MyUser, id=id)
    
    # もし既にフォローしていたら
    if add_user in  user.follow.all():
        # 自分のフォローから削除
        user.follow.remove(add_user)
        # 相手のフォロワーから削除
        add_user.follower.remove(user)
    # それ以外は
    else:
        # 自分のフォローに追加
        user.follow.add(add_user)
        # 相手のフォロワーに追加
        add_user.follower.add(user)
    return redirect('sns:index')

sns/urls.pyにfollow関数を実行するpathを追加します。

path('follow/<int:id>/', views.follow, name="follow"),

base.htmlの編集

base.htmlのlist-group(178行目辺り)内を次のように変更してください。

You should followは右のサイドバーにあるので、base.htmlを編集します。

index関数なのに、base.htmlを編集するのに違和感があるかと思いますが、この問題は次のその3で解決していきます。

<ul class="list-group">
    <!-- user_listのユーザーをmyuserで一人ずつ取り出す -->
    {% for myuser in user_list %}
    <li class="list-group-item border-0 d-flex justify-content-between px-0 border-radius-lg">
        <div class="d-flex align-items-center">
            <!-- 画像の有無で分岐 -->
            {% if myuser.image %}
            <img src="{{ myuser.image.url }}" alt="" class="avatar avatar-sm me-3">
            {% else %}
            <img src="{% static 'assets/img/default-user.png' %}" alt="" class="avatar avatar-sm me-3">
            {% endif %}
            <div class="d-flex flex-column">
                <h6 class="text-xs mb-1 text-dark">{{ myuser.nickname }}</h6>
                <span class="text-xs">@{{ myuser.username }}</span>
            </div>
        </div>
        <div class="d-flex align-self-end">
            <!-- myuserのidを取得しfollow関数を実行 -->
            <form action="{% url 'sns:follow' myuser.id %}" method="POST" enctype="multipart/form-data">
                {% csrf_token %}
                <!-- myuserをフォローしてるか、してないかで表示を変える。 -->
                {% if myuser in follow_list %}
                <button class="btn btn-sm btn-outline-danger rounded-pill shadow-none my-auto px-3" type="submit">UnFollow</button>
                {% else %}
                <button class="btn btn-sm btn-outline-primary rounded-pill shadow-none my-auto px-3" type="submit">Follow</button>
                {% endif %}
            </form>
        </div>
    </li>
    {% endfor %}
</ul>

http://127.0.0.1:8000/にアクセスして、フォロー・アンフォローを試してみましょう!

同時に、ツイート一覧の表示も変わるはずです。


次は、プロフィールページの作成と、いいね機能やリツイート機能を実装していきます。

1 Comment