398 lines
16 KiB
Python
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()
|