Commit 6defd7ba by Sarina Canelake

Merge pull request #790 from edx/unanswered-on-input

Unanswered on input
parents b7e8af65 fe47dcb1
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
% for choice_id, choice_description in choices: % for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}" <label for="input_${id}_${choice_id}"
## If the student has selected this choice...
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ): % if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
<% <%
if status == 'correct': if status == 'correct':
...@@ -32,6 +33,7 @@ ...@@ -32,6 +33,7 @@
% endif % endif
> >
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" aria-describedby="answer_${id}" value="${choice_id}" <input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" aria-describedby="answer_${id}" value="${choice_id}"
## If the student selected this choice...
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ): % if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
checked="true" checked="true"
% elif input_type != 'radio' and choice_id in value: % elif input_type != 'radio' and choice_id in value:
......
<section id="formulaequationinput_${id}" class="formulaequationinput"> <section id="formulaequationinput_${id}" class="inputtype formulaequationinput">
<div class="${reported_status}" id="status_${id}"> <div class="${reported_status}" id="status_${id}">
<input type="text" name="input_${id}" id="input_${id}" <input type="text" name="input_${id}" id="input_${id}"
data-input-id="${id}" value="${value|h}" data-input-id="${id}" value="${value|h}"
......
<form class="option-input"> <form class="inputtype option-input">
<select name="input_${id}" id="input_${id}" aria-describedby="answer_${id}"> <select name="input_${id}" id="input_${id}" aria-describedby="answer_${id}">
<option value="option_${id}_dummy_default"> </option> <option value="option_${id}_dummy_default"> </option>
% for option_id, option_description in options: % for option_id, option_description in options:
......
<% doinline = "inline" if inline else "" %> <% doinline = "inline" if inline else "" %>
<section id="inputtype_${id}" class="${'text-input-dynamath' if do_math else ''} capa_inputtype ${doinline}" > <section id="inputtype_${id}" class="${'text-input-dynamath' if do_math else ''} capa_inputtype ${doinline} textline" >
% if preprocessor is not None: % if preprocessor is not None:
<div class="text-input-dynamath_data" data-preprocessor="${preprocessor['class_name']}"/> <div class="text-input-dynamath_data" data-preprocessor="${preprocessor['class_name']}"/>
......
...@@ -352,10 +352,10 @@ class TextlineTemplateTest(TemplateTestCase): ...@@ -352,10 +352,10 @@ class TextlineTemplateTest(TemplateTestCase):
super(TextlineTemplateTest, self).setUp() super(TextlineTemplateTest, self).setUp()
def test_section_class(self): def test_section_class(self):
cases = [({}, ' capa_inputtype '), cases = [({}, ' capa_inputtype textline'),
({'do_math': True}, 'text-input-dynamath capa_inputtype '), ({'do_math': True}, 'text-input-dynamath capa_inputtype textline'),
({'inline': True}, ' capa_inputtype inline'), ({'inline': True}, ' capa_inputtype inline textline'),
({'do_math': True, 'inline': True}, 'text-input-dynamath capa_inputtype inline'), ] ({'do_math': True, 'inline': True}, 'text-input-dynamath capa_inputtype inline textline'), ]
for (context, css_class) in cases: for (context, css_class) in cases:
base_context = self.context.copy() base_context = self.context.copy()
......
...@@ -25,6 +25,8 @@ class @Problem ...@@ -25,6 +25,8 @@ class @Problem
@$('section.action button.show').click @show @$('section.action button.show').click @show
@$('section.action input.save').click @save @$('section.action input.save').click @save
@bindResetCorrectness()
# Collapsibles # Collapsibles
Collapsible.setCollapsibles(@el) Collapsible.setCollapsibles(@el)
...@@ -370,6 +372,56 @@ class @Problem ...@@ -370,6 +372,56 @@ class @Problem
element.CodeMirror.save() if element.CodeMirror.save element.CodeMirror.save() if element.CodeMirror.save
@answers = @inputs.serialize() @answers = @inputs.serialize()
bindResetCorrectness: ->
# Loop through all input types
# Bind the reset functions at that scope.
$inputtypes = @el.find(".capa_inputtype").add(@el.find(".inputtype"))
$inputtypes.each (index, inputtype) =>
classes = $(inputtype).attr('class').split(' ')
for cls in classes
bindMethod = @bindResetCorrectnessByInputtype[cls]
if bindMethod?
bindMethod(inputtype)
# Find all places where each input type displays its correct-ness
# Replace them with their original state--'unanswered'.
bindResetCorrectnessByInputtype:
# These are run at the scope of the capa inputtype
# They should set handlers on each <input> to reset the whole.
formulaequationinput: (element) ->
$(element).find('input').on 'input', ->
$p = $(element).find('p.status')
$p.text gettext("unanswered")
$p.parent().removeClass().addClass "unanswered"
choicegroup: (element) ->
$element = $(element)
id = ($element.attr('id').match /^inputtype_(.*)$/)[1]
$element.find('input').on 'change', ->
$status = $("#status_#{id}")
if $status[0] # We found a status icon.
$status.removeClass().addClass "unanswered"
$status.empty().css 'display', 'inline-block'
else
# Recreate the unanswered dot on left.
$("<span>", {"class": "unanswered", "style": "display: inline-block;", "id": "status_#{id}"})
$element.find("label").removeClass()
'option-input': (element) ->
$select = $(element).find('select')
id = ($select.attr('id').match /^input_(.*)$/)[1]
$select.on 'change', ->
$status = $("#status_#{id}")
.removeClass().addClass("unanswered")
.find('span').text(gettext('Status: unsubmitted'))
textline: (element) ->
$(element).find('input').on 'input', ->
$p = $(element).find('p.status')
$p.text "unanswered"
$p.parent().removeClass().addClass "unanswered"
inputtypeSetupMethods: inputtypeSetupMethods:
'text-input-dynamath': (element) => 'text-input-dynamath': (element) =>
......
...@@ -15,11 +15,14 @@ describe("Formula Equation Preview", function () { ...@@ -15,11 +15,14 @@ describe("Formula Equation Preview", function () {
var $fixture = this.$fixture = $('\ var $fixture = this.$fixture = $('\
<section class="problems-wrapper" data-url="THE_URL">\ <section class="problems-wrapper" data-url="THE_URL">\
<section class="formulaequationinput">\ <section class="formulaequationinput">\
<input type="text" id="input_THE_ID" data-input-id="THE_ID"\ <div class="INITIAL_STATUS" id="status_THE_ID">\
value="prefilled_value"/>\ <input type="text" id="input_THE_ID" data-input-id="THE_ID"\
<div id="input_THE_ID_preview" class="equation">\ value="PREFILLED_VALUE"/>\
\[\]\ <p class="status">INITIAL_STATUS</p>\
<img class="loading" style="visibility:hidden"/>\ <div id="input_THE_ID_preview" class="equation">\
\[\]\
<img class="loading" style="visibility:hidden"/>\
</div>\
</div>\ </div>\
</section>\ </section>\
</section>'); </section>');
...@@ -62,10 +65,10 @@ describe("Formula Equation Preview", function () { ...@@ -62,10 +65,10 @@ describe("Formula Equation Preview", function () {
MathJax.Hub.Queue = jasmine.createSpy('MathJax.Hub.Queue'); MathJax.Hub.Queue = jasmine.createSpy('MathJax.Hub.Queue');
}); });
it('(the test) should be able to swap out the behavior of $', function () { it('(the test) is able to swap out the behavior of $', function () {
// This was a pain to write, make sure it doesn't get screwed up. // This was a pain to write, make sure it doesn't get screwed up.
// Find the DOM element using DOM methods. // Find the element using DOM methods.
var legitInput = this.$fixture[0].getElementsByTagName("input")[0]; var legitInput = this.$fixture[0].getElementsByTagName("input")[0];
// Use the (modified) jQuery. // Use the (modified) jQuery.
...@@ -96,7 +99,7 @@ describe("Formula Equation Preview", function () { ...@@ -96,7 +99,7 @@ describe("Formula Equation Preview", function () {
"THE_URL", "THE_URL",
"THE_ID", "THE_ID",
"preview_formcalc", "preview_formcalc",
{formula: "prefilled_value", {formula: "PREFILLED_VALUE",
request_start: jasmine.any(Number)}, request_start: jasmine.any(Number)},
jasmine.any(Function) jasmine.any(Function)
]); ]);
...@@ -117,7 +120,7 @@ describe("Formula Equation Preview", function () { ...@@ -117,7 +120,7 @@ describe("Formula Equation Preview", function () {
}); });
}); });
it("shouldn't be requested for empty input", function () { it("isn't requested for empty input", function () {
Problem.inputAjax.reset(); Problem.inputAjax.reset();
// When we make an input of '', // When we make an input of '',
...@@ -136,7 +139,7 @@ describe("Formula Equation Preview", function () { ...@@ -136,7 +139,7 @@ describe("Formula Equation Preview", function () {
}); });
}); });
it('should limit the number of requests per second', function () { it('limits the number of requests per second', function () {
var minDelay = formulaEquationPreview.minDelay; var minDelay = formulaEquationPreview.minDelay;
var end = Date.now() + minDelay * 1.1; var end = Date.now() + minDelay * 1.1;
var step = 10; // ms var step = 10; // ms
...@@ -171,7 +174,7 @@ describe("Formula Equation Preview", function () { ...@@ -171,7 +174,7 @@ describe("Formula Equation Preview", function () {
}); });
describe("Visible results (icon and mathjax)", function () { describe("Visible results (icon and mathjax)", function () {
it('should display a loading icon when requests are open', function () { it('displays a loading icon when requests are open', function () {
var $img = $("img.loading"); var $img = $("img.loading");
expect($img.css('visibility')).toEqual('hidden'); expect($img.css('visibility')).toEqual('hidden');
formulaEquationPreview.enable(); formulaEquationPreview.enable();
...@@ -199,7 +202,7 @@ describe("Formula Equation Preview", function () { ...@@ -199,7 +202,7 @@ describe("Formula Equation Preview", function () {
}); });
}); });
it('should update MathJax and loading icon on callback', function () { it('updates MathJax and loading icon on callback', function () {
formulaEquationPreview.enable(); formulaEquationPreview.enable();
waitsFor(function () { waitsFor(function () {
return Problem.inputAjax.wasCalled; return Problem.inputAjax.wasCalled;
...@@ -217,7 +220,7 @@ describe("Formula Equation Preview", function () { ...@@ -217,7 +220,7 @@ describe("Formula Equation Preview", function () {
expect($("img.loading").css('visibility')).toEqual('hidden'); expect($("img.loading").css('visibility')).toEqual('hidden');
// We should look in the preview div for the MathJax. // We should look in the preview div for the MathJax.
var previewDiv = $("div")[0]; var previewDiv = $("#input_THE_ID_preview")[0];
expect(MathJax.Hub.getAllJax).toHaveBeenCalledWith(previewDiv); expect(MathJax.Hub.getAllJax).toHaveBeenCalledWith(previewDiv);
// Refresh the MathJax. // Refresh the MathJax.
...@@ -242,7 +245,7 @@ describe("Formula Equation Preview", function () { ...@@ -242,7 +245,7 @@ describe("Formula Equation Preview", function () {
// Cannot find MathJax. // Cannot find MathJax.
MathJax.Hub.getAllJax.andReturn([]); MathJax.Hub.getAllJax.andReturn([]);
spyOn(console, 'error'); spyOn(console, 'warn');
callback({ callback({
preview: 'THE_FORMULA', preview: 'THE_FORMULA',
...@@ -250,10 +253,10 @@ describe("Formula Equation Preview", function () { ...@@ -250,10 +253,10 @@ describe("Formula Equation Preview", function () {
}); });
// Tests. // Tests.
expect(console.error).toHaveBeenCalled(); expect(console.warn).toHaveBeenCalled();
// We should look in the preview div for the MathJax. // We should look in the preview div for the MathJax.
var previewElement = $("div")[0]; var previewElement = $("#input_THE_ID_preview")[0];
expect(previewElement.firstChild.data).toEqual("\\[THE_FORMULA\\]"); expect(previewElement.firstChild.data).toEqual("\\[THE_FORMULA\\]");
// Refresh the MathJax. // Refresh the MathJax.
...@@ -263,7 +266,7 @@ describe("Formula Equation Preview", function () { ...@@ -263,7 +266,7 @@ describe("Formula Equation Preview", function () {
}); });
}); });
it('should display errors from the server well', function () { it('displays errors from the server well', function () {
var $img = $("img.loading"); var $img = $("img.loading");
formulaEquationPreview.enable(); formulaEquationPreview.enable();
waitsFor(function () { waitsFor(function () {
...@@ -329,7 +332,7 @@ describe("Formula Equation Preview", function () { ...@@ -329,7 +332,7 @@ describe("Formula Equation Preview", function () {
}); });
}); });
it('should update requests sequentially', function () { it('updates requests sequentially', function () {
var $img = $("img.loading"); var $img = $("img.loading");
expect($img.css('visibility')).toEqual('visible'); expect($img.css('visibility')).toEqual('visible');
...@@ -349,7 +352,7 @@ describe("Formula Equation Preview", function () { ...@@ -349,7 +352,7 @@ describe("Formula Equation Preview", function () {
expect($img.css('visibility')).toEqual('hidden') expect($img.css('visibility')).toEqual('hidden')
}); });
it("shouldn't display outdated information", function () { it("doesn't display outdated information", function () {
var $img = $("img.loading"); var $img = $("img.loading");
expect($img.css('visibility')).toEqual('visible'); expect($img.css('visibility')).toEqual('visible');
...@@ -368,7 +371,7 @@ describe("Formula Equation Preview", function () { ...@@ -368,7 +371,7 @@ describe("Formula Equation Preview", function () {
expect($img.css('visibility')).toEqual('hidden') expect($img.css('visibility')).toEqual('hidden')
}); });
it("shouldn't show an error if the responses are close together", it("doesn't show an error if the responses are close together",
function () { function () {
this.callbacks[0]({ this.callbacks[0]({
error: 'OOPSIE', error: 'OOPSIE',
......
...@@ -52,12 +52,14 @@ formulaEquationPreview.enable = function () { ...@@ -52,12 +52,14 @@ formulaEquationPreview.enable = function () {
// Show the loading icon. // Show the loading icon.
inputData.$img.css('visibility', 'visible'); inputData.$img.css('visibility', 'visible');
// Say we are waiting for request.
inputData.isWaitingForRequest = true; inputData.isWaitingForRequest = true;
// First thing in `sendRequest`, say we aren't anymore.
throttledRequest(inputData, this.value); throttledRequest(inputData, this.value);
}; };
$this.on("input", initializeRequest); $this.on("input", initializeRequest);
// send an initial // Ask for initial preview.
initializeRequest.call(this); initializeRequest.call(this);
} }
...@@ -85,7 +87,7 @@ formulaEquationPreview.enable = function () { ...@@ -85,7 +87,7 @@ formulaEquationPreview.enable = function () {
// // This is run when ajax call fails. // // This is run when ajax call fails.
// // Have an error message and other stuff here? // // Have an error message and other stuff here?
// inputData.$img.css('visibility', 'hidden'); // inputData.$img.css('visibility', 'hidden');
// }); */ // });
} }
else { else {
inputData.requestCallback({ inputData.requestCallback({
...@@ -140,7 +142,7 @@ formulaEquationPreview.enable = function () { ...@@ -140,7 +142,7 @@ formulaEquationPreview.enable = function () {
); );
} }
else if (latex) { else if (latex) {
console.error("Oops no mathjax for ", latex); console.warn("[FormulaEquationInput] Oops no mathjax for ", latex);
// Fall back to modifying the actual element. // Fall back to modifying the actual element.
var textNode = previewElement.childNodes[0]; var textNode = previewElement.childNodes[0];
textNode.data = "\\[" + latex + "\\]"; textNode.data = "\\[" + latex + "\\]";
......
...@@ -7,7 +7,7 @@ Feature: Answer problems ...@@ -7,7 +7,7 @@ Feature: Answer problems
Given External graders respond "correct" Given External graders respond "correct"
And I am viewing a "<ProblemType>" problem And I am viewing a "<ProblemType>" problem
When I answer a "<ProblemType>" problem "correctly" When I answer a "<ProblemType>" problem "correctly"
Then My "<ProblemType>" answer is marked "correct" Then my "<ProblemType>" answer is marked "correct"
And The "<ProblemType>" problem displays a "correct" answer And The "<ProblemType>" problem displays a "correct" answer
Examples: Examples:
...@@ -28,7 +28,7 @@ Feature: Answer problems ...@@ -28,7 +28,7 @@ Feature: Answer problems
Given External graders respond "incorrect" Given External graders respond "incorrect"
And I am viewing a "<ProblemType>" problem And I am viewing a "<ProblemType>" problem
When I answer a "<ProblemType>" problem "incorrectly" When I answer a "<ProblemType>" problem "incorrectly"
Then My "<ProblemType>" answer is marked "incorrect" Then my "<ProblemType>" answer is marked "incorrect"
And The "<ProblemType>" problem displays a "incorrect" answer And The "<ProblemType>" problem displays a "incorrect" answer
Examples: Examples:
...@@ -48,7 +48,7 @@ Feature: Answer problems ...@@ -48,7 +48,7 @@ Feature: Answer problems
Scenario: I can submit a blank answer Scenario: I can submit a blank answer
Given I am viewing a "<ProblemType>" problem Given I am viewing a "<ProblemType>" problem
When I check a problem When I check a problem
Then My "<ProblemType>" answer is marked "incorrect" Then my "<ProblemType>" answer is marked "incorrect"
And The "<ProblemType>" problem displays a "blank" answer And The "<ProblemType>" problem displays a "blank" answer
Examples: Examples:
...@@ -69,7 +69,7 @@ Feature: Answer problems ...@@ -69,7 +69,7 @@ Feature: Answer problems
Given I am viewing a "<ProblemType>" problem Given I am viewing a "<ProblemType>" problem
And I answer a "<ProblemType>" problem "<Correctness>ly" And I answer a "<ProblemType>" problem "<Correctness>ly"
When I reset the problem When I reset the problem
Then My "<ProblemType>" answer is marked "unanswered" Then my "<ProblemType>" answer is marked "unanswered"
And The "<ProblemType>" problem displays a "blank" answer And The "<ProblemType>" problem displays a "blank" answer
Examples: Examples:
...@@ -171,3 +171,68 @@ Feature: Answer problems ...@@ -171,3 +171,68 @@ Feature: Answer problems
| numerical | 1 point possible | | numerical | 1 point possible |
| formula | 1 point possible | | formula | 1 point possible |
| script | 2 points possible | | script | 2 points possible |
Scenario: I can reset the correctness of a problem after changing my answer
Given I am viewing a "<ProblemType>" problem
Then my "<ProblemType>" answer is marked "unanswered"
When I answer a "<ProblemType>" problem "<InitialCorrectness>ly"
And I wait for "1" seconds
And I input an answer on a "<ProblemType>" problem "<OtherCorrectness>ly"
Then my "<ProblemType>" answer is marked "unanswered"
And I reset the problem
Examples:
| ProblemType | InitialCorrectness | OtherCorrectness |
| drop down | correct | incorrect |
| drop down | incorrect | correct |
| checkbox | correct | incorrect |
| checkbox | incorrect | correct |
| string | correct | incorrect |
| string | incorrect | correct |
| numerical | correct | incorrect |
| numerical | incorrect | correct |
| formula | correct | incorrect |
| formula | incorrect | correct |
| script | correct | incorrect |
| script | incorrect | correct |
# Radio groups behave slightly differently than other types of checkboxes, because they
# don't put their status to the top left of the boxes (like checkboxes do), thus, they'll
# not ever have a status of "unanswered" once you've made an answer. They should simply NOT
# be marked either correct or incorrect. Arguably this behavior should be changed; when it
# is, these cases should move into the above Scenario.
Scenario: I can reset the correctness of a radiogroup problem after changing my answer
Given I am viewing a "<ProblemType>" problem
When I answer a "<ProblemType>" problem "<InitialCorrectness>ly"
And I wait for "1" seconds
Then my "<ProblemType>" answer is marked "<InitialCorrectness>"
And I input an answer on a "<ProblemType>" problem "<OtherCorrectness>ly"
Then my "<ProblemType>" answer is NOT marked "<InitialCorrectness>"
And my "<ProblemType>" answer is NOT marked "<OtherCorrectness>"
And I reset the problem
Examples:
| ProblemType | InitialCorrectness | OtherCorrectness |
| multiple choice | correct | incorrect |
| multiple choice | incorrect | correct |
| radio | correct | incorrect |
| radio | incorrect | correct |
Scenario: I can reset the correctness of a problem after submitting a blank answer
Given I am viewing a "<ProblemType>" problem
When I check a problem
And I input an answer on a "<ProblemType>" problem "correctly"
Then my "<ProblemType>" answer is marked "unanswered"
Examples:
| ProblemType |
| drop down |
| multiple choice |
| checkbox |
| radio |
| string |
| numerical |
| formula |
| script |
...@@ -82,14 +82,22 @@ def answer_problem_step(step, problem_type, correctness): ...@@ -82,14 +82,22 @@ def answer_problem_step(step, problem_type, correctness):
*problem_type* is a string representing the type of problem (e.g. 'drop down') *problem_type* is a string representing the type of problem (e.g. 'drop down')
*correctness* is in ['correct', 'incorrect'] *correctness* is in ['correct', 'incorrect']
""" """
# Change the answer on the page
input_problem_answer(step, problem_type, correctness)
# Submit the problem
check_problem(step)
@step(u'I input an answer on a "([^"]*)" problem "([^"]*)ly"')
def input_problem_answer(_, problem_type, correctness):
"""
Have the browser input an answer (either correct or incorrect)
"""
assert(correctness in ['correct', 'incorrect']) assert(correctness in ['correct', 'incorrect'])
assert(problem_type in PROBLEM_DICT) assert(problem_type in PROBLEM_DICT)
answer_problem(problem_type, correctness) answer_problem(problem_type, correctness)
# Submit the problem
check_problem(step)
@step(u'I check a problem') @step(u'I check a problem')
def check_problem(step): def check_problem(step):
...@@ -146,8 +154,8 @@ def see_score(_step, score): ...@@ -146,8 +154,8 @@ def see_score(_step, score):
assert world.browser.is_text_present(score) assert world.browser.is_text_present(score)
@step(u'My "([^"]*)" answer is marked "([^"]*)"') @step(u'[Mm]y "([^"]*)" answer is( NOT)? marked "([^"]*)"')
def assert_answer_mark(step, problem_type, correctness): def assert_answer_mark(_step, problem_type, isnt_marked, correctness):
""" """
Assert that the expected answer mark is visible Assert that the expected answer mark is visible
for a given problem type. for a given problem type.
...@@ -162,7 +170,10 @@ def assert_answer_mark(step, problem_type, correctness): ...@@ -162,7 +170,10 @@ def assert_answer_mark(step, problem_type, correctness):
# At least one of the correct selectors should be present # At least one of the correct selectors should be present
for sel in PROBLEM_DICT[problem_type][correctness]: for sel in PROBLEM_DICT[problem_type][correctness]:
has_expected = world.is_css_present(sel) if isnt_marked:
has_expected = world.is_css_not_present(sel)
else:
has_expected = world.is_css_present(sel)
# As soon as we find the selector, break out of the loop # As soon as we find the selector, break out of the loop
if has_expected: if has_expected:
......
...@@ -24,6 +24,8 @@ from capa.tests.response_xml_factory import OptionResponseXMLFactory, \ ...@@ -24,6 +24,8 @@ from capa.tests.response_xml_factory import OptionResponseXMLFactory, \
# Factories from capa.tests.response_xml_factory that we will use # Factories from capa.tests.response_xml_factory that we will use
# to generate the problem XML, with the keyword args used to configure # to generate the problem XML, with the keyword args used to configure
# the output. # the output.
# 'correct', 'incorrect', and 'unanswered' keys are lists of CSS selectors
# the presence of any in the list is sufficient
PROBLEM_DICT = { PROBLEM_DICT = {
'drop down': { 'drop down': {
'factory': OptionResponseXMLFactory(), 'factory': OptionResponseXMLFactory(),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment