Some checks failed
Chore App Build and Push Docker Images / build-and-push (push) Has been cancelled
- Added checks for accounts marked for deletion in signup, verification, and password reset processes. - Updated reward and task listing to sort user-created items first. - Enhanced user API to clear verification and reset tokens when marking accounts for deletion. - Introduced tests for marked accounts to ensure proper handling in various scenarios. - Updated profile and reward edit components to reflect changes in validation and data handling.
164 lines
7.1 KiB
Python
164 lines
7.1 KiB
Python
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_rewards_set import ChildRewardsSet
|
|
from db.db import reward_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.reward_modified import RewardModified
|
|
from models.reward import Reward
|
|
|
|
reward_api = Blueprint('reward_api', __name__)
|
|
|
|
# Reward endpoints
|
|
@reward_api.route('/reward/add', methods=['PUT'])
|
|
def add_reward():
|
|
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')
|
|
description = data.get('description')
|
|
cost = data.get('cost')
|
|
image = data.get('image_id', '')
|
|
if not name or description is None or cost is None:
|
|
return jsonify({'error': 'Name, description, and cost are required'}), 400
|
|
reward = Reward(name=name, description=description, cost=cost, image_id=image, user_id=user_id)
|
|
reward_db.insert(reward.to_dict())
|
|
send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value, RewardModified(reward.id, RewardModified.OPERATION_ADD)))
|
|
return jsonify({'message': f'Reward {name} added.'}), 201
|
|
|
|
|
|
|
|
@reward_api.route('/reward/<id>', methods=['GET'])
|
|
def get_reward(id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
|
|
RewardQuery = Query()
|
|
result = reward_db.search((RewardQuery.id == id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
|
|
if not result:
|
|
return jsonify({'error': 'Reward not found'}), 404
|
|
return jsonify(result[0]), 200
|
|
|
|
@reward_api.route('/reward/list', methods=['GET'])
|
|
def list_rewards():
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
|
|
ids_param = request.args.get('ids')
|
|
RewardQuery = Query()
|
|
rewards = reward_db.search((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))
|
|
if ids_param is not None:
|
|
if ids_param.strip() == '':
|
|
rewards = []
|
|
else:
|
|
ids = set(ids_param.split(','))
|
|
rewards = [reward for reward in rewards if reward.get('id') in ids]
|
|
|
|
# Filter out default rewards if user-specific version exists (case/whitespace-insensitive)
|
|
user_rewards = {r['name'].strip().lower(): r for r in rewards if r.get('user_id') == user_id}
|
|
filtered_rewards = []
|
|
for r in rewards:
|
|
if r.get('user_id') is None and r['name'].strip().lower() in user_rewards:
|
|
continue # Skip default if user version exists
|
|
filtered_rewards.append(r)
|
|
|
|
# Sort: user-created items first (by name), then default items (by name)
|
|
user_created = sorted([r for r in filtered_rewards if r.get('user_id') == user_id], key=lambda x: x['name'].lower())
|
|
default_items = sorted([r for r in filtered_rewards if r.get('user_id') is None], key=lambda x: x['name'].lower())
|
|
sorted_rewards = user_created + default_items
|
|
|
|
return jsonify({'rewards': sorted_rewards}), 200
|
|
|
|
@reward_api.route('/reward/<id>', methods=['DELETE'])
|
|
def delete_reward(id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
|
|
RewardQuery = Query()
|
|
reward = reward_db.get(RewardQuery.id == id)
|
|
if not reward:
|
|
return jsonify({'error': 'Reward not found'}), 404
|
|
if reward.get('user_id') is None:
|
|
import logging
|
|
logging.warning(f"Forbidden delete attempt on system reward: id={id}, by user_id={user_id}")
|
|
return jsonify({'error': 'System rewards cannot be deleted.'}), 403
|
|
removed = reward_db.remove((RewardQuery.id == id) & (RewardQuery.user_id == user_id))
|
|
if removed:
|
|
# Cascade delete overrides for this reward
|
|
deleted_count = delete_overrides_for_entity(id)
|
|
if deleted_count > 0:
|
|
import logging
|
|
logging.info(f"Cascade deleted {deleted_count} overrides for reward {id}")
|
|
|
|
# remove the reward id from any child's reward list
|
|
ChildQuery = Query()
|
|
for child in child_db.all():
|
|
rewards = child.get('rewards', [])
|
|
if id in rewards:
|
|
rewards.remove(id)
|
|
child_db.update({'rewards': rewards}, ChildQuery.id == child.get('id'))
|
|
send_event_for_current_user(Event(EventType.CHILD_REWARDS_SET.value, ChildRewardsSet(id, rewards)))
|
|
send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value, RewardModified(id, RewardModified.OPERATION_DELETE)))
|
|
return jsonify({'message': f'Reward {id} deleted.'}), 200
|
|
return jsonify({'error': 'Reward not found'}), 404
|
|
|
|
@reward_api.route('/reward/<id>/edit', methods=['PUT'])
|
|
def edit_reward(id):
|
|
user_id = get_validated_user_id()
|
|
if not user_id:
|
|
return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
|
|
RewardQuery = Query()
|
|
existing = reward_db.get((RewardQuery.id == id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
|
|
if not existing:
|
|
return jsonify({'error': 'Reward not found'}), 404
|
|
|
|
reward = Reward.from_dict(existing)
|
|
is_dirty = False
|
|
|
|
data = request.get_json(force=True) or {}
|
|
if 'name' in data:
|
|
name = (data.get('name') or '').strip()
|
|
if not name:
|
|
return jsonify({'error': 'Name cannot be empty'}), 400
|
|
reward.name = name
|
|
is_dirty = True
|
|
|
|
if 'description' in data:
|
|
desc = (data.get('description') or '').strip()
|
|
if not desc:
|
|
return jsonify({'error': 'Description cannot be empty'}), 400
|
|
reward.description = desc
|
|
is_dirty = True
|
|
|
|
if 'cost' in data:
|
|
cost = data.get('cost')
|
|
if not isinstance(cost, int):
|
|
return jsonify({'error': 'Cost must be an integer'}), 400
|
|
if cost <= 0:
|
|
return jsonify({'error': 'Cost must be a positive integer'}), 400
|
|
reward.cost = cost
|
|
is_dirty = True
|
|
|
|
if 'image_id' in data:
|
|
reward.image_id = data.get('image_id', '')
|
|
is_dirty = True
|
|
|
|
if not is_dirty:
|
|
return jsonify({'error': 'No valid fields to update'}), 400
|
|
|
|
if reward.user_id is None: # public reward
|
|
new_reward = Reward(name=reward.name, description=reward.description, cost=reward.cost, image_id=reward.image_id, user_id=user_id)
|
|
reward_db.insert(new_reward.to_dict())
|
|
send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value,
|
|
RewardModified(new_reward.id, RewardModified.OPERATION_ADD)))
|
|
return jsonify(new_reward.to_dict()), 200
|
|
|
|
reward_db.update(reward.to_dict(), (RewardQuery.id == id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
|
|
send_event_for_current_user(Event(EventType.REWARD_MODIFIED.value,
|
|
RewardModified(id, RewardModified.OPERATION_EDIT)))
|
|
return jsonify(reward.to_dict()), 200
|