Skip to content

Commit 6567e70

Browse files
committed
Merged in ^/personal/henrik/6.64.2-ballotapi@14426. This provides personal API keys and a ballot position API at /api/iesg/position. Also added an endpoint description at /api/.
- Legacy-Id: 14430
2 parents f697d9e + a08c8dc commit 6567e70

27 files changed

Lines changed: 948 additions & 102 deletions

bin/daily

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
# Nightly datatracker jobs.
44
#
55
# This script is expected to be triggered by cron from
6-
# $DTDIR/etc/cron.d/datatracker which should be symlinked from
7-
# /etc/cron.d/
6+
# /etc/cron.d/datatracker
87

98
# Run the hourly jobs first
109
$DTDIR/bin/hourly

bin/hourly

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
# Hourly datatracker jobs
44
#
55
# This script is expected to be triggered by cron from
6-
# $DTDIR/etc/cron.d/datatracker which should be symlinked from
7-
# /etc/cron.d/
6+
# /etc/cron.d/datatracker
87

98
DTDIR=/a/www/ietf-datatracker/web
109
cd $DTDIR/

bin/monthly

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/bash
2+
3+
# Weekly datatracker jobs.
4+
#
5+
# This script is expected to be triggered by cron from
6+
# /etc/cron.d/datatracker
7+
8+
DTDIR=/a/www/ietf-datatracker/web
9+
cd $DTDIR/
10+
11+
# Set up the virtual environment
12+
source $DTDIR/env/bin/activate
13+
14+
logger -p user.info -t cron "Running $DTDIR/bin/monthly"
15+

bin/weekly

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
3+
# Weekly datatracker jobs.
4+
#
5+
# This script is expected to be triggered by cron from
6+
# /etc/cron.d/datatracker
7+
8+
DTDIR=/a/www/ietf-datatracker/web
9+
cd $DTDIR/
10+
11+
# Set up the virtual environment
12+
source $DTDIR/env/bin/activate
13+
14+
logger -p user.info -t cron "Running $DTDIR/bin/weekly"
15+
16+
17+
# Send out weekly summaries of apikey usage
18+
19+
$DTDIR/ietf/manage.py send_apikey_usage_emails
20+

ietf/api/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from ietf import api
66
from ietf.api import views as api_views
7+
from ietf.doc import views_ballot
78
from ietf.meeting import views as meeting_views
89
from ietf.submit import views as submit_views
910
from ietf.utils.urls import url
@@ -17,6 +18,7 @@
1718
# Custom API endpoints
1819
url(r'^notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings),
1920
url(r'^submit/?$', submit_views.api_submit),
21+
url(r'^iesg/position', views_ballot.api_set_position),
2022
]
2123

2224
# Additional (standard) Tastypie endpoints

ietf/doc/tests_ballot.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from ietf.group.models import Group, Role
1414
from ietf.name.models import BallotPositionName
1515
from ietf.iesg.models import TelechatDate
16-
from ietf.person.models import Person
16+
from ietf.person.models import Person, PersonalApiKey
1717
from ietf.utils.test_utils import TestCase, unicontent
1818
from ietf.utils.mail import outbox, empty_outbox
1919
from ietf.utils.test_data import make_test_data
@@ -85,6 +85,67 @@ def test_edit_position(self):
8585
self.assertEqual(draft.docevent_set.count(), events_before + 2)
8686
self.assertTrue("Ballot comment text updated" in pos.desc)
8787

