Clean up handling of non-WG groups on the group edit page; restrict parent/child group relationships by type. Fixes #3253. Commit ready for merge.

- Legacy-Id: 19075
This commit is contained in:
Jennifer Richards 2021-06-04 17:31:53 +00:00
parent 0ade3f789a
commit 2a2e5f0c24
8 changed files with 434 additions and 28 deletions

View file

@ -21,6 +21,7 @@ from django.utils.translation import ugettext as _
from ietf.group.models import (Group, GroupFeatures, GroupHistory, GroupEvent, GroupURL, GroupMilestone,
GroupMilestoneHistory, GroupStateTransitions, Role, RoleHistory, ChangeStateGroupEvent,
MilestoneGroupEvent, GroupExtResource, )
from ietf.name.models import GroupTypeName
from ietf.utils.validators import validate_external_resource_value
from ietf.utils.response import permission_denied
@ -139,10 +140,40 @@ class GroupAdmin(admin.ModelAdmin):
admin.site.register(Group, GroupAdmin)
class GroupFeaturesAdmin(admin.ModelAdmin):
list_display = [
class GroupFeaturesAdminForm(forms.ModelForm):
def clean_default_parent(self):
# called before form clean() method -- cannot access other fields
parent_acro = self.cleaned_data['default_parent'].strip().lower()
if len(parent_acro) > 0:
if Group.objects.filter(acronym=parent_acro).count() == 0:
raise forms.ValidationError(
'No group exists with acronym "%(acro)s"',
params=dict(acro=parent_acro),
)
return parent_acro
def clean(self):
# cleaning/validation that requires multiple fields
parent_acro = self.cleaned_data['default_parent']
if len(parent_acro) > 0:
parent_type = GroupTypeName.objects.filter(group__acronym=parent_acro).first()
if parent_type not in self.cleaned_data['parent_types']:
self.add_error(
'default_parent',
forms.ValidationError(
'Default parent group "%(acro)s" is type "%(gtype)s", which is not an allowed parent type.',
params=dict(acro=parent_acro, gtype=parent_type),
)
)
class GroupFeaturesAdmin(admin.ModelAdmin):
form = GroupFeaturesAdminForm
list_display = [
'type',
'need_parent',
'default_parent',
'gf_parent_types',
'has_milestones',
'has_chartering_process',
'has_documents',
@ -165,8 +196,13 @@ class GroupFeaturesAdmin(admin.ModelAdmin):
'groupman_roles',
'matman_roles',
'role_order',
]
def gf_parent_types(self, groupfeatures):
"""Generate list of parent types; needed because many-to-many is not handled automatically"""
return ', '.join([gtn.slug for gtn in groupfeatures.parent_types.all()])
gf_parent_types.short_description = 'Parent Types' # type: ignore # https://github.com/python/mypy/issues/2087
admin.site.register(GroupFeatures, GroupFeaturesAdmin)
class GroupHistoryAdmin(admin.ModelAdmin):

View file

@ -18,10 +18,11 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist
from ietf.group.models import Group, GroupHistory, GroupStateName, GroupFeatures
from ietf.name.models import ReviewTypeName, RoleName, ExtResourceName
from ietf.person.fields import SearchableEmailsField, PersonEmailChoiceField
from ietf.person.models import Person, Email
from ietf.person.models import Email
from ietf.review.models import ReviewerSettings, UnavailablePeriod, ReviewSecretarySettings
from ietf.review.policies import get_reviewer_queue_policy
from ietf.review.utils import close_review_request_states
from ietf.utils import log
from ietf.utils.textupload import get_cleaned_text_file_content
#from ietf.utils.ordereddict import insert_after_in_ordered_dict
from ietf.utils.fields import DatepickerDateField, MultiEmailField
@ -60,7 +61,6 @@ class GroupForm(forms.Form):
acronym = forms.CharField(max_length=40, label="Acronym", required=True)
state = forms.ModelChoiceField(GroupStateName.objects.all(), label="State", required=True)
# Note that __init__ will add role fields here
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active", role__group__type='area').order_by('name'), label="Shepherding AD", empty_label="(None)", required=False)
parent = forms.ModelChoiceField(Group.objects.filter(state="active").order_by('name'), empty_label="(None)", required=False)
list_email = forms.CharField(max_length=64, required=False)
@ -74,9 +74,25 @@ class GroupForm(forms.Form):
self.group = kwargs.pop('group', None)
self.group_type = kwargs.pop('group_type', False)
if self.group:
self.used_roles = self.group.used_roles or self.group.features.default_used_roles
group_features = self.group.features
self.used_roles = self.group.used_roles or group_features.default_used_roles
else:
self.used_roles = GroupFeatures.objects.get(type=self.group_type).default_used_roles
group_features = GroupFeatures.objects.filter(type_id=self.group_type).first()
log.assertion('group_features is not None')
if group_features is not None:
self.used_roles = group_features.default_used_roles
parent_types = group_features.parent_types.all()
need_parent = group_features.need_parent
default_parent = group_features.default_parent
else:
# This should not happen, but in the absence of constraints that ensure it
# cannot, prevent the form from breaking if it does.
self.used_roles = []
parent_types = GroupFeatures.objects.none()
need_parent = False
default_parent = None
if "field" in kwargs:
field = kwargs["field"]
del kwargs["field"]
@ -109,22 +125,21 @@ class GroupForm(forms.Form):
if self.group_type == "rg":
self.fields["state"].queryset = self.fields["state"].queryset.exclude(slug__in=("bof", "bof-conc"))
# if previous AD is now ex-AD, append that person to the list
ad_pk = self.initial.get('ad')
choices = self.fields['ad'].choices
if ad_pk and ad_pk not in [pk for pk, name in choices]:
self.fields['ad'].choices = list(choices) + [("", "-------"), (ad_pk, Person.objects.get(pk=ad_pk).plain_name())]
if self.group:
self.fields['acronym'].widget.attrs['readonly'] = ""
if self.group_type == "rg":
self.fields['ad'].widget = forms.HiddenInput()
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(acronym="irtf")
self.fields['parent'].initial = self.fields['parent'].queryset.first()
self.fields['parent'].widget = forms.HiddenInput()
else:
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(type="area")
# Sort out parent options
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(type__in=parent_types)
if need_parent:
self.fields['parent'].required = True
self.fields['parent'].empty_label = None
# if this is a new group, fill in the default parent, if any
if self.group is None or (not hasattr(self.group, 'pk')):
self.fields['parent'].initial = self.fields['parent'].queryset.filter(
acronym=default_parent
).first()
# label the parent field as 'IETF Area' if appropriate, for consistency with past behavior
if parent_types.count() == 1 and parent_types.first().pk == 'area':
self.fields['parent'].label = "IETF Area"
if field:

View file

@ -0,0 +1,29 @@
# Generated by Django 2.2.19 on 2021-04-13 05:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('name', '0023_change_stream_descriptions'),
('group', '0042_add_liaison_contact_roles_to_used_roles'),
]
operations = [
migrations.AddField(
model_name='groupfeatures',
name='parent_types',
field=models.ManyToManyField(blank=True, help_text='Group types allowed as parent of this group type', related_name='child_features', to='name.GroupTypeName'),
),
migrations.AddField(
model_name='groupfeatures',
name='req_parent',
field=models.BooleanField(default=False, help_text='Does this group type require a parent group?', verbose_name='Need Parent'),
),
migrations.AddField(
model_name='groupfeatures',
name='default_parent',
field=models.CharField(blank=True, default='', help_text='Default parent group acronym for this group type', max_length=40, verbose_name='Default Parent'),
),
]

