Skip to content

Commit 0af1223

Browse files
committed
Merged in [12990] and [12991] from rcross@amsl.com:
Refactor session audio file import. Add informative email. Fixes ietf-tools#2164. - Legacy-Id: 12998 Note: SVN reference [12990] has been migrated to Git commit 084f8a7 Note: SVN reference [12991] has been migrated to Git commit 6a5f180
2 parents fe8cd06 + 6a5f180 commit 0af1223

6 files changed

Lines changed: 273 additions & 34 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.10.5 on 2017-03-07 11:59
3+
from __future__ import unicode_literals
4+
5+
import os
6+
7+
from django.db import migrations
8+
from ietf.secr.proceedings.proc_utils import import_audio_files
9+
10+
11+
def purge_missing_files(apps, meeting):
12+
Document = apps.get_model('doc', 'Document')
13+
url = 'https://www.ietf.org/audio/ietf{}'.format(meeting.number)
14+
documents = Document.objects.filter(external_url__startswith=url)
15+
for document in documents:
16+
filename = document.external_url.split('/')[-1]
17+
if not os.path.exists(os.path.join('/a/www/audio/ietf{}'.format(meeting.number),filename)):
18+
print "Removing missing recording: {} ({})".format(filename,document.pk)
19+
document.delete()
20+
21+
def forward(apps, schema_editor):
22+
Meeting = apps.get_model('meeting', 'Meeting')
23+
Document = apps.get_model('doc', 'Document')
24+
for meeting in Meeting.objects.filter(number__in=range(94,98)):
25+
print '\nMeeting #{}:'.format(meeting.number)
26+
purge_missing_files(apps, meeting)
27+
before = Document.objects.filter(type='recording').count()
28+
import_audio_files(meeting)
29+
after = Document.objects.filter(type='recording').count()
30+
print ' {} Documents Added'.format(after - before)
31+
32+
def backward(apps, schema_editor):
33+
pass
34+
35+
class Migration(migrations.Migration):
36+
37+
dependencies = [
38+
('meeting', '0046_auto_20170201_0857'),
39+
]
40+
41+
operations = [
42+
migrations.RunPython(forward, backward)
43+
]

ietf/meeting/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from ietf.dbtemplate.models import DBTemplate
1212
from ietf.meeting.models import Session
1313
from ietf.group.utils import can_manage_materials
14+
from ietf.secr.proceedings.proc_utils import import_audio_files
1415

1516
def group_sessions(sessions):
1617

@@ -122,6 +123,7 @@ def finalize(meeting):
122123
sp.rev = '00'
123124
sp.save()
124125

126+
import_audio_files(meeting)
125127
create_proceedings_templates(meeting)
126128
meeting.proceedings_final = True
127129
meeting.save()

ietf/secr/proceedings/proc_utils.py

Lines changed: 100 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,58 +7,110 @@
77
import datetime
88
import glob
99
import os
10+
import re
1011
import shutil
1112
import subprocess
1213

1314
import debug # pyflakes:ignore
1415

1516
from django.conf import settings
17+
from django.core.exceptions import ObjectDoesNotExist
1618
from django.http import HttpRequest
1719
from django.shortcuts import render_to_response, render
1820
from django.db.utils import ConnectionDoesNotExist
1921

20-
from ietf.doc.models import Document, RelatedDocument, DocEvent, NewRevisionDocEvent, State
22+
from ietf.doc.models import Document, DocAlias, RelatedDocument, DocEvent, NewRevisionDocEvent, State
2123
from ietf.group.models import Group, Role
2224
from ietf.group.utils import get_charter_text
2325
from ietf.meeting.helpers import get_schedule
24-
from ietf.meeting.models import Session, Meeting, SchedTimeSessAssignment, SessionPresentation
26+
from ietf.meeting.models import Session, Meeting, SchedTimeSessAssignment, SessionPresentation, TimeSlot
2527
from ietf.person.models import Person
2628
from ietf.secr.proceedings.models import InterimMeeting # proxy model
2729
from ietf.secr.proceedings.models import Registration
2830
from ietf.secr.utils.document import get_rfc_num
2931
from ietf.secr.utils.group import groups_by_session
3032
from ietf.secr.utils.meeting import get_proceedings_path, get_materials, get_session
3133
from ietf.utils.log import log
34+
from ietf.utils.mail import send_mail
35+
36+
AUDIO_FILE_RE = re.compile(r'ietf(?P<number>[\d]+)-(?P<room>.*)-(?P<time>[\d]{8}-[\d]{4})')
3237

3338
# -------------------------------------------------
3439
# Helper Functions
3540
# -------------------------------------------------
36-
def check_audio_files(group,meeting):
41+
def import_audio_files(meeting):
3742
'''
3843
Checks for audio files and creates corresponding materials (docs) for the Session
39-
Expects audio files in the format ietf[meeting num]-[room]-YYYMMDD-HHMM-*,
44+
Expects audio files in the format ietf[meeting num]-[room]-YYYMMDD-HHMM.*,
4045
41-
Example: ietf90-salonb-20140721-1710-pm3.mp3
46+
Example: ietf90-salonb-20140721-1710.mp3
47+
'''
48+
unmatched_files = []
49+
path = os.path.join(settings.MEETING_RECORDINGS_DIR, meeting.type.slug + meeting.number)
50+
if not os.path.exists(path):
51+
return None
52+
for filename in os.listdir(path):
53+
timeslot = get_timeslot_for_filename(filename)
54+
if timeslot:
55+
sessionassignments = timeslot.sessionassignments.filter(
56+
schedule=timeslot.meeting.agenda,
57+
session__status='sched',
58+
).exclude(session__agenda_note__icontains='canceled').order_by('timeslot__time')
59+
if not sessionassignments:
60+
continue
61+
doc = get_or_create_recording_document(filename,sessionassignments[0].session)
62+
for sessionassignment in sessionassignments:
63+
session = sessionassignment.session
64+
if doc not in session.materials.all():
65+
# add document to session
66+
presentation = SessionPresentation.objects.create(
67+
session=session,
68+
document=doc,
69+
rev=doc.rev)
70+
session.sessionpresentation_set.add(presentation)
71+
if not doc.docalias_set.filter(name__startswith='recording-{}-{}'.format(meeting.number,session.group.acronym)):
72+
sequence = get_next_sequence(session.group,session.meeting,'recording')
73+
name = 'recording-{}-{}-{}'.format(session.meeting.number,session.group.acronym,sequence)
74+
doc.docalias_set.create(name=name)
75+
else:
76+
# use for reconciliation email
77+
unmatched_files.append(filename)
4278

79+
if unmatched_files:
80+
send_audio_import_warning(unmatched_files)
81+
82+
83+
def get_timeslot_for_filename(filename):
84+
'''Returns a timeslot matching the filename given.
85+
NOTE: currently only works with ietfNN prefix (regular meetings)
4386
'''
44-
for session in Session.objects.filter(group=group,
45-
meeting=meeting,
46-
status=('sched'),
47-
timeslotassignments__schedule=meeting.agenda):
48-
timeslot = session.official_timeslotassignment().timeslot
49-
if not (timeslot.location and timeslot.time):
50-
continue
51-
room = timeslot.location.name.lower()
52-
room = room.replace(' ','')
53-
room = room.replace('/','_')
54-
time = timeslot.time.strftime("%Y%m%d-%H%M")
55-
filename = 'ietf{}-{}-{}*'.format(meeting.number,room,time)
56-
path = os.path.join(settings.MEETING_RECORDINGS_DIR,'ietf{}'.format(meeting.number),filename)
57-
for file in glob.glob(path):
58-
url = 'https://www.ietf.org/audio/ietf{}/{}'.format(meeting.number,os.path.basename(file))
59-
doc = Document.objects.filter(external_url=url).first()
60-
if not doc:
61-
create_recording(session,url)
87+
basename, _ = os.path.splitext(filename)
88+
match = AUDIO_FILE_RE.match(basename)
89+
if match:
90+
try:
91+
meeting = Meeting.objects.get(number=match.groupdict()['number'])
92+
room_mapping = {normalize_room_name(room.name): room.name for room in meeting.room_set.all()}
93+
time = datetime.datetime.strptime(match.groupdict()['time'],'%Y%m%d-%H%M')
94+
return TimeSlot.objects.get(
95+
meeting=meeting,
96+
location__name=room_mapping[match.groupdict()['room']],
97+
time=time)
98+
except (ObjectDoesNotExist, KeyError):
99+
return None
100+
101+
def normalize_room_name(name):
102+
'''Returns room name converted to be used as portion of filename'''
103+
return name.lower().replace(' ','').replace('/','_')
104+
105+
def get_or_create_recording_document(filename,session):
106+
meeting = session.meeting
107+
url = settings.IETF_AUDIO_URL + 'ietf{}/{}'.format(meeting.number, filename)
108+
try:
109+
doc = Document.objects.get(external_url=url)
110+
return doc
111+
except ObjectDoesNotExist:
112+
pass
113+
return create_recording(session,url)
62114

63115

64116
def create_recording(session,url):
@@ -94,6 +146,30 @@ def create_recording(session,url):
94146
pres = SessionPresentation.objects.create(session=session,document=doc,rev=doc.rev)
95147
session.sessionpresentation_set.add(pres)
96148

149+
return doc
150+
151+
def get_next_sequence(group,meeting,type):
152+
'''
153+
Returns the next sequence number to use for a document of type = type.
154+
Takes a group=Group object, meeting=Meeting object, type = string
155+
'''
156+
aliases = DocAlias.objects.filter(name__startswith='{}-{}-{}-'.format(type,meeting.number,group.acronym))
157+
if not aliases:
158+
return 1
159+
aliases = aliases.order_by('name')
160+
sequence = int(aliases.last().name.split('-')[-1]) + 1
161+
return sequence
162+
163+
def send_audio_import_warning(unmatched_files):
164+
'''Send email to interested parties that some audio files weren't matched to timeslots'''
165+
send_mail(request = None,
166+
to = settings.AUDIO_IMPORT_EMAIL,
167+
frm = "IETF Secretariat <ietf-secretariat@ietf.org>",
168+
subject = "Audio file import warning",
169+
template = "proceedings/audio_import_warning.txt",
170+
context = dict(unmatched_files=unmatched_files),
171+
extra = {})
172+
97173
def mycomp(timeslot):
98174
'''
99175
This takes a timeslot object and returns a key to sort by the area acronym or None
@@ -172,13 +248,6 @@ def get_progress_stats(sdate,edate):
172248

173249
return data
174250

175-
def get_next_sequence(group,meeting,type):
176-
'''
177-
Returns the next sequence number to use for a document of type = type.
178-
Takes a group=Group object, meeting=Meeting object, type = string
179-
'''
180-
return Document.objects.filter(name__startswith='{}-{}-{}-'.format(type,meeting.number,group.acronym)).count() + 1
181-
182251
def write_html(path,content):
183252
f = open(path,'w')
184253
f.write(content)
@@ -226,7 +295,7 @@ def create_proceedings(meeting, group, is_final=False):
226295
if meeting.type_id == 'ietf' and int(meeting.number) < 79:
227296
return
228297

229-
check_audio_files(group,meeting)
298+
#check_audio_files(group,meeting)
230299
materials = get_materials(group,meeting)
231300

232301
chairs = group.role_set.filter(name='chair')

ietf/secr/proceedings/tests.py

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@
55
from django.conf import settings
66
from django.core.urlresolvers import reverse
77

8+
from ietf.doc.models import Document
89
from ietf.group.models import Group
9-
from ietf.meeting.models import Session
10+
from ietf.meeting.models import Session, TimeSlot, SchedTimeSessAssignment
1011
from ietf.meeting.test_data import make_meeting_test_data
1112
from ietf.utils.test_data import make_test_data
1213
from ietf.utils.test_utils import TestCase
14+
from ietf.utils.mail import outbox
1315

1416
from ietf.name.models import SessionStatusName
1517
from ietf.meeting.factories import SessionFactory
1618

17-
from ietf.secr.proceedings.proc_utils import create_proceedings
19+
from ietf.secr.proceedings.proc_utils import (create_proceedings, import_audio_files,
20+
get_timeslot_for_filename, normalize_room_name, send_audio_import_warning,
21+
get_or_create_recording_document, create_recording, get_next_sequence)
22+
1823

1924
SECR_USER='secretary'
2025

@@ -33,6 +38,17 @@ def test_main(self):
3338
self.assertEqual(response.status_code, 200)
3439

3540
class RecordingTestCase(TestCase):
41+
def setUp(self):
42+
self.meeting_recordings_dir = os.path.abspath("tmp-meeting-recordings-dir")
43+
self.saved_meeting_recordings_dir = settings.MEETING_RECORDINGS_DIR
44+
settings.MEETING_RECORDINGS_DIR = self.meeting_recordings_dir
45+
if not os.path.exists(self.meeting_recordings_dir):
46+
os.makedirs(self.meeting_recordings_dir)
47+
48+
def tearDown(self):
49+
shutil.rmtree(self.meeting_recordings_dir)
50+
settings.MEETING_RECORDINGS_DIR = self.saved_meeting_recordings_dir
51+
3652
def test_page(self):
3753
meeting = make_meeting_test_data()
3854
url = reverse('ietf.secr.proceedings.views.recording', kwargs={'meeting_num':meeting.number})
@@ -62,6 +78,105 @@ def test_post(self):
6278
response = self.client.post(url,dict(external_url=external_url),follow=True)
6379
self.assertEqual(response.status_code, 200)
6480
self.failUnless(external_url in response.content)
81+
82+
def test_import_audio_files(self):
83+
meeting = make_meeting_test_data()
84+
group = Group.objects.get(acronym='mars')
85+
session = Session.objects.filter(meeting=meeting,group=group).first()
86+
status = SessionStatusName.objects.get(slug='sched')
87+
session.status = status
88+
session.save()
89+
timeslot = session.official_timeslotassignment().timeslot
90+
self.create_audio_file_for_timeslot(timeslot)
91+
import_audio_files(meeting)
92+
self.assertEqual(session.materials.filter(type='recording').count(),1)
93+
94+
def create_audio_file_for_timeslot(self, timeslot):
95+
filename = self.get_filename_for_timeslot(timeslot)
96+
path = os.path.join(settings.MEETING_RECORDINGS_DIR,'ietf' + timeslot.meeting.number,filename)
97+
if not os.path.exists(os.path.dirname(path)):
98+
os.makedirs(os.path.dirname(path))
99+
with open(path, "w") as f:
100+
f.write('dummy')
101+
102+
def get_filename_for_timeslot(self, timeslot):
103+
'''Returns the filename of a session recording given timeslot'''
104+
return "{prefix}-{room}-{date}.mp3".format(
105+
prefix=timeslot.meeting.type.slug + timeslot.meeting.number,
106+
room=normalize_room_name(timeslot.location.name),
107+
date=timeslot.time.strftime('%Y%m%d-%H%M'))
108+
109+
def test_import_audio_files_shared_timeslot(self):
110+
meeting = make_meeting_test_data()
111+
mars_session = Session.objects.filter(meeting=meeting,group__acronym='mars').first()
112+
ames_session = Session.objects.filter(meeting=meeting,group__acronym='ames').first()
113+
scheduled = SessionStatusName.objects.get(slug='sched')
114+
mars_session.status = scheduled
115+
mars_session.save()
116+
ames_session.status = scheduled
117+
ames_session.save()
118+
timeslot = mars_session.official_timeslotassignment().timeslot
119+
SchedTimeSessAssignment.objects.create(timeslot=timeslot,session=ames_session,schedule=meeting.agenda)
120+
self.create_audio_file_for_timeslot(timeslot)
121+
import_audio_files(meeting)
122+
doc = mars_session.materials.filter(type='recording').first()
123+
self.assertTrue(doc in ames_session.materials.all())
124+
self.assertTrue(doc.docalias_set.filter(name='recording-42-mars-1'))
125+
self.assertTrue(doc.docalias_set.filter(name='recording-42-ames-1'))
126+
127+
def test_normalize_room_name(self):
128+
self.assertEqual(normalize_room_name('Test Room'),'testroom')
129+
self.assertEqual(normalize_room_name('Rome/Venice'), 'rome_venice')
130+
131+
def test_get_timeslot_for_filename(self):
132+
meeting = make_meeting_test_data()
133+
timeslot = TimeSlot.objects.filter(meeting=meeting,type='session').first()
134+
name = self.get_filename_for_timeslot(timeslot)
135+
self.assertEqual(get_timeslot_for_filename(name),timeslot)
136+
137+
def test_get_or_create_recording_document(self):
138+
meeting = make_meeting_test_data()
139+
group = Group.objects.get(acronym='mars')
140+
session = Session.objects.filter(meeting=meeting,group=group).first()
141+
142+
# test create
143+
filename = 'ietf42-testroom-20000101-0800.mp3'
144+
docs_before = Document.objects.filter(type='recording').count()
145+
doc = get_or_create_recording_document(filename,session)
146+
docs_after = Document.objects.filter(type='recording').count()
147+
self.assertEqual(docs_after,docs_before + 1)
148+
self.assertTrue(doc.external_url.endswith(filename))
149+
150+
# test get
151+
docs_before = docs_after
152+
doc2 = get_or_create_recording_document(filename,session)
153+
docs_after = Document.objects.filter(type='recording').count()
154+
self.assertEqual(docs_after,docs_before)
155+
self.assertEqual(doc,doc2)
156+
157+
def test_create_recording(self):
158+
meeting = make_meeting_test_data()
159+
group = Group.objects.get(acronym='mars')
160+
session = Session.objects.filter(meeting=meeting,group=group).first()
161+
filename = 'ietf42-testroomt-20000101-0800.mp3'
162+
url = settings.IETF_AUDIO_URL + 'ietf{}/{}'.format(meeting.number, filename)
163+
doc = create_recording(session, url)
164+
self.assertEqual(doc.name,'recording-42-mars-1')
165+
self.assertEqual(doc.group,group)
166+
self.assertEqual(doc.external_url,url)
167+
self.assertTrue(doc in session.materials.all())
168+
169+
def test_get_next_sequence(self):
170+
meeting = make_meeting_test_data()
171+
group = Group.objects.get(acronym='mars')
172+
sequence = get_next_sequence(group,meeting,'recording')
173+
self.assertEqual(sequence,1)
174+
175+
def test_send_audio_import_warning(self):
176+
length_before = len(outbox)
177+
send_audio_import_warning(['recording-43-badroom-20000101-0800.mp3'])
178+
self.assertEqual(len(outbox), length_before + 1)
179+
self.assertTrue('Audio file import' in outbox[-1]['Subject'])
65180

66181
class OldProceedingsTestCase(TestCase):
67182
''' Ensure coverage of fragments of old proceedings generation until those are removed '''

0 commit comments

Comments
 (0)