Evgenii Legotckoi
Evgenii LegotckoiDec. 30, 2017, 9:50 p.m.

Django - Tutorial 029. Adding private messages and chats on the site - Part 1

According to the tradition, I will tell you about my experiences on the introduction of new functionality on the site. At the moment, this functionality is a personal message between users. Of course, this now does not work as well as in well-known social networks ... but in the end everything will work. The main feedback on the forum, please.

So. I really wanted to add personal messages on the site, especially since I already mentioned this six months ago. The question remained, how to do it. When searching on the Internet it was possible to stumble upon an option when the following data model is being formed.

  • Id of the message
  • from_user - sender
  • to_user - recipient
  • pub_date - the date of the message
  • message - message content

Tried to implement this option, but I was stopped by the fact that suddenly after personal messages I want to make chats? So why not immediately lay the foundation for chats?


Chat and Message models

This would be an excellent option for further development of the resource. But in this case, you need to create two models of Chat and 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=_("Member"))

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


class Message(models.Model):
    chat = models.ForeignKey(Chat, verbose_name=_("Chat"))
    author = models.ForeignKey(User, verbose_name=_("User"))
    message = models.TextField(_("Message"))
    pub_date = models.DateTimeField(_('Message date'), default=timezone.now)
    is_readed = models.BooleanField(_('Readed'), default=False)

    class Meta:
        ordering=['pub_date']

    def __str__(self):
        return self.message

If the Message model should be clear. There is a text message, status, whether it was read, the author of the message and the chat in which it was sent, as well as the date the message was sent.

That's the model of Chat somewhat more difficult to be. First, chats can be the spirit of species. The first is a personal conversation between two people. The second kind is a collective chat. Why was this done? This was necessary in order to simplify the search for the desired chat, when sending a message to the user from his personal page. Unfortunately at the moment, only such a way is done to send the first message to the user. That is, go to his page and click the button to write a message. In the future, you can find the user from the dialog page on your personal page. This is still a temporary limitation within the first User Story, which I wrote myself.

Each chat has many-to-many connection, which is responsible for the list of participants within the chat. Due to this, you can limit the viewing of chats, as well as the ability to write to chats, if the user was not invited to this chat room.

urls.py

What kind of views will we need and what routes can be done? To implement this functionality in the simplest form, I had to write three views and three routes to urls.py respectively.

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'),

We will not go deep into which particular application we will use these routes - this is not a matter of principle.

In fact, there are three views, one for the list of dialogues for the user, another for creating a dialog from the page of another user, and the third for the messages in the dialog.

Message form

In this case, you can use the form for the model. We indicate the model, as well as which fields should be displayed with the label removed from the input field.

By default, you will have the usual textarea. I use my own WYSIWYG editor.

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

Views and Templates

Display a list of dialogs

To get a list of all the dialogs in which the user is involved, you need to filter all chats by participants, that is, by the 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})

In this case, I have a template for dialogs, in which I send an authorized user and a chat list. An active user will be needed to correctly display the messages read and not read in the dialog list.

The most interesting is the list of dialogs and its layout, so only the part responsible for the layout will be represented from the template.

<div class="panel">
        {% load tz %}
        {% if chats.count == 0 %}
            <div class="panel panel-body">{% trans "There is not a single dialogue started" %}</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>

You can see my custom inline tag in the template. This tag is responsible for returning the interlocutor in the dialog in order to build on the information about the interlocutor an adequate layout for the list of dialogues and indication of the read and unread messages in the dialog list.

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

Thus, the list of dialogs will look like this:

Current dialog and messages

To display the current dialog and messages, you will need more complex logic. The fact is that here the access to the chat is carried out by ID, but it is necessary to make not only an attempt to get a dialogue, but also to check if there is a user in the list of participants who is trying to get into this chat room. If it does not exist in the list of participants, then access to this chat room is forbidden to him. Among other things, the same view treats the sending of messages and the marking of messages read.

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}))

Message list template

{% if not chat %}
    <div class="panel panel-body">
        {% trans "It's impossible to start a conversation. No user is found or you do not have access to this conversation." %}
    </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 %}

Template of the message itself

{% 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>

Example of the resulting dialog

Starting a conversation with the user

At the moment, only one method is implemented to start a conversation with the user. It is necessary to go to the user's page and click on the "Write a message" button, then a link will be sent through the link in which the chat will be created or an already existing chat with this user is found. This is used to check whether the chat is a dialogue or a conversation between several users. This makes it possible to simplify the search for the necessary dialog.

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}))

