Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
73 changes: 70 additions & 3 deletions openreview/arr/arr.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
from openreview.api import Invitation
from openreview.venue.recruitment import Recruitment
from openreview.arr.helpers import (
setup_arr_invitations
setup_arr_invitations,
setup_arr_root_groups,
ARR_ROLE_NAMES
)
from openreview.stages.arr_content import hide_fields, arr_withdrawal_content

SHORT_BUFFER_MIN = 30
LONG_BUFFER_DAYS = 10
SENIORITY_PUBLICATION_COUNT = 8
ROOT_DOMAIN = 'aclweb.org/ACL/ARR'

class ARR(object):

Expand Down Expand Up @@ -363,6 +366,32 @@ def expire_invitation(self, invitation_id):
def setup(self, program_chair_ids=[], publication_chairs_ids=[]):
setup_value = self.venue.setup(program_chair_ids, publication_chairs_ids)

# Add root role names to domain content
root_content = {}
for role in ARR_ROLE_NAMES:
key = f"root_{role.lower()}_id"
root_content[key] = { 'value': f'{ROOT_DOMAIN}/{role}' }
self.client.post_group_edit(
invitation=self.get_meta_invitation_id(),
readers=[self.venue_id],
writers=[self.venue_id],
signatures=[self.venue_id],
group=openreview.api.Group(
id=self.venue_id,
content=root_content
)
)

# Initialize root-level ARR groups if not populated
# setup_arr_root_groups(self.client, self.support_user)

# Synchronize groups
if self.venue_id != ROOT_DOMAIN:
for role in ARR_ROLE_NAMES:
source_group = self.client.get_group(f'{ROOT_DOMAIN}/{role}')
target_group_id = f'{self.venue_id}/{role}'
self.client.add_members_to_group(target_group_id, source_group.members)

with open(os.path.join(os.path.dirname(__file__), 'webfield/homepageWebfield.js')) as f:
content = f.read()
self.client.post_group_edit(
Expand Down Expand Up @@ -405,7 +434,29 @@ def setup(self, program_chair_ids=[], publication_chairs_ids=[]):
id=self.get_area_chairs_id(),
web=content
)
)
)

with open(os.path.join(os.path.dirname(__file__), 'webfield/reviewersWebfield.js')) as f:
content = f.read()
self.client.post_group_edit(
invitation=self.get_meta_invitation_id(),
signatures=[self.venue_id],
group=openreview.api.Group(
id=self.get_reviewers_id(),
web=content
)
)

with open(os.path.join(os.path.dirname(__file__), 'webfield/ethicsReviewersWebfield.js')) as f:
content = f.read()
self.client.post_group_edit(
invitation=self.get_meta_invitation_id(),
signatures=[self.venue_id],
group=openreview.api.Group(
id=self.get_ethics_reviewers_id(),
web=content
)
)

with open(os.path.join(os.path.dirname(__file__), 'webfield/ethicsChairsWebfield.js')) as f:
content = f.read()
Expand Down Expand Up @@ -568,7 +619,23 @@ def send_decision_notifications(self, decision_options, messages):
return self.venue.send_decision_notifications(decision_options, messages)

def setup_committee_matching(self, committee_id=None, compute_affinity_scores=False, compute_conflicts=False, compute_conflicts_n_years=None, alternate_matching_group=None, submission_track=None):
return self.venue.setup_committee_matching(committee_id, compute_affinity_scores, compute_conflicts, compute_conflicts_n_years, alternate_matching_group, submission_track)
return_value = self.venue.setup_committee_matching(committee_id, compute_affinity_scores, compute_conflicts, compute_conflicts_n_years, alternate_matching_group, submission_track)
# Attach dateprocess to sync custom max papers
self.client.post_invitation_edit(
invitations=self.venue.get_meta_invitation_id(),
readers=[self.venue_id],
writers=[self.venue_id],
signatures=[self.venue_id],
replacement=False,
invitation=openreview.api.Invitation(
id=self.get_custom_max_papers_id(committee_id),
date_processes=[{
'cron': '0 0,12 * * *',
'script': self.invitation_builder.get_process_content('process/sync_cmp_dateprocess.py')
}],
)
)
return return_value

