Adds a meetings tab to the group information page. Links to minutes, agendas, and materials for each session at each meeting. Improves the UI for the session materials page. Commit ready for merge.

- Legacy-Id: 10719
This commit is contained in:
Robert Sparks 2016-01-20 22:59:14 +00:00
commit 7d43d3ada4
14 changed files with 337 additions and 78 deletions

View file

@ -36,6 +36,7 @@ import os
import itertools
import re
from tempfile import mkstemp
import datetime
from collections import OrderedDict
from django.shortcuts import render, redirect
@ -333,6 +334,8 @@ def construct_group_menu_context(request, group, selected, group_type, others):
entries.append(("About", urlreverse("group_about", kwargs=kwargs)))
if group.features.has_materials and get_group_materials(group).exists():
entries.append(("Materials", urlreverse("ietf.group.info.materials", kwargs=kwargs)))
if group.type_id in ('rg','wg'):
entries.append(("Meetings", urlreverse("ietf.group.info.meetings", kwargs=kwargs)))
entries.append(("Email expansions", urlreverse("ietf.group.info.email", kwargs=kwargs)))
entries.append(("History", urlreverse("ietf.group.info.history", kwargs=kwargs)))
if group.features.has_documents:
@ -724,3 +727,47 @@ def email_aliases(request, acronym=None, group_type=None):
return render(request,'group/email_aliases.html',{'aliases':aliases,'ietf_domain':settings.IETF_DOMAIN,'group':group})
def meetings(request, acronym=None, group_type=None):
group = get_group_or_404(acronym,group_type) if acronym else None
four_years_ago = datetime.datetime.now()-datetime.timedelta(days=4*365)
sessions = group.session_set.filter(status__in=['sched','schedw','appr','canceled'],meeting__date__gt=four_years_ago)
def sort_key(session):
if session.meeting.type.slug=='ietf':
official_sessions = session.timeslotassignments.filter(schedule=session.meeting.agenda)
if official_sessions:
return official_sessions.first().timeslot.time
elif session.meeting.date:
return datetime.datetime.combine(session.meeting.date,datetime.datetime.min.time())
else:
return session.requested
else:
# TODO: use timeslots for interims once they have them
return datetime.datetime.combine(session.meeting.date,datetime.datetime.min.time())
for s in sessions:
s.time=sort_key(s)
sessions = sorted(sessions,key=lambda s:s.time,reverse=True)
today = datetime.date.today()
future = []
in_progress = []
past = []
for s in sessions:
if s.meeting.date > today:
future.append(s)
elif s.meeting.end_date() >= today:
in_progress.append(s)
else:
past.append(s)
return render(request,'group/meetings.html',
construct_group_menu_context(request, group, "meetings", group_type, {
'group':group,
'future':future,
'in_progress':in_progress,
'past':past,
}))

View file

@ -22,6 +22,8 @@ from ietf.utils.test_utils import TestCase, unicontent
from ietf.utils.mail import outbox, empty_outbox
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import login_testing_unauthorized
from ietf.group.factories import GroupFactory
from ietf.meeting.factories import SessionFactory
class GroupPagesTests(TestCase):
def setUp(self):
@ -988,3 +990,29 @@ class AjaxTests(TestCase):
mars_wg = Group.objects.get(acronym="mars")
self.assertEqual(mars_wg_data["name"], mars_wg.name)
class MeetingInfoTests(TestCase):
def setUp(self):
self.group = GroupFactory.create(type_id='wg')
today = datetime.date.today()
SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=90))
self.inprog = SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=1))
SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today+datetime.timedelta(days=90))
SessionFactory.create(meeting__type_id='interim',group=self.group,meeting__date=today+datetime.timedelta(days=45))
def test_meeting_info(self):
url = urlreverse('ietf.group.info.meetings',kwargs={'acronym':self.group.acronym})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
q = PyQuery(response.content)
self.assertTrue(q('#inprogressmeets'))
self.assertTrue(q('#futuremeets'))
self.assertTrue(q('#pastmeets'))
self.group.session_set.filter(id=self.inprog.id).delete()
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
q = PyQuery(response.content)
self.assertFalse(q('#inprogressmeets'))

