refactor: Remove secr proceedings (#5256)

* refactor: remove import_audio_files() and related code

* refactor: move functions from proc_utils to meeting/utils

* refactor: remove secr/proceedings
This commit is contained in:
Ryan Cross 2023-03-10 13:33:01 -08:00 committed by GitHub
parent 61504b14aa
commit b654b49d6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 178 additions and 1620 deletions

View file

@ -8,7 +8,6 @@ import os
import sys
from importlib import import_module
from mock import patch
from pathlib import Path
from django.apps import apps
@ -27,7 +26,6 @@ from ietf.doc.models import RelatedDocument, State
from ietf.doc.factories import IndividualDraftFactory, WgDraftFactory
from ietf.group.factories import RoleFactory
from ietf.meeting.factories import MeetingFactory, SessionFactory
from ietf.meeting.test_data import make_meeting_test_data
from ietf.meeting.models import Session
from ietf.person.factories import PersonFactory, random_faker
from ietf.person.models import User
@ -46,20 +44,6 @@ OMITTED_APPS = (
class CustomApiTests(TestCase):
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH']
# 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')
def test_notify_meeting_import_audio_files(self, mock_import_audio):
meeting = make_meeting_test_data()
client = Client(Accept='application/json')
# try invalid method GET
url = urlreverse('ietf.meeting.views.api_import_recordings', kwargs={'number':meeting.number})
r = client.get(url)
self.assertEqual(r.status_code, 405)
# try valid method POST
r = client.post(url)
self.assertEqual(r.status_code, 201)
def test_api_help_page(self):
url = urlreverse('ietf.api.views.api_help')
r = self.client.get(url)

View file

@ -32,8 +32,6 @@ urlpatterns = [
url(r'^meeting/(?P<num>[A-Za-z0-9._+-]+)/agenda-data$', meeting_views.api_get_agenda_data),
# Meeting session materials
url(r'^meeting/session/(?P<session_id>[A-Za-z0-9._+-]+)/materials$', meeting_views.api_get_session_materials),
# Let Meetecho trigger recording imports
url(r'^notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings),
# Let MeetEcho upload bluesheets
url(r'^notify/meeting/bluesheet/?$', meeting_views.api_upload_bluesheet),
# Let MeetEcho tell us about session attendees

View file

@ -12,7 +12,7 @@ from ietf.doc.models import DocEvent
from ietf.meeting.models import Meeting, SessionPresentation
from ietf.person.models import Person
from ietf.secr.proceedings.proc_utils import is_powerpoint, post_process
from ietf.meeting.utils import is_powerpoint, post_process
class Command(BaseCommand):
help = ('Fix uploaded_filename and generate pdf from pptx')

View file

@ -63,7 +63,7 @@ from ietf.iesg.models import TelechatDate
from ietf.iesg.utils import telechat_page_count
from ietf.ietfauth.utils import has_role, role_required, user_is_person
from ietf.person.models import Person
from ietf.secr.proceedings.proc_utils import get_activity_stats
from ietf.meeting.utils import get_activity_stats
from ietf.doc.utils_search import fill_in_document_table_attributes, fill_in_telechat_date
from ietf.utils.timezone import date_today, datetime_from_date

View file

@ -47,6 +47,7 @@ from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignm
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting, make_interim_test_data
from ietf.meeting.utils import finalize, condition_slide_order
from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.meeting.utils import create_recording, get_next_sequence
from ietf.meeting.views import session_draft_list, parse_agenda_filter_params, sessions_post_save, agenda_extract_schedule
from ietf.name.models import SessionStatusName, ImportantDateName, RoleName, ProceedingsMaterialTypeName
from ietf.utils.decorators import skip_coverage
@ -8095,3 +8096,20 @@ class ProceedingsTests(BaseMeetingTestCase):
pm = meeting.proceedings_materials.get(pk=pm.pk)
self.assertEqual(str(pm), 'This Is Not the Default Name')
self.assertEqual(pm.document.rev, orig_rev, 'Renaming should not change document revision')
def test_create_recording(self):
session = SessionFactory(meeting__type_id='ietf', meeting__number=72, group__acronym='mars')
filename = 'ietf42-testroomt-20000101-0800.mp3'
url = settings.IETF_AUDIO_URL + 'ietf{}/{}'.format(session.meeting.number, filename)
doc = create_recording(session, url)
self.assertEqual(doc.name,'recording-72-mars-1')
self.assertEqual(doc.group,session.group)
self.assertEqual(doc.external_url,url)
self.assertTrue(doc in session.materials.all())
def test_get_next_sequence(self):
session = SessionFactory(meeting__type_id='ietf', meeting__number=72, group__acronym='mars')
meeting = session.meeting
group = session.group
sequence = get_next_sequence(group,meeting,'recording')
self.assertEqual(sequence,1)

View file

@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
import datetime
import itertools
import os
import pytz
import requests
import subprocess
@ -19,13 +20,14 @@ from django.utils.encoding import smart_text
import debug # pyflakes:ignore
from ietf.dbtemplate.models import DBTemplate
from ietf.meeting.models import Session, SchedulingEvent, TimeSlot, Constraint, SchedTimeSessAssignment
from ietf.meeting.models import (Session, SchedulingEvent, TimeSlot,
Constraint, SchedTimeSessAssignment, SessionPresentation)
from ietf.doc.models import Document, DocAlias, State, NewRevisionDocEvent
from ietf.doc.models import DocEvent
from ietf.group.models import Group
from ietf.group.utils import can_manage_materials
from ietf.name.models import SessionStatusName, ConstraintName, DocTypeName
from ietf.person.models import Person
from ietf.secr.proceedings.proc_utils import import_audio_files
from ietf.utils.html import sanitize_document
from ietf.utils.log import log
from ietf.utils.timezone import date_today
@ -180,7 +182,6 @@ def finalize(meeting):
sp.rev = '00'
sp.save()
import_audio_files(meeting)
create_proceedings_templates(meeting)
meeting.proceedings_final = True
meeting.save()
@ -756,3 +757,156 @@ def write_doc_for_session(session, type_id, filename, contents):
with open(path / filename, "wb") as file:
file.write(contents.encode('utf-8'))
return
def create_recording(session, url, title=None, user=None):
'''
Creates the Document type=recording, setting external_url and creating
NewRevisionDocEvent
'''
sequence = get_next_sequence(session.group,session.meeting,'recording')
name = 'recording-{}-{}-{}'.format(session.meeting.number,session.group.acronym,sequence)
time = session.official_timeslotassignment().timeslot.time.strftime('%Y-%m-%d %H:%M')
if not title:
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=session.group,
rev='00',
type_id='recording')
doc.set_state(State.objects.get(type='recording', slug='active'))
DocAlias.objects.create(name=doc.name).docs.add(doc)
# create DocEvent
NewRevisionDocEvent.objects.create(type='new_revision',
by=user or Person.objects.get(name='(System)'),
doc=doc,
rev=doc.rev,
desc='New revision available',
time=doc.time)
pres = SessionPresentation.objects.create(session=session,document=doc,rev=doc.rev)
session.sessionpresentation_set.add(pres)
return doc
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
'''
aliases = DocAlias.objects.filter(name__startswith='{}-{}-{}-'.format(type, meeting.number, group.acronym))
if not aliases:
return 1
aliases = aliases.order_by('name')
sequence = int(aliases.last().name.split('-')[-1]) + 1
return sequence
def get_activity_stats(sdate, edate):
'''
This function takes a date range and produces a dictionary of statistics / objects for
use in an activity report. Generally the end date will be the date of the last meeting
and the start date will be the date of the meeting before that.
Data between midnight UTC on the specified dates are included in the stats.
'''
sdatetime = pytz.utc.localize(datetime.datetime.combine(sdate, datetime.time()))
edatetime = pytz.utc.localize(datetime.datetime.combine(edate, datetime.time()))
data = {}
data['sdate'] = sdate
data['edate'] = edate
events = DocEvent.objects.filter(doc__type='draft', time__gte=sdatetime, time__lt=edatetime)
data['actions_count'] = events.filter(type='iesg_approved').count()
data['last_calls_count'] = events.filter(type='sent_last_call').count()
new_draft_events = events.filter(newrevisiondocevent__rev='00')
new_drafts = list(set([e.doc_id for e in new_draft_events]))
data['new_docs'] = list(set([e.doc for e in new_draft_events]))
data['new_drafts_count'] = len(new_drafts)
data['new_drafts_updated_count'] = events.filter(doc__id__in=new_drafts,newrevisiondocevent__rev='01').count()
data['new_drafts_updated_more_count'] = events.filter(doc__id__in=new_drafts,newrevisiondocevent__rev='02').count()
update_events = events.filter(type='new_revision').exclude(doc__id__in=new_drafts)
data['updated_drafts_count'] = len(set([e.doc_id for e in update_events]))
# Calculate Final Four Weeks stats (ffw)
ffwdate = edatetime - datetime.timedelta(days=28)
ffw_new_count = events.filter(time__gte=ffwdate, newrevisiondocevent__rev='00').count()
try:
ffw_new_percent = format(ffw_new_count / float(data['new_drafts_count']), '.0%')
except ZeroDivisionError:
ffw_new_percent = 0
data['ffw_new_count'] = ffw_new_count
data['ffw_new_percent'] = ffw_new_percent
ffw_update_events = events.filter(time__gte=ffwdate, type='new_revision').exclude(doc__id__in=new_drafts)
ffw_update_count = len(set([e.doc_id for e in ffw_update_events]))
try:
ffw_update_percent = format(ffw_update_count / float(data['updated_drafts_count']),'.0%')
except ZeroDivisionError:
ffw_update_percent = 0
data['ffw_update_count'] = ffw_update_count
data['ffw_update_percent'] = ffw_update_percent
rfcs = events.filter(type='published_rfc')
data['rfcs'] = rfcs.select_related('doc').select_related('doc__group').select_related('doc__intended_std_level')
data['counts'] = {'std': rfcs.filter(doc__intended_std_level__in=('ps', 'ds', 'std')).count(),
'bcp': rfcs.filter(doc__intended_std_level='bcp').count(),
'exp': rfcs.filter(doc__intended_std_level='exp').count(),
'inf': rfcs.filter(doc__intended_std_level='inf').count()}
data['new_groups'] = Group.objects.filter(
type='wg',
groupevent__changestategroupevent__state='active',
groupevent__time__gte=sdatetime,
groupevent__time__lt=edatetime)
data['concluded_groups'] = Group.objects.filter(
type='wg',
groupevent__changestategroupevent__state='conclude',
groupevent__time__gte=sdatetime,
groupevent__time__lt=edatetime)
return data
def is_powerpoint(doc):
'''
Returns true if document is a Powerpoint presentation
'''
return doc.file_extension() in ('ppt', 'pptx')
def post_process(doc):
'''
Does post processing on uploaded file.
- Convert PPT to PDF
'''
if is_powerpoint(doc) and hasattr(settings, 'SECR_PPT2PDF_COMMAND'):
try:
cmd = list(settings.SECR_PPT2PDF_COMMAND) # Don't operate on the list actually in settings
cmd.append(doc.get_file_path()) # outdir
cmd.append(os.path.join(doc.get_file_path(), doc.uploaded_filename)) # filename
subprocess.check_call(cmd)
except (subprocess.CalledProcessError, OSError) as error:
log("Error converting PPT: %s" % (error))
return
# change extension
base, ext = os.path.splitext(doc.uploaded_filename)
doc.uploaded_filename = base + '.pdf'
e = DocEvent.objects.create(
type='changed_document',
by=Person.objects.get(name="(System)"),
doc=doc,
rev=doc.rev,
desc='Converted document to PDF',
)
doc.save_with_history([e])

View file

@ -82,10 +82,9 @@ from ietf.meeting.utils import diff_meeting_schedules, prefetch_schedule_diff_ob
from ietf.meeting.utils import swap_meeting_schedule_timeslot_assignments, bulk_create_timeslots
from ietf.meeting.utils import preprocess_meeting_important_dates
from ietf.meeting.utils import new_doc_for_session, write_doc_for_session
from ietf.meeting.utils import get_activity_stats, post_process, create_recording
from ietf.message.utils import infer_message
from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName
from ietf.secr.proceedings.proc_utils import (get_activity_stats, post_process, import_audio_files,
create_recording)
from ietf.utils import markdown
from ietf.utils.decorators import require_api_key
from ietf.utils.hedgedoc import Note, NoteError
@ -3798,16 +3797,6 @@ class OldUploadRedirect(RedirectView):
def get_redirect_url(self, **kwargs):
return reverse_lazy('ietf.meeting.views.session_details',kwargs=self.kwargs)
@csrf_exempt
def api_import_recordings(request, number):
'''REST API to check for recording files and import'''
if request.method == 'POST':
meeting = get_meeting(number)
import_audio_files(meeting)
return HttpResponse(status=201)
else:
return HttpResponse(status=405)
@require_api_key
@role_required('Recording Manager')
@csrf_exempt

View file

@ -1,44 +0,0 @@
# Copyright The IETF Trust 2007-2019, All Rights Reserved
from django import forms
from ietf.doc.models import Document
from ietf.meeting.models import Session
from ietf.meeting.utils import add_event_info_to_session_qs
# ---------------------------------------------
# Globals
# ---------------------------------------------
VALID_SLIDE_EXTENSIONS = ('.doc','.docx','.pdf','.ppt','.pptx','.txt','.zip')
VALID_MINUTES_EXTENSIONS = ('.txt','.html','.htm','.pdf')
VALID_AGENDA_EXTENSIONS = ('.txt','.html','.htm')
VALID_BLUESHEET_EXTENSIONS = ('.pdf','.jpg','.jpeg')
#----------------------------------------------------------
# Forms
#----------------------------------------------------------
class RecordingForm(forms.Form):
external_url = forms.URLField(label='Url')
session = forms.ModelChoiceField(queryset=Session.objects)
session.widget.attrs['class'] = "select2-field"
session.widget.attrs['data-minimum-input-length'] = 0
def __init__(self, *args, **kwargs):
self.meeting = kwargs.pop('meeting')
super(RecordingForm, self).__init__(*args,**kwargs)
self.fields['session'].queryset = add_event_info_to_session_qs(
Session.objects.filter(meeting=self.meeting, type__in=['regular','plenary','other'])
).filter(current_status='sched').order_by('group__acronym')
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'

View file

@ -1,28 +0,0 @@
# Copyright The IETF Trust 2018-2020, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-02-20 10:52
from django.db import migrations
class Migration(migrations.Migration):
initial = True
dependencies = [
('meeting', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='InterimMeeting',
fields=[
],
options={
'proxy': True,
'indexes': [],
},
bases=('meeting.meeting',),
),
]

View file

@ -1,62 +0,0 @@
# Copyright The IETF Trust 2013-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import os
from django.conf import settings
from django.db import models
from ietf.meeting.models import Meeting
class InterimManager(models.Manager):
'''A custom manager to limit objects to type=interim'''
def get_queryset(self):
return super(InterimManager, self).get_queryset().filter(type='interim')
class InterimMeeting(Meeting):
'''
This class is a proxy of Meeting. It's purpose is to provide extra methods that are
useful for an interim meeting, to help in templates. Most information is derived from
the session associated with this meeting. We are assuming there is only one.
'''
class Meta:
proxy = True
objects = InterimManager()
def group(self):
return self.session_set.all()[0].group
def agenda(self): # pylint: disable=method-hidden
session = self.session_set.all()[0]
agendas = session.materials.exclude(states__slug='deleted').filter(type='agenda')
if agendas:
return agendas[0]
else:
return None
def minutes(self):
session = self.session_set.all()[0]
minutes = session.materials.exclude(states__slug='deleted').filter(type='minutes')
if minutes:
return minutes[0]
else:
return None
def get_proceedings_path(self, group=None):
return os.path.join(self.get_materials_path(),'proceedings.html')
def get_proceedings_url(self, group=None):
'''
If the proceedings file doesn't exist return empty string. For use in templates.
'''
if os.path.exists(self.get_proceedings_path()):
url = "%sproceedings/%s/proceedings.html" % (
settings.IETF_HOST_URL,
self.number)
return url
else:
return ''

View file

@ -1,50 +0,0 @@
CREATE TABLE `interim_slides` (
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`meeting_num` integer NOT NULL,
`group_acronym_id` integer,
`slide_num` integer,
`slide_type_id` integer NOT NULL,
`slide_name` varchar(255) NOT NULL,
`irtf` integer NOT NULL,
`interim` bool NOT NULL,
`order_num` integer,
`in_q` integer
)
;
CREATE TABLE `interim_minutes` (
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`meeting_num` integer NOT NULL,
`group_acronym_id` integer NOT NULL,
`filename` varchar(255) NOT NULL,
`irtf` integer NOT NULL,
`interim` bool NOT NULL
)
;
CREATE TABLE `interim_agenda` (
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`meeting_num` integer NOT NULL,
`group_acronym_id` integer NOT NULL,
`filename` varchar(255) NOT NULL,
`irtf` integer NOT NULL,
`interim` bool NOT NULL
)
;
CREATE TABLE `interim_meetings` (
`meeting_num` integer NOT NULL PRIMARY KEY AUTO_INCREMENT,
`start_date` date ,
`end_date` date ,
`city` varchar(255) ,
`state` varchar(255) ,
`country` varchar(255) ,
`time_zone` integer,
`ack` longtext ,
`agenda_html` longtext ,
`agenda_text` longtext ,
`future_meeting` longtext ,
`overview1` longtext ,
`overview2` longtext ,
`group_acronym_id` integer
)
;
alter table interim_meetings auto_increment=201;

View file

@ -1,305 +0,0 @@
# Copyright The IETF Trust 2013-2020, All Rights Reserved
# -*- coding: utf-8 -*-
'''
proc_utils.py
This module contains all the functions for generating static proceedings pages
'''
import datetime
import os
import pytz
import re
import subprocess
from urllib.parse import urlencode
import debug # pyflakes:ignore
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from ietf.doc.models import Document, DocAlias, DocEvent, NewRevisionDocEvent, State
from ietf.group.models import Group
from ietf.meeting.models import Meeting, SessionPresentation, TimeSlot, SchedTimeSessAssignment, Session
from ietf.person.models import Person
from ietf.utils.log import log
from ietf.utils.mail import send_mail
from ietf.utils.timezone import make_aware
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]+)-(?P<name>.*)-(?P<date>\d{8})-(?P<time>\d{4})')
def _get_session(number,name,date,time):
'''Lookup session using data from video title'''
meeting = Meeting.objects.get(number=number)
timeslot_time = make_aware(datetime.datetime.strptime(date + time,'%Y%m%d%H%M'), meeting.tz())
try:
assignment = SchedTimeSessAssignment.objects.get(
schedule__in = [meeting.schedule, meeting.schedule.base],
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 dictionary title,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):
'''
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.mp3
'''
unmatched_files = []
path = os.path.join(settings.MEETING_RECORDINGS_DIR, meeting.type.slug + meeting.number)
if not os.path.exists(path):
return None
for filename in os.listdir(path):
timeslot = get_timeslot_for_filename(filename)
if timeslot:
sessions = Session.objects.with_current_status().filter(
timeslotassignments__schedule=timeslot.meeting.schedule_id,
).filter(
current_status='sched',
).order_by('timeslotassignments__timeslot__time')
if not sessions:
continue
url = settings.IETF_AUDIO_URL + 'ietf{}/{}'.format(meeting.number, filename)
doc = get_or_create_recording_document(url, sessions[0])
attach_recording(doc, sessions)
else:
# use for reconciliation email
unmatched_files.append(filename)
if unmatched_files:
send_audio_import_warning(unmatched_files)
def get_timeslot_for_filename(filename):
'''Returns a timeslot matching the filename given.
NOTE: currently only works with ietfNN prefix (regular meetings)
'''
from ietf.meeting.utils import add_event_info_to_session_qs
basename, _ = os.path.splitext(filename)
match = AUDIO_FILE_RE.match(basename)
if match:
try:
meeting = Meeting.objects.get(number=match.groupdict()['number'])
room_mapping = {normalize_room_name(room.name): room.name for room in meeting.room_set.all()}
time = make_aware(datetime.datetime.strptime(match.groupdict()['time'],'%Y%m%d-%H%M'), meeting.tz())
slots = TimeSlot.objects.filter(
meeting=meeting,
location__name=room_mapping[match.groupdict()['room']],
time=time,
sessionassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None],
).distinct()
uncancelled_slots = [t for t in slots if not add_event_info_to_session_qs(t.sessions.all()).filter(current_status='canceled').exists()]
return uncancelled_slots[0]
except (ObjectDoesNotExist, KeyError, IndexError):
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.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)
DocAlias.objects.create(name=name).docs.add(doc)
def normalize_room_name(name):
'''Returns room name converted to be used as portion of filename'''
return name.lower().replace(' ','').replace('/','_')
def get_or_create_recording_document(url,session):
try:
return Document.objects.get(external_url=url)
except ObjectDoesNotExist:
return create_recording(session,url)
def create_recording(session, url, title=None, user=None):
'''
Creates the Document type=recording, setting external_url and creating
NewRevisionDocEvent
'''
sequence = get_next_sequence(session.group,session.meeting,'recording')
name = 'recording-{}-{}-{}'.format(session.meeting.number,session.group.acronym,sequence)
time = session.official_timeslotassignment().timeslot.time.strftime('%Y-%m-%d %H:%M')
if not title:
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=session.group,
rev='00',
type_id='recording')
doc.set_state(State.objects.get(type='recording', slug='active'))
DocAlias.objects.create(name=doc.name).docs.add(doc)
# create DocEvent
NewRevisionDocEvent.objects.create(type='new_revision',
by=user or Person.objects.get(name='(System)'),
doc=doc,
rev=doc.rev,
desc='New revision available',
time=doc.time)
pres = SessionPresentation.objects.create(session=session,document=doc,rev=doc.rev)
session.sessionpresentation_set.add(pres)
return doc
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
'''
aliases = DocAlias.objects.filter(name__startswith='{}-{}-{}-'.format(type,meeting.number,group.acronym))
if not aliases:
return 1
aliases = aliases.order_by('name')
sequence = int(aliases.last().name.split('-')[-1]) + 1
return sequence
def send_audio_import_warning(unmatched_files):
'''Send email to interested parties that some audio files weren't matched to timeslots'''
send_mail(request = None,
to = settings.AUDIO_IMPORT_EMAIL,
frm = "IETF Secretariat <ietf-secretariat@ietf.org>",
subject = "Audio file import warning",
template = "proceedings/audio_import_warning.txt",
context = dict(unmatched_files=unmatched_files),
extra = {})
# -------------------------------------------------
# End Recording Functions
# -------------------------------------------------
def get_activity_stats(sdate, edate):
'''
This function takes a date range and produces a dictionary of statistics / objects for
use in an activity report. Generally the end date will be the date of the last meeting
and the start date will be the date of the meeting before that.
Data between midnight UTC on the specified dates are included in the stats.
'''
sdatetime = pytz.utc.localize(datetime.datetime.combine(sdate, datetime.time()))
edatetime = pytz.utc.localize(datetime.datetime.combine(edate, datetime.time()))
data = {}
data['sdate'] = sdate
data['edate'] = edate
events = DocEvent.objects.filter(doc__type='draft', time__gte=sdatetime, time__lt=edatetime)
data['actions_count'] = events.filter(type='iesg_approved').count()
data['last_calls_count'] = events.filter(type='sent_last_call').count()
new_draft_events = events.filter(newrevisiondocevent__rev='00')
new_drafts = list(set([ e.doc_id for e in new_draft_events ]))
data['new_docs'] = list(set([ e.doc for e in new_draft_events ]))
data['new_drafts_count'] = len(new_drafts)
data['new_drafts_updated_count'] = events.filter(doc__id__in=new_drafts,newrevisiondocevent__rev='01').count()
data['new_drafts_updated_more_count'] = events.filter(doc__id__in=new_drafts,newrevisiondocevent__rev='02').count()
update_events = events.filter(type='new_revision').exclude(doc__id__in=new_drafts)
data['updated_drafts_count'] = len(set([ e.doc_id for e in update_events ]))
# Calculate Final Four Weeks stats (ffw)
ffwdate = edatetime - datetime.timedelta(days=28)
ffw_new_count = events.filter(time__gte=ffwdate,newrevisiondocevent__rev='00').count()
try:
ffw_new_percent = format(ffw_new_count / float(data['new_drafts_count']),'.0%')
except ZeroDivisionError:
ffw_new_percent = 0
data['ffw_new_count'] = ffw_new_count
data['ffw_new_percent'] = ffw_new_percent
ffw_update_events = events.filter(time__gte=ffwdate,type='new_revision').exclude(doc__id__in=new_drafts)
ffw_update_count = len(set([ e.doc_id for e in ffw_update_events ]))
try:
ffw_update_percent = format(ffw_update_count / float(data['updated_drafts_count']),'.0%')
except ZeroDivisionError:
ffw_update_percent = 0
data['ffw_update_count'] = ffw_update_count
data['ffw_update_percent'] = ffw_update_percent
rfcs = events.filter(type='published_rfc')
data['rfcs'] = rfcs.select_related('doc').select_related('doc__group').select_related('doc__intended_std_level')
data['counts'] = {'std':rfcs.filter(doc__intended_std_level__in=('ps','ds','std')).count(),
'bcp':rfcs.filter(doc__intended_std_level='bcp').count(),
'exp':rfcs.filter(doc__intended_std_level='exp').count(),
'inf':rfcs.filter(doc__intended_std_level='inf').count()}
data['new_groups'] = Group.objects.filter(
type='wg',
groupevent__changestategroupevent__state='active',
groupevent__time__gte=sdatetime,
groupevent__time__lt=edatetime)
data['concluded_groups'] = Group.objects.filter(
type='wg',
groupevent__changestategroupevent__state='conclude',
groupevent__time__gte=sdatetime,
groupevent__time__lt=edatetime)
return data
def is_powerpoint(doc):
'''
Returns true if document is a Powerpoint presentation
'''
return doc.file_extension() in ('ppt','pptx')
def post_process(doc):
'''
Does post processing on uploaded file.
- Convert PPT to PDF
'''
if is_powerpoint(doc) and hasattr(settings,'SECR_PPT2PDF_COMMAND'):
try:
cmd = list(settings.SECR_PPT2PDF_COMMAND) # Don't operate on the list actually in settings
cmd.append(doc.get_file_path()) # outdir
cmd.append(os.path.join(doc.get_file_path(),doc.uploaded_filename)) # filename
subprocess.check_call(cmd)
except (subprocess.CalledProcessError, OSError) as error:
log("Error converting PPT: %s" % (error))
return
# change extension
base,ext = os.path.splitext(doc.uploaded_filename)
doc.uploaded_filename = base + '.pdf'
e = DocEvent.objects.create(
type='changed_document',
by=Person.objects.get(name="(System)"),
doc=doc,
rev=doc.rev,
desc='Converted document to PDF',
)
doc.save_with_history([e])

View file

@ -1,61 +0,0 @@
from django import template
from ietf.person.models import Person
register = template.Library()
@register.filter
def abbr_status(value):
"""
Converts RFC Status to a short abbreviation
"""
d = {'Proposed Standard':'PS',
'Draft Standard':'DS',
'Standard':'S',
'Historic':'H',
'Informational':'I',
'Experimental':'E',
'Best Current Practice':'BCP',
'Internet Standard':'IS'}
return d.get(value,value)
@register.filter(name='display_duration')
def display_duration(value):
"""
Maps a session requested duration from select index to
label."""
map = {'0':'None',
'1800':'30 Minutes',
'3600':'1 Hour',
'5400':'1.5 Hours',
'7200':'2 Hours',
'9000':'2.5 Hours'}
if value in map:
return map[value]
else:
x=int(value)
return "%d Hours %d Minutes %d Seconds"%(x//3600,(x%3600)//60,x%60)
@register.filter
def is_ppt(value):
'''
Checks if the value ends in ppt or pptx
'''
if value.endswith('ppt') or value.endswith('pptx'):
return True
else:
return False
@register.filter
def smart_login(user):
'''
Expects a Person object. If person is a Secretariat returns "on behalf of the"
'''
if not isinstance (user, Person):
return user
if user.role_set.filter(name='secr',group__acronym='secretariat'):
return '%s, on behalf of the' % user
else:
return '%s, a chair of the' % user

View file

@ -1,192 +0,0 @@
# Copyright The IETF Trust 2013-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import debug # pyflakes:ignore
import io
import json
import os
from django.conf import settings
from django.urls import reverse
from ietf.doc.models import Document
from ietf.group.factories import RoleFactory
from ietf.meeting.models import SchedTimeSessAssignment, SchedulingEvent
from ietf.meeting.factories import MeetingFactory, SessionFactory
from ietf.person.models import Person
from ietf.name.models import SessionStatusName
from ietf.utils.test_utils import TestCase
from ietf.utils.mail import outbox
from ietf.secr.proceedings.proc_utils import (import_audio_files,
get_timeslot_for_filename, normalize_room_name, send_audio_import_warning,
get_or_create_recording_document, create_recording, get_next_sequence,
_get_session, _get_urls_from_json)
SECR_USER='secretary'
class ProceedingsTestCase(TestCase):
def test_main(self):
"Main Test"
MeetingFactory(type_id='ietf')
RoleFactory(name_id='chair',person__user__username='marschairman')
url = reverse('ietf.secr.proceedings.views.main')
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# test chair access
self.client.logout()
self.client.login(username="marschairman", password="marschairman+password")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
class VideoRecordingTestCase(TestCase):
def test_get_session(self):
session = SessionFactory()
meeting = session.meeting
number = meeting.number
name = session.group.acronym
ts_time = session.official_timeslotassignment().timeslot.local_start_time()
date = ts_time.strftime('%Y%m%d')
time = ts_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 io.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):
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['MEETING_RECORDINGS_DIR']
def test_page(self):
meeting = MeetingFactory(type_id='ietf')
url = reverse('ietf.secr.proceedings.views.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):
session = SessionFactory(status_id='sched',meeting__type_id='ietf')
meeting = session.meeting
group = session.group
url = reverse('ietf.secr.proceedings.views.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.assertContains(response, group.acronym)
# now test edit
doc = session.materials.filter(type='recording').first()
external_url = 'http://youtube.com/aaa'
url = reverse('ietf.secr.proceedings.views.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.assertContains(response, external_url)
def test_import_audio_files(self):
session = SessionFactory(status_id='sched',meeting__type_id='ietf')
meeting = session.meeting
timeslot = session.official_timeslotassignment().timeslot
self.create_audio_file_for_timeslot(timeslot)
import_audio_files(meeting)
self.assertEqual(session.materials.filter(type='recording').count(),1)
def create_audio_file_for_timeslot(self, timeslot):
filename = self.get_filename_for_timeslot(timeslot)
path = os.path.join(settings.MEETING_RECORDINGS_DIR,'ietf' + timeslot.meeting.number,filename)
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
with io.open(path, "w") as f:
f.write('dummy')
def get_filename_for_timeslot(self, timeslot):
'''Returns the filename of a session recording given timeslot'''
return "{prefix}-{room}-{date}.mp3".format(
prefix=timeslot.meeting.type.slug + timeslot.meeting.number,
room=normalize_room_name(timeslot.location.name),
date=timeslot.local_start_time().strftime('%Y%m%d-%H%M'))
def test_import_audio_files_shared_timeslot(self):
meeting = MeetingFactory(type_id='ietf',number='72')
mars_session = SessionFactory(meeting=meeting,status_id='sched',group__acronym='mars')
ames_session = SessionFactory(meeting=meeting,status_id='sched',group__acronym='ames')
scheduled = SessionStatusName.objects.get(slug='sched')
SchedulingEvent.objects.create(
session=mars_session,
status=scheduled,
by=Person.objects.get(name='(System)')
)
SchedulingEvent.objects.create(
session=ames_session,
status=scheduled,
by=Person.objects.get(name='(System)')
)
timeslot = mars_session.official_timeslotassignment().timeslot
SchedTimeSessAssignment.objects.create(timeslot=timeslot,session=ames_session,schedule=meeting.schedule)
self.create_audio_file_for_timeslot(timeslot)
import_audio_files(meeting)
doc = mars_session.materials.filter(type='recording').first()
self.assertTrue(doc in ames_session.materials.all())
self.assertTrue(doc.docalias.filter(name='recording-72-mars-1'))
self.assertTrue(doc.docalias.filter(name='recording-72-ames-1'))
def test_normalize_room_name(self):
self.assertEqual(normalize_room_name('Test Room'),'testroom')
self.assertEqual(normalize_room_name('Rome/Venice'), 'rome_venice')
def test_get_timeslot_for_filename(self):
session = SessionFactory(meeting__type_id='ietf')
timeslot = session.timeslotassignments.first().timeslot
name = self.get_filename_for_timeslot(timeslot)
self.assertEqual(get_timeslot_for_filename(name),timeslot)
def test_get_or_create_recording_document(self):
session = SessionFactory(meeting__type_id='ietf', meeting__number=72, group__acronym='mars')
# test create
filename = 'ietf42-testroom-20000101-0800.mp3'
docs_before = Document.objects.filter(type='recording').count()
doc = get_or_create_recording_document(filename,session)
docs_after = Document.objects.filter(type='recording').count()
self.assertEqual(docs_after,docs_before + 1)
self.assertTrue(doc.external_url.endswith(filename))
# test get
docs_before = docs_after
doc2 = get_or_create_recording_document(filename,session)
docs_after = Document.objects.filter(type='recording').count()
self.assertEqual(docs_after,docs_before)
self.assertEqual(doc,doc2)
def test_create_recording(self):
session = SessionFactory(meeting__type_id='ietf', meeting__number=72, group__acronym='mars')
filename = 'ietf42-testroomt-20000101-0800.mp3'
url = settings.IETF_AUDIO_URL + 'ietf{}/{}'.format(session.meeting.number, filename)
doc = create_recording(session, url)
self.assertEqual(doc.name,'recording-72-mars-1')
self.assertEqual(doc.group,session.group)
self.assertEqual(doc.external_url,url)
self.assertTrue(doc in session.materials.all())
def test_get_next_sequence(self):
session = SessionFactory(meeting__type_id='ietf', meeting__number=72, group__acronym='mars')
meeting = session.meeting
group = session.group
sequence = get_next_sequence(group,meeting,'recording')
self.assertEqual(sequence,1)
def test_send_audio_import_warning(self):
length_before = len(outbox)
send_audio_import_warning(['recording-43-badroom-20000101-0800.mp3'])
self.assertEqual(len(outbox), length_before + 1)
self.assertTrue('Audio file import' in outbox[-1]['Subject'])

View file

@ -1,16 +0,0 @@
from django.conf import settings
from ietf.meeting.views import OldUploadRedirect
from ietf.utils.urls import url
from ietf.secr.proceedings import views
urlpatterns = [
url(r'^$', views.main),
# special offline URL for testing proceedings build
url(r'^process-pdfs/(?P<meeting_num>\d{1,3})/$', views.process_pdfs),
url(r'^(?P<meeting_num>\d{1,3})/$', views.select),
url(r'^(?P<meeting_num>\d{1,3})/recording/$', views.recording),
url(r'^(?P<meeting_num>\d{1,3})/recording/edit/(?P<name>[A-Za-z0-9_\-\+]+)$', views.recording_edit),
url(r'^(?P<num>\d{1,3}|interim-\d{4}-[A-Za-z0-9_\-\+]+)/%(acronym)s/$' % settings.URL_REGEXPS,
OldUploadRedirect.as_view(permanent=True)),
]

View file

@ -1,324 +0,0 @@
# Copyright The IETF Trust 2013-2020, All Rights Reserved
# -*- coding: utf-8 -*-
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.person.models import Person
from ietf.ietfauth.utils import has_role, role_required
from ietf.meeting.models import Meeting, Session
from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.secr.proceedings.forms import RecordingForm, RecordingEditForm
from ietf.secr.proceedings.proc_utils import (create_recording)
from ietf.utils.timezone import date_today
# -------------------------------------------------
# Globals
# -------------------------------------------------
AUTHORIZED_ROLES=('WG Chair','WG Secretary','RG Chair','RG Secretary', 'AG Secretary', 'RAG 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.
'''
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.uploaded_filename)
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_id == 'regular' 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.
'''
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'))
# --------------------------------------------------
# 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 = 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_sessions = add_event_info_to_session_qs(Session.objects.filter(group__in=groups, meeting__type='interim')).filter(current_status='sched').select_related('meeting')
interim_meetings = sorted({s.meeting for s in interim_sessions}, key=lambda m: m.date, reverse=True)
# 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 = 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',uploaded_filename__endswith='.ppt').exclude(states__slug='deleted')
pptx = Document.objects.filter(session__meeting=meeting,type='slides',uploaded_filename__endswith='.pptx').exclude(states__slug='deleted')
for doc in itertools.chain(ppt,pptx):
base,ext = os.path.splitext(doc.uploaded_filename)
pdf_file = base + '.pdf'
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting_num,'slides',pdf_file)
if os.path.exists(path):
doc.uploaded_filename = 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 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)
sessions = Session.objects.filter(
timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]
).exclude(
type__in=['reg','break']
).order_by('group__acronym')
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)
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='Changed URL to %s' % recording.external_url,
)
recording.save_with_history([e])
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), datetime.timezone.utc)
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',uploaded_filename__endswith='.ppt').exclude(states__slug='deleted')
pptx = Document.objects.filter(session__meeting=meeting,type='slides',uploaded_filename__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},
)

