Commit 3765049c authored by Alan Marchiori's avatar Alan Marchiori
Browse files

menu dev working

parent 49841f63
......@@ -4,4 +4,5 @@ from .userconfig import userconfig
from .test import test
from .grade import grade
from .report import report
__all__ = ['userconfig', 'init', 'check', 'test', 'grade', 'report']
from .menu import menu
__all__ = ['userconfig', 'init', 'check', 'test', 'grade', 'report' ,'menu']
......@@ -143,7 +143,7 @@ def grade(lab, part, clone, dograde, regrade, user, skip, push):
error("Path doesn't exist, cannot grade!")
continue
print('getting history for {} {} on lab {}.'.format(coursename, student, labstr))
#print('getting history for {} {} on lab {}.'.format(coursename, student, labname))
# check for grade history
hfile = None
with History(location=labpath, dbpath='{}/{}/{}'.format(coursename, student, labstr)) as hist:
......@@ -294,28 +294,29 @@ def grade(lab, part, clone, dograde, regrade, user, skip, push):
# add hist.pathfile to GIT and PUSH.
# if not g.clean(cwd=labpath) or g.file_diff(cwd=labpath,
# files=[hfile]):
if push:
# only check if the history file has changes. we won't automatically
# push any other files!
if g.file_diff(cwd=labpath, files=[hfile]):
g.do_push(cwd = labpath,
addfiles = [hfile],
message = 'Lab {} graded by {}'.format(
lab,
who
))
if not g.clean(cwd=labpath):
echo(g.status(cwd=labpath))
if confirm("The working directory is not clean, do you want to add & push ALL changes?"):
g.do_push(cwd=labpath, addall=True,
# hfile is set to none for db backends...
if hfile != None:
if push:
# only check if the history file has changes. we won't automatically
# push any other files!
if g.file_diff(cwd=labpath, files=[hfile]):
g.do_push(cwd = labpath,
addfiles = [hfile],
message = 'Lab {} graded by {}'.format(
lab,
who
))
else:
warn("Not pushing to gitlab by your command.")
if not g.clean(cwd=labpath):
echo(g.status(cwd=labpath))
if confirm("The working directory is not clean, do you want to add & push ALL changes?"):
g.do_push(cwd=labpath, addall=True,
message = 'Lab {} graded by {}'.format(
lab,
who
))
else:
warn("Not pushing to gitlab by your command.")
# fb_filename = os.path.join(labpath, feedback)
# feedbackfile = Path(fb_filename)
# feedbackfile.write_text(report)
......
"""
This is the GUI (console) interface. This is mainly for TAs but may be later
updated for studnets.
This uses prompt_toolkit
https://python-prompt-toolkit.readthedocs.io/en/latest/pages/full_screen_apps.html
"""
import sys
import click
from prompt_toolkit import Application, prompt, print_formatted_text
from prompt_toolkit.application import run_in_terminal
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.application.current import get_app
from prompt_toolkit.filters import has_focus
from prompt_toolkit.filters.utils import to_filter
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.layout.containers import HSplit, VSplit, Window, WindowAlign
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.styles import Style
from prompt_toolkit.widgets import (
Box,
Button,
Checkbox,
Dialog,
Frame,
Label,
MenuContainer,
MenuItem,
ProgressBar,
RadioList,
Checkbox,
CheckboxList,
TextArea,
)
from prompt_toolkit.utils import Event
import os
import platform
from config import UserConfig
import courses
from utils.history import History
from utils.git import Git
from utils.gitlab import GitLab
from utils.debug_port import udbg
import commands
import threading
from pprint import pprint
def get_statusbar_right_text(buf):
# returns a function to return the passed-in buffer pos
return lambda: "{}:{} ".format(
buf.document.cursor_position_row + 1,
buf.document.cursor_position_col + 1,
)
def do_cmd(ctx, cmdstr, proj_list, lab_list, part_list):
# generic command event callback
udbg.send(" ".join(map(str, [cmdstr,
proj_list.current_value,
lab_list.current_value,
part_list.current_value])))
xargs = {}
if cmdstr =='regrade':
cmdstr='grade'
xargs['regrade'] = True
t = getattr(commands, cmdstr)
def it():
ctx.invoke(t,
lab=lab_list.current_value,
user=proj_list.current_value,
part=part_list.current_value,
**xargs)
input ('Press enter to return to menu.')
run_in_terminal(it)
def update_student_list_with_grades(coursename, coursepath, projects, proj_list, labstr):
"""
Query the db for grades and update student list accordingly.
This is run from a backround thread becuase it could be slow.
"""
# proj_list = RadioList(values=[(None, 'All')] + [
# (p['owner']['username'], "{} ({})".format(p['owner']['username'], p['owner']['name']))
# for p in projects])
#for p in projects:
udbg.send("update_w_grades ({}, {}, {}. {})".format(
coursename, coursepath, proj_list, labstr))
rubric = courses.load_rubric(coursename, labstr)
part_ids = [partinfo['index'] for name, partinfo in rubric['parts'].items()]
part_strs = [name for name, partinfo in rubric['parts'].items()]
student_names_by_id = {
p['owner']['username']: p['owner']['name'] for p in projects
}
for pidx, val in enumerate(proj_list.values):
student_id, disp_str = val
if student_id == None:
continue
#split name from "gic004 (Gabriella Colletti)"
#student_name = disp_str.split(' ', 1)[-1][1:-1] # strip parens
student_name = student_names_by_id[student_id]
labpath = os.path.join(coursepath, student_id, rubric['path'])
ginfo = ' ('
with History(location=labpath, save=False,
dbpath='{}/{}/{}'.format(coursename, student_id, labstr)) as h:
if 'grade' in h:
for i,part in zip(part_ids, part_strs):
if part in h['grade']:
ginfo += str(i)
else:
ginfo += '*'
else:
ginfo += "*"*len(part_ids)
ginfo += ')'
if 'grade' in h and 'TOTAL' in h['grade']:
ginfo += "[{:3}]".format(h['grade']['TOTAL']['total'])
else:
ginfo += "[***]"
proj_list.values[pidx] = (student_id,
'{} ({})'.format(
student_id,
student_name).ljust(30) + ginfo)
get_app().invalidate()
# text = prompt('Done?')
# print ('got', text)
last_lab = None
rubrics = {}
def on_invalidate(app, coursename, coursepath, projects, proj_list, lab_list, part_list):
"when selected lab changes, update the part list correctly"
global last_lab
global rubrics
if lab_list.current_value != last_lab:
if lab_list.current_value is None:
part_list.values = [(None, 'select lab first')]
else:
labstr = lab_list.values[lab_list._selected_index][1]
udbg.send("labstr is {}".format(labstr))
if not labstr in rubrics:
rubrics[labstr] = courses.load_rubric(coursename, labstr)
parts = rubrics[labstr]['parts'].keys()
# regen part_list
#[None, 'All'] +
part_list.values = [(None, 'All')] + [
(rubrics[labstr]['parts'][p]['index'],
"{} [{}]".format(p, rubrics[labstr]['parts'][p]['index'])) for p in parts]
#part_list.current_values = []
#part_list.current_value = None
#part_list._selected_index = 0
if lab_list.current_value != None:
threading.Thread(target = update_student_list_with_grades,
name="Project list grade update thread",
args=(coursename, coursepath, projects, proj_list, labstr), daemon = True).start()
# update_student_list_with_grades(coursename, coursepath, projects, proj_list, labstr)
else:
proj_list.values = [(None, 'All')] + [
(p['owner']['username'], "{} ({})".format(p['owner']['username'], p['owner']['name']))
for p in projects]
last_lab = lab_list.current_value
@click.command()
@click.pass_context
def menu(ctx):
"Menu based interaface for labtool."
coursename, coursepath, labname, ista = courses.detect_course()
global labtool_version # from main lt
udbg.send("menu started")
if not ista:
error("You are not a TA for the course {} in {}!".format(
coursename, coursepath))
error("You probably meant to run some other command.")
return
if not coursename:
error("The current directory is not an initialized course! You must first cd into an initialized course to check!")
return
# search and clone student repos....
gl = GitLab()
g = Git()
projects = gl.search_all_projects(coursename, owned=False)
labnames = courses.load_labnames(coursename)
#from pprint import pprint
#pprint(projects)
#exit()
who = "{}@{}".format(os.environ['USER'], platform.node())
buffer1 = Buffer()
proj_list = RadioList(values=[(None, 'All')] + [
(p['owner']['username'], "{} ({})".format(p['owner']['username'], p['owner']['name']))
for p in projects])
lab_list = RadioList(values=[(None, 'All')] + [(int(l[3:]), l) for l in labnames])
part_list = RadioList(values=[(None, 'select lab first')])
command_list = HSplit([
Button(text="check", handler=lambda: do_cmd(ctx, 'check', proj_list, lab_list, part_list)),
Button(text="grade", handler=lambda: do_cmd(ctx, 'grade', proj_list, lab_list, part_list)),
Button(text="regrade", handler=lambda: do_cmd(ctx, 'regrade', proj_list, lab_list, part_list)),
Button(text="report", handler=lambda: do_cmd(ctx, 'report', proj_list, lab_list, part_list)),
Button(text="exit", handler=lambda: get_app().exit(result=False))
])
verstr = "Labtool v{}".format(sys.modules['__main__'].__version__)
root_container = HSplit([
#Window(content=BufferControl(buffer=buffer1)),
VSplit(
[
Frame(title="{} Students".format(coursename), body=proj_list),
Frame(title="Labs", body=lab_list),
Frame(title="Parts", body=part_list),
Frame(title="Commands", body=command_list)
]
),
#Frame(body=Window(height=40,content=BufferControl(buffer=buffer1))),
VSplit(
[
Window(
FormattedTextControl("{}@{}".format(os.environ['USER'], platform.node())), style="class:status"
),
Window(
FormattedTextControl("Press Ctrl-c to exit."),
style="class:status"
),
Window(
FormattedTextControl(verstr, style="class:status"),
width=len(verstr)
),
# Window(
# FormattedTextControl(get_statusbar_right_text(buffer1)),
# style="class:status.right",
# width=9,
# align=WindowAlign.RIGHT,
# ),
],
height=1,
)
])
kb = KeyBindings()
kb.add("tab")(focus_next)
kb.add("s-tab")(focus_previous)
# keybinds for command menu buttons that look like a list
kb.add("up", filter=has_focus(command_list))(focus_previous)
kb.add("down", filter=has_focus(command_list))(focus_next)
f = to_filter(
has_focus(proj_list) |
has_focus(lab_list) |
has_focus(part_list))
# keybinds for command menu right left
kb.add("left", filter=f)(focus_previous)
kb.add("right", filter=f)(focus_next)
@kb.add("left", filter=has_focus(command_list))
def command_list_left(event):
event.app.layout.focus(part_list)
@kb.add("right", filter=has_focus(command_list))
def command_list_right(event):
event.app.layout.focus(buffer1)
@kb.add("c-m")
def root_ctrl_m(event):
event.app.layout.focus(root_container.window)
@kb.add('c-c')
def ctrl_c(event):
event.app.exit()
root_container = MenuContainer(
body=root_container,
menu_items=[
MenuItem("File",
children=[MenuItem("Exit", handler=lambda: get_app().exit(result=False))]),
MenuItem("Command",
children=[
MenuItem("check"),
MenuItem("grade"),
MenuItem("report")
]),
MenuItem("About")
])
style = Style.from_dict(
{
"window.border": "#888888",
"shadow": "bg:#222222",
"menu-bar": "bg:#aaaaaa #888888",
"menu-bar.selected-item": "bg:#ffffff #000000",
"menu": "bg:#888888 #ffffff",
"menu.border": "#aaaaaa",
"window.border shadow": "#444444",
"focused button": "bg:#880000 #ffffff noinherit",
# Styling for Dialog widgets.
"button-bar": "bg:#aaaaff",
"bottom-toolbar": "#aaaa00 bg:#ff0000",
"bottom-toolbar.text": "#aaaa44 bg:#aa4444",
}
)
layout = Layout(root_container, focused_element=proj_list)
app = Application(layout=layout,
key_bindings=kb, full_screen=True,
on_invalidate = lambda app: on_invalidate(app, coursename, coursepath,
projects, proj_list, lab_list, part_list))
app.run()
......@@ -14,6 +14,8 @@ from utils.git import Git
from pprint import pprint
import numpy as np
from collections import defaultdict
from utils.debug_port import udbg
def ta_report(coursename, coursepath, labname, user, clone):
if labname == None:
labnames = courses.load_labnames(coursename)
......@@ -73,7 +75,8 @@ def ta_report(coursename, coursepath, labname, user, clone):
labname = rub['name']
labpath = os.path.join(coursepath, student, rub['path'])
with History(location=labpath,save=False) as h:
with History(location=labpath, save=False,
dbpath='{}/{}/{}'.format(coursename, student, labname)) as h:
if 'grade' in h:
r = h['grade']
......@@ -134,7 +137,6 @@ def student_report(coursename, coursepath, labname, clone):
labname = rub['name']
labpath = os.path.join(coursepath, rub['path'])
if os.path.exists(labpath):
newline()
echo ("-"*max(10,width-5))
......@@ -189,6 +191,10 @@ def report(lab, user, clone):
"""
print grade report, if in ta context all users, for students all lab scores
"""
udbg.send("entered report for {} {} {}".format(
lab, user, clone
))
# convert lab number to filename magic.
if lab:
labstr = "lab{:02}".format(lab)
......
......@@ -98,12 +98,19 @@ def check_rubric(filename, r):
idx = set()
for p, check in r['parts'].items():
assert 'index' in check, "Part {} has no index!".format(p)
if check['index'] in idx:
warn ("Rubric part {} has duplicate index {}".format(
p, check['index']
))
idx.add(check['index'])
for keyname in list(r['parts'].keys()):
if '.' in keyname:
warn("Rubric {} part has '.' in the name, this is not allowed! -- auto fixing.".format(filename))
newkey = keyname.replace(".", "")
r['parts'][newkey] = r['parts'][keyname]
del r['parts'][keyname]
# check points sum to 100
s = 0
for p, check in r['parts'].items():
......
......@@ -14,13 +14,6 @@ you may need to chmod +x lt.py
# https://click.palletsprojects.com
import click
# Local class config info
import config
from config.echo import error
# CLI commands
import commands
# logging
import traceback
import logging
......@@ -29,16 +22,26 @@ import os
import platform
import sys
__version__ = '1.0.10'
__date__ = '2020-04-12T15:39:19.329244'
__user__ = 'cs206'
__host__ = 'linuxremote3.bucknell.edu'
labtool_version = __version__
# Local class config info
import config
from config.echo import error
# CLI commands
# this has to be after get_labtool_version is defined!
import commands
smtp_handler = logging.handlers.SMTPHandler(
mailhost=("smtp.bucknell.edu", 25),
fromaddr="labtool@eg.bucknell.edu",
toaddrs=config.crash_reports,
subject=u"labtool crash report!")
__version__ = '1.0.10'
__date__ = '2020-04-12T15:39:19.329244'
__user__ = 'cs206'
__host__ = 'linuxremote3.bucknell.edu'
@click.group()
def main():
"""Bucknell University Computer Science lab tool [lt].
......
import socket
__all__ = ["udbg"]
class udp_debug:
def __init__(self, ip="127.0.0.1", port=20001):
self.ip = ip
self.port = port
self.socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
#self.socket.bind((self.ip, self.port))
def send(self, msg):
if not msg is str:
msg = str(msg)
self.socket.sendto(msg.encode()+b'\n', (self.ip, self.port))
udbg = udp_debug()
......@@ -4,7 +4,7 @@ import platform
import json
import sys
import config
from config.echo import debug, error
from config.echo import debug, error, echo, warn
from pymongo import MongoClient
class HistoryConfigurationException(Exception):
pass
......@@ -29,6 +29,19 @@ class History(dict):
"class method to determine if a history file exists (w/o creation)"
return os.path.exists(os.path.join(location, filename))
def fixkeys(self, doc):
# some keys have . in their name, mongodb doesn't like this. remove it.
newdoc = {}
for key,val in doc.items():
if '.' in key:
key = key.replace('.', '')
if type(val) is dict:
newdoc[key] = self.fixkeys(val)
else:
newdoc[key] = val
return newdoc
def __init__(self, location=".", dbpath=None,
filename = '.history.json',
save = True,
......@@ -56,6 +69,8 @@ class History(dict):
if self.gradedb == None:
raise HistoryConfigurationException("You must first configure the 'gradedb' value in your userconfig.")
debug("using mongodb for history")
# hack to show not using file backend.
self.filename = None
elif self.gradebackend == History.backend_json:
debug("using history file {}".format(self.pathfile))
......@@ -75,6 +90,8 @@ class History(dict):
warn ("The grade is currently locked by {}".format(d['lock']))
# prompt to take lock or abort
self['path'] = self.dbpath
if d != None:
self.update(d)
# lock the document
......@@ -83,15 +100,24 @@ class History(dict):
{'_id': d['_id']},
{'$set': {'lock': self.who}})
else:
# insert empty document
if self.save:
r = self.mc.insert_one({
'path': self.dbpath,
'lock': self.who})
else:
r = self.mc.insert_one({'path': self.dbpath})
self['lock'] = self.who
# check for disk json file to import to db
if os.path.exists(self.pathfile):
# check if there is a disk history file and use that for initial values
with open(self.pathfile, 'r') as f:
self.rawjson = f.read()
try:
df = json.loads(self.rawjson)
echo ("Loaded jsonfile.")
df = self.fixkeys(df)
except json.decoder.JSONDecodeError:
error("Grading history file at {} is corrupt. Manually remove or fix this file to continue running.".format(self.pathfile))
exit(213)
self.update(df)
# or just insert an empty document
r = self.mc.insert_one(self)
self['_id'] = r.inserted_id
else:
if os.path.exists(self.pathfile):
try:
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment