Skip to content

Commit 8759955

Browse files
Allow nomcom chair to edit liaisons as well as members and generate GroupEvents when changed. Share code between group and nomcom for this purpose. Fixes ietf-tools#3376. Commit ready for merge.
- Legacy-Id: 19710
1 parent dcf4251 commit 8759955

5 files changed

Lines changed: 167 additions & 70 deletions

File tree

ietf/group/utils.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from django.db.models import Q
99
from django.shortcuts import get_object_or_404
10+
from django.utils.html import format_html
1011
from django.utils.safestring import mark_safe
1112
from django.urls import reverse as urlreverse
1213

@@ -15,9 +16,9 @@
1516
from ietf.community.models import CommunityList, SearchRule
1617
from ietf.community.utils import reset_name_contains_index_for_rule, can_manage_community_list
1718
from ietf.doc.models import Document, State
18-
from ietf.group.models import Group, RoleHistory, Role, GroupFeatures
19+
from ietf.group.models import Group, RoleHistory, Role, GroupFeatures, GroupEvent
1920
from ietf.ietfauth.utils import has_role
20-
from ietf.name.models import GroupTypeName
21+
from ietf.name.models import GroupTypeName, RoleName
2122
from ietf.person.models import Email
2223
from ietf.review.utils import can_manage_review_requests_for_team
2324
from ietf.utils import log
@@ -279,4 +280,45 @@ def group_features_role_filter(roles, person, feature):
279280
return roles.none()
280281
q = reduce(lambda a,b:a|b, [ Q(person=person, name__slug__in=getattr(t.features, feature)) for t in group_types ])
281282
return roles.filter(q)
282-
283+
284+
285+
def group_attribute_change_desc(attr, new, old=None):
286+
if old is None:
287+
return format_html('{} changed to <b>{}</b>', attr, new)
288+
else:
289+
return format_html('{} changed to <b>{}</b> from {}', attr, new, old)
290+
291+
292+
def update_role_set(group, role_name, new_value, by):
293+
"""Alter role_set for a group
294+
295+
Updates the value and creates history events.
296+
"""
297+
if isinstance(role_name, str):
298+
role_name = RoleName.objects.get(slug=role_name)
299+
new = set(new_value)
300+
old = set(r.email for r in group.role_set.filter(name=role_name).distinct().select_related("person"))
301+
removed = old - new
302+
added = new - old
303+
if added or removed:
304+
GroupEvent.objects.create(
305+
group=group,
306+
by=by,
307+
type='info_changed',
308+
desc=group_attribute_change_desc(
309+
role_name.name,
310+
", ".join(sorted(x.get_name() for x in new)),
311+
", ".join(sorted(x.get_name() for x in old)),
312+
)
313+
)
314+
315+
group.role_set.filter(name=role_name, email__in=removed).delete()
316+
for email in added:
317+
group.role_set.create(name=role_name, email=email, person=email.person)
318+
319+
for e in new:
320+
if not e.origin or (e.person.user and e.origin == e.person.user.username):
321+
e.origin = "role: %s %s" % (group.acronym, role_name.slug)
322+
e.save()
323+
324+
return added, removed

ietf/group/views.py

Lines changed: 43 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@
7777
ChangeStateGroupEvent, GroupFeatures )
7878
from ietf.group.utils import (get_charter_text, can_manage_all_groups_of_type,
7979
milestone_reviewer_for_group_type, can_provide_status_update,
80-
can_manage_materials,
80+
can_manage_materials, group_attribute_change_desc,
8181
construct_group_menu_context, get_group_materials,
82-
save_group_in_history, can_manage_group,
82+
save_group_in_history, can_manage_group, update_role_set,
8383
get_group_or_404, setup_default_community_list_for_group, )
8484
#
8585
from ietf.ietfauth.utils import has_role, is_authorized_in_group
@@ -873,13 +873,6 @@ def group_photos(request, group_type=None, acronym=None):
873873
def edit(request, group_type=None, acronym=None, action="edit", field=None):
874874
"""Edit or create a group, notifying parties as
875875
necessary and logging changes as group events."""
876-
def desc(attr, new, old):
877-
entry = "%(attr)s changed to <b>%(new)s</b> from %(old)s"
878-
if new_group:
879-
entry = "%(attr)s changed to <b>%(new)s</b>"
880-
881-
return entry % dict(attr=attr, new=new, old=old)
882-
883876
def format_resources(resources, fs="\n"):
884877
res = []
885878
for r in resources:
@@ -896,7 +889,11 @@ def diff(attr, name):
896889
return
897890
v = getattr(group, attr)
898891
if clean[attr] != v:
899-
changes.append((attr, clean[attr], desc(name, clean[attr], v)))
892+
changes.append((
893+
attr,
894+
clean[attr],
895+
group_attribute_change_desc(name, clean[attr], v if v else None)
896+
))
900897
setattr(group, attr, clean[attr])
901898

