Skip to content

Commit 7d43d3a

Browse files
committed
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
2 parents 04cc29d + 4261c4f commit 7d43d3a

14 files changed

Lines changed: 337 additions & 78 deletions

ietf/group/info.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import itertools
3737
import re
3838
from tempfile import mkstemp
39+
import datetime
3940
from collections import OrderedDict
4041

4142
from django.shortcuts import render, redirect
@@ -333,6 +334,8 @@ def construct_group_menu_context(request, group, selected, group_type, others):
333334
entries.append(("About", urlreverse("group_about", kwargs=kwargs)))
334335
if group.features.has_materials and get_group_materials(group).exists():
335336
entries.append(("Materials", urlreverse("ietf.group.info.materials", kwargs=kwargs)))
337+
if group.type_id in ('rg','wg'):
338+
entries.append(("Meetings", urlreverse("ietf.group.info.meetings", kwargs=kwargs)))
336339
entries.append(("Email expansions", urlreverse("ietf.group.info.email", kwargs=kwargs)))
337340
entries.append(("History", urlreverse("ietf.group.info.history", kwargs=kwargs)))
338341
if group.features.has_documents:
@@ -724,3 +727,47 @@ def email_aliases(request, acronym=None, group_type=None):
724727

725728
return render(request,'group/email_aliases.html',{'aliases':aliases,'ietf_domain':settings.IETF_DOMAIN,'group':group})
726729

730+
def meetings(request, acronym=None, group_type=None):
731+
group = get_group_or_404(acronym,group_type) if acronym else None
732+
733+
four_years_ago = datetime.datetime.now()-datetime.timedelta(days=4*365)
734+
735+
sessions = group.session_set.filter(status__in=['sched','schedw','appr','canceled'],meeting__date__gt=four_years_ago)
736+
737+
def sort_key(session):
738+
if session.meeting.type.slug=='ietf':
739+
official_sessions = session.timeslotassignments.filter(schedule=session.meeting.agenda)
740+
if official_sessions:
741+
return official_sessions.first().timeslot.time
742+
elif session.meeting.date:
743+
return datetime.datetime.combine(session.meeting.date,datetime.datetime.min.time())
744+
else:
745+
return session.requested
746+
else:
747+
# TODO: use timeslots for interims once they have them
748+
return datetime.datetime.combine(session.meeting.date,datetime.datetime.min.time())
749+
750+
for s in sessions:
751+
s.time=sort_key(s)
752+
753+
sessions = sorted(sessions,key=lambda s:s.time,reverse=True)
754+
755+
today = datetime.date.today()
756+
future = []
757+
in_progress = []
758+
past = []
759+
for s in sessions:
760+
if s.meeting.date > today:
761+
future.append(s)
762+
elif s.meeting.end_date() >= today:
763+
in_progress.append(s)
764+
else:
765+
past.append(s)
766+
767+
return render(request,'group/meetings.html',
768+
construct_group_menu_context(request, group, "meetings", group_type, {
769+
'group':group,
770+
'future':future,
771+
'in_progress':in_progress,
772+
'past':past,
773+
}))

ietf/group/tests_info.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from ietf.utils.mail import outbox, empty_outbox
2323
from ietf.utils.test_data import make_test_data
2424
from ietf.utils.test_utils import login_testing_unauthorized
25+
from ietf.group.factories import GroupFactory
26+
from ietf.meeting.factories import SessionFactory
2527

2628
class GroupPagesTests(TestCase):
2729
def setUp(self):
@@ -988,3 +990,29 @@ def test_group_menu_data(self):
988990
mars_wg = Group.objects.get(acronym="mars")
989991
self.assertEqual(mars_wg_data["name"], mars_wg.name)
990992

