ar0s commited on
Commit
1ed0506
·
1 Parent(s): b0f500b

refactor: integrate events_df for event processing and filtering

Browse files
Files changed (5) hide show
  1. app.py +3 -1
  2. src/attending.py +16 -72
  3. src/ui/events_df.py +113 -0
  4. src/ui/view.py +33 -53
  5. tests/test_golden.py +37 -30
app.py CHANGED
@@ -32,6 +32,7 @@ from src.ui.view import (
32
  toggle_filter_popover,
33
  view_updates,
34
  )
 
35
 
36
  load_env()
37
 
@@ -47,7 +48,8 @@ def render():
47
  pull_events(EVENTS_PATH)
48
  payload = load_payload(EVENTS_PATH)
49
  results = normalize_results(payload["results"])
50
- category_choices, tag_choices, org_choices = facet_choices(results)
 
51
  summary, spotlight, table, button_label, clear_button = view_updates(
52
  results,
53
  categories=category_choices,
 
32
  toggle_filter_popover,
33
  view_updates,
34
  )
35
+ from src.ui.events_df import events_df
36
 
37
  load_env()
38
 
 
48
  pull_events(EVENTS_PATH)
49
  payload = load_payload(EVENTS_PATH)
50
  results = normalize_results(payload["results"])
51
+ df_all = events_df(results)
52
+ category_choices, tag_choices, org_choices = facet_choices(df_all)
53
  summary, spotlight, table, button_label, clear_button = view_updates(
54
  results,
55
  categories=category_choices,
src/attending.py CHANGED
@@ -1,16 +1,11 @@
1
  from __future__ import annotations
2
 
3
- from collections.abc import Sequence
4
  from html import escape
5
  from urllib.parse import quote
6
 
7
  import pandas as pd
8
 
9
  from .config import local_display_tz
10
- from .events_schema import normalize_results
11
- from .models import parse_dt_utc
12
- from .teams_calendar import event_key
13
-
14
  LOCAL_DISPLAY_TZ = local_display_tz()
15
  WHEN_COL = "When"
16
  COLS = [WHEN_COL, "Event", "Speaker", "Category", "Organization", "Calendar"]
@@ -62,74 +57,26 @@ def share_button(anchor_id: str) -> str:
62
  )
63
 
64
 
65
- def filtered_events(
66
- results: list[dict],
67
- query: str = "",
68
- categories: Sequence[str] | None = None,
69
- tags: Sequence[str] | None = None,
70
- orgs: Sequence[str] | None = None,
71
- ) -> list[tuple[int, int, dict, dict]]:
72
- normalize_results(results)
73
- now = pd.Timestamp.now(tz=LOCAL_DISPLAY_TZ)
74
- needle = query.strip().lower()
75
- category_filter = None if categories is None else set(categories)
76
- tag_filter = None if tags is None else set(tags)
77
- org_filter = None if orgs is None else set(orgs)
78
- matches: list[tuple[object, str, int, int, dict, dict]] = []
79
- for result_index, result in enumerate(results):
80
- if result["status"] != "ok":
81
- continue
82
- for event_index, event in enumerate(result["events"]):
83
- start_time = pd.Timestamp(parse_dt_utc(event["start_time"])).tz_convert(LOCAL_DISPLAY_TZ)
84
- if start_time <= now - pd.Timedelta(hours=1):
85
- continue
86
- if category_filter is not None and event["category"] not in category_filter:
87
- continue
88
- if org_filter is not None and result["org_name"] not in org_filter:
89
- continue
90
- if tag_filter is not None and not tag_filter.intersection(event["tags"]):
91
- continue
92
- haystack = " ".join(
93
- (
94
- result["org_name"],
95
- event["title"],
96
- event["speaker"],
97
- event["affiliation"],
98
- event["category"],
99
- " ".join(event["tags"]),
100
- )
101
- ).lower()
102
- if needle and needle not in haystack:
103
- continue
104
- matches.append((start_time, result["org_name"], result_index, event_index, result, event))
105
-
106
- return [
107
- (result_index, event_index, result, event)
108
- for _, _, result_index, event_index, result, event in sorted(matches)
109
- ]
110
-
111
-
112
  def results_table(
113
- results: list[dict],
114
  colors: dict[str, str],
115
- query: str = "",
116
- categories: Sequence[str] | None = None,
117
- tags: Sequence[str] | None = None,
118
- orgs: Sequence[str] | None = None,
119
  ) -> pd.DataFrame:
 
 
 
