Skip to content

Commit f04b767

Browse files
committed
Added logging for CodeTailor puzzles to a new table.
1 parent dd89589 commit f04b767

8 files changed

Lines changed: 198 additions & 65 deletions

File tree

bases/rsptx/book_server_api/routers/rslogging.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from rsptx.db.crud import (
3939
create_answer_table_entry,
4040
create_code_entry,
41+
create_code_tailor_parsons_entry,
4142
create_useinfo_entry,
4243
create_user_chapter_progress_entry,
4344
create_user_state_entry,
@@ -60,6 +61,7 @@
6061
from rsptx.db.models import (
6162
AuthUserValidator,
6263
CodeValidator,
64+
CodeTailorParsonsValidator,
6365
runestone_component_dict,
6466
UseinfoValidation,
6567
)
@@ -68,6 +70,7 @@
6870
LastPageDataIncoming,
6971
LogItemIncoming,
7072
LogRunIncoming,
73+
LogCodeTailorIncoming,
7174
TimezoneRequest,
7275
ReadingAssignmentSpec,
7376
)
@@ -309,6 +312,55 @@ async def runlog(request: Request, response: Response, data: LogRunIncoming):
309312

310313
return make_json_response(status=status.HTTP_201_CREATED)
311314

315+
# codetailorlog endpoint
316+
# ---------------
317+
# The :ref:`logCodeTailorEvent` client-side function calls this endpoint to record a CodeTailor puzzle creation
318+
@router.post("/codetailor_log")
319+
async def codetailor_log(request: Request, response: Response, data: LogCodeTailorIncoming):
320+
"""Make an entry in the CodeTailorParsons table to record a CodeTailor Parsons puzzle creation.
321+
322+
:param request: A FastAPI Request object
323+
:param response: A FastAPI Response object
324+
:type response: JSONResponse
325+
:param data: the post data
326+
:type data: LogCodeTailorIncoming
327+
:return: JSONResponse
328+
"""
329+
rslogger.debug(f"INCOMING: {data}")
330+
331+
# --- Auth / session checks (same pattern as /runlog) ---
332+
if request.state.user:
333+
if data.course != request.state.user.course_name:
334+
return make_json_response(
335+
status=status.HTTP_401_UNAUTHORIZED,
336+
detail="You appear to have changed courses in another tab. Please switch to this course",
337+
)
338+
sid = request.state.user.username
339+
course_id = request.state.user.course_id
340+
else:
341+
if data.clientLoginStatus is True:
342+
rslogger.error("Session Expired")
343+
return make_json_response(
344+
status=status.HTTP_401_UNAUTHORIZED,
345+
detail="Session Expired",
346+
)
347+
return make_json_response(status=status.HTTP_401_UNAUTHORIZED)
348+
349+
codetailor_row = {
350+
"timestamp": canonical_utcnow(),
351+
"sid": sid,
352+
"ctid": data.ctid,
353+
"course_id": course_id,
354+
"htmlsrc": data.htmlsrc,
355+
"question_json": data.question_json,
356+
}
357+
358+
entry = CodeTailorParsonsValidator(**codetailor_row)
359+
await create_code_tailor_parsons_entry(entry)
360+
361+
return make_json_response(status=status.HTTP_201_CREATED)
362+
363+
312364

313365
async def same_class(user1: AuthUserValidator, user2: str) -> bool:
314366
if user1:

bases/rsptx/interactives/runestone/activecode/js/activecode.js

