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
{% 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 %}
{% 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 %}
{{ 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()