fix: build proceedings attendee list from MeetingRegistration table. … (#6567)

* fix: build proceedings attendee list from MeetingRegistration table. Fixes #6265

* fix: move participants_for_meeting to meeting.utils

* fix: move test_participants_for_meeting to meeting tests
This commit is contained in:
Ryan Cross 2023-11-07 13:09:19 +01:00 committed by GitHub
parent 2bec81da95
commit 2974e81624
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 133 additions and 88 deletions

View file

@ -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)

View file

@ -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
@ -127,30 +125,6 @@ def sort_sessions(sessions):
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)
# 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)

View file

@ -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):

View file

@ -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):

View file

@ -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])

View file

@ -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),
]

View file

@ -14,7 +14,30 @@
</a>
</h1>
<h2>Attendee list of IETF {{ meeting.number }} meeting</h2>
{{ template|safe }}
<table id="id_attendees" class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="last">Last Name</th>
<th scope="col" data-sort="first">First Name</th>
<th scope="col" data-sort="organization">Organization</th>
<th scope="col" data-sort="country">Country</th>
<th scope="col" data-sort="type">Registration Type</th>
</tr>
</thead>
<tbody>
{% for reg in meeting_registrations %}
<tr>
<td>{{ reg.last_name }}</td>
<td>{{ reg.first_name }}</td>
<td>{{ reg.affiliation }}</td>
<td>{{ reg.country_code }}</td>
<td>{{ reg.reg_type }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% block js %}
<script src="{% static "ietf/js/list.js" %}"></script>

View file

@ -1,20 +0,0 @@
<table id="id_attendees" class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="last">Last name</th>
<th scope="col" data-sort="first">First name</th>
<th scope="col" data-sort="organization">Organization</th>
<th scope="col" data-sort="country">Country</th>
</tr>
</thead>
<tbody>
{% for attendee in attendees %}
<tr>
<td>{{ attendee.LastName }}</td>
<td>{{ attendee.FirstName }}</td>
<td>{{ attendee.Company }}</td>
<td>{{ attendee.Country }}</td>
</tr>
{% endfor %}
</tbody>
</table>