From 02a9da52a1fa7a8078ccdd0cd9819c5e64c3b999 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 13 Jul 2015 21:09:39 +0000 Subject: [PATCH 1/8] Manually moved the concepts forward. Reworked some presentation. Working enough to bring testers in at Sprint93 - Legacy-Id: 9717 --- ietf/doc/fields.py | 2 + ietf/doc/mails.py | 23 +++ ietf/doc/models.py | 2 + ietf/doc/tests_draft.py | 23 ++- ietf/doc/urls.py | 1 + ietf/doc/utils.py | 78 ++++++++- ietf/doc/views_doc.py | 6 + ietf/doc/views_draft.py | 155 +++++++++--------- ietf/name/fixtures/names.json | 11 ++ ietf/name/migrations/0005_add_sug_replaces.py | 19 +++ ietf/submit/forms.py | 4 + .../migrations/0003_auto_20150713_1104.py | 20 +++ ietf/submit/models.py | 2 +- ietf/submit/tests.py | 59 +++++-- ietf/submit/utils.py | 56 ++++++- ietf/submit/views.py | 110 ++++++------- ietf/templates/doc/document_draft.html | 30 +++- .../doc/draft/review_possibly_replaces.html | 23 +++ .../mail/review_possibly_replaces_request.txt | 12 ++ ietf/templates/submit/edit_submission.html | 1 + ietf/templates/submit/replaces_form.html | 12 ++ ietf/templates/submit/submission_status.html | 20 ++- 22 files changed, 519 insertions(+), 150 deletions(-) create mode 100644 ietf/name/migrations/0005_add_sug_replaces.py create mode 100644 ietf/submit/migrations/0003_auto_20150713_1104.py create mode 100644 ietf/templates/doc/draft/review_possibly_replaces.html create mode 100644 ietf/templates/doc/mail/review_possibly_replaces_request.txt create mode 100644 ietf/templates/submit/replaces_form.html diff --git a/ietf/doc/fields.py b/ietf/doc/fields.py index b8f482885..09d16b7c1 100644 --- a/ietf/doc/fields.py +++ b/ietf/doc/fields.py @@ -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 diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 3903ad35a..68f10b82e 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -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 ") + elif doc.stream_id == "ise": + to_email.append("Independent Submission Editor ") + elif doc.stream_id == "irtf": + to_email.append("IRSG ") + + 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 }), + }) diff --git a/ietf/doc/models.py b/ietf/doc/models.py index f983d2185..70bbe2142 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -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"), diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index 4ebab5032..44a9b0b61 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -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') diff --git a/ietf/doc/urls.py b/ietf/doc/urls.py index fbb88d38d..d9d8489ec 100644 --- a/ietf/doc/urls.py +++ b/ietf/doc/urls.py @@ -79,6 +79,7 @@ urlpatterns = patterns('', url(r'^(?P[A-Za-z0-9._+-]+)/edit/stream/$', views_draft.change_stream, name='doc_change_stream'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/replaces/$', views_draft.replaces, name='doc_change_replaces'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/notify/$', views_doc.edit_notify, name='doc_change_notify'), + url(r'^(?P[A-Za-z0-9._+-]+)/edit/suggested-replaces/$', views_draft.review_possibly_replaces, name='doc_review_possibly_replaces'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/status/$', views_draft.change_intention, name='doc_change_intended_status'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/telechat/$', views_doc.telechat_date, name='doc_change_telechat_date'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/iesgnote/$', views_draft.edit_iesg_note, name='doc_change_iesg_note'), diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index 014cc5d7f..c3bf73fa3 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -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 %s 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 ", + 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.""" diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index efc5db1df..1d73b8ecc 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -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")], diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index ca5e6826d..2e358d855 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -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 %s 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 ", - "%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") diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 8aad8add6..871d46cb2 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -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, diff --git a/ietf/name/migrations/0005_add_sug_replaces.py b/ietf/name/migrations/0005_add_sug_replaces.py new file mode 100644 index 000000000..7ed0a8d3d --- /dev/null +++ b/ietf/name/migrations/0005_add_sug_replaces.py @@ -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) + ] diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py index 06c1ae997..3ce8b02d2 100644 --- a/ietf/submit/forms.py +++ b/ietf/submit/forms.py @@ -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) diff --git a/ietf/submit/migrations/0003_auto_20150713_1104.py b/ietf/submit/migrations/0003_auto_20150713_1104.py new file mode 100644 index 000000000..92054ee22 --- /dev/null +++ b/ietf/submit/migrations/0003_auto_20150713_1104.py @@ -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, + ), + ] diff --git a/ietf/submit/models.py b/ietf/submit/models.py index 716055650..97761cb10 100644 --- a/ietf/submit/models.py +++ b/ietf/submit/models.py @@ -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 <john@example.org>\".") 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) diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index e1a845a79..d1704d447 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -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 ") + self.assertEqual(submission.replaces, draft.docalias_set.all().first().name) self.assertEqual(submission.state_id, "manual") authors = submission.authors_parsed() diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index cb2561562..f53106d47 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -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 diff --git a/ietf/submit/views.py b/ietf/submit/views.py index 67fe16094..8a6bc5688 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -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 }) diff --git a/ietf/templates/doc/document_draft.html b/ietf/templates/doc/document_draft.html index 7cf63883f..51bf2f85b 100644 --- a/ietf/templates/doc/document_draft.html +++ b/ietf/templates/doc/document_draft.html @@ -93,7 +93,35 @@ {% endif %} - + + {% if possibly_replaces %} + + Possibly Replaces + + {% if can_edit_stream_info %} + Edit + {% endif %} + + + {{ possibly_replaces|join:", "|urlize_ietf_docs }} + + + {% endif %} + + {% if possibly_replaced_by %} + + Possibly Replaced By + + {% if can_edit_stream_info %} + {% comment %}Edit{% endcomment %} + {% endif %} + + + {{ possibly_replaced_by|join:", "|urlize_ietf_docs }} + + + {% endif %} + Stream diff --git a/ietf/templates/doc/draft/review_possibly_replaces.html b/ietf/templates/doc/draft/review_possibly_replaces.html new file mode 100644 index 000000000..9ee449208 --- /dev/null +++ b/ietf/templates/doc/draft/review_possibly_replaces.html @@ -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 %} +

