298 lines
14 KiB
Python
298 lines
14 KiB
Python
# myapp/activity/activity_routes.py
|
|
|
|
import datetime
|
|
import logging
|
|
from flask import request, jsonify, current_app, has_app_context # Flask utilities
|
|
from bson.objectid import ObjectId, InvalidId # For MongoDB ObjectIds
|
|
from functools import wraps # Import wraps for dummy decorator
|
|
|
|
# --- Local Blueprint Import ---
|
|
from . import bp # Import the 'bp' instance defined in the local __init__.py
|
|
|
|
# --- Shared Extensions and Utilities Imports ---
|
|
try:
|
|
from ..extensions import mongo # Import the initialized PyMongo instance
|
|
from ..utils import token_required # Import the authentication decorator
|
|
except ImportError:
|
|
# Fallback or error handling if imports fail
|
|
print("Warning: Could not import mongo or token_required in activity/activity_routes.py.")
|
|
mongo = None
|
|
# Define a dummy decorator if token_required is missing
|
|
def token_required(f):
|
|
@wraps(f) # Use wraps for better introspection
|
|
def wrapper(*args, **kwargs):
|
|
print("ERROR: token_required decorator is not available!")
|
|
return jsonify({"message": "Server configuration error: Missing authentication utility."}), 500
|
|
return wrapper
|
|
|
|
# --- Schema Imports ---
|
|
try:
|
|
# Import the relevant schemas defined in schemas.py
|
|
from ..schemas import ActivityCreateSchema, ActivitySchema
|
|
from marshmallow import ValidationError
|
|
except ImportError:
|
|
print("Warning: Could not import Activity schemas or ValidationError in activity/activity_routes.py.")
|
|
ActivityCreateSchema = None
|
|
ActivitySchema = None
|
|
ValidationError = None # Define ValidationError as None if import fails
|
|
|
|
# --- Helper to get logger safely ---
|
|
def _get_logger():
|
|
if has_app_context():
|
|
return current_app.logger
|
|
return logging.getLogger(__name__)
|
|
|
|
# Note: Routes use paths relative to the '/api/activity' prefix defined in __init__.py.
|
|
|
|
@bp.route('/', methods=['POST']) # Path relative to blueprint prefix
|
|
@token_required
|
|
def create_activity(current_user):
|
|
"""
|
|
Create a new project activity log entry.
|
|
Uses ActivityCreateSchema for input validation.
|
|
Expects 'projectId', 'activityType', and optional 'message' in JSON payload.
|
|
Verifies user has access to the project.
|
|
"""
|
|
logger = _get_logger()
|
|
# Validate user object from token
|
|
if not current_user or not current_user.get("_id"):
|
|
logger.error("Invalid current_user object received in create_activity")
|
|
return jsonify({"message": "Internal authorization error."}), 500
|
|
try:
|
|
user_id = ObjectId(current_user["_id"])
|
|
except (InvalidId, TypeError) as e:
|
|
logger.error(f"User ID conversion error in create_activity: {e}")
|
|
return jsonify({"message": "Invalid user ID format in token."}), 400
|
|
|
|
# Check dependencies
|
|
if not mongo: return jsonify({"message": "Database connection not available."}), 500
|
|
if not ActivityCreateSchema or not ValidationError:
|
|
return jsonify({"message": "Server configuration error: Schema unavailable."}), 500
|
|
|
|
# Get and validate JSON data using the schema
|
|
json_data = request.get_json() or {}
|
|
schema = ActivityCreateSchema()
|
|
try:
|
|
validated_data = schema.load(json_data)
|
|
except ValidationError as err:
|
|
logger.warning(f"Create activity validation failed: {err.messages}")
|
|
# Return validation errors from Marshmallow
|
|
return jsonify(err.messages), 422 # 422 Unprocessable Entity is appropriate
|
|
|
|
# Extract validated data
|
|
project_id_str = validated_data['projectId'] # Already validated as ObjectId string by schema if validator is used
|
|
activity_type = validated_data['activityType']
|
|
message = validated_data.get('message', "") # Get optional message
|
|
|
|
try:
|
|
# Convert projectId string to ObjectId (schema validator should ensure format)
|
|
try:
|
|
project_obj_id = ObjectId(project_id_str)
|
|
except InvalidId:
|
|
# This should ideally be caught by schema validation if using _validate_object_id
|
|
logger.error(f"Schema validation passed but ObjectId conversion failed for: {project_id_str}")
|
|
return jsonify({"message": "Invalid projectId format despite schema validation."}), 400
|
|
|
|
# --- Verify Project Access ---
|
|
db = mongo.db
|
|
project = db.projects.find_one({"_id": project_obj_id}, {"ownerId": 1, "collaborators": 1})
|
|
if not project:
|
|
return jsonify({"message": "Project not found."}), 404 # 404 Not Found
|
|
|
|
owner_id = project.get("ownerId")
|
|
collaborators = project.get("collaborators", [])
|
|
if owner_id != user_id and user_id not in collaborators:
|
|
# 403 Forbidden - authenticated but not authorized for this project
|
|
return jsonify({"message": "You do not have access to this project."}), 403
|
|
|
|
# --- Prepare and Insert Activity Log ---
|
|
now = datetime.datetime.now(datetime.timezone.utc) # Use timezone-aware UTC time
|
|
doc = {
|
|
"projectId": project_obj_id,
|
|
"userId": user_id, # Store the user who performed the activity
|
|
"activityType": activity_type,
|
|
"message": message,
|
|
"createdAt": now
|
|
# No updatedAt for activity logs usually
|
|
}
|
|
result = db.project_activity.insert_one(doc)
|
|
|
|
# Return success response with the ID of the new log entry
|
|
return jsonify({
|
|
"message": "Activity log created successfully.",
|
|
"activity_id": str(result.inserted_id) # Convert ObjectId to string
|
|
}), 201 # 201 Created status code
|
|
|
|
except KeyError: # Should be caught by token_required or initial check
|
|
logger.error(f"User ID (_id) not found in token payload for create_activity.")
|
|
return jsonify({"message": "Authentication token is invalid or missing user ID."}), 401
|
|
except AttributeError:
|
|
logger.error("PyMongo extension not initialized or attached correctly.")
|
|
return jsonify({"message": "Database configuration error."}), 500
|
|
except Exception as e:
|
|
logger.error(f"Error creating activity for user {current_user.get('_id', 'UNKNOWN')}: {e}", exc_info=True)
|
|
return jsonify({"message": "An error occurred while creating the activity log."}), 500
|
|
|
|
|
|
@bp.route('/', methods=['GET']) # Path relative to blueprint prefix
|
|
@token_required
|
|
def list_activity_logs(current_user):
|
|
"""
|
|
List activity logs for a specific project.
|
|
Uses ActivitySchema for output serialization.
|
|
Requires 'projectId' as a query parameter.
|
|
Supports 'limit' and 'offset' for pagination.
|
|
Verifies user has access to the project.
|
|
"""
|
|
logger = _get_logger()
|
|
# Validate user object from token
|
|
if not current_user or not current_user.get("_id"):
|
|
return jsonify({"message": "Internal authorization error."}), 500
|
|
try:
|
|
user_id = ObjectId(current_user["_id"])
|
|
except (InvalidId, TypeError) as e:
|
|
logger.error(f"User ID conversion error in list_activity_logs: {e}")
|
|
return jsonify({"message": "Invalid user ID format in token."}), 400
|
|
|
|
# Check dependencies
|
|
if not mongo: return jsonify({"message": "Database connection not available."}), 500
|
|
if not ActivitySchema: return jsonify({"message": "Server configuration error: Schema unavailable."}), 500
|
|
|
|
try:
|
|
# Get query parameters
|
|
project_id_str = request.args.get("projectId", "")
|
|
limit_str = request.args.get("limit", "20") # Default limit 20
|
|
offset_str = request.args.get("offset", "0") # Default offset 0
|
|
|
|
# Validate and parse pagination parameters
|
|
try:
|
|
limit = max(int(limit_str), 1) # Ensure limit is at least 1
|
|
except ValueError:
|
|
limit = 20 # Default on parsing error
|
|
try:
|
|
offset = max(int(offset_str), 0) # Ensure offset is non-negative
|
|
except ValueError:
|
|
offset = 0 # Default on parsing error
|
|
|
|
# Project ID is required for listing logs
|
|
if not project_id_str:
|
|
return jsonify({"message": "Query parameter 'projectId' is required to list logs."}), 400
|
|
|
|
# Convert projectId string to ObjectId
|
|
try:
|
|
project_obj_id = ObjectId(project_id_str)
|
|
except InvalidId:
|
|
return jsonify({"message": "Invalid projectId format in query parameter."}), 400
|
|
|
|
# --- Verify Project Access ---
|
|
db = mongo.db
|
|
project = db.projects.find_one({"_id": project_obj_id}, {"ownerId": 1, "collaborators": 1})
|
|
if not project:
|
|
return jsonify({"message": "Project not found."}), 404
|
|
|
|
owner_id = project.get("ownerId")
|
|
collaborators = project.get("collaborators", [])
|
|
if owner_id != user_id and user_id not in collaborators:
|
|
return jsonify({"message": "You do not have access to this project's activity logs."}), 403
|
|
|
|
# --- Fetch Activity Logs ---
|
|
cursor = db.project_activity.find(
|
|
{"projectId": project_obj_id}
|
|
).sort("createdAt", -1).skip(offset).limit(limit) # Sort newest first
|
|
|
|
# Convert cursor to list for serialization
|
|
activity_docs = list(cursor)
|
|
|
|
# --- Serialize results using the schema ---
|
|
# Instantiate schema for multiple documents
|
|
output_schema = ActivitySchema(many=True)
|
|
# Use dump() to serialize the list of documents
|
|
# Schema handles ObjectId and datetime conversion
|
|
serialized_result = output_schema.dump(activity_docs)
|
|
|
|
# Return the serialized list of activity logs
|
|
return jsonify({"activity_logs": serialized_result}), 200
|
|
|
|
except KeyError: # Should be caught by token_required or initial check
|
|
logger.error(f"User ID (_id) not found in token payload for list_activity_logs.")
|
|
return jsonify({"message": "Authentication token is invalid or missing user ID."}), 401
|
|
except AttributeError:
|
|
logger.error("PyMongo extension not initialized or attached correctly.")
|
|
return jsonify({"message": "Database configuration error."}), 500
|
|
except Exception as e:
|
|
logger.error(f"Error listing activity logs for user {current_user.get('_id', 'UNKNOWN')}: {e}", exc_info=True)
|
|
return jsonify({"message": "An error occurred while listing activity logs."}), 500
|
|
|
|
|
|
@bp.route('/<string:activity_id>', methods=['DELETE']) # Path relative to blueprint prefix
|
|
@token_required
|
|
def delete_activity_log(current_user, activity_id):
|
|
"""
|
|
Delete a specific activity log entry by its ID.
|
|
Requires the authenticated user to be the owner of the associated project.
|
|
(No schema needed for input/output here)
|
|
"""
|
|
logger = _get_logger()
|
|
# Validate user object from token
|
|
if not current_user or not current_user.get("_id"):
|
|
return jsonify({"message": "Internal authorization error."}), 500
|
|
try:
|
|
user_id = ObjectId(current_user["_id"])
|
|
except (InvalidId, TypeError) as e:
|
|
logger.error(f"User ID conversion error in delete_activity_log: {e}")
|
|
return jsonify({"message": "Invalid user ID format in token."}), 400
|
|
|
|
if not mongo: return jsonify({"message": "Database connection not available."}), 500
|
|
|
|
try:
|
|
# Validate activity_id format
|
|
try:
|
|
obj_activity_id = ObjectId(activity_id)
|
|
except InvalidId:
|
|
return jsonify({"message": "Invalid activity log ID format."}), 400
|
|
|
|
db = mongo.db
|
|
# --- Find Log and Verify Ownership via Project ---
|
|
# Fetch projectId to check ownership
|
|
activity_doc = db.project_activity.find_one({"_id": obj_activity_id}, {"projectId": 1})
|
|
if not activity_doc:
|
|
return jsonify({"message": "Activity log not found."}), 404
|
|
|
|
project_id = activity_doc.get("projectId")
|
|
if not project_id or not isinstance(project_id, ObjectId):
|
|
logger.error(f"Activity log {activity_id} is missing valid projectId.")
|
|
return jsonify({"message": "Cannot verify ownership due to missing project reference."}), 500
|
|
|
|
project = db.projects.find_one({"_id": project_id}, {"ownerId": 1})
|
|
if not project:
|
|
logger.warning(f"Project {project_id} associated with activity log {activity_id} not found.")
|
|
# Even if project is gone, maybe allow deleting orphan log? Or deny? Deny for safety.
|
|
return jsonify({"message": "Associated project not found."}), 404
|
|
|
|
# Verify ownership (only project owner can delete logs in this implementation)
|
|
owner_id = project.get("ownerId")
|
|
if owner_id != user_id:
|
|
return jsonify({"message": "You do not have permission to delete this activity log (must be project owner)."}), 403
|
|
|
|
# --- Perform Deletion ---
|
|
result = db.project_activity.delete_one({"_id": obj_activity_id})
|
|
|
|
# --- Return Response ---
|
|
if result.deleted_count == 1:
|
|
return jsonify({"message": "Activity log deleted successfully."}), 200
|
|
else:
|
|
# Log was found but delete failed
|
|
logger.warning(f"Activity log {activity_id} found but delete_one removed 0 documents.")
|
|
return jsonify({"message": "Failed to delete activity log (already deleted?)."}), 404
|
|
|
|
except KeyError: # Should be caught by token_required or initial check
|
|
logger.error(f"User ID (_id) not found in token payload for delete_activity_log.")
|
|
return jsonify({"message": "Authentication token is invalid or missing user ID."}), 401
|
|
except AttributeError:
|
|
logger.error("PyMongo extension not initialized or attached correctly.")
|
|
return jsonify({"message": "Database configuration error."}), 500
|
|
except Exception as e:
|
|
logger.error(f"Error deleting activity log {activity_id} for user {current_user.get('_id', 'UNKNOWN')}: {e}", exc_info=True)
|
|
return jsonify({"message": "An error occurred while deleting the activity log."}), 500
|
|
|