diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 8809e19acf..2c3788256e 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -18,7 +18,8 @@ from django.core.files.temp import NamedTemporaryFile
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
from PIL import Image
-from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
+from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError
+from django.http import HttpResponseNotFound
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.core.context_processors import csrf
@@ -1600,3 +1601,11 @@ def event(request):
console logs don't get distracted :-)
'''
return HttpResponse(True)
+
+
+def render_404(request):
+ return HttpResponseNotFound(render_to_string('404.html', {}))
+
+
+def render_500(request):
+ return HttpResponseServerError(render_to_string('500.html', {}))
diff --git a/cms/envs/dev.py b/cms/envs/dev.py
index f70f22512e..5612db1396 100644
--- a/cms/envs/dev.py
+++ b/cms/envs/dev.py
@@ -127,8 +127,7 @@ DEBUG_TOOLBAR_PANELS = (
'debug_toolbar.panels.sql.SQLDebugPanel',
'debug_toolbar.panels.signals.SignalDebugPanel',
'debug_toolbar.panels.logger.LoggingPanel',
-# This is breaking Mongo updates-- Christina is investigating.
-# 'debug_toolbar_mongo.panel.MongoDebugPanel',
+ 'debug_toolbar_mongo.panel.MongoDebugPanel',
# Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets
@@ -143,4 +142,4 @@ DEBUG_TOOLBAR_CONFIG = {
# To see stacktraces for MongoDB queries, set this to True.
# Stacktraces slow down page loads drastically (for pages with lots of queries).
-# DEBUG_TOOLBAR_MONGO_STACKTRACES = False
+DEBUG_TOOLBAR_MONGO_STACKTRACES = False
diff --git a/cms/static/coffee/files.json b/cms/static/coffee/files.json
index 2249813b04..e7a66b5bc0 100644
--- a/cms/static/coffee/files.json
+++ b/cms/static/coffee/files.json
@@ -1,12 +1,12 @@
{
- "js_files": [
- "/static/js/vendor/RequireJS.js",
- "/static/js/vendor/jquery.min.js",
- "/static/js/vendor/jquery-ui.min.js",
- "/static/js/vendor/jquery.ui.draggable.js",
- "/static/js/vendor/jquery.cookie.js",
- "/static/js/vendor/json2.js",
- "/static/js/vendor/underscore-min.js",
- "/static/js/vendor/backbone-min.js"
+ "static_files": [
+ "js/vendor/RequireJS.js",
+ "js/vendor/jquery.min.js",
+ "js/vendor/jquery-ui.min.js",
+ "js/vendor/jquery.ui.draggable.js",
+ "js/vendor/jquery.cookie.js",
+ "js/vendor/json2.js",
+ "js/vendor/underscore-min.js",
+ "js/vendor/backbone-min.js"
]
}
diff --git a/cms/templates/404.html b/cms/templates/404.html
new file mode 100644
index 0000000000..a45a223bad
--- /dev/null
+++ b/cms/templates/404.html
@@ -0,0 +1,14 @@
+<%inherit file="base.html" />
+<%block name="title">Page Not Found%block>
+
+<%block name="content">
+
+
+
+
+
Page not found
+
The page that you were looking for was not found. Go back to the homepage or let us know about any pages that may have been moved at technical@edx.org.
+
+
+
+%block>
\ No newline at end of file
diff --git a/cms/templates/500.html b/cms/templates/500.html
new file mode 100644
index 0000000000..2645b0067b
--- /dev/null
+++ b/cms/templates/500.html
@@ -0,0 +1,13 @@
+<%inherit file="base.html" />
+<%block name="title">Server Error%block>
+
+<%block name="content">
+
+
+
+
Currently the edX servers are down
+
Our staff is currently working to get the site back up as soon as possible. Please email us at technical@edx.org to report any problems or downtime.
+
+
+
+%block>
\ No newline at end of file
diff --git a/cms/urls.py b/cms/urls.py
index 92963f2271..d050821318 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -105,3 +105,9 @@ if settings.ENABLE_JASMINE:
urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
urlpatterns = patterns(*urlpatterns)
+
+#Custom error pages
+handler404 = 'contentstore.views.render_404'
+handler500 = 'contentstore.views.render_500'
+
+
diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py
index 391cac8eca..cad3110574 100644
--- a/cms/xmodule_namespace.py
+++ b/cms/xmodule_namespace.py
@@ -1,14 +1,27 @@
+"""
+Namespace defining common fields used by Studio for all blocks
+"""
+
import datetime
from xblock.core import Namespace, Boolean, Scope, ModelType, String
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
+
class DateTuple(ModelType):
"""
ModelType that stores datetime objects as time tuples
@@ -24,6 +37,9 @@ class DateTuple(ModelType):
class CmsNamespace(Namespace):
+ """
+ Namespace with fields common to all blocks in Studio
+ """
is_draft = Boolean(help="Whether this module is a draft", default=False, scope=Scope.settings)
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/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py
index 0881d86124..6394959532 100644
--- a/common/djangoapps/terrain/browser.py
+++ b/common/djangoapps/terrain/browser.py
@@ -3,6 +3,11 @@ from splinter.browser import Browser
from logging import getLogger
import time
+# Let the LMS and CMS do their one-time setup
+# For example, setting up mongo caches
+from lms import one_time_startup
+from cms import one_time_startup
+
logger = getLogger(__name__)
logger.info("Loading the lettuce acceptance testing terrain file...")
diff --git a/common/djangoapps/terrain/factories.py b/common/djangoapps/terrain/factories.py
index a531f4fd26..c36bf935f1 100644
--- a/common/djangoapps/terrain/factories.py
+++ b/common/djangoapps/terrain/factories.py
@@ -121,21 +121,41 @@ class XModuleItemFactory(Factory):
@classmethod
def _create(cls, target_class, *args, **kwargs):
"""
- kwargs must include parent_location, template. Can contain display_name
- target_class is ignored
+ Uses *kwargs*:
+
+ *parent_location* (required): the location of the parent module
+ (e.g. the parent course or section)
+
+ *template* (required): the template to create the item from
+ (e.g. i4x://templates/section/Empty)
+
+ *data* (optional): the data for the item
+ (e.g. XML problem definition for a problem item)
+
+ *display_name* (optional): the display name of the item
+
+ *metadata* (optional): dictionary of metadata attributes
+
+ *target_class* is ignored
"""
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
parent_location = Location(kwargs.get('parent_location'))
template = Location(kwargs.get('template'))
+ data = kwargs.get('data')
display_name = kwargs.get('display_name')
+ metadata = kwargs.get('metadata', {})
store = modulestore('direct')
# This code was based off that in cms/djangoapps/contentstore/views.py
parent = store.get_item(parent_location)
- dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
+
+ # If a display name is set, use that
+ dest_name = display_name.replace(" ", "_") if display_name is not None else uuid4().hex
+ dest_location = parent_location._replace(category=template.category,
+ name=dest_name)
new_item = store.clone_item(template, dest_location)
@@ -143,7 +163,14 @@ class XModuleItemFactory(Factory):
if display_name is not None:
new_item.display_name = display_name
- store.update_metadata(new_item.location.url(), own_metadata(new_item))
+ # Add additional metadata or override current metadata
+ item_metadata = own_metadata(new_item)
+ item_metadata.update(metadata)
+ store.update_metadata(new_item.location.url(), item_metadata)
+
+ # replace the data with the optional *data* parameter
+ if data is not None:
+ store.update_item(new_item.location, data)
if new_item.location.category not in DETACHED_CATEGORIES:
store.update_children(parent_location, parent.children + [new_item.location.url()])
diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py
index 3dcef9b1ed..52eeb23c4a 100644
--- a/common/djangoapps/terrain/steps.py
+++ b/common/djangoapps/terrain/steps.py
@@ -69,6 +69,11 @@ def the_page_title_should_be(step, title):
assert_equals(world.browser.title, title)
+@step(u'the page title should contain "([^"]*)"$')
+def the_page_title_should_contain(step, title):
+ assert(title in world.browser.title)
+
+
@step('I am a logged in user$')
def i_am_logged_in_user(step):
create_user('robot')
@@ -80,18 +85,6 @@ def i_am_not_logged_in(step):
world.browser.cookies.delete()
-@step('I am registered for a course$')
-def i_am_registered_for_a_course(step):
- create_user('robot')
- u = User.objects.get(username='robot')
- CourseEnrollment.objects.get_or_create(user=u, course_id='MITx/6.002x/2012_Fall')
-
-
-@step('I am registered for course "([^"]*)"$')
-def i_am_registered_for_course_by_id(step, course_id):
- register_by_course_id(course_id)
-
-
@step('I am staff for course "([^"]*)"$')
def i_am_staff_for_course_by_id(step, course_id):
register_by_course_id(course_id, True)
@@ -108,6 +101,7 @@ def i_am_an_edx_user(step):
#### helper functions
+
@world.absorb
def scroll_to_bottom():
# Maximize the browser
@@ -116,6 +110,11 @@ def scroll_to_bottom():
@world.absorb
def create_user(uname):
+
+ # If the user already exists, don't try to create it again
+ if len(User.objects.filter(username=uname)) > 0:
+ return
+
portal_user = UserFactory.build(username=uname, email=uname + '@edx.org')
portal_user.set_password('test')
portal_user.save()
@@ -133,13 +132,25 @@ def log_in(email, password):
world.browser.visit(django_url('/'))
world.browser.is_element_present_by_css('header.global', 10)
world.browser.click_link_by_href('#login-modal')
- login_form = world.browser.find_by_css('form#login_form')
+
+ # Wait for the login dialog to load
+ # This is complicated by the fact that sometimes a second #login_form
+ # dialog loads, while the first one remains hidden.
+ # We give them both time to load, starting with the second one.
+ world.browser.is_element_present_by_css('section.content-wrapper form#login_form', wait_time=4)
+ world.browser.is_element_present_by_css('form#login_form', wait_time=2)
+
+ # For some reason, the page sometimes includes two #login_form
+ # elements, the first of which is not visible.
+ # To avoid this, we always select the last of the two #login_form dialogs
+ login_form = world.browser.find_by_css('form#login_form').last
+
login_form.find_by_name('email').fill(email)
login_form.find_by_name('password').fill(password)
login_form.find_by_name('submit').click()
# wait for the page to redraw
- assert world.browser.is_element_present_by_css('.content-wrapper', 10)
+ assert world.browser.is_element_present_by_css('.content-wrapper', wait_time=10)
@world.absorb
diff --git a/common/lib/capa/capa/tests/response_xml_factory.py b/common/lib/capa/capa/tests/response_xml_factory.py
index 7aa299d20d..aa401b70cd 100644
--- a/common/lib/capa/capa/tests/response_xml_factory.py
+++ b/common/lib/capa/capa/tests/response_xml_factory.py
@@ -1,6 +1,7 @@
from lxml import etree
from abc import ABCMeta, abstractmethod
+
class ResponseXMLFactory(object):
""" Abstract base class for capa response XML factories.
Subclasses override create_response_element and
@@ -13,7 +14,7 @@ class ResponseXMLFactory(object):
""" Subclasses override to return an etree element
representing the capa response XML
(e.g. ).
-
+
The tree should NOT contain any input elements
(such as ) as these will be added later."""
return None
@@ -25,7 +26,7 @@ class ResponseXMLFactory(object):
return None
def build_xml(self, **kwargs):
- """ Construct an XML string for a capa response
+ """ Construct an XML string for a capa response
based on **kwargs.
**kwargs is a dictionary that will be passed
@@ -37,7 +38,7 @@ class ResponseXMLFactory(object):
*question_text*: The text of the question to display,
wrapped in
tags.
-
+
*explanation_text*: The detailed explanation that will
be shown if the user answers incorrectly.
@@ -75,7 +76,7 @@ class ResponseXMLFactory(object):
for i in range(0, int(num_responses)):
response_element = self.create_response_element(**kwargs)
root.append(response_element)
-
+
# Add input elements
for j in range(0, int(num_inputs)):
input_element = self.create_input_element(**kwargs)
@@ -135,7 +136,7 @@ class ResponseXMLFactory(object):
# Names of group elements
group_element_names = {'checkbox': 'checkboxgroup',
'radio': 'radiogroup',
- 'multiple': 'choicegroup' }
+ 'multiple': 'choicegroup'}
# Retrieve **kwargs
choices = kwargs.get('choices', [True])
@@ -151,13 +152,11 @@ class ResponseXMLFactory(object):
choice_element = etree.SubElement(group_element, "choice")
choice_element.set("correct", "true" if correct_val else "false")
- # Add some text describing the choice
- etree.SubElement(choice_element, "startouttext")
- etree.text = "Choice description"
- etree.SubElement(choice_element, "endouttext")
-
# Add a name identifying the choice, if one exists
+ # For simplicity, we use the same string as both the
+ # name attribute and the text of the element
if name:
+ choice_element.text = str(name)
choice_element.set("name", str(name))
return group_element
@@ -217,7 +216,7 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
*answer*: Inline script that calculates the answer
"""
-
+
# Retrieve **kwargs
cfn = kwargs.get('cfn', None)
expect = kwargs.get('expect', None)
@@ -247,7 +246,7 @@ class SchematicResponseXMLFactory(ResponseXMLFactory):
def create_response_element(self, **kwargs):
""" Create the XML element.
-
+
Uses *kwargs*:
*answer*: The Python script used to evaluate the answer.
@@ -274,6 +273,7 @@ class SchematicResponseXMLFactory(ResponseXMLFactory):
For testing, we create a bare-bones version of ."""
return etree.Element("schematic")
+
class CodeResponseXMLFactory(ResponseXMLFactory):
""" Factory for creating XML trees """
@@ -286,9 +286,9 @@ class CodeResponseXMLFactory(ResponseXMLFactory):
def create_response_element(self, **kwargs):
""" Create a XML element:
-
+
Uses **kwargs:
-
+
*initial_display*: The code that initially appears in the textbox
[DEFAULT: "Enter code here"]
*answer_display*: The answer to display to the student
@@ -328,6 +328,7 @@ class CodeResponseXMLFactory(ResponseXMLFactory):
# return None here
return None
+
class ChoiceResponseXMLFactory(ResponseXMLFactory):
""" Factory for creating XML trees """
@@ -356,13 +357,13 @@ class FormulaResponseXMLFactory(ResponseXMLFactory):
*num_samples*: The number of times to sample the student's answer
to numerically compare it to the correct answer.
-
+
*tolerance*: The tolerance within which answers will be accepted
- [DEFAULT: 0.01]
+ [DEFAULT: 0.01]
*answer*: The answer to the problem. Can be a formula string
- or a Python variable defined in a script
- (e.g. "$calculated_answer" for a Python variable
+ or a Python variable defined in a script
+ (e.g. "$calculated_answer" for a Python variable
called calculated_answer)
[REQUIRED]
@@ -387,7 +388,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory):
# Set the sample information
sample_str = self._sample_str(sample_dict, num_samples, tolerance)
response_element.set("samples", sample_str)
-
+
# Set the tolerance
responseparam_element = etree.SubElement(response_element, "responseparam")
@@ -408,7 +409,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory):
# We could sample a different range, but for simplicity,
# we use the same sample string for the hints
- # that we used previously.
+ # that we used previously.
formulahint_element.set("samples", sample_str)
formulahint_element.set("answer", str(hint_prompt))
@@ -436,10 +437,11 @@ class FormulaResponseXMLFactory(ResponseXMLFactory):
high_range_vals = [str(f[1]) for f in sample_dict.values()]
sample_str = (",".join(sample_dict.keys()) + "@" +
",".join(low_range_vals) + ":" +
- ",".join(high_range_vals) +
+ ",".join(high_range_vals) +
"#" + str(num_samples))
return sample_str
+
class ImageResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing XML """
@@ -450,9 +452,9 @@ class ImageResponseXMLFactory(ResponseXMLFactory):
def create_input_element(self, **kwargs):
""" Create the element.
-
+
Uses **kwargs:
-
+
*src*: URL for the image file [DEFAULT: "/static/image.jpg"]
*width*: Width of the image [DEFAULT: 100]
@@ -490,7 +492,7 @@ class ImageResponseXMLFactory(ResponseXMLFactory):
input_element.set("src", str(src))
input_element.set("width", str(width))
input_element.set("height", str(height))
-
+
if rectangle:
input_element.set("rectangle", rectangle)
@@ -499,6 +501,7 @@ class ImageResponseXMLFactory(ResponseXMLFactory):
return input_element
+
class JavascriptResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing XML """
@@ -522,7 +525,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory):
# Both display_src and display_class given,
# or neither given
- assert((display_src and display_class) or
+ assert((display_src and display_class) or
(not display_src and not display_class))
# Create the element
@@ -552,6 +555,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory):
""" Create the element """
return etree.Element("javascriptinput")
+
class MultipleChoiceResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing XML """
@@ -564,6 +568,7 @@ class MultipleChoiceResponseXMLFactory(ResponseXMLFactory):
kwargs['choice_type'] = 'multiple'
return ResponseXMLFactory.choicegroup_input_xml(**kwargs)
+
class TrueFalseResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing XML """
@@ -576,6 +581,7 @@ class TrueFalseResponseXMLFactory(ResponseXMLFactory):
kwargs['choice_type'] = 'multiple'
return ResponseXMLFactory.choicegroup_input_xml(**kwargs)
+
class OptionResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing XML"""
@@ -620,7 +626,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
def create_response_element(self, **kwargs):
""" Create a XML element.
-
+
Uses **kwargs:
*answer*: The correct answer (a string) [REQUIRED]
@@ -642,7 +648,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
# Create the element
response_element = etree.Element("stringresponse")
- # Set the answer attribute
+ # Set the answer attribute
response_element.set("answer", str(answer))
# Set the case sensitivity
@@ -667,6 +673,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs)
+
class AnnotationResponseXMLFactory(ResponseXMLFactory):
""" Factory for creating XML trees """
def create_response_element(self, **kwargs):
@@ -679,17 +686,17 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory):
input_element = etree.Element("annotationinput")
text_children = [
- {'tag': 'title', 'text': kwargs.get('title', 'super cool annotation') },
- {'tag': 'text', 'text': kwargs.get('text', 'texty text') },
- {'tag': 'comment', 'text':kwargs.get('comment', 'blah blah erudite comment blah blah') },
- {'tag': 'comment_prompt', 'text': kwargs.get('comment_prompt', 'type a commentary below') },
- {'tag': 'tag_prompt', 'text': kwargs.get('tag_prompt', 'select one tag') }
+ {'tag': 'title', 'text': kwargs.get('title', 'super cool annotation')},
+ {'tag': 'text', 'text': kwargs.get('text', 'texty text')},
+ {'tag': 'comment', 'text':kwargs.get('comment', 'blah blah erudite comment blah blah')},
+ {'tag': 'comment_prompt', 'text': kwargs.get('comment_prompt', 'type a commentary below')},
+ {'tag': 'tag_prompt', 'text': kwargs.get('tag_prompt', 'select one tag')}
]
for child in text_children:
etree.SubElement(input_element, child['tag']).text = child['text']
- default_options = [('green', 'correct'),('eggs', 'incorrect'),('ham', 'partially-correct')]
+ default_options = [('green', 'correct'),('eggs', 'incorrect'), ('ham', 'partially-correct')]
options = kwargs.get('options', default_options)
options_element = etree.SubElement(input_element, 'options')
@@ -698,4 +705,3 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory):
option_element.text = description
return input_element
-
diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py
index f05f419a03..48fbfcced1 100644
--- a/common/lib/xmodule/xmodule/combined_open_ended_module.py
+++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py
@@ -8,41 +8,66 @@ from xmodule.raw_module import RawDescriptor
from .x_module import XModule
from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float, List
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
+from collections import namedtuple
log = logging.getLogger("mitx.courseware")
-
V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload",
- "skip_spelling_checks", "due", "graceperiod", "max_score"]
+ "skip_spelling_checks", "due", "graceperiod", "max_score"]
V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state",
- "student_attempts", "ready_to_reset"]
+ "student_attempts", "ready_to_reset"]
V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES
-VERSION_TUPLES = (
- ('1', CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, V1_STUDENT_ATTRIBUTES),
-)
+VersionTuple = namedtuple('VersionTuple', ['descriptor', 'module', 'settings_attributes', 'student_attributes'])
+VERSION_TUPLES = {
+ 1: VersionTuple(CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES,
+ V1_STUDENT_ATTRIBUTES),
+}
DEFAULT_VERSION = 1
-DEFAULT_VERSION = str(DEFAULT_VERSION)
+
+
+class VersionInteger(Integer):
+ """
+ A model type that converts from strings to integers when reading from json.
+ Also does error checking to see if version is correct or not.
+ """
+
+ def from_json(self, value):
+ try:
+ value = int(value)
+ if value not in VERSION_TUPLES:
+ version_error_string = "Could not find version {0}, using version {1} instead"
+ log.error(version_error_string.format(value, DEFAULT_VERSION))
+ value = DEFAULT_VERSION
+ except:
+ value = DEFAULT_VERSION
+ return value
class CombinedOpenEndedFields(object):
display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings)
current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.student_state)
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.student_state)
- state = String(help="Which step within the current task that the student is on.", default="initial", scope=Scope.student_state)
- student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state)
- ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False, scope=Scope.student_state)
+ state = String(help="Which step within the current task that the student is on.", default="initial",
+ scope=Scope.student_state)
+ student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0,
+ scope=Scope.student_state)
+ ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False,
+ scope=Scope.student_state)
attempts = Integer(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings)
- is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
- accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False, scope=Scope.settings)
- skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True, scope=Scope.settings)
+ is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
+ accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False,
+ scope=Scope.settings)
+ skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True,
+ scope=Scope.settings)
due = String(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)
max_score = Integer(help="Maximum score for the problem.", default=1, scope=Scope.settings)
- version = Integer(help="Current version number", default=DEFAULT_VERSION, 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)
@@ -130,23 +155,10 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
if self.task_states is None:
self.task_states = []
- versions = [i[0] for i in VERSION_TUPLES]
- descriptors = [i[1] for i in VERSION_TUPLES]
- modules = [i[2] for i in VERSION_TUPLES]
- settings_attributes = [i[3] for i in VERSION_TUPLES]
- student_attributes = [i[4] for i in VERSION_TUPLES]
- version_error_string = "Could not find version {0}, using version {1} instead"
+ version_tuple = VERSION_TUPLES[self.version]
- try:
- version_index = versions.index(self.version)
- except:
- #This is a dev_facing_error
- log.error(version_error_string.format(self.version, DEFAULT_VERSION))
- self.version = DEFAULT_VERSION
- version_index = versions.index(self.version)
-
- self.student_attributes = student_attributes[version_index]
- self.settings_attributes = settings_attributes[version_index]
+ self.student_attributes = version_tuple.student_attributes
+ self.settings_attributes = version_tuple.settings_attributes
attributes = self.student_attributes + self.settings_attributes
@@ -154,10 +166,11 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
'rewrite_content_links': self.rewrite_content_links,
}
instance_state = {k: getattr(self, k) for k in attributes}
- self.child_descriptor = descriptors[version_index](self.system)
- self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(self.data), self.system)
- self.child_module = modules[version_index](self.system, location, self.child_definition, self.child_descriptor,
- instance_state=instance_state, static_data=static_data, attributes=attributes)
+ self.child_descriptor = version_tuple.descriptor(self.system)
+ self.child_definition = version_tuple.descriptor.definition_from_xml(etree.fromstring(self.data), self.system)
+ self.child_module = version_tuple.module(self.system, location, self.child_definition, self.child_descriptor,
+ instance_state=instance_state, static_data=static_data,
+ attributes=attributes)
self.save_instance_data()
def get_html(self):
diff --git a/common/lib/xmodule/xmodule/css/poll/display.scss b/common/lib/xmodule/xmodule/css/poll/display.scss
index cfc03bcf91..82c018a3a0 100644
--- a/common/lib/xmodule/xmodule/css/poll/display.scss
+++ b/common/lib/xmodule/xmodule/css/poll/display.scss
@@ -131,6 +131,7 @@ section.poll_question {
box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset;
color: rgb(255, 255, 255);
text-shadow: rgb(7, 103, 148) 0px 1px 0px;
+ background-image: none;
}
.text {
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index c5e5bbfdf8..1bf4763723 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -303,6 +303,7 @@ class MongoModuleStore(ModuleStoreBase):
# this is likely a leaf node, so let's record what metadata we need to inherit
metadata_to_inherit[child] = my_metadata
+
if root is not None:
_compute_inherited_metadata(root)
@@ -330,7 +331,7 @@ class MongoModuleStore(ModuleStoreBase):
return tree
def clear_cached_metadata_inheritance_tree(self, location):
- key_name = '{0}/{1}'.format(location.org, location.course)
+ key_name = '{0}/{1}'.format(location.org, location.course)
if self.metadata_inheritance_cache is not None:
self.metadata_inheritance_cache.delete(key_name)
@@ -387,12 +388,7 @@ class MongoModuleStore(ModuleStoreBase):
resource_fs = OSFS(root)
- metadata_inheritance_tree = None
-
- # if we are loading a course object, there is no parent to inherit the metadata from
- # so don't bother getting it
- if item['location']['category'] != 'course':
- metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location']))
+ metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location']))
# TODO (cdodge): When the 'split module store' work has been completed, we should remove
# the 'metadata_inheritance_tree' parameter
@@ -497,7 +493,10 @@ class MongoModuleStore(ModuleStoreBase):
try:
source_item = self.collection.find_one(location_to_query(source))
source_item['_id'] = Location(location).dict()
- self.collection.insert(source_item)
+ self.collection.insert(source_item,
+ # Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False")
+ # from overriding our default value set in the init method.
+ safe=self.collection.safe)
item = self._load_items([source_item])[0]
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
@@ -560,6 +559,9 @@ class MongoModuleStore(ModuleStoreBase):
{'$set': update},
multi=False,
upsert=True,
+ # Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False")
+ # from overriding our default value set in the init method.
+ safe=self.collection.safe
)
if result['n'] == 0:
raise ItemNotFoundError(location)
@@ -612,7 +614,7 @@ class MongoModuleStore(ModuleStoreBase):
self._update_single_item(location, {'metadata': metadata})
# recompute (and update) the metadata inheritance tree which is cached
- self.get_cached_metadata_inheritance_tree(loc, force_refresh = True)
+ self.get_cached_metadata_inheritance_tree(loc, force_refresh = True)
def delete_item(self, location):
"""
@@ -630,9 +632,12 @@ class MongoModuleStore(ModuleStoreBase):
course.tabs = [tab for tab in existing_tabs if tab.get('url_slug') != location.name]
self.update_metadata(course.location, own_metadata(course))
- self.collection.remove({'_id': Location(location).dict()})
+ self.collection.remove({'_id': Location(location).dict()},
+ # Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False")
+ # from overriding our default value set in the init method.
+ safe=self.collection.safe)
# recompute (and update) the metadata inheritance tree which is cached
- self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True)
+ self.get_cached_metadata_inheritance_tree(Location(location), force_refresh = True)
def get_parent_locations(self, location, course_id):
diff --git a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee
index a5a1deac10..56525af347 100644
--- a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee
+++ b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee
@@ -128,7 +128,9 @@ if Backbone?
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
- @model.set('pinned', true)
+ @model.set('pinned', true)
+ error: =>
+ $('.admin-pin').text("Pinning not currently available")
unPin: ->
url = @model.urlFor("unPinThread")
diff --git a/common/templates/jasmine/base.html b/common/templates/jasmine/base.html
index 96507bdebf..9a1b3bed92 100644
--- a/common/templates/jasmine/base.html
+++ b/common/templates/jasmine/base.html
@@ -13,14 +13,19 @@
+ {% load compressed %}
+ {# static files #}
+ {% for url in suite.static_files %}
+
+ {% endfor %}
+
+ {% compressed_js 'js-test-source' %}
+
{# source files #}
{% for url in suite.js_files %}
{% endfor %}
- {% load compressed %}
- {# static files #}
- {% compressed_js 'js-test-source' %}
{# spec files #}
{% compressed_js 'spec' %}
diff --git a/lms/djangoapps/courseware/features/common.py b/lms/djangoapps/courseware/features/common.py
index 2e19696ad4..8fb2843656 100644
--- a/lms/djangoapps/courseware/features/common.py
+++ b/lms/djangoapps/courseware/features/common.py
@@ -5,6 +5,10 @@ from lettuce.django import django_url
from django.conf import settings
from django.contrib.auth.models import User
from student.models import CourseEnrollment
+from terrain.factories import CourseFactory, ItemFactory
+from xmodule.modulestore import Location
+from xmodule.modulestore.django import _MODULESTORES, modulestore
+from xmodule.templates import update_templates
import time
from logging import getLogger
@@ -81,14 +85,57 @@ def i_am_not_logged_in(step):
world.browser.cookies.delete()
-@step(u'I am registered for a course$')
-def i_am_registered_for_a_course(step):
+TEST_COURSE_ORG = 'edx'
+TEST_COURSE_NAME = 'Test Course'
+TEST_SECTION_NAME = "Problem"
+
+
+@step(u'The course "([^"]*)" exists$')
+def create_course(step, course):
+
+ # First clear the modulestore so we don't try to recreate
+ # the same course twice
+ # This also ensures that the necessary templates are loaded
+ flush_xmodule_store()
+
+ # Create the course
+ # We always use the same org and display name,
+ # but vary the course identifier (e.g. 600x or 191x)
+ course = CourseFactory.create(org=TEST_COURSE_ORG,
+ number=course,
+ display_name=TEST_COURSE_NAME)
+
+ # Add a section to the course to contain problems
+ section = ItemFactory.create(parent_location=course.location,
+ display_name=TEST_SECTION_NAME)
+
+ problem_section = ItemFactory.create(parent_location=section.location,
+ template='i4x://edx/templates/sequential/Empty',
+ display_name=TEST_SECTION_NAME)
+
+
+@step(u'I am registered for the course "([^"]*)"$')
+def i_am_registered_for_the_course(step, course):
+ # Create the course
+ create_course(step, course)
+
+ # Create the user
world.create_user('robot')
u = User.objects.get(username='robot')
- CourseEnrollment.objects.create(user=u, course_id='MITx/6.002x/2012_Fall')
+
+ # If the user is not already enrolled, enroll the user.
+ CourseEnrollment.objects.get_or_create(user=u, course_id=course_id(course))
+
world.log_in('robot@edx.org', 'test')
+@step(u'The course "([^"]*)" has extra tab "([^"]*)"$')
+def add_tab_to_course(step, course, extra_tab_name):
+ section_item = ItemFactory.create(parent_location=course_location(course),
+ template="i4x://edx/templates/static_tab/Empty",
+ display_name=str(extra_tab_name))
+
+
@step(u'I am an edX user$')
def i_am_an_edx_user(step):
world.create_user('robot')
@@ -97,3 +144,37 @@ def i_am_an_edx_user(step):
@step(u'User "([^"]*)" is an edX user$')
def registered_edx_user(step, uname):
world.create_user(uname)
+
+
+def flush_xmodule_store():
+ # Flush and initialize the module store
+ # It needs the templates because it creates new records
+ # by cloning from the template.
+ # Note that if your test module gets in some weird state
+ # (though it shouldn't), do this manually
+ # from the bash shell to drop it:
+ # $ mongo test_xmodule --eval "db.dropDatabase()"
+ _MODULESTORES = {}
+ modulestore().collection.drop()
+ update_templates()
+
+
+def course_id(course_num):
+ return "%s/%s/%s" % (TEST_COURSE_ORG, course_num,
+ TEST_COURSE_NAME.replace(" ", "_"))
+
+
+def course_location(course_num):
+ return Location(loc_or_tag="i4x",
+ org=TEST_COURSE_ORG,
+ course=course_num,
+ category='course',
+ name=TEST_COURSE_NAME.replace(" ", "_"))
+
+
+def section_location(course_num):
+ return Location(loc_or_tag="i4x",
+ org=TEST_COURSE_ORG,
+ course=course_num,
+ category='sequential',
+ name=TEST_SECTION_NAME.replace(" ", "_"))
diff --git a/lms/djangoapps/courseware/features/courses.py b/lms/djangoapps/courseware/features/courses.py
index eb5143b782..4fbbfd24f2 100644
--- a/lms/djangoapps/courseware/features/courses.py
+++ b/lms/djangoapps/courseware/features/courses.py
@@ -9,6 +9,7 @@ logger = getLogger(__name__)
## support functions
+
def get_courses():
'''
Returns dict of lists of courses available, keyed by course.org (ie university).
diff --git a/lms/djangoapps/courseware/features/courseware.feature b/lms/djangoapps/courseware/features/courseware.feature
deleted file mode 100644
index 279e5732c9..0000000000
--- a/lms/djangoapps/courseware/features/courseware.feature
+++ /dev/null
@@ -1,11 +0,0 @@
-Feature: View the Courseware Tab
- As a student in an edX course
- In order to work on the course
- I want to view the info on the courseware tab
-
- Scenario: I can get to the courseware tab when logged in
- Given I am registered for a course
- And I log in
- And I click on View Courseware
- When I click on the "Courseware" tab
- Then the "Courseware" tab is active
diff --git a/lms/djangoapps/courseware/features/high-level-tabs.feature b/lms/djangoapps/courseware/features/high-level-tabs.feature
index 2e9c4f1886..931281a455 100644
--- a/lms/djangoapps/courseware/features/high-level-tabs.feature
+++ b/lms/djangoapps/courseware/features/high-level-tabs.feature
@@ -3,21 +3,18 @@ Feature: All the high level tabs should work
As a student
I want to navigate through the high level tabs
-# Note this didn't work as a scenario outline because
-# before each scenario was not flushing the database
-# TODO: break this apart so that if one fails the others
-# will still run
- Scenario: A student can see all tabs of the course
- Given I am registered for a course
- And I log in
- And I click on View Courseware
- When I click on the "Courseware" tab
- Then the page title should be "6.002x Courseware"
- When I click on the "Course Info" tab
- Then the page title should be "6.002x Course Info"
- When I click on the "Textbook" tab
- Then the page title should be "6.002x Textbook"
- When I click on the "Wiki" tab
- Then the page title should be "6.002x | edX Wiki"
- When I click on the "Progress" tab
- Then the page title should be "6.002x Progress"
+Scenario: I can navigate to all high -level tabs in a course
+ Given: I am registered for the course "6.002x"
+ And The course "6.002x" has extra tab "Custom Tab"
+ And I log in
+ And I click on View Courseware
+ When I click on the "" tab
+ Then the page title should contain ""
+
+ Examples:
+ | TabName | PageTitle |
+ | Courseware | 6.002x Courseware |
+ | Course Info | 6.002x Course Info |
+ | Custom Tab | 6.002x Custom Tab |
+ | Wiki | edX Wiki |
+ | Progress | 6.002x Progress |
diff --git a/lms/djangoapps/courseware/features/homepage.feature b/lms/djangoapps/courseware/features/homepage.feature
index 06a45c4bfa..c0c1c32f02 100644
--- a/lms/djangoapps/courseware/features/homepage.feature
+++ b/lms/djangoapps/courseware/features/homepage.feature
@@ -39,9 +39,9 @@ Feature: Homepage for web users
| MITx |
| HarvardX |
| BerkeleyX |
- | UTx |
+ | UTx |
| WellesleyX |
- | GeorgetownX |
+ | GeorgetownX |
# # TODO: Add scenario that tests the courses available
# # using a policy or a configuration file
diff --git a/lms/djangoapps/courseware/features/login.py b/lms/djangoapps/courseware/features/login.py
index ca7d710c61..094db078ca 100644
--- a/lms/djangoapps/courseware/features/login.py
+++ b/lms/djangoapps/courseware/features/login.py
@@ -34,6 +34,7 @@ def click_the_dropdown(step):
#### helper functions
+
def user_is_an_unactivated_user(uname):
u = User.objects.get(username=uname)
u.is_active = False
diff --git a/lms/djangoapps/courseware/features/openended.feature b/lms/djangoapps/courseware/features/openended.feature
index cc9f6e1c5f..1ab496144f 100644
--- a/lms/djangoapps/courseware/features/openended.feature
+++ b/lms/djangoapps/courseware/features/openended.feature
@@ -3,10 +3,10 @@ Feature: Open ended grading
In order to complete the courseware questions
I want the machine learning grading to be functional
- # Commenting these all out right now until we can
+ # Commenting these all out right now until we can
# make a reference implementation for a course with
# an open ended grading problem that is always available
- #
+ #
# Scenario: An answer that is too short is rejected
# Given I navigate to an openended question
# And I enter the answer "z"
diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature
new file mode 100644
index 0000000000..a7fbac49c7
--- /dev/null
+++ b/lms/djangoapps/courseware/features/problems.feature
@@ -0,0 +1,73 @@
+Feature: Answer choice problems
+ As a student in an edX course
+ In order to test my understanding of the material
+ I want to answer choice based problems
+
+ Scenario: I can answer a problem correctly
+ Given I am viewing a "" problem
+ When I answer a "" problem "correctly"
+ Then My "" answer is marked "correct"
+
+ Examples:
+ | ProblemType |
+ | drop down |
+ | multiple choice |
+ | checkbox |
+ | string |
+ | numerical |
+ | formula |
+ | script |
+
+ Scenario: I can answer a problem incorrectly
+ Given I am viewing a "" problem
+ When I answer a "" problem "incorrectly"
+ Then My "" answer is marked "incorrect"
+
+ Examples:
+ | ProblemType |
+ | drop down |
+ | multiple choice |
+ | checkbox |
+ | string |
+ | numerical |
+ | formula |
+ | script |
+
+ Scenario: I can submit a blank answer
+ Given I am viewing a "" problem
+ When I check a problem
+ Then My "" answer is marked "incorrect"
+
+ Examples:
+ | ProblemType |
+ | drop down |
+ | multiple choice |
+ | checkbox |
+ | string |
+ | numerical |
+ | formula |
+ | script |
+
+
+ Scenario: I can reset a problem
+ Given I am viewing a "" problem
+ And I answer a "" problem "ly"
+ When I reset the problem
+ Then My "" answer is marked "unanswered"
+
+ Examples:
+ | ProblemType | Correctness |
+ | drop down | correct |
+ | drop down | incorrect |
+ | multiple choice | correct |
+ | multiple choice | incorrect |
+ | checkbox | correct |
+ | checkbox | incorrect |
+ | string | correct |
+ | string | incorrect |
+ | numerical | correct |
+ | numerical | incorrect |
+ | formula | correct |
+ | formula | incorrect |
+ | script | correct |
+ | script | incorrect |
diff --git a/lms/djangoapps/courseware/features/problems.py b/lms/djangoapps/courseware/features/problems.py
new file mode 100644
index 0000000000..a6575c3d22
--- /dev/null
+++ b/lms/djangoapps/courseware/features/problems.py
@@ -0,0 +1,271 @@
+from lettuce import world, step
+from lettuce.django import django_url
+from selenium.webdriver.support.ui import Select
+import random
+import textwrap
+from common import i_am_registered_for_the_course, TEST_SECTION_NAME, section_location
+from terrain.factories import ItemFactory
+from capa.tests.response_xml_factory import OptionResponseXMLFactory, \
+ ChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory, \
+ StringResponseXMLFactory, NumericalResponseXMLFactory, \
+ FormulaResponseXMLFactory, CustomResponseXMLFactory
+
+# Factories from capa.tests.response_xml_factory that we will use
+# to generate the problem XML, with the keyword args used to configure
+# the output.
+PROBLEM_FACTORY_DICT = {
+ 'drop down': {
+ 'factory': OptionResponseXMLFactory(),
+ 'kwargs': {
+ 'question_text': 'The correct answer is Option 2',
+ 'options': ['Option 1', 'Option 2', 'Option 3', 'Option 4'],
+ 'correct_option': 'Option 2'}},
+
+ 'multiple choice': {
+ 'factory': MultipleChoiceResponseXMLFactory(),
+ 'kwargs': {
+ 'question_text': 'The correct answer is Choice 3',
+ 'choices': [False, False, True, False],
+ 'choice_names': ['choice_1', 'choice_2', 'choice_3', 'choice_4']}},
+
+ 'checkbox': {
+ 'factory': ChoiceResponseXMLFactory(),
+ 'kwargs': {
+ 'question_text': 'The correct answer is Choices 1 and 3',
+ 'choice_type': 'checkbox',
+ 'choices': [True, False, True, False, False],
+ 'choice_names': ['Choice 1', 'Choice 2', 'Choice 3', 'Choice 4']}},
+
+ 'string': {
+ 'factory': StringResponseXMLFactory(),
+ 'kwargs': {
+ 'question_text': 'The answer is "correct string"',
+ 'case_sensitive': False,
+ 'answer': 'correct string'}},
+
+ 'numerical': {
+ 'factory': NumericalResponseXMLFactory(),
+ 'kwargs': {
+ 'question_text': 'The answer is pi + 1',
+ 'answer': '4.14159',
+ 'tolerance': '0.00001',
+ 'math_display': True}},
+
+ 'formula': {
+ 'factory': FormulaResponseXMLFactory(),
+ 'kwargs': {
+ 'question_text': 'The solution is [mathjax]x^2+2x+y[/mathjax]',
+ 'sample_dict': {'x': (-100, 100), 'y': (-100, 100)},
+ 'num_samples': 10,
+ 'tolerance': 0.00001,
+ 'math_display': True,
+ 'answer': 'x^2+2*x+y'}},
+
+ 'script': {
+ 'factory': CustomResponseXMLFactory(),
+ 'kwargs': {
+ 'question_text': 'Enter two integers that sum to 10.',
+ 'cfn': 'test_add_to_ten',
+ 'expect': '10',
+ 'num_inputs': 2,
+ 'script': textwrap.dedent("""
+ def test_add_to_ten(expect,ans):
+ try:
+ a1=int(ans[0])
+ a2=int(ans[1])
+ except ValueError:
+ a1=0
+ a2=0
+ return (a1+a2)==int(expect)
+ """)}},
+ }
+
+
+def add_problem_to_course(course, problem_type):
+
+ assert(problem_type in PROBLEM_FACTORY_DICT)
+
+ # Generate the problem XML using capa.tests.response_xml_factory
+ factory_dict = PROBLEM_FACTORY_DICT[problem_type]
+ problem_xml = factory_dict['factory'].build_xml(**factory_dict['kwargs'])
+
+ # Create a problem item using our generated XML
+ # We set rerandomize=always in the metadata so that the "Reset" button
+ # will appear.
+ problem_item = ItemFactory.create(parent_location=section_location(course),
+ template="i4x://edx/templates/problem/Blank_Common_Problem",
+ display_name=str(problem_type),
+ data=problem_xml,
+ metadata={'rerandomize': 'always'})
+
+
+@step(u'I am viewing a "([^"]*)" problem')
+def view_problem(step, problem_type):
+ i_am_registered_for_the_course(step, 'model_course')
+
+ # Ensure that the course has this problem type
+ add_problem_to_course('model_course', problem_type)
+
+ # Go to the one section in the factory-created course
+ # which should be loaded with the correct problem
+ chapter_name = TEST_SECTION_NAME.replace(" ", "_")
+ section_name = chapter_name
+ url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' %
+ (chapter_name, section_name))
+
+ world.browser.visit(url)
+
+
+@step(u'I answer a "([^"]*)" problem "([^"]*)ly"')
+def answer_problem(step, problem_type, correctness):
+ """ Mark a given problem type correct or incorrect, then submit it.
+
+ *problem_type* is a string representing the type of problem (e.g. 'drop down')
+ *correctness* is in ['correct', 'incorrect']
+ """
+
+ assert(correctness in ['correct', 'incorrect'])
+
+ if problem_type == "drop down":
+ select_name = "input_i4x-edx-model_course-problem-drop_down_2_1"
+ option_text = 'Option 2' if correctness == 'correct' else 'Option 3'
+ world.browser.select(select_name, option_text)
+
+ elif problem_type == "multiple choice":
+ if correctness == 'correct':
+ inputfield('multiple choice', choice='choice_3').check()
+ else:
+ inputfield('multiple choice', choice='choice_2').check()
+
+ elif problem_type == "checkbox":
+ if correctness == 'correct':
+ inputfield('checkbox', choice='choice_0').check()
+ inputfield('checkbox', choice='choice_2').check()
+ else:
+ inputfield('checkbox', choice='choice_3').check()
+
+ elif problem_type == 'string':
+ textvalue = 'correct string' if correctness == 'correct' else 'incorrect'
+ inputfield('string').fill(textvalue)
+
+ elif problem_type == 'numerical':
+ textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2))
+ inputfield('numerical').fill(textvalue)
+
+ elif problem_type == 'formula':
+ textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2'
+ inputfield('formula').fill(textvalue)
+
+ elif problem_type == 'script':
+ # Correct answer is any two integers that sum to 10
+ first_addend = random.randint(-100, 100)
+ second_addend = 10 - first_addend
+
+ # If we want an incorrect answer, then change
+ # the second addend so they no longer sum to 10
+ if correctness == 'incorrect':
+ second_addend += random.randint(1, 10)
+
+ inputfield('script', input_num=1).fill(str(first_addend))
+ inputfield('script', input_num=2).fill(str(second_addend))
+
+ # Submit the problem
+ check_problem(step)
+
+
+@step(u'I check a problem')
+def check_problem(step):
+ world.browser.find_by_css("input.check").click()
+
+
+@step(u'I reset the problem')
+def reset_problem(step):
+ world.browser.find_by_css('input.reset').click()
+
+
+@step(u'My "([^"]*)" answer is marked "([^"]*)"')
+def assert_answer_mark(step, problem_type, correctness):
+ """ Assert that the expected answer mark is visible for a given problem type.
+
+ *problem_type* is a string identifying the type of problem (e.g. 'drop down')
+ *correctness* is in ['correct', 'incorrect', 'unanswered']
+
+ Asserting that a problem is marked 'unanswered' means that
+ the problem is NOT marked correct and NOT marked incorrect.
+ This can occur, for example, if the user has reset the problem. """
+
+ # Dictionaries that map problem types to the css selectors
+ # for correct/incorrect marks.
+ # The elements are lists of selectors because a particular problem type
+ # might be marked in multiple ways.
+ # For example, multiple choice is marked incorrect differently
+ # depending on whether the user selects an incorrect
+ # item or submits without selecting any item)
+ correct_selectors = {'drop down': ['span.correct'],
+ 'multiple choice': ['label.choicegroup_correct'],
+ 'checkbox': ['span.correct'],
+ 'string': ['div.correct'],
+ 'numerical': ['div.correct'],
+ 'formula': ['div.correct'],
+ 'script': ['div.correct'], }
+
+ incorrect_selectors = {'drop down': ['span.incorrect'],
+ 'multiple choice': ['label.choicegroup_incorrect',
+ 'span.incorrect'],
+ 'checkbox': ['span.incorrect'],
+ 'string': ['div.incorrect'],
+ 'numerical': ['div.incorrect'],
+ 'formula': ['div.incorrect'],
+ 'script': ['div.incorrect']}
+
+ assert(correctness in ['correct', 'incorrect', 'unanswered'])
+ assert(problem_type in correct_selectors and problem_type in incorrect_selectors)
+
+ # Assert that the question has the expected mark
+ # (either correct or incorrect)
+ if correctness in ["correct", "incorrect"]:
+
+ selector_dict = correct_selectors if correctness == "correct" else incorrect_selectors
+
+ # At least one of the correct selectors should be present
+ for sel in selector_dict[problem_type]:
+ has_expected_mark = world.browser.is_element_present_by_css(sel, wait_time=4)
+
+ # As soon as we find the selector, break out of the loop
+ if has_expected_mark:
+ break
+
+ # Expect that we found the right mark (correct or incorrect)
+ assert(has_expected_mark)
+
+ # Assert that the question has neither correct nor incorrect
+ # because it is unanswered (possibly reset)
+ else:
+ # Get all the correct/incorrect selectors for this problem type
+ selector_list = correct_selectors[problem_type] + incorrect_selectors[problem_type]
+
+ # Assert that none of the correct/incorrect selectors are present
+ for sel in selector_list:
+ assert(world.browser.is_element_not_present_by_css(sel, wait_time=4))
+
+
+def inputfield(problem_type, choice=None, input_num=1):
+ """ Return the element for *problem_type*.
+ For example, if problem_type is 'string', return
+ the text field for the string problem in the test course.
+
+ *choice* is the name of the checkbox input in a group
+ of checkboxes. """
+
+ sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" %
+ (problem_type.replace(" ", "_"), str(input_num)))
+
+ if choice is not None:
+ base = "_choice_" if problem_type == "multiple choice" else "_"
+ sel = sel + base + str(choice)
+
+ # If the input element doesn't exist, fail immediately
+ assert(world.browser.is_element_present_by_css(sel, wait_time=4))
+
+ # Retrieve the input element
+ return world.browser.find_by_css(sel)
diff --git a/lms/djangoapps/courseware/features/registration.feature b/lms/djangoapps/courseware/features/registration.feature
index d9b588534b..5933f860bb 100644
--- a/lms/djangoapps/courseware/features/registration.feature
+++ b/lms/djangoapps/courseware/features/registration.feature
@@ -4,13 +4,14 @@ Feature: Register for a course
I want to register for a class on the edX website
Scenario: I can register for a course
- Given I am logged in
+ Given The course "6.002x" exists
+ And I am logged in
And I visit the courses page
- When I register for the course numbered "6.002x"
+ When I register for the course "6.002x"
Then I should see the course numbered "6.002x" in my dashboard
Scenario: I can unregister for a course
- Given I am registered for a course
+ Given I am registered for the course "6.002x"
And I visit the dashboard
When I click the link with the text "Unregister"
And I press the "Unregister" button in the Unenroll dialog
diff --git a/lms/djangoapps/courseware/features/registration.py b/lms/djangoapps/courseware/features/registration.py
index f585136412..94b9b50f6c 100644
--- a/lms/djangoapps/courseware/features/registration.py
+++ b/lms/djangoapps/courseware/features/registration.py
@@ -1,12 +1,13 @@
from lettuce import world, step
+from lettuce.django import django_url
+from common import TEST_COURSE_ORG, TEST_COURSE_NAME
-@step('I register for the course numbered "([^"]*)"$')
+@step('I register for the course "([^"]*)"$')
def i_register_for_the_course(step, course):
- courses_section = world.browser.find_by_css('section.courses')
- course_link_css = 'article[id*="%s"] > div' % course
- course_link = courses_section.find_by_css(course_link_css).first
- course_link.click()
+ cleaned_name = TEST_COURSE_NAME.replace(' ', '_')
+ url = django_url('courses/%s/%s/%s/about' % (TEST_COURSE_ORG, course, cleaned_name))
+ world.browser.visit(url)
intro_section = world.browser.find_by_css('section.intro')
register_link = intro_section.find_by_css('a.register')
diff --git a/lms/djangoapps/courseware/features/smart-accordion.feature b/lms/djangoapps/courseware/features/smart-accordion.feature
index ccf1d45601..fc51eca25d 100644
--- a/lms/djangoapps/courseware/features/smart-accordion.feature
+++ b/lms/djangoapps/courseware/features/smart-accordion.feature
@@ -60,4 +60,4 @@ Feature: There are courses on the homepage
# Scenario: Navigate through course BerkeleyX/CS184.1x/2012_Fall
# Given I am registered for course "BerkeleyX/CS184.1x/2012_Fall"
# And I log in
- # Then I verify all the content of each course
\ No newline at end of file
+ # Then I verify all the content of each course
diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py
index ecff14777d..e7f389696c 100644
--- a/lms/djangoapps/courseware/grades.py
+++ b/lms/djangoapps/courseware/grades.py
@@ -159,6 +159,7 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False
# If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
for moduledescriptor in section['xmoduledescriptors']:
# Create a fake key to pull out a StudentModule object from the ModelDataCache
+
key = LmsKeyValueStore.Key(
Scope.student_state,
student.id,
diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py
index 4d7b56122c..69609dcf01 100644
--- a/lms/djangoapps/django_comment_client/base/views.py
+++ b/lms/djangoapps/django_comment_client/base/views.py
@@ -116,6 +116,10 @@ def create_thread(request, course_id, commentable_id):
thread.save()
+ #patch for backward compatibility to comments service
+ if not 'pinned' in thread.attributes:
+ thread['pinned'] = False
+
if post.get('auto_subscribe', 'false').lower() == 'true':
user = cc.User.from_django_user(request.user)
user.follow(thread)
diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py
index 301bb141be..3eee0948da 100644
--- a/lms/djangoapps/django_comment_client/forum/views.py
+++ b/lms/djangoapps/django_comment_client/forum/views.py
@@ -98,6 +98,11 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
else:
thread['group_name'] = ""
thread['group_string'] = "This post visible to everyone."
+
+ #patch for backward compatibility to comments service
+ if not 'pinned' in thread:
+ thread['pinned'] = False
+
query_params['page'] = page
query_params['num_pages'] = num_pages
@@ -245,6 +250,11 @@ def single_thread(request, course_id, discussion_id, thread_id):
try:
thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id)
+
+ #patch for backward compatibility with comments service
+ if not 'pinned' in thread.attributes:
+ thread['pinned'] = False
+
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
log.error("Error loading single thread.")
raise Http404
@@ -285,6 +295,10 @@ def single_thread(request, course_id, discussion_id, thread_id):
if thread.get('group_id') and not thread.get('group_name'):
thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name
+ #patch for backward compatibility with comments service
+ if not "pinned" in thread:
+ thread["pinned"] = False
+
threads = [utils.safe_content(thread) for thread in threads]
#recent_active_threads = cc.search_recent_active_threads(
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index 0be5724365..671283db9f 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -92,9 +92,15 @@ def instructor_dashboard(request, course_id):
data += compute_course_stats(course).items()
if request.user.is_staff:
for field in course.fields:
+ if getattr(field.scope, 'student', False):
+ continue
+
data.append([field.name, json.dumps(field.read_json(course))])
for namespace in course.namespaces:
for field in getattr(course, namespace).fields:
+ if getattr(field.scope, 'student', False):
+ continue
+
data.append(["{}.{}".format(namespace, field.name), json.dumps(field.read_json(course))])
datatable['data'] = data
diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py
index b6941f4a70..3dac545367 100644
--- a/lms/envs/acceptance.py
+++ b/lms/envs/acceptance.py
@@ -8,16 +8,24 @@ from .test import *
# otherwise the browser will not render the pages correctly
DEBUG = True
-# Show the courses that are in the data directory
-COURSES_ROOT = ENV_ROOT / "data"
-DATA_DIR = COURSES_ROOT
+# Use the mongo store for acceptance tests
+modulestore_options = {
+ 'default_class': 'xmodule.raw_module.RawDescriptor',
+ 'host': 'localhost',
+ 'db': 'test_xmodule',
+ 'collection': 'modulestore',
+ 'fs_root': GITHUB_REPO_ROOT,
+ 'render_template': 'mitxmako.shortcuts.render_to_string',
+}
+
MODULESTORE = {
'default': {
- 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
- 'OPTIONS': {
- 'data_dir': DATA_DIR,
- 'default_class': 'xmodule.hidden_module.HiddenDescriptor',
- }
+ 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
+ 'OPTIONS': modulestore_options
+ },
+ 'direct': {
+ 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
+ 'OPTIONS': modulestore_options
}
}
diff --git a/lms/static/coffee/files.json b/lms/static/coffee/files.json
index 5dc03613b9..0efe488dd9 100644
--- a/lms/static/coffee/files.json
+++ b/lms/static/coffee/files.json
@@ -5,8 +5,5 @@
"/static/js/vendor/jquery-ui.min.js",
"/static/js/vendor/jquery.leanModal.min.js",
"/static/js/vendor/flot/jquery.flot.js"
- ],
- "static_files": [
- "js/application.js"
]
}
diff --git a/lms/static/coffee/spec/calculator_spec.coffee b/lms/static/coffee/spec/calculator_spec.coffee
index 072d220a44..8258d8965a 100644
--- a/lms/static/coffee/spec/calculator_spec.coffee
+++ b/lms/static/coffee/spec/calculator_spec.coffee
@@ -4,9 +4,6 @@ describe 'Calculator', ->
@calculator = new Calculator
describe 'bind', ->
- beforeEach ->
- Calculator.bind()
-
it 'bind the calculator button', ->
expect($('.calc')).toHandleWith 'click', @calculator.toggle
@@ -31,12 +28,19 @@ describe 'Calculator', ->
$('form#calculator').submit()
describe 'toggle', ->
- it 'toggle the calculator and focus the input', ->
- spyOn $.fn, 'focus'
- @calculator.toggle(jQuery.Event("click"))
+ it 'focuses the input when toggled', ->
- expect($('li.calc-main')).toHaveClass('open')
- expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled()
+ # Since the focus is called asynchronously, we need to
+ # wait until focus() is called.
+ didFocus = false
+ runs ->
+ spyOn($.fn, 'focus').andCallFake (elementName) -> didFocus = true
+ @calculator.toggle(jQuery.Event("click"))
+
+ waitsFor (-> didFocus), "focus() should have been called on the input", 1000
+
+ runs ->
+ expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled()
it 'toggle the close button on the calculator button', ->
@calculator.toggle(jQuery.Event("click"))
diff --git a/lms/static/coffee/spec/modules/tab_spec.coffee b/lms/static/coffee/spec/modules/tab_spec.coffee
index 909f0d7cda..6fba470974 100644
--- a/lms/static/coffee/spec/modules/tab_spec.coffee
+++ b/lms/static/coffee/spec/modules/tab_spec.coffee
@@ -22,18 +22,23 @@ describe 'Tab', ->
it 'bind the tabs', ->
expect($.fn.tabs).toHaveBeenCalledWith show: @tab.onShow
+ # As of jQuery 1.9, the onShow callback is deprecated
+ # http://jqueryui.com/upgrade-guide/1.9/#deprecated-show-event-renamed-to-activate
+ # The code below tests that onShow does what is expected,
+ # but note that onShow will NOT be called when the user
+ # clicks on the tab if we're using jQuery version >= 1.9
describe 'onShow', ->
beforeEach ->
@tab = new Tab 1, @items
- $('[href="#tab-1-0"]').click()
+ @tab.onShow($('#tab-1-0'), {'index': 1})
it 'replace content in the container', ->
- $('[href="#tab-1-1"]').click()
+ @tab.onShow($('#tab-1-1'), {'index': 1})
expect($('#tab-1-0').html()).toEqual ''
expect($('#tab-1-1').html()).toEqual 'Video 2'
expect($('#tab-1-2').html()).toEqual ''
it 'trigger contentChanged event on the element', ->
spyOnEvent @tab.el, 'contentChanged'
- $('[href="#tab-1-1"]').click()
+ @tab.onShow($('#tab-1-1'), {'index': 1})
expect('contentChanged').toHaveBeenTriggeredOn @tab.el
diff --git a/lms/static/coffee/spec/navigation_spec.coffee b/lms/static/coffee/spec/navigation_spec.coffee
index 1340984e52..b351164b63 100644
--- a/lms/static/coffee/spec/navigation_spec.coffee
+++ b/lms/static/coffee/spec/navigation_spec.coffee
@@ -32,11 +32,9 @@ describe 'Navigation', ->
heightStyle: 'content'
it 'binds the accordionchange event', ->
- Navigation.bind()
expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log
it 'bind the navigation toggle', ->
- Navigation.bind()
expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle
describe 'when the #accordion does not exists', ->
@@ -45,7 +43,6 @@ describe 'Navigation', ->
it 'does not activate the accordion', ->
spyOn $.fn, 'accordion'
- Navigation.bind()
expect($('#accordion').accordion).wasNotCalled()
describe 'toggle', ->
diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html
index 5fdfb8aa82..24e3b467be 100644
--- a/lms/templates/discussion/_underscore_templates.html
+++ b/lms/templates/discussion/_underscore_templates.html
@@ -45,10 +45,10 @@
${'<%- body %>'}
-
% if course and has_permission(user, 'openclose_thread', course.id):
(this post is about ${'<%- courseware_title %>'})
diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py
index 4c04700a31..423c0eb0ec 100644
--- a/lms/xmodule_namespace.py
+++ b/lms/xmodule_namespace.py
@@ -1,14 +1,28 @@
-from xblock.core import Namespace, Boolean, Scope, String, List, Float
+"""
+Namespace that defines fields common to all blocks used in the LMS
+"""
+from xblock.core import Namespace, Boolean, Scope, String, Float
from xmodule.fields import Date, Timedelta
class StringyBoolean(Boolean):
+ """
+ Reads strings from JSON as booleans.
+
+ 'true' (case insensitive) return True, other strings return False
+ Other types are returned unchanged
+ """
def from_json(self, value):
if isinstance(value, basestring):
return value.lower() == 'true'
return value
+
class StringyFloat(Float):
+ """
+ Reads values as floats. If the value parses as a float, returns
+ that, otherwise returns None
+ """
def from_json(self, value):
try:
return float(value)
@@ -17,6 +31,9 @@ class StringyFloat(Float):
class LmsNamespace(Namespace):
+ """
+ Namespace that defines fields common to all blocks used in the LMS
+ """
hide_from_toc = StringyBoolean(
help="Whether to display this module in the table of contents",
default=False,
@@ -38,8 +55,14 @@ class LmsNamespace(Namespace):
source_file = String(help="DO NOT USE", scope=Scope.settings)
xqa_key = String(help="DO NOT USE", scope=Scope.settings)
ispublic = Boolean(help="Whether this course is open to the public, or only to admins", scope=Scope.settings)
- graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", 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")
rerandomize = String(help="When to rerandomize the problem", default="always", scope=Scope.settings)
- days_early_for_beta = StringyFloat(help="Number of days early to show content to beta users", default=None, scope=Scope.settings)
-
+ days_early_for_beta = StringyFloat(
+ help="Number of days early to show content to beta users",
+ default=None,
+ scope=Scope.settings
+ )