diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 89872937bf..53be1298e6 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -5,6 +5,10 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
+Studio, LMS: Make ModelTypes more strict about their expected content (for
+instance, Boolean, Integer, String), but also allow them to hold either the
+typed value, or a String that can be converted to their typed value. For example,
+an Integer can contain 3 or '3'. This changed an update to the xblock library.
LMS: Some errors handling Non-ASCII data in XML courses have been fixed.
diff --git a/cms/djangoapps/contentstore/features/advanced-settings.feature b/cms/djangoapps/contentstore/features/advanced-settings.feature
index 558294e890..13600f2086 100644
--- a/cms/djangoapps/contentstore/features/advanced-settings.feature
+++ b/cms/djangoapps/contentstore/features/advanced-settings.feature
@@ -28,11 +28,18 @@ Feature: Advanced (manual) course policy
Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio
- When I create a JSON object as a value
+ When I create a JSON object as a value for "discussion_topics"
Then it is displayed as formatted
And I reload the page
Then it is displayed as formatted
+ Scenario: Test error if value supplied is of the wrong type
+ Given I am on the Advanced Course Settings page in Studio
+ When I create a JSON object as a value for "display_name"
+ Then I get an error on save
+ And I reload the page
+ Then the policy key value is unchanged
+
Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes
diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py
index eb00c06ba9..049103db27 100644
--- a/cms/djangoapps/contentstore/features/advanced-settings.py
+++ b/cms/djangoapps/contentstore/features/advanced-settings.py
@@ -2,8 +2,7 @@
#pylint: disable=W0621
from lettuce import world, step
-from common import *
-from nose.tools import assert_false, assert_equal
+from nose.tools import assert_false, assert_equal, assert_regexp_matches
"""
http://selenium.googlecode.com/svn/trunk/docs/api/py/webdriver/selenium.webdriver.common.keys.html
@@ -52,9 +51,9 @@ def edit_the_value_of_a_policy_key_and_save(step):
change_display_name_value(step, '"foo"')
-@step('I create a JSON object as a value$')
-def create_JSON_object(step):
- change_display_name_value(step, '{"key": "value", "key_2": "value_2"}')
+@step('I create a JSON object as a value for "(.*)"$')
+def create_JSON_object(step, key):
+ change_value(step, key, '{"key": "value", "key_2": "value_2"}')
@step('I create a non-JSON value not in quotes$')
@@ -82,7 +81,12 @@ def they_are_alphabetized(step):
@step('it is displayed as formatted$')
def it_is_formatted(step):
- assert_policy_entries([DISPLAY_NAME_KEY], ['{\n "key": "value",\n "key_2": "value_2"\n}'])
+ assert_policy_entries(['discussion_topics'], ['{\n "key": "value",\n "key_2": "value_2"\n}'])
+
+
+@step('I get an error on save$')
+def error_on_save(step):
+ assert_regexp_matches(world.css_text('#notification-error-description'), 'Incorrect setting format')
@step('it is displayed as a string')
@@ -124,11 +128,16 @@ def get_display_name_value():
def change_display_name_value(step, new_value):
+ change_value(step, DISPLAY_NAME_KEY, new_value)
- world.css_find(".CodeMirror")[get_index_of(DISPLAY_NAME_KEY)].click()
+
+def change_value(step, key, new_value):
+ index = get_index_of(key)
+ world.css_find(".CodeMirror")[index].click()
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
- display_name = get_display_name_value()
- for count in range(len(display_name)):
+ current_value = world.css_find(VALUE_CSS)[index].value
+ g._element.send_keys(Keys.CONTROL + Keys.END)
+ for count in range(len(current_value)):
g._element.send_keys(Keys.END, Keys.BACK_SPACE)
# Must delete "" before typing the JSON value
g._element.send_keys(Keys.END, Keys.BACK_SPACE, Keys.BACK_SPACE, new_value)
diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py
index 5dfcf55046..7679128beb 100644
--- a/cms/djangoapps/contentstore/features/problem-editor.py
+++ b/cms/djangoapps/contentstore/features/problem-editor.py
@@ -41,7 +41,9 @@ def i_see_five_settings_with_values(step):
@step('I can modify the display name')
def i_can_modify_the_display_name(step):
- world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill('modified')
+ # Verifying that the display name can be a string containing a floating point value
+ # (to confirm that we don't throw an error because it is of the wrong type).
+ world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill('3.4')
verify_modified_display_name()
@@ -172,7 +174,7 @@ def verify_modified_randomization():
def verify_modified_display_name():
- world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'modified', True)
+ world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '3.4', True)
def verify_modified_display_name_with_special_chars():
diff --git a/cms/djangoapps/contentstore/tests/test_checklists.py b/cms/djangoapps/contentstore/tests/test_checklists.py
index f0889b0861..54bc726092 100644
--- a/cms/djangoapps/contentstore/tests/test_checklists.py
+++ b/cms/djangoapps/contentstore/tests/test_checklists.py
@@ -19,6 +19,24 @@ class ChecklistTestCase(CourseTestCase):
modulestore = get_modulestore(self.course.location)
return modulestore.get_item(self.course.location).checklists
+
+ def compare_checklists(self, persisted, request):
+ """
+ Handles url expansion as possible difference and descends into guts
+ :param persisted:
+ :param request:
+ """
+ self.assertEqual(persisted['short_description'], request['short_description'])
+ compare_urls = (persisted.get('action_urls_expanded') == request.get('action_urls_expanded'))
+ for pers, req in zip(persisted['items'], request['items']):
+ self.assertEqual(pers['short_description'], req['short_description'])
+ self.assertEqual(pers['long_description'], req['long_description'])
+ self.assertEqual(pers['is_checked'], req['is_checked'])
+ if compare_urls:
+ self.assertEqual(pers['action_url'], req['action_url'])
+ self.assertEqual(pers['action_text'], req['action_text'])
+ self.assertEqual(pers['action_external'], req['action_external'])
+
def test_get_checklists(self):
""" Tests the get checklists method. """
checklists_url = get_url_reverse('Checklists', self.course)
@@ -31,9 +49,9 @@ class ChecklistTestCase(CourseTestCase):
self.course.checklists = None
modulestore = get_modulestore(self.course.location)
modulestore.update_metadata(self.course.location, own_metadata(self.course))
- self.assertEquals(self.get_persisted_checklists(), None)
+ self.assertEqual(self.get_persisted_checklists(), None)
response = self.client.get(checklists_url)
- self.assertEquals(payload, response.content)
+ self.assertEqual(payload, response.content)
def test_update_checklists_no_index(self):
""" No checklist index, should return all of them. """
@@ -43,7 +61,8 @@ class ChecklistTestCase(CourseTestCase):
'name': self.course.location.name})
returned_checklists = json.loads(self.client.get(update_url).content)
- self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
+ for pay, resp in zip(self.get_persisted_checklists(), returned_checklists):
+ self.compare_checklists(pay, resp)
def test_update_checklists_index_ignored_on_get(self):
""" Checklist index ignored on get. """
@@ -53,7 +72,8 @@ class ChecklistTestCase(CourseTestCase):
'checklist_index': 1})
returned_checklists = json.loads(self.client.get(update_url).content)
- self.assertListEqual(self.get_persisted_checklists(), returned_checklists)
+ for pay, resp in zip(self.get_persisted_checklists(), returned_checklists):
+ self.compare_checklists(pay, resp)
def test_update_checklists_post_no_index(self):
""" No checklist index, will error on post. """
@@ -78,13 +98,18 @@ class ChecklistTestCase(CourseTestCase):
'course': self.course.location.course,
'name': self.course.location.name,
'checklist_index': 2})
+
+ def get_first_item(checklist):
+ return checklist['items'][0]
+
payload = self.course.checklists[2]
- self.assertFalse(payload.get('is_checked'))
- payload['is_checked'] = True
+ self.assertFalse(get_first_item(payload).get('is_checked'))
+ get_first_item(payload)['is_checked'] = True
returned_checklist = json.loads(self.client.post(update_url, json.dumps(payload), "application/json").content)
- self.assertTrue(returned_checklist.get('is_checked'))
- self.assertEqual(self.get_persisted_checklists()[2], returned_checklist)
+ self.assertTrue(get_first_item(returned_checklist).get('is_checked'))
+ pers = self.get_persisted_checklists()
+ self.compare_checklists(pers[2], returned_checklist)
def test_update_checklists_delete_unsupported(self):
""" Delete operation is not supported. """
@@ -93,4 +118,4 @@ class ChecklistTestCase(CourseTestCase):
'name': self.course.location.name,
'checklist_index': 100})
response = self.client.delete(update_url)
- self.assertContains(response, 'Unsupported request', status_code=400)
\ No newline at end of file
+ self.assertContains(response, 'Unsupported request', status_code=400)
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index e1c176eebe..8762eb3a2a 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -402,8 +402,11 @@ def course_advanced_updates(request, org, course, name):
request_body.update({'tabs': new_tabs})
# Indicate that tabs should *not* be filtered out of the metadata
filter_tabs = False
-
- response_json = json.dumps(CourseMetadata.update_from_json(location,
+ try:
+ response_json = json.dumps(CourseMetadata.update_from_json(location,
request_body,
filter_tabs=filter_tabs))
+ except (TypeError, ValueError), e:
+ return HttpResponseBadRequest("Incorrect setting format. " + str(e), content_type="text/plain")
+
return HttpResponse(response_json, mimetype="application/json")
diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py
index 4857fe68ca..eef4b41f37 100644
--- a/cms/xmodule_namespace.py
+++ b/cms/xmodule_namespace.py
@@ -5,7 +5,6 @@ Namespace defining common fields used by Studio for all blocks
import datetime
from xblock.core import Namespace, Scope, ModelType, String
-from xmodule.fields import StringyBoolean
class DateTuple(ModelType):
@@ -28,4 +27,3 @@ class CmsNamespace(Namespace):
"""
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
published_by = String(help="Id of the user who published this module", scope=Scope.settings)
-
diff --git a/common/lib/xmodule/xmodule/abtest_module.py b/common/lib/xmodule/xmodule/abtest_module.py
index 196154df78..2e61076e94 100644
--- a/common/lib/xmodule/xmodule/abtest_module.py
+++ b/common/lib/xmodule/xmodule/abtest_module.py
@@ -6,7 +6,7 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError
-from xblock.core import String, Scope, Object
+from xblock.core import String, Scope, Dict
DEFAULT = "_DEFAULT_GROUP"
@@ -32,9 +32,9 @@ def group_from_value(groups, v):
class ABTestFields(object):
- group_portions = Object(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content)
- group_assignments = Object(help="What group this user belongs to", scope=Scope.preferences, default={})
- group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
+ group_portions = Dict(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content)
+ group_assignments = Dict(help="What group this user belongs to", scope=Scope.preferences, default={})
+ group_content = Dict(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
has_children = True
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index e8a8dfd94a..ca10d4d2a0 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -18,8 +18,8 @@ from .progress import Progress
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError
-from xblock.core import Scope, String, Boolean, Object
-from .fields import Timedelta, Date, StringyInteger, StringyFloat
+from xblock.core import Scope, String, Boolean, Dict, Integer, Float
+from .fields import Timedelta, Date
from django.utils.timezone import UTC
log = logging.getLogger("mitx.courseware")
@@ -65,8 +65,8 @@ 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(
+ attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state)
+ max_attempts = Integer(
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": 0}, scope=Scope.settings
@@ -95,12 +95,12 @@ class CapaFields(object):
{"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)
+ correct_map = Dict(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
+ input_state = Dict(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
+ student_answers = Dict(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(
+ seed = Integer(help="Random seed for this student", scope=Scope.user_state)
+ weight = Float(
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},
@@ -315,7 +315,7 @@ class CapaModule(CapaFields, XModule):
# If the user has forced the save button to display,
# then show it as long as the problem is not closed
# (past due / too many attempts)
- if self.force_save_button == "true":
+ if self.force_save_button:
return not self.closed()
else:
is_survey_question = (self.max_attempts == 0)
diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py
index 07c0dc3e80..dc753a64b8 100644
--- a/common/lib/xmodule/xmodule/combined_open_ended_module.py
+++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py
@@ -5,10 +5,10 @@ from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor
from .x_module import XModule
-from xblock.core import Integer, Scope, String, List
+from xblock.core import Integer, Scope, String, List, Float, Boolean
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple
-from .fields import Date, StringyFloat, StringyInteger, StringyBoolean
+from .fields import Date
log = logging.getLogger("mitx.courseware")
@@ -53,27 +53,27 @@ class CombinedOpenEndedFields(object):
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)
+ current_task_number = Integer(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,
+ student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0,
scope=Scope.user_state)
- ready_to_reset = StringyBoolean(
+ ready_to_reset = Boolean(
help="If the problem is ready to be reset or not.", default=False,
scope=Scope.user_state
)
- attempts = StringyInteger(
+ attempts = Integer(
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(
+ is_graded = Boolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
+ accept_file_upload = Boolean(
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(
+ skip_spelling_checks = Boolean(
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
@@ -86,7 +86,7 @@ class CombinedOpenEndedFields(object):
)
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(
+ weight = Float(
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"}
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 03cfd3d77b..d0333cbe36 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -15,7 +15,7 @@ from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf
import json
-from xblock.core import Scope, List, String, Object, Boolean
+from xblock.core import Scope, List, String, Dict, Boolean
from .fields import Date
from django.utils.timezone import UTC
from xmodule.util import date_utils
@@ -154,25 +154,25 @@ class CourseFields(object):
start = Date(help="Start time when this module is visible", scope=Scope.settings)
end = Date(help="Date that this class ends", scope=Scope.settings)
advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings)
- grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content)
+ grading_policy = Dict(help="Grading policy definition for this class", scope=Scope.content)
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
display_name = String(help="Display name for this module", scope=Scope.settings)
tabs = List(help="List of tabs to enable in this course", scope=Scope.settings)
end_of_course_survey_url = String(help="Url for the end-of-course survey", scope=Scope.settings)
discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings)
- discussion_topics = Object(
+ discussion_topics = Dict(
help="Map of topics names to ids",
scope=Scope.settings
)
- testcenter_info = Object(help="Dictionary of Test Center info", scope=Scope.settings)
+ testcenter_info = Dict(help="Dictionary of Test Center info", scope=Scope.settings)
announcement = Date(help="Date this course is announced", scope=Scope.settings)
- cohort_config = Object(help="Dictionary defining cohort configuration", scope=Scope.settings)
+ cohort_config = Dict(help="Dictionary defining cohort configuration", scope=Scope.settings)
is_new = Boolean(help="Whether this course should be flagged as new", scope=Scope.settings)
no_grade = Boolean(help="True if this course isn't graded", default=False, scope=Scope.settings)
disable_progress_graph = Boolean(help="True if this course shouldn't display the progress graph", default=False, scope=Scope.settings)
pdf_textbooks = List(help="List of dictionaries containing pdf_textbook configuration", scope=Scope.settings)
html_textbooks = List(help="List of dictionaries containing html_textbook configuration", scope=Scope.settings)
- remote_gradebook = Object(scope=Scope.settings)
+ remote_gradebook = Dict(scope=Scope.settings)
allow_anonymous = Boolean(scope=Scope.settings, default=True)
allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False)
advanced_modules = List(help="Beta modules used in your course", scope=Scope.settings)
diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py
index 8e0294b74a..963b70204e 100644
--- a/common/lib/xmodule/xmodule/fields.py
+++ b/common/lib/xmodule/xmodule/fields.py
@@ -6,7 +6,6 @@ from xblock.core import ModelType
import datetime
import dateutil.parser
-from xblock.core import Integer, Float, Boolean
from django.utils.timezone import UTC
log = logging.getLogger(__name__)
@@ -93,42 +92,3 @@ class Timedelta(ModelType):
if cur_value > 0:
values.append("%d %s" % (cur_value, attr))
return ' '.join(values)
-
-
-class StringyInteger(Integer):
- """
- A model type that converts from strings to integers when reading from json.
- If value does not parse as an int, returns None.
- """
- def from_json(self, value):
- try:
- return int(value)
- except Exception:
- return None
-
-
-class StringyFloat(Float):
- """
- A model type that converts from string to floats when reading from json.
- If value does not parse as a float, returns None.
- """
- def from_json(self, value):
- try:
- return float(value)
- except:
- return None
-
-
-class StringyBoolean(Boolean):
- """
- Reads strings from JSON as booleans.
-
- If the string is 'true' (case insensitive), then return True,
- otherwise False.
-
- JSON values that aren't strings are returned as-is.
- """
- def from_json(self, value):
- if isinstance(value, basestring):
- return value.lower() == 'true'
- return value
diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py
index c6af60647a..979bc95cb0 100644
--- a/common/lib/xmodule/xmodule/peer_grading_module.py
+++ b/common/lib/xmodule/xmodule/peer_grading_module.py
@@ -10,8 +10,8 @@ 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, String, Scope
-from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
+from xblock.core import Dict, String, Scope, Boolean, Integer, Float
+from xmodule.fields import Date
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
from open_ended_grading_classes import combined_open_ended_rubric
@@ -21,7 +21,6 @@ log = logging.getLogger(__name__)
USE_FOR_SINGLE_LOCATION = False
LINK_TO_LOCATION = ""
-TRUE_DICT = [True, "True", "true", "TRUE"]
MAX_SCORE = 1
IS_GRADED = False
@@ -29,7 +28,7 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please
class PeerGradingFields(object):
- use_for_single_location = StringyBoolean(
+ use_for_single_location = Boolean(
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.',
@@ -40,22 +39,22 @@ class PeerGradingFields(object):
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(
+ is_graded = Boolean(
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(
+ max_grade = Integer(
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(
+ student_data_for_location = Dict(
help="Student data for a given peer grading problem.",
scope=Scope.user_state
)
- weight = StringyFloat(
+ weight = Float(
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"}
@@ -85,7 +84,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
else:
self.peer_gs = MockPeerGradingService()
- if self.use_for_single_location in TRUE_DICT:
+ if self.use_for_single_location:
try:
self.linked_problem = modulestore().get_instance(self.system.course_id, self.link_to_location)
except:
@@ -113,7 +112,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
if not self.ajax_url.endswith("/"):
self.ajax_url = self.ajax_url + "/"
- # StringyInteger could return None, so keep this check.
+ # Integer could return None, so keep this check.
if not isinstance(self.max_grade, int):
raise TypeError("max_grade needs to be an integer.")
@@ -147,7 +146,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
"""
if self.closed():
return self.peer_grading_closed()
- if self.use_for_single_location not in TRUE_DICT:
+ if not self.use_for_single_location:
return self.peer_grading()
else:
return self.peer_grading_problem({'location': self.link_to_location})['html']
@@ -204,7 +203,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
'score': score,
'total': max_score,
}
- if self.use_for_single_location not in TRUE_DICT or self.is_graded not in TRUE_DICT:
+ if not self.use_for_single_location or not self.is_graded:
return score_dict
try:
@@ -239,7 +238,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
randomization, and 5/7 on another
'''
max_grade = None
- if self.use_for_single_location in TRUE_DICT and self.is_graded in TRUE_DICT:
+ if self.use_for_single_location and self.is_graded:
max_grade = self.max_grade
return max_grade
@@ -557,7 +556,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
Show individual problem interface
'''
if get is None or get.get('location') is None:
- if self.use_for_single_location not in TRUE_DICT:
+ if not self.use_for_single_location:
# This is an error case, because it must be set to use a single location to be called without get parameters
# This is a dev_facing_error
log.error(
diff --git a/common/lib/xmodule/xmodule/poll_module.py b/common/lib/xmodule/xmodule/poll_module.py
index 6b6dd54e68..9f2359865a 100644
--- a/common/lib/xmodule/xmodule/poll_module.py
+++ b/common/lib/xmodule/xmodule/poll_module.py
@@ -19,7 +19,7 @@ from xmodule.x_module import XModule
from xmodule.stringify import stringify_children
from xmodule.mako_module import MakoModuleDescriptor
from xmodule.xml_module import XmlDescriptor
-from xblock.core import Scope, String, Object, Boolean, List
+from xblock.core import Scope, String, Dict, Boolean, List
log = logging.getLogger(__name__)
@@ -30,7 +30,7 @@ class PollFields(object):
voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.user_state, default=False)
poll_answer = String(help="Student answer", scope=Scope.user_state, default='')
- poll_answers = Object(help="All possible answers for the poll fro other students", scope=Scope.content)
+ poll_answers = Dict(help="All possible answers for the poll fro other students", scope=Scope.content)
answers = List(help="Poll answers from xml", scope=Scope.content, default=[])
question = String(help="Poll question", scope=Scope.content, default='')
diff --git a/common/lib/xmodule/xmodule/tests/test_fields.py b/common/lib/xmodule/xmodule/tests/test_fields.py
index 1b6c86c000..884f218d6d 100644
--- a/common/lib/xmodule/xmodule/tests/test_fields.py
+++ b/common/lib/xmodule/xmodule/tests/test_fields.py
@@ -1,8 +1,9 @@
"""Tests for classes defined in fields.py."""
import datetime
import unittest
-from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
from django.utils.timezone import UTC
+from xmodule.fields import Date, Timedelta
+
class DateTest(unittest.TestCase):
date = Date()
@@ -70,54 +71,22 @@ class DateTest(unittest.TestCase):
"2012-12-31T23:00:01-01:00")
-class StringyIntegerTest(unittest.TestCase):
- def assertEquals(self, expected, arg):
- self.assertEqual(expected, StringyInteger().from_json(arg))
+class TimedeltaTest(unittest.TestCase):
+ delta = Timedelta()
- def test_integer(self):
- self.assertEquals(5, '5')
- self.assertEquals(0, '0')
- self.assertEquals(-1023, '-1023')
+ def test_from_json(self):
+ self.assertEqual(
+ TimedeltaTest.delta.from_json('1 day 12 hours 59 minutes 59 seconds'),
+ datetime.timedelta(days=1, hours=12, minutes=59, seconds=59)
+ )
- def test_none(self):
- self.assertEquals(None, None)
- self.assertEquals(None, 'abc')
- self.assertEquals(None, '[1]')
- self.assertEquals(None, '1.023')
-
-
-class StringyFloatTest(unittest.TestCase):
-
- def assertEquals(self, expected, arg):
- self.assertEqual(expected, StringyFloat().from_json(arg))
-
- def test_float(self):
- self.assertEquals(.23, '.23')
- self.assertEquals(5, '5')
- self.assertEquals(0, '0.0')
- self.assertEquals(-1023.22, '-1023.22')
-
- def test_none(self):
- self.assertEquals(None, None)
- self.assertEquals(None, 'abc')
- self.assertEquals(None, '[1]')
-
-
-class StringyBooleanTest(unittest.TestCase):
-
- def assertEquals(self, expected, arg):
- self.assertEqual(expected, StringyBoolean().from_json(arg))
-
- def test_false(self):
- self.assertEquals(False, "false")
- self.assertEquals(False, "False")
- self.assertEquals(False, "")
- self.assertEquals(False, "hahahahah")
-
- def test_true(self):
- self.assertEquals(True, "true")
- self.assertEquals(True, "TruE")
-
- def test_pass_through(self):
- self.assertEquals(123, 123)
+ self.assertEqual(
+ TimedeltaTest.delta.from_json('1 day 46799 seconds'),
+ datetime.timedelta(days=1, seconds=46799)
+ )
+ def test_to_json(self):
+ self.assertEqual(
+ '1 days 46799 seconds',
+ TimedeltaTest.delta.to_json(datetime.timedelta(days=1, hours=12, minutes=59, seconds=59))
+ )
diff --git a/common/lib/xmodule/xmodule/tests/test_xml_module.py b/common/lib/xmodule/xmodule/tests/test_xml_module.py
index dd59ca2b48..eb715b44bc 100644
--- a/common/lib/xmodule/xmodule/tests/test_xml_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_xml_module.py
@@ -2,11 +2,12 @@
#pylint: disable=C0111
from xmodule.x_module import XModuleFields
-from xblock.core import Scope, String, Object, Boolean
-from xmodule.fields import Date, StringyInteger, StringyFloat
-from xmodule.xml_module import XmlDescriptor
+from xblock.core import Scope, String, Dict, Boolean, Integer, Float, Any, List
+from xmodule.fields import Date, Timedelta
+from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field
import unittest
from .import test_system
+from nose.tools import assert_equals
from mock import Mock
@@ -17,11 +18,11 @@ class CrazyJsonString(String):
class TestFields(object):
# Will be returned by editable_metadata_fields.
- max_attempts = StringyInteger(scope=Scope.settings, default=1000, values={'min': 1, 'max': 10})
+ max_attempts = Integer(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)
+ student_answers = Dict(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='Local Display Name',
help='local help')
@@ -33,9 +34,9 @@ class TestFields(object):
{'display_name': 'second', 'value': 'value b'}]
)
# Used for testing select type
- float_select = StringyFloat(scope=Scope.settings, default=.999, values=[1.23, 0.98])
+ float_select = Float(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})
+ float_non_select = Float(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)
@@ -104,7 +105,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
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".
+ # test_integer_field verifies that a Integer field is of type "Integer".
descriptor = self.get_descriptor({})
editable_fields = descriptor.editable_metadata_fields
@@ -171,3 +172,194 @@ class EditableMetadataFieldsTest(unittest.TestCase):
self.assertEqual(explicitly_set, test_field['explicitly_set'])
self.assertEqual(inheritable, test_field['inheritable'])
+
+
+class TestSerialize(unittest.TestCase):
+ """ Tests the serialize, method, which is not dependent on type. """
+ def test_serialize(self):
+ assert_equals('null', serialize_field(None))
+ assert_equals('-2', serialize_field(-2))
+ assert_equals('"2"', serialize_field('2'))
+ assert_equals('-3.41', serialize_field(-3.41))
+ assert_equals('"2.589"', serialize_field('2.589'))
+ assert_equals('false', serialize_field(False))
+ assert_equals('"false"', serialize_field('false'))
+ assert_equals('"fAlse"', serialize_field('fAlse'))
+ assert_equals('"hat box"', serialize_field('hat box'))
+ assert_equals('{"bar": "hat", "frog": "green"}', serialize_field({'bar': 'hat', 'frog' : 'green'}))
+ assert_equals('[3.5, 5.6]', serialize_field([3.5, 5.6]))
+ assert_equals('["foo", "bar"]', serialize_field(['foo', 'bar']))
+ assert_equals('"2012-12-31T23:59:59Z"', serialize_field("2012-12-31T23:59:59Z"))
+ assert_equals('"1 day 12 hours 59 minutes 59 seconds"',
+ serialize_field("1 day 12 hours 59 minutes 59 seconds"))
+
+
+class TestDeserialize(unittest.TestCase):
+ def assertDeserializeEqual(self, expected, arg):
+ """
+ Asserts the result of deserialize_field.
+ """
+ assert_equals(expected, deserialize_field(self.test_field(), arg))
+
+
+ def assertDeserializeNonString(self):
+ """
+ Asserts input value is returned for None or something that is not a string.
+ For all types, 'null' is also always returned as None.
+ """
+ self.assertDeserializeEqual(None, None)
+ self.assertDeserializeEqual(3.14, 3.14)
+ self.assertDeserializeEqual(True, True)
+ self.assertDeserializeEqual([10], [10])
+ self.assertDeserializeEqual({}, {})
+ self.assertDeserializeEqual([], [])
+ self.assertDeserializeEqual(None, 'null')
+
+
+class TestDeserializeInteger(TestDeserialize):
+ """ Tests deserialize as related to Integer type. """
+
+ test_field = Integer
+
+ def test_deserialize(self):
+ self.assertDeserializeEqual(-2, '-2')
+ self.assertDeserializeEqual("450", '"450"')
+
+ # False can be parsed as a int (converts to 0)
+ self.assertDeserializeEqual(False, 'false')
+ # True can be parsed as a int (converts to 1)
+ self.assertDeserializeEqual(True, 'true')
+ # 2.78 can be converted to int, so the string will be deserialized
+ self.assertDeserializeEqual(-2.78, '-2.78')
+
+
+ def test_deserialize_unsupported_types(self):
+ self.assertDeserializeEqual('[3]', '[3]')
+ # '2.78' cannot be converted to int, so input value is returned
+ self.assertDeserializeEqual('"-2.78"', '"-2.78"')
+ # 'false' cannot be converted to int, so input value is returned
+ self.assertDeserializeEqual('"false"', '"false"')
+ self.assertDeserializeNonString()
+
+
+class TestDeserializeFloat(TestDeserialize):
+ """ Tests deserialize as related to Float type. """
+
+ test_field = Float
+
+ def test_deserialize(self):
+ self.assertDeserializeEqual( -2, '-2')
+ self.assertDeserializeEqual("450", '"450"')
+ self.assertDeserializeEqual(-2.78, '-2.78')
+ self.assertDeserializeEqual("0.45", '"0.45"')
+
+ # False can be parsed as a float (converts to 0)
+ self.assertDeserializeEqual(False, 'false')
+ # True can be parsed as a float (converts to 1)
+ self.assertDeserializeEqual( True, 'true')
+
+ def test_deserialize_unsupported_types(self):
+ self.assertDeserializeEqual('[3]', '[3]')
+ # 'false' cannot be converted to float, so input value is returned
+ self.assertDeserializeEqual('"false"', '"false"')
+ self.assertDeserializeNonString()
+
+
+class TestDeserializeBoolean(TestDeserialize):
+ """ Tests deserialize as related to Boolean type. """
+
+ test_field = Boolean
+
+ def test_deserialize(self):
+ # json.loads converts the value to Python bool
+ self.assertDeserializeEqual(False, 'false')
+ self.assertDeserializeEqual(True, 'true')
+
+ # json.loads fails, string value is returned.
+ self.assertDeserializeEqual('False', 'False')
+ self.assertDeserializeEqual('True', 'True')
+
+ # json.loads deserializes as a string
+ self.assertDeserializeEqual('false', '"false"')
+ self.assertDeserializeEqual('fAlse', '"fAlse"')
+ self.assertDeserializeEqual("TruE", '"TruE"')
+
+ # 2.78 can be converted to a bool, so the string will be deserialized
+ self.assertDeserializeEqual(-2.78, '-2.78')
+
+ self.assertDeserializeNonString()
+
+
+class TestDeserializeString(TestDeserialize):
+ """ Tests deserialize as related to String type. """
+
+ test_field = String
+
+ def test_deserialize(self):
+ self.assertDeserializeEqual('hAlf', '"hAlf"')
+ self.assertDeserializeEqual('false', '"false"')
+ self.assertDeserializeEqual('single quote', 'single quote')
+
+ def test_deserialize_unsupported_types(self):
+ self.assertDeserializeEqual('3.4', '3.4')
+ self.assertDeserializeEqual('false', 'false')
+ self.assertDeserializeEqual('2', '2')
+ self.assertDeserializeEqual('[3]', '[3]')
+ self.assertDeserializeNonString()
+
+
+class TestDeserializeAny(TestDeserialize):
+ """ Tests deserialize as related to Any type. """
+
+ test_field = Any
+
+ def test_deserialize(self):
+ self.assertDeserializeEqual('hAlf', '"hAlf"')
+ self.assertDeserializeEqual('false', '"false"')
+ self.assertDeserializeEqual({'bar': 'hat', 'frog' : 'green'}, '{"bar": "hat", "frog": "green"}')
+ self.assertDeserializeEqual([3.5, 5.6], '[3.5, 5.6]')
+ self.assertDeserializeEqual('[', '[')
+ self.assertDeserializeEqual(False, 'false')
+ self.assertDeserializeEqual(3.4, '3.4')
+ self.assertDeserializeNonString()
+
+
+class TestDeserializeList(TestDeserialize):
+ """ Tests deserialize as related to List type. """
+
+ test_field = List
+
+ def test_deserialize(self):
+ self.assertDeserializeEqual(['foo', 'bar'], '["foo", "bar"]')
+ self.assertDeserializeEqual([3.5, 5.6], '[3.5, 5.6]')
+ self.assertDeserializeEqual([], '[]')
+
+ def test_deserialize_unsupported_types(self):
+ self.assertDeserializeEqual('3.4', '3.4')
+ self.assertDeserializeEqual('false', 'false')
+ self.assertDeserializeEqual('2', '2')
+ self.assertDeserializeNonString()
+
+
+class TestDeserializeDate(TestDeserialize):
+ """ Tests deserialize as related to Date type. """
+
+ test_field = Date
+
+ def test_deserialize(self):
+ self.assertDeserializeEqual('2012-12-31T23:59:59Z', "2012-12-31T23:59:59Z")
+ self.assertDeserializeEqual('2012-12-31T23:59:59Z', '"2012-12-31T23:59:59Z"')
+ self.assertDeserializeNonString()
+
+
+class TestDeserializeTimedelta(TestDeserialize):
+ """ Tests deserialize as related to Timedelta type. """
+
+ test_field = Timedelta
+
+ def test_deserialize(self):
+ self.assertDeserializeEqual('1 day 12 hours 59 minutes 59 seconds',
+ '1 day 12 hours 59 minutes 59 seconds')
+ self.assertDeserializeEqual('1 day 12 hours 59 minutes 59 seconds',
+ '"1 day 12 hours 59 minutes 59 seconds"')
+ self.assertDeserializeNonString()
diff --git a/common/lib/xmodule/xmodule/word_cloud_module.py b/common/lib/xmodule/xmodule/word_cloud_module.py
index 1ec5e3adfa..ac5b3051de 100644
--- a/common/lib/xmodule/xmodule/word_cloud_module.py
+++ b/common/lib/xmodule/xmodule/word_cloud_module.py
@@ -14,8 +14,7 @@ from xmodule.raw_module import RawDescriptor
from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xmodule.x_module import XModule
-from xblock.core import Scope, Object, Boolean, List
-from fields import StringyBoolean, StringyInteger
+from xblock.core import Scope, Dict, Boolean, List, Integer
log = logging.getLogger(__name__)
@@ -32,21 +31,21 @@ def pretty_bool(value):
class WordCloudFields(object):
"""XFields for word cloud."""
- num_inputs = StringyInteger(
+ num_inputs = Integer(
display_name="Inputs",
help="Number of text boxes available for students to input words/sentences.",
scope=Scope.settings,
default=5,
values={"min": 1}
)
- num_top_words = StringyInteger(
+ num_top_words = Integer(
display_name="Maximum Words",
help="Maximum number of words to be displayed in generated word cloud.",
scope=Scope.settings,
default=250,
values={"min": 1}
)
- display_student_percents = StringyBoolean(
+ display_student_percents = Boolean(
display_name="Show Percents",
help="Statistics are shown for entered words near that word.",
scope=Scope.settings,
@@ -64,11 +63,11 @@ class WordCloudFields(object):
scope=Scope.user_state,
default=[]
)
- all_words = Object(
+ all_words = Dict(
help="All possible words from all students.",
scope=Scope.content
)
- top_words = Object(
+ top_words = Dict(
help="Top num_top_words words for word cloud.",
scope=Scope.content
)
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index 2f54bbf405..9b5de9d7e7 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -6,7 +6,7 @@ import sys
from collections import namedtuple
from lxml import etree
-from xblock.core import Object, Scope
+from xblock.core import Dict, Scope
from xmodule.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location
from xmodule.modulestore.inheritance import own_metadata
@@ -79,12 +79,53 @@ class AttrMap(_AttrMapBase):
return _AttrMapBase.__new__(_cls, from_xml, to_xml)
+def serialize_field(value):
+ """
+ Return a string version of the value (where value is the JSON-formatted, internally stored value).
+
+ By default, this is the result of calling json.dumps on the input value.
+ """
+ return json.dumps(value)
+
+
+def deserialize_field(field, value):
+ """
+ Deserialize the string version to the value stored internally.
+
+ Note that this is not the same as the value returned by from_json, as model types typically store
+ their value internally as JSON. By default, this method will return the result of calling json.loads
+ on the supplied value, unless json.loads throws a TypeError, or the type of the value returned by json.loads
+ is not supported for this class (from_json throws an Error). In either of those cases, this method returns
+ the input value.
+ """
+ try:
+ deserialized = json.loads(value)
+ if deserialized is None:
+ return deserialized
+ try:
+ field.from_json(deserialized)
+ return deserialized
+ except (ValueError, TypeError):
+ # Support older serialized version, which was just a string, not result of json.dumps.
+ # If the deserialized version cannot be converted to the type (via from_json),
+ # just return the original value. For example, if a string value of '3.4' was
+ # stored for a String field (before we started storing the result of json.dumps),
+ # then it would be deserialized as 3.4, but 3.4 is not supported for a String
+ # field. Therefore field.from_json(3.4) will throw an Error, and we should
+ # actually return the original value of '3.4'.
+ return value
+
+ except (ValueError, TypeError):
+ # Support older serialized version.
+ return value
+
+
class XmlDescriptor(XModuleDescriptor):
"""
Mixin class for standardized parsing of from xml
"""
- xml_attributes = Object(help="Map of unhandled xml attributes, used only for storage between import and export",
+ xml_attributes = Dict(help="Map of unhandled xml attributes, used only for storage between import and export",
default={}, scope=Scope.settings)
# Extension to append to filename paths
@@ -120,25 +161,15 @@ class XmlDescriptor(XModuleDescriptor):
metadata_to_export_to_policy = ('discussion_topics')
- # A dictionary mapping xml attribute names AttrMaps that describe how
- # to import and export them
- # Allow json to specify either the string "true", or the bool True. The string is preferred.
- to_bool = lambda val: val == 'true' or val == True
- from_bool = lambda val: str(val).lower()
- bool_map = AttrMap(to_bool, from_bool)
-
- to_int = lambda val: int(val)
- from_int = lambda val: str(val)
- int_map = AttrMap(to_int, from_int)
- xml_attribute_map = {
- # type conversion: want True/False in python, "true"/"false" in xml
- 'graded': bool_map,
- 'hide_progress_tab': bool_map,
- 'allow_anonymous': bool_map,
- 'allow_anonymous_to_peers': bool_map,
- 'show_timezone': bool_map,
- }
+ @classmethod
+ def get_map_for_field(cls, attr):
+ for field in set(cls.fields + cls.lms.fields):
+ if field.name == attr:
+ from_xml = lambda val: deserialize_field(field, val)
+ to_xml = lambda val : serialize_field(val)
+ return AttrMap(from_xml, to_xml)
+ return AttrMap()
@classmethod
def definition_from_xml(cls, xml_object, system):
@@ -188,7 +219,6 @@ class XmlDescriptor(XModuleDescriptor):
filepath, location.url(), str(err))
raise Exception, msg, sys.exc_info()[2]
-
@classmethod
def load_definition(cls, xml_object, system, location):
'''Load a descriptor definition from the specified xml_object.
@@ -246,7 +276,7 @@ class XmlDescriptor(XModuleDescriptor):
# don't load these
continue
- attr_map = cls.xml_attribute_map.get(attr, AttrMap())
+ attr_map = cls.get_map_for_field(attr)
metadata[attr] = attr_map.from_xml(val)
return metadata
@@ -258,7 +288,7 @@ class XmlDescriptor(XModuleDescriptor):
through the attrmap. Updates the metadata dict in place.
"""
for attr in policy:
- attr_map = cls.xml_attribute_map.get(attr, AttrMap())
+ attr_map = cls.get_map_for_field(attr)
metadata[cls._translate(attr)] = attr_map.from_xml(policy[attr])
@classmethod
@@ -347,7 +377,7 @@ class XmlDescriptor(XModuleDescriptor):
def export_to_xml(self, resource_fs):
"""
- Returns an xml string representign this module, and all modules
+ Returns an xml string representing this module, and all modules
underneath it. May also write required resources out to resource_fs
Assumes that modules have single parentage (that no module appears twice
@@ -372,7 +402,7 @@ class XmlDescriptor(XModuleDescriptor):
"""Get the value for this attribute that we want to store.
(Possible format conversion through an AttrMap).
"""
- attr_map = self.xml_attribute_map.get(attr, AttrMap())
+ attr_map = self.get_map_for_field(attr)
return attr_map.to_xml(self._model_data[attr])
# Add the non-inherited metadata
diff --git a/common/test/data/full/course.xml b/common/test/data/full/course.xml
index b2f9097020..9ee128da1a 100644
--- a/common/test/data/full/course.xml
+++ b/common/test/data/full/course.xml
@@ -1 +1 @@
-
+
diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py
index 6b78d18db0..aaef0b76db 100644
--- a/lms/xmodule_namespace.py
+++ b/lms/xmodule_namespace.py
@@ -1,15 +1,15 @@
"""
Namespace that defines fields common to all blocks used in the LMS
"""
-from xblock.core import Namespace, Boolean, Scope, String
-from xmodule.fields import Date, Timedelta, StringyFloat, StringyBoolean
+from xblock.core import Namespace, Boolean, Scope, String, Float
+from xmodule.fields import Date, Timedelta
class LmsNamespace(Namespace):
"""
Namespace that defines fields common to all blocks used in the LMS
"""
- hide_from_toc = StringyBoolean(
+ hide_from_toc = Boolean(
help="Whether to display this module in the table of contents",
default=False,
scope=Scope.settings
@@ -37,7 +37,7 @@ class LmsNamespace(Namespace):
)
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
rerandomize = String(help="When to rerandomize the problem", default="always", scope=Scope.settings)
- days_early_for_beta = StringyFloat(
+ days_early_for_beta = Float(
help="Number of days early to show content to beta users",
default=None,
scope=Scope.settings
diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt
index 8f3d4594ac..dc39bd5fa4 100644
--- a/requirements/edx/github.txt
+++ b/requirements/edx/github.txt
@@ -8,6 +8,6 @@
-e git://github.com/eventbrite/zendesk.git@d53fe0e81b623f084e91776bcf6369f8b7b63879#egg=zendesk
# Our libraries:
--e git+https://github.com/edx/XBlock.git@2144a25d#egg=XBlock
+-e git+https://github.com/edx/XBlock.git@4d8735e883#egg=XBlock
-e git+https://github.com/edx/codejail.git@0a1b468#egg=codejail
-e git+https://github.com/edx/diff-cover.git@v0.1.1#egg=diff_cover