View file

@ -1,13 +0,0 @@
<p>Use this to process meeting materials files that have been converted to PDF and uploaded to the server.</p>
<ul class="none">
<li>
<button type="button" onclick="window.location='{% url 'ietf.secr.proceedings.views.process_pdfs' meeting_num=meeting.number %}'">Process PDFs</button>
&nbsp;&nbsp;<span class="{% if ppt_count > 0 %}alert{% endif %}">{{ ppt_count }} PowerPoint files waiting to be converted</span>
</li>
</ul>
<p>Use this to input session recording information.</p>
<ul class="none">
<li><button type="button" onclick="window.location='{% url 'ietf.secr.proceedings.views.recording' meeting_num=meeting.number %}'">Recordings</button>
</li>
</ul>

View file

@ -1,6 +1,5 @@
<ul>
<li><a href="https://www.ietf.org/chairs/session-request-tool-instructions/" target="_blank">Instructions</a>.</li>
<li><a href="{% url 'ietf.secr.proceedings.views.main' %}">IETF Meeting Materials Management Tool</a>.</li>
<li>If you require assistance in using this tool, or wish to report a bug, then please send a message to <a href="mailto:ietf-action@ietf.org">ietf-action@ietf.org</a>.</li>
<li>To submit your request via email, please send your request to <a href="mailto:agenda@ietf.org">agenda@ietf.org</a>.</li>
</ul>

