# pylint: disable=too-many-lines # # File: capa/capa_problem.py # # Nomenclature: # # A capa Problem is a collection of text and capa Response questions. # Each Response may have one or more Input entry fields. # The capa problem may include a solution. # """ Main module which shows problems (of "capa" type). This is used by capa_block. """ import logging import os.path import re from collections import OrderedDict from copy import deepcopy from datetime import datetime from typing import Optional from xml.sax.saxutils import unescape from zoneinfo import ZoneInfo from django.conf import settings from lxml import etree from openedx.core.djangolib.markup import HTML, Text from openedx.core.lib.safe_lxml.xmlparser import XML from xmodule.capa import customrender, inputtypes, responsetypes, xqueue_interface from xmodule.capa.correctmap import CorrectMap from xmodule.capa.safe_exec import safe_exec from xmodule.capa.util import contextualize_text, convert_files_to_filenames, get_course_id_from_capa_block from xmodule.stringify import stringify_children # extra things displayed after "show answers" is pressed solution_tags = ["solution"] # fully accessible capa input types ACCESSIBLE_CAPA_INPUT_TYPES = [ "checkboxgroup", "radiogroup", "choicegroup", "optioninput", "textline", "formulaequationinput", "textbox", ] # these get captured as student responses response_properties = ["codeparam", "responseparam", "answer", "openendedparam"] # special problem tags which should be turned into innocuous HTML html_transforms = { "problem": {"tag": "div"}, "text": {"tag": "span"}, "math": {"tag": "span"}, } # These should be removed from HTML output, including all subelements html_problem_semantics = [ "additional_answer", "codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam", "openendedrubric", ] log = logging.getLogger(__name__) # ----------------------------------------------------------------------------- # main class for this module class LoncapaSystem: # pylint: disable=too-few-public-methods,too-many-instance-attributes """ An encapsulation of resources needed from the outside. These interfaces are collected here so that a caller of LoncapaProblem can provide these resources however make sense for their environment, and this code can remain independent. Attributes: i18n: an object implementing the `gettext.Translations` interface so that we can use `.ugettext` to localize strings. See :class:`ModuleStoreRuntime` for documentation of other attributes. """ def __init__( # pylint: disable=too-many-positional-arguments,too-many-arguments self, ajax_url, anonymous_student_id, cache, can_execute_unsafe_code, get_python_lib_zip, DEBUG, # pylint: disable=invalid-name i18n, render_template, resources_fs, seed, # Why do we do this if we have self.seed? xqueue, matlab_api_key=None, ): self.ajax_url = ajax_url self.anonymous_student_id = anonymous_student_id self.cache = cache self.can_execute_unsafe_code = can_execute_unsafe_code self.get_python_lib_zip = get_python_lib_zip self.DEBUG = DEBUG # pylint: disable=invalid-name self.i18n = i18n self.render_template = render_template self.resources_fs = resources_fs self.seed = seed # Why do we do this if we have self.seed? self.STATIC_URL = settings.STATIC_URL # pylint: disable=invalid-name self.xqueue = xqueue self.matlab_api_key = matlab_api_key class LoncapaProblem: # pylint: disable=too-many-public-methods,too-many-instance-attributes """ Main class for capa Problems. """ def __init__( # pylint: disable=too-many-positional-arguments,too-many-arguments self, problem_text, id, # pylint: disable=redefined-builtin capa_system, capa_block, state=None, seed=None, minimal_init=False, extract_tree=True, ): """ Initializes capa Problem. Arguments: problem_text (string): xml defining the problem. id (string): identifier for this problem, often a filename (no spaces). capa_system (LoncapaSystem): LoncapaSystem instance which provides OS, rendering, user context, and other resources. capa_block: instance needed to access runtime/logging state (dict): containing the following keys: - `seed` (int) random number generator seed - `student_answers` (dict) maps input id to the stored answer for that input - 'has_saved_answers' (Boolean) True if the answer has been saved since last submit. - `correct_map` (CorrectMap) a map of each input to their 'correctness' - `done` (bool) indicates whether or not this problem is considered done - `input_state` (dict) maps input_id to a dictionary that holds the state for that input seed (int): random number generator seed. minimal_init (bool): whether to skip pre-processing student answers extract_tree (bool): whether to parse the problem XML and store the HTML """ # Initialize class variables from state self.do_reset() self.problem_id = id self.capa_system = capa_system self.capa_block = capa_block state = state or {} # Set seed according to the following priority: # 1. Contained in problem's state # 2. Passed into capa_problem via constructor self.seed = state.get("seed", seed) assert self.seed is not None, "Seed must be provided for LoncapaProblem." self.student_answers = state.get("student_answers", {}) self.has_saved_answers = state.get("has_saved_answers", False) if "correct_map" in state: self.correct_map.set_dict(state["correct_map"]) self.correct_map_history = [] for cmap in state.get("correct_map_history", []): correct_map = CorrectMap() correct_map.set_dict(cmap) self.correct_map_history.append(correct_map) self.done = state.get("done", False) self.input_state = state.get("input_state", {}) # Convert startouttext and endouttext to proper problem_text = re.sub(r"startouttext\s*/", "text", problem_text) problem_text = re.sub(r"endouttext\s*/", "/text", problem_text) self.problem_text = problem_text # parse problem XML file into an element tree if isinstance(problem_text, str): # etree chokes on Unicode XML with an encoding declaration problem_text = problem_text.encode("utf-8") self.tree = XML(problem_text) try: self.make_xml_compatible(self.tree) except Exception: capa_block = self.capa_block log.exception( "CAPAProblemError: %s, id:%s, data: %s", capa_block.display_name, self.problem_id, capa_block.data ) raise # handle any tags self._process_includes() # construct script processor context (eg for customresponse problems) if minimal_init: self.context = {} else: self.context = self._extract_context(self.tree) # Pre-parse the XML tree: modifies it to add ID's and perform some in-place # transformations. This also creates the dict (self.responders) of Response # instances for each question in the problem. The dict has keys = xml subtree of # Response, values = Response instance self.problem_data = self._preprocess_problem(self.tree, minimal_init) if not minimal_init: if not self.student_answers: # True when student_answers is an empty dict self.set_initial_display() # dictionary of InputType objects associated with this problem # input_id string -> InputType object self.inputs = {} # Run response late_transforms last (see MultipleChoiceResponse) # Sort the responses to be in *_1 *_2 ... order. responses = list(self.responders.values()) responses = sorted(responses, key=lambda resp: int(resp.id[resp.id.rindex("_") + 1 :])) for response in responses: if hasattr(response, "late_transforms"): response.late_transforms(self) if extract_tree: self.extracted_tree = self._extract_html(self.tree) @property def is_grading_method_enabled(self) -> bool: """ Returns whether the grading method feature is enabled. If the feature is not enabled, the grading method field will not be shown in Studio settings and the default grading method will be used. """ return settings.FEATURES.get("ENABLE_GRADING_METHOD_IN_PROBLEMS", False) def make_xml_compatible(self, tree): """ Adjust tree xml in-place for compatibility before creating a problem from it. The idea here is to provide a central point for XML translation, for example, supporting an old XML format. At present, there just two translations. 1. compatibility translation: old: ANSWER convert to new: OPTIONAL-HINT 2. compatibility translation: optioninput works like this internally: With extended hints there is a new This translation takes in the new format and synthesizes the old option= attribute so all downstream logic works unchanged with the new