diff --git a/backend/api/v1/documents/serializers.py b/backend/api/v1/documents/serializers.py index bfdc12a8..0fa6c1fb 100644 --- a/backend/api/v1/documents/serializers.py +++ b/backend/api/v1/documents/serializers.py @@ -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): """Сериализатор для чтения документа. diff --git a/backend/api/v1/documents/views.py b/backend/api/v1/documents/views.py index 1225a742..6de9509b 100644 --- a/backend/api/v1/documents/views.py +++ b/backend/api/v1/documents/views.py @@ -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). diff --git a/backend/docs/reports/LIBRARY_USAGE_ANALYSIS.md b/backend/docs/reports/LIBRARY_USAGE_ANALYSIS.md new file mode 100644 index 00000000..c03eeeca --- /dev/null +++ b/backend/docs/reports/LIBRARY_USAGE_ANALYSIS.md @@ -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 г. diff --git a/backend/eusrr_backend/settings.py b/backend/eusrr_backend/settings.py index f7d1bf0f..5f98a48b 100644 --- a/backend/eusrr_backend/settings.py +++ b/backend/eusrr_backend/settings.py @@ -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",