Skip to content

Commit f029c88

Browse files
committed
Merged [6629] from tterriberry@mozilla.com:
Allow wgchairs to edit replaces relationships. This actually allows anyone with can_edit_stream_info permission to edit the list. This does draft name completion, but does not currently filter those names for likely replacements. Styling is also basically non-existent. Fixes ietf-tools#1002 - Legacy-Id: 6639 Note: SVN reference [6629] has been migrated to Git commit 9ef29a3
2 parents 0308b2d + 9ef29a3 commit f029c88

4 files changed

Lines changed: 240 additions & 2 deletions

File tree

ietf/doc/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,10 @@
7575
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/submit-to-iesg/$', views_draft.to_iesg, name='doc_to_iesg'),
7676
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/resurrect/$', views_draft.resurrect, name='doc_resurrect'),
7777
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/addcomment/$', views_doc.add_comment, name='doc_add_comment'),
78+
url(r'^ajax/internet_draft/?$', views_draft.doc_ajax_internet_draft, name="doc_ajax_internet_draft"),
7879

7980
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/stream/$', views_draft.change_stream, name='doc_change_stream'),
81+
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/replaces/$', views_draft.replaces, name='doc_change_replaces'),
8082
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/notify/$', views_draft.edit_notices, name='doc_change_notify'),
8183
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/status/$', views_draft.change_intention, name='doc_change_intended_status'),
8284
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/telechat/$', views_draft.telechat_date, name='doc_change_telechat_date'),

ietf/doc/views_draft.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.template.loader import render_to_string
99
from django.template import RequestContext
1010
from django import forms
11+
from django.utils import simplejson
1112
from django.utils.html import strip_tags
1213
from django.db.models import Max
1314
from django.conf import settings
@@ -23,6 +24,7 @@
2324
from ietf.utils.textupload import get_cleaned_text_file_content
2425
from ietf.person.forms import EmailsField
2526
from ietf.group.models import Group
27+
from ietf.secr.lib import jsonapi
2628

2729
from ietf.ietfworkflows.models import Stream
2830
from ietf.ietfworkflows.utils import update_stream
@@ -268,6 +270,143 @@ def change_stream(request, name):
268270
),
269271
context_instance=RequestContext(request))
270272

