Merge branch 'master' into feature/btalbot/studio-alerts
This commit is contained in:
@@ -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', {}))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
14
cms/templates/404.html
Normal file
14
cms/templates/404.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Page Not Found</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
|
||||
<h1>Page not found</h1>
|
||||
<p>The page that you were looking for was not found. Go back to the <a href="/">homepage</a> or let us know about any pages that may have been moved at <a href="mailto:technical@edx.org">technical@edx.org</a>.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</%block>
|
||||
13
cms/templates/500.html
Normal file
13
cms/templates/500.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Server Error</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
<div class="wrapper-content wrapper">
|
||||
<section class="content">
|
||||
<h1>Currently the <em>edX</em> servers are down</h1>
|
||||
<p>Our staff is currently working to get the site back up as soon as possible. Please email us at <a href="mailto:technical@edx.org">technical@edx.org</a> to report any problems or downtime.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</%block>
|
||||
@@ -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'
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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...")
|
||||
|
||||
|
||||
@@ -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()])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. <numericalresponse>).
|
||||
|
||||
|
||||
The tree should NOT contain any input elements
|
||||
(such as <textline />) 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 <p> 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 <schematicresponse> 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 <schematic>."""
|
||||
return etree.Element("schematic")
|
||||
|
||||
|
||||
class CodeResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for creating <coderesponse> XML trees """
|
||||
|
||||
@@ -286,9 +286,9 @@ class CodeResponseXMLFactory(ResponseXMLFactory):
|
||||
|
||||
def create_response_element(self, **kwargs):
|
||||
""" Create a <coderesponse> 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 <choiceresponse> 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 <imageresponse> XML """
|
||||
|
||||
@@ -450,9 +452,9 @@ class ImageResponseXMLFactory(ResponseXMLFactory):
|
||||
|
||||
def create_input_element(self, **kwargs):
|
||||
""" Create the <imageinput> 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 <javascriptresponse> 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 <javascriptresponse> element
|
||||
@@ -552,6 +555,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Create the <javascriptinput> element """
|
||||
return etree.Element("javascriptinput")
|
||||
|
||||
|
||||
class MultipleChoiceResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for producing <multiplechoiceresponse> XML """
|
||||
|
||||
@@ -564,6 +568,7 @@ class MultipleChoiceResponseXMLFactory(ResponseXMLFactory):
|
||||
kwargs['choice_type'] = 'multiple'
|
||||
return ResponseXMLFactory.choicegroup_input_xml(**kwargs)
|
||||
|
||||
|
||||
class TrueFalseResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for producing <truefalseresponse> XML """
|
||||
|
||||
@@ -576,6 +581,7 @@ class TrueFalseResponseXMLFactory(ResponseXMLFactory):
|
||||
kwargs['choice_type'] = 'multiple'
|
||||
return ResponseXMLFactory.choicegroup_input_xml(**kwargs)
|
||||
|
||||
|
||||
class OptionResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for producing <optionresponse> XML"""
|
||||
|
||||
@@ -620,7 +626,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
|
||||
|
||||
def create_response_element(self, **kwargs):
|
||||
""" Create a <stringresponse> XML element.
|
||||
|
||||
|
||||
Uses **kwargs:
|
||||
|
||||
*answer*: The correct answer (a string) [REQUIRED]
|
||||
@@ -642,7 +648,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
|
||||
# Create the <stringresponse> 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 <annotationresponse> 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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -13,14 +13,19 @@
|
||||
<script src="{% static 'js/vendor/jasmine-jquery.js' %}"></script>
|
||||
<script src="{% static 'console-runner.js' %}"></script>
|
||||
|
||||
{% load compressed %}
|
||||
{# static files #}
|
||||
{% for url in suite.static_files %}
|
||||
<script src="{{ STATIC_URL }}{{ url }}"></script>
|
||||
{% endfor %}
|
||||
|
||||
{% compressed_js 'js-test-source' %}
|
||||
|
||||
{# source files #}
|
||||
{% for url in suite.js_files %}
|
||||
<script src="{{ url }}"></script>
|
||||
{% endfor %}
|
||||
|
||||
{% load compressed %}
|
||||
{# static files #}
|
||||
{% compressed_js 'js-test-source' %}
|
||||
|
||||
{# spec files #}
|
||||
{% compressed_js 'spec' %}
|
||||
|
||||
@@ -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(" ", "_"))
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
@@ -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 "<TabName>" tab
|
||||
Then the page title should contain "<PageTitle>"
|
||||
|
||||
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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
73
lms/djangoapps/courseware/features/problems.feature
Normal file
73
lms/djangoapps/courseware/features/problems.feature
Normal file
@@ -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 "<ProblemType>" problem
|
||||
When I answer a "<ProblemType>" problem "correctly"
|
||||
Then My "<ProblemType>" 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 "<ProblemType>" problem
|
||||
When I answer a "<ProblemType>" problem "incorrectly"
|
||||
Then My "<ProblemType>" 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 "<ProblemType>" problem
|
||||
When I check a problem
|
||||
Then My "<ProblemType>" 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 "<ProblemType>" problem
|
||||
And I answer a "<ProblemType>" problem "<Correctness>ly"
|
||||
When I reset the problem
|
||||
Then My "<ProblemType>" 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 |
|
||||
271
lms/djangoapps/courseware/features/problems.py
Normal file
271
lms/djangoapps/courseware/features/problems.py
Normal file
@@ -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 <input> 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
# Then I verify all the content of each course
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', ->
|
||||
|
||||
@@ -45,10 +45,10 @@
|
||||
</header>
|
||||
|
||||
<div class="post-body">${'<%- body %>'}</div>
|
||||
|
||||
% if course and has_permission(user, 'openclose_thread', course.id):
|
||||
<div class="admin-pin discussion-pin notpinned" data-role="thread-pin" data-tooltip="pin this thread">
|
||||
<i class="icon"></i><span class="pin-label">Pin Thread</span></div>
|
||||
|
||||
%else:
|
||||
${"<% if (pinned) { %>"}
|
||||
<div class="discussion-pin notpinned" data-role="thread-pin" data-tooltip="pin this thread">
|
||||
@@ -57,9 +57,6 @@
|
||||
% endif
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
${'<% if (obj.courseware_url) { %>'}
|
||||
<div class="post-context">
|
||||
(this post is about <a href="${'<%- courseware_url%>'}">${'<%- courseware_title %>'}</a>)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user