diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4cd9e625c..a5d1c5bba 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -139,6 +139,8 @@ jobs:
       run: |
         echo "Running checks..."
         ./ietf/manage.py check
+        ./ietf/manage.py migrate || true
+        echo "USE_TZ = True" >> ./ietf/settings_local.py
         ./ietf/manage.py migrate
         echo "Validating migrations..."
         if ! ( ietf/manage.py makemigrations --dry-run --check --verbosity 3 ) ; then
diff --git a/bin/add-old-drafts-from-archive.py b/bin/add-old-drafts-from-archive.py
index e169ecdcf..239ba7837 100755
--- a/bin/add-old-drafts-from-archive.py
+++ b/bin/add-old-drafts-from-archive.py
@@ -16,6 +16,7 @@ from django.conf import settings
 from django.core.validators import validate_email, ValidationError
 from ietf.utils.draft import PlaintextDraft
 from ietf.submit.utils import update_authors
+from ietf.utils.timezone import date_today
 
 import debug                            # pyflakes:ignore
 
@@ -66,9 +67,9 @@ for name in sorted(names):
                     print name, rev, "Can't parse", p,":",e
                     continue
             if draft.errors and draft.errors.keys()!=['draftname',]:
-                print "Errors - could not process", name, rev, datetime.datetime.fromtimestamp(p.stat().st_mtime), draft.errors, draft.get_title().encode('utf8')
+                print "Errors - could not process", name, rev, datetime.datetime.fromtimestamp(p.stat().st_mtime, datetime.timezone.utc), draft.errors, draft.get_title().encode('utf8')
             else:
-                time = datetime.datetime.fromtimestamp(p.stat().st_mtime)
+                time = datetime.datetime.fromtimestamp(p.stat().st_mtime, datetime.timezone.utc)
                 if not doc:
                     doc = Document.objects.create(name=name,
                                                   time=time,
@@ -140,7 +141,7 @@ for name in sorted(names):
                         doc = doc,
                         rev = rev,
                         by = system,
-                        desc = "Revision added from id-archive on %s by %s"%(datetime.date.today(),sys.argv[0]),
+                        desc = "Revision added from id-archive on %s by %s"%(date_today(),sys.argv[0]),
                         time=time,
                 )
                 events.append(e)
diff --git a/bin/check-copyright b/bin/check-copyright
index 6698e3fda..13cbcd858 100755
--- a/bin/check-copyright
+++ b/bin/check-copyright
@@ -162,7 +162,7 @@ def get_first_commit(path):
             else:
                 pass
     except OSError:
-        rev, who, when = None, None, datetime.datetime.now()
+        rev, who, when = None, None, datetime.datetime.now(datetime.timezone.utc)
     return { path: { 'rev': rev, 'who': who, 'date': when.strftime('%Y-%m-%d %H:%M:%S'), }, }
 
 
diff --git a/dev/diff/prepare.sh b/dev/diff/prepare.sh
index c18684fc7..2e80a8e43 100644
--- a/dev/diff/prepare.sh
+++ b/dev/diff/prepare.sh
@@ -12,5 +12,24 @@ yarn legacy:build
 echo "Creating data directories..."
 chmod +x ./docker/scripts/app-create-dirs.sh
 ./docker/scripts/app-create-dirs.sh
+
 ./ietf/manage.py check
-./ietf/manage.py migrate
+if ./ietf/manage.py showmigrations | grep "\[ \] 0003_pause_to_change_use_tz"; then
+    if grep "USE_TZ" ./ietf/settings_local.py; then
+        cat ./ietf/settings_local.py | sed 's/USE_TZ.*$/USE_TZ = False/' > /tmp/settings_local.py && mv /tmp/settings_local.py ./ietf/settings_local.py
+    else
+        echo "USE_TZ = False" >> ./ietf/settings_local.py
+    fi
+    # This is expected to exit non-zero at the pause
+    /usr/local/bin/python ./ietf/manage.py migrate  || true
+    cat ./ietf/settings_local.py | sed 's/USE_TZ.*$/USE_TZ = True/' > /tmp/settings_local.py && mv /tmp/settings_local.py ./ietf/settings_local.py
+    /usr/local/bin/python ./ietf/manage.py migrate
+
+else
+    if grep "USE_TZ" ./ietf/settings_local.py; then
+        cat ./ietf/settings_local.py | sed 's/USE_TZ.*$/USE_TZ = True/' > /tmp/settings_local.py && mv /tmp/settings_local.py ./ietf/settings_local.py
+    else
+        echo "USE_TZ = True" >> ./ietf/settings_local.py
+    /usr/local/bin/python ./ietf/manage.py migrate
+    fi
+fi
diff --git a/docker/scripts/app-init.sh b/docker/scripts/app-init.sh
index bd3d51a98..7557e8f45 100755
--- a/docker/scripts/app-init.sh
+++ b/docker/scripts/app-init.sh
@@ -100,7 +100,28 @@ echo "Starting memcached..."
 
 echo "Running initial checks..."
 /usr/local/bin/python $WORKSPACEDIR/ietf/manage.py check --settings=settings_local
-# /usr/local/bin/python $WORKSPACEDIR/ietf/manage.py migrate --settings=settings_local
+
+# Migrate, adjusting to what the current state of the underlying database might be:
+
+if ietf/manage.py showmigrations | grep "\[ \] 0003_pause_to_change_use_tz"; then
+    if grep "USE_TZ" $WORKSPACEDIR/ietf/settings_local.py; then
+        cat $WORKSPACEDIR/ietf/settings_local.py | sed 's/USE_TZ.*$/USE_TZ = False/' > /tmp/settings_local.py && mv /tmp/settings_local.py $WORKSPACEDIR/ietf/settings_local.py
+    else
+        echo "USE_TZ = False" >> $WORKSPACEDIR/ietf/settings_local.py
+    fi
+    # This is expected to exit non-zero at the pause
+    /usr/local/bin/python $WORKSPACEDIR/ietf/manage.py migrate --settings=settings_local || true
+    cat $WORKSPACEDIR/ietf/settings_local.py | sed 's/USE_TZ.*$/USE_TZ = True/' > /tmp/settings_local.py && mv /tmp/settings_local.py $WORKSPACEDIR/ietf/settings_local.py
+    /usr/local/bin/python $WORKSPACEDIR/ietf/manage.py migrate --settings=settings_local
+
+else
+    if grep "USE_TZ" $WORKSPACEDIR/ietf/settings_local.py; then
+        cat $WORKSPACEDIR/ietf/settings_local.py | sed 's/USE_TZ.*$/USE_TZ = True/' > /tmp/settings_local.py && mv /tmp/settings_local.py $WORKSPACEDIR/ietf/settings_local.py
+    else
+        echo "USE_TZ = True" >> $WORKSPACEDIR/ietf/settings_local.py
+    /usr/local/bin/python $WORKSPACEDIR/ietf/manage.py migrate --settings=settings_local
+    fi
+fi
 
 echo "-----------------------------------------------------------------"
 echo "Done!"
diff --git a/ietf/api/management/commands/makeresources.py b/ietf/api/management/commands/makeresources.py
index 07f8402e7..889b2cdfb 100644
--- a/ietf/api/management/commands/makeresources.py
+++ b/ietf/api/management/commands/makeresources.py
@@ -3,7 +3,6 @@
 
 
 import os
-import datetime
 import collections
 import io
 
@@ -14,6 +13,7 @@ import debug                            # pyflakes:ignore
 from django.core.management.base import AppCommand
 from django.db import models
 from django.template import Template, Context
+from django.utils import timezone
 
 from tastypie.resources import ModelResource
 
@@ -89,7 +89,7 @@ class Command(AppCommand):
                     info = dict(
                         app=app.name,
                         app_label=app.label,
-                        date=datetime.datetime.now()
+                        date=timezone.now()
                     )
                     new_models = {}
                     for model, rclass_name in missing_resources:
diff --git a/ietf/api/tests.py b/ietf/api/tests.py
index ab565a917..1be220f0f 100644
--- a/ietf/api/tests.py
+++ b/ietf/api/tests.py
@@ -32,6 +32,7 @@ from ietf.person.models import User
 from ietf.person.models import PersonalApiKey
 from ietf.stats.models import MeetingRegistration
 from ietf.utils.mail import outbox, get_payload_text
+from ietf.utils.models import DumpInfo
 from ietf.utils.test_utils import TestCase, login_testing_unauthorized
 
 OMITTED_APPS = (
@@ -508,10 +509,17 @@ class CustomApiTests(TestCase):
         self.assertEqual(set(missing_fields), set(drop_fields))
 
     def test_api_version(self):
+        DumpInfo.objects.create(date=timezone.datetime(2022,8,31,7,10,1,tzinfo=timezone.utc), host='testapi.example.com',tz='UTC')
         url = urlreverse('ietf.api.views.version')
         r = self.client.get(url)
         data = r.json()
         self.assertEqual(data['version'], ietf.__version__+ietf.__patch__)
+        self.assertEqual(data['dumptime'], "2022-08-31 07:10:01 +0000")
+        DumpInfo.objects.update(tz='PST8PDT')
+        r = self.client.get(url)
+        data = r.json()        
+        self.assertEqual(data['dumptime'], "2022-08-31 07:10:01 -0700")
+
 
     def test_api_appauth(self):
         url = urlreverse('ietf.api.views.app_auth')
diff --git a/ietf/api/views.py b/ietf/api/views.py
index 97a551789..a6a51f667 100644
--- a/ietf/api/views.py
+++ b/ietf/api/views.py
@@ -203,8 +203,13 @@ def api_new_meeting_registration(request):
 
 
 def version(request):
+    dumpdate = None
     dumpinfo = DumpInfo.objects.order_by('-date').first()
-    dumptime = pytz.timezone(dumpinfo.tz).localize(dumpinfo.date).strftime('%Y-%m-%d %H:%M:%S %z') if dumpinfo else None
+    if dumpinfo:
+        dumpdate = dumpinfo.date
+        if dumpinfo.tz != "UTC":
+            dumpdate = pytz.timezone(dumpinfo.tz).localize(dumpinfo.date.replace(tzinfo=None))
+    dumptime = dumpdate.strftime('%Y-%m-%d %H:%M:%S %z') if dumpinfo else None
     return HttpResponse(
             json.dumps({
                         'version': ietf.__version__+ietf.__patch__,
diff --git a/ietf/bin/create-charter-newrevisiondocevents b/ietf/bin/create-charter-newrevisiondocevents
index c7ce9c522..d91c0b5b7 100755
--- a/ietf/bin/create-charter-newrevisiondocevents
+++ b/ietf/bin/create-charter-newrevisiondocevents
@@ -28,7 +28,7 @@ def warn(string):
 # ------------------------------------------------------------------------------
 
 import re
-from datetime import datetime as Datetime
+import datetime
 
 import django
 django.setup()
@@ -44,7 +44,7 @@ system_entity = Person.objects.get(name="(System)")
 charterdir = Path(settings.CHARTER_PATH)
 for file in charterdir.files("charter-ietf-*.txt"):
     fname = file.name
-    ftime = Datetime.fromtimestamp(file.mtime)
+    ftime = datetime.datetime.fromtimestamp(file.mtime, datetime.timezone.utc)
     match = re.search("^(?P<name>[a-z0-9-]+)-(?P<rev>\d\d-\d\d)\.txt$", fname)
     if match:
         name = match.group("name")
diff --git a/ietf/bin/iana-changes-updates b/ietf/bin/iana-changes-updates
index 817f7e533..b0ea6712e 100755
--- a/ietf/bin/iana-changes-updates
+++ b/ietf/bin/iana-changes-updates
@@ -18,6 +18,7 @@ django.setup()
 
 from django.conf import settings
 from optparse import OptionParser
+from zoneinfo import ZoneInfo
 
 parser = OptionParser()
 parser.add_option("-f", "--from", dest="start",
@@ -38,13 +39,16 @@ CLOCK_SKEW_COMPENSATION = 5 # seconds
 MAX_INTERVAL_ACCEPTED_BY_IANA = datetime.timedelta(hours=23)
 
 
+local_tzinfo = ZoneInfo(settings.TIME_ZONE)
 start = datetime.datetime.now() - datetime.timedelta(hours=23) + datetime.timedelta(seconds=CLOCK_SKEW_COMPENSATION)
 if options.start:
     start = datetime.datetime.strptime(options.start, "%Y-%m-%d %H:%M:%S")
+start = start.replace(tzinfo=local_tzinfo).astimezone(datetime.timezone.utc)
 
 end = start + datetime.timedelta(hours=23)
 if options.end:
-    end = datetime.datetime.strptime(options.end, "%Y-%m-%d %H:%M:%S")
+    end = datetime.datetime.strptime(options.end, "%Y-%m-%d %H:%M:%S").replace(tzinfo=local_tzinfo)
+end = end.astimezone(datetime.timezone.utc)
 
 syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_USER)
 
@@ -52,7 +56,13 @@ syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_USER)
 
 from ietf.sync.iana import fetch_changes_json, parse_changes_json, update_history_with_changes
 
-syslog.syslog("Updating history log with new changes from IANA from %s, period %s - %s" % (settings.IANA_SYNC_CHANGES_URL, start, end))
+syslog.syslog(
+    "Updating history log with new changes from IANA from %s, period %s - %s" % (
+        settings.IANA_SYNC_CHANGES_URL,
+        start.astimezone(local_tzinfo),
+        end.astimezone(local_tzinfo),
+    )
+)
 
 t = start
 while t < end:
diff --git a/ietf/bin/rfc-editor-index-updates b/ietf/bin/rfc-editor-index-updates
index 4ff3bf373..dc7abe26b 100755
--- a/ietf/bin/rfc-editor-index-updates
+++ b/ietf/bin/rfc-editor-index-updates
@@ -29,6 +29,7 @@ from django.core.mail import mail_admins
 from ietf.doc.utils import rebuild_reference_relations
 from ietf.utils.log import log
 from ietf.utils.pipe import pipe
+from ietf.utils.timezone import date_today
 
 import ietf.sync.rfceditor
 
@@ -39,7 +40,7 @@ parser.add_option("-d", dest="skip_date",
 
 options, args = parser.parse_args()
 
-skip_date = datetime.date.today() - datetime.timedelta(days=365)
+skip_date = date_today() - datetime.timedelta(days=365)
 if options.skip_date:
     skip_date = datetime.datetime.strptime(options.skip_date, "%Y-%m-%d").date()
 
diff --git a/ietf/bin/send-review-reminders b/ietf/bin/send-review-reminders
index e74694c8a..317db4376 100755
--- a/ietf/bin/send-review-reminders
+++ b/ietf/bin/send-review-reminders
@@ -26,8 +26,9 @@ from ietf.review.utils import (
     send_unavailability_period_ending_reminder, send_reminder_all_open_reviews,
     send_review_reminder_overdue_assignment, send_reminder_unconfirmed_assignments)
 from ietf.utils.log import log
+from ietf.utils.timezone import date_today, DEADLINE_TZINFO
 
-today = datetime.date.today()
+today = date_today(DEADLINE_TZINFO)
 
 for assignment in review_assignments_needing_reviewer_reminder(today):
     email_reviewer_reminder(assignment)
diff --git a/ietf/bin/send-scheduled-mail b/ietf/bin/send-scheduled-mail
index c6a2dbf50..d3474db09 100755
--- a/ietf/bin/send-scheduled-mail
+++ b/ietf/bin/send-scheduled-mail
@@ -3,7 +3,7 @@
 # This script requires that the proper virtual python environment has been
 # invoked before start
 
-import datetime, os, sys
+import os, sys
 import syslog
 
 # boilerplate
@@ -16,6 +16,7 @@ syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_USER)
 
 import django
 django.setup()
+from django.utils import timezone
 
 from ietf.utils.mail import log_smtp_exception, send_error_email
 from smtplib import SMTPException
@@ -32,7 +33,7 @@ from ietf.message.models import SendQueue
 
 mode = sys.argv[1]
 
-now = datetime.datetime.now()
+now = timezone.now()
 
 needs_sending = SendQueue.objects.filter(sent_at=None).select_related("message")
 if mode == "specific":
diff --git a/ietf/community/views.py b/ietf/community/views.py
index c67f992d6..b0646424a 100644
--- a/ietf/community/views.py
+++ b/ietf/community/views.py
@@ -10,6 +10,7 @@ import uuid
 from django.http import HttpResponse, HttpResponseRedirect, Http404
 from django.shortcuts import get_object_or_404, render
 from django.contrib.auth.decorators import login_required
+from django.utils import timezone
 from django.utils.html import strip_tags
 
 import debug                            # pyflakes:ignore
@@ -218,7 +219,7 @@ def feed(request, username=None, acronym=None, group_type=None):
     significant = request.GET.get('significant', '') == '1'
 
     documents = docs_tracked_by_community_list(clist).values_list('pk', flat=True)
-    since = datetime.datetime.now() - datetime.timedelta(days=14)
+    since = timezone.now() - datetime.timedelta(days=14)
 
     events = DocEvent.objects.filter(
         doc__id__in=documents,
@@ -243,7 +244,7 @@ def feed(request, username=None, acronym=None, group_type=None):
         'title': title,
         'subtitle': subtitle,
         'id': feed_id.urn,
-        'updated': datetime.datetime.now(),
+        'updated': timezone.now(),
     }, content_type='text/xml')
 
 
diff --git a/ietf/doc/expire.py b/ietf/doc/expire.py
index b780c73bd..af48827cf 100644
--- a/ietf/doc/expire.py
+++ b/ietf/doc/expire.py
@@ -4,6 +4,7 @@
 
 
 from django.conf import settings
+from django.utils import timezone
 
 import datetime, os, shutil, glob, re
 from pathlib import Path
@@ -17,6 +18,7 @@ from ietf.person.models import Person
 from ietf.meeting.models import Meeting
 from ietf.doc.utils import add_state_change_event, update_action_holders
 from ietf.mailtrigger.utils import gather_address_lists
+from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO
 
 
 nonexpirable_states: Optional[List[State]] = None
@@ -52,17 +54,17 @@ def expirable_drafts(queryset=None):
 
 
 def get_soon_to_expire_drafts(days_of_warning):
-    start_date = datetime.date.today() - datetime.timedelta(1)
+    start_date = datetime_today(DEADLINE_TZINFO) - datetime.timedelta(1)
     end_date = start_date + datetime.timedelta(days_of_warning)
 
     return expirable_drafts().filter(expires__gte=start_date, expires__lt=end_date)
 
 def get_expired_drafts():
-    return expirable_drafts().filter(expires__lt=datetime.date.today() + datetime.timedelta(1))
+    return expirable_drafts().filter(expires__lt=datetime_today(DEADLINE_TZINFO) + datetime.timedelta(1))
 
 def in_draft_expire_freeze(when=None):
     if when == None:
-        when = datetime.datetime.now()
+        when = timezone.now()
 
     meeting = Meeting.objects.filter(type='ietf', date__gte=when-datetime.timedelta(days=7)).order_by('date').first()
 
@@ -171,7 +173,7 @@ def expire_draft(doc):
 
 def clean_up_draft_files():
     """Move unidentified and old files out of the Internet Draft directory."""
-    cut_off = datetime.date.today()
+    cut_off = date_today()
 
     pattern = os.path.join(settings.INTERNET_DRAFT_PATH, "draft-*.*")
     filename_re = re.compile(r'^(.*)-(\d\d)$')
diff --git a/ietf/doc/factories.py b/ietf/doc/factories.py
index 8fd6feca7..40e0f506b 100644
--- a/ietf/doc/factories.py
+++ b/ietf/doc/factories.py
@@ -10,6 +10,7 @@ import datetime
 from typing import Optional         # pyflakes:ignore
 
 from django.conf import settings
+from django.utils import timezone
 
 from ietf.doc.models import ( Document, DocEvent, NewRevisionDocEvent, DocAlias, State, DocumentAuthor,
     StateDocEvent, BallotPositionDocEvent, BallotDocEvent, BallotType, IRSGBallotDocEvent, TelechatDocEvent,
@@ -18,6 +19,7 @@ from ietf.group.models import Group
 from ietf.person.factories import PersonFactory
 from ietf.group.factories import RoleFactory
 from ietf.utils.text import xslugify
+from ietf.utils.timezone import date_today
 
 
 def draft_name_generator(type_id,group,n):
@@ -38,7 +40,7 @@ class BaseDocumentFactory(factory.django.DjangoModelFactory):
     rev = '00'
     std_level_id = None                 # type: Optional[str]
     intended_std_level_id = None
-    time = datetime.datetime.now()
+    time = timezone.now()
     expires = factory.LazyAttribute(lambda o: o.time+datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE))
     pages = factory.fuzzy.FuzzyInteger(2,400)
 
@@ -320,7 +322,7 @@ class ConflictReviewFactory(BaseDocumentFactory):
 # This is very skeletal. It is enough for the tests that use it now, but when it's needed, it will need to be improved with, at least, a group generator that backs the object with a review team.
 class ReviewFactory(BaseDocumentFactory):
     type_id = 'review'
-    name = factory.LazyAttribute(lambda o: 'review-doesnotexist-00-%s-%s'%(o.group.acronym,datetime.date.today().isoformat()))
+    name = factory.LazyAttribute(lambda o: 'review-doesnotexist-00-%s-%s'%(o.group.acronym,date_today().isoformat()))
     group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='review')
 
 class DocAliasFactory(factory.django.DjangoModelFactory):
@@ -357,7 +359,8 @@ class TelechatDocEventFactory(DocEventFactory):
     class Meta:
         model = TelechatDocEvent
 
-    telechat_date = datetime.datetime.today()+datetime.timedelta(days=14)
+    # note: this is evaluated at import time and not updated - all events will have the same telechat_date
+    telechat_date = timezone.now()+datetime.timedelta(days=14)
     type = 'scheduled_for_telechat'
 
 class NewRevisionDocEventFactory(DocEventFactory):
@@ -410,7 +413,7 @@ class IRSGBallotDocEventFactory(BallotDocEventFactory):
     class Meta:
         model = IRSGBallotDocEvent
 
-    duedate = datetime.datetime.now() + datetime.timedelta(days=14)
+    duedate = timezone.now() + datetime.timedelta(days=14)
     ballot_type = factory.SubFactory(BallotTypeFactory, slug='irsg-approve')
 
 class BallotPositionDocEventFactory(DocEventFactory):
diff --git a/ietf/doc/feeds.py b/ietf/doc/feeds.py
index 1169db105..7885e75e3 100644
--- a/ietf/doc/feeds.py
+++ b/ietf/doc/feeds.py
@@ -10,11 +10,13 @@ from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
 from django.urls import reverse as urlreverse
 from django.template.defaultfilters import truncatewords, truncatewords_html, date as datefilter
 from django.template.defaultfilters import linebreaks # type: ignore
+from django.utils import timezone
 from django.utils.html import strip_tags
 
 from ietf.doc.models import Document, State, LastCallDocEvent, DocEvent
 from ietf.doc.utils import augment_events_with_revision
 from ietf.doc.templatetags.ietf_filters import format_textarea
+from ietf.utils.timezone import RPC_TZINFO
 
 
 def strip_control_characters(s):
@@ -133,9 +135,16 @@ class RfcFeed(Feed):
     
     def items(self):
         if self.year:
-            rfc_events = DocEvent.objects.filter(type='published_rfc',time__year=self.year).order_by('-time')
+            # Find published RFCs based on their official publication year
+            start_of_year = datetime.datetime(int(self.year), 1, 1, tzinfo=RPC_TZINFO)
+            start_of_next_year = datetime.datetime(int(self.year) + 1, 1, 1, tzinfo=RPC_TZINFO)
+            rfc_events = DocEvent.objects.filter(
+                type='published_rfc',
+                time__gte=start_of_year,
+                time__lt=start_of_next_year,
+            ).order_by('-time')
         else:
-            cutoff = datetime.datetime.now() - datetime.timedelta(days=8)
+            cutoff = timezone.now() - datetime.timedelta(days=8)
             rfc_events = DocEvent.objects.filter(type='published_rfc',time__gte=cutoff).order_by('-time')
         results = [(e.doc, e.time) for e in rfc_events]
         for doc,time in results:
diff --git a/ietf/doc/forms.py b/ietf/doc/forms.py
index d209eb0a2..372c43ab0 100644
--- a/ietf/doc/forms.py
+++ b/ietf/doc/forms.py
@@ -15,6 +15,7 @@ from ietf.person.fields import SearchablePersonField, SearchablePersonsField
 from ietf.person.models import Email, Person
 
 from ietf.name.models import ExtResourceName
+from ietf.utils.timezone import date_today
 from ietf.utils.validators import validate_external_resource_value
 
 class TelechatForm(forms.Form):
@@ -34,7 +35,7 @@ class TelechatForm(forms.Form):
         for d in dates:
           self.page_count[d] = telechat_page_count(date=d).for_approval
           choice_display[d] = '%s (%s pages)' % (d.strftime("%Y-%m-%d"),self.page_count[d])
-          if d-datetime.date.today() < datetime.timedelta(days=13):
+          if d - date_today() < datetime.timedelta(days=13):
               choice_display[d] += ' : WARNING - this may not leave enough time for directorate reviews!'
         self.fields['telechat_date'].choices = [("", "(not on agenda)")] + [(d, choice_display[d]) for d in dates]
 
diff --git a/ietf/doc/lastcall.py b/ietf/doc/lastcall.py
index fab3001b1..1c490c64c 100644
--- a/ietf/doc/lastcall.py
+++ b/ietf/doc/lastcall.py
@@ -1,7 +1,5 @@
 # helpers for handling last calls on Internet Drafts
 
-import datetime
-
 from django.db.models import Q
 
 from ietf.doc.models import Document, State, DocEvent, LastCallDocEvent, WriteupDocEvent
@@ -10,6 +8,8 @@ from ietf.person.models import Person
 from ietf.doc.utils import add_state_change_event, update_action_holders
 from ietf.doc.mails import generate_ballot_writeup, generate_approval_mail, generate_last_call_announcement
 from ietf.doc.mails import send_last_call_request, email_last_call_expired, email_last_call_expired_with_downref
+from ietf.utils.timezone import date_today, DEADLINE_TZINFO
+
 
 def request_last_call(request, doc):
     if not doc.latest_event(type="changed_ballot_writeup_text"):
@@ -33,7 +33,7 @@ def request_last_call(request, doc):
     e.save()
 
 def get_expired_last_calls():
-    today = datetime.date.today()
+    today = date_today(DEADLINE_TZINFO)
     for d in Document.objects.filter(Q(states__type="draft-iesg", states__slug="lc")
                                     | Q(states__type="statchg", states__slug="in-lc")):
         e = d.latest_event(LastCallDocEvent, type="sent_last_call")
diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py
index 0f344f015..54e0f47e2 100644
--- a/ietf/doc/mails.py
+++ b/ietf/doc/mails.py
@@ -10,6 +10,7 @@ from django.template.loader import render_to_string
 from django.utils.html import strip_tags
 from django.conf import settings
 from django.urls import reverse as urlreverse
+from django.utils import timezone
 from django.utils.encoding import force_text
 
 import debug                            # pyflakes:ignore
@@ -24,6 +25,7 @@ from ietf.doc.utils_bofreq import bofreq_editors, bofreq_responsible
 from ietf.group.models import Role
 from ietf.doc.models import Document
 from ietf.mailtrigger.utils import gather_address_lists
+from ietf.utils.timezone import date_today, DEADLINE_TZINFO
 
 
 def email_state_changed(request, doc, text, mailtrigger_id=None):
@@ -191,7 +193,7 @@ def generate_ballot_rfceditornote(request, doc):
     return e
 
 def generate_last_call_announcement(request, doc):
-    expiration_date = datetime.date.today() + datetime.timedelta(days=14)
+    expiration_date = date_today(DEADLINE_TZINFO) + datetime.timedelta(days=14)
     if doc.group.type_id in ("individ", "area"):
         group = "an individual submitter"
         expiration_date += datetime.timedelta(days=14)
@@ -418,7 +420,7 @@ def generate_issue_ballot_mail(request, doc, ballot):
     
     e = doc.latest_event(LastCallDocEvent, type="sent_last_call")
     last_call_expires = e.expires if e else None
-    last_call_has_expired = last_call_expires and last_call_expires < datetime.datetime.now()
+    last_call_has_expired = last_call_expires and last_call_expires < timezone.now()
 
     return render_to_string("doc/mail/issue_iesg_ballot_mail.txt",
                             dict(doc=doc,
@@ -437,7 +439,7 @@ def _send_irsg_ballot_email(request, doc, ballot, subject, template):
     (to, cc) = gather_address_lists('irsg_ballot_issued', doc=doc)
     sender = 'IESG Secretary <iesg-secretary@ietf.org>'
 
-    ballot_expired = ballot.duedate < datetime.datetime.now()
+    ballot_expired = ballot.duedate < timezone.now()
     active_ballot = doc.active_ballot()
     if active_ballot is None:
         needed_bps = ''
diff --git a/ietf/doc/management/commands/generate_draft_aliases.py b/ietf/doc/management/commands/generate_draft_aliases.py
index 796e3db63..88f4aa98c 100755
--- a/ietf/doc/management/commands/generate_draft_aliases.py
+++ b/ietf/doc/management/commands/generate_draft_aliases.py
@@ -16,6 +16,7 @@ from tempfile import mkstemp
     
 from django.conf import settings
 from django.core.management.base import BaseCommand
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -101,7 +102,7 @@ class Command(BaseCommand):
             'that have seen activity in the last %s years.' % (DEFAULT_YEARS))
 
     def handle(self, *args, **options):
-        show_since = datetime.datetime.now() - datetime.timedelta(DEFAULT_YEARS*365)
+        show_since = timezone.now() - datetime.timedelta(DEFAULT_YEARS*365)
 
         date = time.strftime("%Y-%m-%d_%H:%M:%S")
         signature = '# Generated by %s at %s\n' % (os.path.abspath(__file__), date)
diff --git a/ietf/doc/management/commands/generate_draft_bibxml_files.py b/ietf/doc/management/commands/generate_draft_bibxml_files.py
index 9ac90523d..f2dc508b9 100644
--- a/ietf/doc/management/commands/generate_draft_bibxml_files.py
+++ b/ietf/doc/management/commands/generate_draft_bibxml_files.py
@@ -10,6 +10,7 @@ import sys
 
 from django.conf import settings
 from django.core.management.base import BaseCommand
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -68,7 +69,7 @@ class Command(BaseCommand):
         if process_all:
             doc_events = NewRevisionDocEvent.objects.filter(type='new_revision', doc__type_id='draft')
         else:
-            start = datetime.datetime.now() - datetime.timedelta(days=days)
+            start = timezone.now() - datetime.timedelta(days=days)
             doc_events = NewRevisionDocEvent.objects.filter(type='new_revision', doc__type_id='draft', time__gte=start)
         doc_events = doc_events.order_by('time')
 
diff --git a/ietf/doc/migrations/0046_use_timezone_now_for_doc_models.py b/ietf/doc/migrations/0046_use_timezone_now_for_doc_models.py
new file mode 100644
index 000000000..3749fd973
--- /dev/null
+++ b/ietf/doc/migrations/0046_use_timezone_now_for_doc_models.py
@@ -0,0 +1,39 @@
+# Generated by Django 2.2.28 on 2022-07-12 11:24
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('doc', '0045_docstates_chatlogs_polls'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='deletedevent',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AlterField(
+            model_name='docevent',
+            name='time',
+            field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, help_text='When the event happened'),
+        ),
+        migrations.AlterField(
+            model_name='dochistory',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AlterField(
+            model_name='document',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AlterField(
+            model_name='documentactionholder',
+            name='time_added',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+    ]
diff --git a/ietf/doc/migrations/0047_tzaware_deletedevents.py b/ietf/doc/migrations/0047_tzaware_deletedevents.py
new file mode 100644
index 000000000..bf258de6e
--- /dev/null
+++ b/ietf/doc/migrations/0047_tzaware_deletedevents.py
@@ -0,0 +1,61 @@
+# Generated by Django 2.2.28 on 2022-08-31 20:26
+
+import datetime
+import json
+
+from zoneinfo import ZoneInfo
+
+from django.db import migrations
+
+
+TZ_BEFORE = ZoneInfo('PST8PDT')
+
+
+def forward(apps, schema_editor):
+    DeletedEvent = apps.get_model('doc', 'DeletedEvent')
+    for deleted_event in DeletedEvent.objects.all():
+        fields = json.loads(deleted_event.json)
+        replacements = {}
+        for k, v in fields.items():
+            if isinstance(v, str):
+                try:
+                    dt = datetime.datetime.strptime(v, '%Y-%m-%d %H:%M:%S')
+                except:
+                    pass
+                else:
+                    replacements[k] = dt.replace(tzinfo=TZ_BEFORE).astimezone(datetime.timezone.utc).isoformat()
+        if len(replacements) > 0:
+            fields.update(replacements)
+            deleted_event.json = json.dumps(fields)
+            deleted_event.save()
+
+
+def reverse(apps, schema_editor):
+    DeletedEvent = apps.get_model('doc', 'DeletedEvent')
+    for deleted_event in DeletedEvent.objects.all():
+        fields = json.loads(deleted_event.json)
+        replacements = {}
+        for k, v in fields.items():
+            if isinstance(v, str) and 'T' in v:
+                try:
+                    dt = datetime.datetime.fromisoformat(v)
+                except:
+                    pass
+                else:
+                    replacements[k] = dt.astimezone(TZ_BEFORE).replace(tzinfo=None).strftime('%Y-%m-%d %H:%M:%S')
+        if len(replacements) > 0:
+            fields.update(replacements)
+            deleted_event.json = json.dumps(fields)
+            deleted_event.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('doc', '0046_use_timezone_now_for_doc_models'),
+        ('utils', '0003_pause_to_change_use_tz'),
+    ]
+
+    operations = [
+        migrations.RunPython(forward, reverse),
+    ]
diff --git a/ietf/doc/models.py b/ietf/doc/models.py
index 6512d4b54..c6e06fecd 100644
--- a/ietf/doc/models.py
+++ b/ietf/doc/models.py
@@ -18,6 +18,7 @@ from django.core.validators import URLValidator, RegexValidator
 from django.urls import reverse as urlreverse
 from django.contrib.contenttypes.models import ContentType
 from django.conf import settings
+from django.utils import timezone
 from django.utils.encoding import force_text
 from django.utils.html import mark_safe # type:ignore
 
@@ -35,6 +36,7 @@ from ietf.utils.decorators import memoize
 from ietf.utils.validators import validate_no_control_chars
 from ietf.utils.mail import formataddr
 from ietf.utils.models import ForeignKey
+from ietf.utils.timezone import date_today, RPC_TZINFO
 if TYPE_CHECKING:
     # importing other than for type checking causes errors due to cyclic imports
     from ietf.meeting.models import ProceedingsMaterial, Session
@@ -85,7 +87,7 @@ IESG_SUBSTATE_TAGS = ('ad-f-up', 'need-rev', 'extpty')
 
 class DocumentInfo(models.Model):
     """Any kind of document.  Draft, RFC, Charter, IPR Statement, Liaison Statement"""
-    time = models.DateTimeField(default=datetime.datetime.now) # should probably have auto_now=True
+    time = models.DateTimeField(default=timezone.now) # should probably have auto_now=True
 
     type = ForeignKey(DocTypeName, blank=True, null=True) # Draft, Agenda, Minutes, Charter, Discuss, Guideline, Email, Review, Issue, Wiki, External ...
     title = models.CharField(max_length=255, validators=[validate_no_control_chars, ])
@@ -682,7 +684,7 @@ class DocumentActionHolder(models.Model):
     """Action holder for a document"""
     document = ForeignKey('Document')
     person = ForeignKey(Person)
-    time_added = models.DateTimeField(default=datetime.datetime.now)
+    time_added = models.DateTimeField(default=timezone.now)
 
     CLEAR_ACTION_HOLDERS_STATES = ['approved', 'ann', 'rfcqueue', 'pub', 'dead']  # draft-iesg state slugs
     GROUP_ROLES_OF_INTEREST = ['chair', 'techadv', 'editor', 'secr']
@@ -829,16 +831,20 @@ class Document(DocumentInfo):
     def telechat_date(self, e=None):
         if not e:
             e = self.latest_event(TelechatDocEvent, type="scheduled_for_telechat")
-        return e.telechat_date if e and e.telechat_date and e.telechat_date >= datetime.date.today() else None
+        return e.telechat_date if e and e.telechat_date and e.telechat_date >= date_today(settings.TIME_ZONE) else None
 
     def past_telechat_date(self):
         "Return the latest telechat date if it isn't in the future; else None"
         e = self.latest_event(TelechatDocEvent, type="scheduled_for_telechat")
-        return e.telechat_date if e and e.telechat_date and e.telechat_date < datetime.date.today() else None
+        return e.telechat_date if e and e.telechat_date and e.telechat_date < date_today(settings.TIME_ZONE) else None
 
     def previous_telechat_date(self):
         "Return the most recent telechat date in the past, if any (even if there's another in the future)"
-        e = self.latest_event(TelechatDocEvent, type="scheduled_for_telechat", telechat_date__lt=datetime.datetime.now())
+        e = self.latest_event(
+            TelechatDocEvent,
+            type="scheduled_for_telechat",
+            telechat_date__lt=date_today(settings.TIME_ZONE),
+        )
         return e.telechat_date if e else None
 
     def request_closed_time(self, review_req):
@@ -904,14 +910,21 @@ class Document(DocumentInfo):
     def future_presentations(self):
         """ returns related SessionPresentation objects for meetings that
             have not yet ended. This implementation allows for 2 week meetings """
-        candidate_presentations = self.sessionpresentation_set.filter(session__meeting__date__gte=datetime.date.today()-datetime.timedelta(days=15))
-        return sorted([pres for pres in candidate_presentations if pres.session.meeting.end_date()>=datetime.date.today()], key=lambda x:x.session.meeting.date)
+        candidate_presentations = self.sessionpresentation_set.filter(
+            session__meeting__date__gte=date_today() - datetime.timedelta(days=15)
+        )
+        return sorted(
+            [pres for pres in candidate_presentations
+             if pres.session.meeting.end_date() >= date_today()],
+            key=lambda x:x.session.meeting.date,
+        )
 
     def last_presented(self):
         """ returns related SessionPresentation objects for the most recent meeting in the past"""
         # Assumes no two meetings have the same start date - if the assumption is violated, one will be chosen arbitrariy
-        candidate_presentations = self.sessionpresentation_set.filter(session__meeting__date__lte=datetime.date.today())
-        candidate_meetings = set([p.session.meeting for p in candidate_presentations if p.session.meeting.end_date()<datetime.date.today()])
+        today = date_today()
+        candidate_presentations = self.sessionpresentation_set.filter(session__meeting__date__lte=today)
+        candidate_meetings = set([p.session.meeting for p in candidate_presentations if p.session.meeting.end_date()<today])
         if candidate_meetings:
             mtg = sorted(list(candidate_meetings),key=lambda x:x.date,reverse=True)[0]
             return self.sessionpresentation_set.filter(session__meeting=mtg)
@@ -924,13 +937,18 @@ class Document(DocumentInfo):
         return s
 
     def pub_date(self):
-        """This is the rfc publication date (datetime) for RFCs, 
-        and the new-revision datetime for other documents."""
+        """Get the publication date for this document
+
+        This is the rfc publication date for RFCs, and the new-revision date for other documents.
+        """
         if self.get_state_slug() == "rfc":
+            # As of Sept 2022, in ietf.sync.rfceditor.update_docs_from_rfc_index() `published_rfc` events are
+            # created with a timestamp whose date *in the PST8PDT timezone* is the official publication date
+            # assigned by the RFC editor.
             event = self.latest_event(type='published_rfc')
         else:
             event = self.latest_event(type='new_revision')
-        return event.time
+        return event.time.astimezone(RPC_TZINFO).date() if event else None
 
     def is_dochistory(self):
         return False
@@ -955,7 +973,7 @@ class Document(DocumentInfo):
             elif rev_events.exists():
                 time = rev_events.first().time
             else:
-                time = datetime.datetime.fromtimestamp(0)
+                time = datetime.datetime.fromtimestamp(0, datetime.timezone.utc)
             dh = DocHistory(name=self.name, rev=rev, doc=self, time=time, type=self.type, title=self.title,
                              stream=self.stream, group=self.group)
 
@@ -1208,7 +1226,7 @@ EVENT_TYPES = [
 
 class DocEvent(models.Model):
     """An occurrence for a document, used for tracking who, when and what."""
-    time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened", db_index=True)
+    time = models.DateTimeField(default=timezone.now, help_text="When the event happened", db_index=True)
     type = models.CharField(max_length=50, choices=EVENT_TYPES)
     by = ForeignKey(Person)
     doc = ForeignKey(Document)
@@ -1388,7 +1406,7 @@ class DeletedEvent(models.Model):
     content_type = ForeignKey(ContentType)
     json = models.TextField(help_text="Deleted object in JSON format, with attribute names chosen to be suitable for passing into the relevant create method.")
     by = ForeignKey(Person)
-    time = models.DateTimeField(default=datetime.datetime.now)
+    time = models.DateTimeField(default=timezone.now)
 
     def __str__(self):
         return u"%s by %s %s" % (self.content_type, self.by, self.time)
diff --git a/ietf/doc/templatetags/ballot_icon.py b/ietf/doc/templatetags/ballot_icon.py
index ee6be139c..8f9aa9ce8 100644
--- a/ietf/doc/templatetags/ballot_icon.py
+++ b/ietf/doc/templatetags/ballot_icon.py
@@ -38,6 +38,7 @@ import debug      # pyflakes:ignore
 from django import template
 from django.urls import reverse as urlreverse
 from django.db.models import Q
+from django.utils import timezone
 from django.utils.safestring import mark_safe
 
 from ietf.ietfauth.utils import user_is_person, has_role
@@ -173,17 +174,17 @@ def state_age_colored(doc):
         if iesg_state in ["dead", "watching", "pub", "idexists"]:
             return ""
         try:
-            state_date = (
+            state_datetime = (
                 doc.docevent_set.filter(
                     Q(type="started_iesg_process")
                     | Q(type="changed_state", statedocevent__state_type="draft-iesg")
                 )
                 .order_by("-time")[0]
-                .time.date()
+                .time
             )
         except IndexError:
-            state_date = datetime.date(1990, 1, 1)
-        days = (datetime.date.today() - state_date).days
+            state_datetime = datetime.datetime(1990, 1, 1, tzinfo=datetime.timezone.utc)
+        days = (timezone.now() - state_datetime).days
         # loosely based on
         # https://trac.ietf.org/trac/iesg/wiki/PublishPath
         if iesg_state == "lc":
diff --git a/ietf/doc/templatetags/ietf_filters.py b/ietf/doc/templatetags/ietf_filters.py
index c309a7ebd..e7e5a4117 100644
--- a/ietf/doc/templatetags/ietf_filters.py
+++ b/ietf/doc/templatetags/ietf_filters.py
@@ -18,6 +18,7 @@ from django.urls import reverse as urlreverse
 from django.core.cache import cache
 from django.core.exceptions import ValidationError
 from django.urls import NoReverseMatch
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -318,7 +319,7 @@ def timesince_days(date):
     """Returns the number of days since 'date' (relative to now)"""
     if date.__class__ is not datetime.datetime:
         date = datetime.datetime(date.year, date.month, date.day)
-    delta = datetime.datetime.now() - date
+    delta = timezone.now() - date
     return delta.days
 
 @register.filter
@@ -637,19 +638,19 @@ def action_holder_badge(action_holder):
     >>> action_holder_badge(DocumentActionHolderFactory())
     ''
 
-    >>> action_holder_badge(DocumentActionHolderFactory(time_added=datetime.datetime.now() - datetime.timedelta(days=15)))
+    >>> action_holder_badge(DocumentActionHolderFactory(time_added=timezone.now() - datetime.timedelta(days=15)))
     ''
 
-    >>> action_holder_badge(DocumentActionHolderFactory(time_added=datetime.datetime.now() - datetime.timedelta(days=16)))
+    >>> action_holder_badge(DocumentActionHolderFactory(time_added=timezone.now() - datetime.timedelta(days=16)))
     '<span class="badge rounded-pill bg-danger" title="In state for 16 days; goal is &lt;15 days."><i class="bi bi-clock-fill"></i> 16</span>'
 
-    >>> action_holder_badge(DocumentActionHolderFactory(time_added=datetime.datetime.now() - datetime.timedelta(days=30)))
+    >>> action_holder_badge(DocumentActionHolderFactory(time_added=timezone.now() - datetime.timedelta(days=30)))
     '<span class="badge rounded-pill bg-danger" title="In state for 30 days; goal is &lt;15 days."><i class="bi bi-clock-fill"></i> 30</span>'
 
     >>> settings.DOC_ACTION_HOLDER_AGE_LIMIT_DAYS = old_limit
     """
     age_limit = settings.DOC_ACTION_HOLDER_AGE_LIMIT_DAYS
-    age = (datetime.datetime.now() - action_holder.time_added).days
+    age = (timezone.now() - action_holder.time_added).days
     if age > age_limit:
         return mark_safe(
             '<span class="badge rounded-pill bg-danger" title="In state for %d day%s; goal is &lt;%d days."><i class="bi bi-clock-fill"></i> %d</span>'
diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py
index e7f8ad21c..19e3669fb 100644
--- a/ietf/doc/tests.py
+++ b/ietf/doc/tests.py
@@ -18,6 +18,7 @@ from pyquery import PyQuery
 from urllib.parse import urlparse, parse_qs
 from tempfile import NamedTemporaryFile
 from collections import defaultdict
+from zoneinfo import ZoneInfo
 
 from django.core.management import call_command
 from django.urls import reverse as urlreverse
@@ -25,6 +26,7 @@ from django.conf import settings
 from django.forms import Form
 from django.utils.html import escape
 from django.test import override_settings
+from django.utils import timezone
 from django.utils.text import slugify
 
 from tastypie.test import ResourceTestCaseMixin
@@ -56,6 +58,8 @@ from ietf.utils.mail import outbox, empty_outbox
 from ietf.utils.test_utils import login_testing_unauthorized, unicontent, reload_db_objects
 from ietf.utils.test_utils import TestCase
 from ietf.utils.text import normalize_text
+from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO, RPC_TZINFO
+
 
 class SearchTests(TestCase):
     def test_search(self):
@@ -385,13 +389,13 @@ class SearchTests(TestCase):
         # Three drafts to show with various warnings
         drafts = WgDraftFactory.create_batch(3,states=[('draft','active'),('draft-iesg','ad-eval')])
         for index, draft in enumerate(drafts):
-            StateDocEventFactory(doc=draft, state=('draft-iesg','ad-eval'), time=datetime.datetime.now()-datetime.timedelta(days=[1,15,29][index]))
+            StateDocEventFactory(doc=draft, state=('draft-iesg','ad-eval'), time=timezone.now()-datetime.timedelta(days=[1,15,29][index]))
             draft.action_holders.set([PersonFactory()])
 
         # And one draft that should not show (with the default of 7 days to view)
         old = WgDraftFactory()
-        old.docevent_set.filter(newrevisiondocevent__isnull=False).update(time=datetime.datetime.now()-datetime.timedelta(days=8))
-        StateDocEventFactory(doc=old, time=datetime.datetime.now()-datetime.timedelta(days=8))
+        old.docevent_set.filter(newrevisiondocevent__isnull=False).update(time=timezone.now()-datetime.timedelta(days=8))
+        StateDocEventFactory(doc=old, time=timezone.now()-datetime.timedelta(days=8))
 
         url = urlreverse('ietf.doc.views_search.recent_drafts')
         r = self.client.get(url)
@@ -764,7 +768,7 @@ Man                    Expires September 22, 2015               [Page 3]
 
         replacement = WgDraftFactory(
             name="draft-ietf-replacement",
-            time=datetime.datetime.now(),
+            time=timezone.now(),
             title="Replacement Draft",
             stream_id=draft.stream_id, group_id=draft.group_id, abstract=draft.abstract,stream=draft.stream, rev=draft.rev,
             pages=draft.pages, intended_std_level_id=draft.intended_std_level_id,
@@ -1427,6 +1431,8 @@ Man                    Expires September 22, 2015               [Page 3]
 
     def test_draft_group_link(self):
         """Link to group 'about' page should have correct format"""
+        event_datetime = datetime.datetime(2010, 10, 10, tzinfo=RPC_TZINFO)
+
         for group_type_id in ['wg', 'rg', 'ag']:
             group = GroupFactory(type_id=group_type_id)
             draft = WgDraftFactory(name='draft-document-%s' % group_type_id, group=group)
@@ -1435,7 +1441,7 @@ Man                    Expires September 22, 2015               [Page 3]
             self.assert_correct_wg_group_link(r, group)
 
             rfc = WgRfcFactory(name='draft-rfc-document-%s' % group_type_id, group=group)
-            DocEventFactory.create(doc=rfc, type='published_rfc', time = '2010-10-10')
+            DocEventFactory.create(doc=rfc, type='published_rfc', time=event_datetime)
             # get the rfc name to avoid a redirect
             rfc_name = rfc.docalias.filter(name__startswith='rfc').first().name
             r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc_name)))
@@ -1450,7 +1456,7 @@ Man                    Expires September 22, 2015               [Page 3]
             self.assert_correct_non_wg_group_link(r, group)
 
             rfc = WgRfcFactory(name='draft-rfc-document-%s' % group_type_id, group=group)
-            DocEventFactory.create(doc=rfc, type='published_rfc', time = '2010-10-10')
+            DocEventFactory.create(doc=rfc, type='published_rfc', time=event_datetime)
             # get the rfc name to avoid a redirect
             rfc_name = rfc.docalias.filter(name__startswith='rfc').first().name
             r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=rfc_name)))
@@ -1586,7 +1592,7 @@ class DocTestCase(TestCase):
             name = "session-72-mars-1",
             meeting = Meeting.objects.get(number='72'),
             group = Group.objects.get(acronym='mars'),
-            modified = datetime.datetime.now(),
+            modified = timezone.now(),
             add_to_schedule=False,
         )
         SchedulingEvent.objects.create(
@@ -1616,7 +1622,7 @@ class DocTestCase(TestCase):
             type="changed_ballot_position",
             pos_id="yes",
             comment="Looks fine to me",
-            comment_time=datetime.datetime.now(),
+            comment_time=timezone.now(),
             balloter=Person.objects.get(user__username="ad"),
             by=Person.objects.get(name="(System)"))
 
@@ -1650,7 +1656,7 @@ class DocTestCase(TestCase):
             type="changed_ballot_position",
             pos_id="noobj",
             comment="Still looks okay to me",
-            comment_time=datetime.datetime.now(),
+            comment_time=timezone.now(),
             balloter=Person.objects.get(user__username="ad"),
             by=Person.objects.get(name="(System)"))
 
@@ -1672,7 +1678,7 @@ class DocTestCase(TestCase):
                 type="changed_ballot_position",
                 pos_id="yes",
                 comment="Looks fine to me",
-                comment_time=datetime.datetime.now(),
+                comment_time=timezone.now(),
                 balloter=Person.objects.get(user__username="ad"),
                 by=Person.objects.get(name="(System)"))
 
@@ -1842,7 +1848,7 @@ class DocTestCase(TestCase):
             desc="Last call\x0b",  # include a control character to be sure it does not break anything
             type="sent_last_call",
             by=Person.objects.get(user__username="secretary"),
-            expires=datetime.date.today() + datetime.timedelta(days=7))
+            expires=datetime_today(DEADLINE_TZINFO) + datetime.timedelta(days=7))
 
         r = self.client.get("/feed/last-call/")
         self.assertEqual(r.status_code, 200)
@@ -1890,10 +1896,14 @@ class DocTestCase(TestCase):
                   #other_aliases = ['rfc6020',],
                   states = [('draft','rfc'),('draft-iesg','pub')],
                   std_level_id = 'ps',
-                  time = datetime.datetime(2010,10,10),
+                  time = datetime.datetime(2010, 10, 10, tzinfo=ZoneInfo(settings.TIME_ZONE)),
               )
         num = rfc.rfc_number()
-        DocEventFactory.create(doc=rfc, type='published_rfc', time = '2010-10-10')
+        DocEventFactory.create(
+            doc=rfc,
+            type='published_rfc',
+            time=datetime.datetime(2010, 10, 10, tzinfo=RPC_TZINFO),
+        )
         #
         url = urlreverse('ietf.doc.views_doc.document_bibtex', kwargs=dict(name=rfc.name))
         r = self.client.get(url)
@@ -1911,10 +1921,14 @@ class DocTestCase(TestCase):
                   stream_id =       'ise',
                   states =          [('draft','rfc'),('draft-iesg','pub')],
                   std_level_id =    'inf',
-                  time =            datetime.datetime(1990,0o4,0o1),
+                  time =            datetime.datetime(1990, 4, 1, tzinfo=ZoneInfo(settings.TIME_ZONE)),
               )
         num = april1.rfc_number()
-        DocEventFactory.create(doc=april1, type='published_rfc', time = '1990-04-01')
+        DocEventFactory.create(
+            doc=april1,
+            type='published_rfc',
+            time=datetime.datetime(1990, 4, 1, tzinfo=RPC_TZINFO),
+        )
         #
         url = urlreverse('ietf.doc.views_doc.document_bibtex', kwargs=dict(name=april1.name))
         r = self.client.get(url)
@@ -2049,7 +2063,8 @@ class GenerateDraftAliasesTests(TestCase):
        super().tearDown()
 
    def testManagementCommand(self):
-       a_month_ago = datetime.datetime.now() - datetime.timedelta(30)
+       a_month_ago = (timezone.now() - datetime.timedelta(30)).astimezone(RPC_TZINFO)
+       a_month_ago = a_month_ago.replace(hour=0, minute=0, second=0, microsecond=0)
        ad = RoleFactory(name_id='ad', group__type_id='area', group__state_id='active').person
        shepherd = PersonFactory()
        author1 = PersonFactory()
@@ -2064,9 +2079,9 @@ class GenerateDraftAliasesTests(TestCase):
        doc1 = IndividualDraftFactory(authors=[author1], shepherd=shepherd.email(), ad=ad)
        doc2 = WgDraftFactory(name='draft-ietf-mars-test', group__acronym='mars', authors=[author2], ad=ad)
        doc3 = WgRfcFactory.create(name='draft-ietf-mars-finished', group__acronym='mars', authors=[author3], ad=ad, std_level_id='ps', states=[('draft','rfc'),('draft-iesg','pub')], time=a_month_ago)
-       DocEventFactory.create(doc=doc3, type='published_rfc', time=a_month_ago.strftime("%Y-%m-%d"))
-       doc4 = WgRfcFactory.create(authors=[author4,author5], ad=ad, std_level_id='ps', states=[('draft','rfc'),('draft-iesg','pub')], time=datetime.datetime(2010,10,10))
-       DocEventFactory.create(doc=doc4, type='published_rfc', time = '2010-10-10')
+       DocEventFactory.create(doc=doc3, type='published_rfc', time=a_month_ago)
+       doc4 = WgRfcFactory.create(authors=[author4,author5], ad=ad, std_level_id='ps', states=[('draft','rfc'),('draft-iesg','pub')], time=datetime.datetime(2010,10,10, tzinfo=ZoneInfo(settings.TIME_ZONE)))
+       DocEventFactory.create(doc=doc4, type='published_rfc', time=datetime.datetime(2010, 10, 10, tzinfo=RPC_TZINFO))
        doc5 = IndividualDraftFactory(authors=[author6])
 
        args = [ ]
@@ -2217,7 +2232,7 @@ class DocumentMeetingTests(TestCase):
         self.other_chair = PersonFactory()
         self.other_group.role_set.create(name_id='chair',person=self.other_chair,email=self.other_chair.email())
 
-        today = datetime.date.today()
+        today = date_today()
         cut_days = settings.MEETING_MATERIALS_DEFAULT_SUBMISSION_CORRECTION_DAYS
         self.past_cutoff = SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=1+cut_days))
         self.past = SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=cut_days/2))
diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py
index 317e4e3a1..8b8d1c1b1 100644
--- a/ietf/doc/tests_ballot.py
+++ b/ietf/doc/tests_ballot.py
@@ -12,6 +12,7 @@ import debug                            # pyflakes:ignore
 from django.test import RequestFactory
 from django.utils.text import slugify
 from django.urls import reverse as urlreverse
+from django.utils import timezone
 
 from ietf.doc.models import (Document, State, DocEvent,
                              BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, TelechatDocEvent)
@@ -30,6 +31,7 @@ from ietf.person.utils import get_active_ads
 from ietf.utils.test_utils import TestCase, login_testing_unauthorized
 from ietf.utils.mail import outbox, empty_outbox, get_payload_text
 from ietf.utils.text import unwrap
+from ietf.utils.timezone import date_today
 
 
 class EditPositionTests(TestCase):
@@ -105,7 +107,7 @@ class EditPositionTests(TestCase):
         draft = WgDraftFactory(ad=ad)
         url = urlreverse('ietf.doc.views_ballot.api_set_position')
         create_ballot_if_not_open(None, draft, ad, 'approve')
-        ad.user.last_login = datetime.datetime.now()
+        ad.user.last_login = timezone.now()
         ad.user.save()
         apikey = PersonalApiKey.objects.create(endpoint=url, person=ad)
 
@@ -238,9 +240,9 @@ class EditPositionTests(TestCase):
             doc=draft, rev=draft.rev, type="changed_ballot_position",
             by=ad, balloter=ad, ballot=ballot, pos=BallotPositionName.objects.get(slug="discuss"),
             discuss="This draft seems to be lacking a clearer title?",
-            discuss_time=datetime.datetime.now(),
+            discuss_time=timezone.now(),
             comment="Test!",
-            comment_time=datetime.datetime.now())
+            comment_time=timezone.now())
         
         url = urlreverse('ietf.doc.views_ballot.send_ballot_comment', kwargs=dict(name=draft.name,
                                                                 ballot_id=ballot.pk))
@@ -466,7 +468,7 @@ class BallotWriteupsTests(TestCase):
                     doc=draft,
                     rev=draft.rev,
                     desc='issued last call',
-                    expires = datetime.datetime.now()+datetime.timedelta(days = 1 if case=='future' else -1)
+                    expires = timezone.now()+datetime.timedelta(days = 1 if case=='future' else -1)
                 )
             url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name))
             login_testing_unauthorized(self, "ad", url)
@@ -791,7 +793,7 @@ class ApproveBallotTests(TestCase):
                   doc=draft,
                   rev=draft.rev,
                   desc='issued last call',
-                  expires = datetime.datetime.now()-datetime.timedelta(days=14) )
+                  expires = timezone.now()-datetime.timedelta(days=14) )
         WriteupDocEvent.objects.create(
                   by=Person.objects.get(name='(System)'),
                   doc=draft,
@@ -898,7 +900,7 @@ class MakeLastCallTests(TestCase):
 
         mailbox_before = len(outbox)
 
-        last_call_sent_date = datetime.date.today()
+        last_call_sent_date = date_today()
         expire_date = last_call_sent_date+datetime.timedelta(days=14)
         
         r = self.client.post(url,
@@ -1117,7 +1119,7 @@ class RegenerateLastCallTestCase(TestCase):
 
 class BallotContentTests(TestCase):
     def test_ballotpositiondocevent_any_email_sent(self):
-        now = datetime.datetime.now()  # be sure event timestamps are at distinct times
+        now = timezone.now()  # be sure event timestamps are at distinct times
         bpde_with_null_send_email = BallotPositionDocEventFactory(
             time=now - datetime.timedelta(minutes=30),
             send_email=None,
@@ -1219,7 +1221,7 @@ class BallotContentTests(TestCase):
             balloter=balloters[0],
             pos_id='discuss',
             discuss='Discussion text',
-            discuss_time=datetime.datetime.now(),
+            discuss_time=timezone.now(),
             send_email=True,
         )
         BallotPositionDocEventFactory(
@@ -1227,7 +1229,7 @@ class BallotContentTests(TestCase):
             balloter=balloters[1],
             pos_id='noobj',
             comment='Commentary',
-            comment_time=datetime.datetime.now(),
+            comment_time=timezone.now(),
             send_email=True,
         )
 
@@ -1237,7 +1239,7 @@ class BallotContentTests(TestCase):
             balloter=balloters[2],
             pos_id='discuss',
             discuss='Discussion text',
-            discuss_time=datetime.datetime.now(),
+            discuss_time=timezone.now(),
             send_email=False,
         )
         BallotPositionDocEventFactory(
@@ -1245,7 +1247,7 @@ class BallotContentTests(TestCase):
             balloter=balloters[3],
             pos_id='noobj',
             comment='Commentary',
-            comment_time=datetime.datetime.now(),
+            comment_time=timezone.now(),
             send_email=False,
         )
 
@@ -1255,7 +1257,7 @@ class BallotContentTests(TestCase):
             balloter=balloters[4],
             pos_id='discuss',
             discuss='Discussion text',
-            discuss_time=datetime.datetime.now() - datetime.timedelta(days=1),
+            discuss_time=timezone.now() - datetime.timedelta(days=1),
             send_email=True,
         )
         BallotPositionDocEventFactory(
@@ -1263,7 +1265,7 @@ class BallotContentTests(TestCase):
             balloter=balloters[4],
             pos_id='discuss',
             discuss='Discussion text',
-            discuss_time=datetime.datetime.now(),
+            discuss_time=timezone.now(),
             send_email=False,
         )
         BallotPositionDocEventFactory(
@@ -1271,7 +1273,7 @@ class BallotContentTests(TestCase):
             balloter=balloters[5],
             pos_id='noobj',
             comment='Commentary',
-            comment_time=datetime.datetime.now() - datetime.timedelta(days=1),
+            comment_time=timezone.now() - datetime.timedelta(days=1),
             send_email=True,
         )
         BallotPositionDocEventFactory(
@@ -1279,7 +1281,7 @@ class BallotContentTests(TestCase):
             balloter=balloters[5],
             pos_id='noobj',
             comment='Commentary',
-            comment_time=datetime.datetime.now(),
+            comment_time=timezone.now(),
             send_email=False,
         )
 
@@ -1296,7 +1298,7 @@ class BallotContentTests(TestCase):
             balloter__plain='plain name1',
             pos_id='discuss',
             discuss='Discussion text',
-            discuss_time=datetime.datetime.now(),
+            discuss_time=timezone.now(),
             send_email=False,
         ).balloter
         send_email_balloter = BallotPositionDocEventFactory(
@@ -1304,7 +1306,7 @@ class BallotContentTests(TestCase):
             balloter__plain='plain name2',
             pos_id='discuss',
             discuss='Discussion text',
-            discuss_time=datetime.datetime.now(),
+            discuss_time=timezone.now(),
             send_email=True,
         ).balloter
         prev_send_email_balloter = BallotPositionDocEventFactory(
@@ -1312,7 +1314,7 @@ class BallotContentTests(TestCase):
             balloter__plain='plain name3',
             pos_id='discuss',
             discuss='Discussion text',
-            discuss_time=datetime.datetime.now() - datetime.timedelta(days=1),
+            discuss_time=timezone.now() - datetime.timedelta(days=1),
             send_email=True,
         ).balloter
         BallotPositionDocEventFactory(
@@ -1320,7 +1322,7 @@ class BallotContentTests(TestCase):
             balloter=prev_send_email_balloter,
             pos_id='discuss',
             discuss='Discussion text',
-            discuss_time=datetime.datetime.now(),
+            discuss_time=timezone.now(),
             send_email=False,
         )
 
@@ -1351,7 +1353,7 @@ class BallotContentTests(TestCase):
             balloter=balloters[0],
             pos_id='discuss',
             discuss='Discussion text',
-            discuss_time=datetime.datetime.now(),
+            discuss_time=timezone.now(),
             send_email=None,
         )
         BallotPositionDocEventFactory(
@@ -1359,7 +1361,7 @@ class BallotContentTests(TestCase):
             balloter=balloters[1],
             pos_id='noobj',
             comment='Commentary',
-            comment_time=datetime.datetime.now(),
+            comment_time=timezone.now(),
             send_email=None,
         )
         old_balloter = BallotPositionDocEventFactory(
@@ -1367,7 +1369,7 @@ class BallotContentTests(TestCase):
             balloter__plain='plain name',  # ensure plain name is slugifiable
             pos_id='discuss',
             discuss='Discussion text',
-            discuss_time=datetime.datetime.now(),
+            discuss_time=timezone.now(),
             send_email=None,
         ).balloter
 
diff --git a/ietf/doc/tests_bofreq.py b/ietf/doc/tests_bofreq.py
index 4d02108d0..9925ec3d1 100644
--- a/ietf/doc/tests_bofreq.py
+++ b/ietf/doc/tests_bofreq.py
@@ -14,6 +14,7 @@ from html import unescape
 from django.conf import settings
 from django.urls import reverse as urlreverse
 from django.template.loader import render_to_string
+from django.utils import timezone
 
 from ietf.group.factories import RoleFactory
 from ietf.doc.factories import BofreqFactory, NewRevisionDocEventFactory
@@ -48,7 +49,7 @@ This test section has some text.
         states = State.objects.filter(type_id='bofreq')
         self.assertTrue(states.count()>0)
         for i in range(3*len(states)):
-           BofreqFactory(states=[('bofreq',states[i%len(states)].slug)],newrevisiondocevent__time=datetime.datetime.today()-datetime.timedelta(days=randint(0,20)))
+           BofreqFactory(states=[('bofreq',states[i%len(states)].slug)],newrevisiondocevent__time=timezone.now()-datetime.timedelta(days=randint(0,20)))
         r = self.client.get(url)
         self.assertEqual(r.status_code, 200)
         q = PyQuery(r.content)
diff --git a/ietf/doc/tests_charter.py b/ietf/doc/tests_charter.py
index 8732b7701..c420fdd0a 100644
--- a/ietf/doc/tests_charter.py
+++ b/ietf/doc/tests_charter.py
@@ -26,6 +26,8 @@ from ietf.person.models import Person
 from ietf.utils.test_utils import TestCase
 from ietf.utils.mail import outbox, empty_outbox, get_payload_text
 from ietf.utils.test_utils import login_testing_unauthorized
+from ietf.utils.timezone import datetime_today, date_today, DEADLINE_TZINFO
+
 
 class ViewCharterTests(TestCase):
     def test_view_revisions(self):
@@ -402,7 +404,7 @@ class EditCharterTests(TestCase):
 
         # Make it so that the charter has been through internal review, and passed its external review
         # ballot on a previous telechat 
-        last_week = datetime.date.today()-datetime.timedelta(days=7)
+        last_week = datetime_today(DEADLINE_TZINFO) - datetime.timedelta(days=7)
         BallotDocEvent.objects.create(type='created_ballot',by=login,doc=charter, rev=charter.rev,
                                       ballot_type=BallotType.objects.get(doc_type=charter.type,slug='r-extrev'),
                                       time=last_week)
@@ -746,7 +748,7 @@ class EditCharterTests(TestCase):
 
         charter.set_state(State.objects.get(used=True, type="charter", slug="iesgrev"))
 
-        due_date = datetime.date.today() + datetime.timedelta(days=180)
+        due_date = date_today(DEADLINE_TZINFO) + datetime.timedelta(days=180)
         m1 = GroupMilestone.objects.create(group=group,
                                            state_id="active",
                                            desc="Has been copied",
@@ -826,7 +828,7 @@ class EditCharterTests(TestCase):
         m = GroupMilestone.objects.create(group=charter.group,
                                           state_id="active",
                                           desc="Test milestone",
-                                          due=datetime.date.today(),
+                                          due=date_today(DEADLINE_TZINFO),
                                           resolved="")
 
         url = urlreverse('ietf.doc.views_charter.charter_with_milestones_txt', kwargs=dict(name=charter.name, rev=charter.rev))
diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py
index 1adf243e2..518bff377 100644
--- a/ietf/doc/tests_draft.py
+++ b/ietf/doc/tests_draft.py
@@ -13,6 +13,7 @@ from pyquery import PyQuery
 
 from django.urls import reverse as urlreverse
 from django.conf import settings
+from django.utils import timezone
 from django.utils.html import escape
 
 import debug                            # pyflakes:ignore
@@ -33,6 +34,7 @@ from ietf.iesg.models import TelechatDate
 from ietf.utils.test_utils import login_testing_unauthorized
 from ietf.utils.mail import outbox, empty_outbox, get_payload_text
 from ietf.utils.test_utils import TestCase
+from ietf.utils.timezone import date_today, datetime_from_date
 
 
 class ChangeStateTests(TestCase):
@@ -401,11 +403,11 @@ class EditInfoTests(TestCase):
 
         # change to a telechat that should cause returning item to be auto-detected
         # First, make it appear that the previous telechat has already passed
-        telechat_event.telechat_date = datetime.date.today()-datetime.timedelta(days=7)
+        telechat_event.telechat_date = date_today() - datetime.timedelta(days=7)
         telechat_event.save()
         ad = Person.objects.get(user__username="ad")
         ballot = create_ballot_if_not_open(None, draft, ad, 'approve')
-        ballot.time = telechat_event.telechat_date
+        ballot.time = datetime_from_date(telechat_event.telechat_date)
         ballot.save()
 
         r = self.client.post(url, data)
@@ -428,7 +430,7 @@ class EditInfoTests(TestCase):
         self.assertTrue("Telechat update" in outbox[-1]['Subject'])
 
         # Put it on an agenda that's very soon from now
-        next_week = datetime.date.today()+datetime.timedelta(days=7)
+        next_week = date_today() + datetime.timedelta(days=7)
         td =  TelechatDate.objects.active()[0]
         td.date = next_week
         td.save()
@@ -618,7 +620,7 @@ class ResurrectTests(DraftFileMixin, TestCase):
         self.assertEqual(draft.docevent_set.count(), events_before + 1)
         self.assertEqual(draft.latest_event().type, "completed_resurrect")
         self.assertEqual(draft.get_state_slug(), "active")
-        self.assertTrue(draft.expires >= datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE - 1))
+        self.assertTrue(draft.expires >= timezone.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE - 1))
         self.assertEqual(len(outbox), mailbox_before + 1)
         self.assertTrue('Resurrection Completed' in outbox[-1]['Subject'])
         self.assertTrue('iesg-secretary' in outbox[-1]['To'])
@@ -638,7 +640,7 @@ class ExpireIDsTests(DraftFileMixin, TestCase):
 
         meeting = Meeting.objects.create(number="123",
                                type=MeetingTypeName.objects.get(slug="ietf"),
-                               date=datetime.date.today())
+                               date=date_today())
         second_cut_off = meeting.get_second_cut_off()
         ietf_monday = meeting.get_ietf_monday()
 
@@ -659,7 +661,7 @@ class ExpireIDsTests(DraftFileMixin, TestCase):
 
         # hack into expirable state
         draft.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
-        draft.expires = datetime.datetime.now() + datetime.timedelta(days=10)
+        draft.expires = timezone.now() + datetime.timedelta(days=10)
         draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")])
 
         self.assertEqual(len(list(get_soon_to_expire_drafts(14))), 1)
@@ -698,7 +700,7 @@ class ExpireIDsTests(DraftFileMixin, TestCase):
         
         # hack into expirable state
         draft.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
-        draft.expires = datetime.datetime.now()
+        draft.expires = timezone.now()
         draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")])
 
         self.assertEqual(len(list(get_expired_drafts())), 1)
@@ -741,7 +743,7 @@ class ExpireIDsTests(DraftFileMixin, TestCase):
 
         draft.delete()
 
-        rgdraft = RgDraftFactory(expires=datetime.datetime.now())
+        rgdraft = RgDraftFactory(expires=timezone.now())
         self.assertEqual(len(list(get_expired_drafts())), 1)
         for slug in ('iesg-rev','irsgpoll'):
             rgdraft.set_state(State.objects.get(type_id='draft-stream-irtf',slug=slug))
@@ -791,7 +793,7 @@ class ExpireIDsTests(DraftFileMixin, TestCase):
 
         # expire draft
         draft.set_state(State.objects.get(used=True, type="draft", slug="expired"))
-        draft.expires = datetime.datetime.now() - datetime.timedelta(days=1)
+        draft.expires = timezone.now() - datetime.timedelta(days=1)
         draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")])
 
         e = DocEvent(doc=draft, rev=draft.rev, type= "expired_document", time=draft.expires,
@@ -824,7 +826,7 @@ class ExpireLastCallTests(TestCase):
 
         e = LastCallDocEvent(doc=draft, rev=draft.rev, type="sent_last_call", by=secretary)
         e.text = "Last call sent"
-        e.expires = datetime.datetime.now() + datetime.timedelta(days=14)
+        e.expires = timezone.now() + datetime.timedelta(days=14)
         e.save()
         
         self.assertEqual(len(list(get_expired_last_calls())), 0)
@@ -832,7 +834,7 @@ class ExpireLastCallTests(TestCase):
         # test expired
         e = LastCallDocEvent(doc=draft, rev=draft.rev, type="sent_last_call", by=secretary)
         e.text = "Last call sent"
-        e.expires = datetime.datetime.now()
+        e.expires = timezone.now()
         e.save()
         
         drafts = list(get_expired_last_calls())
@@ -866,7 +868,7 @@ class ExpireLastCallTests(TestCase):
         e = LastCallDocEvent(doc=draft, rev=draft.rev, type="sent_last_call", by=secretary)
         e.text = "Last call sent"
         e.desc = "Blah, blah, blah.\n\nThis document makes the following downward references (downrefs):\n  ** Downref: Normative reference to an Experimental RFC: RFC 4764"
-        e.expires = datetime.datetime.now()
+        e.expires = timezone.now()
         e.save()
         
         drafts = list(get_expired_last_calls())
@@ -1730,7 +1732,7 @@ class ChangeStreamStateTests(TestCase):
         self.assertEqual(draft.docevent_set.count() - events_before, 2)
         reminder = DocReminder.objects.filter(event__doc=draft, type="stream-s")
         self.assertEqual(len(reminder), 1)
-        due = datetime.datetime.now() + datetime.timedelta(weeks=10)
+        due = timezone.now() + datetime.timedelta(weeks=10)
         self.assertTrue(due - datetime.timedelta(days=1) <= reminder[0].due <= due + datetime.timedelta(days=1))
         self.assertEqual(len(outbox), 1)
         self.assertTrue("state changed" in outbox[0]["Subject"].lower())
@@ -1775,7 +1777,7 @@ class ChangeStreamStateTests(TestCase):
         self.assertEqual(draft.docevent_set.count() - events_before, 2)
         reminder = DocReminder.objects.filter(event__doc=draft, type="stream-s")
         self.assertEqual(len(reminder), 1)
-        due = datetime.datetime.now() + datetime.timedelta(weeks=10)
+        due = timezone.now() + datetime.timedelta(weeks=10)
         self.assertTrue(due - datetime.timedelta(days=1) <= reminder[0].due <= due + datetime.timedelta(days=1))
         self.assertEqual(len(outbox), 1)
         self.assertTrue("state changed" in outbox[0]["Subject"].lower())
@@ -1826,7 +1828,7 @@ class ChangeReplacesTests(TestCase):
             name="draft-test-base-b",
             title="Base B",
             group=mars_wg,
-            expires = datetime.datetime.now() - datetime.timedelta(days = 365 - settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
+            expires = timezone.now() - datetime.timedelta(days = 365 - settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
         )
         p = PersonFactory(name="baseb_author")
         e = Email.objects.create(address="baseb_author@example.com", person=p, origin=p.user.username)
diff --git a/ietf/doc/tests_irsg_ballot.py b/ietf/doc/tests_irsg_ballot.py
index f178bb4e5..da1b48fc6 100644
--- a/ietf/doc/tests_irsg_ballot.py
+++ b/ietf/doc/tests_irsg_ballot.py
@@ -19,6 +19,7 @@ from ietf.doc.utils import create_ballot_if_not_open, close_ballot
 from ietf.person.utils import get_active_irsg, get_active_ads
 from ietf.group.factories import RoleFactory
 from ietf.person.models import Person
+from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO
 
 
 class IssueIRSGBallotTests(TestCase):
@@ -254,7 +255,7 @@ class IssueIRSGBallotTests(TestCase):
         irsgmember = get_active_irsg()[0]
         secr = RoleFactory(group__acronym='secretariat',name_id='secr')
         wg_ballot = create_ballot_if_not_open(None, wg_draft, ad.person, 'approve')
-        due = datetime.date.today()+datetime.timedelta(days=14)
+        due = datetime_today(DEADLINE_TZINFO) + datetime.timedelta(days=14)
         rg_ballot = create_ballot_if_not_open(None, rg_draft, secr.person, 'irsg-approve', due)
 
         url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=wg_draft.name, ballot_id=wg_ballot.pk))
@@ -323,7 +324,7 @@ class BaseManipulationTests():
     def test_issue_ballot(self):
         draft = RgDraftFactory()
         url = urlreverse('ietf.doc.views_ballot.issue_irsg_ballot',kwargs=dict(name=draft.name))
-        due = datetime.date.today()+datetime.timedelta(days=14)
+        due = date_today(DEADLINE_TZINFO)+datetime.timedelta(days=14)
         empty_outbox()
 
         login_testing_unauthorized(self, self.username , url)
@@ -444,7 +445,7 @@ class IRSGMemberTests(TestCase):
 
     def test_cant_issue_irsg_ballot(self):
         draft = RgDraftFactory()
-        due = datetime.date.today()+datetime.timedelta(days=14)
+        due = datetime_today(DEADLINE_TZINFO) + datetime.timedelta(days=14)
         url = urlreverse('ietf.doc.views_ballot.close_irsg_ballot', kwargs=dict(name=draft.name))
 
         self.client.login(username = self.username, password = self.username+'+password')
diff --git a/ietf/doc/tests_material.py b/ietf/doc/tests_material.py
index 1e922197d..05bbc2078 100644
--- a/ietf/doc/tests_material.py
+++ b/ietf/doc/tests_material.py
@@ -4,7 +4,6 @@
 
 import os
 import shutil
-import datetime
 import io
 
 from pathlib import Path
@@ -14,6 +13,7 @@ import debug              # pyflakes:ignore
 
 from django.conf import settings
 from django.urls import reverse as urlreverse
+from django.utils import timezone
 
 from ietf.doc.models import Document, State, DocAlias, NewRevisionDocEvent
 from ietf.group.factories import RoleFactory
@@ -155,7 +155,7 @@ class GroupMaterialTests(TestCase):
             name = "session-42-mars-1",
             meeting = Meeting.objects.get(number='42'),
             group = Group.objects.get(acronym='mars'),
-            modified = datetime.datetime.now(),
+            modified = timezone.now(),
         )
         SchedulingEvent.objects.create(
             session=session,
diff --git a/ietf/doc/tests_review.py b/ietf/doc/tests_review.py
index 7e902514d..8e3fdcf22 100644
--- a/ietf/doc/tests_review.py
+++ b/ietf/doc/tests_review.py
@@ -10,10 +10,10 @@ import email.mime.multipart, email.mime.text, email.utils
 from mock import patch
 from requests import Response
 
-
 from django.apps import apps
 from django.urls import reverse as urlreverse
 from django.conf import settings
+from django.utils import timezone
 
 from pyquery import PyQuery
 
@@ -38,6 +38,7 @@ from ietf.utils.mail import outbox, empty_outbox, parseaddr, on_behalf_of, get_p
 from ietf.utils.test_utils import login_testing_unauthorized, reload_db_objects
 from ietf.utils.test_utils import TestCase
 from ietf.utils.text import strip_prefix, xslugify
+from ietf.utils.timezone import date_today, DEADLINE_TZINFO
 from django.utils.html import escape
 
 class ReviewTests(TestCase):
@@ -67,7 +68,7 @@ class ReviewTests(TestCase):
         RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
         RoleFactory(group=review_team3,person__user__username='reviewsecretary3',person__user__email='reviewsecretary3@example.com',name_id='secr')
 
-        req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
+        req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20))
         ReviewAssignmentFactory(review_request = req, reviewer = rev_role.person.email_set.first(), state_id='accepted')
 
         url = urlreverse('ietf.doc.views_review.request_review', kwargs={ "name": doc.name })
@@ -77,7 +78,7 @@ class ReviewTests(TestCase):
         r = self.client.get(url)
         self.assertEqual(r.status_code, 200)
 
-        deadline = datetime.date.today() + datetime.timedelta(days=10)
+        deadline = date_today() + datetime.timedelta(days=10)
 
         empty_outbox()
 
@@ -145,7 +146,7 @@ class ReviewTests(TestCase):
         doc = WgDraftFactory(group__acronym='mars',rev='01')
         review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
         rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
-        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
+        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20))
         ReviewAssignmentFactory(review_request=review_req, reviewer=rev_role.person.email_set.first(), state_id='accepted')
 
         # move the review request to a doubly-replaced document to
@@ -166,7 +167,7 @@ class ReviewTests(TestCase):
         doc = WgDraftFactory(group__acronym='mars',rev='01', authors=[author])
         review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
         rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
-        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
+        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20))
         ReviewAssignmentFactory(review_request = review_req, reviewer = rev_role.person.email_set.first(), state_id='accepted')
 
         url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
@@ -195,7 +196,7 @@ class ReviewTests(TestCase):
         rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
         RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
         RoleFactory(group=review_team,person__user__username='reviewsecretary2',person__user__email='reviewsecretary2@example.com',name_id='secr')
-        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
+        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20))
         ReviewAssignmentFactory(review_request=review_req, state_id='accepted', reviewer=rev_role.person.email_set.first())
 
         close_url = urlreverse('ietf.doc.views_review.close_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
@@ -260,14 +261,14 @@ class ReviewTests(TestCase):
 
         # previous review
         req = ReviewRequestFactory(
-            time=datetime.datetime.now() - datetime.timedelta(days=100),
+            time=timezone.now() - datetime.timedelta(days=100),
             requested_by=Person.objects.get(name="(System)"),
             doc=doc,
             type_id='early',
             team=review_req.team,
             state_id='assigned',
             requested_rev="01",
-            deadline=datetime.date.today() - datetime.timedelta(days=80),
+            deadline=date_today() - datetime.timedelta(days=80),
         )
         ReviewAssignmentFactory(
             review_request = req,
@@ -372,7 +373,7 @@ class ReviewTests(TestCase):
         doc = WgDraftFactory(group__acronym='mars',rev='01')
         review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
         rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
-        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
+        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20))
         assignment = ReviewAssignmentFactory(review_request=review_req, state_id='assigned', reviewer=rev_role.person.email_set.first())
 
         url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk })
@@ -395,7 +396,7 @@ class ReviewTests(TestCase):
         review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
         rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
         RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
-        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
+        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20))
         assignment = ReviewAssignmentFactory(review_request = review_req, reviewer=rev_role.person.email_set.first(), state_id='accepted')
 
         reject_url = urlreverse('ietf.doc.views_review.reject_reviewer_assignment', kwargs={ "name": doc.name, "assignment_id": assignment.pk })
@@ -495,7 +496,7 @@ class ReviewTests(TestCase):
         review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
         rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
         RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
-        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
+        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20))
         assignment = ReviewAssignmentFactory(review_request=review_req, reviewer=rev_role.person.email_set.first(), state_id='accepted')
 
         # test URL construction
@@ -525,7 +526,7 @@ class ReviewTests(TestCase):
             messages = r.json()["messages"]
             self.assertEqual(len(messages), 2)
 
-            today = datetime.date.today()
+            today = date_today()
 
             self.assertEqual(messages[0]["url"], "https://www.example.com/testmessage")
             self.assertTrue("John Doe" in messages[0]["content"])
@@ -587,7 +588,7 @@ class ReviewTests(TestCase):
         review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
         rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
         RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
-        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
+        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20))
         assignment = ReviewAssignmentFactory(review_request=review_req, state_id='accepted', reviewer=rev_role.person.email_set.first())
         for r in ReviewResultName.objects.filter(slug__in=("issues", "ready")):
             review_req.team.reviewteamsettings.review_results.add(r)
@@ -699,7 +700,7 @@ class ReviewTests(TestCase):
         assignment = reload_db_objects(assignment)
         self.assertEqual(assignment.state_id, "completed")
         # Completed time should be close to now, but will not be exactly, so check within 10s margin
-        completed_time_diff = datetime.datetime.now() - assignment.completed_on
+        completed_time_diff = timezone.now() - assignment.completed_on
         self.assertLess(completed_time_diff, datetime.timedelta(seconds=10))
 
         with io.open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f:
@@ -733,15 +734,15 @@ class ReviewTests(TestCase):
         # The secretary is allowed to set a custom completion date (#2590)
         assignment = reload_db_objects(assignment)
         self.assertEqual(assignment.state_id, "completed")
-        self.assertEqual(assignment.completed_on, datetime.datetime(2012, 12, 24, 12, 13, 14))
+        self.assertEqual(assignment.completed_on, datetime.datetime(2012, 12, 24, 12, 13, 14, tzinfo=DEADLINE_TZINFO))
 
         # There should be two events:
         # - the event logging when the change when it was entered, i.e. very close to now.
         # - the completion of the review, set to the provided date/time
         events = ReviewAssignmentDocEvent.objects.filter(doc=assignment.review_request.doc).order_by('-time')
-        event0_time_diff = datetime.datetime.now() - events[0].time
+        event0_time_diff = timezone.now() - events[0].time
         self.assertLess(event0_time_diff, datetime.timedelta(seconds=10))
-        self.assertEqual(events[1].time, datetime.datetime(2012, 12, 24, 12, 13, 14))
+        self.assertEqual(events[1].time, datetime.datetime(2012, 12, 24, 12, 13, 14, tzinfo=DEADLINE_TZINFO))
         
         with io.open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f:
             self.assertEqual(f.read(), "This is a review\nwith two lines")
@@ -898,7 +899,7 @@ class ReviewTests(TestCase):
             assignment.review_request.team.acronym, 
             assignment.review_request.type.slug,
             xslugify(assignment.reviewer.person.ascii_parts()[3]),
-            datetime.date.today().isoformat(),
+            date_today().isoformat(),
         ]
         review_name = "-".join(c for c in name_components if c).lower()
         Document.objects.create(name=review_name,type_id='review',group=assignment.review_request.team)
@@ -985,7 +986,7 @@ class ReviewTests(TestCase):
         self.assertEqual(assignment.state_id, "completed")
         # The revision event time should be the date the revision was submitted, i.e. not backdated
         event1 = assignment.review_request.doc.latest_event(ReviewAssignmentDocEvent)
-        event_time_diff = datetime.datetime.now() - event1.time
+        event_time_diff = timezone.now() - event1.time
         self.assertLess(event_time_diff, datetime.timedelta(seconds=10))
         self.assertTrue('revised' in event1.desc.lower())
 
@@ -1012,7 +1013,7 @@ class ReviewTests(TestCase):
         assignment = reload_db_objects(assignment)
         self.assertEqual(assignment.review.rev, "01")
         event2 = assignment.review_request.doc.latest_event(ReviewAssignmentDocEvent)
-        event_time_diff = datetime.datetime.now() - event2.time
+        event_time_diff = timezone.now() - event2.time
         self.assertLess(event_time_diff, datetime.timedelta(seconds=10))
         # Ensure that a new event was created for the new revision (#2590)
         self.assertNotEqual(event1.id, event2.id)
@@ -1024,7 +1025,7 @@ class ReviewTests(TestCase):
         review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
         rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
         RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
-        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
+        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='assigned',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20))
         ReviewAssignmentFactory(review_request = review_req, reviewer = rev_role.person.email_set.first(), state_id='accepted')
 
         url = urlreverse('ietf.doc.views_review.edit_comment', kwargs={ "name": doc.name, "request_id": review_req.pk })
@@ -1046,7 +1047,7 @@ class ReviewTests(TestCase):
         review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))
         rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer')
         RoleFactory(group=review_team,person__user__username='reviewsecretary',person__user__email='reviewsecretary@example.com',name_id='secr')
-        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,deadline=datetime.datetime.now()+datetime.timedelta(days=20))
+        review_req = ReviewRequestFactory(doc=doc,team=review_team,type_id='early',state_id='accepted',requested_by=rev_role.person,deadline=timezone.now()+datetime.timedelta(days=20))
         ReviewAssignmentFactory(review_request = review_req, reviewer = rev_role.person.email_set.first(), state_id='accepted')
 
         url = urlreverse('ietf.doc.views_review.edit_deadline', kwargs={ "name": doc.name, "request_id": review_req.pk })
diff --git a/ietf/doc/tests_utils.py b/ietf/doc/tests_utils.py
index aef6eb69a..f5f2fdd6b 100644
--- a/ietf/doc/tests_utils.py
+++ b/ietf/doc/tests_utils.py
@@ -16,6 +16,7 @@ from ietf.doc.models import State, DocumentActionHolder, DocumentAuthor, Documen
 from ietf.doc.utils import (update_action_holders, add_state_change_event, update_documentauthors,
                             fuzzy_find_documents, rebuild_reference_relations)
 from ietf.utils.draft import Draft, PlaintextDraft
+from ietf.utils.timezone import date_today
 from ietf.utils.xmldraft import XMLDraft
 
 
@@ -143,12 +144,13 @@ class ActionHoldersTests(TestCase):
         doc = self.doc_in_iesg_state('pub-req')
         doc.action_holders.set([self.ad])
         dah = doc.documentactionholder_set.get(person=self.ad)
-        dah.time_added = datetime.datetime(2020, 1, 1)  # arbitrary date in the past
+        dah.time_added = datetime.datetime(2020, 1, 1, tzinfo=datetime.timezone.utc)  # arbitrary date in the past
         dah.save()
 
-        self.assertNotEqual(doc.documentactionholder_set.get(person=self.ad).time_added.date(), datetime.date.today())
+        today = date_today()
+        self.assertNotEqual(doc.documentactionholder_set.get(person=self.ad).time_added.date(), today)
         self.update_doc_state(doc, State.objects.get(slug='ad-eval'))
-        self.assertEqual(doc.documentactionholder_set.get(person=self.ad).time_added.date(), datetime.date.today())
+        self.assertEqual(doc.documentactionholder_set.get(person=self.ad).time_added.date(), today)
 
     def test_update_action_holders_add_tag_need_rev(self):
         """Adding need-rev tag adds authors as action holders"""
diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py
index 01b529c5e..65666ab6c 100644
--- a/ietf/doc/utils.py
+++ b/ietf/doc/utils.py
@@ -13,12 +13,14 @@ import textwrap
 
 from collections import defaultdict, namedtuple
 from urllib.parse import quote
+from zoneinfo import ZoneInfo
 
 from django.conf import settings
 from django.contrib import messages
 from django.forms import ValidationError
 from django.http import Http404
 from django.template.loader import render_to_string
+from django.utils import timezone
 from django.utils.html import escape
 from django.urls import reverse as urlreverse
 
@@ -39,6 +41,7 @@ from ietf.review.models import ReviewWish
 from ietf.utils import draft, log
 from ietf.utils.mail import send_mail
 from ietf.mailtrigger.utils import gather_address_lists
+from ietf.utils.timezone import date_today, datetime_from_date, datetime_today, DEADLINE_TZINFO
 from ietf.utils.xmldraft import XMLDraft
 
 
@@ -637,11 +640,22 @@ def has_same_ballot(doc, date1, date2=None):
     """ Test if the most recent ballot created before the end of date1
         is the same as the most recent ballot created before the
         end of date 2. """
+    datetime1 = datetime_from_date(date1, DEADLINE_TZINFO)
     if date2 is None:
-        date2 = datetime.date.today()
-    ballot1 = doc.latest_event(BallotDocEvent,type='created_ballot',time__lt=date1+datetime.timedelta(days=1))
-    ballot2 = doc.latest_event(BallotDocEvent,type='created_ballot',time__lt=date2+datetime.timedelta(days=1))
-    return ballot1==ballot2
+        datetime2 = datetime_today(DEADLINE_TZINFO)
+    else:
+        datetime2 = datetime_from_date(date2, DEADLINE_TZINFO)
+    ballot1 = doc.latest_event(
+        BallotDocEvent,
+        type='created_ballot',
+        time__lt=datetime1 + datetime.timedelta(days=1),
+    )
+    ballot2 = doc.latest_event(
+        BallotDocEvent,
+        type='created_ballot',
+        time__lt=datetime2 + datetime.timedelta(days=1),
+    )
+    return ballot1 == ballot2
 
 def make_notify_changed_event(request, doc, by, new_notify, time=None):
 
@@ -687,7 +701,7 @@ def update_telechat(request, doc, by, new_telechat_date, new_returning_item=None
          and on_agenda
          and prev_agenda
          and new_telechat_date != prev_telechat
-         and prev_telechat < datetime.date.today()
+         and prev_telechat < date_today(DEADLINE_TZINFO)
          and has_same_ballot(doc,prev.telechat_date)
        ):
         returning = True
@@ -718,7 +732,7 @@ def update_telechat(request, doc, by, new_telechat_date, new_returning_item=None
 
     e.save()
 
-    has_short_fuse = doc.type_id=='draft' and new_telechat_date and (( new_telechat_date - datetime.date.today() ) < datetime.timedelta(days=13))
+    has_short_fuse = doc.type_id=='draft' and new_telechat_date and (( new_telechat_date - date_today() ) < datetime.timedelta(days=13))
 
     from ietf.doc.mails import email_update_telechat
 
@@ -808,7 +822,7 @@ def set_replaces_for_document(request, doc, new_replaces, by, email_subject, com
             cc.update(other_addrs.cc)
             RelatedDocument.objects.filter(source=doc, target=d, relationship=relationship).delete()
             if not RelatedDocument.objects.filter(target=d, relationship=relationship):
-                s = 'active' if d.document.expires > datetime.datetime.now() else 'expired'
+                s = 'active' if d.document.expires > timezone.now() else 'expired'
                 d.document.set_state(State.objects.get(type='draft', slug=s))
 
     for d in new_replaces:
@@ -956,6 +970,7 @@ def make_rev_history(doc):
                         history[url]['pages'] = d.history_set.filter(rev=e.newrevisiondocevent.rev).first().pages
 
     if doc.type_id == "draft":
+        # e.time.date() agrees with RPC publication date when shown in the RPC_TZINFO time zone
         e = doc.latest_event(type='published_rfc')
     else:
         e = doc.latest_event(type='iesg_approved')
@@ -1119,7 +1134,7 @@ def build_doc_meta_block(doc, path):
             lines[i] = line
         return lines
     #
-    now = datetime.datetime.now()
+    now = timezone.now()
     draft_state = doc.get_state('draft')
     block = ''
     meta = {}
@@ -1337,14 +1352,15 @@ def bibxml_for_draft(doc, rev=None):
     latest_revision_event = doc.latest_event(NewRevisionDocEvent, type="new_revision")
     latest_revision_rev = latest_revision_event.rev if latest_revision_event else None
     best_events = NewRevisionDocEvent.objects.filter(doc__name=doc.name, rev=(rev or latest_revision_rev))
+    tzinfo = ZoneInfo(settings.TIME_ZONE)
     if best_events.exists():
         # There was a period where it was possible to get more than one NewRevisionDocEvent for a revision.
         # A future data cleanup would allow this to be simplified
         best_event = best_events.order_by('time').first()
         log.assertion('doc.rev == best_event.rev')
-        doc.date = best_event.time.date()
+        doc.date = best_event.time.astimezone(tzinfo).date()
     else:
-        doc.date = doc.time.date()      # Even if this may be incoreect, what would be better?
+        doc.date = doc.time.astimezone(tzinfo).date()      # Even if this may be incorrect, what would be better?
 
     return render_to_string('doc/bibxml.xml', {'name':doc.name, 'doc': doc, 'doc_bibtype':'I-D'})
 
diff --git a/ietf/doc/utils_charter.py b/ietf/doc/utils_charter.py
index ce9552106..d14684d42 100644
--- a/ietf/doc/utils_charter.py
+++ b/ietf/doc/utils_charter.py
@@ -11,6 +11,7 @@ import shutil
 from django.conf import settings
 from django.urls import reverse as urlreverse
 from django.template.loader import render_to_string
+from django.utils import timezone
 from django.utils.encoding import smart_text, force_text
 
 import debug                            # pyflakes:ignore
@@ -24,6 +25,8 @@ from ietf.utils.mail import parse_preformatted
 from ietf.mailtrigger.utils import gather_address_lists
 from ietf.utils.log import log
 from ietf.group.utils import save_group_in_history
+from ietf.utils.timezone import date_today
+
 
 def charter_name_for_group(group):
     if group.type_id == "rg":
@@ -73,7 +76,7 @@ def change_group_state_after_charter_approval(group, by):
 
     save_group_in_history(group)
     group.state = new_state
-    group.time = datetime.datetime.now()
+    group.time = timezone.now()
     group.save()
 
     # create an event for the group state change, too
@@ -132,7 +135,7 @@ def historic_milestones_for_charter(charter, rev):
         # revision (when approving a charter)
         just_before_next_rev = e[0].time - datetime.timedelta(seconds=5)
     else:
-        just_before_next_rev = datetime.datetime.now()
+        just_before_next_rev = timezone.now()
 
     res = []
     if hasattr(charter, 'chartered_group'):
@@ -197,7 +200,7 @@ def derive_new_work_text(review_text,group):
     return smart_text(m.as_string())
 
 def default_review_text(group, charter, by):
-    now = datetime.datetime.now()
+    now = timezone.now()
     addrs = gather_address_lists('charter_external_review',group=group).as_strings(compact=False)
 
     e1 = WriteupDocEvent(doc=charter, rev=charter.rev, by=by)
@@ -215,7 +218,7 @@ def default_review_text(group, charter, by):
                                     parent_ads=group.parent.role_set.filter(name='ad'),
                                     techadv=group.role_set.filter(name="techadv"),
                                     milestones=group.groupmilestone_set.filter(state="charter"),
-                                    review_date=(datetime.date.today() + datetime.timedelta(weeks=1)).isoformat(),
+                                    review_date=(date_today() + datetime.timedelta(weeks=1)).isoformat(),
                                     review_type="new" if group.state_id in ["proposed","bof"] else "recharter",
                                     to=addrs.to,
                                     cc=addrs.cc,
diff --git a/ietf/doc/utils_search.py b/ietf/doc/utils_search.py
index 47532fdb3..0353d5296 100644
--- a/ietf/doc/utils_search.py
+++ b/ietf/doc/utils_search.py
@@ -5,11 +5,17 @@ import re
 import datetime
 import debug                            # pyflakes:ignore
 
+from zoneinfo import ZoneInfo
+
+from django.conf import settings
+
 from ietf.doc.models import Document, DocAlias, RelatedDocument, DocEvent, TelechatDocEvent, BallotDocEvent
 from ietf.doc.expire import expirable_drafts
 from ietf.doc.utils import augment_docs_and_user_with_user_info
 from ietf.meeting.models import SessionPresentation, Meeting, Session
 from ietf.review.utils import review_assignments_to_list_for_docs
+from ietf.utils.timezone import date_today
+
 
 def wrap_value(v):
     return lambda: v
@@ -30,8 +36,9 @@ def fill_in_telechat_date(docs, doc_dict=None, doc_ids=None):
             seen.add(e.doc_id)
 
 def fill_in_document_sessions(docs, doc_dict, doc_ids):
-    beg_date = datetime.date.today()-datetime.timedelta(days=7)
-    end_date = datetime.date.today()+datetime.timedelta(days=30)
+    today = date_today()
+    beg_date = today-datetime.timedelta(days=7)
+    end_date = today+datetime.timedelta(days=30)
     meetings = Meeting.objects.filter(date__gte=beg_date, date__lte=end_date).prefetch_related('session_set')
     # get sessions
     sessions = Session.objects.filter(meeting_id__in=[ m.id for m in meetings ])
@@ -204,7 +211,7 @@ def prepare_document_table(request, docs, query=None, max_results=200):
         if sort_key == "title":
             res.append(d.title)
         elif sort_key == "date":
-            res.append(str(d.latest_revision_date))
+            res.append(str(d.latest_revision_date.astimezone(ZoneInfo(settings.TIME_ZONE))))
         elif sort_key == "status":
             if rfc_num != None:
                 res.append(num(rfc_num))
diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py
index 7a4e34571..207b8b972 100644
--- a/ietf/doc/views_ballot.py
+++ b/ietf/doc/views_ballot.py
@@ -40,6 +40,8 @@ from ietf.person.models import Person
 from ietf.utils.mail import send_mail_text, send_mail_preformatted
 from ietf.utils.decorators import require_api_key
 from ietf.utils.response import permission_denied
+from ietf.utils.timezone import date_today, datetime_from_date, DEADLINE_TZINFO
+
 
 BALLOT_CHOICES = (("yes", "Yes"),
                   ("noobj", "No Objection"),
@@ -1055,9 +1057,11 @@ def make_last_call(request, name):
             e.desc = "The following Last Call announcement was sent out (ends %s):<br><br>" % expiration_date
             e.desc += announcement
 
-            if form.cleaned_data['last_call_sent_date'] != e.time.date():
-                e.time = datetime.datetime.combine(form.cleaned_data['last_call_sent_date'], e.time.time())
-            e.expires = expiration_date
+            e_production_time = e.time.astimezone(DEADLINE_TZINFO)
+            if form.cleaned_data['last_call_sent_date'] != e_production_time.date():
+                lcsd = form.cleaned_data['last_call_sent_date']
+                e.time = e_production_time.replace(year=lcsd.year, month=lcsd.month, day=lcsd.day)  # preserves tzinfo
+            e.expires = datetime_from_date(expiration_date, DEADLINE_TZINFO)
             e.save()
             events.append(e)
 
@@ -1080,7 +1084,7 @@ def make_last_call(request, name):
             return HttpResponseRedirect(doc.get_absolute_url())
     else:
         initial = {}
-        initial["last_call_sent_date"] = datetime.date.today()
+        initial["last_call_sent_date"] = date_today()
         if doc.type.slug == 'draft':
             # This logic is repeated in the code that edits last call text - why?
             expire_days = 14
@@ -1091,7 +1095,7 @@ def make_last_call(request, name):
             expire_days=28
             templ = 'doc/status_change/make_last_call.html'
 
-        initial["last_call_expiration_date"] = datetime.date.today() + datetime.timedelta(days=expire_days)
+        initial["last_call_expiration_date"] = date_today() + datetime.timedelta(days=expire_days)
         
         form = MakeLastCallForm(initial=initial)
   
@@ -1108,7 +1112,7 @@ def issue_irsg_ballot(request, name):
         raise Http404
 
     by = request.user.person
-    fillerdate = datetime.date.today() + datetime.timedelta(weeks=2)
+    fillerdate = date_today(DEADLINE_TZINFO) + datetime.timedelta(weeks=2)
 
     if request.method == 'POST':
         button = request.POST.get("irsg_button")
@@ -1117,7 +1121,7 @@ def issue_irsg_ballot(request, name):
             e = IRSGBallotDocEvent(doc=doc, rev=doc.rev, by=request.user.person)
             if (duedate == None or duedate==""):
                 duedate = str(fillerdate)
-            e.duedate = datetime.datetime.strptime(duedate, '%Y-%m-%d')
+            e.duedate = datetime_from_date(datetime.datetime.strptime(duedate, '%Y-%m-%d'), DEADLINE_TZINFO)
             e.type = "created_ballot"
             e.desc = "Created IRSG Ballot"
             ballot_type = BallotType.objects.get(doc_type=doc.type, slug="irsg-approve")
@@ -1188,7 +1192,10 @@ def irsg_ballot_status(request):
             ballot = doc.active_ballot()
             if ballot:
                 doc.ballot = ballot
-                doc.duedate=datetime.datetime.strftime(ballot.irsgballotdocevent.duedate, '%Y-%m-%d')
+                doc.duedate=datetime.datetime.strftime(
+                    ballot.irsgballotdocevent.duedate.astimezone(DEADLINE_TZINFO),
+                    '%Y-%m-%d',
+                )
 
             docs.append(doc)
 
diff --git a/ietf/doc/views_charter.py b/ietf/doc/views_charter.py
index c2b88ac47..3f85a19ce 100644
--- a/ietf/doc/views_charter.py
+++ b/ietf/doc/views_charter.py
@@ -16,6 +16,7 @@ from django.utils.safestring import mark_safe
 from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth.decorators import login_required
+from django.utils import timezone
 from django.utils.encoding import force_text
 from django.utils.html import escape
 
@@ -77,7 +78,7 @@ def change_state(request, name, option=None):
     chartering_type = get_chartering_type(charter)
 
     initial_review = charter.latest_event(InitialReviewDocEvent, type="initial_review")
-    if charter.get_state_slug() != "infrev" or (initial_review and initial_review.expires < datetime.datetime.now()) or chartering_type == "rechartering":
+    if charter.get_state_slug() != "infrev" or (initial_review and initial_review.expires < timezone.now()) or chartering_type == "rechartering":
         initial_review = None
 
     by = request.user.person
@@ -183,7 +184,7 @@ def change_state(request, name, option=None):
 
             if charter_state.slug == "infrev" and clean["initial_time"] and clean["initial_time"] != 0:
                 e = InitialReviewDocEvent(type="initial_review", by=by, doc=charter, rev=charter.rev)
-                e.expires = datetime.datetime.now() + datetime.timedelta(weeks=clean["initial_time"])
+                e.expires = timezone.now() + datetime.timedelta(weeks=clean["initial_time"])
                 e.desc = "Initial review time expires %s" % e.expires.strftime("%Y-%m-%d")
                 e.save()
 
@@ -506,7 +507,7 @@ def review_announcement_text(request, name):
         existing_new_work.type = "changed_new_work_text"
         existing_new_work.desc = "%s review text was changed" % group.type.name
         existing_new_work.text = derive_new_work_text(existing.text,group)
-        existing_new_work.time = datetime.datetime.now()
+        existing_new_work.time = timezone.now()
 
     form = ReviewAnnouncementTextForm(initial=dict(announcement_text=escape(existing.text),new_work_text=escape(existing_new_work.text)))
 
@@ -514,7 +515,7 @@ def review_announcement_text(request, name):
         form = ReviewAnnouncementTextForm(request.POST)
         if "save_text" in request.POST and form.is_valid():
 
-            now = datetime.datetime.now()
+            now = timezone.now()
             events = []
 
             t = form.cleaned_data['announcement_text']
diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py
index 7475c6b1f..bdf3b6c1a 100644
--- a/ietf/doc/views_doc.py
+++ b/ietf/doc/views_doc.py
@@ -34,7 +34,6 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 
-import datetime
 import glob
 import io
 import json
@@ -85,6 +84,7 @@ from ietf.utils import markup_txt, log, markdown
 from ietf.utils.draft import PlaintextDraft
 from ietf.utils.response import permission_denied
 from ietf.utils.text import maybe_split
+from ietf.utils.timezone import date_today
 
 
 def render_document_top(request, doc, tab, name):
@@ -186,7 +186,7 @@ def document_main(request, name, rev=None):
 
 
     telechat = doc.latest_event(TelechatDocEvent, type="scheduled_for_telechat")
-    if telechat and (not telechat.telechat_date or telechat.telechat_date < datetime.date.today()):
+    if telechat and (not telechat.telechat_date or telechat.telechat_date < date_today(settings.TIME_ZONE)):
        telechat = None
 
 
@@ -990,7 +990,7 @@ def document_bibtex(request, name, rev=None):
 
     latest_revision = doc.latest_event(NewRevisionDocEvent, type="new_revision")
     replaced_by = [d.name for d in doc.related_that("replaces")]
-    published = doc.latest_event(type="published_rfc")
+    published = doc.latest_event(type="published_rfc") is not None
     rfc = latest_revision.doc if latest_revision and latest_revision.doc.get_state_slug() == "rfc" else None
 
     if rev != None and rev != doc.rev:
@@ -1408,11 +1408,12 @@ def telechat_date(request, name):
 
     warnings = []
     if e and e.telechat_date and doc.type.slug != 'charter':
-        if e.telechat_date==datetime.date.today():
+        today = date_today(settings.TIME_ZONE)
+        if e.telechat_date == today:
             warnings.append( "This document is currently scheduled for today's telechat. "
                             +"Please set the returning item bit carefully.")
 
-        elif e.telechat_date<datetime.date.today() and has_same_ballot(doc,e.telechat_date):
+        elif e.telechat_date < today and has_same_ballot(doc,e.telechat_date):
             initial_returning_item = True
             warnings.append(  "This document appears to have been on a previous telechat with the same ballot, "
                             +"so the returning item bit has been set. Clear it if that is not appropriate.")
diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py
index f3a307178..01cb4ea36 100644
--- a/ietf/doc/views_draft.py
+++ b/ietf/doc/views_draft.py
@@ -19,6 +19,7 @@ from django.shortcuts import render, get_object_or_404, redirect
 from django.template.loader import render_to_string
 from django.forms.utils import ErrorList
 from django.template.defaultfilters import pluralize
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -52,6 +53,8 @@ from ietf.utils.mail import send_mail, send_mail_message, on_behalf_of
 from ietf.utils.textupload import get_cleaned_text_file_content
 from ietf.utils import log
 from ietf.utils.response import permission_denied
+from ietf.utils.timezone import datetime_today, DEADLINE_TZINFO
+
 
 class ChangeStateForm(forms.Form):
     state = forms.ModelChoiceField(State.objects.filter(used=True, type="draft-iesg"), empty_label=None, required=True)
@@ -857,7 +860,7 @@ def resurrect(request, name):
         events.append(e)
 
         doc.set_state(State.objects.get(used=True, type="draft", slug="active"))
-        doc.expires = datetime.datetime.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE)
+        doc.expires = timezone.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE)
         doc.save_with_history(events)
 
         restore_draft_file(request, doc)
@@ -1480,7 +1483,7 @@ def adopt_draft(request, name):
 
                 due_date = None
                 if form.cleaned_data["weeks"] != None:
-                    due_date = datetime.date.today() + datetime.timedelta(weeks=form.cleaned_data["weeks"])
+                    due_date = datetime_today(DEADLINE_TZINFO) + datetime.timedelta(weeks=form.cleaned_data["weeks"])
 
                 update_reminder(doc, "stream-s", e, due_date)
 
@@ -1671,7 +1674,7 @@ def change_stream_state(request, name, state_type):
 
                 due_date = None
                 if form.cleaned_data["weeks"] != None:
-                    due_date = datetime.date.today() + datetime.timedelta(weeks=form.cleaned_data["weeks"])
+                    due_date = datetime_today(DEADLINE_TZINFO) + datetime.timedelta(weeks=form.cleaned_data["weeks"])
 
                 update_reminder(doc, "stream-s", e, due_date)
 
diff --git a/ietf/doc/views_review.py b/ietf/doc/views_review.py
index c7d8e5804..29cf67b8d 100644
--- a/ietf/doc/views_review.py
+++ b/ietf/doc/views_review.py
@@ -10,7 +10,9 @@ import datetime
 import requests
 import email.utils
 
+from django.utils import timezone
 from django.utils.http import is_safe_url
+
 from simple_history.utils import update_change_reason
 
 import debug    # pyflakes:ignore
@@ -52,6 +54,8 @@ from ietf.utils.mail import send_mail_message
 from ietf.mailtrigger.utils import gather_address_lists
 from ietf.utils.fields import MultiEmailField
 from ietf.utils.response import permission_denied
+from ietf.utils.timezone import date_today, DEADLINE_TZINFO
+
 
 def clean_doc_revision(doc, rev):
     if rev:
@@ -92,7 +96,7 @@ class RequestReviewForm(forms.ModelForm):
 
     def clean_deadline(self):
         v = self.cleaned_data.get('deadline')
-        if v < datetime.date.today():
+        if v < date_today(DEADLINE_TZINFO):
             raise forms.ValidationError("Select today or a date in the future.")
         return v
 
@@ -117,7 +121,7 @@ def request_review(request, name):
     if not can_request_review_of_doc(request.user, doc):
         permission_denied(request, "You do not have permission to perform this action")
 
-    now = datetime.datetime.now()
+    now = timezone.now()
 
     lc_ends = None
     e = doc.latest_event(LastCallDocEvent, type="sent_last_call")
@@ -348,7 +352,7 @@ class RejectReviewerAssignmentForm(forms.Form):
 def reject_reviewer_assignment(request, name, assignment_id):
     doc = get_object_or_404(Document, name=name)
     review_assignment = get_object_or_404(ReviewAssignment, pk=assignment_id, state__in=["assigned", "accepted"])
-    review_request_past_deadline = review_assignment.review_request.deadline < datetime.date.today()
+    review_request_past_deadline = review_assignment.review_request.deadline < date_today(DEADLINE_TZINFO)
 
     if not review_assignment.reviewer:
         return redirect(review_request, name=review_assignment.review_request.doc.name, request_id=review_assignment.review_request.pk)
@@ -364,7 +368,7 @@ def reject_reviewer_assignment(request, name, assignment_id):
         if form.is_valid():
             # reject the assignment
             review_assignment.state = ReviewAssignmentStateName.objects.get(slug="rejected")
-            review_assignment.completed_on = datetime.datetime.now()
+            review_assignment.completed_on = timezone.now()
             review_assignment.save()
 
             descr = "Assignment of request for {} review by {} to {} was rejected".format(
@@ -531,7 +535,7 @@ class CompleteReviewForm(forms.Form):
     review_url = forms.URLField(label="Link to message", required=False)
     review_file = forms.FileField(label="Text file to upload", required=False)
     review_content = forms.CharField(widget=forms.Textarea, required=False, strip=False)
-    completion_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={ "autoclose": "1" }, initial=datetime.date.today, help_text="Date of announcement of the results of this review")
+    completion_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={ "autoclose": "1" }, initial=date_today, help_text="Date of announcement of the results of this review")
     completion_time = forms.TimeField(widget=forms.HiddenInput, initial=datetime.time.min)
     cc = MultiEmailField(required=False, help_text="Email addresses to send to in addition to the review team list")
     email_ad = forms.BooleanField(label="Send extra email to the responsible AD suggesting early attention", required=False)
@@ -704,7 +708,7 @@ def complete_review(request, name, assignment_id=None, acronym=None):
                     team.acronym, 
                     request_type.slug,
                     xslugify(reviewer.person.ascii_parts()[3]),
-                    datetime.date.today().isoformat(),
+                    date_today().isoformat(),
                 ]
                 review_name = "-".join(c for c in name_components if c).lower()
                 if not Document.objects.filter(name=review_name).exists():
@@ -723,7 +727,7 @@ def complete_review(request, name, assignment_id=None, acronym=None):
                     type=form.cleaned_data['review_type'],
                     doc=doc,
                     team=team,
-                    deadline=datetime.date.today(),
+                    deadline=date_today(DEADLINE_TZINFO),
                     requested_by=Person.objects.get(user=request.user),
                     requested_rev=form.cleaned_data['reviewed_rev'],
                 )
@@ -731,13 +735,13 @@ def complete_review(request, name, assignment_id=None, acronym=None):
                     review_request=review_request,
                     state_id='assigned',
                     reviewer=form.cleaned_data['reviewer'].role_email('reviewer', group=team),
-                    assigned_on=datetime.datetime.now(),
+                    assigned_on=timezone.now(),
                     review = review,
                 )
 
             review.rev = "00" if not review.rev else "{:02}".format(int(review.rev) + 1)
             review.title = "{} Review of {}-{}".format(assignment.review_request.type.name, assignment.review_request.doc.name, form.cleaned_data["reviewed_rev"])
-            review.time = datetime.datetime.now()
+            review.time = timezone.now()
             if review_submission == "link":
                 review.external_url = form.cleaned_data['review_url']
 
@@ -764,9 +768,13 @@ def complete_review(request, name, assignment_id=None, acronym=None):
             with io.open(filename, 'w', encoding='utf-8') as destination:
                 destination.write(content)
 
-            completion_datetime = datetime.datetime.now()
+            completion_datetime = timezone.now()
             if "completion_date" in form.cleaned_data:
-                completion_datetime = datetime.datetime.combine(form.cleaned_data["completion_date"], form.cleaned_data.get("completion_time") or datetime.time.min)
+                completion_datetime = datetime.datetime.combine(
+                    form.cleaned_data["completion_date"],
+                    form.cleaned_data.get("completion_time") or datetime.time.min,
+                    tzinfo=DEADLINE_TZINFO,
+                )
 
             # complete assignment
             assignment.state = form.cleaned_data["state"]
@@ -778,7 +786,7 @@ def complete_review(request, name, assignment_id=None, acronym=None):
 
             need_to_email_review = review_submission != "link" and assignment.review_request.team.list_email and not revising_review
 
-            submitted_on_different_date = completion_datetime.date() != datetime.date.today()
+            submitted_on_different_date = completion_datetime.date() != date_today(DEADLINE_TZINFO)
             desc = "Request for {} review by {} {}: {}. Reviewer: {}.".format(
                 assignment.review_request.type.name,
                 assignment.review_request.team.acronym.upper(),
@@ -799,7 +807,7 @@ def complete_review(request, name, assignment_id=None, acronym=None):
             close_event.by = request.user.person
             close_event.desc = desc
             close_event.state = assignment.state
-            close_event.time = datetime.datetime.now()
+            close_event.time = timezone.now()
             close_event.save()
             
             # If the completion date is different, record when the initial review was made too.
@@ -894,8 +902,13 @@ def complete_review(request, name, assignment_id=None, acronym=None):
         }
 
         try:
-            initial['review_content'] = render_to_string('/group/%s/review/content_templates/%s.txt' % (assignment.review_request.team.acronym,
-                                                                                                        request_type.slug), {'assignment':assignment, 'today':datetime.date.today()}) 
+            initial['review_content'] = render_to_string(
+                f'/group/{assignment.review_request.team.acronym}/review/content_templates/{request_type.slug}.txt',
+                {
+                    'assignment': assignment,
+                    'today': date_today(settings.TIME_ZONE),
+                },
+            )
         except (TemplateDoesNotExist, AttributeError):
             pass
 
@@ -984,7 +997,7 @@ class EditReviewRequestDeadlineForm(forms.ModelForm):
 
     def clean_deadline(self):
         v = self.cleaned_data.get('deadline')
-        if v < datetime.date.today():
+        if v < date_today(DEADLINE_TZINFO):
             raise forms.ValidationError("Select today or a date in the future.")
         return v
 
diff --git a/ietf/doc/views_search.py b/ietf/doc/views_search.py
index efe24d02c..ca2413785 100644
--- a/ietf/doc/views_search.py
+++ b/ietf/doc/views_search.py
@@ -453,7 +453,7 @@ def ad_dashboard_sort_key(doc):
         ageseconds = 0
         changetime= doc.latest_event(type='changed_document')
         if changetime:
-            ad = (datetime.datetime.now()-doc.latest_event(type='changed_document').time)
+            ad = (timezone.now()-doc.latest_event(type='changed_document').time)
             ageseconds = (ad.microseconds + (ad.seconds + ad.days * 24 * 3600) * 10**6) / 10**6
         return "1%d%s%s%010d" % (state[0].order,seed,doc.type.slug,ageseconds)
 
@@ -761,7 +761,7 @@ def recent_drafts(request, days=7):
     cache_key = f'recentdraftsview{days}' 
     cached_val = slowcache.get(cache_key)
     if not cached_val:
-        since = datetime.datetime.now()-datetime.timedelta(days=days)
+        since = timezone.now()-datetime.timedelta(days=days)
         state = State.objects.get(type='draft', slug='active')
         events = NewRevisionDocEvent.objects.filter(time__gt=since)
         names = [ e.doc.name for e in events ]
diff --git a/ietf/doc/views_stats.py b/ietf/doc/views_stats.py
index 414d04bbe..912d4a57c 100644
--- a/ietf/doc/views_stats.py
+++ b/ietf/doc/views_stats.py
@@ -19,6 +19,8 @@ from ietf.doc.utils import get_search_cache_key
 from ietf.doc.views_search import SearchForm, retrieve_search_results
 from ietf.name.models import DocTypeName
 from ietf.person.models import Person
+from ietf.utils.timezone import date_today
+
 
 epochday = datetime.datetime.utcfromtimestamp(0).date().toordinal()
 
@@ -47,7 +49,7 @@ def model_to_timeline_data(model, field='time', **kwargs):
         # This is needed for sqlite, when we're running tests:
         if type(obj_list[0]['date']) != datetime.date:
             obj_list = [ {'date': dt(e['date']), 'count': e['count']} for e in obj_list ]
-        today = datetime.date.today()
+        today = date_today()
         if not obj_list[-1]['date'] == today:
             obj_list += [ {'date': today, 'count': 0} ]
         data = [ ((e['date'].toordinal()-epochday)*1000*60*60*24, e['count']) for e in obj_list ]
diff --git a/ietf/doc/views_status_change.py b/ietf/doc/views_status_change.py
index b64aadfd0..d4868215a 100644
--- a/ietf/doc/views_status_change.py
+++ b/ietf/doc/views_status_change.py
@@ -35,6 +35,7 @@ from ietf.name.models import DocRelationshipName, StdLevelName
 from ietf.person.models import Person
 from ietf.utils.mail import send_mail_preformatted
 from ietf.utils.textupload import get_cleaned_text_file_content
+from ietf.utils.timezone import date_today, DEADLINE_TZINFO
 
 
 class ChangeStateForm(forms.Form):
@@ -638,7 +639,7 @@ def generate_last_call_text(request, doc):
     # and when groups are set, vary the expiration time accordingly
 
     requester = "an individual participant"
-    expiration_date = datetime.date.today() + datetime.timedelta(days=28)
+    expiration_date = date_today(DEADLINE_TZINFO) + datetime.timedelta(days=28)
     cc = []
     
     new_text = render_to_string("doc/status_change/last_call_announcement.txt",
diff --git a/ietf/group/factories.py b/ietf/group/factories.py
index d8d927d80..e7fbef59a 100644
--- a/ietf/group/factories.py
+++ b/ietf/group/factories.py
@@ -5,6 +5,8 @@ import factory
 
 from typing import List    # pyflakes:ignore
 
+from django.utils import timezone
+
 from ietf.group.models import Group, Role, GroupEvent, GroupMilestone, \
                               GroupHistory, RoleHistory
 from ietf.review.factories import ReviewTeamSettingsFactory
@@ -66,7 +68,7 @@ class BaseGroupMilestoneFactory(factory.django.DjangoModelFactory):
 
 class DatedGroupMilestoneFactory(BaseGroupMilestoneFactory):
     group = factory.SubFactory(GroupFactory, uses_milestone_dates=True)
-    due = datetime.datetime.today()+datetime.timedelta(days=180)
+    due = timezone.now()+datetime.timedelta(days=180)
 
 class DatelessGroupMilestoneFactory(BaseGroupMilestoneFactory):
     group = factory.SubFactory(GroupFactory, uses_milestone_dates=False)
diff --git a/ietf/group/forms.py b/ietf/group/forms.py
index 0b8fc8e45..c93ca1d63 100644
--- a/ietf/group/forms.py
+++ b/ietf/group/forms.py
@@ -3,7 +3,6 @@
 
 
 # Stdlib imports
-import datetime
 import re
 
 import debug      # pyflakes:ignore
@@ -27,6 +26,7 @@ from ietf.utils import log
 from ietf.utils.textupload import get_cleaned_text_file_content
 #from ietf.utils.ordereddict import insert_after_in_ordered_dict
 from ietf.utils.fields import DatepickerDateField, MultiEmailField
+from ietf.utils.timezone import date_today
 from ietf.utils.validators import validate_external_resource_value
 
 # --- Constants --------------------------------------------------------
@@ -364,7 +364,7 @@ class AddUnavailablePeriodForm(forms.ModelForm):
     def __init__(self, *args, **kwargs):
         super(AddUnavailablePeriodForm, self).__init__(*args, **kwargs)
 
-        self.fields["start_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label=self.fields["start_date"].label, help_text=self.fields["start_date"].help_text, required=self.fields["start_date"].required, initial=datetime.date.today())
+        self.fields["start_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label=self.fields["start_date"].label, help_text=self.fields["start_date"].help_text, required=self.fields["start_date"].required, initial=date_today())
         self.fields["end_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label=self.fields["end_date"].label, help_text=self.fields["end_date"].help_text, required=self.fields["end_date"].required)
 
         self.fields['availability'].widget = forms.RadioSelect(choices=UnavailablePeriod.LONG_AVAILABILITY_CHOICES)
diff --git a/ietf/group/management/commands/generate_group_aliases.py b/ietf/group/management/commands/generate_group_aliases.py
index 9efbb68ab..2610ba5ad 100755
--- a/ietf/group/management/commands/generate_group_aliases.py
+++ b/ietf/group/management/commands/generate_group_aliases.py
@@ -15,6 +15,7 @@ from tempfile import mkstemp
     
 from django.conf import settings
 from django.core.management.base import BaseCommand
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -39,7 +40,7 @@ class Command(BaseCommand):
             'have seen activity in the last %s years.' % (DEFAULT_YEARS))
 
     def handle(self, *args, **options):
-        show_since = datetime.datetime.now() - datetime.timedelta(DEFAULT_YEARS*365)
+        show_since = timezone.now() - datetime.timedelta(DEFAULT_YEARS*365)
 
         date = time.strftime("%Y-%m-%d_%H:%M:%S")
         signature = '# Generated by %s at %s\n' % (os.path.abspath(__file__), date)
diff --git a/ietf/group/migrations/0059_use_timezone_now_for_group_models.py b/ietf/group/migrations/0059_use_timezone_now_for_group_models.py
new file mode 100644
index 000000000..24c083855
--- /dev/null
+++ b/ietf/group/migrations/0059_use_timezone_now_for_group_models.py
@@ -0,0 +1,29 @@
+# Generated by Django 2.2.28 on 2022-07-12 11:24
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('group', '0058_alter_has_default_chat'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='group',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AlterField(
+            model_name='groupevent',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text='When the event happened'),
+        ),
+        migrations.AlterField(
+            model_name='grouphistory',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+    ]
diff --git a/ietf/group/models.py b/ietf/group/models.py
index 899c88241..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
@@ -13,6 +12,7 @@ from django.core.validators import RegexValidator
 from django.db import models
 from django.db.models.deletion import CASCADE, PROTECT
 from django.dispatch import receiver
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -27,7 +27,7 @@ from ietf.utils.validators import JSONForeignKeyListValidator
 
 
 class GroupInfo(models.Model):
-    time = models.DateTimeField(default=datetime.datetime.now)
+    time = models.DateTimeField(default=timezone.now)
     name = models.CharField(max_length=80)
     state = ForeignKey(GroupStateName, null=True)
     type = ForeignKey(GroupTypeName, null=True)
@@ -180,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):
@@ -353,7 +357,7 @@ GROUP_EVENT_CHOICES = [
 class GroupEvent(models.Model):
     """An occurrence for a group, used for tracking who, when and what."""
     group = ForeignKey(Group)
-    time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened")
+    time = models.DateTimeField(default=timezone.now, help_text="When the event happened")
     type = models.CharField(max_length=50, choices=GROUP_EVENT_CHOICES)
     by = ForeignKey(Person)
     desc = models.TextField()
diff --git a/ietf/group/tests.py b/ietf/group/tests.py
index b168e4d3b..2c63ff95d 100644
--- a/ietf/group/tests.py
+++ b/ietf/group/tests.py
@@ -13,6 +13,7 @@ from django.conf import settings
 from django.urls import reverse as urlreverse
 from django.db.models import Q
 from django.test import Client
+from django.utils import timezone
 
 import debug                             # pyflakes:ignore
 
@@ -115,8 +116,8 @@ class GenerateGroupAliasesTests(TestCase):
         super().tearDown()
 
     def testManagementCommand(self):
-        a_month_ago = datetime.datetime.now() - datetime.timedelta(30)
-        a_decade_ago = datetime.datetime.now() - datetime.timedelta(3650)
+        a_month_ago = timezone.now() - datetime.timedelta(30)
+        a_decade_ago = timezone.now() - datetime.timedelta(3650)
         role1 = RoleFactory(name_id='ad', group__type_id='area', group__acronym='myth', group__state_id='active')
         area = role1.group
         ad = role1.person
diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py
index 0b3ead2c7..9d5328d51 100644
--- a/ietf/group/tests_info.py
+++ b/ietf/group/tests_info.py
@@ -42,6 +42,8 @@ from ietf.person.factories import PersonFactory, EmailFactory
 from ietf.review.factories import ReviewRequestFactory, ReviewAssignmentFactory
 from ietf.utils.mail import outbox, empty_outbox, get_payload_text
 from ietf.utils.test_utils import login_testing_unauthorized, TestCase, unicontent, reload_db_objects
+from ietf.utils.timezone import date_today, DEADLINE_TZINFO
+
 
 def group_urlreverse_list(group, viewname):
     return [
@@ -269,7 +271,7 @@ class GroupPagesTests(TestCase):
             group=group,
             state_id="active",
             desc="Get Work Done",
-            due=datetime.date.today() + datetime.timedelta(days=100))
+            due=date_today(DEADLINE_TZINFO) + datetime.timedelta(days=100))
         milestone.docs.add(draft)
 
         for url in [group.about_url(),] + group_urlreverse_list(group, 'ietf.group.views.group_about'):
@@ -876,7 +878,7 @@ class GroupEditTests(TestCase):
         self.assertEqual(r.status_code, 302)
         review_assignment.state_id = 'accepted'
         review_assignment.save()
-        review_req.deadline = datetime.date.today() - datetime.timedelta(days=1)
+        review_req.deadline = date_today(DEADLINE_TZINFO) - datetime.timedelta(days=1)
         review_req.save()
         
         r = self.client.post(url, post_data)
@@ -1194,7 +1196,7 @@ class MilestoneTests(TestCase):
         m1 = GroupMilestone.objects.create(id=1,
                                            group=group,
                                            desc="Test 1",
-                                           due=datetime.date.today(),
+                                           due=date_today(DEADLINE_TZINFO),
                                            resolved="",
                                            state_id="active")
         m1.docs.set([draft])
@@ -1202,7 +1204,7 @@ class MilestoneTests(TestCase):
         m2 = GroupMilestone.objects.create(id=2,
                                            group=group,
                                            desc="Test 2",
-                                           due=datetime.date.today(),
+                                           due=date_today(DEADLINE_TZINFO),
                                            resolved="",
                                            state_id="charter")
         m2.docs.set([draft])
@@ -1246,7 +1248,7 @@ class MilestoneTests(TestCase):
         events_before = group.groupevent_set.count()
         doc_pks = pklist(Document.objects.filter(type="draft"))
 
-        due = self.last_day_of_month(datetime.date.today() + datetime.timedelta(days=365))
+        due = self.last_day_of_month(date_today(DEADLINE_TZINFO) + datetime.timedelta(days=365))
 
         # faulty post
         r = self.client.post(url, { 'prefix': "m-1",
@@ -1302,7 +1304,7 @@ class MilestoneTests(TestCase):
 
         milestones_before = GroupMilestone.objects.filter(group=group).count()
         events_before = group.groupevent_set.count()
-        due = self.last_day_of_month(datetime.date.today() + datetime.timedelta(days=365))
+        due = self.last_day_of_month(date_today(DEADLINE_TZINFO) + datetime.timedelta(days=365))
 
         # add
         mailbox_before = len(outbox)
@@ -1393,7 +1395,7 @@ class MilestoneTests(TestCase):
         events_before = group.groupevent_set.count()
         doc_pks = pklist(Document.objects.filter(type="draft"))
 
-        due = self.last_day_of_month(datetime.date.today() + datetime.timedelta(days=365))
+        due = self.last_day_of_month(date_today(DEADLINE_TZINFO) + datetime.timedelta(days=365))
 
         # faulty post
         r = self.client.post(url, { 'prefix': "m1",
@@ -1776,7 +1778,7 @@ class MeetingInfoTests(TestCase):
     def setUp(self):
         super().setUp()
         self.group = GroupFactory.create(type_id='wg')
-        today = datetime.date.today()
+        today = date_today()
         SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=14))
         self.inprog = SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today-datetime.timedelta(days=1))
         SessionFactory.create(meeting__type_id='ietf',group=self.group,meeting__date=today+datetime.timedelta(days=90))
@@ -1900,7 +1902,7 @@ class StatusUpdateTests(TestCase):
     def test_view_status_update_for_meeting(self):
         chair = RoleFactory(name_id='chair',group__type_id='wg')
         GroupEventFactory(type='status_update',group=chair.group)
-        sess = SessionFactory.create(meeting__type_id='ietf',group=chair.group,meeting__date=datetime.datetime.today()-datetime.timedelta(days=1))
+        sess = SessionFactory.create(meeting__type_id='ietf',group=chair.group,meeting__date=date_today()-datetime.timedelta(days=1))
         url = urlreverse('ietf.group.views.group_about_status_meeting',kwargs={'acronym':chair.group.acronym,'num':sess.meeting.number}) 
         response = self.client.get(url)
         self.assertEqual(response.status_code,200)
diff --git a/ietf/group/tests_js.py b/ietf/group/tests_js.py
index 49dd33983..1c7f9fc9a 100644
--- a/ietf/group/tests_js.py
+++ b/ietf/group/tests_js.py
@@ -7,6 +7,7 @@ import debug                            # pyflakes:ignore
 from ietf.doc.factories import WgDraftFactory
 from ietf.group.factories import GroupFactory, RoleFactory, DatedGroupMilestoneFactory
 from ietf.utils.jstest import IetfSeleniumTestCase, ifSeleniumEnabled, selenium_enabled
+from ietf.utils.timezone import date_today
 
 if selenium_enabled():
     from selenium.common.exceptions import TimeoutException
@@ -68,7 +69,7 @@ class MilestoneTests(IetfSeleniumTestCase):
         draft = WgDraftFactory()
         WgDraftFactory.create_batch(3)  # some drafts to ignore
         description = 'some description'
-        due_date = datetime.date.today() + datetime.timedelta(days=60)
+        due_date = date_today() + datetime.timedelta(days=60)
 
         assert(len(draft.name) > 5)
         draft_search_string = draft.name[-5:]
diff --git a/ietf/group/tests_review.py b/ietf/group/tests_review.py
index ee4ae96fe..fc09e27e0 100644
--- a/ietf/group/tests_review.py
+++ b/ietf/group/tests_review.py
@@ -7,7 +7,9 @@ import debug      # pyflakes:ignore
 
 from pyquery import PyQuery
 
+from django.conf import settings
 from django.urls import reverse as urlreverse
+from django.utils import timezone
 
 from ietf.review.policies import get_reviewer_queue_policy
 from ietf.utils.test_utils import login_testing_unauthorized, TestCase, reload_db_objects
@@ -25,6 +27,7 @@ from ietf.person.factories import PersonFactory, EmailFactory
 from ietf.doc.factories import DocumentFactory
 from ietf.group.factories import RoleFactory, ReviewTeamFactory, GroupFactory
 from ietf.review.factories import ReviewRequestFactory, ReviewerSettingsFactory, ReviewAssignmentFactory
+from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO
 from django.utils.html import escape
 
 class ReviewTests(TestCase):
@@ -131,7 +134,7 @@ class ReviewTests(TestCase):
         doc.states.add(State.objects.get(type="draft-iesg", slug="lc", used=True))
         LastCallDocEvent.objects.create(
             doc=doc,
-            expires=datetime.datetime.now() + datetime.timedelta(days=365),
+            expires=timezone.now() + datetime.timedelta(days=365),
             by=Person.objects.get(name="(System)"),
             rev=doc.rev
         )
@@ -155,7 +158,7 @@ class ReviewTests(TestCase):
             review_request__doc=review_req1.doc,
             review_request__team=review_req1.team,
             review_request__type_id="early",
-            review_request__deadline=datetime.date.today() + datetime.timedelta(days=30),
+            review_request__deadline=date_today(DEADLINE_TZINFO) + datetime.timedelta(days=30),
             review_request__state_id="assigned",
             review_request__requested_by=Person.objects.get(user__username="reviewer"),
             state_id = "accepted",
@@ -165,7 +168,7 @@ class ReviewTests(TestCase):
         UnavailablePeriod.objects.create(
             team=review_req1.team,
             person=reviewer,
-            start_date=datetime.date.today() - datetime.timedelta(days=10),
+            start_date=date_today() - datetime.timedelta(days=10),
             availability="unavailable",
         )
 
@@ -210,7 +213,7 @@ class ReviewTests(TestCase):
             review_request__doc=review_req2.doc,
             review_request__team=review_req2.team,
             review_request__type_id="lc",
-            review_request__deadline=datetime.date.today() - datetime.timedelta(days=30),
+            review_request__deadline=date_today(DEADLINE_TZINFO) - datetime.timedelta(days=30),
             review_request__state_id="assigned",
             review_request__requested_by=Person.objects.get(user__username="reviewer"),
             state_id = "no-response",
@@ -231,15 +234,15 @@ class ReviewTests(TestCase):
         review_req3 = ReviewRequestFactory(state_id='completed', team=team)
         ReviewAssignmentFactory(
             review_request__doc=review_req3.doc,
-            review_request__time=datetime.date.today() - datetime.timedelta(days=30),
+            review_request__time=datetime_today() - datetime.timedelta(days=30),
             review_request__team=review_req3.team,
             review_request__type_id="telechat",
-            review_request__deadline=datetime.date.today() - datetime.timedelta(days=25),
+            review_request__deadline=date_today(DEADLINE_TZINFO) - datetime.timedelta(days=25),
             review_request__state_id="completed",
             review_request__requested_by=Person.objects.get(user__username="reviewer"),
             state_id = "completed",
             reviewer=reviewer.email_set.first(),
-            assigned_on=datetime.date.today() - datetime.timedelta(days=30)
+            assigned_on=datetime_today() - datetime.timedelta(days=30)
         )
         r = self.client.get(url)
         self.assertEqual(r.status_code, 200)
@@ -252,15 +255,15 @@ class ReviewTests(TestCase):
         for i in range(10):
             ReviewAssignmentFactory(
                 review_request__doc=reqs[i].doc,
-                review_request__time=datetime.date.today() - datetime.timedelta(days=i*30),
+                review_request__time=datetime_today() - datetime.timedelta(days=i*30),
                 review_request__team=reqs[i].team,
                 review_request__type_id="telechat",
-                review_request__deadline=datetime.date.today() - datetime.timedelta(days=i*20),
+                review_request__deadline=date_today(DEADLINE_TZINFO) - datetime.timedelta(days=i*20),
                 review_request__state_id="completed",
                 review_request__requested_by=Person.objects.get(user__username="reviewer"),
                 state_id = "completed",
                 reviewer=reviewer.email_set.first(),
-                assigned_on=datetime.date.today() - datetime.timedelta(days=i*30)
+                assigned_on=datetime_today() - datetime.timedelta(days=i*30)
             )
         r = self.client.get(url)
         self.assertEqual(r.status_code, 200)
@@ -304,28 +307,28 @@ class ReviewTests(TestCase):
         review_req4 = ReviewRequestFactory(state_id='completed', team=team)
         ReviewAssignmentFactory(
             review_request__doc=review_req4.doc,
-            review_request__time=datetime.date.today() - datetime.timedelta(days=80),
+            review_request__time=datetime_today() - datetime.timedelta(days=80),
             review_request__team=review_req4.team,
             review_request__type_id="lc",
-            review_request__deadline=datetime.date.today() - datetime.timedelta(days=60),
+            review_request__deadline=date_today(DEADLINE_TZINFO) - datetime.timedelta(days=60),
             review_request__state_id="assigned",
             review_request__requested_by=Person.objects.get(user__username="reviewer"),
             state_id = "accepted",
             reviewer=reviewer.email_set.first(),
-            assigned_on=datetime.date.today() - datetime.timedelta(days=80)
+            assigned_on=datetime_today() - datetime.timedelta(days=80)
         )
         review_req5 = ReviewRequestFactory(state_id='completed', team=team)
         ReviewAssignmentFactory(
             review_request__doc=review_req5.doc,
-            review_request__time=datetime.date.today() - datetime.timedelta(days=120),
+            review_request__time=datetime_today() - datetime.timedelta(days=120),
             review_request__team=review_req5.team,
             review_request__type_id="lc",
-            review_request__deadline=datetime.date.today() - datetime.timedelta(days=100),
+            review_request__deadline=date_today(DEADLINE_TZINFO) - datetime.timedelta(days=100),
             review_request__state_id="assigned",
             review_request__requested_by=Person.objects.get(user__username="reviewer"),
             state_id = "accepted",
             reviewer=reviewer.email_set.first(),
-            assigned_on=datetime.date.today() - datetime.timedelta(days=120)
+            assigned_on=datetime_today() - datetime.timedelta(days=120)
         )
         r = self.client.get(url)
         self.assertEqual(r.status_code, 200)
@@ -429,7 +432,7 @@ class ReviewTests(TestCase):
         doc.states.add(State.objects.get(type="draft-iesg", slug="lc", used=True))
         LastCallDocEvent.objects.create(
             doc=doc,
-            expires=datetime.datetime.now() + datetime.timedelta(days=365),
+            expires=timezone.now() + datetime.timedelta(days=365),
             by=Person.objects.get(name="(System)"),
             rev=doc.rev
         )
@@ -475,7 +478,7 @@ class ReviewTests(TestCase):
         review_req1 = ReviewRequestFactory()
         review_assignment_completed = ReviewAssignmentFactory(review_request=review_req1,reviewer=EmailFactory(person__user__username='marschairman'), state_id='completed', reviewed_rev=0)
         ReviewAssignmentFactory(review_request=review_req1,reviewer=review_assignment_completed.reviewer)
-        TelechatDocEvent.objects.create(telechat_date=datetime.date.today(), type='scheduled_for_telechat', by=review_assignment_completed.reviewer.person, doc=review_req1.doc, rev=0)
+        TelechatDocEvent.objects.create(telechat_date=date_today(settings.TIME_ZONE), type='scheduled_for_telechat', by=review_assignment_completed.reviewer.person, doc=review_req1.doc, rev=0)
 
         DBTemplateFactory.create(path='/group/defaults/email/open_assignments.txt',
                                  type_id='django',
@@ -555,7 +558,7 @@ class ReviewTests(TestCase):
         # get
         r = self.client.get(url)
         self.assertEqual(r.status_code, 200)
-        self.assertEqual(r.context['period_form']['start_date'].initial, datetime.date.today())
+        self.assertEqual(r.context['period_form']['start_date'].initial, date_today())
 
         # set settings
         empty_outbox()
@@ -596,7 +599,7 @@ class ReviewTests(TestCase):
         self.assertEqual(settings.skip_next, 0)
 
         # add unavailable period
-        start_date = datetime.date.today() + datetime.timedelta(days=10)
+        start_date = date_today() + datetime.timedelta(days=10)
         empty_outbox()
         r = self.client.post(url, {
             "action": "add_period",
diff --git a/ietf/group/views.py b/ietf/group/views.py
index 3a4b9f3e0..4bc31d09f 100644
--- a/ietf/group/views.py
+++ b/ietf/group/views.py
@@ -53,6 +53,7 @@ from django.http import HttpResponse, HttpResponseRedirect, Http404, JsonRespons
 from django.shortcuts import render, redirect, get_object_or_404
 from django.template.loader import render_to_string
 from django.urls import reverse as urlreverse
+from django.utils import timezone
 from django.utils.html import escape
 from django.views.decorators.cache import cache_page, cache_control
 
@@ -118,6 +119,7 @@ from ietf.settings import MAILING_LIST_INFO_URL
 from ietf.utils.response import permission_denied
 from ietf.utils.text import strip_suffix
 from ietf.utils import markdown
+from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO
 
 
 # --- Helpers ----------------------------------------------------------
@@ -566,7 +568,7 @@ def all_status(request):
         if e:
             wg_reports.append(e)
 
-    wg_reports.sort(key=lambda x: (x.group.parent.acronym,datetime.datetime.now()-x.time))
+    wg_reports.sort(key=lambda x: (x.group.parent.acronym,timezone.now()-x.time))
 
     rg_reports = []
     for rg in rgs:
@@ -808,7 +810,7 @@ def email_aliases(request, acronym=None, group_type=None):
 def meetings(request, acronym=None, group_type=None):
     group = get_group_or_404(acronym,group_type) if acronym else None
 
-    four_years_ago = datetime.datetime.now()-datetime.timedelta(days=4*365)
+    four_years_ago = timezone.now()-datetime.timedelta(days=4*365)
 
     sessions = add_event_info_to_session_qs(
         group.session_set.filter(
@@ -972,7 +974,7 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
                 try:
                     group = Group.objects.get(acronym=clean["acronym"])
                     save_group_in_history(group)
-                    group.time = datetime.datetime.now()
+                    group.time = timezone.now()
                     group.save()
                 except Group.DoesNotExist:
                     group = Group.objects.create(name=clean["name"],
@@ -1026,7 +1028,7 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
                     change_text=title + ' deleted: ' + ", ".join(x.name_and_email() for x in deleted)
                     personnel_change_text+=change_text+"\n"
 
-                    today = datetime.date.today()
+                    today = date_today()
                     for deleted_email in deleted:
                         # Verify the person doesn't have a separate reviewer role for the group with a different address
                         if not group.role_set.filter(name_id='reviewer',person=deleted_email.person).exists():
@@ -1071,7 +1073,7 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
                         )
                     ))
 
-            group.time = datetime.datetime.now()
+            group.time = timezone.now()
 
             if changes and not new_group:
                 for attr, new, desc in changes:
@@ -1387,7 +1389,7 @@ def review_requests(request, acronym, group_type=None):
     unassigned_review_requests.sort(key=lambda r: r.doc.name)
 
     open_review_assignments = list(ReviewAssignment.objects.filter(review_request__team=group, state_id__in=('assigned','accepted')).order_by('-assigned_on'))
-    today = datetime.date.today()
+    today = date_today(DEADLINE_TZINFO)
     unavailable_periods = current_unavailable_periods_for_reviewers(group)
     for a in open_review_assignments:
         a.reviewer_unavailable = any(p.availability == "unavailable"
@@ -1420,11 +1422,14 @@ def review_requests(request, acronym, group_type=None):
         }[since]
 
         closed_review_requests = closed_review_requests.filter(
-              Q(reviewrequestdocevent__type='closed_review_request', reviewrequestdocevent__time__gte=datetime.date.today() - date_limit)
-            | Q(reviewrequestdocevent__isnull=True, time__gte=datetime.date.today() - date_limit)
+              Q(reviewrequestdocevent__type='closed_review_request',
+                reviewrequestdocevent__time__gte=datetime_today(DEADLINE_TZINFO) - date_limit)
+            | Q(reviewrequestdocevent__isnull=True, time__gte=datetime_today(DEADLINE_TZINFO) - date_limit)
         ).distinct()
 
-        closed_review_assignments = closed_review_assignments.filter(completed_on__gte = datetime.date.today() - date_limit)
+        closed_review_assignments = closed_review_assignments.filter(
+            completed_on__gte = datetime_today(DEADLINE_TZINFO) - date_limit,
+        )
 
     return render(request, 'group/review_requests.html',
                   construct_group_menu_context(request, group, "review requests", group_type, {
@@ -1455,7 +1460,7 @@ def reviewer_overview(request, acronym, group_type=None):
         unavailable_periods[p.person_id].append(p)
     reviewer_roles = { r.person_id: r for r in Role.objects.filter(group=group, name="reviewer").select_related("email") }
 
-    today = datetime.date.today()
+    today = date_today()
 
     max_closed_reqs = settings.GROUP_REVIEW_MAX_ITEMS_TO_SHOW_IN_REVIEWER_LIST
     days_back = settings.GROUP_REVIEW_DAYS_TO_SHOW_IN_REVIEWER_LIST
@@ -1509,7 +1514,7 @@ def reviewer_overview(request, acronym, group_type=None):
                                     int(math.ceil(d.assignment_to_closure_days)) if d.assignment_to_closure_days is not None else None))
             if d.state in ["completed", "completed_in_time", "completed_late"]:
                 if d.assigned_time is not None:
-                    delta = datetime.datetime.now() - d.assigned_time
+                    delta = timezone.now() - d.assigned_time
                     if d.assignment_to_closure_days is not None:
                         days = int(delta.days - d.assignment_to_closure_days)
                         if days_since > days: days_since = days
@@ -1833,7 +1838,7 @@ def change_reviewer_settings(request, acronym, reviewer_email, group_type=None):
             period.save()
             update_change_reason(period, "Added unavailability period: {}".format(period))
 
-            today = datetime.date.today()
+            today = date_today()
 
             in_the_past = period.end_date and period.end_date < today
 
@@ -1874,7 +1879,7 @@ def change_reviewer_settings(request, acronym, reviewer_email, group_type=None):
                     period.delete()
                     update_change_reason(period, "Removed unavailability period: {}".format(period))
 
-                    today = datetime.date.today()
+                    today = date_today()
 
                     in_the_past = period.end_date and period.end_date < today
 
diff --git a/ietf/idindex/index.py b/ietf/idindex/index.py
index 3d17b197c..d864905e4 100644
--- a/ietf/idindex/index.py
+++ b/ietf/idindex/index.py
@@ -7,10 +7,10 @@
 
 import datetime
 import os
-import pytz
 
 from django.conf import settings
 from django.template.loader import render_to_string
+from django.utils import timezone
 
 import debug    # pyflakes:ignore
 
@@ -296,6 +296,6 @@ def id_index_txt(with_abstracts=False):
 
     return render_to_string("idindex/id_index.txt", {
             'groups': groups,
-            'time': datetime.datetime.now(pytz.UTC).strftime("%Y-%m-%d %H:%M:%S %Z"),
+            'time': timezone.now().astimezone(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z"),
             'with_abstracts': with_abstracts,
             })
diff --git a/ietf/idindex/tests.py b/ietf/idindex/tests.py
index 413e3a4ed..f207fa562 100644
--- a/ietf/idindex/tests.py
+++ b/ietf/idindex/tests.py
@@ -7,6 +7,7 @@ import datetime
 from pathlib import Path
 
 from django.conf import settings
+from django.utils import timezone
 
 import debug    # pyflakes:ignore
 
@@ -120,7 +121,7 @@ class IndexTests(TestCase):
         draft.set_state(State.objects.get(type="draft", slug="active"))
         draft.set_state(State.objects.get(type="draft-iesg", slug="lc"))
 
-        e = LastCallDocEvent.objects.create(doc=draft, rev=draft.rev, type="sent_last_call", expires=datetime.datetime.now() + datetime.timedelta(days=14), by=draft.ad)
+        e = LastCallDocEvent.objects.create(doc=draft, rev=draft.rev, type="sent_last_call", expires=timezone.now() + datetime.timedelta(days=14), by=draft.ad)
 
         t = get_fields(all_id2_txt())
         self.assertEqual(t[11], e.expires.strftime("%Y-%m-%d"))
diff --git a/ietf/iesg/agenda.py b/ietf/iesg/agenda.py
index e923c1bab..113c3c5d1 100644
--- a/ietf/iesg/agenda.py
+++ b/ietf/iesg/agenda.py
@@ -17,13 +17,14 @@ from ietf.doc.models import Document, LastCallDocEvent, ConsensusDocEvent
 from ietf.doc.utils_search import fill_in_telechat_date
 from ietf.iesg.models import TelechatDate, TelechatAgendaItem
 from ietf.review.utils import review_assignments_to_list_for_docs
+from ietf.utils.timezone import date_today
 
 def get_agenda_date(date=None):
     if not date:
         try:
             return TelechatDate.objects.active().order_by('date')[0].date
         except IndexError:
-            return datetime.date.today()
+            return date_today()
     else:
         try:
             return TelechatDate.objects.active().get(date=datetime.datetime.strptime(date, "%Y-%m-%d").date()).date
diff --git a/ietf/iesg/feeds.py b/ietf/iesg/feeds.py
index 6e8d290c6..a8b5c2869 100644
--- a/ietf/iesg/feeds.py
+++ b/ietf/iesg/feeds.py
@@ -2,12 +2,13 @@
 # -*- coding: utf-8 -*-
 
 
-import datetime
-
+from django.conf import settings
 from django.contrib.syndication.views import Feed
 from django.utils.feedgenerator import Atom1Feed
 
 from ietf.doc.models import Document, TelechatDocEvent
+from ietf.utils.timezone import date_today
+
 
 class IESGAgendaFeed(Feed):
     title = "Documents on Future IESG Telechat Agendas"
@@ -16,7 +17,7 @@ class IESGAgendaFeed(Feed):
     description_template = "iesg/feed_item_description.html"
 
     def items(self):
-        docs = Document.objects.filter(docevent__telechatdocevent__telechat_date__gte=datetime.date.today()).distinct()
+        docs = Document.objects.filter(docevent__telechatdocevent__telechat_date__gte=date_today(settings.TIME_ZONE)).distinct()
         for d in docs:
             d.latest_telechat_event = d.latest_event(TelechatDocEvent, type="scheduled_for_telechat")
         docs = [d for d in docs if d.latest_telechat_event.telechat_date]
diff --git a/ietf/iesg/models.py b/ietf/iesg/models.py
index f3690d4c7..6a622e27e 100644
--- a/ietf/iesg/models.py
+++ b/ietf/iesg/models.py
@@ -36,8 +36,12 @@
 
 import datetime
 
+from django.conf import settings
 from django.db import models
 
+from ietf.utils.timezone import date_today
+
+
 class TelechatAgendaItem(models.Model):
     TYPE_CHOICES = (
         (1, "Any Other Business (WG News, New Proposals, etc.)"),
@@ -72,11 +76,11 @@ def next_telechat_date():
     dates = TelechatDate.objects.order_by("-date")
     if dates:
         return dates[0].date + datetime.timedelta(days=14)
-    return datetime.date.today()
+    return date_today(settings.TIME_ZONE)
 
 class TelechatDateManager(models.Manager):
     def active(self):
-        return self.get_queryset().filter(date__gte=datetime.date.today())
+        return self.get_queryset().filter(date__gte=date_today(settings.TIME_ZONE))
 
 class TelechatDate(models.Model):
     objects = TelechatDateManager()
diff --git a/ietf/iesg/tests.py b/ietf/iesg/tests.py
index f317ad32b..5ecb4ed9d 100644
--- a/ietf/iesg/tests.py
+++ b/ietf/iesg/tests.py
@@ -28,6 +28,7 @@ from ietf.name.models import StreamName
 from ietf.person.models import Person
 from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent
 from ietf.iesg.factories import IESGMgmtItemFactory
+from ietf.utils.timezone import date_today, DEADLINE_TZINFO
 
 
 class IESGTests(TestCase):
@@ -58,7 +59,7 @@ class IESGTests(TestCase):
         m = GroupMilestone.objects.create(group=draft.group,
                                           state_id="review",
                                           desc="Test milestone",
-                                          due=datetime.date.today())
+                                          due=date_today(DEADLINE_TZINFO))
 
         url = urlreverse("ietf.iesg.views.milestones_needing_review")
         login_testing_unauthorized(self, "ad", url)
@@ -142,7 +143,7 @@ class IESGAgendaTests(TestCase):
         mgmtitem = self.mgmt_items
 
         # put on agenda
-        date = datetime.date.today() + datetime.timedelta(days=50)
+        date = date_today(settings.TIME_ZONE) + datetime.timedelta(days=50)
         TelechatDate.objects.create(date=date)
         telechat_event = TelechatDocEvent.objects.create(
             type="scheduled_for_telechat",
@@ -430,7 +431,7 @@ class IESGAgendaTests(TestCase):
             self.assertNotIn(d.title, unicontent(r))
         # Add the documents to a past telechat
         by = Person.objects.get(name="Areað Irector")
-        date = datetime.date.today() - datetime.timedelta(days=14)
+        date = date_today(settings.TIME_ZONE) - datetime.timedelta(days=14)
         approved = State.objects.get(type='draft-iesg', slug='approved')
         iesg_eval = State.objects.get(type='draft-iesg', slug='iesg-eva')
         for d in list(self.telechat_docs.values()):
@@ -485,7 +486,7 @@ class IESGAgendaTests(TestCase):
 
     def test_admin_change(self):
         draft = Document.objects.get(name="draft-ietf-mars-test")
-        today = datetime.date.today()
+        today = date_today(settings.TIME_ZONE)
         telechat_date = TelechatDate.objects.get(date=draft.telechat_date())
         url = urlreverse('admin:iesg_telechatdate_change', args=(telechat_date.id,))
         self.client.login(username="secretary", password="secretary+password")
diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py
index 5d1457cee..5334e8a85 100644
--- a/ietf/iesg/views.py
+++ b/ietf/iesg/views.py
@@ -63,6 +63,7 @@ from ietf.iesg.utils import telechat_page_count
 from ietf.ietfauth.utils import has_role, role_required, user_is_person
 from ietf.person.models import Person
 from ietf.doc.utils_search import fill_in_document_table_attributes, fill_in_telechat_date
+from ietf.utils.timezone import date_today, datetime_from_date
 
 def review_decisions(request, year=None):
     events = DocEvent.objects.filter(type__in=("iesg_disapproved", "iesg_approved"))
@@ -73,9 +74,9 @@ def review_decisions(request, year=None):
         year = int(year)
         events = events.filter(time__year=year)
     else:
-        d = datetime.date.today() - datetime.timedelta(days=185)
+        d = date_today() - datetime.timedelta(days=185)
         d = datetime.date(d.year, d.month, 1)
-        events = events.filter(time__gte=d)
+        events = events.filter(time__gte=datetime_from_date(d))
 
     events = events.select_related("doc", "doc__intended_std_level").order_by("-time", "-id")
 
diff --git a/ietf/ietfauth/management/commands/send_apikey_usage_emails.py b/ietf/ietfauth/management/commands/send_apikey_usage_emails.py
index 4aa4e5524..d3fce1bcc 100644
--- a/ietf/ietfauth/management/commands/send_apikey_usage_emails.py
+++ b/ietf/ietfauth/management/commands/send_apikey_usage_emails.py
@@ -8,6 +8,7 @@ from textwrap import dedent
 
 from django.conf import settings
 from django.core.management.base import BaseCommand
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -37,7 +38,7 @@ class Command(BaseCommand):
 
         keys = PersonalApiKey.objects.filter(valid=True)
         for key in keys:
-            earliest = datetime.datetime.now() - datetime.timedelta(days=days)
+            earliest = timezone.now() - datetime.timedelta(days=days)
             events = PersonApiKeyEvent.objects.filter(key=key, time__gt=earliest)
             count = events.count()
             events = events[:32]
diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py
index b6c145e68..983283ae7 100644
--- a/ietf/ietfauth/tests.py
+++ b/ietf/ietfauth/tests.py
@@ -28,6 +28,7 @@ from django.urls import reverse as urlreverse
 from django.contrib.auth.models import User
 from django.conf import settings
 from django.template.loader import render_to_string 
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -46,6 +47,8 @@ from ietf.stats.models import MeetingRegistration
 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
+from ietf.utils.timezone import date_today
+
 
 import ietf.ietfauth.views
 
@@ -390,7 +393,7 @@ class IetfAuthTests(TestCase):
         self.assertFalse(q('#volunteer-button'))
         self.assertFalse(q('#volunteered'))
 
-        year = datetime.date.today().year
+        year = date_today().year
         nomcom = NomComFactory(group__acronym=f'nomcom{year}',is_accepting_volunteers=True)
         r = self.client.get(url)
         self.assertEqual(r.status_code,200)
@@ -505,7 +508,7 @@ class IetfAuthTests(TestCase):
         UnavailablePeriod.objects.create(
             team=review_req.team,
             person=reviewer,
-            start_date=datetime.date.today() - datetime.timedelta(days=10),
+            start_date=date_today() - datetime.timedelta(days=10),
             availability="unavailable",
         )
 
@@ -750,11 +753,11 @@ class IetfAuthTests(TestCase):
             self.assertContains(r, 'Invalid apikey', status_code=403)
 
             # too long since regular login
-            person.user.last_login = datetime.datetime.now() - datetime.timedelta(days=settings.UTILS_APIKEY_GUI_LOGIN_LIMIT_DAYS+1)
+            person.user.last_login = timezone.now() - datetime.timedelta(days=settings.UTILS_APIKEY_GUI_LOGIN_LIMIT_DAYS+1)
             person.user.save()
             r = self.client.post(key.endpoint, {'apikey':key.hash(), 'dummy':'dummy',})
             self.assertContains(r, 'Too long since last regular login', status_code=400)
-            person.user.last_login = datetime.datetime.now()
+            person.user.last_login = timezone.now()
             person.user.save()
 
             # endpoint mismatch
@@ -783,12 +786,12 @@ class IetfAuthTests(TestCase):
         # apikey usage will be registered)
         count = 2
         # avoid usage across dates
-        if datetime.datetime.now().time() > datetime.time(hour=23, minute=59, second=58):
+        if timezone.now().time() > datetime.time(hour=23, minute=59, second=58):
             time.sleep(2)
         for i in range(count):
             for key in person.apikeys.all():
                 self.client.post(key.endpoint, {'apikey':key.hash(), 'dummy': 'dummy', })
-        date = str(datetime.date.today())
+        date = str(date_today())
 
         empty_outbox()
         cmd = Command()
@@ -905,7 +908,7 @@ class OpenIDConnectTests(TestCase):
             # an additional email
             EmailFactory(person=person)
             email_list = person.email_set.all().values_list('address', flat=True)
-            meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
+            meeting = MeetingFactory(type_id='ietf', date=date_today())
             MeetingRegistration.objects.create(
                     meeting=meeting, person=None, first_name=person.first_name(), last_name=person.last_name(),
                     email=email_list[0], ticket_type='full_week', reg_type='remote', affiliation='Some Company',
diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py
index e405d6620..f9db86d76 100644
--- a/ietf/ietfauth/views.py
+++ b/ietf/ietfauth/views.py
@@ -34,9 +34,9 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 
+import datetime
 import importlib
 
-from datetime import date as Date, datetime as DateTime
 # needed if we revert to higher barrier for account creation
 #from datetime import datetime as DateTime, timedelta as TimeDelta, date as Date
 from collections import defaultdict
@@ -78,6 +78,7 @@ from ietf.doc.fields import SearchableDocumentField
 from ietf.utils.decorators import person_required
 from ietf.utils.mail import send_mail
 from ietf.utils.validators import validate_external_resource_value
+from ietf.utils.timezone import date_today, DEADLINE_TZINFO
 
 # These are needed if we revert to the higher bar for account creation
 
@@ -223,7 +224,7 @@ def profile(request):
     emails = Email.objects.filter(person=person).exclude(address__startswith='unknown-email-').order_by('-active','-time')
     new_email_forms = []
 
-    nc = NomCom.objects.filter(group__acronym__icontains=Date.today().year).first()
+    nc = NomCom.objects.filter(group__acronym__icontains=date_today().year).first()
     if nc and nc.volunteer_set.filter(person=person).exists():
         volunteer_status = 'volunteered'
     elif nc and nc.is_accepting_volunteers:
@@ -455,7 +456,7 @@ def confirm_password_reset(request, auth):
         password = data['password']
         last_login = None
         if data['last_login']:
-            last_login = DateTime.fromtimestamp(data['last_login'])
+            last_login = datetime.datetime.fromtimestamp(data['last_login'], datetime.timezone.utc)
     except django.core.signing.BadSignature:
         raise Http404("Invalid or expired auth")
 
@@ -557,7 +558,7 @@ def review_overview(request):
         reviewer__person__user=request.user,
         state__in=["assigned", "accepted"],
     )
-    today = Date.today()
+    today = date_today(DEADLINE_TZINFO)
     for r in open_review_assignments:
         r.due = max(0, (today - r.review_request.deadline).days)
 
diff --git a/ietf/ipr/factories.py b/ietf/ipr/factories.py
index da1262912..e32090a36 100644
--- a/ietf/ipr/factories.py
+++ b/ietf/ipr/factories.py
@@ -5,6 +5,7 @@
 import datetime
 import factory
 
+from django.utils import timezone
 
 from ietf.ipr.models import (
     IprDisclosureBase, HolderIprDisclosure, ThirdPartyIprDisclosure, NonDocSpecificIprDisclosure,
@@ -13,7 +14,7 @@ from ietf.ipr.models import (
 
 def _fake_patent_info():
     return "Date: %s\nNotes: %s\nTitle: %s\nNumber: %s\nInventor: %s\n" % (
-        (datetime.datetime.today()-datetime.timedelta(days=365)).strftime("%Y-%m-%d"),
+        (timezone.now()-datetime.timedelta(days=365)).strftime("%Y-%m-%d"),
         factory.Faker('paragraph'),
         factory.Faker('sentence', nb_words=8),
         'US9999999',
diff --git a/ietf/ipr/mail.py b/ietf/ipr/mail.py
index ea3551d56..f1d8039db 100644
--- a/ietf/ipr/mail.py
+++ b/ietf/ipr/mail.py
@@ -3,13 +3,14 @@
 
 
 import base64
-import email
 import datetime
 from dateutil.tz import tzoffset
 import os
-import pytz
 import re
 
+from email import message_from_bytes
+from email.utils import parsedate_tz
+
 from django.template.loader import render_to_string
 from django.utils.encoding import force_text, force_bytes
 
@@ -50,7 +51,7 @@ def parsedate_to_datetime(date):
     http://python.readthedocs.org/en/latest/library/email.util.html
     """
     try:
-        tuple = email.utils.parsedate_tz(date)
+        tuple = parsedate_tz(date)
         if not tuple:
             return None
         tz = tuple[-1]
@@ -62,10 +63,12 @@ def parsedate_to_datetime(date):
 
 def utc_from_string(s):
     date = parsedate_to_datetime(s)
-    if is_aware(date):
-        return date.astimezone(pytz.utc).replace(tzinfo=None)
+    if date is None:
+        return None
+    elif is_aware(date):
+        return date.astimezone(datetime.timezone.utc)
     else:
-        return date
+        return date.replace(tzinfo=datetime.timezone.utc)
 
 # ----------------------------------------------------------------
 # Email Functions
@@ -174,7 +177,7 @@ def process_response_email(msg):
     a matching value in the reply_to field, associated to an IPR disclosure through
     IprEvent.  Create a Message object for the incoming message and associate it to
     the original message via new IprEvent"""
-    message = email.message_from_bytes(force_bytes(msg))
+    message = message_from_bytes(force_bytes(msg))
     to = message.get('To', '')
 
     # exit if this isn't a response we're interested in (with plus addressing)
diff --git a/ietf/ipr/models.py b/ietf/ipr/models.py
index 282f33568..2eb588b8b 100644
--- a/ietf/ipr/models.py
+++ b/ietf/ipr/models.py
@@ -2,11 +2,10 @@
 # -*- coding: utf-8 -*-
 
 
-import datetime
-
 from django.conf import settings
 from django.db import models
 from django.urls import reverse
+from django.utils import timezone
 
 from ietf.doc.models import DocAlias, DocEvent
 from ietf.name.models import DocRelationshipName,IprDisclosureStateName,IprLicenseTypeName,IprEventTypeName
@@ -220,7 +219,7 @@ class IprEvent(models.Model):
         """Returns true if it's beyond the response_due date and no response has been
         received"""
         qs = IprEvent.objects.filter(disclosure=self.disclosure,in_reply_to=self.message)
-        if not qs and datetime.datetime.now().date() > self.response_due.date():
+        if not qs and timezone.now().date() > self.response_due.date():
             return True
         else:
             return False
diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py
index db9c1371e..ab6b01a5b 100644
--- a/ietf/ipr/tests.py
+++ b/ietf/ipr/tests.py
@@ -8,8 +8,9 @@ import datetime
 from pyquery import PyQuery
 from urllib.parse import quote, urlparse
 
-from django.urls import reverse as urlreverse
 from django.conf import settings
+from django.urls import reverse as urlreverse
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -27,6 +28,7 @@ from ietf.message.models import Message
 from ietf.utils.mail import outbox, empty_outbox, get_payload_text
 from ietf.utils.test_utils import TestCase, login_testing_unauthorized
 from ietf.utils.text import text_to_dict
+from ietf.utils.timezone import date_today
 
 
 def make_data_from_content(content):
@@ -572,7 +574,7 @@ I would like to revoke this declaration.
         self.assertEqual(r.status_code,302)
         self.assertEqual(len(outbox),len_before+2)
         self.assertTrue('george@acme.com' in outbox[len_before]['To'])
-        self.assertIn('posted on '+datetime.date.today().strftime("%Y-%m-%d"), get_payload_text(outbox[len_before]).replace('\n',' '))
+        self.assertIn('posted on '+date_today().strftime("%Y-%m-%d"), get_payload_text(outbox[len_before]).replace('\n',' '))
         self.assertTrue('draft-ietf-mars-test@ietf.org' in outbox[len_before+1]['To'])
         self.assertTrue('mars-wg@ietf.org' in outbox[len_before+1]['Cc'])
         self.assertIn('Secretariat on '+ipr.get_latest_event_submitted().time.strftime("%Y-%m-%d"), get_payload_text(outbox[len_before+1]).replace('\n',' '))
@@ -600,7 +602,7 @@ I would like to revoke this declaration.
         ipr = HolderIprDisclosureFactory()
         url = urlreverse('ietf.ipr.views.email',kwargs={ "id": ipr.id })
         self.client.login(username="secretary", password="secretary+password")
-        yesterday = datetime.date.today() - datetime.timedelta(1)
+        yesterday = date_today() - datetime.timedelta(1)
         data = dict(
             to='joe@test.com',
             frm='ietf-ipr@ietf.org',
@@ -640,7 +642,7 @@ I would like to revoke this declaration.
                 message_string.format(
                     to=addrs.to,
                     cc=addrs.cc,
-                    date=datetime.datetime.now().ctime()
+                    date=timezone.now().ctime()
                 )
             )
             self.assertIsNone(result)
@@ -650,7 +652,7 @@ I would like to revoke this declaration.
 From: joe@test.com
 Date: {}
 Subject: test
-""".format(reply_to, datetime.datetime.now().ctime())
+""".format(reply_to, timezone.now().ctime())
         result = process_response_email(message_string)
 
         self.assertIsInstance(result, Message)
@@ -664,7 +666,7 @@ Subject: test
 From: joe@test.com
 Date: {}
 Subject: test
-""".format(reply_to, datetime.datetime.now().ctime())
+""".format(reply_to, timezone.now().ctime())
         message_bytes = message_string.encode('utf8') + b'\nInvalid stuff: \xfe\xff\n'
         result = process_response_email(message_bytes)
         self.assertIsInstance(result, Message)
@@ -680,7 +682,7 @@ Subject: test
             message_bytes = message_string.format(
                                 to=addrs.to,
                                 cc=addrs.cc,
-                                date=datetime.datetime.now().ctime(),
+                                date=timezone.now().ctime(),
             ).encode('utf8') + b'\nInvalid stuff: \xfe\xff\n'
             result = process_response_email(message_bytes)
             self.assertIsNone(result)
diff --git a/ietf/ipr/views.py b/ietf/ipr/views.py
index 163a858cc..d587eaecd 100644
--- a/ietf/ipr/views.py
+++ b/ietf/ipr/views.py
@@ -43,6 +43,7 @@ from ietf.utils.draft_search import normalize_draftname
 from ietf.utils.mail import send_mail, send_mail_message
 from ietf.utils.response import permission_denied
 from ietf.utils.text import text_to_dict
+from ietf.utils.timezone import datetime_from_date, datetime_today, DEADLINE_TZINFO
 
 # ----------------------------------------------------------------
 # Globals
@@ -147,15 +148,14 @@ def ipr_rfc_number(disclosureDate, thirdPartyDisclosureFlag):
     # made on 1993-07-23, which is more than a year after RFC 1310.
 
     # RFC publication date comes from the RFC Editor announcement
-    # TODO: These times are tzinfo=pytz.utc, but disclosure times are offset-naive
     ipr_rfc_pub_datetime = {
-        1310 : datetime.datetime(1992,  3, 13,  0,  0),
-        1802 : datetime.datetime(1994,  3, 23,  0,  0),
-        2026 : datetime.datetime(1996, 10, 29,  0,  0),
-        3668 : datetime.datetime(2004,  2, 18,  0,  0),
-        3979 : datetime.datetime(2005,  3,  2,  2, 23),
-        4879 : datetime.datetime(2007,  4, 10, 18, 21),
-        8179 : datetime.datetime(2017,  5, 31, 23,  1),
+        1310 : datetime.datetime(1992,  3, 13,  0,  0, tzinfo=datetime.timezone.utc),
+        1802 : datetime.datetime(1994,  3, 23,  0,  0, tzinfo=datetime.timezone.utc),
+        2026 : datetime.datetime(1996, 10, 29,  0,  0, tzinfo=datetime.timezone.utc),
+        3668 : datetime.datetime(2004,  2, 18,  0,  0, tzinfo=datetime.timezone.utc),
+        3979 : datetime.datetime(2005,  3,  2,  2, 23, tzinfo=datetime.timezone.utc),
+        4879 : datetime.datetime(2007,  4, 10, 18, 21, tzinfo=datetime.timezone.utc),
+        8179 : datetime.datetime(2017,  5, 31, 23,  1, tzinfo=datetime.timezone.utc),
     }
 
     if disclosureDate < ipr_rfc_pub_datetime[1310]:
@@ -396,7 +396,7 @@ def email(request, id):
                 type_id = 'msgout',
                 by = request.user.person,
                 disclosure = ipr,
-                response_due = form.cleaned_data['response_due'],
+                response_due = datetime_from_date(form.cleaned_data['response_due'], DEADLINE_TZINFO),
                 message = msg,
             )
 
@@ -590,7 +590,7 @@ def notify(request, id, type):
                     type_id = form.cleaned_data['type'],
                     by = request.user.person,
                     disclosure = ipr,
-                    response_due = datetime.datetime.now().date() + datetime.timedelta(days=30),
+                    response_due = datetime_today(DEADLINE_TZINFO) + datetime.timedelta(days=30),
                     message = message,
                 )
             messages.success(request,'Notifications sent')
diff --git a/ietf/liaisons/forms.py b/ietf/liaisons/forms.py
index c66c27593..b76551953 100644
--- a/ietf/liaisons/forms.py
+++ b/ietf/liaisons/forms.py
@@ -3,7 +3,7 @@
 
 
 import io
-import datetime, os
+import os
 import operator
 
 from typing import Union            # pyflakes:ignore
@@ -34,6 +34,7 @@ from ietf.person.models import Email
 from ietf.person.fields import SearchableEmailField
 from ietf.doc.models import Document, DocAlias
 from ietf.utils.fields import DatepickerDateField
+from ietf.utils.timezone import date_today, datetime_from_date, DEADLINE_TZINFO
 from functools import reduce
 
 '''
@@ -185,9 +186,12 @@ class SearchLiaisonForm(forms.Form):
             end_date = self.cleaned_data.get('end_date')
             events = None
             if start_date:
-                events = LiaisonStatementEvent.objects.filter(type='posted', time__gte=start_date)
+                events = LiaisonStatementEvent.objects.filter(
+                    type='posted',
+                    time__gte=datetime_from_date(start_date, DEADLINE_TZINFO),
+                )
                 if end_date:
-                    events = events.filter(time__lte=end_date)
+                    events = events.filter(time__lte=datetime_from_date(end_date, DEADLINE_TZINFO))
             elif end_date:
                 events = LiaisonStatementEvent.objects.filter(type='posted', time__lte=end_date)
             if events:
@@ -222,7 +226,7 @@ class LiaisonModelForm(BetterModelForm):
     to_groups.widget.attrs['data-minimum-input-length'] = 0
     deadline = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Deadline', required=True)
     related_to = SearchableLiaisonStatementsField(label='Related Liaison Statement', required=False)
-    submitted_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Submission date', required=True, initial=datetime.date.today())
+    submitted_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Submission date', required=True, initial=lambda: date_today(DEADLINE_TZINFO))
     attachments = CustomModelMultipleChoiceField(queryset=Document.objects,label='Attachments', widget=ShowAttachmentsWidget, required=False)
     attach_title = forms.CharField(label='Title', required=False)
     attach_file = forms.FileField(label='File', required=False)
@@ -538,7 +542,7 @@ class EditLiaisonForm(LiaisonModelForm):
         super(EditLiaisonForm, self).save(*args,**kwargs)
         if self.has_changed() and 'submitted_date' in self.changed_data:
             event = self.instance.liaisonstatementevent_set.filter(type='submitted').first()
-            event.time = self.cleaned_data.get('submitted_date')
+            event.time = datetime_from_date(self.cleaned_data.get('submitted_date'), DEADLINE_TZINFO)
             event.save()
 
         return self.instance
diff --git a/ietf/liaisons/mails.py b/ietf/liaisons/mails.py
index 47f18c75b..8708c8a07 100644
--- a/ietf/liaisons/mails.py
+++ b/ietf/liaisons/mails.py
@@ -2,14 +2,14 @@
 # -*- coding: utf-8 -*-
 
 
-import datetime
-
 from django.conf import settings
 from django.template.loader import render_to_string
 
 from ietf.utils.mail import send_mail_text
 from ietf.group.models import Role
 from ietf.mailtrigger.utils import gather_address_lists
+from ietf.utils.timezone import date_today, DEADLINE_TZINFO
+
 
 def send_liaison_by_email(request, liaison):
     subject = 'New Liaison Statement, "%s"' % (liaison.title)
@@ -61,7 +61,7 @@ def possibly_send_deadline_reminder(liaison):
         0: 'today'
         }
 
-    days_to_go = (liaison.deadline - datetime.date.today()).days
+    days_to_go = (liaison.deadline - date_today(DEADLINE_TZINFO)).days
     if not (days_to_go < 0 or days_to_go in list(PREVIOUS_DAYS.keys())):
         return None # no reminder
 
diff --git a/ietf/liaisons/management/commands/check_liaison_deadlines.py b/ietf/liaisons/management/commands/check_liaison_deadlines.py
index 84faa5fed..1fe76029a 100644
--- a/ietf/liaisons/management/commands/check_liaison_deadlines.py
+++ b/ietf/liaisons/management/commands/check_liaison_deadlines.py
@@ -8,13 +8,14 @@ from django.core.management.base import BaseCommand
 
 from ietf.liaisons.models import LiaisonStatement
 from ietf.liaisons.mails import possibly_send_deadline_reminder
+from ietf.utils.timezone import date_today, DEADLINE_TZINFO
 
 
 class Command(BaseCommand):
     help = ("Check liaison deadlines and send a reminder if we are close to a deadline")
 
     def handle(self, *args, **options):
-        today = datetime.date.today()
+        today = date_today(DEADLINE_TZINFO)
         cutoff = today - datetime.timedelta(14)
 
         msgs = []
diff --git a/ietf/liaisons/tests.py b/ietf/liaisons/tests.py
index b08832ae5..845271db7 100644
--- a/ietf/liaisons/tests.py
+++ b/ietf/liaisons/tests.py
@@ -14,6 +14,8 @@ from django.conf import settings
 from django.contrib.auth.models import User
 from django.urls import reverse as urlreverse
 from django.db.models import Q
+from django.utils import timezone
+
 from io import StringIO
 from pyquery import PyQuery
 
@@ -29,6 +31,8 @@ from ietf.person.models import Person
 from ietf.group.models import Group
 from ietf.liaisons.mails import send_sdo_reminder, possibly_send_deadline_reminder
 from ietf.liaisons.views import contacts_from_roles, contact_email_from_role
+from ietf.utils.timezone import date_today, DEADLINE_TZINFO
+
 
 # -------------------------------------------------
 # Helper Functions
@@ -50,7 +54,7 @@ def get_liaison_post_data(type='incoming'):
                 to_contacts='to_contacts@example.com',
                 purpose="info",
                 title="title",
-                submitted_date=datetime.datetime.today().strftime("%Y-%m-%d"),
+                submitted_date=timezone.now().strftime("%Y-%m-%d"),
                 body="body",
                 send="1" )
 
@@ -242,7 +246,7 @@ class ManagementCommandTests(TestCase):
     def test_check_liaison_deadlines(self):
         from django.core.management import call_command
 
-        LiaisonStatementFactory(deadline=datetime.date.today()+datetime.timedelta(days=1))
+        LiaisonStatementFactory(deadline=date_today(DEADLINE_TZINFO)+datetime.timedelta(days=1))
 
         out = io.StringIO()
         mailbox_before = len(outbox)
@@ -310,7 +314,7 @@ class LiaisonManagementTests(TestCase):
         self.assertNotContains(r, 'Private comment')
 
     def test_taken_care_of(self):
-        liaison = LiaisonStatementFactory(deadline=datetime.date.today()+datetime.timedelta(days=1))
+        liaison = LiaisonStatementFactory(deadline=date_today(DEADLINE_TZINFO)+datetime.timedelta(days=1))
 
         url = urlreverse('ietf.liaisons.views.liaison_detail', kwargs=dict(object_id=liaison.pk))
         # normal get
@@ -384,8 +388,8 @@ class LiaisonManagementTests(TestCase):
         self.assertTrue(liaison.liaisonstatementevent_set.filter(type='posted'))
 
     def test_edit_liaison(self):
-        liaison = LiaisonStatementFactory(deadline=datetime.date.today()+datetime.timedelta(days=1))
-        LiaisonStatementEventFactory(statement=liaison,type_id='submitted', time=datetime.datetime.now()-datetime.timedelta(days=1))
+        liaison = LiaisonStatementFactory(deadline=date_today(DEADLINE_TZINFO) + datetime.timedelta(days=1))
+        LiaisonStatementEventFactory(statement=liaison,type_id='submitted', time=timezone.now()-datetime.timedelta(days=1))
         LiaisonStatementEventFactory(statement=liaison,type_id='posted')
         from_group = liaison.from_groups.first()
         to_group = liaison.to_groups.first()
@@ -696,7 +700,7 @@ class LiaisonManagementTests(TestCase):
         from_groups = [ str(g.pk) for g in Group.objects.filter(type="sdo") ]
         to_group = Group.objects.get(acronym="mars")
         submitter = Person.objects.get(user__username="marschairman")
-        today = datetime.date.today()
+        today = date_today()
         related_liaison = liaison
         r = self.client.post(url,
                              dict(from_groups=from_groups,
@@ -775,7 +779,7 @@ class LiaisonManagementTests(TestCase):
         from_group = Group.objects.get(acronym="mars")
         to_group = Group.objects.filter(type="sdo")[0]
         submitter = Person.objects.get(user__username="marschairman")
-        today = datetime.date.today()
+        today = date_today()
         related_liaison = liaison
         r = self.client.post(url,
                              dict(from_groups=str(from_group.pk),
@@ -843,7 +847,7 @@ class LiaisonManagementTests(TestCase):
         from_group = Group.objects.get(acronym="mars")
         to_group = Group.objects.filter(type="sdo")[0]
         submitter = Person.objects.get(user__username="marschairman")
-        today = datetime.date.today()
+        today = date_today()
         r = self.client.post(url,
                              dict(from_groups=str(from_group.pk),
                                   from_contact=submitter.email_address(),
@@ -862,7 +866,7 @@ class LiaisonManagementTests(TestCase):
         self.assertEqual(len(outbox), mailbox_before + 1)
 
     def test_liaison_add_attachment(self):
-        liaison = LiaisonStatementFactory(deadline=datetime.date.today()+datetime.timedelta(days=1))
+        liaison = LiaisonStatementFactory(deadline=date_today()+datetime.timedelta(days=1))
         LiaisonStatementEventFactory(statement=liaison,type_id='submitted')
 
         self.assertEqual(liaison.attachments.count(),0)
@@ -1021,7 +1025,7 @@ class LiaisonManagementTests(TestCase):
         LiaisonStatementEventFactory(type_id='posted', statement__body="Has recently in its body",statement__from_groups=[GroupFactory(type_id='sdo',acronym='ulm'),])
         # Statement 2
         s2 = LiaisonStatementEventFactory(type_id='posted', statement__body="That word does not occur here", statement__title="Nor does it occur here")
-        s2.time=datetime.datetime(2010,1,1)
+        s2.time=datetime.datetime(2010, 1, 1, tzinfo=datetime.timezone.utc)
         s2.save()
 
         # test list only, no search filters
@@ -1148,7 +1152,7 @@ class LiaisonManagementTests(TestCase):
         self.assertTrue('ulm-liaiman@' in outbox[-1]['To'])
 
     def test_send_liaison_deadline_reminder(self):
-        liaison = LiaisonStatementFactory(deadline=datetime.date.today()+datetime.timedelta(days=1))
+        liaison = LiaisonStatementFactory(deadline=date_today(DEADLINE_TZINFO) + datetime.timedelta(days=1))
 
         mailbox_before = len(outbox)
         possibly_send_deadline_reminder(liaison)
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 4fd4579c9..99be2dd89 100644
--- a/ietf/meeting/helpers.py
+++ b/ietf/meeting/helpers.py
@@ -17,6 +17,7 @@ from django.contrib.auth.models import AnonymousUser
 from django.urls import reverse
 from django.shortcuts import get_object_or_404
 from django.template.loader import render_to_string
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -42,7 +43,7 @@ def get_meeting(num=None,type_in=['ietf',],days=28):
     if type_in:
         meetings = meetings.filter(type__in=type_in)
     if num == None:
-        meetings = meetings.filter(date__gte=datetime.datetime.today()-datetime.timedelta(days=days)).order_by('date')
+        meetings = meetings.filter(date__gte=timezone.now()-datetime.timedelta(days=days)).order_by('date')
     else:
         meetings = meetings.filter(number=num)
     if meetings.exists():
@@ -51,7 +52,7 @@ def get_meeting(num=None,type_in=['ietf',],days=28):
         raise Http404("No such meeting found: %s" % num)
 
 def get_current_ietf_meeting():
-    meetings = Meeting.objects.filter(type='ietf',date__gte=datetime.datetime.today()-datetime.timedelta(days=31)).order_by('date')
+    meetings = Meeting.objects.filter(type='ietf',date__gte=timezone.now()-datetime.timedelta(days=31)).order_by('date')
     return meetings.first()
 
 def get_current_ietf_meeting_num():
@@ -117,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 = [ ]
@@ -1148,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
@@ -1160,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/management/commands/meetecho_conferences.py b/ietf/meeting/management/commands/meetecho_conferences.py
index e6525d7dc..8a7eb0ebc 100644
--- a/ietf/meeting/management/commands/meetecho_conferences.py
+++ b/ietf/meeting/management/commands/meetecho_conferences.py
@@ -1,7 +1,5 @@
 # Copyright The IETF Trust 2022, All Rights Reserved
 # -*- coding: utf-8 -*-
-import datetime
-
 from textwrap import dedent
 
 from django.conf import settings
@@ -9,6 +7,7 @@ from django.core.management.base import BaseCommand, CommandError
 
 from ietf.meeting.models import Session
 from ietf.utils.meetecho import ConferenceManager, MeetechoAPIError
+from ietf.utils.timezone import date_today
 
 
 class Command(BaseCommand):
@@ -85,7 +84,7 @@ class Command(BaseCommand):
         for conf in confs:
             conf_sessions[conf.id] = Session.objects.filter(
                 group__acronym=group,
-                meeting__date__gte=datetime.date.today(),
+                meeting__date__gte=date_today(),
                 remote_instructions__contains=conf.url,
             )
         return confs, conf_sessions
diff --git a/ietf/meeting/management/commands/update_important_dates.py b/ietf/meeting/management/commands/update_important_dates.py
index c1a6ad424..9a06e850a 100644
--- a/ietf/meeting/management/commands/update_important_dates.py
+++ b/ietf/meeting/management/commands/update_important_dates.py
@@ -11,6 +11,8 @@ import debug                            # pyflakes:ignore
 from ietf.name.models import ImportantDateName
 from ietf.meeting.helpers import update_important_dates
 from ietf.meeting.models import Meeting, ImportantDate
+from ietf.utils.timezone import date_today
+
 
 class Command(BaseCommand):
 
@@ -29,7 +31,7 @@ class Command(BaseCommand):
             if not meeting:
                 self.stderr.write("\nMeeting not found: %s\n" % (m, ))
                 continue
-            if meeting.date < datetime.date.today() + datetime.timedelta(days=max_offset):
+            if meeting.date < date_today(meeting.tz()) + datetime.timedelta(days=max_offset):
                 self.stderr.write("\nMeeting %s: Won't change dates for meetings in the past or close future\n" % (meeting, ))
                 continue
             self.stdout.write('\n%s\n\n' % (meeting, ))
diff --git a/ietf/meeting/migrations/0056_use_timezone_now_for_meeting_models.py b/ietf/meeting/migrations/0056_use_timezone_now_for_meeting_models.py
new file mode 100644
index 000000000..83d20fd57
--- /dev/null
+++ b/ietf/meeting/migrations/0056_use_timezone_now_for_meeting_models.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.2.28 on 2022-07-12 11:24
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('meeting', '0055_pytz_2022_2_1'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='schedulingevent',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text='When the event happened'),
+        ),
+    ]
diff --git a/ietf/meeting/migrations/0057_fill_in_empty_meeting_time_zone.py b/ietf/meeting/migrations/0057_fill_in_empty_meeting_time_zone.py
new file mode 100644
index 000000000..f009b08f3
--- /dev/null
+++ b/ietf/meeting/migrations/0057_fill_in_empty_meeting_time_zone.py
@@ -0,0 +1,47 @@
+# Generated by Django 2.2.28 on 2022-08-08 11:37
+
+import datetime
+
+from django.db import migrations
+
+
+# date of last meeting with an empty time_zone before this migration
+LAST_EMPTY_TZ = datetime.date(2022, 7, 1)
+
+
+def forward(apps, schema_editor):
+    Meeting = apps.get_model('meeting', 'Meeting')
+
+    # Check that we will be able to identify the migrated meetings later
+    old_meetings_in_pst8pdt = Meeting.objects.filter(type_id='interim', time_zone='PST8PDT', date__lte=LAST_EMPTY_TZ)
+    assert old_meetings_in_pst8pdt.count() == 0, 'not expecting interim meetings in PST8PDT time_zone'
+
+    meetings_with_empty_tz = Meeting.objects.filter(time_zone='')
+    # check our expected conditions
+    for mtg in meetings_with_empty_tz:
+        assert mtg.type_id == 'interim', 'was not expecting non-interim meetings to be affected'
+        assert mtg.date <= LAST_EMPTY_TZ, 'affected meeting outside expected date range'
+        mtg.time_zone = 'PST8PDT'
+
+    # commit the changes
+    Meeting.objects.bulk_update(meetings_with_empty_tz, ['time_zone'])
+
+
+def reverse(apps, schema_editor):
+    Meeting = apps.get_model('meeting', 'Meeting')
+    meetings_to_restore = Meeting.objects.filter(time_zone='PST8PDT', date__lte=LAST_EMPTY_TZ)
+    for mtg in meetings_to_restore:
+        mtg.time_zone = ''
+    # commit the changes
+    Meeting.objects.bulk_update(meetings_to_restore, ['time_zone'])
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('meeting', '0056_use_timezone_now_for_meeting_models'),
+    ]
+
+    operations = [
+        migrations.RunPython(forward, reverse),
+    ]
diff --git a/ietf/meeting/migrations/0058_meeting_time_zone_not_blank.py b/ietf/meeting/migrations/0058_meeting_time_zone_not_blank.py
new file mode 100644
index 000000000..0adec77a2
--- /dev/null
+++ b/ietf/meeting/migrations/0058_meeting_time_zone_not_blank.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.28 on 2022-08-25 12:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('meeting', '0057_fill_in_empty_meeting_time_zone'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='meeting',
+            name='time_zone',
+            field=models.CharField(choices=[('', '---------'), ('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Nuuk', 'America/Nuuk'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Sitka', 'America/Sitka'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Choibalsan', 'Asia/Choibalsan'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Sydney', 'Australia/Sydney'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Kyiv', 'Europe/Kyiv'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Uzhgorod', 'Europe/Uzhgorod'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zaporozhye', 'Europe/Zaporozhye'), ('Europe/Zurich', 'Europe/Zurich'), ('GMT', 'GMT'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Kanton', 'Pacific/Kanton'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('UTC', 'UTC')], default='UTC', max_length=255),
+        ),
+    ]
diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py
index 0265dad4c..83b245f76 100644
--- a/ietf/meeting/models.py
+++ b/ietf/meeting/models.py
@@ -24,6 +24,7 @@ from django.db.models import Max, Subquery, OuterRef, TextField, Value, Q
 from django.db.models.functions import Coalesce
 from django.conf import settings
 from django.urls import reverse as urlreverse
+from django.utils import timezone
 from django.utils.text import slugify
 from django.utils.safestring import mark_safe
 
@@ -40,7 +41,7 @@ from ietf.person.models import Person
 from ietf.utils.decorators import memoize
 from ietf.utils.storage import NoLocationMigrationFileSystemStorage
 from ietf.utils.text import xslugify
-from ietf.utils.timezone import date2datetime
+from ietf.utils.timezone import datetime_from_date, date_today
 from ietf.utils.models import ForeignKey
 from ietf.utils.validators import (
     MaxImageSizeValidator, WrappedValidator, validate_file_size, validate_mime_type,
@@ -85,7 +86,7 @@ class Meeting(models.Model):
     # We can't derive time-zone from country, as there are some that have
     # more than one timezone, and the pytz module doesn't provide timezone
     # lookup information for all relevant city/country combinations.
-    time_zone = models.CharField(blank=True, max_length=255, choices=timezones)
+    time_zone = models.CharField(max_length=255, choices=timezones, default='UTC')
     idsubmit_cutoff_day_offset_00 = models.IntegerField(blank=True,
         default=settings.IDSUBMIT_DEFAULT_CUTOFF_DAY_OFFSET_00,
         help_text = "The number of days before the meeting start date when the submission of -00 drafts will be closed.")
@@ -137,8 +138,21 @@ class Meeting(models.Model):
     def end_date(self):
         return self.get_meeting_date(self.days-1)
 
+    def start_datetime(self):
+        """Start-of-day on meeting.date in meeting time zone"""
+        return datetime_from_date(self.date, self.tz())
+
+    def end_datetime(self):
+        """Datetime of the first instant _after_ the meeting's last day in meeting time zone"""
+        return datetime_from_date(self.get_meeting_date(self.days), self.tz())
+
     def get_00_cutoff(self):
-        start_date = datetime.datetime(year=self.date.year, month=self.date.month, day=self.date.day, tzinfo=pytz.utc)
+        start_date = datetime.datetime(
+            year=self.date.year,
+            month=self.date.month,
+            day=self.date.day,
+            tzinfo=datetime.timezone.utc,
+        )
         importantdate = self.importantdate_set.filter(name_id='idcutoff').first()
         if not importantdate:
             importantdate = self.importantdate_set.filter(name_id='00cutoff').first()
@@ -146,7 +160,7 @@ class Meeting(models.Model):
             cutoff_date = importantdate.date
         else:
             cutoff_date = start_date + datetime.timedelta(days=ImportantDateName.objects.get(slug='idcutoff').default_offset_days)
-        cutoff_time = date2datetime(cutoff_date) + self.idsubmit_cutoff_time_utc
+        cutoff_time = datetime_from_date(cutoff_date) + self.idsubmit_cutoff_time_utc
         return cutoff_time
 
     def get_01_cutoff(self):
@@ -158,7 +172,7 @@ class Meeting(models.Model):
             cutoff_date = importantdate.date
         else:
             cutoff_date = start_date + datetime.timedelta(days=ImportantDateName.objects.get(slug='idcutoff').default_offset_days)
-        cutoff_time = date2datetime(cutoff_date) + self.idsubmit_cutoff_time_utc
+        cutoff_time = datetime_from_date(cutoff_date) + self.idsubmit_cutoff_time_utc
         return cutoff_time
 
     def get_reopen_time(self):
@@ -176,7 +190,7 @@ class Meeting(models.Model):
 
     @classmethod
     def get_current_meeting(cls, type="ietf"):
-        return cls.objects.filter(type=type, date__gte=datetime.datetime.today()-datetime.timedelta(days=7) ).order_by('date').first()
+        return cls.objects.filter(type=type, date__gte=timezone.now()-datetime.timedelta(days=7) ).order_by('date').first()
 
     def get_first_cut_off(self):
         return self.get_00_cutoff()
@@ -338,7 +352,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] = []
@@ -346,15 +360,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
@@ -370,20 +384,24 @@ 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):
-        if self.time_zone:
-            try:
-                tzfn = os.path.join(settings.TZDATA_ICS_PATH, self.time_zone + ".ics")
-                if os.path.exists(tzfn):
-                    with io.open(tzfn) as tzf:
-                        icstext = tzf.read()
-                    vtimezone = re.search("(?sm)(\nBEGIN:VTIMEZONE.*\nEND:VTIMEZONE\n)", icstext).group(1).strip()
-                    if vtimezone:
-                        vtimezone += "\n"
-                    return vtimezone
-            except IOError:
-                pass
-        return ''
+        try:
+            tzfn = os.path.join(settings.TZDATA_ICS_PATH, self.time_zone + ".ics")
+            if os.path.exists(tzfn):
+                with io.open(tzfn) as tzf:
+                    icstext = tzf.read()
+                vtimezone = re.search("(?sm)(\nBEGIN:VTIMEZONE.*\nEND:VTIMEZONE\n)", icstext).group(1).strip()
+                if vtimezone:
+                    vtimezone += "\n"
+                return vtimezone
+        except IOError:
+            pass
+        return None
 
     def set_official_schedule(self, schedule):
         if self.schedule != schedule:
@@ -391,16 +409,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):
@@ -621,41 +637,22 @@ class TimeSlot(models.Model):
         return self._cached_html_location
 
     def tz(self):
-        if not hasattr(self, '_cached_tz'):
-            if self.meeting.time_zone:
-                self._cached_tz = pytz.timezone(self.meeting.time_zone)
-            else:
-                self._cached_tz = None
-        return self._cached_tz
+        return self.meeting.tz()
 
     def tzname(self):
-        if self.tz():
-            return self.tz().tzname(self.time)
-        else:
-            return ""
+        return self.tz().tzname(self.time)
 
     def utc_start_time(self):
-        if self.tz():
-            local_start_time = self.tz().localize(self.time)
-            return local_start_time.astimezone(pytz.utc)
-        else:
-            return None
+        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):
-        if self.tz():
-            return self.tz().localize(self.time)
-        else:
-            return None
+        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):
@@ -753,7 +750,7 @@ class Schedule(models.Model):
     @property
     def is_official_record(self):
         return (self.is_official and
-                self.meeting.end_date() <= datetime.date.today() )
+                self.meeting.end_date() <= date_today() )
 
     # returns a dictionary {group -> [schedtimesessassignment+]}
     # and it has [] if the session is not placed.
@@ -1170,7 +1167,7 @@ class Session(models.Model):
         return can_manage_materials(user,self.group)
 
     def is_material_submission_cutoff(self):
-        return datetime.date.today() > self.meeting.get_submission_correction_date()
+        return date_today(self.meeting.tz()) > self.meeting.get_submission_correction_date()
     
     def joint_with_groups_acronyms(self):
         return [group.acronym for group in self.joint_with_groups.all()]
@@ -1297,7 +1294,7 @@ class Session(models.Model):
 
 class SchedulingEvent(models.Model):
     session = ForeignKey(Session)
-    time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened")
+    time = models.DateTimeField(default=timezone.now, help_text="When the event happened")
     status = ForeignKey(SessionStatusName)
     by = ForeignKey(Person)
 
diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py
index e5fdd71c5..5ecb494df 100644
--- a/ietf/meeting/test_data.py
+++ b/ietf/meeting/test_data.py
@@ -20,11 +20,15 @@ from ietf.name.models import RoomResourceName
 from ietf.person.factories import PersonFactory
 from ietf.person.models import Person
 from ietf.utils.test_data import make_test_data
+from ietf.utils.timezone import date_today
 
-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 +106,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,
@@ -201,8 +218,8 @@ def make_meeting_test_data(meeting=None, create_interims=False):
     mars_session.sessionpresentation_set.add(pres)
     
     # Future Interim Meetings
-    date = datetime.date.today() + datetime.timedelta(days=365)
-    date2 = datetime.date.today() + datetime.timedelta(days=1000)
+    date = date_today() + datetime.timedelta(days=365)
+    date2 = date_today() + datetime.timedelta(days=1000)
     ames = Group.objects.get(acronym="ames")
 
     if create_interims:
@@ -213,9 +230,9 @@ def make_meeting_test_data(meeting=None, create_interims=False):
 
     return meeting
 
-def make_interim_test_data():
-    date = datetime.date.today() + datetime.timedelta(days=365)
-    date2 = datetime.date.today() + datetime.timedelta(days=1000)
+def make_interim_test_data(meeting_tz='UTC'):
+    date = date_today() + datetime.timedelta(days=365)
+    date2 = date_today() + datetime.timedelta(days=1000)
     PersonFactory(user__username='plain')
     area = GroupFactory(type_id='area')
     ad = Person.objects.get(user__username='ad')
@@ -225,10 +242,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_helpers.py b/ietf/meeting/tests_helpers.py
index 3160f9388..d77610baa 100644
--- a/ietf/meeting/tests_helpers.py
+++ b/ietf/meeting/tests_helpers.py
@@ -18,6 +18,7 @@ from ietf.meeting.models import SchedTimeSessAssignment, Session
 from ietf.meeting.test_data import make_meeting_test_data
 from ietf.utils.meetecho import Conference
 from ietf.utils.test_utils import TestCase
+from ietf.utils.timezone import date_today
 
 
 # override the legacy office hours setting to guarantee consistency with the tests
@@ -623,12 +624,13 @@ class HelperTests(TestCase):
     def test_get_ietf_meeting(self):
         """get_ietf_meeting() should only return IETF meetings"""
         # put the IETF far in the past so it's not "current"
-        ietf = MeetingFactory(type_id='ietf', date=datetime.date.today() - datetime.timedelta(days=5 * 365))
+        today = date_today()
+        ietf = MeetingFactory(type_id='ietf', date=today- datetime.timedelta(days=5 * 365))
         # put the interim meeting now so it will be picked up as "current" if there's a bug
-        interim = MeetingFactory(type_id='interim', date=datetime.date.today())
+        interim = MeetingFactory(type_id='interim', date=today)
         self.assertEqual(get_ietf_meeting(ietf.number), ietf, 'Return IETF meeting by number')
         self.assertIsNone(get_ietf_meeting(interim.number), 'Ignore non-IETF meetings')
         self.assertIsNone(get_ietf_meeting(), 'Return None if there is no current IETF meeting')
-        ietf.date = datetime.date.today()
+        ietf.date = today
         ietf.save()
         self.assertEqual(get_ietf_meeting(), ietf, 'Return current meeting if there is one')
diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py
index 2d221994a..d93835ac6 100644
--- a/ietf/meeting/tests_js.py
+++ b/ietf/meeting/tests_js.py
@@ -10,8 +10,8 @@ import re
 from unittest import skipIf
 
 import django
+from django.utils import timezone
 from django.utils.text import slugify
-from django.utils.timezone import now
 from django.db.models import F
 import pytz
 
@@ -34,6 +34,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, datetime_from_date, date_today
 
 if selenium_enabled():
     from selenium.webdriver.common.action_chains import ActionChains
@@ -268,14 +269,32 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
         # modal_open.click()
 
         self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal").is_displayed())
-        self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal [value=\"{}\"]".format("ts-group-{}-{}".format(slot2.time.strftime("%Y%m%d-%H%M"), int(slot2.duration.total_seconds() / 60)))).click()
+        self.driver.find_element(
+            By.CSS_SELECTOR,
+            "#timeslot-group-toggles-modal [value=\"{}\"]".format(
+                "ts-group-{}-{}".format(
+                    slot2.time.astimezone(slot2.tz()).strftime("%Y%m%d-%H%M"),
+                    int(slot2.duration.total_seconds() / 60),
+                ),
+            ),
+        ).click()
         self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal [data-bs-dismiss=\"modal\"]").click()
         self.assertTrue(not self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal").is_displayed())
 
         # swap days
-        self.driver.find_element(By.CSS_SELECTOR, ".day .swap-days[data-dayid=\"{}\"]".format(slot4.time.date().isoformat())).click()
+        self.driver.find_element(
+            By.CSS_SELECTOR,
+            ".day .swap-days[data-dayid=\"{}\"]".format(
+                slot4.time.astimezone(slot4.tz()).date().isoformat(),
+            ),
+        ).click()
         self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#swap-days-modal").is_displayed())
-        self.driver.find_element(By.CSS_SELECTOR, "#swap-days-modal input[name=\"target_day\"][value=\"{}\"]".format(slot1.time.date().isoformat())).click()
+        self.driver.find_element(
+            By.CSS_SELECTOR,
+            "#swap-days-modal input[name=\"target_day\"][value=\"{}\"]".format(
+                slot1.time.astimezone(slot1.tz()).date().isoformat(),
+            ),
+        ).click()
         self.driver.find_element(By.CSS_SELECTOR, "#swap-days-modal button[type=\"submit\"]").click()
 
         self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot4.pk, s1.pk)),
@@ -305,7 +324,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
         room = RoomFactory(meeting=meeting)
 
         # get current time in meeting time zone
-        right_now = now().astimezone(
+        right_now = timezone.now().astimezone(
             pytz.timezone(meeting.time_zone)
         )
         if not settings.USE_TZ:
@@ -392,11 +411,11 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
     def test_past_swap_days_buttons(self):
         """Swap days buttons should be hidden for past items"""
         wait = WebDriverWait(self.driver, 2)
-        meeting = MeetingFactory(type_id='ietf', date=datetime.datetime.today() - datetime.timedelta(days=3), days=7)
+        meeting = MeetingFactory(type_id='ietf', date=timezone.now() - datetime.timedelta(days=3), days=7)
         room = RoomFactory(meeting=meeting)
 
         # get current time in meeting time zone
-        right_now = now().astimezone(
+        right_now = timezone.now().astimezone(
             pytz.timezone(meeting.time_zone)
         )
         if not settings.USE_TZ:
@@ -427,21 +446,24 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
 
         past_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR,
             ','.join(
-                '.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in past_timeslots
+                '.swap-days[data-start="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat())
+                for ts in past_timeslots
             )
         )
         self.assertEqual(len(past_swap_days_buttons), len(past_timeslots), 'Missing past swap days buttons')
 
         future_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR,
             ','.join(
-                '.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in future_timeslots
+                '.swap-days[data-start="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat())
+                for ts in future_timeslots
             )
         )
         self.assertEqual(len(future_swap_days_buttons), len(future_timeslots), 'Missing future swap days buttons')
 
         now_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR,
             ','.join(
-                '.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in now_timeslots
+                '.swap-days[data-start="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat())
+                for ts in now_timeslots
             )
         )
         # only one "now" button because both sessions are on the same day
@@ -492,7 +514,8 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
         self.assertFalse(
             any(radio.is_enabled()
                 for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
-                'input[name="target_day"][value="{}"]'.format(ts.time.date().isoformat()) for ts in past_timeslots)
+                'input[name="target_day"][value="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat())
+                for ts in past_timeslots)
             )),
             'Past day is enabled in swap-days modal for official schedule',
         )
@@ -501,14 +524,16 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
         self.assertTrue(
             all(radio.is_enabled()
                 for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
-                'input[name="target_day"][value="{}"]'.format(ts.time.date().isoformat()) for ts in enabled_timeslots)
+                'input[name="target_day"][value="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat())
+                for ts in enabled_timeslots)
             )),
             'Future day is not enabled in swap-days modal for official schedule',
         )
         self.assertFalse(
             any(radio.is_enabled()
                 for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
-                'input[name="target_day"][value="{}"]'.format(ts.time.date().isoformat()) for ts in now_timeslots)
+                'input[name="target_day"][value="{}"]'.format(ts.time.astimezone(ts.tz()).date().isoformat())
+                for ts in now_timeslots)
             )),
             '"Now" day is enabled in swap-days modal for official schedule',
         )
@@ -516,11 +541,11 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
     def test_past_swap_timeslot_col_buttons(self):
         """Swap timeslot column buttons should be hidden for past items"""
         wait = WebDriverWait(self.driver, 2)
-        meeting = MeetingFactory(type_id='ietf', date=datetime.datetime.today() - datetime.timedelta(days=3), days=7)
+        meeting = MeetingFactory(type_id='ietf', date=timezone.now() - datetime.timedelta(days=3), days=7)
         room = RoomFactory(meeting=meeting)
 
         # get current time in meeting time zone
-        right_now = now().astimezone(
+        right_now = timezone.now().astimezone(
             pytz.timezone(meeting.time_zone)
         )
         if not settings.USE_TZ:
@@ -806,7 +831,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
         To test for recurrence of https://trac.ietf.org/trac/ietfdb/ticket/3327 need to have some constraints that
         do not conflict. Testing with only violated constraints does not exercise the code adequately.
         """
-        meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(), populate_schedule=False)
+        meeting = MeetingFactory(type_id='ietf', date=date_today(), populate_schedule=False)
         TimeSlotFactory.create_batch(5, meeting=meeting)
         schedule = ScheduleFactory(meeting=meeting)
         sessions = SessionFactory.create_batch(5, meeting=meeting, add_to_schedule=False)
@@ -928,7 +953,7 @@ class InterimTests(IetfSeleniumTestCase):
 
         # Create a group with a plenary interim session for testing type filters
         somegroup = GroupFactory(acronym='sg', name='Some Group')
-        sg_interim = make_interim_meeting(somegroup, datetime.date.today() + datetime.timedelta(days=20))
+        sg_interim = make_interim_meeting(somegroup, date_today() + datetime.timedelta(days=20))
         sg_sess = sg_interim.session_set.first()
         sg_slot = sg_sess.timeslotassignments.first().timeslot
         sg_sess.purpose_id = 'plenary'
@@ -960,7 +985,7 @@ class InterimTests(IetfSeleniumTestCase):
             Session.objects.filter(
                 meeting__type_id='interim',
                 timeslotassignments__schedule=F('meeting__schedule'),
-                timeslotassignments__timeslot__time__gte=datetime.datetime.today()
+                timeslotassignments__timeslot__time__gte=timezone.now()
             )
         ).filter(current_status__in=('sched','canceled'))
         meetings = []
@@ -973,7 +998,7 @@ class InterimTests(IetfSeleniumTestCase):
     def all_ietf_meetings(self):
         meetings = Meeting.objects.filter(
             type_id='ietf',
-            date__gte=datetime.datetime.today()-datetime.timedelta(days=7)
+            date__gte=timezone.now()-datetime.timedelta(days=7)
         )
         for m in meetings:
             m.calendar_label = 'IETF %s' % m.number
@@ -1103,7 +1128,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 = [
@@ -1419,7 +1444,7 @@ class ProceedingsMaterialTests(IetfSeleniumTestCase):
     def setUp(self):
         super().setUp()
         self.wait = WebDriverWait(self.driver, 2)
-        self.meeting = MeetingFactory(type_id='ietf', number='123', date=datetime.date.today())
+        self.meeting = MeetingFactory(type_id='ietf', number='123', date=date_today())
 
     def test_add_proceedings_material(self):
         url = self.absreverse(
@@ -1521,7 +1546,7 @@ class EditTimeslotsTests(IetfSeleniumTestCase):
         self.meeting: Meeting = MeetingFactory(
             type_id='ietf',
             number=120,
-            date=datetime.datetime.today() + datetime.timedelta(days=10),
+            date=date_today() + datetime.timedelta(days=10),
             populate_schedule=False,
         )
         self.edit_timeslot_url = self.absreverse(
@@ -1591,22 +1616,23 @@ class EditTimeslotsTests(IetfSeleniumTestCase):
         self.do_delete_timeslot_test(cancel=True)
 
     def do_delete_time_interval_test(self, cancel=False):
-        delete_day = self.meeting.date.date()
+        delete_day = self.meeting.date
         delete_time = datetime.time(hour=10)
-        other_day = self.meeting.get_meeting_date(1).date()
+        other_day = self.meeting.get_meeting_date(1)
         other_time = datetime.time(hour=12)
         duration = datetime.timedelta(minutes=60)
 
         delete: [TimeSlot] = TimeSlotFactory.create_batch(
             2,
             meeting=self.meeting,
-            time=datetime.datetime.combine(delete_day, delete_time),
-            duration=duration)
+            time=datetime_from_date(delete_day, self.meeting.tz()).replace(hour=delete_time.hour),
+            duration=duration,
+        )
 
         keep: [TimeSlot] = [
             TimeSlotFactory(
                 meeting=self.meeting,
-                time=datetime.datetime.combine(day, time),
+                time=datetime_from_date(day, self.meeting.tz()).replace(hour=time.hour),
                 duration=duration
             )
             for (day, time) in (
@@ -1623,7 +1649,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'
                 ))
         )
@@ -1638,22 +1666,22 @@ class EditTimeslotsTests(IetfSeleniumTestCase):
         self.do_delete_time_interval_test(cancel=True)
 
     def do_delete_day_test(self, cancel=False):
-        delete_day = self.meeting.date.date()
-        times = [datetime.time(hour=10), datetime.time(hour=12)]
-        other_days = [self.meeting.get_meeting_date(d).date() for d in range(1, 3)]
+        delete_day = self.meeting.date
+        hours = [10, 12]
+        other_days = [self.meeting.get_meeting_date(d) for d in range(1, 3)]
 
         delete: [TimeSlot] = [
             TimeSlotFactory(
                 meeting=self.meeting,
-                time=datetime.datetime.combine(delete_day, time),
-            ) for time in times
+                time=datetime_from_date(delete_day, self.meeting.tz()).replace(hour=hour),
+            ) for hour in hours
         ]
 
         keep: [TimeSlot] = [
             TimeSlotFactory(
                 meeting=self.meeting,
-                time=datetime.datetime.combine(day, time),
-            ) for day in other_days for time in times
+                time=datetime_from_date(day, self.meeting.tz()).replace(hour=hour),
+            ) for day in other_days for hour in hours
         ]
 
         selector = (
diff --git a/ietf/meeting/tests_models.py b/ietf/meeting/tests_models.py
index 71f5b9361..175236611 100644
--- a/ietf/meeting/tests_models.py
+++ b/ietf/meeting/tests_models.py
@@ -3,6 +3,8 @@
 """Tests of models in the Meeting application"""
 import datetime
 
+from mock import patch
+
 from ietf.meeting.factories import MeetingFactory, SessionFactory, AttendedFactory
 from ietf.stats.factories import MeetingRegistrationFactory
 from ietf.utils.test_utils import TestCase
@@ -86,6 +88,22 @@ class MeetingTests(TestCase):
         self.assertEqual(att.onsite, 1)
         self.assertEqual(att.remote, 0)
 
+    def test_vtimezone(self):
+        # normal time zone that should have a zoneinfo file
+        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', 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', populate_schedule=False)
+        with patch('ietf.meeting.models.io.open', side_effect=IOError):
+            vtz = meeting.vtimezone()
+        self.assertIsNone(vtz)
+
 
 class SessionTests(TestCase):
     def test_chat_archive_url_with_jabber(self):
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 94a52c82b..c8f209bdc 100644
--- a/ietf/meeting/tests_views.py
+++ b/ietf/meeting/tests_views.py
@@ -21,6 +21,7 @@ from urllib.parse import urlparse, urlsplit
 from PIL import Image
 from pathlib import Path
 from tempfile import NamedTemporaryFile
+from zoneinfo import ZoneInfo
 
 from django.urls import reverse as urlreverse
 from django.conf import settings
@@ -29,8 +30,8 @@ from django.test import Client, override_settings
 from django.db.models import F, Max
 from django.http import QueryDict, FileResponse
 from django.template import Context, Template
+from django.utils import timezone
 from django.utils.text import slugify
-from django.utils.timezone import now
 
 import debug           # pyflakes:ignore
 
@@ -51,6 +52,7 @@ from ietf.name.models import SessionStatusName, ImportantDateName, RoleName, Pro
 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.timezone import date_today, time_now
 
 from ietf.person.factories import PersonFactory
 from ietf.group.factories import GroupFactory, GroupEventFactory, RoleFactory
@@ -154,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')
@@ -213,7 +215,10 @@ class MeetingTests(BaseMeetingTestCase):
         )
 
         # plain
-        time_interval = r"%s<span.*/span>-%s" % (slot.time.strftime("%H:%M").lstrip("0"), (slot.time + slot.duration).strftime("%H:%M").lstrip("0"))
+        time_interval = r"{}<span.*/span>-{}".format(
+            slot.time.astimezone(meeting.tz()).strftime("%H:%M").lstrip("0"),
+            slot.end_time().astimezone(meeting.tz()).strftime("%H:%M").lstrip("0"),
+        )
 
         # text
         # the rest of the results don't have as nicely formatted times
@@ -351,7 +356,7 @@ class MeetingTests(BaseMeetingTestCase):
     def test_interim_materials(self):
         make_meeting_test_data()
         group = Group.objects.get(acronym='mars')
-        date = datetime.datetime.today() - datetime.timedelta(days=10)
+        date = timezone.now() - datetime.timedelta(days=10)
         meeting = make_interim_meeting(group=group, date=date, status='sched')
         session = meeting.session_set.first()
 
@@ -548,7 +553,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)
         #
@@ -558,16 +568,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_parse_agenda_filter_params(self):
         def _r(show=(), hide=(), showtypes=(), hidetypes=()):
@@ -673,7 +683,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)
 
@@ -818,7 +828,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),
                     )
 
@@ -889,7 +901,7 @@ class EditMeetingScheduleTests(TestCase):
         # Meeting must be in the future so it can be edited
         meeting = MeetingFactory(
             type_id='ietf',
-            date=datetime.date.today() + datetime.timedelta(days=7),
+            date=date_today() + datetime.timedelta(days=7),
             populate_schedule=False,
         )
         meeting.schedule = ScheduleFactory(meeting=meeting)
@@ -902,7 +914,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)
@@ -997,7 +1013,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)
@@ -1179,12 +1195,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)
@@ -1218,7 +1240,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!')
@@ -1241,20 +1268,18 @@ class EditMeetingScheduleTests(TestCase):
             self.fail('Response was not valid JSON: {}'.format(err))
 
     @staticmethod
-    def _right_now_in(tzname):
-        right_now = now().astimezone(pytz.timezone(tzname))
-        if not settings.USE_TZ:
-            right_now = right_now.replace(tzinfo=None)
+    def _right_now_in(tzinfo):
+        right_now = timezone.now().astimezone(tzinfo)
         return right_now
 
     def test_assign_session(self):
         """Allow assignment to future timeslots only for official schedule"""
         meeting = MeetingFactory(
             type_id='ietf',
-            date=(datetime.datetime.today() - datetime.timedelta(days=1)).date(),
+            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,
@@ -1311,10 +1336,10 @@ class EditMeetingScheduleTests(TestCase):
         """Do not allow assignment of past sessions for official schedule"""
         meeting = MeetingFactory(
             type_id='ietf',
-            date=(datetime.datetime.today() - datetime.timedelta(days=1)).date(),
+            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,
@@ -1446,10 +1471,10 @@ class EditMeetingScheduleTests(TestCase):
         """Allow unassignment only of future timeslots for official schedule"""
         meeting = MeetingFactory(
             type_id='ietf',
-            date=(datetime.datetime.today() - datetime.timedelta(days=1)).date(),
+            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,
@@ -1531,7 +1556,7 @@ class EditMeetingScheduleTests(TestCase):
         """Schedule editor should not crash when there are no timeslots"""
         meeting = MeetingFactory(
             type_id='ietf',
-            date=datetime.date.today() + datetime.timedelta(days=7),
+            date=date_today() + datetime.timedelta(days=7),
             populate_schedule=False,
         )
         meeting.schedule = ScheduleFactory(meeting=meeting)
@@ -1545,6 +1570,58 @@ class EditMeetingScheduleTests(TestCase):
         self.assertContains(r, 'No timeslots exist')
         self.assertContains(r, urlreverse('ietf.meeting.views.edit_timeslots', kwargs={'num': meeting.number}))
 
+    def test_editor_time_zone(self):
+        """Agenda editor should show meeting time zone"""
+        time_zone = 'Etc/GMT+8'
+        meeting_tz = ZoneInfo(time_zone)
+        meeting = MeetingFactory(
+            type_id='ietf',
+            date=date_today(meeting_tz) + datetime.timedelta(days=7),
+            populate_schedule=False,
+            time_zone=time_zone,
+        )
+        meeting.schedule = ScheduleFactory(meeting=meeting)
+        meeting.save()
+        timeslot = TimeSlotFactory(meeting=meeting)
+        ts_start = timeslot.time.astimezone(meeting_tz)
+        ts_end = timeslot.end_time().astimezone(meeting_tz)
+        url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number})
+        self.assertTrue(self.client.login(username='secretary', password='secretary+password'))
+        r = self.client.get(url)
+        self.assertEqual(r.status_code, 200)
+        pq = PyQuery(r.content)
+
+        day_header = pq('.day-flow .day-label')
+        self.assertIn(ts_start.strftime('%A'), day_header.text())
+
+        day_swap = day_header.find('.swap-days')
+        self.assertEqual(day_swap.attr('data-dayid'), ts_start.date().isoformat())
+        self.assertEqual(day_swap.attr('data-start'), ts_start.date().isoformat())
+
+        time_label = pq('.day-flow .time-header .time-label')
+        self.assertEqual(len(time_label), 1)
+        # strftime() does not seem to support hours without leading 0, so do this manually
+        time_label_string = f'{ts_start.hour:d}:{ts_start.minute:02d} - {ts_end.hour:d}:{ts_end.minute:02d}'
+        self.assertIn(time_label_string, time_label.text())
+        self.assertEqual(time_label.attr('data-start'), ts_start.astimezone(datetime.timezone.utc).isoformat())
+        self.assertEqual(time_label.attr('data-end'), ts_end.astimezone(datetime.timezone.utc).isoformat())
+
+        ts_swap = time_label.find('.swap-timeslot-col')
+        origin_label = ts_swap.attr('data-origin-label')
+        # testing the exact date in origin_label is hard because Django's date filter uses
+        # different month formats than Python's strftime, so just check a couple parts.
+        self.assertIn(ts_start.strftime('%A'), origin_label)
+        self.assertIn(f'{ts_start.hour:d}:{ts_start.minute:02d}-{ts_end.hour:d}:{ts_end.minute:02d}', origin_label)
+
+        timeslot_elt = pq(f'#timeslot{timeslot.pk}')
+        self.assertEqual(len(timeslot_elt), 1)
+        self.assertEqual(timeslot_elt.attr('data-start'), ts_start.astimezone(datetime.timezone.utc).isoformat())
+        self.assertEqual(timeslot_elt.attr('data-end'), ts_end.astimezone(datetime.timezone.utc).isoformat())
+
+        timeslot_label = pq(f'#timeslot{timeslot.pk} .time-label')
+        self.assertEqual(len(timeslot_label), 1)
+        self.assertIn(time_label_string, timeslot_label.text())
+
 
 class EditTimeslotsTests(TestCase):
     def login(self, username='secretary'):
@@ -1570,7 +1647,7 @@ class EditTimeslotsTests(TestCase):
         return MeetingFactory(
             type_id='ietf',
             number=number,
-            date=datetime.datetime.today() + datetime.timedelta(days=10),
+            date=date_today() + datetime.timedelta(days=10),
             populate_schedule=False,
         )
 
@@ -1602,7 +1679,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"""
@@ -1771,7 +1849,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)
@@ -1781,7 +1859,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
@@ -1862,17 +1940,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),
+                    )
                 ),
             )
 
@@ -1971,10 +2053,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()
@@ -1991,7 +2071,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()
@@ -2171,8 +2251,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'])
@@ -2181,7 +2261,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',
@@ -2204,8 +2284,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'])
@@ -2419,8 +2499,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(
@@ -2450,10 +2530,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')
@@ -2593,7 +2673,7 @@ class ReorderSlidesTests(TestCase):
     def test_add_slides_to_session(self):
         for type_id in ('ietf','interim'):
             chair_role = RoleFactory(name_id='chair')
-            session = SessionFactory(group=chair_role.group, meeting__date=datetime.date.today()-datetime.timedelta(days=90), meeting__type_id=type_id)
+            session = SessionFactory(group=chair_role.group, meeting__date=date_today() - datetime.timedelta(days=90), meeting__type_id=type_id)
             slides = DocumentFactory(type_id='slides')
             url = urlreverse('ietf.meeting.views.ajax_add_slides_to_session', kwargs={'session_id':session.pk, 'num':session.meeting.number})
 
@@ -2609,7 +2689,7 @@ class ReorderSlidesTests(TestCase):
             self.assertEqual(r.status_code, 403)
             self.assertIn('materials cutoff', unicontent(r))
 
-            session.meeting.date = datetime.date.today()
+            session.meeting.date = date_today()
             session.meeting.save()
 
             # Invalid order
@@ -2696,7 +2776,7 @@ class ReorderSlidesTests(TestCase):
     def test_remove_slides_from_session(self):
         for type_id in ['ietf','interim']:
             chair_role = RoleFactory(name_id='chair')
-            session = SessionFactory(group=chair_role.group, meeting__date=datetime.date.today()-datetime.timedelta(days=90), meeting__type_id=type_id)
+            session = SessionFactory(group=chair_role.group, meeting__date=date_today()-datetime.timedelta(days=90), meeting__type_id=type_id)
             slides = DocumentFactory(type_id='slides')
             url = urlreverse('ietf.meeting.views.ajax_remove_slides_from_session', kwargs={'session_id':session.pk, 'num':session.meeting.number})
 
@@ -2712,7 +2792,7 @@ class ReorderSlidesTests(TestCase):
             self.assertEqual(r.status_code, 403)
             self.assertIn('materials cutoff', unicontent(r))
 
-            session.meeting.date = datetime.date.today()
+            session.meeting.date = date_today()
             session.meeting.save()
 
             # Invalid order
@@ -2807,7 +2887,7 @@ class ReorderSlidesTests(TestCase):
 
     def test_reorder_slides_in_session(self):
         chair_role = RoleFactory(name_id='chair')
-        session = SessionFactory(group=chair_role.group, meeting__date=datetime.date.today()-datetime.timedelta(days=90))
+        session = SessionFactory(group=chair_role.group, meeting__date=date_today() - datetime.timedelta(days=90))
         sp_list = SessionPresentationFactory.create_batch(5, document__type_id='slides', session=session)
         for num, sp in enumerate(sp_list, start=1):
             sp.order = num
@@ -2817,7 +2897,7 @@ class ReorderSlidesTests(TestCase):
         for type_id in ['ietf','interim']:
             
             session.meeting.type_id = type_id
-            session.meeting.date = datetime.date.today()-datetime.timedelta(days=90)
+            session.meeting.date = date_today()-datetime.timedelta(days=90)
             session.meeting.save()
 
             # Not a valid user
@@ -2832,7 +2912,7 @@ class ReorderSlidesTests(TestCase):
             self.assertEqual(r.status_code, 403)
             self.assertIn('materials cutoff', unicontent(r))
 
-            session.meeting.date = datetime.date.today()
+            session.meeting.date = date_today()
             session.meeting.save()
 
             # Bad index values
@@ -2899,7 +2979,7 @@ class ReorderSlidesTests(TestCase):
 
     def test_slide_order_reconditioning(self):
         chair_role = RoleFactory(name_id='chair')
-        session = SessionFactory(group=chair_role.group, meeting__date=datetime.date.today()-datetime.timedelta(days=90))
+        session = SessionFactory(group=chair_role.group, meeting__date=date_today() - datetime.timedelta(days=90))
         sp_list = SessionPresentationFactory.create_batch(5, document__type_id='slides', session=session)
         for num, sp in enumerate(sp_list, start=1):
             sp.order = 2*num
@@ -2918,7 +2998,7 @@ class EditTests(TestCase):
 
     def test_official_record_schedule_is_read_only(self):
         def _set_date_offset_and_retrieve_page(meeting, days_offset, client):
-            meeting.date = datetime.date.today() + datetime.timedelta(days=days_offset)
+            meeting.date = date_today() + datetime.timedelta(days=days_offset)
             meeting.save()
             client.login(username="secretary", password="secretary+password")
             url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number))
@@ -3001,7 +3081,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'))
 
@@ -3223,7 +3305,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)
@@ -3265,7 +3352,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)
@@ -3710,7 +3802,7 @@ class SessionDetailsTests(TestCase):
     def test_session_details(self):
 
         group = GroupFactory.create(type_id='wg',state_id='active')
-        session = SessionFactory.create(meeting__type_id='ietf',group=group, meeting__date=datetime.date.today()+datetime.timedelta(days=90))
+        session = SessionFactory.create(meeting__type_id='ietf',group=group, meeting__date=date_today() + datetime.timedelta(days=90))
         SessionPresentationFactory.create(session=session,document__type_id='draft',rev=None)
         SessionPresentationFactory.create(session=session,document__type_id='minutes')
         SessionPresentationFactory.create(session=session,document__type_id='slides')
@@ -3729,7 +3821,7 @@ class SessionDetailsTests(TestCase):
         session = SessionFactory.create(
             meeting__type_id='ietf',
             group=group,
-            meeting__date=datetime.date.today() + datetime.timedelta(days=90),
+            meeting__date=date_today() + datetime.timedelta(days=90),
         )
         session_details_url = urlreverse(
             'ietf.meeting.views.session_details',
@@ -3781,7 +3873,7 @@ class SessionDetailsTests(TestCase):
     def test_session_details_past_interim(self):
         group = GroupFactory.create(type_id='wg',state_id='active')
         chair = RoleFactory(name_id='chair',group=group)
-        session = SessionFactory.create(meeting__type_id='interim',group=group, meeting__date=datetime.date.today()-datetime.timedelta(days=90))
+        session = SessionFactory.create(meeting__type_id='interim',group=group, meeting__date=date_today() - datetime.timedelta(days=90))
         SessionPresentationFactory.create(session=session,document__type_id='draft',rev=None)
         SessionPresentationFactory.create(session=session,document__type_id='minutes')
         SessionPresentationFactory.create(session=session,document__type_id='slides')
@@ -3800,7 +3892,7 @@ class SessionDetailsTests(TestCase):
         group = GroupFactory.create(type_id='wg',state_id='active')
         group_chair = PersonFactory.create()
         group.role_set.create(name_id='chair',person = group_chair, email = group_chair.email())
-        session = SessionFactory.create(meeting__type_id='ietf',group=group, meeting__date=datetime.date.today()+datetime.timedelta(days=90))
+        session = SessionFactory.create(meeting__type_id='ietf',group=group, meeting__date=date_today() + datetime.timedelta(days=90))
         SessionPresentationFactory.create(session=session,document__type_id='draft',rev=None)
         old_draft = session.sessionpresentation_set.filter(document__type='draft').first().document
         new_draft = DocumentFactory(type_id='draft')
@@ -3966,7 +4058,7 @@ class InterimTests(TestCase):
     def do_interim_skip_announcement_test(self, base_session=False, extra_session=False, canceled_session=False):
         make_meeting_test_data()
         group = Group.objects.get(acronym='irg')
-        date = datetime.date.today() + datetime.timedelta(days=30)
+        date = date_today() + datetime.timedelta(days=30)
         meeting = make_interim_meeting(group=group, date=date, status='scheda')
         session = meeting.session_set.first()
         if base_session:
@@ -4036,12 +4128,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)
@@ -4247,7 +4337,7 @@ class InterimTests(TestCase):
         self.do_interim_approve_by_secretariat_test(extra_session=True, canceled_session=True, base_session=True)
 
     def test_past(self):
-        today = datetime.date.today()
+        today = date_today()
         last_week = today - datetime.timedelta(days=7)
         ietf = SessionFactory(meeting__type_id='ietf',meeting__date=last_week,group__state_id='active',group__parent=GroupFactory(state_id='active'))
         SessionFactory(meeting__type_id='interim',meeting__date=last_week,status_id='canceled',group__state_id='active',group__parent=GroupFactory(state_id='active'))
@@ -4266,7 +4356,7 @@ class InterimTests(TestCase):
         if querystring is not None:
             url += '?' + querystring
 
-        today = datetime.date.today()
+        today = date_today()
         interims = dict(
             mars=add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='mars')).filter(current_status='sched').first().meeting,
             ames=add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='ames')).filter(current_status='canceled').first().meeting,
@@ -4386,9 +4476,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 = datetime.datetime.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.'
@@ -4457,13 +4547,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 = datetime.datetime.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'
@@ -4504,16 +4595,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 = datetime.datetime.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'
@@ -4570,9 +4662,9 @@ class InterimTests(TestCase):
 
     def test_interim_request_multi_day_non_consecutive(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=2)
-        time = datetime.datetime.now().time().replace(microsecond=0,second=0)
+        time = timezone.now().time().replace(microsecond=0,second=0)
         group = Group.objects.get(acronym='mars')
         city = 'San Francisco'
         country = 'US'
@@ -4607,7 +4699,7 @@ class InterimTests(TestCase):
     def test_interim_request_multi_day_cancel(self):
         """All sessions of a multi-day interim request should be canceled"""
         length_before = len(outbox)
-        date = datetime.date.today()+datetime.timedelta(days=15)
+        date = date_today() + datetime.timedelta(days=15)
 
         # Set up an interim request with several sessions 
         num_sessions = 3
@@ -4630,7 +4722,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)
@@ -4640,14 +4732,15 @@ class InterimTests(TestCase):
         if date.year != date2.year:
             date += datetime.timedelta(days=1)
             date2 += datetime.timedelta(days=1)
-        time = datetime.datetime.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'
@@ -4789,14 +4882,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))
@@ -4804,7 +4897,7 @@ class InterimTests(TestCase):
     def test_interim_request_details_announcement(self):
         '''Test access to Announce / Skip Announce features'''
         make_meeting_test_data()
-        date = datetime.date.today() + datetime.timedelta(days=30)
+        date = date_today() + datetime.timedelta(days=30)
         group = Group.objects.get(acronym='mars')
         meeting = make_interim_meeting(group=group, date=date, status='scheda')
         url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number})
@@ -5322,7 +5415,7 @@ class InterimTests(TestCase):
     def test_send_interim_minutes_reminder(self):
         make_meeting_test_data()
         group = Group.objects.get(acronym='mars')
-        date = datetime.datetime.today() - datetime.timedelta(days=10)
+        date = timezone.now() - datetime.timedelta(days=10)
         meeting = make_interim_meeting(group=group, date=date, status='sched')
         length_before = len(outbox)
         send_interim_minutes_reminder(meeting=meeting)
@@ -5337,7 +5430,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)
         #
@@ -5347,8 +5444,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, })
@@ -5539,7 +5636,7 @@ class MaterialsTests(TestCase):
     def test_upload_bluesheets_interim_chair_access(self):
         make_meeting_test_data()
         mars = Group.objects.get(acronym='mars')
-        session=SessionFactory(meeting__type_id='interim',group=mars, meeting__date = datetime.date.today())
+        session=SessionFactory(meeting__type_id='interim',group=mars, meeting__date = date_today())
         url = urlreverse('ietf.meeting.views.upload_session_bluesheets',kwargs={'num':session.meeting.number,'session_id':session.id})
         self.client.login(username="marschairman", password="marschairman+password")
         r = self.client.get(url)
@@ -5782,7 +5879,7 @@ class MaterialsTests(TestCase):
         for type_id in ['ietf','interim']:
             session = SessionFactory(meeting__type_id=type_id)
             chair = RoleFactory(group=session.group,name_id='chair').person
-            session.meeting.importantdate_set.create(name_id='revsub',date=datetime.date.today()+datetime.timedelta(days=20))
+            session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20))
             newperson = PersonFactory()
             
             session_overview_url = urlreverse('ietf.meeting.views.session_details',kwargs={'num':session.meeting.number,'acronym':session.group.acronym})
@@ -5830,7 +5927,7 @@ class MaterialsTests(TestCase):
 
     def test_disapprove_proposed_slides(self):
         submission = SlideSubmissionFactory()
-        submission.session.meeting.importantdate_set.create(name_id='revsub',date=datetime.date.today()+datetime.timedelta(days=20))
+        submission.session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20))
         self.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(), 1)
         chair = RoleFactory(group=submission.session.group,name_id='chair').person
         url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number})
@@ -5848,7 +5945,7 @@ class MaterialsTests(TestCase):
     def test_approve_proposed_slides(self):
         submission = SlideSubmissionFactory()
         session = submission.session
-        session.meeting.importantdate_set.create(name_id='revsub',date=datetime.date.today()+datetime.timedelta(days=20))
+        session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20))
         chair = RoleFactory(group=submission.session.group,name_id='chair').person
         url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number})
         login_testing_unauthorized(self, chair.user.username, url)
@@ -5873,7 +5970,7 @@ class MaterialsTests(TestCase):
         submission = SlideSubmissionFactory(session__meeting__type_id='ietf')
         session1 = submission.session
         session2 = SessionFactory(group=submission.session.group, meeting=submission.session.meeting)
-        submission.session.meeting.importantdate_set.create(name_id='revsub',date=datetime.date.today()+datetime.timedelta(days=20))
+        submission.session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20))
         chair = RoleFactory(group=submission.session.group,name_id='chair').person
         url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number})
         login_testing_unauthorized(self, chair.user.username, url)
@@ -5890,7 +5987,7 @@ class MaterialsTests(TestCase):
         submission = SlideSubmissionFactory(session__meeting__type_id='ietf')
         session1 = submission.session
         session2 = SessionFactory(group=submission.session.group, meeting=submission.session.meeting)
-        submission.session.meeting.importantdate_set.create(name_id='revsub',date=datetime.date.today()+datetime.timedelta(days=20))
+        submission.session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20))
         chair = RoleFactory(group=submission.session.group,name_id='chair').person
         url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number})
         login_testing_unauthorized(self, chair.user.username, url)
@@ -5904,7 +6001,7 @@ class MaterialsTests(TestCase):
     def test_submit_and_approve_multiple_versions(self):
         session = SessionFactory(meeting__type_id='ietf')
         chair = RoleFactory(group=session.group,name_id='chair').person
-        session.meeting.importantdate_set.create(name_id='revsub',date=datetime.date.today()+datetime.timedelta(days=20))
+        session.meeting.importantdate_set.create(name_id='revsub',date=date_today()+datetime.timedelta(days=20))
         newperson = PersonFactory()
         
         propose_url = urlreverse('ietf.meeting.views.propose_session_slides', kwargs={'session_id':session.pk, 'num': session.meeting.number})          
@@ -6285,8 +6382,8 @@ class HasMeetingsTests(TestCase):
         self.assertEqual(r.status_code, 200)
         q = PyQuery(r.content)
         self.assertTrue(q('#id_group option[value="%d"]'%group.pk))
-        date = datetime.date.today() + datetime.timedelta(days=30+meeting_count)
-        time = datetime.datetime.now().time().replace(microsecond=0,second=0)
+        date = date_today() + datetime.timedelta(days=30+meeting_count)
+        time = time_now().replace(microsecond=0,second=0)
         remote_instructions = 'Use webex'
         agenda = 'Intro. Slides. Discuss.'
         agenda_note = 'On second level'
@@ -6382,7 +6479,7 @@ class HasMeetingsTests(TestCase):
             session = SessionFactory(
                 group__type_id = gf.type_id,
                 meeting__type_id='interim', 
-                meeting__date = datetime.datetime.today()+datetime.timedelta(days=30),
+                meeting__date = timezone.now()+datetime.timedelta(days=30),
                 status_id='sched',
             )
             sessions.append(session)
@@ -6398,7 +6495,7 @@ class HasMeetingsTests(TestCase):
         sessions=[]
         for gf in GroupFeatures.objects.filter(has_meetings=True):
             group = GroupFactory(type_id=gf.type_id)
-            meeting_date = datetime.datetime.today() + datetime.timedelta(days=30)
+            meeting_date = timezone.now() + datetime.timedelta(days=30)
             session = SessionFactory(
                 group=group,
                 meeting__type_id='interim', 
@@ -6419,7 +6516,7 @@ class HasMeetingsTests(TestCase):
         sessions=[]
         for gf in GroupFeatures.objects.filter(has_meetings=True):
             group = GroupFactory(type_id=gf.type_id)
-            meeting_date = datetime.datetime.today() + datetime.timedelta(days=30)
+            meeting_date = timezone.now() + datetime.timedelta(days=30)
             session = SessionFactory(
                 group=group,
                 meeting__type_id='interim', 
@@ -7145,7 +7242,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'))
@@ -7254,7 +7351,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 aae286682..e8efb92ad 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
 
@@ -12,6 +13,7 @@ from urllib.error import HTTPError
 from django.conf import settings
 from django.contrib import messages
 from django.template.loader import render_to_string
+from django.utils import timezone
 from django.utils.encoding import smart_text
 
 import debug                            # pyflakes:ignore
@@ -26,6 +28,7 @@ from ietf.person.models import Person
 from ietf.secr.proceedings.proc_utils import import_audio_files
 from ietf.utils.html import sanitize_document
 from ietf.utils.log import log
+from ietf.utils.timezone import date_today
 
 
 def session_time_for_sorting(session, use_meeting_date):
@@ -33,13 +36,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()
@@ -64,12 +71,12 @@ def group_sessions(sessions):
 
     sessions = sorted(sessions,key=lambda s:s.time)
 
-    today = datetime.date.today()
     future = []
     in_progress = []
     recent = []
     past = []
     for s in sessions:
+        today = date_today(s.meeting.tz())
         if s.meeting.date > today:
             future.append(s)
         elif s.meeting.end_date() >= today:
@@ -101,7 +108,7 @@ def get_upcoming_manageable_sessions(user):
     # .filter(date__gte=today - F('days')), but unfortunately, it
     # doesn't work correctly with Django 1.11 and MySQL/SQLite
 
-    today = datetime.date.today()
+    today = date_today()
 
     candidate_sessions = add_event_info_to_session_qs(
         Session.objects.filter(meeting__date__gte=today - datetime.timedelta(days=15))
@@ -158,7 +165,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 ]
@@ -322,7 +334,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'):
@@ -511,7 +525,7 @@ def swap_meeting_schedule_timeslot_assignments(schedule, source_timeslots, targe
             if max_overlap > datetime.timedelta(minutes=5):
                 for a in lts_assignments:
                     a.timeslot = most_overlapping_rts
-                    a.modified = datetime.datetime.now()
+                    a.modified = timezone.now()
                     a.save()
                 swapped = True
 
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index a50ba1ad9..c9e22cdf1 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -36,10 +36,10 @@ from django.db.models import F, Max, Q
 from django.forms.models import modelform_factory, inlineformset_factory
 from django.template import TemplateDoesNotExist
 from django.template.loader import render_to_string
+from django.utils import timezone
 from django.utils.encoding import force_str
 from django.utils.functional import curry
 from django.utils.text import slugify
-from django.utils.timezone import now
 from django.views.decorators.cache import cache_page
 from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
 from django.views.generic import RedirectView
@@ -97,6 +97,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,
@@ -128,22 +129,23 @@ 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()
-    old = datetime.datetime.now() - datetime.timedelta(days=1)
+    now = date_today()
+    old = timezone.now() - datetime.timedelta(days=1)
     if settings.SERVER_MODE != 'production' and '_testoverride' in request.GET:
         pass
     elif now > cor_cut_off_date:
         if meeting.number.isdigit() and int(meeting.number) > 96:
             return redirect('ietf.meeting.views.proceedings', num=meeting.number)
         else:
-            return render(request, "meeting/materials_upload_closed.html", {
-                'meeting_num': meeting.number,
-                'begin_date': begin_date,
-                'cut_off_date': cut_off_date,
-                'cor_cut_off_date': cor_cut_off_date
-            })
+            with timezone.override(meeting.tz()):
+                return render(request, "meeting/materials_upload_closed.html", {
+                    'meeting_num': meeting.number,
+                    'begin_date': begin_date,
+                    'cut_off_date': cut_off_date,
+                    '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)
 
@@ -177,23 +179,24 @@ def materials(request, num=None):
         for type_name in ProceedingsMaterialTypeName.objects.all()
     ]
 
-    return render(request, "meeting/materials.html", {
-        'meeting': meeting,
-        'proceedings_materials': proceedings_materials,
-        'plenaries': plenaries,
-        'ietf': ietf,
-        'training': training,
-        'irtf': irtf,
-        'iab': iab,
-        'other': other,
-        'cut_off_date': cut_off_date,
-        'cor_cut_off_date': cor_cut_off_date,
-        'submission_started': now > begin_date,
-        'old': old,
-    })
+    with timezone.override(meeting.tz()):
+        return render(request, "meeting/materials.html", {
+            'meeting': meeting,
+            'proceedings_materials': proceedings_materials,
+            'plenaries': plenaries,
+            'ietf': ietf,
+            'training': training,
+            'irtf': irtf,
+            'iab': iab,
+            'other': other,
+            'cut_off_date': cut_off_date,
+            'cor_cut_off_date': cor_cut_off_date,
+            'submission_started': now > begin_date,
+            'old': old,
+        })
 
 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)
@@ -282,60 +285,61 @@ def materials_editable_groups(request, num=None):
 def edit_timeslots(request, num=None):
 
     meeting = get_meeting(num)
+    with timezone.override(meeting.tz()):
+        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)
+
+        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,
+                                          })
 
-    # 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,
-                                      })
 
 class NewScheduleForm(forms.ModelForm):
     class Meta:
@@ -443,9 +447,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
 
     lock_time = settings.MEETING_SESSION_LOCK_TIME
     def timeslot_locked(ts):
-        meeting_now = now().astimezone(pytz.timezone(meeting.time_zone))
-        if not settings.USE_TZ:
-            meeting_now = meeting_now.replace(tzinfo=None)
+        meeting_now = timezone.now().astimezone(meeting.tz())
         return schedule.is_official and (ts.time - meeting_now < lock_time)
 
     if not can_see:
@@ -642,7 +644,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
             rd = room_data[t.location_id]
             rd['timeslot_count'] += 1
             rd['start_and_duration'].append((t.time, t.duration))
-            ttd = t.time.date()
+            ttd = t.local_start_time().date()  # date in meeting timezone
             all_days.add(ttd)
             if ttd not in rd['timeslots_by_day']:
                 rd['timeslots_by_day'][ttd] = []
@@ -786,7 +788,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
                         timeslot=old_timeslot,
                     )
 
-                existing_assignments.update(timeslot=timeslot, modified=datetime.datetime.now())
+                existing_assignments.update(timeslot=timeslot, modified=timezone.now())
             else:
                 SchedTimeSessAssignment.objects.create(
                     session=session,
@@ -827,8 +829,8 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
             source_day = swap_days_form.cleaned_data['source_day']
             target_day = swap_days_form.cleaned_data['target_day']
 
-            source_timeslots = [ts for ts in timeslots_qs if ts.time.date() == source_day]
-            target_timeslots = [ts for ts in timeslots_qs if ts.time.date() == target_day]
+            source_timeslots = [ts for ts in timeslots_qs if ts.local_start_time().date() == source_day]
+            target_timeslots = [ts for ts in timeslots_qs if ts.local_start_time().date() == target_day]
             if any(timeslot_locked(ts) for ts in source_timeslots + target_timeslots):
                 return HttpResponseBadRequest("Can't swap these days.")
 
@@ -885,8 +887,8 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
     # possible timeslot start/ends
     timeslot_groups = defaultdict(set)
     for ts in timeslots_qs:
-        ts.start_end_group = "ts-group-{}-{}".format(ts.time.strftime("%Y%m%d-%H%M"), int(ts.duration.total_seconds() / 60))
-        timeslot_groups[ts.time.date()].add((ts.time, ts.end_time(), ts.start_end_group))
+        ts.start_end_group = "ts-group-{}-{}".format(ts.local_start_time().strftime("%Y%m%d-%H%M"), int(ts.duration.total_seconds() / 60))
+        timeslot_groups[ts.local_start_time().date()].add((ts.local_start_time(), ts.local_end_time(), ts.start_end_group))
 
     # prepare sessions
     prepare_sessions_for_display(sessions)
@@ -967,22 +969,23 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
             if ts_type.slug in session_data.get('enabled_timeslot_types', [])
         ]
 
-    return render(request, "meeting/edit_meeting_schedule.html", {
-        'meeting': meeting,
-        'schedule': schedule,
-        'can_edit': can_edit,
-        'can_edit_properties': can_edit or secretariat,
-        'secretariat': secretariat,
-        'days': days,
-        'timeslot_groups': sorted((d, list(sorted(t_groups))) for d, t_groups in timeslot_groups.items()),
-        'unassigned_sessions': unassigned_sessions,
-        'session_parents': session_parents,
-        'session_purposes': session_purposes,
-        'timeslot_types': timeslot_types,
-        'hide_menu': True,
-        'lock_time': lock_time,
-        'enabled_timeslot_types': enabled_timeslot_types,
-    })
+    with timezone.override(meeting.tz()):
+        return render(request, "meeting/edit_meeting_schedule.html", {
+            'meeting': meeting,
+            'schedule': schedule,
+            'can_edit': can_edit,
+            'can_edit_properties': can_edit or secretariat,
+            'secretariat': secretariat,
+            'days': days,
+            'timeslot_groups': sorted((d, list(sorted(t_groups))) for d, t_groups in timeslot_groups.items()),
+            'unassigned_sessions': unassigned_sessions,
+            'session_parents': session_parents,
+            'session_purposes': session_purposes,
+            'timeslot_types': timeslot_types,
+            'hide_menu': True,
+            'lock_time': lock_time,
+            'enabled_timeslot_types': enabled_timeslot_types,
+        })
 
 
 class RoomNameModelChoiceField(forms.ModelChoiceField):
@@ -990,7 +993,7 @@ class RoomNameModelChoiceField(forms.ModelChoiceField):
         return obj.name
 
 class TimeSlotForm(forms.Form):
-    day = forms.TypedChoiceField(coerce=lambda t: datetime.datetime.strptime(t, "%Y-%m-%d").date())
+    day = forms.TypedChoiceField(coerce=lambda t: datetime.datetime.strptime(t, "%Y-%m-%d").date())  # all dates, no tz
     time = forms.TimeField()
     duration = CustomDurationField() # this is just to make 1:30 turn into 1.5 hours instead of 1.5 minutes
     location = RoomNameModelChoiceField(queryset=Room.objects.all(), required=False, empty_label="(No location)")
@@ -1030,8 +1033,8 @@ class TimeSlotForm(forms.Form):
 
         if timeslot:
             self.initial = {
-                'day': timeslot.time.date(),
-                'time': timeslot.time.time(),
+                'day': timeslot.local_start_time().date(),
+                'time': timeslot.local_start_time().time(),
                 'duration': timeslot.duration,
                 'location': timeslot.location_id,
                 'show_location': timeslot.show_location,
@@ -1110,230 +1113,239 @@ def edit_meeting_timeslots_and_misc_sessions(request, num=None, owner=None, name
 
     can_edit = has_role(request.user, 'Secretariat')
 
-    if request.method == 'GET' and request.GET.get('action') == "edit-timeslot":
-        timeslot_pk = request.GET.get('timeslot')
-        if not timeslot_pk or not timeslot_pk.isdecimal():
-            raise Http404
-        timeslot = get_object_or_404(timeslot_qs, pk=timeslot_pk)
+    with timezone.override(meeting.tz()):
+        if request.method == 'GET' and request.GET.get('action') == "edit-timeslot":
+            timeslot_pk = request.GET.get('timeslot')
+            if not timeslot_pk or not timeslot_pk.isdecimal():
+                raise Http404
+            timeslot = get_object_or_404(timeslot_qs, pk=timeslot_pk)
 
-        assigned_session = add_event_info_to_session_qs(Session.objects.filter(
-            timeslotassignments__schedule__in=[schedule, schedule.base],
-            timeslotassignments__timeslot=timeslot,
-        )).first()
+            assigned_session = add_event_info_to_session_qs(Session.objects.filter(
+                timeslotassignments__schedule__in=[schedule, schedule.base],
+                timeslotassignments__timeslot=timeslot,
+            )).first()
 
-        timeslot.can_cancel = not assigned_session or assigned_session.current_status not in ['canceled', 'canceled', 'resched']
+            timeslot.can_cancel = not assigned_session or assigned_session.current_status not in ['canceled', 'canceled', 'resched']
 
-        return JsonResponse({
-            'form': render_to_string("meeting/edit_timeslot_form.html", {
-                'timeslot_form_action': 'edit',
-                'timeslot_form': TimeSlotForm(meeting, schedule, timeslot=timeslot),
-                'timeslot': timeslot,
-                'schedule': schedule,
-                'meeting': meeting,
-                'can_edit': can_edit,
-            }, request=request)
-        })
+            return JsonResponse({
+                'form': render_to_string("meeting/edit_timeslot_form.html", {
+                    'timeslot_form_action': 'edit',
+                    'timeslot_form': TimeSlotForm(meeting, schedule, timeslot=timeslot),
+                    'timeslot': timeslot,
+                    'schedule': schedule,
+                    'meeting': meeting,
+                    'can_edit': can_edit,
+                }, request=request)
+            })
 
-    scroll = request.POST.get('scroll')
+        scroll = request.POST.get('scroll')
 
-    def redirect_with_scroll():
-        url = request.get_full_path()
-        if scroll and scroll.isdecimal():
-            url += "#scroll={}".format(scroll)
-        return HttpResponseRedirect(url)
+        def redirect_with_scroll():
+            url = request.get_full_path()
+            if scroll and scroll.isdecimal():
+                url += "#scroll={}".format(scroll)
+            return HttpResponseRedirect(url)
 
-    add_timeslot_form = None
-    if request.method == 'POST' and request.POST.get('action') == 'add-timeslot' and can_edit:
-        add_timeslot_form = TimeSlotForm(meeting, schedule, request.POST)
-        if add_timeslot_form.is_valid():
-            c = add_timeslot_form.cleaned_data
+        add_timeslot_form = None
+        if request.method == 'POST' and request.POST.get('action') == 'add-timeslot' and can_edit:
+            add_timeslot_form = TimeSlotForm(meeting, schedule, request.POST)
+            if add_timeslot_form.is_valid():
+                c = add_timeslot_form.cleaned_data
 
-            timeslot, created = TimeSlot.objects.get_or_create(
-                meeting=meeting,
-                type=c['type'],
-                name=c['name'],
-                time=datetime.datetime.combine(c['day'], c['time']),
-                duration=c['duration'],
-                location=c['location'],
-                show_location=c['show_location'],
-            )
+                timeslot, created = TimeSlot.objects.get_or_create(
+                    meeting=meeting,
+                    type=c['type'],
+                    name=c['name'],
+                    time=meeting.tz().localize(datetime.datetime.combine(c['day'], c['time'])),
+                    duration=c['duration'],
+                    location=c['location'],
+                    show_location=c['show_location'],
+                )
+
+                if timeslot.type_id != 'regular':
+                    if not created:
+                        Session.objects.filter(timeslotassignments__timeslot=timeslot).delete()
+
+                    session = Session.objects.create(
+                        meeting=meeting,
+                        name=c['name'],
+                        short=c['short'],
+                        group=c['group'],
+                        type=c['type'],
+                        purpose=c['purpose'],
+                        agenda_note=c.get('agenda_note') or "",
+                    )
+
+                    SchedulingEvent.objects.create(
+                        session=session,
+                        status=SessionStatusName.objects.get(slug='sched'),
+                        by=request.user.person,
+                    )
+
+                    SchedTimeSessAssignment.objects.create(
+                        timeslot=timeslot,
+                        session=session,
+                        schedule=schedule
+                    )
+
+                return redirect_with_scroll()
+
+        edit_timeslot_form = None
+        if request.method == 'POST' and request.POST.get('action') == 'edit-timeslot' and can_edit:
+            timeslot_pk = request.POST.get('timeslot')
+            if not timeslot_pk or not timeslot_pk.isdecimal():
+                raise Http404
+
+            timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk)
+
+            edit_timeslot_form = TimeSlotForm(meeting, schedule, request.POST, timeslot=timeslot)
+            if edit_timeslot_form.is_valid() and edit_timeslot_form.active_assignment.schedule_id == schedule.pk:
+
+                c = edit_timeslot_form.cleaned_data
+
+                timeslot.type = c['type']
+                timeslot.name = c['name']
+                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']
+                timeslot.save()
+
+                session = Session.objects.filter(
+                    timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None],
+                    timeslotassignments__timeslot=timeslot,
+                ).select_related('group').first()
+
+                if session:
+                    if timeslot.type_id != 'regular':
+                        session.name = c['name']
+                        session.short = c['short']
+                        session.group = c['group']
+                        session.type = c['type']
+                    session.agenda_note = c.get('agenda_note') or ""
+                    session.save()
+
+                return redirect_with_scroll()
+
+        if request.method == 'POST' and request.POST.get('action') == 'cancel-timeslot' and can_edit:
+            timeslot_pk = request.POST.get('timeslot')
+            if not timeslot_pk or not timeslot_pk.isdecimal():
+                raise Http404
+
+            timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk)
+            if timeslot.type_id != 'break':
+                sessions = add_event_info_to_session_qs(
+                    Session.objects.filter(timeslotassignments__schedule=schedule, timeslotassignments__timeslot=timeslot),
+                ).exclude(current_status__in=['canceled', 'resched'])
+                for session in sessions:
+                    SchedulingEvent.objects.create(
+                        session=session,
+                        status=SessionStatusName.objects.get(slug='canceled'),
+                        by=request.user.person,
+                    )
+
+            return redirect_with_scroll()
+
+        if request.method == 'POST' and request.POST.get('action') == 'delete-timeslot' and can_edit:
+            timeslot_pk = request.POST.get('timeslot')
+            if not timeslot_pk or not timeslot_pk.isdecimal():
+                raise Http404
+
+            timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk)
 
             if timeslot.type_id != 'regular':
-                if not created:
-                    Session.objects.filter(timeslotassignments__timeslot=timeslot).delete()
+                for session in Session.objects.filter(timeslotassignments__schedule=schedule, timeslotassignments__timeslot=timeslot):
+                    for doc in session.materials.all():
+                        doc.set_state(State.objects.get(type=doc.type_id, slug='deleted'))
+                        e = DocEvent(doc=doc, rev=doc.rev, by=request.user.person, type='deleted')
+                        e.desc = "Deleted meeting session"
+                        e.save()
 
-                session = Session.objects.create(
+                    session.delete()
+
+            timeslot.delete()
+
+            return redirect_with_scroll()
+
+        sessions_by_pk = {
+            s.pk: s for s in
+            add_event_info_to_session_qs(
+                Session.objects.filter(
                     meeting=meeting,
-                    name=c['name'],
-                    short=c['short'],
-                    group=c['group'],
-                    type=c['type'],
-                    purpose=c['purpose'],
-                    agenda_note=c.get('agenda_note') or "",
-                )
+                ).order_by('pk'),
+                requested_time=True,
+                requested_by=True,
+            ).filter(
+                current_status__in=['appr', 'schedw', 'scheda', 'sched', 'canceled', 'canceledpa', 'resched']
+            ).prefetch_related(
+                'group', 'group', 'group__type',
+            )
+        }
 
-                SchedulingEvent.objects.create(
-                    session=session,
-                    status=SessionStatusName.objects.get(slug='sched'),
-                    by=request.user.person,
-                )
+        assignments_by_timeslot = defaultdict(list)
+        for a in SchedTimeSessAssignment.objects.filter(schedule__in=[schedule, schedule.base]):
+            assignments_by_timeslot[a.timeslot_id].append(a)
 
-                SchedTimeSessAssignment.objects.create(
-                    timeslot=timeslot,
-                    session=session,
-                    schedule=schedule
-                )
+        days = [meeting.date + datetime.timedelta(days=i) for i in range(meeting.days)]
 
-            return redirect_with_scroll()
+        timeslots_by_day_and_room = defaultdict(list)
+        for t in timeslot_qs:
+            timeslots_by_day_and_room[(t.time.date(), t.location_id)].append(t)
 
-    edit_timeslot_form = None
-    if request.method == 'POST' and request.POST.get('action') == 'edit-timeslot' and can_edit:
-        timeslot_pk = request.POST.get('timeslot')
-        if not timeslot_pk or not timeslot_pk.isdecimal():
-            raise Http404
+        # 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)
 
-        timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk)
+        day_grid = []
+        for d in days:
+            room_timeslots = []
+            for r in rooms:
+                ts = []
+                for t in timeslots_by_day_and_room.get((d, r.pk), []):
+                    # FIXME: the database (as of 2020) contains spurious
+                    # regular timeslots in rooms not intended for regular
+                    # sessions - once those are gone, this filter can go
+                    # away
+                    if t.type_id == 'regular' and not any(t.slug == 'regular' for t in r.session_types.all()):
+                        continue
 
-        edit_timeslot_form = TimeSlotForm(meeting, schedule, request.POST, timeslot=timeslot)
-        if edit_timeslot_form.is_valid() and edit_timeslot_form.active_assignment.schedule_id == schedule.pk:
+                    t.assigned_sessions = []
+                    for a in assignments_by_timeslot.get(t.pk, []):
+                        s = sessions_by_pk.get(a.session_id)
+                        if s:
+                            t.assigned_sessions.append(s)
 
-            c = edit_timeslot_form.cleaned_data
+                    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)
 
-            timeslot.type = c['type']
-            timeslot.name = c['name']
-            timeslot.time = datetime.datetime.combine(c['day'], c['time'])
-            timeslot.duration = c['duration']
-            timeslot.location = c['location']
-            timeslot.show_location = c['show_location']
-            timeslot.save()
+                room_timeslots.append((r, ts))
 
-            session = Session.objects.filter(
-                timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None],
-                timeslotassignments__timeslot=timeslot,
-            ).select_related('group').first()
+            day_grid.append({
+                'day': d,
+                'room_timeslots': room_timeslots
+            })
 
-            if session:
-                if timeslot.type_id != 'regular':
-                    session.name = c['name']
-                    session.short = c['short']
-                    session.group = c['group']
-                    session.type = c['type']
-                session.agenda_note = c.get('agenda_note') or ""
-                session.save()
-
-            return redirect_with_scroll()
-
-    if request.method == 'POST' and request.POST.get('action') == 'cancel-timeslot' and can_edit:
-        timeslot_pk = request.POST.get('timeslot')
-        if not timeslot_pk or not timeslot_pk.isdecimal():
-            raise Http404
-
-        timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk)
-        if timeslot.type_id != 'break':
-            sessions = add_event_info_to_session_qs(
-                Session.objects.filter(timeslotassignments__schedule=schedule, timeslotassignments__timeslot=timeslot),
-            ).exclude(current_status__in=['canceled', 'resched'])
-            for session in sessions:
-                SchedulingEvent.objects.create(
-                    session=session,
-                    status=SessionStatusName.objects.get(slug='canceled'),
-                    by=request.user.person,
-                )
-
-        return redirect_with_scroll()
-
-    if request.method == 'POST' and request.POST.get('action') == 'delete-timeslot' and can_edit:
-        timeslot_pk = request.POST.get('timeslot')
-        if not timeslot_pk or not timeslot_pk.isdecimal():
-            raise Http404
-
-        timeslot = get_object_or_404(TimeSlot, pk=timeslot_pk)
-
-        if timeslot.type_id != 'regular':
-            for session in Session.objects.filter(timeslotassignments__schedule=schedule, timeslotassignments__timeslot=timeslot):
-                for doc in session.materials.all():
-                    doc.set_state(State.objects.get(type=doc.type_id, slug='deleted'))
-                    e = DocEvent(doc=doc, rev=doc.rev, by=request.user.person, type='deleted')
-                    e.desc = "Deleted meeting session"
-                    e.save()
-
-                session.delete()
-
-        timeslot.delete()
-
-        return redirect_with_scroll()
-    
-    sessions_by_pk = {
-        s.pk: s for s in
-        add_event_info_to_session_qs(
-            Session.objects.filter(
-                meeting=meeting,
-            ).order_by('pk'),
-            requested_time=True,
-            requested_by=True,
-        ).filter(
-            current_status__in=['appr', 'schedw', 'scheda', 'sched', 'canceled', 'canceledpa', 'resched']
-        ).prefetch_related(
-            'group', 'group', 'group__type',
-        )
-    }
-
-    assignments_by_timeslot = defaultdict(list)
-    for a in SchedTimeSessAssignment.objects.filter(schedule__in=[schedule, schedule.base]):
-        assignments_by_timeslot[a.timeslot_id].append(a)
-
-    days = [meeting.date + datetime.timedelta(days=i) for i in range(meeting.days)]
-
-    timeslots_by_day_and_room = defaultdict(list)
-    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)])
-    min_max_delta = datetime.datetime.combine(meeting.date, max_time) - datetime.datetime.combine(meeting.date, min_time)
-
-    day_grid = []
-    for d in days:
-        room_timeslots = []
-        for r in rooms:
-            ts = []
-            for t in timeslots_by_day_and_room.get((d, r.pk), []):
-                # FIXME: the database (as of 2020) contains spurious
-                # regular timeslots in rooms not intended for regular
-                # sessions - once those are gone, this filter can go
-                # away
-                if t.type_id == 'regular' and not any(t.slug == 'regular' for t in r.session_types.all()):
-                    continue
-
-                t.assigned_sessions = []
-                for a in assignments_by_timeslot.get(t.pk, []):
-                    s = sessions_by_pk.get(a.session_id)
-                    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
-                t.layout_width = min(100.0 * t.duration / min_max_delta, 100 - t.left_offset)
-                ts.append(t)
-
-            room_timeslots.append((r, ts))
-
-        day_grid.append({
-            'day': d,
-            'room_timeslots': room_timeslots
+        return render(request, "meeting/edit_meeting_timeslots_and_misc_sessions.html", {
+            'meeting': meeting,
+            'schedule': schedule,
+            'can_edit': can_edit,
+            'day_grid': day_grid,
+            'empty_timeslot_form': TimeSlotForm(meeting, schedule),
+            'add_timeslot_form': add_timeslot_form,
+            'edit_timeslot_form': edit_timeslot_form,
+            'scroll': scroll,
+            'hide_menu': True,
         })
 
-    return render(request, "meeting/edit_meeting_timeslots_and_misc_sessions.html", {
-        'meeting': meeting,
-        'schedule': schedule,
-        'can_edit': can_edit,
-        'day_grid': day_grid,
-        'empty_timeslot_form': TimeSlotForm(meeting, schedule),
-        'add_timeslot_form': add_timeslot_form,
-        'edit_timeslot_form': edit_timeslot_form,
-        'scroll': scroll,
-        'hide_menu': True,
-    })
-
 
 class SchedulePropertiesForm(forms.ModelForm):
     class Meta:
@@ -1553,19 +1565,26 @@ def agenda_plain(request, num=None, name=None, base=None, ext=None, owner=None,
 
     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": datetime.datetime.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
+    with timezone.override(display_timezone):
+        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(meeting.tz()),
+                "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],
+        )
 
     return rendered_page
 
@@ -2191,16 +2210,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
 
@@ -2275,7 +2292,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) 
@@ -2343,8 +2362,8 @@ 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),
-                    'now': datetime.datetime.now(),
+                    'thisweek': datetime_today()-datetime.timedelta(days=7),
+                    'now': timezone.now(),
                     'use_codimd': True if meeting.date>=settings.MEETING_USES_CODIMD_DATE else False,
                   })
 
@@ -3432,7 +3451,7 @@ def interim_request_edit(request, number):
 @cache_page(60*60)
 def past(request):
     '''List of past meetings'''
-    today = datetime.datetime.today()
+    today = timezone.now()
 
     meetings = data_for_meetings_overview(Meeting.objects.filter(date__lte=today).order_by('-date'))
 
@@ -3442,7 +3461,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))
@@ -3505,8 +3524,8 @@ def upcoming(request):
                   'menu_actions': actions,
                   'menu_entries': menu_entries,
                   'selected_menu_entry': selected_menu_entry,
-                  'now': datetime.datetime.now(),
-                  'use_codimd': True if datetime.date.today()>=settings.MEETING_USES_CODIMD_DATE else False,
+                  'now': timezone.now(),
+                  'use_codimd': (date_today() >= settings.MEETING_USES_CODIMD_DATE),
                   })
 
 
@@ -3520,7 +3539,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'))
@@ -3552,9 +3571,12 @@ def upcoming_ical(request):
     ietfs = [m for m in meetings if m.type_id == 'ietf']
     preprocess_meeting_important_dates(ietfs)
 
+    meeting_vtz = {meeting.vtimezone() for meeting in meetings}
+    meeting_vtz.discard(None)
+
     # icalendar response file should have '\r\n' line endings per RFC5545
     response = render_to_string('meeting/upcoming.ics', {
-        'vtimezones': ''.join(sorted(list({meeting.vtimezone() for meeting in meetings if meeting.vtimezone()}))),
+        'vtimezones': ''.join(sorted(meeting_vtz)),
         'assignments': assignments,
         'ietfs': ietfs,
     }, request=request)
@@ -3567,7 +3589,7 @@ def upcoming_ical(request):
 
 def upcoming_json(request):
     '''Return Upcoming meetings in json format'''
-    today = datetime.date.today()
+    today = date_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'))
@@ -3598,7 +3620,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(
@@ -3627,20 +3649,21 @@ def proceedings(request, num=None):
                 meeting_sessions.append(s)
         ietf_areas.append((area, meeting_sessions, not_meeting_sessions))
 
-    return render(request, "meeting/proceedings.html", {
-        'meeting': meeting,
-        'plenaries': plenaries, 'ietf': ietf, 'training': training, 'irtf': irtf, 'iab': iab,
-        'ietf_areas': ietf_areas,
-        'cut_off_date': cut_off_date,
-        'cor_cut_off_date': cor_cut_off_date,
-        'submission_started': now > begin_date,
-        'cache_version': cache_version,
-        'attendance': meeting.get_attendance(),
-        'meetinghost_logo': {
-            'max_height': settings.MEETINGHOST_LOGO_MAX_DISPLAY_HEIGHT,
-            'max_width': settings.MEETINGHOST_LOGO_MAX_DISPLAY_WIDTH,
-        }
-    })
+    with timezone.override(meeting.tz()):
+        return render(request, "meeting/proceedings.html", {
+            'meeting': meeting,
+            'plenaries': plenaries, 'ietf': ietf, 'training': training, 'irtf': irtf, 'iab': iab,
+            'ietf_areas': ietf_areas,
+            'cut_off_date': cut_off_date,
+            'cor_cut_off_date': cor_cut_off_date,
+            'submission_started': now > begin_date,
+            'cache_version': cache_version,
+            'attendance': meeting.get_attendance(),
+            'meetinghost_logo': {
+                'max_height': settings.MEETINGHOST_LOGO_MAX_DISPLAY_HEIGHT,
+                'max_width': settings.MEETINGHOST_LOGO_MAX_DISPLAY_WIDTH,
+            }
+        })
 
 @role_required('Secretariat')
 def finalize_proceedings(request, num=None):
@@ -3836,7 +3859,7 @@ def api_upload_chatlog(request):
     try:
         apidata = json.loads(apidata_post)
     except json.decoder.JSONDecodeError:
-        return err(400, "Malformed post") 
+        return err(400, "Malformed post")
     if not ( 'session_id' in apidata and type(apidata['session_id']) is int ):
         return err(400, "Malformed post")
     session_id = apidata['session_id']
@@ -3860,7 +3883,7 @@ def api_upload_chatlog(request):
     write_doc_for_session(session, 'chatlog', filename, json.dumps(apidata['chatlog']))
     e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev)
     doc.save_with_history([e])
-    return HttpResponse("Done", status=200, content_type='text/plain')  
+    return HttpResponse("Done", status=200, content_type='text/plain')
 
 @require_api_key
 @role_required('Recording Manager')
@@ -3876,7 +3899,7 @@ def api_upload_polls(request):
     try:
         apidata = json.loads(apidata_post)
     except json.decoder.JSONDecodeError:
-        return err(400, "Malformed post") 
+        return err(400, "Malformed post")
     if not ( 'session_id' in apidata and type(apidata['session_id']) is int ):
         return err(400, "Malformed post")
     session_id = apidata['session_id']
@@ -3900,7 +3923,7 @@ def api_upload_polls(request):
     write_doc_for_session(session, 'polls', filename, json.dumps(apidata['polls']))
     e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev)
     doc.save_with_history([e])
-    return HttpResponse("Done", status=200, content_type='text/plain') 
+    return HttpResponse("Done", status=200, content_type='text/plain')
 
 @require_api_key
 @role_required('Recording Manager', 'Secretariat')
@@ -3974,7 +3997,7 @@ def important_dates(request, num=None, output_format=None):
     base_num = int(meeting.number)
 
     user = request.user
-    today = datetime.date.today()
+    today = date_today()
     meetings = []
     if meeting.show_important_dates or meeting.date < today:
         meetings.append(meeting)
@@ -4026,23 +4049,24 @@ 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)
+    with timezone.override(meeting.tz()):  # specifies current_timezone used for rendering and form handling
+        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,
+        )
 
 
 @role_required('Secretariat')
@@ -4053,7 +4077,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(
@@ -4241,7 +4265,7 @@ def approve_proposed_slides(request, slidesubmission_id, num):
         }
         form = ApproveSlidesForm(show_apply_to_all_checkbox, initial=initial )
 
-    return render(request, "meeting/approve_proposed_slides.html", 
+    return render(request, "meeting/approve_proposed_slides.html",
                   {'submission': submission,
                    'session_number': session_number,
                    'existing_doc' : existing_doc,
diff --git a/ietf/message/management/commands/show_messages.py b/ietf/message/management/commands/show_messages.py
index b232da481..8741b2129 100644
--- a/ietf/message/management/commands/show_messages.py
+++ b/ietf/message/management/commands/show_messages.py
@@ -6,6 +6,7 @@ import email
 import datetime
 
 from django.core.management.base import BaseCommand
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -25,7 +26,7 @@ class Command(BaseCommand):
     """
 
     def add_arguments(self, parser):
-        default_start = datetime.datetime.now() - datetime.timedelta(days=14)
+        default_start = timezone.now() - datetime.timedelta(days=14)
         parser.add_argument(
             '-t', '--start', '--from', type=str, default=default_start.strftime('%Y-%m-%d %H:%M'),
             help='Limit the list to messages saved after the given time (default %(default)s).',
diff --git a/ietf/message/migrations/0012_use_timezone_now_for_message_models.py b/ietf/message/migrations/0012_use_timezone_now_for_message_models.py
new file mode 100644
index 000000000..dbef893ea
--- /dev/null
+++ b/ietf/message/migrations/0012_use_timezone_now_for_message_models.py
@@ -0,0 +1,24 @@
+# Generated by Django 2.2.28 on 2022-07-12 11:24
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('message', '0011_auto_20201109_0439'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='message',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AlterField(
+            model_name='sendqueue',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+    ]
diff --git a/ietf/message/models.py b/ietf/message/models.py
index 01162a4b6..fe2c8d325 100644
--- a/ietf/message/models.py
+++ b/ietf/message/models.py
@@ -2,10 +2,10 @@
 # -*- coding: utf-8 -*-
 
 
-import datetime
 import email.utils
 
 from django.db import models
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -17,7 +17,7 @@ from ietf.utils.models import ForeignKey
 from ietf.utils.mail import get_email_addresses_from_text
 
 class Message(models.Model):
-    time = models.DateTimeField(default=datetime.datetime.now)
+    time = models.DateTimeField(default=timezone.now)
     by = ForeignKey(Person)
 
     subject = models.CharField(max_length=255)
@@ -62,7 +62,7 @@ class MessageAttachment(models.Model):
 
 
 class SendQueue(models.Model):
-    time = models.DateTimeField(default=datetime.datetime.now)
+    time = models.DateTimeField(default=timezone.now)
     by = ForeignKey(Person)
     
     message = ForeignKey(Message)
diff --git a/ietf/message/tests.py b/ietf/message/tests.py
index 4ab790699..a027df447 100644
--- a/ietf/message/tests.py
+++ b/ietf/message/tests.py
@@ -5,6 +5,7 @@
 import datetime
 
 from django.urls import reverse as urlreverse
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -14,10 +15,13 @@ from ietf.message.utils import send_scheduled_message_from_send_queue
 from ietf.person.models import Person
 from ietf.utils.mail import outbox, send_mail_text, send_mail_message, get_payload_text
 from ietf.utils.test_utils import TestCase
+from ietf.utils.timezone import date_today
+
+
 
 class MessageTests(TestCase):
     def test_message_view(self):
-        nomcom = GroupFactory(name="nomcom%s" % datetime.date.today().year, type_id="nomcom")
+        nomcom = GroupFactory(name="nomcom%s" % date_today().year, type_id="nomcom")
         msg = Message.objects.create(
             by=Person.objects.get(name="(System)"),
             subject="This is a test",
@@ -87,7 +91,7 @@ class SendScheduledAnnouncementsTests(TestCase):
         q = SendQueue.objects.create(
             by=Person.objects.get(name="(System)"),
             message=msg,
-            send_at=datetime.datetime.now() + datetime.timedelta(hours=12)
+            send_at=timezone.now() + datetime.timedelta(hours=12)
             )
 
         mailbox_before = len(outbox)
@@ -113,7 +117,7 @@ class SendScheduledAnnouncementsTests(TestCase):
         q = SendQueue.objects.create(
             by=Person.objects.get(name="(System)"),
             message=msg,
-            send_at=datetime.datetime.now() + datetime.timedelta(hours=12)
+            send_at=timezone.now() + datetime.timedelta(hours=12)
             )
         
         mailbox_before = len(outbox)
diff --git a/ietf/message/utils.py b/ietf/message/utils.py
index 65c018a39..2601eccab 100644
--- a/ietf/message/utils.py
+++ b/ietf/message/utils.py
@@ -2,8 +2,9 @@
 # -*- coding: utf-8 -*-
 
 
-import re, datetime, email
+import re, email
 
+from django.utils import timezone
 from django.utils.encoding import force_str
 
 from ietf.utils.mail import send_mail_text, send_mail_mime
@@ -52,7 +53,7 @@ def send_scheduled_message_from_send_queue(queue_item):
         send_mail_mime(None, message.to, message.frm, message.subject,
                        msg, cc=message.cc, bcc=message.bcc)
 
-    queue_item.sent_at = datetime.datetime.now()
+    queue_item.sent_at = timezone.now()
     queue_item.save()
 
     queue_item.message.sent = queue_item.sent_at
diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json
index 6208e1fe9..b17cf80ba 100644
--- a/ietf/name/fixtures/names.json
+++ b/ietf/name/fixtures/names.json
@@ -16116,7 +16116,7 @@
     "fields": {
       "command": "xym",
       "switch": "--version",
-      "time": "2022-10-12T00:09:36.629",
+      "time": "2022-10-12T00:09:36.629Z",
       "used": true,
       "version": "xym 0.5"
     },
@@ -16127,7 +16127,7 @@
     "fields": {
       "command": "pyang",
       "switch": "--version",
-      "time": "2022-10-12T00:09:36.945",
+      "time": "2022-10-12T00:09:36.945Z",
       "used": true,
       "version": "pyang 2.5.3"
     },
@@ -16138,7 +16138,7 @@
     "fields": {
       "command": "yanglint",
       "switch": "--version",
-      "time": "2022-10-12T00:09:36.957",
+      "time": "2022-10-12T00:09:36.957Z",
       "used": true,
       "version": "yanglint SO 1.9.2"
     },
@@ -16149,7 +16149,7 @@
     "fields": {
       "command": "xml2rfc",
       "switch": "--version",
-      "time": "2022-10-12T00:09:37.890",
+      "time": "2022-10-12T00:09:37.890Z",
       "used": true,
       "version": "xml2rfc 3.14.2"
     },
diff --git a/ietf/nomcom/management/commands/send_reminders.py b/ietf/nomcom/management/commands/send_reminders.py
index 3dce0a361..bc1042543 100644
--- a/ietf/nomcom/management/commands/send_reminders.py
+++ b/ietf/nomcom/management/commands/send_reminders.py
@@ -2,13 +2,14 @@
 # -*- coding: utf-8 -*-
 
 
-import datetime
 import syslog
 
 from django.core.management.base import BaseCommand
 
 from ietf.nomcom.models import NomCom, NomineePosition
 from ietf.nomcom.utils import send_accept_reminder_to_nominee,send_questionnaire_reminder_to_nominee
+from ietf.utils.timezone import date_today
+
 
 def log(message):
     syslog.syslog(message)
@@ -27,10 +28,10 @@ class Command(BaseCommand):
         for nomcom in NomCom.objects.filter(group__state__slug='active'):
             nps = NomineePosition.objects.filter(nominee__nomcom=nomcom,nominee__duplicated__isnull=True)
             for nominee_position in nps.pending():
-                if is_time_to_send(nomcom, datetime.date.today(), nominee_position.time.date()):
+                if is_time_to_send(nomcom, date_today(), nominee_position.time.date()):
                     send_accept_reminder_to_nominee(nominee_position)
                     log('Sent accept reminder to %s' % nominee_position.nominee.email.address)
             for nominee_position in nps.accepted().without_questionnaire_response():
-                if is_time_to_send(nomcom, datetime.date.today(), nominee_position.time.date()):
+                if is_time_to_send(nomcom, date_today(), nominee_position.time.date()):
                     send_questionnaire_reminder_to_nominee(nominee_position)
                     log('Sent questionnaire reminder to %s' % nominee_position.nominee.email.address)
diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py
index 27f55329b..0577ca558 100644
--- a/ietf/nomcom/tests.py
+++ b/ietf/nomcom/tests.py
@@ -17,6 +17,7 @@ from django.conf import settings
 from django.core.files import File
 from django.contrib.auth.models import User
 from django.urls import reverse
+from django.utils import timezone
 from django.utils.encoding import force_str
 
 import debug                            # pyflakes:ignore
@@ -49,6 +50,8 @@ from ietf.stats.models import MeetingRegistration
 from ietf.stats.factories import MeetingRegistrationFactory
 from ietf.utils.mail import outbox, empty_outbox, get_payload_text
 from ietf.utils.test_utils import login_testing_unauthorized, TestCase, unicontent
+from ietf.utils.timezone import date_today, datetime_today, datetime_from_date, DEADLINE_TZINFO
+
 
 client_test_cert_files = None
 
@@ -1091,7 +1094,7 @@ class ReminderTest(TestCase):
         rai = Position.objects.get(nomcom=self.nomcom,name='RAI')
         iab = Position.objects.get(nomcom=self.nomcom,name='IAB')
 
-        today = datetime.date.today()
+        today = datetime_today()
         t_minus_3 = today - datetime.timedelta(days=3)
         t_minus_4 = today - datetime.timedelta(days=4)
         e1 = EmailFactory(address="nominee1@example.org", person=PersonFactory(name="Nominee 1"), origin='test')
@@ -1127,7 +1130,7 @@ class ReminderTest(TestCase):
 
     def test_is_time_to_send(self):
         self.nomcom.reminder_interval = 4
-        today = datetime.date.today()
+        today = date_today()
         self.assertTrue(is_time_to_send(self.nomcom,today+datetime.timedelta(days=4),today))
         for delta in range(4):
             self.assertFalse(is_time_to_send(self.nomcom,today+datetime.timedelta(days=delta),today))
@@ -1233,7 +1236,7 @@ class InactiveNomcomTests(TestCase):
             self.assertIn( 'closed', q('.alert-warning').text())
 
     def test_acceptance_closed(self):
-        today = datetime.date.today().strftime('%Y%m%d')
+        today = date_today().strftime('%Y%m%d')
         pid = self.nc.position_set.first().nomineeposition_set.order_by('pk').first().id 
         url = reverse('ietf.nomcom.views.process_nomination_status', kwargs = {
                       'year' : self.nc.year(),
@@ -1389,7 +1392,7 @@ class FeedbackLastSeenTests(TestCase):
             f.nominees.add(self.nominee)
         f = FeedbackFactory.create(author=self.author,nomcom=self.nc,type_id='comment')
         f.topics.add(self.topic)
-        now = datetime.datetime.now() 
+        now = timezone.now() 
         self.hour_ago = now - datetime.timedelta(hours=1)
         self.half_hour_ago = now - datetime.timedelta(minutes=30)
         self.second_from_now = now + datetime.timedelta(seconds=1)
@@ -1889,7 +1892,7 @@ Junk body for testing
             assert year >= 1990
             return (year-1985)*3+2
         # Create meetings to ensure we have the 'last 5'
-        meeting_start = first_meeting_of_year(datetime.date.today().year-2)
+        meeting_start = first_meeting_of_year(date_today().year-2)
         # Populate the meeting registration records
         for number in range(meeting_start, meeting_start+10):
             meeting = MeetingFactory.create(type_id='ietf', number=number)
@@ -2241,7 +2244,8 @@ class EligibilityUnitTests(TestCase):
     def test_get_eligibility_date(self):
 
         # No Nomcoms exist:
-        self.assertEqual(get_eligibility_date(), datetime.date(datetime.date.today().year,5,1))
+        this_year = date_today().year
+        self.assertEqual(get_eligibility_date(), datetime.date(this_year,5,1))
 
         # a provided date trumps anything in the database
         self.assertEqual(get_eligibility_date(date=datetime.date(2001,2,3)), datetime.date(2001,2,3))
@@ -2255,7 +2259,7 @@ class EligibilityUnitTests(TestCase):
         n.save()
         self.assertEqual(get_eligibility_date(nomcom=n), datetime.date(2015,5,17))
         # No nomcoms in the database with seated members
-        self.assertEqual(get_eligibility_date(), datetime.date(datetime.date.today().year,5,1))
+        self.assertEqual(get_eligibility_date(), datetime.date(this_year,5,1))
 
         RoleFactory(group=n.group,name_id='member')
         self.assertEqual(get_eligibility_date(),datetime.date(2016,5,1))
@@ -2263,7 +2267,6 @@ class EligibilityUnitTests(TestCase):
         NomComFactory(group__acronym='nomcom2016', populate_personnel=False, first_call_for_volunteers=datetime.date(2016,5,4))
         self.assertEqual(get_eligibility_date(),datetime.date(2016,5,4))
 
-        this_year = datetime.date.today().year
         NomComFactory(group__acronym=f'nomcom{this_year}', first_call_for_volunteers=datetime.date(this_year,5,6))
         self.assertEqual(get_eligibility_date(),datetime.date(this_year,5,6))
 
@@ -2417,7 +2420,8 @@ class rfc8989EligibilityTests(TestCase):
 
         nobody=PersonFactory()
         for nomcom in self.nomcoms:
-            before_elig_date = nomcom.first_call_for_volunteers - datetime.timedelta(days=5)
+            elig_datetime = datetime_from_date(nomcom.first_call_for_volunteers, DEADLINE_TZINFO)
+            before_elig_date = elig_datetime - datetime.timedelta(days=5)
 
             chair = RoleFactory(name_id='chair',group__time=before_elig_date).person
 
@@ -2435,7 +2439,7 @@ class rfc8989EligibilityTests(TestCase):
     def test_elig_by_office_edge(self):
 
         for nomcom in self.nomcoms:
-            elig_date=get_eligibility_date(nomcom)
+            elig_date = datetime_from_date(get_eligibility_date(nomcom), DEADLINE_TZINFO)
             day_after = elig_date + datetime.timedelta(days=1)
             two_days_after = elig_date + datetime.timedelta(days=2)
 
@@ -2450,15 +2454,15 @@ class rfc8989EligibilityTests(TestCase):
     def test_elig_by_office_closed_groups(self):
 
         for nomcom in self.nomcoms:
-            elig_date=get_eligibility_date(nomcom)
+            elig_date=datetime_from_date(get_eligibility_date(nomcom), DEADLINE_TZINFO)
             day_before = elig_date-datetime.timedelta(days=1)
             # special case for Feb 29
             if elig_date.month == 2 and elig_date.day == 29:
-                year_before = datetime.date(elig_date.year - 1, 2, 28)
-                three_years_before = datetime.date(elig_date.year - 3, 2, 28)
+                year_before = elig_date.replace(year=elig_date.year - 1, day=28)
+                three_years_before = elig_date.replace(year=elig_date.year - 3, day=28)
             else:
-                year_before = datetime.date(elig_date.year - 1, elig_date.month, elig_date.day)
-                three_years_before = datetime.date(elig_date.year - 3, elig_date.month, elig_date.day)
+                year_before = elig_date.replace(year=elig_date.year - 1)
+                three_years_before = elig_date.replace(year=elig_date.year - 3)
             just_after_three_years_before = three_years_before + datetime.timedelta(days=1)
             just_before_three_years_before = three_years_before - datetime.timedelta(days=1)
 
@@ -2517,14 +2521,14 @@ class rfc8989EligibilityTests(TestCase):
         for nomcom in self.nomcoms:
             elig_date = get_eligibility_date(nomcom)
 
-            last_date = elig_date
+            last_date = datetime_from_date(elig_date, DEADLINE_TZINFO)
             # special case for Feb 29
             if last_date.month == 2 and last_date.day == 29:
-                first_date = datetime.date(last_date.year - 5, 2, 28)
-                middle_date = datetime.date(last_date.year - 3, 2, 28)
+                first_date = last_date.replace(year = last_date.year - 5, day=28)
+                middle_date = last_date.replace(year=first_date.year - 3, day=28)
             else:
-                first_date = datetime.date(last_date.year - 5, last_date.month, last_date.day)
-                middle_date = datetime.date(last_date.year - 3, last_date.month, last_date.day)
+                first_date = last_date.replace(year=last_date.year - 5)
+                middle_date = last_date.replace(year=first_date.year - 3)
             day_after_last_date = last_date+datetime.timedelta(days=1)
             day_before_first_date = first_date-datetime.timedelta(days=1)
 
@@ -2632,8 +2636,8 @@ class VolunteerTests(TestCase):
         r = self.client.get(url)
         self.assertContains(r, 'NomCom is not accepting volunteers at this time', status_code=200)
 
-        year = datetime.date.today().year
-        nomcom = NomComFactory(group__acronym=f'nomcom{year}', is_accepting_volunteers=False)
+        this_year = date_today().year
+        nomcom = NomComFactory(group__acronym=f'nomcom{this_year}', is_accepting_volunteers=False)
         r = self.client.get(url)
         self.assertContains(r, 'NomCom is not accepting volunteers at this time', status_code=200)
         nomcom.is_accepting_volunteers = True
@@ -2656,7 +2660,7 @@ class VolunteerTests(TestCase):
         self.assertContains(r, 'already volunteered', status_code=200)
 
         person.volunteer_set.all().delete()
-        nomcom2 = NomComFactory(group__acronym=f'nomcom{year-1}', is_accepting_volunteers=True)
+        nomcom2 = NomComFactory(group__acronym=f'nomcom{this_year-1}', is_accepting_volunteers=True)
         r = self.client.get(url)
         self.assertEqual(r.status_code, 200)
         q = PyQuery(r.content)
@@ -2717,7 +2721,7 @@ class VolunteerDecoratorUnitTests(TestCase):
         office_person = PersonFactory()
         RoleHistoryFactory(
             name_id='chair',
-            group__time= elig_date - datetime.timedelta(days=365),
+            group__time=datetime_from_date(elig_date) - datetime.timedelta(days=365),
             group__group__state_id='conclude',
             person=office_person,
         )
@@ -2729,11 +2733,13 @@ class VolunteerDecoratorUnitTests(TestCase):
             DocEventFactory(
                 type='published_rfc',
                 doc=da.document,
-                time=datetime.date(
+                time=datetime.datetime(
                     elig_date.year - 3,
                     elig_date.month,
                     28 if elig_date.month == 2 and elig_date.day == 29 else elig_date.day,
-                ))
+                    tzinfo=datetime.timezone.utc,
+                )
+            )
         nomcom.volunteer_set.create(person=author_person)
 
         volunteers = nomcom.volunteer_set.all()
diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py
index 2bf63e194..50712f9aa 100644
--- a/ietf/nomcom/utils.py
+++ b/ietf/nomcom/utils.py
@@ -34,6 +34,7 @@ from ietf.utils.pipe import pipe
 from ietf.utils.mail import send_mail_text, send_mail, get_payload_text
 from ietf.utils.log import log
 from ietf.person.name import unidecode_name
+from ietf.utils.timezone import date_today, datetime_from_date, DEADLINE_TZINFO
 
 import debug                            # pyflakes:ignore
 
@@ -240,7 +241,7 @@ def validate_public_key(public_key):
 
 
 def send_accept_reminder_to_nominee(nominee_position):
-    today = datetime.date.today().strftime('%Y%m%d')
+    today = date_today().strftime('%Y%m%d')
     subject = 'Reminder: please accept (or decline) your nomination.'
     domain = Site.objects.get_current().domain
     position = nominee_position.position
@@ -332,7 +333,7 @@ def make_nomineeposition(nomcom, candidate, position, author):
         from_email = settings.NOMCOM_FROM_EMAIL.format(year=nomcom.year())
         (to_email, cc) = gather_address_lists('nomination_new_nominee',nominee=nominee.email.address)
         domain = Site.objects.get_current().domain
-        today = datetime.date.today().strftime('%Y%m%d')
+        today = date_today().strftime('%Y%m%d')
         hash = get_hash_nominee_position(today, nominee_position.id)
         accept_url = reverse('ietf.nomcom.views.process_nomination_status',
                               None,
@@ -558,34 +559,35 @@ def get_threerule_eligibility_querysets(date, base_qs, three_of_five_callable):
         base_qs = Person.objects.all()
 
     previous_five = previous_five_meetings(date)
+    date_as_dt = datetime_from_date(date, DEADLINE_TZINFO)
     three_of_five_qs = three_of_five_callable(previous_five=previous_five, queryset=base_qs)
 
     # If date is Feb 29, neither 3 nor 5 years ago has a Feb 29. Use Feb 28 instead.
     if date.month == 2 and date.day == 29:
-        three_years_ago = datetime.date(date.year - 3, 2, 28)
-        five_years_ago = datetime.date(date.year - 5, 2, 28)
+        three_years_ago = datetime.datetime(date.year - 3, 2, 28, tzinfo=DEADLINE_TZINFO)
+        five_years_ago = datetime.datetime(date.year - 5, 2, 28, tzinfo=DEADLINE_TZINFO)
     else:
-        three_years_ago = datetime.date(date.year - 3, date.month, date.day)
-        five_years_ago = datetime.date(date.year - 5, date.month, date.day)
+        three_years_ago = datetime.datetime(date.year - 3, date.month, date.day, tzinfo=DEADLINE_TZINFO)
+        five_years_ago = datetime.datetime(date.year - 5, date.month, date.day, tzinfo=DEADLINE_TZINFO)
 
     officer_qs = base_qs.filter(
         # is currently an officer
         Q(role__name_id__in=('chair','secr'),
           role__group__state_id='active',
           role__group__type_id='wg',
-          role__group__time__lte=date, ## TODO - inspect - lots of things affect group__time...
+          role__group__time__lte=date_as_dt, ## TODO - inspect - lots of things affect group__time...
         )
         # was an officer since the given date (I think this is wrong - it looks at when roles _start_, not when roles end)
       | Q(rolehistory__group__time__gte=three_years_ago,
-          rolehistory__group__time__lte=date,
+          rolehistory__group__time__lte=date_as_dt,
           rolehistory__name_id__in=('chair','secr'),
           rolehistory__group__state_id='active',
           rolehistory__group__type_id='wg',
          )
     ).distinct()
 
-    rfc_pks = set(DocEvent.objects.filter(type='published_rfc',time__gte=five_years_ago,time__lte=date).values_list('doc__pk',flat=True))
-    iesgappr_pks = set(DocEvent.objects.filter(type='iesg_approved',time__gte=five_years_ago,time__lte=date).values_list('doc__pk',flat=True))
+    rfc_pks = set(DocEvent.objects.filter(type='published_rfc', time__gte=five_years_ago, time__lte=date_as_dt).values_list('doc__pk', flat=True))
+    iesgappr_pks = set(DocEvent.objects.filter(type='iesg_approved', time__gte=five_years_ago, time__lte=date_as_dt).values_list('doc__pk',flat=True))
     qualifying_pks = rfc_pks.union(iesgappr_pks.difference(rfc_pks))
     author_qs = base_qs.filter(
             documentauthor__document__pk__in=qualifying_pks
@@ -624,7 +626,7 @@ def get_eligibility_date(nomcom=None, date=None):
         last_seated=Role.objects.filter(group__type_id='nomcom',name_id='member').order_by('-group__acronym').first()
         if last_seated:
             last_nomcom_year = int(last_seated.group.acronym[6:])
-            if last_nomcom_year == datetime.date.today().year:
+            if last_nomcom_year == date_today().year:
                 next_nomcom_year = last_nomcom_year
             else:
                 next_nomcom_year = int(last_seated.group.acronym[6:])+1
@@ -634,11 +636,11 @@ def get_eligibility_date(nomcom=None, date=None):
             else:
                 return datetime.date(next_nomcom_year,5,1)
         else:
-            return datetime.date(datetime.date.today().year,5,1)
+            return datetime.date(date_today().year,5,1)
 
 def previous_five_meetings(date = None):
     if date is None:
-        date = datetime.date.today()
+        date = date_today()
     return Meeting.objects.filter(type='ietf',date__lte=date).order_by('-date')[:5]
 
 def three_of_five_eligible_8713(previous_five, queryset=None):
diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py
index 9f752e49e..6f02b16e2 100644
--- a/ietf/nomcom/views.py
+++ b/ietf/nomcom/views.py
@@ -45,6 +45,8 @@ from ietf.nomcom.utils import (get_nomcom_by_year, store_nomcom_private_key, sug
 from ietf.ietfauth.utils import role_required
 from ietf.person.models import Person
 from ietf.utils.response import permission_denied
+from ietf.utils.timezone import date_today
+
 
 import debug                  # pyflakes:ignore
 
@@ -702,7 +704,7 @@ def process_nomination_status(request, year, nominee_position_id, state, date, h
     expiration_days = getattr(settings, 'DAYS_TO_EXPIRE_NOMINATION_LINK', None)
     if expiration_days:
         request_date = datetime.date(int(date[:4]), int(date[4:6]), int(date[6:]))
-        if datetime.date.today() > (request_date + datetime.timedelta(days=settings.DAYS_TO_EXPIRE_NOMINATION_LINK)):
+        if date_today() > (request_date + datetime.timedelta(days=expiration_days)):
             permission_denied(request, "Link expired.")
 
     need_confirmation = True
@@ -952,7 +954,7 @@ def view_feedback_topic(request, year, topic_id):
     feedback_types = FeedbackTypeName.objects.filter(slug__in=['comment',])
 
     last_seen = TopicFeedbackLastSeen.objects.filter(reviewer=request.user.person,topic=topic).first()
-    last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1,month=1,day=1)
+    last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1, month=1, day=1, tzinfo=datetime.timezone.utc)
     if last_seen:
         last_seen.save()
     else:
@@ -974,7 +976,7 @@ def view_feedback_nominee(request, year, nominee_id):
     feedback_types = FeedbackTypeName.objects.filter(slug__in=settings.NOMINEE_FEEDBACK_TYPES)
 
     last_seen = FeedbackLastSeen.objects.filter(reviewer=request.user.person,nominee=nominee).first()
-    last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1,month=1,day=1)
+    last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1, month=1, day=1, tzinfo=datetime.timezone.utc)
     if last_seen:
         last_seen.save()
     else:
diff --git a/ietf/person/management/commands/purge_old_personal_api_key_events.py b/ietf/person/management/commands/purge_old_personal_api_key_events.py
index e60f784b5..47674e2d5 100644
--- a/ietf/person/management/commands/purge_old_personal_api_key_events.py
+++ b/ietf/person/management/commands/purge_old_personal_api_key_events.py
@@ -1,9 +1,10 @@
 # Copyright The IETF Trust 2021, All Rights Reserved
 # -*- coding: utf-8 -*-
 
-from datetime import datetime, timedelta
+from datetime import timedelta
 from django.core.management.base import BaseCommand, CommandError
 from django.db.models import Max, Min
+from django.utils import timezone
 
 from ietf.person.models import PersonApiKeyEvent
 
@@ -33,7 +34,7 @@ class Command(BaseCommand):
         self.stdout.write('Finding events older than {}\n'.format(_format_count(keep_days)))
         self.stdout.flush()
 
-        now = datetime.now()
+        now = timezone.now()
         old_events = PersonApiKeyEvent.objects.filter(
             time__lt=now - timedelta(days=keep_days)
         )
diff --git a/ietf/person/management/commands/tests.py b/ietf/person/management/commands/tests.py
index f30a80bf5..291a6ace5 100644
--- a/ietf/person/management/commands/tests.py
+++ b/ietf/person/management/commands/tests.py
@@ -5,6 +5,7 @@ import datetime
 from io import StringIO
 
 from django.core.management import call_command, CommandError
+from django.utils import timezone
 
 from ietf.person.factories import PersonApiKeyEventFactory
 from ietf.person.models import PersonApiKeyEvent, PersonEvent
@@ -51,7 +52,7 @@ class CommandTests(TestCase):
         # Remember how many PersonEvents were present so we can verify they're cleaned up properly.
         personevents_before = PersonEvent.objects.count()
 
-        now = datetime.datetime.now()
+        now = timezone.now()
         # The first of these events will be timestamped a fraction of a second more than keep_days
         # days ago by the time we call the management command, so will just barely chosen for purge.
         old_events = [
@@ -101,7 +102,7 @@ class CommandTests(TestCase):
 
     def test_purge_old_personal_api_key_events_rejects_invalid_arguments(self):
         """The purge_old_personal_api_key_events command should reject invalid arguments"""
-        event = PersonApiKeyEventFactory(time=datetime.datetime.now() - datetime.timedelta(days=30))
+        event = PersonApiKeyEventFactory(time=timezone.now() - datetime.timedelta(days=30))
 
         with self.assertRaises(CommandError):
             self._call_command('purge_old_personal_api_key_events')
diff --git a/ietf/person/migrations/0026_use_timezone_now_for_person_models.py b/ietf/person/migrations/0026_use_timezone_now_for_person_models.py
new file mode 100644
index 000000000..619b42162
--- /dev/null
+++ b/ietf/person/migrations/0026_use_timezone_now_for_person_models.py
@@ -0,0 +1,34 @@
+# Generated by Django 2.2.28 on 2022-07-12 11:24
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('person', '0025_chat_and_polls_apikey'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='historicalperson',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AlterField(
+            model_name='person',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AlterField(
+            model_name='personalapikey',
+            name='created',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AlterField(
+            model_name='personevent',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now, help_text='When the event happened'),
+        ),
+    ]
diff --git a/ietf/person/models.py b/ietf/person/models.py
index c8982e338..a7e30b2de 100644
--- a/ietf/person/models.py
+++ b/ietf/person/models.py
@@ -2,7 +2,6 @@
 # -*- coding: utf-8 -*-
 
 
-import datetime
 import email.utils
 import email.header
 import jsonfield
@@ -18,6 +17,7 @@ from django.core.validators import validate_email
 from django.db import models
 from django.template.loader import render_to_string
 from django.urls import reverse as urlreverse
+from django.utils import timezone
 from django.utils.encoding import smart_bytes
 from django.utils.text import slugify
 
@@ -43,7 +43,7 @@ def name_character_validator(value):
 class Person(models.Model):
     history = HistoricalRecords()
     user = OneToOneField(User, blank=True, null=True, on_delete=models.SET_NULL)
-    time = models.DateTimeField(default=datetime.datetime.now)      # When this Person record entered the system
+    time = models.DateTimeField(default=timezone.now)      # When this Person record entered the system
     # The normal unicode form of the name.  This must be
     # set to the same value as the ascii-form if equal.
     name = models.CharField("Full Name (Unicode)", max_length=255, db_index=True, help_text="Preferred long form of name.", validators=[name_character_validator])
@@ -357,7 +357,7 @@ PERSON_API_KEY_ENDPOINTS = sorted(list(set([ (v, n) for (v, n, r) in PERSON_API_
 class PersonalApiKey(models.Model):
     person   = ForeignKey(Person, related_name='apikeys')
     endpoint = models.CharField(max_length=128, null=False, blank=False, choices=PERSON_API_KEY_ENDPOINTS)
-    created  = models.DateTimeField(default=datetime.datetime.now, null=False)
+    created  = models.DateTimeField(default=timezone.now, null=False)
     valid    = models.BooleanField(default=True)
     salt     = models.BinaryField(default=salt, max_length=12, null=False, blank=False)
     count    = models.IntegerField(default=0, null=False, blank=False)
@@ -406,7 +406,7 @@ PERSON_EVENT_CHOICES = [
 
 class PersonEvent(models.Model):
     person = ForeignKey(Person)
-    time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened")
+    time = models.DateTimeField(default=timezone.now, help_text="When the event happened")
     type = models.CharField(max_length=50, choices=PERSON_EVENT_CHOICES)
     desc = models.TextField()
 
diff --git a/ietf/person/templatetags/person_filters.py b/ietf/person/templatetags/person_filters.py
index 96696456f..017b29c63 100644
--- a/ietf/person/templatetags/person_filters.py
+++ b/ietf/person/templatetags/person_filters.py
@@ -1,13 +1,12 @@
 # Copyright The IETF Trust 2017-2020, All Rights Reserved
 
-import datetime
-
 from django import template
 
 import debug  # pyflakes:ignore
 
 from ietf.nomcom.utils import is_eligible
 from ietf.person.models import Alias
+from ietf.utils.timezone import date_today
 
 register = template.Library()
 
@@ -15,7 +14,7 @@ register = template.Library()
 @register.filter
 def is_nomcom_eligible(person, date=None):
     if date is None:
-        date = datetime.date.today()
+        date = date_today()
     return is_eligible(person=person, date=date)
 
 
diff --git a/ietf/person/tests.py b/ietf/person/tests.py
index affcb0441..f659b9896 100644
--- a/ietf/person/tests.py
+++ b/ietf/person/tests.py
@@ -14,6 +14,7 @@ from django.core.exceptions import ValidationError
 from django.http import HttpRequest
 from django.test import override_settings
 from django.urls import reverse as urlreverse
+from django.utils import timezone
 from django.utils.encoding import iri_to_uri
 
 import debug                            # pyflakes:ignore
@@ -218,7 +219,7 @@ class PersonUtilsTests(TestCase):
         self.assertEqual(results,(p1,p3))
 
         # both have User
-        today = datetime.datetime.today()
+        today = timezone.now()
         p2.user.last_login = today
         p2.user.save()
         p4.user.last_login = today - datetime.timedelta(days=30)
diff --git a/ietf/person/views.py b/ietf/person/views.py
index fca943e2d..59ab9366c 100644
--- a/ietf/person/views.py
+++ b/ietf/person/views.py
@@ -2,7 +2,6 @@
 # -*- coding: utf-8 -*-
 
 
-import datetime
 from io import StringIO, BytesIO
 from PIL import Image
 
@@ -10,6 +9,7 @@ from django.contrib import messages
 from django.db.models import Q
 from django.http import HttpResponse, Http404
 from django.shortcuts import render, get_object_or_404, redirect
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -76,7 +76,7 @@ def profile(request, email_or_name):
     persons = [ p for p in persons if p and p.id ]
     if not persons:
         raise Http404
-    return render(request, 'person/profile.html', {'persons': persons, 'today':datetime.date.today()})
+    return render(request, 'person/profile.html', {'persons': persons, 'today': timezone.now()})
 
 
 def photo(request, email_or_name):
diff --git a/ietf/review/factories.py b/ietf/review/factories.py
index cf66f1ed8..d6780fad8 100644
--- a/ietf/review/factories.py
+++ b/ietf/review/factories.py
@@ -2,6 +2,8 @@
 import factory
 import datetime
 
+from django.utils import timezone
+
 from ietf.review.models import ReviewTeamSettings, ReviewRequest, ReviewAssignment, ReviewerSettings
 from ietf.name.models import ReviewTypeName, ReviewResultName
 
@@ -39,7 +41,7 @@ class ReviewRequestFactory(factory.django.DjangoModelFactory):
     type_id = 'lc'
     doc = factory.SubFactory('ietf.doc.factories.DocumentFactory',type_id='draft')
     team = factory.SubFactory('ietf.group.factories.ReviewTeamFactory',type_id='review')
-    deadline = datetime.datetime.today()+datetime.timedelta(days=14)
+    deadline = timezone.now()+datetime.timedelta(days=14)
     requested_by = factory.SubFactory('ietf.person.factories.PersonFactory')
 
 class ReviewAssignmentFactory(factory.django.DjangoModelFactory):
@@ -49,7 +51,7 @@ class ReviewAssignmentFactory(factory.django.DjangoModelFactory):
     review_request = factory.SubFactory('ietf.review.factories.ReviewRequestFactory')
     state_id = 'assigned'
     reviewer = factory.SubFactory('ietf.person.factories.EmailFactory')
-    assigned_on = datetime.datetime.now()
+    assigned_on = timezone.now()
 
 class ReviewerSettingsFactory(factory.django.DjangoModelFactory):
     class Meta:
diff --git a/ietf/review/mailarch.py b/ietf/review/mailarch.py
index 2ca6c9566..6ef5909a1 100644
--- a/ietf/review/mailarch.py
+++ b/ietf/review/mailarch.py
@@ -25,6 +25,8 @@ from django.conf import settings
 from django.utils.encoding import force_bytes, force_str
 
 from ietf.utils.mail import get_payload_text
+from ietf.utils.timezone import date_today
+
 
 def list_name_from_email(list_email):
     if not list_email.endswith("@ietf.org"):
@@ -51,7 +53,7 @@ def construct_query_urls(doc, team, query=None):
 
     encoded_query = "?" + urlencode({
         "qdr": "c", # custom time frame
-        "start_date": (datetime.date.today() - datetime.timedelta(days=180)).isoformat(),
+        "start_date": (date_today() - datetime.timedelta(days=180)).isoformat(),
         "email_list": list_name,
         "q": "subject:({})".format(query),
         "as": "1", # this is an advanced search
@@ -89,7 +91,7 @@ def retrieve_messages_from_mbox(mbox_fileobj):
             utcdate = None
             d = email.utils.parsedate_tz(msg["Date"])
             if d:
-                utcdate = datetime.datetime.fromtimestamp(email.utils.mktime_tz(d))
+                utcdate = datetime.datetime.fromtimestamp(email.utils.mktime_tz(d), datetime.timezone.utc)
 
             res.append({
                 "from": msg["From"],
diff --git a/ietf/review/migrations/0029_use_timezone_now_for_review_models.py b/ietf/review/migrations/0029_use_timezone_now_for_review_models.py
new file mode 100644
index 000000000..a8a0558d4
--- /dev/null
+++ b/ietf/review/migrations/0029_use_timezone_now_for_review_models.py
@@ -0,0 +1,29 @@
+# Generated by Django 2.2.28 on 2022-07-12 11:24
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('review', '0028_auto_20220513_1456'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='historicalreviewrequest',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AlterField(
+            model_name='reviewrequest',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AlterField(
+            model_name='reviewwish',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+    ]
diff --git a/ietf/review/migrations/0030_use_date_today_helper.py b/ietf/review/migrations/0030_use_date_today_helper.py
new file mode 100644
index 000000000..b006d276f
--- /dev/null
+++ b/ietf/review/migrations/0030_use_date_today_helper.py
@@ -0,0 +1,24 @@
+# Generated by Django 2.2.28 on 2022-10-18 15:43
+
+from django.db import migrations, models
+import ietf.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('review', '0029_use_timezone_now_for_review_models'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='historicalunavailableperiod',
+            name='start_date',
+            field=models.DateField(default=ietf.utils.timezone.date_today, help_text="Choose the start date so that you can still do a review if it's assigned just before the start date - this usually means you should mark yourself unavailable for assignment some time before you are actually away. The default is today.", null=True),
+        ),
+        migrations.AlterField(
+            model_name='unavailableperiod',
+            name='start_date',
+            field=models.DateField(default=ietf.utils.timezone.date_today, help_text="Choose the start date so that you can still do a review if it's assigned just before the start date - this usually means you should mark yourself unavailable for assignment some time before you are actually away. The default is today.", null=True),
+        ),
+    ]
diff --git a/ietf/review/models.py b/ietf/review/models.py
index 96355417d..f0ec78094 100644
--- a/ietf/review/models.py
+++ b/ietf/review/models.py
@@ -2,11 +2,10 @@
 # -*- coding: utf-8 -*-
 
 
-import datetime
-
 from simple_history.models import HistoricalRecords
 
 from django.db import models
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -17,6 +16,8 @@ from ietf.name.models import ReviewTypeName, ReviewRequestStateName, ReviewResul
     ReviewAssignmentStateName, ReviewerQueuePolicyName
 from ietf.utils.validators import validate_regular_expression_string
 from ietf.utils.models import ForeignKey, OneToOneField
+from ietf.utils.timezone import date_today
+
 
 class ReviewerSettings(models.Model):
     """Keeps track of admin data associated with a reviewer in a team."""
@@ -66,7 +67,7 @@ class UnavailablePeriod(models.Model):
     history      = HistoricalRecords(history_change_reason_field=models.TextField(null=True))
     team         = ForeignKey(Group, limit_choices_to=~models.Q(reviewteamsettings=None))
     person       = ForeignKey(Person)
-    start_date   = models.DateField(default=datetime.date.today, null=True, help_text="Choose the start date so that you can still do a review if it's assigned just before the start date - this usually means you should mark yourself unavailable for assignment some time before you are actually away. The default is today.")
+    start_date   = models.DateField(default=date_today, null=True, help_text="Choose the start date so that you can still do a review if it's assigned just before the start date - this usually means you should mark yourself unavailable for assignment some time before you are actually away. The default is today.")
     end_date     = models.DateField(blank=True, null=True, help_text="Leaving the end date blank means that the period continues indefinitely. You can end it later.")
     AVAILABILITY_CHOICES = [
         ("canfinish", "Can do follow-ups"),
@@ -80,8 +81,7 @@ class UnavailablePeriod(models.Model):
     reason       = models.TextField(verbose_name="Reason why reviewer is unavailable (Optional)", max_length=2048, blank=True, help_text="Provide (for the secretary's benefit) the reason why the review is unavailable", default='')
 
     def state(self):
-        import datetime
-        today = datetime.date.today()
+        today = date_today()
         if self.start_date is None or self.start_date <= today:
             if not self.end_date or today <= self.end_date:
                 return "active"
@@ -95,7 +95,7 @@ class UnavailablePeriod(models.Model):
 
 class ReviewWish(models.Model):
     """Reviewer wishes to review a document when it becomes available for review."""
-    time        = models.DateTimeField(default=datetime.datetime.now)
+    time        = models.DateTimeField(default=timezone.now)
     team        = ForeignKey(Group, limit_choices_to=~models.Q(reviewteamsettings=None))
     person      = ForeignKey(Person)
     doc         = ForeignKey(Document)
@@ -125,7 +125,7 @@ class ReviewRequest(models.Model):
 
     # Fields filled in on the initial record creation - these
     # constitute the request part.
-    time          = models.DateTimeField(default=datetime.datetime.now)
+    time          = models.DateTimeField(default=timezone.now)
     type          = ForeignKey(ReviewTypeName)
     doc           = ForeignKey(Document, related_name='reviewrequest_set')
     team          = ForeignKey(Group, limit_choices_to=~models.Q(reviewteamsettings=None))
diff --git a/ietf/review/tests.py b/ietf/review/tests.py
index 1ecdb4040..43294801e 100644
--- a/ietf/review/tests.py
+++ b/ietf/review/tests.py
@@ -5,6 +5,7 @@ import datetime
 from ietf.group.factories import RoleFactory
 from ietf.utils.mail import empty_outbox, get_payload_text, outbox
 from ietf.utils.test_utils import TestCase, reload_db_objects
+from ietf.utils.timezone import date_today, datetime_from_date
 from .factories import ReviewAssignmentFactory, ReviewRequestFactory, ReviewerSettingsFactory
 from .mailarch import hash_list_message_id
 from .models import ReviewerSettings, ReviewSecretarySettings, ReviewTeamSettings, UnavailablePeriod
@@ -74,7 +75,7 @@ class ReviewAssignmentTest(TestCase):
 
 
 class ReviewAssignmentReminderTests(TestCase):
-    today = datetime.date.today()
+    today = date_today()
     deadline = today + datetime.timedelta(days=6)
 
     def setUp(self):
@@ -345,7 +346,7 @@ class ReviewAssignmentReminderTests(TestCase):
     def test_send_unavailability_period_ending_reminder(self):
         secretary = self.make_secretary(username='reviewsecretary')
         empty_outbox()
-        today = datetime.date.today()
+        today = date_today()
         UnavailablePeriod.objects.create(
             team=self.team,
             person=self.reviewer,
@@ -408,7 +409,7 @@ class ReviewAssignmentReminderTests(TestCase):
             review_request__state_id='assigned',
             review_request__deadline=self.deadline,
             state_id='assigned',
-            assigned_on=self.deadline,
+            assigned_on=datetime_from_date(self.deadline),
             reviewer=self.reviewer.email_set.first(),
         ).review_request.team
         second_team.reviewteamsettings.delete()  # prevent it from being sent reminders
@@ -420,7 +421,7 @@ class ReviewAssignmentReminderTests(TestCase):
             review_request__state_id='assigned',
             review_request__deadline=not_overdue,
             state_id='assigned',
-            assigned_on=not_overdue,
+            assigned_on=datetime_from_date(not_overdue),
             reviewer=self.reviewer.email_set.first(),
         )
         ReviewAssignmentFactory(
@@ -428,7 +429,7 @@ class ReviewAssignmentReminderTests(TestCase):
             review_request__state_id='assigned',
             review_request__deadline=not_overdue,
             state_id='assigned',
-            assigned_on=not_overdue,
+            assigned_on=datetime_from_date(not_overdue),
             reviewer=self.reviewer.email_set.first(),
         )
 
@@ -439,7 +440,7 @@ class ReviewAssignmentReminderTests(TestCase):
             review_request__state_id='assigned',
             review_request__deadline=in_grace_period,
             state_id='assigned',
-            assigned_on=in_grace_period,
+            assigned_on=datetime_from_date(in_grace_period),
             reviewer=self.reviewer.email_set.first(),
         )
         ReviewAssignmentFactory(
@@ -447,7 +448,7 @@ class ReviewAssignmentReminderTests(TestCase):
             review_request__state_id='assigned',
             review_request__deadline=in_grace_period,
             state_id='assigned',
-            assigned_on=in_grace_period,
+            assigned_on=datetime_from_date(in_grace_period),
             reviewer=self.reviewer.email_set.first(),
         )
 
@@ -469,7 +470,7 @@ class ReviewAssignmentReminderTests(TestCase):
         self.assertIn('1 overdue review', log[0])
 
     def test_send_reminder_all_open_reviews(self):
-        today = datetime.date.today()
+        today = date_today()
         self.make_secretary(username='reviewsecretary')
         ReviewerSettingsFactory(team=self.team, person=self.reviewer, remind_days_open_reviews=1)
 
diff --git a/ietf/review/utils.py b/ietf/review/utils.py
index e94dfb2a4..15312dd93 100644
--- a/ietf/review/utils.py
+++ b/ietf/review/utils.py
@@ -12,6 +12,8 @@ from django.template.defaultfilters import pluralize
 from django.template.loader import render_to_string
 from django.urls import reverse as urlreverse
 from django.contrib.sites.models import Site
+from django.utils import timezone
+
 from simple_history.utils import update_change_reason
 
 import debug                            # pyflakes:ignore
@@ -30,6 +32,8 @@ from ietf.review.models import (ReviewRequest, ReviewAssignment, ReviewRequestSt
 from ietf.utils.mail import send_mail
 from ietf.doc.utils import extract_complete_replaces_ancestor_mapping_for_docs
 from ietf.utils import log
+from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO
+
 
 # The origin date is used to have a single reference date for "every X days".
 # This date is arbitrarily chosen and has no special meaning, but should be consistent.
@@ -89,12 +93,12 @@ def no_review_from_teams_on_doc(doc, rev):
 
 def unavailable_periods_to_list(past_days=14):
     return UnavailablePeriod.objects.filter(
-        Q(end_date=None) | Q(end_date__gte=datetime.date.today() - datetime.timedelta(days=past_days)),
+        Q(end_date=None) | Q(end_date__gte=date_today() - datetime.timedelta(days=past_days)),
     ).order_by("start_date")
 
 def current_unavailable_periods_for_reviewers(team):
     """Return dict with currently active unavailable periods for reviewers."""
-    today = datetime.date.today()
+    today = date_today()
 
     unavailable_period_qs = UnavailablePeriod.objects.filter(
         Q(end_date__gte=today) | Q(end_date=None),
@@ -119,7 +123,7 @@ def days_needed_to_fulfill_min_interval_for_reviewers(team):
 
     min_intervals = dict(ReviewerSettings.objects.filter(team=team).values_list("person_id", "min_interval"))
 
-    now = datetime.datetime.now()
+    now = timezone.now()
 
     res = {}
     for person_id, latest_assignment_time in latest_assignments.items():
@@ -192,7 +196,10 @@ def extract_review_assignment_data(teams=None, reviewers=None, time_from=None, t
         assigned_time = assigned_on
         closed_time = completed_on
 
-        late_days = positive_days(datetime.datetime.combine(deadline, datetime.time.max), closed_time)
+        late_days = positive_days(
+            datetime.datetime.combine(deadline, datetime.time.max, tzinfo=DEADLINE_TZINFO),
+            closed_time,
+        )
         request_to_assignment_days = positive_days(requested_time, assigned_time)
         assignment_to_closure_days = positive_days(assigned_time, closed_time)
         request_to_closure_days = positive_days(requested_time, closed_time)
@@ -283,7 +290,7 @@ def latest_review_assignments_for_reviewers(team, days_back=365):
 
     extracted_data = extract_review_assignment_data(
         teams=[team],
-        time_from=datetime.date.today() - datetime.timedelta(days=days_back),
+        time_from=datetime_today(DEADLINE_TZINFO) - datetime.timedelta(days=days_back),
         ordering=["reviewer"],
     )
 
@@ -495,7 +502,7 @@ def suggested_review_requests_for_team(team):
 
     requests = {}
 
-    now = datetime.datetime.now()
+    now = timezone.now()
 
     reviewable_docs_qs = Document.objects.filter(type="draft").exclude(stream="ise")
 
@@ -870,7 +877,7 @@ def email_reviewer_reminder(assignment):
     review_request = assignment.review_request
     team = review_request.team
 
-    deadline_days = (review_request.deadline - datetime.date.today()).days
+    deadline_days = (review_request.deadline - date_today(DEADLINE_TZINFO)).days
 
     subject = "Reminder: deadline for review of {} in {} is {}".format(review_request.doc.name, team.acronym, review_request.deadline.isoformat())
 
@@ -936,7 +943,7 @@ def email_secretary_reminder(assignment, secretary_role):
     review_request = assignment.review_request
     team = review_request.team
 
-    deadline_days = (review_request.deadline - datetime.date.today()).days
+    deadline_days = (review_request.deadline - date_today(DEADLINE_TZINFO)).days
 
     subject = "Reminder: deadline for review of {} in {} is {}".format(review_request.doc.name, team.acronym, review_request.deadline.isoformat())
 
diff --git a/ietf/secr/meetings/forms.py b/ietf/secr/meetings/forms.py
index 50ccddbb7..92c751418 100644
--- a/ietf/secr/meetings/forms.py
+++ b/ietf/secr/meetings/forms.py
@@ -166,6 +166,15 @@ class TimeSlotForm(forms.Form):
         for n in range(-self.meeting.days, self.meeting.days):
             date = start + datetime.timedelta(days=n)
             choices.append((n, date.strftime("%a %b %d")))
+        # make sure the choices include the initial day
+        if self.initial and 'day' in self.initial:
+            day = self.initial['day']
+            date = start + datetime.timedelta(days=day)
+            datestr = date.strftime("%a %b %d")
+            if day < -self.meeting.days:
+                choices.insert(0, (day, datestr))
+            elif day >= self.meeting.days:
+                choices.append((day, datestr))
         return choices
 
 
diff --git a/ietf/secr/meetings/tests.py b/ietf/secr/meetings/tests.py
index c6576a0cc..bf052abe4 100644
--- a/ietf/secr/meetings/tests.py
+++ b/ietf/secr/meetings/tests.py
@@ -14,6 +14,7 @@ import debug         # pyflakes:ignore
 
 from django.conf import settings
 from django.urls import reverse
+from django.utils import timezone
 
 from ietf.group.models import Group, GroupEvent
 from ietf.meeting.factories import MeetingFactory
@@ -137,7 +138,10 @@ class SecrMeetingTestCase(TestCase):
         "Edit Meeting"
         meeting = make_meeting_test_data()
         url = reverse('ietf.secr.meetings.views.edit_meeting',kwargs={'meeting_id':meeting.number})
-        post_data = dict(number=meeting.number,date='2014-07-20',city='Toronto',
+        post_data = dict(number=meeting.number,
+                         date='2014-07-20',
+                         city='Toronto',
+                         time_zone='America/Toronto',
                          days=7,
                          idsubmit_cutoff_day_offset_00=13,
                          idsubmit_cutoff_day_offset_01=20,
@@ -212,8 +216,8 @@ class SecrMeetingTestCase(TestCase):
         self.assertEqual(q('#id_notification_list').html(),'ames, mars')
 
         # test that only changes since last notification show up
-        now = datetime.datetime.now()
-        then = datetime.datetime.now()+datetime.timedelta(hours=1)
+        now = timezone.now()
+        then = timezone.now()+datetime.timedelta(hours=1)
         person = Person.objects.get(name="(System)")
         GroupEvent.objects.create(group=mars_group,time=now,type='sent_notification',
                                   by=person,desc='sent scheduled notification for %s' % meeting)
@@ -285,7 +289,7 @@ class SecrMeetingTestCase(TestCase):
         url = reverse('ietf.secr.meetings.views.times_delete',kwargs={
             'meeting_id':meeting.number,
             'schedule_name':meeting.schedule.name,
-            'time':qs.first().time.strftime("%Y:%m:%d:%H:%M")
+            'time':qs.first().time.astimezone(meeting.tz()).strftime("%Y:%m:%d:%H:%M")
         })
         redirect_url = reverse('ietf.secr.meetings.views.times',kwargs={
             'meeting_id':meeting.number,
@@ -305,7 +309,7 @@ class SecrMeetingTestCase(TestCase):
         url = reverse('ietf.secr.meetings.views.times_edit',kwargs={
             'meeting_id':72,
             'schedule_name':'test-schedule',
-            'time':timeslot.time.strftime("%Y:%m:%d:%H:%M")
+            'time':timeslot.time.astimezone(meeting.tz()).strftime("%Y:%m:%d:%H:%M")
         })
         self.client.login(username="secretary", password="secretary+password")
         response = self.client.post(url, {
@@ -371,7 +375,7 @@ class SecrMeetingTestCase(TestCase):
         timeslot = session.official_timeslotassignment().timeslot
         url = reverse('ietf.secr.meetings.views.misc_session_edit',kwargs={'meeting_id':72,'schedule_name':meeting.schedule.name,'slot_id':timeslot.pk})
         redirect_url = reverse('ietf.secr.meetings.views.misc_sessions',kwargs={'meeting_id':72,'schedule_name':'test-schedule'})
-        new_time = timeslot.time + datetime.timedelta(days=1)
+        new_time = (timeslot.time + datetime.timedelta(days=1)).astimezone(meeting.tz())
         self.client.login(username="secretary", password="secretary+password")
         response = self.client.get(url)
         self.assertEqual(response.status_code, 200)
@@ -389,7 +393,7 @@ class SecrMeetingTestCase(TestCase):
         })
         self.assertRedirects(response, redirect_url)
         timeslot = session.official_timeslotassignment().timeslot
-        self.assertEqual(timeslot.time,new_time)
+        self.assertEqual(timeslot.time, new_time)
 
     def test_meetings_misc_session_delete(self):
         meeting = make_meeting_test_data()
diff --git a/ietf/secr/meetings/views.py b/ietf/secr/meetings/views.py
index 7d6a227dc..bd0c8efa1 100644
--- a/ietf/secr/meetings/views.py
+++ b/ietf/secr/meetings/views.py
@@ -11,6 +11,7 @@ from django.db.models import IntegerField
 from django.db.models.functions import Cast
 from django.forms.models import inlineformset_factory
 from django.shortcuts import render, get_object_or_404, redirect
+from django.utils import timezone
 from django.utils.text import slugify
 
 import debug                            # pyflakes:ignore
@@ -30,6 +31,7 @@ from ietf.secr.meetings.forms import ( BaseMeetingRoomFormSet, MeetingModelForm,
 from ietf.secr.sreq.views import get_initial_session
 from ietf.secr.utils.meeting import get_session, get_timeslot
 from ietf.mailtrigger.utils import gather_address_lists
+from ietf.utils.timezone import date_today, make_aware
 
 
 # prep for agenda changes
@@ -62,15 +64,23 @@ def build_timeslots(meeting,room=None):
         else:
             source_meeting = get_last_meeting(meeting)
 
-        delta = meeting.date - source_meeting.date
         timeslots = []
-        time_seen = set()
+        time_seen = set()  # time of source_meeting timeslot
         for t in source_meeting.timeslot_set.filter(type='regular'):
             if not t.time in time_seen:
                 time_seen.add(t.time)
                 timeslots.append(t)
         for t in timeslots:
-            new_time = t.time + delta
+            # Create new timeslot at the same wall clock time on the same day relative to meeting start
+            day_offset = t.local_start_time().date() - source_meeting.date
+            new_date = meeting.date + day_offset
+            new_time = make_aware(
+                datetime.datetime.combine(
+                    new_date,
+                    t.local_start_time().time(),
+                ),
+                meeting.tz(),
+            )
             for room in rooms:
                 TimeSlot.objects.create(type_id='regular',
                                         meeting=meeting,
@@ -121,7 +131,7 @@ def send_notifications(meeting, groups, person):
     Send session scheduled email notifications for each group in groups.  Person is the
     user who initiated this action, request.uesr.get_profile().
     '''
-    now = datetime.datetime.now()
+    now = timezone.now()
     for group in groups:
         sessions = group.session_set.filter(meeting=meeting)
         addrs = gather_address_lists('session_scheduled',group=group,session=sessions[0])
@@ -304,7 +314,7 @@ def blue_sheet_redirect(request):
     This is the generic blue sheet URL.  It gets the next IETF meeting and redirects
     to the meeting specific URL.
     '''
-    today = datetime.date.today()
+    today = date_today()
     qs = Meeting.objects.filter(date__gt=today,type='ietf').order_by('date')
     if qs:
         meeting = qs[0]
@@ -341,7 +351,6 @@ def edit_meeting(request, meeting_id):
 
     else:
         form = MeetingModelForm(instance=meeting)
-
     return render(request, 'meetings/edit_meeting.html', {
         'meeting': meeting,
         'form' : form, },
@@ -557,7 +566,7 @@ def misc_session_edit(request, meeting_id, schedule_name, slot_id):
                    'name':session.name,
                    'short':session.short,
                    'day':delta.days,
-                   'time':slot.time.strftime('%H:%M'),
+                   'time':slot.time.astimezone(meeting.tz()).strftime('%H:%M'),
                    'duration':duration_string(slot.duration),
                    'show_location':slot.show_location,
                    'purpose': session.purpose,
@@ -798,7 +807,8 @@ def get_timeslot_time(form, meeting):
     day = form.cleaned_data['day']
 
     date = meeting.date + datetime.timedelta(days=int(day))
-    return datetime.datetime(date.year,date.month,date.day,time.hour,time.minute)
+    return make_aware(datetime.datetime(date.year,date.month,date.day,time.hour,time.minute), meeting.tz())
+
 
 @role_required('Secretariat')
 def times_edit(request, meeting_id, schedule_name, time):
@@ -809,15 +819,22 @@ def times_edit(request, meeting_id, schedule_name, time):
     schedule = get_object_or_404(Schedule, meeting=meeting, name=schedule_name)
     
     parts = [ int(x) for x in time.split(':') ]
-    dtime = datetime.datetime(*parts)
+    dtime = make_aware(datetime.datetime(*parts), meeting.tz())
     timeslots = TimeSlot.objects.filter(meeting=meeting,time=dtime)
+    day = (dtime.date() - meeting.date).days
+    initial = {'day': day,
+               'time': dtime.strftime('%H:%M'),
+               'duration': timeslots.first().duration,
+               'name': timeslots.first().name}
 
     if request.method == 'POST':
         button_text = request.POST.get('submit', '')
         if button_text == 'Cancel':
             return redirect('ietf.secr.meetings.views.times', meeting_id=meeting_id,schedule_name=schedule_name)
 
-        form = TimeSlotForm(request.POST, meeting=meeting)
+        # Pass "initial" even for a POST so the choices initialize correctly if day is outside
+        # the standard set of options. See TimeSlotForm.get_day_choices().
+        form = TimeSlotForm(request.POST, initial=initial, meeting=meeting)
         if form.is_valid():
             day = form.cleaned_data['day']
             time = get_timeslot_time(form, meeting)
@@ -836,13 +853,6 @@ def times_edit(request, meeting_id, schedule_name, time):
     else:
         # we need to pass the session to the form in order to disallow changing
         # of group after materials have been uploaded
-        day = dtime.strftime('%w')
-        if day == 6:
-            day = -1
-        initial = {'day':day,
-                   'time':dtime.strftime('%H:%M'),
-                   'duration':timeslots.first().duration,
-                   'name':timeslots.first().name}
         form = TimeSlotForm(initial=initial, meeting=meeting)
 
     return render(request, 'meetings/times_edit.html', {
@@ -860,7 +870,7 @@ def times_delete(request, meeting_id, schedule_name, time):
     meeting = get_object_or_404(Meeting, number=meeting_id)
     
     parts = [ int(x) for x in time.split(':') ]
-    dtime = datetime.datetime(*parts)
+    dtime = make_aware(datetime.datetime(*parts), meeting.tz())
     status = SessionStatusName.objects.get(slug='schedw')
 
     if request.method == 'POST' and request.POST['post'] == 'yes':
diff --git a/ietf/secr/proceedings/proc_utils.py b/ietf/secr/proceedings/proc_utils.py
index 505bcdcc7..3961f907f 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
@@ -24,6 +25,7 @@ from ietf.meeting.models import Meeting, SessionPresentation, TimeSlot, SchedTim
 from ietf.person.models import Person
 from ietf.utils.log import log
 from ietf.utils.mail import send_mail
+from ietf.utils.timezone import make_aware
 
 AUDIO_FILE_RE = re.compile(r'ietf(?P<number>[\d]+)-(?P<room>.*)-(?P<time>[\d]{8}-[\d]{4})')
 VIDEO_TITLE_RE = re.compile(r'IETF(?P<number>[\d]+)-(?P<name>.*)-(?P<date>\d{8})-(?P<time>\d{4})')
@@ -32,7 +34,7 @@ VIDEO_TITLE_RE = re.compile(r'IETF(?P<number>[\d]+)-(?P<name>.*)-(?P<date>\d{8})
 def _get_session(number,name,date,time):
     '''Lookup session using data from video title'''
     meeting = Meeting.objects.get(number=number)
-    timeslot_time = datetime.datetime.strptime(date + time,'%Y%m%d%H%M')
+    timeslot_time = make_aware(datetime.datetime.strptime(date + time,'%Y%m%d%H%M'), meeting.tz())
     try:
         assignment = SchedTimeSessAssignment.objects.get(
             schedule__in = [meeting.schedule, meeting.schedule.base],
@@ -102,7 +104,7 @@ def get_timeslot_for_filename(filename):
         try:
             meeting = Meeting.objects.get(number=match.groupdict()['number'])
             room_mapping = {normalize_room_name(room.name): room.name for room in meeting.room_set.all()}
-            time = datetime.datetime.strptime(match.groupdict()['time'],'%Y%m%d-%H%M')
+            time = make_aware(datetime.datetime.strptime(match.groupdict()['time'],'%Y%m%d-%H%M'), meeting.tz())
             slots = TimeSlot.objects.filter(
                 meeting=meeting,
                 location__name=room_mapping[match.groupdict()['room']],
@@ -201,17 +203,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 +233,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 +264,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/secr/proceedings/reports.py b/ietf/secr/proceedings/reports.py
index 115e4facd..fd360774b 100644
--- a/ietf/secr/proceedings/reports.py
+++ b/ietf/secr/proceedings/reports.py
@@ -1,20 +1,23 @@
 import datetime
 
 from django.template.loader import render_to_string
+from django.utils import timezone
 
 from ietf.meeting.models import Meeting
 from ietf.doc.models import DocEvent, Document
 from ietf.secr.proceedings.proc_utils import get_progress_stats
+from ietf.utils.timezone import datetime_from_date
+
 
 def report_id_activity(start,end):
 
     # get previous meeting
-    meeting = Meeting.objects.filter(date__lt=datetime.datetime.now(),type='ietf').order_by('-date')[0]
+    meeting = Meeting.objects.filter(date__lt=timezone.now(),type='ietf').order_by('-date')[0]
     syear,smonth,sday = start.split('-')
     eyear,emonth,eday = end.split('-')
-    sdate = datetime.datetime(int(syear),int(smonth),int(sday))
-    edate = datetime.datetime(int(eyear),int(emonth),int(eday))
-    
+    sdate = datetime_from_date(datetime.date(int(syear),int(smonth),int(sday)), meeting.tz())
+    edate = datetime_from_date(datetime.date(int(eyear),int(emonth),int(eday)), meeting.tz())
+
     #queryset = Document.objects.filter(type='draft').annotate(start_date=Min('docevent__time'))
     new_docs = Document.objects.filter(type='draft').filter(docevent__type='new_revision',
                                                             docevent__newrevisiondocevent__rev='00',
@@ -44,7 +47,7 @@ def report_id_activity(start,end):
     approved = events.filter(type='iesg_approved').count()
     
     # get 4 weeks
-    monday = Meeting.get_current_meeting().get_ietf_monday()
+    monday = datetime_from_date(Meeting.get_current_meeting().get_ietf_monday(), meeting.tz())
     cutoff = monday + datetime.timedelta(days=3)
     ff1_date = cutoff - datetime.timedelta(days=28)
     #ff2_date = cutoff - datetime.timedelta(days=21)
diff --git a/ietf/secr/proceedings/templatetags/ams_filters.py b/ietf/secr/proceedings/templatetags/ams_filters.py
index ebf166e3c..d5c0837a2 100644
--- a/ietf/secr/proceedings/templatetags/ams_filters.py
+++ b/ietf/secr/proceedings/templatetags/ams_filters.py
@@ -38,20 +38,6 @@ def display_duration(value):
         x=int(value)
         return "%d Hours %d Minutes %d Seconds"%(x//3600,(x%3600)//60,x%60)
 
-@register.filter
-def get_published_date(doc):
-    '''
-    Returns the published date for a RFC Document
-    '''
-    event = doc.latest_event(type='published_rfc')
-    if event:
-        return event.time
-    event = doc.latest_event(type='new_revision')
-    if event:
-        return event.time
-    else:
-        return None
-        
 @register.filter
 def is_ppt(value):
     '''
diff --git a/ietf/secr/proceedings/tests.py b/ietf/secr/proceedings/tests.py
index 9d3f3d27b..22ede4c8c 100644
--- a/ietf/secr/proceedings/tests.py
+++ b/ietf/secr/proceedings/tests.py
@@ -50,8 +50,9 @@ class VideoRecordingTestCase(TestCase):
         meeting = session.meeting
         number = meeting.number
         name = session.group.acronym
-        date = session.official_timeslotassignment().timeslot.time.strftime('%Y%m%d')
-        time = session.official_timeslotassignment().timeslot.time.strftime('%H%M')
+        ts_time = session.official_timeslotassignment().timeslot.local_start_time()
+        date = ts_time.strftime('%Y%m%d')
+        time = ts_time.strftime('%H%M')
         self.assertEqual(_get_session(number,name,date,time),session)
 
     def test_get_urls_from_json(self):
@@ -113,7 +114,7 @@ class RecordingTestCase(TestCase):
         return "{prefix}-{room}-{date}.mp3".format(
             prefix=timeslot.meeting.type.slug + timeslot.meeting.number,
             room=normalize_room_name(timeslot.location.name),
-            date=timeslot.time.strftime('%Y%m%d-%H%M'))
+            date=timeslot.local_start_time().strftime('%Y%m%d-%H%M'))
 
     def test_import_audio_files_shared_timeslot(self):
         meeting = MeetingFactory(type_id='ietf',number='72')
diff --git a/ietf/secr/proceedings/tests_reports.py b/ietf/secr/proceedings/tests_reports.py
index 6039cfced..034dbe5fa 100644
--- a/ietf/secr/proceedings/tests_reports.py
+++ b/ietf/secr/proceedings/tests_reports.py
@@ -1,6 +1,8 @@
 import datetime
 import debug    # pyflakes:ignore
 
+from django.utils import timezone
+
 from ietf.doc.factories import DocumentFactory,NewRevisionDocEventFactory
 from ietf.secr.proceedings.reports import report_id_activity, report_progress_report
 from ietf.utils.test_utils import TestCase
@@ -10,7 +12,7 @@ class ReportsTestCase(TestCase):
 
     def test_report_id_activity(self):
 
-        today = datetime.datetime.today()
+        today = timezone.now()
         yesterday = today - datetime.timedelta(days=1)
         last_quarter = today - datetime.timedelta(days=3*30)
         next_week = today+datetime.timedelta(days=7)
@@ -24,7 +26,7 @@ class ReportsTestCase(TestCase):
         self.assertTrue('IETF Activity since last IETF Meeting' in result)
 
     def test_report_progress_report(self):
-        today = datetime.datetime.today()
+        today = timezone.now()
         last_quarter = today - datetime.timedelta(days=3*30)
         next_week = today+datetime.timedelta(days=7)
 
diff --git a/ietf/secr/proceedings/views.py b/ietf/secr/proceedings/views.py
index b08375176..e88a20196 100644
--- a/ietf/secr/proceedings/views.py
+++ b/ietf/secr/proceedings/views.py
@@ -27,6 +27,7 @@ from ietf.meeting.utils import add_event_info_to_session_qs
 
 from ietf.secr.proceedings.forms import RecordingForm, RecordingEditForm 
 from ietf.secr.proceedings.proc_utils import (create_recording)
+from ietf.utils.timezone import date_today
 
 # -------------------------------------------------
 # Globals 
@@ -154,7 +155,7 @@ def main(request):
         meetings = Meeting.objects.filter(type='ietf').order_by('-number')
     else:
         # select meetings still within the cutoff period
-        today = datetime.date.today()
+        today = date_today()
         meetings = [m for m in Meeting.objects.filter(type='ietf').order_by('-number') if m.get_submission_correction_date()>=today]
 
     groups = get_my_groups(request.user)
@@ -165,7 +166,7 @@ def main(request):
         m.group = m.session_set.first().group
 
     # we today's date to see if we're past the submissio cutoff
-    today = datetime.date.today()
+    today = date_today()
 
     return render(request, 'proceedings/main.html',{
         'meetings': meetings,
@@ -304,7 +305,7 @@ def select(request, meeting_num):
     # get the time proceedings were generated
     path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'index.html')
     if os.path.exists(path):
-        last_run = datetime.datetime.fromtimestamp(os.path.getmtime(path))
+        last_run = datetime.datetime.fromtimestamp(os.path.getmtime(path), datetime.timezone.utc)
     else:
         last_run = None
 
diff --git a/ietf/secr/sreq/templatetags/ams_filters.py b/ietf/secr/sreq/templatetags/ams_filters.py
index 125acdeda..3ef872232 100644
--- a/ietf/secr/sreq/templatetags/ams_filters.py
+++ b/ietf/secr/sreq/templatetags/ams_filters.py
@@ -42,20 +42,6 @@ def display_duration(value):
     else:
         return "%d Hours %d Minutes %d Seconds"%(value//3600,(value%3600)//60,value%60)
 
-@register.filter
-def get_published_date(doc):
-    '''
-    Returns the published date for a RFC Document
-    '''
-    event = doc.latest_event(type='published_rfc')
-    if event:
-        return event.time
-    event = doc.latest_event(type='new_revision')
-    if event:
-        return event.time
-    else:
-        return None
-        
 @register.filter
 def is_ppt(value):
     '''
diff --git a/ietf/secr/sreq/tests.py b/ietf/secr/sreq/tests.py
index fabbc0807..7f6707b4d 100644
--- a/ietf/secr/sreq/tests.py
+++ b/ietf/secr/sreq/tests.py
@@ -16,6 +16,8 @@ from ietf.name.models import ConstraintName, TimerangeName
 from ietf.person.models import Person
 from ietf.secr.sreq.forms import SessionForm
 from ietf.utils.mail import outbox, empty_outbox, get_payload_text
+from ietf.utils.timezone import date_today
+
 
 from pyquery import PyQuery
 
@@ -23,7 +25,7 @@ SECR_USER='secretary'
 
 class SreqUrlTests(TestCase):
     def test_urls(self):
-        MeetingFactory(type_id='ietf',date=datetime.date.today())
+        MeetingFactory(type_id='ietf',date=date_today())
 
         self.client.login(username="secretary", password="secretary+password")
 
@@ -39,7 +41,7 @@ class SreqUrlTests(TestCase):
 
 class SessionRequestTestCase(TestCase):
     def test_main(self):
-        meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
+        meeting = MeetingFactory(type_id='ietf', date=date_today())
         SessionFactory.create_batch(2, meeting=meeting, status_id='sched')
         SessionFactory.create_batch(2, meeting=meeting, status_id='disappr')
         # An additional unscheduled group comes from make_immutable_base_data
@@ -53,7 +55,7 @@ class SessionRequestTestCase(TestCase):
         self.assertEqual(len(unsched), 11)
 
     def test_approve(self):
-        meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
+        meeting = MeetingFactory(type_id='ietf', date=date_today())
         ad = Person.objects.get(user__username='ad')
         area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group
         mars = GroupFactory(parent=area, acronym='mars')
@@ -66,7 +68,7 @@ class SessionRequestTestCase(TestCase):
         self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'appr')
         
     def test_cancel(self):
-        meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
+        meeting = MeetingFactory(type_id='ietf', date=date_today())
         ad = Person.objects.get(user__username='ad')
         area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group
         session = SessionFactory(meeting=meeting, group__parent=area, group__acronym='mars', status_id='sched')
@@ -77,7 +79,7 @@ class SessionRequestTestCase(TestCase):
         self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'deleted')
 
     def test_edit(self):
-        meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
+        meeting = MeetingFactory(type_id='ietf', date=date_today())
         mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group
         group2 = GroupFactory()
         group3 = GroupFactory()
@@ -242,7 +244,7 @@ class SessionRequestTestCase(TestCase):
 
 
     def test_edit_constraint_bethere(self):
-        meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
+        meeting = MeetingFactory(type_id='ietf', date=date_today())
         mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group
         session = SessionFactory(meeting=meeting, group=mars, status_id='sched')
         Constraint.objects.create(
@@ -309,7 +311,7 @@ class SessionRequestTestCase(TestCase):
 
     def test_edit_inactive_conflicts(self):
         """Inactive conflicts should be displayed and removable"""
-        meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(), group_conflicts=['chair_conflict'])
+        meeting = MeetingFactory(type_id='ietf', date=date_today(), group_conflicts=['chair_conflict'])
         mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group
         session = SessionFactory(meeting=meeting, group=mars, status_id='sched')
         other_group = GroupFactory()
@@ -367,7 +369,7 @@ class SessionRequestTestCase(TestCase):
         self.assertEqual(len(mars.constraint_source_set.filter(name_id='conflict')), 0)
 
     def test_tool_status(self):
-        MeetingFactory(type_id='ietf', date=datetime.date.today())
+        MeetingFactory(type_id='ietf', date=date_today())
         url = reverse('ietf.secr.sreq.views.tool_status')
         self.client.login(username="secretary", password="secretary+password")
         r = self.client.get(url)
@@ -381,7 +383,7 @@ class SessionRequestTestCase(TestCase):
         Relies on SessionForm representing constraint values with element IDs
         like id_constraint_<ConstraintName slug>
         """
-        meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
+        meeting = MeetingFactory(type_id='ietf', date=date_today())
         RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars')
         url = reverse('ietf.secr.sreq.views.new', kwargs=dict(acronym='mars'))
         self.client.login(username="marschairman", password="marschairman+password")
@@ -404,7 +406,7 @@ class SessionRequestTestCase(TestCase):
 
     def test_edit_req_constraint_types(self):
         """Editing a request constraint should show the expected constraints"""
-        meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
+        meeting = MeetingFactory(type_id='ietf', date=date_today())
         SessionFactory(group__acronym='mars',
                        status_id='schedw',
                        meeting=meeting,
@@ -438,7 +440,7 @@ class SubmitRequestCase(TestCase):
         MeetingFactory.reset_sequence(0)
 
     def test_submit_request(self):
-        meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
+        meeting = MeetingFactory(type_id='ietf', date=date_today())
         ad = Person.objects.get(user__username='ad')
         area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group
         group = GroupFactory(parent=area)
@@ -506,7 +508,7 @@ class SubmitRequestCase(TestCase):
         self.assertEqual(list(session.joint_with_groups.all()), [group3, group4])
 
     def test_submit_request_invalid(self):
-        MeetingFactory(type_id='ietf', date=datetime.date.today())
+        MeetingFactory(type_id='ietf', date=date_today())
         ad = Person.objects.get(user__username='ad')
         area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group
         group = GroupFactory(parent=area)
@@ -542,8 +544,8 @@ class SubmitRequestCase(TestCase):
         self.assertContains(r, 'Must provide data for all sessions')
 
     def test_submit_request_check_constraints(self):
-        m1 = MeetingFactory(type_id='ietf', date=datetime.date.today() - datetime.timedelta(days=100))
-        MeetingFactory(type_id='ietf', date=datetime.date.today(),
+        m1 = MeetingFactory(type_id='ietf', date=date_today() - datetime.timedelta(days=100))
+        MeetingFactory(type_id='ietf', date=date_today(),
                        group_conflicts=['chair_conflict', 'conflic2', 'conflic3'])
         ad = Person.objects.get(user__username='ad')
         area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group
@@ -604,7 +606,7 @@ class SubmitRequestCase(TestCase):
         self.assertContains(r, "Cannot declare a conflict with the same group")
 
     def test_request_notification(self):
-        meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
+        meeting = MeetingFactory(type_id='ietf', date=date_today())
         ad = Person.objects.get(user__username='ad')
         area = GroupFactory(type_id='area')
         RoleFactory(name_id='ad', person=ad, group=area)
@@ -699,7 +701,7 @@ class SubmitRequestCase(TestCase):
         self.assertNotIn('The third session requires your approval', notification_payload)
 
     def test_request_notification_third_session(self):
-        meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
+        meeting = MeetingFactory(type_id='ietf', date=date_today())
         ad = Person.objects.get(user__username='ad')
         area = GroupFactory(type_id='area')
         RoleFactory(name_id='ad', person=ad, group=area)
@@ -807,7 +809,7 @@ class SubmitRequestCase(TestCase):
 class LockAppTestCase(TestCase):
     def setUp(self):
         super().setUp()
-        self.meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(),session_request_lock_message='locked')
+        self.meeting = MeetingFactory(type_id='ietf', date=date_today(),session_request_lock_message='locked')
         self.group = GroupFactory(acronym='mars')
         RoleFactory(name_id='chair', group=self.group, person__user__username='marschairman')
         SessionFactory(group=self.group,meeting=self.meeting)
@@ -853,7 +855,7 @@ class LockAppTestCase(TestCase):
     
 class NotMeetingCase(TestCase):
     def test_not_meeting(self):
-        MeetingFactory(type_id='ietf',date=datetime.date.today())
+        MeetingFactory(type_id='ietf',date=date_today())
         group = GroupFactory(acronym='mars')
         url = reverse('ietf.secr.sreq.views.no_session',kwargs={'acronym':group.acronym}) 
         self.client.login(username="secretary", password="secretary+password")
diff --git a/ietf/secr/telechat/tests.py b/ietf/secr/telechat/tests.py
index f664745d7..e4661b767 100644
--- a/ietf/secr/telechat/tests.py
+++ b/ietf/secr/telechat/tests.py
@@ -14,6 +14,7 @@ from ietf.doc.factories import (WgDraftFactory, IndividualRfcFactory, CharterFac
 from ietf.doc.models import BallotDocEvent, BallotType, BallotPositionDocEvent, State, Document
 from ietf.doc.utils import update_telechat, create_ballot_if_not_open
 from ietf.utils.test_utils import TestCase
+from ietf.utils.timezone import date_today, datetime_today
 from ietf.iesg.models import TelechatDate
 from ietf.person.models import Person
 from ietf.person.factories import PersonFactory
@@ -22,7 +23,7 @@ from ietf.secr.telechat.views import get_next_telechat_date
 SECR_USER='secretary'
 
 def augment_data():
-    TelechatDate.objects.create(date=datetime.datetime.today())
+    TelechatDate.objects.create(date=date_today())
 
 class SecrTelechatTestCase(TestCase):
     def test_main(self):
@@ -119,7 +120,7 @@ class SecrTelechatTestCase(TestCase):
     def test_doc_detail_charter(self):
         by=Person.objects.get(name="(System)")
         charter = CharterFactory(states=[('charter','intrev')])
-        last_week = datetime.date.today()-datetime.timedelta(days=7)
+        last_week = datetime_today()-datetime.timedelta(days=7)
         BallotDocEvent.objects.create(type='created_ballot',by=by,doc=charter, rev=charter.rev,
                                       ballot_type=BallotType.objects.get(doc_type=charter.type,slug='r-extrev'),
                                       time=last_week)
@@ -138,7 +139,7 @@ class SecrTelechatTestCase(TestCase):
         self.assertEqual(q("#telechat-positions-table").find("th:contains('No Record')").length,1)
 
     def test_bash(self):
-        today = datetime.datetime.today() 
+        today = date_today()
         TelechatDate.objects.create(date=today)
         url = reverse('ietf.secr.telechat.views.bash',kwargs={'date':today.strftime('%Y-%m-%d')})
         self.client.login(username="secretary", password="secretary+password")
@@ -148,7 +149,7 @@ class SecrTelechatTestCase(TestCase):
     def test_doc_detail_post_update_ballot(self):
         by=Person.objects.get(name="(System)")
         charter = CharterFactory(states=[('charter','intrev')])
-        last_week = datetime.date.today()-datetime.timedelta(days=7)
+        last_week = datetime_today()-datetime.timedelta(days=7)
         BallotDocEvent.objects.create(type='created_ballot',by=by,doc=charter, rev=charter.rev,
                                       ballot_type=BallotType.objects.get(doc_type=charter.type,slug='r-extrev'),
                                       time=last_week)
@@ -186,7 +187,7 @@ class SecrTelechatTestCase(TestCase):
     def test_doc_detail_post_update_state(self):
         by=Person.objects.get(name="(System)")
         charter = CharterFactory(states=[('charter','intrev')])
-        last_week = datetime.date.today()-datetime.timedelta(days=7)
+        last_week = datetime_today()-datetime.timedelta(days=7)
         BallotDocEvent.objects.create(type='created_ballot',by=by,doc=charter, rev=charter.rev,
                                       ballot_type=BallotType.objects.get(doc_type=charter.type,slug='r-extrev'),
                                       time=last_week)
@@ -214,7 +215,7 @@ class SecrTelechatTestCase(TestCase):
             ad=Person.objects.get(user__username='ad'),
             authors=PersonFactory.create_batch(3),
         )
-        last_week = datetime.date.today()-datetime.timedelta(days=7)
+        last_week = datetime_today()-datetime.timedelta(days=7)
         BallotDocEvent.objects.create(type='created_ballot',by=by,doc=draft, rev=draft.rev,
                                       ballot_type=BallotType.objects.get(doc_type=draft.type,slug='approve'),
                                       time=last_week)
diff --git a/ietf/secr/telechat/views.py b/ietf/secr/telechat/views.py
index b3f064fa1..4dd4fba4e 100644
--- a/ietf/secr/telechat/views.py
+++ b/ietf/secr/telechat/views.py
@@ -20,6 +20,8 @@ from ietf.iesg.models import TelechatDate, TelechatAgendaItem, Telechat
 from ietf.iesg.agenda import agenda_data, get_doc_section
 from ietf.ietfauth.utils import role_required
 from ietf.secr.telechat.forms import BallotForm, ChangeStateForm, DateSelectForm, TELECHAT_TAGS
+from ietf.utils.timezone import date_today
+
 
 '''
 EXPECTED CHANGES:
@@ -80,14 +82,14 @@ def get_last_telechat_date():
     This function returns the date of the last telechat
     Tried TelechatDocEvent.objects.latest but that will return today's telechat
     '''
-    return TelechatDate.objects.filter(date__lt=datetime.date.today()).order_by('-date')[0].date
+    return TelechatDate.objects.filter(date__lt=date_today()).order_by('-date')[0].date
     #return '2011-11-01' # uncomment for testing
 
 def get_next_telechat_date():
     '''
     This function returns the date of the next telechat
     '''
-    return TelechatDate.objects.filter(date__gte=datetime.date.today()).order_by('date')[0].date
+    return TelechatDate.objects.filter(date__gte=date_today()).order_by('date')[0].date
 
 def get_section_header(doc, agenda):
     '''
diff --git a/ietf/secr/templates/meetings/misc_sessions.html b/ietf/secr/templates/meetings/misc_sessions.html
index 9f9eb0f7a..2ffa1ef9e 100644
--- a/ietf/secr/templates/meetings/misc_sessions.html
+++ b/ietf/secr/templates/meetings/misc_sessions.html
@@ -1,7 +1,7 @@
 {% extends "meetings/base_rooms_times.html" %}
-{% load agenda_custom_tags %}
+{% load agenda_custom_tags tz %}
 {% block subsection %}
-
+{% timezone meeting.time_zone %}
     <div class="module">
         <h2>TimeSlots</h2>
 
@@ -73,7 +73,7 @@
 
     </div> <!-- module -->
 
-
+{% endtimezone %}
 {% endblock %}
 
 {% block extrahead %}
diff --git a/ietf/secr/templates/meetings/regular_session_edit.html b/ietf/secr/templates/meetings/regular_session_edit.html
index 479cef8af..0bf6453f5 100644
--- a/ietf/secr/templates/meetings/regular_session_edit.html
+++ b/ietf/secr/templates/meetings/regular_session_edit.html
@@ -1,5 +1,5 @@
 {% extends "base_site.html" %}
-{% load staticfiles %}
+{% load staticfiles tz %}
 
 {% block title %}Meetings{% endblock %}
 
@@ -19,7 +19,7 @@
 
         <form id="meetings-schedule-form" method="post">{% csrf_token %}
             <div class="inline-related{% if forloop.last %} last-related{% endif %}">
-                <table class="full-width amstable">
+                <table class="full-width amstable">{% timezone meeting.time_zone %}
                     <tbody>
                     <tr>
                         <th scope="row">Day:</th>
@@ -40,7 +40,7 @@
                      <!-- [html-validate-disable-block element-required-attributes -- FIXME: as_table renders without scope] -->
                     {{ form.as_table }}
                 </tbody>
-                </table>
+                {% endtimezone %}</table>
             </div> <!-- inline-related -->
 
         <div class="button-group">
diff --git a/ietf/secr/templates/meetings/session_schedule_notification.txt b/ietf/secr/templates/meetings/session_schedule_notification.txt
index 95cbf3535..5ad129f70 100644
--- a/ietf/secr/templates/meetings/session_schedule_notification.txt
+++ b/ietf/secr/templates/meetings/session_schedule_notification.txt
@@ -1,4 +1,4 @@
-Dear {{ to_name }},
+{% load tz %}{% timezone meeting.time_zone %}Dear {{ to_name }},
 
 The session(s) that you have requested have been scheduled.
 Below is the scheduled session information followed by
@@ -16,4 +16,4 @@ iCalendar: {{ baseurl }}{% url "ietf.meeting.views.agenda_ical" num=meeting.numb
 
 Request Information:
 
-{% include "includes/session_info.txt" %}
+{% include "includes/session_info.txt" %}{% endtimezone %}
diff --git a/ietf/secr/templates/meetings/sessions.html b/ietf/secr/templates/meetings/sessions.html
index 03b4d8489..e774d83ca 100644
--- a/ietf/secr/templates/meetings/sessions.html
+++ b/ietf/secr/templates/meetings/sessions.html
@@ -1,5 +1,5 @@
 {% extends "meetings/base_rooms_times.html" %}
-{% load django_bootstrap5 %}
+{% load django_bootstrap5 tz %}
 
 {% block subsection %}
 
@@ -18,7 +18,7 @@
                     <th scope="col"></th>
                 </tr>
             </thead>
-            <tbody>
+            <tbody>{% timezone meeting.time_zone %}
                 {% for session in sessions %}
                     <tr>
                         <td>{{ session.group.acronym }}</td>
@@ -46,7 +46,7 @@
                         </td>
                     </tr>
                 {% endfor %}
-            </tbody>
+            {% endtimezone %}</tbody>
         </table>
     </div> <!-- module -->
 
diff --git a/ietf/secr/templates/meetings/times.html b/ietf/secr/templates/meetings/times.html
index 0ec175f03..559e2fd77 100644
--- a/ietf/secr/templates/meetings/times.html
+++ b/ietf/secr/templates/meetings/times.html
@@ -1,5 +1,5 @@
 {% extends "meetings/base_rooms_times.html" %}
-
+{% load tz %}
 {% block subsection %}
 
     <div class="module">
@@ -16,7 +16,7 @@
                         <th scope="col"></th>
                     </tr>
                 </thead>
-                <tbody>
+                <tbody>{% timezone meeting.time_zone %}
                     {% for item in times %}
                         <tr class="{% cycle 'row1' 'row2' %}">
                             <td>{{ item.time|date:"D M d" }}</td>
@@ -26,7 +26,7 @@
                             <td><a href="{% url "ietf.secr.meetings.views.times_delete" meeting_id=meeting.number schedule_name=schedule.name time=item.time|date:"Y:m:d:H:i" %}">Delete</a></td>
                         </tr>
                     {% endfor %}
-                </tbody>
+                {% endtimezone %}</tbody>
             </table>
         {% else %}
             <h3>No timeslots exist for this meeting. Add rooms with the "duplicate timeslots" option enabled to copy timeslots from the last meeting.</h3>
diff --git a/ietf/secr/templates/proceedings/recording.html b/ietf/secr/templates/proceedings/recording.html
index 906218f97..964e181ea 100755
--- a/ietf/secr/templates/proceedings/recording.html
+++ b/ietf/secr/templates/proceedings/recording.html
@@ -1,5 +1,5 @@
 {% extends "base_site.html" %}
-{% load staticfiles %}
+{% load staticfiles tz %}
 
 {% block title %}Proceedings{% endblock %}
 
@@ -59,7 +59,7 @@
                     </tr>
                 </thead>
                 {% if sessions %}
-                    <tbody>
+                    <tbody>{% timezone meeting.time_zone %}
 
                         {% for session in sessions %}
                             {% if session.recordings %}
@@ -84,7 +84,7 @@
 
                         {% endfor %}
 
-                    </tbody>
+                    {% endtimezone %}</tbody>
                 {% endif %}
             </table>
         </div> <!-- inline-group -->
diff --git a/ietf/settings.py b/ietf/settings.py
index 5e4912d85..aa35aa8e3 100644
--- a/ietf/settings.py
+++ b/ietf/settings.py
@@ -1041,8 +1041,6 @@ CHAT_URL_PATTERN = 'https://zulip.ietf.org/#narrow/stream/{chat_room_name}'
 # If we need to revert to xmpp
 # CHAT_ARCHIVE_URL_PATTERN = 'https://www.ietf.org/jabber/logs/{chat_room_name}?C=M;O=D'
 
-PRODUCTION_TIMEZONE = "America/Los_Angeles"
-
 PYFLAKES_DEFAULT_ARGS= ["ietf", ]
 VULTURE_DEFAULT_ARGS= ["ietf", ]
 
@@ -1189,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/stats/management/commands/fetch_meeting_attendance.py b/ietf/stats/management/commands/fetch_meeting_attendance.py
index 5078c7cee..7f936531d 100644
--- a/ietf/stats/management/commands/fetch_meeting_attendance.py
+++ b/ietf/stats/management/commands/fetch_meeting_attendance.py
@@ -1,10 +1,10 @@
 # Copyright The IETF Trust 2017-2019, All Rights Reserved
 # Copyright 2016 IETF Trust
 
-import datetime
 import syslog
 
 from django.core.management.base import BaseCommand, CommandError
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -32,7 +32,7 @@ class Command(BaseCommand):
         elif options['all']:
             meetings = Meeting.objects.filter(type="ietf").order_by("date")
         elif options['latest']:
-            meetings = Meeting.objects.filter(type="ietf", date__lte=datetime.datetime.today()).order_by("-date")[:options['latest']]
+            meetings = Meeting.objects.filter(type="ietf", date__lte=timezone.now()).order_by("-date")[:options['latest']]
         else:
             raise CommandError("Please use one of --meeting, --all or --latest")
 
diff --git a/ietf/stats/tests.py b/ietf/stats/tests.py
index 32a0dfea8..dae352752 100644
--- a/ietf/stats/tests.py
+++ b/ietf/stats/tests.py
@@ -13,6 +13,7 @@ from requests import Response
 import debug    # pyflakes:ignore
 
 from django.urls import reverse as urlreverse
+from django.utils import timezone
 
 from ietf.utils.test_utils import login_testing_unauthorized, TestCase
 import ietf.stats.views
@@ -29,6 +30,7 @@ from ietf.review.factories import ReviewRequestFactory, ReviewerSettingsFactory,
 from ietf.stats.models import MeetingRegistration, CountryAlias
 from ietf.stats.factories import MeetingRegistrationFactory
 from ietf.stats.utils import get_meeting_registration_data
+from ietf.utils.timezone import date_today
 
 
 class StatisticsTests(TestCase):
@@ -64,7 +66,7 @@ class StatisticsTests(TestCase):
         Document.objects.filter(pk=draft.pk).update(words=4000)
         # move it back so it shows up in the yearly summaries
         NewRevisionDocEvent.objects.filter(doc=draft, rev=draft.rev).update(
-            time=datetime.datetime.now() - datetime.timedelta(days=500))
+            time=timezone.now() - datetime.timedelta(days=500))
 
         referencing_draft = Document.objects.create(
             name="draft-ietf-mars-referencing",
@@ -89,7 +91,7 @@ class StatisticsTests(TestCase):
             doc=referencing_draft,
             desc="New revision available",
             rev=referencing_draft.rev,
-            time=datetime.datetime.now() - datetime.timedelta(days=1000)
+            time=timezone.now() - datetime.timedelta(days=1000)
         )
 
 
@@ -122,7 +124,7 @@ class StatisticsTests(TestCase):
 
     def test_meeting_stats(self):
         # create some data for the statistics
-        meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(), number="96")
+        meeting = MeetingFactory(type_id='ietf', date=date_today(), number="96")
         MeetingRegistrationFactory(first_name='John', last_name='Smith', country_code='US', email="john.smith@example.us", meeting=meeting, attended=True)
         CountryAlias.objects.get_or_create(alias="US", country=CountryName.objects.get(slug="US"))
         p = MeetingRegistrationFactory(first_name='Jaume', last_name='Guillaume', country_code='FR', email="jaume.guillaume@example.fr", meeting=meeting, attended=False).person
@@ -198,7 +200,7 @@ class StatisticsTests(TestCase):
                 self.assertTrue(q('.review-stats td:contains("1")'))
 
         # check stacked chart
-        expected_date = datetime.date.today().replace(day=1)
+        expected_date = date_today().replace(day=1)
         expected_js_timestamp = calendar.timegm(expected_date.timetuple()) * 1000
         url = urlreverse(ietf.stats.views.review_stats, kwargs={ "stats_type": "time" })
         url += "?team={}".format(review_req.team.acronym)
diff --git a/ietf/stats/views.py b/ietf/stats/views.py
index 1667387bc..09a25b47b 100644
--- a/ietf/stats/views.py
+++ b/ietf/stats/views.py
@@ -18,6 +18,7 @@ from django.db.models import Count, Q
 from django.http import HttpResponseRedirect
 from django.shortcuts import get_object_or_404, render
 from django.urls import reverse as urlreverse
+from django.utils import timezone
 from django.utils.safestring import mark_safe
 from django.utils.text import slugify
 
@@ -39,6 +40,8 @@ from ietf.stats.models import MeetingRegistration, CountryAlias
 from ietf.stats.utils import get_aliased_affiliations, get_aliased_countries, compute_hirsch_index
 from ietf.ietfauth.utils import has_role
 from ietf.utils.response import permission_denied
+from ietf.utils.timezone import date_today, DEADLINE_TZINFO, RPC_TZINFO
+
 
 def stats_index(request):
     return render(request, "stats/index.html")
@@ -196,7 +199,7 @@ def document_stats(request, stats_type=None):
         if "y" in time_choice:
             try:
                 y = int(time_choice.rstrip("y"))
-                from_time = datetime.datetime.today() - dateutil.relativedelta.relativedelta(years=y)
+                from_time = timezone.now() - dateutil.relativedelta.relativedelta(years=y)
             except ValueError:
                 pass
 
@@ -622,8 +625,9 @@ def document_stats(request, stats_type=None):
                 type__in=["published_rfc", "new_revision"],
             ).values_list("doc", "time").order_by("doc")
 
-            for doc, time in docevent_qs.iterator():
-                doc_years[doc].add(time.year)
+            for doc_id, time in docevent_qs.iterator():
+                # RPC_TZINFO is used to match the timezone handling in Document.pub_date()
+                doc_years[doc_id].add(time.astimezone(RPC_TZINFO).year)
 
             person_qs = Person.objects.filter(person_filters)
 
@@ -637,7 +641,7 @@ def document_stats(request, stats_type=None):
             template_name = "yearly"
 
             years_from = from_time.year if from_time else 1
-            years_to = datetime.date.today().year - 1
+            years_to = timezone.now().year - 1
 
 
             if stats_type == "yearly/affiliation":
@@ -905,7 +909,7 @@ def meeting_stats(request, num=None, stats_type=None):
 
                 continents = {}
                 
-                meetings = Meeting.objects.filter(type='ietf', date__lte=datetime.date.today()).order_by('number')
+                meetings = Meeting.objects.filter(type='ietf', date__lte=date_today()).order_by('number')
                 for m in meetings:
                     country = CountryName.objects.get(slug=m.country)
                     continents[country.continent.name] = country.continent.name
@@ -1070,12 +1074,12 @@ def review_stats(request, stats_type=None, acronym=None):
         except ValueError:
             return None
 
-    today = datetime.date.today()
+    today = date_today(DEADLINE_TZINFO)
     from_date = parse_date(request.GET.get("from")) or today - dateutil.relativedelta.relativedelta(years=1)
     to_date = parse_date(request.GET.get("to")) or today
 
-    from_time = datetime.datetime.combine(from_date, datetime.time.min)
-    to_time = datetime.datetime.combine(to_date, datetime.time.max)
+    from_time = datetime.datetime.combine(from_date, datetime.time.min, tzinfo=DEADLINE_TZINFO)
+    to_time = datetime.datetime.combine(to_date, datetime.time.max, tzinfo=DEADLINE_TZINFO)
 
     # teams/reviewers
     teams = list(Group.objects.exclude(reviewrequest=None).distinct().order_by("name"))
diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py
index 34f568d2e..4715f9bb5 100644
--- a/ietf/submit/forms.py
+++ b/ietf/submit/forms.py
@@ -7,7 +7,6 @@ import os
 import re
 import datetime
 import email
-import pytz
 import sys
 import tempfile
 import xml2rfc
@@ -21,6 +20,7 @@ from django import forms
 from django.conf import settings
 from django.utils.html import mark_safe, format_html # type:ignore
 from django.urls import reverse as urlreverse
+from django.utils import timezone
 from django.utils.encoding import force_str
 
 import debug                            # pyflakes:ignore
@@ -42,6 +42,7 @@ from ietf.submit.parsers.xml_parser import XMLParser
 from ietf.utils import log
 from ietf.utils.draft import PlaintextDraft
 from ietf.utils.text import normalize_text
+from ietf.utils.timezone import date_today
 from ietf.utils.xmldraft import XMLDraft, XMLParseError
 
 
@@ -79,7 +80,7 @@ class SubmissionBaseUploadForm(forms.Form):
         self.base_formats = None        # None will raise an exception in clean() if this isn't changed in a subclass
 
     def set_cutoff_warnings(self):
-        now = datetime.datetime.now(pytz.utc)
+        now = timezone.now()
         meeting = Meeting.get_current_meeting()
         if not meeting:
             return
@@ -155,7 +156,7 @@ class SubmissionBaseUploadForm(forms.Form):
             raise forms.ValidationError('The submission tool is currently shut down')
 
         # check general submission rate thresholds before doing any more work
-        today = datetime.date.today()
+        today = date_today()
         self.check_submissions_thresholds(
             "for the same submitter",
             dict(remote_ip=self.remote_ip, submission_date=today),
@@ -572,7 +573,7 @@ class DeprecatedSubmissionBaseUploadForm(SubmissionBaseUploadForm):
                 raise forms.ValidationError(mark_safe(self.cutoff_warning))
 
             # check thresholds
-            today = datetime.date.today()
+            today = date_today()
 
             self.check_submissions_thresholds(
                 "for the draft %s" % self.filename,
diff --git a/ietf/submit/mail.py b/ietf/submit/mail.py
index 16d1f0973..81d2233af 100644
--- a/ietf/submit/mail.py
+++ b/ietf/submit/mail.py
@@ -4,7 +4,6 @@
 
 import re
 import email
-import datetime
 import base64
 import os
 import pyzmail
@@ -28,6 +27,8 @@ from ietf.utils.accesstoken import generate_access_token
 from ietf.mailtrigger.utils import gather_address_lists, get_base_submission_message_address
 from ietf.submit.models import SubmissionEmailEvent, Submission
 from ietf.submit.checkers import DraftIdnitsChecker
+from ietf.utils.timezone import date_today
+
 
 def send_submission_confirmation(request, submission, chair_notice=False):
     subject = 'Confirm submission of I-D %s' % submission.name
@@ -299,7 +300,7 @@ def add_submission_email(request, remote_ip, name, rev, submission_pk, message,
                     rev=rev,
                     title=name,
                     note="",
-                    submission_date=datetime.date.today(),
+                    submission_date=date_today(),
                     replaces="",
             )
             from ietf.submit.utils import create_submission_event, docevent_from_submission
diff --git a/ietf/submit/migrations/0011_use_timezone_now_for_submit_models.py b/ietf/submit/migrations/0011_use_timezone_now_for_submit_models.py
new file mode 100644
index 000000000..69d40bf8a
--- /dev/null
+++ b/ietf/submit/migrations/0011_use_timezone_now_for_submit_models.py
@@ -0,0 +1,29 @@
+# Generated by Django 2.2.28 on 2022-07-12 11:24
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('submit', '0010_create_cancel_stale_submissions_task'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='preapproval',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AlterField(
+            model_name='submissioncheck',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+        migrations.AlterField(
+            model_name='submissionevent',
+            name='time',
+            field=models.DateTimeField(default=django.utils.timezone.now),
+        ),
+    ]
diff --git a/ietf/submit/migrations/0012_use_date_today_helper.py b/ietf/submit/migrations/0012_use_date_today_helper.py
new file mode 100644
index 000000000..3a02552f6
--- /dev/null
+++ b/ietf/submit/migrations/0012_use_date_today_helper.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.2.28 on 2022-10-18 15:43
+
+from django.db import migrations, models
+import ietf.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('submit', '0011_use_timezone_now_for_submit_models'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='submission',
+            name='submission_date',
+            field=models.DateField(default=ietf.utils.timezone.date_today),
+        ),
+    ]
diff --git a/ietf/submit/models.py b/ietf/submit/models.py
index 9f0e2fa33..0f9a86cd2 100644
--- a/ietf/submit/models.py
+++ b/ietf/submit/models.py
@@ -2,11 +2,11 @@
 # -*- coding: utf-8 -*-
 
 
-import datetime
 import email
 import jsonfield
 
 from django.db import models
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -18,6 +18,7 @@ from ietf.name.models import DraftSubmissionStateName, FormalLanguageName
 from ietf.utils.accesstoken import generate_random_key, generate_access_token
 from ietf.utils.text import parse_unicode
 from ietf.utils.models import ForeignKey
+from ietf.utils.timezone import date_today
 
 
 def parse_email_line(line):
@@ -53,7 +54,7 @@ class Submission(models.Model):
     file_types = models.CharField(max_length=50, blank=True)
     file_size = models.IntegerField(null=True, blank=True)
     document_date = models.DateField(null=True, blank=True)
-    submission_date = models.DateField(default=datetime.date.today)
+    submission_date = models.DateField(default=date_today)
     xml_version = models.CharField(null=True, max_length=4, default=None)
 
     submitter = models.CharField(max_length=255, blank=True, help_text="Name and email of submitter, e.g. \"John Doe &lt;john@example.org&gt;\".")
@@ -120,7 +121,7 @@ class Submission(models.Model):
 
 
 class SubmissionCheck(models.Model):
-    time = models.DateTimeField(default=datetime.datetime.now)
+    time = models.DateTimeField(default=timezone.now)
     submission = ForeignKey(Submission, related_name='checks')
     checker = models.CharField(max_length=256, blank=True)
     passed = models.BooleanField(null=True, default=False)
@@ -139,7 +140,7 @@ class SubmissionCheck(models.Model):
 
 class SubmissionEvent(models.Model):
     submission = ForeignKey(Submission)
-    time = models.DateTimeField(default=datetime.datetime.now)
+    time = models.DateTimeField(default=timezone.now)
     by = ForeignKey(Person, null=True, blank=True)
     desc = models.TextField()
 
@@ -157,7 +158,7 @@ class Preapproval(models.Model):
     """Pre-approved draft submission name."""
     name = models.CharField(max_length=255, db_index=True)
     by = ForeignKey(Person)
-    time = models.DateTimeField(default=datetime.datetime.now)
+    time = models.DateTimeField(default=timezone.now)
 
     def __str__(self):
         return self.name
diff --git a/ietf/submit/parsers/base.py b/ietf/submit/parsers/base.py
index 5c5d0d6fc..679798cc4 100644
--- a/ietf/submit/parsers/base.py
+++ b/ietf/submit/parsers/base.py
@@ -3,7 +3,6 @@
 
 
 import re
-import datetime
 import debug                            # pyflakes:ignore
 
 from typing import List, Optional   # pyflakes:ignore
@@ -12,6 +11,8 @@ from django.conf import settings
 from django.template.defaultfilters import filesizeformat
 
 from ietf.utils.mime import get_mime_type
+from ietf.utils.timezone import date_today
+
 
 class MetaData(object):
     rev = None
@@ -60,7 +61,7 @@ class FileParser(object):
         self.parse_max_size();
         self.parse_filename_extension()
         self.parse_file_type()
-        self.parsed_info.metadata.submission_date = datetime.date.today()
+        self.parsed_info.metadata.submission_date = date_today()
         return self.parsed_info
 
     def parse_invalid_chars_in_filename(self):
diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py
index 70baff432..9f6087d9b 100644
--- a/ietf/submit/tests.py
+++ b/ietf/submit/tests.py
@@ -52,6 +52,7 @@ from ietf.utils.accesstoken import generate_access_token
 from ietf.utils.mail import outbox, empty_outbox, get_payload_text
 from ietf.utils.models import VersionInfo
 from ietf.utils.test_utils import login_testing_unauthorized, TestCase
+from ietf.utils.timezone import date_today
 from ietf.utils.draft import PlaintextDraft
 
 
@@ -91,7 +92,7 @@ class BaseSubmitTestCase(TestCase):
         return settings.INTERNET_DRAFT_ARCHIVE_DIR
 
 def submission_file(name_in_doc, name_in_post, group, templatename, author=None, email=None, title=None, year=None, ascii=True):
-    _today = datetime.date.today()
+    _today = date_today()
     # construct appropriate text draft
     f = io.open(os.path.join(settings.BASE_DIR, "submit", templatename))
     template = f.read()
@@ -146,7 +147,7 @@ def create_draft_submission_with_rev_mismatch(rev='01'):
     sub = Submission.objects.create(
         name=draft_name,
         group=None,
-        submission_date=datetime.date.today() - datetime.timedelta(days=1),
+        submission_date=date_today() - datetime.timedelta(days=1),
         rev=rev,
         state_id='posted',
     )
@@ -164,7 +165,7 @@ class SubmitTests(BaseSubmitTestCase):
     def setUp(self):
         super().setUp()
         # Submit views assume there is a "next" IETF to look for cutoff dates against
-        MeetingFactory(type_id='ietf', date=datetime.date.today()+datetime.timedelta(days=180))
+        MeetingFactory(type_id='ietf', date=date_today()+datetime.timedelta(days=180))
 
     def create_and_post_submission(self, name, rev, author, group=None, formats=("txt",), base_filename=None):
         """Helper to create and post a submission
@@ -287,7 +288,7 @@ class SubmitTests(BaseSubmitTestCase):
         # prepare draft to suggest replace
         sug_replaced_draft = Document.objects.create(
             name="draft-ietf-ames-sug-replaced",
-            time=datetime.datetime.now(),
+            time=timezone.now(),
             type_id="draft",
             title="Draft to be suggested to be replaced",
             stream_id="ietf",
@@ -298,7 +299,7 @@ class SubmitTests(BaseSubmitTestCase):
             words=100,
             intended_std_level_id="ps",
             ad=draft.ad,
-            expires=datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
+            expires=timezone.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
             notify="aliens@example.mars",
             note="",
         )
@@ -357,7 +358,7 @@ class SubmitTests(BaseSubmitTestCase):
         self.assertTrue(os.path.exists(os.path.join(self.repository_dir, "%s-%s.txt" % (name, rev))))
         self.assertEqual(draft.type_id, "draft")
         self.assertEqual(draft.stream_id, "ietf")
-        self.assertTrue(draft.expires >= datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE - 1))
+        self.assertTrue(draft.expires >= timezone.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE - 1))
         self.assertEqual(draft.get_state("draft-stream-%s" % draft.stream_id).slug, "wg-doc")
         authors = draft.documentauthor_set.all()
         self.assertEqual(len(authors), 1)
@@ -1355,7 +1356,7 @@ class SubmitTests(BaseSubmitTestCase):
         # edit
         mailbox_before = len(outbox)
         # FIXME If this test is started before midnight, and ends after, it will fail
-        document_date = datetime.date.today() - datetime.timedelta(days=-3)
+        document_date = date_today() - datetime.timedelta(days=-3)
         r = self.client.post(edit_url, {
             "edit-title": "some title",
             "edit-rev": "00",
@@ -1523,7 +1524,7 @@ class SubmitTests(BaseSubmitTestCase):
     def test_expire_submissions(self):
         s = Submission.objects.create(name="draft-ietf-mars-foo",
                                       group=None,
-                                      submission_date=datetime.date.today() - datetime.timedelta(days=10),
+                                      submission_date=date_today() - datetime.timedelta(days=10),
                                       rev="00",
                                       state_id="uploaded")
 
@@ -1557,7 +1558,7 @@ class SubmitTests(BaseSubmitTestCase):
 
         # Put today in the blackout period
         meeting = Meeting.get_current_meeting()
-        meeting.importantdate_set.create(name_id='idcutoff',date=datetime.date.today()-datetime.timedelta(days=2))
+        meeting.importantdate_set.create(name_id='idcutoff',date=date_today()-datetime.timedelta(days=2))
         
         # regular user, no access
         r = self.client.get(url)
@@ -1576,30 +1577,30 @@ class SubmitTests(BaseSubmitTestCase):
         url = urlreverse('ietf.submit.views.upload_submission')
 
         meeting = Meeting.get_current_meeting()
-        meeting.date = datetime.date.today()+datetime.timedelta(days=7)
+        meeting.date = date_today()+datetime.timedelta(days=7)
         meeting.save()
         meeting.importantdate_set.filter(name_id='idcutoff').delete()
-        meeting.importantdate_set.create(name_id='idcutoff', date=datetime.date.today()+datetime.timedelta(days=7))
+        meeting.importantdate_set.create(name_id='idcutoff', date=date_today()+datetime.timedelta(days=7))
         r = self.client.get(url)
         self.assertEqual(r.status_code,200)
         q = PyQuery(r.content)
         self.assertEqual(len(q('input[type=file][name=txt]')), 1)        
 
         meeting = Meeting.get_current_meeting()
-        meeting.date = datetime.date.today()
+        meeting.date = date_today()
         meeting.save()
         meeting.importantdate_set.filter(name_id='idcutoff').delete()
-        meeting.importantdate_set.create(name_id='idcutoff', date=datetime.date.today())
+        meeting.importantdate_set.create(name_id='idcutoff', date=date_today())
         r = self.client.get(url)
         self.assertEqual(r.status_code,200)
         q = PyQuery(r.content)
         self.assertEqual(len(q('input[type=file][name=txt]')), 1)        
 
         meeting = Meeting.get_current_meeting()
-        meeting.date = datetime.date.today()-datetime.timedelta(days=1)
+        meeting.date = date_today()-datetime.timedelta(days=1)
         meeting.save()
         meeting.importantdate_set.filter(name_id='idcutoff').delete()
-        meeting.importantdate_set.create(name_id='idcutoff', date=datetime.date.today()-datetime.timedelta(days=1))
+        meeting.importantdate_set.create(name_id='idcutoff', date=date_today()-datetime.timedelta(days=1))
         r = self.client.get(url)
         self.assertEqual(r.status_code,200)
         q = PyQuery(r.content)
@@ -1864,7 +1865,7 @@ class SubmitTests(BaseSubmitTestCase):
                 self.index += 1
                 sub = Submission.objects.create(name="draft-ietf-mars-bar-%d" % self.index,
                                                 group=Group.objects.get(acronym="mars"),
-                                                submission_date=datetime.date.today(),
+                                                submission_date=date_today(),
                                                 authors=[dict(name=self.author.name,
                                                               email=self.author.user.email,
                                                               affiliation='affiliation',
@@ -2136,17 +2137,17 @@ class ApprovalsTestCase(BaseSubmitTestCase):
 
         Submission.objects.create(name="draft-ietf-mars-foo",
                                   group=Group.objects.get(acronym="mars"),
-                                  submission_date=datetime.date.today(),
+                                  submission_date=date_today(),
                                   rev="00",
                                   state_id="posted")
         Submission.objects.create(name="draft-ietf-mars-bar",
                                   group=Group.objects.get(acronym="mars"),
-                                  submission_date=datetime.date.today(),
+                                  submission_date=date_today(),
                                   rev="00",
                                   state_id="grp-appr")
         Submission.objects.create(name="draft-ietf-mars-quux",
                                   group=Group.objects.get(acronym="mars"),
-                                  submission_date=datetime.date.today(),
+                                  submission_date=date_today(),
                                   rev="00",
                                   state_id="ad-appr")
 
@@ -2295,11 +2296,11 @@ class ManualPostsTestCase(BaseSubmitTestCase):
 
         Submission.objects.create(name="draft-ietf-mars-foo",
                                   group=Group.objects.get(acronym="mars"),
-                                  submission_date=datetime.date.today(),
+                                  submission_date=date_today(),
                                   state_id="manual")
         Submission.objects.create(name="draft-ietf-mars-bar",
                                   group=Group.objects.get(acronym="mars"),
-                                  submission_date=datetime.date.today(),
+                                  submission_date=date_today(),
                                   rev="00",
                                   state_id="grp-appr")
 
@@ -2320,7 +2321,7 @@ Subject: test submission via email
 Please submit my draft at http://test.com/mydraft.txt
 
 Thank you
-""".format(datetime.datetime.now().ctime())
+""".format(timezone.now().ctime())
         message = email.message_from_string(force_str(message_string))
         submission, submission_email_event = (
             add_submission_email(request=None,
@@ -2402,7 +2403,7 @@ Content-Disposition: attachment; filename="attach.txt"
 QW4gZXhhbXBsZSBhdHRhY2htZW50IHd0aG91dCB2ZXJ5IG11Y2ggaW4gaXQuCgpBIGNvdXBs
 ZSBvZiBsaW5lcyAtIGJ1dCBpdCBjb3VsZCBiZSBhIGRyYWZ0Cg==
 --------------090908050800030909090207--
-""".format(frm, datetime.datetime.now().ctime())
+""".format(frm, timezone.now().ctime())
 
         message = email.message_from_string(force_str(message_string))
         submission, submission_email_event = (
@@ -2458,7 +2459,7 @@ Content-Disposition: attachment; filename="attachment.txt"
 QW4gZXhhbXBsZSBhdHRhY2htZW50IHd0aG91dCB2ZXJ5IG11Y2ggaW4gaXQuCgpBIGNvdXBs
 ZSBvZiBsaW5lcyAtIGJ1dCBpdCBjb3VsZCBiZSBhIGRyYWZ0Cg==
 --------------090908050800030909090207--
-""".format(datetime.datetime.now().ctime())
+""".format(timezone.now().ctime())
 
         # Back to secretariat
         self.client.login(username="secretary", password="secretary+password")
@@ -2597,7 +2598,7 @@ Subject: Another message
 About my submission
 
 Thank you
-""".format(datetime.datetime.now().ctime())
+""".format(timezone.now().ctime())
 
             r = self.client.post(add_email_url, {
                 "name": "{}-{}".format(submission.name, submission.rev),
@@ -2664,7 +2665,7 @@ Thank you
 From: {}
 Date: {}
 Subject: test
-""".format(reply_to, to, datetime.datetime.now().ctime())
+""".format(reply_to, to, timezone.now().ctime())
 
         result = process_response_email(message_string)
         self.assertIsInstance(result, Message)
@@ -2885,7 +2886,7 @@ class ApiSubmissionTests(BaseSubmitTestCase):
 
 class SubmissionUploadFormTests(BaseSubmitTestCase):
     def test_check_submission_thresholds(self):
-        today = datetime.date.today()
+        today = date_today()
         yesterday = today - datetime.timedelta(days=1)
         (this_group, that_group) = GroupFactory.create_batch(2, type_id='wg')
         this_ip = '10.0.0.1'
@@ -3101,7 +3102,7 @@ class AsyncSubmissionTests(BaseSubmitTestCase):
     """Tests of async submission-related tasks"""
     def test_process_uploaded_submission(self):
         """process_uploaded_submission should properly process a submission"""
-        _today = datetime.date.today()
+        _today = date_today()
         xml, author = submission_file('draft-somebody-test-00', 'draft-somebody-test-00.xml', None, 'test_submission.xml')
         xml_data = xml.read()
         xml.close()
@@ -3389,7 +3390,7 @@ class ApiSubmitTests(BaseSubmitTestCase):
         super().setUp()
         # break early in case of missing configuration
         self.assertTrue(os.path.exists(settings.IDSUBMIT_IDNITS_BINARY))
-        MeetingFactory(type_id='ietf', date=datetime.date.today()+datetime.timedelta(days=60))
+        MeetingFactory(type_id='ietf', date=date_today()+datetime.timedelta(days=60))
 
     def do_post_submission(self, rev, author=None, name=None, group=None, email=None, title=None, year=None):
         url = urlreverse('ietf.submit.views.api_submit')
diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py
index eb6838d94..6fb0a36b6 100644
--- a/ietf/submit/utils.py
+++ b/ietf/submit/utils.py
@@ -21,6 +21,7 @@ from django.db import transaction
 from django.http import HttpRequest     # pyflakes:ignore
 from django.utils.module_loading import import_string
 from django.contrib.auth.models import AnonymousUser
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -47,6 +48,7 @@ from ietf.utils.accesstoken import generate_random_key
 from ietf.utils.draft import PlaintextDraft
 from ietf.utils.mail import is_valid_email
 from ietf.utils.text import parse_unicode, normalize_text
+from ietf.utils.timezone import date_today
 from ietf.utils.xmldraft import XMLDraft
 from ietf.person.name import unidecode_name
 
@@ -338,7 +340,7 @@ def post_submission(request, submission, approved_doc_desc, approved_subm_desc):
         if stream_slug:
             draft.stream = StreamName.objects.get(slug=stream_slug)
 
-    draft.expires = datetime.datetime.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE)
+    draft.expires = timezone.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE)
     log.log(f"{submission.name}: got draft details")
 
     events = []
@@ -609,7 +611,7 @@ def ensure_person_email_info_exists(name, email, docname):
             email.active = active
         email.person = person
         if email.time is None:
-            email.time = datetime.datetime.now()
+            email.time = timezone.now()
         email.origin = "author: %s" % docname
         email.save()
 
@@ -725,7 +727,7 @@ def recently_approved_by_user(user, since):
     )
 
 def expirable_submissions(older_than_days):
-    cutoff = datetime.date.today() - datetime.timedelta(days=older_than_days)
+    cutoff = date_today() - datetime.timedelta(days=older_than_days)
     return Submission.objects.exclude(state__in=("cancel", "posted")).filter(submission_date__lt=cutoff)
 
 def expire_submission(submission, by):
@@ -848,7 +850,7 @@ def fill_in_submission(form, submission, authors, abstract, file_size):
     submission.file_size = file_size
     submission.file_types = ','.join(form.file_types)
     submission.xml_version = form.xml_version
-    submission.submission_date = datetime.date.today()
+    submission.submission_date = date_today()
     submission.replaces = ""
     if form.parsed_draft is not None:
         submission.pages = form.parsed_draft.get_pagecount()
diff --git a/ietf/submit/views.py b/ietf/submit/views.py
index bab859702..9dcb88d13 100644
--- a/ietf/submit/views.py
+++ b/ietf/submit/views.py
@@ -48,6 +48,8 @@ from ietf.utils.accesstoken import generate_access_token
 from ietf.utils.log import log
 from ietf.utils.mail import parseaddr, send_mail_message
 from ietf.utils.response import permission_denied
+from ietf.utils.timezone import date_today
+
 
 def upload_submission(request):
     if request.method == 'POST':
@@ -161,7 +163,7 @@ def api_submission(request):
                 submission.state = DraftSubmissionStateName.objects.get(slug="validating")
                 submission.remote_ip = form.remote_ip
                 submission.file_types = ','.join(form.file_types)
-                submission.submission_date = datetime.date.today()
+                submission.submission_date = date_today()
                 submission.submitter = user.person.formatted_email()
                 submission.replaces = form.cleaned_data['replaces']
                 submission.save()
@@ -723,7 +725,7 @@ def approvals(request):
     preapprovals = preapprovals_for_user(request.user)
 
     days = 30
-    recently_approved = recently_approved_by_user(request.user, datetime.date.today() - datetime.timedelta(days=days))
+    recently_approved = recently_approved_by_user(request.user, date_today() - datetime.timedelta(days=days))
 
     return render(request, 'submit/approvals.html',
                               {'selected': 'approvals',
diff --git a/ietf/sync/iana.py b/ietf/sync/iana.py
index 9d0f66435..978b49507 100644
--- a/ietf/sync/iana.py
+++ b/ietf/sync/iana.py
@@ -9,7 +9,10 @@ import json
 import re
 import requests
 
+from email.utils import parsedate_to_datetime
+
 from django.conf import settings
+from django.utils import timezone
 from django.utils.encoding import smart_bytes, force_str
 from django.utils.http import urlquote
 
@@ -21,7 +24,6 @@ from ietf.doc.utils import add_state_change_event
 from ietf.person.models import Person
 from ietf.utils.log import log
 from ietf.utils.mail import parseaddr, get_payload_text
-from ietf.utils.timezone import local_timezone_to_utc, email_time_to_local_timezone, utc_to_local_timezone
 
 
 #PROTOCOLS_URL = "https://www.iana.org/protocols/"
@@ -64,8 +66,8 @@ def update_rfc_log_from_protocol_page(rfc_names, rfc_must_published_later_than):
     
 
 def fetch_changes_json(url, start, end):
-    url += "?start=%s&end=%s" % (urlquote(local_timezone_to_utc(start).strftime("%Y-%m-%d %H:%M:%S")),
-                                 urlquote(local_timezone_to_utc(end).strftime("%Y-%m-%d %H:%M:%S")))
+    url += "?start=%s&end=%s" % (urlquote(start.astimezone(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S")),
+                                 urlquote(end.astimezone(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S")))
     # HTTP basic auth
     username = "ietfsync"
     password = settings.IANA_SYNC_PASSWORD
@@ -159,8 +161,7 @@ def update_history_with_changes(changes, send_email=True):
 
     for c in changes:
         docname = c['doc']
-        timestamp = datetime.datetime.strptime(c["time"], "%Y-%m-%d %H:%M:%S")
-        timestamp = utc_to_local_timezone(timestamp) # timestamps are in UTC
+        timestamp = datetime.datetime.strptime(c["time"], "%Y-%m-%d %H:%M:%S",).replace(tzinfo=datetime.timezone.utc)
 
         if c['type'] in ("iana_state", "iana_review"):
             if c['type'] == "iana_state":
@@ -241,9 +242,12 @@ def parse_review_email(text):
     doc_name = strip_version_extension(doc_name)
 
     # date
-    review_time = datetime.datetime.now()
+    review_time = timezone.now()
     if "Date" in msg:
-        review_time = email_time_to_local_timezone(msg["Date"])
+        review_time = parsedate_to_datetime(msg["Date"])
+        # parsedate_to_datetime() may return a naive timezone - treat as UTC
+        if review_time.tzinfo is None or review_time.tzinfo.utcoffset(review_time) is None:
+            review_time = review_time.replace(tzinfo=datetime.timezone.utc)
 
     # by
     by = None
diff --git a/ietf/sync/rfceditor.py b/ietf/sync/rfceditor.py
index c2264a0d3..1da11f01e 100644
--- a/ietf/sync/rfceditor.py
+++ b/ietf/sync/rfceditor.py
@@ -11,6 +11,7 @@ from urllib.parse import urlencode
 from xml.dom import pulldom, Node
 
 from django.conf import settings
+from django.utils import timezone
 from django.utils.encoding import smart_bytes, force_str, force_text
 
 import debug                            # pyflakes:ignore
@@ -24,6 +25,7 @@ from ietf.name.models import StdLevelName, StreamName
 from ietf.person.models import Person
 from ietf.utils.log import log
 from ietf.utils.mail import send_mail_text
+from ietf.utils.timezone import datetime_from_date, RPC_TZINFO
 
 #QUEUE_URL = "https://www.rfc-editor.org/queue2.xml"
 #INDEX_URL = "https://www.rfc-editor.org/rfc/rfc-index.xml"
@@ -331,9 +333,12 @@ def parse_index(response):
 
 
 def update_docs_from_rfc_index(index_data, errata_data, skip_older_than_date=None):
-    """Given parsed data from the RFC Editor index, update the documents
-    in the database. Yields a list of change descriptions for each
-    document, if any."""
+    """Given parsed data from the RFC Editor index, update the documents in the database
+
+    Yields a list of change descriptions for each document, if any.
+
+    The skip_older_than_date is a bare date, not a datetime.
+    """
 
     errata = {}
     for item in errata_data:
@@ -442,8 +447,16 @@ def update_docs_from_rfc_index(index_data, errata_data, skip_older_than_date=Non
             # unfortunately, rfc_published_date doesn't include the correct day
             # at the moment because the data only has month/year, so
             # try to deduce it
-            d = datetime.datetime.combine(rfc_published_date, datetime.time())
-            synthesized = datetime.datetime.now()
+            #
+            # Note: This is in done PST8PDT to preserve compatibility with events created when
+            # USE_TZ was False. The published_rfc event was created with a timestamp whose
+            # server-local datetime (PST8PDT) matched the publication date from the RFC index.
+            # When switching to USE_TZ=True, the timestamps were migrated so they still
+            # matched the publication date in PST8PDT. When interpreting the event timestamp
+            # as a publication date, you must treat it in the PST8PDT time zone. The
+            # RPC_TZINFO constant in ietf.utils.timezone is defined for this purpose.
+            d = datetime_from_date(rfc_published_date, RPC_TZINFO)
+            synthesized = timezone.now().astimezone(RPC_TZINFO)
             if abs(d - synthesized) > datetime.timedelta(days=60):
                 synthesized = d
             else:
diff --git a/ietf/sync/tests.py b/ietf/sync/tests.py
index 61bab795c..f245145d2 100644
--- a/ietf/sync/tests.py
+++ b/ietf/sync/tests.py
@@ -10,6 +10,7 @@ import quopri
 
 from django.conf import settings
 from django.urls import reverse as urlreverse
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -22,6 +23,7 @@ from ietf.sync import iana, rfceditor
 from ietf.utils.mail import outbox, empty_outbox
 from ietf.utils.test_utils import login_testing_unauthorized
 from ietf.utils.test_utils import TestCase
+from ietf.utils.timezone import date_today, RPC_TZINFO
 
 
 class IANASyncTests(TestCase):
@@ -34,11 +36,11 @@ class IANASyncTests(TestCase):
         self.assertEqual(len(rfc_names), 1)
         self.assertEqual(rfc_names[0], "rfc1234")
 
-        iana.update_rfc_log_from_protocol_page(rfc_names, datetime.datetime.now() - datetime.timedelta(days=1))
+        iana.update_rfc_log_from_protocol_page(rfc_names, timezone.now() - datetime.timedelta(days=1))
         self.assertEqual(DocEvent.objects.filter(doc=draft, type="rfc_in_iana_registry").count(), 1)
 
         # make sure it doesn't create duplicates
-        iana.update_rfc_log_from_protocol_page(rfc_names, datetime.datetime.now() - datetime.timedelta(days=1))
+        iana.update_rfc_log_from_protocol_page(rfc_names, timezone.now() - datetime.timedelta(days=1))
         self.assertEqual(DocEvent.objects.filter(doc=draft, type="rfc_in_iana_registry").count(), 1)
 
     def test_changes_sync(self):
@@ -190,7 +192,7 @@ ICANN
                         doc_name, review_time, by, comment = iana.parse_review_email(msg.encode('utf-8'))
     
                         self.assertEqual(doc_name, draft.name)
-                        self.assertEqual(review_time, datetime.datetime(2012, 5, 10, 5, 0, rtime))
+                        self.assertEqual(review_time, datetime.datetime(2012, 5, 10, 12, 0, rtime, tzinfo=datetime.timezone.utc))
                         self.assertEqual(by, Person.objects.get(user__username="iana"))
                         self.assertIn("there are no IANA Actions", comment.replace("\n", ""))
     
@@ -237,7 +239,7 @@ class RFCSyncTests(TestCase):
         DocAlias.objects.create(name=updated_doc.name).docs.add(updated_doc)
         DocAlias.objects.create(name="rfc123").docs.add(updated_doc)
 
-        today = datetime.date.today()
+        today = date_today()
 
         t = '''<?xml version="1.0" encoding="UTF-8"?>
 <rfc-index xmlns="http://www.rfc-editor.org/rfc-index"
@@ -352,7 +354,7 @@ class RFCSyncTests(TestCase):
         self.assertEqual(events[0].type, "sync_from_rfc_editor")
         self.assertEqual(events[1].type, "changed_action_holders")
         self.assertEqual(events[2].type, "published_rfc")
-        self.assertEqual(events[2].time.date(), today)
+        self.assertEqual(events[2].time.astimezone(RPC_TZINFO).date(), today)
         self.assertTrue("errata" in doc.tags.all().values_list("slug", flat=True))
         self.assertTrue(DocAlias.objects.filter(name="rfc1234", docs=doc))
         self.assertTrue(DocAlias.objects.filter(name="bcp1", docs=doc))
diff --git a/ietf/sync/views.py b/ietf/sync/views.py
index d6fc505c2..1d22c424c 100644
--- a/ietf/sync/views.py
+++ b/ietf/sync/views.py
@@ -12,6 +12,7 @@ from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.http import HttpResponse, HttpResponseRedirect, Http404
 from django.shortcuts import render
+from django.utils import timezone
 from django.views.decorators.csrf import csrf_exempt
 
 from ietf.doc.models import DeletedEvent, StateDocEvent, DocEvent
@@ -118,12 +119,12 @@ def rfceditor_undo(request):
     events = []
     events.extend(StateDocEvent.objects.filter(
         state_type="draft-rfceditor",
-        time__gte=datetime.datetime.now() - datetime.timedelta(weeks=1)
+        time__gte=timezone.now() - datetime.timedelta(weeks=1)
     ).order_by("-time", "-id"))
 
     events.extend(DocEvent.objects.filter(
         type="sync_from_rfc_editor",
-        time__gte=datetime.datetime.now() - datetime.timedelta(weeks=1)
+        time__gte=timezone.now() - datetime.timedelta(weeks=1)
     ).order_by("-time", "-id"))
 
     events.sort(key=lambda e: (e.time, e.id), reverse=True)
diff --git a/ietf/templates/doc/document_draft.html b/ietf/templates/doc/document_draft.html
index 63ee22c4f..0bb597b60 100644
--- a/ietf/templates/doc/document_draft.html
+++ b/ietf/templates/doc/document_draft.html
@@ -13,7 +13,7 @@
           title="Document changes"
           href="/feed/document-changes/{{ name }}/">
     <meta name="description"
-          content="{{ doc.title }} {% if doc.get_state_slug == 'rfc' and not snapshot %}(RFC {{ rfc_number }}{% if published %}, {{ published.time|date:'F Y' }}{% endif %}{% if obsoleted_by %}; obsoleted by {{ obsoleted_by|join:', ' }}{% endif %}){% else %}(Internet-Draft, {{ doc.time|date:'Y' }}){% endif %}">
+          content="{{ doc.title }} {% if doc.get_state_slug == 'rfc' and not snapshot %}(RFC {{ rfc_number }}{% if published %}, {{ doc.pub_date|date:'F Y' }}{% endif %}{% if obsoleted_by %}; obsoleted by {{ obsoleted_by|join:', ' }}{% endif %}){% else %}(Internet-Draft, {{ doc.time|date:'Y' }}){% endif %}">
 {% endblock %}
 {% block morecss %}.inline { display: inline; }{% endblock %}
 {% block title %}
@@ -47,7 +47,7 @@
                     {% if doc.get_state_slug == "rfc" and not snapshot %}
                         <span class="text-success">RFC - {{ doc.std_level }}</span>
                         {% if published %}
-                            ({{ published.time|date:"F Y" }})
+                            ({{ doc.pub_date|date:"F Y" }})
                         {% else %}
                             (Publication date unknown)
                         {% endif %}
@@ -115,7 +115,7 @@
                 <td class="edit"></td>
                 <td>
                     {{ doc.time|date:"Y-m-d" }}
-                    {% if latest_revision and latest_revision.time.date != doc.time.date %}
+                    {% if latest_revision and latest_revision.time|date:"Y-m-d" != doc.time|date:"Y-m-d" %}
                         <span class="text-muted">(Latest revision {{ latest_revision.time|date:"Y-m-d" }})</span>
                     {% endif %}
                 </td>
diff --git a/ietf/templates/group/meetings-row.html b/ietf/templates/group/meetings-row.html
index ff3b3af37..f445cf1e5 100644
--- a/ietf/templates/group/meetings-row.html
+++ b/ietf/templates/group/meetings-row.html
@@ -1,4 +1,7 @@
+{% load origin tz %}
+{% origin %}
 {% for s in sessions %}
+    {% timezone s.meeting.time_zone %}
     <tr>
         <td>
             {% if s.meeting.type.slug == 'ietf' %}
@@ -65,4 +68,5 @@
             {% endif %}
         </td>
     </tr>
+    {% endtimezone %}
 {% endfor %}
\ No newline at end of file
diff --git a/ietf/templates/group/meetings.html b/ietf/templates/group/meetings.html
index 69ec5458c..f58f8840b 100644
--- a/ietf/templates/group/meetings.html
+++ b/ietf/templates/group/meetings.html
@@ -1,5 +1,6 @@
 {% extends "group/group_base.html" %}
 {# Copyright The IETF Trust 2015, All Rights Reserved #}
+{# TODO: Add text noting that dates and weekdays are displayed in each meeting's timezone #}
 {% load origin %}
 {% block title %}
     Meetings
diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html
index 65889d7be..9ba473cce 100644
--- a/ietf/templates/meeting/agenda.html
+++ b/ietf/templates/meeting/agenda.html
@@ -6,7 +6,6 @@
 {% load textfilters %}
 {% load htmlfilters agenda_custom_tags %}
 {% load django_vite %}
-
 {% block title %}
     IETF {{ meetingData.meetingNumber }} Meeting Agenda
 {% endblock %}
diff --git a/ietf/templates/meeting/agenda.ics b/ietf/templates/meeting/agenda.ics
index 0c9e84762..778905147 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 '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/approve_proposed_slides.html b/ietf/templates/meeting/approve_proposed_slides.html
index 584e28081..05d969bb8 100644
--- a/ietf/templates/meeting/approve_proposed_slides.html
+++ b/ietf/templates/meeting/approve_proposed_slides.html
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 {# Copyright The IETF Trust 2015, All Rights Reserved #}
-{% load origin static django_bootstrap5 %}
+{% load origin static django_bootstrap5 tz %}
 {% block title %}
     Approve Slides Proposed for {{ submission.session.meeting }} : {{ submission.session.group.acronym }}
 {% endblock %}
@@ -15,7 +15,7 @@
     </h1>
     {% if session_number %}
         <h2>
-            Session {{ session_number }} : {{ submission.session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi" }}
+            Session {{ session_number }} : {{ submission.session.official_timeslotassignment.timeslot.time|timezone:submission.session.meeting.time_zone|date:"D M-d-Y Hi" }}
         </h2>
     {% endif %}
     <p class="alert alert-info my-3">
diff --git a/ietf/templates/meeting/diff_schedules.html b/ietf/templates/meeting/diff_schedules.html
index c5b117d2d..50d8b8420 100644
--- a/ietf/templates/meeting/diff_schedules.html
+++ b/ietf/templates/meeting/diff_schedules.html
@@ -2,7 +2,7 @@
 {# Copyright The IETF Trust 2020, All Rights Reserved #}
 {% load origin %}
 {% load ietf_filters %}
-{% load django_bootstrap5 %}
+{% load django_bootstrap5 tz %}
 {% block title %}Differences between Meeting Agendas for IETF {{ meeting.number }}{% endblock %}
 {% block content %}
     {% origin %}
@@ -26,7 +26,7 @@
                         <th scope="col"></th>
                     </tr>
                 </thead>
-                <tbody>
+                <tbody>{% timezone meeting.time_zone %}
                     {% for d in diffs %}
                         <tr>
                             <td>
@@ -40,7 +40,7 @@
                             </td>
                         </tr>
                     {% endfor %}
-                </tbody>
+                {% endtimezone %}</tbody>
             </table>
         {% else %}
             No differences in scheduled sessions found.
diff --git a/ietf/templates/meeting/important_dates_for_meeting.ics b/ietf/templates/meeting/important_dates_for_meeting.ics
index 25c26eabf..bc505e687 100644
--- a/ietf/templates/meeting/important_dates_for_meeting.ics
+++ b/ietf/templates/meeting/important_dates_for_meeting.ics
@@ -1,9 +1,9 @@
-{% for d in meeting.important_dates %}BEGIN:VEVENT
+{% load tz %}{% for d in meeting.important_dates %}BEGIN:VEVENT
 UID:ietf-{{ meeting.number }}-{{ d.name_id }}-{{ d.date.isoformat }}
 SUMMARY:IETF {{ meeting.number }}: {{ d.name.name }}
 CLASS:PUBLIC
 DTSTART{% if not d.midnight_cutoff %};VALUE=DATE{% endif %}:{{ d.date|date:"Ymd" }}{% if d.midnight_cutoff %}235900Z{% endif %}
-DTSTAMP:{{ meeting.cached_updated|date:"Ymd" }}T{{ meeting.cached_updated|date:"His" }}Z
+DTSTAMP:{{ meeting.cached_updated|utc|date:"Ymd" }}T{{ meeting.cached_updated|utc|date:"His" }}Z
 TRANSP:TRANSPARENT
 DESCRIPTION:{{ d.name.desc }}{% if first and d.name.slug == 'openreg' or first and d.name.slug == 'earlybird' %}\n
  Register here: https://www.ietf.org/how/meetings/register/{% endif %}{% if d.name.slug == 'opensched' %}\n
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_info.txt b/ietf/templates/meeting/interim_info.txt
index 4bb950b7d..6180bc664 100644
--- a/ietf/templates/meeting/interim_info.txt
+++ b/ietf/templates/meeting/interim_info.txt
@@ -1,4 +1,4 @@
-{% load ietf_filters %}
+{% load ietf_filters tz %}{% timezone meeting.time_zone %}
 ---------------------------------------------------------
 {{ group.type.verbose_name }} Name: {{ group.name|safe }}
 {% if group.type.slug == "wg"  or group.type.slug == "directorate" or group.type.slug == "team" %}Area Name: {{ group.parent }}
@@ -17,3 +17,4 @@ Remote Participation Information: {{ session.remote_instructions }}
 Agenda Note: {{ session.agenda_note }}
 {% endfor %}
 ---------------------------------------------------------
+{% endtimezone %}
\ No newline at end of file
diff --git a/ietf/templates/meeting/interim_meeting_cancellation_notice.txt b/ietf/templates/meeting/interim_meeting_cancellation_notice.txt
index 615342d98..d4774c168 100644
--- a/ietf/templates/meeting/interim_meeting_cancellation_notice.txt
+++ b/ietf/templates/meeting/interim_meeting_cancellation_notice.txt
@@ -1,8 +1,8 @@
-{% load ams_filters %}
+{% load ams_filters tz %}{% timezone meeting.time_zone %}
 The {{ group.name }} ({{ group.acronym }}) {% if not meeting.city %}virtual {% endif %}{% if is_multi_day %}multi-day {% endif %}
 interim meeting for {{ meeting.date|date:"Y-m-d" }} from {{ start_time|time:"H:i" }} to {{ end_time|time:"H:i" }} {{ meeting.time_zone }}
 has been cancelled.
 
 {{ meeting.session_set.0.agenda_note }}
-
+{% endtimezone %}
 
diff --git a/ietf/templates/meeting/interim_request_details.html b/ietf/templates/meeting/interim_request_details.html
index b627fdbb8..89545d14e 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 %}
     <link rel="stylesheet" href="{% static 'ietf/css/select2.css' %}">
 {% endblock %}
-{% block content %}
+{% block content %}{% timezone meeting.tz %}
     {% origin %}
     <h1>Interim Meeting Request Details</h1>
     <dl class="row my-3">
@@ -67,7 +67,7 @@
             <dd class="col-sm-10">
                 {{ 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 %}
             </dd>
             <dt class="col-sm-2">
@@ -151,7 +151,7 @@
         {% endif %}
         {% if can_approve and status_slug == 'apprw' %}</form>{% endif %}
     {% endwith %}
-{% endblock %}
+{% endtimezone %}{% endblock %}
 {% block js %}
     <script src="{% static 'ietf/js/select2.js' %}"></script>
     <script src="{% static 'ietf/js/meeting-interim-request.js' %}"></script>
diff --git a/ietf/templates/meeting/interim_session_buttons.html b/ietf/templates/meeting/interim_session_buttons.html
index b8443851b..3378d743b 100644
--- a/ietf/templates/meeting/interim_session_buttons.html
+++ b/ietf/templates/meeting/interim_session_buttons.html
@@ -1,7 +1,7 @@
 {# Copyright The IETF Trust 2015, All Rights Reserved #}
 {% load origin %}
 {% load static %}
-{% load textfilters %}
+{% load textfilters tz %}
 {% origin %}
 {% with item=session.official_timeslotassignment acronym=session.historic_group.acronym %}
     <div role="group" class="btn-group btn-group-sm">
@@ -90,8 +90,8 @@
             {# iCalendar item #}
             <a class="btn btn-outline-primary"
                href="{% url 'ietf.meeting.views.agenda_ical' num=meeting.number session_id=session.id %}"
-               aria-label="icalendar entry for {{ acronym }} session on {{ item.timeslot.utc_start_time|date:'Y-m-d H:i' }} UTC"
-               title="icalendar entry for {{ acronym }} session on {{ item.timeslot.utc_start_time|date:'Y-m-d H:i' }} UTC">
+               aria-label="icalendar entry for {{ acronym }} session on {{ item.timeslot.time|utc|date:'Y-m-d H:i' }} UTC"
+               title="icalendar entry for {{ acronym }} session on {{ item.timeslot.time|utc|date:'Y-m-d H:i' }} UTC">
                 <i class="bi bi-calendar"></i>
             </a>
         {% else %}
diff --git a/ietf/templates/meeting/interim_session_cancellation_notice.txt b/ietf/templates/meeting/interim_session_cancellation_notice.txt
index 7be4c612b..b7a7321a2 100644
--- a/ietf/templates/meeting/interim_session_cancellation_notice.txt
+++ b/ietf/templates/meeting/interim_session_cancellation_notice.txt
@@ -1,7 +1,7 @@
-{% load ams_filters %}
+{% load ams_filters tz %}{% timezone session.meeting.time_zone %}
 {% if session.name %}The "{{ session.name }}"{% else %}A{% endif %} session of the {{ group.name }} ({{ group.acronym }}) {% if not session.meeting.city %}virtual {% endif %}{% if is_multi_day %}multi-day {% endif %}
-interim meeting has been cancelled. This session had been scheduled for {{ meeting.date|date:"Y-m-d" }} from {{ start_time|time:"H:i" }} to {{ end_time|time:"H:i" }} {{ meeting.time_zone }}.
+interim meeting has been cancelled. This session had been scheduled for {{ session.meeting.date|date:"Y-m-d" }} from {{ start_time|time:"H:i" }} to {{ end_time|time:"H:i" }} {{ session.meeting.time_zone }}.
 
 {{ session.agenda_note }}
-
+{% endtimezone %}
 
diff --git a/ietf/templates/meeting/propose_session_slides.html b/ietf/templates/meeting/propose_session_slides.html
index 8315c2deb..caa3a49af 100644
--- a/ietf/templates/meeting/propose_session_slides.html
+++ b/ietf/templates/meeting/propose_session_slides.html
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 {# Copyright The IETF Trust 2015, All Rights Reserved #}
-{% load origin static django_bootstrap5 %}
+{% load origin static django_bootstrap5 tz %}
 {% block title %}Propose Slides for {{ session.meeting }} : {{ session.group.acronym }}{% endblock %}
 {% block content %}
     {% origin %}
@@ -13,7 +13,7 @@
     </h1>
     {% if session_number %}
         <h2 class="mt-3">
-            Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi" }}
+            Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }}
         </h2>
     {% endif %}
     <p class="alert alert-info my-3">
diff --git a/ietf/templates/meeting/session_details.html b/ietf/templates/meeting/session_details.html
index 2ecb7e1d5..c1b9f31fc 100644
--- a/ietf/templates/meeting/session_details.html
+++ b/ietf/templates/meeting/session_details.html
@@ -14,7 +14,7 @@
         <br>
         <small class="text-muted">{{ acronym }}</small>
     </h1>
-    {% if meeting.date >= thisweek %}
+    {% if meeting.start_datetime >= thisweek %}
         <a class="regular float-end"
            title="icalendar entry for {{ acronym }}@{{ meeting.number }}"
            aria-label="icalendar entry for {{ acronym }}@{{ meeting.number }}"
diff --git a/ietf/templates/meeting/session_details_panel.html b/ietf/templates/meeting/session_details_panel.html
index d481955d0..c5c51af78 100644
--- a/ietf/templates/meeting/session_details_panel.html
+++ b/ietf/templates/meeting/session_details_panel.html
@@ -6,7 +6,7 @@
             {% if sessions|length > 1 %}Session {{ forloop.counter }} :{% endif %}
             {% for time in session.times %}
                 {% if not forloop.first %},{% endif %}
-                {{ time|dateformat:"l Y-m-d H:i T" }}
+                {{ time|timezone:session.meeting.time_zone|dateformat:"l Y-m-d H:i T" }}
                 {% if time.tzinfo.zone != "UTC" %}<span class="small">({{ time|utc|dateformat:"H:i T" }})</span>{% endif %}
             {% endfor %}
             {% if session.cancelled %}
diff --git a/ietf/templates/meeting/upcoming.html b/ietf/templates/meeting/upcoming.html
index f3359f4fa..4fe5faac5 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 %}
     <link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
     <link rel="stylesheet" href="{% static "ietf/js/fullcalendar.css" %}">
@@ -67,9 +67,9 @@
                         {% elif entry|classname == 'Session' %}
                             {% with session=entry group=entry.group meeting=entry.meeting %}
                                 <td class="session-time"
-                                    data-start-utc="{{ session.official_timeslotassignment.timeslot.utc_start_time | date:'Y-m-d H:i' }}Z"
-                                    data-end-utc="{{ session.official_timeslotassignment.timeslot.utc_end_time | date:'Y-m-d H:i' }}Z">
-                                    {{ session.official_timeslotassignment.timeslot.utc_start_time | date:"Y-m-d H:i" }}-{{ session.official_timeslotassignment.timeslot.utc_end_time | date:"H:i" }}
+                                    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" }}
                                 </td>
                                 <td>
                                     <a href="{% url 'ietf.group.views.group_home' acronym=group.acronym %}">{{ group.acronym }}</a>
@@ -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/templates/meeting/upload_session_agenda.html b/ietf/templates/meeting/upload_session_agenda.html
index 4bd0b8971..67a6bf79b 100644
--- a/ietf/templates/meeting/upload_session_agenda.html
+++ b/ietf/templates/meeting/upload_session_agenda.html
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 {# Copyright The IETF Trust 2015, All Rights Reserved #}
-{% load origin static django_bootstrap5 %}
+{% load origin static django_bootstrap5 tz %}
 {% block title %}
     {% if agenda_sp %}
         Revise
@@ -24,7 +24,7 @@
         </small>
     </h1>
     {% if session_number %}
-        <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi" }}</h2>
+        <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }}</h2>
     {% endif %}
     <form enctype="multipart/form-data" method="post" class="my-3">
         {% csrf_token %}
diff --git a/ietf/templates/meeting/upload_session_bluesheets.html b/ietf/templates/meeting/upload_session_bluesheets.html
index 4a2f484be..c79934652 100644
--- a/ietf/templates/meeting/upload_session_bluesheets.html
+++ b/ietf/templates/meeting/upload_session_bluesheets.html
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 {# Copyright The IETF Trust 2015, All Rights Reserved #}
-{% load origin static django_bootstrap5 %}
+{% load origin static django_bootstrap5 tz %}
 {% block title %}
     {% if bluesheet_sp %}
         Revise
@@ -24,7 +24,7 @@
         </small>
     </h1>
     {% if session_number %}
-        <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi" }}</h2>
+        <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }}</h2>
     {% endif %}
     <form enctype="multipart/form-data" method="post" class="my-3">
         {% csrf_token %}
diff --git a/ietf/templates/meeting/upload_session_minutes.html b/ietf/templates/meeting/upload_session_minutes.html
index 2e3f87524..fa6f7c15e 100644
--- a/ietf/templates/meeting/upload_session_minutes.html
+++ b/ietf/templates/meeting/upload_session_minutes.html
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 {# Copyright The IETF Trust 2015, All Rights Reserved #}
-{% load origin static django_bootstrap5 %}
+{% load origin static django_bootstrap5 tz %}
 {% block title %}
     {% if minutes_sp %}
         Revise
@@ -24,7 +24,7 @@
         </small>
     </h1>
     {% if session_number %}
-        <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi" }}</h2>
+        <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }}</h2>
     {% endif %}
     <form enctype="multipart/form-data" method="post" class="my-3">
         {% csrf_token %}
diff --git a/ietf/templates/meeting/upload_session_slides.html b/ietf/templates/meeting/upload_session_slides.html
index f5cd35d96..61b2422e4 100644
--- a/ietf/templates/meeting/upload_session_slides.html
+++ b/ietf/templates/meeting/upload_session_slides.html
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 {# Copyright The IETF Trust 2015, All Rights Reserved #}
-{% load origin static django_bootstrap5 %}
+{% load origin static django_bootstrap5 tz %}
 {% block title %}
     {% if slides_sp %}
         Revise
@@ -25,7 +25,7 @@
         </small>
     </h1>
     {% if session_number %}
-        <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|date:"D M-d-Y Hi" }}</h2>
+        <h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }}</h2>
     {% endif %}
     {% if slides_sp %}<h3>{{ slides_sp.document.name }}</h3>{% endif %}
     <form class="my-3" enctype="multipart/form-data" method="post">
diff --git a/ietf/templates/person/profile.html b/ietf/templates/person/profile.html
index 6732804aa..3d0b1e067 100644
--- a/ietf/templates/person/profile.html
+++ b/ietf/templates/person/profile.html
@@ -62,7 +62,7 @@
                 </tbody>
             </table>
             {% else %}
-                <p>{{ person.first_name }} has no active roles as of {{ today }}.</p>
+                <p>{{ person.first_name }} has no active roles as of {{ today|date:"Y-m-d" }}.</p>
             {% endif %}
         {% endif %}
         {% if person.personextresource_set.exists %}
@@ -123,7 +123,7 @@
                 </tbody>
             </table>
         {% else %}
-            {{ person.first_name }} has no RFCs as of {{ today }}.
+            {{ person.first_name }} has no RFCs as of {{ today|date:"Y-m-d" }}.
         {% endif %}
         <h2 class="mt-5" id="drafts-{{ forloop.counter }}">
             Active Drafts <small class="text-muted">({{ person.active_drafts|length }})</small>
@@ -137,7 +137,7 @@
                 {% endfor %}
             </ul>
         {% else %}
-            {{ person.first_name }} has no active drafts as of {{ today }}.
+            {{ person.first_name }} has no active drafts as of {{ today|date:"Y-m-d" }}.
         {% endif %}
         <h2 class="mt-5">
             Expired Drafts <small class="text-muted">({{ person.expired_drafts|length }})</small>
@@ -156,7 +156,7 @@
             </ul>
             (Excluding replaced drafts.)
         {% else %}
-            {{ person.first_name }} has no expired drafts as of {{ today }}.
+            {{ person.first_name }} has no expired drafts as of {{ today|date:"Y-m-d" }}.
         {% endif %}
         {% if person.has_drafts %}
             <h2 class="mt-5">
diff --git a/ietf/utils/decorators.py b/ietf/utils/decorators.py
index 694e989f8..634724880 100644
--- a/ietf/utils/decorators.py
+++ b/ietf/utils/decorators.py
@@ -10,6 +10,7 @@ from django.conf import settings
 from django.contrib.auth import login
 from django.http import HttpResponse
 from django.shortcuts import render
+from django.utils import timezone
 from django.utils.encoding import force_bytes
 
 import debug                            # pyflakes:ignore
@@ -64,7 +65,7 @@ def require_api_key(f, request, *args, **kwargs):
     person = key.person
     last_login = person.user.last_login
     if not person.user.is_staff:
-        time_limit = (datetime.datetime.now() - datetime.timedelta(days=settings.UTILS_APIKEY_GUI_LOGIN_LIMIT_DAYS))
+        time_limit = (timezone.now() - datetime.timedelta(days=settings.UTILS_APIKEY_GUI_LOGIN_LIMIT_DAYS))
         if last_login == None or last_login < time_limit:
             return err(400, "Too long since last regular login")
     # Log in
@@ -74,7 +75,7 @@ def require_api_key(f, request, *args, **kwargs):
     person.user.save()
     # Update stats
     key.count += 1
-    key.latest = datetime.datetime.now()
+    key.latest = timezone.now()
     key.save()
     PersonApiKeyEvent.objects.create(person=person, type='apikey_login', key=key, desc="Logged in with key ID %s, endpoint %s" % (key.id, key.endpoint))
     # Execute decorated function
diff --git a/ietf/utils/draft.py b/ietf/utils/draft.py
index 12a614701..0a379b0e9 100755
--- a/ietf/utils/draft.py
+++ b/ietf/utils/draft.py
@@ -50,6 +50,9 @@ import time
 
 from typing import Dict, List       # pyflakes:ignore
 
+from .timezone import date_today
+
+
 version = "0.35"
 program = os.path.basename(sys.argv[0])
 progdir = os.path.dirname(sys.argv[0])
@@ -467,7 +470,7 @@ class PlaintextDraft(Draft):
                         month = int(mon)
                     else:
                         continue
-                    today = datetime.date.today()
+                    today = date_today()
                     if day==0:
                         # if the date was given with only month and year, use
                         # today's date if month and year is today's month and
diff --git a/ietf/utils/mail.py b/ietf/utils/mail.py
index 04c0ed916..3500d888e 100644
--- a/ietf/utils/mail.py
+++ b/ietf/utils/mail.py
@@ -3,7 +3,6 @@
 
 
 import copy
-import datetime
 #import logging
 import re
 import smtplib
@@ -27,6 +26,7 @@ from django.core.exceptions import ImproperlyConfigured, ValidationError
 from django.core.validators import validate_email
 from django.template.loader import render_to_string
 from django.template import Context,RequestContext
+from django.utils import timezone
 from django.utils.encoding import force_text, force_str, force_bytes
 
 import debug                            # pyflakes:ignore
@@ -324,7 +324,7 @@ def show_that_mail_was_sent(request,leadline,msg,bcc):
         if request and request.user:
             from ietf.ietfauth.utils import has_role
             if has_role(request.user,['Area Director','Secretariat','IANA','RFC Editor','ISE','IAD','IRTF Chair','WG Chair','RG Chair','WG Secretary','RG Secretary']):
-                info =  "%s at %s %s\n" % (leadline,datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),settings.TIME_ZONE)
+                info =  "%s at %s %s\n" % (leadline,timezone.now().strftime("%Y-%m-%d %H:%M:%S"),settings.TIME_ZONE)
                 info += "Subject: %s\n" % force_text(msg.get('Subject','[no subject]'))
                 info += "To: %s\n" % msg.get('To','[no to]')
                 if msg.get('Cc'):
@@ -378,7 +378,7 @@ def send_mail_mime(request, to, frm, subject, msg, cc=None, extra=None, toUser=F
         try:
             send_smtp(msg, bcc)
             if save:
-                message.sent = datetime.datetime.now()
+                message.sent = timezone.now()
                 message.save()
             if settings.SERVER_MODE != 'development':
                 show_that_mail_was_sent(request,'Email was sent',msg,bcc)
@@ -505,7 +505,7 @@ def send_mail_message(request, message, extra=None):
 
 #     msg = send_mail_text(request, message.to, message.frm, message.subject,
 #                           message.body, cc=message.cc, bcc=message.bcc, extra=e, save=False)
-    message.sent = datetime.datetime.now()
+    message.sent = timezone.now()
     message.save()
     return msg
 
diff --git a/ietf/utils/management/commands/check_draft_event_revision_integrity.py b/ietf/utils/management/commands/check_draft_event_revision_integrity.py
index a4c38bd1c..c8d2cbd21 100644
--- a/ietf/utils/management/commands/check_draft_event_revision_integrity.py
+++ b/ietf/utils/management/commands/check_draft_event_revision_integrity.py
@@ -13,6 +13,7 @@ import django
 django.setup()
 
 from django.core.management.base import BaseCommand #, CommandError
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -29,7 +30,7 @@ class Command(BaseCommand):
     """
 
     def add_arguments(self, parser):
-        default_start = datetime.datetime.now() - datetime.timedelta(days=60)
+        default_start = timezone.now() - datetime.timedelta(days=60)
         parser.add_argument(
             '-d', '--from', type=str, default=default_start.strftime('%Y-%m-%d'),
             help='Limit the list to messages saved after the given date (default %(default)s).',
diff --git a/ietf/utils/management/commands/fix_ambiguous_timestamps.py b/ietf/utils/management/commands/fix_ambiguous_timestamps.py
index fc872f458..84a82bb30 100644
--- a/ietf/utils/management/commands/fix_ambiguous_timestamps.py
+++ b/ietf/utils/management/commands/fix_ambiguous_timestamps.py
@@ -10,6 +10,7 @@ from django.apps import apps
 from django.conf import settings
 from django.core.management.base import BaseCommand
 from django.db import models
+from django.utils import timezone
 
 import debug                            # pyflakes:ignore
 
@@ -53,7 +54,7 @@ class Command(BaseCommand):
     def handle(self, *app_labels, **options):
         self.verbosity = options['verbosity']
         self.quiet = self.verbosity < 1
-        stop = datetime.datetime.now()
+        stop = timezone.now()
         start = stop - datetime.timedelta(days=14)
 
         for name, appconf in apps.app_configs.items():
diff --git a/ietf/utils/management/commands/send_gdpr_consent_request.py b/ietf/utils/management/commands/send_gdpr_consent_request.py
new file mode 100644
index 000000000..152fc83fe
--- /dev/null
+++ b/ietf/utils/management/commands/send_gdpr_consent_request.py
@@ -0,0 +1,104 @@
+# Copyright The IETF Trust 2018-2020, All Rights Reserved
+# -*- coding: utf-8 -*-
+
+
+import datetime
+import time
+
+from django.conf import settings
+from django.core.management.base import BaseCommand, CommandError
+from django.utils import timezone
+
+import debug                            # pyflakes:ignore
+
+from ietf.person.models import Person, PersonEvent
+from ietf.utils.mail import send_mail
+
+class Command(BaseCommand):
+    help = ("""
+        Send GDPR consent request emails to persons who have not indicated consent
+        to having their personal information stored.  Each send is logged as a
+        PersonEvent.
+
+        By default email sending happens at a rate of 1 message per second; the
+        rate can be adjusted with the -r option.  At the start of a run, an estimate
+        is given of how many persons to send to, and  how long the run will take.
+
+        By default, emails are not sent out if there is less than 6 days since the
+        previous consent request email.  The interval can be adjusted with the -m
+        option.  One effect of this is that it is possible to break of a run and
+        re-start it with for instance a different rate, without having duplicate
+        messages go out to persons that were handled in the interrupted run.
+        """)
+
+    def add_arguments(self, parser):
+        parser.add_argument('-n', '--dry-run', action='store_true', default=False,
+            help="Don't send email, just list recipients")
+        parser.add_argument('-d', '--date', help="Date of deletion (mentioned in message)")
+        parser.add_argument('-m', '--minimum-interval', type=int, default=6,
+            help="Minimum interval between re-sending email messages, default: %(default)s days")
+        parser.add_argument('-r', '--rate', type=float, default=1.0,
+            help='Rate of sending mail, default: %(default)s/s')
+        parser.add_argument('-R', '--reminder', action='store_true', default=False,
+            help='Preface the subject with "Reminder:"')
+        parser.add_argument('user', nargs='*')
+         
+
+    def handle(self, *args, **options):
+        # Don't send copies of the whole bulk mailing to the debug mailbox
+        if settings.SERVER_MODE == 'production':
+            settings.EMAIL_COPY_TO = "Email Debug Copy <outbound@ietf.org>"
+        #
+        event_type = 'gdpr_notice_email'
+        # Arguments
+        # --date
+        if 'date' in options and options['date'] != None:
+            try:
+                date = datetime.datetime.strptime(options['date'], "%Y-%m-%d").date()
+            except ValueError as e:
+                raise CommandError('%s' % e)
+        else:
+            date = datetime.date.today() + datetime.timedelta(days=30)
+        days = (date - datetime.date.today()).days
+        if days <= 1:
+            raise CommandError('date must be more than 1 day in the future')
+        # --rate
+        delay = 1.0/options['rate']
+        # --minimum_interval
+        minimum_interval = options['minimum_interval']
+        latest_previous = timezone.now() - datetime.timedelta(days=minimum_interval)
+        # user
+        self.stdout.write('Querying the database for matching person records ...')
+        if 'user' in options and options['user']:
+            persons = Person.objects.filter(user__username__in=options['user'])
+        else:
+            exclude = PersonEvent.objects.filter(time__gt=latest_previous, type=event_type)
+            persons = Person.objects.exclude(consent=True).exclude(personevent__in=exclude)
+        # Report the size of the run
+        runtime = persons.count() * delay
+        self.stdout.write('Sending to %d users; estimated time a bit more than %d:%02d hours' % (persons.count(), runtime//3600, runtime%3600//60))
+        subject='Personal Information in the IETF Datatracker'
+        if options['reminder']:
+            subject = "Reminder: " + subject
+        for person in persons:
+            fields = ', '.join(person.needs_consent())
+            if fields and person.email_set.exists():
+                if options['dry_run']:
+                    print(("%-32s %-32s %-32s %-32s %s" % (person.email(), person.name_from_draft or '', person.name, person.ascii, fields)).encode('utf8'))
+                else:
+                    to = [ e.address for e in person.email_set.filter(active=True) ] # pyflakes:ignore
+                    if not to:
+                        to = [ e.address for e in person.email_set.all() ] # pyflakes:ignore
+                    self.stdout.write("Sendimg email to %s" % to)
+                    send_mail(None, to, "<gdprnoreply@ietf.org>",
+                        subject=subject,
+                        template='utils/personal_information_notice.txt',
+                        context={
+                            'date': date, 'days': days, 'fields': fields,
+                            'person': person, 'settings': settings,
+                            },
+                        )
+                    PersonEvent.objects.create(person=person, type='gdpr_notice_email', 
+                                               desc="Sent GDPR notice email to %s with confirmation deadline %s" % (to, date))
+                    time.sleep(delay)
+                
diff --git a/ietf/utils/migrations/0002_convert_timestamps_to_utc.py b/ietf/utils/migrations/0002_convert_timestamps_to_utc.py
new file mode 100644
index 000000000..320992ae9
--- /dev/null
+++ b/ietf/utils/migrations/0002_convert_timestamps_to_utc.py
@@ -0,0 +1,305 @@
+# Generated by Django 2.2.28 on 2022-06-21 11:44
+#
+# Important: To avoid corrupting timestamps in the database, do not use this migration as a dependency for
+# future migrations. Use 0003_pause_to_change_use_tz instead.
+#
+import datetime
+
+from zoneinfo import ZoneInfo
+
+from django.conf import settings
+from django.db import migrations, connection
+
+# to generate the expected columns list:
+#
+# from django.db import connection
+# from pprint import pp
+# cursor = connection.cursor()
+# cursor.execute("""
+# SELECT table_name, column_name
+#     FROM information_schema.columns
+#         WHERE table_schema='ietf_utf8'
+#             AND column_type LIKE 'datetime%'
+#             AND NOT table_name LIKE 'django_celery_beat_%'
+#         ORDER BY table_name, column_name;
+# """)
+# pp(cursor.fetchall())
+#
+expected_datetime_columns = (
+    ('auth_user', 'date_joined'),
+    ('auth_user', 'last_login'),
+    ('community_documentchangedates', 'new_version_date'),
+    ('community_documentchangedates', 'normal_change_date'),
+    ('community_documentchangedates', 'significant_change_date'),
+    ('django_admin_log', 'action_time'),
+    ('django_migrations', 'applied'),
+    ('django_session', 'expire_date'),
+    ('doc_ballotpositiondocevent', 'comment_time'),
+    ('doc_ballotpositiondocevent', 'discuss_time'),
+    ('doc_deletedevent', 'time'),
+    ('doc_docevent', 'time'),
+    ('doc_dochistory', 'expires'),
+    ('doc_dochistory', 'time'),
+    ('doc_docreminder', 'due'),
+    ('doc_document', 'expires'),
+    ('doc_document', 'time'),
+    ('doc_documentactionholder', 'time_added'),
+    ('doc_initialreviewdocevent', 'expires'),
+    ('doc_irsgballotdocevent', 'duedate'),
+    ('doc_lastcalldocevent', 'expires'),
+    ('group_group', 'time'),
+    ('group_groupevent', 'time'),
+    ('group_grouphistory', 'time'),
+    ('group_groupmilestone', 'time'),
+    ('group_groupmilestonehistory', 'time'),
+    ('ipr_iprdisclosurebase', 'time'),
+    ('ipr_iprevent', 'response_due'),
+    ('ipr_iprevent', 'time'),
+    ('liaisons_liaisonstatementevent', 'time'),
+    ('mailinglists_subscribed', 'time'),
+    ('mailinglists_whitelisted', 'time'),
+    ('meeting_floorplan', 'modified'),
+    ('meeting_room', 'modified'),
+    ('meeting_schedtimesessassignment', 'modified'),
+    ('meeting_schedulingevent', 'time'),
+    ('meeting_session', 'modified'),
+    ('meeting_session', 'scheduled'),
+    ('meeting_slidesubmission', 'time'),
+    ('meeting_timeslot', 'modified'),
+    ('meeting_timeslot', 'time'),
+    ('message_message', 'sent'),
+    ('message_message', 'time'),
+    ('message_sendqueue', 'send_at'),
+    ('message_sendqueue', 'sent_at'),
+    ('message_sendqueue', 'time'),
+    ('nomcom_feedback', 'time'),
+    ('nomcom_feedbacklastseen', 'time'),
+    ('nomcom_nomination', 'time'),
+    ('nomcom_nomineeposition', 'time'),
+    ('nomcom_topicfeedbacklastseen', 'time'),
+    ('oidc_provider_code', 'expires_at'),
+    ('oidc_provider_token', 'expires_at'),
+    ('oidc_provider_userconsent', 'date_given'),
+    ('oidc_provider_userconsent', 'expires_at'),
+    ('person_email', 'time'),
+    ('person_historicalemail', 'history_date'),
+    ('person_historicalemail', 'time'),
+    ('person_historicalperson', 'history_date'),
+    ('person_historicalperson', 'time'),
+    ('person_person', 'time'),
+    ('person_personalapikey', 'created'),
+    ('person_personalapikey', 'latest'),
+    ('person_personevent', 'time'),
+    ('request_profiler_profilingrecord', 'end_ts'),
+    ('request_profiler_profilingrecord', 'start_ts'),
+    ('review_historicalreviewassignment', 'assigned_on'),
+    ('review_historicalreviewassignment', 'completed_on'),
+    ('review_historicalreviewassignment', 'history_date'),
+    ('review_historicalreviewersettings', 'history_date'),
+    ('review_historicalreviewrequest', 'history_date'),
+    ('review_historicalreviewrequest', 'time'),
+    ('review_historicalunavailableperiod', 'history_date'),
+    ('review_reviewassignment', 'assigned_on'),
+    ('review_reviewassignment', 'completed_on'),
+    ('review_reviewrequest', 'time'),
+    ('review_reviewwish', 'time'),
+    ('south_migrationhistory', 'applied'),
+    ('submit_preapproval', 'time'),
+    ('submit_submissioncheck', 'time'),
+    ('submit_submissionevent', 'time'),
+    ('tastypie_apikey', 'created'),
+    ('utils_versioninfo', 'time'),
+)
+
+def convert_pre1970_timestamps(apps, schema_editor):
+    """Convert timestamps that CONVERT_TZ cannot handle
+
+    This could be made to do the entire conversion but some tables that require converison
+    do not use 'id' as their PK. Rather than reinvent the ORM, we'll let SQL do what it can
+    with CONVERT_TZ and clean up after. The tables that have pre-1970 timestamps both have
+    'id' columns.
+    """
+    min_timestamp = "1969-12-31 16:00:01"  # minimum PST8PDT timestamp CONVERT_TZ can convert to UTC
+    with connection.cursor() as cursor:
+        # To get these values, use:
+        # convert_manually = [
+        #     (tbl, col) for (tbl, col) in expected_datetime_columns
+        #     if cursor.execute(
+        #         f'SELECT COUNT(*) FROM {tbl} WHERE {col} IS NOT NULL AND {col} <= %s',
+        #         (min_timestamp,)
+        #     ) and cursor.fetchone()[0] > 0
+        # ]
+        convert_manually = [('doc_docevent', 'time'), ('group_groupevent', 'time')]
+        pst8pdt = ZoneInfo('PST8PDT')
+        for (tbl, col) in convert_manually:
+            cursor.execute(f'SELECT id, {col} FROM {tbl} WHERE {col} < %s', (min_timestamp,))
+            for (id, naive_in_pst8pdt) in cursor.fetchall():
+                aware_in_pst8pdt = naive_in_pst8pdt.replace(tzinfo=pst8pdt)
+                aware_in_utc = aware_in_pst8pdt.astimezone(datetime.timezone.utc)
+                naive_in_utc = aware_in_utc.replace(tzinfo=None)
+                cursor.execute(
+                    f'UPDATE {tbl} SET {col}=%s WHERE id=%s',
+                    (naive_in_utc, id)
+                )
+
+
+def forward(apps, schema_editor):
+    # Check that the USE_TZ has been False so far, otherwise we might be corrupting timestamps. If this test
+    # fails, be sure that no timestamps have been set since changing USE_TZ to True before re-running!
+    assert not getattr(settings, 'USE_TZ', False), 'must keep USE_TZ = False until after this migration'
+
+    # Check that we can safely ignore celery beat columns - it defaults to UTC if CELERY_TIMEZONE is not set.
+    celery_timezone = getattr(settings, 'CELERY_TIMEZONE', None)
+    assert celery_timezone in ('UTC', None), 'update migration, celery is not using UTC'
+    # If the CELERY_ENABLE_UTC flag is set, abort because someone is using a strange configuration.
+    assert not hasattr(settings, 'CELERY_ENABLE_UTC'), 'update migration, settings.CELERY_ENABLE_UTC was not expected'
+
+    with connection.cursor() as cursor:
+        # Check that we have timezones.
+        # If these assertions fail, the DB does not know all the necessary time zones.
+        # To load timezones,
+        #   $ mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql
+        # (on a dev system, first connect to the db image with `docker compose exec db bash`)
+        cursor.execute("SELECT CONVERT_TZ('2022-06-22T17:43:00', 'PST8PDT', 'UTC');")
+        assert not any(None in row for row in cursor.fetchall()), 'database does not recognize PST8PDT'
+        cursor.execute(
+            "SELECT CONVERT_TZ('2022-06-22T17:43:00', time_zone, 'UTC') FROM meeting_meeting WHERE time_zone != '';"
+        )
+        assert not any(None in row for row in cursor.fetchall()), 'database does not recognize a meeting time zone'
+
+        # Check that we have all and only the expected datetime columns to work with.
+        # If this fails, figure out what changed and decide how to proceed safely.
+        cursor.execute("""
+        SELECT table_name, column_name 
+            FROM information_schema.columns 
+                WHERE table_schema='ietf_utf8' 
+                    AND column_type LIKE 'datetime%'
+                    AND NOT table_name LIKE 'django_celery_beat_%' 
+                    AND NOT table_name='utils_dumpinfo'
+                ORDER BY table_name, column_name;
+        """)
+        assert cursor.fetchall() == expected_datetime_columns, 'unexpected or missing datetime columns in db'
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('doc', '0046_use_timezone_now_for_doc_models'),
+        ('group', '0059_use_timezone_now_for_group_models'),
+        ('meeting', '0058_meeting_time_zone_not_blank'),
+        ('message', '0012_use_timezone_now_for_message_models'),
+        ('person', '0026_use_timezone_now_for_person_models'),
+        ('review', '0029_use_timezone_now_for_review_models'),
+        ('submit', '0011_use_timezone_now_for_submit_models'),
+        ('utils', '0001_initial'),
+    ]
+
+    # To generate the queries:
+    #
+    # min_timestamp = "1969-12-31 16:00:01"  # minimum PST8PDT timestamp CONVERT_TZ can convert to UTC
+    # pst8pdt_columns = [e for e in expected_datetime_columns if e != ('meeting_timeslot', 'time')]
+    # queries = []
+    # for table, column in pst8pdt_columns:
+    #     queries.append(f"UPDATE {table} SET {column} = CONVERT_TZ({column}, 'PST8PDT', 'UTC') WHERE {column} >= '{min_timestamp}'";)
+    #
+    # queries.append("""
+    # UPDATE meeting_timeslot
+    #   JOIN meeting_meeting
+    #     ON meeting_meeting.id = meeting_id
+    #   SET time = CONVERT_TZ(time, time_zone, 'UTC');
+    # """)
+    #
+    # print("\n".join(queries))
+    #
+    operations = [
+        migrations.RunPython(forward),
+        migrations.RunSQL("""
+UPDATE auth_user SET date_joined = CONVERT_TZ(date_joined, 'PST8PDT', 'UTC') WHERE date_joined >= '1969-12-31 16:00:01';
+UPDATE auth_user SET last_login = CONVERT_TZ(last_login, 'PST8PDT', 'UTC') WHERE last_login >= '1969-12-31 16:00:01';
+UPDATE community_documentchangedates SET new_version_date = CONVERT_TZ(new_version_date, 'PST8PDT', 'UTC') WHERE new_version_date >= '1969-12-31 16:00:01';
+UPDATE community_documentchangedates SET normal_change_date = CONVERT_TZ(normal_change_date, 'PST8PDT', 'UTC') WHERE normal_change_date >= '1969-12-31 16:00:01';
+UPDATE community_documentchangedates SET significant_change_date = CONVERT_TZ(significant_change_date, 'PST8PDT', 'UTC') WHERE significant_change_date >= '1969-12-31 16:00:01';
+UPDATE django_admin_log SET action_time = CONVERT_TZ(action_time, 'PST8PDT', 'UTC') WHERE action_time >= '1969-12-31 16:00:01';
+UPDATE django_migrations SET applied = CONVERT_TZ(applied, 'PST8PDT', 'UTC') WHERE applied >= '1969-12-31 16:00:01';
+UPDATE django_session SET expire_date = CONVERT_TZ(expire_date, 'PST8PDT', 'UTC') WHERE expire_date >= '1969-12-31 16:00:01';
+UPDATE doc_ballotpositiondocevent SET comment_time = CONVERT_TZ(comment_time, 'PST8PDT', 'UTC') WHERE comment_time >= '1969-12-31 16:00:01';
+UPDATE doc_ballotpositiondocevent SET discuss_time = CONVERT_TZ(discuss_time, 'PST8PDT', 'UTC') WHERE discuss_time >= '1969-12-31 16:00:01';
+UPDATE doc_deletedevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE doc_docevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE doc_dochistory SET expires = CONVERT_TZ(expires, 'PST8PDT', 'UTC') WHERE expires >= '1969-12-31 16:00:01';
+UPDATE doc_dochistory SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE doc_docreminder SET due = CONVERT_TZ(due, 'PST8PDT', 'UTC') WHERE due >= '1969-12-31 16:00:01';
+UPDATE doc_document SET expires = CONVERT_TZ(expires, 'PST8PDT', 'UTC') WHERE expires >= '1969-12-31 16:00:01';
+UPDATE doc_document SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE doc_documentactionholder SET time_added = CONVERT_TZ(time_added, 'PST8PDT', 'UTC') WHERE time_added >= '1969-12-31 16:00:01';
+UPDATE doc_initialreviewdocevent SET expires = CONVERT_TZ(expires, 'PST8PDT', 'UTC') WHERE expires >= '1969-12-31 16:00:01';
+UPDATE doc_irsgballotdocevent SET duedate = CONVERT_TZ(duedate, 'PST8PDT', 'UTC') WHERE duedate >= '1969-12-31 16:00:01';
+UPDATE doc_lastcalldocevent SET expires = CONVERT_TZ(expires, 'PST8PDT', 'UTC') WHERE expires >= '1969-12-31 16:00:01';
+UPDATE group_group SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE group_groupevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE group_grouphistory SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE group_groupmilestone SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE group_groupmilestonehistory SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE ipr_iprdisclosurebase SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE ipr_iprevent SET response_due = CONVERT_TZ(response_due, 'PST8PDT', 'UTC') WHERE response_due >= '1969-12-31 16:00:01';
+UPDATE ipr_iprevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE liaisons_liaisonstatementevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE mailinglists_subscribed SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE mailinglists_whitelisted SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE meeting_floorplan SET modified = CONVERT_TZ(modified, 'PST8PDT', 'UTC') WHERE modified >= '1969-12-31 16:00:01';
+UPDATE meeting_room SET modified = CONVERT_TZ(modified, 'PST8PDT', 'UTC') WHERE modified >= '1969-12-31 16:00:01';
+UPDATE meeting_schedtimesessassignment SET modified = CONVERT_TZ(modified, 'PST8PDT', 'UTC') WHERE modified >= '1969-12-31 16:00:01';
+UPDATE meeting_schedulingevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE meeting_session SET modified = CONVERT_TZ(modified, 'PST8PDT', 'UTC') WHERE modified >= '1969-12-31 16:00:01';
+UPDATE meeting_session SET scheduled = CONVERT_TZ(scheduled, 'PST8PDT', 'UTC') WHERE scheduled >= '1969-12-31 16:00:01';
+UPDATE meeting_slidesubmission SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE meeting_timeslot SET modified = CONVERT_TZ(modified, 'PST8PDT', 'UTC') WHERE modified >= '1969-12-31 16:00:01';
+UPDATE message_message SET sent = CONVERT_TZ(sent, 'PST8PDT', 'UTC') WHERE sent >= '1969-12-31 16:00:01';
+UPDATE message_message SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE message_sendqueue SET send_at = CONVERT_TZ(send_at, 'PST8PDT', 'UTC') WHERE send_at >= '1969-12-31 16:00:01';
+UPDATE message_sendqueue SET sent_at = CONVERT_TZ(sent_at, 'PST8PDT', 'UTC') WHERE sent_at >= '1969-12-31 16:00:01';
+UPDATE message_sendqueue SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE nomcom_feedback SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE nomcom_feedbacklastseen SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE nomcom_nomination SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE nomcom_nomineeposition SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE nomcom_topicfeedbacklastseen SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE oidc_provider_code SET expires_at = CONVERT_TZ(expires_at, 'PST8PDT', 'UTC') WHERE expires_at >= '1969-12-31 16:00:01';
+UPDATE oidc_provider_token SET expires_at = CONVERT_TZ(expires_at, 'PST8PDT', 'UTC') WHERE expires_at >= '1969-12-31 16:00:01';
+UPDATE oidc_provider_userconsent SET date_given = CONVERT_TZ(date_given, 'PST8PDT', 'UTC') WHERE date_given >= '1969-12-31 16:00:01';
+UPDATE oidc_provider_userconsent SET expires_at = CONVERT_TZ(expires_at, 'PST8PDT', 'UTC') WHERE expires_at >= '1969-12-31 16:00:01';
+UPDATE person_email SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE person_historicalemail SET history_date = CONVERT_TZ(history_date, 'PST8PDT', 'UTC') WHERE history_date >= '1969-12-31 16:00:01';
+UPDATE person_historicalemail SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE person_historicalperson SET history_date = CONVERT_TZ(history_date, 'PST8PDT', 'UTC') WHERE history_date >= '1969-12-31 16:00:01';
+UPDATE person_historicalperson SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE person_person SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE person_personalapikey SET created = CONVERT_TZ(created, 'PST8PDT', 'UTC') WHERE created >= '1969-12-31 16:00:01';
+UPDATE person_personalapikey SET latest = CONVERT_TZ(latest, 'PST8PDT', 'UTC') WHERE latest >= '1969-12-31 16:00:01';
+UPDATE person_personevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE request_profiler_profilingrecord SET end_ts = CONVERT_TZ(end_ts, 'PST8PDT', 'UTC') WHERE end_ts >= '1969-12-31 16:00:01';
+UPDATE request_profiler_profilingrecord SET start_ts = CONVERT_TZ(start_ts, 'PST8PDT', 'UTC') WHERE start_ts >= '1969-12-31 16:00:01';
+UPDATE review_historicalreviewassignment SET assigned_on = CONVERT_TZ(assigned_on, 'PST8PDT', 'UTC') WHERE assigned_on >= '1969-12-31 16:00:01';
+UPDATE review_historicalreviewassignment SET completed_on = CONVERT_TZ(completed_on, 'PST8PDT', 'UTC') WHERE completed_on >= '1969-12-31 16:00:01';
+UPDATE review_historicalreviewassignment SET history_date = CONVERT_TZ(history_date, 'PST8PDT', 'UTC') WHERE history_date >= '1969-12-31 16:00:01';
+UPDATE review_historicalreviewersettings SET history_date = CONVERT_TZ(history_date, 'PST8PDT', 'UTC') WHERE history_date >= '1969-12-31 16:00:01';
+UPDATE review_historicalreviewrequest SET history_date = CONVERT_TZ(history_date, 'PST8PDT', 'UTC') WHERE history_date >= '1969-12-31 16:00:01';
+UPDATE review_historicalreviewrequest SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE review_historicalunavailableperiod SET history_date = CONVERT_TZ(history_date, 'PST8PDT', 'UTC') WHERE history_date >= '1969-12-31 16:00:01';
+UPDATE review_reviewassignment SET assigned_on = CONVERT_TZ(assigned_on, 'PST8PDT', 'UTC') WHERE assigned_on >= '1969-12-31 16:00:01';
+UPDATE review_reviewassignment SET completed_on = CONVERT_TZ(completed_on, 'PST8PDT', 'UTC') WHERE completed_on >= '1969-12-31 16:00:01';
+UPDATE review_reviewrequest SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE review_reviewwish SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE south_migrationhistory SET applied = CONVERT_TZ(applied, 'PST8PDT', 'UTC') WHERE applied >= '1969-12-31 16:00:01';
+UPDATE submit_preapproval SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE submit_submissioncheck SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE submit_submissionevent SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+UPDATE tastypie_apikey SET created = CONVERT_TZ(created, 'PST8PDT', 'UTC') WHERE created >= '1969-12-31 16:00:01';
+UPDATE utils_versioninfo SET time = CONVERT_TZ(time, 'PST8PDT', 'UTC') WHERE time >= '1969-12-31 16:00:01';
+
+UPDATE meeting_timeslot
+  JOIN meeting_meeting
+    ON meeting_meeting.id = meeting_id
+  SET time = CONVERT_TZ(time, time_zone, 'UTC');
+"""),
+        migrations.RunPython(convert_pre1970_timestamps),
+    ]
diff --git a/ietf/utils/migrations/0003_pause_to_change_use_tz.py b/ietf/utils/migrations/0003_pause_to_change_use_tz.py
new file mode 100644
index 000000000..e2719e241
--- /dev/null
+++ b/ietf/utils/migrations/0003_pause_to_change_use_tz.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.2.28 on 2022-08-29 10:16
+
+from django.conf import settings
+from django.db import migrations
+
+
+def forward(apps, schema_editor):
+    assert getattr(settings, 'USE_TZ', False), 'Please change USE_TZ to True before continuing.'
+
+
+def reverse(apps, schema_editor):
+    assert not getattr(settings, 'USE_TZ', False), 'Please change USE_TZ to False before continuing.'
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('utils', '0002_convert_timestamps_to_utc'),
+    ]
+
+    operations = [
+        migrations.RunPython(forward, reverse),
+    ]
diff --git a/ietf/utils/serialize.py b/ietf/utils/serialize.py
index 64254300a..342d211cf 100644
--- a/ietf/utils/serialize.py
+++ b/ietf/utils/serialize.py
@@ -1,3 +1,5 @@
+import datetime
+
 from django.db import models
 
 def object_as_shallow_dict(obj):
@@ -14,7 +16,7 @@ def object_as_shallow_dict(obj):
         if isinstance(f, models.ManyToManyField):
             v = list(v.values_list("pk", flat=True))
         elif isinstance(f, models.DateTimeField):
-            v = v.strftime('%Y-%m-%d %H:%M:%S')
+            v = v.astimezone(datetime.timezone.utc).isoformat()
         elif isinstance(f, models.DateField):
             v = v.strftime('%Y-%m-%d')
 
diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py
index 7a7a77af4..2c6f048a5 100644
--- a/ietf/utils/test_data.py
+++ b/ietf/utils/test_data.py
@@ -6,6 +6,7 @@ import datetime
 
 from django.conf import settings
 from django.contrib.auth.models import User
+from django.utils import timezone
 from django.utils.encoding import smart_text
 
 import debug                            # pyflakes:ignore
@@ -20,6 +21,7 @@ from ietf.person.models import Person, Email
 from ietf.group.utils import setup_default_community_list_for_group
 from ietf.review.models import (ReviewRequest, ReviewerSettings, ReviewResultName, ReviewTypeName, ReviewTeamSettings )
 from ietf.person.name import unidecode_name
+from ietf.utils.timezone import date_today
 
 
 def create_person(group, role_name, name=None, username=None, email_address=None, password=None, is_staff=False, is_superuser=False):
@@ -51,7 +53,7 @@ def make_immutable_base_data():
     all tests in a run."""
 
     # telechat dates
-    t = datetime.date.today() + datetime.timedelta(days=1)
+    t = date_today() + datetime.timedelta(days=1)
     old = TelechatDate.objects.create(date=t - datetime.timedelta(days=14)).date        # pyflakes:ignore
     date1 = TelechatDate.objects.create(date=t).date                                    # pyflakes:ignore
     date2 = TelechatDate.objects.create(date=t + datetime.timedelta(days=14)).date      # pyflakes:ignore
@@ -271,14 +273,14 @@ def make_test_data():
     # old draft
     old_draft = Document.objects.create(
         name="draft-foo-mars-test",
-        time=datetime.datetime.now() - datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
+        time=timezone.now() - datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
         type_id="draft",
         title="Optimizing Martian Network Topologies",
         stream_id="ietf",
         abstract="Techniques for achieving near-optimal Martian networks.",
         rev="00",
         pages=2,
-        expires=datetime.datetime.now(),
+        expires=timezone.now(),
         )
     old_draft.set_state(State.objects.get(used=True, type="draft", slug="expired"))
     old_alias = DocAlias.objects.create(name=old_draft.name)
@@ -287,7 +289,7 @@ def make_test_data():
     # draft
     draft = Document.objects.create(
         name="draft-ietf-mars-test",
-        time=datetime.datetime.now(),
+        time=timezone.now(),
         type_id="draft",
         title="Optimizing Martian Network Topologies",
         stream_id="ietf",
@@ -298,7 +300,7 @@ def make_test_data():
         intended_std_level_id="ps",
         shepherd=email,
         ad=ad,
-        expires=datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
+        expires=timezone.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
         notify="aliens@example.mars",
         note="",
         )
@@ -364,7 +366,7 @@ def make_test_data():
     ietf72 = Meeting.objects.create(
         number="72",
         type_id="ietf",
-        date=datetime.date.today() + datetime.timedelta(days=180),
+        date=date_today() + datetime.timedelta(days=180),
         city="New York",
         country="US",
         time_zone="US/Eastern",
@@ -458,7 +460,7 @@ def make_review_data(doc):
         doc=doc,
         team=team1,
         type_id="early",
-        deadline=datetime.datetime.now() + datetime.timedelta(days=20),
+        deadline=timezone.now() + datetime.timedelta(days=20),
         state_id="accepted",
         requested_by=reviewer,
         reviewer=email,
diff --git a/ietf/utils/test_runner.py b/ietf/utils/test_runner.py
index f7790df55..dd492e58a 100644
--- a/ietf/utils/test_runner.py
+++ b/ietf/utils/test_runner.py
@@ -40,10 +40,8 @@ import os
 import sys
 import time
 import json
-import pytz
 import importlib
 import socket
-import datetime
 import gzip
 import unittest
 import pathlib
@@ -76,6 +74,7 @@ from django.core.management import call_command
 from django.urls import URLResolver # type: ignore
 from django.template.backends.django import DjangoTemplates
 from django.template.backends.django import Template  # type: ignore[attr-defined]
+from django.utils import timezone
 # from django.utils.safestring import mark_safe
 
 import debug                            # pyflakes:ignore
@@ -571,7 +570,7 @@ class CoverageTest(unittest.TestCase):
             checker.stop()
             # Save to the .coverage file
             checker.save()
-            # Apply the configured and requested omit and include data 
+            # Apply the configured and requested omit and include data
             checker.config.from_args(ignore_errors=None, omit=settings.TEST_CODE_COVERAGE_EXCLUDE_FILES,
                 include=include, file=None)
             for pattern in settings.TEST_CODE_COVERAGE_EXCLUDE_LINES:
@@ -744,7 +743,7 @@ class IetfTestRunner(DiscoverRunner):
         print("     Datatracker %s test suite, %s:" % (ietf.__version__, time.strftime("%d %B %Y %H:%M:%S %Z")))
         print("     Python %s." % sys.version.replace('\n', ' '))
         print("     Django %s, settings '%s'" % (django.get_version(), settings.SETTINGS_MODULE))
-        
+
         settings.TEMPLATES[0]['BACKEND'] = 'ietf.utils.test_runner.ValidatingTemplates'
         if self.check_coverage:
             if self.coverage_file.endswith('.gz'):
@@ -754,19 +753,19 @@ class IetfTestRunner(DiscoverRunner):
                 with io.open(self.coverage_file, encoding='utf-8') as file:
                     self.coverage_master = json.load(file)
             self.coverage_data = {
-                "time": datetime.datetime.now(pytz.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
+                "time": timezone.now().strftime("%Y-%m-%dT%H:%M:%SZ"),
                 "template": {
-                    "coverage": 0.0, 
+                    "coverage": 0.0,
                     "covered": {},
                     "format": 1,        # default format, coverage data in 'covered' are just fractions
                 },
                 "url": {
-                    "coverage": 0.0, 
+                    "coverage": 0.0,
                     "covered": {},
                     "format": 4,
                 },
                 "code": {
-                    "coverage": 0.0, 
+                    "coverage": 0.0,
                     "covered": {},
                     "format": 1,
                 },
@@ -813,8 +812,8 @@ class IetfTestRunner(DiscoverRunner):
         for offset in range(10):
             try:
                 # remember the value so ietf.utils.mail.send_smtp() will use the same
-                ietf.utils.mail.SMTP_ADDR['port'] = base + offset 
-                self.smtpd_driver = SMTPTestServerDriver((ietf.utils.mail.SMTP_ADDR['ip4'],ietf.utils.mail.SMTP_ADDR['port']),None) 
+                ietf.utils.mail.SMTP_ADDR['port'] = base + offset
+                self.smtpd_driver = SMTPTestServerDriver((ietf.utils.mail.SMTP_ADDR['ip4'],ietf.utils.mail.SMTP_ADDR['port']),None)
                 self.smtpd_driver.start()
                 print(("     Running an SMTP test server on %(ip4)s:%(port)s to catch outgoing email." % ietf.utils.mail.SMTP_ADDR))
                 break
diff --git a/ietf/utils/tests_meetecho.py b/ietf/utils/tests_meetecho.py
index d40e013f8..db3d36f40 100644
--- a/ietf/utils/tests_meetecho.py
+++ b/ietf/utils/tests_meetecho.py
@@ -1,15 +1,16 @@
 # Copyright The IETF Trust 2021, All Rights Reserved
 # -*- coding: utf-8 -*-
 import datetime
+import pytz
 import requests
 import requests_mock
 
-from pytz import timezone, utc
 from unittest.mock import patch
 from urllib.parse import urljoin
 
 from django.conf import settings
 from django.test import override_settings
+from django.utils import timezone
 
 from ietf.utils.tests import TestCase
 from .meetecho import Conference, ConferenceManager, MeetechoAPI, MeetechoAPIError
@@ -92,7 +93,7 @@ class APITests(TestCase):
         api = MeetechoAPI(API_BASE, CLIENT_ID, CLIENT_SECRET)
         api_response = api.schedule_meeting(
             wg_token='my-token',
-            start_time=utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)),
+            start_time=pytz.utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)),
             duration=datetime.timedelta(minutes=130),
             description='interim-2021-wgname-01',
             extrainfo='message for staff',
@@ -120,11 +121,11 @@ class APITests(TestCase):
         )
         # same time in different time zones
         for start_time in [
-            utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)),
-            timezone('america/halifax').localize(datetime.datetime(2021, 9, 14, 7, 0, 0)),
-            timezone('europe/kiev').localize(datetime.datetime(2021, 9, 14, 13, 0, 0)),
-            timezone('pacific/easter').localize(datetime.datetime(2021, 9, 14, 5, 0, 0)),
-            timezone('africa/porto-novo').localize(datetime.datetime(2021, 9, 14, 11, 0, 0)),
+            pytz.utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)),
+            pytz.timezone('america/halifax').localize(datetime.datetime(2021, 9, 14, 7, 0, 0)),
+            pytz.timezone('europe/kiev').localize(datetime.datetime(2021, 9, 14, 13, 0, 0)),
+            pytz.timezone('pacific/easter').localize(datetime.datetime(2021, 9, 14, 5, 0, 0)),
+            pytz.timezone('africa/porto-novo').localize(datetime.datetime(2021, 9, 14, 11, 0, 0)),
         ]:
             self.assertEqual(
                 api_response,
@@ -191,7 +192,7 @@ class APITests(TestCase):
                     '3d55bce0-535e-4ba8-bb8e-734911cf3c32': {
                         'room': {
                             'id': 18,
-                            'start_time': utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)),
+                            'start_time': pytz.utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)),
                             'duration': datetime.timedelta(minutes=130),
                             'description': 'interim-2021-wgname-01',
                         },
@@ -201,7 +202,7 @@ class APITests(TestCase):
                     'e68e96d4-d38f-475b-9073-ecab46ca96a5': {
                         'room': {
                             'id': 23,
-                            'start_time': utc.localize(datetime.datetime(2021, 9, 15, 14, 30, 0)),
+                            'start_time': pytz.utc.localize(datetime.datetime(2021, 9, 15, 14, 30, 0)),
                             'duration': datetime.timedelta(minutes=30),
                             'description': 'interim-2021-wgname-02',
                         },
@@ -249,7 +250,7 @@ class APITests(TestCase):
 
     def test_time_serialization(self):
         """Time de/serialization should be consistent"""
-        time = datetime.datetime.now(utc).replace(microsecond=0)  # cut off to 0 microseconds
+        time = timezone.now().astimezone(pytz.utc).replace(microsecond=0)  # cut off to 0 microseconds
         api = MeetechoAPI(API_BASE, CLIENT_ID, CLIENT_SECRET)
         self.assertEqual(api._deserialize_time(api._serialize_time(time)), time)
 
@@ -263,7 +264,7 @@ class ConferenceManagerTests(TestCase):
                 'session-1-uuid': {
                     'room': {
                         'id': 1,
-                        'start_time': utc.localize(datetime.datetime(2022,2,4,1,2,3)),
+                        'start_time': pytz.utc.localize(datetime.datetime(2022,2,4,1,2,3)),
                         'duration': datetime.timedelta(minutes=45),
                         'description': 'some-description',
                     },
@@ -273,7 +274,7 @@ class ConferenceManagerTests(TestCase):
                 'session-2-uuid': {
                     'room': {
                         'id': 2,
-                        'start_time': utc.localize(datetime.datetime(2022,2,5,4,5,6)),
+                        'start_time': pytz.utc.localize(datetime.datetime(2022,2,5,4,5,6)),
                         'duration': datetime.timedelta(minutes=90),
                         'description': 'another-description',
                     },
@@ -290,7 +291,7 @@ class ConferenceManagerTests(TestCase):
                     id=1,
                     public_id='session-1-uuid',
                     description='some-description',
-                    start_time=utc.localize(datetime.datetime(2022, 2, 4, 1, 2, 3)),
+                    start_time=pytz.utc.localize(datetime.datetime(2022, 2, 4, 1, 2, 3)),
                     duration=datetime.timedelta(minutes=45),
                     url='https://example.com/some/url',
                     deletion_token='delete-me',
@@ -300,7 +301,7 @@ class ConferenceManagerTests(TestCase):
                     id=2,
                     public_id='session-2-uuid',
                     description='another-description',
-                    start_time=utc.localize(datetime.datetime(2022, 2, 5, 4, 5, 6)),
+                    start_time=pytz.utc.localize(datetime.datetime(2022, 2, 5, 4, 5, 6)),
                     duration=datetime.timedelta(minutes=90),
                     url='https://example.com/another/url',
                     deletion_token='delete-me-too',
@@ -316,7 +317,7 @@ class ConferenceManagerTests(TestCase):
                 'session-1-uuid': {
                     'room': {
                         'id': 1,
-                        'start_time': utc.localize(datetime.datetime(2022,2,4,1,2,3)),
+                        'start_time': pytz.utc.localize(datetime.datetime(2022,2,4,1,2,3)),
                         'duration': datetime.timedelta(minutes=45),
                         'description': 'some-description',
                     },
@@ -335,7 +336,7 @@ class ConferenceManagerTests(TestCase):
                 id=1,
                 public_id='session-1-uuid',
                 description='some-description',
-                start_time=utc.localize(datetime.datetime(2022,2,4,1,2,3)),
+                start_time=pytz.utc.localize(datetime.datetime(2022,2,4,1,2,3)),
                 duration=datetime.timedelta(minutes=45),
                 url='https://example.com/some/url',
                 deletion_token='delete-me',
@@ -351,7 +352,7 @@ class ConferenceManagerTests(TestCase):
                 'session-1-uuid': {
                     'room': {
                         'id': 1,
-                        'start_time': utc.localize(datetime.datetime(2022,2,4,1,2,3)),
+                        'start_time': pytz.utc.localize(datetime.datetime(2022,2,4,1,2,3)),
                         'duration': datetime.timedelta(minutes=45),
                         'description': 'some-description',
                     },
@@ -369,7 +370,7 @@ class ConferenceManagerTests(TestCase):
                 id=1,
                 public_id='session-1-uuid',
                 description='some-description',
-                start_time=utc.localize(datetime.datetime(2022,2,4,1,2,3)),
+                start_time=pytz.utc.localize(datetime.datetime(2022,2,4,1,2,3)),
                 duration=datetime.timedelta(minutes=45),
                 url='https://example.com/some/url',
                 deletion_token='delete-me',
diff --git a/ietf/utils/timezone.py b/ietf/utils/timezone.py
index 723512efe..0aeedbeda 100644
--- a/ietf/utils/timezone.py
+++ b/ietf/utils/timezone.py
@@ -1,39 +1,82 @@
-import pytz
-import email.utils
 import datetime
 
+from typing import Union
+from zoneinfo import ZoneInfo
+
 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
-    naive datetime with the corresponding UTC time."""
-    local_timezone = pytz.timezone(settings.TIME_ZONE)
 
-    d = local_timezone.localize(d).astimezone(pytz.utc)
+# Timezone constants - tempting to make these settings, but changing them will
+# require code changes.
+#
+# Default time zone for deadlines / expiration dates.
+DEADLINE_TZINFO = ZoneInfo('PST8PDT')
 
-    return d.replace(tzinfo=None)
+# Time zone for dates from the RPC. This value is baked into the timestamps on DocEvents
+# of type="published_rfc" - see Document.pub_date() and ietf.sync.refceditor.update_docs_from_rfc_index()
+# for more information about how that works.
+RPC_TZINFO = ZoneInfo('PST8PDT')
 
-def utc_to_local_timezone(d):
-    """Takes a naive datetime UTC and returns a naive datetime in the
-    local time zone."""
-    local_timezone = pytz.timezone(settings.TIME_ZONE)
 
-    d = local_timezone.normalize(d.replace(tzinfo=pytz.utc).astimezone(local_timezone))
+def _tzinfo(tz: Union[str, datetime.tzinfo, None]):
+    """Helper to convert a tz param into a tzinfo
 
-    return d.replace(tzinfo=None)
+    Accepts Defaults to UTC.
+    """
+    if tz is None:
+        return datetime.timezone.utc
+    elif isinstance(tz, datetime.tzinfo):
+        return tz
+    else:
+        return ZoneInfo(tz)
 
-def email_time_to_local_timezone(date_string):
-    """Takes a time string from an email and returns a naive datetime
-    in the local time zone."""
 
-    t = email.utils.parsedate_tz(date_string)
-    d = datetime.datetime(*t[:6])
+def make_aware(dt, tz):
+    """Assign timezone to a naive datetime
 
-    if t[7] != None:
-        d += datetime.timedelta(seconds=t[9])
+    Helper to deal with both pytz and zoneinfo type time zones. Can go away when pytz is removed.
+    """
+    tzinfo = _tzinfo(tz)
+    if hasattr(tzinfo, 'localize'):
+        return tzinfo.localize(dt)  # pytz-style
+    else:
+        return dt.replace(tzinfo=tzinfo)  # zoneinfo- / datetime.timezone-style
 
-    return utc_to_local_timezone(d)
 
-def date2datetime(date, tz=pytz.utc):
-    return datetime.datetime(*(date.timetuple()[:6]), tzinfo=tz)
-    
+def datetime_from_date(date, tz=None):
+    """Get datetime at midnight on a given date"""
+    # accept either pytz or zoneinfo tzinfos until we get rid of pytz
+    return make_aware(datetime.datetime(date.year, date.month, date.day), _tzinfo(tz))
+
+
+def datetime_today(tz=None):
+    """Get a timezone-aware datetime representing midnight today
+
+    By default, uses settings.TIME_ZONE
+    For use with datetime fields representing a date.
+    """
+    if tz is None:
+        tz = settings.TIME_ZONE
+    return timezone.now().astimezone(_tzinfo(tz)).replace(hour=0, minute=0, second=0, microsecond=0)
+
+
+def date_today(tz=None):
+    """Get the date corresponding to the current moment
+
+    By default, uses settings.TIME_ZONE
+    Note that Dates are not themselves timezone aware.
+    """
+    if tz is None:
+        tz = settings.TIME_ZONE
+    return timezone.now().astimezone(_tzinfo(tz)).date()
+
+
+def time_now(tz=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.)
+    """
+    return timezone.now().astimezone(_tzinfo(tz)).time()