88+
def test_api_set_position(self):
89+
draft = make_test_data()
90+
url = urlreverse('ietf.doc.views_ballot.api_set_position')
91+
ad = Person.objects.get(name="Areað Irector")
92+
create_ballot_if_not_open(None, draft, ad, 'approve')
93+
ad.user.last_login = datetime.datetime.now()
94+
ad.user.save()
95+
apikey = PersonalApiKey.objects.create(endpoint=url, person=ad)
96+
97+
# vote
98+
events_before = draft.docevent_set.count()
99+
100+
r = self.client.post(url, dict(
101+
apikey=apikey.hash(),
102+
doc=draft.name,
103+
position="discuss",
104+
discuss=" This is a discussion test. \n ",
105+
comment=" This is a test. \n ")
106+
)
107+
self.assertEqual(r.content, "Done")
108+
self.assertEqual(r.status_code, 200)
109+
110+
pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
111+
self.assertEqual(pos.pos.slug, "discuss")
112+
self.assertTrue(" This is a discussion test." in pos.discuss)
113+
self.assertTrue(pos.discuss_time != None)
114+
self.assertTrue(" This is a test." in pos.comment)
115+
self.assertTrue(pos.comment_time != None)
116+
self.assertTrue("New position" in pos.desc)
117+
self.assertEqual(draft.docevent_set.count(), events_before + 3)
118+
119+
# recast vote
120+
events_before = draft.docevent_set.count()
121+
r = self.client.post(url, dict(apikey=apikey.hash(), doc=draft.name, position="noobj"))
122+
self.assertEqual(r.status_code, 200)
123+
124+
pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
125+
self.assertEqual(pos.pos.slug, "noobj")
126+
self.assertEqual(draft.docevent_set.count(), events_before + 1)
127+
self.assertTrue("Position for" in pos.desc)
128+
129+
# clear vote
130+
events_before = draft.docevent_set.count()
131+
r = self.client.post(url, dict(apikey=apikey.hash(), doc=draft.name, position="norecord"))
132+
self.assertEqual(r.status_code, 200)
133+
134+
pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
135+
self.assertEqual(pos.pos.slug, "norecord")
136+
self.assertEqual(draft.docevent_set.count(), events_before + 1)
137+
self.assertTrue("Position for" in pos.desc)
138+
139+
# change comment
140+
events_before = draft.docevent_set.count()
141+
r = self.client.post(url, dict(apikey=apikey.hash(), doc=draft.name, position="norecord", comment="New comment."))
142+
self.assertEqual(r.status_code, 200)
143+
144+
pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
145+
self.assertEqual(pos.pos.slug, "norecord")
146+
self.assertEqual(draft.docevent_set.count(), events_before + 2)
147+
self.assertTrue("Ballot comment text updated" in pos.desc)
148+
88149
def test_edit_position_as_secretary(self):
89150
draft = make_test_data()
90151
ad = Person.objects.get(user__username="ad")

ietf/doc/views_ballot.py

Lines changed: 117 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33

44
import datetime, json
55

6-
from django.http import HttpResponseForbidden, HttpResponseRedirect, Http404
7-
from django.shortcuts import render, get_object_or_404, redirect
8-
from django.urls import reverse as urlreverse
9-
from django.template.loader import render_to_string
106
from django import forms
117
from django.conf import settings
8+
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404
9+
from django.shortcuts import render, get_object_or_404, redirect
10+
from django.template.defaultfilters import striptags
11+
from django.template.loader import render_to_string
12+
from django.urls import reverse as urlreverse
13+
from django.views.decorators.csrf import csrf_exempt
14+
1215

1316
import debug # pyflakes:ignore
1417

@@ -30,6 +33,7 @@
3033
from ietf.person.models import Person
3134
from ietf.utils import log
3235
from ietf.utils.mail import send_mail_text, send_mail_preformatted
36+
from ietf.utils.decorators import require_api_key
3337

