Manually moved the concepts forward. Reworked some presentation. Working enough to bring testers in at Sprint93

- Legacy-Id: 9717
This commit is contained in:
Robert Sparks 2015-07-13 21:09:39 +00:00
parent 61474a4988
commit 02a9da52a1
22 changed files with 519 additions and 150 deletions

View file

@ -4,6 +4,8 @@ from django.utils.html import escape
from django import forms
from django.core.urlresolvers import reverse as urlreverse
import debug # pyflakes:ignore
from ietf.doc.models import Document, DocAlias
from ietf.doc.utils import uppercase_std_abbreviated_name

View file

@ -488,3 +488,26 @@ def email_stream_tags_changed(request, doc, added_tags, removed_tags, by, commen
by=by,
comment=comment))
def send_review_possibly_replaces_request(request, doc):
to_email = []
if doc.stream_id == "ietf":
to_email.extend(r.formatted_email() for r in Role.objects.filter(group=doc.group, name="chair").select_related("email", "person"))
elif doc.stream_id == "iab":
to_email.append("IAB Stream <iab-stream@iab.org>")
elif doc.stream_id == "ise":
to_email.append("Independent Submission Editor <rfc-ise@rfc-editor.org>")
elif doc.stream_id == "irtf":
to_email.append("IRSG <irsg@irtf.org>")
if not to_email:
to_email.append("internet-drafts@ietf.org")
if to_email:
send_mail(request, to_email, settings.DEFAULT_FROM_EMAIL,
'Review of suggested possible replacements for %s-%s needed' % (doc.name, doc.rev),
'doc/mail/review_possibly_replaces_request.txt', {
'doc': doc,
'possibly_replaces': doc.related_that_doc("possibly-replaces"),
'review_url': settings.IDTRACKER_BASE_URL + urlreverse("doc_review_possibly_replaces", kwargs={ "name": doc.name }),
})

View file

@ -654,6 +654,8 @@ EVENT_TYPES = [
("completed_resurrect", "Completed resurrect"),
("changed_consensus", "Changed consensus"),
("published_rfc", "Published RFC"),
("added_suggested_replaces", "Added suggested replacement relationships"),
("reviewed_suggested_replaces", "Reviewed suggested replacement relationships"),
# WG events
("changed_group", "Changed group"),

View file

@ -11,7 +11,7 @@ import debug # pyflakes:ignore
from ietf.doc.models import ( Document, DocAlias, DocReminder, DocumentAuthor, DocEvent,
ConsensusDocEvent, LastCallDocEvent, RelatedDocument, State, TelechatDocEvent,
WriteupDocEvent, BallotDocEvent)
WriteupDocEvent, BallotDocEvent, DocRelationshipName)
from ietf.doc.utils import get_tags_for_stream_id
from ietf.name.models import StreamName, IntendedStdLevelName, DocTagName
from ietf.group.models import Group
@ -1255,11 +1255,14 @@ class ChangeReplacesTests(TestCase):
self.assertEqual(len(q('[type=submit]:contains("Save")')), 1)
# Post that says replacea replaces base a
RelatedDocument.objects.create(source=self.replacea, target=self.basea.docalias_set.first(),
relationship=DocRelationshipName.objects.get(slug="possibly-replaces"))
self.assertEqual(self.basea.get_state().slug,'active')
r = self.client.post(url, dict(replaces=str(DocAlias.objects.get(name=self.basea.name).id)))
self.assertEqual(r.status_code, 302)
self.assertEqual(RelatedDocument.objects.filter(relationship__slug='replaces',source=self.replacea).count(),1)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl')
self.assertTrue(not RelatedDocument.objects.filter(relationship='possibly-replaces', source=self.replacea))
# Post that says replaceboth replaces both base a and base b
url = urlreverse('doc_change_replaces', kwargs=dict(name=self.replaceboth.name))
@ -1281,3 +1284,21 @@ class ChangeReplacesTests(TestCase):
self.assertEqual(r.status_code, 302)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'active')
def test_review_possibly_replaces(self):
replaced = self.basea.docalias_set.first()
RelatedDocument.objects.create(source=self.replacea, target=replaced,
relationship=DocRelationshipName.objects.get(slug="possibly-replaces"))
url = urlreverse('doc_review_possibly_replaces', kwargs=dict(name=self.replacea.name))
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEquals(r.status_code, 200)
q = PyQuery(r.content)
self.assertEquals(len(q('form[name=review-suggested-replaces]')), 1)
r = self.client.post(url, dict(replaces=[replaced.pk]))
self.assertEquals(r.status_code, 302)
self.assertTrue(not self.replacea.related_that_doc("possibly-replaces"))
self.assertEqual(len(self.replacea.related_that_doc("replaces")), 1)
self.assertEquals(Document.objects.get(pk=self.basea.pk).get_state().slug, 'repl')

View file

@ -79,6 +79,7 @@ urlpatterns = patterns('',
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/stream/$', views_draft.change_stream, name='doc_change_stream'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/replaces/$', views_draft.replaces, name='doc_change_replaces'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/notify/$', views_doc.edit_notify, name='doc_change_notify'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/suggested-replaces/$', views_draft.review_possibly_replaces, name='doc_review_possibly_replaces'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/status/$', views_draft.change_intention, name='doc_change_intended_status'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/telechat/$', views_doc.telechat_date, name='doc_change_telechat_date'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/iesgnote/$', views_draft.edit_iesg_note, name='doc_change_iesg_note'),

View file

@ -10,8 +10,7 @@ from django.db.models.query import EmptyQuerySet
from django.forms import ValidationError
from django.utils.html import strip_tags, escape
from ietf.utils import markup_txt
from ietf.doc.models import Document, DocHistory
from ietf.doc.models import Document, DocHistory, State
from ietf.doc.models import DocAlias, RelatedDocument, BallotType, DocReminder
from ietf.doc.models import DocEvent, BallotDocEvent, NewRevisionDocEvent, StateDocEvent
from ietf.doc.models import save_document_in_history, STATUSCHANGE_RELATIONS
@ -19,7 +18,7 @@ from ietf.name.models import DocReminderTypeName, DocRelationshipName
from ietf.group.models import Role
from ietf.person.models import Email
from ietf.ietfauth.utils import has_role
from ietf.utils import draft
from ietf.utils import draft, markup_txt
from ietf.utils.mail import send_mail
#FIXME - it would be better if this lived in ietf/doc/mails.py, but there's
@ -314,9 +313,9 @@ def update_reminder(doc, reminder_type_slug, event, due_date):
reminder.active = False
reminder.save()
def prettify_std_name(n):
def prettify_std_name(n, spacing=" "):
if re.match(r"(rfc|bcp|fyi|std)[0-9]+", n):
return n[:3].upper() + " " + n[3:]
return n[:3].upper() + spacing + n[3:]
else:
return n
@ -459,6 +458,75 @@ def rebuild_reference_relations(doc,filename=None):
return ret
def collect_email_addresses(emails, doc):
for author in doc.authors.all():
if author.address not in emails:
emails[author.address] = '"%s"' % (author.person.name)
if doc.group and doc.group.acronym != 'none':
for role in doc.group.role_set.filter(name='chair'):
if role.email.address not in emails:
emails[role.email.address] = '"%s"' % (role.person.name)
if doc.group.type.slug == 'wg':
address = '%s-ads@tools.ietf.org' % doc.group.acronym
if address not in emails:
emails[address] = '"%s-ads"' % (doc.group.acronym)
elif doc.group.type.slug == 'rg':
for role in doc.group.parent.role_set.filter(name='chair'):
if role.email.address not in emails:
emails[role.email.address] = '"%s"' % (role.person.name)
if doc.shepherd and doc.shepherd.address not in emails:
emails[doc.shepherd.address] = u'"%s"' % (doc.shepherd.person.name or "")
def set_replaces_for_document(request, doc, new_replaces, by, email_subject, email_comment=""):
emails = {}
collect_email_addresses(emails, doc)
relationship = DocRelationshipName.objects.get(slug='replaces')
old_replaces = doc.related_that_doc("replaces")
for d in old_replaces:
if d not in new_replaces:
collect_email_addresses(emails, d.document)
RelatedDocument.objects.filter(source=doc, target=d, relationship=relationship).delete()
if not RelatedDocument.objects.filter(target=d, relationship=relationship):
s = 'active' if d.document.expires > datetime.datetime.now() else 'expired'
d.document.set_state(State.objects.get(type='draft', slug=s))
for d in new_replaces:
if d not in old_replaces:
collect_email_addresses(emails, d.document)
RelatedDocument.objects.create(source=doc, target=d, relationship=relationship)
d.document.set_state(State.objects.get(type='draft', slug='repl'))
e = DocEvent(doc=doc, by=by, type='changed_document')
new_replaces_names = u", ".join(d.name for d in new_replaces) or u"None"
old_replaces_names = u", ".join(d.name for d in old_replaces) or u"None"
e.desc = u"This document now replaces <b>%s</b> instead of %s" % (new_replaces_names, old_replaces_names)
e.save()
# make sure there are no lingering suggestions duplicating new replacements
RelatedDocument.objects.filter(source=doc, target__in=new_replaces, relationship="possibly-replaces").delete()
email_desc = e.desc.replace(", ", "\n ")
if email_comment:
email_desc += "\n" + email_comment
to = [
u'%s <%s>' % (emails[email], email) if emails[email] else u'<%s>' % email
for email in sorted(emails)
]
from ietf.doc.mails import html_to_text
send_mail(request, to,
"DraftTracker Mail System <iesg-secretary@ietf.org>",
email_subject,
"doc/mail/change_notice.txt",
dict(text=html_to_text(email_desc),
doc=doc,
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()))
def check_common_doc_name_rules(name):
"""Check common rules for document names for use in forms, throws
ValidationError in case there's a problem."""

View file

@ -345,6 +345,8 @@ def document_main(request, name, rev=None):
replaces = [d.name for d in doc.related_that_doc("replaces")]
replaced_by = [d.name for d in doc.related_that("replaces")]
possibly_replaces = [d.name for d in doc.related_that_doc("possibly-replaces")]
possibly_replaced_by = [d.name for d in doc.related_that("possibly-replaces")]
published = doc.latest_event(type="published_rfc")
started_iesg_process = doc.latest_event(type="started_iesg_process")
@ -355,6 +357,8 @@ def document_main(request, name, rev=None):
table_rows = dict(doc=4, stream=2, iesg=4, iana=2, rfced=1)
table_rows['doc'] += 1 if replaces or can_edit_stream_info else 0
table_rows['doc'] += 1 if replaced_by else 0
table_rows['doc'] += 1 if possibly_replaces else 0
table_rows['doc'] += 1 if possibly_replaced_by else 0
table_rows['doc'] += 1 if doc.get_state_slug() != "rfc" else 0
table_rows['doc'] += 1 if conflict_reviews else 0
@ -396,6 +400,8 @@ def document_main(request, name, rev=None):
replaces=replaces,
replaced_by=replaced_by,
possibly_replaces=possibly_replaces,
possibly_replaced_by=possibly_replaced_by,
updates=[prettify_std_name(d.name) for d in doc.related_that_doc("updates")],
updated_by=[prettify_std_name(d.document.canonical_name()) for d in doc.related_that("updates")],
obsoletes=[prettify_std_name(d.name) for d in doc.related_that_doc("obs")],

View file

@ -13,16 +13,17 @@ from django.contrib.auth.decorators import login_required
from django.template.defaultfilters import pluralize
from django.contrib import messages
from ietf.doc.models import ( Document, DocAlias, DocRelationshipName, RelatedDocument, State,
from ietf.doc.models import ( Document, DocAlias, RelatedDocument, State,
StateType, DocEvent, ConsensusDocEvent, TelechatDocEvent, WriteupDocEvent, IESG_SUBSTATE_TAGS,
save_document_in_history )
from ietf.doc.mails import ( email_ad, email_pulled_from_rfc_queue, email_resurrect_requested,
email_resurrection_completed, email_state_changed, email_stream_changed,
email_stream_state_changed, email_stream_tags_changed, extra_automation_headers,
generate_publication_request, html_to_text )
generate_publication_request )
from ietf.doc.utils import ( add_state_change_event, can_adopt_draft,
get_tags_for_stream_id, nice_consensus,
update_reminder, update_telechat, make_notify_changed_event, get_initial_notify )
update_reminder, update_telechat, make_notify_changed_event, get_initial_notify,
set_replaces_for_document )
from ietf.doc.lastcall import request_last_call
from ietf.doc.fields import SearchableDocAliasesField
from ietf.group.models import Group, Role
@ -287,26 +288,6 @@ def doc_ajax_internet_draft(request):
response = [dict(id=r.id, label=r.name) for r in results]
return response
def collect_email_addresses(emails, doc):
for author in doc.authors.all():
if author.address not in emails:
emails[author.address] = '"%s"' % (author.person.name)
if doc.group.acronym != 'none':
for role in doc.group.role_set.filter(name='chair'):
if role.email.address not in emails:
emails[role.email.address] = '"%s"' % (role.person.name)
if doc.group.type.slug == 'wg':
address = '%s-ads@tools.ietf.org' % doc.group.acronym
if address not in emails:
emails[address] = '"%s-ads"' % (doc.group.acronym)
elif doc.group.type.slug == 'rg':
for role in doc.group.parent.role_set.filter(name='chair'):
if role.email.address not in emails:
emails[role.email.address] = '"%s"' % (role.person.name)
if doc.shepherd and doc.shepherd.address not in emails:
emails[doc.shepherd.address] = u'"%s"' % (doc.shepherd.person.name or "")
return emails
class ReplacesForm(forms.Form):
replaces = SearchableDocAliasesField(required=False)
comment = forms.CharField(widget=forms.Textarea, required=False)
@ -333,68 +314,96 @@ def replaces(request, name):
if not (has_role(request.user, ("Secretariat", "Area Director"))
or is_authorized_in_doc_stream(request.user, doc)):
return HttpResponseForbidden("You do not have the necessary permissions to view this page")
login = request.user.person
if request.method == 'POST':
form = ReplacesForm(request.POST, doc=doc)
if form.is_valid():
new_replaces = set(form.cleaned_data['replaces'])
comment = form.cleaned_data['comment'].strip()
old_replaces = set(doc.related_that_doc("replaces"))
by = request.user.person
if new_replaces != old_replaces:
save_document_in_history(doc)
emails = {}
emails = collect_email_addresses(emails, doc)
relationship = DocRelationshipName.objects.get(slug='replaces')
for d in old_replaces:
if d not in new_replaces:
emails = collect_email_addresses(emails, d.document)
RelatedDocument.objects.filter(source=doc, target=d, relationship=relationship).delete()
if not RelatedDocument.objects.filter(target=d, relationship=relationship):
d.document.set_state(State.objects.get(type='draft',slug='active' if d.document.expires>datetime.datetime.now() else 'expired'))
for d in new_replaces:
if d not in old_replaces:
emails = collect_email_addresses(emails, d.document)
RelatedDocument.objects.create(source=doc, target=d, relationship=relationship)
d.document.set_state(State.objects.get(type='draft',slug='repl'))
e = DocEvent(doc=doc,by=login,type='changed_document')
new_replaces_names = ", ".join([d.name for d in new_replaces])
if not new_replaces_names:
new_replaces_names = "None"
old_replaces_names = ", ".join([d.name for d in old_replaces])
if not old_replaces_names:
old_replaces_names = "None"
e.desc = u"This document now replaces <b>%s</b> instead of %s"% (new_replaces_names, old_replaces_names)
e.save()
email_desc = e.desc.replace(", ", "\n ")
if comment:
c = DocEvent(doc=doc,by=login,type="added_comment")
c.desc = comment
c.save()
email_desc += "\n"+c.desc
doc.time = e.time
doc.time = datetime.datetime.now()
doc.save()
email_list = []
for key in sorted(emails):
if emails[key]:
email_list.append('%s <%s>' % (emails[key], key))
else:
email_list.append('<%s>' % key)
email_string = ", ".join(email_list)
send_mail(request, email_string,
"DraftTracker Mail System <iesg-secretary@ietf.org>",
"%s updated by %s" % (doc.name, login),
"doc/mail/change_notice.txt",
dict(text=html_to_text(email_desc),
doc=doc,
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()))
set_replaces_for_document(request, doc, new_replaces, by=by,
email_subject="%s replacement status updated by %s" % (doc.name, by),
email_comment=comment)
if comment:
DocEvent.objects.create(doc=doc, by=by, type="added_comment", desc=comment)
return HttpResponseRedirect(doc.get_absolute_url())
else:
form = ReplacesForm(doc=doc)
return render_to_response('doc/draft/change_replaces.html',
dict(form=form,
doc=doc,
),
context_instance=RequestContext(request))
return render(request, 'doc/draft/change_replaces.html',
dict(form=form,
doc=doc,
))
class SuggestedReplacesForm(forms.Form):
replaces = forms.ModelMultipleChoiceField(queryset=DocAlias.objects.all(),
label="Suggestions", required=False, widget=forms.CheckboxSelectMultiple,
help_text="Select only the documents that are replaced by this document")
comment = forms.CharField(label="Optional comment", widget=forms.Textarea, required=False)
def __init__(self, suggested, *args, **kwargs):
super(SuggestedReplacesForm, self).__init__(*args, **kwargs)
pks = [d.pk for d in suggested]
self.fields["replaces"].initial = pks
self.fields["replaces"].queryset = self.fields["replaces"].queryset.filter(pk__in=pks)
self.fields["replaces"].choices = [(d.pk, d.name) for d in suggested]
def review_possibly_replaces(request, name):
doc = get_object_or_404(Document, docalias__name=name)
if doc.type_id != 'draft':
raise Http404
if not (has_role(request.user, ("Secretariat", "Area Director"))
or is_authorized_in_doc_stream(request.user, doc)):
return HttpResponseForbidden("You do not have the necessary permissions to view this page")
suggested = list(doc.related_that_doc("possibly-replaces"))
if not suggested:
raise Http404
if request.method == 'POST':
form = SuggestedReplacesForm(suggested, request.POST)
if form.is_valid():
replaces = set(form.cleaned_data['replaces'])
old_replaces = set(doc.related_that_doc("replaces"))
new_replaces = old_replaces.union(replaces)
comment = form.cleaned_data['comment'].strip()
by = request.user.person
save_document_in_history(doc)
doc.time = datetime.datetime.now()
doc.save()
# all suggestions reviewed, so get rid of them
DocEvent.objects.create(doc=doc, by=by, type="reviewed_suggested_replaces",
desc="Reviewed suggested replacement relationships: %s" % ", ".join(d.name for d in suggested))
RelatedDocument.objects.filter(source=doc, target__in=suggested,relationship__slug='possibly-replaces').delete()
if new_replaces != old_replaces:
set_replaces_for_document(request, doc, new_replaces, by=by,
email_subject="%s replacement status updated by %s" % (doc.name, by),
email_comment=comment)
if comment:
DocEvent.objects.create(doc=doc, by=by, type="added_comment", desc=comment)
return HttpResponseRedirect(doc.get_absolute_url())
else:
form = SuggestedReplacesForm(suggested)
return render(request, 'doc/draft/review_possibly_replaces.html',
dict(form=form,
doc=doc,
))
class ChangeIntentionForm(forms.Form):
intended_std_level = forms.ModelChoiceField(IntendedStdLevelName.objects.filter(used=True), empty_label="(None)", required=True, label="Intended RFC status")

View file

@ -293,6 +293,17 @@
"model": "name.docrelationshipname",
"pk": "toexp"
},
{
"fields": {
"order": 0,
"revname": "Possibly Replaced By",
"used": true,
"name": "Possibly Replaces",
"desc": ""
},
"model": "name.docrelationshipname",
"pk": "possibly-replaces"
},
{
"fields": {
"order": 3,

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def add_possibly_replaces(apps, schema_editor):
DocRelationshipName = apps.get_model("name","DocRelationshipName")
DocRelationshipName.objects.create(slug='possibly-replaces',name='Possibly Replaces',revname='Possibly Replaced By')
class Migration(migrations.Migration):
dependencies = [
('name', '0004_auto_20150318_1140'),
]
operations = [
migrations.RunPython(add_possibly_replaces)
]

View file

@ -11,6 +11,7 @@ import debug # pyflakes:ignore
from ietf.group.models import Group
from ietf.doc.models import Document
from ietf.doc.fields import SearchableDocAliasesField
from ietf.meeting.models import Meeting
from ietf.submit.models import Submission, Preapproval
from ietf.submit.utils import validate_submission_rev, validate_submission_document_date
@ -248,6 +249,9 @@ class NameEmailForm(forms.Form):
line += u" <%s>" % email
return line
class ReplacesForm(forms.Form):
replaces = SearchableDocAliasesField(required=False, help_text="Any drafts that this document replaces (approval required for replacing a draft you are not the author of)")
class EditSubmissionForm(forms.ModelForm):
title = forms.CharField(required=True, max_length=255)
rev = forms.CharField(label=u'Revision', max_length=2, required=True)

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('submit', '0002_auto_20150430_0847'),
]
operations = [
migrations.AlterField(
model_name='submission',
name='replaces',
field=models.CharField(max_length=1000, blank=True),
preserve_default=True,
),
]

View file

@ -33,7 +33,7 @@ class Submission(models.Model):
pages = models.IntegerField(null=True, blank=True)
authors = models.TextField(blank=True, help_text="List of author names and emails, one author per line, e.g. \"John Doe &lt;john@example.org&gt;\".")
note = models.TextField(blank=True)
replaces = models.CharField(max_length=255, blank=True)
replaces = models.CharField(max_length=1000, blank=True)
first_two_pages = models.TextField(blank=True)
file_types = models.CharField(max_length=50, blank=True)

View file

@ -16,7 +16,7 @@ from ietf.utils.test_utils import TestCase
from ietf.submit.utils import expirable_submissions, expire_submission, ensure_person_email_info_exists
from ietf.person.models import Person
from ietf.group.models import Group
from ietf.doc.models import Document, DocEvent, State, BallotDocEvent, BallotPositionDocEvent, DocumentAuthor
from ietf.doc.models import Document, DocAlias, DocEvent, State, BallotDocEvent, BallotPositionDocEvent, DocumentAuthor
from ietf.submit.models import Submission, Preapproval
class SubmitTests(TestCase):
@ -85,7 +85,7 @@ class SubmitTests(TestCase):
return status_url
def supply_submitter(self, name, status_url, submitter_name, submitter_email):
def supply_extra_metadata(self, name, status_url, submitter_name, submitter_email, replaces):
# check the page
r = self.client.get(status_url)
q = PyQuery(r.content)
@ -98,10 +98,12 @@ class SubmitTests(TestCase):
"action": action,
"submitter-name": submitter_name,
"submitter-email": submitter_email,
"replaces": replaces,
})
submission = Submission.objects.get(name=name)
self.assertEqual(submission.submitter, u"%s <%s>" % (submitter_name, submitter_email))
self.assertEqual(submission.replaces, ",".join(d.name for d in DocAlias.objects.filter(pk__in=replaces.split(",") if replaces else [])))
return r
@ -121,6 +123,27 @@ class SubmitTests(TestCase):
# submit new -> supply submitter info -> approve
draft = make_test_data()
# prepare draft to suggest replace
sug_replaced_draft = Document.objects.create(
name="draft-ietf-ames-sug-replaced",
time=datetime.datetime.now(),
type_id="draft",
title="Draft to be suggested to be replaced",
stream_id="ietf",
group=Group.objects.get(acronym="ames"),
abstract="Blahblahblah.",
rev="01",
pages=2,
intended_std_level_id="ps",
ad=draft.ad,
expires=datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
notify="aliens@example.mars",
note="",
)
sug_replaced_draft.set_state(State.objects.get(used=True, type="draft", slug="active"))
sug_replaced_alias = DocAlias.objects.create(document=sug_replaced_draft, name=sug_replaced_draft.name)
name = "draft-ietf-mars-testing-tests"
rev = "00"
@ -128,7 +151,9 @@ class SubmitTests(TestCase):
# supply submitter info, then draft should be in and ready for approval
mailbox_before = len(outbox)
r = self.supply_submitter(name, status_url, "Author Name", "author@example.com")
replaced_alias = draft.docalias_set.first()
r = self.supply_extra_metadata(name, status_url, "Author Name", "author@example.com",
replaces=str(replaced_alias.pk) + "," + str(sug_replaced_alias.pk))
self.assertEqual(r.status_code, 302)
status_url = r["Location"]
@ -154,10 +179,11 @@ class SubmitTests(TestCase):
draft = Document.objects.get(docalias__name=name)
self.assertEqual(draft.rev, rev)
new_revision = draft.latest_event()
new_revision = draft.latest_event(type="new_revision")
self.assertEqual(draft.group.acronym, "mars")
self.assertEqual(new_revision.type, "new_revision")
self.assertEqual(new_revision.by.name, "Author Name")
self.assertTrue(draft.latest_event(type="added_suggested_replaces"))
self.assertTrue(not os.path.exists(os.path.join(self.staging_dir, u"%s-%s.txt" % (name, rev))))
self.assertTrue(os.path.exists(os.path.join(self.repository_dir, u"%s-%s.txt" % (name, rev))))
self.assertEqual(draft.type_id, "draft")
@ -167,12 +193,19 @@ class SubmitTests(TestCase):
self.assertEqual(draft.authors.count(), 1)
self.assertEqual(draft.authors.all()[0].get_name(), "Author Name")
self.assertEqual(draft.authors.all()[0].address, "author@example.com")
self.assertEqual(len(outbox), mailbox_before + 2)
self.assertTrue((u"I-D Action: %s" % name) in outbox[-2]["Subject"])
self.assertTrue("Author Name" in unicode(outbox[-2]))
self.assertTrue("New Version Notification" in outbox[-1]["Subject"])
self.assertEqual(draft.relations_that_doc("replaces").count(), 1)
self.assertTrue(draft.relations_that_doc("replaces").first().target, replaced_alias)
self.assertEqual(draft.relations_that_doc("possibly-replaces").count(), 1)
self.assertTrue(draft.relations_that_doc("possibly-replaces").first().target, sug_replaced_alias)
self.assertEqual(len(outbox), mailbox_before + 4)
self.assertTrue((u"I-D Action: %s" % name) in outbox[-3]["Subject"])
self.assertTrue("Author Name" in unicode(outbox[-3]))
self.assertTrue("New Version Notification" in outbox[-2]["Subject"])
self.assertTrue(name in unicode(outbox[-2]))
self.assertTrue("mars" in unicode(outbox[-2]))
self.assertTrue("review" in outbox[-1]["Subject"].lower())
self.assertTrue(name in unicode(outbox[-1]))
self.assertTrue("mars" in unicode(outbox[-1]))
self.assertTrue(sug_replaced_alias.name in unicode(outbox[-1]))
def test_submit_existing(self):
# submit new revision of existing -> supply submitter info -> prev authors confirm
@ -214,7 +247,7 @@ class SubmitTests(TestCase):
# supply submitter info, then previous authors get a confirmation email
mailbox_before = len(outbox)
r = self.supply_submitter(name, status_url, "Submitter Name", "submitter@example.com")
r = self.supply_extra_metadata(name, status_url, "Submitter Name", "submitter@example.com", replaces="")
self.assertEqual(r.status_code, 302)
status_url = r["Location"]
r = self.client.get(status_url)
@ -284,7 +317,7 @@ class SubmitTests(TestCase):
# supply submitter info, then draft should be be ready for email auth
mailbox_before = len(outbox)
r = self.supply_submitter(name, status_url, "Submitter Name", "submitter@example.com")
r = self.supply_extra_metadata(name, status_url, "Submitter Name", "submitter@example.com", replaces="")
self.assertEqual(r.status_code, 302)
status_url = r["Location"]
@ -374,7 +407,7 @@ class SubmitTests(TestCase):
def test_edit_submission_and_force_post(self):
# submit -> edit
make_test_data()
draft = make_test_data()
name = "draft-ietf-mars-testing-tests"
rev = "00"
@ -412,6 +445,7 @@ class SubmitTests(TestCase):
"edit-pages": "123",
"submitter-name": "Some Random Test Person",
"submitter-email": "random@example.com",
"replaces": str(draft.docalias_set.all().first().pk),
"edit-note": "no comments",
"authors-0-name": "Person 1",
"authors-0-email": "person1@example.com",
@ -428,6 +462,7 @@ class SubmitTests(TestCase):
self.assertEqual(submission.pages, 123)
self.assertEqual(submission.note, "no comments")
self.assertEqual(submission.submitter, "Some Random Test Person <random@example.com>")
self.assertEqual(submission.replaces, draft.docalias_set.all().first().name)
self.assertEqual(submission.state_id, "manual")
authors = submission.authors_parsed()

View file

@ -4,8 +4,12 @@ import datetime
from django.conf import settings
from ietf.doc.models import Document, State, DocAlias, DocEvent, DocumentAuthor, NewRevisionDocEvent, save_document_in_history
from ietf.doc.models import Document, State, DocAlias, DocEvent, DocumentAuthor
from ietf.doc.models import NewRevisionDocEvent, save_document_in_history
from ietf.doc.models import RelatedDocument, DocRelationshipName
from ietf.doc.utils import add_state_change_event, rebuild_reference_relations
from ietf.doc.utils import set_replaces_for_document
from ietf.doc.mails import send_review_possibly_replaces_request
from ietf.group.models import Group
from ietf.ietfauth.utils import has_role
from ietf.name.models import StreamName
@ -202,12 +206,62 @@ def post_submission(request, submission):
move_files_to_repository(submission)
submission.state = DraftSubmissionStateName.objects.get(slug="posted")
new_replaces, new_possibly_replaces = update_replaces_from_submission(request, submission, draft)
announce_to_lists(request, submission)
announce_new_version(request, submission, draft, state_change_msg)
announce_to_authors(request, submission)
if new_possibly_replaces:
send_review_possibly_replaces_request(request, draft)
submission.save()
def update_replaces_from_submission(request, submission, draft):
if not submission.replaces:
return [], []
is_secretariat = has_role(request.user, "Secretariat")
is_chair_of = []
if request.user.is_authenticated():
is_chair_of = list(Group.objects.filter(role__person__user=request.user, role__name="chair"))
replaces = DocAlias.objects.filter(name__in=submission.replaces.split(",")).select_related("document", "document__group")
existing_replaces = list(draft.related_that_doc("replaces"))
existing_suggested = set(draft.related_that_doc("possibly-replaces"))
submitter_email = submission.submitter_parsed()["email"]
approved = []
suggested = []
for r in replaces:
if r in existing_replaces:
continue
rdoc = r.document
if (is_secretariat
or (draft.group in is_chair_of and (rdoc.group.type_id == "individ" or rdoc.group in is_chair_of))
or (submitter_email and rdoc.authors.filter(address__iexact=submitter_email)).exists()):
approved.append(r)
else:
if r not in existing_suggested:
suggested.append(r)
by = request.user.person if request.user.is_authenticated() else Person.objects.get(name="(System)")
set_replaces_for_document(request, draft, existing_replaces + approved, by,
email_subject="%s replacement status set during submit by %s" % (draft.name, submission.submitter_parsed()["name"]))
if suggested:
possibly_replaces = DocRelationshipName.objects.get(slug="possibly-replaces")
for r in suggested:
RelatedDocument.objects.create(source=draft, target=r, relationship=possibly_replaces)
DocEvent.objects.create(doc=draft, by=by, type="added_suggested_replaces",
desc="Added suggested replacement relationships: %s" % ", ".join(d.name for d in suggested))
return approved, suggested
def get_person_from_name_email(name, email):
# try email