def set_assignments(self, assignment_title, committee_id, enable_reviewer_reassignment=False, overwrite=False):
return self.venue.set_assignments(assignment_title, committee_id, enable_reviewer_reassignment, overwrite)
Expand Down
128 changes: 120 additions & 8 deletions openreview/arr/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import openreview
import os
import time
from enum import Enum
from datetime import datetime, timedelta
Expand Down Expand Up @@ -46,6 +47,15 @@

from openreview.stages.default_content import comment_v2

ARR_ROLE_NAMES = [
'Reviewers',
'Area_Chairs',
'Senior_Area_Chairs',
'Ethics_Reviewers',
'Ethics_Chairs',
'Program_Chairs'
]

class ARRWorkflow(object):
UPDATE_WAIT_TIME = 5000
CONFIGURATION_INVITATION_CONTENT = {
Expand Down Expand Up @@ -620,14 +630,6 @@ def __init__(self, client_v2, venue, configuration_note, request_form_id, suppor
start_date=self.configuration_note.content.get('setup_shared_data_date'),
process='management/setup_shared_data.py'
),
ARRStage(
type=ARRStage.Type.PROCESS_INVITATION,
required_fields=[],
super_invitation_id=f"{self.venue_id}/-/Register_Authors_To_Reviewers",
stage_arguments={},
process='management/setup_authors_to_reviewers.py',
ignore_dates=['cdate']
),
ARRStage(
type=ARRStage.Type.PROCESS_INVITATION,
required_fields=[],
Expand Down Expand Up @@ -2108,3 +2110,113 @@ def get_resubmissions(submissions, previous_url_field):
lambda s: previous_url_field in s.content and 'value' in s.content[previous_url_field] and len(s.content[previous_url_field]['value']) > 0,
submissions
))

def setup_arr_root_groups(client, support_user):
"""
Creates the root ARR groups and meta-invitation.
Domain: aclweb.org/ACL/ARR
Roles: Reviewers, Area_Chairs, Senior_Area_Chairs, Ethics_Reviewers, Ethics_Chairs, Editors_In_Chief

This function:
1. Ensures the domain group aclweb.org/ACL/ARR exists
2. Creates the meta invitation aclweb.org/ACL/ARR/-/Edit
3. Creates role groups (e.g. aclweb.org/ACL/ARR/Reviewers)
"""
# TODO: Add pointer to the current cycle for edge posting
domain = 'aclweb.org/ACL/ARR'
roles = ARR_ROLE_NAMES
super_meta_invitation_id = support_user.split('/')[0] + '/-/Edit'

# 1. Build domain group path (aclweb.org, aclweb.org/ACL, aclweb.org/ACL/ARR)
path_components = domain.split('/')
paths = ['/'.join(path_components[0:index+1]) for index, path in enumerate(path_components)]

for p in paths:
group = openreview.tools.get_group(client, id=p)
if group is None:
client.post_group_edit(
invitation=super_meta_invitation_id,
readers=['everyone'],
writers=['~Super_User1'],
signatures=['~Super_User1'],
group=openreview.api.Group(
id=p,
readers=['everyone'],
nonreaders=[],
writers=[p],
signatories=[p],
signatures=['~Super_User1'],
members=[],
details={'writable': True}
)
)

# 2. Create meta invitation (aclweb.org/ACL/ARR/-/Edit)
meta_invitation_id = f'{domain}/-/Edit'
meta_invitation = openreview.tools.get_invitation(client, meta_invitation_id)

if meta_invitation is None:
# Get process content from venue module

