-
Notifications
You must be signed in to change notification settings - Fork 36
Add venue profile validation for invite assignment #2572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
3616db6
fac00fc
81bbf7b
a5cee13
147daa1
ce98253
68dcb36
6593141
bc1defd
3fb889a
2fb1b44
872b2f0
d31bf3b
2a1fe72
a19c1a3
7be2c90
9ac7803
406dfec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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) | ||||||
|
||||||
| 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', {}) |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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(): | ||||||||||||||
|
||||||||||||||
| 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(): |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||
|
||||||
| const profileReqs = domain.content.profile_minimum_requirements?.value | |
| const profileReqs = domain.content?.profile_minimum_requirements?.value |
There was a problem hiding this comment.
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?
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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') | ||||||||||
|
|
@@ -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 | ||||||||||
|
|
@@ -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 | ||||||||||
|
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: | ||||||||||
|
|
||||||||||
|
|
@@ -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: | ||||||||||
|
||||||||||
| if is_incomplete: | |
| if is_incomplete: | |
| edge.tail = user_profile.id | |
| venue_client.post_edge(edge) |
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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.