diff --git a/openreview/arr/arr.py b/openreview/arr/arr.py index aec01a931a..dd59cdc299 100644 --- a/openreview/arr/arr.py +++ b/openreview/arr/arr.py @@ -923,7 +923,14 @@ def setup_committee_matching(self, committee_id=None, compute_affinity_scores=Fa return self.venue.setup_committee_matching(committee_id, compute_affinity_scores, compute_conflicts, compute_conflicts_n_years, alternate_matching_group, submission_track) def set_assignments(self, assignment_title, committee_id, enable_reviewer_reassignment=False, overwrite=False): - return self.venue.set_assignments(assignment_title, committee_id, enable_reviewer_reassignment, overwrite) + match_group = self.client.get_group(committee_id) + assignment_invitation = self.client.get_invitation(self.get_assignment_id(match_group.id)) + conference_matching = matching.Matching( + self, + match_group, + submission_content=assignment_invitation.edit.get('head', {}).get('param', {}).get('withContent') + ) + return conference_matching.deploy(assignment_title, overwrite, enable_reviewer_reassignment) def unset_assignments(self, assignment_title, committee_id): return self.venue.unset_assignments(assignment_title, committee_id) diff --git a/openreview/arr/helpers.py b/openreview/arr/helpers.py index e445552bed..63f49da116 100644 --- a/openreview/arr/helpers.py +++ b/openreview/arr/helpers.py @@ -48,6 +48,31 @@ from openreview.stages.default_content import comment_v2 + +def update_emergency_assignment_deadline(client, venue, venue_id, emergency_assignment_deadline): + if not emergency_assignment_deadline: + return + + group_client = venue.client if hasattr(venue, 'client') and hasattr(venue.client, 'post_group_edit') else client + parsed_deadline = openreview.tools.datetime.datetime.strptime( + emergency_assignment_deadline, + '%Y/%m/%d %H:%M' + ) + group_client.post_group_edit( + invitation=venue.get_meta_invitation_id(), + readers=[venue.id], + writers=[venue.id], + signatures=[venue.id], + group=openreview.api.Group( + id=venue_id, + content={ + 'emergency_assignment_deadline': { + 'value': openreview.tools.datetime_millis(parsed_deadline) + } + } + ) + ) + class ARRWorkflow(object): UPDATE_WAIT_TIME = 5000 DEFAULT_AUTHOR_RESPONSE_EXTENSION_CRON = '0 */12 * * *' @@ -172,6 +197,12 @@ class ARRWorkflow(object): "order": 18, "required": False }, + "emergency_assignment_deadline": { + "description": "Assignments made on or after this date are marked as emergency.", + "value-regex": "^[0-9]{4}\\/([1-9]|0[1-9]|1[0-2])\\/([1-9]|0[1-9]|[1-2][0-9]|3[0-1])(\\s+)?((2[0-3]|[01][0-9]|[0-9]):[0-5][0-9])?(\\s+)?$", + "order": 58, + "required": False + }, "ae_checklist_start_date": { "description": "When should the action editor checklist open?", "value-regex": "^[0-9]{4}\\/([1-9]|0[1-9]|1[0-2])\\/([1-9]|0[1-9]|[1-2][0-9]|3[0-1])(\\s+)?((2[0-3]|[01][0-9]|[0-9]):[0-5][0-9])?(\\s+)?$", @@ -1412,6 +1443,25 @@ def __init__(self, client_v2, venue, configuration_note, request_form_id, suppor invitation=emg_score_inv ) + if role in [venue.get_reviewers_id(), venue.get_area_chairs_id()] and not openreview.tools.get_invitation(self.client_v2, f"{role}/-/Type"): + m._create_edge_invitation(f"{role}/-/Type") + type_inv = self.client_v2.get_invitation(f"{role}/-/Type") + type_inv.edit['weight']['param']['optional'] = True + type_inv.edit['label'] = { + "param": { + "regex": ".*", + "optional": True, + "deletable": True + } + } + self.client_v2.post_invitation_edit( + invitations=venue.get_meta_invitation_id(), + readers=[venue.id], + writers=[venue.id], + signatures=[venue.id], + invitation=type_inv + ) + for name in edge_invitation_names: if not openreview.tools.get_invitation(self.client_v2, f"{role}/-/{name}"): cmp_inv = self.client_v2.get_invitation(venue.get_custom_max_papers_id(m.match_group.id)) diff --git a/openreview/arr/invitation.py b/openreview/arr/invitation.py index e533e32d3e..f8af5f76c7 100644 --- a/openreview/arr/invitation.py +++ b/openreview/arr/invitation.py @@ -394,8 +394,24 @@ def set_submission_metadata_revision_invitation(self, arr_stage): self.save_invitation(invitation, replacement=False) return invitation - def set_assignment_invitation(self, committee_id, submission_content=None): - return self.venue_invitation_builder.set_assignment_invitation(committee_id, submission_content) + def set_assignment_invitation(self, committee_id, submission_content=None, cdate=None): + self.venue_invitation_builder.set_assignment_invitation( + committee_id, + submission_content=submission_content, + cdate=cdate + ) + invitation = self.client.get_invitation(self.venue.get_assignment_id(committee_id, deployed=True)) + + committee_name = self.venue.get_committee_name(committee_id) + if committee_name not in self.venue.reviewer_roles and committee_name not in self.venue.area_chair_roles: + return invitation + + arr_process = self.get_process_content('process/assignment_post_process.py') + if invitation.process == arr_process: + return invitation + + invitation.process = arr_process + return self.save_invitation(invitation, replacement=True) def set_group_matching_setup_invitations(self, committee_id): return self.venue_invitation_builder.set_group_matching_setup_invitations(committee_id) diff --git a/openreview/arr/process/assignment_post_process.py b/openreview/arr/process/assignment_post_process.py new file mode 100644 index 0000000000..f88dcff7f9 --- /dev/null +++ b/openreview/arr/process/assignment_post_process.py @@ -0,0 +1,119 @@ +def process_update(client, edge, invitation, existing_edge): + import openreview + + domain = client.get_group(edge.domain) + venue_id = domain.id + meta_invitation_id = domain.content['meta_invitation_id']['value'] + short_phrase = domain.content['subtitle']['value'] + contact = domain.content['contact']['value'] + program_chairs_id = domain.content['program_chairs_id']['value'] + submission_name = domain.content['submission_name']['value'] + reviewers_name = invitation.content['reviewers_name']['value'] + reviewers_id = invitation.content['reviewers_id']['value'] + sync_sac_id = invitation.content.get('sync_sac_id', {}).get('value') + sac_assignment_id = invitation.content.get('sac_assignment_id', {}).get('value') + sender = domain.get_content_value('message_sender') + pretty_name = openreview.tools.pretty_id(reviewers_name) + pretty_name = pretty_name[:-1] if pretty_name.endswith('s') else pretty_name + + note = client.get_note(edge.head) + group = client.get_group(f'{venue_id}/{submission_name}{note.number}/{reviewers_name}') + if edge.ddate and edge.tail in group.members: + assignment_edges = client.get_edges(invitation=edge.invitation, head=edge.head, tail=edge.tail) + if assignment_edges: + return + print(f'Remove member {edge.tail} from {group.id}') + client.remove_members_from_group(group.id, edge.tail) + + if sync_sac_id and sync_sac_id.format(number=note.number) not in edge.signatures: + print('Remove member from SAC group') + assignments = client.get_edges(invitation=sac_assignment_id, head=edge.tail) + if assignments: + client.remove_members_from_group(sync_sac_id.format(number=note.number), assignments[0].tail) + else: + print('No SAC assignments found') + + if not edge.ddate and edge.tail not in group.members: + print(f'Add member {edge.tail} to {group.id}') + client.add_members_to_group(group.id, edge.tail) + client.add_members_to_group(reviewers_id, edge.tail) + + if sync_sac_id: + print('Add the SAC to the paper group') + assignments = client.get_edges(invitation=sac_assignment_id, head=edge.tail) + if assignments: + client.add_members_to_group(sync_sac_id.format(number=note.number), assignments[0].tail) + else: + print('No SAC assignments found') + + signature = openreview.tools.pretty_id(edge.signatures[0]) + if venue_id in edge.signatures or program_chairs_id in edge.signatures: + signature = openreview.tools.pretty_id(program_chairs_id) + + subject = f'[{short_phrase}] You have been assigned as a {pretty_name} for paper number {note.number}' + message = f'''This is to inform you that you have been assigned as a {pretty_name} for paper number {note.number} for {short_phrase}. + +To review this new assignment, please login to OpenReview and go to https://openreview.net/forum?id={note.forum}. + +To check all of your assigned papers, go to https://openreview.net/group?id={reviewers_id}. + +Thank you, + +{signature}''' + + client.post_message( + subject, + [edge.tail], + message, + invitation=meta_invitation_id, + signature=venue_id, + parentGroup=group.id, + replyTo=contact, + sender=sender + ) + + # ----- ARR-specific logic ----- + deadline = domain.content.get('emergency_assignment_deadline', {}).get('value') + if deadline is None: + return + + type_invitation_id = f"{edge.invitation.split('/-/')[0]}/-/Type" + if edge.ddate: + client.delete_edges( + invitation=type_invitation_id, + head=edge.head, + tail=edge.tail, + wait_to_finish=True, + soft_delete=True + ) + return + + assignment_timestamp = edge.tcdate + ## clear type edges in case + if assignment_timestamp < deadline: + client.delete_edges( + invitation=type_invitation_id, + head=edge.head, + tail=edge.tail, + wait_to_finish=True, + soft_delete=True + ) + return + else: + existing_type_edges = client.get_edges( + invitation=type_invitation_id, + head=edge.head, + tail=edge.tail + ) + if existing_type_edges: + return + + client.post_edge( + openreview.api.Edge( + invitation=type_invitation_id, + signatures=[domain.id], + head=edge.head, + tail=edge.tail, + label='Emergency' + ) + ) diff --git a/openreview/arr/process/configuration_process.py b/openreview/arr/process/configuration_process.py index 506dde2591..cc4b769a16 100644 --- a/openreview/arr/process/configuration_process.py +++ b/openreview/arr/process/configuration_process.py @@ -14,4 +14,11 @@ def process(client, note, invitation): support_group = request_form.invitation.split('/-/')[0] venue = openreview.helpers.get_conference(client, request_form_id, support_group) - venue.set_arr_stages(note) \ No newline at end of file + openreview.arr.update_emergency_assignment_deadline( + client, + venue, + venue_id, + note.content.get('emergency_assignment_deadline') + ) + + venue.set_arr_stages(note) diff --git a/openreview/arr/webfield/areachairsWebfield.js b/openreview/arr/webfield/areachairsWebfield.js index b4f8ca9ea0..8871fcd0f5 100644 --- a/openreview/arr/webfield/areachairsWebfield.js +++ b/openreview/arr/webfield/areachairsWebfield.js @@ -52,6 +52,9 @@ browseProposedInvitations.push(`${reviewerGroup}/-/Seniority,head:ignore`) browseInvitations.push(`${reviewerGroup}/-/Status`) browseProposedInvitations.push(`${reviewerGroup}/-/Status`) +browseInvitations.push(`${reviewerGroup}/-/Type`) +browseProposedInvitations.push(`${reviewerGroup}/-/Type`) + browseInvitations.push(`${reviewerGroup}/-/Emergency_Score`) browseProposedInvitations.push(`${reviewerGroup}/-/Emergency_Score`) diff --git a/openreview/arr/webfield/programChairsWebfield.js b/openreview/arr/webfield/programChairsWebfield.js index 70d4ce2bcc..0a2723595c 100644 --- a/openreview/arr/webfield/programChairsWebfield.js +++ b/openreview/arr/webfield/programChairsWebfield.js @@ -9,6 +9,7 @@ const preferredEmailInvitationId = domain.content.preferred_emails_id?.value const browseInvitations = [ domain.content.reviewers_affinity_score_id?.value, domain.content.reviewers_conflict_id?.value, + `${reviewersId}/-/Type`, `${reviewersId}/-/Emergency_Score`, `${reviewersId}/-/Research_Area`, `${reviewersId}/-/Status`, @@ -40,6 +41,7 @@ if (areaChairName) { const browseInvitations = [ domain.content.area_chairs_affinity_score_id?.value, domain.content.area_chairs_conflict_id?.value, + `${areaChairsId}/-/Type`, `${areaChairsId}/-/Emergency_Score`, `${areaChairsId}/-/Research_Area`, `${areaChairsId}/-/Status`, diff --git a/openreview/arr/webfield/seniorAreaChairsWebfield.js b/openreview/arr/webfield/seniorAreaChairsWebfield.js index 2f414c0470..4179558719 100644 --- a/openreview/arr/webfield/seniorAreaChairsWebfield.js +++ b/openreview/arr/webfield/seniorAreaChairsWebfield.js @@ -11,6 +11,7 @@ const preferredEmailInvitationId = domain.content.preferred_emails_id?.value const browseAreaChairInvitations = [ `${areaChairsId}/-/Agreggate_Score`, domain.content.area_chairs_affinity_score_id?.value, + `${areaChairsId}/-/Type`, `${areaChairsId}/-/Emergency_Score`, `${areaChairsId}/-/Research_Area`, ].join(';') @@ -37,6 +38,7 @@ assignmentUrls[domain.content.area_chairs_name?.value] = { const browseReviewerInvitations = [ domain.content.reviewers_affinity_score_id?.value, domain.content.reviewers_conflict_id?.value, + `${reviewersId}/-/Type`, `${reviewersId}/-/Research_Area`, `${reviewersId}/-/Status`, `${reviewersId}/-/Emergency_Score`, diff --git a/openreview/venue/group.py b/openreview/venue/group.py index aa5058f629..492c41dfdb 100644 --- a/openreview/venue/group.py +++ b/openreview/venue/group.py @@ -325,6 +325,9 @@ def create_venue_group(self): if venue_group.content.get('reviewers_proposed_assignment_title'): content['reviewers_proposed_assignment_title'] = venue_group.content.get('reviewers_proposed_assignment_title') + if venue_group.content.get('emergency_assignment_deadline'): + content['emergency_assignment_deadline'] = venue_group.content.get('emergency_assignment_deadline') + if venue_group.content.get('allow_gurobi_solver'): content['allow_gurobi_solver'] = venue_group.content.get('allow_gurobi_solver') diff --git a/tests/test_arr_venue_v2.py b/tests/test_arr_venue_v2.py index 790d96d3a7..cfc2810ee1 100644 --- a/tests/test_arr_venue_v2.py +++ b/tests/test_arr_venue_v2.py @@ -248,10 +248,13 @@ def test_august_cycle(self, client, openreview_client, helpers, test_client, req assert openreview_client.get_group('aclweb.org/ACL/ARR/2023/August/Preferred_Emails_Readers') assert 'Emergency_Score' in openreview_client.get_group('aclweb.org/ACL/ARR/2023/August/Program_Chairs').web + assert '/-/Type' in openreview_client.get_group('aclweb.org/ACL/ARR/2023/August/Program_Chairs').web assert 'reviewers_invite_assignment_id' in openreview_client.get_group('aclweb.org/ACL/ARR/2023/August/Program_Chairs').web assert 'Emergency_Score' in openreview_client.get_group('aclweb.org/ACL/ARR/2023/August/Senior_Area_Chairs').web + assert '/-/Type' in openreview_client.get_group('aclweb.org/ACL/ARR/2023/August/Senior_Area_Chairs').web ac_group = openreview_client.get_group('aclweb.org/ACL/ARR/2023/August/Area_Chairs') assert 'Emergency_Score' in ac_group.web + assert '/-/Type' in ac_group.web openreview_client.post_group_edit( invitation='aclweb.org/ACL/ARR/2023/August/-/Edit', @@ -467,6 +470,10 @@ def test_august_cycle(self, client, openreview_client, helpers, test_client, req assert openreview_client.get_invitation('aclweb.org/ACL/ARR/2023/August/Area_Chairs/-/Recognition_Request').duedate > 0 assert openreview_client.get_invitation('aclweb.org/ACL/ARR/2023/August/Reviewers/-/License_Agreement').duedate > 0 assert openreview_client.get_invitation('aclweb.org/ACL/ARR/2023/August/Area_Chairs/-/Metareview_License_Agreement').duedate > 0 + assert openreview_client.get_invitation('aclweb.org/ACL/ARR/2023/August/Reviewers/-/Type') + assert openreview_client.get_invitation('aclweb.org/ACL/ARR/2023/August/Area_Chairs/-/Type') + assert not openreview.tools.get_invitation(openreview_client, 'aclweb.org/ACL/ARR/2023/August/Senior_Area_Chairs/-/Type') + assert not openreview.tools.get_invitation(openreview_client, 'aclweb.org/ACL/ARR/2023/August/Ethics_Reviewers/-/Type') # Pin 2023 and 2024 into next available year task_array = [ @@ -4191,14 +4198,77 @@ def test_sae_ae_assignments(self, client, openreview_client, helpers, test_clien assignment_invitation = openreview_client.get_invitation('aclweb.org/ACL/ARR/2023/August/Area_Chairs/-/Assignment') assert 'sync_sac_id' not in assignment_invitation.content - # Remove an AC and replace sac_client = openreview.api.OpenReviewClient(username = 'sac2@aclrollingreview.com', password=helpers.strong_password) + + existing_reviewer_assignments = openreview_client.get_edges( + invitation='aclweb.org/ACL/ARR/2023/August/Reviewers/-/Assignment', + head=submissions[0].id, + tail='~Reviewer_ARRFour1' + ) + assert len(existing_reviewer_assignments) == 1 + existing_reviewer_assignment = existing_reviewer_assignments[0] + assert len(openreview_client.get_edges( + invitation='aclweb.org/ACL/ARR/2023/August/Reviewers/-/Type', + head=existing_reviewer_assignment.head, + tail=existing_reviewer_assignment.tail + )) == 0 + assert len(sac_client.get_edges(invitation = 'aclweb.org/ACL/ARR/2023/August/Area_Chairs/-/Assignment', head=submissions[1].id, tail='~AC_ARRTwo1')) == 1 ac_edge = sac_client.get_edges(invitation = 'aclweb.org/ACL/ARR/2023/August/Area_Chairs/-/Assignment', head=submissions[1].id, tail='~AC_ARRTwo1')[0] + assert len(openreview_client.get_edges( + invitation='aclweb.org/ACL/ARR/2023/August/Area_Chairs/-/Type', + head=ac_edge.head, + tail=ac_edge.tail + )) == 0 + + emergency_assignment_deadline = datetime.datetime.strptime( + datetime.datetime.now().strftime('%Y/%m/%d %H:%M'), + '%Y/%m/%d %H:%M' + ) + pc_client.post_note( + openreview.Note( + content={ + 'emergency_assignment_deadline': emergency_assignment_deadline.strftime('%Y/%m/%d %H:%M') + }, + invitation=f'openreview.net/Support/-/Request{request_form.number}/ARR_Configuration', + forum=request_form.id, + readers=['aclweb.org/ACL/ARR/2023/August/Program_Chairs', 'openreview.net/Support'], + referent=request_form.id, + replyto=request_form.id, + signatures=['~Program_ARRChair1'], + writers=[], + ) + ) + helpers.await_queue() + + domain = openreview_client.get_group('aclweb.org/ACL/ARR/2023/August') + assert domain.content['emergency_assignment_deadline']['value'] == openreview.tools.datetime_millis(emergency_assignment_deadline) + assert len(openreview_client.get_edges( + invitation='aclweb.org/ACL/ARR/2023/August/Reviewers/-/Type', + head=existing_reviewer_assignment.head, + tail=existing_reviewer_assignment.tail + )) == 0 + assert len(openreview_client.get_edges( + invitation='aclweb.org/ACL/ARR/2023/August/Area_Chairs/-/Type', + head=ac_edge.head, + tail=ac_edge.tail + )) == 0 + + august_venue.venue.group_builder.create_venue_group() + helpers.await_queue() + domain = openreview_client.get_group('aclweb.org/ACL/ARR/2023/August') + assert domain.content['emergency_assignment_deadline']['value'] == openreview.tools.datetime_millis(emergency_assignment_deadline) + + # Remove an AC and replace ac_edge.ddate = openreview.tools.datetime_millis(openreview.tools.datetime.datetime.now()) openreview_client.post_edge(ac_edge) helpers.await_queue_edit(openreview_client, invitation='aclweb.org/ACL/ARR/2023/August/Area_Chairs/-/Assignment') + assert len(openreview_client.get_edges( + invitation='aclweb.org/ACL/ARR/2023/August/Area_Chairs/-/Type', + head=ac_edge.head, + tail=ac_edge.tail + )) == 0 # ~AC_ARROne1 has CMP=0 from June migration, update quota to allow assignment in August ac_cmp_edge = openreview_client.get_edges( @@ -4219,6 +4289,13 @@ def test_sae_ae_assignments(self, client, openreview_client, helpers, test_clien helpers.await_queue_edit(openreview_client, edit_id=edge.id) assert len(sac_client.get_edges(invitation = 'aclweb.org/ACL/ARR/2023/August/Area_Chairs/-/Assignment', head=submissions[1].id, tail='~AC_ARROne1')) == 1 + ac_type_edges = openreview_client.get_edges( + invitation='aclweb.org/ACL/ARR/2023/August/Area_Chairs/-/Type', + head=submissions[1].id, + tail='~AC_ARROne1' + ) + assert len(ac_type_edges) == 1 + assert ac_type_edges[0].label == 'Emergency' assert len(sac_client.get_group('aclweb.org/ACL/ARR/2023/August/Submission2/Area_Chairs').members) == 1 assert sac_client.get_group('aclweb.org/ACL/ARR/2023/August/Submission2/Area_Chairs').members[0] == '~AC_ARROne1' @@ -4493,11 +4570,32 @@ def test_sae_ae_assignments(self, client, openreview_client, helpers, test_clien weight = 1 )) helpers.await_queue_edit(openreview_client, edit_id=test_assignment_edge.id) - + reviewer_type_edges = openreview_client.get_edges( + invitation='aclweb.org/ACL/ARR/2023/August/Reviewers/-/Type', + head=submissions[1].id, + tail='~Reviewer_ARRFive1' + ) + assert len(reviewer_type_edges) == 1 + assert reviewer_type_edges[0].label == 'Emergency' + ## Delete that assignment edge test_assignment_edge.ddate = openreview.tools.datetime_millis(now) openreview_client.post_edge(test_assignment_edge) helpers.await_queue_edit(openreview_client, edit_id=test_assignment_edge.id) + deadline = time.time() + 30 + reviewer_type_edges = openreview_client.get_edges( + invitation='aclweb.org/ACL/ARR/2023/August/Reviewers/-/Type', + head=submissions[1].id, + tail='~Reviewer_ARRFive1' + ) + while reviewer_type_edges and time.time() < deadline: + time.sleep(0.5) + reviewer_type_edges = openreview_client.get_edges( + invitation='aclweb.org/ACL/ARR/2023/August/Reviewers/-/Type', + head=submissions[1].id, + tail='~Reviewer_ARRFive1' + ) + assert len(reviewer_type_edges) == 0 ## Post a new invite assignment edge (should succeed with quota = 4) test_invite_edge = openreview_client.post_edge(openreview.api.Edge(