client.post_invitation_edit(
invitations=None,
readers=[domain],
writers=[domain],
signatures=['~Super_User1'],
invitation=openreview.api.Invitation(
id=meta_invitation_id,
invitees=[domain],
readers=[domain],
signatures=['~Super_User1'],
edit=True
)
)

editors_in_chief_id = f'{domain}/Editors_In_Chief'

for role in roles:
role_group_id = f'{domain}/{role}'
role_group = openreview.tools.get_group(client, id=role_group_id)

if role_group is None:
if role == 'Editors_In_Chief':
readers = [domain, role_group_id]
writers = [domain, role_group_id]
signatories = [domain, role_group_id]
else:
readers = [domain, editors_in_chief_id, role_group_id]
writers = [domain, editors_in_chief_id]
signatories = [domain, role_group_id]

client.post_group_edit(
invitation=meta_invitation_id,
readers=[domain],
writers=[domain],
signatures=[domain],
group=openreview.api.Group(
id=role_group_id,
readers=readers,
writers=writers,
signatures=[domain],
signatories=signatories,
members=[]
)
)

# Add Editors_In_Chief to the domain group as members
domain_group = openreview.tools.get_group(client, id=domain)

# Add Editors_In_Chief group to domain group members if not already present
if editors_in_chief_id not in domain_group.members:
client.post_group_edit(
invitation=meta_invitation_id,
readers=[domain],
writers=[domain],
signatures=[domain],
group=openreview.api.Group(
id=domain,
members={
'add': [editors_in_chief_id]
}
)
)
97 changes: 63 additions & 34 deletions openreview/arr/management/setup_ae_matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def process(client, invitation):

from openreview.venue import matching
from openreview.arr.helpers import get_resubmissions
from openreview.arr.arr import SENIORITY_PUBLICATION_COUNT
from openreview.arr.arr import SENIORITY_PUBLICATION_COUNT, ROOT_DOMAIN
from collections import defaultdict

