This commit is contained in:
cahrens
2013-01-07 12:06:12 -05:00
201 changed files with 6598 additions and 2893 deletions

View File

@@ -7,6 +7,7 @@ source = common/lib/xmodule
ignore_errors = True
[html]
title = XModule Python Test Coverage Report
directory = reports/common/lib/xmodule/cover
[xml]

View File

@@ -145,6 +145,11 @@ class CapaModule(XModule):
else:
self.seed = None
# Need the problem location in openendedresponse to send out. Adding
# it to the system here seems like the least clunky way to get it
# there.
self.system.set('location', self.location.url())
try:
# TODO (vshnayder): move as much as possible of this work and error
# checking to descriptor load time

View File

@@ -186,7 +186,8 @@ class CourseDescriptor(SequenceDescriptor):
instance = super(CourseDescriptor, cls).from_xml(xml_data, system, org, course)
# bleh, have to parse the XML here to just pull out the url_name attribute
course_file = StringIO(xml_data)
# I don't think it's stored anywhere in the instance.
course_file = StringIO(xml_data.encode('ascii','ignore'))
xml_obj = etree.parse(course_file,parser=edx_xml_parser).getroot()
policy_dir = None
@@ -293,6 +294,10 @@ class CourseDescriptor(SequenceDescriptor):
self.definition['data'].setdefault('grading_policy',{})['GRADE_CUTOFFS'] = value
@property
def lowest_passing_grade(self):
return min(self._grading_policy['GRADE_CUTOFFS'].values())
@property
def tabs(self):
"""
@@ -395,7 +400,20 @@ class CourseDescriptor(SequenceDescriptor):
@property
def start_date_text(self):
displayed_start = self._try_parse_time('advertised_start') or self.start
parsed_advertised_start = self._try_parse_time('advertised_start')
# If the advertised start isn't a real date string, we assume it's free
# form text...
if parsed_advertised_start is None and \
('advertised_start' in self.metadata):
return self.metadata['advertised_start']
displayed_start = parsed_advertised_start or self.start
# If we have neither an advertised start or a real start, just return TBD
if not displayed_start:
return "TBD"
return time.strftime("%b %d, %Y", displayed_start)
@property
@@ -440,7 +458,7 @@ class CourseDescriptor(SequenceDescriptor):
return False
except:
log.exception("Error parsing discussion_blackouts for course {0}".format(self.id))
return True
@property

View File

@@ -121,16 +121,6 @@ section.problem {
}
}
&.processing {
p.status {
@include inline-block();
background: url('../images/spinner.gif') center center no-repeat;
height: 20px;
width: 20px;
text-indent: -9999px;
}
}
&.correct, &.ui-icon-check {
p.status {
@include inline-block();
@@ -250,6 +240,13 @@ section.problem {
}
}
.reload
{
float:right;
margin: 10px;
}
.grader-status {
padding: 9px;
background: #F6F6F6;
@@ -266,6 +263,13 @@ section.problem {
margin: -7px 7px 0 0;
}
.grading {
background: url('../images/info-icon.png') left center no-repeat;
padding-left: 25px;
text-indent: 0px;
margin: 0px 7px 0 0;
}
p {
line-height: 20px;
text-transform: capitalize;
@@ -685,6 +689,21 @@ section.problem {
color: #B00;
}
}
.markup-text{
margin: 5px;
padding: 20px 0px 15px 50px;
border-top: 1px solid #DDD;
border-left: 20px solid #FAFAFA;
bs {
color: #BB0000;
}
bg {
color: #BDA046;
}
}
}
}
}

View File

@@ -149,14 +149,14 @@ class ErrorDescriptor(JSONEditingDescriptor):
'''
try:
xml = etree.fromstring(self.definition['data']['contents'])
return etree.tostring(xml)
return etree.tostring(xml, encoding='unicode')
except etree.XMLSyntaxError:
# still not valid.
root = etree.Element('error')
root.text = self.definition['data']['contents']
err_node = etree.SubElement(root, 'error_msg')
err_node.text = self.definition['data']['error_msg']
return etree.tostring(root)
return etree.tostring(root, encoding='unicode')
class NonStaffErrorDescriptor(ErrorDescriptor):

View File

@@ -6,15 +6,14 @@ import sys
from lxml import etree
from path import path
from .x_module import XModule
from pkg_resources import resource_string
from .xml_module import XmlDescriptor, name_to_pathname
from .editing_module import EditingDescriptor
from .stringify import stringify_children
from .html_checker import check_html
from xmodule.modulestore import Location
from xmodule.contentstore.content import XASSET_SRCREF_PREFIX, StaticContent
from xmodule.editing_module import EditingDescriptor
from xmodule.html_checker import check_html
from xmodule.modulestore import Location
from xmodule.stringify import stringify_children
from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor, name_to_pathname
log = logging.getLogger("mitx.courseware")
@@ -121,7 +120,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
try:
with system.resources_fs.open(filepath) as file:
html = file.read()
html = file.read().decode('utf-8')
# Log a warning if we can't parse the file, but don't error
if not check_html(html):
msg = "Couldn't parse html in {0}.".format(filepath)
@@ -162,7 +161,7 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True)
with resource_fs.open(filepath, 'w') as file:
file.write(self.definition['data'])
file.write(self.definition['data'].encode('utf-8'))
# write out the relative name
relname = path(pathname).basename()

View File

@@ -1,149 +1,150 @@
#describe 'Video', ->
# beforeEach ->
# loadFixtures 'video.html'
# jasmine.stubRequests()
#
# @videosDefinition = '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
# @slowerSpeedYoutubeId = 'slowerSpeedYoutubeId'
# @normalSpeedYoutubeId = 'normalSpeedYoutubeId'
#
# afterEach ->
# window.player = undefined
# window.onYouTubePlayerAPIReady = undefined
#
# describe 'constructor', ->
# beforeEach ->
# @stubVideoPlayer = jasmine.createSpy('VideoPlayer')
# $.cookie.andReturn '0.75'
# window.player = 100
#
# describe 'by default', ->
# beforeEach ->
# @video = new Video 'example', @videosDefinition
#
# it 'reset the current video player', ->
# expect(window.player).toBeNull()
#
# it 'set the elements', ->
# expect(@video.el).toBe '#video_example'
#
# it 'parse the videos', ->
# expect(@video.videos).toEqual
# '0.75': @slowerSpeedYoutubeId
# '1.0': @normalSpeedYoutubeId
#
# it 'fetch the video metadata', ->
# expect(@video.metadata).toEqual
# slowerSpeedYoutubeId:
# id: @slowerSpeedYoutubeId
# duration: 300
# normalSpeedYoutubeId:
# id: @normalSpeedYoutubeId
# duration: 200
#
# it 'parse available video speeds', ->
# expect(@video.speeds).toEqual ['0.75', '1.0']
#
# it 'set current video speed via cookie', ->
# expect(@video.speed).toEqual '0.75'
#
# it 'store a reference for this video player in the element', ->
# expect($('.video').data('video')).toEqual @video
#
# describe 'when the Youtube API is already available', ->
# beforeEach ->
# @originalYT = window.YT
# window.YT = { Player: true }
# spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
# @video = new Video 'example', @videosDefinition
#
# afterEach ->
# window.YT = @originalYT
#
# it 'create the Video Player', ->
# expect(window.VideoPlayer).toHaveBeenCalledWith(video: @video)
# expect(@video.player).toEqual @stubVideoPlayer
#
# describe 'when the Youtube API is not ready', ->
# beforeEach ->
# @originalYT = window.YT
# window.YT = {}
# @video = new Video 'example', @videosDefinition
#
# afterEach ->
# window.YT = @originalYT
#
# it 'set the callback on the window object', ->
# expect(window.onYouTubePlayerAPIReady).toEqual jasmine.any(Function)
#
# describe 'when the Youtube API becoming ready', ->
# beforeEach ->
# @originalYT = window.YT
# window.YT = {}
# spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
# @video = new Video 'example', @videosDefinition
# window.onYouTubePlayerAPIReady()
#
# afterEach ->
# window.YT = @originalYT
#
# it 'create the Video Player for all video elements', ->
# expect(window.VideoPlayer).toHaveBeenCalledWith(video: @video)
# expect(@video.player).toEqual @stubVideoPlayer
#
# describe 'youtubeId', ->
# beforeEach ->
# $.cookie.andReturn '1.0'
# @video = new Video 'example', @videosDefinition
#
# describe 'with speed', ->
# it 'return the video id for given speed', ->
# expect(@video.youtubeId('0.75')).toEqual @slowerSpeedYoutubeId
# expect(@video.youtubeId('1.0')).toEqual @normalSpeedYoutubeId
#
# describe 'without speed', ->
# it 'return the video id for current speed', ->
# expect(@video.youtubeId()).toEqual @normalSpeedYoutubeId
#
# describe 'setSpeed', ->
# beforeEach ->
# @video = new Video 'example', @videosDefinition
#
# describe 'when new speed is available', ->
# beforeEach ->
# @video.setSpeed '0.75'
#
# it 'set new speed', ->
# expect(@video.speed).toEqual '0.75'
#
# it 'save setting for new speed', ->
# expect($.cookie).toHaveBeenCalledWith 'video_speed', '0.75', expires: 3650, path: '/'
#
# describe 'when new speed is not available', ->
# beforeEach ->
# @video.setSpeed '1.75'
#
# it 'set speed to 1.0x', ->
# expect(@video.speed).toEqual '1.0'
#
# describe 'getDuration', ->
# beforeEach ->
# @video = new Video 'example', @videosDefinition
#
# it 'return duration for current video', ->
# expect(@video.getDuration()).toEqual 200
#
# describe 'log', ->
# beforeEach ->
# @video = new Video 'example', @videosDefinition
# @video.setSpeed '1.0'
# spyOn Logger, 'log'
# @video.player = { currentTime: 25 }
# @video.log 'someEvent'
#
# it 'call the logger with valid parameters', ->
# expect(Logger.log).toHaveBeenCalledWith 'someEvent',
# id: 'example'
# code: @normalSpeedYoutubeId
# currentTime: 25
# speed: '1.0'
# TODO: figure out why failing
xdescribe 'Video', ->
beforeEach ->
loadFixtures 'video.html'
jasmine.stubRequests()
@videosDefinition = '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
@slowerSpeedYoutubeId = 'slowerSpeedYoutubeId'
@normalSpeedYoutubeId = 'normalSpeedYoutubeId'
afterEach ->
window.player = undefined
window.onYouTubePlayerAPIReady = undefined
describe 'constructor', ->
beforeEach ->
@stubVideoPlayer = jasmine.createSpy('VideoPlayer')
$.cookie.andReturn '0.75'
window.player = 100
describe 'by default', ->
beforeEach ->
@video = new Video 'example', @videosDefinition
it 'reset the current video player', ->
expect(window.player).toBeNull()
it 'set the elements', ->
expect(@video.el).toBe '#video_example'
it 'parse the videos', ->
expect(@video.videos).toEqual
'0.75': @slowerSpeedYoutubeId
'1.0': @normalSpeedYoutubeId
it 'fetch the video metadata', ->
expect(@video.metadata).toEqual
slowerSpeedYoutubeId:
id: @slowerSpeedYoutubeId
duration: 300
normalSpeedYoutubeId:
id: @normalSpeedYoutubeId
duration: 200
it 'parse available video speeds', ->
expect(@video.speeds).toEqual ['0.75', '1.0']
it 'set current video speed via cookie', ->
expect(@video.speed).toEqual '0.75'
it 'store a reference for this video player in the element', ->
expect($('.video').data('video')).toEqual @video
describe 'when the Youtube API is already available', ->
beforeEach ->
@originalYT = window.YT
window.YT = { Player: true }
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video 'example', @videosDefinition
afterEach ->
window.YT = @originalYT
it 'create the Video Player', ->
expect(window.VideoPlayer).toHaveBeenCalledWith(video: @video)
expect(@video.player).toEqual @stubVideoPlayer
describe 'when the Youtube API is not ready', ->
beforeEach ->
@originalYT = window.YT
window.YT = {}
@video = new Video 'example', @videosDefinition
afterEach ->
window.YT = @originalYT
it 'set the callback on the window object', ->
expect(window.onYouTubePlayerAPIReady).toEqual jasmine.any(Function)
describe 'when the Youtube API becoming ready', ->
beforeEach ->
@originalYT = window.YT
window.YT = {}
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video 'example', @videosDefinition
window.onYouTubePlayerAPIReady()
afterEach ->
window.YT = @originalYT
it 'create the Video Player for all video elements', ->
expect(window.VideoPlayer).toHaveBeenCalledWith(video: @video)
expect(@video.player).toEqual @stubVideoPlayer
describe 'youtubeId', ->
beforeEach ->
$.cookie.andReturn '1.0'
@video = new Video 'example', @videosDefinition
describe 'with speed', ->
it 'return the video id for given speed', ->
expect(@video.youtubeId('0.75')).toEqual @slowerSpeedYoutubeId
expect(@video.youtubeId('1.0')).toEqual @normalSpeedYoutubeId
describe 'without speed', ->
it 'return the video id for current speed', ->
expect(@video.youtubeId()).toEqual @normalSpeedYoutubeId
describe 'setSpeed', ->
beforeEach ->
@video = new Video 'example', @videosDefinition
describe 'when new speed is available', ->
beforeEach ->
@video.setSpeed '0.75'
it 'set new speed', ->
expect(@video.speed).toEqual '0.75'
it 'save setting for new speed', ->
expect($.cookie).toHaveBeenCalledWith 'video_speed', '0.75', expires: 3650, path: '/'
describe 'when new speed is not available', ->
beforeEach ->
@video.setSpeed '1.75'
it 'set speed to 1.0x', ->
expect(@video.speed).toEqual '1.0'
describe 'getDuration', ->
beforeEach ->
@video = new Video 'example', @videosDefinition
it 'return duration for current video', ->
expect(@video.getDuration()).toEqual 200
describe 'log', ->
beforeEach ->
@video = new Video 'example', @videosDefinition
@video.setSpeed '1.0'
spyOn Logger, 'log'
@video.player = { currentTime: 25 }
@video.log 'someEvent'
it 'call the logger with valid parameters', ->
expect(Logger.log).toHaveBeenCalledWith 'someEvent',
id: 'example'
code: @normalSpeedYoutubeId
currentTime: 25
speed: '1.0'

View File

@@ -1953,7 +1953,7 @@ cktsim = (function() {
var module = {
'Circuit': Circuit,
'parse_number': parse_number,
'parse_source': parse_source,
'parse_source': parse_source
}
return module;
}());
@@ -2068,7 +2068,7 @@ schematic = (function() {
'n': [NFet, 'NFet'],
'p': [PFet, 'PFet'],
's': [Probe, 'Voltage Probe'],
'a': [Ammeter, 'Current Probe'],
'a': [Ammeter, 'Current Probe']
};
// global clipboard
@@ -5502,7 +5502,7 @@ schematic = (function() {
'magenta' : 'rgb(255,64,255)',
'yellow': 'rgb(255,255,64)',
'black': 'rgb(0,0,0)',
'x-axis': undefined,
'x-axis': undefined
};
function Probe(x,y,rotation,color,offset) {
@@ -6100,7 +6100,7 @@ schematic = (function() {
'Amplitude',
'Frequency (Hz)',
'Delay until sin starts (secs)',
'Phase offset (degrees)'],
'Phase offset (degrees)']
}
// build property editor div
@@ -6300,7 +6300,7 @@ schematic = (function() {
var module = {
'Schematic': Schematic,
'component_slider': component_slider,
'component_slider': component_slider
}
return module;
}());

View File

@@ -120,7 +120,7 @@ class @SelfAssessment
if @state == 'done'
$.postWithPrefix "#{@ajax_url}/reset", {}, (response) =>
if response.success
@answer_area.html('')
@answer_area.val('')
@rubric_wrapper.html('')
@hint_wrapper.html('')
@message_wrapper.html('')

View File

@@ -2,6 +2,8 @@ class @Video
constructor: (element) ->
@el = $(element).find('.video')
@id = @el.attr('id').replace(/video_/, '')
@start = @el.data('start')
@end = @el.data('end')
@caption_data_dir = @el.data('caption-data-dir')
@caption_asset_path = @el.data('caption-asset-path')
@show_captions = @el.data('show-captions') == "true"

View File

@@ -36,14 +36,21 @@ class @VideoPlayer extends Subview
@volumeControl = new VideoVolumeControl el: @$('.secondary-controls')
@speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed()
@progressSlider = new VideoProgressSlider el: @$('.slider')
@playerVars =
controls: 0
wmode: 'transparent'
rel: 0
showinfo: 0
enablejsapi: 1
modestbranding: 1
if @video.start
@playerVars.start = @video.start
if @video.end
# work in AS3, not HMLT5. but iframe use AS3
@playerVars.end = @video.end
@player = new YT.Player @video.id,
playerVars:
controls: 0
wmode: 'transparent'
rel: 0
showinfo: 0
enablejsapi: 1
modestbranding: 1
playerVars: @playerVars
videoId: @video.youtubeId()
events:
onReady: @onReady

View File

@@ -352,6 +352,12 @@ class ModuleStore(object):
course_filter = Location("i4x", category="course")
return self.get_items(course_filter)
def get_course(self, course_id):
'''
Look for a specific course id. Returns the course descriptor, or None if not found.
'''
raise NotImplementedError
def get_parent_locations(self, location):
'''Find all locations that are the parents of this location. Needed
for path_to_location().
@@ -413,3 +419,10 @@ class ModuleStoreBase(ModuleStore):
errorlog = self._get_errorlog(location)
return errorlog.errors
def get_course(self, course_id):
"""Default impl--linear search through course list"""
for c in self.get_courses():
if c.id == course_id:
return c
return None

View File

@@ -157,7 +157,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
make_name_unique(xml_data)
descriptor = XModuleDescriptor.load_from_xml(
etree.tostring(xml_data), self, self.org,
etree.tostring(xml_data, encoding='unicode'), self, self.org,
self.course, xmlstore.default_class)
except Exception as err:
print err, self.load_error_modules
@@ -419,7 +419,7 @@ class XMLModuleStore(ModuleStoreBase):
self.load_error_modules,
)
course_descriptor = system.process_xml(etree.tostring(course_data))
course_descriptor = system.process_xml(etree.tostring(course_data, encoding='unicode'))
# NOTE: The descriptors end up loading somewhat bottom up, which
# breaks metadata inheritance via get_children(). Instead
@@ -513,6 +513,18 @@ class XMLModuleStore(ModuleStoreBase):
raise NotImplementedError("XMLModuleStores can't guarantee that definitions"
" are unique. Use get_instance.")
def get_items(self, location, depth=0):
items = []
for _, modules in self.modules.iteritems():
for mod_loc, module in modules.iteritems():
# Locations match if each value in `location` is None or if the value from `location`
# matches the value from `mod_loc`
if all(goal is None or goal == value for goal, value in zip(location, mod_loc)):
items.append(module)
return items
def get_courses(self, depth=0):
"""

