summaryrefslogtreecommitdiffstats
path: root/Smarker
diff options
context:
space:
mode:
authorjwansek <eddie.atten.ea29@gmail.com>2022-04-22 16:02:28 +0100
committerjwansek <eddie.atten.ea29@gmail.com>2022-04-22 16:02:28 +0100
commit12ac9bd354c664978523547ffac9bbebf0fcb577 (patch)
treeeb9c3726edfa291d0b6c53cec0f25572946fb887 /Smarker
parent5faf5c34d833f8467bce580e1e249f54856042dc (diff)
downloadSmarker-12ac9bd354c664978523547ffac9bbebf0fcb577.tar.gz
Smarker-12ac9bd354c664978523547ffac9bbebf0fcb577.zip
Moved and adapted to move folder, added rendering to pdfs
Diffstat (limited to 'Smarker')
-rw-r--r--Smarker/database.py97
-rw-r--r--Smarker/jinja_helpers.py137
-rw-r--r--Smarker/mark.py105
-rw-r--r--Smarker/misc_classes.py99
-rw-r--r--Smarker/pytest_template.jinja28
m---------Smarker/python-latex-highlighting0
-rw-r--r--Smarker/reflect.py395
-rw-r--r--Smarker/requirements.txt10
-rw-r--r--Smarker/temp.py6
-rw-r--r--Smarker/templates/markdown.jinja2bin0 -> 26 bytes
-rw-r--r--Smarker/templates/md.jinja2166
-rw-r--r--Smarker/templates/tex.jinja2249
-rw-r--r--Smarker/templates/text.jinja2bin0 -> 28 bytes
-rw-r--r--Smarker/templates/txt.jinja2168
14 files changed, 1440 insertions, 0 deletions
diff --git a/Smarker/database.py b/Smarker/database.py
new file mode 100644
index 0000000..a0e3640
--- /dev/null
+++ b/Smarker/database.py
@@ -0,0 +1,97 @@
+from dataclasses import dataclass
+import pymysql
+
+@dataclass
+class SmarkerDatabase:
+ host:str
+ user:str
+ passwd:str
+ db:str
+ port:int = 3306
+
+ def __enter__(self):
+ try:
+ self.__connection = self.__get_connection()
+ except pymysql.err.OperationalError as e:
+ if e.args[0] == 1049:
+ self.__build_db()
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self.__connection.close()
+
+ def __get_connection(self):
+ return pymysql.connect(
+ host = self.host,
+ port = self.port,
+ user = self.user,
+ passwd = self.passwd,
+ charset = "utf8mb4",
+ database = self.db
+ )
+
+ def __build_db(self):
+ self.__connection = pymysql.connect(
+ host = self.host,
+ port = self.port,
+ user = self.user,
+ passwd = self.passwd,
+ charset = "utf8mb4",
+ )
+ with self.__connection.cursor() as cursor:
+ # unsafe:
+ cursor.execute("CREATE DATABASE %s" % self.db)
+ cursor.execute("USE %s" % self.db)
+ cursor.execute("""
+ CREATE TABLE students(
+ student_no VARCHAR(10) PRIMARY KEY NOT NULL,
+ name TEXT NOT NULL,
+ email VARCHAR(50) NOT NULL
+ );
+ """)
+ cursor.execute("""
+ CREATE TABLE assessment(
+ assessment_name VARCHAR(30) PRIMARY KEY NOT NULL,
+ yaml_path TEXT NOT NULL,
+ num_enrolled INT UNSIGNED NULL
+ );
+ """)
+ cursor.execute("""
+ CREATE TABLE submissions(
+ submission_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ student_no VARCHAR(10) NOT NULL,
+ assessment_name VARCHAR(30) NOT NULL,
+ submission_dt DATETIME NOT NULL default CURRENT_TIMESTAMP,
+ submission_zip_path TEXT NOT NULL,
+ FOREIGN KEY (student_no) REFERENCES students(student_no),
+ FOREIGN KEY (assessment_name) REFERENCES assessment(assessment_name)
+ );
+ """)
+ cursor.execute("""
+ CREATE TABLE assessment_file(
+ file_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ assessment_name VARCHAR(30) NOT NULL,
+ file_name VARCHAR(30) NOT NULL,
+ FOREIGN KEY (assessment_name) REFERENCES assessment(assessment_name)
+ );
+ """)
+ cursor.execute("""
+ CREATE TABLE submitted_files(
+ submission_id INT UNSIGNED NOT NULL,
+ file_id INT UNSIGNED NOT NULL,
+ file_text TEXT NOT NULL,
+ FOREIGN KEY (submission_id) REFERENCES submissions(submission_id),
+ FOREIGN KEY (file_id) REFERENCES assessment_file(file_id),
+ PRIMARY KEY(submission_id, file_id)
+ );
+ """)
+
+
+ self.__connection.commit()
+ return self.__connection
+
+
+ def get_tables(self):
+ with self.__connection.cursor() as cursor:
+ cursor.execute("SHOW TABLES;")
+ return cursor.fetchall()
diff --git a/Smarker/jinja_helpers.py b/Smarker/jinja_helpers.py
new file mode 100644
index 0000000..6d2f34c
--- /dev/null
+++ b/Smarker/jinja_helpers.py
@@ -0,0 +1,137 @@
+"""Functions in this module will be avaliable to call in jinja templates"""
+import subprocess
+import lxml.html
+import datetime
+import tempfile
+import shutil
+import pdfkit
+import yaml
+import json
+import re
+import os
+
+def get_datetime():
+ return str(datetime.datetime.now())
+
+def recurse_class_tree_text(tree, indent = 4):
+ return yaml.dump(tree, indent = indent).replace(": {}", "")
+
+def recurse_class_tree_forest(tree):
+ return re.sub(r"\"|:|\{\}|,", "", json.dumps(tree, indent=4)).replace("{", "[").replace("}", "]")
+
+def junit_xml_to_html(junit_xml, student_id):
+ # setup tempfiles for the junit xml and html
+ with tempfile.NamedTemporaryFile(suffix = ".xml", mode = "w", delete = False) as xml_f:
+ xml_f.write(junit_xml)
+ html_path = os.path.join(tempfile.mkdtemp(), "junit.html")
+
+ # convert the junit xml to html
+ subprocess.run(["junit2html", xml_f.name, html_path])
+
+ # remove the html elements we don't like
+ root = lxml.html.parse(html_path)
+ for toremove in root.xpath("/html/body/h1"):
+ toremove.getparent().remove(toremove)
+ for toremove in root.xpath("/html/body/table"):
+ toremove.getparent().remove(toremove)
+ for toremove in root.xpath("/html/body/p"):
+ toremove.getparent().remove(toremove)
+
+ # convert the html to pdf
+ out_fname = "%s_test_report.pdf" % student_id
+ pdfkit.from_string(lxml.etree.tostring(root).decode(), out_fname)
+
+ # remove the tempfiles
+ # input("%s continue..." % html_path)
+ shutil.rmtree(os.path.split(html_path)[0])
+ os.remove(xml_f.name)
+
+ return out_fname
+
+def flatten_struct(struct):
+ # print("Attempting to flatten: ", struct)
+ out = {}
+ for s in struct:
+ key = list(s.keys())[0]
+ out[key] = s[key]
+ return out
+
+def get_required_num_args(funcname):
+ return int(re.findall(r"(?<=\()(\d+)(?=\))", funcname)[0])
+
+def bool_to_yesno(b:bool):
+ if b:
+ return "YES"
+ else:
+ return "NO"
+
+def bool_to_checkbox(b:bool):
+ if b:
+ return "[x]"
+ else:
+ return "[ ]"
+
+def len_documentation(comments, docs):
+ """This function isn't in jinja"""
+ if comments == "None":
+ commentlen = 0
+ else:
+ commentlen = len(comments)
+
+ if docs == "None":
+ docslen = 0
+ else:
+ docslen = len(docs)
+
+ return commentlen + docslen
+
+def len_(obj):
+ return len(obj)
+
+def get_source_numlines(source):
+ return "%d lines (%d characters)" % (source.count("\n"), len(source))
+
+#https://stackoverflow.com/questions/16259923/how-can-i-escape-latex-special-characters-inside-django-templates
+def tex_escape(text):
+ conv = {
+ '&': r'\&',
+ '%': r'\%',
+ '$': r'\$',
+ '#': r'\#',
+ '_': r'\_',
+ '{': r'\{',
+ '}': r'\}',
+ '~': r'\textasciitilde{}',
+ '^': r'\^{}',
+ '\\': r'\textbackslash{}',
+ '<': r'\textless{}',
+ '>': r'\textgreater{}',
+ }
+ regex = re.compile('|'.join(re.escape(str(key)) for key in sorted(conv.keys(), key = lambda item: - len(item))))
+ # print(text, regex.sub(lambda match: conv[match.group()], text))
+ return regex.sub(lambda match: conv[match.group()], text)
+
+def get_python_latex_highlight_sty_path():
+ return os.path.join(os.path.split(__file__)[0], "python-latex-highlighting", "pythonhighlight")
+
+def _get_helpers():
+ import reflect
+ import os
+
+ r = reflect.Reflect(os.path.split(__file__)[0])
+ r.import_module("jinja_helpers")
+ return {k: v[0] for k, v in r.get_functions("jinja_helpers").items()}
+
+if __name__ == "__main__":
+ # import json
+ # with open("100301654_report.json", "r") as f:
+ # init_struct = json.load(f)["files"]
+
+ # print(flatten_struct(flatten_struct(init_struct)["example.py"]["functions"]))
+
+ print(get_python_latex_highlight_sty_path())
+
+ # print(_get_helpers())
+
+
+ \ No newline at end of file
diff --git a/Smarker/mark.py b/Smarker/mark.py
new file mode 100644
index 0000000..e8070f8
--- /dev/null
+++ b/Smarker/mark.py
@@ -0,0 +1,105 @@
+from dataclasses import dataclass
+import jinja_helpers
+import configparser
+import misc_classes
+import subprocess
+import argparse
+import tempfile
+import zipfile
+import reflect
+import jinja2
+import shutil
+import yaml
+import json
+import os
+
+def main(**kwargs):
+ student_no = os.path.splitext(os.path.split(args["submission"])[-1])[0]
+
+ with misc_classes.ExtractZipToTempDir(args["submission"]) as submission_files:
+ with open(kwargs["assessment"], "r") as f:
+ assessment_struct = yaml.safe_load(f)
+
+ with misc_classes.FileDependencies(assessment_struct):
+ output = reflect.gen_reflection_report(submission_files, assessment_struct, student_no, kwargs, args["submission"])
+ output_file = kwargs["out"]
+
+ if kwargs["format"] == "yaml":
+ strout = yaml.dump(output)
+ elif kwargs["format"] == "json":
+ strout = json.dumps(output, indent = 4)
+ else:
+ fp = os.path.join(os.path.split(__file__)[0], "templates", "%s.jinja2" % kwargs["format"])
+ if kwargs["format"] in ("tex", "pdf"):
+ jinja_template = misc_classes.latex_jinja_env.get_template("tex.jinja2")
+ else:
+ with open(fp, "r") as f:
+ jinja_template = jinja2.Template(f.read())
+
+ strout = jinja_template.render(**output, **jinja_helpers._get_helpers(), **kwargs)
+
+ if output_file == "stdout":
+ print(strout)
+ # input("\n\n[tempdir: %s] Press any key to close..." % tempdir)
+ exit()
+
+ if output_file == "auto":
+ output_file = "%s_report.%s" % (student_no, kwargs["format"])
+
+ with open(output_file, "w") as f:
+ f.write(strout)
+
+ if kwargs["format"] == "pdf":
+ os.environ["TEXINPUTS"] = os.path.join(os.path.split(__file__)[0], "python-latex-highlighting") + ":"
+
+ os.rename(output_file, os.path.splitext(output_file)[0] + ".tex")
+ output_file = os.path.splitext(output_file)[0] + ".tex"
+ subprocess.run(["pdflatex", output_file])
+
+ os.remove(os.path.splitext(output_file)[0] + ".tex")
+ os.remove(os.path.splitext(output_file)[0] + ".log")
+ os.remove(os.path.splitext(output_file)[0] + ".aux")
+
+ # input("\n\n[tempdir: %s] Press any key to close..." % tempdir)
+
+if __name__ == "__main__":
+ config = configparser.ConfigParser()
+ config.read(os.path.join(os.path.split(__file__)[0], "smarker.conf"))
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "-a", "--assessment",
+ help = "Path to an assessment .yml file",
+ type = os.path.abspath,
+ required = True
+ )
+ parser.add_argument(
+ "-s", "--submission",
+ help = "Path to a zip of a student's code",
+ type = os.path.abspath,
+ required = True
+ )
+ parser.add_argument(
+ "-f", "--format",
+ help = "Output format type",
+ type = str,
+ choices = ["yaml", "json", "pdf"] + [os.path.splitext(f)[0] for f in os.listdir(os.path.join(os.path.split(__file__)[0], "templates"))],
+ default = "txt"
+ )
+ parser.add_argument(
+ "-o", "--out",
+ help = "Path to write the output to, or, by default write to stdout. 'auto' automatically generates a file name.",
+ default = "stdout",
+ )
+
+ for section in config.sections():
+ for option in config.options(section):
+ parser.add_argument(
+ "--%s_%s" % (section, option),
+ default = config.get(section, option),
+ help = "Optional argument inherited from config file. Read smarker.conf for details."
+ )
+
+ args = vars(parser.parse_args())
+ main(**args)
+ \ No newline at end of file
diff --git a/Smarker/misc_classes.py b/Smarker/misc_classes.py
new file mode 100644
index 0000000..09c3a7d
--- /dev/null
+++ b/Smarker/misc_classes.py
@@ -0,0 +1,99 @@
+from dataclasses import dataclass
+import tempfile
+import zipfile
+import shutil
+import jinja2
+import os
+
+latex_jinja_env = jinja2.Environment(
+ block_start_string = '((*',
+ block_end_string = '*))',
+ variable_start_string = '(((',
+ variable_end_string = ')))',
+ comment_start_string = "((#",
+ comment_end_string = '#))',
+ line_statement_prefix = '%%',
+ line_comment_prefix = '%#',
+ trim_blocks = True,
+ autoescape = False,
+ loader = jinja2.FileSystemLoader(os.path.abspath(os.path.join(os.path.split(__file__)[0], "templates")))
+)
+
+@dataclass
+class ExtractZipToTempDir(tempfile.TemporaryDirectory):
+ zip_file:str
+
+ def __post_init__(self):
+ super().__init__()
+
+ def __enter__(self):
+ return self.extract()
+
+ def __exit__(self, exc, value, tb):
+ self.cleanup()
+
+ def extract(self):
+ with zipfile.ZipFile(self.zip_file) as z:
+ z.extractall(self.name)
+
+ # some zipping applications make a folder inside the zip with the files in that folder.
+ # try to deal with this here.
+ submission_files = self.name
+ if os.path.isdir(
+ os.path.join(submission_files, os.listdir(submission_files)[0])
+ ) and len(os.listdir(submission_files)) == 1:
+ submission_files = os.path.join(submission_files, os.listdir(submission_files)[0])
+
+ return submission_files
+
+@dataclass
+class FileDependencies:
+ assessment_struct:dict
+ to_:str=str(os.getcwd())
+
+ def __enter__(self):
+ self.get_deps()
+
+ def __exit__(self, type, value, traceback):
+ self.rm_deps()
+
+ def get_deps(self):
+ try:
+ for file_dep in self.assessment_struct["dependencies"]["files"]:
+ if os.path.isfile(file_dep):
+ shutil.copy(file_dep, os.path.join(self.to_, os.path.split(file_dep)[-1]))
+ else:
+ shutil.copytree(file_dep, os.path.join(self.to_, os.path.split(file_dep)[-1]))
+ except KeyError:
+ pass
+
+ def rm_deps(self):
+ stuff_to_remove = []
+ try:
+ stuff_to_remove += [os.path.split(f)[-1] for f in self.assessment_struct["dependencies"]["files"]]
+ except KeyError:
+ pass
+ try:
+ stuff_to_remove += self.assessment_struct["produced_files"]
+ except KeyError:
+ pass
+
+ for file_dep in stuff_to_remove:
+ file_dep = os.path.join(self.to_, file_dep)
+ # print("rm: ", file_dep)
+ if os.path.exists(file_dep):
+ if os.path.isfile(file_dep):
+ os.remove(file_dep)
+ else:
+ shutil.rmtree(file_dep)
+
+@dataclass
+class ChangeDirectory:
+ target:str
+ cwd:str=str(os.getcwd())
+
+ def __enter__(self):
+ os.chdir(self.target)
+
+ def __exit__(self, type, value, traceback):
+ os.chdir(self.cwd)
diff --git a/Smarker/pytest_template.jinja2 b/Smarker/pytest_template.jinja2
new file mode 100644
index 0000000..73c9a40
--- /dev/null
+++ b/Smarker/pytest_template.jinja2
@@ -0,0 +1,8 @@
+{# generating python with python :3 #}
+
+import {{ module }}
+
+{% for i, test_code in enumerate(filestests, 1) %}
+def test_{{ i }}():
+ {{ test_code|indent(4, False) }} {# the code in the config file must be indented with 4 spaces only #}
+{% endfor %} \ No newline at end of file
diff --git a/Smarker/python-latex-highlighting b/Smarker/python-latex-highlighting
new file mode 160000
+Subproject a5b8353876512d8d571a3c3be59452995318a17
diff --git a/Smarker/reflect.py b/Smarker/reflect.py
new file mode 100644
index 0000000..bc5ae0e
--- /dev/null
+++ b/Smarker/reflect.py
@@ -0,0 +1,395 @@
+import xml.etree.ElementTree as etree
+from dataclasses import dataclass
+from functools import reduce
+from operator import getitem
+import jinja_helpers
+import misc_classes
+import subprocess
+import importlib
+import traceback
+import tempfile
+import inspect
+import pkgutil
+import shutil
+import jinja2
+import sys
+import os
+import re
+
+@dataclass
+class Reflect:
+ client_code_path:str
+ imported_modules = {}
+
+ def __post_init__(self):
+ self.client_code_path = os.path.normpath(self.client_code_path)
+ sys.path.insert(1, self.client_code_path)
+ self.client_modules = [p for p in pkgutil.iter_modules() if os.path.normpath(str(p[0])[12:-2]) == self.client_code_path]
+ # print("client moduules ", self.client_modules)
+
+ def import_module(self, module_name):
+ """Imports a module. Before reflection can be conducted, a module
+ must be imported. WARNING: This will execute module code if it isn't
+ in a if __name__ == "__main__". Takes a module name (that the student made)
+ as the first argument.
+
+ Args:
+ module_name (str): The name of a student's module to import
+ """
+ for module in self.client_modules:
+ if module.name == module_name:
+ try:
+ self.imported_modules[module_name] = importlib.import_module(module.name)
+ except ModuleNotFoundError as e:
+ print("Missing library dependency for client module:")
+ print(e)
+ exit()
+ # except Exception as e:
+ # print("CRITICAL ERROR IN CLIENT CODE - CANNOT CONTINUE")
+ # raise ClientCodeException(e)
+
+ def get_module_doc(self, module_name):
+ """Gets the documentation provided for a module.
+
+ Args:
+ module_name (str): The student's module name to get documentation for
+
+ Returns:
+ str: Provided documentation
+ """
+ return {
+ "comments": self.__format_doc(inspect.getcomments(self.imported_modules[module_name])),
+ "doc": self.__format_doc(inspect.getdoc(self.imported_modules[module_name]))
+ }
+
+ def get_classes(self, module_name):
+ """Gets the classes in a given module. The module must be imported first.
+
+ Args:
+ module_name (str): The name of an imported module to get the name of.
+
+ Returns:
+ dict: Dictionary of classes. The name of the class is the index, followed by
+ a tuple containing the class object and the classes' documentation.
+ """
+ return {
+ i[0]: (i[1], {"comments": self.__format_doc(inspect.getcomments(i[1])), "doc": self.__format_doc(inspect.getdoc(i[1]))})
+ for i in inspect.getmembers(self.imported_modules[module_name])
+ if inspect.isclass(i[1]) and self.get_class_full_name(i[1]).split(".")[0] in self.imported_modules.keys()
+ }
+
+ def get_class_methods(self, module_name, class_name):
+ """Gets the user generated methods of a given class. The module must be imported first.
+
+ Args:
+ module_name (str): The name of the module in which the class is contained.
+ class_name (str): The name of the class.
+
+ Returns:
+ dict: A dictionary of the methods. The index is the function name, followed by a tuple
+ containing the function object, the documentation, and the args as a nicely formatted string.
+ """
+ return {
+ i[0]: (
+ i[1],
+ {"comments": self.__format_doc(inspect.getcomments(i[1])), "doc": self.__format_doc(inspect.getdoc(i[1]))},
+ str(inspect.signature(i[1])),
+ inspect.getsource(i[1]).rstrip()
+ )
+ for i in inspect.getmembers(
+ self.get_classes(module_name)[class_name][0],
+ predicate=inspect.isfunction
+ )
+ }
+
+ def get_functions(self, module_name):
+ return {
+ i[0]: (
+ i[1],
+ {"comments": self.__format_doc(inspect.getcomments(i[1])), "doc": self.__format_doc(inspect.getdoc(i[1]))},
+ str(inspect.signature(i[1])),
+ inspect.getsource(i[1]).rstrip()
+ )
+ for i in inspect.getmembers(self.imported_modules[module_name])
+ if inspect.isfunction(i[1])
+ }
+
+ def get_class_full_name(self, class_):
+ """Returns the name of a class object as a nice string. e.g. modulename.classname
+ except if it's a builtin there'll be no module name.
+
+ Args:
+ class_ (class): A class to get the name of
+
+ Returns:
+ str: A nicely formatted class name.
+ """
+ if class_.__module__ in ['builtins', 'exceptions']:
+ return class_.__name__
+ return "%s.%s" % (class_.__module__, class_.__name__)
+
+ # classes that inherit from two classes doesn't print out nicely here.
+ # using this method is better https://pastebin.com/YuxkkTkv
+ def get_class_tree(self):
+ """Generates a dictionary based tree structure showing inheritance of classes
+ of all the *imported modules*. WARNING: It doesn't deal well with multiple inheritance..
+ Read the comments.
+ """
+
+ def expand(a:list):
+ out = []
+ for l in a:
+ for i in reversed(range(0, len(l))):
+ out.append(l[:len(l) - i])
+ return out
+
+ # https://www.geeksforgeeks.org/python-convert-a-list-of-lists-into-tree-like-dict/
+ def getTree(tree, mappings):
+ return reduce(getitem, mappings, tree)
+
+ # https://www.geeksforgeeks.org/python-convert-a-list-of-lists-into-tree-like-dict/
+ def setTree(tree, mappings):
+ getTree(tree, mappings[:-1])[mappings[-1]] = dict()
+
+ unexpanded_class_paths = []
+ for module in self.imported_modules.keys():
+ for class_ in self.get_classes(module).values():
+ unexpanded_class_paths.append([
+ self.get_class_full_name(i)
+ for i in reversed(list(inspect.getmro(class_[0])))
+ ])
+
+ tree = {}
+ added = [] # the expander makes duplicates. keep a list to remove them
+ # sadly a collections.Counter doesnt work with lists of lists
+ for s in expand(unexpanded_class_paths):
+ if s not in added:
+ setTree(tree, [i for i in reversed(s)][::-1])
+ added.append(s)
+
+ # print(tree)
+ # return inspect.getclasstree(classes)
+ return tree
+
+ def run_tests(self, tests, run_colourful = False):
+ """Build and run pytests from the configuration yaml. Indentation needs to
+ be four spaces only otherwise it won't work. We recommend running this last
+ so all modules are already imported.
+
+ Args:
+ tests (dict): dict with the filename as the key followed by a list of
+ python code to make the test
+ run_colourful (bool, optional): Run pytest again, printing out the
+ exact output of pytest as soon as it's ready. Has the advantage that
+ colours are preserved, but is only useful for when the user wants to
+ print out the report to stdout. Defaults to False.
+
+ Returns:
+ [dict]: A dictionary consisting of the pytest output string, junit xml
+ output (which might be useful for rendering nicely in some output formats)
+ and some nice meta information.
+ """
+ test_results = {}
+ test_results["pytest_report"] = ""
+
+ with open(os.path.join(os.path.split(__file__)[0], "pytest_template.jinja2"), "r") as f:
+ jinja_template = jinja2.Template(f.read())
+
+ for filename, filestests in tests.items():
+ with open(os.path.join(self.client_code_path, "test_" + filename), "w") as f:
+ f.write(jinja_template.render(
+ module = os.path.splitext(filename)[0],
+ filestests = filestests,
+ enumerate = enumerate # a function thats needed
+ ))
+
+ with tempfile.TemporaryDirectory() as tmp:
+ junitxmlpath = os.path.join(tmp, "report.xml")
+ test_files = [os.path.join(self.client_code_path, "test_%s" % f) for f in tests.keys()]
+ cmd = ["pytest", "-vv"] + test_files + ["--junitxml=%s" % junitxmlpath]
+ # print("cmd: ", " ".join(cmd))
+ if test_files == []:
+ test_results["pytest_report"] = "*** No Tests ***"
+ return test_results
+ proc = subprocess.Popen(cmd, stdout = subprocess.PIPE)
+ while True:
+ line = proc.stdout.readline()
+ if not line:
+ break
+ test_results["pytest_report"] += line.decode()
+
+ with open(junitxmlpath, "r") as f:
+ test_results["junitxml"] = f.read()
+ root = etree.fromstring(test_results["junitxml"])
+ test_results["meta"] = root.findall("./testsuite")[0].attrib
+
+ if run_colourful:
+ subprocess.run(cmd)
+
+ return test_results
+
+ def __format_doc(*doc):
+ return str(doc[1]).rstrip()
+
+def gen_reflection_report(client_code_path, assessment_struct, student_no, configuration, zip_file):
+ reflection = Reflect(client_code_path)
+ present_module_names = [i.name for i in reflection.client_modules]
+ out = assessment_struct
+ out["student_no"] = student_no
+ tests_to_run = {}
+
+ try:
+ produced_files = assessment_struct["produced_files"]
+ except KeyError:
+ produced_files = []
+
+ for i, required_file in enumerate(assessment_struct["files"], 0):
+ required_file = list(required_file.keys())[0]
+ module_name = os.path.splitext(required_file)[0]
+
+ if module_name in present_module_names:
+ out["files"][i][required_file]["present"] = True
+ else:
+ out["files"][i][required_file]["present"] = False
+ continue
+
+ try:
+ reflection.import_module(module_name)
+ except Exception as e:
+ out["files"][i][required_file]["has_exception"] = True
+ out["files"][i][required_file]["exception"] = {}
+ out["files"][i][required_file]["exception"]["type"] = str(type(e))
+ out["files"][i][required_file]["exception"]["str"] = str(e)
+ # TODO: test this indexing so we can be sure we're getting client code only
+ e_list = traceback.format_exception(None, e, e.__traceback__)
+ out["files"][i][required_file]["exception"]["traceback"] = ''.join([e_list[0]] + e_list[12:])
+
+ continue
+
+ required_files_features = assessment_struct["files"][i][required_file]
+ out["files"][i][required_file]["has_exception"] = False
+ out["files"][i][required_file]["documentation"] = reflection.get_module_doc(module_name)
+ if "classes" in required_files_features.keys():
+
+ present_classes = reflection.get_classes(module_name)
+ for j, class_name in enumerate(required_files_features["classes"], 0):
+ class_name = list(class_name.keys())[0]
+
+ stop_here_flag = False
+ # surprised the yaml parser doesnt do this automatically...
+ if out["files"][i][required_file]["classes"][j][class_name] is None:
+ out["files"][i][required_file]["classes"][j][class_name] = {}
+ stop_here_flag = True
+
+ if class_name in present_classes.keys():
+ out["files"][i][required_file]["classes"][j][class_name]["present"] = True
+ else:
+ out["files"][i][required_file]["classes"][j][class_name]["present"] = False
+ continue
+
+ # print( present_classes[class_name][1])
+ out["files"][i][required_file]["classes"][j][class_name]["documentation"] = present_classes[class_name][1]
+
+ if stop_here_flag:
+ continue
+
+ present_methods = reflection.get_class_methods(module_name, class_name)
+
+ for k, required_method in enumerate(assessment_struct["files"][i][required_file]["classes"][j][class_name]["methods"], 0):
+ out["files"][i][required_file]["classes"][j][class_name]["methods"][k] = {required_method: {}}
+
+ method_name = re.sub(r"\(\d+\)", "", required_method)
+ if method_name in present_methods.keys():
+ out["files"][i][required_file]["classes"][j][class_name]["methods"][k][required_method]["present"] = True
+ else:
+ out["files"][i][required_file]["classes"][j][class_name]["methods"][k][required_method]["present"] = False
+ continue
+
+ out["files"][i][required_file]["classes"][j][class_name]["methods"][k][required_method]["arguments"] = present_methods[method_name][-2]
+ out["files"][i][required_file]["classes"][j][class_name]["methods"][k][required_method]["minimum_arguments"] = present_methods[method_name][-2].count(",") + 1
+ out["files"][i][required_file]["classes"][j][class_name]["methods"][k][required_method]["documentation"] = present_methods[method_name][-3]
+ out["files"][i][required_file]["classes"][j][class_name]["methods"][k][required_method]["source_code"] = present_methods[method_name][-1]
+
+ if "functions" in required_files_features.keys():
+ present_functions = reflection.get_functions(module_name)
+ for j, required_function in enumerate(assessment_struct["files"][i][required_file]["functions"], 0):
+ function_name = re.sub(r"\(\d+\)", "", required_function)
+ out["files"][i][required_file]["functions"][j] = {required_function: {}}
+
+ if function_name in present_functions.keys():
+ out["files"][i][required_file]["functions"][j][required_function]["present"] = True
+ else:
+ out["files"][i][required_file]["functions"][j][required_function]["present"] = False
+ continue
+
+ out["files"][i][required_file]["functions"][j][required_function]["documentation"] = present_functions[function_name][-3]
+ out["files"][i][required_file]["functions"][j][required_function]["arguments"] = present_functions[function_name][-2]
+ out["files"][i][required_file]["functions"][j][required_function]["minimum_arguments"] = present_functions[function_name][-2].count(",") + 1
+ out["files"][i][required_file]["functions"][j][required_function]["source_code"] = present_functions[function_name][-1]
+
+ if "tests" in required_files_features.keys():
+ filename = list(assessment_struct["files"][i].keys())[0]
+ if not out["files"][i][filename]["has_exception"]:
+ for j, test in enumerate(assessment_struct["files"][i][required_file]["tests"], 0):
+ try:
+ tests_to_run[filename].append(test)
+ except KeyError:
+ tests_to_run[filename] = [test]
+
+ if "run" in required_files_features.keys():
+ filename = list(assessment_struct["files"][i].keys())[0]
+ with misc_classes.ExtractZipToTempDir(zip_file) as tempdir:
+ with misc_classes.FileDependencies(assessment_struct, tempdir):
+ with misc_classes.ChangeDirectory(tempdir):
+ for j, runtime in enumerate(assessment_struct["files"][i][required_file]["run"], 0):
+ for cmd, contents in runtime.items():
+ lines = ""
+ if "monitor" in contents.keys():
+
+ if contents["monitor"] not in produced_files:
+ raise MonitoredFileNotInProducedFilesException("The monitored file %s is not in the list of produced files. It needs to be added." % contents["monitor"])
+
+ subprocess.run(cmd.split())
+ if os.path.exists(contents["monitor"]):
+ with open(contents["monitor"], "r") as f:
+ lines = f.read()
+ else:
+ lines = "*** File not produced ***"
+ # yes, this could potentially cause regexes to still be found
+
+ else:
+ proc = subprocess.Popen(cmd.split(), stdout = subprocess.PIPE)
+ while True:
+ line = proc.stdout.readline()
+ if not line:
+ break
+ lines += line.decode()
+
+ lines = lines.replace("\r", "")
+ matches = {}
+ for regex_ in contents["regexes"]:
+ matches[regex_] = re.findall(regex_, lines)
+ required_files_features["run"][j][cmd]["regexes"] = matches
+ required_files_features["run"][j][cmd]["full_output"] = lines
+
+ out["test_results"] = reflection.run_tests(tests_to_run, configuration["out"] == "stdout" and configuration["format"] in ["text", "txt"])
+ out["class_tree"] = reflection.get_class_tree()
+ return out
+
+class MonitoredFileNotInProducedFilesException(Exception):
+ pass
+
+if __name__ == "__main__":
+ # user_code_path = "D:\\Edencloud\\UniStuff\\3.0 - CMP 3rd Year Project\\Smarker\\../ExampleSubmissions/Submission_A"
+
+ # reflect = Reflect(user_code_path)
+ # reflect.import_module("pjtool")
+ # # for c, v in reflect.get_classes(("pjtool")).items():
+ # # print(c, v)
+ # for k, v in reflect.get_functions("pjtool").items():
+ # print(k, v)
+
+ reflect = Reflect(os.getcwd())
+ print(reflect.client_modules)
+ reflect.import_module("jinja_helpers")
+ print({k: v for k, v in reflect.get_functions("jinja_helpers").items()}) \ No newline at end of file
diff --git a/Smarker/requirements.txt b/Smarker/requirements.txt
new file mode 100644
index 0000000..3831b5e
--- /dev/null
+++ b/Smarker/requirements.txt
@@ -0,0 +1,10 @@
+# sudo apt-get install wkhtmltopdf
+# https://github.com/olivierverdier/python-latex-highlighting
+Jinja2==3.0.3
+misaka==2.1.1
+Pygments==2.10.0
+PyYAML==6.0
+pytest
+junit2html
+pdfkit
+lxml
diff --git a/Smarker/temp.py b/Smarker/temp.py
new file mode 100644
index 0000000..60b1c18
--- /dev/null
+++ b/Smarker/temp.py
@@ -0,0 +1,6 @@
+import json
+
+with open("100301654_report.json", "r") as f:
+ tree = json.load(f)["class_tree"]
+
+print(tree) \ No newline at end of file
diff --git a/Smarker/templates/markdown.jinja2 b/Smarker/templates/markdown.jinja2
new file mode 100644
index 0000000..99c26ce
--- /dev/null
+++ b/Smarker/templates/markdown.jinja2
Binary files differ
diff --git a/Smarker/templates/md.jinja2 b/Smarker/templates/md.jinja2
new file mode 100644
index 0000000..e764a49
--- /dev/null
+++ b/Smarker/templates/md.jinja2
@@ -0,0 +1,166 @@
+{%- macro expand_function(function_name, function_contents, x = "Function") -%}
+ - `{{ function_name }}`:
+{%- if function_contents["present"] %}
+ - **Arguments:**
+ - `{{ function_contents["arguments"] }}`
+ - {{ bool_to_checkbox(function_contents["minimum_arguments"] >= get_required_num_args(function_name)) }} Enough?
+ - **Documentation**:
+ - {{ len_documentation(function_contents["documentation"]["comments"], function_contents["documentation"]["doc"]) }} characters long
+{%- if md_show_full_docs == "True" %}
+ - Comments:
+ {%- if function_contents["documentation"]["comments"] == "None" %}
+ - [ ] No comments present
+{%- else %}
+{{ code_block(function_contents["documentation"]["comments"])|indent(12, True) }}
+{%- endif %}
+ - Docstring:
+{%- if function_contents["documentation"]["doc"] == "None" %}
+ - [ ] No docstring present
+{%- else %}
+{{ code_block(function_contents["documentation"]["doc"])|indent(12, True) }}
+{%- endif -%}
+{%- endif %}
+ - **Source**:
+ - {{ get_source_numlines(function_contents["source_code"]) }}
+{%- if md_show_source == "True" %}
+ - Code:
+{{ code_block(function_contents["source_code"])|indent(12, True) }}
+{%- endif %}
+{%- else %}
+ - [ ] {{ x }} not present
+{%- endif %}
+{%- endmacro -%}
+
+{%- macro code_block(code) -%}
+```
+{{ code }}
+```
+{%- endmacro -%}
+
+# {{ name }} - Student ID: {{ student_no }} Automatic marking report
+Report generated at {{ get_datetime() }}
+## Class Tree:
+
+```
+{{ recurse_class_tree_text(class_tree) }}
+```
+
+## File Analysis
+
+{%- set flat_files = flatten_struct(files) %}
+{% for filename, files_contents in flat_files.items() %}
+### File `{{ filename }}`:
+{%- if files_contents["present"] -%}
+{%- if files_contents["has_exception"] %}
+*** File cannot be run - has compile time exception ***
+Please note that this file cannot be analysed or have tests preformed upon it-
+ this can lead to the whole test suite failing if another module imports this.
+ - Exception Type: `{{ files_contents["exception"]["type"] }}`
+ - Exception String: `{{ files_contents["exception"]["str"] }}`
+ - Full Traceback:
+```
+{{ files_contents["exception"]["traceback"] }}
+```
+{%- else %}
+ - #### Documentation:
+ {%- set len_docs = len_documentation(files_contents["documentation"]["comments"], files_contents["documentation"]["doc"]) %}
+ - {{ len_docs }} characters long
+{%- if md_show_full_docs == "True" %}
+ - ##### Comments:
+{%- if files_contents["documentation"]["comments"] == "None" %}
+ - [ ] No comments present
+{%- else %}
+{{ code_block(files_contents["documentation"]["comments"])|indent(8, True) }}
+{%- endif %}
+ - ##### Docstring:
+{%- if files_contents["documentation"]["doc"] == "None" %}
+ - [ ] No docstring present
+{%- else %}
+{{ code_block(files_contents["documentation"]["doc"])|indent(8, True) }}
+{%- endif -%}
+{%- endif %}
+{%- if "classes" in files_contents.keys() %}
+ - #### Classes:
+{%- set flat_classes = flatten_struct(files_contents["classes"]) -%}
+{% for class_name, class_contents in flat_classes.items() %}
+ - ##### `{{ class_name}}`:
+{%- if class_contents["present"] %}
+ - ###### Documentation:
+ {%- set len_docs = len_documentation(class_contents["documentation"]["comments"], class_contents["documentation"]["doc"]) %}
+ - {{ len_docs }} characters long
+{%- if md_show_full_docs == "True" %}
+ - *Comments*:
+{%- if class_contents["documentation"]["comments"] == "None" %}
+ - [ ] No comments present
+{%- else %}
+{{ code_block(class_contents["documentation"]["comments"])|indent(20, True) }}
+{%- endif %}
+ - *Docstring*:
+{%- if class_contents["documentation"]["doc"] == "None" %}
+ - [ ] No docstring present
+{%- else %}
+{{ code_block(class_contents["documentation"]["doc"])|indent(20, True) }}
+{%- endif -%}
+{%- endif %}
+{%- if "methods" in class_contents.keys() %}
+ - ###### Methods:
+{%- set flat_methods = flatten_struct(class_contents["methods"]) -%}
+{%- for method_name, method_contents in flat_methods.items() %}
+{{ expand_function(method_name, method_contents, "Method")|indent(16, True) }}
+{%- endfor -%}
+{%- endif -%}
+{%- else %}
+ - [ ] Class not present
+{%- endif -%}
+{%- endfor -%}
+{%- endif -%}
+{% if "functions" in files_contents.keys() %}
+ - #### Functions:
+{%- set flat_functions = flatten_struct(files_contents["functions"]) %}
+{%- for function_name, function_contents in flat_functions.items() %}
+{{ expand_function(function_name, function_contents)|indent(8, True) }}
+{%- endfor -%}
+{%- endif -%}
+{% if "run" in files_contents.keys() %}
+ - #### Runtime Analysis:
+{%- set flat_runtime = flatten_struct(files_contents["run"]) %}
+{%- for cmd, runtime_contents in flat_runtime.items() %}
+ - ##### Command `{{ cmd }}`:
+ - **Monitor:**
+{%- if "monitor" in runtime_contents.keys() %}
+ - {{ runtime_contents["monitor"] }}
+{%- else %}
+ - stdout
+{%- endif %}
+ - **Regexes:**
+{%- for regex_, results in runtime_contents["regexes"].items() %}
+ - `{{regex_}}`:
+ - Found occurrences: {{ len_(results) }}
+{%- if code_block(runtime_contents["full_output"]) == "*** File not produced ***" %}
+ - *** File was not produced- no occurrences ***
+{%- endif -%}
+{%- if md_show_all_regex_occurrences == "True" and len_(results) > 0 %}
+ - Occurrences list:
+{%- for result in results %}
+ - `{{ result.replace("\n", "\\n") }}`
+{%- endfor -%}
+{%- if md_show_all_run_output == "True" %}
+ - Full runtime output:
+{{ code_block(runtime_contents["full_output"])|indent(24, True) }}
+{%- endif -%}
+{%- endif -%}
+{%- endfor -%}
+{%- endfor -%}
+{%- endif -%}
+{%- endif -%}
+{% else %}
+ - [ ] File not present
+{% endif %}
+{% endfor %}
+
+{% if out != "stdout" and format != "html" -%}
+## Tests:
+```
+{{ test_results["pytest_report"].replace("\r", "") }}
+```
+{%- endif -%} \ No newline at end of file
diff --git a/Smarker/templates/tex.jinja2 b/Smarker/templates/tex.jinja2
new file mode 100644
index 0000000..eaa7db7
--- /dev/null
+++ b/Smarker/templates/tex.jinja2
@@ -0,0 +1,249 @@
+((* macro expand_function(function_name, function_contents, x = "Function") *))
+ \texttt{((( tex_escape(function_name) )))}:
+
+ ((* if function_contents["present"] *))
+ \begin{itemize}
+ \item Arguments: \pyth{((( function_contents["arguments"] )))}
+ \item Documentation: ((( len_documentation(function_contents["documentation"]["comments"], function_contents["documentation"]["doc"]) ))) characters long
+ ((* if tex_show_full_docs == "True" *))
+
+ \textbf{Comments:}
+ ((*- if function_contents["documentation"]["comments"] == "None" *))
+ \errortext{No comments present.}
+ ((* else *))
+ \begin{lstlisting}
+((( function_contents["documentation"]["comments"] )))
+ \end{lstlisting}
+ ((* endif *))
+
+ \textbf{Docstring}:
+ ((*- if function_contents["documentation"]["doc"] == "None" *))
+ \errortext{No docstring present.}
+ ((* else *))
+ \begin{lstlisting}
+((( function_contents["documentation"]["doc"] )))
+ \end{lstlisting}
+ ((* endif *))
+ ((* endif *))
+ \item Code: ((( get_source_numlines(function_contents["source_code"]) )))
+ ((* if tex_show_source == "True" *))
+ \begin{python}
+((( function_contents["source_code"] )))
+ \end{python}
+ ((* endif *))
+ \end{itemize}
+ ((* else *))
+ \errortext{((( x ))) \texttt{((( tex_escape(function_name) )))} not present.}
+ ((* endif *))
+((* endmacro *))
+
+\documentclass{article}
+
+\usepackage{pythonhighlight}
+
+\usepackage[margin=1in]{geometry} % margins
+\usepackage{multicol} % columns
+\usepackage{float} % layout
+\usepackage{forest} % for the class tree
+\usepackage{pdfpages} % for importing the test results pdf
+\usepackage{xcolor} % colours
+\usepackage{listings}
+\lstset{
+basicstyle=\small\ttfamily,
+columns=flexible,
+breaklines=true
+}
+
+\newcommand{\errortext}[1]{\textcolor{red}{\textbf{#1}}}
+
+\author{((( student_no )))}
+\title{((( name ))) - Automatic marking report}
+
+\begin{document}
+
+((* if tex_columns != "1" *))
+\begin{multicols}{((( tex_columns )))}
+((* endif *))
+
+\maketitle
+\section{Class Tree}
+
+\begin{figure}[H]
+ \centering
+ \begin{forest}
+ ((( recurse_class_tree_forest(class_tree)|indent(8, False) )))
+ \end{forest}
+ \caption{Class inheritance tree}
+\end{figure}
+
+\section{File Analysis}
+((* set flat_files = flatten_struct(files) *))
+((* for filename, files_contents in flat_files.items() *))
+ \subsection{\texttt{((( filename )))}}
+ ((* if files_contents["present"] *))
+ ((* if files_contents["has_exception"] *))
+ \errortext{File cannot be run - has compile time exception.}
+
+ Please note that this file cannot be analysed or have tests preformed upon it-
+ this can lead to the whole test suite failing if another module imports this.
+
+ \textbf{Exception Type:} \texttt{((( files_contents["exception"]["type"] )))}
+
+ \textbf{Exception String:} \texttt{((( files_contents["exception"]["str"] )))}
+
+ \textbf{Full Traceback:}
+
+ \begin{lstlisting}
+((( files_contents["exception"]["traceback"] )))
+ \end{lstlisting}
+ ((* else *))
+ \begin{itemize}
+ \item \textbf{Documentation:}
+
+ ((( len_documentation(files_contents["documentation"]["comments"], files_contents["documentation"]["doc"]) ))) characters long
+ ((* if tex_show_full_docs == "True" *))
+
+ \item \textbf{Comments:}
+ ((*- if files_contents["documentation"]["comments"] == "None" *))
+ \errortext{No comments present.}
+ ((* else *))
+ \begin{lstlisting}
+((( files_contents["documentation"]["comments"] )))
+ \end{lstlisting}
+ ((* endif *))
+
+ \item \textbf{Docstring:}
+ ((*- if files_contents["documentation"]["doc"] == "None" *))
+ \errortext{No docstring present.}
+ ((* else *))
+ \begin{lstlisting}
+((( files_contents["documentation"]["doc"] )))
+ \end{lstlisting}
+ ((* endif *))
+
+ ((* endif *))
+ \end{itemize}
+
+ ((* if "classes" in files_contents.keys() *))
+ \subsubsection{Classes}
+
+ ((* set flat_classes = flatten_struct(files_contents["classes"]) *))
+ ((* for class_name, class_contents in flat_classes.items() *))
+ \begin{itemize}
+
+
+ \item \texttt{((( class_name )))}:
+
+ ((* if class_contents["present"] *))
+ \begin{itemize}
+ \item \textbf{Documentation:}
+ ((( len_documentation(class_contents["documentation"]["comments"], class_contents["documentation"]["doc"]) ))) characters long
+
+ ((* if tex_show_full_docs == "True" *))
+
+
+ \item \textbf{Comments:}
+
+ ((* if class_contents["documentation"]["comments"] == "None" -*))
+ \errortext{No comments present.}
+ ((* else *))
+ \begin{lstlisting}
+((( class_contents["documentation"]["comments"] )))
+ \end{lstlisting}
+ ((* endif *))
+
+
+ \item \textbf{Docstring:}
+
+ ((* if class_contents["documentation"]["doc"] == "None" -*))
+ \errortext{No docstring present.}
+ ((* else *))
+ \begin{lstlisting}
+((( class_contents["documentation"]["doc"] )))
+ \end{lstlisting}
+ ((* endif *))
+
+ ((* if "methods" in class_contents.keys() *))
+ \item \textbf{Methods:}
+ ((* set flat_methods = flatten_struct(class_contents["methods"]) *))
+ \begin{itemize}
+ ((* for method_name, method_contents in flat_methods.items() *))
+ \item ((( expand_function(method_name, method_contents, x = "Method") )))
+ ((* endfor *))
+ \end{itemize}
+
+ ((* endif *))
+ \end{itemize}
+ ((* endif *))
+
+ ((* else *))
+
+ \errortext{Class not present.}
+
+ ((* endif *))
+
+ \end{itemize}
+ ((* endfor *))
+
+
+ ((* endif *))
+
+ ((* if "functions" in files_contents.keys() *))
+ \subsubsection{Functions}
+ ((* set flat_functions = flatten_struct(files_contents["functions"]) *))
+ \begin{itemize}
+ ((* for function_name, function_contents in flat_functions.items() *))
+ \item ((( expand_function(function_name, function_contents) )))
+ ((* endfor *))
+ \end{itemize}
+ ((* endif *))
+
+ \subsubsection{Runtime Analysis}
+ ((* set flat_runtime = flatten_struct(files_contents["run"]) *))
+ \begin{itemize}
+ ((* for cmd, runtime_contents in flat_runtime.items() *))
+ \item Command: \texttt{((( tex_escape(cmd) )))}
+ \item Monitor:
+ ((*- if "monitor" in runtime_contents.keys() *))
+ \texttt{((( tex_escape(runtime_contents["monitor"]) )))}
+ ((*- else *))
+ stdout
+ ((*- endif *))
+ \item Regexes:
+ ((* for regex_, results in runtime_contents["regexes"].items() *))
+ \begin{itemize}
+ \item \texttt{((( tex_escape(regex_) )))}:
+ \begin{itemize}
+ \item Found occurrences: ((( len_(results) )))
+ ((* if txt_show_all_regex_occurrences == "True" and len_(results) > 0 *))
+ \item Occurences list:
+ \begin{enumerate}
+ ((* for result in results *))
+ \item \texttt{((( tex_escape(result.replace("\n", "\\n")) )))}
+ ((* endfor *))
+ \end{enumerate}
+ ((* endif *))
+ \end{itemize}
+ \end{itemize}
+ ((*- endfor -*))
+ ((* endfor *))
+ \end{itemize}
+
+ ((* endif *))
+ ((* else *))
+ \errortext{File is not present.}
+ ((* endif *))
+((* endfor *))
+
+\section{Tests}
+((* if test_results["pytest_report"] == "*** No Tests ***" *))
+ No tests were executed.
+((* else *))
+ \includepdf[pages={1-},scale=1.0]{((( junit_xml_to_html(test_results["junitxml"], student_no) )))}
+((* endif *))
+
+((* if tex_columns != "1" *))
+\end{multicols}
+((* endif *))
+
+\end{document} \ No newline at end of file
diff --git a/Smarker/templates/text.jinja2 b/Smarker/templates/text.jinja2
new file mode 100644
index 0000000..eca6ebd
--- /dev/null
+++ b/Smarker/templates/text.jinja2
Binary files differ
diff --git a/Smarker/templates/txt.jinja2 b/Smarker/templates/txt.jinja2
new file mode 100644
index 0000000..9eb4beb
--- /dev/null
+++ b/Smarker/templates/txt.jinja2
@@ -0,0 +1,168 @@
+{%- macro expand_function(function_name, function_contents, x = "Function") -%}
+{{ function_name + ":" }}
+{%- if function_contents["present"] %}
+ Arguments:
+ {{ function_contents["arguments"] }}
+ Enough? {{ bool_to_yesno(function_contents["minimum_arguments"] >= get_required_num_args(function_name)) }}
+ Documentation:
+ {{ len_documentation(function_contents["documentation"]["comments"], function_contents["documentation"]["doc"]) }} characters long
+ {%- if txt_show_full_docs == "True" %}
+ Comments:
+ {%- if function_contents["documentation"]["comments"] == "None" %}
+ *** No comments present ***
+ {%- else %}
+```
+{{ function_contents["documentation"]["comments"] }}
+```
+ {%- endif %}
+ Docstring:
+ {%- if function_contents["documentation"]["doc"] == "None" %}
+ *** No docstring present ***
+ {%- else %}
+```
+{{ function_contents["documentation"]["doc"] }}
+```
+ {%- endif -%}
+ {%- endif %}
+ Source:
+ {{ get_source_numlines(function_contents["source_code"]) }}
+ {%- if txt_show_source == "True" %}
+ Code:
+```
+{{ function_contents["source_code"] }}
+```
+ {%- endif %}
+{%- else %}
+ *** {{ x }} not present ***
+{%- endif %}
+{%- endmacro -%}
+
+=== {{ name }} - Student ID: {{ student_no }} Automatic marking report ===
+Report generated at {{ get_datetime() }}
+
+== Class Tree: ==
+
+{{ recurse_class_tree_text(class_tree) }}
+
+== File Analysis ==
+{%- set flat_files = flatten_struct(files) %}
+{% for filename, files_contents in flat_files.items() %}
+ = {{ filename + " =" -}}
+ {%- if files_contents["present"] -%}
+ {%- if files_contents["has_exception"] %}
+ *** File cannot be run - has compile time exception ***
+ Please note that this file cannot be analysed or have tests preformed upon it-
+ this can lead to the whole test suite failing if another module imports this.
+ Exception Type:
+ {{ files_contents["exception"]["type"] }}
+ Exception String:
+ {{ files_contents["exception"]["str"] }}
+ Full Traceback:
+```
+{{ files_contents["exception"]["traceback"] }}
+```
+ {%- else %}
+ Documentation:
+ {{ len_documentation(files_contents["documentation"]["comments"], files_contents["documentation"]["doc"]) }} characters long
+ {%- if txt_show_full_docs == "True" %}
+ Comments:
+ {%- if files_contents["documentation"]["comments"] == "None" %}
+ *** No comments present ***
+ {%- else %}
+ ```
+ {{ files_contents["documentation"]["comments"]|indent(16, False) }}
+ ```
+ {%- endif %}
+ Docstring:
+ {%- if files_contents["documentation"]["doc"] == "None" %}
+ *** No docstring present ***
+ {%- else %}
+ ```
+ {{ files_contents["documentation"]["doc"]|indent(16, False) }}
+ ```
+ {%- endif -%}
+ {%- endif %}
+ {%- if "classes" in files_contents.keys() %}
+ Classes:
+ {%- set flat_classes = flatten_struct(files_contents["classes"]) -%}
+ {% for class_name, class_contents in flat_classes.items() %}
+ {{ class_name + ":" }}
+ {%- if class_contents["present"] %}
+ Documentation:
+ {{ len_documentation(class_contents["documentation"]["comments"], class_contents["documentation"]["doc"]) }} characters long
+ {%- if txt_show_full_docs == "True" %}
+ Comments:
+ {%- if class_contents["documentation"]["comments"] == "None" %}
+ *** No comments present ***
+ {%- else %}
+ ```
+ {{ class_contents["documentation"]["comments"]|indent(16, False) }}
+ ```
+ {%- endif %}
+ Docstring:
+ {%- if class_contents["documentation"]["doc"] == "None" %}
+ *** No docstring present ***
+ {%- else %}
+ ```
+ {{ class_contents["documentation"]["doc"]|indent(16, False) }}
+ ```
+ {%- endif -%}
+ {%- endif %}
+ {%- if "methods" in class_contents.keys() %}
+ Methods:
+ {%- set flat_methods = flatten_struct(class_contents["methods"]) -%}
+ {%- for method_name, method_contents in flat_methods.items() %}
+ {{ expand_function(method_name, method_contents, "Method")|indent(20, False) }}
+ {%- endfor -%}
+ {%- endif -%}
+ {%- else %}
+ *** Class not present ***
+ {%- endif -%}
+ {%- endfor -%}
+ {%- endif -%}
+ {% if "functions" in files_contents.keys() %}
+ Functions:
+ {%- set flat_functions = flatten_struct(files_contents["functions"]) %}
+ {%- for function_name, function_contents in flat_functions.items() %}
+ {{ expand_function(function_name, function_contents)|indent(12, False) }}
+ {%- endfor -%}
+ {%- endif -%}
+ {% if "run" in files_contents.keys() %}
+ Runtime Analysis:
+ {%- set flat_runtime = flatten_struct(files_contents["run"]) %}
+ {%- for cmd, runtime_contents in flat_runtime.items() %}
+ Command `{{ cmd }}`:
+ Monitor:
+ {%- if "monitor" in runtime_contents.keys() %}
+ {{ runtime_contents["monitor"] }}
+ {%- else %}
+ stdout
+ {%- endif %}
+ Regexes:
+ {%- for regex_, results in runtime_contents["regexes"].items() %}
+ `{{regex_}}`:
+ Found occurrences: {{ len_(results) }}
+ {%- if txt_show_all_regex_occurrences == "True" and len_(results) > 0 %}
+ Occurrences list:
+ {%- for result in results %}
+ {{ result.replace("\n", "\\n") }}
+ {%- endfor -%}
+ {%- endif -%}
+ {%- endfor -%}
+ {%- if txt_show_all_run_output == "True" %}
+ Full runtime output:
+ ```
+ {{ runtime_contents["full_output"]|indent(20, False) }}
+ ```
+ {%- endif -%}
+ {%- endfor -%}
+ {%- endif -%}
+ {%- endif -%}
+ {% else %}
+ *** File not present ***
+ {% endif %}
+{% endfor %}
+
+{% if out != "stdout" -%}
+{{ test_results["pytest_report"].replace("\r", "") }}
+{%- endif -%} \ No newline at end of file