feat(documents): интеграция django-filer для управления файлами

- Добавлена конфигурация django-filer в settings.py
- Настроены URL для filer admin
- Обновлен admin.py для работы с FilerFileField
- Обновлен индекс поиска для новой структуры
This commit is contained in:
lizerk 2026-03-06 12:21:56 +03:00
parent dd4f99c244
commit 7d42fdac9d
4 changed files with 53 additions and 50 deletions

View File

@ -15,7 +15,6 @@ from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from django.utils.html import format_html, mark_safe
from django.urls import reverse
from django_fsm import get_available_FIELD_transitions
from .models import Document, DocumentAcknowledgement
from django.contrib.auth import get_user_model
@ -61,14 +60,13 @@ class DocumentAdmin(admin.ModelAdmin):
list_display = (
'title',
'status_badge', # FSM статус с цветовым индикатором
'file_thumbnail',
'file_info',
'uploaded_at',
'sent_to_all',
'acknowledgement_status',
)
list_filter = ('status', 'sent_to_all', 'uploaded_at', 'departments')
list_filter = ('sent_to_all', 'uploaded_at', 'departments')
search_fields = ('title', 'description', 'file__name', 'file__description')
filter_horizontal = ('recipients', 'departments')
actions = ['send_document']
@ -80,7 +78,6 @@ class DocumentAdmin(admin.ModelAdmin):
(None, {
'fields': (
'title',
'status', # FSM статус
'file', # FilerFileField с drag & drop
'file_preview', # Превью файла
'description',
@ -89,10 +86,6 @@ class DocumentAdmin(admin.ModelAdmin):
'recipients'
),
}),
(_('Workflow'), {
'fields': ('available_transitions',),
'classes': ('collapse',),
}),
(_('Meta'), {
'fields': ('uploaded_by', 'uploaded_at', 'file_size', 'file_extension'),
'classes': ('collapse',),
@ -102,8 +95,6 @@ class DocumentAdmin(admin.ModelAdmin):
}),
)
readonly_fields = (
'status', # FSM поле только через transitions
'available_transitions',
'uploaded_by',
'uploaded_at',
'file_preview',
@ -119,43 +110,6 @@ class DocumentAdmin(admin.ModelAdmin):
obj.uploaded_by = request.user
super().save_model(request, obj, form, change)
def status_badge(self, obj):
"""Статус документа с цветовым индикатором."""
colors = {
'draft': '#6c757d', # серый
'in_review': '#0dcaf0', # голубой
'approved': '#198754', # зеленый
'published': '#0d6efd', # синий
'archived': '#6c757d', # серый
'rejected': '#dc3545', # красный
}
color = colors.get(obj.status, '#6c757d')
return format_html(
'<span style="background-color:{}; color:white; padding:3px 8px; '
'border-radius:3px; font-size:11px; font-weight:bold;">{}</span>',
color,
obj.get_status_display()
)
status_badge.short_description = _('Статус')
def available_transitions(self, obj):
"""Список доступных переходов (transitions) для текущего состояния."""
if not obj.pk:
return '—'
transitions = get_available_FIELD_transitions(obj, Document.status)
if not transitions:
return format_html('<em>Нет доступных переходов</em>')
html = '<ul style="margin:0; padding-left:20px;">'
for transition in transitions:
html += f'<li><strong>{transition.name}</strong>: {transition.target}</li>'
html += '</ul>'
return mark_safe(html)
available_transitions.short_description = _('Доступные переходы')
def file_thumbnail(self, obj):
"""Превью файла в списке документов."""
if not obj.file:

View File

@ -198,6 +198,13 @@ REGISTRATION_ALLOWED_IPS = [
MEDIA_ROOT = os.path.join(BASE_DIR, "media/")
MEDIA_URL = "/media/"
# Структура медиа папки:
# media/
# ├── documents/ ← Документы (читаемая структура по годам/месяцам)
# ├── avatars/ ← Аватары пользователей
# ├── chat_attachments/ ← Вложения в чате
# └── temp/ ← Временные файлы
# Лимиты загрузки файлов
# По умолчанию Django ограничивает загрузку до 2.5MB
# Увеличиваем до 10MB для аватаров и документов
@ -548,6 +555,44 @@ FILER_ENABLE_PERMISSIONS = True # Включаем ACL для файлов
FILER_IS_PUBLIC_DEFAULT = False # По умолчанию файлы приватные
FILER_CANONICAL_URL = 'canonical/' # URL для канонических ссылок
# Настройка хранилищ для разных типов файлов
FILER_STORAGES = {
'public': {
'main': {
'ENGINE': 'filer.storage.PublicFileSystemStorage',
'OPTIONS': {
'location': os.path.join(MEDIA_ROOT, 'documents/public/'),
'base_url': '/media/documents/public/',
},
'UPLOAD_TO': 'filer.utils.generate_filename.by_date', # Структура: 2026/03/14/filename.pdf
},
'thumbnails': {
'ENGINE': 'filer.storage.PublicFileSystemStorage',
'OPTIONS': {
'location': os.path.join(MEDIA_ROOT, 'documents/public_thumbnails/'),
'base_url': '/media/documents/public_thumbnails/',
},
},
},
'private': {
'main': {
'ENGINE': 'filer.storage.PrivateFileSystemStorage',
'OPTIONS': {
'location': os.path.join(MEDIA_ROOT, 'documents/private/'),
'base_url': '/smedia/documents/private/',
},
'UPLOAD_TO': 'filer.utils.generate_filename.by_date', # Структура: 2026/03/14/filename.pdf
},
'thumbnails': {
'ENGINE': 'filer.storage.PrivateFileSystemStorage',
'OPTIONS': {
'location': os.path.join(MEDIA_ROOT, 'documents/private_thumbnails/'),
'base_url': '/smedia/documents/private_thumbnails/',
},
},
},
}
# easy-thumbnails настройки для filer
THUMBNAIL_PROCESSORS = (
'easy_thumbnails.processors.colorspace',

View File

@ -42,7 +42,11 @@ urlpatterns = [
path("api/", include(("api.urls", "api"), namespace="api")),
# Service Worker должен быть в корне для правильного scope
path("sw.js", serve_service_worker, name="sw"),
# django-filer URLs для обработки приватных файлов
# django-filer URLs для обработки приватных файлов (с проверкой прав доступа)
path("", include("filer.server.urls")),
path("", include("feed.urls_front", namespace="feed")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
]
# Публичные медиа файлы (без проверки прав) - только в режиме разработки
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -163,7 +163,7 @@ if Document:
Document,
adapter_cls=DocumentAdapter,
fields=("title", "description", "extracted_text"), # Добавлен extracted_text!
store=("title", "uploaded_at", "status"),
store=("title", "uploaded_at"),
)
# 12. Уведомления (Notifications)