from flask import Blueprint, request, jsonify from tinydb import Query from api.utils import send_event_for_current_user, get_validated_user_id from events.types.child_tasks_set import ChildTasksSet from db.db import task_db, child_db from db.child_overrides import delete_overrides_for_entity from events.types.event import Event from events.types.event_types import EventType from events.types.task_modified import TaskModified from models.task import Task chore_api = Blueprint('chore_api', __name__) TASK_TYPE = 'chore' @chore_api.route('/chore/add', methods=['PUT']) def add_chore(): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 data = request.get_json() name = data.get('name') points = data.get('points') image = data.get('image_id', '') if not name or points is None: return jsonify({'error': 'Name and points are required'}), 400 task = Task(name=name, points=points, type=TASK_TYPE, image_id=image, user_id=user_id) task_db.insert(task.to_dict()) send_event_for_current_user(Event(EventType.TASK_MODIFIED.value, TaskModified(task.id, TaskModified.OPERATION_ADD))) return jsonify({'message': f'Chore {name} added.'}), 201 @chore_api.route('/chore/', methods=['GET']) def get_chore(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 TaskQuery = Query() result = task_db.search( (TaskQuery.id == id) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)) & (TaskQuery.type == TASK_TYPE) ) if not result: return jsonify({'error': 'Chore not found'}), 404 return jsonify(result[0]), 200 @chore_api.route('/chore/list', methods=['GET']) def list_chores(): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 TaskQuery = Query() tasks = task_db.search( ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)) & (TaskQuery.type == TASK_TYPE) ) user_tasks = {t['name'].strip().lower(): t for t in tasks if t.get('user_id') == user_id} filtered_tasks = [] for t in tasks: if t.get('user_id') is None and t['name'].strip().lower() in user_tasks: continue filtered_tasks.append(t) def sort_user_then_default(tasks_group): user_created = sorted( [t for t in tasks_group if t.get('user_id') == user_id], key=lambda x: x['name'].lower(), ) default_items = sorted( [t for t in tasks_group if t.get('user_id') is None], key=lambda x: x['name'].lower(), ) return user_created + default_items sorted_tasks = sort_user_then_default(filtered_tasks) return jsonify({'tasks': sorted_tasks}), 200 @chore_api.route('/chore/', methods=['DELETE']) def delete_chore(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 TaskQuery = Query() task = task_db.get((TaskQuery.id == id) & (TaskQuery.type == TASK_TYPE)) if not task: return jsonify({'error': 'Chore not found'}), 404 if task.get('user_id') is None: import logging logging.warning(f"Forbidden delete attempt on system chore: id={id}, by user_id={user_id}") return jsonify({'error': 'System chores cannot be deleted.'}), 403 removed = task_db.remove((TaskQuery.id == id) & (TaskQuery.user_id == user_id)) if removed: deleted_count = delete_overrides_for_entity(id) if deleted_count > 0: import logging logging.info(f"Cascade deleted {deleted_count} overrides for chore {id}") ChildQuery = Query() for child in child_db.all(): child_tasks = child.get('tasks', []) if id in child_tasks: child_tasks.remove(id) child_db.update({'tasks': child_tasks}, ChildQuery.id == child.get('id')) send_event_for_current_user(Event(EventType.CHILD_TASKS_SET.value, ChildTasksSet(id, child_tasks))) send_event_for_current_user(Event(EventType.TASK_MODIFIED.value, TaskModified(id, TaskModified.OPERATION_DELETE))) return jsonify({'message': f'Chore {id} deleted.'}), 200 return jsonify({'error': 'Chore not found'}), 404 @chore_api.route('/chore//edit', methods=['PUT']) def edit_chore(id): user_id = get_validated_user_id() if not user_id: return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401 TaskQuery = Query() existing = task_db.get( (TaskQuery.id == id) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)) & (TaskQuery.type == TASK_TYPE) ) if not existing: return jsonify({'error': 'Chore not found'}), 404 task = Task.from_dict(existing) is_dirty = False data = request.get_json(force=True) or {} if 'name' in data: name = data.get('name', '').strip() if not name: return jsonify({'error': 'Name cannot be empty'}), 400 task.name = name is_dirty = True if 'points' in data: points = data.get('points') if not isinstance(points, int) or points <= 0: return jsonify({'error': 'Points must be a positive integer'}), 400 task.points = points is_dirty = True if 'image_id' in data: task.image_id = data.get('image_id', '') is_dirty = True if not is_dirty: return jsonify({'error': 'No valid fields to update'}), 400 if task.user_id is None: new_task = Task(name=task.name, points=task.points, type=TASK_TYPE, image_id=task.image_id, user_id=user_id) task_db.insert(new_task.to_dict()) send_event_for_current_user(Event(EventType.TASK_MODIFIED.value, TaskModified(new_task.id, TaskModified.OPERATION_ADD))) return jsonify(new_task.to_dict()), 200 task_db.update(task.to_dict(), (TaskQuery.id == id) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None))) send_event_for_current_user(Event(EventType.TASK_MODIFIED.value, TaskModified(id, TaskModified.OPERATION_EDIT))) return jsonify(task.to_dict()), 200