993+
class MeetingInfoTests(TestCase):
994+
995+
def setUp(self):
996+
self.group = GroupFactory.create(type_id='wg')
997+
today = datetime.date.today()
998+
SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=90))
999+
self.inprog = SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=1))
1000+
SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today+datetime.timedelta(days=90))
1001+
SessionFactory.create(meeting__type_id='interim',group=self.group,meeting__date=today+datetime.timedelta(days=45))
1002+
1003+
1004+
def test_meeting_info(self):
1005+
url = urlreverse('ietf.group.info.meetings',kwargs={'acronym':self.group.acronym})
1006+
response = self.client.get(url)
1007+
self.assertEqual(response.status_code, 200)
1008+
q = PyQuery(response.content)
1009+
self.assertTrue(q('#inprogressmeets'))
1010+
self.assertTrue(q('#futuremeets'))
1011+
self.assertTrue(q('#pastmeets'))
1012+
1013+
self.group.session_set.filter(id=self.inprog.id).delete()
1014+
response = self.client.get(url)
1015+
self.assertEqual(response.status_code, 200)
1016+
q = PyQuery(response.content)
1017+
self.assertFalse(q('#inprogressmeets'))
1018+

ietf/group/urls.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
(r'^chartering/create/(?P<group_type>(wg|rg))/$', 'ietf.group.edit.edit', {'action': "charter"}, "group_create"),
1111
(r'^concluded/$', 'ietf.group.info.concluded_groups'),
1212
(r'^email-aliases/$', 'ietf.group.info.email_aliases'),
13-
1413
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/$', 'ietf.group.info.group_home', None, "group_home"),
1514
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/', include('ietf.group.urls_info_details')),
1615
)

ietf/group/urls_info_details.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
(r'^history/$','ietf.group.info.history'),
1111
(r'^email/$', 'ietf.group.info.email'),
1212
(r'^deps/(?P<output_type>[\w-]+)/$', 'ietf.group.info.dependencies'),
13+
(r'^meetings/$', 'ietf.group.info.meetings'),
1314
(r'^init-charter/', 'ietf.group.edit.submit_initial_charter'),
1415
(r'^edit/$', 'ietf.group.edit.edit', {'action': "edit"}, "group_edit"),
1516
(r'^conclude/$', 'ietf.group.edit.conclude'),

