From a5604992f2c9e20759048bfde5c7bdc0cb6e447f Mon Sep 17 00:00:00 2001 From: Jennifer Richards <jennifer@painless-security.com> Date: Fri, 30 Apr 2021 16:14:00 +0000 Subject: [PATCH] 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. Commit ready for merge. - Legacy-Id: 18970 --- ietf/meeting/tests_js.py | 267 ++++++++++++--- ietf/meeting/views.py | 12 +- ietf/static/ietf/js/agenda/agenda_filter.js | 1 + ietf/static/ietf/js/agenda/agenda_timezone.js | 229 +++++++++++++ ietf/static/ietf/js/agenda/timezone.js | 323 ++++-------------- ietf/templates/meeting/agenda.html | 107 +++--- ietf/templates/meeting/upcoming.html | 191 +++++++++-- 7 files changed, 746 insertions(+), 384 deletions(-) create mode 100644 ietf/static/ietf/js/agenda/agenda_timezone.js diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py index 13fec4bc3..95e7b9a35 100644 --- a/ietf/meeting/tests_js.py +++ b/ietf/meeting/tests_js.py @@ -13,7 +13,8 @@ import django #from django.urls import reverse as urlreverse from django.utils.text import slugify from django.db.models import F -from pytz import timezone +import pytz + #from django.test.utils import override_settings import debug # pyflakes:ignore @@ -37,7 +38,7 @@ from ietf import settings if selenium_enabled(): from selenium.webdriver.common.action_chains import ActionChains 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.common.exceptions import NoSuchElementException @@ -957,25 +958,22 @@ class AgendaTests(IetfSeleniumTestCase): with self.assertRaises(NoSuchElementException): 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): 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')) # wait for the select box to be updated - look for an arbitrary time zone to be in # its options list to detect this - WebDriverWait(self.driver, 2).until( - expected_conditions.presence_of_element_located((By.XPATH, '//option[@value="America/Halifax"]')) + arbitrary_tz = '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') local_tz_link = self.driver.find_element_by_id('local-timezone') utc_tz_link = self.driver.find_element_by_id('utc-timezone') @@ -991,73 +989,110 @@ class AgendaTests(IetfSeleniumTestCase): 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') + 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 - 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: self.assertEqual(disp.text.strip(), self.meeting.time_zone) # Click 'local' button local_tz_link.click() - self._wait_for_tz_change_from(self.meeting.time_zone) - self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), local_tz) + wait.until(expected_conditions.element_selection_state_to_be(meeting_tz_opt, False)) + 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: self.assertEqual(disp.text.strip(), local_tz) # click 'utc' button utc_tz_link.click() - self._wait_for_tz_change_from(local_tz) - self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), 'UTC') + wait.until(expected_conditions.element_to_be_selected(utc_tz_opt)) + 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: self.assertEqual(disp.text.strip(), 'UTC') # click back to meeting meeting_tz_link.click() - self._wait_for_tz_change_from('UTC') - self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), self.meeting.time_zone) + wait.until(expected_conditions.element_to_be_selected(meeting_tz_opt)) + 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: self.assertEqual(disp.text.strip(), self.meeting.time_zone) # and then back to UTC... utc_tz_link.click() - self._wait_for_tz_change_from(self.meeting.time_zone) - self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), 'UTC') + wait.until(expected_conditions.element_to_be_selected(utc_tz_opt)) + 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: self.assertEqual(disp.text.strip(), 'UTC') # ... and test the switch from UTC to local local_tz_link.click() - self._wait_for_tz_change_from('UTC') - self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), local_tz) + wait.until(expected_conditions.element_to_be_selected(local_tz_opt)) + 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: self.assertEqual(disp.text.strip(), local_tz) # Now select a different item from the select input - tz_select_input.select_by_value('America/Halifax') - self._wait_for_tz_change_from(self.meeting.time_zone) - self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), 'America/Halifax') + arbitrary_tz_opt.click() + wait.until(expected_conditions.element_to_be_selected(arbitrary_tz_opt)) + 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: - self.assertEqual(disp.text.strip(), 'America/Halifax') - + self.assertEqual(disp.text.strip(), arbitrary_tz) + def test_agenda_time_zone_selection_updates_weekview(self): """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 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 # its options list to detect this - WebDriverWait(self.driver, 2).until( - expected_conditions.presence_of_element_located((By.XPATH, '//option[@value="America/Halifax"]')) + wait = WebDriverWait(self.driver, 2) + 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 - tz_select_input.select_by_value('America/Halifax') - self._wait_for_tz_change_from(self.meeting.time_zone) - self.assertEqual(tz_select_input.first_selected_option.get_attribute('value'), 'America/Halifax') - self.driver.switch_to.frame('weekview') - wv_url = self.driver.execute_script('return document.location.href') - self.assertIn('tz=america/halifax', wv_url) - + option.click() + try: + wait.until(in_iframe_href('tz=america/halifax', 'weekview')) + except: + self.fail('iframe href not updated to contain selected time zone') + @ifSeleniumEnabled class WeekviewTests(IetfSeleniumTestCase): @@ -1099,7 +1134,7 @@ class WeekviewTests(IetfSeleniumTestCase): zones_to_test = ['utc', 'America/Halifax', 'Asia/Bangkok', 'Africa/Dakar', 'Europe/Dublin'] self.login() 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) for item in self.get_expected_items(): if item.session.name: @@ -1155,10 +1190,10 @@ class WeekviewTests(IetfSeleniumTestCase): # 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 - start_time_utc = timezone('UTC').localize( + start_time_utc = pytz.timezone('UTC').localize( 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( meeting=self.meeting, @@ -1551,6 +1586,152 @@ class InterimTests(IetfSeleniumTestCase): meetings.update(self.displayed_interims(groups=['mars'])) 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 # If you add this to a LiveServerTestCase and run just this test, you can browse diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index d3d3edc09..0b5e662da 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -3363,7 +3363,7 @@ def upcoming(request): # 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)) 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( Session.objects.filter( @@ -3411,13 +3411,11 @@ def upcoming(request): for o in entries: if isinstance(o, Meeting): - o.start_timestamp = int(pytz.utc.localize(datetime.datetime.combine(o.date, datetime.datetime.min.time())).timestamp()) - o.end_timestamp = int(pytz.utc.localize(datetime.datetime.combine(o.end, datetime.datetime.max.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.time.max)).timestamp()) else: - o.start_timestamp = int(o.official_timeslotassignment(). - timeslot.utc_start_time().timestamp()); - o.end_timestamp = int(o.official_timeslotassignment(). - timeslot.utc_end_time().timestamp()); + o.start_timestamp = int(o.official_timeslotassignment().timeslot.utc_start_time().timestamp()) + o.end_timestamp = int(o.official_timeslotassignment().timeslot.utc_end_time().timestamp()) # add menu entries menu_entries = get_interim_menu_entries(request) diff --git a/ietf/static/ietf/js/agenda/agenda_filter.js b/ietf/static/ietf/js/agenda/agenda_filter.js index 9840303df..58799390b 100644 --- a/ietf/static/ietf/js/agenda/agenda_filter.js +++ b/ietf/static/ietf/js/agenda/agenda_filter.js @@ -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. */ function enable () { + // ready handler fires immediately if document is already "ready" $(document).ready(function () { register_handlers(); update_view(); diff --git a/ietf/static/ietf/js/agenda/agenda_timezone.js b/ietf/static/ietf/js/agenda/agenda_timezone.js new file mode 100644 index 000000000..1d05a65d2 --- /dev/null +++ b/ietf/static/ietf/js/agenda/agenda_timezone.js @@ -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; +} \ No newline at end of file diff --git a/ietf/static/ietf/js/agenda/timezone.js b/ietf/static/ietf/js/agenda/timezone.js index 61e271e53..c282185a7 100644 --- a/ietf/static/ietf/js/agenda/timezone.js +++ b/ietf/static/ietf/js/agenda/timezone.js @@ -1,265 +1,74 @@ -// Callback for timezone change - called after current_timezone is updated -var timezone_change_callback; +// Copyright The IETF Trust 2021, All Rights Reserved -// 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(); - }); -} +/* + Timezone selection handling. Relies on the moment.js library. -// Initialize timezone system -function timezone_init(current) { - var tz_names = moment.tz.names(); - var select = $('#timezone_select'); - - select.empty(); - $.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(); -} + To use, create one (or more) select inputs with class "tz-select". When the initialize() + method is called, the options in the select will be replaced with the recognized time zone + names. Time zone can be changed via the select input or by calling the use() method with + the name of a time zone (or 'local' to guess the user's local timezone). + */ +var ietf_timezone; // public interface -// Select which timezone is used, 0 = meeting, 1 = browser local, 2 = UTC -function use_timezone (val) { - switch (val) { - case 0: - tz = meeting_timezone; - break; - case 1: - tz = local_timezone; - break; - default: - tz = 'UTC'; - break; +(function () { + 'use strict'; + // Callback for timezone change - called after current_timezone is updated + var timezone_change_callback; + var current_timezone; + + // Select timezone to use. Arg is name of a timezone or 'local' to guess local tz. + function use_timezone (newtz) { + // Guess local timezone if necessary + if (newtz.toLowerCase() === 'local') { + newtz = moment.tz.guess() + } + + 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 -// use different formats. -// Formats: 0 = long format "Saturday, October 24th 2020, 13:52 +00:00 UTC" -// 1 = Short format "13:52", "13:52 (-1)", or "13:52 (+1)" -// 2 = Short format with weekday, "Friday, 13:52 (-1)" -// 3 = Date only "2020-10-24" -// 4 = Date and time "2020-10-24 13:52" -// 5 = Time only "13:52". + /* Initialize timezone system + * + * This will set the timezone to the value of 'current'. Set up the tz_change callback + * before initializing. + */ + function timezone_init (current) { + var tz_names = moment.tz.names() + var select = $('select.tz-select') -function format_time(t, tz, fmt) { - var out; - var mtz = meeting_timezone; - if (mtz == "") { - mtz = "UTC"; + select.empty() + $.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 + })) + } + }) + 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) { - 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; + + // Expose public interface + ietf_timezone = { + get_current_tz: function() {return current_timezone}, + initialize: timezone_init, + set_tz_change_callback: function(cb) {timezone_change_callback=cb}, + use: use_timezone } - 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; -} \ No newline at end of file +})(); \ No newline at end of file diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html index 4a71b6988..65217663e 100644 --- a/ietf/templates/meeting/agenda.html +++ b/ietf/templates/meeting/agenda.html @@ -40,14 +40,14 @@ font-weight: normal; } .tz-display select { - min-width: 15em; + min-width: 15em; } #affix .nav li.tz-display { - padding: 4px 20px; + padding: 4px 20px; } #affix .nav li.tz-display a { - display: inline; - padding: 0; + display: inline; + padding: 0; } {% endblock %} @@ -76,12 +76,12 @@ <div class="col-xs-6"> <div class="tz-display"> <div><small> - <label for="timezone_select">Time zone:</label> - <a id="meeting-timezone" onclick="use_timezone(0)">Meeting</a> | - <a id="local-timezone" onclick="use_timezone(1)">Local</a> | - <a id="utc-timezone" onclick="use_timezone(2)">UTC</a> + <label for="timezone-select">Time zone:</label> + <a id="meeting-timezone" onclick="ietf_timezone.use('{{ timezone }}')">Meeting</a> | + <a id="local-timezone" onclick="ietf_timezone.use('local')">Local</a> | + <a id="utc-timezone" onclick="ietf_timezone.use('UTC')">UTC</a> </small></div> - <select id="timezone_select"> + <select id="timezone-select" class="tz-select"> {# Avoid blank while loading. JavaScript replaces the option list after init. #} <option selected>{{ timezone }}</option> </select> @@ -348,23 +348,23 @@ </div> <div class="col-md-2 hidden-print bs-docs-sidebar" id="affix"> <ul class="nav nav-pills nav-stacked small" data-spy="affix"> - <li><a href="#now">Now</a></li> - {% for item in filtered_assignments %} - {% 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> - {% endifchanged %} - {% endfor %} - <li><hr/></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 #} - <a onclick="use_timezone(0)">Meeting time</a> | - <a onclick="use_timezone(1)">Local time</a> | - <a onclick="use_timezone(2)">UTC</a></span> - </li> - {% if settings.DEBUG and settings.DEBUG_AGENDA %} - <li><hr/></li> - <li><span id="current-time"></span></li> - {% endif %} + <li><a href="#now">Now</a></li> + {% for item in filtered_assignments %} + {% 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> + {% endifchanged %} + {% endfor %} + <li><hr/></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 #} + <a onclick="ietf_timezone.use('{{ timezone }}')">Meeting time</a> | + <a onclick="ietf_timezone.use('local')">Local time</a> | + <a onclick="ietf_timezone.use('UTC')">UTC</a></span> + </li> + {% if settings.DEBUG and settings.DEBUG_AGENDA %} + <li><hr/></li> + <li><span id="current-time"></span></li> + {% endif %} </ul> </div> </div> @@ -376,8 +376,6 @@ <script src="{% static 'ietf/js/agenda/agenda_filter.js' %}"></script> <script> - var current_timezone = 'UTC'; - // Update the agenda display with specified filters function update_agenda_display(filter_params) { var agenda_rows=$('[id^="row-"]') @@ -402,7 +400,7 @@ // this is a "negative" item by wg: when present, hide these rows agenda_filter.rows_matching_filter_keyword(agenda_rows, v).hide(); }); - + // Now hide any session label rows with no visible sessions. Identify // by matching on start/end timestamps. $('tr.session-label-row').each(function(i, e) { @@ -442,15 +440,15 @@ } update_weekview_display(); } - + function update_weekview_display() { var weekview = $("#weekview"); if (!weekview.hasClass('hidden')) { var queryparams = window.location.search; if (queryparams) { - queryparams += '&tz=' + current_timezone.toLowerCase(); + queryparams += '&tz=' + ietf_timezone.get_current_tz().toLowerCase(); } else { - queryparams = '?tz=' + current_timezone.toLowerCase(); + queryparams = '?tz=' + ietf_timezone.get_current_tz().toLowerCase(); } var new_url = 'week-view.html' + queryparams; var wv_iframe = document.getElementById('weekview'); @@ -545,6 +543,7 @@ <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/timezone.js' %}"></script> + <script src="{% static 'ietf/js/agenda/agenda_timezone.js' %}"></script> <script> {% if settings.DEBUG and settings.DEBUG_AGENDA %} @@ -567,20 +566,40 @@ speedup = 1; {% 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); - agenda_filter.set_update_callback(update_view); - agenda_filter.enable(); + // First, initialize_moments(). This must be done before calling any of the update methods. + // It does not need timezone info, so safe to call before initializing ietf_timezone. + 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> {% endblock %} diff --git a/ietf/templates/meeting/upcoming.html b/ietf/templates/meeting/upcoming.html index 4adfda813..d3d30fc17 100644 --- a/ietf/templates/meeting/upcoming.html +++ b/ietf/templates/meeting/upcoming.html @@ -15,20 +15,56 @@ {% 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 %} {% origin %} <div class="row"> <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 - <span class="regular pull-right"> - <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> - </span> - </h1> - + </div> + </div> + </div> <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> @@ -67,18 +103,27 @@ </thead> <tbody> {% 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 == 'Meeting' %} {% 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><a class="ietf-meeting-link" href="{% url 'ietf.meeting.views.agenda' num=meeting.number %}">IETF {{ meeting.number }}</a></td> <td></td> {% endwith %} {% elif entry|classname == 'Session' %} {% 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 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 %} </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 %} {% block js %} <script src="{% static "jquery.tablesorter/js/jquery.tablesorter.combined.min.js" %}"></script> <script src="{% static 'fullcalendar/core/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/timezone.js' %}"></script> <script> // List of all events with meta-info needed for filtering var all_event_list = [{% for entry in entries %} {% if entry|classname == 'Meeting' %} {% with meeting=entry %} { - title: 'IETF {{ meeting.number }}', - start: '{{meeting.date}}', - end: '{{meeting.end}}', + ietf_meeting_number: '{{ meeting.number }}', + start_moment: moment.tz('{{meeting.date}}', '{{ meeting.time_zone }}').startOf('day'), + end_moment: moment.tz('{{meeting.end}}', '{{ meeting.time_zone }}').endOf('day'), url: '{% url 'ietf.meeting.views.agenda' num=meeting.number %}' }{% if not forloop.last %}, {% endif %} {% endwith %} {% else %} {# if it's not a Meeting, it's a Session #} {% 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 %}', filter_keywords: ["{{ session.filter_keywords|join:'","' }}"], - start: '{{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"}}', + start_moment: moment.utc('{{session.official_timeslotassignment.timeslot.utc_start_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 %}' } {% endwith %} @@ -144,7 +207,9 @@ {% endif %} {% endfor %}]; 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 current_tz = 'UTC'; // Test whether an event should be visible given a set of filter parameters function calendar_event_visible(filter_params, event) { @@ -158,28 +223,55 @@ && 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) { - var calendarEl = document.getElementById('calendar'); - var glue = calendarEl.clientWidth > 720 ? ' ' : '\n'; var filtered_output = []; for (var ii = 0; ii < event_list.length; ii++) { var this_event = event_list[ii]; if (calendar_event_visible(filter_params, this_event)) { - filtered_output.push({ - title: this_event.title + (this_event.group ? (glue + this_event.group) : ''), - start: this_event.start, - end: this_event.end, - url: this_event.url - }) + filtered_output.push(this_event); } } return filtered_output; } - // Initialize or update the calendar, updating the filtered event list - function update_calendar(filter_params) { - filtered_event_list = filter_calendar_events(filter_params, all_event_list); + // format a moment in a tz + var moment_formats = {time: 'HH:mm', date: 'YYYY-MM-DD', datetime: 'YYYY-MM-DD HH:mm'}; + 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) { event_calendar.refetchEvents() } else { @@ -191,7 +283,7 @@ event_calendar = new FullCalendar.Calendar(calendarEl, { plugins: ['dayGrid'], displayEventTime: false, - events: function (fInfo, success) {success(filtered_event_list)}, + events: function (fInfo, success) {success(display_events)}, eventRender: function (info) { $(info.el).tooltip({ title: info.event.title }) }, @@ -239,7 +331,7 @@ function update_view(filter_params) { update_meeting_display(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 @@ -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> {% endblock %}