From 6b383255adb4345ff1717aaef0371f05715445bb Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 6 Jul 2021 18:05:54 +0000 Subject: [PATCH 01/12] Staging for merge forward - Legacy-Id: 19199 --- ietf/doc/admin.py | 9 +- ietf/doc/factories.py | 41 ++- ietf/doc/mails.py | 29 +- ietf/doc/migrations/0042_bofreq_states.py | 36 +++ .../migrations/0043_bofreqeditordocevent.py | 23 ++ ietf/doc/models.py | 10 + ietf/doc/tests_bofreq.py | 285 +++++++++++++++++ ietf/doc/urls.py | 5 +- ietf/doc/urls_bofreq.py | 12 + ietf/doc/views_bofreq.py | 296 ++++++++++++++++++ ietf/doc/views_doc.py | 23 +- ietf/doc/views_help.py | 1 + .../migrations/0023_bofreq_triggers.py | 38 +++ ietf/mailtrigger/models.py | 11 + ietf/name/fixtures/names.json | 182 ++++++++++- ietf/name/migrations/0024_add_bofrequest.py | 23 ++ ietf/settings.py | 2 + ietf/static/ietf/js/upload_bofreq.js | 26 ++ ietf/templates/base/menu.html | 1 + ietf/templates/doc/bofreq/bof_requests.html | 32 ++ ietf/templates/doc/bofreq/bofreq_template.md | 28 ++ ietf/templates/doc/bofreq/change_editors.html | 30 ++ ietf/templates/doc/bofreq/new_bofreq.html | 28 ++ ietf/templates/doc/bofreq/upload_content.html | 28 ++ ietf/templates/doc/document_bofreq.html | 137 ++++++++ .../doc/mail/bofreq_editors_changed.txt | 6 + .../doc/mail/bofreq_new_revision.txt | 3 + .../doc/mail/bofreq_title_changed.txt | 3 + ietf/utils/test_utils.py | 1 - requirements.txt | 1 + 30 files changed, 1329 insertions(+), 21 deletions(-) create mode 100644 ietf/doc/migrations/0042_bofreq_states.py create mode 100644 ietf/doc/migrations/0043_bofreqeditordocevent.py create mode 100644 ietf/doc/tests_bofreq.py create mode 100644 ietf/doc/urls_bofreq.py create mode 100644 ietf/doc/views_bofreq.py create mode 100644 ietf/mailtrigger/migrations/0023_bofreq_triggers.py create mode 100644 ietf/name/migrations/0024_add_bofrequest.py create mode 100644 ietf/static/ietf/js/upload_bofreq.js create mode 100644 ietf/templates/doc/bofreq/bof_requests.html create mode 100644 ietf/templates/doc/bofreq/bofreq_template.md create mode 100644 ietf/templates/doc/bofreq/change_editors.html create mode 100644 ietf/templates/doc/bofreq/new_bofreq.html create mode 100644 ietf/templates/doc/bofreq/upload_content.html create mode 100644 ietf/templates/doc/document_bofreq.html create mode 100644 ietf/templates/doc/mail/bofreq_editors_changed.txt create mode 100644 ietf/templates/doc/mail/bofreq_new_revision.txt create mode 100644 ietf/templates/doc/mail/bofreq_title_changed.txt diff --git a/ietf/doc/admin.py b/ietf/doc/admin.py index b10b061b2..6f4f30143 100644 --- a/ietf/doc/admin.py +++ b/ietf/doc/admin.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2010-2020, All Rights Reserved +# Copyright The IETF Trust 2010-2021, All Rights Reserved # -*- coding: utf-8 -*- @@ -11,7 +11,8 @@ from .models import (StateType, State, RelatedDocument, DocumentAuthor, Document StateDocEvent, ConsensusDocEvent, BallotType, BallotDocEvent, WriteupDocEvent, LastCallDocEvent, TelechatDocEvent, BallotPositionDocEvent, ReviewRequestDocEvent, InitialReviewDocEvent, AddedMessageEvent, SubmissionDocEvent, DeletedEvent, EditedAuthorsDocEvent, DocumentURL, - ReviewAssignmentDocEvent, IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource, DocumentActionHolder ) + ReviewAssignmentDocEvent, IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource, DocumentActionHolder, + BofreqEditorDocEvent ) from ietf.utils.validators import validate_external_resource_value @@ -192,6 +193,10 @@ admin.site.register(BallotPositionDocEvent, BallotPositionDocEventAdmin) class IRSGBallotDocEventAdmin(DocEventAdmin): raw_id_fields = ["doc", "by"] admin.site.register(IRSGBallotDocEvent, IRSGBallotDocEventAdmin) + +class BofreqEditorDocEventAdmin(DocEventAdmin): + raw_id_fields = ["doc", "by", "editors" ] +admin.site.register(BofreqEditorDocEvent, BofreqEditorDocEventAdmin) class DocumentUrlAdmin(admin.ModelAdmin): list_display = ['id', 'doc', 'tag', 'url', 'desc', ] diff --git a/ietf/doc/factories.py b/ietf/doc/factories.py index 33954548e..e37e8119d 100644 --- a/ietf/doc/factories.py +++ b/ietf/doc/factories.py @@ -13,8 +13,11 @@ from django.conf import settings from ietf.doc.models import ( Document, DocEvent, NewRevisionDocEvent, DocAlias, State, DocumentAuthor, StateDocEvent, BallotPositionDocEvent, BallotDocEvent, BallotType, IRSGBallotDocEvent, TelechatDocEvent, - DocumentActionHolder) + DocumentActionHolder, BofreqEditorDocEvent ) from ietf.group.models import Group +from ietf.person.factories import PersonFactory +from ietf.utils.text import xslugify + def draft_name_generator(type_id,group,n): return '%s-%s-%s-%s%d'%( @@ -379,3 +382,39 @@ class DocumentAuthorFactory(factory.DjangoModelFactory): class WgDocumentAuthorFactory(DocumentAuthorFactory): document = factory.SubFactory(WgDraftFactory) + +class BofreqEditorDocEventFactory(DocEventFactory): + class Meta: + model = BofreqEditorDocEvent + + type = "changed_editors" + doc = factory.SubFactory('ietf.doc.factories.BofreqFactory') + + + @factory.post_generation + def editors(obj, create, extracted, **kwargs): + if not create: + return + if extracted: + obj.editors.set(extracted) + else: + obj.editors.set(PersonFactory.create_batch(3)) + obj.desc = f'Changed editors to {", ".join(obj.editors.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') + + @factory.post_generation + def states(obj, create, extracted, **kwargs): + if not create: + return + if extracted: + for (state_type_id,state_slug) in extracted: + obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug)) + else: + obj.set_state(State.objects.get(type_id='bofreq',slug='proposed')) + diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index d86b88597..fc9a2bd26 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -18,7 +18,7 @@ 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 +from ietf.doc.models import WriteupDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent, BofreqEditorDocEvent from ietf.doc.utils import needed_ballot_positions from ietf.group.models import Role from ietf.doc.models import Document @@ -689,3 +689,30 @@ def send_external_resource_change_request(request, doc, submitter_info, requeste doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), ), cc=list(cc),) + +def email_bofreq_title_changed(request, bofreq): + addrs = gather_address_lists('bofreq_title_changed', doc=bofreq) + + send_mail(request, addrs.to, settings.DEFAULT_FROM_EMAIL, + f'BOF Request title changed : {bofreq.name}', + 'doc/mail/bofreq_title_changed.txt', + dict(bofreq=bofreq, request=request), + cc=addrs.cc) + +def email_bofreq_editors_changed(request, bofreq, previous_editors): + editors = bofreq.latest_event(BofreqEditorDocEvent).editors.all() + 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), + cc=addrs.cc) + +def email_bofreq_new_revision(request, bofreq): + addrs = gather_address_lists('bofreq_new_revision', doc=bofreq) + send_mail(request, addrs.to, settings.DEFAULT_FROM_EMAIL, + f'New BOF request revision uploaded: {bofreq.name}-{bofreq.rev}', + 'doc/mail/bofreq_new_revision.txt', + dict(bofreq=bofreq, request=request ), + cc=addrs.cc) diff --git a/ietf/doc/migrations/0042_bofreq_states.py b/ietf/doc/migrations/0042_bofreq_states.py new file mode 100644 index 000000000..0b13e95cc --- /dev/null +++ b/ietf/doc/migrations/0042_bofreq_states.py @@ -0,0 +1,36 @@ +# Copyright The IETF Trust 2021 All Rights Reserved + +# Generated by Django 2.2.23 on 2021-05-21 13:29 + +from django.db import migrations + +def forward(apps, schema_editor): + StateType = apps.get_model('doc', 'StateType') + State = apps.get_model('doc', 'State') + + StateType.objects.create(slug='bofreq', label='Bof Request State') + proposed = State.objects.create(type_id='bofreq', slug='proposed', name='Proposed', used=True, desc='The bof request is proposed', order=0) + approved = State.objects.create(type_id='bofreq', slug='approved', name='Approved', used=True, desc='The bof request is approved', order=1) + declined = State.objects.create(type_id='bofreq', slug='declined', name='Declined', used=True, desc='The bof request is declined', order=2) + replaced = State.objects.create(type_id='bofreq', slug='replaced', name='Replaced', used=True, desc='The bof request is proposed', order=3) + abandoned = State.objects.create(type_id='bofreq', slug='abandoned', name='Abandoned', used=True, desc='The bof request is abandoned', order=4) + + proposed.next_states.set([approved,declined,replaced,abandoned]) + +def reverse(apps, schema_editor): + StateType = apps.get_model('doc', 'StateType') + State = apps.get_model('doc', 'State') + State.objects.filter(type_id='bofreq').delete() + StateType.objects.filter(slug='bofreq').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('doc', '0041_add_documentactionholder'), + ('name', '0024_add_bofrequest'), + ] + + operations = [ + migrations.RunPython(forward, reverse) + ] diff --git a/ietf/doc/migrations/0043_bofreqeditordocevent.py b/ietf/doc/migrations/0043_bofreqeditordocevent.py new file mode 100644 index 000000000..e6c3b433a --- /dev/null +++ b/ietf/doc/migrations/0043_bofreqeditordocevent.py @@ -0,0 +1,23 @@ +# 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',), + ), + ] diff --git a/ietf/doc/models.py b/ietf/doc/models.py index ba49b0741..cab807c4d 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -142,6 +142,8 @@ class DocumentInfo(models.Model): self._cached_file_path = settings.CONFLICT_REVIEW_PATH elif self.type_id == "statchg": self._cached_file_path = settings.STATUS_CHANGE_PATH + elif self.type_id == "bofreq": + self._cached_file_path = settings.BOFREQ_PATH else: self._cached_file_path = settings.DOCUMENT_PATH_PATTERN.format(doc=self) return self._cached_file_path @@ -163,6 +165,8 @@ class DocumentInfo(models.Model): elif self.type_id == 'review': # TODO: This will be wrong if a review is updated on the same day it was created (or updated more than once on the same day) self._cached_base_name = "%s.txt" % self.name + elif self.type_id == 'bofreq': + self._cached_base_name = "%s-%s.md" % (self.name, self.rev) else: if self.rev: self._cached_base_name = "%s-%s.txt" % (self.canonical_name(), self.rev) @@ -1145,6 +1149,9 @@ EVENT_TYPES = [ # IPR events ("posted_related_ipr", "Posted related IPR"), ("removed_related_ipr", "Removed related IPR"), + + # Bofreq Editor events + ("changed_editors", "Changed BOF Request editors") ] class DocEvent(models.Model): @@ -1340,3 +1347,6 @@ class EditedAuthorsDocEvent(DocEvent): Example 'basis' values might be from ['manually adjusted','recomputed by parsing document', etc.] """ 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) \ No newline at end of file diff --git a/ietf/doc/tests_bofreq.py b/ietf/doc/tests_bofreq.py new file mode 100644 index 000000000..c6910e0d2 --- /dev/null +++ b/ietf/doc/tests_bofreq.py @@ -0,0 +1,285 @@ +# Copyright The IETF Trust 2021 All Rights Reserved + +import debug # pyflakes:ignore +import io +import shutil +import os + +from pyquery import PyQuery +from tempfile import NamedTemporaryFile + +from django.conf import settings +from django.urls import reverse as urlreverse + +from ietf.doc.factories import BofreqFactory, NewRevisionDocEventFactory +from ietf.doc.models import State, BofreqEditorDocEvent, Document, DocAlias, NewRevisionDocEvent +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): + self.bofreq_dir = self.tempdir('bofreq') + self.saved_bofreq_path = settings.BOFREQ_PATH + settings.BOFREQ_PATH = self.bofreq_dir + + def tearDown(self): + settings.BOFREQ_PATH = self.saved_bofreq_path + shutil.rmtree(self.bofreq_dir) + + def write_bofreq_file(self, bofreq): + fname = os.path.join(self.bofreq_dir, "%s-%s.md" % (bofreq.canonical_name(), bofreq.rev)) + with io.open(fname, "w") as f: + f.write(f"""# This is a test bofreq. +Version: {bofreq.rev} + +## A section + +This test section has some text. +""") + + def test_show_bof_requests(self): + 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) + self.assertEqual(len(q('#bofreqs-proposed tbody tr')), states.count()) + for i in range(states.count()): + reqs[i].set_state(states[i]) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + for state in states: + self.assertEqual(len(q(f'#bofreqs-{state.slug} tbody tr')), 1) + + + def test_bofreq_main_page(self): + doc = BofreqFactory() + doc.save_with_history(doc.docevent_set.all()) + self.write_bofreq_file(doc) + nr_event = NewRevisionDocEventFactory(doc=doc,rev='01') + doc.rev='01' + doc.save_with_history([nr_event]) + self.write_bofreq_file(doc) + editors = doc.latest_event(BofreqEditorDocEvent).editors.all() + 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) + q = PyQuery(r.content) + self.assertEqual(0, len(q('td.edit>a.btn'))) + self.assertEqual([],q('#change-request')) + editor_row = q('#editors').html() + for editor in editors: + self.assertInHTML(editor.name,editor_row) + for user in ('secretary','ad'): + 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.client.logout() + self.assertNotEqual([],q('#change-request')) + editor = editors.first().user.username + self.client.login(username=editor, password=editor+"+password") + r = self.client.get(url) + self.assertEqual(r.status_code,200) + q = PyQuery(r.content) + self.assertEqual(2, len(q('td.edit>a.btn'))) + self.assertNotEqual([],q('#change-request')) + self.client.logout() + url = urlreverse('ietf.doc.views_doc.document_main', kwargs=dict(name=doc,rev='00')) + r = self.client.get(url) + 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() + 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')) + self.assertEqual(r.status_code, 302) + doc = reload_db_objects(doc) + self.assertEqual(title, doc.title) + nobody = PersonFactory() + self.client.login(username=nobody.user.username,password=nobody.user.username+'+password') + r = self.client.post(url,dict(title='New title')) + self.assertEqual(r.status_code, 403) + doc = reload_db_objects(doc) + self.assertEqual(title, doc.title) + self.client.logout() + for username in ('secretary', 'ad', editor.user.username): + self.client.login(username=username, password=username+'+password') + r = self.client.get(url) + self.assertEqual(r.status_code,200) + docevent_count = doc.docevent_set.count() + empty_outbox() + r = self.client.post(url,dict(title=username)) + self.assertEqual(r.status_code,302) + doc = reload_db_objects(doc) + self.assertEqual(doc.title, username) + self.assertEqual(docevent_count+1, doc.docevent_set.count()) + self.assertEqual(1, len(outbox)) + self.client.logout() + + def state_pk_as_str(self, type_id, slug): + return str(State.objects.get(type_id=type_id, slug=slug).pk) + + def test_edit_state(self): + doc = BofreqFactory() + editor = doc.latest_event(BofreqEditorDocEvent).editors.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'))) + self.assertEqual(r.status_code, 302) + doc = reload_db_objects(doc) + self.assertEqual(state, doc.get_state('bofreq')) + self.client.login(username=editor.user.username,password=editor.user.username+'+password') + r = self.client.post(url, dict(new_state=self.state_pk_as_str('bofreq','approved'))) + self.assertEqual(r.status_code, 403) + doc = reload_db_objects(doc) + self.assertEqual(state,doc.get_state('bofreq')) + self.client.logout() + for username in ('secretary','ad'): + self.client.login(username=username,password=username+'+password') + r = self.client.get(url) + self.assertEqual(r.status_code,200) + docevent_count = doc.docevent_set.count() + r = self.client.post(url,dict(new_state=self.state_pk_as_str('bofreq','approved' if username=='secretary' else 'declined'),comment=f'{username}-2309hnf')) + self.assertEqual(r.status_code,302) + doc = reload_db_objects(doc) + self.assertEqual('approved' if username=='secretary' else 'declined',doc.get_state_slug('bofreq')) + self.assertEqual(docevent_count+2, doc.docevent_set.count()) + self.assertIn(f'{username}-2309hnf',doc.latest_event(type='added_comment').desc) + self.client.logout() + + def test_change_editors(self): + doc = BofreqFactory() + previous_editors = list(doc.latest_event(BofreqEditorDocEvent).editors.all()) + acting_editor = previous_editors[0] + new_editors = set(previous_editors) + new_editors.discard(acting_editor) + new_editors.add(PersonFactory()) + url = urlreverse('ietf.doc.views_bofreq.change_editors', kwargs=dict(name=doc.name)) + 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() + 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() + self.assertEqual(set(previous_editors),set(editors)) + self.client.logout() + for username in (previous_editors[0].user.username, 'secretary', 'ad'): + empty_outbox() + 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') + self.assertIn(editor.name,unescaped) + new_editors = set(previous_editors) + new_editors.discard(acting_editor) + new_editors.add(PersonFactory()) + 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() + 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_submit(self): + doc = BofreqFactory() + url = urlreverse('ietf.doc.views_bofreq.submit', kwargs=dict(name=doc.name)) + + rev = doc.rev + r = self.client.post(url,{'bofreq_submission':'enter','bofreq_content':'# oiwefrase'}) + self.assertEqual(r.status_code, 302) + doc = reload_db_objects(doc) + self.assertEqual(rev, doc.rev) + + nobody = PersonFactory() + self.client.login(username=nobody.user.username, password=nobody.user.username+'+password') + r = self.client.post(url,{'bofreq_submission':'enter','bofreq_content':'# oiwefrase'}) + self.assertEqual(r.status_code, 403) + doc = reload_db_objects(doc) + self.assertEqual(rev, doc.rev) + self.client.logout() + + editor = doc.latest_event(BofreqEditorDocEvent).editors.first() + for username in ('secretary', 'ad', editor.user.username): + self.client.login(username=username, password=username+'+password') + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + file = NamedTemporaryFile(delete=False,mode="w+",encoding='utf-8') + file.write(f'# {username}') + file.close() + for postdict in [ + {'bofreq_submission':'enter','bofreq_content':f'# {username}'}, + {'bofreq_submission':'upload','bofreq_file':open(file.name,'rb')}, + ]: + docevent_count = doc.docevent_set.count() + empty_outbox() + r = self.client.post(url, postdict) + self.assertEqual(r.status_code, 302) + doc = reload_db_objects(doc) + self.assertEqual('%02d'%(int(rev)+1) ,doc.rev) + self.assertEqual(f'# {username}', doc.text()) + self.assertEqual(docevent_count+1, doc.docevent_set.count()) + self.assertEqual(1, len(outbox)) + rev = doc.rev + self.client.logout() + os.unlink(file.name) + + def test_start_new_bofreq(self): + url = urlreverse('ietf.doc.views_bofreq.new_bof_request') + nobody = PersonFactory() + login_testing_unauthorized(self,nobody.user.username,url) + r = self.client.get(url) + self.assertContains(r,'Fill in the details below. Keep items in the order they appear here.',status_code=200) + file = NamedTemporaryFile(delete=False,mode="w+",encoding='utf-8') + file.write('some stuff') + file.close() + for postdict in [ + dict(title='title one', bofreq_submission='enter', bofreq_content='some stuff'), + dict(title='title two', bofreq_submission='upload', bofreq_file=open(file.name,'rb')), + ]: + empty_outbox() + r = self.client.post(url, postdict) + self.assertEqual(r.status_code,302) + name = f"bofreq-{postdict['title']}".replace(' ','-') + bofreq = Document.objects.filter(name=name,type_id='bofreq').first() + self.assertIsNotNone(bofreq) + self.assertIsNotNone(DocAlias.objects.filter(name=name).first()) + 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(bofreq.latest_event(NewRevisionDocEvent).rev, '00') + self.assertEqual(bofreq.text_or_error(), 'some stuff') + self.assertEqual(len(outbox),1) + os.unlink(file.name) + existing_bofreq = BofreqFactory() + for postdict in [ + dict(title='', bofreq_submission='enter', bofreq_content='some stuff'), + dict(title='a title', bofreq_submission='enter', bofreq_content=''), + dict(title=existing_bofreq.title, bofreq_submission='enter', bofreq_content='some stuff'), + dict(title='森川', bofreq_submission='enter', bofreq_content='some stuff'), + dict(title='a title', bofreq_submission='', bofreq_content='some stuff'), + ]: + r = self.client.post(url,postdict) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(q('form div.has-error')) diff --git a/ietf/doc/urls.py b/ietf/doc/urls.py index fc4e0d959..6149e5684 100644 --- a/ietf/doc/urls.py +++ b/ietf/doc/urls.py @@ -37,7 +37,7 @@ from django.conf.urls import include from django.views.generic import RedirectView from django.conf import settings -from ietf.doc import views_search, views_draft, views_ballot, views_status_change, views_doc, views_downref, views_stats, views_help +from ietf.doc import views_search, views_draft, views_ballot, views_status_change, views_doc, views_downref, views_stats, views_help, views_bofreq from ietf.utils.urls import url session_patterns = [ @@ -54,6 +54,8 @@ urlpatterns = [ url(r'^ad2/(?P[\w.-]+)/$', RedirectView.as_view(url='/doc/ad/%(name)s/', permanent=True)), url(r'^rfc-status-changes/?$', views_status_change.rfc_status_changes), url(r'^start-rfc-status-change/(?:%(name)s/)?$' % settings.URL_REGEXPS, views_status_change.start_rfc_status_change), + url(r'^bof-requests/?$', views_bofreq.bof_requests), + url(r'^bof-requests/new/$', views_bofreq.new_bof_request), url(r'^iesg/?$', views_search.drafts_in_iesg_process), url(r'^email-aliases/?$', views_doc.email_aliases), url(r'^downref/?$', views_downref.downref_registry), @@ -144,6 +146,7 @@ urlpatterns = [ url(r'^%(name)s/meetings/?$' % settings.URL_REGEXPS, views_doc.all_presentations), url(r'^%(charter)s/' % settings.URL_REGEXPS, include('ietf.doc.urls_charter')), + url(r'^%(bofreq)s/' % settings.URL_REGEXPS, include('ietf.doc.urls_bofreq')), url(r'^%(name)s/conflict-review/' % settings.URL_REGEXPS, include('ietf.doc.urls_conflict_review')), url(r'^%(name)s/status-change/' % settings.URL_REGEXPS, include('ietf.doc.urls_status_change')), url(r'^%(name)s/material/' % settings.URL_REGEXPS, include('ietf.doc.urls_material')), diff --git a/ietf/doc/urls_bofreq.py b/ietf/doc/urls_bofreq.py new file mode 100644 index 000000000..bbc70ce78 --- /dev/null +++ b/ietf/doc/urls_bofreq.py @@ -0,0 +1,12 @@ + +from ietf.doc import views_bofreq, views_doc +from ietf.utils.urls import url + +urlpatterns = [ + url(r'^notices/$', views_doc.edit_notify, name='ietf.doc.views_doc.edit_notify;bofreq'), + url(r'^relations/$', views_bofreq.edit_relations), + 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), +] \ No newline at end of file diff --git a/ietf/doc/views_bofreq.py b/ietf/doc/views_bofreq.py new file mode 100644 index 000000000..ec764a43f --- /dev/null +++ b/ietf/doc/views_bofreq.py @@ -0,0 +1,296 @@ +# Copyright The IETF Trust 2021 All Rights Reserved + +import debug # pyflakes:ignore + +import io +import markdown + +from django import forms +from django.contrib.auth.decorators import login_required +from django.shortcuts import get_object_or_404, redirect, render +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.utils import add_state_change_event +from ietf.ietfauth.utils import has_role, role_required +from ietf.person.fields import SearchablePersonsField +from ietf.utils.response import permission_denied +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 + + +class BofreqUploadForm(forms.Form): + ACTIONS = [ + ("enter", "Enter content directly"), + ("upload", "Upload content from file"), + ] + bofreq_submission = forms.ChoiceField(choices=ACTIONS, widget=forms.RadioSelect) + bofreq_file = forms.FileField(label="Markdown source file to upload", required=False) + bofreq_content = forms.CharField(widget=forms.Textarea(attrs={'rows':30}), required=False, strip=False) + + def clean_bofreq_content(self): + content = self.cleaned_data["bofreq_content"].replace("\r", "") + try: + _ = markdown.markdown(content, extensions=['extra']) + except Exception as e: + raise forms.ValidationError(f'Markdown processing failed: {e}') + return content + + def clean_bofreq_file(self): + content = get_cleaned_text_file_content(self.cleaned_data["bofreq_file"]) + try: + _ = markdown.markdown(content, extensions=['extra']) + except Exception as e: + raise forms.ValidationError(f'Markdown processing failed: {e}') + return content + + def clean(self): + def require_field(f): + if not self.cleaned_data.get(f): + self.add_error(f, forms.ValidationError("You must fill in this field.")) + + submission_method = self.cleaned_data.get("bofreq_submission") + if submission_method == "enter": + require_field("bofreq_content") + elif submission_method == "upload": + require_field("bofreq_file") + + +@login_required +def submit(request, name): + bofreq = get_object_or_404(Document, type="bofreq", name=name) + previous_editors = bofreq.latest_event(BofreqEditorDocEvent).editors.all() + if not (has_role(request.user,('Secretariat','Area Director')) or request.user.person in previous_editors): + permission_denied(request,"You do not have permission to upload a new revision of this BOF Request") + + if request.method == 'POST': + form = BofreqUploadForm(request.POST, request.FILES) + if form.is_valid(): + bofreq.rev = "%02d" % (int(bofreq.rev)+1) + e = NewRevisionDocEvent.objects.create( + type="new_revision", + doc=bofreq, + by=request.user.person, + rev=bofreq.rev, + desc='New revision available', + time=bofreq.time, + ) + bofreq.save_with_history([e]) + bofreq_submission = form.cleaned_data['bofreq_submission'] + if bofreq_submission == "upload": + content = form.cleaned_data['bofreq_file'] + else: + content = form.cleaned_data['bofreq_content'] + with io.open(bofreq.get_file_name(), 'w', encoding='utf-8') as destination: + destination.write(content) + email_bofreq_new_revision(request, bofreq) + return redirect('ietf.doc.views_doc.document_main', name=bofreq.name) + + else: + init = {'bofreq_content':bofreq.text_or_error(), + 'bofreq_submission':'enter', + } + form = BofreqUploadForm(initial=init) + return render(request, 'doc/bofreq/upload_content.html', + {'form':form,'doc':bofreq}) + +class NewBofreqForm(BofreqUploadForm): + title = forms.CharField(max_length=255) + field_order = ['title','bofreq_submission','bofreq_file','bofreq_content'] + + def name_from_title(self,title): + name = 'bofreq-' + xslugify(title).replace('_', '-')[:128] + return name + + def clean_title(self): + title = self.cleaned_data['title'] + name = self.name_from_title(title) + if name == 'bofreq-': + raise forms.ValidationError('The filename derived from this title is empty. Please include a few descriptive words using ascii or numeric characters') + if Document.objects.filter(name=name).exists(): + raise forms.ValidationError('This title produces a filename already used by an existing BoF request') + return title + +@login_required +def new_bof_request(request): + + if request.method == 'POST': + form = NewBofreqForm(request.POST, request.FILES) + if form.is_valid(): + title = form.cleaned_data['title'] + name = form.name_from_title(title) + bofreq = Document.objects.create( + type_id='bofreq', + name = name, + title = title, + abstract = '', + rev = '00', + ) + bofreq.set_state(State.objects.get(type_id='bofreq',slug='proposed')) + e1 = NewRevisionDocEvent.objects.create( + type="new_revision", + doc=bofreq, + by=request.user.person, + rev=bofreq.rev, + desc='New revision available', + time=bofreq.time, + ) + e2 = BofreqEditorDocEvent.objects.create( + type="changed_editors", + doc=bofreq, + rev=bofreq.rev, + by=request.user.person, + desc= f'Editors changed to {request.user.person.name}', + ) + e2.editors.set([request.user.person]) + bofreq.save_with_history([e1,e2]) + alias = DocAlias.objects.create(name=name) + alias.docs.set([bofreq]) + bofreq_submission = form.cleaned_data['bofreq_submission'] + if bofreq_submission == "upload": + content = form.cleaned_data['bofreq_file'] + else: + content = form.cleaned_data['bofreq_content'] + with io.open(bofreq.get_file_name(), 'w', encoding='utf-8') as destination: + destination.write(content) + email_bofreq_new_revision(request, bofreq) + return redirect('ietf.doc.views_doc.document_main', name=bofreq.name) + + else: + init = {'bofreq_content':render_to_string('doc/bofreq/bofreq_template.md',{}), + 'bofreq_submission':'enter', + } + form = NewBofreqForm(initial=init) + return render(request, 'doc/bofreq/new_bofreq.html', + {'form':form}) + + +class ChangeEditorsForm(forms.Form): + editors = SearchablePersonsField(required=False) + + +@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() + if not (has_role(request.user,('Secretariat','Area Director')) or request.user.person in previous_editors): + permission_denied(request,"You do not have permission to change this document's editors") + + if request.method == 'POST': + form = ChangeEditorsForm(request.POST) + if form.is_valid(): + new_editors = form.cleaned_data['editors'] + if set(new_editors) != set(previous_editors): + e = BofreqEditorDocEvent(type="changed_editors", doc=bofreq, rev=bofreq.rev, by=request.user.person) + e.desc = f'Editors changed to {", ".join([p.name for p in new_editors])}' + e.save() + e.editors.set(new_editors) + bofreq.save_with_history([e]) + email_bofreq_editors_changed(request, bofreq, previous_editors) + return redirect("ietf.doc.views_doc.document_main", name=bofreq.name) + else: + init = { "editors" : previous_editors } + form = ChangeEditorsForm(initial=init) + titletext = bofreq.get_base_name() + return render(request, 'doc/bofreq/change_editors.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() + if not (has_role(request.user,('Secretariat','Area Director')) or request.user.person in editors): + permission_denied(request, "You do not have permission to edit this document's title") + + if request.method == 'POST': + form = ChangeTitleForm(request.POST) + if form.is_valid(): + + bofreq.title = form.cleaned_data['title'] + + c = DocEvent(type="added_comment", doc=bofreq, rev=bofreq.rev, by=request.user.person) + c.desc = "Title changed to '%s'"%bofreq.title + c.save() + + bofreq.save_with_history([c]) + email_bofreq_title_changed(request, bofreq) + + return redirect("ietf.doc.views_doc.document_main", name=bofreq.name) + + else: + init = { "title" : bofreq.title } + form = ChangeTitleForm(initial=init) + + titletext = bofreq.get_base_name() + return render(request, 'doc/change_title.html', + {'form': form, + 'doc': bofreq, + 'titletext' : titletext, + }, + ) + + +class ChangeStateForm(forms.Form): + new_state = forms.ModelChoiceField(State.objects.filter(type="bofreq", used=True), label="BOF Request State", empty_label=None, required=True) + comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the state change history entry.", required=False, strip=False) + + +@role_required("Area Director", "Secretariat") +def change_state(request, name, option=None): + bofreq = get_object_or_404(Document, type="bofreq", name=name) + + login = request.user.person + + if request.method == 'POST': + form = ChangeStateForm(request.POST) + if form.is_valid(): + clean = form.cleaned_data + new_state = clean['new_state'] + comment = clean['comment'].rstrip() + + if comment: + c = DocEvent(type="added_comment", doc=bofreq, rev=bofreq.rev, by=login) + c.desc = comment + c.save() + + prev_state = bofreq.get_state() + if new_state != prev_state: + bofreq.set_state(new_state) + events = [] + events.append(add_state_change_event(bofreq, login, prev_state, new_state)) + bofreq.save_with_history(events) + + return redirect('ietf.doc.views_doc.document_main', name=bofreq.name) + else: + s = bofreq.get_state() + init = dict(new_state=s.pk if s else None) + form = ChangeStateForm(initial=init) + + return render(request, 'doc/change_state.html', + dict(form=form, + doc=bofreq, + login=login, + help_url=urlreverse('ietf.doc.views_help.state_help', kwargs=dict(type="bofreq")), + )) \ No newline at end of file diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index c1b4a59eb..c41f4a5c0 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -40,6 +40,7 @@ import io import json import os import re +import markdown from urllib.parse import quote @@ -54,7 +55,8 @@ 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 ) + IESG_BALLOT_ACTIVE_STATES, STATUSCHANGE_RELATIONS, DocumentActionHolder, DocumentAuthor, + BofreqEditorDocEvent ) 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, @@ -526,6 +528,25 @@ def document_main(request, name, rev=None): can_manage=can_manage, )) + 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() + can_manage = has_role(request.user,['Secretariat','Area Director']) + is_editor = request.user.is_authenticated and request.user.person in editors + + return render(request, "doc/document_bofreq.html", + dict(doc=doc, + top=top, + revisions=revisions, + latest_rev=latest_rev, + content=content, + snapshot=snapshot, + can_manage=can_manage, + editors=editors, + is_editor=is_editor, + )) + if doc.type_id == "conflrev": filename = "%s-%s.txt" % (doc.canonical_name(), doc.rev) pathname = os.path.join(settings.CONFLICT_REVIEW_PATH,filename) diff --git a/ietf/doc/views_help.py b/ietf/doc/views_help.py index 38d96be99..56b354cc4 100644 --- a/ietf/doc/views_help.py +++ b/ietf/doc/views_help.py @@ -17,6 +17,7 @@ def state_help(request, type): "charter": ("charter", "Charter States"), "conflict-review": ("conflrev", "Conflict Review States"), "status-change": ("statchg", "RFC Status Change States"), + "bofreq": ("bofreq", "BOF Request States"), }.get(type, (None, None)) state_type = get_object_or_404(StateType, slug=slug) diff --git a/ietf/mailtrigger/migrations/0023_bofreq_triggers.py b/ietf/mailtrigger/migrations/0023_bofreq_triggers.py new file mode 100644 index 000000000..cfba57644 --- /dev/null +++ b/ietf/mailtrigger/migrations/0023_bofreq_triggers.py @@ -0,0 +1,38 @@ +# Copyright The IETF Trust 2021 All Rights Reserved +# Generated by Django 2.2.23 on 2021-05-26 07:52 + +from django.db import migrations + +def forward(apps, schema_editor): + MailTrigger = apps.get_model('mailtrigger', 'MailTrigger') + Recipient = apps.get_model('mailtrigger', 'Recipient') + + Recipient.objects.create(slug='bofreq_editors',desc='BOF request editors',template='') + 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 %}') + + 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 = 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 = 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'])) + +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() + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailtrigger', '0022_add_doc_external_resource_change_requested'), + ] + + operations = [ + migrations.RunPython(forward,reverse) + ] diff --git a/ietf/mailtrigger/models.py b/ietf/mailtrigger/models.py index 3f8b2b0ea..70e70fdc5 100644 --- a/ietf/mailtrigger/models.py +++ b/ietf/mailtrigger/models.py @@ -6,6 +6,7 @@ from django.db import models from django.template import Template, Context from email.utils import parseaddr +from ietf.doc.models import BofreqEditorDocEvent from ietf.utils.mail import formataddr, get_email_addresses_from_text from ietf.group.models import Group from ietf.person.models import Email, Alias @@ -392,3 +393,13 @@ class Recipient(models.Model): def gather_yang_doctors_secretaries(self, **kwargs): return self.gather_group_secretaries(group=Group.objects.get(acronym='yangdoctors')) + + def gather_bofreq_editors(self, **kwargs): + 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()]) + return addrs + diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index c2d4aa03f..0d4b75290 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -2305,6 +2305,76 @@ "model": "doc.state", "pk": 157 }, + { + "fields": { + "desc": "The bof request is proposed", + "name": "Proposed", + "next_states": [ + 159, + 160, + 161, + 162 + ], + "order": 0, + "slug": "proposed", + "type": "bofreq", + "used": true + }, + "model": "doc.state", + "pk": 158 + }, + { + "fields": { + "desc": "The bof request is approved", + "name": "Approved", + "next_states": [], + "order": 1, + "slug": "approved", + "type": "bofreq", + "used": true + }, + "model": "doc.state", + "pk": 159 + }, + { + "fields": { + "desc": "The bof request is declined", + "name": "Declined", + "next_states": [], + "order": 2, + "slug": "declined", + "type": "bofreq", + "used": true + }, + "model": "doc.state", + "pk": 160 + }, + { + "fields": { + "desc": "The bof request is proposed", + "name": "Replaced", + "next_states": [], + "order": 3, + "slug": "replaced", + "type": "bofreq", + "used": true + }, + "model": "doc.state", + "pk": 161 + }, + { + "fields": { + "desc": "The bof request is abandoned", + "name": "Abandoned", + "next_states": [], + "order": 4, + "slug": "abandoned", + "type": "bofreq", + "used": true + }, + "model": "doc.state", + "pk": 162 + }, { "fields": { "label": "State" @@ -2319,6 +2389,13 @@ "model": "doc.statetype", "pk": "bluesheets" }, + { + "fields": { + "label": "Bof Request State" + }, + "model": "doc.statetype", + "pk": "bofreq" + }, { "fields": { "label": "State" @@ -2990,7 +3067,7 @@ "about_page": "ietf.group.views.group_about", "acts_like_wg": false, "admin_roles": "[\n \"chair\",\n \"secr\"\n]", - "agenda_type": null, + "agenda_type": "ietf", "create_wiki": true, "custom_group_roles": true, "customize_workflow": false, @@ -3002,7 +3079,7 @@ "has_chartering_process": false, "has_default_jabber": false, "has_documents": false, - "has_meetings": false, + "has_meetings": true, "has_milestones": false, "has_nonsession_materials": false, "has_reviews": true, @@ -3349,6 +3426,46 @@ "model": "mailtrigger.mailtrigger", "pk": "ballot_saved" }, + { + "fields": { + "cc": [], + "desc": "Recipients when the editors of a BOF proposal are changed.", + "to": [ + "bofreq_editors", + "bofreq_previous_editors", + "doc_ad", + "doc_notify" + ] + }, + "model": "mailtrigger.mailtrigger", + "pk": "bofreq_editors_changed" + }, + { + "fields": { + "cc": [], + "desc": "Recipients when a new revision of a BOF request is uploaded.", + "to": [ + "bofreq_editors", + "doc_ad", + "doc_notify" + ] + }, + "model": "mailtrigger.mailtrigger", + "pk": "bofreq_new_revision" + }, + { + "fields": { + "cc": [], + "desc": "Recipients when the title of a BOF proposal is changed.", + "to": [ + "bofreq_editors", + "doc_ad", + "doc_notify" + ] + }, + "model": "mailtrigger.mailtrigger", + "pk": "bofreq_title_changed" + }, { "fields": { "cc": [ @@ -4063,6 +4180,7 @@ "fields": { "cc": [ "liaison_cc", + "liaison_coordinators", "liaison_response_contacts", "liaison_technical_contacts" ], @@ -5001,6 +5119,22 @@ "model": "mailtrigger.mailtrigger", "pk": "sub_replaced_doc_director_approval_requested" }, + { + "fields": { + "desc": "BOF request editors", + "template": "" + }, + "model": "mailtrigger.recipient", + "pk": "bofreq_editors" + }, + { + "fields": { + "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 %}" + }, + "model": "mailtrigger.recipient", + "pk": "bofreq_previous_editors" + }, { "fields": { "desc": "The person providing a comment to nomcom", @@ -5401,6 +5535,14 @@ "model": "mailtrigger.recipient", "pk": "liaison_cc" }, + { + "fields": { + "desc": "The IAB liaison coordination team members", + "template": "" + }, + "model": "mailtrigger.recipient", + "pk": "liaison_coordinators" + }, { "fields": { "desc": "The assigned liaison manager for an external group ", @@ -9594,6 +9736,17 @@ "model": "name.doctypename", "pk": "bluesheets" }, + { + "fields": { + "desc": "", + "name": "Bof Request", + "order": 0, + "prefix": "bofreq", + "used": true + }, + "model": "name.doctypename", + "pk": "bofreq" + }, { "fields": { "desc": "", @@ -9901,6 +10054,7 @@ "auth", "aut-appr", "grp-appr", + "ad-appr", "manual", "cancel" ], @@ -11948,7 +12102,7 @@ "fields": { "desc": "", "name": "Liaison CC Contact", - "order": 0, + "order": 9, "used": true }, "model": "name.rolename", @@ -11958,7 +12112,7 @@ "fields": { "desc": "", "name": "Liaison Contact", - "order": 0, + "order": 8, "used": true }, "model": "name.rolename", @@ -12386,7 +12540,7 @@ }, { "fields": { - "desc": "IAB stream", + "desc": "Internet Architecture Board (IAB)", "name": "IAB", "order": 4, "used": true @@ -12396,7 +12550,7 @@ }, { "fields": { - "desc": "IETF stream", + "desc": "Internet Engineering Task Force (IETF)", "name": "IETF", "order": 1, "used": true @@ -12406,7 +12560,7 @@ }, { "fields": { - "desc": "IRTF Stream", + "desc": "Internet Research Task Force (IRTF)", "name": "IRTF", "order": 3, "used": true @@ -12416,7 +12570,7 @@ }, { "fields": { - "desc": "Independent Submission Editor stream", + "desc": "Independent Submission", "name": "ISE", "order": 2, "used": true @@ -15141,9 +15295,9 @@ "fields": { "command": "xym", "switch": "--version", - "time": "2020-11-14T00:10:15.888", + "time": "2021-05-20T00:12:38.672", "used": true, - "version": "xym 0.4.8" + "version": "xym 0.5" }, "model": "utils.versioninfo", "pk": 1 @@ -15152,7 +15306,7 @@ "fields": { "command": "pyang", "switch": "--version", - "time": "2020-11-14T00:10:17.069", + "time": "2021-05-20T00:12:40.445", "used": true, "version": "pyang 2.4.0" }, @@ -15163,7 +15317,7 @@ "fields": { "command": "yanglint", "switch": "--version", - "time": "2020-11-14T00:10:17.405", + "time": "2021-05-20T00:12:40.647", "used": true, "version": "yanglint SO 1.6.7" }, @@ -15174,9 +15328,9 @@ "fields": { "command": "xml2rfc", "switch": "--version", - "time": "2020-11-14T00:10:19.405", + "time": "2021-05-20T00:12:43.645", "used": true, - "version": "xml2rfc 3.4.0" + "version": "xml2rfc 3.7.0" }, "model": "utils.versioninfo", "pk": 4 diff --git a/ietf/name/migrations/0024_add_bofrequest.py b/ietf/name/migrations/0024_add_bofrequest.py new file mode 100644 index 000000000..79e95e7cd --- /dev/null +++ b/ietf/name/migrations/0024_add_bofrequest.py @@ -0,0 +1,23 @@ +# Copyright The IETF Trust 2021 All Rights Reserved + +# Generated by Django 2.2.23 on 2021-05-21 12:48 + +from django.db import migrations + +def forward(apps,schema_editor): + DocTypeName = apps.get_model('name','DocTypeName') + DocTypeName.objects.create(prefix='bofreq', slug='bofreq', name="Bof Request", desc="", used=True, order=0) + +def reverse(apps,schema_editor): + DocTypeName = apps.get_model('name','DocTypeName') + DocTypeName.objects.filter(slug='bofreq').delete() + +class Migration(migrations.Migration): + + dependencies = [ + ('name', '0023_change_stream_descriptions'), + ] + + operations = [ + migrations.RunPython(forward, reverse) + ] diff --git a/ietf/settings.py b/ietf/settings.py index 51eaecded..a8619618a 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -644,6 +644,7 @@ DATETIME_FORMAT = "Y-m-d H:i T" # regex can reasonably be expected to be a unique one-off. URL_REGEXPS = { "acronym": r"(?P[-a-z0-9]+)", + "bofreq": r"(?Pbofreq-[-a-z0-9]+)", "charter": r"(?Pcharter-[-a-z0-9]+)", "date": r"(?P\d{4}-\d{2}-\d{2})", "name": r"(?P[A-Za-z0-9._+-]+?)", @@ -662,6 +663,7 @@ INTERNET_DRAFT_PATH = '/a/ietfdata/doc/draft/repository' INTERNET_DRAFT_PDF_PATH = '/a/www/ietf-datatracker/pdf/' RFC_PATH = '/a/www/ietf-ftp/rfc/' CHARTER_PATH = '/a/ietfdata/doc/charter/' +BOFREQ_PATH = '/a/ietfdata/doc/bofreq/' CONFLICT_REVIEW_PATH = '/a/ietfdata/doc/conflict-review' STATUS_CHANGE_PATH = '/a/ietfdata/doc/status-change' AGENDA_PATH = '/a/www/www6s/proceedings/' diff --git a/ietf/static/ietf/js/upload_bofreq.js b/ietf/static/ietf/js/upload_bofreq.js new file mode 100644 index 000000000..bdfc3617f --- /dev/null +++ b/ietf/static/ietf/js/upload_bofreq.js @@ -0,0 +1,26 @@ +$(document).ready(function () { + var form = $("form.upload-content"); + // review submission selection + form.find("[name=bofreq_submission]").on("click change", function () { + var val = form.find("[name=bofreq_submission]:checked").val(); + + var shouldBeVisible = { + "enter": ['[name="bofreq_content"]'], + "upload": ['[name="bofreq_file"]'], + }; + + for (var v in shouldBeVisible) { + for (var i in shouldBeVisible[v]) { + var selector = shouldBeVisible[v][i]; + var row = form.find(selector); + if (!row.is(".form-group")) + row = row.closest(".form-group"); + + if ($.inArray(selector, shouldBeVisible[val]) != -1) + row.show(); + else + row.hide(); + } + } + }).trigger("change"); +}); \ No newline at end of file diff --git a/ietf/templates/base/menu.html b/ietf/templates/base/menu.html index 12fea0df6..19db79876 100644 --- a/ietf/templates/base/menu.html +++ b/ietf/templates/base/menu.html @@ -27,6 +27,7 @@
  • New work
  • Chartering groups
  • BOFs
  • +
  • BOF Requests
  • {% if flavor == "top" %}{% endif %}
  • Other groups
  • diff --git a/ietf/templates/doc/bofreq/bof_requests.html b/ietf/templates/doc/bofreq/bof_requests.html new file mode 100644 index 000000000..77e366659 --- /dev/null +++ b/ietf/templates/doc/bofreq/bof_requests.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2021 All Rights Reserved #} +{% load origin %} + +{% block title %}BOF Requests{% endblock %} + +{% block content %} + {% origin %} + + {% regroup reqs by get_state_slug as grouped_reqs %} + {% for req_group in grouped_reqs %} +
    +
    {{req_group.grouper|capfirst}} BOF Requests
    +
    + + + + + + {% for req in req_group.list %} + + + + + + {% endfor %} + +
    NameDateTitle
    {{req.name}}-{{req.rev}}{{req.latest_revision_event.time|date:"Y-m-d"}}{{req.title}}
    +
    +
    + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/ietf/templates/doc/bofreq/bofreq_template.md b/ietf/templates/doc/bofreq/bofreq_template.md new file mode 100644 index 000000000..8f81e60eb --- /dev/null +++ b/ietf/templates/doc/bofreq/bofreq_template.md @@ -0,0 +1,28 @@ +# Name: Exact MPLS Edges (EXAMPLE) (There's an acronym for anything if you really want one ;-) +## Description +Replace this with a few paragraphs describing the BoF request. + +Fill in the details below. Keep items in the order they appear here. + +## Required Details +- Status: (not) WG Forming +- Responsible AD: name +- BoF proponents: name , name (1-3 people - who are requesting and coordinating discussion for proposal) +- BoF chairs: TBD +- Number of people expected to attend: 100 +- Length of session (1 or 2 hours): 2 hours +- Conflicts (whole Areas and/or WGs) + - Chair Conflicts: TBD + - Technology Overlap: TBD + - Key Participant Conflict: TBD +## Agenda + - Items, drafts, speakers, timing + - Or a URL +## Links to the mailing list, draft charter if any, relevant Internet-Drafts, etc. + - Mailing List: https://www.ietf.org/mailman/listinfo/example + - Draft charter: https://datatracker.ietf.org/doc/charter-ietf-EXAMPLE/ + - Relevant drafts: + - Use Cases: + - https://datatracker.ietf.org/html/draft-blah-uses + - Solutions + - https://datatracker.ietf.org/html/draft-blah-soln \ No newline at end of file diff --git a/ietf/templates/doc/bofreq/change_editors.html b/ietf/templates/doc/bofreq/change_editors.html new file mode 100644 index 000000000..4570a0692 --- /dev/null +++ b/ietf/templates/doc/bofreq/change_editors.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2021, All Rights Reserved #} +{% load origin %} + +{% load bootstrap3 %} + +{% block title %}Change editors for {{doc.name}}{% endblock %} + +{% block pagehead %} + {{ form.media.css}} +{% endblock %} + +{% block content %} + {% origin %} +

    Change editors
    {{ titletext }}

    + +
    + {% csrf_token %} + {% bootstrap_form form %} + + {% buttons %} + + Back + {% endbuttons %} +
    +{% endblock %} + +{% block js %} + {{ form.media.js }} +{% endblock %} \ No newline at end of file diff --git a/ietf/templates/doc/bofreq/new_bofreq.html b/ietf/templates/doc/bofreq/new_bofreq.html new file mode 100644 index 000000000..b33839542 --- /dev/null +++ b/ietf/templates/doc/bofreq/new_bofreq.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2021, All Rights Reserved #} +{% load origin bootstrap3 static %} + +{% block title %}Start a new BoF Request{% endblock %} + +{% block content %} + {% origin %} +

    Start a new BoF Request

    + +

    Choose a short descriptive title for your request. Take time to choose a good initial title - it will be used to make the filename for your request's content. The title can be changed later, but the filename will not change.

    +

    For example, a request with a title of "A new important bit" will be saved as "bofreq-a-new-important-bit-00.md".

    +
    + {% csrf_token %} + + {% bootstrap_form form layout="horizontal" %} + + {% buttons %} + Cancel + + {% endbuttons %} +
    +{% endblock %} + +{% block js %} + +{% endblock %} + diff --git a/ietf/templates/doc/bofreq/upload_content.html b/ietf/templates/doc/bofreq/upload_content.html new file mode 100644 index 000000000..5591911f0 --- /dev/null +++ b/ietf/templates/doc/bofreq/upload_content.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2021, All Rights Reserved #} +{% load origin bootstrap3 static %} + +{% block title %}Upload new revision: {{doc.name}}{% endblock %} + +{% block content %} + {% origin %} +

    Upload New Revision
    + {{ doc.name }} +

    + +
    + {% csrf_token %} + + {% bootstrap_form form layout="horizontal" %} + + {% buttons %} + Cancel + + {% endbuttons %} +
    +{% endblock %} + +{% block js %} + +{% endblock %} + diff --git a/ietf/templates/doc/document_bofreq.html b/ietf/templates/doc/document_bofreq.html new file mode 100644 index 000000000..abd37c23b --- /dev/null +++ b/ietf/templates/doc/document_bofreq.html @@ -0,0 +1,137 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2021, All Rights Reserved #} +{% load origin %} +{% load static %} +{% load ietf_filters %} +{% load person_filters %} + +{% block pagehead %} + + + +{% endblock %} + +{% block title %}{{ doc.title }}{% endblock %} + +{% block content %} + {% origin %} + {{ top|safe }} + + {% include "doc/revisions_list.html" %} +
    + + + + + {% if doc.rev != latest_rev %} + + {% else %} + + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    The information below is for an older version of this BOF request
    DocumentType + {{doc.get_state.slug|capfirst}} BOF request + {% if snapshot %} + Snapshot + {% endif %} +
    Title + {% if not snapshot %} + {% if is_editor or can_manage %} + {% doc_edit_button 'ietf.doc.views_bofreq.edit_title' name=doc.name %} + {% endif %} + {% endif %} + {{ doc.title }}
    Last updated{{ doc.time|date:"Y-m-d" }}
    State + {% if not snapshot and can_manage %} + {% doc_edit_button 'ietf.doc.views_bofreq.change_state' name=doc.name %} + {% endif %} + + {% if doc.get_state %} + {{ doc.get_state.name }} + {% else %} + No document state + {% endif %} +
    Editor{{editors|pluralize}} + {% if not snapshot %} + {% if is_editor or can_manage %} + {% doc_edit_button 'ietf.doc.views_bofreq.change_editors' name=doc.name %} + {% endif %} + {% endif %} + + {% for editor in editors %} + {% person_link editor %}{% if not forloop.last %}, {% endif %} + {% endfor %} +
    Send notices to + {% if not snapshot %} + {% if can_manage %} + {% doc_edit_button 'ietf.doc.views_doc.edit_notify;bofreq' name=doc.name %} + {% endif %} + {% endif %} + + {{ doc.notify|default:"(None)" }} +
    + + {% if not snapshot %} + {% if is_editor or can_manage %} +

    Change BOF request text

    + {% endif %} + {% endif %} + +
    +
    {{doc.name}}-{{doc.rev}}
    +
    + {{ content|sanitize|safe }} +
    +
    + +{% endblock %} diff --git a/ietf/templates/doc/mail/bofreq_editors_changed.txt b/ietf/templates/doc/mail/bofreq_editors_changed.txt new file mode 100644 index 000000000..647fe1317 --- /dev/null +++ b/ietf/templates/doc/mail/bofreq_editors_changed.txt @@ -0,0 +1,6 @@ +{% autoescape off %}{{request.user.person.name}} has changed the editors for {{bofreq.name}}-{{bofreq.rev}} + +The previous editors were : {{previous_editors|join:", "}} + +The new editors are : {{editors|join:", "}} +{% endautoescape %} \ No newline at end of file diff --git a/ietf/templates/doc/mail/bofreq_new_revision.txt b/ietf/templates/doc/mail/bofreq_new_revision.txt new file mode 100644 index 000000000..5db056263 --- /dev/null +++ b/ietf/templates/doc/mail/bofreq_new_revision.txt @@ -0,0 +1,3 @@ +{% autoescape off %}{{request.user.person.name}} has uploaded {{bofreq.name}}-{{bofreq.rev}} +See {{settings.IDTRACKER_BASE_URL}}{{bofreq.get_absolute_url}} +{% endautoescape %} diff --git a/ietf/templates/doc/mail/bofreq_title_changed.txt b/ietf/templates/doc/mail/bofreq_title_changed.txt new file mode 100644 index 000000000..506df6ea4 --- /dev/null +++ b/ietf/templates/doc/mail/bofreq_title_changed.txt @@ -0,0 +1,3 @@ +{% autoescape off %}{{request.user.person.name}} has changed the title for {{bofreq.name}}-{{bofreq.rev}} to +"{{bofreq.title}}" +{% endautoescape %} \ No newline at end of file diff --git a/ietf/utils/test_utils.py b/ietf/utils/test_utils.py index 495ee77ad..b9a3abbdf 100644 --- a/ietf/utils/test_utils.py +++ b/ietf/utils/test_utils.py @@ -93,7 +93,6 @@ def reload_db_objects(*objects): """Rerequest the given arguments from the database so they're refreshed, to be used like foo, bar = reload_db_objects(foo, bar)""" - t = tuple(o.__class__.objects.get(pk=o.pk) for o in objects) if len(objects) == 1: return t[0] diff --git a/requirements.txt b/requirements.txt index 40588ec7a..d930a341c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,6 +36,7 @@ jsonfield>=3.0 # for SubmissionCheck. This is https://github.com/bradjasper/dj jwcrypto>=0.4.0 # for signed notifications logging_tree>=1.8.1 lxml>=3.4.0,<5 +markdown>=3.3.4 markdown2>=2.3.8 mock>=2.0.0 mypy>=0.782,<0.790 # Version requirements determined by django-stubs. From f5a04263e5144e22f9e7857d8eadeb6668954eb5 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 7 Jul 2021 17:49:35 +0000 Subject: [PATCH 02/12] Added the notion of responsible leadership. - Legacy-Id: 19202 --- ietf/doc/admin.py | 6 +- ietf/doc/factories.py | 23 +++- ietf/doc/mails.py | 20 +++- ietf/doc/migrations/0043_bofreq_docevents.py | 36 ++++++ .../migrations/0043_bofreqeditordocevent.py | 23 ---- ietf/doc/models.py | 7 +- ietf/doc/resources.py | 29 ++++- ietf/doc/tests_bofreq.py | 104 +++++++++++++++--- ietf/doc/urls_bofreq.py | 3 +- ietf/doc/utils_bofreq.py | 12 ++ ietf/doc/views_bofreq.py | 61 ++++++++-- ietf/doc/views_doc.py | 11 +- ietf/group/tests_info.py | 2 - .../migrations/0023_bofreq_triggers.py | 17 ++- ietf/mailtrigger/models.py | 30 ++++- ietf/name/fixtures/names.json | 36 +++++- ietf/templates/doc/bofreq/bof_requests.html | 51 +++++---- .../doc/bofreq/change_responsible.html | 30 +++++ ietf/templates/doc/document_bofreq.html | 18 ++- .../doc/mail/bofreq_responsible_changed.txt | 6 + 20 files changed, 432 insertions(+), 93 deletions(-) create mode 100644 ietf/doc/migrations/0043_bofreq_docevents.py delete mode 100644 ietf/doc/migrations/0043_bofreqeditordocevent.py create mode 100644 ietf/doc/utils_bofreq.py create mode 100644 ietf/templates/doc/bofreq/change_responsible.html create mode 100644 ietf/templates/doc/mail/bofreq_responsible_changed.txt diff --git a/ietf/doc/admin.py b/ietf/doc/admin.py index 6f4f30143..a8c29c5b8 100644 --- a/ietf/doc/admin.py +++ b/ietf/doc/admin.py @@ -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', ] diff --git a/ietf/doc/factories.py b/ietf/doc/factories.py index e37e8119d..699f972ea 100644 --- a/ietf/doc/factories.py +++ b/ietf/doc/factories.py @@ -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): diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index fc9a2bd26..0f344f015 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -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): diff --git a/ietf/doc/migrations/0043_bofreq_docevents.py b/ietf/doc/migrations/0043_bofreq_docevents.py new file mode 100644 index 000000000..7a300426b --- /dev/null +++ b/ietf/doc/migrations/0043_bofreq_docevents.py @@ -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',), + ), + ] diff --git a/ietf/doc/migrations/0043_bofreqeditordocevent.py b/ietf/doc/migrations/0043_bofreqeditordocevent.py deleted file mode 100644 index e6c3b433a..000000000 --- a/ietf/doc/migrations/0043_bofreqeditordocevent.py +++ /dev/null @@ -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',), - ), - ] diff --git a/ietf/doc/models.py b/ietf/doc/models.py index cab807c4d..cfcda14fb 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -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) \ No newline at end of file + """ 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) \ No newline at end of file diff --git a/ietf/doc/resources.py b/ietf/doc/resources.py index afd2d7949..99e26ac33 100644 --- a/ietf/doc/resources.py +++ b/ietf/doc/resources.py @@ -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()) diff --git a/ietf/doc/tests_bofreq.py b/ietf/doc/tests_bofreq.py index 5c5a1ac45..de9cb60c3 100644 --- a/ietf/doc/tests_bofreq.py +++ b/ietf/doc/tests_bofreq.py @@ -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) diff --git a/ietf/doc/urls_bofreq.py b/ietf/doc/urls_bofreq.py index bbc70ce78..856f99e63 100644 --- a/ietf/doc/urls_bofreq.py +++ b/ietf/doc/urls_bofreq.py @@ -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), ] \ No newline at end of file diff --git a/ietf/doc/utils_bofreq.py b/ietf/doc/utils_bofreq.py new file mode 100644 index 000000000..aec8f60ad --- /dev/null +++ b/ietf/doc/utils_bofreq.py @@ -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() \ No newline at end of file diff --git a/ietf/doc/views_bofreq.py b/ietf/doc/views_bofreq.py index abe011ad7..94e821908 100644 --- a/ietf/doc/views_bofreq.py +++ b/ietf/doc/views_bofreq.py @@ -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") diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 3f10480da..56b021686 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -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, )) diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py index 8470981cc..dc0396dcb 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -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 = { diff --git a/ietf/mailtrigger/migrations/0023_bofreq_triggers.py b/ietf/mailtrigger/migrations/0023_bofreq_triggers.py index cfba57644..d9a28d4fd 100644 --- a/ietf/mailtrigger/migrations/0023_bofreq_triggers.py +++ b/ietf/mailtrigger/migrations/0023_bofreq_triggers.py @@ -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): diff --git a/ietf/mailtrigger/models.py b/ietf/mailtrigger/models.py index 70e70fdc5..3cda3d24e 100644 --- a/ietf/mailtrigger/models.py +++ b/ietf/mailtrigger/models.py @@ -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 diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 28b3d9038..3c4ed4eb6 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -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", diff --git a/ietf/templates/doc/bofreq/bof_requests.html b/ietf/templates/doc/bofreq/bof_requests.html index 77e366659..4127de8f0 100644 --- a/ietf/templates/doc/bofreq/bof_requests.html +++ b/ietf/templates/doc/bofreq/bof_requests.html @@ -7,26 +7,35 @@ {% block content %} {% origin %} - {% regroup reqs by get_state_slug as grouped_reqs %} - {% for req_group in grouped_reqs %} -
    -
    {{req_group.grouper|capfirst}} BOF Requests
    -
    - - - - - - {% for req in req_group.list %} - - - - - - {% endfor %} - -
    NameDateTitle
    {{req.name}}-{{req.rev}}{{req.latest_revision_event.time|date:"Y-m-d"}}{{req.title}}
    +

    BOF Requests

    + + {% if not reqs %} +

    There are currently no BoF Requests

    + {% else %} + {% regroup reqs by get_state_slug as grouped_reqs %} + {% for req_group in grouped_reqs %} +
    +
    {{req_group.grouper|capfirst}} BOF Requests
    +
    + + + + + + {% for req in req_group.list %} + + + + + + {% endfor %} + +
    NameDateTitle
    {{req.name}}-{{req.rev}}{{req.latest_revision_event.time|date:"Y-m-d"}}{{req.title}}
    +
    -
    - {% endfor %} + {% endfor %} + {% endif %} + {% if request.user.is_authenticated %} + Start New Bof Request + {% endif %} {% endblock %} \ No newline at end of file diff --git a/ietf/templates/doc/bofreq/change_responsible.html b/ietf/templates/doc/bofreq/change_responsible.html new file mode 100644 index 000000000..23b8609bd --- /dev/null +++ b/ietf/templates/doc/bofreq/change_responsible.html @@ -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 %} +

    Change Responsible Leadership
    {{ titletext }}

    + +
    + {% csrf_token %} + {% bootstrap_form form %} + + {% buttons %} + + Back + {% endbuttons %} +
    +{% endblock %} + +{% block js %} + {{ form.media.js }} +{% endblock %} \ No newline at end of file diff --git a/ietf/templates/doc/document_bofreq.html b/ietf/templates/doc/document_bofreq.html index abd37c23b..4529bcf3d 100644 --- a/ietf/templates/doc/document_bofreq.html +++ b/ietf/templates/doc/document_bofreq.html @@ -87,7 +87,7 @@ Editor{{editors|pluralize}} {% 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 @@ + + + Responsible Leadership + + {% if not snapshot %} + {% if can_manage %} + {% doc_edit_button 'ietf.doc.views_bofreq.change_responsible' name=doc.name %} + {% endif %} + {% endif %} + + + {% for leader in responsible %} + {% person_link leader %}{% if not forloop.last %}, {% endif %} + {% endfor %} + + diff --git a/ietf/templates/doc/mail/bofreq_responsible_changed.txt b/ietf/templates/doc/mail/bofreq_responsible_changed.txt new file mode 100644 index 000000000..134d9f8f2 --- /dev/null +++ b/ietf/templates/doc/mail/bofreq_responsible_changed.txt @@ -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 %} \ No newline at end of file From a76961deb0f81b1da55a8fa0e7bafd352a6e5ce2 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 7 Jul 2021 19:11:58 +0000 Subject: [PATCH 03/12] Disallow saving unchanged template text. Remove stub for relations. Test changes to notify. - Legacy-Id: 19203 --- ietf/doc/tests_bofreq.py | 23 +++++++++++++++++++++++ ietf/doc/urls_bofreq.py | 1 - ietf/doc/views_bofreq.py | 7 +++---- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/ietf/doc/tests_bofreq.py b/ietf/doc/tests_bofreq.py index de9cb60c3..93c0c5dba 100644 --- a/ietf/doc/tests_bofreq.py +++ b/ietf/doc/tests_bofreq.py @@ -10,6 +10,7 @@ from tempfile import NamedTemporaryFile from django.conf import settings from django.urls import reverse as urlreverse +from django.template.loader import render_to_string from ietf.group.factories import RoleFactory from ietf.doc.factories import BofreqFactory, NewRevisionDocEventFactory @@ -326,6 +327,10 @@ This test section has some text. login_testing_unauthorized(self,nobody.user.username,url) r = self.client.get(url) self.assertContains(r,'Fill in the details below. Keep items in the order they appear here.',status_code=200) + r = self.client.post(url, dict(title='default', + bofreq_submission='enter', + bofreq_content=render_to_string('doc/bofreq/bofreq_template.md',{}))) + self.assertContains(r, 'The example content may not be saved.', status_code=200) file = NamedTemporaryFile(delete=False,mode="w+",encoding='utf-8') file.write('some stuff') file.close() @@ -360,3 +365,21 @@ This test section has some text. self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form div.has-error')) + + def test_change_notify(self): + doc = BofreqFactory() + url = urlreverse('ietf.doc.views_doc.edit_notify;bofreq', kwargs=dict(name=doc.name)) + login_testing_unauthorized(self, "secretary", url) + + # get + r = self.client.get(url) + self.assertEqual(r.status_code,200) + q = PyQuery(r.content) + self.assertEqual(len(q('form input[name=notify]')),1) + + # Provide a list + r = self.client.post(url,dict(notify="TJ2APh2P@ietf.org",save_addresses="1")) + self.assertEqual(r.status_code,302) + doc = reload_db_objects(doc) + self.assertEqual(doc.notify,'TJ2APh2P@ietf.org') + diff --git a/ietf/doc/urls_bofreq.py b/ietf/doc/urls_bofreq.py index 856f99e63..4dfde6e5a 100644 --- a/ietf/doc/urls_bofreq.py +++ b/ietf/doc/urls_bofreq.py @@ -4,7 +4,6 @@ from ietf.utils.urls import url urlpatterns = [ url(r'^notices/$', views_doc.edit_notify, name='ietf.doc.views_doc.edit_notify;bofreq'), - url(r'^relations/$', views_bofreq.edit_relations), url(r'^state/$', views_bofreq.change_state), url(r'^submit/$', views_bofreq.submit), url(r'^title/$', views_bofreq.edit_title), diff --git a/ietf/doc/views_bofreq.py b/ietf/doc/views_bofreq.py index 94e821908..732ae3127 100644 --- a/ietf/doc/views_bofreq.py +++ b/ietf/doc/views_bofreq.py @@ -32,10 +32,6 @@ def bof_requests(request): return render(request, 'doc/bofreq/bof_requests.html',dict(reqs=reqs)) -def edit_relations(request, name): - raise NotImplementedError - - class BofreqUploadForm(forms.Form): ACTIONS = [ ("enter", "Enter content directly"), @@ -47,6 +43,9 @@ class BofreqUploadForm(forms.Form): def clean_bofreq_content(self): content = self.cleaned_data["bofreq_content"].replace("\r", "") + default_content = render_to_string('doc/bofreq/bofreq_template.md',{}) + if content==default_content: + raise forms.ValidationError('The example content may not be saved. Edit it as instructed to document this BoF request.') try: _ = markdown.markdown(content, extensions=['extra']) except Exception as e: From 040e26be067c1b6625ddf840699aaf338c7996db Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 7 Jul 2021 22:28:05 +0000 Subject: [PATCH 04/12] Added Additional Resources. Simplified the bofreq urls file. - Legacy-Id: 19204 --- ietf/doc/tests_bofreq.py | 21 ++------------- ietf/doc/urls_bofreq.py | 3 +-- ietf/templates/doc/document_bofreq.html | 35 ++++++++++++++++++++++--- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/ietf/doc/tests_bofreq.py b/ietf/doc/tests_bofreq.py index 93c0c5dba..460734450 100644 --- a/ietf/doc/tests_bofreq.py +++ b/ietf/doc/tests_bofreq.py @@ -97,7 +97,7 @@ This test section has some text. r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) - self.assertEqual(5, len(q('td.edit>a.btn'))) + self.assertEqual(6, len(q('td.edit>a.btn'))) self.client.logout() self.assertNotEqual([],q('#change-request')) editor = editors.first().user.username @@ -105,7 +105,7 @@ This test section has some text. r = self.client.get(url) self.assertEqual(r.status_code,200) q = PyQuery(r.content) - self.assertEqual(2, len(q('td.edit>a.btn'))) + self.assertEqual(3, len(q('td.edit>a.btn'))) self.assertNotEqual([],q('#change-request')) self.client.logout() url = urlreverse('ietf.doc.views_doc.document_main', kwargs=dict(name=doc,rev='00')) @@ -365,21 +365,4 @@ This test section has some text. self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form div.has-error')) - - def test_change_notify(self): - doc = BofreqFactory() - url = urlreverse('ietf.doc.views_doc.edit_notify;bofreq', kwargs=dict(name=doc.name)) - login_testing_unauthorized(self, "secretary", url) - - # get - r = self.client.get(url) - self.assertEqual(r.status_code,200) - q = PyQuery(r.content) - self.assertEqual(len(q('form input[name=notify]')),1) - - # Provide a list - r = self.client.post(url,dict(notify="TJ2APh2P@ietf.org",save_addresses="1")) - self.assertEqual(r.status_code,302) - doc = reload_db_objects(doc) - self.assertEqual(doc.notify,'TJ2APh2P@ietf.org') diff --git a/ietf/doc/urls_bofreq.py b/ietf/doc/urls_bofreq.py index 4dfde6e5a..9e92f6236 100644 --- a/ietf/doc/urls_bofreq.py +++ b/ietf/doc/urls_bofreq.py @@ -1,9 +1,8 @@ -from ietf.doc import views_bofreq, views_doc +from ietf.doc import views_bofreq from ietf.utils.urls import url urlpatterns = [ - url(r'^notices/$', views_doc.edit_notify, name='ietf.doc.views_doc.edit_notify;bofreq'), url(r'^state/$', views_bofreq.change_state), url(r'^submit/$', views_bofreq.submit), url(r'^title/$', views_bofreq.edit_title), diff --git a/ietf/templates/doc/document_bofreq.html b/ietf/templates/doc/document_bofreq.html index 4529bcf3d..a5e194296 100644 --- a/ietf/templates/doc/document_bofreq.html +++ b/ietf/templates/doc/document_bofreq.html @@ -114,10 +114,37 @@ {% person_link leader %}{% if not forloop.last %}, {% endif %} {% endfor %} + - - - + {% with doc.docextresource_set.all as resources %} + {% if resources or is_editor or can_manage %} + + + Additional Resources + + {% if is_editor or can_manage %} + Edit + {% endif %} + + + {% if resources %} + + + {% for resource in resources|dictsort:"display_name" %} + {% if resource.name.type.slug == 'url' or resource.name.type.slug == 'email' %} + + {# Maybe make how a resource displays itself a method on the class so templates aren't doing this switching #} + {% else %} + + {% endif %} + {% endfor %} + +
    - {% firstof resource.display_name resource.name.name %}
    - {% firstof resource.display_name resource.name.name %}: {{resource.value}}
    + {% endif %} + + + {% endif %} + {% endwith %} @@ -125,7 +152,7 @@ {% if not snapshot %} {% if can_manage %} - {% doc_edit_button 'ietf.doc.views_doc.edit_notify;bofreq' name=doc.name %} + {% doc_edit_button 'ietf.doc.views_doc.edit_notify' name=doc.name %} {% endif %} {% endif %} From dcd372a928c6356ab06bad3309b4d4bbf17f8fa9 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 8 Jul 2021 19:16:58 +0000 Subject: [PATCH 05/12] Restrict editor access in all states but proposed. - Legacy-Id: 19205 --- ietf/doc/tests_bofreq.py | 29 ++++++++++++++++++++++++- ietf/doc/utils.py | 5 +++-- ietf/doc/views_bofreq.py | 9 +++++--- ietf/doc/views_doc.py | 4 ++-- ietf/ietfauth/utils.py | 11 ++++++++++ ietf/templates/doc/document_bofreq.html | 10 ++++----- 6 files changed, 55 insertions(+), 13 deletions(-) diff --git a/ietf/doc/tests_bofreq.py b/ietf/doc/tests_bofreq.py index 460734450..d5e0ac7dd 100644 --- a/ietf/doc/tests_bofreq.py +++ b/ietf/doc/tests_bofreq.py @@ -80,7 +80,7 @@ This test section has some text. self.write_bofreq_file(doc) editors = bofreq_editors(doc) responsible = bofreq_responsible(doc) - url = urlreverse('ietf.doc.views_doc.document_main', kwargs=dict(name=doc)) + url = urlreverse('ietf.doc.views_doc.document_main', kwargs=dict(name=doc.name)) r = self.client.get(url) self.assertContains(r,'Version: 01',status_code=200) q = PyQuery(r.content) @@ -365,4 +365,31 @@ This test section has some text. self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form div.has-error')) + + def test_post_proposed_restrictions(self): + states = State.objects.filter(type_id='bofreq').exclude(slug='proposed') + bofreq = BofreqFactory() + editor = bofreq_editors(bofreq).first() + + for view in ('submit', 'change_editors', 'edit_title'): + url = urlreverse(f'ietf.doc.views_bofreq.{view}', kwargs=dict(name=bofreq.name)) + for state in states: + bofreq.set_state(state) + for username in ('secretary', 'ad', 'iab-member'): + self.client.login(username=username, password=username+'+password') + r = self.client.get(url) + self.assertEqual(r.status_code,200) + self.client.logout() + self.client.login(username=editor.user.username, password=editor.user.username+'+password') + r = self.client.get(url) + self.assertEqual(r.status_code, 403, f'editor should not be able to use {view} in state {state.slug}') + self.client.logout() + + url = urlreverse('ietf.doc.views_doc.document_main', kwargs=dict(name=bofreq.name)) + self.client.login(username=editor.user.username, password=editor.user.username+'+password') + r = self.client.get(url) + self.assertEqual(r.status_code,200) + q = PyQuery(r.content) + self.assertEqual(0, len(q('td.edit>a.btn'))) + self.assertEqual([],q('#change-request')) diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index 3b8dd1332..59a02c416 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -29,7 +29,7 @@ from ietf.doc.models import DocEvent, ConsensusDocEvent, BallotDocEvent, IRSGBal from ietf.doc.models import TelechatDocEvent, DocumentActionHolder, EditedAuthorsDocEvent from ietf.name.models import DocReminderTypeName, DocRelationshipName from ietf.group.models import Role, Group -from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, is_individual_draft_author +from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, is_individual_draft_author, is_bofreq_editor from ietf.person.models import Person from ietf.review.models import ReviewWish from ietf.utils import draft, text @@ -152,7 +152,8 @@ def can_unadopt_draft(user, doc): def can_edit_docextresources(user, doc): return (has_role(user, ("Secretariat", "Area Director")) or is_authorized_in_doc_stream(user, doc) - or is_individual_draft_author(user, doc)) + or is_individual_draft_author(user, doc) + or is_bofreq_editor(user, doc)) def two_thirds_rule( recused=0 ): # For standards-track, need positions from 2/3 of the non-recused current IESG. diff --git a/ietf/doc/views_bofreq.py b/ietf/doc/views_bofreq.py index 732ae3127..9ae92f4cd 100644 --- a/ietf/doc/views_bofreq.py +++ b/ietf/doc/views_bofreq.py @@ -76,7 +76,8 @@ class BofreqUploadForm(forms.Form): def submit(request, name): bofreq = get_object_or_404(Document, type="bofreq", name=name) previous_editors = bofreq_editors(bofreq) - if not (has_role(request.user,('Secretariat', 'Area Director', 'IAB')) or request.user.person in previous_editors): + state_id = bofreq.get_state_slug('bofreq') + if not (has_role(request.user,('Secretariat', 'Area Director', 'IAB')) or (state_id=='proposed' and request.user.person in previous_editors)): permission_denied(request,"You do not have permission to upload a new revision of this BOF Request") if request.method == 'POST': @@ -189,7 +190,8 @@ class ChangeEditorsForm(forms.Form): def change_editors(request, name): bofreq = get_object_or_404(Document, type="bofreq", name=name) previous_editors = bofreq_editors(bofreq) - if not (has_role(request.user,('Secretariat', 'Area Director', 'IAB')) or request.user.person in previous_editors): + state_id = bofreq.get_state_slug('bofreq') + if not (has_role(request.user,('Secretariat', 'Area Director', 'IAB')) or (state_id=='proposed' and request.user.person in previous_editors)): permission_denied(request,"You do not have permission to change this document's editors") if request.method == 'POST': @@ -267,7 +269,8 @@ class ChangeTitleForm(forms.Form): def edit_title(request, name): bofreq = get_object_or_404(Document, type="bofreq", name=name) editors = bofreq_editors(bofreq) - if not (has_role(request.user,('Secretariat', 'Area Director', 'IAB')) or request.user.person in editors): + state_id = bofreq.get_state_slug('bofreq') + if not (has_role(request.user,('Secretariat', 'Area Director', 'IAB')) or (state_id=='proposed' and request.user.person in editors)): permission_denied(request, "You do not have permission to edit this document's title") if request.method == 'POST': diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 56b021686..e95077564 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -533,7 +533,7 @@ def document_main(request, name, rev=None): 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 + editor_can_manage = doc.get_state_slug('bofreq')=='proposed' and request.user.is_authenticated and request.user.person in editors return render(request, "doc/document_bofreq.html", dict(doc=doc, @@ -545,7 +545,7 @@ def document_main(request, name, rev=None): can_manage=can_manage, editors=editors, responsible=responsible, - is_editor=is_editor, + editor_can_manage=editor_can_manage, )) if doc.type_id == "conflrev": diff --git a/ietf/ietfauth/utils.py b/ietf/ietfauth/utils.py index f84d430ce..40042fbfd 100644 --- a/ietf/ietfauth/utils.py +++ b/ietf/ietfauth/utils.py @@ -25,6 +25,7 @@ import debug # pyflakes:ignore from ietf.group.models import Role, GroupFeatures from ietf.person.models import Person +from ietf.doc.utils_bofreq import bofreq_editors def user_is_person(user, person): """Test whether user is associated with person.""" @@ -194,6 +195,9 @@ def is_individual_draft_author(user, doc): if not user.is_authenticated: return False + if not doc.type_id=='draft': + return False + if not doc.group.type_id == "individ" : return False @@ -204,6 +208,13 @@ def is_individual_draft_author(user, doc): return True return False + +def is_bofreq_editor(user, doc): + if not user.is_authenticated: + return False + if not doc.type_id=='bofreq': + return False + return user.person in bofreq_editors(doc) def openid_userinfo(claims, user): # Populate claims dict. diff --git a/ietf/templates/doc/document_bofreq.html b/ietf/templates/doc/document_bofreq.html index a5e194296..6ab624a95 100644 --- a/ietf/templates/doc/document_bofreq.html +++ b/ietf/templates/doc/document_bofreq.html @@ -50,7 +50,7 @@ Title {% if not snapshot %} - {% if is_editor or can_manage %} + {% if editor_can_manage or can_manage %} {% doc_edit_button 'ietf.doc.views_bofreq.edit_title' name=doc.name %} {% endif %} {% endif %} @@ -87,7 +87,7 @@ Editor{{editors|pluralize}} {% if not snapshot %} - {% if is_editor or can_manage %} + {% if editor_can_manage or can_manage %} {% doc_edit_button 'ietf.doc.views_bofreq.change_editors' name=doc.name %} {% endif %} {% endif %} @@ -117,12 +117,12 @@ {% with doc.docextresource_set.all as resources %} - {% if resources or is_editor or can_manage %} + {% if resources or editor_can_manage or can_manage %} Additional Resources - {% if is_editor or can_manage %} + {% if editor_can_manage or can_manage %} Edit {% endif %} @@ -165,7 +165,7 @@ {% if not snapshot %} - {% if is_editor or can_manage %} + {% if editor_can_manage or can_manage %}

    Change BOF request text

    {% endif %} {% endif %} From 916e5240cecee7ac6ae879d2ef005074ea8f90c9 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 8 Jul 2021 19:59:43 +0000 Subject: [PATCH 06/12] improved relevant email expansions - Legacy-Id: 19206 --- ietf/mailtrigger/migrations/0023_bofreq_triggers.py | 5 +++++ ietf/mailtrigger/models.py | 4 +++- ietf/mailtrigger/utils.py | 9 +++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/ietf/mailtrigger/migrations/0023_bofreq_triggers.py b/ietf/mailtrigger/migrations/0023_bofreq_triggers.py index d9a28d4fd..4c3580140 100644 --- a/ietf/mailtrigger/migrations/0023_bofreq_triggers.py +++ b/ietf/mailtrigger/migrations/0023_bofreq_triggers.py @@ -26,9 +26,14 @@ def forward(apps, schema_editor): 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=['bofreq_responsible', 'bofreq_editors', 'doc_notify'])) + for recipient in Recipient.objects.filter(slug__in=['bofreq_responsible','bofreq_editors']): + MailTrigger.objects.get(slug='doc_state_edited').to.add(recipient) + def reverse(apps, schema_editor): MailTrigger = apps.get_model('mailtrigger', 'MailTrigger') Recipient = apps.get_model('mailtrigger', 'Recipient') + for recipient in Recipient.objects.filter(slug__in=['bofreq_responsible','bofreq_editors']): + MailTrigger.objects.get(slug='doc_state_edited').to.remove(recipient) 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() diff --git a/ietf/mailtrigger/models.py b/ietf/mailtrigger/models.py index 3cda3d24e..1ead8b804 100644 --- a/ietf/mailtrigger/models.py +++ b/ietf/mailtrigger/models.py @@ -417,7 +417,9 @@ class Recipient(models.Model): def gather_bofreq_previous_responsible(self, **kwargs): addrs = [] - previous_responsible = kwargs['previous_responsible'] + previous_responsible = None + if previous_responsible in kwargs: + previous_responsible = kwargs['previous_responsible'] if previous_responsible: addrs = [p.email_address() for p in previous_responsible] else: diff --git a/ietf/mailtrigger/utils.py b/ietf/mailtrigger/utils.py index 2e998f437..48d91ff6a 100644 --- a/ietf/mailtrigger/utils.py +++ b/ietf/mailtrigger/utils.py @@ -68,8 +68,10 @@ def gather_relevant_expansions(**kwargs): doc = kwargs['doc'] - # PEY: does this need to include irsg_ballot_saved as well? - relevant.update(['doc_state_edited','doc_telechat_details_changed','ballot_deferred','iesg_ballot_saved']) + relevant.add('doc_state_edited') + + if not doc.type_id in ['bofreq',]: + relevant.update(['doc_telechat_details_changed','ballot_deferred','iesg_ballot_saved']) if doc.type_id in ['draft','statchg']: relevant.update(starts_with('last_call_')) @@ -91,6 +93,9 @@ def gather_relevant_expansions(**kwargs): if doc.type_id == 'charter': relevant.update(['charter_external_review','ballot_approved_charter']) + if doc.type_id == 'bofreq': + relevant.update(starts_with('bofreq')) + if 'group' in kwargs: relevant.update(starts_with('group_')) From 885fe35d48bdefa7f49af62b58d11684507dd532 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 8 Jul 2021 20:37:34 +0000 Subject: [PATCH 07/12] show responsible on list of requests - Legacy-Id: 19207 --- ietf/doc/views_bofreq.py | 1 + ietf/templates/doc/bofreq/bof_requests.html | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ietf/doc/views_bofreq.py b/ietf/doc/views_bofreq.py index 9ae92f4cd..c496ec9cc 100644 --- a/ietf/doc/views_bofreq.py +++ b/ietf/doc/views_bofreq.py @@ -29,6 +29,7 @@ def bof_requests(request): reqs = Document.objects.filter(type_id='bofreq') for req in reqs: req.latest_revision_event = req.latest_event(NewRevisionDocEvent) + req.responsible = bofreq_responsible(req) return render(request, 'doc/bofreq/bof_requests.html',dict(reqs=reqs)) diff --git a/ietf/templates/doc/bofreq/bof_requests.html b/ietf/templates/doc/bofreq/bof_requests.html index 4127de8f0..8708066fd 100644 --- a/ietf/templates/doc/bofreq/bof_requests.html +++ b/ietf/templates/doc/bofreq/bof_requests.html @@ -19,7 +19,7 @@
    - + {% for req in req_group.list %} @@ -27,6 +27,7 @@ + {% endfor %} From 805d1f4a7e84492830fab57a61dd13f0d7ffe01d Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 14 Jul 2021 17:33:14 +0000 Subject: [PATCH 08/12] Use BOF consistently. - Legacy-Id: 19212 --- ietf/doc/migrations/0042_bofreq_states.py | 12 ++++---- ietf/doc/models.py | 4 +-- ietf/doc/tests_bofreq.py | 2 +- ietf/doc/tests_charter.py | 2 +- ietf/doc/views_bofreq.py | 4 +-- ietf/group/forms.py | 6 ++-- ietf/group/tests_info.py | 2 +- ietf/ipr/tests.py | 2 ++ .../commands/create_dummy_meeting.py | 8 ++--- .../management/commands/generate_schedule.py | 16 +++++----- .../migrations/0029_businessconstraint.py | 8 ++--- ietf/meeting/tests_views.py | 12 ++++---- ietf/meeting/views.py | 4 +-- ietf/name/fixtures/names.json | 30 ++++++++++--------- ietf/name/migrations/0027_add_bofrequest.py | 2 +- ietf/secr/announcement/forms.py | 2 +- ietf/templates/doc/bofreq/bof_requests.html | 8 ++--- ietf/templates/doc/bofreq/bofreq_template.md | 6 ++-- ietf/templates/doc/bofreq/new_bofreq.html | 4 +-- .../edit_meeting_schedule_session.html | 4 +-- ietf/templates/meeting/important-dates.html | 2 +- .../meeting/important_dates_for_meeting.ics | 2 +- ietf/templates/meeting/request_minutes.txt | 2 +- 23 files changed, 74 insertions(+), 70 deletions(-) diff --git a/ietf/doc/migrations/0042_bofreq_states.py b/ietf/doc/migrations/0042_bofreq_states.py index 110a4fee4..95119f814 100644 --- a/ietf/doc/migrations/0042_bofreq_states.py +++ b/ietf/doc/migrations/0042_bofreq_states.py @@ -8,12 +8,12 @@ def forward(apps, schema_editor): StateType = apps.get_model('doc', 'StateType') State = apps.get_model('doc', 'State') - StateType.objects.create(slug='bofreq', label='Bof Request State') - proposed = State.objects.create(type_id='bofreq', slug='proposed', name='Proposed', used=True, desc='The bof request is proposed', order=0) - approved = State.objects.create(type_id='bofreq', slug='approved', name='Approved', used=True, desc='The bof request is approved', order=1) - declined = State.objects.create(type_id='bofreq', slug='declined', name='Declined', used=True, desc='The bof request is declined', order=2) - replaced = State.objects.create(type_id='bofreq', slug='replaced', name='Replaced', used=True, desc='The bof request is proposed', order=3) - abandoned = State.objects.create(type_id='bofreq', slug='abandoned', name='Abandoned', used=True, desc='The bof request is abandoned', order=4) + StateType.objects.create(slug='bofreq', label='BOF Request State') + proposed = State.objects.create(type_id='bofreq', slug='proposed', name='Proposed', used=True, desc='The BOF request is proposed', order=0) + approved = State.objects.create(type_id='bofreq', slug='approved', name='Approved', used=True, desc='The BOF request is approved', order=1) + declined = State.objects.create(type_id='bofreq', slug='declined', name='Declined', used=True, desc='The BOF request is declined', order=2) + replaced = State.objects.create(type_id='bofreq', slug='replaced', name='Replaced', used=True, desc='The BOF request is proposed', order=3) + abandoned = State.objects.create(type_id='bofreq', slug='abandoned', name='Abandoned', used=True, desc='The BOF request is abandoned', order=4) proposed.next_states.set([approved,declined,replaced,abandoned]) diff --git a/ietf/doc/models.py b/ietf/doc/models.py index cfcda14fb..cf3605d83 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -1349,9 +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.""" + """ 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 """ + """ Capture the responsible leadership (IAB and IESG members) for a BOF Request """ responsible = models.ManyToManyField('person.Person', blank=True) \ No newline at end of file diff --git a/ietf/doc/tests_bofreq.py b/ietf/doc/tests_bofreq.py index d5e0ac7dd..c8c6949eb 100644 --- a/ietf/doc/tests_bofreq.py +++ b/ietf/doc/tests_bofreq.py @@ -46,7 +46,7 @@ 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) + 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()) diff --git a/ietf/doc/tests_charter.py b/ietf/doc/tests_charter.py index f6a972fae..c0aad851e 100644 --- a/ietf/doc/tests_charter.py +++ b/ietf/doc/tests_charter.py @@ -275,7 +275,7 @@ class EditCharterTests(TestCase): login_testing_unauthorized(self, "secretary", url) response=self.client.get(url) self.assertEqual(response.status_code,200) - response = self.client.post(url,{'comment':'Testing Abandoning a Bof Charter'}) + response = self.client.post(url,{'comment':'Testing Abandoning a BOF Charter'}) self.assertEqual(response.status_code,302) charter = Document.objects.get(pk=charter.pk) self.assertEqual(charter.group.state_id,'abandon') diff --git a/ietf/doc/views_bofreq.py b/ietf/doc/views_bofreq.py index c496ec9cc..d82826109 100644 --- a/ietf/doc/views_bofreq.py +++ b/ietf/doc/views_bofreq.py @@ -46,7 +46,7 @@ class BofreqUploadForm(forms.Form): content = self.cleaned_data["bofreq_content"].replace("\r", "") default_content = render_to_string('doc/bofreq/bofreq_template.md',{}) if content==default_content: - raise forms.ValidationError('The example content may not be saved. Edit it as instructed to document this BoF request.') + raise forms.ValidationError('The example content may not be saved. Edit it as instructed to document this BOF request.') try: _ = markdown.markdown(content, extensions=['extra']) except Exception as e: @@ -126,7 +126,7 @@ class NewBofreqForm(BofreqUploadForm): if name == 'bofreq-': raise forms.ValidationError('The filename derived from this title is empty. Please include a few descriptive words using ascii or numeric characters') if Document.objects.filter(name=name).exists(): - raise forms.ValidationError('This title produces a filename already used by an existing BoF request') + raise forms.ValidationError('This title produces a filename already used by an existing BOF request') return title @login_required diff --git a/ietf/group/forms.py b/ietf/group/forms.py index 30a4b8b34..a31897329 100644 --- a/ietf/group/forms.py +++ b/ietf/group/forms.py @@ -188,11 +188,11 @@ class GroupForm(forms.Form): if existing and existing.type_id == self.group_type: if existing.state_id == "bof": - #insert_confirm_field(label="Turn BoF %s into proposed %s and start chartering it" % (existing.acronym, existing.type.name), initial=True) + #insert_confirm_field(label="Turn BOF %s into proposed %s and start chartering it" % (existing.acronym, existing.type.name), initial=True) if confirmed: return acronym else: - raise forms.ValidationError("Warning: Acronym used for an existing BoF (%s)." % existing.acronym) + raise forms.ValidationError("Warning: Acronym used for an existing BOF (%s)." % existing.acronym) else: #insert_confirm_field(label="Set state of %s %s to proposed and start chartering it" % (existing.acronym, existing.type.name), initial=False) if confirmed: @@ -265,7 +265,7 @@ class GroupForm(forms.Form): state = cleaned_data.get('state', None) parent = cleaned_data.get('parent', None) if state and (state.slug in ['bof', ] and 'parent' in self.fields and not parent): - raise forms.ValidationError("You requested the creation of a BoF, but specified no parent area. A parent is required when creating a bof.") + raise forms.ValidationError("You requested the creation of a BOF, but specified no parent area. A parent is required when creating a bof.") return cleaned_data diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py index dc0396dcb..fde22c634 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -561,7 +561,7 @@ class GroupEditTests(TestCase): q = PyQuery(r.content) self.assertTrue(len(q('form .has-error')) > 0) - # try elevating BoF to WG + # try elevating BOF to WG group.state_id = "bof" group.save() diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index 12f587d0f..2e0b02c3e 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -180,6 +180,8 @@ class IprTests(TestCase): # find by patent info r = self.client.get(url + "?submit=patent&patent=%s" % ipr.patent_info) + debug.show('ipr.patent_info') + debug.show('r.content') self.assertContains(r, ipr.title) r = self.client.get(url + "?submit=patent&patent=US12345") diff --git a/ietf/meeting/management/commands/create_dummy_meeting.py b/ietf/meeting/management/commands/create_dummy_meeting.py index bbd3b7835..dcd679d62 100644 --- a/ietf/meeting/management/commands/create_dummy_meeting.py +++ b/ietf/meeting/management/commands/create_dummy_meeting.py @@ -366,7 +366,7 @@ class Command(BaseCommand): attendees=100, agenda_note="", requested_duration=datetime.timedelta(seconds=7200), # 2:00:00 - comments="""Must not conflict with Transport Area BoFs. """, # this is implicit + comments="""Must not conflict with Transport Area BOFs. """, # this is implicit remote_instructions="", ) ## session for tsvwg ## @@ -377,7 +377,7 @@ class Command(BaseCommand): attendees=100, agenda_note="", requested_duration=datetime.timedelta(seconds=7200), # 2:00:00 - comments="""Must not conflict with Transport Area BoFs. """, # this is implicit + comments="""Must not conflict with Transport Area BOFs. """, # this is implicit remote_instructions="", ) c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflict', target_id=1665, ) # intarea @@ -440,7 +440,7 @@ class Command(BaseCommand): attendees=80, agenda_note="Joint with ARTAREA", requested_duration=datetime.timedelta(seconds=7200), # 2:00:00 - comments=""" and avoid the same kind of conflicts with other area meetings and any Bofs and potential new ART WGs.""", # this is implicit + comments=""" and avoid the same kind of conflicts with other area meetings and any BOFs and potential new ART WGs.""", # this is implicit remote_instructions="", ) s.joint_with_groups.set(Group.objects.filter(acronym='artarea')) @@ -2598,7 +2598,7 @@ class Command(BaseCommand): attendees=50, agenda_note="", requested_duration=datetime.timedelta(seconds=5400), # 1:30:00 - comments="""Please avoid collision with any Sec and IoT-related BoFs.""", + comments="""Please avoid collision with any Sec and IoT-related BOFs.""", remote_instructions="", ) c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflict', target_id=1187, ) # saag diff --git a/ietf/meeting/management/commands/generate_schedule.py b/ietf/meeting/management/commands/generate_schedule.py index 2799fa04f..12dc72900 100644 --- a/ietf/meeting/management/commands/generate_schedule.py +++ b/ietf/meeting/management/commands/generate_schedule.py @@ -705,24 +705,24 @@ class Session(object): for other in overlapping_sessions: if not other: continue - # BoFs cannot conflict with PRGs + # BOFs cannot conflict with PRGs if self.is_bof and other.is_prg: - violations.append('{}: BoF overlaps with PRG: {}' + violations.append('{}: BOF overlaps with PRG: {}' .format(self.group, other.group)) cost += self.business_constraint_costs['bof_overlapping_prg'] - # BoFs cannot conflict with any other BoFs + # BOFs cannot conflict with any other BOFs if self.is_bof and other.is_bof: - violations.append('{}: BoF overlaps with other BoF: {}' + violations.append('{}: BOF overlaps with other BOF: {}' .format(self.group, other.group)) cost += self.business_constraint_costs['bof_overlapping_bof'] - # BoFs cannot conflict with any other WGs in their area + # BOFs cannot conflict with any other WGs in their area if self.is_bof and self.parent == other.parent: - violations.append('{}: BoF overlaps with other session from same area: {}' + violations.append('{}: BOF overlaps with other session from same area: {}' .format(self.group, other.group)) cost += self.business_constraint_costs['bof_overlapping_area_wg'] - # BoFs cannot conflict with any area-wide meetings (of any area) + # BOFs cannot conflict with any area-wide meetings (of any area) if self.is_bof and other.is_area_meeting: - violations.append('{}: BoF overlaps with area meeting {}' + violations.append('{}: BOF overlaps with area meeting {}' .format(self.group, other.group)) cost += self.business_constraint_costs['bof_overlapping_area_meeting'] # Area meetings cannot conflict with anything else in their area diff --git a/ietf/meeting/migrations/0029_businessconstraint.py b/ietf/meeting/migrations/0029_businessconstraint.py index ba8ae8939..7e1f13ee7 100644 --- a/ietf/meeting/migrations/0029_businessconstraint.py +++ b/ietf/meeting/migrations/0029_businessconstraint.py @@ -9,22 +9,22 @@ def forward(apps, schema_editor): BusinessConstraint = apps.get_model("meeting", "BusinessConstraint") BusinessConstraint.objects.create( slug="bof_overlapping_prg", - name="BoFs cannot conflict with PRGs", + name="BOFs cannot conflict with PRGs", penalty=100000, ) BusinessConstraint.objects.create( slug="bof_overlapping_bof", - name="BoFs cannot conflict with any other BoFs", + name="BOFs cannot conflict with any other BOFs", penalty=100000, ) BusinessConstraint.objects.create( slug="bof_overlapping_area_wg", - name="BoFs cannot conflict with any other WGs in their area", + name="BOFs cannot conflict with any other WGs in their area", penalty=100000, ) BusinessConstraint.objects.create( slug="bof_overlapping_area_meeting", - name="BoFs cannot conflict with any area-wide meetings (of any area)", + name="BOFs cannot conflict with any area-wide meetings (of any area)", penalty=10000, ) BusinessConstraint.objects.create( diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 6870655a5..ef6a4af3e 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -752,7 +752,7 @@ class MeetingTests(TestCase): # Should be a 'non-area events' link showing appropriate types non_area_labels = [ - 'BoF', 'EDU', 'Hackathon', 'IEPG', 'IESG', 'IETF', 'Plenary', 'Secretariat', 'Tools', + 'BOF', 'EDU', 'Hackathon', 'IEPG', 'IESG', 'IETF', 'Plenary', 'Secretariat', 'Tools', ] self.assertIn('%s?show=%s' % (ical_url, ','.join(non_area_labels).lower()), content) @@ -1029,7 +1029,7 @@ class EditMeetingScheduleTests(TestCase): self.assertEqual(time_labels, time_header_labels) def test_bof_session_tag(self): - """Sessions for BoF groups should be marked as such""" + """Sessions for BOF groups should be marked as such""" meeting = MeetingFactory(type_id='ietf') non_bof_session = SessionFactory(meeting=meeting) @@ -1044,13 +1044,13 @@ class EditMeetingScheduleTests(TestCase): q = PyQuery(r.content) self.assertEqual(len(q('#session{} .bof-tag'.format(non_bof_session.pk))), 0, - 'Non-BoF session should not be tagged as a BoF session') + 'Non-BOF session should not be tagged as a BOF session') bof_tags = q('#session{} .bof-tag'.format(bof_session.pk)) self.assertEqual(len(bof_tags), 1, - 'BoF session should have one BoF session tag') - self.assertIn('BoF', bof_tags.eq(0).text(), - 'BoF tag should contain text "BoF"') + 'BOF session should have one BOF session tag') + self.assertIn('BOF', bof_tags.eq(0).text(), + 'BOF tag should contain text "BOF"') def _setup_for_swap_timeslots(self): """Create a meeting, rooms, and schedule for swap_timeslots testing diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index ad688761b..631e3eac9 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -1552,7 +1552,7 @@ def prepare_filter_keywords(tagged_assignments, group_parents): # Keywords that should appear in 'non-area' column non_area_labels = [ - 'BoF', 'EDU', 'Hackathon', 'IEPG', 'IESG', 'IETF', 'Plenary', 'Secretariat', 'Tools', + 'BOF', 'EDU', 'Hackathon', 'IEPG', 'IESG', 'IETF', 'Plenary', 'Secretariat', 'Tools', ] # Remove any unused non-area keywords non_area_filters = [ @@ -4102,7 +4102,7 @@ def request_minutes(request, num=None): body = render_to_string('meeting/request_minutes.txt', body_context) initial = {'to': 'wgchairs@ietf.org', 'cc': 'irsg@irtf.org', - 'subject': 'Request for IETF WG and Bof Session Minutes', + 'subject': 'Request for IETF WG and BOF Session Minutes', 'body': body, } form = RequestMinutesForm(initial=initial) diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 3c4ed4eb6..5f2700677 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -2307,7 +2307,7 @@ }, { "fields": { - "desc": "The bof request is proposed", + "desc": "The BOF request is proposed", "name": "Proposed", "next_states": [ 159, @@ -2325,7 +2325,7 @@ }, { "fields": { - "desc": "The bof request is approved", + "desc": "The BOF request is approved", "name": "Approved", "next_states": [], "order": 1, @@ -2338,7 +2338,7 @@ }, { "fields": { - "desc": "The bof request is declined", + "desc": "The BOF request is declined", "name": "Declined", "next_states": [], "order": 2, @@ -2351,7 +2351,7 @@ }, { "fields": { - "desc": "The bof request is proposed", + "desc": "The BOF request is proposed", "name": "Replaced", "next_states": [], "order": 3, @@ -2364,7 +2364,7 @@ }, { "fields": { - "desc": "The bof request is abandoned", + "desc": "The BOF request is abandoned", "name": "Abandoned", "next_states": [], "order": 4, @@ -2391,7 +2391,7 @@ }, { "fields": { - "label": "Bof Request State" + "label": "BOF Request State" }, "model": "doc.statetype", "pk": "bofreq" @@ -2702,20 +2702,20 @@ "about_page": "ietf.group.views.group_about", "acts_like_wg": false, "admin_roles": "[\n \"chair\",\n \"secr\"\n]", - "agenda_type": null, + "agenda_type": "ad", "create_wiki": true, "custom_group_roles": true, "customize_workflow": false, "default_parent": "", "default_tab": "ietf.group.views.group_about", "default_used_roles": "[\n \"ad\",\n \"chair\",\n \"reviewer\",\n \"secr\"\n]", - "docman_roles": "[]", + "docman_roles": "[\n \"chair\"\n]", "groupman_authroles": "[\n \"Secretariat\"\n]", "groupman_roles": "[\n \"ad\",\n \"secr\"\n]", "has_chartering_process": false, "has_default_jabber": false, "has_documents": false, - "has_meetings": false, + "has_meetings": true, "has_milestones": false, "has_nonsession_materials": false, "has_reviews": false, @@ -3862,6 +3862,8 @@ "cc": [], "desc": "Recipients when a document's state is manually edited", "to": [ + "bofreq_editors", + "bofreq_responsible", "doc_ad", "doc_affecteddoc_authors", "doc_affecteddoc_group_chairs", @@ -9917,7 +9919,7 @@ { "fields": { "desc": "", - "name": "Bof Request", + "name": "BOF Request", "order": 0, "prefix": "bofreq", "used": true @@ -15483,7 +15485,7 @@ "fields": { "command": "xym", "switch": "--version", - "time": "2021-07-05T00:12:20.968", + "time": "2021-07-13T00:12:25.184", "used": true, "version": "xym 0.5" }, @@ -15494,7 +15496,7 @@ "fields": { "command": "pyang", "switch": "--version", - "time": "2021-07-05T00:12:22.309", + "time": "2021-07-13T00:12:26.721", "used": true, "version": "pyang 2.5.0" }, @@ -15505,7 +15507,7 @@ "fields": { "command": "yanglint", "switch": "--version", - "time": "2021-07-05T00:12:22.603", + "time": "2021-07-13T00:12:27.015", "used": true, "version": "yanglint SO 1.6.7" }, @@ -15516,7 +15518,7 @@ "fields": { "command": "xml2rfc", "switch": "--version", - "time": "2021-07-05T00:12:25.333", + "time": "2021-07-13T00:12:29.814", "used": true, "version": "xml2rfc 3.9.1" }, diff --git a/ietf/name/migrations/0027_add_bofrequest.py b/ietf/name/migrations/0027_add_bofrequest.py index 985c1d27b..c4e31bb2d 100644 --- a/ietf/name/migrations/0027_add_bofrequest.py +++ b/ietf/name/migrations/0027_add_bofrequest.py @@ -6,7 +6,7 @@ from django.db import migrations def forward(apps,schema_editor): DocTypeName = apps.get_model('name','DocTypeName') - DocTypeName.objects.create(prefix='bofreq', slug='bofreq', name="Bof Request", desc="", used=True, order=0) + DocTypeName.objects.create(prefix='bofreq', slug='bofreq', name="BOF Request", desc="", used=True, order=0) def reverse(apps,schema_editor): DocTypeName = apps.get_model('name','DocTypeName') diff --git a/ietf/secr/announcement/forms.py b/ietf/secr/announcement/forms.py index b2f923024..92383631b 100644 --- a/ietf/secr/announcement/forms.py +++ b/ietf/secr/announcement/forms.py @@ -19,7 +19,7 @@ TO_LIST = ('IETF Announcement List ', 'RFP Announcement List ', 'The IESG ', 'Working Group Chairs ', - 'BoF Chairs ', + 'BOF Chairs ', 'Other...') # --------------------------------------------- diff --git a/ietf/templates/doc/bofreq/bof_requests.html b/ietf/templates/doc/bofreq/bof_requests.html index 8708066fd..45a4b4763 100644 --- a/ietf/templates/doc/bofreq/bof_requests.html +++ b/ietf/templates/doc/bofreq/bof_requests.html @@ -9,8 +9,11 @@

    BOF Requests

    + {% if request.user.is_authenticated %} + Start New BOF Request + {% endif %} {% if not reqs %} -

    There are currently no BoF Requests

    +

    There are currently no BOF Requests

    {% else %} {% regroup reqs by get_state_slug as grouped_reqs %} {% for req_group in grouped_reqs %} @@ -36,7 +39,4 @@ {% endfor %} {% endif %} - {% if request.user.is_authenticated %} - Start New Bof Request - {% endif %} {% endblock %} \ No newline at end of file diff --git a/ietf/templates/doc/bofreq/bofreq_template.md b/ietf/templates/doc/bofreq/bofreq_template.md index 8f81e60eb..a55198dbc 100644 --- a/ietf/templates/doc/bofreq/bofreq_template.md +++ b/ietf/templates/doc/bofreq/bofreq_template.md @@ -1,14 +1,14 @@ # Name: Exact MPLS Edges (EXAMPLE) (There's an acronym for anything if you really want one ;-) ## Description -Replace this with a few paragraphs describing the BoF request. +Replace this with a few paragraphs describing the BOF request. Fill in the details below. Keep items in the order they appear here. ## Required Details - Status: (not) WG Forming - Responsible AD: name -- BoF proponents: name , name (1-3 people - who are requesting and coordinating discussion for proposal) -- BoF chairs: TBD +- BOF proponents: name , name (1-3 people - who are requesting and coordinating discussion for proposal) +- BOF chairs: TBD - Number of people expected to attend: 100 - Length of session (1 or 2 hours): 2 hours - Conflicts (whole Areas and/or WGs) diff --git a/ietf/templates/doc/bofreq/new_bofreq.html b/ietf/templates/doc/bofreq/new_bofreq.html index b33839542..e73acbb61 100644 --- a/ietf/templates/doc/bofreq/new_bofreq.html +++ b/ietf/templates/doc/bofreq/new_bofreq.html @@ -2,11 +2,11 @@ {# Copyright The IETF Trust 2021, All Rights Reserved #} {% load origin bootstrap3 static %} -{% block title %}Start a new BoF Request{% endblock %} +{% block title %}Start a new BOF Request{% endblock %} {% block content %} {% origin %} -

    Start a new BoF Request

    +

    Start a new BOF Request

    Choose a short descriptive title for your request. Take time to choose a good initial title - it will be used to make the filename for your request's content. The title can be changed later, but the filename will not change.

    For example, a request with a title of "A new important bit" will be saved as "bofreq-a-new-important-bit-00.md".

    diff --git a/ietf/templates/meeting/edit_meeting_schedule_session.html b/ietf/templates/meeting/edit_meeting_schedule_session.html index 960fcc01b..3d819777d 100644 --- a/ietf/templates/meeting/edit_meeting_schedule_session.html +++ b/ietf/templates/meeting/edit_meeting_schedule_session.html @@ -1,7 +1,7 @@
    {{ session.scheduling_label }} - {% if session.group and session.group.is_bof %}BoF{% endif %} + {% if session.group and session.group.is_bof %}BOF{% endif %}
    @@ -32,7 +32,7 @@ {{ session.scheduling_label }} · {{ session.requested_duration_in_hours }}h {% if session.group %} - · {% if session.group.is_bof %}BoF{% else %}{{ session.group.type.name }}{% endif %} + · {% if session.group.is_bof %}BOF{% else %}{{ session.group.type.name }}{% endif %} {% endif %} {% if session.attendees != None %} · {{ session.attendees }} diff --git a/ietf/templates/meeting/important-dates.html b/ietf/templates/meeting/important-dates.html index 95b75dc0a..6cbde01ec 100644 --- a/ietf/templates/meeting/important-dates.html +++ b/ietf/templates/meeting/important-dates.html @@ -27,7 +27,7 @@ {% if d.name.slug == 'opensched' %} To request a Working Group session, use the IETF Meeting Session Request Tool. - If you are working on a BoF request, it is highly recommended + If you are working on a BOF request, it is highly recommended to tell the IESG now by sending an email to iesg@ietf.org to get advance help with the request. {% endif %} diff --git a/ietf/templates/meeting/important_dates_for_meeting.ics b/ietf/templates/meeting/important_dates_for_meeting.ics index 2cbcf4f53..25c26eabf 100644 --- a/ietf/templates/meeting/important_dates_for_meeting.ics +++ b/ietf/templates/meeting/important_dates_for_meeting.ics @@ -9,7 +9,7 @@ DESCRIPTION:{{ d.name.desc }}{% if first and d.name.slug == 'openreg' or first a Register here: https://www.ietf.org/how/meetings/register/{% endif %}{% if d.name.slug == 'opensched' %}\n To request a Working Group session, use the IETF Meeting Session Request Tool:\n {{ request.scheme }}://{{ request.get_host}}{% url 'ietf.secr.sreq.views.main' %}\n - If you are working on a BoF request, it is highly recommended to tell the IESG\n + If you are working on a BOF request, it is highly recommended to tell the IESG\n now by sending an email to iesg@ietf.org to get advance help with the request.{% endif %}{% if d.name.slug == 'cutoffwgreq' %}\n To request a Working Group session, use the IETF Meeting Session Request Tool:\n {{ request.scheme }}://{{ request.get_host }}{% url 'ietf.secr.sreq.views.main' %}{% endif %}{% if d.name.slug == 'cutoffbofreq' %}\n diff --git a/ietf/templates/meeting/request_minutes.txt b/ietf/templates/meeting/request_minutes.txt index 52daf9a3c..ebe1c667c 100644 --- a/ietf/templates/meeting/request_minutes.txt +++ b/ietf/templates/meeting/request_minutes.txt @@ -16,6 +16,6 @@ manual posting. Groups that are missing minutes:{% for group in needs_minutes %}{% ifchanged group.parent %} {{group.parent.name}}:{% endifchanged %} - {{ group.acronym | upper }}{% if group.state_id == 'bof' %} (BoF){% endif %}{% endfor %} + {{ group.acronym | upper }}{% if group.state_id == 'bof' %} (BOF){% endif %}{% endfor %} {% endautoescape %} From f7de8c04af1265d787dadc25e781fa83d57fd0e2 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 14 Jul 2021 19:06:39 +0000 Subject: [PATCH 09/12] Improved external resource change docevent description - Legacy-Id: 19216 --- ietf/doc/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index 59a02c416..32a15d2cc 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -1220,13 +1220,15 @@ def update_doc_extresources(doc, new_resources, by): if old_res_strs == new_res_strs: return False # no change + old_res_strs = f'\n\n{old_res_strs}\n\n' if old_res_strs else ' None ' + new_res_strs = f'\n\n{new_res_strs}' if new_res_strs else ' None' + doc.docextresource_set.all().delete() for new_res in new_resources: new_res.doc = doc new_res.save() e = DocEvent(doc=doc, rev=doc.rev, by=by, type='changed_document') - e.desc = "Changed document external resources from:\n\n%s\n\nto:\n\n%s" % ( - old_res_strs, new_res_strs) + e.desc = f"Changed document external resources from:{old_res_strs}to:{new_res_strs}" e.save() doc.save_with_history([e]) return True From b1007d5df54bee71ebbeb269a4993f568cb659c8 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 14 Jul 2021 19:11:27 +0000 Subject: [PATCH 10/12] Remove unintended debug statements - Legacy-Id: 19218 --- ietf/ipr/tests.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index 2e0b02c3e..12f587d0f 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -180,8 +180,6 @@ class IprTests(TestCase): # find by patent info r = self.client.get(url + "?submit=patent&patent=%s" % ipr.patent_info) - debug.show('ipr.patent_info') - debug.show('r.content') self.assertContains(r, ipr.title) r = self.client.get(url + "?submit=patent&patent=US12345") From c1272e210e0d61b930519c00c0783a2ac81f8b33 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 14 Jul 2021 19:59:56 +0000 Subject: [PATCH 11/12] Better sort of bofreq list - Legacy-Id: 19219 --- ietf/doc/tests_bofreq.py | 16 +++++++--------- ietf/doc/views_bofreq.py | 3 ++- ietf/templates/doc/bofreq/bof_requests.html | 4 +++- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/ietf/doc/tests_bofreq.py b/ietf/doc/tests_bofreq.py index c8c6949eb..f8b86d30d 100644 --- a/ietf/doc/tests_bofreq.py +++ b/ietf/doc/tests_bofreq.py @@ -1,11 +1,13 @@ # Copyright The IETF Trust 2021 All Rights Reserved +import datetime import debug # pyflakes:ignore import io -import shutil import os +import shutil from pyquery import PyQuery +from random import randint from tempfile import NamedTemporaryFile from django.conf import settings @@ -49,18 +51,14 @@ This test section has some text. 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()) - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - q = PyQuery(r.content) - self.assertEqual(len(q('#bofreqs-proposed tbody tr')), states.count()) - for i in range(states.count()): - reqs[i].set_state(states[i]) + for i in range(3*len(states)): + BofreqFactory(states=[('bofreq',states[i%len(states)].slug)],newrevisiondocevent__time=datetime.datetime.today()-datetime.timedelta(days=randint(0,20))) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) for state in states: - self.assertEqual(len(q(f'#bofreqs-{state.slug} tbody tr')), 1) + self.assertEqual(len(q(f'#bofreqs-{state.slug}')), 1) + self.assertEqual(len(q(f'#bofreqs-{state.slug} tbody tr')), 3) self.assertFalse(q('#start_button')) PersonFactory(user__username='nobody') self.client.login(username='nobody', password='nobody+password') diff --git a/ietf/doc/views_bofreq.py b/ietf/doc/views_bofreq.py index d82826109..1c7252486 100644 --- a/ietf/doc/views_bofreq.py +++ b/ietf/doc/views_bofreq.py @@ -30,7 +30,8 @@ def bof_requests(request): for req in reqs: req.latest_revision_event = req.latest_event(NewRevisionDocEvent) req.responsible = bofreq_responsible(req) - return render(request, 'doc/bofreq/bof_requests.html',dict(reqs=reqs)) + sorted_reqs = sorted(sorted(reqs, key=lambda doc: doc.latest_revision_event.time, reverse=True), key=lambda doc: doc.get_state().order) + return render(request, 'doc/bofreq/bof_requests.html',dict(reqs=sorted_reqs)) class BofreqUploadForm(forms.Form): diff --git a/ietf/templates/doc/bofreq/bof_requests.html b/ietf/templates/doc/bofreq/bof_requests.html index 45a4b4763..2e22f5b55 100644 --- a/ietf/templates/doc/bofreq/bof_requests.html +++ b/ietf/templates/doc/bofreq/bof_requests.html @@ -10,7 +10,9 @@

    BOF Requests

    {% if request.user.is_authenticated %} - Start New BOF Request + {% endif %} {% if not reqs %}

    There are currently no BOF Requests

    From 15939c0114840e1f8d37b8e48c421688e90d580d Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 14 Jul 2021 20:21:47 +0000 Subject: [PATCH 12/12] show editors on bofreq page - Legacy-Id: 19220 --- ietf/doc/views_bofreq.py | 1 + ietf/templates/doc/bofreq/bof_requests.html | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ietf/doc/views_bofreq.py b/ietf/doc/views_bofreq.py index 1c7252486..eca146f80 100644 --- a/ietf/doc/views_bofreq.py +++ b/ietf/doc/views_bofreq.py @@ -30,6 +30,7 @@ def bof_requests(request): for req in reqs: req.latest_revision_event = req.latest_event(NewRevisionDocEvent) req.responsible = bofreq_responsible(req) + req.editors = bofreq_editors(req) sorted_reqs = sorted(sorted(reqs, key=lambda doc: doc.latest_revision_event.time, reverse=True), key=lambda doc: doc.get_state().order) return render(request, 'doc/bofreq/bof_requests.html',dict(reqs=sorted_reqs)) diff --git a/ietf/templates/doc/bofreq/bof_requests.html b/ietf/templates/doc/bofreq/bof_requests.html index 2e22f5b55..4edbf8a30 100644 --- a/ietf/templates/doc/bofreq/bof_requests.html +++ b/ietf/templates/doc/bofreq/bof_requests.html @@ -1,6 +1,7 @@ {% extends "base.html" %} {# Copyright The IETF Trust 2021 All Rights Reserved #} {% load origin %} +{% load person_filters %} {% block title %}BOF Requests{% endblock %} @@ -24,7 +25,7 @@
    NameDateTitle
    NameDateTitleResponsible
    {{req.name}}-{{req.rev}} {{req.latest_revision_event.time|date:"Y-m-d"}} {{req.title}}{{req.responsible|join:', '}}
    - + {% for req in req_group.list %} @@ -32,7 +33,8 @@ - + + {% endfor %}
    NameDateTitleResponsible
    NameDateTitleResponsibleEditors
    {{req.name}}-{{req.rev}} {{req.latest_revision_event.time|date:"Y-m-d"}} {{req.title}}{{req.responsible|join:', '}}{% for person in req.responsible %}{% person_link person %}{% if not forloop.last %}, {% endif %}{% endfor %}{% for person in req.editors %}{% person_link person %}{% if not forloop.last %}, {% endif %}{% endfor %}