From 8b52d27b02cf4e7e34efbaa5e0d640b27cc40f68 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Fri, 26 Aug 2022 16:53:19 -0300 Subject: [PATCH] refactor: refactor timestamp handling so tests in meeting app pass (#4371) * refactor: replace datetime.now with timezone.now * refactor: migrate model fields to use timezone.now as default * refactor: replace datetime.today with timezone.now datetime.datetime.today() is equivalent to datetime.datetime.now(); both return a naive datetime with the current local time. * refactor: rephrase datetime.now(tz) as timezone.now().astimezone(tz) This is effectively the same, but is less likely to encourage accidental use of naive datetimes. * refactor: revert datetime.today() change to old migrations * refactor: change a missed datetime.now to timezone.now * chore: renumber timezone_now migration * chore: add migration to change timestamps to UTC * refactor: move tz instantiation/caching from TimeSlot to Meeting * fix: assume utc if meeting.time_zone is blank * chore: make datetime.combine() calls tz aware in the meeting app * ci: correctly use meeting.tz in TimeSlotFactory * chore: compute TimeSlot utc / local times assuming tz-aware times * chore: use tzaware math for agenda editor timeslot layout * chore: fill in Meeting.time_zone where it is blank Nearly all interim meetings on or before 2016-07-01 have blank time_zone values. This migration fills these in with PST8PDT. * chore: disallow blank Meeting.time_zone value * refactor: no need to handle blank time_zone case in TZ migration * refactor: remove now-unnecessary checks that meeting has time_zone * chore: fix timezone handling in agenda.ics and Meeting.updated() * chore: fix tz handling in interim_request_details, exercise in tests * chore: fix timezone handling for test_interim_send_announcement * chore: fix timezone handling in agenda_json() * chore: fix timezone handling in old agenda * chore: fix timezone handling for EditTimeslotsTests * refactor: refactor a few fixes for more consistent timezone handling * chore: add timezone info to timestamps in fixtures * chore: remove naive datetime warnings found in meetings.tests_views * chore: fix a few more test failures in meetings.tests_views All tests in meetings.tests_views now passing * chore: remove unused import * chore: fix timezone handling in test_schedule_generator.py * chore: fix timezone handling affecting meeting.tests_js * chore: fix timeslot test bug when local date != UTC date * test: fix a few failing tests, all meetings tests now pass (for me, anyway) * chore: renumber migrations * chore: update timestamp conversion migration The django-celery-beat package introduces tables with timestamp columns. These columns are stored in CELERY_TIMEZONE. Because we run with this set to UTC, the migration ignores these columns. * chore: fix pytz-related change in migration * chore: remove duplicate migrations * chore: remove CELERY_BEAT_TZ_AWARE setting now that USE_TZ is True * test: avoid failure in test with bogus timezone --- ietf/group/models.py | 11 +- ietf/meeting/factories.py | 4 +- .../fixtures/proceedings_templates.json | 2 +- ietf/meeting/helpers.py | 21 +- .../commands/create_dummy_meeting.py | 24 +- ietf/meeting/models.py | 43 ++-- ietf/meeting/test_data.py | 43 ++-- ietf/meeting/tests_js.py | 48 ++-- ietf/meeting/tests_models.py | 6 +- ietf/meeting/tests_schedule_generator.py | 14 +- ietf/meeting/tests_views.py | 200 ++++++++++------ ietf/meeting/utils.py | 20 +- ietf/meeting/views.py | 226 ++++++++++-------- ietf/name/fixtures/names.json | 8 +- ietf/secr/proceedings/proc_utils.py | 20 +- ietf/settings.py | 4 +- ietf/templates/meeting/agenda.html | 19 +- ietf/templates/meeting/agenda.ics | 8 +- .../meeting/interim_announcement.txt | 7 +- .../meeting/interim_request_details.html | 8 +- .../templates/meeting/timeslot_start_end.html | 6 +- ietf/templates/meeting/upcoming.html | 12 +- ietf/utils/timezone.py | 34 ++- 23 files changed, 480 insertions(+), 308 deletions(-) diff --git a/ietf/group/models.py b/ietf/group/models.py index fd665e6b3..8183f35a0 100644 --- a/ietf/group/models.py +++ b/ietf/group/models.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- -import datetime import email.utils import jsonfield import os @@ -181,11 +180,15 @@ class Group(GroupInfo): return self.role_set.none() def status_for_meeting(self,meeting): - end_date = meeting.end_date()+datetime.timedelta(days=1) previous_meeting = meeting.previous_meeting() - status_events = self.groupevent_set.filter(type='status_update',time__lte=end_date).order_by('-time') + status_events = self.groupevent_set.filter( + type='status_update', + time__lt=meeting.end_datetime(), + ).order_by('-time') if previous_meeting: - status_events = status_events.filter(time__gte=previous_meeting.end_date()+datetime.timedelta(days=1)) + status_events = status_events.filter( + time__gte=previous_meeting.end_datetime() + ) return status_events.first() def get_description(self): diff --git a/ietf/meeting/factories.py b/ietf/meeting/factories.py index e59b559f4..cf3c87e7c 100644 --- a/ietf/meeting/factories.py +++ b/ietf/meeting/factories.py @@ -187,7 +187,9 @@ class TimeSlotFactory(factory.django.DjangoModelFactory): @factory.lazy_attribute def time(self): - return datetime.datetime.combine(self.meeting.date,datetime.time(11,0)) + return self.meeting.tz().localize( + datetime.datetime.combine(self.meeting.date, datetime.time(11, 0)) + ) @factory.lazy_attribute def duration(self): diff --git a/ietf/meeting/fixtures/proceedings_templates.json b/ietf/meeting/fixtures/proceedings_templates.json index 1594debff..97d38f566 100644 --- a/ietf/meeting/fixtures/proceedings_templates.json +++ b/ietf/meeting/fixtures/proceedings_templates.json @@ -32,7 +32,7 @@ "comments": "", "list_subscribe": "", "state": "active", - "time": "2012-02-26T00:21:36", + "time": "2012-02-26T00:21:36Z", "unused_tags": [], "list_archive": "", "type": "ietf", diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index 5f463b269..99be2dd89 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -118,7 +118,10 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe # assignments = list(assignments_queryset) # make sure we're set in stone assignments = assignments_queryset - meeting_time = datetime.datetime.combine(meeting.date, datetime.time()) + # meeting_time is meeting-local midnight at the start of the meeting date + meeting_time = meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time()) + ) # replace groups with historic counterparts groups = [ ] @@ -1149,11 +1152,15 @@ def sessions_post_cancel(request, sessions): def update_interim_session_assignment(form): - """Helper function to create / update timeslot assigned to interim session""" - time = datetime.datetime.combine( - form.cleaned_data['date'], - form.cleaned_data['time']) + """Helper function to create / update timeslot assigned to interim session + + form is an InterimSessionModelForm + """ session = form.instance + meeting = session.meeting + time = meeting.tz().localize( + datetime.datetime.combine(form.cleaned_data['date'], form.cleaned_data['time']) + ) if session.official_timeslotassignment(): slot = session.official_timeslotassignment().timeslot slot.time = time @@ -1161,14 +1168,14 @@ def update_interim_session_assignment(form): slot.save() else: slot = TimeSlot.objects.create( - meeting=session.meeting, + meeting=meeting, type_id='regular', duration=session.requested_duration, time=time) SchedTimeSessAssignment.objects.create( timeslot=slot, session=session, - schedule=session.meeting.schedule) + schedule=meeting.schedule) def populate_important_dates(meeting): assert ImportantDate.objects.filter(meeting=meeting).exists() is False diff --git a/ietf/meeting/management/commands/create_dummy_meeting.py b/ietf/meeting/management/commands/create_dummy_meeting.py index 79b1e5db6..2a4b1dad7 100644 --- a/ietf/meeting/management/commands/create_dummy_meeting.py +++ b/ietf/meeting/management/commands/create_dummy_meeting.py @@ -48,7 +48,7 @@ import socket import datetime import pytz -from django.core.management.base import BaseCommand +from django.core.management.base import BaseCommand, CommandError from django.db import transaction from django.db.models import Q @@ -75,10 +75,12 @@ class Command(BaseCommand): def _meeting_datetime(self, day, *time_args): """Generate a datetime on a meeting day""" - return datetime.datetime.combine( - self.start_date, - datetime.time(*time_args) - ) + datetime.timedelta(days=day) + return self.meeting_tz.localize( + datetime.datetime.combine( + self.start_date, + datetime.time(*time_args) + ) + datetime.timedelta(days=day) + ) def handle(self, *args, **options): if socket.gethostname().split('.')[0] in ['core3', 'ietfa', 'ietfb', 'ietfc', ]: @@ -87,10 +89,7 @@ class Command(BaseCommand): opt_delete = options.get('delete', False) opt_use_old_conflicts = options.get('old_conflicts', False) self.start_date = options['start_date'] - meeting_tz = options['tz'] - if not opt_delete and (meeting_tz not in pytz.common_timezones): - self.stderr.write("Warning: {} is not a recognized time zone.".format(meeting_tz)) - + meeting_tzname = options['tz'] if opt_delete: if Meeting.objects.filter(number='999').exists(): Meeting.objects.filter(number='999').delete() @@ -98,6 +97,11 @@ class Command(BaseCommand): else: self.stderr.write("Dummy meeting IETF 999 does not exist; nothing to do.\n") else: + try: + self.meeting_tz = pytz.timezone(meeting_tzname) + except pytz.UnknownTimeZoneError: + raise CommandError("{} is not a recognized time zone.".format(meeting_tzname)) + if Meeting.objects.filter(number='999').exists(): self.stderr.write("Dummy meeting IETF 999 already exists; nothing to do.\n") else: @@ -111,7 +115,7 @@ class Command(BaseCommand): type_id='IETF', date=self._meeting_datetime(0).date(), days=7, - time_zone=meeting_tz, + time_zone=meeting_tzname, ) # Set enabled constraints diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index df5876e27..d3fd191fb 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -138,6 +138,11 @@ class Meeting(models.Model): def end_date(self): return self.get_meeting_date(self.days-1) + def end_datetime(self): + """Datetime of the first instant _after_ the meeting's last day""" + return self.tz().localize( + datetime.datetime.combine(self.get_meeting_date(self.days), datetime.time()) + ) def get_00_cutoff(self): start_date = datetime.datetime(year=self.date.year, month=self.date.month, day=self.date.day, tzinfo=pytz.utc) importantdate = self.importantdate_set.filter(name_id='idcutoff').first() @@ -322,7 +327,7 @@ class Meeting(models.Model): for ts in self.timeslot_set.all(): if ts.location_id is None: continue - ymd = ts.time.date() + ymd = ts.local_start_time().date() if ymd not in time_slices: time_slices[ymd] = [] slots[ymd] = [] @@ -330,15 +335,15 @@ class Meeting(models.Model): if ymd in time_slices: # only keep unique entries - if [ts.time, ts.time + ts.duration, ts.duration.seconds] not in time_slices[ymd]: - time_slices[ymd].append([ts.time, ts.time + ts.duration, ts.duration.seconds]) + if [ts.local_start_time(), ts.local_end_time(), ts.duration.seconds] not in time_slices[ymd]: + time_slices[ymd].append([ts.local_start_time(), ts.local_end_time(), ts.duration.seconds]) slots[ymd].append(ts) days.sort() for ymd in time_slices: # Make sure these sort the same way time_slices[ymd].sort() - slots[ymd].sort(key=lambda x: (x.time, x.duration)) + slots[ymd].sort(key=lambda x: (x.local_start_time(), x.duration)) return days,time_slices,slots # this functions makes a list of timeslices and rooms, and @@ -354,6 +359,11 @@ class Meeting(models.Model): # SchedTimeSessAssignment.objects.create(schedule = sched, # timeslot = ts) + def tz(self): + if not hasattr(self, '_cached_tz'): + self._cached_tz = pytz.timezone(self.time_zone) + return self._cached_tz + def vtimezone(self): try: tzfn = os.path.join(settings.TZDATA_ICS_PATH, self.time_zone + ".ics") @@ -374,16 +384,14 @@ class Meeting(models.Model): self.save() def updated(self): - min_time = datetime.datetime(1970, 1, 1, 0, 0, 0) # should be Meeting.modified, but we don't have that + # should be Meeting.modified, but we don't have that + min_time = pytz.utc.localize(datetime.datetime(1970, 1, 1, 0, 0, 0)) timeslots_updated = self.timeslot_set.aggregate(Max('modified'))["modified__max"] or min_time sessions_updated = self.session_set.aggregate(Max('modified'))["modified__max"] or min_time assignments_updated = min_time if self.schedule: assignments_updated = SchedTimeSessAssignment.objects.filter(schedule__in=[self.schedule, self.schedule.base if self.schedule else None]).aggregate(Max('modified'))["modified__max"] or min_time - ts = max(timeslots_updated, sessions_updated, assignments_updated) - tz = pytz.timezone(settings.PRODUCTION_TIMEZONE) - ts = tz.localize(ts) - return ts + return max(timeslots_updated, sessions_updated, assignments_updated) @memoize def previous_meeting(self): @@ -604,29 +612,22 @@ class TimeSlot(models.Model): return self._cached_html_location def tz(self): - if not hasattr(self, '_cached_tz'): - self._cached_tz = pytz.timezone(self.meeting.time_zone) - return self._cached_tz + return self.meeting.tz() def tzname(self): return self.tz().tzname(self.time) def utc_start_time(self): - local_start_time = self.tz().localize(self.time) - return local_start_time.astimezone(pytz.utc) + return self.time.astimezone(pytz.utc) # USE_TZ is True, so time is aware def utc_end_time(self): - utc_start = self.utc_start_time() - # Add duration after converting start time, otherwise errors creep in around DST change - return None if utc_start is None else utc_start + self.duration + return self.time.astimezone(pytz.utc) + self.duration # USE_TZ is True, so time is aware def local_start_time(self): - return self.tz().localize(self.time) + return self.time.astimezone(self.tz()) def local_end_time(self): - local_start = self.local_start_time() - # Add duration after converting start time, otherwise errors creep in around DST change - return None if local_start is None else local_start + self.duration + return (self.time.astimezone(pytz.utc) + self.duration).astimezone(self.tz()) @property def js_identifier(self): diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py index e5fdd71c5..9fd49f1c8 100644 --- a/ietf/meeting/test_data.py +++ b/ietf/meeting/test_data.py @@ -21,10 +21,12 @@ from ietf.person.factories import PersonFactory from ietf.person.models import Person from ietf.utils.test_data import make_test_data -def make_interim_meeting(group,date,status='sched'): +def make_interim_meeting(group,date,status='sched',tz='UTC'): system_person = Person.objects.get(name="(System)") - time = datetime.datetime.combine(date, datetime.time(9)) - meeting = create_interim_meeting(group=group,date=date) + meeting = create_interim_meeting(group=group,date=date,timezone=tz) + time = meeting.tz().localize( + datetime.datetime.combine(date, datetime.time(9)) + ) session = SessionFactory(meeting=meeting, group=group, attendees=10, requested_duration=datetime.timedelta(minutes=20), @@ -102,24 +104,37 @@ def make_meeting_test_data(meeting=None, create_interims=False): # slots session_date = meeting.date + datetime.timedelta(days=1) + tz = meeting.tz() slot1 = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room, duration=datetime.timedelta(minutes=60), - time=datetime.datetime.combine(session_date, datetime.time(9, 30))) + time=tz.localize( + datetime.datetime.combine(session_date, datetime.time(9, 30)) + )) slot2 = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room, duration=datetime.timedelta(minutes=60), - time=datetime.datetime.combine(session_date, datetime.time(10, 50))) + time=tz.localize( + datetime.datetime.combine(session_date, datetime.time(10, 50)) + )) breakfast_slot = TimeSlot.objects.create(meeting=meeting, type_id="lead", location=breakfast_room, duration=datetime.timedelta(minutes=90), - time=datetime.datetime.combine(session_date, datetime.time(7,0))) + time=tz.localize( + datetime.datetime.combine(session_date, datetime.time(7,0)) + )) reg_slot = TimeSlot.objects.create(meeting=meeting, type_id="reg", location=reg_room, duration=datetime.timedelta(minutes=480), - time=datetime.datetime.combine(session_date, datetime.time(9,0))) + time=tz.localize( + datetime.datetime.combine(session_date, datetime.time(9,0)) + )) break_slot = TimeSlot.objects.create(meeting=meeting, type_id="break", location=break_room, duration=datetime.timedelta(minutes=90), - time=datetime.datetime.combine(session_date, datetime.time(7,0))) + time=tz.localize( + datetime.datetime.combine(session_date, datetime.time(7,0)) + )) plenary_slot = TimeSlot.objects.create(meeting=meeting, type_id="plenary", location=room, duration=datetime.timedelta(minutes=60), - time=datetime.datetime.combine(session_date, datetime.time(11,0))) + time=tz.localize( + datetime.datetime.combine(session_date, datetime.time(11,0)) + )) # mars WG mars = Group.objects.get(acronym='mars') mars_session = SessionFactory(meeting=meeting, group=mars, @@ -213,7 +228,7 @@ def make_meeting_test_data(meeting=None, create_interims=False): return meeting -def make_interim_test_data(): +def make_interim_test_data(meeting_tz='UTC'): date = datetime.date.today() + datetime.timedelta(days=365) date2 = datetime.date.today() + datetime.timedelta(days=1000) PersonFactory(user__username='plain') @@ -225,10 +240,10 @@ def make_interim_test_data(): RoleFactory(group=mars,person__user__username='marschairman',name_id='chair') RoleFactory(group=ames,person__user__username='ameschairman',name_id='chair') - make_interim_meeting(group=mars,date=date,status='sched') - make_interim_meeting(group=mars,date=date2,status='apprw') - make_interim_meeting(group=ames,date=date,status='canceled') - make_interim_meeting(group=ames,date=date2,status='apprw') + make_interim_meeting(group=mars,date=date,status='sched',tz=meeting_tz) + make_interim_meeting(group=mars,date=date2,status='apprw',tz=meeting_tz) + make_interim_meeting(group=ames,date=date,status='canceled',tz=meeting_tz) + make_interim_meeting(group=ames,date=date2,status='apprw',tz=meeting_tz) return diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py index f62e41388..6fdc31c50 100644 --- a/ietf/meeting/tests_js.py +++ b/ietf/meeting/tests_js.py @@ -36,6 +36,7 @@ from ietf.meeting.utils import add_event_info_to_session_qs from ietf.utils.test_utils import assert_ical_response_is_valid from ietf.utils.jstest import ( IetfSeleniumTestCase, ifSeleniumEnabled, selenium_enabled, presence_of_element_child_by_css_selector ) +from ietf.utils.timezone import datetime_today if selenium_enabled(): from selenium.webdriver.common.action_chains import ActionChains @@ -1434,13 +1435,16 @@ class AgendaTests(IetfSeleniumTestCase): # for others (break, reg, other): # row-------- meeting_number = components[1] - start_time = datetime.datetime( - year=int(components[2]), - month=int(components[3]), - day=int(components[4]), - hour=int(components[6][0:2]), - minute=int(components[6][2:4]), + start_time = pytz.utc.localize( + datetime.datetime( + year=int(components[2]), + month=int(components[3]), + day=int(components[4]), + hour=int(components[6][0:2]), + minute=int(components[6][2:4]), + ) ) + # If labeled as plenary, it's plenary... if components[7] == '1plenary': session_type = 'plenary' @@ -1904,10 +1908,9 @@ 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 = pytz.timezone('UTC').localize( + start_time_utc = pytz.utc.localize( datetime.datetime.combine(self.meeting.date, datetime.time(23,0)) ) - start_time_local = start_time_utc.astimezone(pytz.timezone(self.meeting.time_zone)) daytime_session = SessionFactory( meeting=self.meeting, @@ -1916,7 +1919,7 @@ class WeekviewTests(IetfSeleniumTestCase): ) daytime_timeslot = TimeSlotFactory( meeting=self.meeting, - time=start_time_local.replace(tzinfo=None), # drop timezone for Django + time=start_time_utc, duration=duration, ) daytime_session.timeslotassignments.create(timeslot=daytime_timeslot, schedule=self.meeting.schedule) @@ -1929,11 +1932,12 @@ class WeekviewTests(IetfSeleniumTestCase): ) overnight_timeslot = TimeSlotFactory( meeting=self.meeting, - time=datetime.datetime.combine(self.meeting.date, datetime.time(23,0)), + time=self.meeting.tz().localize( + 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, @@ -2191,7 +2195,7 @@ class InterimTests(IetfSeleniumTestCase): expected_assignments = list(SchedTimeSessAssignment.objects.filter( schedule__in=expected_schedules, session__in=expected_interim_sessions, - timeslot__time__gte=datetime.date.today(), + timeslot__time__gte=datetime_today(), )) # The UID formats should match those in the upcoming.ics template expected_uids = [ @@ -2688,13 +2692,17 @@ class EditTimeslotsTests(IetfSeleniumTestCase): delete: [TimeSlot] = TimeSlotFactory.create_batch( 2, meeting=self.meeting, - time=datetime.datetime.combine(delete_day, delete_time), + time=self.meeting.tz().localize( + datetime.datetime.combine(delete_day, delete_time) + ), duration=duration) keep: [TimeSlot] = [ TimeSlotFactory( meeting=self.meeting, - time=datetime.datetime.combine(day, time), + time=self.meeting.tz().localize( + datetime.datetime.combine(day, time) + ), duration=duration ) for (day, time) in ( @@ -2711,7 +2719,9 @@ class EditTimeslotsTests(IetfSeleniumTestCase): '[data-col-id="{}T{}-{}"]'.format( delete_day.isoformat(), delete_time.strftime('%H:%M'), - (datetime.datetime.combine(delete_day, delete_time) + duration).strftime( + self.meeting.tz().localize( + datetime.datetime.combine(delete_day, delete_time) + duration + ).strftime( '%H:%M' )) ) @@ -2733,14 +2743,18 @@ class EditTimeslotsTests(IetfSeleniumTestCase): delete: [TimeSlot] = [ TimeSlotFactory( meeting=self.meeting, - time=datetime.datetime.combine(delete_day, time), + time=self.meeting.tz().localize( + datetime.datetime.combine(delete_day, time) + ), ) for time in times ] keep: [TimeSlot] = [ TimeSlotFactory( meeting=self.meeting, - time=datetime.datetime.combine(day, time), + time=self.meeting.tz().localize( + datetime.datetime.combine(day, time) + ), ) for day in other_days for time in times ] diff --git a/ietf/meeting/tests_models.py b/ietf/meeting/tests_models.py index 4a2f42c16..8dec586ec 100644 --- a/ietf/meeting/tests_models.py +++ b/ietf/meeting/tests_models.py @@ -56,16 +56,16 @@ class MeetingTests(TestCase): def test_vtimezone(self): # normal time zone that should have a zoneinfo file - meeting = MeetingFactory(type_id='ietf', time_zone='America/Los_Angeles') + meeting = MeetingFactory(type_id='ietf', time_zone='America/Los_Angeles', populate_schedule=False) vtz = meeting.vtimezone() self.assertIsNotNone(vtz) self.assertGreater(len(vtz), 0) # time zone that does not have a zoneinfo file should return None - meeting = MeetingFactory(type_id='ietf', time_zone='Fake/Time_Zone') + meeting = MeetingFactory(type_id='ietf', time_zone='Fake/Time_Zone', populate_schedule=False) vtz = meeting.vtimezone() self.assertIsNone(vtz) # ioerror trying to read zoneinfo should return None - meeting = MeetingFactory(type_id='ietf', time_zone='America/Los_Angeles') + meeting = MeetingFactory(type_id='ietf', time_zone='America/Los_Angeles', populate_schedule=False) with patch('ietf.meeting.models.io.open', side_effect=IOError): vtz = meeting.vtimezone() self.assertIsNone(vtz) diff --git a/ietf/meeting/tests_schedule_generator.py b/ietf/meeting/tests_schedule_generator.py index d414805d3..a88280208 100644 --- a/ietf/meeting/tests_schedule_generator.py +++ b/ietf/meeting/tests_schedule_generator.py @@ -1,6 +1,7 @@ # Copyright The IETF Trust 2020, All Rights Reserved import calendar import datetime +import pytz from io import StringIO from django.core.management.base import CommandError @@ -36,9 +37,11 @@ class ScheduleGeneratorTest(TestCase): t = TimeSlotFactory( meeting=self.meeting, location=room, - time=datetime.datetime.combine( - self.meeting.date + datetime.timedelta(days=day), - datetime.time(hour, 0), + time=self.meeting.tz().localize( + datetime.datetime.combine( + self.meeting.date + datetime.timedelta(days=day), + datetime.time(hour, 0), + ) ), duration=datetime.timedelta(minutes=60), ) @@ -306,8 +309,11 @@ class ScheduleGeneratorTest(TestCase): add_to_schedule=False ) # use a timeslot not on Sunday + meeting_date = pytz.utc.localize( + datetime.datetime.combine(self.meeting.get_meeting_date(1), datetime.time()) + ) ts = self.meeting.timeslot_set.filter( - time__gt=self.meeting.date + datetime.timedelta(days=1), + time__gt=meeting_date, location__capacity__lt=base_reg_session.attendees, ).order_by( 'time' diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 2b6b20d1c..4d53ab856 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -52,6 +52,7 @@ from ietf.utils.decorators import skip_coverage from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent from ietf.utils.text import xslugify +from ietf.utils.timezone import date_today, time_now from ietf.person.factories import PersonFactory from ietf.group.factories import GroupFactory, GroupEventFactory, RoleFactory @@ -155,7 +156,7 @@ class MeetingTests(BaseMeetingTestCase): # self.write_materials_files(meeting, session) # - future_year = datetime.date.today().year+1 + future_year = date_today().year+1 future_num = (future_year-1984)*3 # valid for the mid-year meeting future_meeting = Meeting.objects.create(date=datetime.date(future_year, 7, 22), number=future_num, type_id='ietf', city="Panama City", country="PA", time_zone='America/Panama') @@ -228,7 +229,10 @@ class MeetingTests(BaseMeetingTestCase): 'Time zone indicator should be in nav sidebar') # plain - time_interval = r"%s-%s" % (slot.time.strftime("%H:%M").lstrip("0"), (slot.time + slot.duration).strftime("%H:%M").lstrip("0")) + time_interval = r"{}-{}".format( + slot.time.astimezone(meeting.tz()).strftime("%H:%M").lstrip("0"), + slot.end_time().astimezone(meeting.tz()).strftime("%H:%M").lstrip("0"), + ) r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number))) self.assertEqual(r.status_code, 200) @@ -430,7 +434,9 @@ class MeetingTests(BaseMeetingTestCase): session_date = meeting.date + datetime.timedelta(days=1) slot3 = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room, duration=datetime.timedelta(minutes=60), - time=datetime.datetime.combine(session_date, datetime.time(13, 30))) + time=meeting.tz().localize( + datetime.datetime.combine(session_date, datetime.time(13, 30)) + )) SchedTimeSessAssignment.objects.create(timeslot=slot3, session=venus_session, schedule=meeting.schedule) url = urlreverse('ietf.meeting.views.agenda', kwargs=dict(num=meeting.number)) r = self.client.get(url) @@ -801,7 +807,12 @@ class MeetingTests(BaseMeetingTestCase): a1 = s1.official_timeslotassignment() t1 = a1.timeslot # Create an extra session - t2 = TimeSlotFactory.create(meeting=meeting, time=datetime.datetime.combine(meeting.date, datetime.time(11, 30))) + t2 = TimeSlotFactory.create( + meeting=meeting, + time=meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time(11, 30)) + ) + ) s2 = SessionFactory.create(meeting=meeting, group=s1.group, add_to_schedule=False) SchedTimeSessAssignment.objects.create(timeslot=t2, session=s2, schedule=meeting.schedule) # @@ -811,16 +822,16 @@ class MeetingTests(BaseMeetingTestCase): r, expected_event_summaries=['mars - Martian Special Interest Group'], expected_event_count=2) - self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S')) - self.assertContains(r, t2.time.strftime('%Y%m%dT%H%M%S')) + self.assertContains(r, t1.local_start_time().strftime('%Y%m%dT%H%M%S')) + self.assertContains(r, t2.local_start_time().strftime('%Y%m%dT%H%M%S')) # url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'session_id':s1.id, }) r = self.client.get(url) assert_ical_response_is_valid(self, r, expected_event_summaries=['mars - Martian Special Interest Group'], expected_event_count=1) - self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S')) - self.assertNotContains(r, t2.time.strftime('%Y%m%dT%H%M%S')) + self.assertContains(r, t1.local_start_time().strftime('%Y%m%dT%H%M%S')) + self.assertNotContains(r, t2.local_start_time().strftime('%Y%m%dT%H%M%S')) def test_meeting_agenda_has_static_ical_links(self): """Links to the agenda_ical view must appear on the agenda page @@ -960,7 +971,7 @@ class MeetingTests(BaseMeetingTestCase): url = urlreverse('ietf.meeting.views.current_materials') response = self.client.get(url) self.assertEqual(response.status_code, 404) - MeetingFactory(type_id='ietf', date=datetime.date.today()) + MeetingFactory(type_id='ietf', date=date_today()) response = self.client.get(url) self.assertEqual(response.status_code, 302) @@ -1105,7 +1116,9 @@ class EditMeetingScheduleTests(TestCase): TimeSlotFactory( meeting=meeting, location=room, - time=datetime.datetime.combine(meeting.date, time), + time=meeting.tz().localize( + datetime.datetime.combine(meeting.date, time) + ), duration=datetime.timedelta(minutes=duration), ) @@ -1189,7 +1202,11 @@ class EditMeetingScheduleTests(TestCase): ] # Set up different sets of timeslots - t0 = datetime.datetime.combine(meeting.date, datetime.time(11, 0)) + # Work with t0 in UTC for arithmetic. This does not change the results but is cleaner if someone looks + # at intermediate results which may be misleading until passed through tz.normalize(). + t0 = meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time(11, 0)) + ).astimezone(pytz.utc) dur = datetime.timedelta(hours=2) for room in room_groups[0]: TimeSlotFactory(meeting=meeting, location=room, duration=dur, time=t0) @@ -1284,7 +1301,7 @@ class EditMeetingScheduleTests(TestCase): self.client.login(username=username, password=username + '+password') # Swap group 0's first and last sessions, first in the past - right_now = self._right_now_in(meeting.time_zone) + right_now = self._right_now_in(meeting.tz()) for room in room_groups[0]: ts = room.timeslot_set.last() ts.time = right_now - datetime.timedelta(minutes=5) @@ -1466,12 +1483,18 @@ class EditMeetingScheduleTests(TestCase): self.client.login(username=username, password=username + '+password') # Swap group 0's first and last sessions, first in the past - right_now = self._right_now_in(meeting.time_zone) - yesterday = (right_now - datetime.timedelta(days=1)).date() - day_before = (right_now - datetime.timedelta(days=2)).date() + right_now = self._right_now_in(meeting.tz()) + yesterday = right_now.date() - datetime.timedelta(days=1) + day_before = right_now.date() - datetime.timedelta(days=2) for room in room_groups[0]: ts = room.timeslot_set.last() - ts.time = datetime.datetime.combine(yesterday, ts.time.time()) + # Calculation keeps local clock time, shifted to a different day. + ts.time = meeting.tz().localize( + datetime.datetime.combine( + yesterday, + ts.time.astimezone(meeting.tz()).time() + ), + ) ts.save() # timeslot_set is ordered by -time, so check that we know which is past/future self.assertTrue(room_groups[0][0].timeslot_set.last().time < right_now) @@ -1505,7 +1528,12 @@ class EditMeetingScheduleTests(TestCase): # now with both in the past for room in room_groups[0]: ts = room.timeslot_set.first() - ts.time = datetime.datetime.combine(day_before, ts.time.time()) + ts.time = meeting.tz().localize( + datetime.datetime.combine( + day_before, + ts.time.astimezone(meeting.tz()).time(), + ) + ) ts.save() past_slots = room_groups[0][0].timeslot_set.filter(time__lt=right_now) self.assertEqual(len(past_slots), 2, 'Need two timeslots in the past!') @@ -1528,8 +1556,8 @@ class EditMeetingScheduleTests(TestCase): self.fail('Response was not valid JSON: {}'.format(err)) @staticmethod - def _right_now_in(tzname): - right_now = timezone.now().astimezone(pytz.timezone(tzname)) + def _right_now_in(tzinfo): + right_now = timezone.now().astimezone(tzinfo) if not settings.USE_TZ: right_now = right_now.replace(tzinfo=None) return right_now @@ -1541,7 +1569,7 @@ class EditMeetingScheduleTests(TestCase): date=(timezone.now() - datetime.timedelta(days=1)).date(), days=3, ) - right_now = self._right_now_in(meeting.time_zone) + right_now = self._right_now_in(meeting.tz()) schedules = dict( official=meeting.schedule, @@ -1601,7 +1629,7 @@ class EditMeetingScheduleTests(TestCase): date=(timezone.now() - datetime.timedelta(days=1)).date(), days=3, ) - right_now = self._right_now_in(meeting.time_zone) + right_now = self._right_now_in(meeting.tz()) schedules = dict( official=meeting.schedule, @@ -1736,7 +1764,7 @@ class EditMeetingScheduleTests(TestCase): date=(timezone.now() - datetime.timedelta(days=1)).date(), days=3, ) - right_now = self._right_now_in(meeting.time_zone) + right_now = self._right_now_in(meeting.tz()) schedules = dict( official=meeting.schedule, @@ -1857,7 +1885,7 @@ class EditTimeslotsTests(TestCase): return MeetingFactory( type_id='ietf', number=number, - date=timezone.now() + datetime.timedelta(days=10), + date=date_today() + datetime.timedelta(days=10), populate_schedule=False, ) @@ -1889,7 +1917,8 @@ class EditTimeslotsTests(TestCase): meeting = self.create_bare_meeting(number=number) RoomFactory.create_batch(8, meeting=meeting) self.create_initial_schedule(meeting) - return meeting + # retrieve meeting from DB so it goes through Django's processing + return Meeting.objects.get(pk=meeting.pk) def test_view_permissions(self): """Only the secretary should be able to edit timeslots""" @@ -2058,7 +2087,7 @@ class EditTimeslotsTests(TestCase): meeting = self.create_meeting() # add some timeslots times = [datetime.time(hour=h) for h in (11, 14)] - days = [meeting.get_meeting_date(ii).date() for ii in range(meeting.days)] + days = [meeting.get_meeting_date(ii) for ii in range(meeting.days)] timeslots = [] duration = datetime.timedelta(minutes=90) @@ -2068,7 +2097,7 @@ class EditTimeslotsTests(TestCase): TimeSlotFactory( meeting=meeting, location=room, - time=datetime.datetime.combine(day, t), + time=meeting.tz().localize(datetime.datetime.combine(day, t)), duration=duration, ) for t in times @@ -2149,17 +2178,21 @@ class EditTimeslotsTests(TestCase): TimeSlotFactory( meeting=meeting, location=meeting.room_set.first(), - time=datetime.datetime.combine( - meeting.get_meeting_date(day).date(), - datetime.time(hour=11) + time=meeting.tz().localize( + datetime.datetime.combine( + meeting.get_meeting_date(day), + datetime.time(hour=11), + ) ), ) TimeSlotFactory( meeting=meeting, location=meeting.room_set.first(), - time=datetime.datetime.combine( - meeting.get_meeting_date(day).date(), - datetime.time(hour=14) + time=meeting.tz().localize( + datetime.datetime.combine( + meeting.get_meeting_date(day), + datetime.time(hour=14), + ) ), ) @@ -2258,10 +2291,8 @@ class EditTimeslotsTests(TestCase): name_before = 'Name Classic (tm)' type_before = 'regular' - time_before = datetime.datetime.combine( - meeting.date, - datetime.time(hour=10), - ) + time_utc = pytz.utc.localize(datetime.datetime.combine(meeting.date, datetime.time(hour=10))) + time_before = time_utc.astimezone(meeting.tz()) duration_before = datetime.timedelta(minutes=60) show_location_before = True location_before = meeting.room_set.first() @@ -2278,7 +2309,7 @@ class EditTimeslotsTests(TestCase): self.login() name_after = 'New Name (tm)' type_after = 'plenary' - time_after = time_before + datetime.timedelta(days=1, hours=2) + time_after = (time_utc + datetime.timedelta(days=1, hours=2)).astimezone(meeting.tz()) duration_after = duration_before * 2 show_location_after = False location_after = meeting.room_set.last() @@ -2458,8 +2489,8 @@ class EditTimeslotsTests(TestCase): ts = meeting.timeslot_set.exclude(pk__in=timeslots_before).first() # only 1 self.assertEqual(ts.name, post_data['name']) self.assertEqual(ts.type_id, post_data['type']) - self.assertEqual(str(ts.time.date().toordinal()), post_data['days']) - self.assertEqual(ts.time.strftime('%H:%M'), post_data['time']) + self.assertEqual(str(ts.local_start_time().date().toordinal()), post_data['days']) + self.assertEqual(ts.local_start_time().strftime('%H:%M'), post_data['time']) self.assertEqual(str(ts.duration), '{}:00'.format(post_data['duration'])) # add seconds self.assertEqual(ts.show_location, post_data['show_location']) self.assertEqual(str(ts.location.pk), post_data['locations']) @@ -2468,7 +2499,7 @@ class EditTimeslotsTests(TestCase): """Creating a single timeslot outside the official meeting days should work""" meeting = self.create_meeting() timeslots_before = set(ts.pk for ts in meeting.timeslot_set.all()) - other_date = meeting.get_meeting_date(-7).date() + other_date = meeting.get_meeting_date(-7) post_data = dict( name='some name', type='regular', @@ -2491,8 +2522,8 @@ class EditTimeslotsTests(TestCase): ts = meeting.timeslot_set.exclude(pk__in=timeslots_before).first() # only 1 self.assertEqual(ts.name, post_data['name']) self.assertEqual(ts.type_id, post_data['type']) - self.assertEqual(ts.time.date(), other_date) - self.assertEqual(ts.time.strftime('%H:%M'), post_data['time']) + self.assertEqual(ts.local_start_time().date(), other_date) + self.assertEqual(ts.local_start_time().strftime('%H:%M'), post_data['time']) self.assertEqual(str(ts.duration), '{}:00'.format(post_data['duration'])) # add seconds self.assertEqual(ts.show_location, post_data['show_location']) self.assertEqual(str(ts.location.pk), post_data['locations']) @@ -2706,8 +2737,8 @@ class EditTimeslotsTests(TestCase): """Creating multiple timeslots should work""" meeting = self.create_meeting() timeslots_before = set(ts.pk for ts in meeting.timeslot_set.all()) - days = [meeting.get_meeting_date(n).date() for n in range(meeting.days)] - other_date = meeting.get_meeting_date(-1).date() # date before start of meeting + days = [meeting.get_meeting_date(n) for n in range(meeting.days)] + other_date = meeting.get_meeting_date(-1) # date before start of meeting self.assertNotIn(other_date, days) locations = meeting.room_set.all() post_data = dict( @@ -2737,10 +2768,10 @@ class EditTimeslotsTests(TestCase): for ts in meeting.timeslot_set.exclude(pk__in=timeslots_before): self.assertEqual(ts.name, post_data['name']) self.assertEqual(ts.type_id, post_data['type']) - self.assertEqual(ts.time.strftime('%H:%M'), post_data['time']) + self.assertEqual(ts.local_start_time().strftime('%H:%M'), post_data['time']) self.assertEqual(str(ts.duration), '{}:00'.format(post_data['duration'])) # add seconds self.assertEqual(ts.show_location, post_data['show_location']) - self.assertIn(ts.time.date(), days) + self.assertIn(ts.local_start_time().date(), days) self.assertIn(ts.location, locations) self.assertIn((ts.time.date(), ts.location), day_locs, 'Duplicated day / location found') @@ -3288,7 +3319,9 @@ class EditTests(TestCase): room = Room.objects.get(meeting=meeting, session_types='regular') base_timeslot = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room, duration=datetime.timedelta(minutes=50), - time=datetime.datetime.combine(meeting.date + datetime.timedelta(days=2), datetime.time(9, 30))) + time=meeting.tz().localize( + datetime.datetime.combine(meeting.date + datetime.timedelta(days=2), datetime.time(9, 30)) + )) timeslots = list(TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('time')) @@ -3510,7 +3543,12 @@ class EditTests(TestCase): self.assertIn("#scroll=1234", r['Location']) test_timeslot = TimeSlot.objects.get(meeting=meeting, name="IETF Testing") - self.assertEqual(test_timeslot.time, datetime.datetime.combine(meeting.date, datetime.time(8, 30))) + self.assertEqual( + test_timeslot.time, + meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time(8, 30)) + ), + ) self.assertEqual(test_timeslot.duration, datetime.timedelta(hours=1, minutes=30)) self.assertEqual(test_timeslot.location_id, break_room.pk) self.assertEqual(test_timeslot.show_location, True) @@ -3552,7 +3590,12 @@ class EditTests(TestCase): }) self.assertNoFormPostErrors(r) test_timeslot.refresh_from_db() - self.assertEqual(test_timeslot.time, datetime.datetime.combine(meeting.date, datetime.time(9, 30))) + self.assertEqual( + test_timeslot.time, + meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time(9, 30)) + ), + ) self.assertEqual(test_timeslot.duration, datetime.timedelta(hours=1)) self.assertEqual(test_timeslot.location_id, breakfast_room.pk) self.assertEqual(test_timeslot.show_location, False) @@ -4329,12 +4372,10 @@ class InterimTests(TestCase): self.do_interim_skip_announcement_test(extra_session=True, canceled_session=True, base_session=True) def do_interim_send_announcement_test(self, base_session=False, extra_session=False, canceled_session=False): - make_interim_test_data() + make_interim_test_data(meeting_tz='America/Los_Angeles') session = Session.objects.with_current_status().filter( meeting__type='interim', group__acronym='mars', current_status='apprw').first() meeting = session.meeting - meeting.time_zone = 'America/Los_Angeles' - meeting.save() if base_session: base_session = SessionFactory(meeting=meeting, status_id='apprw', add_to_schedule=False) @@ -4679,9 +4720,9 @@ class InterimTests(TestCase): def do_interim_request_single_virtual(self, emails_expected): make_meeting_test_data() group = Group.objects.get(acronym='mars') - date = datetime.date.today() + datetime.timedelta(days=30) - time = timezone.now().time().replace(microsecond=0,second=0) - dt = datetime.datetime.combine(date, time) + date = date_today() + datetime.timedelta(days=30) + time = time_now().replace(microsecond=0,second=0) + dt = pytz.utc.localize(datetime.datetime.combine(date, time)) duration = datetime.timedelta(hours=3) remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' @@ -4750,13 +4791,14 @@ class InterimTests(TestCase): def test_interim_request_single_in_person(self): make_meeting_test_data() group = Group.objects.get(acronym='mars') - date = datetime.date.today() + datetime.timedelta(days=30) - time = timezone.now().time().replace(microsecond=0,second=0) - dt = datetime.datetime.combine(date, time) + date = date_today() + datetime.timedelta(days=30) + time = time_now().replace(microsecond=0,second=0) + time_zone = 'America/Los_Angeles' + tz = pytz.timezone(time_zone) + dt = tz.localize(datetime.datetime.combine(date, time)) duration = datetime.timedelta(hours=3) city = 'San Francisco' country = 'US' - time_zone = 'America/Los_Angeles' remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' @@ -4797,16 +4839,17 @@ class InterimTests(TestCase): def test_interim_request_multi_day(self): make_meeting_test_data() - date = datetime.date.today() + datetime.timedelta(days=30) + date = date_today() + datetime.timedelta(days=30) date2 = date + datetime.timedelta(days=1) - time = timezone.now().time().replace(microsecond=0,second=0) - dt = datetime.datetime.combine(date, time) - dt2 = datetime.datetime.combine(date2, time) + time = time_now().replace(microsecond=0,second=0) + time_zone = 'America/Los_Angeles' + tz = pytz.timezone(time_zone) + dt = tz.localize(datetime.datetime.combine(date, time)) + dt2 = tz.localize(datetime.datetime.combine(date2, time)) duration = datetime.timedelta(hours=3) group = Group.objects.get(acronym='mars') city = 'San Francisco' country = 'US' - time_zone = 'America/Los_Angeles' remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' @@ -4923,7 +4966,7 @@ class InterimTests(TestCase): def test_interim_request_series(self): make_meeting_test_data() meeting_count_before = Meeting.objects.filter(type='interim').count() - date = datetime.date.today() + datetime.timedelta(days=30) + date = date_today() + datetime.timedelta(days=30) if (date.month, date.day) == (12, 31): # Avoid date and date2 in separate years # (otherwise the test will fail if run on December 1st) @@ -4933,14 +4976,15 @@ class InterimTests(TestCase): if date.year != date2.year: date += datetime.timedelta(days=1) date2 += datetime.timedelta(days=1) - time = timezone.now().time().replace(microsecond=0,second=0) - dt = datetime.datetime.combine(date, time) - dt2 = datetime.datetime.combine(date2, time) + time = time_now().replace(microsecond=0,second=0) + time_zone = 'America/Los_Angeles' + tz = pytz.timezone(time_zone) + dt = tz.localize(datetime.datetime.combine(date, time)) + dt2 = tz.localize(datetime.datetime.combine(date2, time)) duration = datetime.timedelta(hours=3) group = Group.objects.get(acronym='mars') city = '' country = '' - time_zone = 'America/Los_Angeles' remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' @@ -5082,14 +5126,14 @@ class InterimTests(TestCase): self.assertFalse(can_manage_group(user=user,group=group)) def test_interim_request_details(self): - make_interim_test_data() + make_interim_test_data(meeting_tz='America/Chicago') meeting = Session.objects.with_current_status().filter( meeting__type='interim', group__acronym='mars', current_status='apprw').first().meeting url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number}) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) - start_time = meeting.session_set.first().official_timeslotassignment().timeslot.time.strftime('%H:%M') + start_time = meeting.session_set.first().official_timeslotassignment().timeslot.local_start_time().strftime('%H:%M') utc_start_time = meeting.session_set.first().official_timeslotassignment().timeslot.utc_start_time().strftime('%H:%M') self.assertIn(start_time, unicontent(r)) self.assertIn(utc_start_time, unicontent(r)) @@ -5630,7 +5674,11 @@ class InterimTests(TestCase): a1 = s1.official_timeslotassignment() t1 = a1.timeslot # Create an extra session - t2 = TimeSlotFactory.create(meeting=meeting, time=datetime.datetime.combine(meeting.date, datetime.time(11, 30))) + t2 = TimeSlotFactory.create( + meeting=meeting, + time=meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time(11, 30)) + )) s2 = SessionFactory.create(meeting=meeting, group=s1.group, add_to_schedule=False) SchedTimeSessAssignment.objects.create(timeslot=t2, session=s2, schedule=meeting.schedule) # @@ -5640,8 +5688,8 @@ class InterimTests(TestCase): self.assertContains(r, 'BEGIN:VEVENT') self.assertEqual(r.content.count(b'UID'), 2) self.assertContains(r, 'SUMMARY:mars - Martian Special Interest Group') - self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S')) - self.assertContains(r, t2.time.strftime('%Y%m%dT%H%M%S')) + self.assertContains(r, t1.local_start_time().strftime('%Y%m%dT%H%M%S')) + self.assertContains(r, t2.local_start_time().strftime('%Y%m%dT%H%M%S')) self.assertContains(r, 'END:VEVENT') # url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'session_id':s1.id, }) @@ -7422,7 +7470,7 @@ class ProceedingsTests(BaseMeetingTestCase): def test_proceedings_no_agenda(self): # Meeting number must be larger than the last special-cased proceedings (currently 96) - meeting = MeetingFactory(type_id='ietf',populate_schedule=False,date=datetime.date.today(), number='100') + meeting = MeetingFactory(type_id='ietf',populate_schedule=False,date=date_today(), number='100') url = urlreverse('ietf.meeting.views.proceedings') r = self.client.get(url) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials')) @@ -7531,7 +7579,7 @@ class ProceedingsTests(BaseMeetingTestCase): """Generate a meeting for proceedings material test""" # meeting number 123 avoids various legacy cases that affect these tests # (as of Aug 2021, anything above 96 is probably ok) - return MeetingFactory(type_id='ietf', number='123', date=datetime.date.today()) + return MeetingFactory(type_id='ietf', number='123', date=date_today()) def _secretary_only_permission_test(self, url, include_post=True): self.client.logout() diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index f6c1800f1..25366e7bd 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import datetime import itertools +import pytz import requests import subprocess @@ -34,13 +35,17 @@ def session_time_for_sorting(session, use_meeting_date): if official_timeslot: return official_timeslot.time elif use_meeting_date and session.meeting.date: - return datetime.datetime.combine(session.meeting.date, datetime.time.min) + return session.meeting.tz().localize( + datetime.datetime.combine(session.meeting.date, datetime.time.min) + ) else: first_event = SchedulingEvent.objects.filter(session=session).order_by('time', 'id').first() if first_event: return first_event.time else: - return datetime.datetime.min + # n.b. cannot interpret this in timezones west of UTC. That is not expected to be necessary, + # but could probably safely add a day to the minimum datetime to make that possible. + return pytz.utc.localize(datetime.datetime.min) def session_requested_by(session): first_event = SchedulingEvent.objects.filter(session=session).order_by('time', 'id').first() @@ -159,7 +164,12 @@ def create_proceedings_templates(meeting): def finalize(meeting): end_date = meeting.end_date() - end_time = datetime.datetime.combine(end_date, datetime.datetime.min.time())+datetime.timedelta(days=1) + end_time = meeting.tz().localize( + datetime.datetime.combine( + end_date, + datetime.time.min, + ) + ).astimezone(pytz.utc) + datetime.timedelta(days=1) for session in meeting.session_set.all(): for sp in session.sessionpresentation_set.filter(document__type='draft',rev=None): rev_before_end = [e for e in sp.document.docevent_set.filter(newrevisiondocevent__isnull=False).order_by('-time') if e.time <= end_time ] @@ -323,7 +333,9 @@ def preprocess_constraints_for_meeting_schedule_editor(meeting, sessions): # synthesize AD constraints - we can treat them as a special kind of 'bethere' responsible_ad_for_group = {} session_groups = set(s.group for s in sessions if s.group and s.group.parent and s.group.parent.type_id == 'area') - meeting_time = datetime.datetime.combine(meeting.date, datetime.time(0, 0, 0)) + meeting_time = meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time(0, 0, 0)) + ) # dig up historic AD names for group_id, history_time, pk in Person.objects.filter(rolehistory__name='ad', rolehistory__group__group__in=session_groups, rolehistory__group__time__lte=meeting_time).values_list('rolehistory__group__group', 'rolehistory__group__time', 'pk').order_by('rolehistory__group__time'): diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index c8eac6f9f..1d38346c7 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -96,6 +96,7 @@ from ietf.utils.pipe import pipe from ietf.utils.pdf import pdf_pages from ietf.utils.response import permission_denied from ietf.utils.text import xslugify +from ietf.utils.timezone import datetime_today, date_today from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm, InterimCancelForm, InterimSessionInlineFormSet, RequestMinutesForm, @@ -127,7 +128,7 @@ def materials(request, num=None): begin_date = meeting.get_submission_start_date() cut_off_date = meeting.get_submission_cut_off_date() cor_cut_off_date = meeting.get_submission_correction_date() - now = datetime.date.today() + now = date_today() old = timezone.now() - datetime.timedelta(days=1) if settings.SERVER_MODE != 'production' and '_testoverride' in request.GET: pass @@ -142,7 +143,7 @@ def materials(request, num=None): 'cor_cut_off_date': cor_cut_off_date }) - past_cutoff_date = datetime.date.today() > meeting.get_submission_correction_date() + past_cutoff_date = date_today() > meeting.get_submission_correction_date() schedule = get_schedule(meeting, None) @@ -192,7 +193,7 @@ def materials(request, num=None): }) def current_materials(request): - today = datetime.date.today() + today = date_today() meetings = Meeting.objects.exclude(number__startswith='interim-').filter(date__lte=today).order_by('-date') if meetings: return redirect(materials, meetings[0].number) @@ -281,60 +282,63 @@ def materials_editable_groups(request, num=None): def edit_timeslots(request, num=None): meeting = get_meeting(num) + timezone.activate(meeting.tz()) + try: + if request.method == 'POST': + # handle AJAX requests + action = request.POST.get('action') + if action == 'delete': + # delete a timeslot + # Parameters: + # slot_id: comma-separated list of TimeSlot PKs to delete + slot_id = request.POST.get('slot_id') + if slot_id is None: + return HttpResponseBadRequest('missing slot_id') + slot_ids = [id.strip() for id in slot_id.split(',')] + try: + timeslots = meeting.timeslot_set.filter(pk__in=slot_ids) + except ValueError: + return HttpResponseBadRequest('invalid slot_id specification') + missing_ids = set(slot_ids).difference(str(ts.pk) for ts in timeslots) + if len(missing_ids) != 0: + return HttpResponseNotFound('TimeSlot ids not found in meeting {}: {}'.format( + meeting.number, + ', '.join(sorted(missing_ids)) + )) + timeslots.delete() + return HttpResponse(content='; '.join('Deleted TimeSlot {}'.format(id) for id in slot_ids)) + else: + return HttpResponseBadRequest('unknown action') - if request.method == 'POST': - # handle AJAX requests - action = request.POST.get('action') - if action == 'delete': - # delete a timeslot - # Parameters: - # slot_id: comma-separated list of TimeSlot PKs to delete - slot_id = request.POST.get('slot_id') - if slot_id is None: - return HttpResponseBadRequest('missing slot_id') - slot_ids = [id.strip() for id in slot_id.split(',')] - try: - timeslots = meeting.timeslot_set.filter(pk__in=slot_ids) - except ValueError: - return HttpResponseBadRequest('invalid slot_id specification') - missing_ids = set(slot_ids).difference(str(ts.pk) for ts in timeslots) - if len(missing_ids) != 0: - return HttpResponseNotFound('TimeSlot ids not found in meeting {}: {}'.format( - meeting.number, - ', '.join(sorted(missing_ids)) - )) - timeslots.delete() - return HttpResponse(content='; '.join('Deleted TimeSlot {}'.format(id) for id in slot_ids)) - else: - return HttpResponseBadRequest('unknown action') + # Labels here differ from those in the build_timeslices() method. The labels here are + # relative to the table: time_slices are the row headings (ie, days), date_slices are + # the column headings (i.e., time intervals), and slots are the per-day list of timeslots + # (with only one timeslot per unique time/duration) + time_slices, date_slices, slots = meeting.build_timeslices() - # Labels here differ from those in the build_timeslices() method. The labels here are - # relative to the table: time_slices are the row headings (ie, days), date_slices are - # the column headings (i.e., time intervals), and slots are the per-day list of timeslots - # (with only one timeslot per unique time/duration) - time_slices, date_slices, slots = meeting.build_timeslices() + ts_list = deque() + rooms = meeting.room_set.order_by("capacity","name","id") + for room in rooms: + for day in time_slices: + for slice in date_slices[day]: + ts_list.append(room.timeslot_set.filter(time=slice[0],duration=datetime.timedelta(seconds=slice[2]))) - ts_list = deque() - rooms = meeting.room_set.order_by("capacity","name","id") - for room in rooms: - for day in time_slices: - for slice in date_slices[day]: - ts_list.append(room.timeslot_set.filter(time=slice[0],duration=datetime.timedelta(seconds=slice[2]))) + # Grab these in one query each to identify sessions that are in use and should be handled with care + ts_with_official_assignments = meeting.timeslot_set.filter(sessionassignments__schedule=meeting.schedule) + ts_with_any_assignments = meeting.timeslot_set.filter(sessionassignments__isnull=False) - # Grab these in one query each to identify sessions that are in use and should be handled with care - ts_with_official_assignments = meeting.timeslot_set.filter(sessionassignments__schedule=meeting.schedule) - ts_with_any_assignments = meeting.timeslot_set.filter(sessionassignments__isnull=False) - - return render(request, "meeting/timeslot_edit.html", - {"rooms":rooms, - "time_slices":time_slices, - "slot_slices": slots, - "date_slices":date_slices, - "meeting":meeting, - "ts_list":ts_list, - "ts_with_official_assignments": ts_with_official_assignments, - "ts_with_any_assignments": ts_with_any_assignments, - }) + return render(request, "meeting/timeslot_edit.html", + {"rooms":rooms, + "time_slices":time_slices, + "slot_slices": slots, + "date_slices":date_slices, + "meeting":meeting, + "ts_list":ts_list, + "ts_with_official_assignments": ts_with_official_assignments, + "ts_with_any_assignments": ts_with_any_assignments, + }) + finally: + timezone.deactivate() class NewScheduleForm(forms.ModelForm): class Meta: @@ -1151,7 +1155,7 @@ def edit_meeting_timeslots_and_misc_sessions(request, num=None, owner=None, name meeting=meeting, type=c['type'], name=c['name'], - time=datetime.datetime.combine(c['day'], c['time']), + time=meeting.tz().localize(datetime.datetime.combine(c['day'], c['time'])), duration=c['duration'], location=c['location'], show_location=c['show_location'], @@ -1200,7 +1204,7 @@ def edit_meeting_timeslots_and_misc_sessions(request, num=None, owner=None, name timeslot.type = c['type'] timeslot.name = c['name'] - timeslot.time = datetime.datetime.combine(c['day'], c['time']) + timeslot.time = meeting.tz().localize(datetime.datetime.combine(c['day'], c['time'])) timeslot.duration = c['duration'] timeslot.location = c['location'] timeslot.show_location = c['show_location'] @@ -1287,8 +1291,9 @@ def edit_meeting_timeslots_and_misc_sessions(request, num=None, owner=None, name for t in timeslot_qs: timeslots_by_day_and_room[(t.time.date(), t.location_id)].append(t) - min_time = min([t.time.time() for t in timeslot_qs] + [datetime.time(8)]) - max_time = max([t.end_time().time() for t in timeslot_qs] + [datetime.time(22)]) + # Calculate full time range for display in meeting-local time, always showing at least 8am to 10pm + min_time = min([t.local_start_time().time() for t in timeslot_qs] + [datetime.time(8)]) + max_time = max([t.local_end_time().time() for t in timeslot_qs] + [datetime.time(22)]) min_max_delta = datetime.datetime.combine(meeting.date, max_time) - datetime.datetime.combine(meeting.date, min_time) day_grid = [] @@ -1310,7 +1315,14 @@ def edit_meeting_timeslots_and_misc_sessions(request, num=None, owner=None, name if s: t.assigned_sessions.append(s) - t.left_offset = 100.0 * (t.time - datetime.datetime.combine(t.time.date(), min_time)) / min_max_delta + local_start_dt = t.local_start_time() + local_min_dt = local_start_dt.replace( + hour=min_time.hour, + minute=min_time.minute, + second=min_time.second, + microsecond=min_time.microsecond, + ) + t.left_offset = 100.0 * (local_start_dt - local_min_dt) / min_max_delta t.layout_width = min(100.0 * t.duration / min_max_delta, 100 - t.left_offset) ts.append(t) @@ -1553,19 +1565,29 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc="" is_current_meeting = (num is None) or (num == get_current_ietf_meeting_num()) - rendered_page = render(request, "meeting/"+base+ext, { - "personalize": False, - "schedule": schedule, - "filtered_assignments": filtered_assignments, - "updated": updated, - "filter_categories": filter_organizer.get_filter_categories(), - "non_area_keywords": filter_organizer.get_non_area_keywords(), - "now": timezone.now().astimezone(pytz.utc), - "timezone": meeting.time_zone, - "is_current_meeting": is_current_meeting, - "use_codimd": True if meeting.date>=settings.MEETING_USES_CODIMD_DATE else False, - "cache_time": 150 if is_current_meeting else 3600, - }, content_type=mimetype[ext]) + display_timezone = 'UTC' if utc else meeting.time_zone + timezone.activate(display_timezone) + try: + rendered_page = render( + request, + "meeting/" + base + ext, + { + "personalize": False, + "schedule": schedule, + "filtered_assignments": filtered_assignments, + "updated": updated, + "filter_categories": filter_organizer.get_filter_categories(), + "non_area_keywords": filter_organizer.get_non_area_keywords(), + "now": timezone.now().astimezone(pytz.utc), + "display_timezone": display_timezone, + "is_current_meeting": is_current_meeting, + "use_codimd": True if meeting.date>=settings.MEETING_USES_CODIMD_DATE else False, + "cache_time": 150 if is_current_meeting else 3600, + }, + content_type=mimetype[ext], + ) + finally: + timezone.deactivate() return rendered_page @@ -1917,7 +1939,7 @@ def agenda_personalize(request, num): 'filtered_assignments': filtered_assignments, 'filter_categories': filter_organizer.get_filter_categories(), 'non_area_labels': filter_organizer.get_non_area_keywords(), - 'timezone': meeting.time_zone, + 'display_timezone': meeting.time_zone, 'is_current_meeting': is_current_meeting, 'cache_time': 150 if is_current_meeting else 3600, } @@ -2305,16 +2327,14 @@ def agenda_json(request, num=None): meetinfo.sort(key=lambda x: x['modified'],reverse=True) last_modified = meetinfo and meetinfo[0]['modified'] - tz = pytz.timezone(settings.PRODUCTION_TIMEZONE) - for obj in meetinfo: - obj['modified'] = tz.localize(obj['modified']).astimezone(pytz.utc).strftime('%Y-%m-%dT%H:%M:%SZ') + obj['modified'] = obj['modified'].astimezone(pytz.utc).strftime('%Y-%m-%dT%H:%M:%SZ') data = {"%s"%num: meetinfo} response = HttpResponse(json.dumps(data, indent=2, sort_keys=True), content_type='application/json;charset=%s'%settings.DEFAULT_CHARSET) if last_modified: - last_modified = tz.localize(last_modified).astimezone(pytz.utc) + last_modified = last_modified.astimezone(pytz.utc) response['Last-Modified'] = format_date_time(timegm(last_modified.timetuple())) return response @@ -2386,7 +2406,9 @@ def session_details(request, num, acronym): # Find the time of the meeting, so that we can look back historically # for what the group was called at the time. - meeting_time = datetime.datetime.combine(meeting.date, datetime.time()) + meeting_time = meeting.tz().localize( + datetime.datetime.combine(meeting.date, datetime.time()) + ) groups = list(set([ s.group for s in sessions ])) group_replacements = find_history_replacements_active_at(groups, meeting_time) @@ -2453,7 +2475,7 @@ def session_details(request, num, acronym): 'is_materials_manager' : session.group.has_role(request.user, session.group.features.matman_roles), 'can_manage_materials' : can_manage, 'can_view_request': can_view_request, - 'thisweek': datetime.date.today()-datetime.timedelta(days=7), + 'thisweek': datetime_today()-datetime.timedelta(days=7), 'now': timezone.now(), 'use_codimd': True if meeting.date>=settings.MEETING_USES_CODIMD_DATE else False, }) @@ -3552,7 +3574,7 @@ def past(request): def upcoming(request): '''List of upcoming meetings''' - today = datetime.date.today() + today = datetime_today() # Get ietf meetings starting 7 days ago, and interim meetings starting today ietf_meetings = Meeting.objects.filter(type_id='ietf', date__gte=today-datetime.timedelta(days=7)) @@ -3616,7 +3638,7 @@ def upcoming(request): 'menu_entries': menu_entries, 'selected_menu_entry': selected_menu_entry, 'now': timezone.now(), - 'use_codimd': True if datetime.date.today()>=settings.MEETING_USES_CODIMD_DATE else False, + 'use_codimd': (date_today() >= settings.MEETING_USES_CODIMD_DATE), }) @@ -3630,7 +3652,7 @@ def upcoming_ical(request): except ValueError as e: return HttpResponseBadRequest(str(e)) - today = datetime.date.today() + today = datetime_today() # get meetings starting 7 days ago -- we'll filter out sessions in the past further down meetings = data_for_meetings_overview(Meeting.objects.filter(date__gte=today-datetime.timedelta(days=7)).prefetch_related('schedule').order_by('date')) @@ -3680,7 +3702,7 @@ def upcoming_ical(request): def upcoming_json(request): '''Return Upcoming meetings in json format''' - today = datetime.date.today() + today = datetime_today() # get meetings starting 7 days ago -- we'll filter out sessions in the past further down meetings = data_for_meetings_overview(Meeting.objects.filter(date__gte=today-datetime.timedelta(days=7)).order_by('date')) @@ -3729,7 +3751,7 @@ def proceedings(request, num=None): begin_date = meeting.get_submission_start_date() cut_off_date = meeting.get_submission_cut_off_date() cor_cut_off_date = meeting.get_submission_correction_date() - now = datetime.date.today() + now = date_today() schedule = get_schedule(meeting, None) sessions = add_event_info_to_session_qs( @@ -4026,7 +4048,7 @@ def important_dates(request, num=None, output_format=None): base_num = int(meeting.number) user = request.user - today = datetime.date.today() + today = datetime_today() meetings = [] if meeting.show_important_dates or meeting.date < today: meetings.append(meeting) @@ -4078,23 +4100,27 @@ def edit_timeslot(request, num, slot_id): meeting = get_object_or_404(Meeting, number=num) if timeslot.meeting != meeting: raise Http404() - if request.method == 'POST': - form = TimeSlotEditForm(instance=timeslot, data=request.POST) - if form.is_valid(): - form.save() - return HttpResponseRedirect(reverse('ietf.meeting.views.edit_timeslots', kwargs={'num': num})) - else: - form = TimeSlotEditForm(instance=timeslot) + timezone.activate(meeting.tz()) # specifies current_timezone used for rendering and form handling + try: + if request.method == 'POST': + form = TimeSlotEditForm(instance=timeslot, data=request.POST) + if form.is_valid(): + form.save() + return HttpResponseRedirect(reverse('ietf.meeting.views.edit_timeslots', kwargs={'num': num})) + else: + form = TimeSlotEditForm(instance=timeslot) - sessions = timeslot.sessions.filter( - timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]) + sessions = timeslot.sessions.filter( + timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]) - return render( - request, - 'meeting/edit_timeslot.html', - {'timeslot': timeslot, 'form': form, 'sessions': sessions}, - status=400 if form.errors else 200, - ) + return render( + request, + 'meeting/edit_timeslot.html', + {'timeslot': timeslot, 'form': form, 'sessions': sessions}, + status=400 if form.errors else 200, + ) + finally: + timezone.deactivate() @role_required('Secretariat') @@ -4105,7 +4131,7 @@ def create_timeslot(request, num): if form.is_valid(): bulk_create_timeslots( meeting, - [datetime.datetime.combine(day, form.cleaned_data['time']) + [meeting.tz().localize(datetime.datetime.combine(day, form.cleaned_data['time'])) for day in form.cleaned_data.get('days', [])], form.cleaned_data['locations'], dict( diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 41f7fec62..604b11cb2 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -15998,7 +15998,7 @@ "fields": { "command": "xym", "switch": "--version", - "time": "2022-07-13T00:09:29.108", + "time": "2022-07-13T00:09:29.108Z", "used": true, "version": "xym 0.5" }, @@ -16009,7 +16009,7 @@ "fields": { "command": "pyang", "switch": "--version", - "time": "2022-07-13T00:09:29.475", + "time": "2022-07-13T00:09:29.475Z", "used": true, "version": "pyang 2.5.3" }, @@ -16020,7 +16020,7 @@ "fields": { "command": "yanglint", "switch": "--version", - "time": "2022-07-13T00:09:29.497", + "time": "2022-07-13T00:09:29.497Z", "used": true, "version": "yanglint SO 1.9.2" }, @@ -16031,7 +16031,7 @@ "fields": { "command": "xml2rfc", "switch": "--version", - "time": "2022-07-13T00:09:30.513", + "time": "2022-07-13T00:09:30.513Z", "used": true, "version": "xml2rfc 3.13.0" }, diff --git a/ietf/secr/proceedings/proc_utils.py b/ietf/secr/proceedings/proc_utils.py index 505bcdcc7..f1fcae132 100644 --- a/ietf/secr/proceedings/proc_utils.py +++ b/ietf/secr/proceedings/proc_utils.py @@ -9,6 +9,7 @@ This module contains all the functions for generating static proceedings pages ''' import datetime import os +import pytz import re import subprocess from urllib.parse import urlencode @@ -201,17 +202,22 @@ def send_audio_import_warning(unmatched_files): # End Recording Functions # ------------------------------------------------- -def get_progress_stats(sdate,edate): +def get_progress_stats(sdate, edate): ''' This function takes a date range and produces a dictionary of statistics / objects for use in a progress report. Generally the end date will be the date of the last meeting and the start date will be the date of the meeting before that. + + Data between midnight UTC on the specified dates are included in the stats. ''' + sdatetime = pytz.utc.localize(datetime.datetime.combine(sdate, datetime.time())) + edatetime = pytz.utc.localize(datetime.datetime.combine(edate, datetime.time())) + data = {} data['sdate'] = sdate data['edate'] = edate - events = DocEvent.objects.filter(doc__type='draft',time__gte=sdate,time__lt=edate) + events = DocEvent.objects.filter(doc__type='draft', time__gte=sdatetime, time__lt=edatetime) data['actions_count'] = events.filter(type='iesg_approved').count() data['last_calls_count'] = events.filter(type='sent_last_call').count() @@ -226,7 +232,7 @@ def get_progress_stats(sdate,edate): data['updated_drafts_count'] = len(set([ e.doc_id for e in update_events ])) # Calculate Final Four Weeks stats (ffw) - ffwdate = edate - datetime.timedelta(days=28) + ffwdate = edatetime - datetime.timedelta(days=28) ffw_new_count = events.filter(time__gte=ffwdate,newrevisiondocevent__rev='00').count() try: ffw_new_percent = format(ffw_new_count / float(data['new_drafts_count']),'.0%') @@ -257,14 +263,14 @@ def get_progress_stats(sdate,edate): data['new_groups'] = Group.objects.filter( type='wg', groupevent__changestategroupevent__state='active', - groupevent__time__gte=sdate, - groupevent__time__lt=edate) + groupevent__time__gte=sdatetime, + groupevent__time__lt=edatetime) data['concluded_groups'] = Group.objects.filter( type='wg', groupevent__changestategroupevent__state='conclude', - groupevent__time__gte=sdate, - groupevent__time__lt=edate) + groupevent__time__gte=sdatetime, + groupevent__time__lt=edatetime) return data diff --git a/ietf/settings.py b/ietf/settings.py index c41d4ae0f..0beb06a84 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -114,7 +114,7 @@ SITE_ID = 1 # to load the internationalization machinery. USE_I18N = False -USE_TZ = False +USE_TZ = True if SERVER_MODE == 'production': MEDIA_ROOT = '/a/www/www6s/lib/dt/media/' @@ -1187,8 +1187,6 @@ CELERY_TIMEZONE = 'UTC' CELERY_BROKER_URL = 'amqp://mq/' CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler' CELERY_BEAT_SYNC_EVERY = 1 # update DB after every event -assert not USE_TZ, 'Drop DJANGO_CELERY_BEAT_TZ_AWARE setting once USE_TZ is True!' -DJANGO_CELERY_BEAT_TZ_AWARE = False # Meetecho API setup: Uncomment this and provide real credentials to enable # Meetecho conference creation for interim session requests diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html index 1d037010a..959b6feaf 100644 --- a/ietf/templates/meeting/agenda.html +++ b/ietf/templates/meeting/agenda.html @@ -4,7 +4,7 @@ {% load static %} {% load ietf_filters %} {% load textfilters %} -{% load htmlfilters agenda_custom_tags %} +{% load htmlfilters agenda_custom_tags tz %} {% block title %} IETF {{ schedule.meeting.number }} Meeting Agenda {% if "-utc" in request.path %}(UTC){% endif %} @@ -36,10 +36,10 @@ Jump to current session {% endif %}
- {% include 'meeting/tz-display.html' with id_suffix="-rh" meeting_timezone=timezone minimal=True only %} + {% include 'meeting/tz-display.html' with id_suffix="-rh" meeting_timezone=display_timezone minimal=True only %}
- Showing {{ timezone|split:"_"|join:" "|split:"/"|join:" / " }} time + Showing {{ display_timezone|split:"_"|join:" "|split:"/"|join:" / " }} time
@@ -63,7 +63,7 @@ {{ schedule.meeting.agenda_info_note|removetags:"h1"|safe }}

{% endif %} - {% include 'meeting/tz-display.html' with id_suffix="" meeting_timezone=timezone only %} + {% include 'meeting/tz-display.html' with id_suffix="" meeting_timezone=display_timezone only %} {% include "meeting/agenda_filter.html" with filter_categories=filter_categories customize_button_text="Filter this agenda view..." always_show=personalize %} {% include "meeting/agenda_personalize_buttonlist.html" with meeting=schedule.meeting personalize=personalize only %}
@@ -249,7 +249,8 @@ {% endif %} {% if item.slot_type.slug == 'plenary' %} - {% include "meeting/timeslot_start_end.html" %} + + {% include "meeting/timeslot_start_end.html" %} {% if item.timeslot.show_location and item.timeslot.location %} {% location_anchor item.timeslot %} @@ -327,11 +328,11 @@ {% if item.session.rescheduled_to %} TO
-
{% if "-utc" in request.path %} - {{ item.session.rescheduled_to.utc_start_time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.utc_end_time|date:"G:i" }} + {{ item.session.rescheduled_to.time|utc|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|utc|date:"G:i" }} {% else %} {{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }} {% endif %} @@ -487,7 +488,7 @@ $(document).ready(function() { // Methods/variables here that are not in ietf_timezone or agenda_filter are from agenda_timezone.js - meeting_timezone = '{{ timezone }}'; + meeting_timezone = '{{ display_timezone }}'; // 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. diff --git a/ietf/templates/meeting/agenda.ics b/ietf/templates/meeting/agenda.ics index 4f207afae..18ec8def8 100644 --- a/ietf/templates/meeting/agenda.ics +++ b/ietf/templates/meeting/agenda.ics @@ -1,4 +1,4 @@ -{% load humanize %}{% autoescape off %}{% load ietf_filters textfilters %}{% load cache %}{% cache 1800 ietf_meeting_agenda_ics schedule.meeting.number request.path request.GET %}BEGIN:VCALENDAR +{% load humanize tz %}{% autoescape off %}{% timezone schedule.meeting.tz %}{% load ietf_filters textfilters %}{% load cache %}{% cache 1800 ietf_meeting_agenda_ics schedule.meeting.number request.path request.GET %}BEGIN:VCALENDAR VERSION:2.0 METHOD:PUBLISH PRODID:-//IETF//datatracker.ietf.org ical agenda//EN @@ -8,8 +8,8 @@ SUMMARY:{% if item.session.name %}{{item.session.name|ics_esc}}{% else %}{% if n {% if item.timeslot.show_location %}LOCATION:{{item.timeslot.get_location}} {% endif %}STATUS:{{item.session.ical_status}} CLASS:PUBLIC -DTSTART{% if schedule.meeting.time_zone %};TZID={{schedule.meeting.time_zone|ics_esc}}{%endif%}:{{ item.timeslot.time|date:"Ymd" }}T{{item.timeslot.time|date:"Hi"}}00 -DTEND{% if schedule.meeting.time_zone %};TZID={{schedule.meeting.time_zone|ics_esc}}{%endif%}:{{ item.timeslot.end_time|date:"Ymd" }}T{{item.timeslot.end_time|date:"Hi"}}00 +DTSTART;TZID={{schedule.meeting.time_zone|ics_esc}}:{{ item.timeslot.time|date:"Ymd" }}T{{item.timeslot.time|date:"Hi"}}00 +DTEND;TZID={{schedule.meeting.time_zone|ics_esc}}:{{ item.timeslot.end_time|date:"Ymd" }}T{{item.timeslot.end_time|date:"Hi"}}00 DTSTAMP:{{ item.timeslot.modified|date:"Ymd" }}T{{ item.timeslot.modified|date:"His" }}Z{% if item.session.agenda %} URL:{{item.session.agenda.get_versionless_href}}{% endif %} DESCRIPTION:{{item.timeslot.name|ics_esc}}\n{% if item.session.agenda_note %} @@ -25,4 +25,4 @@ DESCRIPTION:{{item.timeslot.name|ics_esc}}\n{% if item.session.agenda_note %} \n{# link agenda for ietf meetings #} See in schedule: {% absurl 'ietf.meeting.views.agenda' num=schedule.meeting.number %}#row-{{ item.slug }}\n{% endif %} END:VEVENT -{% endif %}{% endfor %}END:VCALENDAR{% endcache %}{% endautoescape %} +{% endif %}{% endfor %}END:VCALENDAR{% endcache %}{% endtimezone %}{% endautoescape %} diff --git a/ietf/templates/meeting/interim_announcement.txt b/ietf/templates/meeting/interim_announcement.txt index 074394099..f9d5394c3 100644 --- a/ietf/templates/meeting/interim_announcement.txt +++ b/ietf/templates/meeting/interim_announcement.txt @@ -1,11 +1,11 @@ -{% load ietf_filters %}{% if is_change %}MEETING DETAILS HAVE CHANGED. SEE LATEST DETAILS BELOW. +{% load ietf_filters tz %}{% timezone meeting.tz %}{% if is_change %}MEETING DETAILS HAVE CHANGED. SEE LATEST DETAILS BELOW. {% endif %}The {{ group.name }} ({{ group.acronym }}) {% if group.type.slug == 'wg' and group.state.slug == 'bof' %}BOF{% else %}{{group.type.name}}{% endif %} will hold -{% if assignments.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ assignments.first.timeslot.time | date:"H:i" }} to {{ assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ assignments.first.timeslot.utc_start_time | date:"H:i" }} to {{ assignments.first.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}. +{% if assignments.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ assignments.first.timeslot.time | date:"H:i" }} to {{ assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ assignments.first.timeslot.time | utc | date:"H:i" }} to {{ assignments.first.timeslot.end_time | utc | date:"H:i" }} UTC){% endif %}. {% else %}a multi-day {% if not meeting.city %}virtual {% endif %}interim meeting. {% for assignment in assignments %}Session {{ forloop.counter }}: -{{ assignment.timeslot.time | date:"Y-m-d" }} {{ assignment.timeslot.time | date:"H:i" }} to {{ assignment.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone }}{% if meeting.time_zone != 'UTC' %}({{ assignment.timeslot.utc_start_time | date:"H:i" }} to {{ assignment.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %} +{{ assignment.timeslot.time | date:"Y-m-d" }} {{ assignment.timeslot.time | date:"H:i" }} to {{ assignment.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone }}{% if meeting.time_zone != 'UTC' %}({{ assignment.timeslot.time | utc | date:"H:i" }} to {{ assignment.timeslot.end_time | utc | date:"H:i" }} UTC){% endif %} {% endfor %}{% endif %} {% if meeting.city %}Meeting Location: {{ meeting.city }}, {{ meeting.country }} @@ -17,3 +17,4 @@ Information about remote participation: {{ meeting.session_set.first.remote_instructions }} {{ meeting.session_set.first.agenda_note }} +{% endtimezone %} \ No newline at end of file diff --git a/ietf/templates/meeting/interim_request_details.html b/ietf/templates/meeting/interim_request_details.html index 0cfec501b..d0476895f 100644 --- a/ietf/templates/meeting/interim_request_details.html +++ b/ietf/templates/meeting/interim_request_details.html @@ -1,12 +1,12 @@ {% extends "base.html" %} {# Copyright The IETF Trust 2015, All Rights Reserved #} {% load origin %} -{% load static django_bootstrap5 widget_tweaks ietf_filters person_filters textfilters %} +{% load static django_bootstrap5 widget_tweaks ietf_filters person_filters textfilters tz %} {% block title %}Interim Request Details{% endblock %} {% block pagehead %} {% endblock %} -{% block content %} +{% block content %}{% timezone meeting.tz %} {% origin %}

Interim Meeting Request Details

@@ -67,7 +67,7 @@
{{ assignment.timeslot.time|date:"H:i" }} {% if meeting.time_zone != 'UTC' %} - ({{ assignment.timeslot.utc_start_time|date:"H:i" }} UTC) + ({{ assignment.timeslot.time|utc|date:"H:i" }} UTC) {% endif %}
@@ -151,7 +151,7 @@ {% endif %} {% endwith %} {% if can_approve and status_slug == 'apprw' %}{% endif %} -{% endblock %} +{% endtimezone %}{% endblock %} {% block js %} diff --git a/ietf/templates/meeting/timeslot_start_end.html b/ietf/templates/meeting/timeslot_start_end.html index 4ee4a9d3c..243c514c1 100644 --- a/ietf/templates/meeting/timeslot_start_end.html +++ b/ietf/templates/meeting/timeslot_start_end.html @@ -2,10 +2,6 @@
- {% if "-utc" in request.path %} - {{ item.timeslot.utc_start_time|date:"H:i" }}
-{{ item.timeslot.utc_end_time|date:"H:i" }} - {% else %} - {{ item.timeslot.time|date:"H:i" }}
-{{ item.timeslot.end_time|date:"H:i" }} - {% endif %} + {{ item.timeslot.time|date:"H:i" }}
-{{ item.timeslot.end_time|date:"H:i" }}
\ No newline at end of file diff --git a/ietf/templates/meeting/upcoming.html b/ietf/templates/meeting/upcoming.html index 43f998c9a..0fb40c971 100644 --- a/ietf/templates/meeting/upcoming.html +++ b/ietf/templates/meeting/upcoming.html @@ -2,7 +2,7 @@ {# Copyright The IETF Trust 2015, 2020, All Rights Reserved #} {% load origin %} {% load cache %} -{% load ietf_filters static classname %} +{% load ietf_filters static classname tz %} {% block pagehead %} @@ -67,9 +67,9 @@ {% elif entry|classname == 'Session' %} {% with session=entry group=entry.group meeting=entry.meeting %} - {{ session.official_timeslotassignment.timeslot.utc_start_time | date:"Y-m-d H:i" }}-{{ session.official_timeslotassignment.timeslot.utc_end_time | date:"H:i" }} + data-start-utc="{{ session.official_timeslotassignment.timeslot.time | utc | date:'Y-m-d H:i' }}Z" + data-end-utc="{{ session.official_timeslotassignment.timeslot.end_time | utc | date:'Y-m-d H:i' }}Z"> + {{ session.official_timeslotassignment.timeslot.time | utc | date:"Y-m-d H:i" }}-{{ session.official_timeslotassignment.timeslot.end_time | utc | date:"H:i" }} {{ group.acronym }} @@ -132,8 +132,8 @@ { group: '{% if session.group %}{{session.group.acronym}}{% endif %}{% if session.name %} - {{session.name}}{% endif %}', filter_keywords: ["{{ session.filter_keywords|join:'","' }}"], - 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"}}'), + start_moment: moment.utc('{{session.official_timeslotassignment.timeslot.time | utc | date:"Y-m-d H:i"}}'), + end_moment: moment.utc('{{session.official_timeslotassignment.timeslot.end_time | utc | date:"Y-m-d H:i"}}'), url: '{% url 'ietf.meeting.views.session_details' num=session.meeting.number acronym=session.group.acronym %}' } {% endwith %} diff --git a/ietf/utils/timezone.py b/ietf/utils/timezone.py index 723512efe..4d3f4be5f 100644 --- a/ietf/utils/timezone.py +++ b/ietf/utils/timezone.py @@ -3,6 +3,7 @@ import email.utils import datetime from django.conf import settings +from django.utils import timezone def local_timezone_to_utc(d): """Takes a naive datetime in the local timezone and returns a @@ -36,4 +37,35 @@ def email_time_to_local_timezone(date_string): def date2datetime(date, tz=pytz.utc): return datetime.datetime(*(date.timetuple()[:6]), tzinfo=tz) - + + +def datetime_today(tzinfo=None): + """Get a timezone-aware datetime representing midnight today + + For use with datetime fields representing a date. + """ + if tzinfo is None: + tzinfo = pytz.utc + return timezone.now().astimezone(tzinfo).replace(hour=0, minute=0, second=0, microsecond=0) + + +def date_today(tzinfo=None): + """Get the date corresponding to the current moment + + Note that Dates are not themselves timezone aware. + """ + if tzinfo is None: + tzinfo = pytz.utc + return timezone.now().astimezone(tzinfo).date() + + +def time_now(tzinfo=None): + """Get the "wall clock" time corresponding to the current moment + + The value returned by this data is a Time with no tzinfo attached. (Time + objects have only limited timezone support, even if tzinfo is filled in, + and may not behave correctly when daylight savings time shifts are relevant.) + """ + if tzinfo is None: + tzinfo = pytz.utc + return timezone.now().astimezone(tzinfo).time()