Corp/backend/templates/feed/post_detail.html
Игорь Надеин c78d1e5fea Ключевые изменения
- **EmployeeActionSerializer**
  - Добавлен `history[]` со снапшотами simple-history и diff по полям: `action`, `date`, `comment`, `extra`, `employee_id`.
  - Локализация названий событий через `employees.constants.ACTION_CHOICES`.
  - Поля `action_display`, `date_display` для удобного фронтового рендера.

- **EmployeeSerializer**
  - Возвращает `actions` (массив событий) с историей, **только** если в контексте выставлено `include_action_history=True`.
  - Навыки: чтение — `skills` как список объектов `{id, name}`, запись — через `skills_ids`.
  - Должность: чтение — объект `{id, name}`, запись — через `position_id`.
  - Отделы: добавлен флаг `is_head` (сотрудник — руководитель отдела).

- **EmployeeListSerializer**
  - Те же правки для навыков/должности и `is_head`, без `actions` (для производительности).

- **Views (EmployeeViewSet)**
  - `get_queryset`: префетч `actions` добавляется **только** для `retrieve/me`, иначе не тянем историю.
  - `get_serializer_context`: для `retrieve` и `me` выставляем `include_action_history=True`.
  - Экшен `me`: выборка с `Prefetch("actions", order_by -date)` и передачей `include_action_history=True`.

- **Разное**
  - Используем `ACTION_CHOICES` для «человеческих» названий событий в истории.
  - `HistoryActionMixin` оставлен без изменений; история объектов доступна по `/{basename}/{pk}/history/`.

Почему так
- История кадровых событий бывает тяжёлой → грузим и отдаём её только там, где нужно (`retrieve/me`).
- Явное разделение read/write для навыков и должности упрощает фронту жизнь и убирает двусмысленность.
- `is_head` позволяет фронту не вычислять этот флаг отдельно.

Файлы
- `backend/api/v1/employees/serializers.py`
- `backend/api/v1/employees/views.py`
- (использованы константы из) `backend/employees/constants.py`

Тесты/проверка
1) GET `/api/v1/employees/1/` → поле `actions[]` заполнено, внутри каждого элемента `history[]` со снапшотами и переводами событий.
2) GET `/api/v1/employees/?page=1` → `actions` отсутствуют (по замыслу), навыки и должность приходят в «красивом» виде.
3) GET `/api/v1/employees/me/` → аналогично `retrieve`, с историей.

* UI (templates/employees/employee\_detail.html)

  * Навыки: больше не итерируем по строке, выводим объекты `{id,name}` (`s.name`).
    Защита от пустых списков.
  * Отделы: убрал «аватарки» у элементов списка, оставил иконку у кнопки раздела.
    Роль сотрудника показывается «Название отдела — Роль».
    Для руководителя добавлен понятный бейдж с иконкой `bi-award-fill` + tooltip «Руководитель отдела».
  * Кадровые события: добавлен раздел «Кадровые события» (для аутентифицированных).
    Отрисовывает список всех событий (дата, комментарий, extra), а также историю каждого события (`action.history`) в `<details>`.
    В истории показываем только название события и новые значения полей (без «создано/изменено» и без зачёркнутых старых значений).
    Перевод кодов событий (`hired`, `dismissed`, …) в человекочитаемый вид согласно `employees/constants.py`.
  * Аккордеон: кнопка и выпадающая панель объединены в один блок `.ios-acc` единой ширины.
    Добавлена разделительная линия между заголовком и контентом (`.ios-acc-body { border-top: ... }`).
    Исправлены дёрганья при анимации: убраны внешние отступы у `.collapse`, возврат стандартной скорости Bootstrap (`.35s ease`), `overflow:hidden` на фазе `collapsing`.

* Views (employees/views.py — веб-приложение)

  * В `employee_detail` и `employee_me` дополнительно запрашиваем полную историю кадровых событий:
    `GET v1/employee-actions/?employee=<id>&ordering=-date` и передаём в шаблон `employee_actions`.
    Тем самым отображаем все события, а не только последнее.
  * Шаблон умеет брать события из `employee_actions`, а при отсутствии — из `emp.actions`.

* API (backend/api/v1/employees/serializers.py)

  * `EmployeeActionSerializer`: добавлен `history` (MethodField) с формированием массива изменений из `HistoricalRecords` (без старых значений; только `changes[field].new`, дата/пользователь).
    Добален `action_display` и `date_display` для удобного рендера на фронте.
  * `EmployeeSerializer`: возвращает `departments` через `get_departments()` (id, name, role\_id, role\_name, is\_head) и `skills` как массив объектов `{id,name}`.

* Прочее/фиксы

  * Безопасные проверки на пустые коллекции (контакты, навыки, отделы, события).
  * Устранена ошибка шаблона «Failed lookup…» при попытке обратиться к отсутствующим ключам.
  * Не изменяем скорость анимации: возвращён дефолт Bootstrap; корректное закрытие панелей.