ietf/meeting/factories.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import factory
2+
import random
3+
import datetime
4+
5+
from django.db.models import Max
6+
7+
from ietf.meeting.models import Meeting, Session, Schedule, TimeSlot
8+
from ietf.group.factories import GroupFactory
9+
from ietf.person.factories import PersonFactory
10+
11+
class MeetingFactory(factory.DjangoModelFactory):
12+
class Meta:
13+
model = Meeting
14+
15+
type_id = factory.Iterator(['ietf','interim'])
16+
date = datetime.date(2010,1,1)+datetime.timedelta(days=random.randint(0,3652))
17+
city = factory.Faker('city')
18+
country = factory.Faker('country_code')
19+
time_zone = factory.Faker('timezone')
20+
idsubmit_cutoff_day_offset_00 = 13
21+
idsubmit_cutoff_day_offset_01 = 13
22+
idsubmit_cutoff_time_utc = datetime.timedelta(0, 86399)
23+
idsubmit_cutoff_warning_days = 21
24+
venue_name = factory.Faker('sentence')
25+
venue_addr = factory.Faker('address')
26+
break_area = factory.Faker('sentence')
27+
reg_area = factory.Faker('sentence')
28+
29+
@factory.lazy_attribute_sequence
30+
def number(self,n):
31+
if self.type_id == 'ietf':
32+
if Meeting.objects.filter(type='ietf').exists():
33+
return '%02d'%(int(Meeting.objects.filter(type='ietf').aggregate(Max('number'))['number__max'])+1)
34+
else:
35+
return '%02d'%(n+80)
36+
else:
37+
return 'interim-%d-%s-%d'%(self.date.year,GroupFactory().acronym,n)
38+
39+
@factory.post_generation
40+
def populate_agenda(self, create, extracted, **kwargs):
41+
'''
42+
Create a default agenda, unless the factory is called
43+
with populate_agenda=False
44+
'''
45+
if extracted is None:
46+
extracted = True
47+
if create and extracted:
48+
for x in range(3):
49+
TimeSlotFactory(meeting=self)
50+
self.agenda = ScheduleFactory(meeting=self)
51+
self.save()
52+
53+
54+
class SessionFactory(factory.DjangoModelFactory):
55+
class Meta:
56+
model = Session
57+
58+
meeting = factory.SubFactory(MeetingFactory)
59+
type_id='session'
60+
group = factory.SubFactory(GroupFactory)
61+
requested_by = factory.SubFactory(PersonFactory)
62+
status_id='sched'
63+
64+
@factory.post_generation
65+
def add_to_schedule(self, create, extracted, **kwargs):
66+
'''
67+
Put this session in a timeslot unless the factory is called
68+
with add_to_schedule=False
69+
'''
70+
if extracted is None:
71+
extracted = True
72+
if create and extracted:
73+
ts = self.meeting.timeslot_set.all()
74+
self.timeslotassignments.create(timeslot=ts[random.randrange(len(ts))],schedule=self.meeting.agenda)
75+
76+
class ScheduleFactory(factory.DjangoModelFactory):
77+
class Meta:
78+
model = Schedule
79+
80+
meeting = factory.SubFactory(MeetingFactory)
81+
name = factory.Faker('text',max_nb_chars=16)
82+
owner = factory.SubFactory(PersonFactory)
83+
84+
class TimeSlotFactory(factory.DjangoModelFactory):
85+
class Meta:
86+
model = TimeSlot
87+
88+
meeting = factory.SubFactory(MeetingFactory)
89+
type_id = 'session'
90+
91+
@factory.lazy_attribute
92+
def time(self):
93+
return datetime.datetime.combine(self.meeting.date,datetime.time(11,0))
94+
95+
@factory.lazy_attribute
96+
def duration(self):
97+
return datetime.timedelta(minutes=30+random.randrange(9)*15)
98+
99+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations
5+
6+
def forward(apps, schema_editor):
7+
Session = apps.get_model('meeting','Session')
8+
assert(Session.objects.filter(meeting__number__in=['88','89'],group__type__in=['ag','iab','rg','wg'],status_id='sched').count() == 0)
9+
Session.objects.filter(meeting__number__in=['88','89'],group__type__in=['ag','iab','rg','wg'],status_id='schedw').update(status_id='sched')
10+
11+
def reverse(apps, schema_editor):
12+
Session = apps.get_model('meeting','Session')
13+
Session.objects.filter(meeting__number__in=['88','89'],group__type__in=['ag','iab','rg','wg'],status_id='sched').update(status_id='schedw')
14+
15+
class Migration(migrations.Migration):
16+
17+
dependencies = [
18+
('meeting', '0015_auto_20151102_1845'),
19+
]
20+
21+
operations = [
22+
migrations.RunPython(forward,reverse),
23+
]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations
5+
from collections import Counter
6+
7+
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']
8+
9+
def forward(apps, schema_editor):
10+
Session = apps.get_model('meeting','Session')
11+
assert( Counter(Session.objects.filter(meeting__number__in=affected).values_list('status',flat=True)) == Counter({u'appr':38}) )
12+
Session.objects.filter(meeting__number__in=affected).update(status_id='sched')
13+
14+
def reverse(apps, schema_editor):
15+
Session = apps.get_model('meeting','Session')
16+
Session.objects.filter(meeting__number__in=affected).update(status_id='appr')
17+
18+
19+
class Migration(migrations.Migration):
20+
21+
dependencies = [
22+
('meeting', '0016_schedule_ietf88_and_89'),
23+
]
24+
25+
operations = [
26+
migrations.RunPython(forward,reverse),
27+
]

ietf/meeting/models.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,12 @@ def get_meeting_date (self,offset):
9696
return self.date + datetime.timedelta(days=offset)
9797

9898
def end_date(self):
99-
return self.get_meeting_date(5)
99+
if self.type.slug == 'ietf':
100+
return self.get_meeting_date(5)
101+
else:
102+
# TODO: Once interims have timeslots assigned,
103+
# look for the last ending timeslot instead
104+
return self.date
100105

101106
def get_00_cutoff(self):
102107
start_date = datetime.datetime(year=self.date.year, month=self.date.month, day=self.date.day, tzinfo=pytz.utc)

ietf/meeting/urls.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,6 @@
6161
(r'^(?P<num>\d+)/session/(?P<sessionid>\d+)/constraints.json', ajax.session_constraints),
6262

6363
(r'^(?P<num>\d+)/session/(?P<acronym>[A-Za-z0-9_\-\+]+)/$', views.session_details),
64-
(r'^(?P<num>\d+)/session/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<seq>\d+)/$', views.session_details),
65-
(r'^(?P<num>\d+)/session/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<week_day>[a-zA-Z]+)/$', views.session_details),
66-
(r'^(?P<num>\d+)/session/(?P<acronym>[A-Za-z0-9_\-\+]+)/(?P<date>\d{4}-\d{2}-\d{2}(-\d{4})?)/$', views.session_details),
67-
(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),
6864

