📱

DjangoでSNS(2/2):CRUD操作編

2024/01/21に公開

前編はこちら

長いので前後半に分割した。

  • 前編:ユーザー認証
  • 後編:SNS本体(CRUD操作)

前編はこちら。
https://zenn.dev/pb/articles/db9b60bd65ea8d

タイムライン画面(投稿一覧画面)

ここから主にsnsアプリケーションを編集する。

まずタイムラインを実装する。これはCRUDのR、読み込みにあたる機能である。

実装前にhttp://127.0.0.1:8000/accounts/login/へアクセスしてログインしておく。

Model

sns\models.pyファイルを以下のように編集する。

django\sns\models.py
from django.db import models

from accounts.models import User


class Post(models.Model):
    username = models.ForeignKey(User, on_delete=models.CASCADE)  # ユーザー名
    text = models.TextField()  # 本文
    created_at = models.DateTimeField(auto_now_add=True)  # 作成日時
    updated_at = models.DateTimeField(auto_now=True)  # 更新日時

    def __str__(self):
        return f"{self.username}, {self.text}"
  • auto_now_add=Trueにすると、オブジェクトを作成したときに自動で現在日時をセットする(参考)。
  • auto_now=Trueにすると、オブジェクトを更新したときに自動で現在日時をセットする(参考)。
  • __str__はオブジェクトの文字列表現を定義するメソッドである。これによってオブジェクトを表示したときに<QuerySet [<Post: Post object (1)>]>ではなく、<QuerySet [<Post: tanaka, hoge]>などと分かりやすく表現できる。

モデルを追加したのでマイグレーションを行う。

command-line
(venv) C:\django> python manage.py makemigrations
(venv) C:\django> python manage.py migrate

View

sns\views.pyファイルに以下を入力する。

django\sns\views.py
from django.shortcuts import render

from .models import Post


def list_view(request):
    object_list = Post.objects.all().order_by("updated_at")
    context = {"object_list": object_list}
    return render(request, "sns/post_list.html", context)
  • .order_by("updated_at")をつけることによって、更新日時順になる。

URL

sns\urls.pyファイルに以下を入力する。

django\sns\urls.py
urlpatterns = [
    path("top/", views.top_view, name="top"),
+     path("", views.list_view, name="list"),
]

ここでaccountsプロジェクトの方に少し戻り、accounts\views.pyファイルを以下のように変更する。

django\accounts\views.py
def login_view(request):
    if request.method == "POST":
        form = LoginForm(request, data=request.POST)
        if form.is_valid():
            user = form.get_user()
            if user:
                login(request, user)
-                 return redirect(to="/accounts/login/#undefined/")
+                 return redirect("sns:list")
  • 今まではログインしてもどこにも遷移しなかったが、これでタイムラインに遷移するようになった。

Template

sns\templates\snsディレクトリにbase.htmlファイルをつくり、以下を入力する。これから作成する各画面の共通コードをまとめた基本テンプレートである。

django\sns\templates\sns\base.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>{% block title %}{% endblock %}</title>
  </head>
  <body>
    <header>
      俺のSNS
      <a href="{% url 'sns:list' %}">タイムライン</a> |
      <a href="#undefined">投稿作成</a> |
      <a href="{% url 'accounts:logout' %}">ログアウト</a> |
      username: {{ user.username }}
    </header>
    {% block content %}
    {% endblock %}
  </body>
</html>
  • {% block title %}{% endblock %}の間に各画面のタイトルが入る(参考)。
  • {% block content %}{% endblock %}の間に各画面の内容が入る。
  • 常に表示するものを設定したいので、ヘッダーを作った。
  • ヘッダーにまずタイムライン画面と投稿作成画面、ログアウトへのリンクを表示した。<a>のhref属性に{% url '...' %}でリンクを設定できる。
  • またヘッダーにログインしているユーザー名を表示させた。{{ user.username }}と記載すれば、Viewにてrender()でTemplateに渡したり、get_context_data()をオーバーライドしたりせずとも値を表示できる(参考)。すげえ!

sns\templates\snsディレクトリにpost_list.htmlファイルをつくり、以下を入力する。

django\sns\templates\sns\post_list.html
{% extends 'sns/base.html' %}

{% block title %}
  タイムライン
{% endblock title %}

