diff --git a/hektor.py b/hektor.py index 82faed73f466ecaa01b519301f86044cd0f196be..3827b7b5cf25ecc86e8b9d603252daa9f95804b5 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 8441fb335cadc5dac896d65388b8ca545ec0af1e..bc963dc0e398bfa001d4652c471033e2b1ad6f39 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 a625ce8140d454d027b867bce880cb5abb50d998..0e13a16e16887b84da4a34c3bf80a4d889f1e958 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 27408353c14b853733d5c52f7e838419706ddc07..8fdbb9525e431833217acb375ce85ba93f158491 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',