Compare commits
No commits in common. "57054bb1bd20d96b5e985a1924afd0526fedbc9f" and "feaefba03c8f3e3bd195758d39c993260808b89e" have entirely different histories.
57054bb1bd
...
feaefba03c
3 changed files with 36 additions and 297 deletions
|
|
@ -1,214 +1,47 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% from "macros.html" import trip_link with context %}
|
||||
{% from "macros.html" import trip_link, conference_row with context %}
|
||||
|
||||
{% block title %}Conferences - Edward Betts{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
{% set column_count = 9 %}
|
||||
<style>
|
||||
/* Timeline */
|
||||
.conf-timeline {
|
||||
position: relative;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
.conf-tl-month {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
border-left: 1px solid #dee2e6;
|
||||
pointer-events: none;
|
||||
}
|
||||
.conf-tl-month-label {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
font-size: 0.68em;
|
||||
color: #6c757d;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.conf-tl-today {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
height: calc(100% - 22px);
|
||||
border-left: 2px solid #dc3545;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
.conf-tl-bar {
|
||||
position: absolute;
|
||||
height: 26px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
z-index: 5;
|
||||
}
|
||||
.conf-tl-bar a, .conf-tl-bar span {
|
||||
display: block;
|
||||
padding: 4px 6px;
|
||||
font-size: 0.72em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 18px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat({{ column_count }}, auto); /* 7 columns for each piece of information */
|
||||
gap: 10px;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
/* Bidirectional hover highlight */
|
||||
.conf-tl-bar.conf-hl {
|
||||
filter: brightness(1.25);
|
||||
box-shadow: inset 0 0 0 2px rgba(255,255,255,0.8);
|
||||
z-index: 15;
|
||||
}
|
||||
tr.conf-hl > td {
|
||||
background-color: rgba(255, 193, 7, 0.25) !important;
|
||||
.grid-item {
|
||||
/* Additional styling for grid items can go here */
|
||||
}
|
||||
|
||||
/* Conference table */
|
||||
.conf-month-row td {
|
||||
background: #e9ecef !important;
|
||||
font-weight: 600;
|
||||
font-size: 0.8em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: #495057;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.3rem;
|
||||
border-bottom: none;
|
||||
}
|
||||
.conf-going {
|
||||
--bs-table-bg: rgba(25, 135, 84, 0.07);
|
||||
.heading {
|
||||
grid-column: 1 / {{ column_count + 1 }}; /* Spans from the 1st line to the 7th line */
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% set tl_colors = ["#0d6efd","#198754","#dc3545","#fd7e14","#6f42c1","#20c997","#0dcaf0","#d63384"] %}
|
||||
|
||||
{% macro render_timeline(timeline) %}
|
||||
{% if timeline %}
|
||||
{% set row_h = 32 %}
|
||||
{% set header_h = 22 %}
|
||||
{% set total_h = timeline.lane_count * row_h + header_h %}
|
||||
<div class="mb-4">
|
||||
<h3>Next 90 days</h3>
|
||||
<div class="conf-timeline" style="height: {{ total_h }}px;">
|
||||
|
||||
{% for m in timeline.months %}
|
||||
<div class="conf-tl-month" style="left: {{ m.left_pct }}%;">
|
||||
<span class="conf-tl-month-label">{{ m.label }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="conf-tl-today" style="left: 0;"></div>
|
||||
|
||||
{% for conf in timeline.confs %}
|
||||
{% set color = tl_colors[conf.lane % tl_colors | length] %}
|
||||
{% set top_px = conf.lane * row_h + header_h %}
|
||||
<div class="conf-tl-bar"
|
||||
style="left: {{ conf.left_pct }}%; width: {{ conf.width_pct }}%; top: {{ top_px }}px; background: {{ color }};"
|
||||
title="{{ conf.label }}"
|
||||
data-conf-key="{{ conf.key }}">
|
||||
{% if conf.url %}<a href="{{ conf.url }}">{{ conf.name }}</a>
|
||||
{% else %}<span>{{ conf.name }}</span>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro conf_table(heading, item_list, badge) %}
|
||||
{% macro section(heading, item_list, badge) %}
|
||||
{% if item_list %}
|
||||
{% set count = item_list | length %}
|
||||
<h2>{{ heading }} <small class="text-muted fs-6 fw-normal">{{ count }} conference{{ "" if count == 1 else "s" }}</small></h2>
|
||||
<table class="table table-sm table-hover align-middle mb-4">
|
||||
<colgroup>
|
||||
<col style="width: 9rem">
|
||||
<col>
|
||||
<col style="width: 18rem">
|
||||
<col style="width: 14rem">
|
||||
<col style="width: 7rem">
|
||||
<col style="width: 10rem">
|
||||
</colgroup>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Dates</th>
|
||||
<th>Conference</th>
|
||||
<th>Topic</th>
|
||||
<th>Location</th>
|
||||
<th>CFP ends</th>
|
||||
<th>Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set ns = namespace(prev_month="") %}
|
||||
<div class="heading">
|
||||
|
||||
<h2>{{ heading }}</h2>
|
||||
|
||||
<p>
|
||||
{% set item_count = item_list|length %}
|
||||
{% if item_count == 1 %}{{ item_count }} conference{% else %}{{ item_count }} conferences{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% 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 %}
|
||||
<tr class="conf-month-row">
|
||||
<td colspan="6">{{ month_label }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr{% if item.going %} class="conf-going"{% endif %} data-conf-key="{{ item.start_date.isoformat() }}|{{ item.name }}">
|
||||
<td class="text-nowrap text-muted small">
|
||||
{%- 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 -%}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.url %}<a href="{{ item.url }}">{{ item.name }}</a>
|
||||
{% else %}{{ item.name }}{% endif %}
|
||||
{% if item.going and not (item.accommodation_booked or item.travel_booked) %}
|
||||
<span class="badge text-bg-primary ms-1">{{ badge }}</span>
|
||||
{% endif %}
|
||||
{% if item.accommodation_booked %}
|
||||
<span class="badge text-bg-success ms-1">accommodation</span>
|
||||
{% endif %}
|
||||
{% if item.transport_booked %}
|
||||
<span class="badge text-bg-success ms-1">transport</span>
|
||||
{% endif %}
|
||||
{% if item.linked_trip %}
|
||||
{% set trip = item.linked_trip %}
|
||||
<a href="{{ url_for('trip_page', start=trip.start.isoformat()) }}"
|
||||
class="text-muted ms-1 text-decoration-none"
|
||||
title="Trip: {{ trip.title }}">
|
||||
🧳{% if trip.title != item.name %} {{ trip.title }}{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-muted small">{{ item.topic }}</td>
|
||||
<td class="text-nowrap">
|
||||
{% 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 %}
|
||||
</td>
|
||||
<td class="text-nowrap text-muted small">
|
||||
{% if item.cfp_end %}{{ item.cfp_end.strftime("%-d %b %Y") }}{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.price and item.currency %}
|
||||
<span class="badge bg-info text-nowrap">{{ "{:,d}".format(item.price | int) }} {{ item.currency }}</span>
|
||||
{% if item.currency != "GBP" and item.currency in fx_rate %}
|
||||
<span class="badge bg-info text-nowrap">{{ "{:,.0f}".format(item.price / fx_rate[item.currency]) }} GBP</span>
|
||||
{% endif %}
|
||||
{% elif item.free %}
|
||||
<span class="badge bg-success text-nowrap">free</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{{ conference_row(item, badge) }}
|
||||
<div class="grid-item">
|
||||
{% if item.linked_trip %} trip: {{ trip_link(item.linked_trip) }} {% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
|
@ -216,35 +49,11 @@ tr.conf-hl > td {
|
|||
|
||||
<div class="container-fluid mt-2">
|
||||
<h1>Conferences</h1>
|
||||
|
||||
{{ render_timeline(timeline) }}
|
||||
|
||||
{{ conf_table("Current", current, "attending") }}
|
||||
{{ conf_table("Future", future, "going") }}
|
||||
{{ conf_table("Past", past|reverse|list, "went") }}
|
||||
<div class="grid-container">
|
||||
{{ section("Current", current, "attending") }}
|
||||
{{ section("Future", future, "going") }}
|
||||
{{ section("Past", past|reverse|list, "went") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// Build a map from conf-key → all matching elements (bars + rows)
|
||||
const map = new Map();
|
||||
document.querySelectorAll('[data-conf-key]').forEach(el => {
|
||||
const k = el.dataset.confKey;
|
||||
if (!map.has(k)) map.set(k, []);
|
||||
map.get(k).push(el);
|
||||
});
|
||||
|
||||
function setHighlight(key, on) {
|
||||
(map.get(key) || []).forEach(el => el.classList.toggle('conf-hl', on));
|
||||
}
|
||||
|
||||
map.forEach((els, key) => {
|
||||
els.forEach(el => {
|
||||
el.addEventListener('mouseenter', () => setHighlight(key, true));
|
||||
el.addEventListener('mouseleave', () => setHighlight(key, false));
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
11
update.py
11
update.py
|
|
@ -29,9 +29,6 @@ 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()
|
||||
|
|
@ -321,7 +318,8 @@ 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) or {}
|
||||
existing_data = agenda.thespacedevs.load_cached_launches(rocket_dir)
|
||||
assert existing_data
|
||||
|
||||
if agenda.thespacedevs.is_launches_cache_fresh(rocket_dir):
|
||||
return
|
||||
|
|
@ -413,13 +411,16 @@ 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()
|
||||
|
|
|
|||
71
web_view.py
71
web_view.py
|
|
@ -440,74 +440,6 @@ 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 = [
|
||||
|
|
@ -556,13 +488,10 @@ 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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue