diff --git a/backend/app/api/v1/export.py b/backend/app/api/v1/export.py index 0fb0097..0234e4a 100644 --- a/backend/app/api/v1/export.py +++ b/backend/app/api/v1/export.py @@ -154,7 +154,7 @@ def export_settings_json( "exported_by": current_user.username, "count": len(settings_list), "settings": { - setting.key: setting.value + setting.key: setting.get_value() for setting in settings_list } } diff --git a/backend/app/api/v1/files.py b/backend/app/api/v1/files.py index 890fb67..51a36c2 100644 --- a/backend/app/api/v1/files.py +++ b/backend/app/api/v1/files.py @@ -137,57 +137,53 @@ def list_files( # Filter by ownership for non-superusers if not current_user.is_superuser: if mine_only: - uploaded_by = current_user.id - is_public = None + files = crud.file_storage.get_multi( + db, + skip=skip, + limit=page_size, + uploaded_by=current_user.id, + is_public=is_public, + content_type=content_type + ) + total = crud.file_storage.count( + db, + uploaded_by=current_user.id, + is_public=is_public, + content_type=content_type + ) else: - # Show user's files and public files - own_files = crud.file_storage.get_multi( + files = crud.file_storage.get_visible_for_user( db, - skip=0, - limit=1000, # Get all for filtering - uploaded_by=current_user.id + user_id=current_user.id, + skip=skip, + limit=page_size, + is_public=is_public, + content_type=content_type ) - public_files = crud.file_storage.get_multi( + total = crud.file_storage.count_visible_for_user( db, - skip=0, - limit=1000, - is_public=True + user_id=current_user.id, + is_public=is_public, + content_type=content_type ) - # Combine and deduplicate - all_files = {f.id: f for f in own_files} - all_files.update({f.id: f for f in public_files}) - files_list = list(all_files.values()) - # Sort by created_at desc - files_list.sort(key=lambda x: x.created_at, reverse=True) - # Paginate - total = len(files_list) - files = files_list[skip:skip + page_size] - - return { - "files": [file_to_schema(f) for f in files], - "total": total, - "page": page, - "page_size": page_size - } - - uploaded_by = current_user.id if mine_only else None else: uploaded_by = current_user.id if mine_only else None - files = crud.file_storage.get_multi( - db, - skip=skip, - limit=page_size, - uploaded_by=uploaded_by, - is_public=is_public, - content_type=content_type - ) + files = crud.file_storage.get_multi( + db, + skip=skip, + limit=page_size, + uploaded_by=uploaded_by, + is_public=is_public, + content_type=content_type + ) - total = crud.file_storage.count( - db, - uploaded_by=uploaded_by, - is_public=is_public - ) + total = crud.file_storage.count( + db, + uploaded_by=uploaded_by, + is_public=is_public, + content_type=content_type + ) return { "files": [file_to_schema(f) for f in files], @@ -197,6 +193,29 @@ def list_files( } +@router.get("/allowed-types/", response_model=List[str]) +def get_allowed_types( + current_user: User = Depends(get_current_user), +): + """ + Get list of allowed file types for upload. + """ + return ALLOWED_CONTENT_TYPES + + +@router.get("/max-size/", response_model=dict) +def get_max_size( + current_user: User = Depends(get_current_user), +): + """ + Get maximum allowed file size. + """ + return { + "max_size_bytes": MAX_FILE_SIZE, + "max_size_mb": MAX_FILE_SIZE / (1024 * 1024) + } + + @router.get("/{file_id}", response_model=StoredFileSchema) def get_file( file_id: str, @@ -352,26 +371,3 @@ def delete_file( crud.file_storage.soft_delete(db, id=file_id) return None - - -@router.get("/allowed-types/", response_model=List[str]) -def get_allowed_types( - current_user: User = Depends(get_current_user), -): - """ - Get list of allowed file types for upload. - """ - return ALLOWED_CONTENT_TYPES - - -@router.get("/max-size/", response_model=dict) -def get_max_size( - current_user: User = Depends(get_current_user), -): - """ - Get maximum allowed file size. - """ - return { - "max_size_bytes": MAX_FILE_SIZE, - "max_size_mb": MAX_FILE_SIZE / (1024 * 1024) - } diff --git a/backend/app/api/v1/notifications.py b/backend/app/api/v1/notifications.py index b97ed46..d5b2b8f 100644 --- a/backend/app/api/v1/notifications.py +++ b/backend/app/api/v1/notifications.py @@ -23,12 +23,12 @@ router = APIRouter() def serialize_notification(db_obj) -> dict: """Serialize notification for response.""" - metadata = None - if db_obj.metadata: + extra_data = None + if db_obj.extra_data: try: - metadata = json.loads(db_obj.metadata) + extra_data = json.loads(db_obj.extra_data) except json.JSONDecodeError: - metadata = None + extra_data = None return { "id": db_obj.id, @@ -37,7 +37,7 @@ def serialize_notification(db_obj) -> dict: "message": db_obj.message, "type": db_obj.type, "link": db_obj.link, - "metadata": metadata, + "extra_data": extra_data, "is_read": db_obj.is_read, "created_at": db_obj.created_at, "read_at": db_obj.read_at @@ -239,6 +239,6 @@ def broadcast_notification( message=notification_in.message, type=notification_in.type, link=notification_in.link, - metadata=notification_in.metadata + extra_data=notification_in.extra_data ) return {"sent_to": count} diff --git a/backend/app/crud/file.py b/backend/app/crud/file.py index a468f87..230f8ca 100644 --- a/backend/app/crud/file.py +++ b/backend/app/crud/file.py @@ -9,6 +9,7 @@ from datetime import datetime from pathlib import Path from typing import Optional, List, BinaryIO from sqlalchemy.orm import Session +from sqlalchemy import or_ from app.models.file import StoredFile from app.schemas.file import FileCreate, FileUpdate, ALLOWED_CONTENT_TYPES, MAX_FILE_SIZE @@ -132,12 +133,36 @@ class CRUDFile: return query.order_by(StoredFile.created_at.desc()).offset(skip).limit(limit).all() + def get_visible_for_user( + self, + db: Session, + *, + user_id: str, + skip: int = 0, + limit: int = 100, + is_public: Optional[bool] = None, + content_type: Optional[str] = None + ) -> List[StoredFile]: + """Get files visible to a user (own + public) with optional filtering.""" + query = db.query(StoredFile).filter( + StoredFile.is_deleted == False, + or_(StoredFile.uploaded_by == user_id, StoredFile.is_public == True) + ) + + if is_public is not None: + query = query.filter(StoredFile.is_public == is_public) + if content_type: + query = query.filter(StoredFile.content_type.like(f"{content_type}%")) + + return query.order_by(StoredFile.created_at.desc()).offset(skip).limit(limit).all() + def count( self, db: Session, *, uploaded_by: Optional[str] = None, - is_public: Optional[bool] = None + is_public: Optional[bool] = None, + content_type: Optional[str] = None ) -> int: """Count files with optional filtering.""" query = db.query(StoredFile).filter(StoredFile.is_deleted == False) @@ -146,9 +171,31 @@ class CRUDFile: query = query.filter(StoredFile.uploaded_by == uploaded_by) if is_public is not None: query = query.filter(StoredFile.is_public == is_public) + if content_type: + query = query.filter(StoredFile.content_type.like(f"{content_type}%")) return query.count() + def count_visible_for_user( + self, + db: Session, + *, + user_id: str, + is_public: Optional[bool] = None, + content_type: Optional[str] = None + ) -> int: + """Count files visible to a user (own + public) with optional filtering.""" + query = db.query(StoredFile).filter( + StoredFile.is_deleted == False, + or_(StoredFile.uploaded_by == user_id, StoredFile.is_public == True) + ) + + if is_public is not None: + query = query.filter(StoredFile.is_public == is_public) + if content_type: + query = query.filter(StoredFile.content_type.like(f"{content_type}%")) + + return query.count() def create( self, db: Session, diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 642c863..f144624 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -244,7 +244,7 @@ export interface NotificationItem { message: string | null; type: 'info' | 'success' | 'warning' | 'error' | 'system'; link: string | null; - metadata: Record | null; + extra_data: Record | null; is_read: boolean; created_at: string; read_at: string | null;