Evgenii Legotckoi
Evgenii Legotckoi30 декабря 2017 г. 10:50

Django - Урок 029. Добавление личных сообщений и чатов на сайте - Часть 1

По сложившейся традиции расскажу о своих опытах по внедрению нового функционала на сайте. На данный момент этим функционалом являются личные сообщения между пользователями. Конечно, это сейчас работает не так хорошо, как в известных социальных сетях... но в итоге всё будет работать. Главное фидбек на форуме , пожалуйста.

Итак. Очень хотелось добавить личные сообщения на сайте, тем более, что я уже обмолвился об этом полгода назад. Оставался вопрос, как вообще это реализовать. При поиске по интернету удалось наткнуться на вариант, когда формируется следующая модель данных.

  • Id сообщения
  • from_user - отправитель
  • to_user - получатель
  • pub_date - дата сообщения
  • message - контент сообщения

Попытался реализовать данный вариант, но меня остановило то, что вдруг после личных сообщений я захочу сделать чаты? Так почему бы сразу не заложить основу для чатов?


Модели Chat и Message

Это был бы отличный вариант для дальнейшего развития ресурса. Но в данном случае требуется создать две модели Chat и Message .

# -*- coding: utf-8 -*-

from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _


class Chat(models.Model):
    DIALOG = 'D'
    CHAT = 'C'
    CHAT_TYPE_CHOICES = (
        (DIALOG, _('Dialog')),
        (CHAT, _('Chat'))
    )

    type = models.CharField(
        _('Тип'),
        max_length=1,
        choices=CHAT_TYPE_CHOICES,
        default=DIALOG
    )
    members = models.ManyToManyField(User, verbose_name=_("Участник"))

    @models.permalink
    def get_absolute_url(self):
        return 'users:messages', (), {'chat_id': self.pk }


class Message(models.Model):
    chat = models.ForeignKey(Chat, verbose_name=_("Чат"))
    author = models.ForeignKey(User, verbose_name=_("Пользователь"))
    message = models.TextField(_("Сообщение"))
    pub_date = models.DateTimeField(_('Дата сообщения'), default=timezone.now)
    is_readed = models.BooleanField(_('Прочитано'), default=False)

    class Meta:
        ordering=['pub_date']

    def __str__(self):
        return self.message

Если с моделью Message всё должно быть понятно. Есть текст сообщения, статус, было ли оно прочитано, автор сообщения и чат в который оно было отправлено, а также дата отправки сообщения.

То вот модель Chat несколько посложнее будет. Во-первых, чаты могут быть дух видов. Первый - это личная беседа двух человек. Второй вид - это коллективный чат. Зачем так было сделано? Это было необходимо для того, чтобы упростить поиск нужного чата, при отправки сообщения пользователю с его личной страницы. К сожалению на данный момент сделан только такой способ отправить первое сообщение пользователю. То есть зайти на его страницу и кликнуть кнопку написать сообщение. В дальнейшем можно будет находить пользователя со страницы диалогов на вашей личной странице. Это пока временные ограничения в рамках первой User Story , которую я себе написал.

Каждый чат имеет many-to-many связь, которая отвечает за список участников в рамках чата. Благодаря этому можно ограничить просмотр чатов, а также возможность писать в чаты, если пользователь не был приглашён в данный чат.

urls.py

Какие вьюшки нам понадобятся и какие маршруты можно сделать? Мне на реализацию данного функционала в самом простом виде понадобилось написать три вьюшки и соответственно три маршрута в urls.py.

url(r'^dialogs/$', login_required(views.DialogsView.as_view()), name='dialogs'),
url(r'^dialogs/create/(?P<user_id>\d+)/$', login_required(views.CreateDialogView.as_view()), name='create_dialog'),
url(r'^dialogs/(?P<chat_id>\d+)/$', login_required(views.MessagesView.as_view()), name='messages'),

Не будем углубляться в каком именно приложении будем использовать данные маршруты - это не принципиально.

По факту здесь имеется три вьюшки, одна для списка диалогов у пользователя, другая для создания диалога со страницы другого пользователя, а третья для сообщений в диалоге.

Форма сообщения

В данном случае можно использовать форму для модели. Укажем модель, а также какие поля должны быть отображены с удалением лейбла у поля ввода.

