diff --git a/openreview/conference/helpers.py b/openreview/conference/helpers.py index 0c7c3c691..72fd70d68 100644 --- a/openreview/conference/helpers.py +++ b/openreview/conference/helpers.py @@ -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.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', '') diff --git a/openreview/tools.py b/openreview/tools.py index 14a81df17..795fd459d 100644 --- a/openreview/tools.py +++ b/openreview/tools.py @@ -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(): + return False + # else: unsupported field, ignore + return True def get_group(client, id): """ diff --git a/openreview/venue/group.py b/openreview/venue/group.py index aa5058f62..83d7e79a4 100644 --- a/openreview/venue/group.py +++ b/openreview/venue/group.py @@ -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 diff --git a/openreview/venue/process/invite_assignment_pre_process.js b/openreview/venue/process/invite_assignment_pre_process.js index f024e2a70..4e36a79e2 100644 --- a/openreview/venue/process/invite_assignment_pre_process.js +++ b/openreview/venue/process/invite_assignment_pre_process.js @@ -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 if (edge.ddate && edge.label !== inviteLabel) { return Promise.reject(new OpenReviewError({ name: 'Error', message: `Cannot cancel the invitation since it has status: "${edge.label}"` })) @@ -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)) { + 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) diff --git a/openreview/venue/venue.py b/openreview/venue/venue.py index a599cefbe..cf08fe26a 100644 --- a/openreview/venue/venue.py +++ b/openreview/venue/venue.py @@ -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 + 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: + 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) diff --git a/tests/test_cvpr_conference_v2.py b/tests/test_cvpr_conference_v2.py index 7134ff794..7d3600b4e 100644 --- a/tests/test_cvpr_conference_v2.py +++ b/tests/test_cvpr_conference_v2.py @@ -577,8 +577,39 @@ def test_invite_reviewers(self, client, openreview_client, helpers, test_client) def test_review_rating_stage(self, client, openreview_client, helpers, test_client): pc_client=openreview.Client(username='pc@cvpr.cc', password=helpers.strong_password) + pc_client_v2=openreview.api.OpenReviewClient(username='pc@cvpr.cc', password=helpers.strong_password) request_form=pc_client.get_notes(invitation='openreview.net/Support/-/Request_Form')[0] + venue = openreview.get_conference(client, request_form.id, support_user='openreview.net/Support') + + submissions = venue.get_submissions(sort='number:asc') + + # Post revs for paper #1 + for rev in ['~Reviewer_CVPROne1', '~Reviewer_CVPRTwo1', '~Reviewer_CVPRThree1']: + pc_client_v2.post_edge(openreview.api.Edge( + invitation = 'thecvf.com/CVPR/2024/Conference/Reviewers/-/Proposed_Assignment', + head = submissions[0].id, + tail = rev, + signatures = ['thecvf.com/CVPR/2024/Conference/Program_Chairs'], + weight = 1, + label = 'rev-matching' + )) + + # Post revs for paper #2 + for rev in ['~Reviewer_CVPRFour1', '~Reviewer_CVPRFive1', '~Reviewer_CVPRSix1']: + pc_client_v2.post_edge(openreview.api.Edge( + invitation = 'thecvf.com/CVPR/2024/Conference/Reviewers/-/Proposed_Assignment', + head = submissions[1].id, + tail = rev, + signatures = ['thecvf.com/CVPR/2024/Conference/Program_Chairs'], + weight = 1, + label = 'rev-matching' + )) + venue.set_assignments(assignment_title='rev-matching', committee_id='thecvf.com/CVPR/2024/Conference/Reviewers', enable_reviewer_reassignment=True) + + assert openreview_client.get_edges_count(invitation='thecvf.com/CVPR/2024/Conference/Reviewers/-/Assignment') == 13 + + # enable review stage first pc_client.post_note(openreview.Note( content= { 'force': 'Yes', @@ -828,8 +859,6 @@ def test_review_rating_stage(self, client, openreview_client, helpers, test_clie helpers.await_queue_edit(openreview_client, edit_id=rating_edit['id']) - pc_client_v2=openreview.api.OpenReviewClient(username='pc@cvpr.cc', password=helpers.strong_password) - notes = pc_client_v2.get_notes(invitation=invitation.id) assert len(notes) == 1 assert notes[0].readers == [ @@ -1497,8 +1526,7 @@ def test_metareview_revision_stage(self, client, openreview_client, helpers, tes expdate=openreview.tools.datetime_millis(due_date + datetime.timedelta(days=1)) ) ) - - + # Secondary AC can't post meta review revision secondary_ac_client = openreview.api.OpenReviewClient(username='ac1@cvpr.cc', password=helpers.strong_password) secondary_ac_anon_group_id = secondary_ac_client.get_groups(prefix=f'thecvf.com/CVPR/2024/Conference/Submission4/Secondary_Area_Chair_.*', signatory='ac1@cvpr.cc')[0].id @@ -1518,3 +1546,278 @@ def test_metareview_revision_stage(self, client, openreview_client, helpers, tes } ) ) + + def test_invite_assignment_profile_requirements(self, client, openreview_client, helpers, test_client): + pc_client=openreview.Client(username='pc@cvpr.cc', password=helpers.strong_password) + pc_client_v2=openreview.api.OpenReviewClient(username='pc@cvpr.cc', password=helpers.strong_password) + request_form=pc_client.get_notes(invitation='openreview.net/Support/-/Request_Form')[0] + venue = openreview.get_conference(client, request_form.id, support_user='openreview.net/Support') + + submissions = venue.get_submissions(sort='number:asc') + + ## Add profile min requirements + openreview_client.post_group_edit( + invitation='thecvf.com/CVPR/2024/Conference/-/Edit', + signatures=['thecvf.com/CVPR/2024/Conference'], + group=openreview.api.Group( + id='thecvf.com/CVPR/2024/Conference', + content={ + 'profile_minimum_requirements': { + 'value': { + 'relations': True, + 'publications': True, + 'expertise': True + } + } + } + ) + ) + + ## Invite users with incomplete profile + ac_client = openreview.api.OpenReviewClient(username='ac1@cvpr.cc', password=helpers.strong_password) + anon_group_id = ac_client.get_groups(prefix='thecvf.com/CVPR/2024/Conference/Submission1/Area_Chair_', signatory='~AC_CVPROne1')[0].id + edge = ac_client.post_edge( + openreview.api.Edge(invitation='thecvf.com/CVPR/2024/Conference/Reviewers/-/Invite_Assignment', + signatures=[anon_group_id], + head=submissions[0].id, + tail='celeste@acm.org', + label='Invitation Sent', + weight=1 + )) + helpers.await_queue_edit(openreview_client, edge.id) + + edge = ac_client.post_edge( + openreview.api.Edge(invitation='thecvf.com/CVPR/2024/Conference/Reviewers/-/Invite_Assignment', + signatures=[anon_group_id], + head=submissions[0].id, + tail='emily@acm.org', + label='Invitation Sent', + weight=1 + )) + helpers.await_queue_edit(openreview_client, edge.id) + + assert openreview_client.get_groups('thecvf.com/CVPR/2024/Conference/Emergency_Reviewers/Invited', member='celeste@acm.org') + assert openreview_client.get_groups('thecvf.com/CVPR/2024/Conference/Emergency_Reviewers/Invited', member='emily@acm.org') + + ## Accept invitations + messages = openreview_client.get_messages(to='celeste@acm.org', subject='[CVPR 2024] Invitation to review paper titled "Paper title 1"') + assert messages and len(messages) == 1 + invitation_url = re.search('https://.*\n', messages[0]['content']['text']).group(0).replace('https://openreview.net', 'http://localhost:3030').replace('&', '&')[:-1] + helpers.respond_invitation_fast(invitation_url, accept=True) + + helpers.await_queue_edit(openreview_client, invitation='thecvf.com/CVPR/2024/Conference/Reviewers/-/Assignment_Recruitment') + + messages = openreview_client.get_messages(to='emily@acm.org', subject='[CVPR 2024] Invitation to review paper titled "Paper title 1"') + assert messages and len(messages) == 1 + invitation_url = re.search('https://.*\n', messages[0]['content']['text']).group(0).replace('https://openreview.net', 'http://localhost:3030').replace('&', '&')[:-1] + helpers.respond_invitation_fast(invitation_url, accept=True) + + helpers.await_queue_edit(openreview_client, invitation='thecvf.com/CVPR/2024/Conference/Reviewers/-/Assignment_Recruitment') + + ## Check pending profile creation + invite_edges=pc_client_v2.get_edges(invitation='thecvf.com/CVPR/2024/Conference/Reviewers/-/Invite_Assignment', head=submissions[0].id, tail='celeste@acm.org') + assert len(invite_edges) == 1 + assert invite_edges[0].label == 'Pending Sign Up' + + celeste_client = helpers.create_user('celeste@acm.org', 'Celeste', 'ACM') + emily_client = helpers.create_user('emily@acm.org', 'Emily', 'ACM') + + ## Run Job + openreview.venue.Venue.check_new_profiles(openreview_client) + + ## Check that assignment is still pending sign up + invite_edges=pc_client_v2.get_edges(invitation='thecvf.com/CVPR/2024/Conference/Reviewers/-/Invite_Assignment', head=submissions[0].id, tail='celeste@acm.org') + assert len(invite_edges) == 0 + + invite_edges=pc_client_v2.get_edges(invitation='thecvf.com/CVPR/2024/Conference/Reviewers/-/Invite_Assignment', head=submissions[0].id, tail='~Celeste_ACM1') + assert len(invite_edges) == 1 + assert invite_edges[0].label == 'Pending Sign Up' + + messages = openreview_client.get_messages(to='celeste@acm.org', subject='[CVPR 2024] Incomplete profile for paper 1') + assert messages and len(messages) == 1 + + ## Run Job again, check new message is sent + openreview.venue.Venue.check_new_profiles(openreview_client) + + messages = openreview_client.get_messages(to='emily@acm.org', subject='[CVPR 2024] Incomplete profile for paper 1') + assert messages and len(messages) == 2 + + ## Update profiles + + ## Profile with conflict + profile = celeste_client.get_profile() + + profile.content['dblp'] = 'https://dblp.org/pid/000.html' + profile.content['relations'].append({ + 'relation': 'Advisor', + 'name': 'SomeFirstName User', + 'username': '~SomeFirstName_User1', + 'start': 2024, + 'end': None + }) + profile.content.setdefault('expertise', []).append({ + 'keywords': ['nlp'], + 'start': 2025, + 'end': None + }) + celeste_client.post_profile(profile) + + ## Profile with no conflict + profile = emily_client.get_profile() + + profile.content['dblp'] = 'https://dblp.org/pid/001.html' + profile.content['relations'].append({ + 'relation': 'Advisor', + 'name': 'Reviewer CVPROne', + 'username': '~Reviewer_CVPROne1', + 'start': 2024, + 'end': None + }) + profile.content.setdefault('expertise', []).append({ + 'keywords': ['machine learning'], + 'start': 2024, + 'end': None + }) + emily_client.post_profile(profile) + + openreview.venue.Venue.check_new_profiles(openreview_client) + + time.sleep(5) + + ## Check new message sent because of missing publication + messages = openreview_client.get_messages(to='celeste@acm.org', subject='[CVPR 2024] Incomplete profile for paper 1') + assert messages and len(messages) == 3 + + ## Add publication to profiles + edit = celeste_client.post_note_edit( + invitation='openreview.net/Archive/-/Direct_Upload', + signatures=['~Celeste_ACM1'], + note = openreview.api.Note( + pdate = openreview.tools.datetime_millis(datetime.datetime(2019, 4, 30)), + content = { + 'title': { 'value': f'Published paper title 1' }, + 'abstract': { 'value': f'Published paper abstract 1' }, + 'authors': { 'value': ['Celeste ACM', 'Emily ACM'] }, + 'authorids': { 'value': ['~Celeste_ACM1', '~Emily_ACM1'] }, + 'venue': { 'value': 'TheWebConf24' } + }, + license = 'CC BY-SA 4.0' + )) + openreview_client.post_note_edit( + invitation='openreview.net/-/Edit', + readers=['openreview.net'], + writers=['openreview.net'], + signatures=['openreview.net'], + note=openreview.api.Note( + id = edit['note']['id'], + content = { + 'venueid': { 'value': 'ACM.org/TheWebConf/2024/Conference' } + } + ) + ) + + openreview.venue.Venue.check_new_profiles(openreview_client) + + time.sleep(5) + + # Check no new message was sent + messages = openreview_client.get_messages(to='celeste@acm.org', subject='[CVPR 2024] Incomplete profile for paper 1') + assert messages and len(messages) == 3 + + # Conflict detected after pending sign up and completing profile + messages = openreview_client.get_messages(to='celeste@acm.org', subject='[CVPR 2024] Conflict detected for paper 1') + assert messages and len(messages) == 1 + + invite_edges=pc_client.get_edges(invitation='thecvf.com/CVPR/2024/Conference/Reviewers/-/Invite_Assignment', head=submissions[0].id, tail='~Celeste_ACM1') + assert len(invite_edges) == 1 + assert invite_edges[0].label == 'Conflict Detected' + + messages = openreview_client.get_messages(to='emily@acm.org', subject='[CVPR 2024] Incomplete profile for paper 1') + assert messages and len(messages) == 3 + + messages = openreview_client.get_messages(to='emily@acm.org', subject='[CVPR 2024] You have been assigned as a Reviewer for paper number 1') + assert messages and len(messages) == 1 + + invite_edges=pc_client.get_edges(invitation='thecvf.com/CVPR/2024/Conference/Reviewers/-/Invite_Assignment', head=submissions[0].id, tail='~Emily_ACM1') + assert len(invite_edges) == 1 + assert invite_edges[0].label == 'Accepted' + + ## Test invite assignment preprocess profile validation + rachel_client = helpers.create_user('rachel@acm.org', 'Rachel', 'ACM') + with pytest.raises(openreview.OpenReviewException, match=r'Can not invite ~Rachel_ACM1, the user has an incomplete profile according to venue standards'): + edge = ac_client.post_edge( + openreview.api.Edge(invitation='thecvf.com/CVPR/2024/Conference/Reviewers/-/Invite_Assignment', + signatures=[anon_group_id], + head=submissions[0].id, + tail='rachel@acm.org', + label='Invitation Sent', + weight=1 + )) + + ## Update profile, invite again, detect conflict + profile = rachel_client.get_profile() + + profile.content['dblp'] = 'https://dblp.org/pid/002.html' + profile.content['relations'].append({ + 'relation': 'Advisor', + 'name': 'SomeFirstName User', + 'username': '~SomeFirstName_User1', + 'start': 2024, + 'end': None + }) + profile.content.setdefault('expertise', []).append({ + 'keywords': ['nlp'], + 'start': 2025, + 'end': None + }) + rachel_client.post_profile(profile) + + # Checks for missing publication + with pytest.raises(openreview.OpenReviewException, match=r'Can not invite ~Rachel_ACM1, the user has an incomplete profile according to venue standards'): + edge = ac_client.post_edge( + openreview.api.Edge(invitation='thecvf.com/CVPR/2024/Conference/Reviewers/-/Invite_Assignment', + signatures=[anon_group_id], + head=submissions[0].id, + tail='rachel@acm.org', + label='Invitation Sent', + weight=1 + )) + + ## Add publication to profile + edit = rachel_client.post_note_edit( + invitation='openreview.net/Archive/-/Direct_Upload', + signatures=['~Rachel_ACM1'], + note = openreview.api.Note( + pdate = openreview.tools.datetime_millis(datetime.datetime(2024, 4, 30)), + content = { + 'title': { 'value': f'Published paper title 2' }, + 'abstract': { 'value': f'Published paper abstract 2' }, + 'authors': { 'value': ['Rachel ACM'] }, + 'authorids': { 'value': ['~Rachel_ACM1'] }, + 'venue': { 'value': 'TheWebConf24' } + }, + license = 'CC BY-SA 4.0' + )) + openreview_client.post_note_edit( + invitation='openreview.net/-/Edit', + readers=['openreview.net'], + writers=['openreview.net'], + signatures=['openreview.net'], + note=openreview.api.Note( + id = edit['note']['id'], + content = { + 'venueid': { 'value': 'ACM.org/TheWebConf/2024/Conference' } + } + ) + ) + + ## Detect conflict + with pytest.raises(openreview.OpenReviewException, match=r'Can not invite ~Rachel_ACM1, the user has a conflict'): + edge = ac_client.post_edge( + openreview.api.Edge(invitation='thecvf.com/CVPR/2024/Conference/Reviewers/-/Invite_Assignment', + signatures=[anon_group_id], + head=submissions[0].id, + tail='rachel@acm.org', + label='Invitation Sent', + weight=1 + )) \ No newline at end of file