{% block content %}
  <h1>タイムライン</h1>
  <hr>
  {% for object in object_list reversed %}
    <div>
      {{ object.updated_at }},
      <a href="#undefined">{{ object.username }}</a>
    </div>
    <a href="#undefined">{{ object.text }}</a>
    <hr>
  {% endfor %}
{% endblock content %}
  • {% extends 'ファイル名' %}で継承する基本テンプレートを指定する。
  • {% block title %}{% endblock title %}の間がタイトルとして表示される。
  • {% block content %}{% endblock content %}の間のいろいろが内容として表示される。
  • {% for object in object_list %}...{% endfor %}はforループであり、オブジェクトのリストを1つずつ表示させる。
  • forループにreversedを指定することで、ループが逆順になる(参考)。デフォルトだと上から古い順に投稿が表示されるが、XやFaceBookのように上から新しい順にしたかったから。
  • {{ object.text }}(本文)を<a href="#undefined">で囲み、仮のリンクを設定した。後で投稿詳細へのリンクに変更する。
  • <hr>は水平線を表示する(参考)。地味に初めて使ったが、シンプルでいいね😎

動作確認

http://127.0.0.1:8000/にアクセスすると、以下のようにタイムライン画面が表示される。

タイムライン

上部にタイムラインなどへのリンクやログインユーザー名が表示されている。下の方に投稿が表示される予定だが、まだ投稿していないので何も表示されていない。投稿作成画面をつくって投稿すればいいのだが、先に動作確認したい。管理者画面から作成する方法もあるが面倒くさいので、シェルから登録する。

シェルを起動する。

