Skip to content

Commit e8f999d

Browse files
committed
Added an API endpoint to let Meetech associate recording urls with sessions.
- Legacy-Id: 14967
1 parent 0ef66ae commit e8f999d

6 files changed

Lines changed: 169 additions & 9 deletions

File tree

PLAN

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ Planned work in rough order
99

1010
* Revisit the review tool, work through the accumulated tickets.
1111

12-
* Introduce an API for Meetecho to use to associate recordings with sessions
13-
(and perhaps automate making copies of those videos)
14-
1512
* GroupFeatures cleanup. Move most to fields on GroupTypeName, and fix places
1613
that still uses lists of group types to determine actions by instead
1714
defining group type fields to hold the selector. (Setting up a new

ietf/api/tests.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
1+
# Copyright The IETF Trust 2015-2018, All Rights Reserved
2+
3+
import json
14
import os
25
import sys
3-
import json
6+
47
from importlib import import_module
58
from mock import patch
69

710
from django.apps import apps
8-
from django.test import Client
911
from django.conf import settings
12+
from django.test import Client
1013
from django.urls import reverse as urlreverse
14+
from django.utils import timezone
1115

1216
from tastypie.test import ResourceTestCaseMixin
1317

1418
import debug # pyflakes:ignore
1519

16-
from ietf.utils.test_utils import TestCase
20+
from ietf.group.factories import RoleFactory
21+
from ietf.meeting.factories import MeetingFactory, SessionFactory
1722
from ietf.meeting.test_data import make_meeting_test_data
23+
from ietf.person.models import PersonalApiKey
24+
from ietf.utils.test_utils import TestCase
1825

1926
OMITTED_APPS = (
2027
'ietf.secr.meetings',
@@ -44,6 +51,59 @@ def test_api_help_page(self):
4451
r = self.client.get(url)
4552
self.assertContains(r, 'The datatracker API', status_code=200)
4653

54+
def test_api_set_session_video_url(self):
55+
url = urlreverse('ietf.meeting.views.api_set_session_video_url')
56+
recmanrole = RoleFactory(group__type_id='ietf', name_id='recman')
57+
recman = recmanrole.person
58+
meeting = MeetingFactory(type_id='ietf')
59+
session = SessionFactory(group__type_id='wg', meeting=meeting)
60+
group = session.group
61+
apikey = PersonalApiKey.objects.create(endpoint=url, person=recman)
62+
63+
# error cases
64+
r = self.client.post(url, {})
65+
self.assertContains(r, "Missing apikey parameter", status_code=400)
66+
67+
badrole = RoleFactory(group__type_id='ietf', name_id='ad')
68+
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
69+
badrole.person.user.last_login = timezone.now()
70+
badrole.person.user.save()
71+
r = self.client.post(url, {'apikey': badapikey.hash()} )
72+
self.assertContains(r, "Restricted to role Recording Manager", status_code=403)
73+
74+
r = self.client.post(url, {'apikey': apikey.hash()} )
75+
self.assertContains(r, "Too long since last regular login", status_code=400)
76+
recman.user.last_login = timezone.now()
77+
recman.user.save()
78+
79+
r = self.client.get(url, {'apikey': apikey.hash()} )
80+
self.assertContains(r, "Method not allowed", status_code=405)
81+
82+
r = self.client.post(url, {'apikey': apikey.hash()} )
83+
self.assertContains(r, "Missing meeting parameter", status_code=400)
84+
85+
86+
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, } )
87+
self.assertContains(r, "Missing group parameter", status_code=400)
88+
89+
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym} )
90+
self.assertContains(r, "Missing item parameter", status_code=400)
91+
92+
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym, 'item': '1'} )
93+
self.assertContains(r, "Missing url parameter", status_code=400)
94+
95+
video = 'https://foo.example.com/bar/beer/'
96+
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym,
97+
'item': '1', 'url': video, })
98+
self.assertContains(r, "Done", status_code=200)
99+
recordings = session.recordings()
100+
self.assertEqual(len(recordings), 1)
101+
doc = recordings[0]
102+
self.assertEqual(doc.external_url, video)
103+
event = doc.latest_event()
104+
self.assertEqual(event.by, recman)
105+
106+
47107
class TastypieApiTestCase(ResourceTestCaseMixin, TestCase):
48108
def __init__(self, *args, **kwargs):
49109
self.apps = {}

