Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
236 changes: 133 additions & 103 deletions grader/grader.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
list_of_float,
printf,
printff,
IDENTITIES,
)

def ellipsize(s, width):
Expand Down Expand Up @@ -185,7 +184,7 @@ class Grader(cmd_completer.Cmd_Completer):
prompt = COLOR['green']+'grader'+COLOR['yellow']+'>'+COLOR['default']+' '
set_completions = cmd_completer.Cmd_Completer.set_completions

def __init__(self, identity, csv_file, history_file=None):
def __init__(self, csv_file, identity=None, history_file=None):
super().__init__(histfile=history_file)

self.identity = identity
Expand Down Expand Up @@ -251,26 +250,41 @@ def _complete_name(self, prefix):
completions[p.name].add(p.lastname)
return completions

identity_options = cmd_completer.PagedArgumentParser('identity')\
.add_argument('identity', type=int, choices=IDENTITIES,
help='become this identity')
identity_options = (
cmd_completer.PagedArgumentParser('identity')
.add_argument('identity',
nargs='?',
help='become this identity')
)

def do_identity(self, args):
"Switch identity"
opts = self.identity_options.parse_args(args.split())
self.identity = opts.identity

exception_options = cmd_completer.PagedArgumentParser('exception')\
.add_argument('exception',
help='the exception type to be raised')
if opts.identity:
known = self.applications.ini.identities()
if opts.identity not in known:
raise ValueError(f"Identity {opts.identity} not defined in .ini ({', '.join(known)})")
self.identity = opts.identity
print(f'Identity set to {self.identity}')
else:
print(f'Running as identity {self.identity}')

exception_options = (
cmd_completer.PagedArgumentParser('exception')
.add_argument('exception',
help='the exception type to be raised')
)

def do_exception(self, args):
"Fake command to test exception capturing"
opts = self.exception_options.parse_args(args.split())
raise getattr(__builtins__, opts.exception[0])

autolabel_options = cmd_completer.PagedArgumentParser('autolabel')\
autolabel_options = (
cmd_completer.PagedArgumentParser('autolabel')
.add_argument('N', type=int, help='length of SHORTLIST')
)