View File

@@ -13,7 +13,7 @@ class RawDescriptor(XmlDescriptor, XMLEditingDescriptor):
"""
@classmethod
def definition_from_xml(cls, xml_object, system):
return {'data': etree.tostring(xml_object, pretty_print=True)}
return {'data': etree.tostring(xml_object, pretty_print=True,encoding='unicode')}
def definition_to_xml(self, resource_fs):
try:

View File

@@ -7,20 +7,21 @@ Parses xml definition file--see below for exact format.
import copy
from fs.errors import ResourceNotFoundError
import itertools
import json
import logging
import os
import sys
from lxml import etree
from lxml.html import rewrite_links
from path import path
import json
from progress import Progress
import os
import sys
from pkg_resources import resource_string
from .capa_module import only_one, ComplexEncoder
from .editing_module import EditingDescriptor
from .html_checker import check_html
from progress import Progress
from .stringify import stringify_children
from .x_module import XModule
from .xml_module import XmlDescriptor
@@ -52,6 +53,8 @@ class SelfAssessmentModule(XModule):
submissions too.)
"""
STATE_VERSION = 1
# states
INITIAL = 'initial'
ASSESSING = 'assessing'
@@ -102,35 +105,130 @@ class SelfAssessmentModule(XModule):
else:
instance_state = {}
# Note: score responses are on scale from 0 to max_score
self.student_answers = instance_state.get('student_answers', [])
self.scores = instance_state.get('scores', [])
self.hints = instance_state.get('hints', [])
instance_state = self.convert_state_to_current_format(instance_state)
# History is a list of tuples of (answer, score, hint), where hint may be
# None for any element, and score and hint can be None for the last (current)
# element.
# Scores are on scale from 0 to max_score
self.history = instance_state.get('history', [])
self.state = instance_state.get('state', 'initial')
self.attempts = instance_state.get('attempts', 0)
self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS))
# Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = int(self.metadata.get('max_score', MAX_SCORE))
self.attempts = instance_state.get('attempts', 0)
self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS))
self.rubric = definition['rubric']
self.prompt = definition['prompt']
self.submit_message = definition['submitmessage']
self.hint_prompt = definition['hintprompt']
def latest_answer(self):
"""None if not available"""
if not self.history:
return None
return self.history[-1].get('answer')
def latest_score(self):
"""None if not available"""
if not self.history:
return None
return self.history[-1].get('score')
def latest_hint(self):
"""None if not available"""
if not self.history:
return None
return self.history[-1].get('hint')
def new_history_entry(self, answer):
self.history.append({'answer': answer})
def record_latest_score(self, score):
"""Assumes that state is right, so we're adding a score to the latest
history element"""
self.history[-1]['score'] = score
def record_latest_hint(self, hint):
"""Assumes that state is right, so we're adding a score to the latest
history element"""
self.history[-1]['hint'] = hint
def change_state(self, new_state):
"""
A centralized place for state changes--allows for hooks. If the
current state matches the old state, don't run any hooks.
"""
if self.state == new_state:
return
self.state = new_state
if self.state == self.DONE:
self.attempts += 1
@staticmethod
def convert_state_to_current_format(old_state):
"""
This module used to use a problematic state representation. This method
converts that into the new format.
Args:
old_state: dict of state, as passed in. May be old.
Returns:
new_state: dict of new state
"""
if old_state.get('version', 0) == SelfAssessmentModule.STATE_VERSION:
# already current
return old_state
# for now, there's only one older format.
new_state = {'version': SelfAssessmentModule.STATE_VERSION}
def copy_if_present(key):
if key in old_state:
new_state[key] = old_state[key]
for to_copy in ['attempts', 'state']:
copy_if_present(to_copy)
# The answers, scores, and hints need to be kept together to avoid them
# getting out of sync.
# NOTE: Since there's only one problem with a few hundred submissions
# in production so far, not trying to be smart about matching up hints
# and submissions in cases where they got out of sync.
student_answers = old_state.get('student_answers', [])
scores = old_state.get('scores', [])
hints = old_state.get('hints', [])
new_state['history'] = [
{'answer': answer,
'score': score,
'hint': hint}
for answer, score, hint in itertools.izip_longest(
student_answers, scores, hints)]
return new_state
def _allow_reset(self):
"""Can the module be reset?"""
return self.state == self.DONE and self.attempts < self.max_attempts
def get_html(self):
#set context variables and render template
if self.state != self.INITIAL and self.student_answers:
previous_answer = self.student_answers[-1]
if self.state != self.INITIAL:
latest = self.latest_answer()
previous_answer = latest if latest is not None else ''
else:
previous_answer = ''
@@ -149,26 +247,19 @@ class SelfAssessmentModule(XModule):
# cdodge: perform link substitutions for any references to course static content (e.g. images)
return rewrite_links(html, self.rewrite_content_links)
def get_score(self):
"""
Returns dict with 'score' key
"""
return {'score': self.get_last_score()}
def max_score(self):
"""
Return max_score
"""
return self._max_score
def get_last_score(self):
def get_score(self):
"""
Returns the last score in the list
"""
last_score=0
if(len(self.scores)>0):
last_score=self.scores[len(self.scores)-1]
return last_score
score = self.latest_score()
return {'score': score if score is not None else 0,
'total': self._max_score}
def get_progress(self):
'''
@@ -176,7 +267,7 @@ class SelfAssessmentModule(XModule):
'''
if self._max_score > 0:
try:
return Progress(self.get_last_score(), self._max_score)
return Progress(self.get_score()['score'], self._max_score)
except Exception as err:
log.exception("Got bad progress")
return None
@@ -250,9 +341,10 @@ class SelfAssessmentModule(XModule):
if self.state in (self.INITIAL, self.ASSESSING):
return ''
if self.state == self.DONE and len(self.hints) > 0:
if self.state == self.DONE:
# display the previous hint
hint = self.hints[-1]
latest = self.latest_hint()
hint = latest if latest is not None else ''
else:
hint = ''
@@ -281,6 +373,14 @@ class SelfAssessmentModule(XModule):
def save_answer(self, get):
"""
After the answer is submitted, show the rubric.
Args:
get: the GET dictionary passed to the ajax request. Should contain
a key 'student_answer'
Returns:
Dictionary with keys 'success' and either 'error' (if not success),
or 'rubric_html' (if success).
"""
# Check to see if attempts are less than max
if self.attempts > self.max_attempts:
@@ -295,8 +395,9 @@ class SelfAssessmentModule(XModule):
if self.state != self.INITIAL:
return self.out_of_sync_error(get)
self.student_answers.append(get['student_answer'])
self.state = self.ASSESSING
# add new history element with answer and empty score and hint.
self.new_history_entry(get['student_answer'])
self.change_state(self.ASSESSING)
return {
'success': True,
@@ -318,27 +419,24 @@ class SelfAssessmentModule(XModule):
'message_html' only if success is true
"""
n_answers = len(self.student_answers)
n_scores = len(self.scores)
if (self.state != self.ASSESSING or n_answers != n_scores + 1):
msg = "%d answers, %d scores" % (n_answers, n_scores)
return self.out_of_sync_error(get, msg)
if self.state != self.ASSESSING:
return self.out_of_sync_error(get)
try:
score = int(get['assessment'])
except:
except ValueError:
return {'success': False, 'error': "Non-integer score value"}
self.scores.append(score)
self.record_latest_score(score)
d = {'success': True,}
if score == self.max_score():
self.state = self.DONE
self.change_state(self.DONE)
d['message_html'] = self.get_message_html()
d['allow_reset'] = self._allow_reset()
else:
self.state = self.REQUEST_HINT
self.change_state(self.REQUEST_HINT)
d['hint_html'] = self.get_hint_html()
d['state'] = self.state
@@ -360,19 +458,15 @@ class SelfAssessmentModule(XModule):
# the same number of hints and answers.
return self.out_of_sync_error(get)
self.hints.append(get['hint'].lower())
self.state = self.DONE
# increment attempts
self.attempts = self.attempts + 1
self.record_latest_hint(get['hint'])
self.change_state(self.DONE)
# To the tracking logs!
event_info = {
'selfassessment_id': self.location.url(),
'state': {
'student_answers': self.student_answers,
'score': self.scores,
'hints': self.hints,
'version': self.STATE_VERSION,
'history': self.history,
}
}
self.system.track_function('save_hint', event_info)
@@ -397,7 +491,7 @@ class SelfAssessmentModule(XModule):
'success': False,
'error': 'Too many attempts.'
}
self.state = self.INITIAL
self.change_state(self.INITIAL)
return {'success': True}
@@ -407,12 +501,11 @@ class SelfAssessmentModule(XModule):
"""
state = {
'student_answers': self.student_answers,
'hints': self.hints,
'version': self.STATE_VERSION,
'history': self.history,
'state': self.state,
'scores': self.scores,
'max_score': self._max_score,
'attempts': self.attempts
'attempts': self.attempts,
}
return json.dumps(state)

View File

@@ -10,7 +10,7 @@ from xmodule.progress import Progress
from xmodule.exceptions import NotFoundError
from pkg_resources import resource_string
log = logging.getLogger("mitx.common.lib.seq_module")
log = logging.getLogger(__name__)
# HACK: This shouldn't be hard-coded to two types
# OBSOLETE: This obsoletes 'type'
@@ -126,8 +126,8 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
children = []
for child in xml_object:
try:
children.append(system.process_xml(etree.tostring(child)).location.url())
except Exception, e:
children.append(system.process_xml(etree.tostring(child, encoding='unicode')).location.url())
except Exception as e:
log.exception("Unable to load child when parsing Sequence. Continuing...")
if system.error_tracker is not None:
system.error_tracker("ERROR: " + str(e))

View File

@@ -22,7 +22,7 @@ def stringify_children(node):
# next element.
parts = [node.text]
for c in node.getchildren():
parts.append(etree.tostring(c, with_tail=True))
parts.append(etree.tostring(c, with_tail=True, encoding='unicode'))
# filter removes possible Nones in texts and tails
return ''.join(filter(None, parts))
return u''.join(filter(None, parts))

View File

@@ -4,7 +4,7 @@ unittests for xmodule
Run like this:
rake test_common/lib/xmodule
"""
import unittest
@@ -19,11 +19,12 @@ import xmodule
from xmodule.x_module import ModuleSystem
from mock import Mock
i4xs = ModuleSystem(
test_system = ModuleSystem(
ajax_url='courses/course_id/modx/a_location',
track_function=Mock(),
get_module=Mock(),
render_template=Mock(),
# "render" to just the context...
render_template=lambda template, context: str(context),
replace_urls=Mock(),
user=Mock(),
filestore=Mock(),

View File

@@ -5,7 +5,7 @@ import unittest
from xmodule.progress import Progress
from xmodule import x_module
from . import i4xs
from . import test_system
class ProgressTest(unittest.TestCase):
''' Test that basic Progress objects work. A Progress represents a
@@ -133,6 +133,6 @@ class ModuleProgressTest(unittest.TestCase):
'''
def test_xmodule_default(self):
'''Make sure default get_progress exists, returns None'''
xm = x_module.XModule(i4xs, 'a://b/c/d/e', None, {})
xm = x_module.XModule(test_system, 'a://b/c/d/e', None, {})
p = xm.get_progress()
self.assertEqual(p, None)

View File

@@ -0,0 +1,54 @@
import json
from mock import Mock
import unittest
from xmodule.self_assessment_module import SelfAssessmentModule
from xmodule.modulestore import Location
from . import test_system
class SelfAssessmentTest(unittest.TestCase):
definition = {'rubric': 'A rubric',
'prompt': 'Who?',
'submitmessage': 'Shall we submit now?',
'hintprompt': 'Consider this...',
}
location = Location(["i4x", "edX", "sa_test", "selfassessment",
"SampleQuestion"])
metadata = {'attempts': '10'}
descriptor = Mock()
def test_import(self):
state = json.dumps({'student_answers': ["Answer 1", "answer 2", "answer 3"],
'scores': [0, 1],
'hints': ['o hai'],
'state': SelfAssessmentModule.ASSESSING,
'attempts': 2})
module = SelfAssessmentModule(test_system, self.location,
self.definition, self.descriptor,
state, {}, metadata=self.metadata)
self.assertEqual(module.get_score()['score'], 0)
self.assertTrue('answer 3' in module.get_html())
self.assertFalse('answer 2' in module.get_html())
module.save_assessment({'assessment': '0'})
self.assertEqual(module.state, module.REQUEST_HINT)
module.save_hint({'hint': 'hint for ans 3'})
self.assertEqual(module.state, module.DONE)
d = module.reset({})
self.assertTrue(d['success'])
self.assertEqual(module.state, module.INITIAL)
# if we now assess as right, skip the REQUEST_HINT state
module.save_answer({'student_answer': 'answer 4'})
module.save_assessment({'assessment': '1'})
self.assertEqual(module.state, module.DONE)

View File

@@ -10,6 +10,9 @@ from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
import datetime
import time
log = logging.getLogger(__name__)
@@ -37,6 +40,7 @@ class VideoModule(XModule):
self.show_captions = xmltree.get('show_captions', 'true')
self.source = self._get_source(xmltree)
self.track = self._get_track(xmltree)
self.start_time, self.end_time = self._get_timeframe(xmltree)
if instance_state is not None:
state = json.loads(instance_state)
@@ -46,11 +50,11 @@ class VideoModule(XModule):
def _get_source(self, xmltree):
# find the first valid source
return self._get_first_external(xmltree, 'source')
def _get_track(self, xmltree):
# find the first valid track
return self._get_first_external(xmltree, 'track')
def _get_first_external(self, xmltree, tag):
"""
Will return the first valid element
@@ -65,6 +69,23 @@ class VideoModule(XModule):
break
return result
def _get_timeframe(self, xmltree):
""" Converts 'from' and 'to' parameters in video tag to seconds.
If there are no parameters, returns empty string. """
def parse_time(s):
"""Converts s in '12:34:45' format to seconds. If s is
None, returns empty string"""
if s is None:
return ''
else:
x = time.strptime(s, '%H:%M:%S')
return datetime.timedelta(hours=x.tm_hour,
minutes=x.tm_min,
seconds=x.tm_sec).total_seconds()
return parse_time(xmltree.get('from')), parse_time(xmltree.get('to'))
def handle_ajax(self, dispatch, get):
'''
Handle ajax calls to this video.
@@ -109,12 +130,14 @@ class VideoModule(XModule):
'id': self.location.html_id(),
'position': self.position,
'source': self.source,
'track' : self.track,
'track': self.track,
'display_name': self.display_name,
# TODO (cpennington): This won't work when we move to data that isn't on the filesystem
'data_dir': self.metadata['data_dir'],
'caption_asset_path': caption_asset_path,
'show_captions': self.show_captions
'show_captions': self.show_captions,
'start': self.start_time,
'end': self.end_time
})

View File

@@ -248,17 +248,17 @@ class XModule(HTMLSnippet):
self._loaded_children = [c for c in children if c is not None]
return self._loaded_children
def get_children_locations(self):
'''
Returns the locations of each of child modules.
Overriding this changes the behavior of get_children and
anything that uses get_children, such as get_display_items.
This method will not instantiate the modules of the children
unless absolutely necessary, so it is cheaper to call than get_children
These children will be the same children returned by the
descriptor unless descriptor.has_dynamic_children() is true.
'''
@@ -303,8 +303,20 @@ class XModule(HTMLSnippet):
return '{}'
def get_score(self):
''' Score the student received on the problem.
'''
"""
Score the student received on the problem, or None if there is no
score.
Returns:
dictionary
{'score': integer, from 0 to get_max_score(),
'total': get_max_score()}
NOTE (vshnayder): not sure if this was the intended return value, but
that's what it's doing now. I suspect that we really want it to just
return a number. Would need to change (at least) capa and
modx_dispatch to match if we did that.
"""
return None
def max_score(self):
@@ -334,6 +346,19 @@ class XModule(HTMLSnippet):
get is a dictionary-like object '''
return ""
# cdodge: added to support dynamic substitutions of
# links for courseware assets (e.g. images). <link> is passed through from lxml.html parser
def rewrite_content_links(self, link):
# see if we start with our format, e.g. 'xasset:<filename>'
if link.startswith(XASSET_SRCREF_PREFIX):
# yes, then parse out the name
name = link[len(XASSET_SRCREF_PREFIX):]
loc = Location(self.location)
# resolve the reference to our internal 'filepath' which
link = StaticContent.compute_location_filename(loc.org, loc.course, name)
return link
def policy_key(location):
"""
@@ -412,7 +437,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
# cdodge: this is a list of metadata names which are 'system' metadata
# and should not be edited by an end-user
system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft']
# A list of descriptor attributes that must be equal for the descriptors to
# be equal
equality_attributes = ('definition', 'metadata', 'location',
@@ -575,18 +600,18 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
self,
metadata=self.metadata
)
def has_dynamic_children(self):
"""
Returns True if this descriptor has dynamic children for a given
student when the module is created.
Returns False if the children of this descriptor are the same
children that the module will return for any student.
children that the module will return for any student.
"""
return False
# ================================= JSON PARSING ===========================
@staticmethod
@@ -810,7 +835,8 @@ class ModuleSystem(object):
debug=False,
xqueue=None,
node_path="",
anonymous_student_id=''):
anonymous_student_id='',
course_id=None):
'''
Create a closure around the system environment.
@@ -845,6 +871,8 @@ class ModuleSystem(object):
ajax results.
anonymous_student_id - Used for tracking modules with student id
course_id - the course_id containing this module
'''
self.ajax_url = ajax_url
self.xqueue = xqueue
@@ -857,6 +885,7 @@ class ModuleSystem(object):
self.replace_urls = replace_urls
self.node_path = node_path
self.anonymous_student_id = anonymous_student_id
self.course_id = course_id
self.user_is_staff = user is not None and user.is_staff
def get(self, attr):

View File

@@ -1,19 +1,20 @@
from xmodule.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location
from lxml import etree
import json
import copy
import logging
import traceback
from collections import namedtuple
from fs.errors import ResourceNotFoundError
import os
import sys
from collections import namedtuple
from lxml import etree
from xmodule.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location
log = logging.getLogger(__name__)
# assume all XML files are persisted as utf-8.
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
remove_comments=True, remove_blank_text=True)
remove_comments=True, remove_blank_text=True,
encoding='utf-8')
def name_to_pathname(name):
"""
@@ -380,7 +381,7 @@ class XmlDescriptor(XModuleDescriptor):
filepath = self.__class__._format_filepath(self.category, url_path)
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
with resource_fs.open(filepath, 'w') as file:
file.write(etree.tostring(xml_object, pretty_print=True))
file.write(etree.tostring(xml_object, pretty_print=True, encoding='utf-8'))
# And return just a pointer with the category and filename.
record_object = etree.Element(self.category)
@@ -395,7 +396,7 @@ class XmlDescriptor(XModuleDescriptor):
record_object.set('org', self.location.org)
record_object.set('course', self.location.course)
return etree.tostring(record_object, pretty_print=True)
return etree.tostring(record_object, pretty_print=True, encoding='utf-8')
def definition_to_xml(self, resource_fs):
"""