Be sure to check for the number of users, since there can be only two users in a personal conversation, well, we check that these users are our authorized users, and also the user with whom we are trying to start a conversation.

After the chat is created, we redirect to the message page.

For Django I recommend VDS-server of Timeweb hoster .

We recommend hosting TIMEWEB
We recommend hosting TIMEWEB
Stable hosting, on which the social network EVILEG is located. For projects on Django we recommend VDS hosting.

Do you like it? Share on social networks!

M
  • March 26, 2018, 5:34 p.m.

Сделал всё в точности как написано, но возникает ошибка, возможно надо было что то импортировать во 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
  • March 26, 2018, 5:44 p.m.

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

apps.py

from django.apps import AppConfig
 
 
class UsersConfig(AppConfig):
    name = 'users'
settings.py
INSTALLED_APPS = [
    'users.apps.UsersConfig',
]
M
  • March 26, 2018, 6:22 p.m.

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

from django.apps import AppConfig


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

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

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

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

Evgenii Legotckoi
  • March 26, 2018, 7:20 p.m.

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

M
  • March 26, 2018, 8:20 p.m.

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

Evgenii Legotckoi
  • March 26, 2018, 8:26 p.m.

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

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

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

M
  • March 27, 2018, 9:56 p.m.

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

Evgenii Legotckoi
  • March 27, 2018, 10:18 p.m.

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

y
  • April 2, 2018, 12:23 a.m.

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

Evgenii Legotckoi
  • April 2, 2018, 12:58 a.m.

Добрый день!

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

спасибо!

l
  • Jan. 10, 2019, 11:47 p.m.

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
  • Jan. 11, 2019, 1:24 p.m.

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
  • Nov. 20, 2019, 3 a.m.

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

Evgenii Legotckoi
  • Nov. 20, 2019, 1:52 p.m.

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

B
  • Feb. 17, 2020, 12:36 a.m.

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

Evgenii Legotckoi
  • Feb. 17, 2020, 2:22 p.m.

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

  • myapp/
    • templatetags/
      • myapp.py
N
  • May 11, 2020, 5:01 p.m.

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

N
  • May 11, 2020, 5:07 p.m.

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

Evgenii Legotckoi
  • May 11, 2020, 8:44 p.m.

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

p
  • Feb. 5, 2021, 12:20 p.m.
  • (edited)

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.

Comments

Only authorized users can post comments.
Please, Log in or Sign up
m

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

  • Result:85points,
  • Rating points6
в

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

  • Result:50points,
  • Rating points-4
l

C++ - Test 005. Structures and Classes

  • Result:91points,
  • Rating points8
Last comments
k
kmssrFeb. 8, 2024, 6:43 p.m.
Qt Linux - Lesson 001. Autorun Qt application under Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
Qt WinAPI - Lesson 007. Working with ICMP Ping in Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVADec. 25, 2023, 10:30 a.m.
Boost - static linking in CMake project under Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJoDec. 25, 2023, 8:38 a.m.
Boost - static linking in CMake project under Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
GvozdikDec. 18, 2023, 9:01 p.m.
Qt/C++ - Lesson 056. Connecting the Boost library in Qt for MinGW and MSVC compilers Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Now discuss on the forum
AC
Alexandru CodreanuJan. 19, 2024, 11:57 a.m.
QML Обнулить значения SpinBox Доброго времени суток, не могу разобраться с обнулением значение SpinBox находящего в делегате. import QtQuickimport QtQuick.ControlsWindow { width: 640 height: 480 visible: tr…
BlinCT
BlinCTDec. 27, 2023, 8:57 a.m.
Растягивать Image на парент по высоте Ну и само собою дял включения scrollbar надо чтобы был Flickable. Так что выходит как то так Flickable{ id: root anchors.fill: parent clip: true property url linkFile p…
Дмитрий
ДмитрийJan. 10, 2024, 4:18 a.m.
Qt Creator загружает всю оперативную память Проблема решена. Удалось разобраться с помощью утилиты strace. Запустил ее: strace ./qtcreator Начал выводиться весь лог работы креатора. В один момент он начал считывать фай…
Evgenii Legotckoi
Evgenii LegotckoiDec. 12, 2023, 6:48 a.m.
Побуквенное сравнение двух строк Добрый день. Там случайно не высылается этот сигнал textChanged ещё и при форматировани текста? Если решиать в лоб, то можно просто отключать сигнал/слотовое соединение внутри слота и …

Follow us in social networks