Skip to content

Commit 8c88bc5

Browse files
committed
Rewrite change stream state page, moving it to doc/views_draft.py,
port associated tests, make the recommended next states clickable with Javascript so a standard state change is just two clicks (next state and save button) - Legacy-Id: 6288
1 parent f1e0be1 commit 8c88bc5

5 files changed

Lines changed: 341 additions & 25 deletions

File tree

ietf/doc/mails.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from ietf.utils.mail import send_mail, send_mail_text
1111
from ietf.ipr.search import iprs_from_docs, related_docs
12-
from ietf.doc.models import WriteupDocEvent, BallotPositionDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent
12+
from ietf.doc.models import WriteupDocEvent, BallotPositionDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent, DocTagName
1313
from ietf.person.models import Person
1414
from ietf.group.models import Group, Role
1515

@@ -414,7 +414,7 @@ def email_last_call_expired(doc):
414414
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
415415
cc="iesg-secretary@ietf.org")
416416

417-
def stream_state_email_recipients(doc, extra_recipients):
417+
def stream_state_email_recipients(doc, extra_recipients=[]):
418418
persons = set()
419419
res = []
420420
for r in Role.objects.filter(group=doc.group, name__in=("chair", "delegate")).select_related("person", "email"):
@@ -426,14 +426,25 @@ def stream_state_email_recipients(doc, extra_recipients):
426426
res.append(email.formatted_email())
427427
persons.add(email.person)
428428

429-
for x in extra_recipients:
430-
if not x in res:
431-
res.append(x)
429+
for p in extra_recipients:
430+
if not p in persons:
431+
res.append(p.formatted_email())
432+
persons.add(p)
432433

433434
return res
434-
435-
def email_stream_state_changed(request, doc, prev_state, new_state, by, comment="", extra_recipients=[]):
436-
recipients = stream_state_email_recipients(doc, extra_recipients)
435+
436+
def email_draft_adopted(request, doc, by, comment):
437+
recipients = stream_state_email_recipients(doc)
438+
send_mail(request, recipients, settings.DEFAULT_FROM_EMAIL,
439+
u"%s adopted in %s %s" % (doc.name, doc.group.acronym, doc.group.type.name),
440+
'doc/mail/draft_adopted_email.txt',
441+
dict(doc=doc,
442+
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
443+
by=by,
444+
comment=comment))
445+
446+
def email_stream_state_changed(request, doc, prev_state, new_state, by, comment=""):
447+
recipients = stream_state_email_recipients(doc)
437448

438449
state_type = (prev_state or new_state).type
439450

@@ -448,12 +459,20 @@ def email_stream_state_changed(request, doc, prev_state, new_state, by, comment=
448459
by=by,
449460
comment=comment))
450461

451-
def email_draft_adopted(request, doc, by, comment):
452-
recipients = stream_state_email_recipients(doc, [])
462+
def email_stream_tags_changed(request, doc, added_tags, removed_tags, by, comment=""):
463+
extra_recipients = []
464+
465+
if DocTagName.objects.get(slug="sheph-u") in added_tags and doc.shepherd:
466+
extra_recipients.append(doc.shepherd)
467+
468+
recipients = stream_state_email_recipients(doc, extra_recipients)
469+
453470
send_mail(request, recipients, settings.DEFAULT_FROM_EMAIL,
454-
u"%s adopted in %s %s" % (doc.name, doc.group.acronym, doc.group.type.name),
455-
'doc/mail/draft_adopted_email.txt',
471+
u"Tags changed for %s" % doc.name,
472+
'doc/mail/stream_tags_changed_email.txt',
456473
dict(doc=doc,
457474
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
475+
added=added_tags,
476+
removed=removed_tags,
458477
by=by,
459478
comment=comment))

ietf/doc/tests_draft.py

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import debug
1212

1313
from ietf.doc.models import *
14+
from ietf.doc .utils import *
1415
from ietf.name.models import *
1516
from ietf.group.models import *
1617
from ietf.person.models import *
@@ -999,9 +1000,9 @@ def test_adopt_document(self):
9991000

10001001
# get
10011002
r = self.client.get(url)
1002-
self.assertEquals(r.status_code, 200)
1003+
self.assertEqual(r.status_code, 200)
10031004
q = PyQuery(r.content)
1004-
self.assertEquals(len(q('form select[name="group"] option')), 1) # we can only select "mars"
1005+
self.assertEqual(len(q('form select[name="group"] option')), 1) # we can only select "mars"
10051006

10061007
# adopt in mars WG
10071008
mailbox_before = len(outbox)
@@ -1010,13 +1011,100 @@ def test_adopt_document(self):
10101011
dict(comment="some comment",
10111012
group=Group.objects.get(acronym="mars").pk,
10121013
weeks="10"))
1013-
self.assertEquals(r.status_code, 302)
1014+
self.assertEqual(r.status_code, 302)
10141015

10151016
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)
1017+
self.assertEqual(draft.group.acronym, "mars")
1018+
self.assertEqual(draft.stream_id, "ietf")
1019+
self.assertEqual(draft.docevent_set.count() - events_before, 4)
1020+
self.assertEqual(len(outbox), mailbox_before + 1)
10201021
self.assertTrue("adopted" in outbox[-1]["Subject"].lower())
10211022
self.assertTrue("wgchairman@ietf.org" in unicode(outbox[-1]))
10221023
self.assertTrue("wgdelegate@ietf.org" in unicode(outbox[-1]))
1024+
1025+
class ChangeStreamStateTests(django.test.TestCase):
1026+
fixtures = ['names']
1027+
1028+
def test_set_tags(self):
1029+
draft = make_test_data()
1030+
draft.tags = DocTagName.objects.filter(slug="w-expert")
1031+
draft.group.unused_tags.add("w-refdoc")
1032+
1033+
url = urlreverse('doc_change_stream_state', kwargs=dict(name=draft.name))
1034+
login_testing_unauthorized(self, "marschairman", url)
1035+
1036+
# get
1037+
r = self.client.get(url)
1038+
self.assertEqual(r.status_code, 200)
1039+
q = PyQuery(r.content)
1040+
# make sure the unused tags are hidden
1041+
unused = draft.group.unused_tags.values_list("slug", flat=True)
1042+
for t in q("input[name=tags]"):
1043+
self.assertTrue(t.attrib["value"] not in unused)
1044+
1045+
# set tags
1046+
mailbox_before = len(outbox)
1047+
events_before = draft.docevent_set.count()
1048+
r = self.client.post(url,
1049+
dict(new_state=draft.get_state("draft-stream-%s" % draft.stream_id).pk,
1050+
comment="some comment",
1051+
weeks="10",
1052+
tags=["need-aut", "sheph-u"],
1053+
))
1054+
self.assertEqual(r.status_code, 302)
1055+
1056+
draft = Document.objects.get(pk=draft.pk)
1057+
self.assertEqual(draft.tags.count(), 2)
1058+
self.assertEqual(draft.tags.filter(slug="w-expert").count(), 0)
1059+
self.assertEqual(draft.tags.filter(slug="need-aut").count(), 1)
1060+
self.assertEqual(draft.tags.filter(slug="sheph-u").count(), 1)
1061+
self.assertEqual(draft.docevent_set.count() - events_before, 2)
1062+
self.assertEqual(len(outbox), mailbox_before + 1)
1063+
self.assertTrue("tags changed" in outbox[-1]["Subject"].lower())
1064+
self.assertTrue("wgchairman@ietf.org" in unicode(outbox[-1]))
1065+
self.assertTrue("wgdelegate@ietf.org" in unicode(outbox[-1]))
1066+
self.assertTrue("plain@example.com" in unicode(outbox[-1]))
1067+
1068+
def test_set_state(self):
1069+
draft = make_test_data()
1070+
1071+
url = urlreverse('doc_change_stream_state', kwargs=dict(name=draft.name))
1072+
login_testing_unauthorized(self, "marschairman", url)
1073+
1074+
# get
1075+
r = self.client.get(url)
1076+
self.assertEqual(r.status_code, 200)
1077+
q = PyQuery(r.content)
1078+
# make sure the unused states are hidden
1079+
unused = draft.group.unused_states.values_list("pk", flat=True)
1080+
for t in q("select[name=new_state]").find("option[name=tags]"):
1081+
self.assertTrue(t.attrib["value"] not in unused)
1082+
self.assertEqual(len(q('select[name=new_state]')), 1)
1083+
1084+
# set new state
1085+
old_state = draft.get_state("draft-stream-%s" % draft.stream_id )
1086+
new_state = State.objects.get(used=True, type="draft-stream-%s" % draft.stream_id, slug="parked")
1087+
self.assertNotEqual(old_state, new_state)
1088+
mailbox_before = len(outbox)
1089+
events_before = draft.docevent_set.count()
1090+
1091+
r = self.client.post(url,
1092+
dict(new_state=new_state.pk,
1093+
comment="some comment",
1094+
weeks="10",
1095+
tags=[t.pk for t in draft.tags.filter(slug__in=get_tags_for_stream_id(draft.stream_id))],
1096+
))
1097+
self.assertEqual(r.status_code, 302)
1098+
1099+
draft = Document.objects.get(pk=draft.pk)
1100+
self.assertEqual(draft.get_state("draft-stream-%s" % draft.stream_id), new_state)
1101+
self.assertEqual(draft.docevent_set.count() - events_before, 2)
1102+
reminder = DocReminder.objects.filter(event__doc=draft, type="stream-s")
1103+
self.assertEqual(len(reminder), 1)
1104+
due = datetime.datetime.now() + datetime.timedelta(weeks=10)
1105+
self.assertTrue(due - datetime.timedelta(days=1) <= reminder[0].due <= due + datetime.timedelta(days=1))
1106+
self.assertEqual(len(outbox), mailbox_before + 1)
1107+
self.assertTrue("state changed" in outbox[-1]["Subject"].lower())
1108+
self.assertTrue("wgchairman@ietf.org" in unicode(outbox[-1]))
1109+
self.assertTrue("wgdelegate@ietf.org" in unicode(outbox[-1]))
1110+

