Files
LinkDesk/backend/routers/activities.py
T
indigo 9527f06b3f Fix activities endpoints to match established project-access convention
get_project_activities, get_task_activities, and get_recent_activities were
gating on "project member OR admin", incorrectly blocking coordinators,
directors, and developers from projects they weren't explicitly added to as
members. Every other project-scoped router (shots.py, assets.py) only
restricts the artist role this way — everyone else has access regardless of
membership. Brought activities.py in line with that convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 04:18:07 +08:00

244 lines
8.5 KiB
Python

from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from sqlalchemy import desc
from typing import List, Optional
from datetime import datetime, timedelta
from database import get_db
from models.user import User, UserRole
from models.activity import Activity, ActivityType
from models.project import ProjectMember
from schemas.activity import ActivityResponse
from utils.auth import get_current_user
from utils.activity import ActivityService
router = APIRouter(prefix="/activities", tags=["activities"])
@router.get("/project/{project_id}", response_model=List[ActivityResponse])
def get_project_activities(
project_id: int,
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
type_filter: Optional[ActivityType] = None,
days: Optional[int] = Query(None, ge=1, le=90),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get activity feed for a specific project (excludes activities for deleted records)."""
# Only artists are restricted to their explicit project memberships; coordinators,
# directors, developers, and admins have access to all projects (matches shots.py/assets.py).
if current_user.role == UserRole.ARTIST:
member = db.query(ProjectMember).filter(
ProjectMember.project_id == project_id,
ProjectMember.user_id == current_user.id
).first()
if not member:
from fastapi import HTTPException
raise HTTPException(status_code=403, detail="Access denied to this project")
# Use ActivityService to get activities excluding deleted records
activities = ActivityService.get_activities_excluding_deleted(
db=db,
project_id=project_id,
skip=skip,
limit=limit,
type_filter=type_filter,
days=days
)
return activities
@router.get("/task/{task_id}", response_model=List[ActivityResponse])
def get_task_activities(
task_id: int,
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get activity timeline for a specific task (excludes activities for deleted records)."""
from models.task import Task
# Verify user has access to the task and it's not deleted
task = db.query(Task).filter(Task.id == task_id, Task.deleted_at.is_(None)).first()
if not task:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Task not found")
# Only artists are restricted to their explicit project memberships; coordinators,
# directors, developers, and admins have access to all tasks (matches shots.py/assets.py).
if current_user.role == UserRole.ARTIST:
member = db.query(ProjectMember).filter(
ProjectMember.project_id == task.project_id,
ProjectMember.user_id == current_user.id
).first()
if not member:
from fastapi import HTTPException
raise HTTPException(status_code=403, detail="Access denied to this task")
# Use ActivityService to get activities excluding deleted records
activities = ActivityService.get_activities_excluding_deleted(
db=db,
task_id=task_id,
skip=skip,
limit=limit
)
return activities
@router.get("/user/{user_id}", response_model=List[ActivityResponse])
def get_user_activities(
user_id: int,
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
days: Optional[int] = Query(None, ge=1, le=90),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get activity history for a specific user (excludes activities for deleted records)."""
# Users can only view their own activity unless they're admin
if user_id != current_user.id and not current_user.is_admin:
from fastapi import HTTPException
raise HTTPException(status_code=403, detail="Access denied")
# Use ActivityService to get activities excluding deleted records
activities = ActivityService.get_activities_excluding_deleted(
db=db,
user_id=user_id,
skip=skip,
limit=limit,
days=days
)
return activities
@router.get("/recent", response_model=List[ActivityResponse])
def get_recent_activities(
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=50),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get recent activities from all projects the user has access to (excludes activities for deleted records)."""
# Only artists are restricted to their explicit project memberships; coordinators,
# directors, developers, and admins see recent activity across all projects
# (matches shots.py/assets.py).
if current_user.role != UserRole.ARTIST:
activities = ActivityService.get_activities_excluding_deleted(
db=db,
skip=skip,
limit=limit
)
return activities
# Get all projects the artist is a member of
project_ids = db.query(ProjectMember.project_id).filter(
ProjectMember.user_id == current_user.id
).all()
project_ids = [pid[0] for pid in project_ids]
if not project_ids:
return []
all_activities = []
for project_id in project_ids:
activities = ActivityService.get_activities_excluding_deleted(
db=db,
project_id=project_id,
skip=0,
limit=limit * 2 # Get more to account for filtering
)
all_activities.extend(activities)
# Sort by created_at and apply pagination
all_activities.sort(key=lambda x: x.created_at, reverse=True)
return all_activities[skip:skip + limit]
# Admin-only endpoints that include activities for deleted records
@router.get("/admin/project/{project_id}/all", response_model=List[ActivityResponse])
def get_project_activities_including_deleted(
project_id: int,
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
type_filter: Optional[ActivityType] = None,
days: Optional[int] = Query(None, ge=1, le=90),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get all activity feed for a specific project including deleted records (admin only)."""
if not current_user.is_admin:
from fastapi import HTTPException
raise HTTPException(status_code=403, detail="Admin access required")
# Use ActivityService to get all activities including deleted records
activities = ActivityService.get_activities_including_deleted(
db=db,
project_id=project_id,
skip=skip,
limit=limit,
type_filter=type_filter,
days=days
)
return activities
@router.get("/admin/user/{user_id}/all", response_model=List[ActivityResponse])
def get_user_activities_including_deleted(
user_id: int,
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
days: Optional[int] = Query(None, ge=1, le=90),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get all activity history for a specific user including deleted records (admin only)."""
if not current_user.is_admin:
from fastapi import HTTPException
raise HTTPException(status_code=403, detail="Admin access required")
# Use ActivityService to get all activities including deleted records
activities = ActivityService.get_activities_including_deleted(
db=db,
user_id=user_id,
skip=skip,
limit=limit,
days=days
)
return activities
@router.get("/admin/all", response_model=List[ActivityResponse])
def get_all_activities_including_deleted(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
type_filter: Optional[ActivityType] = None,
days: Optional[int] = Query(None, ge=1, le=90),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get all activities including deleted records (admin only)."""
if not current_user.is_admin:
from fastapi import HTTPException
raise HTTPException(status_code=403, detail="Admin access required")
# Use ActivityService to get all activities including deleted records
activities = ActivityService.get_activities_including_deleted(
db=db,
skip=skip,
limit=limit,
type_filter=type_filter,
days=days
)
return activities