Skip to content

Commit c508a56

Browse files
committed
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
1 parent c760b48 commit c508a56

8 files changed

Lines changed: 260 additions & 8 deletions

File tree

ietf/doc/mails.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,3 +413,34 @@ def email_last_call_expired(doc):
413413
doc=doc,
414414
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
415415
cc="iesg-secretary@ietf.org")
416+
417+
def stream_state_email_recipients(doc, extra_recipients):
418+
persons = set()
419+
res = []
420+
for r in Role.objects.filter(group=doc.group, name__in=("chair", "delegate")).select_related("person", "email"):
421+
res.append(r.formatted_email())
422+
persons.add(r.person)
423+
424+
for email in doc.authors.all():
425+
if email.person not in persons:
426+
res.append(email.formatted_email())
427+
persons.add(email.person)
428+
429+
for x in extra_recipients:
430+
if not x in res:
431+
res.append(x)
432+
433+
return res
434+
435+
def email_stream_state_changed(request, doc, prev_state, new_state, changed_by, comment="", extra_recipients=[]):
436+
recipients = stream_state_email_recipients(doc, extra_recipients)
437+
438+
send_mail(request, recipients, settings.DEFAULT_FROM_EMAIL,
439+
u"Stream State Changed for Draft %s" % doc.name,
440+
'doc/mail/stream_state_changed_email.txt',
441+
dict(doc=doc,
442+
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
443+
prev_state=prev_state,
444+
new_state=new_state,
445+
changed_by=changed_by,
446+
comment=comment))

ietf/doc/tests_draft.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,3 +983,41 @@ def test_request_publication(self):
983983
# the IANA copy
984984
self.assertTrue("Document Action" in outbox[-1]['Subject'])
985985
self.assertTrue(not outbox[-1]['CC'])
986+
987+
class AdoptDraftTests(django.test.TestCase):
988+
fixtures = ['names']
989+
990+
def test_adopt_document(self):
991+
draft = make_test_data()
992+
draft.stream = None
993+
draft.group = Group.objects.get(type="individ")
994+
draft.save()
995+
draft.unset_state("draft-stream-ietf")
996+
997+
url = urlreverse('doc_adopt_draft', kwargs=dict(name=draft.name))
998+
login_testing_unauthorized(self, "marschairman", url)
999+
1000+
# get
1001+
r = self.client.get(url)
1002+
self.assertEquals(r.status_code, 200)
1003+
q = PyQuery(r.content)
1004+
self.assertEquals(len(q('form select[name="group"] option')), 1) # we can only select "mars"
1005+
1006+
# adopt in mars WG
1007+
mailbox_before = len(outbox)
1008+
events_before = draft.docevent_set.count()
1009+
r = self.client.post(url,
1010+
dict(comment="some comment",
1011+
group=Group.objects.get(acronym="mars").pk,
1012+
weeks="10"))
1013+
self.assertEquals(r.status_code, 302)
1014+
1015+
draft = Document.objects.get(pk=draft.pk)
1016+
self.assertEquals(draft.group.acronym, "mars")
1017+
self.assertEquals(draft.stream_id, "ietf")
1018+
self.assertEquals(draft.docevent_set.count() - events_before, 4)
1019+
self.assertEquals(len(outbox), mailbox_before + 1)
1020+
self.assertTrue("state changed" in outbox[-1]["Subject"].lower())
1021+
self.assertTrue("wgchairman@ietf.org" in unicode(outbox[-1]))
1022+
self.assertTrue("wgdelegate@ietf.org" in unicode(outbox[-1]))
1023+

ietf/doc/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@
8686
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/shepherd/$', views_draft.edit_shepherd, name='doc_edit_shepherd'),
8787
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/shepherdwriteup/$', views_draft.edit_shepherd_writeup, name='doc_edit_shepherd_writeup'),
8888
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/requestpublication/$', views_draft.request_publication, name='doc_request_publication'),
89+
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/adopt/$', views_draft.adopt_draft, name='doc_adopt_draft'),
90+
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/state/stream/$', views_draft.change_stream_state, name='doc_change_stream_state'),
8991

9092
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/clearballot/$', views_ballot.clear_ballot, name='doc_clear_ballot'),
9193
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/deferballot/$', views_ballot.defer_ballot, name='doc_defer_ballot'),

ietf/doc/utils.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from ietf.utils import markup_txt
77
from ietf.doc.models import *
8+
from ietf.group.models import Role
9+
from ietf.ietfauth.utils import has_role
810

911
def get_state_types(doc):
1012
res = []
@@ -37,6 +39,20 @@ def get_tags_for_stream_id(stream_id):
3739
else:
3840
return []
3941

