Files
Synculous-2/api/routes/routine_templates.py
Chelsea Lee b50e0b91fe feat(templates): add category-based organization
- Added 'category' column to routine_templates table
- Categorized all 12 templates into: Daily Routines, Getting Things Done, Health & Body, Errands
- Added /api/templates/categories endpoint to list unique categories
- Updated /api/templates to support filtering by category query param
- Redesigned templates page with collapsible accordion sections by category
- Categories are sorted in logical order (Daily → Work → Health → Errands)
- All categories expanded by default for easy browsing
2026-02-16 06:38:49 -06:00

192 lines
8.0 KiB
Python

"""
Routine Templates API - pre-built routines that users can clone
"""
import os
import uuid
import flask
import jwt
import core.auth as auth
import core.postgres as postgres
def _get_user_uuid(token):
try:
payload = jwt.decode(token, os.getenv("JWT_SECRET"), algorithms=["HS256"])
return payload.get("sub")
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
return None
def _auth(request):
header = request.headers.get("Authorization", "")
if not header.startswith("Bearer "):
return None
token = header[7:]
user_uuid = _get_user_uuid(token)
if not user_uuid or not auth.verifyLoginToken(token, userUUID=user_uuid):
return None
return user_uuid
def register(app):
@app.route("/api/templates", methods=["GET"])
def api_listTemplates():
"""List all available templates. Optional query param: category"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
# Check for category filter
category = flask.request.args.get('category')
if category:
templates = postgres.select("routine_templates", where={"category": category}, order_by="name")
else:
templates = postgres.select("routine_templates", order_by="category, name")
for template in templates:
steps = postgres.select("routine_template_steps", {"template_id": template["id"]}, order_by="position")
template["step_count"] = len(steps)
return flask.jsonify(templates), 200
@app.route("/api/templates/categories", methods=["GET"])
def api_listTemplateCategories():
"""List all unique template categories."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
# Get distinct categories
result = postgres.execute("""
SELECT DISTINCT category FROM routine_templates
WHERE category IS NOT NULL
ORDER BY category
""")
categories = [row["category"] for row in result]
return flask.jsonify(categories), 200
@app.route("/api/templates", methods=["POST"])
def api_createTemplate():
"""Create a new template (admin only in production). Body: {name, description?, icon?}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
if not data:
return flask.jsonify({"error": "missing body"}), 400
if not data.get("name"):
return flask.jsonify({"error": "missing required field: name"}), 400
data["id"] = str(uuid.uuid4())
data["created_by_admin"] = False
template = postgres.insert("routine_templates", data)
return flask.jsonify(template), 201
@app.route("/api/templates/<template_id>", methods=["GET"])
def api_getTemplate(template_id):
"""Get a template with its steps."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
template = postgres.select_one("routine_templates", {"id": template_id})
if not template:
return flask.jsonify({"error": "template not found"}), 404
steps = postgres.select("routine_template_steps", {"template_id": template_id}, order_by="position")
return flask.jsonify({"template": template, "steps": steps}), 200
@app.route("/api/templates/<template_id>/clone", methods=["POST"])
def api_cloneTemplate(template_id):
"""Clone a template to user's routines."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
template = postgres.select_one("routine_templates", {"id": template_id})
if not template:
return flask.jsonify({"error": "template not found"}), 404
template_steps = postgres.select("routine_template_steps", {"template_id": template_id}, order_by="position")
new_routine = {
"id": str(uuid.uuid4()),
"user_uuid": user_uuid,
"name": template["name"],
"description": template.get("description"),
"icon": template.get("icon"),
}
routine = postgres.insert("routines", new_routine)
for step in template_steps:
new_step = {
"id": str(uuid.uuid4()),
"routine_id": routine["id"],
"name": step["name"],
"instructions": step.get("instructions"),
"step_type": step.get("step_type", "generic"),
"duration_minutes": step.get("duration_minutes"),
"media_url": step.get("media_url"),
"position": step["position"],
}
postgres.insert("routine_steps", new_step)
return flask.jsonify(routine), 201
@app.route("/api/templates/<template_id>", methods=["PUT"])
def api_updateTemplate(template_id):
"""Update a template."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
template = postgres.select_one("routine_templates", {"id": template_id})
if not template:
return flask.jsonify({"error": "template not found"}), 404
data = flask.request.get_json()
if not data:
return flask.jsonify({"error": "missing body"}), 400
allowed = ["name", "description", "icon"]
updates = {k: v for k, v in data.items() if k in allowed}
if not updates:
return flask.jsonify({"error": "no valid fields to update"}), 400
result = postgres.update("routine_templates", updates, {"id": template_id})
return flask.jsonify(result[0] if result else {}), 200
@app.route("/api/templates/<template_id>", methods=["DELETE"])
def api_deleteTemplate(template_id):
"""Delete a template."""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
template = postgres.select_one("routine_templates", {"id": template_id})
if not template:
return flask.jsonify({"error": "template not found"}), 404
postgres.delete("routine_template_steps", {"template_id": template_id})
postgres.delete("routine_templates", {"id": template_id})
return flask.jsonify({"deleted": True}), 200
@app.route("/api/templates/<template_id>/steps", methods=["POST"])
def api_addTemplateStep(template_id):
"""Add a step to a template. Body: {name, instructions?, step_type?, duration_minutes?, position?}"""
user_uuid = _auth(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
template = postgres.select_one("routine_templates", {"id": template_id})
if not template:
return flask.jsonify({"error": "template not found"}), 404
data = flask.request.get_json()
if not data or not data.get("name"):
return flask.jsonify({"error": "missing required field: name"}), 400
max_pos = postgres.select(
"routine_template_steps",
{"template_id": template_id},
order_by="position DESC",
limit=1,
)
next_pos = (max_pos[0]["position"] + 1) if max_pos else 1
step = {
"id": str(uuid.uuid4()),
"template_id": template_id,
"name": data["name"],
"instructions": data.get("instructions"),
"step_type": data.get("step_type", "generic"),
"duration_minutes": data.get("duration_minutes"),
"media_url": data.get("media_url"),
"position": data.get("position", next_pos),
}
result = postgres.insert("routine_template_steps", step)
return flask.jsonify(result), 201