diff --git a/src/core/utils.py b/src/core/utils.py index caf4a4af95c030f7c4ff6e8f2085f11bd62f412c..8332304b94ac9ac29c4c68009ea67e151b540b36 100644 --- a/src/core/utils.py +++ b/src/core/utils.py @@ -92,7 +92,7 @@ def str2bool(s, allow_null: bool = False): s = s.strip().lower() - if allow_null and s in ['-', 'none', 'null', 'unknown', '?']: + if allow_null and s in ['', '-', 'none', 'null', 'unknown', '?']: return None if s in ['true', 'on', '1', 't', 'y', 'yes', 'j', 'ja', '✓', '✔']: diff --git a/src/plainui/jinja2/plainui/fahrplan.html b/src/plainui/jinja2/plainui/fahrplan.html index 92754660c5686b62f2ca4e85ceedf488da91180b..dcc20ed7d551dfeefb6ceda34bb7408b129ec2ae 100644 --- a/src/plainui/jinja2/plainui/fahrplan.html +++ b/src/plainui/jinja2/plainui/fahrplan.html @@ -58,9 +58,10 @@ {% if assembly %}<input type="hidden" name="assembly" value="{{assembly.slug}}">{% endif %} {% if track %}<input type="hidden" name="track" value="{{track.slug}}">{% endif %} {% if my_fahrplan %}<input type="hidden" name="my" value="y">{% endif %} + {% if is_recorded is not none %}<input type="hidden" name="rec" value="{{'y' if is_recorded else 'n'}}">{% endif %} + <div class="d-flex gap-3 flex-column flex-md-row align-items-start hub-fahrplan__title mb-2"> - <div class="me-md-4 fw-bold">{{ _("fahrplan.title") }}</div> <div> <button type="submit" @@ -97,6 +98,10 @@ {{ filter_button('d' ~ (n if n != day else ''), n == day, _("Day %(n)s", n=n + 1)) }} {%- endfor %} + <div class="hub-tag-divider"></div> + + {{ filter_button("ry" if is_recorded is not true else "r", is_recorded is true, _("recorded only")) }} + {{ filter_button("rn" if is_recorded is not false else "r", is_recorded is false, _("not recorded only")) }} </div> <div class="hub-tags"> diff --git a/src/plainui/views/fahrplan.py b/src/plainui/views/fahrplan.py index 377c28860052d543d4d8ecf0fcc4168a2d410034..990d6603a8a2623e0fe9aab0ec5991da3b846298 100644 --- a/src/plainui/views/fahrplan.py +++ b/src/plainui/views/fahrplan.py @@ -14,6 +14,7 @@ from core.models import ( ConferenceTrack, Event, ) +from core.utils import str2bool from plainui.views.utils import ConferenceRequiredMixin, event_filter, organize_events_for_calendar, session_get_favorite_events @@ -54,6 +55,7 @@ class FahrplanView(ConferenceRequiredMixin, TemplateView): show_assembly_filters = self.request.GET.get('show_assembly_filters') == 'y' show_track_filters = self.request.GET.get('show_track_filters') == 'y' my_fahrplan = self.request.GET.get('my') == 'y' + is_recorded = str2bool(self.request.GET.get('rec', ''), allow_null=True) to_set = self.request.GET.get('set', None) if to_set: @@ -79,6 +81,14 @@ class FahrplanView(ConferenceRequiredMixin, TemplateView): mode = to_set[1:] elif to_set[0] == 't': track = to_set[1:] + elif to_set[0] == 'r': + match to_set[1:]: + case '1' | 'y': + is_recorded = True + case '0' | 'n': + is_recorded = False + case _: + is_recorded = None context['show_day_filters'] = show_day_filters context['show_assembly_filters'] = show_assembly_filters @@ -130,6 +140,8 @@ class FahrplanView(ConferenceRequiredMixin, TemplateView): track = tracks.get(slug=track) if track else None context['track'] = track + context['is_recorded'] = is_recorded + events = event_filter( self.request.user, self.conf, @@ -139,6 +151,7 @@ class FahrplanView(ConferenceRequiredMixin, TemplateView): track=track, calendar_mode=mode == 'calendar', public_fahrplan=public_fahrplan, + is_recorded=is_recorded, **self.filter_opts, ) diff --git a/src/plainui/views/utils.py b/src/plainui/views/utils.py index 5ada095ab5c7392d96499514d8e874e53bbbf069..5362bec081aa03bf728da1b3d41775573cbfbf16 100644 --- a/src/plainui/views/utils.py +++ b/src/plainui/views/utils.py @@ -185,6 +185,7 @@ def event_filter( upcoming=False, calendar_mode=True, public_fahrplan=None, + is_recorded: Optional[bool] = None, ): min_date, max_date = conf.start, conf.end if min_date is None or max_date is None: @@ -215,9 +216,47 @@ def event_filter( if calendar_mode: events = events.filter(room__isnull=False) res = events.filter(schedule_duration__isnull=False, **filters).order_by('schedule_start', 'schedule_end') + + # filter on the requested recording state + if is_recorded is True: + # don't include all events explicitly labeled "don't record" + res = res.exclude(recording=Event.Recording.NO) + + res = res.filter( + # ... all events of unknown state in rooms with "record by default" + Q(recording=Event.Recording.UNKNOWN, room__isnull=False, room__recording_state__in=[Room.RecordingState.RECORD_BY_DEFAULT]) + # ... all events with state "yes, record" which either ... + | ( + Q(recording=Event.Recording.YES) + & ( + # don't have a room + Q(room__isnull=True) + # have a room which does allow recording + | Q(room__isnull=False, room__recording_state__in=[Room.RecordingState.NOT_RECORDED_BY_DEFAULT, Room.RecordingState.RECORD_BY_DEFAULT]) + ) + ) + ) + + elif is_recorded is False: + # kick all events which don't have a room and don't explicitly specify not to be recorded + res = res.exclude(room__isnull=True, recording__in=[Event.Recording.UNKNOWN, Event.Recording.YES]) + + # kick all events which ... + res = res.exclude( + # shall explicitly be recorded and ... + recording=Event.Recording.YES, + # have a room which permits recording (if an event may not be recorded because of this we can't filter it even though it requests to be recorded) + room__isnull=False, + room__recording_state__in=[Room.RecordingState.RECORD_BY_DEFAULT, Room.RecordingState.NOT_RECORDED_BY_DEFAULT], + ) + + # kick all events which default to their room's setting which would allow recording + res = res.exclude(recording=Event.Recording.UNKNOWN, room__isnull=False, room__recording_state__in=[Room.RecordingState.RECORD_BY_DEFAULT]) + res = res.annotate(track_name=F('track__name')) speakers = EventParticipant.objects.filter(is_public=True, role=EventParticipant.Role.SPEAKER).order_by('participant__username') speakers = speakers.annotate(speaker_name=F('participant__username')) + return res.prefetch_related( Prefetch('participants', queryset=speakers, to_attr='speakers'), Prefetch('tags', to_attr='prefetched_tags', queryset=TagItem.objects.select_related('tag').filter(tag__is_public=True)),