#!/usr/bin/python3 # CodeQ: an online programming tutor. # Copyright (C) 2015 UL FRI # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import sys class Action: def __init__(self, abstime, data): self.type = data['typ'] self.time = abstime # time from start # generic actions if self.type == 'open': self.timestamp = data['time'] elif self.type == 'ins': self.type = 'insert' self.offset = data['off'] self.text = data['txt'] self.length = len(self.text) elif self.type == 'rm': self.type = 'remove' self.offset = data['off'] self.text = data['txt'] self.length = len(self.text) elif self.type == 'test': # data['feedback'] is a 'test_results' hint object, or a list # containing such hint hint = None if isinstance(data['feedback'], list): for hint in data['feedback']: if hint['id'] == 'test_results': break else: hint = data['feedback'] if hint is not None: self.n_correct = hint['args']['passed'] self.n_all = hint['args']['total'] else: self.n_correct = self.n_all = None elif self.type == 'hint': self.feedback = data['feedback'] elif self.type == 'hnt': # obsolete Prolog hint action, with no additional info self.type = 'hint' self.feedback = None elif self.type == 'plan': self.index = data.get('index') # Prolog actions elif self.type == 'prolog_solve': self.query = data['query'] elif self.type == 'slva': # obsolete Prolog "solve all" action self.type = 'prolog_solve' self.query = data['qry'] elif self.type == 'experiment': self.data = data['data'] # Python actions elif self.type == 'python_input': self.text = data['txt'] elif self.type == 'python_run': self.program = data['program'] # robot actions elif self.type == 'robot_run': self.program = data['program'] def __str__(self): s = 't = ' + str(self.time/1000.0) + ' ' + self.type if self.type in {'insert', 'remove'}: s += ': "' + self.text.replace('\n', '\\n').replace('\t', '\\t') + '" at ' + str(self.offset) elif self.type == 'test': s += ': {0} / {1}'.format(self.n_correct, self.n_all) elif self.type == 'hint': if self.feedback is not None: s += ': ' + ', '.join(sorted([hint['id'] for hint in self.feedback])) else: s += ': ?' elif self.type == 'plan': if self.index is not None: s += ': ' + str(self.index) elif self.type == 'prolog_solve': s += ': "' + self.query + '"' elif self.type == 'experiment': s += ': ' + self.data; return s # apply this action to text def apply(self, text): if self.type == 'insert': return text[:self.offset] + self.text + text[self.offset:] elif self.type == 'remove': return text[:self.offset] + text[self.offset+self.length:] else: return text # reverse the application of this action def unapply(self, text): if self.type == 'insert': return text[:self.offset] + text[self.offset+self.length:] elif self.type == 'remove': return text[:self.offset] + self.text + text[self.offset:] else: return text # parse log from database into a list of actions, cleaning up some fluff. # ignore non-text actions (queries and tests) def parse(data): if data == None: return [], [] actions = [] incorrect = set() time = 0 code = '' for packet in data: try: time += packet['dt'] action = Action(time, packet) except: # ignore any errors while decoding a packet sys.stderr.write('Error decoding packet: {}\n'.format(packet)) continue # skip normalization if this is the first action if actions == []: actions.append(action) code = action.apply(code) continue # add to list of actions; modify previously added action if necessary prev = actions[-1] # remove superfluous REMOVE action when newline is inserted (due to editor auto-indent) if prev.type == 'remove' and action.type == 'insert' and \ action.time == prev.time and \ action.offset == prev.offset and action.length > prev.length and \ action.text[action.length-prev.length:] == prev.text: # discard last REMOVE action code = prev.unapply(code) actions.pop() # replace current action with something better length = action.length - prev.length new = Action(prev.time, {'typ': 'ins', 'off': prev.offset, 'txt': action.text[:length]}) actions.append(new) code = new.apply(code) # remove superfluous INSERT action when newline is removed (due to editor auto-indent) elif prev.type == 'remove' and action.type == 'insert' and \ action.time == prev.time and \ action.offset == prev.offset and action.length < prev.length and \ prev.text[prev.length-action.length:] == action.text: # discard last INSERT action code = prev.unapply(code) actions.pop() # replace current action with something better length = prev.length - action.length new = Action(prev.time, {'typ': 'rem', 'off': prev.offset, 'txt': prev.text[:length]}) actions.append(new) code = new.apply(code) # otherwise, simply append the current action else: actions.append(action) code = action.apply(code) return actions # expand any multi-char actions (does not do anything for the 99%) def expand(actions): i = 0 while i < len(actions): if actions[i].type == 'insert' and len(actions[i].text) > 1: a = actions.pop(i) for offset in range(len(a.text)): actions.insert(i+offset, Action(a.time, {'typ': 'ins', 'off': a.offset+offset, 'txt': a.text[offset]})) i += len(a.text) elif actions[i].type == 'remove' and len(actions[i].text) > 1: a = actions.pop(i) for offset in range(len(a.text)): actions.insert(i, Action(a.time, {'typ': 'rm', 'off': a.offset+offset, 'txt': a.text[offset]})) i += len(a.text) else: i += 1 # some sample code if __name__ == '__main__': from db.models import Problem, Solution # print all problem ids print('problems:') for problem in Problem.list(): print(' {}\t{}'.format(problem.id, problem.identifier)) print() pid = input('enter problem id: ') # print all attempt ids for the selected problem print('users solving problem ' + str(pid) + ':') attempts = Solution.filter(problem_id=pid) print(', '.join([str(attempt.codeq_user_id) for attempt in attempts])) print() uid = input('enter user id: ') attempt = Solution.get(problem_id=pid, codeq_user_id=uid) try: actions = parse(attempt.trace) print('read ' + str(len(actions)) + ' actions from log') print('code versions for this attempt:') code = '' for action in actions: code = action.apply(code) print(action) print(code.strip()) print() except Exception as ex: sys.stderr.write('Error parsing action log: ' + str(ex))