initial commit

This commit is contained in:
2025-11-20 14:06:59 -05:00
commit cb0f972a5f
77 changed files with 11579 additions and 0 deletions

296
api/child_api.py Normal file
View File

@@ -0,0 +1,296 @@
from flask import Blueprint, request, jsonify
from tinydb import Query
from db.db import child_db, task_db, reward_db
from api.reward_status import RewardStatus
from api.child_tasks import ChildTask
from models.child import Child
from models.task import Task
from models.reward import Reward
child_api = Blueprint('child_api', __name__)
@child_api.route('/child/<name>', methods=['GET'])
@child_api.route('/child/<id>', methods=['GET'])
def get_child(id):
ChildQuery = Query()
result = child_db.search(ChildQuery.id == id)
if not result:
return jsonify({'error': 'Child not found'}), 404
return jsonify(result[0]), 200
@child_api.route('/child/add', methods=['PUT'])
def add_child():
data = request.get_json()
name = data.get('name')
age = data.get('age')
image = data.get('image_id', None)
if not name:
return jsonify({'error': 'Name is required'}), 400
if not image:
image = 'boy01'
child = Child(name, age, image_id=image)
child_db.insert(child.to_dict())
return jsonify({'message': f'Child {name} added.'}), 201
@child_api.route('/child/<id>/edit', methods=['PUT'])
def edit_child(id):
data = request.get_json()
name = data.get('name', None)
age = data.get('age', None)
points = data.get('points', None)
image = data.get('image_id', None)
ChildQuery = Query()
result = child_db.search(ChildQuery.id == id)
if not result:
return jsonify({'error': 'Child not found'}), 404
child = result[0]
if name is not None:
child['name'] = name
if age is not None:
child['age'] = age
if points is not None:
child['points'] = points
if image is not None:
child['image_id'] = image
child_db.update(child, ChildQuery.id == id)
return jsonify({'message': f'Child {id} updated.'}), 200
@child_api.route('/child/list', methods=['GET'])
def list_children():
children = child_db.all()
return jsonify({'children': children}), 200
# Child DELETE
@child_api.route('/child/<id>', methods=['DELETE'])
def delete_child(id):
ChildQuery = Query()
if child_db.remove(ChildQuery.id == id):
return jsonify({'message': f'Child {id} deleted.'}), 200
return jsonify({'error': 'Child not found'}), 404
@child_api.route('/child/<id>/assign-task', methods=['POST'])
def assign_task_to_child(id):
data = request.get_json()
task_id = data.get('task_id')
if not task_id:
return jsonify({'error': 'task_id is required'}), 400
ChildQuery = Query()
result = child_db.search(ChildQuery.id == id)
if not result:
return jsonify({'error': 'Child not found'}), 404
child = result[0]
if task_id not in child.get('tasks', []):
child['tasks'].append(task_id)
child_db.update({'tasks': child['tasks']}, ChildQuery.id == id)
return jsonify({'message': f'Task {task_id} assigned to {child['name']}.'}), 200
@child_api.route('/child/<id>/remove-task', methods=['POST'])
def remove_task_from_child(id):
data = request.get_json()
task_id = data.get('task_id')
if not task_id:
return jsonify({'error': 'task_id is required'}), 400
ChildQuery = Query()
result = child_db.search(ChildQuery.id == id)
if not result:
return jsonify({'error': 'Child not found'}), 404
child = result[0]
if task_id in child.get('tasks', []):
child['tasks'].remove(task_id)
child_db.update({'tasks': child['tasks']}, ChildQuery.id == id)
return jsonify({'message': f'Task {task_id} removed from {child["name"]}.'}), 200
return jsonify({'error': 'Task not assigned to child'}), 400
@child_api.route('/child/<id>/list-tasks', methods=['GET'])
def list_child_tasks(id):
ChildQuery = Query()
result = child_db.search(ChildQuery.id == id)
if not result:
return jsonify({'error': 'Child not found'}), 404
child = result[0]
task_ids = child.get('tasks', [])
TaskQuery = Query()
child_tasks = []
for tid in task_ids:
task = task_db.get(TaskQuery.id == tid)
if not task:
continue
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())
return jsonify({'child_tasks': child_tasks}), 200
@child_api.route('/child/<id>/list-assignable-tasks', methods=['GET'])
def list_assignable_tasks(id):
ChildQuery = Query()
result = child_db.search(ChildQuery.id == id)
if not result:
return jsonify({'error': 'Child not found'}), 404
child = result[0]
assigned_ids = set(child.get('tasks', []))
# Collect all task ids from the task database
all_task_ids = [t.get('id') for t in task_db.all() if t and t.get('id')]
# Filter out already assigned
assignable_ids = [tid for tid in all_task_ids if tid not in assigned_ids]
# Fetch full task details and wrap in ChildTask
TaskQuery = Query()
assignable_tasks = []
for tid in assignable_ids:
task = task_db.get(TaskQuery.id == tid)
if not task:
continue
ct = ChildTask(task.get('name'), task.get('is_good'), task.get('points'), task.get('image_id'), task.get('id'))
assignable_tasks.append(ct.to_dict())
return jsonify({'assignable_tasks': assignable_tasks, 'count': len(assignable_tasks)}), 200
@child_api.route('/child/<id>/trigger-task', methods=['POST'])
def trigger_child_task(id):
data = request.get_json()
task_id = data.get('task_id')
if not task_id:
return jsonify({'error': 'task_id is required'}), 400
ChildQuery = Query()
result = child_db.search(ChildQuery.id == id)
if not result:
return jsonify({'error': 'Child not found'}), 404
child: Child = Child.from_dict(result[0])
if task_id not in child.tasks:
return jsonify({'error': f'Task not found assigned to child {child.name}'}), 404
# look up the task and get the details
TaskQuery = Query()
task_result = task_db.search(TaskQuery.id == task_id)
if not task_result:
return jsonify({'error': 'Task not found in task database'}), 404
task: Task = Task.from_dict(task_result[0])
# update the child's points based on task type
if task.is_good:
child.points += task.points
else:
child.points -= task.points
child.points = max(child.points, 0)
# update the child in the database
child_db.update({'points': child.points}, ChildQuery.id == id)
return jsonify({'message': f'{task.name} points assigned to {child.name}.', 'points': child.points, 'id': child.id}), 200
@child_api.route('/child/<id>/assign-reward', methods=['POST'])
def assign_reward_to_child(id):
data = request.get_json()
reward_id = data.get('reward_id')
if not reward_id:
return jsonify({'error': 'reward_id is required'}), 400
ChildQuery = Query()
result = child_db.search(ChildQuery.id == id)
if not result:
return jsonify({'error': 'Child not found'}), 404
child = result[0]
if reward_id not in child.get('rewards', []):
child['rewards'].append(reward_id)
child_db.update({'rewards': child['rewards']}, ChildQuery.id == id)
return jsonify({'message': f'Reward {reward_id} assigned to {child["name"]}.'}), 200
@child_api.route('/child/<id>/remove-reward', methods=['POST'])
def remove_reward_from_child(id):
data = request.get_json()
reward_id = data.get('reward_id')
if not reward_id:
return jsonify({'error': 'reward_id is required'}), 400
ChildQuery = Query()
result = child_db.search(ChildQuery.id == id)
if not result:
return jsonify({'error': 'Child not found'}), 404
child = result[0]
if reward_id in child.get('rewards', []):
child['rewards'].remove(reward_id)
child_db.update({'rewards': child['rewards']}, ChildQuery.id == id)
return jsonify({'message': f'Reward {reward_id} removed from {child["name"]}.'}), 200
return jsonify({'error': 'Reward not assigned to child'}), 400
@child_api.route('/child/<id>/trigger-reward', methods=['POST'])
def trigger_child_reward(id):
data = request.get_json()
reward_id = data.get('reward_id')
if not reward_id:
return jsonify({'error': 'reward_id is required'}), 400
ChildQuery = Query()
result = child_db.search(ChildQuery.id == id)
if not result:
return jsonify({'error': 'Child not found'}), 404
child: Child = Child.from_dict(result[0])
if reward_id not in child.rewards:
return jsonify({'error': f'Reward not found assigned to child {child.name}'}), 404
# look up the task and get the details
RewardQuery = Query()
reward_result = reward_db.search(RewardQuery.id == reward_id)
if not reward_result:
return jsonify({'error': 'Reward not found in reward database'}), 404
reward: Reward = Reward.from_dict(reward_result[0])
# update the child's points based on reward cost
child.points -= reward.cost
# update the child in the database
child_db.update({'points': child.points}, ChildQuery.id == id)
return jsonify({'message': f'{reward.name} assigned to {child.name}.', 'points': child.points, 'id': child.id}), 200
@child_api.route('/child/<id>/affordable-rewards', methods=['GET'])
def list_affordable_rewards(id):
ChildQuery = Query()
result = child_db.search(ChildQuery.id == id)
if not result:
return jsonify({'error': 'Child not found'}), 404
child = result[0]
points = child.get('points', 0)
reward_ids = child.get('rewards', [])
RewardQuery = Query()
affordable = [
reward for reward_id in reward_ids
if (reward := reward_db.get(RewardQuery.id == reward_id)) and points >= reward.get('cost', 0)
]
return jsonify({'affordable_rewards': affordable}), 200
@child_api.route('/child/<id>/reward-status', methods=['GET'])
def reward_status(id):
ChildQuery = Query()
result = child_db.search(ChildQuery.id == id)
if not result:
return jsonify({'error': 'Child not found'}), 404
child = result[0]
points = child.get('points', 0)
reward_ids = child.get('rewards', [])
RewardQuery = Query()
statuses = []
for reward_id in reward_ids:
reward = reward_db.get(RewardQuery.id == reward_id)
if not reward:
continue
points_needed = max(0, reward.get('cost', 0) - points)
status = RewardStatus(reward.get('id'), reward.get('name'), points_needed, reward.get('image_id'))
statuses.append(status.to_dict())
statuses.sort(key=lambda s: s['points_needed'])
return jsonify({'reward_status': statuses}), 200

