Skip to content

Commit 159b8fe

Browse files
committed
Merged in [18712] from jennifer@painless-security.com:
Add timezone support to agenda weekview; display UTC on UTC agenda page. Fixes ietf-tools#3111. - Legacy-Id: 18796 Note: SVN reference [18712] has been migrated to Git commit d29553c
2 parents 22de976 + d29553c commit 159b8fe

9 files changed

Lines changed: 354 additions & 72 deletions

File tree

hold-for-merge

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# -*- conf-mode -*-
22

3+
/personal/kivinen/7.22.1.dev0@18689 # Hold for revision based on timezone-aware code
4+
35
/personal/rcross/7.19.1.dev0@18663
46
/personal/rcross/7.19.1.dev0@18662
57

ietf/bower.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
"js-cookie": "~2",
1414
"jquery": "~1",
1515
"jquery.tablesorter": "~2",
16+
"moment": "~2",
17+
"moment-timezone": "~0",
1618
"respond": "~1",
1719
"select2": "~3",
1820
"select2-bootstrap-css": "~1",
@@ -33,6 +35,11 @@
3335
"./fonts/*"
3436
]
3537
},
38+
"moment": {
39+
"main": [
40+
"min/moment.min.js"
41+
]
42+
},
3643
"tablesorter": {
3744
"main": [
3845
"dist/js/jquery.tablesorter.combined.min.js",

ietf/externals/static/moment-timezone/builds/moment-timezone-with-data-10-year-range.min.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ietf/externals/static/moment/min/moment.min.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ietf/meeting/tests_js.py

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from django.urls import reverse as urlreverse
1414
from django.utils.text import slugify
1515
from django.db.models import F
16+
from pytz import timezone
1617
#from django.test.utils import override_settings
1718

1819
import debug # pyflakes:ignore
@@ -23,7 +24,7 @@
2324
from ietf.person.models import Person
2425
from ietf.group.models import Group
2526
from ietf.group.factories import GroupFactory
26-
from ietf.meeting.factories import SessionFactory
27+
from ietf.meeting.factories import SessionFactory, TimeSlotFactory
2728
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting
2829
from ietf.meeting.models import (Schedule, SchedTimeSessAssignment, Session,
2930
Room, TimeSlot, Constraint, ConstraintName,
@@ -904,6 +905,200 @@ def test_session_materials_modal(self):
904905
self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title)
905906

906907

908+
@skipIf(skip_selenium, skip_message)
909+
class WeekviewTests(MeetingTestCase):
910+
def setUp(self):
911+
super(WeekviewTests, self).setUp()
912+
self.meeting = make_meeting_test_data()
913+
914+
def get_expected_items(self):
915+
expected_items = self.meeting.schedule.assignments.exclude(timeslot__type__in=['lead', 'offagenda'])
916+
self.assertGreater(len(expected_items), 0, 'Test setup generated an empty schedule')
917+
return expected_items
918+
919+
def test_timezone_default(self):
920+
"""Week view should show local times by default"""
921+
self.assertNotEqual(self.meeting.time_zone.lower(), 'utc',
922+
'Cannot test local time weekview because meeting is using UTC time.')
923+
self.login()
924+
self.driver.get(self.absreverse('ietf.meeting.views.week_view'))
925+
for item in self.get_expected_items():
926+
if item.session.name:
927+
expected_name = item.session.name
928+
elif item.timeslot.type_id == 'break':
929+
expected_name = item.timeslot.name
930+
else:
931+
expected_name = item.session.group.name
932+
expected_time = '-'.join([item.timeslot.local_start_time().strftime('%H%M'),
933+
item.timeslot.local_end_time().strftime('%H%M')])
934+
WebDriverWait(self.driver, 2).until(
935+
expected_conditions.presence_of_element_located(
936+
(By.XPATH,
937+
'//div/div[contains(text(), "%s")]/span[contains(text(), "%s")]' % (
938+
expected_time, expected_name))
939+
)
940+
)
941+
942+
def test_timezone_selection(self):
943+
"""Week view should show time zones when requested"""
944+
# Must test utc; others are picked arbitrarily
945+
zones_to_test = ['utc', 'America/Halifax', 'Asia/Bangkok', 'Africa/Dakar', 'Europe/Dublin']
946+
self.login()
947+
for zone_name in zones_to_test:
948+
zone = timezone(zone_name)
949+
self.driver.get(self.absreverse('ietf.meeting.views.week_view') + '?tz=' + zone_name)
950+
for item in self.get_expected_items():
951+
if item.session.name:
952+
expected_name = item.session.name
953+
elif item.timeslot.type_id == 'break':
954+
expected_name = item.timeslot.name
955+
else:
956+
expected_name = item.session.group.name
957+
958+
start_time = item.timeslot.utc_start_time().astimezone(zone)
959+
end_time = item.timeslot.utc_end_time().astimezone(zone)
960+
expected_time = '-'.join([start_time.strftime('%H%M'),
961+
end_time.strftime('%H%M')])
962+
963+
WebDriverWait(self.driver, 2).until(
964+
expected_conditions.presence_of_element_located(
965+
(By.XPATH,
966+
'//div/div[contains(text(), "%s")]/span[contains(text(), "%s")]' % (
967+
expected_time, expected_name))
968+
),
969+
'Could not find event "%s" at %s for time zone %s' % (expected_name,
970+
expected_time,
971+
zone_name),
972+
)
973+
974+
def test_event_wrapping(self):
975+
"""Events that overlap midnight should be shown on both days
976+
977+
This assumes that the meeting is in America/New_York timezone.
978+
"""
979+
def _assert_wrapped(displayed, expected_time_string):
980+
self.assertEqual(len(displayed), 2)
981+
first = displayed[0]
982+
first_parent = first.find_element_by_xpath('..')
983+
second = displayed[1]
984+
second_parent = second.find_element_by_xpath('..')
985+
self.assertNotIn('continued', first.text)
986+
self.assertIn(expected_time_string, first_parent.text)
987+
self.assertIn('continued', second.text)
988+
self.assertIn(expected_time_string, second_parent.text)
989+
990+
def _assert_not_wrapped(displayed, expected_time_string):
991+
self.assertEqual(len(displayed), 1)
992+
first = displayed[0]
993+
first_parent = first.find_element_by_xpath('..')
994+
self.assertNotIn('continued', first.text)
995+
self.assertIn(expected_time_string, first_parent.text)
996+
997+
duration = datetime.timedelta(minutes=120) # minutes
998+
999+
# Session during a single day in meeting local time but multi-day UTC
1000+
# Compute a time that overlaps midnight, UTC, but won't when shifted to a local time zone
1001+
start_time_utc = timezone('UTC').localize(
1002+
datetime.datetime.combine(self.meeting.date, datetime.time(23,0))
1003+
)
1004+
start_time_local = start_time_utc.astimezone(timezone(self.meeting.time_zone))
1005+
1006+
daytime_session = SessionFactory(
1007+
meeting=self.meeting,
1008+
name='Single Day Session for Wrapping Test',
1009+
add_to_schedule=False,
1010+
)
1011+
daytime_timeslot = TimeSlotFactory(
1012+
meeting=self.meeting,
1013+
time=start_time_local.replace(tzinfo=None), # drop timezone for Django
1014+
duration=duration,
1015+
)
1016+
daytime_session.timeslotassignments.create(timeslot=daytime_timeslot, schedule=self.meeting.schedule)
1017+
1018+
# Session that overlaps midnight in meeting local time
1019+
overnight_session = SessionFactory(
1020+
meeting=self.meeting,
1021+
name='Overnight Session for Wrapping Test',
1022+
add_to_schedule=False,
1023+
)
1024+
overnight_timeslot = TimeSlotFactory(
1025+
meeting=self.meeting,
1026+
time=datetime.datetime.combine(self.meeting.date, datetime.time(23,0)),
1027+
duration=duration,
1028+
)
1029+
overnight_session.timeslotassignments.create(timeslot=overnight_timeslot, schedule=self.meeting.schedule)
1030+
1031+
# Check assumptions about events overlapping midnight
1032+
self.assertEqual(daytime_timeslot.local_start_time().day,
1033+
daytime_timeslot.local_end_time().day,
1034+
'Daytime event should not overlap midnight in local time')
1035+
self.assertNotEqual(daytime_timeslot.utc_start_time().day,
1036+
daytime_timeslot.utc_end_time().day,
1037+
'Daytime event should overlap midnight in UTC')
1038+
1039+
self.assertNotEqual(overnight_timeslot.local_start_time().day,
1040+
overnight_timeslot.local_end_time().day,
1041+
'Overnight event should overlap midnight in local time')
1042+
self.assertEqual(overnight_timeslot.utc_start_time().day,
1043+
overnight_timeslot.utc_end_time().day,
1044+
'Overnight event should not overlap midnight in UTC')
1045+
1046+
self.login()
1047+
1048+
# Test in meeting local time
1049+
self.driver.get(self.absreverse('ietf.meeting.views.week_view'))
1050+
1051+
time_string = '-'.join([daytime_timeslot.local_start_time().strftime('%H%M'),
1052+
daytime_timeslot.local_end_time().strftime('%H%M')])
1053+
displayed = WebDriverWait(self.driver, 2).until(
1054+
expected_conditions.presence_of_all_elements_located(
1055+
(By.XPATH,
1056+
'//div/div[contains(text(), "%s")]/span[contains(text(), "%s")]' % (
1057+
time_string,
1058+
daytime_session.name))
1059+
)
1060+
)
1061+
_assert_not_wrapped(displayed, time_string)
1062+
1063+
time_string = '-'.join([overnight_timeslot.local_start_time().strftime('%H%M'),
1064+
overnight_timeslot.local_end_time().strftime('%H%M')])
1065+
displayed = WebDriverWait(self.driver, 2).until(
1066+
expected_conditions.presence_of_all_elements_located(
1067+
(By.XPATH,
1068+
'//div/div[contains(text(), "%s")]/span[contains(text(), "%s")]' % (
1069+
time_string,
1070+
overnight_session.name))
1071+
)
1072+
)
1073+
_assert_wrapped(displayed, time_string)
1074+
1075+
# Test in utc time
1076+
self.driver.get(self.absreverse('ietf.meeting.views.week_view') + '?tz=utc')
1077+
1078+
time_string = '-'.join([daytime_timeslot.utc_start_time().strftime('%H%M'),
1079+
daytime_timeslot.utc_end_time().strftime('%H%M')])
1080+
displayed = WebDriverWait(self.driver, 2).until(
1081+
expected_conditions.presence_of_all_elements_located(
1082+
(By.XPATH,
1083+
'//div/div[contains(text(), "%s")]/span[contains(text(), "%s")]' % (
1084+
time_string,
1085+
daytime_session.name))
1086+
)
1087+
)
1088+
_assert_wrapped(displayed, time_string)
1089+
1090+
time_string = '-'.join([overnight_timeslot.utc_start_time().strftime('%H%M'),
1091+
overnight_timeslot.utc_end_time().strftime('%H%M')])
1092+
displayed = WebDriverWait(self.driver, 2).until(
1093+
expected_conditions.presence_of_all_elements_located(
1094+
(By.XPATH,
1095+
'//div/div[contains(text(), "%s")]/span[contains(text(), "%s")]' % (
1096+
time_string,
1097+
overnight_session.name))
1098+
)
1099+
)
1100+
_assert_not_wrapped(displayed, time_string)
1101+
9071102
@skipIf(skip_selenium, skip_message)
9081103
class InterimTests(MeetingTestCase):
9091104
def setUp(self):

ietf/meeting/tests_views.py

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -360,11 +360,17 @@ def test_agenda_room_view(self):
360360

361361
def test_agenda_week_view(self):
362362
meeting = make_meeting_test_data()
363-
url = urlreverse("ietf.meeting.views.week_view",kwargs=dict(num=meeting.number)) + "#farfut"
363+
url = urlreverse("ietf.meeting.views.week_view",kwargs=dict(num=meeting.number)) + "?show=farfut"
364364
r = self.client.get(url)
365365
self.assertEqual(r.status_code,200)
366366
self.assertTrue(all([x in unicontent(r) for x in ['var all_items', 'maximize', 'draw_calendar', ]]))
367367

368+
# Specifying a time zone should not change the output (time zones are handled by the JS)
369+
url = urlreverse("ietf.meeting.views.week_view",kwargs=dict(num=meeting.number)) + "?show=farfut&tz=Asia/Bangkok"
370+
r_with_tz = self.client.get(url)
371+
self.assertEqual(r_with_tz.status_code,200)
372+
self.assertEqual(r.content, r_with_tz.content)
373+
368374
@override_settings(MEETING_MATERIALS_SERVE_LOCALLY=False, MEETING_DOC_HREFS = settings.MEETING_DOC_CDN_HREFS)
369375
def test_materials_through_cdn(self):
370376
meeting = make_meeting_test_data(create_interims=True)
@@ -686,9 +692,6 @@ def _r(show=(), hide=(), showtypes=(), hidetypes=()):
686692

687693
self.assertIsNone(parse_agenda_filter_params(QueryDict('')))
688694

689-
self.assertRaises(ValueError, parse_agenda_filter_params, QueryDict('unknown')) # unknown param
690-
self.assertRaises(ValueError, parse_agenda_filter_params, QueryDict('unknown=x')) # unknown param
691-
692695
# test valid combos (not exhaustive)
693696
for qstr, expected in (
694697
('show=', _r()), ('hide=', _r()), ('showtypes=', _r()), ('hidetypes=', _r()),
@@ -708,16 +711,6 @@ def _r(show=(), hide=(), showtypes=(), hidetypes=()):
708711
'Parsed "%s" incorrectly' % qstr,
709712
)
710713

711-
def test_ical_filter_invalid_syntaxes(self):
712-
meeting = make_meeting_test_data()
713-
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number})
714-
715-
r = self.client.get(url + '?unknownparam=mars')
716-
self.assertEqual(r.status_code, 400, 'Unknown parameter should be rejected')
717-
718-
r = self.client.get(url + '?mars')
719-
self.assertEqual(r.status_code, 400, 'Missing parameter name should be rejected')
720-
721714
def do_ical_filter_test(self, meeting, querystring, expected_session_summaries):
722715
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number})
723716
r = self.client.get(url + querystring)
@@ -2328,16 +2321,6 @@ def test_upcoming_ical_filter(self):
23282321
expected_event_count=2)
23292322

23302323

2331-
def test_upcoming_ical_filter_invalid_syntaxes(self):
2332-
make_meeting_test_data()
2333-
url = urlreverse('ietf.meeting.views.upcoming_ical')
2334-
2335-
r = self.client.get(url + '?unknownparam=mars')
2336-
self.assertEqual(r.status_code, 400, 'Unknown parameter should be rejected')
2337-
2338-
r = self.client.get(url + '?mars')
2339-
self.assertEqual(r.status_code, 400, 'Missing parameter name should be rejected')
2340-
23412324
def test_upcoming_json(self):
23422325
make_meeting_test_data(create_interims=True)
23432326
url = urlreverse("ietf.meeting.views.upcoming_json")

ietf/meeting/views.py

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@
100100
from ietf.utils.pdf import pdf_pages
101101
from ietf.utils.response import permission_denied
102102
from ietf.utils.text import xslugify
103-
from ietf.utils.timezone import date2datetime
104103

105104
from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm,
106105
InterimCancelForm, InterimSessionInlineFormSet, FileUploadForm, RequestMinutesForm,)
@@ -1719,13 +1718,6 @@ def week_view(request, num=None, name=None, owner=None):
17191718
schedule__in=[schedule, schedule.base],
17201719
timeslot__type__private=False,
17211720
)
1722-
# Only show assignments from the traditional meeting "week" (Sat-Fri).
1723-
# We'll determine this using the saturday before the first scheduled regular session.
1724-
first_regular_session = meeting.schedule.qs_assignments_with_sessions.filter(session__type_id='regular').order_by('timeslot__time').first()
1725-
first_regular_session_time = first_regular_session.timeslot.time if first_regular_session else date2datetime(meeting.date)
1726-
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)
1727-
# saturday_after = saturday_before + datetime.timedelta(days=7)
1728-
# filtered_assignments = filtered_assignments.filter(timeslot__time__gte=saturday_before,timeslot__time__lt=saturday_after)
17291721
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
17301722
tag_assignments_with_filter_keywords(filtered_assignments)
17311723

