Skip to content

Commit

Permalink
Merged in ^/personal/henrik/6.64.2-ballotapi@14426. This provides per…
Browse files Browse the repository at this point in the history
…sonal API keys and a ballot position API at /api/iesg/position. Also added an endpoint description at /api/.

 - Legacy-Id: 14430
  • Loading branch information
levkowetz committed Dec 17, 2017
2 parents f697d9e + a08c8dc commit 6567e70
Show file tree
Hide file tree
Showing 27 changed files with 948 additions and 102 deletions.
3 changes: 1 addition & 2 deletions bin/daily
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
# Nightly datatracker jobs.
#
# This script is expected to be triggered by cron from
# $DTDIR/etc/cron.d/datatracker which should be symlinked from
# /etc/cron.d/
# /etc/cron.d/datatracker

# Run the hourly jobs first
$DTDIR/bin/hourly
Expand Down
3 changes: 1 addition & 2 deletions bin/hourly
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
# Hourly datatracker jobs
#
# This script is expected to be triggered by cron from
# $DTDIR/etc/cron.d/datatracker which should be symlinked from
# /etc/cron.d/
# /etc/cron.d/datatracker

DTDIR=/a/www/ietf-datatracker/web
cd $DTDIR/
Expand Down
15 changes: 15 additions & 0 deletions bin/monthly
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/bash

# Weekly datatracker jobs.
#
# This script is expected to be triggered by cron from
# /etc/cron.d/datatracker

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

# Set up the virtual environment
source $DTDIR/env/bin/activate

logger -p user.info -t cron "Running $DTDIR/bin/monthly"

20 changes: 20 additions & 0 deletions bin/weekly
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash

# Weekly datatracker jobs.
#
# This script is expected to be triggered by cron from
# /etc/cron.d/datatracker

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

# Set up the virtual environment
source $DTDIR/env/bin/activate

logger -p user.info -t cron "Running $DTDIR/bin/weekly"


# Send out weekly summaries of apikey usage

$DTDIR/ietf/manage.py send_apikey_usage_emails

2 changes: 2 additions & 0 deletions ietf/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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

# Additional (standard) Tastypie endpoints
Expand Down
63 changes: 62 additions & 1 deletion ietf/doc/tests_ballot.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from ietf.group.models import Group, Role
from ietf.name.models import BallotPositionName
from ietf.iesg.models import TelechatDate
from ietf.person.models import Person
from ietf.person.models import Person, PersonalApiKey
from ietf.utils.test_utils import TestCase, unicontent
from ietf.utils.mail import outbox, empty_outbox
from ietf.utils.test_data import make_test_data
Expand Down Expand Up @@ -85,6 +85,67 @@ def test_edit_position(self):
self.assertEqual(draft.docevent_set.count(), events_before + 2)
self.assertTrue("Ballot comment text updated" in pos.desc)

def test_api_set_position(self):
draft = make_test_data()
url = urlreverse('ietf.doc.views_ballot.api_set_position')
ad = Person.objects.get(name="Areað Irector")
create_ballot_if_not_open(None, draft, ad, 'approve')
ad.user.last_login = datetime.datetime.now()
ad.user.save()
apikey = PersonalApiKey.objects.create(endpoint=url, person=ad)

# vote
events_before = draft.docevent_set.count()

r = self.client.post(url, dict(
apikey=apikey.hash(),
doc=draft.name,
position="discuss",
discuss=" This is a discussion test. \n ",
comment=" This is a test. \n ")
)
self.assertEqual(r.content, "Done")
self.assertEqual(r.status_code, 200)

pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
self.assertEqual(pos.pos.slug, "discuss")
self.assertTrue(" This is a discussion test." in pos.discuss)
self.assertTrue(pos.discuss_time != None)
self.assertTrue(" This is a test." in pos.comment)
self.assertTrue(pos.comment_time != None)
self.assertTrue("New position" in pos.desc)
self.assertEqual(draft.docevent_set.count(), events_before + 3)

# recast vote
events_before = draft.docevent_set.count()
r = self.client.post(url, dict(apikey=apikey.hash(), doc=draft.name, position="noobj"))
self.assertEqual(r.status_code, 200)

pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
self.assertEqual(pos.pos.slug, "noobj")
self.assertEqual(draft.docevent_set.count(), events_before + 1)
self.assertTrue("Position for" in pos.desc)

