Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 47 additions & 28 deletions ietf/meeting/tests_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
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
Expand All @@ -58,10 +58,12 @@

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
Expand Down Expand Up @@ -5967,16 +5969,10 @@ def test_iphone_app_json(self):
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)
Expand Down Expand Up @@ -7852,34 +7848,40 @@ def test_proceedings_acknowledgements_link(self):
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)
Expand Down Expand Up @@ -8266,3 +8268,20 @@ def test_get_next_sequence(self):
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)
43 changes: 14 additions & 29 deletions ietf/meeting/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,22 @@
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

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
Expand Down Expand Up @@ -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, )
Expand Down Expand Up @@ -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)
19 changes: 13 additions & 6 deletions ietf/meeting/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@
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
Expand Down Expand Up @@ -3851,14 +3853,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))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bummer that first/last name is baked in here (and presumably deeper in the registration). If sorting these comes up again, it'd probably be worth factoring out a method to sort by name.


return render(request, "meeting/proceedings_attendees.html", {
'meeting': meeting,
'template': template,
'meeting_registrations': meeting_registrations,
})

def proceedings_overview(request, num=None):
Expand Down
1 change: 1 addition & 0 deletions ietf/nomcom/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2775,6 +2775,7 @@ def test_elig_by_meetings(self):
for person in ineligible_people:
self.assertFalse(is_eligible(person,self.nomcom))


class VolunteerTests(TestCase):

def test_volunteer(self):
Expand Down
7 changes: 3 additions & 4 deletions ietf/nomcom/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
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
Expand Down Expand Up @@ -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])
Expand Down
31 changes: 31 additions & 0 deletions ietf/stats/migrations/0002_fix_meeting_registration_reg_type.py
Original file line number Diff line number Diff line change
@@ -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),
]
25 changes: 24 additions & 1 deletion ietf/templates/meeting/proceedings_attendees.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,30 @@ <h1>
</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>
Expand Down
20 changes: 0 additions & 20 deletions ietf/templates/meeting/proceedings_attendees_table.html

This file was deleted.