refactor: Tie Meetecho resources to Session pk (#5281)

* feat: Use session.id to specify session for api_set_session_video_url

* feat: Use session.id to specify session for api_upload_bluesheet

* refactor: Add audio/video stream and onsite tool URLs to Session model

* refactor: Get onsite tool/stream URLs for agenda from Session

* refactor: Use Session methods for onsite tool/stream a few more places

* refactor: Move hard-coded meetecho URLs into settings.py

* feat: Add has_onsite_flag to Session

* chore: Set has_onsite_tool for sessions that had meetecho UrlResources

* fix: Only show onsite tool URLs when Session.has_onsite_tool is True

* test: Update test_api_upload_bluesheet to test deprecated version

* fix: Fix test failure in api_upload_bluesheet view

* test: Add test of new api_upload_bluesheet view

* style: Apply Black style to test_api_upload_bluesheet

* fix: Fix test failures in api_upload_bluesheet()

* test: Update test_api_set_session_video_url to test deprecated version

* fix: Fix test failure in api_set_session_video_url view

* test: Add test of new api_set_session_video_url view

* style: Apply Black styling to new test

* fix: Fix test failures in api_set_session_video_url view

* test: Fix test_meeting_agenda; set has_onsite_tool in SessionFactory

* feat: Add has_onsite_tool to Session list in admin

* feat: Add has_onsite_tool flag to SessionDetailsForm

* feat: Add has_onsite_tool flag to sreq

* feat: Show has_onsite_tool flag on secr view for a submitted request

* feat: Only prompt for has_onsite_tool in sreq for non-wg type groups

* fix: Clean up styling of sreq view a bit

* chore: Renumber migrations
This commit is contained in:
Jennifer Richards 2023-04-23 19:15:01 -04:00 committed by GitHub
parent 8c98abb537
commit 09ff9c6ced
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 488 additions and 66 deletions

View file

@ -55,7 +55,7 @@ class CustomApiTests(TestCase):
r = self.client.get(url)
self.assertContains(r, 'OpenID Connect Issuer', status_code=200)
def test_api_set_session_video_url(self):
def test_deprecated_api_set_session_video_url(self):
url = urlreverse('ietf.meeting.views.api_set_session_video_url')
recmanrole = RoleFactory(group__type_id='ietf', name_id='recman')
recman = recmanrole.person
@ -84,7 +84,7 @@ class CustomApiTests(TestCase):
r = self.client.get(url, {'apikey': apikey.hash()} )
self.assertContains(r, "Method not allowed", status_code=405)
r = self.client.post(url, {'apikey': apikey.hash()} )
r = self.client.post(url, {'apikey': apikey.hash(), 'group': group.acronym} )
self.assertContains(r, "Missing meeting parameter", status_code=400)
@ -136,6 +136,83 @@ class CustomApiTests(TestCase):
event = doc.latest_event()
self.assertEqual(event.by, recman)
def test_api_set_session_video_url(self):
url = urlreverse("ietf.meeting.views.api_set_session_video_url")
recmanrole = RoleFactory(group__type_id="ietf", name_id="recman")
recman = recmanrole.person
meeting = MeetingFactory(type_id="ietf")
session = SessionFactory(group__type_id="wg", meeting=meeting)
apikey = PersonalApiKey.objects.create(endpoint=url, person=recman)
video = "https://foo.example.com/bar/beer/"
# error cases
r = self.client.post(url, {})
self.assertContains(r, "Missing apikey parameter", status_code=400)
badrole = RoleFactory(group__type_id="ietf", name_id="ad")
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
badrole.person.user.last_login = timezone.now()
badrole.person.user.save()
r = self.client.post(url, {"apikey": badapikey.hash()})
self.assertContains(r, "Restricted to role: Recording Manager", status_code=403)
r = self.client.post(url, {"apikey": apikey.hash()})
self.assertContains(r, "Too long since last regular login", status_code=400)
recman.user.last_login = timezone.now()
recman.user.save()
r = self.client.get(url, {"apikey": apikey.hash()})
self.assertContains(r, "Method not allowed", status_code=405)
r = self.client.post(url, {"apikey": apikey.hash()})
self.assertContains(r, "Missing session_id parameter", status_code=400)
r = self.client.post(url, {"apikey": apikey.hash(), "session_id": session.pk})
self.assertContains(r, "Missing url parameter", status_code=400)
bad_pk = int(Session.objects.order_by("-pk").first().pk) + 1
r = self.client.post(
url,
{
"apikey": apikey.hash(),
"session_id": bad_pk,
"url": video,
},
)
self.assertContains(r, "Session not found", status_code=400)
r = self.client.post(
url,
{
"apikey": apikey.hash(),
"session_id": "foo",
"url": video,
},
)
self.assertContains(r, "Invalid session_id", status_code=400)
r = self.client.post(
url,
{
"apikey": apikey.hash(),
"session_id": session.pk,
"url": "foobar",
},
)
self.assertContains(r, "Invalid url value: 'foobar'", status_code=400)
r = self.client.post(
url, {"apikey": apikey.hash(), "session_id": session.pk, "url": video}
)
self.assertContains(r, "Done", status_code=200)
recordings = session.recordings()
self.assertEqual(len(recordings), 1)
doc = recordings[0]
self.assertEqual(doc.external_url, video)
event = doc.latest_event()
self.assertEqual(event.by, recman)
def test_api_add_session_attendees(self):
url = urlreverse('ietf.meeting.views.api_add_session_attendees')
otherperson = PersonFactory()
@ -289,7 +366,7 @@ class CustomApiTests(TestCase):
newdoccontent = get_unicode_document_content(newdoc.name, Path(session.meeting.get_materials_path()) / type_id / newdoc.uploaded_filename)
self.assertEqual(json.loads(content), json.loads(newdoccontent))
def test_api_upload_bluesheet(self):
def test_deprecated_api_upload_bluesheet(self):
url = urlreverse('ietf.meeting.views.api_upload_bluesheet')
recmanrole = RoleFactory(group__type_id='ietf', name_id='recman')
recman = recmanrole.person
@ -297,12 +374,12 @@ class CustomApiTests(TestCase):
session = SessionFactory(group__type_id='wg', meeting=meeting)
group = session.group
apikey = PersonalApiKey.objects.create(endpoint=url, person=recman)
people = [
{"name":"Andrea Andreotti", "affiliation": "Azienda"},
{"name":"Bosse Bernadotte", "affiliation": "Bolag"},
{"name":"Charles Charlemagne", "affiliation": "Compagnie"},
]
{"name": "Andrea Andreotti", "affiliation": "Azienda"},
{"name": "Bosse Bernadotte", "affiliation": "Bolag"},
{"name": "Charles Charlemagne", "affiliation": "Compagnie"},
]
for i in range(3):
faker = random_faker()
people.append(dict(name=faker.name(), affiliation=faker.company()))
@ -312,63 +389,63 @@ class CustomApiTests(TestCase):
r = self.client.post(url, {})
self.assertContains(r, "Missing apikey parameter", status_code=400)
badrole = RoleFactory(group__type_id='ietf', name_id='ad')
badrole = RoleFactory(group__type_id='ietf', name_id='ad')
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
badrole.person.user.last_login = timezone.now()
badrole.person.user.save()
r = self.client.post(url, {'apikey': badapikey.hash()} )
r = self.client.post(url, {'apikey': badapikey.hash()})
self.assertContains(r, "Restricted to roles: Recording Manager, Secretariat", status_code=403)
r = self.client.post(url, {'apikey': apikey.hash()} )
r = self.client.post(url, {'apikey': apikey.hash()})
self.assertContains(r, "Too long since last regular login", status_code=400)
recman.user.last_login = timezone.now()
recman.user.save()
r = self.client.get(url, {'apikey': apikey.hash()} )
r = self.client.get(url, {'apikey': apikey.hash()})
self.assertContains(r, "Method not allowed", status_code=405)
r = self.client.post(url, {'apikey': apikey.hash()} )
r = self.client.post(url, {'apikey': apikey.hash(), 'group': group.acronym})
self.assertContains(r, "Missing meeting parameter", status_code=400)
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, } )
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, })
self.assertContains(r, "Missing group parameter", status_code=400)
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym} )
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym})
self.assertContains(r, "Missing item parameter", status_code=400)
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym, 'item': '1'} )
r = self.client.post(url,
{'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym, 'item': '1'})
self.assertContains(r, "Missing bluesheet parameter", status_code=400)
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': '1', 'group': group.acronym,
'item': '1', 'bluesheet': bluesheet, })
'item': '1', 'bluesheet': bluesheet, })
self.assertContains(r, "No sessions found for meeting", status_code=400)
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': 'bogous',
'item': '1', 'bluesheet': bluesheet, })
self.assertContains(r, "No sessions found in meeting '%s' for group 'bogous'"%meeting.number, status_code=400)
'item': '1', 'bluesheet': bluesheet, })
self.assertContains(r, "No sessions found in meeting '%s' for group 'bogous'" % meeting.number, status_code=400)
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym,
'item': '1', 'bluesheet': "foobar", })
'item': '1', 'bluesheet': "foobar", })
self.assertContains(r, "Invalid json value: 'foobar'", status_code=400)
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym,
'item': '5', 'bluesheet': bluesheet, })
'item': '5', 'bluesheet': bluesheet, })
self.assertContains(r, "No item '5' found in list of sessions for group", status_code=400)
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym,
'item': 'foo', 'bluesheet': bluesheet, })
'item': 'foo', 'bluesheet': bluesheet, })
self.assertContains(r, "Expected a numeric value for 'item', found 'foo'", status_code=400)
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym,
'item': '1', 'bluesheet': bluesheet, })
'item': '1', 'bluesheet': bluesheet, })
self.assertContains(r, "Done", status_code=200)
# Submit again, with slightly different content, as an updated version
people[1]['affiliation'] = 'Bolaget AB'
bluesheet = json.dumps(people)
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym,
'item': '1', 'bluesheet': bluesheet, })
'item': '1', 'bluesheet': bluesheet, })
self.assertContains(r, "Done", status_code=200)
bluesheet = session.sessionpresentation_set.filter(document__type__slug='bluesheets').first().document
@ -381,6 +458,124 @@ class CustomApiTests(TestCase):
self.assertIn(p['name'], html.unescape(text))
self.assertIn(p['affiliation'], html.unescape(text))
def test_api_upload_bluesheet(self):
url = urlreverse("ietf.meeting.views.api_upload_bluesheet")
recmanrole = RoleFactory(group__type_id="ietf", name_id="recman")
recman = recmanrole.person
meeting = MeetingFactory(type_id="ietf")
session = SessionFactory(group__type_id="wg", meeting=meeting)
group = session.group
apikey = PersonalApiKey.objects.create(endpoint=url, person=recman)
people = [
{"name": "Andrea Andreotti", "affiliation": "Azienda"},
{"name": "Bosse Bernadotte", "affiliation": "Bolag"},
{"name": "Charles Charlemagne", "affiliation": "Compagnie"},
]
for i in range(3):
faker = random_faker()
people.append(dict(name=faker.name(), affiliation=faker.company()))
bluesheet = json.dumps(people)
# error cases
r = self.client.post(url, {})
self.assertContains(r, "Missing apikey parameter", status_code=400)
badrole = RoleFactory(group__type_id="ietf", name_id="ad")
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
badrole.person.user.last_login = timezone.now()
badrole.person.user.save()
r = self.client.post(url, {"apikey": badapikey.hash()})
self.assertContains(
r, "Restricted to roles: Recording Manager, Secretariat", status_code=403
)
r = self.client.post(url, {"apikey": apikey.hash()})
self.assertContains(r, "Too long since last regular login", status_code=400)
recman.user.last_login = timezone.now()
recman.user.save()
r = self.client.get(url, {"apikey": apikey.hash()})
self.assertContains(r, "Method not allowed", status_code=405)
r = self.client.post(url, {"apikey": apikey.hash()})
self.assertContains(r, "Missing session_id parameter", status_code=400)
r = self.client.post(url, {"apikey": apikey.hash(), "session_id": session.pk})
self.assertContains(r, "Missing bluesheet parameter", status_code=400)
r = self.client.post(
url,
{
"apikey": apikey.hash(),
"meeting": meeting.number,
"group": group.acronym,
"item": "1",
"bluesheet": "foobar",
},
)
self.assertContains(r, "Invalid json value: 'foobar'", status_code=400)
bad_session_pk = int(Session.objects.order_by("-pk").first().pk) + 1
r = self.client.post(
url,
{
"apikey": apikey.hash(),
"session_id": bad_session_pk,
"bluesheet": bluesheet,
},
)
self.assertContains(r, "Session not found", status_code=400)
r = self.client.post(
url,
{
"apikey": apikey.hash(),
"session_id": "foo",
"bluesheet": bluesheet,
},
)
self.assertContains(r, "Invalid session_id", status_code=400)
r = self.client.post(
url,
{
"apikey": apikey.hash(),
"session_id": session.pk,
"bluesheet": bluesheet,
},
)
self.assertContains(r, "Done", status_code=200)
# Submit again, with slightly different content, as an updated version
people[1]["affiliation"] = "Bolaget AB"
bluesheet = json.dumps(people)
r = self.client.post(
url,
{
"apikey": apikey.hash(),
"meeting": meeting.number,
"group": group.acronym,
"item": "1",
"bluesheet": bluesheet,
},
)
self.assertContains(r, "Done", status_code=200)
bluesheet = (
session.sessionpresentation_set.filter(document__type__slug="bluesheets")
.first()
.document
)
# We've submitted an update; check that the rev is right
self.assertEqual(bluesheet.rev, "01")
# Check the content
with open(bluesheet.get_file_name()) as file:
text = file.read()
for p in people:
self.assertIn(p["name"], html.unescape(text))
self.assertIn(p["affiliation"], html.unescape(text))
def test_person_export(self):
person = PersonFactory()
url = urlreverse('ietf.api.views.PersonalInformationExportView')

View file

@ -96,7 +96,9 @@ class SchedulingEventInline(admin.TabularInline):
raw_id_fields = ["by"]
class SessionAdmin(admin.ModelAdmin):
list_display = ["meeting", "name", "group_acronym", "purpose", "attendees", "requested", "current_status"]
list_display = [
"meeting", "name", "group_acronym", "purpose", "attendees", "has_onsite_tool", "requested", "current_status"
]
list_filter = ["purpose", "meeting", ]
raw_id_fields = ["meeting", "group", "materials", "joint_with_groups", "tombstone_for"]
search_fields = ["meeting__number", "name", "group__name", "group__acronym", "purpose__name"]

View file

@ -110,6 +110,7 @@ class SessionFactory(factory.django.DjangoModelFactory):
group = factory.SubFactory(GroupFactory)
requested_duration = datetime.timedelta(hours=1)
on_agenda = factory.lazy_attribute(lambda obj: SessionPurposeName.objects.get(pk=obj.purpose_id).on_agenda)
has_onsite_tool = factory.lazy_attribute(lambda obj: obj.purpose_id == 'regular')
@factory.post_generation
def status_id(obj, create, extracted, **kwargs):

View file

@ -727,6 +727,7 @@ class SessionDetailsForm(forms.ModelForm):
'purpose',
session_purposes[0] if len(session_purposes) > 0 else None,
)
kwargs['initial'].setdefault('has_onsite_tool', group.features.acts_like_wg)
super().__init__(*args, **kwargs)
self.fields['type'].widget.attrs.update({
@ -743,8 +744,8 @@ class SessionDetailsForm(forms.ModelForm):
model = Session
fields = (
'purpose', 'name', 'short', 'type', 'requested_duration',
'on_agenda', 'agenda_note', 'remote_instructions', 'attendees',
'comments',
'on_agenda', 'agenda_note', 'has_onsite_tool', 'remote_instructions',
'attendees', 'comments',
)
labels = {'requested_duration': 'Length'}

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2023-03-07 16:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('meeting', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='session',
name='has_onsite_tool',
field=models.BooleanField(default=False, help_text='Does this session use the officially supported onsite and remote tooling?'),
),
]

View file

@ -0,0 +1,33 @@
# Generated by Django 2.2.28 on 2023-03-07 16:54
from django.db import migrations
def forward(apps, schema_editor):
Session = apps.get_model('meeting', 'Session')
Meeting = apps.get_model('meeting', 'Meeting')
Room = apps.get_model('meeting', 'Room')
full_meetings = Meeting.objects.filter(type_id='ietf', schedule__isnull=False)
schedules = {m.schedule for m in full_meetings} | {m.schedule.base for m in full_meetings if m.schedule.base}
rooms_with_meetecho = Room.objects.filter(urlresource__name_id__in=['meetecho', 'meetecho_onsite']).distinct()
sessions_with_meetecho = Session.objects.filter(
timeslotassignments__schedule__in=schedules,
timeslotassignments__timeslot__location__in=rooms_with_meetecho,
).distinct()
sessions_with_meetecho.update(has_onsite_tool=True)
def reverse(apps, schema_editor):
Session = apps.get_model('meeting', 'Session')
Session.objects.all().update(has_onsite_tool=False)
class Migration(migrations.Migration):
dependencies = [
('meeting', '0002_session_has_onsite_tool'),
]
operations = [
migrations.RunPython(forward, reverse),
]

View file

@ -1051,6 +1051,7 @@ class Session(models.Model):
modified = models.DateTimeField(auto_now=True)
remote_instructions = models.CharField(blank=True,max_length=1024)
on_agenda = models.BooleanField(default=True, help_text='Is this session visible on the meeting agenda?')
has_onsite_tool = models.BooleanField(default=False, help_text="Does this session use the officially supported onsite and remote tooling?")
tombstone_for = models.ForeignKey('Session', blank=True, null=True, help_text="This session is the tombstone for a session that was rescheduled", on_delete=models.CASCADE)
@ -1319,6 +1320,33 @@ class Session(models.Model):
if self.group_at_the_time().parent:
return self.meeting.group_at_the_time(self.group_at_the_time().parent)
def audio_stream_url(self):
if (
self.meeting.type.slug == "ietf"
and self.has_onsite_tool
and (url := getattr(settings, "MEETECHO_AUDIO_STREAM_URL", ""))
):
return url.format(session=self)
return None
def video_stream_url(self):
if (
self.meeting.type.slug == "ietf"
and self.has_onsite_tool
and (url := getattr(settings, "MEETECHO_VIDEO_STREAM_URL", ""))
):
return url.format(session=self)
return None
def onsite_tool_url(self):
if (
self.meeting.type.slug == "ietf"
and self.has_onsite_tool
and (url := getattr(settings, "MEETECHO_ONSITE_TOOL_URL", ""))
):
return url.format(session=self)
return None
class SchedulingEvent(models.Model):
session = ForeignKey(Session)

View file

@ -212,14 +212,16 @@ class AgendaApiTests(TestCase):
class MeetingTests(BaseMeetingTestCase):
@override_settings(
MEETECHO_ONSITE_TOOL_URL="https://onsite.example.com",
MEETECHO_VIDEO_STREAM_URL="https://meetecho.example.com",
)
def test_meeting_agenda(self):
meeting = make_meeting_test_data()
session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
session.remote_instructions='https://remote.example.com'
session.save()
slot = TimeSlot.objects.get(sessionassignments__session=session,sessionassignments__schedule=meeting.schedule)
slot.location.urlresource_set.create(name_id='meetecho_onsite', url='https://onsite.example.com')
slot.location.urlresource_set.create(name_id='meetecho', url='https://meetecho.example.com')
meeting.timeslot_set.filter(type_id="break").update(show_location=False)
#
self.write_materials_files(meeting, session)

View file

@ -25,7 +25,7 @@ from django import forms
from django.shortcuts import render, redirect, get_object_or_404
from django.http import (HttpResponse, HttpResponseRedirect, HttpResponseForbidden,
HttpResponseNotFound, Http404, HttpResponseBadRequest,
JsonResponse, HttpResponseGone)
JsonResponse, HttpResponseGone, HttpResponseNotAllowed)
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
@ -1747,10 +1747,10 @@ def agenda_extract_schedule (item):
"chat" : item.session.chat_room_url(),
"chatArchive" : item.session.chat_archive_url(),
"recordings": list(map(agenda_extract_recording, item.session.recordings())),
"videoStream": item.timeslot.location.video_stream_url() if item.timeslot.location else "",
"audioStream": item.timeslot.location.audio_stream_url() if item.timeslot.location else "",
"videoStream": item.session.video_stream_url() or "",
"audioStream": item.session.audio_stream_url() or "",
"webex": item.timeslot.location.webex_url() if item.timeslot.location else "",
"onsiteTool": item.timeslot.location.onsite_tool_url() if item.timeslot.location else "",
"onsiteTool": item.session.onsite_tool_url() or "",
"calendar": reverse(
'ietf.meeting.views.agenda_ical',
kwargs={'num': item.schedule.meeting.number, 'session_id': item.session.id},
@ -3872,6 +3872,65 @@ class OldUploadRedirect(RedirectView):
@role_required('Recording Manager')
@csrf_exempt
def api_set_session_video_url(request):
"""Set video URL for session
parameters:
apikey: the poster's personal API key
session_id: id of session to update
url: The recording url (on YouTube, or whatever)
"""
def err(code, text):
return HttpResponse(text, status=code, content_type='text/plain')
if request.method != 'POST':
return HttpResponseNotAllowed(
content="Method not allowed", content_type="text/plain", permitted_methods=('POST',)
)
# Temporary: fall back to deprecated interface if we have old-style parameters.
# Do away with this once meetecho is using the new pk-based interface.
if any(k in request.POST for k in ['meeting', 'group', 'item']):
return deprecated_api_set_session_video_url(request)
session_id = request.POST.get('session_id', None)
if session_id is None:
return err(400, 'Missing session_id parameter')
incoming_url = request.POST.get('url', None)
if incoming_url is None:
return err(400, 'Missing url parameter')
try:
session = Session.objects.get(pk=session_id)
except Session.DoesNotExist:
return err(400, f"Session not found with session_id '{session_id}'")
except ValueError:
return err(400, "Invalid session_id: {session_id}")
try:
URLValidator()(incoming_url)
except ValidationError:
return err(400, f"Invalid url value: '{incoming_url}'")
recordings = [(r.name, r.title, r) for r in session.recordings() if 'video' in r.title.lower()]
if recordings:
r = recordings[-1][-1]
if r.external_url != incoming_url:
e = DocEvent.objects.create(doc=r, rev=r.rev, type="added_comment", by=request.user.person,
desc="External url changed from %s to %s" % (r.external_url, incoming_url))
r.external_url = incoming_url
r.save_with_history([e])
else:
time = session.official_timeslotassignment().timeslot.time
title = 'Video recording for %s on %s at %s' % (session.group.acronym, time.date(), time.time())
create_recording(session, incoming_url, title=title, user=request.user.person)
return HttpResponse("Done", status=200, content_type='text/plain')
def deprecated_api_set_session_video_url(request):
"""Set video URL for session (deprecated)
Uses meeting/group/item to identify session.
"""
def err(code, text):
return HttpResponse(text, status=code, content_type='text/plain')
if request.method == 'POST':
@ -4045,6 +4104,64 @@ def api_upload_polls(request):
@role_required('Recording Manager', 'Secretariat')
@csrf_exempt
def api_upload_bluesheet(request):
"""Upload bluesheet for a session
parameters:
apikey: the poster's personal API key
session_id: id of session to update
bluesheet: json blob with
[{'name': 'Name', 'affiliation': 'Organization', }, ...]
"""
def err(code, text):
return HttpResponse(text, status=code, content_type='text/plain')
if request.method != 'POST':
return HttpResponseNotAllowed(
content="Method not allowed", content_type="text/plain", permitted_methods=('POST',)
)
# Temporary: fall back to deprecated interface if we have old-style parameters.
# Do away with this once meetecho is using the new pk-based interface.
if any(k in request.POST for k in ['meeting', 'group', 'item']):
return deprecated_api_upload_bluesheet(request)
session_id = request.POST.get('session_id', None)
if session_id is None:
return err(400, 'Missing session_id parameter')
bjson = request.POST.get('bluesheet', None)
if bjson is None:
return err(400, 'Missing bluesheet parameter')
try:
session = Session.objects.get(pk=session_id)
except Session.DoesNotExist:
return err(400, f"Session not found with session_id '{session_id}'")
except ValueError:
return err(400, f"Invalid session_id '{session_id}'")
try:
data = json.loads(bjson)
except json.decoder.JSONDecodeError:
return err(400, f"Invalid json value: '{bjson}'")
text = render_to_string('meeting/bluesheet.txt', {
'data': data,
'session': session,
})
fd, name = tempfile.mkstemp(suffix=".txt", text=True)
os.close(fd)
with open(name, "w") as file:
file.write(text)
with open(name, "br") as file:
save_err = save_bluesheet(request, session, file)
if save_err:
return err(400, save_err)
return HttpResponse("Done", status=200, content_type='text/plain')
def deprecated_api_upload_bluesheet(request):
def err(code, text):
return HttpResponse(text, status=code, content_type='text/plain')
if request.method == 'POST':

View file

@ -9,8 +9,8 @@
<tr class="bg1"><td>Working Group Name:</td><td>{{ group.name }} ({{ group.acronym }})</td></tr>
<tr class="bg2"><td>Area Name:</td><td>{% if group.parent %}{{ group.parent.name }} ({{ group.parent.acronym }}){% endif %}</td></tr>
<tr class="bg1"><td>Number of Sessions:<span class="required">*</span></td><td>{{ form.num_session.errors }}{{ form.num_session }}</td></tr>
{% if group.features.acts_like_wg %}<tr class="bg2" id="session_row_0"><td>Session 1:<span class="required">*</span></td><td>{% include 'meeting/session_details_form.html' with form=form.session_forms.0 only %}</td></tr>
<tr class="bg2" id="session_row_1"><td>Session 2:<span class="required">*</span></td><td>{% include 'meeting/session_details_form.html' with form=form.session_forms.1 only %}</td></tr>
{% if group.features.acts_like_wg %}<tr class="bg2" id="session_row_0"><td>Session 1:<span class="required">*</span></td><td>{% include 'meeting/session_details_form.html' with form=form.session_forms.0 hide_onsite_tool_prompt=True only %}</td></tr>
<tr class="bg2" id="session_row_1"><td>Session 2:<span class="required">*</span></td><td>{% include 'meeting/session_details_form.html' with form=form.session_forms.1 hide_onsite_tool_prompt=True only %}</td></tr>
{% if not is_virtual %}
<tr class="bg2"><td>Time between two sessions:</td><td>{{ form.session_time_relation.errors }}{{ form.session_time_relation }}</td></tr>
{% endif %}
@ -18,7 +18,7 @@
Additional slot may be available after agenda scheduling has closed and with the approval of an Area Director.<br>
<div id="session_row_2">
Third Session:
{% include 'meeting/session_details_form.html' with form=form.session_forms.2 only %}
{% include 'meeting/session_details_form.html' with form=form.session_forms.2 hide_onsite_tool_prompt=True only %}
</div>
</td></tr>
{% else %}{# else not group.features.acts_like_wg #}

View file

@ -16,6 +16,8 @@
{% if sess_form.cleaned_data.purpose.timeslot_types|length > 1 %}({{ sess_form.cleaned_data.type }}
){% endif %}
</dd>
<dt>Onsite tool?</dt>
<dd>{{ sess_form.cleaned_data.has_onsite_tool|yesno }}</dd>
{% endif %}
</dl>
</td>

View file

@ -16,6 +16,8 @@
{% if sess.purpose.timeslot_types|length > 1 %}({{ sess.type }}
){% endif %}
</dd>
<dt>Onsite tool?</dt>
<dd>{{ sess.has_onsite_tool|yesno }}</dd>
{% endif %}
</dl>
</td>

View file

@ -6,9 +6,9 @@
{% block extrastyle %}
<style>
dl {width: 100%;}
dt {float: left; width: 15%; margin: 0.1em 0 0.1em 0; }
dt {float: left; width: 30%; margin: 0.1em 0 0.1em 0; }
dt::after {content: ":";}
dd {float: left; width: 85%; margin: 0.1em 0 0.1em 0;}
dd {float: left; width: 70%; margin: 0.1em 0 0.1em 0;}
</style>
{% endblock %}

View file

@ -7,6 +7,15 @@
<script src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block extrastyle %}
<style>
dl {width: 100%;}
dt {float: left; width: 30%; margin: 0.1em 0 0.1em 0; }
dt::after {content: ":";}
dd {float: left; width: 70%; margin: 0.1em 0 0.1em 0;}
</style>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../">Sessions</a>
&raquo; {{ group.acronym }}

View file

@ -1176,6 +1176,10 @@ CELERY_BEAT_SYNC_EVERY = 1 # update DB after every event
# 'request_timeout': 3.01, # python-requests doc recommend slightly > a multiple of 3 seconds
# }
# Meetecho URLs - instantiate with url.format(session=some_session)
MEETECHO_ONSITE_TOOL_URL = "https://meetings.conf.meetecho.com/onsite{session.meeting.number}/?session={session.pk}"
MEETECHO_VIDEO_STREAM_URL = "https://meetings.conf.meetecho.com/ietf{session.meeting.number}/?session={session.pk}"
MEETECHO_AUDIO_STREAM_URL = "https://mp3.conf.meetecho.com/ietf{session.meeting.number}/{session.pk}.m3u"
# Put the production SECRET_KEY in settings_local.py, and also any other
# sensitive or site-specific changes. DO NOT commit settings_local.py to svn.

View file

@ -13,11 +13,11 @@ DTEND{% ics_date_time item.timeslot.local_end_time schedule.meeting.time_zone %}
DTSTAMP{% ics_date_time item.timeslot.modified|utc 'utc' %}{% if item.session.agenda %}
URL:{{item.session.agenda.get_versionless_href}}{% endif %}
DESCRIPTION:{{item.timeslot.name|ics_esc}}\n{% if item.session.agenda_note %}
Note: {{item.session.agenda_note|ics_esc}}\n{% endif %}{% if item.timeslot.location.onsite_tool_url %}
Note: {{item.session.agenda_note|ics_esc}}\n{% endif %}{% if item.session.onsite_tool_url %}
\n
Onsite tool: {{ item.timeslot.location.onsite_tool_url|format:item.session }}\n{% endif %}{% if item.timeslot.location.video_stream_url %}
Onsite tool: {{ item.session.onsite_tool_url }}\n{% endif %}{% if item.session.video_stream_url %}
\n
Meetecho: {{ item.timeslot.location.video_stream_url|format:item.session }}\n{% endif %}{% if item.timeslot.location.webex_url %}
Meetecho: {{ item.session.video_stream_url }}\n{% endif %}{% if item.timeslot.location.webex_url %}
\n
Webex: {{ item.timeslot.location.webex_url }}\n{% endif %}{% if item.session.remote_instructions %}
\n

View file

@ -74,9 +74,9 @@
<i class="bi bi-people"></i>
</a>
{# Video stream (meetecho) #}
{% elif item.timeslot.location.video_stream_url %}
{% elif session.video_stream_url %}
<a class="btn btn-outline-primary"
href="{{ item.timeslot.location.video_stream_url|format:session }}"
href="{{ session.video_stream_url|format:session }}"
aria-label="Meetecho video stream"
title="Meetecho video stream">
<i class="bi bi-camera-video"></i>
@ -146,7 +146,7 @@
{% endif %}
{% endwith %}
{% endfor %}
{% elif item.timeslot.location.video_stream_url %}
{% elif session.video_stream_url %}
<a class="btn btn-outline-primary"
href="http://www.meetecho.com/ietf{{ meeting.number }}/recordings#{{ acronym.upper }}"
aria-label="Meetecho session recording"

View file

@ -61,30 +61,30 @@
<i class="bi bi-chat"></i>
</a>
{# Video stream (meetecho) #}
{% if timeslot.location.video_stream_url %}
{% if session.video_stream_url %}
<a class="btn btn-outline-primary"
role="button"
href="{{ timeslot.location.video_stream_url|format:session }}"
href="{{ session.video_stream_url }}"
aria-label="Video stream"
title="Video stream">
<i class="bi bi-camera-video"></i>
</a>
{% endif %}
{# Onsite tool (meetecho_onsite) #}
{% if timeslot.location.onsite_tool_url %}
{% if session.onsite_tool_url %}
<a class="btn btn-outline-primary"
role="button"
href="{{ timeslot.location.onsite_tool_url|format:session }}"
href="{{ session.onsite_tool_url }}"
aria-label="Onsite tool"
title="Onsite tool">
<i class="bi bi-phone"></i>
</a>
{% endif %}
{# Audio stream #}
{% if timeslot.location.audio_stream_url %}
{% if session.audio_stream_url %}
<a class="btn btn-outline-primary"
role="button"
href="{{ timeslot.location.audio_stream_url|format:session }}"
href="{{ session.audio_stream_url }}"
aria-label="Audio stream"
title="Audio stream">
<i class="bi bi-headphones"></i>
@ -178,7 +178,7 @@
{% endfor %}
{% endif %}
{% endwith %}
{% if timeslot.location.video_stream_url %}
{% if session.video_stream_url %}
<a class="btn btn-outline-primary"
role="button"
href="https://www.meetecho.com/ietf{{ meeting.number }}/recordings#{{ acronym.upper }}"
@ -246,28 +246,28 @@
</a>
</li>
{# Video stream (meetecho) #}
{% if timeslot.location.video_stream_url %}
{% if session.video_stream_url %}
<li>
<a class="dropdown-item"
href="{{ timeslot.location.video_stream_url|format:session }}">
href="{{ session.video_stream_url }}">
<i class="bi bi-camera-video"></i> Video stream
</a>
</li>
{% endif %}
{# Onsite tool (meetecho_onsite) #}
{% if timeslot.location.onsite_tool_url %}
{% if session.onsite_tool_url %}
<li>
<a class="dropdown-item"
href="{{ timeslot.location.onsite_tool_url|format:session }}">
href="{{ session.onsite_tool_url }}">
<i class="bi bi-phone"></i> Onsite tool
</a>
</li>
{% endif %}
{# Audio stream #}
{% if timeslot.location.audio_stream_url %}
{% if session.audio_stream_url %}
<li>
<a class="dropdown-item"
href="{{ timeslot.location.audio_stream_url|format:session }}">
href="{{ session.audio_stream_url }}">
<i class="bi bi-headphones"></i> Audio stream
</a>
</li>
@ -348,7 +348,7 @@
{% endfor %}
{% endif %}
{% endwith %}
{% if timeslot.location.video_stream_url %}
{% if session.video_stream_url %}
<li>
<a class="dropdown-item"
href="https://www.meetecho.com/ietf{{ meeting.number }}/recordings#{{ acronym.upper }}">

View file

@ -2,6 +2,7 @@
<div class="session-details-form my-3" data-prefix="{{ form.prefix }}">
{% if hidden %}
{{ form.name.as_hidden }}{{ form.purpose.as_hidden }}{{ form.type.as_hidden }}{{ form.requested_duration.as_hidden }}
{{ form.has_onsite_tool.as_hidden }}
{% else %}
<table>
<tbody>
@ -26,8 +27,15 @@
<th scope="row">{{ form.requested_duration.label_tag }}</th>
<td>{{ form.requested_duration }}{{ form.requested_duration.errors }}</td>
</tr>
{% if not hide_onsite_tool_prompt %}
<tr>
<th scope="row">{{ form.has_onsite_tool.label_tag }}</th>
<td>{{ form.has_onsite_tool }}{{ form.has_onsite_tool.errors }}</td>
</tr>
{% endif %}
</tbody>
</table>
{% if hide_onsite_tool_prompt %}{{ form.has_onsite_tool.as_hidden }}{% endif %}
{% endif %}
{# hidden fields included whether or not the whole form is hidden #}
{{ form.attendees.as_hidden }}{{ form.comments.as_hidden }}{{ form.id.as_hidden }}{{ form.on_agenda.as_hidden }}{{ form.DELETE.as_hidden }}{{ form.remote_instructions.as_hidden }}{{ form.short.as_hidden }}{{ form.agenda_note.as_hidden }}

View file

@ -233,30 +233,30 @@
</td>
</tr>
{# Video stream (meetecho) #}
{% if timezone_now < timeslot.end_time %}
{% if session.video_stream_url and timezone_now < timeslot.end_time %}
<tr>
<td>
<a href="{{ timeslot.location.video_stream_url|format:session }}">
<a href="{{ session.video_stream_url }}">
<i class="bi bi-camera-video"></i> Video stream
</a>
</td>
</tr>
{% endif %}
{# Onsite tool (meetecho_onsite) #}
{% if timeslot.location.onsite_tool_url %}
{% if session.onsite_tool_url %}
<tr>
<td>
<a href="{{ timeslot.location.onsite_tool_url|format:session }}">
<a href="{{ session.onsite_tool_url }}">
<i class="bi bi-phone"></i> Onsite tool
</a>
</td>
</tr>
{% endif %}
{# Audio stream #}
{% if timeslot.location.audio_stream_url %}
{% if session.audio_stream_url %}
<tr>
<td>
<a href="{{ timeslot.location.audio_stream_url|format:session }}">
<a href="{{ session.audio_stream_url }}">
<i class="bi bi-headphones"></i> Audio stream
</a>
</td>
@ -341,7 +341,7 @@
{% endfor %}
{% endif %}
{% endwith %}
{% if timeslot.location.video_stream_url %}
{% if session.video_stream_url %}
<tr>
<td>
<a href="https://www.meetecho.com/ietf{{ meeting.number }}/recordings#{{ acronym.upper }}">