diff --git a/ietf/doc/expire.py b/ietf/doc/expire.py
index 328a97404..a2a4a714f 100644
--- a/ietf/doc/expire.py
+++ b/ietf/doc/expire.py
@@ -18,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 datetime_today, DEADLINE_TZINFO
nonexpirable_states: Optional[List[State]] = None
@@ -53,13 +54,13 @@ 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:
diff --git a/ietf/doc/migrations/0046_tzaware_deletedevents.py b/ietf/doc/migrations/0046_tzaware_deletedevents.py
new file mode 100644
index 000000000..197805a99
--- /dev/null
+++ b/ietf/doc/migrations/0046_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', '0045_use_timezone_now_for_doc_models'),
+ ('utils', '0003_pause_to_change_use_tz'),
+ ]
+
+ operations = [
+ migrations.RunPython(forward, reverse),
+ ]
diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py
index c201e7f2d..550e2f879 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
@@ -57,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 datetime_today, DEADLINE_TZINFO
+
class SearchTests(TestCase):
def test_search(self):
@@ -1428,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=ZoneInfo('America/Los_Angeles'))
+
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)
@@ -1436,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)))
@@ -1451,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)))
@@ -1837,7 +1842,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)
@@ -1885,10 +1890,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('America/Los_Angeles')),
)
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=ZoneInfo('America/Los_Angeles')),
+ )
#
url = urlreverse('ietf.doc.views_doc.document_bibtex', kwargs=dict(name=rfc.name))
r = self.client.get(url)
@@ -1906,10 +1915,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('America/Los_Angeles')),
)
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=ZoneInfo('America/Los_Angeles')),
+ )
#
url = urlreverse('ietf.doc.views_doc.document_bibtex', kwargs=dict(name=april1.name))
r = self.client.get(url)
@@ -2044,7 +2057,9 @@ class GenerateDraftAliasesTests(TestCase):
super().tearDown()
def testManagementCommand(self):
- a_month_ago = timezone.now() - datetime.timedelta(30)
+ tz = ZoneInfo('America/Los_Angeles')
+ a_month_ago = (timezone.now() - datetime.timedelta(30)).astimezone(tz)
+ 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()
@@ -2059,9 +2074,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=tz))
+ DocEventFactory.create(doc=doc4, type='published_rfc', time=datetime.datetime(2010, 10, 10, tzinfo=tz))
doc5 = IndividualDraftFactory(authors=[author6])
args = [ ]
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 91d0542ba..a4cb02f11 100644
--- a/ietf/doc/tests_draft.py
+++ b/ietf/doc/tests_draft.py
@@ -34,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):
@@ -402,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)
@@ -429,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()
diff --git a/ietf/doc/tests_irsg_ballot.py b/ietf/doc/tests_irsg_ballot.py
index f178bb4e5..97074197a 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 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))
diff --git a/ietf/doc/tests_review.py b/ietf/doc/tests_review.py
index efe5b7b06..6ff804af3 100644
--- a/ietf/doc/tests_review.py
+++ b/ietf/doc/tests_review.py
@@ -10,7 +10,6 @@ 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
@@ -39,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 DEADLINE_TZINFO
from django.utils.html import escape
class ReviewTests(TestCase):
@@ -734,7 +734,7 @@ 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.
@@ -742,7 +742,7 @@ class ReviewTests(TestCase):
events = ReviewAssignmentDocEvent.objects.filter(doc=assignment.review_request.doc).order_by('-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")
diff --git a/ietf/doc/tests_utils.py b/ietf/doc/tests_utils.py
index aef6eb69a..001bd9737 100644
--- a/ietf/doc/tests_utils.py
+++ b/ietf/doc/tests_utils.py
@@ -143,7 +143,7 @@ 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())
diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py
index c83fa7cf6..d53a0b422 100644
--- a/ietf/doc/utils.py
+++ b/ietf/doc/utils.py
@@ -39,6 +39,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 +638,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 +699,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
diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py
index 7a4e34571..bd6c67737 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):
" % 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)
@@ -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")
diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py
index 5d445cfc2..a28adcb67 100644
--- a/ietf/doc/views_draft.py
+++ b/ietf/doc/views_draft.py
@@ -53,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)
@@ -1477,7 +1479,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)
@@ -1668,7 +1670,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 19c822ec9..1d901ba8a 100644
--- a/ietf/doc/views_review.py
+++ b/ietf/doc/views_review.py
@@ -54,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 DEADLINE_TZINFO
+
def clean_doc_revision(doc, rev):
if rev:
@@ -768,7 +770,11 @@ def complete_review(request, name, assignment_id=None, acronym=None):
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"]
diff --git a/ietf/group/tests_review.py b/ietf/group/tests_review.py
index 924f6fade..4930c740c 100644
--- a/ietf/group/tests_review.py
+++ b/ietf/group/tests_review.py
@@ -26,6 +26,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):
@@ -156,7 +157,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",
@@ -166,7 +167,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",
)
@@ -211,7 +212,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",
@@ -232,15 +233,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)
@@ -253,15 +254,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)
@@ -305,28 +306,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)
diff --git a/ietf/group/views.py b/ietf/group/views.py
index 338defe5e..d003c44a9 100644
--- a/ietf/group/views.py
+++ b/ietf/group/views.py
@@ -119,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 datetime_today, DEADLINE_TZINFO
# --- Helpers ----------------------------------------------------------
@@ -1421,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, {
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/views.py b/ietf/ietfauth/views.py
index 8cf21f52c..20dcb1b71 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
@@ -79,6 +79,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
@@ -224,7 +225,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:
@@ -456,7 +457,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")
@@ -558,7 +559,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/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/views.py b/ietf/ipr/views.py
index 458186949..f6c85bdee 100644
--- a/ietf/ipr/views.py
+++ b/ietf/ipr/views.py
@@ -14,7 +14,6 @@ from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.shortcuts import render, get_object_or_404, redirect
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
import debug # pyflakes:ignore
@@ -44,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
@@ -145,15 +145,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]:
@@ -394,7 +393,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,
)
@@ -588,7 +587,7 @@ def notify(request, id, type):
type_id = form.cleaned_data['type'],
by = request.user.person,
disclosure = ipr,
- response_due = timezone.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..3f46b4320 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=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/tests.py b/ietf/liaisons/tests.py
index 0b3116fb4..f65b18c67 100644
--- a/ietf/liaisons/tests.py
+++ b/ietf/liaisons/tests.py
@@ -1023,7 +1023,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
diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py
index d3fd191fb..59e47e2b9 100644
--- a/ietf/meeting/models.py
+++ b/ietf/meeting/models.py
@@ -41,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
from ietf.utils.models import ForeignKey
from ietf.utils.validators import (
MaxImageSizeValidator, WrappedValidator, validate_file_size, validate_mime_type,
@@ -152,7 +152,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):
@@ -164,7 +164,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):
diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py
index 5c4e1ba2d..5c52fb465 100644
--- a/ietf/nomcom/tests.py
+++ b/ietf/nomcom/tests.py
@@ -50,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 datetime_today, datetime_from_date, DEADLINE_TZINFO
+
client_test_cert_files = None
@@ -1092,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')
@@ -2418,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
@@ -2436,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)
@@ -2451,10 +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)
- 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)
+ # special case for Feb 29
+ if elig_date.month == 2 and elig_date.day == 29:
+ 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 = 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)
@@ -2513,11 +2521,16 @@ class rfc8989EligibilityTests(TestCase):
for nomcom in self.nomcoms:
elig_date = get_eligibility_date(nomcom)
- last_date = elig_date
- first_date = datetime.date(last_date.year-5,last_date.month,last_date.day)
+ 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 = 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 = 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)
- middle_date = datetime.date(last_date.year-3,last_date.month,last_date.day)
eligible = set()
ineligible = set()
@@ -2655,7 +2668,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,
)
@@ -2664,7 +2677,16 @@ class VolunteerDecoratorUnitTests(TestCase):
author_person = PersonFactory()
for i in range(2):
da = WgDocumentAuthorFactory(person=author_person)
- DocEventFactory(type='published_rfc',doc=da.document,time=datetime.date(elig_date.year-3,elig_date.month,elig_date.day))
+ DocEventFactory(
+ type='published_rfc',
+ doc=da.document,
+ 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 b84ae75c9..6fc7dd454 100644
--- a/ietf/nomcom/utils.py
+++ b/ietf/nomcom/utils.py
@@ -31,6 +31,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 datetime_from_date, datetime_today, DEADLINE_TZINFO
import debug # pyflakes:ignore
@@ -536,28 +537,35 @@ def get_8989_eligibility_querysets(date, base_qs):
base_qs = Person.objects.all()
previous_five = previous_five_meetings(date)
+ date_as_dt = datetime_from_date(date, DEADLINE_TZINFO)
three_of_five_qs = new_three_of_five_eligible(previous_five=previous_five, queryset=base_qs)
- three_years_ago = datetime.date(date.year-3,date.month,date.day)
+ # 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.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.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,
- )
+ role__group__time__lte=date_as_dt,
+ )
# 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()
- five_years_ago = datetime.date(date.year-5,date.month,date.day)
- 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
@@ -598,7 +606,7 @@ 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(datetime_today().year,5,1)
def previous_five_meetings(date = None):
if date is None:
diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py
index 5448c1a3b..fc3b0fb61 100644
--- a/ietf/nomcom/views.py
+++ b/ietf/nomcom/views.py
@@ -951,7 +951,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:
@@ -973,7 +973,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/review/tests.py b/ietf/review/tests.py
index 1ecdb4040..260a8646d 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 datetime_from_date
from .factories import ReviewAssignmentFactory, ReviewRequestFactory, ReviewerSettingsFactory
from .mailarch import hash_list_message_id
from .models import ReviewerSettings, ReviewSecretarySettings, ReviewTeamSettings, UnavailablePeriod
@@ -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(),
)
diff --git a/ietf/review/utils.py b/ietf/review/utils.py
index f665ba06b..e8ed632f8 100644
--- a/ietf/review/utils.py
+++ b/ietf/review/utils.py
@@ -32,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 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.
@@ -194,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)
@@ -285,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"],
)
diff --git a/ietf/secr/meetings/tests.py b/ietf/secr/meetings/tests.py
index 2e90a23dc..bf052abe4 100644
--- a/ietf/secr/meetings/tests.py
+++ b/ietf/secr/meetings/tests.py
@@ -138,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,
@@ -286,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,
@@ -306,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, {
@@ -372,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)
@@ -390,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 cf6252cc0..2cef76544 100644
--- a/ietf/secr/meetings/views.py
+++ b/ietf/secr/meetings/views.py
@@ -31,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 make_aware
# prep for agenda changes
@@ -342,7 +343,7 @@ def edit_meeting(request, meeting_id):
else:
form = MeetingModelForm(instance=meeting)
-
+ debug.show('form.errors')
return render(request, 'meetings/edit_meeting.html', {
'meeting': meeting,
'form' : form, },
@@ -799,7 +800,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):
@@ -810,7 +812,7 @@ 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), tzinfo=meeting.tz())
timeslots = TimeSlot.objects.filter(meeting=meeting,time=dtime)
if request.method == 'POST':
@@ -861,7 +863,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), tzinfo=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 f1fcae132..3961f907f 100644
--- a/ietf/secr/proceedings/proc_utils.py
+++ b/ietf/secr/proceedings/proc_utils.py
@@ -25,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[\d]+)-(?P.*)-(?P