Add conference country filter
This commit is contained in:
parent
442ae98463
commit
38dbb2b4e7
3 changed files with 178 additions and 1 deletions
|
|
@ -212,6 +212,28 @@ tr.conf-hl > td {
|
||||||
<div class="container-fluid mt-2">
|
<div class="container-fluid mt-2">
|
||||||
<h1>Conferences</h1>
|
<h1>Conferences</h1>
|
||||||
|
|
||||||
|
{% if country_options %}
|
||||||
|
<form method="get" class="row g-2 align-items-end mb-3">
|
||||||
|
<div class="col-auto">
|
||||||
|
<label for="country-filter" class="form-label mb-1">Country</label>
|
||||||
|
<select id="country-filter" name="country" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||||
|
<option value="">All countries</option>
|
||||||
|
{% for option in country_options %}
|
||||||
|
<option value="{{ option.code }}"{% if option.code == selected_country %} selected{% endif %}>
|
||||||
|
{{ option.flag }} {{ option.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary">Filter</button>
|
||||||
|
{% if selected_country %}
|
||||||
|
<a class="btn btn-sm btn-outline-secondary" href="{{ url_for(request.endpoint) }}">Clear</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{{ render_timeline(timeline) }}
|
{{ render_timeline(timeline) }}
|
||||||
|
|
||||||
<table class="table table-sm table-hover align-middle">
|
<table class="table table-sm table-hover align-middle">
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from types import SimpleNamespace
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
import agenda.fx
|
||||||
import agenda.trip
|
import agenda.trip
|
||||||
import web_view
|
import web_view
|
||||||
|
|
||||||
|
|
@ -121,3 +122,84 @@ def test_conference_series_pages(tmp_path: typing.Any, monkeypatch: typing.Any)
|
||||||
assert b"attended" 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
|
||||||
|
|
||||||
|
|
||||||
|
def test_conference_page_filters_by_country(
|
||||||
|
tmp_path: typing.Any, monkeypatch: typing.Any
|
||||||
|
) -> None:
|
||||||
|
"""Conference page should filter upcoming conferences by country code."""
|
||||||
|
conferences = [
|
||||||
|
{
|
||||||
|
"name": "UK Mapping Conf 2099",
|
||||||
|
"topic": "Maps",
|
||||||
|
"location": "London",
|
||||||
|
"country": "GB",
|
||||||
|
"start": date(2099, 5, 1),
|
||||||
|
"end": date(2099, 5, 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "US Python Conf 2099",
|
||||||
|
"topic": "Python",
|
||||||
|
"location": "Pittsburgh",
|
||||||
|
"country": "US",
|
||||||
|
"start": date(2099, 6, 1),
|
||||||
|
"end": date(2099, 6, 2),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
(tmp_path / "conferences.yaml").write_text(
|
||||||
|
yaml.safe_dump(conferences), encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setitem(web_view.app.config, "PERSONAL_DATA", str(tmp_path))
|
||||||
|
monkeypatch.setattr(agenda.trip, "build_trip_list", lambda: [])
|
||||||
|
monkeypatch.setattr(agenda.fx, "get_rates", lambda config: {})
|
||||||
|
|
||||||
|
web_view.app.config["TESTING"] = True
|
||||||
|
with web_view.app.test_client() as client:
|
||||||
|
response = client.get("/conference?country=gb")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"UK Mapping Conf 2099" in response.data
|
||||||
|
assert b"US Python Conf 2099" not in response.data
|
||||||
|
assert b'<option value="gb" selected>' in response.data
|
||||||
|
assert b'href="/conference"' in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_past_conference_page_filters_by_country(
|
||||||
|
tmp_path: typing.Any, monkeypatch: typing.Any
|
||||||
|
) -> None:
|
||||||
|
"""Past conference page should filter conferences by country code."""
|
||||||
|
conferences = [
|
||||||
|
{
|
||||||
|
"name": "Past UK Conf",
|
||||||
|
"topic": "Maps",
|
||||||
|
"location": "London",
|
||||||
|
"country": "GB",
|
||||||
|
"start": date(2001, 5, 1),
|
||||||
|
"end": date(2001, 5, 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Past US Conf",
|
||||||
|
"topic": "Python",
|
||||||
|
"location": "Pittsburgh",
|
||||||
|
"country": "US",
|
||||||
|
"start": date(2001, 6, 1),
|
||||||
|
"end": date(2001, 6, 2),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
(tmp_path / "conferences.yaml").write_text(
|
||||||
|
yaml.safe_dump(conferences), encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setitem(web_view.app.config, "PERSONAL_DATA", str(tmp_path))
|
||||||
|
monkeypatch.setattr(agenda.trip, "build_trip_list", lambda: [])
|
||||||
|
monkeypatch.setattr(agenda.fx, "get_rates", lambda config: {})
|
||||||
|
|
||||||
|
web_view.app.config["TESTING"] = True
|
||||||
|
with web_view.app.test_client() as client:
|
||||||
|
response = client.get("/conference/past?country=us")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"Past US Conf" in response.data
|
||||||
|
assert b"Past UK Conf" not in response.data
|
||||||
|
assert b'href="/conference/past"' in response.data
|
||||||
|
|
|
||||||
75
web_view.py
75
web_view.py
|
|
@ -400,6 +400,64 @@ def build_conference_list() -> list[StrDict]:
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def conference_country_code(conf: StrDict) -> str | None:
|
||||||
|
"""Return normalized alpha-2 country code for a conference."""
|
||||||
|
country = conf.get("country")
|
||||||
|
if not isinstance(country, str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
code = country.strip().lower()
|
||||||
|
return code if len(code) == 2 else None
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_country_filter(value: str | None) -> str | None:
|
||||||
|
"""Normalize and validate a country query parameter."""
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
|
||||||
|
code = value.strip().lower()
|
||||||
|
if len(code) != 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return code if agenda.get_country(code) else None
|
||||||
|
|
||||||
|
|
||||||
|
def filter_conferences_by_country(
|
||||||
|
items: list[StrDict], country_code: str | None
|
||||||
|
) -> list[StrDict]:
|
||||||
|
"""Filter conferences by country when a country code is provided."""
|
||||||
|
if not country_code:
|
||||||
|
return items
|
||||||
|
|
||||||
|
return [item for item in items if conference_country_code(item) == country_code]
|
||||||
|
|
||||||
|
|
||||||
|
def conference_country_options(items: list[StrDict]) -> list[StrDict]:
|
||||||
|
"""Return country options for the conference country filter."""
|
||||||
|
counts: defaultdict[str, int] = defaultdict(int)
|
||||||
|
for item in items:
|
||||||
|
code = conference_country_code(item)
|
||||||
|
if code and agenda.get_country(code):
|
||||||
|
counts[code] += 1
|
||||||
|
|
||||||
|
options: list[StrDict] = []
|
||||||
|
for code, count in counts.items():
|
||||||
|
country = agenda.get_country(code)
|
||||||
|
if not country:
|
||||||
|
continue
|
||||||
|
options.append(
|
||||||
|
{
|
||||||
|
"code": code,
|
||||||
|
"name": country.name,
|
||||||
|
"flag": country.flag,
|
||||||
|
"count": count,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
options.sort(key=lambda item: str(item["name"]))
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
def build_conference_series_list() -> list[StrDict]:
|
def build_conference_series_list() -> list[StrDict]:
|
||||||
"""Build conference series list with conference counts."""
|
"""Build conference series list with conference counts."""
|
||||||
data_dir = app.config["PERSONAL_DATA"]
|
data_dir = app.config["PERSONAL_DATA"]
|
||||||
|
|
@ -591,6 +649,11 @@ def conference_list() -> str:
|
||||||
"""Page showing a list of conferences."""
|
"""Page showing a list of conferences."""
|
||||||
today = date.today()
|
today = date.today()
|
||||||
items = build_conference_list()
|
items = build_conference_list()
|
||||||
|
country_filter = normalize_country_filter(flask.request.args.get("country"))
|
||||||
|
country_options = conference_country_options(
|
||||||
|
[conf for conf in items if conf["latest_date"] >= today]
|
||||||
|
)
|
||||||
|
items = filter_conferences_by_country(items, country_filter)
|
||||||
|
|
||||||
current = [
|
current = [
|
||||||
conf
|
conf
|
||||||
|
|
@ -612,6 +675,8 @@ def conference_list() -> str:
|
||||||
timeline=timeline,
|
timeline=timeline,
|
||||||
today=today,
|
today=today,
|
||||||
get_country=agenda.get_country,
|
get_country=agenda.get_country,
|
||||||
|
selected_country=country_filter,
|
||||||
|
country_options=country_options,
|
||||||
fx_rate=agenda.fx.get_rates(app.config),
|
fx_rate=agenda.fx.get_rates(app.config),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -620,11 +685,19 @@ def conference_list() -> str:
|
||||||
def past_conference_list() -> str:
|
def past_conference_list() -> str:
|
||||||
"""Page showing a list of conferences."""
|
"""Page showing a list of conferences."""
|
||||||
today = date.today()
|
today = date.today()
|
||||||
|
items = build_conference_list()
|
||||||
|
country_filter = normalize_country_filter(flask.request.args.get("country"))
|
||||||
|
country_options = conference_country_options(
|
||||||
|
[conf for conf in items if conf["latest_date"] < today]
|
||||||
|
)
|
||||||
|
items = filter_conferences_by_country(items, country_filter)
|
||||||
return flask.render_template(
|
return flask.render_template(
|
||||||
"conference_list.html",
|
"conference_list.html",
|
||||||
past=[conf for conf in build_conference_list() if conf["latest_date"] < today],
|
past=[conf for conf in items if conf["latest_date"] < today],
|
||||||
today=today,
|
today=today,
|
||||||
get_country=agenda.get_country,
|
get_country=agenda.get_country,
|
||||||
|
selected_country=country_filter,
|
||||||
|
country_options=country_options,
|
||||||
fx_rate=agenda.fx.get_rates(app.config),
|
fx_rate=agenda.fx.get_rates(app.config),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue