diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py
index c45caa10e..d6bbf291f 100644
--- a/ietf/meeting/tests_views.py
+++ b/ietf/meeting/tests_views.py
@@ -46,7 +46,7 @@ from ietf.meeting.helpers import send_interim_minutes_reminder, populate_importa
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 add_event_info_to_session_qs
+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
from ietf.meeting.views import get_summary_by_area, get_summary_by_type, get_summary_by_purpose
@@ -58,10 +58,12 @@ from ietf.utils.timezone import date_today, time_now
from ietf.person.factories import PersonFactory
from ietf.group.factories import GroupFactory, GroupEventFactory, RoleFactory
-from ietf.meeting.factories import ( SessionFactory, ScheduleFactory,
+from ietf.meeting.factories import (SessionFactory, ScheduleFactory,
SessionPresentationFactory, MeetingFactory, FloorPlanFactory,
TimeSlotFactory, SlideSubmissionFactory, RoomFactory,
- ConstraintFactory, MeetingHostFactory, ProceedingsMaterialFactory )
+ ConstraintFactory, MeetingHostFactory, ProceedingsMaterialFactory,
+ AttendedFactory)
+from ietf.stats.factories import MeetingRegistrationFactory
from ietf.doc.factories import DocumentFactory, WgDraftFactory
from ietf.submit.tests import submission_file
from ietf.utils.test_utils import assert_ical_response_is_valid
@@ -5967,16 +5969,10 @@ class IphoneAppJsonTests(TestCase):
self.assertTrue(msessions.filter(group__acronym=s['group']['acronym']).exists())
class FinalizeProceedingsTests(TestCase):
- @override_settings(STATS_REGISTRATION_ATTENDEES_JSON_URL='https://ietf.example.com/{number}')
- @requests_mock.Mocker()
- def test_finalize_proceedings(self, mock):
+ def test_finalize_proceedings(self):
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)
- mock.get(
- settings.STATS_REGISTRATION_ATTENDEES_JSON_URL.format(number=meeting.number),
- text=json.dumps([{"LastName": "Smith", "FirstName": "John", "Company": "ABC", "Country": "US"}]),
- )
url = urlreverse('ietf.meeting.views.finalize_proceedings',kwargs={'num':meeting.number})
login_testing_unauthorized(self,"secretary",url)
@@ -7852,34 +7848,40 @@ class ProceedingsTests(BaseMeetingTestCase):
0,
)
- @override_settings(STATS_REGISTRATION_ATTENDEES_JSON_URL='https://ietf.example.com/{number}')
- @requests_mock.Mocker()
- def test_proceedings_attendees(self, mock):
+ def test_proceedings_attendees(self):
+ """Test proceedings attendee list. Check the following:
+ - assert onsite checkedin=True appears, not onsite checkedin=False
+ - assert remote attended appears, not remote not attended
+ - prefer onsite checkedin=True to remote attended when same person has both
+ """
+
make_meeting_test_data()
- meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97")
- mock.get(
- settings.STATS_REGISTRATION_ATTENDEES_JSON_URL.format(number=meeting.number),
- text=json.dumps([{"LastName": "Smith", "FirstName": "John", "Company": "ABC", "Country": "US"}]),
- )
- finalize(meeting)
- url = urlreverse('ietf.meeting.views.proceedings_attendees',kwargs={'num':97})
+ meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016, 7, 14), number="97")
+ person_a = PersonFactory(name='Person A')
+ person_b = PersonFactory(name='Person B')
+ person_c = PersonFactory(name='Person C')
+ person_d = PersonFactory(name='Person D')
+ MeetingRegistrationFactory(meeting=meeting, person=person_a, reg_type='onsite', checkedin=True)
+ MeetingRegistrationFactory(meeting=meeting, person=person_b, reg_type='onsite', checkedin=False)
+ MeetingRegistrationFactory(meeting=meeting, person=person_a, reg_type='remote')
+ AttendedFactory(session__meeting=meeting, session__type_id='plenary', person=person_a)
+ MeetingRegistrationFactory(meeting=meeting, person=person_c, reg_type='remote')
+ AttendedFactory(session__meeting=meeting, session__type_id='plenary', person=person_c)
+ MeetingRegistrationFactory(meeting=meeting, person=person_d, reg_type='remote')
+ url = urlreverse('ietf.meeting.views.proceedings_attendees',kwargs={'num': 97})
response = self.client.get(url)
self.assertContains(response, 'Attendee list')
q = PyQuery(response.content)
- self.assertEqual(1,len(q("#id_attendees tbody tr")))
+ self.assertEqual(2, len(q("#id_attendees tbody tr")))
+ text = q('#id_attendees tbody tr').text().replace('\n', ' ')
+ self.assertEqual(text, "A Person onsite C Person remote")
- @override_settings(STATS_REGISTRATION_ATTENDEES_JSON_URL='https://ietf.example.com/{number}')
- @requests_mock.Mocker()
- def test_proceedings_overview(self, mock):
+ 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")
- mock.get(
- settings.STATS_REGISTRATION_ATTENDEES_JSON_URL.format(number=meeting.number),
- text=json.dumps([{"LastName": "Smith", "FirstName": "John", "Company": "ABC", "Country": "US"}]),
- )
finalize(meeting)
url = urlreverse('ietf.meeting.views.proceedings_overview',kwargs={'num':97})
response = self.client.get(url)
@@ -8266,3 +8268,20 @@ class ProceedingsTests(BaseMeetingTestCase):
group = session.group
sequence = get_next_sequence(group,meeting,'recording')
self.assertEqual(sequence,1)
+
+ def test_participants_for_meeting(self):
+ person_a = PersonFactory()
+ person_b = PersonFactory()
+ person_c = PersonFactory()
+ person_d = PersonFactory()
+ m = MeetingFactory.create(type_id='ietf')
+ MeetingRegistrationFactory(meeting=m, person=person_a, reg_type='onsite', checkedin=True)
+ MeetingRegistrationFactory(meeting=m, person=person_b, reg_type='onsite', checkedin=False)
+ MeetingRegistrationFactory(meeting=m, person=person_c, reg_type='remote')
+ MeetingRegistrationFactory(meeting=m, person=person_d, reg_type='remote')
+ AttendedFactory(session__meeting=m, session__type_id='plenary', person=person_c)
+ checked_in, attended = participants_for_meeting(m)
+ self.assertTrue(person_a.pk in checked_in)
+ self.assertTrue(person_b.pk not in checked_in)
+ self.assertTrue(person_c.pk in attended)
+ self.assertTrue(person_d.pk not in attended)
\ No newline at end of file
diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py
index a99f29463..b8bb08247 100644
--- a/ietf/meeting/utils.py
+++ b/ietf/meeting/utils.py
@@ -4,16 +4,14 @@ import datetime
import itertools
import os
import pytz
-import requests
import subprocess
from collections import defaultdict
from pathlib import Path
-from urllib.error import HTTPError
from django.conf import settings
from django.contrib import messages
-from django.template.loader import render_to_string
+from django.db.models import Q
from django.utils import timezone
from django.utils.encoding import smart_str
@@ -21,7 +19,7 @@ import debug # pyflakes:ignore
from ietf.dbtemplate.models import DBTemplate
from ietf.meeting.models import (Session, SchedulingEvent, TimeSlot,
- Constraint, SchedTimeSessAssignment, SessionPresentation)
+ Constraint, SchedTimeSessAssignment, SessionPresentation, Attended)
from ietf.doc.models import Document, DocAlias, State, NewRevisionDocEvent
from ietf.doc.models import DocEvent
from ietf.group.models import Group
@@ -126,31 +124,7 @@ def sort_sessions(sessions):
return sorted(sessions, key=lambda s: (s.meeting.number, s.group.acronym, session_time_for_sorting(s, use_meeting_date=False)))
def create_proceedings_templates(meeting):
- '''Create DBTemplates for meeting proceedings'''
- # Get meeting attendees from registration system
- url = settings.STATS_REGISTRATION_ATTENDEES_JSON_URL.format(number=meeting.number)
- try:
- attendees = requests.get(url, timeout=settings.DEFAULT_REQUESTS_TIMEOUT).json()
- except (ValueError, HTTPError, requests.Timeout) as exc:
- attendees = []
- log(f'Failed to retrieve meeting attendees from [{url}]: {exc}')
-
- if attendees:
- attendees = sorted(attendees, key = lambda a: a['LastName'])
- content = render_to_string('meeting/proceedings_attendees_table.html', {
- 'attendees':attendees})
- try:
- template = DBTemplate.objects.get(path='/meeting/proceedings/%s/attendees.html' % (meeting.number, ))
- template.title='IETF %s Attendee List' % meeting.number
- template.type_id='django'
- template.content=content
- template.save()
- except DBTemplate.DoesNotExist:
- DBTemplate.objects.create(
- path='/meeting/proceedings/%s/attendees.html' % (meeting.number, ),
- title='IETF %s Attendee List' % meeting.number,
- type_id='django',
- content=content)
+ '''Create DBTemplates for meeting proceedings'''
# Make copy of default IETF Overview template
if not meeting.overview:
path = '/meeting/proceedings/%s/overview.rst' % (meeting.number, )
@@ -910,3 +884,14 @@ def post_process(doc):
desc='Converted document to PDF',
)
doc.save_with_history([e])
+
+
+def participants_for_meeting(meeting):
+ """ Return a tuple (checked_in, attended)
+ checked_in = queryset of onsite, checkedin participants values_list('person')
+ attended = queryset of remote participants who attended a session values_list('person')
+ """
+ checked_in = meeting.meetingregistration_set.filter(reg_type='onsite', checkedin=True).values_list('person', flat=True).distinct()
+ sessions = meeting.session_set.filter(Q(type='plenary') | Q(group__type__in=['wg', 'rg']))
+ attended = Attended.objects.filter(session__in=sessions).values_list('person', flat=True).distinct()
+ return (checked_in, attended)
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 48b0f2f06..75444c570 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -84,8 +84,10 @@ from ietf.meeting.utils import swap_meeting_schedule_timeslot_assignments, bulk_
from ietf.meeting.utils import preprocess_meeting_important_dates
from ietf.meeting.utils import new_doc_for_session, write_doc_for_session
from ietf.meeting.utils import get_activity_stats, post_process, create_recording
+from ietf.meeting.utils import participants_for_meeting
from ietf.message.utils import infer_message
from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName
+from ietf.stats.models import MeetingRegistration
from ietf.utils import markdown
from ietf.utils.decorators import require_api_key
from ietf.utils.hedgedoc import Note, NoteError
@@ -3858,14 +3860,19 @@ def proceedings_attendees(request, num=None):
meeting = get_meeting(num)
if meeting.proceedings_format_version == 1:
return HttpResponseRedirect(f'{settings.PROCEEDINGS_V1_BASE_URL.format(meeting=meeting)}/attendee.html')
- overview_template = '/meeting/proceedings/%s/attendees.html' % meeting.number
- try:
- template = render_to_string(overview_template, {})
- except TemplateDoesNotExist:
- raise Http404
+
+ checked_in, attended = participants_for_meeting(meeting)
+ regs = list(MeetingRegistration.objects.filter(meeting__number=num, reg_type='onsite', checkedin=True))
+
+ for mr in MeetingRegistration.objects.filter(meeting__number=num, reg_type='remote').select_related('person'):
+ if mr.person.pk in attended and mr.person.pk not in checked_in:
+ regs.append(mr)
+
+ meeting_registrations = sorted(regs, key=lambda x: (x.last_name, x.first_name))
+
return render(request, "meeting/proceedings_attendees.html", {
'meeting': meeting,
- 'template': template,
+ 'meeting_registrations': meeting_registrations,
})
def proceedings_overview(request, num=None):
diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py
index 0d4684c92..d3da0bddd 100644
--- a/ietf/nomcom/tests.py
+++ b/ietf/nomcom/tests.py
@@ -2775,6 +2775,7 @@ class rfc9389EligibilityTests(TestCase):
for person in ineligible_people:
self.assertFalse(is_eligible(person,self.nomcom))
+
class VolunteerTests(TestCase):
def test_volunteer(self):
diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py
index 6a7e3f40d..220f2e401 100644
--- a/ietf/nomcom/utils.py
+++ b/ietf/nomcom/utils.py
@@ -30,7 +30,8 @@ from ietf.doc.models import DocEvent, NewRevisionDocEvent
from ietf.group.models import Group, Role
from ietf.person.models import Email, Person
from ietf.mailtrigger.utils import gather_address_lists
-from ietf.meeting.models import Meeting, Attended
+from ietf.meeting.models import Meeting
+from ietf.meeting.utils import participants_for_meeting
from ietf.utils.pipe import pipe
from ietf.utils.mail import send_mail_text, send_mail, get_payload_text
from ietf.utils.log import log
@@ -689,9 +690,7 @@ def three_of_five_eligible_9389(previous_five, queryset=None):
counts = defaultdict(lambda: 0)
for meeting in previous_five:
- checked_in = meeting.meetingregistration_set.filter(reg_type='onsite', checkedin=True).values_list('person', flat=True)
- sessions = meeting.session_set.filter(Q(type='plenary') | Q(group__type__in=['wg', 'rg']))
- attended = Attended.objects.filter(session__in=sessions).values_list('person', flat=True)
+ checked_in, attended = participants_for_meeting(meeting)
for id in set(checked_in) | set(attended):
counts[id] += 1
return queryset.filter(pk__in=[id for id, count in counts.items() if count >= 3])
diff --git a/ietf/stats/migrations/0002_fix_meeting_registration_reg_type.py b/ietf/stats/migrations/0002_fix_meeting_registration_reg_type.py
new file mode 100644
index 000000000..c1c516b29
--- /dev/null
+++ b/ietf/stats/migrations/0002_fix_meeting_registration_reg_type.py
@@ -0,0 +1,31 @@
+# Generated by Django 4.2.6 on 2023-10-27 17:57
+
+'''
+MeetingRegistration reg_type and checkedin fields are not populated for meetings
+prior to 108. For meetings 72-106, all records are for onsite checkedin participants.
+For meeting 107 all records are for remote paticipants. Set accordingly.
+'''
+
+from django.db import migrations
+
+
+def forward(apps, schema_editor):
+ MeetingRegistration = apps.get_model("stats", "MeetingRegistration")
+ MeetingRegistration.objects.filter(meeting__number=107).update(reg_type='remote')
+ MeetingRegistration.objects.filter(meeting__number__lte=106, reg_type='').update(reg_type='onsite', checkedin=True)
+
+
+def reverse(apps, schema_editor):
+ MeetingRegistration = apps.get_model("stats", "MeetingRegistration")
+ MeetingRegistration.objects.filter(meeting__number=107).update(reg_type='')
+ MeetingRegistration.objects.filter(meeting__number__lte=106, reg_type='onsite').update(reg_type='', checkedin=False)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("stats", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.RunPython(forward, reverse),
+ ]
diff --git a/ietf/templates/meeting/proceedings_attendees.html b/ietf/templates/meeting/proceedings_attendees.html
index a3a10e61d..3516bc7ee 100644
--- a/ietf/templates/meeting/proceedings_attendees.html
+++ b/ietf/templates/meeting/proceedings_attendees.html
@@ -14,7 +14,30 @@
Attendee list of IETF {{ meeting.number }} meeting
- {{ template|safe }}
+
+
+
+
+ Last Name |
+ First Name |
+ Organization |
+ Country |
+ Registration Type |
+
+
+
+ {% for reg in meeting_registrations %}
+
+ {{ reg.last_name }} |
+ {{ reg.first_name }} |
+ {{ reg.affiliation }} |
+ {{ reg.country_code }} |
+ {{ reg.reg_type }} |
+
+ {% endfor %}
+
+
+
{% endblock %}
{% block js %}
diff --git a/ietf/templates/meeting/proceedings_attendees_table.html b/ietf/templates/meeting/proceedings_attendees_table.html
deleted file mode 100644
index 9ac1fda2a..000000000
--- a/ietf/templates/meeting/proceedings_attendees_table.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
- Last name |
- First name |
- Organization |
- Country |
-
-
-
- {% for attendee in attendees %}
-
- {{ attendee.LastName }} |
- {{ attendee.FirstName }} |
- {{ attendee.Company }} |
- {{ attendee.Country }} |
-
- {% endfor %}
-
-
\ No newline at end of file