273+
@jsonapi
274+
def doc_ajax_internet_draft(request):
275+
if request.method != 'GET' or not request.GET.has_key('term'):
276+
return { 'success' : False, 'error' : 'No term submitted or not GET' }
277+
q = request.GET.get('term')
278+
results = DocAlias.objects.filter(name__icontains=q)
279+
if (results.count() > 20):
280+
results = results[:20]
281+
elif results.count() == 0:
282+
return { 'success' : False, 'error' : "No results" }
283+
response = [dict(id=r.id, label=r.name) for r in results]
284+
return response
285+
286+
def collect_email_addresses(emails, doc):
287+
for author in doc.authors.all():
288+
if author.address not in emails:
289+
emails[author.address] = '"%s"' % (author.person.name)
290+
if doc.group.acronym != 'none':
291+
for role in doc.group.role_set.filter(name='chair'):
292+
if role.email.address not in emails:
293+
emails[role.email.address] = '"%s"' % (role.person.name)
294+
if doc.group.type.slug == 'wg':
295+
address = '%s-ads@tools.ietf.org' % doc.group.acronym
296+
if address not in emails:
297+
emails[address] = '"%s-ads"' % (doc.group.acronym)
298+
elif doc.group.type.slug == 'rg':
299+
email = doc.group.parent.role_set.filter(name='char')[0].email
300+
if email.address not in emails:
301+
emails[email.address] = '"%s"' % (email.person.name)
302+
if doc.shepherd:
303+
address = doc.shepherd.email_address();
304+
if address not in emails:
305+
emails[address] = '"%s"' % (doc.shepherd.name)
306+
return emails
307+
308+
class ReplacesForm(forms.Form):
309+
replaces = forms.CharField(max_length=512,widget=forms.HiddenInput)
310+
comment = forms.CharField(widget=forms.Textarea, required=False)
311+
312+
def __init__(self, *args, **kwargs):
313+
self.doc = kwargs.pop('doc')
314+
super(ReplacesForm, self).__init__(*args, **kwargs)
315+
drafts = {}
316+
for d in self.doc.related_that_doc("replaces"):
317+
drafts[d.id] = d.document.name
318+
self.initial['replaces'] = simplejson.dumps(drafts)
319+
320+
def clean_replaces(self):
321+
data = self.cleaned_data['replaces'].strip()
322+
if data:
323+
ids = [int(x) for x in simplejson.loads(data)]
324+
else:
325+
return []
326+
objects = []
327+
for id in ids:
328+
try:
329+
d = DocAlias.objects.get(pk=id)
330+
except DocAlias.DoesNotExist, e:
331+
raise forms.ValidationError("ERROR: %s not found for id %d" % DocAlias._meta.verbos_name, id)
332+
if d.document == self.doc:
333+
raise forms.ValidationError("ERROR: A draft can't replace itself")
334+
if d.document.type_id == "draft" and d.document.get_state_slug() == "rfc":
335+
raise forms.ValidationError("ERROR: A draft can't replace an RFC")
336+
objects.append(d)
337+
return objects
338+
339+
def replaces(request, name):
340+
"""Change 'replaces' set of a Document of type 'draft' , notifying parties
341+
as necessary and logging the change as a comment."""
342+
doc = get_object_or_404(Document, docalias__name=name)
343+
if doc.type_id != 'draft':
344+
raise Http404
345+
if not (has_role(request.user, ("Secretariat", "Area Director"))
346+
or is_authorized_in_doc_stream(request.user, doc)):
347+
return HttpResponseForbidden("You do not have the necessary permissions to view this page")
348+
login = request.user.get_profile()
349+
if request.method == 'POST':
350+
form = ReplacesForm(request.POST, doc=doc)
351+
if form.is_valid():
352+
new_replaces = set(form.cleaned_data['replaces'])
353+
comment = form.cleaned_data['comment'].strip()
354+
old_replaces = set(doc.related_that_doc("replaces"))
355+
if new_replaces != old_replaces:
356+
save_document_in_history(doc)
357+
emails = {}
358+
emails = collect_email_addresses(emails, doc)
359+
relationship = DocRelationshipName.objects.get(slug='replaces')
360+
for d in old_replaces:
361+
if d not in new_replaces:
362+
emails = collect_email_addresses(emails, d.document)
363+
RelatedDocument.objects.filter(source=doc, target=d,
364+
relationship=relationship).delete()
365+
for d in new_replaces:
366+
if d not in old_replaces:
367+
emails = collect_email_addresses(emails, d.document)
368+
RelatedDocument.objects.create(source=doc, target=d,
369+
relationship=relationship)
370+
e = DocEvent(doc=doc,by=login,type='changed_document')
371+
new_replaces_names = ", ".join([d.name for d in new_replaces])
372+
if not new_replaces_names:
373+
new_replaces_names = "None"
374+
old_replaces_names = ", ".join([d.name for d in old_replaces])
375+
if not old_replaces_names:
376+
old_replaces_names = "None"
377+
e.desc = u"Set of documents this document replaces changed to <b>%s</b> from %s"% (new_replaces_names, old_replaces_names)
378+
e.save()
379+
email_desc = e.desc
380+
if comment:
381+
c = DocEvent(doc=doc,by=login,type="added_comment")
382+
c.desc = comment
383+
c.save()
384+
email_desc += "\n"+c.desc
385+
doc.time = e.time
386+
doc.save()
387+
email_list = []
388+
for key in sorted(emails):
389+
if emails[key]:
390+
email_list.append('%s <%s>' % (emails[key], key))
391+
else:
392+
email_list.append('<%s>' % key)
393+
email_string = ", ".join(email_list)
394+
send_mail(request, email_string,
395+
"DraftTracker Mail System <iesg-secretary@ietf.org>",
396+
"%s updated by %s" % (doc.file_tag, login),
397+
"doc/mail/change_notice.txt",
398+
dict(text=html_to_text(email_desc),
399+
doc=doc,
400+
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()))
401+
return HttpResponseRedirect(doc.get_absolute_url())
402+
else:
403+
form = ReplacesForm(doc=doc)
404+
return render_to_response('idrfc/change_replaces.html',
405+
dict(form=form,
406+
doc=doc,
407+
),
408+
context_instance=RequestContext(request))
409+
271410
class ChangeIntentionForm(forms.Form):
272411
intended_std_level = forms.ModelChoiceField(IntendedStdLevelName.objects.filter(used=True), empty_label="(None)", required=True, label="Intended RFC status")
273412
comment = forms.CharField(widget=forms.Textarea, required=False)

ietf/templates/doc/document_draft.html

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,19 @@
3535
{% if resurrected_by %}- resurrect requested by {{ resurrected_by }}{% endif %}
3636
{% endif %}
3737