По умолчанию у вас будет обычная textarea. Я использую свой самописный WYSIWYG редактор.

class MessageForm(ModelForm):
    class Meta:
        model = Message
        fields = ['message']
        labels = {'message': ""}

Представления и шаблоны

Отображение списка диалогов

Для получения списка всех диалогов, в которых задействован пользователь, необходимо провести фильтрацию всех чатов по участникам, то есть по Many-toMany полю members.

class DialogsView(View):
    def get(self, request):
        chats = Chat.objects.filter(members__in=[request.user.id])
        return render(request, 'users/dialogs.html', {'user_profile': request.user, 'chats': chats})

В данном случае у меня меня имеется шаблон для диалогов, в который я передаю авторизованного пользователя и список чатов. Активный пользователь будет нужен для корректного отображения прочитанных и не прочитанных сообщений в списке диалогов.

Наибольший интерес представляет сам список диалогов и его вёрстка, поэтому из шаблона здесь будет представлена только та часть, которая отвечает за вёрстку.

<div class="panel">
        {% load tz %}
        {% if chats.count == 0 %}
            <div class="panel panel-body">{% trans "Нет ни одного начатого диалога" %}</div>
        {% endif %}
        {% for chat in chats %}
            {% if chat.message_set.count != 0 %}
                {% with last_message=chat.message_set.last %}
                    {% get_companion user chat as companion %}
                    <a class="list-group-item {% if companion == last_message.author and not last_message.is_readed %}unreaded{% endif %}" href="{{ chat.get_absolute_url }}">
                        <img class="avatar-messages" src="{{ companion.userprofile.get_avatar }}">
                        <div class="reply-body">
                            <ul class="list-inline">
                                <li class="drop-left-padding">
                                    <strong class="list-group-item-heading">{{ companion.username }}</strong>
                                </li>
                                <li class="pull-right text-muted"><small>{{ last_message.pub_date|utc }}</small></li>
                            </ul>
                            {% if companion != last_message.author %}
                                <div>
                                    <img class="avatar-rounded-sm" src="{{ last_message.author.userprofile.get_avatar }}">
                                    <div class="attached-reply-body {% if not last_message.is_readed %}unreaded{% endif %}">{{ last_message.message|truncatechars_html:"200"|safe|striptags }}</div>
                                </div>
                            {% else %}
                                <div>{{ last_message.message|truncatechars_html:"200"|safe|striptags }}</div>
                            {% endif %}
                        </div>
                    </a>
                {% endwith %}
            {% endif %}
        {% endfor %}
    </div>

Вы можете заметить в шаблоне мой пользовательский встраиваемый тег. Данный тег отвечает за то, чтобы вернуть собеседника в диалоге, чтобы отталкиваясь от информации о собеседнике сформировать адекватную вёрстку для списка диалогов и указание прочитанных и непрочитанных сообщений в списке диалогов.

@register.simple_tag
def get_companion(user, chat):
    for u in chat.members.all():
        if u != user:
            return u
    return None

Таким образом список диалогов будет выглядеть следующим образом:

Текущий диалог и сообщения

Для отображения текущего диалога и сообщений уже потребуется более сложная логика. Дело в том, что здесь доступ к чату осуществляется по ID, но необходимо сделать не только попытку получения диалога, но и проверить, существует ли в списке участников пользователь, который пытается попасть в данный чат. Если он не существует в списке участников, то доступ в данный чат ему запрещён. Помимо прочего в этом же представлении обрабатывается отсылка сообщений и пометка сообщений прочитанными.

class MessagesView(View):
    def get(self, request, chat_id):
        try:
            chat = Chat.objects.get(id=chat_id)
            if request.user in chat.members.all():
                chat.message_set.filter(is_readed=False).exclude(author=request.user).update(is_readed=True)
            else:
                chat = None
        except Chat.DoesNotExist:
            chat = None

        return render(
            request,
            'users/messages.html',
            {
                'user_profile': request.user,
                'chat': chat,
                'form': MessageForm()
            }
        )

    def post(self, request, chat_id):
        form = MessageForm(data=request.POST)
        if form.is_valid():
            message = form.save(commit=False)
            message.chat_id = chat_id
            message.author = request.user
            message.save()
        return redirect(reverse('users:messages', kwargs={'chat_id': chat_id}))

Шаблон списка сообщений

{% if not chat %}
    <div class="panel panel-body">
        {% trans "Невозможно начать беседу. Не найден пользователь или вы не имеете доступа к данной беседе." %}
    </div>
{% else %}
    {% load tz %}
    {% if chat %}
        <div id="messages" class="panel">
            <div id="innerMessages">
                {% for message in chat.message_set.all %}
                        {% include 'users/message.html' with message_item=message %}
                {% endfor %}
            </div>
        </div>
    {% endif %}
    <div id="message_form">
        <form id="message-form" class="panel panel-body" method="post" >
            {% load bootstrap3 %}
            {% csrf_token %}
            {% bootstrap_form form %}
            <button type="submit" class="btn btn-default btn-sm" onclick="return ETextEditor.validateForm('message-form')"><span class="ico ico-comment"></span>{% trans "Отправить" %}</button>
        </form>
    </div>
{% endif %}

Шаблон самого сообщения

{% url 'users:profile' message_item.author.username as the_user_url%}
{% load tz %}
<div class="list-group-item {% if not message_item.is_readed %}unreaded{% endif %}">
    <a href="{{ the_user_url }}"><img class="avatar-comment" src="{{ message_item.author.userprofile.get_avatar }}"></a>
    <div class="reply-body">
        <ul class="list-inline">
            <li class="drop-left-padding">
                <strong class="list-group-item-heading"><a href="{{ the_user_url }}">{{ message_item.author.username }}</a></strong>
            </li>
            <li class="pull-right text-muted"><small>{{ message_item.pub_date|utc }}</small></li>
        </ul>
        <div>{{ message_item.message|safe }}</div>
    </div>
</div>

Пример получившегося диалога

Начало беседы с пользователем

На данный момент реализован лишь один метод для начала беседы с пользователем. Необходимо перейти на страницу пользователя и нажать на кнопку "Написать сообщение", тогда через ссылку будет отправлен запрос, в котором будет создан чат или найден уже существующий чат с этим пользователем. Здесь испольуется проверка на то, является ли чат диалогом или беседой нескольких пользователей. Что позволяет несколько упростить поиск необходимого диалога.

class CreateDialogView(View):
    def get(self, request, user_id):
        chats = Chat.objects.filter(members__in=[request.user.id, user_id], type=Chat.DIALOG).annotate(c=Count('members')).filter(c=2)
        if chats.count() == 0:
            chat = Chat.objects.create()
            chat.members.add(request.user)
            chat.members.add(user_id)
        else:
            chat = chats.first()
        return redirect(reverse('users:messages', kwargs={'chat_id': chat.id}))

Обязательно делаем проверку на количество пользователей, поскольку в личной беседе может быть только два пользователя, ну и проверяем, чтобы этими пользователями был наш авторизованный пользователей, а также пользователь с которым мы пытаемся начать беседу.

После того, как чат создан, делаем переадресацию на страницу сообщений.

Для Django рекомендую VDS-сервера хостера Timeweb .

Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

Вам это нравится? Поделитесь в социальных сетях!

M
  • 26 марта 2018 г. 7:34

Сделал всё в точности как написано, но возникает ошибка, возможно надо было что то импортировать во views.py(но это точно), мой первый проект на джанго, не кидайтесь тапками ). И так собственно ошибка:

NoReverseMatch at /messages/dialogs/

'users' is not a registered namespace
содержания views.py:(вот здесь думаю нне робит)
from django.shortcuts import render, redirect
from django.views import View
from .models import Chat
from .forms import MessageForm
from django.db.models import Count
from django.urls import reverse
from django.contrib import auth


# Create your views here.

class DialogsView(View):
    def get(self, request):
        chats = Chat.objects.filter(members__in=[request.user.id])
        return render(request, 'users/dialogs.html', {'user': request.user, 'chats': chats})

дальше всё как в статье
Evgenii Legotckoi
  • 26 марта 2018 г. 7:44

Скорее ошибка в том, что у вас App не зарегистрирован в проекте.

apps.py

