Merged in [18970] from jennifer@painless-security.com:

Add timezone selector to upcoming meetings page. Separate general timezone handling from parts only relevant to main agenda page. Speed up agenda timezone javascript tests. Fixes #3184.
 - Legacy-Id: 18979
Note: SVN reference [18970] has been migrated to Git commit a5604992f2
This commit is contained in:
Robert Sparks 2021-05-07 19:11:25 +00:00
commit d119b22b29
7 changed files with 746 additions and 384 deletions

View file

@ -12,7 +12,8 @@ from unittest import skipIf
import django import django
from django.utils.text import slugify from django.utils.text import slugify
from django.db.models import F from django.db.models import F
from pytz import timezone import pytz
#from django.test.utils import override_settings #from django.test.utils import override_settings
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -36,7 +37,7 @@ from ietf import settings
if selenium_enabled(): if selenium_enabled():
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select, WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions from selenium.webdriver.support import expected_conditions
from selenium.common.exceptions import NoSuchElementException from selenium.common.exceptions import NoSuchElementException
@ -975,25 +976,22 @@ class AgendaTests(IetfSeleniumTestCase):
with self.assertRaises(NoSuchElementException): with self.assertRaises(NoSuchElementException):
self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title) self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title)
def _wait_for_tz_change_from(self, old_tz):
"""Helper to wait for tz displays to change from their old value"""
match = 'text()!="%s"' % old_tz
WebDriverWait(self.driver, 2).until(
expected_conditions.presence_of_element_located((By.XPATH, '//*[@class="current-tz"][%s]' % match))
)
def test_agenda_time_zone_selection(self): def test_agenda_time_zone_selection(self):
self.assertNotEqual(self.meeting.time_zone, 'UTC', 'Meeting time zone must not be UTC') self.assertNotEqual(self.meeting.time_zone, 'UTC', 'Meeting time zone must not be UTC')
wait = WebDriverWait(self.driver, 2)
self.driver.get(self.absreverse('ietf.meeting.views.agenda')) self.driver.get(self.absreverse('ietf.meeting.views.agenda'))
# wait for the select box to be updated - look for an arbitrary time zone to be in # wait for the select box to be updated - look for an arbitrary time zone to be in
# its options list to detect this # its options list to detect this
WebDriverWait(self.driver, 2).until( arbitrary_tz = 'America/Halifax'
expected_conditions.presence_of_element_located((By.XPATH, '//option[@value="America/Halifax"]')) arbitrary_tz_opt = WebDriverWait(self.driver, 2).until(
expected_conditions.presence_of_element_located(
(By.CSS_SELECTOR, '#timezone-select > option[value="%s"]' % arbitrary_tz)
)
) )
tz_select_input = Select(self.driver.find_element_by_id('timezone_select')) tz_select_input = self.driver.find_element_by_id('timezone-select')
meeting_tz_link = self.driver.find_element_by_id('meeting-timezone') meeting_tz_link = self.driver.find_element_by_id('meeting-timezone')
local_tz_link = self.driver.find_element_by_id('local-timezone') local_tz_link = self.driver.find_element_by_id('local-timezone')
utc_tz_link = self.driver.find_element_by_id('utc-timezone') utc_tz_link = self.driver.find_element_by_id('utc-timezone')
@ -1009,73 +1007,110 @@ class AgendaTests(IetfSeleniumTestCase):
self.assertNotEqual(self.meeting.time_zone, local_tz, 'Meeting time zone must not be local time zone') self.assertNotEqual(self.meeting.time_zone, local_tz, 'Meeting time zone must not be local time zone')
self.assertNotEqual(local_tz, 'UTC', 'Local time zone must not be UTC') self.assertNotEqual(local_tz, 'UTC', 'Local time zone must not be UTC')
meeting_tz_opt = tz_select_input.find_element_by_css_selector('option[value="%s"]' % self.meeting.time_zone)
local_tz_opt = tz_select_input.find_element_by_css_selector('option[value="%s"]' % local_tz)
utc_tz_opt = tz_select_input.find_element_by_css_selector('option[value="UTC"]')
# Should start off in meeting time zone # Should start off in meeting time zone
self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), self.meeting.time_zone) self.assertTrue(meeting_tz_opt.is_selected())
# don't yet know local_tz, so can't check that it's deselected here
self.assertFalse(arbitrary_tz_opt.is_selected())
self.assertFalse(utc_tz_opt.is_selected())
for disp in tz_displays: for disp in tz_displays:
self.assertEqual(disp.text.strip(), self.meeting.time_zone) self.assertEqual(disp.text.strip(), self.meeting.time_zone)
# Click 'local' button # Click 'local' button
local_tz_link.click() local_tz_link.click()
self._wait_for_tz_change_from(self.meeting.time_zone) wait.until(expected_conditions.element_selection_state_to_be(meeting_tz_opt, False))
self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), local_tz) self.assertFalse(meeting_tz_opt.is_selected())
# just identified the local_tz_opt as being selected, so no check here, either
self.assertFalse(arbitrary_tz_opt.is_selected())
self.assertFalse(utc_tz_opt.is_selected())
for disp in tz_displays: for disp in tz_displays:
self.assertEqual(disp.text.strip(), local_tz) self.assertEqual(disp.text.strip(), local_tz)
# click 'utc' button # click 'utc' button
utc_tz_link.click() utc_tz_link.click()
self._wait_for_tz_change_from(local_tz) wait.until(expected_conditions.element_to_be_selected(utc_tz_opt))
self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), 'UTC') self.assertFalse(meeting_tz_opt.is_selected())
self.assertFalse(local_tz_opt.is_selected()) # finally!
self.assertFalse(arbitrary_tz_opt.is_selected())
self.assertTrue(utc_tz_opt.is_selected())
for disp in tz_displays: for disp in tz_displays:
self.assertEqual(disp.text.strip(), 'UTC') self.assertEqual(disp.text.strip(), 'UTC')
# click back to meeting # click back to meeting
meeting_tz_link.click() meeting_tz_link.click()
self._wait_for_tz_change_from('UTC') wait.until(expected_conditions.element_to_be_selected(meeting_tz_opt))
self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), self.meeting.time_zone) self.assertTrue(meeting_tz_opt.is_selected())
self.assertFalse(local_tz_opt.is_selected())
self.assertFalse(arbitrary_tz_opt.is_selected())
self.assertFalse(utc_tz_opt.is_selected())
for disp in tz_displays: for disp in tz_displays:
self.assertEqual(disp.text.strip(), self.meeting.time_zone) self.assertEqual(disp.text.strip(), self.meeting.time_zone)
# and then back to UTC... # and then back to UTC...
utc_tz_link.click() utc_tz_link.click()
self._wait_for_tz_change_from(self.meeting.time_zone) wait.until(expected_conditions.element_to_be_selected(utc_tz_opt))
self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), 'UTC') self.assertFalse(meeting_tz_opt.is_selected())
self.assertFalse(local_tz_opt.is_selected())
self.assertFalse(arbitrary_tz_opt.is_selected())
self.assertTrue(utc_tz_opt.is_selected())
for disp in tz_displays: for disp in tz_displays:
self.assertEqual(disp.text.strip(), 'UTC') self.assertEqual(disp.text.strip(), 'UTC')
# ... and test the switch from UTC to local # ... and test the switch from UTC to local
local_tz_link.click() local_tz_link.click()
self._wait_for_tz_change_from('UTC') wait.until(expected_conditions.element_to_be_selected(local_tz_opt))
self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), local_tz) self.assertFalse(meeting_tz_opt.is_selected())
self.assertTrue(local_tz_opt.is_selected())
self.assertFalse(arbitrary_tz_opt.is_selected())
self.assertFalse(utc_tz_opt.is_selected())
for disp in tz_displays: for disp in tz_displays:
self.assertEqual(disp.text.strip(), local_tz) self.assertEqual(disp.text.strip(), local_tz)
# Now select a different item from the select input # Now select a different item from the select input
tz_select_input.select_by_value('America/Halifax') arbitrary_tz_opt.click()
self._wait_for_tz_change_from(self.meeting.time_zone) wait.until(expected_conditions.element_to_be_selected(arbitrary_tz_opt))
self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), 'America/Halifax') self.assertFalse(meeting_tz_opt.is_selected())
self.assertFalse(local_tz_opt.is_selected())
self.assertTrue(arbitrary_tz_opt.is_selected())
self.assertFalse(utc_tz_opt.is_selected())
for disp in tz_displays: for disp in tz_displays:
self.assertEqual(disp.text.strip(), 'America/Halifax') self.assertEqual(disp.text.strip(), arbitrary_tz)
def test_agenda_time_zone_selection_updates_weekview(self): def test_agenda_time_zone_selection_updates_weekview(self):
"""Changing the time zone should update the weekview to match""" """Changing the time zone should update the weekview to match"""
class in_iframe_href:
"""Condition class for use with WebDriverWait"""
def __init__(self, fragment, iframe):
self.fragment = fragment
self.iframe = iframe
def __call__(self, driver):
driver.switch_to.frame(self.iframe)
current_href= driver.execute_script(
'return document.location.href'
)
driver.switch_to.default_content()
return self.fragment in current_href
# enable a filter so the weekview iframe is visible # enable a filter so the weekview iframe is visible
self.driver.get(self.absreverse('ietf.meeting.views.agenda') + '?show=mars') self.driver.get(self.absreverse('ietf.meeting.views.agenda') + '?show=mars')
# wait for the select box to be updated - look for an arbitrary time zone to be in # wait for the select box to be updated - look for an arbitrary time zone to be in
# its options list to detect this # its options list to detect this
WebDriverWait(self.driver, 2).until( wait = WebDriverWait(self.driver, 2)
expected_conditions.presence_of_element_located((By.XPATH, '//option[@value="America/Halifax"]')) option = wait.until(
expected_conditions.presence_of_element_located(
(By.CSS_SELECTOR, '#timezone-select > option[value="America/Halifax"]'))
) )
tz_select_input = Select(self.driver.find_element_by_id('timezone_select'))
# Now select a different item from the select input # Now select a different item from the select input
tz_select_input.select_by_value('America/Halifax') option.click()
self._wait_for_tz_change_from(self.meeting.time_zone) try:
self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), 'America/Halifax') wait.until(in_iframe_href('tz=america/halifax', 'weekview'))
self.driver.switch_to.frame('weekview') except:
wv_url = self.driver.execute_script('return document.location.href') self.fail('iframe href not updated to contain selected time zone')
self.assertIn('tz=america/halifax', wv_url)
@ifSeleniumEnabled @ifSeleniumEnabled
class WeekviewTests(IetfSeleniumTestCase): class WeekviewTests(IetfSeleniumTestCase):
@ -1117,7 +1152,7 @@ class WeekviewTests(IetfSeleniumTestCase):
zones_to_test = ['utc', 'America/Halifax', 'Asia/Bangkok', 'Africa/Dakar', 'Europe/Dublin'] zones_to_test = ['utc', 'America/Halifax', 'Asia/Bangkok', 'Africa/Dakar', 'Europe/Dublin']
self.login() self.login()
for zone_name in zones_to_test: for zone_name in zones_to_test:
zone = timezone(zone_name) zone = pytz.timezone(zone_name)
self.driver.get(self.absreverse('ietf.meeting.views.week_view') + '?tz=' + zone_name) self.driver.get(self.absreverse('ietf.meeting.views.week_view') + '?tz=' + zone_name)
for item in self.get_expected_items(): for item in self.get_expected_items():
if item.session.name: if item.session.name:
@ -1173,10 +1208,10 @@ class WeekviewTests(IetfSeleniumTestCase):
# Session during a single day in meeting local time but multi-day UTC # Session during a single day in meeting local time but multi-day UTC
# Compute a time that overlaps midnight, UTC, but won't when shifted to a local time zone # Compute a time that overlaps midnight, UTC, but won't when shifted to a local time zone
start_time_utc = timezone('UTC').localize( start_time_utc = pytz.timezone('UTC').localize(
datetime.datetime.combine(self.meeting.date, datetime.time(23,0)) datetime.datetime.combine(self.meeting.date, datetime.time(23,0))
) )
start_time_local = start_time_utc.astimezone(timezone(self.meeting.time_zone)) start_time_local = start_time_utc.astimezone(pytz.timezone(self.meeting.time_zone))
daytime_session = SessionFactory( daytime_session = SessionFactory(
meeting=self.meeting, meeting=self.meeting,
@ -1569,6 +1604,152 @@ class InterimTests(IetfSeleniumTestCase):
meetings.update(self.displayed_interims(groups=['mars'])) meetings.update(self.displayed_interims(groups=['mars']))
self.do_upcoming_view_filter_test('?show=mars , ames &hide= ames', meetings) self.do_upcoming_view_filter_test('?show=mars , ames &hide= ames', meetings)
def test_upcoming_view_time_zone_selection(self):
wait = WebDriverWait(self.driver, 2)
def _assert_interim_tz_correct(sessions, tz):
zone = pytz.timezone(tz)
for session in sessions:
ts = session.official_timeslotassignment().timeslot
start = ts.utc_start_time().astimezone(zone).strftime('%Y-%m-%d %H:%M')
end = ts.utc_end_time().astimezone(zone).strftime('%H:%M')
meeting_link = self.driver.find_element_by_link_text(session.meeting.number)
time_td = meeting_link.find_element_by_xpath('../../td[@class="session-time"]')
self.assertIn('%s - %s' % (start, end), time_td.text)
def _assert_ietf_tz_correct(meetings, tz):
zone = pytz.timezone(tz)
for meeting in meetings:
meeting_zone = pytz.timezone(meeting.time_zone)
start_dt = meeting_zone.localize(datetime.datetime.combine(
meeting.date,
datetime.time.min
))
end_dt = meeting_zone.localize(datetime.datetime.combine(
start_dt + datetime.timedelta(days=meeting.days - 1),
datetime.time.max
))
start = start_dt.astimezone(zone).strftime('%Y-%m-%d')
end = end_dt.astimezone(zone).strftime('%Y-%m-%d')
meeting_link = self.driver.find_element_by_link_text("IETF " + meeting.number)
time_td = meeting_link.find_element_by_xpath('../../td[@class="meeting-time"]')
self.assertIn('%s - %s' % (start, end), time_td.text)
sessions = [m.session_set.first() for m in self.displayed_interims()]
self.assertGreater(len(sessions), 0)
ietf_meetings = self.all_ietf_meetings()
self.assertGreater(len(ietf_meetings), 0)
self.driver.get(self.absreverse('ietf.meeting.views.upcoming'))
tz_select_input = self.driver.find_element_by_id('timezone-select')
tz_select_bottom_input = self.driver.find_element_by_id('timezone-select-bottom')
local_tz_link = self.driver.find_element_by_id('local-timezone')
utc_tz_link = self.driver.find_element_by_id('utc-timezone')
local_tz_bottom_link = self.driver.find_element_by_id('local-timezone-bottom')
utc_tz_bottom_link = self.driver.find_element_by_id('utc-timezone-bottom')
# wait for the select box to be updated - look for an arbitrary time zone to be in
# its options list to detect this
arbitrary_tz = 'America/Halifax'
arbitrary_tz_opt = wait.until(
expected_conditions.presence_of_element_located(
(By.CSS_SELECTOR, '#timezone-select > option[value="%s"]' % arbitrary_tz)
)
)
arbitrary_tz_bottom_opt = tz_select_bottom_input.find_element_by_css_selector(
'option[value="%s"]' % arbitrary_tz)
utc_tz_opt = tz_select_input.find_element_by_css_selector('option[value="UTC"]')
utc_tz_bottom_opt= tz_select_bottom_input.find_element_by_css_selector('option[value="UTC"]')
# Moment.js guesses local time zone based on the behavior of Selenium's web client. This seems
# to inherit Django's settings.TIME_ZONE but I don't know whether that's guaranteed to be consistent.
# To avoid test fragility, ask Moment what it considers local and expect that.
local_tz = self.driver.execute_script('return moment.tz.guess();')
local_tz_opt = tz_select_input.find_element_by_css_selector('option[value=%s]' % local_tz)
local_tz_bottom_opt = tz_select_bottom_input.find_element_by_css_selector('option[value="%s"]' % local_tz)
# Should start off in local time zone
self.assertTrue(local_tz_opt.is_selected())
self.assertTrue(local_tz_bottom_opt.is_selected())
_assert_interim_tz_correct(sessions, local_tz)
_assert_ietf_tz_correct(ietf_meetings, local_tz)
# click 'utc' button
utc_tz_link.click()
wait.until(expected_conditions.element_to_be_selected(utc_tz_opt))
self.assertFalse(local_tz_opt.is_selected())
self.assertFalse(local_tz_bottom_opt.is_selected())
self.assertFalse(arbitrary_tz_opt.is_selected())
self.assertFalse(arbitrary_tz_bottom_opt.is_selected())
self.assertTrue(utc_tz_opt.is_selected())
self.assertTrue(utc_tz_bottom_opt.is_selected())
_assert_interim_tz_correct(sessions, 'UTC')
_assert_ietf_tz_correct(ietf_meetings, 'UTC')
# click back to 'local'
local_tz_link.click()
wait.until(expected_conditions.element_to_be_selected(local_tz_opt))
self.assertTrue(local_tz_opt.is_selected())
self.assertTrue(local_tz_bottom_opt.is_selected())
self.assertFalse(arbitrary_tz_opt.is_selected())
self.assertFalse(arbitrary_tz_bottom_opt.is_selected())
self.assertFalse(utc_tz_opt.is_selected())
self.assertFalse(utc_tz_bottom_opt.is_selected())
_assert_interim_tz_correct(sessions, local_tz)
_assert_ietf_tz_correct(ietf_meetings, local_tz)
# Now select a different item from the select input
arbitrary_tz_opt.click()
wait.until(expected_conditions.element_to_be_selected(arbitrary_tz_opt))
self.assertFalse(local_tz_opt.is_selected())
self.assertFalse(local_tz_bottom_opt.is_selected())
self.assertTrue(arbitrary_tz_opt.is_selected())
self.assertTrue(arbitrary_tz_bottom_opt.is_selected())
self.assertFalse(utc_tz_opt.is_selected())
self.assertFalse(utc_tz_bottom_opt.is_selected())
_assert_interim_tz_correct(sessions, arbitrary_tz)
_assert_ietf_tz_correct(ietf_meetings, arbitrary_tz)
# Now repeat those tests using the widgets at the bottom of the page
# click 'utc' button
utc_tz_bottom_link.click()
wait.until(expected_conditions.element_to_be_selected(utc_tz_opt))
self.assertFalse(local_tz_opt.is_selected())
self.assertFalse(local_tz_bottom_opt.is_selected())
self.assertFalse(arbitrary_tz_opt.is_selected())
self.assertFalse(arbitrary_tz_bottom_opt.is_selected())
self.assertTrue(utc_tz_opt.is_selected())
self.assertTrue(utc_tz_bottom_opt.is_selected())
_assert_interim_tz_correct(sessions, 'UTC')
_assert_ietf_tz_correct(ietf_meetings, 'UTC')
# click back to 'local'
local_tz_bottom_link.click()
wait.until(expected_conditions.element_to_be_selected(local_tz_opt))
self.assertTrue(local_tz_opt.is_selected())
self.assertTrue(local_tz_bottom_opt.is_selected())
self.assertFalse(arbitrary_tz_opt.is_selected())
self.assertFalse(arbitrary_tz_bottom_opt.is_selected())
self.assertFalse(utc_tz_opt.is_selected())
self.assertFalse(utc_tz_bottom_opt.is_selected())
_assert_interim_tz_correct(sessions, local_tz)
_assert_ietf_tz_correct(ietf_meetings, local_tz)
# Now select a different item from the select input
arbitrary_tz_bottom_opt.click()
wait.until(expected_conditions.element_to_be_selected(arbitrary_tz_opt))
self.assertFalse(local_tz_opt.is_selected())
self.assertFalse(local_tz_bottom_opt.is_selected())
self.assertTrue(arbitrary_tz_opt.is_selected())
self.assertTrue(arbitrary_tz_bottom_opt.is_selected())
self.assertFalse(utc_tz_opt.is_selected())
self.assertFalse(utc_tz_bottom_opt.is_selected())
_assert_interim_tz_correct(sessions, arbitrary_tz)
_assert_ietf_tz_correct(ietf_meetings, arbitrary_tz)
# The following are useful debugging tools # The following are useful debugging tools
# If you add this to a LiveServerTestCase and run just this test, you can browse # If you add this to a LiveServerTestCase and run just this test, you can browse

View file

@ -3363,7 +3363,7 @@ def upcoming(request):
# Get ietf meetings starting 7 days ago, and interim meetings starting today # Get ietf meetings starting 7 days ago, and interim meetings starting today
ietf_meetings = Meeting.objects.filter(type_id='ietf', date__gte=today-datetime.timedelta(days=7)) ietf_meetings = Meeting.objects.filter(type_id='ietf', date__gte=today-datetime.timedelta(days=7))
for m in ietf_meetings: for m in ietf_meetings:
m.end = m.date+datetime.timedelta(days=m.days) m.end = m.date + datetime.timedelta(days=m.days-1) # subtract 1 to avoid counting an extra day
interim_sessions = add_event_info_to_session_qs( interim_sessions = add_event_info_to_session_qs(
Session.objects.filter( Session.objects.filter(
@ -3411,13 +3411,11 @@ def upcoming(request):
for o in entries: for o in entries:
if isinstance(o, Meeting): if isinstance(o, Meeting):
o.start_timestamp = int(pytz.utc.localize(datetime.datetime.combine(o.date, datetime.datetime.min.time())).timestamp()) o.start_timestamp = int(pytz.utc.localize(datetime.datetime.combine(o.date, datetime.time.min)).timestamp())
o.end_timestamp = int(pytz.utc.localize(datetime.datetime.combine(o.end, datetime.datetime.max.time())).timestamp()) o.end_timestamp = int(pytz.utc.localize(datetime.datetime.combine(o.end, datetime.time.max)).timestamp())
else: else:
o.start_timestamp = int(o.official_timeslotassignment(). o.start_timestamp = int(o.official_timeslotassignment().timeslot.utc_start_time().timestamp())
timeslot.utc_start_time().timestamp()); o.end_timestamp = int(o.official_timeslotassignment().timeslot.utc_end_time().timestamp())
o.end_timestamp = int(o.official_timeslotassignment().
timeslot.utc_end_time().timestamp());
# add menu entries # add menu entries
menu_entries = get_interim_menu_entries(request) menu_entries = get_interim_menu_entries(request)

View file

@ -232,6 +232,7 @@ var agenda_filter_for_testing; // methods to be accessed for automated testing
* button UI. Do not call if you only want to use the parameter parsing routines. * button UI. Do not call if you only want to use the parameter parsing routines.
*/ */
function enable () { function enable () {
// ready handler fires immediately if document is already "ready"
$(document).ready(function () { $(document).ready(function () {
register_handlers(); register_handlers();
update_view(); update_view();

View file

@ -0,0 +1,229 @@
// Copyright The IETF Trust 2021, All Rights Reserved
/*
Timezone support specific to the agenda page
To properly handle timezones other than local, needs a method to retrieve
the current timezone. Set this by passing a method taking no parameters and
returning the current timezone to the set_current_tz_cb() method.
This should be done before calling anything else in the file.
*/
var meeting_timezone;
var local_timezone = moment.tz.guess();
// get_current_tz_cb must be overwritten using set_current_tz_cb
var get_current_tz_cb = function () {
throw new Error('Tried to get current timezone before callback registered. Use set_current_tz_cb().')
};
// Initialize moments
function initialize_moments() {
var times=$('span.time')
$.each(times, function(i, item) {
item.start_ts = moment.unix(this.getAttribute("data-start-time")).utc();
item.end_ts = moment.unix(this.getAttribute("data-end-time")).utc();
if (this.hasAttribute("weekday")) {
item.format=2;
} else {
item.format=1;
}
if (this.hasAttribute("format")) {
item.format = +this.getAttribute("format");
}
});
var times=$('[data-slot-start-ts]')
$.each(times, function(i, item) {
item.slot_start_ts = moment.unix(this.getAttribute("data-slot-start-ts")).utc();
item.slot_end_ts = moment.unix(this.getAttribute("data-slot-end-ts")).utc();
});
}
function format_time(t, tz, fmt) {
var out;
var mtz = meeting_timezone;
if (mtz == "") {
mtz = "UTC";
}
switch (fmt) {
case 0:
out = t.tz(tz).format('dddd, ') + '<span class="hidden-xs">' +
t.tz(tz).format('MMMM Do YYYY, ') + '</span>' +
t.tz(tz).format('HH:mm') + '<span class="hidden-xs">' +
t.tz(tz).format(' Z z') + '</span>';
break;
case 1:
// Note, this code does not work if the meeting crosses the
// year boundary.
out = t.tz(tz).format("HH:mm");
if (+t.tz(tz).dayOfYear() < +t.tz(mtz).dayOfYear()) {
out = out + " (-1)";
} else if (+t.tz(tz).dayOfYear() > +t.tz(mtz).dayOfYear()) {
out = out + " (+1)";
}
break;
case 2:
out = t.tz(mtz).format("dddd, ").toUpperCase() +
t.tz(tz).format("HH:mm");
if (+t.tz(tz).dayOfYear() < +t.tz(mtz).dayOfYear()) {
out = out + " (-1)";
} else if (+t.tz(tz).dayOfYear() > +t.tz(mtz).dayOfYear()) {
out = out + " (+1)";
}
break;
case 3:
out = t.utc().format("YYYY-MM-DD");
break;
case 4:
out = t.tz(tz).format("YYYY-MM-DD HH:mm");
break;
case 5:
out = t.tz(tz).format("HH:mm");
break;
}
return out;
}
// Format tooltip notice
function format_tooltip_notice(start, end) {
var notice = "";
if (end.isBefore()) {
notice = "Event ended " + end.fromNow();
} else if (start.isAfter()) {
notice = "Event will start " + start.fromNow();
} else {
notice = "Event started " + start.fromNow() + " and will end " +
end.fromNow();
}
return '<span class="tooltipnotice">' + notice + '</span>';
}
// Format tooltip table
function format_tooltip_table(start, end) {
var current_timezone = get_current_tz_cb();
var out = '<table><tr><th>Timezone</th><th>Start</th><th>End</th></tr>';
if (meeting_timezone !== "") {
out += '<tr><td class="timehead">Meeting timezone:</td><td>' +
format_time(start, meeting_timezone, 0) + '</td><td>' +
format_time(end, meeting_timezone, 0) + '</td></tr>';
}
out += '<tr><td class="timehead">Local timezone:</td><td>' +
format_time(start, local_timezone, 0) + '</td><td>' +
format_time(end, local_timezone, 0) + '</td></tr>';
if (current_timezone !== 'UTC') {
out += '<tr><td class="timehead">Selected Timezone:</td><td>' +
format_time(start, current_timezone, 0) + '</td><td>' +
format_time(end, current_timezone, 0) + '</td></tr>';
}
out += '<tr><td class="timehead">UTC:</td><td>' +
format_time(start, 'UTC', 0) + '</td><td>' +
format_time(end, 'UTC', 0) + '</td></tr>';
out += '</table>' + format_tooltip_notice(start, end);
return out;
}
// Format tooltip for item
function format_tooltip(start, end) {
return '<span class="timetooltiptext">' +
format_tooltip_table(start, end) +
'</span>';
}
// Add tooltips
function add_tooltips() {
$('span.time').each(function () {
var tooltip = $(format_tooltip(this.start_ts, this.end_ts));
tooltip[0].start_ts = this.start_ts;
tooltip[0].end_ts = this.end_ts;
tooltip[0].ustart_ts = moment(this.start_ts).add(-2, 'hours');
tooltip[0].uend_ts = moment(this.end_ts).add(2, 'hours');
$(this).parent().append(tooltip);
});
}
// Update times on the agenda based on the selected timezone
function update_times(newtz) {
$('span.current-tz').html(newtz);
$('span.time').each(function () {
if (this.format == 4) {
var tz = this.start_ts.tz(newtz).format(" z");
if (this.start_ts.tz(newtz).dayOfYear() ==
this.end_ts.tz(newtz).dayOfYear()) {
$(this).html(format_time(this.start_ts, newtz, this.format) +
'-' + format_time(this.end_ts, newtz, 5) + tz);
} else {
$(this).html(format_time(this.start_ts, newtz, this.format) +
'-' +
format_time(this.end_ts, newtz, this.format) + tz);
}
} else {
$(this).html(format_time(this.start_ts, newtz, this.format) + '-' +
format_time(this.end_ts, newtz, this.format));
}
});
update_tooltips_all();
update_clock();
}
// Highlight ongoing based on the current time
function highlight_ongoing() {
$("div#now").remove("#now");
$('.ongoing').removeClass("ongoing");
var agenda_rows=$('[data-slot-start-ts]')
agenda_rows = agenda_rows.filter(function() {
return moment().isBetween(this.slot_start_ts, this.slot_end_ts);
});
agenda_rows.addClass("ongoing");
agenda_rows.first().children("th, td").
prepend($('<div id="now" class="anchor-target"></div>'));
}
// Update tooltips
function update_tooltips() {
var tooltips=$('.timetooltiptext');
tooltips.filter(function() {
return moment().isBetween(this.ustart_ts, this.uend_ts);
}).each(function () {
$(this).html(format_tooltip_table(this.start_ts, this.end_ts));
});
}
// Update all tooltips
function update_tooltips_all() {
var tooltips=$('.timetooltiptext');
tooltips.each(function () {
$(this).html(format_tooltip_table(this.start_ts, this.end_ts));
});
}
// Update clock
function update_clock() {
$('#current-time').html(format_time(moment(), get_current_tz_cb(), 0));
}
$.urlParam = function(name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
if (results == null) {
return null;
} else {
return results[1] || 0;
}
}
function init_timers() {
var fast_timer = 60000 / (speedup > 600 ? 600 : speedup);
update_clock();
highlight_ongoing();
setInterval(function() { update_clock(); }, fast_timer);
setInterval(function() { highlight_ongoing(); }, fast_timer);
setInterval(function() { update_tooltips(); }, fast_timer);
setInterval(function() { update_tooltips_all(); }, 3600000 / speedup);
}
// set method used to find current time zone
function set_current_tz_cb(fn) {
get_current_tz_cb = fn;
}

View file

@ -1,265 +1,74 @@
// Callback for timezone change - called after current_timezone is updated // Copyright The IETF Trust 2021, All Rights Reserved
var timezone_change_callback;
// Initialize moments /*
function initialize_moments() { Timezone selection handling. Relies on the moment.js library.
var times=$('span.time')
$.each(times, function(i, item) {
item.start_ts = moment.unix(this.getAttribute("data-start-time")).utc();
item.end_ts = moment.unix(this.getAttribute("data-end-time")).utc();
if (this.hasAttribute("weekday")) {
item.format=2;
} else {
item.format=1;
}
if (this.hasAttribute("format")) {
item.format = +this.getAttribute("format");
}
});
var times=$('[data-slot-start-ts]')
$.each(times, function(i, item) {
item.slot_start_ts = moment.unix(this.getAttribute("data-slot-start-ts")).utc();
item.slot_end_ts = moment.unix(this.getAttribute("data-slot-end-ts")).utc();
});
}
// Initialize timezone system To use, create one (or more) select inputs with class "tz-select". When the initialize()
function timezone_init(current) { method is called, the options in the select will be replaced with the recognized time zone
var tz_names = moment.tz.names(); names. Time zone can be changed via the select input or by calling the use() method with
var select = $('#timezone_select'); the name of a time zone (or 'local' to guess the user's local timezone).
*/
select.empty(); var ietf_timezone; // public interface
$.each(tz_names, function(i, item) {
if (current === item) {
select.append($('<option/>', {
selected: "selected", html: item, value: item }));
} else {
select.append($('<option/>', {
html: item, value: item }));
}
});
initialize_moments();
select.change(function () {
update_times(this.value);
});
update_times(current);
add_tooltips();
}
// Select which timezone is used, 0 = meeting, 1 = browser local, 2 = UTC (function () {
function use_timezone (val) { 'use strict';
switch (val) { // Callback for timezone change - called after current_timezone is updated
case 0: var timezone_change_callback;
tz = meeting_timezone; var current_timezone;
break;
case 1: // Select timezone to use. Arg is name of a timezone or 'local' to guess local tz.
tz = local_timezone; function use_timezone (newtz) {
break; // Guess local timezone if necessary
default: if (newtz.toLowerCase() === 'local') {
tz = 'UTC'; newtz = moment.tz.guess()
break; }
if (current_timezone !== newtz) {
current_timezone = newtz
// Update values of tz-select inputs but do not trigger change event
$('select.tz-select').val(newtz)
if (timezone_change_callback) {
timezone_change_callback(newtz)
}
}
} }
$('#timezone_select').val(tz);
update_times(tz);
}
// Format time for item for timezone. Depending on the fmt /* Initialize timezone system
// use different formats. *
// Formats: 0 = long format "Saturday, October 24th 2020, 13:52 +00:00 UTC" * This will set the timezone to the value of 'current'. Set up the tz_change callback
// 1 = Short format "13:52", "13:52 (-1)", or "13:52 (+1)" * before initializing.
// 2 = Short format with weekday, "Friday, 13:52 (-1)" */
// 3 = Date only "2020-10-24" function timezone_init (current) {
// 4 = Date and time "2020-10-24 13:52" var tz_names = moment.tz.names()
// 5 = Time only "13:52". var select = $('select.tz-select')
function format_time(t, tz, fmt) { select.empty()
var out; $.each(tz_names, function (i, item) {
var mtz = meeting_timezone; if (current === item) {
if (mtz == "") { select.append($('<option/>', {
mtz = "UTC"; selected: 'selected', html: item, value: item
}))
} else {
select.append($('<option/>', {
html: item, value: item
}))
}
})
select.change(function () {use_timezone(this.value)});
/* When navigating back/forward, the browser may change the select input's
* value after the window load event. It does not fire the change event on
* the input when it does this. The pageshow event occurs after such an update,
* so trigger the change event ourselves to be sure the UI stays consistent
* with the timezone select input. */
window.addEventListener('pageshow', function(){select.change()})
use_timezone(current);
} }
switch (fmt) { // Expose public interface
case 0: ietf_timezone = {
out = t.tz(tz).format('dddd, ') + '<span class="hidden-xs">' + get_current_tz: function() {return current_timezone},
t.tz(tz).format('MMMM Do YYYY, ') + '</span>' + initialize: timezone_init,
t.tz(tz).format('HH:mm') + '<span class="hidden-xs">' + set_tz_change_callback: function(cb) {timezone_change_callback=cb},
t.tz(tz).format(' Z z') + '</span>'; use: use_timezone
break;
case 1:
// Note, this code does not work if the meeting crosses the
// year boundary.
out = t.tz(tz).format("HH:mm");
if (+t.tz(tz).dayOfYear() < +t.tz(mtz).dayOfYear()) {
out = out + " (-1)";
} else if (+t.tz(tz).dayOfYear() > +t.tz(mtz).dayOfYear()) {
out = out + " (+1)";
}
break;
case 2:
out = t.tz(mtz).format("dddd, ").toUpperCase() +
t.tz(tz).format("HH:mm");
if (+t.tz(tz).dayOfYear() < +t.tz(mtz).dayOfYear()) {
out = out + " (-1)";
} else if (+t.tz(tz).dayOfYear() > +t.tz(mtz).dayOfYear()) {
out = out + " (+1)";
}
break;
case 3:
out = t.utc().format("YYYY-MM-DD");
break;
case 4:
out = t.tz(tz).format("YYYY-MM-DD HH:mm");
break;
case 5:
out = t.tz(tz).format("HH:mm");
break;
} }
return out; })();
}
// Format tooltip notice
function format_tooltip_notice(start, end) {
var notice = "";
if (end.isBefore()) {
notice = "Event ended " + end.fromNow();
} else if (start.isAfter()) {
notice = "Event will start " + start.fromNow();
} else {
notice = "Event started " + start.fromNow() + " and will end " +
end.fromNow();
}
return '<span class="tooltipnotice">' + notice + '</span>';
}
// Format tooltip table
function format_tooltip_table(start, end) {
var out = '<table><tr><th>Timezone</th><th>Start</th><th>End</th></tr>';
if (meeting_timezone != "") {
out += '<tr><td class="timehead">Meeting timezone:</td><td>' +
format_time(start, meeting_timezone, 0) + '</td><td>' +
format_time(end, meeting_timezone, 0) + '</td></tr>';
}
out += '<tr><td class="timehead">Local timezone:</td><td>' +
format_time(start, local_timezone, 0) + '</td><td>' +
format_time(end, local_timezone, 0) + '</td></tr>';
if (current_timezone != 'UTC') {
out += '<tr><td class="timehead">Selected Timezone:</td><td>' +
format_time(start, current_timezone, 0) + '</td><td>' +
format_time(end, current_timezone, 0) + '</td></tr>';
}
out += '<tr><td class="timehead">UTC:</td><td>' +
format_time(start, 'UTC', 0) + '</td><td>' +
format_time(end, 'UTC', 0) + '</td></tr>';
out += '</table>' + format_tooltip_notice(start, end);
return out;
}
// Format tooltip for item
function format_tooltip(start, end) {
return '<span class="timetooltiptext">' +
format_tooltip_table(start, end) +
'</span>';
}
// Add tooltips
function add_tooltips() {
$('span.time').each(function () {
var tooltip = $(format_tooltip(this.start_ts, this.end_ts));
tooltip[0].start_ts = this.start_ts;
tooltip[0].end_ts = this.end_ts;
tooltip[0].ustart_ts = moment(this.start_ts).add(-2, 'hours');
tooltip[0].uend_ts = moment(this.end_ts).add(2, 'hours');
$(this).parent().append(tooltip);
});
}
// Update times on the agenda based on the selected timezone
function update_times(newtz) {
current_timezone = newtz;
$('span.current-tz').html(newtz);
$('span.time').each(function () {
if (this.format == 4) {
var tz = this.start_ts.tz(newtz).format(" z");
if (this.start_ts.tz(newtz).dayOfYear() ==
this.end_ts.tz(newtz).dayOfYear()) {
$(this).html(format_time(this.start_ts, newtz, this.format) +
'-' + format_time(this.end_ts, newtz, 5) + tz);
} else {
$(this).html(format_time(this.start_ts, newtz, this.format) +
'-' +
format_time(this.end_ts, newtz, this.format) + tz);
}
} else {
$(this).html(format_time(this.start_ts, newtz, this.format) + '-' +
format_time(this.end_ts, newtz, this.format));
}
});
update_tooltips_all();
update_clock();
if (timezone_change_callback) {
timezone_change_callback(newtz);
}
}
// Highlight ongoing based on the current time
function highlight_ongoing() {
$("div#now").remove("#now");
$('.ongoing').removeClass("ongoing");
var agenda_rows=$('[data-slot-start-ts]')
agenda_rows = agenda_rows.filter(function() {
return moment().isBetween(this.slot_start_ts, this.slot_end_ts);
});
agenda_rows.addClass("ongoing");
agenda_rows.first().children("th, td").
prepend($('<div id="now" class="anchor-target"></div>'));
}
// Update tooltips
function update_tooltips() {
var tooltips=$('.timetooltiptext');
tooltips.filter(function() {
return moment().isBetween(this.ustart_ts, this.uend_ts);
}).each(function () {
$(this).html(format_tooltip_table(this.start_ts, this.end_ts));
});
}
// Update all tooltips
function update_tooltips_all() {
var tooltips=$('.timetooltiptext');
tooltips.each(function () {
$(this).html(format_tooltip_table(this.start_ts, this.end_ts));
});
}
// Update clock
function update_clock() {
$('#current-time').html(format_time(moment(), current_timezone, 0));
}
$.urlParam = function(name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
if (results == null) {
return null;
} else {
return results[1] || 0;
}
}
function init_timers() {
var fast_timer = 60000 / (speedup > 600 ? 600 : speedup);
update_clock();
highlight_ongoing();
setInterval(function() { update_clock(); }, fast_timer);
setInterval(function() { highlight_ongoing(); }, fast_timer);
setInterval(function() { update_tooltips(); }, fast_timer);
setInterval(function() { update_tooltips_all(); }, 3600000 / speedup);
}
// Register a callback for timezone change
function set_tz_change_callback(cb) {
timezone_change_callback = cb;
}

View file

@ -40,14 +40,14 @@
font-weight: normal; font-weight: normal;
} }
.tz-display select { .tz-display select {
min-width: 15em; min-width: 15em;
} }
#affix .nav li.tz-display { #affix .nav li.tz-display {
padding: 4px 20px; padding: 4px 20px;
} }
#affix .nav li.tz-display a { #affix .nav li.tz-display a {
display: inline; display: inline;
padding: 0; padding: 0;
} }
{% endblock %} {% endblock %}
@ -76,12 +76,12 @@
<div class="col-xs-6"> <div class="col-xs-6">
<div class="tz-display"> <div class="tz-display">
<div><small> <div><small>
<label for="timezone_select">Time zone:</label> <label for="timezone-select">Time zone:</label>
<a id="meeting-timezone" onclick="use_timezone(0)">Meeting</a> | <a id="meeting-timezone" onclick="ietf_timezone.use('{{ timezone }}')">Meeting</a> |
<a id="local-timezone" onclick="use_timezone(1)">Local</a> | <a id="local-timezone" onclick="ietf_timezone.use('local')">Local</a> |
<a id="utc-timezone" onclick="use_timezone(2)">UTC</a> <a id="utc-timezone" onclick="ietf_timezone.use('UTC')">UTC</a>
</small></div> </small></div>
<select id="timezone_select"> <select id="timezone-select" class="tz-select">
{# Avoid blank while loading. JavaScript replaces the option list after init. #} {# Avoid blank while loading. JavaScript replaces the option list after init. #}
<option selected>{{ timezone }}</option> <option selected>{{ timezone }}</option>
</select> </select>
@ -348,23 +348,23 @@
</div> </div>
<div class="col-md-2 hidden-print bs-docs-sidebar" id="affix"> <div class="col-md-2 hidden-print bs-docs-sidebar" id="affix">
<ul class="nav nav-pills nav-stacked small" data-spy="affix"> <ul class="nav nav-pills nav-stacked small" data-spy="affix">
<li><a href="#now">Now</a></li> <li><a href="#now">Now</a></li>
{% for item in filtered_assignments %} {% for item in filtered_assignments %}
{% ifchanged item.timeslot.time|date:"Y-m-d" %} {% ifchanged item.timeslot.time|date:"Y-m-d" %}
<li><a href="#{{item.timeslot.time|slugify}}">{{ item.timeslot.time|date:"l, F j, Y" }}</a></li> <li><a href="#{{item.timeslot.time|slugify}}">{{ item.timeslot.time|date:"l, F j, Y" }}</a></li>
{% endifchanged %} {% endifchanged %}
{% endfor %} {% endfor %}
<li><hr/></li> <li><hr/></li>
<li class="tz-display">Showing <span class="current-tz">{{ timezone }}</span> time</li> <li class="tz-display">Showing <span class="current-tz">{{ timezone }}</span> time</li>
<li class="tz-display"><span> {# span avoids applying nav link styling to these shortcuts #} <li class="tz-display"><span> {# span avoids applying nav link styling to these shortcuts #}
<a onclick="use_timezone(0)">Meeting time</a> | <a onclick="ietf_timezone.use('{{ timezone }}')">Meeting time</a> |
<a onclick="use_timezone(1)">Local time</a> | <a onclick="ietf_timezone.use('local')">Local time</a> |
<a onclick="use_timezone(2)">UTC</a></span> <a onclick="ietf_timezone.use('UTC')">UTC</a></span>
</li> </li>
{% if settings.DEBUG and settings.DEBUG_AGENDA %} {% if settings.DEBUG and settings.DEBUG_AGENDA %}
<li><hr/></li> <li><hr/></li>
<li><span id="current-time"></span></li> <li><span id="current-time"></span></li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
</div> </div>
@ -376,8 +376,6 @@
<script src="{% static 'ietf/js/agenda/agenda_filter.js' %}"></script> <script src="{% static 'ietf/js/agenda/agenda_filter.js' %}"></script>
<script> <script>
var current_timezone = 'UTC';
// Update the agenda display with specified filters // Update the agenda display with specified filters
function update_agenda_display(filter_params) { function update_agenda_display(filter_params) {
var agenda_rows=$('[id^="row-"]') var agenda_rows=$('[id^="row-"]')
@ -402,7 +400,7 @@
// this is a "negative" item by wg: when present, hide these rows // this is a "negative" item by wg: when present, hide these rows
agenda_filter.rows_matching_filter_keyword(agenda_rows, v).hide(); agenda_filter.rows_matching_filter_keyword(agenda_rows, v).hide();
}); });
// Now hide any session label rows with no visible sessions. Identify // Now hide any session label rows with no visible sessions. Identify
// by matching on start/end timestamps. // by matching on start/end timestamps.
$('tr.session-label-row').each(function(i, e) { $('tr.session-label-row').each(function(i, e) {
@ -442,15 +440,15 @@
} }
update_weekview_display(); update_weekview_display();
} }
function update_weekview_display() { function update_weekview_display() {
var weekview = $("#weekview"); var weekview = $("#weekview");
if (!weekview.hasClass('hidden')) { if (!weekview.hasClass('hidden')) {
var queryparams = window.location.search; var queryparams = window.location.search;
if (queryparams) { if (queryparams) {
queryparams += '&tz=' + current_timezone.toLowerCase(); queryparams += '&tz=' + ietf_timezone.get_current_tz().toLowerCase();
} else { } else {
queryparams = '?tz=' + current_timezone.toLowerCase(); queryparams = '?tz=' + ietf_timezone.get_current_tz().toLowerCase();
} }
var new_url = 'week-view.html' + queryparams; var new_url = 'week-view.html' + queryparams;
var wv_iframe = document.getElementById('weekview'); var wv_iframe = document.getElementById('weekview');
@ -545,6 +543,7 @@
<script src="{% static 'moment/min/moment.min.js' %}"></script> <script src="{% static 'moment/min/moment.min.js' %}"></script>
<script src="{% static 'moment-timezone/builds/moment-timezone-with-data-10-year-range.min.js' %}"></script> <script src="{% static 'moment-timezone/builds/moment-timezone-with-data-10-year-range.min.js' %}"></script>
<script src="{% static 'ietf/js/agenda/timezone.js' %}"></script> <script src="{% static 'ietf/js/agenda/timezone.js' %}"></script>
<script src="{% static 'ietf/js/agenda/agenda_timezone.js' %}"></script>
<script> <script>
{% if settings.DEBUG and settings.DEBUG_AGENDA %} {% if settings.DEBUG and settings.DEBUG_AGENDA %}
@ -567,20 +566,40 @@
speedup = 1; speedup = 1;
{% endif %} {% endif %}
// Get meeting and local times, initialize timezone system
meeting_timezone = "{{timezone}}";
local_timezone = moment.tz.guess();
{% if "-utc" in request.path %}
timezone_init('UTC');
{% else %}
timezone_init(meeting_timezone);
{% endif %}
init_timers(); $(document).ready(function() {
// Methods/variables here that are not in ietf_timezone or agenda_filter are from agenda_timezone.js
meeting_timezone = '{{ timezone }}';
set_tz_change_callback(update_weekview_display); // First, initialize_moments(). This must be done before calling any of the update methods.
agenda_filter.set_update_callback(update_view); // It does not need timezone info, so safe to call before initializing ietf_timezone.
agenda_filter.enable(); initialize_moments(); // fills in moments in the agenda data
// Now set up callbacks related to ietf_timezone. This must happen before calling initialize().
// In particular, set_current_tz_cb() must be called before the update methods are called.
set_current_tz_cb(ietf_timezone.get_current_tz); // give agenda_timezone access to this method
ietf_timezone.set_tz_change_callback(function(newtz) {
update_times(newtz);
update_weekview_display();
}
);
// With callbacks in place, call ietf_timezone.initialize(). This will call the tz_change callback
// after setting things up.
{% if "-utc" in request.path %}
ietf_timezone.initialize('UTC');
{% else %}
ietf_timezone.initialize(meeting_timezone);
{% endif %}
// Now make other setup calls from agenda_timezone.js
add_tooltips();
init_timers();
// Finally, set up the agenda filter UI. This does not depend on the timezone.
agenda_filter.set_update_callback(update_view);
agenda_filter.enable();
}
);
</script> </script>
{% endblock %} {% endblock %}

View file

@ -15,20 +15,56 @@
{% block title %}Upcoming Meetings{% endblock %} {% block title %}Upcoming Meetings{% endblock %}
{% block morecss %}
div.title-buttons {
margin-bottom: 0.5em;
margin-top: 1em;
text-align: right;
}
.tz-display {
margin-top: 0.5em;
}
.tz-display label {
font-weight: normal;
}
.tz-display a {
cursor: pointer;
}
select.tz-select {
min-width: 15em;
margin-bottom: 0.3em;
}
{% endblock %}
{% block content %} {% block content %}
{% origin %} {% origin %}
<div class="row"> <div class="row">
<div class="col-md-10"> <div class="col-md-10">
<div class="row">
<div class="col-xs-6">
<h1>Upcoming Meetings</h1>
</div>
<div class="title-buttons col-xs-6">
<div>
<a title="iCalendar subscription for upcoming meetings" href="webcal://{{request.get_host}}{% url 'ietf.meeting.views.upcoming_ical' %}">
<span class="fa fa-stack-1"><i class="fa fa-fw fa-calendar-o fa-stack-1x"></i><i class="fa fa-fw fa-repeat fa-stack-xs"></i></span>
</a>
<a title="iCalendar entry for upcoming meetings" href="{% url 'ietf.meeting.views.upcoming_ical' %}"><span class="fa fa-calendar"></span></a>
</div>
<div class="tz-display">
<label for="timezone-select">Time zone:</label>
<small>
<a id="local-timezone" onclick="ietf_timezone.use('local')">Local</a> |
<a id="utc-timezone" onclick="ietf_timezone.use('UTC')">UTC</a>
</small>
<select class="tz-select" id="timezone-select" autocomplete="off">
<!-- Avoid blank while loading. Needs to agree with native times in the table
so the display is correct if JS is not enabled -->
<option selected>UTC</option>
</select>
<h1>Upcoming Meetings </div>
<span class="regular pull-right"> </div>
<a title="iCalendar subscription for upcoming meetings" href="webcal://{{request.get_host}}{% url 'ietf.meeting.views.upcoming_ical' %}"> </div>
<span class="fa fa-stack-1"><i class="fa fa-fw fa-calendar-o fa-stack-1x"></i><i class="fa fa-fw fa-repeat fa-stack-xs"></i></span>
</a>
<a title="iCalendar entry for upcoming meetings" href="{% url 'ietf.meeting.views.upcoming_ical' %}"><span class="fa fa-calendar"></span></a>
</span>
</h1>
<p>For more on regular IETF meetings see <a href="https://www.ietf.org/meeting/upcoming.html">here</a></p> <p>For more on regular IETF meetings see <a href="https://www.ietf.org/meeting/upcoming.html">here</a></p>
<p>Meeting important dates are not included in upcoming meeting calendars. They have <a href="{% url 'ietf.meeting.views.important_dates' %}">their own calendar</a></p> <p>Meeting important dates are not included in upcoming meeting calendars. They have <a href="{% url 'ietf.meeting.views.important_dates' %}">their own calendar</a></p>
@ -67,18 +103,27 @@
</thead> </thead>
<tbody> <tbody>
{% for entry in entries %} {% for entry in entries %}
<tr class="entry" <tr class="entry"
{% if entry|classname == 'Session' %}data-filter-keywords="{{ entry.filter_keywords|join:',' }}"{% endif %}> {% if entry|classname == 'Session' %}data-filter-keywords="{{ entry.filter_keywords|join:',' }}"{% endif %}>
{% if entry|classname == 'Meeting' %} {% if entry|classname == 'Meeting' %}
{% with meeting=entry %} {% with meeting=entry %}
<td>{{ meeting.date }} - {{ meeting.end }}</td> <td class="meeting-time"
data-start-date="{{ meeting.date }}" {# dates local to meeting #}
data-end-date="{{ meeting.end }}"
data-time-zone="{{ meeting.time_zone }}">
{{ meeting.date }} - {{ meeting.end }}
</td>
<td>ietf</td> <td>ietf</td>
<td><a class="ietf-meeting-link" href="{% url 'ietf.meeting.views.agenda' num=meeting.number %}">IETF {{ meeting.number }}</a></td> <td><a class="ietf-meeting-link" href="{% url 'ietf.meeting.views.agenda' num=meeting.number %}">IETF {{ meeting.number }}</a></td>
<td></td> <td></td>
{% endwith %} {% endwith %}
{% elif entry|classname == 'Session' %} {% elif entry|classname == 'Session' %}
{% with session=entry group=entry.group meeting=entry.meeting%} {% with session=entry group=entry.group meeting=entry.meeting%}
<td>{{ session.official_timeslotassignment.timeslot.utc_start_time | date:"Y-m-d H:i"}} - {{ session.official_timeslotassignment.timeslot.utc_end_time | date:"H:i e" }}</td> <td class="session-time"
data-start-utc="{{ session.official_timeslotassignment.timeslot.utc_start_time | date:'Y-m-d H:i' }}Z"
data-end-utc="{{ session.official_timeslotassignment.timeslot.utc_end_time | date:'Y-m-d H:i' }}Z">
{{ session.official_timeslotassignment.timeslot.utc_start_time | date:"Y-m-d H:i"}} - {{ session.official_timeslotassignment.timeslot.utc_end_time | date:"H:i" }}
</td>
<td><a href="{% url 'ietf.group.views.group_home' acronym=group.acronym %}">{{ group.acronym }}</a></td> <td><a href="{% url 'ietf.group.views.group_home' acronym=group.acronym %}">{{ group.acronym }}</a></td>
<td> <td>
<a class="interim-meeting-link" href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=group.acronym %}"> {{ meeting.number }}</a> <a class="interim-meeting-link" href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=group.acronym %}"> {{ meeting.number }}</a>
@ -109,34 +154,52 @@
{% endcache %} {% endcache %}
</div> </div>
</div> </div>
<div id="calendar" class="col-md-10" ></div> <div class="row">
<div class="col-md-10">
<div class="tz-display text-right">
<label for="timezone-select-bottom">Time zone: </label>
<small>
<a id="local-timezone-bottom" onclick="ietf_timezone.use('local')">Local</a> |
<a id="utc-timezone-bottom" onclick="ietf_timezone.use('UTC')">UTC</a>
</small>
<select class="tz-select" id="timezone-select-bottom"></select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-10">
<div id="calendar"></div>
</div>
</div>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script src="{% static "jquery.tablesorter/js/jquery.tablesorter.combined.min.js" %}"></script> <script src="{% static "jquery.tablesorter/js/jquery.tablesorter.combined.min.js" %}"></script>
<script src="{% static 'fullcalendar/core/main.js' %}"></script> <script src="{% static 'fullcalendar/core/main.js' %}"></script>
<script src="{% static 'fullcalendar/daygrid/main.js' %}"></script> <script src="{% static 'fullcalendar/daygrid/main.js' %}"></script>
<script src="{% static 'moment/min/moment.min.js' %}"></script>
<script src="{% static 'moment-timezone/builds/moment-timezone-with-data-10-year-range.min.js' %}"></script>
<script src="{% static 'ietf/js/agenda/agenda_filter.js' %}"></script> <script src="{% static 'ietf/js/agenda/agenda_filter.js' %}"></script>
<script src="{% static 'ietf/js/agenda/timezone.js' %}"></script>
<script> <script>
// List of all events with meta-info needed for filtering // List of all events with meta-info needed for filtering
var all_event_list = [{% for entry in entries %} var all_event_list = [{% for entry in entries %}
{% if entry|classname == 'Meeting' %} {% if entry|classname == 'Meeting' %}
{% with meeting=entry %} {% with meeting=entry %}
{ {
title: 'IETF {{ meeting.number }}', ietf_meeting_number: '{{ meeting.number }}',
start: '{{meeting.date}}', start_moment: moment.tz('{{meeting.date}}', '{{ meeting.time_zone }}').startOf('day'),
end: '{{meeting.end}}', end_moment: moment.tz('{{meeting.end}}', '{{ meeting.time_zone }}').endOf('day'),
url: '{% url 'ietf.meeting.views.agenda' num=meeting.number %}' url: '{% url 'ietf.meeting.views.agenda' num=meeting.number %}'
}{% if not forloop.last %}, {% endif %} }{% if not forloop.last %}, {% endif %}
{% endwith %} {% endwith %}
{% else %} {# if it's not a Meeting, it's a Session #} {% else %} {# if it's not a Meeting, it's a Session #}
{% with session=entry %} {% with session=entry %}
{ {
title: '{{session.official_timeslotassignment.timeslot.utc_start_time|date:"H:i"}}-{{session.official_timeslotassignment.timeslot.utc_end_time|date:"H:i"}}',
group: '{% if session.group %}{{session.group.acronym}}{% endif %}', group: '{% if session.group %}{{session.group.acronym}}{% endif %}',
filter_keywords: ["{{ session.filter_keywords|join:'","' }}"], filter_keywords: ["{{ session.filter_keywords|join:'","' }}"],
start: '{{session.official_timeslotassignment.timeslot.utc_start_time | date:"Y-m-d H:i"}}', start_moment: moment.utc('{{session.official_timeslotassignment.timeslot.utc_start_time | date:"Y-m-d H:i"}}'),
end: '{{session.official_timeslotassignment.timeslot.utc_end_time | date:"Y-m-d H:i"}}', end_moment: moment.utc('{{session.official_timeslotassignment.timeslot.utc_end_time | date:"Y-m-d H:i"}}'),
url: '{% url 'ietf.meeting.views.session_details' num=session.meeting.number acronym=session.group.acronym %}' url: '{% url 'ietf.meeting.views.session_details' num=session.meeting.number acronym=session.group.acronym %}'
} }
{% endwith %} {% endwith %}
@ -144,7 +207,9 @@
{% endif %} {% endif %}
{% endfor %}]; {% endfor %}];
var filtered_event_list = []; // currently visible list var filtered_event_list = []; // currently visible list
var display_events = []; // filtered events, processed for calendar display
var event_calendar; // handle on the calendar object var event_calendar; // handle on the calendar object
var current_tz = 'UTC';
// Test whether an event should be visible given a set of filter parameters // Test whether an event should be visible given a set of filter parameters
function calendar_event_visible(filter_params, event) { function calendar_event_visible(filter_params, event) {
@ -158,28 +223,55 @@
&& agenda_filter.keyword_match(filter_params.show, event.filter_keywords)); && agenda_filter.keyword_match(filter_params.show, event.filter_keywords));
} }
// Apply filter_params to the event list and format data for the calendar /* Apply filter_params to the event list */
function filter_calendar_events(filter_params, event_list) { function filter_calendar_events(filter_params, event_list) {
var calendarEl = document.getElementById('calendar');
var glue = calendarEl.clientWidth > 720 ? ' ' : '\n';
var filtered_output = []; var filtered_output = [];
for (var ii = 0; ii < event_list.length; ii++) { for (var ii = 0; ii < event_list.length; ii++) {
var this_event = event_list[ii]; var this_event = event_list[ii];
if (calendar_event_visible(filter_params, this_event)) { if (calendar_event_visible(filter_params, this_event)) {
filtered_output.push({ filtered_output.push(this_event);
title: this_event.title + (this_event.group ? (glue + this_event.group) : ''),
start: this_event.start,
end: this_event.end,
url: this_event.url
})
} }
} }
return filtered_output; return filtered_output;
} }
// Initialize or update the calendar, updating the filtered event list // format a moment in a tz
function update_calendar(filter_params) { var moment_formats = {time: 'HH:mm', date: 'YYYY-MM-DD', datetime: 'YYYY-MM-DD HH:mm'};
filtered_event_list = filter_calendar_events(filter_params, all_event_list); function format_moment(t_moment, tz, fmt_type) {
return t_moment.tz(tz).format(moment_formats[fmt_type]);
}
function make_display_events(event_data, tz) {
var output = [];
var calendarEl = document.getElementById('calendar');
var glue = calendarEl.clientWidth > 720 ? ' ' : '\n';
return $.map(event_data, function(src_event) {
var title;
// Render IETF meetings with meeting dates, sessions with actual times
if (src_event.ietf_meeting_number) {
title = 'IETF ' + src_event.ietf_meeting_number;
} else {
title = (format_moment(src_event.start_moment, tz, 'time') + '-'
+ format_moment(src_event.end_moment, tz, 'time')
+ glue + (src_event.group || 'Invalid event'));
}
return {
title: title,
start: format_moment(src_event.start_moment, tz, 'datetime'),
end: format_moment(src_event.end_moment, tz, 'datetime'),
url: src_event.url
}; // all events have the URL
});
}
// Initialize or update the calendar, updating the filtered event list and/or timezone
function update_calendar(tz, filter_params) {
if (filter_params) {
// Update event list if we were called with filter params
filtered_event_list = filter_calendar_events(filter_params, all_event_list);
}
display_events = make_display_events(filtered_event_list, tz);
if (event_calendar) { if (event_calendar) {
event_calendar.refetchEvents() event_calendar.refetchEvents()
} else { } else {
@ -191,7 +283,7 @@
event_calendar = new FullCalendar.Calendar(calendarEl, { event_calendar = new FullCalendar.Calendar(calendarEl, {
plugins: ['dayGrid'], plugins: ['dayGrid'],
displayEventTime: false, displayEventTime: false,
events: function (fInfo, success) {success(filtered_event_list)}, events: function (fInfo, success) {success(display_events)},
eventRender: function (info) { eventRender: function (info) {
$(info.el).tooltip({ title: info.event.title }) $(info.el).tooltip({ title: info.event.title })
}, },
@ -239,7 +331,7 @@
function update_view(filter_params) { function update_view(filter_params) {
update_meeting_display(filter_params); update_meeting_display(filter_params);
update_links(filter_params); update_links(filter_params);
update_calendar(filter_params); update_calendar(current_tz, filter_params);
} }
// Set up the filtering - the callback will be called when the page loads and on any filter changes // Set up the filtering - the callback will be called when the page loads and on any filter changes
@ -276,5 +368,38 @@
}); });
} }
}); });
function format_session_time(session_elt, tz) {
var start = moment.utc($(session_elt).attr('data-start-utc'));
var end = moment.utc($(session_elt).attr('data-end-utc'));
return format_moment(start, tz, 'datetime') + ' - ' + format_moment(end, tz, 'time');
}
function format_meeting_time(meeting_elt, tz) {
var meeting_tz = $(meeting_elt).attr('data-time-zone');
var start = moment.tz($(meeting_elt).attr('data-start-date'), meeting_tz).startOf('day');
var end = moment.tz($(meeting_elt).attr('data-end-date'), meeting_tz).endOf('day');
return format_moment(start, tz, 'date') + ' - ' + format_moment(end, tz, 'date');
}
function timezone_changed(newtz) {
// update times for events in the table
if (current_tz !== newtz) {
current_tz = newtz;
$('.session-time').each(function () {
$(this).html(format_session_time(this, newtz));
});
$('.meeting-time').each(function () {
$(this).html(format_meeting_time(this, newtz));
});
}
update_calendar(newtz);
}
// Init with best guess at local timezone.
ietf_timezone.set_tz_change_callback(timezone_changed);
ietf_timezone.initialize('local');
</script> </script>
{% endblock %} {% endblock %}