feat: add PendingRewardDialog, RewardConfirmDialog, and TaskConfirmDialog components
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 25s

- Implemented PendingRewardDialog for handling pending reward requests.
- Created RewardConfirmDialog for confirming reward redemption.
- Developed TaskConfirmDialog for task confirmation with child name display.

test: add unit tests for ChildView and ParentView components

- Added comprehensive tests for ChildView including task triggering and SSE event handling.
- Implemented tests for ParentView focusing on override modal and SSE event management.

test: add ScrollingList component tests

- Created tests for ScrollingList to verify item fetching, loading states, and custom item classes.
- Included tests for two-step click interactions and edit button display logic.
- Moved toward hashed passwords.
This commit is contained in:
2026-02-10 20:21:05 -05:00
parent 3dee8b80a2
commit 401c21ad82
45 changed files with 4353 additions and 441 deletions

View File

@@ -10,6 +10,7 @@ from api.reward_status import RewardStatus
from api.utils import send_event_for_current_user
from db.db import child_db, task_db, reward_db, pending_reward_db
from db.tracking import insert_tracking_event
from db.child_overrides import get_override, delete_override, delete_overrides_for_child
from events.types.child_modified import ChildModified
from events.types.child_reward_request import ChildRewardRequest
from events.types.child_reward_triggered import ChildRewardTriggered
@@ -133,6 +134,12 @@ def delete_child(id):
if not user_id:
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
ChildQuery = Query()
# Cascade delete overrides for this child
deleted_count = delete_overrides_for_child(id)
if deleted_count > 0:
logger.info(f"Cascade deleted {deleted_count} overrides for child {id}")
if child_db.remove((ChildQuery.id == id) & (ChildQuery.user_id == user_id)):
resp = send_event_for_current_user(Event(EventType.CHILD_MODIFIED.value, ChildModified(id, ChildModified.OPERATION_DELETE)))
if resp:
@@ -192,6 +199,17 @@ def set_child_tasks(id):
# Convert back to list if needed
new_tasks = list(new_task_ids)
# Identify unassigned tasks and delete their overrides
old_task_ids = set(child.tasks)
unassigned_task_ids = old_task_ids - new_task_ids
for task_id in unassigned_task_ids:
# Only delete overrides for task entities
override = get_override(id, task_id)
if override and override.entity_type == 'task':
delete_override(id, task_id)
logger.info(f"Deleted override for unassigned task: child={id}, task={task_id}")
# Replace tasks with validated IDs
child_db.update({'tasks': new_tasks}, ChildQuery.id == id)
resp = send_event_for_current_user(Event(EventType.CHILD_TASKS_SET.value, ChildTasksSet(id, new_tasks)))
@@ -246,8 +264,16 @@ def list_child_tasks(id):
task = task_db.get((TaskQuery.id == tid) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)))
if not task:
continue
# Check for override
override = get_override(id, tid)
custom_value = override.custom_value if override else None
ct = ChildTask(task.get('name'), task.get('is_good'), task.get('points'), task.get('image_id'), task.get('id'))
child_tasks.append(ct.to_dict())
ct_dict = ct.to_dict()
if custom_value is not None:
ct_dict['custom_value'] = custom_value
child_tasks.append(ct_dict)
return jsonify({'tasks': child_tasks}), 200
@@ -372,11 +398,15 @@ def trigger_child_task(id):
# Capture points before modification
points_before = child.points
# Check for override
override = get_override(id, task_id)
points_value = override.custom_value if override else task.points
# update the child's points based on task type
if task.is_good:
child.points += task.points
child.points += points_value
else:
child.points -= task.points
child.points -= points_value
child.points = max(child.points, 0)
# update the child in the database
@@ -384,6 +414,15 @@ def trigger_child_task(id):
# Create tracking event
entity_type = 'penalty' if not task.is_good else 'task'
tracking_metadata = {
'task_name': task.name,
'is_good': task.is_good,
'default_points': task.points
}
if override:
tracking_metadata['custom_points'] = override.custom_value
tracking_metadata['has_override'] = True
tracking_event = TrackingEvent.create_event(
user_id=user_id,
child_id=child.id,
@@ -392,7 +431,7 @@ def trigger_child_task(id):
action='activated',
points_before=points_before,
points_after=child.points,
metadata={'task_name': task.name, 'is_good': task.is_good}
metadata=tracking_metadata
)
insert_tracking_event(tracking_event)
log_tracking_event(tracking_event)
@@ -494,6 +533,9 @@ def set_child_rewards(id):
result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
if not result:
return jsonify({'error': 'Child not found'}), 404
child = Child.from_dict(result[0])
old_reward_ids = set(child.rewards)
# Optional: validate reward IDs exist in the reward DB
RewardQuery = Query()
@@ -501,6 +543,15 @@ def set_child_rewards(id):
for rid in new_reward_ids:
if reward_db.get((RewardQuery.id == rid) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))):
valid_reward_ids.append(rid)
# Identify unassigned rewards and delete their overrides
new_reward_ids_set = set(valid_reward_ids)
unassigned_reward_ids = old_reward_ids - new_reward_ids_set
for reward_id in unassigned_reward_ids:
override = get_override(id, reward_id)
if override and override.entity_type == 'reward':
delete_override(id, reward_id)
logger.info(f"Deleted override for unassigned reward: child={id}, reward={reward_id}")
# Replace rewards with validated IDs
child_db.update({'rewards': valid_reward_ids}, ChildQuery.id == id)
@@ -553,8 +604,16 @@ def list_child_rewards(id):
reward = reward_db.get((RewardQuery.id == rid) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
if not reward:
continue
# Check for override
override = get_override(id, rid)
custom_value = override.custom_value if override else None
cr = ChildReward(reward.get('name'), reward.get('cost'), reward.get('image_id'), reward.get('id'))
child_rewards.append(cr.to_dict())
cr_dict = cr.to_dict()
if custom_value is not None:
cr_dict['custom_value'] = custom_value
child_rewards.append(cr_dict)
return jsonify({'rewards': child_rewards}), 200
@@ -618,15 +677,19 @@ def trigger_child_reward(id):
return jsonify({'error': 'Reward not found in reward database'}), 404
reward: Reward = Reward.from_dict(reward_result[0])
# Check for override
override = get_override(id, reward_id)
cost_value = override.custom_value if override else reward.cost
# Check if child has enough points
logger.info(f'Child {child.name} has {child.points} points, reward {reward.name} costs {reward.cost} points')
if child.points < reward.cost:
points_needed = reward.cost - child.points
logger.info(f'Child {child.name} has {child.points} points, reward {reward.name} costs {cost_value} points')
if child.points < cost_value:
points_needed = cost_value - child.points
return jsonify({
'error': 'Insufficient points',
'points_needed': points_needed,
'current_points': child.points,
'reward_cost': reward.cost
'reward_cost': cost_value
}), 400
# Remove matching pending reward requests for this child and reward
@@ -641,11 +704,20 @@ def trigger_child_reward(id):
points_before = child.points
# update the child's points based on reward cost
child.points -= reward.cost
child.points -= cost_value
# update the child in the database
child_db.update({'points': child.points}, ChildQuery.id == id)
# Create tracking event
tracking_metadata = {
'reward_name': reward.name,
'reward_cost': reward.cost,
'default_cost': reward.cost
}
if override:
tracking_metadata['custom_cost'] = override.custom_value
tracking_metadata['has_override'] = True
tracking_event = TrackingEvent.create_event(
user_id=user_id,
child_id=child.id,
@@ -654,7 +726,7 @@ def trigger_child_reward(id):
action='redeemed',
points_before=points_before,
points_after=child.points,
metadata={'reward_name': reward.name, 'reward_cost': reward.cost}
metadata=tracking_metadata
)
insert_tracking_event(tracking_event)
log_tracking_event(tracking_event)
@@ -702,15 +774,24 @@ def reward_status(id):
RewardQuery = Query()
statuses = []
for reward_id in reward_ids:
reward: Reward = Reward.from_dict(reward_db.get((RewardQuery.id == reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))))
if not reward:
reward_dict = reward_db.get((RewardQuery.id == reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
if not reward_dict:
continue
points_needed = max(0, reward.cost - points)
reward: Reward = Reward.from_dict(reward_dict)
# Check for override
override = get_override(id, reward_id)
cost_value = override.custom_value if override else reward.cost
points_needed = max(0, cost_value - points)
#check to see if this reward id and child id is in the pending rewards db if so set its redeeming flag to true
pending_query = Query()
pending = pending_reward_db.get((pending_query.child_id == child.id) & (pending_query.reward_id == reward.id) & (pending_query.user_id == user_id))
status = RewardStatus(reward.id, reward.name, points_needed, reward.cost, pending is not None, reward.image_id)
statuses.append(status.to_dict())
status = RewardStatus(reward.id, reward.name, points_needed, cost_value, pending is not None, reward.image_id)
status_dict = status.to_dict()
if override:
status_dict['custom_value'] = override.custom_value
statuses.append(status_dict)
statuses.sort(key=lambda s: (not s['redeeming'], s['cost']))
return jsonify({'reward_status': statuses}), 200