Merged in [18798] from jennifer@painless-security.com:

Improve handling of submissions for closed working groups. Fixes #3058.
 - Legacy-Id: 18807
Note: SVN reference [18798] has been migrated to Git commit 233bff8196
This commit is contained in:
Robert Sparks 2021-01-27 23:19:42 +00:00
commit 9a9f3fa360
14 changed files with 870 additions and 119 deletions

View file

@ -49,6 +49,8 @@ class GroupInfo(models.Model):
uses_milestone_dates = models.BooleanField(default=True)
ACTIVE_STATE_IDS = ('active', 'bof', 'proposed') # states considered "active"
def __str__(self):
return self.name
@ -72,12 +74,39 @@ class GroupInfo(models.Model):
def is_bof(self):
return self.state_id in ["bof", "bof-conc"]
@property
def is_wg(self):
return self.type_id == 'wg'
@property
def is_active(self):
# N.B., this has only been thought about for groups of type WG!
return self.state_id in self.ACTIVE_STATE_IDS
@property
def is_individual(self):
return self.acronym == 'none'
@property
def area(self):
if self.type_id == 'area':
return self
elif not self.is_individual and self.parent:
return self.parent
return None
class Meta:
abstract = True
class GroupManager(models.Manager):
def wgs(self):
return self.get_queryset().filter(type='wg')
def active_wgs(self):
return self.get_queryset().filter(type='wg', state__in=('bof','proposed','active'))
return self.wgs().filter(state__in=Group.ACTIVE_STATE_IDS)
def closed_wgs(self):
return self.wgs().exclude(state__in=Group.ACTIVE_STATE_IDS)
class Group(GroupInfo):
objects = GroupManager()
@ -114,6 +143,13 @@ class Group(GroupInfo):
chair = self.role_set.filter(name__slug='chair')[:1]
return chair and chair[0] or None
@property
def ads(self):
return sorted(
self.role_set.filter(name="ad").select_related("email", "person"),
key=lambda role: role.person.name_parts()[3], # gets last name
)
# these are copied to Group because it is still proxied.
@property
def upcase_acronym(self):

View file

@ -221,7 +221,6 @@ def wg_summary_area(request, group_type):
raise Http404
areas = Group.objects.filter(type="area", state="active").order_by("name")
for area in areas:
area.ads = sorted(roles(area, "ad"), key=extract_last_name)
area.groups = Group.objects.filter(parent=area, type="wg", state="active").order_by("acronym")
for group in area.groups:
group.chairs = sorted(roles(group, "chair"), key=extract_last_name)
@ -250,13 +249,11 @@ def wg_charters(request, group_type):
raise Http404
areas = Group.objects.filter(type="area", state="active").order_by("name")
for area in areas:
area.ads = sorted(roles(area, "ad"), key=extract_last_name)
area.groups = Group.objects.filter(parent=area, type="wg", state="active").order_by("name")
for group in area.groups:
fill_in_charter_info(group)
fill_in_wg_roles(group)
fill_in_wg_drafts(group)
group.area = area
return render(request, 'group/1wg-charters.txt',
{ 'areas': areas },
content_type='text/plain; charset=UTF-8')
@ -265,17 +262,12 @@ def wg_charters(request, group_type):
def wg_charters_by_acronym(request, group_type):
if group_type != "wg":
raise Http404
areas = dict((a.id, a) for a in Group.objects.filter(type="area", state="active").order_by("name"))
for area in areas.values():
area.ads = sorted(roles(area, "ad"), key=extract_last_name)
groups = Group.objects.filter(type="wg", state="active").exclude(parent=None).order_by("acronym")
for group in groups:
fill_in_charter_info(group)
fill_in_wg_roles(group)
fill_in_wg_drafts(group)
group.area = areas.get(group.parent_id)
return render(request, 'group/1wg-charters-by-acronym.txt',
{ 'groups': groups },
content_type='text/plain; charset=UTF-8')
@ -313,7 +305,6 @@ def active_dirs(request):
dirs = Group.objects.filter(type__in=['dir', 'review'], state="active").order_by("name")
for group in dirs:
group.chairs = sorted(roles(group, "chair"), key=extract_last_name)
group.ads = sorted(roles(group, "ad"), key=extract_last_name)
group.secretaries = sorted(roles(group, "secr"), key=extract_last_name)
return render(request, 'group/active_dirs.html', {'dirs' : dirs })
@ -321,7 +312,6 @@ def active_review_dirs(request):
dirs = Group.objects.filter(type="review", state="active").order_by("name")
for group in dirs:
group.chairs = sorted(roles(group, "chair"), key=extract_last_name)
group.ads = sorted(roles(group, "ad"), key=extract_last_name)
group.secretaries = sorted(roles(group, "secr"), key=extract_last_name)
return render(request, 'group/active_review_dirs.html', {'dirs' : dirs })
@ -345,14 +335,15 @@ def active_wgs(request):
areas = Group.objects.filter(type="area", state="active").order_by("name")
for area in areas:
# dig out information for template
area.ads = (list(sorted(roles(area, "ad"), key=extract_last_name))
+ list(sorted(roles(area, "pre-ad"), key=extract_last_name)))
area.ads_and_pre_ads = (
list(area.ads) + list(sorted(roles(area, "pre-ad"), key=extract_last_name))
)
area.groups = Group.objects.filter(parent=area, type="wg", state="active").order_by("acronym")
area.urls = area.groupextresource_set.all().order_by("name")
for group in area.groups:
group.chairs = sorted(roles(group, "chair"), key=extract_last_name)
group.ad_out_of_area = group.ad_role() and group.ad_role().person not in [role.person for role in area.ads]
group.ad_out_of_area = group.ad_role() and group.ad_role().person not in [role.person for role in area.ads_and_pre_ads]
# get the url for mailing list subscription
if group.list_subscribe.startswith('http'):
group.list_subscribe_url = group.list_subscribe
@ -378,7 +369,6 @@ def active_ags(request):
groups = Group.objects.filter(type="ag", state="active").order_by("acronym")
for group in groups:
group.chairs = sorted(roles(group, "chair"), key=extract_last_name)
group.ads = sorted(roles(group, "ad"), key=extract_last_name)
return render(request, 'group/active_ags.html', { 'groups': groups })

View file

