<commit_before>#
# File:   courseware/capa/inputtypes.py
#

'''
Module containing the problem elements which render into input objects

- textline
- textbox     (change this to textarea?)
- schemmatic
- choicegroup (for multiplechoice: checkbox, radio, or select option)
- imageinput  (for clickable image)
- optioninput (for option list)

These are matched by *.html files templates/*.html which are mako templates with the actual html.

Each input type takes the xml tree as 'element', the previous answer as 'value', and the graded status as 'status'

'''

# TODO: rename "state" to "status" for all below
# status is currently the answer for the problem ID for the input element,
# but it will turn into a dict containing both the answer and any associated message for the problem ID for the input element.

import logging
import re
import shlex # for splitting quoted strings

from lxml import etree
import xml.sax.saxutils as saxutils

log = logging.getLogger('mitx.' + __name__)

def get_input_xml_tags():
    ''' Eventually, this will be for all registered input types '''
    return SimpleInput.get_xml_tags()

class SimpleInput():# XModule
    '''
    Type for simple inputs -- plain HTML with a form element
    '''

    xml_tags = {} ## Maps tags to functions

    def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'):
        '''
        Instantiate a SimpleInput class.  Arguments:

        - system    : I4xSystem instance which provides OS, rendering, and user context 
        - xml       : Element tree of this Input element
        - item_id   : id for this input element (assigned by capa_problem.LoncapProblem) - string
        - track_url : URL used for tracking - string
        - state     : a dictionary with optional keys: 
                      * Value
                      * ID
                      * Status (answered, unanswered, unsubmitted)
                      * Feedback (dictionary containing keys for hints, errors, or other 
                        feedback from previous attempt)
        - use        :
        '''

        self.xml = xml
        self.tag = xml.tag
        self.system = system
        if not state: state = {}

        ## ID should only come from one place. 
        ## If it comes from multiple, we use state first, XML second, and parameter
        ## third. Since we don't make this guarantee, we can swap this around in 
        ## the future if there's a more logical order. 
        if item_id:       self.id = item_id
        if xml.get('id'): self.id = xml.get('id')
        if 'id' in state: self.id = state['id']

        self.value = ''
        if 'value' in state:
            self.value = state['value']

        self.msg = ''
        feedback = state.get('feedback')
        if feedback is not None:
            self.msg = feedback.get('message','')
            self.hint = feedback.get('hint','')
            self.hintmode = feedback.get('hintmode',None)
            
            # put hint above msg if to be displayed
            if self.hintmode == 'always':
                self.msg = self.hint + ('<br/.>' if self.msg else '') + self.msg
 
        self.status = 'unanswered'
        if 'status' in state:
            self.status = state['status']

    @classmethod
    def get_xml_tags(c):
        return c.xml_tags.keys()

    @classmethod
    def get_uses(c):
        return ['capa_input', 'capa_transform']

    def get_html(self):
        return self.xml_tags[self.tag](self.xml, self.value, self.status, self.system.render_template, self.msg)

def register_render_function(fn, names=None, cls=SimpleInput):
    if names is None:
        SimpleInput.xml_tags[fn.__name__] = fn
    else:
        raise NotImplementedError
    def wrapped():
        return fn
    return wrapped

#-----------------------------------------------------------------------------

@register_render_function
def optioninput(element, value, status, render_template, msg=''):
    '''
    Select option input type.

    Example:

    <optioninput options="('Up','Down')" correct="Up"/><text>The location of the sky</text>
    '''
    eid=element.get('id')
    options = element.get('options')
    if not options:
        raise Exception("[courseware.capa.inputtypes.optioninput] Missing options specification in " + etree.tostring(element))
    oset = shlex.shlex(options[1:-1])
    oset.quotes = "'"
    oset.whitespace = ","
    oset = [x[1:-1] for x  in list(oset)]

    # osetdict = dict([('option_%s_%s' % (eid,x),oset[x]) for x in range(len(oset)) ])	# make dict with IDs
    osetdict = dict([(oset[x],oset[x]) for x in range(len(oset)) ])	# make dict with key,value same
    
    context={'id':eid,
             'value':value,
             'state':status,
             'msg':msg,
             'options':osetdict,
             }

    html = render_template("optioninput.html", context)
    return etree.XML(html)