16
api/child_tasks.py Normal file
View File

@@ -0,0 +1,16 @@
class ChildTask:
def __init__(self, name, is_good, points, image_id, id):
self.id = id
self.name = name
self.is_good = is_good
self.points = points
self.image_id = image_id
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'is_good': self.is_good,
'points': self.points,
'image_id': self.image_id
}

107
api/image_api.py Normal file
View File

@@ -0,0 +1,107 @@
import os
import uuid
from io import BytesIO
from flask import Blueprint, request, jsonify, send_file
from db.db import image_db
from models.image import Image
from tinydb import Query
from PIL import Image as PILImage, UnidentifiedImageError
image_api = Blueprint('image_api', __name__)
UPLOAD_FOLDER = './resources/images'
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png'}
IMAGE_TYPE_PROFILE = 1
IMAGE_TYPE_ICON = 2
MAX_DIMENSION = 512
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@image_api.route('/image/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return jsonify({'error': 'No file part in the request'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No selected file'}), 400
image_type = request.form.get('type', None)
if not image_type:
return jsonify({'error': 'Image type is required'}), 400
try:
image_type = int(image_type)
if image_type not in [IMAGE_TYPE_PROFILE, IMAGE_TYPE_ICON]:
return jsonify({'error': 'Invalid image type. Must be 1 or 2'}), 400
except ValueError:
return jsonify({'error': 'Image type must be an integer'}), 400
perm = request.form.get('permanent', "false").lower() == 'true'
if not file or not allowed_file(file.filename):
return jsonify({'error': 'Invalid file type or no file selected'}), 400
try:
pil_image = PILImage.open(file.stream)
original_format = pil_image.format # store before convert
pil_image.verify() # quick integrity check
file.stream.seek(0)
pil_image = PILImage.open(file.stream).convert('RGBA' if pil_image.mode in ('RGBA', 'LA') else 'RGB')
except UnidentifiedImageError:
return jsonify({'error': 'Uploaded file is not a valid image'}), 400
except Exception:
return jsonify({'error': 'Failed to process image'}), 400
if original_format not in ('JPEG', 'PNG'):
return jsonify({'error': 'Only JPEG and PNG images are allowed'}), 400
# Resize preserving aspect ratio (in-place thumbnail)
pil_image.thumbnail((MAX_DIMENSION, MAX_DIMENSION), PILImage.Resampling.LANCZOS)
format_extension_map = {'JPEG': '.jpg', 'PNG': '.png'}
extension = format_extension_map.get(original_format, '.png')
_id = str(uuid.uuid4())
filename = _id + extension
filepath = os.path.abspath(os.path.join(UPLOAD_FOLDER, filename))
try:
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
# Save with appropriate format
save_params = {}
if pil_image.format == 'JPEG':
save_params['quality'] = 90
save_params['optimize'] = True
pil_image.save(filepath, format=pil_image.format, **save_params)
except Exception:
return jsonify({'error': 'Failed to save processed image'}), 500
image_record = Image(image_type, extension, permanent=perm, id=_id)
image_db.insert(image_record.to_dict())
return jsonify({'message': 'Image uploaded successfully', 'filename': filename, 'id': _id}), 200
@image_api.route('/image/request/<id>', methods=['GET'])
def request_image(id):
ImageQuery = Query()
image = image_db.get(ImageQuery.id == id)
if not image:
return jsonify({'error': 'Image not found'}), 404
filename = f"{image['id']}{image['extension']}"
filepath = os.path.abspath(os.path.join(UPLOAD_FOLDER, filename))
if not os.path.exists(filepath):
return jsonify({'error': 'File not found'}), 404
return send_file(filepath)
@image_api.route('/image/list', methods=['GET'])
def list_images():
image_type = request.args.get('type', type=int)
ImageQuery = Query()
if image_type is not None:
if image_type not in [IMAGE_TYPE_PROFILE, IMAGE_TYPE_ICON]:
return jsonify({'error': 'Invalid image type'}), 400
images = image_db.search(ImageQuery.type == image_type)
else:
images = image_db.all()
image_ids = [img['id'] for img in images]
return jsonify({'ids': image_ids, 'count': len(image_ids)}), 200

