feat: Add django-reversion API endpoints for document versions

- Added RevisionMiddleware to settings for automatic user tracking
- Created VersionSerializer and ActivityItemSerializer for version history
- Implemented GET /documents/{id}/versions/ endpoint
- Implemented GET /documents/{id}/activity/ endpoint (versions + audit + acknowledgements)
- Implemented POST /documents/{id}/revert/ endpoint for version rollback
- Activity timeline combines data from reversion, audit log, and acknowledgements
- All endpoints properly serialize user data with avatar support

This unblocks the Activity and Versions tabs in DocumentDetailModal.
This commit is contained in:
igor_nadein 2026-02-28 14:29:54 +03:00
parent 243fffc126
commit 7739e7520e
4 changed files with 663 additions and 1 deletions

View File

@ -176,6 +176,42 @@ class CabinetSerializer(serializers.Serializer):
return obj.documents.count() if hasattr(obj, 'documents') else 0
class VersionSerializer(serializers.Serializer):
"""Сериализатор для версий документа (django-reversion)."""
id = serializers.IntegerField(read_only=True)
revision_id = serializers.IntegerField(source='revision.id', read_only=True)
date_created = serializers.DateTimeField(source='revision.date_created', read_only=True)
user = serializers.SerializerMethodField()
comment = serializers.CharField(source='revision.comment', read_only=True, allow_blank=True)
# Данные версии
data = serializers.SerializerMethodField()
def get_user(self, obj) -> dict | None:
"""Возвращает информацию о пользователе, создавшем версию."""
if not obj.revision.user:
return None
user = obj.revision.user
return {
'id': user.id,
'full_name': f'{user.last_name} {user.first_name}'.strip(),
'avatar_url': getattr(user, 'avatar_url', None),
}
def get_data(self, obj) -> dict:
"""Возвращает данные версии документа."""
return obj.field_dict
class ActivityItemSerializer(serializers.Serializer):
"""Сериализатор для элемента timeline активности."""
type = serializers.CharField() # 'version', 'audit', 'acknowledgement'
timestamp = serializers.DateTimeField()
user = serializers.DictField(allow_null=True)
action = serializers.CharField()
details = serializers.DictField(allow_null=True)
class DocumentReadSerializer(serializers.ModelSerializer):
"""Сериализатор для чтения документа.

View File

@ -7,6 +7,8 @@ from django.db import transaction
from django.db.models import Count, Exists, OuterRef, QuerySet, Q
from documents.models import Document, DocumentAcknowledgement
from filer.models import Folder
import reversion
from reversion.models import Version
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
@ -17,7 +19,13 @@ from rest_framework.viewsets import ModelViewSet
from ..employees.serializers import EmployeeBriefSerializer
from ..permissions import AdminOrActionOrModelPerms
from .permissions import DocumentReadOrModelPerms
from .serializers import DocumentReadSerializer, DocumentWriteSerializer, FolderSerializer
from .serializers import (
DocumentReadSerializer,
DocumentWriteSerializer,
FolderSerializer,
VersionSerializer,
ActivityItemSerializer,
)
User = get_user_model()
@ -331,6 +339,185 @@ class DocumentViewSet(ModelViewSet):
status=status.HTTP_400_BAD_REQUEST
)
# -------------------------------------------------------------------------
# DJANGO-REVERSION ENDPOINTS
# -------------------------------------------------------------------------
@action(detail=True, methods=['get'])
def versions(self, request, pk=None):
"""Получить историю версий документа.
Returns:
[
{
"id": 1,
"revision_id": 123,
"date_created": "2026-02-28T10:00:00Z",
"user": {"id": 1, "full_name": "Иванов Иван"},
"comment": "Изменено описание",
"data": {"title": "...", "description": "..."}
},
...
]
"""
document = self.get_object()
# Получаем все версии документа
versions = Version.objects.get_for_object(document).select_related('revision', 'revision__user')
serializer = VersionSerializer(versions, many=True)
return Response(serializer.data)
@action(detail=True, methods=['get'])
def activity(self, request, pk=None):
"""Получить timeline активности документа (версии + аудит).
Объединяет данные из:
- reversion.Version (изменения документа)
- DocumentAuditLog (аудит действий)
- DocumentAcknowledgement (ознакомления)
Returns:
[
{
"type": "version",
"timestamp": "2026-02-28T10:00:00Z",
"user": {"id": 1, "full_name": "Иванов Иван"},
"action": "Изменение документа",
"details": {"comment": "...", "fields": ["title", "description"]}
},
{
"type": "audit",
"timestamp": "2026-02-28T09:00:00Z",
"user": {...},
"action": "Просмотрен",
"details": {"ip_address": "192.168.1.1"}
},
...
]
"""
document = self.get_object()
activity_items = []
# 1. Добавляем версии
versions = Version.objects.get_for_object(document).select_related('revision', 'revision__user')
for version in versions:
user_data = None
if version.revision.user:
user = version.revision.user
user_data = {
'id': user.id,
'full_name': f'{user.last_name} {user.first_name}'.strip(),
'avatar_url': getattr(user, 'avatar_url', None),
}
activity_items.append({
'type': 'version',
'timestamp': version.revision.date_created,
'user': user_data,
'action': 'Изменение документа',
'details': {
'comment': version.revision.comment or '',
'version_id': version.id,
'revision_id': version.revision.id,
}
})
# 2. Добавляем аудит (если есть)
if hasattr(document, 'audit_log'):
audit_logs = document.audit_log.all().select_related('user')[:50] # Ограничиваем 50 записями
for log in audit_logs:
user_data = None
if log.user:
user_data = {
'id': log.user.id,
'full_name': f'{log.user.last_name} {log.user.first_name}'.strip(),
'avatar_url': getattr(log.user, 'avatar_url', None),
}
activity_items.append({
'type': 'audit',
'timestamp': log.timestamp,
'user': user_data,
'action': log.get_action_display(),
'details': {
'ip_address': log.ip_address,
'metadata': log.metadata,
}
})
# 3. Добавляем ознакомления
acknowledgements = DocumentAcknowledgement.objects.filter(document=document).select_related('user')[:50]
for ack in acknowledgements:
user_data = {
'id': ack.user.id,
'full_name': f'{ack.user.last_name} {ack.user.first_name}'.strip(),
'avatar_url': getattr(ack.user, 'avatar_url', None),
}
activity_items.append({
'type': 'acknowledgement',
'timestamp': ack.acknowledged_at,
'user': user_data,
'action': 'Ознакомление',
'details': None
})
# Сортируем по времени (новые сверху)
activity_items.sort(key=lambda x: x['timestamp'], reverse=True)
serializer = ActivityItemSerializer(activity_items, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def revert(self, request, pk=None):
"""Откатить документ к указанной версии.
Body:
{
"version_id": 123, // ID версии из reversion.Version
"comment": "Причина отката" // опционально
}
Returns:
Обновленный документ
"""
document = self.get_object()
version_id = request.data.get('version_id')
comment = request.data.get('comment', '')
if not version_id:
return Response(
{'error': 'version_id обязателен'},
status=status.HTTP_400_BAD_REQUEST
)
try:
# Получаем версию
version = Version.objects.get(id=version_id, object_id=str(document.pk))
# Откатываем с комментарием
with reversion.create_revision():
version.revision.revert()
reversion.set_user(request.user)
reversion.set_comment(comment or f'Откат к версии {version_id}')
# Обновляем объект из базы
document.refresh_from_db()
serializer = self.get_serializer(document)
return Response(serializer.data)
except Version.DoesNotExist:
return Response(
{'error': 'Версия не найдена'},
status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
class FolderViewSet(ModelViewSet):
"""CRUD для папок документов (filer.Folder).