ietf/doc/views_draft.py

Lines changed: 132 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from django.conf import settings
1414
from django.forms.util import ErrorList
1515
from django.contrib.auth.decorators import login_required
16+
from django.template.defaultfilters import pluralize
1617

1718
from ietf.utils.mail import send_mail_text, send_mail_message
1819
from ietf.ietfauth.decorators import role_required
@@ -1123,7 +1124,7 @@ def adopt_draft(request, name):
11231124
doc = get_object_or_404(Document, type="draft", name=name)
11241125

11251126
if not can_adopt_draft(request.user, doc):
1126-
return HttpResponseForbidden("You don't have permission to access this view")
1127+
return HttpResponseForbidden("You don't have permission to access this page")
11271128

11281129
if request.method == 'POST':
11291130
form = AdoptDraftForm(request.POST, user=request.user)
@@ -1137,15 +1138,14 @@ def adopt_draft(request, name):
11371138
doc.time = datetime.datetime.now()
11381139

11391140
group = form.cleaned_data["group"]
1140-
comment = form.cleaned_data["comment"].strip()
1141-
11421141
if group.type.slug == "rg":
11431142
new_stream = StreamName.objects.get(slug="irtf")
11441143
adopt_state_slug = "active"
11451144
else:
11461145
new_stream = StreamName.objects.get(slug="ietf")
11471146
adopt_state_slug = "c-adopt"
11481147

1148+
# stream
11491149
if doc.stream != new_stream:
11501150
e = DocEvent(type="changed_stream", time=doc.time, by=by, doc=doc)
11511151
e.desc = u"Changed stream to <b>%s</b>" % new_stream.name
@@ -1154,6 +1154,7 @@ def adopt_draft(request, name):
11541154
e.save()
11551155
doc.stream = new_stream
11561156

1157+
# group
11571158
if group != doc.group:
11581159
e = DocEvent(type="changed_group", time=doc.time, by=by, doc=doc)
11591160
e.desc = u"Changed group to <b>%s (%s)</b>" % (group.name, group.acronym.upper())
@@ -1164,9 +1165,9 @@ def adopt_draft(request, name):
11641165

11651166
doc.save()
11661167

1168+
# state
11671169
prev_state = doc.get_state("draft-stream-%s" % doc.stream_id)
11681170
new_state = State.objects.get(slug=adopt_state_slug, type="draft-stream-%s" % doc.stream_id, used=True)
1169-
11701171
if new_state != prev_state:
11711172
doc.set_state(new_state)
11721173
e = add_state_change_event(doc, by, prev_state, new_state, doc.time)
@@ -1177,6 +1178,8 @@ def adopt_draft(request, name):
11771178

11781179
update_reminder(doc, "stream-s", e, due_date)
11791180

1181+
# comment
1182+
comment = form.cleaned_data["comment"].strip()
11801183
if comment:
11811184
e = DocEvent(type="added_comment", time=doc.time, by=by, doc=doc)
11821185
e.desc = comment
@@ -1194,5 +1197,128 @@ def adopt_draft(request, name):
11941197
},
11951198
context_instance=RequestContext(request))
11961199