ietf/api/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
url(r'^v1/?$', api_views.top_level),
1818
# Custom API endpoints
1919
url(r'^notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings),
20+
url(r'^meeting/session/video/url$', meeting_views.api_set_session_video_url),
2021
url(r'^submit/?$', submit_views.api_submit),
2122
url(r'^iesg/position', views_ballot.api_set_position),
2223
]

ietf/meeting/views.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright The IETF Trust 2007, All Rights Reserved
1+
# Copyright The IETF Trust 2007-2018, All Rights Reserved
22

33
import csv
44
import datetime
@@ -23,6 +23,8 @@
2323
from django.conf import settings
2424
from django.contrib import messages
2525
from django.contrib.auth.decorators import login_required
26+
from django.core.exceptions import ValidationError
27+
from django.core.validators import URLValidator
2628
from django.urls import reverse,reverse_lazy
2729
from django.db.models import Min, Max, Q
2830
from django.forms.models import modelform_factory, inlineformset_factory
@@ -59,7 +61,8 @@
5961
from ietf.meeting.utils import finalize
6062
from ietf.secr.proceedings.utils import handle_upload_file
6163
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
62-
import_youtube_video_urls)
64+
import_youtube_video_urls, create_recording)
65+
from ietf.utils.decorators import require_api_key
6366
from ietf.utils.mail import send_mail_message
6467
from ietf.utils.pipe import pipe
6568
from ietf.utils.pdf import pdf_pages
@@ -2178,6 +2181,62 @@ def api_import_recordings(request, number):
21782181
else:
21792182
return HttpResponse(status=405)
21802183

2184+
@require_api_key
2185+
@role_required('Recording Manager')
2186+
@csrf_exempt
2187+
def api_set_session_video_url(request):
2188+
def err(code, text):
2189+
return HttpResponse(text, status=code, content_type='text/plain')
2190+
if request.method == 'POST':
2191+
# parameters:
2192+
# apikey: the poster's personal API key
2193+
# meeting: '101', or 'interim-2018-quic-02'
2194+
# group: 'quic' or 'plenary'
2195+
# item: '1', '2', '3' (the group's first, second, third etc.
2196+
# session during the week)
2197+
# url: The recording url (on YouTube, or whatever)
2198+
user = request.user.person
2199+
for item in ['meeting', 'group', 'item', 'url',]:
2200+
value = request.POST.get(item)
2201+
if not value:
2202+
return err(400, "Missing %s parameter" % item)
2203+
number = request.POST.get('meeting')
2204+
sessions = Session.objects.filter(meeting__number=number)
2205+
if not sessions.exists():
2206+
return err(404, "No sessions found for meeting '%s'" % (number, ))
2207+
acronym = request.POST.get('group')
2208+
sessions = sessions.filter(group__acronym=acronym)
2209+
if not sessions.exists():
2210+
return err(404, "No sessions found in meeting '%s' for group '%s'" % (number, acronym))
2211+
session_times = [ (s.official_timeslotassignment().timeslot.time, s) for s in sessions ]
2212+
session_times.sort()
2213+
item = request.POST.get('item')
2214+
if not item.isdigit():
2215+
return err(400, "Expected a numeric value for 'item', found '%s'" % (item, ))
2216+
n = int(item)-1 # change 1-based to 0-based
2217+
try:
2218+
time, session = session_times[n]
2219+
except IndexError:
2220+
return err(400, "No item '%s' found in list of sessions for group" % (item, ))
2221+
url = request.POST.get('url')
2222+
try:
2223+
URLValidator()(url)
2224+
except ValidationError:
2225+
return err(400, "Invalid url value: '%s'" % (url, ))
2226+
recordings = [ (r.name, r.title, r) for r in session.recordings() if 'video' in r.title.lower() ]
2227+
if recordings:
2228+
r = recordings[-1][-1]
2229+
r.external_url = url
2230+
else:
2231+
time = session.official_timeslotassignment().timeslot.time
2232+
title = 'Video recording for %s on %s at %s' % (acronym, time.date(), time.time())
2233+
create_recording(session, url, title=title, user=user)
2234+
else:
2235+
return err(405, "Method not allowed")
2236+
2237+
return HttpResponse("Done", status=200, content_type='text/plain')
2238+
2239+
21812240
def important_dates(request, num=None):
21822241
assert num is None or num.isdigit()
21832242
preview_roles = ['Area Director', 'Secretariat', 'IETF Chair', 'IAD', ]

