Skip to content

Commit ed15193

Browse files
committed
Introduce more generic ballot models, start porting doc ballot page to the
new database schema. - Legacy-Id: 4240
1 parent 0fea5e0 commit ed15193

10 files changed

Lines changed: 793 additions & 12 deletions

File tree

ietf/doc/models.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,8 @@ class DocReminder(models.Model):
299299
# IESG events
300300
("started_iesg_process", "Started IESG process on document"),
301301

302+
("created_ballot", "Created ballot"),
303+
("closed_ballot", "Closed ballot"),
302304
("sent_ballot_announcement", "Sent ballot announcement"),
303305
("changed_ballot_position", "Changed ballot position"),
304306

@@ -335,7 +337,25 @@ class NewRevisionDocEvent(DocEvent):
335337
rev = models.CharField(max_length=16)
336338

337339
# IESG events
340+
class BallotType(models.Model):
341+
doc_type = models.ForeignKey(DocTypeName, blank=True, null=True)
342+
slug = models.SlugField()
343+
name = models.CharField(max_length=255)
344+
question = models.TextField(blank=True)
345+
used = models.BooleanField(default=True)
346+
order = models.IntegerField(default=0)
347+
348+
def __unicode__(self):
349+
return self.name
350+
351+
class Meta:
352+
ordering = ['order']
353+
354+
class BallotDocEvent(DocEvent):
355+
ballot_type = models.ForeignKey(BallotType)
356+
338357
class BallotPositionDocEvent(DocEvent):
358+
# ballot = models.ForeignKey(BallotDocEvent, null=True)
339359
ad = models.ForeignKey(Person)
340360
pos = models.ForeignKey(BallotPositionName, verbose_name="position", default="norecord")
341361
discuss = models.TextField(help_text="Discuss text if position is discuss", blank=True)

ietf/doc/utils.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,19 @@ def get_tags_for_stream_id(stream_id):
3030
else:
3131
return []
3232

33-
def active_ballot_positions(doc):
33+
def active_ballot_positions(doc, ballot=None):
3434
"""Return dict mapping each active AD to a current ballot position (or None if they haven't voted)."""
3535
active_ads = list(Person.objects.filter(role__name="ad", role__group__state="active"))
3636
res = {}
3737

38-
positions = BallotPositionDocEvent.objects.filter(doc=doc, type="changed_ballot_position", ad__in=active_ads).select_related('ad').order_by("-time", "-id")
38+
# FIXME: do something with ballot
39+
40+
start = datetime.datetime.min
41+
e = doc.latest_event(type="started_iesg_process")
42+
if e:
43+
start = e.time
44+
45+
positions = BallotPositionDocEvent.objects.filter(doc=doc, type="changed_ballot_position", ad__in=active_ads, time__gte=start).select_related('ad').order_by("-time", "-id")
3946

4047
for pos in positions:
4148
if pos.ad not in res:
@@ -46,7 +53,46 @@ def active_ballot_positions(doc):
4653
res[ad] = None
4754

4855
return res
56+
57+
def needed_ballot_positions(doc, active_positions):
58+
'''Returns text answering the question "what does this document
59+
need to pass?". The return value is only useful if the document
60+
is currently in IESG evaluation.'''
61+
yes = [p for p in active_positions if p.pos_id == "yes"]
62+
noobj = [p for p in active_positions if p.pos_id == "noobj"]
63+
blocking = [p for p in active_positions if p.pos.blocking]
64+
recuse = [p for p in active_positions if p.pos_id == "recuse"]
65+
66+
answer = []
67+
if yes < 1:
68+
answer.append("Needs a YES.")
69+
if blocking:
70+
if blocking:
71+
answer.append("Has a %s." % blocking[0].name.upper())
72+
else:
73+
answer.append("Has %d %s." % (len(blocking), blocking[0].name.upper()))
74+
needed = 1
75+
if doc.type_id == "draft" and doc.intended_std_level_id in ("bcp", "ps", "ds", "std"):
76+
# For standards-track, need positions from 2/3 of the
77+
# non-recused current IESG.
78+
needed = len(active_positions) - recuse * 2 / 3
79+
80+
have = len(yes) + len(noobj) + len(blocking)
81+
if have < needed:
82+
more = needed - have
83+
if more == 1:
84+
answer.append("Needs %d more position." % more)
85+
else:
86+
answer.append("Needs %d more positions." % more)
87+
else:
88+
if blocking:
89+
answer.append("Has enough positions to pass.")
90+
else:
91+
answer.append("Has enough positions to pass once %s positions are resolved." % blocking[0].name.upper())
92+
93+
return " ".join(answer)
4994

95+
5096
def get_rfc_number(doc):
5197
qs = doc.docalias_set.filter(name__startswith='rfc')
5298
return qs[0].name[3:] if qs else None

ietf/idrfc/views_doc.py

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@
3030
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
3131
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3232

33-
import re, os
34-
from datetime import datetime, time
33+
import re, os, datetime
3534