@ -0,0 +1,58 @@
# Generated by Django 2.2.17 on 2020-11-23 09:54
from django.db import migrations
def forward(apps, schema_editor):
"""Add new MailTrigger and Recipients, remove the one being replaced"""
MailTrigger = apps.get_model('mailtrigger', 'MailTrigger')
Recipient = apps.get_model('mailtrigger', 'Recipient')
MailTrigger.objects.create(
slug='sub_director_approval_requested',
desc='Recipients for a message requesting AD approval of a revised draft submission',
).to.add(
Recipient.objects.create(
pk='sub_group_parent_directors',
desc="ADs for the parent group of a submission"
)
)
MailTrigger.objects.create(
slug='sub_replaced_doc_chair_approval_requested',
desc='Recipients for a message requesting chair approval of a replaced WG document',
).to.add(
Recipient.objects.get(pk='doc_group_chairs')
)
MailTrigger.objects.create(
slug='sub_replaced_doc_director_approval_requested',
desc='Recipients for a message requesting AD approval of a replaced WG document',
).to.add(
Recipient.objects.create(
pk='doc_group_parent_directors',
desc='ADs for the parent group of a doc'
)
)
def reverse(apps, schema_editor):
"""Remove the MailTrigger and Recipient created in the forward migration"""
MailTrigger = apps.get_model('mailtrigger', 'MailTrigger')
MailTrigger.objects.filter(slug='sub_director_approval_requested').delete()
MailTrigger.objects.filter(slug='sub_replaced_doc_chair_approval_requested').delete()
MailTrigger.objects.filter(slug='sub_replaced_doc_director_approval_requested').delete()
Recipient = apps.get_model('mailtrigger', 'Recipient')
Recipient.objects.filter(pk='sub_group_parent_directors').delete()
Recipient.objects.filter(pk='doc_group_parent_directors').delete()
class Migration(migrations.Migration):
dependencies = [
('mailtrigger', '0019_email_iana_expert_review_state_changed'),
]
operations = [
migrations.RunPython(forward, reverse)
]

View file

