Skip to content

Commit 7b780c3

Browse files
zaryatyklingpre-commit-ci[bot]
authored
Added streaming selection option to events (#1830)
* Added streaming selection option to events * Updated migration * Added data migration to the database migration * Make streaming seperate from recording * Add column streaming and tooltips to event_list_table * Updated icons for event pages * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update src/program/models.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update src/program/migrations/0106_event_video_streaming_and_more.py Co-authored-by: Thomas Steen Rasmussen <tykling@bornhack.org> * Update src/program/migrations/0106_event_video_streaming_and_more.py --------- Co-authored-by: Thomas Steen Rasmussen <tykling@bornhack.org> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 4e0bdf1 commit 7b780c3

11 files changed

Lines changed: 144 additions & 62 deletions

File tree

src/bornhack/settings.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,22 +221,18 @@
221221
"SCOPES": {
222222
# required
223223
"openid": "OpenID Connect scope",
224-
225224
# deprecated api scope, remove after 2025 camp
226225
"profile:read": "Allow the remote site to read your bornhack.dk username (uuid), user id, profile public credit name, profile description, and a list of team memberships using the profile API endpoint (scope profile:read). NOTE: This scope is being deprecated soon! Ask the BornHack website team for more info.",
227-
228226
# standard OIDC claim scopes
229227
"profile": "Allow the remote site to read your profile public_credit_name, description, and update time (scope: profile)",
230228
"email": "Allow the remote site to read your email address (scope: email)",
231229
"address": "Allow the remote site to read your profile location (scope: address)",
232230
"phone": "Allow the remote site to read your profile phonenumber (scope: phone)",
233-
234231
# custom bornhack user claim scopes
235232
"groups:read": "Allow the remote site to read a list of your group memberships (scope: groups:read).",
236233
"location:read": "Allow the remote site to read your profile location (scope: loocation:read)",
237234
"permissions:read": "Allow the remote site to read a list of your assigned permissions (scope: permissions:read).",
238235
"teams:read": "Allow the remote site to read a list of your team memberships and team lead status (scope: teams:read)",
239-
240236
# api scopes
241237
"phonebook:admin": "Allow the remote site to read the camp phonebook, including service numbers and unlisted numbers. Also allow the remote site to use to the POC API. This scope is only relevant for POC team leads (scope: phonebook:admin).",
242238
"phonebook:read": "Allow the remote site to read the camp phonebook (scope: phonebook:read).",

src/profiles/forms.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
from django import forms
22
from bornhack.oauth_validators import BornhackOAuth2Validator
33

4+
45
def get_scopes() -> list[str]:
56
validator = BornhackOAuth2Validator()
6-
return ((scope, scope) for scope in sorted(set(validator.oidc_claim_scope.values())) if scope!="openid")
7+
return (
8+
(scope, scope)
9+
for scope in sorted(set(validator.oidc_claim_scope.values()))
10+
if scope != "openid"
11+
)
12+
713

814
class OIDCForm(forms.Form):
915
scopes = forms.MultipleChoiceField(

src/profiles/templates/oidc.html

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,53 +11,53 @@
1111
<h4>OIDC Claims</h4>
1212
</div>
1313
<div class="card-body">
14-
<p class="lead">When using BornHack as an IDP (logging into other sites using your BornHack account) you can control which <i>user claims</i> are shared with the remote site by asking for one or more of the following <i>claim scopes</i>:</p>
14+
<p class="lead">When using BornHack as an IDP (logging into other sites using your BornHack account) you can control which <i>user claims</i> are shared with the remote site by asking for one or more of the following <i>claim scopes</i>:</p>
1515
<p><ul>
1616
{% for scope in all_scopes %}
17-
<li><code>{{ scope }}</code></li>
17+
<li><code>{{ scope }}</code></li>
1818
{% endfor %}
1919
</ul></p>
2020
<p>Note: In addition to this list the default <code>openid</code> scope is available (it is part of the standard) and must always be included when asking for a jwt.</p>
2121
<p class="lead">This form allows you to see which OIDC user claims are returned for your user with any combination of scopes.</p>
2222
<form method="GET">
23-
{% bootstrap_form form %}
24-
<button class="btn btn-primary" type="submit">Submit</button>
23+
{% bootstrap_form form %}
24+
<button class="btn btn-primary" type="submit">Submit</button>
2525
</form>
2626
<hr>
2727
{% if not active_scopes %}
28-
<p class="lead">Select scopes in the form to see user claims</p>
28+
<p class="lead">Select scopes in the form to see user claims</p>
2929
{% else %}
30-
<p class="lead">The following user claims will be returned in a jwt with these scopes:</p>
31-
<p>
32-
<ul>
33-
{% for scope in active_scopes %}
34-
<li><code>{{ scope }}</code></li>
35-
{% endfor %}
36-
</ul>
37-
</p>
38-
<table class="table table-striped">
39-
<tr>
40-
<th>Claim Name</th>
41-
<th>Required Scope</th>
42-
<th>Claim Value (JSON)</th>
43-
</tr>
44-
<tr>
45-
<td><code>sub</code></td>
46-
<td><code>openid</code></td>
47-
<td>{{ request.user.username }}</td>
48-
</tr>
49-
{% for claim, value in claims.items %}
50-
{% for claimname, scope in scopes.items %}
51-
{% if claimname == claim %}
30+
<p class="lead">The following user claims will be returned in a jwt with these scopes:</p>
31+
<p>
32+
<ul>
33+
{% for scope in active_scopes %}
34+
<li><code>{{ scope }}</code></li>
35+
{% endfor %}
36+
</ul>
37+
</p>
38+
<table class="table table-striped">
5239
<tr>
53-
<td><code>{{ claim }}</code></td>
54-
<td><code>{{ scope }}</code></td>
55-
<td>{{ value }}</td>
40+
<th>Claim Name</th>
41+
<th>Required Scope</th>
42+
<th>Claim Value (JSON)</th>
5643
</tr>
57-
{% endif %}
58-
{% endfor %}
59-
{% endfor %}
60-
</table>
44+
<tr>
45+
<td><code>sub</code></td>
46+
<td><code>openid</code></td>
47+
<td>{{ request.user.username }}</td>
48+
</tr>
49+
{% for claim, value in claims.items %}
50+
{% for claimname, scope in scopes.items %}
51+
{% if claimname == claim %}
52+
<tr>
53+
<td><code>{{ claim }}</code></td>
54+
<td><code>{{ scope }}</code></td>
55+
<td>{{ value }}</td>
56+
</tr>
57+
{% endif %}
58+
{% endfor %}
59+
{% endfor %}
60+
</table>
6161
{% endif %}
6262
</div>
6363
</div>

src/profiles/templates/profile_base.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ <h2>Your BornHack Account</h2>
9494
{% url 'profiles:oidc' as profile_oidc_url %}
9595
<li class="nav-item">
9696
<a class="nav-link{% if request.path == profile_oidc_url %} active{% endif %}" href="{{ profile_oidc_url }}">
97-
OIDC Scope<i class="fas fa-arrow-right"></i>Claim
97+
OIDC Scope<i class="fas fa-arrow-right"></i>Claim
9898
</a>
9999
</li>
100100

src/profiles/views.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def get_form(self, form_class=None):
122122
if form_class is None:
123123
form_class = self.get_form_class()
124124
scopes = self.request.GET.getlist(key="scopes")
125-
self.initial['scopes'] = scopes
125+
self.initial["scopes"] = scopes
126126
return form_class(**self.get_form_kwargs())
127127

128128
def get_context_data(self, **kwargs):
@@ -133,7 +133,9 @@ def get_context_data(self, **kwargs):
133133
if scope in self.request.GET.getlist(key="scopes"):
134134
context["claims"][claim] = value
135135
context["scopes"] = self.scopes
136-
context["active_scopes"] = ["openid"] + sorted(set(self.request.GET.getlist(key="scopes")))
136+
context["active_scopes"] = ["openid"] + sorted(
137+
set(self.request.GET.getlist(key="scopes")),
138+
)
137139
context["all_scopes"] = sorted(set(self.scopes.values()))
138-
del(context["all_scopes"][context["all_scopes"].index("openid")])
140+
del context["all_scopes"][context["all_scopes"].index("openid")]
139141
return context

src/program/forms.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ class Meta:
268268
"title",
269269
"abstract",
270270
"allow_video_recording",
271+
"allow_video_streaming",
271272
"duration",
272273
"tags",
273274
"slides_url",
@@ -313,8 +314,9 @@ def __init__(self, camp, event_type=None, matrix=None, *args, **kwargs):
313314
self.fields["track"].empty_label = None
314315
self.fields["track"].queryset = EventTrack.objects.filter(camp=camp)
315316

316-
# make sure video_recording checkbox defaults to checked
317+
# make sure video_recording and streaming checkbox defaults to checked
317318
self.fields["allow_video_recording"].initial = True
319+
self.fields["allow_video_streaming"].initial = True
318320

319321
if event_type.name not in [TALK, LIGHTNING_TALK]:
320322
# Only talk or lightning talk should show the slides_url field
@@ -374,6 +376,7 @@ def __init__(self, camp, event_type=None, matrix=None, *args, **kwargs):
374376

375377
# no video recording for music acts
376378
del self.fields["allow_video_recording"]
379+
del self.fields["allow_video_streaming"]
377380

378381
elif event_type.name == RECREATIONAL_EVENT:
379382
# fix label and help_text for the title field
@@ -394,6 +397,7 @@ def __init__(self, camp, event_type=None, matrix=None, *args, **kwargs):
394397

395398
# no video recording for music acts
396399
del self.fields["allow_video_recording"]
400+
del self.fields["allow_video_streaming"]
397401

398402
elif event_type.name in [TALK, LIGHTNING_TALK]:
399403
# fix label and help_text for the title field
@@ -439,6 +443,7 @@ def __init__(self, camp, event_type=None, matrix=None, *args, **kwargs):
439443

440444
# no video recording for workshops
441445
del self.fields["allow_video_recording"]
446+
del self.fields["allow_video_streaming"]
442447

443448
elif event_type.name == RECREATIONAL_EVENT:
444449
# fix label and help_text for the title field
@@ -459,6 +464,7 @@ def __init__(self, camp, event_type=None, matrix=None, *args, **kwargs):
459464

460465
# no video recording for recreational events
461466
del self.fields["allow_video_recording"]
467+
del self.fields["allow_video_streaming"]
462468

463469
elif event_type.name == MEETUP:
464470
# fix label and help_text for the title field
@@ -479,6 +485,7 @@ def __init__(self, camp, event_type=None, matrix=None, *args, **kwargs):
479485

480486
# no video recording for meetups
481487
del self.fields["allow_video_recording"]
488+
del self.fields["allow_video_streaming"]
482489

483490
else:
484491
raise ImproperlyConfigured(
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Generated by Django 4.2.20 on 2025-04-21 17:36
2+
3+
from django.db import migrations, models
4+
5+
def update_streaming(apps, schema_editor):
6+
# We can't import the models directly as it may be a newer
7+
# version than this migration expects. We use the historical version.
8+
Event = apps.get_model("program", "Event")
9+
EventProposal = apps.get_model("program", "EventProposal")
10+
11+
Event.objects.filter(video_recording=True).update(video_streaming=True)
12+
Event.objects.filter(video_recording=False).update(video_streaming=False)
13+
EventProposal.objects.filter(allow_video_recording=False).update(allow_video_streaming=False)
14+
EventProposal.objects.filter(allow_video_recording=True).update(allow_video_streaming=True)
15+
16+
class Migration(migrations.Migration):
17+
18+
dependencies = [
19+
('program', '0105_cascade_delete_event_urls'),
20+
]
21+
22+
operations = [
23+
migrations.AddField(
24+
model_name='event',
25+
name='video_streaming',
26+
field=models.BooleanField(default=True, help_text='Do we intend to stream video of this event?'),
27+
),
28+
migrations.AddField(
29+
model_name='eventproposal',
30+
name='allow_video_streaming',
31+
field=models.BooleanField(default=False, help_text='Uncheck if you do not want the event live streamed.'),
32+
),
33+
migrations.RunPython(update_streaming),
34+
]

src/program/models.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,11 @@ class EventProposal(ExportModelOperationsMixin("event_proposal"), UserSubmittedM
544544
help_text="Recordings are made available under the <b>CC BY-SA 4.0</b> license. Uncheck if you do not want the event recorded, or if you cannot accept the license.",
545545
)
546546

547+
allow_video_streaming = models.BooleanField(
548+
default=False,
549+
help_text="Uncheck if you do not want the event live streamed.",
550+
)
551+
547552
duration = models.IntegerField(
548553
blank=True,
549554
help_text="How much time (in minutes) should we set aside for this event?",
@@ -609,6 +614,7 @@ def mark_as_approved(self, request=None):
609614
event.event_type = self.event_type
610615
event.proposal = self
611616
event.video_recording = self.allow_video_recording
617+
event.video_streaming = self.allow_video_streaming
612618
event.save()
613619
# loop through the speaker_proposals linked to this event_proposal and associate any related speaker objects with this event
614620
for sp in self.speakers.all():
@@ -1207,10 +1213,12 @@ def get_ics_event(self):
12071213
domain = Site.objects.get_current().domain
12081214
speakers = ", ".join(self.event.speakers.all().values_list("name", flat=True))
12091215
recorded = "Yes" if self.event.video_recording else "No"
1216+
streamed = "Yes" if self.event.video_streaming else "No"
12101217
ievent["description"] = (
12111218
f"URL: https://{domain}{self.event.get_absolute_url()}\n\n"
12121219
f"Speaker(s): {speakers}\n\n"
12131220
f"Recorded: {recorded}\n\n"
1221+
f"Streamed: {streamed}\n\n"
12141222
f"{self.event.abstract}"
12151223
)
12161224
ievent["dtstart"] = icalendar.vDatetime(self.when.lower).to_ical()
@@ -1308,6 +1316,11 @@ class Event(ExportModelOperationsMixin("event"), CampRelatedModel):
13081316
help_text="Do we intend to record video of this event?",
13091317
)
13101318

1319+
video_streaming = models.BooleanField(
1320+
default=True,
1321+
help_text="Do we intend to stream video of this event?",
1322+
)
1323+
13111324
proposal = models.OneToOneField(
13121325
"program.EventProposal",
13131326
null=True,
@@ -1378,10 +1391,14 @@ def serialize(self):
13781391
"event_type": self.event_type.name,
13791392
}
13801393

1381-
if self.video_recording:
1382-
video_state = "to-be-recorded"
1394+
if self.video_recording and self.video_streaming:
1395+
video_state = "to-be-streamed-to-be-recorded"
1396+
elif self.video_recording:
1397+
video_state = "to-be-recorded-not-to-be-streamed"
1398+
elif self.video_streaming:
1399+
video_state = "to-be-streamed-not-to-be-recorded"
13831400
else:
1384-
video_state = "not-to-be-recorded"
1401+
video_state = "not-to-be-recorded-not-to-be-streamed"
13851402

13861403
data["video_state"] = video_state
13871404

@@ -1513,10 +1530,14 @@ def serialize(self, user=None):
15131530
"timeslots": self.timeslots,
15141531
}
15151532

1516-
if self.event.video_recording:
1517-
video_state = "to-be-recorded"
1533+
if self.event.video_recording and self.event.video_streaming:
1534+
video_state = "to-be-recorded-to-be-streamed"
1535+
elif self.event.video_recording:
1536+
video_state = "to-be-recorded-not-to-be-streamed"
1537+
elif self.event.video_streaming:
1538+
video_state = "to-be-streamed-not-to-be-recorded"
15181539
else:
1519-
video_state = "not-to-be-recorded"
1540+
video_state = "not-to-be-recorded-not-to-be-streamed"
15201541

15211542
data["video_state"] = video_state
15221543

src/program/templates/event_detail.html

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,25 @@ <h4>
4141
<h4>Metadata for <i>{{ event.title }}</i></h4>
4242
<strong>To be recorded</strong>:
4343
<span class="fa-stack">
44-
<i class="fas fa-video fa-stack-1x"></i>
4544
{% if event.video_recording %}
46-
<i class="far fa-circle fa-stack-2x"></i>
45+
<i class="fas fa-floppy-disk fa-stack-1x"></i>
4746
{% else %}
47+
<i class="fas fa-floppy-disk fa-stack-1x"></i>
4848
<i class="fas fa-ban fa-stack-2x text-danger"></i>
4949
{% endif %}
5050
</span>
5151
{{ event.video_recording|yesno:"Yes,No" }}
52+
<br>
53+
<strong>To be streamed</strong>:
54+
<span class="fa-stack">
55+
{% if event.video_streaming %}
56+
<i class="far fa-broadcast-tower fa-stack-1x"></i>
57+
{% else %}
58+
<i class="fas fa-broadcast-tower fa-stack-1x"></i>
59+
<i class="fas fa-ban fa-stack-2x text-danger"></i>
60+
{% endif %}
61+
</span>
62+
{{ event.video_streaming|yesno:"Yes,No" }}
5263
<hr>
5364

5465
<h4>URLs for <i>{{ event.title }}</i></h4>

src/program/templates/includes/event_list_table.html

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
<th data-priority="2">Event Type</th>
99
<th data-priority="100" class="text-center">Tags</th>
1010
<th data-priority="3">Speakers</th>
11-
<th data-priority="100"><i class="fas fa-video"></i></th>
11+
<th data-priority="100"><i class="fas fa-video" title="Recording" alt="Recording"></i></th>
12+
<th data-priority="100"><i class="fas fa-floppy-disk" title="Recording" alt="Recording"></i></th>
13+
<th data-priority="100"><i class="fas fa-broadcast-tower" title="Streaming" alt="Streaming"></i></th>
1214
<th data-priority="1">Scheduled</th>
1315
</tr>
1416
</thead>
@@ -37,15 +39,17 @@
3739
N/A
3840
{% endfor %}
3941
</td>
40-
<td class="text-center"><span class="hidden">{{ event.video_recording }}</span>{{ event.video_recording|truefalseicon }}</td>
41-
<td data-order="{{ event.event_slots.all.0.when.lower|date:"YmdHis" }}">
42-
{% for slot in event.event_slots.all %}
43-
{{ slot.event_location.icon_html }} {{ slot.event_location.name }} at {{ slot.when.lower }}<br>
44-
{% empty %}
45-
<i>Not scheduled yet</i>
46-
{% endfor %}
47-
</td>
48-
</tr>
42+
<td class="text-center"><span class="hidden">{% if event.video_recording or event.video_streaming %}True</span>{{ True|truefalseicon }}{% else %}False</span>{{ False|truefalseicon }}{% endif %}</td>
43+
<td class="text-center" title="Recording: {{ event.video_recording|yesno }}"><span class="hidden">{{ event.video_recording }}</span>{{ event.video_recording|truefalseicon }}</td>
44+
<td class="text-center" title="Streaming: {{ event.video_streaming|yesno }}"><span class="hidden">{{ event.video_streaming }}</span>{{ event.video_streaming|truefalseicon }}</td>
45+
<td data-order="{{ event.event_slots.all.0.when.lower|date:"YmdHis" }}">
46+
{% for slot in event.event_slots.all %}
47+
{{ slot.event_location.icon_html }} {{ slot.event_location.name }} at {{ slot.when.lower }}<br>
48+
{% empty %}
49+
<i>Not scheduled yet</i>
50+
{% endfor %}
51+
</td>
52+
</tr>
4953
{% endif %}
5054
{% endfor %}
5155
</tbody>

0 commit comments

Comments
 (0)