#-----------------------------------------------------------------------------
@register_render_function
def choicegroup(element, value, status, render_template, msg=''):
    '''
    Radio button inputs: multiple choice or true/false

    TODO: allow order of choices to be randomized, following lon-capa spec.  Use "location" attribute,
    ie random, top, bottom.
    '''
    eid=element.get('id')
    if element.get('type') == "MultipleChoice":
        type="radio"
    elif element.get('type') == "TrueFalse":
        type="checkbox"
    else:
        type="radio"
    choices={}
    for choice in element:
        if not choice.tag=='choice':
            raise Exception("[courseware.capa.inputtypes.choicegroup] Error only <choice> tags should be immediate children of a <choicegroup>, found %s instead" % choice.tag)
        ctext = ""
        ctext += ''.join([etree.tostring(x) for x in choice])	# TODO: what if choice[0] has math tags in it?
        ctext += choice.text		# TODO: fix order?
        choices[choice.get("name")] = ctext
    context={'id':eid, 'value':value, 'state':status, 'type':type, 'choices':choices}
    html = render_template("choicegroup.html", context)
    return etree.XML(html)

@register_render_function
def textline(element, value, status, render_template, msg=""):
    '''
    Simple text line input, with optional size specification.
    '''
    if element.get('math') or element.get('dojs'):		# 'dojs' flag is temporary, for backwards compatibility with 8.02x
        return SimpleInput.xml_tags['textline_dynamath'](element,value,status,render_template,msg)
    eid=element.get('id')
    if eid is None:
        msg = 'textline has no id: it probably appears outside of a known response type'
        msg += "\nSee problem XML source line %s" % getattr(element,'sourceline','<unavailable>')
        raise Exception(msg)
    count = int(eid.split('_')[-2])-1 # HACK
    size = element.get('size')
    hidden = element.get('hidden','')	# if specified, then textline is hidden and id is stored in div of name given by hidden
    context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg': msg, 'hidden': hidden}
    html = render_template("textinput.html", context)
    return etree.XML(html)<commit_msg>fixed two bugs in inputtypes: textline with quotes crashed problems, and choice without text caused error<commit_after>#
# File:   courseware/capa/inputtypes.py
#

'''
Module containing the problem elements which render into input objects

- textline
- textbox     (change this to textarea?)
- schemmatic
- choicegroup (for multiplechoice: checkbox, radio, or select option)
- imageinput  (for clickable image)
- optioninput (for option list)

These are matched by *.html files templates/*.html which are mako templates with the actual html.

Each input type takes the xml tree as 'element', the previous answer as 'value', and the graded status as 'status'

'''

# TODO: rename "state" to "status" for all below
# status is currently the answer for the problem ID for the input element,
# but it will turn into a dict containing both the answer and any associated message for the problem ID for the input element.

import logging
import re
import shlex # for splitting quoted strings

from lxml import etree
import xml.sax.saxutils as saxutils

log = logging.getLogger('mitx.' + __name__)

def get_input_xml_tags():
    ''' Eventually, this will be for all registered input types '''
    return SimpleInput.get_xml_tags()

class SimpleInput():# XModule
    '''
    Type for simple inputs -- plain HTML with a form element
    '''

    xml_tags = {} ## Maps tags to functions

    def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'):
        '''
        Instantiate a SimpleInput class.  Arguments:

        - system    : I4xSystem instance which provides OS, rendering, and user context 
        - xml       : Element tree of this Input element
        - item_id   : id for this input element (assigned by capa_problem.LoncapProblem) - string
        - track_url : URL used for tracking - string
        - state     : a dictionary with optional keys: 
                      * Value
                      * ID
                      * Status (answered, unanswered, unsubmitted)
                      * Feedback (dictionary containing keys for hints, errors, or other 
                        feedback from previous attempt)
        - use        :
        '''

        self.xml = xml
        self.tag = xml.tag
        self.system = system
        if not state: state = {}

        ## ID should only come from one place. 
        ## If it comes from multiple, we use state first, XML second, and parameter
        ## third. Since we don't make this guarantee, we can swap this around in 
        ## the future if there's a more logical order. 
        if item_id:       self.id = item_id
        if xml.get('id'): self.id = xml.get('id')
        if 'id' in state: self.id = state['id']

        self.value = ''
        if 'value' in state:
            self.value = state['value']

        self.msg = ''
        feedback = state.get('feedback')
        if feedback is not None:
            self.msg = feedback.get('message','')
            self.hint = feedback.get('hint','')
            self.hintmode = feedback.get('hintmode',None)
            
            # put hint above msg if to be displayed
            if self.hintmode == 'always':
                self.msg = self.hint + ('<br/.>' if self.msg else '') + self.msg
 
        self.status = 'unanswered'
        if 'status' in state:
            self.status = state['status']

    @classmethod
    def get_xml_tags(c):
        return c.xml_tags.keys()

    @classmethod
    def get_uses(c):
        return ['capa_input', 'capa_transform']

    def get_html(self):
        return self.xml_tags[self.tag](self.xml, self.value, self.status, self.system.render_template, self.msg)

