Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -157,6 +157,7 @@ def get_conference(client, request_form_id, support_user='OpenReview.net/Support
if venue.use_publication_chairs:
preferred_emails_groups.append(venue.get_publication_chairs_id())
venue.preferred_emails_groups = preferred_emails_groups
venue.profile_minimum_requirements = venue_content.get('profile_minimum_requirements', {}).get('value', False)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would prefer a more descriptive name and instead of storing this in the venue domain, we can store it in the invite assignment invitation so it can be different per committee role.

What about calling it "invitee_profile_minim_requirement"?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wish the new UI can have a subinvitation to edit the Invite_Assignment invitation to configure these requirements.

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.

venue.profile_minimum_requirements is set to False when absent, but elsewhere it is initialized as {} and treated like a dict of requirements. Using {}/None here would keep the type consistent and avoid callers needing to handle both dict and bool values.

Suggested change
venue.profile_minimum_requirements = venue_content.get('profile_minimum_requirements', {}).get('value', False)
venue.profile_minimum_requirements = venue_content.get('profile_minimum_requirements', {}).get('value', {})

Copilot uses AI. Check for mistakes.
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
29 changes: 29 additions & 0 deletions openreview/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,35 @@ def get_publications(profile):

return profiles

def check_profile_minimum_requirements(profile, min_requirements):
"""
Check if a profile meets the minimum requirements specified by a venue.

Supported keys in min_requirements:
- 'history': profile must have at least 1 history entry
- 'relations': profile must have at least 1 relations entry
- 'expertise': profile must have at least 1 expertise entry
- 'publications': profile must have at least 1 publication
- 'active': profile must be active

:param profile: Profile to check against requirements
:type profile: Profile
:param min_requirements: Dictionary mapping requirement name to True/False
:type min_requirements: dict
:return: True if profile meets all requirements, False otherwise
"""
for field, required in min_requirements.items():
if not required:
continue

if field in ('relations', 'expertise', 'history', 'publications'):
if not profile.content.get(field):
return False
elif field == 'active':
if not profile.state or 'active' not in profile.state.lower():
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.

check_profile_minimum_requirements checks the active requirement via profile.state, but other code paths (e.g., check_new_profiles) treat profile.active as the authoritative boolean. If state is missing while active is true, this will incorrectly flag profiles as incomplete. Consider checking profile.active (or falling back to state only when active is None) for the active requirement.

Suggested change
if not profile.state or 'active' not in profile.state.lower():
profile_active = getattr(profile, 'active', None)
if profile_active is not None:
if not profile_active:
return False
elif not profile.state or 'active' not in profile.state.lower():

Copilot uses AI. Check for mistakes.
return False
# else: unsupported field, ignore
return True

def get_group(client, id):
"""
Expand Down
3 changes: 3 additions & 0 deletions openreview/venue/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,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('profile_minimum_requirements'):
content['profile_minimum_requirements'] = venue_group.content.get('profile_minimum_requirements')

if self.venue.is_template_related_workflow():
submission_name = self.venue.submission_stage.name
Expand Down
6 changes: 6 additions & 0 deletions openreview/venue/process/invite_assignment_pre_process.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ async function process(client, edge, invitation) {
const conflictPolicy = domain.content?.[`${committeeRole}_conflict_policy`]?.value
const conflictNYears = domain.content?.[`${committeeRole}_conflict_n_years`]?.value
const quota = domain.content?.[`submission_assignment_max_${committeeRole}`]?.value
const profileReqs = domain.content.profile_minimum_requirements?.value
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.

domain.content is accessed without optional chaining when reading profile_minimum_requirements. If domain.content is undefined/null (which other lines already guard against with domain.content?.[...]), this will throw and break the invite pre-process. Use domain.content?.profile_minimum_requirements?.value (and similarly guard any nested access) to keep behavior consistent with the rest of the file.

Suggested change
const profileReqs = domain.content.profile_minimum_requirements?.value
const profileReqs = domain.content?.profile_minimum_requirements?.value

Copilot uses AI. Check for mistakes.

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 Down Expand Up @@ -85,6 +86,11 @@ async function process(client, edge, invitation) {
return Promise.reject(new OpenReviewError({ name: 'Error', message: `Can not invite ${userProfile.id}, the user is an official reviewer` }))
}
}

// Check for complete profile, if no profile then go to pending sign up
if (profileReqs && !client.tools.checkProfileMinimumRequirements(userProfile, profileReqs)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do these requirements apply to all the committee role or just reviewers?

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

const authorProfiles = await client.tools.getProfiles(submission.content.authorids?.value, true)
Expand Down
82 changes: 64 additions & 18 deletions openreview/venue/venue.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,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.profile_minimum_requirements = {}
self.submission_human_verification = None
venue_webfield_dir = os.path.join(os.path.dirname(__file__), 'webfield')
self.homepage_webfield_path = os.path.join(venue_webfield_dir, 'homepageWebfield.js')
Expand Down Expand Up @@ -2268,6 +2269,37 @@ def compute_acs_stats(self):
@classmethod
def check_new_profiles(Venue, client):

def send_incomplete_profile_notification(venue_group, edge, submission, user_profile, min_requirements):
missing = []
for field, required in (min_requirements or {}).items():
if not required:
continue
if field == 'publications' and not user_profile.content.get('publications'):
missing.append('You don\'t have a publication visible to the venue. You may import papers from DBLP or ORCID, see: https://docs.openreview.net/')
elif field == 'relations' and not user_profile.content.get('relations'):
missing.append('You don\'t have a relation visible to the venue. Please add a relation or update the visibility.')
elif field == 'expertise' and not user_profile.content.get('expertise'):
missing.append('You don\'t have any expertise keywords. Please add your areas of expertise.')
elif field == 'history' and not user_profile.content.get('history'):
missing.append('You don\'t have any employment or education history. Please add at least one entry to your profile history.')
elif field == 'active' and (not user_profile.state or 'active' not in user_profile.state.lower()):
missing.append('Your profile is not active. Please complete profile activation.')
missing_text = '\n'.join(f'- {m}' for m in missing)

## 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 address the following:

{missing_text}

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 @@ -2349,29 +2381,35 @@ def mark_as_accepted(venue_group, edge, submission, user_profile, invite_assignm
active_venues = client.get_group('active_venues').members

for venue_id in active_venues:
# Create new client for each venue
Comment thread
melisabok marked this conversation as resolved.
venue_client = openreview.api.OpenReviewClient(
baseurl=openreview.tools.get_base_urls(client)[1],
token=client.token
)
venue_client.impersonate(venue_id)

venue_group = client.get_group(venue_id)
venue_group = venue_client.get_group(venue_id)

if hasattr(venue_group, 'domain') and venue_group.content:

print(f'Check active venue {venue_group.id}')

edge_invitations = client.get_all_invitations(prefix=venue_id, type='edge', domain=venue_id)
edge_invitations = venue_client.get_all_invitations(prefix=venue_id, type='edge', domain=venue_id)
invite_assignment_invitations = [inv.id for inv in edge_invitations if inv.id.endswith('Invite_Assignment')]

for invite_assignment_invitation_id in invite_assignment_invitations:

## check if it is expired?
invite_assignment_invitation = openreview.tools.get_invitation(client, invite_assignment_invitation_id)
invite_assignment_invitation = openreview.tools.get_invitation(venue_client, invite_assignment_invitation_id)

if invite_assignment_invitation:
grouped_edges = client.get_grouped_edges(invitation=invite_assignment_invitation.id, label='Pending Sign Up', groupby='tail', domain=venue_id)
grouped_edges = venue_client.get_grouped_edges(invitation=invite_assignment_invitation.id, label='Pending Sign Up', groupby='tail', domain=venue_id)
print('Pending sign up edges found', len(grouped_edges))

for grouped_edge in grouped_edges:

tail = grouped_edge['id']['tail']
profiles=openreview.tools.get_profiles(client, [tail], with_publications=True, with_relations=True)
profiles=openreview.tools.get_profiles(venue_client, [tail], with_publications=True, with_relations=True)

if profiles and profiles[0].active:

Expand All @@ -2383,33 +2421,41 @@ def mark_as_accepted(venue_group, edge, submission, user_profile, invite_assignm
for edge in edges:

edge = openreview.api.Edge.from_json(edge)
submission=client.get_note(id=edge.head)
submission=venue_client.get_note(id=edge.head)

if submission.content['venueid']['value'] == venue_group.content.get('submission_venue_id', {}).get('value'):

## Check if there is already an accepted edge for that profile id
accepted_edges = client.get_edges(invitation=invite_assignment_invitation.id, label='Accepted', head=submission.id, tail=user_profile.id)
accepted_edges = venue_client.get_edges(invitation=invite_assignment_invitation.id, label='Accepted', head=submission.id, tail=user_profile.id)

if not accepted_edges:
## Check if the user was invited again with a profile id
invitation_edges = client.get_edges(invitation=invite_assignment_invitation.id, label='Invitation Sent', head=submission.id, tail=user_profile.id)
invitation_edges = venue_client.get_edges(invitation=invite_assignment_invitation.id, label='Invitation Sent', head=submission.id, tail=user_profile.id)
if invitation_edges:
invitation_edge = invitation_edges[0]
print(f'User invited twice, remove double invitation edge {invitation_edge.id}')
invitation_edge.ddate = openreview.tools.datetime_millis(datetime.datetime.now())
client.post_edge(invitation_edge)
venue_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'))
## Check venue profile requirements
min_requirements = venue_group.content.get('profile_minimum_requirements', {}).get('value')
is_incomplete = bool(min_requirements) and not openreview.tools.check_profile_minimum_requirements(user_profile, min_requirements)

if conflicts:
print(f'Conflicts detected for {edge.head} and {user_profile.id}', conflicts)
mark_as_conflict(venue_group, edge, submission, user_profile)
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, min_requirements)
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(venue_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