Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions openreview/conference/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def get_conference(client, request_form_id, support_user='OpenReview.net/Support
venue.submission_assignment_max_reviewers = int(note.content.get('submission_assignment_max_reviewers')) if note.content.get('submission_assignment_max_reviewers') is not None else None
venue.comment_notification_threshold = int(note.content.get('comment_notification_threshold')) if note.content.get('comment_notification_threshold') is not None else None
venue.preferred_emails_groups = note.content.get('preferred_emails_groups', [])
venue.invited_reviewer_profile_minimum_requirements = venue_content.get('invited_reviewer_profile_minimum_requirements', {}).get('value', False)
venue.iThenticate_plagiarism_check = note.content.get('iThenticate_plagiarism_check', 'No') == 'Yes'
venue.iThenticate_plagiarism_check_api_key = note.content.get('iThenticate_plagiarism_check_api_key', '')
venue.iThenticate_plagiarism_check_api_base_url = note.content.get('iThenticate_plagiarism_check_api_base_url', '')
Expand Down
3 changes: 3 additions & 0 deletions openreview/venue/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,9 @@ def create_venue_group(self):

if self.venue.comment_notification_threshold:
content['comment_notification_threshold'] = { 'value': self.venue.comment_notification_threshold }

if venue_group.content.get('invited_reviewer_profile_minimum_requirements'):
content['invited_reviewer_profile_minimum_requirements'] = venue_group.content.get('invited_reviewer_profile_minimum_requirements')

update_content = self.get_update_content(venue_group.content, content)
if update_content:
Expand Down
48 changes: 48 additions & 0 deletions openreview/venue/process/invite_assignment_pre_process.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ async function process(client, edge, invitation) {
const conflictNYears = domain.content.reviewers_conflict_n_years?.value
const reviewersName = reviewersId.split('/').pop().toLowerCase()
const quota = domain.content?.['submission_assignment_max_' + reviewersName]?.value
const profileReqs = domain.content.invited_reviewer_profile_minimum_requirements?.value

if (edge.ddate && edge.label !== inviteLabel) {
return Promise.reject(new OpenReviewError({ name: 'Error', message: `Cannot cancel the invitation since it has status: "${edge.label}"` }))
Expand All @@ -27,6 +28,53 @@ async function process(client, edge, invitation) {
const profiles = await client.tools.getProfiles([edge.tail], true)
const userProfile = profiles[0]

// Check for complete profile, if no profile then go to pending sign up
if (profileReqs && !userProfile.id.includes('@')) {
Comment thread
melisabok marked this conversation as resolved.
Outdated
let isIncomplete = false;

for (const [profilePath, expectedValue] of Object.entries(profileReqs)) {
const pathItems = profilePath.split('.');
let actualValue = userProfile;

// Resolve actual value from the profile
for (const item of pathItems) {
if (actualValue && typeof actualValue === 'object') {
actualValue = actualValue?.[item];
} else {
actualValue = null;
}

if (actualValue === null || actualValue === undefined) {
break;
}
}

// If checking publications, filter notes for pdate
if (pathItems[pathItems.length - 1] === 'publications') {
actualValue = actualValue.filter(pub => pub.hasOwnProperty('pdate'));
Comment thread
enrubio marked this conversation as resolved.
Outdated
}

// Check against requirement
// Check number of entries
if (typeof expectedValue === 'number') {
if (actualValue?.length < expectedValue) {
isIncomplete = true;
break;
}
// Check if field exists in profile (e.g. links)
} else if (expectedValue === true && !actualValue) {
isIncomplete = true;
break;
} else {
console.log(`Invalid path: ${profilePath}`);
}
}

if (isIncomplete) {
return Promise.reject(new OpenReviewError({ name: 'Error', message: `Can not invite ${userProfile.id}, the user has an incomplete profile according to venue standards` }))
}
}

if (userProfile.id !== edge.tail) {
const { edges } = await client.getEdges({ invitation: edge.invitation, head: edge.head, tail: userProfile.id })
if (edges.length) {
Expand Down
78 changes: 68 additions & 10 deletions openreview/venue/venue.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def __init__(self, client, venue_id, support_user):
self.iThenticate_plagiarism_check_exclude_custom_sections = False
self.iThenticate_plagiarism_check_exclude_small_matches = 8
self.comment_notification_threshold = None
self.invited_reviewer_profile_minimum_requirements = {}

def get_id(self):
return self.venue_id
Expand Down Expand Up @@ -1373,6 +1374,19 @@ def poll_ithenticate_for_status(self):
@classmethod
def check_new_profiles(Venue, client):

def send_incomplete_profile_notification(venue_group, edge, submission, user_profile):
## Send email to reviewer
subject=f"[{venue_group.content['subtitle']['value']}] Incomplete profile for paper {submission.number}"
message =f'''Hi {{{{fullname}}}},
You have accepted the invitation to review the paper number: {submission.number}, title: {submission.content['title']['value']}.

However, your profile was found to be incomplete according to {venue_group.content['subtitle']['value']} standards and the assignment is pending your profile completion. Please review your venue's profile requirements and update your profile.

If you have any questions, please contact us as info@openreview.net.

OpenReview Team'''
response = client.post_message(subject, [edge.tail], message, invitation=venue_group.content['meta_invitation_id']['value'], signature=venue_group.id, replyTo=venue_group.content['contact']['value'], sender=venue_group.content['message_sender']['value'])

def mark_as_conflict(venue_group, edge, submission, user_profile):
edge.label='Conflict Detected'
edge.tail=user_profile.id
Expand Down Expand Up @@ -1524,17 +1538,61 @@ def mark_as_accepted(venue_group, edge, submission, user_profile, invite_assignm
invitation_edge.ddate = openreview.tools.datetime_millis(datetime.datetime.now())
client.post_edge(invitation_edge)

## Check conflicts
author_profiles = openreview.tools.get_profiles(client, submission.content['authorids']['value'], with_publications=True, with_relations=True)
conflicts=openreview.tools.get_conflicts(author_profiles, user_profile, policy=venue_group.content.get('reviewers_conflict_policy', {}).get('value'), n_years=venue_group.content.get('reviewers_conflict_n_years', {}).get('value'))

if conflicts:
print(f'Conflicts detected for {edge.head} and {user_profile.id}', conflicts)
mark_as_conflict(venue_group, edge, submission, user_profile)
## Check venue profile requirements
min_requirements = venue_group.content.get('invited_reviewer_profile_minimum_requirements', {}).get('value')
Comment thread
melisabok marked this conversation as resolved.
Outdated
is_incomplete = False

if min_requirements:
# { content.relations: 2, content.dblp: true, active: true }
for profile_path, expected_value in min_requirements.items():
path_items = profile_path.split('.')
actual_value = user_profile

## Resolve actual value from the profile
for item in path_items:
if isinstance(actual_value, openreview.Profile):
actual_value = getattr(actual_value, item, None)
elif isinstance(actual_value, dict):
actual_value = actual_value.get(item)
else:
## Can't traverse, but more items to resolve
actual_value = None

if actual_value is None:
break

## If checking publications, filter notes for pdate
if path_items[-1] == "publications":
actual_value = [pub for pub in actual_value if hasattr(pub, 'pdate')]

## Check against requirement
## Check number of entries
if type(expected_value) == int:
if not actual_value or not isinstance(actual_value, list) or len(actual_value) < expected_value:
is_incomplete = True
break
## Check if field exists in profile (e.g. links)
elif expected_value is True and not actual_value:
is_incomplete = True
break
else:
print(f'Invalid path: {profile_path}')
Comment thread
enrubio marked this conversation as resolved.
Outdated

if is_incomplete:
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a real profile is found for a pending-signup edge and the profile is still incomplete, the code only sends a notification but does not persist any edge update. This means the edge tail stays as the raw email until the profile becomes complete, which conflicts with the intended behavior in the added test (email-tail edge removed, profile-id-tail edge remains in Pending Sign Up) and keeps re-grouping by email. Consider updating/posting the edge to set edge.tail = user_profile.id (keeping label Pending Sign Up) before/while sending the incomplete-profile notification.

Suggested change
if is_incomplete:
if is_incomplete:
edge.tail = user_profile.id
venue_client.post_edge(edge)

Copilot uses AI. Check for mistakes.
print(f'Sending messages for incomplete profile {user_profile.id} for paper {edge.head}')
send_incomplete_profile_notification(venue_group, edge, submission, user_profile)
else:
print(f'Mark accepted for {edge.head} and {user_profile.id}')
mark_as_accepted(venue_group, edge, submission, user_profile, invite_assignment_invitation)

## Check conflicts
author_profiles = openreview.tools.get_profiles(client, submission.content['authorids']['value'], with_publications=True, with_relations=True)
conflicts=openreview.tools.get_conflicts(author_profiles, user_profile, policy=venue_group.content.get('reviewers_conflict_policy', {}).get('value'), n_years=venue_group.content.get('reviewers_conflict_n_years', {}).get('value'))

if conflicts:
print(f'Conflicts detected for {edge.head} and {user_profile.id}', conflicts)
mark_as_conflict(venue_group, edge, submission, user_profile)
else:
print(f'Mark accepted for {edge.head} and {user_profile.id}')
mark_as_accepted(venue_group, edge, submission, user_profile, invite_assignment_invitation)

else:
print("user already accepted with another invitation edge", submission.id, user_profile.id)

Expand Down
Loading