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:
Chris Dodge
2013-01-11 14:04:46 -05:00
35 changed files with 1572 additions and 301 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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.
'''

View File

@@ -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()},

View File

@@ -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":

View File

@@ -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)

View File

@@ -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)

View 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)