Added the notion of responsible leadership.

- Legacy-Id: 19202
This commit is contained in:
Robert Sparks 2021-07-07 17:49:35 +00:00
parent 338da98661
commit f5a04263e5
20 changed files with 432 additions and 93 deletions

View file

@ -12,7 +12,7 @@ from .models import (StateType, State, RelatedDocument, DocumentAuthor, Document
TelechatDocEvent, BallotPositionDocEvent, ReviewRequestDocEvent, InitialReviewDocEvent,
AddedMessageEvent, SubmissionDocEvent, DeletedEvent, EditedAuthorsDocEvent, DocumentURL,
ReviewAssignmentDocEvent, IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource, DocumentActionHolder,
BofreqEditorDocEvent )
BofreqEditorDocEvent, BofreqResponsibleDocEvent )
from ietf.utils.validators import validate_external_resource_value
@ -198,6 +198,10 @@ class BofreqEditorDocEventAdmin(DocEventAdmin):
raw_id_fields = ["doc", "by", "editors" ]
admin.site.register(BofreqEditorDocEvent, BofreqEditorDocEventAdmin)
class BofreqResponsibleDocEventAdmin(DocEventAdmin):
raw_id_fields = ["doc", "by", "responsible" ]
admin.site.register(BofreqResponsibleDocEvent, BofreqResponsibleDocEventAdmin)
class DocumentUrlAdmin(admin.ModelAdmin):
list_display = ['id', 'doc', 'tag', 'url', 'desc', ]
search_fields = ['doc__name', 'url', ]

View file

@ -13,9 +13,10 @@ from django.conf import settings
from ietf.doc.models import ( Document, DocEvent, NewRevisionDocEvent, DocAlias, State, DocumentAuthor,
StateDocEvent, BallotPositionDocEvent, BallotDocEvent, BallotType, IRSGBallotDocEvent, TelechatDocEvent,
DocumentActionHolder, BofreqEditorDocEvent )
DocumentActionHolder, BofreqEditorDocEvent, BofreqResponsibleDocEvent )
from ietf.group.models import Group
from ietf.person.factories import PersonFactory
from ietf.group.factories import RoleFactory
from ietf.utils.text import xslugify
@ -401,12 +402,32 @@ class BofreqEditorDocEventFactory(DocEventFactory):
obj.editors.set(PersonFactory.create_batch(3))
obj.desc = f'Changed editors to {", ".join(obj.editors.values_list("name",flat=True)) or "(None)"}'
class BofreqResponsibleDocEventFactory(DocEventFactory):
class Meta:
model = BofreqResponsibleDocEvent
type = "changed_responsible"
doc = factory.SubFactory('ietf.doc.factories.BofreqFactory')
@factory.post_generation
def responsible(obj, create, extracted, **kwargs):
if not create:
return
if extracted:
obj.responsible.set(extracted)
else:
ad = RoleFactory(group__type_id='area',name_id='ad').person
obj.responsible.set([ad])
obj.desc = f'Changed responsible leadership to {", ".join(obj.responsible.values_list("name",flat=True)) or "(None)"}'
class BofreqFactory(BaseDocumentFactory):
type_id = 'bofreq'
title = factory.Faker('sentence')
name = factory.LazyAttribute(lambda o: 'bofreq-%s'%(xslugify(o.title)))
bofreqeditordocevent = factory.RelatedFactory('ietf.doc.factories.BofreqEditorDocEventFactory','doc')
bofreqresponsibledocevent = factory.RelatedFactory('ietf.doc.factories.BofreqResponsibleDocEventFactory','doc')
@factory.post_generation
def states(obj, create, extracted, **kwargs):

View file

@ -18,8 +18,9 @@ from ietf.doc.templatetags.mail_filters import std_level_prompt
from ietf.utils import log
from ietf.utils.mail import send_mail, send_mail_text
from ietf.ipr.utils import iprs_from_docs, related_docs
from ietf.doc.models import WriteupDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent, BofreqEditorDocEvent
from ietf.doc.models import WriteupDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent
from ietf.doc.utils import needed_ballot_positions
from ietf.doc.utils_bofreq import bofreq_editors, bofreq_responsible
from ietf.group.models import Role
from ietf.doc.models import Document
from ietf.mailtrigger.utils import gather_address_lists
@ -699,14 +700,27 @@ def email_bofreq_title_changed(request, bofreq):
dict(bofreq=bofreq, request=request),
cc=addrs.cc)
def plain_names(persons):
return [p.plain_name() for p in persons]
def email_bofreq_editors_changed(request, bofreq, previous_editors):
editors = bofreq.latest_event(BofreqEditorDocEvent).editors.all()
editors = bofreq_editors(bofreq)
addrs = gather_address_lists('bofreq_editors_changed', doc=bofreq, previous_editors=previous_editors)
send_mail(request, addrs.to, settings.DEFAULT_FROM_EMAIL,
f'BOF Request editors changed : {bofreq.name}',
'doc/mail/bofreq_editors_changed.txt',
dict(bofreq=bofreq, request=request, editors=editors, previous_editors=previous_editors),
dict(bofreq=bofreq, request=request, editors=plain_names(editors), previous_editors=plain_names(previous_editors)),
cc=addrs.cc)
def email_bofreq_responsible_changed(request, bofreq, previous_responsible):
responsible = bofreq_responsible(bofreq)
addrs = gather_address_lists('bofreq_responsible_changed', doc=bofreq, previous_responsible=previous_responsible)
send_mail(request, addrs.to, settings.DEFAULT_FROM_EMAIL,
f'BOF Request responsible leadership changed : {bofreq.name}',
'doc/mail/bofreq_responsible_changed.txt',
dict(bofreq=bofreq, request=request, responsible=plain_names(responsible), previous_responsible=plain_names(previous_responsible)),
cc=addrs.cc)
def email_bofreq_new_revision(request, bofreq):

