Skip to content

Commit 1e00e8b

Browse files
authored
enhance: update live quiz response logic to support temporary participant accounts (#4727)
1 parent 5008128 commit 1e00e8b

3 files changed

Lines changed: 244 additions & 118 deletions

File tree

apps/func-incoming-responses/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ const httpTrigger = async function (
7474
if (
7575
typeof participantData !== 'string' &&
7676
participantData.sub &&
77-
participantData.role === 'PARTICIPANT'
77+
(participantData.role === 'PARTICIPANT' ||
78+
participantData.role === 'TEMPORARY_PARTICIPANT')
7879
) {
7980
messageId = `${participantData.sub}-${body.sessionId}`
8081
}

apps/func-response-processor/src/index.ts

Lines changed: 151 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,48 @@ Sentry.init()
3030

3131
const redisExec = getRedis()
3232

33+
function updateLeaderboards({
34+
redisMulti,
35+
participantId,
36+
participantRole,
37+
sessionKey,
38+
sessionBlockId,
39+
pointsAwarded,
40+
xpAwarded,
41+
}: {
42+
redisMulti: ChainableCommander
43+
participantId: string
44+
participantRole: string
45+
sessionKey: string
46+
sessionBlockId: string
47+
pointsAwarded: number
48+
xpAwarded: number
49+
}) {
50+
// depending on the participant account type (permanent student account or
51+
// temporary pseudonym), set the correct points / experience points
52+
if (participantRole === 'PARTICIPANT') {
53+
redisMulti.hincrby(
54+
`${sessionKey}:b:${sessionBlockId}:lb`,
55+
participantId,
56+
pointsAwarded
57+
)
58+
redisMulti.hincrby(`${sessionKey}:lb`, participantId, pointsAwarded)
59+
redisMulti.hincrby(`${sessionKey}:xp`, participantId, xpAwarded)
60+
} else if (participantRole === 'TEMPORARY_PARTICIPANT') {
61+
// temporary participants are only granted points, xp cannot be collected
62+
redisMulti.hincrby(
63+
`${sessionKey}:b:${sessionBlockId}:lbTemporary`,
64+
participantId,
65+
pointsAwarded
66+
)
67+
redisMulti.hincrby(
68+
`${sessionKey}:lbTemporary`,
69+
participantId,
70+
pointsAwarded
71+
)
72+
}
73+
}
74+
3375
interface Message {
3476
messageId: string
3577
sessionId: string
@@ -96,7 +138,12 @@ const serviceBusTrigger = async function (
96138
process.env.APP_SECRET
97139
) as { sub: string; role: string }
98140

99-
if (participantData.role !== 'PARTICIPANT') {
141+
if (
142+
!(
143+
participantData.role === 'PARTICIPANT' ||
144+
participantData.role === 'TEMPORARY_PARTICIPANT'
145+
)
146+
) {
100147
participantData = null
101148
} else {
102149
context.log("Participant's JWT verified", participantData)
@@ -106,12 +153,15 @@ const serviceBusTrigger = async function (
106153
context.error('JWT verification failed', e, queueItem.cookie)
107154
Sentry.captureException(e)
108155
}
156+
109157
// if the participant has already responded to the question instance, return instantly
110158
if (
111159
participantData &&
112160
(await redisExec.hexists(
113161
`${instanceKey}:responses`,
114-
participantData.sub
162+
participantData.role === 'TEMPORARY_PARTICIPANT'
163+
? `temporary-${participantData.sub}`
164+
: participantData.sub
115165
))
116166
) {
117167
context.log(
@@ -120,13 +170,13 @@ const serviceBusTrigger = async function (
120170
return { status: 200 }
121171
}
122172
}
173+
123174
const instanceInfo = await redisExec.hgetall(`${instanceKey}:info`)
124175
// if the instance metadata is not available, it has been closed and purged already
125176
if (!instanceInfo) {
126177
context.log('Question instance metadata not found', queueItem)
127178
return { status: 400 }
128179
}
129-
130180
context.log('Instance info', instanceInfo)
131181

132182
const {
@@ -156,12 +206,15 @@ const serviceBusTrigger = async function (
156206
case 'SC':
157207
case 'MC':
158208
case 'KPRIM': {
209+
// add the vote to the aggregated results
159210
response.choices.forEach((choiceIndex: number) => {
160211
redisMulti.hincrby(`${instanceKey}:results`, String(choiceIndex), 1)
161212
})
162213
redisMulti.hincrby(`${instanceKey}:results`, 'participants', 1)
214+
215+
// if the participant was logged in, award points (and xp if regular student acount was used)
163216
if (participantData) {
164-
let pointsPercentage
217+
let pointsPercentage: number | null
165218
if (type === 'SC') {
166219
pointsPercentage = gradeQuestionSC({
167220
responseCount: Number(choiceCount),
@@ -219,27 +272,31 @@ const serviceBusTrigger = async function (
219272
responseTimestamp
220273
)
221274
}
275+
222276
redisMulti.hset(
223277
`${instanceKey}:responses`,
224-
participantData.sub,
278+
participantData.role === 'TEMPORARY_PARTICIPANT'
279+
? `temporary-${participantData.sub}`
280+
: participantData.sub,
225281
`[${String(response.choices)}]`
226282
)
227-
redisMulti.hincrby(
228-
`${sessionKey}:b:${sessionBlockId}:lb`,
229-
participantData.sub,
230-
pointsAwarded
231-
)
232-
redisMulti.hincrby(
233-
`${sessionKey}:lb`,
234-
participantData.sub,
235-
pointsAwarded
236-
)
237-
redisMulti.hincrby(`${sessionKey}:xp`, participantData.sub, xpAwarded)
283+
284+
// update both the regular and temporary live quiz leaderboards
285+
updateLeaderboards({
286+
redisMulti,
287+
participantId: participantData.sub,
288+
participantRole: participantData.role,
289+
sessionKey,
290+
sessionBlockId,
291+
pointsAwarded,
292+
xpAwarded,
293+
})
238294
}
239295
break
240296
}
241297
// TODO: points based on distance to correct range?
242298
case 'NUMERICAL': {
299+
// add the response to the aggregated results
243300
const MD5 = createHash('md5')
244301
MD5.update(response.value)
245302
const responseHash = MD5.digest('hex')
@@ -251,19 +308,20 @@ const serviceBusTrigger = async function (
251308
)
252309
redisMulti.hincrby(`${instanceKey}:results`, 'participants', 1)
253310

254-
const exactSolutionsDefined =
255-
typeof parsedSolutions !== 'undefined' &&
256-
parsedSolutions.length > 0 &&
257-
(typeof parsedSolutions[0] === 'number' ||
258-
typeof parsedSolutions[0] === 'string')
259-
260-
const answerCorrect = gradeQuestionNumerical({
261-
response: response.value,
262-
solutionRanges: exactSolutionsDefined ? undefined : parsedSolutions,
263-
exactSolutions: exactSolutionsDefined ? parsedSolutions : undefined,
264-
})
265-
311+
// if the participant was logged in, award points (and xp if regular student acount was used)
266312
if (participantData) {
313+
const exactSolutionsDefined =
314+
typeof parsedSolutions !== 'undefined' &&
315+
parsedSolutions.length > 0 &&
316+
(typeof parsedSolutions[0] === 'number' ||
317+
typeof parsedSolutions[0] === 'string')
318+
319+
const answerCorrect = gradeQuestionNumerical({
320+
response: response.value,
321+
solutionRanges: exactSolutionsDefined ? undefined : parsedSolutions,
322+
exactSolutions: exactSolutionsDefined ? parsedSolutions : undefined,
323+
})
324+
267325
pointsAwarded = computeAwardedPoints({
268326
firstResponseReceivedAt,
269327
responseTimestamp,
@@ -298,29 +356,32 @@ const serviceBusTrigger = async function (
298356
responseTimestamp
299357
)
300358
}
359+
301360
redisMulti.hset(
302361
`${instanceKey}:responses`,
303-
participantData.sub,
362+
participantData.role === 'TEMPORARY_PARTICIPANT'
363+
? `temporary-${participantData.sub}`
364+
: participantData.sub,
304365
String(response.value)
305366
)
306-
redisMulti.hincrby(
307-
`${sessionKey}:b:${sessionBlockId}:lb`,
308-
participantData.sub,
309-
pointsAwarded
310-
)
311-
redisMulti.hincrby(
312-
`${sessionKey}:lb`,
313-
participantData.sub,
314-
pointsAwarded
315-
)
316-
redisMulti.hincrby(`${sessionKey}:xp`, participantData.sub, xpAwarded)
367+
368+
// update both the regular and temporary live quiz leaderboards
369+
updateLeaderboards({
370+
redisMulti,
371+
participantId: participantData.sub,
372+
participantRole: participantData.role,
373+
sessionKey,
374+
sessionBlockId,
375+
pointsAwarded,
376+
xpAwarded,
377+
})
317378
}
318379
break
319380
}
320381
// TODO: future -> distance in embedding space?
321382
case 'FREE_TEXT': {
383+
// add the response to the aggregated results
322384
const cleanResponseValue = toLowerCase(response.value.trim())
323-
324385
const MD5 = createHash('md5')
325386
MD5.update(cleanResponseValue)
326387
const responseHash = MD5.digest('hex')
@@ -331,6 +392,8 @@ const serviceBusTrigger = async function (
331392
cleanResponseValue
332393
)
333394
redisMulti.hincrby(`${instanceKey}:results`, 'participants', 1)
395+
396+
// if the participant was logged in, award points (and xp if regular student acount was used)
334397
if (participantData) {
335398
const answerCorrect = gradeQuestionFreeText({
336399
response: cleanResponseValue,
@@ -371,26 +434,30 @@ const serviceBusTrigger = async function (
371434
responseTimestamp
372435
)
373436
}
437+
374438
redisMulti.hset(
375439
`${instanceKey}:responses`,
376-
participantData.sub,
440+
participantData.role === 'TEMPORARY_PARTICIPANT'
441+
? `temporary-${participantData.sub}`
442+
: participantData.sub,
377443
cleanResponseValue
378444
)
379-
redisMulti.hincrby(
380-
`${sessionKey}:b:${sessionBlockId}:lb`,
381-
participantData.sub,
382-
pointsAwarded
383-
)
384-
redisMulti.hincrby(
385-
`${sessionKey}:lb`,
386-
participantData.sub,
387-
pointsAwarded
388-
)
389-
redisMulti.hincrby(`${sessionKey}:xp`, participantData.sub, xpAwarded)
445+
446+
// update both the regular and temporary live quiz leaderboards
447+
updateLeaderboards({
448+
redisMulti,
449+
participantId: participantData.sub,
450+
participantRole: participantData.role,
451+
sessionKey,
452+
sessionBlockId,
453+
pointsAwarded,
454+
xpAwarded,
455+
})
390456
}
391457
break
392458
}
393459
case 'SELECTION': {
460+
// add the response to the aggregated results
394461
response.selection.forEach((answerId: number) => {
395462
// skipped input fields should not be considered
396463
if (answerId === -1) {
@@ -401,6 +468,7 @@ const serviceBusTrigger = async function (
401468
})
402469
redisMulti.hincrby(`${instanceKey}:results`, 'participants', 1)
403470

471+
// if the participant was logged in, award points (and xp if regular student acount was used)
404472
if (participantData) {
405473
const pointsPercentage = gradeQuestionSelection({
406474
numberOfInputs: parseInt(instanceInfo.numberOfInputs),
@@ -450,24 +518,27 @@ const serviceBusTrigger = async function (
450518

451519
redisMulti.hset(
452520
`${instanceKey}:responses`,
453-
participantData.sub,
521+
participantData.role === 'TEMPORARY_PARTICIPANT'
522+
? `temporary-${participantData.sub}`
523+
: participantData.sub,
454524
`[${String(response.selection.filter((r: number) => r !== -1))}]` // filter out skipped response fields
455525
)
456-
redisMulti.hincrby(
457-
`${sessionKey}:b:${sessionBlockId}:lb`,
458-
participantData.sub,
459-
pointsAwarded
460-
)
461-
redisMulti.hincrby(
462-
`${sessionKey}:lb`,
463-
participantData.sub,
464-
pointsAwarded
465-
)
466-
redisMulti.hincrby(`${sessionKey}:xp`, participantData.sub, xpAwarded)
526+
527+
// update both the regular and temporary live quiz leaderboards
528+
updateLeaderboards({
529+
redisMulti,
530+
participantId: participantData.sub,
531+
participantRole: participantData.role,
532+
sessionKey,
533+
sessionBlockId,
534+
pointsAwarded,
535+
xpAwarded,
536+
})
467537
}
468538
break
469539
}
470540
case 'CASE_STUDY': {
541+
// add the response to the aggregated results
471542
Object.entries(response.assessment).forEach(([caseId, caseData]) => {
472543
Object.entries(caseData).forEach(([itemId, itemData]) => {
473544
Object.entries(itemData).forEach(
@@ -500,6 +571,7 @@ const serviceBusTrigger = async function (
500571
// increment participant count
501572
redisMulti.hincrby(`${instanceKey}:results`, 'participants', 1)
502573

574+
// if the participant was logged in, award points (and xp if regular student acount was used)
503575
if (participantData) {
504576
const pointsPercentage = gradeQuestionCaseStudy({
505577
response: response.assessment,
@@ -546,20 +618,22 @@ const serviceBusTrigger = async function (
546618
}
547619
redisMulti.hset(
548620
`${instanceKey}:responses`,
549-
participantData.sub,
621+
participantData.role === 'TEMPORARY_PARTICIPANT'
622+
? `temporary-${participantData.sub}`
623+
: participantData.sub,
550624
JSON.stringify(response.assessment)
551625
)
552-
redisMulti.hincrby(
553-
`${sessionKey}:b:${sessionBlockId}:lb`,
554-
participantData.sub,
555-
pointsAwarded
556-
)
557-
redisMulti.hincrby(
558-
`${sessionKey}:lb`,
559-
participantData.sub,
560-
pointsAwarded
561-
)
562-
redisMulti.hincrby(`${sessionKey}:xp`, participantData.sub, xpAwarded)
626+
627+
// update both the regular and temporary live quiz leaderboards
628+
updateLeaderboards({
629+
redisMulti,
630+
participantId: participantData.sub,
631+
participantRole: participantData.role,
632+
sessionKey,
633+
sessionBlockId,
634+
pointsAwarded,
635+
xpAwarded,
636+
})
563637
}
564638

565639
break

0 commit comments

Comments
 (0)