PaferaPy Async 0.1
ASGI framework focused on simplicity and efficiency
Loading...
Searching...
No Matches
challenge.py
Go to the documentation of this file.
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4import json
5import time
6
7from statistics import median
8
9from pafera.utils import *
10
11import pafera.db
13
14from pafera.validators import *
15
20
21# Challenge types
22CHALLENGE_HOMEWORK = 1
23CHALLENGE_CLASSWORK = 2
24CHALLENGE_CLASSPARTICIPATION = 3
25CHALLENGE_QUIZ = 4
26CHALLENGE_TEST = 5
27CHALLENGE_EXAM = 6
28
29# Challenge flags
30CHALLENGE_NEEDS_ANALYSIS = 0x01
31CHALLENGE_USE_STUDYLIST = 0x02
32
33# Challenge results
34CHALLENGE_DIDNT_TRY = 0
35CHALLENGE_FAILED = 1
36CHALLENGE_COMPLETED = 2
37
38# *********************************************************************
40 """In the Pafera Learning System, we handle homework and classwork
41 by issuing challenges to students. Points are then calculated based
42 upon how the students did, and grades are automatically assigned by
43 the system.
44
45 There are a variety of challenges available, so check challenges.js
46 to see what they are and test them to see what you can do with them.
47
48 Problems for challenges can be assigned by adding lessons, individual
49 problems, or can be added straight from their study list.
50
51 Upon a challenge ending, the next time that you visit /learn/home.html,
52 student scores will be automatically calculated and applied to their
53 grades. You can then see such statistics as the average score, what
54 percentage of students passed, which problems were often answered
55 incorrectly, and such in the analysis section.
56 """
57
58 _dbfields = {
59 'rid': ('INTEGER', 'PRIMARY KEY NOT NULL',),
60 'classid': ('INT', 'NOT NULL', BlankValidator()),
61 'starttime': ('DATETIME', 'NOT NULL',),
62 'endtime': ('DATETIME', 'NOT NULL',),
63 'challenge': ('TEXT', 'NOT NULL', BlankValidator()),
64 'title': ('TRANSLATION', 'NOT NULL', BlankValidator()),
65 'instructions': ('TRANSLATION', 'NOT NULL',),
66 'question': ('TRANSLATION', 'NOT NULL',),
67 'image': ('IMAGEFILE', 'NOT NULL',),
68 'sound': ('SOUNDFILE', 'NOT NULL',),
69 'video': ('VIDEOFILE', 'NOT NULL',),
70 'files': ('NEWLINELIST', "NOT NULL DEFAULT ''",),
71 'lessontitles': ('NEWLINELIST', "NOT NULL DEFAULT ''",),
72 'lessonids': ('NEWLINELIST', "NOT NULL DEFAULT ''",),
73 'problemids': ('NEWLINELIST', "NOT NULL DEFAULT ''",),
74 'results': ('DICT', "NOT NULL DEFAULT ''",),
75 'analysis': ('DICT', "NOT NULL DEFAULT ''",),
76 'averagescore': ('INT16', 'NOT NULL DEFAULT 0',),
77 'averageright': ('INT16', 'NOT NULL DEFAULT 0',),
78 'averagepercent': ('INT16', 'NOT NULL DEFAULT 0',),
79 'percenttried': ('INT16', 'NOT NULL DEFAULT 0',),
80 'percentcomplete': ('INT16', 'NOT NULL DEFAULT 0',),
81 'minscore': ('INT16', 'NOT NULL DEFAULT 0',),
82 'minpercentage': ('INT16', 'NOT NULL DEFAULT 0',),
83 'numpoints': ('INT16', 'NOT NULL', BlankValidator()),
84 'didnttrypenalty': ('INT16', 'NOT NULL DEFAULT 0', ),
85 'numtries': ('INT16', 'NOT NULL', BlankValidator()),
86 'numproblems': ('INT16', 'NOT NULL DEFAULT 0', ),
87 'timelimit': ('INT16', 'NOT NULL DEFAULT 0',),
88 'challengetype': ('INT16', 'NOT NULL DEFAULT 0',),
89 'flags': ('INT16', 'NOT NULL DEFAULT 0',),
90 }
91 _dbindexes = ()
92 _dblinks = [
93 ]
94 _dbdisplay = [
95 'classid',
96 'challenge',
97 'lessontitles',
98 'problemids',
99 'minscore',
100 'minpercentage',
101 'numpoints'
102 ]
103 _dbflags = 0
104
105 # -------------------------------------------------------------------
106 def __init__(self):
107 super().__init__()
108
109 # -------------------------------------------------------------------
111 """Returns the number of remaining tries available to the current
112 user.
113
114 As a side effect, all previous results will be available in
115 obj.previousresults as learn_challengeresult objects.
116 """
117 if not hasattr(self, 'previousresults'):
118 self.previousresults = g.db.Find(
119 apps.learn.challengeresult.learn_challengeresult,
120 'WHERE challengeid = ? AND userid = ?',
121 [
122 self.rid,
123 g.session.userid
124 ],
125 fields = 'rid, problemresults, score, percentright',
126 orderby = 'endtime',
127 )
128
129 return Bound(self.numtries - len(self.previousresults), 0, 9999)
130
131 # -------------------------------------------------------------------
132 def Analyze(self, g):
133 """Calculates all scores and saves statistics for this challenge.
134
135 Typically, this will be done automatically for you after endtime
136 has passed.
137 """
138
139 # Points for Write Answer challenges are manually assigned by the
140 # teacher, so no statistics or points need to be calculated.
141 if self.challenge == 'Write Answer':
142 return
143
144 cls = g.db.Load(apps.learn.schoolclass.learn_schoolclass, self.classid)
145
146 currenttime = time.time()
147
148 self.results = {}
149 studentnames = {}
150 wrongproblems = {}
151 answers = {}
152
153 # Classwork scores are automatically applied to the current answer object
154 if (self.challengetype == CHALLENGE_CLASSWORK
155 or self.challengetype == CHALLENGE_CLASSPARTICIPATION
156 or self.challenge == 'Typing'
157 ):
158 for r in g.db.Find(
159 apps.learn.answer.learn_answer,
160 'WHERE classid = ? AND NOT (flags & ?)',
161 [
162 self.classid,
163 apps.learn.answer.ANSWER_TEACHER
164 ],
165 fields = 'rid, userid, displayname, numright, numwrong, bonusscore, score'
166 ):
167
168 answers[r.userid] = r
169
170 # Class participation scores are calculated from the current answer
171 # object
172 if self.challengetype == CHALLENGE_CLASSPARTICIPATION:
173 for k, v in answers.items():
174 o = v.ToJSON('displayname, numright, numwrong, bonusscore, score')
175
176 if v.numright + v.numwrong:
177 o['percentage'] = int(v.numright / (v.numright + v.numwrong) * 100)
178 else:
179 o['percentage'] = 0
180
181 self.results[ToShortCode(k)] = o
182
183 self.Set(results = self.results)
184
185 if time.time() > self.endtime.timestamp():
186 self.Set(flags = self.flags & (~CHALLENGE_NEEDS_ANALYSIS))
187
188 g.db.Update(self)
189
190 for k, v in answers.items():
191 cls.UpdateGrades(g, k)
192
193 g.db.Commit()
194 return self
195
196 # Get all students for the class and see if any failed to even attempt the challenge
197 for r in g.db.Linked(cls, apps.learn.student.learn_student, fields = 'userid, displayname'):
198 studentid = ToShortCode(r.userid)
199
200 studentnames[r.userid] = r.displayname
201
202 if studentid not in self.results:
203 self.results[studentid] = [r.displayname, CHALLENGE_DIDNT_TRY, 0, 0, -self.didnttrypenalty]
204
205 # Iterate through all results and calculate statistics
206 for r in g.db.Find(
207 apps.learn.challengeresult.learn_challengeresult,
208 'WHERE challengeid = ?',
209 self.rid
210 ):
211
212 studentid = ToShortCode(r.userid)
213
214 if r.userid not in studentnames:
215 studentnames[r.userid] = '[Removed Student ' + studentid + ']'
216 self.results[studentid] = [studentnames[r.userid], CHALLENGE_DIDNT_TRY, r.score, r.percentright, -self.didnttrypenalty]
217
218 if r.flags & apps.learn.challengeresult.CHALLENGERESULT_PASSED:
219 self.results[studentid] = [studentnames[r.userid], CHALLENGE_COMPLETED, r.score, r.percentright, self.numpoints]
220 elif self.results[studentid][1] == CHALLENGE_DIDNT_TRY or self.results[studentid][2] > r.score:
221 self.results[studentid] = [studentnames[r.userid], CHALLENGE_FAILED, r.score, r.percentright, 0]
222
223 if r.problemresults:
224 for p in r.problemresults:
225 if 'id' in p:
226 if p['status'] == 'wrong':
227 if p['id'] not in wrongproblems:
228 problemtext = '[Text not found]'
229
230 try:
231 problemtext = g.db.Execute(f'''
232 SELECT content
233 FROM learn_problem
234 JOIN learn_card ON learn_problem.problemid = learn_card.rid
235 WHERE learn_problem.rid = ?
236 ''',
237 FromShortCode(p['id'])
238 )[0]
239 except Exception as e:
240 print('>>> Problem getting problem text', e)
241
242 wrongproblems[ p['id'] ] = {
243 'problemtext': problemtext,
244 'studentids': [],
245 }
246
247 wrongproblems[ p['id'] ]['studentids'].append(ToShortCode(r.userid))
248
249 # Apply penalties if someone didn't complete their classwork
250 if self.challengetype == CHALLENGE_CLASSWORK and self.didnttrypenalty and self.endtime.timestamp() < currenttime:
251 for k, v in self.results.items():
252 if v[1] == CHALLENGE_DIDNT_TRY:
253 userid = FromShortCode(k)
254
255 if userid not in answers:
256 if userid not in studentnames:
257 studentnames[userid] = {
258 'en': f'[Removed Student {k}]'
259 }
260
261 newanswer = apps.learn.answer.learn_answer()
262 newanswer.Set(
263 classid = self.classid,
264 userid = userid,
265 displayname = studentnames[userid],
266 )
267
268 newanswer.AddHomeworkScores(g)
269
270 g.db.Insert(newanswer)
271 g.db.Commit()
272
273 answers[userid] = newanswer
274
275 answerobj = answers[userid]
276
277 answerobj.Set(
278 bonusscore = answerobj.bonusscore - self.didnttrypenalty,
279 )
280
281 answerobj.UpdateScore()
282
283 g.db.Update(answerobj)
284
285 self.Set(
286 results = self.results,
287 analysis = {
288 'wrongproblems': wrongproblems
289 }
290 )
291
292 # Calculate averages
293 numdidnttry = 0
294 numfailed = 0
295 numcompleted = 0
296 averagescore = []
297 averagepercent = []
298
299 for v in self.results.values():
300 if v[1] == CHALLENGE_DIDNT_TRY:
301 numdidnttry += 1
302 else:
303 numfailed += 1
304
305 averagescore.append(v[2])
306 averagepercent.append(v[3])
307
308 if v[1] == CHALLENGE_COMPLETED:
309 numcompleted += 1
310
311 totalids = numdidnttry + numfailed + numcompleted
312
313 percenttried = numfailed / totalids * 100
314 percentcomplete = numcompleted / totalids * 100
315
316 self.Set(
317 percenttried = percenttried,
318 percentcomplete = percentcomplete,
319 averagescore = median(averagescore) if averagescore else 0,
320 averagepercent = median(averagepercent) if averagepercent else 0,
321 )
322
323 # If endtime has passed, then no further data will be added and we don't
324 # need to do this again
325 if currenttime > self.endtime.timestamp():
326 self.Set(flags = self.flags & (~CHALLENGE_NEEDS_ANALYSIS))
327
328 g.db.Update(self)
329
330# =====================================================================
331def GetCurrentChallenges(g, classid, challengetype = CHALLENGE_HOMEWORK):
332 """Returns a list of all currently running challenges for the logged in user.
333
334 Useful for seeing what this student has finished and what they still
335 need to do.
336 """
337
338 challenges = []
339 currenttime = time.time()
340
341 for r in g.db.Find(
342 learn_challenge,
343 'WHERE classid = ? AND starttime < ? AND endtime > ? AND challengetype = ?',
344 [
345 classid,
346 currenttime,
347 currenttime,
348 challengetype,
349 ],
350 fields = 'rid, challenge, title, endtime, numpoints, numtries, flags',
351 ):
352
353 completed = 0
354
355 results = g.db.Find(
356 apps.learn.learn_challengeresult,
357 'WHERE challengeid = ? AND userid = ?',
358 [r.rid, g.session.userid],
359 fields = 'rid, flags'
360 )
361
362 for s in results:
363 if s.flags & apps.learn.challengeresult.CHALLENGERESULT_PASSED:
364 completed = 1
365 break
366
367 c = r.ToJSON('rid, challenge, title, endtime, numpoints, numtries, flags')
368
369 c['triesleft'] = Bound(r.numtries - len(results), 0, 9999)
370 c['completed'] = completed
371 c['timeremaining'] = int(r.endtime.timestamp() - currenttime)
372
373 challenges.append(c)
374
375 return challenges
In the Pafera Learning System, we handle homework and classwork by issuing challenges to students.
Definition: challenge.py:39
def __init__(self)
Initialize all fields at creation like a good programmer should.
Definition: challenge.py:106
def GetNumRemainingAttempts(self, g)
Returns the number of remaining tries available to the current user.
Definition: challenge.py:110
def Analyze(self, g)
Calculates all scores and saves statistics for this challenge.
Definition: challenge.py:132
Base class for all database models.
Definition: modelbase.py:20
def Set(self, **kwargs)
We use this method instead of direct attribute access in order to keep track of what values have been...
Definition: modelbase.py:115
Throws an exception on blank values.
Definition: validators.py:43
def GetCurrentChallenges(g, classid, challengetype=CHALLENGE_HOMEWORK)
Returns a list of all currently running challenges for the logged in user.
Definition: challenge.py:331
Definition: db.py:1
def Bound(val, min, max)
Returns a value that is no smaller than min or larger than max.
Definition: utils.py:21
def FromShortCode(code, chars='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_')
Turns a six character alphanumeric code into a 32-bit value.
Definition: utils.py:64
def ToShortCode(val, chars='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_')
Turns a 32-bit value into a six character alphanumeric code.
Definition: utils.py:36