from django.apps import AppConfig
 
 
class UsersConfig(AppConfig):
    name = 'users'
settings.py
INSTALLED_APPS = [
    'users.apps.UsersConfig',
]
M
  • 26 марта 2018 г. 8:22

Откуда вы взяли приложение users?
Я создал было для данной статьи отдельное приложение 'Messages', в нём есть файл app.py с содержанием:

from django.apps import AppConfig


class MessagesConfig(AppConfig):
    name = 'Messages'
у вас есть отдельно статья по созданию 'user.apps.UsersConfig'?
Evgenii Legotckoi
  • 26 марта 2018 г. 8:37

users - это приложение для отображения личного кабинета пользователей. И некоторые страницы отображаются в рамках этого приложения.
Полагаю, что в вашем случае нужно сменить users на Messages в ряде мест в шаблонах, например, эту строку

return redirect(reverse('users:messages', kwargs={'chat_id': chat.id}))
заменить на
return redirect(reverse('Messages:messages', kwargs={'chat_id': chat.id}))
Примерно так всё и поменять, отслеживая, чего не хватает.
Конкретно для личного кабинета пользователей статьи нет. Статья основана конкретно на коде данного сайта и является обобщённым примером основных моментов, что-то может быть опущено. Многие статьи ориентированы на наличие определённого опыта у программиста. Поэтому данные статьи и не являются полным руководством к повторению.
M
  • 26 марта 2018 г. 9:13

Ошибка не изменилась.

Evgenii Legotckoi
  • 26 марта 2018 г. 9:20

Ну а шаблоны? которые html. Там тоже есть {% url 'users:blablabla' %}

M
  • 26 марта 2018 г. 10:20

ну ошибка такая же но только не users а messeages найти не может

Evgenii Legotckoi
  • 26 марта 2018 г. 10:26

А вы когда создавали приложение для комментариев, вы его urls подключили в urls.py файле всего проекта?

Например, в этой статье есть два приложения: home и knowledge и их urls подключаются в urls.py файле всего проекта. Вы их подключали?
M
  • 27 марта 2018 г. 11:23

да, конечно, и в instaled_apps прописал как Messages.apps.MessagesConfig

M
  • 27 марта 2018 г. 11:56

и ещё, эта ссылка работает пока тебе или ты не напишешь сообщения
url(r'^dialogs/$', login_required(views.DialogsView.as_view()), name='dialogs'),

Evgenii Legotckoi
  • 27 марта 2018 г. 12:18

Внимательнее проанализируйте код из статьи и вывод ошибок, вы однозначно где-то накосячили. Вполне возможно, что вы не зарегистрировали пользовательский тег get_companion, Вы знаете как регистрировать пользовательские теги ?

y
  • 1 апреля 2018 г. 14:23

Добрый день, очень интересные статьи у Вас на сайте, касательно Django  и  Python. Когда планируете создать часть 2 Чата. Евгений, это ведь не чат в режиме реального времени? каждый раз приходится обновлять страницу. Не планируете использовать для чата channels ?

Evgenii Legotckoi
  • 1 апреля 2018 г. 14:58

Добрый день!

Спасибо за отзыв.
Всё планирую, а также планирую использовать channels, но пока не так много времени на внедрение всего функционала на сайте. Поэтому по срокам вообще ничего не могу сказать.
g
  • 14 мая 2018 г. 10:32

спасибо!

l
  • 10 января 2019 г. 12:47

Thank you for the article. Is there the github folder or the source code folder for this chat features, because for the code provided here sometimes I don't really know the exact place to put and make the project work.

Evgenii Legotckoi
  • 11 января 2019 г. 2:24

Hi, unfortunately - no. But You can ask your question on the forum of this site . I try answer on questions in all sections of forum.

D
  • 19 ноября 2019 г. 16:00

Привет! Пытаюсь освоить создание чата по вашей статье, сделал все как написано. Пока что результат такой (см. на прикрепленные скрины). Подскажите, пожалуйста, почему такой внешний вид и вдруг мб я чего не учел? Я так понял того, что вы выложили - недостаточно для полноценного чата и у меня получился тот максимум, котоырй можно выжать из вашего кода. Подскажите, как развить дальше, пожалуйста, ресурс какой-нибудь или из своего поделитесь! Спасибо большое за статью!

