Merged in ^/personal/henrik/6.76.1-meetecho-api/ which provides new API endpoint to let video urls be associated with sessions. Also added some more checks to the test_api_set_session_video_url() test.
- Legacy-Id: 14969
This commit is contained in:
commit
a1c79a075d
5
PLAN
5
PLAN
|
@ -9,13 +9,10 @@ Planned work in rough order
|
|||
|
||||
* Revisit the review tool, work through the accumulated tickets.
|
||||
|
||||
* Introduce an API for Meetecho to use to associate recordings with sessions
|
||||
(and perhaps automate making copies of those videos)
|
||||
|
||||
* GroupFeatures cleanup. Move most to fields on GroupTypeName, and fix places
|
||||
that still uses lists of group types to determine actions by instead
|
||||
defining group type fields to hold the selector. (Setting up a new
|
||||
group type for things like PechaCucha and Hot RFC Lightning Tals probably
|
||||
group type for things like PechaCucha and Hot RFC Lightning Talks probably
|
||||
could have been done with only table edits if there hadn't been so much code
|
||||
using type_id lists and features needing code changes).
|
||||
|
||||
|
|
|
@ -1,20 +1,27 @@
|
|||
# Copyright The IETF Trust 2015-2018, All Rights Reserved
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
from importlib import import_module
|
||||
from mock import patch
|
||||
|
||||
from django.apps import apps
|
||||
from django.test import Client
|
||||
from django.conf import settings
|
||||
from django.test import Client
|
||||
from django.urls import reverse as urlreverse
|
||||
from django.utils import timezone
|
||||
|
||||
from tastypie.test import ResourceTestCaseMixin
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.utils.test_utils import TestCase
|
||||
from ietf.group.factories import RoleFactory
|
||||
from ietf.meeting.factories import MeetingFactory, SessionFactory
|
||||
from ietf.meeting.test_data import make_meeting_test_data
|
||||
from ietf.person.models import PersonalApiKey
|
||||
from ietf.utils.test_utils import TestCase
|
||||
|
||||
OMITTED_APPS = (
|
||||
'ietf.secr.meetings',
|
||||
|
@ -44,6 +51,75 @@ class CustomApiTestCase(TestCase):
|
|||
r = self.client.get(url)
|
||||
self.assertContains(r, 'The datatracker API', status_code=200)
|
||||
|
||||
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)
|
||||
group = session.group
|
||||
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 meeting parameter", status_code=400)
|
||||
|
||||
|
||||
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} )
|
||||
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'} )
|
||||
self.assertContains(r, "Missing url parameter", status_code=400)
|
||||
|
||||
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': '1', 'group': group.acronym,
|
||||
'item': '1', 'url': video, })
|
||||
self.assertContains(r, "No sessions found for meeting", status_code=404)
|
||||
|
||||
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': 'bogous',
|
||||
'item': '1', 'url': video, })
|
||||
self.assertContains(r, "No sessions found in meeting '%s' for group 'bogous'"%meeting.number, status_code=404)
|
||||
|
||||
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym,
|
||||
'item': '1', 'url': "foobar", })
|
||||
self.assertContains(r, "Invalid url value: 'foobar'", status_code=400)
|
||||
|
||||
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym,
|
||||
'item': '5', 'url': video, })
|
||||
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': '1', '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)
|
||||
|
||||
|
||||
class TastypieApiTestCase(ResourceTestCaseMixin, TestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.apps = {}
|
||||
|
|
|
@ -17,6 +17,7 @@ urlpatterns = [
|
|||
url(r'^v1/?$', api_views.top_level),
|
||||
# Custom API endpoints
|
||||
url(r'^notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings),
|
||||
url(r'^meeting/session/video/url$', meeting_views.api_set_session_video_url),
|
||||
url(r'^submit/?$', submit_views.api_submit),
|
||||
url(r'^iesg/position', views_ballot.api_set_position),
|
||||
]
|
||||
|
|
|
@ -71,6 +71,7 @@ def has_role(user, role_names, *args, **kwargs):
|
|||
"Nomcom": Q(person=person, group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
|
||||
"Liaison Manager": Q(person=person,name="liaiman",group__type="sdo",group__state="active", ),
|
||||
"Authorized Individual": Q(person=person,name="auth",group__type="sdo",group__state="active", ),
|
||||
"Recording Manager": Q(person=person,name="recman",group__type="ietf",group__state="active", ),
|
||||
"Reviewer": Q(person=person, name="reviewer", group__state="active"),
|
||||
"Review Team Secretary": Q(person=person, name="secr", group__reviewteamsettings__isnull=False,group__state="active", ),
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright The IETF Trust 2007, All Rights Reserved
|
||||
# Copyright The IETF Trust 2007-2018, All Rights Reserved
|
||||
|
||||
import csv
|
||||
import datetime
|
||||
|
@ -23,6 +23,8 @@ from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidde
|
|||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import URLValidator
|
||||
from django.urls import reverse,reverse_lazy
|
||||
from django.db.models import Min, Max, Q
|
||||
from django.forms.models import modelform_factory, inlineformset_factory
|
||||
|
@ -59,7 +61,8 @@ from ietf.meeting.helpers import send_interim_announcement_request
|
|||
from ietf.meeting.utils import finalize
|
||||
from ietf.secr.proceedings.utils import handle_upload_file
|
||||
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
|
||||
import_youtube_video_urls)
|
||||
import_youtube_video_urls, create_recording)
|
||||
from ietf.utils.decorators import require_api_key
|
||||
from ietf.utils.mail import send_mail_message
|
||||
from ietf.utils.pipe import pipe
|
||||
from ietf.utils.pdf import pdf_pages
|
||||
|
@ -2180,6 +2183,62 @@ def api_import_recordings(request, number):
|
|||
else:
|
||||
return HttpResponse(status=405)
|
||||
|
||||
@require_api_key
|
||||
@role_required('Recording Manager')
|
||||
@csrf_exempt
|
||||
def api_set_session_video_url(request):
|
||||
def err(code, text):
|
||||
return HttpResponse(text, status=code, content_type='text/plain')
|
||||
if request.method == 'POST':
|
||||
# parameters:
|
||||
# apikey: the poster's personal API key
|
||||
# meeting: '101', or 'interim-2018-quic-02'
|
||||
# group: 'quic' or 'plenary'
|
||||
# item: '1', '2', '3' (the group's first, second, third etc.
|
||||
# session during the week)
|
||||
# url: The recording url (on YouTube, or whatever)
|
||||
user = request.user.person
|
||||
for item in ['meeting', 'group', 'item', 'url',]:
|
||||
value = request.POST.get(item)
|
||||
if not value:
|
||||
return err(400, "Missing %s parameter" % item)
|
||||
number = request.POST.get('meeting')
|
||||
sessions = Session.objects.filter(meeting__number=number)
|
||||
if not sessions.exists():
|
||||
return err(404, "No sessions found for meeting '%s'" % (number, ))
|
||||
acronym = request.POST.get('group')
|
||||
sessions = sessions.filter(group__acronym=acronym)
|
||||
if not sessions.exists():
|
||||
return err(404, "No sessions found in meeting '%s' for group '%s'" % (number, acronym))
|
||||
session_times = [ (s.official_timeslotassignment().timeslot.time, s) for s in sessions ]
|
||||
session_times.sort()
|
||||
item = request.POST.get('item')
|
||||
if not item.isdigit():
|
||||
return err(400, "Expected a numeric value for 'item', found '%s'" % (item, ))
|
||||
n = int(item)-1 # change 1-based to 0-based
|
||||
try:
|
||||
time, session = session_times[n]
|
||||
except IndexError:
|
||||
return err(400, "No item '%s' found in list of sessions for group" % (item, ))
|
||||
url = request.POST.get('url')
|
||||
try:
|
||||
URLValidator()(url)
|
||||
except ValidationError:
|
||||
return err(400, "Invalid url value: '%s'" % (url, ))
|
||||
recordings = [ (r.name, r.title, r) for r in session.recordings() if 'video' in r.title.lower() ]
|
||||
if recordings:
|
||||
r = recordings[-1][-1]
|
||||
r.external_url = url
|
||||
else:
|
||||
time = session.official_timeslotassignment().timeslot.time
|
||||
title = 'Video recording for %s on %s at %s' % (acronym, time.date(), time.time())
|
||||
create_recording(session, url, title=title, user=user)
|
||||
else:
|
||||
return err(405, "Method not allowed")
|
||||
|
||||
return HttpResponse("Done", status=200, content_type='text/plain')
|
||||
|
||||
|
||||
def important_dates(request, num=None):
|
||||
assert num is None or num.isdigit()
|
||||
preview_roles = ['Area Director', 'Secretariat', 'IETF Chair', 'IAD', ]
|
||||
|
|
|
@ -9538,6 +9538,16 @@
|
|||
"model": "name.rolename",
|
||||
"pk": "pre-ad"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"name": "Recording Manager",
|
||||
"order": 13,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.rolename",
|
||||
"pk": "recman"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
|
@ -9578,6 +9588,16 @@
|
|||
"model": "name.rolename",
|
||||
"pk": "trac-admin"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "Provides log-in permission to restricted Trac instances",
|
||||
"name": "Trac Editor",
|
||||
"order": 0,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.rolename",
|
||||
"pk": "trac-editor"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "Audio streaming support",
|
||||
|
@ -10002,7 +10022,7 @@
|
|||
"fields": {
|
||||
"command": "xym",
|
||||
"switch": "--version",
|
||||
"time": "2017-12-31T00:07:14.314",
|
||||
"time": "2018-03-23T00:08:43.130",
|
||||
"used": true,
|
||||
"version": "xym 0.4"
|
||||
},
|
||||
|
@ -10013,9 +10033,9 @@
|
|||
"fields": {
|
||||
"command": "pyang",
|
||||
"switch": "--version",
|
||||
"time": "2017-12-31T00:07:15.241",
|
||||
"time": "2018-03-23T00:08:44.177",
|
||||
"used": true,
|
||||
"version": "pyang 1.7.3"
|
||||
"version": "pyang 1.7.4"
|
||||
},
|
||||
"model": "utils.versioninfo",
|
||||
"pk": 2
|
||||
|
@ -10024,11 +10044,22 @@
|
|||
"fields": {
|
||||
"command": "yanglint",
|
||||
"switch": "--version",
|
||||
"time": "2017-12-31T00:07:15.325",
|
||||
"time": "2018-03-23T00:08:44.295",
|
||||
"used": true,
|
||||
"version": "yanglint 0.14.53"
|
||||
"version": "yanglint 0.14.73"
|
||||
},
|
||||
"model": "utils.versioninfo",
|
||||
"pk": 3
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"command": "xml2rfc",
|
||||
"switch": "--version",
|
||||
"time": "2018-03-23T00:08:45.862",
|
||||
"used": true,
|
||||
"version": "xml2rfc 2.9.6"
|
||||
},
|
||||
"model": "utils.versioninfo",
|
||||
"pk": 4
|
||||
}
|
||||
]
|
||||
|
|
|
@ -290,6 +290,7 @@ def salt():
|
|||
# Manual maintenance: List all endpoints that use @require_api_key here
|
||||
PERSON_API_KEY_ENDPOINTS = [
|
||||
("/api/iesg/position", "/api/iesg/position"),
|
||||
("/api/meeting/session/video/url", "/api/meeting/session/video/url"),
|
||||
]
|
||||
|
||||
class PersonalApiKey(models.Model):
|
||||
|
@ -304,7 +305,10 @@ class PersonalApiKey(models.Model):
|
|||
@classmethod
|
||||
def validate_key(cls, s):
|
||||
import struct, hashlib, base64
|
||||
key = base64.urlsafe_b64decode(six.binary_type(s))
|
||||
try:
|
||||
key = base64.urlsafe_b64decode(six.binary_type(s))
|
||||
except TypeError:
|
||||
return None
|
||||
id, salt, hash = struct.unpack(KEY_STRUCT, key)
|
||||
k = cls.objects.filter(id=id)
|
||||
if not k.exists():
|
||||
|
|
|
@ -177,7 +177,7 @@ def get_or_create_recording_document(url,session):
|
|||
except ObjectDoesNotExist:
|
||||
return create_recording(session,url)
|
||||
|
||||
def create_recording(session,url):
|
||||
def create_recording(session, url, title=None, user=None):
|
||||
'''
|
||||
Creates the Document type=recording, setting external_url and creating
|
||||
NewRevisionDocEvent
|
||||
|
@ -185,10 +185,11 @@ def create_recording(session,url):
|
|||
sequence = get_next_sequence(session.group,session.meeting,'recording')
|
||||
name = 'recording-{}-{}-{}'.format(session.meeting.number,session.group.acronym,sequence)
|
||||
time = session.official_timeslotassignment().timeslot.time.strftime('%Y-%m-%d %H:%M')
|
||||
if url.endswith('mp3'):
|
||||
title = 'Audio recording for {}'.format(time)
|
||||
else:
|
||||
title = 'Video recording for {}'.format(time)
|
||||
if not title:
|
||||
if url.endswith('mp3'):
|
||||
title = 'Audio recording for {}'.format(time)
|
||||
else:
|
||||
title = 'Video recording for {}'.format(time)
|
||||
|
||||
doc = Document.objects.create(name=name,
|
||||
title=title,
|
||||
|
@ -202,7 +203,7 @@ def create_recording(session,url):
|
|||
|
||||
# create DocEvent
|
||||
NewRevisionDocEvent.objects.create(type='new_revision',
|
||||
by=Person.objects.get(name='(System)'),
|
||||
by=user or Person.objects.get(name='(System)'),
|
||||
doc=doc,
|
||||
rev=doc.rev,
|
||||
desc='New revision available',
|
||||
|
|
|
@ -18,6 +18,7 @@ warnings.filterwarnings("ignore", message="on_delete will be a required arg for
|
|||
warnings.filterwarnings("ignore", message="The load_template\(\) method is deprecated. Use get_template\(\) instead.")
|
||||
warnings.filterwarnings("ignore", message="escape isn't the last filter in")
|
||||
warnings.filterwarnings("ignore", message="Deprecated allow_tags attribute used on field")
|
||||
warnings.filterwarnings("ignore", message="You passed a bytestring as `filenames`. This will not work on Python 3.")
|
||||
|
||||
try:
|
||||
import syslog
|
||||
|
|
|
@ -226,6 +226,45 @@
|
|||
Done
|
||||
</pre>
|
||||
|
||||
|
||||
|
||||
<h3 id="session-video-url-api">Set session video URL</h3>
|
||||
|
||||
<p>
|
||||
|
||||
This interface is intended for Meetecho, to provide a way to set the
|
||||
URL of a video recording for a given session. It is available at
|
||||
<code>{% url 'ietf.meeting.views.api_set_session_video_url' %}</code>.
|
||||
Access is limited to recording managers.
|
||||
|
||||
</p>
|
||||
<p>
|
||||
The interface requires the use of a personal API key, which can be created at
|
||||
<a href="{% url 'ietf.ietfauth.views.apikey_index' %}">{% url 'ietf.ietfauth.views.apikey_index' %}</a>
|
||||
</p>
|
||||
<p>
|
||||
The ballot position API takes the following parameters:
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>apikey</code> (required) which is the personal API key hash</li>
|
||||
<li><code>meeting</code> (required) which is the meeting number</li>
|
||||
<li><code>group</code> (required) which is the group acronym</li>
|
||||
<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>
|
||||
<li><code>url</code> (required) which is the url that points to the video recording</li>
|
||||
</ul>
|
||||
<p>
|
||||
It returns an appropriate http result code, and a brief explanatory text message.
|
||||
</p>
|
||||
<p>
|
||||
Here is an example:</li>
|
||||
</p>
|
||||
<pre>
|
||||
$ 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
|
||||
Done
|
||||
</pre>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue