Reimplement WG/RG adoption of drafts with the new schema, move it to

doc/ together with test and utilities, rewrite the UI to be more in
line with the rest of the edit pages (including the revamped stream
state change UI)
 - Legacy-Id: 6269
This commit is contained in:
Ole Laursen 2013-09-26 13:14:07 +00:00
parent c760b48b38
commit c508a5672d
8 changed files with 260 additions and 8 deletions

View file

@ -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))

View file

@ -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]))

View file

@ -86,6 +86,8 @@ urlpatterns += patterns('',
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/shepherd/$', views_draft.edit_shepherd, name='doc_edit_shepherd'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/shepherdwriteup/$', views_draft.edit_shepherd_writeup, name='doc_edit_shepherd_writeup'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/requestpublication/$', views_draft.request_publication, name='doc_request_publication'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/adopt/$', views_draft.adopt_draft, name='doc_adopt_draft'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/state/stream/$', views_draft.change_stream_state, name='doc_change_stream_state'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/clearballot/$', views_ballot.clear_ballot, name='doc_clear_ballot'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/deferballot/$', views_ballot.defer_ballot, name='doc_defer_ballot'),

View file

@ -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:]

View file

@ -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))))

View file

@ -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 <b>%s</b>" % 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 <b>%s (%s)</b>" % (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

View file

@ -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 %}
<h1>Adopt {{ doc }} in Group</h1>
<p class="intro">You can adopt this draft into a group.</p>
<p class="intro">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".</p>
<form class="adopt-draft" action="" method="post">
{% for field in form.hidden_fields %}{{ field }}{% endfor %}
<table>
{% for field in form.visible_fields %}
<tr>
<th>{{ field.label_tag }}:</th>
<td>{{ field }}
{% if field.help_text %}<div class="help">{{ field.help_text }}</div>{% endif %}
{{ field.errors }}
</td>
</tr>
{% endfor %}
<tr>
<td colspan="2" class="actions">
<a class="button" href="{{ doc.get_absolute_url }}">Cancel</a>
<input class="button" type="submit" value="Adopt Draft"/>
</td>
</tr>
</table>
</form>
{% endblock %}

View file

@ -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 %}