View file

@ -1,6 +0,0 @@
<ul>
<li><a href="https://www.ietf.org/instructions/meeting_materials_tool.html" target="_blank">Instructions</a>.</li>
<li>If you require assistance in using this tool, or wish to report a bug, then please send a message to <a href="mailto:{{settings.SECRETARIAT_ACTION_EMAIL}}">ietf-action@ietf.org</a>.</li>
<li>To submit your materials via email, please send agendas to <a href="mailto:agenda@ietf.org">agenda@ietf.org</a> and minutes/presentation slides to <a href="mailto:proceedings@ietf.org">proceedings@ietf.org</a>.</li>
<li><b>Note:</b> Normal session materials materials management is now performed using the {% if meeting.number %}<a href="{% url 'ietf.meeting.views.materials' num=meeting.number %}">{% endif %}materials page{% if meeting.number %}</a>{% endif %}</li>
</ul>

View file

@ -32,7 +32,6 @@
<h2>Meetings and Proceedings</h2>
<ul>
<li> <a href="{% url "ietf.secr.sreq.views.main" %}"><b>Session Requests</b></a></li>
<li> <a href="{% url 'ietf.secr.proceedings.views.main' %}"><b>Meeting Materials Manager (Proceedings)</b></a></li>
<li> <a href="{% url "ietf.secr.meetings.views.main" %}"><b>Meeting Manager</b></a></li>
<li> <a href="{% url "ietf.secr.meetings.views.blue_sheet_redirect" %}"><b>Blue Sheets</b></a></li>
</ul>
@ -56,7 +55,6 @@
<h2>Section 1</h2>
<ul>
<li> <a href="{% url "ietf.secr.sreq.views.main" %}"><b>Session Requests</b></a></li>
<li> <a href="{% url 'ietf.secr.proceedings.views.main' %}"><b>Meeting Material Manager</b></a></li>
<li> <a href="{% url 'ietf.secr.announcement.views.main' %}"><b>Announcements</b></a></li>
</ul>
</td>

View file

@ -1,9 +0,0 @@
WARNING:
After the last meeting session audio file import there are {{ unmatched_files|length }}
file(s) that were not matched to a timeslot.
{% for file in unmatched_files %}{{ file }}
{% endfor %}

View file

@ -1,36 +0,0 @@
{% extends "base_site.html" %}
{% block content %}
<h2>Interim Meeting Proceedings</h2>
<table id="interim-directory-table">
<tr>
<td><a href="https://www.ietf.org/meeting/interim/proceedings.html">Date</a></td>
<td><a href="https://www.ietf.org/meeting/interim/proceedings-bygroup.html">Group</a></td>
<td></td>
<td></td>
</tr>
{% for meeting in meetings %}
<tr class="{% cycle 'row1' 'row2' %}">
<td class="text-start text-nowrap">{{ meeting.date }}</td>
<td><a href="{% url 'ietf.group.views.group_home' acronym=meeting.group.acronym %}">{{ meeting.group.acronym }}</a></td>
{% if meeting.schedule %}
<td class="text-center"><a href="{{ meeting.schedule.get_absolute_url }}">Agenda</a></td>
{% else %}
<td class="text-center">Agenda</td>
{% endif %}
{% if meeting.minutes %}
<td class="text-center"><a href="{{ meeting.minutes.get_absolute_url }}">Minutes</a></td>
{% else %}
<td class="text-center">Minutes</td>
{% endif %}
{% if meeting.get_proceedings_url %}
<td><a href="{{ meeting.get_proceedings_url }}">Proceedings</a></td>
{% else %}
<td>Proceedings</td>
{% endif %}
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -1,96 +0,0 @@
{% extends "base_site.html" %}
{% load ietf_filters %}
{% load staticfiles %}
{% block title %}Proceeding manager{% endblock %}
{% block extrahead %}{{ block.super }}
<script src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; Proceedings
{% endblock %}
{% block content %}
<div class="module" >
<h2>Proceedings</h2>
<div id="proceedings-left-col">
<table id="proceedings-list-table" class="full-width{% if user|has_role:"Secretariat" %} secretariat{% endif %}">
<thead>
<tr>
<th scope="col">IETF Meeting</th>
</tr>
</thead>
{% if meetings %}
<tbody>
{% for meeting in meetings %}
<tr class="{% cycle 'row1' 'row2' %}{% if meeting.get_submission_correction_date < today %} frozen{% else %} open{% endif %}">
<td>
<a href="{% url 'ietf.secr.proceedings.views.select' meeting_num=meeting.number %}">{{ meeting.number }}</a>
</td>
</tr>
{% endfor %}
</tbody>
{% endif %}
</table>
{% if user|has_role:"Secretariat" %}
<div class="button-group">
<ul id="proceedings-meeting-buttons">
<li><button type="button" onclick="window.location='{% url "ietf.secr.meetings.views.add" %}'">Add</button></li>
</ul>
</div> <!-- button-group -->
{% endif %}
</div>
<div id="proceedings-right-col">
<div class="interim-scroll">
<table id="proceedings-interim-table" class="full-width{% if user|has_role:"Secretariat" %} secretariat{% endif %}">
<thead>
<tr>
<th scope="col">Interim Meeting</th>
</tr>
</thead>
{% if interim_meetings %}
<tbody>
{% for meeting in interim_meetings %}
<tr class="{% cycle 'row1' 'row2' %}">
<td>{{ meeting.group.acronym }}</td>
<td><a href="{% url "ietf.meeting.views.session_details" num=meeting.number acronym=meeting.group.acronym %}">{{ meeting.date }}</a></td>
</tr>
{% endfor %}
</tbody>
{% endif %}
</table>
</div> <!-- scroll -->
<div class="button-group">
<ul id="proceedings-interim-buttons">
<li><button type="button" onclick="window.location='{% url "ietf.meeting.views.interim_request" %}'">Request Interim Meeting</button></li>
</ul>
</div> <!-- button-group -->
</div>
<br>
{% if not user|has_role:"Secretariat" %}
<br>
<hr>
<p>The list(s) above includes those meetings which you can upload materials for. Click on the meeting number or interim meeting date to continue.</p>
{% endif %}
<div class="button-group">
<ul id="proceedings-button-list">
<li><button type="button" onclick="window.location='../'">Back</button></li>
</ul>
</div> <!-- button-group -->
</div> <!-- module -->
{% endblock %}
{% block footer-extras %}
{% include "includes/upload_footer.html" %}
{% endblock %}
~
~
~

