datatracker/ietf/doc/expire.py
Jennifer Richards 32054111df
fix: fix failing tests and eliminate naive datetime warnings (#4402)
* test: fix timestamp construction in several doc tests

* refactor: rename date2datetime to datetime_from_date and clarify code

* chore: helper to get tzinfo for PRODUCTION_TIMEZONE

* fix: fix timezone handling in make_last_call()

* test: fix datetime generation in doc.tests_charter

* refactor: remove PRODUCTION_TIMEZONE setting

Replaces the PRODUCTION_TIMEZONE setting with a constant,
DEADLINE_TZINFO, in ietf.utils.timezone.

* test: be more careful about timezone in tests_charter.py

* test: be more careful about timezone in doc/tests.py

* fix: fix timezone handling affecting doc.tests_draft

* fix: fix timezone handling affecting tests_irsg_ballot.py

* fix: fix timezone handling affecting tests_review.py

* fix: fix timezone handling affecting last ietf.doc tests

* fix: fix timezone handling affecting last ietf.group tests

* fix: fix timezone handling affecting ietf.iesg tests

* fix: handle timezones in get_8989_eligibility_querysets

* fix: handle timezones affecting ietfauth tests

* fix: return tz-aware datetime from utc_from_string

* fix: specify timezone for constants in ipr_rfc_number()

* fix: specify tz for ipr deadlines

* fix: handle timezones affecting liaisons tests

* fix: treat leap day in get_8989_eligibility_querysets()

Manual cherry-pick of 248d6474

* test: treat leap day properly in nomcom tests

* fix: fix timezone handling affecting nomcom tests

* test: fix timezone handling in review tests

* fix: fix timezone handling affecting secr.meetings tests

* fix: handle both pytz and zoneinfo timezones in ietf.utils.timezone

* fix: fix timezone handling affecting secr.proceedings tests

* refactor: use make_aware() helper in secr.meetings tests

* test: fix timezone handling in secr.telechat tests

* fix: fix timezone handling affecting stats tests

* fix: eliminate tz-naive helpers affecting sync email parsing

* fix: include timezone data when serializing DeletedEvent data

* fix: fix timezone handling affecting sync tests

* style: remove unused import
2022-09-01 13:07:28 -03:00

224 lines
7.9 KiB
Python

# Copyright The IETF Trust 2010-2020, All Rights Reserved
# -*- coding: utf-8 -*-
# expiry of Internet Drafts
from django.conf import settings
from django.utils import timezone
import datetime, os, shutil, glob, re
from pathlib import Path
from typing import List, Optional # pyflakes:ignore
from ietf.utils import log
from ietf.utils.mail import send_mail
from ietf.doc.models import Document, DocEvent, State, IESG_SUBSTATE_TAGS
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
def expirable_drafts(queryset=None):
"""Return a queryset with expirable drafts."""
global nonexpirable_states
# the general rule is that each active draft is expirable, unless
# it's in a state where we shouldn't touch it
if not queryset:
queryset = Document.objects.all()
# Populate this first time through (but after django has been set up)
if nonexpirable_states is None:
# all IESG states except I-D Exists, AD Watching, and Dead block expiry
nonexpirable_states = list(State.objects.filter(used=True, type="draft-iesg").exclude(slug__in=("idexists","watching", "dead")))
# sent to RFC Editor and RFC Published block expiry (the latter
# shouldn't be possible for an active draft, though)
nonexpirable_states += list(State.objects.filter(used=True, type__in=("draft-stream-iab", "draft-stream-irtf", "draft-stream-ise"), slug__in=("rfc-edit", "pub")))
# other IRTF states that block expiration
nonexpirable_states += list(State.objects.filter(used=True, type_id="draft-stream-irtf", slug__in=("irsgpoll", "iesg-rev",)))
return queryset.filter(
states__type="draft", states__slug="active"
).exclude(
expires=None
).exclude(
states__in=nonexpirable_states
).exclude(
tags="rfc-rev" # under review by the RFC Editor blocks expiry
).distinct()
def get_soon_to_expire_drafts(days_of_warning):
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_today(DEADLINE_TZINFO) + datetime.timedelta(1))
def in_draft_expire_freeze(when=None):
if when == None:
when = timezone.now()
meeting = Meeting.objects.filter(type='ietf', date__gte=when-datetime.timedelta(days=7)).order_by('date').first()
if not meeting:
return False
d = meeting.get_second_cut_off()
# for some reason, the old Perl code started at 9 am
second_cut_off = datetime.datetime.combine(d, datetime.time(9, 0))
d = meeting.get_ietf_monday()
ietf_monday = datetime.datetime.combine(d, datetime.time(0, 0))
return second_cut_off <= when < ietf_monday
def send_expire_warning_for_draft(doc):
if ((doc.get_state_slug("draft-iesg") == "dead") or
(doc.get_state_slug("draft") != "active")):
return # don't warn about dead or inactive documents
expiration = doc.expires.date()
(to,cc) = gather_address_lists('doc_expires_soon',doc=doc)
s = doc.get_state("draft-iesg")
log.assertion('s')
state = s.name if s else "I-D Exists" # TODO remove the if clause after some runtime shows no assertions
frm = None
request = None
if to or cc:
send_mail(request, to, frm,
"Expiration impending: %s" % doc.file_tag(),
"doc/draft/expire_warning_email.txt",
dict(doc=doc,
state=state,
expiration=expiration
),
cc=cc)
def send_expire_notice_for_draft(doc):
if doc.get_state_slug("draft-iesg") == "dead":
return
s = doc.get_state("draft-iesg")
log.assertion('s')
state = s.name if s else "I-D Exists" # TODO remove the if clause after some rintime shows no assertions
request = None
(to,cc) = gather_address_lists('doc_expired',doc=doc)
send_mail(request, to,
"I-D Expiring System <ietf-secretariat-reply@ietf.org>",
"I-D was expired %s" % doc.file_tag(),
"doc/draft/id_expired_email.txt",
dict(doc=doc,
state=state,
),
cc=cc)
def move_draft_files_to_archive(doc, rev):
def move_file(f):
src = os.path.join(settings.INTERNET_DRAFT_PATH, f)
dst = os.path.join(settings.INTERNET_DRAFT_ARCHIVE_DIR, f)
if os.path.exists(src):
try:
shutil.move(src, dst)
except IOError as e:
if "No such file or directory" in str(e):
pass
else:
raise
src_dir = Path(settings.INTERNET_DRAFT_PATH)
for file in src_dir.glob("%s-%s.*" % (doc.name, rev)):
move_file(str(file.name))
def expire_draft(doc):
# clean up files
move_draft_files_to_archive(doc, doc.rev)
system = Person.objects.get(name="(System)")
events = []
# change the state
if doc.latest_event(type='started_iesg_process'):
new_state = State.objects.get(used=True, type="draft-iesg", slug="dead")
prev_state = doc.get_state(new_state.type_id)
prev_tags = doc.tags.filter(slug__in=IESG_SUBSTATE_TAGS)
if new_state != prev_state:
doc.set_state(new_state)
doc.tags.remove(*prev_tags)
e = add_state_change_event(doc, system, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
if e:
events.append(e)
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
if e:
events.append(e)
events.append(DocEvent.objects.create(doc=doc, rev=doc.rev, by=system, type="expired_document", desc="Document has expired"))
doc.set_state(State.objects.get(used=True, type="draft", slug="expired"))
doc.save_with_history(events)
def clean_up_draft_files():
"""Move unidentified and old files out of the Internet Draft directory."""
cut_off = datetime.date.today()
pattern = os.path.join(settings.INTERNET_DRAFT_PATH, "draft-*.*")
filename_re = re.compile(r'^(.*)-(\d\d)$')
def splitext(fn):
"""
Split the pathname path into a pair (root, ext) such that root + ext
== path, and ext is empty or begins with a period and contains all
periods in the last path component.
This differs from os.path.splitext in the number of periods in the ext
parts when the final path component contains more than one period.
"""
s = fn.rfind("/")
if s == -1:
s = 0
i = fn[s:].find(".")
if i == -1:
return fn, ''
else:
return fn[:s+i], fn[s+i:]
for path in glob.glob(pattern):
basename = os.path.basename(path)
stem, ext = splitext(basename)
match = filename_re.search(stem)
if not match:
filename, revision = ("UNKNOWN", "00")
else:
filename, revision = match.groups()
def move_file_to(subdir):
shutil.move(path,
os.path.join(settings.INTERNET_DRAFT_ARCHIVE_DIR, subdir, basename))
try:
doc = Document.objects.get(name=filename, rev=revision)
state = doc.get_state_slug()
if state in ("rfc","repl"):
move_file_to("")
elif state in ("expired", "auth-rm", "ietf-rm") and doc.expires and doc.expires.date() < cut_off:
move_file_to("")
except Document.DoesNotExist:
move_file_to("unknown_ids")