DjangoでSNS(2/2):CRUD操作編
前編はこちら
長いので前後半に分割した。
- 前編:ユーザー認証
- 後編:SNS本体(CRUD操作)
前編はこちら。
タイムライン画面(投稿一覧画面)
ここから主にsns
アプリケーションを編集する。
まずタイムラインを実装する。これはCRUDのR、読み込みにあたる機能である。
実装前にhttp://127.0.0.1:8000/accounts/login/
へアクセスしてログインしておく。
Model
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]>
などと分かりやすく表現できる。
モデルを追加したのでマイグレーションを行う。
(venv) C:\django> python manage.py makemigrations
(venv) C:\django> python manage.py migrate
View
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
ファイルに以下を入力する。
urlpatterns = [
path("top/", views.top_view, name="top"),
+ path("", views.list_view, name="list"),
]
ここでaccounts
プロジェクトの方に少し戻り、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
ファイルをつくり、以下を入力する。これから作成する各画面の共通コードをまとめた基本テンプレートである。
<!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
ファイルをつくり、以下を入力する。
{% 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/
にアクセスすると、以下のようにタイムライン画面が表示される。
上部にタイムラインなどへのリンクやログインユーザー名が表示されている。下の方に投稿が表示される予定だが、まだ投稿していないので何も表示されていない。投稿作成画面をつくって投稿すればいいのだが、先に動作確認したい。管理者画面から作成する方法もあるが面倒くさいので、シェルから登録する。
シェルを起動する。
(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
があるときは、まず参照先のモデルでオブジェクトをつくる(参考)。したがって「ユーザー登録」で登録したユーザーを取得する。
>>> from accounts.models import User
>>> tanaka = User.objects.get(username='tanaka')
>>> yamada = User.objects.get(username='yamada')
取得したユーザーに紐づけた投稿をcreate()
で作成する。
投稿1
>>> from sns.models import Post
>>> Post.objects.create(username=tanaka, text='just setting up my sns')
<Post: tanaka, just setting up my sns>
投稿2
>>> Post.objects.create(username=tanaka, text='くぁwせdrftgyふじこlp')
<Post: tanaka,くぁwせdrftgyふじこlp>
投稿3
>>> Post.objects.create(username=tanaka, text='wwwwwwwwwwwwwwwwwwwwwwwww')
<Post: tanaka, wwwwwwwwwwwwwwwwwwwwwwwww>
投稿4
>>> Post.objects.create(username=yamada, text='山田でーす')
<Post: yamada, 山田でーす>
投稿5
>>> Post.objects.create(username=yamada, text='今日寒いねー')
<Post: yamada, 今日寒いねー>
再びhttp://127.0.0.1:8000/
にアクセスすると、以下のようにタイムライン画面が表示される(図らずも2chみたいになった)。
ユーザーごとの投稿一覧画面
タイムラインと同様にCRUDのR、読み込みにあたる機能である。
View
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
ファイルをつくり、以下を入力する。
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
ファイルをつくり、以下を入力する。
{% 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
ファイルを以下のように変更する。
- <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
ファイルに以下を入力する。
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
ファイルに以下を追加する。
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
ファイルをつくり、以下を入力する。
{% 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.username
はstr
型、object.username
はaccounts.models.User
という型らしく、そのまま比較できない。そこで後者に|stringformat:"s"
をくっつけてstr
型に変換する必要がある(参考)。 -
stringformat:"引数"
は引数に応じて変数の表示形式を変更できる。このようにテンプレート内で変数を加工する仕組みを「組み込みフィルタ」と呼ぶ。
sns\templates\sns\post_list.html
ファイルとsns\templates\sns\post_user_list.html
ファイルのリンクも変更する。これで投稿本文をクリックすると、投稿詳細画面に遷移できるようになった。
- <a href="#undefined">{{ object.text }}</a>
+ <a href="{% url 'sns:detail' object.pk %}">{{ object.text }}</a>
- <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
ファイルに以下を入力する。
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
ファイルに以下を追加する。
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
ファイルをつくり、以下を入力する。
{% 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
ファイルのリンクも変更する。これでフッターから投稿作成画面に遷移できるようになった。
- <a href="#undefined">投稿作成</a> |
+ <a href="{% url 'sns:create' %}">投稿作成</a> |
動作確認
http://127.0.0.1:8000/create/
にアクセスすると、以下のように画面が表示される。
何か入力して投稿作成ボタンを押すと、タイムラインにリダイレクトする。一覧に先ほどの投稿が表示される。
投稿更新画面
CRUDのU、更新にあたる機能である。
View
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
ファイルに以下を追加する。
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
ファイルをつくり、以下を入力する。
{% 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
ファイルを以下のように編集する。これで投稿詳細画面から投稿更新画面に遷移できるようになる。
- <button onclick="location.href=`#undefined`">
+ <button onclick="location.href=`{% url 'sns:update' object.pk %}`">
更新
</button>
動作確認
任意の投稿で更新ボタンをクリックすると、以下のように画面が表示される。
何か変更して更新ボタンを押すと、投稿詳細画面にリダイレクトする。投稿が更新されていることが確認できる。
投稿削除画面
CRUDのD、削除にあたる機能である。
View
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
ファイルに以下を追加する。
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
ファイルをつくり、以下を入力する。
{% 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
ファイルを以下のように編集する。これで投稿詳細画面から投稿削除画面に遷移できるようになる。
- <button onclick="location.href=`#undefined`">
+ <button onclick="location.href=`{% url 'sns:delete' object.pk %}`">
削除
</button>
動作確認
任意の投稿の詳細画面で削除ボタンをクリックすると、以下のように画面が表示される。
削除ボタンを押すと自分の投稿一覧にリダイレクトする。先ほど削除した投稿が消えている。
未ログインユーザーのアクセス禁止
これでSNS部分の実装は終わったが、まだユーザー認証との紐づけを行っていない。したがって今のままだとログインしていない状態でも投稿したりタイムラインを閲覧したりできる。ログイン済みユーザーのみアクセスできるように編集する。
View
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.py
のLOGIN_URL
で指定したURLにリダイレクトする。
リダイレクト先を指定
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