Added the notion of responsible leadership.
- Legacy-Id: 19202
This commit is contained in:
parent
338da98661
commit
f5a04263e5
|
@ -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', ]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
36
ietf/doc/migrations/0043_bofreq_docevents.py
Normal file
36
ietf/doc/migrations/0043_bofreq_docevents.py
Normal 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',),
|
||||
),
|
||||
]
|
|
@ -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',),
|
||||
),
|
||||
]
|
|
@ -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):
|
||||
""" 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)
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
for editor in previous_editors:
|
||||
unescaped = unicontent(r).encode('utf-8').decode('unicode-escape')
|
||||
for editor in previous_editors:
|
||||
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)
|
||||
|
|
|
@ -9,4 +9,5 @@ urlpatterns = [
|
|||
url(r'^submit/$', views_bofreq.submit),
|
||||
url(r'^title/$', views_bofreq.edit_title),
|
||||
url(r'^editors/$', views_bofreq.change_editors),
|
||||
url(r'^responsible/$', views_bofreq.change_responsible),
|
||||
]
|
12
ietf/doc/utils_bofreq.py
Normal file
12
ietf/doc/utils_bofreq.py
Normal 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()
|
|
@ -13,9 +13,11 @@ 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
|
||||
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")
|
||||
|
||||
|
|
|
@ -55,8 +55,7 @@ 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,
|
||||
|
@ -64,6 +63,7 @@ from ietf.doc.utils import (add_links_in_new_revision_events, augment_events_wit
|
|||
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)
|
||||
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,
|
||||
))
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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()
|
||||
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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
{% block content %}
|
||||
{% origin %}
|
||||
|
||||
<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">
|
||||
|
@ -29,4 +34,8 @@
|
|||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
30
ietf/templates/doc/bofreq/change_responsible.html
Normal file
30
ietf/templates/doc/bofreq/change_responsible.html
Normal 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 %}
|
|
@ -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">
|
||||
|
|
6
ietf/templates/doc/mail/bofreq_responsible_changed.txt
Normal file
6
ietf/templates/doc/mail/bofreq_responsible_changed.txt
Normal 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 %}
|
Loading…
Reference in a new issue