View file

@ -0,0 +1,91 @@
# Generated by Django 2.2.19 on 2021-04-13 09:17
from django.db import migrations
def populate_parent_types(apps, schema_editor):
"""Add default parent_types entries
Data were determined from existing groups via this query:
{t.slug: list(
Group.objects.filter(type=t, parent__isnull=False).values_list('parent__type', flat=True).distinct()
) for t in GroupTypeName.objects.all()}
"""
GroupFeatures = apps.get_model('group', 'GroupFeatures')
GroupTypeName = apps.get_model('name', 'GroupTypeName')
type_map = {
'adhoc': ['ietf'],
'admin': [],
'ag': ['area', 'ietf'],
'area': ['ietf'],
'dir': ['area'],
'iab': ['ietf'],
'iana': [],
'iesg': [],
'ietf': ['ietf'],
'individ': ['area'],
'irtf': ['irtf'],
'ise': [],
'isoc': ['isoc'],
'nomcom': ['area'],
'program': ['ietf'],
'rag': ['irtf'],
'review': ['area'],
'rfcedtyp': [],
'rg': ['irtf'],
'sdo': ['sdo', 'area'],
'team': ['area'],
'wg': ['area']
}
for type_slug, parent_slugs in type_map.items():
if len(parent_slugs) > 0:
features = GroupFeatures.objects.get(type__slug=type_slug)
features.parent_types.add(*GroupTypeName.objects.filter(slug__in=parent_slugs))
# validate
for gtn in GroupTypeName.objects.all():
slugs_in_db = set(type.slug for type in gtn.features.parent_types.all())
assert(slugs_in_db == set(type_map[gtn.slug]))
def set_req_parent_values(apps, schema_editor):
"""Set req_parent values
Data determined from existing groups using:
GroupTypeName.objects.exclude(pk__in=Group.objects.filter(parent__isnull=True).values('type'))
'iesg' has been removed because there are no groups of this type, so no parent types have
been made available to it.
"""
GroupFeatures = apps.get_model('group', 'GroupFeatures')
GroupFeatures.objects.filter(
type_id__in=('area', 'dir', 'individ', 'review', 'rg',)
).update(req_parent=True)
def set_default_parents(apps, schema_editor):
GroupFeatures = apps.get_model('group', 'GroupFeatures')
# rg-typed groups are children of the irtf group
rg_features = GroupFeatures.objects.filter(type_id='rg').first()
if rg_features:
rg_features.default_parent = 'irtf'
rg_features.save()
def empty_reverse(apps, schema_editor):
pass # nothing to do, field will be dropped
class Migration(migrations.Migration):
dependencies = [
('group', '0043_add_groupfeatures_parent_type_fields'),
]
operations = [
migrations.RunPython(populate_parent_types, empty_reverse),
migrations.RunPython(set_req_parent_values, empty_reverse),
migrations.RunPython(set_default_parents, empty_reverse),
]

