Commit 9069d822 by Brian Talbot

Merge branch 'master' into fix/btalbot/studio-sasscleanup

parents 8580e982 05ba082c
...@@ -18,7 +18,8 @@ from django.core.files.temp import NamedTemporaryFile ...@@ -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' # to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
from PIL import Image 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.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.context_processors import csrf from django.core.context_processors import csrf
...@@ -1597,3 +1598,11 @@ def event(request): ...@@ -1597,3 +1598,11 @@ def event(request):
console logs don't get distracted :-) console logs don't get distracted :-)
''' '''
return HttpResponse(True) 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 = ( ...@@ -127,8 +127,7 @@ DEBUG_TOOLBAR_PANELS = (
'debug_toolbar.panels.sql.SQLDebugPanel', 'debug_toolbar.panels.sql.SQLDebugPanel',
'debug_toolbar.panels.signals.SignalDebugPanel', 'debug_toolbar.panels.signals.SignalDebugPanel',
'debug_toolbar.panels.logger.LoggingPanel', '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 # 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 # Django=1.3.1/1.4 where requests to views get duplicated (your method gets
...@@ -143,4 +142,4 @@ DEBUG_TOOLBAR_CONFIG = { ...@@ -143,4 +142,4 @@ DEBUG_TOOLBAR_CONFIG = {
# To see stacktraces for MongoDB queries, set this to True. # To see stacktraces for MongoDB queries, set this to True.
# Stacktraces slow down page loads drastically (for pages with lots of queries). # Stacktraces slow down page loads drastically (for pages with lots of queries).
# DEBUG_TOOLBAR_MONGO_STACKTRACES = False DEBUG_TOOLBAR_MONGO_STACKTRACES = False
{ {
"js_files": [ "static_files": [
"/static/js/vendor/RequireJS.js", "js/vendor/RequireJS.js",
"/static/js/vendor/jquery.min.js", "js/vendor/jquery.min.js",
"/static/js/vendor/jquery-ui.min.js", "js/vendor/jquery-ui.min.js",
"/static/js/vendor/jquery.ui.draggable.js", "js/vendor/jquery.ui.draggable.js",
"/static/js/vendor/jquery.cookie.js", "js/vendor/jquery.cookie.js",
"/static/js/vendor/json2.js", "js/vendor/json2.js",
"/static/js/vendor/underscore-min.js", "js/vendor/underscore-min.js",
"/static/js/vendor/backbone-min.js" "js/vendor/backbone-min.js"
] ]
} }
<%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>
\ No newline at end of file
<%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>
\ No newline at end of file
...@@ -104,3 +104,9 @@ if settings.ENABLE_JASMINE: ...@@ -104,3 +104,9 @@ if settings.ENABLE_JASMINE:
urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),) urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
urlpatterns = patterns(*urlpatterns) urlpatterns = patterns(*urlpatterns)
#Custom error pages
handler404 = 'contentstore.views.render_404'
handler500 = 'contentstore.views.render_500'
"""
Namespace defining common fields used by Studio for all blocks
"""
import datetime import datetime
from xblock.core import Namespace, Boolean, Scope, ModelType, String from xblock.core import Namespace, Boolean, Scope, ModelType, String
class StringyBoolean(Boolean): 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): def from_json(self, value):
if isinstance(value, basestring): if isinstance(value, basestring):
return value.lower() == 'true' return value.lower() == 'true'
return value return value
class DateTuple(ModelType): class DateTuple(ModelType):
""" """
ModelType that stores datetime objects as time tuples ModelType that stores datetime objects as time tuples
...@@ -24,6 +37,9 @@ class DateTuple(ModelType): ...@@ -24,6 +37,9 @@ class DateTuple(ModelType):
class CmsNamespace(Namespace): 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) 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_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) published_by = String(help="Id of the user who published this module", scope=Scope.settings)
......
...@@ -3,6 +3,11 @@ from splinter.browser import Browser ...@@ -3,6 +3,11 @@ from splinter.browser import Browser
from logging import getLogger from logging import getLogger
import time 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 = getLogger(__name__)
logger.info("Loading the lettuce acceptance testing terrain file...") logger.info("Loading the lettuce acceptance testing terrain file...")
......
...@@ -121,21 +121,41 @@ class XModuleItemFactory(Factory): ...@@ -121,21 +121,41 @@ class XModuleItemFactory(Factory):
@classmethod @classmethod
def _create(cls, target_class, *args, **kwargs): def _create(cls, target_class, *args, **kwargs):
""" """
kwargs must include parent_location, template. Can contain display_name Uses *kwargs*:
target_class is ignored
*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'] DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
parent_location = Location(kwargs.get('parent_location')) parent_location = Location(kwargs.get('parent_location'))
template = Location(kwargs.get('template')) template = Location(kwargs.get('template'))
data = kwargs.get('data')
display_name = kwargs.get('display_name') display_name = kwargs.get('display_name')
metadata = kwargs.get('metadata', {})
store = modulestore('direct') store = modulestore('direct')
# This code was based off that in cms/djangoapps/contentstore/views.py # This code was based off that in cms/djangoapps/contentstore/views.py
parent = store.get_item(parent_location) 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) new_item = store.clone_item(template, dest_location)
...@@ -143,7 +163,14 @@ class XModuleItemFactory(Factory): ...@@ -143,7 +163,14 @@ class XModuleItemFactory(Factory):
if display_name is not None: if display_name is not None:
new_item.display_name = display_name 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: if new_item.location.category not in DETACHED_CATEGORIES:
store.update_children(parent_location, parent.children + [new_item.location.url()]) store.update_children(parent_location, parent.children + [new_item.location.url()])
......
...@@ -69,6 +69,11 @@ def the_page_title_should_be(step, title): ...@@ -69,6 +69,11 @@ def the_page_title_should_be(step, title):
assert_equals(world.browser.title, 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$') @step('I am a logged in user$')
def i_am_logged_in_user(step): def i_am_logged_in_user(step):
create_user('robot') create_user('robot')
...@@ -80,18 +85,6 @@ def i_am_not_logged_in(step): ...@@ -80,18 +85,6 @@ def i_am_not_logged_in(step):
world.browser.cookies.delete() 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 "([^"]*)"$') @step('I am staff for course "([^"]*)"$')
def i_am_staff_for_course_by_id(step, course_id): def i_am_staff_for_course_by_id(step, course_id):
register_by_course_id(course_id, True) register_by_course_id(course_id, True)
...@@ -108,6 +101,7 @@ def i_am_an_edx_user(step): ...@@ -108,6 +101,7 @@ def i_am_an_edx_user(step):
#### helper functions #### helper functions
@world.absorb @world.absorb
def scroll_to_bottom(): def scroll_to_bottom():
# Maximize the browser # Maximize the browser
...@@ -116,6 +110,11 @@ def scroll_to_bottom(): ...@@ -116,6 +110,11 @@ def scroll_to_bottom():
@world.absorb @world.absorb
def create_user(uname): 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 = UserFactory.build(username=uname, email=uname + '@edx.org')
portal_user.set_password('test') portal_user.set_password('test')
portal_user.save() portal_user.save()
...@@ -133,13 +132,25 @@ def log_in(email, password): ...@@ -133,13 +132,25 @@ def log_in(email, password):
world.browser.visit(django_url('/')) world.browser.visit(django_url('/'))
world.browser.is_element_present_by_css('header.global', 10) world.browser.is_element_present_by_css('header.global', 10)
world.browser.click_link_by_href('#login-modal') 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('email').fill(email)
login_form.find_by_name('password').fill(password) login_form.find_by_name('password').fill(password)
login_form.find_by_name('submit').click() login_form.find_by_name('submit').click()
# wait for the page to redraw # 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 @world.absorb
......
from lxml import etree from lxml import etree
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
class ResponseXMLFactory(object): class ResponseXMLFactory(object):
""" Abstract base class for capa response XML factories. """ Abstract base class for capa response XML factories.
Subclasses override create_response_element and Subclasses override create_response_element and
...@@ -13,7 +14,7 @@ class ResponseXMLFactory(object): ...@@ -13,7 +14,7 @@ class ResponseXMLFactory(object):
""" Subclasses override to return an etree element """ Subclasses override to return an etree element
representing the capa response XML representing the capa response XML
(e.g. <numericalresponse>). (e.g. <numericalresponse>).
The tree should NOT contain any input elements The tree should NOT contain any input elements
(such as <textline />) as these will be added later.""" (such as <textline />) as these will be added later."""
return None return None
...@@ -25,7 +26,7 @@ class ResponseXMLFactory(object): ...@@ -25,7 +26,7 @@ class ResponseXMLFactory(object):
return None return None
def build_xml(self, **kwargs): def build_xml(self, **kwargs):
""" Construct an XML string for a capa response """ Construct an XML string for a capa response
based on **kwargs. based on **kwargs.
**kwargs is a dictionary that will be passed **kwargs is a dictionary that will be passed
...@@ -37,7 +38,7 @@ class ResponseXMLFactory(object): ...@@ -37,7 +38,7 @@ class ResponseXMLFactory(object):
*question_text*: The text of the question to display, *question_text*: The text of the question to display,
wrapped in <p> tags. wrapped in <p> tags.
*explanation_text*: The detailed explanation that will *explanation_text*: The detailed explanation that will
be shown if the user answers incorrectly. be shown if the user answers incorrectly.
...@@ -75,7 +76,7 @@ class ResponseXMLFactory(object): ...@@ -75,7 +76,7 @@ class ResponseXMLFactory(object):
for i in range(0, int(num_responses)): for i in range(0, int(num_responses)):
response_element = self.create_response_element(**kwargs) response_element = self.create_response_element(**kwargs)
root.append(response_element) root.append(response_element)
# Add input elements # Add input elements
for j in range(0, int(num_inputs)): for j in range(0, int(num_inputs)):
input_element = self.create_input_element(**kwargs) input_element = self.create_input_element(**kwargs)
...@@ -135,7 +136,7 @@ class ResponseXMLFactory(object): ...@@ -135,7 +136,7 @@ class ResponseXMLFactory(object):
# Names of group elements # Names of group elements
group_element_names = {'checkbox': 'checkboxgroup', group_element_names = {'checkbox': 'checkboxgroup',
'radio': 'radiogroup', 'radio': 'radiogroup',
'multiple': 'choicegroup' } 'multiple': 'choicegroup'}
# Retrieve **kwargs # Retrieve **kwargs
choices = kwargs.get('choices', [True]) choices = kwargs.get('choices', [True])
...@@ -151,13 +152,11 @@ class ResponseXMLFactory(object): ...@@ -151,13 +152,11 @@ class ResponseXMLFactory(object):
choice_element = etree.SubElement(group_element, "choice") choice_element = etree.SubElement(group_element, "choice")
choice_element.set("correct", "true" if correct_val else "false") 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 # 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: if name:
choice_element.text = str(name)
choice_element.set("name", str(name)) choice_element.set("name", str(name))
return group_element return group_element
...@@ -217,7 +216,7 @@ class CustomResponseXMLFactory(ResponseXMLFactory): ...@@ -217,7 +216,7 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
*answer*: Inline script that calculates the answer *answer*: Inline script that calculates the answer
""" """
# Retrieve **kwargs # Retrieve **kwargs
cfn = kwargs.get('cfn', None) cfn = kwargs.get('cfn', None)
expect = kwargs.get('expect', None) expect = kwargs.get('expect', None)
...@@ -247,7 +246,7 @@ class SchematicResponseXMLFactory(ResponseXMLFactory): ...@@ -247,7 +246,7 @@ class SchematicResponseXMLFactory(ResponseXMLFactory):
def create_response_element(self, **kwargs): def create_response_element(self, **kwargs):
""" Create the <schematicresponse> XML element. """ Create the <schematicresponse> XML element.
Uses *kwargs*: Uses *kwargs*:
*answer*: The Python script used to evaluate the answer. *answer*: The Python script used to evaluate the answer.
...@@ -274,6 +273,7 @@ class SchematicResponseXMLFactory(ResponseXMLFactory): ...@@ -274,6 +273,7 @@ class SchematicResponseXMLFactory(ResponseXMLFactory):
For testing, we create a bare-bones version of <schematic>.""" For testing, we create a bare-bones version of <schematic>."""
return etree.Element("schematic") return etree.Element("schematic")
class CodeResponseXMLFactory(ResponseXMLFactory): class CodeResponseXMLFactory(ResponseXMLFactory):
""" Factory for creating <coderesponse> XML trees """ """ Factory for creating <coderesponse> XML trees """
...@@ -286,9 +286,9 @@ class CodeResponseXMLFactory(ResponseXMLFactory): ...@@ -286,9 +286,9 @@ class CodeResponseXMLFactory(ResponseXMLFactory):
def create_response_element(self, **kwargs): def create_response_element(self, **kwargs):
""" Create a <coderesponse> XML element: """ Create a <coderesponse> XML element:
Uses **kwargs: Uses **kwargs:
*initial_display*: The code that initially appears in the textbox *initial_display*: The code that initially appears in the textbox
[DEFAULT: "Enter code here"] [DEFAULT: "Enter code here"]
*answer_display*: The answer to display to the student *answer_display*: The answer to display to the student
...@@ -328,6 +328,7 @@ class CodeResponseXMLFactory(ResponseXMLFactory): ...@@ -328,6 +328,7 @@ class CodeResponseXMLFactory(ResponseXMLFactory):
# return None here # return None here
return None return None
class ChoiceResponseXMLFactory(ResponseXMLFactory): class ChoiceResponseXMLFactory(ResponseXMLFactory):
""" Factory for creating <choiceresponse> XML trees """ """ Factory for creating <choiceresponse> XML trees """
...@@ -356,13 +357,13 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): ...@@ -356,13 +357,13 @@ class FormulaResponseXMLFactory(ResponseXMLFactory):
*num_samples*: The number of times to sample the student's answer *num_samples*: The number of times to sample the student's answer
to numerically compare it to the correct answer. to numerically compare it to the correct answer.
*tolerance*: The tolerance within which answers will be accepted *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 *answer*: The answer to the problem. Can be a formula string
or a Python variable defined in a script or a Python variable defined in a script
(e.g. "$calculated_answer" for a Python variable (e.g. "$calculated_answer" for a Python variable
called calculated_answer) called calculated_answer)
[REQUIRED] [REQUIRED]
...@@ -387,7 +388,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): ...@@ -387,7 +388,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory):
# Set the sample information # Set the sample information
sample_str = self._sample_str(sample_dict, num_samples, tolerance) sample_str = self._sample_str(sample_dict, num_samples, tolerance)
response_element.set("samples", sample_str) response_element.set("samples", sample_str)
# Set the tolerance # Set the tolerance
responseparam_element = etree.SubElement(response_element, "responseparam") responseparam_element = etree.SubElement(response_element, "responseparam")
...@@ -408,7 +409,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): ...@@ -408,7 +409,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory):
# We could sample a different range, but for simplicity, # We could sample a different range, but for simplicity,
# we use the same sample string for the hints # 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("samples", sample_str)
formulahint_element.set("answer", str(hint_prompt)) formulahint_element.set("answer", str(hint_prompt))
...@@ -436,10 +437,11 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): ...@@ -436,10 +437,11 @@ class FormulaResponseXMLFactory(ResponseXMLFactory):
high_range_vals = [str(f[1]) for f in sample_dict.values()] high_range_vals = [str(f[1]) for f in sample_dict.values()]
sample_str = (",".join(sample_dict.keys()) + "@" + sample_str = (",".join(sample_dict.keys()) + "@" +
",".join(low_range_vals) + ":" + ",".join(low_range_vals) + ":" +
",".join(high_range_vals) + ",".join(high_range_vals) +
"#" + str(num_samples)) "#" + str(num_samples))
return sample_str return sample_str
class ImageResponseXMLFactory(ResponseXMLFactory): class ImageResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing <imageresponse> XML """ """ Factory for producing <imageresponse> XML """
...@@ -450,9 +452,9 @@ class ImageResponseXMLFactory(ResponseXMLFactory): ...@@ -450,9 +452,9 @@ class ImageResponseXMLFactory(ResponseXMLFactory):
def create_input_element(self, **kwargs): def create_input_element(self, **kwargs):
""" Create the <imageinput> element. """ Create the <imageinput> element.
Uses **kwargs: Uses **kwargs:
*src*: URL for the image file [DEFAULT: "/static/image.jpg"] *src*: URL for the image file [DEFAULT: "/static/image.jpg"]
*width*: Width of the image [DEFAULT: 100] *width*: Width of the image [DEFAULT: 100]
...@@ -490,7 +492,7 @@ class ImageResponseXMLFactory(ResponseXMLFactory): ...@@ -490,7 +492,7 @@ class ImageResponseXMLFactory(ResponseXMLFactory):
input_element.set("src", str(src)) input_element.set("src", str(src))
input_element.set("width", str(width)) input_element.set("width", str(width))
input_element.set("height", str(height)) input_element.set("height", str(height))
if rectangle: if rectangle:
input_element.set("rectangle", rectangle) input_element.set("rectangle", rectangle)
...@@ -499,6 +501,7 @@ class ImageResponseXMLFactory(ResponseXMLFactory): ...@@ -499,6 +501,7 @@ class ImageResponseXMLFactory(ResponseXMLFactory):
return input_element return input_element
class JavascriptResponseXMLFactory(ResponseXMLFactory): class JavascriptResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing <javascriptresponse> XML """ """ Factory for producing <javascriptresponse> XML """
...@@ -522,7 +525,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory): ...@@ -522,7 +525,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory):
# Both display_src and display_class given, # Both display_src and display_class given,
# or neither 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)) (not display_src and not display_class))
# Create the <javascriptresponse> element # Create the <javascriptresponse> element
...@@ -552,6 +555,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory): ...@@ -552,6 +555,7 @@ class JavascriptResponseXMLFactory(ResponseXMLFactory):
""" Create the <javascriptinput> element """ """ Create the <javascriptinput> element """
return etree.Element("javascriptinput") return etree.Element("javascriptinput")
class MultipleChoiceResponseXMLFactory(ResponseXMLFactory): class MultipleChoiceResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing <multiplechoiceresponse> XML """ """ Factory for producing <multiplechoiceresponse> XML """
...@@ -564,6 +568,7 @@ class MultipleChoiceResponseXMLFactory(ResponseXMLFactory): ...@@ -564,6 +568,7 @@ class MultipleChoiceResponseXMLFactory(ResponseXMLFactory):
kwargs['choice_type'] = 'multiple' kwargs['choice_type'] = 'multiple'
return ResponseXMLFactory.choicegroup_input_xml(**kwargs) return ResponseXMLFactory.choicegroup_input_xml(**kwargs)
class TrueFalseResponseXMLFactory(ResponseXMLFactory): class TrueFalseResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing <truefalseresponse> XML """ """ Factory for producing <truefalseresponse> XML """
...@@ -576,6 +581,7 @@ class TrueFalseResponseXMLFactory(ResponseXMLFactory): ...@@ -576,6 +581,7 @@ class TrueFalseResponseXMLFactory(ResponseXMLFactory):
kwargs['choice_type'] = 'multiple' kwargs['choice_type'] = 'multiple'
return ResponseXMLFactory.choicegroup_input_xml(**kwargs) return ResponseXMLFactory.choicegroup_input_xml(**kwargs)
class OptionResponseXMLFactory(ResponseXMLFactory): class OptionResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing <optionresponse> XML""" """ Factory for producing <optionresponse> XML"""
...@@ -620,7 +626,7 @@ class StringResponseXMLFactory(ResponseXMLFactory): ...@@ -620,7 +626,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
def create_response_element(self, **kwargs): def create_response_element(self, **kwargs):
""" Create a <stringresponse> XML element. """ Create a <stringresponse> XML element.
Uses **kwargs: Uses **kwargs:
*answer*: The correct answer (a string) [REQUIRED] *answer*: The correct answer (a string) [REQUIRED]
...@@ -642,7 +648,7 @@ class StringResponseXMLFactory(ResponseXMLFactory): ...@@ -642,7 +648,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
# Create the <stringresponse> element # Create the <stringresponse> element
response_element = etree.Element("stringresponse") response_element = etree.Element("stringresponse")
# Set the answer attribute # Set the answer attribute
response_element.set("answer", str(answer)) response_element.set("answer", str(answer))
# Set the case sensitivity # Set the case sensitivity
...@@ -667,6 +673,7 @@ class StringResponseXMLFactory(ResponseXMLFactory): ...@@ -667,6 +673,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
def create_input_element(self, **kwargs): def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs) return ResponseXMLFactory.textline_input_xml(**kwargs)
class AnnotationResponseXMLFactory(ResponseXMLFactory): class AnnotationResponseXMLFactory(ResponseXMLFactory):
""" Factory for creating <annotationresponse> XML trees """ """ Factory for creating <annotationresponse> XML trees """
def create_response_element(self, **kwargs): def create_response_element(self, **kwargs):
...@@ -679,17 +686,17 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory): ...@@ -679,17 +686,17 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory):
input_element = etree.Element("annotationinput") input_element = etree.Element("annotationinput")
text_children = [ text_children = [
{'tag': 'title', 'text': kwargs.get('title', 'super cool annotation') }, {'tag': 'title', 'text': kwargs.get('title', 'super cool annotation')},
{'tag': 'text', 'text': kwargs.get('text', 'texty text') }, {'tag': 'text', 'text': kwargs.get('text', 'texty text')},
{'tag': 'comment', 'text':kwargs.get('comment', 'blah blah erudite comment blah blah') }, {'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': 'comment_prompt', 'text': kwargs.get('comment_prompt', 'type a commentary below')},
{'tag': 'tag_prompt', 'text': kwargs.get('tag_prompt', 'select one tag') } {'tag': 'tag_prompt', 'text': kwargs.get('tag_prompt', 'select one tag')}
] ]
for child in text_children: for child in text_children:
etree.SubElement(input_element, child['tag']).text = child['text'] 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 = kwargs.get('options', default_options)
options_element = etree.SubElement(input_element, 'options') options_element = etree.SubElement(input_element, 'options')
...@@ -698,4 +705,3 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory): ...@@ -698,4 +705,3 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory):
option_element.text = description option_element.text = description
return input_element return input_element
...@@ -8,41 +8,66 @@ from xmodule.raw_module import RawDescriptor ...@@ -8,41 +8,66 @@ from xmodule.raw_module import RawDescriptor
from .x_module import XModule from .x_module import XModule
from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float, List 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 xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload", 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", 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 V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES
VERSION_TUPLES = ( VersionTuple = namedtuple('VersionTuple', ['descriptor', 'module', 'settings_attributes', 'student_attributes'])
('1', CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, V1_STUDENT_ATTRIBUTES), VERSION_TUPLES = {
) 1: VersionTuple(CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES,
V1_STUDENT_ATTRIBUTES),
}
DEFAULT_VERSION = 1 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): class CombinedOpenEndedFields(object):
display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings) 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) 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) 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) state = String(help="Which step within the current task that the student is on.", default="initial",
student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state) 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) 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) 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) 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) accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False,
skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True, scope=Scope.settings) 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) 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) 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) data = String(help="XML data for the problem", scope=Scope.content)
...@@ -130,23 +155,10 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): ...@@ -130,23 +155,10 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
if self.task_states is None: if self.task_states is None:
self.task_states = [] self.task_states = []
versions = [i[0] for i in VERSION_TUPLES] version_tuple = VERSION_TUPLES[self.version]
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"
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.student_attributes = version_tuple.student_attributes
self.settings_attributes = settings_attributes[version_index] self.settings_attributes = version_tuple.settings_attributes
attributes = self.student_attributes + self.settings_attributes attributes = self.student_attributes + self.settings_attributes
...@@ -154,10 +166,11 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): ...@@ -154,10 +166,11 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
'rewrite_content_links': self.rewrite_content_links, 'rewrite_content_links': self.rewrite_content_links,
} }
instance_state = {k: getattr(self, k) for k in attributes} instance_state = {k: getattr(self, k) for k in attributes}
self.child_descriptor = descriptors[version_index](self.system) self.child_descriptor = version_tuple.descriptor(self.system)
self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(self.data), self.system) self.child_definition = version_tuple.descriptor.definition_from_xml(etree.fromstring(self.data), self.system)
self.child_module = modules[version_index](self.system, location, self.child_definition, self.child_descriptor, 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) instance_state=instance_state, static_data=static_data,
attributes=attributes)
self.save_instance_data() self.save_instance_data()
def get_html(self): def get_html(self):
......
...@@ -131,6 +131,7 @@ section.poll_question { ...@@ -131,6 +131,7 @@ section.poll_question {
box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset; box-shadow: rgb(97, 184, 225) 0px 1px 0px 0px inset;
color: rgb(255, 255, 255); color: rgb(255, 255, 255);
text-shadow: rgb(7, 103, 148) 0px 1px 0px; text-shadow: rgb(7, 103, 148) 0px 1px 0px;
background-image: none;
} }
.text { .text {
......
...@@ -303,6 +303,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -303,6 +303,7 @@ class MongoModuleStore(ModuleStoreBase):
# this is likely a leaf node, so let's record what metadata we need to inherit # this is likely a leaf node, so let's record what metadata we need to inherit
metadata_to_inherit[child] = my_metadata metadata_to_inherit[child] = my_metadata
if root is not None: if root is not None:
_compute_inherited_metadata(root) _compute_inherited_metadata(root)
...@@ -330,7 +331,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -330,7 +331,7 @@ class MongoModuleStore(ModuleStoreBase):
return tree return tree
def clear_cached_metadata_inheritance_tree(self, location): 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: if self.metadata_inheritance_cache is not None:
self.metadata_inheritance_cache.delete(key_name) self.metadata_inheritance_cache.delete(key_name)
...@@ -387,12 +388,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -387,12 +388,7 @@ class MongoModuleStore(ModuleStoreBase):
resource_fs = OSFS(root) resource_fs = OSFS(root)
metadata_inheritance_tree = None metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location']))
# 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']))
# TODO (cdodge): When the 'split module store' work has been completed, we should remove # TODO (cdodge): When the 'split module store' work has been completed, we should remove
# the 'metadata_inheritance_tree' parameter # the 'metadata_inheritance_tree' parameter
...@@ -497,7 +493,10 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -497,7 +493,10 @@ class MongoModuleStore(ModuleStoreBase):
try: try:
source_item = self.collection.find_one(location_to_query(source)) source_item = self.collection.find_one(location_to_query(source))
source_item['_id'] = Location(location).dict() 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] 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 # 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): ...@@ -560,6 +559,9 @@ class MongoModuleStore(ModuleStoreBase):
{'$set': update}, {'$set': update},
multi=False, multi=False,
upsert=True, 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: if result['n'] == 0:
raise ItemNotFoundError(location) raise ItemNotFoundError(location)
...@@ -612,7 +614,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -612,7 +614,7 @@ class MongoModuleStore(ModuleStoreBase):
self._update_single_item(location, {'metadata': metadata}) self._update_single_item(location, {'metadata': metadata})
# recompute (and update) the metadata inheritance tree which is cached # 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): def delete_item(self, location):
""" """
...@@ -630,9 +632,12 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -630,9 +632,12 @@ class MongoModuleStore(ModuleStoreBase):
course.tabs = [tab for tab in existing_tabs if tab.get('url_slug') != location.name] course.tabs = [tab for tab in existing_tabs if tab.get('url_slug') != location.name]
self.update_metadata(course.location, own_metadata(course)) 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 # 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): def get_parent_locations(self, location, course_id):
......
...@@ -128,7 +128,9 @@ if Backbone? ...@@ -128,7 +128,9 @@ if Backbone?
type: "POST" type: "POST"
success: (response, textStatus) => success: (response, textStatus) =>
if textStatus == 'success' if textStatus == 'success'
@model.set('pinned', true) @model.set('pinned', true)
error: =>
$('.admin-pin').text("Pinning not currently available")
unPin: -> unPin: ->
url = @model.urlFor("unPinThread") url = @model.urlFor("unPinThread")
......
...@@ -13,14 +13,19 @@ ...@@ -13,14 +13,19 @@
<script src="{% static 'js/vendor/jasmine-jquery.js' %}"></script> <script src="{% static 'js/vendor/jasmine-jquery.js' %}"></script>
<script src="{% static 'console-runner.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 #} {# source files #}
{% for url in suite.js_files %} {% for url in suite.js_files %}
<script src="{{ url }}"></script> <script src="{{ url }}"></script>
{% endfor %} {% endfor %}
{% load compressed %}
{# static files #}
{% compressed_js 'js-test-source' %}
{# spec files #} {# spec files #}
{% compressed_js 'spec' %} {% compressed_js 'spec' %}
......
...@@ -5,6 +5,10 @@ from lettuce.django import django_url ...@@ -5,6 +5,10 @@ from lettuce.django import django_url
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from student.models import CourseEnrollment 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 import time
from logging import getLogger from logging import getLogger
...@@ -81,14 +85,57 @@ def i_am_not_logged_in(step): ...@@ -81,14 +85,57 @@ def i_am_not_logged_in(step):
world.browser.cookies.delete() world.browser.cookies.delete()
@step(u'I am registered for a course$') TEST_COURSE_ORG = 'edx'
def i_am_registered_for_a_course(step): 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') world.create_user('robot')
u = User.objects.get(username='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') 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$') @step(u'I am an edX user$')
def i_am_an_edx_user(step): def i_am_an_edx_user(step):
world.create_user('robot') world.create_user('robot')
...@@ -97,3 +144,37 @@ def i_am_an_edx_user(step): ...@@ -97,3 +144,37 @@ def i_am_an_edx_user(step):
@step(u'User "([^"]*)" is an edX user$') @step(u'User "([^"]*)" is an edX user$')
def registered_edx_user(step, uname): def registered_edx_user(step, uname):
world.create_user(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__) ...@@ -9,6 +9,7 @@ logger = getLogger(__name__)
## support functions ## support functions
def get_courses(): def get_courses():
''' '''
Returns dict of lists of courses available, keyed by course.org (ie university). Returns dict of lists of courses available, keyed by course.org (ie university).
......
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 ...@@ -3,21 +3,18 @@ Feature: All the high level tabs should work
As a student As a student
I want to navigate through the high level tabs I want to navigate through the high level tabs
# Note this didn't work as a scenario outline because Scenario: I can navigate to all high -level tabs in a course
# before each scenario was not flushing the database Given: I am registered for the course "6.002x"
# TODO: break this apart so that if one fails the others And The course "6.002x" has extra tab "Custom Tab"
# will still run And I log in
Scenario: A student can see all tabs of the course And I click on View Courseware
Given I am registered for a course When I click on the "<TabName>" tab
And I log in Then the page title should contain "<PageTitle>"
And I click on View Courseware
When I click on the "Courseware" tab Examples:
Then the page title should be "6.002x Courseware" | TabName | PageTitle |
When I click on the "Course Info" tab | Courseware | 6.002x Courseware |
Then the page title should be "6.002x Course Info" | Course Info | 6.002x Course Info |
When I click on the "Textbook" tab | Custom Tab | 6.002x Custom Tab |
Then the page title should be "6.002x Textbook" | Wiki | edX Wiki |
When I click on the "Wiki" tab | Progress | 6.002x Progress |
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"
...@@ -39,9 +39,9 @@ Feature: Homepage for web users ...@@ -39,9 +39,9 @@ Feature: Homepage for web users
| MITx | | MITx |
| HarvardX | | HarvardX |
| BerkeleyX | | BerkeleyX |
| UTx | | UTx |
| WellesleyX | | WellesleyX |
| GeorgetownX | | GeorgetownX |
# # TODO: Add scenario that tests the courses available # # TODO: Add scenario that tests the courses available
# # using a policy or a configuration file # # using a policy or a configuration file
...@@ -34,6 +34,7 @@ def click_the_dropdown(step): ...@@ -34,6 +34,7 @@ def click_the_dropdown(step):
#### helper functions #### helper functions
def user_is_an_unactivated_user(uname): def user_is_an_unactivated_user(uname):
u = User.objects.get(username=uname) u = User.objects.get(username=uname)
u.is_active = False u.is_active = False
......
...@@ -3,10 +3,10 @@ Feature: Open ended grading ...@@ -3,10 +3,10 @@ Feature: Open ended grading
In order to complete the courseware questions In order to complete the courseware questions
I want the machine learning grading to be functional 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 # make a reference implementation for a course with
# an open ended grading problem that is always available # an open ended grading problem that is always available
# #
# Scenario: An answer that is too short is rejected # Scenario: An answer that is too short is rejected
# Given I navigate to an openended question # Given I navigate to an openended question
# And I enter the answer "z" # And I enter the answer "z"
......
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 |
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 ...@@ -4,13 +4,14 @@ Feature: Register for a course
I want to register for a class on the edX website I want to register for a class on the edX website
Scenario: I can register for a course 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 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 Then I should see the course numbered "6.002x" in my dashboard
Scenario: I can unregister for a course 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 And I visit the dashboard
When I click the link with the text "Unregister" When I click the link with the text "Unregister"
And I press the "Unregister" button in the Unenroll dialog And I press the "Unregister" button in the Unenroll dialog
......
from lettuce import world, step 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): def i_register_for_the_course(step, course):
courses_section = world.browser.find_by_css('section.courses') cleaned_name = TEST_COURSE_NAME.replace(' ', '_')
course_link_css = 'article[id*="%s"] > div' % course url = django_url('courses/%s/%s/%s/about' % (TEST_COURSE_ORG, course, cleaned_name))
course_link = courses_section.find_by_css(course_link_css).first world.browser.visit(url)
course_link.click()
intro_section = world.browser.find_by_css('section.intro') intro_section = world.browser.find_by_css('section.intro')
register_link = intro_section.find_by_css('a.register') register_link = intro_section.find_by_css('a.register')
......
...@@ -60,4 +60,4 @@ Feature: There are courses on the homepage ...@@ -60,4 +60,4 @@ Feature: There are courses on the homepage
# Scenario: Navigate through course BerkeleyX/CS184.1x/2012_Fall # Scenario: Navigate through course BerkeleyX/CS184.1x/2012_Fall
# Given I am registered for course "BerkeleyX/CS184.1x/2012_Fall" # Given I am registered for course "BerkeleyX/CS184.1x/2012_Fall"
# And I log in # And I log in
# Then I verify all the content of each course # Then I verify all the content of each course
\ No newline at end of file
...@@ -159,6 +159,7 @@ def grade(student, request, course, model_data_cache=None, keep_raw_scores=False ...@@ -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% # 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']: for moduledescriptor in section['xmoduledescriptors']:
# Create a fake key to pull out a StudentModule object from the ModelDataCache # Create a fake key to pull out a StudentModule object from the ModelDataCache
key = LmsKeyValueStore.Key( key = LmsKeyValueStore.Key(
Scope.student_state, Scope.student_state,
student.id, student.id,
......
...@@ -116,6 +116,10 @@ def create_thread(request, course_id, commentable_id): ...@@ -116,6 +116,10 @@ def create_thread(request, course_id, commentable_id):
thread.save() 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': if post.get('auto_subscribe', 'false').lower() == 'true':
user = cc.User.from_django_user(request.user) user = cc.User.from_django_user(request.user)
user.follow(thread) user.follow(thread)
......
...@@ -98,6 +98,11 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG ...@@ -98,6 +98,11 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
else: else:
thread['group_name'] = "" thread['group_name'] = ""
thread['group_string'] = "This post visible to everyone." 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['page'] = page
query_params['num_pages'] = num_pages query_params['num_pages'] = num_pages
...@@ -245,6 +250,11 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -245,6 +250,11 @@ def single_thread(request, course_id, discussion_id, thread_id):
try: try:
thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id) 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: except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
log.error("Error loading single thread.") log.error("Error loading single thread.")
raise Http404 raise Http404
...@@ -285,6 +295,10 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -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'): 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 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] threads = [utils.safe_content(thread) for thread in threads]
#recent_active_threads = cc.search_recent_active_threads( #recent_active_threads = cc.search_recent_active_threads(
......
...@@ -92,9 +92,15 @@ def instructor_dashboard(request, course_id): ...@@ -92,9 +92,15 @@ def instructor_dashboard(request, course_id):
data += compute_course_stats(course).items() data += compute_course_stats(course).items()
if request.user.is_staff: if request.user.is_staff:
for field in course.fields: for field in course.fields:
if getattr(field.scope, 'student', False):
continue
data.append([field.name, json.dumps(field.read_json(course))]) data.append([field.name, json.dumps(field.read_json(course))])
for namespace in course.namespaces: for namespace in course.namespaces:
for field in getattr(course, namespace).fields: 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))]) data.append(["{}.{}".format(namespace, field.name), json.dumps(field.read_json(course))])
datatable['data'] = data datatable['data'] = data
......
...@@ -8,16 +8,24 @@ from .test import * ...@@ -8,16 +8,24 @@ from .test import *
# otherwise the browser will not render the pages correctly # otherwise the browser will not render the pages correctly
DEBUG = True DEBUG = True
# Show the courses that are in the data directory # Use the mongo store for acceptance tests
COURSES_ROOT = ENV_ROOT / "data" modulestore_options = {
DATA_DIR = COURSES_ROOT '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 = { MODULESTORE = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': { 'OPTIONS': modulestore_options
'data_dir': DATA_DIR, },
'default_class': 'xmodule.hidden_module.HiddenDescriptor', 'direct': {
} 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options
} }
} }
......
...@@ -5,8 +5,5 @@ ...@@ -5,8 +5,5 @@
"/static/js/vendor/jquery-ui.min.js", "/static/js/vendor/jquery-ui.min.js",
"/static/js/vendor/jquery.leanModal.min.js", "/static/js/vendor/jquery.leanModal.min.js",
"/static/js/vendor/flot/jquery.flot.js" "/static/js/vendor/flot/jquery.flot.js"
],
"static_files": [
"js/application.js"
] ]
} }
...@@ -4,9 +4,6 @@ describe 'Calculator', -> ...@@ -4,9 +4,6 @@ describe 'Calculator', ->
@calculator = new Calculator @calculator = new Calculator
describe 'bind', -> describe 'bind', ->
beforeEach ->
Calculator.bind()
it 'bind the calculator button', -> it 'bind the calculator button', ->
expect($('.calc')).toHandleWith 'click', @calculator.toggle expect($('.calc')).toHandleWith 'click', @calculator.toggle
...@@ -31,12 +28,19 @@ describe 'Calculator', -> ...@@ -31,12 +28,19 @@ describe 'Calculator', ->
$('form#calculator').submit() $('form#calculator').submit()
describe 'toggle', -> describe 'toggle', ->
it 'toggle the calculator and focus the input', -> it 'focuses the input when toggled', ->
spyOn $.fn, 'focus'
@calculator.toggle(jQuery.Event("click")) # 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
expect($('li.calc-main')).toHaveClass('open') runs ->
expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled() expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled()
it 'toggle the close button on the calculator button', -> it 'toggle the close button on the calculator button', ->
@calculator.toggle(jQuery.Event("click")) @calculator.toggle(jQuery.Event("click"))
......
...@@ -22,18 +22,23 @@ describe 'Tab', -> ...@@ -22,18 +22,23 @@ describe 'Tab', ->
it 'bind the tabs', -> it 'bind the tabs', ->
expect($.fn.tabs).toHaveBeenCalledWith show: @tab.onShow 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', -> describe 'onShow', ->
beforeEach -> beforeEach ->
@tab = new Tab 1, @items @tab = new Tab 1, @items
$('[href="#tab-1-0"]').click() @tab.onShow($('#tab-1-0'), {'index': 1})
it 'replace content in the container', -> 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-0').html()).toEqual ''
expect($('#tab-1-1').html()).toEqual 'Video 2' expect($('#tab-1-1').html()).toEqual 'Video 2'
expect($('#tab-1-2').html()).toEqual '' expect($('#tab-1-2').html()).toEqual ''
it 'trigger contentChanged event on the element', -> it 'trigger contentChanged event on the element', ->
spyOnEvent @tab.el, 'contentChanged' spyOnEvent @tab.el, 'contentChanged'
$('[href="#tab-1-1"]').click() @tab.onShow($('#tab-1-1'), {'index': 1})
expect('contentChanged').toHaveBeenTriggeredOn @tab.el expect('contentChanged').toHaveBeenTriggeredOn @tab.el
...@@ -32,11 +32,9 @@ describe 'Navigation', -> ...@@ -32,11 +32,9 @@ describe 'Navigation', ->
heightStyle: 'content' heightStyle: 'content'
it 'binds the accordionchange event', -> it 'binds the accordionchange event', ->
Navigation.bind()
expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log
it 'bind the navigation toggle', -> it 'bind the navigation toggle', ->
Navigation.bind()
expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle
describe 'when the #accordion does not exists', -> describe 'when the #accordion does not exists', ->
...@@ -45,7 +43,6 @@ describe 'Navigation', -> ...@@ -45,7 +43,6 @@ describe 'Navigation', ->
it 'does not activate the accordion', -> it 'does not activate the accordion', ->
spyOn $.fn, 'accordion' spyOn $.fn, 'accordion'
Navigation.bind()
expect($('#accordion').accordion).wasNotCalled() expect($('#accordion').accordion).wasNotCalled()
describe 'toggle', -> describe 'toggle', ->
......
...@@ -45,10 +45,10 @@ ...@@ -45,10 +45,10 @@
</header> </header>
<div class="post-body">${'<%- body %>'}</div> <div class="post-body">${'<%- body %>'}</div>
% if course and has_permission(user, 'openclose_thread', course.id): % 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"> <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> <i class="icon"></i><span class="pin-label">Pin Thread</span></div>
%else: %else:
${"<% if (pinned) { %>"} ${"<% if (pinned) { %>"}
<div class="discussion-pin notpinned" data-role="thread-pin" data-tooltip="pin this thread"> <div class="discussion-pin notpinned" data-role="thread-pin" data-tooltip="pin this thread">
...@@ -57,9 +57,6 @@ ...@@ -57,9 +57,6 @@
% endif % endif
${'<% if (obj.courseware_url) { %>'} ${'<% if (obj.courseware_url) { %>'}
<div class="post-context"> <div class="post-context">
(this post is about <a href="${'<%- courseware_url%>'}">${'<%- courseware_title %>'}</a>) (this post is about <a href="${'<%- courseware_url%>'}">${'<%- courseware_title %>'}</a>)
......
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 from xmodule.fields import Date, Timedelta
class StringyBoolean(Boolean): 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): def from_json(self, value):
if isinstance(value, basestring): if isinstance(value, basestring):
return value.lower() == 'true' return value.lower() == 'true'
return value return value
class StringyFloat(Float): class StringyFloat(Float):
"""
Reads values as floats. If the value parses as a float, returns
that, otherwise returns None
"""
def from_json(self, value): def from_json(self, value):
try: try:
return float(value) return float(value)
...@@ -17,6 +31,9 @@ class StringyFloat(Float): ...@@ -17,6 +31,9 @@ class StringyFloat(Float):
class LmsNamespace(Namespace): class LmsNamespace(Namespace):
"""
Namespace that defines fields common to all blocks used in the LMS
"""
hide_from_toc = StringyBoolean( hide_from_toc = StringyBoolean(
help="Whether to display this module in the table of contents", help="Whether to display this module in the table of contents",
default=False, default=False,
...@@ -38,8 +55,14 @@ class LmsNamespace(Namespace): ...@@ -38,8 +55,14 @@ class LmsNamespace(Namespace):
source_file = String(help="DO NOT USE", scope=Scope.settings) source_file = String(help="DO NOT USE", scope=Scope.settings)
xqa_key = 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) 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") 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) 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
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment