Commit 481aa063 by Brian Talbot

Merge branch 'master' into feature/btalbot/studio-checklists

parents 38df54dc 93eebdcd
...@@ -41,7 +41,8 @@ disable= ...@@ -41,7 +41,8 @@ disable=
# R0902: Too many instance attributes # R0902: Too many instance attributes
# R0903: Too few public methods (1/2) # R0903: Too few public methods (1/2)
# R0904: Too many public methods # R0904: Too many public methods
W0141,W0142,R0201,R0901,R0902,R0903,R0904 # R0913: Too many arguments
W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913
[REPORTS] [REPORTS]
......
...@@ -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
...@@ -142,8 +142,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -142,8 +142,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
onDelete: function(event) { onDelete: function(event) {
event.preventDefault(); event.preventDefault();
// TODO ask for confirmation
// remove the dom element and delete the model if (!confirm('Are you sure you want to delete this update? This action cannot be undone.')) {
return;
}
var targetModel = this.eventModel(event); var targetModel = this.eventModel(event);
this.modelDom(event).remove(); this.modelDom(event).remove();
var cacheThis = this; var cacheThis = this;
......
...@@ -15,6 +15,24 @@ from .models import CourseUserGroup ...@@ -15,6 +15,24 @@ from .models import CourseUserGroup
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# tl;dr: global state is bad. capa reseeds random every time a problem is loaded. Even
# if and when that's fixed, it's a good idea to have a local generator to avoid any other
# code that messes with the global random module.
_local_random = None
def local_random():
"""
Get the local random number generator. In a function so that we don't run
random.Random() at import time.
"""
# ironic, isn't it?
global _local_random
if _local_random is None:
_local_random = random.Random()
return _local_random
def is_course_cohorted(course_id): def is_course_cohorted(course_id):
""" """
Given a course id, return a boolean for whether or not the course is Given a course id, return a boolean for whether or not the course is
...@@ -129,13 +147,7 @@ def get_cohort(user, course_id): ...@@ -129,13 +147,7 @@ def get_cohort(user, course_id):
return None return None
# Put user in a random group, creating it if needed # Put user in a random group, creating it if needed
choice = random.randrange(0, n) group_name = local_random().choice(choices)
group_name = choices[choice]
# Victor: we are seeing very strange behavior on prod, where almost all users
# end up in the same group. Log at INFO to try to figure out what's going on.
log.info("DEBUG: adding user {0} to cohort {1}. choice={2}".format(
user, group_name,choice))
group, created = CourseUserGroup.objects.get_or_create( group, created = CourseUserGroup.objects.get_or_create(
course_id=course_id, course_id=course_id,
......
...@@ -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()])
......
...@@ -9,6 +9,7 @@ from bs4 import BeautifulSoup ...@@ -9,6 +9,7 @@ from bs4 import BeautifulSoup
import time import time
import re import re
import os.path import os.path
from selenium.common.exceptions import WebDriverException
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -69,6 +70,11 @@ def the_page_title_should_be(step, title): ...@@ -69,6 +70,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 +86,6 @@ def i_am_not_logged_in(step): ...@@ -80,18 +86,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 +102,7 @@ def i_am_an_edx_user(step): ...@@ -108,6 +102,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 +111,11 @@ def scroll_to_bottom(): ...@@ -116,6 +111,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 +133,25 @@ def log_in(email, password): ...@@ -133,13 +133,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
...@@ -203,3 +215,15 @@ def save_the_course_content(path='/tmp'): ...@@ -203,3 +215,15 @@ def save_the_course_content(path='/tmp'):
f = open('%s/%s' % (path, filename), 'w') f = open('%s/%s' % (path, filename), 'w')
f.write(output) f.write(output)
f.close f.close
@world.absorb
def css_click(css_selector):
try:
world.browser.find_by_css(css_selector).click()
except WebDriverException:
# Occassionally, MathJax or other JavaScript can cover up
# an element temporarily.
# If this happens, wait a second, then try again
time.sleep(1)
world.browser.find_by_css(css_selector).click()
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
...@@ -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
...@@ -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 """
...@@ -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 """
...@@ -440,6 +441,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory): ...@@ -440,6 +441,7 @@ class FormulaResponseXMLFactory(ResponseXMLFactory):
"#" + 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 """
...@@ -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 """
...@@ -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"""
...@@ -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
...@@ -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 {
......
...@@ -8,7 +8,7 @@ from collections import namedtuple ...@@ -8,7 +8,7 @@ from collections import namedtuple
from fs.osfs import OSFS from fs.osfs import OSFS
from itertools import repeat from itertools import repeat
from path import path from path import path
from datetime import datetime, timedelta from datetime import datetime
from importlib import import_module from importlib import import_module
from xmodule.errortracker import null_error_tracker, exc_info_to_str from xmodule.errortracker import null_error_tracker, exc_info_to_str
...@@ -246,6 +246,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -246,6 +246,7 @@ class MongoModuleStore(ModuleStoreBase):
self.fs_root = path(fs_root) self.fs_root = path(fs_root)
self.error_tracker = error_tracker self.error_tracker = error_tracker
self.render_template = render_template self.render_template = render_template
self.ignore_write_events_on_courses = []
def get_metadata_inheritance_tree(self, location): def get_metadata_inheritance_tree(self, location):
''' '''
...@@ -303,6 +304,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -303,6 +304,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)
...@@ -329,6 +331,11 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -329,6 +331,11 @@ class MongoModuleStore(ModuleStoreBase):
return tree return tree
def refresh_cached_metadata_inheritance_tree(self, location):
pseudo_course_id = '/'.join([location.org, location.course])
if pseudo_course_id not in self.ignore_write_events_on_courses:
self.get_cached_metadata_inheritance_tree(location, force_refresh = True)
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:
...@@ -375,7 +382,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -375,7 +382,7 @@ class MongoModuleStore(ModuleStoreBase):
return data return data
def _load_item(self, item, data_cache): def _load_item(self, item, data_cache, should_apply_metadata_inheritence=True):
""" """
Load an XModuleDescriptor from item, using the children stored in data_cache Load an XModuleDescriptor from item, using the children stored in data_cache
""" """
...@@ -389,9 +396,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -389,9 +396,7 @@ class MongoModuleStore(ModuleStoreBase):
metadata_inheritance_tree = None metadata_inheritance_tree = None
# if we are loading a course object, there is no parent to inherit the metadata from if should_apply_metadata_inheritence:
# 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 # TODO (cdodge): When the 'split module store' work has been completed, we should remove
...@@ -414,7 +419,10 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -414,7 +419,10 @@ class MongoModuleStore(ModuleStoreBase):
""" """
data_cache = self._cache_children(items, depth) data_cache = self._cache_children(items, depth)
return [self._load_item(item, data_cache) for item in items] # if we are loading a course object, if we're not prefetching children (depth != 0) then don't
# bother with the metadata inheritence
return [self._load_item(item, data_cache,
should_apply_metadata_inheritence=(item['location']['category'] != 'course' or depth != 0)) for item in items]
def get_courses(self): def get_courses(self):
''' '''
...@@ -497,7 +505,12 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -497,7 +505,12 @@ 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
...@@ -519,7 +532,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -519,7 +532,7 @@ class MongoModuleStore(ModuleStoreBase):
raise DuplicateItemError(location) raise DuplicateItemError(location)
# 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.refresh_cached_metadata_inheritance_tree(Location(location))
def get_course_for_item(self, location, depth=0): def get_course_for_item(self, location, depth=0):
''' '''
...@@ -560,6 +573,9 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -560,6 +573,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)
...@@ -586,7 +602,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -586,7 +602,7 @@ class MongoModuleStore(ModuleStoreBase):
self._update_single_item(location, {'definition.children': children}) self._update_single_item(location, {'definition.children': children})
# 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.refresh_cached_metadata_inheritance_tree(Location(location))
def update_metadata(self, location, metadata): def update_metadata(self, location, metadata):
""" """
...@@ -612,7 +628,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -612,7 +628,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.refresh_cached_metadata_inheritance_tree(loc)
def delete_item(self, location): def delete_item(self, location):
""" """
...@@ -630,10 +646,12 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -630,10 +646,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.refresh_cached_metadata_inheritance_tree(Location(location))
def get_parent_locations(self, location, course_id): def get_parent_locations(self, location, course_id):
'''Find all locations that are the parents of this location in this '''Find all locations that are the parents of this location in this
......
...@@ -201,6 +201,17 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -201,6 +201,17 @@ def import_from_xml(store, data_dir, course_dirs=None,
course_items = [] course_items = []
for course_id in module_store.modules.keys(): for course_id in module_store.modules.keys():
if target_location_namespace is not None:
pseudo_course_id = '/'.join([target_location_namespace.org, target_location_namespace.course])
else:
course_id_components = course_id.split('/')
pseudo_course_id = '/'.join([course_id_components[0], course_id_components[1]])
try:
# turn off all write signalling while importing as this is a high volume operation
if pseudo_course_id not in store.ignore_write_events_on_courses:
store.ignore_write_events_on_courses.append(pseudo_course_id)
course_data_path = None course_data_path = None
course_location = None course_location = None
...@@ -295,6 +306,12 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -295,6 +306,12 @@ def import_from_xml(store, data_dir, course_dirs=None,
# NOTE: It's important to use own_metadata here to avoid writing # NOTE: It's important to use own_metadata here to avoid writing
# inherited metadata everywhere. # inherited metadata everywhere.
store.update_metadata(module.location, dict(own_metadata(module))) store.update_metadata(module.location, dict(own_metadata(module)))
finally:
# turn back on all write signalling
if pseudo_course_id in store.ignore_write_events_on_courses:
store.ignore_write_events_on_courses.remove(pseudo_course_id)
store.refresh_cached_metadata_inheritance_tree(target_location_namespace if
target_location_namespace is not None else course_location)
return module_store, course_items return module_store, course_items
......
...@@ -129,6 +129,8 @@ if Backbone? ...@@ -129,6 +129,8 @@ if Backbone?
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")
......
...@@ -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
Scenario: A student can see all tabs of the course
Given I am registered for a course
And I log in And I log in
And I click on View Courseware And I click on View Courseware
When I click on the "Courseware" tab When I click on the "<TabName>" tab
Then the page title should be "6.002x Courseware" Then the page title should contain "<PageTitle>"
When I click on the "Course Info" tab
Then the page title should be "6.002x Course Info" Examples:
When I click on the "Textbook" tab | TabName | PageTitle |
Then the page title should be "6.002x Textbook" | Courseware | 6.002x Courseware |
When I click on the "Wiki" tab | Course Info | 6.002x Course Info |
Then the page title should be "6.002x | edX Wiki" | Custom Tab | 6.002x Custom Tab |
When I click on the "Progress" tab | Wiki | edX Wiki |
Then the page title should be "6.002x Progress" | Progress | 6.002x Progress |
...@@ -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
......
Feature: Answer problems
As a student in an edX course
In order to test my understanding of the material
I want to answer problems
Scenario: I can answer a problem correctly
Given External graders respond "correct"
And 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 |
| code |
Scenario: I can answer a problem incorrectly
Given External graders respond "incorrect"
And 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 |
| code |
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 |
...@@ -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')
......
from courseware.mock_xqueue_server.mock_xqueue_server import MockXQueueServer
from lettuce import before, after, world
from django.conf import settings
import threading
@before.all
def setup_mock_xqueue_server():
# Retrieve the local port from settings
server_port = settings.XQUEUE_PORT
# Create the mock server instance
server = MockXQueueServer(server_port)
# Start the server running in a separate daemon thread
# Because the thread is a daemon, it will terminate
# when the main thread terminates.
server_thread = threading.Thread(target=server.serve_forever)
server_thread.daemon = True
server_thread.start()
# Store the server instance in lettuce's world
# so that other steps can access it
# (and we can shut it down later)
world.xqueue_server = server
@after.all
def teardown_mock_xqueue_server(total):
# Stop the xqueue server and free up the port
world.xqueue_server.shutdown()
...@@ -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,
......
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
import json
import urllib
import urlparse
import threading
from logging import getLogger
logger = getLogger(__name__)
class MockXQueueRequestHandler(BaseHTTPRequestHandler):
'''
A handler for XQueue POST requests.
'''
protocol = "HTTP/1.0"
def do_HEAD(self):
self._send_head()
def do_POST(self):
'''
Handle a POST request from the client
Sends back an immediate success/failure response.
It then POSTS back to the client
with grading results, as configured in MockXQueueServer.
'''
self._send_head()
# Retrieve the POST data
post_dict = self._post_dict()
# Log the request
logger.debug("XQueue received POST request %s to path %s" %
(str(post_dict), self.path))
# Respond only to grading requests
if self._is_grade_request():
try:
xqueue_header = json.loads(post_dict['xqueue_header'])
xqueue_body = json.loads(post_dict['xqueue_body'])
callback_url = xqueue_header['lms_callback_url']
except KeyError:
# If the message doesn't have a header or body,
# then it's malformed.
# Respond with failure
error_msg = "XQueue received invalid grade request"
self._send_immediate_response(False, message=error_msg)
except ValueError:
# If we could not decode the body or header,
# respond with failure
error_msg = "XQueue could not decode grade request"
self._send_immediate_response(False, message=error_msg)
else:
# Send an immediate response of success
# The grade request is formed correctly
self._send_immediate_response(True)
# Wait a bit before POSTing back to the callback url with the
# grade result configured by the server
# Otherwise, the problem will not realize it's
# queued and it will keep waiting for a response
# indefinitely
delayed_grade_func = lambda: self._send_grade_response(callback_url,
xqueue_header)
timer = threading.Timer(2, delayed_grade_func)
timer.start()
# If we get a request that's not to the grading submission
# URL, return an error
else:
error_message = "Invalid request URL"
self._send_immediate_response(False, message=error_message)
def _send_head(self):
'''
Send the response code and MIME headers
'''
if self._is_grade_request():
self.send_response(200)
else:
self.send_response(500)
self.send_header('Content-type', 'text/plain')
self.end_headers()
def _post_dict(self):
'''
Retrieve the POST parameters from the client as a dictionary
'''
try:
length = int(self.headers.getheader('content-length'))
post_dict = urlparse.parse_qs(self.rfile.read(length))
# The POST dict will contain a list of values
# for each key.
# None of our parameters are lists, however,
# so we map [val] --> val
# If the list contains multiple entries,
# we pick the first one
post_dict = dict(map(lambda (key, list_val): (key, list_val[0]),
post_dict.items()))
except:
# We return an empty dict here, on the assumption
# that when we later check that the request has
# the correct fields, it won't find them,
# and will therefore send an error response
return {}
return post_dict
def _send_immediate_response(self, success, message=""):
'''
Send an immediate success/failure message
back to the client
'''
# Send the response indicating success/failure
response_str = json.dumps({'return_code': 0 if success else 1,
'content': message})
# Log the response
logger.debug("XQueue: sent response %s" % response_str)
self.wfile.write(response_str)
def _send_grade_response(self, postback_url, xqueue_header):
'''
POST the grade response back to the client
using the response provided by the server configuration
'''
response_dict = {'xqueue_header': json.dumps(xqueue_header),
'xqueue_body': json.dumps(self.server.grade_response())}
# Log the response
logger.debug("XQueue: sent grading response %s" % str(response_dict))
MockXQueueRequestHandler.post_to_url(postback_url, response_dict)
def _is_grade_request(self):
return 'xqueue/submit' in self.path
@staticmethod
def post_to_url(url, param_dict):
'''
POST *param_dict* to *url*
We make this a separate function so we can easily patch
it during testing.
'''
urllib.urlopen(url, urllib.urlencode(param_dict))
class MockXQueueServer(HTTPServer):
'''
A mock XQueue grading server that responds
to POST requests to localhost.
'''
def __init__(self, port_num,
grade_response_dict={'correct': True, 'score': 1, 'msg': ''}):
'''
Initialize the mock XQueue server instance.
*port_num* is the localhost port to listen to
*grade_response_dict* is a dictionary that will be JSON-serialized
and sent in response to XQueue grading requests.
'''
self.set_grade_response(grade_response_dict)
handler = MockXQueueRequestHandler
address = ('', port_num)
HTTPServer.__init__(self, address, handler)
def shutdown(self):
'''
Stop the server and free up the port
'''
# First call superclass shutdown()
HTTPServer.shutdown(self)
# We also need to manually close the socket
self.socket.close()
def grade_response(self):
return self._grade_response
def set_grade_response(self, grade_response_dict):
# Check that the grade response has the right keys
assert('correct' in grade_response_dict and
'score' in grade_response_dict and
'msg' in grade_response_dict)
# Wrap the message in <div> tags to ensure that it is valid XML
grade_response_dict['msg'] = "<div>%s</div>" % grade_response_dict['msg']
# Save the response dictionary
self._grade_response = grade_response_dict
import mock
import unittest
import threading
import json
import urllib
import urlparse
import time
from mock_xqueue_server import MockXQueueServer, MockXQueueRequestHandler
class MockXQueueServerTest(unittest.TestCase):
'''
A mock version of the XQueue server that listens on a local
port and responds with pre-defined grade messages.
Used for lettuce BDD tests in lms/courseware/features/problems.feature
and lms/courseware/features/problems.py
This is temporary and will be removed when XQueue is
rewritten using celery.
'''
def setUp(self):
# Create the server
server_port = 8034
self.server_url = 'http://127.0.0.1:%d' % server_port
self.server = MockXQueueServer(server_port,
{'correct': True, 'score': 1, 'msg': ''})
# Start the server in a separate daemon thread
server_thread = threading.Thread(target=self.server.serve_forever)
server_thread.daemon = True
server_thread.start()
def tearDown(self):
# Stop the server, freeing up the port
self.server.shutdown()
def test_grade_request(self):
# Patch post_to_url() so we can intercept
# outgoing POST requests from the server
MockXQueueRequestHandler.post_to_url = mock.Mock()
# Send a grade request
callback_url = 'http://127.0.0.1:8000/test_callback'
grade_header = json.dumps({'lms_callback_url': callback_url,
'lms_key': 'test_queuekey',
'queue_name': 'test_queue'})
grade_body = json.dumps({'student_info': 'test',
'grader_payload': 'test',
'student_response': 'test'})
grade_request = {'xqueue_header': grade_header,
'xqueue_body': grade_body}
response_handle = urllib.urlopen(self.server_url + '/xqueue/submit',
urllib.urlencode(grade_request))
response_dict = json.loads(response_handle.read())
# Expect that the response is success
self.assertEqual(response_dict['return_code'], 0)
# Wait a bit before checking that the server posted back
time.sleep(3)
# Expect that the server tries to post back the grading info
xqueue_body = json.dumps({'correct': True, 'score': 1,
'msg': '<div></div>'})
expected_callback_dict = {'xqueue_header': grade_header,
'xqueue_body': xqueue_body}
MockXQueueRequestHandler.post_to_url.assert_called_with(callback_url,
expected_callback_dict)
"""
Classes to provide the LMS runtime data storage to XBlocks
"""
import json import json
from collections import namedtuple, defaultdict from collections import namedtuple, defaultdict
from itertools import chain from itertools import chain
...@@ -14,10 +18,16 @@ from xblock.core import Scope ...@@ -14,10 +18,16 @@ from xblock.core import Scope
class InvalidWriteError(Exception): class InvalidWriteError(Exception):
pass """
Raised to indicate that writing to a particular key
in the KeyValueStore is disabled
"""
def chunks(items, chunk_size): def chunks(items, chunk_size):
"""
Yields the values from items in chunks of size chunk_size
"""
items = list(items) items = list(items)
return (items[i:i + chunk_size] for i in xrange(0, len(items), chunk_size)) return (items[i:i + chunk_size] for i in xrange(0, len(items), chunk_size))
...@@ -67,6 +77,15 @@ class ModelDataCache(object): ...@@ -67,6 +77,15 @@ class ModelDataCache(object):
""" """
def get_child_descriptors(descriptor, depth, descriptor_filter): def get_child_descriptors(descriptor, depth, descriptor_filter):
"""
Return a list of all child descriptors down to the specified depth
that match the descriptor filter. Includes `descriptor`
descriptor: The parent to search inside
depth: The number of levels to descend, or None for infinite depth
descriptor_filter(descriptor): A function that returns True
if descriptor should be included in the results
"""
if descriptor_filter(descriptor): if descriptor_filter(descriptor):
descriptors = [descriptor] descriptors = [descriptor]
else: else:
...@@ -121,7 +140,7 @@ class ModelDataCache(object): ...@@ -121,7 +140,7 @@ class ModelDataCache(object):
'module_state_key__in', 'module_state_key__in',
(descriptor.location.url() for descriptor in self.descriptors), (descriptor.location.url() for descriptor in self.descriptors),
course_id=self.course_id, course_id=self.course_id,
student=self.user, student=self.user.pk,
) )
elif scope == Scope.content: elif scope == Scope.content:
return self._chunked_query( return self._chunked_query(
...@@ -145,13 +164,13 @@ class ModelDataCache(object): ...@@ -145,13 +164,13 @@ class ModelDataCache(object):
XModuleStudentPrefsField, XModuleStudentPrefsField,
'module_type__in', 'module_type__in',
set(descriptor.location.category for descriptor in self.descriptors), set(descriptor.location.category for descriptor in self.descriptors),
student=self.user, student=self.user.pk,
field_name__in=set(field.name for field in fields), field_name__in=set(field.name for field in fields),
) )
elif scope == Scope.student_info: elif scope == Scope.student_info:
return self._query( return self._query(
XModuleStudentInfoField, XModuleStudentInfoField,
student=self.user, student=self.user.pk,
field_name__in=set(field.name for field in fields), field_name__in=set(field.name for field in fields),
) )
else: else:
...@@ -168,6 +187,9 @@ class ModelDataCache(object): ...@@ -168,6 +187,9 @@ class ModelDataCache(object):
return scope_map return scope_map
def _cache_key_from_kvs_key(self, key): def _cache_key_from_kvs_key(self, key):
"""
Return the key used in the ModelDataCache for the specified KeyValueStore key
"""
if key.scope == Scope.student_state: if key.scope == Scope.student_state:
return (key.scope, key.block_scope_id.url()) return (key.scope, key.block_scope_id.url())
elif key.scope == Scope.content: elif key.scope == Scope.content:
...@@ -180,6 +202,10 @@ class ModelDataCache(object): ...@@ -180,6 +202,10 @@ class ModelDataCache(object):
return (key.scope, key.field_name) return (key.scope, key.field_name)
def _cache_key_from_field_object(self, scope, field_object): def _cache_key_from_field_object(self, scope, field_object):
"""
Return the key used in the ModelDataCache for the specified scope and
field
"""
if scope == Scope.student_state: if scope == Scope.student_state:
return (scope, field_object.module_state_key) return (scope, field_object.module_state_key)
elif scope == Scope.content: elif scope == Scope.content:
...@@ -230,7 +256,7 @@ class ModelDataCache(object): ...@@ -230,7 +256,7 @@ class ModelDataCache(object):
usage_id='%s-%s' % (self.course_id, key.block_scope_id.url()), usage_id='%s-%s' % (self.course_id, key.block_scope_id.url()),
) )
elif key.scope == Scope.student_preferences: elif key.scope == Scope.student_preferences:
field_object, _= XModuleStudentPrefsField.objects.get_or_create( field_object, _ = XModuleStudentPrefsField.objects.get_or_create(
field_name=key.field_name, field_name=key.field_name,
module_type=key.block_scope_id, module_type=key.block_scope_id,
student=self.user, student=self.user,
...@@ -276,6 +302,7 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -276,6 +302,7 @@ class LmsKeyValueStore(KeyValueStore):
Scope.student_info, Scope.student_info,
Scope.children, Scope.children,
) )
def __init__(self, descriptor_model_data, model_data_cache): def __init__(self, descriptor_model_data, model_data_cache):
self._descriptor_model_data = descriptor_model_data self._descriptor_model_data = descriptor_model_data
self._model_data_cache = model_data_cache self._model_data_cache = model_data_cache
...@@ -357,4 +384,3 @@ class LmsKeyValueStore(KeyValueStore): ...@@ -357,4 +384,3 @@ class LmsKeyValueStore(KeyValueStore):
LmsUsage = namedtuple('LmsUsage', 'id, def_id') LmsUsage = namedtuple('LmsUsage', 'id, def_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)
......
...@@ -99,6 +99,11 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG ...@@ -99,6 +99,11 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
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(
......
...@@ -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
} }
} }
...@@ -32,6 +40,18 @@ DATABASES = { ...@@ -32,6 +40,18 @@ DATABASES = {
} }
} }
# Set up XQueue information so that the lms will send
# requests to a mock XQueue server running locally
XQUEUE_PORT = 8027
XQUEUE_INTERFACE = {
"url": "http://127.0.0.1:%d" % XQUEUE_PORT,
"django_auth": {
"username": "lms",
"password": "***REMOVED***"
},
"basic_auth": ('anant', 'agarwal'),
}
# Do not display the YouTube videos in the browser while running the # Do not display the YouTube videos in the browser while running the
# acceptance tests. This makes them faster and more reliable # acceptance tests. This makes them faster and more reliable
MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True
......
...@@ -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>)
......
...@@ -73,41 +73,6 @@ ...@@ -73,41 +73,6 @@
</article> </article>
--> -->
<article id="associate-legal-counsel" class="job">
<div class="inner-wrapper">
<h3><strong>ASSOCIATE LEGAL COUNSEL</strong></h3>
<p>We are seeking a talented lawyer with the ability to operate independently in a fast-paced environment and work proactively with all members of the edX team. You must have thorough knowledge of intellectual property law, contracts and licensing. </p>
<p><strong>Key Responsibilities: </strong></p>
<ul>
<li>Drive the negotiating, reviewing, drafting and overseeing of a wide range of transactional arrangements, including collaborations related to the provision of online education, inbound and outbound licensing of intellectual property, strategic partnerships, nondisclosure agreements, and services agreements.</li>
<li>Provide counseling on the legal implications/considerations of business and technical strategies and projects, with special emphasis on regulations related to higher education, data security and privacy.</li>
<li>Provide advice and support company-wide on a variety of legal issues in a timely and effective manner.</li>
<li>Assist on other matters as needed.</li>
</ul>
<p><strong>Requirements:</strong></p>
<ul>
<li>JD from an accredited law school</li>
<li>Massachusetts bar admission required</li>
<li>2-3 years of transactional experience at a major law firm and/or as an in-house counselor</li>
<li>Substantial IP licensing experience</li>
<li>Knowledge of copyright, trademark and patent law</li>
<li>Experience with open source content and open source software preferred</li>
<li>Outstanding communications skills (written and oral)</li>
<li>Experience with drafting and legal review of Internet privacy policies and terms of use.</li>
<li>Understanding of how to balance legal risks with business objectives</li>
<li>Ability to develop an innovative approach to legal issues in support of strategic business initiatives</li>
<li>An internal business and customer focused proactive attitude with ability to prioritize effectively</li>
<li>Experience with higher education preferred but not required</li>
</ul>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
</div>
</article>
<article id="director-of-education-services" class="job"> <article id="director-of-education-services" class="job">
<div class="inner-wrapper"> <div class="inner-wrapper">
<h3><strong>DIRECTOR OF EDUCATIONAL SERVICES</strong></h3> <h3><strong>DIRECTOR OF EDUCATIONAL SERVICES</strong></h3>
...@@ -369,40 +334,6 @@ ...@@ -369,40 +334,6 @@
</article> </article>
<article id="director-engineering-open-source" class="job">
<div class="inner-wrapper">
<h3><strong>DIRECTOR ENGINEERING, OPEN SOURCE COMMUNITY MANAGER</strong></h3>
<p>In edX courses, students make (and break) electronic circuits, they manipulate molecules on the fly and they do it all at once, in their tens of thousands. We have great Professors and great Universities. But we can’t possibly keep up with all the great ideas out there, so we’re making our platform open source, to turn up the volume on great education. To do that well, we’ll need a Director of Engineering who can lead our Open Source Community efforts.</p>
<p><strong>Responsibilities:</strong></p>
<ul>
<li>Define and implement software design standards that make the open source community most welcome and productive.</li>
<li>Work with others to establish the governance standards for the edX Open Source Platform, establish the infrastructure, and manage the team to deliver releases and leverage our University partners and stakeholders to</li> make the edX platform the world’s best learning platform.
<li>Help the organization recognize the benefits and limitations inherent in open source solutions.</li>
<li>Establish best practices and key tool usage, especially those based on industry standards.</li>
<li>Provide visibility for the leadership team into the concerns and challenges faced by the open source community.</li>
<li>Foster a thriving community by providing the communication, documentation and feedback that they need to be enthusiastic.</li>
<li>Maximize the good code design coming from the open source community.</li>
<li>Provide the wit and firmness that the community needs to channel their energy productively.</li>
<li>Tactfully balance the internal needs of the organization to pursue new opportunities with the community’s need to participate in the platform’s evolution.</li>
<li>Shorten lines of communication and build trust across entire team</li>
</ul>
<p><strong>Qualifications:</strong></p>
<ul>
<li>Bachelors, preferably Masters in Computer Science</li>
<li>Solid communication skills, especially written</li>
<li>Committed to Agile practice, Scrum and Kanban</li>
<li>Charm and humor</li>
<li>Deep familiarity with Open Source, participant and contributor</li>
<li>Python, Django, Javascript</li>
<li>Commitment to support your technical recommendations, both within and beyond the organization.</li>
</ul>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a>.</p>
</div>
</article>
<article id="software-engineer" class="job"> <article id="software-engineer" class="job">
<div class="inner-wrapper"> <div class="inner-wrapper">
<h3><strong>SOFTWARE ENGINEER</strong></h3> <h3><strong>SOFTWARE ENGINEER</strong></h3>
...@@ -441,7 +372,6 @@ ...@@ -441,7 +372,6 @@
<section class="jobs-sidebar"> <section class="jobs-sidebar">
<h2>Positions</h2> <h2>Positions</h2>
<nav> <nav>
<a href="#associate-legal-counsel">Associate Legal Counsel</a>
<a href="#director-of-education-services">Director of Education Services</a> <a href="#director-of-education-services">Director of Education Services</a>
<a href="#manager-of-training-services">Manager of Training Services</a> <a href="#manager-of-training-services">Manager of Training Services</a>
<a href="#instructional-designer">Instructional Designer</a> <a href="#instructional-designer">Instructional Designer</a>
...@@ -449,7 +379,6 @@ ...@@ -449,7 +379,6 @@
<a href="#project-manager-pmo">Project Manager (PMO)</a> <a href="#project-manager-pmo">Project Manager (PMO)</a>
<a href="#director-of-product-management">Director of Product Management</a> <a href="#director-of-product-management">Director of Product Management</a>
<a href="#content-engineer">Content Engineer</a> <a href="#content-engineer">Content Engineer</a>
<a href="#director-engineering-open-source">Director Engineering, Open Source Community Manager</a>
<a href="#software-engineer">Software Engineer</a> <a href="#software-engineer">Software Engineer</a>
</nav> </nav>
<h2>How to Apply</h2> <h2>How to Apply</h2>
......
Django==1.3.1
flup==1.0.3.dev-20110405
lxml==2.3.4
Mako==0.7.0
Markdown==2.1.1
markdown2==1.4.2
python-memcached==1.48
numpy==1.6.1
Pygments==1.5
boto==2.3.0
django-storages==1.1.4
django-masquerade==0.1.5
fs==0.4.0
django-jasmine==0.3.2
path.py==2.2.2
requests==0.12.1
BeautifulSoup==3.2.1
BeautifulSoup4==4.1.1
newrelic==1.3.0.289
ipython==0.12.1
django-pipeline==1.2.12
django-staticfiles==1.2.1
glob2==0.3
sympy==0.7.1
pymongo==2.2.1
rednose==0.3.3
mock==0.8.0
GitPython==0.3.2.RC1
PyYAML==3.10
feedparser==5.1.2
MySQL-python==1.2.3
matplotlib==1.1.0
scipy==0.10.1
akismet==0.2.0
Coffin==0.3.6
django-celery==2.2.7
django-countries==1.0.5
django-followit==0.0.3
django-keyedcache==1.4-6
django-kombu==0.9.2
django-mako==0.1.5pre
django-recaptcha-works==0.3.4
django-robots==0.8.1
django-ses==0.4.1
django-threaded-multihost==1.4-1
html5lib==0.90
Jinja2==2.6
oauth2==1.5.211
pystache==0.3.1
python-openid==2.2.5
South==0.7.5
Unidecode==0.04.9
dogstatsd-python==0.2.1
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