changes to support video content in proceedings ('recording' document type). Commit ready for merge

- Legacy-Id: 8237
This commit is contained in:
Ryan Cross 2014-08-04 17:17:52 +00:00
commit 96bccc7b17
18 changed files with 625 additions and 130 deletions

View file

@ -3,7 +3,9 @@
import datetime, os
from django.db import models
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse as urlreverse
from django.core.validators import URLValidator
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
from django.utils.html import mark_safe
@ -90,6 +92,13 @@ class DocumentInfo(models.Model):
return settings.DOCUMENT_PATH_PATTERN.format(doc=self)
def href(self):
validator = URLValidator()
try:
validator(self.external_url)
return self.external_url
except ValidationError:
pass
meeting_related = self.meeting_related()
settings_var = settings.DOC_HREFS

View file

@ -51,10 +51,10 @@ def has_role(user, role_names, *args, **kwargs):
"IRTF Chair": Q(person=person, name="chair", group__acronym="irtf"),
"IAB Chair": Q(person=person, name="chair", group__acronym="iab"),
"IAB Group Chair": Q(person=person, name="chair", group__type="iab", group__state="active"),
"WG Chair": Q(person=person,name="chair", group__type="wg", group__state__in=["active","bof"]),
"WG Secretary": Q(person=person,name="secr", group__type="wg", group__state__in=["active","bof"]),
"RG Chair": Q(person=person,name="chair", group__type="rg", group__state="active"),
"RG Secretary": Q(person=person,name="secr", group__type="rg", group__state="active"),
"WG Chair": Q(person=person,name="chair", group__type="wg", group__state__in=["active","bof","proposed"]),
"WG Secretary": Q(person=person,name="secr", group__type="wg", group__state__in=["active","bof","proposed"]),
"RG Chair": Q(person=person,name="chair", group__type="rg", group__state="active"),
"RG Secretary": Q(person=person,name="secr", group__type="rg", group__state="active"),
"Team Chair": Q(person=person,name="chair", group__type="team", group__state="active"),
"Nomcom Chair": Q(person=person, name="chair", group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')),
"Nomcom Advisor": Q(person=person, name="advisor", group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')),

View file

@ -81,7 +81,7 @@
"model": "name.constraintname",
"fields": {
"order": 0,
"penalty": 0,
"penalty": 100000,
"used": true,
"name": "Conflicts with",
"desc": ""
@ -92,7 +92,7 @@
"model": "name.constraintname",
"fields": {
"order": 0,
"penalty": 0,
"penalty": 10000,
"used": true,
"name": "Conflicts with (secondary)",
"desc": ""
@ -103,7 +103,7 @@
"model": "name.constraintname",
"fields": {
"order": 0,
"penalty": 0,
"penalty": 1000,
"used": true,
"name": "Conflicts with (tertiary)",
"desc": ""
@ -114,7 +114,7 @@
"model": "name.constraintname",
"fields": {
"order": 0,
"penalty": 0,
"penalty": 200000,
"used": true,
"name": "Person must be present",
"desc": ""
@ -734,6 +734,16 @@
"desc": ""
}
},
{
"pk": "recording",
"model": "name.doctypename",
"fields": {
"order": 0,
"used": true,
"name": "Recording",
"desc": ""
}
},
{
"pk": "uploaded",
"model": "name.draftsubmissionstatename",
@ -1109,6 +1119,26 @@
"desc": "An IETF/IAB Nominating Committee. Use 'SDO' for external nominating committees."
}
},
{
"pk": "iab",
"model": "name.grouptypename",
"fields": {
"order": 0,
"used": true,
"name": "IAB",
"desc": ""
}
},
{
"pk": "isoc",
"model": "name.grouptypename",
"fields": {
"order": 0,
"used": true,
"name": "ISOC",
"desc": ""
}
},
{
"pk": "ps",
"model": "name.intendedstdlevelname",
@ -1270,22 +1300,12 @@
}
},
{
"pk": "ad",
"pk": "ceo",
"model": "name.rolename",
"fields": {
"order": 0,
"used": true,
"name": "Area Director",
"desc": ""
}
},
{
"pk": "pre-ad",
"model": "name.rolename",
"fields": {
"order": 0,
"used": true,
"name": "Incoming Area Director",
"name": "CEO",
"desc": ""
}
},
@ -1293,39 +1313,19 @@
"pk": "chair",
"model": "name.rolename",
"fields": {
"order": 0,
"order": 1,
"used": true,
"name": "Chair",
"desc": ""
}
},
{
"pk": "editor",
"pk": "ad",
"model": "name.rolename",
"fields": {
"order": 0,
"order": 2,
"used": true,
"name": "Editor",
"desc": ""
}
},
{
"pk": "secr",
"model": "name.rolename",
"fields": {
"order": 0,
"used": true,
"name": "Secretary",
"desc": ""
}
},
{
"pk": "techadv",
"model": "name.rolename",
"fields": {
"order": 0,
"used": true,
"name": "Tech Advisor",
"name": "Area Director",
"desc": ""
}
},
@ -1333,90 +1333,170 @@
"pk": "execdir",
"model": "name.rolename",
"fields": {
"order": 0,
"order": 2,
"used": true,
"name": "Executive Director",
"desc": ""
}
},
{
"pk": "pre-ad",
"model": "name.rolename",
"fields": {
"order": 3,
"used": true,
"name": "Incoming Area Director",
"desc": ""
}
},
{
"pk": "admdir",
"model": "name.rolename",
"fields": {
"order": 0,
"order": 3,
"used": true,
"name": "Administrative Director",
"desc": ""
}
},
{
"pk": "techadv",
"model": "name.rolename",
"fields": {
"order": 4,
"used": true,
"name": "Tech Advisor",
"desc": ""
}
},
{
"pk": "liaiman",
"model": "name.rolename",
"fields": {
"order": 0,
"order": 4,
"used": true,
"name": "Liaison Manager",
"desc": ""
}
},
{
"pk": "advisor",
"model": "name.rolename",
"fields": {
"order": 4,
"used": true,
"name": "Advisor",
"desc": "Advisor in a group that has explicit membership, such as the NomCom"
}
},
{
"pk": "editor",
"model": "name.rolename",
"fields": {
"order": 5,
"used": true,
"name": "Editor",
"desc": ""
}
},
{
"pk": "auth",
"model": "name.rolename",
"fields": {
"order": 0,
"order": 5,
"used": true,
"name": "Authorized Individual",
"desc": ""
}
},
{
"pk": "secr",
"model": "name.rolename",
"fields": {
"order": 6,
"used": true,
"name": "Secretary",
"desc": ""
}
},
{
"pk": "delegate",
"model": "name.rolename",
"fields": {
"order": 0,
"order": 6,
"used": true,
"name": "Delegate",
"desc": ""
}
},
{
"pk": "atlarge",
"model": "name.rolename",
"fields": {
"order": 0,
"used": true,
"name": "At Large Member",
"desc": ""
}
},
{
"pk": "member",
"model": "name.rolename",
"fields": {
"order": 0,
"order": 7,
"used": true,
"name": "Member",
"desc": "Regular group member in a group that has explicit membership, such as the NomCom"
}
},
{
"pk": "atlarge",
"model": "name.rolename",
"fields": {
"order": 10,
"used": true,
"name": "At Large Member",
"desc": ""
}
},
{
"pk": "liaison",
"model": "name.rolename",
"fields": {
"order": 0,
"order": 11,
"used": true,
"name": "Liaison Member",
"desc": "Liaison group member in a group that has explicit membership, such as the NomCom"
}
},
{
"pk": "advisor",
"pk": "announce",
"model": "name.rolename",
"fields": {
"order": 12,
"used": true,
"name": "List Announcer",
"desc": "Authorised to send announcements to the ietf-announce and other lists"
}
},
{
"pk": "project",
"model": "name.roomresourcename",
"fields": {
"order": 0,
"used": true,
"name": "Advisor",
"desc": "Advisor in a group that has explicit membership, such as the NomCom"
"name": "LCD projector",
"desc": "The room will have a computer projector"
}
},
{
"pk": "proj2",
"model": "name.roomresourcename",
"fields": {
"order": 0,
"used": true,
"name": "second LCD projector",
"desc": "The room will have a second computer projector"
}
},
{
"pk": "meetecho",
"model": "name.roomresourcename",
"fields": {
"order": 0,
"used": true,
"name": "Meetecho Remote Partition Support",
"desc": "The room will have a meetecho wrangler"
}
},
{
@ -1818,6 +1898,13 @@
"label": "RFC Status Change"
}
},
{
"pk": "recording",
"model": "doc.statetype",
"fields": {
"label": "Recording State"
}
},
{
"pk": 81,
"model": "doc.state",
@ -3487,6 +3574,32 @@
"desc": ""
}
},
{
"pk": 135,
"model": "doc.state",
"fields": {
"used": true,
"name": "Active",
"next_states": [],
"slug": "active",
"type": "recording",
"order": 0,
"desc": ""
}
},
{
"pk": 136,
"model": "doc.state",
"fields": {
"used": true,
"name": "deleted",
"next_states": [],
"slug": "deleted",
"type": "recording",
"order": 0,
"desc": ""
}
},
{
"pk": 77,
"model": "doc.state",

View file

@ -5,9 +5,9 @@ from django.conf import settings
from django.template.defaultfilters import filesizeformat
from ietf.doc.models import Document
from ietf.group.models import Group
from ietf.name.models import DocTypeName
from ietf.meeting.models import Meeting
from ietf.meeting.models import Meeting, Session
# ---------------------------------------------
@ -22,6 +22,14 @@ VALID_AGENDA_EXTENSIONS = ('.txt','.html','.htm')
# Forms
#----------------------------------------------------------
class AjaxChoiceField(forms.ChoiceField):
'''
Special ChoiceField to use when populating options with Ajax. The submitted value
is not in the initial choices list so we need to override valid_value().
'''
def valid_value(self, value):
return True
class EditSlideForm(forms.ModelForm):
class Meta:
model = Document
@ -42,6 +50,37 @@ class InterimMeetingForm(forms.Form):
raise forms.ValidationError('A meeting already exists for this date.')
return cleaned_data
class RecordingForm(forms.Form):
group = forms.CharField(max_length=40)
external_url = forms.URLField(label='Url')
session = AjaxChoiceField(choices=(('','----'),))
def clean_session(self):
'''
Emulate ModelChoiceField functionality
'''
id = self.cleaned_data.get('session')
try:
return Session.objects.get(id=id)
except Session.DoesNotExist:
raise forms.ValidationError('Invalid Session')
def clean_group(self):
acronym = self.cleaned_data.get('group')
try:
return Group.objects.get(acronym=acronym)
except Group.DoesNotExist:
raise forms.ValidationError('Invalid group name')
class RecordingEditForm(forms.ModelForm):
class Meta:
model = Document
fields = ['external_url']
def __init__(self, *args, **kwargs):
super(RecordingEditForm, self).__init__(*args, **kwargs)
self.fields['external_url'].label='Url'
class ReplaceSlideForm(forms.ModelForm):
file = forms.FileField(label='Select File')

View file

@ -12,22 +12,74 @@ import shutil
from django.conf import settings
from django.shortcuts import render_to_response
from ietf.doc.models import Document, RelatedDocument, DocEvent
from ietf.doc.models import Document, RelatedDocument, DocEvent, NewRevisionDocEvent, State
from ietf.group.models import Group, Role
from ietf.group.utils import get_charter_text
from ietf.meeting.helpers import get_schedule
from ietf.meeting.models import Session, Meeting, ScheduledSession
from ietf.person.models import Person
from ietf.secr.proceedings.models import InterimMeeting # proxy model
from ietf.secr.proceedings.models import Registration
from ietf.secr.utils.document import get_rfc_num
from ietf.secr.utils.group import groups_by_session
from ietf.secr.utils.meeting import get_upload_root, get_proceedings_path, get_material, get_session
from ietf.secr.utils.meeting import get_upload_root, get_proceedings_path, get_materials, get_session
# -------------------------------------------------
# Helper Functions
# -------------------------------------------------
def check_audio_files(group,meeting):
'''
Checks for audio files and creates corresponding materials (docs) for the Session
Expects audio files in the format ietf[meeting num]-[room]-YYYMMDD-HHMM-*,
Example: ietf90-salonb-20140721-1710-pm3.mp3
'''
for session in Session.objects.filter(group=group,meeting=meeting,status__in=('sched','schedw')):
timeslot = session.official_scheduledsession().timeslot
room = timeslot.location.name.lower()
room = room.replace(' ','')
room = room.replace('/','')
time = timeslot.time.strftime("%Y%m%d-%H%M")
filename = 'ietf{}-{}-{}-*'.format(meeting.number,room,time)
path = os.path.join(settings.MEETING_RECORDINGS_DIR,'ietf{}'.format(meeting.number),filename)
for file in glob.glob(path):
url = 'http://www.ietf.org/audio/ietf{}/{}'.format(meeting.number,os.path.basename(file))
doc = Document.objects.filter(external_url=url).first()
if not doc:
create_recording(session,meeting,group,url)
def create_recording(session,meeting,group,url):
'''
Creates the Document type=recording, setting external_url and creating
NewRevisionDocEvent
'''
sequence = get_next_sequence(group,meeting,'recording')
name = 'recording-{}-{}-{}'.format(meeting.number,group.acronym,sequence)
time = session.official_scheduledsession().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)
doc = Document.objects.create(name=name,
title=title,
external_url=url,
group=group,
rev='00',
type_id='recording')
doc.set_state(State.objects.get(type='recording', slug='active'))
# create DocEvent
NewRevisionDocEvent.objects.create(type='new_revision',
by=Person.objects.get(name='(System)'),
doc=doc,
rev=doc.rev,
desc='New revision available',
time=doc.time)
session.materials.add(doc)
def mycomp(timeslot):
'''
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):
return data
def get_next_sequence(group,meeting,type):
'''
Returns the next sequence number to use for a document of type = type.
Takes a group=Group object, meeting=Meeting object, type = string
'''
return Document.objects.filter(name__startswith='{}-{}-{}-'.format(type,meeting.number,group.acronym)).count() + 1
def write_html(path,content):
f = open(path,'w')
f.write(content)
@ -188,14 +247,8 @@ def create_proceedings(meeting, group, is_final=False):
if meeting.type_id == 'ietf' and int(meeting.number) < 79:
return
sessions = Session.objects.filter(meeting=meeting,group=group)
if sessions:
session = sessions[0]
agenda,minutes,slides = get_material(session)
else:
agenda = None
minutes = None
slides = None
check_audio_files(group,meeting)
materials = get_materials(group,meeting)
chairs = group.role_set.filter(name='chair')
secretaries = group.role_set.filter(name='secr')
@ -215,7 +268,7 @@ def create_proceedings(meeting, group, is_final=False):
settings.MEDIA_URL,
meeting.date.strftime('%Y/%m/%d'),
group.acronym)
# Only do these tasks if we are running official proceedings generation,
# otherwise skip them for expediency. This procedure is called any time meeting
# 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):
'tas': tas,
'meeting': meeting,
'rfcs': rfcs,
'slides': slides,
'minutes': minutes,
'agenda': agenda}
'materials': materials}
)
# save proceedings

View file

@ -2,10 +2,11 @@ import debug # pyflakes:ignore
from django.core.urlresolvers import reverse
from ietf.utils.test_utils import TestCase
from ietf.meeting.models import Meeting
from ietf.group.models import Group
from ietf.meeting.models import Meeting, Session
from ietf.meeting.test_data import make_meeting_test_data
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import TestCase
SECR_USER='secretary'
@ -18,11 +19,30 @@ class MainTestCase(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_view(self):
"View Test"
class RecordingTestCase(TestCase):
def test_page(self):
make_test_data()
meeting = Meeting.objects.all()[0]
url = reverse('meetings_view', kwargs={'meeting_id':meeting.number})
meeting = Meeting.objects.first()
url = reverse('proceedings_recording', kwargs={'meeting_num':meeting.number})
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_post(self):
make_meeting_test_data()
meeting = Meeting.objects.first()
group = Group.objects.get(acronym='mars')
session = Session.objects.filter(meeting=meeting,group=group,status__in=('sched','schedw')).first()
url = reverse('proceedings_recording', kwargs={'meeting_num':meeting.number})
data = dict(group=group.acronym,external_url='http://youtube.com/xyz',session=session.pk)
self.client.login(username="secretary", password="secretary+password")
response = self.client.post(url,data,follow=True)
self.assertEqual(response.status_code, 200)
self.failUnless(group.acronym in response.content)
# now test edit
doc = session.materials.filter(type='recording').first()
external_url = 'http://youtube.com/aaa'
url = reverse('proceedings_recording_edit', kwargs={'meeting_num':meeting.number,'name':doc.name})
response = self.client.post(url,dict(external_url=external_url),follow=True)
self.assertEqual(response.status_code, 200)
self.failUnless(external_url in response.content)

View file

@ -3,6 +3,7 @@ from django.conf.urls import patterns, url
urlpatterns = patterns('ietf.secr.proceedings.views',
url(r'^$', 'main', name='proceedings'),
url(r'^ajax/generate-proceedings/(?P<meeting_num>\d{1,3})/$', 'ajax_generate_proceedings', name='proceedings_ajax_generate_proceedings'),
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'),
url(r'^ajax/order-slide/$', 'ajax_order_slide', name='proceedings_ajax_order_slide'),
# special offline URL for testing proceedings build
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 @@ urlpatterns = patterns('ietf.secr.proceedings.views',
url(r'^progress-report/(?P<meeting_num>\d{1,3})/$', 'progress_report', name='proceedings_progress_report'),
url(r'^replace-slide/(?P<slide_id>[A-Za-z0-9._\-\+]+)/$', 'replace_slide', name='proceedings_replace_slide'),
url(r'^(?P<meeting_num>\d{1,3})/$', 'select', name='proceedings_select'),
url(r'^(?P<meeting_num>\d{1,3})/recording/$', 'recording', name='proceedings_recording'),
url(r'^(?P<meeting_num>\d{1,3})/recording/edit/(?P<name>[A-Za-z0-9_\-\+]+)$', 'recording_edit', name='proceedings_recording_edit'),
# NOTE: we have two entries here which both map to upload_unified, passing session_id or acronym
url(r'^(?P<meeting_num>\d{1,3}|interim-\d{4}-[A-Za-z0-9_\-\+]+)/(?P<session_id>\d{1,6})/$',
'upload_unified', name='proceedings_upload_unified'),

View file

@ -10,7 +10,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
from django.db.models import Max
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response, get_object_or_404
from django.shortcuts import render_to_response, get_object_or_404, redirect
from django.template import RequestContext
from django.utils.text import slugify
@ -19,15 +19,15 @@ from ietf.secr.sreq.forms import GroupSelectForm
from ietf.secr.utils.decorators import check_permissions, sec_only
from ietf.secr.utils.document import get_full_path
from ietf.secr.utils.group import get_my_groups, groups_by_session
from ietf.secr.utils.meeting import get_upload_root, get_material, get_timeslot
from ietf.secr.utils.meeting import get_upload_root, get_materials, get_timeslot
from ietf.doc.models import Document, DocAlias, DocEvent, State, NewRevisionDocEvent
from ietf.group.models import Group
from ietf.ietfauth.utils import has_role
from ietf.meeting.models import Meeting, Session, TimeSlot, ScheduledSession
from ietf.secr.proceedings.forms import EditSlideForm, InterimMeetingForm, ReplaceSlideForm, UnifiedUploadForm
from ietf.secr.proceedings.forms import EditSlideForm, InterimMeetingForm, RecordingForm, RecordingEditForm, ReplaceSlideForm, UnifiedUploadForm
from ietf.secr.proceedings.proc_utils import ( gen_acknowledgement, gen_agenda, gen_areas, gen_attendees,
gen_group_pages, gen_index, gen_irtf, gen_overview, gen_plenaries, gen_progress, gen_research,
gen_training, create_proceedings, create_interim_directory )
gen_training, create_proceedings, create_interim_directory, create_recording )
from ietf.secr.proceedings.models import InterimMeeting # proxy model
@ -84,14 +84,14 @@ def get_extras(meeting):
def get_next_interim_num(acronym,date):
'''
This function takes a group acronym and date object and returns the next number to use for an
interim meeting. The format is interim-[year]-[acronym]-[1-9]
interim meeting. The format is interim-[year]-[acronym]-[1-99]
'''
base = 'interim-%s-%s-' % (date.year, acronym)
# can't use count() to calculate the next number in case one was deleted
meetings = list(Meeting.objects.filter(type='interim',number__startswith=base).order_by('number'))
meetings = Meeting.objects.filter(type='interim',number__startswith=base)
if meetings:
parts = meetings[-1].number.split('-')
return base + str(int(parts[-1]) + 1)
nums = sorted([ int(x.number.split('-')[-1]) for x in meetings ])
return base + str(nums[-1] + 1)
else:
return base + '1'
@ -250,6 +250,33 @@ def ajax_generate_proceedings(request, meeting_num):
RequestContext(request,{}),
)
@jsonapi
def ajax_get_sessions(request, meeting_num, acronym):
'''
Ajax function to get session info for group / meeting
returns JSON format response: [{id:session_id, value:session info},...]
If there are no sessions an empty list is returned.
'''
results=[]
try:
meeting = Meeting.objects.get(number=meeting_num)
group = Group.objects.get(acronym=acronym)
except ObjectDoesNotExist:
return results
sessions = Session.objects.filter(meeting=meeting,group=group,status='sched')
# order by time scheduled
sessions = sorted(sessions,key = lambda x: x.official_scheduledsession().timeslot.time)
for n,session in enumerate(sessions,start=1):
timeslot = session.official_scheduledsession().timeslot
val = '{}: {} {}'.format(n,timeslot.time.strftime('%m-%d %H:%M'),timeslot.location.name)
d = {'id':session.id, 'value': val}
results.append(d)
return results
@jsonapi
def ajax_order_slide(request):
'''
@ -573,6 +600,69 @@ def progress_report(request, meeting_num):
url = reverse('proceedings_select', kwargs={'meeting_num':meeting_num})
return HttpResponseRedirect(url)
@sec_only
def recording(request, meeting_num):
'''
Enter Session recording info. Creates Document and associates it with Session
'''
meeting = get_object_or_404(Meeting, number=meeting_num)
recordings = Document.objects.filter(name__startswith='recording-{}'.format(meeting.number),states__slug='active').order_by('group__acronym')
if request.method == 'POST':
form = RecordingForm(request.POST)
if form.is_valid():
group = form.cleaned_data['group']
external_url = form.cleaned_data['external_url']
session = form.cleaned_data['session']
if Document.objects.filter(type='recording',external_url=external_url):
messages.error(request, "Recording already exists")
return redirect('proceedings_recording', meeting_num=meeting_num)
else:
create_recording(session,meeting,group,external_url)
# rebuild proceedings
create_proceedings(meeting,group)
messages.success(request,'Recording added')
return redirect('proceedings_recording', meeting_num=meeting_num)
else:
form = RecordingForm()
return render_to_response('proceedings/recording.html',{
'meeting':meeting,
'form':form,
'recordings':recordings},
RequestContext(request, {}),
)
@sec_only
def recording_edit(request, meeting_num, name):
'''
Edit recording Document
'''
recording = get_object_or_404(Document, name=name)
meeting = get_object_or_404(Meeting, number=meeting_num)
if request.method == 'POST':
form = RecordingEditForm(request.POST, instance=recording)
if form.is_valid():
# save record and rebuild proceedings
form.save()
create_proceedings(meeting,recording.group)
messages.success(request,'Recording saved')
return redirect('proceedings_recording', meeting_num=meeting_num)
else:
form = RecordingEditForm(instance=recording)
return render_to_response('proceedings/recording_edit.html',{
'meeting':meeting,
'form':form,
'recording':recording},
RequestContext(request, {}),
)
@check_permissions
def replace_slide(request, slide_id):
'''
@ -860,7 +950,7 @@ def upload_unified(request, meeting_num, acronym=None, session_id=None):
else:
form = UnifiedUploadForm(initial={'meeting_id':meeting.id,'acronym':group.acronym,'material_type':'slides'})
agenda,minutes,slides = get_material(session)
materials = get_materials(group,meeting)
# gather DocEvents
# include deleted material to catch deleted doc events
@ -877,11 +967,9 @@ def upload_unified(request, meeting_num, acronym=None, session_id=None):
'docevents': docevents,
'meeting': meeting,
'group': group,
'minutes': minutes,
'agenda': agenda,
'materials': materials,
'form': form,
'session_name': session_name, # for Tutorials, etc
'slides':slides,
'proceedings_url': proceedings_url},
RequestContext(request, {}),
)

View file

@ -24,3 +24,8 @@
&nbsp;&nbsp;<a href="{{ MEDIA_URL }}proceedings/{{ meeting.number }}/progress-report.html">Progress Report</a>
</li>
</ul>
<p>Use this to input session recording information.</p>
<ul class="none">
<li><button onclick="window.location='{% url "proceedings_recording" meeting_num=meeting.number %}'">Recordings</button>
</li>
</ul>

View file

@ -11,7 +11,7 @@
</tr>
</thead>
<tbody>
{% for slide in slides %}
{% for slide in materials.slides %}
<tr id="slide_{{ forloop.counter }}" class="{% cycle 'row1' 'row2' %}">
<td><span class="ui-icon ui-icon-arrowthick-2-n-s"></span><a href="{{ slide.get_absolute_url }}" target="_blank">{{ slide.title }}</a></td>
<td>{{ slide.external_url }}{% if slide.external_url|is_ppt %}<span class="required"> *</span>{% endif %}</td>

View file

@ -22,16 +22,16 @@ and end with
{% endif %}
<h3>
{% if minutes %}
<a href="{{ minutes.get_absolute_url }}">Minutes</a>
{% if materials.minutes %}
<a href="{{ materials.minutes.get_absolute_url }}">Minutes</a>
{% else %}
Minutes
{% endif %}&nbsp;&nbsp;|&nbsp;&nbsp;
{% if meeting.type.slug == "ietf" %}
<a href="/audio/ietf{{ meeting.number }}/">Audio Archives</a>&nbsp;&nbsp;|&nbsp;&nbsp;
{% comment %}<a href="/audio/ietf{{ meeting.number }}/">Audio Archives</a>&nbsp;&nbsp;|&nbsp;&nbsp; {% endcomment %}
{% else %}
{% if agenda %}
<a href="{{ agenda.get_absolute_url }}">Agenda</a>
{% if materials.agenda %}
<a href="{{ materials.agenda.get_absolute_url }}">Agenda</a>
{% else %}
Agenda
{% endif %}&nbsp;&nbsp;|&nbsp;&nbsp;
@ -84,10 +84,21 @@ and end with
{% endif %}
<br /><br /></td></tr></table>
<h3>Meeting Slides:</h3>
{% if slides %}
<h3>Recordings:</h3>
{% if materials.record %}
<ul>
{% for slide in slides %}
{% for record in materials.record %}
<li><a href="{{ record.href }}" target="_blank">{{ record.title }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No Recordings Present</p>
{% endif %}
<h3>Meeting Slides:</h3>
{% if materials.slides %}
<ul>
{% for slide in materials.slides %}
<li><a href="{{ slide.get_absolute_url }}" target="_blank">{{ slide.title }}</a></li>
{% endfor %}
</ul>

View file

@ -0,0 +1,80 @@
{% extends "base_site.html" %}
{% block title %}Proceedings{% endblock %}
{% block extrastyle %}{{ block.super }}
<link rel="stylesheet" type="text/css" href="{{ SECR_STATIC_URL }}css/jquery-ui-modified.css" />
{% endblock %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/jquery-ui-1.8.9.min.js"></script>
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/proceedings-recording.js"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
{% if meeting.type_id == "interim" %}
&raquo; <a href="{% url "proceedings" %}">Proceedings</a>
&raquo; <a href="{% url "proceedings_select_interim" %}">Interim Select Group</a>
&raquo; <a href="{% url "proceedings_interim" acronym=group.acronym%}">{{ group.acronym }}</a>
&raquo; {{ meeting }}
{% else %}
&raquo; <a href="{% url "proceedings" %}">Proceedings</a>
&raquo; <a href="{% url "proceedings_select" meeting_num=meeting.number %}">{{ meeting.number }}</a>
&raquo; Recording
{% endif %}
{% endblock %}
{% block content %}
<div class="module interim-container">
<h2>Recording Metadata</h2>
<form id="recording-form" enctype="multipart/form-data" action="." method="post">{% csrf_token %}
<table class="center" id="proceedings-upload-table">
{{ form.as_table }}
</table>
{% include "includes/buttons_submit_back.html" %}
</form>
<div class="inline-related">
<h2>{{ meeting }} - Recordings
</h2>
<table class="center">
<thead>
<tr>
<th>Group</th>
<th>Session</th>
<th>Name</th>
<th>URL</th>
<th>Edit</th>
</tr>
</thead>
<tbody>
{% for record in recordings %}
<tr>
<td>{{ record.group.acronym }}</td>
<td>{{ record.session_set.first.official_scheduledsession.timeslot.time|date:"m-d H:i" }}</td>
<td>{{ record.name }}</td>
<td><a href="{{ record.href }}">{{ record.href }}</a></td>
<td><a href="{% url "proceedings_recording_edit" meeting_num=meeting.number name=record.name %}">Edit</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div> <!-- inline-group -->
</div> <!-- module -->
{% if docevents %}
<br>
<div class="module interim-container">
{% include "includes/docevents.html" %}
</div>
{% endif %}
{% endblock %}
{% block footer-extras %}
{% include "includes/upload_footer.html" %}
{% endblock %}

View file

@ -0,0 +1,45 @@
{% extends "base_site.html" %}
{% block title %}Edit Recording{% endblock %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/utils.js"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
{% if meeting.type_id == "interim" %}
&raquo; <a href="{% url "proceedings" %}">Proceedings</a>
&raquo; <a href="{% url "proceedings_select_interim" %}">Interim Select Group</a>
&raquo; <a href="{% url "proceedings_interim" acronym=group.acronym%}">{{ group.acronym }}</a>
&raquo; {{ meeting }}
&raquo; <a href="{% url "proceedings_recording" meeting_num=meeting.number %}">Recording</a>
&raquo; {{ recording.name }}
{% else %}
&raquo; <a href="{% url "proceedings" %}">Proceedings</a>
&raquo; <a href="{% url "proceedings_select" meeting_num=meeting.number %}">{{ meeting.number }}</a>
&raquo; <a href="{% url "proceedings_recording" meeting_num=meeting.number %}">Recording</a>
&raquo; {{ recording.name }}
{% endif %}
{% endblock %}
{% block content %}
<div class="module interim-container">
<h2>Recording Metadata for Group: {{ form.instance.group.acronym }} | Session: {{ form.instance.session_set.first.official_scheduledsession.timeslot.time }}</h2>
<p><h3>Edit Recording Metadata:</h3></p>
<form id="recording-form" action="" method="post">{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<div class="button-group">
<ul>
<li><button type="submit" name="submit" value="Submit">Submit</button></li>
<li><button type="button" onclick="window.location='{% url "proceedings_recording" meeting_num=meeting.number %}'">Cancel</button></li>
</ul>
</div> <!-- button-group -->
</form>
</div> <!-- module -->
{% endblock %}

View file

@ -51,10 +51,10 @@
<td></td>
</tr>
<tr>
{% if minutes %}
<td><a href="{{ minutes.get_absolute_url }}" target="_blank">Minutes</td>
<td>{{ minutes.external_url }}</td>
<td><a href="{% url "proceedings_delete_material" slide_id=minutes.name %}">Delete</a></td>
{% if materials.minutes %}
<td><a href="{{ materials.minutes.get_absolute_url }}" target="_blank">Minutes</td>
<td>{{ materials.minutes.external_url }}</td>
<td><a href="{% url "proceedings_delete_material" slide_id=materials.minutes.name %}">Delete</a></td>
{% else %}
<td>Minutes</td>
<td>(not uploaded)</td>
@ -62,10 +62,10 @@
{% endif %}
</tr>
<tr>
{% if agenda %}
<td><a href="{{ agenda.get_absolute_url }}" target="_blank">Agenda</a></td>
<td>{{ agenda.external_url }}</td>
<td><a href="{% url "proceedings_delete_material" slide_id=agenda.name %}">Delete</a></td>
{% if materials.agenda %}
<td><a href="{{ materials.agenda.get_absolute_url }}" target="_blank">Agenda</a></td>
<td>{{ materials.agenda.external_url }}</td>
<td><a href="{% url "proceedings_delete_material" slide_id=materials.agenda.name %}">Delete</a></td>
{% else %}
<td>Agenda</td>
<td>(not uploaded)</td>

View file

@ -2,26 +2,27 @@ import os
from django.conf import settings
from ietf.meeting.models import Meeting
from ietf.meeting.models import Meeting, Session
def get_current_meeting():
'''Returns the most recent IETF meeting'''
return Meeting.objects.filter(type='ietf').order_by('-number')[0]
def get_material(session):
def get_materials(group,meeting):
'''
This function takes a session object and returns a tuple of active materials:
agenda(Document), minutes(Document), slides(list of Documents)
Returns the materials as a dictionary with keys = doctype.
NOTE, if the group has multiple sessions all materials but recordings will be
attached to all sessions.
'''
active_materials = session.materials.exclude(states__slug='deleted')
slides = active_materials.filter(type='slides').order_by('order')
minutes = active_materials.filter(type='minutes')
minutes = minutes[0] if minutes else None
agenda = active_materials.filter(type='agenda')
agenda = agenda[0] if agenda else None
return agenda,minutes,slides
materials = dict(slides=[],recording=[])
# TODO: status should only be sched, but there is a bug in the scheduler
for session in Session.objects.filter(group=group,meeting=meeting,status__in=('sched','schedw')):
for doc in session.materials.exclude(states__slug='deleted').order_by('order'):
if doc.type.slug in ('minutes','agenda'):
materials[doc.type.slug] = doc
elif doc not in materials[doc.type.slug]:
materials[doc.type.slug].append(doc)
return materials
def get_proceedings_path(meeting, group):
if meeting.type.slug == 'interim':

View file

@ -275,6 +275,7 @@ IESG_ROLL_CALL_FILE = '/a/www/www6/iesg/internal/rollcall.txt'
IESG_MINUTES_FILE = '/a/www/www6/iesg/internal/minutes.txt'
IESG_WG_EVALUATION_DIR = "/a/www/www6/iesg/evaluation"
INTERNET_DRAFT_ARCHIVE_DIR = '/a/www/www6s/draft-archive'
MEETING_RECORDINGS_DIR = '/a/www/audio'
# Mailing list info URL for lists hosted on the IETF servers
MAILING_LIST_INFO_URL = "https://www.ietf.org/mailman/listinfo/%(list_addr)s"
@ -297,6 +298,7 @@ MEETING_DOC_HREFS = {
"agenda": "/meeting/{meeting}/agenda/{doc.group.acronym}/",
"minutes": "http://www.ietf.org/proceedings/{meeting}/minutes/{doc.external_url}",
"slides": "http://www.ietf.org/proceedings/{meeting}/slides/{doc.external_url}",
"recording": "{doc.external_url}",
}
# Override this in settings_local.py if needed

View file

@ -575,6 +575,10 @@ div.interim-scroll {
border-left: 1px solid #CCCCCC;
}
#recording-form #id_external_url {
width: 40em;
}
td.hidden {
display: none;
}

View file

@ -0,0 +1,24 @@
/* proceedings-recordings.js - utility functions */
$(document).ready(function() {
// auto populate Session select list
$('#id_group').blur(function(){
var loadUrl = "/secr/proceedings/ajax/get-sessions/";
var url = window.location.pathname;
var parts = url.split("/");
var acronym = $(this).val();
loadUrl = loadUrl+parts[3]+"/"+acronym+"/";
$('.errorlist').remove();
$.getJSON(loadUrl,function(data) {
$('#id_session').find('option').remove();
if (data.length == 0) {
$( '<ul class="errorlist"><li>No sessions found</li></ul>' ).insertBefore( "#id_group" );
} else {
$.each(data,function(i,item) {
$('#id_session').append('<option value="'+item.id+'">'+item.value+'</option>');
});
}
});
});
});