Lines changed: 33 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ export class ActiveCode extends RunestoneBase {
8080
this.code = $(orig).text() || "\n\n\n\n\n";
8181
this.parsonspersonalize = $(orig).data("parsonspersonalize"); // CodeTailor: <choose 1 out of two> Allows the instructor to choose personalization level: solution-level or block-and-solution level
8282
this.parsonsexample = $(orig).data("parsonsexample"); // CodeTailor: <choose 1 out of two> Allows the instructor to choose an example Parsons problem or LLM-example (default auto-fill, means the example is generated by LLM)
83-
this.puzzleScaffoldingDivid = `help_puzzle_${this.parsonspersonalize}_${this.divid}`; // CodeTailor: the div id for the puzzle scaffolding help window
83+
this.codetailortimestamp = Math.floor(Date.now() / 1000);
84+
this.puzzleScaffoldingDivid = `${this.divid}_codetailor_${this.parsonspersonalize}_${this.sid}_${this.codetailortimestamp}`; // CodeTailor: the div id for the puzzle scaffolding help window
8485
this.language = $(orig).data("lang");
8586
this.timelimit = $(orig).data("timelimit");
8687
this.highlightLines = $(orig).data("highlight-lines");
@@ -635,6 +636,7 @@ export class ActiveCode extends RunestoneBase {
635636
return promise;
636637
}
637638

639+
638640
// isNewHelp: when true, this handler was not called by pressing the button.
639641
// it was used to open a new generated help.
640642
async reopenHelpBtnHandler(isNewHelp) {
@@ -693,9 +695,9 @@ export class ActiveCode extends RunestoneBase {
693695
reopen: true,
694696
}
695697
this.logBookEvent({
696-
event: "gptparsons_base_reopen",
698+
event: "codetailor_base_reopen",
697699
act: JSON.stringify(reopen_act),
698-
div_id: this.divid
700+
div_id: this.puzzleScaffoldingDivid
699701
});
700702
return;
701703
}
@@ -732,66 +734,45 @@ export class ActiveCode extends RunestoneBase {
732734

733735
// Function to handle sending the rest of the data and splitting large strings into chunks
734736
const logActInParts = (act) => {
735-
// First, send the smaller fields (non-chunked) with event name "gptparsons-0"
737+
// First, send the smaller fields (non-chunked) with event name "codetailor-0"
736738
const basicData = {
737739
type: act.type,
738740
reopen: act.reopen,
739741
generation_type: act.generation_type,
740742
solution_type: act.solution_type,
741743
};
742744

743-
// First, send the smaller fields (non-chunked) with event name "gptparsons-0"
745+
// First, send the smaller fields (non-chunked) with event name "codetailor-0"
744746
// // console.log("Sending basic data:", basicData);
745747
this.logBookEvent({
746-
event: "gptparsons_base", // Special event name for the first log
748+
event: "codetailor_base", // Special event name for the first log
747749
act: JSON.stringify(basicData),
748-
div_id: this.divid
750+
div_id: this.puzzleScaffoldingDivid
749751
});
750752

751753
// Split and send the code chunks
752-
if (act.code_answer) {
753-
let chunkIndex = 1; // Initialize a single counter for all chunks
754-
const codeChunks = splitIntoChunks(act.code_answer, 512);
755-
codeChunks.forEach((chunk) => {
756-
this.logBookEvent({
757-
event: `gptparsons_code_${chunkIndex}`,
758-
act: JSON.stringify({
759-
reopen: !isNewHelp,
760-
type: 'code',
761-
content: chunk,
762-
chunkIndex: chunkIndex,
763-
totalChunks: codeChunks.length + (act.puzzle ? splitIntoChunks(act.puzzle, 512).length : 0)
764-
}),
765-
div_id: this.divid
766-
});
767-
chunkIndex++;
768-
});
769-
}
754+
const puzzle_json = {
755+
type: act.type,
756+
generation_type: act.generation_type,
757+
solution_type: act.solution_type,
758+
code_answer: act.code_answer || null,
759+
};
770760

771-
// Split and send the puzzle chunks if they exist
772-
if (act.puzzle) {
773-
const puzzleChunks = splitIntoChunks(act.puzzle, 512);
774-
let puzzleIndex = 1;
775-
puzzleChunks.forEach((chunk) => {
776-
this.logBookEvent({
777-
event: `gptparsons_puzzle_${puzzleIndex}`,
778-
act: JSON.stringify({
779-
reopen: !isNewHelp,
780-
type: 'puzzle',
781-
content: chunk,
782-
chunkIndex: puzzleIndex,
783-
totalChunks: puzzleChunks.length
784-
}),
785-
div_id: this.divid
786-
});
787-
puzzleIndex++;
788-
});
789-
}
761+
// Send ONE request to the new table
762+
this.logCodeTailorEvent({
763+
div_id: this.divid,
764+
sid: this.sid,
765+
ctid: this.puzzleScaffoldingDivid,
766+
course: this.courseName, // must match request.state.user.course_name
767+
htmlsrc: this.puzzleHTML || null, // raw puzzle HTML
768+
puzzle_json: puzzle_json, // JSON column
769+
});
790770
}
791771

792772
// Log the data in parts
793773
logActInParts(act);
794774

775+
795776
let probDescHTML = $(this.outerDiv).find(".ac_question").last().html();
796777
if (puzzle_rst !== "correctCode" && puzzle_rst !== "emptyHelpParsons") {
797778
var puzzleCode = `
@@ -851,9 +832,9 @@ export class ActiveCode extends RunestoneBase {
851832
type: 'close_help'
852833
}
853834
this.logBookEvent({
854-
event: "gptparsons_close",
835+
event: "codetailor_close",
855836
act: JSON.stringify({ ...act }), // Clone to avoid future changes affecting the log
856-
div_id: close_divId
837+
div_id: this.puzzleScaffoldingDivid
857838
});
858839
$(`#scaffolding-container-${this.divid}`).addClass('hidden');
859840
}
@@ -913,9 +894,9 @@ export class ActiveCode extends RunestoneBase {
913894
type: 'copy_help'
914895
}
915896
this.logBookEvent({
916-
event: "gptparsons_copy",
897+
event: "codetailor_copy",
917898
act: JSON.stringify({ ...act }), // Clone to avoid future changes affecting the log
918-
div_id: this.divid
899+
div_id: this.puzzleScaffoldingDivid
919900
});
920901
$(`#copy-answer-button-${this.puzzleScaffoldingDivid}`).text('Copied!').prop('disabled', true);
921902

@@ -1018,9 +999,9 @@ export class ActiveCode extends RunestoneBase {
1018999
}
10191000

10201001
this.logBookEvent({
1021-
event: "gptparsons_request_base",
1002+
event: "codetailor_request_base",
10221003
act: this.parsonspersonalize,
1023-
div_id: this.divid
1004+
div_id: this.puzzleScaffoldingDivid
10241005
});
10251006

10261007
// Split and send the code chunks
@@ -1029,14 +1010,14 @@ export class ActiveCode extends RunestoneBase {
10291010
const codeChunks = splitIntoChunks(request_act.code, 512);
10301011
codeChunks.forEach((chunk) => {
10311012
this.logBookEvent({
1032-
event: `gptparsons_request_code_${requestChunkIndex}`,
1013+
event: `codetailor_request_code_${requestChunkIndex}`,
10331014
act: JSON.stringify({
10341015
type: 'code',
10351016
content: chunk,
10361017
requestChunkIndex: requestChunkIndex,
10371018
totalChunks: requestChunkIndex.length + splitIntoChunks(request_act.code, 512).length
10381019
}),
1039-
div_id: this.divid
1020+
div_id: this.puzzleScaffoldingDivid
10401021
});
10411022
requestChunkIndex++;
10421023
});

bases/rsptx/interactives/runestone/common/js/runestonebase.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,73 @@ export default class RunestoneBase {
337337
}
338338
return post_promise;
339339
}
340+
341+
// logCodeTailorEvent
342+
// ------------------
343+
// This function sends the provided ``eventInfo`` to the `codetailor_log endpoint` of the server.
344+
// This function is modified based on logRunEvent and logBookEvent
345+
async logCodeTailorEvent(eventInfo) {
346+
if (this.graderactive) {
347+
return;
348+
}
349+
350+
let post_return;
351+
352+
// Match backend expectations
353+
eventInfo.course = eBookConfig.course;
354+
eventInfo.clientLoginStatus = eBookConfig.isLoggedIn;
355+
eventInfo.timezoneoffset = new Date().getTimezoneOffset() / 60;
356+
357+
if (
358+
eBookConfig.isLoggedIn &&
359+
eBookConfig.useRunestoneServices &&
360+
eBookConfig.logLevel > 0
361+
) {
362+
let request = new Request(
363+
`${eBookConfig.new_server_prefix}/logger/codetailor_log`,
364+
{
365+
method: "POST",
366+
headers: this.jsonHeaders,
367+
body: JSON.stringify(eventInfo),
368+
}
369+
);
370+
371+
let response = await fetch(request);
372+
373+
if (!response.ok) {
374+
post_return = await response.json();
375+
if (eBookConfig.useRunestoneServices) {
376+
alert(`Failed to log your CodeTailor event
377+
Status is ${response.status}
378+
Detail: ${JSON.stringify(post_return.detail, null, 4)}`);
379+
} else {
380+
console.log(
381+
`Did not log the CodeTailor event.
382+
Status: ${response.status}
383+
Detail: ${JSON.stringify(post_return.detail, null, 4)}`
384+
);
385+
}
386+
} else {
387+
post_return = await response.json();
388+
}
389+
}
390+
391+
if (!this.isTimed || eBookConfig.debug) {
392+
let prefix = eBookConfig.isLoggedIn ? "Save" : "Not";
393+
console.log(`${prefix} codetailor logging ` + JSON.stringify(eventInfo));
394+
}
395+
396+
if (
397+
typeof pageProgressTracker.updateProgress === "function" &&
398+
this.optional == false
399+
) {
400+
pageProgressTracker.updateProgress(eventInfo.ctid);
401+
}
402+
403+
return post_return;
404+
}
405+
406+
340407
/* Checking/loading from storage
341408
**WARNING:** DO NOT `await` this function!
342409
This function, although async, does not explicitly resolve its promise by returning a value. The reason for this is because it is called by the constructor for nearly every component. In Javascript constructors cannot be async!

bases/rsptx/interactives/runestone/parsons/js/parsons.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,7 +1753,7 @@ export default class Parsons extends RunestoneBase {
17531753
$(this.checkButton).prop("disabled", true);
17541754
// CodeTailor: make the Copy Answer button visible when the puzzle is solved
17551755
if (this.hasSolved && this.options.scaffolding) {
1756-
const copyBtn = document.querySelector(`#copy-answer-button-${this.divid}`); // copy-answer-button-${puzzleScaffoldingDivid}
1756+
const copyBtn = document.querySelector(`#copy-answer-button-${this.puzzleScaffoldingDivid}`); // copy-answer-button-${puzzleScaffoldingDivid}
17571757
if (copyBtn) {
17581758
copyBtn.classList.remove('copy-button-hide');
17591759
}
@@ -2998,7 +2998,7 @@ export default class Parsons extends RunestoneBase {
29982998
this.clearFeedback();
29992999
// CodeTailor: Hide the Copy Answer Button again
30003000
if (this.options.scaffolding === true) {
3001-
const copyBtn = document.querySelector(`#copy-answer-button-${this.divid}`); // copy-answer-button-${puzzleScaffoldingDivid}
3001+
const copyBtn = document.querySelector(`#copy-answer-button-${this.puzzleScaffoldingDivid}`); // copy-answer-button-${puzzleScaffoldingDivid}
30023002
if (copyBtn) {
30033003
copyBtn.classList.add('copy-button-hide');
30043004
}

components/rsptx/db/crud/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
count_useinfo_for,
168168
create_answer_table_entry,
169169
create_code_entry,
170+
create_code_tailor_parsons_entry,
170171
create_useinfo_entry,
171172
fetch_code,
172173
fetch_last_answer_table_entry,
@@ -395,6 +396,7 @@
395396
"count_useinfo_for",
396397
"create_answer_table_entry",
397398
"create_code_entry",
399+
"create_code_tailor_parsons_entry",
398400
"create_useinfo_entry",
399401
"fetch_code",
400402
"fetch_last_answer_table_entry",

components/rsptx/db/crud/rslogging.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
Code,
1111
CodeValidator,
1212
runestone_component_dict,
13+
CodeTailorParsons,
14+
CodeTailorParsonsValidator,
1315
)
1416
from ..async_session import async_session
1517
from rsptx.validation import schemas
@@ -250,3 +252,20 @@ async def fetch_code(
250252
# We retrieved most recent first, but want to return results in chronological order
251253
code_list.reverse()
252254
return code_list
255+
256+
# CodeTailorParsons
257+
# ----
258+
# write a function that takes a CodeTailorParsonsValidator as a parameter and inserts a new CodeTailorParsons into the database
259+
async def create_code_tailor_parsons_entry(data: CodeTailorParsonsValidator) -> CodeTailorParsonsValidator:
260+
"""
261+
Create a new CodeTailorParsons entry with the given data
262+
263+
:param data: CodeTailorParsonsValidator, validated input data
264+
:return: CodeTailorParsonsValidator, the newly created entry
265+
"""
266+
new_entry = CodeTailorParsons(**data.dict())
267+
268+
async with async_session.begin() as session:
269+
session.add(new_entry)
270+
271+
return CodeTailorParsonsValidator.from_orm(new_entry)

0 commit comments

Comments
 (0)