По сложившейся традиции расскажу о своих опытах по внедрению нового функционала на сайте. На данный момент этим функционалом являются личные сообщения между пользователями. Конечно, это сейчас работает не так хорошо, как в известных социальных сетях… но в итоге всё будет работать. Главное фидбек на форуме , пожалуйста.
Итак. Очень хотелось добавить личные сообщения на сайте, тем более, что я уже обмолвился об этом полгода назад. Оставался вопрос, как вообще это реализовать. При поиске по интернету удалось наткнуться на вариант, когда формируется следующая модель данных.
- 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}))
Обязательно делаем проверку на количество пользователей, поскольку в личной беседе может быть только два пользователя, ну и проверяем, чтобы этими пользователями был наш авторизованный пользователей, а также пользователь с которым мы пытаемся начать беседу.
После того, как чат создан, делаем переадресацию на страницу сообщений.
Статья взята с сайта: https://evileg.com