diff --git a/templates/conference_list.html b/templates/conference_list.html index a900f7d..7ac5ddc 100644 --- a/templates/conference_list.html +++ b/templates/conference_list.html @@ -1,59 +1,250 @@ {% extends "base.html" %} -{% from "macros.html" import trip_link, conference_row with context %} +{% from "macros.html" import trip_link with context %} {% block title %}Conferences - Edward Betts{% endblock %} {% block style %} -{% set column_count = 9 %} {% endblock %} -{% macro section(heading, item_list, badge) %} - {% if item_list %} -
+{% set tl_colors = ["#0d6efd","#198754","#dc3545","#fd7e14","#6f42c1","#20c997","#0dcaf0","#d63384"] %} -

{{ heading }}

+{% macro render_timeline(timeline) %} +{% if timeline %} +{% set row_h = 32 %} +{% set header_h = 22 %} +{% set total_h = timeline.lane_count * row_h + header_h %} +
+

Next 90 days

+
-

- {% set item_count = item_list|length %} - {% if item_count == 1 %}{{ item_count }} conference{% else %}{{ item_count }} conferences{% endif %} -

-
- - {% for item in item_list %} - {{ conference_row(item, badge) }} -
- {% if item.linked_trip %} trip: {{ trip_link(item.linked_trip) }} {% endif %} + {% for m in timeline.months %} +
+ {{ m.label }}
{% endfor %} - {% endif %} + +
+ + {% for conf in timeline.confs %} + {% set color = tl_colors[conf.lane % tl_colors | length] %} + {% set top_px = conf.lane * row_h + header_h %} +
+ {% if conf.url %}{{ conf.name }} + {% else %}{{ conf.name }}{% endif %} +
+ {% endfor %} + +
+
+{% endif %} +{% endmacro %} + +{% macro conf_table(heading, item_list, badge) %} +{% if item_list %} +{% set count = item_list | length %} +

{{ heading }} {{ count }} conference{{ "" if count == 1 else "s" }}

+ + + + + + + + + + + + + + + + + + + + + {% set ns = namespace(prev_month="") %} + {% for item in item_list %} + {% set month_label = item.start_date.strftime("%B %Y") %} + {% if month_label != ns.prev_month %} + {% set ns.prev_month = month_label %} + + + + {% endif %} + + + + + + + + + {% endfor %} + +
DatesConferenceTopicLocationCFP endsPrice
{{ month_label }}
+ {%- if item.start_date == item.end_date -%} + {{ item.start_date.strftime("%-d %b %Y") }} + {%- elif item.start_date.year == item.end_date.year and item.start_date.month == item.end_date.month -%} + {{ item.start_date.strftime("%-d") }}–{{ item.end_date.strftime("%-d %b %Y") }} + {%- else -%} + {{ item.start_date.strftime("%-d %b") }}–{{ item.end_date.strftime("%-d %b %Y") }} + {%- endif -%} + + {% if item.url %}{{ item.name }} + {% else %}{{ item.name }}{% endif %} + {% if item.going and not (item.accommodation_booked or item.travel_booked) %} + {{ badge }} + {% endif %} + {% if item.accommodation_booked %} + accommodation + {% endif %} + {% if item.transport_booked %} + transport + {% endif %} + {% if item.linked_trip %} + {% set trip = item.linked_trip %} + + 🧳{% if trip.title != item.name %} {{ trip.title }}{% endif %} + + {% endif %} + {{ item.topic }} + {% set country = get_country(item.country) if item.country else None %} + {% if country %}{{ country.flag }} {{ item.location }} + {% elif item.online %}💻 Online + {% else %}{{ item.location }}{% endif %} + + {% if item.cfp_end %}{{ item.cfp_end.strftime("%-d %b %Y") }}{% endif %} + + {% if item.price and item.currency %} + {{ "{:,d}".format(item.price | int) }} {{ item.currency }} + {% if item.currency != "GBP" and item.currency in fx_rate %} + {{ "{:,.0f}".format(item.price / fx_rate[item.currency]) }} GBP + {% endif %} + {% elif item.free %} + free + {% endif %} +
+{% endif %} {% endmacro %} {% block content %}

Conferences

