First synculous 2 Big-Pickle pass.
This commit is contained in:
224
core/routines.py
Normal file
224
core/routines.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
core/routines.py - Shared business logic for routines
|
||||
|
||||
This module contains reusable functions for routine operations
|
||||
that can be called from API routes, bot commands, or scheduler.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, date
|
||||
import core.postgres as postgres
|
||||
|
||||
|
||||
def start_session(routine_id, user_uuid):
|
||||
"""
|
||||
Create and start a new routine session.
|
||||
Returns the session object or None if routine not found.
|
||||
"""
|
||||
routine = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid})
|
||||
if not routine:
|
||||
return None
|
||||
|
||||
active_session = postgres.select_one(
|
||||
"routine_sessions",
|
||||
{"user_uuid": user_uuid, "status": "active"}
|
||||
)
|
||||
if active_session:
|
||||
return {"error": "already_active", "session_id": active_session["id"]}
|
||||
|
||||
steps = postgres.select(
|
||||
"routine_steps",
|
||||
{"routine_id": routine_id},
|
||||
order_by="position"
|
||||
)
|
||||
if not steps:
|
||||
return {"error": "no_steps"}
|
||||
|
||||
session = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"routine_id": routine_id,
|
||||
"user_uuid": user_uuid,
|
||||
"status": "active",
|
||||
"current_step_index": 0,
|
||||
}
|
||||
result = postgres.insert("routine_sessions", session)
|
||||
return {"session": result, "current_step": steps[0]}
|
||||
|
||||
|
||||
def pause_session(session_id, user_uuid):
|
||||
"""Pause an active session."""
|
||||
session = postgres.select_one(
|
||||
"routine_sessions",
|
||||
{"id": session_id, "user_uuid": user_uuid}
|
||||
)
|
||||
if not session:
|
||||
return None
|
||||
if session.get("status") != "active":
|
||||
return {"error": "not_active"}
|
||||
result = postgres.update(
|
||||
"routine_sessions",
|
||||
{"status": "paused", "paused_at": datetime.now().isoformat()},
|
||||
{"id": session_id}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def resume_session(session_id, user_uuid):
|
||||
"""Resume a paused session."""
|
||||
session = postgres.select_one(
|
||||
"routine_sessions",
|
||||
{"id": session_id, "user_uuid": user_uuid}
|
||||
)
|
||||
if not session:
|
||||
return None
|
||||
if session.get("status") != "paused":
|
||||
return {"error": "not_paused"}
|
||||
result = postgres.update(
|
||||
"routine_sessions",
|
||||
{"status": "active", "paused_at": None},
|
||||
{"id": session_id}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def abort_session(session_id, user_uuid, reason=None):
|
||||
"""Abort a session with optional reason."""
|
||||
session = postgres.select_one(
|
||||
"routine_sessions",
|
||||
{"id": session_id, "user_uuid": user_uuid}
|
||||
)
|
||||
if not session:
|
||||
return None
|
||||
result = postgres.update(
|
||||
"routine_sessions",
|
||||
{
|
||||
"status": "aborted",
|
||||
"abort_reason": reason or "Aborted by user",
|
||||
"completed_at": datetime.now().isoformat()
|
||||
},
|
||||
{"id": session_id}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def complete_session(session_id, user_uuid):
|
||||
"""Mark a session as completed and update streak."""
|
||||
session = postgres.select_one(
|
||||
"routine_sessions",
|
||||
{"id": session_id, "user_uuid": user_uuid}
|
||||
)
|
||||
if not session:
|
||||
return None
|
||||
|
||||
completed_at = datetime.now()
|
||||
result = postgres.update(
|
||||
"routine_sessions",
|
||||
{"status": "completed", "completed_at": completed_at.isoformat()},
|
||||
{"id": session_id}
|
||||
)
|
||||
|
||||
_update_streak(user_uuid, session["routine_id"])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def clone_template(template_id, user_uuid):
|
||||
"""Clone a template to user's routines."""
|
||||
template = postgres.select_one("routine_templates", {"id": template_id})
|
||||
if not template:
|
||||
return None
|
||||
|
||||
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 routine
|
||||
|
||||
|
||||
def _update_streak(user_uuid, routine_id):
|
||||
"""Update streak after completing a session. Resets if day was missed."""
|
||||
today = date.today()
|
||||
|
||||
streak = postgres.select_one(
|
||||
"routine_streaks",
|
||||
{"user_uuid": user_uuid, "routine_id": routine_id}
|
||||
)
|
||||
|
||||
if not streak:
|
||||
new_streak = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"user_uuid": user_uuid,
|
||||
"routine_id": routine_id,
|
||||
"current_streak": 1,
|
||||
"longest_streak": 1,
|
||||
"last_completed_date": today.isoformat(),
|
||||
}
|
||||
return postgres.insert("routine_streaks", new_streak)
|
||||
|
||||
last_completed = streak.get("last_completed_date")
|
||||
if last_completed:
|
||||
if isinstance(last_completed, str):
|
||||
last_completed = date.fromisoformat(last_completed)
|
||||
days_diff = (today - last_completed).days
|
||||
if days_diff == 0:
|
||||
return streak
|
||||
elif days_diff == 1:
|
||||
new_streak = streak["current_streak"] + 1
|
||||
else:
|
||||
new_streak = 1
|
||||
else:
|
||||
new_streak = 1
|
||||
|
||||
longest = max(streak["longest_streak"], new_streak)
|
||||
|
||||
postgres.update(
|
||||
"routine_streaks",
|
||||
{
|
||||
"current_streak": new_streak,
|
||||
"longest_streak": longest,
|
||||
"last_completed_date": today.isoformat(),
|
||||
},
|
||||
{"id": streak["id"]}
|
||||
)
|
||||
return streak
|
||||
|
||||
|
||||
def calculate_streak(user_uuid, routine_id):
|
||||
"""Get current streak for a routine."""
|
||||
streak = postgres.select_one(
|
||||
"routine_streaks",
|
||||
{"user_uuid": user_uuid, "routine_id": routine_id}
|
||||
)
|
||||
return streak
|
||||
|
||||
|
||||
def get_active_session(user_uuid):
|
||||
"""Get user's currently active session."""
|
||||
return postgres.select_one(
|
||||
"routine_sessions",
|
||||
{"user_uuid": user_uuid, "status": "active"}
|
||||
)
|
||||
160
core/stats.py
Normal file
160
core/stats.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
core/stats.py - Statistics calculations for routines
|
||||
|
||||
This module contains functions for calculating routine statistics,
|
||||
completion rates, streaks, and weekly summaries.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta, date
|
||||
import core.postgres as postgres
|
||||
|
||||
|
||||
def get_routine_stats(routine_id, user_uuid, days=30):
|
||||
"""
|
||||
Get completion statistics for a routine over a period.
|
||||
Returns dict with completion_rate, avg_duration, total_time, etc.
|
||||
"""
|
||||
sessions = postgres.select(
|
||||
"routine_sessions",
|
||||
{"routine_id": routine_id, "user_uuid": user_uuid},
|
||||
limit=days * 3,
|
||||
)
|
||||
|
||||
completed = sum(1 for s in sessions if s.get("status") == "completed")
|
||||
aborted = sum(1 for s in sessions if s.get("status") == "aborted")
|
||||
|
||||
total_duration = sum(
|
||||
s.get("actual_duration_minutes", 0) or 0
|
||||
for s in sessions
|
||||
if s.get("actual_duration_minutes")
|
||||
)
|
||||
|
||||
avg_duration = total_duration / completed if completed > 0 else 0
|
||||
completion_rate = (completed / len(sessions) * 100) if sessions else 0
|
||||
|
||||
return {
|
||||
"total_sessions": len(sessions),
|
||||
"completed": completed,
|
||||
"aborted": aborted,
|
||||
"completion_rate_percent": round(completion_rate, 1),
|
||||
"avg_duration_minutes": round(avg_duration, 1),
|
||||
"total_time_minutes": total_duration,
|
||||
}
|
||||
|
||||
|
||||
def get_user_streaks(user_uuid):
|
||||
"""
|
||||
Get all streaks for a user across all routines.
|
||||
Returns list of streak objects with routine names.
|
||||
"""
|
||||
streaks = postgres.select("routine_streaks", {"user_uuid": user_uuid})
|
||||
routines = postgres.select("routines", {"user_uuid": user_uuid})
|
||||
routine_map = {r["id"]: r["name"] for r in routines}
|
||||
|
||||
result = []
|
||||
for streak in streaks:
|
||||
result.append({
|
||||
"routine_id": streak["routine_id"],
|
||||
"routine_name": routine_map.get(streak["routine_id"], "Unknown"),
|
||||
"current_streak": streak["current_streak"],
|
||||
"longest_streak": streak["longest_streak"],
|
||||
"last_completed_date": streak.get("last_completed_date"),
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_weekly_summary(user_uuid):
|
||||
"""
|
||||
Get weekly progress summary for a user.
|
||||
Returns total completed, total time, routines started, per-routine breakdown.
|
||||
"""
|
||||
routines = postgres.select("routines", {"user_uuid": user_uuid})
|
||||
if not routines:
|
||||
return {
|
||||
"total_completed": 0,
|
||||
"total_time_minutes": 0,
|
||||
"routines_started": 0,
|
||||
"routines": [],
|
||||
}
|
||||
|
||||
week_ago = datetime.now() - timedelta(days=7)
|
||||
sessions = postgres.select("routine_sessions", {"user_uuid": user_uuid})
|
||||
week_sessions = [
|
||||
s for s in sessions
|
||||
if s.get("created_at") and s["created_at"] >= week_ago
|
||||
]
|
||||
|
||||
completed = [s for s in week_sessions if s.get("status") == "completed"]
|
||||
total_time = sum(
|
||||
s.get("actual_duration_minutes", 0) or 0
|
||||
for s in completed
|
||||
if s.get("actual_duration_minutes")
|
||||
)
|
||||
|
||||
routine_summaries = []
|
||||
for routine in routines:
|
||||
r_sessions = [s for s in week_sessions if s.get("routine_id") == routine["id"]]
|
||||
r_completed = sum(1 for s in r_sessions if s.get("status") == "completed")
|
||||
routine_summaries.append({
|
||||
"routine_id": routine["id"],
|
||||
"name": routine["name"],
|
||||
"completed_this_week": r_completed,
|
||||
})
|
||||
|
||||
return {
|
||||
"total_completed": len(completed),
|
||||
"total_time_minutes": total_time,
|
||||
"routines_started": len(set(s.get("routine_id") for s in week_sessions)),
|
||||
"routines": routine_summaries,
|
||||
}
|
||||
|
||||
|
||||
def calculate_completion_rate(sessions, completed_only=True):
|
||||
"""Calculate completion rate from a list of sessions."""
|
||||
if not sessions:
|
||||
return 0.0
|
||||
|
||||
if completed_only:
|
||||
completed = sum(1 for s in sessions if s.get("status") == "completed")
|
||||
return (completed / len(sessions)) * 100
|
||||
|
||||
return 0.0
|
||||
|
||||
|
||||
def get_monthly_summary(user_uuid, year=None, month=None):
|
||||
"""
|
||||
Get monthly progress summary.
|
||||
Defaults to current month if year/month not specified.
|
||||
"""
|
||||
if year is None or month is None:
|
||||
now = datetime.now()
|
||||
year = now.year
|
||||
month = now.month
|
||||
|
||||
start_date = datetime(year, month, 1)
|
||||
if month == 12:
|
||||
end_date = datetime(year + 1, 1, 1)
|
||||
else:
|
||||
end_date = datetime(year, month + 1, 1)
|
||||
|
||||
sessions = postgres.select("routine_sessions", {"user_uuid": user_uuid})
|
||||
month_sessions = [
|
||||
s for s in sessions
|
||||
if s.get("created_at") and start_date <= s["created_at"] < end_date
|
||||
]
|
||||
|
||||
completed = [s for s in month_sessions if s.get("status") == "completed"]
|
||||
total_time = sum(
|
||||
s.get("actual_duration_minutes", 0) or 0
|
||||
for s in completed
|
||||
if s.get("actual_duration_minutes")
|
||||
)
|
||||
|
||||
return {
|
||||
"year": year,
|
||||
"month": month,
|
||||
"total_sessions": len(month_sessions),
|
||||
"completed": len(completed),
|
||||
"total_time_minutes": total_time,
|
||||
}
|
||||
Reference in New Issue
Block a user