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,
"count": len(settings_list),
"settings": {
setting.key: setting.value
setting.key: setting.get_value()
for setting in settings_list
}
}

View File

@@ -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)
}

View File

@@ -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}

View File

@@ -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,

View File

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