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

Add timezone support to agenda weekview; display UTC on UTC agenda page. Fixes #3111.
 - Legacy-Id: 18796
Note: SVN reference [18712] has been migrated to Git commit d29553c0bc
This commit is contained in:
Robert Sparks 2021-01-15 19:59:56 +00:00
commit 159b8fe37c
9 changed files with 354 additions and 72 deletions

View file

@ -1,5 +1,7 @@
# -*- conf-mode -*- # -*- conf-mode -*-
/personal/kivinen/7.22.1.dev0@18689 # Hold for revision based on timezone-aware code
/personal/rcross/7.19.1.dev0@18663 /personal/rcross/7.19.1.dev0@18663
/personal/rcross/7.19.1.dev0@18662 /personal/rcross/7.19.1.dev0@18662

View file

@ -13,6 +13,8 @@
"js-cookie": "~2", "js-cookie": "~2",
"jquery": "~1", "jquery": "~1",
"jquery.tablesorter": "~2", "jquery.tablesorter": "~2",
"moment": "~2",
"moment-timezone": "~0",
"respond": "~1", "respond": "~1",
"select2": "~3", "select2": "~3",
"select2-bootstrap-css": "~1", "select2-bootstrap-css": "~1",
@ -33,6 +35,11 @@
"./fonts/*" "./fonts/*"
] ]
}, },
"moment": {
"main": [
"min/moment.min.js"
]
},
"tablesorter": { "tablesorter": {
"main": [ "main": [
"dist/js/jquery.tablesorter.combined.min.js", "dist/js/jquery.tablesorter.combined.min.js",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -13,6 +13,7 @@ import django
from django.urls import reverse as urlreverse from django.urls import reverse as urlreverse
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
#from django.test.utils import override_settings #from django.test.utils import override_settings
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -23,7 +24,7 @@ from ietf.group import colors
from ietf.person.models import Person from ietf.person.models import Person
from ietf.group.models import Group from ietf.group.models import Group
from ietf.group.factories import GroupFactory from ietf.group.factories import GroupFactory
from ietf.meeting.factories import SessionFactory from ietf.meeting.factories import SessionFactory, TimeSlotFactory
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting
from ietf.meeting.models import (Schedule, SchedTimeSessAssignment, Session, from ietf.meeting.models import (Schedule, SchedTimeSessAssignment, Session,
Room, TimeSlot, Constraint, ConstraintName, Room, TimeSlot, Constraint, ConstraintName,
@ -904,6 +905,200 @@ class AgendaTests(MeetingTestCase):
self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title) self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title)
@skipIf(skip_selenium, skip_message)
class WeekviewTests(MeetingTestCase):
def setUp(self):
super(WeekviewTests, self).setUp()
self.meeting = make_meeting_test_data()
def get_expected_items(self):
expected_items = self.meeting.schedule.assignments.exclude(timeslot__type__in=['lead', 'offagenda'])
self.assertGreater(len(expected_items), 0, 'Test setup generated an empty schedule')
return expected_items
def test_timezone_default(self):
"""Week view should show local times by default"""
self.assertNotEqual(self.meeting.time_zone.lower(), 'utc',
'Cannot test local time weekview because meeting is using UTC time.')
self.login()
self.driver.get(self.absreverse('ietf.meeting.views.week_view'))
for item in self.get_expected_items():
if item.session.name:
expected_name = item.session.name
elif item.timeslot.type_id == 'break':
expected_name = item.timeslot.name
else:
expected_name = item.session.group.name
expected_time = '-'.join([item.timeslot.local_start_time().strftime('%H%M'),
item.timeslot.local_end_time().strftime('%H%M')])
WebDriverWait(self.driver, 2).until(
expected_conditions.presence_of_element_located(
(By.XPATH,
'//div/div[contains(text(), "%s")]/span[contains(text(), "%s")]' % (
expected_time, expected_name))
)
)
def test_timezone_selection(self):
"""Week view should show time zones when requested"""
# Must test utc; others are picked arbitrarily
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)
self.driver.get(self.absreverse('ietf.meeting.views.week_view') + '?tz=' + zone_name)
for item in self.get_expected_items():
if item.session.name:
expected_name = item.session.name
elif item.timeslot.type_id == 'break':
expected_name = item.timeslot.name
else:
expected_name = item.session.group.name
start_time = item.timeslot.utc_start_time().astimezone(zone)
end_time = item.timeslot.utc_end_time().astimezone(zone)
expected_time = '-'.join([start_time.strftime('%H%M'),
end_time.strftime('%H%M')])
WebDriverWait(self.driver, 2).until(
expected_conditions.presence_of_element_located(
(By.XPATH,
'//div/div[contains(text(), "%s")]/span[contains(text(), "%s")]' % (
expected_time, expected_name))
),
'Could not find event "%s" at %s for time zone %s' % (expected_name,
expected_time,
zone_name),
)
def test_event_wrapping(self):
"""Events that overlap midnight should be shown on both days
This assumes that the meeting is in America/New_York timezone.
"""
def _assert_wrapped(displayed, expected_time_string):
self.assertEqual(len(displayed), 2)
first = displayed[0]
first_parent = first.find_element_by_xpath('..')
second = displayed[1]
second_parent = second.find_element_by_xpath('..')
self.assertNotIn('continued', first.text)
self.assertIn(expected_time_string, first_parent.text)
self.assertIn('continued', second.text)
self.assertIn(expected_time_string, second_parent.text)
def _assert_not_wrapped(displayed, expected_time_string):
self.assertEqual(len(displayed), 1)
first = displayed[0]
first_parent = first.find_element_by_xpath('..')
self.assertNotIn('continued', first.text)
self.assertIn(expected_time_string, first_parent.text)
duration = datetime.timedelta(minutes=120) # minutes
# 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(
datetime.datetime.combine(self.meeting.date, datetime.time(23,0))
)
start_time_local = start_time_utc.astimezone(timezone(self.meeting.time_zone))
daytime_session = SessionFactory(
meeting=self.meeting,
name='Single Day Session for Wrapping Test',
add_to_schedule=False,
)
daytime_timeslot = TimeSlotFactory(
meeting=self.meeting,
time=start_time_local.replace(tzinfo=None), # drop timezone for Django
duration=duration,
)
daytime_session.timeslotassignments.create(timeslot=daytime_timeslot, schedule=self.meeting.schedule)
# Session that overlaps midnight in meeting local time
overnight_session = SessionFactory(
meeting=self.meeting,
name='Overnight Session for Wrapping Test',
add_to_schedule=False,
)
overnight_timeslot = TimeSlotFactory(
meeting=self.meeting,
time=datetime.datetime.combine(self.meeting.date, datetime.time(23,0)),
duration=duration,
)
overnight_session.timeslotassignments.create(timeslot=overnight_timeslot, schedule=self.meeting.schedule)
# Check assumptions about events overlapping midnight
self.assertEqual(daytime_timeslot.local_start_time().day,
daytime_timeslot.local_end_time().day,
'Daytime event should not overlap midnight in local time')
self.assertNotEqual(daytime_timeslot.utc_start_time().day,
daytime_timeslot.utc_end_time().day,
'Daytime event should overlap midnight in UTC')
self.assertNotEqual(overnight_timeslot.local_start_time().day,
overnight_timeslot.local_end_time().day,
'Overnight event should overlap midnight in local time')
self.assertEqual(overnight_timeslot.utc_start_time().day,
overnight_timeslot.utc_end_time().day,
'Overnight event should not overlap midnight in UTC')
self.login()
# Test in meeting local time
self.driver.get(self.absreverse('ietf.meeting.views.week_view'))
time_string = '-'.join([daytime_timeslot.local_start_time().strftime('%H%M'),
daytime_timeslot.local_end_time().strftime('%H%M')])
displayed = WebDriverWait(self.driver, 2).until(
expected_conditions.presence_of_all_elements_located(
(By.XPATH,
'//div/div[contains(text(), "%s")]/span[contains(text(), "%s")]' % (
time_string,
daytime_session.name))
)
)
_assert_not_wrapped(displayed, time_string)
time_string = '-'.join([overnight_timeslot.local_start_time().strftime('%H%M'),
overnight_timeslot.local_end_time().strftime('%H%M')])
displayed = WebDriverWait(self.driver, 2).until(
expected_conditions.presence_of_all_elements_located(
(By.XPATH,
'//div/div[contains(text(), "%s")]/span[contains(text(), "%s")]' % (
time_string,
overnight_session.name))
)
)
_assert_wrapped(displayed, time_string)
# Test in utc time
self.driver.get(self.absreverse('ietf.meeting.views.week_view') + '?tz=utc')
time_string = '-'.join([daytime_timeslot.utc_start_time().strftime('%H%M'),
daytime_timeslot.utc_end_time().strftime('%H%M')])
displayed = WebDriverWait(self.driver, 2).until(
expected_conditions.presence_of_all_elements_located(
(By.XPATH,
'//div/div[contains(text(), "%s")]/span[contains(text(), "%s")]' % (
time_string,
daytime_session.name))
)
)
_assert_wrapped(displayed, time_string)
time_string = '-'.join([overnight_timeslot.utc_start_time().strftime('%H%M'),
overnight_timeslot.utc_end_time().strftime('%H%M')])
displayed = WebDriverWait(self.driver, 2).until(
expected_conditions.presence_of_all_elements_located(
(By.XPATH,
'//div/div[contains(text(), "%s")]/span[contains(text(), "%s")]' % (
time_string,
overnight_session.name))
)
)
_assert_not_wrapped(displayed, time_string)
@skipIf(skip_selenium, skip_message) @skipIf(skip_selenium, skip_message)
class InterimTests(MeetingTestCase): class InterimTests(MeetingTestCase):
def setUp(self): def setUp(self):

View file

@ -360,11 +360,17 @@ class MeetingTests(TestCase):
def test_agenda_week_view(self): def test_agenda_week_view(self):
meeting = make_meeting_test_data() meeting = make_meeting_test_data()
url = urlreverse("ietf.meeting.views.week_view",kwargs=dict(num=meeting.number)) + "#farfut" url = urlreverse("ietf.meeting.views.week_view",kwargs=dict(num=meeting.number)) + "?show=farfut"
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code,200) self.assertEqual(r.status_code,200)
self.assertTrue(all([x in unicontent(r) for x in ['var all_items', 'maximize', 'draw_calendar', ]])) self.assertTrue(all([x in unicontent(r) for x in ['var all_items', 'maximize', 'draw_calendar', ]]))
# Specifying a time zone should not change the output (time zones are handled by the JS)
url = urlreverse("ietf.meeting.views.week_view",kwargs=dict(num=meeting.number)) + "?show=farfut&tz=Asia/Bangkok"
r_with_tz = self.client.get(url)
self.assertEqual(r_with_tz.status_code,200)
self.assertEqual(r.content, r_with_tz.content)
@override_settings(MEETING_MATERIALS_SERVE_LOCALLY=False, MEETING_DOC_HREFS = settings.MEETING_DOC_CDN_HREFS) @override_settings(MEETING_MATERIALS_SERVE_LOCALLY=False, MEETING_DOC_HREFS = settings.MEETING_DOC_CDN_HREFS)
def test_materials_through_cdn(self): def test_materials_through_cdn(self):
meeting = make_meeting_test_data(create_interims=True) meeting = make_meeting_test_data(create_interims=True)
@ -686,9 +692,6 @@ class MeetingTests(TestCase):
self.assertIsNone(parse_agenda_filter_params(QueryDict(''))) self.assertIsNone(parse_agenda_filter_params(QueryDict('')))
self.assertRaises(ValueError, parse_agenda_filter_params, QueryDict('unknown')) # unknown param
self.assertRaises(ValueError, parse_agenda_filter_params, QueryDict('unknown=x')) # unknown param
# test valid combos (not exhaustive) # test valid combos (not exhaustive)
for qstr, expected in ( for qstr, expected in (
('show=', _r()), ('hide=', _r()), ('showtypes=', _r()), ('hidetypes=', _r()), ('show=', _r()), ('hide=', _r()), ('showtypes=', _r()), ('hidetypes=', _r()),
@ -708,16 +711,6 @@ class MeetingTests(TestCase):
'Parsed "%s" incorrectly' % qstr, 'Parsed "%s" incorrectly' % qstr,
) )
def test_ical_filter_invalid_syntaxes(self):
meeting = make_meeting_test_data()
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number})
r = self.client.get(url + '?unknownparam=mars')
self.assertEqual(r.status_code, 400, 'Unknown parameter should be rejected')
r = self.client.get(url + '?mars')
self.assertEqual(r.status_code, 400, 'Missing parameter name should be rejected')
def do_ical_filter_test(self, meeting, querystring, expected_session_summaries): def do_ical_filter_test(self, meeting, querystring, expected_session_summaries):
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number}) url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number})
r = self.client.get(url + querystring) r = self.client.get(url + querystring)
@ -2328,16 +2321,6 @@ class InterimTests(TestCase):
expected_event_count=2) expected_event_count=2)
def test_upcoming_ical_filter_invalid_syntaxes(self):
make_meeting_test_data()
url = urlreverse('ietf.meeting.views.upcoming_ical')
r = self.client.get(url + '?unknownparam=mars')
self.assertEqual(r.status_code, 400, 'Unknown parameter should be rejected')
r = self.client.get(url + '?mars')
self.assertEqual(r.status_code, 400, 'Missing parameter name should be rejected')
def test_upcoming_json(self): def test_upcoming_json(self):
make_meeting_test_data(create_interims=True) make_meeting_test_data(create_interims=True)
url = urlreverse("ietf.meeting.views.upcoming_json") url = urlreverse("ietf.meeting.views.upcoming_json")

