Files
chore/backend/api/child_override_api.py
Ryan Kegel eb775ba7d8
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m0s
feat: Implement routines feature with CRUD operations and child assignment
- Add backend routines management with add, get, update, delete, and list functionalities.
- Create models for Routine, RoutineItem, RoutineSchedule, and RoutineExtension.
- Develop event types for routine confirmation and modification.
- Implement frontend components for routine assignment, confirmation dialog, and routine management views.
- Add unit tests for routine API and integration tests for routine CRUD flow.
- Create end-to-end test plan for routines feature covering parent and child interactions.
2026-05-05 09:08:19 -04:00

187 lines
7.4 KiB
Python

from flask import Blueprint, request, jsonify
from tinydb import Query
from api.utils import get_validated_user_id, send_event_for_current_user
from api.error_codes import ErrorCodes
from db.db import child_db, task_db, reward_db, routine_db
from db.child_overrides import (
insert_override,
get_override,
get_overrides_for_child,
delete_override
)
from models.child_override import ChildOverride
from events.types.event import Event
from events.types.event_types import EventType
from events.types.child_override_set import ChildOverrideSetPayload
from events.types.child_override_deleted import ChildOverrideDeletedPayload
import logging
child_override_api = Blueprint('child_override_api', __name__)
logger = logging.getLogger(__name__)
@child_override_api.route('/child/<child_id>/override', methods=['PUT'])
def set_child_override(child_id):
"""
Set or update a custom value for a task/reward for a specific child.
"""
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
# Validate child exists and belongs to user
ChildQuery = Query()
child_result = child_db.search((ChildQuery.id == child_id) & (ChildQuery.user_id == user_id))
if not child_result:
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
child_dict = child_result[0]
# Parse request data
data = request.get_json() or {}
entity_id = data.get('entity_id')
entity_type = data.get('entity_type')
custom_value = data.get('custom_value')
# Validate required fields
if not entity_id:
return jsonify({'error': 'entity_id is required', 'code': ErrorCodes.MISSING_FIELD, 'field': 'entity_id'}), 400
if not entity_type:
return jsonify({'error': 'entity_type is required', 'code': ErrorCodes.MISSING_FIELD, 'field': 'entity_type'}), 400
if custom_value is None:
return jsonify({'error': 'custom_value is required', 'code': ErrorCodes.MISSING_FIELD, 'field': 'custom_value'}), 400
# Validate entity_type
if entity_type not in ['task', 'reward', 'routine']:
return jsonify({'error': 'entity_type must be "task", "reward", or "routine"', 'code': ErrorCodes.INVALID_VALUE, 'field': 'entity_type'}), 400
# Validate custom_value range
if not isinstance(custom_value, int) or custom_value < 0 or custom_value > 10000:
return jsonify({'error': 'custom_value must be an integer between 0 and 10000', 'code': ErrorCodes.INVALID_VALUE, 'field': 'custom_value'}), 400
# Validate entity exists and is assigned to child
if entity_type == 'task':
EntityQuery = Query()
entity_result = task_db.search(
(EntityQuery.id == entity_id) &
((EntityQuery.user_id == user_id) | (EntityQuery.user_id == None))
)
if not entity_result:
return jsonify({'error': 'Task not found', 'code': ErrorCodes.TASK_NOT_FOUND}), 404
# Check if task is assigned to child
assigned_tasks = child_dict.get('tasks', [])
if entity_id not in assigned_tasks:
return jsonify({'error': 'Task not assigned to child', 'code': ErrorCodes.ENTITY_NOT_ASSIGNED}), 404
elif entity_type == 'reward':
EntityQuery = Query()
entity_result = reward_db.search(
(EntityQuery.id == entity_id) &
((EntityQuery.user_id == user_id) | (EntityQuery.user_id == None))
)
if not entity_result:
return jsonify({'error': 'Reward not found', 'code': ErrorCodes.REWARD_NOT_FOUND}), 404
# Check if reward is assigned to child
assigned_rewards = child_dict.get('rewards', [])
if entity_id not in assigned_rewards:
return jsonify({'error': 'Reward not assigned to child', 'code': ErrorCodes.ENTITY_NOT_ASSIGNED}), 404
else: # routine
EntityQuery = Query()
entity_result = routine_db.search(
(EntityQuery.id == entity_id) &
((EntityQuery.user_id == user_id) | (EntityQuery.user_id == None))
)
if not entity_result:
return jsonify({'error': 'Routine not found', 'code': 'ROUTINE_NOT_FOUND'}), 404
assigned_routines = child_dict.get('routines', [])
if entity_id not in assigned_routines:
return jsonify({'error': 'Routine not assigned to child', 'code': ErrorCodes.ENTITY_NOT_ASSIGNED}), 404
# Create and insert override
try:
override = ChildOverride.create_override(
child_id=child_id,
entity_id=entity_id,
entity_type=entity_type,
custom_value=custom_value
)
insert_override(override)
# Send SSE event
resp = send_event_for_current_user(
Event(EventType.CHILD_OVERRIDE_SET.value, ChildOverrideSetPayload(override))
)
if resp:
return resp
return jsonify({'override': override.to_dict()}), 200
except ValueError as e:
return jsonify({'error': str(e), 'code': ErrorCodes.VALIDATION_ERROR}), 400
except Exception as e:
logger.error(f"Error setting override: {e}")
return jsonify({'error': 'Internal server error', 'code': ErrorCodes.INTERNAL_ERROR}), 500
@child_override_api.route('/child/<child_id>/overrides', methods=['GET'])
def get_child_overrides(child_id):
"""
Get all overrides for a specific child.
"""
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
# Validate child exists and belongs to user
ChildQuery = Query()
child_result = child_db.search((ChildQuery.id == child_id) & (ChildQuery.user_id == user_id))
if not child_result:
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
# Get all overrides for child
overrides = get_overrides_for_child(child_id)
return jsonify({'overrides': [o.to_dict() for o in overrides]}), 200
@child_override_api.route('/child/<child_id>/override/<entity_id>', methods=['DELETE'])
def delete_child_override(child_id, entity_id):
"""
Delete an override (reset to default).
"""
user_id = get_validated_user_id()
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
# Validate child exists and belongs to user
ChildQuery = Query()
child_result = child_db.search((ChildQuery.id == child_id) & (ChildQuery.user_id == user_id))
if not child_result:
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
# Get override to determine entity_type for event
override = get_override(child_id, entity_id)
if not override:
return jsonify({'error': 'Override not found', 'code': ErrorCodes.OVERRIDE_NOT_FOUND}), 404
entity_type = override.entity_type
# Delete override
deleted = delete_override(child_id, entity_id)
if not deleted:
return jsonify({'error': 'Override not found', 'code': ErrorCodes.OVERRIDE_NOT_FOUND}), 404
# Send SSE event
resp = send_event_for_current_user(
Event(EventType.CHILD_OVERRIDE_DELETED.value,
ChildOverrideDeletedPayload(child_id, entity_id, entity_type))
)
if resp:
return resp
return jsonify({'message': 'Override deleted'}), 200