3438
BALLOT_CHOICES = (("yes", "Yes"),
3539
("noobj", "No Objection"),
@@ -106,6 +110,74 @@ def clean_discuss(self):
106110
raise forms.ValidationError("You must enter a non-empty discuss")
107111
return entered_discuss
108112

113+
def save_position(form, doc, ballot, ad, login=None):
114+
# save the vote
115+
if login is None:
116+
login = ad
117+
clean = form.cleaned_data
118+
119+
old_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, ballot=ballot)
120+
pos = BallotPositionDocEvent(doc=doc, rev=doc.rev, by=login)
121+
pos.type = "changed_ballot_position"
122+
pos.ballot = ballot
123+
pos.ad = ad
124+
pos.pos = clean["position"]
125+
pos.comment = clean["comment"].rstrip()
126+
pos.comment_time = old_pos.comment_time if old_pos else None
127+
pos.discuss = clean["discuss"].rstrip()
128+
if not pos.pos.blocking:
129+
pos.discuss = ""
130+
pos.discuss_time = old_pos.discuss_time if old_pos else None
131+
132+
changes = []
133+
added_events = []
134+
# possibly add discuss/comment comments to history trail
135+
# so it's easy to see what's happened
136+
old_comment = old_pos.comment if old_pos else ""
137+
if pos.comment != old_comment:
138+
pos.comment_time = pos.time
139+
changes.append("comment")
140+
141+
if pos.comment:
142+
e = DocEvent(doc=doc, rev=doc.rev)
143+
e.by = ad # otherwise we can't see who's saying it
144+
e.type = "added_comment"
145+
e.desc = "[Ballot comment]\n" + pos.comment
146+
added_events.append(e)
147+
148+
old_discuss = old_pos.discuss if old_pos else ""
149+
if pos.discuss != old_discuss:
150+
pos.discuss_time = pos.time
151+
changes.append("discuss")
152+
153+
if pos.pos.blocking:
154+
e = DocEvent(doc=doc, rev=doc.rev, by=login)
155+
e.by = ad # otherwise we can't see who's saying it
156+
e.type = "added_comment"
157+
e.desc = "[Ballot %s]\n" % pos.pos.name.lower()
158+
e.desc += pos.discuss
159+
added_events.append(e)
160+
161+
# figure out a description
162+
if not old_pos and pos.pos.slug != "norecord":
163+
pos.desc = u"[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.plain_name())
164+
elif old_pos and pos.pos != old_pos.pos:
165+
pos.desc = "[Ballot Position Update] Position for %s has been changed to %s from %s" % (pos.ad.plain_name(), pos.pos.name, old_pos.pos.name)
166+
167+
if not pos.desc and changes:
168+
pos.desc = u"Ballot %s text updated for %s" % (u" and ".join(changes), ad.plain_name())
169+
170+
# only add new event if we actually got a change
171+
if pos.desc:
172+
if login != ad:
173+
pos.desc += u" by %s" % login.plain_name()
174+
175+
pos.save()
176+
177+
for e in added_events:
178+
e.save() # save them after the position is saved to get later id for sorting order
179+
180+
109181
@role_required('Area Director','Secretariat')
110182
def edit_position(request, name, ballot_id):
111183
"""Vote and edit discuss and comment on document as Area Director."""
@@ -126,77 +198,14 @@ def edit_position(request, name, ballot_id):
126198
raise Http404
127199
ad = get_object_or_404(Person, pk=ad_id)
128200

129-
old_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, ballot=ballot)
130-
131201
if request.method == 'POST':
132202
if not has_role(request.user, "Secretariat") and not ad.role_set.filter(name="ad", group__type="area", group__state="active"):
133203
# prevent pre-ADs from voting
134204
return HttpResponseForbidden("Must be a proper Area Director in an active area to cast ballot")
135205

