Merge branch 'master' into feature/diana/rubric-input

Fix some merge issues involving rubric rendering

Conflicts:
	common/lib/xmodule/xmodule/combined_open_ended_rubric.py
	common/lib/xmodule/xmodule/open_ended_module.py
	common/lib/xmodule/xmodule/self_assessment_module.py
This commit is contained in:
Diana Huang
2013-01-18 16:36:22 -05:00
21 changed files with 253 additions and 160 deletions

View File

@@ -24,8 +24,6 @@ import open_ended_module
from combined_open_ended_rubric import CombinedOpenEndedRubric
from .stringify import stringify_children
from mitxmako.shortcuts import render_to_string
log = logging.getLogger("mitx.courseware")
# Set the default number of max attempts. Should be 1 for production
@@ -142,7 +140,7 @@ class CombinedOpenEndedModule(XModule):
# completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = int(self.metadata.get('max_score', MAX_SCORE))
rubric_renderer = CombinedOpenEndedRubric(True)
rubric_renderer = CombinedOpenEndedRubric(system, True)
success, rubric_feedback = rubric_renderer.render_rubric(stringify_children(definition['rubric']))
if not success:
error_message="Could not parse rubric : {0}".format(definition['rubric'])
@@ -327,7 +325,7 @@ class CombinedOpenEndedModule(XModule):
Output: HTML rendered directly via Mako
"""
context = self.get_context()
html = render_to_string('combined_open_ended.html', context)
html = self.system.render_template('combined_open_ended.html', context)
return html
def get_html_base(self):
@@ -377,17 +375,17 @@ class CombinedOpenEndedModule(XModule):
self.static_data, instance_state=task_state)
last_response = task.latest_answer()
last_score = task.latest_score()
last_post_assessment = task.latest_post_assessment()
last_post_assessment = task.latest_post_assessment(self.system)
last_post_feedback = ""
if task_type == "openended":
last_post_assessment = task.latest_post_assessment(short_feedback=False, join_feedback=False)
last_post_assessment = task.latest_post_assessment(self.system, short_feedback=False, join_feedback=False)
if isinstance(last_post_assessment, list):
eval_list = []
for i in xrange(0, len(last_post_assessment)):
eval_list.append(task.format_feedback_with_evaluation(last_post_assessment[i]))
eval_list.append(task.format_feedback_with_evaluation(self.system, last_post_assessment[i]))
last_post_evaluation = "".join(eval_list)
else:
last_post_evaluation = task.format_feedback_with_evaluation(last_post_assessment)
last_post_evaluation = task.format_feedback_with_evaluation(self.system, last_post_assessment)
last_post_assessment = last_post_evaluation
last_correctness = task.is_last_response_correct()
max_score = task.max_score()
@@ -450,7 +448,7 @@ class CombinedOpenEndedModule(XModule):
self.update_task_states()
response_dict = self.get_last_response(task_number)
context = {'results': response_dict['post_assessment'], 'task_number': task_number + 1}
html = render_to_string('combined_open_ended_results.html', context)
html = self.system.render_template('combined_open_ended_results.html', context)
return {'html': html, 'success': True}
def handle_ajax(self, dispatch, get):
@@ -603,4 +601,4 @@ class CombinedOpenEndedDescriptor(XmlDescriptor, EditingDescriptor):
for child in ['task']:
add_child(child)
return elt
return elt

View File

@@ -1,14 +1,14 @@
from mitxmako.shortcuts import render_to_string
import logging
from lxml import etree
log=logging.getLogger(__name__)
class CombinedOpenEndedRubric:
class CombinedOpenEndedRubric(object):
def __init__ (self, view_only = False):
def __init__ (self, system, view_only = False):
self.has_score = False
self.view_only = view_only
self.system
'''
render_rubric: takes in an xml string and outputs the corresponding
@@ -23,7 +23,7 @@ class CombinedOpenEndedRubric:
success = False
try:
rubric_categories = self.extract_categories(rubric_xml)
html = render_to_string('open_ended_rubric.html',
html = self.system.render_template('open_ended_rubric.html',
{'categories' : rubric_categories,
'has_score': self.has_score,
'view_only': self.view_only})

View File

@@ -1,4 +1,5 @@
import logging
from math import exp, erf
from lxml import etree
from path import path # NOTE (THK): Only used for detecting presence of syllabus
import requests
@@ -183,35 +184,66 @@ class CourseDescriptor(SequenceDescriptor):
@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
"""
Returns if the course has been flagged as new in the metadata. If
there is no flag, return a heuristic value considering the
announcement and the start dates.
"""
flag = self.metadata.get('is_new', None)
if flag is None:
return self.days_until_start > 1
# Use a heuristic if the course has not been flagged
announcement, start, now = self._sorting_dates()
if announcement and (now - announcement).days < 30:
# The course has been announced for less that month
return True
elif (now - start).days < 1:
# The course has not started yet
return True
else:
return False
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):
def sorting_score(self):
"""
Returns a number that can be used to sort the courses according
the how "new"" they are. The "newness"" score is computed using a
heuristic that takes into account the announcement and
(advertized) start dates of the course if available.
The lower the number the "newer" the course.
"""
# Make courses that have an announcement date shave a lower
# score than courses than don't, older courses should have a
# higher score.
announcement, start, now = self._sorting_dates()
scale = 300.0 # about a year
if announcement:
days = (now - announcement).days
score = -exp(-days/scale)
else:
days = (now - start).days
score = exp(days/scale)
return score
def _sorting_dates(self):
# utility function to get datetime objects for dates used to
# compute the is_new flag and the sorting_score
def to_datetime(timestamp):
return datetime.fromtimestamp(time.mktime(timestamp))
start_date = convert_to_datetime(self.start)
def get_date(field):
timetuple = self._try_parse_time(field)
return to_datetime(timetuple) if timetuple else None
# 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''
announcement = get_date('announcement')
start = get_date('advertised_start') or to_datetime(self.start)
now = to_datetime(time.gmtime())
now = convert_to_datetime(time.gmtime())
days_until_start = (start_date - now).days
return days_until_start
return announcement, start, now
@lazyproperty
def grading_context(self):
@@ -387,9 +419,9 @@ class CourseDescriptor(SequenceDescriptor):
self.first_eligible_appointment_date = self._try_parse_time('First_Eligible_Appointment_Date')
if self.first_eligible_appointment_date is None:
raise ValueError("First appointment date must be specified")
# TODO: If defaulting the last appointment date, it should be the
# TODO: If defaulting the last appointment date, it should be the
# *end* of the same day, not the same time. It's going to be used as the
# end of the exam overall, so we don't want the exam to disappear too soon.
# end of the exam overall, so we don't want the exam to disappear too soon.
# It's also used optionally as the registration end date, so time matters there too.
self.last_eligible_appointment_date = self._try_parse_time('Last_Eligible_Appointment_Date') # or self.first_eligible_appointment_date
if self.last_eligible_appointment_date is None:
@@ -403,7 +435,7 @@ class CourseDescriptor(SequenceDescriptor):
raise ValueError("First appointment date must be before last appointment date")
if self.registration_end_date > self.last_eligible_appointment_date:
raise ValueError("Registration end date must be before last appointment date")
def _try_parse_time(self, key):
"""
@@ -434,7 +466,7 @@ class CourseDescriptor(SequenceDescriptor):
def is_registering(self):
now = time.gmtime()
return now >= self.registration_start_date and now <= self.registration_end_date
@property
def first_eligible_appointment_date_text(self):
return time.strftime("%b %d, %Y", self.first_eligible_appointment_date)
@@ -451,7 +483,7 @@ class CourseDescriptor(SequenceDescriptor):
def current_test_center_exam(self):
exams = [exam for exam in self.test_center_exams if exam.has_started_registration() and not exam.has_ended()]
if len(exams) > 1:
# TODO: output some kind of warning. This should already be
# TODO: output some kind of warning. This should already be
# caught if we decide to do validation at load time.
return exams[0]
elif len(exams) == 1:

