#!/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 . class Action: # type ∈ ['insert', 'remove', 'solve', 'solve_all', 'next', 'stop', 'test', 'hint'] # time: absolute elapsed time since the attempt started, in ms # offset: position of the first inserted/removed character # text: inserted/removed text or query # total, passed: number of test cases def __init__(self, type, time, offset=0, text='', total=0, passed=0): self.type = type self.time = time if type in {'insert', 'remove'}: self.offset = offset self.length = len(text) self.text = text elif type in {'prolog_solve', 'solve_all'}: self.query = text elif type == 'test': self.total = total self.passed = passed 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 in {'prolog_solve', 'solve_all'}: s += ' "' + self.query + '"' elif self.type == 'test': s += ' {0} / {1}'.format(self.passed, self.total) 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 _packet_action_map = { 'ins': lambda packet, time, code: Action('insert', time, offset=packet['off'], text=packet['txt']), 'rm': lambda packet, time, code: Action('remove', time, offset=packet['off'], text=packet['txt']), 'tst': lambda packet, time, code: Action('test', time, total=packet['tot'], passed=packet['pas']), 'hnt': lambda packet, time, code: Action('hint', time), 'slva': lambda packet, time, code: Action('solve_all', time, text=packet['qry']), 'prolog_solve': lambda packet, time, code: Action('prolog_solve', time, text=packet['query']), 'prolog_next': lambda packet, time, code: Action('next', time), 'prolog_end': lambda packet, time, code: Action('stop', time), } # 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 = _packet_action_map[packet['typ']](packet, time, code) except: # ignore any errors while decoding a 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('insert', prev.time, offset = prev.offset, text = 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('remove', prev.time, offset = prev.offset, text = prev.text[:length]) actions.append(new) code = new.apply(code) # discard INSERT/REMOVE pairs (typos) elif prev.type == 'insert' and action.type == 'remove' and \ action.time - prev.time < 10000 and \ action.offset == prev.offset and action.text == prev.text: # discard last and current actions code = prev.unapply(code) actions.pop() # discard REMOVE/INSERT pairs (deleted char then typed back) elif prev.type == 'remove' and action.type == 'insert' and \ action.offset == prev.offset and action.text == prev.text: # discard last and current actions code = prev.unapply(code) actions.pop() # 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('insert', a.time, a.offset+offset, 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('remove', a.time, a.offset+offset, a.text[offset])) i += len(a.text) else: i += 1 # some sample code if __name__ == '__main__': import sys, os.path sys.path.append(os.path.dirname(os.path.realpath(__file__)) + '/..') # the parent directory is the app directory 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))