View file

@ -100,7 +100,6 @@ from ietf.utils.pipe import pipe
from ietf.utils.pdf import pdf_pages from ietf.utils.pdf import pdf_pages
from ietf.utils.response import permission_denied from ietf.utils.response import permission_denied
from ietf.utils.text import xslugify from ietf.utils.text import xslugify
from ietf.utils.timezone import date2datetime
from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm, from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm,
InterimCancelForm, InterimSessionInlineFormSet, FileUploadForm, RequestMinutesForm,) InterimCancelForm, InterimSessionInlineFormSet, FileUploadForm, RequestMinutesForm,)
@ -1719,13 +1718,6 @@ def week_view(request, num=None, name=None, owner=None):
schedule__in=[schedule, schedule.base], schedule__in=[schedule, schedule.base],
timeslot__type__private=False, timeslot__type__private=False,
) )
# Only show assignments from the traditional meeting "week" (Sat-Fri).
# We'll determine this using the saturday before the first scheduled regular session.
first_regular_session = meeting.schedule.qs_assignments_with_sessions.filter(session__type_id='regular').order_by('timeslot__time').first()
first_regular_session_time = first_regular_session.timeslot.time if first_regular_session else date2datetime(meeting.date)
saturday_before = first_regular_session_time.replace(hour=0, minute=0, second=0, microsecond=0) - datetime.timedelta(days=(first_regular_session_time.weekday() - 5)%7)
# saturday_after = saturday_before + datetime.timedelta(days=7)
# filtered_assignments = filtered_assignments.filter(timeslot__time__gte=saturday_before,timeslot__time__lt=saturday_after)
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting) filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
tag_assignments_with_filter_keywords(filtered_assignments) tag_assignments_with_filter_keywords(filtered_assignments)
@ -1734,16 +1726,8 @@ def week_view(request, num=None, name=None, owner=None):
# we don't HTML escape any of these as the week-view code is using createTextNode # we don't HTML escape any of these as the week-view code is using createTextNode
item = { item = {
"key": str(a.timeslot.pk), "key": str(a.timeslot.pk),
"day": (a.timeslot.time - saturday_before).days - 1, "utc_time": a.timeslot.utc_start_time().strftime("%Y%m%dT%H%MZ"), # ISO8601 compliant
"time": a.timeslot.time.strftime("%H%M") + "-" + a.timeslot.end_time().strftime("%H%M"),
"duration": a.timeslot.duration.seconds, "duration": a.timeslot.duration.seconds,
"time_id": a.timeslot.time.strftime("%m%d%H%M"),
"dayname": "{weekday}, {month} {day_of_month}, {year}".format(
weekday=a.timeslot.time.strftime("%A").upper(),
month=a.timeslot.time.strftime("%B"),
day_of_month=a.timeslot.time.strftime("%d").lstrip("0"),
year=a.timeslot.time.strftime("%Y"),
),
"type": a.timeslot.type.name, "type": a.timeslot.type.name,
"filter_keywords": ",".join(a.filter_keywords), "filter_keywords": ",".join(a.filter_keywords),
} }
@ -1780,6 +1764,7 @@ def week_view(request, num=None, name=None, owner=None):
return render(request, "meeting/week-view.html", { return render(request, "meeting/week-view.html", {
"items": json.dumps(items), "items": json.dumps(items),
"timezone": meeting.time_zone,
}) })
@role_required('Area Director','Secretariat','IAB') @role_required('Area Director','Secretariat','IAB')
@ -1866,20 +1851,14 @@ def parse_agenda_filter_params(querydict):
if len(querydict) == 0: if len(querydict) == 0:
return None return None
# Parse group filters from GET parameters. The keys in this dict define the # Parse group filters from GET parameters. Other params are ignored.
# allowed querystring parameters.
filt_params = {'show': set(), 'hide': set(), 'showtypes': set(), 'hidetypes': set()} filt_params = {'show': set(), 'hide': set(), 'showtypes': set(), 'hidetypes': set()}
for key, value in querydict.items(): for key, value in querydict.items():
if key not in filt_params: if key in filt_params:
raise ValueError('Unrecognized parameter "%s"' % key) vals = unquote(value).lower().split(',')
if value is None: vals = [v.strip() for v in vals]
return ValueError( filt_params[key] = set([v for v in vals if len(v) > 0]) # remove empty strings
'Parameter "%s" is not assigned a value (use "key=" for an empty value)' % key
)
vals = unquote(value).lower().split(',')
vals = [v.strip() for v in vals]
filt_params[key] = set([v for v in vals if len(v) > 0]) # remove empty strings
return filt_params return filt_params

