feat: Add session recordings (#8218)
* feat: add session recordings * feat: add session recordings * feat: deleting recordings * feat: deleting recordings and initial form values * feat: use meeting date rather than today for initial title field. Fix delete recording * feat: confirm delete recordings modal. fix server utils delete recording * fix: removing debug console.log * feat: change button name from 'Ok' to 'Delete' for confirm deletion to be clearer * feat: UTC time in string and delete modal text * fix: django html validation tests * fix: django html validation tests * fix: django html validation tests * refactor: Work with SessionPresentations * fix: better ordering * chore: drop rev, hide table when empty * test: test delete_recordings method * fix: debug delete_recordings * test: test add_session_recordings view * fix: better permissions handling * fix: only delete recordings for selected session * refactor: inline script -> js module * chore: remove accidental import *shakes fist at pycharm* * fix: consistent timestamp format plus slight rephrase * style: Black * chore: remove comment * test: update test to match * fix: reversible url pattern for materials Tests were perturbed in a way that led to a test getting an interim instead of an IETF meeting. This exposed a bug reversing the URL for the materials_document() view. This splits it into two patterns that are equivalent to the original. --------- Co-authored-by: Jennifer Richards <jennifer@staff.ietf.org>
This commit is contained in:
parent
02a680f872
commit
db7d3074da
|
@ -48,7 +48,7 @@ from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignm
|
||||||
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting, make_interim_test_data
|
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting, make_interim_test_data
|
||||||
from ietf.meeting.utils import condition_slide_order
|
from ietf.meeting.utils import condition_slide_order
|
||||||
from ietf.meeting.utils import add_event_info_to_session_qs, participants_for_meeting
|
from ietf.meeting.utils import add_event_info_to_session_qs, participants_for_meeting
|
||||||
from ietf.meeting.utils import create_recording, get_next_sequence, bluesheet_data
|
from ietf.meeting.utils import create_recording, delete_recording, get_next_sequence, bluesheet_data
|
||||||
from ietf.meeting.views import session_draft_list, parse_agenda_filter_params, sessions_post_save, agenda_extract_schedule
|
from ietf.meeting.views import session_draft_list, parse_agenda_filter_params, sessions_post_save, agenda_extract_schedule
|
||||||
from ietf.meeting.views import get_summary_by_area, get_summary_by_type, get_summary_by_purpose, generate_agenda_data
|
from ietf.meeting.views import get_summary_by_area, get_summary_by_type, get_summary_by_purpose, generate_agenda_data
|
||||||
from ietf.name.models import SessionStatusName, ImportantDateName, RoleName, ProceedingsMaterialTypeName
|
from ietf.name.models import SessionStatusName, ImportantDateName, RoleName, ProceedingsMaterialTypeName
|
||||||
|
@ -441,6 +441,48 @@ class MeetingTests(BaseMeetingTestCase):
|
||||||
self.assertIn(new_recording_title, links[0].text_content())
|
self.assertIn(new_recording_title, links[0].text_content())
|
||||||
#debug.show("q(f'#notes_and_recordings_{session_pk}')")
|
#debug.show("q(f'#notes_and_recordings_{session_pk}')")
|
||||||
|
|
||||||
|
def test_delete_recordings(self):
|
||||||
|
# No user specified, active recording state
|
||||||
|
sp = SessionPresentationFactory(
|
||||||
|
document__type_id="recording",
|
||||||
|
document__external_url="https://example.com/some-recording",
|
||||||
|
document__states=[("recording", "active")],
|
||||||
|
)
|
||||||
|
doc = sp.document
|
||||||
|
doc.docevent_set.all().delete() # clear this out
|
||||||
|
delete_recording(sp)
|
||||||
|
self.assertFalse(SessionPresentation.objects.filter(pk=sp.pk).exists())
|
||||||
|
self.assertEqual(doc.get_state("recording").slug, "deleted", "recording state updated")
|
||||||
|
self.assertEqual(doc.docevent_set.count(), 1, "one event added")
|
||||||
|
event = doc.docevent_set.first()
|
||||||
|
self.assertEqual(event.type, "changed_state", "event is a changed_state event")
|
||||||
|
self.assertEqual(event.by.name, "(System)", "system user is responsible")
|
||||||
|
|
||||||
|
# Specified user, no recording state
|
||||||
|
sp = SessionPresentationFactory(
|
||||||
|
document__type_id="recording",
|
||||||
|
document__external_url="https://example.com/some-recording",
|
||||||
|
document__states=[],
|
||||||
|
)
|
||||||
|
doc = sp.document
|
||||||
|
doc.docevent_set.all().delete() # clear this out
|
||||||
|
user = PersonFactory() # naming matches the methods - user is a Person, not a User
|
||||||
|
delete_recording(sp, user=user)
|
||||||
|
self.assertFalse(SessionPresentation.objects.filter(pk=sp.pk).exists())
|
||||||
|
self.assertEqual(doc.get_state("recording").slug, "deleted", "recording state updated")
|
||||||
|
self.assertEqual(doc.docevent_set.count(), 1, "one event added")
|
||||||
|
event = doc.docevent_set.first()
|
||||||
|
self.assertEqual(event.type, "changed_state", "event is a changed_state event")
|
||||||
|
self.assertEqual(event.by, user, "user is responsible")
|
||||||
|
|
||||||
|
# Document is not a recording
|
||||||
|
sp = SessionPresentationFactory(
|
||||||
|
document__type_id="draft",
|
||||||
|
document__external_url="https://example.com/some-recording",
|
||||||
|
)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
delete_recording(sp)
|
||||||
|
|
||||||
def test_agenda_ical_next_meeting_type(self):
|
def test_agenda_ical_next_meeting_type(self):
|
||||||
# start with no upcoming IETF meetings, just an interim
|
# start with no upcoming IETF meetings, just an interim
|
||||||
MeetingFactory(
|
MeetingFactory(
|
||||||
|
@ -7363,6 +7405,118 @@ class SessionTests(TestCase):
|
||||||
self.assertEqual(r.status_code,302)
|
self.assertEqual(r.status_code,302)
|
||||||
self.assertEqual(len(outbox),1)
|
self.assertEqual(len(outbox),1)
|
||||||
|
|
||||||
|
@override_settings(YOUTUBE_DOMAINS=["youtube.com"])
|
||||||
|
def test_add_session_recordings(self):
|
||||||
|
session = SessionFactory(meeting__type_id="ietf")
|
||||||
|
url = urlreverse(
|
||||||
|
"ietf.meeting.views.add_session_recordings",
|
||||||
|
kwargs={"session_id": session.pk, "num": session.meeting.number},
|
||||||
|
)
|
||||||
|
# does not fully validate authorization for non-secretariat users :-(
|
||||||
|
login_testing_unauthorized(self, "secretary", url)
|
||||||
|
r = self.client.get(url)
|
||||||
|
pq = PyQuery(r.content)
|
||||||
|
title_input = pq("input#id_title")
|
||||||
|
self.assertIsNotNone(title_input)
|
||||||
|
self.assertEqual(
|
||||||
|
title_input.attr.value,
|
||||||
|
"Video recording of {acro} for {timestamp}".format(
|
||||||
|
acro=session.group.acronym,
|
||||||
|
timestamp=session.official_timeslotassignment().timeslot.utc_start_time().strftime(
|
||||||
|
"%Y-%m-%d %H:%M"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("ietf.meeting.views.create_recording") as mock_create:
|
||||||
|
r = self.client.post(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
"title": "This is my video title",
|
||||||
|
"url": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertFalse(mock_create.called)
|
||||||
|
|
||||||
|
with patch("ietf.meeting.views.create_recording") as mock_create:
|
||||||
|
r = self.client.post(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
"title": "This is my video title",
|
||||||
|
"url": "https://yubtub.com/this-is-not-a-youtube-video",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertFalse(mock_create.called)
|
||||||
|
|
||||||
|
with patch("ietf.meeting.views.create_recording") as mock_create:
|
||||||
|
r = self.client.post(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
"title": "This is my video title",
|
||||||
|
"url": "https://youtube.com/finally-a-video",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertTrue(mock_create.called)
|
||||||
|
self.assertEqual(
|
||||||
|
mock_create.call_args,
|
||||||
|
call(
|
||||||
|
session,
|
||||||
|
"https://youtube.com/finally-a-video",
|
||||||
|
title="This is my video title",
|
||||||
|
user=Person.objects.get(user__username="secretary"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# CAN delete session presentation for this session
|
||||||
|
sp = SessionPresentationFactory(
|
||||||
|
session=session,
|
||||||
|
document__type_id="recording",
|
||||||
|
document__external_url="https://example.com/some-video",
|
||||||
|
)
|
||||||
|
with patch("ietf.meeting.views.delete_recording") as mock_delete:
|
||||||
|
r = self.client.post(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
"delete": str(sp.pk),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
self.assertTrue(mock_delete.called)
|
||||||
|
self.assertEqual(mock_delete.call_args, call(sp))
|
||||||
|
|
||||||
|
# ValueError message from delete_recording does not reach the user
|
||||||
|
sp = SessionPresentationFactory(
|
||||||
|
session=session,
|
||||||
|
document__type_id="recording",
|
||||||
|
document__external_url="https://example.com/some-video",
|
||||||
|
)
|
||||||
|
with patch("ietf.meeting.views.delete_recording", side_effect=ValueError("oh joy!")) as mock_delete:
|
||||||
|
r = self.client.post(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
"delete": str(sp.pk),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertTrue(mock_delete.called)
|
||||||
|
self.assertNotContains(r, "oh joy!", status_code=200)
|
||||||
|
|
||||||
|
# CANNOT delete session presentation for a different session
|
||||||
|
sp_for_other_session = SessionPresentationFactory(
|
||||||
|
document__type_id="recording",
|
||||||
|
document__external_url="https://example.com/some-other-video",
|
||||||
|
)
|
||||||
|
with patch("ietf.meeting.views.delete_recording") as mock_delete:
|
||||||
|
r = self.client.post(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
"delete": str(sp_for_other_session.pk),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(r.status_code, 404)
|
||||||
|
self.assertFalse(mock_delete.called)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HasMeetingsTests(TestCase):
|
class HasMeetingsTests(TestCase):
|
||||||
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH']
|
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH']
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ class AgendaRedirectView(RedirectView):
|
||||||
safe_for_all_meeting_types = [
|
safe_for_all_meeting_types = [
|
||||||
url(r'^session/(?P<acronym>[-a-z0-9]+)/?$', views.session_details),
|
url(r'^session/(?P<acronym>[-a-z0-9]+)/?$', views.session_details),
|
||||||
url(r'^session/(?P<session_id>\d+)/drafts$', views.add_session_drafts),
|
url(r'^session/(?P<session_id>\d+)/drafts$', views.add_session_drafts),
|
||||||
|
url(r'^session/(?P<session_id>\d+)/recordings$', views.add_session_recordings),
|
||||||
url(r'^session/(?P<session_id>\d+)/attendance$', views.session_attendance),
|
url(r'^session/(?P<session_id>\d+)/attendance$', views.session_attendance),
|
||||||
url(r'^session/(?P<session_id>\d+)/bluesheets$', views.upload_session_bluesheets),
|
url(r'^session/(?P<session_id>\d+)/bluesheets$', views.upload_session_bluesheets),
|
||||||
url(r'^session/(?P<session_id>\d+)/minutes$', views.upload_session_minutes),
|
url(r'^session/(?P<session_id>\d+)/minutes$', views.upload_session_minutes),
|
||||||
|
@ -63,7 +64,8 @@ type_ietf_only_patterns = [
|
||||||
type_interim_patterns = [
|
type_interim_patterns = [
|
||||||
url(r'^agenda/(?P<acronym>[A-Za-z0-9-]+)-drafts.pdf$', views.session_draft_pdf),
|
url(r'^agenda/(?P<acronym>[A-Za-z0-9-]+)-drafts.pdf$', views.session_draft_pdf),
|
||||||
url(r'^agenda/(?P<acronym>[A-Za-z0-9-]+)-drafts.tgz$', views.session_draft_tarfile),
|
url(r'^agenda/(?P<acronym>[A-Za-z0-9-]+)-drafts.tgz$', views.session_draft_tarfile),
|
||||||
url(r'^materials/%(document)s((?P<ext>\.[a-z0-9]+)|/)?$' % settings.URL_REGEXPS, views.materials_document),
|
url(r'^materials/%(document)s(?P<ext>\.[a-z0-9]+)$' % settings.URL_REGEXPS, views.materials_document),
|
||||||
|
url(r'^materials/%(document)s/?$' % settings.URL_REGEXPS, views.materials_document),
|
||||||
url(r'^agenda.json$', views.agenda_json)
|
url(r'^agenda.json$', views.agenda_json)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ import debug # pyflakes:ignore
|
||||||
from ietf.dbtemplate.models import DBTemplate
|
from ietf.dbtemplate.models import DBTemplate
|
||||||
from ietf.meeting.models import (Session, SchedulingEvent, TimeSlot,
|
from ietf.meeting.models import (Session, SchedulingEvent, TimeSlot,
|
||||||
Constraint, SchedTimeSessAssignment, SessionPresentation, Attended)
|
Constraint, SchedTimeSessAssignment, SessionPresentation, Attended)
|
||||||
from ietf.doc.models import Document, State, NewRevisionDocEvent
|
from ietf.doc.models import Document, State, NewRevisionDocEvent, StateDocEvent
|
||||||
from ietf.doc.models import DocEvent
|
from ietf.doc.models import DocEvent
|
||||||
from ietf.group.models import Group
|
from ietf.group.models import Group
|
||||||
from ietf.group.utils import can_manage_materials
|
from ietf.group.utils import can_manage_materials
|
||||||
|
@ -853,6 +853,26 @@ def create_recording(session, url, title=None, user=None):
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
def delete_recording(session_presentation, user=None):
|
||||||
|
"""Delete a session recording"""
|
||||||
|
document = session_presentation.document
|
||||||
|
if document.type_id != "recording":
|
||||||
|
raise ValueError(f"Document {document.pk} is not a recording (type_id={document.type_id})")
|
||||||
|
recording_state = document.get_state("recording")
|
||||||
|
deleted_state = State.objects.get(type_id="recording", slug="deleted")
|
||||||
|
if recording_state != deleted_state:
|
||||||
|
# Update the recording state and create a history event
|
||||||
|
document.set_state(deleted_state)
|
||||||
|
StateDocEvent.objects.create(
|
||||||
|
type="changed_state",
|
||||||
|
by=user or Person.objects.get(name="(System)"),
|
||||||
|
doc=document,
|
||||||
|
rev=document.rev,
|
||||||
|
state_type=deleted_state.type,
|
||||||
|
state=deleted_state,
|
||||||
|
)
|
||||||
|
session_presentation.delete()
|
||||||
|
|
||||||
def get_next_sequence(group, meeting, type):
|
def get_next_sequence(group, meeting, type):
|
||||||
'''
|
'''
|
||||||
Returns the next sequence number to use for a document of type = type.
|
Returns the next sequence number to use for a document of type = type.
|
||||||
|
|
|
@ -20,7 +20,7 @@ from collections import OrderedDict, Counter, deque, defaultdict, namedtuple
|
||||||
from functools import partialmethod
|
from functools import partialmethod
|
||||||
import jsonschema
|
import jsonschema
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit
|
from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit, urlparse
|
||||||
from tempfile import mkstemp
|
from tempfile import mkstemp
|
||||||
from wsgiref.handlers import format_date_time
|
from wsgiref.handlers import format_date_time
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ from ietf.meeting.utils import diff_meeting_schedules, prefetch_schedule_diff_ob
|
||||||
from ietf.meeting.utils import swap_meeting_schedule_timeslot_assignments, bulk_create_timeslots
|
from ietf.meeting.utils import swap_meeting_schedule_timeslot_assignments, bulk_create_timeslots
|
||||||
from ietf.meeting.utils import preprocess_meeting_important_dates
|
from ietf.meeting.utils import preprocess_meeting_important_dates
|
||||||
from ietf.meeting.utils import new_doc_for_session, write_doc_for_session
|
from ietf.meeting.utils import new_doc_for_session, write_doc_for_session
|
||||||
from ietf.meeting.utils import get_activity_stats, post_process, create_recording
|
from ietf.meeting.utils import get_activity_stats, post_process, create_recording, delete_recording
|
||||||
from ietf.meeting.utils import participants_for_meeting, generate_bluesheet, bluesheet_data, save_bluesheet
|
from ietf.meeting.utils import participants_for_meeting, generate_bluesheet, bluesheet_data, save_bluesheet
|
||||||
from ietf.message.utils import infer_message
|
from ietf.message.utils import infer_message
|
||||||
from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName
|
from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName
|
||||||
|
@ -103,6 +103,7 @@ from ietf.utils.pdf import pdf_pages
|
||||||
from ietf.utils.response import permission_denied
|
from ietf.utils.response import permission_denied
|
||||||
from ietf.utils.text import xslugify
|
from ietf.utils.text import xslugify
|
||||||
from ietf.utils.timezone import datetime_today, date_today
|
from ietf.utils.timezone import datetime_today, date_today
|
||||||
|
from ietf.settings import YOUTUBE_DOMAINS
|
||||||
|
|
||||||
from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm,
|
from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm,
|
||||||
InterimCancelForm, InterimSessionInlineFormSet, RequestMinutesForm,
|
InterimCancelForm, InterimSessionInlineFormSet, RequestMinutesForm,
|
||||||
|
@ -2568,6 +2569,89 @@ def add_session_drafts(request, session_id, num):
|
||||||
'form': form,
|
'form': form,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
class SessionRecordingsForm(forms.Form):
|
||||||
|
title = forms.CharField(max_length=255)
|
||||||
|
url = forms.URLField(label="URL of the recording (YouTube only)")
|
||||||
|
|
||||||
|
def clean_url(self):
|
||||||
|
url = self.cleaned_data['url']
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
if parsed_url.hostname not in YOUTUBE_DOMAINS:
|
||||||
|
raise forms.ValidationError("Must be a YouTube URL")
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def add_session_recordings(request, session_id, num):
|
||||||
|
# num is redundant, but we're dragging it along an artifact of where we are in the current URL structure
|
||||||
|
session = get_object_or_404(Session, pk=session_id)
|
||||||
|
if not session.can_manage_materials(request.user):
|
||||||
|
permission_denied(
|
||||||
|
request, "You don't have permission to manage recordings for this session."
|
||||||
|
)
|
||||||
|
if session.is_material_submission_cutoff() and not has_role(
|
||||||
|
request.user, "Secretariat"
|
||||||
|
):
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
session_number = None
|
||||||
|
official_timeslotassignment = session.official_timeslotassignment()
|
||||||
|
assertion("official_timeslotassignment is not None")
|
||||||
|
initial = {
|
||||||
|
"title": "Video recording of {acronym} for {timestamp}".format(
|
||||||
|
acronym=session.group.acronym,
|
||||||
|
timestamp=official_timeslotassignment.timeslot.utc_start_time().strftime(
|
||||||
|
"%Y-%m-%d %H:%M"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# find session number if WG has more than one session at the meeting
|
||||||
|
sessions = get_sessions(session.meeting.number, session.group.acronym)
|
||||||
|
if len(sessions) > 1:
|
||||||
|
session_number = 1 + sessions.index(session)
|
||||||
|
|
||||||
|
presentations = session.presentations.filter(
|
||||||
|
document__in=session.get_material("recording", only_one=False),
|
||||||
|
).order_by("document__title", "document__external_url")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
pk_to_delete = request.POST.get("delete", None)
|
||||||
|
if pk_to_delete is not None:
|
||||||
|
session_presentation = get_object_or_404(presentations, pk=pk_to_delete)
|
||||||
|
try:
|
||||||
|
delete_recording(session_presentation)
|
||||||
|
except ValueError as err:
|
||||||
|
log(f"Error deleting recording from session {session.pk}: {err}")
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"Unable to delete this recording. Please contact the secretariat for assistance.",
|
||||||
|
)
|
||||||
|
form = SessionRecordingsForm(initial=initial)
|
||||||
|
else:
|
||||||
|
form = SessionRecordingsForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
title = form.cleaned_data["title"]
|
||||||
|
url = form.cleaned_data["url"]
|
||||||
|
create_recording(session, url, title=title, user=request.user.person)
|
||||||
|
return redirect(
|
||||||
|
"ietf.meeting.views.session_details",
|
||||||
|
num=session.meeting.number,
|
||||||
|
acronym=session.group.acronym,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
form = SessionRecordingsForm(initial=initial)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"meeting/add_session_recordings.html",
|
||||||
|
{
|
||||||
|
"session": session,
|
||||||
|
"session_number": session_number,
|
||||||
|
"already_linked": presentations,
|
||||||
|
"form": form,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def session_attendance(request, session_id, num):
|
def session_attendance(request, session_id, num):
|
||||||
"""Session attendance view
|
"""Session attendance view
|
||||||
|
|
|
@ -1397,3 +1397,6 @@ if SERVER_MODE != 'production':
|
||||||
CSRF_TRUSTED_ORIGINS += ['http://localhost:8000', 'http://127.0.0.1:8000', 'http://[::1]:8000']
|
CSRF_TRUSTED_ORIGINS += ['http://localhost:8000', 'http://127.0.0.1:8000', 'http://[::1]:8000']
|
||||||
SESSION_COOKIE_SECURE = False
|
SESSION_COOKIE_SECURE = False
|
||||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||||
|
|
||||||
|
|
||||||
|
YOUTUBE_DOMAINS = ['www.youtube.com', 'youtube.com', 'youtu.be', 'm.youtube.com', 'youtube-nocookie.com', 'www.youtube-nocookie.com']
|
||||||
|
|
30
ietf/static/js/add_session_recordings.js
Normal file
30
ietf/static/js/add_session_recordings.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// Copyright The IETF Trust 2024-2025, All Rights Reserved
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const form = document.getElementById('delete_recordings_form')
|
||||||
|
const dialog = document.getElementById('delete_confirm_dialog')
|
||||||
|
const dialog_link = document.getElementById('delete_confirm_link')
|
||||||
|
const dialog_submit = document.getElementById('delete_confirm_submit')
|
||||||
|
const dialog_cancel = document.getElementById('delete_confirm_cancel')
|
||||||
|
|
||||||
|
dialog.style.maxWidth = '30vw'
|
||||||
|
|
||||||
|
form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
dialog_submit.value = e.submitter.value
|
||||||
|
const recording_link = e.submitter.closest('tr').querySelector('a')
|
||||||
|
dialog_link.setAttribute('href', recording_link.getAttribute('href'))
|
||||||
|
dialog_link.textContent = recording_link.textContent
|
||||||
|
dialog.showModal()
|
||||||
|
})
|
||||||
|
|
||||||
|
dialog_cancel.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
dialog.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (dialog.open && e.key === 'Escape') {
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
78
ietf/templates/meeting/add_session_recordings.html
Normal file
78
ietf/templates/meeting/add_session_recordings.html
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||||
|
{% load origin static django_bootstrap5 %}
|
||||||
|
{% block title %}Add I-Ds to {{ session.meeting }} : {{ session.group.acronym }}{% endblock %}
|
||||||
|
{% block pagehead %}{{ form.media.css }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
{% origin %}
|
||||||
|
<h1>
|
||||||
|
Add Recordings to {{ session.meeting }}
|
||||||
|
{% if session_number %}: Session {{ session_number }}{% endif %}
|
||||||
|
<br>
|
||||||
|
<small class="text-body-secondary">{{ session.group.acronym }}
|
||||||
|
{% if session.name %}: {{ session.name }}{% endif %}
|
||||||
|
</small>
|
||||||
|
</h1>
|
||||||
|
{% if session.is_material_submission_cutoff %}
|
||||||
|
<div class="alert alert-warning my-3">
|
||||||
|
The deadline for submission corrections has passed. This may affect published proceedings.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if already_linked|length > 0 %}
|
||||||
|
<h2 class="mt-5">Recordings already linked to this session</h2>
|
||||||
|
<form method="post" id="delete_recordings_form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<table class="table table-sm table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Title</th>
|
||||||
|
<th scope="col">URL</th>
|
||||||
|
<th scope="col">Delete</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for sp in already_linked %}{% with recording_doc=sp.document %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ recording_doc.title }}</td>
|
||||||
|
<td><a href="{{ recording_doc.external_url }}">{{ recording_doc.external_url }}</a></td>
|
||||||
|
<td>
|
||||||
|
<button type="submit"
|
||||||
|
aria-label="Delete {{ recording_doc.title }}"
|
||||||
|
class="btn btn-danger"
|
||||||
|
name="delete"
|
||||||
|
value="{{ sp.pk }}">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endwith %}{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
<dialog id="delete_confirm_dialog">
|
||||||
|
<p>Really delete the link to <a href="#" id="delete_confirm_link">(default)</a>?</p>
|
||||||
|
<form method="post" class="d-flex justify-content-between">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="btn btn-secondary" type="button" id="delete_confirm_cancel">Cancel</button>
|
||||||
|
<button class="btn btn-danger" type="submit" name="delete" id="delete_confirm_submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
<h2 class="mt-5">Add a recording to this session</h2>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
<button class="btn btn-{% if session.is_material_submission_cutoff %}warning{% else %}primary{% endif %}"
|
||||||
|
type="submit">
|
||||||
|
Add recording
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-secondary float-end"
|
||||||
|
href="{% url 'ietf.meeting.views.session_details' num=session.meeting.number acronym=session.group.acronym %}">
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
{% block js %}
|
||||||
|
{{ form.media.js }}
|
||||||
|
<script src="{% static 'ietf/js/add_session_recordings.js' %}"></script>
|
||||||
|
{% endblock %}
|
|
@ -368,5 +368,13 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if can_manage_materials %}
|
||||||
|
<a class="btn btn-primary"
|
||||||
|
href="{% url 'ietf.meeting.views.add_session_recordings' session_id=session.pk num=session.meeting.number %}">
|
||||||
|
Link additional recordings to session
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endwith %}{% endwith %}
|
{% endwith %}{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -111,6 +111,7 @@
|
||||||
"ietf/static/images/irtf-logo-card.png",
|
"ietf/static/images/irtf-logo-card.png",
|
||||||
"ietf/static/images/irtf-logo-white.svg",
|
"ietf/static/images/irtf-logo-white.svg",
|
||||||
"ietf/static/images/irtf-logo.svg",
|
"ietf/static/images/irtf-logo.svg",
|
||||||
|
"ietf/static/js/add_session_recordings.js",
|
||||||
"ietf/static/js/agenda_filter.js",
|
"ietf/static/js/agenda_filter.js",
|
||||||
"ietf/static/js/agenda_materials.js",
|
"ietf/static/js/agenda_materials.js",
|
||||||
"ietf/static/js/complete-review.js",
|
"ietf/static/js/complete-review.js",
|
||||||
|
|
Loading…
Reference in a new issue