Реализована полная система объявлений с уникальной бизнес-логикой
Основные возможности:
- Только 1 объявление на пользователя (database constraint)
- Автогенерация названия 'Объявления от {имя}'
- Только создатель может публиковать сообщения
- Админы могут блокировать/разблокировать объявления
- Заблокированные объявления скрыты от всех (кроме staff)
- Запрет на ответы (reply) в объявлениях (can_reply=False)
- Запрет на редактирование/удаление сообщений
- Уведомления всем участникам при новом объявлении
Детали реализации:
- Добавлены поля: is_blocked, blocked_at, blocked_by, can_reply
- API endpoints: /api/announcement/<id>/block/ и /unblock/
- Валидация на уровне views, API и WebSocket
- Новый тип уведомлений: announcement_new_message
- UI индикаторы блокировки и прав доступа
- Компактное отображение статуса 'только просмотр'
Затронуты файлы:
- models.py: новые поля и constraint
- api_views.py: валидация и API блокировки
- consumers.py: проверки прав в WebSocket
- views.py: фильтрация и контекст прав
- templates: UI блокировки и статусов
- notification_signals.py: уведомления для объявлений
This commit is contained in:
parent
f329feb6a2
commit
6f1e477fe8
@ -70,12 +70,42 @@ def create_chat(request):
|
||||
},
|
||||
status=400
|
||||
)
|
||||
|
||||
# Валидация для announcement - только 1 на пользователя
|
||||
if chat_type == 'announcement':
|
||||
existing = Chat.objects.filter(
|
||||
type='announcement',
|
||||
created_by=request.user
|
||||
).first()
|
||||
if existing:
|
||||
if existing.is_blocked:
|
||||
return JsonResponse(
|
||||
{
|
||||
'ok': False,
|
||||
'error': 'Ваш чат объявлений заблокирован администратором'
|
||||
},
|
||||
status=403
|
||||
)
|
||||
else:
|
||||
return JsonResponse(
|
||||
{
|
||||
'ok': False,
|
||||
'error': 'У вас уже есть чат объявлений',
|
||||
'existing_chat_id': existing.id
|
||||
},
|
||||
status=400
|
||||
)
|
||||
|
||||
# Валидация названия
|
||||
if not name and chat_type != 'private':
|
||||
return JsonResponse(
|
||||
{'ok': False, 'error': 'Name is required'}, status=400
|
||||
)
|
||||
# Для announcement генерируем название автоматически
|
||||
if chat_type == 'announcement':
|
||||
user_name = request.user.get_full_name() or request.user.username
|
||||
name = f"Объявления от {user_name}"
|
||||
else:
|
||||
return JsonResponse(
|
||||
{'ok': False, 'error': 'Name is required'}, status=400
|
||||
)
|
||||
|
||||
# Валидация для чата отдела
|
||||
if chat_type == 'department':
|
||||
@ -131,15 +161,14 @@ def create_chat(request):
|
||||
created_by=request.user,
|
||||
department=department,
|
||||
include_all_employees=include_all,
|
||||
is_main=is_main
|
||||
is_main=is_main,
|
||||
can_reply=(chat_type != 'announcement') # Для announcement нельзя
|
||||
)
|
||||
|
||||
|
||||
# Добавляем аватар если был загружен
|
||||
if avatar:
|
||||
chat.avatar = avatar
|
||||
chat.save()
|
||||
|
||||
# Создатель - владелец с полными правами
|
||||
chat.save() # Создатель - владелец с полными правами
|
||||
ChatMembership.objects.create(
|
||||
chat=chat,
|
||||
user=request.user,
|
||||
@ -173,7 +202,14 @@ def create_chat(request):
|
||||
id=request.user.id
|
||||
)
|
||||
for employee in all_employees:
|
||||
can_send = chat_type not in ['channel', 'announcement']
|
||||
# Для announcement и channel - обычные участники не могут писать
|
||||
if chat_type == 'announcement':
|
||||
can_send = False # Только создатель может писать
|
||||
elif chat_type == 'channel':
|
||||
can_send = False # Только модераторы могут писать
|
||||
else:
|
||||
can_send = True
|
||||
|
||||
ChatMembership.objects.get_or_create(
|
||||
chat=chat,
|
||||
user=employee,
|
||||
@ -210,6 +246,65 @@ def create_chat(request):
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def block_announcement(request, chat_id):
|
||||
"""
|
||||
Блокирует announcement чат (только для staff/superuser)
|
||||
После блокировки чат скрывается у всех пользователей
|
||||
"""
|
||||
if not (request.user.is_staff or request.user.is_superuser):
|
||||
return JsonResponse(
|
||||
{'ok': False, 'error': 'Permission denied'}, status=403
|
||||
)
|
||||
|
||||
chat = get_object_or_404(Chat, pk=chat_id, type='announcement')
|
||||
|
||||
if chat.is_blocked:
|
||||
return JsonResponse(
|
||||
{'ok': False, 'error': 'Chat already blocked'}, status=400
|
||||
)
|
||||
|
||||
chat.is_blocked = True
|
||||
chat.blocked_at = timezone.now()
|
||||
chat.blocked_by = request.user
|
||||
chat.save()
|
||||
|
||||
return JsonResponse({
|
||||
'ok': True,
|
||||
'message': f'Announcement "{chat.name}" blocked'
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def unblock_announcement(request, chat_id):
|
||||
"""
|
||||
Разблокирует announcement чат (только для staff/superuser)
|
||||
"""
|
||||
if not (request.user.is_staff or request.user.is_superuser):
|
||||
return JsonResponse(
|
||||
{'ok': False, 'error': 'Permission denied'}, status=403
|
||||
)
|
||||
|
||||
chat = get_object_or_404(Chat, pk=chat_id, type='announcement')
|
||||
|
||||
if not chat.is_blocked:
|
||||
return JsonResponse(
|
||||
{'ok': False, 'error': 'Chat is not blocked'}, status=400
|
||||
)
|
||||
|
||||
chat.is_blocked = False
|
||||
chat.blocked_at = None
|
||||
chat.blocked_by = None
|
||||
chat.save()
|
||||
|
||||
return JsonResponse({
|
||||
'ok': True,
|
||||
'message': f'Announcement "{chat.name}" unblocked'
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def update_chat(request, chat_id):
|
||||
|
||||
@ -165,9 +165,36 @@ class ChatConsumer(AsyncJsonWebsocketConsumer):
|
||||
if not text:
|
||||
return
|
||||
|
||||
# Проверка на ответ
|
||||
# Проверка прав на отправку сообщений
|
||||
if self.chat.type == "announcement":
|
||||
# Только создатель может писать в объявление
|
||||
if self.chat.created_by_id != user.id:
|
||||
await self.send_json({
|
||||
"type": "error",
|
||||
"error": "Только автор может публиковать в это объявление"
|
||||
})
|
||||
return
|
||||
else:
|
||||
# В других типах проверяем can_send_messages
|
||||
has_permission = await self._check_send_permission(user)
|
||||
if not has_permission:
|
||||
await self.send_json({
|
||||
"type": "error",
|
||||
"error": "У вас нет прав для отправки сообщений"
|
||||
})
|
||||
return
|
||||
|
||||
# Проверка на ответ (reply)
|
||||
reply_to_id = content.get("reply_to_id")
|
||||
|
||||
# Запрет на reply в чатах где can_reply=False
|
||||
if reply_to_id and not self.chat.can_reply:
|
||||
await self.send_json({
|
||||
"type": "error",
|
||||
"error": "В этом чате запрещены ответы на сообщения"
|
||||
})
|
||||
return
|
||||
|
||||
msg = await self._create_message(self.chat, user, text, reply_to_id)
|
||||
await self._mark_read(self.chat, user)
|
||||
|
||||
@ -188,6 +215,14 @@ class ChatConsumer(AsyncJsonWebsocketConsumer):
|
||||
if not message_id or not new_content:
|
||||
return
|
||||
|
||||
# Запрет на редактирование в объявлениях
|
||||
if self.chat.type == "announcement":
|
||||
await self.send_json({
|
||||
"type": "error",
|
||||
"error": "Редактирование запрещено в объявлениях"
|
||||
})
|
||||
return
|
||||
|
||||
success = await self._edit_message(message_id, user, new_content)
|
||||
if success:
|
||||
msg = await self._get_message(message_id)
|
||||
@ -208,6 +243,14 @@ class ChatConsumer(AsyncJsonWebsocketConsumer):
|
||||
if not message_id:
|
||||
return
|
||||
|
||||
# Запрет на удаление в объявлениях
|
||||
if self.chat.type == "announcement":
|
||||
await self.send_json({
|
||||
"type": "error",
|
||||
"error": "Удаление запрещено в объявлениях"
|
||||
})
|
||||
return
|
||||
|
||||
success = await self._delete_message(message_id, user)
|
||||
if success:
|
||||
await self.channel_layer.group_send(
|
||||
@ -348,6 +391,17 @@ class ChatConsumer(AsyncJsonWebsocketConsumer):
|
||||
.get(pk=chat_id)
|
||||
)
|
||||
|
||||
@database_sync_to_async
|
||||
def _check_send_permission(self, user) -> bool:
|
||||
"""Проверка права на отправку сообщений"""
|
||||
from .models import ChatMembership
|
||||
membership = ChatMembership.objects.filter(
|
||||
chat=self.chat, user=user
|
||||
).first()
|
||||
if membership:
|
||||
return membership.can_send_messages
|
||||
return True # По умолчанию можно писать
|
||||
|
||||
@database_sync_to_async
|
||||
def _user_can_access(self, chat: Chat, user) -> bool:
|
||||
if chat.type == "global":
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
# Generated by Django 5.2.4 on 2025-11-30 08:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('communications', '0014_remove_messengermessage_sender_account_and_more'),
|
||||
('employees', '0034_alter_employee_telegram'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='chat',
|
||||
name='blocked_at',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата блокировки'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chat',
|
||||
name='blocked_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='blocked_chats', to=settings.AUTH_USER_MODEL, verbose_name='Кто заблокировал'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chat',
|
||||
name='can_reply',
|
||||
field=models.BooleanField(default=True, help_text='False для announcement (только реакции)', verbose_name='Можно отвечать'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='chat',
|
||||
name='is_blocked',
|
||||
field=models.BooleanField(default=False, help_text='Для announcement: заблокирован администратором, скрыт у всех', verbose_name='Заблокирован'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='chat',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('type', 'announcement')), fields=('type', 'created_by'), name='unique_announcement_per_user'),
|
||||
),
|
||||
]
|
||||
@ -142,6 +142,31 @@ class Chat(models.Model):
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан")
|
||||
is_main = models.BooleanField(default=False, verbose_name="Основной чат")
|
||||
|
||||
# Новые поля для announcement и channel
|
||||
is_blocked = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name="Заблокирован",
|
||||
help_text="Для announcement: заблокирован администратором, скрыт у всех"
|
||||
)
|
||||
blocked_at = models.DateTimeField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Дата блокировки"
|
||||
)
|
||||
blocked_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='blocked_chats',
|
||||
verbose_name="Кто заблокировал"
|
||||
)
|
||||
can_reply = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name="Можно отвечать",
|
||||
help_text="False для announcement (только реакции)"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Чат"
|
||||
@ -159,6 +184,12 @@ class Chat(models.Model):
|
||||
condition=Q(is_main=True, type="department"),
|
||||
name="unique_main_department_chat",
|
||||
),
|
||||
# Только 1 announcement на сотрудника
|
||||
models.UniqueConstraint(
|
||||
fields=["type", "created_by"],
|
||||
condition=Q(type="announcement"),
|
||||
name="unique_announcement_per_user",
|
||||
),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(fields=["type", "is_main"]),
|
||||
|
||||
@ -98,6 +98,7 @@ def create_message_notifications(sender, instance, created, **kwargs):
|
||||
|
||||
# Определяем, кому отправлять обычные уведомления
|
||||
# Для приватных чатов - всегда отправляем
|
||||
# Для announcement - отправляем ВСЕМ участникам (это важные объявления)
|
||||
# Для групповых - можно добавить логику (например, только онлайн или с настройками)
|
||||
if chat.type == 'private':
|
||||
recipients_for_new_message = participants.exclude(id__in=excluded_ids)
|
||||
@ -117,6 +118,26 @@ def create_message_notifications(sender, instance, created, **kwargs):
|
||||
'author_id': author.id,
|
||||
}
|
||||
)
|
||||
elif chat.type == 'announcement':
|
||||
# Для объявлений отправляем ВСЕМ участникам
|
||||
recipients_for_announcement = participants.exclude(id__in=excluded_ids)
|
||||
|
||||
for recipient in recipients_for_announcement:
|
||||
NotificationService.create_notification(
|
||||
recipient=recipient,
|
||||
notification_type_code='announcement_new_message',
|
||||
title=f'Новое объявление от {author.get_full_name() or author.username}',
|
||||
message=truncate_message(content, 150),
|
||||
content_object=instance,
|
||||
action_url=f'/communications/chats/{chat.id}/',
|
||||
metadata={
|
||||
'chat_id': chat.id,
|
||||
'chat_name': get_chat_name(chat),
|
||||
'message_id': instance.id,
|
||||
'author_id': author.id,
|
||||
'is_announcement': True,
|
||||
}
|
||||
)
|
||||
elif chat.type in ['group', 'department', 'channel']:
|
||||
# Для групповых чатов - отправляем уведомление только если есть упоминание или ответ
|
||||
# Обычные сообщения не создают уведомления, чтобы не спамить
|
||||
|
||||
@ -40,6 +40,16 @@ urlpatterns = [
|
||||
api_views.set_chat_notifications,
|
||||
name="api_chat_notifications"
|
||||
),
|
||||
path(
|
||||
"api/announcement/<int:chat_id>/block/",
|
||||
api_views.block_announcement,
|
||||
name="api_block_announcement"
|
||||
),
|
||||
path(
|
||||
"api/announcement/<int:chat_id>/unblock/",
|
||||
api_views.unblock_announcement,
|
||||
name="api_unblock_announcement"
|
||||
),
|
||||
|
||||
# Message actions
|
||||
path(
|
||||
|
||||
@ -140,8 +140,19 @@ class ChatListView(LoginRequiredMixin, ListView):
|
||||
| Q(type="private", participants=user)
|
||||
| Q(id__in=membership_chat_ids) # group/channel/announcement
|
||||
)
|
||||
.exclude(
|
||||
# Скрываем заблокированные announcement
|
||||
Q(type="announcement", is_blocked=True) &
|
||||
~Q(created_by=user) # Создателю показываем
|
||||
)
|
||||
.exclude(
|
||||
# Скрываем заблокированные announcement и для создателя
|
||||
# если он не staff (чтобы staff мог разблокировать)
|
||||
Q(type="announcement", is_blocked=True, created_by=user) &
|
||||
Q(created_by__is_staff=False)
|
||||
)
|
||||
.distinct()
|
||||
.select_related("department")
|
||||
.select_related("department", "created_by", "blocked_by")
|
||||
.annotate(last_msg_at=Subquery(last_qs))
|
||||
.annotate(
|
||||
last_read_at=Coalesce(
|
||||
@ -331,37 +342,72 @@ class ChatDetailView(LoginRequiredMixin, DetailView, FormView):
|
||||
else:
|
||||
context["unread_from_ts"] = None
|
||||
|
||||
# Права на отправку сообщений в объявлениях
|
||||
if chat.type == "announcement":
|
||||
# Только создатель может писать в объявление
|
||||
context["can_send_messages"] = (chat.created_by == user)
|
||||
context["is_announcement_creator"] = (chat.created_by == user)
|
||||
else:
|
||||
# В других типах чатов проверяем can_send_messages
|
||||
membership = ChatMembership.objects.filter(
|
||||
chat=chat, user=user
|
||||
).first()
|
||||
context["can_send_messages"] = (
|
||||
membership.can_send_messages if membership
|
||||
else True # По умолчанию можно писать
|
||||
)
|
||||
context["is_announcement_creator"] = False
|
||||
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
user = request.user
|
||||
chat = self.object
|
||||
|
||||
if not ChatDetailView._user_has_access(self, self.object, user):
|
||||
if not ChatDetailView._user_has_access(self, chat, user):
|
||||
from django.http import HttpResponseForbidden
|
||||
|
||||
return HttpResponseForbidden("forbidden")
|
||||
|
||||
# Проверка прав на отправку сообщений
|
||||
if chat.type == "announcement":
|
||||
# Только создатель может писать в объявление
|
||||
if chat.created_by != user:
|
||||
from django.http import HttpResponseForbidden
|
||||
return HttpResponseForbidden(
|
||||
"Только автор может публиковать в это объявление"
|
||||
)
|
||||
else:
|
||||
# В других типах чатов проверяем can_send_messages
|
||||
membership = ChatMembership.objects.filter(
|
||||
chat=chat, user=user
|
||||
).first()
|
||||
if membership and not membership.can_send_messages:
|
||||
from django.http import HttpResponseForbidden
|
||||
return HttpResponseForbidden(
|
||||
"У вас нет прав для отправки сообщений"
|
||||
)
|
||||
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
msg = form.save(commit=False)
|
||||
msg.chat = self.object
|
||||
msg.chat = chat
|
||||
msg.author = user
|
||||
msg.save()
|
||||
|
||||
# автору сразу «прочитано» — делаем безопасно
|
||||
ts = msg.created_at
|
||||
updated = ChatReadState.objects.filter(
|
||||
chat=self.object, user=user, last_read_at__lt=ts
|
||||
chat=chat, user=user, last_read_at__lt=ts
|
||||
).update(last_read_at=ts)
|
||||
if not updated:
|
||||
try:
|
||||
ChatReadState.objects.create(
|
||||
chat=self.object, user=user, last_read_at=ts
|
||||
chat=chat, user=user, last_read_at=ts
|
||||
)
|
||||
except IntegrityError:
|
||||
ChatReadState.objects.filter(
|
||||
chat=self.object, user=user, last_read_at__lt=ts
|
||||
chat=chat, user=user, last_read_at__lt=ts
|
||||
).update(last_read_at=ts)
|
||||
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
@ -65,6 +65,18 @@ class Command(BaseCommand):
|
||||
},
|
||||
'is_groupable': False,
|
||||
},
|
||||
{
|
||||
'code': 'announcement_new_message',
|
||||
'name': 'Новое объявление',
|
||||
'description': 'Опубликовано новое объявление',
|
||||
'priority': 'high',
|
||||
'default_channels': {
|
||||
'web': True,
|
||||
'email': False,
|
||||
'telegram': True
|
||||
},
|
||||
'is_groupable': False,
|
||||
},
|
||||
],
|
||||
},
|
||||
'documents': {
|
||||
|
||||
@ -18,6 +18,19 @@
|
||||
{% elif chat.type == 'department' %}
|
||||
<i class="bi-diagram-3 fs-2 text-primary"></i>
|
||||
<h2 class="title mb-0">Чат отдела: {{ chat.department.name }}</h2>
|
||||
{% elif chat.type == 'announcement' %}
|
||||
<i class="bi-megaphone fs-2 text-warning"></i>
|
||||
<h2 class="title mb-0">{{ chat.name|default:"Объявление" }}</h2>
|
||||
{% if chat.is_blocked %}
|
||||
<span class="badge bg-danger ms-2">
|
||||
<i class="bi-lock-fill"></i> Заблокировано
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if not is_announcement_creator %}
|
||||
<span class="badge bg-info ms-2">
|
||||
<i class="bi-eye"></i> Только просмотр
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<i class="bi-person fs-2 text-primary"></i>
|
||||
<h2 class="title mb-0">
|
||||
@ -31,7 +44,18 @@
|
||||
{% endfor %}
|
||||
</h2>
|
||||
{% endif %}
|
||||
<div class="ms-auto">
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
{% if chat.type == 'announcement' and user.is_staff %}
|
||||
{% if chat.is_blocked %}
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="unblockAnnouncement({{ chat.id }})">
|
||||
<i class="bi-unlock"></i> Разблокировать
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="blockAnnouncement({{ chat.id }})">
|
||||
<i class="bi-lock"></i> Заблокировать
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="{% url 'communications:chat_list' %}" class="btn btn-ghost">
|
||||
<i class="bi-arrow-left"></i> К чатам
|
||||
</a>
|
||||
@ -41,6 +65,35 @@
|
||||
<!-- Корпус чата -->
|
||||
<article class="card chat-box">
|
||||
|
||||
<!-- Предупреждение о блокировке -->
|
||||
{% if chat.is_blocked %}
|
||||
<div class="alert alert-danger m-3 mb-0" role="alert">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi-lock-fill fs-4 me-2"></i>
|
||||
<div>
|
||||
<strong>Это объявление заблокировано администратором</strong>
|
||||
<br>
|
||||
<small>
|
||||
Заблокировано: {{ chat.blocked_at|date:"d.m.Y в H:i" }}
|
||||
{% if chat.blocked_by %}
|
||||
пользователем <strong>{{ chat.blocked_by.get_full_name|default:chat.blocked_by.username }}</strong>
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Информация об объявлении -->
|
||||
{% if chat.type == 'announcement' and not is_announcement_creator %}
|
||||
<div class="alert alert-light border m-3 mb-0 py-2" role="alert">
|
||||
<small class="text-secondary">
|
||||
<i class="bi-info-circle me-1"></i>
|
||||
Объявление от <strong>{{ chat.created_by.get_full_name|default:chat.created_by.username }}</strong>
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Кнопка «вниз» -->
|
||||
<button id="scrollBtn" class="scroll-to-bottom" type="button" aria-label="Прокрутить вниз">
|
||||
<i class="bi-arrow-down"></i>
|
||||
@ -157,6 +210,7 @@
|
||||
|
||||
<!-- Композер -->
|
||||
<div class="chat-composer">
|
||||
{% if can_send_messages %}
|
||||
<div id="typing" class="small text-secondary ps-1 pt-1 d-none">Собеседник печатает…</div>
|
||||
<form id="chatForm"
|
||||
class="composer-wrap"
|
||||
@ -247,6 +301,18 @@
|
||||
<!-- Превью прикрепленных файлов -->
|
||||
<div id="attachmentPreview" class="attachment-preview d-none"></div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="text-center text-secondary py-3">
|
||||
<i class="bi-lock fs-4"></i>
|
||||
<p class="mb-0 mt-2">
|
||||
{% if chat.type == 'announcement' %}
|
||||
Только автор объявления может публиковать сообщения
|
||||
{% else %}
|
||||
У вас нет прав для отправки сообщений в этот чат
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@ -261,4 +327,58 @@
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
|
||||
<script type="module" src="{% static 'js/components/chatComposer.js' %}"></script>
|
||||
<script type="module" src="{% static 'js/components/chatHistoryLoader.js' %}"></script>
|
||||
|
||||
<script>
|
||||
// Блокировка объявления
|
||||
async function blockAnnouncement(chatId) {
|
||||
if (!confirm('Вы уверены, что хотите заблокировать это объявление? Оно исчезнет из списка чатов всех пользователей.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/communications/api/announcement/${chatId}/block/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.error || 'Ошибка блокировки');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error blocking announcement:', error);
|
||||
alert('Ошибка при блокировке объявления');
|
||||
}
|
||||
}
|
||||
|
||||
// Разблокировка объявления
|
||||
async function unblockAnnouncement(chatId) {
|
||||
try {
|
||||
const response = await fetch(`/communications/api/announcement/${chatId}/unblock/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.error || 'Ошибка разблокировки');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error unblocking announcement:', error);
|
||||
alert('Ошибка при разблокировке объявления');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -507,6 +507,11 @@
|
||||
<a href="{% url 'communications:chat_detail' chat.pk %}" class="card-title text-decoration-none stretched-link">
|
||||
{{ chat.name }}
|
||||
</a>
|
||||
{% if chat.is_blocked %}
|
||||
<span class="badge bg-danger">
|
||||
<i class="bi-lock-fill"></i> Заблокировано
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="badge bg-light text-dark border">
|
||||
<i class="bi-chat-left-text"></i> {{ chat.messages.count }}
|
||||
</span>
|
||||
@ -913,7 +918,15 @@
|
||||
window.location.href = `/communications/chats/${data.chat_id}/`;
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert('Ошибка: ' + (err.error || 'Неизвестная ошибка'));
|
||||
|
||||
// Если это ошибка с existing_chat_id - редиректим на существующий чат
|
||||
if(err.existing_chat_id) {
|
||||
if(confirm(err.error + '\n\nПерейти к вашему чату объявлений?')) {
|
||||
window.location.href = `/communications/chats/${err.existing_chat_id}/`;
|
||||
}
|
||||
} else {
|
||||
alert('Ошибка: ' + (err.error || 'Неизвестная ошибка'));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user