|
| 1 | +import sqlite3 |
| 2 | +import os, tabulate, numpy |
| 3 | +from six import print_ |
| 4 | + |
| 5 | +#Jeremy Reizenstein 2016 |
| 6 | +#This is a simple module for managing the marks to parts of an assignment |
| 7 | +#It relies internally on a single sqlite database |
| 8 | + |
| 9 | +#Each candidate can be awarded marks of either numeric or text types. |
| 10 | +#A numeric mark type has an OUT_OF value, and optionally a weighting. If a mark type has no weighting, it is taken to be the same as OUT_OF. weightings are used to calculate the total percentage marks. |
| 11 | +# The vast majority of public functions are just for reporting. Aside from initial table creation, the db is only ever modified by the last few functions in this file. They can be disabled by calling disableMutation |
| 12 | +#mark types can be tagged, this facility isn't really used |
| 13 | +#There is no string handling in this file - unicode in comes out as unicode. If you have a non-ascii character in a mark, maybe you need to add something like -fno-color-diagnostics in your call to the compiler |
| 14 | +#candidates can either be specified by candidate number or a string consisting of the candidate number followed by "d". |
| 15 | + |
| 16 | +file = "marks.sqlite" |
| 17 | +if os.environ.has_key("MARKS_FILE"): |
| 18 | + file = os.environ["MARKS_FILE"] |
| 19 | + |
| 20 | +con = sqlite3.connect(file) |
| 21 | +if 1: |
| 22 | + c = con.cursor() |
| 23 | + c.execute("CREATE TABLE IF NOT EXISTS MARKS (CANDIDATE INT, TYPE TEXT, VALUE, OVERRIDDEN)") |
| 24 | + c.execute("CREATE TABLE IF NOT EXISTS TYPES (NAME TEXT, DESCRIPTION TEXT, NOTES TEXT, OUT_OF REAL, WEIGHTING REAL, IMPORTANCE INT)") |
| 25 | + c.execute("CREATE TABLE IF NOT EXISTS TYPE_TAG (TAG TEXT, MARKTYPE TEXT)") |
| 26 | + con.commit() |
| 27 | + |
| 28 | +def candidateToInt(candidate): |
| 29 | + if type(candidate)==str and candidate[-1]=="d": |
| 30 | + return int(candidate[:-1]) |
| 31 | + return int(candidate) |
| 32 | + |
| 33 | +def _getMark(markType, candidate):#candidate must be int |
| 34 | + i = con.execute("select value from marks where overridden is null and candidate = ? and type = ?",(candidate,markType)).fetchall() |
| 35 | + if len(i)==0: |
| 36 | + return None |
| 37 | + if len(i)>1: |
| 38 | + raise RuntimeError("more than one value") |
| 39 | + return i[0][0] |
| 40 | + |
| 41 | +def truncate(s, n=10): |
| 42 | + if (str==type(s) or unicode == type(s)) and len(s)>n: |
| 43 | + return s[:n] |
| 44 | + return s |
| 45 | + |
| 46 | +def getMark(markType, candidate): |
| 47 | + assertMarkType(markType) |
| 48 | + return _getMark(markType, candidateToInt(candidate)) |
| 49 | + |
| 50 | +def tab(tag=None): |
| 51 | + headers = listMarkTypes() if (tag is None) else listMarksWithTag(tag) |
| 52 | + rows = listCandidates() |
| 53 | + out = [[r]+[truncate(_getMark(markType, r)) for markType in headers] for r in rows] |
| 54 | + headers = [truncate(i,12) for i in headers] |
| 55 | + print (tabulate.tabulate(out, headers = ["candidate"] + headers)) |
| 56 | + |
| 57 | +def checkAllMarksAssigned(): |
| 58 | + cs = listCandidates() |
| 59 | + for markType in listMarkTypes(): |
| 60 | + for candidate in cs: |
| 61 | + if _getMark(markType,candidate) is None: |
| 62 | + print ("no "+markType+" for "+str(candidate)) |
| 63 | + |
| 64 | +def tabNumeric(file = False): |
| 65 | +# total=True |
| 66 | + headers = listNumeric() |
| 67 | + rows = listCandidates() |
| 68 | + out = [[r]+[truncate(_getMark(markType, r)) for markType in headers] + [candidateTotalPercentage_(r)] for r in rows] |
| 69 | + headers.append("TOTAL") |
| 70 | + tab = tabulate.tabulate(out, headers = ["candidate"] + headers) |
| 71 | + if file: |
| 72 | + with open("numericSummary.txt","w") as f: |
| 73 | + print_(tab,file=f) |
| 74 | + else: |
| 75 | + print (tab) |
| 76 | + |
| 77 | +def listMarkTypes(): |
| 78 | + i = con.execute("select name from types ").fetchall() |
| 79 | + return [ii for (ii,) in i] |
| 80 | +def listNumeric(): |
| 81 | + i = con.execute("select name from types where OUT_OF is not null").fetchall() |
| 82 | + return [ii for (ii,) in i] |
| 83 | +def listNonNumeric(): |
| 84 | + i = con.execute("select name from types where OUT_OF is null").fetchall() |
| 85 | + return [ii for (ii,) in i] |
| 86 | +def listCandidates(): |
| 87 | + i = con.execute("select distinct(candidate) from marks where OVERRIDDEN is null order by candidate").fetchall() |
| 88 | + return [ii for (ii,) in i] |
| 89 | +def listTags(): |
| 90 | + i = con.execute("select distinct(tag) from type_tag order by tag").fetchall() |
| 91 | + return [ii for (ii,) in i] |
| 92 | +def listTagsOfMark(marktype): |
| 93 | + i = con.execute("select tag from type_tag where marktype = ? order by tag", (marktype,)).fetchall() |
| 94 | + return [ii for (ii,) in i] |
| 95 | +def listMarksWithTag(tag): |
| 96 | + i = con.execute("select marktype from type_tag where tag = ?", (tag,)).fetchall() |
| 97 | + return [ii for (ii,) in i] |
| 98 | + |
| 99 | +def hasMark(candidate, markType): |
| 100 | + assertMarkType(markType) |
| 101 | + return _getMark(markType, candidateToInt(candidate)) is not None |
| 102 | +def hasTag(mark, tag): |
| 103 | + return (1,) == con.execute("select count(*) from type_tag where marktype = ? and tag = ?", (mark,tag)).fetchone() |
| 104 | + |
| 105 | +def isMarkType(marktype): |
| 106 | + check = con.execute("SELECT COUNT(*) FROM TYPES WHERE NAME = ?", (marktype,)).fetchone() |
| 107 | + return check == (1,) |
| 108 | +def assertMarkType(marktype): |
| 109 | + if not isMarkType(marktype): |
| 110 | + raise RuntimeError(str(marktype) + " is not a type of mark I know of") |
| 111 | + |
| 112 | +def isUsedTag(tag): |
| 113 | + return (0,) != con.execute("select count(*) from type_tag where tag = ?", (tag,)).fetchone() |
| 114 | + |
| 115 | +def tagTable(): |
| 116 | + headers = listTags() |
| 117 | + rows = listMarkTypes() |
| 118 | + out = [ [r]+["+" if hasTag(r,t) else " " for t in headers] for r in rows] |
| 119 | + print (tabulate.tabulate(out, headers = [""]+headers)) |
| 120 | + |
| 121 | +def m(): |
| 122 | + return con.execute("select * from types").fetchall() |
| 123 | +def n(): |
| 124 | + return con.execute("select * from marks").fetchall() |
| 125 | +def t(): |
| 126 | + return con.execute("select * from type_tag").fetchall() |
| 127 | + |
| 128 | +#def out_of_total(): |
| 129 | +# return con.execute("select sum(OUT_OF) from types").fetchone()[0] |
| 130 | +def candidateTotalPercentage_(candidate): #candidate an int |
| 131 | + total = 0.0 |
| 132 | + denominator = 0.0 |
| 133 | + for markType in listNumeric(): |
| 134 | + mark = _getMark(markType,candidate) |
| 135 | + if mark is None: |
| 136 | + print ("no "+markType+" for "+str(candidate)) |
| 137 | + out_of,weighting = con.execute("select OUT_OF, WEIGHTING from types where name = ?",(markType,)).fetchone() |
| 138 | + if weighting is None: |
| 139 | + weighting = out_of |
| 140 | + denominator = denominator + weighting |
| 141 | + total = total + float(mark) * float(weighting) / float(out_of) |
| 142 | + return 100*total / denominator |
| 143 | + |
| 144 | +def totalWeighting(): |
| 145 | + denominator = 0.0 |
| 146 | + for markType in listNumeric(): |
| 147 | + out_of,weighting = con.execute("select OUT_OF, WEIGHTING from types where name = ?",(markType,)).fetchone() |
| 148 | + if weighting is None: |
| 149 | + weighting = out_of |
| 150 | + denominator = denominator + weighting |
| 151 | + return denominator |
| 152 | + |
| 153 | +def tableWeightings(): |
| 154 | + denominator = 0.0 |
| 155 | + a=[] |
| 156 | + for markType in listNumeric(): |
| 157 | + out_of,weighting = con.execute("select OUT_OF, WEIGHTING from types where name = ?",(markType,)).fetchone() |
| 158 | + if weighting is None: |
| 159 | + weighting = out_of |
| 160 | + denominator = denominator + weighting |
| 161 | + a.append([markType,out_of,weighting]) |
| 162 | + print ("totalWeighting: "+str(denominator)) |
| 163 | + for aa in a: |
| 164 | + aa.append(100*aa[-1]/denominator) |
| 165 | + print(tabulate.tabulate(a,headers=("name","out of","weight","percentage"))) |
| 166 | + |
| 167 | +def candidateTotalPercentage(candidate, decimalPlaces = 0): |
| 168 | + return round(candidateTotalPercentage_(candidateToInt(candidate)),decimalPlaces) |
| 169 | + |
| 170 | +def distribution(): |
| 171 | + a=numpy.round(numpy.sort([candidateTotalPercentage_(c) for c in listCandidates()])) |
| 172 | + print_ (numpy.mean(a), numpy.std(a)) |
| 173 | + return a |
| 174 | + |
| 175 | +def printMarks(markType): |
| 176 | + for c in listCandidates(): |
| 177 | + print_ (c, _getMark(markType,c)) |
| 178 | + |
| 179 | +def writeSummary(): |
| 180 | + with open("MarkingSummary.txt","w") as f: |
| 181 | + num = listNumeric() |
| 182 | + outofs=dict() |
| 183 | + def p(x): print_(x, file=f) |
| 184 | + p("Marks ") |
| 185 | + totalWeight=totalWeighting() |
| 186 | + for markType in num: |
| 187 | + out_of,weighting, descr = con.execute("select OUT_OF, WEIGHTING, DESCRIPTION from types where name = ?",(markType,)).fetchone() |
| 188 | + outofs[markType]=out_of |
| 189 | + if descr is None: |
| 190 | + descr = "" |
| 191 | + elif len(descr)>1: |
| 192 | + descr = "("+descr+")" |
| 193 | + if weighting is None: |
| 194 | + weighting = out_of |
| 195 | + percentage = 100.0 * weighting / totalWeight |
| 196 | + print_(markType,descr, "out of", out_of, " worth "+str(percentage)+"%", file=f) |
| 197 | + for candidate in listCandidates(): |
| 198 | + print_("\n",candidate, ": ", round(candidateTotalPercentage_(candidate),1), "%", file=f, sep="") |
| 199 | + for markType in listMarkTypes(): |
| 200 | + if markType in num: |
| 201 | + print_(markType,": ",_getMark(markType,candidate),"/", outofs[markType],file=f,sep="") |
| 202 | + else: |
| 203 | + res = _getMark(markType,candidate) |
| 204 | + if(len(unicode(res))>1): |
| 205 | + |
| 206 | + p(markType) |
| 207 | + p(res.encode("iso-8859-1")) |
| 208 | + |
| 209 | +#Mutation functions are below here |
| 210 | +mutationEnabled = True |
| 211 | + |
| 212 | +def disableMutation(): |
| 213 | + global mutationEnabled |
| 214 | + mutationEnabled=False |
| 215 | + |
| 216 | +def startMutation(): |
| 217 | + if not mutationEnabled: |
| 218 | + raise RuntimeError("no mutation allowed") |
| 219 | + |
| 220 | +def newMark(name, description, outOf = None): #supply outOf if the mark is to be a numeric value |
| 221 | + startMutation() |
| 222 | + if isMarkType(name): |
| 223 | + raise RuntimeError(str(name) + " is a type of mark I already know of") |
| 224 | + c=con.cursor() |
| 225 | + c.execute("INSERT INTO TYPES (NAME, DESCRIPTION, NOTES, OUT_OF, WEIGHTING, IMPORTANCE) VALUES (?,?,'',?,NULL,1)",(name,description,outOf)) |
| 226 | + con.commit() |
| 227 | + |
| 228 | +def setWeighting(markType, weighting): |
| 229 | + startMutation() |
| 230 | + if markType not in listNumeric(): |
| 231 | + raise RuntimeError(str(markType) + " is not a type of numeric mark I already know of") |
| 232 | + if type(weighting) not in (type(None), float,int): |
| 233 | + raise RuntimeError("not a good weight type") |
| 234 | + c.execute("update types set WEIGHTING=? WHERE NAME = ?",(weighting,markType)) |
| 235 | + con.commit() |
| 236 | + |
| 237 | +def tagMark(marktype, tag, remove = False): |
| 238 | + startMutation() |
| 239 | + assertMarkType(marktype) |
| 240 | + if (not remove) and hasTag(marktype, tag): |
| 241 | + raise RuntimeError(str(marktype) + " is already tagged with "+tag) |
| 242 | + if remove and not hasTag(marktype, tag): |
| 243 | + raise RuntimeError(str(marktype) + " is not already tagged with "+tag) |
| 244 | + if remove: |
| 245 | + con.execute("delete from type_tag where marktype = ? and tag = ?", (marktype, tag)) |
| 246 | + else: |
| 247 | + con.execute("insert into type_tag (marktype, tag) values (?,?)", (marktype, tag)) |
| 248 | + con.commit() |
| 249 | + |
| 250 | +def _removeMark(candidate, marktype, c): |
| 251 | + c.execute ("UPDATE MARKS SET OVERRIDDEN = DATETIME('NOW') WHERE CANDIDATE = ? AND TYPE = ? and OVERRIDDEN is NULL", (candidate, marktype)) |
| 252 | + |
| 253 | +def removeMark(candidate, marktype): |
| 254 | + startMutation() |
| 255 | + c = con.cursor() |
| 256 | + assertMarkType(marktype) |
| 257 | + if not hasMark(candidate, markType): |
| 258 | + raise RuntimeError(str(marktype) + " has not been given to "+candidate) |
| 259 | + _removeMark(candidateToInt(candidate), marktype, c) |
| 260 | + con.commit() |
| 261 | + |
| 262 | +def assignMark(candidate, marktype, value, force = False): |
| 263 | + startMutation() |
| 264 | + candidate = candidateToInt(candidate) |
| 265 | + c = con.cursor() |
| 266 | + assertMarkType(marktype) |
| 267 | + isNumeric = c.execute("SELECT OUT_OF is not NULL from TYPES where name = ?", (marktype,)).fetchone() |
| 268 | + isNumeric = isNumeric != (0,) |
| 269 | + if isNumeric and type(value) == str: |
| 270 | + raise RuntimeError(str(marktype) + " is a numeric mark. It cannot be given this value.") |
| 271 | + #perhaps should check numeric marks are in range. |
| 272 | + if _getMark(marktype,candidate) is not None: |
| 273 | + if force: |
| 274 | + _removeMark(candidate, marktype, c) |
| 275 | + else: |
| 276 | + raise RuntimeError(str(marktype) + " has already been assigned to "+str(candidate)) |
| 277 | + c.execute("INSERT INTO MARKS (CANDIDATE, TYPE, VALUE, OVERRIDDEN) VALUES (?,?,?,NULL)",(candidate,marktype,value)) |
| 278 | + con.commit() |
| 279 | + |
0 commit comments