Skip to content

Commit 2a2e5f0

Browse files
Clean up handling of non-WG groups on the group edit page; restrict parent/child group relationships by type. Fixes ietf-tools#3253. Commit ready for merge.
- Legacy-Id: 19075
1 parent 0ade3f7 commit 2a2e5f0

8 files changed

Lines changed: 433 additions & 27 deletions

File tree

ietf/group/admin.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from ietf.group.models import (Group, GroupFeatures, GroupHistory, GroupEvent, GroupURL, GroupMilestone,
2222
GroupMilestoneHistory, GroupStateTransitions, Role, RoleHistory, ChangeStateGroupEvent,
2323
MilestoneGroupEvent, GroupExtResource, )
24+
from ietf.name.models import GroupTypeName
2425

2526
from ietf.utils.validators import validate_external_resource_value
2627
from ietf.utils.response import permission_denied
@@ -139,10 +140,40 @@ def send_one_reminder(self, request, object_id):
139140

140141
admin.site.register(Group, GroupAdmin)
141142

143+
144+
class GroupFeaturesAdminForm(forms.ModelForm):
145+
def clean_default_parent(self):
146+
# called before form clean() method -- cannot access other fields
147+
parent_acro = self.cleaned_data['default_parent'].strip().lower()
148+
if len(parent_acro) > 0:
149+
if Group.objects.filter(acronym=parent_acro).count() == 0:
150+
raise forms.ValidationError(
151+
'No group exists with acronym "%(acro)s"',
152+
params=dict(acro=parent_acro),
153+
)
154+
return parent_acro
155+
156+
def clean(self):
157+
# cleaning/validation that requires multiple fields
158+
parent_acro = self.cleaned_data['default_parent']
159+
if len(parent_acro) > 0:
160+
parent_type = GroupTypeName.objects.filter(group__acronym=parent_acro).first()
161+
if parent_type not in self.cleaned_data['parent_types']:
162+
self.add_error(
163+
'default_parent',
164+
forms.ValidationError(
165+
'Default parent group "%(acro)s" is type "%(gtype)s", which is not an allowed parent type.',
166+
params=dict(acro=parent_acro, gtype=parent_type),
167+
)
168+
)
169+
142170
class GroupFeaturesAdmin(admin.ModelAdmin):
171+
form = GroupFeaturesAdminForm
143172
list_display = [
144-
145173
'type',
174+
'need_parent',
175+
'default_parent',
176+
'gf_parent_types',
146177
'has_milestones',
147178
'has_chartering_process',
148179
'has_documents',
@@ -165,8 +196,13 @@ class GroupFeaturesAdmin(admin.ModelAdmin):
165196
'groupman_roles',
166197
'matman_roles',
167198
'role_order',
168-
169199
]
200+
201+
def gf_parent_types(self, groupfeatures):
202+
"""Generate list of parent types; needed because many-to-many is not handled automatically"""
203+
return ', '.join([gtn.slug for gtn in groupfeatures.parent_types.all()])
204+
gf_parent_types.short_description = 'Parent Types' # type: ignore # https://github.com/python/mypy/issues/2087
205+
170206
admin.site.register(GroupFeatures, GroupFeaturesAdmin)
171207

172208
class GroupHistoryAdmin(admin.ModelAdmin):

ietf/group/forms.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818
from ietf.group.models import Group, GroupHistory, GroupStateName, GroupFeatures
1919
from ietf.name.models import ReviewTypeName, RoleName, ExtResourceName
2020
from ietf.person.fields import SearchableEmailsField, PersonEmailChoiceField
21-
from ietf.person.models import Person, Email
21+
from ietf.person.models import Email
2222
from ietf.review.models import ReviewerSettings, UnavailablePeriod, ReviewSecretarySettings
2323
from ietf.review.policies import get_reviewer_queue_policy
2424
from ietf.review.utils import close_review_request_states
25+
from ietf.utils import log
2526
from ietf.utils.textupload import get_cleaned_text_file_content
2627
#from ietf.utils.ordereddict import insert_after_in_ordered_dict
2728
from ietf.utils.fields import DatepickerDateField, MultiEmailField
@@ -60,7 +61,6 @@ class GroupForm(forms.Form):
6061
acronym = forms.CharField(max_length=40, label="Acronym", required=True)
6162
state = forms.ModelChoiceField(GroupStateName.objects.all(), label="State", required=True)
6263
# Note that __init__ will add role fields here
63-
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)
6464

