見出し画像

Djangoでチャットアプリを開発するレシピ(Part2)

このレシピではDjangoで以下のようなチャットアプリを構築する方法を学ぶことができます。
Django、DRF(Django REST framework)、JavaScriptの構成で開発を行います。


ユーザ検索&追加機能

このレシピは全体で3つのパートで構成されています。

  1. Part1:認証機能の実装

  2. Part2:ユーザ検索と追加機能の実装

  3. Part3:チャット機能の実装

このPart2では、ユーザ検索と追加機能の実装まで行います。

Part1を完了していない方は以下のPart1から始めましょう。


1.ユーザ検索機能の実装

Part1ではチャット画面のTOPページの作成とメールアドレスとパスワードでログオンするカスタムユーザモデルの認証機能実装まで行いました。

Part2では、チャット対象ユーザの検索追加機能を実装していきます。
まず最初にユーザ検索機能の実装を行います。

事前準備

まず最初に、サンプルとなるユーザ情報を数名登録しておきましょう。
adminサイト(http://127.0.0.1:8000/admin)にログオンしてACCOUNTS->ユーザをクリック→「ユーザを追加」ボタンから3~4名ユーザを登録します。

ユーザ登録時は「ユーザ名、Email、パスワード、サムネイル画像」を指定しておきます。
サムネイル画像は何でもよいので好きな画像を利用してください。

ここでは以下の通り5名のユーザを登録しました。


ユーザ検索機能の実装

データベース上に登録されているユーザ一覧の表示と、友達登録しているユーザ一覧を表示する機能を追加します。

そのため、まずは友達情報を格納するFriendsテーブルクラスを定義します。

chat\models.pyに以下の通りクラスを定義します。

from django.db import models
from accounts.models import CustomUser


class Friends(models.Model):

    class Meta:
        verbose_name = '友達リスト'
        verbose_name_plural = '友達リスト'

    user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name = "user_friends")
    friend = models.ForeignKey(CustomUser, on_delete=models.CASCADE, verbose_name="友達", related_name = "friend_friends")

    def __str__(self):
        return f"{self.friend}"

Friendsクラスのカラム名として以下の2つを定義します。

あるユーザ(user)における友達をfriendフィールドとして表現します。

userとfriendで同じ外部テーブル(CustomUser)を参照するため、各フィールドに対してrelated_nameを設定しておき、CustomUser側から逆向きにFriendクラスのuserまたはfriendを参照できるようにします。

Friendsモデルを追加したので、一旦マイグレーションを行います。

python manage.py makemigrations

Migrations for 'chat':
  chat\migrations\0001_initial.py
    - Create model Friends
python manage.py migrate

Operations to perform:
  Apply all migrations: accounts, admin, auth, chat, contenttypes, sessions
Running migrations:
  Applying chat.0001_initial... OK


また、Friendsモデルをadminサイトに表示させるため、chat\admin.pyに以下のコードを追加します。

from django.contrib import admin

from .models import Friends

class FriendsAdmin(admin.ModelAdmin):
    list_display=('pk','user', 'friend',)


admin.site.register(Friends,FriendsAdmin)


次に、chat\views.pyに友達リスト情報を取得するためのgetFriendsList関数を定義します。

from accounts.models import CustomUser  #追加

def getFriendsList(username):
    """
    指定したユーザの友達リストを取得
    :param: ユーザ名
    :return: ユーザ名の友達リスト
    """
    try:
        user = CustomUser.objects.get(username=username)
        friends = list(user.user_friends.all())
        return friends
    except:
        return []

getFriendsList関数は、引数で受け取ったユーザ名(username)を元にCustomUserクラスから該当ユーザ情報を取得しuser変数に格納します。

user = CustomUser.objects.get(username=username)

その後、該当ユーザの友達情報を取得するため、逆参照でFriendsテーブルのデータを取得しfrineds変数にリスト型に変換して格納し、frinedsを戻り値として返す関数です。
友達が一人も登録されていない場合空のリスト「[]」を返します。

friends = list(user.user_friends.all())

Friendsモデルクラスのuserカラムでrelated_name = "user_friends"の様に設定していることで、
CustomUserクラス側からは以下のような書き方で逆参照することができます。

<参照元クラス>.<related_name>.all()

例えば、CustomUseruserが"siny"であった場合、逆参照によってFriendsクラスのuser列が"siny"であるすべてのレコード(=sinyユーザのすべての友達情報)を取得することができます。

説明だけでは理解できない場合、実際にコマンドを実行してみましょう。

まず、adminサイト上の「友達リスト」→「追加」からユーザ「siny」に対するユーザを複数登録しておきます。(対象ユーザはなんでもよいです)

以下の様に「user」欄は「siny」、「友達」欄に任意の友達を指定して保存します。

この状態で、DjangoのShellモードを起動して実際にsinyの友達リストを取得してみます。

 python manage.py shell

