Track multiple submission cycles per conference
All checks were successful
Build and deploy static pages / build-and-push (push) Successful in 13s
All checks were successful
Build and deploy static pages / build-and-push (push) Successful in 13s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,9 @@ PRESERVED_VENUE_FIELDS = {"notes", "deadline_source"}
|
||||
# Fields removed from existing front matter on every regeneration:
|
||||
# - "url" reserved by Hugo (overrides page URL)
|
||||
# - "papers" moved from front matter to rendered markdown body
|
||||
_STRIP_FROM_EXISTING = {"url", "papers"}
|
||||
# - "cycles" / "next_cycle" rebuilt from deadlines.yaml every run, so a venue
|
||||
# dropping back to a single round does not keep a stale cycle list
|
||||
_STRIP_FROM_EXISTING = {"url", "papers", "cycles", "next_cycle"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -118,13 +120,37 @@ def scimago_lookup(data: dict[str, dict], full_name: str, issn: str) -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
# Fields describing one submission cycle. Everything else on a deadline entry
|
||||
# (event_dates, location, cfp_url, source) describes the event itself.
|
||||
CYCLE_FIELDS = ("name", "abstract_deadline", "submission_deadline", "notification",
|
||||
"camera_ready")
|
||||
|
||||
|
||||
def _normalise_cycles(entry: dict) -> None:
|
||||
"""Fold a flat, single-cycle entry into a one-element `cycles` list, in place.
|
||||
|
||||
Venues with a single submission round stay flat in YAML; consumers below only
|
||||
ever see `cycles`. `cfp_url` stays at the top level — it describes the call,
|
||||
not one round — but a cycle may override it where a venue publishes separate
|
||||
calls per round.
|
||||
"""
|
||||
if entry.get("cycles"):
|
||||
return
|
||||
cycle = {k: entry.pop(k) for k in CYCLE_FIELDS if k in entry}
|
||||
entry["cycles"] = [cycle] if cycle else []
|
||||
|
||||
|
||||
def load_deadlines(path: str) -> dict[str, dict]:
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
return {}
|
||||
with open(p) as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
return data.get("deadlines", {})
|
||||
deadlines = data.get("deadlines") or {}
|
||||
for entry in deadlines.values():
|
||||
if entry:
|
||||
_normalise_cycles(entry)
|
||||
return deadlines
|
||||
|
||||
|
||||
def load_venue_digests(papers_dir: Path) -> dict[str, list[dict]]:
|
||||
@@ -182,17 +208,40 @@ def _conf_body(fm: dict, digests: list[dict] | None = None, base_path: str = "")
|
||||
count = latest.get("paper_count", 0)
|
||||
suffix = f"— {count} papers" if count else "*(digest pending)*"
|
||||
lines.append(f"| **Latest digest** | [{title} {year}]({base_path}/digests/{slug}/) {suffix} |")
|
||||
lines += [
|
||||
"\n## Upcoming Deadline\n",
|
||||
"| | |", "|---|---|",
|
||||
]
|
||||
if abstract_dl and abstract_dl != dl:
|
||||
lines.append(f"| **Abstract deadline** | {abstract_dl} |")
|
||||
lines.append(f"| **Paper deadline** | {dl} |")
|
||||
if notification:
|
||||
lines.append(f"| **Notification** | {notification} |")
|
||||
if camera_ready:
|
||||
lines.append(f"| **Camera-ready** | {camera_ready} |")
|
||||
cycles = fm.get("cycles") or []
|
||||
if len(cycles) > 1:
|
||||
lines += [
|
||||
"\n## Submission Cycles\n",
|
||||
"| Cycle | Abstract | Paper deadline | Notification | Camera-ready |",
|
||||
"|---|---|---|---|---|",
|
||||
]
|
||||
next_name = fm.get("next_cycle")
|
||||
marked = False
|
||||
for cycle in cycles:
|
||||
name = cycle.get("name") or "—"
|
||||
if not marked and name == next_name:
|
||||
name = f"**{name}** (next)"
|
||||
marked = True
|
||||
paper = cycle.get("submission_deadline") or cycle.get("abstract_deadline") or "TBD"
|
||||
abstract = cycle.get("abstract_deadline") or ""
|
||||
abs_cell = abstract if (abstract and abstract != paper) else ""
|
||||
lines.append(
|
||||
f"| {name} | {abs_cell} | {paper} | {cycle.get('notification') or ''} "
|
||||
f"| {cycle.get('camera_ready') or ''} |"
|
||||
)
|
||||
lines += ["\n| | |", "|---|---|"]
|
||||
else:
|
||||
lines += [
|
||||
"\n## Upcoming Deadline\n",
|
||||
"| | |", "|---|---|",
|
||||
]
|
||||
if abstract_dl and abstract_dl != dl:
|
||||
lines.append(f"| **Abstract deadline** | {abstract_dl} |")
|
||||
lines.append(f"| **Paper deadline** | {dl} |")
|
||||
if notification:
|
||||
lines.append(f"| **Notification** | {notification} |")
|
||||
if camera_ready:
|
||||
lines.append(f"| **Camera-ready** | {camera_ready} |")
|
||||
lines.append(f"| **Event dates** | {event} |")
|
||||
if location:
|
||||
lines.append(f"| **Location** | {location} |")
|
||||
@@ -320,6 +369,30 @@ def _parse_date(s: str, month_only: bool = False) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _cycle_deadline(cycle: dict) -> str:
|
||||
"""The date a cycle is judged by: its paper deadline, else its abstract deadline."""
|
||||
return cycle.get("submission_deadline") or cycle.get("abstract_deadline") or ""
|
||||
|
||||
|
||||
def _next_cycle(entry: dict, today: str | None = None) -> dict:
|
||||
"""The cycle a reader should act on.
|
||||
|
||||
The earliest cycle whose paper deadline has not passed; if every cycle has
|
||||
elapsed, the last one, so a page shows the most recent round rather than
|
||||
going blank. Cycles with unparseable dates sort last.
|
||||
"""
|
||||
cycles = entry.get("cycles") or []
|
||||
if not cycles:
|
||||
return {}
|
||||
today = today or datetime.now().strftime("%Y-%m-%d")
|
||||
ordered = sorted(cycles, key=lambda c: _parse_date(_cycle_deadline(c)) or "9999-12-31")
|
||||
for cycle in ordered:
|
||||
iso = _parse_date(_cycle_deadline(cycle))
|
||||
if iso and iso >= today:
|
||||
return cycle
|
||||
return ordered[-1]
|
||||
|
||||
|
||||
def _parse_date_range(s: str) -> tuple[str | None, str | None]:
|
||||
"""Return (start_iso, end_iso_exclusive) from a date-range string.
|
||||
|
||||
@@ -374,9 +447,14 @@ def _conferences_section_body(venues: dict, icore: dict, deadlines: dict,
|
||||
acronym = conf.get("acronym", "")
|
||||
rank = icore.get(acronym.upper(), "—") or "—"
|
||||
dl = deadlines.get(acronym, {})
|
||||
abstract = dl.get("abstract_deadline") or ""
|
||||
paper = dl.get("submission_deadline") or dl.get("abstract_deadline") or "TBD"
|
||||
notif = dl.get("notification") or ""
|
||||
# The venue index lists venues, not cycles: show the next open round and
|
||||
# name it, so a passed round never reads as "you missed this conference".
|
||||
nxt = _next_cycle(dl)
|
||||
abstract = nxt.get("abstract_deadline") or ""
|
||||
paper = nxt.get("submission_deadline") or nxt.get("abstract_deadline") or "TBD"
|
||||
if len(dl.get("cycles") or []) > 1 and nxt.get("name"):
|
||||
paper = f"{paper} ({nxt['name']})"
|
||||
notif = nxt.get("notification") or ""
|
||||
event = dl.get("event_dates") or "—"
|
||||
location = dl.get("location") or ""
|
||||
domains = ", ".join(f"`{d}`" for d in conf.get("domain", []))
|
||||
@@ -432,25 +510,30 @@ def _calendar_events(venues: dict, deadlines: dict, base_path: str = "") -> list
|
||||
dl = deadlines.get(acronym, {})
|
||||
url = f"{base_path}/venues/conferences/{acronym.lower()}/"
|
||||
location = dl.get("location") or ""
|
||||
cfp_url = dl.get("cfp_url") or ""
|
||||
|
||||
for field, label, color in [
|
||||
("abstract_deadline", "abstract deadline", "#f4a261"),
|
||||
("submission_deadline", "paper deadline", "#e63946"),
|
||||
("notification", "notification", "#2a9d8f"),
|
||||
]:
|
||||
iso = _parse_date(dl.get(field) or "")
|
||||
if iso:
|
||||
ev: dict = {"title": f"{acronym} — {label}", "start": iso,
|
||||
"url": url, "color": color, "allDay": True}
|
||||
props: dict = {}
|
||||
if location:
|
||||
props["location"] = location
|
||||
if cfp_url:
|
||||
props["cfp_url"] = cfp_url
|
||||
if props:
|
||||
ev["extendedProps"] = props
|
||||
events.append(ev)
|
||||
for cycle in dl.get("cycles") or []:
|
||||
cycle_name = cycle.get("name") or ""
|
||||
prefix = f"{acronym} {cycle_name}".strip()
|
||||
cfp_url = cycle.get("cfp_url") or dl.get("cfp_url") or ""
|
||||
for field, label, color in [
|
||||
("abstract_deadline", "abstract deadline", "#f4a261"),
|
||||
("submission_deadline", "paper deadline", "#e63946"),
|
||||
("notification", "notification", "#2a9d8f"),
|
||||
]:
|
||||
iso = _parse_date(cycle.get(field) or "")
|
||||
if iso:
|
||||
ev: dict = {"title": f"{prefix} — {label}", "start": iso,
|
||||
"url": url, "color": color, "allDay": True}
|
||||
props: dict = {}
|
||||
if location:
|
||||
props["location"] = location
|
||||
if cfp_url:
|
||||
props["cfp_url"] = cfp_url
|
||||
if cycle_name:
|
||||
props["cycle"] = cycle_name
|
||||
if props:
|
||||
ev["extendedProps"] = props
|
||||
events.append(ev)
|
||||
|
||||
start, end = _parse_date_range(dl.get("event_dates") or "")
|
||||
if start:
|
||||
@@ -503,26 +586,28 @@ def _calendar_body(venues: dict, icore: dict, deadlines: dict, base_path: str =
|
||||
acronym = conf.get("acronym", "")
|
||||
rank = icore.get(acronym.upper(), "—") or "—"
|
||||
dl_data = deadlines.get(acronym, {})
|
||||
abstract = dl_data.get("abstract_deadline") or ""
|
||||
paper = dl_data.get("submission_deadline") or dl_data.get("abstract_deadline") or "TBD"
|
||||
notif = dl_data.get("notification") or ""
|
||||
event = dl_data.get("event_dates") or "—"
|
||||
location = dl_data.get("location") or ""
|
||||
conf_rows.append((acronym, conf.get("full_name", ""), abstract, paper,
|
||||
notif, event, location, rank))
|
||||
# One row per submission cycle — a venue's later rounds are the point.
|
||||
for cycle in dl_data.get("cycles") or [{}]:
|
||||
abstract = cycle.get("abstract_deadline") or ""
|
||||
paper = cycle.get("submission_deadline") or cycle.get("abstract_deadline") or "TBD"
|
||||
notif = cycle.get("notification") or ""
|
||||
conf_rows.append((acronym, conf.get("full_name", ""), cycle.get("name") or "—",
|
||||
abstract, paper, notif, event, location, rank))
|
||||
|
||||
conf_rows.sort(key=lambda r: (parse_deadline(r[3]), r[0]))
|
||||
conf_rows.sort(key=lambda r: (parse_deadline(r[4]), r[0]))
|
||||
|
||||
lines += [
|
||||
"\n## Conference Deadlines\n",
|
||||
"| Venue | Name | Abstract | Paper deadline | Notification | Event dates | Location | ICORE |",
|
||||
"|---|---|---|---|---|---|---|---|",
|
||||
"| Venue | Name | Cycle | Abstract | Paper deadline | Notification | Event dates | Location | ICORE |",
|
||||
"|---|---|---|---|---|---|---|---|---|",
|
||||
]
|
||||
for acronym, full_name, abstract, paper, notif, event, location, rank in conf_rows:
|
||||
for acronym, full_name, cycle_name, abstract, paper, notif, event, location, rank in conf_rows:
|
||||
abs_cell = abstract if (abstract and abstract != paper) else ""
|
||||
link = f"[{acronym}]({base_path}/venues/conferences/{acronym.lower()}/)"
|
||||
lines.append(
|
||||
f"| {link} | {full_name} | {abs_cell} | {paper} | {notif} "
|
||||
f"| {link} | {full_name} | {cycle_name} | {abs_cell} | {paper} | {notif} "
|
||||
f"| {event} | {location} | {rank} |"
|
||||
)
|
||||
|
||||
@@ -617,16 +702,24 @@ def gen_conferences(venues: dict, icore: dict, deadlines: dict, root: Path,
|
||||
"draft": False,
|
||||
}
|
||||
dl = deadlines.get(acronym, {})
|
||||
if dl.get("submission_deadline"):
|
||||
new_fm["next_deadline"] = dl["submission_deadline"]
|
||||
elif dl.get("abstract_deadline"):
|
||||
new_fm["next_deadline"] = dl["abstract_deadline"]
|
||||
if dl.get("abstract_deadline") and dl.get("submission_deadline"):
|
||||
new_fm["abstract_deadline"] = dl["abstract_deadline"]
|
||||
if dl.get("notification"):
|
||||
new_fm["notification"] = dl["notification"]
|
||||
if dl.get("camera_ready"):
|
||||
new_fm["camera_ready"] = dl["camera_ready"]
|
||||
cycles = dl.get("cycles") or []
|
||||
nxt = _next_cycle(dl)
|
||||
if nxt.get("submission_deadline"):
|
||||
new_fm["next_deadline"] = nxt["submission_deadline"]
|
||||
elif nxt.get("abstract_deadline"):
|
||||
new_fm["next_deadline"] = nxt["abstract_deadline"]
|
||||
if nxt.get("abstract_deadline") and nxt.get("submission_deadline"):
|
||||
new_fm["abstract_deadline"] = nxt["abstract_deadline"]
|
||||
if nxt.get("notification"):
|
||||
new_fm["notification"] = nxt["notification"]
|
||||
if nxt.get("camera_ready"):
|
||||
new_fm["camera_ready"] = nxt["camera_ready"]
|
||||
# Only multi-cycle venues carry the full list; single-cycle pages keep
|
||||
# the flat front matter they have always had.
|
||||
if len(cycles) > 1:
|
||||
new_fm["cycles"] = cycles
|
||||
if nxt.get("name"):
|
||||
new_fm["next_cycle"] = nxt["name"]
|
||||
if dl.get("event_dates"):
|
||||
new_fm["next_event"] = dl["event_dates"]
|
||||
if dl.get("location"):
|
||||
|
||||
Reference in New Issue
Block a user