177 lines
7.5 KiB
Python
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
|