Staging for merge forward

- Legacy-Id: 19199
This commit is contained in:
Robert Sparks 2021-07-06 18:05:54 +00:00
parent bfad845662
commit 6b383255ad
30 changed files with 1329 additions and 21 deletions

View file

@ -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', ]

View file

@ -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'))

View file

@ -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)

View file

@ -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)
]

View file

@ -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',),
),
]

View file

@ -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)

285
ietf/doc/tests_bofreq.py Normal file
View file

@ -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'))

View file

@ -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<name>[\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')),

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

@ -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),
]

296
ietf/doc/views_bofreq.py Normal file
View file

@ -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")),
))

View file

@ -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)

View file

@ -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)

View file

@ -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)
]

View file

@ -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

View file

@ -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": "<liaison-coordination@iab.org>"
},
"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

View file

@ -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)
]

View file

@ -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<acronym>[-a-z0-9]+)",
"bofreq": r"(?P<name>bofreq-[-a-z0-9]+)",
"charter": r"(?P<name>charter-[-a-z0-9]+)",
"date": r"(?P<date>\d{4}-\d{2}-\d{2})",
"name": r"(?P<name>[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/'

View file

@ -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");
});

View file

@ -27,6 +27,7 @@
<li {%if flavor == "top" %}class="dropdown-header hidden-xs"{% else %}class="nav-header"{% endif %}>New work</li>
<li><a href="{% url "ietf.group.views.chartering_groups" %}">Chartering groups</a></li>
<li><a href="{% url "ietf.group.views.bofs" group_type="wg" %}">BOFs</a></li>
<li><a href="{% url "ietf.doc.views_bofreq.bof_requests" %}">BOF Requests</a></li>
{% if flavor == "top" %}<li class="divider hidden-xs"></li>{% endif %}
<li {%if flavor == "top" %}class="dropdown-header hidden-xs"{% else %}class="nav-header"{% endif %}>Other groups</li>

View file

@ -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 %}
<div class="panel panel-default">
<div class="panel-heading">{{req_group.grouper|capfirst}} BOF Requests</div>
<div class="panel-body">
<table id="bofreqs-{{req_group.grouper}}" class="table table-condensed table-striped tablesorter">
<thead>
<tr><th class="col-sm-4">Name</th><th class="col-sm-1">Date</th><th>Title</th></tr>
</thead>
<tbody>
{% for req in req_group.list %}
<tr>
<td><a href={% url 'ietf.doc.views_doc.document_main' name=req.name %}>{{req.name}}-{{req.rev}}</a></td>
<td>{{req.latest_revision_event.time|date:"Y-m-d"}}</td>
<td>{{req.title}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
{% endblock %}

View file

@ -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 <email>, name <email> (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

View file

@ -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 %}
<h1>Change editors<br><small>{{ titletext }}</small></h1>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">Submit</button>
<a class="btn btn-default pull-right" href="{% url "ietf.doc.views_doc.document_main" name=doc.canonical_name %}">Back</a>
{% endbuttons %}
</form>
{% endblock %}
{% block js %}
{{ form.media.js }}
{% endblock %}

View file

@ -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 %}
<h1>Start a new BoF Request</h1>
<p>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.</p>
<p>For example, a request with a title of "A new important bit" will be saved as "bofreq-a-new-important-bit-00.md".</p>
<form class="upload-content form-horizontal" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form form layout="horizontal" %}
{% buttons %}
<a class="btn btn-default" href="{{ doc.get_absolute_url }}">Cancel</a>
<button type="submit" class="btn btn-primary">Submit</button>
{% endbuttons %}
</form>
{% endblock %}
{% block js %}
<script src="{% static 'ietf/js/upload_bofreq.js' %}"></script>
{% endblock %}

View file

