Skip to content

Commit 6e38e97

Browse files
committed
Merged in [8237] from rcross@amsl.com:
Changes to support video content in proceedings ('recording' document type). - Legacy-Id: 8462 Note: SVN reference [8237] has been migrated to Git commit 96bccc7
3 parents 28f8b30 + faa6469 + feec689 commit 6e38e97

16 files changed

Lines changed: 448 additions & 65 deletions

File tree

ietf/doc/models.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import datetime, os
44

55
from django.db import models
6+
from django.core.exceptions import ValidationError
67
from django.core.urlresolvers import reverse as urlreverse
8+
from django.core.validators import URLValidator
79
from django.contrib.contenttypes.models import ContentType
810
from django.conf import settings
911
from django.utils.html import mark_safe
@@ -90,6 +92,19 @@ def get_file_path(self):
9092
return settings.DOCUMENT_PATH_PATTERN.format(doc=self)
9193

9294
def href(self):
95+
# If self.external_url truly is an url, use it. This is a change from
96+
# the earlier resulution order, but there's at the moment one single
97+
# instance which matches this (with correct results), so we won't
98+
# break things all over the place.
99+
# XXX TODO: move all non-URL 'external_url' values into a field which is
100+
# better named, or regularize the filename based on self.name
101+
validator = URLValidator()
102+
try:
103+
validator(self.external_url)
104+
return self.external_url
105+
except ValidationError:
106+
pass
107+
93108
meeting_related = self.meeting_related()
94109

95110
settings_var = settings.DOC_HREFS

ietf/secr/proceedings/forms.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
from django.template.defaultfilters import filesizeformat
66

77
from ietf.doc.models import Document
8+
from ietf.group.models import Group
89
from ietf.name.models import DocTypeName
9-
from ietf.meeting.models import Meeting
10-
10+
from ietf.meeting.models import Meeting, Session
1111

1212

1313
# ---------------------------------------------
@@ -22,6 +22,14 @@
2222
# Forms
2323
#----------------------------------------------------------
2424

25+
class AjaxChoiceField(forms.ChoiceField):
26+
'''
27+
Special ChoiceField to use when populating options with Ajax. The submitted value
28+
is not in the initial choices list so we need to override valid_value().
29+
'''
30+
def valid_value(self, value):
31+
return True
32+
2533
class EditSlideForm(forms.ModelForm):
2634
class Meta:
2735
model = Document
@@ -42,6 +50,37 @@ def clean(self):
4250
raise forms.ValidationError('A meeting already exists for this date.')
4351
return cleaned_data
4452

53+
class RecordingForm(forms.Form):
54+
group = forms.CharField(max_length=40)
55+
external_url = forms.URLField(label='Url')
56+
session = AjaxChoiceField(choices=(('','----'),))
57+
58+
def clean_session(self):
59+
'''
60+
Emulate ModelChoiceField functionality
61+
'''
62+
id = self.cleaned_data.get('session')
63+
try:
64+
return Session.objects.get(id=id)
65+
except Session.DoesNotExist:
66+
raise forms.ValidationError('Invalid Session')
67+
68+
def clean_group(self):
69+
acronym = self.cleaned_data.get('group')
70+
try:
71+
return Group.objects.get(acronym=acronym)
72+
except Group.DoesNotExist:
73+
raise forms.ValidationError('Invalid group name')
74+
75+
class RecordingEditForm(forms.ModelForm):
76+
class Meta:
77+
model = Document
78+
fields = ['external_url']
79+
80+
def __init__(self, *args, **kwargs):
81+
super(RecordingEditForm, self).__init__(*args, **kwargs)
82+
self.fields['external_url'].label='Url'
83+
4584
class ReplaceSlideForm(forms.ModelForm):
4685
file = forms.FileField(label='Select File')
4786

ietf/secr/proceedings/proc_utils.py

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,74 @@
1212
from django.conf import settings
1313
from django.shortcuts import render_to_response
1414

15-
from ietf.doc.models import Document, RelatedDocument, DocEvent
15+
from ietf.doc.models import Document, RelatedDocument, DocEvent, NewRevisionDocEvent, State
1616
from ietf.group.models import Group, Role
1717
from ietf.group.utils import get_charter_text
1818
from ietf.meeting.helpers import get_schedule
1919
from ietf.meeting.models import Session, Meeting, ScheduledSession
20+
from ietf.person.models import Person
2021
from ietf.secr.proceedings.models import InterimMeeting # proxy model
2122
from ietf.secr.proceedings.models import Registration
2223
from ietf.secr.utils.document import get_rfc_num
2324
from ietf.secr.utils.group import groups_by_session
24-
from ietf.secr.utils.meeting import get_upload_root, get_proceedings_path, get_material, get_session
25-
25+
from ietf.secr.utils.meeting import get_upload_root, get_proceedings_path, get_materials, get_session
2626

