見出し画像

Djangoで写真投稿SNSを作成しよう-ストーリー対話形式で学ぶ④ フォロー機能とタイムラインの実装

以下のnoteの続きです。

コードをPushしたGitリポジトリはこちら。



登場人物

新卒エンジニア:さくら

IT企業に新卒で入社したばかりの元気な女の子。プログラミングは学校で学んだ程度だが、熱意と好奇心は人一倍。

エンジニア:たけし

さくらの教育係。優しく教えるのが得意で、さくらの成長を見守る頼れる先輩。


フォロー機能とタイムラインの実装

フォロー機能の概要

たけし: 「さくらちゃん、今日はフォロー機能を実装していくよ。これでユーザー同士がフォローし合って、特定のユーザーの投稿をタイムラインで見られるようになるんだ。」

さくら: 「フォロー機能って、SNSの基本機能ですよね!これがあれば、ユーザー同士のつながりが強化されて、もっと活発なコミュニケーションが期待できそうです。」

たけし: 「その通りだよ。フォローすることで、自分が興味を持っているユーザーの投稿を優先的にタイムラインに表示できるようになる。まずは、フォロー・アンフォローの機能から始めよう。」

さくら: 「フォローしたユーザーの投稿がFollowing Timelineに表示されて、アンフォローすると表示されなくなるんですね。これでユーザーが自分の興味に合わせてタイムラインをカスタマイズできるんだ。」


フォロータイムラインの実装

たけし: 「フォローしたユーザーの投稿をまとめて表示するために、following_timelineビューを作成しよう。このビューでは、ログインユーザーがフォローしているユーザーの投稿を取得して、タイムラインに表示するんだ。」

# posts/views.py

from django.shortcuts import render
from .models import Post
from django.contrib.auth.decorators import login_required

# 途中のコードは省略

@login_required
def following_timeline(request):
    user = request.user
    following_users = user.following.all()
    posts = Post.objects.filter(user__in=following_users).order_by('-created_at')
    return render(request, 'following_timeline.html', {'posts': posts})

たけし: 「ここで一つクイズを出そう。@login_requiredを付けたView関数にはユーザーがログインしている状態でないとアクセスできなくなる。ここでこれを使う理由がわかるかな?」

突然の質問にさくらは数秒考えこむ。

さくら: 「え、え~と。フォローしてるアカウントの投稿を表示するわけだから、ログインしてないとフォロー中のアカウントが分からないから、ですかね?」

たけし: 「素晴らしい、分かってきたね。posts/urls.pyも書き換えておくよ。ホーム画面がフォロータイムラインになるようにする。」

from django.urls import path
from . import views

urlpatterns = [
    path('', views.following_timeline, name='home'),
    path('create_post/', views.create_post, name='create_post'),
]

さくら: 「フォローしているユーザーの投稿だけをフィルターして表示するんですね。これで、自分が興味のあるユーザーの投稿だけが見られるようになるんだ。」

たけし: 「その通り。次に、このタイムラインを表示するためのテンプレートを作成しよう。」


フォロータイムラインのテンプレートの作成

たけし: 「次にfollowing_timeline.htmlというテンプレートをtemplatesフォルダに作成して、Following Timelineを表示できるようにしよう。このテンプレートでは、フォローしているユーザーの投稿をカード形式で表示するよ。」

<!-- templates/following_timeline.html -->

{% extends 'base_generic.html' %}

{% block content %}
  <h2>Following Timeline</h2>
  {% for post in posts %}
    <div class="post">
      <h3>{{ post.user.username }}</h3>
      <img src="{{ post.image.url }}" alt="Post image">
      <p>{{ post.caption }}</p>
      <p>Tags: {{ post.tags }}</p>
      <p>Posted at {{ post.created_at }}</p>
    </div>
  {% endfor %}
{% endblock %}

さくら: 「これで、フォローしているユーザーの投稿がまとめて見られるんですね。すごく便利になりました!」

投稿詳細ページの概要
たけし: 「さくらちゃん、次は各投稿の詳細ページを作成していくよ。これを作ることで、タイムラインから投稿をクリックすると、その写真の詳細な情報やタグが見られるようになる。」

さくら: 「いいですね!詳細ページがあると、投稿に対してさらに興味がわきそうです。ユーザーが投稿者をフォローするボタンもこのページに入れるんでしたよね?」

たけし: 「そうだね。投稿詳細ページからフォローやアンフォローができるようにしていく。まず、post_detail.htmlを呼び出すためのビューを作成してみよう。」


投稿詳細ページを表示するViewの作成

たけし: 「まず、ビューを作成して、指定した投稿の詳細情報を表示する処理を実装しよう。このビューでは、URLで指定された投稿IDを元に、その投稿の詳細をデータベースから取得して、テンプレートに渡す流れだ。」

