From 2a2e5f0c24f4f4f2f41f75a9307b888d2450777e Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Fri, 4 Jun 2021 17:31:53 +0000 Subject: [PATCH] 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 --- ietf/group/admin.py | 42 +++++- ietf/group/forms.py | 49 ++++--- ...43_add_groupfeatures_parent_type_fields.py | 29 ++++ ...pulate_groupfeatures_parent_type_fields.py | 91 ++++++++++++ ietf/group/models.py | 11 ++ ietf/group/tests_info.py | 129 +++++++++++++++++- ietf/group/views.py | 9 +- ietf/name/fixtures/names.json | 102 ++++++++++++++ 8 files changed, 434 insertions(+), 28 deletions(-) create mode 100644 ietf/group/migrations/0043_add_groupfeatures_parent_type_fields.py create mode 100644 ietf/group/migrations/0044_populate_groupfeatures_parent_type_fields.py diff --git a/ietf/group/admin.py b/ietf/group/admin.py index a4aec0883..78bc3c5a7 100644 --- a/ietf/group/admin.py +++ b/ietf/group/admin.py @@ -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): diff --git a/ietf/group/forms.py b/ietf/group/forms.py index 05932a79b..30a4b8b34 100644 --- a/ietf/group/forms.py +++ b/ietf/group/forms.py @@ -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: diff --git a/ietf/group/migrations/0043_add_groupfeatures_parent_type_fields.py b/ietf/group/migrations/0043_add_groupfeatures_parent_type_fields.py new file mode 100644 index 000000000..c878937ce --- /dev/null +++ b/ietf/group/migrations/0043_add_groupfeatures_parent_type_fields.py @@ -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'), + ), + ] diff --git a/ietf/group/migrations/0044_populate_groupfeatures_parent_type_fields.py b/ietf/group/migrations/0044_populate_groupfeatures_parent_type_fields.py new file mode 100644 index 000000000..2e806bfa4 --- /dev/null +++ b/ietf/group/migrations/0044_populate_groupfeatures_parent_type_fields.py @@ -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), + ] diff --git a/ietf/group/models.py b/ietf/group/models.py index b693371cb..aae5c4807 100644 --- a/ietf/group/models.py +++ b/ietf/group/models.py @@ -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) diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py index 4144645d0..e89eb43ac 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -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') diff --git a/ietf/group/views.py b/ietf/group/views.py index fc0a942ad..0eae9f11c 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -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', diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index c2d4aa03f..ce101e8ae 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -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