View file

@ -1,121 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles tz %}
{% block title %}Proceedings{% endblock %}
{% block extrastyle %}{{ block.super }}
<link rel="stylesheet" href="{% static 'ietf/css/jquery-ui.css' %}">
<link rel="stylesheet" href="{% static 'ietf/css/select2.css' %}">
{% endblock %}
{% block extrahead %}{{ block.super }}
<script src="{% static 'ietf/js/jquery-ui.js' %}"></script>
<script src="{% static 'ietf/js/select2.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
{% if meeting.type_id == "interim" %}
&raquo; <a href="{% url 'ietf.secr.proceedings.views.main' %}">Proceedings</a>
&raquo; {{ meeting }}
{% else %}
&raquo; <a href="{% url 'ietf.secr.proceedings.views.main' %}">Proceedings</a>
&raquo; <a href="{% url 'ietf.secr.proceedings.views.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">
<tbody>
<!-- [html-validate-disable-block wcag/h63 -- FIXME: as_table renders without scope] -->
{{ form.as_table }}
</tbody>
</table>
<div class="button-group">
<ul>
<li><button type="submit" name="submit" value="Submit">Submit</button></li>
<li><button type="button" onclick="window.location='../'">Back</button></li>
</ul>
</div> <!-- button-group -->
</form>
<div class="inline-related">
<h2>{{ meeting }} - Recordings</h2>
<table class="center">
<thead>
<tr>
<th scope="col">Group</th>
<th scope="col">Session</th>
<th scope="col">Name</th>
<th scope="col">URL</th>
<th scope="col">Edit</th>
</tr>
</thead>
{% if sessions %}
<tbody>{% timezone meeting.time_zone %}
{% for session in sessions %}
{% if session.recordings %}
{% for recording in session.recordings %}
<tr>
<td>{{ session.group.acronym }}</td>
<td>{{ session.official_timeslotassignment.timeslot.time|date:"m-d H:i" }}</td>
<td class="document-name" >{{ recording.name }}</td>
<td><a href="{{ recording.get_href }}">{{ recording.get_href }}</a></td>
<td><a href="{% url 'ietf.secr.proceedings.views.recording_edit' meeting_num=meeting.number name=recording.name %}">Edit</a></td>
</tr>
{% endfor %}
{% else %}
<tr>
<td>{{ session.group.acronym }}</td>
<td>{{ session.official_timeslotassignment.timeslot.time|date:"m-d H:i" }}</td>
<td></td>
<td></td>
<td></td>
</tr>
{% endif %}
{% endfor %}
{% endtimezone %}</tbody>
{% endif %}
</table>
</div> <!-- inline-group -->
{% if unmatched_recordings %}
<div class="inline-related">
<h2>Unmatched Recording Files</h2>
<table class="center">
<thead>
<tr>
<th scope="col">Filename</th>
</tr>
</thead>
{% if unmatched_recordings %}
<tbody>
{% for file in unmatched_recordings %}
<tr>
<td>{{ file }}</td>
</tr>
{% endfor %}
</tbody>
{% endif %}
</table>
</div> <!-- inline-group -->
{% endif %}
</div> <!-- module -->
{% endblock %}
{% block footer-extras %}
{% include "includes/upload_footer.html" %}
{% endblock %}