command-line
(venv) C:\django> python manage.py shell
Python 3.11.1 (tags/v3.11.1:a7a450f, Dec  6 2022, 19:58:39) [MSC v.1934 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

モデルの中にForeignKeyがあるときは、まず参照先のモデルでオブジェクトをつくる(参考)。したがって「ユーザー登録」で登録したユーザーを取得する。

command-line
>>> from accounts.models import User
>>> tanaka = User.objects.get(username='tanaka')
>>> yamada = User.objects.get(username='yamada')

取得したユーザーに紐づけた投稿をcreate()で作成する。

投稿1

command-line
>>> from sns.models import Post
>>> Post.objects.create(username=tanaka, text='just setting up my sns')
<Post: tanaka, just setting up my sns>

投稿2

command-line
>>> Post.objects.create(username=tanaka, text='くぁwせdrftgyふじこlp')
<Post: tanaka,くぁwせdrftgyふじこlp>

投稿3

command-line
>>> Post.objects.create(username=tanaka, text='wwwwwwwwwwwwwwwwwwwwwwwww')
<Post: tanaka, wwwwwwwwwwwwwwwwwwwwwwwww>

投稿4

command-line
>>> Post.objects.create(username=yamada, text='山田でーす')
<Post: yamada, 山田でーす>

投稿5

command-line
>>> Post.objects.create(username=yamada, text='今日寒いねー')
<Post: yamada, 今日寒いねー>

再びhttp://127.0.0.1:8000/にアクセスすると、以下のようにタイムライン画面が表示される(図らずも2chみたいになった)。

タイムライン

ユーザーごとの投稿一覧画面

タイムラインと同様にCRUDのR、読み込みにあたる機能である。

View

sns\views.pyファイルに以下を入力する。

django\sns\views.py
from django.shortcuts import render

from .models import Post, User

⋮

def user_list_view(request, user):
    selected_user = User.objects.get(username=user)
    object_list = Post.objects.filter(username=selected_user).order_by("updated_at")
    context = {"selected_user": selected_user, "object_list": object_list}
    return render(request, "sns/post_user_list.html", context)
  • 引数のuserは↓の<str:user>から受け取る。str型である。
  • 選択したユーザーのオブジェクトをつくってから投稿を取得する。

URL

snsディレクトリにurls.pyファイルをつくり、以下を入力する。

django\sns\urls.py
urlpatterns = [
    path("top/", views.top_view, name="top"),
    path("", views.list_view, name="list"),
+     path("user/<str:user>/", views.user_list_view, name="userlist"),
]

Template

sns\templates\snsディレクトリにpost_user_list.htmlファイルをつくり、以下を入力する。

django\sns\templates\sns\post_user_list.html
{% extends 'sns/base.html' %}

{% block title %}
  {{ selected_user.username }} の投稿一覧
{% endblock title %}

{% block content %}
  <h1>{{ selected_user.username }} の投稿一覧</h1>
  <hr>
  {% for object in object_list reversed %}
    <div>
      {{ object.updated_at }}
    </div>
    <a href="#undefined">{{ object.text }}</a>
    <hr>
  {% endfor %}
{% endblock content %}

post_list.htmlファイルを以下のように変更する。

django\sns\templates\sns\post_list.html
-     <a href="#undefined">{{ object.username }}</a>
+     <a href="{% url 'sns:userlist' object.username %}">{{ object.username }}</a>

動作確認

http://127.0.0.1:8000/にアクセスして任意の投稿のユーザー名をクリックすると、以下のようにそのユーザーの投稿一覧画面が表示される。

ユーザーごとの投稿一覧画面

投稿詳細画面

CRUDのR、読み込みにあたる機能である。

View

sns\views.pyファイルに以下を入力する。

django\sns\views.py
from django.shortcuts import render

from .models import Post, User

⋮

def detail_view(request, pk):
    object = Post.objects.get(pk=pk)
    context = {"object": object}
    return render(request, "sns/post_detail.html", context)

URL

sns\urls.pyファイルに以下を追加する。

django\sns\urls.py
urlpatterns = [
    path("top/", views.top_view, name="top"),
    path("", views.list_view, name="list"),
    path("user/<str:user>/", views.user_list_view, name="userlist"),
+     path("detail/<int:pk>/", views.detail_view, name="detail"),
]

Template

sns\templates\snsディレクトリにpost_detail.htmlファイルをつくり、以下を入力する。

django\sns\templates\sns\post_detail.html
{% extends 'sns/base.html' %}

{% block title %}
  投稿詳細
{% endblock title %}

{% block content %}
  <h1>投稿詳細</h1>
  <div>created_at: {{ object.created_at }}</div>
  <div>updated_at: {{ object.updated_at }}</div>
  <div>username: {{ object.username }}</div>
  <div>text: {{ object.text }}</div>
  <button type="button" onclick="location.href=`{% url 'sns:userlist' object.username %}`">
    ユーザーの投稿一覧
  </button>

  {% if user.username == object.username|stringformat:"s" %}
    <button onclick="location.href=`#undefined`">
      更新
    </button>
    <button onclick="location.href=`#undefined`">
      削除
    </button>
  {% endif %}
{% endblock content %}
  • {% if user.username == object.username|stringformat:"s" %}...{% endif %}は、ログインしているユーザー名と投稿者のユーザー名が一致したときだけ(つまり自分が作成した投稿だけ)更新ボタンと削除ボタンを表示する条件分岐である。
  • ユーザー名を比較するだけなら、{% if object.username == user.username %}でいいのではないかと思うかもしれない(私はそう思い、そしてハマった)。user.usernamestr型、object.usernameaccounts.models.Userという型らしく、そのまま比較できない。そこで後者に|stringformat:"s"をくっつけてstr型に変換する必要がある(参考)。
  • stringformat:"引数"は引数に応じて変数の表示形式を変更できる。このようにテンプレート内で変数を加工する仕組みを「組み込みフィルタ」と呼ぶ。

sns\templates\sns\post_list.htmlファイルとsns\templates\sns\post_user_list.htmlファイルのリンクも変更する。これで投稿本文をクリックすると、投稿詳細画面に遷移できるようになった。

django\sns\templates\sns\post_list.html
- <a href="#undefined">{{ object.text }}</a>
+ <a href="{% url 'sns:detail' object.pk %}">{{ object.text }}</a>
django\sns\templates\sns\post_user_list.html
- <a href="#undefined">{{ object.text }}</a>
+ <a href="{% url 'sns:detail' object.pk %}">{{ object.text }}</a>

動作確認

タイムラインの任意の投稿本文をクリックすると、以下のように投稿詳細画面が表示される。

投稿詳細画面

※投稿詳細画面のURLの一番後ろはpkである。画像では1番目の投稿をクリックしたのでhttp://127.0.0.1:8000/detail/1/になるはずだが、http://127.0.0.1:8000/detail/12/
になっている。これは筆者が裏で試行錯誤した結果なので、気にせず進んでほしい。

投稿作成画面

投稿作成はCRUDのC、新規作成にあたる機能である。

View

sns\views.pyファイルに以下を入力する。

django\sns\views.py
from django.shortcuts import redirect, render

from .models import Post, User

⋮

def create_view(request):
    if request.method == "POST":
        username = request.user
        text = request.POST["text"]
        Post.objects.create(username=username, text=text)
        return redirect("sns:list")
    if request.method == "GET":
        return render(request, "sns/post_form.html")

URL

sns\urls.pyファイルに以下を追加する。

django\sns\urls.py
urlpatterns = [
    path("top/", views.top_view, name="top"),
    path("", views.list_view, name="list"),
    path("user/<str:user>/", views.user_list_view, name="userlist"),
    path("detail/<int:pk>/", views.detail_view, name="detail"),
+     path("create/", views.create_view, name="create"),
]

Template

sns\templates\snsディレクトリにpost_form.htmlファイルをつくり、以下を入力する。

django\sns\templates\sns\post_form.html
{% extends 'sns/base.html' %}

{% block title %}
  投稿作成
{% endblock title %}

{% block content %}
  <h1>投稿作成</h1>
  <form method="post" action="{% url 'sns:create' %}">
    {% csrf_token %}
    <input type="text" name="text" />
    <button>投稿作成</button>
  </form>
  <button type="button" onclick="history.back()">戻る</button>
{% endblock content %}
  • 戻るボタンのonclick="history.back()"によって前のページに戻ることができる。これはJavaScriptである(参考)。

sns\templates\sns\base.htmlファイルのリンクも変更する。これでフッターから投稿作成画面に遷移できるようになった。

django\sns\templates\sns\base.html
- <a href="#undefined">投稿作成</a> |
+ <a href="{% url 'sns:create' %}">投稿作成</a> |

動作確認

http://127.0.0.1:8000/create/にアクセスすると、以下のように画面が表示される。

投稿作成画面

何か入力して投稿作成ボタンを押すと、タイムラインにリダイレクトする。一覧に先ほどの投稿が表示される。

タイムライン

投稿更新画面

CRUDのU、更新にあたる機能である。

View

sns\views.pyファイルに以下を入力する。

django\sns\views.py
from django.shortcuts import redirect, render

from .models import Post, User

⋮

def update_view(request, pk):
    object = Post.objects.get(pk=pk)
    if request.method == "POST":
        object.text = request.POST["text"]
        object.save()
        return redirect("sns:detail", pk)
    else:
        context = {"object": object}
        return render(request, "sns/post_update.html", context)

URL

sns\urls.pyファイルに以下を追加する。

django\sns\urls.py
urlpatterns = [
    path("top/", views.top_view, name="top"),
    path("", views.list_view, name="list"),
    path("user/<str:user>/", views.user_list_view, name="userlist"),
    path("detail/<int:pk>/", views.detail_view, name="detail"),
    path("create/", views.create_view, name="create"),
+     path("update/<int:pk>/", views.update_view, name="update"),
]

Template

sns\templates\snsディレクトリにpost_update.htmlファイルをつくり、以下を入力する。

django\sns\templates\sns\post_update.html
{% extends 'sns/base.html' %}

{% block title %}
  投稿更新
{% endblock title %}

{% block content %}
  <h1>投稿更新</h1>
  <form method="POST" action="{% url 'sns:update' object.pk %}">
    {% csrf_token %}
    <input type="text" name="text" value="{{ object.text }}" />
    <button>更新</button>
  </form>
  <button type="button" onclick="history.back()">戻る</button>
{% endblock content %}

post_detail.htmlファイルを以下のように編集する。これで投稿詳細画面から投稿更新画面に遷移できるようになる。

django\sns\templates\sns\post_detail.html
- <button onclick="location.href=`#undefined`">
+ <button onclick="location.href=`{% url 'sns:update' object.pk %}`">
    更新
  </button>

動作確認

任意の投稿で更新ボタンをクリックすると、以下のように画面が表示される。

投稿更新画面

何か変更して更新ボタンを押すと、投稿詳細画面にリダイレクトする。投稿が更新されていることが確認できる。

投稿詳細画面

投稿削除画面

CRUDのD、削除にあたる機能である。

View

sns\views.pyファイルに以下を入力する。

django\sns\views.py
from django.shortcuts import redirect, render

from .models import Post, User

⋮

def delete_view(request, pk):
    object = Post.objects.get(pk=pk)
    if request.method == "POST":
        object.delete()
        return redirect("sns:userlist", object.username)
    else:
        context = {"object": object}
        return render(request, "sns/post_confirm_delete.html", context)

URL

sns\urls.pyファイルに以下を追加する。

django\sns\urls.py
urlpatterns = [
    path("top/", views.top_view, name="top"),
    path("", views.list_view, name="list"),
    path("user/<str:user>/", views.user_list_view, name="userlist"),
    path("detail/<int:pk>/", views.detail_view, name="detail"),
    path("create/", views.create_view, name="create"),
    path("update/<int:pk>/", views.update_view, name="update"),
+     path("delete/<int:pk>/", views.delete_view, name="delete"),
]

Template

sns\templates\snsディレクトリにpost_confirm_delete.htmlファイルをつくり、以下を入力する。

django\sns\templates\sns\post_confirm_delete.html
{% extends 'sns/base.html' %}

{% block title %}
  投稿削除
{% endblock title %}

{% block content %}
  <h1>投稿削除</h1>
  <form method="POST">
    {% csrf_token %}
    <div>text: {{ object.text }}</div>
    <div>削除してもよろしいですか?</div>
    <button type="button" onclick="history.back()">戻る</button>
    <button>削除</button>
  </form>
{% endblock content %}

post_detail.htmlファイルを以下のように編集する。これで投稿詳細画面から投稿削除画面に遷移できるようになる。

django\sns\templates\sns\post_detail.html
- <button onclick="location.href=`#undefined`">
+ <button onclick="location.href=`{% url 'sns:delete' object.pk %}`">
    削除
  </button>

動作確認

任意の投稿の詳細画面で削除ボタンをクリックすると、以下のように画面が表示される。

投稿削除画面

削除ボタンを押すと自分の投稿一覧にリダイレクトする。先ほど削除した投稿が消えている。

自分の投稿一覧

未ログインユーザーのアクセス禁止

これでSNS部分の実装は終わったが、まだユーザー認証との紐づけを行っていない。したがって今のままだとログインしていない状態でも投稿したりタイムラインを閲覧したりできる。ログイン済みユーザーのみアクセスできるように編集する。

View

sns\views.pyを以下のように編集する。

django\sns\views.py
+ from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render

from .models import Post, User


def top_view(request):
⋮

+ @login_required
def list_view(request):
⋮

+ @login_required
def user_list_view(request, user):
⋮

+ @login_required
def detail_view(request, pk):
⋮

+ @login_required
def create_view(request):
⋮

+ @login_required
def update_view(request, pk):
⋮

+ @login_required
def delete_view(request, pk):
⋮
  • 関数の前に@login_requiredをつけるだけである。これでログイン済みユーザーのみアクセスできるようになった(トップ画面、ユーザー登録画面、ログイン画面を除く)。
  • もし未ログインユーザーがアクセスしようとした場合、次のsettings.pyLOGIN_URLで指定したURLにリダイレクトする。

リダイレクト先を指定

settings.pyファイルに以下を追加する(一番下とかに)。未ログインユーザーはログイン画面にリダイレクトすることになる。

django\config\settings.py
+ LOGIN_URL = "/accounts/login/"

動作確認

一度http://127.0.0.1:8000/accounts/logout/でログアウトする。それから以下のURLにアクセスすると、いずれもログイン画面にリダイレクトする。

  • http://127.0.0.1:8000/(タイムライン画面)
  • http://127.0.0.1:8000/user/tanaka/(ユーザーごとの投稿一覧画面)
  • http://127.0.0.1:8000/detail/2/(投稿詳細画面)
  • http://127.0.0.1:8000/create/(投稿作成画面)
  • http://127.0.0.1:8000/update/2/(投稿更新画面)
  • http://127.0.0.1:8000/delete/2/(投稿削除画面)

そしてhttp://127.0.0.1:8000/accounts/login/でログインした場合、(2番目の投稿を削除していなければ)上記すべてのURLにアクセスできる。

感想

これでSNS製作は以上である。シンプルなアプリではあるが、できあがったものを操作していると少なからず感動を覚えた。

コード全体を見ると複雑に感じるが、CRUD操作やユーザー認証など基本的な技術の積み重ねなのでそれほど難しくはない。今後関数ベースビューからクラスベースビューへの置き換えや、いいね機能・フォロー機能の追加にも挑戦したい。

Github

TODO:コードアップロードする

参考

Discussion