diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index 7f483ad5e..65bce6d11 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -413,3 +413,34 @@ def email_last_call_expired(doc): doc=doc, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), cc="iesg-secretary@ietf.org") + +def stream_state_email_recipients(doc, extra_recipients): + persons = set() + res = [] + for r in Role.objects.filter(group=doc.group, name__in=("chair", "delegate")).select_related("person", "email"): + res.append(r.formatted_email()) + persons.add(r.person) + + for email in doc.authors.all(): + if email.person not in persons: + res.append(email.formatted_email()) + persons.add(email.person) + + for x in extra_recipients: + if not x in res: + res.append(x) + + return res + +def email_stream_state_changed(request, doc, prev_state, new_state, changed_by, comment="", extra_recipients=[]): + recipients = stream_state_email_recipients(doc, extra_recipients) + + send_mail(request, recipients, settings.DEFAULT_FROM_EMAIL, + u"Stream State Changed for Draft %s" % doc.name, + 'doc/mail/stream_state_changed_email.txt', + dict(doc=doc, + url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + prev_state=prev_state, + new_state=new_state, + changed_by=changed_by, + comment=comment)) diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index 03ca24c72..6e2cbfad6 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -983,3 +983,41 @@ class RequestPublicationTestCase(django.test.TestCase): # the IANA copy self.assertTrue("Document Action" in outbox[-1]['Subject']) self.assertTrue(not outbox[-1]['CC']) + +class AdoptDraftTests(django.test.TestCase): + fixtures = ['names'] + + def test_adopt_document(self): + draft = make_test_data() + draft.stream = None + draft.group = Group.objects.get(type="individ") + draft.save() + draft.unset_state("draft-stream-ietf") + + url = urlreverse('doc_adopt_draft', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "marschairman", url) + + # get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form select[name="group"] option')), 1) # we can only select "mars" + + # adopt in mars WG + mailbox_before = len(outbox) + events_before = draft.docevent_set.count() + r = self.client.post(url, + dict(comment="some comment", + group=Group.objects.get(acronym="mars").pk, + weeks="10")) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(pk=draft.pk) + self.assertEquals(draft.group.acronym, "mars") + self.assertEquals(draft.stream_id, "ietf") + self.assertEquals(draft.docevent_set.count() - events_before, 4) + self.assertEquals(len(outbox), mailbox_before + 1) + self.assertTrue("state changed" in outbox[-1]["Subject"].lower()) + self.assertTrue("wgchairman@ietf.org" in unicode(outbox[-1])) + self.assertTrue("wgdelegate@ietf.org" in unicode(outbox[-1])) + diff --git a/ietf/doc/urls.py b/ietf/doc/urls.py index 49df3de21..6453de180 100644 --- a/ietf/doc/urls.py +++ b/ietf/doc/urls.py @@ -86,6 +86,8 @@ urlpatterns += patterns('', url(r'^(?P[A-Za-z0-9._+-]+)/edit/shepherd/$', views_draft.edit_shepherd, name='doc_edit_shepherd'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/shepherdwriteup/$', views_draft.edit_shepherd_writeup, name='doc_edit_shepherd_writeup'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/requestpublication/$', views_draft.request_publication, name='doc_request_publication'), + url(r'^(?P[A-Za-z0-9._+-]+)/edit/adopt/$', views_draft.adopt_draft, name='doc_adopt_draft'), + url(r'^(?P[A-Za-z0-9._+-]+)/edit/state/stream/$', views_draft.change_stream_state, name='doc_change_stream_state'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/clearballot/$', views_ballot.clear_ballot, name='doc_clear_ballot'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/deferballot/$', views_ballot.defer_ballot, name='doc_defer_ballot'), diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index 7798969f9..d12699b14 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -5,6 +5,8 @@ from django.conf import settings from ietf.utils import markup_txt from ietf.doc.models import * +from ietf.group.models import Role +from ietf.ietfauth.utils import has_role def get_state_types(doc): res = [] @@ -37,6 +39,20 @@ def get_tags_for_stream_id(stream_id): else: return [] +def can_adopt_draft(user, doc): + if not user.is_authenticated(): + return False + + if has_role(user, "Secretariat"): + return True + + return (doc.stream_id in (None, "ietf", "irtf") + and doc.group.type_id == "individ" + and Role.objects.filter(name__in=("chair", "delegate", "secr"), + group__type__in=("wg", "rg"), + group__state="active", + person__user=user).exists()) + def needed_ballot_positions(doc, active_positions): '''Returns text answering the question "what does this document need to pass?". The return value is only useful if the document @@ -225,7 +241,30 @@ def add_state_change_event(doc, by, prev_state, new_state, timestamp=None): e.time = timestamp e.save() return e - + +def update_reminder(doc, reminder_type_slug, event, due_date): + reminder_type = DocReminderTypeName.objects.get(slug=reminder_type_slug) + + try: + reminder = DocReminder.objects.get(event__doc=doc, type=reminder_type, active=True) + except DocReminder.DoesNotExist: + reminder = None + + if due_date: + # activate/update reminder + if not reminder: + reminder = DocReminder(type=reminder_type) + + reminder.event = event + reminder.due = due_date + reminder.active = True + reminder.save() + else: + # deactivate reminder + if reminder: + reminder.active = False + reminder.save() + def prettify_std_name(n): if re.match(r"(rfc|bcp|fyi|std)[0-9]+", n): return n[:3].upper() + " " + n[3:] diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 6328fcc1c..fe5a42214 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -290,13 +290,8 @@ def document_main(request, name, rev=None): # remaining actions actions = [] - if ((not doc.stream_id or doc.stream_id in ("ietf", "irtf")) and group.type_id == "individ" and - (request.user.is_authenticated() and - Role.objects.filter(person__user=request.user, name__in=("chair", "secr", "delegate"), - group__type__in=("wg","rg"), - group__state="active") - or has_role(request.user, "Secretariat"))): - actions.append(("Adopt in Group", urlreverse('edit_adopt', kwargs=dict(name=doc.name)))) + if can_adopt_draft(request.user, doc): + actions.append(("Adopt in Group", urlreverse('doc_adopt_draft', kwargs=dict(name=doc.name)))) if doc.get_state_slug() == "expired" and not resurrected_by and can_edit: actions.append(("Request Resurrect", urlreverse('doc_request_resurrect', kwargs=dict(name=doc.name)))) diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index 7d3cd472a..e12d3f918 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -1100,3 +1100,99 @@ def request_publication(request, name): ), context_instance = RequestContext(request)) +class AdoptDraftForm(forms.Form): + group = forms.ModelChoiceField(queryset=Group.objects.filter(type__in=["wg", "rg"], state="active").order_by("-type", "acronym"), required=True, empty_label=None) + comment = forms.CharField(widget=forms.Textarea, required=False, label="Comment", help_text="Optional comment explaining the reasons for the adoption") + weeks = forms.IntegerField(required=False, label="Expected weeks in adoption state") + + def __init__(self, *args, **kwargs): + user = kwargs.pop("user") + + super(AdoptDraftForm, self).__init__(*args, **kwargs) + + if has_role(user, "Secretariat"): + pass # all groups + else: + self.fields["group"].queryset = self.fields["group"].queryset.filter(role__person__user=user, role__name__in=("chair", "delegate", "secr")).distinct() + + self.fields['group'].choices = [(g.pk, '%s - %s' % (g.acronym, g.name)) for g in self.fields["group"].queryset] + + +@login_required +def adopt_draft(request, name): + doc = get_object_or_404(Document, type="draft", name=name) + + if not can_adopt_draft(request.user, doc): + return HttpResponseForbidden("You don't have permission to access this view") + + if request.method == 'POST': + form = AdoptDraftForm(request.POST, user=request.user) + + if form.is_valid(): + # adopt + by = request.user.get_profile() + + save_document_in_history(doc) + + doc.time = datetime.datetime.now() + + group = form.cleaned_data["group"] + comment = form.cleaned_data["comment"].strip() + + if group.type.slug == "rg": + new_stream = StreamName.objects.get(slug="irtf") + adopt_state_slug = "active" + else: + new_stream = StreamName.objects.get(slug="ietf") + adopt_state_slug = "c-adopt" + + if doc.stream != new_stream: + e = DocEvent(type="changed_stream", time=doc.time, by=by, doc=doc) + e.desc = u"Changed stream to %s" % new_stream.name + if doc.stream: + e.desc += u" from %s" % doc.stream.name + e.save() + doc.stream = new_stream + + if group != doc.group: + e = DocEvent(type="changed_group", time=doc.time, by=by, doc=doc) + e.desc = u"Changed group to %s (%s)" % (group.name, group.acronym.upper()) + if doc.group.type_id != "individ": + e.desc += " from %s (%s)" % (doc.group.name, doc.group.acronym.upper()) + e.save() + doc.group = group + + doc.save() + + prev_state = doc.get_state("draft-stream-%s" % doc.stream_id) + new_state = State.objects.get(slug=adopt_state_slug, type="draft-stream-%s" % doc.stream_id, used=True) + + if new_state != prev_state: + doc.set_state(new_state) + e = add_state_change_event(doc, by, prev_state, new_state, doc.time) + + due_date = None + if form.cleaned_data["weeks"] != None: + due_date = datetime.date.today() + datetime.timedelta(weeks=form.cleaned_data["weeks"]) + + update_reminder(doc, "stream-s", e, due_date) + + email_stream_state_changed(request, doc, prev_state, new_state, by, comment) + + if comment: + e = DocEvent(type="added_comment", time=doc.time, by=by, doc=doc) + e.desc = comment + e.save() + + return HttpResponseRedirect(doc.get_absolute_url()) + else: + form = AdoptDraftForm(user=request.user) + + return render_to_response('doc/draft/adopt_draft.html', + {'doc': doc, + 'form': form, + }, + context_instance=RequestContext(request)) + +def change_stream_state(request): + pass diff --git a/ietf/templates/doc/draft/adopt_draft.html b/ietf/templates/doc/draft/adopt_draft.html new file mode 100644 index 000000000..062a13799 --- /dev/null +++ b/ietf/templates/doc/draft/adopt_draft.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block title %}Adopt {{ doc }} in Group{% endblock %} + +{% block morecss %} +form.adopt-draft th { width: 8em; } +form.adopt-draft #id_comment { width: 30em; } +form.adopt-draft #id_weeks { width: 3em; } +form.adopt-draft .actions { text-align: right; padding-top: 1em; } +p.intro { max-width: 50em; } +{% endblock %} + +{% block content %} +

