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
This commit is contained in:
Ole Laursen 2013-09-27 14:19:27 +00:00
parent f1e0be1033
commit 8c88bc5aec
5 changed files with 341 additions and 25 deletions

View file

@ -9,7 +9,7 @@ from django.core.urlresolvers import reverse as urlreverse
from ietf.utils.mail import send_mail, send_mail_text
from ietf.ipr.search import iprs_from_docs, related_docs
from ietf.doc.models import WriteupDocEvent, BallotPositionDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent
from ietf.doc.models import WriteupDocEvent, BallotPositionDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent, DocTagName
from ietf.person.models import Person
from ietf.group.models import Group, Role
@ -414,7 +414,7 @@ def email_last_call_expired(doc):
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
cc="iesg-secretary@ietf.org")
def stream_state_email_recipients(doc, extra_recipients):
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"):
@ -426,14 +426,25 @@ def stream_state_email_recipients(doc, extra_recipients):
res.append(email.formatted_email())
persons.add(email.person)
for x in extra_recipients:
if not x in res:
res.append(x)
for p in extra_recipients:
if not p in persons:
res.append(p.formatted_email())
persons.add(p)
return res
def email_stream_state_changed(request, doc, prev_state, new_state, by, comment="", extra_recipients=[]):
recipients = stream_state_email_recipients(doc, extra_recipients)
def email_draft_adopted(request, doc, by, comment):
recipients = stream_state_email_recipients(doc)
send_mail(request, recipients, settings.DEFAULT_FROM_EMAIL,
u"%s adopted in %s %s" % (doc.name, doc.group.acronym, doc.group.type.name),
'doc/mail/draft_adopted_email.txt',
dict(doc=doc,
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
by=by,
comment=comment))
def email_stream_state_changed(request, doc, prev_state, new_state, by, comment=""):
recipients = stream_state_email_recipients(doc)
state_type = (prev_state or new_state).type
@ -448,12 +459,20 @@ def email_stream_state_changed(request, doc, prev_state, new_state, by, comment=
by=by,
comment=comment))
def email_draft_adopted(request, doc, by, comment):
recipients = stream_state_email_recipients(doc, [])
def email_stream_tags_changed(request, doc, added_tags, removed_tags, by, comment=""):
extra_recipients = []
if DocTagName.objects.get(slug="sheph-u") in added_tags and doc.shepherd:
extra_recipients.append(doc.shepherd)
recipients = stream_state_email_recipients(doc, extra_recipients)
send_mail(request, recipients, settings.DEFAULT_FROM_EMAIL,
u"%s adopted in %s %s" % (doc.name, doc.group.acronym, doc.group.type.name),
'doc/mail/draft_adopted_email.txt',
u"Tags changed for %s" % doc.name,
'doc/mail/stream_tags_changed_email.txt',
dict(doc=doc,
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
added=added_tags,
removed=removed_tags,
by=by,
comment=comment))

View file