Review suggestions for documents that {{ doc }} replaces

+ +
+ {% csrf_token %} + + {% bootstrap_form form %} + + {% buttons %} + Cancel + + {% endbuttons %} +
+ +{% endblock %} diff --git a/ietf/templates/doc/mail/review_possibly_replaces_request.txt b/ietf/templates/doc/mail/review_possibly_replaces_request.txt new file mode 100644 index 000000000..100289268 --- /dev/null +++ b/ietf/templates/doc/mail/review_possibly_replaces_request.txt @@ -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 %} diff --git a/ietf/templates/submit/edit_submission.html b/ietf/templates/submit/edit_submission.html index ec73ed478..7203ff007 100644 --- a/ietf/templates/submit/edit_submission.html +++ b/ietf/templates/submit/edit_submission.html @@ -69,6 +69,7 @@

Submitter

{% include "submit/submitter_form.html" %} + {% include "submit/replaces_form.html" %} {% for form in author_forms %}
diff --git a/ietf/templates/submit/replaces_form.html b/ietf/templates/submit/replaces_form.html new file mode 100644 index 000000000..0f5eae31f --- /dev/null +++ b/ietf/templates/submit/replaces_form.html @@ -0,0 +1,12 @@ + {% for field in replaces_form %} + + {{ field.label_tag }} + + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {{ field.errors }} + + + {% endfor %} diff --git a/ietf/templates/submit/submission_status.html b/ietf/templates/submit/submission_status.html index 51cbc0727..1351040ff 100644 --- a/ietf/templates/submit/submission_status.html +++ b/ietf/templates/submit/submission_status.html @@ -6,6 +6,12 @@ {% block title %}Submission status of {{ submission.name }}-{{ submission.rev }}{% endblock %} +{% block pagehead %} + {{ block.super }} + + +{% endblock %} + {% block submit_content %} {% origin %} {% if submission.state_id != "uploaded" %} @@ -26,7 +32,7 @@

{% endif %} - {% if submitter_form.errors %} + {% if submitter_form.errors or replaces_form.errors %}

Please fix errors in the form below.

{% endif %} @@ -218,6 +224,7 @@
{% csrf_token %} {% include "submit/submitter_form.html" %} + {% include "submit/replaces_form.html" %}
@@ -243,6 +250,12 @@ Email address{{ submission.submitter_parsed.email }} {% endif %} + {% if submission.replaces %} +

