Skip to content

Commit 5f890ad

Browse files
feat: add event-linked Q&A
Closes #215
1 parent 5634ec1 commit 5f890ad

50 files changed

Lines changed: 5556 additions & 12 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

_docs/api/admin-openapi.json

Lines changed: 969 additions & 0 deletions
Large diffs are not rendered by default.

_docs/architecture/event-qna-integration.md

Lines changed: 276 additions & 0 deletions
Large diffs are not rendered by default.

accounts/studio_roles.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
SPONSORS_READ = "core.read_sponsors"
2424
SPONSORS_WRITE = "core.change_sponsors"
2525
SPONSORS_EXPORT = "core.export_sponsors"
26+
EVENT_QNA_VIEW = "events.view_event_qna"
27+
EVENT_QNA_MANAGE = "events.manage_event_qna"
2628

2729
_ROLE_PERMISSIONS: Mapping[str, frozenset[str]] = MappingProxyType(
2830
{
@@ -33,6 +35,8 @@
3335
MANAGE_API_CREDENTIALS,
3436
HISTORICAL_REGISTRATION_IMPORT_MANAGE,
3537
HISTORICAL_REGISTRATION_MAPPING_MANAGE,
38+
EVENT_QNA_VIEW,
39+
EVENT_QNA_MANAGE,
3640
COURSE_REGISTRATION_COUNT_BASELINE_MANAGE,
3741
SITE_SETTINGS_READ,
3842
SITE_SETTINGS_WRITE,
@@ -55,14 +59,14 @@
5559
SPONSORS_EXPORT,
5660
}
5761
),
58-
"course_operator": frozenset(
59-
{STUDIO_ACCESS, COURSE_REGISTRATION_COUNT_BASELINE_MANAGE}
60-
),
62+
"course_operator": frozenset({STUDIO_ACCESS, COURSE_REGISTRATION_COUNT_BASELINE_MANAGE}),
6163
"event_operator": frozenset(
6264
{
6365
STUDIO_ACCESS,
6466
HISTORICAL_REGISTRATION_IMPORT_MANAGE,
6567
HISTORICAL_REGISTRATION_MAPPING_MANAGE,
68+
EVENT_QNA_VIEW,
69+
EVENT_QNA_MANAGE,
6670
}
6771
),
6872
"email_operator": frozenset({STUDIO_ACCESS}),

accounts/tests/test_development_owner.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ def test_create_and_reconcile_are_exact_idempotent_and_secret_safe(self) -> None
6868
("management_auth", "manage_api_credentials"),
6969
("events", "historical_registration_import_manage"),
7070
("events", "historical_registration_mapping_manage"),
71+
("events", "view_event_qna"),
72+
("events", "manage_event_qna"),
7173
("courses", "registration_count_baseline_manage"),
7274
("core", "read_operational_settings"),
7375
("core", "change_operational_settings"),

ci/focused_tests.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@ def run(selection_path: str) -> None:
1212
if selection["profile"] != "focused":
1313
raise ValueError("the focused runner requires profile=focused")
1414
subprocess.run(
15-
[sys.executable, "manage.py", "test", *selection["test_labels"]],
15+
[
16+
sys.executable,
17+
"manage.py",
18+
"test",
19+
"--parallel",
20+
"--noinput",
21+
*selection["test_labels"],
22+
],
1623
check=True,
1724
)
1825

ci/ownership.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
{"id":"app.management_api","kind":"owner","prefixes":["management_api/"],"exact":[],"downstream":["django.api","django.management_api","django.studio"],"components":["container","django","playwright","quality"],"environment_dimensions":["architecture","database","django","operating_system","python","runner_image","uv"],"validity_class":"standard","risk_flags":[],"render_flags":[]},
6666
{"id":"app.management_auth","kind":"owner","prefixes":["management_auth/"],"exact":[],"downstream":["django.accounts","django.api","django.core","django.management_api","django.management_auth","django.studio"],"components":["container","django","playwright","quality"],"environment_dimensions":["architecture","database","django","operating_system","python","runner_image","uv"],"validity_class":"standard","risk_flags":[],"render_flags":[]},
6767
{"id":"app.review_import","kind":"owner","prefixes":["review_import/"],"exact":[],"downstream":["django.accounts","django.courses","django.review_import"],"components":["container","django","playwright","quality"],"environment_dimensions":["architecture","database","django","operating_system","python","runner_image","uv"],"validity_class":"standard","risk_flags":[],"render_flags":[]},
68-
{"id":"app.studio","kind":"owner","prefixes":["studio/"],"exact":[],"downstream":["django.accounts","django.core","django.studio"],"components":["container","django","playwright","quality"],"environment_dimensions":["architecture","database","django","operating_system","python","runner_image","uv"],"validity_class":"standard","risk_flags":[],"render_flags":[]},
68+
{"id":"app.studio","kind":"owner","prefixes":["studio/"],"exact":[],"downstream":["django.accounts","django.core","django.events","django.studio"],"components":["container","django","playwright","quality"],"environment_dimensions":["architecture","database","django","operating_system","python","runner_image","uv"],"validity_class":"standard","risk_flags":[],"render_flags":[]},
6969

7070
{"id":"shared.accounts","kind":"owner","prefixes":["accounts/"],"exact":[],"downstream":[],"components":["container","django","playwright","quality"],"environment_dimensions":["architecture","database","django","operating_system","python","runner_image","uv"],"validity_class":"standard","risk_flags":["shared_runtime","auth_security_privacy"],"render_flags":[]},
7171
{"id":"shared.core","kind":"owner","prefixes":["core/"],"exact":[],"downstream":[],"components":["container","django","playwright","quality"],"environment_dimensions":["architecture","database","django","operating_system","python","runner_image","uv"],"validity_class":"standard","risk_flags":["shared_runtime"],"render_flags":[]},

