backup
This commit is contained in:
939
backend/routes/todos.py
Normal file
939
backend/routes/todos.py
Normal file
@@ -0,0 +1,939 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
|
||||
from datetime import datetime, date, timedelta
|
||||
from sqlalchemy import or_, and_
|
||||
from sqlalchemy.orm import selectinload, joinedload
|
||||
from models import (
|
||||
db, TodoItem, TodoItemResponsible, TodoItemFollower,
|
||||
TodoAuditLog, TodoUserPref
|
||||
)
|
||||
from utils.logger import get_logger
|
||||
from utils.ldap_utils import validate_ad_accounts
|
||||
import uuid
|
||||
|
||||
todos_bp = Blueprint('todos', __name__)
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@todos_bp.route('', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_todos():
|
||||
"""Get todos with filtering and pagination"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
|
||||
# Filters
|
||||
status = request.args.get('status')
|
||||
priority = request.args.get('priority')
|
||||
starred = request.args.get('starred', type=bool)
|
||||
due_from = request.args.get('due_from')
|
||||
due_to = request.args.get('due_to')
|
||||
search = request.args.get('search')
|
||||
view_type = request.args.get('view', 'all') # all, created, responsible, following, dashboard
|
||||
|
||||
# Base query with eager loading to prevent N+1 queries
|
||||
query = TodoItem.query.options(
|
||||
joinedload(TodoItem.responsible_users),
|
||||
joinedload(TodoItem.followers)
|
||||
)
|
||||
|
||||
# Apply view type filter
|
||||
if view_type == 'created':
|
||||
query = query.filter(TodoItem.creator_ad == identity)
|
||||
elif view_type == 'responsible':
|
||||
query = query.join(TodoItemResponsible).filter(
|
||||
TodoItemResponsible.ad_account == identity
|
||||
)
|
||||
elif view_type == 'following':
|
||||
query = query.join(TodoItemFollower).filter(
|
||||
TodoItemFollower.ad_account == identity
|
||||
)
|
||||
elif view_type == 'dashboard':
|
||||
# Dashboard view: only user-related todos (excluding other people's public todos)
|
||||
query = query.filter(
|
||||
or_(
|
||||
TodoItem.creator_ad == identity, # Created by user
|
||||
TodoItem.responsible_users.any(TodoItemResponsible.ad_account == identity), # User is responsible
|
||||
TodoItem.followers.any(TodoItemFollower.ad_account == identity) # User is follower
|
||||
)
|
||||
)
|
||||
else: # all - show todos user can view (public + private with access)
|
||||
query = query.filter(
|
||||
or_(
|
||||
TodoItem.is_public == True, # All public todos
|
||||
TodoItem.creator_ad == identity, # Created by user
|
||||
TodoItem.responsible_users.any(TodoItemResponsible.ad_account == identity), # User is responsible
|
||||
TodoItem.followers.any(TodoItemFollower.ad_account == identity) # User is follower
|
||||
)
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if status:
|
||||
query = query.filter(TodoItem.status == status)
|
||||
if priority:
|
||||
query = query.filter(TodoItem.priority == priority)
|
||||
if starred is not None:
|
||||
query = query.filter(TodoItem.starred == starred)
|
||||
if due_from:
|
||||
query = query.filter(TodoItem.due_date >= datetime.strptime(due_from, '%Y-%m-%d').date())
|
||||
if due_to:
|
||||
query = query.filter(TodoItem.due_date <= datetime.strptime(due_to, '%Y-%m-%d').date())
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
TodoItem.title.contains(search),
|
||||
TodoItem.description.contains(search)
|
||||
)
|
||||
)
|
||||
|
||||
# Order by due date and priority (MySQL compatible)
|
||||
query = query.order_by(
|
||||
TodoItem.due_date.asc(),
|
||||
TodoItem.priority.desc(),
|
||||
TodoItem.created_at.desc()
|
||||
)
|
||||
|
||||
# Paginate
|
||||
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
todos = [todo.to_dict() for todo in pagination.items]
|
||||
|
||||
return jsonify({
|
||||
'todos': todos,
|
||||
'total': pagination.total,
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'pages': pagination.pages
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching todos: {str(e)}")
|
||||
return jsonify({'error': 'Failed to fetch todos'}), 500
|
||||
|
||||
@todos_bp.route('/<todo_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_todo(todo_id):
|
||||
"""Get single todo details"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
|
||||
todo = TodoItem.query.options(
|
||||
joinedload(TodoItem.responsible_users),
|
||||
joinedload(TodoItem.followers)
|
||||
).filter_by(id=todo_id).first()
|
||||
if not todo:
|
||||
return jsonify({'error': 'Todo not found'}), 404
|
||||
|
||||
# Check permission
|
||||
if not todo.can_view(identity):
|
||||
return jsonify({'error': 'Access denied'}), 403
|
||||
|
||||
return jsonify(todo.to_dict()), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching todo {todo_id}: {str(e)}")
|
||||
return jsonify({'error': 'Failed to fetch todo'}), 500
|
||||
|
||||
@todos_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_todo():
|
||||
"""Create new todo"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
claims = get_jwt()
|
||||
data = request.get_json()
|
||||
|
||||
# Validate required fields
|
||||
if not data.get('title'):
|
||||
return jsonify({'error': 'Title is required'}), 400
|
||||
|
||||
# Parse due date if provided
|
||||
due_date = None
|
||||
if data.get('due_date'):
|
||||
try:
|
||||
due_date = datetime.strptime(data['due_date'], '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
return jsonify({'error': 'Invalid due date format. Use YYYY-MM-DD'}), 400
|
||||
|
||||
# Create todo
|
||||
todo = TodoItem(
|
||||
id=str(uuid.uuid4()),
|
||||
title=data['title'],
|
||||
description=data.get('description', ''),
|
||||
status=data.get('status', 'NEW'),
|
||||
priority=data.get('priority', 'MEDIUM'),
|
||||
due_date=due_date,
|
||||
creator_ad=identity,
|
||||
creator_display_name=claims.get('display_name', identity),
|
||||
creator_email=claims.get('email', ''),
|
||||
starred=data.get('starred', False),
|
||||
is_public=data.get('is_public', False),
|
||||
tags=data.get('tags', [])
|
||||
)
|
||||
db.session.add(todo)
|
||||
|
||||
# Add responsible users
|
||||
responsible_accounts = data.get('responsible_users', [])
|
||||
if responsible_accounts:
|
||||
valid_accounts = validate_ad_accounts(responsible_accounts)
|
||||
for account in responsible_accounts:
|
||||
if account in valid_accounts:
|
||||
responsible = TodoItemResponsible(
|
||||
todo_id=todo.id,
|
||||
ad_account=account,
|
||||
added_by=identity
|
||||
)
|
||||
db.session.add(responsible)
|
||||
|
||||
# Add followers
|
||||
follower_accounts = data.get('followers', [])
|
||||
if follower_accounts:
|
||||
valid_accounts = validate_ad_accounts(follower_accounts)
|
||||
for account in follower_accounts:
|
||||
if account in valid_accounts:
|
||||
follower = TodoItemFollower(
|
||||
todo_id=todo.id,
|
||||
ad_account=account,
|
||||
added_by=identity
|
||||
)
|
||||
db.session.add(follower)
|
||||
|
||||
# Add audit log
|
||||
audit = TodoAuditLog(
|
||||
actor_ad=identity,
|
||||
todo_id=todo.id,
|
||||
action='CREATE',
|
||||
detail={'title': todo.title, 'due_date': str(due_date) if due_date else None}
|
||||
)
|
||||
db.session.add(audit)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Todo created: {todo.id} by {identity}")
|
||||
return jsonify(todo.to_dict()), 201
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error creating todo: {str(e)}")
|
||||
return jsonify({'error': 'Failed to create todo'}), 500
|
||||
|
||||
@todos_bp.route('/<todo_id>', methods=['PATCH'])
|
||||
@jwt_required()
|
||||
def update_todo(todo_id):
|
||||
"""Update todo"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
data = request.get_json()
|
||||
|
||||
todo = TodoItem.query.filter_by(id=todo_id).first()
|
||||
if not todo:
|
||||
return jsonify({'error': 'Todo not found'}), 404
|
||||
|
||||
# Check permission
|
||||
if not todo.can_edit(identity):
|
||||
return jsonify({'error': 'Access denied'}), 403
|
||||
|
||||
# Track changes for audit
|
||||
changes = {}
|
||||
|
||||
# Update fields
|
||||
if 'title' in data:
|
||||
changes['title'] = {'old': todo.title, 'new': data['title']}
|
||||
todo.title = data['title']
|
||||
|
||||
if 'description' in data:
|
||||
changes['description'] = {'old': todo.description, 'new': data['description']}
|
||||
todo.description = data['description']
|
||||
|
||||
if 'status' in data:
|
||||
changes['status'] = {'old': todo.status, 'new': data['status']}
|
||||
todo.status = data['status']
|
||||
|
||||
# Set completed_at if status is DONE
|
||||
if data['status'] == 'DONE' and not todo.completed_at:
|
||||
todo.completed_at = datetime.utcnow()
|
||||
elif data['status'] != 'DONE':
|
||||
todo.completed_at = None
|
||||
|
||||
if 'priority' in data:
|
||||
changes['priority'] = {'old': todo.priority, 'new': data['priority']}
|
||||
todo.priority = data['priority']
|
||||
|
||||
if 'due_date' in data:
|
||||
old_due = str(todo.due_date) if todo.due_date else None
|
||||
if data['due_date']:
|
||||
todo.due_date = datetime.strptime(data['due_date'], '%Y-%m-%d').date()
|
||||
new_due = data['due_date']
|
||||
else:
|
||||
todo.due_date = None
|
||||
new_due = None
|
||||
changes['due_date'] = {'old': old_due, 'new': new_due}
|
||||
|
||||
if 'starred' in data:
|
||||
changes['starred'] = {'old': todo.starred, 'new': data['starred']}
|
||||
todo.starred = data['starred']
|
||||
|
||||
if 'is_public' in data:
|
||||
changes['is_public'] = {'old': todo.is_public, 'new': data['is_public']}
|
||||
todo.is_public = data['is_public']
|
||||
|
||||
if 'tags' in data:
|
||||
changes['tags'] = {'old': todo.tags, 'new': data['tags']}
|
||||
todo.tags = data['tags']
|
||||
|
||||
# Update responsible users
|
||||
if 'responsible_users' in data:
|
||||
# Remove existing
|
||||
TodoItemResponsible.query.filter_by(todo_id=todo_id).delete()
|
||||
|
||||
# Add new
|
||||
responsible_accounts = data['responsible_users']
|
||||
if responsible_accounts:
|
||||
valid_accounts = validate_ad_accounts(responsible_accounts)
|
||||
for account in responsible_accounts:
|
||||
if account in valid_accounts:
|
||||
responsible = TodoItemResponsible(
|
||||
todo_id=todo.id,
|
||||
ad_account=account,
|
||||
added_by=identity
|
||||
)
|
||||
db.session.add(responsible)
|
||||
|
||||
changes['responsible_users'] = data['responsible_users']
|
||||
|
||||
# Update followers
|
||||
if 'followers' in data:
|
||||
# Remove existing
|
||||
TodoItemFollower.query.filter_by(todo_id=todo_id).delete()
|
||||
|
||||
# Add new
|
||||
follower_accounts = data['followers']
|
||||
if follower_accounts:
|
||||
valid_accounts = validate_ad_accounts(follower_accounts)
|
||||
for account in follower_accounts:
|
||||
if account in valid_accounts:
|
||||
follower = TodoItemFollower(
|
||||
todo_id=todo.id,
|
||||
ad_account=account,
|
||||
added_by=identity
|
||||
)
|
||||
db.session.add(follower)
|
||||
|
||||
changes['followers'] = data['followers']
|
||||
|
||||
# Add audit log
|
||||
if changes:
|
||||
audit = TodoAuditLog(
|
||||
actor_ad=identity,
|
||||
todo_id=todo.id,
|
||||
action='UPDATE',
|
||||
detail=changes
|
||||
)
|
||||
db.session.add(audit)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Todo updated: {todo_id} by {identity}")
|
||||
return jsonify(todo.to_dict()), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error updating todo {todo_id}: {str(e)}")
|
||||
return jsonify({'error': 'Failed to update todo'}), 500
|
||||
|
||||
@todos_bp.route('/<todo_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
def delete_todo(todo_id):
|
||||
"""Delete todo"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
|
||||
todo = TodoItem.query.filter_by(id=todo_id).first()
|
||||
if not todo:
|
||||
return jsonify({'error': 'Todo not found'}), 404
|
||||
|
||||
# Only creator can delete
|
||||
if todo.creator_ad != identity:
|
||||
return jsonify({'error': 'Only creator can delete todo'}), 403
|
||||
|
||||
# Add audit log before deletion
|
||||
audit = TodoAuditLog(
|
||||
actor_ad=identity,
|
||||
todo_id=None, # Will be null after deletion
|
||||
action='DELETE',
|
||||
detail={'title': todo.title, 'deleted_todo_id': todo_id}
|
||||
)
|
||||
db.session.add(audit)
|
||||
|
||||
# Delete todo (cascades will handle related records)
|
||||
db.session.delete(todo)
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Todo deleted: {todo_id} by {identity}")
|
||||
return jsonify({'message': 'Todo deleted successfully'}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error deleting todo {todo_id}: {str(e)}")
|
||||
return jsonify({'error': 'Failed to delete todo'}), 500
|
||||
|
||||
@todos_bp.route('/batch', methods=['PATCH'])
|
||||
@jwt_required()
|
||||
def batch_update_todos():
|
||||
"""Batch update multiple todos"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
data = request.get_json()
|
||||
|
||||
todo_ids = data.get('todo_ids', [])
|
||||
updates = data.get('updates', {})
|
||||
|
||||
if not todo_ids or not updates:
|
||||
return jsonify({'error': 'Todo IDs and updates required'}), 400
|
||||
|
||||
updated_count = 0
|
||||
errors = []
|
||||
|
||||
for todo_id in todo_ids:
|
||||
try:
|
||||
todo = TodoItem.query.filter_by(id=todo_id).first()
|
||||
if not todo:
|
||||
errors.append({'todo_id': todo_id, 'error': 'Not found'})
|
||||
continue
|
||||
|
||||
if not todo.can_edit(identity):
|
||||
errors.append({'todo_id': todo_id, 'error': 'Access denied'})
|
||||
continue
|
||||
|
||||
# Apply updates
|
||||
if 'status' in updates:
|
||||
todo.status = updates['status']
|
||||
if updates['status'] == 'DONE':
|
||||
todo.completed_at = datetime.utcnow()
|
||||
else:
|
||||
todo.completed_at = None
|
||||
|
||||
if 'priority' in updates:
|
||||
todo.priority = updates['priority']
|
||||
|
||||
if 'due_date' in updates:
|
||||
if updates['due_date']:
|
||||
todo.due_date = datetime.strptime(updates['due_date'], '%Y-%m-%d').date()
|
||||
else:
|
||||
todo.due_date = None
|
||||
|
||||
# Add audit log
|
||||
audit = TodoAuditLog(
|
||||
actor_ad=identity,
|
||||
todo_id=todo.id,
|
||||
action='UPDATE',
|
||||
detail={'batch_update': updates}
|
||||
)
|
||||
db.session.add(audit)
|
||||
|
||||
updated_count += 1
|
||||
|
||||
except Exception as e:
|
||||
errors.append({'todo_id': todo_id, 'error': str(e)})
|
||||
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Batch update: {updated_count} todos updated by {identity}")
|
||||
|
||||
return jsonify({
|
||||
'updated': updated_count,
|
||||
'errors': errors
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error in batch update: {str(e)}")
|
||||
return jsonify({'error': 'Batch update failed'}), 500
|
||||
|
||||
@todos_bp.route('/<todo_id>/responsible', methods=['POST'])
|
||||
@jwt_required()
|
||||
def add_responsible_user(todo_id):
|
||||
"""Add responsible user to todo"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'ad_account' not in data:
|
||||
return jsonify({'error': 'AD account is required'}), 400
|
||||
|
||||
ad_account = data['ad_account']
|
||||
|
||||
# Get todo
|
||||
todo = TodoItem.query.filter_by(id=todo_id).first()
|
||||
if not todo:
|
||||
return jsonify({'error': 'Todo not found'}), 404
|
||||
|
||||
# Check permission
|
||||
if not todo.can_edit(identity):
|
||||
return jsonify({'error': 'No permission to edit this todo'}), 403
|
||||
|
||||
# Validate AD account
|
||||
valid_accounts = validate_ad_accounts([ad_account])
|
||||
if ad_account not in valid_accounts:
|
||||
return jsonify({'error': 'Invalid AD account'}), 400
|
||||
|
||||
# Check if already responsible
|
||||
existing = TodoItemResponsible.query.filter_by(
|
||||
todo_id=todo_id, ad_account=ad_account
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
return jsonify({'error': 'User is already responsible for this todo'}), 400
|
||||
|
||||
# Add responsible user
|
||||
responsible = TodoItemResponsible(
|
||||
todo_id=todo_id,
|
||||
ad_account=ad_account,
|
||||
added_by=identity
|
||||
)
|
||||
db.session.add(responsible)
|
||||
|
||||
# Log audit
|
||||
audit = TodoAuditLog(
|
||||
actor_ad=identity,
|
||||
todo_id=todo_id,
|
||||
action='UPDATE',
|
||||
detail={
|
||||
'field': 'responsible_users',
|
||||
'action': 'add',
|
||||
'ad_account': ad_account
|
||||
}
|
||||
)
|
||||
db.session.add(audit)
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Added responsible user {ad_account} to todo {todo_id} by {identity}")
|
||||
return jsonify({'message': 'Responsible user added successfully'}), 201
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Add responsible user error: {str(e)}")
|
||||
return jsonify({'error': 'Failed to add responsible user'}), 500
|
||||
|
||||
@todos_bp.route('/<todo_id>/responsible/<ad_account>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
def remove_responsible_user(todo_id, ad_account):
|
||||
"""Remove responsible user from todo"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
|
||||
# Get todo
|
||||
todo = TodoItem.query.filter_by(id=todo_id).first()
|
||||
if not todo:
|
||||
return jsonify({'error': 'Todo not found'}), 404
|
||||
|
||||
# Check permission
|
||||
if not todo.can_edit(identity):
|
||||
return jsonify({'error': 'No permission to edit this todo'}), 403
|
||||
|
||||
# Find responsible relationship
|
||||
responsible = TodoItemResponsible.query.filter_by(
|
||||
todo_id=todo_id, ad_account=ad_account
|
||||
).first()
|
||||
|
||||
if not responsible:
|
||||
return jsonify({'error': 'User is not responsible for this todo'}), 404
|
||||
|
||||
# Remove responsible user
|
||||
db.session.delete(responsible)
|
||||
|
||||
# Log audit
|
||||
audit = TodoAuditLog(
|
||||
actor_ad=identity,
|
||||
todo_id=todo_id,
|
||||
action='UPDATE',
|
||||
detail={
|
||||
'field': 'responsible_users',
|
||||
'action': 'remove',
|
||||
'ad_account': ad_account
|
||||
}
|
||||
)
|
||||
db.session.add(audit)
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Removed responsible user {ad_account} from todo {todo_id} by {identity}")
|
||||
return jsonify({'message': 'Responsible user removed successfully'}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Remove responsible user error: {str(e)}")
|
||||
return jsonify({'error': 'Failed to remove responsible user'}), 500
|
||||
|
||||
@todos_bp.route('/<todo_id>/followers', methods=['POST'])
|
||||
@jwt_required()
|
||||
def add_follower(todo_id):
|
||||
"""Add follower to todo"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'ad_account' not in data:
|
||||
return jsonify({'error': 'AD account is required'}), 400
|
||||
|
||||
ad_account = data['ad_account']
|
||||
|
||||
# Get todo
|
||||
todo = TodoItem.query.filter_by(id=todo_id).first()
|
||||
if not todo:
|
||||
return jsonify({'error': 'Todo not found'}), 404
|
||||
|
||||
# Check permission (anyone who can view the todo can add followers)
|
||||
if not todo.can_view(identity):
|
||||
return jsonify({'error': 'No permission to view this todo'}), 403
|
||||
|
||||
# Validate AD account
|
||||
valid_accounts = validate_ad_accounts([ad_account])
|
||||
if ad_account not in valid_accounts:
|
||||
return jsonify({'error': 'Invalid AD account'}), 400
|
||||
|
||||
# Check if already following
|
||||
existing = TodoItemFollower.query.filter_by(
|
||||
todo_id=todo_id, ad_account=ad_account
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
return jsonify({'error': 'User is already following this todo'}), 400
|
||||
|
||||
# Add follower
|
||||
follower = TodoItemFollower(
|
||||
todo_id=todo_id,
|
||||
ad_account=ad_account,
|
||||
added_by=identity
|
||||
)
|
||||
db.session.add(follower)
|
||||
|
||||
# Log audit
|
||||
audit = TodoAuditLog(
|
||||
actor_ad=identity,
|
||||
todo_id=todo_id,
|
||||
action='UPDATE',
|
||||
detail={
|
||||
'field': 'followers',
|
||||
'action': 'add',
|
||||
'ad_account': ad_account
|
||||
}
|
||||
)
|
||||
db.session.add(audit)
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Added follower {ad_account} to todo {todo_id} by {identity}")
|
||||
return jsonify({'message': 'Follower added successfully'}), 201
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Add follower error: {str(e)}")
|
||||
return jsonify({'error': 'Failed to add follower'}), 500
|
||||
|
||||
@todos_bp.route('/<todo_id>/followers/<ad_account>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
def remove_follower(todo_id, ad_account):
|
||||
"""Remove follower from todo"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
|
||||
# Get todo
|
||||
todo = TodoItem.query.filter_by(id=todo_id).first()
|
||||
if not todo:
|
||||
return jsonify({'error': 'Todo not found'}), 404
|
||||
|
||||
# Check permission (user can remove themselves or todo editors can remove anyone)
|
||||
if ad_account != identity and not todo.can_edit(identity):
|
||||
return jsonify({'error': 'No permission to remove this follower'}), 403
|
||||
|
||||
# Find follower relationship
|
||||
follower = TodoItemFollower.query.filter_by(
|
||||
todo_id=todo_id, ad_account=ad_account
|
||||
).first()
|
||||
|
||||
if not follower:
|
||||
return jsonify({'error': 'User is not following this todo'}), 404
|
||||
|
||||
# Remove follower
|
||||
db.session.delete(follower)
|
||||
|
||||
# Log audit
|
||||
audit = TodoAuditLog(
|
||||
actor_ad=identity,
|
||||
todo_id=todo_id,
|
||||
action='UPDATE',
|
||||
detail={
|
||||
'field': 'followers',
|
||||
'action': 'remove',
|
||||
'ad_account': ad_account
|
||||
}
|
||||
)
|
||||
db.session.add(audit)
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Removed follower {ad_account} from todo {todo_id} by {identity}")
|
||||
return jsonify({'message': 'Follower removed successfully'}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Remove follower error: {str(e)}")
|
||||
return jsonify({'error': 'Failed to remove follower'}), 500
|
||||
|
||||
@todos_bp.route('/<todo_id>/star', methods=['POST'])
|
||||
@jwt_required()
|
||||
def star_todo(todo_id):
|
||||
"""Star/unstar a todo item"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
|
||||
# Get todo
|
||||
todo = TodoItem.query.filter_by(id=todo_id).first()
|
||||
if not todo:
|
||||
return jsonify({'error': 'Todo not found'}), 404
|
||||
|
||||
# Check permission
|
||||
if not todo.can_view(identity):
|
||||
return jsonify({'error': 'No permission to view this todo'}), 403
|
||||
|
||||
# Only creator can star/unstar
|
||||
if todo.creator_ad != identity:
|
||||
return jsonify({'error': 'Only creator can star/unstar todos'}), 403
|
||||
|
||||
# Toggle star status
|
||||
todo.starred = not todo.starred
|
||||
|
||||
# Log audit
|
||||
audit = TodoAuditLog(
|
||||
actor_ad=identity,
|
||||
todo_id=todo_id,
|
||||
action='UPDATE',
|
||||
detail={
|
||||
'field': 'starred',
|
||||
'value': todo.starred
|
||||
}
|
||||
)
|
||||
db.session.add(audit)
|
||||
db.session.commit()
|
||||
|
||||
action = 'starred' if todo.starred else 'unstarred'
|
||||
logger.info(f"Todo {todo_id} {action} by {identity}")
|
||||
|
||||
return jsonify({
|
||||
'message': f'Todo {action} successfully',
|
||||
'starred': todo.starred
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Star todo error: {str(e)}")
|
||||
return jsonify({'error': 'Failed to star todo'}), 500
|
||||
|
||||
@todos_bp.route('/public', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_public_todos():
|
||||
"""Get all public todos"""
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
|
||||
# Filters for public todos
|
||||
status = request.args.get('status')
|
||||
priority = request.args.get('priority')
|
||||
search = request.args.get('search')
|
||||
tags = request.args.getlist('tags')
|
||||
|
||||
# Query only public todos
|
||||
query = TodoItem.query.filter(TodoItem.is_public == True).options(
|
||||
joinedload(TodoItem.responsible_users),
|
||||
joinedload(TodoItem.followers)
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if status:
|
||||
query = query.filter(TodoItem.status == status)
|
||||
if priority:
|
||||
query = query.filter(TodoItem.priority == priority)
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
TodoItem.title.contains(search),
|
||||
TodoItem.description.contains(search)
|
||||
)
|
||||
)
|
||||
if tags:
|
||||
for tag in tags:
|
||||
query = query.filter(TodoItem.tags.contains(tag))
|
||||
|
||||
# Order by created_at desc
|
||||
query = query.order_by(TodoItem.created_at.desc())
|
||||
|
||||
# Paginate
|
||||
paginated = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
return jsonify({
|
||||
'todos': [todo.to_dict() for todo in paginated.items],
|
||||
'total': paginated.total,
|
||||
'pages': paginated.pages,
|
||||
'current_page': page
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get public todos error: {str(e)}")
|
||||
return jsonify({'error': 'Failed to fetch public todos'}), 500
|
||||
|
||||
@todos_bp.route('/following', methods=['GET'])
|
||||
@jwt_required()
|
||||
def get_following_todos():
|
||||
"""Get todos that user is following"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
|
||||
# Query todos where user is a follower
|
||||
query = TodoItem.query.join(TodoItemFollower).filter(
|
||||
TodoItemFollower.ad_account == identity
|
||||
).options(
|
||||
joinedload(TodoItem.responsible_users),
|
||||
joinedload(TodoItem.followers)
|
||||
)
|
||||
|
||||
# Order by created_at desc
|
||||
query = query.order_by(TodoItem.created_at.desc())
|
||||
|
||||
# Paginate
|
||||
paginated = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
return jsonify({
|
||||
'todos': [todo.to_dict() for todo in paginated.items],
|
||||
'total': paginated.total,
|
||||
'pages': paginated.pages,
|
||||
'current_page': page
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get following todos error: {str(e)}")
|
||||
return jsonify({'error': 'Failed to fetch following todos'}), 500
|
||||
|
||||
@todos_bp.route('/<todo_id>/visibility', methods=['PATCH'])
|
||||
@jwt_required()
|
||||
def update_todo_visibility(todo_id):
|
||||
"""Toggle todo visibility (public/private)"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
|
||||
# Get todo
|
||||
todo = TodoItem.query.get(todo_id)
|
||||
if not todo:
|
||||
return jsonify({'error': 'Todo not found'}), 404
|
||||
|
||||
# Only creator can change visibility
|
||||
if todo.creator_ad != identity:
|
||||
return jsonify({'error': 'Only creator can change visibility'}), 403
|
||||
|
||||
# Toggle visibility
|
||||
data = request.get_json()
|
||||
is_public = data.get('is_public', not todo.is_public)
|
||||
todo.is_public = is_public
|
||||
|
||||
# Log audit
|
||||
audit = TodoAuditLog(
|
||||
actor_ad=identity,
|
||||
todo_id=todo_id,
|
||||
action='UPDATE',
|
||||
detail={
|
||||
'field': 'is_public',
|
||||
'old_value': not is_public,
|
||||
'new_value': is_public
|
||||
}
|
||||
)
|
||||
db.session.add(audit)
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Todo {todo_id} visibility changed to {'public' if is_public else 'private'} by {identity}")
|
||||
|
||||
return jsonify({
|
||||
'message': f'Todo is now {"public" if is_public else "private"}',
|
||||
'is_public': todo.is_public
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Update visibility error: {str(e)}")
|
||||
return jsonify({'error': 'Failed to update visibility'}), 500
|
||||
|
||||
@todos_bp.route('/<todo_id>/follow', methods=['POST'])
|
||||
@jwt_required()
|
||||
def follow_todo(todo_id):
|
||||
"""Follow a public todo"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
|
||||
# Get todo
|
||||
todo = TodoItem.query.get(todo_id)
|
||||
if not todo:
|
||||
return jsonify({'error': 'Todo not found'}), 404
|
||||
|
||||
# Check if todo is public or user has permission
|
||||
if not todo.is_public and not todo.can_edit(identity):
|
||||
return jsonify({'error': 'Cannot follow private todo'}), 403
|
||||
|
||||
# Check if already following
|
||||
existing = TodoItemFollower.query.filter_by(
|
||||
todo_id=todo_id,
|
||||
ad_account=identity
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
return jsonify({'message': 'Already following this todo'}), 200
|
||||
|
||||
# Add follower
|
||||
follower = TodoItemFollower(
|
||||
todo_id=todo_id,
|
||||
ad_account=identity,
|
||||
added_by=identity
|
||||
)
|
||||
db.session.add(follower)
|
||||
|
||||
# Note: Skip audit log for FOLLOW action until ENUM is updated
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"User {identity} followed todo {todo_id}")
|
||||
|
||||
return jsonify({'message': 'Successfully followed todo'}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Follow todo error: {str(e)}")
|
||||
return jsonify({'error': 'Failed to follow todo'}), 500
|
||||
|
||||
@todos_bp.route('/<todo_id>/follow', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
def unfollow_todo(todo_id):
|
||||
"""Unfollow a todo"""
|
||||
try:
|
||||
identity = get_jwt_identity()
|
||||
|
||||
# Get follower record
|
||||
follower = TodoItemFollower.query.filter_by(
|
||||
todo_id=todo_id,
|
||||
ad_account=identity
|
||||
).first()
|
||||
|
||||
if not follower:
|
||||
return jsonify({'message': 'Not following this todo'}), 200
|
||||
|
||||
# Remove follower
|
||||
db.session.delete(follower)
|
||||
|
||||
# Note: Skip audit log for UNFOLLOW action until ENUM is updated
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"User {identity} unfollowed todo {todo_id}")
|
||||
|
||||
return jsonify({'message': 'Successfully unfollowed todo'}), 200
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Unfollow todo error: {str(e)}")
|
||||
return jsonify({'error': 'Failed to unfollow todo'}), 500
|
Reference in New Issue
Block a user