1197-
def change_stream_state(request):
1198-
pass
1200+
class ChangeStreamStateForm(forms.Form):
1201+
new_state = forms.ModelChoiceField(queryset=State.objects.filter(used=True), label='State')
1202+
weeks = forms.IntegerField(label='Expected weeks in state',required=False)
1203+
comment = forms.CharField(widget=forms.Textarea, required=False, help_text="Optional comment for the document history")
1204+
tags = forms.ModelMultipleChoiceField(queryset=DocTagName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False)
1205+
1206+
def __init__(self, *args, **kwargs):
1207+
doc = kwargs.pop("doc")
1208+
state_type = kwargs.pop("state_type")
1209+
super(ChangeStreamStateForm, self).__init__(*args, **kwargs)
1210+
1211+
f = self.fields["new_state"]
1212+
f.queryset = f.queryset.filter(type=state_type)
1213+
if doc.group:
1214+
unused_states = doc.group.unused_states.values_list("pk", flat=True)
1215+
f.queryset = f.queryset.exclude(pk__in=unused_states)
1216+
f.label = state_type.label
1217+
1218+
f = self.fields['tags']
1219+
f.queryset = f.queryset.filter(slug__in=get_tags_for_stream_id(doc.stream_id))
1220+
if doc.group:
1221+
unused_tags = doc.group.unused_tags.values_list("pk", flat=True)
1222+
f.queryset = f.queryset.exclude(pk__in=unused_tags)
1223+
1224+
def next_states_for_stream_state(doc, state_type, current_state):
1225+
# find next states
1226+
next_states = []
1227+
if current_state:
1228+
next_states = current_state.next_states.all()
1229+
1230+
if doc.stream_id == "ietf" and doc.group:
1231+
transitions = doc.group.groupstatetransitions_set.filter(state=current_state)
1232+
if transitions:
1233+
next_states = transitions[0].next_states.all()
1234+
else:
1235+
# return the initial state
1236+
states = State.objects.filter(used=True, type=state_type).order_by('order')
1237+
if states:
1238+
next_states = states[:1]
1239+
1240+
if doc.group:
1241+
unused_states = doc.group.unused_states.values_list("pk", flat=True)
1242+
next_states = [n for n in next_states if n.pk not in unused_states]
1243+
1244+
return next_states
1245+
1246+
@login_required
1247+
def change_stream_state(request, name):
1248+
doc = get_object_or_404(Document, type="draft", name=name)
1249+
if not doc.stream:
1250+
raise Http404
1251+
1252+
if not is_authorized_in_doc_stream(request.user, doc):
1253+
return HttpResponseForbidden("You don't have permission to access this page")
1254+
1255+
state_type = StateType.objects.get(slug="draft-stream-%s" % doc.stream_id)
1256+
prev_state = doc.get_state(state_type.slug)
1257+
next_states = next_states_for_stream_state(doc, state_type, prev_state)
1258+
1259+
if request.method == 'POST':
1260+
form = ChangeStreamStateForm(request.POST, doc=doc, state_type=state_type)
1261+
if form.is_valid():
1262+
by = request.user.get_profile()
1263+
1264+
save_document_in_history(doc)
1265+
1266+
doc.time = datetime.datetime.now()
1267+
comment = form.cleaned_data["comment"].strip()
1268+
1269+
# state
1270+
new_state = form.cleaned_data["new_state"]
1271+
if new_state != prev_state:
1272+
doc.set_state(new_state)
1273+
e = add_state_change_event(doc, by, prev_state, new_state, doc.time)
1274+
1275+
due_date = None
1276+
if form.cleaned_data["weeks"] != None:
1277+
due_date = datetime.date.today() + datetime.timedelta(weeks=form.cleaned_data["weeks"])
1278+
1279+
update_reminder(doc, "stream-s", e, due_date)
1280+
1281+
email_stream_state_changed(request, doc, prev_state, new_state, by, comment)
1282+
1283+
# tags
1284+
existing_tags = set(doc.tags.all())
1285+
new_tags = set(form.cleaned_data["tags"])
1286+
1287+
if existing_tags != new_tags:
1288+
doc.tags = new_tags
1289+
1290+
e = DocEvent(type="changed_document", time=doc.time, by=by, doc=doc)
1291+
added_tags = new_tags - existing_tags
1292+
removed_tags = existing_tags - new_tags
1293+
l = []
1294+
if added_tags:
1295+
l.append(u"Tag%s %s set." % (pluralize(added_tags), ", ".join(t.name for t in added_tags)))
1296+
if removed_tags:
1297+
l.append(u"Tag%s %s cleared." % (pluralize(removed_tags), ", ".join(t.name for t in removed_tags)))
1298+
e.desc = " ".join(l)
1299+
e.save()
1300+
1301+
email_stream_tags_changed(request, doc, added_tags, removed_tags, by, comment)
1302+
1303+
# comment
1304+
if comment:
1305+
e = DocEvent(type="added_comment", time=doc.time, by=by, doc=doc)
1306+
e.desc = comment
1307+
e.save()
1308+
1309+
return HttpResponseRedirect(doc.get_absolute_url())
1310+
else:
1311+
form = ChangeStreamStateForm(initial=dict(new_state=prev_state.pk),
1312+
doc=doc, state_type=state_type)
1313+
1314+
milestones = doc.groupmilestone_set.all()
1315+
1316+
1317+
return render_to_response("doc/draft/change_stream_state.html",
1318+
{"doc": doc,
1319+
"form": form,
1320+
"milestones": milestones,
1321+
"state_type": state_type,
1322+
"next_states": next_states,
1323+
},
1324+
context_instance=RequestContext(request))

0 commit comments

Comments
 (0)