diff --git a/agenda/add_new_conference.py b/agenda/add_new_conference.py index 71c12ce..e46df3c 100644 --- a/agenda/add_new_conference.py +++ b/agenda/add_new_conference.py @@ -16,8 +16,6 @@ import pycountry import requests import yaml -from agenda.conference import conference_date_fields - USER_AGENT = "add-new-conference/0.1" COORDINATE_PATTERNS = ( re.compile(r"@(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)"), @@ -162,9 +160,7 @@ def webpage_to_text(root: lxml.html.HtmlElement) -> str: text_maker = html2text.HTML2Text() text_maker.ignore_links = True text_maker.ignore_images = True - return typing.cast( - str, text_maker.handle(lxml.html.tostring(root_copy, encoding="unicode")) - ) + return text_maker.handle(lxml.html.tostring(root_copy, encoding="unicode")) def parse_osm_url(url: str) -> tuple[float, float] | None: @@ -271,13 +267,13 @@ def insert_sorted( ) -> list[dict[str, typing.Any]]: """Insert a conference sorted by start date and skip duplicate URLs.""" new_url = new_conf.get("url") - new_start = conference_sort_datetime(new_conf) + new_start = parse_date(str(new_conf["start"])) new_year = new_start.year if new_url: for conf in conferences: if conf.get("url") == new_url: - existing_start = conference_sort_datetime(conf) + existing_start = parse_date(str(conf["start"])) existing_year = existing_start.year if url_has_year_component(new_url): @@ -291,7 +287,7 @@ def insert_sorted( return conferences for idx, conf in enumerate(conferences): - existing_start = conference_sort_datetime(conf) + existing_start = parse_date(str(conf["start"])) if new_start < existing_start: conferences.insert(idx, new_conf) return conferences @@ -299,14 +295,6 @@ def insert_sorted( return conferences -def conference_sort_datetime(conf: dict[str, typing.Any]) -> datetime: - """Return conference sort date as a datetime.""" - sort_date = conference_date_fields(conf)["sort_date"] - if isinstance(sort_date, datetime): - return sort_date - return datetime.combine(sort_date, time()) - - def validate_country(conf: dict[str, typing.Any]) -> None: """Ensure country is a valid ISO 3166-1 alpha-2 code, normalise if possible.""" country = conf.get("country") @@ -389,11 +377,7 @@ def maybe_extract_explicit_end_time(source_text: str) -> int | None: def normalise_end_field(new_conf: dict[str, typing.Any], source_text: str) -> None: """Ensure an end value exists, with a Geomob-specific fallback.""" - dates = new_conf.get("dates") - nested_dates = dates if isinstance(dates, dict) else None - start_value = ( - nested_dates.get("start") if nested_dates is not None else new_conf.get("start") - ) + start_value = new_conf.get("start") if start_value is None: return @@ -411,16 +395,9 @@ def normalise_end_field(new_conf: dict[str, typing.Any], source_text: str) -> No end_hour = 22 geomob_end = start_dt.replace(hour=end_hour, minute=0, second=0, microsecond=0) - end_value = same_type_as_start(start_value, geomob_end, prefer_datetime=True) - if nested_dates is not None: - nested_dates["end"] = end_value - else: - new_conf["end"] = end_value - return - - if nested_dates is not None: - if "end" not in nested_dates: - nested_dates["end"] = same_type_as_start(start_value, start_dt) + new_conf["end"] = same_type_as_start( + start_value, geomob_end, prefer_datetime=True + ) return if "end" not in new_conf: diff --git a/agenda/conference.py b/agenda/conference.py index f9e15b4..642f792 100644 --- a/agenda/conference.py +++ b/agenda/conference.py @@ -2,18 +2,14 @@ import dataclasses import decimal -import typing from datetime import date, datetime import yaml from . import utils from .event import Event -from .types import DateOrDateTime, StrDict MAX_CONF_DAYS = 20 -DATE_STATUSES = {"exact", "approximate", "tentative"} -DATED_STATUSES = {"exact", "tentative"} @dataclasses.dataclass @@ -23,8 +19,8 @@ class Conference: name: str topic: str location: str - start: date | datetime | None = None - end: date | datetime | None = None + start: date | datetime + end: date | datetime trip: date | None = None country: str | None = None venue: str | None = None @@ -50,7 +46,6 @@ class Conference: attendees: int | None = None hashtag: str | None = None description: str | None = None - dates: StrDict | None = None @property def display_name(self) -> str: @@ -62,135 +57,22 @@ class Conference: ) -def _date_range_label(start: date, end: date) -> str: - """Format conference date range for display.""" - if start == end: - return start.strftime("%a %-d %b %Y") - if start.year == end.year and start.month == end.month: - return f"{start.strftime('%a %-d')}-{end.strftime('%-d %b %Y')}" - return f"{start.strftime('%a %-d %b')}-{end.strftime('%-d %b %Y')}" - - -def _require_date_value(value: typing.Any, field_name: str) -> DateOrDateTime: - """Return date-like field value or raise ValueError.""" - if isinstance(value, (date, datetime)): - return value - raise ValueError(f"conference dates field {field_name!r} must be a date/datetime") - - -def _require_date_only(value: typing.Any, field_name: str) -> date: - """Return field value as a date or raise ValueError.""" - return typing.cast(date, utils.as_date(_require_date_value(value, field_name))) - - -def conference_date_fields(item: StrDict) -> StrDict: - """Return derived date fields for a conference YAML item.""" - raw_dates = item.get("dates") - if raw_dates is None: - status = typing.cast(str, item.get("date_status", "exact")) - if status not in DATE_STATUSES: - raise ValueError(f"unknown conference date status {status!r}") - start = _require_date_value(item.get("start"), "start") - end = _require_date_value(item.get("end", start), "end") - start_date = utils.as_date(start) - end_date = utils.as_date(end) - return { - "date_status": status, - "start": start, - "end": end, - "start_date": start_date, - "end_date": end_date, - "sort_date": start_date, - "latest_date": end_date, - "display_date": _date_range_label(start_date, end_date), - "has_exact_dates": status == "exact", - } - - if not isinstance(raw_dates, dict): - raise ValueError("conference dates must be a mapping") - - status_value = raw_dates.get("status", "exact") - if not isinstance(status_value, str) or status_value not in DATE_STATUSES: - raise ValueError(f"unknown conference date status {status_value!r}") - - if status_value in DATED_STATUSES: - start = _require_date_value(raw_dates.get("start", item.get("start")), "start") - end = _require_date_value(raw_dates.get("end", item.get("end", start)), "end") - start_date = utils.as_date(start) - end_date = utils.as_date(end) - label = raw_dates.get("label") - display_date = ( - label if isinstance(label, str) else _date_range_label(start_date, end_date) - ) - return { - "date_status": status_value, - "start": start, - "end": end, - "start_date": start_date, - "end_date": end_date, - "sort_date": start_date, - "latest_date": end_date, - "display_date": display_date, - "has_exact_dates": status_value == "exact", - } - - earliest = _require_date_only(raw_dates.get("earliest"), "earliest") - latest = _require_date_only(raw_dates.get("latest"), "latest") - label = raw_dates.get("label") - display_date = ( - label if isinstance(label, str) else _date_range_label(earliest, latest) - ) - return { - "date_status": status_value, - "start_date": earliest, - "end_date": latest, - "sort_date": earliest, - "latest_date": latest, - "display_date": display_date, - "has_exact_dates": False, - } - - -def validate_conference_date_fields(item: StrDict) -> StrDict: - """Validate conference date fields and return derived values.""" - fields = conference_date_fields(item) - if fields["start_date"] > fields["end_date"]: - raise ValueError("conference ends before it starts") - - if fields["date_status"] in DATED_STATUSES: - duration = (fields["end_date"] - fields["start_date"]).days - if duration >= MAX_CONF_DAYS: - raise ValueError( - f"conference is {duration} days; maximum is {MAX_CONF_DAYS - 1}" - ) - return fields - - def get_list(filepath: str) -> list[Event]: """Read conferences from a YAML file and return a list of Event objects.""" events: list[Event] = [] - for item in yaml.safe_load(open(filepath, "r")): - try: - fields = validate_conference_date_fields(item) - except ValueError as exc: - raise AssertionError(str(exc)) from exc - normalized_item = dict(item) - if "start" in fields: - normalized_item["start"] = fields["start"] - normalized_item["end"] = fields["end"] - conf = Conference(**normalized_item) - - if fields["has_exact_dates"]: - assert conf.start is not None and conf.end is not None - event = Event( - name="conference", - date=conf.start, - end_date=conf.end, - title=conf.display_name, - url=conf.url, - going=conf.going, - ) - events.append(event) + for conf in (Conference(**conf) for conf in yaml.safe_load(open(filepath, "r"))): + assert conf.start <= conf.end + duration = (utils.as_date(conf.end) - utils.as_date(conf.start)).days + assert duration < MAX_CONF_DAYS + event = Event( + name="conference", + date=conf.start, + end_date=conf.end, + title=conf.display_name, + url=conf.url, + going=conf.going, + ) + events.append(event) if not conf.cfp_end: continue cfp_end_event = Event( diff --git a/docs/personal-data-yaml.md b/docs/personal-data-yaml.md deleted file mode 100644 index a538fff..0000000 --- a/docs/personal-data-yaml.md +++ /dev/null @@ -1,779 +0,0 @@ -# Personal Data YAML Formats - -This document describes the YAML files read from `../personal-data/`. It is intended for humans and LLMs generating new entries. - -## General Rules - -- Use YAML lists for most files. `airports.yaml` is a mapping keyed by IATA code. -- Use ISO-like YAML dates and datetimes: - - Date: `2026-03-14` - - Datetime with timezone: `2026-03-14 09:30:00+01:00` -- Use lowercase ISO 3166-1 alpha-2 country codes, for example `gb`, `be`, `us`. -- Use quoted strings for prices and identifiers that might otherwise be parsed as numbers: `'154.34'`, `'06525269'`, `'0042'`. -- Currencies must be in `config.CURRENCIES` or `GBP`. -- Travel and trip-related entries are grouped by the `trip` date. That date should match an entry in `trips.yaml` when a named trip is needed, but trip groups can also be created from travel/accommodation/conference entries. -- Keep chronological files sorted by their natural start field. `validate_yaml.py` checks ordering for trips, flights, trains, ferries, conferences, and accommodation. -- Coordinates are `latitude` then `longitude`, both numeric. - -## Cross-File References - -- `flights.yaml` flight `airline` values reference `airlines.yaml` `iata`. -- `flights.yaml` flight `from` and `to` values reference `airports.yaml` keys. -- `trains.yaml` journey and leg `from` and `to` values reference `stations.yaml` `name`. -- `ferries.yaml` `from` and `to` values reference `ferry_terminals.yaml` `name`. -- `buses.yaml` `from` and `to` values reference `bus_stops.yaml` `name`. -- `coaches.yaml` `from` and `to` values reference `coach_stations.yaml` `name`. -- Station, stop, and terminal `routes` values name GeoJSON files without the `.geojson` extension. - -## `accommodation.yaml` - -Top-level shape: list of accommodation stays. - -Used by: agenda events, trip pages, trip maps, busy/location logic. - -Required fields: - -- `type`: accommodation category such as `hotel`, `apartment`, `airbnb`. -- `name`: property name. -- `country`: lowercase country code. -- `location`: city or place name. -- `trip`: trip start date. -- `from`: check-in datetime. -- `to`: check-out datetime. - -Common optional fields: - -- Booking: `operator`, `booking_reference`, `confirmation_code`, `booking_url`, `url`, `email`, `phone`. -- Money: `price`, `currency`, `room_rate`, `estimated_taxes`, `estimated_additional_fees`. -- Room/stay: `address`, `room_type`, `room_name`, `room_number`, `number_of_adults`, `breakfast_included`, `breakfast`, `cancellation_policy`, `free_cancellation`, `refundable`. -- Coordinates/IDs: `latitude`, `longitude`, `timezone`, `osm_node`, `wikidata`. -- Loyalty: `rewards`, `radisson_rewards_number`. - -Example: - -```yaml -- type: hotel - operator: Example Hotels - name: Example Central Hotel - location: Brussels - country: be - trip: 2026-02-06 - from: 2026-02-06 15:00:00+01:00 - to: 2026-02-09 11:00:00+01:00 - address: 1 Example Street, Brussels - confirmation_code: ABC123 - price: '312.50' - currency: EUR - number_of_adults: 1 - room_type: Standard double - breakfast_included: true - latitude: 50.8466 - longitude: 4.3528 -``` - -## `airlines.yaml` - -Top-level shape: list of airlines. - -Used by: flight loading and display. - -Required fields: - -- `iata`: two-character IATA airline code. -- `icao`: three-character ICAO airline code. -- `name`: airline name. - -Optional fields: - -- `flight_number_prefer_icao`: boolean. When true, display flight numbers with the ICAO code instead of the IATA code. - -Example: - -```yaml -- iata: BA - icao: BAW - name: British Airways -- iata: U2 - icao: EZY - name: easyJet - flight_number_prefer_icao: true -``` - -## `airports.yaml` - -Top-level shape: mapping keyed by IATA airport code. - -Used by: flight loading, distance calculation, maps, unbooked route hints. - -Required fields for each airport: - -- `iata`: IATA code. Should match the mapping key. -- `name`: airport name. -- `city`: city or main served place. -- `country`: lowercase country code. -- `latitude`, `longitude`: numeric coordinates. -- `qid`: Wikidata QID. - -Optional fields: - -- `alt_name`: display name override used in labels. -- `elevation`: metres. -- `website`, `url`. - -Example: - -```yaml -BRU: - iata: BRU - name: Brussels Airport - city: Brussels - country: be - qid: Q220613 - latitude: 50.9014 - longitude: 4.4844 - elevation: 56 - website: https://www.brusselsairport.be/ -``` - -## `bus_stops.yaml` - -Top-level shape: list of bus stops. - -Used by: bus trip loading, maps, route rendering. - -Required fields: - -- `name`: stop name referenced by `buses.yaml`. -- `city`: city or place. -- `country`: lowercase country code. -- `latitude`, `longitude`: numeric coordinates. -- `routes`: mapping from destination stop name to GeoJSON filename without `.geojson`. - -Optional fields: - -- `Atco`: UK ATCO stop code. -- `osm_node`. - -Example: - -```yaml -- name: West Street - city: Bristol - country: gb - Atco: '0100BRA10073' - osm_node: 485403178 - latitude: 51.4393854 - longitude: -2.6017977 - routes: - Bristol Airport: West_Street_to_Bristol_Airport -``` - -## `buses.yaml` - -Top-level shape: list of bus journeys. - -Used by: trip loading, maps, trip timeline. Bus journeys are not counted for Schengen tracking. - -Required fields: - -- `trip`: trip start date. -- `depart`: departure datetime. -- `arrive`: arrival datetime. `validate_yaml.py` requires arrival after departure and duration no more than 12 hours. -- `from`, `to`: names from `bus_stops.yaml`. - -Optional fields: - -- `operator`, `price`, `currency`. - -Example: - -```yaml -- trip: 2026-03-14 - depart: 2026-03-14 08:20:00+00:00 - arrive: 2026-03-14 08:55:00+00:00 - from: West Street - to: Bristol Airport - operator: First Bus - price: '2.00' - currency: GBP -``` - -## `coach_stations.yaml` - -Top-level shape: list of coach stations. - -Used by: coach trip loading, maps, route rendering. - -Fields are the same pattern as `bus_stops.yaml`, except entries describe coach stations. - -Example: - -```yaml -- name: Example Coach Station - city: Example City - country: gb - latitude: 51.4500 - longitude: -2.5800 - routes: - Other Coach Station: example_city_to_other_city -``` - -## `coaches.yaml` - -Top-level shape: list of coach journeys. - -Used by: trip loading, maps, trip timeline. Coach journeys are not counted for Schengen tracking. - -Required fields: - -- `trip`, `depart`, `arrive`, `from`, `to`. -- `from` and `to` must be names from `coach_stations.yaml`. - -Optional fields: - -- `operator`, `class`, `booking_reference`, `price`, `currency`, `price_details`. - -Example: - -```yaml -- booking_reference: ABC123 - trip: 2026-05-25 - price: '55.00' - currency: GBP - depart: 2026-05-26 14:45:00+01:00 - arrive: 2026-05-26 18:30:00+01:00 - from: Example Coach Station - to: Other Coach Station - operator: Example Coaches - class: Standard - price_details: - base_fare: '55.00' -``` - -## `conferences.yaml` - -Top-level shape: list of conferences and conference-like events. - -Used by: agenda events, trip pages, trip maps, conference list, CFP reminders. - -Required fields: - -- `name`: event name. -- `topic`: topic/category. -- `location`: city or location label. -- Date information, either as legacy top-level `start` and `end`, or preferred nested `dates`. - -Preferred `dates` fields: - -- `status`: one of `exact`, `tentative`, or `approximate`. -- For `exact` and `tentative`: `start` and `end` dates/datetimes. `end` must be no earlier than `start`, and duration must be under 20 days. -- For `approximate`: `earliest` and `latest` dates for sorting/past-future filtering. -- `label`: optional human-readable date text. Recommended for `tentative` and `approximate`, for example `likely first weekend of February 2027` or `March 2027`. -- `basis`: optional explanation of why a tentative date is expected. - -Date status behavior: - -- `exact`: confirmed dates. These create agenda events, iCalendar entries, and timeline bars. -- `tentative`: guessed or unconfirmed exact dates. These appear on the conference list with a status badge, but do not create agenda/iCalendar events or timeline bars. -- `approximate`: only a broad date range is known. These appear on the conference list with a status badge, but do not create agenda/iCalendar events or timeline bars. - -Legacy fields: - -- Existing top-level `start` and `end` are still supported and are treated as `exact` unless `date_status` says otherwise. - -Common optional fields: - -- Trip/location: `trip`, `country`, `venue`, `address`, `latitude`, `longitude`. -- Attendance: `going`, `registered`, `speaking`, `online`, `accommodation_booked`, `transport_booked`. -- Partial attendance: `attend_start`, `attend_end`. These may be dates or timezone-aware datetimes and are used on trip pages instead of official dates. -- Web/CFP: `url`, `cfp_end`, `cfp_url`, `hashtag`, `description`. -- Money/tickets: `free`, `price`, `currency`, `ticket_type`. -- Other flags: `hackathon`, `attendees`. - -Exact example: - -```yaml -- name: FOSDEM - topic: FOSDEM - location: Brussels - country: be - trip: 2026-02-06 - dates: - status: exact - start: 2026-02-07 - end: 2026-02-08 - attend_start: 2026-02-07 14:00:00+01:00 - attend_end: 2026-02-08 - going: true - registered: true - accommodation_booked: true - transport_booked: true - url: https://fosdem.org/2026/ - venue: Universite Libre de Bruxelles - address: Av. Franklin Roosevelt 50, 1050 Bruxelles, Belgium - latitude: 50.8132 - longitude: 4.3822 -``` - -Tentative example: - -```yaml -- name: FOSDEM - topic: FOSDEM - location: Brussels - country: be - dates: - status: tentative - start: 2027-01-30 - end: 2027-01-31 - label: likely first weekend of February 2027 - basis: FOSDEM is usually on the weekend where Sunday is the first Sunday in February - url: https://fosdem.org/2027/ -``` - -Approximate examples: - -```yaml -- name: Wikimedia Hackathon 2027 - topic: Wikimedia - location: Albania - country: al - dates: - status: approximate - label: mid-April 2027 - earliest: 2027-04-11 - latest: 2027-04-20 - hackathon: true - -- name: PyCascades 2027 - topic: Python - location: TBC - dates: - status: approximate - label: March 2027 - earliest: 2027-03-01 - latest: 2027-03-31 -``` - -## `entities.yaml` - -Top-level shape: list of people/entities. - -Used by: birthday events. - -Required fields for birthday support: - -- `name`: full name. -- `label`: display name. -- `type`: for example `human`. -- `birthday`: mapping with `day`, `month`, and optionally `year`. - -Optional fields: - -- `relation`, `email`. - -If `birthday.year` is omitted, age is shown as unknown. - -Example: - -```yaml -- name: Ada Example - label: Ada - type: human - relation: friend - birthday: - day: 10 - month: 12 - year: 1990 -``` - -## `events.yaml` - -Top-level shape: list of general events. - -Used by: agenda events and trip pages. - -Required fields: - -- `name`: event type. -- One date source: - - `date`: single event date/datetime, or - - `start_date`: used for events with a separate `end_date`, or - - `rrule`: recurrence rule string. - -Optional fields: - -- `title`: display title. -- `end_date`: explicit end date/datetime. -- `duration`: ISO 8601 duration such as `PT2H`, `P1D`. -- `url`. -- Trip/map fields: `trip`, `location`, `country`, `venue`, `address`, `latitude`, `longitude`. - -Special cases: - -- For `name: travel_insurance`, the event date field is `end_date`; no `end_date` is attached to the generated event. -- For recurring events, if the `rrule` has no `BYHOUR`, `BYMINUTE`, or `BYSECOND`, generated events are all-day dates. Otherwise generated datetimes are localized to UK time. -- `skip_trips=True` consumers ignore entries with `trip`. - -Examples: - -```yaml -- name: travel_insurance - start_date: 2026-05-04 - end_date: 2027-05-03 - -- name: meetup - title: Example Geo Meetup - date: 2026-06-18 18:30:00+01:00 - duration: PT2H - url: https://example.org/meetup - location: Bristol - country: gb - latitude: 51.4545 - longitude: -2.5879 - -- name: market - title: Monthly Example Market - rrule: FREQ=MONTHLY;BYDAY=1SA -``` - -## `ferries.yaml` - -Top-level shape: list of ferry journeys. - -Used by: trip loading, maps, trip timeline, Schengen tracking. - -Required fields: - -- `trip`: trip start date. -- `depart`, `arrive`: datetimes. Ferry `arrive` is required. -- `from`, `to`: names from `ferry_terminals.yaml`. - -Common optional fields: - -- `operator`, `ferry`, `direction`, `class`, `booking_reference`, `price`, `currency`. -- `price_details`: free-form mapping of fare components. -- `vehicle`: mapping with fields such as `type`, `registration`, `height`, `length`, `extras`. - -Example: - -```yaml -- booking_reference: ABC123 - trip: 2026-05-04 - price: '302.00' - currency: GBP - depart: 2026-05-04 23:00:00+01:00 - arrive: 2026-05-05 08:00:00+02:00 - from: Portsmouth - to: Cherbourg - operator: Brittany Ferries - class: Commodore cabin - price_details: - base_fare: '153.00' - cabin: '149.00' - vehicle: - type: Example car - registration: AB12CDE - height: 1.63m - length: 4.15m -``` - -## `ferry_terminals.yaml` - -Top-level shape: list of ferry terminals. - -Used by: ferry loading and route rendering. - -Required fields: - -- `name`: terminal name referenced by `ferries.yaml`. -- `city`, `country`. -- `latitude`, `longitude`. -- `routes`: mapping from destination terminal name to GeoJSON filename without `.geojson`. Ferry route rendering expects a GeoJSON route. - -Optional fields: - -- `osm_node`, `osm_way`. - -Example: - -```yaml -- name: Portsmouth - city: Portsmouth - country: gb - osm_way: 123456 - latitude: 50.8120 - longitude: -1.0880 - routes: - Cherbourg: portsmouth_cherbourg -``` - -## `flight_destinations.yaml` - -Top-level shape: list of origin rules for unbooked conference flight route hints. - -Used by: trip maps when a trip has conferences but no booked travel. - -Required fields: - -- `origin`: origin airport IATA code. -- `airline`: airline IATA code. Currently loaded for validation/description but not used in origin selection. -- `destinations`: list of destination airport IATA codes. - -Example: - -```yaml -- origin: BRS - airline: U2 - destinations: - - AMS - - BCN - - CDG -``` - -## `flights.yaml` - -Top-level shape: list of flight bookings. Each booking contains one or more flight legs. - -Used by: agenda transport events, trip loading, maps, distance calculation. - -Required booking fields: - -- `trip`: trip start date. -- `flights`: list of flight leg mappings. - -Common optional booking fields: - -- `booking_reference`, `price`, `currency`. - -Required flight leg fields: - -- `depart`: departure datetime. -- `from`, `to`: airport IATA codes from `airports.yaml`. -- `flight_number`: numeric/string flight number without airline prefix. -- `airline`: airline IATA code from `airlines.yaml`. - -Common optional flight leg fields: - -- Time/location: `arrive`, `from_terminal`, `to_terminal`, `duration`. -- Seat/cabin: `seat`, `seat_type`, `class`, `cabin`. -- Aircraft: `plane`, `registration`. -- Tracking: `distance`, `co2_kg`, `openflights_trip`, `reason`. -- Ticket/passenger: `e_ticket_number`, `ticket_number`, `frequent_flyer_number`, `passenger_name`, `passengers`, `baggage`, `payment_details`. - -`validate_yaml.py` checks that every booking has `trip`, all flight airlines exist in `airlines.yaml`, bookings are sorted by first departure, and currencies are configured. It reports flights missing `co2_kg`. - -Example: - -```yaml -- booking_reference: ABC123 - trip: 2026-04-22 - price: '62.50' - currency: GBP - flights: - - depart: 2026-04-22 17:20:00+01:00 - arrive: 2026-04-22 20:20:00+02:00 - from: LHR - to: BRU - flight_number: '1234' - airline: BA - duration: 01:00 - seat: 5F - seat_type: W - class: C - cabin: business - plane: Airbus A320 - registration: G-ABCD - co2_kg: 154 -``` - -## `follow_launches.yaml` - -Top-level shape: list of SpaceDevs launch slugs. - -Used by: no current in-repo reader was found, but the file appears intended as a watch list for launch update tooling. - -Example: - -```yaml -- starship-integrated-flight-test-5 -- artemis-ii -``` - -## `stations.yaml` - -Top-level shape: list of railway stations. - -Used by: train loading, maps, route rendering. - -Required fields: - -- `name`: station name referenced by `trains.yaml`. -- `country`: lowercase country code. -- `latitude`, `longitude`. -- `routes`: mapping from destination station name to GeoJSON filename without `.geojson`. - -Common optional fields: - -- `uic`, `alpha3`, `wikidata`, `osm_node`. - -Note: the code reads `routes`, not `rotues`; `rotues` appears to be a typo in existing data and should not be used for new entries. - -Example: - -```yaml -- name: London St Pancras - uic: 7015400 - alpha3: STP - wikidata: Q720102 - latitude: 51.531921 - longitude: -0.126361 - country: gb - routes: - Brussels Midi: london_brussels_eurostar -``` - -## `subscriptions.yaml` - -Top-level shape: list of subscriptions. - -Used by: subscription renewal agenda events when `renewal_date` is present. - -Required fields: - -- `name`: subscription name. - -Common optional fields: - -- Dates: `start`, `start_date`, `renewal_date`. -- `price`: mapping with `amount` and `currency`. -- `term`: mapping with `duration` and `unit` or `term_unit`. -- Account: `email`, `account_url`, `account_number`. - -Only items with `renewal_date` create agenda events. - -Example: - -```yaml -- name: Example Magazine - start_date: 2026-01-01 - renewal_date: 2027-01-01 - price: - amount: 99 - currency: GBP - term: - duration: 1 - unit: year - email: me@example.com - account_url: https://example.com/account - account_number: '001234' -``` - -## `trains.yaml` - -Top-level shape: list of train journeys. Each journey contains one or more legs. - -Used by: agenda transport events, trip loading, trip timeline, maps, stats. - -Required journey fields: - -- `operator`: booking/operator label. -- `from`, `to`: station names from `stations.yaml`. -- `trip`: trip start date. -- `depart`, `arrive`: journey datetimes or dates. -- `legs`: list of leg mappings. - -Common optional journey fields: - -- `class`, `number`, `tickets`, `ticket_code`, `total_price`, `co2_kg`. - -Required leg fields: - -- `from`, `to`: station names from `stations.yaml`. -- `depart`, `arrive`. -- `operator`. - -Common optional leg fields: - -- `train`, `number`, `service`, `service_number`, `service_numbers`, `reporting_number`, `mode`. -- Seat/reservation: `coach`, `seat`, `seat_type`, `seat_features`, `reservation_number`, `platform`. -- `class`, `trip`, `url`. - -Ticket fields are free-form but commonly include `booking_reference`, `url`, `price`, `currency`, `booking_date`, `ticket`, `ticket_code`, `ticket_type`, `from`, `to`, `class`, `validity`, `route`, `fare`, `quantity`, `seat_reservation`. - -Example: - -```yaml -- operator: eurostar - from: London St Pancras - to: Brussels Midi - trip: 2026-02-06 - depart: 2026-02-06 15:04:00+00:00 - arrive: 2026-02-06 18:12:00+01:00 - class: Standard Premier - tickets: - - booking_reference: ABCDEF - url: https://example.com/booking/ABCDEF - price: '89.00' - currency: GBP - legs: - - from: London St Pancras - to: Brussels Midi - depart: 2026-02-06 15:04:00+00:00 - arrive: 2026-02-06 18:12:00+01:00 - coach: 1 - seat: 41 - operator: Eurostar -``` - -## `travel_rewards.yaml` - -Top-level shape: list of travel loyalty accounts. - -Used by: no current in-repo reader was found. The file is structured as account metadata. - -Common fields: - -- `name`: programme name. -- `type`: category such as `hotel`, `airline`, `rail`. -- `member_number`: membership identifier. -- `balance`: current points/miles balance. -- `expiry`: expiry date or null. -- `url`: account URL. -- `person`: account holder key/name. -- `email`, `note`. - -Example: - -```yaml -- name: Example Rewards - type: hotel - member_number: '123456789' - balance: 3665 - expiry: 2027-08-15 - url: https://example.com/rewards - person: edward -``` - -## `trips.yaml` - -Top-level shape: list of trip metadata. - -Used by: trip grouping and trip titles. - -Required fields: - -- `trip`: trip start date. This is the grouping key used by travel, accommodation, conferences, and trip events. - -Optional fields: - -- `name`: explicit trip title. -- `private`: boolean. Private trips are hidden from unauthenticated users. - -Example: - -```yaml -- trip: 2026-02-06 - name: Brussels for FOSDEM - private: false -``` diff --git a/templates/conference_list.html b/templates/conference_list.html index 6529b22..4e49074 100644 --- a/templates/conference_list.html +++ b/templates/conference_list.html @@ -138,21 +138,22 @@ tr.conf-hl > td { {% set ns = namespace(prev_month="") %} {% for item in item_list %} - {% set month_label = item.sort_date.strftime("%B %Y") %} + {% set month_label = item.start_date.strftime("%B %Y") %} {% if month_label != ns.prev_month %} {% set ns.prev_month = month_label %}