View file

@ -10,7 +10,6 @@ urlpatterns = patterns('',
(r'^chartering/create/(?P<group_type>(wg|rg))/$', 'ietf.group.edit.edit', {'action': "charter"}, "group_create"),
(r'^concluded/$', 'ietf.group.info.concluded_groups'),
(r'^email-aliases/$', 'ietf.group.info.email_aliases'),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/$', 'ietf.group.info.group_home', None, "group_home"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/', include('ietf.group.urls_info_details')),
)

View file

@ -10,6 +10,7 @@ urlpatterns = patterns('',
(r'^history/$','ietf.group.info.history'),
(r'^email/$', 'ietf.group.info.email'),
(r'^deps/(?P<output_type>[\w-]+)/$', 'ietf.group.info.dependencies'),
(r'^meetings/$', 'ietf.group.info.meetings'),
(r'^init-charter/', 'ietf.group.edit.submit_initial_charter'),
(r'^edit/$', 'ietf.group.edit.edit', {'action': "edit"}, "group_edit"),
(r'^conclude/$', 'ietf.group.edit.conclude'),

99
ietf/meeting/factories.py Normal file
View file

@ -0,0 +1,99 @@
import factory
import random
import datetime
from django.db.models import Max
from ietf.meeting.models import Meeting, Session, Schedule, TimeSlot
from ietf.group.factories import GroupFactory
from ietf.person.factories import PersonFactory
class MeetingFactory(factory.DjangoModelFactory):
class Meta:
model = Meeting
type_id = factory.Iterator(['ietf','interim'])
date = datetime.date(2010,1,1)+datetime.timedelta(days=random.randint(0,3652))
city = factory.Faker('city')
country = factory.Faker('country_code')
time_zone = factory.Faker('timezone')
idsubmit_cutoff_day_offset_00 = 13
idsubmit_cutoff_day_offset_01 = 13
idsubmit_cutoff_time_utc = datetime.timedelta(0, 86399)
idsubmit_cutoff_warning_days = 21
venue_name = factory.Faker('sentence')
venue_addr = factory.Faker('address')
break_area = factory.Faker('sentence')
reg_area = factory.Faker('sentence')
@factory.lazy_attribute_sequence
def number(self,n):
if self.type_id == 'ietf':
if Meeting.objects.filter(type='ietf').exists():
return '%02d'%(int(Meeting.objects.filter(type='ietf').aggregate(Max('number'))['number__max'])+1)
else:
return '%02d'%(n+80)
else:
return 'interim-%d-%s-%d'%(self.date.year,GroupFactory().acronym,n)
@factory.post_generation
def populate_agenda(self, create, extracted, **kwargs):
'''
Create a default agenda, unless the factory is called
with populate_agenda=False
'''
if extracted is None:
extracted = True
if create and extracted:
for x in range(3):
TimeSlotFactory(meeting=self)
self.agenda = ScheduleFactory(meeting=self)
self.save()
class SessionFactory(factory.DjangoModelFactory):
class Meta:
model = Session
meeting = factory.SubFactory(MeetingFactory)
type_id='session'
group = factory.SubFactory(GroupFactory)
requested_by = factory.SubFactory(PersonFactory)
status_id='sched'
@factory.post_generation
def add_to_schedule(self, create, extracted, **kwargs):
'''
Put this session in a timeslot unless the factory is called
with add_to_schedule=False
'''
if extracted is None:
extracted = True
if create and extracted:
ts = self.meeting.timeslot_set.all()
self.timeslotassignments.create(timeslot=ts[random.randrange(len(ts))],schedule=self.meeting.agenda)
class ScheduleFactory(factory.DjangoModelFactory):
class Meta:
model = Schedule
meeting = factory.SubFactory(MeetingFactory)
name = factory.Faker('text',max_nb_chars=16)
owner = factory.SubFactory(PersonFactory)
class TimeSlotFactory(factory.DjangoModelFactory):
class Meta:
model = TimeSlot
meeting = factory.SubFactory(MeetingFactory)
type_id = 'session'
@factory.lazy_attribute
def time(self):
return datetime.datetime.combine(self.meeting.date,datetime.time(11,0))
@factory.lazy_attribute
def duration(self):
return datetime.timedelta(minutes=30+random.randrange(9)*15)

View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def forward(apps, schema_editor):
Session = apps.get_model('meeting','Session')
assert(Session.objects.filter(meeting__number__in=['88','89'],group__type__in=['ag','iab','rg','wg'],status_id='sched').count() == 0)
Session.objects.filter(meeting__number__in=['88','89'],group__type__in=['ag','iab','rg','wg'],status_id='schedw').update(status_id='sched')
def reverse(apps, schema_editor):
Session = apps.get_model('meeting','Session')
Session.objects.filter(meeting__number__in=['88','89'],group__type__in=['ag','iab','rg','wg'],status_id='sched').update(status_id='schedw')
class Migration(migrations.Migration):
dependencies = [
('meeting', '0015_auto_20151102_1845'),
]
operations = [
migrations.RunPython(forward,reverse),
]

View file

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
from collections import Counter
affected = ['interim-2010-drinks-1','interim-2010-core-1','interim-2010-behave-1','interim-2010-siprec-1','interim-2010-cuss-1','interim-2010-iri-1','interim-2010-pcp-1','interim-2010-geopriv-1','interim-2010-soc-1','interim-2010-precis-1','interim-2010-mptcp-1','interim-2010-roll-1','interim-2011-sipclf-1','interim-2011-ipsecme-1','interim-2011-siprec-1','interim-2011-alto-1','interim-2011-xmpp-1','interim-2011-precis-1','interim-2011-nfsv4-1','interim-2011-pcp-1','interim-2011-clue-1','interim-2011-oauth-1','interim-2011-rtcweb-1','interim-2011-drinks-1','interim-2011-atoca-1','interim-2011-cuss-1','interim-2011-softwire-1','interim-2011-ppsp-1','interim-2011-homenet-1','interim-2011-mptcp-1','interim-2012-rtcweb-1','interim-2012-drinks-1','interim-2012-sidr-1','interim-2012-clue-1','interim-2012-krb-wg-1','interim-2012-behave-1','interim-2012-bfcpbis-1','interim-2012-mboned-1']
def forward(apps, schema_editor):
Session = apps.get_model('meeting','Session')
assert( Counter(Session.objects.filter(meeting__number__in=affected).values_list('status',flat=True)) == Counter({u'appr':38}) )
Session.objects.filter(meeting__number__in=affected).update(status_id='sched')
def reverse(apps, schema_editor):
Session = apps.get_model('meeting','Session')
Session.objects.filter(meeting__number__in=affected).update(status_id='appr')
class Migration(migrations.Migration):
dependencies = [
('meeting', '0016_schedule_ietf88_and_89'),
]
operations = [
migrations.RunPython(forward,reverse),
]

View file

@ -96,7 +96,12 @@ class Meeting(models.Model):
return self.date + datetime.timedelta(days=offset)
def end_date(self):
return self.get_meeting_date(5)
if self.type.slug == 'ietf':
return self.get_meeting_date(5)
else:
# TODO: Once interims have timeslots assigned,
# look for the last ending timeslot instead
return self.date
def get_00_cutoff(self):
start_date = datetime.datetime(year=self.date.year, month=self.date.month, day=self.date.day, tzinfo=pytz.utc)

View file

@ -61,10 +61,6 @@ urlpatterns = patterns('',
(r'^(?P<num>\d+)/session/(?P<sessionid>\d+)/constraints.json', ajax.session_constraints),
(r'^(?P<num>\d+)/session/(?P<acronym>[A-Za-z0-9_\-\+]+)/$', views.session_details),
(r'^(?P<num>\d+)/session/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<seq>\d+)/$', views.session_details),
(r'^(?P<num>\d+)/session/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<week_day>[a-zA-Z]+)/$', views.session_details),
(r'^(?P<num>\d+)/session/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<date>\d{4}-\d{2}-\d{2}(-\d{4})?)/$', views.session_details),
(r'^(?P<num>\d+)/session/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<date>\d{4}-\d{2}-\d{2}(-\d{4})?)/(?P<seq>\d+)/$', views.session_details),
(r'^(?P<num>\d+)/constraint/(?P<constraintid>\d+).json', ajax.constraint_json),
(r'^(?P<num>\d+).json$', ajax.meeting_json),

View file

@ -825,30 +825,13 @@ def meeting_requests(request, num=None):
{"meeting": meeting, "sessions":sessions,
"groups_not_meeting": groups_not_meeting})
def session_details(request, num, acronym, date=None, week_day=None, seq=None):
def session_details(request, num, acronym ):
meeting = get_meeting(num)
sessions = Session.objects.filter(meeting=meeting,group__acronym=acronym,type__in=['session','plenary','other'])
if not sessions:
sessions = Session.objects.filter(meeting=meeting,short=acronym)
if date:
if len(date)==15:
start = datetime.datetime.strptime(date,"%Y-%m-%d-%H%M")
sessions = sessions.filter(timeslotassignments__schedule=meeting.agenda,timeslotassignments__timeslot__time=start)
else:
start = datetime.datetime.strptime(date,"%Y-%m-%d").date()
end = start+datetime.timedelta(days=1)
sessions = sessions.filter(timeslotassignments__schedule=meeting.agenda,timeslotassignments__timeslot__time__range=(start,end))
if week_day:
try:
dow = ['sun','mon','tue','wed','thu','fri','sat'].index(week_day.lower()[:3]) + 1
except ValueError:
raise Http404
sessions = sessions.filter(timeslotassignments__schedule=meeting.agenda,timeslotassignments__timeslot__time__week_day=dow)
def sort_key(session):
official_sessions = session.timeslotassignments.filter(schedule=session.meeting.agenda)
if official_sessions:
@ -858,34 +841,19 @@ def session_details(request, num, acronym, date=None, week_day=None, seq=None):
sessions = sorted(sessions,key=sort_key)
if seq:
iseq = int(seq) - 1
if not iseq in range(0,len(sessions)):
raise Http404
else:
sessions= [sessions[iseq]]
if not sessions:
raise Http404
if len(sessions)==1:
session = sessions[0]
scheduled_time = "Not yet scheduled"
for session in sessions:
ss = session.timeslotassignments.filter(schedule=meeting.agenda).order_by('timeslot__time')
if ss:
scheduled_time = ','.join(x.timeslot.time.strftime("%A %b-%d %H%M") for x in ss)
session.time = ', '.join(x.timeslot.time.strftime("%A %b-%d %H%M") for x in ss) if ss else 'Not yet scheduled'
# TODO FIXME Deleted materials shouldn't be in the sessionpresentation_set
filtered_sessionpresentation_set = [p for p in session.sessionpresentation_set.all() if p.document.get_state_slug(p.document.type_id)!='deleted']
return render(request, "meeting/session_details.html",
{ 'session':sessions[0] ,
'meeting' :meeting ,
'acronym' :acronym,
'time': scheduled_time,
'filtered_sessionpresentation_set': filtered_sessionpresentation_set
})
else:
return render(request, "meeting/session_list.html",
{ 'sessions':sessions ,
'meeting' :meeting ,
'acronym' :acronym,
})
session.filtered_sessionpresentation_set = [p for p in session.sessionpresentation_set.all() if p.document.get_state_slug(p.document.type_id)!='deleted']
return render(request, "meeting/session_details.html",
{ 'sessions':sessions ,
'meeting' :meeting ,
'acronym' :acronym,
})

View file

@ -0,0 +1,27 @@
<table class="table table-condensed table-striped">
<thead>
<tr>
<th class="col-md-2"></th>
<th class="col-md-2"></th>
<th class="col-md-1"></th>
<th class="col-md-1"></th>
<th class="col-md-6"></th>
</tr>
</thead>
<tbody>
{% for s in sessions %}
<tr>
<td>{% ifchanged s.meeting %}{% if s.meeting.type.slug == 'ietf' %}IETF{% endif %}{{s.meeting.number}}{% endifchanged %}</td>
<td>
{% if s.status.slug == "sched" %}
{% if s.meeting.type.slug == 'ietf' %}{{s.time|date:"D M d, Y Hi"}}{% else %}{{s.time|date:"D M d, Y"}}{% endif %}
{% else %}
{{s.status}}
{% endif %}
</td>
<td>{% if s.minutes %}<a href="{{ s.minutes.get_absolute_url }}">Minutes</a>{% endif %}</td>
<td>{% if s.agenda %}<a href="{{ s.agenda.get_absolute_url }}">Agenda</a>{% endif %}</td>
<td>{% if s.meeting.type.slug == 'ietf' %}<a href="{% url 'ietf.meeting.views.session_details' num=s.meeting.number acronym=s.group.acronym %}">Materials</a>{% endif %}</td>
{% endfor %}
</tbody>
</table>

View file

@ -0,0 +1,50 @@
{% extends "group/group_base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block title %}Meetings{% if group %} for {{group.acronym}}{% endif %}{% endblock %}
{% block group_content %}
{% origin %}
{% if in_progress %}
<div class="panel panel-default" id="inprogressmeets">
<div class="panel-heading">
Meetings in progress
</div>
<div class="panel-body">
{% with in_progress as sessions %}
{% include "group/meetings-row.html" %}
{% endwith %}
</div>
</div>
{% endif %}
{% if future %}
<div class="panel panel-default" id="futuremeets">
<div class="panel-heading">
Future Meetings
</div>
<div class="panel-body">
{% with future as sessions %}
{% include "group/meetings-row.html" %}
{% endwith %}
</div>
</div>
{% endif %}
{% if past %}
<div class="panel panel-default" id="pastmeets">
<div class="panel-heading">
Past Meetings
</div>
<div class="panel-body">
{% with past as sessions %}
{% include "group/meetings-row.html" %}
{% endwith %}
</div>
</div>
{% endif %}
<div>This page shows meetings within the last four years. For earlier meetings, please see the proceedings.</div>
{% endblock %}

View file

@ -6,20 +6,25 @@
{% block content %}
{% origin %}
<h1>{{ meeting }} : {{ acronym }} : {{ time }} </h1>
<h1>{{ meeting }} : {{ acronym }}</h1>
{% if session.name %}
<h2>{{ session.name }}</h2>
{% endif %}
{% for session in sessions %}
<h2>{{ session.time }}{% if session.name %} : {{ session.name }}{% endif %}</h2>
{% if filtered_sessionpresentation_set %}
<p>Materials:</p>
{% if session.filtered_sessionpresentation_set %}
<p>Materials:</p>
<ul>
{% for pres in filtered_sessionpresentation_set %}
<li><a href="{% url 'doc_view' name=pres.document.name rev=pres.rev%}">{{ pres.document.name }}-{{ pres.rev }}</a></li>
{% endfor %}
</ul>
{% endif %}
<table class="table table-condensed table-striped">
{% for pres in session.filtered_sessionpresentation_set %}
<tr>
<td>
<a href="{% url 'doc_view' name=pres.document.name rev=pres.rev%}">{{pres.document.title}} ({{ pres.document.name }}-{{ pres.rev }})
</a>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endfor %}
{% endblock %}

View file

@ -1,16 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block title %}{{ meeting }} : {{ acronym }}{% endblock %}
{% block content %}
{% origin %}
<h1>{{ meeting }} : {{ acronym }}</h1>
<ul>
{% for session in sessions %}
<li> <a href="{{ forloop.counter }}/">{{session}}</a></li>
{% endfor %}
</ul>
{% endblock %}