diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index b79d86b52f..66e6551019 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -5,7 +5,7 @@ from django.test.utils import override_settings
from django.conf import settings
from django.core.urlresolvers import reverse
from path import path
-from tempfile import mkdtemp
+from tempdir import mkdtemp_clean
import json
from fs.osfs import OSFS
import copy
@@ -194,7 +194,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
import_from_xml(ms, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
- root_dir = path(mkdtemp())
+ root_dir = path(mkdtemp_clean())
print 'Exporting to tempdir = {0}'.format(root_dir)
@@ -264,6 +264,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
+
class ContentStoreTest(ModuleStoreTestCase):
"""
Tests for the CMS ContentStore application.
@@ -421,6 +422,64 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertIn('markdown', problem.metadata, "markdown is missing from metadata")
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
+ def test_import_metadata_with_attempts_empty_string(self):
+ import_from_xml(modulestore(), 'common/test/data/', ['simple'])
+ ms = modulestore('direct')
+ did_load_item = False
+ try:
+ ms.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
+ did_load_item = True
+ except ItemNotFoundError:
+ pass
+
+ # make sure we found the item (e.g. it didn't error while loading)
+ self.assertTrue(did_load_item)
+
+ def test_metadata_inheritance(self):
+ import_from_xml(modulestore(), 'common/test/data/', ['full'])
+
+ ms = modulestore('direct')
+ course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
+
+ verticals = ms.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
+
+ # let's assert on the metadata_inheritance on an existing vertical
+ for vertical in verticals:
+ self.assertIn('xqa_key', vertical.metadata)
+ self.assertEqual(course.metadata['xqa_key'], vertical.metadata['xqa_key'])
+
+ self.assertGreater(len(verticals), 0)
+
+ new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
+ source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
+
+ # crate a new module and add it as a child to a vertical
+ ms.clone_item(source_template_location, new_component_location)
+ parent = verticals[0]
+ ms.update_children(parent.location, parent.definition.get('children', []) + [new_component_location.url()])
+
+ # flush the cache
+ ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
+ new_module = ms.get_item(new_component_location)
+
+ # check for grace period definition which should be defined at the course level
+ self.assertIn('graceperiod', new_module.metadata)
+
+ self.assertEqual(course.metadata['graceperiod'], new_module.metadata['graceperiod'])
+
+ #
+ # now let's define an override at the leaf node level
+ #
+ new_module.metadata['graceperiod'] = '1 day'
+ ms.update_metadata(new_module.location, new_module.metadata)
+
+ # flush the cache and refetch
+ ms.get_cached_metadata_inheritance_tree(new_component_location, -1)
+ new_module = ms.get_item(new_component_location)
+
+ self.assertIn('graceperiod', new_module.metadata)
+ self.assertEqual('1 day', new_module.metadata['graceperiod'])
+
class TemplateTestCase(ModuleStoreTestCase):
diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py
index 166982e35f..c4a46459e2 100644
--- a/cms/djangoapps/contentstore/tests/tests.py
+++ b/cms/djangoapps/contentstore/tests/tests.py
@@ -4,7 +4,6 @@ from django.test.client import Client
from django.conf import settings
from django.core.urlresolvers import reverse
from path import path
-from tempfile import mkdtemp
import json
from fs.osfs import OSFS
import copy
diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py
index be028b2836..b6b8cd5023 100644
--- a/cms/djangoapps/contentstore/tests/utils.py
+++ b/cms/djangoapps/contentstore/tests/utils.py
@@ -1,6 +1,6 @@
import json
import copy
-from time import time
+from uuid import uuid4
from django.test import TestCase
from django.conf import settings
@@ -20,13 +20,12 @@ class ModuleStoreTestCase(TestCase):
def _pre_setup(self):
super(ModuleStoreTestCase, self)._pre_setup()
- # Use the current seconds since epoch to differentiate
+ # Use a uuid to differentiate
# the mongo collections on jenkins.
- sec_since_epoch = '%s' % int(time() * 100)
self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
self.test_MODULESTORE = self.orig_MODULESTORE
- self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % sec_since_epoch
- self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % sec_since_epoch
+ self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
+ self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
settings.MODULESTORE = self.test_MODULESTORE
# Flush and initialize the module store
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 281dd97f20..50f237c374 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -20,7 +20,6 @@ Longer TODO:
"""
import sys
-import tempfile
import os.path
import os
import lms.envs.common
@@ -59,7 +58,8 @@ sys.path.append(COMMON_ROOT / 'lib')
############################# WEB CONFIGURATION #############################
# This is where we stick our compiled template files.
-MAKO_MODULE_DIR = tempfile.mkdtemp('mako')
+from tempdir import mkdtemp_clean
+MAKO_MODULE_DIR = mkdtemp_clean('mako')
MAKO_TEMPLATES = {}
MAKO_TEMPLATES['main'] = [
PROJECT_ROOT / 'templates',
diff --git a/common/djangoapps/mitxmako/makoloader.py b/common/djangoapps/mitxmako/makoloader.py
index 29184299b6..d623e8bcff 100644
--- a/common/djangoapps/mitxmako/makoloader.py
+++ b/common/djangoapps/mitxmako/makoloader.py
@@ -9,6 +9,7 @@ from django.template.loaders.app_directories import Loader as AppDirectoriesLoad
from mitxmako.template import Template
import mitxmako.middleware
+import tempdir
log = logging.getLogger(__name__)
@@ -30,7 +31,7 @@ class MakoLoader(object):
if module_directory is None:
log.warning("For more caching of mako templates, set the MAKO_MODULE_DIR in settings!")
- module_directory = tempfile.mkdtemp()
+ module_directory = tempdir.mkdtemp_clean()
self.module_directory = module_directory
diff --git a/common/djangoapps/mitxmako/middleware.py b/common/djangoapps/mitxmako/middleware.py
index 64cb2e5415..3f66f8cc48 100644
--- a/common/djangoapps/mitxmako/middleware.py
+++ b/common/djangoapps/mitxmako/middleware.py
@@ -13,7 +13,7 @@
# limitations under the License.
from mako.lookup import TemplateLookup
-import tempfile
+import tempdir
from django.template import RequestContext
from django.conf import settings
@@ -29,7 +29,7 @@ class MakoMiddleware(object):
module_directory = getattr(settings, 'MAKO_MODULE_DIR', None)
if module_directory is None:
- module_directory = tempfile.mkdtemp()
+ module_directory = tempdir.mkdtemp_clean()
for location in template_locations:
lookup[location] = TemplateLookup(directories=template_locations[location],
diff --git a/common/djangoapps/student/management/commands/tests/test_pearson.py b/common/djangoapps/student/management/commands/tests/test_pearson.py
index 12969405de..65d628fba0 100644
--- a/common/djangoapps/student/management/commands/tests/test_pearson.py
+++ b/common/djangoapps/student/management/commands/tests/test_pearson.py
@@ -7,6 +7,7 @@ import logging
import os
from tempfile import mkdtemp
import cStringIO
+import shutil
import sys
from django.test import TestCase
@@ -143,23 +144,18 @@ class PearsonTestCase(TestCase):
'''
Base class for tests running Pearson-related commands
'''
- import_dir = mkdtemp(prefix="import")
- export_dir = mkdtemp(prefix="export")
def assertErrorContains(self, error_message, expected):
self.assertTrue(error_message.find(expected) >= 0, 'error message "{}" did not contain "{}"'.format(error_message, expected))
+ def setUp(self):
+ self.import_dir = mkdtemp(prefix="import")
+ self.addCleanup(shutil.rmtree, self.import_dir)
+ self.export_dir = mkdtemp(prefix="export")
+ self.addCleanup(shutil.rmtree, self.export_dir)
+
def tearDown(self):
- def delete_temp_dir(dirname):
- if os.path.exists(dirname):
- for filename in os.listdir(dirname):
- os.remove(os.path.join(dirname, filename))
- os.rmdir(dirname)
-
- # clean up after any test data was dumped to temp directory
- delete_temp_dir(self.import_dir)
- delete_temp_dir(self.export_dir)
-
+ pass
# and clean up the database:
# TestCenterUser.objects.all().delete()
# TestCenterRegistration.objects.all().delete()
diff --git a/common/lib/tempdir.py b/common/lib/tempdir.py
new file mode 100644
index 0000000000..0acd92ba33
--- /dev/null
+++ b/common/lib/tempdir.py
@@ -0,0 +1,17 @@
+"""Make temporary directories nicely."""
+
+import atexit
+import os.path
+import shutil
+import tempfile
+
+def mkdtemp_clean(suffix="", prefix="tmp", dir=None):
+ """Just like mkdtemp, but the directory will be deleted when the process ends."""
+ the_dir = tempfile.mkdtemp(suffix=suffix, prefix=prefix, dir=dir)
+ atexit.register(cleanup_tempdir, the_dir)
+ return the_dir
+
+def cleanup_tempdir(the_dir):
+ """Called on process exit to remove a temp directory."""
+ if os.path.exists(the_dir):
+ shutil.rmtree(the_dir)
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 2c69c449ba..8b2d5a6c92 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -352,6 +352,13 @@ class CourseDescriptor(SequenceDescriptor):
"""
return self.metadata.get('tabs')
+ @property
+ def pdf_textbooks(self):
+ """
+ Return the pdf_textbooks config, as a python object, or None if not specified.
+ """
+ return self.metadata.get('pdf_textbooks')
+
@tabs.setter
def tabs(self, value):
self.metadata['tabs'] = value
diff --git a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
index 39c91d8c70..d38036c8de 100644
--- a/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/combinedopenended/display.coffee
@@ -1,6 +1,21 @@
class @Rubric
constructor: () ->
+ @initialize: (location) ->
+ $('.rubric').data("location", location)
+ $('input[class="score-selection"]').change @tracking_callback
+
+ @tracking_callback: (event) ->
+ target_selection = $(event.target).val()
+ # chop off the beginning of the name so that we can get the number of the category
+ category = $(event.target).data("category")
+ location = $('.rubric').data('location')
+ # probably want the original problem location as well
+
+ data = {location: location, selection: target_selection, category: category}
+ Logger.log 'rubric_select', data
+
+
# finds the scores for each rubric category
@get_score_list: () =>
# find the number of categories:
@@ -45,6 +60,9 @@ class @CombinedOpenEnded
@task_count = @el.data('task-count')
@task_number = @el.data('task-number')
@accept_file_upload = @el.data('accept-file-upload')
+ @location = @el.data('location')
+ # set up handlers for click tracking
+ Rubric.initialize(@location)
@allow_reset = @el.data('allow_reset')
@reset_button = @$('.reset-button')
@@ -118,6 +136,9 @@ class @CombinedOpenEnded
@submit_evaluation_button = $('.submit-evaluation-button')
@submit_evaluation_button.click @message_post
Collapsible.setCollapsibles(@results_container)
+ # make sure we still have click tracking
+ $('.evaluation-response a').click @log_feedback_click
+ $('input[name="evaluation-score"]').change @log_feedback_selection
show_results: (event) =>
status_item = $(event.target).parent()
@@ -155,7 +176,6 @@ class @CombinedOpenEnded
@legend_container= $('.legend-container')
message_post: (event)=>
- Logger.log 'message_post', @answers
external_grader_message=$(event.target).parent().parent().parent()
evaluation_scoring = $(event.target).parent()
@@ -184,6 +204,7 @@ class @CombinedOpenEnded
$('section.evaluation').slideToggle()
@message_wrapper.html(response.message_html)
+
$.ajaxWithPrefix("#{@ajax_url}/save_post_assessment", settings)
@@ -406,7 +427,7 @@ class @CombinedOpenEnded
$.postWithPrefix "#{@ajax_url}/check_for_score", (response) =>
if response.state == "done" or response.state=="post_assessment"
delete window.queuePollerID
- location.reload()
+ @reload()
else
window.queuePollerID = window.setTimeout(@poll, 10000)
@@ -440,7 +461,9 @@ class @CombinedOpenEnded
@prompt_container.toggleClass('open')
if @question_header.text() == "(Hide)"
new_text = "(Show)"
+ Logger.log 'oe_hide_question', {location: @location}
else
+ Logger.log 'oe_show_question', {location: @location}
new_text = "(Hide)"
@question_header.text(new_text)
@@ -456,4 +479,16 @@ class @CombinedOpenEnded
@prompt_container.toggleClass('open')
@question_header.text("(Show)")
+ log_feedback_click: (event) ->
+ link_text = $(event.target).html()
+ if link_text == 'See full feedback'
+ Logger.log 'oe_show_full_feedback', {}
+ else if link_text == 'Respond to Feedback'
+ Logger.log 'oe_show_respond_to_feedback', {}
+ else
+ generated_event_type = link_text.toLowerCase().replace(" ","_")
+ Logger.log "oe_" + generated_event_type, {}
+ log_feedback_selection: (event) ->
+ target_selection = $(event.target).val()
+ Logger.log 'oe_feedback_response_selected', {value: target_selection}
diff --git a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee
index 63c58e1766..0b38090e43 100644
--- a/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee
+++ b/common/lib/xmodule/xmodule/js/src/peergrading/peer_grading_problem.coffee
@@ -426,6 +426,7 @@ class @PeerGradingProblem
@submit_button.hide()
@action_button.hide()
@calibration_feedback_panel.hide()
+ Rubric.initialize(@location)
render_calibration_feedback: (response) =>
@@ -476,7 +477,9 @@ class @PeerGradingProblem
@prompt_container.slideToggle()
@prompt_container.toggleClass('open')
if @question_header.text() == "(Hide)"
+ Logger.log 'peer_grading_hide_question', {location: @location}
new_text = "(Show)"
else
+ Logger.log 'peer_grading_show_question', {location: @location}
new_text = "(Hide)"
@question_header.text(new_text)
diff --git a/common/lib/xmodule/xmodule/mako_module.py b/common/lib/xmodule/xmodule/mako_module.py
index dab5d5e85b..da96bfa212 100644
--- a/common/lib/xmodule/xmodule/mako_module.py
+++ b/common/lib/xmodule/xmodule/mako_module.py
@@ -44,5 +44,6 @@ class MakoModuleDescriptor(XModuleDescriptor):
# cdodge: encapsulate a means to expose "editable" metadata fields (i.e. not internal system metadata)
@property
def editable_metadata_fields(self):
- subset = [name for name in self.metadata.keys() if name not in self.system_metadata_fields]
+ subset = [name for name in self.metadata.keys() if name not in self.system_metadata_fields and
+ name not in self._inherited_metadata]
return subset
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index f4db62ac31..012efb0c27 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -1,11 +1,13 @@
import pymongo
import sys
import logging
+import copy
from bson.son import SON
from fs.osfs import OSFS
from itertools import repeat
from path import path
+from datetime import datetime, timedelta
from importlib import import_module
from xmodule.errortracker import null_error_tracker, exc_info_to_str
@@ -27,9 +29,11 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
"""
A system that has a cache of module json that it will use to load modules
from, with a backup of calling to the underlying modulestore for more data
+ TODO (cdodge) when the 'split module store' work has been completed we can remove all
+ references to metadata_inheritance_tree
"""
def __init__(self, modulestore, module_data, default_class, resources_fs,
- error_tracker, render_template):
+ error_tracker, render_template, metadata_inheritance_tree = None):
"""
modulestore: the module store that can be used to retrieve additional modules
@@ -54,6 +58,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
# cdodge: other Systems have a course_id attribute defined. To keep things consistent, let's
# define an attribute here as well, even though it's None
self.course_id = None
+ self.metadata_inheritance_tree = metadata_inheritance_tree
def load_item(self, location):
location = Location(location)
@@ -61,11 +66,13 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
if json_data is None:
return self.modulestore.get_item(location)
else:
- # TODO (vshnayder): metadata inheritance is somewhat broken because mongo, doesn't
- # always load an entire course. We're punting on this until after launch, and then
- # will build a proper course policy framework.
+ # load the module and apply the inherited metadata
try:
- return XModuleDescriptor.load_from_json(json_data, self, self.default_class)
+ module = XModuleDescriptor.load_from_json(json_data, self, self.default_class)
+ if self.metadata_inheritance_tree is not None:
+ metadata_to_inherit = self.metadata_inheritance_tree.get('parent_metadata', {}).get(location.url(),{})
+ module.inherit_metadata(metadata_to_inherit)
+ return module
except:
return ErrorDescriptor.from_json(
json_data,
@@ -142,6 +149,82 @@ class MongoModuleStore(ModuleStoreBase):
self.fs_root = path(fs_root)
self.error_tracker = error_tracker
self.render_template = render_template
+ self.metadata_inheritance_cache = {}
+
+ def get_metadata_inheritance_tree(self, location):
+ '''
+ TODO (cdodge) This method can be deleted when the 'split module store' work has been completed
+ '''
+
+ # get all collections in the course, this query should not return any leaf nodes
+ query = { '_id.org' : location.org,
+ '_id.course' : location.course,
+ '_id.revision' : None,
+ 'definition.children':{'$ne': []}
+ }
+ # we just want the Location, children, and metadata
+ record_filter = {'_id':1,'definition.children':1,'metadata':1}
+
+ # call out to the DB
+ resultset = self.collection.find(query, record_filter)
+
+ results_by_url = {}
+ root = None
+
+ # now go through the results and order them by the location url
+ for result in resultset:
+ location = Location(result['_id'])
+ results_by_url[location.url()] = result
+ if location.category == 'course':
+ root = location.url()
+
+ # now traverse the tree and compute down the inherited metadata
+ metadata_to_inherit = {}
+ def _compute_inherited_metadata(url):
+ my_metadata = results_by_url[url]['metadata']
+ for key in my_metadata.keys():
+ if key not in XModuleDescriptor.inheritable_metadata:
+ del my_metadata[key]
+ results_by_url[url]['metadata'] = my_metadata
+
+ # go through all the children and recurse, but only if we have
+ # in the result set. Remember results will not contain leaf nodes
+ for child in results_by_url[url].get('definition',{}).get('children',[]):
+ if child in results_by_url:
+ new_child_metadata = copy.deepcopy(my_metadata)
+ new_child_metadata.update(results_by_url[child]['metadata'])
+ results_by_url[child]['metadata'] = new_child_metadata
+ metadata_to_inherit[child] = new_child_metadata
+ _compute_inherited_metadata(child)
+ else:
+ # this is likely a leaf node, so let's record what metadata we need to inherit
+ metadata_to_inherit[child] = my_metadata
+
+ if root is not None:
+ _compute_inherited_metadata(root)
+
+ cache = {'parent_metadata': metadata_to_inherit,
+ 'timestamp' : datetime.now()}
+
+ return cache
+
+ def get_cached_metadata_inheritance_tree(self, location, max_age_allowed):
+ '''
+ TODO (cdodge) This method can be deleted when the 'split module store' work has been completed
+ '''
+ cache_name = '{0}/{1}'.format(location.org, location.course)
+ cache = self.metadata_inheritance_cache.get(cache_name,{'parent_metadata': {},
+ 'timestamp': datetime.now() - timedelta(hours=1)})
+ age = (datetime.now() - cache['timestamp'])
+
+ if age.seconds >= max_age_allowed:
+ logging.debug('loading entire inheritance tree for {0}'.format(cache_name))
+ cache = self.get_metadata_inheritance_tree(location)
+ self.metadata_inheritance_cache[cache_name] = cache
+
+ return cache
+
+
def _clean_item_data(self, item):
"""
@@ -196,6 +279,8 @@ class MongoModuleStore(ModuleStoreBase):
resource_fs = OSFS(root)
+ # TODO (cdodge): When the 'split module store' work has been completed, we should remove
+ # the 'metadata_inheritance_tree' parameter
system = CachingDescriptorSystem(
self,
data_cache,
@@ -203,6 +288,7 @@ class MongoModuleStore(ModuleStoreBase):
resource_fs,
self.error_tracker,
self.render_template,
+ metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location']), 60)
)
return system.load_item(item['location'])
@@ -261,11 +347,11 @@ class MongoModuleStore(ModuleStoreBase):
descendents of the queried modules for more efficient results later
in the request. The depth is counted in the number of
calls to get_children() to cache. None indicates to cache all descendents.
-
"""
location = Location.ensure_fully_specified(location)
item = self._find_one(location)
- return self._load_items([item], depth)[0]
+ module = self._load_items([item], depth)[0]
+ return module
def get_instance(self, course_id, location, depth=0):
"""
@@ -285,7 +371,8 @@ class MongoModuleStore(ModuleStoreBase):
sort=[('revision', pymongo.ASCENDING)],
)
- return self._load_items(list(items), depth)
+ modules = self._load_items(list(items), depth)
+ return modules
def clone_item(self, source, location):
"""
@@ -313,7 +400,7 @@ class MongoModuleStore(ModuleStoreBase):
raise DuplicateItemError(location)
- def get_course_for_item(self, location):
+ def get_course_for_item(self, location, depth=0):
'''
VS[compat]
cdodge: for a given Xmodule, return the course that it belongs to
@@ -327,7 +414,7 @@ class MongoModuleStore(ModuleStoreBase):
# know the 'name' parameter in this context, so we have
# to assume there's only one item in this query even though we are not specifying a name
course_search_location = ['i4x', location.org, location.course, 'course', None]
- courses = self.get_items(course_search_location)
+ courses = self.get_items(course_search_location, depth=depth)
# make sure we found exactly one match on this above course search
found_cnt = len(courses)
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
index d6fe53d982..171441c562 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
@@ -330,6 +330,7 @@ class CombinedOpenEndedV1Module():
'status': self.get_status(False),
'display_name': self.display_name,
'accept_file_upload': self.accept_file_upload,
+ 'location': self.location,
'legend_list' : LEGEND_LIST,
}
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
index 42f5dfa1d7..50f9534717 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/openendedchild.py
@@ -357,6 +357,10 @@ class OpenEndedChild(object):
if get_data['can_upload_files'] in ['true', '1']:
has_file_to_upload = True
file = get_data['student_file'][0]
+ if self.system.track_fuction:
+ self.system.track_function('open_ended_image_upload', {'filename': file.name})
+ else:
+ log.info("No tracking function found when uploading image.")
uploaded_to_s3, image_ok, s3_public_url = self.upload_image_to_s3(file)
if uploaded_to_s3:
image_tag = self.generate_image_tag_from_url(s3_public_url, file.name)
diff --git a/common/lib/xmodule/xmodule/tests/test_export.py b/common/lib/xmodule/xmodule/tests/test_export.py
index da1b04bd94..e9fb89e9f6 100644
--- a/common/lib/xmodule/xmodule/tests/test_export.py
+++ b/common/lib/xmodule/xmodule/tests/test_export.py
@@ -4,7 +4,7 @@ from fs.osfs import OSFS
from nose.tools import assert_equals, assert_true
from path import path
from tempfile import mkdtemp
-from shutil import copytree
+import shutil
from xmodule.modulestore.xml import XMLModuleStore
@@ -46,11 +46,11 @@ class RoundTripTestCase(unittest.TestCase):
Thus we make sure that export and import work properly.
'''
def check_export_roundtrip(self, data_dir, course_dir):
- root_dir = path(mkdtemp())
+ root_dir = path(self.temp_dir)
print "Copying test course to temp dir {0}".format(root_dir)
data_dir = path(data_dir)
- copytree(data_dir / course_dir, root_dir / course_dir)
+ shutil.copytree(data_dir / course_dir, root_dir / course_dir)
print "Starting import"
initial_import = XMLModuleStore(root_dir, course_dirs=[course_dir])
@@ -108,6 +108,8 @@ class RoundTripTestCase(unittest.TestCase):
def setUp(self):
self.maxDiff = None
+ self.temp_dir = mkdtemp()
+ self.addCleanup(shutil.rmtree, self.temp_dir)
def test_toy_roundtrip(self):
self.check_export_roundtrip(DATA_DIR, "toy")
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index b5e9b10ea6..dccc96a7ca 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -411,7 +411,6 @@ class ResourceTemplates(object):
return templates
-
class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
"""
An XModuleDescriptor is a specification for an element of a course. This
@@ -585,11 +584,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
def inherit_metadata(self, metadata):
"""
Updates this module with metadata inherited from a containing module.
- Only metadata specified in self.inheritable_metadata will
+ Only metadata specified in inheritable_metadata will
be inherited
"""
# Set all inheritable metadata from kwargs that are
- # in self.inheritable_metadata and aren't already set in metadata
+ # in inheritable_metadata and aren't already set in metadata
for attr in self.inheritable_metadata:
if attr not in self.metadata and attr in metadata:
self._inherited_metadata.add(attr)
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index 64c3aabbcc..773531c528 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -128,8 +128,7 @@ class XmlDescriptor(XModuleDescriptor):
'graded': bool_map,
'hide_progress_tab': bool_map,
'allow_anonymous': bool_map,
- 'allow_anonymous_to_peers': bool_map,
- 'weight': int_map
+ 'allow_anonymous_to_peers': bool_map
}
diff --git a/common/static/css/pdfviewer.css b/common/static/css/pdfviewer.css
new file mode 100644
index 0000000000..716db251f0
--- /dev/null
+++ b/common/static/css/pdfviewer.css
@@ -0,0 +1,760 @@
+/* Copyright 2012 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+* {
+ padding: 0;
+ margin: 0;
+}
+
+html {
+ height: 100%;
+}
+
+body {
+ height: 100%;
+ background-color: #404040;
+ background-image: url(vendor/pdfjs/images/texture.png);
+}
+
+body,
+input,
+button,
+select {
+ font: message-box;
+}
+
+.hidden {
+ display: none;
+}
+[hidden] {
+ display: none !important;
+}
+
+#viewerContainer.presentationControls {
+ cursor: default;
+}
+*/
+
+/* outer/inner center provides horizontal center */
+.outerCenter {
+ float: right;
+ position: relative;
+ right: 50%;
+}
+
+.innerCenter {
+ float: right;
+ position: relative;
+ right: -50%;
+}
+
+#outerContainer {
+ width: 100%;
+ height: 100%;
+}
+
+#mainContainer {
+/* position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;*/
+ -webkit-transition-duration: 200ms;
+ -webkit-transition-timing-function: ease;
+ -moz-transition-duration: 200ms;
+ -moz-transition-timing-function: ease;
+ -ms-transition-duration: 200ms;
+ -ms-transition-timing-function: ease;
+ -o-transition-duration: 200ms;
+ -o-transition-timing-function: ease;
+ transition-duration: 200ms;
+ transition-timing-function: ease;
+}
+
+#viewerContainer {
+ overflow: auto;
+ box-shadow: inset 1px 0 0 hsla(0,0%,100%,.05);
+/* position: absolute;
+ top: 32px;
+ right: 0;
+ bottom: 0;
+ left: 0; */
+}
+
+.toolbar {
+/* position: absolute; */
+ left: 0;
+ right: 0;
+ height: 32px;
+ z-index: 9999;
+ cursor: default;
+}
+
+#toolbarContainer {
+ width: 100%;
+}
+
+
+#toolbarViewer {
+ position: relative;
+ height: 32px;
+ background-image: url(vendor/pdfjs/images/texture.png),
+ -webkit-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
+ background-image: url(vendor/pdfjs/images/texture.png),
+ -moz-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
+ background-image: url(vendor/pdfjs/images/texture.png),
+ -ms-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
+ background-image: url(vendor/pdfjs/images/texture.png),
+ -o-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
+ background-image: url(vendor/pdfjs/images/texture.png),
+ linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
+ box-shadow: inset 1px 0 0 hsla(0,0%,100%,.08),
+ inset 0 1px 1px hsla(0,0%,0%,.15),
+ inset 0 -1px 0 hsla(0,0%,100%,.05),
+ 0 1px 0 hsla(0,0%,0%,.15),
+ 0 1px 1px hsla(0,0%,0%,.1);
+}
+
+#toolbarViewerLeft {
+ margin-left: -1px;
+/* position: absolute; */
+ top: 0;
+ left: 0;
+}
+
+#toolbarViewerRight {
+/* position: absolute; */
+ top: 0;
+ right: 0;
+}
+
+#toolbarViewerLeft > *,
+#toolbarViewerMiddle > *,
+#toolbarViewerRight > * {
+ float: left;
+}
+
+.splitToolbarButton {
+ margin: 3px 2px 4px 0;
+ display: inline-block;
+}
+.splitToolbarButton > .toolbarButton {
+ border-radius: 0;
+ float: left;
+}
+
+.toolbarButton {
+ border: 0 none;
+ background-color: rgba(0, 0, 0, 0);
+ width: 32px;
+ height: 25px;
+}
+
+.toolbarButton > span {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+}
+
+.toolbarButton[disabled] {
+ opacity: .5;
+}
+
+.toolbarButton.group {
+ margin-right:0;
+}
+
+.splitToolbarButton.toggled .toolbarButton {
+ margin: 0;
+}
+
+.splitToolbarButton:hover > .toolbarButton,
+.splitToolbarButton:focus > .toolbarButton,
+.splitToolbarButton.toggled > .toolbarButton,
+.toolbarButton.textButton {
+ background-color: hsla(0,0%,0%,.12);
+ background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-clip: padding-box;
+ border: 1px solid hsla(0,0%,0%,.35);
+ border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42);
+ box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
+ 0 0 1px hsla(0,0%,100%,.15) inset,
+ 0 1px 0 hsla(0,0%,100%,.05);
+ -webkit-transition-property: background-color, border-color, box-shadow;
+ -webkit-transition-duration: 150ms;
+ -webkit-transition-timing-function: ease;
+ -moz-transition-property: background-color, border-color, box-shadow;
+ -moz-transition-duration: 150ms;
+ -moz-transition-timing-function: ease;
+ -ms-transition-property: background-color, border-color, box-shadow;
+ -ms-transition-duration: 150ms;
+ -ms-transition-timing-function: ease;
+ -o-transition-property: background-color, border-color, box-shadow;
+ -o-transition-duration: 150ms;
+ -o-transition-timing-function: ease;
+ transition-property: background-color, border-color, box-shadow;
+ transition-duration: 150ms;
+ transition-timing-function: ease;
+
+}
+.splitToolbarButton > .toolbarButton:hover,
+.splitToolbarButton > .toolbarButton:focus,
+.dropdownToolbarButton:hover,
+.toolbarButton.textButton:hover,
+.toolbarButton.textButton:focus {
+ background-color: hsla(0,0%,0%,.2);
+ box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
+ 0 0 1px hsla(0,0%,100%,.15) inset,
+ 0 0 1px hsla(0,0%,0%,.05);
+ z-index: 199;
+}
+.splitToolbarButton > .toolbarButton:first-child {
+ position: relative;
+ margin: 0;
+ margin-right: -1px;
+ border-top-left-radius: 2px;
+ border-bottom-left-radius: 2px;
+ border-right-color: transparent;
+}
+.splitToolbarButton > .toolbarButton:last-child {
+ position: relative;
+ margin: 0;
+ margin-left: -1px;
+ border-top-right-radius: 2px;
+ border-bottom-right-radius: 2px;
+ border-left-color: transparent;
+}
+.splitToolbarButtonSeparator {
+ padding: 8px 0;
+ width: 1px;
+ background-color: hsla(0,0%,00%,.5);
+ z-index: 99;
+ box-shadow: 0 0 0 1px hsla(0,0%,100%,.08);
+ display: inline-block;
+ margin: 5px 0;
+ float:left;
+}
+}
+.splitToolbarButton:hover > .splitToolbarButtonSeparator,
+.splitToolbarButton.toggled > .splitToolbarButtonSeparator {
+ padding: 12px 0;
+ margin: 1px 0;
+ box-shadow: 0 0 0 1px hsla(0,0%,100%,.03);
+ -webkit-transition-property: padding;
+ -webkit-transition-duration: 10ms;
+ -webkit-transition-timing-function: ease;
+ -moz-transition-property: padding;
+ -moz-transition-duration: 10ms;
+ -moz-transition-timing-function: ease;
+ -ms-transition-property: padding;
+ -ms-transition-duration: 10ms;
+ -ms-transition-timing-function: ease;
+ -o-transition-property: padding;
+ -o-transition-duration: 10ms;
+ -o-transition-timing-function: ease;
+ transition-property: padding;
+ transition-duration: 10ms;
+ transition-timing-function: ease;
+}
+
+.toolbarButton,
+.dropdownToolbarButton {
+ min-width: 16px;
+ padding: 2px 6px 0;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ color: hsl(0,0%,95%);
+ font-size: 12px;
+ line-height: 14px;
+ -webkit-user-select:none;
+ -moz-user-select:none;
+ -ms-user-select:none;
+ /* Opera does not support user-select, use <... unselectable="on"> instead */
+ cursor: default;
+ -webkit-transition-property: background-color, border-color, box-shadow;
+ -webkit-transition-duration: 150ms;
+ -webkit-transition-timing-function: ease;
+ -moz-transition-property: background-color, border-color, box-shadow;
+ -moz-transition-duration: 150ms;
+ -moz-transition-timing-function: ease;
+ -ms-transition-property: background-color, border-color, box-shadow;
+ -ms-transition-duration: 150ms;
+ -ms-transition-timing-function: ease;
+ -o-transition-property: background-color, border-color, box-shadow;
+ -o-transition-duration: 150ms;
+ -o-transition-timing-function: ease;
+ transition-property: background-color, border-color, box-shadow;
+ transition-duration: 150ms;
+ transition-timing-function: ease;
+}
+
+.toolbarButton {
+ margin: 3px 2px 4px 0;
+}
+
+.toolbarButton:hover,
+.toolbarButton:focus,
+.dropdownToolbarButton {
+ background-color: hsla(0,0%,0%,.12);
+ background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-clip: padding-box;
+ border: 1px solid hsla(0,0%,0%,.35);
+ border-color: hsla(0,0%,0%,.32) hsla(0,0%,0%,.38) hsla(0,0%,0%,.42);
+ box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
+ 0 0 1px hsla(0,0%,100%,.15) inset,
+ 0 1px 0 hsla(0,0%,100%,.05);
+}
+
+.toolbarButton:hover:active,
+.dropdownToolbarButton:hover:active {
+ background-color: hsla(0,0%,0%,.2);
+ background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ border-color: hsla(0,0%,0%,.35) hsla(0,0%,0%,.4) hsla(0,0%,0%,.45);
+ box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
+ 0 0 1px hsla(0,0%,0%,.2) inset,
+ 0 1px 0 hsla(0,0%,100%,.05);
+ -webkit-transition-property: background-color, border-color, box-shadow;
+ -webkit-transition-duration: 10ms;
+ -webkit-transition-timing-function: linear;
+ -moz-transition-property: background-color, border-color, box-shadow;
+ -moz-transition-duration: 10ms;
+ -moz-transition-timing-function: linear;
+ -ms-transition-property: background-color, border-color, box-shadow;
+ -ms-transition-duration: 10ms;
+ -ms-transition-timing-function: linear;
+ -o-transition-property: background-color, border-color, box-shadow;
+ -o-transition-duration: 10ms;
+ -o-transition-timing-function: linear;
+ transition-property: background-color, border-color, box-shadow;
+ transition-duration: 10ms;
+ transition-timing-function: linear;
+}
+
+.toolbarButton.toggled,
+.splitToolbarButton.toggled > .toolbarButton.toggled {
+ background-color: hsla(0,0%,0%,.3);
+ background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: -ms-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: -o-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
+ border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.45) hsla(0,0%,0%,.5);
+ box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
+ 0 0 1px hsla(0,0%,0%,.2) inset,
+ 0 1px 0 hsla(0,0%,100%,.05);
+ -webkit-transition-property: background-color, border-color, box-shadow;
+ -webkit-transition-duration: 10ms;
+ -webkit-transition-timing-function: linear;
+ -moz-transition-property: background-color, border-color, box-shadow;
+ -moz-transition-duration: 10ms;
+ -moz-transition-timing-function: linear;
+ -ms-transition-property: background-color, border-color, box-shadow;
+ -ms-transition-duration: 10ms;
+ -ms-transition-timing-function: linear;
+ -o-transition-property: background-color, border-color, box-shadow;
+ -o-transition-duration: 10ms;
+ -o-transition-timing-function: linear;
+ transition-property: background-color, border-color, box-shadow;
+ transition-duration: 10ms;
+ transition-timing-function: linear;
+}
+
+.toolbarButton.toggled:hover:active,
+.splitToolbarButton.toggled > .toolbarButton.toggled:hover:active {
+ background-color: hsla(0,0%,0%,.4);
+ border-color: hsla(0,0%,0%,.4) hsla(0,0%,0%,.5) hsla(0,0%,0%,.55);
+ box-shadow: 0 1px 1px hsla(0,0%,0%,.2) inset,
+ 0 0 1px hsla(0,0%,0%,.3) inset,
+ 0 1px 0 hsla(0,0%,100%,.05);
+}
+
+.dropdownToolbarButton {
+ max-width: 120px;
+ padding: 3px 2px 2px;
+ overflow: hidden;
+ background: url(vendor/pdfjs/images/toolbarButton-menuArrows.png) no-repeat;
+ background-position: 95%;
+}
+
+.dropdownToolbarButton > select {
+ -webkit-appearance: none;
+ -moz-appearance: none; /* in the future this might matter, see bugzilla bug #649849 */
+ min-width: 140px;
+ font-size: 12px;
+ color: hsl(0,0%,95%);
+ margin:0;
+ padding:0;
+ border:none;
+ background: rgba(0,0,0,0); /* Opera does not support 'transparent'