Evgenii Legotckoi
  • 20 ноября 2019 г. 2:52

Добрый день.
Вам нужно самостоятельно написать стили, то есть CSS, это здесь уже на усмотрение пользователей, то есть на ваше личное усмотрение. Изучайте CSS и делайте как хотите вёрстку.

B
  • 16 февраля 2020 г. 13:36

Добрый вечер! Монжно по подробней о теге get_companion? ссылка не работает.

Evgenii Legotckoi
  • 17 февраля 2020 г. 3:22

Добрый день. Это кастомный тег, помещается в файл, который находится в каталоге templatetags

  • myapp/
    • templatetags/
      • myapp.py
N
  • 11 мая 2020 г. 7:01

Добрый день. Подскажите пожалуйста как вы реализовали определение прочитано сообщение или нет.Спасибо

N
  • 11 мая 2020 г. 7:07

if request.user in chat.members.all():
chat.message_set.filter(is_readed=False).exclude(author=request.user).update(is_readed=True)
И можно подробнее эту строчку. Спасибо

Evgenii Legotckoi
  • 11 мая 2020 г. 10:44

А вот эта строчка как раз и отвечает за определение, прочитано сообщение или нет.
Данная строка фильтрует все непрочитанные сообщения, исключая сообщения, автором которых является текущий пользователь сессии. А потом делает все сообщения прочитанными. То есть если человек открывает диалог, то все входящие сообщения обновляются как прочитанные, а исходящие так и остаются непрочитанные до тех пор, пока другой участник диалога их не откроет диалог.

p
  • 5 февраля 2021 г. 1:20
  • (ред.)

Hi! could you please explain to me how to sort messages in the dialog by the pub_date of message not by the creation of the chat?
i mean that when someone sends me a message ,i want it to be on the top of messages in my dialog.

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
г
  • ги
  • 23 апреля 2024 г. 22:51

C++ - Тест 005. Структуры и Классы

  • Результат:41баллов,
  • Очки рейтинга-8
l
  • laei
  • 23 апреля 2024 г. 16:19

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:10баллов,
  • Очки рейтинга-10
l
  • laei
  • 23 апреля 2024 г. 16:17

C++ - Тест 003. Условия и циклы

  • Результат:50баллов,
  • Очки рейтинга-4
Последние комментарии
k
kmssr9 февраля 2024 г. 2:43
Qt Linux - Урок 001. Автозапуск Qt приложения под Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко5 февраля 2024 г. 9:50
Qt WinAPI - Урок 007. Работаем с ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 декабря 2023 г. 18:30
Boost - статическая линковка в CMake проекте под Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 декабря 2023 г. 16:38
Boost - статическая линковка в CMake проекте под Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik19 декабря 2023 г. 5:01
Qt/C++ - Урок 056. Подключение библиотеки Boost в Qt для компиляторов MinGW и MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Сейчас обсуждают на форуме
G
Gar22 апреля 2024 г. 12:46
Clipboard Как скопировать окно целиком в clipb?
DA
Dr Gangil Academics20 апреля 2024 г. 14:45
Unlock Your Aesthetic Potential: Explore MSC in Facial Aesthetics and Cosmetology in India Embark on a transformative journey with an msc in facial aesthetics and cosmetology in india . Delve into the intricate world of beauty and rejuvenation, guided by expert faculty and …
a
a_vlasov14 апреля 2024 г. 13:41
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Евгений, добрый день! Такой вопрос. Верно ли следующее утверждение: Любое Android-приложение, написанное на Java/Kotlin чисто теоретически (пусть и с большими трудностями) можно написать и на C+…
Павел Дорофеев
Павел Дорофеев14 апреля 2024 г. 9:35
QTableWidget с 2 заголовками Вот тут есть кастомный QTableView с многорядностью проект поддерживается, обращайтесь
f
fastrex4 апреля 2024 г. 11:47
Вернуть старое поведение QComboBox, не менять индекс при resetModel Добрый день! У нас много проектов в которых используется QComboBox, в версии 5.5.1, когда модель испускает сигнал resetModel, currentIndex не менялся. В версии 5.15 при resetModel происходит try…

Следите за нами в социальных сетях