assignments on the base schedule are shown in the pages for the schedule, read-only. This allows managing things like breaks and misc sessions separately from the regular WG sessions. Base schedules are not allowed to be the base of other base schedules (the hierarchy can only be one level deep) to simplify the mental model and the code. Add link for creating new schedules instead of relying on copying Empty-Schedule and change the meeting creation code to no longer create the special Empty-Schedule. Instead a "base" schedule is created and a first schedule with the name and permissions of the user creating the meeting, using "base" as base. Speed up a couple of the Secretariat/AD agenda views by adding prefetches. - Legacy-Id: 18355
382 lines
19 KiB
Python
382 lines
19 KiB
Python
# Copyright The IETF Trust 2014-2020, All Rights Reserved
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
import sys
|
|
import time
|
|
import datetime
|
|
from unittest import skipIf
|
|
|
|
import django
|
|
from django.urls import reverse as urlreverse
|
|
#from django.test.utils import override_settings
|
|
|
|
import debug # pyflakes:ignore
|
|
|
|
from ietf.doc.factories import DocumentFactory
|
|
from ietf.group import colors
|
|
from ietf.person.models import Person
|
|
from ietf.meeting.factories import SessionFactory
|
|
from ietf.meeting.test_data import make_meeting_test_data
|
|
from ietf.meeting.models import Schedule, SchedTimeSessAssignment, Session, Room, TimeSlot, Constraint, ConstraintName
|
|
from ietf.meeting.models import SchedulingEvent, SessionStatusName
|
|
from ietf.utils.test_runner import IetfLiveServerTestCase
|
|
from ietf.utils.pipe import pipe
|
|
from ietf import settings
|
|
|
|
skip_selenium = False
|
|
skip_message = ""
|
|
try:
|
|
from selenium import webdriver
|
|
from selenium.webdriver.common.action_chains import ActionChains
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions
|
|
except ImportError as e:
|
|
skip_selenium = True
|
|
skip_message = "Skipping selenium tests: %s" % e
|
|
|
|
executable_name = 'chromedriver'
|
|
code, out, err = pipe('{} --version'.format(executable_name))
|
|
if code != 0:
|
|
skip_selenium = True
|
|
skip_message = "Skipping selenium tests: '{}' executable not found.".format(executable_name)
|
|
if skip_selenium:
|
|
sys.stderr.write(" "+skip_message+'\n')
|
|
|
|
def start_web_driver():
|
|
options = webdriver.ChromeOptions()
|
|
options.add_argument("headless")
|
|
options.add_argument("disable-extensions")
|
|
options.add_argument("disable-gpu") # headless needs this
|
|
options.add_argument("no-sandbox") # docker needs this
|
|
return webdriver.Chrome(options=options, service_log_path=settings.TEST_GHOSTDRIVER_LOG_PATH)
|
|
|
|
@skipIf(skip_selenium, skip_message)
|
|
class EditMeetingScheduleTests(IetfLiveServerTestCase):
|
|
def setUp(self):
|
|
self.driver = start_web_driver()
|
|
self.driver.set_window_size(1024,768)
|
|
|
|
def tearDown(self):
|
|
self.driver.close()
|
|
|
|
def debug_snapshot(self,filename='debug_this.png'):
|
|
self.driver.execute_script("document.body.bgColor = 'white';")
|
|
self.driver.save_screenshot(filename)
|
|
|
|
def absreverse(self,*args,**kwargs):
|
|
return '%s%s'%(self.live_server_url,urlreverse(*args,**kwargs))
|
|
|
|
def login(self):
|
|
url = self.absreverse('ietf.ietfauth.views.login')
|
|
self.driver.get(url)
|
|
self.driver.find_element_by_name('username').send_keys('plain')
|
|
self.driver.find_element_by_name('password').send_keys('plain+password')
|
|
self.driver.find_element_by_xpath('//button[@type="submit"]').click()
|
|
|
|
def test_edit_meeting_schedule(self):
|
|
meeting = make_meeting_test_data()
|
|
|
|
schedule = Schedule.objects.filter(meeting=meeting, owner__user__username="plain").first()
|
|
|
|
room1 = Room.objects.get(name="Test Room")
|
|
slot1 = TimeSlot.objects.filter(meeting=meeting, location=room1).order_by('time').first()
|
|
|
|
room2 = Room.objects.create(meeting=meeting, name="Test Room2", capacity=1)
|
|
room2.session_types.add('regular')
|
|
slot2 = TimeSlot.objects.create(
|
|
meeting=meeting,
|
|
type_id='regular',
|
|
location=room2,
|
|
duration=datetime.timedelta(hours=2),
|
|
time=slot1.time - datetime.timedelta(minutes=10),
|
|
)
|
|
|
|
slot3 = TimeSlot.objects.create(
|
|
meeting=meeting,
|
|
type_id='regular',
|
|
location=room2,
|
|
duration=datetime.timedelta(hours=2),
|
|
time=max(slot1.end_time(), slot2.end_time()) + datetime.timedelta(minutes=10),
|
|
)
|
|
|
|
slot4 = TimeSlot.objects.create(
|
|
meeting=meeting,
|
|
type_id='regular',
|
|
location=room1,
|
|
duration=datetime.timedelta(hours=2),
|
|
time=slot1.time + datetime.timedelta(days=1),
|
|
)
|
|
|
|
s1, s2 = Session.objects.filter(meeting=meeting, type='regular')
|
|
s2.requested_duration = slot2.duration + datetime.timedelta(minutes=10)
|
|
s2.save()
|
|
SchedTimeSessAssignment.objects.filter(session=s1).delete()
|
|
|
|
s2b = Session.objects.create(meeting=meeting, group=s2.group, attendees=10, requested_duration=datetime.timedelta(minutes=60), type_id='regular')
|
|
|
|
SchedulingEvent.objects.create(
|
|
session=s2b,
|
|
status=SessionStatusName.objects.get(slug='appr'),
|
|
by=Person.objects.get(name='(System)'),
|
|
)
|
|
|
|
Constraint.objects.create(
|
|
meeting=meeting,
|
|
source=s1.group,
|
|
target=s2.group,
|
|
name=ConstraintName.objects.get(slug="conflict"),
|
|
)
|
|
|
|
self.login()
|
|
url = self.absreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number, name=schedule.name, owner=schedule.owner_email()))
|
|
self.driver.get(url)
|
|
|
|
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.edit-meeting-schedule')))
|
|
|
|
self.assertEqual(len(self.driver.find_elements_by_css_selector('.session')), 3)
|
|
|
|
# select - show session info
|
|
s2_element = self.driver.find_element_by_css_selector('#session{}'.format(s2.pk))
|
|
s2_element.click()
|
|
|
|
session_info_container = self.driver.find_element_by_css_selector('.session-info-container')
|
|
self.assertIn(s2.group.acronym, session_info_container.find_element_by_css_selector(".title").text)
|
|
self.assertEqual(session_info_container.find_element_by_css_selector(".other-session .time").text, "not yet scheduled")
|
|
|
|
# deselect
|
|
self.driver.find_element_by_css_selector('.scheduling-panel').click()
|
|
|
|
self.assertEqual(session_info_container.find_elements_by_css_selector(".title"), [])
|
|
|
|
# unschedule
|
|
|
|
# we would like to do
|
|
#
|
|
# unassigned_sessions_element = self.driver.find_element_by_css_selector('.unassigned-sessions')
|
|
# ActionChains(self.driver).drag_and_drop(s2_element, unassigned_sessions_element).perform()
|
|
#
|
|
# but unfortunately, Selenium does not simulate drag and drop events, see
|
|
#
|
|
# https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/3604
|
|
#
|
|
# so for the time being we inject the Javascript workaround here and do it from JS
|
|
#
|
|
# https://storage.googleapis.com/google-code-attachments/selenium/issue-3604/comment-9/drag_and_drop_helper.js
|
|
|
|
self.driver.execute_script('!function(s){s.fn.simulateDragDrop=function(t){return this.each(function(){new s.simulateDragDrop(this,t)})},s.simulateDragDrop=function(t,a){this.options=a,this.simulateEvent(t,a)},s.extend(s.simulateDragDrop.prototype,{simulateEvent:function(t,a){var e="dragstart",n=this.createEvent(e);this.dispatchEvent(t,e,n),e="drop";var r=this.createEvent(e,{});r.dataTransfer=n.dataTransfer,this.dispatchEvent(s(a.dropTarget)[0],e,r),e="dragend";var i=this.createEvent(e,{});i.dataTransfer=n.dataTransfer,this.dispatchEvent(t,e,i)},createEvent:function(t){var a=document.createEvent("CustomEvent");return a.initCustomEvent(t,!0,!0,null),a.dataTransfer={data:{},setData:function(t,a){this.data[t]=a},getData:function(t){return this.data[t]}},a},dispatchEvent:function(t,a,e){t.dispatchEvent?t.dispatchEvent(e):t.fireEvent&&t.fireEvent("on"+a,e)}})}(jQuery);')
|
|
|
|
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '.unassigned-sessions .drop-target'}});".format(s2.pk))
|
|
|
|
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.unassigned-sessions #session{}'.format(s2.pk))))
|
|
|
|
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(session=s2, schedule=schedule)), [])
|
|
|
|
# sorting unassigned
|
|
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.group.acronym, s.requested_duration, s.pk))]
|
|
self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=name]').click()
|
|
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{} + #session{}'.format(*sorted_pks)))
|
|
|
|
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.group.parent.acronym, s.group.acronym, s.requested_duration, s.pk))]
|
|
self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=parent]').click()
|
|
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
|
|
|
|
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.requested_duration, s.group.parent.acronym, s.group.acronym, s.pk))]
|
|
self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=duration]').click()
|
|
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
|
|
|
|
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (int(bool(s.comments)), s.group.parent.acronym, s.group.acronym, s.requested_duration, s.pk))]
|
|
self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=comments]').click()
|
|
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
|
|
|
|
# schedule
|
|
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{} .drop-target'}});".format(s2.pk, slot1.pk))
|
|
|
|
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot1.pk, s2.pk))))
|
|
|
|
assignment = SchedTimeSessAssignment.objects.get(session=s2, schedule=schedule)
|
|
self.assertEqual(assignment.timeslot, slot1)
|
|
|
|
# timeslot constraint hints when selected
|
|
s1_element = self.driver.find_element_by_css_selector('#session{}'.format(s1.pk))
|
|
s1_element.click()
|
|
|
|
# violated due to constraints
|
|
self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{}.would-violate-hint'.format(slot1.pk)))
|
|
# violated due to missing capacity
|
|
self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{}.would-violate-hint'.format(slot3.pk)))
|
|
|
|
# reschedule
|
|
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{} .drop-target'}});".format(s2.pk, slot2.pk))
|
|
|
|
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot2.pk, s2.pk))))
|
|
|
|
assignment = SchedTimeSessAssignment.objects.get(session=s2, schedule=schedule)
|
|
self.assertEqual(assignment.timeslot, slot2)
|
|
|
|
# too many attendees warning
|
|
self.assertTrue(self.driver.find_elements_by_css_selector('#session{}.too-many-attendees'.format(s2.pk)))
|
|
|
|
# overfull timeslot
|
|
self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{}.overfull'.format(slot2.pk)))
|
|
|
|
# constraint hints
|
|
s1_element.click()
|
|
constraint_element = s2_element.find_element_by_css_selector(".constraints span[data-sessions=\"{}\"].would-violate-hint".format(s1.pk))
|
|
self.assertTrue(constraint_element.is_displayed())
|
|
|
|
# current constraint violations
|
|
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{} .drop-target'}});".format(s1.pk, slot1.pk))
|
|
|
|
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot1.pk, s1.pk))))
|
|
|
|
constraint_element = s2_element.find_element_by_css_selector(".constraints span[data-sessions=\"{}\"].violated-hint".format(s1.pk))
|
|
self.assertTrue(constraint_element.is_displayed())
|
|
|
|
# hide sessions in area
|
|
self.assertTrue(s1_element.is_displayed())
|
|
self.driver.find_element_by_css_selector(".session-parent-toggles [value=\"{}\"]".format(s1.group.parent.acronym)).click()
|
|
self.assertTrue(not s1_element.is_displayed())
|
|
self.driver.find_element_by_css_selector(".session-parent-toggles [value=\"{}\"]".format(s1.group.parent.acronym)).click()
|
|
self.assertTrue(s1_element.is_displayed())
|
|
|
|
# hide timeslots
|
|
self.driver.find_element_by_css_selector(".timeslot-group-toggles button").click()
|
|
self.assertTrue(self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal").is_displayed())
|
|
self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal [value=\"{}\"]".format("ts-group-{}-{}".format(slot2.time.strftime("%Y%m%d-%H%M"), int(slot2.duration.total_seconds() / 60)))).click()
|
|
self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal [data-dismiss=\"modal\"]").click()
|
|
self.assertTrue(not self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal").is_displayed())
|
|
|
|
# swap days
|
|
self.driver.find_element_by_css_selector(".day [data-target=\"#swap-days-modal\"][data-dayid=\"{}\"]".format(slot4.time.date().isoformat())).click()
|
|
self.assertTrue(self.driver.find_element_by_css_selector("#swap-days-modal").is_displayed())
|
|
self.driver.find_element_by_css_selector("#swap-days-modal input[name=\"target_day\"][value=\"{}\"]".format(slot1.time.date().isoformat())).click()
|
|
self.driver.find_element_by_css_selector("#swap-days-modal button[type=\"submit\"]").click()
|
|
|
|
self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{} #session{}'.format(slot4.pk, s1.pk)))
|
|
|
|
|
|
@skipIf(skip_selenium, skip_message)
|
|
@skipIf(django.VERSION[0]==2, "Skipping test with race conditions under Django 2")
|
|
class ScheduleEditTests(IetfLiveServerTestCase):
|
|
def setUp(self):
|
|
self.driver = start_web_driver()
|
|
self.driver.set_window_size(1024,768)
|
|
|
|
def tearDown(self):
|
|
self.driver.close()
|
|
|
|
def debug_snapshot(self,filename='debug_this.png'):
|
|
self.driver.execute_script("document.body.bgColor = 'white';")
|
|
self.driver.save_screenshot(filename)
|
|
|
|
def absreverse(self,*args,**kwargs):
|
|
return '%s%s'%(self.live_server_url,urlreverse(*args,**kwargs))
|
|
|
|
def login(self):
|
|
url = self.absreverse('ietf.ietfauth.views.login')
|
|
self.driver.get(url)
|
|
self.driver.find_element_by_name('username').send_keys('plain')
|
|
self.driver.find_element_by_name('password').send_keys('plain+password')
|
|
self.driver.find_element_by_xpath('//button[@type="submit"]').click()
|
|
|
|
def testUnschedule(self):
|
|
|
|
meeting = make_meeting_test_data()
|
|
colors.fg_group_colors['FARFUT'] = 'blue'
|
|
colors.bg_group_colors['FARFUT'] = 'white'
|
|
|
|
self.assertEqual(SchedTimeSessAssignment.objects.filter(session__meeting=meeting, session__group__acronym='mars', schedule__name='test-schedule').count(),1)
|
|
|
|
|
|
ss = list(SchedTimeSessAssignment.objects.filter(session__meeting__number=72,session__group__acronym='mars',schedule__name='test-schedule')) # pyflakes:ignore
|
|
|
|
self.login()
|
|
url = self.absreverse('ietf.meeting.views.edit_schedule',kwargs=dict(num='72',name='test-schedule',owner='plain@example.com'))
|
|
self.driver.get(url)
|
|
|
|
# driver.get() will wait for scripts to finish, but not ajax
|
|
# requests. Wait for completion of the permissions check:
|
|
read_only_note = self.driver.find_element_by_id('read_only')
|
|
WebDriverWait(self.driver, 10).until(expected_conditions.invisibility_of_element(read_only_note), "Read-only schedule")
|
|
|
|
s1 = Session.objects.filter(group__acronym='mars', meeting=meeting).first()
|
|
selector = "#session_{}".format(s1.pk)
|
|
WebDriverWait(self.driver, 30).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, selector)), "Did not find %s"%selector)
|
|
|
|
self.assertEqual(self.driver.find_elements_by_css_selector("#sortable-list #session_{}".format(s1.pk)), [])
|
|
|
|
element = self.driver.find_element_by_id('session_{}'.format(s1.pk))
|
|
target = self.driver.find_element_by_id('sortable-list')
|
|
ActionChains(self.driver).drag_and_drop(element,target).perform()
|
|
|
|
self.assertTrue(self.driver.find_elements_by_css_selector("#sortable-list #session_{}".format(s1.pk)))
|
|
|
|
time.sleep(0.1) # The API that modifies the database runs async
|
|
|
|
self.assertEqual(SchedTimeSessAssignment.objects.filter(session__meeting__number=72,session__group__acronym='mars',schedule__name='test-schedule').count(),0)
|
|
|
|
@skipIf(skip_selenium, skip_message)
|
|
class SlideReorderTests(IetfLiveServerTestCase):
|
|
def setUp(self):
|
|
self.driver = start_web_driver()
|
|
self.driver.set_window_size(1024,768)
|
|
self.session = SessionFactory(meeting__type_id='ietf', status_id='sched')
|
|
self.session.sessionpresentation_set.create(document=DocumentFactory(type_id='slides',name='one'),order=1)
|
|
self.session.sessionpresentation_set.create(document=DocumentFactory(type_id='slides',name='two'),order=2)
|
|
self.session.sessionpresentation_set.create(document=DocumentFactory(type_id='slides',name='three'),order=3)
|
|
|
|
def tearDown(self):
|
|
self.driver.close()
|
|
|
|
def absreverse(self,*args,**kwargs):
|
|
return '%s%s'%(self.live_server_url,urlreverse(*args,**kwargs))
|
|
|
|
def secr_login(self):
|
|
url = '%s%s'%(self.live_server_url, urlreverse('ietf.ietfauth.views.login'))
|
|
self.driver.get(url)
|
|
self.driver.find_element_by_name('username').send_keys('secretary')
|
|
self.driver.find_element_by_name('password').send_keys('secretary+password')
|
|
self.driver.find_element_by_xpath('//button[@type="submit"]').click()
|
|
|
|
#@override_settings(DEBUG=True)
|
|
def testReorderSlides(self):
|
|
return
|
|
url = self.absreverse('ietf.meeting.views.session_details',
|
|
kwargs=dict(
|
|
num=self.session.meeting.number,
|
|
acronym = self.session.group.acronym,))
|
|
self.secr_login()
|
|
self.driver.get(url)
|
|
#debug.show('unicode(self.driver.page_source)')
|
|
second = self.driver.find_element_by_css_selector('#slides tr:nth-child(2)')
|
|
third = self.driver.find_element_by_css_selector('#slides tr:nth-child(3)')
|
|
ActionChains(self.driver).drag_and_drop(second,third).perform()
|
|
|
|
time.sleep(0.1) # The API that modifies the database runs async
|
|
names=self.session.sessionpresentation_set.values_list('document__name',flat=True)
|
|
self.assertEqual(list(names),['one','three','two'])
|
|
|
|
# The following are useful debugging tools
|
|
|
|
# If you add this to a LiveServerTestCase and run just this test, you can browse
|
|
# to the test server with the data loaded by setUp() to debug why, for instance,
|
|
# a particular view isn't giving you what you expect
|
|
# def testJustSitThere(self):
|
|
# time.sleep(10000)
|
|
|
|
# The LiveServerTestCase server runs in a mode like production - it hides crashes with the
|
|
# user-friendly message about mail being sent to the maintainers, and eats that mail.
|
|
# Loading the page that crashed with just a TestCase will at least let you see the
|
|
# traceback.
|
|
#
|
|
#from ietf.utils.test_utils import TestCase
|
|
#class LookAtCrashTest(TestCase):
|
|
# def setUp(self):
|
|
# make_meeting_test_data()
|
|
#
|
|
# def testOpenSchedule(self):
|
|
# url = urlreverse('ietf.meeting.views.edit_schedule', kwargs=dict(num='72',name='test-schedule'))
|
|
# r = self.client.get(url)
|