Corp/backend/feed/views.py

429 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django import forms
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.db.models import BooleanField, Count, Exists, F, OuterRef, Prefetch, Value
from django.http import HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from employees.models import Department, Employee, EmployeeDepartment
from .constants import TYPE_COMPANY, TYPE_DEPARTMENT, TYPE_EMPLOYEE
from .forms import CommentForm, PostForm
from .models import Comment, Post, PostLike
# ---------- Общие утилиты ----------
def _feed_queryset(base_qs, user):
"""
Базовый queryset для ленты:
- is_liked: отмечено ли текущим пользователем
- comments_count: количество комментариев
- last_comment_list: последний комментарий (to_attr)
"""
if user.is_authenticated:
is_liked_expr = Exists(PostLike.objects.filter(post=OuterRef("pk"), user=user))
else:
is_liked_expr = Value(False, output_field=BooleanField())
last_comment_qs = (
Comment.objects.select_related("author")
.order_by("-created_at")[:1]
)
return (
base_qs
.select_related("author", "department")
.annotate(
is_liked=is_liked_expr,
comments_count=Count("comments"),
)
.prefetch_related(
Prefetch("comments", queryset=last_comment_qs, to_attr="last_comment_list")
)
.order_by("-pinned", "-created_at")
)
def is_department_head(user, department: Department) -> bool:
return bool(user and (user.is_staff or (department and department.head_id == user.id)))
def can_edit_post(user, post: Post) -> bool:
if not user or not user.is_authenticated:
return False
if user.is_staff:
return True
if post.type == TYPE_COMPANY:
return False # только staff
if post.type == TYPE_DEPARTMENT:
return post.author_id == user.id or is_department_head(user, post.department)
return post.author_id == user.id # личная публикация
def back_url_for_post(post: Post, user) -> str:
if post.type == TYPE_COMPANY:
return reverse("feed:feed_list")
if post.type == TYPE_DEPARTMENT and post.department_id:
return reverse("employees:department_detail", args=[post.department_id])
return reverse(
"feed:employee_feed",
args=[user.pk if user and user.is_authenticated else post.author_id],
)
# ---------- Ленты ----------
def feed_list(request):
"""
Лента компании (доступна анонимам).
Возвращаем posts с аннотациями (is_liked, comments_count, last_comment_list).
"""
posts = _feed_queryset(Post.objects.filter(type=TYPE_COMPANY), request.user)
# «Новые сотрудники»: верхние 10% активных, но минимум 1 (как было)
active_employees = sorted(
(emp for emp in Employee.objects.all() if emp.is_actually_active),
key=lambda e: e.created_at,
reverse=True,
)
new_count = max(1, int(len(active_employees) * 0.1))
new_employees = Employee.get_active()[:new_count]
return render(
request,
"feed/feed_list.html",
{"posts": posts, "new_employees": new_employees},
)
@login_required
def department_feed(request, pk):
"""Историческая страница ленты отдела (основная — employees:department_detail)."""
department = get_object_or_404(Department, pk=pk)
posts = _feed_queryset(
Post.objects.filter(type=TYPE_DEPARTMENT, department=department),
request.user,
)
emp_links = (
EmployeeDepartment.objects.filter(department=department, is_active=True)
.select_related("employee")
.order_by("employee__last_name", "employee__first_name")
)
return render(
request,
"feed/feed_list.html",
{
"department": department,
"posts": posts,
"new_employees": getattr(department, "new_employees", []),
"emp_links": emp_links,
},
)
@login_required
def employee_feed(request, pk):
"""Лента конкретного сотрудника."""
employee = get_object_or_404(Employee, pk=pk)
posts = _feed_queryset(
Post.objects.filter(type=TYPE_EMPLOYEE, author=employee),
request.user,
)
return render(
request,
"feed/feed_list.html",
{"employee": employee, "posts": posts},
)
# ---------- Детальная + комментарии ----------
@login_required
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk)
comments = post.comments.select_related("author")
if request.method == "POST":
# инлайн-форма из ленты также постится сюда
text = request.POST.get("text")
if text:
comment_form = CommentForm(request.POST)
if comment_form.is_valid():
comment = comment_form.save(commit=False)
comment.author = request.user
comment.post = post
comment.save()
messages.success(request, "Комментарий добавлен!")
next_url = request.POST.get("next")
if next_url:
return redirect(next_url)
return redirect("feed:post_detail", pk=pk)
else:
comment_form = CommentForm(request.POST)
if comment_form.is_valid():
comment = comment_form.save(commit=False)
comment.author = request.user
comment.post = post
comment.save()
messages.success(request, "Комментарий добавлен!")
return redirect("feed:post_detail", pk=pk)
else:
comment_form = CommentForm()
return render(
request,
"feed/post_detail.html",
{"post": post, "comments": comments, "comment_form": comment_form},
)
@login_required
def comment_update(request, pk):
"""Редактирование комментария (автор или staff)."""
comment = get_object_or_404(Comment, pk=pk)
if not (request.user.is_staff or comment.author_id == request.user.id):
messages.error(request, "У вас нет прав для редактирования этого комментария.")
return redirect("feed:post_detail", pk=comment.post_id)
if request.method == "POST":
form = CommentForm(request.POST, instance=comment)
if form.is_valid():
form.save()
messages.success(request, "Комментарий обновлён.")
return redirect(f"{reverse('feed:post_detail', args=[comment.post_id])}#c{comment.pk}")
else:
form = CommentForm(instance=comment)
return render(
request,
"feed/comment_form.html",
{"form": form, "comment": comment, "post": comment.post},
)
@login_required
def comment_delete(request, pk):
"""Удаление комментария (автор или staff; подтверждение через GET-страницу)."""
comment = get_object_or_404(Comment, pk=pk)
if not (request.user.is_staff or comment.author_id == request.user.id):
messages.error(request, "У вас нет прав для удаления этого комментария.")
return redirect("feed:post_detail", pk=comment.post_id)
if request.method == "POST":
post_id = comment.post_id
comment.delete()
messages.success(request, "Комментарий удалён.")
return redirect("feed:post_detail", pk=post_id)
return render(
request,
"feed/comment_confirm_delete.html",
{"comment": comment, "post": comment.post},
)
# ---------- Создание/редактирование/удаление поста ----------
@login_required
def post_create(request):
"""
Универсальная форма создания поста.
?department=<id> -> пост отдела
?type=company -> новость компании
?type=employee -> личная публикация
По умолчанию — company.
"""
dept_id = request.GET.get("department")
force_type = request.GET.get("type")
department = get_object_or_404(Department, pk=dept_id) if dept_id else None
if department:
context_type = TYPE_DEPARTMENT
elif force_type in {TYPE_COMPANY, TYPE_EMPLOYEE}:
context_type = force_type
else:
context_type = TYPE_COMPANY
cancel_url = (
request.GET.get("next")
or (reverse("employees:department_detail", args=[department.pk]) if department else None)
or (reverse("feed:feed_list") if context_type == TYPE_COMPANY else reverse("feed:employee_feed", args=[request.user.pk]))
)
if request.method == "POST":
form = PostForm(request.POST, request.FILES)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
if context_type == TYPE_DEPARTMENT:
post.type = TYPE_DEPARTMENT
post.department = department
elif context_type == TYPE_COMPANY:
post.type = TYPE_COMPANY
post.department = None
else:
post.type = TYPE_EMPLOYEE
post.department = None
if post.type == TYPE_DEPARTMENT and not post.department:
messages.error(request, "Для новостей отдела отдел обязателен.")
return render(
request,
"feed/post_form.html",
{"form": form, "context_type": context_type, "department": department, "cancel_url": cancel_url},
)
post.save()
messages.success(request, "Публикация создана!")
return redirect(back_url_for_post(post, request.user))
else:
initial = {}
if context_type == TYPE_DEPARTMENT:
initial.update({"type": TYPE_DEPARTMENT, "department": department})
elif context_type == TYPE_COMPANY:
initial.update({"type": TYPE_COMPANY, "department": None})
else:
initial.update({"type": TYPE_EMPLOYEE})
form = PostForm(initial=initial)
# прячем лишние поля
if "type" in form.fields:
form.fields["type"].initial = initial["type"]
form.fields["type"].widget = forms.HiddenInput()
if context_type == TYPE_DEPARTMENT and "department" in form.fields:
form.fields["department"].initial = department.pk
form.fields["department"].widget = forms.HiddenInput()
else:
form.fields.pop("department", None)
return render(
request,
"feed/post_form.html",
{"form": form, "context_type": context_type, "department": department, "cancel_url": cancel_url},
)
@login_required
def post_update(request, pk):
post = get_object_or_404(Post, pk=pk)
if not can_edit_post(request.user, post):
messages.error(request, "Нет прав на редактирование этой публикации.")
return redirect("feed:post_detail", pk=pk)
cancel_url = request.GET.get("next") or back_url_for_post(post, request.user)
if request.method == "POST":
form = PostForm(request.POST, request.FILES, instance=post)
if form.is_valid():
edited = form.save(commit=False)
edited.type = post.type
edited.department = post.department
edited.author = post.author
edited.save()
messages.success(request, "Публикация обновлена!")
return redirect(back_url_for_post(post, request.user))
else:
form = PostForm(instance=post)
if "type" in form.fields:
form.fields["type"].widget = forms.HiddenInput()
if "department" in form.fields:
form.fields["department"].widget = forms.HiddenInput()
context_type = post.type
department = post.department if post.type == TYPE_DEPARTMENT else None
return render(
request,
"feed/post_form.html",
{"form": form, "context_type": context_type, "department": department, "cancel_url": cancel_url},
)
@login_required
def post_delete(request, pk):
post = get_object_or_404(Post, pk=pk)
if not can_edit_post(request.user, post):
messages.error(request, "Нет прав на удаление этой публикации.")
return redirect("feed:post_detail", pk=pk)
cancel_url = request.GET.get("next") or back_url_for_post(post, request.user)
if request.method == "POST":
back_url = back_url_for_post(post, request.user)
post.delete()
messages.success(request, "Публикация удалена.")
return redirect(back_url)
return render(
request,
"feed/post_confirm_delete.html",
{"post": post, "cancel_url": cancel_url},
)
@login_required
def pin_post(request, pk):
post = get_object_or_404(Post, pk=pk)
if post.type == TYPE_COMPANY and not request.user.is_staff:
messages.error(request, "Только администратор может закреплять новости компании.")
return redirect("feed:post_detail", pk=pk)
if post.type == TYPE_DEPARTMENT and not is_department_head(request.user, post.department):
messages.error(request, "Только руководитель отдела может закреплять новости отдела.")
return redirect("feed:post_detail", pk=pk)
post.pinned = not post.pinned
post.save()
messages.success(request, "Новость закреплена!" if post.pinned else "Новость откреплена!")
return redirect(back_url_for_post(post, request.user))
# ---------- Лайки ----------
@login_required
def toggle_like(request, pk):
"""
Переключение лайка. При AJAX/Fetch отдаёт JSON, иначе редиректит «назад».
Счётчик поддерживаем атомарно через F().
"""
if request.method != "POST":
return redirect("feed:post_detail", pk=pk)
post = get_object_or_404(Post, pk=pk)
with transaction.atomic():
like, created = PostLike.objects.select_for_update().get_or_create(
post=post, user=request.user
)
if created:
Post.objects.filter(pk=post.pk).update(likes_count=F("likes_count") + 1)
is_liked = True
else:
like.delete()
Post.objects.filter(pk=post.pk, likes_count__gt=0).update(
likes_count=F("likes_count") - 1
)
is_liked = False
post.refresh_from_db(fields=["likes_count"])
wants_json = (
"application/json" in request.headers.get("Accept", "")
or request.headers.get("X-Requested-With") == "XMLHttpRequest"
)
if wants_json:
return JsonResponse(
{"ok": True, "post_id": post.pk, "is_liked": is_liked, "likes": post.likes_count}
)
next_url = (
request.POST.get("next")
or request.META.get("HTTP_REFERER")
or reverse("feed:post_detail", args=[post.pk])
)
return HttpResponseRedirect(next_url)