View file

@ -6,14 +6,13 @@ from django.conf import settings
from django.core.urlresolvers import reverse as urlreverse
from django.core.validators import validate_email, ValidationError
from django.http import HttpResponseRedirect, Http404, HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.shortcuts import get_object_or_404, redirect, render
from ietf.doc.models import Document
from ietf.doc.models import Document, DocAlias
from ietf.doc.utils import prettify_std_name
from ietf.group.models import Group
from ietf.ietfauth.utils import has_role, role_required
from ietf.submit.forms import UploadForm, NameEmailForm, EditSubmissionForm, PreapprovalForm
from ietf.submit.forms import UploadForm, NameEmailForm, EditSubmissionForm, PreapprovalForm, ReplacesForm
from ietf.submit.mail import send_full_url, send_approval_request_to_group, send_submission_confirmation, submission_confirmation_email_list, send_manual_post_request
from ietf.submit.models import Submission, Preapproval, DraftSubmissionStateName
from ietf.submit.utils import approvable_submissions_for_user, preapprovals_for_user, recently_approved_by_user
@ -98,18 +97,15 @@ def upload_submission(request):
else:
form = UploadForm(request=request)
return render_to_response('submit/upload_submission.html',
return render(request, 'submit/upload_submission.html',
{'selected': 'index',
'form': form},
context_instance=RequestContext(request))
'form': form})
def note_well(request):
return render_to_response('submit/note_well.html', {'selected': 'notewell'},
context_instance=RequestContext(request))
return render(request, 'submit/note_well.html', {'selected': 'notewell'})
def tool_instructions(request):
return render_to_response('submit/tool_instructions.html', {'selected': 'instructions'},
context_instance=RequestContext(request))
return render(request, 'submit/tool_instructions.html', {'selected': 'instructions'})
def search_submission(request):
error = None
@ -120,11 +116,10 @@ def search_submission(request):
if submission:
return redirect(submission_status, submission_id=submission.pk)
error = 'No valid submission found for %s' % name
return render_to_response('submit/search_submission.html',
return render(request, 'submit/search_submission.html',
{'selected': 'status',
'error': error,
'name': name},
context_instance=RequestContext(request))
'name': name})
def can_edit_submission(user, submission, access_token):
key_matched = access_token and submission.access_token() == access_token
@ -153,12 +148,7 @@ def submission_status(request, submission_id, access_token=None):
confirmation_list = submission_confirmation_email_list(submission)
try:
preapproval = Preapproval.objects.get(name=submission.name)
except Preapproval.DoesNotExist:
preapproval = None
requires_group_approval = submission.rev == '00' and submission.group and submission.group.type_id in ("wg", "rg", "ietf", "irtf", "iab", "iana", "rfcedtyp") and not preapproval
requires_group_approval = (submission.rev == '00' and submission.group and submission.group.type_id in ("wg", "rg", "ietf", "irtf", "iab", "iana", "rfcedtyp") and not Preapproval.objects.filter(name=submission.name).exists())
requires_prev_authors_approval = Document.objects.filter(name=submission.name)
@ -175,6 +165,7 @@ def submission_status(request, submission_id, access_token=None):
submitter_form = NameEmailForm(initial=submission.submitter_parsed(), prefix="submitter")
replaces_form = ReplacesForm(initial=DocAlias.objects.filter(name__in=submission.replaces.split(",")))
if request.method == 'POST':
action = request.POST.get('action')
@ -183,8 +174,12 @@ def submission_status(request, submission_id, access_token=None):
return HttpResponseForbidden("You do not have permission to perfom this action")
submitter_form = NameEmailForm(request.POST, prefix="submitter")
if submitter_form.is_valid():
replaces_form = ReplacesForm(request.POST)
validations = [submitter_form.is_valid(), replaces_form.is_valid()]
if all(validations):
submission.submitter = submitter_form.cleaned_line()
replaces = replaces_form.cleaned_data.get("replaces", [])
submission.replaces = ",".join(o.name for o in replaces)
if requires_group_approval:
submission.state = DraftSubmissionStateName.objects.get(slug="grp-appr")
@ -209,7 +204,11 @@ def submission_status(request, submission_id, access_token=None):
else:
desc = u"sent confirmation email to submitter and authors: %s" % u", ".join(sent_to)
create_submission_event(request, submission, u"Set submitter to \"%s\" and %s" % (submission.submitter, desc))
msg = u"Set submitter to \"%s\", replaces to %s and %s" % (
submission.submitter,
", ".join(prettify_std_name(r.name) for r in replaces) if replaces else "(none)",
desc)
create_submission_event(request, submission, msg)
if access_token:
return redirect("submit_submission_status_by_hash", submission_id=submission.pk, access_token=access_token)
@ -271,23 +270,23 @@ def submission_status(request, submission_id, access_token=None):
# something went wrong, turn this into a GET and let the user deal with it
return HttpResponseRedirect("")
return render_to_response('submit/submission_status.html',
{'selected': 'status',
'submission': submission,
'errors': errors,
'passes_idnits': passes_idnits,
'submitter_form': submitter_form,
'message': message,
'can_edit': can_edit,
'can_force_post': can_force_post,
'can_group_approve': can_group_approve,
'can_cancel': can_cancel,
'show_send_full_url': show_send_full_url,
'requires_group_approval': requires_group_approval,
'requires_prev_authors_approval': requires_prev_authors_approval,
'confirmation_list': confirmation_list,
},
context_instance=RequestContext(request))
return render(request, 'submit/submission_status.html', {
'selected': 'status',
'submission': submission,
'errors': errors,
'passes_idnits': passes_idnits,
'submitter_form': submitter_form,
'replaces_form': replaces_form,
'message': message,
'can_edit': can_edit,
'can_force_post': can_force_post,
'can_group_approve': can_group_approve,
'can_cancel': can_cancel,
'show_send_full_url': show_send_full_url,
'requires_group_approval': requires_group_approval,
'requires_prev_authors_approval': requires_prev_authors_approval,
'confirmation_list': confirmation_list,
})
def edit_submission(request, submission_id, access_token=None):
@ -312,14 +311,17 @@ def edit_submission(request, submission_id, access_token=None):
edit_form = EditSubmissionForm(request.POST, instance=submission, prefix="edit")
submitter_form = NameEmailForm(request.POST, prefix="submitter")
replaces_form = ReplacesForm(request.POST)
author_forms = [ NameEmailForm(request.POST, email_required=False, prefix=prefix)
for prefix in request.POST.getlist("authors-prefix")
if prefix != "authors-" ]
# trigger validation of all forms
validations = [edit_form.is_valid(), submitter_form.is_valid()] + [ f.is_valid() for f in author_forms ]
validations = [edit_form.is_valid(), submitter_form.is_valid(), replaces_form.is_valid()] + [ f.is_valid() for f in author_forms ]
if all(validations):
submission.submitter = submitter_form.cleaned_line()
replaces = replaces_form.cleaned_data.get("replaces", [])
submission.replaces = ",".join(o.name for o in replaces)
submission.authors = "\n".join(f.cleaned_line() for f in author_forms)
edit_form.save(commit=False) # transfer changes
@ -350,20 +352,21 @@ def edit_submission(request, submission_id, access_token=None):
else:
edit_form = EditSubmissionForm(instance=submission, prefix="edit")
submitter_form = NameEmailForm(initial=submission.submitter_parsed(), prefix="submitter")
replaces_form = ReplacesForm(initial=DocAlias.objects.filter(name__in=submission.replaces.split(",")))
author_forms = [ NameEmailForm(initial=author, email_required=False, prefix="authors-%s" % i)
for i, author in enumerate(submission.authors_parsed()) ]
return render_to_response('submit/edit_submission.html',
return render(request, 'submit/edit_submission.html',
{'selected': 'status',
'submission': submission,
'edit_form': edit_form,
'submitter_form': submitter_form,
'replaces_form': replaces_form,
'author_forms': author_forms,
'empty_author_form': empty_author_form,
'errors': errors,
'form_errors': form_errors,
},
context_instance=RequestContext(request))
})
def confirm_submission(request, submission_id, auth_token):
@ -379,10 +382,10 @@ def confirm_submission(request, submission_id, auth_token):
return redirect("doc_view", name=submission.name)
return render_to_response('submit/confirm_submission.html', {
return render(request, 'submit/confirm_submission.html', {
'submission': submission,
'key_matched': key_matched,
}, context_instance=RequestContext(request))
})
def approvals(request):
@ -392,13 +395,12 @@ def approvals(request):
days = 30
recently_approved = recently_approved_by_user(request.user, datetime.date.today() - datetime.timedelta(days=days))
return render_to_response('submit/approvals.html',
return render(request, 'submit/approvals.html',
{'selected': 'approvals',
'approvals': approvals,
'preapprovals': preapprovals,
'recently_approved': recently_approved,
'days': days },
context_instance=RequestContext(request))
'days': days })
@role_required("Secretariat", "WG Chair", "RG Chair")
@ -421,11 +423,10 @@ def add_preapproval(request):
else:
form = PreapprovalForm()
return render_to_response('submit/add_preapproval.html',
return render(request, 'submit/add_preapproval.html',
{'selected': 'approvals',
'groups': groups,
'form': form },
context_instance=RequestContext(request))
'form': form })
@role_required("Secretariat", "WG Chair", "RG Chair")
def cancel_preapproval(request, preapproval_id):
@ -439,7 +440,6 @@ def cancel_preapproval(request, preapproval_id):
return HttpResponseRedirect(urlreverse("submit_approvals") + "#preapprovals")
return render_to_response('submit/cancel_preapproval.html',
return render(request, 'submit/cancel_preapproval.html',
{'selected': 'approvals',
'preapproval': preapproval },
context_instance=RequestContext(request))
'preapproval': preapproval })

View file

@ -93,7 +93,35 @@
</td>
</tr>
{% endif %}
{% if possibly_replaces %}
<tr>
<th>Possibly Replaces</th>
<td class="edit">
{% if can_edit_stream_info %}
<a class="btn btn-default btn-xs" href="{% url "doc_review_possibly_replaces" name=doc.name %}">Edit</a>
{% endif %}
</td>
<td>
{{ possibly_replaces|join:", "|urlize_ietf_docs }}
</td>
</tr>
{% endif %}
{% if possibly_replaced_by %}
<tr>
<th>Possibly Replaced By</th>
<td class="edit">
{% if can_edit_stream_info %}
{% comment %}<a class="btn btn-default btn-xs" href="{% url "doc_review_possibly_replaces" name=doc.name %}">Edit</a>{% endcomment %}
{% endif %}
</td>
<td>
{{ possibly_replaced_by|join:", "|urlize_ietf_docs }}
</td>
</tr>
{% endif %}
<tr>
<th>Stream</th>
<td class="edit">

View file

@ -0,0 +1,23 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% block title %}Review suggestions for documents that {{ doc }} replaces{% endblock %}
{% block content %}
{% origin %}
<h1>Review suggestions for documents that {{ doc }} replaces</h1>
<form name="review-suggested-replaces" role="form" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<a class="btn btn-default pull-right" href="{{ doc.get_absolute_url }}">Cancel</a>
<button type="submit" value="Save" class="btn btn-primary">Save</button>
{% endbuttons %}
</form>
{% endblock %}

