feat: Improve file display, timezone handling, and LOT management
Changes: - Fix datetime serialization with UTC 'Z' suffix for correct timezone display - Add PDF upload support with extension fallback for MIME detection - Fix LOT add/remove by creating new list for SQLAlchemy JSON change detection - Add file message components (FileMessage, ImageLightbox, UploadPreview) - Add multi-file upload support with progress tracking - Link uploaded files to chat messages via message_id - Include file attachments in AI report generation - Update specs for file-storage, realtime-messaging, and ai-report-generation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,8 @@ from app.modules.file_storage.schemas import FileUploadResponse, FileMetadata, F
|
||||
from app.modules.file_storage.services.file_service import FileService
|
||||
from app.modules.file_storage.services import minio_service
|
||||
from app.modules.realtime.websocket_manager import manager as websocket_manager
|
||||
from app.modules.realtime.schemas import FileUploadedBroadcast, FileDeletedBroadcast, FileUploadAck
|
||||
from app.modules.realtime.schemas import FileUploadedBroadcast, FileDeletedBroadcast, FileUploadAck, MessageDeletedBroadcast, MessageBroadcast, MessageTypeEnum
|
||||
from app.modules.realtime.services.message_service import MessageService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,11 +59,52 @@ async def upload_file(
|
||||
# Upload file
|
||||
result = FileService.upload_file(db, room_id, user_email, file, description)
|
||||
|
||||
# Fetch the message and display name for broadcasting (before background task)
|
||||
message_obj = MessageService.get_message(db, result.message_id) if result.message_id else None
|
||||
display_name = MessageService.get_display_name(db, user_email)
|
||||
|
||||
# Prepare message broadcast data (needed before db session closes)
|
||||
message_data = None
|
||||
if message_obj:
|
||||
message_data = {
|
||||
"message_id": message_obj.message_id,
|
||||
"room_id": message_obj.room_id,
|
||||
"sender_id": message_obj.sender_id,
|
||||
"sender_display_name": display_name,
|
||||
"content": message_obj.content,
|
||||
"message_type": message_obj.message_type.value,
|
||||
"metadata": message_obj.message_metadata,
|
||||
"created_at": message_obj.created_at,
|
||||
"sequence_number": message_obj.sequence_number,
|
||||
}
|
||||
|
||||
# Broadcast file upload event to room members via WebSocket
|
||||
async def broadcast_file_upload():
|
||||
try:
|
||||
# First, broadcast the message event so it appears in chat
|
||||
if message_data:
|
||||
logger.info(f"Broadcasting message for file upload. message_data: {message_data}")
|
||||
msg_broadcast = MessageBroadcast(
|
||||
type="message",
|
||||
message_id=message_data["message_id"],
|
||||
room_id=message_data["room_id"],
|
||||
sender_id=message_data["sender_id"],
|
||||
sender_display_name=message_data["sender_display_name"],
|
||||
content=message_data["content"],
|
||||
message_type=MessageTypeEnum(message_data["message_type"]),
|
||||
metadata=message_data["metadata"],
|
||||
created_at=message_data["created_at"],
|
||||
sequence_number=message_data["sequence_number"],
|
||||
)
|
||||
broadcast_dict = msg_broadcast.model_dump(mode='json')
|
||||
logger.info(f"Message broadcast dict: {broadcast_dict}")
|
||||
await websocket_manager.broadcast_to_room(room_id, broadcast_dict)
|
||||
logger.info(f"Broadcasted file message: {message_data['message_id']} to room {room_id}")
|
||||
|
||||
# Then broadcast file uploaded event (for file drawer updates)
|
||||
broadcast = FileUploadedBroadcast(
|
||||
file_id=result.file_id,
|
||||
message_id=result.message_id,
|
||||
room_id=room_id,
|
||||
uploader_id=user_email,
|
||||
filename=result.filename,
|
||||
@@ -70,10 +112,11 @@ async def upload_file(
|
||||
file_size=result.file_size,
|
||||
mime_type=result.mime_type,
|
||||
download_url=result.download_url,
|
||||
thumbnail_url=result.thumbnail_url,
|
||||
uploaded_at=result.uploaded_at
|
||||
)
|
||||
await websocket_manager.broadcast_to_room(room_id, broadcast.to_dict())
|
||||
logger.info(f"Broadcasted file upload event: {result.file_id} to room {room_id}")
|
||||
logger.info(f"Broadcasted file upload event: {result.file_id} (message: {result.message_id}) to room {room_id}")
|
||||
|
||||
# Send acknowledgment to uploader
|
||||
ack = FileUploadAck(
|
||||
@@ -86,7 +129,7 @@ async def upload_file(
|
||||
logger.error(f"Failed to broadcast file upload: {e}")
|
||||
|
||||
# Run broadcast in background
|
||||
background_tasks.add_task(asyncio.create_task, broadcast_file_upload())
|
||||
background_tasks.add_task(broadcast_file_upload)
|
||||
|
||||
return result
|
||||
|
||||
@@ -149,9 +192,13 @@ async def get_file(
|
||||
expiry_seconds=3600
|
||||
)
|
||||
|
||||
# For images, the download URL also serves as thumbnail (CSS resized on frontend)
|
||||
thumbnail_url = download_url if file_record.file_type == "image" else None
|
||||
|
||||
# Build response with download URL
|
||||
return FileMetadata(
|
||||
file_id=file_record.file_id,
|
||||
message_id=file_record.message_id,
|
||||
room_id=file_record.room_id,
|
||||
filename=file_record.filename,
|
||||
file_type=file_record.file_type,
|
||||
@@ -162,7 +209,8 @@ async def get_file(
|
||||
uploaded_at=file_record.uploaded_at,
|
||||
uploader_id=file_record.uploader_id,
|
||||
deleted_at=file_record.deleted_at,
|
||||
download_url=download_url
|
||||
download_url=download_url,
|
||||
thumbnail_url=thumbnail_url
|
||||
)
|
||||
|
||||
|
||||
@@ -204,25 +252,38 @@ async def delete_file(
|
||||
# Check if admin
|
||||
is_admin = membership_service.is_system_admin(user_email)
|
||||
|
||||
# Delete file (service will verify permissions)
|
||||
deleted_file = FileService.delete_file(db, file_id, user_email, is_room_owner or is_admin)
|
||||
# Delete file (service will verify permissions and cascade to message)
|
||||
deleted_file, deleted_message_id = FileService.delete_file(db, file_id, user_email, is_room_owner or is_admin)
|
||||
|
||||
# Broadcast file deletion event to room members via WebSocket
|
||||
# Broadcast file and message deletion events to room members via WebSocket
|
||||
if deleted_file:
|
||||
async def broadcast_file_delete():
|
||||
try:
|
||||
broadcast = FileDeletedBroadcast(
|
||||
# Broadcast file deleted event
|
||||
file_broadcast = FileDeletedBroadcast(
|
||||
file_id=file_id,
|
||||
message_id=deleted_message_id,
|
||||
room_id=room_id,
|
||||
deleted_by=user_email,
|
||||
deleted_at=deleted_file.deleted_at
|
||||
)
|
||||
await websocket_manager.broadcast_to_room(room_id, broadcast.to_dict())
|
||||
await websocket_manager.broadcast_to_room(room_id, file_broadcast.to_dict())
|
||||
logger.info(f"Broadcasted file deletion event: {file_id} from room {room_id}")
|
||||
|
||||
# Also broadcast message deleted event if there was an associated message
|
||||
if deleted_message_id:
|
||||
msg_broadcast = MessageDeletedBroadcast(
|
||||
message_id=deleted_message_id,
|
||||
room_id=room_id,
|
||||
deleted_by=user_email,
|
||||
deleted_at=deleted_file.deleted_at
|
||||
)
|
||||
await websocket_manager.broadcast_to_room(room_id, msg_broadcast.to_dict())
|
||||
logger.info(f"Broadcasted message deletion event: {deleted_message_id} from room {room_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to broadcast file deletion: {e}")
|
||||
logger.error(f"Failed to broadcast file/message deletion: {e}")
|
||||
|
||||
# Run broadcast in background
|
||||
background_tasks.add_task(asyncio.create_task, broadcast_file_delete())
|
||||
background_tasks.add_task(broadcast_file_delete)
|
||||
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user