Skip to content

Commit da9c9d8

Browse files
committed
Merged in [19710] from jennifer@painless-security.com:
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. - Legacy-Id: 19730 Note: SVN reference [19710] has been migrated to Git commit 8759955
2 parents e299df9 + 8759955 commit da9c9d8

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)