# clear vote
events_before = draft.docevent_set.count()
r = self.client.post(url, dict(apikey=apikey.hash(), doc=draft.name, position="norecord"))
self.assertEqual(r.status_code, 200)

pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
self.assertEqual(pos.pos.slug, "norecord")
self.assertEqual(draft.docevent_set.count(), events_before + 1)
self.assertTrue("Position for" in pos.desc)

# change comment
events_before = draft.docevent_set.count()
r = self.client.post(url, dict(apikey=apikey.hash(), doc=draft.name, position="norecord", comment="New comment."))
self.assertEqual(r.status_code, 200)

pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
self.assertEqual(pos.pos.slug, "norecord")
self.assertEqual(draft.docevent_set.count(), events_before + 2)
self.assertTrue("Ballot comment text updated" in pos.desc)

def test_edit_position_as_secretary(self):
draft = make_test_data()
ad = Person.objects.get(user__username="ad")
Expand Down
185 changes: 117 additions & 68 deletions ietf/doc/views_ballot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@

import datetime, json

from django.http import HttpResponseForbidden, HttpResponseRedirect, Http404
from django.shortcuts import render, get_object_or_404, redirect
from django.urls import reverse as urlreverse
from django.template.loader import render_to_string
from django import forms
from django.conf import settings
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404
from django.shortcuts import render, get_object_or_404, redirect
from django.template.defaultfilters import striptags
from django.template.loader import render_to_string
from django.urls import reverse as urlreverse
from django.views.decorators.csrf import csrf_exempt


import debug # pyflakes:ignore

Expand All @@ -30,6 +33,7 @@
from ietf.person.models import Person
from ietf.utils import log
from ietf.utils.mail import send_mail_text, send_mail_preformatted
from ietf.utils.decorators import require_api_key

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

def save_position(form, doc, ballot, ad, login=None):
# save the vote
if login is None:
login = ad
clean = form.cleaned_data

old_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, ballot=ballot)
pos = BallotPositionDocEvent(doc=doc, rev=doc.rev, by=login)
pos.type = "changed_ballot_position"
pos.ballot = ballot
pos.ad = ad
pos.pos = clean["position"]
pos.comment = clean["comment"].rstrip()
pos.comment_time = old_pos.comment_time if old_pos else None
pos.discuss = clean["discuss"].rstrip()
if not pos.pos.blocking:
pos.discuss = ""
pos.discuss_time = old_pos.discuss_time if old_pos else None

changes = []
added_events = []
# possibly add discuss/comment comments to history trail
# so it's easy to see what's happened
old_comment = old_pos.comment if old_pos else ""
if pos.comment != old_comment:
pos.comment_time = pos.time
changes.append("comment")

if pos.comment:
e = DocEvent(doc=doc, rev=doc.rev)
e.by = ad # otherwise we can't see who's saying it
e.type = "added_comment"
e.desc = "[Ballot comment]\n" + pos.comment
added_events.append(e)

old_discuss = old_pos.discuss if old_pos else ""
if pos.discuss != old_discuss:
pos.discuss_time = pos.time
changes.append("discuss")

if pos.pos.blocking:
e = DocEvent(doc=doc, rev=doc.rev, by=login)
e.by = ad # otherwise we can't see who's saying it
e.type = "added_comment"
e.desc = "[Ballot %s]\n" % pos.pos.name.lower()
e.desc += pos.discuss
added_events.append(e)

# figure out a description
if not old_pos and pos.pos.slug != "norecord":
pos.desc = u"[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.plain_name())
elif old_pos and pos.pos != old_pos.pos:
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)

if not pos.desc and changes:
pos.desc = u"Ballot %s text updated for %s" % (u" and ".join(changes), ad.plain_name())

# only add new event if we actually got a change
if pos.desc:
if login != ad:
pos.desc += u" by %s" % login.plain_name()

pos.save()

for e in added_events:
e.save() # save them after the position is saved to get later id for sorting order


@role_required('Area Director','Secretariat')
def edit_position(request, name, ballot_id):
"""Vote and edit discuss and comment on document as Area Director."""
Expand All @@ -126,77 +198,14 @@ def edit_position(request, name, ballot_id):
raise Http404
ad = get_object_or_404(Person, pk=ad_id)

old_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, ballot=ballot)

if request.method == 'POST':
if not has_role(request.user, "Secretariat") and not ad.role_set.filter(name="ad", group__type="area", group__state="active"):
# prevent pre-ADs from voting
return HttpResponseForbidden("Must be a proper Area Director in an active area to cast ballot")