Replaced documents

+ + +
Replaces{{ submission.replaces|split:","|join:", "|urlize_ietf_docs }}
+ {% endif %} {% endif %} {% if can_cancel %} @@ -321,3 +334,8 @@ {% include "submit/problem-reports-footer.html" %} {% endblock %} + +{% block js %} + + +{% endblock %} From 44435ffc2ffb5f45c8a515692853f228ca93812a Mon Sep 17 00:00:00 2001 From: Adam Roach Date: Sat, 18 Jul 2015 09:28:28 +0000 Subject: [PATCH 2/8] foo - Legacy-Id: 9768 --- x | 1 + 1 file changed, 1 insertion(+) create mode 100644 x diff --git a/x b/x new file mode 100644 index 000000000..137d409d7 --- /dev/null +++ b/x @@ -0,0 +1 @@ +Hi there! From 42d817aa8355dc97679394f12a509540037a7179 Mon Sep 17 00:00:00 2001 From: Adam Roach Date: Sat, 18 Jul 2015 09:29:10 +0000 Subject: [PATCH 3/8] Removing - Legacy-Id: 9769 --- x | 1 - 1 file changed, 1 deletion(-) delete mode 100644 x diff --git a/x b/x deleted file mode 100644 index 137d409d7..000000000 --- a/x +++ /dev/null @@ -1 +0,0 @@ -Hi there! From 4080ceee89fe9127f20a05c073e2e0021407614f Mon Sep 17 00:00:00 2001 From: Adam Roach Date: Sat, 18 Jul 2015 09:32:47 +0000 Subject: [PATCH 4/8] foo - Legacy-Id: 9771 --- x | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 x diff --git a/x b/x new file mode 100644 index 000000000..e69de29bb From 7414a8ecc058cd4e4ba26847576433288517c0fb Mon Sep 17 00:00:00 2001 From: Adam Roach Date: Sat, 18 Jul 2015 09:32:59 +0000 Subject: [PATCH 5/8] bar - Legacy-Id: 9772 --- x | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 x diff --git a/x b/x deleted file mode 100644 index e69de29bb..000000000 From 03e52126f30934d07d1ca9c8daec97566e7a5958 Mon Sep 17 00:00:00 2001 From: Adam Roach Date: Sat, 18 Jul 2015 12:18:38 +0000 Subject: [PATCH 6/8] Only display 'possibly replaces' information to those people who can approve it and to authors - Legacy-Id: 9786 --- ietf/doc/views_doc.py | 12 +++++- ietf/templates/doc/document_draft.html | 52 +++++++++++++------------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 1d73b8ecc..3844504ba 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -156,6 +156,11 @@ def document_main(request, name, rev=None): person__user=request.user))) can_edit_iana_state = has_role(request.user, ("Secretariat", "IANA")) + can_edit_replaces = has_role(request.user, ("Area Director", "Secretariat", "WG Chair", "RG Chair", "WG Secretary", "RG Secretary")) + + is_author = unicode(request.user) in set([email.address for email in doc.authors.all()]) + can_view_possibly_replaces = can_edit_replaces or is_author + rfc_number = name[3:] if name.startswith("") else None draft_name = None for a in aliases: @@ -357,8 +362,9 @@ 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 + if can_view_possibly_replaces: + 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 @@ -390,6 +396,8 @@ def document_main(request, name, rev=None): can_edit_notify=can_edit_notify, can_edit_iana_state=can_edit_iana_state, can_edit_consensus=can_edit_consensus, + can_edit_replaces=can_edit_replaces, + can_view_possibly_replaces=can_view_possibly_replaces, rfc_number=rfc_number, draft_name=draft_name, diff --git a/ietf/templates/doc/document_draft.html b/ietf/templates/doc/document_draft.html index 51bf2f85b..e0f2b890f 100644 --- a/ietf/templates/doc/document_draft.html +++ b/ietf/templates/doc/document_draft.html @@ -94,32 +94,34 @@ {% endif %} - {% if possibly_replaces %} - - Possibly Replaces - - {% if can_edit_stream_info %} - Edit - {% endif %} - - - {{ possibly_replaces|join:", "|urlize_ietf_docs }} - - - {% endif %} + {% if can_view_possibly_replaces %} + {% if possibly_replaces %} + + Possibly Replaces + + {% if can_edit_replaces %} + Edit + {% endif %} + + + {{ possibly_replaces|join:", "|urlize_ietf_docs }} + + + {% endif %} - {% if possibly_replaced_by %} - - Possibly Replaced By - - {% if can_edit_stream_info %} - {% comment %}Edit{% endcomment %} - {% endif %} - - - {{ possibly_replaced_by|join:", "|urlize_ietf_docs }} - - + {% if possibly_replaced_by %} + + Possibly Replaced By + + {% if can_edit_replaces %} + {% comment %}Edit{% endcomment %} + {% endif %} + + + {{ possibly_replaced_by|join:", "|urlize_ietf_docs }} + + + {% endif %} {% endif %} From 313704f6379cd1aae15badd262ebd1785437b2d3 Mon Sep 17 00:00:00 2001 From: Adam Roach Date: Sat, 18 Jul 2015 13:56:24 +0000 Subject: [PATCH 7/8] Now sends email to chairs of WG of 'possibly-replaced' document, if any - Legacy-Id: 9794 --- ietf/doc/mails.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 68f10b82e..88bc553c6 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -13,6 +13,7 @@ from ietf.doc.models import WriteupDocEvent, BallotPositionDocEvent, LastCallDoc from ietf.doc.utils import needed_ballot_positions from ietf.person.models import Person from ietf.group.models import Group, Role +from ietf.doc.models import Document def email_state_changed(request, doc, text): to = [x.strip() for x in doc.notify.replace(';', ',').split(',')] @@ -500,11 +501,15 @@ def send_review_possibly_replaces_request(request, doc): elif doc.stream_id == "irtf": to_email.append("IRSG ") + possibly_replaces = Document.objects.filter(name__in=[alias.name for alias in doc.related_that_doc("possibly-replaces")]) + other_chairs = Role.objects.filter(group__in=[other.group for other in possibly_replaces], name="chair").select_related("email", "person") + to_email.extend(r.formatted_email() for r in other_chairs) + if not to_email: to_email.append("internet-drafts@ietf.org") if to_email: - send_mail(request, to_email, settings.DEFAULT_FROM_EMAIL, + send_mail(request, list(set(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, From 5cb7c82e39164fc540ed864754837d188ff0c530 Mon Sep 17 00:00:00 2001 From: Adam Roach Date: Sat, 18 Jul 2015 16:07:26 +0000 Subject: [PATCH 8/8] Check that email is sent to chairs of both replaced and replacing document - Legacy-Id: 9808 --- ietf/submit/tests.py | 3 +++ ietf/utils/test_data.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index d1704d447..c9ae76198 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -203,9 +203,12 @@ class SubmitTests(TestCase): self.assertTrue("New Version Notification" in outbox[-2]["Subject"]) self.assertTrue(name in unicode(outbox[-2])) self.assertTrue("mars" in unicode(outbox[-2])) + # Check "Review of suggested possible replacements for..." mail self.assertTrue("review" in outbox[-1]["Subject"].lower()) self.assertTrue(name in unicode(outbox[-1])) self.assertTrue(sug_replaced_alias.name in unicode(outbox[-1])) + self.assertTrue("ameschairman" in outbox[-1]["To"].lower()) + self.assertTrue("marschairman" in outbox[-1]["To"].lower()) def test_submit_existing(self): # submit new revision of existing -> supply submitter info -> prev authors confirm diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 5fbb91447..4a5790765 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -149,6 +149,7 @@ def make_test_data(): parent=area, list_email="ames-wg@ietf.org", ) + ames_wg = group charter = Document.objects.create( name="charter-ietf-" + group.acronym, type_id="charter", @@ -174,10 +175,13 @@ def make_test_data(): # group personnel create_person(mars_wg, "chair", name="WG Chair Man", username="marschairman") create_person(mars_wg, "delegate", name="WG Delegate", username="marsdelegate") - mars_wg.role_set.get_or_create(name_id='ad',person=ad,email=ad.role_email('ad')) mars_wg.save() + create_person(ames_wg, "chair", name="WG Chair Man", username="ameschairman") + create_person(ames_wg, "delegate", name="WG Delegate", username="amesdelegate") + ames_wg.role_set.get_or_create(name_id='ad',person=ad,email=ad.role_email('ad')) + ames_wg.save() # draft draft = Document.objects.create(