Python 3.8.3 (default, Jul  2 2020, 17:30:36) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

以下のコマンドを実行してユーザ名sinyの友達情報(Fruends)を全件取得します。

>>> from accounts.models import CustomUser
>>> user = CustomUser.objects.get(username="siny")
>>> friends = list(user.user_friends.all())
>>> for friend in friends:
...     print(friend.friend.username)
...
gold_tanaka
s_taka
sekishin
>>>

先ほどadminサイトで登録した3名のユーザが取得できていることが確認できます。

次にchat\urls.pyにユーザ検索用のURLパターンを追加します。

from django.urls import path
from chat.views import *

app_name = 'chat'

urlpatterns = [
    path('', Home.as_view(), name="home"),
    path('chat_room/', ChatRoom.as_view(), name="chat_room"),
    path("search/", SearchUser.as_view(), name="search_user"),  #追加
]

次に、chat\views.pyにユーザ情報を検索するSearchUserクラスを以下の様に定義します。

from django.shortcuts import render
from django.views.generic import TemplateView
from accounts.models import CustomUser
from django.views import View   #追加


class SearchUser(View):
    
    def get(self, request, *args, **kwargs):
        
        if 'search' in self.request.GET:
            query = request.GET.get("search")
            users = list(CustomUser.objects.all())
            user_list = []
            for user in users:
                #検索文字列を含むユーザ情報を取得(自分は除外)
                if query in user.username and user.username != request.user.username:
                    user_list.append(user)
        else:
            user_list = list(CustomUser.objects.all())  #全ユーザ一覧を取得
            for user in user_list:
                if user.username == request.user.username:
                    user_list.remove(user)  #自分のユーザだけ除外
                    break
        
        friends = getFriendsList(request.user.username)  #自分のフレンド一覧を取得
        return render(request, "chat/search.html", {'users': user_list, 'friends': friends})

今回はDjangoのクラスベースビューであるViewを継承してSearchUserクラスを定義します。

SearchUserクラス内にgetメソッドを定義します。

検索画面が表示された場合にデータベース上に登録されている全ユーザ情報を表示し、任意の文字列でユーザ検索処理を実行した場合は、該当文字列が含まれるユーザ情報だけ表示する処理をgetメソッド内に定義します。

getメソッドの内容についてみていきましょう。
まず前半部分です。

def get(self, request, *args, **kwargs):
    if 'search' in self.request.GET:
        query = request.GET.get("search")
        users = list(CustomUser.objects.all())
        user_list = []
        for user in users:
            #検索文字列を含むユーザ情報を取得(自分は除外)
            if query in user.username and user.username != request.user.username:
                user_list.append(user)

if 'search' in self.request.GETでGETリクエスト内にname属性=serachが存在していた場合の処理を定義しています。

検索画面から検索処理を実行した場合にこのif文が実行されます。

まず、CustomUserから全ユーザ情報を取得してリスト化してusers変数に格納します。

その後、forループでユーザ情報を1つ1つ取り出し、検索文字列(query)がユーザ名(user.username)に含まれる自分のユーザ名(request.user.username)以外の場合にuser_list変数に該当ユーザ名を格納するようにします。

これで、自分を除いて検索文字列を含む全ユーザ情報が取得できます。

次に後半部分です。

else:
    user_list = list(CustomUser.objects.all())  #全ユーザ一覧を取得
    for user in user_list:
        if user.username == request.user.username:
            user_list.remove(user)  #自分のユーザだけ除外
            break

このelseは検索画面が単純に表示された場合に実行されます。
まず、すべてのユーザ情報を取得した後に、自分のユーザだけ除外するようにしています。

最後にgetFriendsList関数の引数に自分のユーザ名(request.user.username)を与えて自分の友達一覧を取得します。
最後にserch.htmlusers(ユーザ情報)friends(自分の友達情報)を渡すようにします。

friends = getFriendsList(request.user.username)  #自分のフレンド一覧を取得
return render(request, "chat/search.html", {'users': user_list, 'friends': friends})

ビューの定義が終わったので、ユーザ検索画面のテンプレートtemplates\chat\search.htmlを以下の通り作成します。

{% extends './base.html' %}
{% load static %}
{% block title %}
<h3 class=" text-center">Django-Chat検索画面</h3>
{% endblock %}
{% block contents %}
<div class="mesgs">
    <div class="msg_history">
        <div class="wrapper" style="width: 400px;">
            <h4 style="margin: 16px 0px">ユーザを追加する</h4>
            <div class="topnav">
                <div class="search-container">
                    <form method="get">
                        {% csrf_token %}
                        <div class="input-group mb-3">
                            <input type="text" class="form-control-sm" placeholder="ユーザ名で検索" name="search"
                                saria-describedby="basic-addon2">
                            <button type="submit" class="btn btn-success ml-2">検索</button>
                        </div>
                    </form>
                </div>
            </div>
            <ul class="list-group list-group-flush mt-3">
                {% for user in users %}
                <a href="#" id="user_card">
                    <li class="list-group-item list-group-item-action">
                        <div class="row">
                            <div class="col">
                                <img src="#" style="width: 50px; height: 50px;">
                            </div>
                            <div class="col">
                                <div class="row user-add">
                                    <p>@{{user.username}}</p>
                                </div>
                            </div>
                        </div>
                    </li>
                </a>
                {% endfor %}
            </ul>
        </div>
    </div>