# posts/views.py

from django.shortcuts import render, get_object_or_404
from .models import Post


# 途中のコードは省略

def post_detail(request, post_id):
    post = get_object_or_404(Post, id=post_id)
    is_following = post.user in request.user.following.all()

    return render(request, 'post_detail.html', {
        'post': post,
        'is_following': is_following,
    })

さくら: 「get_object_or_404を使って、指定されたIDの投稿を取得するんですね。もし投稿が見つからなかったら404エラーページを表示するという処理ですね。」

たけし: 「その通り。あと大事なのはis_followingでフォロー判定をしているところだ。これはポストの投稿者をフォロー済みの場合にアンフォローボタンを表示させるのに使う。 次は、このビューに対応するURLをurls.pyに追加しよう。」

URLの設定

たけし: 「次に、投稿詳細ページを表示するためのURLパターンを追加しよう。このURLは、タイムラインや他のページから投稿をクリックしたときに呼び出されるように設定するよ。あとフォロー機能のパスも一緒に設定しておく」

from django.urls import path
from . import views

urlpatterns = [
    # 途中は省略
    path('post/<int:post_id>/', views.post_detail, name='post_detail'),
    path('follow_unfollow_user/<int:id>/', views.follow_unfollow_user, name='follow_unfollow_user'),
]

さくら: 「投稿のIDをURLの一部に含めるようになっていて、それによって特定の投稿を表示できるんですね。これで、クリックした投稿の詳細ページに遷移できるようになったんですね。」

フォロータイムラインから投稿詳細への遷移

たけし: 「次に、フォロータイムラインから特定の投稿をクリックすると、その投稿の詳細ページに遷移するようにしてみよう。まず、following_timeline.htmlにリンクを追加して、投稿詳細ページへ飛べるようにするんだ。」

<!-- templates/following_timeline.html -->

{% extends 'base_generic.html' %}

{% block content %}
  <h2>Following Timeline</h2>
  {% for post in posts %}
    <div class="post">
      <h3>{{ post.user.username }}</h3>
      <a href="{% url 'post_detail' post.id %}">
        <img src="{{ post.image.url }}" alt="Post image">
      </a>
      <p>{{ post.caption }}</p>
      <p>Tags: {{ post.tags }}</p>
      <p>Posted at {{ post.created_at }}</p>
    </div>
  {% endfor %}
{% endblock %}

さくら: 「タイムラインに表示されている画像をクリックすると、その投稿の詳細ページに遷移するんですね。」

たけし: 「そうだよ。こうすることで、ユーザーは気になる投稿をクリックして、詳しい情報を確認できるんだ。」

投稿詳細画面でのフォロー機能の追加

たけし: 「投稿詳細ページを作成するよ。templatesフォルダにpost_detail.htmlを作ってね。これには投稿の情報や投稿者のフォローボタンを含めてある」

<!-- templates/posts/post_detail.html -->

{% extends 'base_generic.html' %}

{% block content %}
<div 
  <h2>{{ post.user.username }}'s Post</h2>
  <img class="post_detail" src="{{ post.image.url }}" alt="Post image">
  <p>{{ post.caption }}</p>
  <p>Tags: {{ post.tags }}</p>
  <p>Posted by {{ post.user.username }} at {{ post.created_at }}</p>

  {% if user != post.user %}
    <form method="post" action="{% url 'follow_unfollow_user' post.id %}">
      {% csrf_token %}
      {% if is_following %}
        <button type="submit" class="button-unfollow">Unfollow</button>
      {% else %}
        <button type="submit" class="button-follow">Follow</button>
      {% endif %}
    </form>
  {% endif %}
{% endblock %}

たけし: 「viewにもフォロー機能を追加するよ.。ポストのIDをもとに投稿主のIDを取得して、それが未フォローならフォローするし、フォロー済みなら解除する。最後に投稿詳細画面にリダイレクトするよ」

def follow_unfollow_user(request, id):
    user_id = Post.objects.get(id=id).user.id
    user_to_follow = get_object_or_404(CustomUser, id=user_id)
    if user_to_follow in request.user.following.all():
        request.user.following.remove(user_to_follow)
    else:
        request.user.following.add(user_to_follow)

    return redirect('post_detail', post_id=id)

さくら: 「タイムラインから投稿をクリックして、その投稿者のプロフィールが表示されると、フォローボタンやアンフォローボタンが表示されるんですね。」

たけし: 「そうだよ。これで、タイムラインから投稿を選んで、詳細ページでフォローやアンフォローの操作もできるようになった」

CSSの設定

たけし: 「この辺りでそろそろCSSを設定をしておこう。アプリのデザインを綺麗にしたり、画像の表示サイズを制限したりする必要もある」

myproject/static/cssフォルダを作成してそこにstyle.cssを作成