3635
from django.http import HttpResponse, Http404
3736
from django.shortcuts import render_to_response, get_object_or_404, redirect
@@ -50,8 +49,8 @@
5049
from ietf.idrfc.models import RfcIndex, DraftVersions
5150
from ietf.idrfc.idrfc_wrapper import BallotWrapper, IdWrapper, RfcWrapper
5251
from ietf.ietfworkflows.utils import get_full_info_for_draft
53-
from ietf.doc.models import Document, DocEvent, NewRevisionDocEvent, WriteupDocEvent, TelechatDocEvent
54-
from ietf.doc.utils import get_chartering_type
52+
from ietf.doc.models import *
53+
from ietf.doc.utils import get_chartering_type, needed_ballot_positions
5554
from ietf.utils.history import find_history_active_at
5655
from ietf.ietfauth.decorators import has_role
5756

@@ -240,7 +239,74 @@ def document_writeup(request, name):
240239
),
241240
context_instance=RequestContext(request))
242241

242+
def document_ballot_content(request, name, ballot, editable=True):
243+
doc = get_object_or_404(Document, docalias__name=name)
244+
245+
if ballot != None:
246+
b = doc.latest_event(BallotDocEvent, type="created_ballot", id=ballot)
247+
else:
248+
b = doc.latest_event(BallotDocEvent, type="created_ballot")
249+
250+
if not b:
251+
raise Http404()
252+
253+
deferred = None
254+
if doc.get_state_slug("%s-iesg" % doc.type) == "defer":
255+
# FIXME: fragile
256+
deferred = doc.latest_event(type="changed_document", desc__startswith="State changed to <b>IESG Evaluation - Defer</b>")
257+
258+
# collect positions
259+
active_ads = list(Person.objects.filter(role__name="ad", role__group__state="active").distinct())
260+
261+
positions = []
262+
seen = {}
263+
# FIXME: restrict on ballot
264+
for e in BallotPositionDocEvent.objects.filter(doc=doc, type="changed_ballot_position").select_related('ad', 'pos').order_by("-time", '-id'):
265+
if e.ad not in seen:
266+
e.old_ad = e.ad in active_ads
267+
e.old_positions = []
268+
positions.append(e)
269+
seen[e.ad] = pos
270+
else:
271+
latest = seen[e.ad]
272+
if latest.old_positions:
273+
prev = latest.old_positions[-1]
274+
else:
275+
prev = latest
276+
277+
if e.pos != prev.pos:
278+
latest.old_positions.append(pos)
279+
280+
# add any missing ADs through fake No Record events
281+
for ad in active_ads:
282+
if ad not in seen:
283+
e = BallotPositionDocEvent(type="changed_ballot_position", doc=doc, ad=ad)
284+
e.pos_id = "norecord"
285+
e.old_ad = False
286+
e.old_positions = []
287+
positions.append(e)
288+
289+
# put into position groups
290+
position_groups = []
291+
for n in BallotPositionName.objects.filter(slug__in=[p.pos_id for p in positions]).order_by('order'):
292+
g = (n, [p for p in positions if p.pos_id == n.slug])
293+
if n.blocking:
294+
position_groups.insert(0, g)
295+
else:
296+
position_groups.append(g)
243297

298+
summary = needed_ballot_positions(doc, [p for p in positions if not p.old_ad])
299+
300+
return render_to_string("idrfc/document_ballot_content.html",
301+
dict(doc=doc,
302+
ballot=b,
303+
position_groups=position_groups,
304+
positions=positions,
305+
editable=editable,
306+
deferred=deferred,
307+
summary=summary,
308+
),
309+
context_instance=RequestContext(request))
244310

245311
def document_ballot(request, name, ballot=None):
246312
if name.lower().startswith("draft") or name.lower().startswith("rfc"):
@@ -249,9 +315,14 @@ def document_ballot(request, name, ballot=None):
249315
doc = get_object_or_404(Document, docalias__name=name)
250316
top = render_document_top(request, doc, "ballot")
251317

252-
# FIXME: port implementation from wgcharter
318+
c = document_ballot_content(request, name, ballot, editable=True)
253319

254-
raise Http404()
320+
return render_to_response("idrfc/document_ballot.html",
321+
dict(doc=doc,
322+
top=top,
323+
ballot_content=c,
324+
),
325+
context_instance=RequestContext(request))
255326

256327
def document_debug(request, name):
257328
r = re.compile("^rfc([1-9][0-9]*)$")
@@ -424,11 +495,11 @@ def _get_history(doc, versions):
424495

425496
# convert plain dates to datetimes (required for sorting)
426497
for x in results:
427-
if not isinstance(x['date'], datetime):
498+
if not isinstance(x['date'], datetime.datetime):
428499
if x['date']:
429-
x['date'] = datetime.combine(x['date'], time(0,0,0))
500+
x['date'] = datetime.datetime.combine(x['date'], datetime.time(0,0,0))
430501
else:
431-
x['date'] = datetime(1970,1,1)
502+
x['date'] = datetime.datetime(1970,1,1)
432503

433504
results.sort(key=lambda x: x['date'])
434505
results.reverse()

0 commit comments

Comments
 (0)