Refs: employees/constants.py (ACTION\_CHOICES), templates/employees/employee\_detail.html, employees/views.py, api/v1/employees/serializers.py
2025-08-27 17:58:46 +03:00

280 lines
12 KiB
HTML
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.

{% extends 'base.html' %}
{% load static django_bootstrap5 %}
{% block title %}{% firstof post.title "Публикация" %}{% endblock %}
{% block content %}
{% firstof post.id post.pk as post_id %}
{% firstof post.author.id post.author.pk post.author_id as author_id %}
{% firstof post.department.id post.department.pk post.department_id as dept_id %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-9 col-xl-8">
<!-- Навигация и действия -->
<div class="d-flex align-items-center gap-2 mb-3">
{% if post.type == 'company' %}
<a href="{% url 'feed:feed_list' %}" class="btn btn-ghost">
<i class="bi-arrow-left"></i> К ленте
</a>
{% elif post.type == 'department' and dept_id %}
<a href="{% url 'employees:department_detail' dept_id %}" class="btn btn-ghost">
<i class="bi-arrow-left"></i> К отделу
</a>
{% else %}
{% if author_id %}
<a href="{% url 'feed:employee_feed' author_id %}" class="btn btn-ghost">
<i class="bi-arrow-left"></i> К публикациям автора
</a>
{% else %}
<a href="{% url 'feed:feed_list' %}" class="btn btn-ghost">
<i class="bi-arrow-left"></i> К ленте
</a>
{% endif %}
{% endif %}
<span class="small text-secondary ms-auto">
</span>
{# Пин — без скобок в условии #}
{% if post_id %}
{% if post.type == 'company' and user.is_staff %}
<a href="{% url 'feed:pin_post' post_id %}" class="btn btn-ghost">
<i class="bi-pin-angle"></i> {% if post.pinned %}Открепить{% else %}Закрепить{% endif %}
</a>
{% elif post.type == 'department' and dept_id %}
{% if user.is_staff %}
<a href="{% url 'feed:pin_post' post_id %}" class="btn btn-ghost">
<i class="bi-pin-angle"></i> {% if post.pinned %}Открепить{% else %}Закрепить{% endif %}
</a>
{% elif post.department and post.department.head_id == user.id %}
<a href="{% url 'feed:pin_post' post_id %}" class="btn btn-ghost">
<i class="bi-pin-angle"></i> {% if post.pinned %}Открепить{% else %}Закрепить{% endif %}
</a>
{% endif %}
{% endif %}
{% endif %}
{# Редактирование #}
{% if post_id %}
{% if user.is_staff or user.id == post.author_id %}
<a href="{% url 'feed:post_update' post_id %}" class="btn btn-ghost">
<i class="bi-pencil"></i> Редактировать
</a>
{% elif post.type == 'department' and post.department and post.department.head_id == user.id %}
<a href="{% url 'feed:post_update' post_id %}" class="btn btn-ghost">
<i class="bi-pencil"></i> Редактировать
</a>
{% endif %}
{% endif %}
</div>
<!-- Карточка поста -->
<article class="feed-card" data-post-card>
<header class="feed-hd">
<div class="feed-ava">
{% with a=post.author %}
{% if a %}
{% firstof a.avatar.url a.avatar_url as ava_url %}
{% if ava_url %}<img src="{{ ava_url }}" alt="">{% else %}<i class="bi-person"></i>{% endif %}
{% else %}
<i class="bi-person"></i>
{% endif %}
{% endwith %}
</div>
<div class="feed-meta">
<div class="feed-author">
{% if author_id %}
<a href="{% url 'employees:employee_detail' author_id %}" class="text-decoration-none">
{% with a=post.author %}
{% if a %}
{% firstof a.full_name a.get_full_name as nm %}
{% if nm %}{{ nm }}{% else %}
{% if a.first_name or a.last_name %}
{{ a.first_name }}{% if a.last_name %} {{ a.last_name }}{% endif %}
{% else %}
{% firstof a.username a.email "Сотрудник" %}
{% endif %}
{% endif %}
{% else %}Сотрудник #{{ author_id }}{% endif %}
{% endwith %}
</a>
{% else %}Автор не указан{% endif %}
</div>
<div class="feed-sub">
{% if post.created_at %}
{{ post.created_at_display }}
{% endif %}
{% if post.type == 'department' %}
{% firstof post.department.name post.department_name as dept_name %}
{% if dept_name %} · {{ dept_name }}{% endif %}
{% endif %}
</div>
</div>
{% if post.pinned %}
<span class="feed-pin"><i class="bi-pin-angle"></i> закреплено</span>
{% endif %}
</header>
{% if post.image %}
{% firstof post.image.url post.image as img_url %}
{% if img_url %}
<div class="feed-img">
<img src="{{ img_url }}" alt="{% firstof post.title 'Публикация' %}">
</div>
{% endif %}
{% endif %}
<div class="feed-body">
<h1 class="feed-title fs-4 mb-2">{% firstof post.title "Без названия" %}</h1>
<div class="fs-6 lh-base">{{ post.body|linebreaks }}</div>
{% if post.attachment %}
{% firstof post.attachment.url post.attachment as att_url %}
{% if att_url %}
<div class="mt-3">
<a href="{{ att_url }}" target="_blank" class="btn btn-ghost">
<i class="bi-paperclip"></i> Вложение
</a>
</div>
{% endif %}
{% endif %}
</div>
<footer class="feed-actions">
<a href="#comments" class="btn btn-ghost">
<i class="bi-chat-dots"></i> Комментарии ({{ comments|length }})
</a>
<span class="ms-auto"></span>
{% if user.is_authenticated and post_id %}
<form method="post" action="{% url 'feed:toggle_like' post_id %}">
{% csrf_token %}
<button type="submit"
class="btn btn-ghost {% if post.is_liked or is_liked %}is-liked{% endif %}"
data-like-post="{{ post_id }}"
data-like-url="{% url 'feed:toggle_like' post_id %}"
aria-pressed="{% if post.is_liked or is_liked %}true{% else %}false{% endif %}">
<i class="bi {% if post.is_liked or is_liked %}bi-heart-fill{% else %}bi-heart{% endif %}"></i>
<span data-like-count>{{ post.likes_count|default:0 }}</span>
</button>
</form>
{% else %}
<span class="small text-secondary d-inline-flex align-items-center gap-1">
<i class="bi-heart{% if post.is_liked %}-fill{% endif %}"></i>
<span data-like-count>{{ post.likes_count|default:0 }}</span>
</span>
{% endif %}
</footer>
</article>
<!-- Комментарии -->
<section id="comments" class="mt-4">
<div class="feed-header mb-2">
<i class="bi-chat-dots fs-4 text-primary"></i>
<h3 class="title mb-0">Комментарии</h3>
<span class="small text-secondary ms-2">({{ comments|length }})</span>
</div>
{% if comments %}
<div class="feed-stream">
{% for c in comments %}
{% firstof c.id c.pk as cid %}
{% firstof c.author.id c.author.pk c.author_id as c_author_id %}
<article id="c{{ cid }}" class="feed-card">
<header class="feed-hd">
<div class="feed-ava" style="width:36px;height:36px;">
{% with a=c.author %}
{% if a %}
{% firstof a.avatar.url a.avatar_url as c_ava %}
{% if c_ava %}<img src="{{ c_ava }}" alt="">{% else %}<i class="bi-person"></i>{% endif %}
{% else %}
<i class="bi-person"></i>
{% endif %}
{% endwith %}
</div>
<div class="feed-meta">
<div class="feed-author">
{% if c_author_id %}
<a href="{% url 'employees:employee_detail' c_author_id %}" class="text-decoration-none">
{% with a=c.author %}
{% if a %}
{% firstof a.full_name a.get_full_name as nm %}
{% if nm %}{{ nm }}{% else %}
{% if a.first_name or a.last_name %}
{{ a.first_name }}{% if a.last_name %} {{ a.last_name }}{% endif %}
{% else %}
{% firstof a.username a.email "Сотрудник" %}
{% endif %}
{% endif %}
{% else %}Сотрудник #{{ c_author_id }}{% endif %}
{% endwith %}
</a>
{% else %}Автор{% endif %}
</div>
<div class="feed-sub">
{% if c.created_at %}
<time datetime="{{ c.created_at }}">
{{ c.created_at_display }}
</time>
{% endif %}
</div>
</div>
{% if request.user.is_staff or request.user.id == c.author_id %}
<div class="ms-auto d-flex gap-2">
{% if cid %}
<a href="{% url 'feed:comment_update' cid %}" class="btn btn-ghost btn-sm">
<i class="bi-pencil"></i>
</a>
<a href="{% url 'feed:comment_delete' cid %}" class="btn btn-ghost btn-sm text-danger">
<i class="bi-trash"></i>
</a>
{% endif %}
</div>
{% endif %}
</header>
<div class="feed-body">
<div class="fs-6">{{ c.text|linebreaks }}</div>
</div>
</article>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info shadow-sm rounded-3">Пока нет комментариев.</div>
{% endif %}
</section>
<!-- Форма добавления комментария -->
{% if user.is_authenticated and post_id %}
<div class="feed-card mt-3">
<div class="feed-body">
<h6 class="mb-2">Оставить комментарий</h6>
<form method="post" action="">
{% csrf_token %}
<div class="input-group">
<textarea name="text" class="form-control" rows="2" placeholder="Напишите комментарий…"></textarea>
<button class="btn btn-primary" type="submit">
<i class="bi-send"></i>
</button>
</div>
</form>
</div>
</div>
{% else %}
<div class="text-center mt-3">
<a class="btn btn-primary" href="{% url 'auth_front:login' %}?next={{ request.path|urlencode }}">
Войдите, чтобы комментировать
</a>
</div>
{% endif %}
<div class="mb-5"></div>
</div>
</div>
</div>
{% endblock %}