datatracker/ietf/meeting/tests_views.py
Matthew Holloway db7d3074da
feat: Add session recordings (#8218)
* feat: add session recordings

* feat: add session recordings

* feat: deleting recordings

* feat: deleting recordings and initial form values

* feat: use meeting date rather than today for initial title field. Fix delete recording

* feat: confirm delete recordings modal. fix server utils delete recording

* fix: removing debug console.log

* feat: change button name from 'Ok' to 'Delete' for confirm deletion to be clearer

* feat: UTC time in string and delete modal text

* fix: django html validation tests

* fix: django html validation tests

* fix: django html validation tests

* refactor: Work with SessionPresentations

* fix: better ordering

* chore: drop rev, hide table when empty

* test: test delete_recordings method

* fix: debug delete_recordings

* test: test add_session_recordings view

* fix: better permissions handling

* fix: only delete recordings for selected session

* refactor: inline script -> js module

* chore: remove accidental import

*shakes fist at pycharm*

* fix: consistent timestamp format

plus slight rephrase

* style: Black

* chore: remove comment

* test: update test to match

* fix: reversible url pattern for materials

Tests were perturbed in a way that led to a test
getting an interim instead of an IETF meeting.
This exposed a bug reversing the URL for the
materials_document() view. This splits it into
two patterns that are equivalent to the original.

---------

Co-authored-by: Jennifer Richards <jennifer@staff.ietf.org>
2025-01-31 10:28:39 -06:00

9092 lines
441 KiB
Python

# Copyright The IETF Trust 2009-2024, All Rights Reserved
# -*- coding: utf-8 -*-
import datetime
import io
import json
import os
import random
import re
import shutil
import pytz
import requests.exceptions
import requests_mock
from unittest import skipIf
from mock import call, patch, PropertyMock
from pyquery import PyQuery
from lxml.etree import tostring
from io import StringIO, BytesIO
from bs4 import BeautifulSoup
from urllib.parse import urlparse, urlsplit
from PIL import Image
from pathlib import Path
from tempfile import NamedTemporaryFile
from zoneinfo import ZoneInfo
from django.urls import reverse as urlreverse
from django.conf import settings
from django.contrib.auth.models import User
from django.core.serializers.json import DjangoJSONEncoder
from django.test import Client, override_settings
from django.db.models import F, Max
from django.http import QueryDict, FileResponse
from django.template import Context, Template
from django.utils import timezone
from django.utils.text import slugify
import debug # pyflakes:ignore
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.meeting.helpers import can_approve_interim_request, can_request_interim_meeting, 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 condition_slide_order
from ietf.meeting.utils import add_event_info_to_session_qs, participants_for_meeting
from ietf.meeting.utils import create_recording, delete_recording, get_next_sequence, bluesheet_data
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, generate_agenda_data
from ietf.name.models import SessionStatusName, ImportantDateName, RoleName, ProceedingsMaterialTypeName
from ietf.utils.decorators import skip_coverage
from ietf.utils.mail import outbox, empty_outbox, get_payload_text
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent
from ietf.utils.timezone import date_today, time_now
from ietf.person.factories import PersonFactory, PersonalApiKeyFactory
from ietf.group.factories import GroupFactory, GroupEventFactory, RoleFactory
from ietf.meeting.factories import (SessionFactory, ScheduleFactory,
SessionPresentationFactory, MeetingFactory, FloorPlanFactory,
TimeSlotFactory, SlideSubmissionFactory, RoomFactory,
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
if os.path.exists(settings.GHOSTSCRIPT_COMMAND):
skip_pdf_tests = False
skip_message = ""
else:
skip_pdf_tests = True
skip_message = ("Skipping pdf test: The binary for ghostscript wasn't found in the\n "
"location indicated in settings.py.")
print(" "+skip_message)
class BaseMeetingTestCase(TestCase):
"""Base class for meeting-related tests that need to set up temporary directories
This creates temporary directories for meeting-related uploads, then updates settings
to point to them. It also patches the Storage class to use the temporary directories.
When done, removes its files, resets the settings, and shuts off the patched Storage.
If subclasses have their own setUp/tearDown routines, they must remember to call the
superclass methods.
"""
def setUp(self):
super().setUp()
self.materials_dir = self.tempdir('materials')
self.storage_dir = self.tempdir('storage')
#
archive_dir = Path(settings.INTERNET_DRAFT_ARCHIVE_DIR)
(archive_dir / "unknown_ids").mkdir()
(archive_dir / "deleted_tombstones").mkdir()
(archive_dir / "expired_without_tombstone").mkdir()
#
self.saved_agenda_path = settings.AGENDA_PATH
self.saved_meetinghost_logo_path = settings.MEETINGHOST_LOGO_PATH
#
settings.AGENDA_PATH = self.materials_dir
settings.MEETINGHOST_LOGO_PATH = self.storage_dir
# The FileSystemStorage has already set its location before
# the settings were changed. Mock the method it uses to get the
# location and fill in our temporary location. Without this, test
# files will upload to the locations specified in settings.py.
# Note that this will affect any use of the storage class in
# meeting.models - i.e., FloorPlan.image and MeetingHost.logo
self.patcher = patch('ietf.meeting.models.NoLocationMigrationFileSystemStorage.base_location',
new_callable=PropertyMock)
mocked = self.patcher.start()
mocked.return_value = self.storage_dir
def tearDown(self):
self.patcher.stop()
#
shutil.rmtree(self.storage_dir)
shutil.rmtree(self.materials_dir)
#
settings.AGENDA_PATH = self.saved_agenda_path
settings.MEETINGHOST_LOGO_PATH = self.saved_meetinghost_logo_path
super().tearDown()
def write_materials_file(self, meeting, doc, content, charset="utf-8", with_ext=None):
if with_ext is None:
filename = doc.uploaded_filename
else:
filename = Path(doc.uploaded_filename).with_suffix(with_ext)
path = os.path.join(self.materials_dir, "%s/%s/%s" % (meeting.number, doc.type_id, filename))
dirname = os.path.dirname(path)
if not os.path.exists(dirname):
os.makedirs(dirname)
if isinstance(content, str):
content = content.encode(charset)
with io.open(path, "wb") as f:
f.write(content)
def write_materials_files(self, meeting, session):
draft = Document.objects.filter(type="draft", group=session.group).first()
self.write_materials_file(meeting, session.materials.get(type="agenda"),
"1. WG status (15 minutes)\n\n2. Status of %s\n\n" % draft.name)
self.write_materials_file(meeting, session.materials.get(type="minutes"),
"1. More work items underway\n\n2. The draft will be finished before next meeting\n\n")
self.write_materials_file(meeting, session.materials.filter(type="slides").exclude(states__type__slug='slides',states__slug='deleted').first(),
"This is a slideshow")
class AgendaApiTests(TestCase):
def test_agenda_extract_schedule_location(self):
meeting = MeetingFactory(type_id='ietf')
room = RoomFactory(meeting=meeting, floorplan=FloorPlanFactory(meeting=meeting))
hidden_ts = TimeSlotFactory(meeting=meeting, location=room, show_location=False)
shown_ts = TimeSlotFactory(meeting=meeting, location=room, show_location=True)
hidden_sess = SessionFactory(meeting=meeting, add_to_schedule=False)
shown_sess = SessionFactory(meeting=meeting, add_to_schedule=False)
meeting.schedule.assignments.create(timeslot=hidden_ts, session=hidden_sess)
meeting.schedule.assignments.create(timeslot=shown_ts, session=shown_sess)
processed = preprocess_assignments_for_agenda(
SchedTimeSessAssignment.objects.filter(session__in=[hidden_sess, shown_sess]),
meeting
)
AgendaKeywordTagger(assignments=processed).apply()
extracted = {item.session.pk: agenda_extract_schedule(item) for item in processed}
hidden = extracted[hidden_sess.pk]
self.assertIsNone(hidden['room'])
self.assertEqual(hidden['location'], {})
shown = extracted[shown_sess.pk]
self.assertEqual(shown['room'], room.name)
self.assertEqual(shown['location'], {'name': room.floorplan.name, 'short': room.floorplan.short})
def test_agenda_extract_schedule_names(self):
meeting = MeetingFactory(type_id='ietf')
named_timeslots = TimeSlotFactory.create_batch(2, meeting=meeting, name='Timeslot Name')
unnamed_timeslots = TimeSlotFactory.create_batch(2, meeting=meeting, name='')
named_sessions = SessionFactory.create_batch(2, meeting=meeting, name='Session Name')
unnamed_sessions = SessionFactory.create_batch(2, meeting=meeting, name='')
pk_with = {
'both named': named_sessions[0].timeslotassignments.create(
schedule=meeting.schedule,
timeslot=named_timeslots[0],
).pk,
'session named': named_sessions[1].timeslotassignments.create(
schedule=meeting.schedule,
timeslot=unnamed_timeslots[0],
).pk,
'timeslot named': unnamed_sessions[0].timeslotassignments.create(
schedule=meeting.schedule,
timeslot=named_timeslots[1],
).pk,
'neither named': unnamed_sessions[1].timeslotassignments.create(
schedule=meeting.schedule,
timeslot=unnamed_timeslots[1],
).pk,
}
processed = preprocess_assignments_for_agenda(meeting.schedule.assignments.all(), meeting)
AgendaKeywordTagger(assignments=processed).apply()
extracted = {item.pk: agenda_extract_schedule(item) for item in processed}
self.assertEqual(extracted[pk_with['both named']]['name'], 'Session Name')
self.assertEqual(extracted[pk_with['both named']]['slotName'], 'Timeslot Name')
self.assertEqual(extracted[pk_with['session named']]['name'], 'Session Name')
self.assertEqual(extracted[pk_with['session named']]['slotName'], '')
self.assertEqual(extracted[pk_with['timeslot named']]['name'], '')
self.assertEqual(extracted[pk_with['timeslot named']]['slotName'], 'Timeslot Name')
self.assertEqual(extracted[pk_with['neither named']]['name'], '')
self.assertEqual(extracted[pk_with['neither named']]['slotName'], '')
class MeetingTests(BaseMeetingTestCase):
@override_settings(
MEETECHO_ONSITE_TOOL_URL="https://onsite.example.com",
MEETECHO_VIDEO_STREAM_URL="https://meetecho.example.com",
)
def test_meeting_agenda(self):
meeting = make_meeting_test_data()
session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
session.remote_instructions='https://remote.example.com'
session.save()
slot = TimeSlot.objects.get(sessionassignments__session=session,sessionassignments__schedule=meeting.schedule)
meeting.timeslot_set.filter(type_id="break").update(show_location=False)
#
self.write_materials_files(meeting, session)
#
future_year = date_today().year+1
future_num = (future_year-1984)*3 # valid for the mid-year meeting
future_meeting = Meeting.objects.create(date=datetime.date(future_year, 7, 22), number=future_num, type_id='ietf',
city="Panama City", country="PA", time_zone='America/Panama')
registration_text = "Registration"
# Extremely rudementary test of agenda-neue - to be replaced with back-end tests as the front-end tests are developed.
r = self.client.get(urlreverse("agenda", kwargs=dict(num=meeting.number,utc='-utc')))
self.assertEqual(r.status_code, 200)
# Agenda API tests
# -> Meeting data
# First, check that the generation function does the right thing
generated_data = generate_agenda_data(meeting.number)
self.assertEqual(
generated_data,
{
"meeting": {
"number": meeting.number,
"city": meeting.city,
"startDate": meeting.date.isoformat(),
"endDate": meeting.end_date().isoformat(),
"updated": generated_data.get("meeting").get("updated"), # Just expect the value to exist
"timezone": meeting.time_zone,
"infoNote": meeting.agenda_info_note,
"warningNote": meeting.agenda_warning_note
},
"categories": generated_data.get("categories"), # Just expect the value to exist
"isCurrentMeeting": True,
"usesNotes": False, # make_meeting_test_data sets number=72
"schedule": generated_data.get("schedule"), # Just expect the value to exist
"floors": []
}
)
with patch("ietf.meeting.views.generate_agenda_data", return_value=generated_data):
r = self.client.get(urlreverse("ietf.meeting.views.api_get_agenda_data", kwargs=dict(num=meeting.number)))
self.assertEqual(r.status_code, 200)
# json.dumps using the DjangoJSONEncoder to handle timestamps consistently
self.assertJSONEqual(r.content.decode("utf8"), json.dumps(generated_data, cls=DjangoJSONEncoder))
# -> Session MaterialM
r = self.client.get(urlreverse("ietf.meeting.views.api_get_session_materials", kwargs=dict(session_id=session.id)))
self.assertEqual(r.status_code, 200)
rjson = json.loads(r.content.decode("utf8"))
minutes = session.minutes()
self.assertJSONEqual(
r.content.decode("utf8"),
{
"url": session.agenda().get_href(),
"slides": rjson.get("slides"), # Just expect the value to exist
"minutes": {
"id": minutes.id,
"title": minutes.title,
"url": minutes.get_href(),
"ext": minutes.file_extension()
} if minutes is not None else None
}
)
# text
r = self.client.get(urlreverse("ietf.meeting.views.agenda_plain", kwargs=dict(num=meeting.number, ext=".txt")))
self.assertContains(r, session.group.acronym)
self.assertContains(r, session.group.name)
self.assertContains(r, session.group.parent.acronym.upper())
self.assertContains(r, slot.location.name)
self.assertContains(r, "{}-{}".format(
slot.time.astimezone(meeting.tz()).strftime("%H%M"),
(slot.time + slot.duration).astimezone(meeting.tz()).strftime("%H%M"),
))
self.assertContains(r, f"shown in the {meeting.tz()} time zone")
updated = meeting.updated().astimezone(meeting.tz()).strftime("%Y-%m-%d %H:%M:%S %Z")
self.assertContains(r, f"Updated {updated}")
# text, UTC
r = self.client.get(urlreverse(
"ietf.meeting.views.agenda_plain",
kwargs=dict(num=meeting.number, ext=".txt", utc="-utc"),
))
self.assertContains(r, session.group.acronym)
self.assertContains(r, session.group.name)
self.assertContains(r, session.group.parent.acronym.upper())
self.assertContains(r, slot.location.name)
self.assertContains(r, "{}-{}".format(
slot.time.astimezone(datetime.timezone.utc).strftime("%H%M"),
(slot.time + slot.duration).astimezone(datetime.timezone.utc).strftime("%H%M"),
))
self.assertContains(r, "shown in UTC")
updated = meeting.updated().astimezone(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z")
self.assertContains(r, f"Updated {updated}")
# text, invalid updated (none)
with patch("ietf.meeting.models.Meeting.updated", return_value=None):
r = self.client.get(urlreverse(
"ietf.meeting.views.agenda_plain",
kwargs=dict(num=meeting.number, ext=".txt", utc="-utc"),
))
self.assertNotContains(r, "Updated ")
# future meeting, no agenda
r = self.client.get(urlreverse("ietf.meeting.views.agenda_plain", kwargs=dict(num=future_meeting.number, ext=".txt")))
self.assertContains(r, "There is no agenda available yet.")
self.assertTemplateUsed(r, 'meeting/no-agenda.txt')
# CSV
r = self.client.get(urlreverse("ietf.meeting.views.agenda_plain", kwargs=dict(num=meeting.number, ext=".csv")))
self.assertContains(r, session.group.acronym)
self.assertContains(r, session.group.name)
self.assertContains(r, session.group.parent.acronym.upper())
self.assertContains(r, slot.location.name)
self.assertContains(r, registration_text)
start_time = slot.time.astimezone(meeting.tz())
end_time = slot.end_time().astimezone(meeting.tz())
self.assertContains(r, '"{}","{}","{}"'.format(
start_time.strftime("%Y-%m-%d"),
start_time.strftime("%H%M"),
end_time.strftime("%H%M"),
))
self.assertContains(r, session.materials.get(type='agenda').uploaded_filename)
self.assertContains(r, session.materials.filter(type='slides').exclude(states__type__slug='slides',states__slug='deleted').first().uploaded_filename)
self.assertNotContains(r, session.materials.filter(type='slides',states__type__slug='slides',states__slug='deleted').first().uploaded_filename)
# CSV, utc
r = self.client.get(urlreverse(
"ietf.meeting.views.agenda_plain",
kwargs=dict(num=meeting.number, ext=".csv", utc="-utc"),
))
self.assertContains(r, session.group.acronym)
self.assertContains(r, session.group.name)
self.assertContains(r, session.group.parent.acronym.upper())
self.assertContains(r, slot.location.name)
self.assertContains(r, registration_text)
start_time = slot.time.astimezone(datetime.timezone.utc)
end_time = slot.end_time().astimezone(datetime.timezone.utc)
self.assertContains(r, '"{}","{}","{}"'.format(
start_time.strftime("%Y-%m-%d"),
start_time.strftime("%H%M"),
end_time.strftime("%H%M"),
))
self.assertContains(r, session.materials.get(type='agenda').uploaded_filename)
self.assertContains(r, session.materials.filter(type='slides').exclude(states__type__slug='slides',states__slug='deleted').first().uploaded_filename)
self.assertNotContains(r, session.materials.filter(type='slides',states__type__slug='slides',states__slug='deleted').first().uploaded_filename)
# iCal, no session filtering
ical_url = urlreverse("ietf.meeting.views.agenda_ical", kwargs=dict(num=meeting.number))
r = self.client.get(ical_url)
assert_ical_response_is_valid(self, r)
self.assertContains(r, "BEGIN:VTIMEZONE")
self.assertContains(r, "END:VTIMEZONE")
# iCal, single group
r = self.client.get(ical_url + "?show=" + session.group.parent.acronym.upper())
assert_ical_response_is_valid(self, r)
self.assertContains(r, session.group.acronym)
self.assertContains(r, session.group.name)
self.assertContains(r, session.remote_instructions)
self.assertContains(r, slot.location.name)
self.assertContains(r, 'https://onsite.example.com')
self.assertContains(r, 'https://meetecho.example.com')
self.assertContains(r, "BEGIN:VTIMEZONE")
self.assertContains(r, "END:VTIMEZONE")
self.assertContains(r, session.agenda().get_href())
self.assertContains(
r,
urlreverse(
'ietf.meeting.views.session_details',
kwargs=dict(num=meeting.number, acronym=session.group.acronym)),
msg_prefix='ical should contain link to meeting materials page for session')
# Floor Plan
r = self.client.get(urlreverse('floor-plan', kwargs=dict(num=meeting.number)))
self.assertEqual(r.status_code, 200)
def test_session_recordings_via_factories(self):
session = SessionFactory(meeting__type_id="ietf", meeting__date=date_today()-datetime.timedelta(days=180))
self.assertEqual(session.meetecho_recording_name, "")
self.assertEqual(len(session.recordings()), 0)
url = urlreverse("ietf.meeting.views.session_details", kwargs=dict(num=session.meeting.number, acronym=session.group.acronym))
r = self.client.get(url)
q = PyQuery(r.content)
# debug.show("q(f'#notes_and_recordings_{session.pk}')")
self.assertEqual(len(q(f"#notes_and_recordings_{session.pk} tr")), 1)
link = q(f"#notes_and_recordings_{session.pk} tr a")
self.assertEqual(len(link), 1)
self.assertEqual(link[0].attrib['href'], str(session.session_recording_url()))
session.meetecho_recording_name = 'my_test_session_name'
session.save()
r = self.client.get(url)
q = PyQuery(r.content)
self.assertEqual(len(q(f"#notes_and_recordings_{session.pk} tr")), 1)
links = q(f"#notes_and_recordings_{session.pk} tr a")
self.assertEqual(len(links), 1)
self.assertEqual(links[0].attrib['href'], session.session_recording_url())
new_recording_url = "https://www.youtube.com/watch?v=jNQXAC9IVRw"
new_recording_title = "Me at the zoo"
create_recording(session, new_recording_url, new_recording_title)
r = self.client.get(url)
q = PyQuery(r.content)
self.assertEqual(len(q(f"#notes_and_recordings_{session.pk} tr")), 2)
links = q(f"#notes_and_recordings_{session.pk} tr a")
self.assertEqual(len(links), 2)
self.assertEqual(links[0].attrib['href'], new_recording_url)
self.assertIn(new_recording_title, links[0].text_content())
#debug.show("q(f'#notes_and_recordings_{session_pk}')")
def test_delete_recordings(self):
# No user specified, active recording state
sp = SessionPresentationFactory(
document__type_id="recording",
document__external_url="https://example.com/some-recording",
document__states=[("recording", "active")],
)
doc = sp.document
doc.docevent_set.all().delete() # clear this out
delete_recording(sp)
self.assertFalse(SessionPresentation.objects.filter(pk=sp.pk).exists())
self.assertEqual(doc.get_state("recording").slug, "deleted", "recording state updated")
self.assertEqual(doc.docevent_set.count(), 1, "one event added")
event = doc.docevent_set.first()
self.assertEqual(event.type, "changed_state", "event is a changed_state event")
self.assertEqual(event.by.name, "(System)", "system user is responsible")
# Specified user, no recording state
sp = SessionPresentationFactory(
document__type_id="recording",
document__external_url="https://example.com/some-recording",
document__states=[],
)
doc = sp.document
doc.docevent_set.all().delete() # clear this out
user = PersonFactory() # naming matches the methods - user is a Person, not a User
delete_recording(sp, user=user)
self.assertFalse(SessionPresentation.objects.filter(pk=sp.pk).exists())
self.assertEqual(doc.get_state("recording").slug, "deleted", "recording state updated")
self.assertEqual(doc.docevent_set.count(), 1, "one event added")
event = doc.docevent_set.first()
self.assertEqual(event.type, "changed_state", "event is a changed_state event")
self.assertEqual(event.by, user, "user is responsible")
# Document is not a recording
sp = SessionPresentationFactory(
document__type_id="draft",
document__external_url="https://example.com/some-recording",
)
with self.assertRaises(ValueError):
delete_recording(sp)
def test_agenda_ical_next_meeting_type(self):
# start with no upcoming IETF meetings, just an interim
MeetingFactory(
type_id="interim", date=date_today() + datetime.timedelta(days=15)
)
r = self.client.get(urlreverse("ietf.meeting.views.agenda_ical", kwargs={}))
self.assertEqual(
r.status_code, 404, "Should not return an interim meeting as next meeting"
)
# create an IETF meeting after the interim - it should be found as "next"
ietf_meeting = MeetingFactory(
type_id="ietf", date=date_today() + datetime.timedelta(days=30)
)
SessionFactory(meeting=ietf_meeting, name="Session at IETF meeting")
r = self.client.get(urlreverse("ietf.meeting.views.agenda_ical", kwargs={}))
self.assertContains(r, "Session at IETF meeting", status_code=200)
def test_agenda_json_next_meeting_type(self):
# start with no upcoming IETF meetings, just an interim
MeetingFactory(
type_id="interim", date=date_today() + datetime.timedelta(days=15)
)
r = self.client.get(urlreverse("ietf.meeting.views.agenda_json", kwargs={}))
self.assertEqual(
r.status_code, 404, "Should not return an interim meeting as next meeting"
)
# create an IETF meeting after the interim - it should be found as "next"
ietf_meeting = MeetingFactory(
type_id="ietf", date=date_today() + datetime.timedelta(days=30)
)
SessionFactory(meeting=ietf_meeting, name="Session at IETF meeting")
r = self.client.get(urlreverse("ietf.meeting.views.agenda_json", kwargs={}))
self.assertContains(r, "Session at IETF meeting", status_code=200)
@override_settings(PROCEEDINGS_V1_BASE_URL='https://example.com/{meeting.number}')
def test_agenda_redirects_for_old_meetings(self):
"""Meetings before 64 should be forwarded to their proceedings"""
# meeting with record but no schedule
MeetingFactory(type_id='ietf', number='35', populate_schedule=False)
r = self.client.get(
urlreverse(
'agenda',
kwargs={'num': '35', 'ext': '.html'},
))
self.assertRedirects(r, 'https://example.com/35', fetch_redirect_response=False)
# meeting with record and schedule but no assignments
meeting_with_schedule = MeetingFactory(type_id='ietf', number='36', populate_schedule=True)
r = self.client.get(
urlreverse(
'agenda',
kwargs={'num': '36', 'ext': '.html'},
))
self.assertRedirects(r, 'https://example.com/36', fetch_redirect_response=False)
# meeting with an assignment
SessionFactory(meeting=meeting_with_schedule)
r = self.client.get(
urlreverse(
'agenda',
kwargs={'num': '36', 'ext': '.html'},
))
self.assertRedirects(r, 'https://example.com/36', fetch_redirect_response=False)
def test_agenda_for_nonexistent_meeting(self):
"""Return a 404 for a bad IETF meeting number"""
# Meetings pre-64 are redirected, but should be a 404 if there is no Meeting instance
r = self.client.get(
urlreverse(
'agenda',
kwargs={'num': '32', 'ext': '.html'},
))
self.assertEqual(r.status_code, 404)
@override_settings(MEETING_MATERIALS_SERVE_LOCALLY=False, MEETING_DOC_HREFS = settings.MEETING_DOC_CDN_HREFS)
def test_materials_through_cdn(self):
meeting = make_meeting_test_data(create_interims=True)
session107 = SessionFactory(meeting__number='172',group__acronym='mars')
doc = DocumentFactory.create(name='agenda-172-mars', type_id='agenda', title="Agenda",
uploaded_filename="agenda-172-mars.txt", group=session107.group, rev='00', states=[('agenda','active')])
pres = SessionPresentation.objects.create(session=session107,document=doc,rev=doc.rev)
session107.presentations.add(pres) #
doc = DocumentFactory.create(name='minutes-172-mars', type_id='minutes', title="Minutes",
uploaded_filename="minutes-172-mars.md", group=session107.group, rev='00', states=[('minutes','active')])
pres = SessionPresentation.objects.create(session=session107,document=doc,rev=doc.rev)
session107.presentations.add(pres)
doc = DocumentFactory.create(name='slides-172-mars-1-active', type_id='slides', title="Slideshow",
uploaded_filename="slides-172-mars.txt", group=session107.group, rev='00',
states=[('slides','active'), ('reuse_policy', 'single')])
pres = SessionPresentation.objects.create(session=session107,document=doc,rev=doc.rev)
session107.presentations.add(pres)
for session in (
Session.objects.filter(meeting=meeting, group__acronym="mars").first(),
session107,
Session.objects.filter(meeting__type_id='interim', group__acronym='mars', schedulingevent__status='sched').first(),
):
self.write_materials_files(session.meeting, session)
for document in (session.agenda(),session.minutes(),session.slides()[0]):
url = urlreverse("ietf.meeting.views.materials_document",
kwargs=dict(num=session.meeting.number, document=document))
r = self.client.get(url)
if session.meeting.number.isdigit() and int(session.meeting.number)<=96:
self.assertEqual(r.status_code,200)
else:
self.assertEqual(r.status_code,302)
self.assertEqual(r['Location'],document.get_href())
self.assertNotEqual(urlsplit(r['Location'])[2],url)
def test_materials(self):
meeting = make_meeting_test_data()
session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
self.do_test_materials(meeting, session)
def test_interim_materials(self):
make_meeting_test_data()
group = Group.objects.get(acronym='mars')
date = timezone.now() - datetime.timedelta(days=10)
meeting = make_interim_meeting(group=group, date=date, status='sched')
session = meeting.session_set.first()
self.do_test_materials(meeting, session)
def test_named_session(self):
"""Session with a name should appear separately in the materials"""
meeting = MeetingFactory(type_id='ietf', number='100')
meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20))
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', '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(
session=plain_session,
document__type_id=doc_type_id,
document__uploaded_filename=f'upload-{doc_type_id}-plain',
document__external_url=f'external_url-{doc_type_id}-plain',
)
SessionPresentationFactory(
session=named_session,
document__type_id=doc_type_id,
document__uploaded_filename=f'upload-{doc_type_id}-named',
document__external_url=f'external_url-{doc_type_id}-named',
)
url = urlreverse('ietf.meeting.views.materials', kwargs={'num': meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
plain_label = q(f'div#{group.acronym}')
self.assertEqual(plain_label.text(), group.acronym)
plain_row = plain_label.closest('tr')
self.assertTrue(plain_row)
named_label = q(f'div#{slugify(named_session.name)}')
self.assertEqual(named_label.text(), named_session.name)
named_row = named_label.closest('tr')
self.assertTrue(named_row)
for material in (sp.document for sp in plain_session.presentations.all()):
if material.type_id == 'draft':
expected_url = urlreverse(
'ietf.doc.views_doc.document_main',
kwargs={'name': material.name},
)
else:
expected_url = material.get_href(meeting)
self.assertTrue(plain_row.find(f'a[href="{expected_url}"]'))
self.assertFalse(named_row.find(f'a[href="{expected_url}"]'))
for material in (sp.document for sp in named_session.presentations.all()):
if material.type_id == 'draft':
expected_url = urlreverse(
'ietf.doc.views_doc.document_main',
kwargs={'name': material.name},
)
else:
expected_url = material.get_href(meeting)
self.assertFalse(plain_row.find(f'a[href="{expected_url}"]'))
self.assertTrue(named_row.find(f'a[href="{expected_url}"]'))
@override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True)
def test_meeting_materials_non_utf8(self):
meeting = make_meeting_test_data()
session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
doc = session.materials.get(type="minutes")
self.write_materials_file(meeting,
doc,
"1. More work items underway\n\n2. The draft will be finished before next meeting\n\n - É",
charset="iso-8859-1")
url = urlreverse("ietf.meeting.views.materials_document",
kwargs=dict(num=meeting.number, document=session.minutes()))
for accept, cont_type, content in [
('text/html,text/plain,text/markdown', 'text/html', '<li>\n<p>More work items underway</p>\n</li>'),
('text/markdown,text/html,text/plain', 'text/markdown', '1. More work items underway'),
('text/plain,text/markdown, text/html', 'text/plain', '1. More work items underway'),
('text/html', 'text/html', '<li>\n<p>More work items underway</p>\n</li>'),
('text/markdown', 'text/markdown', '1. More work items underway'),
('text/plain', 'text/plain', '1. More work items underway'),
]:
client = Client(HTTP_ACCEPT=accept)
r = client.get(url)
rtype = r['Content-Type'].split(';')[0]
self.assertEqual(cont_type, rtype)
self.assertContains(r, content)
@override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True)
def do_test_materials(self, meeting, session):
self.write_materials_files(meeting, session)
# session agenda
document = session.agenda()
url = urlreverse("ietf.meeting.views.materials_document",
kwargs=dict(num=meeting.number, document=document))
r = self.client.get(url)
if r.status_code != 200:
q = PyQuery(r.content)
debug.show('q(".alert").text()')
self.assertContains(r, "1. WG status")
# session minutes
url = urlreverse("ietf.meeting.views.materials_document",
kwargs=dict(num=meeting.number, document=session.minutes()))
r = self.client.get(url)
self.assertContains(r, "1. More work items underway")
cont_disp = r.headers.get('content-disposition', ('Content-Disposition', ''))[1]
cont_disp = re.split('; ?', cont_disp)
cont_disp_settings = dict( e.split('=', 1) for e in cont_disp if '=' in e )
filename = cont_disp_settings.get('filename', '').strip('"')
if filename.endswith('.md'):
for accept, cont_type, content in [
('text/html,text/plain,text/markdown', 'text/html', '<li>\n<p>More work items underway</p>\n</li>'),
('text/markdown,text/html,text/plain', 'text/markdown', '1. More work items underway'),
('text/plain,text/markdown, text/html', 'text/plain', '1. More work items underway'),
('text/html', 'text/html', '<li>\n<p>More work items underway</p>\n</li>'),
('text/markdown', 'text/markdown', '1. More work items underway'),
('text/plain', 'text/plain', '1. More work items underway'),
]:
client = Client(HTTP_ACCEPT=accept)
r = client.get(url)
rtype = r['Content-Type'].split(';')[0]
self.assertEqual(cont_type, rtype)
self.assertContains(r, content)
# test with explicit meeting number in url
if meeting.number.isdigit():
url = urlreverse("ietf.meeting.views.materials", kwargs=dict(num=meeting.number))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
row = q('#content #%s' % str(session.group.acronym)).closest("tr")
self.assertTrue(row.find('a:contains("Agenda")'))
self.assertTrue(row.find('a:contains("Minutes")'))
self.assertTrue(row.find('a:contains("Slideshow")'))
self.assertFalse(row.find("a:contains(\"Bad Slideshow\")"))
# test with no meeting number in url
# Add various group sessions
groups = []
parent_groups = [
GroupFactory.create(type_id="area", acronym="gen"),
GroupFactory.create(acronym="iab"),
GroupFactory.create(acronym="irtf"),
]
for parent in parent_groups:
groups.append(GroupFactory.create(parent=parent))
for acronym in ["rsab", "edu"]:
groups.append(GroupFactory.create(acronym=acronym))
for group in groups:
SessionFactory(meeting=meeting, group=group)
self.write_materials_files(meeting, session)
url = urlreverse("ietf.meeting.views.materials", kwargs=dict())
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
row = q('#content #%s' % str(session.group.acronym)).closest("tr")
self.assertTrue(row.find('a:contains("Agenda")'))
self.assertTrue(row.find('a:contains("Minutes")'))
self.assertTrue(row.find('a:contains("Slideshow")'))
self.assertFalse(row.find("a:contains(\"Bad Slideshow\")"))
# test for different sections
sections = ["plenaries", "gen", "iab", "editorial", "irtf", "training"]
for section in sections:
self.assertEqual(len(q(f"#{section}")), 1, f"{section} section should exists in proceedings")
# test with a loggged-in wg chair
self.client.login(username="marschairman", password="marschairman+password")
url = urlreverse("ietf.meeting.views.materials", kwargs=dict(num=meeting.number))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
row = q('#content #%s' % str(session.group.acronym)).closest("tr")
self.assertTrue(row.find('a:contains("Agenda")'))
self.assertTrue(row.find('a:contains("Minutes")'))
self.assertTrue(row.find('a:contains("Slideshow")'))
self.assertFalse(row.find("a:contains(\"Bad Slideshow\")"))
self.assertTrue(row.find('a:contains("Edit materials")'))
# FIXME: missing tests of .pdf/.tar generation (some code can
# probably be lifted from similar tests in iesg/tests.py)
# document-specific urls
for doc in session.materials.exclude(states__slug='deleted'):
url = urlreverse('ietf.meeting.views.materials_document', kwargs=dict(num=meeting.number, document=doc.name))
r = self.client.get(url)
self.assertEqual(unicontent(r), doc.text())
def test_materials_has_edit_links(self):
meeting = make_meeting_test_data()
url = urlreverse("ietf.meeting.views.materials", kwargs=dict(num=meeting.number))
r = self.client.get(url)
self.assertNotContains(r, 'Edit materials', status_code=200)
# mars chairman can edit materials for mars group
self.client.login(username='marschairman', password='marschairman+password')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content.decode())
edit_url = urlreverse(
'ietf.meeting.views.session_details',
kwargs={'num': meeting.number, 'acronym': 'mars'},
)
self.assertEqual(len(q(f'a[href^="{edit_url}"]')), 1, 'Link to mars session_details for mars chairman')
for acro in ['ietf', 'ames']: # other groups with materials
edit_url = urlreverse(
'ietf.meeting.views.session_details',
kwargs={'num': meeting.number, 'acronym': acro},
)
self.assertEqual(len(q(f'a[href^="{edit_url}"]')), 0, f'No link to {acro} session_details for mars chairman')
# secretary can edit all groups
self.client.login(username='secretary', password='secretary+password')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content.decode())
for acro in ['mars', 'ames']: # wgs
edit_url = urlreverse(
'ietf.meeting.views.session_details',
kwargs={'num': meeting.number, 'acronym': acro},
)
self.assertEqual(len(q(f'a[href^="{edit_url}"]')), 1, f'Link to session_details page for {acro}')
# The IETF Plenary has a "#sessionX" tacked on to the edit url to differentiate from other sessions,
# so test it separately. Not bothering to check the exact session pk in detail.
edit_url = urlreverse(
'ietf.meeting.views.session_details',
kwargs={'num': meeting.number, 'acronym': 'ietf'},
)
self.assertEqual(len(q(f'a[href^="{edit_url}#session"]')), 1, f'Link to session_details page for {acro}')
def test_materials_document_extension_choice(self):
def _url(**kwargs):
return urlreverse("ietf.meeting.views.materials_document", kwargs=kwargs)
presentation = SessionPresentationFactory(
document__rev="00",
document__name="slides-whatever",
document__uploaded_filename="slides-whatever-00.txt",
document__type_id="slides",
document__states=(("reuse_policy", "single"),)
)
session = presentation.session
meeting = session.meeting
# This is not a realistic set of files to exist, but is useful for testing. Normally,
# we'd have _either_ txt, pdf, or pptx + pdf.
self.write_materials_file(meeting, presentation.document, "Hi I'm a txt", with_ext=".txt")
self.write_materials_file(meeting, presentation.document, "Hi I'm a pptx", with_ext=".pptx")
# with no rev, prefers the uploaded_filename
r = self.client.get(_url(document="slides-whatever", num=meeting.number)) # no rev
self.assertEqual(r.status_code, 200)
self.assertEqual(r.content.decode(), "Hi I'm a txt")
# with a rev, prefers pptx because it comes first alphabetically
r = self.client.get(_url(document="slides-whatever-00", num=meeting.number))
self.assertEqual(r.status_code, 200)
self.assertEqual(r.content.decode(), "Hi I'm a pptx")
# now create a pdf
self.write_materials_file(meeting, presentation.document, "Hi I'm a pdf", with_ext=".pdf")
# with no rev, still prefers uploaded_filename
r = self.client.get(_url(document="slides-whatever", num=meeting.number)) # no rev
self.assertEqual(r.status_code, 200)
self.assertEqual(r.content.decode(), "Hi I'm a txt")
# pdf should be preferred with a rev
r = self.client.get(_url(document="slides-whatever-00", num=meeting.number))
self.assertEqual(r.status_code, 200)
self.assertEqual(r.content.decode(), "Hi I'm a pdf")
# and explicit extensions should, of course, be respected
for ext in ["pdf", "pptx", "txt"]:
r = self.client.get(_url(document="slides-whatever-00", num=meeting.number, ext=f".{ext}"))
self.assertEqual(r.status_code, 200)
self.assertEqual(r.content.decode(), f"Hi I'm a {ext}")
# and 404 should come up if the ext is not found
r = self.client.get(_url(document="slides-whatever-00", num=meeting.number, ext=".docx"))
self.assertEqual(r.status_code, 404)
def test_materials_editable_groups(self):
meeting = make_meeting_test_data()
self.client.login(username="marschairman", password="marschairman+password")
r = self.client.get(urlreverse("ietf.meeting.views.materials_editable_groups", kwargs={'num':meeting.number}))
self.assertContains(r, meeting.number)
self.assertContains(r, "mars")
self.assertNotContains(r, "No session requested")
self.client.login(username="ad", password="ad+password")
r = self.client.get(urlreverse("ietf.meeting.views.materials_editable_groups", kwargs={'num':meeting.number}))
self.assertContains(r, meeting.number)
self.assertContains(r, "frfarea")
self.assertContains(r, "No session requested")
self.client.login(username="plain",password="plain+password")
r = self.client.get(urlreverse("ietf.meeting.views.materials_editable_groups", kwargs={'num':meeting.number}))
self.assertContains(r, meeting.number)
self.assertContains(r, "You cannot manage the meeting materials for any groups")
@override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True)
def test_materials_name_endswith_hyphen_number_number(self):
# be sure a shadowed filename without the hyphen does not interfere
shadow = SessionPresentationFactory(
document__name="slides-115-junk",
document__type_id="slides",
document__states=[("reuse_policy", "single")],
)
shadow.document.uploaded_filename = (
f"{shadow.document.name}-{shadow.document.rev}.pdf"
)
shadow.document.save()
# create the material we want to find for the test
sp = SessionPresentationFactory(
document__name="slides-115-junk-15",
document__type_id="slides",
document__states=[("reuse_policy", "single")],
)
sp.document.uploaded_filename = f"{sp.document.name}-{sp.document.rev}.pdf"
sp.document.save()
self.write_materials_file(
sp.session.meeting, sp.document, "Fake slide contents rev 00"
)
# create rev 01
sp.document.rev = "01"
sp.document.uploaded_filename = f"{sp.document.name}-{sp.document.rev}.pdf"
sp.document.save_with_history(
[
NewRevisionDocEvent.objects.create(
type="new_revision",
doc=sp.document,
rev=sp.document.rev,
by=Person.objects.get(name="(System)"),
desc=f"New version available: <b>{sp.document.name}-{sp.document.rev}.txt</b>",
)
]
)
self.write_materials_file(
sp.session.meeting, sp.document, "Fake slide contents rev 01"
)
url = urlreverse(
"ietf.meeting.views.materials_document",
kwargs=dict(document=sp.document.name, num=sp.session.meeting.number),
)
r = self.client.get(url)
self.assertContains(
r,
"Fake slide contents rev 01",
status_code=200,
msg_prefix="Should return latest rev by default",
)
url = urlreverse(
"ietf.meeting.views.materials_document",
kwargs=dict(document=sp.document.name + "-00", num=sp.session.meeting.number),
)
r = self.client.get(url)
self.assertContains(
r,
"Fake slide contents rev 00",
status_code=200,
msg_prefix="Should return existing version on request",
)
url = urlreverse(
"ietf.meeting.views.materials_document",
kwargs=dict(document=sp.document.name + "-02", num=sp.session.meeting.number),
)
r = self.client.get(url)
self.assertEqual(r.status_code, 404, "Should not find nonexistent version")
def test_important_dates(self):
meeting=MeetingFactory(type_id='ietf')
meeting.show_important_dates = True
meeting.save()
populate_important_dates(meeting)
url = urlreverse('ietf.meeting.views.important_dates',kwargs={'num':meeting.number})
r = self.client.get(url)
self.assertContains(r, str(meeting.importantdate_set.first().date))
idn = ImportantDateName.objects.filter(used=True).first()
pre_date = meeting.importantdate_set.get(name=idn).date
idn.default_offset_days -= 1
idn.save()
update_important_dates(meeting)
post_date = meeting.importantdate_set.get(name=idn).date
self.assertEqual(pre_date, post_date+datetime.timedelta(days=1))
def test_important_dates_ical(self):
meeting = MeetingFactory(type_id='ietf')
meeting.show_important_dates = True
meeting.save()
populate_important_dates(meeting)
url = urlreverse('ietf.meeting.views.important_dates', kwargs={'num': meeting.number, 'output_format': 'ics'})
r = self.client.get(url)
for d in meeting.importantdate_set.all():
self.assertContains(r, d.date.isoformat())
updated = meeting.updated()
self.assertIsNotNone(updated)
expected_updated = updated.astimezone(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
self.assertContains(r, f"DTSTAMP:{expected_updated}")
dtstamps_count = r.content.decode("utf-8").count(f"DTSTAMP:{expected_updated}")
self.assertEqual(dtstamps_count, meeting.importantdate_set.count())
# With default cached_updated, 1970-01-01
with patch("ietf.meeting.models.Meeting.updated", return_value=None):
r = self.client.get(url)
for d in meeting.importantdate_set.all():
self.assertContains(r, d.date.isoformat())
expected_updated = "19700101T000000Z"
self.assertContains(r, f"DTSTAMP:{expected_updated}")
dtstamps_count = r.content.decode("utf-8").count(f"DTSTAMP:{expected_updated}")
self.assertEqual(dtstamps_count, meeting.importantdate_set.count())
def test_group_ical(self):
meeting = make_meeting_test_data()
s1 = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
a1 = s1.official_timeslotassignment()
t1 = a1.timeslot
# Create an extra session
t2 = TimeSlotFactory.create(
meeting=meeting,
time=meeting.tz().localize(
datetime.datetime.combine(meeting.date, datetime.time(11, 30))
)
)
s2 = SessionFactory.create(meeting=meeting, group=s1.group, add_to_schedule=False)
SchedTimeSessAssignment.objects.create(timeslot=t2, session=s2, schedule=meeting.schedule)
#
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'acronym':s1.group.acronym, })
r = self.client.get(url)
assert_ical_response_is_valid(self,
r,
expected_event_summaries=['mars - Martian Special Interest Group'],
expected_event_count=2)
self.assertContains(r, t1.local_start_time().strftime('%Y%m%dT%H%M%S'))
self.assertContains(r, t2.local_start_time().strftime('%Y%m%dT%H%M%S'))
#
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'session_id':s1.id, })
r = self.client.get(url)
assert_ical_response_is_valid(self, r,
expected_event_summaries=['mars - Martian Special Interest Group'],
expected_event_count=1)
self.assertContains(r, t1.local_start_time().strftime('%Y%m%dT%H%M%S'))
self.assertNotContains(r, t2.local_start_time().strftime('%Y%m%dT%H%M%S'))
def test_parse_agenda_filter_params(self):
def _r(show=(), hide=(), showtypes=(), hidetypes=()):
"""Helper to create expected result dict"""
return dict(show=set(show), hide=set(hide), showtypes=set(showtypes), hidetypes=set(hidetypes))
self.assertIsNone(parse_agenda_filter_params(QueryDict('')))
# test valid combos (not exhaustive)
for qstr, expected in (
('show=', _r()), ('hide=', _r()), ('showtypes=', _r()), ('hidetypes=', _r()),
('show=x', _r(show=['x'])), ('hide=x', _r(hide=['x'])),
('showtypes=x', _r(showtypes=['x'])), ('hidetypes=x', _r(hidetypes=['x'])),
('show=x,y,z', _r(show=['x','y','z'])),
('hide=x,y,z', _r(hide=['x','y','z'])),
('showtypes=x,y,z', _r(showtypes=['x','y','z'])),
('hidetypes=x,y,z', _r(hidetypes=['x','y','z'])),
('show=a&hide=a', _r(show=['a'], hide=['a'])),
('show=a&hide=b', _r(show=['a'], hide=['b'])),
('show=a&hide=b&showtypes=c&hidetypes=d', _r(show=['a'], hide=['b'], showtypes=['c'], hidetypes=['d'])),
):
self.assertEqual(
parse_agenda_filter_params(QueryDict(qstr)),
expected,
'Parsed "%s" incorrectly' % qstr,
)
def do_ical_filter_test(self, meeting, querystring, expected_session_summaries):
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number})
r = self.client.get(url + querystring)
self.assertEqual(r.status_code, 200)
assert_ical_response_is_valid(self,
r,
expected_event_summaries=expected_session_summaries,
expected_event_count=len(expected_session_summaries))
def test_ical_filter(self):
# Just a quick check of functionality - permutations tested via tests_js.AgendaTests
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='',
expected_session_summaries=[
'Morning Break',
'Registration',
'IETF Plenary',
'ames - Asteroid Mining Equipment Standardization Group',
'mars - Martian Special Interest Group',
]
)
self.do_ical_filter_test(
meeting,
querystring='?show=plenary,secretariat,ames&hide=admin',
expected_session_summaries=[
'Morning Break',
'IETF Plenary',
'ames - Asteroid Mining Equipment Standardization Group',
]
)
def build_session_setup(self):
# This setup is intentionally unusual - the session has one draft attached as a session presentation,
# but lists a different on in its agenda. The expectation is that the pdf and tgz views will return both.
session = SessionFactory(group__type_id='wg',meeting__type_id='ietf')
draft1 = WgDraftFactory(group=session.group)
session.presentations.create(document=draft1)
draft2 = WgDraftFactory(group=session.group)
agenda = DocumentFactory(type_id='agenda',group=session.group, uploaded_filename='agenda-%s-%s' % (session.meeting.number,session.group.acronym), states=[('agenda','active')])
session.presentations.create(document=agenda)
self.write_materials_file(session.meeting, session.materials.get(type="agenda"),
"1. WG status (15 minutes)\n\n2. Status of %s\n\n" % draft2.name)
filenames = []
for d in (draft1, draft2):
file,_ = submission_file(name_in_doc=f'{d.name}-00',name_in_post=f'{d.name}-00.txt',templatename='test_submission.txt',group=session.group)
filename = os.path.join(d.get_file_path(),file.name)
with io.open(filename,'w') as draftbits:
draftbits.write(file.getvalue())
filenames.append(filename)
self.assertEqual( len(session_draft_list(session.meeting.number,session.group.acronym)), 2)
return (session, filenames)
def test_session_draft_tarfile(self):
session, filenames = self.build_session_setup()
try:
url = urlreverse('ietf.meeting.views.session_draft_tarfile', kwargs={'num':session.meeting.number,'acronym':session.group.acronym})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.get('Content-Type'), 'application/octet-stream')
finally:
for filename in filenames:
os.unlink(filename)
@skipIf(skip_pdf_tests, skip_message)
@skip_coverage
def test_session_draft_pdf(self):
session, filenames = self.build_session_setup()
try:
url = urlreverse('ietf.meeting.views.session_draft_pdf', kwargs={'num':session.meeting.number,'acronym':session.group.acronym})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.get('Content-Type'), 'application/pdf')
finally:
for filename in filenames:
os.unlink(filename)
def test_current_materials(self):
url = urlreverse('ietf.meeting.views.current_materials')
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
MeetingFactory(type_id='ietf', date=date_today())
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
def test_edit_schedule_properties(self):
self.client.login(username='secretary',password='secretary+password')
url = urlreverse('ietf.meeting.views.edit_schedule_properties',kwargs={'owner':'does@notexist.example','name':'doesnotexist','num':00})
response = self.client.get(url)
self.assertEqual(response.status_code,404)
self.client.logout()
schedule = ScheduleFactory(meeting__type_id='ietf',visible=False,public=False)
url = urlreverse('ietf.meeting.views.edit_schedule_properties',kwargs={'owner':schedule.owner.email(),'name':schedule.name,'num':schedule.meeting.number})
response = self.client.get(url)
self.assertEqual(response.status_code,302)
self.client.login(username='secretary',password='secretary+password')
response = self.client.get(url)
self.assertEqual(response.status_code,200)
new_base = Schedule.objects.create(name="newbase", owner=schedule.owner, meeting=schedule.meeting)
response = self.client.post(url, {
'name': 'some-other-name',
'visible':True,
'public':True,
'notes': "New Notes",
'base': new_base.pk,
}
)
self.assertNoFormPostErrors(response)
self.assertRedirects(
response,
urlreverse(
'ietf.meeting.views.edit_meeting_schedule',
kwargs={'num': schedule.meeting.number, 'owner': schedule.owner.email(), 'name': 'some-other-name'}
),
)
schedule.refresh_from_db()
self.assertTrue(schedule.visible)
self.assertTrue(schedule.public)
self.assertEqual(schedule.notes, "New Notes")
self.assertEqual(schedule.base_id, new_base.pk)
self.assertEqual(schedule.name, 'some-other-name')
def test_agenda_by_type_ics(self):
session=SessionFactory(meeting__type_id='ietf',type_id='lead')
url = urlreverse('ietf.meeting.views.agenda_by_type_ics',kwargs={'num':session.meeting.number,'type':'lead'})
login_testing_unauthorized(self,"secretary",url)
response = self.client.get(url)
self.assertEqual(response.status_code,200)
self.assertEqual(response.get('Content-Type'), 'text/calendar')
def test_cancelled_ics(self):
session=SessionFactory(meeting__type_id='ietf',status_id='canceled')
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs=dict(num=session.meeting.number))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertIn('STATUS:CANCELLED',unicontent(r))
self.assertNotIn('STATUS:CONFIRMED',unicontent(r))
def test_session_materials(self):
meeting = make_meeting_test_data()
session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
url = urlreverse('ietf.meeting.views.session_materials', kwargs=dict(session_id=session.pk))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
agenda_div = q('div.agenda-frame')
self.assertIsNotNone(agenda_div)
self.assertEqual(agenda_div.attr('data-src'), session.agenda().get_href())
minutes_div = q('div.minutes-frame')
self.assertIsNotNone(minutes_div)
self.assertEqual(minutes_div.attr('data-src'), session.minutes().get_href())
# Make sure undeleted slides are present and deleted slides are not
not_deleted_slides = session.materials.filter(
type='slides'
).exclude(
states__type__slug='slides',states__slug='deleted'
)
self.assertGreater(not_deleted_slides.count(), 0) # make sure this isn't a pointless test
deleted_slides = session.materials.filter(
type='slides', states__type__slug='slides', states__slug='deleted'
)
self.assertGreater(deleted_slides.count(), 0) # make sure this isn't a pointless test
# live slides should be found
for slide in not_deleted_slides:
self.assertTrue(q('ul li a:contains("%s")' % slide.title))
# deleted slides should not be found
for slide in deleted_slides:
self.assertFalse(q('ul li a:contains("%s")' % slide.title))
def test_meetinghost_logo(self):
host = MeetingHostFactory()
url = urlreverse('ietf.meeting.views_proceedings.meetinghost_logo',kwargs=dict(host_id=host.pk,num=host.meeting.number))
r = self.client.get(url)
self.assertIs(type(r),FileResponse)
@override_settings(MEETING_SESSION_LOCK_TIME=datetime.timedelta(minutes=10))
class EditMeetingScheduleTests(TestCase):
"""Tests of the meeting editor view
This has tests in tests_js.py as well.
"""
def test_room_grouping(self):
"""Blocks of rooms in the editor should have identical timeslots"""
# set up a meeting, but we'll construct our own timeslots/rooms
meeting = MeetingFactory(type_id='ietf', populate_schedule=False)
sched = ScheduleFactory(meeting=meeting)
# Make groups of rooms with timeslots identical within a group, distinct between groups
times = [
[datetime.time(11,0), datetime.time(12,0), datetime.time(13,0)],
[datetime.time(11,0), datetime.time(12,0), datetime.time(13,0)], # same times, but durations will differ
[datetime.time(11,30), datetime.time(12, 0), datetime.time(13,0)], # different time
[datetime.time(12,0)], # different number of timeslots
]
durations = [
[30, 60, 90],
[60, 60, 90],
[30, 60, 90],
[60],
]
# check that times and durations are same-sized arrays
self.assertEqual(len(times), len(durations))
for time_row, duration_row in zip(times, durations):
self.assertEqual(len(time_row), len(duration_row))
# Create an array of room groups, each with rooms_per_group Rooms in it.
# Assign TimeSlots according to the times/durations above to each Room.
room_groups = []
rooms_in_group = 1 # will be incremented with each group
for time_row, duration_row in zip(times, durations):
room_groups.append(RoomFactory.create_batch(rooms_in_group, meeting=meeting))
rooms_in_group += 1 # put a different number of rooms in each group to help identify errors in grouping
for time, duration in zip(time_row, duration_row):
for room in room_groups[-1]:
TimeSlotFactory(
meeting=meeting,
location=room,
time=meeting.tz().localize(
datetime.datetime.combine(meeting.date, time)
),
duration=datetime.timedelta(minutes=duration),
)
# Now retrieve the edit meeting schedule page
url = urlreverse('ietf.meeting.views.edit_meeting_schedule',
kwargs=dict(num=meeting.number, owner=sched.owner.email(), name=sched.name))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
day_divs = q('div.day')
# There's only one day with TimeSlots. This means there will be two divs with class 'day':
# the first is the room label column, the second is the TimeSlot grid.
# Using eq() instead of [] gives us PyQuery objects instead of Elements
label_divs = day_divs.eq(0).find('div.room-group')
self.assertEqual(len(label_divs), len(room_groups))
room_group_divs = day_divs.eq(1).find('div.room-group')
self.assertEqual(len(room_group_divs), len(room_groups))
for rg, l_div, rg_div in zip(
room_groups,
label_divs.items(), # items() gives us PyQuery objects
room_group_divs.items(), # items() gives us PyQuery objects
):
# Check that room labels are correctly grouped
self.assertCountEqual(
[div.text() for div in l_div.find('div.room-name').items()],
[room.name for room in rg],
)
# And that the time labels are correct. Just check that the individual timeslot labels agree with
# the time-header above each room group.
time_header_labels = rg_div.find('div.time-header div.time-label').text()
timeslot_rows = rg_div.find('div.timeslots')
for row in timeslot_rows.items():
time_labels = row.find('div.time-label div:not(.past-flag)').text()
self.assertEqual(time_labels, time_header_labels)
def test_bof_session_tag(self):
"""Sessions for BOF groups should be marked as such"""
meeting = MeetingFactory(type_id='ietf')
non_bof_session = SessionFactory(meeting=meeting)
bof_session = SessionFactory(meeting=meeting, group__state_id='bof')
url = urlreverse('ietf.meeting.views.edit_meeting_schedule',
kwargs=dict(num=meeting.number))
self.client.login(username='secretary', password='secretary+password')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('#session{} .bof-tag'.format(non_bof_session.pk))), 0,
'Non-BOF session should not be tagged as a BOF session')
bof_tags = q('#session{} .bof-tag'.format(bof_session.pk))
self.assertEqual(len(bof_tags), 1,
'BOF session should have one BOF session tag')
self.assertIn('BOF', bof_tags.eq(0).text(),
'BOF tag should contain text "BOF"')
def _setup_for_swap_timeslots(self):
"""Create a meeting, rooms, and schedule for swap_timeslots testing
Creates two groups of rooms with disjoint timeslot sets, modeling the room grouping in
the edit_meeting_schedule view.
"""
# Meeting must be in the future so it can be edited
meeting = MeetingFactory(
type_id='ietf',
date=date_today() + datetime.timedelta(days=7),
populate_schedule=False,
)
meeting.schedule = ScheduleFactory(meeting=meeting)
meeting.save()
# Create room groups
room_groups = [
RoomFactory.create_batch(2, meeting=meeting),
RoomFactory.create_batch(2, meeting=meeting),
]
# Set up different sets of timeslots
# Work with t0 in UTC for arithmetic. This does not change the results but is cleaner if someone looks
# at intermediate results which may be misleading until passed through tz.normalize().
t0 = meeting.tz().localize(
datetime.datetime.combine(meeting.date, datetime.time(11, 0))
).astimezone(pytz.utc)
dur = datetime.timedelta(hours=2)
for room in room_groups[0]:
TimeSlotFactory(meeting=meeting, location=room, duration=dur, time=t0)
TimeSlotFactory(meeting=meeting, location=room, duration=dur, time=t0 + datetime.timedelta(days=1, hours=2))
TimeSlotFactory(meeting=meeting, location=room, duration=dur, time=t0 + datetime.timedelta(days=2, hours=4))
for room in room_groups[1]:
TimeSlotFactory(meeting=meeting, location=room, duration=dur, time=t0 + datetime.timedelta(hours=1))
TimeSlotFactory(meeting=meeting, location=room, duration=dur, time=t0 + datetime.timedelta(days=1, hours=3))
TimeSlotFactory(meeting=meeting, location=room, duration=dur, time=t0 + datetime.timedelta(days=2, hours=5))
# And now put sessions in the timeslots
for ts in meeting.timeslot_set.all():
SessionFactory(
meeting=meeting,
name=str(ts.pk), # label to identify where it started
add_to_schedule=False,
).timeslotassignments.create(
timeslot=ts,
schedule=meeting.schedule,
)
return meeting, room_groups
def test_swap_timeslots(self):
"""Schedule timeslot groups should swap properly
This tests the case currently exercised by the UI - where the rooms are grouped according to
entirely equivalent sets of timeslots. Thus, there is always a matching timeslot for every (or no)
room as long as the rooms parameter to the ajax call includes only one group.
"""
meeting, room_groups = self._setup_for_swap_timeslots()
url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number))
username = meeting.schedule.owner.user.username
self.client.login(username=username, password=username + '+password')
# Swap group 0's first and last sessions
r = self.client.post(
url,
dict(
action='swaptimeslots',
origin_timeslot=str(room_groups[0][0].timeslot_set.first().pk),
target_timeslot=str(room_groups[0][0].timeslot_set.last().pk),
rooms=','.join([str(room.pk) for room in room_groups[0]]),
)
)
self.assertEqual(r.status_code, 302)
# Validate results
for index, room in enumerate(room_groups[0]):
timeslots = list(room.timeslot_set.all())
self.assertEqual(timeslots[0].session.name, str(timeslots[-1].pk),
'Session from last timeslot in room (0, {}) should now be in first'.format(index))
self.assertEqual(timeslots[-1].session.name, str(timeslots[0].pk),
'Session from first timeslot in room (0, {}) should now be in last'.format(index))
self.assertEqual(
[ts.session.name for ts in timeslots[1:-1]],
[str(ts.pk) for ts in timeslots[1:-1]],
'Sessions in middle timeslots should be unchanged'
)
for index, room in enumerate(room_groups[1]):
timeslots = list(room.timeslot_set.all())
self.assertFalse(
any(ts.session is None for ts in timeslots),
"Sessions in other room group's timeslots should still be assigned"
)
self.assertEqual(
[ts.session.name for ts in timeslots],
[str(ts.pk) for ts in timeslots],
"Sessions in other room group's timeslots should be unchanged"
)
def test_swap_timeslots_denies_past(self):
"""Swapping past timeslots is not allowed for an official schedule"""
meeting, room_groups = self._setup_for_swap_timeslots()
# clone official schedule as an unofficial schedule
Schedule.objects.create(
name='unofficial',
owner=meeting.schedule.owner,
meeting=meeting,
base=meeting.schedule.base,
origin=meeting.schedule,
)
official_url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number))
unofficial_url = urlreverse('ietf.meeting.views.edit_meeting_schedule',
kwargs=dict(num=meeting.number,
owner=str(meeting.schedule.owner.email()),
name='unofficial'))
username = meeting.schedule.owner.user.username
self.client.login(username=username, password=username + '+password')
# Swap group 0's first and last sessions, first in the past
right_now = self._right_now_in(meeting.tz())
for room in room_groups[0]:
ts = room.timeslot_set.last()
ts.time = right_now - datetime.timedelta(minutes=5)
ts.save()
# timeslot_set is ordered by -time, so check that we know which is past/future
self.assertTrue(room_groups[0][0].timeslot_set.last().time < right_now)
self.assertTrue(room_groups[0][0].timeslot_set.first().time > right_now)
post_data = dict(
action='swaptimeslots',
origin_timeslot=str(room_groups[0][0].timeslot_set.first().pk),
target_timeslot=str(room_groups[0][0].timeslot_set.last().pk),
rooms=','.join([str(room.pk) for room in room_groups[0]]),
)
r = self.client.post(official_url, post_data)
self.assertContains(r, "Can't swap these timeslots.", status_code=400)
# same request should succeed for an unofficial schedule
r = self.client.post(unofficial_url, post_data)
self.assertEqual(r.status_code, 302)
# now with origin/target reversed
post_data = dict(
action='swaptimeslots',
origin_timeslot=str(room_groups[0][0].timeslot_set.last().pk),
target_timeslot=str(room_groups[0][0].timeslot_set.first().pk),
rooms=','.join([str(room.pk) for room in room_groups[0]]),
)
r = self.client.post(official_url, post_data)
self.assertContains(r, "Can't swap these timeslots.", status_code=400)
# same request should succeed for an unofficial schedule
r = self.client.post(unofficial_url, post_data)
self.assertEqual(r.status_code, 302)
# now with the "past" timeslot less than MEETING_SESSION_LOCK_TIME in the future
for room in room_groups[0]:
ts = room.timeslot_set.last()
ts.time = right_now + datetime.timedelta(minutes=9) # must be < MEETING_SESSION_LOCK_TIME
ts.save()
self.assertTrue(room_groups[0][0].timeslot_set.last().time < right_now + settings.MEETING_SESSION_LOCK_TIME)
self.assertTrue(room_groups[0][0].timeslot_set.first().time > right_now + settings.MEETING_SESSION_LOCK_TIME)
post_data = dict(
action='swaptimeslots',
origin_timeslot=str(room_groups[0][0].timeslot_set.first().pk),
target_timeslot=str(room_groups[0][0].timeslot_set.last().pk),
rooms=','.join([str(room.pk) for room in room_groups[0]]),
)
r = self.client.post(official_url, post_data)
self.assertContains(r, "Can't swap these timeslots.", status_code=400)
# now with both in the past
for room in room_groups[0]:
ts = room.timeslot_set.last()
ts.time = right_now - datetime.timedelta(minutes=5)
ts.save()
ts = room.timeslot_set.first()
ts.time = right_now - datetime.timedelta(hours=1)
ts.save()
past_slots = room_groups[0][0].timeslot_set.filter(time__lt=right_now)
self.assertEqual(len(past_slots), 2, 'Need two timeslots in the past!')
post_data = dict(
action='swaptimeslots',
origin_timeslot=str(past_slots[0].pk),
target_timeslot=str(past_slots[1].pk),
rooms=','.join([str(room.pk) for room in room_groups[0]]),
)
r = self.client.post(official_url, post_data)
self.assertContains(r, "Can't swap these timeslots.", status_code=400)
# same request should succeed for an unofficial schedule
r = self.client.post(unofficial_url, post_data)
self.assertEqual(r.status_code, 302)
def test_swap_timeslots_handles_unmatched(self):
"""Sessions in unmatched timeslots should be unassigned when swapped
This more generally tests the back end by exercising the situation where a timeslot in the
affected rooms does not have an equivalent timeslot target. This is not used by the UI as of
now (2021-06-22), but should function correctly.
"""
meeting, room_groups = self._setup_for_swap_timeslots()
# Remove a timeslot and session from only one room in group 0
ts_to_remove = room_groups[0][1].timeslot_set.last()
ts_to_remove.session.delete()
ts_to_remove.delete() # our object still exists but has no db object
# Add a matching timeslot to group 1 so we can be sure it's being ignored.
# If not, this session will be unassigned when we swap timeslots on group 0.
new_ts = TimeSlotFactory(
meeting=meeting,
location=room_groups[1][0],
duration=ts_to_remove.duration,
time=ts_to_remove.time,
)
SessionFactory(
meeting=meeting,
name=str(new_ts.pk),
add_to_schedule=False,
).timeslotassignments.create(
timeslot=new_ts,
schedule=meeting.schedule,
)
url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number))
username = meeting.schedule.owner.user.username
self.client.login(username=username, password=username + '+password')
# Now swap between first and last timeslots in group 0
r = self.client.post(
url,
dict(
action='swaptimeslots',
origin_timeslot=str(room_groups[0][0].timeslot_set.first().pk),
target_timeslot=str(room_groups[0][0].timeslot_set.last().pk),
rooms=','.join([str(room.pk) for room in room_groups[0]]),
)
)
self.assertEqual(r.status_code, 302)
# Validate results
for index, room in enumerate(room_groups[0]):
timeslots = list(room.timeslot_set.all())
if index == 1:
# special case - this has no matching timeslot because we deleted it above
self.assertIsNone(timeslots[0].session, 'Unmatched timeslot should be empty after swap')
session_that_should_be_unassigned = Session.objects.get(name=str(timeslots[0].pk))
self.assertEqual(session_that_should_be_unassigned.timeslotassignments.count(), 0,
'Session that was in an unmatched timeslot should now be unassigned')
# check from 2nd timeslot to the last since we deleted the original last timeslot
self.assertEqual(
[ts.session.name for ts in timeslots[1:]],
[str(ts.pk) for ts in timeslots[1:]],
'Sessions in middle timeslots should be unchanged'
)
else:
self.assertEqual(timeslots[0].session.name, str(timeslots[-1].pk),
'Session from last timeslot in room (0, {}) should now be in first'.format(index))
self.assertEqual(timeslots[-1].session.name, str(timeslots[0].pk),
'Session from first timeslot in room (0, {}) should now be in last'.format(index))
self.assertEqual(
[ts.session.name for ts in timeslots[1:-1]],
[str(ts.pk) for ts in timeslots[1:-1]],
'Sessions in middle timeslots should be unchanged'
)
# Still should have no effect on other rooms, even if they matched a timeslot
for index, room in enumerate(room_groups[1]):
timeslots = list(room.timeslot_set.all())
self.assertFalse(
any(ts.session is None for ts in timeslots),
"Sessions in other room group's timeslots should still be assigned"
)
self.assertEqual(
[ts.session.name for ts in timeslots],
[str(ts.pk) for ts in timeslots],
"Sessions in other room group's timeslots should be unchanged"
)
def test_swap_days_denies_past(self):
"""Swapping past days is not allowed for an official schedule"""
meeting, room_groups = self._setup_for_swap_timeslots()
# clone official schedule as an unofficial schedule
Schedule.objects.create(
name='unofficial',
owner=meeting.schedule.owner,
meeting=meeting,
base=meeting.schedule.base,
origin=meeting.schedule,
)
official_url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number))
unofficial_url = urlreverse('ietf.meeting.views.edit_meeting_schedule',
kwargs=dict(num=meeting.number,
owner=str(meeting.schedule.owner.email()),
name='unofficial'))
username = meeting.schedule.owner.user.username
self.client.login(username=username, password=username + '+password')
# Swap group 0's first and last sessions, first in the past
right_now = self._right_now_in(meeting.tz())
yesterday = right_now.date() - datetime.timedelta(days=1)
day_before = right_now.date() - datetime.timedelta(days=2)
for room in room_groups[0]:
ts = room.timeslot_set.last()
# Calculation keeps local clock time, shifted to a different day.
ts.time = meeting.tz().localize(
datetime.datetime.combine(
yesterday,
ts.time.astimezone(meeting.tz()).time()
),
)
ts.save()
# timeslot_set is ordered by -time, so check that we know which is past/future
self.assertTrue(room_groups[0][0].timeslot_set.last().time < right_now)
self.assertTrue(room_groups[0][0].timeslot_set.first().time > right_now)
post_data = dict(
action='swapdays',
source_day=yesterday.isoformat(),
target_day=room_groups[0][0].timeslot_set.first().time.date().isoformat(),
)
r = self.client.post(official_url, post_data)
self.assertContains(r, "Can't swap these days.", status_code=400)
# same request should succeed for an unofficial schedule
r = self.client.post(unofficial_url, post_data)
self.assertEqual(r.status_code, 302)
# now with origin/target reversed
post_data = dict(
action='swapdays',
source_day=room_groups[0][0].timeslot_set.first().time.date().isoformat(),
target_day=yesterday.isoformat(),
rooms=','.join([str(room.pk) for room in room_groups[0]]),
)
r = self.client.post(official_url, post_data)
self.assertContains(r, "Can't swap these days.", status_code=400)
# same request should succeed for an unofficial schedule
r = self.client.post(unofficial_url, post_data)
self.assertEqual(r.status_code, 302)
# now with both in the past
for room in room_groups[0]:
ts = room.timeslot_set.first()
ts.time = meeting.tz().localize(
datetime.datetime.combine(
day_before,
ts.time.astimezone(meeting.tz()).time(),
)
)
ts.save()
past_slots = room_groups[0][0].timeslot_set.filter(time__lt=right_now)
self.assertEqual(len(past_slots), 2, 'Need two timeslots in the past!')
post_data = dict(
action='swapdays',
source_day=yesterday.isoformat(),
target_day=day_before.isoformat(),
)
r = self.client.post(official_url, post_data)
self.assertContains(r, "Can't swap these days.", status_code=400)
# same request should succeed for an unofficial schedule
r = self.client.post(unofficial_url, post_data)
self.assertEqual(r.status_code, 302)
def _decode_json_response(self, r):
try:
return json.loads(r.content.decode())
except json.JSONDecodeError as err:
self.fail('Response was not valid JSON: {}'.format(err))
@staticmethod
def _right_now_in(tzinfo):
right_now = timezone.now().astimezone(tzinfo)
return right_now
def test_assign_session(self):
"""Allow assignment to future timeslots only for official schedule"""
meeting = MeetingFactory(
type_id='ietf',
date=(timezone.now() - datetime.timedelta(days=1)).date(),
days=3,
)
right_now = self._right_now_in(meeting.tz())
schedules = dict(
official=meeting.schedule,
unofficial=ScheduleFactory(meeting=meeting, owner=meeting.schedule.owner),
)
timeslots = dict(
past=TimeSlotFactory(meeting=meeting, time=right_now - datetime.timedelta(hours=1)),
future=TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(hours=1)),
)
url_for = lambda sched: urlreverse(
'ietf.meeting.views.edit_meeting_schedule',
kwargs=dict(
num=meeting.number,
owner=str(sched.owner.email()),
name=sched.name,
)
)
post_data = lambda ts: dict(
action='assign',
session=str(SessionFactory(meeting=meeting, add_to_schedule=False).pk),
timeslot=str(ts.pk),
)
username = meeting.schedule.owner.user.username
self.assertTrue(self.client.login(username=username, password=username + '+password'))
# past timeslot, official schedule: reject
r = self.client.post(url_for(schedules['official']), post_data(timeslots['past']))
self.assertEqual(r.status_code, 400)
self.assertEqual(
self._decode_json_response(r),
dict(success=False, error="Can't assign to this timeslot."),
)
# past timeslot, unofficial schedule: allow
r = self.client.post(url_for(schedules['unofficial']), post_data(timeslots['past']))
self.assertEqual(r.status_code, 200)
self.assertTrue(self._decode_json_response(r)['success'])
# future timeslot, official schedule: allow
r = self.client.post(url_for(schedules['official']), post_data(timeslots['future']))
self.assertEqual(r.status_code, 200)
self.assertTrue(self._decode_json_response(r)['success'])
# future timeslot, unofficial schedule: allow
r = self.client.post(url_for(schedules['unofficial']), post_data(timeslots['future']))
self.assertEqual(r.status_code, 200)
self.assertTrue(self._decode_json_response(r)['success'])
def test_reassign_session(self):
"""Do not allow assignment of past sessions for official schedule"""
meeting = MeetingFactory(
type_id='ietf',
date=(timezone.now() - datetime.timedelta(days=1)).date(),
days=3,
)
right_now = self._right_now_in(meeting.tz())
schedules = dict(
official=meeting.schedule,
unofficial=ScheduleFactory(meeting=meeting, owner=meeting.schedule.owner),
)
timeslots = dict(
past=TimeSlotFactory(meeting=meeting, time=right_now - datetime.timedelta(hours=1)),
other_past=TimeSlotFactory(meeting=meeting, time=right_now - datetime.timedelta(hours=2)),
barely_future=TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(minutes=9)),
future=TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(hours=1)),
other_future=TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(hours=2)),
)
self.assertLess(
timeslots['barely_future'].time - right_now,
settings.MEETING_SESSION_LOCK_TIME,
'"barely_future" timeslot is too far in the future. Check MEETING_SESSION_LOCK_TIME settings',
)
url_for = lambda sched: urlreverse(
'ietf.meeting.views.edit_meeting_schedule',
kwargs=dict(
num=meeting.number,
owner=str(sched.owner.email()),
name=sched.name,
)
)
def _new_session_in(timeslot, schedule):
return SchedTimeSessAssignment.objects.create(
schedule=schedule,
session=SessionFactory(meeting=meeting, add_to_schedule=False),
timeslot=timeslot,
).session
post_data = lambda session, new_ts: dict(
action='assign',
session=str(session.pk),
timeslot=str(new_ts.pk),
)
username = meeting.schedule.owner.user.username
self.assertTrue(self.client.login(username=username, password=username + '+password'))
# past session to past timeslot, official: not allowed
session = _new_session_in(timeslots['past'], schedules['official'])
r = self.client.post(url_for(schedules['official']), post_data(session, timeslots['other_past']))
self.assertEqual(r.status_code, 400)
self.assertEqual(
self._decode_json_response(r),
dict(success=False, error="Can't assign to this timeslot."),
)
session.delete() # takes the SchedTimeSessAssignment with it
# past session to future timeslot, official: not allowed
session = _new_session_in(timeslots['past'], schedules['official'])
r = self.client.post(url_for(schedules['official']), post_data(session, timeslots['future']))
self.assertEqual(r.status_code, 400)
self.assertEqual(
self._decode_json_response(r),
dict(success=False, error="Can't reassign this session."),
)
session.delete() # takes the SchedTimeSessAssignment with it
# future session to past, timeslot, official: not allowed
session = _new_session_in(timeslots['future'], schedules['official'])
r = self.client.post(url_for(schedules['official']), post_data(session, timeslots['past']))
self.assertEqual(r.status_code, 400)
self.assertEqual(
self._decode_json_response(r),
dict(success=False, error="Can't assign to this timeslot."),
)
session.delete() # takes the SchedTimeSessAssignment with it
# future session to future timeslot, unofficial: allowed
session = _new_session_in(timeslots['future'], schedules['unofficial'])
r = self.client.post(url_for(schedules['unofficial']), post_data(session, timeslots['other_future']))
self.assertEqual(r.status_code, 200)
self.assertTrue(self._decode_json_response(r)['success'])
session.delete() # takes the SchedTimeSessAssignment with it
# future session to barely future timeslot, official: not allowed
session = _new_session_in(timeslots['future'], schedules['official'])
r = self.client.post(url_for(schedules['official']), post_data(session, timeslots['barely_future']))
self.assertEqual(r.status_code, 400)
self.assertEqual(
self._decode_json_response(r),
dict(success=False, error="Can't assign to this timeslot."),
)
session.delete() # takes the SchedTimeSessAssignment with it
# future session to future timeslot, unofficial: allowed
session = _new_session_in(timeslots['future'], schedules['unofficial'])
r = self.client.post(url_for(schedules['unofficial']), post_data(session, timeslots['barely_future']))
self.assertEqual(r.status_code, 200)
self.assertTrue(self._decode_json_response(r)['success'])
session.delete() # takes the SchedTimeSessAssignment with it
# past session to past timeslot, unofficial: allowed
session = _new_session_in(timeslots['past'], schedules['unofficial'])
r = self.client.post(url_for(schedules['unofficial']), post_data(session, timeslots['other_past']))
self.assertEqual(r.status_code, 200)
self.assertTrue(self._decode_json_response(r)['success'])
session.delete() # takes the SchedTimeSessAssignment with it
# past session to future timeslot, unofficial: allowed
session = _new_session_in(timeslots['past'], schedules['unofficial'])
r = self.client.post(url_for(schedules['unofficial']), post_data(session, timeslots['future']))
self.assertEqual(r.status_code, 200)
self.assertTrue(self._decode_json_response(r)['success'])
session.delete() # takes the SchedTimeSessAssignment with it
# future session to past timeslot, unofficial: allowed
session = _new_session_in(timeslots['future'], schedules['unofficial'])
r = self.client.post(url_for(schedules['unofficial']), post_data(session, timeslots['past']))
self.assertEqual(r.status_code, 200)
self.assertTrue(self._decode_json_response(r)['success'])
session.delete() # takes the SchedTimeSessAssignment with it
# future session to future timeslot, unofficial: allowed
session = _new_session_in(timeslots['future'], schedules['unofficial'])
r = self.client.post(url_for(schedules['unofficial']), post_data(session, timeslots['other_future']))
self.assertEqual(r.status_code, 200)
self.assertTrue(self._decode_json_response(r)['success'])
session.delete() # takes the SchedTimeSessAssignment with it
def test_unassign_session(self):
"""Allow unassignment only of future timeslots for official schedule"""
meeting = MeetingFactory(
type_id='ietf',
date=(timezone.now() - datetime.timedelta(days=1)).date(),
days=3,
)
right_now = self._right_now_in(meeting.tz())
schedules = dict(
official=meeting.schedule,
unofficial=ScheduleFactory(meeting=meeting, owner=meeting.schedule.owner),
)
timeslots = dict(
past=TimeSlotFactory(meeting=meeting, time=right_now - datetime.timedelta(hours=1)),
future=TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(hours=1)),
barely_future=TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(minutes=9)),
)
self.assertLess(
timeslots['barely_future'].time - right_now,
settings.MEETING_SESSION_LOCK_TIME,
'"barely_future" timeslot is too far in the future. Check MEETING_SESSION_LOCK_TIME settings',
)
url_for = lambda sched: urlreverse(
'ietf.meeting.views.edit_meeting_schedule',
kwargs=dict(
num=meeting.number,
owner=str(sched.owner.email()),
name=sched.name,
)
)
post_data = lambda ts, sched: dict(
action='unassign',
session=str(
SchedTimeSessAssignment.objects.create(
schedule=sched,
timeslot=ts,
session=SessionFactory(meeting=meeting, add_to_schedule=False),
).session.pk
),
)
username = meeting.schedule.owner.user.username
self.assertTrue(self.client.login(username=username, password=username + '+password'))
# past session, official schedule: reject
r = self.client.post(url_for(schedules['official']), post_data(timeslots['past'], schedules['official']))
self.assertEqual(r.status_code, 400)
self.assertEqual(
self._decode_json_response(r),
dict(success=False, error="Can't unassign this session."),
)
# past timeslot, unofficial schedule: allow
r = self.client.post(url_for(schedules['unofficial']), post_data(timeslots['past'], schedules['unofficial']))
self.assertEqual(r.status_code, 200)
self.assertTrue(self._decode_json_response(r)['success'])
# barely future session, official schedule: reject
r = self.client.post(url_for(schedules['official']), post_data(timeslots['barely_future'], schedules['official']))
self.assertEqual(r.status_code, 400)
self.assertEqual(
self._decode_json_response(r),
dict(success=False, error="Can't unassign this session."),
)
# barely future timeslot, unofficial schedule: allow
r = self.client.post(url_for(schedules['unofficial']), post_data(timeslots['barely_future'], schedules['unofficial']))
self.assertEqual(r.status_code, 200)
self.assertTrue(self._decode_json_response(r)['success'])
# future timeslot, official schedule: allow
r = self.client.post(url_for(schedules['official']), post_data(timeslots['future'], schedules['official']))
self.assertEqual(r.status_code, 200)
self.assertTrue(self._decode_json_response(r)['success'])
# future timeslot, unofficial schedule: allow
r = self.client.post(url_for(schedules['unofficial']), post_data(timeslots['future'], schedules['unofficial']))
self.assertEqual(r.status_code, 200)
self.assertTrue(self._decode_json_response(r)['success'])
def test_editor_with_no_timeslots(self):
"""Schedule editor should not crash when there are no timeslots"""
meeting = MeetingFactory(
type_id='ietf',
date=date_today() + datetime.timedelta(days=7),
populate_schedule=False,
)
meeting.schedule = ScheduleFactory(meeting=meeting)
meeting.save()
SessionFactory(meeting=meeting, add_to_schedule=False)
self.assertEqual(meeting.timeslot_set.count(), 0, 'Test problem - meeting should not have any timeslots')
url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number})
self.assertTrue(self.client.login(username='secretary', password='secretary+password'))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'No timeslots exist')
self.assertContains(r, urlreverse('ietf.meeting.views.edit_timeslots', kwargs={'num': meeting.number}))
def test_editor_time_zone(self):
"""Agenda editor should show meeting time zone"""
time_zone = 'Etc/GMT+8'
meeting_tz = ZoneInfo(time_zone)
meeting = MeetingFactory(
type_id='ietf',
date=date_today(meeting_tz) + datetime.timedelta(days=7),
populate_schedule=False,
time_zone=time_zone,
)
meeting.schedule = ScheduleFactory(meeting=meeting)
meeting.save()
timeslot = TimeSlotFactory(meeting=meeting)
ts_start = timeslot.time.astimezone(meeting_tz)
ts_end = timeslot.end_time().astimezone(meeting_tz)
url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number})
self.assertTrue(self.client.login(username='secretary', password='secretary+password'))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
pq = PyQuery(r.content)
day_header = pq('.day-flow .day-label')
self.assertIn(ts_start.strftime('%A'), day_header.text())
day_swap = day_header.find('.swap-days')
self.assertEqual(day_swap.attr('data-dayid'), ts_start.date().isoformat())
self.assertEqual(day_swap.attr('data-start'), ts_start.date().isoformat())
time_label = pq('.day-flow .time-header .time-label')
self.assertEqual(len(time_label), 1)
# strftime() does not seem to support hours without leading 0, so do this manually
time_label_string = f'{ts_start.hour:d}:{ts_start.minute:02d} - {ts_end.hour:d}:{ts_end.minute:02d}'
self.assertIn(time_label_string, time_label.text())
self.assertEqual(time_label.attr('data-start'), ts_start.astimezone(datetime.timezone.utc).isoformat())
self.assertEqual(time_label.attr('data-end'), ts_end.astimezone(datetime.timezone.utc).isoformat())
ts_swap = time_label.find('.swap-timeslot-col')
origin_label = ts_swap.attr('data-origin-label')
# testing the exact date in origin_label is hard because Django's date filter uses
# different month formats than Python's strftime, so just check a couple parts.
self.assertIn(ts_start.strftime('%A'), origin_label)
self.assertIn(f'{ts_start.hour:d}:{ts_start.minute:02d}-{ts_end.hour:d}:{ts_end.minute:02d}', origin_label)
timeslot_elt = pq(f'#timeslot{timeslot.pk}')
self.assertEqual(len(timeslot_elt), 1)
self.assertEqual(timeslot_elt.attr('data-start'), ts_start.astimezone(datetime.timezone.utc).isoformat())
self.assertEqual(timeslot_elt.attr('data-end'), ts_end.astimezone(datetime.timezone.utc).isoformat())
timeslot_label = pq(f'#timeslot{timeslot.pk} .time-label')
self.assertEqual(len(timeslot_label), 1)
self.assertIn(time_label_string, timeslot_label.text())
class EditTimeslotsTests(TestCase):
def login(self, username='secretary'):
"""Log in with permission to edit timeslots"""
self.client.login(username=username, password='{}+password'.format(username))
@staticmethod
def edit_timeslots_url(meeting):
return urlreverse('ietf.meeting.views.edit_timeslots', kwargs={'num': meeting.number})
@staticmethod
def edit_timeslot_url(ts: TimeSlot):
return urlreverse('ietf.meeting.views.edit_timeslot',
kwargs={'num': ts.meeting.number, 'slot_id': ts.pk})
@staticmethod
def create_timeslots_url(meeting):
return urlreverse('ietf.meeting.views.create_timeslot', kwargs={'num': meeting.number})
@staticmethod
def create_bare_meeting(number=120) -> Meeting:
"""Create a basic IETF meeting"""
return MeetingFactory(
type_id='ietf',
number=number,
date=date_today() + datetime.timedelta(days=10),
populate_schedule=False,
)
@staticmethod
def create_initial_schedule(meeting):
"""Create initial / base schedule in the same manner as through the UI"""
owner = User.objects.get(username='secretary').person
base_schedule = Schedule.objects.create(
meeting=meeting,
name='base',
owner=owner,
visible=True,
public=True,
)
schedule = Schedule.objects.create(meeting = meeting,
name = "%s-1" % slugify(owner.plain_name()),
owner = owner,
visible = True,
public = True,
base = base_schedule,
)
meeting.schedule = schedule
meeting.save()
def create_meeting(self, number=120):
"""Create a meeting ready for adding timeslots in the usual workflow"""
meeting = self.create_bare_meeting(number=number)
RoomFactory.create_batch(8, meeting=meeting)
self.create_initial_schedule(meeting)
# retrieve meeting from DB so it goes through Django's processing
return Meeting.objects.get(pk=meeting.pk)
def test_view_permissions(self):
"""Only the secretary should be able to edit timeslots"""
# test prep and helper method
usernames_to_reject = [
'plain',
RoleFactory(name_id='chair').person.user.username,
RoleFactory(name_id='ad', group__type_id='area').person.user.username,
]
meeting = self.create_bare_meeting()
url = self.edit_timeslots_url(meeting)
def _assert_permissions(comment):
self.client.logout()
logged_in_username = '<nobody>'
try:
# loop through all the usernames that should be rejected
for username in usernames_to_reject:
login_testing_unauthorized(self, username, url)
logged_in_username = username
# test the last username to reject and log in as secretary
login_testing_unauthorized(self, 'secretary', url)
except AssertionError:
# give a better failure message
self.fail(
'{} should not be able to access the edit timeslots page {}'.format(
logged_in_username,
comment,
)
)
r = self.client.get(url) # confirm secretary can retrieve the page
self.assertEqual(r.status_code, 200,
'secretary should be able to access the edit timeslots page {}'.format(comment))
# Actual tests here
_assert_permissions('without schedule') # first test without a meeting schedule
self.create_initial_schedule(meeting)
_assert_permissions('with schedule') # then test with a meeting schedule
def test_linked_from_agenda_list(self):
"""The edit timeslots view should be linked from the agenda list view"""
ad = RoleFactory(name_id='ad', group__type_id='area').person
meeting = self.create_bare_meeting()
self.create_initial_schedule(meeting)
url = urlreverse('ietf.meeting.views.list_schedules', kwargs={'num': meeting.number})
# Should have no link when logged in as area director
self.login(ad.user.username)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(
len(q('a[href="{}"]'.format(self.edit_timeslots_url(meeting)))),
0,
'User who cannot edit timeslots should not see a link to the edit timeslots page'
)
# Should have a link when logged in as secretary
self.login()
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertGreaterEqual(
len(q('a[href="{}"]'.format(self.edit_timeslots_url(meeting)))),
1,
'Must be at least one link from the agenda list page to the edit timeslots page'
)
def assert_helpful_url(self, response, helpful_url, message):
q = PyQuery(response.content)
self.assertGreaterEqual(
len(q('.timeslot-edit a[href="{}"]'.format(helpful_url))),
1,
message,
)
def test_with_no_rooms(self):
"""Editor should be helpful when there are no rooms yet"""
meeting = self.create_bare_meeting()
self.login()
# with no schedule, should get a link to the meeting page in the secr app until we can
# handle this situation in the meeting app
r = self.client.get(self.edit_timeslots_url(meeting))
self.assertEqual(r.status_code, 200)
self.assert_helpful_url(
r,
urlreverse('ietf.secr.meetings.views.view', kwargs={'meeting_id': meeting.number}),
'Must be a link to a helpful URL when there are no rooms and no schedule'
)
# with a schedule, should get a link to the create rooms page in the secr app
self.create_initial_schedule(meeting)
r = self.client.get(self.edit_timeslots_url(meeting))
self.assertEqual(r.status_code, 200)
self.assert_helpful_url(
r,
urlreverse('ietf.secr.meetings.views.rooms',
kwargs={'meeting_id': meeting.number, 'schedule_name': meeting.schedule.name}),
'Must be a link to a helpful URL when there are no rooms'
)
def test_with_no_timeslots(self):
"""Editor should be helpful when there are rooms but no timeslots yet"""
meeting = self.create_bare_meeting()
RoomFactory(meeting=meeting)
self.login()
helpful_url = self.create_timeslots_url(meeting)
# with no schedule, should get a link to the meeting page in the secr app until we can
# handle this situation in the meeting app
r = self.client.get(self.edit_timeslots_url(meeting))
self.assertEqual(r.status_code, 200)
self.assert_helpful_url(r, helpful_url,
'Must be a link to a helpful URL when there are no timeslots and no schedule')
# with a schedule, should get a link to the create rooms page in the secr app
self.create_initial_schedule(meeting)
r = self.client.get(self.edit_timeslots_url(meeting))
self.assertEqual(r.status_code, 200)
self.assert_helpful_url(r, helpful_url,
'Must be a link to a helpful URL when there are no timeslots')
def assert_required_links_present(self, response, meeting):
"""Assert that required links on the editor page are present"""
q = PyQuery(response.content)
self.assertGreaterEqual(
len(q('a[href="{}"]'.format(self.create_timeslots_url(meeting)))),
1,
'Timeslot edit page should have a link to create timeslots'
)
self.assertGreaterEqual(
len(q('a[href="{}"]'.format(urlreverse('ietf.secr.meetings.views.rooms',
kwargs={'meeting_id': meeting.number,
'schedule_name': meeting.schedule.name}))
)),
1,
'Timeslot edit page should have a link to edit rooms'
)
def test_required_links_present(self):
"""Editor should have links to create timeslots and edit rooms"""
meeting = self.create_meeting()
self.create_initial_schedule(meeting)
RoomFactory.create_batch(8, meeting=meeting)
self.login()
r = self.client.get(self.edit_timeslots_url(meeting))
self.assertEqual(r.status_code, 200)
self.assert_required_links_present(r, meeting)
def test_shows_timeslots(self):
"""Timeslots should be displayed properly"""
def _col_index(elt):
"""Find the column index of an element in its table row
First column is 1
"""
selector = 'td, th' # accept both td and th elements
col_elt = elt.closest(selector)
tr = col_elt.parent('tr')
return 1 + tr.children(selector).index(col_elt[0]) # [0] gets bare element
meeting = self.create_meeting()
# add some timeslots
times = [datetime.time(hour=h) for h in (11, 14)]
days = [meeting.get_meeting_date(ii) for ii in range(meeting.days)]
timeslots = []
duration = datetime.timedelta(minutes=90)
for room in meeting.room_set.all():
for day in days:
timeslots.extend(
TimeSlotFactory(
meeting=meeting,
location=room,
time=meeting.tz().localize(datetime.datetime.combine(day, t)),
duration=duration,
)
for t in times
)
# get the page under test
self.login()
r = self.client.get(self.edit_timeslots_url(meeting))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
table = q('#timeslot-table')
self.assertEqual(len(table), 1, 'Exactly one timeslot-table required')
table = table.eq(0)
# check the day super-column headings
day_headings = table.find('.day-label')
self.assertEqual(len(day_headings), len(days))
day_columns = dict() # map datetime to iterable with table col indices for that day
next_col = _col_index(day_headings.eq(0)) # find column of the first day
for day, heading in zip(days, day_headings.items()):
self.assertIn(day.strftime('%a'), heading.text(),
'Weekday abbrev for {} not found in heading'.format(day))
self.assertIn(day.strftime('%Y-%m-%d'), heading.text(),
'Numeric date for {} not found in heading'.format(day))
cols = int(heading.attr('colspan')) # columns spanned by day header
day_columns[day] = range(next_col, next_col + cols)
next_col += cols
# check the timeslot time headings
time_headings = table.find('.time-label')
self.assertEqual(len(time_headings), len(times) * len(days))
expected_columns = dict() # [date][time] element is expected column for a timeslot
for day, columns in day_columns.items():
headings = time_headings.filter(
# selector for children in any of the day's columns
','.join(
':nth-child({})'.format(col)
for col in columns
)
)
expected_columns[day] = dict()
for time, heading in zip(times, headings.items()):
self.assertIn(time.strftime('%H:%M'), heading.text(),
'Timeslot start {} not found for day {}'.format(time, day))
expected_columns[day][time] = _col_index(heading)
# check that the expected timeslots are shown with expected info / ui features
timeslot_elts = table.find('.timeslot')
self.assertEqual(len(timeslot_elts), len(timeslots), 'Unexpected or missing timeslot elements')
for ts in timeslots:
pk_elts = timeslot_elts.filter('#timeslot{}'.format(ts.pk))
self.assertEqual(len(pk_elts), 1, 'Expect exactly one element for each timeslot')
elt = pk_elts.eq(0)
self.assertIn(ts.name, elt.text(), 'Timeslot name should appear in the element for {}'.format(ts))
self.assertIn(str(ts.type), elt.text(), 'Timeslot type should appear in the element for {}'.format(ts))
self.assertEqual(_col_index(elt), expected_columns[ts.time.date()][ts.time.time()],
'Timeslot {} is in the wrong column'.format(ts))
delete_btn = elt.find('.delete-button[data-delete-scope="timeslot"]')
self.assertEqual(len(delete_btn), 1,
'Timeslot {} should have one delete button'.format(ts))
edit_btn = elt.find('a[href="{}"]'.format(
urlreverse('ietf.meeting.views.edit_timeslot',
kwargs=dict(num=meeting.number, slot_id=ts.pk))
))
self.assertEqual(len(edit_btn), 1,
'Timeslot {} should have one edit button'.format(ts))
# find the room heading for the row
tr = elt.closest('tr')
self.assertIn(ts.location.name, tr.children('th').eq(0).text(),
'Timeslot {} is not shown in the correct row'.format(ts))
def test_bulk_delete_buttons_exist(self):
"""Delete buttons for days and columns should be shown"""
meeting = self.create_meeting()
for day in range(meeting.days):
TimeSlotFactory(
meeting=meeting,
location=meeting.room_set.first(),
time=meeting.tz().localize(
datetime.datetime.combine(
meeting.get_meeting_date(day),
datetime.time(hour=11),
)
),
)
TimeSlotFactory(
meeting=meeting,
location=meeting.room_set.first(),
time=meeting.tz().localize(
datetime.datetime.combine(
meeting.get_meeting_date(day),
datetime.time(hour=14),
)
),
)
self.login()
r = self.client.get(self.edit_timeslots_url(meeting))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
table = q('#timeslot-table')
days = table.find('.day-label')
self.assertEqual(len(days), meeting.days, 'Wrong number of day labels')
for day_label in days.items():
self.assertEqual(len(day_label.find('.delete-button[data-delete-scope="day"]')), 1,
'No delete button for day {}'.format(day_label.text()))
slots = table.find('.time-label')
self.assertEqual(len(slots), 2 * meeting.days, 'Wrong number of slot labels')
for slot_label in slots.items():
self.assertEqual(len(slot_label.find('.delete-button[data-delete-scope="column"]')), 1,
'No delete button for slot {}'.format(slot_label.text()))
def test_timeslot_collision_flag(self):
"""Overlapping timeslots in a room should be flagged
Only checks exact overlap because that is all we currently handle. The display puts
overlapping but not exactly matching timeslots in separate columns which must be
manually checked.
"""
meeting = self.create_bare_meeting()
t1 = TimeSlotFactory(meeting=meeting)
TimeSlotFactory(meeting=meeting, time=t1.time, duration=t1.duration, location=t1.location)
TimeSlotFactory(meeting=meeting, time=t1.time, duration=t1.duration) # other location
TimeSlotFactory(meeting=meeting, time=t1.time.replace(hour=t1.time.hour + 1), location=t1.location) # other time
self.login()
r = self.client.get(self.edit_timeslots_url(meeting))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
slots = q('#timeslot-table .tscell')
self.assertEqual(len(slots), 4) # one per location per distinct time
collision = slots.filter('.timeslot-collision')
no_collision = slots.filter(':not(.timeslot-collision)')
self.assertEqual(len(collision), 1, 'Wrong number of timeslot collisions flagged')
self.assertEqual(len(no_collision), 3, 'Wrong number of non-colliding timeslots')
# check that the cell containing t1 is the one flagged as a conflict
self.assertEqual(len(collision.find('#timeslot{}'.format(t1.pk))), 1,
'Wrong timeslot cell flagged as having a collision')
def test_timeslot_in_use_flag(self):
"""Timeslots that are in use should be flagged"""
meeting = self.create_meeting()
# assign sessions to some timeslots
empty, has_official, has_other = TimeSlotFactory.create_batch(3, meeting=meeting, location=meeting.room_set.first())
SchedTimeSessAssignment.objects.create(
timeslot=has_official,
session=SessionFactory(meeting=meeting, add_to_schedule=False),
schedule=meeting.schedule, # official schedule
)
SchedTimeSessAssignment.objects.create(
timeslot=has_other,
session=SessionFactory(meeting=meeting, add_to_schedule=False),
schedule=ScheduleFactory(meeting=meeting), # not the official schedule
)
# get the page
self.login()
r = self.client.get(self.edit_timeslots_url(meeting))
self.assertEqual(r.status_code, 200)
# now check that all timeslots appear, flagged appropriately
q = PyQuery(r.content)
empty_elt = q('#timeslot{}'.format(empty.pk))
has_official_elt = q('#timeslot{}'.format(has_official.pk))
has_other_elt = q('#timeslot{}'.format(has_other.pk))
self.assertEqual(empty_elt.attr('data-unofficial-use'), 'false', 'Unused timeslot should not be in use')
self.assertEqual(empty_elt.attr('data-official-use'), 'false', 'Unused timeslot should not be in use')
self.assertEqual(has_other_elt.attr('data-unofficial-use'), 'true',
'Unofficially used timeslot should be flagged')
self.assertEqual(has_other_elt.attr('data-official-use'), 'false',
'Unofficially used timeslot is not in official use')
self.assertEqual(has_official_elt.attr('data-unofficial-use'), 'false',
'Officially used timeslot not in unofficial use')
self.assertEqual(has_official_elt.attr('data-official-use'), 'true',
'Officially used timeslot should be flagged')
def test_edit_timeslot(self):
"""Edit page should work as expected"""
meeting = self.create_meeting()
name_before = 'Name Classic (tm)'
type_before = 'regular'
time_utc = pytz.utc.localize(datetime.datetime.combine(meeting.date, datetime.time(hour=10)))
time_before = time_utc.astimezone(meeting.tz())
duration_before = datetime.timedelta(minutes=60)
show_location_before = True
location_before = meeting.room_set.first()
ts = TimeSlotFactory(
meeting=meeting,
name=name_before,
type_id=type_before,
time=time_before,
duration=duration_before,
show_location=show_location_before,
location=location_before,
)
self.login()
url = self.edit_timeslot_url(ts)
# check that sched parameter is preserved
r = self.client.get(url)
self.assertNotContains(r, '?sched=', status_code=200)
r = self.client.get(url + '?sched=1234')
self.assertContains(r, '?sched=1234', status_code=200) # could check in more detail
name_after = 'New Name (tm)'
type_after = 'plenary'
time_after = (time_utc + datetime.timedelta(days=1, hours=2)).astimezone(meeting.tz())
duration_after = duration_before * 2
show_location_after = False
location_after = meeting.room_set.last()
post_data = dict(
name=name_after,
type=type_after,
time_0=time_after.strftime('%Y-%m-%d'), # date for SplitDateTimeField
time_1=time_after.strftime('%H:%M'), # time for SplitDateTimeField
duration=str(duration_after),
# show_location=show_location_after, # False values are omitted from form
location=location_after.pk,
)
r = self.client.post(url, data=post_data)
self.assertEqual(r.status_code, 302) # expect redirect to timeslot edit url
self.assertEqual(r['Location'], self.edit_timeslots_url(meeting),
'Expected to be redirected to meeting timeslots edit page')
# check that we changed things
self.assertNotEqual(name_before, name_after)
self.assertNotEqual(type_before, type_after)
self.assertNotEqual(time_before, time_after)
self.assertNotEqual(duration_before, duration_after)
self.assertNotEqual(location_before, location_after)
# and that we have the new values
ts = TimeSlot.objects.get(pk=ts.pk)
self.assertEqual(ts.name, name_after)
self.assertEqual(ts.type_id, type_after)
self.assertEqual(ts.time, time_after)
self.assertEqual(ts.duration, duration_after)
self.assertEqual(ts.show_location, show_location_after)
self.assertEqual(ts.location, location_after)
# and check with sched param set
r = self.client.post(url + '?sched=1234', data=post_data)
self.assertEqual(r.status_code, 302) # expect redirect to timeslot edit url
self.assertEqual(r['Location'], self.edit_timeslots_url(meeting) + '?sched=1234',
'Expected to be redirected to meeting timeslots edit page with sched param set')
def test_invalid_edit_timeslot(self):
meeting = self.create_bare_meeting()
ts: TimeSlot = TimeSlotFactory(meeting=meeting, name='slot') # type: ignore[annotation-unchecked]
self.login()
r = self.client.post(
self.edit_timeslot_url(ts),
data=dict(
name='',
type=ts.type.pk,
time_0=ts.time.strftime('%Y-%m-%d'),
time_1=ts.time.strftime('%H:%M'),
duration=str(ts.duration),
show_location=ts.show_location,
location=str(ts.location.pk),
)
)
self.assertContains(r, 'This field is required', status_code=400,
msg_prefix='Missing name not properly rejected')
r = self.client.post(
self.edit_timeslot_url(ts),
data=dict(
name='different name',
type='this is not a type id',
time_0=ts.time.strftime('%Y-%m-%d'),
time_1=ts.time.strftime('%H:%M'),
duration=str(ts.duration),
show_location=ts.show_location,
location=str(ts.location.pk),
)
)
self.assertContains(r, 'Select a valid choice', status_code=400,
msg_prefix='Invalid type not properly rejected')
r = self.client.post(
self.edit_timeslot_url(ts),
data=dict(
name='different name',
type=ts.type.pk,
time_0='this is not a date',
time_1=ts.time.strftime('%H:%M'),
duration=str(ts.duration),
show_location=ts.show_location,
location=str(ts.location.pk),
)
)
self.assertContains(r, 'Enter a valid date', status_code=400,
msg_prefix='Invalid date not properly rejected')
r = self.client.post(
self.edit_timeslot_url(ts),
data=dict(
name='different name',
type=ts.type.pk,
time_0=ts.time.strftime('%Y-%m-%d'),
time_1='this is not a time',
duration=str(ts.duration),
show_location=ts.show_location,
location=str(ts.location.pk),
)
)
self.assertContains(r, 'Enter a valid time', status_code=400,
msg_prefix='Invalid time not properly rejected')
r = self.client.post(
self.edit_timeslot_url(ts),
data=dict(
name='different name',
type=ts.type.pk,
time_0=ts.time.strftime('%Y-%m-%d'),
time_1=ts.time.strftime('%H:%M'),
duration='this is not a duration',
show_location=ts.show_location,
location=str(ts.location.pk),
)
)
self.assertContains(r, 'Enter a valid duration', status_code=400,
msg_prefix='Invalid duration not properly rejected')
r = self.client.post(
self.edit_timeslot_url(ts),
data=dict(
name='different name',
type=ts.type.pk,
time_0=ts.time.strftime('%Y-%m-%d'),
time_1=ts.time.strftime('%H:%M'),
duration='26:00', # longer than 12 hours,
show_location=ts.show_location,
location=str(ts.location.pk),
)
)
self.assertContains(r, 'Ensure this value is less than or equal to', status_code=400,
msg_prefix='Overlong duration not properly rejected')
r = self.client.post(
self.edit_timeslot_url(ts),
data=dict(
name='different name',
type=str(ts.type.pk),
time_0=ts.time.strftime('%Y-%m-%d'),
time_1=ts.time.strftime('%H:%M'),
duration=str(ts.duration),
show_location=ts.show_location,
location='this is not a location',
)
)
self.assertContains(r, 'Select a valid choice', status_code=400,
msg_prefix='Invalid location not properly rejected')
ts_after = meeting.timeslot_set.get(pk=ts.pk)
self.assertEqual(ts.name, ts_after.name)
self.assertEqual(ts.type, ts_after.type)
self.assertEqual(ts.time, ts_after.time)
self.assertEqual(ts.duration, ts_after.duration)
self.assertEqual(ts.show_location, ts_after.show_location)
self.assertEqual(ts.location, ts_after.location)
def test_create_single_timeslot(self):
"""Creating a single timeslot should work"""
meeting = self.create_meeting()
timeslots_before = set(ts.pk for ts in meeting.timeslot_set.all())
url = self.create_timeslots_url(meeting)
post_data = dict(
name='some name',
type='regular',
days=str(meeting.date.toordinal()),
time='14:37',
duration='1:13', # does not include seconds
show_location=True,
locations=str(meeting.room_set.first().pk),
)
self.login()
# check that sched parameter is preserved
r = self.client.get(url)
self.assertNotContains(r, '?sched=', status_code=200)
r = self.client.get(url + '?sched=1234')
self.assertContains(r, '?sched=1234', status_code=200) # could check in more detail
r = self.client.post(url, data=post_data)
self.assertEqual(r.status_code, 302)
self.assertEqual(r['Location'], self.edit_timeslots_url(meeting),
'Expected to be redirected to meeting timeslots edit page')
self.assertEqual(meeting.timeslot_set.count(), len(timeslots_before) + 1)
ts = meeting.timeslot_set.exclude(pk__in=timeslots_before).first() # only 1
self.assertEqual(ts.name, post_data['name'])
self.assertEqual(ts.type_id, post_data['type'])
self.assertEqual(str(ts.local_start_time().date().toordinal()), post_data['days'])
self.assertEqual(ts.local_start_time().strftime('%H:%M'), post_data['time'])
self.assertEqual(str(ts.duration), '{}:00'.format(post_data['duration'])) # add seconds
self.assertEqual(ts.show_location, post_data['show_location'])
self.assertEqual(str(ts.location.pk), post_data['locations'])
# check again with sched parameter
r = self.client.post(url + '?sched=1234', data=post_data)
self.assertEqual(r.status_code, 302)
self.assertEqual(r['Location'], self.edit_timeslots_url(meeting) + '?sched=1234',
'Expected to be redirected to meeting timeslots edit page with sched parameter set')
def test_create_single_timeslot_outside_meeting_days(self):
"""Creating a single timeslot outside the official meeting days should work"""
meeting = self.create_meeting()
timeslots_before = set(ts.pk for ts in meeting.timeslot_set.all())
other_date = meeting.get_meeting_date(-7)
post_data = dict(
name='some name',
type='regular',
other_date=other_date.strftime('%Y-%m-%d'),
time='14:37',
duration='1:13', # does not include seconds
show_location=True,
locations=str(meeting.room_set.first().pk),
)
self.login()
r = self.client.post(
self.create_timeslots_url(meeting),
data=post_data,
)
self.assertEqual(r.status_code, 302)
self.assertEqual(r['Location'], self.edit_timeslots_url(meeting),
'Expected to be redirected to meeting timeslots edit page')
self.assertEqual(meeting.timeslot_set.count(), len(timeslots_before) + 1)
ts = meeting.timeslot_set.exclude(pk__in=timeslots_before).first() # only 1
self.assertEqual(ts.name, post_data['name'])
self.assertEqual(ts.type_id, post_data['type'])
self.assertEqual(ts.local_start_time().date(), other_date)
self.assertEqual(ts.local_start_time().strftime('%H:%M'), post_data['time'])
self.assertEqual(str(ts.duration), '{}:00'.format(post_data['duration'])) # add seconds
self.assertEqual(ts.show_location, post_data['show_location'])
self.assertEqual(str(ts.location.pk), post_data['locations'])
def test_invalid_create_timeslot(self):
meeting = self.create_bare_meeting()
room_pk = str(RoomFactory(meeting=meeting).pk)
timeslot_count = TimeSlot.objects.count()
self.login()
r = self.client.post(
self.create_timeslots_url(meeting),
data=dict(
name='',
type='regular',
days=str(meeting.date.toordinal()),
time='14:37',
duration='1:13', # does not include seconds
show_location=True,
locations=room_pk,
)
)
self.assertContains(r, 'This field is required', status_code=400,
msg_prefix='Empty name not rejected properly')
r = self.client.post(
self.create_timeslots_url(meeting),
data=dict(
name='this is a name',
type='this is not a type',
days=str(meeting.date.toordinal()),
time='14:37',
duration='1:13', # does not include seconds
show_location=True,
locations=room_pk,
)
)
self.assertContains(r, 'Select a valid choice', status_code=400,
msg_prefix='Invalid type not rejected properly')
r = self.client.post(
self.create_timeslots_url(meeting),
data=dict(
name='this is a name',
type='regular',
# days='',
time='14:37',
duration='1:13', # does not include seconds
show_location=True,
locations=room_pk,
)
)
self.assertContains(r, 'Please select a day or specify a date', status_code=400,
msg_prefix='Missing date not rejected properly')
r = self.client.post(
self.create_timeslots_url(meeting),
data=dict(
name='this is a name',
type='regular',
days='this is not an ordinal date',
time='14:37',
duration='1:13', # does not include seconds
show_location=True,
locations=room_pk,
)
)
self.assertContains(r, 'Select a valid choice', status_code=400,
msg_prefix='Invalid day not rejected properly')
r = self.client.post(
self.create_timeslots_url(meeting),
data=dict(
name='this is a name',
type='regular',
days=[str(meeting.date.toordinal()), 'this is not an ordinal date'],
time='14:37',
duration='1:13', # does not include seconds
show_location=True,
locations=room_pk,
)
)
self.assertContains(r, 'Select a valid choice', status_code=400,
msg_prefix='Invalid day with valid day not rejected properly')
r = self.client.post(
self.create_timeslots_url(meeting),
data=dict(
name='this is a name',
type='regular',
days=str(meeting.date.toordinal()),
other_date='this is not a date',
time='14:37',
duration='1:13', # does not include seconds
show_location=True,
locations=room_pk,
)
)
self.assertContains(r, 'Enter a valid date', status_code=400,
msg_prefix='Invalid other_date with valid days not rejected properly')
r = self.client.post(
self.create_timeslots_url(meeting),
data=dict(
name='this is a name',
type='regular',
days='this is not an ordinal date',
other_date='2021-07-13',
time='14:37',
duration='1:13', # does not include seconds
show_location=True,
locations=room_pk,
)
)
self.assertContains(r, 'Select a valid choice', status_code=400,
msg_prefix='Invalid day with valid other_date not rejected properly')
r = self.client.post(
self.create_timeslots_url(meeting),
data=dict(
name='this is a name',
type='regular',
other_date='this is not a date',
time='14:37',
duration='1:13', # does not include seconds
show_location=True,
locations=room_pk,
)
)
self.assertContains(r, 'Enter a valid date', status_code=400,
msg_prefix='Invalid other_date not rejected properly')
r = self.client.post(
self.create_timeslots_url(meeting),
data=dict(
name='this is a name',
type='regular',
days=str(meeting.date.toordinal()),
time='14:37',
duration="ceci n'est pas une duree",
show_location=True,
locations=room_pk,
)
)
self.assertContains(r, 'Enter a valid duration', status_code=400,
msg_prefix='Invalid duration not rejected properly')
r = self.client.post(
self.create_timeslots_url(meeting),
data=dict(
name='this is a name',
type='regular',
days=str(meeting.date.toordinal()),
time='14:37',
duration="26:00",
show_location=True,
locations=room_pk,
)
)
self.assertContains(r, 'Ensure this value is less than or equal to', status_code=400,
msg_prefix='Overlong duration not rejected properly')
r = self.client.post(
self.create_timeslots_url(meeting),
data=dict(
name='this is a name',
type='regular',
days=str(meeting.date.toordinal()),
time='14:37',
duration="1:13",
show_location=True,
locations='this is not a room',
)
)
self.assertContains(r, 'is not a valid value', status_code=400,
msg_prefix='Invalid location not rejected properly')
r = self.client.post(
self.create_timeslots_url(meeting),
data=dict(
name='this is a name',
type='regular',
days=str(meeting.date.toordinal()),
time='14:37',
duration="1:13",
show_location=True,
locations=[room_pk, 'this is not a room'],
)
)
self.assertContains(r, 'is not a valid value', status_code=400,
msg_prefix='Invalid location with valid location not rejected properly')
r = self.client.post(
self.create_timeslots_url(meeting),
data=dict(
name='this is a name',
type='regular',
days=str(meeting.date.toordinal()),
time='14:37',
duration="1:13",
show_location=True,
)
)
self.assertContains(r, 'This field is required', status_code=400,
msg_prefix='Missing location not rejected properly')
self.assertEqual(TimeSlot.objects.count(), timeslot_count,
'TimeSlot unexpectedly created')
def test_create_bulk_timeslots(self):
"""Creating multiple timeslots should work"""
meeting = self.create_meeting()
timeslots_before = set(ts.pk for ts in meeting.timeslot_set.all())
days = [meeting.get_meeting_date(n) for n in range(meeting.days)]
other_date = meeting.get_meeting_date(-1) # date before start of meeting
self.assertNotIn(other_date, days)
locations = meeting.room_set.all()
post_data = dict(
name='some name',
type='regular',
days=[str(d.toordinal()) for d in days],
other_date=other_date.strftime('%Y-%m-%d'),
time='14:37',
duration='1:13', # does not include seconds
show_location=True,
locations=[str(loc.pk) for loc in locations],
)
self.login()
r = self.client.post(
self.create_timeslots_url(meeting),
data=post_data,
)
self.assertEqual(r.status_code, 302)
self.assertEqual(r['Location'], self.edit_timeslots_url(meeting),
'Expected to be redirected to meeting timeslots edit page')
days.append(other_date)
new_slot_count = len(days) * len(locations)
self.assertEqual(meeting.timeslot_set.count(), len(timeslots_before) + new_slot_count)
day_locs = set((day, loc) for day in days for loc in locations) # cartesian product
for ts in meeting.timeslot_set.exclude(pk__in=timeslots_before):
self.assertEqual(ts.name, post_data['name'])
self.assertEqual(ts.type_id, post_data['type'])
self.assertEqual(ts.local_start_time().strftime('%H:%M'), post_data['time'])
self.assertEqual(str(ts.duration), '{}:00'.format(post_data['duration'])) # add seconds
self.assertEqual(ts.show_location, post_data['show_location'])
self.assertIn(ts.local_start_time().date(), days)
self.assertIn(ts.location, locations)
self.assertIn((ts.time.date(), ts.location), day_locs,
'Duplicated day / location found')
day_locs.discard((ts.time.date(), ts.location))
self.assertEqual(day_locs, set(), 'Not all day/location combinations created')
def test_sched_param_preserved(self):
meeting = MeetingFactory(type_id='ietf')
url = urlreverse('ietf.meeting.views.edit_timeslots', kwargs={'num': meeting.number})
self.client.login(username='secretary', password='secretary+password')
r = self.client.get(url)
self.assertNotContains(r, '?sched=', status_code=200)
self.assertNotContains(r, "Back to agenda")
r = self.client.get(url + '?sched=1234')
self.assertContains(r, '?sched=1234', status_code=200) # could check in more detail
self.assertContains(r, "Back to agenda")
def test_ajax_delete_timeslot(self):
"""AJAX call to delete timeslot should work"""
meeting = self.create_bare_meeting()
ts_to_del, ts_to_keep = TimeSlotFactory.create_batch(2, meeting=meeting)
self.login()
r = self.client.post(
self.edit_timeslots_url(meeting),
data=dict(
action='delete',
slot_id=str(ts_to_del.pk),
)
)
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'Deleted TimeSlot {}'.format(ts_to_del.pk))
self.assertNotContains(r, 'Deleted TimeSlot {}'.format(ts_to_keep.pk))
self.assertEqual(meeting.timeslot_set.filter(pk=ts_to_del.pk).count(), 0,
'Timeslot not deleted')
self.assertEqual(meeting.timeslot_set.filter(pk=ts_to_keep.pk).count(), 1,
'Extra timeslot deleted')
def test_ajax_delete_timeslots(self):
"""AJAX call to delete several timeslots should work"""
meeting = self.create_bare_meeting()
ts_to_del = TimeSlotFactory.create_batch(5, meeting=meeting)
ts_to_keep = TimeSlotFactory(meeting=meeting)
self.login()
r = self.client.post(
self.edit_timeslots_url(meeting),
data=dict(
action='delete',
slot_id=','.join(str(ts.pk) for ts in ts_to_del),
)
)
self.assertEqual(r.status_code, 200)
for ts in ts_to_del:
self.assertContains(r, 'Deleted TimeSlot {}'.format(ts.pk))
self.assertNotContains(r, 'Deleted TimeSlot {}'.format(ts_to_keep.pk))
self.assertEqual(
meeting.timeslot_set.filter(pk__in=(ts.pk for ts in ts_to_del)).count(),
0,
'Timeslots not deleted',
)
self.assertEqual(meeting.timeslot_set.filter(pk=ts_to_keep.pk).count(), 1,
'Extra timeslot deleted')
def test_ajax_delete_timeslots_invalid(self):
meeting = self.create_bare_meeting()
ts = TimeSlotFactory(meeting=meeting)
self.login()
r = self.client.post(
self.edit_timeslots_url(meeting),
)
self.assertEqual(r.status_code, 400, 'Missing POST data not handled')
r = self.client.post(
self.edit_timeslots_url(meeting),
data=dict()
)
self.assertEqual(r.status_code, 400, 'Empty POST data not handled')
r = self.client.post(
self.edit_timeslots_url(meeting),
data=dict(
slot_id=str(ts.pk),
)
)
self.assertEqual(r.status_code, 400, 'Missing action not handled')
r = self.client.post(
self.edit_timeslots_url(meeting),
data=dict(
action='deletify',
slot_id=str(ts.pk),
)
)
self.assertEqual(r.status_code, 400, 'Invalid action not handled')
r = self.client.post(
self.edit_timeslots_url(meeting),
data=dict(
action='delete',
)
)
self.assertEqual(r.status_code, 400, 'Missing slot_id not handled')
r = self.client.post(
self.edit_timeslots_url(meeting),
data=dict(
action='delete',
slot_id='not an id',
)
)
self.assertEqual(r.status_code, 400, 'Invalid slot_id not handled')
r = self.client.post(
self.edit_timeslots_url(meeting),
data=dict(
action='delete',
slot_id='{}, not an id'.format(ts.pk),
)
)
self.assertEqual(r.status_code, 400, 'Invalid slot_id not handled in bulk')
nonexistent_id = TimeSlot.objects.all().aggregate(Max('id'))['id__max'] + 1
r = self.client.post(
self.edit_timeslots_url(meeting),
data=dict(
action='delete',
slot_id=str(nonexistent_id),
)
)
self.assertEqual(r.status_code, 404, 'Nonexistent slot_id not handled in bulk')
r = self.client.post(
self.edit_timeslots_url(meeting),
data=dict(
action='delete',
slot_id='{},{}'.format(nonexistent_id, ts.pk),
)
)
self.assertEqual(r.status_code, 404, 'Nonexistent slot_id not handled in bulk')
self.assertEqual(meeting.timeslot_set.filter(pk=ts.pk).count(), 1,
'TimeSlot unexpectedly deleted')
class ReorderSlidesTests(TestCase):
@override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls
@patch("ietf.meeting.views.SlidesManager")
def test_add_slides_to_session(self, mock_slides_manager_cls):
for type_id in ('ietf','interim'):
chair_role = RoleFactory(name_id='chair')
session = SessionFactory(group=chair_role.group, meeting__date=date_today() - datetime.timedelta(days=90), meeting__type_id=type_id)
slides = DocumentFactory(type_id='slides')
url = urlreverse('ietf.meeting.views.ajax_add_slides_to_session', kwargs={'session_id':session.pk, 'num':session.meeting.number})
# Not a valid user
r = self.client.post(url, {'order':1, 'name':slides.name })
self.assertEqual(r.status_code, 403)
self.assertIn('have permission', unicontent(r))
self.assertFalse(mock_slides_manager_cls.called)
self.client.login(username=chair_role.person.user.username, password=chair_role.person.user.username+"+password")
# Past submission cutoff
r = self.client.post(url, {'order':0, 'name':slides.name })
self.assertEqual(r.status_code, 403)
self.assertIn('materials cutoff', unicontent(r))
self.assertFalse(mock_slides_manager_cls.called)
session.meeting.date = date_today()
session.meeting.save()
# Invalid order
r = self.client.post(url, {})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('No data',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
r = self.client.post(url, {'garbage':'garbage'})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('order is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
r = self.client.post(url, {'order':0, 'name':slides.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('order is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
r = self.client.post(url, {'order':2, 'name':slides.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('order is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
r = self.client.post(url, {'order':'garbage', 'name':slides.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('order is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
# Invalid name
r = self.client.post(url, {'order':1 })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('name is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
r = self.client.post(url, {'order':1, 'name':'garbage' })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('name is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
# Valid post
r = self.client.post(url, {'order':1, 'name':slides.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertEqual(session.presentations.count(),1)
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.add.called)
self.assertEqual(mock_slides_manager_cls.return_value.add.call_args, call(session=session, slides=slides, order=1))
mock_slides_manager_cls.reset_mock()
# Ignore a request to add slides that are already in a session
r = self.client.post(url, {'order':1, 'name':slides.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertEqual(session.presentations.count(),1)
self.assertFalse(mock_slides_manager_cls.called)
session2 = SessionFactory(group=session.group, meeting=session.meeting)
SessionPresentationFactory.create_batch(3, document__type_id='slides', session=session2)
for num, sp in enumerate(session2.presentations.filter(document__type_id='slides'),start=1):
sp.order = num
sp.save()
url = urlreverse('ietf.meeting.views.ajax_add_slides_to_session', kwargs={'session_id':session2.pk, 'num':session2.meeting.number})
more_slides = DocumentFactory.create_batch(3, type_id='slides')
# Insert at beginning
r = self.client.post(url, {'order':1, 'name':more_slides[0].name})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertEqual(session2.presentations.get(document=more_slides[0]).order,1)
self.assertEqual(list(session2.presentations.order_by('order').values_list('order',flat=True)), list(range(1,5)))
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.add.called)
self.assertEqual(mock_slides_manager_cls.return_value.add.call_args, call(session=session2, slides=more_slides[0], order=1))
mock_slides_manager_cls.reset_mock()
# Insert at end
r = self.client.post(url, {'order':5, 'name':more_slides[1].name})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertEqual(session2.presentations.get(document=more_slides[1]).order,5)
self.assertEqual(list(session2.presentations.order_by('order').values_list('order',flat=True)), list(range(1,6)))
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.add.called)
self.assertEqual(mock_slides_manager_cls.return_value.add.call_args, call(session=session2, slides=more_slides[1], order=5))
mock_slides_manager_cls.reset_mock()
# Insert in middle
r = self.client.post(url, {'order':3, 'name':more_slides[2].name})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertEqual(session2.presentations.get(document=more_slides[2]).order,3)
self.assertEqual(list(session2.presentations.order_by('order').values_list('order',flat=True)), list(range(1,7)))
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.add.called)
self.assertEqual(mock_slides_manager_cls.return_value.add.call_args, call(session=session2, slides=more_slides[2], order=3))
mock_slides_manager_cls.reset_mock()
@override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls
@patch("ietf.meeting.views.SlidesManager")
def test_remove_slides_from_session(self, mock_slides_manager_cls):
for type_id in ['ietf','interim']:
chair_role = RoleFactory(name_id='chair')
session = SessionFactory(group=chair_role.group, meeting__date=date_today()-datetime.timedelta(days=90), meeting__type_id=type_id)
slides = DocumentFactory(type_id='slides')
url = urlreverse('ietf.meeting.views.ajax_remove_slides_from_session', kwargs={'session_id':session.pk, 'num':session.meeting.number})
# Not a valid user
r = self.client.post(url, {'oldIndex':1, 'name':slides.name })
self.assertEqual(r.status_code, 403)
self.assertIn('have permission', unicontent(r))
self.assertFalse(mock_slides_manager_cls.called)
self.client.login(username=chair_role.person.user.username, password=chair_role.person.user.username+"+password")
# Past submission cutoff
r = self.client.post(url, {'oldIndex':0, 'name':slides.name })
self.assertEqual(r.status_code, 403)
self.assertIn('materials cutoff', unicontent(r))
self.assertFalse(mock_slides_manager_cls.called)
session.meeting.date = date_today()
session.meeting.save()
# Invalid order
r = self.client.post(url, {})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('No data',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
r = self.client.post(url, {'garbage':'garbage'})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('index is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
r = self.client.post(url, {'oldIndex':0, 'name':slides.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('index is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
r = self.client.post(url, {'oldIndex':'garbage', 'name':slides.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('index is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
# No matching thing to delete
r = self.client.post(url, {'oldIndex':1, 'name':slides.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('index is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
session.presentations.create(document=slides, rev=slides.rev, order=1)
# Bad names
r = self.client.post(url, {'oldIndex':1})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('name is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
r = self.client.post(url, {'oldIndex':1, 'name':'garbage' })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('name is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
slides2 = DocumentFactory(type_id='slides')
# index/name mismatch
r = self.client.post(url, {'oldIndex':1, 'name':slides2.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('SessionPresentation not found',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
session.presentations.create(document=slides2, rev=slides2.rev, order=2)
r = self.client.post(url, {'oldIndex':1, 'name':slides2.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('Name does not match index',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
# valid removal
r = self.client.post(url, {'oldIndex':1, 'name':slides.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertEqual(session.presentations.count(),1)
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.delete.called)
self.assertEqual(mock_slides_manager_cls.return_value.delete.call_args, call(session=session, slides=slides))
mock_slides_manager_cls.reset_mock()
session2 = SessionFactory(group=session.group, meeting=session.meeting)
sp_list = SessionPresentationFactory.create_batch(5, document__type_id='slides', session=session2)
for num, sp in enumerate(session2.presentations.filter(document__type_id='slides'),start=1):
sp.order = num
sp.save()
url = urlreverse('ietf.meeting.views.ajax_remove_slides_from_session', kwargs={'session_id':session2.pk, 'num':session2.meeting.number})
# delete at first of list
r = self.client.post(url, {'oldIndex':1, 'name':sp_list[0].document.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertFalse(session2.presentations.filter(pk=sp_list[0].pk).exists())
self.assertEqual(list(session2.presentations.order_by('order').values_list('order',flat=True)), list(range(1,5)))
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.delete.called)
self.assertEqual(mock_slides_manager_cls.return_value.delete.call_args, call(session=session2, slides=sp_list[0].document))
mock_slides_manager_cls.reset_mock()
# delete in middle of list
r = self.client.post(url, {'oldIndex':4, 'name':sp_list[4].document.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertFalse(session2.presentations.filter(pk=sp_list[4].pk).exists())
self.assertEqual(list(session2.presentations.order_by('order').values_list('order',flat=True)), list(range(1,4)))
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.delete.called)
self.assertEqual(mock_slides_manager_cls.return_value.delete.call_args, call(session=session2, slides=sp_list[4].document))
mock_slides_manager_cls.reset_mock()
# delete at end of list
r = self.client.post(url, {'oldIndex':2, 'name':sp_list[2].document.name })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertFalse(session2.presentations.filter(pk=sp_list[2].pk).exists())
self.assertEqual(list(session2.presentations.order_by('order').values_list('order',flat=True)), list(range(1,3)))
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.delete.called)
self.assertEqual(mock_slides_manager_cls.return_value.delete.call_args, call(session=session2, slides=sp_list[2].document))
mock_slides_manager_cls.reset_mock()
@override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls
@patch("ietf.meeting.views.SlidesManager")
def test_reorder_slides_in_session(self, mock_slides_manager_cls):
def _sppk_at(sppk, positions):
return [sppk[p-1] for p in positions]
chair_role = RoleFactory(name_id='chair')
session = SessionFactory(group=chair_role.group, meeting__date=date_today() - datetime.timedelta(days=90))
sp_list = SessionPresentationFactory.create_batch(5, document__type_id='slides', session=session)
sppk = [o.pk for o in sp_list]
for num, sp in enumerate(sp_list, start=1):
sp.order = num
sp.save()
url = urlreverse('ietf.meeting.views.ajax_reorder_slides_in_session', kwargs={'session_id':session.pk, 'num':session.meeting.number})
for type_id in ['ietf','interim']:
session.meeting.type_id = type_id
session.meeting.date = date_today()-datetime.timedelta(days=90)
session.meeting.save()
# Not a valid user
r = self.client.post(url, {'oldIndex':1, 'newIndex':2 })
self.assertEqual(r.status_code, 403)
self.assertIn('have permission', unicontent(r))
self.assertFalse(mock_slides_manager_cls.called)
self.client.login(username=chair_role.person.user.username, password=chair_role.person.user.username+"+password")
# Past submission cutoff
r = self.client.post(url, {'oldIndex':1, 'newIndex':2 })
self.assertEqual(r.status_code, 403)
self.assertIn('materials cutoff', unicontent(r))
self.assertFalse(mock_slides_manager_cls.called)
session.meeting.date = date_today()
session.meeting.save()
# Bad index values
r = self.client.post(url, {'oldIndex':0, 'newIndex':2 })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('index is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
r = self.client.post(url, {'oldIndex':2, 'newIndex':6 })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('index is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
r = self.client.post(url, {'oldIndex':2, 'newIndex':2 })
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],False)
self.assertIn('index is not valid',r.json()['error'])
self.assertFalse(mock_slides_manager_cls.called)
# Move from beginning
r = self.client.post(url, {'oldIndex':1, 'newIndex':3})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertEqual(list(session.presentations.order_by('order').values_list('pk',flat=True)),_sppk_at(sppk,[2,3,1,4,5]))
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.send_update.called)
self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_args, call(session))
mock_slides_manager_cls.reset_mock()
# Move to beginning
r = self.client.post(url, {'oldIndex':3, 'newIndex':1})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertEqual(list(session.presentations.order_by('order').values_list('pk',flat=True)),_sppk_at(sppk,[1,2,3,4,5]))
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.send_update.called)
self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_args, call(session))
mock_slides_manager_cls.reset_mock()
# Move from end
r = self.client.post(url, {'oldIndex':5, 'newIndex':3})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertEqual(list(session.presentations.order_by('order').values_list('pk',flat=True)),_sppk_at(sppk,[1,2,5,3,4]))
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.send_update.called)
self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_args, call(session))
mock_slides_manager_cls.reset_mock()
# Move to end
r = self.client.post(url, {'oldIndex':3, 'newIndex':5})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertEqual(list(session.presentations.order_by('order').values_list('pk',flat=True)),_sppk_at(sppk,[1,2,3,4,5]))
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.send_update.called)
self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_args, call(session))
mock_slides_manager_cls.reset_mock()
# Move beginning to end
r = self.client.post(url, {'oldIndex':1, 'newIndex':5})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertEqual(list(session.presentations.order_by('order').values_list('pk',flat=True)),_sppk_at(sppk,[2,3,4,5,1]))
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.send_update.called)
self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_args, call(session))
mock_slides_manager_cls.reset_mock()
# Move middle to middle
r = self.client.post(url, {'oldIndex':3, 'newIndex':4})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertEqual(list(session.presentations.order_by('order').values_list('pk',flat=True)),_sppk_at(sppk,[2,3,5,4,1]))
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.send_update.called)
self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_args, call(session))
mock_slides_manager_cls.reset_mock()
r = self.client.post(url, {'oldIndex':3, 'newIndex':2})
self.assertEqual(r.status_code, 200)
self.assertEqual(r.json()['success'],True)
self.assertEqual(list(session.presentations.order_by('order').values_list('pk',flat=True)),_sppk_at(sppk,[2,5,3,4,1]))
self.assertTrue(mock_slides_manager_cls.called)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertTrue(mock_slides_manager_cls.return_value.send_update.called)
self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_args, call(session))
mock_slides_manager_cls.reset_mock()
# Reset for next iteration in the loop
session.presentations.update(order=F('pk'))
self.client.logout()
def test_slide_order_reconditioning(self):
chair_role = RoleFactory(name_id='chair')
session = SessionFactory(group=chair_role.group, meeting__date=date_today() - datetime.timedelta(days=90))
sp_list = SessionPresentationFactory.create_batch(5, document__type_id='slides', session=session)
for num, sp in enumerate(sp_list, start=1):
sp.order = 2*num
sp.save()
try:
condition_slide_order(session)
except AssertionError:
pass
self.assertEqual(list(session.presentations.order_by('order').values_list('order',flat=True)),list(range(1,6)))
class EditTests(TestCase):
"""Test schedule edit operations"""
def test_official_record_schedule_is_read_only(self):
def _set_date_offset_and_retrieve_page(meeting, days_offset, client):
meeting.date = date_today() + datetime.timedelta(days=days_offset)
meeting.save()
client.login(username="secretary", password="secretary+password")
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number))
r = client.get(url)
q = PyQuery(r.content)
return(r, q)
# Setup
####################################################################################
# Basic test data
meeting = make_meeting_test_data()
# Set the secretary as the owner of the schedule
schedule = meeting.schedule
schedule.owner = Person.objects.get(user__username="secretary")
schedule.save()
# Tests
####################################################################################
# 1) Check that we get told the page is not editable
#######################################################
r, q = _set_date_offset_and_retrieve_page(meeting,
0 - 2 - meeting.days, # Meeting ended 2 days ago
self.client)
self.assertTrue(q(""".alert:contains("You can't edit this schedule")"""))
self.assertTrue(q(""".alert:contains("This is the official schedule for a meeting in the past")"""))
# 2) An ongoing meeting
#######################################################
r, q = _set_date_offset_and_retrieve_page(meeting,
0, # Meeting starts today
self.client)
self.assertFalse(q(""".alert:contains("You can't edit this schedule")"""))
self.assertFalse(q(""".alert:contains("This is the official schedule for a meeting in the past")"""))
# 3) A meeting in the future
#######################################################
r, q = _set_date_offset_and_retrieve_page(meeting,
7, # Meeting starts next week
self.client)
self.assertFalse(q(""".alert:contains("You can't edit this schedule")"""))
self.assertFalse(q(""".alert:contains("This is the official schedule for a meeting in the past")"""))
def test_edit_meeting_schedule(self):
meeting = make_meeting_test_data()
self.client.login(username="secretary", password="secretary+password")
s1 = Session.objects.filter(meeting=meeting, type='regular').first()
s2 = Session.objects.filter(meeting=meeting, type='regular').exclude(group=s1.group).first()
s1.comments = "Hello world!"
s1.attendees = 1234
s1.save()
Constraint.objects.create(
meeting=meeting,
source=s1.group,
target=s2.group,
name=ConstraintName.objects.get(slug="conflict"),
)
p = Person.objects.order_by('pk')[1]
Constraint.objects.create(
meeting=meeting,
source=s1.group,
person=p,
name=ConstraintName.objects.get(slug="bethere"),
)
Constraint.objects.create(
meeting=meeting,
source=s2.group,
person=p,
name=ConstraintName.objects.get(slug="bethere"),
)
room = Room.objects.get(meeting=meeting, session_types='regular')
base_timeslot = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room,
duration=datetime.timedelta(minutes=50),
time=meeting.tz().localize(
datetime.datetime.combine(meeting.date + datetime.timedelta(days=2), datetime.time(9, 30))
))
timeslots = list(TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('time'))
base_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="irg"),
attendees=20, requested_duration=datetime.timedelta(minutes=30),
add_to_schedule=False)
SchedulingEvent.objects.create(session=base_session, status_id='schedw', by=Person.objects.get(user__username='secretary'))
SchedTimeSessAssignment.objects.create(timeslot=base_timeslot, session=base_session, schedule=meeting.schedule.base)
# check we have the grid and everything set up as a baseline -
# the Javascript tests check that the Javascript can work with
# it
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number))
r = self.client.get(url)
q = PyQuery(r.content)
self.assertTrue(q(".room-name:contains(\"{}\")".format(room.name)))
self.assertTrue(q(".room-name:contains(\"{}\")".format(room.capacity)))
self.assertTrue(q("#timeslot{}".format(timeslots[0].pk)))
for s in [s1, s2]:
e = q("#session{}".format(s.pk))
# should be link to edit/cancel session
edit_session_url = urlreverse(
'ietf.meeting.views.edit_session', kwargs={'session_id': s.pk}
) + f'?sched={meeting.schedule.pk}'
self.assertTrue(
e.find(f'a[href="{edit_session_url}"]')
)
self.assertTrue(
e.find('a[href="{}?sched={}"]'.format(
urlreverse('ietf.meeting.views.cancel_session', kwargs={'session_id': s.pk}),
meeting.schedule.pk,
))
)
# info in the item representing the session that can be moved around
self.assertIn(s.group.acronym, e.find(".session-label").text())
if s.comments:
self.assertTrue(e.find(".comments"))
if s.attendees is not None:
self.assertIn(str(s.attendees), e.find(".attendees").text())
self.assertTrue(e.hasClass("parent-{}".format(s.group.parent.acronym)))
constraints = e.find(".constraints > span")
s_other = s2 if s == s1 else s1
self.assertEqual(len(constraints), 3)
self.assertEqual(constraints.eq(0).attr("data-sessions"), str(s_other.pk))
self.assertEqual(constraints.eq(0).find(".bi-people-fill").parent().text(), "1") # 1 person in the constraint
self.assertEqual(constraints.eq(1).attr("data-sessions"), str(s_other.pk))
self.assertEqual(constraints.eq(1).find(".encircled").text(), "1" if s_other == s2 else "-1")
self.assertEqual(constraints.eq(2).attr("data-sessions"), str(s_other.pk))
self.assertEqual(constraints.eq(2).find(".encircled").text(), "AD")
# session info for the panel
self.assertIn(str(round(s.requested_duration.total_seconds() / 60.0 / 60, 1)), e.find(".session-info .title").text())
event = SchedulingEvent.objects.filter(session=s).order_by("id").first()
if event:
self.assertTrue(e.find("div:contains(\"{}\")".format(event.by.name)))
if s.comments:
self.assertIn(s.comments, e.find(".comments").text())
formatted_constraints1 = q("#session{} .session-info .formatted-constraints > *".format(s1.pk))
self.assertIn(s2.group.acronym, formatted_constraints1.eq(0).html())
self.assertIn(p.name, formatted_constraints1.eq(1).html())
formatted_constraints2 = q("#session{} .session-info .formatted-constraints > *".format(s2.pk))
self.assertIn(p.name, formatted_constraints2.eq(0).html())
self.assertEqual(len(q("#session{}.readonly".format(base_session.pk))), 1)
self.assertTrue(q(".alert:contains(\"You can't edit this schedule\")"))
# can't change anything
r = self.client.post(url, {
'action': 'assign',
'timeslot': timeslots[0].pk,
'session': s1.pk,
})
self.assertEqual(r.status_code, 403)
# turn us into owner
schedule = meeting.schedule
schedule.owner = Person.objects.get(user__username="secretary")
schedule.save()
meeting.schedule = None
meeting.save()
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=schedule.owner_email(), name=schedule.name))
r = self.client.get(url)
q = PyQuery(r.content)
self.assertTrue(not q("em:contains(\"You can't edit this schedule\")"))
SchedTimeSessAssignment.objects.filter(session=s1).delete()
# assign
r = self.client.post(url, {
'action': 'assign',
'timeslot': timeslots[0].pk,
'session': s1.pk,
})
self.assertEqual(json.loads(r.content)['success'], True)
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[0])
# move assignment on unofficial schedule
r = self.client.post(url, {
'action': 'assign',
'timeslot': timeslots[1].pk,
'session': s1.pk,
})
self.assertEqual(json.loads(r.content)['success'], True)
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[1])
# move assignment on official schedule, leaving tombstone
meeting.schedule = schedule
meeting.save()
SchedulingEvent.objects.create(
session=s1,
status=SessionStatusName.objects.get(slug='sched'),
by=Person.objects.get(name='(System)')
)
r = self.client.post(url, {
'action': 'assign',
'timeslot': timeslots[0].pk,
'session': s1.pk,
})
json_content = json.loads(r.content)
self.assertEqual(json_content['success'], True)
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[0])
sessions_for_group = Session.objects.filter(group=s1.group, meeting=meeting)
self.assertEqual(len(sessions_for_group), 2)
s_tombstone = [s for s in sessions_for_group if s != s1][0]
self.assertEqual(s_tombstone.tombstone_for, s1)
tombstone_event = SchedulingEvent.objects.get(session=s_tombstone)
self.assertEqual(tombstone_event.status_id, 'resched')
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s_tombstone).timeslot, timeslots[1])
self.assertTrue(PyQuery(json_content['tombstone'])("#session{}.readonly".format(s_tombstone.pk)).html())
# unassign
r = self.client.post(url, {
'action': 'unassign',
'session': s1.pk,
})
self.assertEqual(json.loads(r.content)['success'], True)
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1)), [])
# try swapping days
SchedTimeSessAssignment.objects.create(schedule=schedule, session=s1, timeslot=timeslots[0])
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[0])), 1)
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[1])), 1)
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), [])
r = self.client.post(url, {
'action': 'swapdays',
'source_day': timeslots[0].time.date().isoformat(),
'target_day': timeslots[2].time.date().isoformat(),
})
self.assertEqual(r.status_code, 302)
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[0])), [])
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[1])), [])
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[2])), 1)
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2)), [])
# swap back
r = self.client.post(url, {
'action': 'swapdays',
'source_day': timeslots[2].time.date().isoformat(),
'target_day': timeslots[0].time.date().isoformat(),
})
self.assertEqual(r.status_code, 302)
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[0])), 1)
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[1])), [])
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), [])
def test_edit_meeting_timeslots_and_misc_sessions(self):
meeting = make_meeting_test_data()
self.client.login(username="secretary", password="secretary+password")
# check we have the grid and everything set up as a baseline -
# the Javascript tests check that the Javascript can work with
# it
url = urlreverse("ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions", kwargs=dict(num=meeting.number, owner=meeting.schedule.base.owner_email(), name=meeting.schedule.base.name))
r = self.client.get(url)
q = PyQuery(r.content)
breakfast_room = Room.objects.get(meeting=meeting, name="Breakfast Room")
break_room = Room.objects.get(meeting=meeting, name="Break Area")
reg_room = Room.objects.get(meeting=meeting, name="Registration Area")
for i in range(meeting.days):
self.assertTrue(q("[data-day=\"{}\"]".format((meeting.date + datetime.timedelta(days=i)).isoformat())))
self.assertTrue(q(".room-label:contains(\"{}\")".format(breakfast_room.name)))
self.assertTrue(q(".room-label:contains(\"{}\")".format(break_room.name)))
self.assertTrue(q(".room-label:contains(\"{}\")".format(reg_room.name)))
break_slot = TimeSlot.objects.get(location=break_room, type='break')
room_row = q(".room-row[data-day=\"{}\"][data-room=\"{}\"]".format(break_slot.time.date().isoformat(), break_slot.location_id))
self.assertTrue(room_row)
self.assertTrue(room_row.find("#timeslot{}".format(break_slot.pk)))
self.assertTrue(q(".timeslot-form"))
# add timeslot
ietf_group = Group.objects.get(acronym='ietf')
r = self.client.post(url, {
'day': meeting.date,
'time': '08:30',
'duration': '1:30',
'location': break_room.pk,
'show_location': 'on',
'type': 'other',
'group': ietf_group.pk,
'name': "IETF Testing",
'short': "ietf-testing",
'scroll': 1234,
'action': 'add-timeslot',
})
self.assertNoFormPostErrors(r)
self.assertIn("#scroll=1234", r['Location'])
test_timeslot = TimeSlot.objects.get(meeting=meeting, name="IETF Testing")
self.assertEqual(
test_timeslot.time,
meeting.tz().localize(
datetime.datetime.combine(meeting.date, datetime.time(8, 30))
),
)
self.assertEqual(test_timeslot.duration, datetime.timedelta(hours=1, minutes=30))
self.assertEqual(test_timeslot.location_id, break_room.pk)
self.assertEqual(test_timeslot.show_location, True)
self.assertEqual(test_timeslot.type_id, 'other')
test_session = Session.objects.get(meeting=meeting, timeslotassignments__timeslot=test_timeslot)
self.assertEqual(test_session.short, 'ietf-testing')
self.assertEqual(test_session.group, ietf_group)
self.assertTrue(SchedulingEvent.objects.filter(session=test_session, status='sched'))
# edit timeslot
r = self.client.get(url, {
'timeslot': test_timeslot.pk,
'action': 'edit-timeslot',
})
self.assertEqual(r.status_code, 200)
edit_form_html = json.loads(r.content)['form']
q = PyQuery(edit_form_html)
self.assertEqual(q("[name=name]").val(), test_timeslot.name)
self.assertEqual(q("[name=location]").val(), str(test_timeslot.location_id))
self.assertEqual(q("[name=timeslot]").val(), str(test_timeslot.pk))
self.assertEqual(q("[name=type]").val(), str(test_timeslot.type_id))
self.assertEqual(q("[name=group]").val(), str(ietf_group.pk))
iab_group = Group.objects.get(acronym='iab')
r = self.client.post(url, {
'timeslot': test_timeslot.pk,
'day': meeting.date,
'time': '09:30',
'duration': '1:00',
'location': breakfast_room.pk,
'type': 'other',
'group': iab_group.pk,
'name': "IETF Testing 2",
'short': "ietf-testing2",
'action': 'edit-timeslot',
})
self.assertNoFormPostErrors(r)
test_timeslot.refresh_from_db()
self.assertEqual(
test_timeslot.time,
meeting.tz().localize(
datetime.datetime.combine(meeting.date, datetime.time(9, 30))
),
)
self.assertEqual(test_timeslot.duration, datetime.timedelta(hours=1))
self.assertEqual(test_timeslot.location_id, breakfast_room.pk)
self.assertEqual(test_timeslot.show_location, False)
self.assertEqual(test_timeslot.type_id, 'other')
test_session.refresh_from_db()
self.assertEqual(test_session.short, 'ietf-testing2')
self.assertEqual(test_session.group, iab_group)
# cancel timeslot
r = self.client.post(url, {
'timeslot': test_timeslot.pk,
'action': 'cancel-timeslot',
})
self.assertNoFormPostErrors(r)
event = SchedulingEvent.objects.filter(
session__timeslotassignments__timeslot=test_timeslot
).order_by('-id').first()
self.assertEqual(event.status_id, 'canceled')
# delete timeslot
test_presentation = Document.objects.create(name='slides-test', type_id='slides')
SessionPresentation.objects.create(
document=test_presentation,
rev='1',
session=test_session
)
r = self.client.post(url, {
'timeslot': test_timeslot.pk,
'action': 'delete-timeslot',
})
self.assertNoFormPostErrors(r)
self.assertEqual(list(TimeSlot.objects.filter(pk=test_timeslot.pk)), [])
self.assertEqual(list(Session.objects.filter(pk=test_session.pk)), [])
self.assertEqual(test_presentation.get_state_slug(), 'deleted')
# set agenda note
assignment = SchedTimeSessAssignment.objects.filter(session__group__acronym='mars', schedule=meeting.schedule).first()
url = urlreverse("ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
r = self.client.post(url, {
'timeslot': assignment.timeslot_id,
'day': assignment.timeslot.time.date().isoformat(),
'time': assignment.timeslot.time.time().isoformat(),
'duration': assignment.timeslot.duration,
'location': assignment.timeslot.location_id,
'type': assignment.slot_type().slug,
'name': assignment.timeslot.name,
'agenda_note': "New Test Note",
'action': 'edit-timeslot',
})
self.assertNoFormPostErrors(r)
assignment.session.refresh_from_db()
self.assertEqual(assignment.session.agenda_note, "New Test Note")
def test_edit_meeting_schedule_conflict_types(self):
"""The meeting schedule editor should show the constraint types enabled for the meeting"""
meeting = MeetingFactory(
type_id='ietf',
group_conflicts=[], # show none to start with
)
s1 = SessionFactory(
meeting=meeting,
type_id='regular',
attendees=12,
comments='chair conflict',
)
s2 = SessionFactory(
meeting=meeting,
type_id='regular',
attendees=34,
comments='old-fashioned conflict',
)
Constraint.objects.create(
meeting=meeting,
source=s1.group,
target=s2.group,
name=ConstraintName.objects.get(slug="chair_conflict"),
)
Constraint.objects.create(
meeting=meeting,
source=s2.group,
target=s1.group,
name=ConstraintName.objects.get(slug="conflict"),
)
# log in as secretary so we have access
self.client.login(username="secretary", password="secretary+password")
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number))
# Should have no conflict constraints listed because the meeting has all disabled
r = self.client.get(url)
q = PyQuery(r.content)
self.assertEqual(len(q('#session{} span.constraints > span'.format(s1.pk))), 0)
self.assertEqual(len(q('#session{} span.constraints > span'.format(s2.pk))), 0)
# Now enable the 'chair_conflict' constraint only
chair_conflict = ConstraintName.objects.get(slug='chair_conflict')
chair_conf_label = b'<i class="bi bi-circle-fill"/>' # result of etree.tostring(etree.fromstring(editor_label))
meeting.group_conflict_types.add(chair_conflict)
r = self.client.get(url)
q = PyQuery(r.content)
# verify that there is a constraint pointing from 1 to 2
#
# The constraint is represented in the HTML as
# <div id="session<pk>">
# [...]
# <span class="constraints">
# <span data-sessions="<other pk>">[constraint label]</span>
# </span>
# </div>
#
# Where the constraint label is the editor_label for the ConstraintName.
# If this pk is the constraint target, the editor_label includes a
# '-' prefix, which may be before the editor_label or inserted inside
# it.
#
# For simplicity, this test is tied to the current values of editor_label.
# It also assumes the order of constraints will be constant.
# If those change, the test will need to be updated.
s1_constraints = q('#session{} span.constraints > span'.format(s1.pk))
s2_constraints = q('#session{} span.constraints > span'.format(s2.pk))
# Check the forward constraint
self.assertEqual(len(s1_constraints), 1)
self.assertEqual(s1_constraints[0].attrib['data-sessions'], str(s2.pk))
self.assertEqual(s1_constraints[0].text, None) # no '-' prefix on the source
self.assertEqual(tostring(s1_constraints[0][0]), chair_conf_label) # [0][0] is the innermost <span>
# And the reverse constraint
self.assertEqual(len(s2_constraints), 1)
self.assertEqual(s2_constraints[0].attrib['data-sessions'], str(s1.pk))
self.assertEqual(s2_constraints[0].text, '-') # '-' prefix on the target
self.assertEqual(tostring(s2_constraints[0][0]), chair_conf_label) # [0][0] is the innermost <span>
# Now also enable the 'conflict' constraint
conflict = ConstraintName.objects.get(slug='conflict')
conf_label = b'<span class="encircled">1</span>'
conf_label_reversed = b'<span class="encircled">-1</span>' # the '-' is inside the span!
meeting.group_conflict_types.add(conflict)
r = self.client.get(url)
q = PyQuery(r.content)
s1_constraints = q('#session{} span.constraints > span'.format(s1.pk))
s2_constraints = q('#session{} span.constraints > span'.format(s2.pk))
# Check the forward constraint
self.assertEqual(len(s1_constraints), 2)
self.assertEqual(s1_constraints[0].attrib['data-sessions'], str(s2.pk))
self.assertEqual(s1_constraints[0].text, None) # no '-' prefix on the source
self.assertEqual(tostring(s1_constraints[0][0]), chair_conf_label) # [0][0] is the innermost <span>
self.assertEqual(s1_constraints[1].attrib['data-sessions'], str(s2.pk))
self.assertEqual(tostring(s1_constraints[1][0]), conf_label_reversed) # [0][0] is the innermost <span>
# And the reverse constraint
self.assertEqual(len(s2_constraints), 2)
self.assertEqual(s2_constraints[0].attrib['data-sessions'], str(s1.pk))
self.assertEqual(s2_constraints[0].text, '-') # '-' prefix on the target
self.assertEqual(tostring(s2_constraints[0][0]), chair_conf_label) # [0][0] is the innermost <span>
self.assertEqual(s2_constraints[1].attrib['data-sessions'], str(s1.pk))
self.assertEqual(tostring(s2_constraints[1][0]), conf_label) # [0][0] is the innermost <span>
def test_new_meeting_schedule(self):
"""Can create a meeting schedule from scratch"""
meeting = make_meeting_test_data()
self.client.login(username="secretary", password="secretary+password")
# new from scratch
url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.client.post(url, {
'name': "scratch",
'public': "on",
'visible': "on",
'notes': "New scratch",
'base': meeting.schedule.base_id,
})
self.assertNoFormPostErrors(r)
new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='scratch')
self.assertEqual(new_schedule.public, True)
self.assertEqual(new_schedule.visible, True)
self.assertEqual(new_schedule.notes, "New scratch")
self.assertEqual(new_schedule.origin, None)
self.assertEqual(new_schedule.base_id, meeting.schedule.base_id)
def test_copy_meeting_schedule(self):
"""Can create a copy of an existing meeting schedule"""
meeting = make_meeting_test_data()
self.client.login(username="secretary", password="secretary+password")
url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.client.post(url, {
'name': "copy",
'public': "on",
'notes': "New copy",
'base': meeting.schedule.base_id,
})
self.assertNoFormPostErrors(r)
new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='copy')
self.assertEqual(new_schedule.public, True)
self.assertEqual(new_schedule.visible, False)
self.assertEqual(new_schedule.notes, "New copy")
self.assertEqual(new_schedule.origin, meeting.schedule)
self.assertEqual(new_schedule.base_id, meeting.schedule.base_id)
old_assignments = {(a.session_id, a.timeslot_id) for a in SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule)}
for a in SchedTimeSessAssignment.objects.filter(schedule=new_schedule):
self.assertIn((a.session_id, a.timeslot_id), old_assignments)
def test_schedule_read_permissions(self):
meeting = make_meeting_test_data()
schedule = meeting.schedule
# try to get non-existing agenda
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number,
owner=schedule.owner_email(),
name="foo"))
r = self.client.get(url)
self.assertEqual(r.status_code, 404)
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number,
owner=schedule.owner_email(),
name=schedule.name))
self.client.login(username='ad', password='ad+password')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
schedule.visible = True
schedule.public = False
schedule.save()
# get as anonymous doesn't work
self.client.logout()
r = self.client.get(url)
self.assertEqual(r.status_code, 403)
# public, now anonymous works
schedule.public = True
schedule.save()
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# Secretariat can always see it
schedule.visible = False
schedule.public = False
schedule.save()
self.client.login(username="secretary", password="secretary+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
def test_new_meeting_schedule_rejects_invalid_names(self):
meeting = make_meeting_test_data()
orig_schedule_count = meeting.schedule_set.count()
self.client.login(username="ad", password="ad+password")
url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number))
r = self.client.post(url, {
'name': "/no/this/should/not/work/it/is/too/long",
'public': "on",
'notes': "Name too long",
'base': meeting.schedule.base_id,
})
self.assertEqual(r.status_code, 200)
self.assertFormError(r.context["form"], 'name', 'Enter a valid value.')
self.assertEqual(meeting.schedule_set.count(), orig_schedule_count, 'Schedule should not be created')
r = self.client.post(url, {
'name': "/invalid/chars/",
'public': "on",
'notes': "Name too long",
'base': meeting.schedule.base_id,
})
self.assertEqual(r.status_code, 200)
self.assertFormError(r.context["form"], 'name', 'Enter a valid value.')
self.assertEqual(meeting.schedule_set.count(), orig_schedule_count, 'Schedule should not be created')
# Non-ASCII alphanumeric characters
r = self.client.post(url, {
'name': "f\u00E9ling",
'public': "on",
'notes': "Name too long",
'base': meeting.schedule.base_id,
})
self.assertEqual(r.status_code, 200)
self.assertFormError(r.context["form"], 'name', 'Enter a valid value.')
self.assertEqual(meeting.schedule_set.count(), orig_schedule_count, 'Schedule should not be created')
def test_edit_session(self):
session = SessionFactory(meeting__type_id='ietf', group__type_id='team') # type determines allowed session purposes
edit_meeting_url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': session.meeting.number})
self.client.login(username='secretary', password='secretary+password')
url = urlreverse('ietf.meeting.views.edit_session', kwargs={'session_id': session.pk})
r = self.client.get(url)
self.assertContains(r, 'Edit session', status_code=200)
pq = PyQuery(r.content)
back_button = pq(f'a[href="{edit_meeting_url}"]')
self.assertEqual(len(back_button), 1)
post_data = {
'name': 'this is a name',
'short': 'tian',
'purpose': 'coding',
'type': 'other',
'requested_duration': '3600',
'on_agenda': True,
'remote_instructions': 'Do this do that',
'attendees': '103',
'comments': 'So much to say',
'chat_room': 'xyzzy',
}
r = self.client.post(url, post_data)
self.assertNoFormPostErrors(r)
self.assertRedirects(r, edit_meeting_url)
session = Session.objects.get(pk=session.pk) # refresh objects from DB
self.assertEqual(session.name, 'this is a name')
self.assertEqual(session.short, 'tian')
self.assertEqual(session.purpose_id, 'coding')
self.assertEqual(session.type_id, 'other')
self.assertEqual(session.requested_duration, datetime.timedelta(hours=1))
self.assertEqual(session.on_agenda, True)
self.assertEqual(session.remote_instructions, 'Do this do that')
self.assertEqual(session.attendees, 103)
self.assertEqual(session.comments, 'So much to say')
self.assertEqual(session.chat_room, 'xyzzy')
# Verify return to correct schedule when sched query parameter is present
other_schedule = ScheduleFactory(meeting=session.meeting)
r = self.client.get(url + f'?sched={other_schedule.pk}')
edit_meeting_url = urlreverse(
'ietf.meeting.views.edit_meeting_schedule',
kwargs={
'num': session.meeting.number,
'owner': other_schedule.owner.email(),
'name': other_schedule.name,
},
)
pq = PyQuery(r.content)
back_button = pq(f'a[href="{edit_meeting_url}"]')
self.assertEqual(len(back_button), 1)
r = self.client.post(url + f'?sched={other_schedule.pk}', post_data)
self.assertRedirects(r, edit_meeting_url)
def test_cancel_session(self):
# session for testing with official schedule
session = SessionFactory(meeting__type_id='ietf')
url = urlreverse('ietf.meeting.views.cancel_session', kwargs={'session_id': session.pk})
return_url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': session.meeting.number})
# session for testing with unofficial schedule
other_session = SessionFactory(meeting=session.meeting)
unofficial_schedule = ScheduleFactory(meeting=other_session.meeting)
url_unofficial = urlreverse(
'ietf.meeting.views.cancel_session',
kwargs={'session_id': other_session.pk},
) + f'?sched={unofficial_schedule.pk}'
return_url_unofficial = urlreverse(
'ietf.meeting.views.edit_meeting_schedule',
kwargs={
'num': other_session.meeting.number,
'name': unofficial_schedule.name,
'owner': unofficial_schedule.owner_email(),
},
)
login_testing_unauthorized(self, 'secretary', url)
r = self.client.get(url)
self.assertContains(r, 'Cancel session', status_code=200)
self.assertIn(return_url, r.content.decode())
r = self.client.get(url_unofficial)
self.assertContains(r, 'Cancel session', status_code=200)
self.assertIn(return_url_unofficial, r.content.decode())
r = self.client.post(url, {})
self.assertFormError(r.context["form"], 'confirmed', 'This field is required.')
r = self.client.post(url_unofficial, {})
self.assertFormError(r.context["form"], 'confirmed', 'This field is required.')
r = self.client.post(url, {'confirmed': 'on'})
self.assertRedirects(r, return_url)
session = Session.objects.with_current_status().get(pk=session.pk)
self.assertEqual(session.current_status, 'canceled')
r = self.client.get(url)
self.assertRedirects(r, return_url) # should redirect immediately when session is already canceled
r = self.client.post(url_unofficial, {'confirmed': 'on'})
self.assertRedirects(r, return_url_unofficial)
other_session = Session.objects.with_current_status().get(pk=other_session.pk)
self.assertEqual(other_session.current_status, 'canceled')
r = self.client.get(url_unofficial)
self.assertRedirects(r, return_url_unofficial) # should redirect immediately when session is already canceled
def test_edit_timeslots(self):
meeting = make_meeting_test_data()
self.client.login(username="secretary", password="secretary+password")
r = self.client.get(urlreverse("ietf.meeting.views.edit_timeslots", kwargs=dict(num=meeting.number)))
self.assertContains(r, meeting.room_set.all().first().name)
def test_edit_timeslot_type(self):
timeslot = TimeSlotFactory(meeting__type_id='ietf')
url = urlreverse('ietf.meeting.views.edit_timeslot_type', kwargs=dict(num=timeslot.meeting.number,slot_id=timeslot.id))
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
r = self.client.post(url,{'type':'other',})
self.assertEqual(r.status_code, 302)
timeslot = TimeSlot.objects.get(id=timeslot.id)
self.assertEqual(timeslot.type.slug,'other')
def test_slot_to_the_right(self):
meeting = make_meeting_test_data()
session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
mars_scheduled = session.timeslotassignments.get(schedule__name='test-schedule')
mars_slot = TimeSlot.objects.get(sessionassignments__session=session,sessionassignments__schedule__name='test-schedule')
mars_ends = mars_slot.time + mars_slot.duration
session = Session.objects.filter(meeting=meeting, group__acronym="ames").first()
ames_slot_qs = TimeSlot.objects.filter(sessionassignments__session=session,sessionassignments__schedule__name='test-schedule')
ames_slot_qs.update(time=mars_ends + datetime.timedelta(seconds=11 * 60))
self.assertTrue(not mars_slot.slot_to_the_right)
self.assertTrue(not mars_scheduled.slot_to_the_right)
ames_slot_qs.update(time=mars_ends + datetime.timedelta(seconds=10 * 60))
self.assertTrue(mars_slot.slot_to_the_right)
self.assertTrue(mars_scheduled.slot_to_the_right)
def test_updateview(self):
"""The updateview action should set visible timeslot types in the session"""
meeting = MeetingFactory(type_id='ietf')
url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number})
types_to_enable = ['regular', 'reg', 'other']
r = self.client.post(
url,
{
'action': 'updateview',
'enabled_timeslot_types[]': types_to_enable,
},
)
self.assertEqual(r.status_code, 200)
session_data = self.client.session
self.assertIn('edit_meeting_schedule', session_data)
self.assertCountEqual(
session_data['edit_meeting_schedule']['enabled_timeslot_types'],
types_to_enable,
'Should set types requested',
)
r = self.client.post(
url,
{
'action': 'updateview',
'enabled_timeslot_types[]': types_to_enable + ['faketype'],
},
)
self.assertEqual(r.status_code, 200)
session_data = self.client.session
self.assertIn('edit_meeting_schedule', session_data)
self.assertCountEqual(
session_data['edit_meeting_schedule']['enabled_timeslot_types'],
types_to_enable,
'Should ignore unknown types',
)
def test_persistent_enabled_timeslot_types(self):
meeting = MeetingFactory(type_id='ietf')
TimeSlotFactory(meeting=meeting, type_id='other')
TimeSlotFactory(meeting=meeting, type_id='reg')
# test default behavior (only 'regular' enabled)
r = self.client.get(urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number}))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="regular"][checked]')), 1)
self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="other"]:not([checked])')), 1)
self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="reg"]:not([checked])')), 1)
# test with 'regular' and 'other' enabled via session store
client_session = self.client.session # must store as var, new session is created on access
client_session['edit_meeting_schedule'] = {
'enabled_timeslot_types': ['regular', 'other']
}
client_session.save()
r = self.client.get(urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number}))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="regular"][checked]')), 1)
self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="other"][checked]')), 1)
self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="reg"]:not([checked])')), 1)
class SessionDetailsTests(TestCase):
def test_session_details(self):
group = GroupFactory.create(type_id='wg',state_id='active')
session = SessionFactory.create(meeting__type_id='ietf',group=group, meeting__date=date_today() + datetime.timedelta(days=90))
SessionPresentationFactory.create(session=session,document__type_id='draft',rev=None)
SessionPresentationFactory.create(session=session,document__type_id='minutes')
SessionPresentationFactory.create(session=session,document__type_id='slides')
SessionPresentationFactory.create(session=session,document__type_id='agenda')
url = urlreverse('ietf.meeting.views.session_details', kwargs=dict(num=session.meeting.number, acronym=group.acronym))
r = self.client.get(url)
self.assertTrue(all([x in unicontent(r) for x in ('slides','agenda','minutes','draft')]))
self.assertNotContains(r, 'deleted')
def test_session_details_has_import_minutes_buttons(self):
group = GroupFactory.create(
type_id='wg',
state_id='active',
)
session = SessionFactory.create(
meeting__type_id='ietf',
group=group,
meeting__date=date_today() + datetime.timedelta(days=90),
)
session_details_url = urlreverse(
'ietf.meeting.views.session_details',
kwargs={'num': session.meeting.number, 'acronym': group.acronym},
)
import_minutes_url = urlreverse(
'ietf.meeting.views.import_session_minutes',
kwargs={'num': session.meeting.number, 'session_id': session.pk},
)
# test without existing minutes
with patch('ietf.meeting.views.can_manage_session_materials', return_value=False):
r = self.client.get(session_details_url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(
len(q(f'a[href="{import_minutes_url}"]')), 0,
'Do not show import new minutes buttons to non-materials manager',
)
with patch('ietf.meeting.views.can_manage_session_materials', return_value=True):
r = self.client.get(session_details_url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertGreater(
len(q(f'a[href="{import_minutes_url}"]')), 0,
'Show import new minutes buttons to materials manager',
)
# now create minutes and test that we can still have the import button
SessionPresentationFactory.create(session=session,document__type_id='minutes')
with patch('ietf.meeting.views.can_manage_session_materials', return_value=False):
r = self.client.get(session_details_url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(
len(q(f'a[href="{import_minutes_url}"]')), 0,
'Do not show import revised minutes buttons to non-materials manager',
)
with patch('ietf.meeting.views.can_manage_session_materials', return_value=True):
r = self.client.get(session_details_url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertGreater(
len(q(f'a[href="{import_minutes_url}"]')), 0,
'Show import revised minutes buttons to materials manager',
)
def test_session_details_past_interim(self):
group = GroupFactory.create(type_id='wg',state_id='active')
chair = RoleFactory(name_id='chair',group=group)
session = SessionFactory.create(meeting__type_id='interim',group=group, meeting__date=date_today() - datetime.timedelta(days=90))
SessionPresentationFactory.create(session=session,document__type_id='draft',rev=None)
SessionPresentationFactory.create(session=session,document__type_id='minutes')
SessionPresentationFactory.create(session=session,document__type_id='slides')
SessionPresentationFactory.create(session=session,document__type_id='agenda')
url = urlreverse('ietf.meeting.views.session_details', kwargs=dict(num=session.meeting.number, acronym=group.acronym))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertNotIn('The materials upload cutoff date for this session has passed', unicontent(r))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.client.login(username=chair.person.user.username,password=chair.person.user.username+'+password')
self.assertTrue(all([x in unicontent(r) for x in ('slides','agenda','minutes','draft')]))
def test_add_session_drafts(self):
group = GroupFactory.create(type_id='wg',state_id='active')
group_chair = PersonFactory.create()
group.role_set.create(name_id='chair',person = group_chair, email = group_chair.email())
session = SessionFactory.create(meeting__type_id='ietf',group=group, meeting__date=date_today() + datetime.timedelta(days=90))
SessionPresentationFactory.create(session=session,document__type_id='draft',rev=None)
old_draft = session.presentations.filter(document__type='draft').first().document
new_draft = DocumentFactory(type_id='draft')
url = urlreverse('ietf.meeting.views.add_session_drafts', kwargs=dict(num=session.meeting.number, session_id=session.pk))
r = self.client.get(url)
self.assertEqual(r.status_code, 404)
self.client.login(username="plain",password="plain+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 404)
self.client.login(username=group_chair.user.username, password='%s+password'%group_chair.user.username)
r = self.client.get(url)
self.assertContains(r, old_draft.name)
r = self.client.post(url,dict(drafts=[new_draft.pk, old_draft.pk]))
self.assertTrue(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn("Already linked:", q('form .text-danger').text())
self.assertEqual(1,session.presentations.count())
r = self.client.post(url,dict(drafts=[new_draft.pk,]))
self.assertTrue(r.status_code, 302)
self.assertEqual(2,session.presentations.count())
session.meeting.date -= datetime.timedelta(days=180)
session.meeting.save()
r = self.client.get(url)
self.assertEqual(r.status_code,404)
self.client.login(username='secretary',password='secretary+password')
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(1,len(q(".alert-warning:contains('may affect published proceedings')")))
class EditScheduleListTests(TestCase):
def setUp(self):
super().setUp()
self.mtg = MeetingFactory(type_id='ietf')
ScheduleFactory(meeting=self.mtg, name='secretary1')
def test_list_schedules(self):
url = urlreverse('ietf.meeting.views.list_schedules',kwargs={'num':self.mtg.number})
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertTrue(r.status_code, 200)
def test_diff_schedules(self):
meeting = make_meeting_test_data()
url = urlreverse('ietf.meeting.views.diff_schedules',kwargs={'num':meeting.number})
login_testing_unauthorized(self,"secretary", url)
r = self.client.get(url)
self.assertTrue(r.status_code, 200)
from_schedule = Schedule.objects.get(meeting=meeting, name="test-unofficial-schedule")
session1 = Session.objects.filter(meeting=meeting, group__acronym='mars').first()
session2 = Session.objects.filter(meeting=meeting, group__acronym='ames').first()
session3 = SessionFactory(meeting=meeting, group=Group.objects.get(acronym='mars'),
attendees=10, requested_duration=datetime.timedelta(minutes=70),
add_to_schedule=False)
SchedulingEvent.objects.create(session=session3, status_id='schedw', by=Person.objects.first())
slot2 = TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('-time').first()
slot3 = TimeSlot.objects.create(
meeting=meeting, type_id='regular', location=slot2.location,
duration=datetime.timedelta(minutes=60),
time=slot2.time + datetime.timedelta(minutes=60),
)
# copy
new_url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number, owner=from_schedule.owner_email(), name=from_schedule.name))
r = self.client.post(new_url, {
'name': "newtest",
'public': "on",
})
self.assertNoFormPostErrors(r)
to_schedule = Schedule.objects.get(meeting=meeting, name='newtest')
# make some changes
edit_url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=to_schedule.owner_email(), name=to_schedule.name))
# schedule session
r = self.client.post(edit_url, {
'action': 'assign',
'timeslot': slot3.pk,
'session': session3.pk,
})
self.assertEqual(json.loads(r.content)['success'], True)
# unschedule session
r = self.client.post(edit_url, {
'action': 'unassign',
'session': session1.pk,
})
self.assertEqual(json.loads(r.content)['success'], True)
# move session
r = self.client.post(edit_url, {
'action': 'assign',
'timeslot': slot2.pk,
'session': session2.pk,
})
self.assertEqual(json.loads(r.content)['success'], True)
# now get differences
r = self.client.get(url, {
'from_schedule': from_schedule.name,
'to_schedule': to_schedule.name,
})
self.assertTrue(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q(".schedule-diffs tr")), 3+1)
def test_delete_schedule(self):
url = urlreverse('ietf.meeting.views.delete_schedule',
kwargs={'num':self.mtg.number,
'owner':self.mtg.schedule.owner.email_address(),
'name':self.mtg.schedule.name,
})
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertTrue(r.status_code, 403)
r = self.client.post(url,{'save':1})
self.assertTrue(r.status_code, 403)
self.assertEqual(self.mtg.schedule_set.count(),2)
self.mtg.schedule=None
self.mtg.save()
r = self.client.get(url)
self.assertTrue(r.status_code, 200)
r = self.client.post(url,{'save':1})
self.assertTrue(r.status_code, 302)
self.assertEqual(self.mtg.schedule_set.count(),1)
def test_make_schedule_official(self):
schedule = self.mtg.schedule_set.exclude(id=self.mtg.schedule.id).first()
url = urlreverse('ietf.meeting.views.make_schedule_official',
kwargs={'num':self.mtg.number,
'owner':schedule.owner.email_address(),
'name':schedule.name,
})
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertTrue(r.status_code, 200)
r = self.client.post(url,{'save':1})
self.assertTrue(r.status_code, 302)
mtg = Meeting.objects.get(number=self.mtg.number)
self.assertEqual(mtg.schedule,schedule)
# -------------------------------------------------
# Interim Meeting Tests
# -------------------------------------------------
class InterimTests(TestCase):
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH']
# test_interim_announce subsumed by test_appears_on_announce
def do_interim_skip_announcement_test(self, base_session=False, extra_session=False, canceled_session=False):
make_meeting_test_data()
group = Group.objects.get(acronym='irg')
date = date_today() + datetime.timedelta(days=30)
meeting = make_interim_meeting(group=group, date=date, status='scheda')
session = meeting.session_set.first()
if base_session:
base_session = SessionFactory(meeting=meeting, status_id='apprw', add_to_schedule=False)
meeting.schedule.base = Schedule.objects.create(
meeting=meeting, owner=PersonFactory(), name="base", visible=True, public=True
)
SchedTimeSessAssignment.objects.create(
timeslot=TimeSlotFactory.create(meeting=meeting),
session=base_session,
schedule=meeting.schedule.base,
)
meeting.schedule.save()
if extra_session:
extra_session = SessionFactory(meeting=meeting, status_id='scheda')
if canceled_session:
canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa')
url = urlreverse("ietf.meeting.views.interim_skip_announcement", kwargs={'number': meeting.number})
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# check post
len_before = len(outbox)
r = self.client.post(url)
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_announce'))
meeting_sessions = meeting.session_set.with_current_status()
self.assertEqual(meeting_sessions.get(pk=session.pk).current_status, 'sched')
if base_session:
self.assertEqual(meeting_sessions.get(pk=base_session.pk).current_status, 'sched')
if extra_session:
self.assertEqual(meeting_sessions.get(pk=extra_session.pk).current_status, 'sched')
if canceled_session:
self.assertEqual(meeting_sessions.get(pk=canceled_session.pk).current_status, 'canceledpa')
self.assertEqual(len(outbox), len_before)
def test_interim_skip_announcement(self):
"""skip_announcement should move single session to sched state"""
self.do_interim_skip_announcement_test()
def test_interim_skip_announcement_with_base_sched(self):
"""skip_announcement should move single session to sched state"""
self.do_interim_skip_announcement_test(base_session=True)
def test_interim_skip_announcement_with_extra_session(self):
"""skip_announcement should move multiple sessions to sched state"""
self.do_interim_skip_announcement_test(extra_session=True)
def test_interim_skip_announcement_with_extra_session_and_base_sched(self):
"""skip_announcement should move multiple sessions to sched state"""
self.do_interim_skip_announcement_test(extra_session=True, base_session=True)
def test_interim_skip_announcement_with_canceled_session(self):
"""skip_announcement should not schedule a canceled session"""
self.do_interim_skip_announcement_test(canceled_session=True)
def test_interim_skip_announcement_with_canceled_session_and_base_sched(self):
"""skip_announcement should not schedule a canceled session"""
self.do_interim_skip_announcement_test(canceled_session=True, base_session=True)
def test_interim_skip_announcement_with_extra_and_canceled_sessions(self):
"""skip_announcement should schedule multiple sessions and leave canceled session alone"""
self.do_interim_skip_announcement_test(extra_session=True, canceled_session=True)
def test_interim_skip_announcement_with_extra_and_canceled_sessions_and_base_sched(self):
"""skip_announcement should schedule multiple sessions and leave canceled session alone"""
self.do_interim_skip_announcement_test(extra_session=True, canceled_session=True, base_session=True)
def do_interim_send_announcement_test(self, base_session=False, extra_session=False, canceled_session=False):
make_interim_test_data(meeting_tz='America/Los_Angeles')
session = Session.objects.with_current_status().filter(
meeting__type='interim', group__acronym='mars', current_status='apprw').first()
meeting = session.meeting
if base_session:
base_session = SessionFactory(meeting=meeting, status_id='apprw', add_to_schedule=False)
meeting.schedule.base = Schedule.objects.create(
meeting=meeting, owner=PersonFactory(), name="base", visible=True, public=True
)
SchedTimeSessAssignment.objects.create(
timeslot=TimeSlotFactory.create(meeting=meeting),
session=base_session,
schedule=meeting.schedule.base,
)
meeting.schedule.save()
if extra_session:
extra_session = SessionFactory(meeting=meeting, status_id='apprw')
if canceled_session:
canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa')
url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number': meeting.number})
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
initial = r.context['form'].initial
# send announcement
len_before = len(outbox)
r = self.client.post(url, initial)
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_announce'))
self.assertEqual(len(outbox), len_before + 1)
announcement_msg = outbox[-1]
announcement_text = get_payload_text(announcement_msg)
self.assertIn('WG Virtual Meeting', announcement_msg['Subject'])
self.assertIn('09:00 to 09:20 America/Los_Angeles', announcement_text)
for sess in [session, base_session, extra_session]:
if sess:
timeslot = sess.official_timeslotassignment().timeslot
self.assertIn(timeslot.time.strftime('%Y-%m-%d'), announcement_text)
self.assertRegex(
announcement_text,
r'(%s\s+to\s+%s\s+UTC)' % (
timeslot.utc_start_time().strftime('%H:%M'),timeslot.utc_end_time().strftime('%H:%M')
))
# Count number of sessions listed
if base_session and extra_session:
expected_session_matches = 3
elif base_session or extra_session:
expected_session_matches = 2
else:
expected_session_matches = 0 # no session list when only one session
session_matches = re.findall(r'Session \d+:', announcement_text)
self.assertEqual(len(session_matches), expected_session_matches)
meeting_sessions = meeting.session_set.with_current_status()
self.assertEqual(meeting_sessions.get(pk=session.pk).current_status, 'sched')
if base_session:
self.assertEqual(meeting_sessions.get(pk=base_session.pk).current_status, 'sched')
if extra_session:
self.assertEqual(meeting_sessions.get(pk=extra_session.pk).current_status, 'sched')
if canceled_session:
self.assertEqual(meeting_sessions.get(pk=canceled_session.pk).current_status, 'canceledpa')
def test_interim_send_announcement(self):
self.do_interim_send_announcement_test()
def test_interim_send_announcement_with_base_sched(self):
self.do_interim_send_announcement_test(base_session=True)
def test_interim_send_announcement_with_extra_session(self):
self.do_interim_send_announcement_test(extra_session=True)
def test_interim_send_announcement_with_extra_session_and_base_sched(self):
self.do_interim_send_announcement_test(extra_session=True, base_session=True)
def test_interim_send_announcement_with_canceled_session(self):
self.do_interim_send_announcement_test(canceled_session=True)
def test_interim_send_announcement_with_canceled_session_and_base_sched(self):
self.do_interim_send_announcement_test(canceled_session=True, base_session=True)
def test_interim_send_announcement_with_extra_and_canceled_sessions(self):
self.do_interim_send_announcement_test(extra_session=True, canceled_session=True)
def test_interim_send_announcement_with_extra_and_canceled_sessions_and_base_sched(self):
self.do_interim_send_announcement_test(extra_session=True, canceled_session=True, base_session=True)
def do_interim_approve_by_ad_test(self, base_session=False, extra_session=False, canceled_session=False):
make_interim_test_data()
session = Session.objects.with_current_status().filter(
meeting__type='interim', group__acronym='mars', current_status='apprw').first()
meeting = session.meeting
if base_session:
base_session = SessionFactory(meeting=meeting, status_id='apprw', add_to_schedule=False)
meeting.schedule.base = Schedule.objects.create(
meeting=meeting, owner=PersonFactory(), name="base", visible=True, public=True
)
SchedTimeSessAssignment.objects.create(
timeslot=TimeSlotFactory.create(meeting=meeting),
session=base_session,
schedule=meeting.schedule.base,
)
meeting.schedule.save()
if extra_session:
extra_session = SessionFactory(meeting=meeting, status_id='apprw')
if canceled_session:
canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa')
url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})
length_before = len(outbox)
login_testing_unauthorized(self, "ad", url)
r = self.client.post(url, {'approve': 'approve'})
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_pending'))
for sess in [session, base_session, extra_session]:
if sess:
self.assertEqual(Session.objects.with_current_status().get(pk=sess.pk).current_status,
'scheda')
if canceled_session:
self.assertEqual(Session.objects.with_current_status().get(pk=canceled_session.pk).current_status,
'canceledpa')
self.assertEqual(len(outbox), length_before + 1)
self.assertIn('ready for announcement', outbox[-1]['Subject'])
def test_interim_approve_by_ad(self):
self.do_interim_approve_by_ad_test()
def test_interim_approve_by_ad_with_base_sched(self):
self.do_interim_approve_by_ad_test(base_session=True)
def test_interim_approve_by_ad_with_extra_session(self):
self.do_interim_approve_by_ad_test(extra_session=True)
def test_interim_approve_by_ad_with_extra_session_and_base_sched(self):
self.do_interim_approve_by_ad_test(extra_session=True, base_session=True)
def test_interim_approve_by_ad_with_canceled_session(self):
self.do_interim_approve_by_ad_test(canceled_session=True)
def test_interim_approve_by_ad_with_canceled_session_and_base_sched(self):
self.do_interim_approve_by_ad_test(canceled_session=True, base_session=True)
def test_interim_approve_by_ad_with_extra_and_canceled_sessions(self):
self.do_interim_approve_by_ad_test(extra_session=True, canceled_session=True)
def test_interim_approve_by_ad_with_extra_and_canceled_sessions_and_base_sched(self):
self.do_interim_approve_by_ad_test(extra_session=True, canceled_session=True, base_session=True)
def do_interim_approve_by_secretariat_test(self, base_session=False, extra_session=False, canceled_session=False):
make_interim_test_data()
session = Session.objects.with_current_status().filter(
meeting__type='interim', group__acronym='mars', current_status='apprw').first()
meeting = session.meeting
if base_session:
base_session = SessionFactory(meeting=meeting, status_id='apprw', add_to_schedule=False)
meeting.schedule.base = Schedule.objects.create(
meeting=meeting, owner=PersonFactory(), name="base", visible=True, public=True
)
SchedTimeSessAssignment.objects.create(
timeslot=TimeSlotFactory.create(meeting=meeting),
session=base_session,
schedule=meeting.schedule.base,
)
meeting.schedule.save()
if extra_session:
extra_session = SessionFactory(meeting=meeting, status_id='apprw')
if canceled_session:
canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa')
url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})
length_before = len(outbox)
login_testing_unauthorized(self, "secretary", url)
r = self.client.post(url, {'approve': 'approve'})
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_send_announcement', kwargs={'number': meeting.number}))
for sess in [session, base_session, extra_session]:
if sess:
self.assertEqual(Session.objects.with_current_status().get(pk=sess.pk).current_status,
'scheda')
if canceled_session:
self.assertEqual(Session.objects.with_current_status().get(pk=canceled_session.pk).current_status,
'canceledpa')
self.assertEqual(len(outbox), length_before)
def test_interim_approve_by_secretariat(self):
self.do_interim_approve_by_secretariat_test()
def test_interim_approve_by_secretariat_with_base_sched(self):
self.do_interim_approve_by_secretariat_test(base_session=True)
def test_interim_approve_by_secretariat_with_extra_session(self):
self.do_interim_approve_by_secretariat_test(extra_session=True)
def test_interim_approve_by_secretariat_with_extra_session_and_base_sched(self):
self.do_interim_approve_by_secretariat_test(extra_session=True, base_session=True)
def test_interim_approve_by_secretariat_with_canceled_session(self):
self.do_interim_approve_by_secretariat_test(canceled_session=True)
def test_interim_approve_by_secretariat_with_canceled_session_and_base_sched(self):
self.do_interim_approve_by_secretariat_test(canceled_session=True, base_session=True)
def test_interim_approve_by_secretariat_with_extra_and_canceled_sessions(self):
self.do_interim_approve_by_secretariat_test(extra_session=True, canceled_session=True)
def test_interim_approve_by_secretariat_with_extra_and_canceled_sessions_and_base_sched(self):
self.do_interim_approve_by_secretariat_test(extra_session=True, canceled_session=True, base_session=True)
def test_past(self):
today = date_today()
last_week = today - datetime.timedelta(days=7)
ietf = SessionFactory(meeting__type_id='ietf',meeting__date=last_week,group__state_id='active',group__parent=GroupFactory(state_id='active'))
SessionFactory(meeting__type_id='interim',meeting__date=last_week,status_id='canceled',group__state_id='active',group__parent=GroupFactory(state_id='active'))
url = urlreverse('ietf.meeting.views.past')
r = self.client.get(url)
self.assertContains(r, 'IETF-%02d'%int(ietf.meeting.number))
q = PyQuery(r.content)
#id="-%s" % interim.group.acronym
#self.assertIn('Cancelled', q('[id*="'+id+'"]').text())
self.assertIn('Cancelled', q('tr>td>a+span').text())
def do_upcoming_test(self, querystring=None, create_meeting=True):
if create_meeting:
make_meeting_test_data(create_interims=True)
url = urlreverse("ietf.meeting.views.upcoming")
if querystring is not None:
url += '?' + querystring
today = date_today()
interims = dict(
mars=add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='mars')).filter(current_status='sched').first().meeting,
ames=add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='ames')).filter(current_status='canceled').first().meeting,
)
return self.client.get(url), interims
def test_upcoming(self):
r, interims = self.do_upcoming_test()
self.assertContains(r, interims['mars'].number)
self.assertContains(r, interims['ames'].number)
self.assertContains(r, 'IETF 72')
# cancelled session
q = PyQuery(r.content)
self.assertIn('Cancelled', q('tr>td.text-end>span').text())
# test_upcoming_filters_ignored removed - we _don't_ want to ignore filters now, and the test passed because it wasn't testing the filtering anyhow (which requires testing the js).
def test_upcoming_ical(self):
meeting = make_meeting_test_data(create_interims=True)
populate_important_dates(meeting)
url = urlreverse("ietf.meeting.views.upcoming_ical")
# Expect events 3 sessions - one for each WG and one for the IETF meeting
expected_event_summaries = [
'ames - Asteroid Mining Equipment Standardization Group',
'mars - Martian Special Interest Group',
'IETF 72',
]
Session.objects.filter(
meeting__type_id='interim',
group__acronym="mars",
).update(
remote_instructions='https://someurl.example.com',
)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
assert_ical_response_is_valid(self, r,
expected_event_summaries=expected_event_summaries,
expected_event_count=len(expected_event_summaries))
self.assertContains(r, 'Remote instructions: https://someurl.example.com')
Session.objects.filter(meeting__type_id='interim').update(remote_instructions='')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
assert_ical_response_is_valid(self, r,
expected_event_summaries=expected_event_summaries,
expected_event_count=len(expected_event_summaries))
self.assertNotContains(r, 'Remote instructions:')
updated = meeting.updated()
self.assertIsNotNone(updated)
expected_updated = updated.astimezone(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
self.assertContains(r, f"DTSTAMP:{expected_updated}")
# With default cached_updated, 1970-01-01
with patch("ietf.meeting.models.Meeting.updated", return_value=None):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(meeting.type_id, "ietf")
expected_updated = "19700101T000000Z"
self.assertEqual(1, r.content.decode("utf-8").count(f"DTSTAMP:{expected_updated}"))
@patch("ietf.meeting.utils.preprocess_meeting_important_dates")
def test_upcoming_ical_filter(self, mock_preprocess_meeting_important_dates):
# Just a quick check of functionality - details tested by test_js.InterimTests
make_meeting_test_data(create_interims=True)
url = urlreverse("ietf.meeting.views.upcoming_ical")
r = self.client.get(url + '?show=mars')
self.assertEqual(r.status_code, 200)
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'mars - Martian Special Interest Group',
],
expected_event_count=1)
r = self.client.get(url + '?show=mars,ietf-meetings')
self.assertEqual(r.status_code, 200)
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'mars - Martian Special Interest Group',
'IETF 72',
],
expected_event_count=2)
# Verify preprocess_meeting_important_dates isn't being called
mock_preprocess_meeting_important_dates.assert_not_called()
def test_upcoming_json(self):
make_meeting_test_data(create_interims=True)
url = urlreverse("ietf.meeting.views.upcoming_json")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.get('Content-Type'), "application/json;charset=utf-8")
data = r.json()
self.assertEqual(len(data), 3)
def test_interim_request_permissions(self):
'''Ensure only authorized users see link to request interim meeting'''
make_meeting_test_data()
# test unauthorized not logged in
upcoming_url = urlreverse("ietf.meeting.views.upcoming")
request_url = urlreverse("ietf.meeting.views.interim_request")
r = self.client.get(upcoming_url)
self.assertNotContains(r,'Request new interim meeting')
# test unauthorized user
login_testing_unauthorized(self,"plain",request_url)
r = self.client.get(upcoming_url)
self.assertNotContains(r,'Request new interim meeting')
r = self.client.get(request_url)
self.assertEqual(r.status_code, 403)
self.client.logout()
# test authorized
for username in ('secretary','ad','marschairman','irtf-chair','irgchairman'):
self.client.login(username=username, password= username + "+password")
r = self.client.get(upcoming_url)
self.assertContains(r,'Request new interim meeting')
r = self.client.get(request_url)
self.assertEqual(r.status_code, 200)
self.client.logout()
def test_interim_request_options(self):
make_meeting_test_data()
# secretariat can request for any group
self.client.login(username="secretary", password="secretary+password")
r = self.client.get("/meeting/interim/request/")
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(Group.objects.with_meetings().filter(state__in=('active', 'proposed', 'bof')).count(),
len(q("#id_group option")) - 1) # -1 for options placeholder
self.client.logout()
# wg chair
self.client.login(username="marschairman", password="marschairman+password")
r = self.client.get("/meeting/interim/request/")
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
user = User.objects.get(username='marschairman')
person = user.person
count = person.role_set.filter(name='chair',group__type__in=('wg', 'rg'), group__state__in=('active', 'proposed')).count()
self.assertEqual(count, len(q("#id_group option")) - 1) # -1 for options placeholder
# wg AND rg chair
group = Group.objects.get(acronym='irg')
Role.objects.create(name_id='chair',group=group,person=person,email=person.email())
r = self.client.get("/meeting/interim/request/")
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
count = person.role_set.filter(name='chair',group__type__in=('wg', 'rg'), group__state__in=('active', 'proposed')).count()
self.assertEqual(count, len(q("#id_group option")) - 1) # -1 for options placeholder
def do_interim_request_single_virtual(self, emails_expected):
make_meeting_test_data()
group = Group.objects.get(acronym='mars')
date = date_today() + datetime.timedelta(days=30)
time = time_now().replace(microsecond=0,second=0)
dt = pytz.utc.localize(datetime.datetime.combine(date, time))
duration = datetime.timedelta(hours=3)
remote_instructions = 'Use webex'
agenda = 'Intro. Slides. Discuss.'
agenda_note = 'On second level'
length_before = len(outbox)
meeting_count = Meeting.objects.filter(number__contains='-%s-'%group.acronym, date__year=date.year).count()
next_num = "%02d" % (meeting_count+1)
self.client.login(username="marschairman", password="marschairman+password")
data = {'group':group.pk,
'meeting_type':'single',
'city':'',
'country':'',
'time_zone':'UTC',
'session_set-0-date':date.strftime("%Y-%m-%d"),
'session_set-0-time':time.strftime('%H:%M'),
'session_set-0-requested_duration':'03:00:00',
'session_set-0-remote_instructions':remote_instructions,
'session_set-0-agenda':agenda,
'session_set-0-agenda_note':agenda_note,
'session_set-TOTAL_FORMS':1,
'session_set-INITIAL_FORMS':0,
'session_set-MIN_NUM_FORMS':0,
'session_set-MAX_NUM_FORMS':1000}
with patch('ietf.meeting.views.sessions_post_save', wraps=sessions_post_save) as mock:
r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data)
self.assertTrue(mock.called)
self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming'))
meeting = Meeting.objects.order_by('id').last()
self.assertEqual(meeting.type_id,'interim')
self.assertEqual(meeting.date,date)
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year, group.acronym, next_num))
self.assertEqual(meeting.city,'')
self.assertEqual(meeting.country,'')
self.assertEqual(meeting.time_zone,'UTC')
session = meeting.session_set.first()
self.assertEqual(session.remote_instructions,remote_instructions)
self.assertEqual(session.agenda_note,agenda_note)
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,dt)
self.assertEqual(timeslot.duration,duration)
# ensure agenda document was created
self.assertEqual(session.materials.count(),1)
doc = session.materials.first()
path = os.path.join(doc.get_file_path(),doc.filename_with_rev())
self.assertTrue(os.path.exists(path))
# check notices to secretariat and chairs
self.assertEqual(len(outbox), length_before + emails_expected)
return meeting
@override_settings(VIRTUAL_INTERIMS_REQUIRE_APPROVAL = True)
def test_interim_request_single_virtual_settings_approval_required(self):
meeting = self.do_interim_request_single_virtual(emails_expected=1)
self.assertEqual(meeting.session_set.last().schedulingevent_set.last().status_id,'apprw')
self.assertIn('New Interim Meeting Request', outbox[-1]['Subject'])
self.assertIn('session-request@ietf.org', outbox[-1]['To'])
self.assertIn('aread@example.org', outbox[-1]['Cc'])
@override_settings(VIRTUAL_INTERIMS_REQUIRE_APPROVAL = False)
def test_interim_request_single_virtual_settings_approval_not_required(self):
meeting = self.do_interim_request_single_virtual(emails_expected=2)
self.assertEqual(meeting.session_set.last().schedulingevent_set.last().status_id,'scheda')
self.assertIn('iesg-secretary@ietf.org', outbox[-1]['To'])
self.assertIn('interim meeting ready for announcement', outbox[-1]['Subject'])
def test_interim_request_single_in_person(self):
make_meeting_test_data()
group = Group.objects.get(acronym='mars')
date = date_today() + datetime.timedelta(days=30)
time = time_now().replace(microsecond=0,second=0)
time_zone = 'America/Los_Angeles'
tz = pytz.timezone(time_zone)
dt = tz.localize(datetime.datetime.combine(date, time))
duration = datetime.timedelta(hours=3)
city = 'San Francisco'
country = 'US'
remote_instructions = 'Use webex'
agenda = 'Intro. Slides. Discuss.'
agenda_note = 'On second level'
meeting_count = Meeting.objects.filter(number__contains='-%s-'%group.acronym, date__year=date.year).count()
next_num = "%02d" % (meeting_count+1)
self.client.login(username="secretary", password="secretary+password")
data = {'group':group.pk,
'meeting_type':'single',
'city':city,
'country':country,
'time_zone':time_zone,
'session_set-0-date':date.strftime("%Y-%m-%d"),
'session_set-0-time':time.strftime('%H:%M'),
'session_set-0-requested_duration':'03:00:00',
'session_set-0-remote_instructions':remote_instructions,
'session_set-0-agenda':agenda,
'session_set-0-agenda_note':agenda_note,
'session_set-TOTAL_FORMS':1,
'session_set-INITIAL_FORMS':0}
with patch('ietf.meeting.views.sessions_post_save', wraps=sessions_post_save) as mock:
r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data)
self.assertTrue(mock.called)
self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming'))
meeting = Meeting.objects.order_by('id').last()
self.assertEqual(meeting.type_id,'interim')
self.assertEqual(meeting.date,date)
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year, group.acronym, next_num))
self.assertEqual(meeting.city,city)
self.assertEqual(meeting.country,country)
self.assertEqual(meeting.time_zone,time_zone)
session = meeting.session_set.first()
self.assertEqual(session.remote_instructions,remote_instructions)
self.assertEqual(session.agenda_note,agenda_note)
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,dt)
self.assertEqual(timeslot.duration,duration)
def test_interim_request_multi_day(self):
make_meeting_test_data()
date = date_today() + datetime.timedelta(days=30)
date2 = date + datetime.timedelta(days=1)
time = time_now().replace(microsecond=0,second=0)
time_zone = 'America/Los_Angeles'
tz = pytz.timezone(time_zone)
dt = tz.localize(datetime.datetime.combine(date, time))
dt2 = tz.localize(datetime.datetime.combine(date2, time))
duration = datetime.timedelta(hours=3)
group = Group.objects.get(acronym='mars')
city = 'San Francisco'
country = 'US'
remote_instructions = 'Use webex'
agenda = 'Intro. Slides. Discuss.'
agenda_note = 'On second level'
meeting_count = Meeting.objects.filter(number__contains='-%s-'%group.acronym, date__year=date.year).count()
next_num = "%02d" % (meeting_count+1)
self.client.login(username="secretary", password="secretary+password")
data = {'group':group.pk,
'meeting_type':'multi-day',
'city':city,
'country':country,
'time_zone':time_zone,
'session_set-0-date':date.strftime("%Y-%m-%d"),
'session_set-0-time':time.strftime('%H:%M'),
'session_set-0-requested_duration':'03:00:00',
'session_set-0-remote_instructions':remote_instructions,
'session_set-0-agenda':agenda,
'session_set-0-agenda_note':agenda_note,
'session_set-1-date':date2.strftime("%Y-%m-%d"),
'session_set-1-time':time.strftime('%H:%M'),
'session_set-1-requested_duration':'03:00:00',
'session_set-1-remote_instructions':remote_instructions,
'session_set-1-agenda':agenda,
'session_set-1-agenda_note':agenda_note,
'session_set-TOTAL_FORMS':2,
'session_set-INITIAL_FORMS':0}
with patch('ietf.meeting.views.sessions_post_save', wraps=sessions_post_save) as mock:
r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data)
self.assertTrue(mock.called)
self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming'))
meeting = Meeting.objects.order_by('id').last()
self.assertEqual(meeting.type_id,'interim')
self.assertEqual(meeting.date,date)
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year, group.acronym, next_num))
self.assertEqual(meeting.city,city)
self.assertEqual(meeting.country,country)
self.assertEqual(meeting.time_zone,time_zone)
self.assertEqual(meeting.session_set.count(),2)
# first sesstion
session = meeting.session_set.all()[0]
self.assertEqual(session.remote_instructions,remote_instructions)
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,dt)
self.assertEqual(timeslot.duration,duration)
self.assertEqual(session.agenda_note,agenda_note)
# second sesstion
session = meeting.session_set.all()[1]
self.assertEqual(session.remote_instructions,remote_instructions)
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,dt2)
self.assertEqual(timeslot.duration,duration)
self.assertEqual(session.agenda_note,agenda_note)
def test_interim_request_multi_day_non_consecutive(self):
make_meeting_test_data()
date = date_today() + datetime.timedelta(days=30)
date2 = date + datetime.timedelta(days=2)
time = timezone.now().time().replace(microsecond=0,second=0)
group = Group.objects.get(acronym='mars')
city = 'San Francisco'
country = 'US'
time_zone = 'America/Los_Angeles'
remote_instructions = 'Use webex'
agenda = 'Intro. Slides. Discuss.'
agenda_note = 'On second level'
self.client.login(username="secretary", password="secretary+password")
data = {'group':group.pk,
'meeting_type':'multi-day',
'city':city,
'country':country,
'time_zone':time_zone,
'session_set-0-date':date.strftime("%Y-%m-%d"),
'session_set-0-time':time.strftime('%H:%M'),
'session_set-0-requested_duration':'03:00:00',
'session_set-0-remote_instructions':remote_instructions,
'session_set-0-agenda':agenda,
'session_set-0-agenda_note':agenda_note,
'session_set-1-date':date2.strftime("%Y-%m-%d"),
'session_set-1-time':time.strftime('%H:%M'),
'session_set-1-requested_duration':'03:00:00',
'session_set-1-remote_instructions':remote_instructions,
'session_set-1-agenda':agenda,
'session_set-1-agenda_note':agenda_note,
'session_set-TOTAL_FORMS':2,
'session_set-INITIAL_FORMS':0}
r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data)
self.assertContains(r, 'days must be consecutive')
def test_interim_request_multi_day_cancel(self):
"""All sessions of a multi-day interim request should be canceled"""
length_before = len(outbox)
date = date_today() + datetime.timedelta(days=15)
# Set up an interim request with several sessions
num_sessions = 3
meeting = MeetingFactory(type_id='interim', date=date)
for _ in range(num_sessions):
SessionFactory(meeting=meeting)
# Cancel the interim request
url = urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number})
self.client.login(username="secretary", password="secretary+password")
r = self.client.post(url)
# Verify results
self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming'))
for session in add_event_info_to_session_qs(meeting.session_set.all()):
self.assertEqual(session.current_status, 'canceled')
self.assertEqual(len(outbox), length_before + 1)
self.assertIn('Interim Meeting Cancelled', outbox[-1]['Subject'])
def test_interim_request_series(self):
make_meeting_test_data()
meeting_count_before = Meeting.objects.filter(type='interim').count()
date = date_today() + datetime.timedelta(days=30)
if (date.month, date.day) == (12, 31):
# Avoid date and date2 in separate years
# (otherwise the test will fail if run on December 1st)
date += datetime.timedelta(days=1)
date2 = date + datetime.timedelta(days=1)
# ensure dates are in the same year
if date.year != date2.year:
date += datetime.timedelta(days=1)
date2 += datetime.timedelta(days=1)
time = time_now().replace(microsecond=0,second=0)
time_zone = 'America/Los_Angeles'
tz = pytz.timezone(time_zone)
dt = tz.localize(datetime.datetime.combine(date, time))
dt2 = tz.localize(datetime.datetime.combine(date2, time))
duration = datetime.timedelta(hours=3)
group = Group.objects.get(acronym='mars')
city = ''
country = ''
remote_instructions = 'Use webex'
agenda = 'Intro. Slides. Discuss.'
agenda_note = 'On second level'
meeting_count = Meeting.objects.filter(number__contains='-%s-'%group.acronym, date__year=date.year).count()
next_num = "%02d" % (meeting_count+1)
next_num2 = "%02d" % (meeting_count+2)
self.client.login(username="secretary", password="secretary+password")
r = self.client.get(urlreverse("ietf.meeting.views.interim_request"))
self.assertEqual(r.status_code, 200)
data = {'group':group.pk,
'meeting_type':'series',
'city':city,
'country':country,
'time_zone':time_zone,
'session_set-0-date':date.strftime("%Y-%m-%d"),
'session_set-0-time':time.strftime('%H:%M'),
'session_set-0-requested_duration':'03:00:00',
'session_set-0-remote_instructions':remote_instructions,
'session_set-0-agenda':agenda,
'session_set-0-agenda_note':agenda_note,
'session_set-1-date':date2.strftime("%Y-%m-%d"),
'session_set-1-time':time.strftime('%H:%M'),
'session_set-1-requested_duration':'03:00:00',
'session_set-1-remote_instructions':remote_instructions,
'session_set-1-agenda':agenda,
'session_set-1-agenda_note':agenda_note,
'session_set-TOTAL_FORMS':2,
'session_set-INITIAL_FORMS':0}
with patch('ietf.meeting.views.sessions_post_save', wraps=sessions_post_save) as mock:
r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data)
self.assertTrue(mock.called)
self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming'))
meeting_count_after = Meeting.objects.filter(type='interim').count()
self.assertEqual(meeting_count_after,meeting_count_before + 2)
meetings = Meeting.objects.order_by('-id')[:2]
# first meeting
meeting = meetings[1]
self.assertEqual(meeting.type_id,'interim')
self.assertEqual(meeting.date,date)
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year, group.acronym, next_num))
self.assertEqual(meeting.city,city)
self.assertEqual(meeting.country,country)
self.assertEqual(meeting.time_zone,time_zone)
self.assertEqual(meeting.session_set.count(),1)
session = meeting.session_set.first()
self.assertEqual(session.remote_instructions,remote_instructions)
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,dt)
self.assertEqual(timeslot.duration,duration)
self.assertEqual(session.agenda_note,agenda_note)
# second meeting
meeting = meetings[0]
self.assertEqual(meeting.type_id,'interim')
self.assertEqual(meeting.date,date2)
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date2.year, group.acronym, next_num2))
self.assertEqual(meeting.city,city)
self.assertEqual(meeting.country,country)
self.assertEqual(meeting.time_zone,time_zone)
self.assertEqual(meeting.session_set.count(),1)
session = meeting.session_set.first()
self.assertEqual(session.remote_instructions,remote_instructions)
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,dt2)
self.assertEqual(timeslot.duration,duration)
self.assertEqual(session.agenda_note,agenda_note)
# test_interim_pending subsumed by test_appears_on_pending
def test_can_approve_interim_request(self):
make_interim_test_data()
# unprivileged user
user = User.objects.get(username='plain')
group = Group.objects.get(acronym='mars')
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group=group)).filter(current_status='apprw').first().meeting
self.assertFalse(can_approve_interim_request(meeting=meeting,user=user))
# Secretariat
user = User.objects.get(username='secretary')
self.assertTrue(can_approve_interim_request(meeting=meeting,user=user))
# related AD
user = User.objects.get(username='ad')
self.assertTrue(can_approve_interim_request(meeting=meeting,user=user))
# AD from other area
user = User.objects.get(username='ops-ad')
self.assertFalse(can_approve_interim_request(meeting=meeting,user=user))
# AD from other area assigned as the WG AD anyhow (cross-area AD)
user = RoleFactory(name_id='ad',group=group).person.user
self.assertTrue(can_approve_interim_request(meeting=meeting,user=user))
# WG Chair
user = User.objects.get(username='marschairman')
self.assertFalse(can_approve_interim_request(meeting=meeting,user=user))
def test_can_view_interim_request(self):
make_interim_test_data()
# unprivileged user
user = User.objects.get(username='plain')
group = Group.objects.get(acronym='mars')
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group=group)).filter(current_status='apprw').first().meeting
self.assertFalse(can_view_interim_request(meeting=meeting,user=user))
# Secretariat
user = User.objects.get(username='secretary')
self.assertTrue(can_view_interim_request(meeting=meeting,user=user))
# related AD
user = User.objects.get(username='ad')
self.assertTrue(can_view_interim_request(meeting=meeting,user=user))
# other AD
user = User.objects.get(username='ops-ad')
self.assertTrue(can_view_interim_request(meeting=meeting,user=user))
# WG Chair
user = User.objects.get(username='marschairman')
self.assertTrue(can_view_interim_request(meeting=meeting,user=user))
# Other WG Chair
user = User.objects.get(username='ameschairman')
self.assertFalse(can_view_interim_request(meeting=meeting,user=user))
def test_can_manage_group(self):
make_meeting_test_data()
# unprivileged user
user = User.objects.get(username='plain')
group = Group.objects.get(acronym='mars')
self.assertFalse(can_manage_group(user=user,group=group))
# Secretariat
user = User.objects.get(username='secretary')
self.assertTrue(can_manage_group(user=user,group=group))
# related AD
user = User.objects.get(username='ad')
self.assertTrue(can_manage_group(user=user,group=group))
# other AD
user = User.objects.get(username='ops-ad')
self.assertTrue(can_manage_group(user=user,group=group))
# WG Chair
user = User.objects.get(username='marschairman')
self.assertTrue(can_manage_group(user=user,group=group))
# Other WG Chair
user = User.objects.get(username='ameschairman')
self.assertFalse(can_manage_group(user=user,group=group))
def test_interim_request_details(self):
make_interim_test_data(meeting_tz='America/Chicago')
meeting = Session.objects.with_current_status().filter(
meeting__type='interim', group__acronym='mars', current_status='apprw').first().meeting
url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number})
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
start_time = meeting.session_set.first().official_timeslotassignment().timeslot.local_start_time().strftime('%H:%M')
utc_start_time = meeting.session_set.first().official_timeslotassignment().timeslot.utc_start_time().strftime('%H:%M')
self.assertIn(start_time, unicontent(r))
self.assertIn(utc_start_time, unicontent(r))
def test_interim_request_details_announcement(self):
'''Test access to Announce / Skip Announce features'''
make_meeting_test_data()
date = date_today() + datetime.timedelta(days=30)
group = Group.objects.get(acronym='mars')
meeting = make_interim_meeting(group=group, date=date, status='scheda')
url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number})
# Chair, no access
self.client.login(username="marschairman", password="marschairman+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q("a.btn:contains('Announce')")),0)
# Secretariat has access
self.client.login(username="secretary", password="secretary+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q("a.btn:contains('nnounce')")),2)
def test_interim_request_details_cancel(self):
"""Test access to cancel meeting / session features"""
make_interim_test_data()
mars_sessions = Session.objects.with_current_status(
).filter(
meeting__type='interim',
group__acronym='mars',
)
meeting_apprw = mars_sessions.filter(current_status='apprw').first().meeting
meeting_sched = mars_sessions.filter(current_status='sched').first().meeting
# All these roles should have access to cancel the request
usernames_and_passwords = (
('marschairman', 'marschairman+password'),
('secretary', 'secretary+password')
)
# Start with one session - there should not be any cancel session buttons
for meeting in (meeting_apprw, meeting_sched):
url = urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': meeting.number})
for username, password in usernames_and_passwords:
self.client.login(username=username, password=password)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
cancel_meeting_btns = q("a.btn:contains('Cancel meeting')")
self.assertEqual(len(cancel_meeting_btns), 1,
'Should be exactly one cancel meeting button for user %s' % username)
self.assertEqual(cancel_meeting_btns.eq(0).attr('href'),
urlreverse('ietf.meeting.views.interim_request_cancel',
kwargs={'number': meeting.number}),
'Cancel meeting points to wrong URL')
self.assertEqual(len(q("a.btn:contains('Cancel Session')")), 0,
'Should be no cancel session buttons for user %s' % username)
# Add a second session
SessionFactory(meeting=meeting_apprw, status_id='apprw')
SessionFactory(meeting=meeting_sched, status_id='sched')
for meeting in (meeting_apprw, meeting_sched):
url = urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': meeting.number})
for username, password in usernames_and_passwords:
self.client.login(username=username, password=password)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
cancel_meeting_btns = q("a.btn:contains('Cancel meeting')")
self.assertEqual(len(cancel_meeting_btns), 1,
'Should be exactly one cancel meeting button for user %s' % username)
self.assertEqual(cancel_meeting_btns.eq(0).attr('href'),
urlreverse('ietf.meeting.views.interim_request_cancel',
kwargs={'number': meeting.number}),
'Cancel meeting button points to wrong URL')
cancel_session_btns = q("a.btn:contains('Cancel session')")
self.assertEqual(len(cancel_session_btns), 2,
'Should be two cancel session buttons for user %s' % username)
hrefs = [btn.attr('href') for btn in cancel_session_btns.items()]
for index, session in enumerate(meeting.session_set.all()):
self.assertIn(urlreverse('ietf.meeting.views.interim_request_session_cancel',
kwargs={'sessionid': session.pk}),
hrefs,
'Session missing a link to its cancel URL')
def test_interim_request_details_status(self):
"""Test statuses on the interim request details page"""
make_interim_test_data()
some_person = PersonFactory()
self.client.login(username='marschairman', password='marschairman+password')
# These are the first sessions for each meeting - hang on to them
sessions = list(
Session.objects.with_current_status().filter(meeting__type='interim', group__acronym='mars')
)
# Hack: change the name for the 'canceled' session status so we can tell it apart
# from the 'canceledpa' session status more easily
canceled_status = SessionStatusName.objects.get(slug='canceled')
canceled_status.name = 'This is cancelled'
canceled_status.save()
canceledpa_status = SessionStatusName.objects.get(slug='canceledpa')
notmeet_status = SessionStatusName.objects.get(slug='notmeet')
# Simplest case - single session for each meeting
for session in [Session.objects.with_current_status().get(pk=s.pk) for s in sessions]:
url = urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': session.meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
status = SessionStatusName.objects.get(slug=session.current_status)
self.assertEqual(
len(q("dd:contains('%s')" % status.name)),
1 # once - for the meeting status, no session status shown when only one session
)
# Now add a second session with a different status - it should not change meeting status
for session in [Session.objects.with_current_status().get(pk=s.pk) for s in sessions]:
SessionFactory(meeting=session.meeting, status_id=notmeet_status.pk)
url = urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': session.meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
status = SessionStatusName.objects.get(slug=session.current_status)
self.assertEqual(
len(q("dd:contains('%s')" % status.name)),
2 # twice - once as the meeting status, once as the session status
)
self.assertEqual(
len(q("dd:contains('%s')" % notmeet_status.name)),
1 # only for the session status
)
# Now cancel the first session - second meeting status should be shown for meeting
for session in [Session.objects.with_current_status().get(pk=s.pk) for s in sessions]:
# Use 'canceledpa' here and 'canceled' later
SchedulingEvent.objects.create(session=session,
status=canceledpa_status,
by=some_person)
url = urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': session.meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(
len(q("dd:contains('%s')" % canceledpa_status.name)),
1 # only for the session status
)
self.assertEqual(
len(q("dd:contains('%s')" % notmeet_status.name)),
2 # twice - once as the meeting status, once as the session status
)
# Now cancel the second session - first meeting status should be shown for meeting again
for session in [Session.objects.with_current_status().get(pk=s.pk) for s in sessions]:
second_session = session.meeting.session_set.exclude(pk=session.pk).first()
# use canceled so we can differentiate between the first and second session statuses
SchedulingEvent.objects.create(session=second_session,
status=canceled_status,
by=some_person)
url = urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': session.meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(
len(q("dd:contains('%s')" % canceledpa_status.name)),
2 # twice - once as the meeting status, once as the session status
)
self.assertEqual(
len(q("dd:contains('%s')" % canceled_status.name)),
1 # only as the session status
)
def do_interim_request_disapprove_test(self, extra_session=False, canceled_session=False):
make_interim_test_data()
session = Session.objects.with_current_status().filter(
meeting__type='interim', group__acronym='mars', current_status='apprw').first()
meeting = session.meeting
if extra_session:
extra_session = SessionFactory(meeting=meeting, status_id='apprw')
if canceled_session:
canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa')
url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number})
login_testing_unauthorized(self,"secretary",url)
r = self.client.post(url,{'disapprove':'Disapprove'})
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_pending'))
for sess in [session, extra_session]:
if sess:
self.assertEqual(Session.objects.with_current_status().get(pk=sess.pk).current_status,
'disappr')
if canceled_session:
self.assertEqual(Session.objects.with_current_status().get(pk=canceled_session.pk).current_status,
'canceledpa')
def test_interim_request_disapprove(self):
self.do_interim_request_disapprove_test()
def test_interim_request_disapprove_with_extra_session(self):
self.do_interim_request_disapprove_test(extra_session=True)
def test_interim_request_disapprove_with_canceled_session(self):
self.do_interim_request_disapprove_test(canceled_session=True)
def test_interim_request_disapprove_with_extra_and_canceled_sessions(self):
self.do_interim_request_disapprove_test(extra_session=True, canceled_session=True)
@patch('ietf.meeting.views.sessions_post_cancel')
def test_interim_request_cancel(self, mock):
"""Test that interim request cancel function works
Does not test that UI buttons are present, that is handled elsewhere.
"""
make_interim_test_data()
meeting = Session.objects.with_current_status(
).filter(
meeting__type='interim',
group__acronym='mars',
current_status='apprw',
).first().meeting
# ensure fail unauthorized
url = urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number})
comments = 'Bob cannot make it'
self.client.login(username="ameschairman", password="ameschairman+password")
r = self.client.post(url, {'comments': comments})
self.assertEqual(r.status_code, 403)
self.assertFalse(mock.called, 'Should not cancel sessions if request rejected')
# test with overly-long comments
comments += '0123456789abcdef'*32
self.client.login(username="marschairman", password="marschairman+password")
r = self.client.post(url, {'comments': comments})
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('form .is-invalid'))
# truncate to max_length
comments = comments[:512]
# test cancelling before announcement
length_before = len(outbox)
r = self.client.post(url, {'comments': comments})
self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming'))
for session in meeting.session_set.with_current_status():
self.assertEqual(session.current_status,'canceledpa')
self.assertEqual(session.agenda_note, comments)
self.assertEqual(len(outbox), length_before) # no email notice
self.assertTrue(mock.called, 'Should cancel sessions if request handled')
self.assertCountEqual(mock.call_args[0][1], meeting.session_set.all())
# test cancelling after announcement
mock.reset_mock()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='sched').first().meeting
url = urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number})
r = self.client.post(url, {'comments': comments})
self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming'))
for session in meeting.session_set.with_current_status():
self.assertEqual(session.current_status,'canceled')
self.assertEqual(session.agenda_note, comments)
self.assertEqual(len(outbox), length_before + 1)
self.assertIn('Interim Meeting Cancelled', outbox[-1]['Subject'])
self.assertIn(comments, get_payload_text(outbox[-1]))
self.assertTrue(mock.called, 'Should cancel sessions if request handled')
self.assertCountEqual(mock.call_args[0][1], meeting.session_set.all())
@patch('ietf.meeting.views.sessions_post_cancel')
def test_interim_request_session_cancel(self, mock):
"""Test that interim meeting session cancellation functions
Does not test that UI buttons are present, that is handled elsewhere.
"""
make_interim_test_data()
session = Session.objects.with_current_status().filter(
meeting__type='interim', group__acronym='mars', current_status='apprw',).first()
meeting = session.meeting
comments = 'Bob cannot make it'
# Should not be able to cancel when there is only one session
self.client.login(username="marschairman", password="marschairman+password")
url = urlreverse('ietf.meeting.views.interim_request_session_cancel', kwargs={'sessionid': session.pk})
r = self.client.post(url, {'comments': comments})
self.assertEqual(r.status_code, 409)
self.assertFalse(mock.called, 'Should not cancel sessions if request rejected')
# Add a second session
SessionFactory(meeting=meeting, status_id='apprw')
# ensure fail unauthorized
url = urlreverse('ietf.meeting.views.interim_request_session_cancel', kwargs={'sessionid': session.pk})
self.client.login(username="ameschairman", password="ameschairman+password")
r = self.client.post(url, {'comments': comments})
self.assertEqual(r.status_code, 403)
self.assertFalse(mock.called, 'Should not cancel sessions if request rejected')
# test cancelling before announcement
self.client.login(username="marschairman", password="marschairman+password")
length_before = len(outbox)
canceled_count_before = meeting.session_set.with_current_status().filter(
current_status__in=['canceled', 'canceledpa']).count()
r = self.client.post(url, {'comments': comments})
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': meeting.number}))
self.assertTrue(mock.called, 'Should cancel sessions if request handled')
self.assertCountEqual(mock.call_args[0][1], [session])
# This session should be canceled...
sessions = meeting.session_set.with_current_status()
session = sessions.filter(id=session.pk).first() # reload our session info
self.assertEqual(session.current_status, 'canceledpa')
self.assertEqual(session.agenda_note, comments)
# But others should not - count should have changed by only 1
self.assertEqual(
sessions.filter(current_status__in=['canceled', 'canceledpa']).count(),
canceled_count_before + 1
)
self.assertEqual(len(outbox), length_before) # no email notice
# test cancelling after announcement
mock.reset_mock()
session = Session.objects.with_current_status().filter(
meeting__type='interim', group__acronym='mars', current_status='sched').first()
meeting = session.meeting
# Try to cancel when there's only one session in the meeting
url = urlreverse('ietf.meeting.views.interim_request_session_cancel', kwargs={'sessionid': session.pk})
r = self.client.post(url, {'comments': comments})
self.assertEqual(r.status_code, 409)
self.assertFalse(mock.called, 'Should not cancel sessions if request rejected')
# Add another session
SessionFactory(meeting=meeting, status_id='sched') # two sessions so canceling a session makes sense
canceled_count_before = meeting.session_set.with_current_status().filter(
current_status__in=['canceled', 'canceledpa']).count()
r = self.client.post(url, {'comments': comments})
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_request_details',
kwargs={'number': meeting.number}))
self.assertTrue(mock.called, 'Should cancel sessions if request handled')
self.assertCountEqual(mock.call_args[0][1], [session])
# This session should be canceled...
sessions = meeting.session_set.with_current_status()
session = sessions.filter(id=session.pk).first() # reload our session info
self.assertEqual(session.current_status, 'canceled')
self.assertEqual(session.agenda_note, comments)
# But others should not - count should have changed by only 1
self.assertEqual(
sessions.filter(current_status__in=['canceled', 'canceledpa']).count(),
canceled_count_before + 1
)
self.assertEqual(len(outbox), length_before + 1) # email notice sent
self.assertIn('session cancelled', outbox[-1]['Subject'])
def test_interim_request_edit_no_notice(self):
'''Edit a request. No notice should go out if it hasn't been announced yet'''
make_interim_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
group = meeting.session_set.first().group
url = urlreverse('ietf.meeting.views.interim_request_edit', kwargs={'number': meeting.number})
# test unauthorized access
self.client.login(username="ameschairman", password="ameschairman+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 403)
# test authorized use
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# post changes
length_before = len(outbox)
form_initial = r.context['form'].initial
formset_initial = r.context['formset'].forms[0].initial
data = {'group':group.pk,
'meeting_type':'single',
'session_set-0-id':meeting.session_set.first().id,
'session_set-0-date':formset_initial['date'].strftime('%Y-%m-%d'),
'session_set-0-time':'12:34',
'session_set-0-requested_duration': '00:30',
'session_set-0-remote_instructions':formset_initial['remote_instructions'],
#'session_set-0-agenda':formset_initial['agenda'],
'session_set-0-agenda_note':formset_initial['agenda_note'],
'session_set-TOTAL_FORMS':1,
'session_set-INITIAL_FORMS':1}
data.update(form_initial)
r = self.client.post(url, data)
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number}))
self.assertEqual(len(outbox),length_before)
session = meeting.session_set.first()
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(
timeslot.time,
meeting.tz().localize(datetime.datetime.combine(formset_initial['date'], datetime.time(12, 34))),
)
def test_interim_request_edit(self):
'''Edit request. Send notice of change'''
make_interim_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='sched').first().meeting
group = meeting.session_set.first().group
url = urlreverse('ietf.meeting.views.interim_request_edit', kwargs={'number': meeting.number})
# test unauthorized access
self.client.login(username="ameschairman", password="ameschairman+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 403)
# test authorized use
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# post changes
length_before = len(outbox)
form_initial = r.context['form'].initial
formset_initial = r.context['formset'].forms[0].initial
new_duration = formset_initial['requested_duration'] + datetime.timedelta(hours=1)
data = {'group':group.pk,
'meeting_type':'single',
'session_set-0-id':meeting.session_set.first().id,
'session_set-0-date':formset_initial['date'].strftime('%Y-%m-%d'),
'session_set-0-time': '12:34',
'session_set-0-requested_duration':self.strfdelta(new_duration, '{hours}:{minutes}'),
'session_set-0-remote_instructions':formset_initial['remote_instructions'],
#'session_set-0-agenda':formset_initial['agenda'],
'session_set-0-agenda_note':formset_initial['agenda_note'],
'session_set-TOTAL_FORMS':1,
'session_set-INITIAL_FORMS':1}
data.update(form_initial)
r = self.client.post(url, data)
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number}))
self.assertEqual(len(outbox),length_before+1)
self.assertIn('CHANGED', outbox[-1]['Subject'])
session = meeting.session_set.first()
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(
timeslot.time,
meeting.tz().localize(datetime.datetime.combine(formset_initial['date'], datetime.time(12, 34))),
)
self.assertEqual(timeslot.duration,new_duration)
def strfdelta(self, tdelta, fmt):
d = {"days": tdelta.days}
d["hours"], rem = divmod(tdelta.seconds, 3600)
d["minutes"], d["seconds"] = divmod(rem, 60)
return fmt.format(**d)
def test_interim_request_edit_agenda_updates_doc(self):
"""Updating the agenda through the request edit form should update the doc correctly"""
make_interim_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='sched').first().meeting
group = meeting.session_set.first().group
url = urlreverse('ietf.meeting.views.interim_request_edit', kwargs={'number': meeting.number})
session = meeting.session_set.first()
agenda_doc = session.agenda()
rev_before = agenda_doc.rev
uploaded_filename_before = agenda_doc.uploaded_filename
self.client.login(username='secretary', password='secretary+password')
r = self.client.get(url)
form_initial = r.context['form'].initial
formset_initial = r.context['formset'].forms[0].initial
data = {
'group': group.pk,
'meeting_type': 'single',
'session_set-0-id': session.id,
'session_set-0-date': formset_initial['date'].strftime('%Y-%m-%d'),
'session_set-0-time': formset_initial['time'].strftime('%H:%M'),
'session_set-0-requested_duration': '00:30',
'session_set-0-remote_instructions': formset_initial['remote_instructions'],
'session_set-0-agenda': 'modified agenda contents',
'session_set-0-agenda_note': formset_initial['agenda_note'],
'session_set-TOTAL_FORMS': 1,
'session_set-INITIAL_FORMS': 1,
}
data.update(form_initial)
r = self.client.post(url, data)
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number}))
session = Session.objects.get(pk=session.pk) # refresh
agenda_doc = session.agenda()
self.assertEqual(agenda_doc.rev, f'{int(rev_before) + 1:02}', 'Revision of agenda should increase')
self.assertNotEqual(agenda_doc.uploaded_filename, uploaded_filename_before, 'Uploaded filename should be updated')
with (Path(agenda_doc.get_file_path()) / agenda_doc.uploaded_filename).open() as f:
self.assertEqual(f.read(), 'modified agenda contents', 'New agenda contents should be saved')
def test_interim_request_details_permissions(self):
make_interim_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number})
# unprivileged user
login_testing_unauthorized(self,"plain",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 403)
def test_send_interim_approval_request(self):
make_interim_test_data()
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
length_before = len(outbox)
send_interim_approval_request(meetings=[meeting])
self.assertEqual(len(outbox),length_before+1)
self.assertIn('New Interim Meeting Request', outbox[-1]['Subject'])
def test_send_interim_meeting_cancellation_notice(self):
make_interim_test_data()
meeting = Session.objects.with_current_status(
).filter(
meeting__type='interim',
group__acronym='mars',
current_status='sched',
).first().meeting
length_before = len(outbox)
send_interim_meeting_cancellation_notice(meeting)
self.assertEqual(len(outbox),length_before + 1)
self.assertIn('Interim Meeting Cancelled', outbox[-1]['Subject'])
def test_send_interim_session_cancellation_notice(self):
make_interim_test_data()
session = Session.objects.with_current_status(
).filter(
meeting__type='interim',
group__acronym='mars',
current_status='sched',
).first()
length_before = len(outbox)
send_interim_session_cancellation_notice(session)
self.assertEqual(len(outbox), length_before + 1)
self.assertIn('session cancelled', outbox[-1]['Subject'])
def test_send_interim_minutes_reminder(self):
make_meeting_test_data()
group = Group.objects.get(acronym='mars')
date = timezone.now() - datetime.timedelta(days=10)
meeting = make_interim_meeting(group=group, date=date, status='sched')
length_before = len(outbox)
send_interim_minutes_reminder(meeting=meeting)
self.assertEqual(len(outbox),length_before+1)
self.assertIn('Action Required: Minutes', outbox[-1]['Subject'])
def test_group_ical(self):
make_interim_test_data()
meeting = Meeting.objects.filter(type='interim', session__group__acronym='mars').first()
s1 = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
self.assertGreater(len(s1.remote_instructions), 0, 'Expected remote_instructions to be set')
a1 = s1.official_timeslotassignment()
t1 = a1.timeslot
# Create an extra session
t2 = TimeSlotFactory.create(
meeting=meeting,
time=meeting.tz().localize(
datetime.datetime.combine(meeting.date, datetime.time(11, 30))
))
s2 = SessionFactory.create(meeting=meeting, group=s1.group, add_to_schedule=False)
SchedTimeSessAssignment.objects.create(timeslot=t2, session=s2, schedule=meeting.schedule)
#
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'acronym':s1.group.acronym, })
r = self.client.get(url)
self.assertEqual(r.get('Content-Type'), "text/calendar")
self.assertContains(r, 'BEGIN:VEVENT')
self.assertEqual(r.content.count(b'UID'), 2)
self.assertContains(r, 'SUMMARY:mars - Martian Special Interest Group')
self.assertContains(r, t1.local_start_time().strftime('%Y%m%dT%H%M%S'))
self.assertContains(r, s1.remote_instructions)
self.assertContains(r, t2.local_start_time().strftime('%Y%m%dT%H%M%S'))
self.assertContains(r, 'END:VEVENT')
#
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'session_id':s1.id, })
r = self.client.get(url)
self.assertEqual(r.get('Content-Type'), "text/calendar")
self.assertContains(r, 'BEGIN:VEVENT')
self.assertEqual(r.content.count(b'UID'), 1)
self.assertContains(r, 'SUMMARY:mars - Martian Special Interest Group')
self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S'))
self.assertContains(r, s1.remote_instructions)
self.assertNotContains(r, t2.time.strftime('%Y%m%dT%H%M%S'))
self.assertContains(r, 'END:VEVENT')
class IphoneAppJsonTests(TestCase):
def test_iphone_app_json_interim(self):
make_interim_test_data()
meeting = Meeting.objects.filter(type_id='interim').order_by('id').last()
url = urlreverse('ietf.meeting.views.agenda_json',kwargs={'num':meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code,200)
data = r.json()
self.assertIn(meeting.number, data.keys())
jsessions = [ s for s in data[meeting.number] if s['objtype'] == 'session' ]
msessions = meeting.session_set.exclude(type__in=['lead','offagenda','break','reg'])
self.assertEqual(len(jsessions), msessions.count())
for s in jsessions:
self.assertTrue(msessions.filter(group__acronym=s['group']['acronym']).exists())
def test_iphone_app_json(self):
make_meeting_test_data()
meeting = Meeting.objects.filter(type_id='ietf').order_by('id').last()
floorplan = FloorPlanFactory.create(meeting=meeting)
for room in meeting.room_set.all():
room.floorplan = floorplan
room.x1 = random.randint(0,100)
room.y1 = random.randint(0,100)
room.x2 = random.randint(0,100)
room.y2 = random.randint(0,100)
room.save()
url = urlreverse('ietf.meeting.views.agenda_json',kwargs={'num':meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code,200)
data = r.json()
self.assertIn(meeting.number, data.keys())
jsessions = [ s for s in data[meeting.number] if s['objtype'] == 'session' ]
msessions = meeting.session_set.exclude(type__in=['lead','offagenda','break','reg'])
self.assertEqual(len(jsessions), msessions.count())
for s in jsessions:
self.assertTrue(msessions.filter(group__acronym=s['group']['acronym']).exists())
class FinalizeProceedingsTests(TestCase):
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().presentations.create(document=Document.objects.filter(type='draft').first(),rev=None)
url = urlreverse('ietf.meeting.views.finalize_proceedings',kwargs={'num':meeting.number})
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(meeting.proceedings_final,False)
self.assertEqual(meeting.session_set.filter(group__acronym="mars").first().presentations.filter(document__type="draft").first().rev,None)
r = self.client.post(url,{'finalize':1})
self.assertEqual(r.status_code, 302)
meeting = Meeting.objects.get(pk=meeting.pk)
self.assertEqual(meeting.proceedings_final,True)
self.assertEqual(meeting.session_set.filter(group__acronym="mars").first().presentations.filter(document__type="draft").first().rev,'00')
@patch("ietf.meeting.utils.generate_bluesheet")
def test_bluesheet_generation(self, mock):
meeting = MeetingFactory(type_id="ietf", number="107") # number where generate_bluesheets should not be called
SessionFactory.create_batch(5, meeting=meeting)
url = urlreverse("ietf.meeting.views.finalize_proceedings", kwargs={"num": meeting.number})
self.client.login(username="secretary", password="secretary+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertFalse(mock.called)
r = self.client.post(url,{'finalize': 1})
self.assertEqual(r.status_code, 302)
self.assertFalse(mock.called)
meeting = MeetingFactory(type_id="ietf", number="108") # number where generate_bluesheets should be called
SessionFactory.create_batch(5, meeting=meeting)
url = urlreverse("ietf.meeting.views.finalize_proceedings", kwargs={"num": meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertFalse(mock.called)
r = self.client.post(url,{'finalize': 1})
self.assertEqual(r.status_code, 302)
self.assertTrue(mock.called)
self.assertCountEqual(
[call_args[0][1] for call_args in mock.call_args_list],
[sess for sess in meeting.session_set.all()],
)
class MaterialsTests(TestCase):
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + [
'AGENDA_PATH',
'SLIDE_STAGING_PATH'
]
def setUp(self):
super().setUp()
self.materials_dir = self.tempdir('materials')
if not os.path.exists(self.materials_dir):
os.mkdir(self.materials_dir)
def tearDown(self):
shutil.rmtree(self.materials_dir)
super().tearDown()
def crawl_materials(self, url, top):
seen = set()
def follow(url):
seen.add(url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
if not ('.' in url and url.rsplit('.', 1)[1] in ['tgz', 'pdf', ]):
if r.content:
page = unicontent(r)
soup = BeautifulSoup(page, 'html.parser')
for a in soup('a'):
href = a.get('href')
path = urlparse(href).path
if (path and path not in seen and path.startswith(top)):
follow(path)
follow(url)
def test_upload_bluesheets(self):
session = SessionFactory(meeting__type_id='ietf')
url = urlreverse('ietf.meeting.views.upload_session_bluesheets',kwargs={'num':session.meeting.number,'session_id':session.id})
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Upload', str(q("title")))
self.assertFalse(session.presentations.exists())
test_file = StringIO('%PDF-1.4\n%âãÏÓ\nthis is some text for a test')
test_file.name = "not_really.pdf"
r = self.client.post(url,dict(file=test_file))
self.assertEqual(r.status_code, 302)
bs_doc = session.presentations.filter(document__type_id='bluesheets').first().document
self.assertEqual(bs_doc.rev,'00')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Revise', str(q("title")))
test_file = StringIO('%PDF-1.4\n%âãÏÓ\nthis is some different text for a test')
test_file.name = "also_not_really.pdf"
r = self.client.post(url,dict(file=test_file))
self.assertEqual(r.status_code, 302)
bs_doc = Document.objects.get(pk=bs_doc.pk)
self.assertEqual(bs_doc.rev,'01')
def test_upload_bluesheets_chair_access(self):
make_meeting_test_data()
mars = Group.objects.get(acronym='mars')
session=SessionFactory(meeting__type_id='ietf',group=mars)
url = urlreverse('ietf.meeting.views.upload_session_bluesheets',kwargs={'num':session.meeting.number,'session_id':session.id})
self.client.login(username="marschairman", password="marschairman+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 403)
def test_upload_bluesheets_interim(self):
session=SessionFactory(meeting__type_id='interim')
url = urlreverse('ietf.meeting.views.upload_session_bluesheets',kwargs={'num':session.meeting.number,'session_id':session.id})
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Upload', str(q("title")))
self.assertFalse(session.presentations.exists())
test_file = StringIO('%PDF-1.4\n%âãÏÓ\nthis is some text for a test')
test_file.name = "not_really.pdf"
r = self.client.post(url,dict(file=test_file))
self.assertEqual(r.status_code, 302)
bs_doc = session.presentations.filter(document__type_id='bluesheets').first().document
self.assertEqual(bs_doc.rev,'00')
def test_upload_bluesheets_interim_chair_access(self):
make_meeting_test_data()
mars = Group.objects.get(acronym='mars')
session=SessionFactory(meeting__type_id='interim',group=mars, meeting__date = date_today())
url = urlreverse('ietf.meeting.views.upload_session_bluesheets',kwargs={'num':session.meeting.number,'session_id':session.id})
self.client.login(username="marschairman", password="marschairman+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Upload', str(q("title")))
def test_upload_minutes_agenda(self):
for doctype in ('minutes','agenda'):
session = SessionFactory(meeting__type_id='ietf')
if doctype == 'minutes':
url = urlreverse('ietf.meeting.views.upload_session_minutes',kwargs={'num':session.meeting.number,'session_id':session.id})
else:
url = urlreverse('ietf.meeting.views.upload_session_agenda',kwargs={'num':session.meeting.number,'session_id':session.id})
self.client.logout()
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Upload', str(q("Title")))
self.assertFalse(session.presentations.exists())
self.assertFalse(q('form input[type="checkbox"]'))
session2 = SessionFactory(meeting=session.meeting,group=session.group)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('form input[type="checkbox"]'))
# test not submitting a file
r = self.client.post(url, dict(submission_method="upload"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q("form .is-invalid"))
test_file = BytesIO(b'this is some text for a test')
test_file.name = "not_really.json"
r = self.client.post(url,dict(submission_method="upload",file=test_file))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('form .is-invalid'))
test_file = BytesIO(b'this is some text for a test'*1510000)
test_file.name = "not_really.pdf"
r = self.client.post(url,dict(submission_method="upload",file=test_file))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('form .is-invalid'))
test_file = BytesIO(b'<html><frameset><frame src="foo.html"></frame><frame src="bar.html"></frame></frameset></html>')
test_file.name = "not_really.html"
r = self.client.post(url,dict(submission_method="upload",file=test_file))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('form .is-invalid'))
# Test html sanitization
test_file = BytesIO(b'<html><head><title>Title</title></head><body><h1>Title</h1><section>Some text</section></body></html>')
test_file.name = "some.html"
r = self.client.post(url,dict(submission_method="upload",file=test_file))
self.assertEqual(r.status_code, 302)
doc = session.presentations.filter(document__type_id=doctype).first().document
self.assertEqual(doc.rev,'00')
text = doc.text()
self.assertIn('Some text', text)
self.assertNotIn('<section>', text)
# txt upload
test_file = BytesIO(b'This is some text for a test, with the word\nvirtual at the beginning of a line.')
test_file.name = "some.txt"
r = self.client.post(url,dict(submission_method="upload",file=test_file,apply_to_all=False))
self.assertEqual(r.status_code, 302)
doc = session.presentations.filter(document__type_id=doctype).first().document
self.assertEqual(doc.rev,'01')
self.assertFalse(session2.presentations.filter(document__type_id=doctype))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Revise', str(q("Title")))
test_file = BytesIO(b'this is some different text for a test')
test_file.name = "also_some.txt"
r = self.client.post(url,dict(submission_method="upload",file=test_file,apply_to_all=True))
self.assertEqual(r.status_code, 302)
doc = Document.objects.get(pk=doc.pk)
self.assertEqual(doc.rev,'02')
self.assertTrue(session2.presentations.filter(document__type_id=doctype))
# Test bad encoding
test_file = BytesIO('<html><h1>Title</h1><section>Some\x93text</section></html>'.encode('latin1'))
test_file.name = "some.html"
r = self.client.post(url,dict(submission_method="upload",file=test_file))
self.assertContains(r, 'Could not identify the file encoding')
doc = Document.objects.get(pk=doc.pk)
self.assertEqual(doc.rev,'02')
# Verify that we don't have dead links
url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym})
top = '/meeting/%s/' % session.meeting.number
self.requests_mock.get(f'{session.notes_url()}/download', text='markdown notes')
self.requests_mock.get(f'{session.notes_url()}/info', text=json.dumps({'title': 'title', 'updatetime': '2021-12-01T17:11:00z'}))
self.crawl_materials(url=url, top=top)
def test_upload_minutes_agenda_unscheduled(self):
for doctype in ('minutes','agenda'):
session = SessionFactory(meeting__type_id='ietf', add_to_schedule=False)
if doctype == 'minutes':
url = urlreverse('ietf.meeting.views.upload_session_minutes',kwargs={'num':session.meeting.number,'session_id':session.id})
else:
url = urlreverse('ietf.meeting.views.upload_session_agenda',kwargs={'num':session.meeting.number,'session_id':session.id})
self.client.logout()
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Upload', str(q("Title")))
self.assertFalse(session.presentations.exists())
self.assertFalse(q('form input[type="checkbox"]'))
test_file = BytesIO(b'this is some text for a test')
test_file.name = "not_really.txt"
r = self.client.post(url,dict(submission_method="upload",file=test_file,apply_to_all=False))
self.assertEqual(r.status_code, 410)
@override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True)
def test_upload_minutes_agenda_interim(self):
session=SessionFactory(meeting__type_id='interim')
for doctype in ('minutes','agenda'):
if doctype=='minutes':
url = urlreverse('ietf.meeting.views.upload_session_minutes',kwargs={'num':session.meeting.number,'session_id':session.id})
else:
url = urlreverse('ietf.meeting.views.upload_session_agenda',kwargs={'num':session.meeting.number,'session_id':session.id})
self.client.logout()
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Upload', str(q("title")))
self.assertFalse(session.presentations.filter(document__type_id=doctype))
test_file = BytesIO(b'this is some text for a test')
test_file.name = "not_really.txt"
r = self.client.post(url,dict(submission_method="upload",file=test_file))
self.assertEqual(r.status_code, 302)
doc = session.presentations.filter(document__type_id=doctype).first().document
self.assertEqual(doc.rev,'00')
# Verify that we don't have dead links
url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym})
top = '/meeting/%s/' % session.meeting.number
self.requests_mock.get(f'{session.notes_url()}/download', text='markdown notes')
self.requests_mock.get(f'{session.notes_url()}/info', text=json.dumps({'title': 'title', 'updatetime': '2021-12-01T17:11:00z'}))
self.crawl_materials(url=url, top=top)
@override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True)
def test_upload_narrativeminutes(self):
for type_id in ["interim","ietf"]:
session=SessionFactory(meeting__type_id=type_id,group__acronym='iesg')
doctype='narrativeminutes'
url = urlreverse('ietf.meeting.views.upload_session_narrativeminutes',kwargs={'num':session.meeting.number,'session_id':session.id})
self.client.logout()
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Upload', str(q("title")))
self.assertFalse(session.presentations.filter(document__type_id=doctype))
test_file = BytesIO(b'this is some text for a test')
test_file.name = "not_really.txt"
r = self.client.post(url,dict(submission_method="upload",file=test_file))
self.assertEqual(r.status_code, 302)
doc = session.presentations.filter(document__type_id=doctype).first().document
self.assertEqual(doc.rev,'00')
# Verify that we don't have dead links
url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym})
top = '/meeting/%s/' % session.meeting.number
self.requests_mock.get(f'{session.notes_url()}/download', text='markdown notes')
self.requests_mock.get(f'{session.notes_url()}/info', text=json.dumps({'title': 'title', 'updatetime': '2021-12-01T17:11:00z'}))
self.crawl_materials(url=url, top=top)
def test_enter_agenda(self):
session = SessionFactory(meeting__type_id='ietf')
url = urlreverse('ietf.meeting.views.upload_session_agenda',kwargs={'num':session.meeting.number,'session_id':session.id})
redirect_url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number,'acronym':session.group.acronym})
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Upload', str(q("Title")))
self.assertFalse(session.presentations.exists())
test_text = 'Enter agenda from scratch'
r = self.client.post(url,dict(submission_method="enter",content=test_text))
self.assertRedirects(r, redirect_url)
doc = session.presentations.filter(document__type_id='agenda').first().document
self.assertEqual(doc.rev,'00')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Revise', str(q("Title")))
test_file = BytesIO(b'Upload after enter')
test_file.name = "some.txt"
r = self.client.post(url,dict(submission_method="upload",file=test_file))
self.assertRedirects(r, redirect_url)
doc = Document.objects.get(pk=doc.pk)
self.assertEqual(doc.rev,'01')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Revise', str(q("Title")))
test_text = 'Enter after upload'
r = self.client.post(url,dict(submission_method="enter",content=test_text))
self.assertRedirects(r, redirect_url)
doc = Document.objects.get(pk=doc.pk)
self.assertEqual(doc.rev,'02')
@override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls
@patch("ietf.meeting.views.SlidesManager")
def test_upload_slides(self, mock_slides_manager_cls):
session1 = SessionFactory(meeting__type_id='ietf')
session2 = SessionFactory(meeting=session1.meeting,group=session1.group)
url = urlreverse('ietf.meeting.views.upload_session_slides',kwargs={'num':session1.meeting.number,'session_id':session1.id})
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertFalse(mock_slides_manager_cls.called)
q = PyQuery(r.content)
self.assertIn('Upload', str(q("title")))
self.assertFalse(session1.presentations.filter(document__type_id='slides'))
test_file = BytesIO(b'this is not really a slide')
test_file.name = 'not_really.txt'
r = self.client.post(url,dict(file=test_file,title='a test slide file',apply_to_all=True,approved=True))
self.assertEqual(r.status_code, 302)
self.assertEqual(session1.presentations.count(),1)
self.assertEqual(session2.presentations.count(),1)
sp = session2.presentations.first()
self.assertEqual(sp.document.name, 'slides-%s-%s-a-test-slide-file' % (session1.meeting.number,session1.group.acronym ) )
self.assertEqual(sp.order,1)
self.assertEqual(mock_slides_manager_cls.call_count, 1)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 2)
# don't care which order they were called in, just that both sessions were updated
self.assertCountEqual(
mock_slides_manager_cls.return_value.add.call_args_list,
[
call(session=session1, slides=sp.document, order=1),
call(session=session2, slides=sp.document, order=1),
],
)
mock_slides_manager_cls.reset_mock()
url = urlreverse('ietf.meeting.views.upload_session_slides',kwargs={'num':session2.meeting.number,'session_id':session2.id})
test_file = BytesIO(b'some other thing still not slidelike')
test_file.name = 'also_not_really.txt'
r = self.client.post(url,dict(file=test_file,title='a different slide file',apply_to_all=False,approved=True))
self.assertEqual(r.status_code, 302)
self.assertEqual(session1.presentations.count(),1)
self.assertEqual(session2.presentations.count(),2)
sp = session2.presentations.get(document__name__endswith='-a-different-slide-file')
self.assertEqual(sp.order,2)
self.assertEqual(sp.rev,'00')
self.assertEqual(sp.document.rev,'00')
self.assertEqual(mock_slides_manager_cls.call_count, 1)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 1)
self.assertEqual(
mock_slides_manager_cls.return_value.add.call_args,
call(session=session2, slides=sp.document, order=2),
)
mock_slides_manager_cls.reset_mock()
url = urlreverse('ietf.meeting.views.upload_session_slides',kwargs={'num':session2.meeting.number,'session_id':session2.id,'name':session2.presentations.get(order=2).document.name})
r = self.client.get(url)
self.assertTrue(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Revise', str(q("title")))
test_file = BytesIO(b'new content for the second slide deck')
test_file.name = 'doesnotmatter.txt'
r = self.client.post(url,dict(file=test_file,title='rename the presentation',apply_to_all=False, approved=True))
self.assertEqual(r.status_code, 302)
self.assertEqual(session1.presentations.count(),1)
self.assertEqual(session2.presentations.count(),2)
replacement_sp = session2.presentations.get(order=2)
self.assertEqual(replacement_sp.rev,'01')
self.assertEqual(replacement_sp.document.rev,'01')
self.assertEqual(mock_slides_manager_cls.call_count, 1)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertEqual(mock_slides_manager_cls.return_value.revise.call_count, 1)
self.assertEqual(
mock_slides_manager_cls.return_value.revise.call_args,
call(session=session2, slides=sp.document),
)
def test_upload_slide_title_bad_unicode(self):
session1 = SessionFactory(meeting__type_id='ietf')
url = urlreverse('ietf.meeting.views.upload_session_slides',kwargs={'num':session1.meeting.number,'session_id':session1.id})
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Upload', str(q("title")))
self.assertFalse(session1.presentations.filter(document__type_id='slides'))
test_file = BytesIO(b'this is not really a slide')
test_file.name = 'not_really.txt'
r = self.client.post(url,dict(file=test_file,title='title with bad character \U0001fabc '))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('form .is-invalid'))
self.assertIn("Unicode BMP", q('form .is-invalid div').text())
@override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls
@patch("ietf.meeting.views.SlidesManager")
def test_remove_sessionpresentation(self, mock_slides_manager_cls):
session = SessionFactory(meeting__type_id='ietf')
agenda = DocumentFactory(type_id='agenda')
doc = DocumentFactory(type_id='slides')
session.presentations.create(document=agenda)
session.presentations.create(document=doc)
url = urlreverse('ietf.meeting.views.remove_sessionpresentation',kwargs={'num':session.meeting.number,'session_id':session.id,'name':'no-such-doc'})
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
self.assertFalse(mock_slides_manager_cls.called)
url = urlreverse('ietf.meeting.views.remove_sessionpresentation',kwargs={'num':session.meeting.number,'session_id':0,'name':doc.name})
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
self.assertFalse(mock_slides_manager_cls.called)
url = urlreverse('ietf.meeting.views.remove_sessionpresentation',kwargs={'num':session.meeting.number,'session_id':session.id,'name':doc.name})
login_testing_unauthorized(self,"secretary",url)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertFalse(mock_slides_manager_cls.called)
# Removing slides should remove the materials and call MeetechoAPI
self.assertEqual(2, session.presentations.count())
response = self.client.post(url,{'remove_session':''})
self.assertEqual(response.status_code, 302)
self.assertEqual(1, session.presentations.count())
self.assertEqual(2, doc.docevent_set.count())
self.assertEqual(mock_slides_manager_cls.call_count, 1)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertEqual(mock_slides_manager_cls.return_value.delete.call_count, 1)
self.assertEqual(
mock_slides_manager_cls.return_value.delete.call_args,
call(session=session, slides=doc),
)
mock_slides_manager_cls.reset_mock()
# Removing non-slides should only remove the materials
url = urlreverse(
"ietf.meeting.views.remove_sessionpresentation",
kwargs={
"num": session.meeting.number,
"session_id": session.id,
"name": agenda.name,
},
)
response = self.client.post(url, {"remove_session" : ""})
self.assertEqual(response.status_code, 302)
self.assertEqual(0, session.presentations.count())
self.assertEqual(2, agenda.docevent_set.count())
self.assertFalse(mock_slides_manager_cls.called)
def test_propose_session_slides(self):
for type_id in ['ietf','interim']:
session = SessionFactory(meeting__type_id=type_id)
chair = RoleFactory(group=session.group,name_id='chair').person
session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20))
newperson = PersonFactory()
session_overview_url = urlreverse('ietf.meeting.views.session_details',kwargs={'num':session.meeting.number,'acronym':session.group.acronym})
upload_url = urlreverse('ietf.meeting.views.upload_session_slides', kwargs={'session_id':session.pk, 'num': session.meeting.number})
r = self.client.get(session_overview_url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertFalse(q('.uploadslides'))
self.assertFalse(q('.proposeslides'))
self.client.login(username=newperson.user.username,password=newperson.user.username+"+password")
r = self.client.get(session_overview_url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertTrue(q('.proposeslides'))
self.client.logout()
login_testing_unauthorized(self,newperson.user.username,upload_url)
r = self.client.get(upload_url)
self.assertEqual(r.status_code,200)
test_file = BytesIO(b'this is not really a slide')
test_file.name = 'not_really.txt'
empty_outbox()
r = self.client.post(upload_url,dict(file=test_file,title='a test slide file',apply_to_all=True,approved=False))
self.assertEqual(r.status_code, 302)
session = Session.objects.get(pk=session.pk)
self.assertEqual(session.slidesubmission_set.count(),1)
self.assertEqual(len(outbox),1)
r = self.client.get(session_overview_url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.proposedslidelist p')), 1)
SlideSubmissionFactory(session = session)
self.client.logout()
self.client.login(username=chair.user.username, password=chair.user.username+"+password")
r = self.client.get(session_overview_url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.proposedslidelist p')), 2)
self.client.logout()
login_testing_unauthorized(self,chair.user.username,upload_url)
r = self.client.get(upload_url)
self.assertEqual(r.status_code,200)
test_file = BytesIO(b'this is not really a slide either')
test_file.name = 'again_not_really.txt'
empty_outbox()
r = self.client.post(upload_url,dict(file=test_file,title='a selfapproved test slide file',apply_to_all=True,approved=True))
self.assertEqual(r.status_code, 302)
self.assertEqual(len(outbox),0)
self.assertEqual(session.slidesubmission_set.count(),2)
self.client.logout()
self.client.login(username=chair.user.username, password=chair.user.username+"+password")
r = self.client.get(session_overview_url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.uploadslidelist p')), 0)
self.client.logout()
def test_disapprove_proposed_slides(self):
submission = SlideSubmissionFactory()
submission.session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20))
self.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(), 1)
chair = RoleFactory(group=submission.session.group,name_id='chair').person
url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number})
login_testing_unauthorized(self, chair.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code,200)
r = self.client.post(url,dict(title='some title',disapprove="disapprove"))
self.assertEqual(r.status_code,302)
self.assertEqual(SlideSubmission.objects.filter(status__slug = 'rejected').count(), 1)
self.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(), 0)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertRegex(r.content.decode(), r"These\s+slides\s+have\s+already\s+been\s+rejected")
@override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls
@patch("ietf.meeting.views.SlidesManager")
def test_approve_proposed_slides(self, mock_slides_manager_cls):
submission = SlideSubmissionFactory()
session = submission.session
session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20))
chair = RoleFactory(group=submission.session.group,name_id='chair').person
url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number})
login_testing_unauthorized(self, chair.user.username, url)
self.assertEqual(submission.status_id, 'pending')
self.assertIsNone(submission.doc)
r = self.client.get(url)
self.assertEqual(r.status_code,200)
empty_outbox()
r = self.client.post(url,dict(title='different title',approve='approve'))
self.assertEqual(r.status_code,302)
self.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(), 0)
self.assertEqual(SlideSubmission.objects.filter(status__slug = 'approved').count(), 1)
submission.refresh_from_db()
self.assertEqual(submission.status_id, 'approved')
self.assertIsNotNone(submission.doc)
self.assertEqual(session.presentations.count(),1)
self.assertEqual(session.presentations.first().document.title,'different title')
self.assertEqual(mock_slides_manager_cls.call_count, 1)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 1)
self.assertEqual(
mock_slides_manager_cls.return_value.add.call_args,
call(session=session, slides=submission.doc, order=1),
)
mock_slides_manager_cls.reset_mock()
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertRegex(r.content.decode(), r"These\s+slides\s+have\s+already\s+been\s+approved")
self.assertFalse(mock_slides_manager_cls.called)
self.assertEqual(len(outbox), 1)
self.assertIn(submission.submitter.email_address(), outbox[0]['To'])
self.assertIn('Slides approved', outbox[0]['Subject'])
@override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls
@patch("ietf.meeting.views.SlidesManager")
def test_approve_proposed_slides_multisession_apply_one(self, mock_slides_manager_cls):
submission = SlideSubmissionFactory(session__meeting__type_id='ietf')
session1 = submission.session
session2 = SessionFactory(group=submission.session.group, meeting=submission.session.meeting)
submission.session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20))
chair = RoleFactory(group=submission.session.group,name_id='chair').person
url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number})
login_testing_unauthorized(self, chair.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertTrue(q('#id_apply_to_all'))
r = self.client.post(url,dict(title='yet another title',approve='approve'))
submission.refresh_from_db()
self.assertIsNotNone(submission.doc)
self.assertEqual(r.status_code,302)
self.assertEqual(session1.presentations.count(),1)
self.assertEqual(session2.presentations.count(),0)
self.assertEqual(mock_slides_manager_cls.call_count, 1)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 1)
self.assertEqual(
mock_slides_manager_cls.return_value.add.call_args,
call(session=session1, slides=submission.doc, order=1),
)
@override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls
@patch("ietf.meeting.views.SlidesManager")
def test_approve_proposed_slides_multisession_apply_all(self, mock_slides_manager_cls):
submission = SlideSubmissionFactory(session__meeting__type_id='ietf')
session1 = submission.session
session2 = SessionFactory(group=submission.session.group, meeting=submission.session.meeting)
submission.session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20))
chair = RoleFactory(group=submission.session.group,name_id='chair').person
url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number})
login_testing_unauthorized(self, chair.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code,200)
r = self.client.post(url,dict(title='yet another title',apply_to_all=1,approve='approve'))
submission.refresh_from_db()
self.assertEqual(r.status_code,302)
self.assertEqual(session1.presentations.count(),1)
self.assertEqual(session2.presentations.count(),1)
self.assertEqual(mock_slides_manager_cls.call_count, 1)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 2)
self.assertCountEqual(
mock_slides_manager_cls.return_value.add.call_args_list,
[
call(session=session1, slides=submission.doc, order=1),
call(session=session2, slides=submission.doc, order=1),
]
)
@override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls
@patch("ietf.meeting.views.SlidesManager")
def test_submit_and_approve_multiple_versions(self, mock_slides_manager_cls):
session = SessionFactory(meeting__type_id='ietf')
chair = RoleFactory(group=session.group,name_id='chair').person
session.meeting.importantdate_set.create(name_id='revsub',date=date_today()+datetime.timedelta(days=20))
newperson = PersonFactory()
upload_url = urlreverse('ietf.meeting.views.upload_session_slides', kwargs={'session_id':session.pk, 'num': session.meeting.number})
login_testing_unauthorized(self,newperson.user.username,upload_url)
test_file = BytesIO(b'this is not really a slide')
test_file.name = 'not_really.txt'
r = self.client.post(upload_url,dict(file=test_file,title='a test slide file',apply_to_all=True,approved=False))
self.assertEqual(r.status_code, 302)
self.client.logout()
submission = SlideSubmission.objects.get(session=session)
approve_url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number})
login_testing_unauthorized(self, chair.user.username, approve_url)
r = self.client.post(approve_url,dict(title=submission.title,approve='approve'))
submission.refresh_from_db()
self.assertEqual(r.status_code,302)
self.client.logout()
self.assertEqual(mock_slides_manager_cls.call_count, 1)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 1)
self.assertEqual(
mock_slides_manager_cls.return_value.add.call_args,
call(session=session, slides=submission.doc, order=1),
)
mock_slides_manager_cls.reset_mock()
self.assertEqual(session.presentations.first().document.rev,'00')
login_testing_unauthorized(self,newperson.user.username,upload_url)
test_file = BytesIO(b'this is not really a slide, but it is another version of it')
test_file.name = 'not_really.txt'
r = self.client.post(upload_url,dict(file=test_file,title='a test slide file',apply_to_all=True))
self.assertEqual(r.status_code, 302)
test_file = BytesIO(b'this is not really a slide, but it is third version of it')
test_file.name = 'not_really.txt'
r = self.client.post(upload_url,dict(file=test_file,title='a test slide file',apply_to_all=True))
self.assertEqual(r.status_code, 302)
self.client.logout()
(first_submission, second_submission) = SlideSubmission.objects.filter(session=session, status__slug = 'pending').order_by('id')
approve_url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':second_submission.pk,'num':second_submission.session.meeting.number})
login_testing_unauthorized(self, chair.user.username, approve_url)
r = self.client.post(approve_url,dict(title=submission.title,approve='approve'))
first_submission.refresh_from_db()
second_submission.refresh_from_db()
self.assertEqual(r.status_code,302)
self.assertEqual(mock_slides_manager_cls.call_count, 1)
self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings"))
self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 0)
self.assertEqual(mock_slides_manager_cls.return_value.revise.call_count, 1)
self.assertEqual(
mock_slides_manager_cls.return_value.revise.call_args,
call(session=session, slides=second_submission.doc),
)
mock_slides_manager_cls.reset_mock()
disapprove_url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':first_submission.pk,'num':first_submission.session.meeting.number})
r = self.client.post(disapprove_url,dict(title='some title',disapprove="disapprove"))
self.assertEqual(r.status_code,302)
self.client.logout()
self.assertFalse(mock_slides_manager_cls.called)
self.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(),0)
self.assertEqual(SlideSubmission.objects.filter(status__slug = 'rejected').count(),1)
self.assertEqual(session.presentations.first().document.rev,'01')
path = os.path.join(submission.session.meeting.get_materials_path(),'slides')
filename = os.path.join(path,session.presentations.first().document.name+'-01.txt')
self.assertTrue(os.path.exists(filename))
fd = io.open(filename, 'r')
contents = fd.read()
fd.close()
self.assertIn('third version', contents)
@override_settings(IETF_NOTES_URL='https://notes.ietf.org/')
class ImportNotesTests(TestCase):
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH']
def setUp(self):
super().setUp()
self.session = SessionFactory(meeting__type_id='ietf')
self.meeting = self.session.meeting
def test_retrieves_note(self):
"""Can import and preview a note from notes.ietf.org"""
url = urlreverse('ietf.meeting.views.import_session_minutes',
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
self.client.login(username='secretary', password='secretary+password')
with requests_mock.Mocker() as mock:
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='markdown text')
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info',
text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"}))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
iframe = q('iframe#preview')
self.assertEqual('<p>markdown text</p>', iframe.attr('srcdoc'))
markdown_text_input = q('form #id_markdown_text')
self.assertEqual(markdown_text_input.val(), 'markdown text')
def test_retrieves_with_broken_metadata(self):
"""Can import and preview a note even if it has a metadata problem"""
url = urlreverse('ietf.meeting.views.import_session_minutes',
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
self.client.login(username='secretary', password='secretary+password')
with requests_mock.Mocker() as mock:
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='markdown text')
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info', text='this is not valid json {]')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
iframe = q('iframe#preview')
self.assertEqual('<p>markdown text</p>', iframe.attr('srcdoc'))
markdown_text_input = q('form #id_markdown_text')
self.assertEqual(markdown_text_input.val(), 'markdown text')
def test_redirects_on_success(self):
"""Redirects to session details page after import"""
url = urlreverse('ietf.meeting.views.import_session_minutes',
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
self.client.login(username='secretary', password='secretary+password')
r = self.client.post(url, {'markdown_text': 'markdown text'})
self.assertRedirects(
r,
urlreverse(
'ietf.meeting.views.session_details',
kwargs={
'num': self.meeting.number,
'acronym': self.session.group.acronym,
},
),
)
def test_imports_previewed_text(self):
"""Import text that was shown as preview even if notes site is updated"""
url = urlreverse('ietf.meeting.views.import_session_minutes',
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
self.client.login(username='secretary', password='secretary+password')
with requests_mock.Mocker() as mock:
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='updated markdown text')
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info',
text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"}))
r = self.client.post(url, {'markdown_text': 'original markdown text'})
self.assertEqual(r.status_code, 302)
minutes_path = Path(self.meeting.get_materials_path()) / 'minutes'
with (minutes_path / self.session.minutes().uploaded_filename).open() as f:
self.assertEqual(f.read(), 'original markdown text')
def test_refuses_identical_import(self):
"""Should not be able to import text identical to the current revision"""
url = urlreverse('ietf.meeting.views.import_session_minutes',
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
self.client.login(username='secretary', password='secretary+password')
with requests_mock.Mocker() as mock:
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='original markdown text')
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info',
text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"}))
# Create a revision. Run the original text through the preprocessing done when importing
# from the notes site.
r = self.client.get(url) # let GET do its preprocessing
q = PyQuery(r.content)
r = self.client.post(url, {'markdown_text': q('input[name="markdown_text"]').attr['value']})
self.assertEqual(r.status_code, 302)
r = self.client.get(url) # try to import the same text
self.assertContains(r, "This document is identical", status_code=200)
q = PyQuery(r.content)
self.assertEqual(len(q('#content button:disabled[type="submit"]')), 1)
self.assertEqual(len(q('#content button:enabled[type="submit"]')), 0)
def test_allows_import_on_existing_bad_unicode(self):
"""Should not be able to import text identical to the current revision"""
url = urlreverse('ietf.meeting.views.import_session_minutes',
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
self.client.login(username='secretary', password='secretary+password')
r = self.client.post(url, {'markdown_text': 'replaced below'}) # create a rev
with open(
self.session.presentations.filter(document__type="minutes").first().document.get_file_name(),
'wb'
) as f:
# Replace existing content with an invalid Unicode byte string. The particular invalid
# values here are accented characters in the MacRoman charset (see ticket #3756).
f.write(b'invalid \x8e unicode \x99\n')
self.assertEqual(r.status_code, 302)
with requests_mock.Mocker() as mock:
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='original markdown text')
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info',
text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"}))
r = self.client.get(url) # try to import the same text
self.assertNotContains(r, "This document is identical", status_code=200)
q = PyQuery(r.content)
self.assertEqual(len(q('#content button:enabled[type="submit"]')), 1)
self.assertEqual(len(q('#content button:disabled[type="submit"]')), 0)
def test_handles_missing_previous_revision_file(self):
"""Should still allow import if the file for the previous revision is missing"""
url = urlreverse('ietf.meeting.views.import_session_minutes',
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
self.client.login(username='secretary', password='secretary+password')
r = self.client.post(url, {'markdown_text': 'original markdown text'}) # create a rev
# remove the file uploaded for the first rev
minutes_docs = self.session.presentations.filter(document__type='minutes')
self.assertEqual(minutes_docs.count(), 1)
Path(minutes_docs.first().document.get_file_name()).unlink()
self.assertEqual(r.status_code, 302)
with requests_mock.Mocker() as mock:
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='original markdown text')
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info',
text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"}))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
iframe = q('iframe#preview')
self.assertEqual('<p>original markdown text</p>', iframe.attr('srcdoc'))
markdown_text_input = q('form #id_markdown_text')
self.assertEqual(markdown_text_input.val(), 'original markdown text')
def test_handles_note_does_not_exist(self):
"""Should not try to import a note that does not exist"""
url = urlreverse('ietf.meeting.views.import_session_minutes',
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
self.client.login(username='secretary', password='secretary+password')
with requests_mock.Mocker() as mock:
mock.get(requests_mock.ANY, status_code=404)
r = self.client.get(url, follow=True)
self.assertContains(r, 'Could not import', status_code=200)
def test_handles_notes_server_failure(self):
"""Problems communicating with the notes server should be handled gracefully"""
url = urlreverse('ietf.meeting.views.import_session_minutes',
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
self.client.login(username='secretary', password='secretary+password')
with requests_mock.Mocker() as mock:
mock.get(re.compile(r'.+/download'), exc=requests.exceptions.ConnectTimeout)
mock.get(re.compile(r'.+//info'), text='{}')
r = self.client.get(url, follow=True)
self.assertContains(r, 'Could not reach the notes server', status_code=200)
class SessionTests(TestCase):
def test_get_summary_by_area(self):
meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100'))
sessions = Session.objects.filter(meeting=meeting).with_current_status()
data = get_summary_by_area(sessions)
self.assertEqual(data[0][0], 'Duration')
self.assertGreater(len(data), 2)
self.assertEqual(data[-1][0], 'Total Hours')
def test_get_summary_by_type(self):
meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100'))
sessions = Session.objects.filter(meeting=meeting).with_current_status()
data = get_summary_by_type(sessions)
self.assertEqual(data[0][0], 'Group Type')
self.assertGreater(len(data), 2)
def test_get_summary_by_purpose(self):
meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100'))
sessions = Session.objects.filter(meeting=meeting).with_current_status()
data = get_summary_by_purpose(sessions)
self.assertEqual(data[0][0], 'Purpose')
self.assertGreater(len(data), 2)
def test_meeting_requests(self):
meeting = MeetingFactory(type_id='ietf')
# a couple non-wg group types, confirm that their has_meetings features are as expected
group_type_with_meetings = 'adhoc'
self.assertTrue(GroupFeatures.objects.get(pk=group_type_with_meetings).has_meetings)
group_type_without_meetings = 'sdo'
self.assertFalse(GroupFeatures.objects.get(pk=group_type_without_meetings).has_meetings)
area = GroupFactory(type_id='area', acronym='area')
requested_session = SessionFactory(meeting=meeting,group__parent=area,status_id='schedw',add_to_schedule=False)
conflicting_session = SessionFactory(meeting=meeting,group__parent=area,status_id='schedw',add_to_schedule=False)
ConstraintFactory(name_id='key_participant',meeting=meeting,source=requested_session.group,target=conflicting_session.group)
not_meeting = SessionFactory(meeting=meeting,group__parent=area,status_id='notmeet',add_to_schedule=False)
has_meetings = SessionFactory(
meeting=meeting,
group__type_id=group_type_with_meetings,
status_id='schedw',
add_to_schedule=False,
)
has_meetings_not_meeting = SessionFactory(
meeting=meeting,
group__type_id=group_type_with_meetings,
status_id='notmeet',
add_to_schedule=False,
)
# admin and social sessions are not to be shown on the requests page
has_meetings_admin_session = SessionFactory(
meeting=meeting,
group__type_id=group_type_with_meetings,
status_id='schedw',
purpose_id='admin',
type_id='other',
add_to_schedule=False,
)
has_meetings_social_session = SessionFactory(
meeting=meeting,
group__type_id=group_type_with_meetings,
status_id='schedw',
purpose_id='social',
type_id='break',
add_to_schedule=False,
)
not_has_meetings = SessionFactory(
meeting=meeting,
group__type_id=group_type_without_meetings,
status_id='schedw',
add_to_schedule=False,
)
# bof sessions should be shown
bof_session = SessionFactory(
meeting=meeting,
group__parent=area,
group__state_id='bof',
status_id='schedw',
add_to_schedule=False,
)
# proposed WG sessions should be shown
proposed_wg_session = SessionFactory(
meeting=meeting,
group__parent=area,
group__state_id='proposed',
status_id='schedw',
add_to_schedule=False,
)
# rg sessions should be shown under 'irtf' heading
rg_session = SessionFactory(
meeting=meeting,
group__type_id='rg',
status_id='schedw',
add_to_schedule=False,
)
session_with_none_purpose = SessionFactory(
meeting=meeting,
group__parent=area,
purpose_id="none",
status_id="schedw",
add_to_schedule=False,
)
tutorial_session = SessionFactory(
meeting=meeting,
group__parent=area,
purpose_id="tutorial",
status_id="schedw",
add_to_schedule=False,
)
def _sreq_edit_link(sess):
return urlreverse(
'ietf.secr.sreq.views.edit',
kwargs={
'num': meeting.number,
'acronym': sess.group.acronym,
},
)
url = urlreverse('ietf.meeting.views.meeting_requests',kwargs={'num':meeting.number})
r = self.client.get(url)
# requested_session group should be listed with a link to the request
self.assertContains(r, requested_session.group.acronym)
self.assertContains(r, _sreq_edit_link(requested_session)) # link to the session request
self.assertContains(r, not_meeting.group.acronym)
# The admin/social session groups should be listed under "no timeslot request received"; it's easier
# to check that the group is listed but that there is no link to the session request than to try to
# parse the HTML. If the view is changed to link to the "no timeslot request received" session requests,
# then need to revisit.
self.assertContains(r, has_meetings_admin_session.group.acronym)
self.assertNotContains(r, _sreq_edit_link(has_meetings_admin_session)) # no link to the session request
self.assertContains(r, has_meetings_social_session.group.acronym)
self.assertNotContains(r, _sreq_edit_link(has_meetings_social_session)) # no link to the session request
self.assertContains(r, requested_session.constraints().first().name)
self.assertContains(r, conflicting_session.group.acronym)
self.assertContains(r, _sreq_edit_link(conflicting_session)) # link to the session request
self.assertContains(r, has_meetings.group.acronym)
self.assertContains(r, _sreq_edit_link(has_meetings)) # link to the session request
self.assertContains(r, has_meetings_not_meeting.group.acronym)
self.assertContains(r, _sreq_edit_link(has_meetings_not_meeting)) # link to the session request
self.assertNotContains(r, not_has_meetings.group.acronym)
self.assertNotContains(r, _sreq_edit_link(not_has_meetings)) # no link to the session request
self.assertContains(r, bof_session.group.acronym)
self.assertContains(r, _sreq_edit_link(bof_session)) # link to the session request
self.assertContains(r, proposed_wg_session.group.acronym)
self.assertContains(r, _sreq_edit_link(proposed_wg_session)) # link to the session request
self.assertContains(r, rg_session.group.acronym)
self.assertContains(r, _sreq_edit_link(rg_session)) # link to the session request
self.assertContains(r, session_with_none_purpose.group.acronym)
self.assertContains(r, tutorial_session.group.acronym)
# check headings - note that the special types (has_meetings, etc) do not have a group parent
# so they show up in 'other'
q = PyQuery(r.content)
self.assertEqual(len(q('h2#area')), 1)
self.assertEqual(len(q('h2#other-groups')), 1)
self.assertEqual(len(q('h2#irtf')), 1) # rg group has irtf group as parent
# check rounded pills
self.assertNotContains( # no rounded pill for sessions with regular purpose
r,
'<span class="badge rounded-pill text-bg-info">Regular</span>',
html=True,
)
self.assertNotContains( # no rounded pill for session with no purpose specified
r,
'<span class="badge rounded-pill text-bg-info">None</span>',
html=True,
)
self.assertContains( # rounded pill for session with non-regular purpose
r,
'<span class="badge rounded-pill text-bg-info">Tutorial</span>',
html=True,
)
def test_request_minutes(self):
meeting = MeetingFactory(type_id='ietf')
area = GroupFactory(type_id='area')
has_minutes = SessionFactory(meeting=meeting,group__parent=area)
has_no_minutes = SessionFactory(meeting=meeting,group__parent=area)
SessionPresentation.objects.create(session=has_minutes,document=DocumentFactory(type_id='minutes'))
empty_outbox()
url = urlreverse('ietf.meeting.views.request_minutes',kwargs={'num':meeting.number})
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertNotContains(r, has_minutes.group.acronym.upper())
self.assertContains(r, has_no_minutes.group.acronym.upper())
r = self.client.post(url,{'to':'wgchairs@ietf.org',
'cc': 'irsg@irtf.org',
'subject': 'I changed the subject',
'body': 'corpus',
})
self.assertEqual(r.status_code,302)
self.assertEqual(len(outbox),1)
@override_settings(YOUTUBE_DOMAINS=["youtube.com"])
def test_add_session_recordings(self):
session = SessionFactory(meeting__type_id="ietf")
url = urlreverse(
"ietf.meeting.views.add_session_recordings",
kwargs={"session_id": session.pk, "num": session.meeting.number},
)
# does not fully validate authorization for non-secretariat users :-(
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
pq = PyQuery(r.content)
title_input = pq("input#id_title")
self.assertIsNotNone(title_input)
self.assertEqual(
title_input.attr.value,
"Video recording of {acro} for {timestamp}".format(
acro=session.group.acronym,
timestamp=session.official_timeslotassignment().timeslot.utc_start_time().strftime(
"%Y-%m-%d %H:%M"
),
),
)
with patch("ietf.meeting.views.create_recording") as mock_create:
r = self.client.post(
url,
data={
"title": "This is my video title",
"url": "",
}
)
self.assertFalse(mock_create.called)
with patch("ietf.meeting.views.create_recording") as mock_create:
r = self.client.post(
url,
data={
"title": "This is my video title",
"url": "https://yubtub.com/this-is-not-a-youtube-video",
}
)
self.assertFalse(mock_create.called)
with patch("ietf.meeting.views.create_recording") as mock_create:
r = self.client.post(
url,
data={
"title": "This is my video title",
"url": "https://youtube.com/finally-a-video",
}
)
self.assertTrue(mock_create.called)
self.assertEqual(
mock_create.call_args,
call(
session,
"https://youtube.com/finally-a-video",
title="This is my video title",
user=Person.objects.get(user__username="secretary"),
),
)
# CAN delete session presentation for this session
sp = SessionPresentationFactory(
session=session,
document__type_id="recording",
document__external_url="https://example.com/some-video",
)
with patch("ietf.meeting.views.delete_recording") as mock_delete:
r = self.client.post(
url,
data={
"delete": str(sp.pk),
}
)
self.assertEqual(r.status_code, 200)
self.assertTrue(mock_delete.called)
self.assertEqual(mock_delete.call_args, call(sp))
# ValueError message from delete_recording does not reach the user
sp = SessionPresentationFactory(
session=session,
document__type_id="recording",
document__external_url="https://example.com/some-video",
)
with patch("ietf.meeting.views.delete_recording", side_effect=ValueError("oh joy!")) as mock_delete:
r = self.client.post(
url,
data={
"delete": str(sp.pk),
}
)
self.assertTrue(mock_delete.called)
self.assertNotContains(r, "oh joy!", status_code=200)
# CANNOT delete session presentation for a different session
sp_for_other_session = SessionPresentationFactory(
document__type_id="recording",
document__external_url="https://example.com/some-other-video",
)
with patch("ietf.meeting.views.delete_recording") as mock_delete:
r = self.client.post(
url,
data={
"delete": str(sp_for_other_session.pk),
}
)
self.assertEqual(r.status_code, 404)
self.assertFalse(mock_delete.called)
class HasMeetingsTests(TestCase):
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH']
def do_request_interim(self, url, group, user, meeting_count):
login_testing_unauthorized(self,user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('#id_group option[value="%d"]'%group.pk))
date = date_today() + datetime.timedelta(days=30+meeting_count)
time = time_now().replace(microsecond=0,second=0)
remote_instructions = 'Use webex'
agenda = 'Intro. Slides. Discuss.'
agenda_note = 'On second level'
meeting_count = Meeting.objects.filter(number__contains='-%s-'%group.acronym, date__year=date.year).count()
next_num = "%02d" % (meeting_count+1)
data = {'group':group.pk,
'meeting_type':'single',
'city':'',
'country':'',
'time_zone':'UTC',
'session_set-0-date':date.strftime("%Y-%m-%d"),
'session_set-0-time':time.strftime('%H:%M'),
'session_set-0-requested_duration':'03:00:00',
'session_set-0-remote_instructions':remote_instructions,
'session_set-0-agenda':agenda,
'session_set-0-agenda_note':agenda_note,
'session_set-TOTAL_FORMS':1,
'session_set-INITIAL_FORMS':0,
'session_set-MIN_NUM_FORMS':0,
'session_set-MAX_NUM_FORMS':1000}
empty_outbox()
r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data)
self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming'))
meeting = Meeting.objects.order_by('id').last()
self.assertEqual(meeting.type_id,'interim')
self.assertEqual(meeting.date,date)
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year, group.acronym, next_num))
self.assertTrue(len(outbox)>0)
self.assertIn('interim approved',outbox[0]["Subject"])
self.assertIn(user.person.email().address,outbox[0]["To"])
self.client.logout()
def create_role_for_authrole(self, authrole):
role = None
if authrole == 'Secretariat':
role = RoleFactory.create(group__acronym='secretariat',name_id='secr')
elif authrole == 'Area Director':
role = RoleFactory.create(name_id='ad', group__type_id='area')
elif authrole == 'IAB':
role = RoleFactory.create(name_id='member', group__acronym='iab')
elif authrole == 'IRTF Chair':
role = RoleFactory.create(name_id='chair', group__acronym='irtf')
if role is None:
self.assertIsNone("Can't test authrole:"+authrole)
self.assertNotEqual(role, None)
return role
def test_can_request_interim(self):
url = urlreverse('ietf.meeting.views.interim_request')
for gf in GroupFeatures.objects.filter(has_meetings=True):
meeting_count = 0
for role in gf.groupman_roles:
role = RoleFactory(group__type_id=gf.type_id, name_id=role)
self.do_request_interim(url, role.group, role.person.user, meeting_count)
for authrole in gf.groupman_authroles:
group = GroupFactory(type_id=gf.type_id)
role = self.create_role_for_authrole(authrole)
self.do_request_interim(url, group, role.person.user, 0)
def test_cannot_request_interim(self):
url = urlreverse('ietf.meeting.views.interim_request')
self.client.login(username='secretary', password='secretary+password')
nomeetings = []
for gf in GroupFeatures.objects.exclude(has_meetings=True):
nomeetings.append(GroupFactory(type_id=gf.type_id))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
for group in nomeetings:
self.assertFalse(q('#id_group option[value="%d"]'%group.pk))
self.client.logout()
all_role_names = set(RoleName.objects.values_list('slug',flat=True))
for gf in GroupFeatures.objects.filter(has_meetings=True):
for role_name in all_role_names - set(gf.groupman_roles):
role = RoleFactory(group__type_id=gf.type_id,name_id=role_name)
self.assertFalse(can_request_interim_meeting(role.person.user))
def test_appears_on_upcoming(self):
url = urlreverse('ietf.meeting.views.upcoming')
sessions=[]
for gf in GroupFeatures.objects.filter(has_meetings=True):
session = SessionFactory(
group__type_id = gf.type_id,
meeting__type_id='interim',
meeting__date = timezone.now()+datetime.timedelta(days=30),
status_id='sched',
)
sessions.append(session)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
for session in sessions:
self.assertIn(session.meeting.number, q('.interim-meeting-link').text())
def test_appears_on_pending(self):
url = urlreverse('ietf.meeting.views.interim_pending')
sessions=[]
for gf in GroupFeatures.objects.filter(has_meetings=True):
group = GroupFactory(type_id=gf.type_id)
meeting_date = timezone.now() + datetime.timedelta(days=30)
session = SessionFactory(
group=group,
meeting__type_id='interim',
meeting__date = meeting_date,
meeting__number = 'interim-%d-%s-00'%(meeting_date.year,group.acronym),
status_id='apprw',
)
sessions.append(session)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
for session in sessions:
self.assertIn(session.meeting.number, q('.interim-meeting-link').text())
def test_appears_on_announce(self):
url = urlreverse('ietf.meeting.views.interim_announce')
sessions=[]
for gf in GroupFeatures.objects.filter(has_meetings=True):
group = GroupFactory(type_id=gf.type_id)
meeting_date = timezone.now() + datetime.timedelta(days=30)
session = SessionFactory(
group=group,
meeting__type_id='interim',
meeting__date = meeting_date,
meeting__number = 'interim-%d-%s-00'%(meeting_date.year,group.acronym),
status_id='scheda',
)
sessions.append(session)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
for session in sessions:
self.assertIn(session.meeting.number, q('.interim-meeting-link').text())
class AgendaFilterTests(TestCase):
"""Tests for the AgendaFilter template"""
def test_agenda_filter_template(self):
"""Test rendering of input data by the agenda filter template"""
def _assert_button_ok(btn, expected_label=None, expected_filter_item=None,
expected_filter_keywords=None):
"""Test button properties"""
if expected_label:
self.assertIn(btn.text(), expected_label)
self.assertEqual(btn.attr('data-filter-item'), expected_filter_item)
self.assertEqual(btn.attr('data-filter-keywords'), expected_filter_keywords)
template = Template('{% include "meeting/agenda_filter.html" %}')
# Test with/without custom button text
context = Context({'customize_button_text': None, 'filter_categories': []})
q = PyQuery(template.render(context))
self.assertIn('Customize...', q('h2.accordion-header').text())
self.assertEqual(q('table'), []) # no filter_categories, so no button table
context['customize_button_text'] = 'My custom text...'
q = PyQuery(template.render(context))
self.assertIn(context['customize_button_text'], q('h2.accordion-header').text())
self.assertEqual(q('table'), []) # no filter_categories, so no button table
# Now add a non-trivial set of filters
context['filter_categories'] = [
[ # first category
dict(
label='area0',
keyword='keyword0',
children=[
dict(
label='child00',
keyword='keyword00',
toggled_by=['keyword0'],
is_bof=False,
),
dict(
label='child01',
keyword='keyword01',
toggled_by=['keyword0', 'bof'],
is_bof=True,
),
]),
dict(
label='area1',
keyword='keyword1',
children=[
dict(
label='child10',
keyword='keyword10',
toggled_by=['keyword1'],
is_bof=False,
),
dict(
label='child11',
keyword='keyword11',
toggled_by=['keyword1', 'bof'],
is_bof=True,
),
]),
],
[ # second category
dict(
label='area2',
keyword='keyword2',
children=[
dict(
label='child20',
keyword='keyword20',
toggled_by=['keyword2', 'bof'],
is_bof=True,
),
dict(
label='child21',
keyword='keyword21',
toggled_by=['keyword2'],
is_bof=False,
),
]),
],
[ # third category
dict(
label=None,
keyword=None,
children=[
dict(
label='child30',
keyword='keyword30',
toggled_by=[],
is_bof=False,
),
dict(
label='child31',
keyword='keyword31',
toggled_by=['bof'],
is_bof=True,
),
]),
],
]
q = PyQuery(template.render(context))
self.assertIn(context['customize_button_text'], q('h2.accordion-header').text())
self.assertNotEqual(q('button.pickview'), []) # should now have group buttons
# Check that buttons are present for the expected things
header_row = q('.col-1 .row:first')
self.assertEqual(len(header_row), 4)
button_row = q('.row.view')
self.assertEqual(len(button_row), 4)
# verify correct headers
header_cells = header_row('.row')
self.assertEqual(len(header_cells), 4)
header_buttons = header_cells('button.pickview')
self.assertEqual(len(header_buttons), 3) # last column has disabled header, so only 3
# verify buttons
button_cells = button_row('.btn-group-vertical')
# area0
_assert_button_ok(header_cells.eq(0)('button.keyword0'),
expected_label='area0',
expected_filter_item='keyword0')
buttons = button_cells.eq(0)('button.pickview')
self.assertEqual(len(buttons), 2) # two children
_assert_button_ok(buttons('.keyword00'),
expected_label='child00',
expected_filter_item='keyword00',
expected_filter_keywords='keyword0')
_assert_button_ok(buttons('.keyword01'),
expected_label='child01',
expected_filter_item='keyword01',
expected_filter_keywords='keyword0,bof')
# area1
_assert_button_ok(header_cells.eq(1)('button.keyword1'),
expected_label='area1',
expected_filter_item='keyword1')
buttons = button_cells.eq(1)('button.pickview')
self.assertEqual(len(buttons), 2) # two children
_assert_button_ok(buttons('.keyword10'),
expected_label='child10',
expected_filter_item='keyword10',
expected_filter_keywords='keyword1')
_assert_button_ok(buttons('.keyword11'),
expected_label='child11',
expected_filter_item='keyword11',
expected_filter_keywords='keyword1,bof')
# area2
_assert_button_ok(header_cells.eq(2)('button.keyword2'),
expected_label='area2',
expected_filter_item='keyword2')
buttons = button_cells.eq(2)('button.pickview')
self.assertEqual(len(buttons), 2) # two children
_assert_button_ok(buttons('.keyword20'),
expected_label='child20',
expected_filter_item='keyword20',
expected_filter_keywords='keyword2,bof')
_assert_button_ok(buttons('.keyword21'),
expected_label='child21',
expected_filter_item='keyword21',
expected_filter_keywords='keyword2')
# area3
_assert_button_ok(header_cells.eq(3)('button.keyword2'),
expected_label=None,
expected_filter_item=None)
buttons = button_cells.eq(3)('button.pickview')
self.assertEqual(len(buttons), 2) # two children
_assert_button_ok(buttons('.keyword30'),
expected_label='child30',
expected_filter_item='keyword30',
expected_filter_keywords=None)
_assert_button_ok(buttons('.keyword31'),
expected_label='child31',
expected_filter_item='keyword31',
expected_filter_keywords='bof')
def logo_file(width=128, height=128, format='PNG', ext=None):
img = Image.new('RGB', (width, height)) # just a black image
data = BytesIO()
img.save(data, format=format)
data.seek(0)
data.name = f'logo.{ext if ext is not None else format.lower()}'
return data
class MeetingHostTests(BaseMeetingTestCase):
def _assertHostFieldCountGreaterEqual(self, r, min_count):
q = PyQuery(r.content)
self.assertGreaterEqual(
len(q('input[type="text"][name^="meetinghosts-"][name$="-name"]')),
min_count,
f'Must have at least {min_count} host name field(s)',
)
self.assertGreaterEqual(
len(q('input[type="file"][name^="meetinghosts-"][name$="-logo"]')),
min_count,
f'Must have at least {min_count} host logo field(s)',
)
def _create_first_host(self, meeting, logo, url):
"""Helper to create a first host via POST"""
return self.client.post(
url,
{
'meetinghosts-TOTAL_FORMS': '2',
'meetinghosts-INITIAL_FORMS': '0',
'meetinghosts-MIN_NUM_FORMS': '0',
'meetinghosts-MAX_NUM_FORMS': '1000',
'meetinghosts-0-id': '',
'meetinghosts-0-meeting': str(meeting.pk),
'meetinghosts-0-name': 'Some Sponsor, Inc.',
'meetinghosts-0-logo': logo,
'meetinghosts-1-id': '',
'meetinghosts-1-meeting': str(meeting.pk),
'meetinghosts-1-name': '',
},
)
def test_permissions(self):
meeting = MeetingFactory(type_id='ietf')
url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number))
self.client.logout()
login_testing_unauthorized(self, 'ad', url)
login_testing_unauthorized(self, 'secretary', url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.client.logout()
login_testing_unauthorized(self, 'ad', url, method='post')
login_testing_unauthorized(self, 'secretary', url, method='post')
# don't bother checking a real post - it'll be tested in other methods
def _assertMatch(self, value, pattern):
self.assertIsNotNone(re.match(pattern, value))
def test_add(self):
"""Can add a new meeting host"""
meeting = MeetingFactory(type_id='ietf')
url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number))
# get the edit page to check that it has the necessary fields
self.client.login(username='secretary', password='secretary+password')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self._assertHostFieldCountGreaterEqual(r, 1)
# post our response
logos = [logo_file() for _ in range(2)]
r = self._create_first_host(meeting, logos[0], url)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number)))
self.assertEqual(meeting.meetinghosts.count(), 1)
host = meeting.meetinghosts.first()
self.assertEqual(host.name, 'Some Sponsor, Inc.')
logo_filename = Path(host.logo.path)
self._assertMatch(logo_filename.name, r'logo-[a-z]+.png')
self.assertCountEqual(
logo_filename.parent.iterdir(),
[logo_filename],
'Unexpected or missing files in the output directory',
)
# retrieve the page again to ensure we have more fields
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self._assertHostFieldCountGreaterEqual(r, 2) # must have at least one extra
# post our response to add an additional host
r = self.client.post(
url,
{
'meetinghosts-TOTAL_FORMS': '3',
'meetinghosts-INITIAL_FORMS': '1',
'meetinghosts-MIN_NUM_FORMS': '0',
'meetinghosts-MAX_NUM_FORMS': '1000',
'meetinghosts-0-id': str(host.pk),
'meetinghosts-0-meeting': str(meeting.pk),
'meetinghosts-0-name': 'Some Sponsor, Inc.',
'meetinghosts-1-id':'',
'meetinghosts-1-meeting': str(meeting.pk),
'meetinghosts-1-name': 'Another Sponsor, Ltd.',
'meetinghosts-1-logo': logos[1],
'meetinghosts-2-id':'',
'meetinghosts-2-meeting': str(meeting.pk),
'meetinghosts-2-name': '',
},
)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number)))
self.assertEqual(meeting.meetinghosts.count(), 2)
host = meeting.meetinghosts.first()
self.assertEqual(host.name, 'Some Sponsor, Inc.')
logo_filename = Path(host.logo.path)
self._assertMatch(logo_filename.name, r'logo-[a-z]+.png')
host = meeting.meetinghosts.last()
self.assertEqual(host.name, 'Another Sponsor, Ltd.')
logo2_filename = Path(host.logo.path)
self._assertMatch(logo2_filename.name, r'logo-[a-z]+.png')
self.assertCountEqual(
logo_filename.parent.iterdir(),
[logo_filename, logo2_filename],
'Unexpected or missing files in the output directory',
)
# retrieve the page again to ensure we have yet more fields
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self._assertHostFieldCountGreaterEqual(r, 3) # must have at least one extra
def test_edit_name(self):
"""Can change name of meeting host
The main complication is checking that the file has been
renamed to match the new host name.
"""
meeting = MeetingFactory(type_id='ietf')
url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number))
# create via UI so we don't have to deal with creating storage paths
self.client.login(username='secretary', password='secretary+password')
logo = logo_file()
r = self._create_first_host(meeting, logo, url)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number)))
self.assertEqual(meeting.meetinghosts.count(), 1)
host = meeting.meetinghosts.first()
self.assertEqual(host.name, 'Some Sponsor, Inc.')
orig_logopath = Path(host.logo.path)
self._assertMatch(orig_logopath.name, r'logo-[a-z]+.png')
self.assertTrue(orig_logopath.exists())
# post our response to modify the name
r = self.client.post(
url,
{
'meetinghosts-TOTAL_FORMS': '3',
'meetinghosts-INITIAL_FORMS': '1',
'meetinghosts-MIN_NUM_FORMS': '0',
'meetinghosts-MAX_NUM_FORMS': '1000',
'meetinghosts-0-id': str(host.pk),
'meetinghosts-0-meeting': str(meeting.pk),
'meetinghosts-0-name': 'Modified Sponsor, Inc.',
'meetinghosts-1-id':'',
'meetinghosts-1-meeting': str(meeting.pk),
'meetinghosts-1-name': '',
'meetinghosts-2-id':'',
'meetinghosts-2-meeting': str(meeting.pk),
'meetinghosts-2-name': '',
},
)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number)))
self.assertEqual(meeting.meetinghosts.count(), 1)
host = meeting.meetinghosts.first()
self.assertEqual(host.name, 'Modified Sponsor, Inc.')
second_logopath = Path(host.logo.path)
self.assertEqual(second_logopath, orig_logopath)
self.assertTrue(second_logopath.exists())
with second_logopath.open('rb') as f:
self.assertEqual(f.read(), logo.getvalue())
def test_meeting_host_replace_logo(self):
"""Can replace logo of a meeting host"""
meeting = MeetingFactory(type_id='ietf')
url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number))
# create via UI so we don't have to deal with creating storage paths
self.client.login(username='secretary', password='secretary+password')
logo = logo_file()
r = self._create_first_host(meeting, logo, url)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number)))
self.assertEqual(meeting.meetinghosts.count(), 1)
host = meeting.meetinghosts.first()
self.assertEqual(host.name, 'Some Sponsor, Inc.')
orig_logopath = Path(host.logo.path)
self._assertMatch(orig_logopath.name, r'logo-[a-z]+.png')
self.assertTrue(orig_logopath.exists())
# post our response to replace the logo
new_logo = logo_file(200, 200) # different size to distinguish images
r = self.client.post(
url,
{
'meetinghosts-TOTAL_FORMS': '3',
'meetinghosts-INITIAL_FORMS': '1',
'meetinghosts-MIN_NUM_FORMS': '0',
'meetinghosts-MAX_NUM_FORMS': '1000',
'meetinghosts-0-id': str(host.pk),
'meetinghosts-0-meeting': str(meeting.pk),
'meetinghosts-0-name': 'Some Sponsor, Inc.',
'meetinghosts-0-logo': new_logo,
'meetinghosts-1-id':'',
'meetinghosts-1-meeting': str(meeting.pk),
'meetinghosts-1-name': '',
'meetinghosts-2-id':'',
'meetinghosts-2-meeting': str(meeting.pk),
'meetinghosts-2-name': '',
},
)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number)))
self.assertEqual(meeting.meetinghosts.count(), 1)
host = meeting.meetinghosts.first()
self.assertEqual(host.name, 'Some Sponsor, Inc.')
second_logopath = Path(host.logo.path)
self._assertMatch(second_logopath.name, r'logo-[a-z]+.png')
self.assertTrue(second_logopath.exists())
with second_logopath.open('rb') as f:
self.assertEqual(f.read(), new_logo.getvalue())
def test_change_name_and_replace_logo(self):
"""Can simultaneously change name and replace logo"""
meeting = MeetingFactory(type_id='ietf')
url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number))
# create via UI so we don't have to deal with creating storage paths
self.client.login(username='secretary', password='secretary+password')
logo = logo_file()
r = self._create_first_host(meeting, logo, url)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number)))
self.assertEqual(meeting.meetinghosts.count(), 1)
host = meeting.meetinghosts.first()
self.assertEqual(host.name, 'Some Sponsor, Inc.')
orig_logopath = Path(host.logo.path)
self._assertMatch(orig_logopath.name, r'logo-[a-z]+.png')
self.assertTrue(orig_logopath.exists())
# post our response to replace the logo
new_logo = logo_file(200, 200) # different size to distinguish images
r = self.client.post(
url,
{
'meetinghosts-TOTAL_FORMS': '3',
'meetinghosts-INITIAL_FORMS': '1',
'meetinghosts-MIN_NUM_FORMS': '0',
'meetinghosts-MAX_NUM_FORMS': '1000',
'meetinghosts-0-id': str(host.pk),
'meetinghosts-0-meeting': str(meeting.pk),
'meetinghosts-0-name': 'Modified Sponsor, Ltd.',
'meetinghosts-0-logo': new_logo,
'meetinghosts-1-id':'',
'meetinghosts-1-meeting': str(meeting.pk),
'meetinghosts-1-name': '',
'meetinghosts-2-id':'',
'meetinghosts-2-meeting': str(meeting.pk),
'meetinghosts-2-name': '',
},
)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number)))
self.assertEqual(meeting.meetinghosts.count(), 1)
host = meeting.meetinghosts.first()
self.assertEqual(host.name, 'Modified Sponsor, Ltd.')
second_logopath = Path(host.logo.path)
self._assertMatch(second_logopath.name, r'logo-[a-z]+.png')
self.assertTrue(second_logopath.exists())
with second_logopath.open('rb') as f:
self.assertEqual(f.read(), new_logo.getvalue())
self.assertFalse(orig_logopath.exists())
def test_remove(self):
"""Can delete a meeting host and its logo"""
meeting = MeetingFactory(type_id='ietf')
url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number))
# create via UI so we don't have to deal with creating storage paths
self.client.login(username='secretary', password='secretary+password')
logo = logo_file()
r = self._create_first_host(meeting, logo, url)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number)))
self.assertEqual(meeting.meetinghosts.count(), 1)
host = meeting.meetinghosts.first()
self.assertEqual(host.name, 'Some Sponsor, Inc.')
logopath = Path(host.logo.path)
self._assertMatch(logopath.name, r'logo-[a-z]+.png')
self.assertTrue(logopath.exists())
# now delete
r = self.client.post(
url,
{
'meetinghosts-TOTAL_FORMS': '3',
'meetinghosts-INITIAL_FORMS': '1',
'meetinghosts-MIN_NUM_FORMS': '0',
'meetinghosts-MAX_NUM_FORMS': '1000',
'meetinghosts-0-id': str(host.pk),
'meetinghosts-0-meeting': str(meeting.pk),
'meetinghosts-0-name': 'Modified Sponsor, Ltd.',
'meetinghosts-0-DELETE': 'on',
'meetinghosts-1-id':'',
'meetinghosts-1-meeting': str(meeting.pk),
'meetinghosts-1-name': '',
'meetinghosts-2-id':'',
'meetinghosts-2-meeting': str(meeting.pk),
'meetinghosts-2-name': '',
},
)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number)))
self.assertEqual(meeting.meetinghosts.count(), 0)
self.assertFalse(logopath.exists())
def test_remove_with_selected_logo(self):
"""Can delete a meeting host after selecting a replacement file"""
meeting = MeetingFactory(type_id='ietf')
url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number))
# create via UI so we don't have to deal with creating storage paths
self.client.login(username='secretary', password='secretary+password')
logo = logo_file()
r = self._create_first_host(meeting, logo, url)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number)))
self.assertEqual(meeting.meetinghosts.count(), 1)
host = meeting.meetinghosts.first()
self.assertEqual(host.name, 'Some Sponsor, Inc.')
logopath = Path(host.logo.path)
self._assertMatch(logopath.name, r'logo-[a-z]+.png')
self.assertTrue(logopath.exists())
# now delete
r = self.client.post(
url,
{
'meetinghosts-TOTAL_FORMS': '3',
'meetinghosts-INITIAL_FORMS': '1',
'meetinghosts-MIN_NUM_FORMS': '0',
'meetinghosts-MAX_NUM_FORMS': '1000',
'meetinghosts-0-id': str(host.pk),
'meetinghosts-0-meeting': str(meeting.pk),
'meetinghosts-0-name': 'Modified Sponsor, Ltd.',
'meetinghosts-0-DELETE': 'on',
'meetinghosts-0-logo': logo_file(format='JPEG'),
'meetinghosts-1-id':'',
'meetinghosts-1-meeting': str(meeting.pk),
'meetinghosts-1-name': '',
'meetinghosts-2-id':'',
'meetinghosts-2-meeting': str(meeting.pk),
'meetinghosts-2-name': '',
},
)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number)))
self.assertEqual(meeting.meetinghosts.count(), 0)
self.assertFalse(logopath.exists())
def test_logo_types_checked(self):
"""Only allowed image types should be accepted"""
allowed_formats = [('JPEG', 'jpg'), ('JPEG', 'jpeg'), ('PNG', 'png')]
meeting = MeetingFactory(type_id='ietf')
url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number))
self.client.login(username='secretary', password='secretary+password')
junk = BytesIO()
junk.write(b'this is not an image')
junk.seek(0)
r = self._create_first_host(meeting, junk, url)
self.assertContains(r, 'Upload a valid image', status_code=200)
self.assertEqual(meeting.meetinghosts.count(), 0)
for fmt, ext in allowed_formats:
r = self._create_first_host(
meeting,
logo_file(format=fmt, ext=ext),
url
)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number)))
self.assertEqual(meeting.meetinghosts.count(), 1)
meeting.meetinghosts.all().delete()
# Keep these settings consistent with the assumptions in these tests
@override_settings(PROCEEDINGS_VERSION_CHANGES=[0, 97, 111])
class ProceedingsTests(BaseMeetingTestCase):
"""Tests related to meeting proceedings display
Fills in all material types
"""
def _create_proceedings_materials(self, meeting):
"""Create various types of proceedings materials for meeting"""
MeetingHostFactory.create_batch(2, meeting=meeting) # create a couple of meeting hosts/logos
ProceedingsMaterialFactory(
# default title, not removed
meeting=meeting,
type=ProceedingsMaterialTypeName.objects.get(slug='supporters')
)
ProceedingsMaterialFactory(
# custom title, not removed
meeting=meeting,
type=ProceedingsMaterialTypeName.objects.get(slug='host_speaker_series'),
document__title='Speakers'
)
ProceedingsMaterialFactory(
# default title, removed
meeting=meeting,
type=ProceedingsMaterialTypeName.objects.get(slug='social_event'),
document__states=[('procmaterials', 'removed')]
)
ProceedingsMaterialFactory(
# custom title, removed
meeting=meeting,
type=ProceedingsMaterialTypeName.objects.get(slug='additional_information'),
document__title='Party', document__states=[('procmaterials', 'removed')]
)
ProceedingsMaterialFactory(
# url
meeting=meeting,
type=ProceedingsMaterialTypeName.objects.get(slug='wiki'),
document__external_url='https://example.com/wiki'
)
@staticmethod
def _proceedings_file():
"""Get a file containing content suitable for a proceedings document
Currently returns the same file every time.
"""
path = Path(settings.BASE_DIR) / 'meeting/test_procmat.pdf'
return path.open('rb')
def _assertMeetingHostsDisplayed(self, response, meeting):
pq = PyQuery(response.content)
host_divs = pq('div.host-logo')
self.assertEqual(len(host_divs), meeting.meetinghosts.count(), 'Should have a logo for every meeting host')
self.assertEqual(
[(img.attr('title'), img.attr('src')) for img in host_divs.items('img')],
[
(host.name,
urlreverse(
'ietf.meeting.views_proceedings.meetinghost_logo',
kwargs=dict(num=meeting.number, host_id=host.pk),
))
for host in meeting.meetinghosts.all()
],
'Correct image and name for each host should appear in the correct order'
)
def _assertProceedingsMaterialsDisplayed(self, response, meeting):
"""Checks that all (and only) active materials are linked with correct href and title"""
expected_materials = [
m for m in meeting.proceedings_materials.order_by('type__order') if m.active()
]
pq = PyQuery(response.content)
links = pq('div.proceedings-material a')
self.assertEqual(len(links), len(expected_materials), 'Should have an entry for each active ProceedingsMaterial')
self.assertEqual(
[(link.eq(0).text(), link.eq(0).attr('href')) for link in links.items()],
[(str(pm), pm.get_href()) for pm in expected_materials],
'Correct title and link for each ProceedingsMaterial should appear in the correct order'
)
def _assertGroupSessions(self, response, meeting):
"""Checks that group/sessions are present"""
pq = PyQuery(response.content)
sections = ["plenaries", "gen", "iab", "editorial", "irtf", "training"]
for section in sections:
self.assertEqual(len(pq(f"#{section}")), 1, f"{section} section should exists in proceedings")
def test_proceedings(self):
"""Proceedings should be displayed correctly
Currently only tests that the view responds with a 200 response code and checks the ProceedingsMaterials
at the top of the proceedings. Ought to actually test the display of the individual group/session
materials as well.
"""
meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100'))
session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
GroupEventFactory(group=session.group,type='status_update')
SessionPresentationFactory(document__type_id='recording',session=session)
SessionPresentationFactory(document__type_id='recording',session=session,document__title="Audio recording for tests")
# Add various group sessions
groups = []
parent_groups = [
GroupFactory.create(type_id="area", acronym="gen"),
GroupFactory.create(acronym="iab"),
GroupFactory.create(acronym="irtf"),
]
for parent in parent_groups:
groups.append(GroupFactory.create(parent=parent))
for acronym in ["rsab", "edu"]:
groups.append(GroupFactory.create(acronym=acronym))
for group in groups:
SessionFactory(meeting=meeting, group=group)
self.write_materials_files(meeting, session)
self._create_proceedings_materials(meeting)
url = urlreverse("ietf.meeting.views.proceedings", kwargs=dict(num=meeting.number))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
if len(meeting.city) > 0:
self.assertContains(r, meeting.city)
if len(meeting.venue_name) > 0:
self.assertContains(r, meeting.venue_name)
# standard items on every proceedings
pq = PyQuery(r.content)
self.assertNotEqual(
pq('a[href="{}"]'.format(
urlreverse('ietf.meeting.views.proceedings_overview', kwargs=dict(num=meeting.number)))
),
[],
'Should have a link to IETF overview',
)
self.assertNotEqual(
pq('a[href="{}"]'.format(
urlreverse('ietf.meeting.views.proceedings_attendees', kwargs=dict(num=meeting.number)))
),
[],
'Should have a link to attendees',
)
self.assertNotEqual(
pq('a[href="{}"]'.format(
urlreverse('ietf.meeting.views.proceedings_activity_report', kwargs=dict(num=meeting.number)))
),
[],
'Should have a link to activity report',
)
self.assertNotEqual(
pq('a[href="{}"]'.format(
urlreverse('ietf.meeting.views.important_dates', kwargs=dict(num=meeting.number)))
),
[],
'Should have a link to important dates',
)
# configurable contents
self._assertMeetingHostsDisplayed(r, meeting)
self._assertProceedingsMaterialsDisplayed(r, meeting)
self._assertGroupSessions(r, meeting)
def test_named_session(self):
"""Session with a name should appear separately in the proceedings"""
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')
for doc_type_id in ('agenda', 'minutes', 'bluesheets', 'recording', '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(
session=plain_session,
document__type_id=doc_type_id,
document__uploaded_filename=f'upload-{doc_type_id}-plain',
document__external_url=f'external_url-{doc_type_id}-plain',
)
SessionPresentationFactory(
session=named_session,
document__type_id=doc_type_id,
document__uploaded_filename=f'upload-{doc_type_id}-named',
document__external_url=f'external_url-{doc_type_id}-named',
)
url = urlreverse('ietf.meeting.views.proceedings', kwargs={'num': meeting.number})
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
plain_label = q(f'div#{group.acronym}')
self.assertEqual(plain_label.text(), group.acronym)
plain_row = plain_label.closest('tr')
self.assertTrue(plain_row)
named_label = q(f'div#{slugify(named_session.name)}')
self.assertEqual(named_label.text(), named_session.name)
named_row = named_label.closest('tr')
self.assertTrue(named_row)
for material in (sp.document for sp in plain_session.presentations.all()):
if material.type_id == 'draft':
expected_url = urlreverse(
'ietf.doc.views_doc.document_main',
kwargs={'name': material.name},
)
else:
expected_url = material.get_href(meeting)
self.assertTrue(plain_row.find(f'a[href="{expected_url}"]'))
self.assertFalse(named_row.find(f'a[href="{expected_url}"]'))
for material in (sp.document for sp in named_session.presentations.all()):
if material.type_id == 'draft':
expected_url = urlreverse(
'ietf.doc.views_doc.document_main',
kwargs={'name': material.name},
)
else:
expected_url = material.get_href(meeting)
self.assertFalse(plain_row.find(f'a[href="{expected_url}"]'))
self.assertTrue(named_row.find(f'a[href="{expected_url}"]'))
def test_proceedings_no_agenda(self):
# Meeting number must be larger than the last special-cased proceedings (currently 96)
meeting = MeetingFactory(type_id='ietf',populate_schedule=False,date=date_today(), number='100')
url = urlreverse('ietf.meeting.views.proceedings')
r = self.client.get(url)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials'))
url = urlreverse('ietf.meeting.views.proceedings', kwargs=dict(num=meeting.number))
r = self.client.get(url)
self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number)))
def test_proceedings_acknowledgements(self):
make_meeting_test_data()
meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97")
meeting.acknowledgements = 'test acknowledgements'
meeting.save()
url = urlreverse('ietf.meeting.views.proceedings_acknowledgements',kwargs={'num':meeting.number})
response = self.client.get(url)
self.assertContains(response, 'test acknowledgements')
def test_proceedings_acknowledgements_link(self):
"""Link to proceedings_acknowledgements view should not appear for 'new' meetings
With the PROCEEDINGS_VERSION_CHANGES settings value used here, expect the proceedings_acknowledgements
view to be linked for meetings 95-110.
"""
meeting_with_acks = MeetingFactory(type_id='ietf', date=datetime.date(2020,7,25), number='108')
SessionFactory(meeting=meeting_with_acks) # make sure meeting has a scheduled session
meeting_with_acks.acknowledgements = 'these acknowledgements should appear'
meeting_with_acks.save()
url = urlreverse('ietf.meeting.views.proceedings',kwargs={'num':meeting_with_acks.number})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
q = PyQuery(response.content)
self.assertEqual(
len(q('a[href="{}"]'.format(
urlreverse('ietf.meeting.views.proceedings_acknowledgements',
kwargs={'num':meeting_with_acks.number})
))),
1,
)
meeting_without_acks = MeetingFactory(type_id='ietf', date=datetime.date(2022,7,25), number='113')
SessionFactory(meeting=meeting_without_acks) # make sure meeting has a scheduled session
meeting_without_acks.acknowledgements = 'these acknowledgements should not appear'
meeting_without_acks.save()
url = urlreverse('ietf.meeting.views.proceedings',kwargs={'num':meeting_without_acks.number})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
q = PyQuery(response.content)
self.assertEqual(
len(q('a[href="{}"]'.format(
urlreverse('ietf.meeting.views.proceedings_acknowledgements',
kwargs={'num':meeting_without_acks.number})
))),
0,
)
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
"""
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')
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': 118})
response = self.client.get(url)
self.assertContains(response, 'Attendee list')
q = PyQuery(response.content)
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")
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.
'''
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')
def test_proceedings_activity_report(self):
make_meeting_test_data()
MeetingFactory(type_id='ietf', date=datetime.date(2016,4,3), number="96")
MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97")
url = urlreverse('ietf.meeting.views.proceedings_activity_report',kwargs={'num':97})
response = self.client.get(url)
self.assertContains(response, 'Activity Report')
def test_feed(self):
meeting = make_meeting_test_data()
session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
r = self.client.get("/feed/wg-proceedings/")
self.assertContains(r, "agenda")
self.assertContains(r, session.group.acronym)
def _procmat_test_meeting(self):
"""Generate a meeting for proceedings material test"""
# meeting number 123 avoids various legacy cases that affect these tests
# (as of Aug 2021, anything above 96 is probably ok)
return MeetingFactory(type_id='ietf', number='123', date=date_today())
def _secretary_only_permission_test(self, url, include_post=True):
self.client.logout()
login_testing_unauthorized(self, 'ad', url)
login_testing_unauthorized(self, 'secretary', url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
if include_post:
self.client.logout()
login_testing_unauthorized(self, 'ad', url, method='post')
login_testing_unauthorized(self, 'secretary', url, method='post')
# don't bother checking a real post - it'll be tested in other methods
def test_material_management_permissions(self):
"""Only the secreatariat should be able to manage proceedings materials"""
meeting = self._procmat_test_meeting()
# test all materials types in case they wind up treated differently
# (unlikely, but more likely than an unwieldy number of types are introduced)
for mat_type in ProceedingsMaterialTypeName.objects.filter(used=True):
self._secretary_only_permission_test(
urlreverse(
'ietf.meeting.views_proceedings.material_details',
kwargs=dict(num=meeting.number),
))
self._secretary_only_permission_test(
urlreverse(
'ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=meeting.number, material_type=mat_type.slug),
))
# remaining tests need material to exist, so create
ProceedingsMaterialFactory(meeting=meeting, type=mat_type)
self._secretary_only_permission_test(
urlreverse(
'ietf.meeting.views_proceedings.edit_material',
kwargs=dict(num=meeting.number, material_type=mat_type.slug),
))
self._secretary_only_permission_test(
urlreverse(
'ietf.meeting.views_proceedings.remove_material',
kwargs=dict(num=meeting.number, material_type=mat_type.slug),
))
# it's ok to use active materials for restore test - no restore is actually issued
self._secretary_only_permission_test(
urlreverse(
'ietf.meeting.views_proceedings.restore_material',
kwargs=dict(num=meeting.number, material_type=mat_type.slug),
))
def test_proceedings_material_details(self):
"""Material details page should correctly show materials"""
meeting = self._procmat_test_meeting()
url = urlreverse('ietf.meeting.views_proceedings.material_details', kwargs=dict(num=meeting.number))
self.client.login(username='secretary', password='secretary+password')
procmat_types = ProceedingsMaterialTypeName.objects.filter(used=True)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
pq = PyQuery(r.content)
body_rows = pq('tbody > tr')
self.assertEqual(len(body_rows), procmat_types.count())
for row, mat_type in zip(body_rows.items(), procmat_types.all()):
cells = row.find('td')
# no materials, so rows should be empty except for label and 'Add' button
self.assertEqual(len(cells), 3) # label, blank, buttons
self.assertEqual(cells.eq(0).text(), str(mat_type), 'First column should be material type name')
self.assertEqual(cells.eq(1).text(), '', 'Second column should be empty')
add_url = urlreverse('ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=meeting.number, material_type=mat_type.slug))
self.assertEqual(len(cells.eq(2).find(f'a[href="{add_url}"]')), 1, 'Third column should have Add link')
self._create_proceedings_materials(meeting)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
pq = PyQuery(r.content)
body_rows = pq('tbody > tr')
self.assertEqual(len(body_rows), procmat_types.count())
# n.b., this loop is over materials, not the type names!
for row, mat in zip(body_rows.items(), meeting.proceedings_materials.order_by('type__order')):
add_url = urlreverse('ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=meeting.number, material_type=mat.type.slug))
edit_url = urlreverse('ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=meeting.number, material_type=mat.type.slug))
remove_url = urlreverse('ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=meeting.number, material_type=mat.type.slug))
restore_url = urlreverse('ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=meeting.number, material_type=mat.type.slug))
cells = row.find('td')
# no materials, so rows should be empty except for label and 'Add' button
self.assertEqual(cells.eq(0).text(), str(mat.type), 'First column should be material type name')
if mat.active():
self.assertEqual(len(cells), 5) # label, title, doc, updated, buttons
self.assertEqual(cells.eq(1).text(), str(mat), 'Second column should be active material title')
self.assertEqual(
cells.eq(2).text(),
'{} ({})'.format(
str(mat.document),
'external URL' if mat.document.external_url else 'uploaded file',
))
mod_time = mat.document.time.astimezone(pytz.utc)
c3text = cells.eq(3).text()
self.assertIn(mod_time.strftime('%Y-%m-%d'), c3text, 'Updated date incorrect')
self.assertIn(mod_time.strftime('%H:%M:%S'), c3text, 'Updated time incorrect')
self.assertEqual(len(cells.eq(4).find(f'a[href="{add_url}"]')), 1,
'Fourth column should have a Replace link')
self.assertEqual(len(cells.eq(4).find(f'a[href="{edit_url}"]')), 1,
'Fourth column should have an Edit link')
self.assertEqual(len(cells.eq(4).find(f'a[href="{remove_url}"]')), 1,
'Fourth column should have a Remove link')
else:
self.assertEqual(len(cells), 3) # label, blank, buttons
self.assertEqual(cells.eq(0).text(), str(mat.type), 'First column should be material type name')
self.assertEqual(cells.eq(1).text(), '', 'Second column should be empty')
add_url = urlreverse('ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=meeting.number, material_type=mat.type.slug))
self.assertEqual(len(cells.eq(2).find(f'a[href="{add_url}"]')), 1,
'Third column should have Add link')
self.assertEqual(len(cells.eq(2).find(f'a[href="{restore_url}"]')), 1,
'Third column should have Restore link')
def upload_proceedings_material_test(self, meeting, mat_type, post_data):
"""Test the upload_proceedings view using provided POST data"""
url = urlreverse(
'ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=meeting.number, material_type=mat_type.slug),
)
self.client.login(username='secretary', password='secretary+password')
mats_before = [m.pk for m in meeting.proceedings_materials.all()]
r = self.client.post(url, post_data)
self.assertRedirects(
r,
urlreverse('ietf.meeting.views_proceedings.material_details',
kwargs=dict(num=meeting.number)),
)
self.assertEqual(meeting.proceedings_materials.count(), len(mats_before) + 1)
mat = meeting.proceedings_materials.exclude(pk__in=mats_before).first()
self.assertEqual(mat.type, mat_type)
self.assertEqual(str(mat), mat_type.name)
self.assertEqual(mat.document.rev, '00')
return mat
# use a simple and predictable href format for this test
@override_settings(MEETING_DOC_HREFS={'procmaterials': '{doc.name}:{doc.rev}'})
def test_add_proceedings_material_doc(self):
"""Upload proceedings materials document"""
meeting = self._procmat_test_meeting()
for mat_type in ProceedingsMaterialTypeName.objects.filter(used=True):
with self._proceedings_file() as fd:
mat = self.upload_proceedings_material_test(
meeting,
mat_type,
{'file': fd, 'external_url': ''},
)
self.assertEqual(mat.get_href(), f'{mat.document.name}:00')
def test_add_proceedings_material_doc_invalid_ext(self):
"""Upload proceedings materials document with disallowed extension"""
meeting = self._procmat_test_meeting()
self.client.login(username='secretary', password='secretary+password')
with NamedTemporaryFile('w+', suffix='.png') as invalid_file:
invalid_file.write('this is not a PDF file!!')
for mat_type in ProceedingsMaterialTypeName.objects.filter(used=True):
url = urlreverse(
'ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=meeting.number, material_type=mat_type.slug),
)
invalid_file.seek(0) # read the file contents again
r = self.client.post(url, {'file': invalid_file, 'external_url': ''})
self.assertEqual(r.status_code, 200)
self.assertFormError(r.context["form"], 'file', 'Found an unexpected extension: .png. Expected one of .pdf')
def test_add_proceedings_material_doc_empty(self):
"""Upload proceedings materials document without specifying a file"""
meeting = self._procmat_test_meeting()
self.client.login(username='secretary', password='secretary+password')
for mat_type in ProceedingsMaterialTypeName.objects.filter(used=True):
url = urlreverse(
'ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=meeting.number, material_type=mat_type.slug),
)
r = self.client.post(url, {'external_url': ''})
self.assertEqual(r.status_code, 200)
self.assertFormError(r.context["form"], 'file', 'This field is required')
def test_add_proceedings_material_url(self):
"""Add a URL as proceedings material"""
meeting = self._procmat_test_meeting()
for mat_type in ProceedingsMaterialTypeName.objects.filter(used=True):
mat = self.upload_proceedings_material_test(
meeting,
mat_type,
{'use_url': 'on', 'external_url': 'https://example.com'},
)
self.assertEqual(mat.get_href(), 'https://example.com')
def test_add_proceedings_material_url_invalid(self):
"""Add proceedings materials URL with a non-URL value"""
meeting = self._procmat_test_meeting()
self.client.login(username='secretary', password='secretary+password')
for mat_type in ProceedingsMaterialTypeName.objects.filter(used=True):
url = urlreverse(
'ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=meeting.number, material_type=mat_type.slug),
)
r = self.client.post(url, {'use_url': 'on', 'external_url': "Ceci n'est pas une URL"})
self.assertEqual(r.status_code, 200)
self.assertFormError(r.context["form"], 'external_url', 'Enter a valid URL.')
def test_add_proceedings_material_url_empty(self):
"""Add proceedings materials URL without specifying the URL"""
meeting = self._procmat_test_meeting()
self.client.login(username='secretary', password='secretary+password')
for mat_type in ProceedingsMaterialTypeName.objects.filter(used=True):
url = urlreverse(
'ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=meeting.number, material_type=mat_type.slug),
)
r = self.client.post(url, {'use_url': 'on', 'external_url': ''})
self.assertEqual(r.status_code, 200)
self.assertFormError(r.context["form"], 'external_url', 'This field is required')
@override_settings(MEETING_DOC_HREFS={'procmaterials': '{doc.name}:{doc.rev}'})
def test_replace_proceedings_material(self):
"""Replace uploaded document with new uploaded document"""
# Set up a meeting with a proceedings material in place
meeting = self._procmat_test_meeting()
pm_doc = ProceedingsMaterialFactory(meeting=meeting)
with self._proceedings_file() as f:
self.write_materials_file(meeting, pm_doc.document, f.read())
pm_url = ProceedingsMaterialFactory(meeting=meeting, document__external_url='https://example.com/first')
success_url = urlreverse('ietf.meeting.views_proceedings.material_details', kwargs=dict(num=meeting.number))
self.assertNotEqual(pm_doc.type, pm_url.type)
self.assertEqual(meeting.proceedings_materials.count(), 2)
# Replace the uploaded document with another uploaded document
pm_doc_url = urlreverse(
'ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=meeting.number, material_type=pm_doc.type.slug),
)
self.client.login(username='secretary', password='secretary+password')
with self._proceedings_file() as fd:
r = self.client.post(pm_doc_url, {'file': fd, 'external_url': ''})
self.assertRedirects(r, success_url)
self.assertEqual(meeting.proceedings_materials.count(), 2)
pm_doc = meeting.proceedings_materials.get(pk=pm_doc.pk) # refresh from DB
self.assertEqual(pm_doc.document.rev, '01')
self.assertEqual(pm_doc.get_href(), f'{pm_doc.document.name}:01')
# Replace the uploaded document with a URL
r = self.client.post(pm_doc_url, {'use_url': 'on', 'external_url': 'https://example.com/second'})
self.assertRedirects(r, success_url)
self.assertEqual(meeting.proceedings_materials.count(), 2)
pm_doc = meeting.proceedings_materials.get(pk=pm_doc.pk) # refresh from DB
self.assertEqual(pm_doc.document.rev, '02')
self.assertEqual(pm_doc.get_href(), 'https://example.com/second')
# Now replace the URL doc with another URL
pm_url_url = urlreverse(
'ietf.meeting.views_proceedings.upload_material',
kwargs=dict(num=meeting.number, material_type=pm_url.type.slug),
)
r = self.client.post(pm_url_url, {'use_url': 'on', 'external_url': 'https://example.com/third'})
self.assertRedirects(r, success_url)
self.assertEqual(meeting.proceedings_materials.count(), 2)
pm_url = meeting.proceedings_materials.get(pk=pm_url.pk) # refresh from DB
self.assertEqual(pm_url.document.rev, '01')
self.assertEqual(pm_url.get_href(), 'https://example.com/third')
# Now replace the URL doc with an uploaded file
with self._proceedings_file() as fd:
r = self.client.post(pm_url_url, {'file': fd, 'external_url': ''})
self.assertRedirects(r, success_url)
self.assertEqual(meeting.proceedings_materials.count(), 2)
pm_url = meeting.proceedings_materials.get(pk=pm_url.pk) # refresh from DB
self.assertEqual(pm_url.document.rev, '02')
self.assertEqual(pm_url.get_href(), f'{pm_url.document.name}:02')
def test_remove_proceedings_material(self):
"""Proceedings material can be removed"""
meeting = self._procmat_test_meeting()
pm = ProceedingsMaterialFactory(meeting=meeting)
self.assertEqual(pm.active(), True)
url = urlreverse(
'ietf.meeting.views_proceedings.remove_material',
kwargs=dict(num=meeting.number, material_type=pm.type.slug),
)
self.client.login(username='secretary', password='secretary+password')
r = self.client.post(url)
self.assertRedirects(
r,
urlreverse('ietf.meeting.views_proceedings.material_details',
kwargs=dict(num=meeting.number)),
)
pm = meeting.proceedings_materials.get(pk=pm.pk)
self.assertEqual(pm.active(), False)
def test_restore_proceedings_material(self):
"""Proceedings material can be removed"""
meeting = self._procmat_test_meeting()
pm = ProceedingsMaterialFactory(meeting=meeting, document__states=[('procmaterials', 'removed')])
self.assertEqual(pm.active(), False)
url = urlreverse(
'ietf.meeting.views_proceedings.restore_material',
kwargs=dict(num=meeting.number, material_type=pm.type.slug),
)
self.client.login(username='secretary', password='secretary+password')
r = self.client.post(url)
self.assertRedirects(
r,
urlreverse('ietf.meeting.views_proceedings.material_details',
kwargs=dict(num=meeting.number)),
)
pm = meeting.proceedings_materials.get(pk=pm.pk)
self.assertEqual(pm.active(), True)
def test_rename_proceedings_material(self):
"""Proceedings material can be renamed"""
meeting = self._procmat_test_meeting()
pm = ProceedingsMaterialFactory(meeting=meeting)
self.assertEqual(str(pm), pm.type.name)
orig_rev = pm.document.rev
url = urlreverse(
'ietf.meeting.views_proceedings.edit_material',
kwargs=dict(num=meeting.number, material_type=pm.type.slug),
)
self.client.login(username='secretary', password='secretary+password')
r = self.client.post(url, {'title': 'This Is Not the Default Name'})
self.assertRedirects(
r,
urlreverse('ietf.meeting.views_proceedings.material_details',
kwargs=dict(num=meeting.number)),
)
pm = meeting.proceedings_materials.get(pk=pm.pk)
self.assertEqual(str(pm), 'This Is Not the Default Name')
self.assertEqual(pm.document.rev, orig_rev, 'Renaming should not change document revision')
def test_create_recording(self):
session = SessionFactory(meeting__type_id='ietf', meeting__number=72, group__acronym='mars')
filename = 'ietf42-testroomt-20000101-0800.mp3'
url = settings.IETF_AUDIO_URL + 'ietf{}/{}'.format(session.meeting.number, filename)
doc = create_recording(session, url)
self.assertEqual(doc.name,'recording-72-mars-1')
self.assertEqual(doc.group,session.group)
self.assertEqual(doc.external_url,url)
self.assertTrue(doc in session.materials.all())
def test_get_next_sequence(self):
session = SessionFactory(meeting__type_id='ietf', meeting__number=72, group__acronym='mars')
meeting = session.meeting
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)
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 = PersonalApiKeyFactory(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.plain_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].plain_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].plain_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.presentations.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.plain_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.presentations.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())
def test_bluesheet_data(self):
session = SessionFactory(meeting__type_id="ietf")
attended_with_affil = MeetingRegistrationFactory(meeting=session.meeting, affiliation="Somewhere")
AttendedFactory(session=session, person=attended_with_affil.person, time="2023-03-13T01:24:00Z") # joined 2nd
attended_no_affil = MeetingRegistrationFactory(meeting=session.meeting)
AttendedFactory(session=session, person=attended_no_affil.person, time="2023-03-13T01:23:00Z") # joined 1st
MeetingRegistrationFactory(meeting=session.meeting) # did not attend
data = bluesheet_data(session)
self.assertEqual(
data,
[
{"name": attended_no_affil.person.plain_name(), "affiliation": ""},
{"name": attended_with_affil.person.plain_name(), "affiliation": "Somewhere"},
]
)