from collections import defaultdict from flask import Blueprint, render_template, redirect, request, url_for import humanize from pytz import timezone, utc from sqlalchemy import func from app.models import Reading, db from app.forms import DeleteForm from flask_login import login_required, current_user from datetime import date, datetime, timedelta main = Blueprint('main', __name__) @main.route('/', methods=['GET']) def landing(): if current_user.is_authenticated: return redirect(url_for('main.dashboard')) return render_template('landing.html') @main.route('/health') def health(): return "OK", 200 @main.route('/dashboard', methods=['GET', 'POST']) @login_required def dashboard(): # Helper function to get first and last reading timestamps def get_reading_date_range(user_id): result = ( db.session.query( func.min(Reading.timestamp).label('first'), func.max(Reading.timestamp).label('last') ) .filter(Reading.user_id == user_id) .first() ) return result.first, result.last # Helper function to calculate weekly summary averages def calculate_weekly_summary(readings): one_week_ago = datetime.now() - timedelta(days=7) weekly_readings = [r for r in readings if r.timestamp >= one_week_ago] if weekly_readings: systolic_avg = round(sum(r.systolic for r in weekly_readings) / len(weekly_readings), 1) diastolic_avg = round(sum(r.diastolic for r in weekly_readings) / len(weekly_readings), 1) heart_rate_avg = round(sum(r.heart_rate for r in weekly_readings) / len(weekly_readings), 1) else: systolic_avg = diastolic_avg = heart_rate_avg = 0 return systolic_avg, diastolic_avg, heart_rate_avg # Helper function to calculate progress badges def calculate_progress_badges(readings): """Calculate badges based on user reading activity.""" badges = [] now = datetime.utcnow().date() # Prepare sorted readings by timestamp sorted_readings = sorted(readings, key=lambda r: r.timestamp) # Incremental milestone badge def highest_milestone(total_readings, milestones): """Determine the highest milestone achieved.""" for milestone in reversed(milestones): if total_readings >= milestone: return f"{milestone} Readings Logged" return None highest_milestone_badge = highest_milestone(len(readings), [10, 50, 100, 500, 1000, 5000, 10000]) if highest_milestone_badge: badges.append(highest_milestone_badge) # Streaks and calendar month badges if sorted_readings: streak_count = 1 daily_streak = True monthly_tracker = defaultdict(int) # Start with the first reading previous_date = sorted_readings[0].timestamp.date() for reading in sorted_readings[1:]: current_date = reading.timestamp.date() # Check for consecutive daily streaks if (current_date - previous_date).days == 1: streak_count += 1 elif (current_date - previous_date).days > 1: daily_streak = False # Track monthly activity monthly_tracker[current_date.strftime('%Y-%m')] += 1 previous_date = current_date # Add streak badges if daily_streak and streak_count >= 1: badges.append(f"Current Streak: {streak_count} Days") if daily_streak and streak_count >= 7: badges.append("Logged Every Day for a Week") if daily_streak and streak_count >= 30 and previous_date == now: badges.append("Monthly Streak") # Add calendar month streak badges for month, count in monthly_tracker.items(): if count >= 30: badges.append(f"Full Month of Logging: {month}") # Time-specific badges (morning/night logging) def is_morning(reading_time): return 5 <= reading_time.hour < 12 def is_night(reading_time): return 18 <= reading_time.hour <= 23 if all(is_morning(r.timestamp) for r in sorted_readings[-7:]): badges.append("Morning Riser: Logged Readings Every Morning for a Week") if all(is_night(r.timestamp) for r in sorted_readings[-7:]): badges.append("Night Owl: Logged Readings Every Night for a Week") return badges def generate_monthly_calendar(readings, selected_date, local_timezone): # Convert selected date to user's timezone and extract the start/end dates today = datetime.now(local_timezone).date() date = selected_date.astimezone(local_timezone).date() first_day_of_month = date.replace(day=1) days_to_subtract = (first_day_of_month.weekday() + 1) % 7 start_date = first_day_of_month - timedelta(days=days_to_subtract) end_date = start_date + timedelta(days=6 * 7 - 1) # Group readings by day readings_by_day = {} for reading in readings: local_date = reading.timestamp.astimezone(local_timezone).date() readings_by_day.setdefault(local_date, []).append(reading) # Build calendar days calendar = [] current_date = start_date while current_date <= end_date: calendar.append({ 'day': current_date.day, 'is_today': current_date == today, 'is_in_current_month': current_date.month == date.month, 'readings': readings_by_day.get(current_date, []), }) current_date += timedelta(days=1) return calendar # Get the first and last reading timestamps first_reading_timestamp, last_reading_timestamp = get_reading_date_range(current_user.id) # Set default start and end dates start_date = first_reading_timestamp.strftime('%Y-%m-%d') if first_reading_timestamp else None end_date = last_reading_timestamp.strftime('%Y-%m-%d') if last_reading_timestamp else None # Handle filtering for POST request readings_query = Reading.query.filter_by(user_id=current_user.id) if request.method == 'POST': start_date = request.form.get('start_date') or start_date end_date = request.form.get('end_date') or end_date if start_date and end_date: readings_query = readings_query.filter( Reading.timestamp >= datetime.strptime(start_date, '%Y-%m-%d'), Reading.timestamp <= datetime.strptime(end_date, '%Y-%m-%d') ) # Fetch readings readings = readings_query.order_by(Reading.timestamp.desc()).all() # Fetch the user's timezone (default to 'UTC' if none is set) user_timezone = current_user.profile.timezone if current_user.profile and current_user.profile.timezone else 'UTC' local_tz = timezone(user_timezone) # Add relative & local timestamps to readings now = datetime.utcnow() for reading in readings: reading.relative_timestamp = humanize.naturaltime(now - reading.timestamp) reading.local_timestamp = utc.localize(reading.timestamp).astimezone(local_tz) month_view = generate_monthly_calendar(readings, now, local_tz) # Calculate weekly summary and progress badges systolic_avg, diastolic_avg, heart_rate_avg = calculate_weekly_summary(readings) badges = calculate_progress_badges(readings) # Prepare graph data timestamps = [reading.timestamp.strftime('%b %d') for reading in readings] systolic = [reading.systolic for reading in readings] diastolic = [reading.diastolic for reading in readings] heart_rate = [reading.heart_rate for reading in readings] # Group readings by date readings_by_date = {} for reading in readings: date_key = reading.timestamp.date() if date_key not in readings_by_date: readings_by_date[date_key] = [] readings_by_date[date_key].append(reading) # Render template return render_template( 'dashboard.html', readings=readings, profile=current_user.profile, badges=badges, systolic_avg=systolic_avg, diastolic_avg=diastolic_avg, heart_rate_avg=heart_rate_avg, delete_form=DeleteForm(), timestamps=timestamps, systolic=systolic, diastolic=diastolic, heart_rate=heart_rate, start_date=start_date, end_date=end_date, readings_by_date=readings_by_date, month = month_view, date=date, timedelta=timedelta )