View file

@ -1,41 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Edit Recording{% endblock %}
{% block extrahead %}{{ block.super }}
<script src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
{% if meeting.type_id == "interim" %}
&raquo; <a href="{% url 'ietf.secr.proceedings.views.main' %}">Proceedings</a>
&raquo; {{ meeting }}
&raquo; <a href="{% url 'ietf.secr.proceedings.views.recording' meeting_num=meeting.number %}">Recording</a>
&raquo; {{ recording.name }}
{% else %}
&raquo; <a href="{% url 'ietf.secr.proceedings.views.main' %}">Proceedings</a>
&raquo; <a href="{% url 'ietf.secr.proceedings.views.select' meeting_num=meeting.number %}">{{ meeting.number }}</a>
&raquo; <a href="{% url 'ietf.secr.proceedings.views.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" method="post">{% csrf_token %}
<table>
<tbody>
<!-- [html-validate-disable-block wcag/h63 -- FIXME: as_table renders without scope] -->
{{ form.as_table }}
</tbody>
</table>
{% include "includes/buttons_save_cancel.html" %}
</form>
</div> <!-- module -->
{% endblock %}

View file

@ -1,39 +0,0 @@
{% extends "base_site.html" %}
{% load ietf_filters %}
{% load staticfiles %}
{% block title %}Proceedings{% endblock %}
{% block extrahead %}{{ block.super }}
<script src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="{% url 'ietf.secr.proceedings.views.main' %}">Proceedings</a>
&raquo; {{ meeting.number }}
{% endblock %}
{% block instructions %}
<a href="https://www.ietf.org/instructions/meeting_materials_tool.html" target="_blank">Instructions</a>
{% endblock %}
{% block content %}
<div class="module interim-container">
<div class="inline-related">
<h2>Secretariat Only Functions</h2>
<div id="private-functions">
{% include "includes/proceedings_functions.html" %}
</div> <!-- private-functions -->
</div> <!-- inline-group -->
</div> <!-- module -->
{% endblock %}
{% block footer-extras %}
{% include "includes/upload_footer.html" %}
{% endblock %}