@ -11,6 +11,7 @@ from pyquery import PyQuery
import debug
from ietf.doc.models import *
from ietf.doc .utils import *
from ietf.name.models import *
from ietf.group.models import *
from ietf.person.models import *
@ -999,9 +1000,9 @@ class AdoptDraftTests(django.test.TestCase):
# get
r = self.client.get(url)
self.assertEquals(r.status_code, 200)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEquals(len(q('form select[name="group"] option')), 1) # we can only select "mars"
self.assertEqual(len(q('form select[name="group"] option')), 1) # we can only select "mars"
# adopt in mars WG
mailbox_before = len(outbox)
@ -1010,13 +1011,100 @@ class AdoptDraftTests(django.test.TestCase):
dict(comment="some comment",
group=Group.objects.get(acronym="mars").pk,
weeks="10"))
self.assertEquals(r.status_code, 302)
self.assertEqual(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.assertEqual(draft.group.acronym, "mars")
self.assertEqual(draft.stream_id, "ietf")
self.assertEqual(draft.docevent_set.count() - events_before, 4)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("adopted" in outbox[-1]["Subject"].lower())
self.assertTrue("wgchairman@ietf.org" in unicode(outbox[-1]))
self.assertTrue("wgdelegate@ietf.org" in unicode(outbox[-1]))
class ChangeStreamStateTests(django.test.TestCase):
fixtures = ['names']
def test_set_tags(self):
draft = make_test_data()
draft.tags = DocTagName.objects.filter(slug="w-expert")
draft.group.unused_tags.add("w-refdoc")
url = urlreverse('doc_change_stream_state', kwargs=dict(name=draft.name))
login_testing_unauthorized(self, "marschairman", url)
# get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
# make sure the unused tags are hidden
unused = draft.group.unused_tags.values_list("slug", flat=True)
for t in q("input[name=tags]"):
self.assertTrue(t.attrib["value"] not in unused)
# set tags
mailbox_before = len(outbox)
events_before = draft.docevent_set.count()
r = self.client.post(url,
dict(new_state=draft.get_state("draft-stream-%s" % draft.stream_id).pk,
comment="some comment",
weeks="10",
tags=["need-aut", "sheph-u"],
))
self.assertEqual(r.status_code, 302)
draft = Document.objects.get(pk=draft.pk)
self.assertEqual(draft.tags.count(), 2)
self.assertEqual(draft.tags.filter(slug="w-expert").count(), 0)
self.assertEqual(draft.tags.filter(slug="need-aut").count(), 1)
self.assertEqual(draft.tags.filter(slug="sheph-u").count(), 1)
self.assertEqual(draft.docevent_set.count() - events_before, 2)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("tags changed" in outbox[-1]["Subject"].lower())
self.assertTrue("wgchairman@ietf.org" in unicode(outbox[-1]))
self.assertTrue("wgdelegate@ietf.org" in unicode(outbox[-1]))
self.assertTrue("plain@example.com" in unicode(outbox[-1]))
def test_set_state(self):
draft = make_test_data()
url = urlreverse('doc_change_stream_state', kwargs=dict(name=draft.name))
login_testing_unauthorized(self, "marschairman", url)
# get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
# make sure the unused states are hidden
unused = draft.group.unused_states.values_list("pk", flat=True)
for t in q("select[name=new_state]").find("option[name=tags]"):
self.assertTrue(t.attrib["value"] not in unused)
self.assertEqual(len(q('select[name=new_state]')), 1)
# set new state
old_state = draft.get_state("draft-stream-%s" % draft.stream_id )
new_state = State.objects.get(used=True, type="draft-stream-%s" % draft.stream_id, slug="parked")
self.assertNotEqual(old_state, new_state)
mailbox_before = len(outbox)
events_before = draft.docevent_set.count()
r = self.client.post(url,
dict(new_state=new_state.pk,
comment="some comment",
weeks="10",
tags=[t.pk for t in draft.tags.filter(slug__in=get_tags_for_stream_id(draft.stream_id))],
))
self.assertEqual(r.status_code, 302)
draft = Document.objects.get(pk=draft.pk)
self.assertEqual(draft.get_state("draft-stream-%s" % draft.stream_id), new_state)
self.assertEqual(draft.docevent_set.count() - events_before, 2)
reminder = DocReminder.objects.filter(event__doc=draft, type="stream-s")
self.assertEqual(len(reminder), 1)
due = datetime.datetime.now() + datetime.timedelta(weeks=10)
self.assertTrue(due - datetime.timedelta(days=1) <= reminder[0].due <= due + datetime.timedelta(days=1))
self.assertEqual(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

@ -13,6 +13,7 @@ from django.db.models import Max
from django.conf import settings
from django.forms.util import ErrorList
from django.contrib.auth.decorators import login_required
from django.template.defaultfilters import pluralize
from ietf.utils.mail import send_mail_text, send_mail_message
from ietf.ietfauth.decorators import role_required
@ -1123,7 +1124,7 @@ 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")
return HttpResponseForbidden("You don't have permission to access this page")
if request.method == 'POST':
form = AdoptDraftForm(request.POST, user=request.user)
@ -1137,8 +1138,6 @@ def adopt_draft(request, name):
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"
@ -1146,6 +1145,7 @@ def adopt_draft(request, name):
new_stream = StreamName.objects.get(slug="ietf")
adopt_state_slug = "c-adopt"
# stream
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
@ -1154,6 +1154,7 @@ def adopt_draft(request, name):
e.save()
doc.stream = new_stream
# group
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())
@ -1164,9 +1165,9 @@ def adopt_draft(request, name):
doc.save()
# state
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)
@ -1177,6 +1178,8 @@ def adopt_draft(request, name):
update_reminder(doc, "stream-s", e, due_date)
# comment
comment = form.cleaned_data["comment"].strip()
if comment:
e = DocEvent(type="added_comment", time=doc.time, by=by, doc=doc)
e.desc = comment
@ -1194,5 +1197,128 @@ def adopt_draft(request, name):
},
context_instance=RequestContext(request))
def change_stream_state(request):
pass
class ChangeStreamStateForm(forms.Form):
new_state = forms.ModelChoiceField(queryset=State.objects.filter(used=True), label='State')
weeks = forms.IntegerField(label='Expected weeks in state',required=False)
comment = forms.CharField(widget=forms.Textarea, required=False, help_text="Optional comment for the document history")
tags = forms.ModelMultipleChoiceField(queryset=DocTagName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False)
def __init__(self, *args, **kwargs):
doc = kwargs.pop("doc")
state_type = kwargs.pop("state_type")
super(ChangeStreamStateForm, self).__init__(*args, **kwargs)
f = self.fields["new_state"]
f.queryset = f.queryset.filter(type=state_type)
if doc.group:
unused_states = doc.group.unused_states.values_list("pk", flat=True)
f.queryset = f.queryset.exclude(pk__in=unused_states)
f.label = state_type.label
f = self.fields['tags']
f.queryset = f.queryset.filter(slug__in=get_tags_for_stream_id(doc.stream_id))
if doc.group:
unused_tags = doc.group.unused_tags.values_list("pk", flat=True)
f.queryset = f.queryset.exclude(pk__in=unused_tags)
def next_states_for_stream_state(doc, state_type, current_state):
# find next states
next_states = []
if current_state:
next_states = current_state.next_states.all()
if doc.stream_id == "ietf" and doc.group:
transitions = doc.group.groupstatetransitions_set.filter(state=current_state)
if transitions:
next_states = transitions[0].next_states.all()
else:
# return the initial state
states = State.objects.filter(used=True, type=state_type).order_by('order')
if states:
next_states = states[:1]
if doc.group:
unused_states = doc.group.unused_states.values_list("pk", flat=True)
next_states = [n for n in next_states if n.pk not in unused_states]
return next_states
@login_required
def change_stream_state(request, name):
doc = get_object_or_404(Document, type="draft", name=name)
if not doc.stream:
raise Http404
if not is_authorized_in_doc_stream(request.user, doc):
return HttpResponseForbidden("You don't have permission to access this page")
state_type = StateType.objects.get(slug="draft-stream-%s" % doc.stream_id)
prev_state = doc.get_state(state_type.slug)
next_states = next_states_for_stream_state(doc, state_type, prev_state)
if request.method == 'POST':
form = ChangeStreamStateForm(request.POST, doc=doc, state_type=state_type)
if form.is_valid():
by = request.user.get_profile()
save_document_in_history(doc)
doc.time = datetime.datetime.now()
comment = form.cleaned_data["comment"].strip()
# state
new_state = form.cleaned_data["new_state"]
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)
# tags
existing_tags = set(doc.tags.all())
new_tags = set(form.cleaned_data["tags"])
if existing_tags != new_tags:
doc.tags = new_tags
e = DocEvent(type="changed_document", time=doc.time, by=by, doc=doc)
added_tags = new_tags - existing_tags
removed_tags = existing_tags - new_tags
l = []
if added_tags:
l.append(u"Tag%s %s set." % (pluralize(added_tags), ", ".join(t.name for t in added_tags)))
if removed_tags:
l.append(u"Tag%s %s cleared." % (pluralize(removed_tags), ", ".join(t.name for t in removed_tags)))
e.desc = " ".join(l)
e.save()
email_stream_tags_changed(request, doc, added_tags, removed_tags, by, comment)
# 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 = ChangeStreamStateForm(initial=dict(new_state=prev_state.pk),
doc=doc, state_type=state_type)
milestones = doc.groupmilestone_set.all()
return render_to_response("doc/draft/change_stream_state.html",
{"doc": doc,
"form": form,
"milestones": milestones,
"state_type": state_type,
"next_states": next_states,
},
context_instance=RequestContext(request))