ietf/person/models.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ def salt():
290290
# Manual maintenance: List all endpoints that use @require_api_key here
291291
PERSON_API_KEY_ENDPOINTS = [
292292
("/api/iesg/position", "/api/iesg/position"),
293+
("/api/meeting/session/video/url", "/api/meeting/session/video/url"),
293294
]
294295

295296
class PersonalApiKey(models.Model):
@@ -304,7 +305,10 @@ class PersonalApiKey(models.Model):
304305
@classmethod
305306
def validate_key(cls, s):
306307
import struct, hashlib, base64
307-
key = base64.urlsafe_b64decode(six.binary_type(s))
308+
try:
309+
key = base64.urlsafe_b64decode(six.binary_type(s))
310+
except TypeError:
311+
return None
308312
id, salt, hash = struct.unpack(KEY_STRUCT, key)
309313
k = cls.objects.filter(id=id)
310314
if not k.exists():

ietf/templates/api/index.html

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,45 @@ <h3 id="iesg-position-api">IESG ballot position API</h3>
226226
Done
227227
</pre>
228228

229+
230+
231+
<h3 id="session-video-url-api">Set session video URL</h3>
232+
233+
<p>
234+
235+
This interface is intended for Meetecho, to provide a way to set the
236+
URL of a video recording for a given session. It is available at
237+
<code>{% url 'ietf.meeting.views.api_set_session_video_url' %}</code>.
238+
Access is limited to recording managers.
239+
240+
</p>
241+
<p>
242+
The interface requires the use of a personal API key, which can be created at
243+
<a href="{% url 'ietf.ietfauth.views.apikey_index' %}">{% url 'ietf.ietfauth.views.apikey_index' %}</a>
244+
</p>
245+
<p>
246+
The ballot position API takes the following parameters:
247+
</p>
248+
<ul>
249+
<li><code>apikey</code> (required) which is the personal API key hash</li>
250+
<li><code>meeting</code> (required) which is the meeting number</li>
251+
<li><code>group</code> (required) which is the group acronym</li>
252+
<li><code>item</code> (required) which is the chronological sequence number of the session (1 for a group's first session, 2 for the second, etc.)</li>
253+
<li><code>url</code> (required) which is the url that points to the video recording</li>
254+
</ul>
255+
<p>
256+
It returns an appropriate http result code, and a brief explanatory text message.
257+
</p>
258+
<p>
259+
Here is an example:</li>
260+
</p>
261+
<pre>
262+
$ curl -S -F "apikey=DgAAAMLSi3coaE5TjrRs518xO8eBRlCmFF3eQcC8_SjUTtRGLGiJh7-1SYPT5WiS" -F "meeting=101" -F "group=mptcp" -F "item=1" -F "url=https://foo.example/beer/mptcp" https://datatracker.ietf.org/api/meeting/session/video/url
263+
Done
264+
</pre>
265+
266+
267+
229268
</div>
230269

231270
{% endblock %}

0 commit comments

Comments
 (0)