</div>
{% endblock %}

search.htmlbase.htmlを拡張して作成します。

{% extends './base.html' %}
{% load static %}

ユーザ検索フォームは以下のformタグ内で定義しています。

<form method="get">
    <div class="input-group mb-3">
        <input type="text" class="form-control-sm" placeholder="ユーザ名で検索" name="search"
            saria-describedby="basic-addon2">
        <button type="submit" class="btn btn-success ml-2">検索</button>
    </div>
</form>

今回はユーザ検索機能なので、フォームのメソッドはgetを定義しています。
name属性に"search"を設定しておくことで、先ほどSearchUserクラス内で定義した以下のコードでフォームに入力された文字列を取得できるようにしています。

query = request.GET.get("search")

検索結果のユーザ一覧を表示しているのが以下の部分です。

<ul class="list-group list-group-flush mt-3">
    {% for user in users %}
    <a href="#" id="user_card">
        <li class="list-group-item list-group-item-action">
            <div class="row">
                <div class="col">
                    <img src="#" style="width: 50px; height: 50px;">
                </div>
                <div class="col">
                    <div class="row user-add">
                        <p>@{{user.username}}</p>
                    </div>
                </div>
            </div>
        </li>
    </a>
    {% endfor %}
</ul>

SearchUserクラスビューから受け取ったユーザ一覧(users)をforループで回してユーザ名(user.username)を@{{user.username}}で表示させています。

以上でユーザ検索が可能になりました。
それでは動作確認をしてみましょう。

特定のユーザでログオンした状態でhttp://127.0.0.1:8000/search/にアクセスして以下のような画面が表示されればOKです。

以下の様に、ユーザ検索フォームで任意の文字列でユーザを検索すると指定した文字列を含むユーザだけが一覧表示されます。


画像ファイルの設定


現状では、上記デモ動画のようにユーザのサムネイルが表示されていませんので、サムネイルが表示されるように画像関連の設定を行います。

まず、config\settings.pyに以下の設定を追加します。

# ファイルアップロード用
MEDIA_URL = '/media/'

テンプレート内からメディアファイル(画像)を参照できるようにメディアファイルのURLパスのルートを上記のように定義します。

http://127.0.0.1:8000/media/がメディアファイルを参照するURLルートパスになります。

次にテンプレート内からメディアファイル(画像)を参照できるようにconfig\urls.pyに以下の設定を追加します。

from django.contrib import admin
from django.urls import path, include
from django.conf import settings  #追加
from django.conf.urls.static import static  #追加


urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('accounts.urls')),
    path('accounts/', include('django.contrib.auth.urls')),
    path('', include("chat.urls")),   
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)  #追加

最後にsearch.htmlを一部修正してサムネイルを表示するようにします。

変更前

<div class="col">
    <img src="#" style="width: 50px; height: 50px;">
</div>

変更後

<div class="col">
    <img src="{{ user.thumbnail.url }}" style="width: 50px; height: 50px;">
</div>

{{ user.thumbnail.url }}とすることでCustomUserクラスのthumbnailフィールドに指定された画像のURLのパスを得ることができます。

imgタグのsrc属性に{{ user.thumbnail.url }}を指定することで該当ユーザのサムネイル画像を表示させることができます。

画面を更新して以下の様にユーザのサムネイル画像が表示されるようになればOKです。

2.友達追加機能の実装

ユーザの検索機能が実装できたので、次は検索画面で表示されたユーザ一覧から会話したいユーザをクリックした際に画面左側のユーザ一覧にユーザが追加されるようにします。

ここから先は

6,372字 / 3画像
このマガジンはDjangoでリアルタイムチャットアプリを開発するチュートリアルノートです。 ※期間限定で100円配布中♪ 全体は以下の3つのノートで構成されています。 Part1:認証機能の実装 Part2:ユーザ検索と追加機能の実装 Part3:チャット機能の実装 Django、DRF(Django REST framework)、JavaScriptの構成で開発を行います。 DjangoとJavaScriptの基礎がある程度わかる人を前提にしています。

このマガジンはDjangoデリアリタイムチャットアプリを開発する方法を学べるチュートリアルノートです。 以下の3つの記事で構成されてい…

主にITテクノロジー系に興味があります。 【現在興味があるもの】 python、Django,統計学、機械学習、ディープラーニングなど。 技術系ブログもやってます。 https://sinyblog.com/