diff --git a/ietf/doc/feeds.py b/ietf/doc/feeds.py
index cddc3329b..7885e75e3 100644
--- a/ietf/doc/feeds.py
+++ b/ietf/doc/feeds.py
@@ -16,6 +16,7 @@ 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):
@@ -134,7 +135,14 @@ 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 = timezone.now() - datetime.timedelta(days=8)
rfc_events = DocEvent.objects.filter(type='published_rfc',time__gte=cutoff).order_by('-time')
diff --git a/ietf/doc/models.py b/ietf/doc/models.py
index b0e7fca25..4e2e8836d 100644
--- a/ietf/doc/models.py
+++ b/ietf/doc/models.py
@@ -36,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 RPC_TZINFO
if TYPE_CHECKING:
# importing other than for type checking causes errors due to cyclic imports
from ietf.meeting.models import ProceedingsMaterial, Session
@@ -925,13 +926,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
diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py
index 550e2f879..4c769b0d8 100644
--- a/ietf/doc/tests.py
+++ b/ietf/doc/tests.py
@@ -58,7 +58,7 @@ 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
+from ietf.utils.timezone import datetime_today, DEADLINE_TZINFO, RPC_TZINFO
class SearchTests(TestCase):
@@ -1431,7 +1431,7 @@ 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'))
+ 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)
@@ -1890,13 +1890,13 @@ class DocTestCase(TestCase):
#other_aliases = ['rfc6020',],
states = [('draft','rfc'),('draft-iesg','pub')],
std_level_id = 'ps',
- time = datetime.datetime(2010, 10, 10, tzinfo=ZoneInfo('America/Los_Angeles')),
+ time = datetime.datetime(2010, 10, 10, tzinfo=ZoneInfo(settings.TIME_ZONE)),
)
num = rfc.rfc_number()
DocEventFactory.create(
doc=rfc,
type='published_rfc',
- time=datetime.datetime(2010, 10, 10, tzinfo=ZoneInfo('America/Los_Angeles')),
+ time=datetime.datetime(2010, 10, 10, tzinfo=RPC_TZINFO),
)
#
url = urlreverse('ietf.doc.views_doc.document_bibtex', kwargs=dict(name=rfc.name))
@@ -1915,13 +1915,13 @@ class DocTestCase(TestCase):
stream_id = 'ise',
states = [('draft','rfc'),('draft-iesg','pub')],
std_level_id = 'inf',
- time = datetime.datetime(1990, 4, 1, tzinfo=ZoneInfo('America/Los_Angeles')),
+ time = datetime.datetime(1990, 4, 1, tzinfo=ZoneInfo(settings.TIME_ZONE)),
)
num = april1.rfc_number()
DocEventFactory.create(
doc=april1,
type='published_rfc',
- time=datetime.datetime(1990, 4, 1, tzinfo=ZoneInfo('America/Los_Angeles')),
+ time=datetime.datetime(1990, 4, 1, tzinfo=RPC_TZINFO),
)
#
url = urlreverse('ietf.doc.views_doc.document_bibtex', kwargs=dict(name=april1.name))
@@ -2057,8 +2057,7 @@ class GenerateDraftAliasesTests(TestCase):
super().tearDown()
def testManagementCommand(self):
- tz = ZoneInfo('America/Los_Angeles')
- a_month_ago = (timezone.now() - datetime.timedelta(30)).astimezone(tz)
+ 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()
@@ -2075,8 +2074,8 @@ class GenerateDraftAliasesTests(TestCase):
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)
- 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))
+ 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 = [ ]
diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py
index d53a0b422..13cfe5265 100644
--- a/ietf/doc/utils.py
+++ b/ietf/doc/utils.py
@@ -968,6 +968,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')
diff --git a/ietf/doc/utils_search.py b/ietf/doc/utils_search.py
index 47532fdb3..67024ed05 100644
--- a/ietf/doc/utils_search.py
+++ b/ietf/doc/utils_search.py
@@ -5,6 +5,10 @@ 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
@@ -204,7 +208,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_doc.py b/ietf/doc/views_doc.py
index e2ec39edc..670aff436 100644
--- a/ietf/doc/views_doc.py
+++ b/ietf/doc/views_doc.py
@@ -967,7 +967,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:
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/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/stats/views.py b/ietf/stats/views.py
index 33b0d3c19..784b7312c 100644
--- a/ietf/stats/views.py
+++ b/ietf/stats/views.py
@@ -40,7 +40,7 @@ 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
+from ietf.utils.timezone import date_today, DEADLINE_TZINFO, RPC_TZINFO
def stats_index(request):
@@ -625,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)
diff --git a/ietf/sync/rfceditor.py b/ietf/sync/rfceditor.py
index 4bedc9017..1da11f01e 100644
--- a/ietf/sync/rfceditor.py
+++ b/ietf/sync/rfceditor.py
@@ -25,7 +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
+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"
@@ -333,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:
@@ -373,7 +376,7 @@ def update_docs_from_rfc_index(index_data, errata_data, skip_older_than_date=Non
for rfc_number, title, authors, rfc_published_date, current_status, updates, updated_by, obsoletes, obsoleted_by, also, draft, has_errata, stream, wg, file_formats, pages, abstract in index_data:
- if skip_older_than_date and datetime_from_date(rfc_published_date) < datetime_from_date(skip_older_than_date):
+ if skip_older_than_date and rfc_published_date < skip_older_than_date:
# speed up the process by skipping old entries
continue
@@ -444,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_from_date(rfc_published_date)
- synthesized = timezone.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 10a5483d0..f245145d2 100644
--- a/ietf/sync/tests.py
+++ b/ietf/sync/tests.py
@@ -23,7 +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
+from ietf.utils.timezone import date_today, RPC_TZINFO
class IANASyncTests(TestCase):
@@ -354,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/templates/doc/document_draft.html b/ietf/templates/doc/document_draft.html
index dc1d59155..2367c12eb 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 }}/">
+ 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 %}
RFC - {{ doc.std_level }}
{% if published %}
- ({{ published.time|date:"F Y" }})
+ ({{ doc.pub_date|date:"F Y" }})
{% else %}
(Publication date unknown)
{% endif %}
diff --git a/ietf/utils/timezone.py b/ietf/utils/timezone.py
index dabe3c27d..149e471a3 100644
--- a/ietf/utils/timezone.py
+++ b/ietf/utils/timezone.py
@@ -7,9 +7,17 @@ from django.conf import settings
from django.utils import timezone
+# 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')
+# 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 make_aware(dt, tzinfo):
"""Assign timezone to a naive datetime