feat: Implement routines feature with CRUD operations and child assignment
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m0s
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.
This commit is contained in:
185
backend/api/routine_api.py
Normal file
185
backend/api/routine_api.py
Normal file
@@ -0,0 +1,185 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from tinydb import Query
|
||||
|
||||
from api.utils import send_event_for_current_user, get_validated_user_id
|
||||
from db.db import routine_db, child_db, pending_confirmations_db
|
||||
from db.child_overrides import delete_overrides_for_entity
|
||||
from db.routine_items import delete_for_routine
|
||||
from db.routine_schedules import delete_schedules_for_routine
|
||||
from db.routine_extensions import delete_extensions_for_routine
|
||||
from events.types.event import Event
|
||||
from events.types.event_types import EventType
|
||||
from events.types.routine_modified import RoutineModified
|
||||
from events.types.child_routines_set import ChildRoutinesSet
|
||||
from models.routine import Routine
|
||||
|
||||
routine_api = Blueprint('routine_api', __name__)
|
||||
|
||||
|
||||
@routine_api.route('/routine/add', methods=['PUT'])
|
||||
def add_routine():
|
||||
user_id = get_validated_user_id()
|
||||
if not user_id:
|
||||
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
|
||||
|
||||
data = request.get_json() or {}
|
||||
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
|
||||
|
||||
routine = Routine(name=name, points=points, image_id=image, user_id=user_id)
|
||||
routine_db.insert(routine.to_dict())
|
||||
|
||||
send_event_for_current_user(
|
||||
Event(EventType.ROUTINE_MODIFIED.value, RoutineModified(routine.id, RoutineModified.OPERATION_ADD))
|
||||
)
|
||||
return jsonify({'message': f'Routine {name} added.', 'routine': routine.to_dict()}), 201
|
||||
|
||||
|
||||
@routine_api.route('/routine/<id>', methods=['GET'])
|
||||
def get_routine(id):
|
||||
user_id = get_validated_user_id()
|
||||
if not user_id:
|
||||
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
|
||||
|
||||
q = Query()
|
||||
result = routine_db.search((q.id == id) & ((q.user_id == user_id) | (q.user_id == None)))
|
||||
if not result:
|
||||
return jsonify({'error': 'Routine not found'}), 404
|
||||
|
||||
return jsonify(result[0]), 200
|
||||
|
||||
|
||||
@routine_api.route('/routine/list', methods=['GET'])
|
||||
def list_routines():
|
||||
user_id = get_validated_user_id()
|
||||
if not user_id:
|
||||
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
|
||||
|
||||
ids_param = request.args.get('ids')
|
||||
q = Query()
|
||||
routines = routine_db.search((q.user_id == user_id) | (q.user_id == None))
|
||||
|
||||
if ids_param is not None:
|
||||
if ids_param.strip() == '':
|
||||
routines = []
|
||||
else:
|
||||
ids = set(ids_param.split(','))
|
||||
routines = [routine for routine in routines if routine.get('id') in ids]
|
||||
|
||||
user_routines = {r['name'].strip().lower(): r for r in routines if r.get('user_id') == user_id}
|
||||
filtered_routines = []
|
||||
for routine in routines:
|
||||
if routine.get('user_id') is None and routine['name'].strip().lower() in user_routines:
|
||||
continue
|
||||
filtered_routines.append(routine)
|
||||
|
||||
user_created = sorted(
|
||||
[r for r in filtered_routines if r.get('user_id') == user_id],
|
||||
key=lambda x: x['name'].lower(),
|
||||
)
|
||||
default_items = sorted(
|
||||
[r for r in filtered_routines if r.get('user_id') is None],
|
||||
key=lambda x: x['name'].lower(),
|
||||
)
|
||||
|
||||
return jsonify({'routines': user_created + default_items}), 200
|
||||
|
||||
|
||||
@routine_api.route('/routine/<id>/edit', methods=['PUT'])
|
||||
def edit_routine(id):
|
||||
user_id = get_validated_user_id()
|
||||
if not user_id:
|
||||
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
|
||||
|
||||
q = Query()
|
||||
existing = routine_db.get((q.id == id) & ((q.user_id == user_id) | (q.user_id == None)))
|
||||
if not existing:
|
||||
return jsonify({'error': 'Routine not found'}), 404
|
||||
|
||||
routine = Routine.from_dict(existing)
|
||||
data = request.get_json(force=True) or {}
|
||||
is_dirty = False
|
||||
|
||||
if 'name' in data:
|
||||
name = data.get('name', '').strip()
|
||||
if not name:
|
||||
return jsonify({'error': 'Name cannot be empty'}), 400
|
||||
routine.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
|
||||
routine.points = points
|
||||
is_dirty = True
|
||||
|
||||
if 'image_id' in data:
|
||||
routine.image_id = data.get('image_id', '')
|
||||
is_dirty = True
|
||||
|
||||
if not is_dirty:
|
||||
return jsonify({'error': 'No valid fields to update'}), 400
|
||||
|
||||
if routine.user_id is None:
|
||||
new_routine = Routine(name=routine.name, points=routine.points, image_id=routine.image_id, user_id=user_id)
|
||||
routine_db.insert(new_routine.to_dict())
|
||||
send_event_for_current_user(
|
||||
Event(EventType.ROUTINE_MODIFIED.value, RoutineModified(new_routine.id, RoutineModified.OPERATION_ADD))
|
||||
)
|
||||
return jsonify(new_routine.to_dict()), 200
|
||||
|
||||
routine_db.update(routine.to_dict(), q.id == id)
|
||||
send_event_for_current_user(
|
||||
Event(EventType.ROUTINE_MODIFIED.value, RoutineModified(id, RoutineModified.OPERATION_EDIT))
|
||||
)
|
||||
return jsonify(routine.to_dict()), 200
|
||||
|
||||
|
||||
@routine_api.route('/routine/<id>', methods=['DELETE'])
|
||||
def delete_routine(id):
|
||||
user_id = get_validated_user_id()
|
||||
if not user_id:
|
||||
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
|
||||
|
||||
q = Query()
|
||||
routine = routine_db.get(q.id == id)
|
||||
if not routine:
|
||||
return jsonify({'error': 'Routine not found'}), 404
|
||||
|
||||
if routine.get('user_id') is None:
|
||||
return jsonify({'error': 'System routines cannot be deleted.'}), 403
|
||||
|
||||
removed = routine_db.remove((q.id == id) & (q.user_id == user_id))
|
||||
if not removed:
|
||||
return jsonify({'error': 'Routine not found'}), 404
|
||||
|
||||
delete_overrides_for_entity(id)
|
||||
delete_for_routine(id)
|
||||
delete_schedules_for_routine(id)
|
||||
delete_extensions_for_routine(id)
|
||||
|
||||
pending_q = Query()
|
||||
pending_confirmations_db.remove(
|
||||
(pending_q.entity_id == id) & (pending_q.entity_type == 'routine') & (pending_q.user_id == user_id)
|
||||
)
|
||||
|
||||
child_q = Query()
|
||||
children = child_db.search(child_q.user_id == user_id)
|
||||
for child in children:
|
||||
routine_ids = child.get('routines', [])
|
||||
if id in routine_ids:
|
||||
routine_ids = [rid for rid in routine_ids if rid != id]
|
||||
child_db.update({'routines': routine_ids}, child_q.id == child.get('id'))
|
||||
send_event_for_current_user(
|
||||
Event(EventType.CHILD_ROUTINES_SET.value, ChildRoutinesSet(child.get('id'), routine_ids))
|
||||
)
|
||||
|
||||
send_event_for_current_user(
|
||||
Event(EventType.ROUTINE_MODIFIED.value, RoutineModified(id, RoutineModified.OPERATION_DELETE))
|
||||
)
|
||||
return jsonify({'message': f'Routine {id} deleted.'}), 200
|
||||
Reference in New Issue
Block a user