Skip to content

Commit 66687a5

Browse files
Update any_email_sent() to use balloters instead of old ad field. Add tests to catch the otherwise quiet failure. Fixes ietf-tools#3438. Commit ready for merge.
- Legacy-Id: 19837
1 parent efa42e1 commit 66687a5

4 files changed

Lines changed: 287 additions & 11 deletions

File tree

ietf/doc/factories.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -411,12 +411,8 @@ class Meta:
411411
model = BallotPositionDocEvent
412412

413413
type = 'changed_ballot_position'
414-
415-
# This isn't right - it needs to build a ballot for the same doc as this position
416-
# For now, deal with this in test code by building BallotDocEvent and BallotPositionDocEvent
417-
# separately and passing the same doc into thier factories.
418-
ballot = factory.SubFactory(BallotDocEventFactory)
419-
414+
ballot = factory.SubFactory(BallotDocEventFactory)
415+
doc = factory.SelfAttribute('ballot.doc') # point to same doc as the ballot
420416
balloter = factory.SubFactory('ietf.person.factories.PersonFactory')
421417
pos_id = 'discuss'
422418

ietf/doc/models.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1353,7 +1353,11 @@ class BallotPositionDocEvent(DocEvent):
13531353
def any_email_sent(self):
13541354
# When the send_email field is introduced, old positions will have it
13551355
# set to None. We still essentially return True, False, or don't know:
1356-
sent_list = BallotPositionDocEvent.objects.filter(ballot=self.ballot, time__lte=self.time, ad=self.ad).values_list('send_email', flat=True)
1356+
sent_list = BallotPositionDocEvent.objects.filter(
1357+
ballot=self.ballot,
1358+
time__lte=self.time,
1359+
balloter=self.balloter,
1360+
).values_list('send_email', flat=True)
13571361
false = any( s==False for s in sent_list )
13581362
true = any( s==True for s in sent_list )
13591363
return True if true else False if false else None

ietf/doc/tests_ballot.py

Lines changed: 276 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,24 @@
99

1010
import debug # pyflakes:ignore
1111

12+
from django.test import RequestFactory
13+
from django.utils.text import slugify
1214
from django.urls import reverse as urlreverse
1315

14-
from ietf.doc.models import ( Document, State, DocEvent,
15-
BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, TelechatDocEvent )
16-
from ietf.doc.factories import DocumentFactory, IndividualDraftFactory, IndividualRfcFactory, WgDraftFactory
16+
from ietf.doc.models import (Document, State, DocEvent,
17+
BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, TelechatDocEvent)
18+
from ietf.doc.factories import (DocumentFactory, IndividualDraftFactory, IndividualRfcFactory, WgDraftFactory,
19+
BallotPositionDocEventFactory, BallotDocEventFactory)
1720
from ietf.doc.utils import create_ballot_if_not_open
21+
from ietf.doc.views_doc import document_ballot_content
1822
from ietf.group.models import Group, Role
1923
from ietf.group.factories import GroupFactory, RoleFactory, ReviewTeamFactory
2024
from ietf.ipr.factories import HolderIprDisclosureFactory
2125
from ietf.name.models import BallotPositionName
2226
from ietf.iesg.models import TelechatDate
2327
from ietf.person.models import Person, PersonalApiKey
2428
from ietf.person.factories import PersonFactory
29+
from ietf.person.utils import get_active_ads
2530
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
2631
from ietf.utils.mail import outbox, empty_outbox, get_payload_text
2732
from ietf.utils.text import unwrap
@@ -1100,11 +1105,278 @@ def test_regenerate_last_call(self):
11001105
self.assertTrue("rfc6666" in lc_text)
11011106
self.assertTrue("Independent Submission" in lc_text)
11021107

1103-
draft.relateddocument_set.create(target=rfc.docalias.get(name='rfc6666'),relationship_id='downref-approval')
1108+
draft.relateddocument_set.create(target=rfc.docalias.get(name='rfc6666'), relationship_id='downref-approval')
11041109

