From 5dce32c303a3b7ab7050c1f33fbf1026edd433e8 Mon Sep 17 00:00:00 2001 From: lizerk Date: Tue, 10 Mar 2026 13:07:05 +0300 Subject: [PATCH] =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D0=B8=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BB=D0=B8=D1=88=D0=BD=D0=B5=D0=B3=D0=BE=20=D0=B8=D0=B7=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BE=D1=81=D1=82=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/feed/forms.py | 25 ---- backend/feed/forms_front.py | 42 ------ backend/feed/tests.py | 3 - backend/feed/urls_front.py | 14 -- backend/feed/views_front.py | 252 ------------------------------------ 5 files changed, 336 deletions(-) delete mode 100644 backend/feed/forms.py delete mode 100644 backend/feed/forms_front.py delete mode 100644 backend/feed/tests.py delete mode 100644 backend/feed/urls_front.py delete mode 100644 backend/feed/views_front.py diff --git a/backend/feed/forms.py b/backend/feed/forms.py deleted file mode 100644 index f48049a6..00000000 --- a/backend/feed/forms.py +++ /dev/null @@ -1,25 +0,0 @@ -from django import forms -from .models import Post, Comment - - -class PostForm(forms.ModelForm): - class Meta: - model = Post - fields = ['type', 'department', 'title', 'body', 'image', 'attachment'] - widgets = { - 'type': forms.Select(attrs={'class': 'form-select'}), - 'department': forms.Select(attrs={'class': 'form-select'}), - 'title': forms.TextInput(attrs={'class': 'form-control'}), - 'body': forms.Textarea(attrs={'class': 'form-control', 'rows': 5}), - 'image': forms.ClearableFileInput(attrs={'class': 'form-control'}), - 'attachment': forms.ClearableFileInput(attrs={'class': 'form-control'}), - } - - -class CommentForm(forms.ModelForm): - class Meta: - model = Comment - fields = ['text'] - widgets = { - 'text': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), - } diff --git a/backend/feed/forms_front.py b/backend/feed/forms_front.py deleted file mode 100644 index 701a1d7d..00000000 --- a/backend/feed/forms_front.py +++ /dev/null @@ -1,42 +0,0 @@ -from django import forms - -TYPE_COMPANY = "company" -TYPE_DEPARTMENT = "department" - - -class LoginForm(forms.Form): - email = forms.EmailField(label="Email") - password = forms.CharField(label="Пароль", widget=forms.PasswordInput) - - -class PostCreateForm(forms.Form): - type = forms.ChoiceField( - choices=[(TYPE_COMPANY, "Главная (компания)"), (TYPE_DEPARTMENT, "Отдел")] - ) - department = forms.IntegerField( - required=False, min_value=1, label="Отдел (для новостей отдела)" - ) - title = forms.CharField(max_length=200, label="Заголовок") - body = forms.CharField(widget=forms.Textarea, label="Текст") - image = forms.ImageField(required=False, label="Изображение") - attachment = forms.FileField(required=False, label="Вложение") - - -class CommentForm(forms.Form): - post = forms.IntegerField(widget=forms.HiddenInput) - text = forms.CharField( - required=False, widget=forms.Textarea(attrs={"rows": 2}), label="Комментарий" - ) - image = forms.ImageField(required=False, label="Изображение") - attachment = forms.FileField(required=False, label="Файл") - - def clean(self): - cleaned = super().clean() - has_any = bool( - (cleaned.get("text") or "").strip() - or cleaned.get("image") - or cleaned.get("attachment") - ) - if not has_any: - raise forms.ValidationError("Нужно указать текст, изображение или файл.") - return cleaned diff --git a/backend/feed/tests.py b/backend/feed/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/backend/feed/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/backend/feed/urls_front.py b/backend/feed/urls_front.py deleted file mode 100644 index 4d510835..00000000 --- a/backend/feed/urls_front.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.urls import path - -from . import views_front - -app_name = "feed" - -urlpatterns = [ - path("", views_front.feed_list, name="feed_list"), - path("departments//", views_front.department_feed, name="department_feed"), - path("employees//", views_front.employee_feed, name="employee_feed"), - path("post//", views_front.post_detail, name="post_detail"), - path("post//pin/", views_front.pin_post, name="pin_post"), - path("post//like/", views_front.toggle_like, name="toggle_like"), -] diff --git a/backend/feed/views_front.py b/backend/feed/views_front.py deleted file mode 100644 index 40e1387f..00000000 --- a/backend/feed/views_front.py +++ /dev/null @@ -1,252 +0,0 @@ -# backend/feed/views.py -from __future__ import annotations - -from datetime import timedelta - -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.http import HttpResponseRedirect, JsonResponse -from django.shortcuts import redirect, render -from django.urls import reverse -from django.utils import timezone - -from api.client import ApiResponse, get_api_client -from .constants import TYPE_COMPANY, TYPE_DEPARTMENT -from .forms import CommentForm - -# ---------- Константы путей API ---------- -API_POSTS = "v1/posts/" -API_COMMENTS = "v1/comments/" -API_DEPARTMENTS = "v1/departments/" -API_EMPLOYEES = "v1/employees/" - - -# ---------- Вспомогательное: доступ к ApiClient ---------- -def _api(request): - return get_api_client(request) - - -def _paged_or_list(resp: ApiResponse): - """Извлекает results из пагинированного ответа или возвращает сырой список.""" - data = resp.json or {} - return data.get("results", data) - - -def _extract_page_data(resp: ApiResponse): - """Извлекает данные пагинации из ответа API. - - Returns: - tuple: (items, count, next_url, prev_url) - """ - data = resp.json or {} - if isinstance(data, dict) and "results" in data: - return ( - data["results"], - data.get("count", 0), - data.get("next"), - data.get("previous"), - ) - return data if isinstance(data, list) else [], 0, None, None - - -# ---------- Хелперы API для ленты ---------- -def _api_list_posts(request, **params) -> list[dict]: - resp = _api(request).get(API_POSTS, params=params) - if not resp.ok: - messages.error(request, f"Не удалось получить публикации: {resp.status}") - return [] - return _paged_or_list(resp) - - -def _api_get_post(request, post_id: int) -> dict | None: - resp = _api(request).get(f"{API_POSTS}{post_id}/") - if not resp.ok: - messages.error(request, f"Публикация недоступна: {resp.status}") - return None - return resp.json or None - - -def _api_get_comments_for_post(request, post_id: int) -> list[dict]: - resp = _api(request).get( - API_COMMENTS, params={"post": post_id, "ordering": "created_at"} - ) - if not resp.ok: - messages.error(request, f"Не удалось получить комментарии: {resp.status}") - return [] - return _paged_or_list(resp) - - -def _api_get_department(request, dept_id: int) -> dict | None: - resp = _api(request).get(f"{API_DEPARTMENTS}{dept_id}/") - return resp.json if resp.ok else None - - -def _api_get_employee(request, employee_id: int) -> dict | None: - resp = _api(request).get(f"{API_EMPLOYEES}{employee_id}/") - return resp.json if resp.ok else None - - -def _api_list_department_members(request, dept_id: int) -> list[dict]: - # Предпочтительно: /v1/employees/?department=&is_active=true&ordering=last_name,first_name - resp = _api(request).get( - API_EMPLOYEES, - params={ - "department": dept_id, - "is_active": "true", - "ordering": "last_name,first_name", - }, - ) - return _paged_or_list(resp) if resp.ok else [] - - -def _api_list_new_employees(request, limit: int = 10) -> list[dict]: - """Получить новых сотрудников через API""" - since = (timezone.now() - timedelta(days=14)).isoformat() - resp = _api(request).get( - API_EMPLOYEES, - params={ - "active": "true", - "created_at__gte": since, - "ordering": "-created_at", - "page_size": limit, - }, - ) - return _paged_or_list(resp) if resp.ok else [] - - -# ---------- Навигационные хелперы ---------- -def _post_back_url(post: dict, user) -> str: - t = post.get("type") - if t == TYPE_COMPANY: - return reverse("feed:feed_list") - if t == TYPE_DEPARTMENT and post.get("department_id"): - return reverse("employees:department_detail", args=[post["department_id"]]) - # страница пользователя — все его публикации - author_id = post.get("author_id") or ( - user.pk if getattr(user, "is_authenticated", False) else None - ) - return reverse("feed:employee_feed", args=[author_id or 0]) - - -# ---------- Ленты ---------- - - -def feed_list(request): - """ - Лента компании - посты и новые сотрудники загружаются через API на клиенте. - """ - return render( - request, - "feed/feed_list.html", - ) - - -@login_required -def department_feed(request, pk): - """Лента отдела""" - department = _api_get_department(request, pk) - if not department: - messages.error(request, "Отдел недоступен.") - return redirect("feed:feed_list") - posts = _api_list_posts(request, type=TYPE_DEPARTMENT, department=pk) - emp_links = _api_list_department_members(request, pk) - return render( - request, - "feed/feed_list.html", - { - "department": department, - "posts": posts, - "new_employees": [], - "emp_links": emp_links, - }, - ) - - -@login_required -def employee_feed(request, pk): - """Лента сотрудника""" - employee = _api_get_employee(request, pk) - if not employee: - messages.error(request, "Сотрудник недоступен.") - return redirect("feed:feed_list") - posts = _api_list_posts(request, author=pk) - return render( - request, "feed/feed_list.html", {"employee": employee, "posts": posts} - ) - - -# ---------- Детальная + комментарии ---------- - - -@login_required -def post_detail(request, pk): - """Рендерит детальную страницу поста с комментариями. CRUD через API в JavaScript.""" - post = _api_get_post(request, pk) - if not post: - return redirect("feed:feed_list") - - comments = _api_get_comments_for_post(request, pk) - form = CommentForm(initial={"post": pk}) - - return render( - request, - "feed/post_detail.html", - {"post": post, "comments": comments, "comment_form": form}, - ) - - -@login_required -def pin_post(request, pk): - post = _api_get_post(request, pk) - if not post: - return redirect("feed:feed_list") - action = "unpin" if post.get("pinned") else "pin" - resp = _api(request).post(f"{API_POSTS}{pk}/{action}/") - if resp.ok: - messages.success( - request, "Новость закреплена!" if action == "pin" else "Новость откреплена!" - ) - else: - messages.error(request, f"Операция недоступна: {resp.status}") - return redirect(_post_back_url(post, request.user)) - - -# ---------- Лайки ---------- - - -@login_required -def toggle_like(request, pk): - if request.method != "POST": - return redirect("feed:post_detail", pk=pk) - - op = (request.POST.get("op") or "like").lower() - if op not in {"like", "unlike"}: - op = "like" - - endpoint = "like" if op == "like" else "unlike" - resp = _api(request).post(f"{API_POSTS}{pk}/{endpoint}/") - - if not resp.ok: - messages.error(request, f"Не удалось {op} публикацию: {resp.status}") - - wants_json = ( - "application/json" in request.headers.get("Accept", "") - or request.headers.get("X-Requested-With") == "XMLHttpRequest" - ) - if wants_json: - data = resp.json or {} - return JsonResponse( - { - "ok": bool(resp.ok), - "post_id": pk, - "liked": data.get("liked"), - "likes": data.get("likes_count"), - } - ) - - next_url = ( - request.POST.get("next") - or request.META.get("HTTP_REFERER") - or reverse("feed:post_detail", args=[pk]) - ) - return HttpResponseRedirect(next_url)