From 68872e76eb92e0104120d035fa871b5dc81c4d61 Mon Sep 17 00:00:00 2001
From: janmax <j.michal@stud.uni-goettingen.de>
Date: Wed, 14 Mar 2018 18:47:58 +0100
Subject: [PATCH] Fixed a bug in the qti converter

---
 hektor.py  | 44 +++++++++++++++++++++++++++++++-------------
 lib/qti.py |  5 +++--
 lib/xls.py |  6 ++++--
 setup.py   |  2 +-
 4 files changed, 39 insertions(+), 18 deletions(-)

diff --git a/hektor.py b/hektor.py
index 82faed7..3827b7b 100644
--- a/hektor.py
+++ b/hektor.py
@@ -94,6 +94,11 @@ def setup_argparse():
         default=True,
         help='asserts that output data will be in a certain format'
     )
+    parser.add_argument(
+        '-r', '--readable-code',
+        action='store_true',
+        default=True,
+        help='make student code readable by inserting artificial line breaks')
 
     args = parser.parse_args()
 
@@ -105,7 +110,7 @@ def setup_argparse():
 def compose(*functions: Sequence[Callable]) -> Callable:
     """ Standard function composition. Takes a Sequence of functions [f, g, h, ...]
     and returns the composite function i(x) = f(g(h(x))). There are no checks
-    that validate if domain and image of these functions are compatible."""
+    that validate if domain and image of these functions are compatible. """
     return functools.reduce(lambda f,
                             g: lambda x: f(g(x)),
                             functions,
@@ -119,6 +124,20 @@ def abort(message='Bye.'):
 
 
 # ========================== =- Post processors -= ========================== #
+def student_replacer(processor):
+    ''' A simple decorator that is used to remove students and put them back in
+    when the preprocessor is dome with them'''
+
+    @functools.wraps(processor)
+    def processor_closure(structured_data: Dict[str, Any]) -> Dict[str, Any]:
+        students = structured_data.pop('students')
+        students_replacement = processor(students)
+        structured_data['students'] = students_replacement
+        return structured_data
+
+    return processor_closure
+
+
 def do_add_meta(structured_data: Dict[str, Any]) -> Dict[str, Any]:
     ''' Asks the user for metadata about the exam '''
     structured_data['author'] = input('[Q] author: ')
@@ -132,6 +151,7 @@ def do_verify(structured_data: Dict[str, Any]) -> Dict[str, Any]:
     on by default. The impact on performance is neglectable. '''
     def assert_submission(submission):
         assert 'code' in submission, 'A submission needs code'
+        assert type(submission['code']) in [str, list], 'Code is readable'
         assert 'type' in submission, 'A submission has to be of some type'
         assert 'tests' in submission, 'A tests dict has to be present.'
 
@@ -172,18 +192,12 @@ def do_verify(structured_data: Dict[str, Any]) -> Dict[str, Any]:
     return structured_data
 
 
-def student_replacer(processor):
-    ''' A simple decorator that is used to remove students and put them back in
-    when the preprocessor is dome with them'''
-
-    @functools.wraps(processor)
-    def processor_closure(structured_data: Dict[str, Any]) -> Dict[str, Any]:
-        students = structured_data.pop('students')
-        students_replacement = processor(students)
-        structured_data['students'] = students_replacement
-        return structured_data
-
-    return processor_closure
+@student_replacer
+def do_readable_code(students: Dict[str, Union[str, List]]):
+    for student in students:
+        for submission in student['submissions']:
+            submission['code'] = submission['code'].split('\n')
+    return students
 
 
 @student_replacer
@@ -249,6 +263,7 @@ def do_decrypt(students):
         abort('Your key is bad (%s).' % err)
 
 
+# ======================= =- Post processor helper -= ======================= #
 def transform(students, function):
     return [
         {'fullname': function(student['fullname']),
@@ -294,6 +309,7 @@ def get_active_postprocessors():
     postprocessor_order = (
         do_add_meta,
         do_verify,
+        do_readable_code,
         do_anonymous,
         do_encrypt,
         do_decrypt
@@ -309,6 +325,8 @@ def _preprocessing(filepath: str) -> str:
 
 
 def _processing(filepath: str) -> Dict[str, Any]:
+    ''' Find the first apropriate converter and run pass it the path to the
+    datafile. '''
     try:
         return next(converter().convert(filepath)
                     for converter in Converter.implementations()
diff --git a/lib/qti.py b/lib/qti.py
index 8441fb3..bc963dc 100644
--- a/lib/qti.py
+++ b/lib/qti.py
@@ -55,7 +55,7 @@ def process_users(results_tree):
 
 
 def convert_code(text):
-    return base64.b64decode(text).decode('utf-8').split('\n')
+    return base64.b64decode(text).decode('utf-8')
 
 
 def process_solutions(results_tree, task_id):
@@ -128,8 +128,9 @@ ignore_user_fields = ("user_fi",
 
 
 def add_users(base, data):
-    for userdata in data['results'].values():
+    for userdata in data['results']:
         userdata['identifier'] = userdata['user_fi']
+        userdata['username'] = userdata['user_fi']
         for field in ignore_user_fields:
             userdata.pop(field)
     base['students'] = data['results']
diff --git a/lib/xls.py b/lib/xls.py
index a625ce8..0e13a16 100755
--- a/lib/xls.py
+++ b/lib/xls.py
@@ -50,7 +50,7 @@ task_head_re = re.compile(r'^Quellcode Frage (?P<title>.*?) ?(\d{8})?$')
 # nor parsing the weird mat no
 matno_re = re.compile(r'^(?P<matrikel_no>\d{8})-(\d+)-(\d+)$')
 
-COLUMNS_BEFORE_TASKS = 19
+TABWIDTH = 4
 
 
 def converter(infile, usernames=None, number_of_tasks=0,):
@@ -94,7 +94,9 @@ def converter(infile, usernames=None, number_of_tasks=0,):
                 'type': 'SourceCode'
             }
             root[-1].append(task.group('title'))
-            root[-1].append(urllib.parse.unquote(code).strip())
+            root[-1].append(urllib.parse
+                            .unquote(code)
+                            .replace('\t', ' ' * TABWIDTH))
 
     if number_of_tasks:
         for (user, *task_list) in sorted(root, key=lambda u: u[0].name):
diff --git a/setup.py b/setup.py
index 2740835..8fdbb95 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@ from setuptools import setup
 
 setup(
     name='hektor',
-    version='0.3',
+    version='0.3.1',
     description='A QTI-XML/XLS to JSON converter for humans',
     author='Jan Maximilian Michal',
     author_email='mail@janmax.org',
-- 
GitLab