Source code for pykubegrader.widgets_base.reading

import copy
from typing import Optional

import panel as pn

from ..telemetry import ensure_responses, update_responses
from ..utils import shuffle_options


[docs] class ReadingPython: def __init__( self, title: str, question_number: int, options: dict, ) -> None: # Load responses from JSON (or create file if it doesn't exist) responses = ensure_responses() self.question_number = question_number default = None # Dynamically assign attributes based on keys, with default values from responses for num in range(len(options["lines_to_comment"]) + options["n_rows"]): key = f"q{question_number}_{num+1}" # Dynamically assign the value from the responses file for persistence if num < len(options["lines_to_comment"]): setattr(self, key, responses.get(key, default)) else: setattr( self, key, responses.get(key, [default] * (len(options["table_headers"]) - 1)), ) # Checks that a seed was assigned to responses try: seed: int = responses["seed"] except ValueError: raise ValueError( "You must submit your student info before starting the exam" ) # # Question title # question_title = pn.pane.HTML(f"<h2>Question {question_number}: {title}</h2>") # # Comment dropdowns # self.dropdowns_for_comments: dict[str, pn.widgets.Select] = { line: pn.widgets.Select( options=shuffle_options(options["comments_options"], seed), name=f"Line {line}:", value=getattr(self, f"q{question_number}_{i_comments+1}"), width=600, ) for i_comments, line in enumerate(options["lines_to_comment"]) } comment_dropdowns = pn.Column(*self.dropdowns_for_comments.values()) # # Execution dropdowns # # Instructions execution_instructions = pn.pane.HTML( "<h3>For each step, select the appropriate response:</h3>" ) # Header row header_row = pn.Row( *[ pn.pane.HTML(f"<strong>{header}</strong>", width=150) for header in options["table_headers"] ] ) # Make a deep copy of the lines to comment and add a null value to the beginning # This is to provide the null response to the question line_comment: list[int | str] = copy.deepcopy(options["lines_to_comment"]) line_comment.insert(0, "") dropdown_options = [ line_comment, options["variables_changed"], options["current_values"], options["datatypes"], ] # Function to create a row with dropdowns def create_row(step: int) -> pn.Row: widgets_list = [ pn.pane.HTML(f"Step {step+1}", width=150) if i == 0 else pn.widgets.Select( options=dropdown_options[i - 1], value=getattr( self, f'q{question_number}_{len(options["lines_to_comment"])+step+1}', )[i - 1], width=150, ) for i in range(len(options["table_headers"])) ] return pn.Row(*widgets_list) # Generate rows dynamically based on n_rows self.rows = [create_row(step) for step in range(options["n_rows"])] # Combine header and rows execution_steps = pn.Column(header_row, *self.rows) # Submit button self.submit_button = pn.widgets.Button(name="Submit") self.submit_button.on_click(self.submit) # Combine everything into a single layout self.layout = pn.Column( question_title, comment_dropdowns, execution_instructions, execution_steps, self.submit_button, )
[docs] def submit(self, _) -> None: # Get section 1 responses self.output_comments: list[str] = [] for out in self.dropdowns_for_comments.values(): self.output_comments.append(out.value if isinstance(out.value, str) else "") # Get section 2 responses self.output_execution: list[list[Optional[str | int]]] = [] for row in self.rows[:]: row_value: list[Optional[str | int]] = [] for box in row.objects: if isinstance(box, pn.widgets.Select): row_value.append(box.value) if not any(row_value): continue self.output_execution.append(row_value) # Persist responses to JSON i = 0 for comment_val in self.output_comments: i += 1 update_responses(f"q{self.question_number}_{i}", comment_val) for exec_val in self.output_execution: i += 1 update_responses(f"q{self.question_number}_{i}", exec_val) print("Responses recorded successfully")
[docs] def show(self): return self.layout