From ee8c387559daada02693d4321803271bbe06149b Mon Sep 17 00:00:00 2001
From: Jan Maximilian Michal <j.michal@stud.uni-goettingen.de>
Date: Thu, 24 Nov 2016 00:39:01 +0000
Subject: [PATCH] updated readme, added comments, allow multiple scripts on
 command line

---
 README.md                                  |   9 +-
 grim.py                                    |  70 +++++++++-----
 hallgrim/custom_markdown.py                |  80 ++++++++++++++++
 hallgrim/messages.py                       |   4 +-
 hallgrim/parser.py                         |  87 +----------------
 scripts/{ => examples}/feldarbeit_param.py |   0
 scripts/{ => examples}/multi_proto.py      |   0
 scripts/examples/select.py                 | 105 +++++++++++++++++++++
 scripts/{ => examples}/single_proto.py     |   0
 9 files changed, 241 insertions(+), 114 deletions(-)
 create mode 100644 hallgrim/custom_markdown.py
 rename scripts/{ => examples}/feldarbeit_param.py (100%)
 rename scripts/{ => examples}/multi_proto.py (100%)
 create mode 100644 scripts/examples/select.py
 rename scripts/{ => examples}/single_proto.py (100%)

diff --git a/README.md b/README.md
index 226a746..4dc1f45 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 246cf3c..49ac09f 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 0000000..a315559
--- /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 e698bfd..e437a59 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 6411497..58de73a 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 0000000..4aefe5d
--- /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
-- 
GitLab