diff --git a/openreview/arr/webfield/programChairsWebfield.js b/openreview/arr/webfield/programChairsWebfield.js index 70d4ce2bcc..9b29066c78 100644 --- a/openreview/arr/webfield/programChairsWebfield.js +++ b/openreview/arr/webfield/programChairsWebfield.js @@ -189,6 +189,175 @@ return { return hasReply; }) return metaReviewReplies?.length??0; + `, + reviewerEmergencyDeclarationCount: ` + const replies = row.note?.details?.replies ?? []; + const reviewers = row.reviewers ?? []; + const signatureToReviewerIndex = new Map(); + const reviewersWithEmergencyDeclaration = new Set(); + + reviewers.forEach((reviewer, reviewerIndex) => { + if (reviewer?.anonymizedGroup) { + signatureToReviewerIndex.set(reviewer.anonymizedGroup, reviewerIndex); + } + if (reviewer?.preferredId) { + signatureToReviewerIndex.set(reviewer.preferredId, reviewerIndex); + } + }); + + replies.forEach(reply => { + const replySignature = reply?.signatures?.[0]; + const isEmergencyDeclaration = (reply?.invitations ?? []).some(invitation => invitation.includes('Emergency_Declaration')); + + if (!isEmergencyDeclaration || !signatureToReviewerIndex.has(replySignature)) { + return; + } + + reviewersWithEmergencyDeclaration.add(signatureToReviewerIndex.get(replySignature)); + }); + + return reviewersWithEmergencyDeclaration.size; + `, + areaChairEmergencyDeclarationCount: ` + const replies = row.note?.details?.replies ?? []; + const areaChairs = row.metaReviewData?.areaChairs ?? []; + const signatureToAreaChairIndex = new Map(); + const areaChairsWithEmergencyDeclaration = new Set(); + + areaChairs.forEach((areaChair, areaChairIndex) => { + if (areaChair?.anonymizedGroup) { + signatureToAreaChairIndex.set(areaChair.anonymizedGroup, areaChairIndex); + } + if (areaChair?.preferredId) { + signatureToAreaChairIndex.set(areaChair.preferredId, areaChairIndex); + } + }); + + replies.forEach(reply => { + const replySignature = reply?.signatures?.[0]; + const isEmergencyDeclaration = (reply?.invitations ?? []).some(invitation => invitation.includes('Emergency_Declaration')); + + if (!isEmergencyDeclaration || !signatureToAreaChairIndex.has(replySignature)) { + return; + } + + areaChairsWithEmergencyDeclaration.add(signatureToAreaChairIndex.get(replySignature)); + }); + + return areaChairsWithEmergencyDeclaration.size; + `, + reviewerDelayNotificationCount: ` + const replies = row.note?.details?.replies ?? []; + const reviewers = row.reviewers ?? []; + const signatureToReviewerIndex = new Map(); + const reviewersWithDelayNotification = new Set(); + + reviewers.forEach((reviewer, reviewerIndex) => { + if (reviewer?.anonymizedGroup) { + signatureToReviewerIndex.set(reviewer.anonymizedGroup, reviewerIndex); + } + if (reviewer?.preferredId) { + signatureToReviewerIndex.set(reviewer.preferredId, reviewerIndex); + } + }); + + replies.forEach(reply => { + const replySignature = reply?.signatures?.[0]; + const isDelayNotification = (reply?.invitations ?? []).some(invitation => invitation.includes('Delay_Notification')); + + if (!isDelayNotification || !signatureToReviewerIndex.has(replySignature)) { + return; + } + + reviewersWithDelayNotification.add(signatureToReviewerIndex.get(replySignature)); + }); + + return reviewersWithDelayNotification.size; + `, + areaChairDelayNotificationCount: ` + const replies = row.note?.details?.replies ?? []; + const areaChairs = row.metaReviewData?.areaChairs ?? []; + const signatureToAreaChairIndex = new Map(); + const areaChairsWithDelayNotification = new Set(); + + areaChairs.forEach((areaChair, areaChairIndex) => { + if (areaChair?.anonymizedGroup) { + signatureToAreaChairIndex.set(areaChair.anonymizedGroup, areaChairIndex); + } + if (areaChair?.preferredId) { + signatureToAreaChairIndex.set(areaChair.preferredId, areaChairIndex); + } + }); + + replies.forEach(reply => { + const replySignature = reply?.signatures?.[0]; + const isDelayNotification = (reply?.invitations ?? []).some(invitation => invitation.includes('Delay_Notification')); + + if (!isDelayNotification || !signatureToAreaChairIndex.has(replySignature)) { + return; + } + + areaChairsWithDelayNotification.add(signatureToAreaChairIndex.get(replySignature)); + }); + + return areaChairsWithDelayNotification.size; + `, + assignedReviewersMinusEmergencyDeclarationsCount: ` + const replies = row.note?.details?.replies ?? []; + const reviewers = row.reviewers ?? []; + const signatureToReviewerIndex = new Map(); + const reviewersWithEmergencyDeclaration = new Set(); + + reviewers.forEach((reviewer, reviewerIndex) => { + if (reviewer?.anonymizedGroup) { + signatureToReviewerIndex.set(reviewer.anonymizedGroup, reviewerIndex); + } + if (reviewer?.preferredId) { + signatureToReviewerIndex.set(reviewer.preferredId, reviewerIndex); + } + }); + + replies.forEach(reply => { + const replySignature = reply?.signatures?.[0]; + const isEmergencyDeclaration = (reply?.invitations ?? []).some(invitation => invitation.includes('Emergency_Declaration')); + + if (!isEmergencyDeclaration || !signatureToReviewerIndex.has(replySignature)) { + return; + } + + reviewersWithEmergencyDeclaration.add(signatureToReviewerIndex.get(replySignature)); + }); + + return Math.max(0, reviewers.length - reviewersWithEmergencyDeclaration.size); + `, + completedReviewsPlusDelayNotificationsCount: ` + const replies = row.note?.details?.replies ?? []; + const reviewers = row.reviewers ?? []; + const officialReviews = row.officialReviews ?? []; + const signatureToReviewerIndex = new Map(); + const reviewersWithDelayNotification = new Set(); + + reviewers.forEach((reviewer, reviewerIndex) => { + if (reviewer?.anonymizedGroup) { + signatureToReviewerIndex.set(reviewer.anonymizedGroup, reviewerIndex); + } + if (reviewer?.preferredId) { + signatureToReviewerIndex.set(reviewer.preferredId, reviewerIndex); + } + }); + + replies.forEach(reply => { + const replySignature = reply?.signatures?.[0]; + const isDelayNotification = (reply?.invitations ?? []).some(invitation => invitation.includes('Delay_Notification')); + + if (!isDelayNotification || !signatureToReviewerIndex.has(replySignature)) { + return; + } + + reviewersWithDelayNotification.add(signatureToReviewerIndex.get(replySignature)); + }); + + return officialReviews.length + reviewersWithDelayNotification.size; ` }, reviewerEmailFuncs: [ diff --git a/openreview/arr/webfield/seniorAreaChairsWebfield.js b/openreview/arr/webfield/seniorAreaChairsWebfield.js index 2f414c0470..75bfbef46f 100644 --- a/openreview/arr/webfield/seniorAreaChairsWebfield.js +++ b/openreview/arr/webfield/seniorAreaChairsWebfield.js @@ -128,6 +128,175 @@ return { return hasReply; }) return metaReviewReplies?.length??0; + `, + reviewerEmergencyDeclarationCount: ` + const replies = row.note?.details?.replies ?? []; + const reviewers = row.reviewers ?? []; + const signatureToReviewerIndex = new Map(); + const reviewersWithEmergencyDeclaration = new Set(); + + reviewers.forEach((reviewer, reviewerIndex) => { + if (reviewer?.anonymizedGroup) { + signatureToReviewerIndex.set(reviewer.anonymizedGroup, reviewerIndex); + } + if (reviewer?.preferredId) { + signatureToReviewerIndex.set(reviewer.preferredId, reviewerIndex); + } + }); + + replies.forEach(reply => { + const replySignature = reply?.signatures?.[0]; + const isEmergencyDeclaration = (reply?.invitations ?? []).some(invitation => invitation.includes('Emergency_Declaration')); + + if (!isEmergencyDeclaration || !signatureToReviewerIndex.has(replySignature)) { + return; + } + + reviewersWithEmergencyDeclaration.add(signatureToReviewerIndex.get(replySignature)); + }); + + return reviewersWithEmergencyDeclaration.size; + `, + areaChairEmergencyDeclarationCount: ` + const replies = row.note?.details?.replies ?? []; + const areaChairs = row.metaReviewData?.areaChairs ?? []; + const signatureToAreaChairIndex = new Map(); + const areaChairsWithEmergencyDeclaration = new Set(); + + areaChairs.forEach((areaChair, areaChairIndex) => { + if (areaChair?.anonymizedGroup) { + signatureToAreaChairIndex.set(areaChair.anonymizedGroup, areaChairIndex); + } + if (areaChair?.preferredId) { + signatureToAreaChairIndex.set(areaChair.preferredId, areaChairIndex); + } + }); + + replies.forEach(reply => { + const replySignature = reply?.signatures?.[0]; + const isEmergencyDeclaration = (reply?.invitations ?? []).some(invitation => invitation.includes('Emergency_Declaration')); + + if (!isEmergencyDeclaration || !signatureToAreaChairIndex.has(replySignature)) { + return; + } + + areaChairsWithEmergencyDeclaration.add(signatureToAreaChairIndex.get(replySignature)); + }); + + return areaChairsWithEmergencyDeclaration.size; + `, + reviewerDelayNotificationCount: ` + const replies = row.note?.details?.replies ?? []; + const reviewers = row.reviewers ?? []; + const signatureToReviewerIndex = new Map(); + const reviewersWithDelayNotification = new Set(); + + reviewers.forEach((reviewer, reviewerIndex) => { + if (reviewer?.anonymizedGroup) { + signatureToReviewerIndex.set(reviewer.anonymizedGroup, reviewerIndex); + } + if (reviewer?.preferredId) { + signatureToReviewerIndex.set(reviewer.preferredId, reviewerIndex); + } + }); + + replies.forEach(reply => { + const replySignature = reply?.signatures?.[0]; + const isDelayNotification = (reply?.invitations ?? []).some(invitation => invitation.includes('Delay_Notification')); + + if (!isDelayNotification || !signatureToReviewerIndex.has(replySignature)) { + return; + } + + reviewersWithDelayNotification.add(signatureToReviewerIndex.get(replySignature)); + }); + + return reviewersWithDelayNotification.size; + `, + areaChairDelayNotificationCount: ` + const replies = row.note?.details?.replies ?? []; + const areaChairs = row.metaReviewData?.areaChairs ?? []; + const signatureToAreaChairIndex = new Map(); + const areaChairsWithDelayNotification = new Set(); + + areaChairs.forEach((areaChair, areaChairIndex) => { + if (areaChair?.anonymizedGroup) { + signatureToAreaChairIndex.set(areaChair.anonymizedGroup, areaChairIndex); + } + if (areaChair?.preferredId) { + signatureToAreaChairIndex.set(areaChair.preferredId, areaChairIndex); + } + }); + + replies.forEach(reply => { + const replySignature = reply?.signatures?.[0]; + const isDelayNotification = (reply?.invitations ?? []).some(invitation => invitation.includes('Delay_Notification')); + + if (!isDelayNotification || !signatureToAreaChairIndex.has(replySignature)) { + return; + } + + areaChairsWithDelayNotification.add(signatureToAreaChairIndex.get(replySignature)); + }); + + return areaChairsWithDelayNotification.size; + `, + assignedReviewersMinusEmergencyDeclarationsCount: ` + const replies = row.note?.details?.replies ?? []; + const reviewers = row.reviewers ?? []; + const signatureToReviewerIndex = new Map(); + const reviewersWithEmergencyDeclaration = new Set(); + + reviewers.forEach((reviewer, reviewerIndex) => { + if (reviewer?.anonymizedGroup) { + signatureToReviewerIndex.set(reviewer.anonymizedGroup, reviewerIndex); + } + if (reviewer?.preferredId) { + signatureToReviewerIndex.set(reviewer.preferredId, reviewerIndex); + } + }); + + replies.forEach(reply => { + const replySignature = reply?.signatures?.[0]; + const isEmergencyDeclaration = (reply?.invitations ?? []).some(invitation => invitation.includes('Emergency_Declaration')); + + if (!isEmergencyDeclaration || !signatureToReviewerIndex.has(replySignature)) { + return; + } + + reviewersWithEmergencyDeclaration.add(signatureToReviewerIndex.get(replySignature)); + }); + + return Math.max(0, reviewers.length - reviewersWithEmergencyDeclaration.size); + `, + completedReviewsPlusDelayNotificationsCount: ` + const replies = row.note?.details?.replies ?? []; + const reviewers = row.reviewers ?? []; + const officialReviews = row.officialReviews ?? []; + const signatureToReviewerIndex = new Map(); + const reviewersWithDelayNotification = new Set(); + + reviewers.forEach((reviewer, reviewerIndex) => { + if (reviewer?.anonymizedGroup) { + signatureToReviewerIndex.set(reviewer.anonymizedGroup, reviewerIndex); + } + if (reviewer?.preferredId) { + signatureToReviewerIndex.set(reviewer.preferredId, reviewerIndex); + } + }); + + replies.forEach(reply => { + const replySignature = reply?.signatures?.[0]; + const isDelayNotification = (reply?.invitations ?? []).some(invitation => invitation.includes('Delay_Notification')); + + if (!isDelayNotification || !signatureToReviewerIndex.has(replySignature)) { + return; + } + + reviewersWithDelayNotification.add(signatureToReviewerIndex.get(replySignature)); + }); + + return officialReviews.length + reviewersWithDelayNotification.size; ` } } diff --git a/tests/test_arr_venue_v2.py b/tests/test_arr_venue_v2.py index 790d96d3a7..14c77cee40 100644 --- a/tests/test_arr_venue_v2.py +++ b/tests/test_arr_venue_v2.py @@ -9,6 +9,7 @@ import sys from copy import deepcopy from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC @@ -6942,13 +6943,44 @@ def test_reviewer_management_forms(self, client, openreview_client, helpers, tes assert 'above and beyond' in great_ac_note.content['justification']['value'] def test_email_options(self, client, openreview_client, helpers, test_client, request_page, selenium): + venue_id = 'aclweb.org/ACL/ARR/2023/August' pc_client = openreview.api.OpenReviewClient(username='pc@aclrollingreview.org', password=helpers.strong_password) - submissions = pc_client.get_notes(invitation='aclweb.org/ACL/ARR/2023/August/-/Submission', sort='number:asc') + pc_console_client = openreview.Client(username='pc@aclrollingreview.org', password=helpers.strong_password) + submissions = pc_client.get_notes(invitation=f'{venue_id}/-/Submission', sort='number:asc') submissions_by_number = {s.number : s for s in submissions} submissions_by_id = {s.id : s for s in submissions} now = datetime.datetime.now() now_millis = openreview.tools.datetime_millis(now) - + + def assert_submission_query(console_group_id, console_client, query_text, expected_numbers): + request_page( + selenium, + f'http://localhost:3030/group?id={console_group_id}#submission-status', + console_client, + wait_for_element='header' + ) + selenium.find_element(By.LINK_TEXT, 'Submission Status').click() + paper_status = WebDriverWait(selenium, 10).until( + lambda driver: driver.find_element(By.ID, 'submission-status') + ) + search_input = paper_status.find_element(By.CLASS_NAME, 'search-input') + search_input.clear() + search_input.send_keys(query_text) + search_input.send_keys(Keys.ENTER) + + def get_displayed_paper_numbers(driver): + note_numbers = driver.find_element(By.ID, 'submission-status').find_elements(By.CLASS_NAME, 'note-number') + return [int(note_number.text) for note_number in note_numbers] + + assert WebDriverWait(selenium, 20).until( + lambda driver: get_displayed_paper_numbers(driver) == expected_numbers + ) + + sac_console_client = openreview.Client( + username='sac2@aclrollingreview.com', + password=helpers.strong_password + ) + ## Build missing data # Reviewer who is available and responded to emergency form helpers.create_user('reviewer7@aclrollingreview.com', 'Reviewer', 'ARRSeven') @@ -7122,6 +7154,153 @@ def test_email_options(self, client, openreview_client, helpers, test_client, re ) ) + # Additional submission-status query data + ac_one_client = openreview.api.OpenReviewClient(username='ac1@aclrollingreview.com', password=helpers.strong_password) + ac_one_sig = ac_one_client.get_groups( + prefix=f'{venue_id}/Submission2/Area_Chair_', + signatory='~AC_ARROne1' + )[0].id + ac_delay_edit = ac_one_client.post_note_edit( + invitation=f'{venue_id}/Submission2/-/Delay_Notification', + signatures=[ac_one_sig], + note=openreview.api.Note( + content={ + 'notification': {'value': 'My meta-review will be submitted tomorrow due to an unexpected emergency.'} + } + ) + ) + assert ac_one_client.get_note(ac_delay_edit['note']['id']) + ac_emergency_edit = ac_one_client.post_note_edit( + invitation=f'{venue_id}/Submission2/-/Emergency_Declaration', + signatures=[ac_one_sig], + note=openreview.api.Note( + content={ + 'declaration': {'value': 'Medical'}, + 'explanation': {'value': 'I have a medical emergency and need to step back from this assignment.'} + } + ) + ) + assert ac_one_client.get_note(ac_emergency_edit['note']['id']) + + reviewer_one_client = openreview.api.OpenReviewClient(username='reviewer1@aclrollingreview.com', password=helpers.strong_password) + reviewer_one_sig = reviewer_one_client.get_groups( + prefix=f'{venue_id}/Submission3/Reviewer_', + signatory='~Reviewer_ARROne1' + )[0].id + reviewer_delay_edit = reviewer_one_client.post_note_edit( + invitation=f'{venue_id}/Submission3/-/Delay_Notification', + signatures=[reviewer_one_sig], + note=openreview.api.Note( + content={ + 'notification': {'value': 'My review is complete, but I am also notifying the chairs about a delay.'} + } + ) + ) + assert reviewer_one_client.get_note(reviewer_delay_edit['note']['id']) + + assert_submission_query( + f'{venue_id}/Program_Chairs', + pc_console_client, + '+number=1 AND reviewerEmergencyDeclarationCount=0 AND reviewerDelayNotificationCount=0 AND assignedReviewersMinusEmergencyDeclarationsCount=1 AND completedReviewsPlusDelayNotificationsCount=0', + [1] + ) + assert_submission_query( + f'{venue_id}/Program_Chairs', + pc_console_client, + '+number=2 AND reviewerEmergencyDeclarationCount=1', + [2] + ) + assert_submission_query( + f'{venue_id}/Program_Chairs', + pc_console_client, + '+number=2 AND areaChairEmergencyDeclarationCount=1', + [2] + ) + assert_submission_query( + f'{venue_id}/Program_Chairs', + pc_console_client, + '+number=2 AND reviewerDelayNotificationCount=1', + [2] + ) + assert_submission_query( + f'{venue_id}/Program_Chairs', + pc_console_client, + '+number=2 AND areaChairDelayNotificationCount=1', + [2] + ) + assert_submission_query( + f'{venue_id}/Program_Chairs', + pc_console_client, + '+number=2 AND assignedReviewersMinusEmergencyDeclarationsCount=1', + [2] + ) + assert_submission_query( + f'{venue_id}/Program_Chairs', + pc_console_client, + '+number=2 AND completedReviewsPlusDelayNotificationsCount=1', + [2] + ) + assert_submission_query( + f'{venue_id}/Program_Chairs', + pc_console_client, + '+number=2 AND reviewerEmergencyDeclarationCount=1 AND areaChairEmergencyDeclarationCount=1 AND reviewerDelayNotificationCount=1 AND areaChairDelayNotificationCount=1 AND assignedReviewersMinusEmergencyDeclarationsCount=1 AND completedReviewsPlusDelayNotificationsCount=1', + [2] + ) + assert_submission_query( + f'{venue_id}/Program_Chairs', + pc_console_client, + '+number=3 AND reviewerEmergencyDeclarationCount=0 AND areaChairEmergencyDeclarationCount=0 AND reviewerDelayNotificationCount=1 AND areaChairDelayNotificationCount=0 AND assignedReviewersMinusEmergencyDeclarationsCount=1 AND completedReviewsPlusDelayNotificationsCount=2', + [3] + ) + assert_submission_query( + f'{venue_id}/Senior_Area_Chairs', + sac_console_client, + '+number=2 AND reviewerEmergencyDeclarationCount=1', + [2] + ) + assert_submission_query( + f'{venue_id}/Senior_Area_Chairs', + sac_console_client, + '+number=2 AND areaChairEmergencyDeclarationCount=1', + [2] + ) + assert_submission_query( + f'{venue_id}/Senior_Area_Chairs', + sac_console_client, + '+number=2 AND reviewerDelayNotificationCount=1', + [2] + ) + assert_submission_query( + f'{venue_id}/Senior_Area_Chairs', + sac_console_client, + '+number=2 AND areaChairDelayNotificationCount=1', + [2] + ) + assert_submission_query( + f'{venue_id}/Senior_Area_Chairs', + sac_console_client, + '+number=2 AND assignedReviewersMinusEmergencyDeclarationsCount=1', + [2] + ) + assert_submission_query( + f'{venue_id}/Senior_Area_Chairs', + sac_console_client, + '+number=2 AND completedReviewsPlusDelayNotificationsCount=1', + [2] + ) + assert_submission_query( + f'{venue_id}/Senior_Area_Chairs', + sac_console_client, + '+number=2 AND reviewerEmergencyDeclarationCount=1 AND areaChairEmergencyDeclarationCount=1 AND reviewerDelayNotificationCount=1 AND areaChairDelayNotificationCount=1 AND assignedReviewersMinusEmergencyDeclarationsCount=1 AND completedReviewsPlusDelayNotificationsCount=1', + [2] + ) + assert_submission_query( + f'{venue_id}/Senior_Area_Chairs', + sac_console_client, + '+number=3 AND reviewerEmergencyDeclarationCount=0 AND areaChairEmergencyDeclarationCount=0 AND reviewerDelayNotificationCount=1 AND areaChairDelayNotificationCount=0 AND assignedReviewersMinusEmergencyDeclarationsCount=1 AND completedReviewsPlusDelayNotificationsCount=2', + [3] + ) + def send_email(email_option, role): role_tab_id_format = role.replace('_', '-') role_message_id_format = role.replace('_', '')