This commit is contained in:
beabigegg
2025-08-29 19:02:19 +08:00
parent b0c86302ff
commit f3f2b7d596
17 changed files with 1632 additions and 157 deletions

View File

@@ -49,12 +49,13 @@ def get_todos():
query = query.join(TodoItemFollower).filter(
TodoItemFollower.ad_account == identity
)
else: # all
else: # all - show todos user can view (public + private with access)
query = query.filter(
or_(
TodoItem.creator_ad == identity,
TodoItem.responsible_users.any(TodoItemResponsible.ad_account == identity),
TodoItem.followers.any(TodoItemFollower.ad_account == identity)
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
)
)
@@ -157,7 +158,9 @@ def create_todo():
creator_ad=identity,
creator_display_name=claims.get('display_name', identity),
creator_email=claims.get('email', ''),
starred=data.get('starred', False)
starred=data.get('starred', False),
is_public=data.get('is_public', False),
tags=data.get('tags', [])
)
db.session.add(todo)
@@ -262,6 +265,14 @@ def update_todo(todo_id):
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
@@ -706,4 +717,228 @@ def star_todo(todo_id):
except Exception as e:
db.session.rollback()
logger.error(f"Star todo error: {str(e)}")
return jsonify({'error': 'Failed to star todo'}), 500
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)
# Log audit
audit = TodoAuditLog(
actor_ad=identity,
todo_id=todo_id,
action='FOLLOW',
detail={'follower': identity}
)
db.session.add(audit)
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)
# Log audit
audit = TodoAuditLog(
actor_ad=identity,
todo_id=todo_id,
action='UNFOLLOW',
detail={'follower': identity}
)
db.session.add(audit)
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