38-
39-
{% if replaces %}<div>Replaces: {{ replaces|join:", "|urlize_ietf_docs }}</div>{% endif %}
38+
{% if can_edit_stream_info %}
39+
<div>
40+
{% if replaces %}
41+
<a class="editlink" href="{% url doc_change_replaces name=doc.name %}">Replaces</a>:
42+
{{ replaces|join:", "|urlize_ietf_docs }}
43+
{% else %}
44+
<a class="editlink" href="{% url doc_change_replaces name=doc.name %}">Replaces: None</a>
45+
{% endif %}
46+
</a>
47+
</div>
48+
{% else %}
49+
{% if replaces %}<div>Replaces: {{ replaces|join:", "|urlize_ietf_docs }}</div>{% endif %}
50+
{% endif %}
4051
{% if replaced_by %}<div>Replaced by: {{ replaced_by|join:", "|urlize_ietf_docs }}</div>{% endif %}
4152
</td>
4253
</tr>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
{% extends "base.html" %}
2+
{% block title %}Change which documents {{ doc }} replaces{% endblock %}
3+
{% block pagehead %}{{ block.super }}
4+
<!--TODO: The template will later load another version of jQuery.
5+
We should eliminate the duplication.-->
6+
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/jquery-1.5.1.min.js"></script>
7+
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/jquery-ui-1.8.9.min.js"></script>
8+
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/jquery.json-2.2.min.js"></script>
9+
<link rel="stylesheet" href="{{ SECR_STATIC_URL }}css/redmond/jquery-ui-1.8.9.custom.css" type="text/css" media="screen" charset="utf-8"/>
10+
11+
<script type="text/javascript">
12+
// Free up the $ and jQuery names.
13+
j=$.noConflict(true);
14+
j(document).ready(function($) {
15+
function add_to_list(list, id, label) {
16+
list.append('<li><a href="' + id
17+
+ '"><img src="{{ SECR_STATIC_URL }}img/delete.png" alt="delete"></a> '
18+
+ label + '</li>');
19+
}
20+
21+
function setup_ajax(field, list, searchfield, url) {
22+
var datastore = {};
23+
window.field = field;
24+
if (field.val() != '') {
25+
datastore = $.parseJSON(field.val());
26+
}
27+
$.each(datastore, function(k, v) {
28+
add_to_list(list, k, v);
29+
});
30+
searchfield.autocomplete({
31+
source: url,
32+
minLength: 1,
33+
select: function(event, ui) {
34+
datastore[ui.item.id] = ui.item.label;
35+
field.val($.toJSON(datastore));
36+
searchfield.val('');
37+
add_to_list(list, ui.item.id, ui.item.label);
38+
return false;
39+
}
40+
});
41+
// Automatically select the first element in the autocomplete list, so
42+
// that hitting Enter adds it.
43+
$(".ui-autocomplete-input").live("autocompleteopen", function() {
44+
var autocomplete = $(this).data("autocomplete");
45+
var menu = autocomplete.menu;
46+
menu.activate($.Event({ type: "mouseenter" }),
47+
menu.element.children().first());
48+
});
49+
list.delegate("a", "click", function() {
50+
delete datastore[$(this).attr("href")];
51+
field.val($.toJSON(datastore));
52+
$(this).closest("li").remove();
53+
return false;
54+
});
55+
}
56+
57+
setup_ajax($("#id_replaces"), $("#replaces_list"),
58+
$("#id_replaces_search"), "{% url doc_ajax_internet_draft %}");
59+
});
60+
</script>
61+
{% endblock %}
62+
63+
{% block content %}
64+
<h1>Change which documents {{ doc }} replaces</h1>
65+
66+
<form class="change-replaces" action="" method="post">
67+
{{ form.non_field_errors }}
68+
{{ form.replaces.label_tag }}
69+
<input type="text" id="id_replaces_search">
70+
{{ form.replaces }}
71+
<ul id="replaces_list"></ul>
72+
{{ form.replaces.errors }}
73+
<table>
74+
<tr>
75+
<td>{{ form.comment.label_tag }}</td>
76+
<td>{{ form.comment.errors }} {{ form.comment }}</td>
77+
</tr>
78+
<tr>
79+
<td colspan="2" class="actions">
80+
<a href="{{ doc.get_absolute_url }}">Back</a>
81+
<input type="submit" value="Save"/>
82+
</td>
83+
</tr>
84+
</table>
85+
</form>
86+
{% endblock %}

0 commit comments

Comments
 (0)