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:
- How can I make it run faster?
- How can I write it in a OOP paradigm, and will it run faster?
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from# __future__The importmaster print_function
importdata sys
importset os
importis re
fromcontained xlrdin importexcel open_workbook
importfile xlwt
from('moodle.xls'). xlwtThe importtoponomy easyxf
fromfor operatorthe importactivity itemgetter,is attrgetter
fromthe timeone importin gmtime,a strftime2.5+ moodle instance. The colums in excel follow the format
from# datetime importcolumns datetime= module name/date,time/(Not timedeltaused):e.g. IP address/name student/activity type (full)/activity name
from# xlutils.copy importrows copy= chronological order in time period
if# sysThe 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.version
# >Each '3'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 unicodea =log str(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
class MoodleCheat(object):
def __init__(self, log_filepath, source_filepath):
self.source_filepath = source_filepath
self.log_filepath = log_filepath
self.students_entries = []
self.xl_out = []
# The typical running time for self.valid_quizzesthe =class []
on a 8GB Mac with dual processor OSX self10.done_quizzes = []
self9.list_of_quizzes = []
3 is 31 seconds to find self.inlisted_studentsthe =quiz []
activity of 120 students for 3 quizzes amongst self.listed_students12k =students []records
self.stylo = {
'normal': easyxf('pattern: pattern no_fill;'
+ 'font: name Arial,'
from +mmap 'heightimport 260mmap, italic True'
),ACCESS_READ
'answer':from easyxf('pattern:xlrd patternimport solid_fillopen_workbook,'
+ 'fore_colour yellow;'
+ 'font: name ArialXL_CELL_TEXT,'
+ 'height 260'
)cellname,
'problem': easyxf('pattern: pattern solid_fillcellnameabs,'
+ 'fore_colour gray50;'
+ 'font: name Arial,'colname
+ 'heightimport 260'xlwt
from xlwt import )Workbook,
'too long': easyxf('pattern: pattern solid_fill, '
#from +xlutils.styles 'fore_colourimport light_yellow;'Styles
+from 'font:operator nameimport Arial,'itemgetter
from +time 'heightimport 260gmtime,'
+ 'bold True'
)strftime,
'way too long': easyxf('pattern: pattern solid_fillstrptime, '
+ 'fore_colour light_orange;'clock
+from 'font:datetime nameimport Arialdatetime,'
+ 'height 260'timedelta
from xlutils.copy import )}copy
defclass moodledateconvertmoodlecheat(self, date_inobject):
""" function to return a moodle date in date.time/python convention """
split_date = ' '.join(re.findall(r"[\w']+"mois={'january':'jan', date_in))
return datetime.strptime(split_date'February':'Feb', '%d %B %Y %I %M %p')'March':'Mar','April':'Apr','May':'May','June':'Jun','July':'Jul','August':'Aug','September':'Sep','October':'Oct','November':'Nov','December':'Dec'}
def timegap__init__(self, time_latelogfilepath, time_earlysourcefilepath):
"""self.sourcefilepath calculates= sourcefilepath
between the difference between 2 dates self.logfilepath=logfilepath
format
e self.gstudentsentries=[]
self.xlout=[]
<'01 September 2001, 11:50AM'> -> time/python convention""" self.validquizzes=[]
returnself.donequizzes=[]
self.moodledateconvert(time_late)listofquizzes=[]
- self.moodledateconvert(time_early)inactivestudents=[]
self.activestudents=[]
def openworkbook( 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'),
""" open the workbook containing the activity entries. """3:easyxf('pattern: pattern solid_fill, fore_colour gray50;' 'font: name Arial, height 260'),
self.work_book = open_workbook 4:easyxf(os.path.join'pattern: pattern solid_fill, fore_colour light_yellow;' 'font: name Arial, height 260, bold True'),
5:easyxf(self.source_filepath'pattern: +pattern 'activitylog.xlsx'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 on_demand=True 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 extractstudentslistudconv(self,datein):
"""d=datein.split(',')[0]
extract the list of all students from thed1=datein.split(',')[1]
2nd worksheet 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
Keyword arguments:
listed_students -- list of students, a flag showing taking a quiz
"""
self.sheet = self.work_book.sheet_by_index(1)
self.listed_students = [Student(_student) for _student in self.sheet.col_values(0)]
self.listed_students.sort(key=attrgetter('_name'))
def extractstudents_entries(self):
""" Extract all entries for the students listed in [listed_students].
Keyword arguments:
students_entries# -- all entries for the students listed
row -- row number in the data entries worksheet
"""
------------------------ self.sheetMethods =----------------------------------------------- self.work_book.sheet_by_index(0)
for row indef rangeopenworkbook(self.sheet.nrows):
for x in self.listed_students:
if x._name == self.sheet.cell_value(row, 3):
self.students_entries.append(Entry(*self.sheet.row_values(row)))
break
self.students_entrieswb = sortedopen_workbook(self.students_entriessourcefilepath+'activitylog.xlsx',
key=attrgetter('_name'))
self.students_entries.reverse(on_demand=True)
def listquizzesextractstudentslist(self):
"""# Extract the list [activestudents] of all quizesstudents thatfrom werethe completed3rd 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)]
Keyworddef argumentsextractstudentsentries(self):
valid_quizzes -- list of tuples (student name, quizzes# completed)
Extract activity from the master dataset <studentsentries> for _entryall --the anstudents entrylisted in the<activestudents>
[students_entries] list
self.sh = """self.wb.sheet_by_index(0)
for _entryr in range(self.students_entriessh.nrows):
if [self.sh.cell_value(r,3),False] in self.activestudents:
_entry._action[:10] == 'quiz close'self.studentsentries.append(self.sh.row_values(r))
anddef listquizzes(_entry._name, _entry._infoself):
# list all quizes that were completed into <validquizzes>
notfor x in self.valid_quizzesstudentsentries:
if x[4][:10]=='quiz close' and [x[3],x[5]] not in )self.validquizzes:
self.valid_quizzesvalidquizzes.append((_entry._name[x[3], _entry._info)x[5],-1])
def fliptakingflip_quiz(self, _searched_studentitem,series):
""" Changes# themethod attributeto forchange the student classrecord to show name is taking quiz."""
for _student in self.listed_students:
if _student._namea ==_searched_student:quiz
_student._taking_quizposition = not (_student._taking_quiz)
break
def flipactive[(selfi, _searched_student):
""" Changes the flag for the student to show is taking quiz."""
for _student in self.listed_students:
if _student._name ==_searched_student:
_studentname._active = True
break
def istakingindex(self, _searched_studentitem):
""" Changes the flag) for the student to show has completed a quiz."""
fori, _studentname in self.listed_students:
if _student._name ==_searched_student:
return _student._taking_quiz
def logtimestampenumerate(self, messageseries):
""" Writes a message with timeif stampitem in sourcefilepath/log.txt."""
self.message = messagename]
with open(os.path.joinseries[position[0][0]][(self.log_filepath, 'log.txt'position[0][1]), 'a'+1]) as file:
file.write('\n == {} at {}'.
format(
self.message,
strftime=not('%Y-%m-%d %H:%M:%S',gmtimeseries[position[0][0]][())
position[0][1])+1])
def logmessagelogtimestamp(self, message):
""" Writes# awrites message<message> in log.txt inat sourcefilepath/log.txt."""
self.message = messagemessage=message
withflog=open open(os.path.join(self.log_filepath, 'loglogfilepath+'log.txt'), 'a'"a")
as file:
flog.write('\n == '+self.message+ ' at '+strftime("%Y-%m-%d file.write%H:%M:%S", gmtime('\n')))
+ print self.message)
def plogmessage(self, namemessage):
""" for test, returns# thewrites value<message> ofin log.taking_quiz attributetxt at \logfilepath
for _searched_studentself.
"""message=message
for x inflog=open (self.listed_students:
if xlogfilepath+'log._name ==txt', n._name:"a")
flog.write('\n'+self.message) print (x._name, x._taking_quiz)
def perstudentquizreport(self):
""" logs quiz information for# eachwrites studentstart and xl ouput table.
Keyword arguments:
xl_out -- list of tuples (student name, quizzes completed)
_entry -- an entry in the [students_entries] list
completion times for each students """
self.logmessage ('\n\nReport on quizzes')
self.logmessage ('=================\n')
self.old_timeoldtime = str('25 April 2014, 11:41 AM')
# Main loop
for _recordm in (sorted(reversed(self.students_entriesstudentsentries), key=itemgetter(3))):
# log messagesself.sc=self.stylo[1]
and set flags for each entry when in quiz
# If student starting a quiz and quiz has self.scnot =been self.stylo['normal']
completed yet => log the entries into [quizzdb], log the quizz name ifinto (([validquizzes], flip the flag to show student is taking a quiz _record._action.split(' (')[0] == 'quiz continue attempt' \
if (m[4].split(' (')[0]=='quiz continue attempt' or _record._actionm[4].split(' (')[0][0]=='quiz ==attempt' '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)):
and (_record self._nameflip_quiz(m[3], _recordself._infoactivestudents)
not in self.done_quizzes \
self.logmessage (m[3]+' has made first answer in the quiz and'+m[5]+ self ' at '+m[1].istakingsplit(_record._name', ')[1])
== False \
and ((_record._name, _record._info) in selfif (m[4].valid_quizzes
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.fliptakingflip_quiz(_recordm[3],self._nameactivestudents)
self.logmessage( '{} has made first answer(m[3]+' inretook 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' .formatand (
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]])
_record self._nameflip_quiz(m[3],self.activestudents)
self.logmessage(m[3]+' has finished the quiz '+m[5]+ ' at '+m[1].split(', ')[1])
_recordself._info,logmessage(('-')*130)
if (self.takingquiz(m[3],self.activestudents)==True):
if m[4][:4]!='quiz': # _record._time.split(',')[1]
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]
if ( (_record._action.split(' (')[0] == 'quiz continue attempt' \
or _record._action.split(' (')[0] == 'quiz attempt'
)) \
and (_record._name, _record._info) in self.done_quizzes \
and self.istaking(_record._name) == False \
and ((_record._name, _record._info) in self.valid_quizzes
):
self.fliptaking(_record._name)
self.logmessage('{} retook the quiz {} at {} -This is an issue' +
' if the quiz is not a practice quiz'
.format(
_record._name,
_record._info,
_record._time.split(',')[1]
))
elif _record._action[:10] == 'quiz close':
self.done_quizzes.append((_record._name, _record._info))
self.fliptaking(_record._name)
self.flipactive(_record._name)
self.logmessage('{} has finished the quiz {} at {}'
.format(
_record._name,
_record._info,
_record._time.splittimegap('m[1],')[1]
))
self.logmessage(('-'oldtime) * 130)
# unusual actions during a valid quiz
if self.istaking>timedelta(_record._nameminutes=1) == True:
if _record._action[:4] != 'quiz':
self.logmessage('!! -- {} {} at {}'
.format(
_record._action.split('('),
_record._info,
_record._time.split(',')[1]
# More than 1 minute for an ))action
self.sc = self.stylo['problem']
elif ((self.timegap(_record._time, selfsc=self.old_time))
> timedelta(minutes=1)):stylo[4]
self.sc = self.stylo['too long']
else:
self.scsc=self.stylo[2]# =Answering self.stylo['answer']quiz
if (self.timegap(_record._timem[1], self.old_timeoldtime)) > timedelta>timedelta(minutes=3):
# More than 3 minutes for an action
self.logmessage('!! - Time gap between answers is greater' +
'greater than 3 minutes. '+unicode({}) at {}'
.format(
self.timegap(_record._timem[1], self.old_timeoldtime),
))+' at _record._time'+unicode(m[1].split(', ')[1]
))
self.sc = self.stylo['way too long']stylo[5]
self.xl_out.append([_record._name,
if _record._time(m[4].split(', (')[1],
[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] _record._info,]
self.validquizzes.remove([m[3],m[5],self.quest[0]])
_recordself._actionvalidquizzes.split('append(')[0][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(_record._timem[1], self.old_timeoldtime),
self.sc]
) #(self.takingquiz(m[3],self.activestudents))
self.old_timeoldtime = _record._timem[1]
def listallquizzes(self):
""" Creates # report a list of all quizzes
taken by students."""
self.logmessage('\nList of quizzes submitted by the students')
self.logmessage(('=') * 47*47)
for i in self.done_quizzesdonequizzes:
if i[1] not in self.list_of_quizzeslistofquizzes:
self.list_of_quizzeslistofquizzes.append(i[1])
self.logmessage(i[1])
def listinlisted_studentslistinactivestudents(self):
""" Write the names # report a list of students who didn't take a quiz
in log.txt. """
self.logmessage('\nList of students who did not take any quiz')
self.logmessage(('=') * 37*37)
self.inactivestudents=[x for x in ([i[0] for i in self.listed_students:
activestudents]) if x not in ([j[0] for j in self.donequizzes])]
if x._active == False for i in self.inactivestudents:
self.logmessage(x._namei)
# ------------------------------------- xl writing ---------------------------------------------
def createallstudentfiles(self):
"""# CreatesCreate anindividual excelxl filefiles for each student.the """students
for self.x in [i for i in self.listed_studentsactivestudents if i[0] not in self.inactivestudents]:
self.wpath = os.path.join((selfwpath=self.log_filepath),strlogfilepath+str(self.x._namex[0]) +'.xls')
# Creates a workbook w in memory for a student
self.w = xlwt.Workbook(encoding="utf-8") # Creates new workbook for the students
self.sheetsh = self.w.add_sheet('Activity')
# Writes theself.w.save(self.wpath) workbook# wCreate atall wpaththe fileworkbooks for that student
self.w.save(self.wpath)
def fillingindividualstudentfiles(self):
""" Fill in each workbook created for a student with xl_out data. """
self.name = 'empty'
self.row = 0
for self.x in self.xl_out:
self.row += 1
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
def fillingindividualstudentfiles(self): # next student
# Fill in individual xl files for the liststudents
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 lastxlout studentis not ordered by name in the list?loop
iffor self.y in range(1,5):
self.x[0]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.namew.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.xl_outxlout.index(self.x) == (len(self.xl_outxlout) - 1)): #next student in the list or student last in the list
self.w.save(self.wpath)
self.name = selfname=self.x[0]
self.row = 0row=0
self.wpath = os.path.join((selfwpath=self.log_filepath),
strlogfilepath+str(self.x[0]) +'.xls'
)
# re-open existing workbook for student
self.w = copyw=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.extractstudents_entriesextractstudentsentries()
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.listinlisted_studentslistinactivestudents()
self.logtimestamp('Found missing students and listed quizzes')
self.logtimestamp('Writing individual student xl files')
self.createallstudentfiles()
self.fillingindividualstudentfiles()
self.logtimestamp('Students files written')
class Student(MoodleCheat):
""" class describing individual students with name and flags. """
def __init__(self, name, active=False, taking_quiz=False):
self._name = name
self._active = active
self._taking_quiz = taking_quiz
## For further optimisations
## def is_active(self):
## return self._active
##
## def is_taking_quiz(self):
## return self._taking_quiz
class Entry(MoodleCheat):
""" class describing master data set in source excel file. """
def __init__(self, module, time, ip_addr, name, action, info):
self._module = module
self._time = time
self._ip_addr = ip_addr
self._name = name
self._action = action
self._info = info
## For optimisation
##if __name__ == '__main__':
## import cProfile
moo =moo=moodlecheat 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.