Skip to content
22 changes: 22 additions & 0 deletions ietf/meeting/migrations/0005_attended_origin_attended_time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright The IETF Trust 2024, All Rights Reserved

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("meeting", "0004_session_chat_room"),
]

operations = [
migrations.AddField(
model_name="attended",
name="origin",
field=models.CharField(default="datatracker", max_length=32),
),
migrations.AddField(
model_name="attended",
name="time",
field=models.DateTimeField(auto_now_add=True, null=True),
),
]
4 changes: 3 additions & 1 deletion ietf/meeting/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright The IETF Trust 2007-2022, All Rights Reserved
# Copyright The IETF Trust 2007-2024, All Rights Reserved
# -*- coding: utf-8 -*-


Expand Down Expand Up @@ -1407,6 +1407,8 @@ class Meta:
class Attended(models.Model):
person = ForeignKey(Person)
session = ForeignKey(Session)
time = models.DateTimeField(auto_now_add=True, null=True, blank=True)
origin = models.CharField(max_length=32, default='datatracker')
Comment thread
pselkirk marked this conversation as resolved.

class Meta:
unique_together = (('person', 'session'),)
Expand Down
128 changes: 119 additions & 9 deletions ietf/meeting/tests_views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright The IETF Trust 2009-2023, All Rights Reserved
# Copyright The IETF Trust 2009-2024, All Rights Reserved
# -*- coding: utf-8 -*-
import datetime
import io
Expand Down Expand Up @@ -38,14 +38,14 @@
from ietf.doc.models import Document, NewRevisionDocEvent
from ietf.group.models import Group, Role, GroupFeatures
from ietf.group.utils import can_manage_group
from ietf.person.models import Person
from ietf.person.models import Person, PersonalApiKey
from ietf.meeting.helpers import can_approve_interim_request, can_view_interim_request, preprocess_assignments_for_agenda
from ietf.meeting.helpers import send_interim_approval_request, AgendaKeywordTagger
from ietf.meeting.helpers import send_interim_meeting_cancellation_notice, send_interim_session_cancellation_notice
from ietf.meeting.helpers import send_interim_minutes_reminder, populate_important_dates, update_important_dates
from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignment, Schedule, SessionPresentation, SlideSubmission, SchedulingEvent, Room, Constraint, ConstraintName
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 condition_slide_order
from ietf.meeting.utils import add_event_info_to_session_qs, participants_for_meeting
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
Expand Down Expand Up @@ -517,7 +517,7 @@ def test_named_session(self):
group = GroupFactory()
plain_session = SessionFactory(meeting=meeting, group=group)
named_session = SessionFactory(meeting=meeting, group=group, name='I Got a Name')
for doc_type_id in ('agenda', 'minutes', 'bluesheets', 'slides', 'draft'):
for doc_type_id in ('agenda', 'minutes', 'slides', 'draft'):
# Set up sessions materials that will have distinct URLs for each session.
# This depends on settings.MEETING_DOC_HREFS and may need updating if that changes.
SessionPresentationFactory(
Expand Down Expand Up @@ -7774,7 +7774,7 @@ def test_proceedings(self):

def test_named_session(self):
"""Session with a name should appear separately in the proceedings"""
meeting = MeetingFactory(type_id='ietf', number='100')
meeting = MeetingFactory(type_id='ietf', number='100', proceedings_final=True)
group = GroupFactory()
plain_session = SessionFactory(meeting=meeting, group=group)
named_session = SessionFactory(meeting=meeting, group=group, name='I Got a Name')
Expand Down Expand Up @@ -7895,7 +7895,6 @@ def test_proceedings_attendees(self):
- prefer onsite checkedin=True to remote attended when same person has both
"""

make_meeting_test_data()
meeting = MeetingFactory(type_id='ietf', date=datetime.date(2023, 11, 4), number="118")
person_a = PersonFactory(name='Person A')
person_b = PersonFactory(name='Person B')
Expand All @@ -7920,9 +7919,14 @@ 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()
meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97")
finalize(meeting)
meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97"))

# finalize meeting
url = urlreverse('ietf.meeting.views.finalize_proceedings',kwargs={'num':meeting.number})
login_testing_unauthorized(self,"secretary",url)
r = self.client.post(url,{'finalize':1})
self.assertEqual(r.status_code, 302)

url = urlreverse('ietf.meeting.views.proceedings_overview',kwargs={'num':97})
response = self.client.get(url)
self.assertContains(response, 'The Internet Engineering Task Force')
Expand Down Expand Up @@ -8325,3 +8329,109 @@ def test_participants_for_meeting(self):
self.assertTrue(person_b.pk not in checked_in)
self.assertTrue(person_c.pk in attended)
self.assertTrue(person_d.pk not in attended)

def test_session_attendance(self):
meeting = MeetingFactory(type_id='ietf', date=datetime.date(2023, 11, 4), number='118')
make_meeting_test_data(meeting=meeting)
session = Session.objects.filter(meeting=meeting, group__acronym='mars').first()
regs = MeetingRegistrationFactory.create_batch(3, meeting=meeting)
persons = [reg.person for reg in regs]
self.assertEqual(session.attended_set.count(), 0)

# If there are no attendees, the link isn't offered, and getting
# the page directly returns an empty list.
session_url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':meeting.number, 'acronym':session.group.acronym})
attendance_url = urlreverse('ietf.meeting.views.session_attendance', kwargs={'num':meeting.number, 'session_id':session.id})
r = self.client.get(session_url)
self.assertNotContains(r, attendance_url)
r = self.client.get(attendance_url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, '0 attendees')

# Add some attendees
add_attendees_url = urlreverse('ietf.meeting.views.api_add_session_attendees')
recmanrole = RoleFactory(group__type_id='ietf', name_id='recman', person__user__last_login=timezone.now())
recman = recmanrole.person
apikey = PersonalApiKey.objects.create(endpoint=add_attendees_url, person=recman)
attendees = [person.user.pk for person in persons]
self.client.login(username='recman', password='recman+password')
r = self.client.post(add_attendees_url, {'apikey':apikey.hash(), 'attended':f'{{"session_id":{session.pk},"attendees":{attendees}}}'})
self.assertEqual(r.status_code, 200)
self.assertEqual(session.attended_set.count(), 3)

# Before a meeting is finalized, session_attendance renders a live
# view of the Attended records for the session.
r = self.client.get(session_url)
self.assertContains(r, attendance_url)
r = self.client.get(attendance_url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, '3 attendees')
for person in persons:
self.assertContains(r, person.name)

# Test for the "I was there" button.
def _test_button(person, expected):
username = person.user.username
self.client.login(username=username, password=f'{username}+password')
r = self.client.get(attendance_url)
self.assertEqual(b"I was there" in r.content, expected)
# recman isn't registered for the meeting
_test_button(recman, False)
# person0 is already on the bluesheet
_test_button(persons[0], False)
# person3 attests he was there
persons.append(MeetingRegistrationFactory(meeting=meeting).person)
# button isn't shown if we're outside the corrections windows
meeting.importantdate_set.create(name_id='revsub',date=date_today() - datetime.timedelta(days=20))
_test_button(persons[3], False)
# attempt to POST anyway is ignored
r = self.client.post(attendance_url)
self.assertEqual(r.status_code, 200)
self.assertNotContains(r, persons[3].name)
self.assertEqual(session.attended_set.count(), 3)
# button is shown, and POST is accepted
meeting.importantdate_set.update(name_id='revsub',date=date_today() + datetime.timedelta(days=20))
_test_button(persons[3], True)
r = self.client.post(attendance_url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, persons[3].name)
self.assertEqual(session.attended_set.count(), 4)

# When the meeting is finalized, a bluesheet file is generated,
# and session_attendance redirects to the file.
self.client.login(username='secretary',password='secretary+password')
finalize_url = urlreverse('ietf.meeting.views.finalize_proceedings', kwargs={'num':meeting.number})
r = self.client.post(finalize_url, {'finalize':1})
self.assertRedirects(r, urlreverse('ietf.meeting.views.proceedings', kwargs={'num':meeting.number}))
doc = session.sessionpresentation_set.filter(document__type_id='bluesheets').first().document
self.assertEqual(doc.rev,'00')
text = doc.text()
self.assertIn('4 attendees', text)
for person in persons:
self.assertIn(person.name, text)
r = self.client.get(session_url)
self.assertContains(r, doc.get_href())
self.assertNotContains(r, attendance_url)
r = self.client.get(attendance_url)
self.assertEqual(r.status_code,302)
self.assertEqual(r['Location'],doc.get_href())

# An interim meeting is considered finalized immediately.
meeting = make_interim_meeting(group=GroupFactory(acronym='mars'), date=date_today())
session = Session.objects.filter(meeting=meeting, group__acronym='mars').first()
attendance_url = urlreverse('ietf.meeting.views.session_attendance', kwargs={'num':meeting.number, 'session_id':session.id})
self.assertEqual(session.attended_set.count(), 0)
self.client.login(username='recman', password='recman+password')
attendees = [person.user.pk for person in persons]
r = self.client.post(add_attendees_url, {'apikey':apikey.hash(), 'attended':f'{{"session_id":{session.pk},"attendees":{attendees}}}'})
self.assertEqual(r.status_code, 200)
self.assertEqual(session.attended_set.count(), 4)
doc = session.sessionpresentation_set.filter(document__type_id='bluesheets').first().document
self.assertEqual(doc.rev,'00')
session_url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':meeting.number, 'acronym':session.group.acronym})
r = self.client.get(session_url)
self.assertContains(r, doc.get_href())
self.assertNotContains(r, attendance_url)
r = self.client.get(attendance_url)
self.assertEqual(r.status_code,302)
self.assertEqual(r['Location'],doc.get_href())
3 changes: 2 additions & 1 deletion ietf/meeting/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright The IETF Trust 2007-2020, All Rights Reserved
# Copyright The IETF Trust 2007-2024, All Rights Reserved

from django.conf import settings
from django.urls import include
Expand All @@ -16,6 +16,7 @@ def get_redirect_url(self, *args, **kwargs):
safe_for_all_meeting_types = [
url(r'^session/(?P<acronym>[-a-z0-9]+)/?$', views.session_details),
url(r'^session/(?P<session_id>\d+)/drafts$', views.add_session_drafts),
url(r'^session/(?P<session_id>\d+)/attendance$', views.session_attendance),
url(r'^session/(?P<session_id>\d+)/bluesheets$', views.upload_session_bluesheets),
url(r'^session/(?P<session_id>\d+)/minutes$', views.upload_session_minutes),
url(r'^session/(?P<session_id>\d+)/agenda$', views.upload_session_agenda),
Expand Down
11 changes: 9 additions & 2 deletions ietf/meeting/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright The IETF Trust 2016-2020, All Rights Reserved
# Copyright The IETF Trust 2016-2024, All Rights Reserved
# -*- coding: utf-8 -*-
import datetime
import itertools
Expand Down Expand Up @@ -139,7 +139,8 @@ def create_proceedings_templates(meeting):
meeting.overview = template
meeting.save()

def finalize(meeting):
def finalize(request, meeting):
from ietf.meeting.views import generate_bluesheet
end_date = meeting.end_date()
end_time = meeting.tz().localize(
datetime.datetime.combine(
Expand All @@ -155,6 +156,12 @@ def finalize(meeting):
else:
sp.rev = '00'
sp.save()

# Don't try to generate a bluesheet if it's before we had Attended records.
if int(meeting.number) >= 108:
save_error = generate_bluesheet(request, session)
if save_error:
messages.error(request, save_error)

create_proceedings_templates(meeting)
meeting.proceedings_final = True
Expand Down
Loading