@ -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 %}
<h1>Upload New Revision<br>
<small>{{ doc.name }}</small>
</h1>
<form class="upload-content form-horizontal" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form form layout="horizontal" %}
{% buttons %}
<a class="btn btn-default" href="{{ doc.get_absolute_url }}">Cancel</a>
<button type="submit" class="btn btn-primary">Submit</button>
{% endbuttons %}
</form>
{% endblock %}
{% block js %}
<script src="{% static 'ietf/js/upload_bofreq.js' %}"></script>
{% endblock %}

View file

@ -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 %}
<script src="{% static 'd3/d3.min.js' %}"></script>
<script src="{% static 'jquery/jquery.min.js' %}"></script>
<script src="{% static 'ietf/js/document_timeline.js' %}"></script>
{% endblock %}
{% block title %}{{ doc.title }}{% endblock %}
{% block content %}
{% origin %}
{{ top|safe }}
{% include "doc/revisions_list.html" %}
<div id="timeline"></div>
<table class="table table-condensed">
<thead id="message-row">
<tr>
{% if doc.rev != latest_rev %}
<th colspan="4" class="alert-warning">The information below is for an older version of this BOF request</th>
{% else %}
<th colspan="4"></th>
{% endif %}
</tr>
</thead>
<tbody class="meta">
<tr>
<th>Document</th>
<th>Type</th>
<td class="edit"></td>
<td>
{{doc.get_state.slug|capfirst}} BOF request
{% if snapshot %}
<span class="label label-warning">Snapshot</span>
{% endif %}
</td>
</tr>
<tr>
<td></td>
<th>Title</th>
<td class="edit">
{% if not snapshot %}
{% if is_editor or can_manage %}
{% doc_edit_button 'ietf.doc.views_bofreq.edit_title' name=doc.name %}
{% endif %}
{% endif %}
</td>
<td>{{ doc.title }}</td>
</tr>
<tr>
<td></td>
<th>Last updated</th>
<td class="edit"></td>
<td>{{ doc.time|date:"Y-m-d" }}</td>
</tr>
<tr>
<td></td>
<th><a href="{% url 'ietf.doc.views_help.state_help' type='bofreq' %}">State</a></th>
<td class="edit">
{% if not snapshot and can_manage %}
{% doc_edit_button 'ietf.doc.views_bofreq.change_state' name=doc.name %}
{% endif %}
</td>
<td>
{% if doc.get_state %}
<span title="{{ doc.get_state.desc }}">{{ doc.get_state.name }}</span>
{% else %}
No document state
{% endif %}
</td>
</tr>
<tr id="editors">
<td></td>
<th>Editor{{editors|pluralize}}</th>
<td class="edit">
{% if not snapshot %}
{% if is_editor or can_manage %}
{% doc_edit_button 'ietf.doc.views_bofreq.change_editors' name=doc.name %}
{% endif %}
{% endif %}
</td>
<td>
{% for editor in editors %}
{% person_link editor %}{% if not forloop.last %}, {% endif %}
{% endfor %}
</td>
</tr>
</tbody>
<tbody class="meta">
<tr>
<td></td>
<th>Send notices to</th>
<td class="edit">
{% if not snapshot %}
{% if can_manage %}
{% doc_edit_button 'ietf.doc.views_doc.edit_notify;bofreq' name=doc.name %}
{% endif %}
{% endif %}
</td>
<td>
{{ doc.notify|default:"(None)" }}
</td>
</tr>
</tbody>
</table>
{% if not snapshot %}
{% if is_editor or can_manage %}
<p id="change-request"><a class="btn btn-default" href="{% url 'ietf.doc.views_bofreq.submit' name=doc.name %}">Change BOF request text</a></p>
{% endif %}
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">{{doc.name}}-{{doc.rev}}</div>
<div class="panel-body">
{{ content|sanitize|safe }}
</div>
</div>
{% endblock %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -0,0 +1,3 @@
{% autoescape off %}{{request.user.person.name}} has changed the title for {{bofreq.name}}-{{bofreq.rev}} to
"{{bofreq.title}}"
{% endautoescape %}

View file

@ -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]

View file

@ -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.