136206
form = EditPositionForm(request.POST, ballot_type=ballot.ballot_type)
137207
if form.is_valid():
138-
# save the vote
139-
clean = form.cleaned_data
140-
141-
pos = BallotPositionDocEvent(doc=doc, rev=doc.rev, by=login)
142-
pos.type = "changed_ballot_position"
143-
pos.ballot = ballot
144-
pos.ad = ad
145-
pos.pos = clean["position"]
146-
pos.comment = clean["comment"].rstrip()
147-
pos.comment_time = old_pos.comment_time if old_pos else None
148-
pos.discuss = clean["discuss"].rstrip()
149-
if not pos.pos.blocking:
150-
pos.discuss = ""
151-
pos.discuss_time = old_pos.discuss_time if old_pos else None
152-
153-
changes = []
154-
added_events = []
155-
# possibly add discuss/comment comments to history trail
156-
# so it's easy to see what's happened
157-
old_comment = old_pos.comment if old_pos else ""
158-
if pos.comment != old_comment:
159-
pos.comment_time = pos.time
160-
changes.append("comment")
161-
162-
if pos.comment:
163-
e = DocEvent(doc=doc, rev=doc.rev)
164-
e.by = ad # otherwise we can't see who's saying it
165-
e.type = "added_comment"
166-
e.desc = "[Ballot comment]\n" + pos.comment
167-
added_events.append(e)
168-
169-
old_discuss = old_pos.discuss if old_pos else ""
170-
if pos.discuss != old_discuss:
171-
pos.discuss_time = pos.time
172-
changes.append("discuss")
173-
174-
if pos.pos.blocking:
175-
e = DocEvent(doc=doc, rev=doc.rev, by=login)
176-
e.by = ad # otherwise we can't see who's saying it
177-
e.type = "added_comment"
178-
e.desc = "[Ballot %s]\n" % pos.pos.name.lower()
179-
e.desc += pos.discuss
180-
added_events.append(e)
181-
182-
# figure out a description
183-
if not old_pos and pos.pos.slug != "norecord":
184-
pos.desc = u"[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.plain_name())
185-
elif old_pos and pos.pos != old_pos.pos:
186-
pos.desc = "[Ballot Position Update] Position for %s has been changed to %s from %s" % (pos.ad.plain_name(), pos.pos.name, old_pos.pos.name)
187-
188-
if not pos.desc and changes:
189-
pos.desc = u"Ballot %s text updated for %s" % (u" and ".join(changes), ad.plain_name())
190-
191-
# only add new event if we actually got a change
192-
if pos.desc:
193-
if login != ad:
194-
pos.desc += u" by %s" % login.plain_name()
195-
196-
pos.save()
197-
198-
for e in added_events:
199-
e.save() # save them after the position is saved to get later id for sorting order
208+
save_position(form, doc, ballot, ad, login)
200209

201210
if request.POST.get("send_mail"):
202211
qstr=""
@@ -211,6 +220,7 @@ def edit_position(request, name, ballot_id):
211220
return HttpResponseRedirect(return_to_url)
212221
else:
213222
initial = {}
223+
old_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, ballot=ballot)
214224
if old_pos:
215225
initial['position'] = old_pos.pos.slug
216226
initial['discuss'] = old_pos.discuss
@@ -234,6 +244,45 @@ def edit_position(request, name, ballot_id):
234244
blocking_positions=json.dumps(blocking_positions),
235245
))
236246

247+
@require_api_key
248+
@role_required('Area Director', 'Secretariat')
249+
@csrf_exempt
250+
def api_set_position(request):
251+
def err(code, text):
252+
return HttpResponse(text, status=code, content_type='text/plain')
253+
if request.method == 'POST':
254+
ad = request.user.person
255+
name = request.POST.get('doc')
256+
if not name:
257+
return err(400, "Missing document name")
258+
try:
259+
doc = Document.objects.get(docalias__name=name)
260+
except Document.DoesNotExist:
261+
return err(404, "Document not found")
262+
position_names = BallotPositionName.objects.values_list('slug', flat=True)
263+
position = request.POST.get('position')
264+
if not position:
265+
return err(400, "Missing parameter: position, one of: %s " % ','.join(position_names))
266+
if not position in position_names:
267+
return err(400, "Bad position name, must be one of: %s " % ','.join(position_names))
268+
ballot = doc.active_ballot()
269+
if not ballot:
270+
return err(404, "No open ballot found")
271+
form = EditPositionForm(request.POST, ballot_type=ballot.ballot_type)
272+
if form.is_valid():
273+
save_position(form, doc, ballot, ad)
274+
else:
275+
debug.type('form.errors')
276+
debug.show('form.errors')
277+
errors = form.errors
278+
summary = ','.join([ "%s: %s" % (f, striptags(errors[f])) for f in errors ])
279+
return err(400, "Form not valid: %s" % summary)
280+
else:
281+
return err(405, "Method not allowed")
282+
283+
return HttpResponse("Done", status=200, content_type='text/plain')
284+
285+
237286
@role_required('Area Director','Secretariat')
238287
def send_ballot_comment(request, name, ballot_id):
239288
"""Email document ballot position discuss/comment for Area Director."""

ietf/ietfauth/management/__init__.py

Whitespace-only changes.

ietf/ietfauth/management/commands/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)