View file

@ -0,0 +1,12 @@
{% autoescape off %}
{{ doc }} is suggested to replace:
{% for d in possibly_replaces %} {{ d.name }}
{% endfor %}
Please visit
{{ review_url }}
and either accept or decline the suggestion{{ suggested_replaces|pluralize:"s" }}.
{% endautoescape %}

View file

@ -69,6 +69,7 @@
<h3>Submitter</h3>
{% include "submit/submitter_form.html" %}
{% include "submit/replaces_form.html" %}
{% for form in author_forms %}
<div {% if forloop.last %}id="cloner"{% endif %}>

View file

@ -0,0 +1,12 @@
{% for field in replaces_form %}
<tr{% if field.errors %} class="error"{% endif %}>
<th>{{ field.label_tag }}</th>
<td>
{{ field }}
{% if field.help_text %}
<div class="helptext">{{ field.help_text }}</div>
{% endif %}
{{ field.errors }}
</td>
</tr>
{% endfor %}

View file

@ -6,6 +6,12 @@
{% block title %}Submission status of {{ submission.name }}-{{ submission.rev }}{% endblock %}
{% block pagehead %}
{{ block.super }}
<link rel="stylesheet" href="/css/lib/select2.css">
<link rel="stylesheet" href="/css/lib/select2-bootstrap.css">
{% endblock %}
{% block submit_content %}
{% origin %}
{% if submission.state_id != "uploaded" %}
@ -26,7 +32,7 @@
</p>
{% endif %}
{% if submitter_form.errors %}
{% if submitter_form.errors or replaces_form.errors %}
<p class="alert alert-danger">Please fix errors in the form below.</p>
{% endif %}
@ -218,6 +224,7 @@
<form class="idsubmit" method="post">
{% csrf_token %}
{% include "submit/submitter_form.html" %}
{% include "submit/replaces_form.html" %}
<input type="hidden" name="action" value="autopost">
<button class="btn btn-primary" type="submit">Post submission</button>
</form>
@ -243,6 +250,12 @@
<tr><th>Email address</th><td>{{ submission.submitter_parsed.email }}</td></tr>
</table>
{% endif %}
{% if submission.replaces %}
<h3>Replaced documents</h3>
<table class="table table-condensed table-striped">
<tr><th>Replaces</th><td>{{ submission.replaces|split:","|join:", "|urlize_ietf_docs }}</td></tr>
</table>
{% endif %}
{% endif %}
{% if can_cancel %}
@ -321,3 +334,8 @@
{% include "submit/problem-reports-footer.html" %}
{% endblock %}
{% block js %}
<script src="/js/lib/select2-3.5.2.min.js"></script>
<script src="/js/select2-field.js"></script>
{% endblock %}