diff --git a/app/routes/main.py b/app/routes/main.py index 976aea6..688f37b 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -12,9 +12,7 @@ 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') + return redirect(url_for('main.dashboard')) if current_user.is_authenticated else render_template('landing.html') @main.route('/health') def health(): @@ -23,58 +21,31 @@ def health(): @main.route('/dashboard', methods=['GET', 'POST']) @login_required def dashboard(): - # Get the first and last reading timestamps - first_reading_timestamp, last_reading_timestamp = get_reading_date_range(current_user.id) + """Render the dashboard with readings, stats, and calendar views.""" + user_tz = timezone(current_user.profile.timezone or 'UTC') - # 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 + # Get date range for readings + first_reading, last_reading = get_reading_date_range(current_user.id) + start_date = request.form.get('start_date') or (first_reading and first_reading.strftime('%Y-%m-%d')) + end_date = request.form.get('end_date') or (last_reading and last_reading.strftime('%Y-%m-%d')) - # 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 + # Fetch filtered readings + readings = fetch_readings(current_user.id, start_date, end_date) 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) + # Annotate readings with relative and localized timestamps + annotate_readings(readings, now, user_tz) - # Calculate weekly summary and progress badges + # Generate calendar view + month_view = generate_monthly_calendar(readings, datetime.now(user_tz), user_tz) + + # Calculate stats and 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] + graph_data = prepare_graph_data(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, @@ -84,140 +55,139 @@ def dashboard(): 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, + month=month_view, date=date, - timedelta=timedelta + timedelta=timedelta, + **graph_data ) -# 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() - ) + """Fetch the earliest and latest reading timestamps for a user.""" + 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 fetch_readings(user_id, start_date, end_date): + """Retrieve readings filtered by date range.""" + query = Reading.query.filter_by(user_id=user_id) + if start_date and end_date: + query = query.filter( + Reading.timestamp >= datetime.strptime(start_date, '%Y-%m-%d'), + Reading.timestamp <= datetime.strptime(end_date, '%Y-%m-%d') + ) + return query.order_by(Reading.timestamp.desc()).all() + +def annotate_readings(readings, now, user_tz): + """Add relative and localized timestamps to readings.""" + for reading in readings: + reading.relative_timestamp = humanize.naturaltime(now - reading.timestamp) + reading.local_timestamp = utc.localize(reading.timestamp).astimezone(user_tz) + def calculate_weekly_summary(readings): - one_week_ago = datetime.now() - timedelta(days=7) + """Calculate averages for the past week.""" + one_week_ago = datetime.utcnow() - 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 + return ( + round(sum(r.systolic for r in weekly_readings) / len(weekly_readings), 1), + round(sum(r.diastolic for r in weekly_readings) / len(weekly_readings), 1), + round(sum(r.heart_rate for r in weekly_readings) / len(weekly_readings), 1), + ) + return 0, 0, 0 -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) +def generate_monthly_calendar(readings, selected_date, local_tz): + """Generate a monthly calendar view.""" + today = datetime.now(local_tz).date() + first_day = selected_date.replace(day=1) + start_date = first_day - timedelta(days=(first_day.weekday() + 1) % 7) + end_date = start_date + timedelta(days=41) - # Group readings by day - readings_by_day = {} + readings_by_day = defaultdict(list) for reading in readings: - local_date = reading.timestamp.astimezone(local_timezone).date() - readings_by_day.setdefault(local_date, []).append(reading) + local_date = reading.timestamp.astimezone(local_tz).date() + readings_by_day[local_date].append(reading) - # Build calendar days - calendar = [] - current_date = start_date - while current_date <= end_date: - calendar.append({ + return [ + { 'day': current_date.day, 'is_today': current_date == today, - 'is_in_current_month': current_date.month == date.month, + 'is_in_current_month': current_date.month == selected_date.month, 'readings': readings_by_day.get(current_date, []), - }) - current_date += timedelta(days=1) + } + for current_date in (start_date + timedelta(days=i) for i in range((end_date - start_date).days + 1)) + ] - return calendar +def prepare_graph_data(readings): + """Prepare data for graph rendering.""" + return { + 'timestamps': [r.timestamp.strftime('%b %d') for r in readings], + 'systolic': [r.systolic for r in readings], + 'diastolic': [r.diastolic for r in readings], + 'heart_rate': [r.heart_rate for r in readings], + } - # Helper function to calculate progress badges def calculate_progress_badges(readings): - """Calculate badges based on user reading activity.""" - badges = [] + """Generate badges based on user activity and milestones.""" now = datetime.utcnow().date() + streak_count, badges = 1, [] - # Prepare sorted readings by timestamp - sorted_readings = sorted(readings, key=lambda r: r.timestamp) + if not readings: + return badges + + sorted_readings = sorted(readings, key=lambda r: r.timestamp.date()) + streak_count = 1 + daily_streak = True + monthly_tracker = defaultdict(int) - # 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 + # Start with the first reading + previous_date = sorted_readings[0].timestamp.date() - highest_milestone_badge = highest_milestone(len(readings), [10, 50, 100, 500, 1000, 5000, 10000]) - if highest_milestone_badge: - badges.append(highest_milestone_badge) + for reading in sorted_readings[1:]: + current_date = reading.timestamp.date() - # Streaks and calendar month badges - if sorted_readings: - streak_count = 1 - daily_streak = True - monthly_tracker = defaultdict(int) + # 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 - # Start with the first reading - previous_date = sorted_readings[0].timestamp.date() + # Track monthly activity + monthly_tracker[current_date.strftime('%Y-%m')] += 1 - for reading in sorted_readings[1:]: - current_date = reading.timestamp.date() + previous_date = current_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 + # 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") - # Track monthly activity - monthly_tracker[current_date.strftime('%Y-%m')] += 1 + if daily_streak and streak_count >= 30 and previous_date == now: + badges.append("Monthly Streak") - previous_date = current_date + # Add calendar month streak badges + for month, count in monthly_tracker.items(): + if count >= 30: + badges.append(f"Full Month of Logging: {month}") - # 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 streak_count >= 7: + badges.append("Logged Every Day for a Week") + if streak_count >= 30 and previous_date == now: + badges.append("Monthly Streak") - 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:]): + if all(5 <= r.timestamp.hour < 12 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:]): + if all(18 <= r.timestamp.hour <= 23 for r in sorted_readings[-7:]): badges.append("Night Owl: Logged Readings Every Night for a Week") - return badges \ No newline at end of file + milestones = [10, 50, 100, 500, 1000, 5000, 10000] + highest_milestone = max((m for m in milestones if len(readings) >= m), default=None) + if highest_milestone: + badges.append(f"{highest_milestone} Readings Logged") + + return badges