def replace_edge(existing_edge=None, edge_inv=None, new_weight=None, submission_id=None, profile_id=None, edge_readers=None):
Expand Down Expand Up @@ -60,6 +60,7 @@ def replace_edge(existing_edge=None, edge_inv=None, new_weight=None, submission_
ae_cmp_inv = domain.content['area_chairs_custom_max_papers_id']['value']
reviewers_id = domain.content['reviewers_id']['value']
area_chairs_id = domain.content['area_chairs_id']['value']
area_chairs_name = domain.content['area_chairs_name']['value']
area_chairs_group = client.get_group(area_chairs_id).members
senior_area_chairs_id = domain.content['senior_area_chairs_id']['value']
tracks_field_name = 'research_area'
Expand Down Expand Up @@ -106,31 +107,29 @@ def replace_edge(existing_edge=None, edge_inv=None, new_weight=None, submission_
name_to_id[name_obj['username']] = profile.id

# Build load map
print(f"num profiles {len(all_profiles)}")
id_to_load_note = {}
for role_id in [area_chairs_id]:
load_notes = client.get_all_notes(invitation=f"{role_id}/-/{max_load_name}") ## Assume only 1 note per user
for note in load_notes:
if note.signatures[0] not in name_to_id:
continue
note_signature_id = name_to_id[note.signatures[0]]
id_to_load_note[note_signature_id] = note
root_role_id = domain.content['root_area_chairs_id']['value']
load_notes = client.get_all_notes(invitation=f"{root_role_id}/-/{max_load_name}") ## Assume only 1 note per user
for note in load_notes:
if note.signatures[0] not in name_to_id:
continue
note_signature_id = name_to_id[note.signatures[0]]
id_to_load_note[note_signature_id] = note

# Build track map
track_to_ids = {}
for role_id in [area_chairs_id]:
track_to_ids[role_id] = defaultdict(list)
registration_notes = client.get_all_notes(invitation=f"{role_id}/-/{registration_name}")
for note in registration_notes:
if note.signatures[0] not in name_to_id:
continue
note_signature_id = name_to_id[note.signatures[0]]
for track in note.content[tracks_field_name]['value']:
track_to_ids[role_id][track].append(note_signature_id)
track_to_ids[area_chairs_id] = defaultdict(set)
registration_notes = client.get_all_notes(invitation=f"{root_role_id}/-/{registration_name}")
for note in registration_notes:
if note.signatures[0] not in name_to_id:
continue
note_signature_id = name_to_id[note.signatures[0]]
for track in note.content[tracks_field_name]['value']:
track_to_ids[area_chairs_id][track].add(note_signature_id)

# Build research area invitation
matching.Matching(venue, client.get_group(role_id), None)._create_edge_invitation(
edge_id=f"{role_id}/-/{tracks_inv_name}"
matching.Matching(venue, client.get_group(area_chairs_id), None)._create_edge_invitation(
edge_id=f"{area_chairs_id}/-/{tracks_inv_name}"
)
track_edge_readers = {
area_chairs_id: [venue_id, senior_area_chairs_id]
Expand All @@ -141,22 +140,16 @@ def replace_edge(existing_edge=None, edge_inv=None, new_weight=None, submission_
}

# Reset custom max papers to ground truth notes
for role_id in [area_chairs_id]:
cmp_to_post = []
role_cmp_inv = f"{role_id}/-/Custom_Max_Papers"
print(f"num of notes {len(id_to_load_note)}")
for id, note in id_to_load_note.items():
load_invitation = [inv for inv in note.invitations if max_load_name in inv][0]
if role_id not in load_invitation:
continue

cmp_to_post.append(
openreview.api.Edge(
invitation=role_cmp_inv,
head=role_id,
cmp_to_post = []
role_cmp_inv = f"{area_chairs_id}/-/Custom_Max_Papers"
for id, note in id_to_load_note.items():
cmp_to_post.append(
openreview.api.Edge(
invitation=role_cmp_inv,
head=area_chairs_id,
tail=id,
weight=int(note.content['maximum_load_this_cycle']['value']),
readers=track_edge_readers[role_id] + [id],
readers=track_edge_readers[area_chairs_id] + [id],
writers=[venue_id],
signatures=[venue_id]
)
Expand All @@ -168,6 +161,42 @@ def replace_edge(existing_edge=None, edge_inv=None, new_weight=None, submission_
)
print(f"posting {len(cmp_to_post)} custom max papers for {role_id}")
openreview.tools.post_bulk_edges(client=client, edges=cmp_to_post)

# Create Reviewing_Resubmissions edges from root domain load notes
resubmissions_to_post = []
role_resubmissions_inv = f"{area_chairs_id}/-/Reviewing_Resubmissions"

for id, note in id_to_load_note.items():
max_load = int(note.content['maximum_load_this_cycle']['value'])
for_resubmissions = note.content.get('maximum_load_this_cycle_for_resubmissions', {}).get('value', '')

availability_label = None
if 'yes' in for_resubmissions.lower() and max_load == 0:
availability_label = 'Only Reviewing Resubmissions'
elif 'yes' in for_resubmissions.lower():
availability_label = 'Yes'
elif 'no' in for_resubmissions.lower():
availability_label = 'No'

if availability_label:
resubmissions_to_post.append(
openreview.api.Edge(
invitation=role_resubmissions_inv,
head=area_chairs_id,
tail=id,
label=availability_label,
readers=track_edge_readers[area_chairs_id] + [id],
writers=[venue_id],
signatures=[venue_id]
)
)

client.delete_edges(
invitation=role_resubmissions_inv,
soft_delete=True,
wait_to_finish=True
)
openreview.tools.post_bulk_edges(client=client, edges=resubmissions_to_post)

ae_exceptions = {}
for submission in resubmissions:
Expand Down
Loading