LinkDesk/backend/utils/activity.py

398 lines
16 KiB
Python

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()