Initial Commit
This commit is contained in:
11
backend_flask/myapp/activity/__init__.py
Normal file
11
backend_flask/myapp/activity/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# myapp/activity/__init__.py
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
# Define the Blueprint instance for the project activity module.
|
||||
bp = Blueprint('activity', __name__, url_prefix='/api/activity')
|
||||
|
||||
# Import the routes module for this blueprint.
|
||||
# This assumes your routes are defined in 'activity_routes.py'.
|
||||
# The import MUST come AFTER 'bp' is defined.
|
||||
from . import activity_routes
|
||||
297
backend_flask/myapp/activity/activity_routes.py
Normal file
297
backend_flask/myapp/activity/activity_routes.py
Normal file
@@ -0,0 +1,297 @@
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user