@ -242,6 +242,27 @@ class Recipient(models.Model):
addrs.extend(Recipient.objects.get(slug='group_chairs').gather(**{'group':submission.group}))
return addrs
def gather_sub_group_parent_directors(self, **kwargs):
addrs = []
if 'submission' in kwargs:
submission = kwargs['submission']
if submission.group and submission.group.parent:
addrs.extend(
Recipient.objects.get(
slug='group_responsible_directors').gather(group=submission.group.parent)
)
return addrs
def gather_doc_group_parent_directors(self, **kwargs):
addrs = []
doc = kwargs.get('doc')
if doc and doc.group and doc.group.parent:
addrs.extend(
Recipient.objects.get(
slug='group_responsible_directors').gather(group=doc.group.parent)
)
return addrs
def gather_submission_confirmers(self, **kwargs):
"""If a submitted document is revising an existing document, the confirmers
are the authors of that existing document, and the chairs if the document is

View file

@ -4890,6 +4890,17 @@
"model": "mailtrigger.mailtrigger",
"pk": "sub_confirmation_requested"
},
{
"fields": {
"cc": [],
"desc": "Recipients for a message requesting AD approval of a revised draft submission",
"to": [
"sub_group_parent_directors"
]
},
"model": "mailtrigger.mailtrigger",
"pk": "sub_director_approval_requested"
},
{
"fields": {
"cc": [],
@ -4942,6 +4953,28 @@
"model": "mailtrigger.mailtrigger",
"pk": "sub_new_wg_00"
},
{
"fields": {
"cc": [],
"desc": "Recipients for a message requesting chair approval of a replaced WG document",
"to": [
"doc_group_chairs"
]
},
"model": "mailtrigger.mailtrigger",
"pk": "sub_replaced_doc_chair_approval_requested"
},
{
"fields": {
"cc": [],
"desc": "Recipients for a message requesting AD approval of a replaced WG document",
"to": [
"doc_group_parent_directors"
]
},
"model": "mailtrigger.mailtrigger",
"pk": "sub_replaced_doc_director_approval_requested"
},
{
"fields": {
"desc": "The person providing a comment to nomcom",
@ -5046,6 +5079,14 @@
"model": "mailtrigger.recipient",
"pk": "doc_group_mail_list"
},
{
"fields": {
"desc": "ADs for the parent group of a doc",
"template": null
},
"model": "mailtrigger.recipient",
"pk": "doc_group_parent_directors"
},
{
"fields": {
"desc": "The document's group's responsible AD(s) or IRTF chair",
@ -5534,6 +5575,14 @@
"model": "mailtrigger.recipient",
"pk": "stream_managers"
},
{
"fields": {
"desc": "ADs for the parent group of a submission",
"template": null
},
"model": "mailtrigger.recipient",
"pk": "sub_group_parent_directors"
},
{
"fields": {
"desc": "The authors of a submitted draft",
@ -9702,6 +9751,20 @@
"model": "name.docurltagname",
"pk": "yang-module-metadata"
},
{
"fields": {
"desc": "",
"name": "Awaiting AD Approval",
"next_states": [
"cancel",
"posted"
],
"order": 5,
"used": true
},
"model": "name.draftsubmissionstatename",
"pk": "ad-appr"
},
{
"fields": {
"desc": "",
@ -9737,7 +9800,7 @@
"desc": "",
"name": "Cancelled",
"next_states": [],
"order": 6,
"order": 7,
"used": true
},
"model": "name.draftsubmissionstatename",
@ -9779,7 +9842,7 @@
"cancel",
"posted"
],
"order": 5,
"order": 6,
"used": true
},
"model": "name.draftsubmissionstatename",
@ -9790,7 +9853,7 @@
"desc": "",
"name": "Posted",
"next_states": [],
"order": 7,
"order": 8,
"used": true
},
"model": "name.draftsubmissionstatename",
@ -9821,7 +9884,7 @@
"cancel",
"posted"
],
"order": 8,
"order": 9,
"used": true
},
"model": "name.draftsubmissionstatename",

View file

@ -0,0 +1,70 @@
# Generated by Django 2.2.17 on 2020-11-18 07:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('name', '0020_add_rescheduled_session_name'),
]
def up(apps, schema_editor):
DraftSubmissionStateName = apps.get_model('name', 'DraftSubmissionStateName')
new_state_name = DraftSubmissionStateName.objects.create(
slug='ad-appr',
name='Awaiting AD Approval',
used=True,
)
new_state_name.next_states.add(DraftSubmissionStateName.objects.get(slug='posted'))
new_state_name.next_states.add(DraftSubmissionStateName.objects.get(slug='cancel'))
DraftSubmissionStateName.objects.get(slug='uploaded').next_states.add(new_state_name)
# Order so the '-appr' states are together
for slug, order in (
('confirmed', 0),
('uploaded', 1),
('auth', 2),
('aut-appr', 3),
('grp-appr', 4),
('ad-appr', 5),
('manual', 6),
('cancel', 7),
('posted', 8),
('waiting-for-draft', 9),
):
state_name = DraftSubmissionStateName.objects.get(slug=slug)
state_name.order = order
state_name.save()
def down(apps, schema_editor):
DraftSubmissionStateName = apps.get_model('name', 'DraftSubmissionStateName')
Submission = apps.get_model('submit', 'Submission')
name_to_delete = DraftSubmissionStateName.objects.get(slug='ad-appr')
# Refuse to migrate if there are any Submissions using the state we're about to remove
assert(Submission.objects.filter(state=name_to_delete).count() == 0)
DraftSubmissionStateName.objects.get(slug='uploaded').next_states.remove(name_to_delete)
name_to_delete.delete()
# restore original order
for slug, order in (
('confirmed', 0),
('uploaded', 1),
('auth', 2),
('aut-appr', 3),
('grp-appr', 4),
('manual', 5),
('cancel', 6),
('posted', 7),
('waiting-for-draft', 8),
):
state_name = DraftSubmissionStateName.objects.get(slug=slug)
state_name.order = order
state_name.save()
operations = [
migrations.RunPython(up, down),
]

View file

@ -67,23 +67,49 @@ def send_full_url(request, submission):
all_addrs.extend(cc)
return all_addrs
def send_approval_request_to_group(request, submission):
def send_approval_request(request, submission, replaced_doc=None):
"""Send an approval request for a submission
If replaced_doc is not None, requests will be sent to the wg chairs or ADs
responsible for that doc's group instead of the submission.
"""
subject = 'New draft waiting for approval: %s' % submission.name
from_email = settings.IDSUBMIT_FROM_EMAIL
(to_email,cc) = gather_address_lists('sub_chair_approval_requested',submission=submission)
# Sort out which MailTrigger to use
mt_kwargs = dict(submission=submission)
if replaced_doc:
mt_kwargs['doc'] = replaced_doc
if submission.state_id == 'ad-appr':
approval_type = 'ad'
if replaced_doc:
mt_slug = 'sub_replaced_doc_director_approval_requested'
else:
mt_slug = 'sub_director_approval_requested'
else:
approval_type = 'chair'
if replaced_doc:
mt_slug = 'sub_replaced_doc_chair_approval_requested'
else:
mt_slug = 'sub_chair_approval_requested'
(to_email,cc) = gather_address_lists(mt_slug, **mt_kwargs)
if not to_email:
return to_email
send_mail(request, to_email, from_email, subject, 'submit/approval_request.txt',
send_mail(request, to_email, from_email, subject, 'submit/approval_request.txt',
{
'submission': submission,
'domain': Site.objects.get_current().domain,
'submission': submission,
'domain': Site.objects.get_current().domain,
'approval_type': approval_type,
},
cc=cc)
all_addrs = to_email
all_addrs.extend(cc)
return all_addrs
def send_manual_post_request(request, submission, errors):
subject = 'Manual Post Requested for %s' % submission.name
from_email = settings.IDSUBMIT_FROM_EMAIL

View file

@ -77,7 +77,42 @@ class Submission(models.Model):
def has_yang(self):
return any ( [ c.checker=='yang validation' and c.passed is not None for c in self.latest_checks()] )
@property
def replaces_names(self):
return self.replaces.split(',')
@property
def area(self):
return self.group.area if self.group else None
@property
def is_individual(self):
return self.group.is_individual if self.group else True
@property
def revises_wg_draft(self):
return (
self.rev != '00'
and self.group
and self.group.is_wg
)
@property
def active_wg_drafts_replaced(self):
return Document.objects.filter(
docalias__name__in=self.replaces.split(','),
group__in=Group.objects.active_wgs()
)
@property
def closed_wg_drafts_replaced(self):
return Document.objects.filter(
docalias__name__in=self.replaces.split(','),
group__in=Group.objects.closed_wgs()
)
class SubmissionCheck(models.Model):
time = models.DateTimeField(default=datetime.datetime.now)
submission = ForeignKey(Submission, related_name='checks')

View file

@ -379,6 +379,84 @@ class SubmitTests(TestCase):
def text_submit_new_wg_txt_xml(self):
self.submit_new_wg(["txt", "xml"])
def test_submit_new_wg_as_author(self):
"""A new WG submission by a logged-in author needs chair approval"""
# submit new -> supply submitter info -> approve
mars = GroupFactory(type_id='wg', acronym='mars')
draft = WgDraftFactory(group=mars)
setup_default_community_list_for_group(draft.group)
name = "draft-ietf-mars-testing-tests"
rev = "00"
group = "mars"
status_url, author = self.do_submission(name, rev, group)
username = author.user.email
# supply submitter info, then draft should be in and ready for approval
mailbox_before = len(outbox)
self.client.login(username=username, password=username+'+password') # log in as the author
r = self.supply_extra_metadata(name, status_url, author.ascii, author.email().address.lower(), replaces='')
self.assertEqual(r.status_code, 302)
status_url = r["Location"]
# Draft should be in the 'grp-appr' state to await approval by WG chair
submission = Submission.objects.get(name=name, rev=rev)
self.assertEqual(submission.state_id, 'grp-appr')
# Approval request notification should be sent to the WG chair
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("New draft waiting for approval" in outbox[-1]["Subject"])
self.assertTrue(name in outbox[-1]["Subject"])
self.assertTrue('mars-chairs@ietf.org' in outbox[-1]['To'])
# Status page should show that group chair approval is needed
r = self.client.get(status_url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'The submission is pending approval by the group chairs.')
def submit_new_concluded_wg_as_author(self, group_state_id='conclude'):
"""A new concluded WG submission by a logged-in author needs AD approval"""
mars = GroupFactory(type_id='wg', acronym='mars', state_id=group_state_id)
draft = WgDraftFactory(group=mars)
setup_default_community_list_for_group(draft.group)
name = "draft-ietf-mars-testing-tests"
rev = "00"
group = "mars"
status_url, author = self.do_submission(name, rev, group)
username = author.user.email
# Should receive an error because group is not active
self.client.login(username=username, password=username+'+password') # log in as the author
r = self.client.get(status_url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'Group exists but is not an active group')
q = PyQuery(r.content)
post_button = q('[type=submit]:contains("Post")')
self.assertEqual(len(post_button), 0) # no UI option to post the submission in this state
# Try to post anyway
r = self.client.post(status_url,
{'submitter-name': author.name,
'submitter-email': username,
'action': 'autopost',
'replaces': ''})
# Attempt should fail and draft should remain in the uploaded state
self.assertEqual(r.status_code, 403)
submission = Submission.objects.get(name=name, rev=rev)
self.assertEqual(submission.state_id, 'uploaded')
def test_submit_new_concluded_wg_as_author(self):
self.submit_new_concluded_wg_as_author()
def test_submit_new_bofconc_wg_as_author(self):
self.submit_new_concluded_wg_as_author('bof-conc')
def test_submit_new_replaced_wg_as_author(self):
self.submit_new_concluded_wg_as_author('replaced')
def submit_existing(self, formats, change_authors=True, group_type='wg', stream_type='ietf'):
# submit new revision of existing -> supply submitter info -> prev authors confirm
if stream_type == 'ietf':
@ -592,6 +670,67 @@ class SubmitTests(TestCase):
def test_submit_existing_iab(self):
self.submit_existing(["txt"],stream_type='iab', group_type='individ')
def do_submit_existing_concluded_wg_test(self, group_state_id='conclude', submit_as_author=False):
"""A revision to an existing WG draft should go to AD for approval if WG is not active"""
ad = Person.objects.get(user__username='ad')
area = GroupFactory(type_id='area')
RoleFactory(name_id='ad', group=area, person=ad)
group = GroupFactory(type_id='wg', state_id=group_state_id, parent=area, acronym='mars')
author = PersonFactory()
draft = WgDraftFactory(group=group, authors=[author])
draft.set_state(State.objects.get(type_id='draft-stream-ietf', slug='wg-doc'))
name = draft.name
rev = "%02d" % (int(draft.rev) + 1)
if submit_as_author:
username = author.user.email
self.client.login(username=username, password=username + '+password')
status_url, author = self.do_submission(name, rev, group, author=author)
mailbox_before = len(outbox)
# Try to post anyway
r = self.client.post(status_url,
{'submitter-name': author.name,
'submitter-email': 'submitter@example.com',
'action': 'autopost',
'replaces': ''})
self.assertEqual(r.status_code, 302)
status_url = r["Location"]
# Draft should be in the 'ad-appr' state to await approval
submission = Submission.objects.get(name=name, rev=rev)
self.assertEqual(submission.state_id, 'ad-appr')
# Approval request notification should be sent to the AD for the group
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("New draft waiting for approval" in outbox[-1]["Subject"])
self.assertTrue(name in outbox[-1]["Subject"])
self.assertTrue(ad.user.email in outbox[-1]['To'])
# Status page should show that AD approval is needed
r = self.client.get(status_url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'The submission is pending approval by the area director.')
def test_submit_existing_concluded_wg(self):
self.do_submit_existing_concluded_wg_test()
def test_submit_existing_concluded_wg_as_author(self):
self.do_submit_existing_concluded_wg_test(submit_as_author=True)
def test_submit_existing_bofconc_wg(self):
self.do_submit_existing_concluded_wg_test(group_state_id='bof-conc')
def test_submit_existing_bofconc_wg_as_author(self):
self.do_submit_existing_concluded_wg_test(group_state_id='bof-conc', submit_as_author=True)
def test_submit_existing_replaced_wg(self):
self.do_submit_existing_concluded_wg_test(group_state_id='replaced')
def test_submit_existing_replaced_wg_as_author(self):
self.do_submit_existing_concluded_wg_test(group_state_id='replaced', submit_as_author=True)
def submit_new_individual(self, formats):
# submit new -> supply submitter info -> confirm
@ -737,6 +876,51 @@ class SubmitTests(TestCase):
self.assertContains(r, draft.name)
self.assertContains(r, draft.title)
def submit_new_individual_replacing_wg(self, logged_in=False, group_state_id='active', notify_ad=False):
"""Chair of an active WG should be notified if individual draft is proposed to replace a WG draft"""
name = "draft-authorname-testing-tests"
rev = "00"
group = None
status_url, author = self.do_submission(name, rev, group)
ad = Person.objects.get(user__username='ad')
replaced_draft = WgDraftFactory(group__state_id=group_state_id)
RoleFactory(name_id='ad', group=replaced_draft.group.parent, person=ad)
if logged_in:
username = author.user.email
self.client.login(username=username, password=username + '+password')
mailbox_before = len(outbox)
self.supply_extra_metadata(
name,
status_url,
"Submitter Name",
"submitter@example.com",
replaces=str(replaced_draft.docalias.first().pk),
)
submission = Submission.objects.get(name=name, rev=rev)
self.assertEqual(submission.state_id, 'ad-appr' if notify_ad else 'grp-appr')
self.assertEqual(len(outbox), mailbox_before + 1)
notice = outbox[-1]
self.assertIn(
ad.user.email if notify_ad else '%s-chairs@ietf.org' % replaced_draft.group.acronym,
notice['To']
)
self.assertIn('New draft waiting for approval', notice['Subject'])
def test_submit_new_individual_replacing_wg(self):
self.submit_new_individual_replacing_wg()
def test_submit_new_individual_replacing_wg_logged_in(self):
self.submit_new_individual_replacing_wg(logged_in=True)
def test_submit_new_individual_replacing_concluded_wg(self):
self.submit_new_individual_replacing_wg(group_state_id='conclude', notify_ad=True)
def test_submit_new_individual_replacing_concluded_wg_logged_in(self):
self.submit_new_individual_replacing_wg(group_state_id='conclude', notify_ad=True, logged_in=True)
def test_submit_cancel_confirmation(self):
ad=Person.objects.get(user__username='ad')
# Group of None here does not reflect real individual submissions
@ -1338,12 +1522,110 @@ class SubmitTests(TestCase):
"""An existing SubmissionDocEvent with later rev should trigger manual processing"""
self.submit_conflicting_submissiondocevent_rev('01', '02')
def do_wg_approval_auth_test(self, state, chair_can_approve=False):
"""Helper to test approval authorization
Assumes approval allowed by AD and secretary and, optionally, chair of WG
"""
class _SubmissionFactory:
"""Helper class to generate fresh submissions"""
def __init__(self, author, state):
self.author = author
self.state = state
self.index = 0
def next(self):
self.index += 1
sub = Submission.objects.create(name="draft-ietf-mars-bar-%d" % self.index,
group=Group.objects.get(acronym="mars"),
submission_date=datetime.date.today(),
authors=[dict(name=self.author.name,
email=self.author.user.email,
affiliation='affiliation',
country='country')],
rev="00",
state_id=self.state)
status_url = urlreverse('ietf.submit.views.submission_status',
kwargs=dict(submission_id=sub.pk,
access_token=sub.access_token()))
return sub, status_url
def _assert_approval_refused(username, submission_factory, user_description):
"""Helper to attempt to approve a document and check that it fails"""
if username:
self.client.login(username=username, password=username + '+password')
submission, status_url = submission_factory.next()
r = self.client.post(status_url, dict(action='approve'))
self.assertEqual(r.status_code, 403, '%s should not be able to approve' % user_description.capitalize())
submission = Submission.objects.get(pk=submission.pk) # refresh from DB
self.assertEqual(submission.state_id, state,
'Submission should still be awaiting approval after %s approval attempt fails' % user_description)
def _assert_approval_allowed(username, submission_factory, user_description):
"""Helper to attempt to approve a document and check that it succeeds"""
self.client.login(username=username, password=username + '+password')
submission, status_url = submission_factory.next()
r = self.client.post(status_url, dict(action='approve'))
self.assertEqual(r.status_code, 302, '%s should be able to approve' % user_description.capitalize())
submission = Submission.objects.get(pk=submission.pk) # refresh from DB
self.assertEqual(submission.state_id, 'posted',
'Submission should be posted after %s approves' % user_description)
# create WGs
area = GroupFactory(type_id='area', acronym='area')
mars = GroupFactory(type_id='wg', acronym='mars', parent=area) # WG for submission
ames = GroupFactory(type_id='wg', acronym='ames', parent=area) # another WG
# create / get users and roles
ad = Person.objects.get(user__username='ad')
RoleFactory(name_id='ad', group=area, person=ad)
RoleFactory(name_id='chair', group=mars, person__user__username='marschairman')
RoleFactory(name_id='chair', group=ames, person__user__username='ameschairman')
author = PersonFactory(user__username='author_user')
PersonFactory(user__username='ordinary_user')
submission_factory = _SubmissionFactory(author, state)
# Most users should not be allowed to approve
_assert_approval_refused(None, submission_factory, 'anonymous user')
_assert_approval_refused('ordinary_user', submission_factory, 'ordinary user')
_assert_approval_refused('author_user', submission_factory, 'author')
_assert_approval_refused('ameschairman', submission_factory, 'wrong WG chair')
# chair of correct wg should be able to approve if chair_can_approve == True
if chair_can_approve:
_assert_approval_allowed('marschairman', submission_factory, 'WG chair')
else:
_assert_approval_refused('marschairman', submission_factory, 'WG chair')
# ADs and secretaries can always approve
_assert_approval_allowed('ad', submission_factory, 'AD')
_assert_approval_allowed('secretary', submission_factory, 'secretary')
def test_submit_wg_group_approval_auth(self):
"""Group chairs should be able to approve submissions in grp-appr state"""
self.do_wg_approval_auth_test('grp-appr', chair_can_approve=True)
def test_submit_wg_ad_approval_auth(self):
"""Area directors should be able to approve submissions in ad-appr state"""
self.do_wg_approval_auth_test('ad-appr', chair_can_approve=False)
class ApprovalsTestCase(TestCase):
def test_approvals(self):
RoleFactory(name_id='chair', group__acronym='mars', person__user__username='marschairman')
RoleFactory(name_id='chair',
group__acronym='mars',
person__user__username='marschairman')
RoleFactory(name_id='chair',
group__acronym='ames',
person__user__username='ameschairman')
RoleFactory(name_id='ad',
group=Group.objects.get(acronym='mars').parent,
person=Person.objects.get(user__username='ad'))
RoleFactory(name_id='ad',
group=Group.objects.get(acronym='ames').parent,
person__user__username='other-ad')
url = urlreverse('ietf.submit.views.approvals')
self.client.login(username="marschairman", password="marschairman+password")
Preapproval.objects.create(name="draft-ietf-mars-foo", by=Person.objects.get(user__username="marschairman"))
Preapproval.objects.create(name="draft-ietf-mars-baz", by=Person.objects.get(user__username="marschairman"))
@ -1358,17 +1640,101 @@ class ApprovalsTestCase(TestCase):
submission_date=datetime.date.today(),
rev="00",
state_id="grp-appr")
Submission.objects.create(name="draft-ietf-mars-quux",
group=Group.objects.get(acronym="mars"),
submission_date=datetime.date.today(),
rev="00",
state_id="ad-appr")
# get
# get as wg chair
self.client.login(username="marschairman", password="marschairman+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-foo")')), 0)
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-bar")')), 1)
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-quux")')), 0) # wg chair does not see ad-appr
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-foo")')), 0)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-baz")')), 1)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-bar")')), 0)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-quux")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-foo")')), 1)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-baz")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-bar")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-quux")')), 0)
# get as AD - sees everything
self.client.login(username="ad", password="ad+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-foo")')), 0)
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-bar")')), 1) # AD sees grp-appr in their area
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-quux")')), 1) # AD does see ad-appr
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-foo")')), 0)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-baz")')), 1)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-bar")')), 0)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-quux")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-foo")')), 1)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-baz")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-bar")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-quux")')), 0)
# get as wg chair for a different group - should see nothing
self.client.login(username="ameschairman", password="ameschairman+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-foo")')), 0)
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-bar")')), 0)
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-quux")')), 0)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-foo")')), 0)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-baz")')), 0)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-bar")')), 0)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-quux")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-foo")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-baz")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-bar")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-quux")')), 0)
# get as AD for a different area - should see nothing
self.client.login(username="other-ad", password="other-ad+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-foo")')), 0)
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-bar")')), 0)
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-quux")')), 0)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-foo")')), 0)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-baz")')), 0)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-bar")')), 0)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-quux")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-foo")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-baz")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-bar")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-quux")')), 0)
# get as secretary - should see everything
self.client.login(username="secretary", password="secretary+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-foo")')), 0)
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-bar")')), 1)
self.assertEqual(len(q('.approvals a:contains("draft-ietf-mars-quux")')), 1)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-foo")')), 0)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-baz")')), 1)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-bar")')), 0)
self.assertEqual(len(q('.preapprovals td:contains("draft-ietf-mars-quux")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-foo")')), 1)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-baz")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-bar")')), 0)
self.assertEqual(len(q('.recently-approved a:contains("draft-ietf-mars-quux")')), 0)
def test_add_preapproval(self):
RoleFactory(name_id='chair', group__acronym='mars', person__user__username='marschairman')

View file

@ -25,7 +25,7 @@ from ietf.doc.models import ( Document, State, DocAlias, DocEvent, SubmissionDoc
from ietf.doc.models import NewRevisionDocEvent
from ietf.doc.models import RelatedDocument, DocRelationshipName
from ietf.doc.utils import add_state_change_event, rebuild_reference_relations
from ietf.doc.utils import set_replaces_for_document
from ietf.doc.utils import set_replaces_for_document, prettify_std_name
from ietf.doc.mails import send_review_possibly_replaces_request
from ietf.group.models import Group
from ietf.ietfauth.utils import has_role
@ -33,7 +33,7 @@ from ietf.name.models import StreamName, FormalLanguageName
from ietf.person.models import Person, Email
from ietf.community.utils import update_name_contains_indexes_with_new_doc
from ietf.submit.mail import ( announce_to_lists, announce_new_version, announce_to_authors,
send_approval_request_to_group, send_submission_confirmation, announce_new_wg_00 )
announce_new_wg_00, send_approval_request, send_submission_confirmation )
from ietf.submit.models import Submission, SubmissionEvent, Preapproval, DraftSubmissionStateName, SubmissionCheck
from ietf.utils import log
from ietf.utils.accesstoken import generate_random_key
@ -605,12 +605,19 @@ def approvable_submissions_for_user(user):
if not user.is_authenticated:
return []
res = Submission.objects.filter(state="grp-appr").order_by('-submission_date')
# Submissions that are group / AD approvable by someone
group_approvable = Submission.objects.filter(state="grp-appr")
ad_approvable = Submission.objects.filter(state="ad-appr")
if has_role(user, "Secretariat"):
return res
return (group_approvable | ad_approvable).order_by('-submission_date')
# those we can reach as chair
return res.filter(group__role__name="chair", group__role__person__user=user)
# group-approvable that we can reach as chair plus group-approvable that we can reach as AD
# plus AD-approvable that we can reach as ad
return (
group_approvable.filter(group__role__name="chair", group__role__person__user=user)
| group_approvable.filter(group__parent__role__name="ad", group__parent__role__person__user=user)
| ad_approvable.filter(group__parent__role__name="ad", group__parent__role__person__user=user)
).order_by('-submission_date')
def preapprovals_for_user(user):
if not user.is_authenticated:
@ -621,7 +628,11 @@ def preapprovals_for_user(user):
if has_role(user, "Secretariat"):
return res
acronyms = [g.acronym for g in Group.objects.filter(role__person__user=user, type__features__req_subm_approval=True)]
accessible_groups = (
Group.objects.filter(role__person__user=user, type__features__req_subm_approval=True)
| Group.objects.filter(parent__role__name='ad', parent__role__person__user=user, type__features__req_subm_approval=True)
)
acronyms = [g.acronym for g in accessible_groups]
res = res.filter(name__regex="draft-[^-]+-(%s)-.*" % "|".join(acronyms))
@ -635,8 +646,11 @@ def recently_approved_by_user(user, since):
if has_role(user, "Secretariat"):
return res
# those we can reach as chair
return res.filter(group__role__name="chair", group__role__person__user=user)
# those we can reach as chair or ad
return (
res.filter(group__role__name="chair", group__role__person__user=user)
| res.filter(group__parent__role__name="ad", group__parent__role__person__user=user)
)
def expirable_submissions(older_than_days):
cutoff = datetime.date.today() - datetime.timedelta(days=older_than_days)
@ -794,26 +808,107 @@ def apply_checkers(submission, file_name):
tau = time.time() - mark
log.log(f"ran submission checks ({tau:.3}s) for {file_name}")
def send_confirmation_emails(request, submission, requires_group_approval, requires_prev_authors_approval):
docevent_from_submission(request, submission, desc="Uploaded new revision")
def accept_submission_requires_prev_auth_approval(submission):
"""Does acceptance process require approval of previous authors?"""
return Document.objects.filter(name=submission.name).exists()
if requires_group_approval:
def accept_submission_requires_group_approval(submission):
"""Does acceptance process require group approval?
Depending on the state of the group, this approval may come from group chair or area director.
"""
return (
submission.rev == '00'
and submission.group and submission.group.features.req_subm_approval
and not Preapproval.objects.filter(name=submission.name).exists()
)
def accept_submission(request, submission, autopost=False):
"""Accept a submission and post or put in correct state to await approvals
If autopost is True, will post draft if submitter is authorized to do so.
"""
doc = submission.existing_document()
prev_authors = [] if not doc else [ author.person for author in doc.documentauthor_set.all() ]
curr_authors = [ get_person_from_name_email(author["name"], author.get("email"))
for author in submission.authors ]
# Is the user authenticated as an author who can approve this submission?
user_is_author = (
request.user.is_authenticated
and request.user.person in (prev_authors if submission.rev != '00' else curr_authors) # type: ignore
)
# If "who" is None, docevent_from_submission will pull it out of submission
docevent_from_submission(
request,
submission,
desc="Uploaded new revision",
who=request.user.person if user_is_author else None,
)
replaces = DocAlias.objects.filter(name__in=submission.replaces_names)
pretty_replaces = '(none)' if not replaces else (
', '.join(prettify_std_name(r.name) for r in replaces)
)
active_wg_drafts_replaced = submission.active_wg_drafts_replaced
closed_wg_drafts_replaced = submission.closed_wg_drafts_replaced
# Determine which approvals are required
requires_prev_authors_approval = accept_submission_requires_prev_auth_approval(submission)
requires_group_approval = accept_submission_requires_group_approval(submission)
requires_ad_approval = submission.revises_wg_draft and not submission.group.is_active
if submission.is_individual:
requires_prev_group_approval = active_wg_drafts_replaced.exists()
requires_prev_ad_approval = closed_wg_drafts_replaced.exists()
else:
requires_prev_group_approval = False
requires_prev_ad_approval = False
# Partial message for submission event
sub_event_desc = 'Set submitter to \"%s\", replaces to %s' % (submission.submitter, pretty_replaces)
docevent_desc = None
address_list = []
if requires_ad_approval or requires_prev_ad_approval:
submission.state_id = "ad-appr"
submission.save()
if closed_wg_drafts_replaced.exists():
replaced_document = closed_wg_drafts_replaced.first()
else:
replaced_document = None
address_list = send_approval_request(
request,
submission,
replaced_document, # may be None
)
sent_to = ', '.join(address_list)
sub_event_desc += ' and sent approval email to AD: %s' % sent_to
docevent_desc = "Request for posting approval emailed to AD: %s" % sent_to
elif requires_group_approval or requires_prev_group_approval:
submission.state = DraftSubmissionStateName.objects.get(slug="grp-appr")
submission.save()
sent_to = send_approval_request_to_group(request, submission)
desc = "sent approval email to group chairs: %s" % ", ".join(sent_to)
docDesc = "Request for posting approval emailed to group chairs: %s" % ", ".join(sent_to)
if active_wg_drafts_replaced.exists():
replaced_document = active_wg_drafts_replaced.first()
else:
replaced_document = None
address_list = send_approval_request(
request,
submission,
replaced_document, # may be None
)
sent_to = ', '.join(address_list)
sub_event_desc += ' and sent approval email to group chairs: %s' % sent_to
docevent_desc = "Request for posting approval emailed to group chairs: %s" % sent_to
elif user_is_author and autopost:
# go directly to posting submission
sub_event_desc = "New version accepted (logged-in submitter: %s)" % request.user.person # type: ignore
post_submission(request, submission, sub_event_desc, sub_event_desc)
sub_event_desc = None # do not create submission event below, post_submission() handles it
else:
group_authors_changed = False
doc = submission.existing_document()
if doc and doc.group:
old_authors = [ author.person for author in doc.documentauthor_set.all() ]
new_authors = [ get_person_from_name_email(author["name"], author.get("email")) for author in submission.authors ]
group_authors_changed = set(old_authors)!=set(new_authors)
submission.auth_key = generate_random_key()
if requires_prev_authors_approval:
submission.state = DraftSubmissionStateName.objects.get(slug="aut-appr")
@ -821,14 +916,29 @@ def send_confirmation_emails(request, submission, requires_group_approval, requi
submission.state = DraftSubmissionStateName.objects.get(slug="auth")
submission.save()
sent_to = send_submission_confirmation(request, submission, chair_notice=group_authors_changed)
group_authors_changed = False
doc = submission.existing_document()
if doc and doc.group:
old_authors = [ author.person for author in doc.documentauthor_set.all() ]
new_authors = [ get_person_from_name_email(author["name"], author.get("email")) for author in submission.authors ]
group_authors_changed = set(old_authors)!=set(new_authors)
address_list = send_submission_confirmation(
request,
submission,
chair_notice=group_authors_changed
)
sent_to = ', '.join(address_list)
if submission.state_id == "aut-appr":
desc = "sent confirmation email to previous authors: %s" % ", ".join(sent_to)
docDesc = "Request for posting confirmation emailed to previous authors: %s" % ", ".join(sent_to)
sub_event_desc += " and sent confirmation email to previous authors: %s" % sent_to
docevent_desc = "Request for posting confirmation emailed to previous authors: %s" % sent_to
else:
desc = "sent confirmation email to submitter and authors: %s" % ", ".join(sent_to)
docDesc = "Request for posting confirmation emailed to submitter and authors: %s" % ", ".join(sent_to)
return sent_to, desc, docDesc
sub_event_desc += " and sent confirmation email to submitter and authors: %s" % sent_to
docevent_desc = "Request for posting confirmation emailed to submitter and authors: %s" % sent_to
if sub_event_desc:
create_submission_event(request, submission, sub_event_desc)
if docevent_desc:
docevent_from_submission(request, submission, docevent_desc, who=Person.objects.get(name="(System)"))
return address_list

View file

@ -22,23 +22,23 @@ from django.views.decorators.csrf import csrf_exempt
import debug # pyflakes:ignore
from ietf.doc.models import Document, DocAlias, AddedMessageEvent
from ietf.doc.utils import prettify_std_name
from ietf.group.models import Group
from ietf.group.utils import group_features_group_filter
from ietf.ietfauth.utils import has_role, role_required
from ietf.mailtrigger.utils import gather_address_lists
from ietf.message.models import Message, MessageAttachment
from ietf.person.models import Person, Email
from ietf.person.models import Email
from ietf.submit.forms import ( SubmissionManualUploadForm, SubmissionAutoUploadForm, AuthorForm,
SubmitterForm, EditSubmissionForm, PreapprovalForm, ReplacesForm, SubmissionEmailForm, MessageModelForm )
from ietf.submit.mail import ( send_full_url, send_manual_post_request, add_submission_email, get_reply_to )
from ietf.submit.mail import send_full_url, send_manual_post_request, add_submission_email, get_reply_to
from ietf.submit.models import (Submission, Preapproval,
DraftSubmissionStateName, SubmissionEmailEvent )
from ietf.submit.utils import ( approvable_submissions_for_user, preapprovals_for_user,
recently_approved_by_user, validate_submission, create_submission_event, docevent_from_submission,
post_submission, cancel_submission, rename_submission_files, remove_submission_files, get_draft_meta,
get_submission, fill_in_submission, apply_checkers, send_confirmation_emails, save_files,
get_person_from_name_email, check_submission_revision_consistency )
get_submission, fill_in_submission, apply_checkers, save_files,
check_submission_revision_consistency, accept_submission, accept_submission_requires_group_approval,
accept_submission_requires_prev_auth_approval)
from ietf.stats.utils import clean_country_name
from ietf.utils.accesstoken import generate_access_token
from ietf.utils.log import log
@ -174,18 +174,7 @@ def api_submit(request):
raise ValidationError('Submitter %s is not one of the document authors' % user.username)
submission.submitter = user.person.formatted_email()
docevent_from_submission(request, submission, desc="Uploaded new revision")
requires_group_approval = (submission.rev == '00'
and submission.group and submission.group.features.req_subm_approval
and not Preapproval.objects.filter(name=submission.name).exists())
requires_prev_authors_approval = Document.objects.filter(name=submission.name)
sent_to, desc, docDesc = send_confirmation_emails(request, submission, requires_group_approval, requires_prev_authors_approval)
msg = "Set submitter to \"%s\" and %s" % (submission.submitter, desc)
create_submission_event(request, submission, msg)
docevent_from_submission(request, submission, docDesc, who=Person.objects.get(name="(System)"))
sent_to = accept_submission(request, submission)
return HttpResponse(
"Upload of %s OK, confirmation requests sent to:\n %s" % (submission.name, ',\n '.join(sent_to)),
@ -253,10 +242,14 @@ def submission_status(request, submission_id, access_token=None):
is_secretariat = has_role(request.user, "Secretariat")
is_chair = submission.group and submission.group.has_role(request.user, "chair")
area = submission.area
is_ad = area and area.has_role(request.user, "ad")
can_edit = can_edit_submission(request.user, submission, access_token) and submission.state_id == "uploaded"
can_cancel = (key_matched or is_secretariat) and submission.state.next_states.filter(slug="cancel")
can_group_approve = (is_secretariat or is_chair) and submission.state_id == "grp-appr"
can_group_approve = (is_secretariat or is_ad or is_chair) and submission.state_id == "grp-appr"
can_ad_approve = (is_secretariat or is_ad) and submission.state_id == "ad-appr"
can_force_post = (
is_secretariat
and submission.state.next_states.filter(slug="posted").exists()
@ -273,26 +266,18 @@ def submission_status(request, submission_id, access_token=None):
# Convert from RFC 2822 format if needed
confirmation_list = [ "%s <%s>" % parseaddr(a) for a in addresses ]
requires_group_approval = (submission.rev == '00'
and submission.group and submission.group.features.req_subm_approval
and not Preapproval.objects.filter(name=submission.name).exists())
requires_prev_authors_approval = Document.objects.filter(name=submission.name)
message = None
if submission.state_id == "cancel":
message = ('error', 'This submission has been cancelled, modification is no longer possible.')
elif submission.state_id == "auth":
message = ('success', 'The submission is pending email authentication. An email has been sent to: %s' % ", ".join(confirmation_list))
elif submission.state_id == "grp-appr":
message = ('success', 'The submission is pending approval by the group chairs.')
elif submission.state_id == "ad-appr":
message = ('success', 'The submission is pending approval by the area director.')
elif submission.state_id == "aut-appr":
message = ('success', 'The submission is pending approval by the authors of the previous version. An email has been sent to: %s' % ", ".join(confirmation_list))
submitter_form = SubmitterForm(initial=submission.submitter_parsed(), prefix="submitter")
replaces_form = ReplacesForm(name=submission.name,initial=DocAlias.objects.filter(name__in=submission.replaces.split(",")))
@ -313,6 +298,9 @@ def submission_status(request, submission_id, access_token=None):
approvals_received = submitter_form.cleaned_data['approvals_received']
if submission.rev == '00' and submission.group and not submission.group.is_active:
permission_denied(request, 'Posting a new draft for an inactive group is not permitted.')
if approvals_received:
if not is_secretariat:
permission_denied(request, 'You do not have permission to perform this action')
@ -324,26 +312,8 @@ def submission_status(request, submission_id, access_token=None):
post_submission(request, submission, desc, desc)
else:
doc = submission.existing_document()
prev_authors = [] if not doc else [ author.person for author in doc.documentauthor_set.all() ]
curr_authors = [ get_person_from_name_email(author["name"], author.get("email")) for author in submission.authors ]
accept_submission(request, submission, autopost=True)
if request.user.is_authenticated and request.user.person in (prev_authors if submission.rev != '00' else curr_authors): # type: ignore
# go directly to posting submission
docevent_from_submission(request, submission, desc="Uploaded new revision", who=request.user.person) # type: ignore
desc = "New version accepted (logged-in submitter: %s)" % request.user.person # type: ignore
post_submission(request, submission, desc, desc)
else:
sent_to, desc, docDesc = send_confirmation_emails(request, submission, requires_group_approval, requires_prev_authors_approval)
msg = "Set submitter to \"%s\", replaces to %s and %s" % (
submission.submitter,
", ".join(prettify_std_name(r.name) for r in replaces) if replaces else "(none)",
desc)
create_submission_event(request, submission, msg)
docevent_from_submission(request, submission, docDesc, who=Person.objects.get(name="(System)"))
if access_token:
return redirect("ietf.submit.views.submission_status", submission_id=submission.pk, access_token=access_token)
else:
@ -372,6 +342,13 @@ def submission_status(request, submission_id, access_token=None):
return redirect("ietf.submit.views.submission_status", submission_id=submission_id)
elif action == "approve" and submission.state_id == "ad-appr":
if not can_ad_approve:
permission_denied(request, 'You do not have permission to perform this action.')
post_submission(request, submission, "WG -00 approved", "Approved and posted submission")
return redirect("ietf.doc.views_doc.document_main", name=submission.name)
elif action == "approve" and submission.state_id == "grp-appr":
if not can_group_approve:
@ -381,7 +358,6 @@ def submission_status(request, submission_id, access_token=None):
return redirect("ietf.doc.views_doc.document_main", name=submission.name)
elif action == "forcepost" and submission.state.next_states.filter(slug="posted"):
if not can_force_post:
permission_denied(request, 'You do not have permission to perform this action.')
@ -416,8 +392,8 @@ def submission_status(request, submission_id, access_token=None):
'can_group_approve': can_group_approve,
'can_cancel': can_cancel,
'show_send_full_url': show_send_full_url,
'requires_group_approval': requires_group_approval,
'requires_prev_authors_approval': requires_prev_authors_approval,
'requires_group_approval': accept_submission_requires_group_approval(submission),
'requires_prev_authors_approval': accept_submission_requires_prev_auth_approval(submission),
'confirmation_list': confirmation_list,
})

View file

@ -24,10 +24,10 @@
{% for area in areas %}
<h2 class="anchor-target" id="{{area.acronym}}">{{ area.name }} ({{ area.acronym }})</h2>
{% if area.ads %}
<h3>{{ area.acronym }} Area Director{{ area.ads|pluralize }} (AD{{ area.ads|pluralize }})</h3>
{% if area.ads_and_pre_ads %}
<h3>{{ area.acronym }} Area Director{{ area.ads_and_pre_ads|pluralize }} (AD{{ area.ads_and_pre_ads|pluralize }})</h3>
<ul class="list-unstyled">
{% for ad in area.ads %}
{% for ad in area.ads_and_pre_ads %}
<li>
<a href="mailto:{{ ad.email.address }}"><span class="fa fa-envelope-o tiny"></span></a>
<a href="{% url 'ietf.person.views.profile' email_or_name=ad.person.name %}">{{ ad.person.plain_name }}</a>

View file

@ -1,15 +1,17 @@
{% autoescape off %}
Hi,
Chair approval is needed for posting of {{ submission.name }}-{{ submission.rev }}.
{% if approval_type|lower == 'ad' %}Area Director{% else %}Chair{% endif %} approval is needed for posting of {{ submission.name }}-{{ submission.rev }}.
{% if not submission.group.is_active %}
Note: This submission belongs to an inactive working group.
{% endif %}
To approve the draft, go to this URL (note: you need to login to be able to approve):
https://{{ domain }}{% url "ietf.submit.views.submission_status" submission_id=submission.pk access_token=submission.access_token %}
File name : {{ submission.name }}
Revision : {{ submission.rev }}
Submission date : {{ submission.submission_date }}
Group : {{ submission.group|default:"Individual Submission" }}
Group : {{ submission.group|default:"Individual Submission" }}{% if submission.group and not submission.group.is_active %} (inactive group){% endif %}
Title : {{ submission.title }}
Document date : {{ submission.document_date }}

View file

@ -295,13 +295,11 @@
<p>
{% if requires_group_approval %}
Notifies group chairs to get approval.
{% else %}
{% if requires_prev_authors_approval %}
Notifies group chairs to get approval.
{% elif requires_prev_authors_approval %}
Notifies authors of previous revision of draft to get approval.
{% else %}
{% else %}
Notifies submitter and authors for confirmation.
{% endif %}
{% endif %}
</p>
{% endif %}