Skip to content
Snippets Groups Projects
Commit 110f89a6 authored by tuhe's avatar tuhe
Browse files

updates

parent 6cae7b2b
Branches
No related tags found
No related merge requests found
Showing
with 2027 additions and 6 deletions
......@@ -33,4 +33,9 @@ setuptools.setup(
python_requires=">=3.8",
license="MIT",
install_requires=['numpy', 'tabulate', "pyfiglet", "coverage", "colorama", 'tqdm', 'importnb', 'requests'],
include_package_data=True,
package_data={'': ['dashboard/static/*', 'dashboard/templates/*'],}, # so far no Manifest.in.
entry_points={
'console_scripts': ['unitgrade=unitgrade.dashboard.dashboard_cli:main'],
}
)
Metadata-Version: 2.1
Name: unitgrade
Version: 0.1.26
Version: 0.1.28.1
Summary: A student homework/exam evaluation framework build on pythons unittest framework.
Home-page: https://lab.compute.dtu.dk/tuhe/unitgrade
Author: Tue Herlau
Author-email: tuhe@dtu.dk
License: MIT
Project-URL: Bug Tracker, https://lab.compute.dtu.dk/tuhe/unitgrade/issues
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
......@@ -190,3 +191,4 @@ Please contact me and we can discuss your specific concerns.
year={2022},
}
```
......@@ -3,6 +3,7 @@ README.md
pyproject.toml
setup.py
src/unitgrade/__init__.py
src/unitgrade/artifacts.py
src/unitgrade/evaluate.py
src/unitgrade/framework.py
src/unitgrade/runners.py
......@@ -11,5 +12,19 @@ src/unitgrade/version.py
src/unitgrade.egg-info/PKG-INFO
src/unitgrade.egg-info/SOURCES.txt
src/unitgrade.egg-info/dependency_links.txt
src/unitgrade.egg-info/entry_points.txt
src/unitgrade.egg-info/requires.txt
src/unitgrade.egg-info/top_level.txt
src/unitgrade/dashboard/static/favicon.ico
src/unitgrade/dashboard/static/sidebars.css
src/unitgrade/dashboard/static/sidebars.js
src/unitgrade/dashboard/static/unitgrade.css
src/unitgrade/dashboard/static/unitgrade.js
src/unitgrade/dashboard/static/wz_js.js
src/unitgrade/dashboard/static/wz_style.css
src/unitgrade/dashboard/static/wz_style_modified.css
src/unitgrade/dashboard/templates/base.html
src/unitgrade/dashboard/templates/bootstrap.html
src/unitgrade/dashboard/templates/index3.html
src/unitgrade/dashboard/templates/terminal.html
src/unitgrade/dashboard/templates/wz.html
\ No newline at end of file
colorama
coverage
importnb
numpy
tabulate
pyfiglet
coverage
colorama
requests
tabulate
tqdm
importnb
import fnmatch
from threading import Lock
import argparse
import datetime
import os
import subprocess
import logging
import sys
import glob
from pathlib import Path
from flask import Flask, render_template
from flask_socketio import SocketIO
from pupdb.core import PupDB
from unitgrade_private.hidden_gather_upload import dict2picklestring, picklestring2dict
from unitgrade.dashboard.app_helpers import get_available_reports
from unitgrade.dashboard.watcher import Watcher
from unitgrade.dashboard.file_change_handler import FileChangeHandler
from unitgrade_private import load_token
logging.getLogger('werkzeug').setLevel("WARNING")
def mkapp(base_dir="./"):
app = Flask(__name__, template_folder="templates", static_folder="static", static_url_path="/static")
x = {'watcher': None, 'handler': None} # maintain program state across functions.
app.config["SECRET_KEY"] = "secret!"
app.config["fd"] = None
app.config["TEMPLATES_AUTO_RELOAD"] = True
app.config["child_pid"] = None
socketio = SocketIO(app)
available_reports = get_available_reports(jobfolder=base_dir)
current_report = {}
watched_files_lock = Lock()
watched_files_dictionary = {}
def do_something(file_pattern):
"""
Oh crap, `file` has changed on disk. We need to open it, look at it, and then do stuff based on what is in it.
That is, we push all chnages in the file to clients.
We don't know what are on the clients, so perhaps push everything and let the browser resolve it.
"""
with watched_files_lock:
file = watched_files_dictionary[file_pattern]['file']
type = watched_files_dictionary[file_pattern]['type']
lrc = watched_files_dictionary[file_pattern]['last_recorded_change']
if type == 'question_json': # file.endswith(".json"):
if file is None:
return # There is nothing to do, the file does not exist.
db = PupDB(file)
if "state" not in db.keys(): # Test has not really been run yet. There is no reason to submit this change to the UI.
return
state = db.get('state')
key = os.path.basename(file)[:-5]
wz = db.get('wz_stacktrace') if 'wz_stacktrace' in db.keys() else None
if wz is not None:
wz = wz.replace('<div class="traceback">', f'<div class="traceback"><div class="{key}-traceback">')
wz += "</div>"
coverage_files_changed = db.get('coverage_files_changed') if 'coverage_files_changed' in db.keys() else None
if state == "fail":
print("State is fail, I am performing update", state, key)
socketio.emit('testupdate', {"id": key, 'state': state, 'stacktrace': wz, 'stdout': db.get('stdout'), 'run_id': db.get('run_id'),
'coverage_files_changed': coverage_files_changed}, namespace="/status")
elif type =='coverage':
if lrc is None: # Program startup. We don't care about this.
return
db = get_report_database()
for q in db['questions']:
for i in db['questions'][q]['tests']:
# key = '-'.join(i)
test_invalidated = False
for f in db['questions'][q]['tests'][i]['coverage_files']:
# fnmatch.fnmatch(f, file_pattern)
if fnmatch.fnmatch(file, "**/" + f):
# This file has been matched. The question is now invalid.
test_invalidated = True
break
if test_invalidated:
# Why not simply write this bitch into the db?
dbf = current_report['root_dir'] + "/" + current_report['questions'][q]['tests'][i]['artifact_file']
db = PupDB(dbf)
print("A test has been invalidated. Setting coverage files", file)
db.set('coverage_files_changed', [file])
# print("dbf", dbf)
# print("marking a test as invalidated: ", db)
elif type =="token":
a, b = load_token(file)
rs = {}
for k in a['details']:
for ikey in a['details'][k]['items']:
rs['-'.join(ikey)] = a['details'][k]['items'][ikey]['status']
socketio.emit('token_update', {"full_path": file, 'token': os.path.basename(file),
'results': rs, 'state': 'evaluated'}, namespace="/status")
else:
raise Exception("Bad type: " + type)
def get_json_base(jobfolder):
return current_report['json']
def get_report_database():
dbjson = get_json_base(base_dir)
db = PupDB(dbjson)
from unitgrade_private.hidden_gather_upload import picklestring2dict
rs = {}
for k in db.keys():
if k == 'questions':
qenc, _ = picklestring2dict(db.get("questions"))
rs['questions'] = qenc # This feels like a good place to find the test-file stuff.
else:
rs[k] = db.get(k)
lpath_full = Path(os.path.normpath(os.path.dirname(dbjson) + "/../" + os.path.basename(dbjson)[12:-5] + ".py"))
rpath = Path(db.get('relative_path'))
base = lpath_full.parts[:-len(rpath.parts)]
rs['local_base_dir_for_test_module'] = str(Path(*base))
rs['test_module'] = ".".join(db.get('modules'))
return rs
def select_report_file(json):
current_report.clear()
for k, v in available_reports[json].items():
current_report[k] = v
for k, v in get_report_database().items():
current_report[k] = v
def mkempty(pattern, type):
fls = glob.glob(current_report['root_dir'] + pattern)
fls.sort(key=os.path.getmtime)
# if type == 'token':
# fls.sort(key= lambda s: int(s[:-6].split("_")[-3]) ) # Sort by points obtained.
f = None if len(fls) == 0 else fls[-1] # Bootstrap with the given best matched file.
return {'type': type, 'last_recorded_change': None, 'last_handled_change': None, 'file': f}
with watched_files_lock:
watched_files_dictionary.clear()
db = PupDB(json)
dct = picklestring2dict(db.get('questions'))[0]
for q in dct.values():
for i in q['tests'].values():
file = "*/"+i['artifact_file']
watched_files_dictionary[file] = mkempty(file, 'question_json') # when the file was last changed and when that change was last handled.
for c in i['coverage_files']:
file = "*/"+c
watched_files_dictionary[file] = mkempty(file, "coverage")
# tdir = "*/"+os.path.dirname(current_report['relative_path_token']) + "/" + os.path.basename(current_report['relative_path'])[:-3] + "*.token"
tdir = "*/"+current_report['token_stub'] + "*.token"
watched_files_dictionary[tdir] = mkempty(tdir, 'token')
for l in ['watcher', 'handler']:
if x[l] is not None: x[l].close()
x['watcher'] = Watcher(current_report['root_dir'], watched_files_dictionary, watched_files_lock)
x['watcher'].run()
x['handler'] = FileChangeHandler(watched_files_dictionary, watched_files_lock, do_something)
x['handler'].start()
if len(available_reports) == 0:
print("Unitgrade was launched in the directory")
print(">", base_dir)
print("But this directory does not contain any reports. Run unitgrade from a directory containing report files.")
sys.exit()
# x['current_report'] =
select_report_file(list(available_reports.keys()).pop())
@app.route("/app.js")
def appjs():
return render_template("app.js")
@socketio.on("ping", namespace="/status")
def ping():
json = get_json_base(jobfolder=base_dir)[0]
socketio.emit("pong", {'base_json': json})
@app.route("/")
def index_bare():
# select_report_file()
return index(list(available_reports.values()).pop()['menu_name'])
@app.route("/report/<report>")
def index(report):
if report != current_report['menu_name']:
for k, r in available_reports.items():
if report == r['menu_name']:
select_report_file(k)
raise Exception("Bad report selected", report)
rs = get_report_database()
qenc = rs['questions']
x = {}
for k, v in current_report.items():
x[k] = v
x['questions'] = {}
for q in qenc:
items = {}
for it_key, it_value in qenc[q]['tests'].items():
it_key_js = "-".join(it_key)
# do a quick formatting of the hints. Split into list by breaking at *.
hints = it_value['hints']
for k in range(len(hints)):
ahints = []
for h in hints[k][0].split("\n"):
if h.strip().startswith("*"):
ahints.append('')
h = h.strip()[1:]
ahints[-1] += "\n" + h
hints[k] = (ahints, hints[k][1], hints[k][2])
items[it_key_js] = {'title': it_value['title'], 'hints': hints}
x['questions'][q] = {'title': qenc[q]['title'], 'tests': items}
run_cmd_grade = '.'.join(x['modules']) + "_grade"
x['grade_script'] = x['modules'][-1] + "_grade.py"
x['run_cmd_grade'] = f"python -m {run_cmd_grade}"
x['available_reports'] = available_reports
return render_template("index3.html", **x)
@socketio.on("rerun", namespace="/status")
def rerun(data):
"""write to the child pty. The pty sees this as if you are typing in a real
terminal.
"""
db = get_report_database()
targs = ".".join( data['test'].split("-") )
m = '.'.join(db['modules'])
# cmd = f"python -m {m} {targs}"
cmd = f"python -m unittest {m}.{targs}"
try:
out = subprocess.run(cmd, cwd=db['local_base_dir_for_test_module'], shell=True, check=True, capture_output=True, text=True)
except Exception as e: # I think this is related to simple exceptions being treated as errors.
print(e)
pass
# print("oh dear.")
for q in db['questions']:
for i in db['questions'][q]['tests']:
if "-".join(i) == data['test']:
with watched_files_lock:
watched_files_dictionary["*/"+db['questions'][q]['tests'][i]['artifact_file']]['last_recorded_change'] = datetime.datetime.now()
@socketio.on("rerun_all", namespace="/status")
def rerun_all(data):
"""write to the child pty. The pty sees this as if you are typing in a real
terminal.
"""
db = get_report_database()
# targs = ".".join( data['test'].split("-") )
m = '.'.join(db['modules'])
# cmd = f"python -m {m}"
cmd = f"python -m unittest {m}"
try:
out = subprocess.run(cmd, cwd=db['local_base_dir_for_test_module'], shell=True, check=True, capture_output=True, text=True)
except Exception as e:
pass
@socketio.on("pty-input", namespace="/pty")
def pty_input(data):
"""write to the child pty. The pty sees this as if you are typing in a real
terminal.
"""
if app.config["fd"]:
logging.debug("received input from browser: %s" % data["input"])
os.write(app.config["fd"], data["input"].encode())
@app.route("/crash")
def navbar():
assert False
@app.route('/wz')
def wz():
return render_template('wz.html')
@socketio.on("reconnected", namespace="/status")
def client_reconnected(data):
"""write to the child pty. The pty sees this as if you are typing in a real
terminal.
"""
print("Client has reconnected...")
with watched_files_lock:
for k in watched_files_dictionary:
if watched_files_dictionary[k]['type'] in ['token', 'question_json']:
watched_files_dictionary[k]['last_handled_change'] = None
elif watched_files_dictionary[k]['type'] == 'coverage':
pass
else:
raise Exception()
closeables = [x['watcher'], x['handler']]
return app, socketio, closeables
def main():
parser = argparse.ArgumentParser(
description=(
"A fully functional terminal in your browser. "
"https://github.com/cs01/pyxterm.js"
),
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("-p", "--port", default=5000, help="port to run server on")
parser.add_argument("--host",default="127.0.0.1", help="host to run server on (use 0.0.0.0 to allow access from other hosts)",)
parser.add_argument("--debug", action="store_true", help="debug the server")
parser.add_argument("--version", action="store_true", help="print version and exit")
# parser.add_argument("--command", default="bash", help="Command to run in the terminal")
# parser.add_argument("--cmd-args",default="", help="arguments to pass to command (i.e. --cmd-args='arg1 arg2 --flag')",)
args = parser.parse_args()
args_port = 5000
args_host = "127.0.0.1"
from cs108 import deploy
deploy.main(with_coverage=True)
import subprocess
from cs108.report_devel import mk_bad
mk_bad()
bdir = os.path.dirname(deploy.__file__)
app, socketio, closeables = mkapp(base_dir=bdir)
# app.config["cmd"] = [args.command] + shlex.split(args.cmd_args)
green = "\033[92m"
end = "\033[0m"
log_format = green + "pyxtermjs > " + end + "%(levelname)s (%(funcName)s:%(lineno)s) %(message)s"
logging.basicConfig(
format=log_format,
stream=sys.stdout,
level=logging.DEBUG if args.debug else logging.INFO,
)
logging.info(f"serving on http://{args.host}:{args.port}")
debug = args.debug
debug = True
# os.environ["WERKZEUG_DEBUG_PIN"] = "off"
socketio.run(app, debug=debug, port=args_port, host=args.host, allow_unsafe_werkzeug=debug)
for c in closeables:
c.close()
sys.exit()
if __name__ == "__main__":
main()
\ No newline at end of file
#!/usr/bin/env python3
from queue import Queue
from threading import Lock
import argparse
import datetime
import subprocess
from flask import Flask, render_template
from flask_socketio import SocketIO
import pty
import os
import subprocess
import select
import termios
import struct
import fcntl
import shlex
import logging
import sys
import glob
from pupdb.core import PupDB
from unitgrade_private.hidden_gather_upload import picklestring2dict
from unitgrade_private.hidden_gather_upload import dict2picklestring, picklestring2dict
from pathlib import Path
def get_available_reports(jobfolder):
bdir = os.path.abspath(jobfolder)
available_reports = {}
if os.path.isdir(bdir):
fls = glob.glob(bdir + "/**/main_config_*.json", recursive=True)
elif os.path.isfile(bdir):
fls = glob.glob(os.path.dirname(bdir) + "/**/main_config_*.json", recursive=True)
else:
raise Exception("No report files found in the given directory. Start the dashboard in a folder which contains a report test file.")
for f in fls:
db = PupDB(f)
report_py = db.get('relative_path')
lpath_full = Path(os.path.normpath(os.path.dirname(f) + f"/../{os.path.basename(report_py)}"))
# rpath =
base = lpath_full.parts[:-len(Path(report_py).parts)]
# rs['local_base_dir_for_test_module'] = str(Path(*base))
root_dir = str(Path(*base))
token = report_py[:-3] + "_grade.py"
available_reports[f] = {'json': f,
'relative_path': report_py,
'root_dir': root_dir,
'title': db.get('title'),
'relative_path_token': None if not os.path.isfile(root_dir + "/" + token) else token,
'menu_name': os.path.basename(report_py),
}
return available_reports
\ No newline at end of file
from unitgrade.version import __version__
import argparse
import os
import logging
import sys
from unitgrade.dashboard.app import mkapp
def main():
parser = argparse.ArgumentParser(
description=(
"Unitgrade dashboard"
"https://lab.compute.dtu.dk/tuhe/unitgrade"
),
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument('dir', nargs='?', default=os.getcwd(), help="Directory to listen in (default to current directory)")
parser.add_argument("-p", "--port", default=5000, help="port to run server on")
parser.add_argument("--host",default="127.0.0.1", help="host to run server on (use 0.0.0.0 to allow access from other hosts)",)
parser.add_argument("--debug", action="store_true", help="debug the server")
parser.add_argument("--version", action="store_true", help="print version and exit")
args = parser.parse_args()
if args.version:
print(__version__)
exit(0)
# from cs108 import deploy
# deploy.main(with_coverage=True)
# import subprocess
# subprocess.run("python ", cwd="")
# from cs108.report_devel import mk_bad
# mk_bad()
# bdir = os.path.dirname(deploy.__file__)
app, socketio, closeables = mkapp(base_dir=args.dir)
# app.config["cmd"] = [args.command] + shlex.split(args.cmd_args)
green = "\033[92m"
end = "\033[0m"
log_format = green + "pyxtermjs > " + end + "%(levelname)s (%(funcName)s:%(lineno)s) %(message)s"
logging.basicConfig(
format=log_format,
stream=sys.stdout,
level=logging.DEBUG if args.debug else logging.INFO,
)
url = f"http://{args.host}:{args.port}"
logging.info(f"Starting unitgrade dashboard version {__version__}")
logging.info(f"Serving dashboard on: {url}")
# debug = args.debug
debug = False
os.environ["WERKZEUG_DEBUG_PIN"] = "off"
import webbrowser
# url = 'https://codefather.tech/blog/'
webbrowser.open(url)
socketio.run(app, debug=debug, port=args.port, host=args.host) # , allow_unsafe_werkzeug=True )
for c in closeables:
c.close()
sys.exit()
if __name__ == "__main__":
main()
\ No newline at end of file
from threading import Thread
from queue import Queue, Empty
import threading
import datetime
import time
class FileChangeHandler(Thread):
def __init__(self, watched_files_dictionary, watched_files_lock, do_something):
super().__init__()
self.watched_files_dictionary = watched_files_dictionary
self.watched_files_lock = watched_files_lock
self.do_something = do_something
self.stoprequest = threading.Event()
def run(self):
# As long as we weren't asked to stop, try to take new tasks from the
# queue. The tasks are taken with a blocking 'get', so no CPU
# cycles are wasted while waiting.
# Also, 'get' is given a timeout, so stoprequest is always checked,
# even if there's nothing in the queue.
while not self.stoprequest.is_set():
ct = datetime.datetime.now()
# try:
file_to_handle = None
with self.watched_files_lock:
for k, v in self.watched_files_dictionary.items():
if v['last_handled_change'] is None:
file_to_handle = k
break
else:
# This file has been handled recently. Check last change to the file.
if v['last_recorded_change'] is not None:
from datetime import timedelta
if (v['last_recorded_change'] - v['last_handled_change'] ) > timedelta(seconds=0):
file_to_handle = k
break
if file_to_handle is not None:
# Handle the changes made to this exact file.
self.do_something(file_to_handle)
with self.watched_files_lock:
self.watched_files_dictionary[file_to_handle]['last_handled_change'] = datetime.datetime.now()
time.sleep(min(0.1, (datetime.datetime.now()-ct).seconds ) )
def join(self, timeout=None):
# print("Got a stop")
self.stoprequest.set()
super().join(timeout)
def close(self):
print("Closing change handler..")
self.join()
print("Closed.")
#
# q = Queue()
# try:
# e = q.get_nowait()
# except Empty as e:
# print("empty queue. ")
# # menial tasks if required.
src/unitgrade/dashboard/static/favicon.ico

15 KiB

body {
min-height: 100vh;
min-height: -webkit-fill-available;
height: 100%;
}
html {
height: -webkit-fill-available;
}
.box {
}
main {
display: flex;
display: flex;
flex-wrap: nowrap;
/** height: -webkit-fill-available; **/
min-height: 100vh;
/*
*/
/* max-height: 100vh;
overflow-y: hidden;
*/
overflow-x: off;
}
.b-example-divider {
/* flex-shrink: 0;
height: 100%;
*/
width: calc(100% - 280px);
background-color: rgba(0, 0, 0, .1);
border: solid rgba(0, 0, 0, .15);
border-width: 1px 0;
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
padding-left: 10px;
padding-top: 10px;
padding-right: 10px;
padding-bottom: 10px;
/* padding: 0px; */
}
.bi {
vertical-align: -.125em;
pointer-events: none;
fill: currentColor;
}
.dropdown-toggle { outline: 0; }
.nav-flush .nav-link {
border-radius: 0;
}
.btn-toggle {
display: inline-flex;
align-items: center;
padding: .25rem .5rem;
font-weight: 600;
color: rgba(0, 0, 0, .65);
background-color: transparent;
border: 0;
}
.btn-toggle:hover,
.btn-toggle:focus {
color: rgba(0, 0, 0, .85);
background-color: #d2f4ea;
}
.btn-toggle::before {
width: 1.25em;
line-height: 0;
content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
transition: transform .35s ease;
transform-origin: .5em 50%;
}
.btn-toggle[aria-expanded="true"] {
color: rgba(0, 0, 0, .85);
}
.btn-toggle[aria-expanded="true"]::before {
transform: rotate(90deg);
}
.btn-toggle-nav a {
display: inline-flex;
padding: .1875rem .5rem;
margin-top: .125rem;
margin-left: 1.25rem;
text-decoration: none;
}
.btn-toggle-nav a:hover,
.btn-toggle-nav a:focus {
background-color: #d2f4ea;
}
.scrollarea {
overflow-y: auto;
}
.fw-semibold { font-weight: 600; }
.lh-tight { line-height: 1.25; }
/* global bootstrap: false */
(function () {
'use strict'
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl)
})
})()
.list-unstyled .btn{
text-align: left;''
background-color: rgba(0,0,0,0.05);
}
.list-unstyled .btn:hover{
background-color: rgba(0,0,0,0.25);
}
.traceback{
font-size: 12px;
line-height: .9em;
}
const socket = io.connect("/status"); // Status of the tests.
socket.on("connect", () => {
$("#status-connected").show(); // className = "badge rounded-pill bg-success"
$("#status-connecting").hide(); // className = "badge rounded-pill bg-success"
});
socket.on("disconnect", () => {
$("#status-connected").hide(); // className = "badge rounded-pill bg-success"
$("#status-connecting").show(); // className = "badge rounded-pill bg-success"
});
function re_run_all_tests(){
socket.emit("rerun_all", {});
for(test in terminals){
ui_set_state(test, 'running', {'stacktrace': 'Test is running'});
terminals[test][0].reset();
terminals[test][0].writeln('Rerunning test...');
}
}
function re_run_test(test){
socket.emit("rerun", {'test': test});
ui_set_state(test, 'running', {'stacktrace': 'Test is running'});
terminals[test][0].reset();
terminals[test][0].writeln('Rerunning test...');
}
function tests_and_state(){
/** This function update the token/test results. **/
}
function ui_set_token_state(){
/** React to a change in the .token file state **/
}
td_classes = {'fail': 'table-danger',
'pass': 'table-success',
'running': 'table-warning',
}
$("#token-blurb").hide();
socket.on("token_update", function(data){
console.log('> Updating token from remote...');
// console.log(data);
$("#token-blurb").show();
for(const e of $(".current_token_file")){
e.innerHTML = data.token;
}
for(k in data.results){
console.log(k);
console.log(data.results[k]);
state = data.results[k];
$("#tbl-"+k+"-token").removeClass();
$("#tbl-"+k+"-token").addClass(td_classes[state]);
$("#tbl-"+k+"-token")[0].querySelector("span").innerHTML = state;
}
});
var questions = {}
function ui_set_state(test_id, state, data){
/** Set the state of the test in the UI. Does not fire any events to the server. **/
state_emojis = {'fail': "bi bi-emoji-frown",
'pass': "bi bi-emoji-smile",
'running': 'spinner-border text-primary spinner-border-sm',
}
state_classes = {'fail': 'text-danger',
'pass': 'text-success',
'running': 'text-warning',
}
$("#tbl-"+test_id+"-title").removeClass();
$("#tbl-"+test_id+"-title").addClass(td_classes[state]);
$("#tbl-"+test_id+"-unit").removeClass();
$("#tbl-"+test_id+"-unit").addClass(td_classes[state]);
$("#tbl-"+test_id+"-unit")[0].querySelector("span").innerHTML = state;
for(const e of $("." + test_id + "-status")){
var icon = e.querySelector("#" + test_id + "-icon")
if (icon != null){
icon.setAttribute("class", state_emojis[state]);
}
var icon = e.querySelector("#" + test_id + "-status")
if (icon != null){
nc = state_classes[state]
if(data.coverage_files_changed != null){
nc = nc + " text-decoration-line-through";
}
icon.setAttribute("class", nc);
}
}
if (state == 'pass'){
$('#'+test_id+'-stacktrace').html('The test passed successfully!')
}
if(state == 'fail'){
js = " <script> $('.traceback').on('load', function() { console.log('STUFF'); do_call_doc_ready(); } ); </script>";
js = "";
if ( !(test_id in questions) ){
questions[test_id] = {'stacktrace': ''}
}
/** We are doing this to avoid adding the same event listener twice. If we do that, we will open/close the trace (if added an even number of times) **/
if (questions[test_id]['stacktrace'] == data.stacktrace){
// Do nothing.
}
else{
questions[test_id]['stacktrace'] = data.stacktrace;
$('#'+test_id+'-stacktrace').html(data.stacktrace+js).ready(
function(){
$('.'+test_id +"-traceback").ready( function() {
console.log('in 200ms, I will call: do_call_doc_ready("' + test_id+'")');
setTimeout(function(){
do_call_doc_ready(test_id)
}, 200);
});
});
}
}
}
// const status = document.getElementById("status");
/**
socket.of("/admin").on("state", function (data) {
console.log("new output received from server:", data.output);
term.write(data.output);
});
socket.on("update", function (data) {
console.log("new output received from server:", data.output);
term.write(data.output);
});
socket.on('test_update', function (data){
console.log('test got some new stuff');
});
function fitToscreen() {
//fit.fit();
const dims = { cols: term.cols, rows: term.rows };
console.log("sending new dimensions to server's pty", dims);
socket.emit("resize", dims);
}
**/
socket.on("testupdate", function(data){
console.log('> ', data.state, ': updating test with with id', data.id);
ui_set_state(data.id, data.state, data);
const targetNode = document.getElementById(''+data.id+'-stacktrace');
const callback = (mutationList, observer) => {
for (const mutation of mutationList) {
}
};
//console.log(data.stdout);
if(data.run_id != terminals[data.id][2]['run_id']){
terminals[data.id][0].reset();
// terminals[data.id][0].writeln('> Tes');
terminals[data.id][2]['run_id'] = data.run_id;
terminals[data.id][2]['last_chunk_id'] = -1;
}
if(data.stdout != null){
for (const o of data.stdout){
if (o[0] > terminals[data.id][2]['last_chunk_id']){
terminals[data.id][0].write(o[1]);
terminals[data.id][2]['last_chunk_id'] = o[0]
}
}
}
});
function debounce(func, wait_ms) {
let timeout;
return function (...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait_ms);
};
}
$("#status-connected").hide();
function reconnect(){
console.log("> Reconnected to server...");
socket.emit("reconnected", {'hello': 'world'});
// <span class="badge rounded-pill bg-success">Success</span>
// $('#status').innerHTML = '<span style="background-color: lightgreen;">connected tp tje server.</span>';
$("#status-connected").show(); // className = "badge rounded-pill bg-success"
$("#status-connecting").hide(); // className = "badge rounded-pill bg-success"
// $("#status").text('Connected')
// console.log("changed html");
/**
socket.on("connect", () => {
fitToscreen();
status.innerHTML =
'<span style="background-color: lightgreen;">connected</span>';
});
**/
}
const wait_ms = 50;
// window.onresize = debounce(fitToscreen, wait_ms);
//reconnect();
window.onload = debounce(reconnect, wait_ms);
/** This block of code is responsible for managing the terminals */
//console.log(terminals);
for (var key in terminals) {
const term = new Terminal({
rows: 22,
cursorBlink: true,
macOptionIsMeta: true,
scrollback: 5000,
disableStdin: true,
convertEol: true,
});
const fit = new FitAddon.FitAddon();
term.loadAddon(fit);
term.open(document.getElementById(key));
fit.fit();
term.writeln("Welcome back! Press the blue 'rerun' button above to run the test anew.")
terminals[key] = [term, fit, {'last_run_id': -1, 'last_chunk': 0}]; // Last item are the state details.
}
function fitToscreen() {
mpt = $("#main_page_tabs")[0]
for(k in terminals){
e = mpt.querySelector("#"+k + "-pane");
if ( e.classList.contains("active") ){
console.log("Fitting the terminal given by ", k)
terminals[k][1].fit();
}
}
}
window.onresize = debounce(fitToscreen, wait_ms);
$('button[data-toggle="tab"]').on('shown.bs.tab', function (e) {
for(key in terminals){
terminals[key][0].write(''); // This appears to refresh the terminal.
}
});
/** THIS CODE STORES THE CURRENTLY SELECTED TAB AND SELECTS IT AGAIN ON PAGE REFRESH **/
/* https://stackoverflow.com/questions/18999501/how-can-i-keep-selected-bootstrap-tab-on-page-refresh
$('#myTab button').click(function(e) {
e.preventDefault();
console.log("clicked")
$(this).tab('show');
});
// store the currently selected tab in the hash value
$('button[data-toggle="tab"]').on("shown.bs.tab", function(e) {
var id = $(e.target).attr("data-bs-target").substr(1);
console.log(id);
window.location.hash = id;
});
// on load of the page: switch to the currently selected tab
var hash = window.location.hash;
$('#myTab button[data-bs-target="' + hash + '"]').tab('show');
*/
\ No newline at end of file
function do_call_doc_ready(id){
docReady(() => {
if (!EVALEX_TRUSTED) {
initPinBox();
}
// if we are in console mode, show the console.
if (CONSOLE_MODE && EVALEX) {
createInteractiveConsole();
}
const frames = document.querySelectorAll("div."+id +"-traceback div.frame");
if (EVALEX) {
addConsoleIconToFrames(frames);
}
addEventListenersToElements(document.querySelectorAll("div.detail"), "click", () =>
document.querySelector("div."+id+"-traceback").scrollIntoView(false)
);
addToggleFrameTraceback(frames);
addToggleTraceTypesOnClick(document.querySelectorAll("h2.traceback"));
addInfoPrompt(document.querySelectorAll("span.nojavascript"));
console.log("Document is ready; setting traceback.")
});
}
function addToggleFrameTraceback(frames) {
frames.forEach((frame) => { // the problem may be the event listener is added multiple times...
console.log("Adding event listener to frame ", frame);
frame.addEventListener("click", () => {
console.log("Now the element has been clicked. " + frame + " " + frame.getElementsByTagName("pre")[0].parentElement);
frame.getElementsByTagName("pre")[0].parentElement.classList.toggle("expanded");
});
})
}
function wrapPlainTraceback() {
const plainTraceback = document.querySelector("div.plain textarea");
const wrapper = document.createElement("pre");
const textNode = document.createTextNode(plainTraceback.textContent);
wrapper.appendChild(textNode);
plainTraceback.replaceWith(wrapper);
}
function initPinBox() {
document.querySelector(".pin-prompt form").addEventListener(
"submit",
function (event) {
event.preventDefault();
const pin = encodeURIComponent(this.pin.value);
const encodedSecret = encodeURIComponent(SECRET);
const btn = this.btn;
btn.disabled = true;
fetch(
`${document.location.pathname}?__debugger__=yes&cmd=pinauth&pin=${pin}&s=${encodedSecret}`
)
.then((res) => res.json())
.then(({auth, exhausted}) => {
if (auth) {
EVALEX_TRUSTED = true;
fadeOut(document.getElementsByClassName("pin-prompt")[0]);
} else {
alert(
`Error: ${
exhausted
? "too many attempts. Restart server to retry."
: "incorrect pin"
}`
);
}
})
.catch((err) => {
alert("Error: Could not verify PIN. Network error?");
console.error(err);
})
.finally(() => (btn.disabled = false));
},
false
);
}
function promptForPin() {
if (!EVALEX_TRUSTED) {
const encodedSecret = encodeURIComponent(SECRET);
fetch(
`${document.location.pathname}?__debugger__=yes&cmd=printpin&s=${encodedSecret}`
);
const pinPrompt = document.getElementsByClassName("pin-prompt")[0];
fadeIn(pinPrompt);
document.querySelector('.pin-prompt input[name="pin"]').focus();
}
}
/**
* Helper function for shell initialization
*/
function openShell(consoleNode, target, frameID) {
promptForPin();
if (consoleNode) {
slideToggle(consoleNode);
return consoleNode;
}
let historyPos = 0;
const history = [""];
const consoleElement = createConsole();
const output = createConsoleOutput();
const form = createConsoleInputForm();
const command = createConsoleInput();
target.parentNode.appendChild(consoleElement);
consoleElement.append(output);
consoleElement.append(form);
form.append(command);
command.focus();
slideToggle(consoleElement);
form.addEventListener("submit", (e) => {
handleConsoleSubmit(e, command, frameID).then((consoleOutput) => {
output.append(consoleOutput);
command.focus();
consoleElement.scrollTo(0, consoleElement.scrollHeight);
const old = history.pop();
history.push(command.value);
if (typeof old !== "undefined") {
history.push(old);
}
historyPos = history.length - 1;
command.value = "";
});
});
command.addEventListener("keydown", (e) => {
if (e.key === "l" && e.ctrlKey) {
output.innerText = "--- screen cleared ---";
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
// Handle up arrow and down arrow.
if (e.key === "ArrowUp" && historyPos > 0) {
e.preventDefault();
historyPos--;
} else if (e.key === "ArrowDown" && historyPos < history.length - 1) {
historyPos++;
}
command.value = history[historyPos];
}
return false;
});
return consoleElement;
}
function addEventListenersToElements(elements, event, listener) {
elements.forEach((el) => el.addEventListener(event, listener));
}
/**
* Add extra info
*/
function addInfoPrompt(elements) {
for (let i = 0; i < elements.length; i++) {
elements[i].innerHTML =
"<p>To switch between the interactive traceback and the plaintext " +
'one, you can click on the "Traceback" headline. From the text ' +
"traceback you can also create a paste of it. " +
(!EVALEX
? ""
: "For code execution mouse-over the frame you want to debug and " +
"click on the console icon on the right side." +
"<p>You can execute arbitrary Python code in the stack frames and " +
"there are some extra helpers available for introspection:" +
"<ul><li><code>dump()</code> shows all variables in the frame" +
"<li><code>dump(obj)</code> dumps all that's known about the object</ul>");
elements[i].classList.remove("nojavascript");
}
}
function addConsoleIconToFrames(frames) {
for (let i = 0; i < frames.length; i++) {
let consoleNode = null;
const target = frames[i];
const frameID = frames[i].id.substring(6);
for (let j = 0; j < target.getElementsByTagName("pre").length; j++) {
const img = createIconForConsole();
img.addEventListener("click", (e) => {
e.stopPropagation();
consoleNode = openShell(consoleNode, target, frameID);
return false;
});
target.getElementsByTagName("pre")[j].append(img);
}
}
}
function slideToggle(target) {
target.classList.toggle("active");
}
/**
* toggle traceback types on click.
*/
function addToggleTraceTypesOnClick(elements) {
// logger.log("something..")
for (let i = 0; i < elements.length; i++) {
elements[i].addEventListener("click", () => {
document.querySelector("div.traceback").classList.toggle("hidden");
document.querySelector("div.plain").classList.toggle("hidden");
});
elements[i].style.cursor = "pointer";
document.querySelector("div.plain").classList.toggle("hidden");
}
}
function createConsole() {
const consoleNode = document.createElement("pre");
consoleNode.classList.add("console");
consoleNode.classList.add("active");
return consoleNode;
}
function createConsoleOutput() {
const output = document.createElement("div");
output.classList.add("output");
output.innerHTML = "[console ready]";
return output;
}
function createConsoleInputForm() {
const form = document.createElement("form");
form.innerHTML = "&gt;&gt;&gt; ";
return form;
}
function createConsoleInput() {
const command = document.createElement("input");
command.type = "text";
command.setAttribute("autocomplete", "off");
command.setAttribute("spellcheck", false);
command.setAttribute("autocapitalize", "off");
command.setAttribute("autocorrect", "off");
return command;
}
function createIconForConsole() {
const img = document.createElement("img");
img.setAttribute("src", "?__debugger__=yes&cmd=resource&f=console.png");
img.setAttribute("title", "Open an interactive python shell in this frame");
return img;
}
function createExpansionButtonForConsole() {
const expansionButton = document.createElement("a");
expansionButton.setAttribute("href", "#");
expansionButton.setAttribute("class", "toggle");
expansionButton.innerHTML = "&nbsp;&nbsp;";
return expansionButton;
}
function createInteractiveConsole() {
const target = document.querySelector("div.console div.inner");
while (target.firstChild) {
target.removeChild(target.firstChild);
}
openShell(null, target, 0);
}
function handleConsoleSubmit(e, command, frameID) {
// Prevent page from refreshing.
e.preventDefault();
return new Promise((resolve) => {
// Get input command.
const cmd = command.value;
// Setup GET request.
const urlPath = "";
const params = {
__debugger__: "yes",
cmd: cmd,
frm: frameID,
s: SECRET,
};
const paramString = Object.keys(params)
.map((key) => {
return "&" + encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
})
.join("");
fetch(urlPath + "?" + paramString)
.then((res) => {
return res.text();
})
.then((data) => {
const tmp = document.createElement("div");
tmp.innerHTML = data;
resolve(tmp);
// Handle expandable span for long list outputs.
// Example to test: list(range(13))
let wrapperAdded = false;
const wrapperSpan = document.createElement("span");
const expansionButton = createExpansionButtonForConsole();
tmp.querySelectorAll("span.extended").forEach((spanToWrap) => {
const parentDiv = spanToWrap.parentNode;
if (!wrapperAdded) {
parentDiv.insertBefore(wrapperSpan, spanToWrap);
wrapperAdded = true;
}
parentDiv.removeChild(spanToWrap);
wrapperSpan.append(spanToWrap);
spanToWrap.hidden = true;
expansionButton.addEventListener("click", () => {
spanToWrap.hidden = !spanToWrap.hidden;
expansionButton.classList.toggle("open");
return false;
});
});
// Add expansion button at end of wrapper.
if (wrapperAdded) {
wrapperSpan.append(expansionButton);
}
})
.catch((err) => {
console.error(err);
});
return false;
});
}
function fadeOut(element) {
element.style.opacity = 1;
(function fade() {
element.style.opacity -= 0.1;
if (element.style.opacity < 0) {
element.style.display = "none";
} else {
requestAnimationFrame(fade);
}
})();
}
function fadeIn(element, display) {
element.style.opacity = 0;
element.style.display = display || "block";
(function fade() {
let val = parseFloat(element.style.opacity) + 0.1;
if (val <= 1) {
element.style.opacity = val;
requestAnimationFrame(fade);
}
})();
}
function docReady(fn) {
if (document.readyState === "complete" || document.readyState === "interactive") {
setTimeout(fn, 1);
} else {
document.addEventListener("DOMContentLoaded", fn);
}
}
body, input { font-family: sans-serif; color: #000; text-align: center;
margin: 1em; padding: 0; font-size: 15px; }
h1, h2, h3 { font-weight: normal; }
input { background-color: #fff; margin: 0; text-align: left;
outline: none !important; }
input[type="submit"] { padding: 3px 6px; }
a { color: #11557C; }
a:hover { color: #177199; }
pre, code,
textarea { font-family: monospace; font-size: 14px; }
div.debugger { text-align: left; padding: 12px; margin: auto;
background-color: white; }
h1 { font-size: 36px; margin: 0 0 0.3em 0; }
div.detail { cursor: pointer; }
div.detail p { margin: 0 0 8px 13px; font-size: 14px; white-space: pre-wrap;
font-family: monospace; }
div.explanation { margin: 20px 13px; font-size: 15px; color: #555; }
div.footer { font-size: 13px; text-align: right; margin: 30px 0;
color: #86989B; }
h2 { font-size: 16px; margin: 1.3em 0 0.0 0; padding: 9px;
background-color: #11557C; color: white; }
h2 em, h3 em { font-style: normal; color: #A5D6D9; font-weight: normal; }
div.traceback, div.plain { border: 1px solid #ddd; margin: 0 0 1em 0; padding: 10px; }
div.plain p { margin: 0; }
div.plain textarea,
div.plain pre { margin: 10px 0 0 0; padding: 4px;
background-color: #E8EFF0; border: 1px solid #D3E7E9; }
div.plain textarea { width: 99%; height: 300px; }
div.traceback h3 { font-size: 1em; margin: 0 0 0.8em 0; }
div.traceback ul { list-style: none; margin: 0; padding: 0 0 0 1em; }
div.traceback h4 { font-size: 13px; font-weight: normal; margin: 0.7em 0 0.1em 0; }
div.traceback pre { margin: 0; padding: 5px 0 3px 15px;
background-color: #E8EFF0; border: 1px solid #D3E7E9; }
div.traceback .library .current { background: white; color: #555; }
div.traceback .expanded .current { background: #E8EFF0; color: black; }
div.traceback pre:hover { background-color: #DDECEE; color: black; cursor: pointer; }
div.traceback div.source.expanded pre + pre { border-top: none; }
div.traceback span.ws { display: none; }
div.traceback pre.before, div.traceback pre.after { display: none; background: white; }
div.traceback div.source.expanded pre.before,
div.traceback div.source.expanded pre.after {
display: block;
}
div.traceback div.source.expanded span.ws {
display: inline;
}
div.traceback blockquote { margin: 1em 0 0 0; padding: 0; white-space: pre-line; }
div.traceback img { float: right; padding: 2px; margin: -3px 2px 0 0; display: none; }
div.traceback img:hover { background-color: #ddd; cursor: pointer;
border-color: #BFDDE0; }
div.traceback pre:hover img { display: block; }
div.traceback cite.filename { font-style: normal; color: #3B666B; }
pre.console { border: 1px solid #ccc; background: white!important;
color: black; padding: 5px!important;
margin: 3px 0 0 0!important; cursor: default!important;
max-height: 400px; overflow: auto; }
pre.console form { color: #555; }
pre.console input { background-color: transparent; color: #555;
width: 90%; font-family: monospace; font-size: 14px;
border: none!important; }
span.string { color: #30799B; }
span.number { color: #9C1A1C; }
span.help { color: #3A7734; }
span.object { color: #485F6E; }
span.extended { opacity: 0.5; }
span.extended:hover { opacity: 1; }
a.toggle { text-decoration: none; background-repeat: no-repeat;
background-position: center center;
background-image: url(?__debugger__=yes&cmd=resource&f=more.png); }
a.toggle:hover { background-color: #444; }
a.open { background-image: url(?__debugger__=yes&cmd=resource&f=less.png); }
pre.console div.traceback,
pre.console div.box { margin: 5px 10px; white-space: normal;
border: 1px solid #11557C; padding: 10px;
font-family: sans-serif; }
pre.console div.box h3,
pre.console div.traceback h3 { margin: -10px -10px 10px -10px; padding: 5px;
background: #11557C; color: white; }
pre.console div.traceback pre:hover { cursor: default; background: #E8EFF0; }
pre.console div.traceback pre.syntaxerror { background: inherit; border: none;
margin: 20px -10px -10px -10px;
padding: 10px; border-top: 1px solid #BFDDE0;
background: #E8EFF0; }
pre.console div.noframe-traceback pre.syntaxerror { margin-top: -10px; border: none; }
pre.console div.box pre.repr { padding: 0; margin: 0; background-color: white; border: none; }
pre.console div.box table { margin-top: 6px; }
pre.console div.box pre { border: none; }
pre.console div.box pre.help { background-color: white; }
pre.console div.box pre.help:hover { cursor: default; }
pre.console table tr { vertical-align: top; }
div.console { border: 1px solid #ccc; padding: 4px; background-color: #fafafa; }
div.traceback pre, div.console pre {
white-space: pre-wrap; /* css-3 should we be so lucky... */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 ?? */
white-space: -o-pre-wrap; /* Opera 7 ?? */
word-wrap: break-word; /* Internet Explorer 5.5+ */
_white-space: pre; /* IE only hack to re-specify in
addition to word-wrap */
}
div.pin-prompt {
position: absolute;
display: none;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.8);
}
div.pin-prompt .inner {
background: #eee;
padding: 10px 50px;
width: 350px;
margin: 10% auto 0 auto;
border: 1px solid #ccc;
border-radius: 2px;
}
div.exc-divider {
margin: 0.7em 0 0 -1em;
padding: 0.5em;
background: #11557C;
color: #ddd;
border: 1px solid #ddd;
}
.console.active {
max-height: 0!important;
display: none;
}
.hidden {
display: none;
}
/**
body, input { font-family: sans-serif; color: #000; text-align: center;
margin: 1em; padding: 0; font-size: 15px; }
**/
h1, h2, h3 { font-weight: normal; }
input { background-color: #fff; margin: 0; text-align: left;
outline: none !important; }
input[type="submit"] { padding: 3px 6px; }
a { color: #11557C; }
a:hover { color: #177199; }
pre, code,
textarea { font-family: monospace; font-size: 14px; }
div.debugger { text-align: left; padding: 12px; margin: auto;
background-color: white; }
h1 { font-size: 36px; margin: 0 0 0.3em 0; }
div.detail { cursor: pointer; }
div.detail p { margin: 0 0 8px 13px; font-size: 14px; white-space: pre-wrap;
font-family: monospace; }
div.explanation { margin: 20px 13px; font-size: 15px; color: #555; }
div.footer { font-size: 13px; text-align: right; margin: 30px 0;
color: #86989B; }
h2 { font-size: 16px; margin: 1.3em 0 0.0 0; padding: 9px;
background-color: #11557C; color: white; }
h2 em, h3 em { font-style: normal; color: #A5D6D9; font-weight: normal; }
div.traceback, div.plain { border: 1px solid #ddd; margin: 0 0 1em 0; padding: 10px; }
div.plain p { margin: 0; }
div.plain textarea,
div.plain pre { margin: 10px 0 0 0; padding: 4px;
background-color: #E8EFF0; border: 1px solid #D3E7E9; }
div.plain textarea { width: 99%; height: 300px; }
div.traceback h3 { font-size: 1em; margin: 0 0 0.8em 0; }
div.traceback ul { list-style: none; margin: 0; padding: 0 0 0 1em; }
div.traceback h4 { font-size: 13px; font-weight: normal; margin: 0.7em 0 0.1em 0; }
div.traceback pre { margin: 0; padding: 5px 0 3px 15px;
background-color: #E8EFF0; border: 1px solid #D3E7E9; }
div.traceback .library .current { background: white; color: #555; }
div.traceback .expanded .current { background: #E8EFF0; color: black; }
div.traceback pre:hover { background-color: #DDECEE; color: black; cursor: pointer; }
div.traceback div.source.expanded pre + pre { border-top: none; }
div.traceback span.ws { display: none; }
div.traceback pre.before, div.traceback pre.after { display: none; background: white; }
div.traceback div.source.expanded pre.before,
div.traceback div.source.expanded pre.after {
display: block;
}
div.traceback div.source.expanded span.ws {
display: inline;
}
div.traceback blockquote { margin: 1em 0 0 0; padding: 0; white-space: pre-line; }
div.traceback img { float: right; padding: 2px; margin: -3px 2px 0 0; display: none; }
div.traceback img:hover { background-color: #ddd; cursor: pointer;
border-color: #BFDDE0; }
div.traceback pre:hover img { display: block; }
div.traceback cite.filename { font-style: normal; color: #3B666B; }
pre.console { border: 1px solid #ccc; background: white!important;
color: black; padding: 5px!important;
margin: 3px 0 0 0!important; cursor: default!important;
max-height: 400px; overflow: auto; }
pre.console form { color: #555; }
pre.console input { background-color: transparent; color: #555;
width: 90%; font-family: monospace; font-size: 14px;
border: none!important; }
span.string { color: #30799B; }
span.number { color: #9C1A1C; }
span.help { color: #3A7734; }
span.object { color: #485F6E; }
span.extended { opacity: 0.5; }
span.extended:hover { opacity: 1; }
a.toggle { text-decoration: none; background-repeat: no-repeat;
background-position: center center;
background-image: url(?__debugger__=yes&cmd=resource&f=more.png); }
a.toggle:hover { background-color: #444; }
a.open { background-image: url(?__debugger__=yes&cmd=resource&f=less.png); }
pre.console div.traceback,
pre.console div.box { margin: 5px 10px; white-space: normal;
border: 1px solid #11557C; padding: 10px;
font-family: sans-serif; }
pre.console div.box h3,
pre.console div.traceback h3 { margin: -10px -10px 10px -10px; padding: 5px;
background: #11557C; color: white; }
pre.console div.traceback pre:hover { cursor: default; background: #E8EFF0; }
pre.console div.traceback pre.syntaxerror { background: inherit; border: none;
margin: 20px -10px -10px -10px;
padding: 10px; border-top: 1px solid #BFDDE0;
background: #E8EFF0; }
pre.console div.noframe-traceback pre.syntaxerror { margin-top: -10px; border: none; }
pre.console div.box pre.repr { padding: 0; margin: 0; background-color: white; border: none; }
pre.console div.box table { margin-top: 6px; }
pre.console div.box pre { border: none; }
pre.console div.box pre.help { background-color: white; }
pre.console div.box pre.help:hover { cursor: default; }
pre.console table tr { vertical-align: top; }
div.console { border: 1px solid #ccc; padding: 4px; background-color: #fafafa; }
div.traceback pre, div.console pre {
white-space: pre-wrap; /* css-3 should we be so lucky... */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 ?? */
white-space: -o-pre-wrap; /* Opera 7 ?? */
word-wrap: break-word; /* Internet Explorer 5.5+ */
_white-space: pre; /* IE only hack to re-specify in
addition to word-wrap */
}
div.pin-prompt {
position: absolute;
display: none;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.8);
}
div.pin-prompt .inner {
background: #eee;
padding: 10px 50px;
width: 350px;
margin: 10% auto 0 auto;
border: 1px solid #ccc;
border-radius: 2px;
}
div.exc-divider {
margin: 0.7em 0 0 -1em;
padding: 0.5em;
background: #11557C;
color: #ddd;
border: 1px solid #ddd;
}
.console.active {
max-height: 0!important;
display: none;
}
.hidden {
display: none;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<meta name="description" content="">
<meta name="author" content="Tue Herlau">
<meta name="generator" content="Unitgrade">
<title>Unitgrade dashboard</title>
<link rel="canonical" href="https://getbootstrap.com/docs/5.0/examples/sidebars/">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
<script src="https://unpkg.com/xterm@4.11.0/lib/xterm.js"></script>
<script src="https://unpkg.com/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script>
<script src="https://unpkg.com/xterm-addon-web-links@0.4.0/lib/xterm-addon-web-links.js"></script>
<script src="https://unpkg.com/xterm-addon-search@0.8.0/lib/xterm-addon-search.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.3.1.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
<!-- terminal related -->
<link rel="stylesheet" href="https://unpkg.com/xterm@4.11.0/css/xterm.css"/>
<script src="https://unpkg.com/xterm@4.11.0/lib/xterm.js"></script>
<script src="https://unpkg.com/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script>
<script src="https://unpkg.com/xterm-addon-web-links@0.4.0/lib/xterm-addon-web-links.js"></script>
<script src="https://unpkg.com/xterm-addon-search@0.8.0/lib/xterm-addon-search.js"></script>
<!-- end terminal related -->
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
<link href="/static/sidebars.css" rel="stylesheet">
<link href="/static/wz_style_modified.css" rel="stylesheet">
<script>
var CONSOLE_MODE = false,
EVALEX = false, // console mode is possible.
EVALEX_TRUSTED = true,
SECRET = "Xbtn32ZR6AqRabFk2a3l";
</script>
<link href="/static/unitgrade.css" rel="stylesheet">
</head>
<body>
<main>
<div class="flex-shrink-0 p-3 bg-white" style="width: 280px; background-color: #f0f0f0">
<span role="tablist" aria-orientation="vertical">
<div class="d-flex flex-column" style="min-height: calc(100vh - 30px);">
<header>
<a href="/" class="d-flex align-items-center pb-1 mb-1 link-dark text-decoration-none">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi me-2" width="30" height="24" viewBox="0 0 16 16">
<path d="M8.211 2.047a.5.5 0 0 0-.422 0l-7.5 3.5a.5.5 0 0 0 .025.917l7.5 3a.5.5 0 0 0 .372 0L14 7.14V13a1 1 0 0 0-1 1v2h3v-2a1 1 0 0 0-1-1V6.739l.686-.275a.5.5 0 0 0 .025-.917l-7.5-3.5Z"/>
<path d="M4.176 9.032a.5.5 0 0 0-.656.327l-.5 1.7a.5.5 0 0 0 .294.605l4.5 1.8a.5.5 0 0 0 .372 0l4.5-1.8a.5.5 0 0 0 .294-.605l-.5-1.7a.5.5 0 0 0-.656-.327L8 10.466 4.176 9.032Z"/>
</svg>
<span class="fs-5 fw-semibold"> Unitgrade</span>&nbsp;
<span class="badge rounded-pill bg-success" id="status-connected">Connected</span>
<span class="badge rounded-pill bg-warning text-dark" id="status-connecting">Connecting</span>
</a>
<!-- Example single danger button -->
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle btn-sm" style="width: 100%" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
{{menu_name}}
</button>
<ul class="dropdown-menu " aria-labelledby="dropdownMenuButton1">
{% for a in available_reports.values() %}
<li><a class="dropdown-item" href="/report/{{a.menu_name}}">{{a.menu_name}}</a></li>
{% endfor %}
</ul>
</div>
</header>
<content>
<section>
{% block navigation %} {% endblock %}
</section>
</content>
<footer class="mt-auto">
{% block navigation_footer %}{% endblock %}
</footer>
</div>
</span>
</div>
<div class="b-example-divider">
{% block content %}{% endblock %}
</div> <!-- example divider ends -->
</main>
<!--
<script src="../assets/dist/js/bootstrap.bundle.min.js"></script> -->
<script src="/static/sidebars.js"></script>
<script src="/static/unitgrade.js"></script>
<script src="/static/wz_js.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
</head>
<body>
<h1>Hello, world!</h1>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
<div class="accordion" id="accordionPanelsStayOpenExample">
<div class="accordion-item">
<h2 class="accordion-header" id="panelsStayOpen-headingOne">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#panelsStayOpen-collapseOne" aria-expanded="true" aria-controls="panelsStayOpen-collapseOne">
Accordion Item #1 <span class="glyphicon glyphicon-star" aria-hidden="true"> das sdafsdf</span> Star
</button>
</h2>
<div id="panelsStayOpen-collapseOne" class="accordion-collapse collapse show" aria-labelledby="panelsStayOpen-headingOne">
<div class="accordion-body">
<strong>This is the first item's accordion body.</strong> It is shown by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow.
<div class="accordion" id="accordionPanelsStayOpenExample2">
<div class="accordion-item">
<h2 class="accordion-header" id="panelsStayOpen-headingOne2">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#panelsStayOpen-collapseOne2" aria-expanded="true" aria-controls="panelsStayOpen-collapseOne">
Accordion Item #1
</button>
</h2>
<div id="panelsStayOpen-collapseOne2" class="accordion-collapse collapse show" aria-labelledby="panelsStayOpen-headingOne">
<div class="accordion-body">
<strong>This is the first item's accordion body.</strong> It is shown by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="panelsStayOpen-headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#panelsStayOpen-collapseTwo" aria-expanded="false" aria-controls="panelsStayOpen-collapseTwo">
Accordion Item #2
</button>
</h2>
<div id="panelsStayOpen-collapseTwo" class="accordion-collapse collapse" aria-labelledby="panelsStayOpen-headingTwo">
<div class="accordion-body">
<strong>This is the second item's accordion body.</strong> It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow.
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="panelsStayOpen-headingThree">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#panelsStayOpen-collapseThree" aria-expanded="false" aria-controls="panelsStayOpen-collapseThree">
Accordion Item #3
</button>
</h2>
<div id="panelsStayOpen-collapseThree" class="accordion-collapse collapse" aria-labelledby="panelsStayOpen-headingThree">
<div class="accordion-body">
<strong>This is the third item's accordion body.</strong> It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow.
</div>
</div>
</div>
</div>
</body>
</html>
\ No newline at end of file
{% extends 'base.html' %}
{% macro build_question_body(hi) %}
{{hi}}
{% endmacro %}
{% block head %}
{% endblock %}
{% block content %}
<div class="tab-content container-fluid" id="main_page_tabs">
<script>
var terminals = {};
</script>
{% set count=0 %}
{% for qkey, qbody in questions.items() %}
{% set outer_loop = loop %}
{% for ikey, ibody in qbody.tests.items() %}
<script>terminals["{{ikey}}"] = null; </script>
<div class="tab-pane fade {{ 'show active' if outer_loop.index == 1 and loop.index == 1 else ''}}" id="{{ikey}}-pane" role="tabpanel" aria-labelledby="{{ikey}}-pane-tab">
<!-- begin tab card -->
<h1>{{qbody.title}}</h1>
<h4>
<span class="{{ikey}}-status">
<span id="{{ikey}}-status"><i id="{{ikey}}-icon" class="bi bi-emoji-neutral"></i> <span class="text-left">{{ibody.title}}</span></span>
</span>
<a onclick="re_run_test('{{ikey}}');" type="button" class="btn btn-primary">Rerun</a>
</h4>
<div class="card shadow mb-3 bg-white rounded">
<div class="card-header">
Terminal Output
</div>
<div class="card-body">
<div style="padding-left: 5px; padding-top: 5px; background-color: black;">
<div style="width: 100%; height: 20%;" id="{{ikey}}"></div>
</div>
<p class="card-text">
</p>
</div>
</div>
<div class="row">
<div class="col-sm-8">
<div class="card shadow mb-5 bg-white rounded">
<div class="card-header">Test outcome</div>
<div class="card-body">
<div id="{{ikey}}-stacktrace">{{ibody.wz if ibody.wz else 'Your results will be shown here'}}</div>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="card shadow mb-5 bg-white rounded">
<div class="card-header"> Hints </div>
<div class="card-body">
<dl>
{% for h in ibody.hints %}
<dt>{% if not h[1] %} Overall hints: {% else %} From the file <emph>{{ h[1] }}</emph> {% endif %}</dt>
<dd>
<ul>
{% for hitem in h[0] %}
<li>{{hitem}}</li>
{% endfor %}
</ul>
</dd>
{% endfor %}
</dl>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
{% endfor %}
<div class="tab-pane fade" id="token-pane" role="tabpanel" aria-labelledby="token-pane-tab">
<div class="row">
<div class="col-sm-2"> </div>
<div class="col-sm-8">
<div class="card shadow mb-5 bg-white rounded">
<div class="card-header">Your submission</div>
<div class="card-body">
<span id="#token-blurb">The following results are based on: <code><span class="current_token_file">no file here. </span></code> </span>
<table class="table table-hover" style="td-height: 10px;">
<!--
<thead>
<tr>
<td style="border: none;"></td>
<td style="border: none;">Unittests result</td>
<td style="border: none;"><code>.token</code>-file result</td>
</tr>
</thead>
-->
{% for qkey, qbody in questions.items() %}
<!--
<h6> {{qbody.title}}</h6>
-->
<tr>
{% if loop.index == 1 %}
<td><strong>{{qbody.title}}</strong></td>
<td>Unittests result</td>
<td><code>.token</code>-file result</td>
{% else %}
<td colspan="3"><strong>{{qbody.title}}</strong></td>
{% endif %}
</tr>
{% for ikey, ibody in qbody.tests.items() %}
<tr style="line-height: 10px; height: 10px;">
<td id="tbl-{{ikey}}-title">{{ibody.title}}</td>
<td id="tbl-{{ikey}}-unit"><span class="test-state">Test has not been run</span></td>
<td id="tbl-{{ikey}}-token"><span class="test-state">Test has not been run</span></td>
</tr>
<!--
<div class="tab-pane fade" id="{{ikey}}-pane" role="tabpanel" aria-labelledby="{{ikey}}-pane-tab">
-->
<!-- begin tab card -->
<!--
<h1>{{qbody.title}}</h1>-->
{% endfor %}
</table>
{% endfor %}
</div></div>
<div class="card shadow mb-5 bg-white rounded">
<div class="card-header">Hand-in instructions:</div>
<div class="card-body">
<p>
To hand in your results, you should run the file <code>{{grade_script}}</code>. You can either do this from your IDE, or by going to the directory:
<pre>
<code>{{root_dir}}</code>
</pre>
and from there run the command:<pre><code>
{{run_cmd_grade}}</code>
</pre>
This will generate a <code>.token</code> file which contains your answers and you should upload to DTU learn.
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
<!---------------------------------- NAVIGATION SECTION -------------------->
{% block navigation %}
<ul class="list-unstyled ps-0">
{% for qkey, qbody in questions.items() %}
{% set outer_loop = loop %}
<li class="mb-1">
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#{{qkey}}-collapse" aria-expanded="true" >
{{qbody.title}} </button>
<div class="collapse show" id="{{qkey}}-collapse">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small" id="myTab">
{% for ikey, ibody in qbody.tests.items() %}
<li>
<div class="container" style="">
<div class="row" style="background-color: white;">
<div class="col col-lg-11 text-truncate" style="background-color: white;">
<button class="btn rounded collapsed nav-link {{ 'active' if outer_loop.index == 1 and loop.index == 1 else ''}} text-left" style="width: 100%;" id="{{ikey}}-pane-tab" data-bs-toggle="pill" data-bs-target="#{{ikey}}-pane" type="button" role="tab" aria-controls="{{ikey}}-pane" aria-selected="false" data-toggle="tab">
<span class="{{ikey}}-status">
<span id="{{ikey}}-status"><i style="height: 16px;width: 16px; id="{{ikey}}-icon" class="bi bi-emoji-neutral"></i> <span class="text-left">{{ibody.title}}</span></span>
</span>
</button>
</div>
<div class="col col-lg-auto" style="padding: 0px; backgrund-color: white;">
<a onclick="re_run_test('{{ikey}}');" type="button" class="btn btn-primary btn-sm" style="padding: 0px; margin: 0px;"><i class="bi bi-arrow-clockwise"></i></a>
</div>
</div>
</div>
<!--
<button class="nav-link" id="v-pills-settings-tab" data-bs-toggle="pill" data-bs-target="#v-pills-settings" type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false">Settings</button>
-->
</li>
{% endfor %} <!--
<li><a href="#" class="link-dark rounded">Updates</a></li>
<li><a href="#" class="link-dark rounded">Reports</a></li> -->
</ul>
</div>
</li>
{% endfor %}
</ul>
<hr/>
{% endblock %}
{% block navigation_footer %}
<div class="btn-group-vertical" style="width: 100%">
<a onclick="re_run_all_tests();" class="btn btn-primary btn-sm" style="width: 100%;" type="button">Rerun all tests</a>
<button class="btn btn-success btn-sm" style="width: 100%;" id="token-pane-tab" data-bs-toggle="pill" data-bs-target="#token-pane" type="button" role="tab" aria-controls="token-pane" aria-selected="false" data-toggle="tab">Submit results</button>
</div>
<!--
<p>
<a onclick="re_run_all_tests();" class="btn btn-primary btn-sm" style="width: 100%;" type="button">
Rerun all tests
</a></p>
<p>
<button class="btn btn-success btn-sm" style="width: 100%;" id="token-pane-tab" data-bs-toggle="pill" data-bs-target="#token-pane" type="button" role="tab" aria-controls="token-pane" aria-selected="false" data-toggle="tab">
Submit results
</button>
</p>-->
{% endblock %}
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Unitgrade Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
<script src="https://unpkg.com/xterm@4.11.0/lib/xterm.js"></script>
<script src="https://unpkg.com/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script>
<script src="https://unpkg.com/xterm-addon-web-links@0.4.0/lib/xterm-addon-web-links.js"></script>
<script src="https://unpkg.com/xterm-addon-search@0.8.0/lib/xterm-addon-search.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.3.1.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
</head>
<body>
{% block head %} {% endblock %}
{% block content %}
{% endblock %}
</body>
</html>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment