Improve conference importer and series attendance
This commit is contained in:
parent
dbce9e5358
commit
a87c9f993e
5 changed files with 87 additions and 0 deletions
|
|
@ -502,6 +502,42 @@ def parse_yaml_datetime(value: typing.Any) -> datetime | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_yaml_date_value(value: typing.Any) -> date | datetime | None:
|
||||||
|
"""Convert YAML date/datetime strings to date-like values."""
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value
|
||||||
|
|
||||||
|
if isinstance(value, date):
|
||||||
|
return value
|
||||||
|
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if " " in value or "T" in value:
|
||||||
|
return datetime.fromisoformat(value)
|
||||||
|
return date.fromisoformat(value)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_date_values(conf: dict[str, typing.Any]) -> None:
|
||||||
|
"""Normalize quoted ISO date/datetime values produced by the LLM."""
|
||||||
|
dates = conf.get("dates")
|
||||||
|
if isinstance(dates, dict):
|
||||||
|
for field in ("start", "end", "earliest", "latest"):
|
||||||
|
if field in dates:
|
||||||
|
parsed = parse_yaml_date_value(dates[field])
|
||||||
|
if parsed is not None:
|
||||||
|
dates[field] = parsed
|
||||||
|
|
||||||
|
for field in ("start", "end"):
|
||||||
|
if field in conf:
|
||||||
|
parsed = parse_yaml_date_value(conf[field])
|
||||||
|
if parsed is not None:
|
||||||
|
conf[field] = parsed
|
||||||
|
|
||||||
|
|
||||||
def same_type_as_start(
|
def same_type_as_start(
|
||||||
start_value: typing.Any,
|
start_value: typing.Any,
|
||||||
new_dt: datetime,
|
new_dt: datetime,
|
||||||
|
|
@ -529,6 +565,7 @@ def same_type_as_start(
|
||||||
|
|
||||||
def normalize_dates_field(conf: dict[str, typing.Any]) -> None:
|
def normalize_dates_field(conf: dict[str, typing.Any]) -> None:
|
||||||
"""Move legacy top-level date fields into the nested dates mapping."""
|
"""Move legacy top-level date fields into the nested dates mapping."""
|
||||||
|
normalize_date_values(conf)
|
||||||
raw_dates = conf.get("dates")
|
raw_dates = conf.get("dates")
|
||||||
dates = raw_dates if isinstance(raw_dates, dict) else None
|
dates = raw_dates if isinstance(raw_dates, dict) else None
|
||||||
|
|
||||||
|
|
@ -550,6 +587,20 @@ def normalize_dates_field(conf: dict[str, typing.Any]) -> None:
|
||||||
conf.pop("start", None)
|
conf.pop("start", None)
|
||||||
conf.pop("end", None)
|
conf.pop("end", None)
|
||||||
conf.pop("date_status", None)
|
conf.pop("date_status", None)
|
||||||
|
normalize_date_values(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_generated_conference(conf: dict[str, typing.Any]) -> None:
|
||||||
|
"""Validate generated conference YAML before inserting it."""
|
||||||
|
try:
|
||||||
|
conference_date_fields(conf)
|
||||||
|
except ValueError as exc:
|
||||||
|
generated_yaml = yaml.dump(conf, sort_keys=False, allow_unicode=True).strip()
|
||||||
|
raise ValueError(
|
||||||
|
"Generated conference YAML is missing valid date information. "
|
||||||
|
"Expected nested `dates:` with exact/tentative start/end or "
|
||||||
|
f"approximate earliest/latest.\n\nGenerated YAML:\n{generated_yaml}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
def maybe_extract_explicit_end_time(source_text: str) -> int | None:
|
def maybe_extract_explicit_end_time(source_text: str) -> int | None:
|
||||||
|
|
@ -661,6 +712,7 @@ def add_new_conference(url: str, yaml_path: str) -> bool:
|
||||||
normalize_dates_field(new_conf)
|
normalize_dates_field(new_conf)
|
||||||
normalise_end_field(new_conf, source_text)
|
normalise_end_field(new_conf, source_text)
|
||||||
normalize_dates_field(new_conf)
|
normalize_dates_field(new_conf)
|
||||||
|
validate_generated_conference(new_conf)
|
||||||
|
|
||||||
if detected_coordinates is not None:
|
if detected_coordinates is not None:
|
||||||
new_conf["latitude"] = detected_coordinates[0]
|
new_conf["latitude"] = detected_coordinates[0]
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ url_for('conference_series_page', series_id=item.id) }}">{{ item.name }}</a>
|
<a href="{{ url_for('conference_series_page', series_id=item.id) }}">{{ item.name }}</a>
|
||||||
|
{% if item.attended %}
|
||||||
|
<span class="badge text-bg-success ms-1">attended</span>
|
||||||
|
{% endif %}
|
||||||
{% if item.url %}
|
{% if item.url %}
|
||||||
<a class="text-muted ms-1 text-decoration-none" href="{{ item.url }}">↗</a>
|
<a class="text-muted ms-1 text-decoration-none" href="{{ item.url }}">↗</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,35 @@ def test_normalize_dates_field_moves_legacy_dates() -> None:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_normalize_dates_field_parses_quoted_dates() -> None:
|
||||||
|
"""Quoted ISO dates from generated YAML should become date objects."""
|
||||||
|
conf: dict[str, typing.Any] = {
|
||||||
|
"name": "Git Merge",
|
||||||
|
"dates": {
|
||||||
|
"status": "exact",
|
||||||
|
"start": "2026-09-16",
|
||||||
|
"end": "2026-09-17",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
add_new_conference.normalize_dates_field(conf)
|
||||||
|
|
||||||
|
assert conf["dates"]["start"] == date(2026, 9, 16)
|
||||||
|
assert conf["dates"]["end"] == date(2026, 9, 17)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_generated_conference_reports_missing_dates() -> None:
|
||||||
|
"""Missing generated dates should raise a clear importer error."""
|
||||||
|
conf: dict[str, typing.Any] = {
|
||||||
|
"name": "Git Merge",
|
||||||
|
"topic": "Git",
|
||||||
|
"location": "TBC",
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="missing valid date information"):
|
||||||
|
add_new_conference.validate_generated_conference(conf)
|
||||||
|
|
||||||
|
|
||||||
def test_build_prompt_includes_nested_dates_and_series() -> None:
|
def test_build_prompt_includes_nested_dates_and_series() -> None:
|
||||||
"""The prompt should describe nested dates and known series IDs."""
|
"""The prompt should describe nested dates and known series IDs."""
|
||||||
prompt = add_new_conference.build_prompt(
|
prompt = add_new_conference.build_prompt(
|
||||||
|
|
|
||||||
|
|
@ -118,5 +118,6 @@ def test_conference_series_pages(tmp_path: typing.Any, monkeypatch: typing.Any)
|
||||||
|
|
||||||
assert index_response.status_code == 200
|
assert index_response.status_code == 200
|
||||||
assert b"PyCascades" in index_response.data
|
assert b"PyCascades" in index_response.data
|
||||||
|
assert b"attended" in index_response.data
|
||||||
assert detail_response.status_code == 200
|
assert detail_response.status_code == 200
|
||||||
assert b"trip: Seattle Python trip" in detail_response.data
|
assert b"trip: Seattle Python trip" in detail_response.data
|
||||||
|
|
|
||||||
|
|
@ -413,12 +413,14 @@ def build_conference_series_list() -> list[StrDict]:
|
||||||
next_conf = next(
|
next_conf = next(
|
||||||
(conf for conf in linked if conf["latest_date"] >= date.today()), None
|
(conf for conf in linked if conf["latest_date"] >= date.today()), None
|
||||||
)
|
)
|
||||||
|
attended = any(conf.get("going") or conf.get("linked_trip") for conf in linked)
|
||||||
item: StrDict = {
|
item: StrDict = {
|
||||||
"id": series_id,
|
"id": series_id,
|
||||||
**series,
|
**series,
|
||||||
"count": len(linked),
|
"count": len(linked),
|
||||||
"latest": latest,
|
"latest": latest,
|
||||||
"next_conf": next_conf,
|
"next_conf": next_conf,
|
||||||
|
"attended": attended,
|
||||||
}
|
}
|
||||||
series_items.append(item)
|
series_items.append(item)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue