datatracker/ietf/secr/proceedings/views.py

393 lines
15 KiB
Python

import datetime
import glob
import itertools
import os
import debug # pyflakes:ignore
from django.conf import settings
from django.contrib import messages
from django.urls import reverse
from django.db.models import Max
from django.http import HttpResponseRedirect
from django.shortcuts import render, get_object_or_404, redirect
from ietf.secr.utils.decorators import sec_only
from ietf.secr.utils.group import get_my_groups
from ietf.secr.utils.meeting import get_timeslot, get_proceedings_url
from ietf.doc.models import Document, DocEvent
from ietf.group.models import Group
from ietf.person.models import Person
from ietf.ietfauth.utils import has_role, role_required
from ietf.meeting.models import Meeting, Session, TimeSlot
from ietf.secr.proceedings.forms import RecordingForm, RecordingEditForm
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_recording )
# -------------------------------------------------
# Globals
# -------------------------------------------------
AUTHORIZED_ROLES=('WG Chair','WG Secretary','RG Chair','RG Secretary', 'AG Secretary','IRTF Chair','IETF Trust Chair','IAB Group Chair','IAOC Chair','IAD','Area Director','Secretariat','Team Chair')
# -------------------------------------------------
# Helper Functions
# -------------------------------------------------
def build_choices(queryset):
'''
This function takes a queryset (or list) of Groups and builds a list of tuples for use
as choices in a select widget. Using acronym for both value and label.
'''
choices = [ (g.acronym,g.acronym) for g in queryset ]
return sorted(choices, key=lambda choices: choices[1])
def find_index(slide_id, qs):
'''
This function looks up a slide in a queryset of slides,
returning the index.
'''
for i in range(0,qs.count()):
if str(qs[i].pk) == slide_id:
return i
def get_doc_filename(doc):
'''
This function takes a Document of type slides,minute or agenda and returns
the full path to the file on disk. During migration of the system the
filename was saved in external_url, new files will also use this convention.
'''
session = doc.session_set.all()[0]
meeting = session.meeting
if doc.external_url:
return os.path.join(meeting.get_materials_path(),doc.type.slug,doc.external_url)
else:
path = os.path.join(meeting.get_materials_path(),doc.type.slug,doc.name)
files = glob.glob(path + '.*')
# TODO we might want to choose from among multiple files using some logic
return files[0]
def get_unmatched_recordings(meeting):
'''
Returns a list of recording filenames that haven't been matched to a session
'''
unmatched_recordings = []
path = os.path.join(settings.MEETING_RECORDINGS_DIR,'ietf{}'.format(meeting.number))
try:
files = os.listdir(path)
except OSError:
files = []
url = settings.IETF_AUDIO_URL + 'ietf%s' % meeting.number
recordings = Document.objects.filter(type='recording',external_url__startswith=url)
filenames = [ d.external_url.split('/')[-1] for d in recordings ]
for file in files:
if file not in filenames:
unmatched_recordings.append(file)
return unmatched_recordings
def get_extras(meeting):
'''
Gather "extras" which are one off groups. ie iab-wcit(86)
'''
groups = []
sessions = Session.objects.filter(meeting=meeting).exclude(group__parent__type__in=('area','irtf'))
for session in sessions:
timeslot = get_timeslot(session)
if timeslot and timeslot.type.slug == 'session' and session.materials.all():
groups.append(session.group)
return groups
def get_next_slide_num(session):
'''
This function takes a session object and returns the
next slide number to use for a newly added slide as a string.
'''
"""
slides = session.materials.filter(type='slides').order_by('-name')
if slides:
# we need this special case for non wg/rg sessions because the name format is different
# it should be changed to match the rest
if session.group.type.slug not in ('wg','rg'):
nums = [ s.name.split('-')[3] for s in slides ]
else:
nums = [ s.name.split('-')[-1] for s in slides ]
"""
if session.meeting.type_id == 'ietf':
pattern = 'slides-%s-%s' % (session.meeting.number,session.group.acronym)
elif session.meeting.type_id == 'interim':
pattern = 'slides-%s' % (session.meeting.number)
slides = Document.objects.filter(type='slides',name__startswith=pattern)
if slides:
nums = [ s.name.split('-')[-1] for s in slides ]
nums.sort(key=int)
return str(int(nums[-1]) + 1)
else:
return '0'
def get_next_order_num(session):
'''
This function takes a session object and returns the
next slide order number to use for a newly added slide as an integer.
'''
max_order = session.materials.aggregate(Max('order'))['order__max']
return max_order + 1 if max_order else 1
def parsedate(d):
'''
This function takes a date object and returns a tuple of year,month,day
'''
return (d.strftime('%Y'),d.strftime('%m'),d.strftime('%d'))
# -------------------------------------------------
# AJAX Functions
# -------------------------------------------------
@sec_only
def ajax_generate_proceedings(request, meeting_num):
'''
Ajax function which takes a meeting number and generates the proceedings
pages for the meeting. It returns a snippet of HTML that gets placed in the
Secretariat Only section of the select page.
'''
meeting = get_object_or_404(Meeting, number=meeting_num)
areas = Group.objects.filter(type='area',state='active').order_by('name')
others = TimeSlot.objects.filter(sessionassignments__schedule=meeting.agenda,type='other').order_by('time')
extras = get_extras(meeting)
context = {'meeting':meeting,
'areas':areas,
'others':others,
'extras':extras,
'request':request}
proceedings_url = get_proceedings_url(meeting)
# the acknowledgement page can be edited manually so only produce if it doesn't already exist
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'acknowledgement.html')
if not os.path.exists(path):
gen_acknowledgement(context)
gen_overview(context)
gen_progress(context)
gen_agenda(context)
gen_attendees(context)
gen_index(context)
gen_areas(context)
gen_plenaries(context)
gen_training(context)
gen_irtf(context)
gen_research(context)
gen_group_pages(context)
# get the time proceedings were generated
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'index.html')
last_run = datetime.datetime.fromtimestamp(os.path.getmtime(path))
return render(request, 'includes/proceedings_functions.html',{
'meeting':meeting,
'last_run':last_run,
'proceedings_url':proceedings_url},
)
# --------------------------------------------------
# STANDARD VIEW FUNCTIONS
# --------------------------------------------------
@role_required(*AUTHORIZED_ROLES)
def main(request):
'''
List IETF Meetings. If the user is Secratariat list includes all meetings otherwise
show only those meetings whose corrections submission date has not passed.
**Templates:**
* ``proceedings/main.html``
**Template Variables:**
* meetings, interim_meetings, today
'''
if has_role(request.user,'Secretariat'):
meetings = Meeting.objects.filter(type='ietf').order_by('-number')
else:
# select meetings still within the cutoff period
today = datetime.date.today()
meetings = [m for m in Meeting.objects.filter(type='ietf').order_by('-number') if m.get_submission_correction_date()>=today]
groups = get_my_groups(request.user)
interim_meetings = Meeting.objects.filter(type='interim',session__group__in=groups,session__status='sched').order_by('-date')
# tac on group for use in templates
for m in interim_meetings:
m.group = m.session_set.first().group
# we today's date to see if we're past the submissio cutoff
today = datetime.date.today()
return render(request, 'proceedings/main.html',{
'meetings': meetings,
'interim_meetings': interim_meetings,
'today': today},
)
@sec_only
def process_pdfs(request, meeting_num):
'''
This function is used to update the database once meeting materials in PPT format
are converted to PDF format and uploaded to the server. It basically finds every PowerPoint
slide document for the given meeting and checks to see if there is a PDF version. If there
is external_url is changed. Then when proceedings are generated the URL will refer to the
PDF document.
'''
warn_count = 0
count = 0
meeting = get_object_or_404(Meeting, number=meeting_num)
ppt = Document.objects.filter(session__meeting=meeting,type='slides',external_url__endswith='.ppt').exclude(states__slug='deleted')
pptx = Document.objects.filter(session__meeting=meeting,type='slides',external_url__endswith='.pptx').exclude(states__slug='deleted')
for doc in itertools.chain(ppt,pptx):
base,ext = os.path.splitext(doc.external_url)
pdf_file = base + '.pdf'
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting_num,'slides',pdf_file)
if os.path.exists(path):
doc.external_url = pdf_file
e = DocEvent.objects.create(
type='changed_document',
by=Person.objects.get(name="(System)"),
doc=doc,
rev=doc.rev,
desc='Set URL to PDF version',
)
doc.save_with_history([e])
count += 1
else:
warn_count += 1
if warn_count:
messages.warning(request, '%s PDF files processed. %s PowerPoint files still not converted.' % (count, warn_count))
else:
messages.success(request, '%s PDF files processed' % count)
url = reverse('ietf.secr.proceedings.views.select', kwargs={'meeting_num':meeting_num})
return HttpResponseRedirect(url)
@role_required('Secretariat')
def progress_report(request, meeting_num):
'''
This function generates the proceedings progress report for use at the Plenary.
'''
meeting = get_object_or_404(Meeting, number=meeting_num)
gen_progress({'meeting':meeting},final=False)
url = reverse('ietf.secr.proceedings.views.select', kwargs={'meeting_num':meeting_num})
return HttpResponseRedirect(url)
@role_required('Secretariat')
def recording(request, meeting_num):
'''
Enter Session recording info. Creates Document and associates it with Session.
For auditing purposes, lists all scheduled sessions and associated recordings, if
any. Also lists those audio recording files which haven't been matched to a
session.
'''
meeting = get_object_or_404(Meeting, number=meeting_num)
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':
form = RecordingForm(request.POST,meeting=meeting)
if form.is_valid():
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('ietf.secr.proceedings.views.recording', meeting_num=meeting_num)
else:
create_recording(session,external_url)
# rebuild proceedings
create_proceedings(meeting,session.group)
messages.success(request,'Recording added')
return redirect('ietf.secr.proceedings.views.recording', meeting_num=meeting_num)
else:
form = RecordingForm(meeting=meeting)
return render(request, 'proceedings/recording.html',{
'meeting':meeting,
'form':form,
'sessions':sessions,
'unmatched_recordings': get_unmatched_recordings(meeting)},
)
@role_required('Secretariat')
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':
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
return redirect('ietf.secr.proceedings.views.recording', meeting_num=meeting_num)
form = RecordingEditForm(request.POST, instance=recording)
if form.is_valid():
# save record and rebuild proceedings
form.save(commit=False)
e = DocEvent.objects.create(
type='changed_document',
by=request.user.person,
doc=recording,
rev=recording.rev,
desc=u'Changed URL to %s' % recording.external_url,
)
recording.save_with_history([e])
create_proceedings(meeting,recording.group)
messages.success(request,'Recording saved')
return redirect('ietf.secr.proceedings.views.recording', meeting_num=meeting_num)
else:
form = RecordingEditForm(instance=recording)
return render(request, 'proceedings/recording_edit.html',{
'meeting':meeting,
'form':form,
'recording':recording},
)
# TODO - should probably rename this since it's not selecting groups anymore
def select(request, meeting_num):
'''
Provide the secretariat only functions related to meeting materials management
'''
if not has_role(request.user,'Secretariat'):
return HttpResponseRedirect(reverse('ietf.meeting.views.materials_editable_groups', kwargs={'num':meeting_num}))
meeting = get_object_or_404(Meeting, number=meeting_num)
proceedings_url = get_proceedings_url(meeting)
# get the time proceedings were generated
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'index.html')
if os.path.exists(path):
last_run = datetime.datetime.fromtimestamp(os.path.getmtime(path))
else:
last_run = None
# count PowerPoint files waiting to be converted
# TODO : This should look at SessionPresentation instead
ppt = Document.objects.filter(session__meeting=meeting,type='slides',external_url__endswith='.ppt').exclude(states__slug='deleted')
pptx = Document.objects.filter(session__meeting=meeting,type='slides',external_url__endswith='.pptx').exclude(states__slug='deleted')
ppt_count = ppt.count() + pptx.count()
return render(request, 'proceedings/select.html', {
'meeting': meeting,
'last_run': last_run,
'proceedings_url': proceedings_url,
'ppt_count': ppt_count},
)