48
api/reward_api.py Normal file
View File

@@ -0,0 +1,48 @@
from flask import Blueprint, request, jsonify
from tinydb import Query
from models.reward import Reward
from db.db import reward_db, child_db
reward_api = Blueprint('reward_api', __name__)
# Reward endpoints
@reward_api.route('/reward/add', methods=['PUT'])
def add_reward():
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, description, cost, image_id=image)
reward_db.insert(reward.to_dict())
return jsonify({'message': f'Reward {name} added.'}), 201
@reward_api.route('/reward/<id>', methods=['GET'])
def get_reward(id):
RewardQuery = Query()
result = reward_db.search(RewardQuery.id == id)
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():
rewards = reward_db.all()
return jsonify({'rewards': rewards}), 200
@reward_api.route('/reward/<id>', methods=['DELETE'])
def delete_reward(id):
RewardQuery = Query()
removed = reward_db.remove(RewardQuery.id == id)
if removed:
# 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'))
return jsonify({'message': f'Reward {id} deleted.'}), 200
return jsonify({'error': 'Reward not found'}), 404

14
api/reward_status.py Normal file
View File

@@ -0,0 +1,14 @@
class RewardStatus:
def __init__(self, id, name, points_needed, image_id):
self.id = id
self.name = name
self.points_needed = points_needed
self.image_id = image_id
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'points_needed': self.points_needed,
'image_id': self.image_id
}