View File

@@ -30,7 +30,6 @@ from xmodule.modulestore import Location
from capa.util import *
import openendedchild
from mitxmako.shortcuts import render_to_string
from numpy import median
from datetime import datetime
@@ -256,7 +255,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
@param system: Modulesystem
@return: Boolean True (not useful currently)
"""
new_score_msg = self._parse_score_msg(score_msg)
new_score_msg = self._parse_score_msg(score_msg, system)
if not new_score_msg['valid']:
score_msg['feedback'] = 'Invalid grader reply. Please contact the course staff.'
@@ -370,7 +369,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
return u"\n".join([feedback_list_part1, feedback_list_part2])
def _format_feedback(self, response_items):
def _format_feedback(self, response_items, system):
"""
Input:
Dictionary called feedback. Must contain keys seen below.
@@ -382,14 +381,18 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
rubric_feedback=""
feedback = self._convert_longform_feedback_to_html(response_items)
if response_items['rubric_scores_complete']==True:
<<<<<<< HEAD
rubric_renderer = CombinedOpenEndedRubric(True)
success, rubric_feedback = rubric_renderer.render_rubric(response_items['rubric_xml'])
=======
rubric_feedback = CombinedOpenEndedRubric.render_rubric(response_items['rubric_xml'], system)
>>>>>>> master
if not response_items['success']:
return system.render_template("open_ended_error.html",
{'errors': feedback})
feedback_template = render_to_string("open_ended_feedback.html", {
feedback_template = system.render_template("open_ended_feedback.html", {
'grader_type': response_items['grader_type'],
'score': "{0} / {1}".format(response_items['score'], self.max_score()),
'feedback': feedback,
@@ -399,7 +402,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
return feedback_template
def _parse_score_msg(self, score_msg, join_feedback=True):
def _parse_score_msg(self, score_msg, system, join_feedback=True):
"""
Grader reply is a JSON-dump of the following dict
{ 'correct': True/False,
@@ -451,7 +454,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'rubric_scores_complete' : score_result['rubric_scores_complete'][i],
'rubric_xml' : score_result['rubric_xml'][i],
}
feedback_items.append(self._format_feedback(new_score_result))
feedback_items.append(self._format_feedback(new_score_result, system))
if join_feedback:
feedback = "".join(feedback_items)
else:
@@ -459,7 +462,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
score = int(median(score_result['score']))
else:
#This is for instructor and ML grading
feedback = self._format_feedback(score_result)
feedback = self._format_feedback(score_result, system)
score = score_result['score']
self.submission_id = score_result['submission_id']
@@ -467,7 +470,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
return {'valid': True, 'score': score, 'feedback': feedback}
def latest_post_assessment(self, short_feedback=False, join_feedback=True):
def latest_post_assessment(self, system, short_feedback=False, join_feedback=True):
"""
Gets the latest feedback, parses, and returns
@param short_feedback: If the long feedback is wanted or not
@@ -476,7 +479,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
if not self.history:
return ""
feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', ""), join_feedback=join_feedback)
feedback_dict = self._parse_score_msg(self.history[-1].get('post_assessment', ""), system, join_feedback=join_feedback)
if not short_feedback:
return feedback_dict['feedback'] if feedback_dict['valid'] else ''
if feedback_dict['valid']:
@@ -484,14 +487,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
json.loads(self.history[-1].get('post_assessment', "")))
return short_feedback if feedback_dict['valid'] else ''
def format_feedback_with_evaluation(self, feedback):
def format_feedback_with_evaluation(self, system, feedback):
"""
Renders a given html feedback into an evaluation template
@param feedback: HTML feedback
@return: Rendered html
"""
context = {'msg': feedback, 'id': "1", 'rows': 50, 'cols': 50}
html = render_to_string('open_ended_evaluation.html', context)
html = system.render_template('open_ended_evaluation.html', context)
return html
def handle_ajax(self, dispatch, get, system):
@@ -583,7 +586,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
if self.state != self.INITIAL:
latest = self.latest_answer()
previous_answer = latest if latest is not None else self.initial_display
post_assessment = self.latest_post_assessment()
post_assessment = self.latest_post_assessment(system)
score = self.latest_score()
correct = 'correct' if self.is_submission_correct(score) else 'incorrect'
else:

View File

@@ -35,7 +35,7 @@ MAX_ATTEMPTS = 1
# Overriden by max_score specified in xml.
MAX_SCORE = 1
class OpenEndedChild():
class OpenEndedChild(object):
"""
States:
@@ -123,7 +123,7 @@ class OpenEndedChild():
return None
return self.history[-1].get('score')
def latest_post_assessment(self):
def latest_post_assessment(self, system):
"""None if not available"""
if not self.history:
return ""

View File

@@ -122,8 +122,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
if self.state == self.INITIAL:
return ''
rubric_renderer = CombinedOpenEndedRubric(True)
rubric_renderer = CombinedOpenEndedRubric(system, True)
success, rubric_html = rubric_renderer.render_rubric(self.rubric)
# we'll render it
@@ -149,7 +148,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
if self.state == self.DONE:
# display the previous hint
latest = self.latest_post_assessment()
latest = self.latest_post_assessment(system)
hint = latest if latest is not None else ''
else:
hint = ''

View File

@@ -1,5 +1,5 @@
import unittest
from time import strptime, gmtime
from time import strptime
from fs.memoryfs import MemoryFS
from mock import Mock, patch
@@ -39,52 +39,81 @@ class DummySystem(ImportSystem):
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):
def get_dummy_course(start, announcement=None, is_new=None):
"""Get a dummy course"""
system = DummySystem(load_error_modules)
is_new = '' if is_new is None else 'is_new="{0}"'.format(is_new).lower()
system = DummySystem(load_error_modules=True)
def to_attrb(n, v):
return '' if v is None else '{0}="{1}"'.format(n, v).lower()
is_new = to_attrb('is_new', is_new)
announcement = to_attrb('announcement', announcement)
start_xml = '''
<course org="{org}" course="{course}"
graceperiod="1 day" url_name="test"
start="{start}"
{announcement}
{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)
'''.format(org=ORG, course=COURSE, start=start, is_new=is_new,
announcement=announcement)
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')
def test_sorting_score(self, gmtime_mock):
gmtime_mock.return_value = NOW
assert(descriptor.is_new == True)
assert(descriptor.days_until_start == 4)
dates = [('2012-10-01T12:00', '2012-09-01T12:00'), # 0
('2012-12-01T12:00', '2012-11-01T12:00'), # 1
('2013-02-01T12:00', '2012-12-01T12:00'), # 2
('2013-02-01T12:00', '2012-11-10T12:00'), # 3
('2013-02-01T12:00', None), # 4
('2013-03-01T12:00', None), # 5
('2013-04-01T12:00', None), # 6
('2012-11-01T12:00', None), # 7
('2012-09-01T12:00', None), # 8
('1990-01-01T12:00', None), # 9
('2013-01-02T12:00', None), # 10
('2013-01-10T12:00', '2012-12-31T12:00'), # 11
('2013-01-10T12:00', '2013-01-01T12:00'), # 12
]
data = []
for i, d in enumerate(dates):
descriptor = self.get_dummy_course(start=d[0], announcement=d[1])
score = descriptor.sorting_score
data.append((score, i))
result = [d[1] for d in sorted(data)]
assert(result == [12, 11, 2, 3, 1, 0, 6, 5, 4, 10, 7, 8, 9])
@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):
def test_is_new(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)
assert(descriptor.is_new is True)
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=False)
assert(descriptor.is_new == False)
assert(descriptor.days_until_start > 0)
assert(descriptor.is_new is False)
descriptor = self.get_dummy_course(start='2013-02-02T12:00', is_new=True)
assert(descriptor.is_new == True)
assert(descriptor.days_until_start > 0)
assert(descriptor.is_new is True)
descriptor = self.get_dummy_course(start='2013-01-15T12:00')
assert(descriptor.is_new is True)
descriptor = self.get_dummy_course(start='2013-03-00T12:00')
assert(descriptor.is_new is True)
descriptor = self.get_dummy_course(start='2012-10-15T12:00')
assert(descriptor.is_new is False)
descriptor = self.get_dummy_course(start='2012-12-31T12:00')
assert(descriptor.is_new is True)

View File

@@ -339,19 +339,6 @@ class ImportTestCase(unittest.TestCase):
self.assertRaises(etree.XMLSyntaxError, system.process_xml, bad_xml)
def test_selfassessment_import(self):
'''
Check to see if definition_from_xml in self_assessment_module.py
works properly. Pulls data from the self_assessment directory in the test data directory.
'''
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['self_assessment'])
sa_id = "edX/sa_test/2012_Fall"
location = Location(["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"])
sa_sample = modulestore.get_instance(sa_id, location)
#10 attempts is hard coded into SampleQuestion, which is the url_name of a selfassessment xml tag
self.assertEqual(sa_sample.metadata['attempts'], '10')
def test_graphicslidertool_import(self):
'''

View File

@@ -4,6 +4,7 @@ import unittest
from xmodule.self_assessment_module import SelfAssessmentModule
from xmodule.modulestore import Location
from lxml import etree
from . import test_system
@@ -26,22 +27,37 @@ class SelfAssessmentTest(unittest.TestCase):
state = json.dumps({'student_answers': ["Answer 1", "answer 2", "answer 3"],
'scores': [0, 1],
'hints': ['o hai'],
'state': SelfAssessmentModule.ASSESSING,
'state': SelfAssessmentModule.INITIAL,
'attempts': 2})
rubric = '''<rubric><rubric>
<category>
<description>Response Quality</description>
<option>The response is not a satisfactory answer to the question. It either fails to address the question or does so in a limited way, with no evidence of higher-order thinking.</option>
</category>
</rubric></rubric>'''
prompt = etree.XML("<prompt>Text</prompt>")
static_data = {
'max_attempts': 10,
'rubric': etree.XML(rubric),
'prompt': prompt,
'max_score': 1
}
module = SelfAssessmentModule(test_system, self.location,
self.definition, self.descriptor,
state, {}, metadata=self.metadata)
static_data, 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_answer({'student_answer': "I am an answer"}, test_system)
self.assertEqual(module.state, module.ASSESSING)
module.save_hint({'hint': 'hint for ans 3'})
module.save_assessment({'assessment': '0'}, test_system)
self.assertEqual(module.state, module.POST_ASSESSMENT)
module.save_hint({'hint': 'this is a hint'}, test_system)
self.assertEqual(module.state, module.DONE)
d = module.reset({})
@@ -49,6 +65,6 @@ class SelfAssessmentTest(unittest.TestCase):
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'})
module.save_answer({'student_answer': 'answer 4'}, test_system)
module.save_assessment({'assessment': '1'}, test_system)
self.assertEqual(module.state, module.DONE)