6965
(r'^(?P<num>\d+)/constraint/(?P<constraintid>\d+).json', ajax.constraint_json),
7066
(r'^(?P<num>\d+).json$', ajax.meeting_json),

ietf/meeting/views.py

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -825,30 +825,13 @@ def meeting_requests(request, num=None):
825825
{"meeting": meeting, "sessions":sessions,
826826
"groups_not_meeting": groups_not_meeting})
827827

828-
def session_details(request, num, acronym, date=None, week_day=None, seq=None):
828+
def session_details(request, num, acronym ):
829829
meeting = get_meeting(num)
830830
sessions = Session.objects.filter(meeting=meeting,group__acronym=acronym,type__in=['session','plenary','other'])
831831

832832
if not sessions:
833833
sessions = Session.objects.filter(meeting=meeting,short=acronym)
834834

835-
if date:
836-
if len(date)==15:
837-
start = datetime.datetime.strptime(date,"%Y-%m-%d-%H%M")
838-
sessions = sessions.filter(timeslotassignments__schedule=meeting.agenda,timeslotassignments__timeslot__time=start)
839-
else:
840-
start = datetime.datetime.strptime(date,"%Y-%m-%d").date()
841-
end = start+datetime.timedelta(days=1)
842-
sessions = sessions.filter(timeslotassignments__schedule=meeting.agenda,timeslotassignments__timeslot__time__range=(start,end))
843-
844-
if week_day:
845-
try:
846-
dow = ['sun','mon','tue','wed','thu','fri','sat'].index(week_day.lower()[:3]) + 1
847-
except ValueError:
848-
raise Http404
849-
sessions = sessions.filter(timeslotassignments__schedule=meeting.agenda,timeslotassignments__timeslot__time__week_day=dow)
850-
851-
852835
def sort_key(session):
853836
official_sessions = session.timeslotassignments.filter(schedule=session.meeting.agenda)
854837
if official_sessions:
@@ -858,34 +841,19 @@ def sort_key(session):
858841

859842
sessions = sorted(sessions,key=sort_key)
860843

861-
if seq:
862-
iseq = int(seq) - 1
863-
if not iseq in range(0,len(sessions)):
864-
raise Http404
865-
else:
866-
sessions= [sessions[iseq]]
867-
868844
if not sessions:
869845
raise Http404
870846

871-
if len(sessions)==1:
872-
session = sessions[0]
873-
scheduled_time = "Not yet scheduled"
847+
for session in sessions:
848+
874849
ss = session.timeslotassignments.filter(schedule=meeting.agenda).order_by('timeslot__time')
875-
if ss:
876-
scheduled_time = ','.join(x.timeslot.time.strftime("%A %b-%d %H%M") for x in ss)
850+
session.time = ', '.join(x.timeslot.time.strftime("%A %b-%d %H%M") for x in ss) if ss else 'Not yet scheduled'
851+
877852
# TODO FIXME Deleted materials shouldn't be in the sessionpresentation_set
878-
filtered_sessionpresentation_set = [p for p in session.sessionpresentation_set.all() if p.document.get_state_slug(p.document.type_id)!='deleted']
879-
return render(request, "meeting/session_details.html",
880-
{ 'session':sessions[0] ,
881-
'meeting' :meeting ,
882-
'acronym' :acronym,
883-
'time': scheduled_time,
884-
'filtered_sessionpresentation_set': filtered_sessionpresentation_set
885-
})
886-
else:
887-
return render(request, "meeting/session_list.html",
888-
{ 'sessions':sessions ,
889-
'meeting' :meeting ,
890-
'acronym' :acronym,
891-
})
853+
session.filtered_sessionpresentation_set = [p for p in session.sessionpresentation_set.all() if p.document.get_state_slug(p.document.type_id)!='deleted']
854+
855+
return render(request, "meeting/session_details.html",
856+
{ 'sessions':sessions ,
857+
'meeting' :meeting ,
858+
'acronym' :acronym,
859+
})

0 commit comments

Comments
 (0)