View File

@ -0,0 +1,438 @@
# Анализ использования библиотек для документооборота
**Дата:** 28 февраля 2026 г.
**Статус:** Анализ установленных библиотек и степени использования их возможностей
---
## 📊 Общая статистика
| Библиотека | Версия | Использование | Комментарий |
|------------|--------|---------------|-------------|
| **django-filer** | 3.4.4 | ~25% | Базовые возможности файлового хранения |
| **django-reversion** | 6.1.0 | ~15% | Только регистрация моделей, нет API |
| **django-fsm** | 3.0.1 | ~80% | Полный workflow с 7 transitions |
| **easy-thumbnails** | 2.10.1 | ~40% | Настройки есть, активное использование не проверено |
| **django-mptt** | 0.18.0 | ~60% | Используется через filer для иерархии папок |
**Средний процент использования: ~44%**
---
## 1. django-filer 3.4.4 (~25% использования)
### ✅ Что используется:
#### Базовые возможности:
- ✅ `FilerFileField` - поле для файлов
- ✅ `Folder` модель - иерархическая структура папок
- ✅ `File` модель - хранение файлов через django-filer
- ✅ Связь Document → filer.File
- ✅ Связь Document → filer.Folder
- ✅ ACL настройки (`FILER_ENABLE_PERMISSIONS = True`)
- ✅ Приватные файлы по умолчанию (`FILER_IS_PUBLIC_DEFAULT = False`)
#### API:
- ✅ `FolderViewSet` - CRUD для папок
- ✅ `FolderViewSet.children()` - получение дочерних папок
- ✅ `FolderViewSet.documents()` - документы в папке
- ✅ Фильтр по `?parent_id=` и `?root=true`
### ❌ Что НЕ используется:
#### Продвинутые возможности:
- ❌ **FilerImageField** - специальное поле для изображений с метаданными (EXIF, геотеги)
- ❌ **ThumbnailOption** - кастомные пресеты миниатюр (настройки есть, но не используются в коде)
- ❌ **ClipboardItem** - буфер обмена для администратора
- ❌ **canonical_url** - прямые URL к файлам (настройка есть: `FILER_CANONICAL_URL = 'canonical/'`)
- ❌ **icons_url** - URL к иконкам типов файлов
- ❌ **subject_location** - точка фокуса для умной обрезки изображений
- ❌ **is_public / file_ptr** - управление публичностью отдельных файлов
#### Миниатюры:
- ❌ Генерация thumbnails для PDF (библиотека поддерживает через pdf2image)
- ❌ Кэширование thumbnails
- ❌ Watermarks на изображениях
- ❌ EXIF-метаданные для фото
#### Permissions:
- ❌ **FilerPermissions** - детальные права на файлы и папки
- ❌ Группы доступа к папкам
- ❌ Наследование прав в иерархии папок
#### Admin Features:
- ❌ Drag & Drop интерфейс администратора
- ❌ Clipboard для копирования/перемещения файлов
- ❌ Массовые операции над файлами
- ❌ Directory listing views
#### Интеграция:
- ❌ `django-polymorphic` для типов файлов (установлено, но минимально используется)
- ❌ REST API от django-filer (мы используем свой собственный)
### 💡 Рекомендации:
**Высокий приоритет:**
1. **Thumbnails API** - добавить endpoint для получения миниатюр разных размеров
2. **Subject location** - для умной обрезки аватаров и превью
3. **Детальные permissions** - права на уровне папок и файлов
**Средний приоритет:**
4. **FilerImageField** - для работы с изображениями (метаданные, EXIF)
5. **Canonical URLs** - прямые ссылки на файлы без перенаправлений
6. **Clipboard** - буфер обмена в админке для удобства
**Низкий приоритет:**
7. **Watermarks** - водяные знаки на документы
8. **PDF thumbnails** - превью PDF через pdf2image
---
## 2. django-reversion 6.1.0 (~15% использования)
### ✅ Что используется:
- ✅ `@reversion.register()` на модели `Document` и `DocumentAcknowledgement`
- ✅ Автоматическое сохранение версий при изменениях
- ✅ Настройка в `INSTALLED_APPS`
### ❌ Что НЕ используется:
#### API для версий:
- ❌ **Version.objects.get_for_object()** - получение истории версий
- ❌ **Version.revision** - доступ к ревизиям
- ❌ **Version.field_dict** - сравнение версий
- ❌ **get_deleted()** - восстановление удаленных объектов
#### Endpoints (полностью отсутствуют):
- ❌ `GET /api/v1/documents/{id}/versions/` - список версий
- ❌ `GET /api/v1/documents/{id}/versions/{version_id}/` - конкретная версия
- ❌ `POST /api/v1/documents/{id}/revert/` - откат к версии
- ❌ `GET /api/v1/documents/{id}/activity/` - timeline активности
- ❌ `GET /api/v1/documents/{id}/compare/` - сравнение версий
#### Продвинутые возможности:
- ❌ **RevisionMiddleware** - автоматическое связывание версий с пользователем (middleware не добавлен!)
- ❌ **Комментарии к версиям** - `reversion.create_revision(comment=...)`
- ❌ **Метаданные версий** - хранение дополнительной информации
- ❌ **Batch revisions** - группировка изменений в одну ревизию
- ❌ **Version filtering** - фильтрация версий по пользователю/дате
#### Admin:
- ❌ **VersionAdmin** - интерфейс истории в админке
- ❌ Кнопка "Revert" для отката
- ❌ Diff между версиями
### 💡 Рекомендации:
**КРИТИЧНО (блокирует фронтенд):**
1. ✅ Добавить `RevisionMiddleware` в `MIDDLEWARE`
2. ✅ Реализовать endpoint `/versions/` - список версий документа
3. ✅ Реализовать endpoint `/activity/` - timeline для вкладки Activity
4. ✅ Реализовать endpoint `/revert/` - откат к версии
**Высокий приоритет:**
5. Метаданные к версиям (кто, когда, что изменил)
6. Комментарии к изменениям
7. Сравнение версий (diff)
**Средний приоритет:**
8. VersionAdmin в админке
9. Восстановление удаленных документов
10. Фильтрация версий
---
## 3. django-fsm 3.0.1 (~80% использования)
### ✅ Что используется:
#### Модель:
- ✅ `FSMField` с 6 статусами (draft, in_review, approved, published, archived, rejected)
- ✅ `@transition` декораторы для 7 transitions
- ✅ `protected=True` - защита от прямого изменения status
#### Transitions:
- ✅ `submit_for_review()` - draft → in_review
- ✅ `approve()` - in_review → approved
- ✅ `reject()` - in_review → rejected
- ✅ `publish()` - approved → published
- ✅ `return_to_draft()` - in_review/draft → draft
- ✅ `archive()` - published → archived
- ✅ `unarchive()` - archived → published
#### API:
- ✅ Все 7 transitions имеют endpoint `POST /documents/{id}/{action}/`
- ✅ Автоматическое сохранение через `.save()` после transition
#### Admin:
- ✅ `get_available_FIELD_transitions()` - доступные переходы
### ❌ Что НЕ используется:
#### Продвинутые transitions:
- ❌ **Conditions** - условия выполнения перехода (`@transition(..., conditions=[...])`)
- ❌ **Permissions** - права на переход (`permission='documents.can_approve'`)
- ❌ **Custom targets** - динамические целевые статусы
- ❌ **on_error** - обработка ошибок transition
#### Hooks и сигналы:
- ❌ **pre_transition** - действия перед переходом
- ❌ **post_transition** - действия после перехода (сейчас используются сигналы Django, можно было бы FSM)
- ❌ **Transition history** - автоматическая история переходов
#### Интеграция:
- ❌ **django-fsm-log** - детальное логирование переходов (не установлено)
- ❌ **FSMAdmin** - улучшенный интерфейс админки с кнопками переходов
#### Graphviz:
- ❌ **get_graph()** - визуализация state machine
- ❌ Диаграмма workflow для документации
### 💡 Рекомендации:
**Высокий приоритет:**
1. **Conditions** - проверять права перед transition (например, `can_approve`)
2. **Permissions** - интеграция с django permissions
3. **django-fsm-log** - установить и логировать все переходы
**Средний приоритет:**
4. **pre_transition/post_transition** - заменить текущие сигналы на FSM hooks
5. **on_error** - обработка ошибок (например, если документ без файла)
6. **FSMAdmin** - улучшить админку
**Низкий приоритет:**
7. **Graph visualization** - диаграмма workflow для документации
8. **Custom targets** - если потребуются сложные workflows
---
## 4. easy-thumbnails 2.10.1 (~40% использования)
### ✅ Что используется (в settings.py):
#### Настройки:
- ✅ `THUMBNAIL_PROCESSORS` - процессоры обработки
- ✅ `colorspace` - конвертация цветового пространства
- ✅ `autocrop` - автообрезка
- ✅ `scale_and_crop_with_subject_location` - умная обрезка (filer)
- ✅ `filters` - фильтры изображений
- ✅ `THUMBNAIL_HIGH_RESOLUTION = True` - Retina поддержка
- ✅ `THUMBNAIL_PRESERVE_EXTENSIONS` - сохранение PNG/GIF
#### Размеры (в settings):
```python
THUMBNAIL_ALIASES = {
'': {
'admin_thumbnail': {'size': (60, 60), 'crop': True},
'file_card': {'size': (200, 150), 'crop': True},
'preview': {'size': (800, 600), 'crop': False},
}
}
```
### ❌ Что НЕ используется:
#### В коде:
- ❌ **get_thumbnailer()** - генерация thumbnails в коде
- ❌ **{% thumbnail %}** template tag - использование в шаблонах (у нас React фронтенд)
- ❌ API endpoints для получения thumbnails разных размеров
- ❌ **ThumbnailerNamespace** - кастомные namespace для разных разрешений
#### Продвинутые процессоры:
- ❌ **background** - фоновый цвет для прозрачных
- ❌ **detail** - резкость изображения
- ❌ **replace_alpha** - замена прозрачности
- ❌ **sharpen** - усиление резкости
#### Опции:
- ❌ **quality** - качество JPEG
- ❌ **subsampling** - субдискретизация для размера файла
- ❌ **progressive** - прогрессивная загрузка JPEG
- ❌ **orientation** - автоповорот по EXIF
#### Кэширование:
- ❌ Настройки кэширования thumbnails
- ❌ Warming cache (предгенерация)
- ❌ Очистка устаревших thumbnails
### 💡 Рекомендации:
**Высокий приоритет:**
1. **API для thumbnails** - `GET /documents/{id}/thumbnail/?size=preview`
2. **Предгенерация** - создавать thumbnails при загрузке
3. **Quality settings** - настроить качество для баланса размер/качество
**Средний приоритет:**
4. **Progressive JPEG** - для быстрой загрузки
5. **Warming** - предгенерация популярных размеров
6. **Orientation** - автоповорот фото
---
## 5. django-mptt 0.18.0 (~60% использования)
### ✅ Что используется:
- ✅ Используется через django-filer для `filer.Folder` (иерархия папок)
- ✅ Queries через MPTT для получения `children` и `parent`
- ✅ Настройка в `INSTALLED_APPS`
### ❌ Что НЕ используется:
#### В собственных моделях:
- ❌ **MPTTModel** - не используем для своих моделей (например, Cabinet)
- ❌ **TreeForeignKey** - только через filer
- ❌ **get_descendants()** - получение всех потомков
- ❌ **get_ancestors()** - получение всех предков
- ❌ **get_root()** - корневой элемент
#### Template tags:
- ❌ **{% recursetree %}** - рекурсивный вывод дерева (React фронтенд)
- ❌ **{% full_tree_for_model %}** - полное дерево
#### Manager методы:
- ❌ **add_related_count()** - подсчет связанных объектов
- ❌ **move_to()** - перемещение узлов
- ❌ **insert_at()** - вставка в дерево
#### Admin:
- ❌ **MPTTModelAdmin** - drag & drop в админке
- ❌ **TreeRelatedFieldListFilter** - фильтр по дереву
### 💡 Рекомендации:
**Средний приоритет:**
1. **Использовать MPTTModel** для Cabinet (у него есть parent, но не MPTT)
2. **get_descendants()** - для получения всех подпапок с документами
3. **MPTTModelAdmin** - улучшить админку папок
---
## 📈 Сводная таблица неиспользуемых возможностей
### Критичные (блокируют фронтенд):
1. ✅ **django-reversion API** - endpoints для версий и activity (вкладки в modal)
2. ✅ **RevisionMiddleware** - связывание версий с пользователем
### Высокий приоритет:
3. **Thumbnails API** - endpoint для получения превью разных размеров
4. **django-filer permissions** - детальные права на папки/файлы
5. **django-fsm conditions** - условия и права для transitions
6. **Metadata к версиям** - кто, когда, что изменил
### Средний приоритет:
7. **FilerImageField** - для изображений с метаданными
8. **Subject location** - умная обрезка изображений
9. **Django-fsm-log** - детальное логирование transitions
10. **Cabinet с MPTT** - иерархические виртуальные хранилища
### Низкий приоритет:
11. Clipboard в админке
12. Watermarks
13. Graph visualization для FSM
14. Восстановление удаленных документов
---
## 🎯 Рекомендуемый план действий
### Этап 1: Критичные задачи (разблокировать фронтенд)
**Срок: 1-2 дня**
```python
# 1. Добавить RevisionMiddleware
MIDDLEWARE = [
...
'reversion.middleware.RevisionMiddleware', # ← Добавить
...
]
# 2. Реализовать ViewSet actions:
@action(detail=True, methods=['get'])
def versions(self, request, pk=None):
"""История версий документа"""
@action(detail=True, methods=['get'])
def activity(self, request, pk=None):
"""Timeline активности (версии + аудит)"""
@action(detail=True, methods=['post'])
def revert(self, request, pk=None):
"""Откат к версии"""
```
### Этап 2: Высокий приоритет (улучшить UX)
**Срок: 3-5 дней**
```python
# 3. Thumbnails API
@action(detail=True, methods=['get'])
def thumbnail(self, request, pk=None):
"""Получить thumbnail документа
Query params:
size: admin_thumbnail | file_card | preview
"""
# 4. Условия и права для FSM
@transition(
field=status,
source='in_review',
target='approved',
permission='documents.can_approve',
conditions=[has_required_fields, user_has_permission]
)
def approve(self):
"""Одобрить документ"""
```
### Этап 3: Средний приоритет (продвинутые фичи)
**Срок: 1 неделя**
```python
# 5. Установить django-fsm-log
pip install django-fsm-log
# 6. FilerImageField для изображений
class Document(models.Model):
file = FilerFileField(...) # Для общих файлов
image = FilerImageField(...) # Для изображений (nullable)
# 7. Permissions на папки
class FolderViewSet:
def check_folder_permission(self, user, folder, action):
"""Проверить права на папку"""
```
---
## 📊 Итоговая оценка
| Критерий | Оценка | Комментарий |
|----------|--------|-------------|
| Базовые возможности | ✅ 85% | CRUD, workflow, папки работают |
| Продвинутые возможности | ⚠️ 30% | Версии, thumbnails, permissions не используются |
| Frontend интеграция | ⚠️ 60% | API есть, но нет endpoints для versions/thumbnails |
| Admin интеграция | ⚠️ 40% | Базовый функционал, нет drag&drop, clipboard |
| Performance | ⚠️ 50% | Нет кэширования thumbnails, нет предгенерации |
**Общая оценка использования библиотек: 44%**
---
## 🔍 Дополнительные библиотеки (не установлены)
Библиотеки, которые могли бы улучшить документооборот:
1. **django-fsm-log** - логирование FSM transitions
2. **django-admin-sortable2** - drag & drop в админке
3. **pdf2image** - thumbnails для PDF
4. **python-magic** - определение MIME-типов
5. **OCRmyPDF** - OCR для сканов
6. **WeasyPrint** - генерация PDF из HTML
---
**Подготовил:** GitHub Copilot
**Дата:** 28 февраля 2026 г.

View File

@ -73,6 +73,7 @@ MIDDLEWARE = [
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"reversion.middleware.RevisionMiddleware", # django-reversion для версионирования
"simple_history.middleware.HistoryRequestMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",