902899
if action == "edit":
@@ -972,47 +969,33 @@ def diff(attr, name):
972969

973970
title = f.label
974971

975-
new = clean[attr]
976-
old = Email.objects.filter(role__group=group, role__name=slug).select_related("person")
977-
if set(new) != set(old):
978-
changes.append((attr, new, desc(title,
979-
", ".join(sorted(x.get_name() for x in new)),
980-
", ".join(sorted(x.get_name() for x in old)))))
981-
group.role_set.filter(name=slug).delete()
982-
for e in new:
983-
Role.objects.get_or_create(name_id=slug, email=e, group=group, person=e.person)
984-
if not e.origin or (e.person.user and e.origin == e.person.user.username):
985-
e.origin = "role: %s %s" % (group.acronym, slug)
986-
e.save()
987-
988-
added = set(new) - set(old)
989-
deleted = set(old) - set(new)
990-
if added:
991-
change_text=title + ' added: ' + ", ".join(x.name_and_email() for x in added)
992-
personnel_change_text+=change_text+"\n"
993-
if deleted:
994-
change_text=title + ' deleted: ' + ", ".join(x.name_and_email() for x in deleted)
995-
personnel_change_text+=change_text+"\n"
996-
997-
today = datetime.date.today()
998-
for deleted_email in deleted:
999-
# Verify the person doesn't have a separate reviewer role for the group with a different address
1000-
if not group.role_set.filter(name_id='reviewer',person=deleted_email.person).exists():
1001-
active_assignments = ReviewAssignment.objects.filter(
1002-
review_request__team=group,
1003-
reviewer__person=deleted_email.person,
1004-
state_id__in=['accepted', 'assigned'],
1005-
)
1006-
for assignment in active_assignments:
1007-
if assignment.review_request.deadline > today:
1008-
assignment.state_id = 'withdrawn'
1009-
else:
1010-
assignment.state_id = 'no-response'
1011-
# save() will update review_request state to 'requested'
1012-
# if needed, so that the review can be assigned to someone else
1013-
assignment.save()
1014-
1015-
changed_personnel.update(set(old)^set(new))
972+
added, deleted = update_role_set(group, slug, clean[attr], request.user.person)
973+
changed_personnel.update(added | deleted)
974+
if added:
975+
change_text=title + ' added: ' + ", ".join(x.name_and_email() for x in added)
976+
personnel_change_text+=change_text+"\n"
977+
if deleted:
978+
change_text=title + ' deleted: ' + ", ".join(x.name_and_email() for x in deleted)
979+
personnel_change_text+=change_text+"\n"
980+
981+
today = datetime.date.today()
982+
for deleted_email in deleted:
983+
# Verify the person doesn't have a separate reviewer role for the group with a different address
984+
if not group.role_set.filter(name_id='reviewer',person=deleted_email.person).exists():
985+
active_assignments = ReviewAssignment.objects.filter(
986+
review_request__team=group,
987+
reviewer__person=deleted_email.person,
988+
state_id__in=['accepted', 'assigned'],
989+
)
990+
for assignment in active_assignments:
991+
if assignment.review_request.deadline > today:
992+
assignment.state_id = 'withdrawn'
993+
else:
994+
assignment.state_id = 'no-response'
995+
# save() will update review_request state to 'requested'
996+
# if needed, so that the review can be assigned to someone else
997+
assignment.save()
998+
1016999

