Heading 1
+${_("Heading 1")}
Multiple Choice
+${_("Multiple Choice")}
diff --git a/.ruby-gemset b/.ruby-gemset
index 93a8706d3e..77266c35f0 100644
--- a/.ruby-gemset
+++ b/.ruby-gemset
@@ -1 +1 @@
-mitx
+edx-platform
diff --git a/AUTHORS b/AUTHORS
index 67279f9053..091e054a45 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -73,3 +73,4 @@ David Baumgold
+ <% _.each(_.range(numEntries), function() { %>
+
+ <% }) %>
+
diff --git a/cms/templates/js/metadata-number-entry.underscore b/cms/templates/js/metadata-number-entry.underscore
new file mode 100644
index 0000000000..333233ef4e
--- /dev/null
+++ b/cms/templates/js/metadata-number-entry.underscore
@@ -0,0 +1,8 @@
+
{0}
-
-{1}
-
-{2}
-
-{3}
-
-
-
-
-""".format(
- pprint.pformat(request),
- pprint.pformat(dict(request.GET)),
- pprint.pformat(dict(request.POST)),
- pprint.pformat(dict(request.REQUEST)),
- ))
diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py
index 2f33b66bfd..2ee82e2fb4 100644
--- a/common/lib/calc/calc.py
+++ b/common/lib/calc/calc.py
@@ -182,8 +182,8 @@ def evaluator(variables, functions, string, cs=False):
number_part = Word(nums)
- # 0.33 or 7 or .34
- inner_number = (number_part + Optional("." + number_part)) | ("." + number_part)
+ # 0.33 or 7 or .34 or 16.
+ inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part)
# 0.33k or -17
number = (Optional(minus | plus) + inner_number
diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/tests/test_calc.py
index 58d0860af6..cfa1b7525d 100644
--- a/common/lib/calc/tests/test_calc.py
+++ b/common/lib/calc/tests/test_calc.py
@@ -5,6 +5,7 @@ Unit tests for calc.py
import unittest
import numpy
import calc
+from pyparsing import ParseException
class EvaluatorTest(unittest.TestCase):
@@ -20,6 +21,11 @@ class EvaluatorTest(unittest.TestCase):
def test_number_input(self):
"""
Test different kinds of float inputs
+
+ See also
+ test_trailing_period (slightly different)
+ test_exponential_answer
+ test_si_suffix
"""
easy_eval = lambda x: calc.evaluator({}, {}, x)
@@ -30,7 +36,22 @@ class EvaluatorTest(unittest.TestCase):
self.assertEqual(easy_eval("-13"), -13)
self.assertEqual(easy_eval("-3.14"), -3.14)
self.assertEqual(easy_eval("-.618033989"), -0.618033989)
- # See also test_exponential_answer and test_si_suffix
+
+ def test_period(self):
+ """
+ The string '.' should not evaluate to anything.
+ """
+ self.assertRaises(ParseException, calc.evaluator, {}, {}, '.')
+ self.assertRaises(ParseException, calc.evaluator, {}, {}, '1+.')
+
+ def test_trailing_period(self):
+ """
+ Test that things like '4.' will be 4 and not throw an error
+ """
+ try:
+ self.assertEqual(4.0, calc.evaluator({}, {}, '4.'))
+ except ParseException:
+ self.fail("'4.' is a valid input, but threw an exception")
def test_exponential_answer(self):
"""
diff --git a/common/lib/capa/setup.py b/common/lib/capa/setup.py
index 2e73701060..dcb631e376 100644
--- a/common/lib/capa/setup.py
+++ b/common/lib/capa/setup.py
@@ -4,5 +4,5 @@ setup(
name="capa",
version="0.1",
packages=find_packages(exclude=["tests"]),
- install_requires=["distribute==0.6.28"],
+ install_requires=["distribute>=0.6.28"],
)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index eb6bdc18c9..9ac540138e 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -66,22 +66,51 @@ class ComplexEncoder(json.JSONEncoder):
class CapaFields(object):
attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state)
- max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
+ max_attempts = StringyInteger(
+ display_name="Maximum Attempts",
+ help="Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.",
+ values={"min": 1}, scope=Scope.settings
+ )
due = Date(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
- showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed",
- values=["answered", "always", "attempted", "closed", "never"])
+ showanswer = String(
+ display_name="Show Answer",
+ help="Defines when to show the answer to the problem. A default value can be set in Advanced Settings.",
+ scope=Scope.settings, default="closed",
+ values=[
+ {"display_name": "Always", "value": "always"},
+ {"display_name": "Answered", "value": "answered"},
+ {"display_name": "Attempted", "value": "attempted"},
+ {"display_name": "Closed", "value": "closed"},
+ {"display_name": "Finished", "value": "finished"},
+ {"display_name": "Past Due", "value": "past_due"},
+ {"display_name": "Never", "value": "never"}]
+ )
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
- rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
+ rerandomize = Randomization(
+ display_name="Randomization", help="Defines how often inputs are randomized when a student loads the problem. This setting only applies to problems that can have randomly generated numeric values. A default value can be set in Advanced Settings.",
+ default="always", scope=Scope.settings, values=[{"display_name": "Always", "value": "always"},
+ {"display_name": "On Reset", "value": "onreset"},
+ {"display_name": "Never", "value": "never"},
+ {"display_name": "Per Student", "value": "per_student"}]
+ )
data = String(help="XML data for the problem", scope=Scope.content)
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state)
- weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
+ weight = StringyFloat(
+ display_name="Problem Weight",
+ help="Defines the number of points each problem is worth. If the value is not set, each response field in the problem is worth one point.",
+ values={"min": 0, "step": .1},
+ scope=Scope.settings
+ )
markdown = String(help="Markdown source of this module", scope=Scope.settings)
- source_code = String(help="Source code for LaTeX and Word problems. This feature is not well-supported.", scope=Scope.settings)
+ source_code = String(
+ help="Source code for LaTeX and Word problems. This feature is not well-supported.",
+ scope=Scope.settings
+ )
class CapaModule(CapaFields, XModule):
diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py
index 0e32770afc..b3f0e19109 100644
--- a/common/lib/xmodule/xmodule/combined_open_ended_module.py
+++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py
@@ -5,7 +5,7 @@ from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor
from .x_module import XModule
-from xblock.core import Integer, Scope, String, Boolean, List
+from xblock.core import Integer, Scope, String, List
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple
from .fields import Date, StringyFloat, StringyInteger, StringyBoolean
@@ -48,27 +48,49 @@ class VersionInteger(Integer):
class CombinedOpenEndedFields(object):
- display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings)
+ display_name = String(
+ display_name="Display Name",
+ help="This name appears in the horizontal navigation at the top of the page.",
+ default="Open Ended Grading", scope=Scope.settings
+ )
current_task_number = StringyInteger(help="Current task that the student is on.", default=0, scope=Scope.user_state)
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state)
state = String(help="Which step within the current task that the student is on.", default="initial",
scope=Scope.user_state)
student_attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0,
scope=Scope.user_state)
- ready_to_reset = StringyBoolean(help="If the problem is ready to be reset or not.", default=False,
- scope=Scope.user_state)
- attempts = StringyInteger(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings)
- is_graded = StringyBoolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
- accept_file_upload = StringyBoolean(help="Whether or not the problem accepts file uploads.", default=False,
- scope=Scope.settings)
- skip_spelling_checks = StringyBoolean(help="Whether or not to skip initial spelling checks.", default=True,
- scope=Scope.settings)
+ ready_to_reset = StringyBoolean(
+ help="If the problem is ready to be reset or not.", default=False,
+ scope=Scope.user_state
+ )
+ attempts = StringyInteger(
+ display_name="Maximum Attempts",
+ help="The number of times the student can try to answer this problem.", default=1,
+ scope=Scope.settings, values = {"min" : 1 }
+ )
+ is_graded = StringyBoolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
+ accept_file_upload = StringyBoolean(
+ display_name="Allow File Uploads",
+ help="Whether or not the student can submit files as a response.", default=False, scope=Scope.settings
+ )
+ skip_spelling_checks = StringyBoolean(
+ display_name="Disable Quality Filter",
+ help="If False, the Quality Filter is enabled and submissions with poor spelling, short length, or poor grammar will not be peer reviewed.",
+ default=False, scope=Scope.settings
+ )
due = Date(help="Date that this problem is due by", default=None, scope=Scope.settings)
- graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
- scope=Scope.settings)
+ graceperiod = String(
+ help="Amount of time after the due date that submissions will be accepted",
+ default=None,
+ scope=Scope.settings
+ )
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content)
- weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
+ weight = StringyFloat(
+ display_name="Problem Weight",
+ help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
+ scope=Scope.settings, values = {"min" : 0 , "step": ".1"}
+ )
markdown = String(help="Markdown source of this module", scope=Scope.settings)
@@ -244,6 +266,6 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
def non_editable_metadata_fields(self):
non_editable_fields = super(CombinedOpenEndedDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([CombinedOpenEndedDescriptor.due, CombinedOpenEndedDescriptor.graceperiod,
- CombinedOpenEndedDescriptor.markdown])
+ CombinedOpenEndedDescriptor.markdown, CombinedOpenEndedDescriptor.version])
return non_editable_fields
diff --git a/common/lib/xmodule/xmodule/css/editor/edit.scss b/common/lib/xmodule/xmodule/css/editor/edit.scss
index ac53bb5a70..d30f69bcd2 100644
--- a/common/lib/xmodule/xmodule/css/editor/edit.scss
+++ b/common/lib/xmodule/xmodule/css/editor/edit.scss
@@ -10,8 +10,6 @@
position: relative;
@include linear-gradient(top, #d4dee8, #c9d5e2);
padding: 5px;
- border: 1px solid #3c3c3c;
- border-radius: 3px 3px 0 0;
border-bottom-color: #a5aaaf;
@include clearfix;
diff --git a/common/lib/xmodule/xmodule/css/problem/edit.scss b/common/lib/xmodule/xmodule/css/problem/edit.scss
index be5455e901..249b767e5e 100644
--- a/common/lib/xmodule/xmodule/css/problem/edit.scss
+++ b/common/lib/xmodule/xmodule/css/problem/edit.scss
@@ -5,7 +5,7 @@
.advanced-toggle {
@include white-button;
height: auto;
- margin-top: -1px;
+ margin-top: -4px;
padding: 3px 9px;
font-size: 12px;
@@ -16,7 +16,7 @@
color: $darkGrey !important;
pointer-events: none;
cursor: none;
-
+
&:hover {
box-shadow: 0 0 0 0 !important;
}
@@ -27,7 +27,7 @@
width: 21px;
height: 21px;
padding: 0;
- margin: 0 5px 0 15px;
+ margin: -1px 5px 0 15px;
border-radius: 22px;
border: 1px solid #a5aaaf;
background: #e5ecf3;
@@ -99,6 +99,13 @@
}
}
+.problem-editor {
+// adding padding to simple editor only - adjacent selector is needed since there are no toggles for CodeMirror
+ .markdown-box+.CodeMirror {
+ padding: 10px;
+ }
+}
+
.problem-editor-icon {
display: inline-block;
width: 26px;
diff --git a/common/lib/xmodule/xmodule/css/sequence/display.scss b/common/lib/xmodule/xmodule/css/sequence/display.scss
index e006e02773..ed09d5cf0f 100644
--- a/common/lib/xmodule/xmodule/css/sequence/display.scss
+++ b/common/lib/xmodule/xmodule/css/sequence/display.scss
@@ -170,7 +170,7 @@ nav.sequence-nav {
font-family: $sans-serif;
line-height: lh();
left: 0px;
- opacity: 0;
+ opacity: 0.0;
padding: 6px;
position: absolute;
top: 48px;
@@ -204,7 +204,7 @@ nav.sequence-nav {
p {
display: block;
margin-top: 4px;
- opacity: 1;
+ opacity: 1.0;
}
}
}
@@ -248,12 +248,12 @@ nav.sequence-nav {
}
&:hover {
- opacity: .5;
+ opacity: 0.5;
}
&.disabled {
cursor: normal;
- opacity: .4;
+ opacity: 0.4;
}
}
}
@@ -320,12 +320,12 @@ nav.sequence-bottom {
outline: 0;
&:hover {
- opacity: .5;
+ opacity: 0.5;
background-position: center 15px;
}
&.disabled {
- opacity: .4;
+ opacity: 0.4;
}
&:focus {
diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss
index bf575e74a3..f3f76dc0d6 100644
--- a/common/lib/xmodule/xmodule/css/video/display.scss
+++ b/common/lib/xmodule/xmodule/css/video/display.scss
@@ -41,7 +41,7 @@ div.video {
&:hover {
ul, div {
- opacity: 1;
+ opacity: 1.0;
}
}
@@ -158,7 +158,7 @@ div.video {
ol.video_speeds {
display: block;
- opacity: 1;
+ opacity: 1.0;
padding: 0;
margin: 0;
list-style: none;
@@ -208,7 +208,7 @@ div.video {
}
&:hover, &:active, &:focus {
- opacity: 1;
+ opacity: 1.0;
background-color: #444;
}
}
@@ -221,7 +221,7 @@ div.video {
border: 1px solid #000;
bottom: 46px;
display: none;
- opacity: 0;
+ opacity: 0.0;
position: absolute;
width: 133px;
z-index: 10;
@@ -264,7 +264,7 @@ div.video {
&.open {
.volume-slider-container {
display: block;
- opacity: 1;
+ opacity: 1.0;
}
}
@@ -302,7 +302,7 @@ div.video {
border: 1px solid #000;
bottom: 46px;
display: none;
- opacity: 0;
+ opacity: 0.0;
position: absolute;
width: 45px;
height: 125px;
@@ -395,7 +395,7 @@ div.video {
font-weight: 800;
line-height: 46px; //height of play pause buttons
margin-left: 0;
- opacity: 1;
+ opacity: 1.0;
padding: 0 lh(.5);
position: relative;
text-indent: -9999px;
@@ -410,7 +410,7 @@ div.video {
}
&.off {
- opacity: .7;
+ opacity: 0.7;
}
}
}
@@ -418,7 +418,7 @@ div.video {
&:hover section.video-controls {
ul, div {
- opacity: 1;
+ opacity: 1.0;
}
div.slider {
diff --git a/common/lib/xmodule/xmodule/css/videoalpha/display.scss b/common/lib/xmodule/xmodule/css/videoalpha/display.scss
index bf575e74a3..f3f76dc0d6 100644
--- a/common/lib/xmodule/xmodule/css/videoalpha/display.scss
+++ b/common/lib/xmodule/xmodule/css/videoalpha/display.scss
@@ -41,7 +41,7 @@ div.video {
&:hover {
ul, div {
- opacity: 1;
+ opacity: 1.0;
}
}
@@ -158,7 +158,7 @@ div.video {
ol.video_speeds {
display: block;
- opacity: 1;
+ opacity: 1.0;
padding: 0;
margin: 0;
list-style: none;
@@ -208,7 +208,7 @@ div.video {
}
&:hover, &:active, &:focus {
- opacity: 1;
+ opacity: 1.0;
background-color: #444;
}
}
@@ -221,7 +221,7 @@ div.video {
border: 1px solid #000;
bottom: 46px;
display: none;
- opacity: 0;
+ opacity: 0.0;
position: absolute;
width: 133px;
z-index: 10;
@@ -264,7 +264,7 @@ div.video {
&.open {
.volume-slider-container {
display: block;
- opacity: 1;
+ opacity: 1.0;
}
}
@@ -302,7 +302,7 @@ div.video {
border: 1px solid #000;
bottom: 46px;
display: none;
- opacity: 0;
+ opacity: 0.0;
position: absolute;
width: 45px;
height: 125px;
@@ -395,7 +395,7 @@ div.video {
font-weight: 800;
line-height: 46px; //height of play pause buttons
margin-left: 0;
- opacity: 1;
+ opacity: 1.0;
padding: 0 lh(.5);
position: relative;
text-indent: -9999px;
@@ -410,7 +410,7 @@ div.video {
}
&.off {
- opacity: .7;
+ opacity: 0.7;
}
}
}
@@ -418,7 +418,7 @@ div.video {
&:hover section.video-controls {
ul, div {
- opacity: 1;
+ opacity: 1.0;
}
div.slider {
diff --git a/common/lib/xmodule/xmodule/discussion_module.py b/common/lib/xmodule/xmodule/discussion_module.py
index 98082ddea2..aef4821839 100644
--- a/common/lib/xmodule/xmodule/discussion_module.py
+++ b/common/lib/xmodule/xmodule/discussion_module.py
@@ -8,8 +8,16 @@ from xblock.core import String, Scope
class DiscussionFields(object):
discussion_id = String(scope=Scope.settings)
- discussion_category = String(scope=Scope.settings)
- discussion_target = String(scope=Scope.settings)
+ discussion_category = String(
+ display_name="Category",
+ help="A category name for the discussion. This name appears in the left pane of the discussion forum for the course.",
+ scope=Scope.settings
+ )
+ discussion_target = String(
+ display_name="Subcategory",
+ help="A subcategory name for the discussion. This name appears in the left pane of the discussion forum for the course.",
+ scope=Scope.settings
+ )
sort_key = String(scope=Scope.settings)
diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
index d4c2ff00ae..5939fbcdd8 100644
--- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
@@ -289,6 +289,9 @@ class @CombinedOpenEnded
if @child_type == "openended"
@submit_button.hide()
@queueing()
+ if @task_number==1 and @task_count==1
+ @grader_status = $('.grader-status')
+ @grader_status.html("Response submitted for scoring.
") else if @child_state == 'post_assessment' if @child_type=="openended" @skip_button.show() @@ -311,6 +314,8 @@ class @CombinedOpenEnded if @task_number<@task_count @next_problem() else + if @task_number==1 and @task_count==1 + @show_combined_rubric_current() @show_results_current() @reset_button.show() diff --git a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee index 22308a5568..561ca07c8a 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee @@ -66,7 +66,7 @@ class @VideoPlayer extends Subview at: 'top center' onReady: (event) => - unless onTouchBasedDevice() + unless onTouchBasedDevice() or $('.video:first').data('autoplay') == 'False' $('.video-load-complete:first').data('video').player.play() onStateChange: (event) => diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/controller_query_service.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/controller_query_service.py index b807e05160..3668cd6cc9 100644 --- a/common/lib/xmodule/xmodule/open_ended_grading_classes/controller_query_service.py +++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/controller_query_service.py @@ -85,7 +85,7 @@ class MockControllerQueryService(object): def __init__(self, config, system): pass - def check_if_name_is_unique(self, **params): + def check_if_name_is_unique(self, *args, **kwargs): """ Mock later if needed. Stub function for now. @param params: @@ -93,7 +93,7 @@ class MockControllerQueryService(object): """ pass - def check_for_eta(self, **params): + def check_for_eta(self, *args, **kwargs): """ Mock later if needed. Stub function for now. @param params: @@ -101,19 +101,19 @@ class MockControllerQueryService(object): """ pass - def check_combined_notifications(self, **params): + def check_combined_notifications(self, *args, **kwargs): combined_notifications = '{"flagged_submissions_exist": false, "version": 1, "new_student_grading_to_view": false, "success": true, "staff_needs_to_grade": false, "student_needs_to_peer_grade": true, "overall_need_to_check": true}' return combined_notifications - def get_grading_status_list(self, **params): + def get_grading_status_list(self, *args, **kwargs): grading_status_list = '{"version": 1, "problem_list": [{"problem_name": "Science Question -- Machine Assessed", "grader_type": "NA", "eta_available": true, "state": "Waiting to be Graded", "eta": 259200, "location": "i4x://MITx/oe101x/combinedopenended/Science_SA_ML"}, {"problem_name": "Humanities Question -- Peer Assessed", "grader_type": "NA", "eta_available": true, "state": "Waiting to be Graded", "eta": 259200, "location": "i4x://MITx/oe101x/combinedopenended/Humanities_SA_Peer"}], "success": true}' return grading_status_list - def get_flagged_problem_list(self, **params): + def get_flagged_problem_list(self, *args, **kwargs): flagged_problem_list = '{"version": 1, "success": false, "error": "No flagged submissions exist for course: MITx/oe101x/2012_Fall"}' return flagged_problem_list - def take_action_on_flags(self, **params): + def take_action_on_flags(self, *args, **kwargs): """ Mock later if needed. Stub function for now. @param params: diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index b0d666621f..ccc3e31f51 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -10,7 +10,7 @@ from .x_module import XModule from xmodule.raw_module import RawDescriptor from xmodule.modulestore.django import modulestore from .timeinfo import TimeInfo -from xblock.core import Object, Integer, Boolean, String, Scope +from xblock.core import Object, String, Scope from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService @@ -22,24 +22,43 @@ USE_FOR_SINGLE_LOCATION = False LINK_TO_LOCATION = "" TRUE_DICT = [True, "True", "true", "TRUE"] MAX_SCORE = 1 -IS_GRADED = True +IS_GRADED = False EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please notify course staff." class PeerGradingFields(object): - use_for_single_location = StringyBoolean(help="Whether to use this for a single location or as a panel.", - default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings) - link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION, - scope=Scope.settings) - is_graded = StringyBoolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings) + use_for_single_location = StringyBoolean( + display_name="Show Single Problem", + help='When True, only the single problem specified by "Link to Problem Location" is shown. ' + 'When False, a panel is displayed with all problems available for peer grading.', + default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings + ) + link_to_location = String( + display_name="Link to Problem Location", + help='The location of the problem being graded. Only used when "Show Single Problem" is True.', + default=LINK_TO_LOCATION, scope=Scope.settings + ) + is_graded = StringyBoolean( + display_name="Graded", + help='Defines whether the student gets credit for grading this problem. Only used when "Show Single Problem" is True.', + default=IS_GRADED, scope=Scope.settings + ) due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings) grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings) - max_grade = StringyInteger(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE, - scope=Scope.settings) - student_data_for_location = Object(help="Student data for a given peer grading problem.", - scope=Scope.user_state) - weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings) + max_grade = StringyInteger( + help="The maximum grade that a student can receive for this problem.", default=MAX_SCORE, + scope=Scope.settings, values={"min": 0} + ) + student_data_for_location = Object( + help="Student data for a given peer grading problem.", + scope=Scope.user_state + ) + weight = StringyFloat( + display_name="Problem Weight", + help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.", + scope=Scope.settings, values={"min": 0, "step": ".1"} + ) class PeerGradingModule(PeerGradingFields, XModule): @@ -590,3 +609,11 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor): #Specify whether or not to pass in open ended interface needs_open_ended_interface = True + + @property + def non_editable_metadata_fields(self): + non_editable_fields = super(PeerGradingDescriptor, self).non_editable_metadata_fields + non_editable_fields.extend([PeerGradingFields.due_date, PeerGradingFields.grace_period_string, + PeerGradingFields.max_grade]) + return non_editable_fields + diff --git a/common/lib/xmodule/xmodule/templates/combinedopenended/default.yaml b/common/lib/xmodule/xmodule/templates/combinedopenended/default.yaml index d74517a3c3..f7d639ebfb 100644 --- a/common/lib/xmodule/xmodule/templates/combinedopenended/default.yaml +++ b/common/lib/xmodule/xmodule/templates/combinedopenended/default.yaml @@ -1,12 +1,6 @@ --- metadata: display_name: Open Ended Response - attempts: 1 - is_graded: False - version: 1 - skip_spelling_checks: False - accept_file_upload: False - weight: "" markdown: "" data: |
diff --git a/common/lib/xmodule/xmodule/templates/problem/empty.yaml b/common/lib/xmodule/xmodule/templates/problem/empty.yaml
index 39c9e7671c..97a2aef423 100644
--- a/common/lib/xmodule/xmodule/templates/problem/empty.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/empty.yaml
@@ -2,11 +2,8 @@
metadata:
display_name: Blank Common Problem
rerandomize: never
- showanswer: always
+ showanswer: finished
markdown: ""
- weight: ""
- empty: True
- attempts: ""
data: |
diff --git a/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml b/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml
index 3ef619d54b..ab1f22e3b2 100644
--- a/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml
@@ -2,9 +2,7 @@
metadata:
display_name: Image Mapped Input
rerandomize: never
- showanswer: always
- weight: ""
- attempts: ""
+ showanswer: finished
data: |
diff --git a/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml b/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml
index 3a35a35199..10d51de280 100644
--- a/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml
@@ -2,9 +2,7 @@
metadata:
display_name: Multiple Choice
rerandomize: never
- showanswer: always
- weight: ""
- attempts: ""
+ showanswer: finished
markdown:
"A multiple choice problem presents radio buttons for student input. Students can only select a single
option presented. Multiple Choice questions have been the subject of many areas of research due to the early
diff --git a/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml b/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml
index 1dc46f5f51..548fd94fab 100644
--- a/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml
@@ -2,9 +2,7 @@
metadata:
display_name: Numerical Input
rerandomize: never
- showanswer: always
- weight: ""
- attempts: ""
+ showanswer: finished
markdown:
"A numerical input problem accepts a line of text input from the
student, and evaluates the input for correctness based on its
diff --git a/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml b/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml
index f523c7fdc5..c2edfb1cbc 100644
--- a/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml
@@ -2,9 +2,7 @@
metadata:
display_name: Dropdown
rerandomize: never
- showanswer: always
- weight: ""
- attempts: ""
+ showanswer: finished
markdown:
"Dropdown problems give a limited set of options for students to respond with, and present those options
in a format that encourages them to search for a specific answer rather than being immediately presented
diff --git a/common/lib/xmodule/xmodule/templates/problem/string_response.yaml b/common/lib/xmodule/xmodule/templates/problem/string_response.yaml
index c018d3f6cf..64e3dc062f 100644
--- a/common/lib/xmodule/xmodule/templates/problem/string_response.yaml
+++ b/common/lib/xmodule/xmodule/templates/problem/string_response.yaml
@@ -2,9 +2,7 @@
metadata:
display_name: Text Input
rerandomize: never
- showanswer: always
- weight: ""
- attempts: ""
+ showanswer: finished
# Note, the extra newlines are needed to make the yaml parser add blank lines instead of folding
markdown:
"A text input problem accepts a line of text from the
diff --git a/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml b/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml
index 7f1c838ca9..53e9eeaae4 100644
--- a/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml
+++ b/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml
@@ -1,9 +1,5 @@
---
metadata:
display_name: Word cloud
- version: 1
- num_inputs: 5
- num_top_words: 250
- display_student_percents: True
data: {}
children: []
diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py
index 5fe57892be..a75dfc8d20 100644
--- a/common/lib/xmodule/xmodule/tests/test_import.py
+++ b/common/lib/xmodule/xmodule/tests/test_import.py
@@ -460,8 +460,8 @@ class ImportTestCase(BaseCourseTestCase):
)
module = modulestore.get_instance(course.id, location)
self.assertEqual(len(module.get_children()), 0)
- self.assertEqual(module.num_inputs, '5')
- self.assertEqual(module.num_top_words, '250')
+ self.assertEqual(module.num_inputs, 5)
+ self.assertEqual(module.num_top_words, 250)
def test_cohort_config(self):
"""
diff --git a/common/lib/xmodule/xmodule/tests/test_xml_module.py b/common/lib/xmodule/xmodule/tests/test_xml_module.py
index e41bcdd73a..dd59ca2b48 100644
--- a/common/lib/xmodule/xmodule/tests/test_xml_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_xml_module.py
@@ -1,69 +1,141 @@
+# disable missing docstring
+#pylint: disable=C0111
+
from xmodule.x_module import XModuleFields
-from xblock.core import Scope, String, Object
-from xmodule.fields import Date, StringyInteger
+from xblock.core import Scope, String, Object, Boolean
+from xmodule.fields import Date, StringyInteger, StringyFloat
from xmodule.xml_module import XmlDescriptor
import unittest
-from . import test_system
+from .import test_system
from mock import Mock
+class CrazyJsonString(String):
+ def to_json(self, value):
+ return value + " JSON"
+
+
class TestFields(object):
# Will be returned by editable_metadata_fields.
- max_attempts = StringyInteger(scope=Scope.settings, default=1000)
+ max_attempts = StringyInteger(scope=Scope.settings, default=1000, values={'min': 1, 'max': 10})
# Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields.
due = Date(scope=Scope.settings)
# Will not be returned by editable_metadata_fields because is not Scope.settings.
student_answers = Object(scope=Scope.user_state)
# Will be returned, and can override the inherited value from XModule.
- display_name = String(scope=Scope.settings, default='local default')
+ display_name = String(scope=Scope.settings, default='local default', display_name='Local Display Name',
+ help='local help')
+ # Used for testing select type, effect of to_json method
+ string_select = CrazyJsonString(
+ scope=Scope.settings,
+ default='default value',
+ values=[{'display_name': 'first', 'value': 'value a'},
+ {'display_name': 'second', 'value': 'value b'}]
+ )
+ # Used for testing select type
+ float_select = StringyFloat(scope=Scope.settings, default=.999, values=[1.23, 0.98])
+ # Used for testing float type
+ float_non_select = StringyFloat(scope=Scope.settings, default=.999, values={'min': 0, 'step': .3})
+ # Used for testing that Booleans get mapped to select type
+ boolean_select = Boolean(scope=Scope.settings)
class EditableMetadataFieldsTest(unittest.TestCase):
-
def test_display_name_field(self):
editable_fields = self.get_xml_editable_fields({})
# Tests that the xblock fields (currently tags and name) get filtered out.
# Also tests that xml_attributes is filtered out of XmlDescriptor.
self.assertEqual(1, len(editable_fields), "Expected only 1 editable field for xml descriptor.")
- self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
- explicitly_set=False, inheritable=False, value=None, default_value=None)
+ self.assert_field_values(
+ editable_fields, 'display_name', XModuleFields.display_name,
+ explicitly_set=False, inheritable=False, value=None, default_value=None
+ )
def test_override_default(self):
# Tests that explicitly_set is correct when a value overrides the default (not inheritable).
editable_fields = self.get_xml_editable_fields({'display_name': 'foo'})
- self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
- explicitly_set=True, inheritable=False, value='foo', default_value=None)
+ self.assert_field_values(
+ editable_fields, 'display_name', XModuleFields.display_name,
+ explicitly_set=True, inheritable=False, value='foo', default_value=None
+ )
- def test_additional_field(self):
- descriptor = self.get_descriptor({'max_attempts' : '7'})
+ def test_integer_field(self):
+ descriptor = self.get_descriptor({'max_attempts': '7'})
editable_fields = descriptor.editable_metadata_fields
- self.assertEqual(2, len(editable_fields))
- self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts,
- explicitly_set=True, inheritable=False, value=7, default_value=1000)
- self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
- explicitly_set=False, inheritable=False, value='local default', default_value='local default')
+ self.assertEqual(6, len(editable_fields))
+ self.assert_field_values(
+ editable_fields, 'max_attempts', TestFields.max_attempts,
+ explicitly_set=True, inheritable=False, value=7, default_value=1000, type='Integer',
+ options=TestFields.max_attempts.values
+ )
+ self.assert_field_values(
+ editable_fields, 'display_name', TestFields.display_name,
+ explicitly_set=False, inheritable=False, value='local default', default_value='local default'
+ )
editable_fields = self.get_descriptor({}).editable_metadata_fields
- self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts,
- explicitly_set=False, inheritable=False, value=1000, default_value=1000)
+ self.assert_field_values(
+ editable_fields, 'max_attempts', TestFields.max_attempts,
+ explicitly_set=False, inheritable=False, value=1000, default_value=1000, type='Integer',
+ options=TestFields.max_attempts.values
+ )
def test_inherited_field(self):
- model_val = {'display_name' : 'inherited'}
+ model_val = {'display_name': 'inherited'}
descriptor = self.get_descriptor(model_val)
# Mimic an inherited value for display_name (inherited and inheritable are the same in this case).
descriptor._inherited_metadata = model_val
descriptor._inheritable_metadata = model_val
editable_fields = descriptor.editable_metadata_fields
- self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
- explicitly_set=False, inheritable=True, value='inherited', default_value='inherited')
+ self.assert_field_values(
+ editable_fields, 'display_name', TestFields.display_name,
+ explicitly_set=False, inheritable=True, value='inherited', default_value='inherited'
+ )
- descriptor = self.get_descriptor({'display_name' : 'explicit'})
+ descriptor = self.get_descriptor({'display_name': 'explicit'})
# Mimic the case where display_name WOULD have been inherited, except we explicitly set it.
- descriptor._inheritable_metadata = {'display_name' : 'inheritable value'}
+ descriptor._inheritable_metadata = {'display_name': 'inheritable value'}
descriptor._inherited_metadata = {}
editable_fields = descriptor.editable_metadata_fields
- self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
- explicitly_set=True, inheritable=True, value='explicit', default_value='inheritable value')
+ self.assert_field_values(
+ editable_fields, 'display_name', TestFields.display_name,
+ explicitly_set=True, inheritable=True, value='explicit', default_value='inheritable value'
+ )
+
+ def test_type_and_options(self):
+ # test_display_name_field verifies that a String field is of type "Generic".
+ # test_integer_field verifies that a StringyInteger field is of type "Integer".
+
+ descriptor = self.get_descriptor({})
+ editable_fields = descriptor.editable_metadata_fields
+
+ # Tests for select
+ self.assert_field_values(
+ editable_fields, 'string_select', TestFields.string_select,
+ explicitly_set=False, inheritable=False, value='default value', default_value='default value',
+ type='Select', options=[{'display_name': 'first', 'value': 'value a JSON'},
+ {'display_name': 'second', 'value': 'value b JSON'}]
+ )
+
+ self.assert_field_values(
+ editable_fields, 'float_select', TestFields.float_select,
+ explicitly_set=False, inheritable=False, value=.999, default_value=.999,
+ type='Select', options=[1.23, 0.98]
+ )
+
+ self.assert_field_values(
+ editable_fields, 'boolean_select', TestFields.boolean_select,
+ explicitly_set=False, inheritable=False, value=None, default_value=None,
+ type='Select', options=[{'display_name': "True", "value": True}, {'display_name': "False", "value": False}]
+ )
+
+ # Test for float
+ self.assert_field_values(
+ editable_fields, 'float_non_select', TestFields.float_non_select,
+ explicitly_set=False, inheritable=False, value=.999, default_value=.999,
+ type='Float', options={'min': 0, 'step': .3}
+ )
+
# Start of helper methods
def get_xml_editable_fields(self, model_data):
@@ -73,7 +145,6 @@ class EditableMetadataFieldsTest(unittest.TestCase):
def get_descriptor(self, model_data):
class TestModuleDescriptor(TestFields, XmlDescriptor):
-
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(TestModuleDescriptor, self).non_editable_metadata_fields
@@ -84,10 +155,19 @@ class EditableMetadataFieldsTest(unittest.TestCase):
system.render_template = Mock(return_value="
- Have a course-specific question?
-
- Post it on the course forums.
-
- Have a general question about edX? Check the FAQ. For questions on course lectures, homework, tools, or materials for this course, post in the
+ course discussion forum.
+ Have general questions about edX? You can find lots of helpful information in the edX
+ FAQ. Have a question about something specific? You can contact the edX general
+ support team directly:
-% endif
-