2727

2828
# -------------------------------------------------
2929
# Helper Functions
3030
# -------------------------------------------------
31+
def check_audio_files(group,meeting):
32+
'''
33+
Checks for audio files and creates corresponding materials (docs) for the Session
34+
Expects audio files in the format ietf[meeting num]-[room]-YYYMMDD-HHMM-*,
35+
36+
Example: ietf90-salonb-20140721-1710-pm3.mp3
37+
38+
'''
39+
for session in Session.objects.filter(group=group,meeting=meeting,status__in=('sched','schedw')):
40+
timeslot = session.official_scheduledsession().timeslot
41+
room = timeslot.location.name.lower()
42+
room = room.replace(' ','')
43+
room = room.replace('/','')
44+
time = timeslot.time.strftime("%Y%m%d-%H%M")
45+
filename = 'ietf{}-{}-{}-*'.format(meeting.number,room,time)
46+
path = os.path.join(settings.MEETING_RECORDINGS_DIR,'ietf{}'.format(meeting.number),filename)
47+
for file in glob.glob(path):
48+
url = 'http://www.ietf.org/audio/ietf{}/{}'.format(meeting.number,os.path.basename(file))
49+
doc = Document.objects.filter(external_url=url).first()
50+
if not doc:
51+
create_recording(session,meeting,group,url)
52+
53+
def create_recording(session,meeting,group,url):
54+
'''
55+
Creates the Document type=recording, setting external_url and creating
56+
NewRevisionDocEvent
57+
'''
58+
sequence = get_next_sequence(group,meeting,'recording')
59+
name = 'recording-{}-{}-{}'.format(meeting.number,group.acronym,sequence)
60+
time = session.official_scheduledsession().timeslot.time.strftime('%Y-%m-%d %H:%M')
61+
if url.endswith('mp3'):
62+
title = 'Audio recording for {}'.format(time)
63+
else:
64+
title = 'Video recording for {}'.format(time)
65+
66+
doc = Document.objects.create(name=name,
67+
title=title,
68+
external_url=url,
69+
group=group,
70+
rev='00',
71+
type_id='recording')
72+
doc.set_state(State.objects.get(type='recording', slug='active'))
73+
74+
# create DocEvent
75+
NewRevisionDocEvent.objects.create(type='new_revision',
76+
by=Person.objects.get(name='(System)'),
77+
doc=doc,
78+
rev=doc.rev,
79+
desc='New revision available',
80+
time=doc.time)
81+
session.materials.add(doc)
82+
3183
def mycomp(timeslot):
3284
'''
3385
This takes a timeslot object and returns a key to sort by the area acronym or None
@@ -141,6 +193,13 @@ def get_progress_stats(sdate,edate):
141193

142194
return data
143195

196+
def get_next_sequence(group,meeting,type):
197+
'''
198+
Returns the next sequence number to use for a document of type = type.
199+
Takes a group=Group object, meeting=Meeting object, type = string
200+
'''
201+
return Document.objects.filter(name__startswith='{}-{}-{}-'.format(type,meeting.number,group.acronym)).count() + 1
202+
144203
def write_html(path,content):
145204
f = open(path,'w')
146205
f.write(content)
@@ -188,14 +247,8 @@ def create_proceedings(meeting, group, is_final=False):
188247
if meeting.type_id == 'ietf' and int(meeting.number) < 79:
189248
return
190249

191-
sessions = Session.objects.filter(meeting=meeting,group=group)
192-
if sessions:
193-
session = sessions[0]
194-
agenda,minutes,slides = get_material(session)
195-
else:
196-
agenda = None
197-
minutes = None
198-
slides = None
250+
check_audio_files(group,meeting)
251+
materials = get_materials(group,meeting)
199252

200253
chairs = group.role_set.filter(name='chair')
201254
secretaries = group.role_set.filter(name='secr')
@@ -215,7 +268,7 @@ def create_proceedings(meeting, group, is_final=False):
215268
settings.MEDIA_URL,
216269
meeting.date.strftime('%Y/%m/%d'),
217270
group.acronym)
218-
271+
219272
# Only do these tasks if we are running official proceedings generation,
220273
# otherwise skip them for expediency. This procedure is called any time meeting
221274
# materials are uploaded/deleted, and we don't want to do all this work each time.
@@ -313,9 +366,7 @@ def create_proceedings(meeting, group, is_final=False):
313366
'tas': tas,
314367
'meeting': meeting,
315368
'rfcs': rfcs,
316-
'slides': slides,
317-
'minutes': minutes,
318-
'agenda': agenda}
369+
'materials': materials}
319370
)
320371

321372
# save proceedings

ietf/secr/proceedings/tests.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
from django.core.urlresolvers import reverse
44

5-
from ietf.utils.test_utils import TestCase
6-
from ietf.meeting.models import Meeting
5+
from ietf.group.models import Group
6+
from ietf.meeting.models import Meeting, Session
7+
from ietf.meeting.test_data import make_meeting_test_data
78
from ietf.utils.test_data import make_test_data
8-
9+
from ietf.utils.test_utils import TestCase
910

1011
SECR_USER='secretary'
1112

@@ -18,11 +19,30 @@ def test_main(self):
1819
response = self.client.get(url)
1920
self.assertEqual(response.status_code, 200)
2021

21-
def test_view(self):
22-
"View Test"
22+
class RecordingTestCase(TestCase):
23+
def test_page(self):
2324
make_test_data()
24-
meeting = Meeting.objects.all()[0]
25-
url = reverse('meetings_view', kwargs={'meeting_id':meeting.number})
25+
meeting = Meeting.objects.first()
26+
url = reverse('proceedings_recording', kwargs={'meeting_num':meeting.number})
2627
self.client.login(username="secretary", password="secretary+password")
2728
response = self.client.get(url)
2829
self.assertEqual(response.status_code, 200)
30+
def test_post(self):
31+
make_meeting_test_data()
32+
meeting = Meeting.objects.first()
33+
group = Group.objects.get(acronym='mars')
34+
session = Session.objects.filter(meeting=meeting,group=group,status__in=('sched','schedw')).first()
35+
url = reverse('proceedings_recording', kwargs={'meeting_num':meeting.number})
36+
data = dict(group=group.acronym,external_url='http://youtube.com/xyz',session=session.pk)
37+
self.client.login(username="secretary", password="secretary+password")
38+
response = self.client.post(url,data,follow=True)
39+
self.assertEqual(response.status_code, 200)
40+
self.failUnless(group.acronym in response.content)
41+
42+
# now test edit
43+
doc = session.materials.filter(type='recording').first()
44+
external_url = 'http://youtube.com/aaa'
45+
url = reverse('proceedings_recording_edit', kwargs={'meeting_num':meeting.number,'name':doc.name})
46+
response = self.client.post(url,dict(external_url=external_url),follow=True)
47+
self.assertEqual(response.status_code, 200)
48+
self.failUnless(external_url in response.content)

ietf/secr/proceedings/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
urlpatterns = patterns('ietf.secr.proceedings.views',
44
url(r'^$', 'main', name='proceedings'),
55
url(r'^ajax/generate-proceedings/(?P<meeting_num>\d{1,3})/$', 'ajax_generate_proceedings', name='proceedings_ajax_generate_proceedings'),
6+
url(r'^ajax/get-sessions/(?P<meeting_num>\d{1,3})/(?P<acronym>[A-Za-z0-9_\-\+]+)/', 'ajax_get_sessions', name='proceedings_ajax_get_sessions'),
67
url(r'^ajax/order-slide/$', 'ajax_order_slide', name='proceedings_ajax_order_slide'),
78
# special offline URL for testing proceedings build
89
url(r'^build/(?P<meeting_num>\d{1,3}|interim-\d{4}-[A-Za-z0-9_\-\+]+)/(?P<acronym>[A-Za-z0-9_\-\+]+)/$',
@@ -15,6 +16,8 @@
1516
url(r'^progress-report/(?P<meeting_num>\d{1,3})/$', 'progress_report', name='proceedings_progress_report'),
1617
url(r'^replace-slide/(?P<slide_id>[A-Za-z0-9._\-\+]+)/$', 'replace_slide', name='proceedings_replace_slide'),
1718
url(r'^(?P<meeting_num>\d{1,3})/$', 'select', name='proceedings_select'),
19+
url(r'^(?P<meeting_num>\d{1,3})/recording/$', 'recording', name='proceedings_recording'),
20+
url(r'^(?P<meeting_num>\d{1,3})/recording/edit/(?P<name>[A-Za-z0-9_\-\+]+)$', 'recording_edit', name='proceedings_recording_edit'),
1821
# NOTE: we have two entries here which both map to upload_unified, passing session_id or acronym
1922
url(r'^(?P<meeting_num>\d{1,3}|interim-\d{4}-[A-Za-z0-9_\-\+]+)/(?P<session_id>\d{1,6})/$',
2023
'upload_unified', name='proceedings_upload_unified'),

0 commit comments

Comments
 (0)