6565
parent = forms.ModelChoiceField(Group.objects.filter(state="active").order_by('name'), empty_label="(None)", required=False)
6666
list_email = forms.CharField(max_length=64, required=False)
@@ -74,9 +74,25 @@ def __init__(self, *args, **kwargs):
7474
self.group = kwargs.pop('group', None)
7575
self.group_type = kwargs.pop('group_type', False)
7676
if self.group:
77-
self.used_roles = self.group.used_roles or self.group.features.default_used_roles
77+
group_features = self.group.features
78+
self.used_roles = self.group.used_roles or group_features.default_used_roles
7879
else:
79-
self.used_roles = GroupFeatures.objects.get(type=self.group_type).default_used_roles
80+
group_features = GroupFeatures.objects.filter(type_id=self.group_type).first()
81+
82+
log.assertion('group_features is not None')
83+
if group_features is not None:
84+
self.used_roles = group_features.default_used_roles
85+
parent_types = group_features.parent_types.all()
86+
need_parent = group_features.need_parent
87+
default_parent = group_features.default_parent
88+
else:
89+
# This should not happen, but in the absence of constraints that ensure it
90+
# cannot, prevent the form from breaking if it does.
91+
self.used_roles = []
92+
parent_types = GroupFeatures.objects.none()
93+
need_parent = False
94+
default_parent = None
95+
8096
if "field" in kwargs:
8197
field = kwargs["field"]
8298
del kwargs["field"]
@@ -109,22 +125,21 @@ def __init__(self, *args, **kwargs):
109125
if self.group_type == "rg":
110126
self.fields["state"].queryset = self.fields["state"].queryset.exclude(slug__in=("bof", "bof-conc"))
111127

112-
# if previous AD is now ex-AD, append that person to the list
113-
ad_pk = self.initial.get('ad')
114-
choices = self.fields['ad'].choices
115-
if ad_pk and ad_pk not in [pk for pk, name in choices]:
116-
self.fields['ad'].choices = list(choices) + [("", "-------"), (ad_pk, Person.objects.get(pk=ad_pk).plain_name())]
117-
118128
if self.group:
119129
self.fields['acronym'].widget.attrs['readonly'] = ""
120130

121-
if self.group_type == "rg":
122-
self.fields['ad'].widget = forms.HiddenInput()
123-
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(acronym="irtf")
124-
self.fields['parent'].initial = self.fields['parent'].queryset.first()
125-
self.fields['parent'].widget = forms.HiddenInput()
126-
else:
127-
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(type="area")
131+
# Sort out parent options
132+
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(type__in=parent_types)
133+
if need_parent:
134+
self.fields['parent'].required = True
135+
self.fields['parent'].empty_label = None
136+
# if this is a new group, fill in the default parent, if any
137+
if self.group is None or (not hasattr(self.group, 'pk')):
138+
self.fields['parent'].initial = self.fields['parent'].queryset.filter(
139+
acronym=default_parent
140+
).first()
141+
# label the parent field as 'IETF Area' if appropriate, for consistency with past behavior
142+
if parent_types.count() == 1 and parent_types.first().pk == 'area':
128143
self.fields['parent'].label = "IETF Area"
129144