View file

@ -95,6 +95,9 @@ class GroupInfo(models.Model):
return self.parent
return None
def get_used_roles(self):
return self.used_roles if len(self.used_roles) > 0 else self.features.default_used_roles
class Meta:
abstract = True
@ -250,6 +253,14 @@ validate_comma_separated_roles = RegexValidator(
class GroupFeatures(models.Model):
type = OneToOneField(GroupTypeName, primary_key=True, null=False, related_name='features')
#history = HistoricalRecords()
#
need_parent = models.BooleanField("Need Parent", default=False, help_text="Does this group type require a parent group?")
parent_types = models.ManyToManyField(GroupTypeName, blank=True, related_name='child_features',
help_text="Group types allowed as parent of this group type")
default_parent = models.CharField("Default Parent", max_length=40, blank=True, default="",
help_text="Default parent group acronym for this group type")
#
has_milestones = models.BooleanField("Milestones", default=False)
has_chartering_process = models.BooleanField("Chartering", default=False)

View file

@ -541,7 +541,7 @@ class GroupEditTests(TestCase):
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form input[name=acronym]')), 1)
self.assertEqual(q('form input[name=parent]').attr('value'),'%s'%irtf.pk)
self.assertEqual(q('form select[name=parent]')[0].value,'%s'%irtf.pk)
r = self.client.post(url, dict(acronym="testrg", name="Testing RG", state=proposed_state.pk, parent=irtf.pk))
self.assertEqual(r.status_code, 302)
@ -632,6 +632,7 @@ class GroupEditTests(TestCase):
parent=area.pk,
ad=ad.pk,
state=state.pk,
ad_roles=ad.email().address,
chair_roles="aread@example.org, ad1@example.org",
secr_roles="aread@example.org, ad1@example.org, ad2@example.org",
liaison_contact_roles="ad1@example.org",
@ -886,6 +887,132 @@ class GroupEditTests(TestCase):
self.assertEqual(r.status_code,403)
self.assertEqual(len(outbox),5)
class GroupFormTests(TestCase):
"""Tests of the GroupForm form"""
@staticmethod
def _format_resource(r):
if r.display_name:
return '{} {} ({})'.format(r.name.slug, r.value, r.display_name.strip('()'))
else:
return '{} {}'.format(r.name.slug, r.value)
def _group_post_data(self, group):
data=dict(
name=group.name,
acronym=group.acronym,
state=group.state_id,
parent=group.parent_id or '',
list_email=group.list_email if group.list_email else None,
list_subscribe=group.list_subscribe if group.list_subscribe else '',
list_archive=group.list_archive if group.list_archive else '',
resources='\n'.join(self._format_resource(r) for r in group.groupextresource_set.all()),
closing_note='', # not a group attribute, handled specially by the view; ignore in this test
)
# fill in original values
for rslug in group.get_used_roles():
data['{}_roles'.format(rslug)] = ','.join(
group.role_set.filter(name_id=rslug).values_list('email__address', flat=True),
)
return data
def _assert_cleaned_data_equal(self, cleaned_data, post_data):
for attr, expected in post_data.items():
value = cleaned_data[attr]
if attr.endswith('_roles'):
actual = ','.join(value.values_list('address', flat=True))
elif attr == 'resources':
# must handle resources specially
actual = '\n'.join(self._format_resource(r) for r in value)
elif hasattr(value, 'pk'):
actual = value.pk
else:
actual = '' if value is None else value
self.assertEqual(actual, expected, 'unexpected value for {}'.format(attr))
def do_edit_roles_test(self, group):
# get post_data for the group
orig_data = self._group_post_data(group)
# create a user to be assigned roles
new_email = EmailFactory()
# Now check that we can replace each used_role without disturbing the others.
# This does not actually update group, so start with orig_data each time.
for rslug in group.get_used_roles():
data = orig_data.copy()
edit_field = '{}_roles'.format(rslug)
data[edit_field] = new_email.address # comma-separated list of addresses with only one
form = GroupForm(data, group=group, group_type=group.type_id, field=None)
self.assertTrue(form.is_valid())
# Check that all cleaned values match what we passed to the form.
self._assert_cleaned_data_equal(form.cleaned_data, data)
def test_edit_roles(self):
"""Test that roles can be edited for all group types
N.B., the combinations of group type and parent group and the used_roles are
obtained from the GroupFeatures in the database. The handling of these combinations
is validated, but this test cannot check that the rules themselves are correct.
As long as names.json is up to date, this will test what we want.
"""
# Test every parent type that is allowed for at least one group type
for parent_type in GroupTypeName.objects.filter(child_features__isnull=False).distinct():
parent = GroupFactory(type_id=parent_type.pk)
for child_features in parent_type.child_features.all():
# create a group of each child type for this parent and populate its roles
group_type = child_features.type
group = GroupFactory(type_id=group_type.pk, parent=parent)
for rslug in group.get_used_roles():
RoleFactory(name_id=rslug, group=group, person=PersonFactory())
self.do_edit_roles_test(group)
def test_need_parent(self):
"""GroupForm should enforce non-null parent when required"""
group = GroupFactory()
parent = group.parent
other_parent = GroupFactory(type_id=parent.type_id)
for rslug in group.get_used_roles():
RoleFactory(name_id=rslug, group=group, person=PersonFactory())
data = self._group_post_data(group)
# First, test with parent required
group.type.features.need_parent = True
group.type.features.save()
group = Group.objects.get(pk=group.pk) # renew object to clear features cache
# should fail with empty parent
data['parent'] = ''
form = GroupForm(data, group=group, group_type=group.type_id, field=None)
self.assertFalse(form.is_valid()) # cannot update to empty parent
# should succeed with non-empty parent
data['parent'] = other_parent.pk
form = GroupForm(data, group=group, group_type=group.type_id, field=None)
self.assertTrue(form.is_valid())
self._assert_cleaned_data_equal(form.cleaned_data, data)
# Second, test with parent not required
group.type.features.need_parent = False
group.type.features.save()
group = Group.objects.get(pk=group.pk) # renew object to clear features cache
# should succeed with empty parent
data['parent'] = ''
form = GroupForm(data, group=group, group_type=group.type_id, field=None)
self.assertTrue(form.is_valid())
self._assert_cleaned_data_equal(form.cleaned_data, data)
# should succeed with non-empty parent
data['parent'] = other_parent.pk
form = GroupForm(data, group=group, group_type=group.type_id, field=None)
self.assertTrue(form.is_valid())
self._assert_cleaned_data_equal(form.cleaned_data, data)
class MilestoneTests(TestCase):
def create_test_milestones(self):
group = GroupFactory(acronym='mars',parent=GroupFactory(type_id='area'),list_email='mars-wg@ietf.org')

View file

@ -942,7 +942,7 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
changed_personnel = set()
# update roles
for attr, f in form.fields.items():
if not (attr.endswith("_roles") or attr == "ad"):
if not attr.endswith("_roles"):
continue
slug = attr
@ -951,8 +951,6 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
title = f.label
new = clean[attr]
if attr == 'ad':
new = [ new.role_email('ad') ] if new else []
old = Email.objects.filter(role__group=group, role__name=slug).select_related("person")
if set(new) != set(old):
changes.append((attr, new, desc(title,
@ -1044,7 +1042,6 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
return HttpResponseRedirect(group.about_url())
else: # Not POST:
if not new_group:
ad_role = group.ad_role()
closing_note = ""
e = group.latest_event(type='closing_note')
if e:
@ -1055,7 +1052,6 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
init = dict(name=group.name,
acronym=group.acronym,
state=group.state,
ad=ad_role and ad_role.person and ad_role.person.id,
parent=group.parent.id if group.parent else None,
list_email=group.list_email if group.list_email else None,
list_subscribe=group.list_subscribe if group.list_subscribe else None,
@ -1065,8 +1061,7 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
)
else:
init = dict(ad=request.user.person.id if group_type == "wg" and has_role(request.user, "Area Director") else None,
)
init = dict()
form = GroupForm(initial=init, group=group, group_type=group_type, field=field)
return render(request, 'group/edit.html',

View file

@ -2482,6 +2482,7 @@
"create_wiki": true,
"custom_group_roles": false,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"matman\",\n \"ad\",\n \"chair\",\n \"lead\"\n]",
"docman_roles": "[\n \"chair\"\n]",
@ -2498,6 +2499,10 @@
"is_schedulable": true,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"chair\",\n \"lead\",\n \"delegate\",\n \"matman\"\n]",
"parent_types": [
"ietf"
],
"need_parent": false,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"lead\",\n \"delegate\",\n \"matman\"\n]",
"show_on_agenda": true
@ -2514,6 +2519,7 @@
"create_wiki": false,
"custom_group_roles": false,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"member\",\n \"chair\"\n]",
"docman_roles": "[\n \"chair\"\n]",
@ -2530,6 +2536,8 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"chair\"\n]",
"parent_types": [],
"need_parent": false,
"req_subm_approval": false,
"role_order": "[\n \"chair\"\n]",
"show_on_agenda": false
@ -2546,6 +2554,7 @@
"create_wiki": true,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"ad\",\n \"chair\",\n \"secr\"\n]",
"docman_roles": "[\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
@ -2562,6 +2571,11 @@
"is_schedulable": true,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"ad\",\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
"parent_types": [
"area",
"ietf"
],
"need_parent": false,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]",
"show_on_agenda": true
@ -2578,6 +2592,7 @@
"create_wiki": true,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"ad\",\n \"liaison_contact\",\n \"liaison_cc_contact\"\n]",
"docman_roles": "[\n \"ad\",\n \"delegate\",\n \"secr\"\n]",
@ -2594,6 +2609,10 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"ad\",\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
"parent_types": [
"ietf"
],
"need_parent": true,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]",
"show_on_agenda": false
@ -2610,6 +2629,7 @@
"create_wiki": true,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"ad\",\n \"chair\",\n \"reviewer\",\n \"secr\"\n]",
"docman_roles": "[]",
@ -2626,6 +2646,10 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"ad\",\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
"parent_types": [
"area"
],
"need_parent": true,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]",
"show_on_agenda": false
@ -2642,6 +2666,7 @@
"create_wiki": false,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"chair\"\n]",
"docman_roles": "[\n \"chair\"\n]",
@ -2658,6 +2683,10 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"chair\",\n \"delegate\"\n]",
"parent_types": [
"ietf"
],
"need_parent": false,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]",
"show_on_agenda": true
@ -2674,6 +2703,7 @@
"create_wiki": false,
"custom_group_roles": false,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"auth\"\n]",
"docman_roles": "[\n \"chair\"\n]",
@ -2690,6 +2720,8 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"chair\"\n]",
"parent_types": [],
"need_parent": false,
"req_subm_approval": false,
"role_order": "[\n \"chair\"\n]",
"show_on_agenda": false
@ -2706,6 +2738,7 @@
"create_wiki": false,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[]",
"docman_roles": "[\n \"chair\"\n]",
@ -2722,6 +2755,8 @@
"is_schedulable": false,
"material_types": "\"[]\"",
"matman_roles": "[\n \"chair\",\n \"delegate\",\n \"member\"\n]",
"parent_types": [],
"need_parent": false,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"delegate\",\n \"member\"\n]",
"show_on_agenda": false
@ -2738,6 +2773,7 @@
"create_wiki": false,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"ad\",\n \"member\",\n \"comdir\",\n \"delegate\",\n \"execdir\",\n \"recman\",\n \"secr\",\n \"trac-editor\",\n \"trac-admin\",\n \"chair\"\n]",
"docman_roles": "[\n \"chair\"\n]",
@ -2754,6 +2790,10 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"chair\",\n \"delegate\"\n]",
"parent_types": [
"ietf"
],
"need_parent": false,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]",
"show_on_agenda": false
@ -2770,6 +2810,7 @@
"create_wiki": false,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"ad\"\n]",
"docman_roles": "[\n \"auth\"\n]",
@ -2786,6 +2827,10 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[]",
"parent_types": [
"area"
],
"need_parent": true,
"req_subm_approval": false,
"role_order": "[\n \"chair\",\n \"secr\"\n]",
"show_on_agenda": false
@ -2802,6 +2847,7 @@
"create_wiki": false,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"member\",\n \"atlarge\",\n \"chair\"\n]",
"docman_roles": "[]",
@ -2818,6 +2864,10 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
"parent_types": [
"irtf"
],
"need_parent": false,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]",
"show_on_agenda": false
@ -2834,6 +2884,7 @@
"create_wiki": false,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"chair\"\n]",
"docman_roles": "[\n \"chair\"\n]",
@ -2850,6 +2901,8 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"chair\",\n \"delegate\"\n]",
"parent_types": [],
"need_parent": false,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"delegate\"\n]",
"show_on_agenda": false
@ -2866,6 +2919,7 @@
"create_wiki": false,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"chair\",\n \"ceo\"\n]",
"docman_roles": "[]",
@ -2882,6 +2936,10 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"chair\",\n \"secr\"\n]",
"parent_types": [
"isoc"
],
"need_parent": false,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]",
"show_on_agenda": false
@ -2898,6 +2956,7 @@
"create_wiki": true,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"member\",\n \"advisor\",\n \"liaison\",\n \"chair\",\n \"techadv\"\n]",
"docman_roles": "[\n \"chair\"\n]",
@ -2914,6 +2973,10 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"chair\"\n]",
"parent_types": [
"area"
],
"need_parent": false,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"member\",\n \"advisor\"\n]",
"show_on_agenda": false
@ -2930,6 +2993,7 @@
"create_wiki": false,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"member\",\n \"chair\",\n \"lead\"\n]",
"docman_roles": "[\n \"lead\",\n \"chair\",\n \"secr\"\n]",
@ -2946,6 +3010,10 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"lead\",\n \"chair\",\n \"secr\"\n]",
"parent_types": [
"ietf"
],
"need_parent": false,
"req_subm_approval": false,
"role_order": "[\n \"lead\",\n \"chair\",\n \"secr\"\n]",
"show_on_agenda": false
@ -2962,6 +3030,7 @@
"create_wiki": true,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"chair\",\n \"secr\"\n]",
"docman_roles": "[\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
@ -2978,6 +3047,10 @@
"is_schedulable": true,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
"parent_types": [
"irtf"
],
"need_parent": false,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]",
"show_on_agenda": true
@ -2994,6 +3067,7 @@
"create_wiki": true,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.review_requests",
"default_used_roles": "[\n \"ad\",\n \"chair\",\n \"reviewer\",\n \"secr\"\n]",
"docman_roles": "[\n \"secr\"\n]",
@ -3010,6 +3084,10 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"ad\",\n \"secr\"\n]",
"parent_types": [
"area"
],
"need_parent": true,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]",
"show_on_agenda": false
@ -3026,6 +3104,7 @@
"create_wiki": false,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"auth\",\n \"chair\"\n]",
"docman_roles": "[]",
@ -3042,6 +3121,8 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[]",
"parent_types": [],
"need_parent": false,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]",
"show_on_agenda": false
@ -3058,6 +3139,7 @@
"create_wiki": true,
"custom_group_roles": false,
"customize_workflow": true,
"default_parent": "irtf",
"default_tab": "ietf.group.views.group_documents",
"default_used_roles": "[\n \"chair\",\n \"techadv\",\n \"secr\",\n \"delegate\"\n]",
"docman_roles": "[\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
@ -3074,6 +3156,10 @@
"is_schedulable": true,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
"parent_types": [
"irtf"
],
"need_parent": true,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
"show_on_agenda": true
@ -3090,6 +3176,7 @@
"create_wiki": false,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"liaiman\",\n \"ceo\",\n \"coord\",\n \"auth\",\n \"chair\",\n \"liaison_contact\",\n \"liaison_cc_contact\"\n]",
"docman_roles": "[\n \"liaiman\",\n \"matman\"\n]",
@ -3106,6 +3193,11 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[]",
"parent_types": [
"area",
"sdo"
],
"need_parent": false,
"req_subm_approval": true,
"role_order": "[\n \"liaiman\"\n]",
"show_on_agenda": false
@ -3122,6 +3214,7 @@
"create_wiki": true,
"custom_group_roles": true,
"customize_workflow": false,
"default_parent": "",
"default_tab": "ietf.group.views.group_about",
"default_used_roles": "[\n \"ad\",\n \"member\",\n \"delegate\",\n \"secr\",\n \"liaison\",\n \"atlarge\",\n \"chair\",\n \"matman\",\n \"techadv\"\n]",
"docman_roles": "[\n \"chair\"\n]",
@ -3138,6 +3231,10 @@
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"chair\",\n \"matman\"\n]",
"parent_types": [
"area"
],
"need_parent": false,
"req_subm_approval": false,
"role_order": "[\n \"chair\",\n \"member\",\n \"matman\"\n]",
"show_on_agenda": false
@ -3154,6 +3251,7 @@
"create_wiki": true,
"custom_group_roles": false,
"customize_workflow": true,
"default_parent": "",
"default_tab": "ietf.group.views.group_documents",
"default_used_roles": "[\n \"ad\",\n \"editor\",\n \"delegate\",\n \"secr\",\n \"chair\",\n \"matman\",\n \"techadv\",\n \"liaison_contact\",\n \"liaison_cc_contact\"\n]",
"docman_roles": "[\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
@ -3170,6 +3268,10 @@
"is_schedulable": true,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"ad\",\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
"parent_types": [
"area"
],
"need_parent": false,
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\",\n \"delegate\"\n]",
"show_on_agenda": true