ci/tests/test_selection.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,9 @@ def test_json_and_summary_are_deterministic_and_do_not_include_filenames(tmp_pat
227227
assert json.loads(first.read_text(encoding="utf-8")) == selection
228228

229229

230-
def test_focused_runner_uses_one_process_and_only_validated_labels(tmp_path: Path) -> None:
230+
def test_focused_runner_uses_parallel_noninteractive_command_and_only_validated_labels(
231+
tmp_path: Path,
232+
) -> None:
231233
selection = classify_records(
232234
(ChangeRecord("M", ("content/a.py",)),), event="push", base=BASE, head=HEAD
233235
)
@@ -242,6 +244,8 @@ def test_focused_runner_uses_one_process_and_only_validated_labels(tmp_path: Pat
242244
focused_tests.sys.executable,
243245
"manage.py",
244246
"test",
247+
"--parallel",
248+
"--noinput",
245249
"accounts",
246250
"content.tests",
247251
"content_sync",

content/public_urls.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from django.urls import path, re_path
22

3+
from events.qna import views as qna_views
4+
35
from . import legal_views, public_views
46

57
urlpatterns = [
@@ -102,6 +104,49 @@
102104
public_views.event_legacy_redirect,
103105
name="public-event-legacy-trailing-slash",
104106
),
107+
# Event-linked Q&A routes must precede the generic event detail route. The
108+
# public numeric ID and current title slug remain the only Event lookup
109+
# inputs; the slug is cosmetic and stale spellings redirect on HTML only.
110+
re_path(
111+
r"^events/(?P<event_id>[1-9][0-9]*)/(?P<slug>[-a-zA-Z0-9_]+)/qna/$",
112+
qna_views.public_qna,
113+
name="public-event-qna",
114+
),
115+
re_path(
116+
r"^events/(?P<event_id>[1-9][0-9]*)/(?P<slug>[-a-zA-Z0-9_]+)/qna/api/questions/$",
117+
qna_views.qna_questions,
118+
name="public-event-qna-questions",
119+
),
120+
re_path(
121+
r"^events/(?P<event_id>[1-9][0-9]*)/(?P<slug>[-a-zA-Z0-9_]+)/qna/api/questions/(?P<question_id>[-A-Za-z0-9_]+)/$",
122+
qna_views.qna_question,
123+
name="public-event-qna-question",
124+
),
125+
re_path(
126+
r"^events/(?P<event_id>[1-9][0-9]*)/(?P<slug>[-a-zA-Z0-9_]+)/qna/api/questions/(?P<question_id>[-A-Za-z0-9_]+)/vote/$",
127+
qna_views.qna_vote,
128+
name="public-event-qna-vote",
129+
),
130+
re_path(
131+
r"^events/(?P<event_id>[1-9][0-9]*)/(?P<slug>[-a-zA-Z0-9_]+)/qna/cohost/(?P<name>[a-zA-Z0-9-]+)/$",
132+
qna_views.qna_cohost_gate,
133+
name="public-event-qna-cohost",
134+
),
135+
re_path(
136+
r"^events/(?P<event_id>[1-9][0-9]*)/(?P<slug>[-a-zA-Z0-9_]+)/qna/host/$",
137+
qna_views.qna_host,
138+
name="public-event-qna-host",
139+
),
140+
re_path(
141+
r"^events/(?P<event_id>[1-9][0-9]*)/(?P<slug>[-a-zA-Z0-9_]+)/qna/present/$",
142+
qna_views.qna_present,
143+
name="public-event-qna-present",
144+
),
145+
re_path(
146+
r"^events/(?P<event_id>[1-9][0-9]*)/(?P<slug>[-a-zA-Z0-9_]+)/qna/qr\.(?P<kind>svg|png)$",
147+
qna_views.qna_qr,
148+
name="public-event-qna-qr",
149+
),
105150
re_path(
106151
r"^events/(?P<event_id>[1-9][0-9]*)/(?P<slug>[-a-zA-Z0-9_]+)$",
107152
public_views.event_detail,

content/public_views.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
resolve_public_id,
3333
resolve_uuid,
3434
)
35+
from events.models import EventQnaSession
3536
from events.services import public_registration_total
3637

3738
from . import wiki_content
@@ -392,6 +393,13 @@ def event_detail(request: HttpRequest, event_id: str, slug: str) -> HttpResponse
392393
if event["ends_at"]:
393394
entity["endDate"] = event["ends_at"]
394395
registration_total = public_registration_total(event)
396+
qna_url = ""
397+
qna_session = EventQnaSession.objects.filter(event_id=identity.id).first()
398+
if qna_session is not None and qna_session.state in {
399+
EventQnaSession.State.OPEN,
400+
EventQnaSession.State.CLOSED,
401+
}:
402+
qna_url = f"{event['public_path']}/qna/"
395403
response = _render(
396404
request,
397405
"public/event_detail.html",
@@ -402,6 +410,7 @@ def event_detail(request: HttpRequest, event_id: str, slug: str) -> HttpResponse
402410
"event": event,
403411
"event_state": event_state,
404412
"registration_total": registration_total,
413+
"qna_url": qna_url,
405414
"og_type": "event",
406415
"structured_data": _json_ld(
407416
entity,

core/accessibility_registry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ def template_readability_issues(source: str) -> list[str]:
288288
"templates/404.html": "public",
289289
"templates/core": "public",
290290
"templates/public": "public",
291+
"templates/events": "public",
291292
"templates/review": "public-review",
292293
"templates/studio": "studio",
293294
"templates/registration": "account",

0 commit comments

Comments
 (0)