View file

@ -370,13 +370,21 @@
// Filtering is enabled // Filtering is enabled
weekview.removeClass("hidden"); weekview.removeClass("hidden");
var wv_iframe = document.getElementById('weekview'); var wv_iframe = document.getElementById('weekview');
var wv_window = wv_iframe.contentWindow; var wv_window = wv_iframe.contentWindow;
var new_url = 'week-view.html' + window.location.search; var queryparams = window.location.search;
{% if "-utc" in request.path %}
if (queryparams) {
queryparams += '&tz=utc';
} else {
queryparams = '?tz=utc';
}
{% endif %}
var new_url = 'week-view.html' + queryparams;
if (wv_iframe.src && wv_window.history && wv_window.history.replaceState) { if (wv_iframe.src && wv_window.history && wv_window.history.replaceState) {
wv_window.history.replaceState({}, '', new_url); wv_window.history.replaceState({}, '', new_url);
wv_window.draw_calendar() wv_window.redraw_weekview();
} else { } else {
// either have not yet loaded the iframe or we do not support history replacement // either have not yet loaded the iframe or we do not support history replacement
wv_iframe.src = new_url; wv_iframe.src = new_url;

View file

@ -3,17 +3,12 @@
{% load static %} {% load static %}
<html> <head> <html> <head>
<script src="{% static 'ietf/js/agenda/agenda_filter.js' %}"></script> <script src="{% static 'ietf/js/agenda/agenda_filter.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 type="text/javascript"> <script type="text/javascript">
var all_items = {{ items|safe }}; var all_items = {{ items|safe }};
/* Also, process clock times to "minutes past midnight" */
all_items.forEach(function(item) {
item.start_time = parseInt(item.time.substr(0,2),10) * 60 +
parseInt(item.time.substr(2,2),10);
item.end_time = item.start_time + (item.duration / 60)
});
var color = { var color = {
'app': { fg: "#008", bg: "#eef"}, 'app': { fg: "#008", bg: "#eef"},
'art': { fg: "#808", bg: "#fef"}, 'art': { fg: "#808", bg: "#fef"},
@ -123,15 +118,11 @@
//=========================================================================== //===========================================================================
function draw_calendar() { function draw_calendar(items, filter_params) {
var width = document.body.clientWidth; var width = document.body.clientWidth;
var height = document.body.clientHeight; var height = document.body.clientHeight;
var visible_items = all_items; var visible_items = items;
var filter_params = agenda_filter.get_filter_params(
agenda_filter.parse_query_params(window.location.search)
);
if (agenda_filter.filtering_is_enabled(filter_params)) { if (agenda_filter.filtering_is_enabled(filter_params)) {
visible_items = visible_items.filter(is_visible(filter_params)); visible_items = visible_items.filter(is_visible(filter_params));
} }
@ -143,8 +134,8 @@
day_start = visible_items[0].start_time; day_start = visible_items[0].start_time;
} else { } else {
// fallback in case all items were filtered // fallback in case all items were filtered
start_day = all_items[0].day; start_day = items[0].day;
day_start = all_items[0].start_time; day_start = items[0].start_time;
} }
var end_day = start_day; var end_day = start_day;
var day_end = 0; var day_end = 0;
@ -496,10 +487,124 @@
e.style.fontSize="8pt"; e.style.fontSize="8pt";
} }
//===========================================================================
//
function get_first_item(items, type) {
var earliest;
for (var ii=0; ii < items.length; ii++) {
var this_item = items[ii];
if (type && (this_item.type !== type)) {
continue;
}
// Update earliest if we don't have an earliest item yet or this_item is earlier
if (!earliest || (items[ii].utc_time < earliest.utc_time)) {
earliest = items[ii];
}
}
return earliest;
}
//===========================================================================
//
function prepare_items(items, timezone_name) {
function make_display_item(item) {
return {
name: item.name,
group: item.group,
area: item.area,
room: item.room,
agenda: item.agenda,
key: item.key,
type: item.type,
filter_keywords: item.filter_keywords
}
};
/* Ported from Django view, which had the following comment:
* Only show assignments from the traditional meeting "week" (Sat-Fri).
* We'll determine this using the saturday before the first scheduled regular session. */
var first_session = get_first_item(items, 'Regular');
if (!first_session) {
first_session = get_first_item(items); // any type
}
var first_session_time = moment(first_session.utc_time).utc();
if (timezone_name) {
first_session_time.tz(timezone_name); // mutates the moment
}
// Moment.js day() uses 0 == Sunday, 6 == Saturday
days_since_saturday = first_session_time.day() - 6;
if (days_since_saturday < 0) {
days_since_saturday += 7;
}
saturday_before = first_session_time.clone().startOf('day').subtract(days_since_saturday, 'days');
var display_items = [];
for (var ii = 0; ii < items.length; ii++) {
var this_item = items[ii];
/* It's possible an event overlaps the moment of a daylight savings shift.
* Calculate the end_moment in utc() time, which has no DST. Once we switch
* to a time zone, end time minus start time may not equal duration. */
var start_moment = moment(this_item.utc_time).utc();
var end_moment = start_moment.clone().add(this_item.duration, 'seconds');
if (timezone_name) {
start_moment.tz(timezone_name);
end_moment.tz(timezone_name);
}
// Avoid off-by-one day number calculations if a session ends at midnight
var just_before_end_moment = end_moment.clone().subtract(1, 'millisecond');
var start_day = start_moment.diff(saturday_before, 'days') - 1; // shift so sunday = 0
var end_day = just_before_end_moment.diff(saturday_before, 'days') - 1; // shift so sunday = 0
// Generate display items - create multiple if item ends on different day than starts
for (var day = start_day; day <= end_day; day++) {
var display_item = make_display_item(this_item);
display_item.day = day;
if (day === start_day) {
// First day of session - compute start time
display_item.start_time = start_moment.diff(
start_moment.clone().startOf('day'),
'minutes'
);
} else {
// Not first day, start at midnight
display_item.start_time = 0;
display_item.name += " - continued";
}
if (day === end_day) {
// Last day of session - compute end time
display_item.end_time = end_moment.diff(
just_before_end_moment.clone().startOf('day'),
'minutes'
);
} else {
/* Not last day, use full day. Calculate this on the fly to account for
* daylight savings shifts, when a calendar day is not 24*60 minutes long. */
display_item.end_time = just_before_end_moment.clone().endOf('day').diff(
just_before_end_moment.clone().startOf('day'),
'minutes'
);
}
display_item.time = start_moment.format('HHmm') + '-' + end_moment.format('HHmm');
display_item.dayname = start_moment.format('dddd, ').toUpperCase() +
start_moment.format('MMMM D, Y');
display_items.push(display_item);
}
}
return display_items;
}
//=========================================================================== //===========================================================================
// Set up events for drawing the calendar // Set up events for drawing the calendar
function redraw_weekview() { function redraw_weekview() {
draw_calendar(); var query_params = agenda_filter.parse_query_params(window.location.search);
var timezone_name = query_params.tz || '{{ timezone }}';
items = prepare_items(all_items, timezone_name);
draw_calendar(items, agenda_filter.get_filter_params(query_params));
} }
window.addEventListener("resize", redraw_weekview, false); window.addEventListener("resize", redraw_weekview, false);