* chore: Remove unused "rendertest" stuff (#6015) * fix: restore ability to create status change documents (#5963) * fix: restore ability to create status change documents Fixes #5962 * chore: address review comment * fix: Provide human-friendly status in submission status API response (#6011) Co-authored-by: nectostr <bastinda96@gmail.com> * fix: Make name/email lookups case-insensitive (#5972) (#6007) * fix: Make name/email lookups case-insensitive (#5972) Use icontains so that looking up name or email is case insensitive Added a test Fixes: 5972 * fix: Use __iexact not __icontains * fix: Clarify no-action-needed (#5918) (#6020) When a draft is submitted for manual processing, clarify that no action is needed; the Secretariat has the next steps. Fixes: #5918 * fix: Fix menu hover issue (#6019) * fix: Fix menu hover issue Fixes #5702 * Fix leftmenu hover issue * fix: Server error from api_get_session_materials() (#6025) Fixes #5877 * fix: Clarify Questionnaire label (#4688) (#6017) When filtering nominees, `Questionnaire` implies `Accepted == yes` so fix the dropdown test tosay that. Fixes: #4688 * chore: Merge from @martinthomson's rfc-txt-html (#6023) * fix:no history entry when changing RFC Editor note for doc (#6021) * fix:no history entry when changing RFC Editor note for doc * fix:no history entry when changing RFC Editor note for doc --------- Co-authored-by: Priyanka Narkar <priyankanarkar@dhcp-91f8.meeting.ietf.org> * fix: avoid deprecation warning on view_list() for objs without CommunityList Fixes #5942 * fix: return 404 for non-existing revisions (#6014) * fix: return 404 for non-existing revisions Links to non-existing revisions to docs should return 404 * fix: change rfc/rev and search behaviour * refactor: fix tab level * fix: return 404 for rfc revision for bibtex * fix: provide date for revisions in bibtex output (#6029) * fix: provide date for revisions in bibtex output * refactor: change walrus to if's * fix: specify particular revision for events * fix: review refactoring issue fixes #5447 * fix: Remove automatically suggested document for document that is already has review request (fixes #3211) (#5425) * Added check that if there is already review request for the document in question, ignore the automatic suggestion for that document. Fixes #3211. * fix: dont block on open requests for a previous version. Add tests --------- Co-authored-by: Nicolas Giard <github@ngpixel.com> Co-authored-by: Robert Sparks <rjsparks@nostrum.com> * feat: IAB statements (#5940) * feat: support iab and iesg statements. Import iab statements. (#5895) * feat: infrastructure for statements doctype * chore: basic test framework * feat: basic statement document view * feat: show replaced statements * chore: black * fix: state help for statements * fix: cleanout non-relevant email expansions * feat: import iab statements, provide group statements tab * fix: guard against running import twice * feat: build redirect csv for iab statements * fix: set document state on import * feat: show published date on main doc view * feat: handle pdf statements * feat: create new and update statements * chore: copyright block updates * chore: remove flakes * chore: black * feat: add edit/new buttons for the secretariat * fix: address PR #5895 review comments * fix: pin pydantic until inflect catches up (#5901) (#5902) * chore: re-un-pin pydantic * feat: include submitter in email about submitted slides (#6033) * feat: include submitter in email about submitted slides fixes #6031 * chore: remove unintended whitespace change * chore(dev): update .vscode/settings.json with new taskExplorer settings * fix: Add editorial stream to proceedings (#6027) * fix: Add editorial stream to proceedings Fixes #5717 * fix: Move editorial stream after the irtf in proceedings * fix: Add editorial stream to meeting materials (#6047) Fixes #6042 * fix: Shows requested reviews for doc fixes (#6022) * Fix: Shows requested reviews for doc * Changed template includes to only give required variables to them. * feat: allow openId to choose an unactive email if there are none active (#6041) * feat: allow openId to choose an unactive email if there are no active ones * chore: correct typo * chore: rename unactive to inactive * fix: Make review table more responsive (#6053) * fix: Improve layout of review table * Progress * Progress * Final changes * Fix tests * Remove fluff * Undo commits * ci: add --validate-html-harder to tests * ci: add --validate-html-harder to build.yml workflow * fix: Set colspan to actual number of columns (#6069) * fix: Clean up view_feedback_pending (#6070) - Remove "Unclassified" column header, which caused misalignment in the table body. - Show the message author - previously displayed as `(None)`. * docs: Update LICENSE year * fix: Remove IESG state edit button when state is 'dead' (#6051) (#6065) * fix: Correctly order "last call requested" column in the IESG dashboard (#6079) * ci: update dev sandbox init script to start memcached * feat: Reclassify nomcom feedback (#6002) * fix: Clean up view_feedback_pending - Remove "Unclassified" column header, which caused misalignment in the table body. - Show the message author - previously displayed as `(None)`. * feat: Reclassify nomcom feedback (#4669) - There's a new `Chair/Advisor Tasks` menu item `Reclassify feedback`. - I overloaded `view_feedback*` URLs with a `?reclassify` parameter. - This adds a checkbox to each feedback message, and a `Reclassify` button at the bottom of each feedback page. - "Reclassifying" basically de-classifies the feedback, and punts it back to the "Pending emails" view for reclassification. - If a feedback has been applied to multiple nominees, declassifying it from one nominee removes it from all. * fix: Remove unused local variables * fix: Fix some missing and mis-nested html * test: Add tests for reclassifying feedback * refactor: Substantial redesign of feedback reclassification - Break out reclassify_feedback* as their own URLs and views, and revert changes to view_feedback*.html. - Replace checkboxes with a Reclassify button on each message. * fix: Remember to clear the feedback associations when reclassifying * feat: Add an 'Overcome by events' feedback type * refactor: When invoking reclassification from a view-feedback page, load the corresponding reclassify-feedback page * fix: De-conflict migration with 0004_statements Also change the coding style to match, and add a reverse migration. * fix: Fix a test case to account for new feedback type * fix: 842e730 broke the Back button * refactor: Reclassify feedback directly instead of putting it back in the work queue * fix: Adjust tests to new workflow * refactor: Further refine reclassification to avoid redirects * refactor: Impose a FeedbackTypeName ordering Also add FeedbackTypeName.legend field, rather than synthesizing it every time we classify or reclassify feedback. In the reclassification forms, only show the relevant feedback types. * refactor: Merge reclassify_feedback_* back into view_feedback_* This means the "Reclassify" button is always present, but eliminates some complexity. * refactor: Add filter(used=True) on FeedbackTypeName querysets * refactor: Add the new FeedbackTypeName to the reclassification success message * fix: Secure reclassification against rogue nomcom members * fix: Print decoded key and fully clean up test nomcom (#6094) * fix: Delete Person records when deleting a test nomcom * fix: Decode test nomcom private key before printing * test: Use correct time zone for test_statement_doc_view (#6064) * chore(deps): update all npm dependencies for playwright (#6061) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> * chore(deps): update all npm dependencies for dev/diff (#6062) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> * chore(deps): update all npm dependencies for dev/coverage-action (#6063) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> * fix: Hash cache key for default memcached cache (#6089) * feat: Show docs that an AD hasn't balloted on that need ballots to progress (#6075) * fix(doc): Unify help texts for document states (#6060) * Fix IESG State help text link (only) * Intermediate checkpoint * Correct URL filtering of state descriptions * Unify help texts for document states * Remove redundant load static from template --------- Co-authored-by: Robert Sparks <rjsparks@nostrum.com> * ci: fix sandbox start.sh memcached user * fix: refactor how settings handles cache definitions (#6099) * fix: refactor how settings handles cache definitions * chore: more english-speaker readable expression * fix: Cast cache key to str before calling encode (#6100) --------- Co-authored-by: Robert Sparks <rjsparks@nostrum.com> Co-authored-by: Liubov Kurafeeva <liubov.kurafeeva@gmail.com> Co-authored-by: nectostr <bastinda96@gmail.com> Co-authored-by: Rich Salz <rsalz@akamai.com> Co-authored-by: PriyankaN <priyanka@amsl.com> Co-authored-by: Priyanka Narkar <priyankanarkar@dhcp-91f8.meeting.ietf.org> Co-authored-by: Ali <alireza83@gmail.com> Co-authored-by: Roman Beltiukov <maybe.hello.world@gmail.com> Co-authored-by: Tero Kivinen <kivinen@iki.fi> Co-authored-by: Nicolas Giard <github@ngpixel.com> Co-authored-by: Kesara Rathnayake <kesara@fq.nz> Co-authored-by: Jennifer Richards <jennifer@staff.ietf.org> Co-authored-by: Paul Selkirk <paul@painless-security.com> Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> Co-authored-by: Jim Fenton <fenton@bluepopcorn.net>
736 lines
31 KiB
Python
736 lines
31 KiB
Python
# Copyright The IETF Trust 2012-2020, All Rights Reserved
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
import datetime
|
|
import io
|
|
import os
|
|
import re
|
|
|
|
from typing import Dict # pyflakes:ignore
|
|
|
|
from django import forms
|
|
from django.shortcuts import render, get_object_or_404, redirect
|
|
from django.http import Http404, HttpResponseRedirect
|
|
from django.urls import reverse
|
|
from django.template.loader import render_to_string
|
|
from django.conf import settings
|
|
from django.utils.encoding import force_str
|
|
from django.utils.html import escape
|
|
|
|
import debug # pyflakes:ignore
|
|
from ietf.doc.mails import email_ad_approved_status_change
|
|
|
|
from ietf.doc.models import ( Document, DocAlias, State, DocEvent, BallotDocEvent,
|
|
BallotPositionDocEvent, NewRevisionDocEvent, WriteupDocEvent, STATUSCHANGE_RELATIONS )
|
|
from ietf.doc.forms import AdForm
|
|
from ietf.doc.lastcall import request_last_call
|
|
from ietf.doc.utils import add_state_change_event, update_telechat, close_open_ballots, create_ballot_if_not_open
|
|
from ietf.doc.views_ballot import LastCallTextForm
|
|
from ietf.group.models import Group
|
|
from ietf.iesg.models import TelechatDate
|
|
from ietf.ietfauth.utils import has_role, role_required
|
|
from ietf.mailtrigger.utils import gather_address_lists
|
|
from ietf.name.models import DocRelationshipName, StdLevelName
|
|
from ietf.person.models import Person
|
|
from ietf.utils.mail import send_mail_preformatted
|
|
from ietf.utils.textupload import get_cleaned_text_file_content
|
|
from ietf.utils.timezone import date_today, DEADLINE_TZINFO
|
|
|
|
|
|
class ChangeStateForm(forms.Form):
|
|
new_state = forms.ModelChoiceField(State.objects.filter(type="statchg", used=True), label="Status Change Evaluation State", empty_label=None, required=True)
|
|
comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the review history.", required=False, strip=False)
|
|
|
|
|
|
@role_required("Area Director", "Secretariat")
|
|
def change_state(request, name, option=None):
|
|
"""Change state of an status-change document, notifying parties as necessary
|
|
and logging the change as a comment."""
|
|
status_change = get_object_or_404(Document, type="statchg", name=name)
|
|
|
|
login = request.user.person
|
|
|
|
if request.method == 'POST':
|
|
form = ChangeStateForm(request.POST)
|
|
if form.is_valid():
|
|
clean = form.cleaned_data
|
|
new_state = clean['new_state']
|
|
comment = clean['comment'].rstrip()
|
|
|
|
if comment:
|
|
c = DocEvent(type="added_comment", doc=status_change, rev=status_change.rev, by=login)
|
|
c.desc = comment
|
|
c.save()
|
|
|
|
prev_state = status_change.get_state()
|
|
if new_state != prev_state:
|
|
status_change.set_state(new_state)
|
|
events = []
|
|
events.append(add_state_change_event(status_change, login, prev_state, new_state))
|
|
status_change.save_with_history(events)
|
|
|
|
if new_state.slug == "iesgeval":
|
|
e = create_ballot_if_not_open(request, status_change, login, "statchg", status_change.time) # pyflakes:ignore
|
|
ballot = status_change.latest_event(BallotDocEvent, type="created_ballot")
|
|
if has_role(request.user, "Area Director") and not status_change.latest_event(BallotPositionDocEvent, balloter=login, ballot=ballot, type="changed_ballot_position"):
|
|
|
|
# The AD putting a status change into iesgeval who doesn't already have a position is saying "yes"
|
|
pos = BallotPositionDocEvent(doc=status_change, rev=status_change.rev, by=login)
|
|
pos.ballot = ballot
|
|
pos.type = "changed_ballot_position"
|
|
pos.balloter = login
|
|
pos.pos_id = "yes"
|
|
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.balloter.plain_name())
|
|
pos.save()
|
|
|
|
send_status_change_eval_email(request,status_change)
|
|
|
|
|
|
if new_state.slug == "lc-req":
|
|
request_last_call(request, status_change)
|
|
return render(request, 'doc/draft/last_call_requested.html',
|
|
dict(doc=status_change,
|
|
url = status_change.get_absolute_url(),
|
|
))
|
|
elif new_state.slug == 'appr-pend' and has_role(request.user, "Area Director"):
|
|
related_docs = status_change.relateddocument_set.filter(
|
|
relationship__slug__in=STATUSCHANGE_RELATIONS
|
|
)
|
|
related_doc_info = [
|
|
dict(title=rel_doc.target.document.title,
|
|
canonical_name=rel_doc.target.document.canonical_name(),
|
|
newstatus=newstatus(rel_doc))
|
|
for rel_doc in related_docs
|
|
]
|
|
email_ad_approved_status_change(
|
|
request,
|
|
status_change,
|
|
related_doc_info=related_doc_info,
|
|
)
|
|
|
|
return redirect('ietf.doc.views_doc.document_main', name=status_change.name)
|
|
else:
|
|
s = status_change.get_state()
|
|
init = dict(new_state=s.pk if s else None,
|
|
type='statchg',
|
|
label='Status Change Evaluation State',
|
|
)
|
|
form = ChangeStateForm(initial=init)
|
|
|
|
return render(request, 'doc/change_state.html',
|
|
dict(form=form,
|
|
doc=status_change,
|
|
login=login,
|
|
help_url=reverse('ietf.doc.views_help.state_help', kwargs=dict(type="status-change")),
|
|
))
|
|
|
|
def send_status_change_eval_email(request,doc):
|
|
for target in ('iesg_ballot_issued', 'ballot_issued_iana'):
|
|
addrs = gather_address_lists(target,doc=doc).as_strings()
|
|
msg = render_to_string("doc/eval_email.txt",
|
|
dict(doc=doc,
|
|
doc_url = settings.IDTRACKER_BASE_URL+doc.get_absolute_url(),
|
|
to = addrs.to,
|
|
cc = addrs.cc
|
|
)
|
|
)
|
|
send_mail_preformatted(request,msg)
|
|
|
|
class UploadForm(forms.Form):
|
|
content = forms.CharField(widget=forms.Textarea, label="Status change text", help_text="Edit the status change text.", required=False, strip=False)
|
|
txt = forms.FileField(label=".txt format", help_text="Or upload a .txt file.", required=False)
|
|
|
|
def clean_content(self):
|
|
return self.cleaned_data["content"].replace("\r", "")
|
|
|
|
def clean_txt(self):
|
|
return get_cleaned_text_file_content(self.cleaned_data["txt"])
|
|
|
|
def save(self, doc):
|
|
filename = os.path.join(settings.STATUS_CHANGE_PATH, '%s-%s.txt' % (doc.canonical_name(), doc.rev))
|
|
with io.open(filename, 'w', encoding='utf-8') as destination:
|
|
if self.cleaned_data['txt']:
|
|
destination.write(self.cleaned_data['txt'])
|
|
else:
|
|
destination.write(self.cleaned_data['content'])
|
|
|
|
#This is very close to submit on charter - can we get better reuse?
|
|
@role_required('Area Director','Secretariat')
|
|
def submit(request, name):
|
|
doc = get_object_or_404(Document, type="statchg", name=name)
|
|
|
|
login = request.user.person
|
|
|
|
path = os.path.join(settings.STATUS_CHANGE_PATH, '%s-%s.txt' % (doc.canonical_name(), doc.rev))
|
|
not_uploaded_yet = doc.rev == "00" and not os.path.exists(path)
|
|
|
|
if not_uploaded_yet:
|
|
# this case is special - the status change text document doesn't actually exist yet
|
|
next_rev = doc.rev
|
|
else:
|
|
next_rev = "%02d" % (int(doc.rev)+1)
|
|
|
|
if request.method == 'POST':
|
|
if "submit_response" in request.POST:
|
|
form = UploadForm(request.POST, request.FILES)
|
|
if form.is_valid():
|
|
doc.rev = next_rev
|
|
|
|
events = []
|
|
e = NewRevisionDocEvent(doc=doc, by=login, type="new_revision")
|
|
e.desc = "New version available: <b>%s-%s.txt</b>" % (doc.canonical_name(), doc.rev)
|
|
e.rev = doc.rev
|
|
e.save()
|
|
events.append(e)
|
|
|
|
# Save file on disk
|
|
form.save(doc)
|
|
|
|
doc.save_with_history(events)
|
|
|
|
return redirect('ietf.doc.views_doc.document_main', name=doc.name)
|
|
|
|
elif "reset_text" in request.POST:
|
|
|
|
init = { "content": render_to_string("doc/status_change/initial_template.txt",dict())}
|
|
form = UploadForm(initial=init)
|
|
|
|
# Protect against handcrufted malicious posts
|
|
else:
|
|
form = None
|
|
|
|
else:
|
|
form = None
|
|
|
|
if not form:
|
|
init = { "content": ""}
|
|
|
|
if not_uploaded_yet:
|
|
init["content"] = render_to_string("doc/status_change/initial_template.txt",
|
|
dict(),
|
|
)
|
|
else:
|
|
filename = os.path.join(settings.STATUS_CHANGE_PATH, '%s-%s.txt' % (doc.canonical_name(), doc.rev))
|
|
try:
|
|
with io.open(filename, 'r') as f:
|
|
init["content"] = f.read()
|
|
except IOError:
|
|
pass
|
|
|
|
form = UploadForm(initial=init)
|
|
|
|
return render(request, 'doc/status_change/submit.html',
|
|
{'form': form,
|
|
'next_rev': next_rev,
|
|
'doc' : doc,
|
|
})
|
|
|
|
class ChangeTitleForm(forms.Form):
|
|
title = forms.CharField(max_length=255, label="Title", required=True)
|
|
|
|
@role_required("Area Director", "Secretariat")
|
|
def edit_title(request, name):
|
|
"""Change the title for this status_change document."""
|
|
|
|
status_change = get_object_or_404(Document, type="statchg", name=name)
|
|
|
|
if request.method == 'POST':
|
|
form = ChangeTitleForm(request.POST)
|
|
if form.is_valid():
|
|
|
|
status_change.title = form.cleaned_data['title']
|
|
|
|
c = DocEvent(type="added_comment", doc=status_change, rev=status_change.rev, by=request.user.person)
|
|
c.desc = "Title changed to '%s'"%status_change.title
|
|
c.save()
|
|
|
|
status_change.save_with_history([c])
|
|
|
|
return redirect("ietf.doc.views_doc.document_main", name=status_change.name)
|
|
|
|
else:
|
|
init = { "title" : status_change.title }
|
|
form = ChangeTitleForm(initial=init)
|
|
|
|
titletext = '%s-%s.txt' % (status_change.canonical_name(),status_change.rev)
|
|
return render(request, 'doc/change_title.html',
|
|
{'form': form,
|
|
'doc': status_change,
|
|
'titletext' : titletext,
|
|
},
|
|
)
|
|
|
|
@role_required("Area Director", "Secretariat")
|
|
def edit_ad(request, name):
|
|
"""Change the shepherding Area Director for this status_change."""
|
|
|
|
status_change = get_object_or_404(Document, type="statchg", name=name)
|
|
|
|
if request.method == 'POST':
|
|
form = AdForm(request.POST)
|
|
if form.is_valid():
|
|
status_change.ad = form.cleaned_data['ad']
|
|
|
|
c = DocEvent(type="added_comment", doc=status_change, rev=status_change.rev, by=request.user.person)
|
|
c.desc = "Shepherding AD changed to "+status_change.ad.name
|
|
c.save()
|
|
|
|
status_change.save_with_history([c])
|
|
|
|
return redirect("ietf.doc.views_doc.document_main", name=status_change.name)
|
|
|
|
else:
|
|
init = { "ad" : status_change.ad_id }
|
|
form = AdForm(initial=init)
|
|
|
|
titletext = '%s-%s.txt' % (status_change.canonical_name(),status_change.rev)
|
|
return render(request, 'doc/change_ad.html',
|
|
{'form': form,
|
|
'doc': status_change,
|
|
'titletext' : titletext,
|
|
},
|
|
)
|
|
|
|
def newstatus(relateddoc):
|
|
|
|
level_map = {
|
|
'tops' : 'ps',
|
|
'tois' : 'std',
|
|
'tohist' : 'hist',
|
|
'toinf' : 'inf',
|
|
'tobcp' : 'bcp',
|
|
'toexp' : 'exp',
|
|
}
|
|
|
|
return StdLevelName.objects.get(slug=level_map[relateddoc.relationship.slug])
|
|
|
|
def default_approval_text(status_change,relateddoc):
|
|
|
|
current_text = status_change.text_or_error() # pyflakes:ignore
|
|
|
|
if relateddoc.target.document.std_level_id in ('std','ps','ds','bcp',):
|
|
action = "Protocol Action"
|
|
else:
|
|
action = "Document Action"
|
|
|
|
|
|
addrs = gather_address_lists('ballot_approved_status_change',doc=status_change).as_strings(compact=False)
|
|
text = render_to_string("doc/status_change/approval_text.txt",
|
|
dict(status_change=status_change,
|
|
status_change_url = settings.IDTRACKER_BASE_URL+status_change.get_absolute_url(),
|
|
relateddoc= relateddoc,
|
|
relateddoc_url = settings.IDTRACKER_BASE_URL+relateddoc.target.document.get_absolute_url(),
|
|
approved_text = current_text,
|
|
action=action,
|
|
newstatus=newstatus(relateddoc),
|
|
to=addrs.to,
|
|
cc=addrs.cc,
|
|
)
|
|
)
|
|
|
|
return text
|
|
|
|
from django.forms.formsets import formset_factory
|
|
|
|
class AnnouncementForm(forms.Form):
|
|
announcement_text = forms.CharField(widget=forms.Textarea, label="Status Change Announcement", help_text="Edit the announcement message.", required=True, strip=False)
|
|
label = None
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(self.__class__, self).__init__(*args, **kwargs)
|
|
self.label = self.initial.get('label')
|
|
|
|
@role_required("Secretariat")
|
|
def approve(request, name):
|
|
"""Approve this status change, setting the appropriate state and send the announcements to the right parties."""
|
|
status_change = get_object_or_404(Document, type="statchg", name=name)
|
|
|
|
if status_change.get_state('statchg').slug not in ('appr-pend'):
|
|
raise Http404
|
|
|
|
login = request.user.person
|
|
|
|
AnnouncementFormSet = formset_factory(AnnouncementForm,extra=0)
|
|
|
|
if request.method == 'POST':
|
|
|
|
formset = AnnouncementFormSet(request.POST)
|
|
|
|
if formset.is_valid():
|
|
|
|
prev_state = status_change.get_state()
|
|
new_state = State.objects.get(type='statchg', slug='appr-sent')
|
|
|
|
status_change.set_state(new_state)
|
|
|
|
events = []
|
|
events.append(add_state_change_event(status_change, login, prev_state, new_state))
|
|
|
|
close_open_ballots(status_change, login)
|
|
|
|
e = DocEvent(doc=status_change, rev=status_change.rev, by=login)
|
|
e.type = "iesg_approved"
|
|
e.desc = "IESG has approved the status change"
|
|
e.save()
|
|
events.append(e)
|
|
|
|
status_change.save_with_history(events)
|
|
|
|
|
|
for form in formset.forms:
|
|
|
|
send_mail_preformatted(request, form.cleaned_data['announcement_text'], extra={})
|
|
|
|
c = DocEvent(type="added_comment", doc=status_change, rev=status_change.rev, by=login)
|
|
c.desc = "The following approval message was sent\n"+form.cleaned_data['announcement_text']
|
|
c.save()
|
|
|
|
for rel in status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS):
|
|
# Add a document event to each target
|
|
c = DocEvent(type="added_comment", doc=rel.target.document, rev=rel.target.document.rev, by=login)
|
|
c.desc = "New status of %s approved by the IESG\n%s%s" % (newstatus(rel), settings.IDTRACKER_BASE_URL,reverse('ietf.doc.views_doc.document_main', kwargs={'name': status_change.name}))
|
|
c.save()
|
|
|
|
return HttpResponseRedirect(status_change.get_absolute_url())
|
|
|
|
else:
|
|
|
|
init = []
|
|
for rel in status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS):
|
|
init.append({"announcement_text" : escape(default_approval_text(status_change,rel)),
|
|
"label": "Announcement text for %s to %s"%(rel.target.document.canonical_name(),newstatus(rel)),
|
|
})
|
|
formset = AnnouncementFormSet(initial=init)
|
|
for form in formset.forms:
|
|
form.fields['announcement_text'].label=form.label
|
|
|
|
return render(request, 'doc/status_change/approve.html',
|
|
dict(
|
|
doc = status_change,
|
|
formset = formset,
|
|
))
|
|
|
|
def clean_helper(form, formtype):
|
|
cleaned_data = super(formtype, form).clean()
|
|
|
|
new_relations = {}
|
|
rfc_fields = {}
|
|
status_fields={}
|
|
for k in sorted(form.data.keys()):
|
|
v = form.data[k].lower()
|
|
if k.startswith('new_relation_row'):
|
|
if re.match(r'\d{1,4}',v):
|
|
v = 'rfc'+v
|
|
rfc_fields[k[17:]]=v
|
|
elif k.startswith('statchg_relation_row'):
|
|
status_fields[k[21:]]=v
|
|
for key in rfc_fields:
|
|
if rfc_fields[key]!="":
|
|
if key in status_fields:
|
|
new_relations[rfc_fields[key]]=status_fields[key]
|
|
else:
|
|
new_relations[rfc_fields[key]]=None
|
|
|
|
form.relations = new_relations
|
|
|
|
errors=[]
|
|
for key in new_relations:
|
|
|
|
if not re.match(r'(?i)rfc\d{1,4}',key):
|
|
errors.append(key+" is not a valid RFC - please use the form RFCn\n")
|
|
elif not DocAlias.objects.filter(name=key):
|
|
errors.append(key+" does not exist\n")
|
|
|
|
if new_relations[key] not in STATUSCHANGE_RELATIONS:
|
|
errors.append("Please choose a new status level for "+key+"\n")
|
|
|
|
if errors:
|
|
raise forms.ValidationError(errors)
|
|
|
|
cleaned_data['relations']=new_relations
|
|
|
|
return cleaned_data
|
|
|
|
class EditStatusChangeForm(forms.Form):
|
|
relations={} # type: Dict[str, str]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(self.__class__, self).__init__(*args, **kwargs)
|
|
self.relations = self.initial.get('relations')
|
|
|
|
def clean(self):
|
|
return clean_helper(self,EditStatusChangeForm)
|
|
|
|
class StartStatusChangeForm(forms.Form):
|
|
document_name = forms.CharField(max_length=255, label="Document name", help_text="A descriptive name such as status-change-md2-to-historic is better than status-change-rfc1319.", required=True)
|
|
title = forms.CharField(max_length=255, label="Title", required=True)
|
|
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active",role__group__type='area').order_by('name'),
|
|
label="Shepherding AD", empty_label="(None)", required=False)
|
|
create_in_state = forms.ModelChoiceField(State.objects.filter(type="statchg", slug__in=("needshep", "adrev")), empty_label=None, required=False)
|
|
notify = forms.CharField(
|
|
widget=forms.Textarea,
|
|
max_length=1023,
|
|
label="Notice emails",
|
|
help_text="Separate email addresses with commas.",
|
|
required=False,
|
|
)
|
|
telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False, widget=forms.Select(attrs={'onchange':'make_bold()'}))
|
|
relations={} # type: Dict[str, str]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(self.__class__, self).__init__(*args, **kwargs)
|
|
self.relations = self.initial.get('relations')
|
|
|
|
# telechat choices
|
|
dates = [d.date for d in TelechatDate.objects.active().order_by('date')]
|
|
self.fields['telechat_date'].choices = [("", "(not on agenda)")] + [(d, d.strftime("%Y-%m-%d")) for d in dates]
|
|
|
|
def clean_document_name(self):
|
|
name = self.cleaned_data['document_name']
|
|
errors=[]
|
|
if re.search("[^a-z0-9-]", name):
|
|
errors.append("The name of the document may only contain digits, lowercase letters and dashes")
|
|
if re.search("--", name):
|
|
errors.append("Please do not put more than one hyphen between any two words in the name")
|
|
if name.startswith('status-change'):
|
|
errors.append("status-change- will be added automatically as a prefix")
|
|
if name.startswith('-'):
|
|
errors.append("status-change- will be added automatically as a prefix, starting with a - will result in status-change-%s"%name)
|
|
if re.search("-[0-9]{2}$", name):
|
|
errors.append("This name looks like ends in a version number. -00 will be added automatically. Please adjust the end of the name.")
|
|
if Document.objects.filter(name='status-change-%s'%name):
|
|
errors.append("status-change-%s already exists"%name)
|
|
if name.endswith('CHANGETHIS'):
|
|
errors.append("Please change CHANGETHIS to reflect the intent of this status change")
|
|
if errors:
|
|
raise forms.ValidationError(errors)
|
|
return name
|
|
|
|
def clean_title(self):
|
|
title = self.cleaned_data['title']
|
|
errors=[]
|
|
if title.endswith('CHANGETHIS'):
|
|
errors.append("Please change CHANGETHIS to reflect the intent of this status change")
|
|
if errors:
|
|
raise forms.ValidationError(errors)
|
|
return title
|
|
|
|
def clean(self):
|
|
return clean_helper(self,StartStatusChangeForm)
|
|
|
|
def rfc_status_changes(request):
|
|
"""Show the rfc status changes that are under consideration, and those that are completed."""
|
|
|
|
docs=Document.objects.filter(type__slug='statchg')
|
|
doclist=[x for x in docs]
|
|
doclist.sort(key=lambda obj: obj.get_state().order)
|
|
return render(request, 'doc/status_change/status_changes.html',
|
|
{'docs' : doclist,
|
|
},
|
|
)
|
|
|
|
@role_required("Area Director","Secretariat")
|
|
def start_rfc_status_change(request, name=None):
|
|
"""Start the RFC status change review process, setting the initial shepherding AD, and possibly putting the review on a telechat."""
|
|
|
|
if name:
|
|
if not re.match("(?i)rfc[0-9]{1,4}",name):
|
|
raise Http404
|
|
seed_rfc = get_object_or_404(Document, type="draft", docalias__name=name)
|
|
|
|
login = request.user.person
|
|
|
|
relation_slugs = DocRelationshipName.objects.filter(slug__in=STATUSCHANGE_RELATIONS)
|
|
|
|
if request.method == 'POST':
|
|
form = StartStatusChangeForm(request.POST)
|
|
if form.is_valid():
|
|
|
|
iesg_group = Group.objects.get(acronym='iesg')
|
|
|
|
status_change = Document.objects.create(
|
|
type_id="statchg",
|
|
name='status-change-'+form.cleaned_data['document_name'],
|
|
title=form.cleaned_data['title'],
|
|
rev="00",
|
|
ad=form.cleaned_data['ad'],
|
|
notify=form.cleaned_data['notify'],
|
|
stream_id='ietf',
|
|
group=iesg_group,
|
|
)
|
|
status_change.set_state(form.cleaned_data['create_in_state'])
|
|
|
|
DocAlias.objects.create( name= 'status-change-'+form.cleaned_data['document_name']).docs.add(status_change)
|
|
|
|
for key in form.cleaned_data['relations']:
|
|
status_change.relateddocument_set.create(target=DocAlias.objects.get(name=key),
|
|
relationship_id=form.cleaned_data['relations'][key])
|
|
|
|
|
|
tc_date = form.cleaned_data['telechat_date']
|
|
if tc_date:
|
|
update_telechat(request, status_change, login, tc_date)
|
|
|
|
return HttpResponseRedirect(status_change.get_absolute_url())
|
|
else:
|
|
init = {}
|
|
if name:
|
|
init['title'] = "%s to CHANGETHIS" % seed_rfc.title
|
|
init['document_name'] = "%s-to-CHANGETHIS" % seed_rfc.canonical_name()
|
|
relations={}
|
|
relations[seed_rfc.canonical_name()]=None
|
|
init['relations'] = relations
|
|
form = StartStatusChangeForm(initial=init)
|
|
|
|
return render(request, 'doc/status_change/start.html',
|
|
{'form': form,
|
|
'relation_slugs': relation_slugs,
|
|
},
|
|
)
|
|
|
|
@role_required("Area Director", "Secretariat")
|
|
def edit_relations(request, name):
|
|
"""Change the affected set of RFCs"""
|
|
|
|
status_change = get_object_or_404(Document, type="statchg", name=name)
|
|
|
|
login = request.user.person
|
|
|
|
relation_slugs = DocRelationshipName.objects.filter(slug__in=STATUSCHANGE_RELATIONS)
|
|
|
|
if request.method == 'POST':
|
|
form = EditStatusChangeForm(request.POST)
|
|
if form.is_valid():
|
|
|
|
old_relations={}
|
|
for rel in status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS):
|
|
old_relations[rel.target.document.canonical_name()]=rel.relationship.slug
|
|
new_relations=form.cleaned_data['relations']
|
|
status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS).delete()
|
|
for key in new_relations:
|
|
status_change.relateddocument_set.create(target=DocAlias.objects.get(name=key),
|
|
relationship_id=new_relations[key])
|
|
c = DocEvent(type="added_comment", doc=status_change, rev=status_change.rev, by=login)
|
|
c.desc = "Affected RFC list changed.\nOLD:"
|
|
for relname,relslug in (set(old_relations.items())-set(new_relations.items())):
|
|
c.desc += "\n "+relname+": "+DocRelationshipName.objects.get(slug=relslug).name
|
|
c.desc += "\nNEW:"
|
|
for relname,relslug in (set(new_relations.items())-set(old_relations.items())):
|
|
c.desc += "\n "+relname+": "+DocRelationshipName.objects.get(slug=relslug).name
|
|
c.desc += "\n"
|
|
c.save()
|
|
|
|
return HttpResponseRedirect(status_change.get_absolute_url())
|
|
|
|
else:
|
|
relations={}
|
|
for rel in status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS):
|
|
relations[rel.target.document.canonical_name()]=rel.relationship.slug
|
|
init = { "relations":relations,
|
|
}
|
|
form = EditStatusChangeForm(initial=init)
|
|
|
|
return render(request, 'doc/status_change/edit_relations.html',
|
|
{
|
|
'doc': status_change,
|
|
'form': form,
|
|
'relation_slugs': relation_slugs,
|
|
},
|
|
)
|
|
|
|
def generate_last_call_text(request, doc):
|
|
|
|
# requester should be set based on doc.group once the group for a status change can be set to something other than the IESG
|
|
# and when groups are set, vary the expiration time accordingly
|
|
|
|
requester = "an individual participant"
|
|
expiration_date = date_today(DEADLINE_TZINFO) + datetime.timedelta(days=28)
|
|
cc = []
|
|
|
|
new_text = render_to_string("doc/status_change/last_call_announcement.txt",
|
|
dict(doc=doc,
|
|
settings=settings,
|
|
requester=requester,
|
|
expiration_date=expiration_date.strftime("%Y-%m-%d"),
|
|
changes=['%s from %s to %s\n (%s)'%(rel.target.name.upper(),rel.target.document.std_level.name,newstatus(rel),rel.target.document.title) for rel in doc.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS)],
|
|
urls=[rel.target.document.get_absolute_url() for rel in doc.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS)],
|
|
cc=cc
|
|
)
|
|
)
|
|
|
|
e = WriteupDocEvent()
|
|
e.type = 'changed_last_call_text'
|
|
e.by = request.user.person
|
|
e.doc = doc
|
|
e.rev = doc.rev
|
|
e.desc = 'Last call announcement was generated'
|
|
e.text = force_str(new_text)
|
|
e.save()
|
|
|
|
return e
|
|
|
|
@role_required("Area Director", "Secretariat")
|
|
def last_call(request, name):
|
|
"""Edit the Last Call Text for this status change and possibly request IETF LC"""
|
|
|
|
status_change = get_object_or_404(Document, type="statchg", name=name)
|
|
|
|
login = request.user.person
|
|
|
|
last_call_event = status_change.latest_event(WriteupDocEvent, type="changed_last_call_text")
|
|
if not last_call_event:
|
|
last_call_event = generate_last_call_text(request, status_change)
|
|
|
|
form = LastCallTextForm(initial=dict(last_call_text=escape(last_call_event.text)))
|
|
|
|
if request.method == 'POST':
|
|
if "save_last_call_text" in request.POST or ("send_last_call_request" in request.POST and status_change.ad is not None):
|
|
form = LastCallTextForm(request.POST)
|
|
if form.is_valid():
|
|
events = []
|
|
|
|
t = form.cleaned_data['last_call_text']
|
|
if t != last_call_event.text:
|
|
e = WriteupDocEvent(doc=status_change, rev=status_change.rev, by=login)
|
|
e.by = login
|
|
e.type = "changed_last_call_text"
|
|
e.desc = "Last call announcement was changed"
|
|
e.text = t
|
|
e.save()
|
|
|
|
events.append(e)
|
|
|
|
if "send_last_call_request" in request.POST:
|
|
prev_state = status_change.get_state()
|
|
new_state = State.objects.get(type='statchg', slug='lc-req')
|
|
|
|
status_change.set_state(new_state)
|
|
e = add_state_change_event(status_change, login, prev_state, new_state)
|
|
if e:
|
|
events.append(e)
|
|
|
|
if events:
|
|
status_change.save_with_history(events)
|
|
|
|
request_last_call(request, status_change)
|
|
|
|
return render(request, 'doc/draft/last_call_requested.html',
|
|
dict(doc=status_change,
|
|
url = status_change.get_absolute_url(),
|
|
)
|
|
)
|
|
|
|
if "regenerate_last_call_text" in request.POST:
|
|
e = generate_last_call_text(request,status_change)
|
|
form = LastCallTextForm(initial=dict(last_call_text=escape(e.text)))
|
|
|
|
return render(request, 'doc/status_change/last_call.html',
|
|
dict(doc=status_change,
|
|
back_url = status_change.get_absolute_url(),
|
|
last_call_event = last_call_event,
|
|
last_call_form = form,
|
|
),
|
|
)
|
|
|