View file

@ -0,0 +1,73 @@
{% extends "base.html" %}
{% block title %}Change {{ state_type.label }} of {{ doc }}{% endblock %}
{% block morecss %}
.next-states { margin-bottom: 1em; }
.next-states a {
display: inline-block;
margin: 0 0.2em;
padding: 0.2em 0.3em;
border-radius: 0.2em;
cursor: pointer;
color: #333;
font-weight: bold;
}
.next-states a:hover { background-color: #eee; transition-duration: 0.2s; }
form.change-state th { max-width: 8em; }
form.change-state th, form.change-state td { padding-bottom: 0.3em; }
form.change-state #id_comment { width: 30em; }
form.change-state #id_weeks { width: 2em;}
form.change-state .actions { text-align: right; padding-top: 1em; }
form.change-state ul { padding: 0; margin: 0; }
form.change-state ul li { padding: 0; padding-bottom: 0.1em; margin: 0; list-style-type: none; }
form.change-state ul li input { vertical-align: middle; }
form.change-state ul li label { cursor: pointer; }
{% endblock %}
{% block content %}
<h1>Change {{ state_type.label }} of {{ doc }}</h1>
<p class="intro">For help on the states, see the <a href="{% url state_help type=state_type.slug %}">state table</a>.</p>
{% if next_states %}
<div class="next-states">
<span>Recommended next state{{next_states|pluralize}}:</span>
{% for state in next_states %}<a tabindex="0" data-state="{{ state.pk }}">{{ state.name }}</a> {% if not forloop.last %} or {% endif %}{% endfor %}
</div>
{% endif %}
<form class="change-state" action="" method="post">
<table cellspacing="0">
{% 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="Save"/>
</td>
</tr>
</table>
</form>
{% endblock %}
{% block js %}
<script>
jQuery(document).ready(function () {
jQuery(".next-states a").click(function (e) {
e.preventDefault();
var s = jQuery(this).data("state");
console.log(s)
jQuery("#id_new_state").val(s);
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,10 @@
{% autoescape off %}{% filter wordwrap:73 %}
The tags on {{ doc }} have been changed by {{ by }}:
{{ url }}
{% if added %}Tag{{ added|pluralize }} {% for t in added %}"{{ t }}"{% if not forloop.last %}, {% endif %}{% endfor %} added.{% endif %}
{% if removed %}Tag{{ removed|pluralize }} {% for t in removed %}"{{ t }}"{% if not forloop.last %}, {% endif %}{% endfor %} cleared.{% endif %}
{% if comment %}
Comment:
{{ comment }}{% endif %}{% endfilter %}{% endautoescape %}