10171000
if personnel_change_text!="":
10181001
changed_personnel = [ str(p) for p in changed_personnel ]
@@ -1030,7 +1013,15 @@ def diff(attr, name):
10301013
value = parts[1]
10311014
display_name = ' '.join(parts[2:]).strip('()')
10321015
group.groupextresource_set.create(value=value, name_id=name, display_name=display_name)
1033-
changes.append(('resources', new_resources, desc('Resources', ", ".join(new_resources), ", ".join(old_resources))))
1016+
changes.append((
1017+
'resources',
1018+
new_resources,
1019+
group_attribute_change_desc(
1020+
'Resources',
1021+
", ".join(new_resources),
1022+
", ".join(old_resources) if old_resources else None
1023+
)
1024+
))
10341025

10351026
group.time = datetime.datetime.now()
10361027

ietf/nomcom/forms.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,23 @@ def clean(self, value):
8686
return result
8787

8888

89-
class NewEditMembersForm(forms.Form):
89+
class EditMembersForm(forms.Form):
90+
members = SearchableEmailsField(only_users=True, all_emails=True, required=False)
91+
liaisons = SearchableEmailsField(only_users=True, all_emails=True, required=False)
92+
93+
def __init__(self, nomcom, *args, **kwargs):
94+
initial = kwargs.setdefault('initial', {})
95+
roles = nomcom.group.role_set.filter(
96+
name__slug__in=('member', 'liaison')
97+
).order_by('email__person__name').select_related('email')
98+
initial['members'] = [
99+
r.email for r in roles if r.name.slug == 'member'
100+
]
101+
initial['liaisons'] = [
102+
r.email for r in roles if r.name.slug =='liaison'
103+
]
104+
super().__init__(*args, **kwargs)
90105

91-
members = SearchableEmailsField(only_users=True,all_emails=True)
92106

93107
class EditNomcomForm(forms.ModelForm):
94108

ietf/nomcom/tests.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -408,9 +408,14 @@ def test_private_merge_view(self):
408408

409409
self.client.logout()
410410

411-
def change_members(self, members):
412-
members_emails = ','.join(['%s%s' % (member, EMAIL_DOMAIN) for member in members])
413-
test_data = {'members': members_emails,}
411+
def change_members(self, members=None, liaisons=None):
412+
test_data = {}
413+
if members is not None:
414+
members_emails = ','.join(['%s%s' % (member, EMAIL_DOMAIN) for member in members])
415+
test_data['members'] = members_emails
416+
if liaisons is not None:
417+
liaisons_emails = ','.join(['%s%s' % (liaison, EMAIL_DOMAIN) for liaison in liaisons])
418+
test_data['liaisons'] = liaisons_emails
414419
self.client.post(self.edit_members_url, test_data)
415420

416421
def test_edit_members_view(self):
@@ -432,6 +437,54 @@ def test_edit_members_view(self):
432437
self.check_url_status(self.private_index_url, 403)
433438
self.client.logout()
434439

