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:
@@ -69,11 +69,38 @@ class FileService:
|
||||
detail="File storage service temporarily unavailable"
|
||||
)
|
||||
|
||||
# Create database record
|
||||
# Create database record and associated message
|
||||
try:
|
||||
# Generate presigned download URL
|
||||
download_url = minio_service.generate_presigned_url(
|
||||
bucket=settings.MINIO_BUCKET,
|
||||
object_path=object_path,
|
||||
expiry_seconds=3600
|
||||
)
|
||||
|
||||
# For images, the download URL also serves as thumbnail (CSS resized on frontend)
|
||||
thumbnail_url = download_url if file_type == "image" else None
|
||||
|
||||
# Create the associated chat message first
|
||||
message = FileService.create_file_reference_message(
|
||||
db=db,
|
||||
room_id=room_id,
|
||||
sender_id=uploader_id,
|
||||
file_id=file_id,
|
||||
filename=file.filename,
|
||||
file_type=file_type,
|
||||
mime_type=mime_type,
|
||||
file_size=file_size,
|
||||
file_url=download_url,
|
||||
thumbnail_url=thumbnail_url,
|
||||
description=description
|
||||
)
|
||||
|
||||
# Create file record with message_id reference
|
||||
room_file = RoomFile(
|
||||
file_id=file_id,
|
||||
room_id=room_id,
|
||||
message_id=message.message_id,
|
||||
uploader_id=uploader_id,
|
||||
filename=file.filename,
|
||||
file_type=file_type,
|
||||
@@ -88,20 +115,15 @@ class FileService:
|
||||
db.commit()
|
||||
db.refresh(room_file)
|
||||
|
||||
# Generate presigned download URL
|
||||
download_url = minio_service.generate_presigned_url(
|
||||
bucket=settings.MINIO_BUCKET,
|
||||
object_path=object_path,
|
||||
expiry_seconds=3600
|
||||
)
|
||||
|
||||
return FileUploadResponse(
|
||||
file_id=file_id,
|
||||
message_id=message.message_id,
|
||||
filename=file.filename,
|
||||
file_type=file_type,
|
||||
file_size=file_size,
|
||||
mime_type=mime_type,
|
||||
download_url=download_url,
|
||||
thumbnail_url=thumbnail_url,
|
||||
uploaded_at=room_file.uploaded_at,
|
||||
uploader_id=uploader_id
|
||||
)
|
||||
@@ -160,12 +182,17 @@ class FileService:
|
||||
file_id: str,
|
||||
user_id: str,
|
||||
is_room_owner: bool = False
|
||||
) -> Optional[RoomFile]:
|
||||
"""Soft delete file"""
|
||||
) -> tuple[Optional[RoomFile], Optional[str]]:
|
||||
"""
|
||||
Soft delete file and its associated message.
|
||||
|
||||
Returns:
|
||||
Tuple of (deleted_file, deleted_message_id) or (None, None) if not found
|
||||
"""
|
||||
file = db.query(RoomFile).filter(RoomFile.file_id == file_id).first()
|
||||
|
||||
if not file:
|
||||
return None
|
||||
return None, None
|
||||
|
||||
# Check permissions
|
||||
if not is_room_owner and file.uploader_id != user_id:
|
||||
@@ -174,12 +201,21 @@ class FileService:
|
||||
detail="Only file uploader or room owner can delete files"
|
||||
)
|
||||
|
||||
# Soft delete
|
||||
deleted_message_id = None
|
||||
|
||||
# Soft delete the associated message if it exists
|
||||
if file.message_id:
|
||||
message = db.query(Message).filter(Message.message_id == file.message_id).first()
|
||||
if message and message.deleted_at is None:
|
||||
message.deleted_at = datetime.utcnow()
|
||||
deleted_message_id = message.message_id
|
||||
|
||||
# Soft delete the file
|
||||
file.deleted_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(file)
|
||||
|
||||
return file
|
||||
return file, deleted_message_id
|
||||
|
||||
@staticmethod
|
||||
def check_room_membership(db: Session, room_id: str, user_id: str) -> Optional[RoomMember]:
|
||||
@@ -205,7 +241,10 @@ class FileService:
|
||||
file_id: str,
|
||||
filename: str,
|
||||
file_type: str,
|
||||
mime_type: str,
|
||||
file_size: int,
|
||||
file_url: str,
|
||||
thumbnail_url: Optional[str] = None,
|
||||
description: Optional[str] = None
|
||||
) -> Message:
|
||||
"""
|
||||
@@ -218,7 +257,10 @@ class FileService:
|
||||
file_id: File ID in room_files table
|
||||
filename: Original filename
|
||||
file_type: Type of file (image, document, log)
|
||||
mime_type: MIME type of the file
|
||||
file_size: File size in bytes
|
||||
file_url: Presigned download URL
|
||||
thumbnail_url: Presigned thumbnail URL for images
|
||||
description: Optional description for the file
|
||||
|
||||
Returns:
|
||||
@@ -237,9 +279,15 @@ class FileService:
|
||||
"file_id": file_id,
|
||||
"file_url": file_url,
|
||||
"filename": filename,
|
||||
"file_type": file_type
|
||||
"file_type": file_type,
|
||||
"mime_type": mime_type,
|
||||
"file_size": file_size
|
||||
}
|
||||
|
||||
# Add thumbnail URL for images
|
||||
if thumbnail_url:
|
||||
metadata["thumbnail_url"] = thumbnail_url
|
||||
|
||||
# Use MessageService to create the message
|
||||
return MessageService.create_message(
|
||||
db=db,
|
||||
|
||||
Reference in New Issue
Block a user