@@ -1734,16 +1726,8 @@ def week_view(request, num=None, name=None, owner=None):
17341726
# we don't HTML escape any of these as the week-view code is using createTextNode
17351727
item = {
17361728
"key": str(a.timeslot.pk),
1737-
"day": (a.timeslot.time - saturday_before).days - 1,
1738-
"time": a.timeslot.time.strftime("%H%M") + "-" + a.timeslot.end_time().strftime("%H%M"),
1729+
"utc_time": a.timeslot.utc_start_time().strftime("%Y%m%dT%H%MZ"), # ISO8601 compliant
17391730
"duration": a.timeslot.duration.seconds,
1740-
"time_id": a.timeslot.time.strftime("%m%d%H%M"),
1741-
"dayname": "{weekday}, {month} {day_of_month}, {year}".format(
1742-
weekday=a.timeslot.time.strftime("%A").upper(),
1743-
month=a.timeslot.time.strftime("%B"),
1744-
day_of_month=a.timeslot.time.strftime("%d").lstrip("0"),
1745-
year=a.timeslot.time.strftime("%Y"),
1746-
),
17471731
"type": a.timeslot.type.name,
17481732
"filter_keywords": ",".join(a.filter_keywords),
17491733
}
@@ -1780,6 +1764,7 @@ def week_view(request, num=None, name=None, owner=None):
17801764

17811765
return render(request, "meeting/week-view.html", {
17821766
"items": json.dumps(items),
1767+
"timezone": meeting.time_zone,
17831768
})
17841769

17851770
@role_required('Area Director','Secretariat','IAB')
@@ -1866,20 +1851,14 @@ def parse_agenda_filter_params(querydict):
18661851
if len(querydict) == 0:
18671852
return None
18681853

1869-
# Parse group filters from GET parameters. The keys in this dict define the
1870-
# allowed querystring parameters.
1854+
# Parse group filters from GET parameters. Other params are ignored.
18711855
filt_params = {'show': set(), 'hide': set(), 'showtypes': set(), 'hidetypes': set()}
18721856

18731857
for key, value in querydict.items():
1874-
if key not in filt_params:
1875-
raise ValueError('Unrecognized parameter "%s"' % key)
1876-
if value is None:
1877-
return ValueError(
1878-
'Parameter "%s" is not assigned a value (use "key=" for an empty value)' % key
1879-
)
1880-
vals = unquote(value).lower().split(',')
1881-
vals = [v.strip() for v in vals]
1882-
filt_params[key] = set([v for v in vals if len(v) > 0]) # remove empty strings
1858+
if key in filt_params:
1859+
vals = unquote(value).lower().split(',')
1860+
vals = [v.strip() for v in vals]
1861+
filt_params[key] = set([v for v in vals if len(v) > 0]) # remove empty strings
18831862

18841863
return filt_params
18851864

0 commit comments

Comments
 (0)