120
  rows: list[dict[str, object]] = []
121
- for _, _, result, event in filtered_events(results, query, categories, tags, orgs):
122
- current_event_key = event_key(result, event)
123
  anchor_id = f"event-{current_event_key}"
124
- title = event["title"] or "View event"
125
- source_time = format_event_time(event["start_time"])
126
- local_time = format_event_time(event["start_time"], tz_name=LOCAL_DISPLAY_TZ)
127
- tag_chips = "".join(chip(tag) for tag in event["tags"])
128
  event_cell = (
129
  f'<a class="event-anchor event-anchor--{anchor_id}" '
130
- f'href="{escape(event["event_url"], quote=True)}">'
131
  f"{escape(title)}</a>"
132
- if event["event_url"]
133
  else f'<strong class="event-anchor event-anchor--{anchor_id}">{escape(title)}</strong>'
134
  )
135
  event_cell = f"{event_cell}{share_button(anchor_id)}"
@@ -139,14 +86,11 @@ def results_table(
139
  {
140
  WHEN_COL: f"**{local_time}**<br><span class='muted-cell'>Source: {source_time}</span>",
141
  "Event": event_cell,
142
- "Speaker": speaker_cell(event["speaker"], event["affiliation"]),
143
- "Category": chip(event["category"], "pill"),
144
- "Organization": org_badge(result["org_name"], colors),
145
- "Calendar": calendar_badge(current_event_key, event["attending_count"]),
146
  }
147
  )
148
 
149
- if not rows:
150
- return pd.DataFrame(columns=COLS)
151
-
152
  return pd.DataFrame(rows).reset_index(drop=True)[COLS]
 
1
  from __future__ import annotations
2
 
 
3
  from html import escape
4
  from urllib.parse import quote
5
 
6
  import pandas as pd
7
 
8
  from .config import local_display_tz
 
 
 
 
9
  LOCAL_DISPLAY_TZ = local_display_tz()
10
  WHEN_COL = "When"
11
  COLS = [WHEN_COL, "Event", "Speaker", "Category", "Organization", "Calendar"]
 
57
  )
58
 
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  def results_table(
61
+ df_filtered: pd.DataFrame,
62
  colors: dict[str, str],
 
 
 
 
63
  ) -> pd.DataFrame:
64
+ if df_filtered.empty:
65
+ return pd.DataFrame(columns=COLS)
66
+
67
  rows: list[dict[str, object]] = []
68
+ for row in df_filtered.itertuples(index=False):
69
+ current_event_key = row.event_key
70
  anchor_id = f"event-{current_event_key}"
71
+ title = row.title or "View event"
72
+ source_time = format_event_time(row.start_time)
73
+ local_time = format_event_time(row.start_time, tz_name=LOCAL_DISPLAY_TZ)
74
+ tag_chips = "".join(chip(tag) for tag in (row.tags or []))
75
  event_cell = (
76
  f'<a class="event-anchor event-anchor--{anchor_id}" '
77
+ f'href="{escape(row.event_url, quote=True)}">'
78
  f"{escape(title)}</a>"
79
+ if row.event_url
80
  else f'<strong class="event-anchor event-anchor--{anchor_id}">{escape(title)}</strong>'
81
  )
82
  event_cell = f"{event_cell}{share_button(anchor_id)}"
 
