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 }} + + + + + + + + + + + + + {% for reg in meeting_registrations %} + + + + + + + + {% endfor %} + +
Last NameFirst NameOrganizationCountryRegistration Type
{{ reg.last_name }}{{ reg.first_name }}{{ reg.affiliation }}{{ reg.country_code }}{{ reg.reg_type }}
+ {% 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 @@ - - - - - - - - - - - {% for attendee in attendees %} - - - - - - - {% endfor %} - -
Last nameFirst nameOrganizationCountry
{{ attendee.LastName }}{{ attendee.FirstName }}{{ attendee.Company }}{{ attendee.Country }}
\ No newline at end of file