diff --git a/devel/example_devel/instructor/cs108/db.pkl b/devel/example_devel/instructor/cs108/db.pkl new file mode 100644 index 0000000000000000000000000000000000000000..d5e7f185526f6f1e94e0a707668bef186d84fad8 Binary files /dev/null and b/devel/example_devel/instructor/cs108/db.pkl differ diff --git a/devel/example_devel/instructor/cs108/deploy.py b/devel/example_devel/instructor/cs108/deploy.py index da92995cd2d5c11a34d096258b3c7ce4fdbda6c3..81ba32b57cd5c440582c966c47cce96241939350 100644 --- a/devel/example_devel/instructor/cs108/deploy.py +++ b/devel/example_devel/instructor/cs108/deploy.py @@ -1,7 +1,12 @@ -from cs108.report_devel import Report2 +from cs108.report_devel import Report2, mk_ok from unitgrade_private.hidden_create_files import setup_grade_file_report from snipper.snip_dir import snip_dir +def main(with_coverage=True): + mk_ok() + setup_grade_file_report(Report2, with_coverage=with_coverage, minify=False, obfuscate=False,bzip=False) + + if __name__ == "__main__": # import pickle # with open("unitgrade_data/Week1.pkl", 'rb') as f: @@ -15,5 +20,6 @@ if __name__ == "__main__": # data, txt = load_token("Report2_handin_38_of_38.token") # print(data['details'][1]['items'] ) - setup_grade_file_report(Report2, with_coverage=True) - snip_dir("./", "../../students/cs108", clean_destination_dir=True, exclude=['*.token', 'deploy.py']) + # None of that coverage shit. + # snip_dir("./", "../../students/cs108", clean_destination_dir=True, exclude=['*.token', 'deploy.py']) + main() diff --git a/devel/example_devel/instructor/cs108/report_devel.py b/devel/example_devel/instructor/cs108/report_devel.py index 79c4ba414ff0ef6386e844b0e8df8ad5c02de262..9da58ef01e05a8a0e531eaf4a4e2fbbbc64ac6fc 100644 --- a/devel/example_devel/instructor/cs108/report_devel.py +++ b/devel/example_devel/instructor/cs108/report_devel.py @@ -4,20 +4,71 @@ from cs108.homework1 import add, reverse_list, linear_regression_weights, linear from unitgrade import UTestCase, cache import time import numpy as np +import pickle +import os + + +def mk_bad(): + with open(os.path.dirname(__file__)+"/db.pkl", 'wb') as f: + d = {'x1': 100, 'x2': 300} + pickle.dump(d, f) + +def mk_ok(): + with open(os.path.dirname(__file__)+"/db.pkl", 'wb') as f: + d = {'x1': 1, 'x2': 2} + pickle.dump(d, f) class Numpy(UTestCase): + @classmethod + def setUpClass(cls) -> None: + print("Set up.") # must be handled seperately. + # raise Exception("bad set up class") + + + def test_bad(self): + """ + Hints: + * Remember to properly de-indent your code. + * Do more stuff which works. + """ + # raise Exception("This ended poorly") + # print("Here we go") + # return + # self.assertEqual(1, 1) + with open(os.path.dirname(__file__)+"/db.pkl", 'rb') as f: + # d = {'x1': 1, 'x2': 2} + # pickle.dump(d, f) + d = pickle.load(f) + # print(d) + # assert False + for i in range(10): + print("The current number is", i) + # time.sleep(1) + self.assertEqual(1, d['x1']) + + # assert False + pass + def test_weights(self): """ Hints: * Try harder! + * Check the chapter on linear regression. """ n = 3 m = 2 np.random.seed(5) - X = np.random.randn(n, m) - y = np.random.randn(n) + # from numpy import asdfaskdfj + # X = np.random.randn(n, m) + # y = np.random.randn(n) foo() - self.assertL2(linear_regression_weights(X, y), msg="the message") + # assert 2 == 3 + # raise Exception("Bad exit") + # self.assertEqual(2, np.random.randint(1000)) + # self.assertEqual(2, np.random.randint(1000)) + # self.assertL2(linear_regression_weights(X, y), msg="the message") + self.assertEqual(1, 1) + # self.assertEqual(1,2) return "THE RESULT OF THE TEST" @@ -30,4 +81,8 @@ class Report2(Report): pack_imports = [cs108] if __name__ == "__main__": - evaluate_report_student(Report2()) + # import texttestrunner + import unittest + unittest.main(exit=False) + + # evaluate_report_student(Report2()) diff --git a/devel/example_devel/instructor/cs108/report_devel_grade.py b/devel/example_devel/instructor/cs108/report_devel_grade.py index 735cf35ae671ff6977b1bd7df8438cb3b695de50..ba74cb097701d32063d19a8563095cc034d5a5ec 100644 --- a/devel/example_devel/instructor/cs108/report_devel_grade.py +++ b/devel/example_devel/instructor/cs108/report_devel_grade.py @@ -1,3 +1,537 @@ -''' WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt. ''' -import bz2, base64 -exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file +# cs108/report_devel.py + +import hashlib +import io +import tokenize +import numpy as np +from tabulate import tabulate +from datetime import datetime +import pyfiglet +import unittest +import inspect +import os +import argparse +import time + +parser = argparse.ArgumentParser(description='Evaluate your report.', epilog="""Example: +To run all tests in a report: + +> python assignment1_dp.py + +To run only question 2 or question 2.1 + +> python assignment1_dp.py -q 2 +> python assignment1_dp.py -q 2.1 + +Note this scripts does not grade your report. To grade your report, use: + +> python report1_grade.py + +Finally, note that if your report is part of a module (package), and the report script requires part of that package, the -m option for python may be useful. +For instance, if the report file is in Documents/course_package/report3_complete.py, and `course_package` is a python package, then change directory to 'Documents/` and run: + +> python -m course_package.report1 + +see https://docs.python.org/3.9/using/cmdline.html +""", formatter_class=argparse.RawTextHelpFormatter) +parser.add_argument('-q', nargs='?', type=str, default=None, help='Only evaluate this question (e.g.: -q 2)') +parser.add_argument('--showexpected', action="store_true", help='Show the expected/desired result') +parser.add_argument('--showcomputed', action="store_true", help='Show the answer your code computes') +parser.add_argument('--unmute', action="store_true", help='Show result of print(...) commands in code') +parser.add_argument('--passall', action="store_true", help='Automatically pass all tests. Useful when debugging.') +parser.add_argument('--noprogress', action="store_true", help='Disable progress bars.') + +def evaluate_report_student(report, question=None, qitem=None, unmute=None, passall=None, ignore_missing_file=False, show_tol_err=False, show_privisional=True, noprogress=None): + args = parser.parse_args() + if noprogress is None: + noprogress = args.noprogress + + if question is None and args.q is not None: + question = args.q + if "." in question: + question, qitem = [int(v) for v in question.split(".")] + else: + question = int(question) + + if hasattr(report, "computed_answer_file") and not os.path.isfile(report.computed_answers_file) and not ignore_missing_file: + raise Exception("> Error: The pre-computed answer file", os.path.abspath(report.computed_answers_file), "does not exist. Check your package installation") + + if unmute is None: + unmute = args.unmute + if passall is None: + passall = args.passall + + results, table_data = evaluate_report(report, question=question, show_progress_bar=not unmute and not noprogress, qitem=qitem, + verbose=False, passall=passall, show_expected=args.showexpected, show_computed=args.showcomputed,unmute=unmute, + show_tol_err=show_tol_err) + + + if question is None and show_privisional: + print("Provisional evaluation") + tabulate(table_data) + table = table_data + print(tabulate(table)) + print(" ") + + fr = inspect.getouterframes(inspect.currentframe())[1].filename + gfile = os.path.basename(fr)[:-3] + "_grade.py" + if os.path.exists(gfile): + print("Note your results have not yet been registered. \nTo register your results, please run the file:") + print(">>>", gfile) + print("In the same manner as you ran this file.") + + + return results + + +def upack(q): + # h = zip([(i['w'], i['possible'], i['obtained']) for i in q.values()]) + h =[(i['w'], i['possible'], i['obtained']) for i in q.values()] + h = np.asarray(h) + return h[:,0], h[:,1], h[:,2], + +class SequentialTestLoader(unittest.TestLoader): + def getTestCaseNames(self, testCaseClass): + test_names = super().getTestCaseNames(testCaseClass) + # testcase_methods = list(testCaseClass.__dict__.keys()) + ls = [] + for C in testCaseClass.mro(): + if issubclass(C, unittest.TestCase): + ls = list(C.__dict__.keys()) + ls + testcase_methods = ls + test_names.sort(key=testcase_methods.index) + return test_names + +def evaluate_report(report, question=None, qitem=None, passall=False, verbose=False, show_expected=False, show_computed=False,unmute=False, show_help_flag=True, silent=False, + show_progress_bar=True, + show_tol_err=False, + big_header=True): + + now = datetime.now() + if big_header: + ascii_banner = pyfiglet.figlet_format("UnitGrade", font="doom") + b = "\n".join( [l for l in ascii_banner.splitlines() if len(l.strip()) > 0] ) + else: + b = "Unitgrade" + dt_string = now.strftime("%d/%m/%Y %H:%M:%S") + print(b + " v" + __version__ + ", started: " + dt_string+ "\n") + # print("Started: " + dt_string) + report._check_remote_versions() # Check (if report.url is present) that remote files exist and are in sync. + s = report.title + if hasattr(report, "version") and report.version is not None: + s += f" version {report.version}" + print(s, "(use --help for options)" if show_help_flag else "") + # print(f"Loaded answers from: ", report.computed_answers_file, "\n") + table_data = [] + t_start = time.time() + score = {} + loader = SequentialTestLoader() + + for n, (q, w) in enumerate(report.questions): + if question is not None and n+1 != question: + continue + suite = loader.loadTestsFromTestCase(q) + qtitle = q.question_title() if hasattr(q, 'question_title') else q.__qualname__ + if not report.abbreviate_questions: + q_title_print = "Question %i: %s"%(n+1, qtitle) + else: + q_title_print = "q%i) %s" % (n + 1, qtitle) + + print(q_title_print, end="") + q.possible = 0 + q.obtained = 0 + # q_ = {} # Gather score in this class. + UTextResult.q_title_print = q_title_print # Hacky + UTextResult.show_progress_bar = show_progress_bar # Hacky. + UTextResult.number = n + UTextResult.nL = report.nL + UTextResult.unmute = unmute # Hacky as well. + UTextResult.setUpClass_time = q._cache.get(((q.__name__, 'setUpClass'), 'time'), 3) if hasattr(q, '_cache') and q._cache is not None else 3 + + + res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite) + details = {} + for s, msg in res.successes + res.failures + res.errors: + # from unittest.suite import _ErrorHolder + # from unittest import _Err + # if isinstance(s, _ErrorHolder) + if hasattr(s, '_testMethodName'): + key = (q.__name__, s._testMethodName) + else: + # In case s is an _ErrorHolder (unittest.suite) + key = (q.__name__, s.id()) + # key = (q.__name__, s._testMethodName) # cannot use the cache_id method bc. it is not compatible with plain unittest. + + detail = {} + if (s,msg) in res.successes: + detail['status'] = "pass" + elif (s,msg) in res.failures: + detail['status'] = 'fail' + elif (s,msg) in res.errors: + detail['status'] = 'error' + else: + raise Exception("Status not known.") + + nice_title = s.title + detail = {**detail, **msg, 'nice_title': nice_title}#['message'] = msg + details[key] = detail + + # q_[s._testMethodName] = ("pass", None) + # for (s,msg) in res.failures: + # q_[s._testMethodName] = ("fail", msg) + # for (s,msg) in res.errors: + # q_[s._testMethodName] = ("error", msg) + # res.successes[0]._get_outcome() + + possible = res.testsRun + obtained = len(res.successes) + + # assert len(res.successes) + len(res.errors) + len(res.failures) == res.testsRun + + obtained = int(w * obtained * 1.0 / possible ) if possible > 0 else 0 + score[n] = {'w': w, 'possible': w, 'obtained': obtained, 'items': details, 'title': qtitle, 'name': q.__name__, + } + q.obtained = obtained + q.possible = possible + # print(q._cache) + # print(q._covcache) + s1 = f" * q{n+1}) Total" + s2 = f" {q.obtained}/{w}" + print(s1 + ("."* (report.nL-len(s1)-len(s2) )) + s2 ) + print(" ") + table_data.append([f"q{n+1}) Total", f"{q.obtained}/{w}"]) + + ws, possible, obtained = upack(score) + possible = int( msum(possible) ) + obtained = int( msum(obtained) ) # Cast to python int + report.possible = possible + report.obtained = obtained + now = datetime.now() + dt_string = now.strftime("%H:%M:%S") + + dt = int(time.time()-t_start) + minutes = dt//60 + seconds = dt - minutes*60 + plrl = lambda i, s: str(i) + " " + s + ("s" if i != 1 else "") + + dprint(first = "Total points at "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +")", + last=""+str(report.obtained)+"/"+str(report.possible), nL = report.nL) + + # print(f"Completed at "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +"). Total") + + table_data.append(["Total", ""+str(report.obtained)+"/"+str(report.possible) ]) + results = {'total': (obtained, possible), 'details': score} + return results, table_data + + +def python_code_str_id(python_code, strip_comments_and_docstring=True): + s = python_code + + if strip_comments_and_docstring: + try: + s = remove_comments_and_docstrings(s) + except Exception as e: + print("--"*10) + print(python_code) + print(e) + + s = "".join([c.strip() for c in s.split()]) + hash_object = hashlib.blake2b(s.encode()) + return hash_object.hexdigest() + + +def file_id(file, strip_comments_and_docstring=True): + with open(file, 'r') as f: + # s = f.read() + return python_code_str_id(f.read()) + + +def remove_comments_and_docstrings(source): + """ + Returns 'source' minus comments and docstrings. + """ + io_obj = io.StringIO(source) + out = "" + prev_toktype = tokenize.INDENT + last_lineno = -1 + last_col = 0 + for tok in tokenize.generate_tokens(io_obj.readline): + token_type = tok[0] + token_string = tok[1] + start_line, start_col = tok[2] + end_line, end_col = tok[3] + ltext = tok[4] + # The following two conditionals preserve indentation. + # This is necessary because we're not using tokenize.untokenize() + # (because it spits out code with copious amounts of oddly-placed + # whitespace). + if start_line > last_lineno: + last_col = 0 + if start_col > last_col: + out += (" " * (start_col - last_col)) + # Remove comments: + if token_type == tokenize.COMMENT: + pass + # This series of conditionals removes docstrings: + elif token_type == tokenize.STRING: + if prev_toktype != tokenize.INDENT: + # This is likely a docstring; double-check we're not inside an operator: + if prev_toktype != tokenize.NEWLINE: + # Note regarding NEWLINE vs NL: The tokenize module + # differentiates between newlines that start a new statement + # and newlines inside of operators such as parens, brackes, + # and curly braces. Newlines inside of operators are + # NEWLINE and newlines that start new code are NL. + # Catch whole-module docstrings: + if start_col > 0: + # Unlabelled indentation means we're inside an operator + out += token_string + # Note regarding the INDENT token: The tokenize module does + # not label indentation inside of an operator (parens, + # brackets, and curly braces) as actual indentation. + # For example: + # def foo(): + # "The spaces before this docstring are tokenize.INDENT" + # test = [ + # "The spaces before this string do not get a token" + # ] + else: + out += token_string + prev_toktype = token_type + last_col = end_col + last_lineno = end_line + return out + +import lzma +import base64 +import textwrap +import hashlib +import bz2 +import pickle +import os +import zipfile +import io + + +def bzwrite(json_str, token): # to get around obfuscation issues + with getattr(bz2, 'open')(token, "wt") as f: + f.write(json_str) + + +def gather_imports(imp): + resources = {} + m = imp + f = m.__file__ + if hasattr(m, '__file__') and not hasattr(m, '__path__'): + top_package = os.path.dirname(m.__file__) + module_import = True + else: + im = __import__(m.__name__.split('.')[0]) + if isinstance(im, list): + print("im is a list") + print(im) + # the __path__ attribute *may* be a string in some cases. I had to fix this. + print("path.:", __import__(m.__name__.split('.')[0]).__path__) + # top_package = __import__(m.__name__.split('.')[0]).__path__._path[0] + top_package = __import__(m.__name__.split('.')[0]).__path__[0] + module_import = False + + found_hashes = {} + # pycode = {} + resources['pycode'] = {} + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, 'w') as zip: + for root, dirs, files in os.walk(top_package): + for file in files: + if file.endswith(".py"): + fpath = os.path.join(root, file) + v = os.path.relpath(fpath, os.path.dirname(top_package) if not module_import else top_package) + zip.write(fpath, v) + if not fpath.endswith("_grade.py"): # Exclude grade files. + with open(fpath, 'r') as f: + s = f.read() + found_hashes[v] = python_code_str_id(s) + resources['pycode'][v] = s + + resources['zipfile'] = zip_buffer.getvalue() + resources['top_package'] = top_package + resources['module_import'] = module_import + resources['blake2b_file_hashes'] = found_hashes + return resources, top_package + + +import argparse +parser = argparse.ArgumentParser(description='Evaluate your report.', epilog="""Use this script to get the score of your report. Example: + +> python report1_grade.py + +Finally, note that if your report is part of a module (package), and the report script requires part of that package, the -m option for python may be useful. +For instance, if the report file is in Documents/course_package/report3_complete.py, and `course_package` is a python package, then change directory to 'Documents/` and run: + +> python -m course_package.report1 + +see https://docs.python.org/3.9/using/cmdline.html +""", formatter_class=argparse.RawTextHelpFormatter) +parser.add_argument('--noprogress', action="store_true", help='Disable progress bars') +parser.add_argument('--autolab', action="store_true", help='Show Autolab results') + +def gather_report_source_include(report): + sources = {} + # print("") + # if not args.autolab: + if len(report.individual_imports) > 0: + print("By uploading the .token file, you verify the files:") + for m in report.individual_imports: + print(">", m.__file__) + print("Are created/modified individually by you in agreement with DTUs exam rules") + report.pack_imports += report.individual_imports + + if len(report.pack_imports) > 0: + print("Including files in upload...") + for k, m in enumerate(report.pack_imports): + nimp, top_package = gather_imports(m) + _, report_relative_location, module_import = report._import_base_relative() + + nimp['report_relative_location'] = report_relative_location + nimp['report_module_specification'] = module_import + nimp['name'] = m.__name__ + sources[k] = nimp + print(f" * {m.__name__}") + return sources + +# def report_script_relative_location(report): +# """ +# Given the grade script corresponding to the 'report', work out it's relative location either compared to the +# package it is in or directory. +# """ +# if len(report.individual_imports) == 0: +# return "./" +# else: +# +# pass + +def gather_upload_to_campusnet(report, output_dir=None, token_include_plaintext_source=False): + # n = report.nL + args = parser.parse_args() + results, table_data = evaluate_report(report, show_help_flag=False, show_expected=False, show_computed=False, silent=True, + show_progress_bar=not args.noprogress, + big_header=not args.autolab, + ) + print("") + sources = {} + if not args.autolab: + results['sources'] = sources = gather_report_source_include(report) + + token_plain = """ +# This file contains your results. Do not edit its content. Simply upload it as it is. """ + + s_include = [token_plain] + known_hashes = [] + cov_files = [] + use_coverage = True + if report._config is not None: + known_hashes = report._config['blake2b_file_hashes'] + for Q, _ in report.questions: + use_coverage = use_coverage and isinstance(Q, UTestCase) + for key in Q._cache: + if len(key) >= 2 and key[1] == "coverage": + for f in Q._cache[key]: + cov_files.append(f) + + for s in sources.values(): + for f_rel, hash in s['blake2b_file_hashes'].items(): + if hash in known_hashes and f_rel not in cov_files and use_coverage: + print("Skipping", f_rel) + else: + if token_include_plaintext_source: + s_include.append("#"*3 +" Content of " + f_rel +" " + "#"*3) + s_include.append("") + s_include.append(s['pycode'][f_rel]) + s_include.append("") + + if output_dir is None: + output_dir = os.getcwd() + + payload_out_base = report.__class__.__name__ + "_handin" + + obtain, possible = results['total'] + vstring = f"_v{report.version}" if report.version is not None else "" + token = "%s_%i_of_%i%s.token"%(payload_out_base, obtain, possible,vstring) + token = os.path.normpath(os.path.join(output_dir, token)) + + save_token(results, "\n".join(s_include), token) + + if not args.autolab: + print("> Testing token file integrity...", sep="") + load_token(token) + print("Done!") + print(" ") + print("To get credit for your results, please upload the single unmodified file: ") + print(">", token) + + + +def dict2picklestring(dd): + b = lzma.compress(pickle.dumps(dd)) + b_hash = hashlib.blake2b(b).hexdigest() + return base64.b64encode(b).decode("utf-8"), b_hash + +def picklestring2dict(picklestr): + b = base64.b64decode(picklestr) + hash = hashlib.blake2b(b).hexdigest() + dictionary = pickle.loads(lzma.decompress(b)) + return dictionary, hash + + +token_sep = "-"*70 + " ..ooO0Ooo.. " + "-"*70 +def save_token(dictionary, plain_text, file_out): + if plain_text is None: + plain_text = "" + if len(plain_text) == 0: + plain_text = "Start token file" + plain_text = plain_text.strip() + b, b_hash = dict2picklestring(dictionary) + b_l1 = len(b) + b = "."+b+"." + b = "\n".join( textwrap.wrap(b, 180)) + + out = [plain_text, token_sep, f"{b_hash} {b_l1}", token_sep, b] + with open(file_out, 'w') as f: + f.write("\n".join(out)) + +def load_token(file_in): + with open(file_in, 'r') as f: + s = f.read() + splt = s.split(token_sep) + data = splt[-1] + info = splt[-2] + head = token_sep.join(splt[:-2]) + plain_text=head.strip() + hash, l1 = info.split(" ") + data = "".join( data.strip()[1:-1].splitlines() ) + l1 = int(l1) + + dictionary, b_hash = picklestring2dict(data) + + assert len(data) == l1 + assert b_hash == hash.strip() + return dictionary, plain_text + + +def source_instantiate(name, report1_source, payload): + # print("Executing sources", report1_source) + eval("exec")(report1_source, globals()) + # print("Loaind gpayload..") + pl = pickle.loads(bytes.fromhex(payload)) + report = eval(name)(payload=pl, strict=True) + return report + + + +report1_source = '# from unitgrade import hide\n# from unitgrade import utils\n# import os\n# import lzma\n# import pickle\n\n# DONT\'t import stuff here since install script requires __version__\n\n# def cache_write(object, file_name, verbose=True):\n# # raise Exception("bad")\n# # import compress_pickle\n# dn = os.path.dirname(file_name)\n# if not os.path.exists(dn):\n# os.mkdir(dn)\n# if verbose: print("Writing cache...", file_name)\n# with lzma.open(file_name, \'wb\', ) as f:\n# pickle.dump(object, f)\n# if verbose: print("Done!")\n#\n#\n# def cache_exists(file_name):\n# # file_name = cn_(file_name) if cache_prefix else file_name\n# return os.path.exists(file_name)\n#\n#\n# def cache_read(file_name):\n# # import compress_pickle # Import here because if you import in top the __version__ tag will fail.\n# # file_name = cn_(file_name) if cache_prefix else file_name\n# if os.path.exists(file_name):\n# try:\n# with lzma.open(file_name, \'rb\') as f:\n# return pickle.load(f)\n# except Exception as e:\n# print("Tried to load a bad pickle file at", file_name)\n# print("If the file appears to be automatically generated, you can try to delete it, otherwise download a new version")\n# print(e)\n# # return pickle.load(f)\n# else:\n# return None\n\n\n\nimport re\nimport sys\nimport threading\nimport time\nfrom collections import namedtuple\nfrom io import StringIO\nimport numpy as np\nimport tqdm\nfrom colorama import Fore\nfrom functools import _make_key\n\n_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])\n\n\ndef gprint(s):\n print(f"{Fore.LIGHTGREEN_EX}{s}")\n\n\nmyround = lambda x: np.round(x) # required for obfuscation.\nmsum = lambda x: sum(x)\nmfloor = lambda x: np.floor(x)\n\n\n\n\nclass Logger(object):\n def __init__(self, buffer, write_to_stdout=True):\n # assert False\n self.terminal = sys.stdout\n self.write_to_stdout = write_to_stdout\n self.log = buffer\n\n def write(self, message):\n if self.write_to_stdout:\n self.terminal.write(message)\n self.log.write(message)\n\n def flush(self):\n # this flush method is needed for python 3 compatibility.\n pass\n\n\nclass Capturing(list):\n def __init__(self, *args, stdout=None, unmute=False, **kwargs):\n self._stdout = stdout\n self.unmute = unmute\n super().__init__(*args, **kwargs)\n\n def __enter__(self, capture_errors=True): # don\'t put arguments here.\n self._stdout = sys.stdout if self._stdout == None else self._stdout\n self._stringio = StringIO()\n if self.unmute:\n sys.stdout = Logger(self._stringio)\n else:\n sys.stdout = self._stringio\n\n if capture_errors:\n self._sterr = sys.stderr\n sys.sterr = StringIO() # memory hole it\n self.capture_errors = capture_errors\n return self\n\n def __exit__(self, *args):\n self.extend(self._stringio.getvalue().splitlines())\n del self._stringio # free up some memory\n sys.stdout = self._stdout\n if self.capture_errors:\n sys.sterr = self._sterr\n\n\nclass Capturing2(Capturing):\n def __exit__(self, *args):\n lines = self._stringio.getvalue().splitlines()\n txt = "\\n".join(lines)\n numbers = extract_numbers(rm_progress_bar(txt))\n self.extend(lines)\n del self._stringio # free up some memory\n sys.stdout = self._stdout\n if self.capture_errors:\n sys.sterr = self._sterr\n\n self.output = txt\n self.numbers = numbers\n\n\ndef rm_progress_bar(txt):\n # More robust version. Apparently length of bar can depend on various factors, so check for order of symbols.\n nlines = []\n for l in txt.splitlines():\n pct = l.find("%")\n ql = False\n if pct > 0:\n i = l.find("|", pct + 1)\n if i > 0 and l.find("|", i + 1) > 0:\n ql = True\n if not ql:\n nlines.append(l)\n return "\\n".join(nlines)\n\n\nclass ActiveProgress():\n def __init__(self, t, start=True, title="my progress bar", show_progress_bar=True, file=None, mute_stdout=False):\n if file == None:\n file = sys.stdout\n self.file = file\n self.mute_stdout = mute_stdout\n self._running = False\n self.title = title\n self.dt = 0.025\n self.n = max(1, int(np.round(t / self.dt)))\n self.show_progress_bar = show_progress_bar\n self.pbar = None\n\n if start:\n self.start()\n\n def start(self):\n if self.mute_stdout:\n import io\n # from unitgrade.utils import Logger\n self._stdout = sys.stdout\n sys.stdout = Logger(io.StringIO(), write_to_stdout=False)\n\n self._running = True\n if self.show_progress_bar:\n self.thread = threading.Thread(target=self.run)\n self.thread.start()\n self.time_started = time.time()\n\n def terminate(self):\n if not self._running:\n print("Stopping a progress bar which is not running (class unitgrade.utils.ActiveProgress")\n pass\n # raise Exception("Stopping a stopped progress bar. ")\n self._running = False\n if self.show_progress_bar:\n self.thread.join()\n if self.pbar is not None:\n self.pbar.update(1)\n self.pbar.close()\n self.pbar = None\n\n self.file.flush()\n\n if self.mute_stdout:\n import io\n # from unitgrade.utils import Logger\n sys.stdout = self._stdout #= sys.stdout\n\n # sys.stdout = Logger(io.StringIO(), write_to_stdout=False)\n\n return time.time() - self.time_started\n\n def run(self):\n self.pbar = tqdm.tqdm(total=self.n, file=self.file, position=0, leave=False, desc=self.title, ncols=100,\n bar_format=\'{l_bar}{bar}| [{elapsed}<{remaining}]\')\n t_ = time.time()\n for _ in range(self.n - 1): # Don\'t terminate completely; leave bar at 99% done until terminate.\n if not self._running:\n self.pbar.close()\n self.pbar = None\n break\n tc = time.time()\n tic = max(0, self.dt - (tc - t_))\n if tic > 0:\n time.sleep(tic)\n t_ = time.time()\n self.pbar.update(1)\n\n\ndef dprint(first, last, nL, extra = "", file=None, dotsym=\'.\', color=\'white\'):\n if file == None:\n file = sys.stdout\n dot_parts = (dotsym * max(0, nL - len(last) - len(first)))\n print(first + dot_parts, end="", file=file)\n last += extra\n print(last, file=file)\n\n\ndef hide(func):\n return func\n\n\ndef makeRegisteringDecorator(foreignDecorator):\n """\n Returns a copy of foreignDecorator, which is identical in every\n way(*), except also appends a .decorator property to the callable it\n spits out.\n """\n\n def newDecorator(func):\n # Call to newDecorator(method)\n # Exactly like old decorator, but output keeps track of what decorated it\n R = foreignDecorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done\n R.decorator = newDecorator # keep track of decorator\n # R.original = func # might as well keep track of everything!\n return R\n\n newDecorator.__name__ = foreignDecorator.__name__\n newDecorator.__doc__ = foreignDecorator.__doc__\n return newDecorator\n\n\nhide = makeRegisteringDecorator(hide)\n\n\ndef extract_numbers(txt):\n numeric_const_pattern = r\'[-+]? (?: (?: \\d* \\. \\d+ ) | (?: \\d+ \\.? ) )(?: [Ee] [+-]? \\d+ ) ?\'\n rx = re.compile(numeric_const_pattern, re.VERBOSE)\n all = rx.findall(txt)\n all = [float(a) if (\'.\' in a or "e" in a) else int(a) for a in all]\n if len(all) > 500:\n print(txt)\n raise Exception("unitgrade_v1.unitgrade_v1.py: Warning, too many numbers!", len(all))\n return all\n\n\ndef cache(foo, typed=False):\n """ Magic cache wrapper\n https://github.com/python/cpython/blob/main/Lib/functools.py\n """\n maxsize = None\n def wrapper(self, *args, **kwargs):\n key = (self.cache_id(), ("@cache", foo.__name__, _make_key(args, kwargs, typed)))\n # print(self._cache.keys())\n # for k in self._cache:\n # print(k)\n if not self._cache_contains(key):\n value = foo(self, *args, **kwargs)\n self._cache_put(key, value)\n else:\n value = self._cache_get(key)\n # This appears to be required since there are two caches. Otherwise, when deploy method is run twice,\n # the cache will not be set correctly.\n self._cache_put(key, value)\n return value\n\n return wrapper\n\n\ndef methodsWithDecorator(cls, decorator):\n """\n Returns all methods in CLS with DECORATOR as the\n outermost decorator.\n\n DECORATOR must be a "registering decorator"; one\n can make any decorator "registering" via the\n makeRegisteringDecorator function.\n\n import inspect\n ls = list(methodsWithDecorator(GeneratorQuestion, deco))\n for f in ls:\n print(inspect.getsourcelines(f) ) # How to get all hidden questions.\n """\n for maybeDecorated in cls.__dict__.values():\n if hasattr(maybeDecorated, \'decorator\'):\n if maybeDecorated.decorator == decorator:\n print(maybeDecorated)\n yield maybeDecorated\n\nimport io\nimport sys\nimport time\nimport unittest\nfrom unittest.runner import _WritelnDecorator\nimport numpy as np\n\n\nclass UTextResult(unittest.TextTestResult):\n nL = 80\n number = -1 # HAcky way to set question number.\n show_progress_bar = True\n unmute = False # Whether to redirect stdout.\n cc = None\n setUpClass_time = 3 # Estimated time to run setUpClass in TestCase. Must be set externally. See key (("ClassName", "setUpClass"), "time") in _cache.\n\n def __init__(self, stream, descriptions, verbosity):\n super().__init__(stream, descriptions, verbosity)\n self.successes = []\n\n def printErrors(self) -> None:\n # TODO: Fix here. probably also needs to flush stdout.\n self.printErrorList(\'ERROR\', [(test, res[\'stderr\']) for test, res in self.errors])\n self.printErrorList(\'FAIL\', [(test, res[\'stderr\']) for test, res in self.failures])\n\n def addError(self, test, err):\n super(unittest.TextTestResult, self).addError(test, err)\n err = self.errors[-1][1]\n if hasattr(sys.stdout, \'log\'):\n stdout = sys.stdout.log.readlines() # Only works because we set sys.stdout to a unitgrade.Logger\n else:\n stdout = ""\n self.errors[-1] = (self.errors[-1][0], {\'return\': None,\n \'stderr\': err,\n \'stdout\': stdout\n })\n\n if not hasattr(self, \'item_title_print\'):\n # In case setUpClass() fails with an error the short description may not be set. This will fix that problem.\n self.item_title_print = test.shortDescription()\n if self.item_title_print is None: # In case the short description is not set either...\n self.item_title_print = test.id()\n\n\n self.cc_terminate(success=False)\n\n def addFailure(self, test, err):\n super(unittest.TextTestResult, self).addFailure(test, err)\n err = self.failures[-1][1]\n stdout = sys.stdout.log.readlines() # Only works because we set sys.stdout to a unitgrade.Logger\n self.failures[-1] = (self.failures[-1][0], {\'return\': None,\n \'stderr\': err,\n \'stdout\': stdout\n })\n self.cc_terminate(success=False)\n\n\n def addSuccess(self, test: unittest.case.TestCase) -> None:\n msg = None\n stdout = sys.stdout.log.readlines() # Only works because we set sys.stdout to a unitgrade.Logger\n\n if hasattr(test, \'_get_outcome\'):\n o = test._get_outcome()\n if isinstance(o, dict):\n key = (test.cache_id(), "return")\n if key in o:\n msg = test._get_outcome()[key]\n\n # print(sys.stdout.readlines())\n self.successes.append((test, None)) # (test, message) (to be consistent with failures and errors).\n self.successes[-1] = (self.successes[-1][0], {\'return\': msg,\n \'stdout\': stdout,\n \'stderr\': None})\n\n self.cc_terminate()\n\n def cc_terminate(self, success=True):\n if self.show_progress_bar or True:\n tsecs = np.round(self.cc.terminate(), 2)\n self.cc.file.flush()\n ss = self.item_title_print\n\n state = "PASS" if success else "FAILED"\n\n dot_parts = (\'.\' * max(0, self.nL - len(state) - len(ss)))\n if self.show_progress_bar or True:\n print(self.item_title_print + dot_parts, end="", file=self.cc.file)\n else:\n print(dot_parts, end="", file=self.cc.file)\n\n if tsecs >= 0.5:\n state += " (" + str(tsecs) + " seconds)"\n print(state, file=self.cc.file)\n\n def startTest(self, test):\n name = test.__class__.__name__\n if self.testsRun == 0 and hasattr(test.__class__, \'_cache2\'): # Disable this if the class is pure unittest.TestCase\n # This is the first time we are running a test. i.e. we can time the time taken to call setupClass.\n if test.__class__._cache2 is None:\n test.__class__._cache2 = {}\n test.__class__._cache2[((name, \'setUpClass\'), \'time\')] = time.time() - self.t_start\n\n self.testsRun += 1\n item_title = test.shortDescription() # Better for printing (get from cache).\n\n if item_title == None:\n # For unittest framework where getDescription may return None.\n item_title = self.getDescription(test)\n self.item_title_print = " * q%i.%i) %s" % (UTextResult.number + 1, self.testsRun, item_title)\n # if self.show_progress_bar or True:\n estimated_time = test.__class__._cache.get(((name, test._testMethodName), \'time\'), 100) if hasattr(test.__class__, \'_cache\') else 4\n self.cc = ActiveProgress(t=estimated_time, title=self.item_title_print, show_progress_bar=self.show_progress_bar)\n # else:\n # print(self.item_title_print + (\'.\' * max(0, self.nL - 4 - len(self.item_title_print))), end="")\n self._test = test\n # if not self.unmute:\n self._stdout = sys.stdout # Redundant. remove later.\n sys.stdout = Logger(io.StringIO(), write_to_stdout=self.unmute)\n\n def stopTest(self, test):\n # if not self.unmute:\n buff = sys.stdout.log\n sys.stdout = self._stdout # redundant.\n buff.close()\n super().stopTest(test)\n\n def _setupStdout(self):\n if self._previousTestClass == None:\n self.t_start = time.time()\n if hasattr(self.__class__, \'q_title_print\'):\n q_title_print = self.__class__.q_title_print\n else:\n q_title_print = "<unnamed test. See unitgrade.framework.py>"\n\n cc = ActiveProgress(t=self.setUpClass_time, title=q_title_print, show_progress_bar=self.show_progress_bar, mute_stdout=not self.unmute)\n self.cc = cc\n\n\n def _restoreStdout(self): # Used when setting up the test.\n if self._previousTestClass is None:\n q_time = self.cc.terminate()\n q_time = np.round(q_time, 2)\n sys.stdout.flush()\n if self.show_progress_bar:\n print(self.cc.title, end="")\n print(" " * max(0, self.nL - len(self.cc.title)) + (" (" + str(q_time) + " seconds)" if q_time >= 0.5 else ""))\n\n\nclass UTextTestRunner(unittest.TextTestRunner):\n def __init__(self, *args, **kwargs):\n stream = io.StringIO()\n super().__init__(*args, stream=stream, **kwargs)\n\n def _makeResult(self):\n # stream = self.stream # not you!\n stream = sys.stdout\n stream = _WritelnDecorator(stream)\n return self.resultclass(stream, self.descriptions, self.verbosity)\n\nimport importnb\nimport numpy as np\nimport sys\nimport pickle\nimport os\nimport inspect\nimport colorama\nimport unittest\nimport time\nimport textwrap\nimport urllib.parse\nimport requests\nimport ast\nimport numpy\n\ncolorama.init(autoreset=True) # auto resets your settings after every output\nnumpy.seterr(all=\'raise\')\n\ndef setup_dir_by_class(C, base_dir):\n name = C.__class__.__name__\n return base_dir, name\n\n\nclass Report:\n title = "report title"\n abbreviate_questions = False # Should the test items start with \'Question ...\' or just be q1).\n version = None # A version number of the report (1.0). Used to compare version numbers with online resources.\n url = None # Remote location of this problem.\n\n questions = []\n pack_imports = []\n individual_imports = []\n\n _remote_check_cooldown_seconds = 1 # Seconds between remote check of report.\n nL = 120 # Maximum line width\n _config = None # Private variable. Used when collecting results from student computers. Should only be read/written by teacher and never used for regular evaluation.\n _setup_mode = False # True if test is being run in setup-mode, i.e. will not fail because of bad configurations, etc.\n\n @classmethod\n def reset(cls):\n for (q, _) in cls.questions:\n if hasattr(q, \'reset\'):\n q.reset()\n\n @classmethod\n def mfile(clc):\n return inspect.getfile(clc)\n\n def _file(self):\n return inspect.getfile(type(self))\n\n def _artifact_file(self):\n """ File for the artifacts DB (thread safe). This file is optinal. Note that it is a pupdb database file.\n Note the file is shared between all sub-questions. """\n return os.path.join(os.path.dirname(self._file()), "unitgrade_data/main_config_"+ os.path.basename(self._file()[:-3]) + ".json")\n\n\n def _is_run_in_grade_mode(self):\n """ True if this report is being run as part of a grade run. """\n return self._file().endswith("_grade.py") # Not sure I love this convention.\n\n def _import_base_relative(self):\n if hasattr(self.pack_imports[0], \'__path__\'):\n root_dir = self.pack_imports[0].__path__[0]\n else:\n root_dir = self.pack_imports[0].__file__\n\n root_dir = os.path.dirname(root_dir)\n relative_path = os.path.relpath(self._file(), root_dir)\n modules = os.path.normpath(relative_path[:-3]).split(os.sep)\n relative_path = relative_path.replace("\\\\", "/")\n\n return root_dir, relative_path, modules\n\n def __init__(self, strict=False, payload=None):\n working_directory = os.path.abspath(os.path.dirname(self._file()))\n self.wdir, self.name = setup_dir_by_class(self, working_directory)\n # self.computed_answers_file = os.path.join(self.wdir, self.name + "_resources_do_not_hand_in.dat")\n for (q, _) in self.questions:\n q.nL = self.nL # Set maximum line length.\n\n if payload is not None:\n self.set_payload(payload, strict=strict)\n\n def main(self, verbosity=1):\n # Run all tests using standard unittest (nothing fancy).\n loader = unittest.TestLoader()\n for q, _ in self.questions:\n start = time.time() #\n suite = loader.loadTestsFromTestCase(q)\n unittest.TextTestRunner(verbosity=verbosity).run(suite)\n total = time.time() - start\n q.time = total\n\n def _setup_answers(self, with_coverage=False, verbose=True):\n if with_coverage:\n for q, _ in self.questions:\n q._with_coverage = True\n q._report = self\n for q, _ in self.questions:\n q._setup_answers_mode = True\n\n evaluate_report_student(self, unmute=verbose, noprogress=not verbose)\n\n # self.main() # Run all tests in class just to get that out of the way...\n report_cache = {}\n for q, _ in self.questions:\n # print(self.questions)\n if hasattr(q, \'_save_cache\'):\n q()._save_cache()\n print("q is", q())\n report_cache[q.__qualname__] = q._cache2\n else:\n report_cache[q.__qualname__] = {\'no cache see _setup_answers in framework.py\': True}\n if with_coverage:\n for q, _ in self.questions:\n q._with_coverage = False\n # report_cache is saved on a per-question basis.\n # it could also contain additional information such as runtime metadata etc. This may not be appropriate to store with the invidivual questions(?).\n # In this case, the function should be re-defined.\n return report_cache\n\n def set_payload(self, payloads, strict=False):\n for q, _ in self.questions:\n q._cache = payloads[q.__qualname__]\n self._config = payloads[\'config\']\n\n def _check_remote_versions(self):\n if self.url is None:\n return\n url = self.url\n if not url.endswith("/"):\n url += "/"\n snapshot_file = os.path.dirname(self._file()) + "/unitgrade_data/.snapshot"\n # print("Sanity checking time using snapshot", snapshot_file)\n # print("and using self-identified file", self._file())\n\n if os.path.isfile(snapshot_file):\n with open(snapshot_file, \'r\') as f:\n t = f.read()\n if (time.time() - float(t)) < self._remote_check_cooldown_seconds:\n return\n # print("Is this file run in local mode?", self._is_run_in_grade_mode())\n\n if self.url.startswith("https://gitlab"):\n # Try to turn url into a \'raw\' format.\n # "https://gitlab.compute.dtu.dk/tuhe/unitgrade_private/-/raw/master/examples/autolab_example_py_upload/instructor/cs102_autolab/report2_test.py?inline=false"\n # url = self.url\n url = url.replace("-/tree", "-/raw")\n # print(url)\n # "https://gitlab.compute.dtu.dk/tuhe/unitgrade_private/-/tree/master/examples/autolab_example_py_upload/instructor/cs102_autolab"\n # "https://gitlab.compute.dtu.dk/tuhe/unitgrade_private/-/raw/master/examples/autolab_example_py_upload/instructor/report2_test.py?inline=false"\n # "https://gitlab.compute.dtu.dk/tuhe/unitgrade_private/-/raw/master/examples/autolab_example_py_upload/instructor/cs102_autolab/report2_test.py?inline=false"\n raw_url = urllib.parse.urljoin(url, os.path.basename(self._file()) + "?inline=false")\n # print("Is this file run in local mode?", self._is_run_in_grade_mode())\n if self._is_run_in_grade_mode():\n remote_source = requests.get(raw_url).text\n with open(self._file(), \'r\') as f:\n local_source = f.read()\n if local_source != remote_source:\n print("\\nThe local version of this report is not identical to the remote version which can be found at")\n print(self.url)\n print("The most likely reason for this is that the remote version was updated by the teacher due to some issue.")\n print("You should check if there was an announcement and update the test to the most recent version; most likely")\n print("This can be done by running the command")\n print("> git pull")\n print("You can find the most recent code here:")\n print(self.url)\n raise Exception(f"Version of grade script does not match the remote version. Please update using git pull")\n #\n # # node = ast.parse(text)\n # # classes = [n for n in node.body if isinstance(n, ast.ClassDef) if n.name == self.__class__.__name__][0]\n #\n # # for b in classes.body:\n # # print(b.)\n # # if b.targets[0].id == "version":\n # # print(b)\n # # print(b.value)\n # version_remote = b.value.value\n # break\n # if version_remote != self.version:\n else:\n text = requests.get(raw_url).text\n node = ast.parse(text)\n classes = [n for n in node.body if isinstance(n, ast.ClassDef) if n.name == self.__class__.__name__][0]\n for b in classes.body:\n # print(b.)\n if b.targets[0].id == "version":\n # print(b)\n # print(b.value)\n version_remote = b.value.value\n break\n if version_remote != self.version:\n print("\\nThe version of this report", self.version, "does not match the version of the report on git", version_remote)\n print("The most likely reason for this is that the remote version was updated by the teacher due to some issue.")\n print("You should check if there was an announcement and update the test to the most recent version; most likely")\n print("This can be done by running the command")\n print("> git pull")\n print("You can find the most recent code here:")\n print(self.url)\n raise Exception(f"Version of test on remote is {version_remote}, which is different than this version of the test {self.version}. Please update your test to the most recent version.")\n\n for (q,_) in self.questions:\n qq = q(skip_remote_check=True)\n cfile = qq._cache_file()\n\n relpath = os.path.relpath(cfile, os.path.dirname(self._file()))\n relpath = relpath.replace("\\\\", "/")\n raw_url = urllib.parse.urljoin(url, relpath + "?inline=false")\n # requests.get(raw_url)\n\n with open(cfile, \'rb\') as f:\n b1 = f.read()\n\n b2 = requests.get(raw_url).content\n if b1 != b2:\n print("\\nQuestion ", qq.title, "relies on the data file", cfile)\n print("However, it appears that this file is missing or in a different version than the most recent found here:")\n print(self.url)\n print("The most likely reason for this is that the remote version was updated by the teacher due to some issue.")\n print("You should check if there was an announcement and update the test to the most recent version; most likely")\n print("This can be done by simply running the command")\n print("> git pull")\n print("to avoid running bad tests against good code, the program will now stop. Please update and good luck!")\n raise Exception("The data file for the question", qq.title, "did not match remote source found on git. The test will therefore automatically fail. Please update your test/data files.")\n\n t = time.time()\n if os.path.isdir(os.path.dirname(self._file()) + "/unitgrade_data"):\n with open(snapshot_file, \'w\') as f:\n f.write(f"{t}")\n\ndef get_hints(ss):\n """ Extract all blocks of the forms:\n\n Hints:\n bla-bla.\n\n and returns the content unaltered.\n """\n if ss == None:\n return None\n try:\n ss = textwrap.dedent(ss)\n ss = ss.replace(\'\'\'"""\'\'\', "").strip()\n hints = ["hints:", "hint:"]\n indexes = [ss.lower().find(h) for h in hints]\n j = np.argmax(indexes)\n if indexes[j] == -1:\n return None\n h = hints[j]\n ss = ss[ss.lower().find(h) + len(h) + 1:]\n ss = "\\n".join([l for l in ss.split("\\n") if not l.strip().startswith(":")])\n ss = textwrap.dedent(ss).strip()\n # if ss.startswith(\'*\'):\n # ss = ss[1:].strip()\n return ss\n except Exception as e:\n print("bad hints", ss, e)\n\n\nclass UTestCase(unittest.TestCase):\n _outcome = None # A dictionary which stores the user-computed outcomes of all the tests. This differs from the cache.\n _cache = None # Read-only cache. Ensures method always produce same result.\n _cache2 = None # User-written cache.\n _with_coverage = False\n _covcache = None # Coverage cache. Written to if _with_coverage is true.\n _report = None # The report used. This is very, very hacky and should always be None. Don\'t rely on it!\n\n # If true, the tests will not fail when cache is used. This is necesary since otherwise the cache will not be updated\n # during setup, and the deploy script must be run many times.\n _setup_answers_mode = False\n\n def capture(self):\n if hasattr(self, \'_stdout\') and self._stdout is not None:\n file = self._stdout\n else:\n file = sys.stdout\n return Capturing2(stdout=file)\n\n @classmethod\n def question_title(cls):\n """ Return the question title """\n if cls.__doc__ is not None:\n title = cls.__doc__.strip().splitlines()[0].strip()\n if not (title.startswith("Hints:") or title.startswith("Hint:") ):\n return title\n return cls.__qualname__\n\n def run(self, result):\n from unittest.case import TestCase\n from pupdb.core import PupDB\n db = PupDB(self._artifact_file())\n\n db.set(\'run_id\', np.random.randint(1000*1000))\n db.set("state", "running")\n db.set(\'coverage_files_changed\', None)\n\n _stdout = sys.stdout\n _stderr = sys.stderr\n\n std_capture = StdCapturing(stdout=sys.stdout, stderr=sys.stderr, db=db, mute=False)\n\n # stderr_capture = StdCapturing(sys.stderr, db=db)\n # std_err_capture = StdCapturing(sys.stderr, "stderr", db=db)\n\n try:\n # Run this unittest and record all of the output.\n # This is probably where we should hijack the stdout output and save it -- after all, this is where the test is actually run.\n # sys.stdout = stdout_capture\n sys.stderr = std_capture.dummy_stderr\n sys.stdout = std_capture.dummy_stdout\n\n result_ = TestCase.run(self, result)\n\n from werkzeug.debug.tbtools import DebugTraceback, _process_traceback\n # print(result_._excinfo[0])\n actual_errors = []\n for test, err in self._error_fed_during_run:\n if err is None:\n continue\n else:\n import traceback\n # traceback.print_tb(err[2])\n actual_errors.append(err)\n\n if len(actual_errors) > 0:\n ex, exi, tb = actual_errors[0]\n\n # exi = result_._excinfo[0]._excinfo\n # tb = result_._excinfo[0]._excinfo[-1]\n # DebugTraceback(tb)\n # ex = exi[1]\n exi.__traceback__ = tb\n # tbe = _process_traceback(ex)\n dbt = DebugTraceback(exi)\n # dbt.render_traceback_text()\n sys.stderr.write(dbt.render_traceback_text())\n html = dbt.render_traceback_html(include_title="hello world")\n # print(HEADER)\n\n # from unittest.case import As\n db.set(\'wz_stacktrace\', html)\n db.set(\'state\', \'fail\')\n\n\n # print("> Set state of test to:", "fail", exi, tb)\n\n else:\n print("> Set state of test to:", "pass")\n db.set(\'state\', \'pass\')\n\n\n except Exception as e:\n print("-----------------.///////////////////////////////////////////////////////////////")\n # print(e)\n import traceback\n traceback.print_exc()\n raise e\n\n finally:\n\n sys.stdout = _stdout\n sys.stderr = _stderr\n\n std_capture.close()\n\n # stderr_capture.close()\n # if len(actual_errors)\n\n # print(result._test._error_fed_during_run)\n # print(self._error_fed_during_run)\n # print( result.errors[0][0]._error_fed_during_run )\n #\n # result_.errors[0][0]._error_fed_during_run\n\n # result_._excinfo[0].errisinstance(Exception)\n # import AssertionError\n from werkzeug.debug.tbtools import HEADER\n # from pupdb.core import PupDB\n # db = PupDB(self._artifact_file())\n\n # actual_errors\n\n\n return result_\n\n\n @classmethod\n def before_setup_called(cls):\n print("hi")\n # print("I am called before the fucking class is fucking made. setUpClass has been broken!")\n pass\n\n setUpClass_not_overwritten = False\n @classmethod\n def setUpClass(cls) -> None:\n cls.setUpClass_not_overwritten = True\n pass\n\n @classmethod\n def __new__(cls, *args, **kwargs):\n old_setup = cls.setUpClass\n def new_setup():\n cls.before_setup_called()\n try:\n old_setup()\n except Exception as e:\n raise e\n finally:\n pass\n\n cls.setUpClass = new_setup\n return super().__new__(cls)\n\n\n def _callSetUp(self):\n if self._with_coverage:\n if self._covcache is None:\n self._covcache = {}\n import coverage\n self.cov = coverage.Coverage(data_file=None)\n self.cov.start()\n self.setUp()\n\n def _callTearDown(self):\n self.tearDown()\n # print("Teardown.")\n if self._with_coverage:\n # print("with cov")\n from pathlib import Path\n from snipper import snipper_main\n try:\n self.cov.stop()\n except Exception as e:\n print("Something went wrong while tearing down coverage test")\n print(e)\n data = self.cov.get_data()\n base, _, _ = self._report._import_base_relative()\n for file in data.measured_files():\n file = os.path.normpath(file)\n root = Path(base)\n child = Path(file)\n if root in child.parents:\n # print("Reading file", child)\n with open(child, \'r\') as f:\n s = f.read()\n lines = s.splitlines()\n garb = \'GARBAGE\'\n lines2 = snipper_main.censor_code(lines, keep=True)\n # print("\\n".join(lines2))\n if len(lines) != len(lines2):\n for k in range(len(lines)):\n print(k, ">", lines[k], "::::::::", lines2[k])\n print("Snipper failure; line lenghts do not agree. Exiting..")\n print(child, "len(lines) == len(lines2)", len(lines), len(lines2))\n import sys\n sys.exit()\n\n assert len(lines) == len(lines2)\n for ll in data.contexts_by_lineno(file):\n l = ll-1\n if l < len(lines2) and lines2[l].strip() == garb:\n # print("Got a hit at l", l)\n rel = os.path.relpath(child, root)\n cc = self._covcache\n j = 0\n for j in range(l, -1, -1):\n if "def" in lines2[j] or "class" in lines2[j]:\n break\n from snipper.legacy import gcoms\n\n fun = lines2[j]\n comments, _ = gcoms("\\n".join(lines2[j:l]))\n if rel not in cc:\n cc[rel] = {}\n cc[rel][fun] = (l, "\\n".join(comments))\n # print("found", rel, fun)\n self._cache_put((self.cache_id(), \'coverage\'), self._covcache)\n\n def shortDescriptionStandard(self):\n sd = super().shortDescription()\n if sd is None or sd.strip().startswith("Hints:") or sd.strip().startswith("Hint:"):\n sd = self._testMethodName\n return sd\n\n def shortDescription(self):\n sd = self.shortDescriptionStandard()\n title = self._cache_get((self.cache_id(), \'title\'), sd)\n return title if title is not None else sd\n\n @property\n def title(self):\n return self.shortDescription()\n\n @title.setter\n def title(self, value):\n self._cache_put((self.cache_id(), \'title\'), value)\n\n def _get_outcome(self):\n if not hasattr(self.__class__, \'_outcome\') or self.__class__._outcome is None:\n self.__class__._outcome = {}\n return self.__class__._outcome\n\n def _callTestMethod(self, testMethod):\n t = time.time()\n self._ensure_cache_exists() # Make sure cache is there.\n if self._testMethodDoc is not None:\n self._cache_put((self.cache_id(), \'title\'), self.shortDescriptionStandard())\n\n self._cache2[(self.cache_id(), \'assert\')] = {}\n res = testMethod()\n elapsed = time.time() - t\n self._get_outcome()[ (self.cache_id(), "return") ] = res\n self._cache_put((self.cache_id(), "time"), elapsed)\n\n\n def cache_id(self):\n c = self.__class__.__qualname__\n m = self._testMethodName\n return c, m\n\n def __init__(self, *args, skip_remote_check=False, **kwargs):\n super().__init__(*args, **kwargs)\n self._load_cache()\n self._assert_cache_index = 0\n # Perhaps do a sanity check here to see if the cache is up to date? To do that, we must make sure the\n # cache exists locally.\n # Find the report class this class is defined within.\n if skip_remote_check:\n return\n # import inspect\n\n # file = inspect.getfile(self.__class__)\n import importlib, inspect\n found_reports = []\n # print("But do I have report", self._report)\n # print("I think I am module", self.__module__)\n # print("Importlib says", importlib.import_module(self.__module__))\n # This will delegate you to the wrong main clsas when running in grade mode.\n for name, cls in inspect.getmembers(importlib.import_module(self.__module__), inspect.isclass):\n # print("checking", cls)\n if issubclass(cls, Report):\n for q,_ in cls.questions:\n if q == self.__class__:\n found_reports.append(cls)\n if len(found_reports) == 0:\n pass # This case occurs when the report _grade script is being run.\n # raise Exception("This question is not a member of a report. Very, very odd.")\n if len(found_reports) > 1:\n raise Exception("This question is a member of multiple reports. That should not be the case -- don\'t get too creative.")\n if len(found_reports) > 0:\n report = found_reports[0]\n report()._check_remote_versions()\n\n\n def _ensure_cache_exists(self):\n if not hasattr(self.__class__, \'_cache\') or self.__class__._cache == None:\n self.__class__._cache = dict()\n if not hasattr(self.__class__, \'_cache2\') or self.__class__._cache2 == None:\n self.__class__._cache2 = dict()\n\n def _cache_get(self, key, default=None):\n self._ensure_cache_exists()\n return self.__class__._cache.get(key, default)\n\n def _cache_put(self, key, value):\n self._ensure_cache_exists()\n self.__class__._cache2[key] = value\n\n def _cache_contains(self, key):\n self._ensure_cache_exists()\n return key in self.__class__._cache\n\n def get_expected_test_value(self):\n key = (self.cache_id(), \'assert\')\n id = self._assert_cache_index\n cache = self._cache_get(key)\n _expected = cache.get(id, f"Key {id} not found in cache; framework files missing. Please run deploy()")\n return _expected\n\n def wrap_assert(self, assert_fun, first, *args, **kwargs):\n key = (self.cache_id(), \'assert\')\n if not self._cache_contains(key):\n print("Warning, framework missing", key)\n self.__class__._cache[key] = {} # A new dict. We manually insert it because we have to use that the dict is mutable.\n cache = self._cache_get(key)\n id = self._assert_cache_index\n _expected = cache.get(id, f"Key {id} not found in cache; framework files missing. Please run deploy()")\n if not id in cache:\n print("Warning, framework missing cache index", key, "id =", id, " - The test will be skipped for now.")\n if self._setup_answers_mode:\n _expected = first # Bypass by setting equal to first. This is in case multiple self.assertEqualC\'s are run in a row and have to be set.\n\n # The order of these calls is important. If the method assert fails, we should still store the correct result in cache.\n cache[id] = first\n self._cache_put(key, cache)\n self._assert_cache_index += 1\n if not self._setup_answers_mode:\n assert_fun(first, _expected, *args, **kwargs)\n else:\n try:\n assert_fun(first, _expected, *args, **kwargs)\n except Exception as e:\n print("Mumble grumble. Cache function failed during class setup. Most likely due to old cache. Re-run deploy to check it pass.", id)\n print("> first", first)\n print("> expected", _expected)\n print(e)\n\n\n def assertEqualC(self, first, msg=None):\n self.wrap_assert(self.assertEqual, first, msg)\n\n def _shape_equal(self, first, second):\n a1 = np.asarray(first).squeeze()\n a2 = np.asarray(second).squeeze()\n msg = None\n msg = "" if msg is None else msg\n if len(msg) > 0:\n msg += "\\n"\n self.assertEqual(a1.shape, a2.shape, msg=msg + "Dimensions of input data does not agree.")\n assert(np.all(np.isinf(a1) == np.isinf(a2))) # Check infinite part.\n a1[np.isinf(a1)] = 0\n a2[np.isinf(a2)] = 0\n diff = np.abs(a1 - a2)\n return diff\n\n\n def assertLinf(self, first, second=None, tol=1e-5, msg=None):\n """ Test in the L_infinity norm.\n :param first:\n :param second:\n :param tol:\n :param msg:\n :return:\n """\n if second is None:\n return self.wrap_assert(self.assertLinf, first, tol=tol, msg=msg)\n else:\n diff = self._shape_equal(first, second)\n np.testing.assert_allclose(first, second, atol=tol)\n \n max_diff = max(diff.flat)\n if max_diff >= tol:\n from unittest.util import safe_repr\n # msg = f\'{safe_repr(first)} != {safe_repr(second)} : Not equal within tolerance {tol}\'\n # print(msg)\n # np.testing.assert_almost_equal\n # import numpy as np\n print(f"|first - second|_max = {max_diff} > {tol} ")\n np.testing.assert_almost_equal(first, second)\n # If the above fail, make sure to throw an error:\n self.assertFalse(max_diff >= tol, msg=f\'Input arrays are not equal within tolerance {tol}\')\n # self.assertEqual(first, second, msg=f\'Not equal within tolerance {tol}\')\n\n def assertL2(self, first, second=None, tol=1e-5, msg=None, relative=False):\n if second is None:\n return self.wrap_assert(self.assertL2, first, tol=tol, msg=msg, relative=relative)\n else:\n # We first test using numpys build-in testing method to see if one coordinate deviates a great deal.\n # This gives us better output, and we know that the coordinate wise difference is lower than the norm difference.\n if not relative:\n np.testing.assert_allclose(first, second, atol=tol)\n diff = self._shape_equal(first, second)\n diff = ( ( np.asarray( diff.flatten() )**2).sum() )**.5\n\n scale = (2/(np.linalg.norm(np.asarray(first).flat) + np.linalg.norm(np.asarray(second).flat)) ) if relative else 1\n max_diff = diff*scale\n if max_diff >= tol:\n msg = "" if msg is None else msg\n print(f"|first - second|_2 = {max_diff} > {tol} ")\n # Deletage to numpy. Let numpy make nicer messages.\n np.testing.assert_almost_equal(first, second) # This function does not take a msg parameter.\n # Make sure to throw an error no matter what.\n self.assertFalse(max_diff >= tol, msg=f\'Input arrays are not equal within tolerance {tol}\')\n # self.assertEqual(first, second, msg=msg + f"Not equal within tolerance {tol}")\n\n def _cache_file(self):\n return os.path.dirname(inspect.getabsfile(type(self))) + "/unitgrade_data/" + self.__class__.__name__ + ".pkl"\n\n def _artifact_file(self):\n """ File for the artifacts DB (thread safe). This file is optinal. Note that it is a pupdb database file.\n Note the file is shared between all sub-questions. """\n return os.path.join(os.path.dirname(self._cache_file()), \'-\'.join(self.cache_id()) + ".json")\n\n def _save_cache(self):\n # get the class name (i.e. what to save to).\n cfile = self._cache_file()\n if not os.path.isdir(os.path.dirname(cfile)):\n os.makedirs(os.path.dirname(cfile))\n\n if hasattr(self.__class__, \'_cache2\'):\n with open(cfile, \'wb\') as f:\n pickle.dump(self.__class__._cache2, f)\n\n # But you can also set cache explicitly.\n def _load_cache(self):\n if self._cache is not None: # Cache already loaded. We will not load it twice.\n return\n # raise Exception("Loaded cache which was already set. What is going on?!")\n cfile = self._cache_file()\n if os.path.exists(cfile):\n try:\n with open(cfile, \'rb\') as f:\n data = pickle.load(f)\n self.__class__._cache = data\n except Exception as e:\n print("Cache file did not exist:", cfile)\n print(e)\n else:\n print("Warning! data file not found", cfile)\n\n def _get_coverage_files(self):\n key = (self.cache_id(), \'coverage\')\n # CC = None\n # if self._cache_contains(key):\n return self._cache_get(key, None)\n # return CC\n\n def _get_hints(self):\n """\n This code is run when the test is set up to generate the hints and store them in an artifact file. It may be beneficial to simple compute them beforehand\n and store them in the local unitgrade pickle file. This code is therefore expected to superceede the alterative code later.\n """\n hints = []\n # print("Getting hint")\n key = (self.cache_id(), \'coverage\')\n if self._cache_contains(key):\n CC = self._cache_get(key)\n # cl, m = self.cache_id()\n # print("Getting hint using", CC)\n # Insert newline to get better formatting.\n # gprint(\n # f"\\n> An error occured during the test: {cl}.{m}. The following files/methods has code in them you are supposed to edit and may therefore be the cause of the problem:")\n for file in CC:\n rec = CC[file]\n # gprint(f"> * {file}")\n for l in rec:\n _, comments = CC[file][l]\n hint = get_hints(comments)\n\n if hint != None:\n hints.append((hint, file, l))\n\n doc = self._testMethodDoc\n # print("doc", doc)\n if doc is not None:\n hint = get_hints(self._testMethodDoc)\n if hint is not None:\n hints = [(hint, None, self.cache_id()[1])] + hints\n\n return hints\n\n def _feedErrorsToResult(self, result, errors):\n """ Use this to show hints on test failure.\n It feeds error to the result -- so if there are errors, they will crop up here\n """\n self._error_fed_during_run = errors.copy() # import to copy the error list.\n\n # result._test._error_fed_during_run = errors.copy()\n\n if not isinstance(result, UTextResult):\n er = [e for e, v in errors if v != None]\n # print("Errors are", errors)\n if len(er) > 0:\n hints = []\n key = (self.cache_id(), \'coverage\')\n if self._cache_contains(key):\n CC = self._cache_get(key)\n cl, m = self.cache_id()\n # Insert newline to get better formatting.\n gprint(f"\\n> An error occured during the test: {cl}.{m}. The following files/methods has code in them you are supposed to edit and may therefore be the cause of the problem:")\n for file in CC:\n rec = CC[file]\n gprint(f"> * {file}")\n for l in rec:\n _, comments = CC[file][l]\n hint = get_hints(comments)\n\n if hint != None:\n hints.append((hint, file, l) )\n gprint(f"> - {l}")\n\n er = er[0]\n\n doc = er._testMethodDoc\n # print("doc", doc)\n if doc is not None:\n hint = get_hints(er._testMethodDoc)\n if hint is not None:\n hints = [(hint, None, self.cache_id()[1] )] + hints\n if len(hints) > 0:\n # print(hints)\n for hint, file, method in hints:\n s = (f"\'{method.strip()}\'" if method is not None else "")\n if method is not None and file is not None:\n s += " in "\n try:\n s += (file.strip() if file is not None else "")\n gprint(">")\n gprint("> Hints (from " + s + ")")\n gprint(textwrap.indent(hint, "> "))\n except Exception as e:\n print("Bad stuff in hints. ")\n print(hints)\n # result._last_errors = errors\n super()._feedErrorsToResult(result, errors)\n b = 234\n\n def startTestRun(self):\n super().startTestRun()\n\nclass Required:\n pass\n\nclass ParticipationTest(UTestCase,Required):\n max_group_size = None\n students_in_group = None\n workload_assignment = {\'Question 1\': [1, 0, 0]}\n\n def test_students(self):\n pass\n\n def test_workload(self):\n pass\n\n# 817, 705\nclass NotebookTestCase(UTestCase):\n notebook = None\n _nb = None\n @classmethod\n def setUpClass(cls) -> None:\n with Capturing():\n cls._nb = importnb.Notebook.load(cls.notebook)\n\n @property\n def nb(self):\n return self.__class__._nb\n\nimport hashlib\nimport io\nimport tokenize\nimport numpy as np\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport pyfiglet\nimport unittest\nimport inspect\nimport os\nimport argparse\nimport time\n\nparser = argparse.ArgumentParser(description=\'Evaluate your report.\', epilog="""Example: \nTo run all tests in a report: \n\n> python assignment1_dp.py\n\nTo run only question 2 or question 2.1\n\n> python assignment1_dp.py -q 2\n> python assignment1_dp.py -q 2.1\n\nNote this scripts does not grade your report. To grade your report, use:\n\n> python report1_grade.py\n\nFinally, note that if your report is part of a module (package), and the report script requires part of that package, the -m option for python may be useful.\nFor instance, if the report file is in Documents/course_package/report3_complete.py, and `course_package` is a python package, then change directory to \'Documents/` and run:\n\n> python -m course_package.report1\n\nsee https://docs.python.org/3.9/using/cmdline.html\n""", formatter_class=argparse.RawTextHelpFormatter)\nparser.add_argument(\'-q\', nargs=\'?\', type=str, default=None, help=\'Only evaluate this question (e.g.: -q 2)\')\nparser.add_argument(\'--showexpected\', action="store_true", help=\'Show the expected/desired result\')\nparser.add_argument(\'--showcomputed\', action="store_true", help=\'Show the answer your code computes\')\nparser.add_argument(\'--unmute\', action="store_true", help=\'Show result of print(...) commands in code\')\nparser.add_argument(\'--passall\', action="store_true", help=\'Automatically pass all tests. Useful when debugging.\')\nparser.add_argument(\'--noprogress\', action="store_true", help=\'Disable progress bars.\')\n\ndef evaluate_report_student(report, question=None, qitem=None, unmute=None, passall=None, ignore_missing_file=False, show_tol_err=False, show_privisional=True, noprogress=None):\n args = parser.parse_args()\n if noprogress is None:\n noprogress = args.noprogress\n\n if question is None and args.q is not None:\n question = args.q\n if "." in question:\n question, qitem = [int(v) for v in question.split(".")]\n else:\n question = int(question)\n\n if hasattr(report, "computed_answer_file") and not os.path.isfile(report.computed_answers_file) and not ignore_missing_file:\n raise Exception("> Error: The pre-computed answer file", os.path.abspath(report.computed_answers_file), "does not exist. Check your package installation")\n\n if unmute is None:\n unmute = args.unmute\n if passall is None:\n passall = args.passall\n\n results, table_data = evaluate_report(report, question=question, show_progress_bar=not unmute and not noprogress, qitem=qitem,\n verbose=False, passall=passall, show_expected=args.showexpected, show_computed=args.showcomputed,unmute=unmute,\n show_tol_err=show_tol_err)\n\n\n if question is None and show_privisional:\n print("Provisional evaluation")\n tabulate(table_data)\n table = table_data\n print(tabulate(table))\n print(" ")\n\n fr = inspect.getouterframes(inspect.currentframe())[1].filename\n gfile = os.path.basename(fr)[:-3] + "_grade.py"\n if os.path.exists(gfile):\n print("Note your results have not yet been registered. \\nTo register your results, please run the file:")\n print(">>>", gfile)\n print("In the same manner as you ran this file.")\n\n\n return results\n\n\ndef upack(q):\n # h = zip([(i[\'w\'], i[\'possible\'], i[\'obtained\']) for i in q.values()])\n h =[(i[\'w\'], i[\'possible\'], i[\'obtained\']) for i in q.values()]\n h = np.asarray(h)\n return h[:,0], h[:,1], h[:,2],\n\nclass SequentialTestLoader(unittest.TestLoader):\n def getTestCaseNames(self, testCaseClass):\n test_names = super().getTestCaseNames(testCaseClass)\n # testcase_methods = list(testCaseClass.__dict__.keys())\n ls = []\n for C in testCaseClass.mro():\n if issubclass(C, unittest.TestCase):\n ls = list(C.__dict__.keys()) + ls\n testcase_methods = ls\n test_names.sort(key=testcase_methods.index)\n return test_names\n\ndef evaluate_report(report, question=None, qitem=None, passall=False, verbose=False, show_expected=False, show_computed=False,unmute=False, show_help_flag=True, silent=False,\n show_progress_bar=True,\n show_tol_err=False,\n big_header=True):\n\n now = datetime.now()\n if big_header:\n ascii_banner = pyfiglet.figlet_format("UnitGrade", font="doom")\n b = "\\n".join( [l for l in ascii_banner.splitlines() if len(l.strip()) > 0] )\n else:\n b = "Unitgrade"\n dt_string = now.strftime("%d/%m/%Y %H:%M:%S")\n print(b + " v" + __version__ + ", started: " + dt_string+ "\\n")\n # print("Started: " + dt_string)\n report._check_remote_versions() # Check (if report.url is present) that remote files exist and are in sync.\n s = report.title\n if hasattr(report, "version") and report.version is not None:\n s += f" version {report.version}"\n print(s, "(use --help for options)" if show_help_flag else "")\n # print(f"Loaded answers from: ", report.computed_answers_file, "\\n")\n table_data = []\n t_start = time.time()\n score = {}\n loader = SequentialTestLoader()\n\n for n, (q, w) in enumerate(report.questions):\n if question is not None and n+1 != question:\n continue\n suite = loader.loadTestsFromTestCase(q)\n qtitle = q.question_title() if hasattr(q, \'question_title\') else q.__qualname__\n if not report.abbreviate_questions:\n q_title_print = "Question %i: %s"%(n+1, qtitle)\n else:\n q_title_print = "q%i) %s" % (n + 1, qtitle)\n\n print(q_title_print, end="")\n q.possible = 0\n q.obtained = 0\n # q_ = {} # Gather score in this class.\n UTextResult.q_title_print = q_title_print # Hacky\n UTextResult.show_progress_bar = show_progress_bar # Hacky.\n UTextResult.number = n\n UTextResult.nL = report.nL\n UTextResult.unmute = unmute # Hacky as well.\n UTextResult.setUpClass_time = q._cache.get(((q.__name__, \'setUpClass\'), \'time\'), 3) if hasattr(q, \'_cache\') and q._cache is not None else 3\n\n\n res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite)\n details = {}\n for s, msg in res.successes + res.failures + res.errors:\n # from unittest.suite import _ErrorHolder\n # from unittest import _Err\n # if isinstance(s, _ErrorHolder)\n if hasattr(s, \'_testMethodName\'):\n key = (q.__name__, s._testMethodName)\n else:\n # In case s is an _ErrorHolder (unittest.suite)\n key = (q.__name__, s.id())\n # key = (q.__name__, s._testMethodName) # cannot use the cache_id method bc. it is not compatible with plain unittest.\n\n detail = {}\n if (s,msg) in res.successes:\n detail[\'status\'] = "pass"\n elif (s,msg) in res.failures:\n detail[\'status\'] = \'fail\'\n elif (s,msg) in res.errors:\n detail[\'status\'] = \'error\'\n else:\n raise Exception("Status not known.")\n\n nice_title = s.title\n detail = {**detail, **msg, \'nice_title\': nice_title}#[\'message\'] = msg\n details[key] = detail\n\n # q_[s._testMethodName] = ("pass", None)\n # for (s,msg) in res.failures:\n # q_[s._testMethodName] = ("fail", msg)\n # for (s,msg) in res.errors:\n # q_[s._testMethodName] = ("error", msg)\n # res.successes[0]._get_outcome()\n\n possible = res.testsRun\n obtained = len(res.successes)\n\n # assert len(res.successes) + len(res.errors) + len(res.failures) == res.testsRun\n\n obtained = int(w * obtained * 1.0 / possible ) if possible > 0 else 0\n score[n] = {\'w\': w, \'possible\': w, \'obtained\': obtained, \'items\': details, \'title\': qtitle, \'name\': q.__name__,\n }\n q.obtained = obtained\n q.possible = possible\n # print(q._cache)\n # print(q._covcache)\n s1 = f" * q{n+1}) Total"\n s2 = f" {q.obtained}/{w}"\n print(s1 + ("."* (report.nL-len(s1)-len(s2) )) + s2 )\n print(" ")\n table_data.append([f"q{n+1}) Total", f"{q.obtained}/{w}"])\n\n ws, possible, obtained = upack(score)\n possible = int( msum(possible) )\n obtained = int( msum(obtained) ) # Cast to python int\n report.possible = possible\n report.obtained = obtained\n now = datetime.now()\n dt_string = now.strftime("%H:%M:%S")\n\n dt = int(time.time()-t_start)\n minutes = dt//60\n seconds = dt - minutes*60\n plrl = lambda i, s: str(i) + " " + s + ("s" if i != 1 else "")\n\n dprint(first = "Total points at "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +")",\n last=""+str(report.obtained)+"/"+str(report.possible), nL = report.nL)\n\n # print(f"Completed at "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +"). Total")\n\n table_data.append(["Total", ""+str(report.obtained)+"/"+str(report.possible) ])\n results = {\'total\': (obtained, possible), \'details\': score}\n return results, table_data\n\n\ndef python_code_str_id(python_code, strip_comments_and_docstring=True):\n s = python_code\n\n if strip_comments_and_docstring:\n try:\n s = remove_comments_and_docstrings(s)\n except Exception as e:\n print("--"*10)\n print(python_code)\n print(e)\n\n s = "".join([c.strip() for c in s.split()])\n hash_object = hashlib.blake2b(s.encode())\n return hash_object.hexdigest()\n\n\ndef file_id(file, strip_comments_and_docstring=True):\n with open(file, \'r\') as f:\n # s = f.read()\n return python_code_str_id(f.read())\n\n\ndef remove_comments_and_docstrings(source):\n """\n Returns \'source\' minus comments and docstrings.\n """\n io_obj = io.StringIO(source)\n out = ""\n prev_toktype = tokenize.INDENT\n last_lineno = -1\n last_col = 0\n for tok in tokenize.generate_tokens(io_obj.readline):\n token_type = tok[0]\n token_string = tok[1]\n start_line, start_col = tok[2]\n end_line, end_col = tok[3]\n ltext = tok[4]\n # The following two conditionals preserve indentation.\n # This is necessary because we\'re not using tokenize.untokenize()\n # (because it spits out code with copious amounts of oddly-placed\n # whitespace).\n if start_line > last_lineno:\n last_col = 0\n if start_col > last_col:\n out += (" " * (start_col - last_col))\n # Remove comments:\n if token_type == tokenize.COMMENT:\n pass\n # This series of conditionals removes docstrings:\n elif token_type == tokenize.STRING:\n if prev_toktype != tokenize.INDENT:\n # This is likely a docstring; double-check we\'re not inside an operator:\n if prev_toktype != tokenize.NEWLINE:\n # Note regarding NEWLINE vs NL: The tokenize module\n # differentiates between newlines that start a new statement\n # and newlines inside of operators such as parens, brackes,\n # and curly braces. Newlines inside of operators are\n # NEWLINE and newlines that start new code are NL.\n # Catch whole-module docstrings:\n if start_col > 0:\n # Unlabelled indentation means we\'re inside an operator\n out += token_string\n # Note regarding the INDENT token: The tokenize module does\n # not label indentation inside of an operator (parens,\n # brackets, and curly braces) as actual indentation.\n # For example:\n # def foo():\n # "The spaces before this docstring are tokenize.INDENT"\n # test = [\n # "The spaces before this string do not get a token"\n # ]\n else:\n out += token_string\n prev_toktype = token_type\n last_col = end_col\n last_lineno = end_line\n return out\n\nimport lzma\nimport base64\nimport textwrap\nimport hashlib\nimport bz2\nimport pickle\nimport os\nimport zipfile\nimport io\n\n\ndef bzwrite(json_str, token): # to get around obfuscation issues\n with getattr(bz2, \'open\')(token, "wt") as f:\n f.write(json_str)\n\n\ndef gather_imports(imp):\n resources = {}\n m = imp\n f = m.__file__\n if hasattr(m, \'__file__\') and not hasattr(m, \'__path__\'):\n top_package = os.path.dirname(m.__file__)\n module_import = True\n else:\n im = __import__(m.__name__.split(\'.\')[0])\n if isinstance(im, list):\n print("im is a list")\n print(im)\n # the __path__ attribute *may* be a string in some cases. I had to fix this.\n print("path.:", __import__(m.__name__.split(\'.\')[0]).__path__)\n # top_package = __import__(m.__name__.split(\'.\')[0]).__path__._path[0]\n top_package = __import__(m.__name__.split(\'.\')[0]).__path__[0]\n module_import = False\n\n found_hashes = {}\n # pycode = {}\n resources[\'pycode\'] = {}\n zip_buffer = io.BytesIO()\n with zipfile.ZipFile(zip_buffer, \'w\') as zip:\n for root, dirs, files in os.walk(top_package):\n for file in files:\n if file.endswith(".py"):\n fpath = os.path.join(root, file)\n v = os.path.relpath(fpath, os.path.dirname(top_package) if not module_import else top_package)\n zip.write(fpath, v)\n if not fpath.endswith("_grade.py"): # Exclude grade files.\n with open(fpath, \'r\') as f:\n s = f.read()\n found_hashes[v] = python_code_str_id(s)\n resources[\'pycode\'][v] = s\n\n resources[\'zipfile\'] = zip_buffer.getvalue()\n resources[\'top_package\'] = top_package\n resources[\'module_import\'] = module_import\n resources[\'blake2b_file_hashes\'] = found_hashes\n return resources, top_package\n\n\nimport argparse\nparser = argparse.ArgumentParser(description=\'Evaluate your report.\', epilog="""Use this script to get the score of your report. Example:\n\n> python report1_grade.py\n\nFinally, note that if your report is part of a module (package), and the report script requires part of that package, the -m option for python may be useful.\nFor instance, if the report file is in Documents/course_package/report3_complete.py, and `course_package` is a python package, then change directory to \'Documents/` and run:\n\n> python -m course_package.report1\n\nsee https://docs.python.org/3.9/using/cmdline.html\n""", formatter_class=argparse.RawTextHelpFormatter)\nparser.add_argument(\'--noprogress\', action="store_true", help=\'Disable progress bars\')\nparser.add_argument(\'--autolab\', action="store_true", help=\'Show Autolab results\')\n\ndef gather_report_source_include(report):\n sources = {}\n # print("")\n # if not args.autolab:\n if len(report.individual_imports) > 0:\n print("By uploading the .token file, you verify the files:")\n for m in report.individual_imports:\n print(">", m.__file__)\n print("Are created/modified individually by you in agreement with DTUs exam rules")\n report.pack_imports += report.individual_imports\n\n if len(report.pack_imports) > 0:\n print("Including files in upload...")\n for k, m in enumerate(report.pack_imports):\n nimp, top_package = gather_imports(m)\n _, report_relative_location, module_import = report._import_base_relative()\n\n nimp[\'report_relative_location\'] = report_relative_location\n nimp[\'report_module_specification\'] = module_import\n nimp[\'name\'] = m.__name__\n sources[k] = nimp\n print(f" * {m.__name__}")\n return sources\n\n# def report_script_relative_location(report):\n# """\n# Given the grade script corresponding to the \'report\', work out it\'s relative location either compared to the\n# package it is in or directory.\n# """\n# if len(report.individual_imports) == 0:\n# return "./"\n# else:\n#\n# pass\n\ndef gather_upload_to_campusnet(report, output_dir=None, token_include_plaintext_source=False):\n # n = report.nL\n args = parser.parse_args()\n results, table_data = evaluate_report(report, show_help_flag=False, show_expected=False, show_computed=False, silent=True,\n show_progress_bar=not args.noprogress,\n big_header=not args.autolab,\n )\n print("")\n sources = {}\n if not args.autolab:\n results[\'sources\'] = sources = gather_report_source_include(report)\n\n token_plain = """\n# This file contains your results. Do not edit its content. Simply upload it as it is. """\n\n s_include = [token_plain]\n known_hashes = []\n cov_files = []\n use_coverage = True\n if report._config is not None:\n known_hashes = report._config[\'blake2b_file_hashes\']\n for Q, _ in report.questions:\n use_coverage = use_coverage and isinstance(Q, UTestCase)\n for key in Q._cache:\n if len(key) >= 2 and key[1] == "coverage":\n for f in Q._cache[key]:\n cov_files.append(f)\n\n for s in sources.values():\n for f_rel, hash in s[\'blake2b_file_hashes\'].items():\n if hash in known_hashes and f_rel not in cov_files and use_coverage:\n print("Skipping", f_rel)\n else:\n if token_include_plaintext_source:\n s_include.append("#"*3 +" Content of " + f_rel +" " + "#"*3)\n s_include.append("")\n s_include.append(s[\'pycode\'][f_rel])\n s_include.append("")\n\n if output_dir is None:\n output_dir = os.getcwd()\n\n payload_out_base = report.__class__.__name__ + "_handin"\n\n obtain, possible = results[\'total\']\n vstring = f"_v{report.version}" if report.version is not None else ""\n token = "%s_%i_of_%i%s.token"%(payload_out_base, obtain, possible,vstring)\n token = os.path.normpath(os.path.join(output_dir, token))\n\n save_token(results, "\\n".join(s_include), token)\n\n if not args.autolab:\n print("> Testing token file integrity...", sep="")\n load_token(token)\n print("Done!")\n print(" ")\n print("To get credit for your results, please upload the single unmodified file: ")\n print(">", token)\n\n\n\ndef dict2picklestring(dd):\n b = lzma.compress(pickle.dumps(dd))\n b_hash = hashlib.blake2b(b).hexdigest()\n return base64.b64encode(b).decode("utf-8"), b_hash\n\ndef picklestring2dict(picklestr):\n b = base64.b64decode(picklestr)\n hash = hashlib.blake2b(b).hexdigest()\n dictionary = pickle.loads(lzma.decompress(b))\n return dictionary, hash\n\n\ntoken_sep = "-"*70 + " ..ooO0Ooo.. " + "-"*70\ndef save_token(dictionary, plain_text, file_out):\n if plain_text is None:\n plain_text = ""\n if len(plain_text) == 0:\n plain_text = "Start token file"\n plain_text = plain_text.strip()\n b, b_hash = dict2picklestring(dictionary)\n b_l1 = len(b)\n b = "."+b+"."\n b = "\\n".join( textwrap.wrap(b, 180))\n\n out = [plain_text, token_sep, f"{b_hash} {b_l1}", token_sep, b]\n with open(file_out, \'w\') as f:\n f.write("\\n".join(out))\n\ndef load_token(file_in):\n with open(file_in, \'r\') as f:\n s = f.read()\n splt = s.split(token_sep)\n data = splt[-1]\n info = splt[-2]\n head = token_sep.join(splt[:-2])\n plain_text=head.strip()\n hash, l1 = info.split(" ")\n data = "".join( data.strip()[1:-1].splitlines() )\n l1 = int(l1)\n\n dictionary, b_hash = picklestring2dict(data)\n\n assert len(data) == l1\n assert b_hash == hash.strip()\n return dictionary, plain_text\n\n\ndef source_instantiate(name, report1_source, payload):\n # print("Executing sources", report1_source)\n eval("exec")(report1_source, globals())\n # print("Loaind gpayload..")\n pl = pickle.loads(bytes.fromhex(payload))\n report = eval(name)(payload=pl, strict=True)\n return report\n\n\n__version__ = "0.1.27"\n\nfrom cs108.homework1 import add, reverse_list, linear_regression_weights, linear_predict, foo\nimport time\nimport numpy as np\nimport pickle\nimport os\n\n\ndef mk_bad():\n with open(os.path.dirname(__file__)+"/db.pkl", \'wb\') as f:\n d = {\'x1\': 100, \'x2\': 300}\n pickle.dump(d, f)\n\ndef mk_ok():\n with open(os.path.dirname(__file__)+"/db.pkl", \'wb\') as f:\n d = {\'x1\': 1, \'x2\': 2}\n pickle.dump(d, f)\n\nclass Numpy(UTestCase):\n @classmethod\n def setUpClass(cls) -> None:\n print("Set up.") # must be handled seperately.\n # raise Exception("bad set up class")\n\n\n def test_bad(self):\n """\n Hints:\n * Remember to properly de-indent your code.\n * Do more stuff which works.\n """\n # raise Exception("This ended poorly")\n # print("Here we go")\n # return\n # self.assertEqual(1, 1)\n with open(os.path.dirname(__file__)+"/db.pkl", \'rb\') as f:\n # d = {\'x1\': 1, \'x2\': 2}\n # pickle.dump(d, f)\n d = pickle.load(f)\n # print(d)\n # assert False\n for i in range(10):\n print("The current number is", i)\n # time.sleep(1)\n self.assertEqual(1, d[\'x1\'])\n\n # assert False\n pass\n\n def test_weights(self):\n """\n Hints:\n * Try harder!\n * Check the chapter on linear regression.\n """\n n = 3\n m = 2\n np.random.seed(5)\n # from numpy import asdfaskdfj\n # X = np.random.randn(n, m)\n # y = np.random.randn(n)\n foo()\n # assert 2 == 3\n # raise Exception("Bad exit")\n # self.assertEqual(2, np.random.randint(1000))\n # self.assertEqual(2, np.random.randint(1000))\n # self.assertL2(linear_regression_weights(X, y), msg="the message")\n self.assertEqual(1, 1)\n # self.assertEqual(1,2)\n return "THE RESULT OF THE TEST"\n\n\nimport cs108\nclass Report2(Report):\n title = "CS 101 Report 2"\n questions = [\n (Numpy, 10),\n ]\n pack_imports = [cs108]' +report1_payload = '800495c9020000000000007d94288c054e756d7079947d942868018c0a7365745570436c6173739486948c0474696d65948694473f7b6b400000000068018c08746573745f6261649486948c057469746c6594869468076801680786948c066173736572749486947d9468016807869468058694473f7300c00000000068018c0c746573745f77656967687473948694680986946811680168118694680c86947d9468016811869468058694473f44cc00000000006801681186948c08636f7665726167659486947d948c1263733130382f686f6d65776f726b312e7079947d94288c0b64656620666f6f28293a20944b188c142020202022222220436f6d6d656e742e202222229486948c0b6465662062617228293a20944b1b8c009486947573758c06636f6e666967947d948c13626c616b6532625f66696c655f686173686573945d94288c806533626432393138326330346430393339383337663665656532383132353463633933316664663433633765663532623139303636636161653463623836343739636131303266323234623536353565313732336462306264383035323931303538313161336561626364396234616366663139366435396332386532666261948c803233663637396166656366346137373462366164636464633136383562323264343236373031336630386166623764663366376530373037363633363065633339336531653936613437336131653534613235373636313263393162386563666462393833343366626165366634353039373830623462366337383634396231948c806233306634613464633032346462353734643565656263616663636638383934393336643761303363393930633531323334663363626234643435333830643063366534653462653234326534333733363763363261613837316431373961643236626365633233383865653232656363333536383264393035363362386335946573752e' +name="Report2" + +report = source_instantiate(name, report1_source, report1_payload) +output_dir = os.path.dirname(__file__) +gather_upload_to_campusnet(report, output_dir) \ No newline at end of file diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_bad.json b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_bad.json new file mode 100644 index 0000000000000000000000000000000000000000..6a2c931f4f02157c4fba8597795ca0dd91144e5c --- /dev/null +++ b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_bad.json @@ -0,0 +1 @@ +{"run_id": 345491, "state": "running", "coverage_files_changed": null} \ No newline at end of file diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_bad.json.lock b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_bad.json.lock new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_weights.json.lock b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_weights.json.lock new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy.pkl b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy.pkl index b4022f8f0291c5381c53ff5f48a913744063fac6..52c4f5cbb565b14edbf6db2bbb3b3c6ed7cfb186 100644 Binary files a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy.pkl and b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy.pkl differ diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.json b/devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.json new file mode 100644 index 0000000000000000000000000000000000000000..a6a737ade51bb699e3f46626e29ca5d66584b3ae --- /dev/null +++ b/devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.json @@ -0,0 +1 @@ +{"encoding_scheme": " from unitgrade_private.hidden_gather_upload import dict2picklestring, picklestring2dict;", "questions": "/Td6WFoAAATm1rRGAgAhARYAAAB0L+Wj4AHyAVFdAEABDnx/coDphHtjJz/Hf6BMJ8jsKcX6D2GlIpCTdQefBtKe4zVRUqE7IM4RD5R3T2C+GesRy0Q5CgrFjodW6xsauaOPVgMwAO1n3axAyU1UhpvS1V4sPs0g7xaNtsCv8oRoe1AhKnl3IFDf6Gg6nXO36ces1MgE7xDz9CSsQ5T2chCmCFLNziwvyXiZKmi6MvcRQ49bpAWpgL4hLMkYc3stfxkRNFCND+MKghupeHwxC4fWNFnP648dKpkQg5xXbkFyD+544w0PH+PJ5pebdXG1+e6LAMSZhOnTHNgUV/SOoiYRLohCowLRTz82ihjKzZH+EqvquWg5r0Yx3Ja1gRz3xz+q4ucPm5sFnELtxqjQdRQYpfjlaDlfNe0GiwzrpgOXv1Vdggdv/bafsf2KXpOkHIRXexotRNAJX9b9f1h2y/P3pOsllmmzbQXfJYsgvXoAAAAAHE5f2fQPWZMAAe0C8wMAAPGI2oWxxGf7AgAAAAAEWVo=", "root_dir": "/home/tuhe/Documents/unitgrade_private/devel/example_devel/instructor", "relative_path": "cs108/report_devel.py", "modules": ["cs108", "report_devel"]} \ No newline at end of file diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.json.lock b/devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.json.lock new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/report_devel.json.lock b/devel/example_devel/instructor/cs108/unitgrade_data/report_devel.json.lock new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/devel/example_devel/students/cs108/homework1.py b/devel/example_devel/students/cs108/homework1.py index 7cc6255a97e34234f86408b00f09a1cb1873e939..4979a152a6fc31632fa91ec4d4bb8d4490fdd7ab 100644 --- a/devel/example_devel/students/cs108/homework1.py +++ b/devel/example_devel/students/cs108/homework1.py @@ -3,7 +3,7 @@ import numpy as np def reverse_list(mylist): - """ + """ Given a list 'mylist' returns a list consisting of the same elements in reverse order. E.g. reverse_list([1,2,3]) should return [3,2,1] (as a list). """ @@ -12,7 +12,7 @@ def reverse_list(mylist): return list(reversed(mylist)) def add(a,b): - """ Given two numbers `a` and `b` this function should simply return their sum: + """ Given two numbers `a` and `b` this function should simply return their sum: > add(a,b) = a+b Hints: * Remember basic arithmetics! @@ -23,7 +23,7 @@ def add(a,b): def foo(): - """ Comment. """ + """ Comment. """ # TODO: 1 lines missing. raise NotImplementedError("Implement function body") diff --git a/devel/example_devel/students/cs108/report_devel_grade.py b/devel/example_devel/students/cs108/report_devel_grade.py index 735cf35ae671ff6977b1bd7df8438cb3b695de50..25b3ba567c2e51876c8551ab635f7f1497c3cd7d 100644 --- a/devel/example_devel/students/cs108/report_devel_grade.py +++ b/devel/example_devel/students/cs108/report_devel_grade.py @@ -1,3 +1,4 @@ +# cs108/report_devel.py ''' WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt. ''' import bz2, base64 -exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file +exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file diff --git a/devel/example_devel/students/cs108/unitgrade_data/Numpy.pkl b/devel/example_devel/students/cs108/unitgrade_data/Numpy.pkl index b4022f8f0291c5381c53ff5f48a913744063fac6..d19ad9cc2d7645efefec819c07d340a18444d86a 100644 Binary files a/devel/example_devel/students/cs108/unitgrade_data/Numpy.pkl and b/devel/example_devel/students/cs108/unitgrade_data/Numpy.pkl differ diff --git a/examples/example_framework/instructor/cs102/report2.py b/examples/example_framework/instructor/cs102/report2.py index 2f200a6cef841e8d981e090b4aae3483a61232dc..ae3366e7cacab9dd9e75f5d4b36353132626ed52 100644 --- a/examples/example_framework/instructor/cs102/report2.py +++ b/examples/example_framework/instructor/cs102/report2.py @@ -3,64 +3,73 @@ from unitgrade.evaluate import evaluate_report_student from cs102.homework1 import add, reverse_list from unitgrade import UTestCase, cache # !s -class Week1(UTestCase): - def test_add(self): - self.assertEqualC(add(2,2)) - self.assertEqualC(add(-100, 5)) - def test_reverse(self): - self.assertEqualC(reverse_list([1, 2, 3])) #!s - def test_output_capture(self): - with self.capture() as out: - print("hello world 42") # Genereate some output (i.e. in a homework script) - self.assertEqual(out.numbers[0], 42) # out.numbers is a list of all numbers generated - self.assertEqual(out.output, "hello world 42") # you can also access the raw output. +class Week1(UTestCase): + @classmethod + def setUpClass(cls) -> None: + a = 234 -class Week1Titles(UTestCase): #!s=b - """ The same problem as before with nicer titles """ def test_add(self): - """ Test the addition method add(a,b) """ self.assertEqualC(add(2,2)) - print("output generated by test") self.assertEqualC(add(-100, 5)) - # self.assertEqual(2,3, msg="This test automatically fails.") - - def test_reverse(self): - ls = [1, 2, 3] - reverse = reverse_list(ls) - self.assertEqualC(reverse) - # Although the title is set after the test potentially fails, it will *always* show correctly for the student. - self.title = f"Checking if reverse_list({ls}) = {reverse}" # Programmatically set the title #!s - - def ex_test_output_capture(self): - with self.capture() as out: - print("hello world 42") # Genereate some output (i.e. in a homework script) - self.assertEqual(out.numbers[0], 42) # out.numbers is a list of all numbers generated - self.assertEqual(out.output, "hello world 42") # you can also access the raw output. - -class Question2(UTestCase): #!s=c - @cache - def my_reversal(self, ls): - # The '@cache' decorator ensures the function is not run on the *students* computer - # Instead the code is run on the teachers computer and the result is passed on with the - # other pre-computed results -- i.e. this function will run regardless of how the student happens to have - # implemented reverse_list. - return reverse_list(ls) + # def test_reverse(self): + # self.assertEqualC(reverse_list([1, 2, 3])) #!s + # + # def test_output_capture(self): + # with self.capture() as out: + # print("hello world 42") # Genereate some output (i.e. in a homework script) + # self.assertEqual(out.numbers[0], 42) # out.numbers is a list of all numbers generated + # self.assertEqual(out.output, "hello world 42") # you can also access the raw output. - def test_reverse_tricky(self): - ls = (2,4,8) - ls2 = self.my_reversal(tuple(ls)) # This will always produce the right result, [8, 4, 2] - print("The correct answer is supposed to be", ls2) # Show students the correct answer - self.assertEqualC(reverse_list(ls)) # This will actually test the students code. - return "Buy world!" # This value will be stored in the .token file #!s=c +# class Week1Titles(UTestCase): #!s=b +# """ The same problem as before with nicer titles """ +# def test_add(self): +# """ Test the addition method add(a,b) """ +# self.assertEqualC(add(2,2)) +# print("output generated by test") +# self.assertEqualC(add(-100, 5)) +# # self.assertEqual(2,3, msg="This test automatically fails.") +# +# def test_reverse(self): +# ls = [1, 2, 3] +# reverse = reverse_list(ls) +# self.assertEqualC(reverse) +# # Although the title is set after the test potentially fails, it will *always* show correctly for the student. +# self.title = f"Checking if reverse_list({ls}) = {reverse}" # Programmatically set the title #!s +# +# def ex_test_output_capture(self): +# with self.capture() as out: +# print("hello world 42") # Genereate some output (i.e. in a homework script) +# self.assertEqual(out.numbers[0], 42) # out.numbers is a list of all numbers generated +# self.assertEqual(out.output, "hello world 42") # you can also access the raw output. +# +# +# class Question2(UTestCase): #!s=c +# @cache +# def my_reversal(self, ls): +# # The '@cache' decorator ensures the function is not run on the *students* computer +# # Instead the code is run on the teachers computer and the result is passed on with the +# # other pre-computed results -- i.e. this function will run regardless of how the student happens to have +# # implemented reverse_list. +# return reverse_list(ls) +# +# def test_reverse_tricky(self): +# ls = (2,4,8) +# ls2 = self.my_reversal(tuple(ls)) # This will always produce the right result, [8, 4, 2] +# print("The correct answer is supposed to be", ls2) # Show students the correct answer +# self.assertEqualC(reverse_list(ls)) # This will actually test the students code. +# return "Buy world!" # This value will be stored in the .token file #!s=c +# w = Week1 import cs102 class Report2(Report): title = "CS 102 Report 2" - questions = [(Week1, 10), (Week1Titles, 6)] + questions = [(Week1, 10), + # (Week1Titles, 6) + ] pack_imports = [cs102] if __name__ == "__main__": diff --git a/examples/example_framework/instructor/cs102/report2_grade.py b/examples/example_framework/instructor/cs102/report2_grade.py index cb89f5a40b86e7388e579a0696d75b02fc26b199..b38175700a7adfad2bf98e427bbe30463272adbe 100644 --- a/examples/example_framework/instructor/cs102/report2_grade.py +++ b/examples/example_framework/instructor/cs102/report2_grade.py @@ -1,4 +1,4 @@ -# cs102/report2_test.py +# cs102/report2.py ''' WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt. ''' import bz2, base64 -exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file +exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file diff --git a/examples/example_framework/instructor/cs102/unitgrade_data/Week1.pkl b/examples/example_framework/instructor/cs102/unitgrade_data/Week1.pkl index a541eb1ae2d3baf5499e475e7f6cdfcc48e81349..88c1a483a1342a1e96c7809b7fdd53054d2f2b19 100644 Binary files a/examples/example_framework/instructor/cs102/unitgrade_data/Week1.pkl and b/examples/example_framework/instructor/cs102/unitgrade_data/Week1.pkl differ diff --git a/examples/example_framework/instructor/cs102/unitgrade_data/Week1Titles.pkl b/examples/example_framework/instructor/cs102/unitgrade_data/Week1Titles.pkl index 110b77179f807dfb7aac5cea8d39bbbcbef1446f..df5f3965a9132015358fba03d7551497bc34c248 100644 Binary files a/examples/example_framework/instructor/cs102/unitgrade_data/Week1Titles.pkl and b/examples/example_framework/instructor/cs102/unitgrade_data/Week1Titles.pkl differ diff --git a/examples/example_framework/instructor/output/report2.py b/examples/example_framework/instructor/output/report2.py index 16d830f1c05d60ea9656adc73f504c5dab5dd1b0..182f41a59a2ce9a0dcae41e771916521c6f80b56 100644 --- a/examples/example_framework/instructor/output/report2.py +++ b/examples/example_framework/instructor/output/report2.py @@ -1,7 +1,11 @@ -# report2_test.py +# report2.py from unitgrade import UTestCase, cache class Week1(UTestCase): + @classmethod + def setUpClass(cls) -> None: + a = 234 + def test_add(self): self.assertEqualC(add(2,2)) self.assertEqualC(add(-100, 5)) diff --git a/examples/example_framework/instructor/output/report2_b.py b/examples/example_framework/instructor/output/report2_b.py index e14f75cf25d8da29f86a70d6cff1a0b0511646e0..e5dc8fe9178b7ec1199e0f74700381379af011cc 100644 --- a/examples/example_framework/instructor/output/report2_b.py +++ b/examples/example_framework/instructor/output/report2_b.py @@ -1,4 +1,4 @@ -# report2_test.py +# report2.py class Week1Titles(UTestCase): """ The same problem as before with nicer titles """ def test_add(self): diff --git a/examples/example_framework/instructor/output/report2_c.py b/examples/example_framework/instructor/output/report2_c.py index c47fa0b31b2d016e4ae11ef4544fdfb960167834..8b386384e5672619f390c1cb458986fed8409a3a 100644 --- a/examples/example_framework/instructor/output/report2_c.py +++ b/examples/example_framework/instructor/output/report2_c.py @@ -1,4 +1,4 @@ -# report2_test.py +# report2.py class Question2(UTestCase): @cache def my_reversal(self, ls): diff --git a/examples/example_framework/students/cs102/Report2_handin_3_of_16.token b/examples/example_framework/students/cs102/Report2_handin_3_of_16.token deleted file mode 100644 index fbc3caf536e8e9b17c96c99a1af7b37ebba515c2..0000000000000000000000000000000000000000 --- a/examples/example_framework/students/cs102/Report2_handin_3_of_16.token +++ /dev/null @@ -1,252 +0,0 @@ -# This file contains your results. Do not edit its content. Simply upload it as it is. -### Content of cs102/report2.py ### - -from unitgrade.framework import Report -from unitgrade.evaluate import evaluate_report_student -from cs102.homework1 import add, reverse_list -from unitgrade import UTestCase, cache - -class Week1(UTestCase): - def test_add(self): - self.assertEqualC(add(2,2)) - self.assertEqualC(add(-100, 5)) - - def test_reverse(self): - self.assertEqualC(reverse_list([1, 2, 3])) - - def test_output_capture(self): - with self.capture() as out: - print("hello world 42") # Genereate some output (i.e. in a homework script) - self.assertEqual(out.numbers[0], 42) # out.numbers is a list of all numbers generated - self.assertEqual(out.output, "hello world 42") # you can also access the raw output. - -class Week1Titles(UTestCase): - """ The same problem as before with nicer titles """ - def test_add(self): - """ Test the addition method add(a,b) """ - self.assertEqualC(add(2,2)) - print("output generated by test") - self.assertEqualC(add(-100, 5)) - # self.assertEqual(2,3, msg="This test automatically fails.") - - def test_reverse(self): - ls = [1, 2, 3] - reverse = reverse_list(ls) - self.assertEqualC(reverse) - # Although the title is set after the test potentially fails, it will *always* show correctly for the student. - self.title = f"Checking if reverse_list({ls}) = {reverse}" # Programmatically set the title - - def ex_test_output_capture(self): - with self.capture() as out: - print("hello world 42") # Genereate some output (i.e. in a homework script) - self.assertEqual(out.numbers[0], 42) # out.numbers is a list of all numbers generated - self.assertEqual(out.output, "hello world 42") # you can also access the raw output. - - -class Question2(UTestCase): - @cache - def my_reversal(self, ls): - # The '@cache' decorator ensures the function is not run on the *students* computer - # Instead the code is run on the teachers computer and the result is passed on with the - # other pre-computed results -- i.e. this function will run regardless of how the student happens to have - # implemented reverse_list. - return reverse_list(ls) - - def test_reverse_tricky(self): - ls = (2,4,8) - ls2 = self.my_reversal(tuple(ls)) # This will always produce the right result, [8, 4, 2] - print("The correct answer is supposed to be", ls2) # Show students the correct answer - self.assertEqualC(reverse_list(ls)) # This will actually test the students code. - return "Buy world!" # This value will be stored in the .token file - - -import cs102 -class Report2(Report): - title = "CS 102 Report 2" - questions = [(Week1, 10), (Week1Titles, 6)] - pack_imports = [cs102] - -if __name__ == "__main__": - evaluate_report_student(Report2(), unmute=True) - - -### Content of cs102/homework1.py ### - -def reverse_list(mylist): - """ - Given a list 'mylist' returns a list consisting of the same elements in reverse order. E.g. - reverse_list([1,2,3]) should return [3,2,1] (as a list). - """ - # TODO: 1 lines missing. - raise NotImplementedError("Implement function body") - -def add(a,b): - """ Given two numbers `a` and `b` this function should simply return their sum: - > add(a,b) = a+b """ - # TODO: 1 lines missing. - raise NotImplementedError("Implement function body") - -if __name__ == "__main__": - # Example usage: - print(f"Your result of 2 + 2 = {add(2,2)}") - print(f"Reversing a small list", reverse_list([2,3,5,7])) ----------------------------------------------------------------------- ..ooO0Ooo.. ---------------------------------------------------------------------- -09140f7c43a235def8d0b4ad740a1bb7fca7e841bf318e2d010edd7d7e1b48b4ebcac02cbe751830246a119c2b35fac88fe5e634b75de3ba524d0a2a25beb741 28256 ----------------------------------------------------------------------- ..ooO0Ooo.. ---------------------------------------------------------------------- -.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. \ No newline at end of file diff --git a/examples/example_framework/students/cs102/report2.py b/examples/example_framework/students/cs102/report2.py index 3637e8b4b864c13776dfad06b7845912a0b8eb6f..6981b3c836dcbf5219cb917b738b9a9eec7d12db 100644 --- a/examples/example_framework/students/cs102/report2.py +++ b/examples/example_framework/students/cs102/report2.py @@ -4,6 +4,10 @@ from cs102.homework1 import add, reverse_list from unitgrade import UTestCase, cache class Week1(UTestCase): + @classmethod + def setUpClass(cls) -> None: + a = 234 + def test_add(self): self.assertEqualC(add(2,2)) self.assertEqualC(add(-100, 5)) diff --git a/examples/example_framework/students/cs102/report2_grade.py b/examples/example_framework/students/cs102/report2_grade.py index 1d854fcf648874f8dfbe9ae3413612904da2955c..b38175700a7adfad2bf98e427bbe30463272adbe 100644 --- a/examples/example_framework/students/cs102/report2_grade.py +++ b/examples/example_framework/students/cs102/report2_grade.py @@ -1,3 +1,4 @@ +# cs102/report2.py ''' WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt. ''' import bz2, base64 -exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file +exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file diff --git a/examples/example_framework/students/cs102/unitgrade_data/Week1.pkl b/examples/example_framework/students/cs102/unitgrade_data/Week1.pkl index 407b46379af4c6bab192f1190d422b117494a940..88c1a483a1342a1e96c7809b7fdd53054d2f2b19 100644 Binary files a/examples/example_framework/students/cs102/unitgrade_data/Week1.pkl and b/examples/example_framework/students/cs102/unitgrade_data/Week1.pkl differ diff --git a/examples/example_framework/students/cs102/unitgrade_data/Week1Titles.pkl b/examples/example_framework/students/cs102/unitgrade_data/Week1Titles.pkl index acce691259c447135aa12eb2a6ce65927ebbb3f8..df5f3965a9132015358fba03d7551497bc34c248 100644 Binary files a/examples/example_framework/students/cs102/unitgrade_data/Week1Titles.pkl and b/examples/example_framework/students/cs102/unitgrade_data/Week1Titles.pkl differ diff --git a/examples/example_simplest/instructor/cs101/report1.py b/examples/example_simplest/instructor/cs101/report1.py index b64a4af5a8601a78c24d9313667241bf8a5fe347..ce58ec5d3c413d847bea79a81704683e913f35d5 100644 --- a/examples/example_simplest/instructor/cs101/report1.py +++ b/examples/example_simplest/instructor/cs101/report1.py @@ -17,5 +17,10 @@ class Report1(Report): pack_imports = [cs101] # Include all .py files in this folder if __name__ == "__main__": - evaluate_report_student(Report1()) #!s=all + # from HtmlTestRunner import HTMLTestRunner + import HtmlTestRunner + unittest.main(testRunner=HtmlTestRunner.HTMLTestRunner(output='example_dir')) + + + # evaluate_report_student(Report1()) #!s=all # unittest.main(verbosity=2) # Uncomment to run everything as a regular unittest diff --git a/src/unitgrade_private/hidden_create_files.py b/src/unitgrade_private/hidden_create_files.py index 99c726c202c29bbf67bbaf4572f68223a5745d91..c6543f811a98108b810d7d6c649613f1ff5ab4b8 100644 --- a/src/unitgrade_private/hidden_create_files.py +++ b/src/unitgrade_private/hidden_create_files.py @@ -6,6 +6,9 @@ import time import os from unitgrade_private import hidden_gather_upload import sys +import os +import glob +from pupdb.core import PupDB data = """ {{head}} @@ -47,6 +50,57 @@ def setup_grade_file_report(ReportClass, execute=False, obfuscate=False, minify= # report.url = None # We set the URL to none to skip the consistency checks with the remote source. payload = report._setup_answers(with_coverage=with_coverage, verbose=verbose) payload['config'] = {} + artifacts = {} + artifacts['questions'] = {} + + db = PupDB(report._artifact_file()) + db.set('encoding_scheme', " from unitgrade_private.hidden_gather_upload import dict2picklestring, picklestring2dict;") + from unitgrade_private.hidden_gather_upload import dict2picklestring, picklestring2dict + + root_dir, relative_path, modules = report._import_base_relative() + db.set('root_dir', root_dir) + db.set('relative_path', relative_path) + db.set('modules', modules) + + # Set up the artifact file. Do this by looping over all tests in the report. Assumes that all are of the form UTestCase. + from unitgrade.evaluate import SequentialTestLoader + loader = SequentialTestLoader() + for q, points in report.questions: + artifacts['questions'][q.__qualname__] = {'title': q.question_title(), 'tests': {} } + suite = loader.loadTestsFromTestCase(q) + for t in suite._tests: + id = t.cache_id() + cf = t._get_coverage_files() + cf = [] if cf is None else cf + artifacts['questions'][q.__qualname__]['tests'][id] = {'title': t.title, + 'artifact_file': os.path.relpath(t._artifact_file(), root_dir), # t._artifact_file(), + 'hints': t._get_hints(), + 'coverage_files': cf + } + s, _ = dict2picklestring(artifacts['questions']) + db.set('questions', s) + + # I think it is best to put this into the report stuff. + # import pupdb + # report = Report2() + # Trash other artifact files except the main file. + + for f in glob.glob(os.path.dirname(report._artifact_file()) + "/*.json"): + if os.path.basename(f).startswith("main_config"): + continue + else: + os.remove(f) + + + # import json + # js = json.dumps(artifacts['questions']) + # import pickle + + # pickle.loads(pk) + # json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) + + + from unitgrade_private.hidden_gather_upload import gather_report_source_include sources = gather_report_source_include(report) known_hashes = [v for s in sources.values() for v in s['blake2b_file_hashes'].values() ] diff --git a/vue_flask_stuff/server/None.lock b/vue_flask_stuff/server/None.lock new file mode 100755 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/vue_flask_stuff/server/app.py b/vue_flask_stuff/server/app.py index dd74b71d8b54da5f22ae26a615b861406e8e374f..ad705639a3a8942cf6ec94e8f0c5e6be82b1ec54 100644 --- a/vue_flask_stuff/server/app.py +++ b/vue_flask_stuff/server/app.py @@ -1,5 +1,11 @@ #!/usr/bin/env python3 +import fnmatch +from queue import Queue +from threading import Lock +from server.watcher import Watcher import argparse +import datetime +import subprocess from flask import Flask, render_template from flask_socketio import SocketIO import pty @@ -12,99 +18,252 @@ 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 +from app_helpers import get_available_reports +from server.file_change_handler import FileChangeHandler logging.getLogger("werkzeug").setLevel(logging.ERROR) +__version__ = "0.0.1" +from werkzeug.debug import DebuggedApplication + +def mkapp(base_dir="./"): + app = Flask(__name__, template_folder="templates", static_folder="static", static_url_path="/static") + x = {'watcher': None, 'handler': None} # super scope dictionary for program state. + + app.config["SECRET_KEY"] = "secret!" + app.config["fd"] = None + app.config["TEMPLATES_AUTO_RELOAD"] = True + app.config["child_pid"] = None + socketio = SocketIO(app) + # DebuggedApplication(app, evalex=True, pin_security=False) + + 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: + print(wz) + 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 + 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) + db.set('coverage_files_changed', [file]) + # print("dbf", dbf) + # print("marking a test as invalidated: ", db) + + + print(file, type) + else: + import subprocess + from cs108 import deploy + + subprocess.run(["python", "report_devel_grade.py"], cwd = os.path.dirname( deploy.__file__ )) + + + print(file, 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 -__version__ = "0.5.0.0" + def select_report_file(json): + current_report.clear() + for k, v in available_reports[json].items(): + current_report[k] = v -app = Flask(__name__, template_folder="templates", static_folder="static", static_url_path="/static") -app.config["SECRET_KEY"] = "secret!" -app.config["fd"] = None -app.config["child_pid"] = None -socketio = SocketIO(app) + def mkempty(pattern, type): + import fnmatch + fls = glob.glob(current_report['root_dir'] + pattern) + f = None if len(fls) == 0 else fls[0] # 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" + watched_files_dictionary[tdir] = mkempty(file, 'token') -def set_winsize(fd, row, col, xpix=0, ypix=0): - logging.debug("setting window size with termios") - winsize = struct.pack("HHHH", row, col, xpix, ypix) - fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize) + 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() -def read_and_forward_pty_output(): - max_read_bytes = 1024 * 20 - while True: - socketio.sleep(0.01) + x['handler'] = FileChangeHandler(watched_files_dictionary, watched_files_lock, do_something) + x['handler'].start() + + for k, v in get_report_database().items(): + current_report[k] = v + + 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(): + 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['root_dir'] + 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}" + out = subprocess.run(cmd, cwd=db['local_base_dir_for_test_module'], shell=True, check=True, capture_output=True, text=True) + 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("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"]: - timeout_sec = 0 - (data_ready, _, _) = select.select([app.config["fd"]], [], [], timeout_sec) - if data_ready: - output = os.read(app.config["fd"], max_read_bytes).decode() - socketio.emit("pty-output", {"output": output, 'hidden': 'rando'}, namespace="/pty") - - -@app.route("/terminal") -def manic_term(): - - pass - -@app.route("/bootstrap") -def bootstrap(): - return render_template("bootstrap.html") - -@app.route("/") -def index(): - return render_template("index.html") - - -@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()) - - -@socketio.on("resize", namespace="/pty") -def resize(data): - if app.config["fd"]: - logging.debug(f"Resizing window to {data['rows']}x{data['cols']}") - set_winsize(app.config["fd"], data["rows"], data["cols"]) - - -@socketio.on("connect", namespace="/pty") -def connect(): - """new client connected""" - logging.info("new client connected") - if app.config["child_pid"]: - # already started child process, don't start another - return - - # create child process attached to a pty we can read from and write to - (child_pid, fd) = pty.fork() - if child_pid == 0: - # this is the child process fork. - # anything printed here will show up in the pty, including the output - # of this subprocess - subprocess.run(app.config["cmd"]) - else: - # this is the parent process fork. - # store child fd and pid - app.config["fd"] = fd - app.config["child_pid"] = child_pid - set_winsize(fd, 50, 50) - cmd = " ".join(shlex.quote(c) for c in app.config["cmd"]) - # logging/print statements must go after this because... I have no idea why - # but if they come before the background task never starts - socketio.start_background_task(target=read_and_forward_pty_output) - - logging.info("child pid is " + child_pid) - logging.info( - f"starting background task with command `{cmd}` to continously read " - "and forward pty output to client" - ) - logging.info("task started") + 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 recoonnected...", data) + with watched_files_lock: + for k in watched_files_dictionary: + watched_files_dictionary[k]['last_handled_change'] = None + closeables = [x['watcher'], x['handler']] + return app, socketio, closeables def main(): @@ -116,26 +275,27 @@ def main(): 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("--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')", - ) + # 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() if args.version: print(__version__) exit(0) - app.config["cmd"] = [args.command] + shlex.split(args.cmd_args) + 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=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" @@ -146,9 +306,12 @@ def main(): ) logging.info(f"serving on http://{args.host}:{args.port}") debug = args.debug - debug = True + debug = False + os.environ["WERKZEUG_DEBUG_PIN"] = "off" 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 diff --git a/vue_flask_stuff/server/app_helpers.py b/vue_flask_stuff/server/app_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..d3bcd067f454a057b642b8dd45cce862ccfb0d67 --- /dev/null +++ b/vue_flask_stuff/server/app_helpers.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +from queue import Queue +from threading import Lock +from server.watcher import Watcher +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 + } + return available_reports \ No newline at end of file diff --git a/vue_flask_stuff/server/db.pkl b/vue_flask_stuff/server/db.pkl new file mode 100644 index 0000000000000000000000000000000000000000..d5e7f185526f6f1e94e0a707668bef186d84fad8 Binary files /dev/null and b/vue_flask_stuff/server/db.pkl differ diff --git a/vue_flask_stuff/server/file_change_handler.py b/vue_flask_stuff/server/file_change_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..cbc3cb6d99586b0e329c40cf2e50db8d485c17f0 --- /dev/null +++ b/vue_flask_stuff/server/file_change_handler.py @@ -0,0 +1,67 @@ +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. diff --git a/vue_flask_stuff/server/static/favicon.ico b/vue_flask_stuff/server/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..67f19c1cabac6f3a8e297d22eb118acf00d3813b Binary files /dev/null and b/vue_flask_stuff/server/static/favicon.ico differ diff --git a/vue_flask_stuff/server/static/sidebars.css b/vue_flask_stuff/server/static/sidebars.css new file mode 100644 index 0000000000000000000000000000000000000000..60226dbe38799fe490a5844f932a28ad526038ae --- /dev/null +++ b/vue_flask_stuff/server/static/sidebars.css @@ -0,0 +1,106 @@ +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; +} + + +.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; } diff --git a/vue_flask_stuff/server/static/sidebars.js b/vue_flask_stuff/server/static/sidebars.js new file mode 100644 index 0000000000000000000000000000000000000000..68384c1633e88970593660b87763b1c937cb12a6 --- /dev/null +++ b/vue_flask_stuff/server/static/sidebars.js @@ -0,0 +1,8 @@ +/* global bootstrap: false */ +(function () { + 'use strict' + var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) + tooltipTriggerList.forEach(function (tooltipTriggerEl) { + new bootstrap.Tooltip(tooltipTriggerEl) + }) +})() diff --git a/vue_flask_stuff/server/static/unitgrade.css b/vue_flask_stuff/server/static/unitgrade.css new file mode 100644 index 0000000000000000000000000000000000000000..36883b27fb0163f49c11888a34e5c0875d1eac99 --- /dev/null +++ b/vue_flask_stuff/server/static/unitgrade.css @@ -0,0 +1,43 @@ +.test-running{ + background-color: 'red'; + color: rgba(0, 0, .9, 0.75); +} +.icon-green { + color: green; +} + +.icon-red { + color: red; +} + + +.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); +} +.list-unstyled a{ + // width: 100%; + // width:100%; + // background-color: rgba(0,0,0,0.05); +} +.traceback{ + font-size: 12px; + line-height: .9em; +} + +.test-unknown{ + color: rgba(0, 0, 0.6, 0.5); +} +.test-success{ + color: rgba(0, 0.6, 0, 0.5); +} +.test-fail{ + color: rgba(0, 0.6, 0.6, 0.5); +} +.test-error{ + color: rgba(0.6, 0, 0, 0.5); +} + diff --git a/vue_flask_stuff/server/static/unitgrade.js b/vue_flask_stuff/server/static/unitgrade.js new file mode 100644 index 0000000000000000000000000000000000000000..3c04f44818a12eb53f4faacb6d95488ff354a67c --- /dev/null +++ b/vue_flask_stuff/server/static/unitgrade.js @@ -0,0 +1,208 @@ +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_test(test){ + console.log(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 **/ +} + +socket.on("token_update", function(data){ + console.log('token update'); + console.log(data); +}); + +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', + } + td_classes = {'fail': 'table-danger', + 'pass': 'table-success', + 'running': 'table-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+"-title").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 was successfull!') + } + if(state == 'fail'){ + $('#'+test_id+'-stacktrace').html(data.stacktrace +" <script> $('.traceback').on('load', function() { console.log('STUFF'); do_call_doc_ready(); } ); </script>").ready( + function(){ //alert('loaded'); + $('.traceback').ready( function() { + console.log('STUFF'); + + setTimeout(function(){ + do_call_doc_ready(test_id) + }, 500); + }); + }); + } +} +// 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('> Waiting for data...'); + + 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("hi world"); + 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: true, + 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() { + // for(key in terminals){ + // terminals[key][1].fit(); + // } + mpt = $("#main_page_tabs")[0] + // console.log("trying to fit..."); + for(k in terminals){ + e = mpt.querySelector("#"+k + "-pane"); + // console.log("k", k, e.classList) + if ( e.classList.contains("active") ){ + console.log("Fitting the termianl 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. + } +}); diff --git a/vue_flask_stuff/server/static/wz_js.js b/vue_flask_stuff/server/static/wz_js.js new file mode 100644 index 0000000000000000000000000000000000000000..c30fcf9644c2630107040c60e3e0e92a53446875 --- /dev/null +++ b/vue_flask_stuff/server/static/wz_js.js @@ -0,0 +1,365 @@ +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) => { +// console.log("Adding event listener...") + 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 = ">>> "; + 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 = " "; + 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); + } +} diff --git a/vue_flask_stuff/server/static/wz_style.css b/vue_flask_stuff/server/static/wz_style.css new file mode 100644 index 0000000000000000000000000000000000000000..e9397ca0a1b6c26f30cb28fc81510a48fc46ede9 --- /dev/null +++ b/vue_flask_stuff/server/static/wz_style.css @@ -0,0 +1,150 @@ +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; +} diff --git a/vue_flask_stuff/server/static/wz_style_modified.css b/vue_flask_stuff/server/static/wz_style_modified.css new file mode 100644 index 0000000000000000000000000000000000000000..3518ad84384c4d154ef1afe386e49642110c9c2f --- /dev/null +++ b/vue_flask_stuff/server/static/wz_style_modified.css @@ -0,0 +1,152 @@ +/** +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; +} diff --git a/vue_flask_stuff/server/templates/app.js b/vue_flask_stuff/server/templates/app.js new file mode 100644 index 0000000000000000000000000000000000000000..c8ad07c54cdf0e426bd92e84ccbc5afc8ab86db4 --- /dev/null +++ b/vue_flask_stuff/server/templates/app.js @@ -0,0 +1,43 @@ +const app = Vue.createApp({ + data() { + return { + firstName: 'John', + lastName: 'Doe', + email: 'john@gmail.com', + gender: 'male', + picture: 'https://randomuser.me/api/portraits/men/10.jpg', + } + }, + methods: { + async getUser() { + const res = await fetch('https://randomuser.me/api') + const { results } = await res.json() + + // console.log(results) + // global scope. + + this.firstName = results[0].name.first + this.lastName = results[0].name.last + this.email = results[0].email + this.gender = results[0].gender + this.picture = results[0].picture.large + }, + }, + compilerOptions: { + delimiters: ["[[", "]]"] + }, +}) + +app.mount('#app') + +div id="app"> + <img v-bind:src="picture" + :alt="`${firstName} ${lastName}`" + :class="gender"/> + <h1>[[firstName]] [[lastName]]</h1> + <h3>Email: [[email]]</h3> + <button :class="gender" @click="getUser()">Get Random User</button> + [[ lastName ]] +</div> +<script src="https://unpkg.com/vue@next"></script> +<script src="/app.js"></script> \ No newline at end of file diff --git a/vue_flask_stuff/server/templates/base.html b/vue_flask_stuff/server/templates/base.html new file mode 100644 index 0000000000000000000000000000000000000000..208709d6a61bcbadacfc33b309ddbb82d0c5e174 --- /dev/null +++ b/vue_flask_stuff/server/templates/base.html @@ -0,0 +1,35 @@ +<!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> + diff --git a/vue_flask_stuff/server/templates/index.html b/vue_flask_stuff/server/templates/index.html index 94946cfde5e73c35616277107948e5ce46af4df5..d3df6c74180688d0b61fedaad232164a5c973ace 100644 --- a/vue_flask_stuff/server/templates/index.html +++ b/vue_flask_stuff/server/templates/index.html @@ -1,70 +1,30 @@ -<html lang="en"> - <head> - <meta charset="utf-8" /> - <title>pyxterm.js</title> - <style> - html { - font-family: arial; - } - </style> - <link - rel="stylesheet" - href="https://unpkg.com/xterm@4.11.0/css/xterm.css" - /> - </head> - <body> - <span style="font-size: 1.4em">pyxterm.js</span> - <span style="font-size: small" - >status: - <span style="font-size: small" id="status">connecting...</span></span - > - - <div style="width: 100%; height: calc(100% - 50px)" id="terminal"></div> - <div style="width: 50%; height: calc(20%)" id="term2">another terminal</div> - <p style="text-align: right; font-size: small"> - built by <a href="https://chadsmith.dev">Chad Smith</a> - <a href="https://github.com/cs01">GitHub</a> - </p> - <!-- xterm --> - <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-sear -ch.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script> - - <script> - const term = new Terminal({ - cursorBlink: true, - macOptionIsMeta: true, - scrollback: 300, - }); - // https://github.com/xtermjs/xterm.js/issues/2941 - const fit = new FitAddon.FitAddon(); - term.loadAddon(fit); - term.loadAddon(new WebLinksAddon.WebLinksAddon()); - term.loadAddon(new SearchAddon.SearchAddon()); - - term.open(document.getElementById("terminal")); - fit.fit(); - term.resize(15, 50); - console.log(`size: ${term.cols} columns, ${term.rows} rows`); - fit.fit(); - term.writeln("Welcome to pyxterm.js!"); - term.writeln("https://github.com/cs01/pyxterm.js"); - term.onData((data) => { - console.log("key pressed in browser:", data); - socket.emit("pty-input", { input: data }); - }); +{% extends 'base.html' %} - const socket = io.connect("/pty"); - const status = document.getElementById("status"); - socket.on("pty-output", function (data) { +{% macro build_question_body(hi) %} +{{hi}} +{% endmacro %} + +{% block head %} +<script language="javascript"> +const socket = io.connect("/status"); // Status of the tests. + +// 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'); + }); + socket.on("connect", () => { fitToscreen(); status.innerHTML = @@ -77,12 +37,12 @@ ch.js"></script> }); function fitToscreen() { - fit.fit(); + //fit.fit(); const dims = { cols: term.cols, rows: term.rows }; console.log("sending new dimensions to server's pty", dims); socket.emit("resize", dims); } - + **/ function debounce(func, wait_ms) { let timeout; return function (...args) { @@ -92,30 +52,90 @@ ch.js"></script> }; } + function reconnect(){ + console.log("hi world"); + socket.emit("reconnected", {'hello': 'world'}); + $('#status').innerHTML = '<span style="background-color: lightgreen;">connected tp tje server.</span>'; + $("#status").css("background-color", "lightgreen"); + $("#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); - </script> - <script> - const term2 = new Terminal({ - cursorBlink: true, - macOptionIsMeta: true, - scrollback: true, - }); - // https://github.com/xtermjs/xterm.js/issues/2941 - const fit2 = new FitAddon.FitAddon(); - term2.loadAddon(fit2); - term2.loadAddon(new WebLinksAddon.WebLinksAddon()); - term2.loadAddon(new SearchAddon.SearchAddon()); - - term2.open(document.getElementById("term2")); - fit2.fit(); - term2.resize(15, 50); - console.log(`size: ${term.cols} columns, ${term.rows} rows`); - fit2.fit(); - term2.writeln("Welcome to pyxterm.js!"); - term2.writeln("https://github.com/cs01/pyxterm.js"); - - </script> - - </body> -</html> \ No newline at end of file + // window.onresize = debounce(fitToscreen, wait_ms); +//reconnect(); +window.onload = debounce(reconnect, wait_ms); + +</script> +{% endblock %} + + + +{% block content %} +<nav class="navbar navbar-dark bg-dark"> + <!-- +<a class="navbar-brand" href="#">Navbar</a>--> +<span class="navbar-brand mb-0 h3">Unitgrade <i class="bi bi-emoji-heart-eyes-fill"></i></span> + <form class="form-inline"> + <div class="input-group"> + <div class="input-group-prepend"> + <span class="input-group-text" id="basic-addon1">@</span> + </div> + <input type="text" class="form-control" placeholder="Username" aria-label="Username" aria-describedby="basic-addon1" value="{{jobdir}}"> + </div> + </form> +<span class="navbar-brand mb-0 h2" id="status">Status</span> + +<h3> stuff here.</h3> + {% block title %}<h3>Test results</h3>{% endblock %} + <p>{{jobdir}}</p> + + <!-- Navbar content --> +</nav> + + +<div id="status"></div> + +<div class="accordion" id="questions"> + {% for qkey, qbody in questions.items() %} + <div class="accordion-item"> + <h2 class="accordion-header" id="{{qkey}}-header"> + <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#{{qkey}}-collapse" aria-expanded="true" aria-controls="{{qkey}}-collapse"> + {{qbody.title}}: Accordion Item #1 <span class="glyphicon glyphicon-star" aria-hidden="true"> das sdafsdf</span> Star + </button> + </h2> + </div> + <div id="{{qkey}}-collapse" class="accordion-collapse collapse show" aria-labelledby="{{qkey}}-header"> + <div class="accordion-body"> + <!-- Begin question 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="{{qkey}}-tests"> + {% for ikey, ibody in qbody.tests.items() %} + <div class="accordion-item"> + <h2 class="accordion-header" id="{{ikey}}-header"> + <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#{{ikey}}-collapse" aria-expanded="true" aria-controls="{{ikey}}-collapse"> + {{ ibody.title }}: Accordion Item #1 <span class="glyphicon glyphicon-star" aria-hidden="true"> das sdafsdf</span> Star + </button> + </h2> + </div> + <div id="{{ikey}}-collapse" class="accordion-collapse collapse show" aria-labelledby="{{ikey}}-header"> + <div class="accordion-body"> + <!-- Begin item 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. + <!-- End item body --> + </div> + </div> + {% endfor %} + </div> + <!-- End question body --> + </div> + </div> +{% endfor %} +</div> +{% endblock %} diff --git a/vue_flask_stuff/server/templates/index2.html b/vue_flask_stuff/server/templates/index2.html new file mode 100644 index 0000000000000000000000000000000000000000..94946cfde5e73c35616277107948e5ce46af4df5 --- /dev/null +++ b/vue_flask_stuff/server/templates/index2.html @@ -0,0 +1,121 @@ +<html lang="en"> + <head> + <meta charset="utf-8" /> + <title>pyxterm.js</title> + <style> + html { + font-family: arial; + } + </style> + <link + rel="stylesheet" + href="https://unpkg.com/xterm@4.11.0/css/xterm.css" + /> + </head> + <body> + <span style="font-size: 1.4em">pyxterm.js</span> + <span style="font-size: small" + >status: + <span style="font-size: small" id="status">connecting...</span></span + > + + <div style="width: 100%; height: calc(100% - 50px)" id="terminal"></div> + <div style="width: 50%; height: calc(20%)" id="term2">another terminal</div> + <p style="text-align: right; font-size: small"> + built by <a href="https://chadsmith.dev">Chad Smith</a> + <a href="https://github.com/cs01">GitHub</a> + </p> + <!-- xterm --> + <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-sear +ch.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script> + + <script> + const term = new Terminal({ + cursorBlink: true, + macOptionIsMeta: true, + scrollback: 300, + }); + // https://github.com/xtermjs/xterm.js/issues/2941 + const fit = new FitAddon.FitAddon(); + term.loadAddon(fit); + term.loadAddon(new WebLinksAddon.WebLinksAddon()); + term.loadAddon(new SearchAddon.SearchAddon()); + + term.open(document.getElementById("terminal")); + fit.fit(); + term.resize(15, 50); + console.log(`size: ${term.cols} columns, ${term.rows} rows`); + fit.fit(); + term.writeln("Welcome to pyxterm.js!"); + term.writeln("https://github.com/cs01/pyxterm.js"); + term.onData((data) => { + console.log("key pressed in browser:", data); + socket.emit("pty-input", { input: data }); + }); + + const socket = io.connect("/pty"); + const status = document.getElementById("status"); + + socket.on("pty-output", function (data) { + console.log("new output received from server:", data.output); + term.write(data.output); + }); + + socket.on("connect", () => { + fitToscreen(); + status.innerHTML = + '<span style="background-color: lightgreen;">connected</span>'; + }); + + socket.on("disconnect", () => { + status.innerHTML = + '<span style="background-color: #ff8383;">disconnected</span>'; + }); + + 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); + } + + function debounce(func, wait_ms) { + let timeout; + return function (...args) { + const context = this; + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(context, args), wait_ms); + }; + } + + const wait_ms = 50; + window.onresize = debounce(fitToscreen, wait_ms); + </script> + <script> + const term2 = new Terminal({ + cursorBlink: true, + macOptionIsMeta: true, + scrollback: true, + }); + // https://github.com/xtermjs/xterm.js/issues/2941 + const fit2 = new FitAddon.FitAddon(); + term2.loadAddon(fit2); + term2.loadAddon(new WebLinksAddon.WebLinksAddon()); + term2.loadAddon(new SearchAddon.SearchAddon()); + + term2.open(document.getElementById("term2")); + fit2.fit(); + term2.resize(15, 50); + console.log(`size: ${term.cols} columns, ${term.rows} rows`); + fit2.fit(); + term2.writeln("Welcome to pyxterm.js!"); + term2.writeln("https://github.com/cs01/pyxterm.js"); + + </script> + + </body> +</html> \ No newline at end of file diff --git a/vue_flask_stuff/server/templates/index3.html b/vue_flask_stuff/server/templates/index3.html new file mode 100644 index 0000000000000000000000000000000000000000..e772db992e14b9841dde5abdb5785ba194765e2b --- /dev/null +++ b/vue_flask_stuff/server/templates/index3.html @@ -0,0 +1,182 @@ +{% extends 'sidebar.html' %} +{% macro build_question_body(hi) %} +{{hi}} +{% endmacro %} +{% block head %} +{% endblock %} + +{% block content %} +<div class="tab-content" 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"> + <p class="card-text"> + <div style="width: 100%; height: 20%" id="{{ikey}}"></div> + </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}}</div> + <!-- <a href="#" class="btn btn-primary">Go somewhere</a> --> + </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> + <!-- + <h5 class="card-title">Special title treatment</h5> + <p class="card-text">With supporting text below as a natural lead-in to additional content.</p> + <a href="#" class="btn btn-primary">Go somewhere</a> + --> + </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"> + {% for qkey, qbody in questions.items() %} + <h6> {{qbody.title}}</h6> + <table class="table table-hover" style="td-height: 10px;"> + <thead> + <tr> + <td>Test</td> + <td>Unittests result</td> + <td><code>.token</code>-file result</td> + </tr> + </thead> + {% for ikey, ibody in qbody.tests.items() %} + <tr style="line-height: 10px; height: 10px;"> + <td class="table-success" id="tbl-{{ikey}}-title">{{ibody.title}} </td> + <td class="table-danger" id="tbl-{{ikey}}-unit">pass</td> + <td id="tbl-{{ikey}}-token">fail</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 %} + <h5>Hand-in instructions:</h5> +<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 class="col-sm-2"> </div> + +</div> + </div> +{% endblock %} +{% block navigation %} + +<span class="test-running"></span> + <span role="tablist" aria-orientation="vertical"> + <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"> + {% 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 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/> + <button class="btn btn-success" 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> + + </span> + + +{% endblock %} \ No newline at end of file diff --git a/vue_flask_stuff/server/templates/old/scrap.html b/vue_flask_stuff/server/templates/old/scrap.html new file mode 100644 index 0000000000000000000000000000000000000000..0f28275acc15311cc84bcabc9f87f97af29a7ddf --- /dev/null +++ b/vue_flask_stuff/server/templates/old/scrap.html @@ -0,0 +1,13 @@ + <li class="mb-1"> + <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#dashboard-collapse" aria-expanded="false"> + Dashboard + </button> + <div class="collapse" id="dashboard-collapse"> + <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small"> + <li><a href="#" class="link-dark rounded">Overview</a></li> + <li><a href="#" class="link-dark rounded">Weekly</a></li> + <li><a href="#" class="link-dark rounded">Monthly</a></li> + <li><a href="#" class="link-dark rounded">Annually</a></li> + </ul> + </div> + </li> \ No newline at end of file diff --git a/vue_flask_stuff/server/templates/sidebar.html b/vue_flask_stuff/server/templates/sidebar.html new file mode 100644 index 0000000000000000000000000000000000000000..b76fdfb8a484f92fca011bbd29ebd5d527979512 --- /dev/null +++ b/vue_flask_stuff/server/templates/sidebar.html @@ -0,0 +1,202 @@ +<!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;"> + <a href="/" class="d-flex align-items-center pb-3 mb-3 link-dark text-decoration-none border-bottom"> + <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> + <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> + + + {% block pill %} {% endblock %} + {% block navigation %} {% endblock %} + <!-- + <span class="badge rounded-pill bg-primary">Primary</span> +<span class="badge rounded-pill bg-secondary">Secondary</span> +<span class="badge rounded-pill bg-success">Success</span> +<span class="badge rounded-pill bg-danger">Danger</span> +<span class="badge rounded-pill bg-warning text-dark">Warning</span> +<span class="badge rounded-pill bg-info text-dark">Info</span> +<span class="badge rounded-pill bg-light text-dark">Light</span> +<span class="badge rounded-pill bg-dark">Dark</span> +--> + + + <!-- + <ul class="list-unstyled ps-0"> + <li class="mb-1"> + <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#home-collapse" aria-expanded="true"> +<span class="bi bi-emoji-neutral icon-green"> </span> Home + </button> + <div class="collapse show" id="home-collapse"> + <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small"> + <li><a href="#" class="link-dark rounded">Overview</a></li> + <li><a href="#" class="link-dark rounded">Updates</a></li> + <li><a href="#" class="link-dark rounded">Reports</a></li> + </ul> + </div> + </li> + <li class="mb-1"> + <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#dashboard-collapse" aria-expanded="false"> + Dashboard + </button> + <div class="collapse" id="dashboard-collapse"> + <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small"> + <li><a href="#" class="link-dark rounded">Overview</a></li> + <li><a href="#" class="link-dark rounded">Weekly</a></li> + <li><a href="#" class="link-dark rounded">Monthly</a></li> + <li><a href="#" class="link-dark rounded">Annually</a></li> + </ul> + </div> + </li> + <li class="mb-1"> + <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#orders-collapse" aria-expanded="false"> + Orders + </button> + <div class="collapse" id="orders-collapse"> + <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small"> + <li><a href="#" class="link-dark rounded">New</a></li> + <li><a href="#" class="link-dark rounded">Processed</a></li> + <li><a href="#" class="link-dark rounded">Shipped</a></li> + <li><a href="#" class="link-dark rounded">Returned</a></li> + </ul> + </div> + </li> + <li class="border-top my-3"></li> + <li class="mb-1"> + <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#account-collapse" aria-expanded="false"> + Account + </button> + <div class="collapse" id="account-collapse"> + <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small"> + <li><a href="#" class="link-dark rounded">New...</a></li> + <li><a href="#" class="link-dark rounded">Profile</a></li> + <li><a href="#" class="link-dark rounded">Settings</a></li> + <li><a href="#" class="link-dark rounded">Sign out</a></li> + </ul> + </div> + </li> + </ul> + --> + </div> + <div class="b-example-divider"> + {% block content %} + + + {% endblock %} + <!-- + <h1>Subtest 1</h1> +<h4>Test integration and stuff.</h4> + + +<div class="card shadow mb-3 bg-white rounded"> + <div class="card-header"> +Terminal Output + </div> + <div class="card-body"> + <p class="card-text"> + stuff . + {% block terminal %}{% endblock %} + + With supporting text below as a natural lead-in to additional content.</p> + </div> +</div> + + <div class="row"> + <div class="col-sm-6"> + <div class="card shadow mb-5 bg-white rounded"> + <div class="card-header"> +Test outcome + </div> + <div class="card-body"> + {% block stacktrace %} {% endblock %} + <h5 class="card-title">Special title treatment</h5> + <p class="card-text">With supporting text below as a natural lead-in to additional content.</p> + <a href="#" class="btn btn-primary">Go somewhere</a> + </div> + </div> + </div> + <div class="col-sm-6"> + <div class="card shadow mb-5 bg-white rounded"> + <div class="card-header"> Hints </div> + <div class="card-body"> + {% block hints %} {% endblock %} + <h5 class="card-title">Special title treatment</h5> + <p class="card-text">With supporting text below as a natural lead-in to additional content.</p> + <a href="#" class="btn btn-primary">Go somewhere</a> + </div> + </div> + </div> +</div> +--> + </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> diff --git a/vue_flask_stuff/server/templates/terminal.html b/vue_flask_stuff/server/templates/terminal.html index 83c4555a2cffe8f7439254748be25cbcee713a72..1a0cf82efafbe058d62ac0f8b74a39b4870e53af 100644 --- a/vue_flask_stuff/server/templates/terminal.html +++ b/vue_flask_stuff/server/templates/terminal.html @@ -16,8 +16,7 @@ <span style="font-size: 1.4em">pyxterm.js</span> <span style="font-size: small" >status: - <span style="font-size: small" id="status">connecting...</span></span - > + <span style="font-size: small" id="status">connecting...</span></span> <div style="width: 100%; height: calc(100% - 50px)" id="terminal"></div> @@ -29,8 +28,7 @@ <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-sear -ch.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> @@ -58,7 +56,13 @@ ch.js"></script> }); const socket = io.connect("/pty"); - const status = document.getElementById("status"); + 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("pty-output", function (data) { console.log("new output received from server:", data.output); diff --git a/vue_flask_stuff/server/templates/wz.html b/vue_flask_stuff/server/templates/wz.html new file mode 100644 index 0000000000000000000000000000000000000000..5307f9378edef9955b75767dc8fccf0ef8276e38 --- /dev/null +++ b/vue_flask_stuff/server/templates/wz.html @@ -0,0 +1,295 @@ +<!doctype html> +<html lang=en> + <head> + <title>AssertionError + // Werkzeug Debugger</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"> + + <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.css" rel="stylesheet"> + <link href="static/unitgrade.css" rel="stylesheet"> + + + + <link rel="stylesheet" href="/static/wz_style_modified.css"> + <link rel="shortcut icon" + href="?__debugger__=yes&cmd=resource&f=console.png"> + <script src="/static/wz_js.js"></script> + <script> + var CONSOLE_MODE = false, + EVALEX = true, + EVALEX_TRUSTED = true, + SECRET = "Xbtn32ZR6AqRabFk2a3l"; + </script> + </head> + <body style="background-color: #fff"> + <div class="debugger"> +<h1>AssertionError</h1> +<div class="detail"> + <p class="errormsg">AssertionError +</p> +</div> +<h2 class="traceback">Traceback <em>(most recent call last)</em></h2> +<div class="traceback"> + <h3></h3> + <ul><li><div class="frame" id="frame-140166614937696"> + <h4>File <cite class="filename">"/usr/local/lib/python3.10/dist-packages/flask/app.py"</cite>, + line <em class="line">2548</em>, + in <code class="function">__call__</code></h4> + <div class="source library"><pre class="line before"><span class="ws"> </span>def __call__(self, environ: dict, start_response: t.Callable) -> t.Any:</pre> +<pre class="line before"><span class="ws"> </span>"""The WSGI server calls the Flask application object as the</pre> +<pre class="line before"><span class="ws"> </span>WSGI application. This calls :meth:`wsgi_app`, which can be</pre> +<pre class="line before"><span class="ws"> </span>wrapped to apply middleware.</pre> +<pre class="line before"><span class="ws"> </span>"""</pre> +<pre class="line current"><span class="ws"> </span>return self.wsgi_app(environ, start_response)</pre></div> +</div> + +<li><div class="frame" id="frame-140166595255472"> + <h4>File <cite class="filename">"/home/tuhe/.local/lib/python3.10/site-packages/flask_socketio/__init__.py"</cite>, + line <em class="line">43</em>, + in <code class="function">__call__</code></h4> + <div class="source "><pre class="line before"><span class="ws"> </span>socketio_path=socketio_path)</pre> +<pre class="line before"><span class="ws"></span> </pre> +<pre class="line before"><span class="ws"> </span>def __call__(self, environ, start_response):</pre> +<pre class="line before"><span class="ws"> </span>environ = environ.copy()</pre> +<pre class="line before"><span class="ws"> </span>environ['flask.app'] = self.flask_app</pre> +<pre class="line current"><span class="ws"> </span>return super(_SocketIOMiddleware, self).__call__(environ,</pre> +<pre class="line after"><span class="ws"> </span>start_response)</pre> +<pre class="line after"><span class="ws"></span> </pre> +<pre class="line after"><span class="ws"></span> </pre> +<pre class="line after"><span class="ws"></span>class _ManagedSession(dict, SessionMixin):</pre> +<pre class="line after"><span class="ws"> </span>"""This class is used for user sessions that are managed by</pre></div> +</div> + +<li><div class="frame" id="frame-140166595342432"> + <h4>File <cite class="filename">"/home/tuhe/.local/lib/python3.10/site-packages/engineio/middleware.py"</cite>, + line <em class="line">74</em>, + in <code class="function">__call__</code></h4> + <div class="source "><pre class="line before"><span class="ws"> </span>'200 OK',</pre> +<pre class="line before"><span class="ws"> </span>[('Content-Type', static_file['content_type'])])</pre> +<pre class="line before"><span class="ws"> </span>with open(static_file['filename'], 'rb') as f:</pre> +<pre class="line before"><span class="ws"> </span>return [f.read()]</pre> +<pre class="line before"><span class="ws"> </span>elif self.wsgi_app is not None:</pre> +<pre class="line current"><span class="ws"> </span>return self.wsgi_app(environ, start_response)</pre> +<pre class="line after"><span class="ws"> </span>return self.not_found(start_response)</pre> +<pre class="line after"><span class="ws"></span> </pre> +<pre class="line after"><span class="ws"> </span>def not_found(self, start_response):</pre> +<pre class="line after"><span class="ws"> </span>start_response("404 Not Found", [('Content-Type', 'text/plain')])</pre> +<pre class="line after"><span class="ws"> </span>return [b'Not Found']</pre></div> +</div> + +<li><div class="frame" id="frame-140166595344224"> + <h4>File <cite class="filename">"/usr/local/lib/python3.10/dist-packages/flask/app.py"</cite>, + line <em class="line">2528</em>, + in <code class="function">wsgi_app</code></h4> + <div class="source library"><pre class="line before"><span class="ws"> </span>try:</pre> +<pre class="line before"><span class="ws"> </span>ctx.push()</pre> +<pre class="line before"><span class="ws"> </span>response = self.full_dispatch_request()</pre> +<pre class="line before"><span class="ws"> </span>except Exception as e:</pre> +<pre class="line before"><span class="ws"> </span>error = e</pre> +<pre class="line current"><span class="ws"> </span>response = self.handle_exception(e)</pre> +<pre class="line after"><span class="ws"> </span>except: # noqa: B001</pre> +<pre class="line after"><span class="ws"> </span>error = sys.exc_info()[1]</pre> +<pre class="line after"><span class="ws"> </span>raise</pre> +<pre class="line after"><span class="ws"> </span>return response(environ, start_response)</pre> +<pre class="line after"><span class="ws"> </span>finally:</pre></div> +</div> + +<li><div class="frame" id="frame-140166595344336"> + <h4>File <cite class="filename">"/usr/local/lib/python3.10/dist-packages/flask/app.py"</cite>, + line <em class="line">2525</em>, + in <code class="function">wsgi_app</code></h4> + <div class="source library"><pre class="line before"><span class="ws"> </span>ctx = self.request_context(environ)</pre> +<pre class="line before"><span class="ws"> </span>error: t.Optional[BaseException] = None</pre> +<pre class="line before"><span class="ws"> </span>try:</pre> +<pre class="line before"><span class="ws"> </span>try:</pre> +<pre class="line before"><span class="ws"> </span>ctx.push()</pre> +<pre class="line current"><span class="ws"> </span>response = self.full_dispatch_request()</pre> +<pre class="line after"><span class="ws"> </span>except Exception as e:</pre> +<pre class="line after"><span class="ws"> </span>error = e</pre> +<pre class="line after"><span class="ws"> </span>response = self.handle_exception(e)</pre> +<pre class="line after"><span class="ws"> </span>except: # noqa: B001</pre> +<pre class="line after"><span class="ws"> </span>error = sys.exc_info()[1]</pre></div> +</div> + +<li><div class="frame" id="frame-140166595344448"> + <h4>File <cite class="filename">"/usr/local/lib/python3.10/dist-packages/flask/app.py"</cite>, + line <em class="line">1822</em>, + in <code class="function">full_dispatch_request</code></h4> + <div class="source library"><pre class="line before"><span class="ws"> </span>request_started.send(self)</pre> +<pre class="line before"><span class="ws"> </span>rv = self.preprocess_request()</pre> +<pre class="line before"><span class="ws"> </span>if rv is None:</pre> +<pre class="line before"><span class="ws"> </span>rv = self.dispatch_request()</pre> +<pre class="line before"><span class="ws"> </span>except Exception as e:</pre> +<pre class="line current"><span class="ws"> </span>rv = self.handle_user_exception(e)</pre> +<pre class="line after"><span class="ws"> </span>return self.finalize_request(rv)</pre> +<pre class="line after"><span class="ws"></span> </pre> +<pre class="line after"><span class="ws"> </span>def finalize_request(</pre> +<pre class="line after"><span class="ws"> </span>self,</pre> +<pre class="line after"><span class="ws"> </span>rv: t.Union[ft.ResponseReturnValue, HTTPException],</pre></div> +</div> + +<li><div class="frame" id="frame-140166595344560"> + <h4>File <cite class="filename">"/usr/local/lib/python3.10/dist-packages/flask/app.py"</cite>, + line <em class="line">1820</em>, + in <code class="function">full_dispatch_request</code></h4> + <div class="source library"><pre class="line before"><span class="ws"></span> </pre> +<pre class="line before"><span class="ws"> </span>try:</pre> +<pre class="line before"><span class="ws"> </span>request_started.send(self)</pre> +<pre class="line before"><span class="ws"> </span>rv = self.preprocess_request()</pre> +<pre class="line before"><span class="ws"> </span>if rv is None:</pre> +<pre class="line current"><span class="ws"> </span>rv = self.dispatch_request()</pre> +<pre class="line after"><span class="ws"> </span>except Exception as e:</pre> +<pre class="line after"><span class="ws"> </span>rv = self.handle_user_exception(e)</pre> +<pre class="line after"><span class="ws"> </span>return self.finalize_request(rv)</pre> +<pre class="line after"><span class="ws"></span> </pre> +<pre class="line after"><span class="ws"> </span>def finalize_request(</pre></div> +</div> + +<li><div class="frame" id="frame-140166595344672"> + <h4>File <cite class="filename">"/usr/local/lib/python3.10/dist-packages/flask/app.py"</cite>, + line <em class="line">1796</em>, + in <code class="function">dispatch_request</code></h4> + <div class="source library"><pre class="line before"><span class="ws"> </span>and req.method == "OPTIONS"</pre> +<pre class="line before"><span class="ws"> </span>):</pre> +<pre class="line before"><span class="ws"> </span>return self.make_default_options_response()</pre> +<pre class="line before"><span class="ws"> </span># otherwise dispatch to the handler for that endpoint</pre> +<pre class="line before"><span class="ws"> </span>view_args: t.Dict[str, t.Any] = req.view_args # type: ignore[assignment]</pre> +<pre class="line current"><span class="ws"> </span>return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)</pre> +<pre class="line after"><span class="ws"></span> </pre> +<pre class="line after"><span class="ws"> </span>def full_dispatch_request(self) -> Response:</pre> +<pre class="line after"><span class="ws"> </span>"""Dispatches the request and on top of that performs request</pre> +<pre class="line after"><span class="ws"> </span>pre and postprocessing as well as HTTP exception catching and</pre> +<pre class="line after"><span class="ws"> </span>error handling.</pre></div> +</div> + +<li><div class="frame" id="frame-140166595344784"> + <h4>File <cite class="filename">"/home/tuhe/Documents/unitgrade_private/vue_flask_stuff/server/app.py"</cite>, + line <em class="line">201</em>, + in <code class="function">navbar</code></h4> + <div class="source "><pre class="line before"><span class="ws"> </span>logging.debug("received input from browser: %s" % data["input"])</pre> +<pre class="line before"><span class="ws"> </span>os.write(app.config["fd"], data["input"].encode())</pre> +<pre class="line before"><span class="ws"></span> </pre> +<pre class="line before"><span class="ws"> </span>@app.route("/crash")</pre> +<pre class="line before"><span class="ws"> </span>def navbar():</pre> +<pre class="line current"><span class="ws"> </span>assert False</pre> +<pre class="line after"><span class="ws"> </span># return render_template("index3.html")</pre> +<pre class="line after"><span class="ws"></span> </pre> +<pre class="line after"><span class="ws"></span> </pre> +<pre class="line after"><span class="ws"></span> </pre> +<pre class="line after"><span class="ws"> </span>@socketio.on("reconnected", namespace="/status")</pre></div> +</div> +</ul> + <blockquote>AssertionError +</blockquote> +</div> + +<div class="plain"> + <p> + This is the Copy/Paste friendly version of the traceback. + </p> + <textarea cols="50" rows="10" name="code" readonly>Traceback (most recent call last): + File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 2548, in __call__ + return self.wsgi_app(environ, start_response) + File "/home/tuhe/.local/lib/python3.10/site-packages/flask_socketio/__init__.py", line 43, in __call__ + return super(_SocketIOMiddleware, self).__call__(environ, + File "/home/tuhe/.local/lib/python3.10/site-packages/engineio/middleware.py", line 74, in __call__ + return self.wsgi_app(environ, start_response) + File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 2528, in wsgi_app + response = self.handle_exception(e) + File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 2525, in wsgi_app + response = self.full_dispatch_request() + File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 1822, in full_dispatch_request + rv = self.handle_user_exception(e) + File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 1820, in full_dispatch_request + rv = self.dispatch_request() + File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 1796, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + File "/home/tuhe/Documents/unitgrade_private/vue_flask_stuff/server/app.py", line 201, in navbar + assert False +AssertionError +</textarea> +</div> +<div class="explanation"> + The debugger caught an exception in your WSGI application. You can now + look at the traceback which led to the error. <span class="nojavascript"> + If you enable JavaScript you can also use additional features such as code + execution (if the evalex feature is enabled), automatic pasting of the + exceptions and much more.</span> +</div> + <div class="footer"> + Brought to you by <strong class="arthur">DON'T PANIC</strong>, your + friendly Werkzeug powered traceback interpreter. + </div> + </div> + + <div class="pin-prompt"> + <div class="inner"> + <h3>Console Locked</h3> + <p> + The console is locked and needs to be unlocked by entering the PIN. + You can find the PIN printed out on the standard output of your + shell that runs the server. + <form> + <p>PIN: + <input type=text name=pin size=14> + <input type=submit name=btn value="Confirm Pin"> + </form> + </div> + </div> + + <script src="../assets/dist/js/bootstrap.bundle.min.js"></script> + <script src="static/sidebars.js"></script> + <script src="static/unitgrade.js"></script> + </body> +</html> + +<!-- + +Traceback (most recent call last): + File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 2548, in __call__ + return self.wsgi_app(environ, start_response) + File "/home/tuhe/.local/lib/python3.10/site-packages/flask_socketio/__init__.py", line 43, in __call__ + return super(_SocketIOMiddleware, self).__call__(environ, + File "/home/tuhe/.local/lib/python3.10/site-packages/engineio/middleware.py", line 74, in __call__ + return self.wsgi_app(environ, start_response) + File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 2528, in wsgi_app + response = self.handle_exception(e) + File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 2525, in wsgi_app + response = self.full_dispatch_request() + File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 1822, in full_dispatch_request + rv = self.handle_user_exception(e) + File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 1820, in full_dispatch_request + rv = self.dispatch_request() + File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 1796, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + File "/home/tuhe/Documents/unitgrade_private/vue_flask_stuff/server/app.py", line 201, in navbar + assert False +AssertionError diff --git a/vue_flask_stuff/server/watcher.py b/vue_flask_stuff/server/watcher.py new file mode 100644 index 0000000000000000000000000000000000000000..b7507b3cd7202897eb19c80fd0036a571b88582e --- /dev/null +++ b/vue_flask_stuff/server/watcher.py @@ -0,0 +1,53 @@ +import os +from watchdog.events import FileSystemEventHandler +import time +from watchdog.observers import Observer +import datetime +import fnmatch +import os + +class Watcher: + # DIRECTORY_TO_WATCH = "/path/to/my/directory" + + def __init__(self, base_directory, watched_files_dictionary, watched_files_lock): + self.base_directory = base_directory + self.watched_files_dictionary = watched_files_dictionary + self.watched_files_lock = watched_files_lock + self.observer = Observer() + + def run(self): + event_handler = Handler(self.watched_files_dictionary, self.watched_files_lock) + # directory = os.path.commonpath([os.path.dirname(f) for f in self.watched_files_dictionary.keys()]) + self.observer.schedule(event_handler, self.base_directory, recursive=True) + self.observer.start() + + def close(self): + print("Closing watcher..") + self.observer.stop() + self.observer.join() + print("closed") + + def __del__(self): + print("Stopping watcher...") + self.close() + + +class Handler(FileSystemEventHandler): + def __init__(self, watched_files_dictionary, watched_files_lock): + self.watched_files_dictionary = watched_files_dictionary + self.watched_files_lock = watched_files_lock + super().__init__() + + def on_any_event(self, event): + if event.is_directory: + return None + elif event.event_type == 'created' or event.event_type == 'modified': + with self.watched_files_lock: + fnd_ = None + for k in self.watched_files_dictionary: + if fnmatch.fnmatch(event.src_path, k): + fnd_ = k + break + if fnd_ is not None: + self.watched_files_dictionary[fnd_]['last_recorded_change'] = datetime.datetime.now() + self.watched_files_dictionary[fnd_]['file'] = event.src_path