Merged in ^/branch/proceedings/6.33.1.dev0@12141 from rcross@amsl.com:

Added meeting proceedings introduction pages: Progress Report and Attendees.  Added a data migration to provide proceedings introduction information for IETF 95 and 96.
 - Legacy-Id: 12150
This commit is contained in:
Henrik Levkowetz 2016-10-14 15:48:10 +00:00
commit 1a92efb77e
16 changed files with 298 additions and 132 deletions

View file

@ -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

View file

@ -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),
]

View file

@ -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)

View file

@ -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 = [

View file

@ -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)
@ -83,6 +119,8 @@ def finalize(meeting):
else:
sp.rev = '00'
sp.save()
create_proceedings_templates(meeting)
meeting.proceedings_final = True
meeting.save()
return

View file

@ -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
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)

View file

@ -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"

View file

@ -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
@ -318,15 +317,6 @@ 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:

View file

@ -106,98 +106,68 @@ 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)
# calculate sent last call
data['last_call'] = events.filter(type='sent_last_call').count()
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()
# calculate approved
data['approved'] = events.filter(type='iesg_approved').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 ]))
# 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 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
# 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['ffw_new_count'] = ffw_new_count
data['ffw_new_percent'] = ffw_new_percent
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
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
# 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['ffw_update_count'] = ffw_update_count
data['ffw_update_percent'] = ffw_update_percent
data['new_groups'] = Group.objects.filter(type='wg',
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=sdate,
groupevent__time__lt=edate)
data['concluded_groups'] = Group.objects.filter(type='wg',
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()}
return data
def get_next_sequence(group,meeting,type):

View file

@ -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"

View file

@ -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; }

View file

@ -33,13 +33,15 @@
{% load cache %}
{% cache 900 ietf_meeting_proceedings meeting.number cache_version %}
{% if meeting.proceedings_final %}
<h2 class="anchor-target" id="introduction">Introduction</h2>
<div>
<a href="{% url 'ietf.meeting.views.proceedings_acknowledgements' num=meeting.number %}">Acknowledgements</a><br>
<a href="{% url 'ietf.meeting.views.proceedings_overview' num=meeting.number %}">IETF Overview</a><br>
<a href="#">Progress Report</a><br>
<a href="#">Attendees</a><br>
<a href="{% url 'ietf.meeting.views.proceedings_progress_report' num=meeting.number %}">Progress Report</a><br>
<a href="{% url 'ietf.meeting.views.proceedings_attendees' num=meeting.number %}">Attendees</a><br>
</div>
{% endif %}
{% with "True" as show_agenda %}
<!-- Plenaries -->

View file

@ -0,0 +1,14 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin markup_tags %}
{% block title %}IETF {{ meeting.number }} Proceedings {% endblock %}
{% block content %}
{% origin %}
<h1><a href="{% url 'ietf.meeting.views.proceedings' num=meeting.number %}">IETF {{ meeting.number }} Proceedings</a></h1>
<h2>Attendee List of IETF {{ meeting.number }} Meeting</h2>
{{ template|safe }}
{% endblock %}

View file

@ -0,0 +1,18 @@
<table id="id_attendees" class="table">
<thead>
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Organization</th>
</tr>
</thead>
<tbody>
{% for attendee in attendees %}
<tr>
<td>{{ attendee.LastName }}</td>
<td>{{ attendee.FirstName }}</td>
<td>{{ attendee.Company }}</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -0,0 +1,53 @@
{% extends "base.html" %}
{% load ams_filters %}
{% block title %}IETF {{ meeting.number }} Proceedings - Progress Report{% endblock %}
{% block content %}
<h1><a href="{% url 'ietf.meeting.views.proceedings' num=meeting.number %}">IETF {{ meeting.number }} Proceedings</a></h1>
<h2>IETF Progress Report</h2>
<h4>{{ sdate|date:"d-F-y" }} to {{ edate|date:"d-F-y" }}</h4>
<ul class="progress-section">
<li>{{ actions_count }} IESG Protocol and Document Actions this period</li>
<li>{{ last_calls_count }} IESG Last Calls issued to the IETF this period</li>
<li></li>
<li>{{ new_drafts_count|stringformat:"3s" }} New I-Ds ({{ new_drafts_updated_count }} of which were updated, some ({{ new_drafts_updated_more_count }}) more than once)</li>
<li>{{ updated_drafts_count|stringformat:"3s" }} I-Ds were updated (Some more than once)</li>
<li></li>
<li><h4>In the final 4 weeks before meeting</h4></li>
<li>{{ ffw_new_count|stringformat:"3s" }} New I-Ds were received - {{ ffw_new_percent }} of total newbies since last meeting</li>
<li>{{ ffw_update_count|stringformat:"3s" }} I-Ds were updated - {{ ffw_update_percent }} of total updated since last meeting</li>
</ul>
<h4>{{ new_groups.count }} New Working Group(s) formed this period</h4>
<ul class="progress-section">
{% for group in new_groups %}
<li>{{ group.name }} ({{ group.acronym }})</li>
{% endfor %}
</ul>
<h4>{{ concluded_groups.count }} Working Group(s) concluded this period</h4>
<ul class="progress-section">
{% for group in concluded_groups %}
<li>{{ group.name }} ({{ group.acronym }})</li>
{% endfor %}
</ul>
<h4>{{ rfcs.count }} RFCs published this period</h4>
<p>{{ counts.std }} Standards Track; {{ counts.bcp }} BCP; {{ counts.exp }} Experimental; {{ counts.inf }} Informational</p>
<table class="table">
{% for rfc in rfcs %}
<tr>
<td><a href="{{ rfc.doc.get_absolute_url }}">{{ rfc.doc.canonical_name|upper }}</a></td>
<td>{{ rfc.doc.intended_std_level.name|abbr_status }}</td>
<td>({{ rfc.doc.group.acronym }})</td>
<td>{{ rfc.time|date:"F Y" }}</td>
<td>{{ rfc.doc.title }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -18,6 +18,7 @@ html5lib>=0.90,<0.99999999 # ietf.utils.html needs a rewrite for html5lib 1.x --
jsonfield>=1.0.3 # for SubmissionCheck. This is https://github.com/bradjasper/django-jsonfield/.
#lxml>=3.4.0 # from PyQuery;
mimeparse>=0.1.3 # from TastyPie
mock>=2.0.0
MySQL-python>=1.2.5
pathlib>=1.0
Pillow>=3.0