View file

@ -0,0 +1,36 @@
# Generated by Django 2.2.24 on 2021-07-06 13:34
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('person', '0019_auto_20210604_1443'),
('doc', '0042_bofreq_states'),
]
operations = [
migrations.AlterField(
model_name='docevent',
name='type',
field=models.CharField(choices=[('new_revision', 'Added new revision'), ('new_submission', 'Uploaded new revision'), ('changed_document', 'Changed document metadata'), ('added_comment', 'Added comment'), ('added_message', 'Added message'), ('edited_authors', 'Edited the documents author list'), ('deleted', 'Deleted document'), ('changed_state', 'Changed state'), ('changed_stream', 'Changed document stream'), ('expired_document', 'Expired document'), ('extended_expiry', 'Extended expiry of document'), ('requested_resurrect', 'Requested resurrect'), ('completed_resurrect', 'Completed resurrect'), ('changed_consensus', 'Changed consensus'), ('published_rfc', 'Published RFC'), ('added_suggested_replaces', 'Added suggested replacement relationships'), ('reviewed_suggested_replaces', 'Reviewed suggested replacement relationships'), ('changed_action_holders', 'Changed action holders for document'), ('changed_group', 'Changed group'), ('changed_protocol_writeup', 'Changed protocol writeup'), ('changed_charter_milestone', 'Changed charter milestone'), ('initial_review', 'Set initial review time'), ('changed_review_announcement', 'Changed WG Review text'), ('changed_action_announcement', 'Changed WG Action text'), ('started_iesg_process', 'Started IESG process on document'), ('created_ballot', 'Created ballot'), ('closed_ballot', 'Closed ballot'), ('sent_ballot_announcement', 'Sent ballot announcement'), ('changed_ballot_position', 'Changed ballot position'), ('changed_ballot_approval_text', 'Changed ballot approval text'), ('changed_ballot_writeup_text', 'Changed ballot writeup text'), ('changed_rfc_editor_note_text', 'Changed RFC Editor Note text'), ('changed_last_call_text', 'Changed last call text'), ('requested_last_call', 'Requested last call'), ('sent_last_call', 'Sent last call'), ('scheduled_for_telechat', 'Scheduled for telechat'), ('iesg_approved', 'IESG approved document (no problem)'), ('iesg_disapproved', 'IESG disapproved document (do not publish)'), ('approved_in_minute', 'Approved in minute'), ('iana_review', 'IANA review comment'), ('rfc_in_iana_registry', 'RFC is in IANA registry'), ('rfc_editor_received_announcement', 'Announcement was received by RFC Editor'), ('requested_publication', 'Publication at RFC Editor requested'), ('sync_from_rfc_editor', 'Received updated information from RFC Editor'), ('requested_review', 'Requested review'), ('assigned_review_request', 'Assigned review request'), ('closed_review_request', 'Closed review request'), ('closed_review_assignment', 'Closed review assignment'), ('downref_approved', 'Downref approved'), ('posted_related_ipr', 'Posted related IPR'), ('removed_related_ipr', 'Removed related IPR'), ('changed_editors', 'Changed BOF Request editors')], max_length=50),
),
migrations.CreateModel(
name='BofreqResponsibleDocEvent',
fields=[
('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')),
('responsible', models.ManyToManyField(blank=True, to='person.Person')),
],
bases=('doc.docevent',),
),
migrations.CreateModel(
name='BofreqEditorDocEvent',
fields=[
('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')),
('editors', models.ManyToManyField(blank=True, to='person.Person')),
],
bases=('doc.docevent',),
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 2.2.23 on 2021-05-25 12:02
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('person', '0018_auto_20201109_0439'),
('doc', '0042_bofreq_states'),
]
operations = [
migrations.CreateModel(
name='BofreqEditorDocEvent',
fields=[
('docevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.DocEvent')),
('editors', models.ManyToManyField(blank=True, to='person.Person')),
],
bases=('doc.docevent',),
),
]

View file

@ -1349,4 +1349,9 @@ class EditedAuthorsDocEvent(DocEvent):
basis = models.CharField(help_text="What is the source or reasoning for the changes to the author list",max_length=255)
class BofreqEditorDocEvent(DocEvent):
editors = models.ManyToManyField('person.Person',blank=True)
""" Capture the proponents of a Bof Request."""
editors = models.ManyToManyField('person.Person', blank=True)
class BofreqResponsibleDocEvent(DocEvent):
""" Capture the responsible leadership (IAB and IESG members) for a BoF Request """
responsible = models.ManyToManyField('person.Person', blank=True)

View file

@ -17,7 +17,8 @@ from ietf.doc.models import (BallotType, DeletedEvent, StateType, State, Documen
InitialReviewDocEvent, DocHistoryAuthor, BallotDocEvent, RelatedDocument,
RelatedDocHistory, BallotPositionDocEvent, AddedMessageEvent, SubmissionDocEvent,
ReviewRequestDocEvent, ReviewAssignmentDocEvent, EditedAuthorsDocEvent, DocumentURL,
IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource, DocumentActionHolder, BofreqEditorDocEvent)
IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource, DocumentActionHolder,
BofreqEditorDocEvent,BofreqResponsibleDocEvent)
from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource
class BallotTypeResource(ModelResource):
@ -832,3 +833,29 @@ class BofreqEditorDocEventResource(ModelResource):
"editors": ALL_WITH_RELATIONS,
}
api.doc.register(BofreqEditorDocEventResource())
from ietf.person.resources import PersonResource
class BofreqResponsibleDocEventResource(ModelResource):
by = ToOneField(PersonResource, 'by')
doc = ToOneField(DocumentResource, 'doc')
docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr')
responsible = ToManyField(PersonResource, 'responsible', null=True)
class Meta:
queryset = BofreqResponsibleDocEvent.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'bofreqresponsibledocevent'
ordering = ['docevent_ptr', ]
filtering = {
"id": ALL,
"time": ALL,
"type": ALL,
"rev": ALL,
"desc": ALL,
"by": ALL_WITH_RELATIONS,
"doc": ALL_WITH_RELATIONS,
"docevent_ptr": ALL_WITH_RELATIONS,
"responsible": ALL_WITH_RELATIONS,
}
api.doc.register(BofreqResponsibleDocEventResource())

View file

@ -11,14 +11,15 @@ from tempfile import NamedTemporaryFile
from django.conf import settings
from django.urls import reverse as urlreverse
from ietf.group.factories import RoleFactory
from ietf.doc.factories import BofreqFactory, NewRevisionDocEventFactory
from ietf.doc.models import State, BofreqEditorDocEvent, Document, DocAlias, NewRevisionDocEvent
from ietf.doc.models import State, Document, DocAlias, NewRevisionDocEvent
from ietf.doc.utils_bofreq import bofreq_editors, bofreq_responsible
from ietf.person.factories import PersonFactory
from ietf.utils.mail import outbox, empty_outbox
from ietf.utils.test_utils import TestCase, reload_db_objects, unicontent, login_testing_unauthorized
class BofreqTests(TestCase):
def setUp(self):
@ -42,10 +43,12 @@ This test section has some text.
""")
def test_show_bof_requests(self):
url = urlreverse('ietf.doc.views_bofreq.bof_requests')
r = self.client.get(url)
self.assertContains(r, 'There are currently no BoF Requests', status_code=200)
states = State.objects.filter(type_id='bofreq')
self.assertTrue(states.count()>0)
reqs = BofreqFactory.create_batch(states.count())
url = urlreverse('ietf.doc.views_bofreq.bof_requests')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
@ -57,6 +60,13 @@ This test section has some text.
q = PyQuery(r.content)
for state in states:
self.assertEqual(len(q(f'#bofreqs-{state.slug} tbody tr')), 1)
self.assertFalse(q('#start_button'))
PersonFactory(user__username='nobody')
self.client.login(username='nobody', password='nobody+password')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('#start_button'))
def test_bofreq_main_page(self):
@ -67,7 +77,8 @@ This test section has some text.
doc.rev='01'
doc.save_with_history([nr_event])
self.write_bofreq_file(doc)
editors = doc.latest_event(BofreqEditorDocEvent).editors.all()
editors = bofreq_editors(doc)
responsible = bofreq_responsible(doc)
url = urlreverse('ietf.doc.views_doc.document_main', kwargs=dict(name=doc))
r = self.client.get(url)
self.assertContains(r,'Version: 01',status_code=200)
@ -77,12 +88,15 @@ This test section has some text.
editor_row = q('#editors').html()
for editor in editors:
self.assertInHTML(editor.plain_name(),editor_row)
responsible_row = q('#responsible').html()
for leader in responsible:
self.assertInHTML(leader.plain_name(),responsible_row)
for user in ('secretary','ad','iab-member'):
self.client.login(username=user,password=user+"+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(4, len(q('td.edit>a.btn')))
self.assertEqual(5, len(q('td.edit>a.btn')))
self.client.logout()
self.assertNotEqual([],q('#change-request'))
editor = editors.first().user.username
@ -98,9 +112,10 @@ This test section has some text.
self.assertContains(r,'Version: 00',status_code=200)
self.assertContains(r,'is for an older version')
def test_edit_title(self):
doc = BofreqFactory()
editor = doc.latest_event(BofreqEditorDocEvent).editors.first()
editor = bofreq_editors(doc).first()
url = urlreverse('ietf.doc.views_bofreq.edit_title', kwargs=dict(name=doc.name))
title = doc.title
r = self.client.post(url,dict(title='New title'))
@ -133,7 +148,7 @@ This test section has some text.
def test_edit_state(self):
doc = BofreqFactory()
editor = doc.latest_event(BofreqEditorDocEvent).editors.first()
editor = bofreq_editors(doc).first()
url = urlreverse('ietf.doc.views_bofreq.change_state', kwargs=dict(name=doc.name))
state = doc.get_state('bofreq')
r = self.client.post(url, dict(new_state=self.state_pk_as_str('bofreq','approved')))
@ -162,7 +177,7 @@ This test section has some text.
def test_change_editors(self):
doc = BofreqFactory()
previous_editors = list(doc.latest_event(BofreqEditorDocEvent).editors.all())
previous_editors = list(bofreq_editors(doc))
acting_editor = previous_editors[0]
new_editors = set(previous_editors)
new_editors.discard(acting_editor)
@ -171,13 +186,13 @@ This test section has some text.
postdict = dict(editors=','.join([str(p.pk) for p in new_editors]))
r = self.client.post(url, postdict)
self.assertEqual(r.status_code,302)
editors = doc.latest_event(BofreqEditorDocEvent).editors.all()
editors = bofreq_editors(doc)
self.assertEqual(set(previous_editors),set(editors))
nobody = PersonFactory()
self.client.login(username=nobody.user.username,password=nobody.user.username+'+password')
r = self.client.post(url, postdict)
self.assertEqual(r.status_code,403)
editors = doc.latest_event(BofreqEditorDocEvent).editors.all()
editors = bofreq_editors(doc)
self.assertEqual(set(previous_editors),set(editors))
self.client.logout()
for username in (previous_editors[0].user.username, 'secretary', 'ad', 'iab-member'):
@ -185,8 +200,8 @@ This test section has some text.
self.client.login(username=username,password=username+'+password')
r = self.client.get(url)
self.assertEqual(r.status_code,200)
unescaped = unicontent(r).encode('utf-8').decode('unicode-escape')
for editor in previous_editors:
unescaped = unicontent(r).encode('utf-8').decode('unicode-escape')
self.assertIn(editor.name,unescaped)
new_editors = set(previous_editors)
new_editors.discard(acting_editor)
@ -194,13 +209,74 @@ This test section has some text.
postdict = dict(editors=','.join([str(p.pk) for p in new_editors]))
r = self.client.post(url,postdict)
self.assertEqual(r.status_code, 302)
updated_editors = doc.latest_event(BofreqEditorDocEvent).editors.all()
updated_editors = bofreq_editors(doc)
self.assertEqual(new_editors,set(updated_editors))
previous_editors = new_editors
self.client.logout()
self.assertEqual(len(outbox),1)
self.assertIn('BOF Request editors changed',outbox[0]['Subject'])
def test_change_responsible(self):
doc = BofreqFactory()
previous_responsible = list(bofreq_responsible(doc))
new_responsible = set(previous_responsible[1:])
new_responsible.add(RoleFactory(group__type_id='area',name_id='ad').person)
url = urlreverse('ietf.doc.views_bofreq.change_responsible', kwargs=dict(name=doc.name))
postdict = dict(responsible=','.join([str(p.pk) for p in new_responsible]))
r = self.client.post(url, postdict)
self.assertEqual(r.status_code,302)
responsible = bofreq_responsible(doc)
self.assertEqual(set(previous_responsible), set(responsible))
PersonFactory(user__username='nobody')
self.client.login(username='nobody',password='nobody+password')
r = self.client.post(url, postdict)
self.assertEqual(r.status_code,403)
responsible = bofreq_responsible(doc)
self.assertEqual(set(previous_responsible), set(responsible))
self.client.logout()
for username in ('secretary', 'ad', 'iab-member'):
empty_outbox()
self.client.login(username=username,password=username+'+password')
r = self.client.get(url)
self.assertEqual(r.status_code,200)
unescaped = unicontent(r).encode('utf-8').decode('unicode-escape')
for responsible in previous_responsible:
self.assertIn(responsible.name,unescaped)
new_responsible = set(previous_responsible)
new_responsible.add(RoleFactory(group__type_id='area',name_id='ad').person)
postdict = dict(responsible=','.join([str(p.pk) for p in new_responsible]))
r = self.client.post(url,postdict)
self.assertEqual(r.status_code, 302)
updated_responsible = bofreq_responsible(doc)
self.assertEqual(new_responsible,set(updated_responsible))
previous_responsible = new_responsible
self.client.logout()
self.assertEqual(len(outbox),1)
self.assertIn('BOF Request responsible leadership changed',outbox[0]['Subject'])
def test_change_responsible_validation(self):
doc = BofreqFactory()
url = urlreverse('ietf.doc.views_bofreq.change_responsible', kwargs=dict(name=doc.name))
login_testing_unauthorized(self,'secretary',url)
bad_batch = PersonFactory.create_batch(3)
good_batch = list()
good_batch.append(RoleFactory(group__type_id='area', name_id='ad').person)
good_batch.append(RoleFactory(group__acronym='iab', name_id='member').person)
pks = set()
pks.update([p.pk for p in good_batch])
pks.update([p.pk for p in bad_batch])
postdict = dict(responsible=','.join([str(pk) for pk in pks]))
r = self.client.post(url,postdict)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
error_text = q('.has-error .alert').text()
for p in good_batch:
self.assertNotIn(p.plain_name(), error_text)
for p in bad_batch:
self.assertIn(p.plain_name(), error_text)
def test_submit(self):
doc = BofreqFactory()
url = urlreverse('ietf.doc.views_bofreq.submit', kwargs=dict(name=doc.name))
@ -219,7 +295,7 @@ This test section has some text.
self.assertEqual(rev, doc.rev)
self.client.logout()
editor = doc.latest_event(BofreqEditorDocEvent).editors.first()
editor = bofreq_editors(doc).first()
for username in ('secretary', 'ad', 'iab-member', editor.user.username):
self.client.login(username=username, password=username+'+password')
r = self.client.get(url)
@ -267,7 +343,7 @@ This test section has some text.
self.assertEqual(bofreq.title, postdict['title'])
self.assertEqual(bofreq.rev, '00')
self.assertEqual(bofreq.get_state_slug(), 'proposed')
self.assertEqual(list(bofreq.latest_event(BofreqEditorDocEvent).editors.all()), [nobody])
self.assertEqual(list(bofreq_editors(bofreq)), [nobody])
self.assertEqual(bofreq.latest_event(NewRevisionDocEvent).rev, '00')
self.assertEqual(bofreq.text_or_error(), 'some stuff')
self.assertEqual(len(outbox),1)

View file

@ -8,5 +8,6 @@ urlpatterns = [
url(r'^state/$', views_bofreq.change_state),
url(r'^submit/$', views_bofreq.submit),
url(r'^title/$', views_bofreq.edit_title),
url(r'^editors/$', views_bofreq.change_editors),
url(r'^editors/$', views_bofreq.change_editors),
url(r'^responsible/$', views_bofreq.change_responsible),
]

12
ietf/doc/utils_bofreq.py Normal file
View file

@ -0,0 +1,12 @@
# Copyright The IETF Trust 2021 All Rights Reserved
from ietf.doc.models import BofreqEditorDocEvent, BofreqResponsibleDocEvent
from ietf.person.models import Person
def bofreq_editors(bofreq):
e = bofreq.latest_event(BofreqEditorDocEvent)
return e.editors.all() if e else Person.objects.none()
def bofreq_responsible(bofreq):
e = bofreq.latest_event(BofreqResponsibleDocEvent)
return e.responsible.all() if e else Person.objects.none()

View file

@ -12,10 +12,12 @@ from django.template.loader import render_to_string
from django.urls import reverse as urlreverse
from ietf.doc.mails import ( email_bofreq_title_changed, email_bofreq_editors_changed,
email_bofreq_new_revision, )
from ietf.doc.models import Document, DocAlias, DocEvent, NewRevisionDocEvent, BofreqEditorDocEvent, State
from ietf.doc.mails import (email_bofreq_title_changed, email_bofreq_editors_changed,
email_bofreq_new_revision, email_bofreq_responsible_changed)
from ietf.doc.models import (Document, DocAlias, DocEvent, NewRevisionDocEvent,
BofreqEditorDocEvent, BofreqResponsibleDocEvent, State)
from ietf.doc.utils import add_state_change_event
from ietf.doc.utils_bofreq import bofreq_editors, bofreq_responsible
from ietf.ietfauth.utils import has_role, role_required
from ietf.person.fields import SearchablePersonsField
from ietf.utils.response import permission_denied
@ -23,13 +25,13 @@ from ietf.utils.text import xslugify
from ietf.utils.textupload import get_cleaned_text_file_content
def bof_requests(request):
reqs = Document.objects.filter(type_id='bofreq')
for req in reqs:
req.latest_revision_event = req.latest_event(NewRevisionDocEvent)
return render(request, 'doc/bofreq/bof_requests.html',dict(reqs=reqs))
def edit_relations(request, name):
raise NotImplementedError
@ -74,7 +76,7 @@ class BofreqUploadForm(forms.Form):
@login_required
def submit(request, name):
bofreq = get_object_or_404(Document, type="bofreq", name=name)
previous_editors = bofreq.latest_event(BofreqEditorDocEvent).editors.all()
previous_editors = bofreq_editors(bofreq)
if not (has_role(request.user,('Secretariat', 'Area Director', 'IAB')) or request.user.person in previous_editors):
permission_denied(request,"You do not have permission to upload a new revision of this BOF Request")
@ -187,7 +189,7 @@ class ChangeEditorsForm(forms.Form):
@login_required
def change_editors(request, name):
bofreq = get_object_or_404(Document, type="bofreq", name=name)
previous_editors = bofreq.latest_event(BofreqEditorDocEvent).editors.all()
previous_editors = bofreq_editors(bofreq)
if not (has_role(request.user,('Secretariat', 'Area Director', 'IAB')) or request.user.person in previous_editors):
permission_denied(request,"You do not have permission to change this document's editors")
@ -214,13 +216,58 @@ def change_editors(request, name):
},
)
class ChangeResponsibleForm(forms.Form):
responsible = SearchablePersonsField(required=False)
def clean_responsible(self):
responsible = self.cleaned_data['responsible']
not_leadership = list()
for person in responsible:
if not has_role(person.user, ('Area Director', 'IAB')):
not_leadership.append(person)
if not_leadership:
raise forms.ValidationError('Only current IAB and IESG members are allowed. Please remove: '+', '.join([person.plain_name() for person in not_leadership]))
return responsible
@login_required
def change_responsible(request,name):
if not has_role(request.user,('Secretariat', 'Area Director', 'IAB')):
permission_denied(request,"You do not have permission to change this document's responsible leadership")
bofreq = get_object_or_404(Document, type="bofreq", name=name)
previous_responsible = bofreq_responsible(bofreq)
if request.method == 'POST':
form = ChangeResponsibleForm(request.POST)
if form.is_valid():
new_responsible = form.cleaned_data['responsible']
if set(new_responsible) != set(previous_responsible):
e = BofreqResponsibleDocEvent(type="changed_responsible", doc=bofreq, rev=bofreq.rev, by=request.user.person)
e.desc = f'Responsible leadership changed to {", ".join([p.name for p in new_responsible])}'
e.save()
e.responsible.set(new_responsible)
bofreq.save_with_history([e])
email_bofreq_responsible_changed(request, bofreq, previous_responsible)
return redirect("ietf.doc.views_doc.document_main", name=bofreq.name)
else:
init = { "responsible" : previous_responsible }
form = ChangeResponsibleForm(initial=init)
titletext = bofreq.get_base_name()
return render(request, 'doc/bofreq/change_responsible.html',
{'form': form,
'doc': bofreq,
'titletext' : titletext,
},
)
class ChangeTitleForm(forms.Form):
title = forms.CharField(max_length=255, label="Title", required=True)
@login_required
def edit_title(request, name):
bofreq = get_object_or_404(Document, type="bofreq", name=name)
editors = bofreq.latest_event(BofreqEditorDocEvent).editors.all()
editors = bofreq_editors(bofreq)
if not (has_role(request.user,('Secretariat', 'Area Director', 'IAB')) or request.user.person in editors):
permission_denied(request, "You do not have permission to edit this document's title")

View file

@ -55,15 +55,15 @@ import debug # pyflakes:ignore
from ietf.doc.models import ( Document, DocAlias, DocHistory, DocEvent, BallotDocEvent, BallotType,
ConsensusDocEvent, NewRevisionDocEvent, TelechatDocEvent, WriteupDocEvent, IanaExpertDocEvent,
IESG_BALLOT_ACTIVE_STATES, STATUSCHANGE_RELATIONS, DocumentActionHolder, DocumentAuthor,
BofreqEditorDocEvent )
IESG_BALLOT_ACTIVE_STATES, STATUSCHANGE_RELATIONS, DocumentActionHolder, DocumentAuthor)
from ietf.doc.utils import (add_links_in_new_revision_events, augment_events_with_revision,
can_adopt_draft, can_unadopt_draft, get_chartering_type, get_tags_for_stream_id,
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
get_initial_notify, make_notify_changed_event, make_rev_history, default_consensus,
add_events_message_info, get_unicode_document_content, build_doc_meta_block,
augment_docs_and_user_with_user_info, irsg_needed_ballot_positions, add_action_holder_change_event,
build_doc_supermeta_block, build_file_urls, update_documentauthors )
build_doc_supermeta_block, build_file_urls, update_documentauthors)
from ietf.doc.utils_bofreq import bofreq_editors, bofreq_responsible
from ietf.group.models import Role, Group
from ietf.group.utils import can_manage_group_type, can_manage_materials, group_features_role_filter
from ietf.ietfauth.utils import ( has_role, is_authorized_in_doc_stream, user_is_person,
@ -529,9 +529,9 @@ def document_main(request, name, rev=None):
))
if doc.type_id == "bofreq":
# content = markdown2.markdown(doc.text_or_error(),extras=['tables'])
content = markdown.markdown(doc.text_or_error(),extensions=['extra'])
editors = doc.latest_event(BofreqEditorDocEvent).editors.all()
editors = bofreq_editors(doc)
responsible = bofreq_responsible(doc)
can_manage = has_role(request.user,['Secretariat', 'Area Director', 'IAB'])
is_editor = request.user.is_authenticated and request.user.person in editors
@ -544,6 +544,7 @@ def document_main(request, name, rev=None):
snapshot=snapshot,
can_manage=can_manage,
editors=editors,
responsible=responsible,
is_editor=is_editor,
))

View file

@ -273,8 +273,6 @@ class GroupPagesTests(TestCase):
def test_group_about(self):
RoleFactory(group=Group.objects.get(acronym='iab'),name_id='member',person=PersonFactory(user__username='iab-member'))
interesting_users = [ 'plain','iana','iab-chair','irtf-chair', 'marschairman', 'teamchairman','ad', 'iab-member', 'secretary', ]
can_edit = {

View file

@ -11,20 +11,27 @@ def forward(apps, schema_editor):
Recipient.objects.create(slug='bofreq_previous_editors',desc='Editors of the prior version of a BOF request',
template='{% for editor in previous_editors %}{{editor.email_address}}{% if not forloop.last %},{% endif %}{% endfor %}')
Recipient.objects.create(slug='bofreq_responsible',desc='BOF request responsible leadership',template='')
Recipient.objects.create(slug='bofreq_previous_responsible',desc='BOF request responsible leadership before change', template='')
mt = MailTrigger.objects.create(slug='bofreq_title_changed',desc='Recipients when the title of a BOF proposal is changed.')
mt.to.set(Recipient.objects.filter(slug__in=['doc_ad', 'bofreq_editors', 'doc_notify']))
mt.to.set(Recipient.objects.filter(slug__in=['bofreq_responsible', 'bofreq_editors', 'doc_notify']))
mt = MailTrigger.objects.create(slug='bofreq_editors_changed',desc='Recipients when the editors of a BOF proposal are changed.')
mt.to.set(Recipient.objects.filter(slug__in=['doc_ad', 'bofreq_editors', 'bofreq_previous_editors', 'doc_notify']))
mt.to.set(Recipient.objects.filter(slug__in=['bofreq_responsible', 'bofreq_editors', 'bofreq_previous_editors', 'doc_notify']))
mt = MailTrigger.objects.create(slug='bofreq_responsible_changed',desc='Recipients when the responsible leadership of a BOF proposal are changed.')
mt.to.set(Recipient.objects.filter(slug__in=['bofreq_responsible', 'bofreq_editors', 'bofreq_previous_responsible', 'doc_notify']))
mt = MailTrigger.objects.create(slug='bofreq_new_revision', desc='Recipients when a new revision of a BOF request is uploaded.')
mt.to.set(Recipient.objects.filter(slug__in=['doc_ad', 'bofreq_editors', 'doc_notify']))
mt.to.set(Recipient.objects.filter(slug__in=['bofreq_responsible', 'bofreq_editors', 'doc_notify']))
def reverse(apps, schema_editor):
MailTrigger = apps.get_model('mailtrigger', 'MailTrigger')
Recipient = apps.get_model('mailtrigger', 'Recipient')
MailTrigger.objects.filter(slug__in=('bofreq_title_changed','bofreq_editors_changed','bofreq_new_revision')).delete()
Recipient.objects.filter(slug__in=('bofreq_editors','bofreq_previous_editors')).delete()
MailTrigger.objects.filter(slug__in=('bofreq_title_changed', 'bofreq_editors_changed', 'bofreq_new_revision', 'bofreq_responsible_changed')).delete()
Recipient.objects.filter(slug__in=('bofreq_editors', 'bofreq_previous_editors')).delete()
Recipient.objects.filter(slug__in=('bofreq_responsible', 'bofreq_previous_responsible')).delete()
class Migration(migrations.Migration):

View file

@ -6,7 +6,8 @@ from django.db import models
from django.template import Template, Context
from email.utils import parseaddr
from ietf.doc.models import BofreqEditorDocEvent
from ietf.doc.utils_bofreq import bofreq_editors, bofreq_responsible
from ietf.utils.mail import formataddr, get_email_addresses_from_text
from ietf.group.models import Group
from ietf.person.models import Email, Alias
@ -398,8 +399,29 @@ class Recipient(models.Model):
addrs = []
if 'doc' in kwargs:
bofreq = kwargs['doc']
editor_event = bofreq.latest_event(BofreqEditorDocEvent)
if editor_event:
addrs.extend([editor.email_address() for editor in editor_event.editors.all()])
editors = bofreq_editors(bofreq)
addrs.extend([editor.email_address() for editor in editors])
return addrs
def gather_bofreq_responsible(self, **kwargs):
addrs = []
if 'doc' in kwargs:
bofreq = kwargs['doc']
responsible = bofreq_responsible(bofreq)
if responsible:
addrs.extend([leader.email_address() for leader in responsible])
else:
addrs.extend(Recipient.objects.get(slug='iab').gather(**{}))
addrs.extend(Recipient.objects.get(slug='iesg').gather(**{}))
return addrs
def gather_bofreq_previous_responsible(self, **kwargs):
addrs = []
previous_responsible = kwargs['previous_responsible']
if previous_responsible:
addrs = [p.email_address() for p in previous_responsible]
else:
addrs.extend(Recipient.objects.get(slug='iab').gather(**{}))
addrs.extend(Recipient.objects.get(slug='iesg').gather(**{}))
return addrs

View file

@ -3535,7 +3535,7 @@
"to": [
"bofreq_editors",
"bofreq_previous_editors",
"doc_ad",
"bofreq_responsible",
"doc_notify"
]
},
@ -3548,20 +3548,34 @@
"desc": "Recipients when a new revision of a BOF request is uploaded.",
"to": [
"bofreq_editors",
"doc_ad",
"bofreq_responsible",
"doc_notify"
]
},
"model": "mailtrigger.mailtrigger",
"pk": "bofreq_new_revision"
},
{
"fields": {
"cc": [],
"desc": "Recipients when the responsible leadership of a BOF proposal are changed.",
"to": [
"bofreq_editors",
"bofreq_previous_responsible",
"bofreq_responsible",
"doc_notify"
]
},
"model": "mailtrigger.mailtrigger",
"pk": "bofreq_responsible_changed"
},
{
"fields": {
"cc": [],
"desc": "Recipients when the title of a BOF proposal is changed.",
"to": [
"bofreq_editors",
"doc_ad",
"bofreq_responsible",
"doc_notify"
]
},
@ -5237,6 +5251,22 @@
"model": "mailtrigger.recipient",
"pk": "bofreq_previous_editors"
},
{
"fields": {
"desc": "BOF request responsible leadership before change",
"template": ""
},
"model": "mailtrigger.recipient",
"pk": "bofreq_previous_responsible"
},
{
"fields": {
"desc": "BOF request responsible leadership",
"template": ""
},
"model": "mailtrigger.recipient",
"pk": "bofreq_responsible"
},
{
"fields": {
"desc": "The person providing a comment to nomcom",

View file

@ -7,26 +7,35 @@
{% block content %}
{% origin %}
{% regroup reqs by get_state_slug as grouped_reqs %}
{% for req_group in grouped_reqs %}
<div class="panel panel-default">
<div class="panel-heading">{{req_group.grouper|capfirst}} BOF Requests</div>
<div class="panel-body">
<table id="bofreqs-{{req_group.grouper}}" class="table table-condensed table-striped tablesorter">
<thead>
<tr><th class="col-sm-4">Name</th><th class="col-sm-1">Date</th><th>Title</th></tr>
</thead>
<tbody>
{% for req in req_group.list %}
<tr>
<td><a href={% url 'ietf.doc.views_doc.document_main' name=req.name %}>{{req.name}}-{{req.rev}}</a></td>
<td>{{req.latest_revision_event.time|date:"Y-m-d"}}</td>
<td>{{req.title}}</td>
</tr>
{% endfor %}
</tbody>
</table>
<h1>BOF Requests</h1>
{% if not reqs %}
<p>There are currently no BoF Requests</p>
{% else %}
{% regroup reqs by get_state_slug as grouped_reqs %}
{% for req_group in grouped_reqs %}
<div class="panel panel-default">
<div class="panel-heading">{{req_group.grouper|capfirst}} BOF Requests</div>
<div class="panel-body">
<table id="bofreqs-{{req_group.grouper}}" class="table table-condensed table-striped tablesorter">
<thead>
<tr><th class="col-sm-4">Name</th><th class="col-sm-1">Date</th><th>Title</th></tr>
</thead>
<tbody>
{% for req in req_group.list %}
<tr>
<td><a href={% url 'ietf.doc.views_doc.document_main' name=req.name %}>{{req.name}}-{{req.rev}}</a></td>
<td>{{req.latest_revision_event.time|date:"Y-m-d"}}</td>
<td>{{req.title}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endfor %}
{% endfor %}
{% endif %}
{% if request.user.is_authenticated %}
<a id="start_button" class="btn btn-primary" href="{% url 'ietf.doc.views_bofreq.new_bof_request' %}">Start New Bof Request</a>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,30 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2021, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% block title %}Change responsible leadership for {{doc.name}}{% endblock %}
{% block pagehead %}
{{ form.media.css}}
{% endblock %}
{% block content %}
{% origin %}
<h1>Change Responsible Leadership<br><small>{{ titletext }}</small></h1>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">Submit</button>
<a class="btn btn-default pull-right" href="{% url "ietf.doc.views_doc.document_main" name=doc.canonical_name %}">Back</a>
{% endbuttons %}
</form>
{% endblock %}
{% block js %}
{{ form.media.js }}
{% endblock %}

View file

@ -87,7 +87,7 @@
<th>Editor{{editors|pluralize}}</th>
<td class="edit">
{% if not snapshot %}
{% if is_editor or can_manage %}
{% if is_editor or can_manage %}
{% doc_edit_button 'ietf.doc.views_bofreq.change_editors' name=doc.name %}
{% endif %}
{% endif %}
@ -99,6 +99,22 @@
</td>
</tr>
<tr id="responsible">
<td></td>
<th>Responsible Leadership</th>
<td class="edit">
{% if not snapshot %}
{% if can_manage %}
{% doc_edit_button 'ietf.doc.views_bofreq.change_responsible' name=doc.name %}
{% endif %}
{% endif %}
</td>
<td>
{% for leader in responsible %}
{% person_link leader %}{% if not forloop.last %}, {% endif %}
{% endfor %}
</td>
</tbody>
<tbody class="meta">

View file

@ -0,0 +1,6 @@
{% autoescape off %}{{request.user.person.name}} has changed the responsible leadership for {{bofreq.name}}-{{bofreq.rev}}
The previous leaders were : {{previous_responsible|join:", "}}
The new leaders are : {{responsible|join:", "}}
{% endautoescape %}