View file

@ -1,47 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Proceedings - Status{% endblock %}
{% block extrahead %}{{ block.super }}
<script src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../../">Proceedings</a>
&raquo; <a href="../">{{ meeting.meeting_num }}</a>
&raquo; Status
{% endblock %}
{% block content %}
<ul class="errorlist"><li>Changing one of the status below will result in changing the status of Proceedings {{ meeting.meeting_num }}. </li></ul>
<div class="module">
<h2>IETF {{ meeting.meeting_num }}</h2>
<table class="center">
<form action="modify/" method="post">{% csrf_token %}
<tr>
<input type="hidden" name="frozen" value="{{ proceeding.frozen }}">
{% if not proceeding.frozen %}
<tr>
<td>Active Proceeding</td>
<td><button type="submit" name="submit">Freeze</button></td>
{% endif %}
{% if proceeding.frozen %}
<tr>
<td>Frozen Proceeding</td>
<td><button type="submit" name="submit">Activate</button></td>
{% endif %}
</tr>
</form>
</table>
<div class="button-group">
<ul>
<li><button type="button" onclick="history.go(-1);return true">Back</button></li>
</ul>
</div> <!-- button-group -->
</div> <!-- module -->
{% endblock %}

View file

@ -1,59 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Proceedings - View{% endblock %}
{% block extrahead %}{{ block.super }}
<script src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; <a href="../">Proceedings</a>
&raquo; {{ meeting.number }}
{% endblock %}
{% block content %}
<div class="module">
<h2>IETF {{meeting.number}} Meeting - View</h2>
{% if meeting.frozen == 1 %}
<ul class="errorlist"><li> THIS IS A FROZEN PROCEEDING</li></ul>
{% endif %}
<table class="full-width" id="proceedings-view-first-col">
<tbody>
<tr><td>Meeting Start Date:</td><td> {{ meeting.date }}</td></tr>
<tr><td>Meeting End Date:</td><td> {{ meeting.end_date }} </td></tr>
<tr><td>Meeting City:</td><td> {{ meeting.city }} </td></tr>
<tr><td>Meeting Country:</td><td> {{ meeting.country }} </td></tr>
</tbody>
</table>
<div class="inline-related">
<h2>Dates</h2>
<table class="full-width" id="proceedings-view-first-col">
<tbody>
<tr><td>Submission Start Date:</td><td> {{ meeting.get_submission_start_date }} </td></tr>
<tr><td>Submission Cut Off Date:</td><td> {{ meeting.get_submission_cut_off_date }} </td></tr>
<tr><td>Submission Correction Cut Off Date:</td><td> {{ meeting.get_submission_correction_date }} </td></tr>
<tr><td>Progress Report Start:</td><td> {{ meeting.pr_from_date }} </td></tr>
<tr><td>Progress Report End:</td><td> {{ meeting.pr_to_date }} </td></tr>
</tbody>
</table>
</div><!-- inline-related-->
<div class="button-group">
{% if meeting.frozen == 0 %}
<ul>
<li><button type="button" onclick="window.location='{% url 'ietf.secr.proceedings.views.select' meeting_num=meeting.number %}'">Upload Materials</button></li>
<li><button type="button" onclick="window.location='convert/'">Convert Materials</button></li>
<li><button type="button" onclick="window.location='status/'">Proceedings {{ meeting.number }} Status</button></li>
</ul>
{% endif %}
{% if meeting.frozen == 1 %}
<ul>
<li><button type="button" onclick="window.location='status/'">Proceedings {{ meeting.number }} Status</button></li>
</ul>
{% endif %}
</div> <!-- button-group -->
</div> <!-- module -->
{% endblock %}

