Commit a361191e by Renzo Lucioni

Merge pull request #9381 from edx/release-2015-08-18-conflict

Release 2015 08 18 conflict
parents 08b9b03b c5f5a58c
...@@ -10,7 +10,7 @@ class CorrectMap(object): ...@@ -10,7 +10,7 @@ class CorrectMap(object):
in a capa problem. The response evaluation result for each answer_id includes in a capa problem. The response evaluation result for each answer_id includes
(correctness, npoints, msg, hint, hintmode). (correctness, npoints, msg, hint, hintmode).
- correctness : 'correct', 'incorrect', or 'partially-correct' - correctness : either 'correct' or 'incorrect'
- npoints : None, or integer specifying number of points awarded for this answer_id - npoints : None, or integer specifying number of points awarded for this answer_id
- msg : string (may have HTML) giving extra message response - msg : string (may have HTML) giving extra message response
(displayed below textline or textbox) (displayed below textline or textbox)
...@@ -101,23 +101,10 @@ class CorrectMap(object): ...@@ -101,23 +101,10 @@ class CorrectMap(object):
self.set(k, **correct_map[k]) self.set(k, **correct_map[k])
def is_correct(self, answer_id): def is_correct(self, answer_id):
"""
Takes an answer_id
Returns true if the problem is correct OR partially correct.
"""
if answer_id in self.cmap: if answer_id in self.cmap:
return self.cmap[answer_id]['correctness'] in ['correct', 'partially-correct'] return self.cmap[answer_id]['correctness'] in ['correct', 'partially-correct']
return None return None
def is_partially_correct(self, answer_id):
"""
Takes an answer_id
Returns true if the problem is partially correct.
"""
if answer_id in self.cmap:
return self.cmap[answer_id]['correctness'] == 'partially-correct'
return None
def is_queued(self, answer_id): def is_queued(self, answer_id):
return answer_id in self.cmap and self.cmap[answer_id]['queuestate'] is not None return answer_id in self.cmap and self.cmap[answer_id]['queuestate'] is not None
......
...@@ -85,7 +85,6 @@ class Status(object): ...@@ -85,7 +85,6 @@ class Status(object):
names = { names = {
'correct': _('correct'), 'correct': _('correct'),
'incorrect': _('incorrect'), 'incorrect': _('incorrect'),
'partially-correct': _('partially correct'),
'incomplete': _('incomplete'), 'incomplete': _('incomplete'),
'unanswered': _('unanswered'), 'unanswered': _('unanswered'),
'unsubmitted': _('unanswered'), 'unsubmitted': _('unanswered'),
...@@ -95,7 +94,6 @@ class Status(object): ...@@ -95,7 +94,6 @@ class Status(object):
# Translators: these are tooltips that indicate the state of an assessment question # Translators: these are tooltips that indicate the state of an assessment question
'correct': _('This is correct.'), 'correct': _('This is correct.'),
'incorrect': _('This is incorrect.'), 'incorrect': _('This is incorrect.'),
'partially-correct': _('This is partially correct.'),
'unanswered': _('This is unanswered.'), 'unanswered': _('This is unanswered.'),
'unsubmitted': _('This is unanswered.'), 'unsubmitted': _('This is unanswered.'),
'queued': _('This is being processed.'), 'queued': _('This is being processed.'),
...@@ -898,7 +896,7 @@ class MatlabInput(CodeInput): ...@@ -898,7 +896,7 @@ class MatlabInput(CodeInput):
Right now, we only want this button to show up when a problem has not been Right now, we only want this button to show up when a problem has not been
checked. checked.
""" """
if self.status in ['correct', 'incorrect', 'partially-correct']: if self.status in ['correct', 'incorrect']:
return False return False
else: else:
return True return True
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<div id="input_${id}_preview" class="equation"></div> <div id="input_${id}_preview" class="equation"></div>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div> </div>
% endif % endif
</div> </div>
...@@ -7,8 +7,6 @@ ...@@ -7,8 +7,6 @@
<% <%
if status == 'correct': if status == 'correct':
correctness = 'correct' correctness = 'correct'
elif status == 'partially-correct':
correctness = 'partially-correct'
elif status == 'incorrect': elif status == 'incorrect':
correctness = 'incorrect' correctness = 'incorrect'
else: else:
...@@ -33,7 +31,7 @@ ...@@ -33,7 +31,7 @@
/> ${choice_description} /> ${choice_description}
% 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 in ('correct', 'partially-correct', 'incorrect') and not show_correctness=='never': % if status in ('correct', 'incorrect') and not show_correctness=='never':
<span class="sr status">${choice_description|h} - ${status.display_name}</span> <span class="sr status">${choice_description|h} - ${status.display_name}</span>
% endif % endif
% endif % endif
...@@ -62,4 +60,4 @@ ...@@ -62,4 +60,4 @@
% if msg: % if msg:
<span class="message">${msg|n}</span> <span class="message">${msg|n}</span>
% endif % endif
</form> </form>
\ No newline at end of file
...@@ -20,8 +20,6 @@ ...@@ -20,8 +20,6 @@
correctness = 'correct' correctness = 'correct'
elif status == 'incorrect': elif status == 'incorrect':
correctness = 'incorrect' correctness = 'incorrect'
elif status == 'partially-correct':
correctness = 'partially-correct'
else: else:
correctness = None correctness = None
%> %>
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<div class="script_placeholder" data-src="/static/js/sylvester.js"></div> <div class="script_placeholder" data-src="/static/js/sylvester.js"></div>
<div class="script_placeholder" data-src="/static/js/crystallography.js"></div> <div class="script_placeholder" data-src="/static/js/crystallography.js"></div>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
<div class="status ${status.classname}" id="status_${id}"> <div class="status ${status.classname}" id="status_${id}">
% endif % endif
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
<span class="message">${msg|n}</span> <span class="message">${msg|n}</span>
% endif % endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div> </div>
% endif % endif
</section> </section>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<div class="script_placeholder" data-src="/static/js/capa/protex/protex.nocache.js?raw"/> <div class="script_placeholder" data-src="/static/js/capa/protex/protex.nocache.js?raw"/>
<div class="script_placeholder" data-src="${applet_loader}"/> <div class="script_placeholder" data-src="${applet_loader}"/>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
<div class="${status.classname}" id="status_${id}"> <div class="${status.classname}" id="status_${id}">
% endif % endif
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
</p> </p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div> </div>
% endif % endif
</section> </section>
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<div class="script_placeholder" data-src="${STATIC_URL}js/capa/drag_and_drop.js"></div> <div class="script_placeholder" data-src="${STATIC_URL}js/capa/drag_and_drop.js"></div>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
<div class="${status.classname}" id="status_${id}"> <div class="${status.classname}" id="status_${id}">
% endif % endif
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
<span class="message">${msg|n}</span> <span class="message">${msg|n}</span>
% endif % endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div> </div>
% endif % endif
</div> </div>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<div class="script_placeholder" data-src="/static/js/capa/genex/genex.nocache.js?raw"/> <div class="script_placeholder" data-src="/static/js/capa/genex/genex.nocache.js?raw"/>
<div class="script_placeholder" data-src="${applet_loader}"/> <div class="script_placeholder" data-src="${applet_loader}"/>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
<div class="${status.classname}" id="status_${id}"> <div class="${status.classname}" id="status_${id}">
% endif % endif
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
</p> </p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div> </div>
% endif % endif
</section> </section>
......
<section id="editamoleculeinput_${id}" class="editamoleculeinput"> <section id="editamoleculeinput_${id}" class="editamoleculeinput">
<div class="script_placeholder" data-src="${applet_loader}"/> <div class="script_placeholder" data-src="${applet_loader}"/>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
<div class="${status.classname}" id="status_${id}"> <div class="${status.classname}" id="status_${id}">
% endif % endif
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
<div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div> <div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div> </div>
% endif % endif
</section> </section>
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
<div class="script_placeholder" data-src="${jschannel_loader}"/> <div class="script_placeholder" data-src="${jschannel_loader}"/>
<div class="script_placeholder" data-src="${jsinput_loader}"/> <div class="script_placeholder" data-src="${jsinput_loader}"/>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
<div class="${status.classname}" id="status_${id}"> <div class="${status.classname}" id="status_${id}">
% endif % endif
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
<div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div> <div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div> </div>
% endif % endif
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<div class="script_placeholder" data-src="${preprocessor['script_src']}"/> <div class="script_placeholder" data-src="${preprocessor['script_src']}"/>
% endif % endif
% if status in ('unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete'): % if status in ('unsubmitted', 'correct', 'incorrect', 'incomplete'):
<div class="${status.classname} ${doinline}" id="status_${id}"> <div class="${status.classname} ${doinline}" id="status_${id}">
% endif % endif
% if hidden: % if hidden:
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
% endif % endif
% if status in ('unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete'): % if status in ('unsubmitted', 'correct', 'incorrect', 'incomplete'):
</div> </div>
% endif % endif
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<div class="script_placeholder" data-src="/static/js/vsepr/vsepr.js"></div> <div class="script_placeholder" data-src="/static/js/vsepr/vsepr.js"></div>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
<div class="${status.classname}" id="status_${id}"> <div class="${status.classname}" id="status_${id}">
% endif % endif
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
% if msg: % if msg:
<span class="message">${msg|n}</span> <span class="message">${msg|n}</span>
% endif % endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div> </div>
% endif % endif
</section> </section>
...@@ -49,9 +49,6 @@ class ResponseXMLFactory(object): ...@@ -49,9 +49,6 @@ class ResponseXMLFactory(object):
*num_inputs*: The number of input elements *num_inputs*: The number of input elements
to create [DEFAULT: 1] to create [DEFAULT: 1]
*credit_type*: String of comma-separated words specifying the
partial credit grading scheme.
Returns a string representation of the XML tree. Returns a string representation of the XML tree.
""" """
...@@ -61,7 +58,6 @@ class ResponseXMLFactory(object): ...@@ -61,7 +58,6 @@ class ResponseXMLFactory(object):
script = kwargs.get('script', None) script = kwargs.get('script', None)
num_responses = kwargs.get('num_responses', 1) num_responses = kwargs.get('num_responses', 1)
num_inputs = kwargs.get('num_inputs', 1) num_inputs = kwargs.get('num_inputs', 1)
credit_type = kwargs.get('credit_type', None)
# The root is <problem> # The root is <problem>
root = etree.Element("problem") root = etree.Element("problem")
...@@ -79,11 +75,6 @@ class ResponseXMLFactory(object): ...@@ -79,11 +75,6 @@ class ResponseXMLFactory(object):
# Add the response(s) # Add the response(s)
for __ in range(int(num_responses)): for __ in range(int(num_responses)):
response_element = self.create_response_element(**kwargs) response_element = self.create_response_element(**kwargs)
# Set partial credit
if credit_type is not None:
response_element.set('partial_credit', str(credit_type))
root.append(response_element) root.append(response_element)
# Add input elements # Add input elements
...@@ -141,10 +132,6 @@ class ResponseXMLFactory(object): ...@@ -141,10 +132,6 @@ class ResponseXMLFactory(object):
*choice_names": List of strings identifying the choices. *choice_names": List of strings identifying the choices.
If specified, you must ensure that If specified, you must ensure that
len(choice_names) == len(choices) len(choice_names) == len(choices)
*points*: List of strings giving partial credit values (0-1)
for each choice. Interpreted as floats in problem.
If specified, ensure len(points) == len(choices)
""" """
# Names of group elements # Names of group elements
group_element_names = { group_element_names = {
...@@ -157,23 +144,15 @@ class ResponseXMLFactory(object): ...@@ -157,23 +144,15 @@ class ResponseXMLFactory(object):
choices = kwargs.get('choices', [True]) choices = kwargs.get('choices', [True])
choice_type = kwargs.get('choice_type', 'multiple') choice_type = kwargs.get('choice_type', 'multiple')
choice_names = kwargs.get('choice_names', [None] * len(choices)) choice_names = kwargs.get('choice_names', [None] * len(choices))
points = kwargs.get('points', [None] * len(choices))
# Create the <choicegroup>, <checkboxgroup>, or <radiogroup> element # Create the <choicegroup>, <checkboxgroup>, or <radiogroup> element
assert choice_type in group_element_names assert choice_type in group_element_names
group_element = etree.Element(group_element_names[choice_type]) group_element = etree.Element(group_element_names[choice_type])
# Create the <choice> elements # Create the <choice> elements
for (correct_val, name, pointval) in zip(choices, choice_names, points): for (correct_val, name) in zip(choices, choice_names):
choice_element = etree.SubElement(group_element, "choice") choice_element = etree.SubElement(group_element, "choice")
if correct_val is True: choice_element.set("correct", "true" if correct_val else "false")
correctness = 'true'
elif correct_val is False:
correctness = 'false'
elif 'partial' in correct_val:
correctness = 'partial'
choice_element.set('correct', correctness)
# Add a name identifying the choice, if one exists # Add a name identifying the choice, if one exists
# For simplicity, we use the same string as both the # For simplicity, we use the same string as both the
...@@ -182,10 +161,6 @@ class ResponseXMLFactory(object): ...@@ -182,10 +161,6 @@ class ResponseXMLFactory(object):
choice_element.text = str(name) choice_element.text = str(name)
choice_element.set("name", str(name)) choice_element.set("name", str(name))
# Add point values for partially-correct choices.
if pointval:
choice_element.set("point_value", str(pointval))
return group_element return group_element
...@@ -201,22 +176,10 @@ class NumericalResponseXMLFactory(ResponseXMLFactory): ...@@ -201,22 +176,10 @@ class NumericalResponseXMLFactory(ResponseXMLFactory):
*tolerance*: The tolerance within which a response *tolerance*: The tolerance within which a response
is considered correct. Can be a decimal (e.g. "0.01") is considered correct. Can be a decimal (e.g. "0.01")
or percentage (e.g. "2%") or percentage (e.g. "2%")
*credit_type*: String of comma-separated words specifying the
partial credit grading scheme.
*partial_range*: The multiplier for the tolerance that will
still provide partial credit in the "close" grading style
*partial_answers*: A string of comma-separated alternate
answers that will receive partial credit in the "list" style
""" """
answer = kwargs.get('answer', None) answer = kwargs.get('answer', None)
tolerance = kwargs.get('tolerance', None) tolerance = kwargs.get('tolerance', None)
credit_type = kwargs.get('credit_type', None)
partial_range = kwargs.get('partial_range', None)
partial_answers = kwargs.get('partial_answers', None)
response_element = etree.Element('numericalresponse') response_element = etree.Element('numericalresponse')
...@@ -230,13 +193,6 @@ class NumericalResponseXMLFactory(ResponseXMLFactory): ...@@ -230,13 +193,6 @@ class NumericalResponseXMLFactory(ResponseXMLFactory):
responseparam_element = etree.SubElement(response_element, 'responseparam') responseparam_element = etree.SubElement(response_element, 'responseparam')
responseparam_element.set('type', 'tolerance') responseparam_element.set('type', 'tolerance')
responseparam_element.set('default', str(tolerance)) responseparam_element.set('default', str(tolerance))
if partial_range is not None and 'close' in credit_type:
responseparam_element.set('partial_range', str(partial_range))
if partial_answers is not None and 'list' in credit_type:
# The line below throws a false positive pylint violation, so it's excepted.
responseparam_element = etree.SubElement(response_element, 'responseparam') # pylint: disable=E1101
responseparam_element.set('partial_answers', partial_answers)
return response_element return response_element
...@@ -673,25 +629,15 @@ class OptionResponseXMLFactory(ResponseXMLFactory): ...@@ -673,25 +629,15 @@ class OptionResponseXMLFactory(ResponseXMLFactory):
*options*: a list of possible options the user can choose from [REQUIRED] *options*: a list of possible options the user can choose from [REQUIRED]
You must specify at least 2 options. You must specify at least 2 options.
*correct_option*: a string with comma-separated correct choices [REQUIRED] *correct_option*: the correct choice from the list of options [REQUIRED]
*partial_option*: a string with comma-separated partially-correct choices
*point_values*: a string with comma-separated values (0-1) that give the
partial credit values in the "points" grading scheme.
Must have one per partial option.
*credit_type*: String of comma-separated words specifying the
partial credit grading scheme.
""" """
options_list = kwargs.get('options', None) options_list = kwargs.get('options', None)
correct_option = kwargs.get('correct_option', None) correct_option = kwargs.get('correct_option', None)
partial_option = kwargs.get('partial_option', None)
point_values = kwargs.get('point_values', None)
credit_type = kwargs.get('credit_type', None)
assert options_list and correct_option assert options_list and correct_option
assert len(options_list) > 1 assert len(options_list) > 1
for option in correct_option.split(','): assert correct_option in options_list
assert option.strip() in options_list
# Create the <optioninput> element # Create the <optioninput> element
optioninput_element = etree.Element("optioninput") optioninput_element = etree.Element("optioninput")
...@@ -705,15 +651,6 @@ class OptionResponseXMLFactory(ResponseXMLFactory): ...@@ -705,15 +651,6 @@ class OptionResponseXMLFactory(ResponseXMLFactory):
# Set the "correct" attribute # Set the "correct" attribute
optioninput_element.set('correct', str(correct_option)) optioninput_element.set('correct', str(correct_option))
# If we have 'points'-style partial credit...
if 'points' in str(credit_type):
# Set the "partial" attribute
optioninput_element.set('partial', str(partial_option))
# Set the "point_values" attribute, if it's specified.
if point_values is not None:
optioninput_element.set('point_values', str(point_values))
return optioninput_element return optioninput_element
......
...@@ -17,7 +17,7 @@ class CorrectMapTest(unittest.TestCase): ...@@ -17,7 +17,7 @@ class CorrectMapTest(unittest.TestCase):
self.cmap = CorrectMap() self.cmap = CorrectMap()
def test_set_input_properties(self): def test_set_input_properties(self):
# Set the correctmap properties for three inputs # Set the correctmap properties for two inputs
self.cmap.set( self.cmap.set(
answer_id='1_2_1', answer_id='1_2_1',
correctness='correct', correctness='correct',
...@@ -41,34 +41,15 @@ class CorrectMapTest(unittest.TestCase): ...@@ -41,34 +41,15 @@ class CorrectMapTest(unittest.TestCase):
queuestate=None queuestate=None
) )
self.cmap.set(
answer_id='3_2_1',
correctness='partially-correct',
npoints=3,
msg=None,
hint=None,
hintmode=None,
queuestate=None
)
# Assert that each input has the expected properties # Assert that each input has the expected properties
self.assertTrue(self.cmap.is_correct('1_2_1')) self.assertTrue(self.cmap.is_correct('1_2_1'))
self.assertFalse(self.cmap.is_correct('2_2_1')) self.assertFalse(self.cmap.is_correct('2_2_1'))
self.assertTrue(self.cmap.is_correct('3_2_1'))
self.assertTrue(self.cmap.is_partially_correct('3_2_1'))
self.assertFalse(self.cmap.is_partially_correct('2_2_1'))
# Intentionally testing an item that's not in cmap.
self.assertFalse(self.cmap.is_partially_correct('9_2_1'))
self.assertEqual(self.cmap.get_correctness('1_2_1'), 'correct') self.assertEqual(self.cmap.get_correctness('1_2_1'), 'correct')
self.assertEqual(self.cmap.get_correctness('2_2_1'), 'incorrect') self.assertEqual(self.cmap.get_correctness('2_2_1'), 'incorrect')
self.assertEqual(self.cmap.get_correctness('3_2_1'), 'partially-correct')
self.assertEqual(self.cmap.get_npoints('1_2_1'), 5) self.assertEqual(self.cmap.get_npoints('1_2_1'), 5)
self.assertEqual(self.cmap.get_npoints('2_2_1'), 0) self.assertEqual(self.cmap.get_npoints('2_2_1'), 0)
self.assertEqual(self.cmap.get_npoints('3_2_1'), 3)
self.assertEqual(self.cmap.get_msg('1_2_1'), 'Test message') self.assertEqual(self.cmap.get_msg('1_2_1'), 'Test message')
self.assertEqual(self.cmap.get_msg('2_2_1'), None) self.assertEqual(self.cmap.get_msg('2_2_1'), None)
...@@ -102,8 +83,6 @@ class CorrectMapTest(unittest.TestCase): ...@@ -102,8 +83,6 @@ class CorrectMapTest(unittest.TestCase):
# 3) incorrect, 5 points # 3) incorrect, 5 points
# 4) incorrect, None points # 4) incorrect, None points
# 5) correct, 0 points # 5) correct, 0 points
# 4) partially correct, 2.5 points
# 5) partially correct, None points
self.cmap.set( self.cmap.set(
answer_id='1_2_1', answer_id='1_2_1',
correctness='correct', correctness='correct',
...@@ -134,30 +113,15 @@ class CorrectMapTest(unittest.TestCase): ...@@ -134,30 +113,15 @@ class CorrectMapTest(unittest.TestCase):
npoints=0 npoints=0
) )
self.cmap.set(
answer_id='6_2_1',
correctness='partially-correct',
npoints=2.5
)
self.cmap.set(
answer_id='7_2_1',
correctness='partially-correct',
npoints=None
)
# Assert that we get the expected points # Assert that we get the expected points
# If points assigned --> npoints # If points assigned --> npoints
# If no points assigned and correct --> 1 point # If no points assigned and correct --> 1 point
# If no points assigned and partially correct --> 1 point
# If no points assigned and incorrect --> 0 points # If no points assigned and incorrect --> 0 points
self.assertEqual(self.cmap.get_npoints('1_2_1'), 5.3) self.assertEqual(self.cmap.get_npoints('1_2_1'), 5.3)
self.assertEqual(self.cmap.get_npoints('2_2_1'), 1) self.assertEqual(self.cmap.get_npoints('2_2_1'), 1)
self.assertEqual(self.cmap.get_npoints('3_2_1'), 5) self.assertEqual(self.cmap.get_npoints('3_2_1'), 5)
self.assertEqual(self.cmap.get_npoints('4_2_1'), 0) self.assertEqual(self.cmap.get_npoints('4_2_1'), 0)
self.assertEqual(self.cmap.get_npoints('5_2_1'), 0) self.assertEqual(self.cmap.get_npoints('5_2_1'), 0)
self.assertEqual(self.cmap.get_npoints('6_2_1'), 2.5)
self.assertEqual(self.cmap.get_npoints('7_2_1'), 1)
def test_set_overall_message(self): def test_set_overall_message(self):
......
...@@ -24,7 +24,6 @@ ...@@ -24,7 +24,6 @@
$annotation-yellow: rgba(255,255,10,0.3); $annotation-yellow: rgba(255,255,10,0.3);
$color-copy-tip: rgb(100,100,100); $color-copy-tip: rgb(100,100,100);
$correct: $green-d1; $correct: $green-d1;
$partiallycorrect: $green-d1;
$incorrect: $red; $incorrect: $red;
// +Extends - Capa // +Extends - Capa
...@@ -76,11 +75,6 @@ h2 { ...@@ -76,11 +75,6 @@ h2 {
color: $correct; color: $correct;
} }
.feedback-hint-partially-correct {
margin-top: ($baseline/2);
color: $partiallycorrect;
}
.feedback-hint-incorrect { .feedback-hint-incorrect {
margin-top: ($baseline/2); margin-top: ($baseline/2);
color: $incorrect; color: $incorrect;
...@@ -180,16 +174,6 @@ div.problem { ...@@ -180,16 +174,6 @@ div.problem {
} }
} }
&.choicegroup_partially-correct {
@include status-icon($partiallycorrect, "\f00c");
border: 2px solid $partiallycorrect;
// keep green for correct answers on hover.
&:hover {
border-color: $partiallycorrect;
}
}
&.choicegroup_incorrect { &.choicegroup_incorrect {
@include status-icon($incorrect, "\f00d"); @include status-icon($incorrect, "\f00d");
border: 2px solid $incorrect; border: 2px solid $incorrect;
...@@ -243,11 +227,6 @@ div.problem { ...@@ -243,11 +227,6 @@ div.problem {
@include status-icon($correct, "\f00c"); @include status-icon($correct, "\f00c");
} }
// CASE: partially correct answer
&.partially-correct {
@include status-icon($partiallycorrect, "\f00c");
}
// CASE: incorrect answer // CASE: incorrect answer
&.incorrect { &.incorrect {
@include status-icon($incorrect, "\f00d"); @include status-icon($incorrect, "\f00d");
...@@ -359,19 +338,6 @@ div.problem { ...@@ -359,19 +338,6 @@ div.problem {
} }
} }
&.partially-correct, &.ui-icon-check {
p.status {
display: inline-block;
width: 25px;
height: 20px;
background: url('../images/partially-correct-icon.png') center center no-repeat;
}
input {
border-color: $partiallycorrect;
}
}
&.processing { &.processing {
p.status { p.status {
display: inline-block; display: inline-block;
...@@ -747,7 +713,7 @@ div.problem { ...@@ -747,7 +713,7 @@ div.problem {
height: 46px; height: 46px;
} }
> .incorrect, .partially-correct, .correct, .unanswered { > .incorrect, .correct, .unanswered {
.status { .status {
display: inline-block; display: inline-block;
...@@ -768,18 +734,6 @@ div.problem { ...@@ -768,18 +734,6 @@ div.problem {
} }
} }
// CASE: partially correct answer
> .partially-correct {
input {
border: 2px solid $partiallycorrect;
}
.status {
@include status-icon($partiallycorrect, "\f00c");
}
}
// CASE: correct answer // CASE: correct answer
> .correct { > .correct {
...@@ -821,7 +775,7 @@ div.problem { ...@@ -821,7 +775,7 @@ div.problem {
.indicator-container { .indicator-container {
display: inline-block; display: inline-block;
.status.correct:after, .status.partially-correct:after, .status.incorrect:after, .status.unanswered:after { .status.correct:after, .status.incorrect:after, .status.unanswered:after {
@include margin-left(0); @include margin-left(0);
} }
} }
...@@ -987,20 +941,6 @@ div.problem { ...@@ -987,20 +941,6 @@ div.problem {
} }
} }
.detailed-targeted-feedback-partially-correct {
> p:first-child {
@extend %t-strong;
color: $partiallycorrect;
text-transform: uppercase;
font-style: normal;
font-size: 0.9em;
}
p:last-child {
margin-bottom: 0;
}
}
.detailed-targeted-feedback-correct { .detailed-targeted-feedback-correct {
> p:first-child { > p:first-child {
@extend %t-strong; @extend %t-strong;
...@@ -1195,14 +1135,6 @@ div.problem { ...@@ -1195,14 +1135,6 @@ div.problem {
} }
} }
.result-partially-correct {
background: url('../images/partially-correct-icon.png') left 20px no-repeat;
.result-actual-output {
color: #090;
}
}
.result-incorrect { .result-incorrect {
background: url('../images/incorrect-icon.png') left 20px no-repeat; background: url('../images/incorrect-icon.png') left 20px no-repeat;
...@@ -1408,14 +1340,6 @@ div.problem { ...@@ -1408,14 +1340,6 @@ div.problem {
} }
} }
label.choicetextgroup_partially-correct, section.choicetextgroup_partially-correct {
@extend label.choicegroup_partially-correct;
input[type="text"] {
border-color: $partiallycorrect;
}
}
label.choicetextgroup_incorrect, section.choicetextgroup_incorrect { label.choicetextgroup_incorrect, section.choicetextgroup_incorrect {
@extend label.choicegroup_incorrect; @extend label.choicegroup_incorrect;
} }
......
...@@ -64,12 +64,6 @@ class ProblemPage(PageObject): ...@@ -64,12 +64,6 @@ class ProblemPage(PageObject):
""" """
self.q(css='div.problem div.capa_inputtype.textline input').fill(text) self.q(css='div.problem div.capa_inputtype.textline input').fill(text)
def fill_answer_numerical(self, text):
"""
Fill in the answer to a numerical problem.
"""
self.q(css='div.problem section.inputtype input').fill(text)
def click_check(self): def click_check(self):
""" """
Click the Check button! Click the Check button!
...@@ -90,24 +84,6 @@ class ProblemPage(PageObject): ...@@ -90,24 +84,6 @@ class ProblemPage(PageObject):
""" """
return self.q(css="div.problem div.capa_inputtype.textline div.correct span.status").is_present() return self.q(css="div.problem div.capa_inputtype.textline div.correct span.status").is_present()
def simpleprob_is_correct(self):
"""
Is there a "correct" status showing? Works with simple problem types.
"""
return self.q(css="div.problem section.inputtype div.correct span.status").is_present()
def simpleprob_is_partially_correct(self):
"""
Is there a "partially correct" status showing? Works with simple problem types.
"""
return self.q(css="div.problem section.inputtype div.partially-correct span.status").is_present()
def simpleprob_is_incorrect(self):
"""
Is there an "incorrect" status showing? Works with simple problem types.
"""
return self.q(css="div.problem section.inputtype div.incorrect span.status").is_present()
def click_clarification(self, index=0): def click_clarification(self, index=0):
""" """
Click on an inline icon that can be included in problem text using an HTML <clarification> element: Click on an inline icon that can be included in problem text using an HTML <clarification> element:
......
...@@ -213,35 +213,3 @@ class ProblemWithMathjax(ProblemsTest): ...@@ -213,35 +213,3 @@ class ProblemWithMathjax(ProblemsTest):
self.assertIn("Hint (2 of 2): mathjax should work2", problem_page.hint_text) self.assertIn("Hint (2 of 2): mathjax should work2", problem_page.hint_text)
self.assertTrue(problem_page.mathjax_rendered_in_hint, "MathJax did not rendered in problem hint") self.assertTrue(problem_page.mathjax_rendered_in_hint, "MathJax did not rendered in problem hint")
class ProblemPartialCredit(ProblemsTest):
"""
Makes sure that the partial credit is appearing properly.
"""
def get_problem(self):
"""
Create a problem with partial credit.
"""
xml = dedent("""
<problem>
<p>The answer is 1. Partial credit for -1.</p>
<numericalresponse answer="1" partial_credit="list">
<formulaequationinput label="How many miles away from Earth is the sun? Use scientific notation to answer." />
<responseparam type="tolerance" default="0.01" />
<responseparam partial_answers="-1" />
</numericalresponse>
</problem>
""")
return XBlockFixtureDesc('problem', 'PARTIAL CREDIT TEST PROBLEM', data=xml)
def test_partial_credit(self):
"""
Test that we can see the partial credit value and feedback.
"""
self.courseware_page.visit()
problem_page = ProblemPage(self.browser)
self.assertEqual(problem_page.problem_name, 'PARTIAL CREDIT TEST PROBLEM')
problem_page.fill_answer_numerical('-1')
problem_page.click_check()
self.assertTrue(problem_page.simpleprob_is_partially_correct())
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
This test file will run through some LMS test scenarios regarding access and navigation of the LMS This test file will run through some LMS test scenarios regarding access and navigation of the LMS
""" """
import time import time
from mock import patch
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from django.conf import settings from django.conf import settings
...@@ -12,6 +13,7 @@ from courseware.tests.helpers import LoginEnrollmentTestCase ...@@ -12,6 +13,7 @@ from courseware.tests.helpers import LoginEnrollmentTestCase
from courseware.tests.factories import GlobalStaffFactory from courseware.tests.factories import GlobalStaffFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.django import modulestore
@attr('shard_1') @attr('shard_1')
...@@ -266,3 +268,45 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -266,3 +268,45 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase):
kwargs={'course_id': test_course_id} kwargs={'course_id': test_course_id}
) )
self.assert_request_status_code(302, url) self.assert_request_status_code(302, url)
def test_proctoring_js_includes(self):
"""
Make sure that proctoring JS does not get included on
courseware pages if either the FEATURE flag is turned off
or the course is not proctored enabled
"""
email, password = self.STUDENT_INFO[0]
self.login(email, password)
self.enroll(self.test_course, True)
test_course_id = self.test_course.id.to_deprecated_string()
with patch.dict(settings.FEATURES, {'ENABLE_PROCTORED_EXAMS': False}):
url = reverse(
'courseware',
kwargs={'course_id': test_course_id}
)
resp = self.client.get(url)
self.assertNotContains(resp, '/static/js/lms-proctoring.js')
with patch.dict(settings.FEATURES, {'ENABLE_PROCTORED_EXAMS': True}):
url = reverse(
'courseware',
kwargs={'course_id': test_course_id}
)
resp = self.client.get(url)
self.assertNotContains(resp, '/static/js/lms-proctoring.js')
# now set up a course which is proctored enabled
self.test_course.enable_proctored_exams = True
self.test_course.save()
modulestore().update_item(self.test_course, self.user.id)
resp = self.client.get(url)
self.assertContains(resp, '/static/js/lms-proctoring.js')
...@@ -39,6 +39,34 @@ class TestProctoringDashboardViews(ModuleStoreTestCase): ...@@ -39,6 +39,34 @@ class TestProctoringDashboardViews(ModuleStoreTestCase):
""" """
Test Pass Proctoring Tab is in the Instructor Dashboard Test Pass Proctoring Tab is in the Instructor Dashboard
""" """
self.instructor.is_staff = True
self.instructor.save()
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertTrue(self.proctoring_link in response.content) self.assertTrue(self.proctoring_link in response.content)
self.assertTrue('Allowance Section' in response.content) self.assertTrue('Allowance Section' in response.content)
def test_no_tab_non_global_staff(self):
"""
Test Pass Proctoring Tab is not in the Instructor Dashboard
for non global staff users
"""
self.instructor.is_staff = False
self.instructor.save()
response = self.client.get(self.url)
self.assertFalse(self.proctoring_link in response.content)
self.assertFalse('Allowance Section' in response.content)
@patch.dict(settings.FEATURES, {'ENABLE_PROCTORED_EXAMS': False})
def test_no_tab_flag_unset(self):
"""
Test Pass Proctoring Tab is not in the Instructor Dashboard
if the feature flag 'ENABLE_PROCTORED_EXAMS' is unset.
"""
self.instructor.is_staff = True
self.instructor.save()
response = self.client.get(self.url)
self.assertFalse(self.proctoring_link in response.content)
self.assertFalse('Allowance Section' in response.content)
...@@ -143,7 +143,13 @@ def instructor_dashboard_2(request, course_id): ...@@ -143,7 +143,13 @@ def instructor_dashboard_2(request, course_id):
sections.append(_section_e_commerce(course, access, paid_modes[0], is_white_label, is_white_label)) sections.append(_section_e_commerce(course, access, paid_modes[0], is_white_label, is_white_label))
# Gate access to Proctoring tab # Gate access to Proctoring tab
if settings.FEATURES.get('ENABLE_PROCTORED_EXAMS', False) and course.enable_proctored_exams: # only global staff (user.is_staff) is allowed to see this tab
can_see_proctoring = (
settings.FEATURES.get('ENABLE_PROCTORED_EXAMS', False) and
course.enable_proctored_exams and
request.user.is_staff
)
if can_see_proctoring:
sections.append(_section_proctoring(course, access)) sections.append(_section_proctoring(course, access))
# Certificates panel # Certificates panel
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
this.perPage = options.per_page || 10; this.perPage = options.per_page || 10;
this.username = options.username; this.username = options.username;
this.privileged = options.privileged; this.privileged = options.privileged;
this.staff = options.staff;
this.server_api = _.extend( this.server_api = _.extend(
{ {
...@@ -26,11 +27,11 @@ ...@@ -26,11 +27,11 @@
model: TeamMembershipModel, model: TeamMembershipModel,
canUserCreateTeam: function() { canUserCreateTeam: function() {
// Note: non-privileged users are automatically added to any team // Note: non-staff and non-privileged users are automatically added to any team
// that they create. This means that if multiple team membership is // that they create. This means that if multiple team membership is
// disabled that they cannot create a new team when they already // disabled that they cannot create a new team when they already
// belong to one. // belong to one.
return this.privileged || this.length === 0; return this.privileged || this.staff || this.length === 0;
} }
}); });
return TeamMembershipCollection; return TeamMembershipCollection;
......
...@@ -15,6 +15,7 @@ define(["jquery", "backbone", "teams/js/teams_tab_factory"], ...@@ -15,6 +15,7 @@ define(["jquery", "backbone", "teams/js/teams_tab_factory"],
userInfo: { userInfo: {
username: 'test-user', username: 'test-user',
privileged: false, privileged: false,
staff: false,
team_memberships_data: null team_memberships_data: null
} }
}); });
......
...@@ -173,7 +173,7 @@ define([ ...@@ -173,7 +173,7 @@ define([
AjaxHelpers.respondWithError( AjaxHelpers.respondWithError(
requests, requests,
400, 400,
{'error_message': {'user_message': 'User message', 'developer_message': 'Developer message' }} {'user_message': 'User message', 'developer_message': 'Developer message'}
); );
expect(teamEditView.$('.wrapper-msg .copy').text().trim()).toBe("User message"); expect(teamEditView.$('.wrapper-msg .copy').text().trim()).toBe("User message");
......
...@@ -130,7 +130,7 @@ define([ ...@@ -130,7 +130,7 @@ define([
it('does not allow access if the user is neither privileged nor a team member', function () { it('does not allow access if the user is neither privileged nor a team member', function () {
var teamsTabView = createTeamsTabView({ var teamsTabView = createTeamsTabView({
userInfo: TeamSpecHelpers.createMockUserInfo({ privileged: false }) userInfo: TeamSpecHelpers.createMockUserInfo({ privileged: false, staff: true })
}); });
expect(teamsTabView.readOnlyDiscussion({ expect(teamsTabView.readOnlyDiscussion({
attributes: { membership: [] } attributes: { membership: [] }
......
...@@ -100,6 +100,15 @@ define([ ...@@ -100,6 +100,15 @@ define([
verifyActions(teamsView); verifyActions(teamsView);
}); });
it('shows actions for a staff user already in a team', function () {
var staffMembership = TeamSpecHelpers.createMockTeamMemberships(
TeamSpecHelpers.createMockTeamMembershipsData(1, 5),
{ privileged: false, staff: true }
),
teamsView = createTopicTeamsView({ teamMemberships: staffMembership });
verifyActions(teamsView);
});
/* /*
// TODO: make this ready for prime time // TODO: make this ready for prime time
it('refreshes when the team membership changes', function() { it('refreshes when the team membership changes', function() {
......
...@@ -89,7 +89,8 @@ define([ ...@@ -89,7 +89,8 @@ define([
parse: true, parse: true,
url: 'api/teams/team_memberships', url: 'api/teams/team_memberships',
username: testUser, username: testUser,
privileged: false privileged: false,
staff: false
}), }),
options) options)
); );
...@@ -100,6 +101,7 @@ define([ ...@@ -100,6 +101,7 @@ define([
{ {
username: testUser, username: testUser,
privileged: false, privileged: false,
staff: false,
team_memberships_data: createMockTeamMembershipsData(1, 5) team_memberships_data: createMockTeamMembershipsData(1, 5)
}, },
options options
......
...@@ -125,9 +125,9 @@ ...@@ -125,9 +125,9 @@
}) })
.fail(function(data) { .fail(function(data) {
var response = JSON.parse(data.responseText); var response = JSON.parse(data.responseText);
var message = gettext("An error occurred. Please try again.") var message = gettext("An error occurred. Please try again.");
if ('error_message' in response && 'user_message' in response['error_message']){ if ('user_message' in response){
message = response['error_message']['user_message']; message = response.user_message;
} }
view.showMessage(message, message); view.showMessage(message, message);
}); });
......
...@@ -92,6 +92,7 @@ ...@@ -92,6 +92,7 @@
course_id: this.courseID, course_id: this.courseID,
username: this.userInfo.username, username: this.userInfo.username,
privileged: this.userInfo.privileged, privileged: this.userInfo.privileged,
staff: this.userInfo.staff,
parse: true parse: true
} }
).bootstrap(); ).bootstrap();
......
...@@ -143,7 +143,8 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase): ...@@ -143,7 +143,8 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase):
'name': 'Public Profiles', 'name': 'Public Profiles',
'description': 'Description for topic 6.' 'description': 'Description for topic 6.'
}, },
] ],
'max_team_size': 1
} }
cls.test_course_2 = CourseFactory.create( cls.test_course_2 = CourseFactory.create(
org='MIT', org='MIT',
...@@ -185,6 +186,13 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase): ...@@ -185,6 +186,13 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase):
profile.year_of_birth = 1970 profile.year_of_birth = 1970
profile.save() profile.save()
# This student is enrolled in the other course, but not yet a member of a team. This is to allow
# course_2 to use a max_team_size of 1 without breaking other tests on course_1
self.create_and_enroll_student(
courses=[self.test_course_2],
username='student_enrolled_other_course_not_on_team'
)
# 'solar team' is intentionally lower case to test case insensitivity in name ordering # 'solar team' is intentionally lower case to test case insensitivity in name ordering
self.test_team_1 = CourseTeamFactory.create( self.test_team_1 = CourseTeamFactory.create(
name=u'sólar team', name=u'sólar team',
...@@ -219,6 +227,14 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase): ...@@ -219,6 +227,14 @@ class TeamAPITestCase(APITestCase, SharedModuleStoreTestCase):
self.test_team_5.add_user(self.users['student_enrolled_both_courses_other_team']) self.test_team_5.add_user(self.users['student_enrolled_both_courses_other_team'])
self.test_team_6.add_user(self.users['student_enrolled_public_profile']) self.test_team_6.add_user(self.users['student_enrolled_public_profile'])
def build_membership_data_raw(self, username, team):
"""Assembles a membership creation payload based on the raw values provided."""
return {'username': username, 'team_id': team}
def build_membership_data(self, username, team):
"""Assembles a membership creation payload based on the username and team model provided."""
return self.build_membership_data_raw(self.users[username].username, team.team_id)
def create_and_enroll_student(self, courses=None, username=None): def create_and_enroll_student(self, courses=None, username=None):
""" Creates a new student and enrolls that student in the course. """ Creates a new student and enrolls that student in the course.
...@@ -507,14 +523,38 @@ class TestCreateTeamAPI(TeamAPITestCase): ...@@ -507,14 +523,38 @@ class TestCreateTeamAPI(TeamAPITestCase):
self.post_create_team(status, data) self.post_create_team(status, data)
def test_student_in_team(self): def test_student_in_team(self):
self.post_create_team( response = self.post_create_team(
400, 400,
{ data=self.build_team_data(
'course_id': str(self.test_course_1.id), name="Doomed team",
'description': "You are already on a team in this course." course=self.test_course_1,
}, description="Overly ambitious student"
),
user='student_enrolled' user='student_enrolled'
) )
self.assertEqual(
"You are already in a team in this course.",
json.loads(response.content)["user_message"]
)
@ddt.data('staff', 'course_staff', 'community_ta')
def test_privileged_create_multiple_teams(self, user):
""" Privileged users can create multiple teams, even if they are already in one. """
# First add the privileged user to a team.
self.post_create_membership(
200,
self.build_membership_data(user, self.test_team_1),
user=user
)
self.post_create_team(
data=self.build_team_data(
name="Another team",
course=self.test_course_1,
description="Privileged users are the best"
),
user=user
)
@ddt.data({'description': ''}, {'name': 'x' * 1000}, {'name': ''}) @ddt.data({'description': ''}, {'name': 'x' * 1000}, {'name': ''})
def test_bad_fields(self, kwargs): def test_bad_fields(self, kwargs):
...@@ -877,14 +917,6 @@ class TestListMembershipAPI(TeamAPITestCase): ...@@ -877,14 +917,6 @@ class TestListMembershipAPI(TeamAPITestCase):
class TestCreateMembershipAPI(TeamAPITestCase): class TestCreateMembershipAPI(TeamAPITestCase):
"""Test cases for the membership creation endpoint.""" """Test cases for the membership creation endpoint."""
def build_membership_data_raw(self, username, team):
"""Assembles a membership creation payload based on the raw values provided."""
return {'username': username, 'team_id': team}
def build_membership_data(self, username, team):
"""Assembles a membership creation payload based on the username and team model provided."""
return self.build_membership_data_raw(self.users[username].username, team.team_id)
@ddt.data( @ddt.data(
(None, 401), (None, 401),
('student_inactive', 401), ('student_inactive', 401),
...@@ -956,6 +988,14 @@ class TestCreateMembershipAPI(TeamAPITestCase): ...@@ -956,6 +988,14 @@ class TestCreateMembershipAPI(TeamAPITestCase):
) )
self.assertIn('not enrolled', json.loads(response.content)['developer_message']) self.assertIn('not enrolled', json.loads(response.content)['developer_message'])
def test_over_max_team_size_in_course_2(self):
response = self.post_create_membership(
400,
self.build_membership_data('student_enrolled_other_course_not_on_team', self.test_team_5),
user='student_enrolled_other_course_not_on_team'
)
self.assertIn('full', json.loads(response.content)['developer_message'])
@ddt.ddt @ddt.ddt
class TestDetailMembershipAPI(TeamAPITestCase): class TestDetailMembershipAPI(TeamAPITestCase):
......
...@@ -96,9 +96,13 @@ class TeamsDashboardView(View): ...@@ -96,9 +96,13 @@ class TeamsDashboardView(View):
context = { context = {
"course": course, "course": course,
"topics": topics_serializer.data, "topics": topics_serializer.data,
# It is necessary to pass both privileged and staff because only privileged users can
# administer discussion threads, but both privileged and staff users are allowed to create
# multiple teams (since they are not automatically added to teams upon creation).
"user_info": { "user_info": {
"username": user.username, "username": user.username,
"privileged": has_discussion_privileges(user, course_key), "privileged": has_discussion_privileges(user, course_key),
"staff": bool(has_access(user, 'staff', course_key)),
"team_memberships_data": team_memberships_serializer.data, "team_memberships_data": team_memberships_serializer.data,
}, },
"topic_url": reverse( "topic_url": reverse(
...@@ -372,14 +376,16 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -372,14 +376,16 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
'field_errors': field_errors, 'field_errors': field_errors,
}, status=status.HTTP_400_BAD_REQUEST) }, status=status.HTTP_400_BAD_REQUEST)
if CourseTeamMembership.user_in_team_for_course(request.user, course_key): # Course and global staff, as well as discussion "privileged" users, will not automatically
# be added to a team when they create it. They are allowed to create multiple teams.
team_administrator = (has_access(request.user, 'staff', course_key)
or has_discussion_privileges(request.user, course_key))
if not team_administrator and CourseTeamMembership.user_in_team_for_course(request.user, course_key):
error_message = build_api_error( error_message = build_api_error(
ugettext_noop('You are already in a team in this course.'), ugettext_noop('You are already in a team in this course.'),
course_id=course_id course_id=course_id
) )
return Response({ return Response(error_message, status=status.HTTP_400_BAD_REQUEST)
'error_message': error_message,
}, status=status.HTTP_400_BAD_REQUEST)
if course_key and not has_team_api_access(request.user, course_key): if course_key and not has_team_api_access(request.user, course_key):
return Response(status=status.HTTP_403_FORBIDDEN) return Response(status=status.HTTP_403_FORBIDDEN)
...@@ -396,8 +402,7 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -396,8 +402,7 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
}, status=status.HTTP_400_BAD_REQUEST) }, status=status.HTTP_400_BAD_REQUEST)
else: else:
team = serializer.save() team = serializer.save()
if not (has_access(request.user, 'staff', course_key) if not team_administrator:
or has_discussion_privileges(request.user, course_key)):
# Add the creating user to the team. # Add the creating user to the team.
team.add_user(request.user) team.add_user(request.user)
return Response(CourseTeamSerializer(team).data) return Response(CourseTeamSerializer(team).data)
...@@ -914,6 +919,13 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -914,6 +919,13 @@ class MembershipListView(ExpandableFieldViewMixin, GenericAPIView):
except User.DoesNotExist: except User.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND) return Response(status=status.HTTP_404_NOT_FOUND)
course_module = modulestore().get_course(team.course_id)
if course_module.teams_max_size is not None and team.users.count() >= course_module.teams_max_size:
return Response(
build_api_error(ugettext_noop("This team is already full.")),
status=status.HTTP_400_BAD_REQUEST
)
try: try:
membership = team.add_user(user) membership = team.add_user(user)
except AlreadyOnTeamInCourse: except AlreadyOnTeamInCourse:
......
...@@ -33,9 +33,9 @@ specific_student_selected = selected(not staff_selected and masquerade.user_name ...@@ -33,9 +33,9 @@ specific_student_selected = selected(not staff_selected and masquerade.user_name
student_selected = selected(not staff_selected and not specific_student_selected and not masquerade_group_id) student_selected = selected(not staff_selected and not specific_student_selected and not masquerade_group_id)
include_proctoring = settings.FEATURES.get('ENABLE_PROCTORED_EXAMS', False) and course.enable_proctored_exams include_proctoring = settings.FEATURES.get('ENABLE_PROCTORED_EXAMS', False) and course.enable_proctored_exams
%> %>
<%static:js group='proctoring'/>
% if include_proctoring: % if include_proctoring:
<%static:js group='proctoring'/>
% for template_name in ["proctored-exam-status"]: % for template_name in ["proctored-exam-status"]:
<script type="text/template" id="${template_name}-tpl"> <script type="text/template" id="${template_name}-tpl">
<%static:include path="courseware/${template_name}.underscore" /> <%static:include path="courseware/${template_name}.underscore" />
......
...@@ -58,7 +58,7 @@ git+https://github.com/edx/ecommerce-api-client.git@1.1.0#egg=ecommerce-api-clie ...@@ -58,7 +58,7 @@ git+https://github.com/edx/ecommerce-api-client.git@1.1.0#egg=ecommerce-api-clie
-e git+https://github.com/edx/edx-user-state-client.git@30c0ad4b9f57f8d48d6943eb585ec8a9205f4469#egg=edx-user-state-client -e git+https://github.com/edx/edx-user-state-client.git@30c0ad4b9f57f8d48d6943eb585ec8a9205f4469#egg=edx-user-state-client
-e git+https://github.com/edx/edx-organizations.git@release-2015-08-03#egg=edx-organizations -e git+https://github.com/edx/edx-organizations.git@release-2015-08-03#egg=edx-organizations
git+https://github.com/edx/edx-proctoring.git@release-2015-08-18#egg=edx-proctoring==0.6.0 git+https://github.com/edx/edx-proctoring.git@0.6.2#egg=edx-proctoring==0.6.2
# Third Party XBlocks # Third Party XBlocks
-e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga -e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga
......
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