Merged in [13485] from rcross@amsl.com:
Add import of YouTube session videos using YouTube Data API. Fixes #2249.
- Legacy-Id: 13509
Note: SVN reference [13485] has been migrated to Git commit 7ee2a16967
This commit is contained in:
commit
d3decdcb61
|
@ -2,6 +2,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
|
@ -23,7 +24,12 @@ OMITTED_APPS = (
|
||||||
)
|
)
|
||||||
|
|
||||||
class CustomApiTestCase(TestCase):
|
class CustomApiTestCase(TestCase):
|
||||||
def test_notify_meeting_import_audio_files(self):
|
|
||||||
|
# Using mock to patch the import functions in ietf.meeting.views, where
|
||||||
|
# api_import_recordings() are using them:
|
||||||
|
@patch('ietf.meeting.views.import_audio_files')
|
||||||
|
@patch('ietf.meeting.views.import_youtube_video_urls')
|
||||||
|
def test_notify_meeting_import_audio_files(self, mock_import_youtube, mock_import_audio):
|
||||||
meeting = make_meeting_test_data()
|
meeting = make_meeting_test_data()
|
||||||
client = Client(Accept='application/json')
|
client = Client(Accept='application/json')
|
||||||
# try invalid method GET
|
# try invalid method GET
|
||||||
|
|
|
@ -80,13 +80,14 @@ class InterimSessionInlineFormSet(BaseInlineFormSet):
|
||||||
if date:
|
if date:
|
||||||
dates.append(date)
|
dates.append(date)
|
||||||
if len(dates) < 2:
|
if len(dates) < 2:
|
||||||
return
|
return self.cleaned_data
|
||||||
dates.sort()
|
dates.sort()
|
||||||
last_date = dates[0]
|
last_date = dates[0]
|
||||||
for date in dates[1:]:
|
for date in dates[1:]:
|
||||||
if date - last_date != datetime.timedelta(days=1):
|
if date - last_date != datetime.timedelta(days=1):
|
||||||
raise forms.ValidationError('For Multi-Day meetings, days must be consecutive')
|
raise forms.ValidationError('For Multi-Day meetings, days must be consecutive')
|
||||||
last_date = date
|
last_date = date
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
class InterimMeetingModelForm(forms.ModelForm):
|
class InterimMeetingModelForm(forms.ModelForm):
|
||||||
group = GroupModelChoiceField(queryset=Group.objects.filter(type__in=('wg', 'rg'), state__in=('active', 'proposed', 'bof')).order_by('acronym'), required=False)
|
group = GroupModelChoiceField(queryset=Group.objects.filter(type__in=('wg', 'rg'), state__in=('active', 'proposed', 'bof')).order_by('acronym'), required=False)
|
||||||
|
|
|
@ -987,7 +987,6 @@ class InterimTests(TestCase):
|
||||||
'session_set-INITIAL_FORMS':0}
|
'session_set-INITIAL_FORMS':0}
|
||||||
|
|
||||||
r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data)
|
r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data)
|
||||||
|
|
||||||
self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming'))
|
self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming'))
|
||||||
meeting = Meeting.objects.order_by('id').last()
|
meeting = Meeting.objects.order_by('id').last()
|
||||||
self.assertEqual(meeting.type_id,'interim')
|
self.assertEqual(meeting.type_id,'interim')
|
||||||
|
|
|
@ -58,7 +58,8 @@ from ietf.meeting.helpers import send_interim_approval_request
|
||||||
from ietf.meeting.helpers import send_interim_announcement_request
|
from ietf.meeting.helpers import send_interim_announcement_request
|
||||||
from ietf.meeting.utils import finalize
|
from ietf.meeting.utils import finalize
|
||||||
from ietf.secr.proceedings.utils import handle_upload_file
|
from ietf.secr.proceedings.utils import handle_upload_file
|
||||||
from ietf.secr.proceedings.proc_utils import get_progress_stats, post_process, import_audio_files
|
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
|
||||||
|
import_youtube_video_urls)
|
||||||
from ietf.utils import log
|
from ietf.utils import log
|
||||||
from ietf.utils.mail import send_mail_message
|
from ietf.utils.mail import send_mail_message
|
||||||
from ietf.utils.pipe import pipe
|
from ietf.utils.pipe import pipe
|
||||||
|
@ -2189,6 +2190,7 @@ def api_import_recordings(request, number):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
meeting = get_meeting(number)
|
meeting = get_meeting(number)
|
||||||
import_audio_files(meeting)
|
import_audio_files(meeting)
|
||||||
|
import_youtube_video_urls(meeting)
|
||||||
return HttpResponse(status=201)
|
return HttpResponse(status=201)
|
||||||
else:
|
else:
|
||||||
return HttpResponse(status=405)
|
return HttpResponse(status=405)
|
||||||
|
|
|
@ -3,16 +3,19 @@ proc_utils.py
|
||||||
|
|
||||||
This module contains all the functions for generating static proceedings pages
|
This module contains all the functions for generating static proceedings pages
|
||||||
'''
|
'''
|
||||||
from urllib2 import urlopen
|
|
||||||
import datetime
|
import datetime
|
||||||
import glob
|
import glob
|
||||||
|
import httplib2
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import urllib2
|
||||||
|
from urllib import urlencode
|
||||||
|
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
|
from apiclient.discovery import build
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
@ -34,10 +37,84 @@ from ietf.utils.log import log
|
||||||
from ietf.utils.mail import send_mail
|
from ietf.utils.mail import send_mail
|
||||||
|
|
||||||
AUDIO_FILE_RE = re.compile(r'ietf(?P<number>[\d]+)-(?P<room>.*)-(?P<time>[\d]{8}-[\d]{4})')
|
AUDIO_FILE_RE = re.compile(r'ietf(?P<number>[\d]+)-(?P<room>.*)-(?P<time>[\d]{8}-[\d]{4})')
|
||||||
|
VIDEO_TITLE_RE = re.compile(r'IETF(?P<number>\d{2})-(?P<name>.*)-(?P<date>\d{8})-(?P<time>\d{4})')
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Helper Functions
|
# Recording Functions
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
|
|
||||||
|
def import_youtube_video_urls(meeting, http=httplib2.Http()):
|
||||||
|
'''Create Document and set external_url for session videos'''
|
||||||
|
youtube = build(settings.YOUTUBE_API_SERVICE_NAME, settings.YOUTUBE_API_VERSION,
|
||||||
|
developerKey=settings.YOUTUBE_API_KEY, http=http)
|
||||||
|
playlistid = get_youtube_playlistid(youtube, 'IETF' + meeting.number)
|
||||||
|
if playlistid is None:
|
||||||
|
return None
|
||||||
|
for video in get_youtube_videos(youtube, playlistid):
|
||||||
|
match = VIDEO_TITLE_RE.match(video['title'])
|
||||||
|
if match:
|
||||||
|
session = _get_session(**match.groupdict())
|
||||||
|
if session:
|
||||||
|
url = video['url']
|
||||||
|
get_or_create_recording_document(url,session)
|
||||||
|
|
||||||
|
def get_youtube_playlistid(youtube, title, http=httplib2.Http()):
|
||||||
|
'''Returns the youtube playlistId matching title string, a string'''
|
||||||
|
request = youtube.search().list(
|
||||||
|
q=title,
|
||||||
|
part='id,snippet',
|
||||||
|
channelId=settings.YOUTUBE_IETF_CHANNEL_ID,
|
||||||
|
type='playlist',
|
||||||
|
maxResults=1
|
||||||
|
)
|
||||||
|
search_response = request.execute(http=http)
|
||||||
|
|
||||||
|
try:
|
||||||
|
playlistid = search_response['items'][0]['id']['playlistId']
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
return None
|
||||||
|
return playlistid
|
||||||
|
|
||||||
|
def get_youtube_videos(youtube, playlistid, http=httplib2.Http()):
|
||||||
|
'''Returns list of dictionaries with title, urls keys'''
|
||||||
|
videos = []
|
||||||
|
kwargs = dict(part="snippet",playlistId=playlistid,maxResults=50)
|
||||||
|
playlistitems = youtube.playlistItems()
|
||||||
|
request = playlistitems.list(**kwargs)
|
||||||
|
# handle pagination
|
||||||
|
while request is not None:
|
||||||
|
playlistitems_doc = request.execute(http=http)
|
||||||
|
videos.extend(_get_urls_from_json(playlistitems_doc))
|
||||||
|
request = playlistitems.list_next(request, playlistitems_doc)
|
||||||
|
return videos
|
||||||
|
|
||||||
|
def _get_session(number,name,date,time):
|
||||||
|
'''Lookup session using data from video title'''
|
||||||
|
meeting = Meeting.objects.get(number=number)
|
||||||
|
schedule = meeting.agenda
|
||||||
|
timeslot_time = datetime.datetime.strptime(date + time,'%Y%m%d%H%M')
|
||||||
|
try:
|
||||||
|
assignment = SchedTimeSessAssignment.objects.get(
|
||||||
|
schedule = schedule,
|
||||||
|
session__group__acronym = name.lower(),
|
||||||
|
timeslot__time = timeslot_time,
|
||||||
|
)
|
||||||
|
except (SchedTimeSessAssignment.DoesNotExist, SchedTimeSessAssignment.MultipleObjectsReturned):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return assignment.session
|
||||||
|
|
||||||
|
def _get_urls_from_json(doc):
|
||||||
|
'''Returns list of dictonary titel,url from search results'''
|
||||||
|
urls = []
|
||||||
|
for item in doc['items']:
|
||||||
|
title = item['snippet']['title']
|
||||||
|
#params = dict(v=item['snippet']['resourceId']['videoId'], list=item['snippet']['playlistId'])
|
||||||
|
params = [('v',item['snippet']['resourceId']['videoId']), ('list',item['snippet']['playlistId'])]
|
||||||
|
url = settings.YOUTUBE_BASE_URL + '?' + urlencode(params)
|
||||||
|
urls.append(dict(title=title, url=url))
|
||||||
|
return urls
|
||||||
|
|
||||||
def import_audio_files(meeting):
|
def import_audio_files(meeting):
|
||||||
'''
|
'''
|
||||||
Checks for audio files and creates corresponding materials (docs) for the Session
|
Checks for audio files and creates corresponding materials (docs) for the Session
|
||||||
|
@ -58,20 +135,9 @@ def import_audio_files(meeting):
|
||||||
).exclude(session__agenda_note__icontains='canceled').order_by('timeslot__time')
|
).exclude(session__agenda_note__icontains='canceled').order_by('timeslot__time')
|
||||||
if not sessionassignments:
|
if not sessionassignments:
|
||||||
continue
|
continue
|
||||||
doc = get_or_create_recording_document(filename,sessionassignments[0].session)
|
url = settings.IETF_AUDIO_URL + 'ietf{}/{}'.format(meeting.number, filename)
|
||||||
for sessionassignment in sessionassignments:
|
doc = get_or_create_recording_document(url,sessionassignments[0].session)
|
||||||
session = sessionassignment.session
|
attach_recording(doc, [ x.session for x in sessionassignments ])
|
||||||
if doc not in session.materials.all():
|
|
||||||
# add document to session
|
|
||||||
presentation = SessionPresentation.objects.create(
|
|
||||||
session=session,
|
|
||||||
document=doc,
|
|
||||||
rev=doc.rev)
|
|
||||||
session.sessionpresentation_set.add(presentation)
|
|
||||||
if not doc.docalias_set.filter(name__startswith='recording-{}-{}'.format(meeting.number,session.group.acronym)):
|
|
||||||
sequence = get_next_sequence(session.group,session.meeting,'recording')
|
|
||||||
name = 'recording-{}-{}-{}'.format(session.meeting.number,session.group.acronym,sequence)
|
|
||||||
doc.docalias_set.create(name=name)
|
|
||||||
else:
|
else:
|
||||||
# use for reconciliation email
|
# use for reconciliation email
|
||||||
unmatched_files.append(filename)
|
unmatched_files.append(filename)
|
||||||
|
@ -98,20 +164,30 @@ def get_timeslot_for_filename(filename):
|
||||||
except (ObjectDoesNotExist, KeyError):
|
except (ObjectDoesNotExist, KeyError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def attach_recording(doc, sessions):
|
||||||
|
'''Associate recording document with sessions'''
|
||||||
|
for session in sessions:
|
||||||
|
if doc not in session.materials.all():
|
||||||
|
# add document to session
|
||||||
|
presentation = SessionPresentation.objects.create(
|
||||||
|
session=session,
|
||||||
|
document=doc,
|
||||||
|
rev=doc.rev)
|
||||||
|
session.sessionpresentation_set.add(presentation)
|
||||||
|
if not doc.docalias_set.filter(name__startswith='recording-{}-{}'.format(session.meeting.number,session.group.acronym)):
|
||||||
|
sequence = get_next_sequence(session.group,session.meeting,'recording')
|
||||||
|
name = 'recording-{}-{}-{}'.format(session.meeting.number,session.group.acronym,sequence)
|
||||||
|
doc.docalias_set.create(name=name)
|
||||||
|
|
||||||
def normalize_room_name(name):
|
def normalize_room_name(name):
|
||||||
'''Returns room name converted to be used as portion of filename'''
|
'''Returns room name converted to be used as portion of filename'''
|
||||||
return name.lower().replace(' ','').replace('/','_')
|
return name.lower().replace(' ','').replace('/','_')
|
||||||
|
|
||||||
def get_or_create_recording_document(filename,session):
|
def get_or_create_recording_document(url,session):
|
||||||
meeting = session.meeting
|
|
||||||
url = settings.IETF_AUDIO_URL + 'ietf{}/{}'.format(meeting.number, filename)
|
|
||||||
try:
|
try:
|
||||||
doc = Document.objects.get(external_url=url)
|
return Document.objects.get(external_url=url)
|
||||||
return doc
|
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
pass
|
return create_recording(session,url)
|
||||||
return create_recording(session,url)
|
|
||||||
|
|
||||||
|
|
||||||
def create_recording(session,url):
|
def create_recording(session,url):
|
||||||
'''
|
'''
|
||||||
|
@ -182,6 +258,10 @@ def mycomp(timeslot):
|
||||||
key = None
|
key = None
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# End Recording Functions
|
||||||
|
# -------------------------------------------------
|
||||||
|
|
||||||
def get_progress_stats(sdate,edate):
|
def get_progress_stats(sdate,edate):
|
||||||
'''
|
'''
|
||||||
This function takes a date range and produces a dictionary of statistics / objects for
|
This function takes a date range and produces a dictionary of statistics / objects for
|
||||||
|
@ -489,7 +569,7 @@ def gen_agenda(context):
|
||||||
|
|
||||||
# get the text agenda from datatracker
|
# get the text agenda from datatracker
|
||||||
url = 'https://datatracker.ietf.org/meeting/%s/agenda.txt' % meeting.number
|
url = 'https://datatracker.ietf.org/meeting/%s/agenda.txt' % meeting.number
|
||||||
text = urlopen(url).read()
|
text = urllib2.urlopen(url).read()
|
||||||
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'agenda.txt')
|
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'agenda.txt')
|
||||||
write_html(path,text)
|
write_html(path,text)
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,28 @@
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
from apiclient.discovery import build
|
||||||
|
from apiclient.http import HttpMock
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from ietf.doc.models import Document
|
from ietf.doc.models import Document
|
||||||
from ietf.group.models import Group
|
from ietf.group.models import Group
|
||||||
|
from ietf.meeting.factories import SessionFactory
|
||||||
from ietf.meeting.models import Session, TimeSlot, SchedTimeSessAssignment
|
from ietf.meeting.models import Session, TimeSlot, SchedTimeSessAssignment
|
||||||
from ietf.meeting.test_data import make_meeting_test_data
|
from ietf.meeting.test_data import make_meeting_test_data
|
||||||
|
from ietf.name.models import SessionStatusName
|
||||||
from ietf.utils.test_data import make_test_data
|
from ietf.utils.test_data import make_test_data
|
||||||
from ietf.utils.test_utils import TestCase
|
from ietf.utils.test_utils import TestCase
|
||||||
from ietf.utils.mail import outbox
|
from ietf.utils.mail import outbox
|
||||||
|
|
||||||
from ietf.name.models import SessionStatusName
|
|
||||||
from ietf.meeting.factories import SessionFactory
|
|
||||||
|
|
||||||
from ietf.secr.proceedings.proc_utils import (create_proceedings, import_audio_files,
|
from ietf.secr.proceedings.proc_utils import (create_proceedings, import_audio_files,
|
||||||
get_timeslot_for_filename, normalize_room_name, send_audio_import_warning,
|
get_timeslot_for_filename, normalize_room_name, send_audio_import_warning,
|
||||||
get_or_create_recording_document, create_recording, get_next_sequence)
|
get_or_create_recording_document, create_recording, get_next_sequence,
|
||||||
|
get_youtube_playlistid, get_youtube_videos, import_youtube_video_urls,
|
||||||
|
_get_session, _get_urls_from_json)
|
||||||
|
|
||||||
|
|
||||||
SECR_USER='secretary'
|
SECR_USER='secretary'
|
||||||
|
@ -37,6 +41,68 @@ class ProceedingsTestCase(TestCase):
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoRecordingTestCase(TestCase):
|
||||||
|
@patch('ietf.secr.proceedings.proc_utils.get_youtube_videos')
|
||||||
|
@patch('ietf.secr.proceedings.proc_utils.get_youtube_playlistid')
|
||||||
|
def test_import_youtube_video_urls(self, mock_playlistid, mock_videos):
|
||||||
|
meeting = make_meeting_test_data()
|
||||||
|
session = Session.objects.filter(meeting=meeting, group__acronym='mars').first()
|
||||||
|
title = self._get_video_title_for_session(session)
|
||||||
|
url = 'https://youtube.com?v=test'
|
||||||
|
mock_playlistid.return_value = 'PLC86T-6ZTP5g87jdxNqdWV5475U-yEE8M'
|
||||||
|
mock_videos.return_value = [{'title':title,'url':url}]
|
||||||
|
discovery = os.path.join(settings.BASE_DIR, "../test/data/youtube-discovery.json")
|
||||||
|
http = HttpMock(discovery, {'status': '200'})
|
||||||
|
import_youtube_video_urls(meeting=meeting, http=http)
|
||||||
|
doc = Document.objects.get(external_url=url)
|
||||||
|
self.assertTrue(doc in session.materials.all())
|
||||||
|
|
||||||
|
def _get_video_title_for_session(self, session):
|
||||||
|
'''Returns the youtube video title of a session recording given session'''
|
||||||
|
timeslot = session.official_timeslotassignment().timeslot
|
||||||
|
return "{prefix}-{group}-{date}".format(
|
||||||
|
prefix=session.meeting.type.slug + session.meeting.number,
|
||||||
|
group=session.group.acronym,
|
||||||
|
date=timeslot.time.strftime('%Y%m%d-%H%M')).upper()
|
||||||
|
|
||||||
|
def test_get_youtube_playlistid(self):
|
||||||
|
discovery = os.path.join(settings.BASE_DIR, "../test/data/youtube-discovery.json")
|
||||||
|
http = HttpMock(discovery, {'status': '200'})
|
||||||
|
youtube = build(settings.YOUTUBE_API_SERVICE_NAME, settings.YOUTUBE_API_VERSION,
|
||||||
|
developerKey='',http=http)
|
||||||
|
path = os.path.join(settings.BASE_DIR, "../test/data/youtube-playlistid.json")
|
||||||
|
http = HttpMock(path, {'status': '200'})
|
||||||
|
self.assertEqual(get_youtube_playlistid(youtube, 'IETF98', http=http),'PLC86T-test')
|
||||||
|
|
||||||
|
def test_get_youtube_videos(self):
|
||||||
|
discovery = os.path.join(settings.BASE_DIR, "../test/data/youtube-discovery.json")
|
||||||
|
http = HttpMock(discovery, {'status': '200'})
|
||||||
|
youtube = build(settings.YOUTUBE_API_SERVICE_NAME, settings.YOUTUBE_API_VERSION,
|
||||||
|
developerKey='',http=http)
|
||||||
|
path = os.path.join(settings.BASE_DIR, "../test/data/youtube-playlistitems.json")
|
||||||
|
http = HttpMock(path, {'status': '200'})
|
||||||
|
videos = get_youtube_videos(youtube, 'PLC86T', http=http)
|
||||||
|
self.assertEqual(len(videos),2)
|
||||||
|
|
||||||
|
def test_get_session(self):
|
||||||
|
meeting = make_meeting_test_data()
|
||||||
|
session = Session.objects.filter(meeting=meeting, group__acronym='mars').first()
|
||||||
|
number = meeting.number
|
||||||
|
name = session.group.acronym
|
||||||
|
date = session.official_timeslotassignment().timeslot.time.strftime('%Y%m%d')
|
||||||
|
time = session.official_timeslotassignment().timeslot.time.strftime('%H%M')
|
||||||
|
self.assertEqual(_get_session(number,name,date,time),session)
|
||||||
|
|
||||||
|
def test_get_urls_from_json(self):
|
||||||
|
path = os.path.join(settings.BASE_DIR, "../test/data/youtube-playlistitems.json")
|
||||||
|
with open(path) as f:
|
||||||
|
doc = json.load(f)
|
||||||
|
urls = _get_urls_from_json(doc)
|
||||||
|
self.assertEqual(len(urls),2)
|
||||||
|
self.assertEqual(urls[0]['title'],'IETF98 Wrap Up')
|
||||||
|
self.assertEqual(urls[0]['url'],'https://www.youtube.com/watch?v=lhYWB5FFkg4&list=PLC86T-6ZTP5jo6kIuqdyeYYhsKv9sUwG1')
|
||||||
|
|
||||||
class RecordingTestCase(TestCase):
|
class RecordingTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.meeting_recordings_dir = self.tempdir('meeting-recordings')
|
self.meeting_recordings_dir = self.tempdir('meeting-recordings')
|
||||||
|
|
|
@ -289,7 +289,8 @@ def recording(request, meeting_num):
|
||||||
session.
|
session.
|
||||||
'''
|
'''
|
||||||
meeting = get_object_or_404(Meeting, number=meeting_num)
|
meeting = get_object_or_404(Meeting, number=meeting_num)
|
||||||
sessions = meeting.session_set.filter(type__in=('session','plenary','other'),status='sched').order_by('group__acronym')
|
assignments = meeting.agenda.assignments.exclude(session__type__in=('reg','break')).order_by('session__group__acronym')
|
||||||
|
sessions = [ x.session for x in assignments ]
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = RecordingForm(request.POST,meeting=meeting)
|
form = RecordingForm(request.POST,meeting=meeting)
|
||||||
|
|
|
@ -733,6 +733,11 @@ SECR_PPT2PDF_COMMAND = ['/usr/bin/soffice','--headless','--convert-to','pdf','--
|
||||||
REGISTRATION_ATTENDEES_BASE_URL = 'https://ietf.org/registration/attendees/'
|
REGISTRATION_ATTENDEES_BASE_URL = 'https://ietf.org/registration/attendees/'
|
||||||
NEW_PROCEEDINGS_START = 95
|
NEW_PROCEEDINGS_START = 95
|
||||||
USE_ETAGS=True
|
USE_ETAGS=True
|
||||||
|
YOUTUBE_API_KEY = ''
|
||||||
|
YOUTUBE_API_SERVICE_NAME = 'youtube'
|
||||||
|
YOUTUBE_API_VERSION = 'v3'
|
||||||
|
YOUTUBE_BASE_URL = 'https://www.youtube.com/watch'
|
||||||
|
YOUTUBE_IETF_CHANNEL_ID = 'UC8dtK9njBLdFnBahHFp0eZQ'
|
||||||
|
|
||||||
PRODUCTION_TIMEZONE = "America/Los_Angeles"
|
PRODUCTION_TIMEZONE = "America/Los_Angeles"
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,11 @@ django-tastypie>=0.13.2
|
||||||
django-widget-tweaks>=1.3
|
django-widget-tweaks>=1.3
|
||||||
docutils>=0.12
|
docutils>=0.12
|
||||||
factory-boy>=2.8.1
|
factory-boy>=2.8.1
|
||||||
|
google-api-python-client
|
||||||
# Faker # from factory-boy
|
# Faker # from factory-boy
|
||||||
hashids>=1.1.0
|
hashids>=1.1.0
|
||||||
html5lib>=0.90,<0.99999999 # ietf.utils.html needs a rewrite for html5lib 1.x -- major code changes in sanitizer
|
html5lib>=0.90,<0.99999999 # ietf.utils.html needs a rewrite for html5lib 1.x -- major code changes in sanitizer
|
||||||
|
httplib2>=0.10.3
|
||||||
jsonfield>=1.0.3 # for SubmissionCheck. This is https://github.com/bradjasper/django-jsonfield/.
|
jsonfield>=1.0.3 # for SubmissionCheck. This is https://github.com/bradjasper/django-jsonfield/.
|
||||||
#lxml>=3.4.0 # from PyQuery;
|
#lxml>=3.4.0 # from PyQuery;
|
||||||
mimeparse>=0.1.3 # from TastyPie
|
mimeparse>=0.1.3 # from TastyPie
|
||||||
|
|
10879
test/data/youtube-discovery.json
Normal file
10879
test/data/youtube-discovery.json
Normal file
File diff suppressed because it is too large
Load diff
1
test/data/youtube-playlistid.json
Normal file
1
test/data/youtube-playlistid.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"regionCode": "US", "kind": "youtube#searchListResponse", "etag": "\"m2yskBQFythfE4irbTIeOgYYfBU/eoV8llUEbIu5LXnwqBaLOkOK0Hg\"", "pageInfo": {"resultsPerPage": 1, "totalResults": 1}, "items": [{"snippet": {"thumbnails": {"default": {"url": "https://i.ytimg.com/vi/lhYWB5FFkg4/default.jpg", "width": 120, "height": 90}, "high": {"url": "https://i.ytimg.com/vi/lhYWB5FFkg4/hqdefault.jpg", "width": 480, "height": 360}, "medium": {"url": "https://i.ytimg.com/vi/lhYWB5FFkg4/mqdefault.jpg", "width": 320, "height": 180}}, "title": "IETF98", "channelId": "UC8dtK9njBLdFnBahHFp0eZQ", "publishedAt": "2017-03-30T12:41:04.000Z", "liveBroadcastContent": "none", "channelTitle": "IETF - Internet Engineering Task Force", "description": "Videos from the IETF 98 Meeting held in Chicago, Illinois, United States 26-31 March 2017."}, "kind": "youtube#searchResult", "etag": "\"m2yskBQFythfE4irbTIeOgYYfBU/X3dbZGRvgpvedtOP0KLGhZLg5UI\"", "id": {"kind": "youtube#playlist", "playlistId": "PLC86T-test"}}]}
|
1
test/data/youtube-playlistitems.json
Normal file
1
test/data/youtube-playlistitems.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"items": [{"snippet": {"playlistId": "PLC86T-6ZTP5jo6kIuqdyeYYhsKv9sUwG1", "thumbnails": {"default": {"url": "https://i.ytimg.com/vi/lhYWB5FFkg4/default.jpg", "width": 120, "height": 90}, "high": {"url": "https://i.ytimg.com/vi/lhYWB5FFkg4/hqdefault.jpg", "width": 480, "height": 360}, "medium": {"url": "https://i.ytimg.com/vi/lhYWB5FFkg4/mqdefault.jpg", "width": 320, "height": 180}, "maxres": {"url": "https://i.ytimg.com/vi/lhYWB5FFkg4/maxresdefault.jpg", "width": 1280, "height": 720}, "standard": {"url": "https://i.ytimg.com/vi/lhYWB5FFkg4/sddefault.jpg", "width": 640, "height": 480}}, "title": "IETF98 Wrap Up", "resourceId": {"kind": "youtube#video", "videoId": "lhYWB5FFkg4"}, "channelId": "UC8dtK9njBLdFnBahHFp0eZQ", "publishedAt": "2017-04-06T13:32:39.000Z", "channelTitle": "IETF - Internet Engineering Task Force", "position": 0, "description": "Jari Arkko and Alissa Cooper recap some highlights the IETF 98 meeting held 26-31 March 2017 in Chicago, Illinois, United States"}, "kind": "youtube#playlistItem", "etag": "\"m2yskBQFythfE4irbTIeOgYYfBU/eW_De3gQF2fRzN_rPBbX-kY7oBI\"", "id": "UExDODZULTZaVFA1am82a0l1cWR5ZVlZaHNLdjlzVXdHMS40OTQ5QjlEMDgzN0FBNUIw"}, {"snippet": {"playlistId": "PLC86T-6ZTP5jo6kIuqdyeYYhsKv9sUwG1", "thumbnails": {"default": {"url": "https://i.ytimg.com/vi/lPSTcBITbvs/default.jpg", "width": 120, "height": 90}, "high": {"url": "https://i.ytimg.com/vi/lPSTcBITbvs/hqdefault.jpg", "width": 480, "height": 360}, "medium": {"url": "https://i.ytimg.com/vi/lPSTcBITbvs/mqdefault.jpg", "width": 320, "height": 180}}, "title": "IETF 98 - QUIC Tutorial", "resourceId": {"kind": "youtube#video", "videoId": "lPSTcBITbvs"}, "channelId": "UC8dtK9njBLdFnBahHFp0eZQ", "publishedAt": "2017-03-30T12:41:35.000Z", "channelTitle": "IETF - Internet Engineering Task Force", "position": 1, "description": "A tutorial about the new QUIC protocol"}, "kind": "youtube#playlistItem", "etag": "\"m2yskBQFythfE4irbTIeOgYYfBU/GhKVt6zTuEpFavgtf9GWlWuzX9s\"", "id": "UExDODZULTZaVFA1am82a0l1cWR5ZVlZaHNLdjlzVXdHMS41NkI0NEY2RDEwNTU3Q0M2"}], "kind": "youtube#playlistItemListResponse", "etag": "\"m2yskBQFythfE4irbTIeOgYYfBU/jlFue-jZVpFMOuLUXQZH4Y0Lh3Y\"", "pageInfo": {"resultsPerPage": 2, "totalResults": 110}}
|
Loading…
Reference in a new issue