def do_autolabel(self, args):
"""Automatically label highlanders as INVITE and the next N as SHORTLIST
Expand All @@ -282,8 +296,7 @@ def do_autolabel(self, args):
try:
ranked = self.last_ranking
except AttributeError:
printff('You need to rank applications first!')
return
raise ValueError('You need to rank applications first!')
counter = 0
for person in ranked:
if person.highlander:
Expand All @@ -295,25 +308,27 @@ def do_autolabel(self, args):
else:
return

dump_options = cmd_completer.PagedArgumentParser('dump')\
.add_argument('-d', '--detailed', action='store_const',
dest='format', const='long', default='short',
help='do not truncate free texts')\
.add_argument('-f', '--format',
dest='format', choices=DUMP_FMTS.keys(),
help='use this format')\
.add_argument('-s', '--sorted', action='store_true',
help='print applications sorted by rank')\
.add_argument('-L', '--highlanders', action='store_const',
const=True, default=False,
help='print applications only for highlanders')\
.add_argument('-l', '--label', type=str, nargs='+', default=(),
help='print applications only for people with label')\
.add_argument('-a', '--attribute', nargs='+', metavar='ATTRNAME ATTRVALUE', action='append',
help='print applications only for people with matching attributes'
', e.g. -a n_applied 3 -a . Call "-a list" to get a list of attributes.')\
.add_argument('persons', nargs='*',
help='name fragments of people to display')
dump_options = (
cmd_completer.PagedArgumentParser('dump')
.add_argument('-d', '--detailed', action='store_const',
dest='format', const='long', default='short',
help='do not truncate free texts')
.add_argument('-f', '--format',
dest='format', choices=DUMP_FMTS.keys(),
help='use this format')
.add_argument('-s', '--sorted', action='store_true',
help='print applications sorted by rank')
.add_argument('-L', '--highlanders', action='store_const',
const=True, default=False,
help='print applications only for highlanders')
.add_argument('-l', '--label', type=str, nargs='+', default=(),
help='print applications only for people with label')
.add_argument('-a', '--attribute', nargs='+', metavar='ATTRNAME ATTRVALUE', action='append',
help='print applications only for people with matching attributes, '
'e.g. -a n_applied 3 -a . Call "-a list" to get a list of attributes.')
.add_argument('persons', nargs='*',
help='name fragments of people to display')
)

def do_dump(self, args):
"Print information about applications"
Expand Down Expand Up @@ -413,27 +428,29 @@ def _dumpone(self, p, format='short'):
labels_newline=labels + '\n' if labels else '',
**cat_ratings)

grep_options = cmd_completer.PagedArgumentParser('grep')\
.add_argument('-n', '--fullname', dest='what', action='store_const',
const=operator.attrgetter('fullname'), default=str,
help='grep institutes')\
.add_argument('--affiliation', dest='what', action='store_const',
const=operator.attrgetter('affiliation'), default=str,
help='grep affiliation')\
.add_argument('--nationality', dest='what', action='store_const',
const=operator.attrgetter('nationality'), default=str,
help='grep nationality')\
.add_argument('--institute', dest='what', action='store_const',
const=operator.attrgetter('institute'),
help='grep institutes')\
.add_argument('-g', '--group', dest='what', action='store_const',
const=operator.attrgetter('group'),
help='grep groups')\
.add_argument('-l', '--long', dest='format',
action='store_const', const='long', default='short',
help='provide full listing')\
.add_argument('pattern',
help='pattern to look for')
grep_options = (
cmd_completer.PagedArgumentParser('grep')
.add_argument('-n', '--fullname', dest='what', action='store_const',
const=operator.attrgetter('fullname'), default=str,
help='grep institutes')
.add_argument('--affiliation', dest='what', action='store_const',
const=operator.attrgetter('affiliation'), default=str,
help='grep affiliation')
.add_argument('--nationality', dest='what', action='store_const',
const=operator.attrgetter('nationality'), default=str,
help='grep nationality')
.add_argument('--institute', dest='what', action='store_const',
const=operator.attrgetter('institute'),
help='grep institutes')
.add_argument('-g', '--group', dest='what', action='store_const',
const=operator.attrgetter('group'),
help='grep groups')
.add_argument('-l', '--long', dest='format',
action='store_const', const='long', default='short',
help='provide full listing')
.add_argument('pattern',
help='pattern to look for')
)

def do_grep(self, args):
"Look for string in applications"
Expand All @@ -452,20 +469,22 @@ def print_grading_stats(self, applications):
stats.rename(index={'nan':'todo', 'NaN':'todo'}, inplace=True)
print(stats)

grade_options = cmd_completer.PagedArgumentParser('grade')\
.add_argument('what', choices=['motivation', 'formula', 'location'],
help='what to grade | set formula | set location')\
.add_argument('-s', '--stat', action='store_true',
help='display statics about the grading process itself')\
.add_argument('-g', '--graded', type=int,
nargs='?', const=all, metavar='SCORE',
help='grade already graded too, optionally with specified score')\
.add_argument('-l', '--label', nargs='+', default=(),
help='show only people with all of those labels')\
.add_argument('-d', '--disagreement', type=int,
nargs='?', const=all, metavar='WHO',
help='grade people who have a >1 pt difference')\
.add_argument('person', nargs='*')
grade_options = (
cmd_completer.PagedArgumentParser('grade')
.add_argument('what', choices=['motivation', 'formula', 'location'],
help='what to grade | set formula | set location')
.add_argument('-s', '--stat', action='store_true',
help='display statics about the grading process itself')
.add_argument('-g', '--graded', type=int,
nargs='?', const=all, metavar='SCORE',
help='grade already graded too, optionally with specified score')
.add_argument('-l', '--label', nargs='+', default=(),
help='show only people with all of those labels')
.add_argument('-d', '--disagreement', type=int,
nargs='?', const=all, metavar='WHO',
help='grade people who have a >1 pt difference')
.add_argument('person', nargs='*')
)

@set_completions('formula',
'location',
Expand Down Expand Up @@ -589,12 +608,14 @@ def do_grade(self, arg):

RATING_CATEGORIES = ['programming', 'open_source', 'python', 'vcs', 'underrep']

rate_options = cmd_completer.PagedArgumentParser('rate')\
.add_argument('-m', '--missing', action='store_true',
help='rate all missing fields')\
.add_argument('what', nargs='?',
choices=RATING_CATEGORIES)\
.add_argument('args', nargs='*')
rate_options = (
cmd_completer.PagedArgumentParser('rate')
.add_argument('-m', '--missing', action='store_true',
help='rate all missing fields')
.add_argument('what', nargs='?',
choices=RATING_CATEGORIES)
.add_argument('args', nargs='*')
)

@set_completions(*RATING_CATEGORIES)
def do_rate(self, arg):
Expand Down Expand Up @@ -645,6 +666,7 @@ def _set_grading(self, person, score):
printff('Motivation score set to {}', score)

def _grade(self, person, disagreement):
"This is the function that does the loop asking question +1/0/-1/…"
if disagreement:
scores = self._get_gradings(person)
else:
Expand Down Expand Up @@ -829,21 +851,23 @@ def _equiv_master(self, variant):
return key
return variant.strip()

rank_options = cmd_completer.PagedArgumentParser('rank')\
.add_argument('-s', '--short', action='store_const',
dest='format', const='short', default='long',
help='show only names and emails')\
.add_argument('--use-labels', action='store_true', default=True,
help=argparse.SUPPRESS)\
.add_argument('-n', '--no-labels', action='store_false', dest='use_labels',
help="don't use labels in ranking")\
.add_argument('-l', '--label', nargs='+', default=(),
help='show only people with all of those labels')\
.add_argument('-f', '--format', choices=RANK_FORMATS.keys(),
help='use format')\
.add_argument('-c', '--column-width',
dest='width', type=int, default=20,
help='specify width of institute and group columns')
rank_options = (
cmd_completer.PagedArgumentParser('rank')
.add_argument('-s', '--short', action='store_const',
dest='format', const='short', default='long',
help='show only names and emails')
.add_argument('--use-labels', action='store_true', default=True,
help=argparse.SUPPRESS)
.add_argument('-n', '--no-labels', action='store_false', dest='use_labels',
help="don't use labels in ranking")
.add_argument('-l', '--label', nargs='+', default=(),
help='show only people with all of those labels')
.add_argument('-f', '--format', choices=RANK_FORMATS.keys(),
help='use format')
.add_argument('-c', '--column-width',
dest='width', type=int, default=20,
help='specify width of institute and group columns')
)

def do_rank(self, args):
"Print list of people sorted by ranking"
Expand Down Expand Up @@ -1157,8 +1181,10 @@ def do_label(self, args):

do_label.completions = _complete_name

save_options = cmd_completer.PagedArgumentParser('save')\
.add_argument('filename', nargs='?')
save_options = (
cmd_completer.PagedArgumentParser('save')
.add_argument('filename', nargs='?')
)

def do_save(self, args):
"Save the fruits of thy labour"
Expand Down Expand Up @@ -1428,31 +1454,35 @@ def wrap_paragraphs(text, prefix=''):
return ('\n'+prefix).join(wrapped)


grader_options = cmd_completer.ModArgumentParser('grader')\
.add_argument('-i', '--identity', type=int,
choices=IDENTITIES,
help='Index of person grading applications')\
.add_argument('--history-file',
type=pathlib.Path,
default=pathlib.Path('~/.grader_history').expanduser(),
help='File to record typed in commands')\
.add_argument('applications',
nargs='?',
type=pathlib.Path,
default=pathlib.Path('applications.csv'),
help='CSV file with application data')
grader_options = (
cmd_completer.ModArgumentParser('grader')
.add_argument('-i', '--identity',
help='Index of person grading applications')
.add_argument('--history-file',
type=pathlib.Path,
default=pathlib.Path('~/.grader_history').expanduser(),
help='File to record typed in commands')
.add_argument('applications',
nargs='?',
type=pathlib.Path,
default=pathlib.Path('applications.csv'),
help='CSV file with application data')
)

def main():
logging.basicConfig(level=logging.INFO)

opts = grader_options.parse_args()

cmd = Grader(
identity=opts.identity,
csv_file=opts.applications,
history_file=opts.history_file,
)

# Set identity in a way that checks that the identity is known
if opts.identity:
cmd.do_identity(opts.identity)

if sys.stdin.isatty():
while True:
try:
Expand Down
33 changes: 33 additions & 0 deletions grader/test_grader.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .grader import Grader

import pytest

CSV_APPLICATIONS = """
"First name","Last name","Nationality","Affiliation","programming","open_source","applied","python","born","gender","email","group","institute","vcs","underrep","position","position_other","programming_description","open_source_description","cv","motivation"
Expand Down Expand Up @@ -62,6 +63,10 @@
underrep = underrep