def register_render_function(fn, names=None, cls=SimpleInput):
    if names is None:
        SimpleInput.xml_tags[fn.__name__] = fn
    else:
        raise NotImplementedError
    def wrapped():
        return fn
    return wrapped

#-----------------------------------------------------------------------------

@register_render_function
def optioninput(element, value, status, render_template, msg=''):
    '''
    Select option input type.

    Example:

    <optioninput options="('Up','Down')" correct="Up"/><text>The location of the sky</text>
    '''
    eid=element.get('id')
    options = element.get('options')
    if not options:
        raise Exception("[courseware.capa.inputtypes.optioninput] Missing options specification in " + etree.tostring(element))
    oset = shlex.shlex(options[1:-1])
    oset.quotes = "'"
    oset.whitespace = ","
    oset = [x[1:-1] for x  in list(oset)]

    # osetdict = dict([('option_%s_%s' % (eid,x),oset[x]) for x in range(len(oset)) ])	# make dict with IDs
    osetdict = dict([(oset[x],oset[x]) for x in range(len(oset)) ])	# make dict with key,value same
    
    context={'id':eid,
             'value':value,
             'state':status,
             'msg':msg,
             'options':osetdict,
             }

    html = render_template("optioninput.html", context)
    return etree.XML(html)

#-----------------------------------------------------------------------------
@register_render_function
def choicegroup(element, value, status, render_template, msg=''):
    '''
    Radio button inputs: multiple choice or true/false

    TODO: allow order of choices to be randomized, following lon-capa spec.  Use "location" attribute,
    ie random, top, bottom.
    '''
    eid=element.get('id')
    if element.get('type') == "MultipleChoice":
        type="radio"
    elif element.get('type') == "TrueFalse":
        type="checkbox"
    else:
        type="radio"
    choices={}
    for choice in element:
        if not choice.tag=='choice':
            raise Exception("[courseware.capa.inputtypes.choicegroup] Error only <choice> tags should be immediate children of a <choicegroup>, found %s instead" % choice.tag)
        ctext = ""
        ctext += ''.join([etree.tostring(x) for x in choice])	# TODO: what if choice[0] has math tags in it?
        if choice.text is not None:
            ctext += choice.text		# TODO: fix order?
        choices[choice.get("name")] = ctext
    context={'id':eid, 'value':value, 'state':status, 'type':type, 'choices':choices}
    html = render_template("choicegroup.html", context)
    return etree.XML(html)

@register_render_function
def textline(element, value, status, render_template, msg=""):
    '''
    Simple text line input, with optional size specification.
    '''
    if element.get('math') or element.get('dojs'):		# 'dojs' flag is temporary, for backwards compatibility with 8.02x
        return SimpleInput.xml_tags['textline_dynamath'](element,value,status,render_template,msg)
    eid=element.get('id')
    if eid is None:
        msg = 'textline has no id: it probably appears outside of a known response type'
        msg += "\nSee problem XML source line %s" % getattr(element,'sourceline','<unavailable>')
        raise Exception(msg)
    count = int(eid.split('_')[-2])-1 # HACK
    size = element.get('size')
    hidden = element.get('hidden','')	# if specified, then textline is hidden and id is stored in div of name given by hidden
    escapedict = {'"': '&quot;'}
    value = saxutils.escape(value,escapedict)	# otherwise, answers with quotes in them crashes the system!
    context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg': msg, 'hidden': hidden}
    html = render_template("textinput.html", context)
    try:
        xhtml = etree.XML(html)
    except Exception as err:
        if True: # TODO needs to be self.system.DEBUG - but can't access system 
            log.debug('[inputtypes.textline] failed to parse XML for:\n%s' % html)
            raise
    return xhtml
{'fraction_non_alphanumeric': 0.13443459575611066, 'fraction_numerical': 0.00147730325006715, 'mean_word_length': 2.500117508813161, 'pattern_counts': {'":': 4, '<': 17, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 38}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1336085', 'n_tokens_mistral': 4300, 'n_tokens_neox': 4195, 'n_words': 1388}