Improve file listing and fix notification metadata field

Backend:
- Optimize file listing for non-superusers with dedicated CRUD methods
- Add get_visible_for_user and count_visible_for_user for efficient queries
- Move /allowed-types/ and /max-size/ routes before /{file_id} for proper matching
- Rename notification 'metadata' field to 'extra_data' for clarity
- Fix settings export to use get_value() method

Frontend:
- Update NotificationItem interface to use extra_data field

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-20 22:27:08 +01:00
parent 724d550599
commit fc605f03c9
5 changed files with 117 additions and 74 deletions

View File

@@ -154,7 +154,7 @@ def export_settings_json(
"exported_by": current_user.username, "exported_by": current_user.username,
"count": len(settings_list), "count": len(settings_list),
"settings": { "settings": {
setting.key: setting.value setting.key: setting.get_value()
for setting in settings_list for setting in settings_list
} }
} }

View File

@@ -137,57 +137,53 @@ def list_files(
# Filter by ownership for non-superusers # Filter by ownership for non-superusers
if not current_user.is_superuser: if not current_user.is_superuser:
if mine_only: if mine_only:
uploaded_by = current_user.id files = crud.file_storage.get_multi(
is_public = None 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: else:
# Show user's files and public files files = crud.file_storage.get_visible_for_user(
own_files = crud.file_storage.get_multi(
db, db,
skip=0, user_id=current_user.id,
limit=1000, # Get all for filtering skip=skip,
uploaded_by=current_user.id 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, db,
skip=0, user_id=current_user.id,
limit=1000, is_public=is_public,
is_public=True 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: else:
uploaded_by = current_user.id if mine_only else None uploaded_by = current_user.id if mine_only else None
files = crud.file_storage.get_multi( files = crud.file_storage.get_multi(
db, db,
skip=skip, skip=skip,
limit=page_size, limit=page_size,
uploaded_by=uploaded_by, uploaded_by=uploaded_by,
is_public=is_public, is_public=is_public,
content_type=content_type content_type=content_type
) )
total = crud.file_storage.count( total = crud.file_storage.count(
db, db,
uploaded_by=uploaded_by, uploaded_by=uploaded_by,
is_public=is_public is_public=is_public,
) content_type=content_type
)
return { return {
"files": [file_to_schema(f) for f in files], "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) @router.get("/{file_id}", response_model=StoredFileSchema)
def get_file( def get_file(
file_id: str, file_id: str,
@@ -352,26 +371,3 @@ def delete_file(
crud.file_storage.soft_delete(db, id=file_id) crud.file_storage.soft_delete(db, id=file_id)
return None 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)
}

View File

@@ -23,12 +23,12 @@ router = APIRouter()
def serialize_notification(db_obj) -> dict: def serialize_notification(db_obj) -> dict:
"""Serialize notification for response.""" """Serialize notification for response."""
metadata = None extra_data = None
if db_obj.metadata: if db_obj.extra_data:
try: try:
metadata = json.loads(db_obj.metadata) extra_data = json.loads(db_obj.extra_data)
except json.JSONDecodeError: except json.JSONDecodeError:
metadata = None extra_data = None
return { return {
"id": db_obj.id, "id": db_obj.id,
@@ -37,7 +37,7 @@ def serialize_notification(db_obj) -> dict:
"message": db_obj.message, "message": db_obj.message,
"type": db_obj.type, "type": db_obj.type,
"link": db_obj.link, "link": db_obj.link,
"metadata": metadata, "extra_data": extra_data,
"is_read": db_obj.is_read, "is_read": db_obj.is_read,
"created_at": db_obj.created_at, "created_at": db_obj.created_at,
"read_at": db_obj.read_at "read_at": db_obj.read_at
@@ -239,6 +239,6 @@ def broadcast_notification(
message=notification_in.message, message=notification_in.message,
type=notification_in.type, type=notification_in.type,
link=notification_in.link, link=notification_in.link,
metadata=notification_in.metadata extra_data=notification_in.extra_data
) )
return {"sent_to": count} return {"sent_to": count}

View File

@@ -9,6 +9,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Optional, List, BinaryIO from typing import Optional, List, BinaryIO
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import or_
from app.models.file import StoredFile from app.models.file import StoredFile
from app.schemas.file import FileCreate, FileUpdate, ALLOWED_CONTENT_TYPES, MAX_FILE_SIZE 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() 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( def count(
self, self,
db: Session, db: Session,
*, *,
uploaded_by: Optional[str] = None, uploaded_by: Optional[str] = None,
is_public: Optional[bool] = None is_public: Optional[bool] = None,
content_type: Optional[str] = None
) -> int: ) -> int:
"""Count files with optional filtering.""" """Count files with optional filtering."""
query = db.query(StoredFile).filter(StoredFile.is_deleted == False) query = db.query(StoredFile).filter(StoredFile.is_deleted == False)
@@ -146,9 +171,31 @@ class CRUDFile:
query = query.filter(StoredFile.uploaded_by == uploaded_by) query = query.filter(StoredFile.uploaded_by == uploaded_by)
if is_public is not None: if is_public is not None:
query = query.filter(StoredFile.is_public == is_public) query = query.filter(StoredFile.is_public == is_public)
if content_type:
query = query.filter(StoredFile.content_type.like(f"{content_type}%"))
return query.count() 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( def create(
self, self,
db: Session, db: Session,

View File

@@ -244,7 +244,7 @@ export interface NotificationItem {
message: string | null; message: string | null;
type: 'info' | 'success' | 'warning' | 'error' | 'system'; type: 'info' | 'success' | 'warning' | 'error' | 'system';
link: string | null; link: string | null;
metadata: Record<string, unknown> | null; extra_data: Record<string, unknown> | null;
is_read: boolean; is_read: boolean;
created_at: string; created_at: string;
read_at: string | null; read_at: string | null;