All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m0s
- 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.
179 lines
7.0 KiB
Python
179 lines
7.0 KiB
Python
from flask import Blueprint, request, jsonify
|
|
from tinydb import Query
|
|
|
|
from api.error_codes import ErrorCodes
|
|
from api.utils import get_validated_user_id, send_event_for_current_user
|
|
from db.db import child_db, pending_confirmations_db
|
|
from db.routine_extensions import add_extension, delete_extension_for_child_routine, get_extension
|
|
from db.routine_schedules import delete_schedule, get_schedule, upsert_schedule
|
|
from events.types.child_routine_confirmation import ChildRoutineConfirmation
|
|
from events.types.event import Event
|
|
from events.types.event_types import EventType
|
|
from events.types.routine_schedule_modified import RoutineScheduleModified
|
|
from events.types.routine_time_extended import RoutineTimeExtended
|
|
from models.routine_extension import RoutineExtension
|
|
from models.routine_schedule import RoutineSchedule
|
|
|
|
routine_schedule_api = Blueprint('routine_schedule_api', __name__)
|
|
|
|
|
|
def _validate_child(child_id: str, user_id: str):
|
|
q = Query()
|
|
result = child_db.search((q.id == child_id) & (q.user_id == user_id))
|
|
return result[0] if result else None
|
|
|
|
|
|
@routine_schedule_api.route('/child/<child_id>/routine/<routine_id>/schedule', methods=['GET'])
|
|
def get_routine_schedule(child_id, routine_id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
if not _validate_child(child_id, user_id):
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
schedule = get_schedule(child_id, routine_id)
|
|
if not schedule:
|
|
return jsonify({'error': 'Schedule not found'}), 404
|
|
|
|
return jsonify(schedule.to_dict()), 200
|
|
|
|
|
|
@routine_schedule_api.route('/child/<child_id>/routine/<routine_id>/schedule', methods=['PUT'])
|
|
def set_routine_schedule(child_id, routine_id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
if not _validate_child(child_id, user_id):
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
data = request.get_json() or {}
|
|
mode = data.get('mode')
|
|
if mode not in ('days', 'interval'):
|
|
return jsonify({'error': 'mode must be "days" or "interval"', 'code': ErrorCodes.INVALID_VALUE}), 400
|
|
|
|
enabled = data.get('enabled', True)
|
|
if not isinstance(enabled, bool):
|
|
return jsonify({'error': 'enabled must be a boolean', 'code': ErrorCodes.INVALID_VALUE}), 400
|
|
|
|
if mode == 'days':
|
|
day_configs = data.get('day_configs', [])
|
|
if not isinstance(day_configs, list):
|
|
return jsonify({'error': 'day_configs must be a list', 'code': ErrorCodes.INVALID_VALUE}), 400
|
|
|
|
schedule = RoutineSchedule(
|
|
child_id=child_id,
|
|
routine_id=routine_id,
|
|
mode='days',
|
|
day_configs=day_configs,
|
|
default_hour=data.get('default_hour', 8),
|
|
default_minute=data.get('default_minute', 0),
|
|
default_has_deadline=data.get('default_has_deadline', True),
|
|
enabled=enabled,
|
|
)
|
|
else:
|
|
interval_days = data.get('interval_days', 2)
|
|
anchor_date = data.get('anchor_date', '')
|
|
interval_has_deadline = data.get('interval_has_deadline', True)
|
|
interval_hour = data.get('interval_hour', 0)
|
|
interval_minute = data.get('interval_minute', 0)
|
|
|
|
if not isinstance(interval_days, int) or not (1 <= interval_days <= 7):
|
|
return jsonify({'error': 'interval_days must be an integer between 1 and 7', 'code': ErrorCodes.INVALID_VALUE}), 400
|
|
|
|
schedule = RoutineSchedule(
|
|
child_id=child_id,
|
|
routine_id=routine_id,
|
|
mode='interval',
|
|
interval_days=interval_days,
|
|
anchor_date=anchor_date,
|
|
interval_has_deadline=interval_has_deadline,
|
|
interval_hour=interval_hour,
|
|
interval_minute=interval_minute,
|
|
enabled=enabled,
|
|
)
|
|
|
|
delete_extension_for_child_routine(child_id, routine_id)
|
|
upsert_schedule(schedule)
|
|
|
|
pending_q = Query()
|
|
pending_routines = pending_confirmations_db.search(
|
|
(pending_q.child_id == child_id) & (pending_q.entity_id == routine_id) &
|
|
(pending_q.entity_type == 'routine') & (pending_q.status == 'pending')
|
|
)
|
|
for _ in pending_routines:
|
|
pending_confirmations_db.remove(
|
|
(pending_q.child_id == child_id) & (pending_q.entity_id == routine_id) &
|
|
(pending_q.entity_type == 'routine') & (pending_q.status == 'pending')
|
|
)
|
|
send_event_for_current_user(
|
|
Event(
|
|
EventType.CHILD_ROUTINE_CONFIRMATION.value,
|
|
ChildRoutineConfirmation(child_id, routine_id, ChildRoutineConfirmation.OPERATION_RESET)
|
|
)
|
|
)
|
|
|
|
send_event_for_current_user(
|
|
Event(
|
|
EventType.ROUTINE_SCHEDULE_MODIFIED.value,
|
|
RoutineScheduleModified(child_id, routine_id, RoutineScheduleModified.OPERATION_SET)
|
|
)
|
|
)
|
|
|
|
return jsonify(schedule.to_dict()), 200
|
|
|
|
|
|
@routine_schedule_api.route('/child/<child_id>/routine/<routine_id>/schedule', methods=['DELETE'])
|
|
def delete_routine_schedule(child_id, routine_id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
if not _validate_child(child_id, user_id):
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
removed = delete_schedule(child_id, routine_id)
|
|
if not removed:
|
|
return jsonify({'error': 'Schedule not found'}), 404
|
|
|
|
send_event_for_current_user(
|
|
Event(
|
|
EventType.ROUTINE_SCHEDULE_MODIFIED.value,
|
|
RoutineScheduleModified(child_id, routine_id, RoutineScheduleModified.OPERATION_DELETED)
|
|
)
|
|
)
|
|
return jsonify({'message': 'Schedule deleted'}), 200
|
|
|
|
|
|
@routine_schedule_api.route('/child/<child_id>/routine/<routine_id>/extend', methods=['POST'])
|
|
def extend_routine_time(child_id, routine_id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': ErrorCodes.UNAUTHORIZED}), 401
|
|
|
|
if not _validate_child(child_id, user_id):
|
|
return jsonify({'error': 'Child not found', 'code': ErrorCodes.CHILD_NOT_FOUND}), 404
|
|
|
|
data = request.get_json() or {}
|
|
date = data.get('date')
|
|
if not date or not isinstance(date, str):
|
|
return jsonify({'error': 'date is required (ISO date string)', 'code': ErrorCodes.MISSING_FIELD}), 400
|
|
|
|
existing = get_extension(child_id, routine_id, date)
|
|
if existing:
|
|
return jsonify({'error': 'Routine already extended for this date', 'code': 'ALREADY_EXTENDED'}), 409
|
|
|
|
delete_extension_for_child_routine(child_id, routine_id)
|
|
extension = RoutineExtension(child_id=child_id, routine_id=routine_id, date=date)
|
|
add_extension(extension)
|
|
|
|
send_event_for_current_user(
|
|
Event(
|
|
EventType.ROUTINE_TIME_EXTENDED.value,
|
|
RoutineTimeExtended(child_id, routine_id)
|
|
)
|
|
)
|
|
|
|
return jsonify(extension.to_dict()), 200
|