# Copyright The IETF Trust 2013-2020, All Rights Reserved # -*- coding: utf-8 -*- import datetime import mock from pyquery import PyQuery import debug # pyflakes:ignore from django.test import RequestFactory from django.utils.text import slugify from django.urls import reverse as urlreverse from django.utils import timezone from ietf.doc.models import (Document, State, DocEvent, BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, TelechatDocEvent) from ietf.doc.factories import (DocumentFactory, IndividualDraftFactory, IndividualRfcFactory, WgDraftFactory, BallotPositionDocEventFactory, BallotDocEventFactory, IRSGBallotDocEventFactory) from ietf.doc.templatetags.ietf_filters import can_defer from ietf.doc.utils import create_ballot_if_not_open from ietf.doc.views_ballot import parse_ballot_edit_return_point from ietf.doc.views_doc import document_ballot_content from ietf.group.models import Group, Role from ietf.group.factories import GroupFactory, RoleFactory, ReviewTeamFactory from ietf.ipr.factories import HolderIprDisclosureFactory from ietf.name.models import BallotPositionName from ietf.iesg.models import TelechatDate from ietf.person.models import Person, PersonalApiKey from ietf.person.factories import PersonFactory from ietf.person.utils import get_active_ads from ietf.utils.test_utils import TestCase, login_testing_unauthorized from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.text import unwrap from ietf.utils.timezone import date_today, datetime_today class EditPositionTests(TestCase): def test_edit_position(self): ad = Person.objects.get(user__username="ad") draft = IndividualDraftFactory(ad=ad,stream_id='ietf') ballot = create_ballot_if_not_open(None, draft, ad, 'approve') url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=draft.name, ballot_id=ballot.pk)) login_testing_unauthorized(self, "ad", url) ad = Person.objects.get(name="Areað Irector") # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(len(q('form input[name=position]')) > 0) self.assertEqual(len(q('form textarea[name=comment]')), 1) # vote events_before = draft.docevent_set.count() r = self.client.post(url, dict(position="discuss", discuss=" This is a discussion test. \n ", comment=" This is a test. \n ")) self.assertEqual(r.status_code, 302) pos = draft.latest_event(BallotPositionDocEvent, balloter=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(position="noobj")) self.assertEqual(r.status_code, 302) draft = Document.objects.get(name=draft.name) pos = draft.latest_event(BallotPositionDocEvent, balloter=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(position="norecord")) self.assertEqual(r.status_code, 302) draft = Document.objects.get(name=draft.name) pos = draft.latest_event(BallotPositionDocEvent, balloter=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(position="norecord", comment="New comment.")) self.assertEqual(r.status_code, 302) draft = Document.objects.get(name=draft.name) pos = draft.latest_event(BallotPositionDocEvent, balloter=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_api_set_position(self): ad = Person.objects.get(name="Areað Irector") draft = WgDraftFactory(ad=ad) url = urlreverse('ietf.doc.views_ballot.api_set_position') create_ballot_if_not_open(None, draft, ad, 'approve') ad.user.last_login = timezone.now() ad.user.save() apikey = PersonalApiKey.objects.create(endpoint=url, person=ad) # vote events_before = draft.docevent_set.count() mailbox_before = len(outbox) 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.assertContains(r, "Done") pos = draft.latest_event(BallotPositionDocEvent, balloter=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) self.assertEqual(len(outbox), mailbox_before + 1) # recast vote events_before = draft.docevent_set.count() mailbox_before = len(outbox) r = self.client.post(url, dict(apikey=apikey.hash(), doc=draft.name, position="noobj")) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) pos = draft.latest_event(BallotPositionDocEvent, balloter=ad) self.assertEqual(pos.pos.slug, "noobj") self.assertEqual(draft.docevent_set.count(), events_before + 1) self.assertTrue("Position for" in pos.desc) self.assertEqual(len(outbox), mailbox_before + 1) m = outbox[-1] self.assertIn('No Objection', m['Subject']) self.assertIn('iesg@', m['To']) self.assertIn(draft.name, m['Cc']) self.assertIn(draft.group.acronym+'-chairs@', m['Cc']) # clear vote events_before = draft.docevent_set.count() mailbox_before = len(outbox) r = self.client.post(url, dict(apikey=apikey.hash(), doc=draft.name, position="norecord")) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) pos = draft.latest_event(BallotPositionDocEvent, balloter=ad) self.assertEqual(pos.pos.slug, "norecord") self.assertEqual(draft.docevent_set.count(), events_before + 1) self.assertTrue("Position for" in pos.desc) self.assertEqual(len(outbox), mailbox_before + 1) m = outbox[-1] self.assertIn('No Record', m['Subject']) # change comment events_before = draft.docevent_set.count() mailbox_before = len(outbox) r = self.client.post(url, dict(apikey=apikey.hash(), doc=draft.name, position="norecord", comment="New comment.")) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) pos = draft.latest_event(BallotPositionDocEvent, balloter=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) self.assertEqual(len(outbox), mailbox_before + 1) m = outbox[-1] self.assertIn('COMMENT', m['Subject']) self.assertIn('New comment', get_payload_text(m)) def test_edit_position_as_secretary(self): draft = IndividualDraftFactory() ad = Person.objects.get(user__username="ad") ballot = create_ballot_if_not_open(None, draft, ad, 'approve') url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=draft.name, ballot_id=ballot.pk)) ad = Person.objects.get(name="Areað Irector") url += "?balloter=%s" % ad.pk login_testing_unauthorized(self, "secretary", url) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(len(q('form input[name=position]')) > 0) # vote on behalf of AD # events_before = draft.docevent_set.count() r = self.client.post(url, dict(position="discuss", discuss="Test discuss text")) self.assertEqual(r.status_code, 302) pos = draft.latest_event(BallotPositionDocEvent, balloter=ad) self.assertEqual(pos.pos.slug, "discuss") self.assertEqual(pos.discuss, "Test discuss text") self.assertTrue("New position" in pos.desc) self.assertTrue("by Sec" in pos.desc) def test_cannot_edit_position_as_pre_ad(self): draft = IndividualDraftFactory() ad = Person.objects.get(user__username="ad") ballot = create_ballot_if_not_open(None, draft, ad, 'approve') url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=draft.name, ballot_id=ballot.pk)) # transform to pre-ad ad_role = Role.objects.filter(name="ad")[0] ad_role.name_id = "pre-ad" ad_role.save() # we can see login_testing_unauthorized(self, ad_role.person.user.username, url) # but not touch r = self.client.post(url, dict(position="discuss", discuss="Test discuss text")) self.assertEqual(r.status_code, 403) # N.B. This test needs to be rewritten to exercise all types of ballots (iesg, irsg, rsab) # and test against the output of the mailtriggers instead of looking for hardcoded values # in the To and CC results. See #7864 def test_send_ballot_comment(self): ad = Person.objects.get(user__username="ad") draft = WgDraftFactory(ad=ad,group__acronym='mars') draft.notify = "somebody@example.com" draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")]) ballot = create_ballot_if_not_open(None, draft, ad, 'approve') BallotPositionDocEvent.objects.create( doc=draft, rev=draft.rev, type="changed_ballot_position", by=ad, balloter=ad, ballot=ballot, pos=BallotPositionName.objects.get(slug="discuss"), discuss="This draft seems to be lacking a clearer title?", discuss_time=timezone.now(), comment="Test!", comment_time=timezone.now()) url = urlreverse('ietf.doc.views_ballot.send_ballot_comment', kwargs=dict(name=draft.name, ballot_id=ballot.pk)) login_testing_unauthorized(self, "ad", url) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(len(q('form input[name="extra_cc"]')) > 0) # send mailbox_before = len(outbox) r = self.client.post(url, dict(extra_cc="test298347@example.com", cc_choices=['doc_notify','doc_group_chairs'])) self.assertEqual(r.status_code, 302) self.assertEqual(len(outbox), mailbox_before + 1) m = outbox[-1] self.assertTrue("COMMENT" in m['Subject']) self.assertTrue("DISCUSS" in m['Subject']) self.assertTrue(draft.name in m['Subject']) self.assertTrue("clearer title" in str(m)) self.assertTrue("Test!" in str(m)) self.assertTrue("iesg@" in m['To']) # cc_choice doc_group_chairs self.assertTrue("mars-chairs@" in m['Cc']) # cc_choice doc_notify self.assertTrue("somebody@example.com" in m['Cc']) # cc_choice doc_group_email_list was not selected self.assertFalse(draft.group.list_email in m['Cc']) # extra-cc self.assertTrue("test298347@example.com" in m['Cc']) r = self.client.post(url, dict(cc="")) self.assertEqual(r.status_code, 302) self.assertEqual(len(outbox), mailbox_before + 2) m = outbox[-1] self.assertTrue("iesg@" in m['To']) self.assertFalse(m['Cc'] and draft.group.list_email in m['Cc']) class BallotWriteupsTests(TestCase): def test_edit_last_call_text(self): draft = IndividualDraftFactory(ad=Person.objects.get(user__username='ad'),states=[('draft','active'),('draft-iesg','ad-eval')]) url = urlreverse('ietf.doc.views_ballot.lastcalltext', kwargs=dict(name=draft.name)) login_testing_unauthorized(self, "secretary", url) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q('textarea[name=last_call_text]')), 1) self.assertTrue(q('[type=submit]:contains("Save")')) # we're Secretariat, so we got The Link self.assertEqual(len(q('a:contains("Issue last call")')), 1) # subject error r = self.client.post(url, dict( last_call_text="Subject: test\r\nhello\r\n\r\n", save_last_call_text="1")) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(len(q('form .is-invalid')) > 0) # save r = self.client.post(url, dict( last_call_text="This is a simple test.", save_last_call_text="1")) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) self.assertTrue("This is a simple test" in draft.latest_event(WriteupDocEvent, type="changed_last_call_text").text) # test regenerate r = self.client.post(url, dict( last_call_text="This is a simple test.", regenerate_last_call_text="1")) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) text = q("[name=last_call_text]").text() self.assertTrue("Subject: Last Call" in text) def test_request_last_call(self): ad = Person.objects.get(user__username="ad") draft = IndividualDraftFactory(ad=ad,states=[('draft-iesg','iesg-eva')]) url = urlreverse('ietf.doc.views_ballot.lastcalltext', kwargs=dict(name=draft.name)) login_testing_unauthorized(self, "secretary", url) # give us an announcement to send r = self.client.post(url, dict(regenerate_last_call_text="1")) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) text = q("[name=last_call_text]").text() mailbox_before = len(outbox) # send r = self.client.post(url, dict( last_call_text=text, send_last_call_request="1")) draft = Document.objects.get(name=draft.name) self.assertEqual(draft.get_state_slug("draft-iesg"), "lc-req") self.assertCountEqual(draft.action_holders.all(), [ad]) self.assertIn('Changed action holders', draft.latest_event(type='changed_action_holders').desc) self.assertEqual(len(outbox), mailbox_before + 1) self.assertTrue("Last Call" in outbox[-1]['Subject']) self.assertTrue(draft.name in outbox[-1]['Subject']) self.assertTrue('iesg-secretary@' in outbox[-1]['To']) self.assertTrue('aread@' in outbox[-1]['Cc']) def test_edit_ballot_writeup(self): draft = IndividualDraftFactory(states=[('draft','active'),('draft-iesg','iesg-eva')]) url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name)) login_testing_unauthorized(self, "secretary", url) # add a IANA review note draft.set_state(State.objects.get(used=True, type="draft-iana-review", slug="not-ok")) DocEvent.objects.create(type="iana_review", doc=draft, rev=draft.rev, by=Person.objects.get(user__username="iana"), desc="IANA does not approve of this document, it does not make sense.", ) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1) self.assertTrue(q('[type=submit]:contains("Save")')) self.assertContains(r, "IANA does not") # save r = self.client.post(url, dict( ballot_writeup="This is a simple test.", save_ballot_writeup="1")) self.assertEqual(r.status_code, 200) d = Document.objects.get(name=draft.name) self.assertTrue("This is a simple test" in d.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text").text) self.assertTrue('iesg-eva' == d.get_state_slug('draft-iesg')) def test_edit_ballot_writeup_already_approved(self): draft = IndividualDraftFactory(states=[('draft','active'),('draft-iesg','approved')]) url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name)) login_testing_unauthorized(self, "secretary", url) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1) self.assertTrue(q('[type=submit]:contains("Save")')) # save r = self.client.post(url, dict( ballot_writeup="This is a simple test.", save_ballot_writeup="1")) self.assertEqual(r.status_code, 200) d = Document.objects.get(name=draft.name) self.assertTrue('approved' == d.get_state_slug('draft-iesg')) def test_edit_ballot_rfceditornote(self): draft = IndividualDraftFactory() url = urlreverse('ietf.doc.views_ballot.ballot_rfceditornote', kwargs=dict(name=draft.name)) login_testing_unauthorized(self, "secretary", url) # add a note to the RFC Editor WriteupDocEvent.objects.create( doc=draft, rev=draft.rev, desc="Changed text", type="changed_rfc_editor_note_text", text="This is a note for the RFC Editor.", by=Person.objects.get(name="(System)")) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q('textarea[name=rfc_editor_note]')), 1) self.assertTrue(q('[type=submit]:contains("Save")')) self.assertContains(r, "") self.assertContains(r, "This is a note for the RFC Editor") # save with a note empty_outbox() r = self.client.post(url, dict( rfc_editor_note="This is a simple test.", save_ballot_rfceditornote="1")) self.assertEqual(r.status_code, 302) draft = Document.objects.get(name=draft.name) self.assertTrue(draft.has_rfc_editor_note()) self.assertTrue("This is a simple test" in draft.latest_event(WriteupDocEvent, type="changed_rfc_editor_note_text").text) self.assertEqual(len(outbox), 0) # clear the existing note r = self.client.post(url, dict( rfc_editor_note=" ", clear_ballot_rfceditornote="1")) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) self.assertFalse(draft.has_rfc_editor_note()) # Add a note after the doc is approved empty_outbox() draft.set_state(State.objects.get(type='draft-iesg',slug='approved')) r = self.client.post(url, dict( rfc_editor_note='This is a new note.', save_ballot_rfceditornote="1")) self.assertEqual(r.status_code, 302) self.assertEqual(len(outbox),1) self.assertIn('RFC Editor note changed',outbox[-1]['Subject']) def test_issue_ballot(self): ad = Person.objects.get(user__username="ad") for case in ('none','past','future'): draft = IndividualDraftFactory(ad=ad) if case in ('past','future'): LastCallDocEvent.objects.create( by=Person.objects.get(name='(System)'), type='sent_last_call', doc=draft, rev=draft.rev, desc='issued last call', expires = timezone.now()+datetime.timedelta(days = 1 if case=='future' else -1) ) url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name)) login_testing_unauthorized(self, "ad", url) empty_outbox() r = self.client.post(url, dict( ballot_writeup="This is a test.", issue_ballot="1")) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) self.assertTrue(draft.latest_event(type="sent_ballot_announcement")) self.assertEqual(len(outbox), 2) self.assertTrue('Ballot issued:' in outbox[-2]['Subject']) self.assertTrue('iesg@' in outbox[-2]['To']) self.assertTrue('Ballot issued:' in outbox[-1]['Subject']) self.assertTrue('drafts-eval@' in outbox[-1]['To']) self.assertTrue('X-IETF-Draft-string' in outbox[-1]) if case=='none': self.assertNotIn('call expire', get_payload_text(outbox[-1])) elif case=='past': self.assertIn('call expired', get_payload_text(outbox[-1])) else: self.assertIn('call expires', get_payload_text(outbox[-1])) self.client.logout() def test_issue_ballot_auto_state_change(self): ad = Person.objects.get(user__username="ad") draft = IndividualDraftFactory(ad=ad, states=[('draft','active'),('draft-iesg','writeupw')]) url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name)) login_testing_unauthorized(self, "secretary", url) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1) self.assertFalse(q('[class=form-text]:contains("not completed IETF Last Call")')) self.assertTrue(q('[type=submit]:contains("Save")')) self.assertCountEqual(draft.action_holders.all(), []) # save r = self.client.post(url, dict( ballot_writeup="This is a simple test.", issue_ballot="1")) self.assertEqual(r.status_code, 200) d = Document.objects.get(name=draft.name) self.assertTrue('iesg-eva' == d.get_state_slug('draft-iesg')) self.assertCountEqual(draft.action_holders.all(), [ad]) def test_issue_ballot_warn_if_early(self): ad = Person.objects.get(user__username="ad") draft = IndividualDraftFactory(ad=ad, states=[('draft','active'),('draft-iesg','lc')]) url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=draft.name)) login_testing_unauthorized(self, "secretary", url) # expect warning about issuing a ballot before IETF Last Call is done # No last call has yet been issued r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1) self.assertTrue(q('[class=text-danger]:contains("not completed IETF Last Call")')) self.assertTrue(q('[type=submit]:contains("Save")')) # Last call exists but hasn't expired LastCallDocEvent.objects.create( doc=draft, expires=datetime_today()+datetime.timedelta(days=14), by=Person.objects.get(name="(System)") ) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('[class=text-danger]:contains("not completed IETF Last Call")')) # Last call exists and has expired LastCallDocEvent.objects.filter(doc=draft).update(expires=datetime_today()-datetime.timedelta(days=2)) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertFalse(q('[class=text-danger]:contains("not completed IETF Last Call")')) for state_slug in ["lc", "ad-eval"]: draft.set_state(State.objects.get(type="draft-iesg",slug=state_slug)) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('[class=text-danger]:contains("It would be unexpected to issue a ballot while in this state.")')) draft.set_state(State.objects.get(type="draft-iesg",slug="writeupw")) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertFalse(q('[class=text-danger]:contains("It would be unexpected to issue a ballot while in this state.")')) def test_edit_approval_text(self): ad = Person.objects.get(user__username="ad") draft = WgDraftFactory(ad=ad,states=[('draft','active'),('draft-iesg','iesg-eva')],intended_std_level_id='ps',group__parent=Group.objects.get(acronym='farfut')) url = urlreverse('ietf.doc.views_ballot.ballot_approvaltext', kwargs=dict(name=draft.name)) login_testing_unauthorized(self, "secretary", url) # normal get r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q('textarea[name=approval_text]')), 1) self.assertTrue(q('[type=submit]:contains("Save")')) # save r = self.client.post(url, dict( approval_text="This is a simple test.", save_approval_text="1")) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) self.assertTrue("This is a simple test" in draft.latest_event(WriteupDocEvent, type="changed_ballot_approval_text").text) # test regenerate r = self.client.post(url, dict(regenerate_approval_text="1")) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) self.assertTrue("Subject: Protocol Action" in draft.latest_event(WriteupDocEvent, type="changed_ballot_approval_text").text) # test regenerate when it's a disapprove draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="nopubadw")) r = self.client.post(url, dict(regenerate_approval_text="1")) self.assertEqual(r.status_code, 200) draft = Document.objects.get(name=draft.name) self.assertIn("NOT be published", unwrap(draft.latest_event(WriteupDocEvent, type="changed_ballot_approval_text").text)) # test regenerate when it's a conflict review draft.group = Group.objects.get(type="individ") draft.stream_id = "irtf" draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="iesg-eva")) draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")]) r = self.client.post(url, dict(regenerate_approval_text="1")) self.assertEqual(r.status_code, 200) self.assertTrue("Subject: Results of IETF-conflict review" in draft.latest_event(WriteupDocEvent, type="changed_ballot_approval_text").text) def test_edit_verify_permissions(self): def verify_fail(username, url): if username: self.client.login(username=username, password=username+"+password") r = self.client.get(url) self.assertEqual(r.status_code,403) def verify_can_see(username, url): self.client.login(username=username, password=username+"+password") r = self.client.get(url) self.assertEqual(r.status_code,200) q = PyQuery(r.content) self.assertEqual(len(q("