Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions bases/rsptx/book_server_api/routers/coach.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ def extract_parsons_solution(parsonsexample_code):
correct solution), and scaffolding tags ("#settled", "#tag:...;...;") are
stripped from the remaining blocks. Mirrors the block-parsing logic in
runestone/parsons/js/parsons.js::initializeLines.

extract_parsons_code() deliberately leaves HTML entities (e.g. "<")
escaped, since its result is also re-embedded into a <pre> block for the
Parsons widget to re-render. This function's result instead goes straight
into a plain-text answer (clipboard copy, code_answer), so it must be
unescaped here or a "<" in the source shows up as literal "&lt;".
"""
blocks = parsonsexample_code.split("---")
clean_lines = []
Expand All @@ -160,7 +166,7 @@ def extract_parsons_solution(parsonsexample_code):
line = re.sub(r"#tag:[^;]*;[^;]*;\s*$", "", line)
line = line.rstrip()
if line.strip() and line.strip() != "=====":
clean_lines.append(line)
clean_lines.append(html.unescape(line))
return "\n".join(clean_lines)


Expand Down Expand Up @@ -466,7 +472,13 @@ def parsons_help(
"Problem Name": problem_id,
"Problem Description": problem_description,
"Unittest_Code": internal_test_case,
"Example": parsonsexample_code, # This is the html of the example Parsons problem
"Example": (
parsonsexample_code
if parsonsexample_code == "LLM-example"
else extract_parsons_solution(parsonsexample_code)
), # compilable solution code, not raw Parsons block markup --
# this can be returned verbatim as the fixed/example code when
# LLM personalization falls back (see end_to_end.generate_example_solution)
"CF (Code)": student_code,
}
return get_parsons_help(api_token, language, input_dict, personalization_level)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,15 +492,20 @@ def aggregate_code_to_Parsons_block_with_distractor(blocks):

def aggregate_code_to_full_Parsons_block(blocks):
"""
Aggregate the code into full Parsons blocks. All code lines with the same indentation level are grouped together.
Aggregate the code into full Parsons blocks. Lines nested inside something
(indented deeper than the snippet's root level) are grouped together as a
single block per contiguous run, since only their placement under the
enclosing header matters. Lines at the root level always get their own
block, since each one is an independent, reorderable statement.
1. Function definitions (def) and return statements are treated as separate blocks.
2. Import statements are grouped together into a single block.
3. Other lines with the same indentation level are grouped together.
4. If the indentation level changes, a new block is started.
3. Root-level lines are never merged with each other.
4. Nested lines at the same indentation level are grouped together, until the indentation level changes.
Comment thread
aspadiyath marked this conversation as resolved.
5. Blank lines are preserved within blocks.
6. Each block ends with a newline character.
"""
current_indent = check_indentation_level(blocks[0])
base_indent = check_indentation_level(blocks[0])
current_indent = base_indent
all_Parsons_blocks = []
Parsons_block = ""
import_block = ""
Expand All @@ -510,26 +515,31 @@ def aggregate_code_to_full_Parsons_block(blocks):
block += "\n"

this_indent = check_indentation_level(block)
stripped_block = block.strip()
is_import_line = bool(re.match(r"^(import|from)\b", stripped_block))

if block.strip().startswith("import"):
if is_import_line:
import_block += block # add to the import block
continue # continue processing without disrupting other logic

if import_block and not block.strip().startswith("import"):
if import_block and not is_import_line:
all_Parsons_blocks.append(
import_block
) # add the collected import block at the beginning
import_block = "" # reset the import block

if block.strip().startswith(("def", "return")):
if re.match(r"^(def|return)\b", stripped_block):
if Parsons_block: # append any current accumulated block
all_Parsons_blocks.append(Parsons_block)
Parsons_block = "" # reset Parsons block
all_Parsons_blocks.append(
block
) # add the def or return statement as its own block
current_indent = this_indent
elif this_indent == current_indent:
elif this_indent == current_indent and this_indent != base_indent:
# Only merge lines that are nested inside something (indented deeper
# than the snippet's root level) -- sibling statements at the root
# level each get their own block, same as def/return already do.
Parsons_block += block
else:
if Parsons_block: # append current block before resetting
Expand Down
8 changes: 7 additions & 1 deletion bases/rsptx/interactives/runestone/parsons/js/parsons.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,13 @@ export default class Parsons extends RunestoneBase {
this.checkCount = 0;
this.numDistinct = 0;
this.hasSolved = false;
this.initializeLines(fulltext.trim());
// Only trim trailing whitespace here. A leading trim() would strip real
// leading indentation off the first line of the first block whenever the
// <pre> tag's own leading newline has already been consumed by the browser's
// HTML parser (a well-known <pre> quirk), silently de-indenting only the
// first block relative to every other block. Blank leading/trailing lines
// within a block are still discarded further down, per block.
this.initializeLines(fulltext.trimEnd());
this.initializeView();
this.caption = "Parsons";
this.addCaption("runestone");
Expand Down
Loading