diff --git a/src/bornhack/settings.py b/src/bornhack/settings.py
index bd419f186..0648a2c10 100644
--- a/src/bornhack/settings.py
+++ b/src/bornhack/settings.py
@@ -221,22 +221,18 @@
"SCOPES": {
# required
"openid": "OpenID Connect scope",
-
# deprecated api scope, remove after 2025 camp
"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.",
-
# standard OIDC claim scopes
"profile": "Allow the remote site to read your profile public_credit_name, description, and update time (scope: profile)",
"email": "Allow the remote site to read your email address (scope: email)",
"address": "Allow the remote site to read your profile location (scope: address)",
"phone": "Allow the remote site to read your profile phonenumber (scope: phone)",
-
# custom bornhack user claim scopes
"groups:read": "Allow the remote site to read a list of your group memberships (scope: groups:read).",
"location:read": "Allow the remote site to read your profile location (scope: loocation:read)",
"permissions:read": "Allow the remote site to read a list of your assigned permissions (scope: permissions:read).",
"teams:read": "Allow the remote site to read a list of your team memberships and team lead status (scope: teams:read)",
-
# api scopes
"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).",
"phonebook:read": "Allow the remote site to read the camp phonebook (scope: phonebook:read).",
diff --git a/src/profiles/forms.py b/src/profiles/forms.py
index d7f8cac65..ff8d3153b 100644
--- a/src/profiles/forms.py
+++ b/src/profiles/forms.py
@@ -1,9 +1,15 @@
from django import forms
from bornhack.oauth_validators import BornhackOAuth2Validator
+
def get_scopes() -> list[str]:
validator = BornhackOAuth2Validator()
- return ((scope, scope) for scope in sorted(set(validator.oidc_claim_scope.values())) if scope!="openid")
+ return (
+ (scope, scope)
+ for scope in sorted(set(validator.oidc_claim_scope.values()))
+ if scope != "openid"
+ )
+
class OIDCForm(forms.Form):
scopes = forms.MultipleChoiceField(
diff --git a/src/profiles/templates/oidc.html b/src/profiles/templates/oidc.html
index fea18d726..81395bd90 100644
--- a/src/profiles/templates/oidc.html
+++ b/src/profiles/templates/oidc.html
@@ -11,53 +11,53 @@
OIDC Claims
-
When using BornHack as an IDP (logging into other sites using your BornHack account) you can control which user claims are shared with the remote site by asking for one or more of the following claim scopes:
+
When using BornHack as an IDP (logging into other sites using your BornHack account) you can control which user claims are shared with the remote site by asking for one or more of the following claim scopes:
{% for scope in all_scopes %}
- {{ scope }}
+ {{ scope }}
{% endfor %}
Note: In addition to this list the default openid scope is available (it is part of the standard) and must always be included when asking for a jwt.
This form allows you to see which OIDC user claims are returned for your user with any combination of scopes.
{% if not active_scopes %}
-
Select scopes in the form to see user claims
+
Select scopes in the form to see user claims
{% else %}
-
The following user claims will be returned in a jwt with these scopes:
-
-
- {% for scope in active_scopes %}
- {{ scope }}
- {% endfor %}
-
-
-
-
- | Claim Name |
- Required Scope |
- Claim Value (JSON) |
-
-
- sub |
- openid |
- {{ request.user.username }} |
-
- {% for claim, value in claims.items %}
- {% for claimname, scope in scopes.items %}
- {% if claimname == claim %}
+ The following user claims will be returned in a jwt with these scopes:
+
+
+ {% for scope in active_scopes %}
+ {{ scope }}
+ {% endfor %}
+
+
+
- {{ claim }} |
- {{ scope }} |
- {{ value }} |
+ Claim Name |
+ Required Scope |
+ Claim Value (JSON) |
- {% endif %}
- {% endfor %}
- {% endfor %}
-
+
+ sub |
+ openid |
+ {{ request.user.username }} |
+
+ {% for claim, value in claims.items %}
+ {% for claimname, scope in scopes.items %}
+ {% if claimname == claim %}
+
+ {{ claim }} |
+ {{ scope }} |
+ {{ value }} |
+
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+
{% endif %}
diff --git a/src/profiles/templates/profile_base.html b/src/profiles/templates/profile_base.html
index 4d44076d3..57b9e81ed 100644
--- a/src/profiles/templates/profile_base.html
+++ b/src/profiles/templates/profile_base.html
@@ -94,7 +94,7 @@ Your BornHack Account
{% url 'profiles:oidc' as profile_oidc_url %}
- OIDC ScopeClaim
+ OIDC ScopeClaim
diff --git a/src/profiles/views.py b/src/profiles/views.py
index b8c0a6020..c3ea25017 100644
--- a/src/profiles/views.py
+++ b/src/profiles/views.py
@@ -122,7 +122,7 @@ def get_form(self, form_class=None):
if form_class is None:
form_class = self.get_form_class()
scopes = self.request.GET.getlist(key="scopes")
- self.initial['scopes'] = scopes
+ self.initial["scopes"] = scopes
return form_class(**self.get_form_kwargs())
def get_context_data(self, **kwargs):
@@ -133,7 +133,9 @@ def get_context_data(self, **kwargs):
if scope in self.request.GET.getlist(key="scopes"):
context["claims"][claim] = value
context["scopes"] = self.scopes
- context["active_scopes"] = ["openid"] + sorted(set(self.request.GET.getlist(key="scopes")))
+ context["active_scopes"] = ["openid"] + sorted(
+ set(self.request.GET.getlist(key="scopes")),
+ )
context["all_scopes"] = sorted(set(self.scopes.values()))
- del(context["all_scopes"][context["all_scopes"].index("openid")])
+ del context["all_scopes"][context["all_scopes"].index("openid")]
return context
diff --git a/src/program/forms.py b/src/program/forms.py
index 41bc86de1..6d4288953 100644
--- a/src/program/forms.py
+++ b/src/program/forms.py
@@ -268,6 +268,7 @@ class Meta:
"title",
"abstract",
"allow_video_recording",
+ "allow_video_streaming",
"duration",
"tags",
"slides_url",
@@ -313,8 +314,9 @@ def __init__(self, camp, event_type=None, matrix=None, *args, **kwargs):
self.fields["track"].empty_label = None
self.fields["track"].queryset = EventTrack.objects.filter(camp=camp)
- # make sure video_recording checkbox defaults to checked
+ # make sure video_recording and streaming checkbox defaults to checked
self.fields["allow_video_recording"].initial = True
+ self.fields["allow_video_streaming"].initial = True
if event_type.name not in [TALK, LIGHTNING_TALK]:
# 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):
# no video recording for music acts
del self.fields["allow_video_recording"]
+ del self.fields["allow_video_streaming"]
elif event_type.name == RECREATIONAL_EVENT:
# fix label and help_text for the title field
@@ -394,6 +397,7 @@ def __init__(self, camp, event_type=None, matrix=None, *args, **kwargs):
# no video recording for music acts
del self.fields["allow_video_recording"]
+ del self.fields["allow_video_streaming"]
elif event_type.name in [TALK, LIGHTNING_TALK]:
# fix label and help_text for the title field
@@ -439,6 +443,7 @@ def __init__(self, camp, event_type=None, matrix=None, *args, **kwargs):
# no video recording for workshops
del self.fields["allow_video_recording"]
+ del self.fields["allow_video_streaming"]
elif event_type.name == RECREATIONAL_EVENT:
# fix label and help_text for the title field
@@ -459,6 +464,7 @@ def __init__(self, camp, event_type=None, matrix=None, *args, **kwargs):
# no video recording for recreational events
del self.fields["allow_video_recording"]
+ del self.fields["allow_video_streaming"]
elif event_type.name == MEETUP:
# fix label and help_text for the title field
@@ -479,6 +485,7 @@ def __init__(self, camp, event_type=None, matrix=None, *args, **kwargs):
# no video recording for meetups
del self.fields["allow_video_recording"]
+ del self.fields["allow_video_streaming"]
else:
raise ImproperlyConfigured(
diff --git a/src/program/migrations/0106_event_video_streaming_and_more.py b/src/program/migrations/0106_event_video_streaming_and_more.py
new file mode 100644
index 000000000..ece5a648c
--- /dev/null
+++ b/src/program/migrations/0106_event_video_streaming_and_more.py
@@ -0,0 +1,34 @@
+# Generated by Django 4.2.20 on 2025-04-21 17:36
+
+from django.db import migrations, models
+
+def update_streaming(apps, schema_editor):
+ # We can't import the models directly as it may be a newer
+ # version than this migration expects. We use the historical version.
+ Event = apps.get_model("program", "Event")
+ EventProposal = apps.get_model("program", "EventProposal")
+
+ Event.objects.filter(video_recording=True).update(video_streaming=True)
+ Event.objects.filter(video_recording=False).update(video_streaming=False)
+ EventProposal.objects.filter(allow_video_recording=False).update(allow_video_streaming=False)
+ EventProposal.objects.filter(allow_video_recording=True).update(allow_video_streaming=True)
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('program', '0105_cascade_delete_event_urls'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='event',
+ name='video_streaming',
+ field=models.BooleanField(default=True, help_text='Do we intend to stream video of this event?'),
+ ),
+ migrations.AddField(
+ model_name='eventproposal',
+ name='allow_video_streaming',
+ field=models.BooleanField(default=False, help_text='Uncheck if you do not want the event live streamed.'),
+ ),
+ migrations.RunPython(update_streaming),
+ ]
diff --git a/src/program/models.py b/src/program/models.py
index 55b7883bd..d90c96258 100644
--- a/src/program/models.py
+++ b/src/program/models.py
@@ -544,6 +544,11 @@ class EventProposal(ExportModelOperationsMixin("event_proposal"), UserSubmittedM
help_text="Recordings are made available under the CC BY-SA 4.0 license. Uncheck if you do not want the event recorded, or if you cannot accept the license.",
)
+ allow_video_streaming = models.BooleanField(
+ default=False,
+ help_text="Uncheck if you do not want the event live streamed.",
+ )
+
duration = models.IntegerField(
blank=True,
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):
event.event_type = self.event_type
event.proposal = self
event.video_recording = self.allow_video_recording
+ event.video_streaming = self.allow_video_streaming
event.save()
# loop through the speaker_proposals linked to this event_proposal and associate any related speaker objects with this event
for sp in self.speakers.all():
@@ -1207,10 +1213,12 @@ def get_ics_event(self):
domain = Site.objects.get_current().domain
speakers = ", ".join(self.event.speakers.all().values_list("name", flat=True))
recorded = "Yes" if self.event.video_recording else "No"
+ streamed = "Yes" if self.event.video_streaming else "No"
ievent["description"] = (
f"URL: https://{domain}{self.event.get_absolute_url()}\n\n"
f"Speaker(s): {speakers}\n\n"
f"Recorded: {recorded}\n\n"
+ f"Streamed: {streamed}\n\n"
f"{self.event.abstract}"
)
ievent["dtstart"] = icalendar.vDatetime(self.when.lower).to_ical()
@@ -1308,6 +1316,11 @@ class Event(ExportModelOperationsMixin("event"), CampRelatedModel):
help_text="Do we intend to record video of this event?",
)
+ video_streaming = models.BooleanField(
+ default=True,
+ help_text="Do we intend to stream video of this event?",
+ )
+
proposal = models.OneToOneField(
"program.EventProposal",
null=True,
@@ -1378,10 +1391,14 @@ def serialize(self):
"event_type": self.event_type.name,
}
- if self.video_recording:
- video_state = "to-be-recorded"
+ if self.video_recording and self.video_streaming:
+ video_state = "to-be-streamed-to-be-recorded"
+ elif self.video_recording:
+ video_state = "to-be-recorded-not-to-be-streamed"
+ elif self.video_streaming:
+ video_state = "to-be-streamed-not-to-be-recorded"
else:
- video_state = "not-to-be-recorded"
+ video_state = "not-to-be-recorded-not-to-be-streamed"
data["video_state"] = video_state
@@ -1513,10 +1530,14 @@ def serialize(self, user=None):
"timeslots": self.timeslots,
}
- if self.event.video_recording:
- video_state = "to-be-recorded"
+ if self.event.video_recording and self.event.video_streaming:
+ video_state = "to-be-recorded-to-be-streamed"
+ elif self.event.video_recording:
+ video_state = "to-be-recorded-not-to-be-streamed"
+ elif self.event.video_streaming:
+ video_state = "to-be-streamed-not-to-be-recorded"
else:
- video_state = "not-to-be-recorded"
+ video_state = "not-to-be-recorded-not-to-be-streamed"
data["video_state"] = video_state
diff --git a/src/program/templates/event_detail.html b/src/program/templates/event_detail.html
index 634ffb7a3..65e17a14e 100644
--- a/src/program/templates/event_detail.html
+++ b/src/program/templates/event_detail.html
@@ -41,14 +41,25 @@
Metadata for {{ event.title }}
To be recorded:
-
{% if event.video_recording %}
-
+
{% else %}
+
{% endif %}
{{ event.video_recording|yesno:"Yes,No" }}
+
+ To be streamed:
+
+ {% if event.video_streaming %}
+
+ {% else %}
+
+
+ {% endif %}
+
+ {{ event.video_streaming|yesno:"Yes,No" }}
URLs for {{ event.title }}
diff --git a/src/program/templates/includes/event_list_table.html b/src/program/templates/includes/event_list_table.html
index 625821e20..946455553 100644
--- a/src/program/templates/includes/event_list_table.html
+++ b/src/program/templates/includes/event_list_table.html
@@ -8,7 +8,9 @@
Event Type |
Tags |
Speakers |
- |
+ |
+ |
+ |
Scheduled |
@@ -37,15 +39,17 @@
N/A
{% endfor %}
- {{ event.video_recording }}{{ event.video_recording|truefalseicon }} |
-
- {% for slot in event.event_slots.all %}
- {{ slot.event_location.icon_html }} {{ slot.event_location.name }} at {{ slot.when.lower }}
- {% empty %}
- Not scheduled yet
- {% endfor %}
- |
-
+ {% if event.video_recording or event.video_streaming %}True{{ True|truefalseicon }}{% else %}False{{ False|truefalseicon }}{% endif %} |
+ {{ event.video_recording }}{{ event.video_recording|truefalseicon }} |
+ {{ event.video_streaming }}{{ event.video_streaming|truefalseicon }} |
+
+ {% for slot in event.event_slots.all %}
+ {{ slot.event_location.icon_html }} {{ slot.event_location.name }} at {{ slot.when.lower }}
+ {% empty %}
+ Not scheduled yet
+ {% endfor %}
+ |
+
{% endif %}
{% endfor %}
diff --git a/src/utils/management/commands/bootstrap_devsite.py b/src/utils/management/commands/bootstrap_devsite.py
index 413435da1..501a2c08e 100644
--- a/src/utils/management/commands/bootstrap_devsite.py
+++ b/src/utils/management/commands/bootstrap_devsite.py
@@ -229,6 +229,7 @@ class Meta:
title = factory.Faker("sentence")
abstract = output_fake_md_description()
allow_video_recording = factory.Iterator([True, True, True, False])
+ allow_video_streaming = factory.Iterator([True, True, True, False])
submission_notes = factory.Iterator(["", output_fake_description()])
use_provided_speaker_laptop = factory.Iterator([True, False])