-
- {{ section("Current", current, "attending") }} - {{ section("Future", future, "going") }} - {{ section("Past", past|reverse|list, "went") }} -
+ + {{ render_timeline(timeline) }} + + {{ conf_table("Current", current, "attending") }} + {{ conf_table("Future", future, "going") }} + {{ conf_table("Past", past|reverse|list, "went") }}
+ + {% endblock %} diff --git a/update.py b/update.py index 00460c2..efa4bd9 100755 --- a/update.py +++ b/update.py @@ -29,6 +29,9 @@ from agenda.types import StrDict from web_view import app +from datetime import date, timedelta + + async def update_bank_holidays(config: flask.config.Config) -> None: """Update cached copy of UK Bank holidays.""" t0 = time() @@ -318,8 +321,7 @@ def update_thespacedevs(config: flask.config.Config) -> None: """ rocket_dir = os.path.join(config["DATA_DIR"], "thespacedevs") - existing_data = agenda.thespacedevs.load_cached_launches(rocket_dir) - assert existing_data + existing_data = agenda.thespacedevs.load_cached_launches(rocket_dir) or {} if agenda.thespacedevs.is_launches_cache_fresh(rocket_dir): return @@ -411,16 +413,13 @@ def update_gandi(config: flask.config.Config) -> None: def update_weather(config: flask.config.Config) -> None: """Refresh weather cache for home and all upcoming trips.""" - from datetime import date, timedelta today = date.today() forecast_window = today + timedelta(days=8) trips = agenda.trip.build_trip_list() upcoming = [ - t - for t in trips - if (t.end or t.start) >= today and t.start <= forecast_window + t for t in trips if (t.end or t.start) >= today and t.start <= forecast_window ] seen: set[tuple[float, float]] = set() diff --git a/web_view.py b/web_view.py index 4bd5203..8ba8b25 100755 --- a/web_view.py +++ b/web_view.py @@ -440,6 +440,74 @@ def _conference_description(conf: StrDict) -> str: return "\n".join(lines) if lines else "Conference" +def build_conference_timeline( + current: list[StrDict], future: list[StrDict], today: date, days: int = 90 +) -> dict | None: + """Build data for a Gantt-style timeline of upcoming conferences.""" + timeline_start = today + timeline_end = today + timedelta(days=days) + + visible = [ + c + for c in (current + future) + if c["start_date"] <= timeline_end and c["end_date"] >= today + ] + if not visible: + return None + + visible.sort(key=lambda c: c["start_date"]) + + # Greedy interval-coloring: assign each conference a lane (row) + lane_ends: list[date] = [] + conf_data = [] + for conf in visible: + lane = next( + (i for i, end in enumerate(lane_ends) if end < conf["start_date"]), + len(lane_ends), + ) + if lane == len(lane_ends): + lane_ends.append(conf["end_date"]) + else: + lane_ends[lane] = conf["end_date"] + + start_off = max((conf["start_date"] - timeline_start).days, 0) + end_off = min((conf["end_date"] - timeline_start).days + 1, days) + left_pct = round(start_off / days * 100, 2) + width_pct = max(round((end_off - start_off) / days * 100, 2), 0.5) + + conf_data.append( + { + "name": conf["name"], + "url": conf.get("url"), + "lane": lane, + "left_pct": left_pct, + "width_pct": width_pct, + "key": f"{conf['start_date'].isoformat()}|{conf['name']}", + "label": ( + f"{conf['name']}" + f" ({conf['start_date'].strftime('%-d %b')}–" + f"{conf['end_date'].strftime('%-d %b')})" + ), + } + ) + + # Month markers for x-axis labels + months = [] + d = today.replace(day=1) + while d <= timeline_end: + off = max((d - timeline_start).days, 0) + months.append({"label": d.strftime("%b %Y"), "left_pct": round(off / days * 100, 2)}) + # advance to next month + d = (d.replace(day=28) + timedelta(days=4)).replace(day=1) + + return { + "confs": conf_data, + "lane_count": len(lane_ends), + "months": months, + "days": days, + } + + def build_conference_ical(items: list[StrDict]) -> bytes: """Build iCalendar feed for all conferences.""" lines = [ @@ -488,10 +556,13 @@ def conference_list() -> str: ] future = [conf for conf in items if conf["start_date"] > today] + timeline = build_conference_timeline(current, future, today) + return flask.render_template( "conference_list.html", current=current, future=future, + timeline=timeline, today=today, get_country=agenda.get_country, fx_rate=agenda.fx.get_rates(app.config),