🌏

Python、Django でプロトタイプ・テキスト SNS を実装(後半)

に公開

後半、いきまっす!

今回やること(後半)

  1. チェック、一覧表示、解除
  2. コメント投稿、削除
  3. つながる、一覧表示、またね
  4. いいね(非同期)
  5. 検索
  6. 今回のコード
  7. 「いいね」を参考にさせてもらったレクチャー

1. チェック、一覧表示、解除

urls.py、models.py、views.py、html の順でファイルを作ります。
チェック(お氣にいり)用と、チェック一覧表示用のパスを指定します。

text_sns/urls.py
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 は、現在アクティブなユーザ・モデルを返します。

text_sns/models.py
...
class Check(models.Model):
    content = models.ForeignKey(Content, on_delete=models.CASCADE)
    reader = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)

チェックは、リンクをクリックするたびにオン・オフする簡易仕様にします。
また、チェックしたときは、チェック一覧に遷移するようにします。

text_sns/views.py
...
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)

前回忘れていた全件表示とチェック一覧、チェックするリンクをはります。

index.html
...
  {% 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 の順にファイルを作ります。

text_sns/urls.py
...
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)に紐づけます。

models.py
...
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]
...

コメント入力フォームを作り、入力表示を調整します。

text_sns/forms.py
from .models import Content, Comment
...
class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = '__all__'
        widgets = {
            'comment': forms.Textarea(attrs={'rows': 3, 'cols': 40}),
        }

ビューを作成します。

text_sns/views.py
...
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" で入力しています。

text_sns/comment.html
{% 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 %}
text_sns/index.html
      <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'),
]

モデルを作成します。

text_sns/models.py
...
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}'

ビューを作成します。
インデックスに、つながった人の投稿も表示します。
前回、忘れていたので全件表示にもページネーションを実装します。

text_sns/views.py
...
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 を作成していきます。

text_sns/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>

リンクをはります。

index.html
...
      <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 の順でファイルを作成します。

text_sns/urls.py
...
urlpatterns = [
    ...
    path('good/<int:content_id>/', views.good_func, name='good'),
]

モデルを作成します。

text_sns/models.py
...
class Good(models.Model):
    content = models.ForeignKey(Content, on_delete=models.CASCADE)
    reader = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)

ビューを作成します。

text_sns/views.py
...
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>&nbsp;
...
static/text_sns/js/script.js
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 の順にファイルを作成します。

text_sns/urls.py
...
urlpatterns = [
    ...
    path('search/', views.search_func, name='search'),
]

フォームを作成します。

text_sns/forms.py
...
class SearchForm(forms.Form):
    keyword = forms.CharField(label='キーワード', max_length=32)

ビューを作成します。
名前、タイトル、内容のすべてで検索できるようにします。
QuerySet: https://docs.djangoproject.com/ja/5.2/ref/models/querysets/

text_sns/views.py
...
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 ファイルを作成します。

text_sns/search.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 %}
text_sns/index.html
...
      <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 レクチャー


(追記:管理画面でモデルを扱うためのコード)

text_sns/admin.py
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