import base64 import re import zipfile import lib.generic from lxml import etree class QTIConverter(lib.generic.Converter): """ XLSConverter class (Currently raw xml input is not supported) """ accepted_files = ('.zip', '.xml') def convert(self, filepath): with zipfile.ZipFile(filepath) as archive: data = dict(process_archive(archive)) return give_me_structure(data) file_regex = re.compile( r'(\d+)__(\d+)__(?P<data>results|qti|tst)_(?P<id>\d+).xml') task_id_regex = re.compile(r'il_\d+_qst_(?P<task_id>\d+)') tasks_path = ('./assessment/section') users = './tst_active/row' solutions = './tst_solutions/row[@question_fi="%s"]' lecturer_xpath = ('./MetaData/Lifecycle/Contribute' '[@Role="Author"]/Entity/text()') types_xpath = ('./item/itemmetadata/qtimetadata/qtimetadatafield/' 'fieldlabel[text()="QUESTIONTYPE"]/../fieldentry/text()') def process_qti(tree, only_of_type=('assSourceCode',), **kwargs): tasks = tree.xpath(tasks_path)[0] titles = tasks.xpath('./item/@title') types = tasks.xpath(types_xpath) ids = [re.search(task_id_regex, ident).group('task_id') for ident in tasks.xpath('./item/@ident')] texts = ['\n'.join(flow.xpath('./material/mattext/text()')) for flow in tasks.xpath('./item/presentation/flow')] return {id: {'title': title, 'text': text, 'type': type} for id, type, title, text in zip(ids, types, titles, texts) if not only_of_type or type in only_of_type} def process_users(results_tree): return [dict(row.attrib) for row in results_tree.xpath(users)] def convert_code(text): return base64.b64decode(text).decode('utf-8') def process_solutions(results_tree, task_id): return {row.attrib['active_fi']: convert_code(row.attrib['value1']) for row in results_tree.xpath(solutions % task_id)} def process_results(tree, qti=(), **kwargs): questions = qti users = process_users(tree) id2user = {user['active_id']: user for user in users} for user in users: user['submissions'] = [] for question_key, question in questions.items(): solutions = process_solutions(tree, question_key) for user_id, solution in solutions.items(): id2user[user_id]['submissions'].append({'type': question['title'], 'code': solution, 'tests': {}}) return users def process_tst(tree): title = tree.xpath('./MetaData/General/Title/text()') lecturer = tree.xpath(lecturer_xpath) return {'exam': title[0], 'author': lecturer[0]} def eval_file(archive, match, cache): funcname = 'process_' + match.group('data') with archive.open(match.string) as datafile: tree = etree.parse(datafile) return globals()[funcname](tree, **cache) def process_archive(archive): files = {match.group('data'): match for match in (re.search(file_regex, name) for name in archive.NameToInfo) if match} order = ('tst', 'qti', 'results') cache = {} for key in order: cache[key] = eval_file(archive, files[key], cache) return cache def add_meta(base, data): base.update(data['tst']) def add_tasks(base, data): base['tasks'] = list(data['qti'].values()) ignore_user_fields = ("user_fi", "active_id", "usr_id", "anonymous_id", "test_fi", "lastindex", "tries", "submitted", "submittimestamp", "tstamp", "user_criteria",) def add_users(base, data): 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'] def give_me_structure(data): base = {} add_meta(base, data) add_tasks(base, data) add_users(base, data) return base