from typing import Optional, Dict, Any from sqlalchemy.orm import Session from models.activity import Activity, ActivityType from models.user import User from models.task import Task import logging logger = logging.getLogger(__name__) class ActivityService: """Service for creating and managing activity logs.""" @staticmethod def log_activity( db: Session, type: ActivityType, user_id: int, description: str, project_id: Optional[int] = None, task_id: Optional[int] = None, asset_id: Optional[int] = None, shot_id: Optional[int] = None, submission_id: Optional[int] = None, activity_metadata: Optional[Dict[str, Any]] = None ) -> Optional[Activity]: """Log an activity to the database.""" try: activity = Activity( type=type, user_id=user_id, description=description, project_id=project_id, task_id=task_id, asset_id=asset_id, shot_id=shot_id, submission_id=submission_id, activity_metadata=activity_metadata ) db.add(activity) db.commit() db.refresh(activity) logger.info(f"Activity logged: {type} by user {user_id}") return activity except Exception as e: logger.error(f"Failed to log activity: {str(e)}") db.rollback() return None @staticmethod def log_task_created(db: Session, task: Task, user: User): """Log task creation activity.""" description = f"{user.first_name} {user.last_name} created task '{task.name}'" ActivityService.log_activity( db=db, type=ActivityType.TASK_CREATED, user_id=user.id, description=description, project_id=task.project_id, task_id=task.id, activity_metadata={"task_type": task.task_type, "status": task.status} ) @staticmethod def log_task_assigned(db: Session, task: Task, assigned_user: User, assigner: User): """Log task assignment activity.""" description = f"{assigner.first_name} {assigner.last_name} assigned task '{task.name}' to {assigned_user.first_name} {assigned_user.last_name}" ActivityService.log_activity( db=db, type=ActivityType.TASK_ASSIGNED, user_id=assigner.id, description=description, project_id=task.project_id, task_id=task.id, activity_metadata={"assigned_to": assigned_user.id} ) @staticmethod def log_task_status_changed(db: Session, task: Task, old_status: str, new_status: str, user: User): """Log task status change activity.""" description = f"{user.first_name} {user.last_name} changed task '{task.name}' status from {old_status} to {new_status}" ActivityService.log_activity( db=db, type=ActivityType.TASK_STATUS_CHANGED, user_id=user.id, description=description, project_id=task.project_id, task_id=task.id, activity_metadata={"old_status": old_status, "new_status": new_status} ) @staticmethod def log_submission_created(db: Session, submission, user: User): """Log submission creation activity.""" description = f"{user.first_name} {user.last_name} submitted work for task '{submission.task.name}' (Version {submission.version_number})" ActivityService.log_activity( db=db, type=ActivityType.SUBMISSION_CREATED, user_id=user.id, description=description, project_id=submission.task.project_id, task_id=submission.task_id, submission_id=submission.id, activity_metadata={"version": submission.version_number} ) @staticmethod def log_submission_reviewed(db: Session, submission, review, reviewer: User): """Log submission review activity.""" decision = "approved" if review.decision == "approved" else "requested retakes for" description = f"{reviewer.first_name} {reviewer.last_name} {decision} submission for task '{submission.task.name}'" ActivityService.log_activity( db=db, type=ActivityType.SUBMISSION_REVIEWED, user_id=reviewer.id, description=description, project_id=submission.task.project_id, task_id=submission.task_id, submission_id=submission.id, activity_metadata={"decision": review.decision, "has_feedback": bool(review.feedback)} ) @staticmethod def log_comment_added(db: Session, task: Task, user: User, comment_text: str): """Log comment addition activity.""" preview = comment_text[:50] + "..." if len(comment_text) > 50 else comment_text description = f"{user.first_name} {user.last_name} commented on task '{task.name}': {preview}" ActivityService.log_activity( db=db, type=ActivityType.COMMENT_ADDED, user_id=user.id, description=description, project_id=task.project_id, task_id=task.id ) @staticmethod def log_asset_created(db: Session, asset, user: User): """Log asset creation activity.""" description = f"{user.first_name} {user.last_name} created asset '{asset.name}' ({asset.category})" ActivityService.log_activity( db=db, type=ActivityType.ASSET_CREATED, user_id=user.id, description=description, project_id=asset.project_id, asset_id=asset.id, activity_metadata={"category": asset.category} ) @staticmethod def log_shot_created(db: Session, shot, user: User): """Log shot creation activity.""" description = f"{user.first_name} {user.last_name} created shot '{shot.name}'" ActivityService.log_activity( db=db, type=ActivityType.SHOT_CREATED, user_id=user.id, description=description, project_id=shot.episode.project_id if shot.episode else None, shot_id=shot.id ) @staticmethod def log_project_created(db: Session, project, user: User): """Log project creation activity.""" description = f"{user.first_name} {user.last_name} created project '{project.name}'" ActivityService.log_activity( db=db, type=ActivityType.PROJECT_CREATED, user_id=user.id, description=description, project_id=project.id, activity_metadata={"project_type": project.project_type} ) @staticmethod def log_user_joined_project(db: Session, project, user: User, added_by: User): """Log user joining project activity.""" description = f"{added_by.first_name} {added_by.last_name} added {user.first_name} {user.last_name} to project '{project.name}'" ActivityService.log_activity( db=db, type=ActivityType.USER_JOINED_PROJECT, user_id=added_by.id, description=description, project_id=project.id, activity_metadata={"added_user_id": user.id} ) @staticmethod def log_shot_soft_deletion(db: Session, shot, user: User, deletion_info: Dict[str, Any]): """Log shot soft deletion activity.""" description = f"{user.first_name} {user.last_name} deleted shot '{shot.name}' from episode '{deletion_info.get('episode_name', 'Unknown')}'" ActivityService.log_activity( db=db, type=ActivityType.SHOT_DELETED, user_id=user.id, description=description, project_id=deletion_info.get('project_id'), shot_id=shot.id, activity_metadata={ "shot_name": shot.name, "episode_name": deletion_info.get('episode_name'), "project_name": deletion_info.get('project_name'), "task_count": deletion_info.get('task_count', 0), "submission_count": deletion_info.get('submission_count', 0), "attachment_count": deletion_info.get('attachment_count', 0), "note_count": deletion_info.get('note_count', 0), "review_count": deletion_info.get('review_count', 0), "affected_users": deletion_info.get('affected_users', []) } ) @staticmethod def log_asset_soft_deletion(db: Session, asset, user: User, deletion_info: Dict[str, Any]): """Log asset soft deletion activity.""" description = f"{user.first_name} {user.last_name} deleted asset '{asset.name}' ({asset.category})" ActivityService.log_activity( db=db, type=ActivityType.ASSET_DELETED, user_id=user.id, description=description, project_id=asset.project_id, asset_id=asset.id, activity_metadata={ "asset_name": asset.name, "asset_category": asset.category, "project_name": deletion_info.get('project_name'), "task_count": deletion_info.get('task_count', 0), "submission_count": deletion_info.get('submission_count', 0), "attachment_count": deletion_info.get('attachment_count', 0), "note_count": deletion_info.get('note_count', 0), "review_count": deletion_info.get('review_count', 0), "affected_users": deletion_info.get('affected_users', []) } ) @staticmethod def log_shot_recovery(db: Session, shot, user: User, recovery_info: Dict[str, Any]): """Log shot recovery activity.""" description = f"{user.first_name} {user.last_name} recovered shot '{shot.name}' from episode '{recovery_info.get('episode_name', 'Unknown')}'" ActivityService.log_activity( db=db, type=ActivityType.SHOT_RECOVERED, user_id=user.id, description=description, project_id=recovery_info.get('project_id'), shot_id=shot.id, activity_metadata={ "shot_name": shot.name, "episode_name": recovery_info.get('episode_name'), "project_name": recovery_info.get('project_name'), "recovered_tasks": recovery_info.get('recovered_tasks', 0), "recovered_submissions": recovery_info.get('recovered_submissions', 0), "recovered_attachments": recovery_info.get('recovered_attachments', 0), "recovered_notes": recovery_info.get('recovered_notes', 0), "recovered_reviews": recovery_info.get('recovered_reviews', 0), "deleted_at": recovery_info.get('deleted_at'), "deleted_by": recovery_info.get('deleted_by') } ) @staticmethod def log_asset_recovery(db: Session, asset, user: User, recovery_info: Dict[str, Any]): """Log asset recovery activity.""" description = f"{user.first_name} {user.last_name} recovered asset '{asset.name}' ({asset.category})" ActivityService.log_activity( db=db, type=ActivityType.ASSET_RECOVERED, user_id=user.id, description=description, project_id=asset.project_id, asset_id=asset.id, activity_metadata={ "asset_name": asset.name, "asset_category": asset.category, "project_name": recovery_info.get('project_name'), "recovered_tasks": recovery_info.get('recovered_tasks', 0), "recovered_submissions": recovery_info.get('recovered_submissions', 0), "recovered_attachments": recovery_info.get('recovered_attachments', 0), "recovered_notes": recovery_info.get('recovered_notes', 0), "recovered_reviews": recovery_info.get('recovered_reviews', 0), "deleted_at": recovery_info.get('deleted_at'), "deleted_by": recovery_info.get('deleted_by') } ) @staticmethod def get_activities_excluding_deleted( db: Session, project_id: Optional[int] = None, task_id: Optional[int] = None, user_id: Optional[int] = None, skip: int = 0, limit: int = 50, type_filter: Optional[ActivityType] = None, days: Optional[int] = None ): """Get activities excluding those related to deleted records.""" from sqlalchemy import and_, or_, desc from datetime import datetime, timedelta from models.task import Task from models.shot import Shot from models.asset import Asset from models.submission import Submission query = db.query(Activity) # Base filters if project_id: query = query.filter(Activity.project_id == project_id) if task_id: query = query.filter(Activity.task_id == task_id) if user_id: query = query.filter(Activity.user_id == user_id) if type_filter: query = query.filter(Activity.type == type_filter) if days: cutoff_date = datetime.utcnow() - timedelta(days=days) query = query.filter(Activity.created_at >= cutoff_date) # Exclude activities related to deleted records # For task-related activities, exclude if task is deleted task_subquery = db.query(Task.id).filter(Task.deleted_at.is_(None)).subquery() # For shot-related activities, exclude if shot is deleted shot_subquery = db.query(Shot.id).filter(Shot.deleted_at.is_(None)).subquery() # For asset-related activities, exclude if asset is deleted asset_subquery = db.query(Asset.id).filter(Asset.deleted_at.is_(None)).subquery() # For submission-related activities, exclude if submission is deleted submission_subquery = db.query(Submission.id).filter(Submission.deleted_at.is_(None)).subquery() # Apply exclusion filters query = query.filter( or_( Activity.task_id.is_(None), Activity.task_id.in_(task_subquery) ) ).filter( or_( Activity.shot_id.is_(None), Activity.shot_id.in_(shot_subquery) ) ).filter( or_( Activity.asset_id.is_(None), Activity.asset_id.in_(asset_subquery) ) ).filter( or_( Activity.submission_id.is_(None), Activity.submission_id.in_(submission_subquery) ) ) return query.order_by(desc(Activity.created_at)).offset(skip).limit(limit).all() @staticmethod def get_activities_including_deleted( db: Session, project_id: Optional[int] = None, task_id: Optional[int] = None, user_id: Optional[int] = None, skip: int = 0, limit: int = 50, type_filter: Optional[ActivityType] = None, days: Optional[int] = None ): """Get all activities including those related to deleted records (admin only).""" from sqlalchemy import desc from datetime import datetime, timedelta query = db.query(Activity) # Base filters if project_id: query = query.filter(Activity.project_id == project_id) if task_id: query = query.filter(Activity.task_id == task_id) if user_id: query = query.filter(Activity.user_id == user_id) if type_filter: query = query.filter(Activity.type == type_filter) if days: cutoff_date = datetime.utcnow() - timedelta(days=days) query = query.filter(Activity.created_at >= cutoff_date) return query.order_by(desc(Activity.created_at)).offset(skip).limit(limit).all() # Global activity service instance activity_service = ActivityService()