Python、Django でプロトタイプ・テキスト SNS を実装(後半)
後半、いきまっす!
今回やること(後半)
- チェック、一覧表示、解除
- コメント投稿、削除
- つながる、一覧表示、またね
- いいね(非同期)
- 検索
- 今回のコード
- 「いいね」を参考にさせてもらったレクチャー
1. チェック、一覧表示、解除
urls.py、models.py、views.py、html の順でファイルを作ります。
チェック(お氣にいり)用と、チェック一覧表示用のパスを指定します。
urlpatterns = [
...
path('check/<int:content_id>/', views.check_func, name='check'),
path('check_contents/', views.check_contents_func, name='check_contents'),
...
ForeignKey で、Content と get_user_model( reader )に紐づけます。
get_user_model: https://docs.djangoproject.com/ja/5.2/topics/auth/customizing/#django.contrib.auth.get_user_model
get_user_model は、現在アクティブなユーザ・モデルを返します。
...
class Check(models.Model):
content = models.ForeignKey(Content, on_delete=models.CASCADE)
reader = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
チェックは、リンクをクリックするたびにオン・オフする簡易仕様にします。
また、チェックしたときは、チェック一覧に遷移するようにします。
...
from .models import Content, Check
...
def check_func(request, content_id):
reader = request.user
content = Content.objects.get(pk=content_id)
exists = Check.objects.filter(content=content, reader=reader).exists()
if not exists:
check = Check.objects.create(content=content, reader=reader)
check.save()
messages.warning(request, 'チェックしました')
else:
check = Check.objects.get(content=content, reader=reader)
check.delete()
messages.success(request, 'チェックを外しました')
return redirect('text_sns:check_contents')
def check_contents_func(request):
check_objs= Check.objects.filter(reader=request.user).all()
check_content_id_list = [obj.content_id for obj in check_objs]
check_content_objs = Content.objects.filter(id__in=check_content_id_list).order_by('-created_at')
paginator = Paginator(check_content_objs, PAGENATOR_NUM)
page_number = request.GET.get('page')
context = {
'page_obj': paginator.get_page(page_number),
'page_number': page_number,
'title': 'チェック一覧',
}
return render(request, 'text_sns/index.html', context)
前回忘れていた全件表示とチェック一覧、チェックするリンクをはります。
...
{% if user.is_authenticated %}
<p>{{ user.username }}:{{ title }}</p>
<p>
...
<a class="navbar-brand fs-6" href="{% url 'text_sns:posts' %}">投稿</a>
<a class="navbar-brand fs-6" href="{% url 'text_sns:all_contents' %}">全件表示</a>
<a class="navbar-brand fs-6" href="{% url 'text_sns:check_contents' %}">チェック一覧</a>
...
{% for obj in page_obj %}
<p class="text-muted small m-1">
「{{ obj.title }}」author: {{ obj.author }}
<span class="m-3"><a href="{% url 'text_sns:check' obj.id %}">チェック</a></span>
...
マイグレートします。
python manage.py makemigrations
python manage.py migrate
動作確認

投稿「てすと」をチェックすると、チェック一覧表示になります。

もう一度チェックをクリックすると、チェックが外れます。

2. コメント投稿、削除
urls.py、models.py、forms.py、views.py、html の順にファイルを作ります。
...
urlpatterns = [
...
path('comment/<int:content_id>/', views.comment_func, name='comment'),
path('comment_delete/<int:comment_id>/', views.comment_delete_func, name='comment_delete'),
]
チェック同様、ForeignKey で、Content と get_user_model( writer)に紐づけます。
...
class Comment(models.Model):
content = models.ForeignKey(Content, on_delete=models.CASCADE)
writer = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
comment = models.TextField('コメント', max_length=128)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.comment[:10]
...
コメント入力フォームを作り、入力表示を調整します。
from .models import Content, Comment
...
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = '__all__'
widgets = {
'comment': forms.Textarea(attrs={'rows': 3, 'cols': 40}),
}
ビューを作成します。
...
from .forms import SignupForm, LoginForm, PostsForm, CommentForm
from .models import Content, Check, Comment
...
def comment_func(request, content_id):
if request.method == 'POST':
comment_form = CommentForm(request.POST)
if comment_form.is_valid():
comment_form.save()
messages.error(request, 'コメントしました')
return redirect('text_sns:index')
else:
messages.error(request, 'コメントできませんでした')
return redirect('text_sns:index')
else:
comment_form = CommentForm()
context={
'form': comment_form,
'content_id': content_id,
}
return render(request, 'text_sns/comment.html', context)
HTML ファイルを作成していきます。
コンテンツとユーザーは固定すべきなので、type="hidden" で入力しています。
{% extends 'text_sns/input.html' %}
{% block sub_content %}
{% csrf_token %}
<div>
<input type="hidden" name="content" value="{{ content_id }}">
<input type="hidden" name="writer" value="{{ user.id }}">
{% for item in form %}
{% if item.label != 'Content' and item.label != 'Writer' %}
{{ item.label }}:<br>
{{ item }}
{% endif %}
{% endfor %}
</div>
<br>
<button class="w-100 btn btn-light" type="submit">投稿</button>
{% endblock %}
<p class="text-muted small">
<a href="{% url 'text_sns:comment' obj.id %}">コメントする</a>
<span class="m-2">コメント件数: {{ obj.comment_set.all.count }}</span><br>
{% if obj.comment_set.all %}
{% for comment in obj.comment_set.all %}
{{ comment.writer }}:
{{ comment.comment | linebreaksbr | urlize }}
({{ comment.created_at }})
{% endfor %}
{% endif %}
</p>
{% endfor %}
<span class="d-flex justify-content-center">
{% include 'snippets/pagination.html' %}
</span>
...
マイグレートします。
python manage.py makemigrations
python manage.py migrate
動作確認(レイアウトを調整してます)

「てすと」にコメントします。

無事にコメントされました。

削除もうまくいきました。

3. つながる、一覧表示、またね
urls.py、models.py、views.py、html の順にファイルを作成していきます。
URL パスを指定します。
...
urlpatterns = [
...
path('connect/<int:content_id>/', views.connect_func, name='connect'),
path('connects/', views.connects_func, name='connects'),
path('connect_seeyou/<int:connected_id>/', views.connect_seeyou_func, name='connect_seeyou'),
]
モデルを作成します。
...
from django.contrib.auth.models import User
...
class Connect(models.Model):
connect = models.ForeignKey(get_user_model(), related_name='connect', on_delete=models.CASCADE)
connected = models.ForeignKey(User, related_name='connected', on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f'{self.connected.id}:{self.connected}'
ビューを作成します。
インデックスに、つながった人の投稿も表示します。
前回、忘れていたので全件表示にもページネーションを実装します。
...
from .models import Content, Check, Comment, Connect
...
def index(request):
user_id = request.user.id
connects = Connect.objects.filter(connect=request.user.id)
author_id_list = [connect.connected for connect in connects]
author_id_list.append(user_id)
user_objects = Content.objects.order_by('-created_at').filter(author_id__in=author_id_list)
paginator = Paginator(user_objects, PAGENATOR_NUM)
page_number = request.GET.get('page')
context = {
'page_obj': paginator.get_page(page_number),
'page_number': page_number,
'title': 'ホーム',
}
return render(request, 'text_sns/index.html', context)
...
def all_contents_func(request):
objs = Content.objects.all().order_by('-created_at')
paginator = Paginator(objs, PAGENATOR_NUM)
page_number = request.GET.get('page')
context = {
'page_obj': paginator.get_page(page_number),
'page_number': page_number,
'title': 'ホーム',
}
return render(request, 'text_sns/index.html', context)
...
def connect_func(request, content_id):
author_id = Content.objects.get(pk=content_id).author_id
author = User.objects.get(pk=author_id)
user = request.user
if user == author:
messages.warning(request, '自分とはつながれません')
return redirect('text_sns:index')
connects = Connect.objects.filter(connect=user.id)
connected_id_list = [connect.connected.id for connect in connects]
if author_id not in connected_id_list:
connect = Connect.objects.create(connect=user, connected=author)
connect.save()
else:
messages.warning(request, '{}さんとは、すでにつながっています'.format(author.username))
connects = Connect.objects.filter(connect=user.id)
return render(request, 'text_sns/connects.html', context={'connects': connects})
def connects_func(request):
connects = Connect.objects.filter(connect=request.user.id)
return render(request, 'text_sns/connects.html', context={'connects': connects})
def connect_seeyou_func(request, connected_id):
connect = Connect.objects.filter(connect=request.user.id, connected=connected_id)
connect.delete()
connected = User.objects.get(pk=connected_id)
messages.success(request, '{}さんと、またねしました'.format(connected.username))
connects = Connect.objects.filter(connect=request.user.id)
return render(request, 'text_sns/connects.html', context = {'connects': connects})
HTML を作成していきます。
{% extends 'text_sns/base.html' %}
{% block main_content %}
<div class="justify-content-center">
<p class="h3 p-4 text-center">みこと(Mikoto) SNS</p>
<hr>
<p class="text-center">{% include 'snippets/message.html' %}</p>
{% for connect in connects %}
<p class="text-center">
{{ connect.connected.id }}:{{ connect.connected.username }}:
<a href="{% url 'text_sns:connect_seeyou' connect.connected.id %}">またねする</a>
</p>
{% endfor %}
<hr>
<p class="d-flex justify-content-center">
<a href="{% url 'text_sns:index' %}">ホーム</a>
リンクをはります。
...
<a class="navbar-brand fs-6" href="{% url 'text_sns:check_contents' %}">チェック一覧</a>
<a class="navbar-brand fs-6" href="{% url 'text_sns:connects' %}">つながり</a>
...
{% for obj in page_obj %}
<p class="text-muted small m-1">
「{{ obj.title }}」 {{ obj.author }}({{ obj.created_at }})<br>
<a href="{% url 'text_sns:connect' obj.id %}">つながる</a>
<a href="{% url 'text_sns:check' obj.id %}">チェック</a>
...
マイグレートします。
python manage.py makemigrations
python manage.py migrate
動作確認
ユーザ「てすと」で、「あにまる」とつながります。

ホーム画面に、「あにまる」も表示されるようになります。

つながるリンクをクリックすると、つながり一覧へ遷移します。

またねすると、つがながりが解除されます。

ホーム画面の表示も、元に戻ります。

4. いいね(非同期)
いいねリンクは非同期(JavaScript)で実装、クリックするたびにオンオフする仕様にします。
Udemy のレクチャーを参考にして、実装させてもらいました。(後述)
urls.py、models.py、views.py、html、js の順でファイルを作成します。
...
urlpatterns = [
...
path('good/<int:content_id>/', views.good_func, name='good'),
]
モデルを作成します。
...
class Good(models.Model):
content = models.ForeignKey(Content, on_delete=models.CASCADE)
reader = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
ビューを作成します。
...
from django.http import JsonResponse
...
from django.views.decorators.csrf import ensure_csrf_cookie
...
from .models import Content, Check, Comment, Connect, Good
...
@ensure_csrf_cookie
def good_func(request, content_id):
d = {"message": "error"}
if request.method == 'POST':
reader = request.user
content = Content.objects.get(pk=content_id)
exists = Good.objects.filter(content=content, reader=reader).exists()
if not exists:
good = Good.objects.create(content=content, reader=reader)
good.save()
d = {"message": "create"}
else:
good = Good.objects.get(content=content, reader=reader)
good.delete()
d = {"message": "delete"}
return JsonResponse(d)
JavaScript ファイルを作成して、読み込むやり方で実装します。
...
{% block main_content %}
{% load static %}
<script src="{% static 'text_sns/js/script.js' %}"></script>
<div class="flex justify-content-center align-items-center">
...
<p class="border border-2 p-1 rounded-3">
{{ obj.content | linebreaksbr | urlize }}
</p>
<p class="text-muted small">
<span id="obj_id" hidden>{{ obj.id }}</span>
<button id="good_btn" type="button" class="btn btn-light p-1" style="float: right;">
<span class="small">いいね: </span>
<span id="good_count" class="small">{{ obj.good_set.all.count }}</span>
</button>
<a href="{% url 'text_sns:comment' obj.id %}">コメントする</a>
...
window.addEventListener("DOMContentLoaded", (event) => {
var likeBtn = document.getElementById("good_btn");
likeBtn.addEventListener('click', (event) => {
var obj_id = document.getElementById("obj_id").innerText;
var url = `/text_sns/good/${obj_id}/`; // Django と連携
var csrfToken = getCookie("csrftoken");
var data = {};
fetch(url, {
method: "POST",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
body: JSON.stringify(data),
})
.then(Response => Response.json())
.then(data => { // 受け取ったデータ
if (data["message"] === "create") {
var likeCount = document.getElementById("good_count");
likeCount.innerText = (Number(likeCount.innerText)+1).toString();
} else if (data["message"] === "delete") {
var likeCount = document.getElementById("good_count");
likeCount.innerText = (Number(likeCount.innerText)-1).toString();
}
})
.catch((error) => {
console.log(error);
});
}, false);
}, false);
function getCookie(name) {
if (document.cookie && document.cookie !== "") {
for (var cookie of document.cookie.split(';')) {
var [key, value] = cookie.trim().split("=");
if (key === name) {
return decodeURIComponent(value);
}
}
}
return null;
}
マイグレートします。
python manage.py makemigrations
python manage.py migrate
動作確認します。

いいねをクリックすると、非同期(画面全体が更新されない)で無事に1増加しました。

もう一度クリックすると、ちゃんと非同期で1減りました。

5. 検索
urls.py、forms.py、views.py、html の順にファイルを作成します。
...
urlpatterns = [
...
path('search/', views.search_func, name='search'),
]
フォームを作成します。
...
class SearchForm(forms.Form):
keyword = forms.CharField(label='キーワード', max_length=32)
ビューを作成します。
名前、タイトル、内容のすべてで検索できるようにします。
QuerySet: https://docs.djangoproject.com/ja/5.2/ref/models/querysets/
...
from django.db.models import Q
...
from .forms import SignupForm, LoginForm, PostsForm, CommentForm, SearchForm
...
def search_func(request):
if request.method == 'POST':
searchForm = SearchForm(request.POST)
if searchForm.is_valid():
keyword = searchForm.cleaned_data['keyword']
object_list = Content.objects.filter(Q(author__username__icontains=keyword) |
Q(title__icontains=keyword) |
Q(content__icontains=keyword))
paginator = Paginator(object_list, PAGENATOR_NUM)
page_number = request.GET.get('page')
context = {
'page_obj': paginator.get_page(page_number),
'page_number': page_number,
'title': f'キーワード: {keyword} 検索結果',
}
return render(request, 'text_sns/index.html', context)
else:
messages.error('検索内容を変更してください')
searchForm = SearchForm()
return render(request, 'text_sns/search.html', context)
else:
searchForm = SearchForm()
context={'form': searchForm}
return render(request, 'text_sns/search.html', context)
HTML ファイルを作成します。
{% extends 'text_sns/input.html' %}
{% block sub_content %}
{% csrf_token %}
<div>
{% for item in form %}
{{ item.label }}:<br>
{{ item }}<br>
{% endfor %}
</div>
<br>
<button class="w-100 btn btn-light" type="submit">検索</button>
{% endblock %}
...
<a class="navbar-brand fs-6" href="{% url 'text_sns:connects' %}">つながり</a>
<a class="navbar-brand fs-6" href="{% url 'text_sns:search' %}">検索</a>
...
動作確認します。
「あにまる」で検索します。

今回は、名前のみですが、無事に検索できました。

6. 今回のプロジェクト
GitHub: https://github.com/Animalyzm/mikoto_project
今回のプロジェクトは、django/sns です。
(Git のコミット・メッセージは django_sns_second です)
データベースなどは削除してるので、使用するにはマイグレートが必要になります。
python manage.py makemigrations
python manage.py migrate
7. 「いいね」実装で参考にさせてもらったレクチャー
taku(最終更新月2022年11月)、【Python】 Django3で「本当に使える」WEBアプリケーションを開発しよう、Udemy レクチャー
(追記:管理画面でモデルを扱うためのコード)
from django.contrib import admin
from .models import Content, Check, Comment, Connect, Good
admin.site.register(Content)
admin.site.register(Check)
admin.site.register(Comment)
admin.site.register(Connect)
admin.site.register(Good)
2回にわたり、テキスト SNS を実装してきましたー。
Django で実装したので、少人数からある程度の規模まで、使える感じにはなったんではないかと思います。(実用したわけじゃないんで、あれですが)
おつきあい、ありがとうございましたー♪
Discussion