/* Reset CSS */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

/* Body and basic typography */
body {
    font-family: Arial, sans-serif;
    background-color: #f5f8fa;
    color: #333;
    line-height: 1.6;
}

a {
    text-decoration: none;
    color: #1da1f2;
}

a:hover {
    text-decoration: underline;
}

/* Navbar */
.navbar {
    background-color: #ffffff;
    padding: 10px 20px;
    border-bottom: 1px solid #e1e8ed;
}

.navbar .navbar-brand {
    font-size: 24px;
    color: #1da1f2;
    font-weight: bold;
}

.navbar .navbar-nav {
    list-style-type: none;
    display: flex;
    gap: 20px;
}

.navbar .navbar-nav li {
    display: inline;
}

.navbar .navbar-nav a {
    color: #657786;
    font-weight: 500;
}

.navbar .navbar-nav a:hover {
    color: #1da1f2;
}

/* Container */
.container {
    width: 30%;
    max-width: 1000px;
    margin: 0 auto;
    padding: 20px 0;
}

/* Footer */
.footer {
    background-color: #ffffff;
    padding: 20px;
    text-align: center;
    border-top: 1px solid #e1e8ed;
    margin-top: 40px;
    color: #657786;
}

/* Forms */
form {
    padding: 20px;
    border-radius: 5px;
    margin-bottom: 20px;
}

form input[type="text"],
form input[type="file"],
form textarea {
    width: 100%;
    padding: 10px;
    margin: 10px 0;
    border: 1px solid #ccd6dd;
    border-radius: 4px;
}

form button {
    background-color: #1da1f2;
    color: white;
    padding: 10px 15px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

form button:hover {
    background-color: #0c85d0;
}

/* Posts (timeline) */
.post {
    background-color: #ffffff;
    padding: 20px;
    margin-bottom: 15px;
    border-radius: 5px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    border: 1px solid #e1e8ed;
}

.post img {
    max-width: 50%;
    border-radius: 5px;
    margin-bottom: 10px;
}

.post .post-caption {
    font-size: 16px;
    color: #333;
    margin-bottom: 10px;
}

.post .post-tags {
    font-size: 14px;
    color: #657786;
}

.post .post-author {
    font-weight: bold;
    color: #1da1f2;
}

.post .post-timestamp {
    font-size: 12px;
    color: #657786;
}

/* Buttons */
.button-follow {
    background-color: #1da1f2;
    color: white;
    padding: 5px 10px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
}

.button-follow:hover {
    background-color: #0c85d0;
}

.button-unfollow {
    background-color: #e0245e;
    color: white;
    padding: 5px 10px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
}

.button-unfollow:hover {
    background-color: #b81c47;
}

.post_detail {
    width: 200%;
    height: auto;
    display: block;
    margin: 10px 0;
}

settings.pyを開いて末尾に以下を追加

STATIC_URL = 'static/'
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),
]

たけし: 「さて、これでタイムラインを表示させてみよう。どんな手順が必要かわかるかい?」

さくら: 「フォローしてるアカウントの投稿を表示させるから、アカウントが2つ必要ですね。それで片方のアカウントでもう片方をフォローして、フォローされた方のアカウントが写真を投稿してればOKです!」

たけし: 「正解! 最初はタイムラインに投稿が表示されないから、管理画面(http://127.0.0.1:8000/admin)から必要な操作を行おうか。フォローするにはUsersテーブルのFollowing項目でフォロー先のアカウントを選択してSaveすればOK」


さくら: 「できました! デザインもかわいくなりました!」

たけし: 「デザインやUIが良くないとアプリを使いたいという気持ちになってもらえないこともあるから大事だよね。」

さくら: 「詳細画面も大丈夫そうです。ちゃんとフォローボタンが使えます!」


実装のまとめ

たけし: 「今回は、投稿詳細ページを作成して、タイムラインからそのページに遷移できるようにした。フォロー機能も詳細ページから操作できるようにして、ユーザー間のインタラクションがさらに強化されたね。」

さくら: 「とてもわかりやすかったです!投稿詳細ページからフォローやアンフォローができるのは、ユーザーにとっても便利ですし、SNSらしい機能がどんどん揃ってきましたね。」

たけし: 「その通りだね。これで基本的な投稿とフォロー機能は完成したから、次はおすすめ機能をさらに強化して、よりパーソナライズされたタイムラインを作っていこう。」

さくら: 「次のステップも楽しみです。ありがとうございます!」

こうして、たけしとさくらは投稿詳細ページの実装を終え、SNSがさらに充実したものとなった。次は、より高度なおすすめ機能の実装に向けて進んでいく。

反響次第で次回へ続く。

スキや感想をnote・Xでお待ちしてます!


サポートは料理好きなのでの食材費にさせていただきます。