diff --git a/ietf/ietfauth/utils.py b/ietf/ietfauth/utils.py index b4c6da14ea..aac579dd1f 100644 --- a/ietf/ietfauth/utils.py +++ b/ietf/ietfauth/utils.py @@ -137,6 +137,10 @@ def has_role(user, role_names, *args, **kwargs): group__type="sdo", group__state="active", ), + "Liaison Coordinator": Q( + name="liaison_coordinator", + group__acronym="iab", + ), "Authorized Individual": Q( name="auth", group__type="sdo", diff --git a/ietf/liaisons/forms.py b/ietf/liaisons/forms.py index d579011b6c..a116a0e784 100644 --- a/ietf/liaisons/forms.py +++ b/ietf/liaisons/forms.py @@ -104,13 +104,14 @@ def internal_groups_for_person(person: Optional[Person]): if has_role( person.user, ( - "Secretariat", - "IETF Chair", - "IAB Chair", - "IAB Executive Director", - "Liaison Manager", - "Authorized Individual", - ), # todo liaison coordinator as well + "Secretariat", + "IETF Chair", + "IAB Chair", + "IAB Executive Director", + "Liaison Manager", + "Liaison Coordinator", + "Authorized Individual", + ), ): return all_internal_groups() # Interesting roles, as Group queries @@ -491,8 +492,12 @@ def is_approved(self): return True def get_post_only(self): - from_groups = self.cleaned_data.get('from_groups') - if has_role(self.user, "Secretariat") or is_authorized_individual(self.user,from_groups): + from_groups = self.cleaned_data.get("from_groups") + if ( + has_role(self.user, "Secretariat") + or has_role(self.user, "Liaison Coordinator") + or is_authorized_individual(self.user, from_groups) + ): return False return True @@ -505,8 +510,14 @@ def set_from_fields(self): if len(qs) == 1: self.fields['from_groups'].initial = qs - if not has_role(self.user, "Secretariat"): - self.fields["from_contact"].initial = self.person.role_set.filter(group=qs[0]).first().email.address + # Note that the IAB chair currently doesn't get to work with incoming liaison statements + if not ( + has_role(self.user, "Secretariat") + or has_role(self.user, "Liaison Coordinator") + ): + self.fields["from_contact"].initial = ( + self.person.role_set.filter(group=qs[0]).first().email.address + ) self.fields["from_contact"].widget.attrs["disabled"] = True def set_to_fields(self): diff --git a/ietf/liaisons/tests.py b/ietf/liaisons/tests.py index 1742687f14..865de35f74 100644 --- a/ietf/liaisons/tests.py +++ b/ietf/liaisons/tests.py @@ -462,11 +462,12 @@ def test_edit_liaison(self): def test_incoming_access(self): - '''Ensure only Secretariat, Liaison Managers, and Authorized Individuals + '''Ensure only Secretariat, Liaison Managers, Liaison Coordinators, and Authorized Individuals have access to incoming liaisons. ''' sdo = RoleFactory(name_id='liaiman',group__type_id='sdo', person__user__username='ulm-liaiman').group RoleFactory(name_id='auth',group=sdo,person__user__username='ulm-auth') + RoleFactory(name_id='liaison_coordinator', group__acronym='iab', person__user__username='liaison-coordinator') stmt = LiaisonStatementFactory(from_groups=[sdo,]) LiaisonStatementEventFactory(statement=stmt,type_id='posted') RoleFactory(name_id='chair',person__user__username='marschairman',group__acronym='mars') @@ -499,6 +500,15 @@ def test_incoming_access(self): r = self.client.get(addurl) self.assertEqual(r.status_code, 200) + # Liaison Coordinator has access + self.client.login(username="liaison-coordinator", password="liaison-coordinator+password") + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertEqual(len(q('a.btn:contains("New incoming liaison")')), 1) + r = self.client.get(addurl) + self.assertEqual(r.status_code, 200) + # Authorized Individual has access self.client.login(username="ulm-auth", password="ulm-auth+password") r = self.client.get(url) @@ -521,6 +531,7 @@ def test_outgoing_access(self): sdo = RoleFactory(name_id='liaiman',group__type_id='sdo', person__user__username='ulm-liaiman').group RoleFactory(name_id='auth',group=sdo,person__user__username='ulm-auth') + RoleFactory(name_id='liaison_coordinator', group__acronym='iab', person__user__username='liaison-coordinator') mars = RoleFactory(name_id='chair',person__user__username='marschairman',group__acronym='mars').group RoleFactory(name_id='secr',group=mars,person__user__username='mars-secr') RoleFactory(name_id='execdir',group=Group.objects.get(acronym='iab'),person__user__username='iab-execdir') @@ -599,6 +610,15 @@ def test_outgoing_access(self): r = self.client.get(addurl) self.assertEqual(r.status_code, 200) + # Liaison Coordinator has access + self.assertTrue(self.client.login(username="liaison-coordinator", password="liaison-coordinator+password")) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertEqual(len(q('a.btn:contains("New outgoing liaison")')), 1) + r = self.client.get(addurl) + self.assertEqual(r.status_code, 200) + # Authorized Individual has no access self.assertTrue(self.client.login(username="ulm-auth", password="ulm-auth+password")) r = self.client.get(url) diff --git a/ietf/liaisons/tests_forms.py b/ietf/liaisons/tests_forms.py index 512cf40d07..c2afddea65 100644 --- a/ietf/liaisons/tests_forms.py +++ b/ietf/liaisons/tests_forms.py @@ -156,6 +156,7 @@ def test_external_groups_for_person(self): group=Group.objects.get(acronym="iab"), person__user__username="iab-execdir", ) + RoleFactory(name_id="liaison_coordinator", group__acronym="iab", person__user__username="liaison-coordinator") the_sdo = GroupFactory(type_id="sdo", acronym="the-sdo") liaison_manager = RoleFactory(name_id="liaiman", group=the_sdo).person authperson = RoleFactory(name_id="auth", group=the_sdo).person @@ -166,6 +167,7 @@ def test_external_groups_for_person(self): "ietf-chair", "iab-chair", "iab-execdir", + "liaison-coordinator", "ad", "sopschairman", "sopssecretary", diff --git a/ietf/liaisons/utils.py b/ietf/liaisons/utils.py index 05c7c8a85c..98855b4650 100644 --- a/ietf/liaisons/utils.py +++ b/ietf/liaisons/utils.py @@ -11,13 +11,14 @@ "IAB Executive Director", "IETF Chair", "Liaison Manager", + "Liaison Coordinator", "Secretariat", "WG Chair", "WG Secretary", ] # Roles allowed to create and manage incoming liaison statements. -INCOMING_LIAISON_ROLES = ["Authorized Individual", "Liaison Manager", "Secretariat"] +INCOMING_LIAISON_ROLES = ["Authorized Individual", "Liaison Manager", "Liaison Coordinator", "Secretariat"] can_submit_liaison_required = passes_test_decorator( lambda u, *args, **kwargs: can_add_liaison(u), diff --git a/ietf/liaisons/views.py b/ietf/liaisons/views.py index a8e80a5194..a4f7d06f91 100644 --- a/ietf/liaisons/views.py +++ b/ietf/liaisons/views.py @@ -196,7 +196,13 @@ def post_only(group,person): - Authorized Individuals have full access for the group they are associated with - Liaison Managers can post only ''' - if group.type_id == 'sdo' and ( not(has_role(person.user,"Secretariat") or group.role_set.filter(name='auth',person=person)) ): + if group.type_id == "sdo" and ( + not ( + has_role(person.user, "Secretariat") + or has_role(person.user, "Liaison Coordinator") + or group.role_set.filter(name="auth", person=person) + ) + ): return True else: return False diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 15ae71d849..cfb866e02a 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -5392,6 +5392,21 @@ "model": "mailtrigger.mailtrigger", "pk": "review_completed_opsdir_telechat" }, + { + "fields": { + "cc": [ + "ietf_last_call", + "review_doc_all_parties", + "review_doc_group_mail_list" + ], + "desc": "Recipients when a perfmetrdir Telechat review is completed", + "to": [ + "review_team_mail_list" + ] + }, + "model": "mailtrigger.mailtrigger", + "pk": "review_completed_perfmetrdir_telechat" + }, { "fields": { "cc": [ @@ -11330,6 +11345,17 @@ "model": "name.extresourcename", "pk": "mailing_list_archive" }, + { + "fields": { + "desc": "ORCID", + "name": "ORCID", + "order": 0, + "type": "url", + "used": true + }, + "model": "name.extresourcename", + "pk": "orcid" + }, { "fields": { "desc": "Related Implementations", @@ -13562,7 +13588,7 @@ "desc": "", "name": "Liaison CC Contact", "order": 9, - "used": true + "used": false }, "model": "name.rolename", "pk": "liaison_cc_contact" @@ -13572,11 +13598,21 @@ "desc": "", "name": "Liaison Contact", "order": 8, - "used": true + "used": false }, "model": "name.rolename", "pk": "liaison_contact" }, + { + "fields": { + "desc": "Coordinates liaison handling for the IAB", + "name": "Liaison Coordinator", + "order": 0, + "used": true + }, + "model": "name.rolename", + "pk": "liaison_coordinator" + }, { "fields": { "desc": "", diff --git a/ietf/name/migrations/0018_alter_liaisonrolenames.py b/ietf/name/migrations/0018_alter_liaisonrolenames.py new file mode 100644 index 0000000000..5d57f34a64 --- /dev/null +++ b/ietf/name/migrations/0018_alter_liaisonrolenames.py @@ -0,0 +1,33 @@ +# Copyright The IETF Trust 2025, All Rights Reserved# Generated by Django 4.2.21 on 2025-05-30 16:35 + +from django.db import migrations + + +def forward(apps, schema_editor): + RoleName = apps.get_model("name", "RoleName") + RoleName.objects.filter(slug__in=["liaison_contact", "liaison_cc_contact"]).update( + used=False + ) + RoleName.objects.get_or_create( + slug="liaison_coordinator", + defaults={ + "name": "Liaison Coordinator", + "desc": "Coordinates liaison handling for the IAB", + }, + ) + + +def reverse(apps, schema_editor): + RoleName = apps.get_model("name", "RoleName") + RoleName.objects.filter(slug__in=["liaison_contact", "liaison_cc_contact"]).update( + used=True + ) + RoleName.objects.filter(slug="liaison_coordinator").delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("name", "0017_populate_new_reg_names"), + ] + + operations = [migrations.RunPython(forward, reverse)]