[equivs]

[motivation_score-zbyszek]

[motivation_score-rike]
"""


Expand All @@ -73,6 +78,34 @@ def _tmp_application_files(tmp_path, config_string, csv_string):
return config_tmp_path, csv_tmp_path


def test_grader_identity(tmp_path, capsys):
config_tmp_path, csv_tmp_path = _tmp_application_files(tmp_path, CONF, CSV_APPLICATIONS)

grader = Grader(identity='zbyszek', csv_file=csv_tmp_path)
assert grader.identity == 'zbyszek'

grader.do_identity(' rike ')
assert grader.identity == 'rike'

out, err = capsys.readouterr()
assert err == ''
assert 'Identity set to rike' in out

def test_grader_bad_identity(tmp_path, capsys):
config_tmp_path, csv_tmp_path = _tmp_application_files(tmp_path, CONF, CSV_APPLICATIONS)

grader = Grader(csv_file=csv_tmp_path)

assert grader.identity is None

with pytest.raises(ValueError):
grader.do_identity(' bad ')
assert grader.identity is None

out, err = capsys.readouterr()
assert err == ''
assert 'Identity set to bad' not in out

def test_grader_rank(tmp_path, capsys):
# Basic test, just checking that it does not crash
config_tmp_path, csv_tmp_path = _tmp_application_files(tmp_path, CONF, CSV_APPLICATIONS)
Expand Down
Loading