Below is a script to find cheating students in a quiz in a daily moodle activity log exported in .xls. It works fine and is written in a procedural way. Essentially the script isolates the activity log entry in a worksheet for students listed in another worksheet, identifies when they are taking a quiz (looking ahead in the activity log) and if they are taking an unusually long time or doing something different while taking a quiz (they will complete). These different scenarios are then highlighted in a different row colour in an xl file listing all activity for each student in the list. The script also writes to a .txt file, logging a summary for each student and all quizzes completed and the students listed who did not take any quizzes. The questions are simple: 1. How can I make it run faster? 2. How can I write it in a OOP paradigm, and will it run faster? <!--- ---> #!/usr/bin/env python # -*- coding: utf-8 -*- # The master data set is contained in excel file ('moodle.xls'). The toponomy for the activity is the one in a 2.5+ moodle instance. The colums in excel follow the format # columns = module name/date,time/(Not used):e.g. IP address/name student/activity type (full)/activity name # rows = chronological order in time period # The class (moodlecheat) returns: a log containing a summary of cheats per students, a set of individual xl files with a log per student with quiz question highlighted. # Each row is colour coded to facilitate detection: yellow/any valid quiz question answered | light yellow/any question which is answered in more than 2 minutes | grey/any non quiz activity during a quiz | Red/any activity taking longer than 5 minutes during a quiz # The data listed in a log (log.tx) which details the running times per module and the activity for all students highlighting any suspected cheating # Worksheet 1 = xl report for moodle activity log for the time period e.g. a day # Worksheet 2 = Name of users/students whose quiz data is to be mined out # The typical running time for the class on a 8GB Mac with dual processor OSX 10.9.3 is 31 seconds to find the quiz activity of 120 students for 3 quizzes amongst 12k students records from mmap import mmap, ACCESS_READ from xlrd import open_workbook, XL_CELL_TEXT, cellname, cellnameabs, colname import xlwt from xlwt import Workbook, easyxf #from xlutils.styles import Styles from operator import itemgetter from time import gmtime, strftime, strptime, clock from datetime import datetime, timedelta from xlutils.copy import copy class moodlecheat(object): mois={'january':'jan','February':'Feb','March':'Mar','April':'Apr','May':'May','June':'Jun','July':'Jul','August':'Aug','September':'Sep','October':'Oct','November':'Nov','December':'Dec'} def __init__(self, logfilepath, sourcefilepath): self.sourcefilepath = sourcefilepath self.logfilepath=logfilepath self.studentsentries=[] self.xlout=[] self.validquizzes=[] self.donequizzes=[] self.listofquizzes=[] self.inactivestudents=[] self.activestudents=[] self.stylo = {1:easyxf('pattern: pattern no_fill;' 'font: name Arial, height 260, italic True'), 2:easyxf('pattern: pattern solid_fill, fore_colour yellow;' 'font: name Arial, height 260'), 3:easyxf('pattern: pattern solid_fill, fore_colour gray50;' 'font: name Arial, height 260'), 4:easyxf('pattern: pattern solid_fill, fore_colour light_yellow;' 'font: name Arial, height 260, bold True'), 5:easyxf('pattern: pattern solid_fill, fore_colour light_orange;' 'font: name Arial, height 260')} # //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// # 0 1 2 3 4 5 # studentsentries module name date,time IP address name student activity type (full) activity name # xlout name student, time activity name activity type (short) time since previous question xl file colour row # valid quizzes name student activity name number of question answered in quiz. # done quizzes name student activity name # done quizzes name student activity name # list of quizzes activity name # inactivestudents name student # active students name student flag student taking quiz # 0 1 # ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// # ------------------------------- Functions ------------------------- # function returning True when a student is taking a quiz def takingquiz(self,item,series): position = [(i, name.index(item)) for i, name in enumerate(series) if item in name] return (series[position[0][0]][(position[0][1])+1]) # function to return a moodle date in date.time/python convention <y=01 m=11 d=1 sec=50 msec=00> <- unicode strings/moodle moodle convention <01 September 2001, 11:50AM> def udconv(self,datein): d=datein.split(',')[0] d1=datein.split(',')[1] d2=d+d1 d3 = moodlecheat.mois[d2.split(' ')[1]] d4=d2.split(' ')[0]+' '+d3+' '+d2.split(' ')[2]+' '+d2.split(' ')[3]+d2.split(' ')[4] d5 = datetime.strptime(d4, '%d %b %Y %I:%M%p') return d5 # function to calculate between 2 dates in unicode string/moodle log format e.g. <'01 September 2001, 11:50AM'> -> time/python convention def timegap(self,i,j): self.gap = self.udconv(i) - self.udconv(j) return self.gap # ---------------------------- Methods ----------------------------------------------- def openworkbook(self): self.wb = open_workbook(self.sourcefilepath+'activitylog.xlsx', on_demand=True) def extractstudentslist(self): # Extract the list [activestudents] of all students from the 3rd worksheet (list student names) in moodle.xlsx self.sh = self.wb.sheet_by_index(1) self.activestudents = [[x,False] for x in self.sh.col_values(0)] def extractstudentsentries(self): # Extract activity from the master dataset <studentsentries> for all the students listed in <activestudents> self.sh = self.wb.sheet_by_index(0) for r in range(self.sh.nrows): if [self.sh.cell_value(r,3),False] in self.activestudents: self.studentsentries.append(self.sh.row_values(r)) def listquizzes(self): # list all quizes that were completed into <validquizzes> for x in self.studentsentries: if x[4][:10]=='quiz close' and [x[3],x[5]] not in self.validquizzes: self.validquizzes.append([x[3],x[5],-1]) def flip_quiz(self,item,series): # method to change the student record to show taking a quiz position = [(i, name.index(item)) for i, name in enumerate(series) if item in name] (series[position[0][0]][(position[0][1])+1])=not(series[position[0][0]][(position[0][1])+1]) def logtimestamp(self,message): # writes <message> in log.txt at sourcefilepath self.message=message flog=open (self.logfilepath+'log.txt', "a") flog.write('\n == '+self.message+ ' at '+strftime("%Y-%m-%d %H:%M:%S", gmtime())) print self.message def logmessage(self,message): # writes <message> in log.txt at logfilepath self.message=message flog=open (self.logfilepath+'log.txt', "a") flog.write('\n'+self.message) def perstudentquizreport(self): # writes start and completion times for each students self.logmessage ('\n\nReport on quizzes') self.logmessage ('=================\n') self.oldtime = str('25 April 2014, 11:41 AM') for m in (sorted(reversed(self.studentsentries), key=itemgetter(3))): self.sc=self.stylo[1] # If student starting a quiz and quiz has not been completed yet => log the entries into [quizzdb], log the quizz name into [validquizzes], flip the flag to show student is taking a quiz if (m[4].split(' (')[0]=='quiz continue attempt' or m[4].split(' (')[0]=='quiz attempt' ) and (self.takingquiz(m[3],self.activestudents)==False) and (any(p[0]==m[3] and p[1]==m[5] for p in self.validquizzes)) and not (any(p[0]==m[3] and p[1]==m[5] for p in self.donequizzes)): self.flip_quiz(m[3],self.activestudents) self.logmessage (m[3]+' has made first answer in the quiz '+m[5]+ ' at '+m[1].split(', ')[1]) if (m[4].split(' (')[0]=='quiz continue attempt' or m[4].split(' (')[0]=='quiz attempt' ) and (self.takingquiz(m[3],self.activestudents)==False) and (any(p[0]==m[3] and p[1]==m[5] for p in self.validquizzes)) and (any(p[0]==m[3] and p[1]==m[5] for p in self.donequizzes)): self.flip_quiz(m[3],self.activestudents) self.logmessage (m[3]+' retook the quiz '+m[5]+ ' at '+m[1].split(', ')[1]+' - This is an issue if the quiz is not a practice quiz') elif m[4][:10]=='quiz close' and (any(p[0]==m[3] and p[1]==m[5] for p in self.validquizzes)): # finished a quiz self.donequizzes.append([m[3],m[5]]) self.flip_quiz(m[3],self.activestudents) self.logmessage(m[3]+' has finished the quiz '+m[5]+ ' at '+m[1].split(', ')[1]) self.logmessage(('-')*130) if (self.takingquiz(m[3],self.activestudents)==True): if m[4][:4]!='quiz': # unusual action during the quiz self.logmessage('!! -- '+unicode(m[4].split('(')[0])+' '+unicode(m[5])+' at '+unicode(m[1].split(', ')[1])) self.sc = self.stylo[3] elif (self.timegap(m[1],self.oldtime))>timedelta(minutes=1): # More than 1 minute for an action self.sc=self.stylo[4] else: self.sc=self.stylo[2]# Answering quiz if (self.timegap(m[1],self.oldtime))>timedelta(minutes=3): # More than 3 minutes for an action self.logmessage('!! - Time gap between answers is greater than 3 minutes. '+unicode((self.timegap(m[1],self.oldtime)))+' at '+unicode(m[1].split(', ')[1])) self.sc = self.stylo[5] if (m[4].split(' (')[0]=='quiz continue attempt' or m[4].split(' (')[0]=='quiz attempt'): #log the number of questions answered self.quest=[ l[2] for l in self.validquizzes if l[0]==m[3] and l[1]==m[5] ] self.validquizzes.remove([m[3],m[5],self.quest[0]]) self.validquizzes.append([m[3],m[5],self.quest[0]+1]) self.xlout.append([m[3], m[1].split(', ')[1], m[5], m[4].split('(')[0], self.timegap(m[1],self.oldtime),self.sc]) #(self.takingquiz(m[3],self.activestudents)) self.oldtime = m[1] def listallquizzes(self): # report a list of quizzes self.logmessage('\nList of quizzes submitted by the students') self.logmessage(('=')*47) for i in self.donequizzes: if i[1] not in self.listofquizzes: self.listofquizzes.append(i[1]) self.logmessage(i[1]) def listinactivestudents(self): # report a list of students who didn't take a quiz self.logmessage('\nList of students who did not take any quiz') self.logmessage(('=')*37) self.inactivestudents=[x for x in ([i[0] for i in self.activestudents]) if x not in ([j[0] for j in self.donequizzes])] for i in self.inactivestudents: self.logmessage(i) # ------------------------------------- xl writing --------------------------------------------- def createallstudentfiles(self): # Create individual xl files for the students for self.x in [i for i in self.activestudents if i[0] not in self.inactivestudents]: self.wpath=self.logfilepath+str(self.x[0])+'.xls' self.w = xlwt.Workbook(encoding="utf-8") # Creates new workbook for the students self.sh = self.w.add_sheet('Activity') self.w.save(self.wpath) # Create all the workbooks for that student def fillingindividualstudentfiles(self): # Fill in individual xl files for the students self.name='empty' self.row=0 for self.x in (sorted(self.xlout, key=itemgetter(0))): self.row +=1 # row=(open_workbook(wpath).sheet_by_index(0).nrows) use if studentsentries or xlout is not ordered by name in the loop for self.y in range(1,5): self.w.get_sheet(0).write(self.row, self.y-1, str(self.x[self.y]),self.x[5]) self.w.get_sheet(0).col(0).width = 256*19 self.w.get_sheet(0).col(1).width = 256*90 self.w.get_sheet(0).col(2).width = 256*30 self.w.get_sheet(0).col(3).width = 256*30 if self.x[0]!=self.name or self.xlout.index(self.x)==(len(self.xlout)-1): #next student in the list or student last in the list self.w.save(self.wpath) self.name=self.x[0] self.row=0 self.wpath=self.logfilepath+str(self.x[0])+'.xls' self.w=copy(open_workbook(self.wpath, formatting_info=True, on_demand=True)) #Re-open existing workbook for student and makes a copy in w for editing def main(self): self.logtimestamp('Importing the data from the workbook started') self.openworkbook() self.logtimestamp('Data uploaded') self.logtimestamp('Uploading students list') self.extractstudentslist() self.logtimestamp('Students list uploaded') self.logtimestamp('Dumping data in holding table started') self.extractstudentsentries() self.logtimestamp('Data dumped ') self.logtimestamp('Creating a list of quizzes completed started') self.listquizzes() self.logtimestamp('List of quizzes completed dumped') self.logtimestamp('Data processing started') self.perstudentquizreport() self.logtimestamp('Data processing complete') self.logtimestamp('Finding missing students and listing quizzes') self.listallquizzes() self.listinactivestudents() self.logtimestamp('Found missing students and listed quizzes') self.logtimestamp('Writing individual student xl files') self.createallstudentfiles() self.fillingindividualstudentfiles() self.logtimestamp('Students files written') moo=moodlecheat ('/Users/macadmin1/Desktop/moodle reports/Reports/','/Users/macadmin1/Desktop/moodle reports/') moo.main() Any feedback is welcome, especially about defining data structures and classes that would supplant list. I think I understand (to an extent) how to write a simple procedural script like this one in Python and I really want to move onto OOP now.