Files
bloodpressure/app/routes/main.py
T

177 lines
7.5 KiB
Python

from datetime import date, datetime, timedelta
from flask import Blueprint, render_template, redirect, request, url_for
from flask_login import login_required, current_user
from pytz import utc
from app.models import Reading
from app.insights import DateRange, user_timezone, parse_range, readings_query, summary, annotate, chart_data
main = Blueprint('main', __name__)
PAGE_SIZE = 25
@main.route('/')
def landing():
return redirect(url_for('main.dashboard')) if current_user.is_authenticated else render_template('landing.html')
@main.route('/health')
def health():
return 'OK'
@main.route('/dashboard')
@login_required
def dashboard():
view = request.args.get('view', 'overview')
if view not in ('overview', 'history', 'trends'):
view = 'overview'
tz = user_timezone()
mode = request.args.get('mode', 'readings') if view == 'history' else 'readings'
calendar = None
try:
selected = parse_range(request.args, tz, 7 if view == 'overview' else 30)
if mode == 'calendar':
unit = request.args.get('unit', 'month')
if unit not in ('week', 'month'):
raise ValueError('Choose a week or month calendar.')
anchor = date.fromisoformat(request.args.get('anchor', datetime.now(tz).date().isoformat()))
if not 1900 <= anchor.year <= 2099:
raise ValueError('Choose a calendar date between 1900 and 2099.')
if unit == 'week':
start = anchor - timedelta(days=anchor.weekday())
end = start + timedelta(days=6)
prev, following = start - timedelta(days=7), start + timedelta(days=7)
else:
start = anchor.replace(day=1)
following = (start + timedelta(days=32)).replace(day=1)
end = following - timedelta(days=1)
prev = (start - timedelta(days=1)).replace(day=1)
selected = DateRange(start, end, tz)
calendar = dict(unit=unit, anchor=anchor.isoformat(), prev=prev.isoformat(), next=following.isoformat())
except ValueError as error:
return render_template('error.html', message=str(error)), 400
query = readings_query(current_user.id, selected)
stats = summary(query)
pagination = None
readings = []
latest = None
chart = None
if view == 'overview':
latest = readings_query(current_user.id).order_by(Reading.timestamp.desc(), Reading.id.desc()).first()
if latest:
annotate([latest], tz)
readings = annotate(query.order_by(Reading.timestamp.desc(), Reading.id.desc()).limit(5).all(), tz)
elif mode != 'calendar':
if view == 'history':
page = max(1, request.args.get('page', 1, type=int))
pagination = query.order_by(Reading.timestamp.desc(), Reading.id.desc()).paginate(page=page, per_page=PAGE_SIZE, error_out=False)
readings = annotate(pagination.items, tz)
if view in ('overview', 'trends') or calendar:
chart = chart_data(query, selected)
if calendar:
by_day = {p['date']: p for p in chart['points']}
calendar['days'] = [dict(date=(selected.start + timedelta(days=i)), data=by_day.get((selected.start + timedelta(days=i)).isoformat()))
for i in range((selected.end - selected.start).days + 1)]
profile = current_user.profile
active_days = (selected.end - selected.start).days + 1 if selected.end == datetime.now(tz).date() else None
return render_template('dashboard.html', view=view, mode=mode, selected=selected, stats=stats,
readings=readings, latest=latest, chart=chart, pagination=pagination,
calendar=calendar, timezone_name=str(tz), active_days=active_days,
sys_threshold=(profile.systolic_threshold or 140) if profile else 140,
dia_threshold=(profile.diastolic_threshold or 90) if profile else 90)
@main.route('/dashboard/report')
@login_required
def report():
tz = user_timezone()
try:
selected = parse_range(request.args, tz, 30)
except ValueError as error:
return render_template('error.html', message=str(error)), 400
query = readings_query(current_user.id, selected)
readings = annotate(query.order_by(Reading.timestamp, Reading.id).all(), tz)
return render_template('report.html', selected=selected, stats=summary(query), readings=readings, timezone_name=str(tz))
# Preserve bookmarks to the former separate views.
@main.route('/dashboard/list')
@main.route('/dashboard/table')
@login_required
def dashboard_table():
params = request.args.to_dict()
params['view'] = 'history'
return redirect(url_for('main.dashboard', **params))
@main.route('/dashboard/graph')
@login_required
def dashboard_graph():
params = request.args.to_dict()
params['view'] = 'trends'
return redirect(url_for('main.dashboard', **params))
@main.route('/dashboard/weekly')
@main.route('/dashboard/monthly')
@login_required
def dashboard_calendar():
unit = 'week' if request.path.endswith('weekly') else 'month'
anchor = datetime.now(user_timezone()).date()
offset = request.args.get('week_offset' if unit == 'week' else 'month_offset', 0, type=int)
try:
if unit == 'week':
anchor += timedelta(weeks=offset)
else:
month_index = anchor.year * 12 + anchor.month - 1 + offset
anchor = date(month_index // 12, month_index % 12 + 1, 1)
except (OverflowError, ValueError):
return render_template('error.html', message='Choose a valid calendar date.'), 400
return redirect(url_for('main.dashboard', view='history', mode='calendar', unit=unit, anchor=anchor.isoformat()))
def _compute_badges(total_readings, timestamps, user_tz, now_local=None):
if now_local is None:
now_local = datetime.now(user_tz).date()
badges = []
if total_readings == 0:
return badges
streak_count = 0
if timestamps:
distinct_dates = []
last_date = None
for (ts,) in timestamps:
local_date = utc.localize(ts).astimezone(user_tz).date()
if local_date != last_date:
distinct_dates.append(local_date)
last_date = local_date
if distinct_dates:
most_recent_date = distinct_dates[0]
if (now_local - most_recent_date).days <= 1:
streak_count = 1
current_check_date = most_recent_date
for d in distinct_dates[1:]:
if (current_check_date - d).days == 1:
streak_count += 1
current_check_date = d
else:
break
if streak_count >= 1:
badges.append(f"Current Streak: {streak_count} Days")
if streak_count >= 7:
badges.append("Logged Every Day for a Week")
if streak_count >= 30:
badges.append("Monthly Streak")
last_7_readings = timestamps[:7]
if len(last_7_readings) == 7:
if all(5 <= utc.localize(ts).astimezone(user_tz).hour < 12 for (ts,) in last_7_readings):
badges.append("Morning Riser: Logged Readings Every Morning for a Week")
if all(18 <= utc.localize(ts).astimezone(user_tz).hour <= 23 for (ts,) in last_7_readings):
badges.append("Night Owl: Logged Readings Every Night for a Week")
milestones = [10, 50, 100, 500, 1000, 5000, 10000]
highest_milestone = max((m for m in milestones if total_readings >= m), default=None)
if highest_milestone:
badges.append(f"{highest_milestone} Readings Logged")
return badges