|
| 1 | +"""Calendar data helpers for the tracekit web app.""" |
| 2 | + |
| 3 | +import calendar as _cal |
| 4 | +from datetime import UTC, datetime |
| 5 | +from typing import Any |
| 6 | + |
| 7 | +from db_init import _init_db |
| 8 | +from helpers import get_current_date_in_timezone |
| 9 | + |
| 10 | + |
| 11 | +def get_sync_calendar_data(config: dict[str, Any]) -> dict[str, Any]: |
| 12 | + """Compatibility shim — returns full calendar data (used by tests). |
| 13 | +
|
| 14 | + In production the page uses get_calendar_shell + get_single_month_data |
| 15 | + so that each month loads independently. This function still works for |
| 16 | + the test suite which imports it directly. |
| 17 | + """ |
| 18 | + shell = get_calendar_shell(config) |
| 19 | + if shell.get("error"): |
| 20 | + return shell |
| 21 | + |
| 22 | + months_with_data = [] |
| 23 | + for stub in shell["months"]: |
| 24 | + month_data = get_single_month_data(config, stub["year_month"]) |
| 25 | + if month_data.get("error"): |
| 26 | + months_with_data.append(stub) |
| 27 | + else: |
| 28 | + months_with_data.append(month_data) |
| 29 | + |
| 30 | + return { |
| 31 | + "months": months_with_data, |
| 32 | + "providers": shell["providers"], |
| 33 | + "date_range": shell["date_range"], |
| 34 | + "total_months": shell["total_months"], |
| 35 | + } |
| 36 | + |
| 37 | + |
| 38 | +def get_calendar_shell(config: dict[str, Any] | None = None) -> dict[str, Any]: |
| 39 | + """Return month stubs and providers list — no activity table scans.""" |
| 40 | + if not _init_db(): |
| 41 | + return {"error": "Database not available"} |
| 42 | + |
| 43 | + try: |
| 44 | + from tracekit.db import get_db |
| 45 | + from tracekit.provider_sync import ProviderSync |
| 46 | + |
| 47 | + db = get_db() |
| 48 | + db.connect(reuse_if_open=True) |
| 49 | + |
| 50 | + rows = ProviderSync.select(ProviderSync.year_month, ProviderSync.provider).order_by( |
| 51 | + ProviderSync.year_month, ProviderSync.provider |
| 52 | + ) |
| 53 | + records = [(r.year_month, r.provider) for r in rows] |
| 54 | + |
| 55 | + if not records: |
| 56 | + return {"months": [], "providers": [], "date_range": (None, None), "total_months": 0} |
| 57 | + |
| 58 | + year_months_all = [r[0] for r in records] |
| 59 | + date_range = (min(year_months_all), max(year_months_all)) |
| 60 | + providers = sorted({r[1] for r in records}) |
| 61 | + |
| 62 | + start_year, start_month = map(int, date_range[0].split("-")) |
| 63 | + end_year, end_month = map(int, date_range[1].split("-")) |
| 64 | + |
| 65 | + current_date = get_current_date_in_timezone(config) |
| 66 | + current_ym = f"{current_date.year:04d}-{current_date.month:02d}" |
| 67 | + if current_ym > date_range[1]: |
| 68 | + end_year, end_month = current_date.year, current_date.month |
| 69 | + |
| 70 | + all_months = [] |
| 71 | + year, month = start_year, start_month |
| 72 | + while year < end_year or (year == end_year and month <= end_month): |
| 73 | + ym = f"{year:04d}-{month:02d}" |
| 74 | + all_months.append( |
| 75 | + { |
| 76 | + "year_month": ym, |
| 77 | + "year": year, |
| 78 | + "month": month, |
| 79 | + "month_name": datetime(year, month, 1).strftime("%B"), |
| 80 | + } |
| 81 | + ) |
| 82 | + month += 1 |
| 83 | + if month > 12: |
| 84 | + month = 1 |
| 85 | + year += 1 |
| 86 | + |
| 87 | + return { |
| 88 | + "months": all_months, |
| 89 | + "providers": providers, |
| 90 | + "date_range": date_range, |
| 91 | + "total_months": len(all_months), |
| 92 | + } |
| 93 | + except Exception as e: |
| 94 | + return {"error": f"Database error: {e}"} |
| 95 | + |
| 96 | + |
| 97 | +def get_single_month_data(config: dict[str, Any] | None, year_month: str) -> dict[str, Any]: |
| 98 | + """Return sync status and activity counts for one month. |
| 99 | +
|
| 100 | + Activity queries are scoped to the month's timestamp range so this is |
| 101 | + fast even for large databases. |
| 102 | + """ |
| 103 | + if not _init_db(): |
| 104 | + return {"error": "Database not available"} |
| 105 | + |
| 106 | + try: |
| 107 | + from tracekit.db import get_db |
| 108 | + from tracekit.provider_sync import ProviderSync |
| 109 | + from tracekit.providers.file.file_activity import FileActivity |
| 110 | + from tracekit.providers.garmin.garmin_activity import GarminActivity |
| 111 | + from tracekit.providers.ridewithgps.ridewithgps_activity import RideWithGPSActivity |
| 112 | + from tracekit.providers.spreadsheet.spreadsheet_activity import SpreadsheetActivity |
| 113 | + from tracekit.providers.strava.strava_activity import StravaActivity |
| 114 | + from tracekit.providers.stravajson.stravajson_activity import StravaJsonActivity |
| 115 | + |
| 116 | + db = get_db() |
| 117 | + db.connect(reuse_if_open=True) |
| 118 | + |
| 119 | + synced_rows = ProviderSync.select(ProviderSync.provider).where(ProviderSync.year_month == year_month) |
| 120 | + synced_providers = [r.provider for r in synced_rows] |
| 121 | + |
| 122 | + all_rows = ProviderSync.select(ProviderSync.provider).distinct() |
| 123 | + providers = sorted({r.provider for r in all_rows}) |
| 124 | + |
| 125 | + provider_status = {p: p in synced_providers for p in providers} |
| 126 | + |
| 127 | + year_int, month_int = map(int, year_month.split("-")) |
| 128 | + start_ts = int(datetime(year_int, month_int, 1, tzinfo=UTC).timestamp()) |
| 129 | + last_day = _cal.monthrange(year_int, month_int)[1] |
| 130 | + end_ts = int(datetime(year_int, month_int, last_day, 23, 59, 59, tzinfo=UTC).timestamp()) |
| 131 | + |
| 132 | + provider_models = { |
| 133 | + "strava": StravaActivity, |
| 134 | + "garmin": GarminActivity, |
| 135 | + "ridewithgps": RideWithGPSActivity, |
| 136 | + "spreadsheet": SpreadsheetActivity, |
| 137 | + "file": FileActivity, |
| 138 | + "stravajson": StravaJsonActivity, |
| 139 | + } |
| 140 | + |
| 141 | + activity_counts: dict[str, int] = {} |
| 142 | + for provider, model in provider_models.items(): |
| 143 | + try: |
| 144 | + count = ( |
| 145 | + model.select() |
| 146 | + .where( |
| 147 | + model.start_time.is_null(False) & (model.start_time >= start_ts) & (model.start_time <= end_ts) |
| 148 | + ) |
| 149 | + .count() |
| 150 | + ) |
| 151 | + if count > 0: |
| 152 | + activity_counts[provider] = count |
| 153 | + except Exception as e: |
| 154 | + print(f"Error counting {provider} activities for {year_month}: {e}") |
| 155 | + |
| 156 | + total_activities = sum(activity_counts.values()) |
| 157 | + |
| 158 | + return { |
| 159 | + "year_month": year_month, |
| 160 | + "year": year_int, |
| 161 | + "month": month_int, |
| 162 | + "month_name": datetime(year_int, month_int, 1).strftime("%B"), |
| 163 | + "providers": providers, |
| 164 | + "synced_providers": synced_providers, |
| 165 | + "provider_status": provider_status, |
| 166 | + "activity_counts": activity_counts, |
| 167 | + "total_activities": total_activities, |
| 168 | + } |
| 169 | + except Exception as e: |
| 170 | + return {"error": f"Database error: {e}"} |
0 commit comments