Evgenii Legotckoi
Evgenii Legotckoi30 грудня 2017 р. 10:50

Django - Підручник 029. Додавання приватних повідомлень і чатів на сайт - Частина 1

За традицією, що склалася, розповім про свої досвіди з впровадження нового функціоналу на сайті. На даний момент цим функціоналом є особисті повідомлення користувача. Звичайно, це зараз працює не так добре, як у відомих соціальних мережах... але все буде працювати. Головне фідбек на форумі , будь ласка.

Отже. Дуже хотілося додати особисті повідомлення на сайті, тим більше, що я вже сказав про це півроку тому. Залишалося питання, як це взагалі реалізувати. При пошуку інтернету вдалося натрапити на варіант, коли формується наступна модель даних.

  • Id повідомлення
  • from_user - відправник
  • to_user - одержувач
  • pub_date - дата повідомлення
  • 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 р. 07: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 р. 07:44

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

apps.py

from django.apps import AppConfig
 
 
class UsersConfig(AppConfig):
    name = 'users'
settings.py
INSTALLED_APPS = [
    'users.apps.UsersConfig',
]
M
  • 26 березня 2018 р. 08:22

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

from django.apps import AppConfig


class MessagesConfig(AppConfig):
    name = 'Messages'
у вас есть отдельно статья по созданию 'user.apps.UsersConfig'?
Evgenii Legotckoi
  • 26 березня 2018 р. 08: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 р. 09:13

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

Evgenii Legotckoi
  • 26 березня 2018 р. 09: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
  • 01 квітня 2018 р. 14:23

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

Evgenii Legotckoi
  • 01 квітня 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 р. 02: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 р. 02:52

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

B
  • 16 лютого 2020 р. 13:36

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

Evgenii Legotckoi
  • 17 лютого 2020 р. 03:22

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

  • myapp/
    • templatetags/
      • myapp.py
N
  • 11 травня 2020 р. 07:01

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

N
  • 11 травня 2020 р. 07: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
  • 05 лютого 2021 р. 01: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.

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up
г
  • ги
  • 23 квітня 2024 р. 15:51

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

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

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

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

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

  • Результат:50бали,
  • Рейтинг балів-4
Останні коментарі
k
kmssr08 лютого 2024 р. 18:43
Qt Linux - Урок 001. Автозапуск програми Qt під Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко05 лютого 2024 р. 01:50
Qt WinAPI - Урок 007. Робота з ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 грудня 2023 р. 10:30
Boost - статичне зв&#39;язування в проекті CMake під Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 грудня 2023 р. 08:38
Boost - статичне зв&#39;язування в проекті CMake під Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik18 грудня 2023 р. 21:01
Qt/C++ - Урок 056. Підключення бібліотеки Boost в Qt для компіляторів MinGW і MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Тепер обговоріть на форумі
G
Gar22 квітня 2024 р. 05:46
Clipboard Как скопировать окно целиком в clipb?
DA
Dr Gangil Academics20 квітня 2024 р. 07: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 р. 06:41
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Евгений, добрый день! Такой вопрос. Верно ли следующее утверждение: Любое Android-приложение, написанное на Java/Kotlin чисто теоретически (пусть и с большими трудностями) можно написать и на C+…
Павел Дорофеев
Павел Дорофеев14 квітня 2024 р. 02:35
QTableWidget с 2 заголовками Вот тут есть кастомный QTableView с многорядностью проект поддерживается, обращайтесь
f
fastrex04 квітня 2024 р. 04:47
Вернуть старое поведение QComboBox, не менять индекс при resetModel Добрый день! У нас много проектов в которых используется QComboBox, в версии 5.5.1, когда модель испускает сигнал resetModel, currentIndex не менялся. В версии 5.15 при resetModel происходит try…

Слідкуйте за нами в соціальних мережах