diff --git a/hold-for-merge b/hold-for-merge index e43f62e22..0379a279c 100644 --- a/hold-for-merge +++ b/hold-for-merge @@ -1,6 +1,6 @@ # -*- conf-mode -*- -/branch/proceedings/6.33.1.dev0@12111 # Missing data migration to add dbtemplates ? + /branch/proceedings/6.29.1.dev0@11850 # Merged into /branch/proceedings/6.30.1.dev0, will be merged from there /branch/proceedings/6.29.1.dev0@11856 # Merged into /branch/proceedings/6.30.1.dev0, will be merged from there /personal/rcross/6.29.1.dev0@11765 # Merged into ^/personal/rjs/6.29.1.dev0@11770, will be merged from there diff --git a/ietf/meeting/migrations/0038_auto_20161013_1459.py b/ietf/meeting/migrations/0038_auto_20161013_1459.py new file mode 100644 index 000000000..15d82e05f --- /dev/null +++ b/ietf/meeting/migrations/0038_auto_20161013_1459.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + +from ietf.meeting.utils import create_proceedings_templates + +def create_attendee_templates(apps, schema_editor): + """Create attendee templates for supported meetings""" + Meeting = apps.get_model("meeting", "Meeting") + create_proceedings_templates(Meeting.objects.get(number=95)) + create_proceedings_templates(Meeting.objects.get(number=96)) + +class Migration(migrations.Migration): + + dependencies = [ + ('meeting', '0037_change_meta_options_on_sessionpresentation'), + ] + + operations = [ + migrations.RunPython(create_attendee_templates), + ] diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 34c3f5729..3fbd1da33 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -11,6 +11,7 @@ from django.core.urlresolvers import reverse as urlreverse from django.conf import settings from django.contrib.auth.models import User +from mock import patch from pyquery import PyQuery from StringIO import StringIO @@ -22,6 +23,7 @@ from ietf.meeting.helpers import send_interim_cancellation_notice from ietf.meeting.helpers import send_interim_minutes_reminder from ietf.meeting.models import Session, TimeSlot, Meeting from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting +from ietf.meeting.utils import finalize from ietf.name.models import SessionStatusName from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent from ietf.utils.mail import outbox @@ -33,6 +35,7 @@ from ietf.meeting.factories import ( SessionFactory, SessionPresentationFactory, MeetingFactory, FloorPlanFactory ) from ietf.doc.factories import DocumentFactory + class MeetingTests(TestCase): def setUp(self): self.materials_dir = os.path.abspath(settings.TEST_MATERIALS_DIR) @@ -272,41 +275,50 @@ class MeetingTests(TestCase): self.assertEqual(r.status_code, 200) def test_proceedings_acknowledgements(self): - meeting = make_meeting_test_data() + make_meeting_test_data() + meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="96") + meeting.acknowledgements = 'test acknowledgements' + meeting.save() url = urlreverse('ietf.meeting.views.proceedings_acknowledgements',kwargs={'num':meeting.number}) - login_testing_unauthorized(self,"secretary",url) - r = self.client.get(url) - self.assertEqual(r.status_code, 200) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTrue('test acknowledgements' in response.content) + + @patch('urllib2.urlopen') + def test_proceedings_attendees(self, mock_urlopen): + mock_urlopen.return_value = StringIO('[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]') + make_meeting_test_data() + meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="96") + finalize(meeting) + url = urlreverse('ietf.meeting.views.proceedings_attendees',kwargs={'num':96}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTrue('Attendee List' in response.content) + q = PyQuery(response.content) + self.assertEqual(1,len(q("#id_attendees tbody tr"))) def test_proceedings_overview(self): '''Test proceedings IETF Overview page. Note: old meetings aren't supported so need to add a new meeting then test. ''' make_meeting_test_data() - # add meeting requires a previous meeting to work - date = datetime.date(2016,7,14) - Meeting.objects.create(type_id='ietf',date=date,number=96) - url = urlreverse('ietf.secr.meetings.views.add') - post_data = dict(number='97',city='Seoul',date='2016-11-13',country='KR', - time_zone='Asia/Seoul',venue_name='Conrad Seoul', - venue_addr='10 Gukjegeumyung-ro', - idsubmit_cutoff_day_offset_00=13, - idsubmit_cutoff_day_offset_01=20, - idsubmit_cutoff_time_utc =datetime.timedelta(hours=23, minutes=59, seconds=59), - idsubmit_cutoff_warning_days =datetime.timedelta(days=21), - submission_start_day_offset=90, - submission_cutoff_day_offset=26, - submission_correction_day_offset=50, - ) - self.client.login(username='secretary', password='secretary+password') - - response = self.client.post(url, post_data) - self.assertRedirects(response,urlreverse('ietf.secr.meetings.views.main')) - url = urlreverse('ietf.meeting.views.proceedings_overview',kwargs={'num':97}) + meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="96") + finalize(meeting) + url = urlreverse('ietf.meeting.views.proceedings_overview',kwargs={'num':96}) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertTrue('The Internet Engineering Task Force' in response.content) + def test_proceedings_progress_report(self): + make_meeting_test_data() + MeetingFactory(type_id='ietf', date=datetime.date(2016,4,3), number="95") + MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="96") + + url = urlreverse('ietf.meeting.views.proceedings_progress_report',kwargs={'num':96}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTrue('Progress Report' in response.content) + def test_feed(self): meeting = make_meeting_test_data() session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() @@ -1279,7 +1291,9 @@ class IphoneAppJsonTests(TestCase): self.assertEqual(r.status_code,200) class FinalizeProceedingsTests(TestCase): - def test_finalize_proceedings(self): + @patch('urllib2.urlopen') + def test_finalize_proceedings(self, mock_urlopen): + mock_urlopen.return_value = StringIO('[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]') make_meeting_test_data() meeting = Meeting.objects.filter(type_id='ietf').order_by('id').last() meeting.session_set.filter(group__acronym='mars').first().sessionpresentation_set.create(document=Document.objects.filter(type='draft').first(),rev=None) diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py index 295ddb410..ef30e5891 100644 --- a/ietf/meeting/urls.py +++ b/ietf/meeting/urls.py @@ -78,7 +78,9 @@ type_ietf_only_patterns_id_optional = [ url(r'^proceedings(?:.html)?/?$', views.proceedings), url(r'^proceedings(?:.html)?/finalize/?$', views.finalize_proceedings), url(r'^proceedings/acknowledgements/$', views.proceedings_acknowledgements), + url(r'^proceedings/attendees/$', views.proceedings_attendees), url(r'^proceedings/overview/$', views.proceedings_overview), + url(r'^proceedings/progress-report/$', views.proceedings_progress_report), ] urlpatterns = [ diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index 8a5a08ed2..4828881d6 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -1,5 +1,12 @@ import datetime +import json +import urllib2 +import urlparse +from django.conf import settings +from django.template.loader import render_to_string + +from ietf.dbtemplate.models import DBTemplate from ietf.meeting.models import Session from ietf.group.utils import can_manage_materials @@ -72,6 +79,35 @@ def sort_sessions(sessions): return meeting_sorted +def create_proceedings_templates(meeting): + '''Create DBTemplates for meeting proceedings''' + # Get meeting attendees from registration system + url = urlparse.urljoin(settings.REGISTRATION_ATTENDEES_BASE_URL,meeting.number) + try: + attendees = json.load(urllib2.urlopen(url)) + except (ValueError, urllib2.HTTPError): + attendees = [] + + if attendees: + attendees = sorted(attendees, key = lambda a: a['LastName']) + content = render_to_string('meeting/proceedings_attendees_table.html', { + 'attendees':attendees}) + DBTemplate.objects.create( + path='/meeting/proceedings/%s/attendees.html' % meeting.number, + title='IETF %s Attendee List' % meeting.number, + type_id='django', + content=content) + + # Make copy of default IETF Overview template + if not meeting.overview: + template = DBTemplate.objects.get(path='/meeting/proceedings/defaults/overview.rst') + template.id = None + template.path = '/meeting/proceedings/%s/overview.rst' % (meeting.number) + template.title = 'IETF %s Proceedings Overview' % (meeting.number) + template.save() + meeting.overview = template + meeting.save() + def finalize(meeting): end_date = meeting.end_date() end_time = datetime.datetime.combine(end_date, datetime.datetime.min.time())+datetime.timedelta(days=1) @@ -81,8 +117,10 @@ def finalize(meeting): if rev_before_end: sp.rev = rev_before_end[-1].newrevisiondocevent.rev else: - sp.rev = '00' + sp.rev = '00' sp.save() + + create_proceedings_templates(meeting) meeting.proceedings_final = True meeting.save() return diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 64a96e8fc..d540e9317 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -25,6 +25,7 @@ from django.db.models import Min, Max from django.conf import settings from django.forms.models import modelform_factory, inlineformset_factory from django.forms import ModelForm +from django.template import TemplateDoesNotExist from django.template.loader import render_to_string from django.utils.functional import curry from django.views.decorators.cache import cache_page @@ -56,6 +57,7 @@ from ietf.meeting.helpers import send_interim_approval_request from ietf.meeting.helpers import send_interim_announcement_request from ietf.meeting.utils import finalize from ietf.secr.proceedings.utils import handle_upload_file +from ietf.secr.proceedings.proc_utils import get_progress_stats from ietf.utils.mail import send_mail_message from ietf.utils.pipe import pipe from ietf.utils.pdf import pdf_pages @@ -2009,30 +2011,65 @@ def finalize_proceedings(request, num=None): return render(request, "meeting/finalize.html", {'meeting':meeting,}) -@role_required('Secretariat') def proceedings_acknowledgements(request, num=None): - + '''Display Acknowledgements for meeting''' meeting = get_meeting(num) - if meeting.number < 95: + if not num.isdigit(): + raise Http404 + if int(meeting.number) < settings.NEW_PROCEEDINGS_START: return HttpResponseRedirect( 'https://www.ietf.org/proceedings/%s/acknowledgement.html' % num ) return render(request, "meeting/proceedings_acknowledgements.html", { 'meeting': meeting, }) -@role_required('Secretariat') +def proceedings_attendees(request, num=None): + '''Display list of meeting attendees''' + meeting = get_meeting(num) + if not num.isdigit(): + raise Http404 + if int(meeting.number) < settings.NEW_PROCEEDINGS_START: + return HttpResponseRedirect( 'https://www.ietf.org/proceedings/%s/attendees.html' % num ) + overview_template = '/meeting/proceedings/%s/attendees.html' % meeting.number + try: + template = render_to_string(overview_template, {}) + except TemplateDoesNotExist: + raise Http404 + return render(request, "meeting/proceedings_attendees.html", { + 'meeting': meeting, + 'template': template, + }) + def proceedings_overview(request, num=None): '''Display Overview for given meeting''' meeting = get_meeting(num) - if meeting.number < 95: + if not num.isdigit(): + raise Http404 + if int(meeting.number) < settings.NEW_PROCEEDINGS_START: return HttpResponseRedirect( 'https://www.ietf.org/proceedings/%s/overview.html' % num ) overview_template = '/meeting/proceedings/%s/overview.rst' % meeting.number - template = render_to_string(overview_template, {}) - + try: + template = render_to_string(overview_template, {}) + except TemplateDoesNotExist: + raise Http404 return render(request, "meeting/proceedings_overview.html", { 'meeting': meeting, 'template': template, }) +@cache_page( 60 * 60 ) +def proceedings_progress_report(request, num=None): + '''Display Progress Report (stats since last meeting)''' + meeting = get_meeting(num) + if not num.isdigit(): + raise Http404 + if int(meeting.number) < settings.NEW_PROCEEDINGS_START: + return HttpResponseRedirect( 'https://www.ietf.org/proceedings/%s/progress-report.html' % num ) + sdate = meeting.previous_meeting().date + edate = meeting.date + context = get_progress_stats(sdate,edate) + context['meeting'] = meeting + return render(request, "meeting/proceedings_progress_report.html", context) + class OldUploadRedirect(RedirectView): def get_redirect_url(self, **kwargs): return reverse_lazy('ietf.meeting.views.session_details',kwargs=self.kwargs) diff --git a/ietf/secr/meetings/tests.py b/ietf/secr/meetings/tests.py index 929ac45dd..cb890f7e5 100644 --- a/ietf/secr/meetings/tests.py +++ b/ietf/secr/meetings/tests.py @@ -85,8 +85,6 @@ class SecrMeetingTestCase(TestCase): response = self.client.post(url, post_data, follow=True) self.assertEqual(response.status_code, 200) self.assertEqual(Meeting.objects.count(),count + 1) - meeting = Meeting.objects.order_by('id').last() - self.assertEqual(meeting.overview.path,'/meeting/proceedings/%s/overview.rst' % meeting.number) def test_edit_meeting(self): "Edit Meeting" diff --git a/ietf/secr/meetings/views.py b/ietf/secr/meetings/views.py index ad47992f0..882fb2410 100644 --- a/ietf/secr/meetings/views.py +++ b/ietf/secr/meetings/views.py @@ -14,7 +14,6 @@ from django.shortcuts import render_to_response, get_object_or_404, redirect from django.template import RequestContext from django.utils.functional import curry -from ietf.dbtemplate.models import DBTemplate from ietf.ietfauth.utils import role_required from ietf.utils.mail import send_mail from ietf.meeting.helpers import get_meeting, make_materials_directories @@ -317,16 +316,7 @@ def add(request): # Create Physical new meeting directory and subdirectories make_materials_directories(meeting) - - # Make copy of IETF Overview template - template = DBTemplate.objects.get(path='/meeting/proceedings/defaults/overview.rst') - template.id = None - template.path = '/meeting/proceedings/%s/overview.rst' % (meeting.number) - template.title = 'IETF %s Proceedings Overview' % (meeting.number) - template.save() - meeting.overview = template - meeting.save() - + messages.success(request, 'The Meeting was created successfully!') return redirect('meetings') else: diff --git a/ietf/secr/proceedings/proc_utils.py b/ietf/secr/proceedings/proc_utils.py index 30e8299e0..932ee0c6c 100644 --- a/ietf/secr/proceedings/proc_utils.py +++ b/ietf/secr/proceedings/proc_utils.py @@ -106,97 +106,67 @@ def mycomp(timeslot): def get_progress_stats(sdate,edate): ''' - This function takes a date range and produces a dictionary of statistics / objects for use - in a progress report. Generally the end date will be the date of the last meeting + This function takes a date range and produces a dictionary of statistics / objects for + use in a progress 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 = {} data['sdate'] = sdate data['edate'] = edate - # Activty Report Section - new_docs = Document.objects.filter(type='draft').filter(docevent__type='new_revision', - docevent__newrevisiondocevent__rev='00', - docevent__time__gte=sdate, - docevent__time__lt=edate) - data['new'] = new_docs.count() - data['updated'] = 0 - data['updated_more'] = 0 - for d in new_docs: - updates = d.docevent_set.filter(type='new_revision',time__gte=sdate,time__lt=edate).count() - if updates > 1: - data['updated'] += 1 - if updates > 2: - data['updated_more'] +=1 - - # calculate total documents updated, not counting new, rev=00 - result = set() events = DocEvent.objects.filter(doc__type='draft',time__gte=sdate,time__lt=edate) - for e in events.filter(type='new_revision').exclude(newrevisiondocevent__rev='00'): - result.add(e.doc) - data['total_updated'] = len(result) + + 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_drafts_count'] = len(new_drafts) + data['new_drafts_updated_count'] = events.filter(doc__in=new_drafts,newrevisiondocevent__rev='01').count() + data['new_drafts_updated_more_count'] = events.filter(doc__in=new_drafts,newrevisiondocevent__rev='02').count() + + update_events = events.filter(type='new_revision').exclude(doc__in=new_drafts) + data['updated_drafts_count'] = len(set([ e.doc_id for e in update_events ])) + + # Calculate Final Four Weeks stats (ffw) + ffwdate = edate - 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__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 - # calculate sent last call - data['last_call'] = events.filter(type='sent_last_call').count() + rfcs = events.filter(type='published_rfc') + data['rfcs'] = rfcs.select_related('doc').select_related('doc__group').select_related('doc__intended_std_level') - # calculate approved - data['approved'] = events.filter(type='iesg_approved').count() + 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()} - # get 4 weeks - ff1_date = edate - datetime.timedelta(days=28) - ff_docs = Document.objects.filter(type='draft').filter(docevent__type='new_revision', - docevent__newrevisiondocevent__rev='00', - docevent__time__gte=ff1_date, - docevent__time__lt=edate) - ff_new_count = ff_docs.count() - ff_new_percent = format(ff_new_count / float(data['new']),'.0%') - - # calculate total documents updated in final four weeks, not counting new, rev=00 - result = set() - events = DocEvent.objects.filter(doc__type='draft',time__gte=ff1_date,time__lt=edate) - for e in events.filter(type='new_revision').exclude(newrevisiondocevent__rev='00'): - result.add(e.doc) - ff_update_count = len(result) - ff_update_percent = format(ff_update_count / float(data['total_updated']),'.0%') - - data['ff_new_count'] = ff_new_count - data['ff_new_percent'] = ff_new_percent - data['ff_update_count'] = ff_update_count - data['ff_update_percent'] = ff_update_percent - - # Progress Report Section - data['docevents'] = DocEvent.objects.filter(doc__type='draft',time__gte=sdate,time__lt=edate) - data['action_events'] = data['docevents'].filter(type='iesg_approved') - data['lc_events'] = data['docevents'].filter(type='sent_last_call') - - data['new_groups'] = Group.objects.filter(type='wg', - groupevent__changestategroupevent__state='active', - groupevent__time__gte=sdate, - groupevent__time__lt=edate) - - data['concluded_groups'] = Group.objects.filter(type='wg', - groupevent__changestategroupevent__state='conclude', - groupevent__time__gte=sdate, - groupevent__time__lt=edate) - - data['new_docs'] = Document.objects.filter(type='draft').filter(docevent__type='new_revision', - docevent__time__gte=sdate, - docevent__time__lt=edate).distinct() - - data['rfcs'] = DocEvent.objects.filter(type='published_rfc', - doc__type='draft', - time__gte=sdate, - time__lt=edate) - - # attach the ftp URL for use in the template - for event in data['rfcs']: - num = get_rfc_num(event.doc) - event.ftp_url = 'ftp://ftp.ietf.org/rfc/rfc%s.txt' % num - - data['counts'] = {'std':data['rfcs'].filter(doc__intended_std_level__in=('ps','ds','std')).count(), - 'bcp':data['rfcs'].filter(doc__intended_std_level='bcp').count(), - 'exp':data['rfcs'].filter(doc__intended_std_level='exp').count(), - 'inf':data['rfcs'].filter(doc__intended_std_level='inf').count()} + data['new_groups'] = Group.objects.filter( + type='wg', + groupevent__changestategroupevent__state='active', + groupevent__time__gte=sdate, + groupevent__time__lt=edate) + + data['concluded_groups'] = Group.objects.filter( + type='wg', + groupevent__changestategroupevent__state='conclude', + groupevent__time__gte=sdate, + groupevent__time__lt=edate) return data diff --git a/ietf/settings.py b/ietf/settings.py index 88717165b..25f9d8696 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -599,7 +599,8 @@ SECR_INTERIM_LISTING_DIR = '/a/www/www6/meeting/interim' SECR_MAX_UPLOAD_SIZE = 40960000 SECR_PROCEEDINGS_DIR = '/a/www/www6s/proceedings/' SECR_PPT2PDF_COMMAND = ['/usr/bin/soffice','--headless','--convert-to','pdf','--outdir'] - +REGISTRATION_ATTENDEES_BASE_URL = 'https://ietf.org/registration/attendees/' +NEW_PROCEEDINGS_START = 95 USE_ETAGS=True PRODUCTION_TIMEZONE = "America/Los_Angeles" diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css index ab81ad8f7..cfa55a5bb 100644 --- a/ietf/static/ietf/css/ietf.css +++ b/ietf/static/ietf/css/ietf.css @@ -544,6 +544,12 @@ ul.errorlist { color: #555; } +/* === Proceedings ===================================================== */ +ul.progress-section { + list-style-type: none; + margin-bottom: 2em; +} + /* === Font-Awesome ========================================================= */ .btn .fa-stack { width: 1em; height: 1em; } diff --git a/ietf/templates/meeting/proceedings.html b/ietf/templates/meeting/proceedings.html index ea6e6eb4b..f97955a0f 100644 --- a/ietf/templates/meeting/proceedings.html +++ b/ietf/templates/meeting/proceedings.html @@ -33,13 +33,15 @@ {% load cache %} {% cache 900 ietf_meeting_proceedings meeting.number cache_version %} + {% if meeting.proceedings_final %}
Last Name | +First Name | +Organization | +
---|---|---|
{{ attendee.LastName }} | +{{ attendee.FirstName }} | +{{ attendee.Company }} | +
{{ counts.std }} Standards Track; {{ counts.bcp }} BCP; {{ counts.exp }} Experimental; {{ counts.inf }} Informational
+ +{{ rfc.doc.canonical_name|upper }} | +{{ rfc.doc.intended_std_level.name|abbr_status }} | +({{ rfc.doc.group.acronym }}) | +{{ rfc.time|date:"F Y" }} | +{{ rfc.doc.title }} | +