Adopt {{ doc }} in Group

+ +

You can adopt this draft into a group.

+ +

For a WG, the draft enters the IETF stream and the +stream state becomes "Call for Adoption by WG Issued". For an RG, the +draft enters the IRTF stream and the stream state becomes "Active RG +Document".

+ +
+ {% for field in form.hidden_fields %}{{ field }}{% endfor %} + + {% for field in form.visible_fields %} + + + + + {% endfor %} + + + +
{{ field.label_tag }}:{{ field }} + {% if field.help_text %}
{{ field.help_text }}
{% endif %} + {{ field.errors }} +
+ Cancel + +
+
+{% endblock %} diff --git a/ietf/templates/doc/mail/stream_state_changed_email.txt b/ietf/templates/doc/mail/stream_state_changed_email.txt new file mode 100644 index 000000000..84378acd7 --- /dev/null +++ b/ietf/templates/doc/mail/stream_state_changed_email.txt @@ -0,0 +1,8 @@ +{% autoescape off %}{% filter wordwrap:73 %} +The stream state of {{ doc }} has been changed{% if prev_state %} from {{ prev_state.name }}{% endif %} to {{ new_state.name }} by {{ changed_by }}. +{% if comment %} + +Comment: +{{ comment }} +{% endif %} +{% endfilter %}{% endautoescape %}