48
api/task_api.py Normal file
View File

@@ -0,0 +1,48 @@
from flask import Blueprint, request, jsonify
from tinydb import Query
from models.task import Task
from db.db import task_db, child_db
task_api = Blueprint('task_api', __name__)
# Task endpoints
@task_api.route('/task/add', methods=['PUT'])
def add_task():
data = request.get_json()
name = data.get('name')
points = data.get('points')
is_good = data.get('is_good')
image = data.get('image_id', '')
if not name or points is None or is_good is None:
return jsonify({'error': 'Name, points, and is_good are required'}), 400
task = Task(name, points, is_good, image_id=image)
task_db.insert(task.to_dict())
return jsonify({'message': f'Task {name} added.'}), 201
@task_api.route('/task/<id>', methods=['GET'])
def get_task(id):
TaskQuery = Query()
result = task_db.search(TaskQuery.id == id)
if not result:
return jsonify({'error': 'Task not found'}), 404
return jsonify(result[0]), 200
@task_api.route('/task/list', methods=['GET'])
def list_tasks():
tasks = task_db.all()
return jsonify({'tasks': tasks}), 200
@task_api.route('/task/<id>', methods=['DELETE'])
def delete_task(id):
TaskQuery = Query()
removed = task_db.remove(TaskQuery.id == id)
if removed:
# remove the task id from any child's task list
ChildQuery = Query()
for child in child_db.all():
tasks = child.get('tasks', [])
if id in tasks:
tasks.remove(id)
child_db.update({'tasks': tasks}, ChildQuery.id == child.get('id'))
return jsonify({'message': f'Task {id} deleted.'}), 200
return jsonify({'error': 'Task not found'}), 404