Добавление личных сообщений и чатов на сайте — Часть 1

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

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

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

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


Модели Chat и Message

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

  1. # -*- coding: utf-8 -*-
  2.  
  3. from django.db import models
  4. from django.contrib.auth.models import User
  5. from django.utils import timezone
  6. from django.utils.translation import ugettext_lazy as _
  7.  
  8.  
  9. class Chat(models.Model):
  10. DIALOG = ‘D’
  11. CHAT = ‘C’
  12. CHAT_TYPE_CHOICES = (
  13. (DIALOG, _(‘Dialog’)),
  14. (CHAT, _(‘Chat’))
  15. )
  16.  
  17. type = models.CharField(
  18. _(‘Тип’),
  19. max_length=1,
  20. choices=CHAT_TYPE_CHOICES,
  21. default=DIALOG
  22. )
  23. members = models.ManyToManyField(User, verbose_name=_(«Участник»))
  24.  
  25. @models.permalink
  26. def get_absolute_url(self):
  27. return ‘users:messages’, (), {‘chat_id’: self.pk }
  28.  
  29.  
  30. class Message(models.Model):
  31. chat = models.ForeignKey(Chat, verbose_name=_(«Чат»))
  32. author = models.ForeignKey(User, verbose_name=_(«Пользователь»))
  33. message = models.TextField(_(«Сообщение»))
  34. pub_date = models.DateTimeField(_(‘Дата сообщения’), default=timezone.now)
  35. is_readed = models.BooleanField(_(‘Прочитано’), default=False)
  36.  
  37. class Meta:
  38. ordering=[‘pub_date’]
  39.  
  40. def __str__(self):
  41. return self.message

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

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

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

urls.py

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

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

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

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

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

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

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

  1. class MessageForm(ModelForm):
  2. class Meta:
  3. model = Message
  4. fields = [‘message’]
  5. labels = {‘message’: «»}

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

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

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

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

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

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

  1. <div class=«panel»>
  2. {% load tz %}
  3. {% if chats.count == 0 %}
  4. <div class=«panel panel-body»>{% trans «Нет ни одного начатого диалога» %}</div>
  5. {% endif %}
  6. {% for chat in chats %}
  7. {% if chat.message_set.count != 0 %}
  8. {% with last_message=chat.message_set.last %}
  9. {% get_companion user chat as companion %}
  10. <a class=«list-group-item {% if companion == last_message.author and not last_message.is_readed %}unreaded{% endif %}» href=«{{ chat.get_absolute_url }}»>
  11. <img class=«avatar-messages» src=«{{ companion.userprofile.get_avatar }}»>
  12. <div class=«reply-body»>
  13. <ul class=«list-inline»>
  14. <li class=«drop-left-padding»>
  15. <strong class=«list-group-item-heading»>{{ companion.username }}</strong>
  16. </li>
  17. <li class=«pull-right text-muted»><small>{{ last_message.pub_date|utc }}</small></li>
  18. </ul>
  19. {% if companion != last_message.author %}
  20. <div>
  21. <img class=«avatar-rounded-sm» src=«{{ last_message.author.userprofile.get_avatar }}»>
  22. <div class=«attached-reply-body {% if not last_message.is_readed %}unreaded{% endif %}»>{{ last_message.message|truncatechars_html:»200″|safe|striptags }}</div>
  23. </div>
  24. {% else %}
  25. <div>{{ last_message.message|truncatechars_html:»200″|safe|striptags }}</div>
  26. {% endif %}
  27. </div>
  28. </a>
  29. {% endwith %}
  30. {% endif %}
  31. {% endfor %}
  32. </div>

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

  1. @register.simple_tag
  2. def get_companion(user, chat):
  3. for u in chat.members.all():
  4. if u != user:
  5. return u
  6. return None

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

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

  1. class MessagesView(View):
  2. def get(self, request, chat_id):
  3. try:
  4. chat = Chat.objects.get(id=chat_id)
  5. if request.user in chat.members.all():
  6. chat.message_set.filter(is_readed=False).exclude(author=request.user).update(is_readed=True)
  7. else:
  8. chat = None
  9. except Chat.DoesNotExist:
  10. chat = None
  11.  
  12. return render(
  13. request,
  14. ‘users/messages.html’,
  15. {
  16. ‘user_profile’: request.user,
  17. ‘chat’: chat,
  18. ‘form’: MessageForm()
  19. }
  20. )
  21.  
  22. def post(self, request, chat_id):
  23. form = MessageForm(data=request.POST)
  24. if form.is_valid():
  25. message = form.save(commit=False)
  26. message.chat_id = chat_id
  27. message.author = request.user
  28. message.save()
  29. return redirect(reverse(‘users:messages’, kwargs={‘chat_id’: chat_id}))

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

  1. {% if not chat %}
  2. <div class=«panel panel-body»>
  3. {% trans «Невозможно начать беседу. Не найден пользователь или вы не имеете доступа к данной беседе.» %}
  4. </div>
  5. {% else %}
  6. {% load tz %}
  7. {% if chat %}
  8. <div id=«messages» class=«panel»>
  9. <div id=«innerMessages»>
  10. {% for message in chat.message_set.all %}
  11. {% include ‘users/message.html’ with message_item=message %}
  12. {% endfor %}
  13. </div>
  14. </div>
  15. {% endif %}
  16. <div id=«message_form»>
  17. <form id=«message-form» class=«panel panel-body» method=«post» >
  18. {% load bootstrap3 %}
  19. {% csrf_token %}
  20. {% bootstrap_form form %}
  21. <button type=«submit» class=«btn btn-default btn-sm» onclick=«return ETextEditor.validateForm(‘message-form’)»><span class=«ico ico-comment»></span>{% trans «Отправить» %}</button>
  22. </form>
  23. </div>
  24. {% endif %}

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

  1. {% url ‘users:profile’ message_item.author.username as the_user_url%}
  2. {% load tz %}
  3. <div class=«list-group-item {% if not message_item.is_readed %}unreaded{% endif %}»>
  4. <a href=«{{ the_user_url }}»><img class=«avatar-comment» src=«{{ message_item.author.userprofile.get_avatar }}»></a>
  5. <div class=«reply-body»>
  6. <ul class=«list-inline»>
  7. <li class=«drop-left-padding»>
  8. <strong class=«list-group-item-heading»><a href=«{{ the_user_url }}»>{{ message_item.author.username }}</a></strong>
  9. </li>
  10. <li class=«pull-right text-muted»><small>{{ message_item.pub_date|utc }}</small></li>
  11. </ul>
  12. <div>{{ message_item.message|safe }}</div>
  13. </div>
  14. </div>

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

 

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

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

  1. class CreateDialogView(View):
  2. def get(self, request, user_id):
  3. chats = Chat.objects.filter(members__in=[request.user.id, user_id], type=Chat.DIALOG).annotate(c=Count(‘members’)).filter(c=2)
  4. if chats.count() == 0:
  5. chat = Chat.objects.create()
  6. chat.members.add(request.user)
  7. chat.members.add(user_id)
  8. else:
  9. chat = chats.first()
  10. return redirect(reverse(‘users:messages’, kwargs={‘chat_id’: chat.id}))

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

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

Статья взята с сайта: https://evileg.com

Добавить комментарий