86
  {
87
  WHEN_COL: f"**{local_time}**<br><span class='muted-cell'>Source: {source_time}</span>",
88
  "Event": event_cell,
89
+ "Speaker": speaker_cell(row.speaker, row.affiliation),
90
+ "Category": chip(row.category, "pill"),
91
+ "Organization": org_badge(row.org_name, colors),
92
+ "Calendar": calendar_badge(current_event_key, row.attending_count),
93
  }
94
  )
95
 
 
 
 
96
  return pd.DataFrame(rows).reset_index(drop=True)[COLS]
src/ui/events_df.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+
5
+ import pandas as pd
6
+
7
+ from ..attending import LOCAL_DISPLAY_TZ
8
+ from ..events_schema import normalize_results
9
+ from ..models import parse_dt_utc
10
+ from ..teams_calendar import event_key as compute_event_key
11
+
12
+
13
+ def events_df(results: list[dict]) -> pd.DataFrame:
14
+ normalize_results(results)
15
+ now = pd.Timestamp.now(tz=LOCAL_DISPLAY_TZ)
16
+
17
+ rows: list[dict[str, object]] = []
18
+ for result in results:
19
+ if result["status"] != "ok":
20
+ continue
21
+ for event in result["events"]:
22
+ start_local = pd.Timestamp(parse_dt_utc(event["start_time"])).tz_convert(LOCAL_DISPLAY_TZ)
23
+ if start_local <= now - pd.Timedelta(hours=1):
24
+ continue
25
+ tags = event["tags"]
26
+ rows.append(
27
+ {
28
+ "start_local": start_local,
29
+ "org_name": result["org_name"],
30
+ "source_url": result["source_url"],
31
+ "title": event["title"],
32
+ "start_time": event["start_time"],
33
+ "event_url": event["event_url"],
34
+ "speaker": event["speaker"],
35
+ "affiliation": event["affiliation"],
36
+ "category": event["category"],
37
+ "tags": tags,
38
+ "attending_count": event["attending_count"],
39
+ "event_key": compute_event_key(result, event),
40
+ "haystack": " ".join(
41
+ (
42
+ result["org_name"],
43
+ event["title"],
44
+ event["speaker"],
45
+ event["affiliation"],
46
+ event["category"],
47
+ " ".join(tags),
48
+ )
49
+ ).lower(),
50
+ }
51
+ )
52
+
53
+ if not rows:
54
+ return pd.DataFrame(
55
+ columns=[
56
+ "start_local",
57
+ "org_name",
58
+ "source_url",
59
+ "title",
60
+ "start_time",
61
+ "event_url",
62
+ "speaker",
63
+ "affiliation",
64
+ "category",
65
+ "tags",
66
+ "attending_count",
67
+ "event_key",
68
+ "haystack",
69
+ ]
70
+ )
71
+
72
+ df = pd.DataFrame(rows)
73
+ df = df.sort_values(["start_local", "org_name"], kind="mergesort").reset_index(drop=True)
74
+ return df
75
+
76
+
77
+ def _facet_mask(values: Sequence[str] | None, series: pd.Series) -> pd.Series:
78
+ if values is None:
79
+ return pd.Series(True, index=series.index)
80
+ if not values:
81
+ return pd.Series(False, index=series.index)
82
+ return series.isin(list(values))
83
+
84
+
85
+ def filter_events_df(
86
+ df: pd.DataFrame,
87
+ query: str = "",
88
+ categories: Sequence[str] | None = None,
89
+ tags: Sequence[str] | None = None,
90
+ orgs: Sequence[str] | None = None,
91
+ ) -> pd.DataFrame:
92
+ if df.empty:
93
+ return df
94
+
95
+ mask = pd.Series(True, index=df.index)
96
+
97
+ mask &= _facet_mask(categories, df["category"])
98
+ mask &= _facet_mask(orgs, df["org_name"])
99
+
100
+ if tags is None:
101
+ pass
102
+ elif not tags:
103
+ mask &= False
104
+ else:
105
+ wanted = set(tags)
106
+ mask &= df["tags"].apply(lambda row_tags: bool(wanted.intersection(row_tags)))
107
+
108
+ needle = query.strip().lower()
109
+ if needle:
110
+ mask &= df["haystack"].str.contains(needle, regex=False)
111
+
112
+ return df.loc[mask].reset_index(drop=True)
113
+
src/ui/view.py CHANGED
@@ -7,12 +7,15 @@ from urllib.parse import urlencode
7
  import gradio as gr