42+
def can_adopt_draft(user, doc):
43+
if not user.is_authenticated():
44+
return False
45+
46+
if has_role(user, "Secretariat"):
47+
return True
48+
49+
return (doc.stream_id in (None, "ietf", "irtf")
50+
and doc.group.type_id == "individ"
51+
and Role.objects.filter(name__in=("chair", "delegate", "secr"),
52+
group__type__in=("wg", "rg"),
53+
group__state="active",
54+
person__user=user).exists())
55+
4056
def needed_ballot_positions(doc, active_positions):
4157
'''Returns text answering the question "what does this document
4258
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):
225241
e.time = timestamp
226242
e.save()
227243
return e
228-
244+
245+
def update_reminder(doc, reminder_type_slug, event, due_date):
246+
reminder_type = DocReminderTypeName.objects.get(slug=reminder_type_slug)
247+
248+
try:
249+
reminder = DocReminder.objects.get(event__doc=doc, type=reminder_type, active=True)
250+
except DocReminder.DoesNotExist:
251+
reminder = None
252+
253+
if due_date:
254+
# activate/update reminder
255+
if not reminder:
256+
reminder = DocReminder(type=reminder_type)
257+
258+
reminder.event = event
259+
reminder.due = due_date
260+
reminder.active = True
261+
reminder.save()
262+
else:
263+
# deactivate reminder
264+
if reminder:
265+
reminder.active = False
266+
reminder.save()
267+
229268
def prettify_std_name(n):
230269
if re.match(r"(rfc|bcp|fyi|std)[0-9]+", n):
231270
return n[:3].upper() + " " + n[3:]

ietf/doc/views_doc.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -290,13 +290,8 @@ def document_main(request, name, rev=None):
290290
# remaining actions
291291
actions = []
292292

293-
if ((not doc.stream_id or doc.stream_id in ("ietf", "irtf")) and group.type_id == "individ" and
294-
(request.user.is_authenticated() and
295-
Role.objects.filter(person__user=request.user, name__in=("chair", "secr", "delegate"),
296-
group__type__in=("wg","rg"),
297-
group__state="active")
298-
or has_role(request.user, "Secretariat"))):
299-
actions.append(("Adopt in Group", urlreverse('edit_adopt', kwargs=dict(name=doc.name))))
293+
if can_adopt_draft(request.user, doc):
294+
actions.append(("Adopt in Group", urlreverse('doc_adopt_draft', kwargs=dict(name=doc.name))))
300295

301296
if doc.get_state_slug() == "expired" and not resurrected_by and can_edit:
302297
actions.append(("Request Resurrect", urlreverse('doc_request_resurrect', kwargs=dict(name=doc.name))))

ietf/doc/views_draft.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,3 +1100,99 @@ def request_publication(request, name):
11001100
),
11011101
context_instance = RequestContext(request))
11021102

1103+
class AdoptDraftForm(forms.Form):
1104+
group = forms.ModelChoiceField(queryset=Group.objects.filter(type__in=["wg", "rg"], state="active").order_by("-type", "acronym"), required=True, empty_label=None)
1105+
comment = forms.CharField(widget=forms.Textarea, required=False, label="Comment", help_text="Optional comment explaining the reasons for the adoption")
1106+
weeks = forms.IntegerField(required=False, label="Expected weeks in adoption state")
1107+
1108+
def __init__(self, *args, **kwargs):
1109+
user = kwargs.pop("user")
1110+
1111+
super(AdoptDraftForm, self).__init__(*args, **kwargs)
1112+
1113+
if has_role(user, "Secretariat"):
1114+
pass # all groups
1115+
else:
1116+
self.fields["group"].queryset = self.fields["group"].queryset.filter(role__person__user=user, role__name__in=("chair", "delegate", "secr")).distinct()
1117+
1118+
self.fields['group'].choices = [(g.pk, '%s - %s' % (g.acronym, g.name)) for g in self.fields["group"].queryset]
1119+
1120+
1121+
@login_required
1122+
def adopt_draft(request, name):
1123+
doc = get_object_or_404(Document, type="draft", name=name)
1124+
1125+
if not can_adopt_draft(request.user, doc):
1126+
return HttpResponseForbidden("You don't have permission to access this view")
1127+
1128+
if request.method == 'POST':
1129+
form = AdoptDraftForm(request.POST, user=request.user)
1130+
1131+
if form.is_valid():
1132+
# adopt
1133+
by = request.user.get_profile()
1134+
1135+
save_document_in_history(doc)
1136+
1137+
doc.time = datetime.datetime.now()
1138+
1139+
group = form.cleaned_data["group"]
1140+
comment = form.cleaned_data["comment"].strip()
1141+
1142+
if group.type.slug == "rg":
1143+
new_stream = StreamName.objects.get(slug="irtf")
1144+
adopt_state_slug = "active"
1145+
else:
1146+
new_stream = StreamName.objects.get(slug="ietf")
1147+
adopt_state_slug = "c-adopt"
1148+
1149+
if doc.stream != new_stream:
1150+
e = DocEvent(type="changed_stream", time=doc.time, by=by, doc=doc)
1151+
e.desc = u"Changed stream to <b>%s</b>" % new_stream.name
1152+
if doc.stream:
1153+
e.desc += u" from %s" % doc.stream.name
1154+
e.save()
1155+
doc.stream = new_stream
1156+
1157+
if group != doc.group:
1158+
e = DocEvent(type="changed_group", time=doc.time, by=by, doc=doc)
1159+
e.desc = u"Changed group to <b>%s (%s)</b>" % (group.name, group.acronym.upper())
1160+
if doc.group.type_id != "individ":
1161+
e.desc += " from %s (%s)" % (doc.group.name, doc.group.acronym.upper())
1162+
e.save()
1163+
doc.group = group
1164+
1165+
doc.save()
1166+
1167+
prev_state = doc.get_state("draft-stream-%s" % doc.stream_id)
1168+
new_state = State.objects.get(slug=adopt_state_slug, type="draft-stream-%s" % doc.stream_id, used=True)
1169+
1170+
if new_state != prev_state:
1171+
doc.set_state(new_state)
1172+
e = add_state_change_event(doc, by, prev_state, new_state, doc.time)
1173+
1174+
due_date = None
1175+
if form.cleaned_data["weeks"] != None:
1176+
due_date = datetime.date.today() + datetime.timedelta(weeks=form.cleaned_data["weeks"])
1177+
1178+
update_reminder(doc, "stream-s", e, due_date)
1179+
1180+
email_stream_state_changed(request, doc, prev_state, new_state, by, comment)
1181+
1182+
if comment:
1183+
e = DocEvent(type="added_comment", time=doc.time, by=by, doc=doc)
1184+
e.desc = comment
1185+
e.save()
1186+
1187+
return HttpResponseRedirect(doc.get_absolute_url())
1188+
else:
1189+
form = AdoptDraftForm(user=request.user)
1190+
1191+
return render_to_response('doc/draft/adopt_draft.html',
1192+
{'doc': doc,
1193+
'form': form,
1194+
},
1195+
context_instance=RequestContext(request))
1196+
1197+
def change_stream_state(request):
1198+
pass
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{% extends "base.html" %}
2+
3+
{% block title %}Adopt {{ doc }} in Group{% endblock %}
4+
5+
{% block morecss %}
6+
form.adopt-draft th { width: 8em; }
7+
form.adopt-draft #id_comment { width: 30em; }
8+
form.adopt-draft #id_weeks { width: 3em; }
9+
form.adopt-draft .actions { text-align: right; padding-top: 1em; }
10+
p.intro { max-width: 50em; }
11+
{% endblock %}
12+
13+
{% block content %}
14+
<h1>Adopt {{ doc }} in Group</h1>
15+
16+
<p class="intro">You can adopt this draft into a group.</p>
17+
18+
<p class="intro">For a WG, the draft enters the IETF stream and the
19+
stream state becomes "Call for Adoption by WG Issued". For an RG, the
20+
draft enters the IRTF stream and the stream state becomes "Active RG
21+
Document".</p>
22+
23+
<form class="adopt-draft" action="" method="post">
24+
{% for field in form.hidden_fields %}{{ field }}{% endfor %}
25+
<table>
26+
{% for field in form.visible_fields %}
27+
<tr>
28+
<th>{{ field.label_tag }}:</th>
29+
<td>{{ field }}
30+
{% if field.help_text %}<div class="help">{{ field.help_text }}</div>{% endif %}
31+
{{ field.errors }}
32+
</td>
33+
</tr>
34+
{% endfor %}
35+
<tr>
36+
<td colspan="2" class="actions">
37+
<a class="button" href="{{ doc.get_absolute_url }}">Cancel</a>
38+
<input class="button" type="submit" value="Adopt Draft"/>
39+
</td>
40+
</tr>
41+
</table>
42+
</form>
43+
{% endblock %}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% autoescape off %}{% filter wordwrap:73 %}
2+
The stream state of {{ doc }} has been changed{% if prev_state %} from {{ prev_state.name }}{% endif %} to {{ new_state.name }} by {{ changed_by }}.
3+
{% if comment %}
4+
5+
Comment:
6+
{{ comment }}
7+
{% endif %}
8+
{% endfilter %}{% endautoescape %}

0 commit comments

Comments
 (0)