View file

@ -1,30 +0,0 @@
{% extends "base_site.html" %}
{% load staticfiles %}
{% block title %}Proceeding manager{% endblock %}
{% block extrahead %}{{ block.super }}
<script src="{% static 'secr/js/utils.js' %}"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; Proceedings
{% endblock %}
{% block content %}
<div class="module" >
<h2>Proceedings</h2>
<hr>
<h2>{{ message }}</h2>
<img class="loading" src="{% static "secr/images/ajax-loader.webp" %}" alt="loading...">
</div> <!-- module -->
{% endblock %}
{% block footer-extras %}
{% include "includes/upload_footer.html" %}
{% endblock %}
~
~
~

View file

@ -8,7 +8,6 @@ urlpatterns = [
url(r'^console/', include('ietf.secr.console.urls')),
url(r'^groups/', include('ietf.secr.groups.urls')),
url(r'^meetings/', include('ietf.secr.meetings.urls')),
url(r'^proceedings/', include('ietf.secr.proceedings.urls')),
url(r'^roles/', include('ietf.secr.roles.urls')),
url(r'^rolodex/', include('ietf.secr.rolodex.urls')),
url(r'^sreq/', include('ietf.secr.sreq.urls')),

View file

@ -23,8 +23,6 @@
href="{% url 'ietf.meeting.views_proceedings.edit_meetinghosts' num=meeting.number %}">
Edit meeting hosts
</a>
<a class="btn btn-primary"
href="{% url 'ietf.secr.proceedings.views.main' %}">Secretariat proceedings functions</a>
{% if meeting.end_date.today > meeting.end_date %}
<a class="btn btn-primary"
href="{% url 'ietf.meeting.views.request_minutes' num=meeting.number %}">