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:
parent
61504b14aa
commit
b654b49d6b
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
@ -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',),
|
||||
),
|
||||
]
|
|
@ -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 ''
|
||||
|
|
@ -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;
|
||||
|
|
@ -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])
|
|
@ -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
|
|
@ -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'])
|
|
@ -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)),
|
||||
]
|
|
@ -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},
|
||||
)
|
||||
|
|
@ -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>
|
||||
<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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
|
@ -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 }}
|
||||
» 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 %}
|
||||
~
|
||||
~
|
||||
~
|
|
@ -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" %}
|
||||
» <a href="{% url 'ietf.secr.proceedings.views.main' %}">Proceedings</a>
|
||||
» {{ meeting }}
|
||||
{% else %}
|
||||
» <a href="{% url 'ietf.secr.proceedings.views.main' %}">Proceedings</a>
|
||||
» <a href="{% url 'ietf.secr.proceedings.views.select' meeting_num=meeting.number %}">{{ meeting.number }}</a>
|
||||
» 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 %}
|
|
@ -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" %}
|
||||
» <a href="{% url 'ietf.secr.proceedings.views.main' %}">Proceedings</a>
|
||||
» {{ meeting }}
|
||||
» <a href="{% url 'ietf.secr.proceedings.views.recording' meeting_num=meeting.number %}">Recording</a>
|
||||
» {{ recording.name }}
|
||||
{% else %}
|
||||
» <a href="{% url 'ietf.secr.proceedings.views.main' %}">Proceedings</a>
|
||||
» <a href="{% url 'ietf.secr.proceedings.views.select' meeting_num=meeting.number %}">{{ meeting.number }}</a>
|
||||
» <a href="{% url 'ietf.secr.proceedings.views.recording' meeting_num=meeting.number %}">Recording</a>
|
||||
» {{ 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 %}
|
|
@ -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 }}
|
||||
» <a href="{% url 'ietf.secr.proceedings.views.main' %}">Proceedings</a>
|
||||
» {{ 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 %}
|
|
@ -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 }}
|
||||
» <a href="../../">Proceedings</a>
|
||||
» <a href="../">{{ meeting.meeting_num }}</a>
|
||||
» 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 %}
|
|
@ -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 }}
|
||||
» <a href="../">Proceedings</a>
|
||||
» {{ 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 %}
|
|
@ -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 }}
|
||||
» 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 %}
|
||||
~
|
||||
~
|
||||
~
|
||||
|
|
@ -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')),
|
||||
|
|
|
@ -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 %}">
|
||||
|
|
Loading…
Reference in a new issue