Merge branch 'master' into feature/cdodge/cms-master-merge3
Conflicts: common/lib/xmodule/xmodule/course_module.py common/lib/xmodule/xmodule/modulestore/mongo.py common/lib/xmodule/xmodule/modulestore/search.py common/lib/xmodule/xmodule/modulestore/xml.py doc/development.md lms/envs/common.py
This commit is contained in:
@@ -633,7 +633,7 @@ class MultipleChoiceResponse(LoncapaResponse):
|
||||
# define correct choices (after calling secondary setup)
|
||||
xml = self.xml
|
||||
cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]', id=xml.get('id'))
|
||||
self.correct_choices = [choice.get('name') for choice in cxml]
|
||||
self.correct_choices = [contextualize_text(choice.get('name'), self.context) for choice in cxml]
|
||||
|
||||
def mc_setup_response(self):
|
||||
'''
|
||||
@@ -727,7 +727,7 @@ class OptionResponse(LoncapaResponse):
|
||||
return cmap
|
||||
|
||||
def get_answers(self):
|
||||
amap = dict([(af.get('id'), af.get('correct')) for af in self.answer_fields])
|
||||
amap = dict([(af.get('id'), contextualize_text(af.get('correct'), self.context)) for af in self.answer_fields])
|
||||
# log.debug('%s: expected answers=%s' % (unicode(self),amap))
|
||||
return amap
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import logging
|
||||
from cStringIO import StringIO
|
||||
from lxml import etree
|
||||
from path import path # NOTE (THK): Only used for detecting presence of syllabus
|
||||
from xmodule.graders import grader_from_conf
|
||||
from path import path # NOTE (THK): Only used for detecting presence of syllabus
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from xmodule.graders import load_grading_policy
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.seq_module import SequenceDescriptor, SequenceModule
|
||||
from xmodule.timeparse import parse_time, stringify_time
|
||||
from xmodule.util.decorators import lazyproperty
|
||||
from xmodule.graders import grader_from_conf
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
@@ -16,11 +22,13 @@ import copy
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
remove_comments=True, remove_blank_text=True)
|
||||
|
||||
_cached_toc = {}
|
||||
|
||||
|
||||
class CourseDescriptor(SequenceDescriptor):
|
||||
module_class = SequenceModule
|
||||
|
||||
@@ -334,6 +342,38 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
def show_calculator(self):
|
||||
return self.metadata.get("show_calculator", None) == "Yes"
|
||||
|
||||
@property
|
||||
def is_new(self):
|
||||
# The course is "new" if either if the metadata flag is_new is
|
||||
# true or if the course has not started yet
|
||||
flag = self.metadata.get('is_new', None)
|
||||
if flag is None:
|
||||
return self.days_until_start > 1
|
||||
elif isinstance(flag, basestring):
|
||||
return flag.lower() in ['true', 'yes', 'y']
|
||||
else:
|
||||
return bool(flag)
|
||||
|
||||
@property
|
||||
def days_until_start(self):
|
||||
def convert_to_datetime(timestamp):
|
||||
return datetime.fromtimestamp(time.mktime(timestamp))
|
||||
|
||||
start_date = convert_to_datetime(self.start)
|
||||
|
||||
# Try to use course advertised date if we can parse it
|
||||
advertised_start = self.metadata.get('advertised_start', None)
|
||||
if advertised_start:
|
||||
try:
|
||||
start_date = datetime.strptime(advertised_start,
|
||||
"%Y-%m-%dT%H:%M")
|
||||
except ValueError:
|
||||
pass # Invalid date, keep using 'start''
|
||||
|
||||
now = convert_to_datetime(time.gmtime())
|
||||
days_until_start = (start_date - now).days
|
||||
return days_until_start
|
||||
|
||||
@lazyproperty
|
||||
def grading_context(self):
|
||||
"""
|
||||
@@ -413,7 +453,6 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
raise ValueError("{0} is not a course location".format(loc))
|
||||
return "/".join([loc.org, loc.course, loc.name])
|
||||
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Return the course_id for this course"""
|
||||
@@ -427,7 +466,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
# form text...
|
||||
if parsed_advertised_start is None and \
|
||||
('advertised_start' in self.metadata):
|
||||
return self.metadata['advertised_start']
|
||||
return self.metadata['advertised_start']
|
||||
|
||||
displayed_start = parsed_advertised_start or self.start
|
||||
|
||||
@@ -511,5 +550,3 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
def org(self):
|
||||
return self.location.org
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -364,9 +364,9 @@ class ModuleStore(object):
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def get_parent_locations(self, location):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
|
||||
returns an iterable of things that can be passed to Location.
|
||||
'''
|
||||
|
||||
@@ -402,6 +402,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
self._update_single_item(location, {'metadata': metadata})
|
||||
|
||||
|
||||
def delete_item(self, location):
|
||||
"""
|
||||
Delete an item from this modulestore
|
||||
@@ -420,12 +421,10 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
self.collection.remove({'_id': Location(location).dict()})
|
||||
|
||||
def get_parent_locations(self, location):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
|
||||
returns an iterable of things that can be passed to Location. This may
|
||||
be empty if there are no parents.
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
'''
|
||||
location = Location.ensure_fully_specified(location)
|
||||
items = self.collection.find({'definition.children': location.url()},
|
||||
|
||||
@@ -60,9 +60,11 @@ def path_to_location(modulestore, course_id, location):
|
||||
(loc, path) = queue.pop() # Takes from the end
|
||||
loc = Location(loc)
|
||||
|
||||
# Call get_parent_locations first to make sure the location is there
|
||||
# (even if it's a course, and we would otherwise immediately exit).
|
||||
parents = modulestore.get_parent_locations(loc)
|
||||
# get_parent_locations should raise ItemNotFoundError if location
|
||||
# isn't found so we don't have to do it explicitly. Call this
|
||||
# first to make sure the location is there (even if it's a course, and
|
||||
# we would otherwise immediately exit).
|
||||
parents = modulestore.get_parent_locations(loc, course_id)
|
||||
|
||||
# print 'Processing loc={0}, path={1}'.format(loc, path)
|
||||
if loc.category == "course":
|
||||
|
||||
@@ -23,12 +23,3 @@ def check_path_to_location(modulestore):
|
||||
for location in not_found:
|
||||
assert_raises(ItemNotFoundError, path_to_location, modulestore, course_id, location)
|
||||
|
||||
# Since our test files are valid, there shouldn't be any
|
||||
# elements with no path to them. But we can look for them in
|
||||
# another course.
|
||||
no_path = (
|
||||
"i4x://edX/simple/video/Lost_Video",
|
||||
)
|
||||
for location in no_path:
|
||||
assert_raises(NoPathToItem, path_to_location, modulestore, course_id, location)
|
||||
|
||||
|
||||
@@ -280,14 +280,16 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
self.default_class = class_
|
||||
|
||||
self.parent_tracker = ParentTracker()
|
||||
self.parent_trackers = defaultdict(ParentTracker)
|
||||
|
||||
# If we are specifically asked for missing courses, that should
|
||||
# be an error. If we are asked for "all" courses, find the ones
|
||||
# that have a course.xml
|
||||
# that have a course.xml. We sort the dirs in alpha order so we always
|
||||
# read things in the same order (OS differences in load order have
|
||||
# bitten us in the past.)
|
||||
if course_dirs is None:
|
||||
course_dirs = [d for d in os.listdir(self.data_dir) if
|
||||
os.path.exists(self.data_dir / d / "course.xml")]
|
||||
course_dirs = sorted([d for d in os.listdir(self.data_dir) if
|
||||
os.path.exists(self.data_dir / d / "course.xml")])
|
||||
|
||||
for course_dir in course_dirs:
|
||||
self.try_load_course(course_dir)
|
||||
@@ -312,7 +314,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
if course_descriptor is not None:
|
||||
self.courses[course_dir] = course_descriptor
|
||||
self._location_errors[course_descriptor.location] = errorlog
|
||||
self.parent_tracker.make_known(course_descriptor.location)
|
||||
self.parent_trackers[course_descriptor.id].make_known(course_descriptor.location)
|
||||
else:
|
||||
# Didn't load course. Instead, save the errors elsewhere.
|
||||
self.errored_courses[course_dir] = errorlog
|
||||
@@ -415,7 +417,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
course_dir,
|
||||
policy,
|
||||
tracker,
|
||||
self.parent_tracker,
|
||||
self.parent_trackers[course_id],
|
||||
self.load_error_modules,
|
||||
)
|
||||
|
||||
@@ -578,12 +580,15 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
raise NotImplementedError("XMLModuleStores are read-only")
|
||||
|
||||
def get_parent_locations(self, location):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
course. Needed for path_to_location().
|
||||
|
||||
returns an iterable of things that can be passed to Location. This may
|
||||
be empty if there are no parents.
|
||||
'''
|
||||
location = Location.ensure_fully_specified(location)
|
||||
return self.parent_tracker.parents(location)
|
||||
if not self.parent_trackers[course_id].is_known(location):
|
||||
raise ItemNotFoundError("{0} not in {1}".format(location, course_id))
|
||||
|
||||
return self.parent_trackers[course_id].parents(location)
|
||||
|
||||
90
common/lib/xmodule/xmodule/tests/test_course_module.py
Normal file
90
common/lib/xmodule/xmodule/tests/test_course_module.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import unittest
|
||||
from time import strptime, gmtime
|
||||
from fs.memoryfs import MemoryFS
|
||||
|
||||
from mock import Mock, patch
|
||||
|
||||
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
|
||||
|
||||
|
||||
ORG = 'test_org'
|
||||
COURSE = 'test_course'
|
||||
|
||||
NOW = strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00')
|
||||
|
||||
|
||||
class DummySystem(ImportSystem):
|
||||
@patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
|
||||
def __init__(self, load_error_modules):
|
||||
|
||||
xmlstore = XMLModuleStore("data_dir", course_dirs=[],
|
||||
load_error_modules=load_error_modules)
|
||||
course_id = "/".join([ORG, COURSE, 'test_run'])
|
||||
course_dir = "test_dir"
|
||||
policy = {}
|
||||
error_tracker = Mock()
|
||||
parent_tracker = Mock()
|
||||
|
||||
super(DummySystem, self).__init__(
|
||||
xmlstore,
|
||||
course_id,
|
||||
course_dir,
|
||||
policy,
|
||||
error_tracker,
|
||||
parent_tracker,
|
||||
load_error_modules=load_error_modules,
|
||||
)
|
||||
|
||||
|
||||
class IsNewCourseTestCase(unittest.TestCase):
|
||||
"""Make sure the property is_new works on courses"""
|
||||
@staticmethod
|
||||
def get_dummy_course(start, is_new=None, load_error_modules=True):
|
||||
"""Get a dummy course"""
|
||||
|
||||
system = DummySystem(load_error_modules)
|
||||
is_new = '' if is_new is None else 'is_new="{0}"'.format(is_new).lower()
|
||||
|
||||
start_xml = '''
|
||||
<course org="{org}" course="{course}"
|
||||
graceperiod="1 day" url_name="test"
|
||||
start="{start}"
|
||||
{is_new}>
|
||||
<chapter url="hi" url_name="ch" display_name="CH">
|
||||
<html url_name="h" display_name="H">Two houses, ...</html>
|
||||
</chapter>
|
||||
</course>
|
||||
'''.format(org=ORG, course=COURSE, start=start, is_new=is_new)
|
||||
|
||||
return system.process_xml(start_xml)
|
||||
|
||||
@patch('xmodule.course_module.time.gmtime')
|
||||
def test_non_started_yet(self, gmtime_mock):
|
||||
descriptor = self.get_dummy_course(start='2013-01-05T12:00')
|
||||
gmtime_mock.return_value = NOW
|
||||
assert(descriptor.is_new == True)
|
||||
assert(descriptor.days_until_start == 4)
|
||||
|
||||
@patch('xmodule.course_module.time.gmtime')
|
||||
def test_already_started(self, gmtime_mock):
|
||||
gmtime_mock.return_value = NOW
|
||||
|
||||
descriptor = self.get_dummy_course(start='2012-12-02T12:00')
|
||||
assert(descriptor.is_new == False)
|
||||
assert(descriptor.days_until_start < 0)
|
||||
|
||||
@patch('xmodule.course_module.time.gmtime')
|
||||
def test_is_new_set(self, gmtime_mock):
|
||||
gmtime_mock.return_value = NOW
|
||||
|
||||
descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True)
|
||||
assert(descriptor.is_new == True)
|
||||
assert(descriptor.days_until_start < 0)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=False)
|
||||
assert(descriptor.is_new == False)
|
||||
assert(descriptor.days_until_start > 0)
|
||||
|
||||
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=True)
|
||||
assert(descriptor.is_new == True)
|
||||
assert(descriptor.days_until_start > 0)
|
||||
Reference in New Issue
Block a user