form = EditPositionForm(request.POST, ballot_type=ballot.ballot_type)
if form.is_valid():
# save the vote
clean = form.cleaned_data

pos = BallotPositionDocEvent(doc=doc, rev=doc.rev, by=login)
pos.type = "changed_ballot_position"
pos.ballot = ballot
pos.ad = ad
pos.pos = clean["position"]
pos.comment = clean["comment"].rstrip()
pos.comment_time = old_pos.comment_time if old_pos else None
pos.discuss = clean["discuss"].rstrip()
if not pos.pos.blocking:
pos.discuss = ""
pos.discuss_time = old_pos.discuss_time if old_pos else None

changes = []
added_events = []
# possibly add discuss/comment comments to history trail
# so it's easy to see what's happened
old_comment = old_pos.comment if old_pos else ""
if pos.comment != old_comment:
pos.comment_time = pos.time
changes.append("comment")

if pos.comment:
e = DocEvent(doc=doc, rev=doc.rev)
e.by = ad # otherwise we can't see who's saying it
e.type = "added_comment"
e.desc = "[Ballot comment]\n" + pos.comment
added_events.append(e)

old_discuss = old_pos.discuss if old_pos else ""
if pos.discuss != old_discuss:
pos.discuss_time = pos.time
changes.append("discuss")

if pos.pos.blocking:
e = DocEvent(doc=doc, rev=doc.rev, by=login)
e.by = ad # otherwise we can't see who's saying it
e.type = "added_comment"
e.desc = "[Ballot %s]\n" % pos.pos.name.lower()
e.desc += pos.discuss
added_events.append(e)

# figure out a description
if not old_pos and pos.pos.slug != "norecord":
pos.desc = u"[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.plain_name())
elif old_pos and pos.pos != old_pos.pos:
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)

if not pos.desc and changes:
pos.desc = u"Ballot %s text updated for %s" % (u" and ".join(changes), ad.plain_name())

# only add new event if we actually got a change
if pos.desc:
if login != ad:
pos.desc += u" by %s" % login.plain_name()

pos.save()

for e in added_events:
e.save() # save them after the position is saved to get later id for sorting order
save_position(form, doc, ballot, ad, login)

if request.POST.get("send_mail"):
qstr=""
Expand All @@ -211,6 +220,7 @@ def edit_position(request, name, ballot_id):
return HttpResponseRedirect(return_to_url)
else:
initial = {}
old_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, ballot=ballot)
if old_pos:
initial['position'] = old_pos.pos.slug
initial['discuss'] = old_pos.discuss
Expand All @@ -234,6 +244,45 @@ def edit_position(request, name, ballot_id):
blocking_positions=json.dumps(blocking_positions),
))

@require_api_key
@role_required('Area Director', 'Secretariat')
@csrf_exempt
def api_set_position(request):
def err(code, text):
return HttpResponse(text, status=code, content_type='text/plain')
if request.method == 'POST':
ad = request.user.person
name = request.POST.get('doc')
if not name:
return err(400, "Missing document name")
try:
doc = Document.objects.get(docalias__name=name)
except Document.DoesNotExist:
return err(404, "Document not found")
position_names = BallotPositionName.objects.values_list('slug', flat=True)
position = request.POST.get('position')
if not position:
return err(400, "Missing parameter: position, one of: %s " % ','.join(position_names))
if not position in position_names:
return err(400, "Bad position name, must be one of: %s " % ','.join(position_names))
ballot = doc.active_ballot()
if not ballot:
return err(404, "No open ballot found")
form = EditPositionForm(request.POST, ballot_type=ballot.ballot_type)
if form.is_valid():
save_position(form, doc, ballot, ad)
else:
debug.type('form.errors')
debug.show('form.errors')
errors = form.errors
summary = ','.join([ "%s: %s" % (f, striptags(errors[f])) for f in errors ])
return err(400, "Form not valid: %s" % summary)
else:
return err(405, "Method not allowed")

return HttpResponse("Done", status=200, content_type='text/plain')


@role_required('Area Director','Secretariat')
def send_ballot_comment(request, name, ballot_id):
"""Email document ballot position discuss/comment for Area Director."""
Expand Down
Empty file.
Empty file.
Loading

0 comments on commit 6567e70

Please sign in to comment.