diff --git a/README.md b/README.md index 226a746c0607c878dff1e173823407467274ee74..4dc1f45a91d65b2ff51fc4fa889bc8921c2d1db2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Invoke the script with `python3 grim.py -h` in order to see usage. Currently many features are not yet implemented. Single and Multiple Chice questions can be generated also in parametrized form. -Example scripts can be found in `scripts/`. +Example scripts can be found in `scripts/examples/`. A first version of the tool that relied on autoilias is still in the repository (see `generator.py`). It will be removed as soon as the necessary features are @@ -15,16 +15,17 @@ implemented. ### Dependencies `pip install mistune` +`pip install pygments` ### TODO * Add a good description / documentation. -* Add more functionality (gap, alignment, etc.) +* Add more functionality (finalize gap, alignment) * Make parsers more robust. * reverse ILIAS authentication mechanism for automated upload. * Create whole test object with questions for direct import. Create two versions (one for internal use and one for the test.) -* move moints from meta to parser +* add zip support ### Notes @@ -40,5 +41,5 @@ data and assumes unknown properties) ### LaTeX Support Hallgrim supports the native latex approach by ILIAS. To typeset a formula just -out in backets like this `[[\\sum_{i=1}^n i = \\frac{n(n+1)}{2}]]`. Special +out in backets like this `[[\\suam_{i=1}^n i = \\frac{n(n+1)}{2}]]`. Special carecters (mostly `\`) have to be escaped unless you use raw strings (`r'a raw string'`). diff --git a/grim.py b/grim.py index 246cf3c70ba2312806049874fbd8ddd1100d12bb..49ac09ff4b457ffb7404ec91d2a0b4c76afb6028 100755 --- a/grim.py +++ b/grim.py @@ -1,5 +1,22 @@ #!/usr/local/bin/python3 +################################################################################ +# +# This script contains the main part of hallgrim and ist the only script that +# needs to ne invoked. The steps it takes to generate a task are as follows: +# +# * parse the commandline arguments with argparse +# * for each script determine the type and validate correct syntax +# * delegate the script to a handler for the specific type +# * the handler parses the script into the intermediate represenatation (mostly +# arrays) +# * the handler passes the needed information to the script generator, which +# will ptint the final xml file. +# * a finisher compresses data if needed (needs to be implemented, maybe as s +# eperate subparser). +# +################################################################################ + import importlib import argparse import os @@ -10,6 +27,19 @@ from hallgrim.IliasXMLCreator import packer from hallgrim.messages import * from hallgrim.parser import * +scaffolding = r''' +meta = {{ + 'author': '{}', + 'title': '{}', + 'type': '{}', + 'points': {}, +}} + +task = """ decription """ +{} +feedback = """ decription """ +''' + def file_to_module(name): return name.rstrip('.py').replace('/', '.') @@ -69,6 +99,7 @@ def parseme(): "--points", help='Points given for correct answer (different behavior for different questions)', type=float, + default=0.0, metavar='POINTS', ) @@ -83,6 +114,7 @@ def parseme(): parser_gen.add_argument( 'input', help='Script to execute', + nargs='+', type=file_exists, metavar='FILE') parser_gen.add_argument( @@ -103,15 +135,16 @@ def parseme(): parser.print_help() -def delegator(output, script_name, instances): - script = importlib.import_module(file_to_module(script_name)) - handler = { - 'gap': handle_gap_questions, - 'single choice': handle_choice_questions, - 'multiple choice': handle_choice_questions - }[script.meta['type']] +def delegator(output, script_list, instances): + for script_name in filter(lambda a: a.endswith('.py'), script_list): + script = importlib.import_module(file_to_module(script_name)) + handler = { + 'gap': handle_gap_questions, + 'single choice': handle_choice_questions, + 'multiple choice': handle_choice_questions + }[script.meta['type']] - handler(output, script, instances) + handler(output, script, instances) def handle_gap_questions(output, script, instances): @@ -151,29 +184,18 @@ def handle_choice_questions(output, script, instances): output = os.path.join( 'output', script.meta['title']) + '.xml' if not output else output packer.convert_and_print(data, output, instances) - info('Processed "{}" and wrote xml to "{}".'.format( - script.__name__, output)) + info('Processed "{}" and'.format(script.__name__)) + info('wrote xml "{}"'.format(output), notag=True) def handle_new_script(name, qtype, author, points): - with open('scripts/' + name + '.py', 'x') as new_script: + with open('scripts/' + name + '.py', 'w') as new_script: choice = '' if qtype in ['multi', 'single']: choice = '\nchoices = """\n[X] A\n[ ] B\n[ ] C\n[X] D\n"""\n' - scaffolding = ''' -meta = {{ - 'author': '{}', - 'title': '{}', - 'type': '{}', - 'points': {}, # per correct choice -}} - -task = """ decription """ -{} -feedback = """ decription """ -'''.format(author, name, qtype, points, choice).strip() - print(scaffolding, file=new_script) + print(scaffolding.format( + author, name, qtype, points, choice).strip(), file=new_script) info('Generated new script "{}."'.format(new_script.name)) if __name__ == '__main__': diff --git a/hallgrim/custom_markdown.py b/hallgrim/custom_markdown.py new file mode 100644 index 0000000000000000000000000000000000000000..a315559ce0e3e8da5b31d68baef75c3400b87cc4 --- /dev/null +++ b/hallgrim/custom_markdown.py @@ -0,0 +1,80 @@ +import re + +try: + from mistune import Renderer, InlineLexer, Markdown, escape + from pygments import highlight + from pygments.lexers import get_lexer_by_name + from pygments.formatters import HtmlFormatter +except ImportError as err: + print("Please install mistune to make use of markdown parsing.") + print("\t pip install mistune") + + +no_copy = "-webkit-touch-callout: none; \ + -webkit-user-select: none; \ + -khtml-user- select: none; \ + -moz-user-select: none;\ + -ms-user-select: none;\ + user-select: none;" + + +def box(content, color): + return '<div style="background-color: #ffedc9; border: 1px solid {}; \ + padding: 10px; font-size: smaller;">{}</div>'.format(color, content) + + +def yellow_box(content): + return box(content, '#FFB12E') + + +def blue_box(content): + return box(content, '#9999ff') + + +def markdown(value): + renderer = HighlightRenderer() + markdown = Markdown(renderer=renderer) + return markdown(value) + + +class LaTeXRenderer(Renderer): + + def latex(self, formula): + return '<span class="latex">{}</span>'.format(formula) + + def block_code(self, code, lang): + if not lang: + return '\n<pre><code>%s</code></pre>\n' % \ + escape(code) + lexer = get_lexer_by_name(lang, stripall=True) + formatter = HtmlFormatter(noclasses=True, cssstyles=no_copy) + return highlight(code, lexer, formatter) + + +class LaTeXInlineLexer(InlineLexer): + + """ Classes are inspired by the lexer example in the mistune readme """ + + def enable_latex(self): + # add latex rules + self.rules.latex = re.compile( + r'\[\[' # [[ + r'([^\]]+)' # formula + r'\]\](?!\])' # ]] + ) + self.default_rules.insert(3, 'latex') + + def output_latex(self, m): + formula = m.group(1).replace('\n', ' ') + return self.renderer.latex(formula) + + +def get_custom_markdown(): + renderer = LaTeXRenderer() + inline = LaTeXInlineLexer(renderer) + + # enable the feature + inline.enable_latex() + return Markdown(renderer, inline=inline) + +markdown = get_custom_markdown() diff --git a/hallgrim/messages.py b/hallgrim/messages.py index e698bfd73bb7f51c7daa90b0ed31bd4b88c6add9..e437a5912b66902f579a6fcc4ecf46a973238922 100644 --- a/hallgrim/messages.py +++ b/hallgrim/messages.py @@ -6,8 +6,8 @@ def warn(msg): def debug(msg): print('[DEBUG]', msg) -def info(msg): - print('[INFO]', msg) +def info(msg, notag=False): + print('[INFO]' if not notag else ' ', msg) def error(msg): print('[ERROR]', msg) diff --git a/hallgrim/parser.py b/hallgrim/parser.py index 641149780b7f4e0a722a61095519c08a9eb5e013..58de73a7855f1bbf365b86087011ce3212d47f6e 100644 --- a/hallgrim/parser.py +++ b/hallgrim/parser.py @@ -1,87 +1,6 @@ import re -try: - from mistune import Renderer, InlineLexer, Markdown, escape - from pygments import highlight - from pygments.lexers import get_lexer_by_name - from pygments.formatters import HtmlFormatter -except ImportError as err: - print("Please install mistune to make use of markdown parsing.") - print("\t pip install mistune") - -## TODO get lexer elsewhere - - -no_copy = "-webkit-touch-callout: none; \ - -webkit-user-select: none; \ - -khtml-user- select: none; \ - -moz-user-select: none;\ - -ms-user-select: none;\ - user-select: none;" - - -def box(content, color): - return '<div style="background-color: #ffedc9; border: 1px solid {}; \ - padding: 10px; font-size: smaller;">{}</div>'.format(color, content) - - -def yellow_box(content): - return box(content, '#FFB12E') - - -def blue_box(content): - return box(content, '#9999ff') - - -def markdown(value): - renderer = HighlightRenderer() - markdown = Markdown(renderer=renderer) - return markdown(value) - - -class LaTeXRenderer(Renderer): - - def latex(self, formula): - return '<span class="latex">{}</span>'.format(formula) - - def block_code(self, code, lang): - if not lang: - return '\n<pre><code>%s</code></pre>\n' % \ - escape(code) - lexer = get_lexer_by_name(lang, stripall=True) - formatter = HtmlFormatter(noclasses=True, cssstyles=no_copy) - return highlight(code, lexer, formatter) - - -class LaTeXInlineLexer(InlineLexer): - - """ Classes are inspired by the lexer example in the mistune readme """ - - def enable_latex(self): - # add latex rules - self.rules.latex = re.compile( - r'\[\[' # [[ - r'([^\]]+)' # formula - r'\]\](?!\])' # ]] - ) - self.default_rules.insert(3, 'latex') - - def output_latex(self, m): - formula = m.group(1).replace('\n', ' ') - return self.renderer.latex(formula) - - -def get_custom_markdown(): - renderer = LaTeXRenderer() - inline = LaTeXInlineLexer(renderer) - - # enable the feature - inline.enable_latex() - return Markdown(renderer, inline=inline) - -markdown = get_custom_markdown() - - +from hallgrim.custom_markdown import markdown def choice_parser(raw_choices, points): """ Parse the multiple choice answers and form an array that has the @@ -94,13 +13,13 @@ def choice_parser(raw_choices, points): lines = raw_choices.strip().split('\n') elif type(raw_choices) is list: lines = raw_choices - regex = re.compile('\[(\d|X| )\]\s+([\w\W]+)', re.MULTILINE) + regex = re.compile('\[(([0-9]*[.])?[0-9]+|X| )\]\s+([\w\W]+)', re.MULTILINE) parse = [re.match(regex, line).groups() for line in lines] final = [( markdown(text), True if mark != ' ' else False, float(mark) if mark not in ' X' else points) - for mark, text in parse] + for mark, _, text in parse] return final def gap_parser(task): diff --git a/scripts/feldarbeit_param.py b/scripts/examples/feldarbeit_param.py similarity index 100% rename from scripts/feldarbeit_param.py rename to scripts/examples/feldarbeit_param.py diff --git a/scripts/multi_proto.py b/scripts/examples/multi_proto.py similarity index 100% rename from scripts/multi_proto.py rename to scripts/examples/multi_proto.py diff --git a/scripts/examples/select.py b/scripts/examples/select.py new file mode 100644 index 0000000000000000000000000000000000000000..4aefe5dd25382d10f130e650155dc86b46653626 --- /dev/null +++ b/scripts/examples/select.py @@ -0,0 +1,105 @@ +meta = { + 'author': 'Jan Maximilian Michal', + 'title': 'Zeilen sind anders, Spalten auch (I1-ID: lhu27691fzg0)', + 'type': 'gap', +} + + +gap_1 = """[select] +[1] int n_ze = m.length; +[ ] int n_ze = m[0].length; +[ ] int n_ze = m.length(); +[ ] int n_ze = m[0].length(); +[/select]""" + +gap_2 = """[select] +[ ] int n_sp = m.length(); +[ ] int n_sp = m[0].length(); +[ ] int n_sp = m.length; +[1] int n_sp = m[0].length; +[/select]""" + +gap_3 = """[select] +[ ] for (int i = 1; i <= n_ze; i++) +[1] for (int i = 0; i < n_ze; i++) +[ ] for (int i = 1; i <= n_sp; i++) +[ ] for (int i = 0; i < n_sp; i++) +[/select]""" + +gap_4 = """[select] +[ ] ms[i] = ms[i] + m[j][i]*s[i]; +[ ] ms[i] = ms[i] + m[i][j]*s[i]; +[ ] ms[i] = ms[i] + m[j][i]*s[j]; +[1] ms[i] = ms[i] + m[i][j]*s[j]; +[/select]""" + + +task = """ Folgendes Codefragment soll die Multiplikation eines Zeilenvektors mit einer Matrix sowie einer Matrix mit einem Spaltenvektor realisieren. Wählen Sie die fehlenden Zeilen unten entsprechend aus: + +```java +01: int[][] m = {{ 0, 1, 2, 3}, // Matrix +02: { 4, 5, 6, 7}, +03: {8, 9, 10, 11}}; +04: int[] z = { 12, 13, 14}; // Zeile +05: int[] s = {15, 16, 17, 18}; // Spalte +06: +07: /* CODEZEILE AUSWÄHLEN */ // Zeilenanzahl der Matrix +08: /* CODEZEILE AUSWÄHLEN */ // Spaltenanzahl der Matrix +09: +10: /* 1: Zeile mal Matrix */011: int[] zm = new int[n_sp]; +12: for (int j = 0; j < n_sp; j++) { +13: zm[j] = 0; +14: /* CODEZEILE AUSWÄHLEN */ +15: zm[j] = zm[j] + z[i]*m[i][j]; +16: } +17: /* 2: Matrix mal Spalte */ +18: int[] ms = new int[n_ze]; +19: for (int i = 0; i < n_ze; i++) { +20: ms[i] = 0; +21: for (int j = 0; j < n_sp; j++) +22: /* CODEZEILE AUSWÄHLEN */ +23: +``` + +**Zeile 7:** +%s + +**Zeile 8:** +%s + +**Zeile 14:** +%s + +**Zeile 22:** +%s +""" % (gap_1, gap_2, gap_3, gap_4) + +feedback = """ + +Der vollständige Code: + +```java +int[][] m = {{ 0, 1, 2, 3}, // Matrix + { 4, 5, 6, 7}, + {8, 9, 10, 11}}; +int[] z = { 12, 13, 14}; // Zeile +int[] s = {15, 16, 17, 18}; // Spalte + +int n_ze = m.length; // Zeilenanzahl der Matrix +int n_sp = m[0].length; // Spaltenanzahl der Matrix + +/* 1: Zeile mal Matrix */011: int[] zm = new int[n_sp]; +for (int j = 0; j < n_sp; j++) { + zm[j] = 0; + for (int i = 0; i < n_ze; i++) + zm[j] = zm[j] + z[i]*m[i][j]; +} +/* 2: Matrix mal Spalte */ +int[] ms = new int[n_ze]; +for (int i = 0; i < n_ze; i++) { + ms[i] = 0; + for (int j = 0; j < n_sp; j++) + ms[i] = ms[i] + m[i][j]*s[j]; + +``` +""" \ No newline at end of file diff --git a/scripts/single_proto.py b/scripts/examples/single_proto.py similarity index 100% rename from scripts/single_proto.py rename to scripts/examples/single_proto.py