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:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user