130145
if field:
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 2.2.19 on 2021-04-13 05:17
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('name', '0023_change_stream_descriptions'),
10+
('group', '0042_add_liaison_contact_roles_to_used_roles'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='groupfeatures',
16+
name='parent_types',
17+
field=models.ManyToManyField(blank=True, help_text='Group types allowed as parent of this group type', related_name='child_features', to='name.GroupTypeName'),
18+
),
19+
migrations.AddField(
20+
model_name='groupfeatures',
21+
name='req_parent',
22+
field=models.BooleanField(default=False, help_text='Does this group type require a parent group?', verbose_name='Need Parent'),
23+
),
24+
migrations.AddField(
25+
model_name='groupfeatures',
26+
name='default_parent',
27+
field=models.CharField(blank=True, default='', help_text='Default parent group acronym for this group type', max_length=40, verbose_name='Default Parent'),
28+
),
29+
]
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Generated by Django 2.2.19 on 2021-04-13 09:17
2+
3+
from django.db import migrations
4+
5+
def populate_parent_types(apps, schema_editor):
6+
"""Add default parent_types entries
7+
8+
Data were determined from existing groups via this query:
9+
{t.slug: list(
10+
Group.objects.filter(type=t, parent__isnull=False).values_list('parent__type', flat=True).distinct()
11+
) for t in GroupTypeName.objects.all()}
12+
"""
13+
GroupFeatures = apps.get_model('group', 'GroupFeatures')
14+
GroupTypeName = apps.get_model('name', 'GroupTypeName')
15+
type_map = {
16+
'adhoc': ['ietf'],
17+
'admin': [],
18+
'ag': ['area', 'ietf'],
19+
'area': ['ietf'],
20+
'dir': ['area'],
21+
'iab': ['ietf'],
22+
'iana': [],
23+
'iesg': [],
24+
'ietf': ['ietf'],
25+
'individ': ['area'],
26+
'irtf': ['irtf'],
27+
'ise': [],
28+
'isoc': ['isoc'],
29+
'nomcom': ['area'],
30+
'program': ['ietf'],
31+
'rag': ['irtf'],
32+
'review': ['area'],
33+
'rfcedtyp': [],
34+
'rg': ['irtf'],
35+
'sdo': ['sdo', 'area'],
36+
'team': ['area'],
37+
'wg': ['area']
38+
}
39+
for type_slug, parent_slugs in type_map.items():
40+
if len(parent_slugs) > 0:
41+
features = GroupFeatures.objects.get(type__slug=type_slug)
42+
features.parent_types.add(*GroupTypeName.objects.filter(slug__in=parent_slugs))
43+
44+
# validate
45+
for gtn in GroupTypeName.objects.all():
46+
slugs_in_db = set(type.slug for type in gtn.features.parent_types.all())
47+
assert(slugs_in_db == set(type_map[gtn.slug]))
48+
49+
50+
def set_req_parent_values(apps, schema_editor):
51+
"""Set req_parent values
52+
53+
Data determined from existing groups using:
54+
55+
GroupTypeName.objects.exclude(pk__in=Group.objects.filter(parent__isnull=True).values('type'))
56+
57+
'iesg' has been removed because there are no groups of this type, so no parent types have
58+
been made available to it.
59+
"""
60+
GroupFeatures = apps.get_model('group', 'GroupFeatures')
61+
62+
GroupFeatures.objects.filter(
63+
type_id__in=('area', 'dir', 'individ', 'review', 'rg',)
64+
).update(req_parent=True)
65+
66+
67+
def set_default_parents(apps, schema_editor):
68+
GroupFeatures = apps.get_model('group', 'GroupFeatures')
69+
70+
# rg-typed groups are children of the irtf group
71+
rg_features = GroupFeatures.objects.filter(type_id='rg').first()
72+
if rg_features:
73+
rg_features.default_parent = 'irtf'
74+
rg_features.save()
75+
76+
77+
def empty_reverse(apps, schema_editor):
78+
pass # nothing to do, field will be dropped
79+
80+
81+
class Migration(migrations.Migration):
82+
83+
dependencies = [
84+
('group', '0043_add_groupfeatures_parent_type_fields'),
85+
]
86+
87+
operations = [
88+
migrations.RunPython(populate_parent_types, empty_reverse),
89+
migrations.RunPython(set_req_parent_values, empty_reverse),
90+
migrations.RunPython(set_default_parents, empty_reverse),
91+
]

ietf/group/models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ def area(self):
9595
return self.parent
9696
return None
9797

98+
def get_used_roles(self):
99+
return self.used_roles if len(self.used_roles) > 0 else self.features.default_used_roles
100+
98101
class Meta:
99102
abstract = True
100103

@@ -250,6 +253,14 @@ def get_description(self):
250253
class GroupFeatures(models.Model):
251254
type = OneToOneField(GroupTypeName, primary_key=True, null=False, related_name='features')
252255
#history = HistoricalRecords()
256+
257+
#
258+
need_parent = models.BooleanField("Need Parent", default=False, help_text="Does this group type require a parent group?")
259+
parent_types = models.ManyToManyField(GroupTypeName, blank=True, related_name='child_features',
260+
help_text="Group types allowed as parent of this group type")
261+
default_parent = models.CharField("Default Parent", max_length=40, blank=True, default="",
262+
help_text="Default parent group acronym for this group type")
263+
253264
#
254265
has_milestones = models.BooleanField("Milestones", default=False)
255266
has_chartering_process = models.BooleanField("Chartering", default=False)

0 commit comments

Comments
 (0)