440+
def test_edit_members_only_removes_member_roles(self):
441+
"""Removing a member or liaison should not affect other roles"""
442+
# log in and set up members/liaisons lists
443+
self.access_chair_url(self.edit_members_url)
444+
self.change_members(
445+
members=[CHAIR_USER, COMMUNITY_USER],
446+
liaisons=[CHAIR_USER, COMMUNITY_USER],
447+
)
448+
nomcom_group = Group.objects.get(acronym=f'nomcom{self.year}')
449+
self.assertCountEqual(
450+
nomcom_group.role_set.filter(name='member').values_list('email__address', flat=True),
451+
[CHAIR_USER + EMAIL_DOMAIN, COMMUNITY_USER + EMAIL_DOMAIN],
452+
)
453+
self.assertCountEqual(
454+
nomcom_group.role_set.filter(name='liaison').values_list('email__address', flat=True),
455+
[CHAIR_USER + EMAIL_DOMAIN, COMMUNITY_USER + EMAIL_DOMAIN],
456+
)
457+
458+
# remove a member who is also a liaison and check that the liaisons list is unchanged
459+
self.change_members(
460+
members=[COMMUNITY_USER],
461+
liaisons=[CHAIR_USER, COMMUNITY_USER],
462+
)
463+
nomcom_group = Group.objects.get(pk=nomcom_group.pk) # refresh from db
464+
self.assertCountEqual(
465+
nomcom_group.role_set.filter(name='member').values_list('email__address', flat=True),
466+
[COMMUNITY_USER + EMAIL_DOMAIN],
467+
)
468+
self.assertCountEqual(
469+
nomcom_group.role_set.filter(name='liaison').values_list('email__address', flat=True),
470+
[CHAIR_USER + EMAIL_DOMAIN, COMMUNITY_USER + EMAIL_DOMAIN],
471+
)
472+
473+
# remove a liaison who is also a member and check that the members list is unchanged
474+
self.change_members(
475+
members=[COMMUNITY_USER],
476+
liaisons=[CHAIR_USER],
477+
)
478+
nomcom_group = Group.objects.get(pk=nomcom_group.pk) # refresh from db
479+
self.assertCountEqual(
480+
nomcom_group.role_set.filter(name='member').values_list('email__address', flat=True),
481+
[COMMUNITY_USER + EMAIL_DOMAIN],
482+
)
483+
self.assertCountEqual(
484+
nomcom_group.role_set.filter(name='liaison').values_list('email__address', flat=True),
485+
[CHAIR_USER + EMAIL_DOMAIN],
486+
)
487+
435488
def test_edit_nomcom_view(self):
436489
r = self.access_chair_url(self.edit_nomcom_url)
437490
q = PyQuery(r.content)

ietf/nomcom/views.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
from ietf.dbtemplate.models import DBTemplate
2323
from ietf.dbtemplate.views import group_template_edit, group_template_show
2424
from ietf.name.models import NomineePositionStateName, FeedbackTypeName
25-
from ietf.group.models import Group, GroupEvent, Role
25+
from ietf.group.models import Group, GroupEvent, Role
26+
from ietf.group.utils import update_role_set
2627
from ietf.message.models import Message
2728

2829
from ietf.nomcom.decorators import nomcom_private_key_required
@@ -31,7 +32,7 @@
3132
PrivateKeyForm, EditNomcomForm, EditNomineeForm,
3233
PendingFeedbackForm, ReminderDatesForm, FullFeedbackFormSet,
3334
FeedbackEmailForm, NominationResponseCommentForm, TopicForm,
34-
NewEditMembersForm, VolunteerForm, )
35+
EditMembersForm, VolunteerForm, )
3536
from ietf.nomcom.models import (Position, NomineePosition, Nominee, Feedback, NomCom, ReminderDates,
3637
FeedbackLastSeen, Topic, TopicFeedbackLastSeen, )
3738
from ietf.nomcom.utils import (get_nomcom_by_year, store_nomcom_private_key, suggest_affiliation,
@@ -1230,18 +1231,14 @@ def edit_members(request, year):
12301231
if nomcom.group.state_id=='conclude':
12311232
permission_denied(request, 'This nomcom is closed.')
12321233

1233-
old_members_email = [r.email for r in nomcom.group.role_set.filter(name='member')]
1234-
12351234
if request.method=='POST':
1236-
form = NewEditMembersForm(data=request.POST)
1235+
form = EditMembersForm(nomcom, data=request.POST)
12371236
if form.is_valid():
1238-
new_members_email = form.cleaned_data['members']
1239-
nomcom.group.role_set.filter( email__in=set(old_members_email)-set(new_members_email) ).delete()
1240-
for email in set(new_members_email)-set(old_members_email):
1241-
nomcom.group.role_set.create(email=email,person=email.person,name_id='member')
1237+
update_role_set(nomcom.group, 'member', form.cleaned_data['members'], request.user.person)
1238+
update_role_set(nomcom.group, 'liaison', form.cleaned_data['liaisons'], request.user.person)
12421239
return HttpResponseRedirect(reverse('ietf.nomcom.views.private_index',kwargs={'year':year}))
12431240
else:
1244-
form = NewEditMembersForm(initial={ 'members' : old_members_email })
1241+
form = EditMembersForm(nomcom)
12451242

12461243
return render(request, 'nomcom/new_edit_members.html',
12471244
{'nomcom' : nomcom,

0 commit comments

Comments
 (0)