8
  from fastapi import Request
9
 
10
- from ..attending import LOCAL_DISPLAY_TZ, filtered_events, format_event_time, results_table
 
 
11
  from ..org_colors import org_colors
 
12
 
13
 
14
  def render_hero(payload: dict, results: list[dict]) -> str:
15
- total_events = len(filtered_events(results))
16
  updated = payload["meta"]["cached_at"]
17
  return f"""
18
  <section class="hero-shell">
@@ -29,26 +32,12 @@ def render_hero(payload: dict, results: list[dict]) -> str:
29
  """
30
 
31
 
32
- def facet_choices(results: list[dict]) -> tuple[list[str], list[str], list[str]]:
33
- categories = sorted(
34
- {
35
- event["category"]
36
- for result in results
37
- if result["status"] == "ok"
38
- for event in result["events"]
39
- if event["category"]
40
- }
41
- )
42
- tags = sorted(
43
- {
44
- tag
45
- for result in results
46
- if result["status"] == "ok"
47
- for event in result["events"]
48
- for tag in event["tags"]
49
- }
50
- )
51
- orgs = sorted({result["org_name"] for result in results if result["status"] == "ok"})
52
  return categories, tags, orgs
53
 
54
 
@@ -100,14 +89,13 @@ def has_active_filters(
100
 
101
 
102
  def render_filter_summary(
103
- results: list[dict],
 
104
  query: str = "",
105
  categories: Sequence[str] | None = None,
106
  tags: Sequence[str] | None = None,
107
  orgs: Sequence[str] | None = None,
108
  ) -> str:
109
- total = len(filtered_events(results))
110
- matches = len(filtered_events(results, query, categories, tags, orgs))
111
  active = []
112
  if query.strip():
113
  active.append("search")
@@ -122,7 +110,7 @@ def render_filter_summary(
122
 
123
 
124
  def render_spotlight(
125
- results: list[dict],
126
  query: str = "",
127
  categories: Sequence[str] | None = None,
128
  tags: Sequence[str] | None = None,
@@ -137,18 +125,20 @@ def render_spotlight(
137
  cards = [
138
  f"""
139
  <article class="spotlight-card">
140
- <div class="spotlight-time">{escape(format_event_time(event["start_time"], tz_name=LOCAL_DISPLAY_TZ))}</div>
141
- <h3><a href="{escape(event["event_url"] or result["source_url"], quote=True)}">{escape(event["title"] or "View event")}</a></h3>
142
- <p>{escape(event["speaker"] or event["affiliation"] or event["category"])}</p>
143
  <div>
144
- {"".join(f'<span class="tag-chip">{escape(tag)}</span>' for tag in event["tags"][:2])}
145
- <span class="tag-chip">{escape(result["org_name"])}</span>
146
- <span class="pill">{escape(event["category"])}</span>
147
  </div>
148
  </article>
149
  """
150
- for _, _, result, event in filtered_events(results, query, categories, tags, orgs)[:3]
151
- ] or [f'<p class="empty-state">{escape(empty_state)}</p>']
 
 
152
  return f"""
153
  <section class="spotlight-shell">
154
  <p class="eyebrow">Next up</p>
@@ -157,22 +147,6 @@ def render_spotlight(
157
  """
158
 
159
 
160
- def build_view(
161
- results: list[dict],
162
- query: str = "",
163
- categories: Sequence[str] | None = None,
164
- tags: Sequence[str] | None = None,
165
- orgs: Sequence[str] | None = None,
166
- ):
167
- colors = org_colors(result["org_name"] for result in results)
168
- table = results_table(results, colors, query, categories, tags, orgs)
169
- return (
170
- render_filter_summary(results, query, categories, tags, orgs),
171
- render_spotlight(results, query, categories, tags, orgs),
172
- table,
173
- )
174
-
175
-
176
  def view_updates(
177
  results: list[dict],
178
  query: str = "",
@@ -180,12 +154,17 @@ def view_updates(
180
  tags: Sequence[str] | None = (),
181
  orgs: Sequence[str] | None = (),
182
  ):
183
- category_choices, tag_choices, org_choices = facet_choices(results)
 
184
  categories = coerce_facet_selection(categories, category_choices)
185
  tags = coerce_facet_selection(tags, tag_choices)
186
  orgs = coerce_facet_selection(orgs, org_choices)
187
 
188
- summary, spotlight, table = build_view(results, query, categories, tags, orgs)
 
 
 
 
189
  return (
190
  summary,
191
  spotlight,
@@ -196,7 +175,8 @@ def view_updates(
196
 
197
 
198
  def reset_filters(results: list[dict]):
199
- category_choices, tag_choices, org_choices = facet_choices(results)
 
200
  summary, spotlight, table, button_label, clear_button = view_updates(
201
  results,
202
  categories=category_choices,
 
7
  import gradio as gr
8
  from fastapi import Request
9
 
10
+ import pandas as pd
11
+
12
+ from ..attending import LOCAL_DISPLAY_TZ, format_event_time, results_table
13
  from ..org_colors import org_colors
14
+ from .events_df import events_df, filter_events_df
15
 
16
 
17
  def render_hero(payload: dict, results: list[dict]) -> str:
18
+ total_events = len(events_df(results))
19
  updated = payload["meta"]["cached_at"]
20
  return f"""
21
  <section class="hero-shell">
 
32
  """
33
 
34
 
35
+ def facet_choices(df_all: "pd.DataFrame") -> tuple[list[str], list[str], list[str]]:
36
+ if df_all.empty:
37
+ return [], [], []
38
+ categories = sorted([value for value in df_all["category"].dropna().unique().tolist() if value])
39
+ orgs = sorted([value for value in df_all["org_name"].dropna().unique().tolist() if value])
40
+ tags = sorted([value for value in df_all["tags"].explode().dropna().unique().tolist() if value])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  return categories, tags, orgs
42
 
43
 
 
89
 
90
 
91
  def render_filter_summary(
92
+ total: int,
93
+ matches: int,
94
  query: str = "",
95
  categories: Sequence[str] | None = None,
96
  tags: Sequence[str] | None = None,
97
  orgs: Sequence[str] | None = None,
98
  ) -> str:
 
 
99
  active = []
100
  if query.strip():
101
  active.append("search")
 
110
 
111
 
112
  def render_spotlight(
113
+ df_filtered: "pd.DataFrame",
114
  query: str = "",
115
  categories: Sequence[str] | None = None,
116
  tags: Sequence[str] | None = None,
 
125
  cards = [
126
  f"""
127
  <article class="spotlight-card">
128
+ <div class="spotlight-time">{escape(format_event_time(row.start_time, tz_name=LOCAL_DISPLAY_TZ))}</div>
129
+ <h3><a href="{escape(row.event_url or row.source_url, quote=True)}">{escape(row.title or "View event")}</a></h3>
130
+ <p>{escape(row.speaker or row.affiliation or row.category)}</p>
131
  <div>
132
+ {"".join(f'<span class="tag-chip">{escape(tag)}</span>' for tag in (row.tags or [])[:2])}
133
+ <span class="tag-chip">{escape(row.org_name)}</span>
134
+ <span class="pill">{escape(row.category)}</span>
135
  </div>
136
  </article>
137
  """
138
+ for row in df_filtered.head(3).itertuples(index=False)
139
+ ]
140
+ if not cards:
141
+ cards = [f'<p class="empty-state">{escape(empty_state)}</p>']
142
  return f"""
143
  <section class="spotlight-shell">
144
  <p class="eyebrow">Next up</p>
 
147
  """
148
 
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  def view_updates(
151
  results: list[dict],
152
  query: str = "",
 
154
  tags: Sequence[str] | None = (),
155
  orgs: Sequence[str] | None = (),
156
  ):
157
+ df_all = events_df(results)
158
+ category_choices, tag_choices, org_choices = facet_choices(df_all)
159
  categories = coerce_facet_selection(categories, category_choices)
160
  tags = coerce_facet_selection(tags, tag_choices)
161
  orgs = coerce_facet_selection(orgs, org_choices)
162
 
163
+ df_filtered = filter_events_df(df_all, query, categories, tags, orgs)
164
+ colors = org_colors(df_all["org_name"].tolist())
165
+ summary = render_filter_summary(len(df_all), len(df_filtered), query, categories, tags, orgs)
166
+ spotlight = render_spotlight(df_filtered, query, categories, tags, orgs)
167
+ table = results_table(df_filtered, colors)
168
  return (
169
  summary,
170
  spotlight,
 
175
 
176
 
177
  def reset_filters(results: list[dict]):
178
+ df_all = events_df(results)
179
+ category_choices, tag_choices, org_choices = facet_choices(df_all)
180
  summary, spotlight, table, button_label, clear_button = view_updates(
181
  results,
182
  categories=category_choices,
tests/test_golden.py CHANGED
@@ -5,9 +5,10 @@ import pytest
5
  from fastapi.testclient import TestClient
6
 
7
  import app
8
- from src.attending import COLS, WHEN_COL, filtered_events, results_table
9
  from src.models import SCHEMA_VERSION, infer_category, load_payload, parse_dt_utc, write_payload
10
  from src.teams_calendar import PENDING_STATES, SESSION_COOKIE, SESSIONS, ensure_user_on_event, event_key
 
11
 
12
 
13
  def sample_results() -> list[dict]:
@@ -170,7 +171,9 @@ def client():
170
 
171
  def test_results_table_shows_source_local_time_and_calendar_badge():
172
  results = sample_results()
173
- table = results_table(results, {"CMU": "#000000"})
 
 
174
  current_event_key = first_event_key(results)
175
 
176
  assert list(table.columns) == COLS
@@ -189,56 +192,59 @@ def test_results_table_shows_source_local_time_and_calendar_badge():
189
 
190
 
191
  def test_results_table_filters_case_insensitively():
192
- table = results_table(searchable_results(), searchable_colors(), query="VISION")
 
193
 
194
  assert table.shape[0] == 2
195
  assert "Vision Event" in table.loc[0, "Event"]
196
  assert "Robot vision in the wild" in table.loc[1, "Event"]
197
 
198
 
199
- def test_filtered_events_none_facets_do_not_filter():
200
- matches = filtered_events(searchable_results(), categories=None, tags=None, orgs=None)
201
 
202
- assert [event["title"] for _, _, _, event in matches] == [
203
  "Vision Event",
204
  "Robot vision in the wild",
205
  "Humanoid Control",
206
  ]
207
 
208
 
209
- def test_filtered_events_filter_by_category_tag_and_org():
210
- matches = filtered_events(
211
- searchable_results(), categories=["Lecture"], tags=["vision"], orgs=["Stanford"]
212
  )
213
 
214
- assert len(matches) == 1
215
- assert matches[0][3]["title"] == "Robot vision in the wild"
216
 
217
 
218
- def test_filtered_events_use_or_within_same_group():
219
- matches = filtered_events(searchable_results(), tags=["humanoids", "vision"])
220
 
221
- assert [event["title"] for _, _, _, event in matches] == ["Robot vision in the wild", "Humanoid Control"]
222
 
223
 
224
  def test_results_table_filters_by_category():
225
- table = results_table(searchable_results(), searchable_colors(), categories=["Seminar"])
 
226
 
227
  assert table.shape[0] == 1
228
  assert "Vision Event" in table.loc[0, "Event"]
229
 
230
 
231
  def test_results_table_filters_by_university_org():
232
- table = results_table(searchable_results(), searchable_colors(), orgs=["Stanford"])
 
233
 
234
  assert table.shape[0] == 1
235
  assert "Robot vision in the wild" in table.loc[0, "Event"]
236
 
237
 
238
  def test_render_filter_summary_shows_active_filters():
239
- summary = app.render_filter_summary(
240
- searchable_results(), query="vision", tags=["vision"], orgs=["Stanford"]
241
- )
242
 
243
  assert "**1** of **3** upcoming events." in summary
244
  assert "search" in summary
@@ -251,7 +257,8 @@ def test_filter_button_label_counts_selected_facets():
251
 
252
 
253
  def test_view_updates_treat_full_selection_as_default():
254
- categories, tags, orgs = app.facet_choices(searchable_results())
 
255
  summary, _, _, button_label, clear_button = app.view_updates(
256
  searchable_results(),
257
  categories=categories,
@@ -285,23 +292,24 @@ def test_render_selects_all_filter_choices_by_default(monkeypatch):
285
 
286
 
287
  def test_results_table_returns_empty_columns_when_filter_explicitly_empty():
288
- table = results_table(searchable_results(), searchable_colors(), tags=[])
 
289
 
290
  assert table.empty
291
  assert list(table.columns) == COLS
292
 
293
 
294
  def test_results_table_returns_empty_columns_when_filters_miss():
295
- table = results_table(
296
- searchable_results(), searchable_colors(), categories=["Seminar"], orgs=["Stanford"]
297
- )
298
 
299
  assert table.empty
300
  assert list(table.columns) == COLS
301
 
302
 
303
  def test_results_table_shows_only_future_events():
304
- table = results_table(mixed_results(), {"CMU": "#000000"})
 
305
 
306
  assert table.shape[0] == 1
307
  assert "Future Event" in table.loc[0, "Event"]
@@ -344,16 +352,15 @@ def test_filtered_events_keep_recently_started_events_visible_for_one_hour():
344
  "error": None,
345
  }
346
  ]
347
-
348
- matches = filtered_events(results)
349
-
350
- assert [event["title"] for _, _, _, event in matches] == ["Just Started"]
351
 
352
 
353
  def test_render_upcoming_counts_ignore_past_events():
354
  results = mixed_results()
355
  hero = app.render_hero({"meta": {"cached_at": "2026-04-12T20:22:12+00:00"}}, results)
356
- summary = app.render_filter_summary(results)
 
357
 
358
  assert "<strong>1</strong> upcoming" in hero
359
  assert "**1** of **1** upcoming events." in summary
 
5
  from fastapi.testclient import TestClient
6
 
7
  import app
8
+ from src.attending import COLS, WHEN_COL, results_table
9
  from src.models import SCHEMA_VERSION, infer_category, load_payload, parse_dt_utc, write_payload
10
  from src.teams_calendar import PENDING_STATES, SESSION_COOKIE, SESSIONS, ensure_user_on_event, event_key
11
+ from src.ui.events_df import events_df, filter_events_df
12
 
13
 
14
  def sample_results() -> list[dict]:
 
171
 
172
  def test_results_table_shows_source_local_time_and_calendar_badge():
173
  results = sample_results()
174
+ df_all = events_df(results)
175
+ df_filtered = filter_events_df(df_all)
176
+ table = results_table(df_filtered, {"CMU": "#000000"})
177
  current_event_key = first_event_key(results)
178
 
179
  assert list(table.columns) == COLS
 
192
 
193
 
194
  def test_results_table_filters_case_insensitively():
195
+ df_filtered = filter_events_df(events_df(searchable_results()), query="VISION")
196
+ table = results_table(df_filtered, searchable_colors())
197
 
198
  assert table.shape[0] == 2
199
  assert "Vision Event" in table.loc[0, "Event"]
200
  assert "Robot vision in the wild" in table.loc[1, "Event"]
201
 
202
 
203
+ def test_events_df_none_facets_do_not_filter():
204
+ df = filter_events_df(events_df(searchable_results()), categories=None, tags=None, orgs=None)
205
 
206
+ assert df["title"].tolist() == [
207
  "Vision Event",
208
  "Robot vision in the wild",
209
  "Humanoid Control",
210
  ]
211
 
212
 
213
+ def test_events_df_filter_by_category_tag_and_org():
214
+ df = filter_events_df(
215
+ events_df(searchable_results()), categories=["Lecture"], tags=["vision"], orgs=["Stanford"]
216
  )
217
 
218
+ assert df.shape[0] == 1
219
+ assert df.loc[0, "title"] == "Robot vision in the wild"
220
 
221
 
222
+ def test_events_df_use_or_within_same_group():
223
+ df = filter_events_df(events_df(searchable_results()), tags=["humanoids", "vision"])
224
 
225
+ assert df["title"].tolist() == ["Robot vision in the wild", "Humanoid Control"]
226
 
227
 
228
  def test_results_table_filters_by_category():
229
+ df_filtered = filter_events_df(events_df(searchable_results()), categories=["Seminar"])
230
+ table = results_table(df_filtered, searchable_colors())
231
 
232
  assert table.shape[0] == 1
233
  assert "Vision Event" in table.loc[0, "Event"]
234
 
235
 
236
  def test_results_table_filters_by_university_org():
237
+ df_filtered = filter_events_df(events_df(searchable_results()), orgs=["Stanford"])
238
+ table = results_table(df_filtered, searchable_colors())
239
 
240
  assert table.shape[0] == 1
241
  assert "Robot vision in the wild" in table.loc[0, "Event"]
242
 
243
 
244
  def test_render_filter_summary_shows_active_filters():
245
+ df_all = events_df(searchable_results())
246
+ df_filtered = filter_events_df(df_all, query="vision", tags=["vision"], orgs=["Stanford"])
247
+ summary = app.render_filter_summary(len(df_all), len(df_filtered), query="vision", tags=["vision"], orgs=["Stanford"])
248
 
249
  assert "**1** of **3** upcoming events." in summary
250
  assert "search" in summary
 
257
 
258
 
259
  def test_view_updates_treat_full_selection_as_default():
260
+ df_all = events_df(searchable_results())
261
+ categories, tags, orgs = app.facet_choices(df_all)
262
  summary, _, _, button_label, clear_button = app.view_updates(
263
  searchable_results(),
264
  categories=categories,
 
292
 
293
 
294
  def test_results_table_returns_empty_columns_when_filter_explicitly_empty():
295
+ df_filtered = filter_events_df(events_df(searchable_results()), tags=[])
296
+ table = results_table(df_filtered, searchable_colors())
297
 
298
  assert table.empty
299
  assert list(table.columns) == COLS
300
 
301
 
302
  def test_results_table_returns_empty_columns_when_filters_miss():
303
+ df_filtered = filter_events_df(events_df(searchable_results()), categories=["Seminar"], orgs=["Stanford"])
304
+ table = results_table(df_filtered, searchable_colors())
 
305
 
306
  assert table.empty
307
  assert list(table.columns) == COLS
308
 
309
 
310
  def test_results_table_shows_only_future_events():
311
+ df_filtered = filter_events_df(events_df(mixed_results()))
312
+ table = results_table(df_filtered, {"CMU": "#000000"})
313
 
314
  assert table.shape[0] == 1
315
  assert "Future Event" in table.loc[0, "Event"]
 
352
  "error": None,
353
  }
354
  ]
355
+ df_filtered = filter_events_df(events_df(results))
356
+ assert df_filtered["title"].tolist() == ["Just Started"]
 
 
357
 
358
 
359
  def test_render_upcoming_counts_ignore_past_events():
360
  results = mixed_results()
361
  hero = app.render_hero({"meta": {"cached_at": "2026-04-12T20:22:12+00:00"}}, results)
362
+ df_all = events_df(results)
363
+ summary = app.render_filter_summary(len(df_all), len(df_all))
364
 
365
  assert "<strong>1</strong> upcoming" in hero
366
  assert "**1** of **1** upcoming events." in summary