Repository: ietf-tools/datatracker

Issue:
interim notification templates include timestamps in wrong tz
### Describe the issue

In both `interim_meeting_cancellation_notice.txt` and `interim_session_cancellation_notice.txt`, the context variables `start_time` and `end_time` are interpolated. These should be shown in meeting time. Currently they'll be displayed in `settings.TIME_ZONE` (aka PST8PDT) instead.


### Code of Conduct

- [X] I agree to follow the [IETF's Code of Conduct](https://github.com/ietf-tools/.github/blob/main/CODE_OF_CONDUCT.md)


Discussion:

Resolving patch:
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -1577,7 +1577,7 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
                 "updated": updated,
                 "filter_categories": filter_organizer.get_filter_categories(),
                 "non_area_keywords": filter_organizer.get_non_area_keywords(),
-                "now": timezone.now().astimezone(pytz.utc),
+                "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,
diff --git a/ietf/secr/meetings/forms.py b/ietf/secr/meetings/forms.py
--- a/ietf/secr/meetings/forms.py
+++ b/ietf/secr/meetings/forms.py
@@ -166,6 +166,15 @@ def get_day_choices(self):
         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/views.py b/ietf/secr/meetings/views.py
--- a/ietf/secr/meetings/views.py
+++ b/ietf/secr/meetings/views.py
@@ -558,7 +558,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,
@@ -813,13 +813,20 @@ def times_edit(request, meeting_id, schedule_name, time):
     parts = [ int(x) for x in time.split(':') ]
     dtime = make_aware(datetime.datetime(*parts), meeting.tz())
     timeslots = TimeSlot.objects.filter(meeting=meeting,time=dtime)
+    day = (dtime.date() - meeting.date) // datetime.timedelta(days=1)
+    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)
@@ -838,13 +845,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', {


Test patch:
diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py
--- a/ietf/meeting/tests_js.py
+++ b/ietf/meeting/tests_js.py
@@ -271,14 +271,32 @@ def test_edit_meeting_schedule(self):
         # 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)),
@@ -430,21 +448,24 @@ def test_past_swap_days_buttons(self):
 
         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
@@ -495,7 +516,8 @@ def test_past_swap_days_buttons(self):
         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',
         )
@@ -504,14 +526,16 @@ def test_past_swap_days_buttons(self):
         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',
         )