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:
parent
c760b48b38
commit
c508a5672d
|
@ -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))
|
||||
|
|
|
@ -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]))
|
||||
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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:]
|
||||
|
|
|
@ -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))))
|
||||
|
|
|
@ -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
|
||||
|
|
43
ietf/templates/doc/draft/adopt_draft.html
Normal file
43
ietf/templates/doc/draft/adopt_draft.html
Normal 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 %}
|
8
ietf/templates/doc/mail/stream_state_changed_email.txt
Normal file
8
ietf/templates/doc/mail/stream_state_changed_email.txt
Normal 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 %}
|
Loading…
Reference in a new issue