11051110
r = self.client.post(url, dict(regenerate_last_call_text="1"))
11061111
self.assertEqual(r.status_code, 200)
11071112
draft = Document.objects.get(name=draft.name)
11081113
lc_text = draft.latest_event(WriteupDocEvent, type="changed_last_call_text").text
11091114
self.assertFalse("contains these normative down" in lc_text)
11101115
self.assertFalse("rfc6666" in lc_text)
1116+
1117+
1118+
class BallotContentTests(TestCase):
1119+
def test_ballotpositiondocevent_any_email_sent(self):
1120+
now = datetime.datetime.now() # be sure event timestamps are at distinct times
1121+
bpde_with_null_send_email = BallotPositionDocEventFactory(
1122+
time=now - datetime.timedelta(minutes=30),
1123+
send_email=None,
1124+
)
1125+
ballot = bpde_with_null_send_email.ballot
1126+
balloter = bpde_with_null_send_email.balloter
1127+
self.assertIsNone(
1128+
bpde_with_null_send_email.any_email_sent(),
1129+
'Result is None when only send_email is None',
1130+
)
1131+
1132+
self.assertIsNone(
1133+
BallotPositionDocEventFactory(
1134+
ballot=ballot,
1135+
balloter=balloter,
1136+
time=now - datetime.timedelta(minutes=29),
1137+
send_email=None,
1138+
).any_email_sent(),
1139+
'Result is None when all send_email values are None',
1140+
)
1141+
1142+
# test with assertIs instead of assertFalse to distinguish None from False
1143+
self.assertIs(
1144+
BallotPositionDocEventFactory(
1145+
ballot=ballot,
1146+
balloter=balloter,
1147+
time=now - datetime.timedelta(minutes=28),
1148+
send_email=False,
1149+
).any_email_sent(),
1150+
False,
1151+
'Result is False when current send_email is False'
1152+
)
1153+
1154+
self.assertIs(
1155+
BallotPositionDocEventFactory(
1156+
ballot=ballot,
1157+
balloter=balloter,
1158+
time=now - datetime.timedelta(minutes=27),
1159+
send_email=None,
1160+
).any_email_sent(),
1161+
False,
1162+
'Result is False when earlier send_email is False'
1163+
)
1164+
1165+
self.assertIs(
1166+
BallotPositionDocEventFactory(
1167+
ballot=ballot,
1168+
balloter=balloter,
1169+
time=now - datetime.timedelta(minutes=26),
1170+
send_email=True,
1171+
).any_email_sent(),
1172+
True,
1173+
'Result is True when current send_email is True'
1174+
)
1175+
1176+
self.assertIs(
1177+
BallotPositionDocEventFactory(
1178+
ballot=ballot,
1179+
balloter=balloter,
1180+
time=now - datetime.timedelta(minutes=25),
1181+
send_email=None,
1182+
).any_email_sent(),
1183+
True,
1184+
'Result is True when earlier send_email is True and current is None'
1185+
)
1186+
1187+
self.assertIs(
1188+
BallotPositionDocEventFactory(
1189+
ballot=ballot,
1190+
balloter=balloter,
1191+
time=now - datetime.timedelta(minutes=24),
1192+
send_email=False,
1193+
).any_email_sent(),
1194+
True,
1195+
'Result is True when earlier send_email is True and current is False'
1196+
)
1197+
1198+
def _assertBallotMessage(self, q, balloter, expected):
1199+
heading = q(f'h4[id$="_{slugify(balloter.plain_name())}"]')
1200+
self.assertEqual(len(heading), 1)
1201+
# <h4/> is followed by a panel with the message of interest, so use next()
1202+
self.assertEqual(
1203+
len(heading.next().find(
1204+
f'*[title="{expected}"]'
1205+
)),
1206+
1,
1207+
)
1208+
1209+
def test_document_ballot_content_email_sent(self):
1210+
"""Ballot content correctly describes whether email is requested for each position"""
1211+
ballot = BallotDocEventFactory()
1212+
balloters = get_active_ads()
1213+
self.assertGreaterEqual(len(balloters), 6,
1214+
'Oops! Need to create additional active balloters for test')
1215+
1216+
# send_email is True
1217+
BallotPositionDocEventFactory(
1218+
ballot=ballot,
1219+
balloter=balloters[0],
1220+
pos_id='discuss',
1221+
discuss='Discussion text',
1222+
discuss_time=datetime.datetime.now(),
1223+
send_email=True,
1224+
)
1225+
BallotPositionDocEventFactory(
1226+
ballot=ballot,
1227+
balloter=balloters[1],
1228+
pos_id='noobj',
1229+
comment='Commentary',
1230+
comment_time=datetime.datetime.now(),
1231+
send_email=True,
1232+
)
1233+
1234+
# send_email False
1235+
BallotPositionDocEventFactory(
1236+
ballot=ballot,
1237+
balloter=balloters[2],
1238+
pos_id='discuss',
1239+
discuss='Discussion text',
1240+
discuss_time=datetime.datetime.now(),
1241+
send_email=False,
1242+
)
1243+
BallotPositionDocEventFactory(
1244+
ballot=ballot,
1245+
balloter=balloters[3],
1246+
pos_id='noobj',
1247+
comment='Commentary',
1248+
comment_time=datetime.datetime.now(),
1249+
send_email=False,
1250+
)
1251+
1252+
# send_email False but earlier position had send_email True
1253+
BallotPositionDocEventFactory(
1254+
ballot=ballot,
1255+
balloter=balloters[4],
1256+
pos_id='discuss',
1257+
discuss='Discussion text',
1258+
discuss_time=datetime.datetime.now() - datetime.timedelta(days=1),
1259+
send_email=True,
1260+
)
1261+
BallotPositionDocEventFactory(
1262+
ballot=ballot,
1263+
balloter=balloters[4],
1264+
pos_id='discuss',
1265+
discuss='Discussion text',
1266+
discuss_time=datetime.datetime.now(),
1267+
send_email=False,
1268+
)
1269+
BallotPositionDocEventFactory(
1270+
ballot=ballot,
1271+
balloter=balloters[5],
1272+
pos_id='noobj',
1273+
comment='Commentary',
1274+
comment_time=datetime.datetime.now() - datetime.timedelta(days=1),
1275+
send_email=True,
1276+
)
1277+
BallotPositionDocEventFactory(
1278+
ballot=ballot,
1279+
balloter=balloters[5],
1280+
pos_id='noobj',
1281+
comment='Commentary',
1282+
comment_time=datetime.datetime.now(),
1283+
send_email=False,
1284+
)
1285+
1286+
# Create a few positions with non-active-ad people. These will be treated
1287+
# as "old" ballot positions because the people are not in the list returned
1288+
# by get_active_ads()
1289+
#
1290+
# Some faked non-ASCII names wind up with plain names that cannot be slugified.
1291+
# This causes test failure because that slug is used in an HTML element ID.
1292+
# Until that's fixed, set the plain names to something guaranteed unique so
1293+
# the test does not randomly fail.
1294+
no_email_balloter = BallotPositionDocEventFactory(
1295+
ballot=ballot,
1296+
balloter__plain='plain name1',
1297+
pos_id='discuss',
1298+
discuss='Discussion text',
1299+
discuss_time=datetime.datetime.now(),
1300+
send_email=False,
1301+
).balloter
1302+
send_email_balloter = BallotPositionDocEventFactory(
1303+
ballot=ballot,
1304+
balloter__plain='plain name2',
1305+
pos_id='discuss',
1306+
discuss='Discussion text',
1307+
discuss_time=datetime.datetime.now(),
1308+
send_email=True,
1309+
).balloter
1310+
prev_send_email_balloter = BallotPositionDocEventFactory(
1311+
ballot=ballot,
1312+
balloter__plain='plain name3',
1313+
pos_id='discuss',
1314+
discuss='Discussion text',
1315+
discuss_time=datetime.datetime.now() - datetime.timedelta(days=1),
1316+
send_email=True,
1317+
).balloter
1318+
BallotPositionDocEventFactory(
1319+
ballot=ballot,
1320+
balloter=prev_send_email_balloter,
1321+
pos_id='discuss',
1322+
discuss='Discussion text',
1323+
discuss_time=datetime.datetime.now(),
1324+
send_email=False,
1325+
)
1326+
1327+
content = document_ballot_content(
1328+
request=RequestFactory(),
1329+
doc=ballot.doc,
1330+
ballot_id=ballot.pk,
1331+
)
1332+
q = PyQuery(content)
1333+
self._assertBallotMessage(q, balloters[0], 'Email requested to be sent for this discuss')
1334+
self._assertBallotMessage(q, balloters[1], 'Email requested to be sent for this comment')
1335+
self._assertBallotMessage(q, balloters[2], 'No email send requests for this discuss')
1336+
self._assertBallotMessage(q, balloters[3], 'No email send requests for this comment')
1337+
self._assertBallotMessage(q, balloters[4], 'Email requested to be sent for earlier discuss')
1338+
self._assertBallotMessage(q, balloters[5], 'Email requested to be sent for earlier comment')
1339+
self._assertBallotMessage(q, no_email_balloter, 'No email send requests for this ballot position')
1340+
self._assertBallotMessage(q, send_email_balloter, 'Email requested to be sent for this ballot position')
1341+
self._assertBallotMessage(q, prev_send_email_balloter, 'Email requested to be sent for earlier ballot position')
1342+
1343+
def test_document_ballot_content_without_send_email_values(self):
1344+
"""Ballot content correctly indicates lack of send_email field in records"""
1345+
ballot = BallotDocEventFactory()
1346+
balloters = get_active_ads()
1347+
self.assertGreaterEqual(len(balloters), 2,
1348+
'Oops! Need to create additional active balloters for test')
1349+
BallotPositionDocEventFactory(
1350+
ballot=ballot,
1351+
balloter=balloters[0],
1352+
pos_id='discuss',
1353+
discuss='Discussion text',
1354+
discuss_time=datetime.datetime.now(),
1355+
send_email=None,
1356+
)
1357+
BallotPositionDocEventFactory(
1358+
ballot=ballot,
1359+
balloter=balloters[1],
1360+
pos_id='noobj',
1361+
comment='Commentary',
1362+
comment_time=datetime.datetime.now(),
1363+
send_email=None,
1364+
)
1365+
old_balloter = BallotPositionDocEventFactory(
1366+
ballot=ballot,
1367+
balloter__plain='plain name', # ensure plain name is slugifiable
1368+
pos_id='discuss',
1369+
discuss='Discussion text',
1370+
discuss_time=datetime.datetime.now(),
1371+
send_email=None,
1372+
).balloter
1373+
1374+
content = document_ballot_content(
1375+
request=RequestFactory(),
1376+
doc=ballot.doc,
1377+
ballot_id=ballot.pk,
1378+
)
1379+
q = PyQuery(content)
1380+
self._assertBallotMessage(q, balloters[0], 'No email send requests for this discuss')
1381+
self._assertBallotMessage(q, balloters[1], 'No ballot position send log available')
1382+
self._assertBallotMessage(q, old_balloter, 'No ballot position send log available')

ietf/doc/views_doc.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,6 +1177,10 @@ def document_ballot_content(request, doc, ballot_id, editable=True):
11771177
positions = ballot.all_positions()
11781178

11791179
# put into position groups
1180+
#
1181+
# Each position group is a tuple (BallotPositionName, [BallotPositionDocEvent, ...])
1182+
# THe list contains the latest entry for each AD, possibly with a fake 'no record' entry
1183+
# for any ADs without an event. Blocking positions are earlier in the list than non-blocking.
11801184
position_groups = []
11811185
for n in BallotPositionName.objects.filter(slug__in=[p.pos_id for p in positions]).order_by('order'):
11821186
g = (n, [p for p in positions if p.pos_id == n.slug])

0 commit comments

Comments
 (0)