Use the new CombinedOpenEndedRubric class for everything
related to rubric rendering. Remove the old input type.
This commit is contained in:
@@ -735,142 +735,3 @@ class ChemicalEquationInput(InputTypeBase):
|
||||
registry.register(ChemicalEquationInput)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class RubricInput(InputTypeBase):
|
||||
"""
|
||||
This is the logic for parsing and displaying a rubric of type
|
||||
"""
|
||||
|
||||
template = "rubricinput.html"
|
||||
tags = ['rubric']
|
||||
|
||||
# pulled out for testing
|
||||
submitted_msg = ("Feedback not yet available. Reload to check again. "
|
||||
"Once the problem is graded, this message will be "
|
||||
"replaced with the grader's feedback.")
|
||||
has_score = False
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
"""
|
||||
Convert options to a convenient format.
|
||||
"""
|
||||
return [
|
||||
Attribute('height', None),
|
||||
Attribute('width', None)]
|
||||
|
||||
def _extra_context(self):
|
||||
"""
|
||||
Add in the various bits and pieces that we need
|
||||
"""
|
||||
return {'categories': self.categories,
|
||||
'view_only': False,
|
||||
'has_score': self.has_score}
|
||||
|
||||
def setup(self):
|
||||
# set the categories
|
||||
self.categories = self.extract_categories(self.xml)
|
||||
|
||||
def extract_categories(self, element):
|
||||
'''
|
||||
Contstruct a list of categories such that the structure looks like:
|
||||
[ { category: "Category 1 Name",
|
||||
options: [{text: "Option 1 Name", points: 0}, {text:"Option 2 Name", points: 5}]
|
||||
},
|
||||
{ category: "Category 2 Name",
|
||||
options: [{text: "Option 1 Name", points: 0},
|
||||
{text: "Option 2 Name", points: 1},
|
||||
{text: "Option 3 Name", points: 2]}]
|
||||
|
||||
'''
|
||||
categories = []
|
||||
for category in element:
|
||||
if category.tag != 'category':
|
||||
raise Exception("[capa.inputtypes.extract_categories] Expected a <category> tag: got {0} instead".format(category.tag))
|
||||
else:
|
||||
categories.append(self.extract_category(category))
|
||||
return categories
|
||||
|
||||
|
||||
def extract_category(self, category):
|
||||
'''
|
||||
construct an individual category
|
||||
{category: "Category 1 Name",
|
||||
options: [{text: "Option 1 text", points: 1},
|
||||
{text: "Option 2 text", points: 2}]}
|
||||
|
||||
all sorting and auto-point generation occurs in this function
|
||||
'''
|
||||
descriptionxml = category[0]
|
||||
optionsxml = category[1:]
|
||||
scorexml = category[1]
|
||||
score = None
|
||||
if scorexml.tag == 'score':
|
||||
score_text = scorexml.text
|
||||
optionsxml = category[2:]
|
||||
score = int(score_text)
|
||||
self.has_score = True
|
||||
# if we are missing the score tag and we are expecting one
|
||||
elif self.has_score:
|
||||
raise Exception("[inputtypes.extract_category] Category {0} is missing a score".format(descriptionxml.text))
|
||||
|
||||
|
||||
# parse description
|
||||
if descriptionxml.tag != 'description':
|
||||
raise Exception("[extract_category]: expected description tag, got {0} instead".format(descriptionxml.tag))
|
||||
|
||||
description = descriptionxml.text
|
||||
|
||||
cur_points = 0
|
||||
options = []
|
||||
autonumbering = True
|
||||
# parse options
|
||||
for option in optionsxml:
|
||||
if option.tag != 'option':
|
||||
raise Exception("[extract_category]: expected option tag, got {0} instead".format(option.tag))
|
||||
else:
|
||||
pointstr = option.get("points")
|
||||
if pointstr:
|
||||
autonumbering = False
|
||||
# try to parse this into an int
|
||||
try:
|
||||
points = int(pointstr)
|
||||
except ValueError:
|
||||
raise Exception("[extract_category]: expected points to have int, got {0} instead".format(pointstr))
|
||||
elif autonumbering:
|
||||
# use the generated one if we're in the right mode
|
||||
points = cur_points
|
||||
cur_points = cur_points + 1
|
||||
else:
|
||||
raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly dfined.")
|
||||
|
||||
selected = score == points
|
||||
optiontext = option.text
|
||||
options.append({'text': option.text, 'points': points, 'selected': selected})
|
||||
|
||||
# sort and check for duplicates
|
||||
options = sorted(options, key=lambda option: option['points'])
|
||||
RubricInput.validate_options(options)
|
||||
|
||||
return {'description': description, 'options': options}
|
||||
|
||||
@staticmethod
|
||||
def validate_options(options):
|
||||
'''
|
||||
Validates a set of options. This can and should be extended to filter out other bad edge cases
|
||||
'''
|
||||
if len(options) == 0:
|
||||
raise Exception("[extract_category]: no options associated with this category")
|
||||
if len(options) == 1:
|
||||
return
|
||||
prev = options[0]['points']
|
||||
for option in options[1:]:
|
||||
if prev == option['points']:
|
||||
raise Exception("[extract_category]: found duplicate point values between two different options")
|
||||
else:
|
||||
prev = option['points']
|
||||
|
||||
|
||||
registry.register(RubricInput)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<form class="rubric-template" id="inputtype_${id}">
|
||||
<h3>Rubric</h3>
|
||||
% if view_only and has_score:
|
||||
<p>This is the rubric that was used to grade your submission.The highlighted selection matches how the grader feels you performed in each category.</p>
|
||||
% elif view_only:
|
||||
<p>Use the below rubric to rate this submission.</p>
|
||||
% else:
|
||||
<p>Select the criteria you feel best represents this submission in each category.</p>
|
||||
% endif
|
||||
<table class="rubric">
|
||||
% for i in range(len(categories)):
|
||||
<% category = categories[i] %>
|
||||
<tr>
|
||||
<th>${category['description']}</th>
|
||||
% for j in range(len(category['options'])):
|
||||
<% option = category['options'][j] %>
|
||||
<td>
|
||||
% if view_only:
|
||||
## if this is the selected rubric block, show it
|
||||
% if option['selected']:
|
||||
<div class="view-only selected-grade">
|
||||
% else:
|
||||
<div class="view-only">
|
||||
% endif
|
||||
${option['text']}
|
||||
<div class="grade">[${option['points']} points]</div>
|
||||
</div>
|
||||
% else:
|
||||
<input type="radio" class="score-selection" name="score-selection-${i}" id="score-${i}-${j}" value="${option['points']}"/>
|
||||
<label for="score-${i}-${j}">${option['text']}</label>
|
||||
% endif
|
||||
</td>
|
||||
% endfor
|
||||
</tr>
|
||||
% endfor
|
||||
</table>
|
||||
</form>
|
||||
@@ -6,41 +6,48 @@ log=logging.getLogger(__name__)
|
||||
|
||||
class CombinedOpenEndedRubric:
|
||||
|
||||
@staticmethod
|
||||
def render_rubric(rubric_xml):
|
||||
def __init__ (self, view_only = False):
|
||||
self.has_score = False
|
||||
self.view_only = view_only
|
||||
|
||||
def render_rubric(self, rubric_xml):
|
||||
try:
|
||||
rubric_categories = CombinedOpenEndedRubric.extract_rubric_categories(rubric_xml)
|
||||
html = render_to_string('open_ended_rubric.html', {'rubric_categories' : rubric_categories})
|
||||
rubric_categories = self.extract_categories(rubric_xml)
|
||||
html = render_to_string('open_ended_rubric.html',
|
||||
{'categories' : rubric_categories,
|
||||
'has_score': self.has_score,
|
||||
'view_only': self.view_only})
|
||||
except:
|
||||
log.exception("Could not parse the rubric.")
|
||||
html = rubric_xml
|
||||
html = etree.tostring(rubric_xml, pretty_print=True)
|
||||
return html
|
||||
|
||||
@staticmethod
|
||||
def extract_rubric_categories(element):
|
||||
def extract_categories(self, element):
|
||||
'''
|
||||
Contstruct a list of categories such that the structure looks like:
|
||||
[ { category: "Category 1 Name",
|
||||
options: [{text: "Option 1 Name", points: 0}, {text:"Option 2 Name", points: 5}]
|
||||
},
|
||||
{ category: "Category 2 Name",
|
||||
options: [{text: "Option 1 Name", points: 0},
|
||||
{text: "Option 2 Name", points: 1},
|
||||
options: [{text: "Option 1 Name", points: 0},
|
||||
{text: "Option 2 Name", points: 1},
|
||||
{text: "Option 3 Name", points: 2]}]
|
||||
|
||||
'''
|
||||
element = etree.fromstring(element)
|
||||
if element.tag != 'rubric':
|
||||
raise Exception("[extract_categories] Expected a <rubric> tag: got {0} instead".format(element.tag))
|
||||
categorylist = list(element)
|
||||
categories = []
|
||||
for category in element:
|
||||
for category in categorylist:
|
||||
if category.tag != 'category':
|
||||
raise Exception("[capa.inputtypes.extract_categories] Expected a <category> tag: got {0} instead".format(category.tag))
|
||||
raise Exception("[extract_categories] Expected a <category> tag: got {0} instead".format(category.tag))
|
||||
else:
|
||||
categories.append(CombinedOpenEndedRubric.extract_category(category))
|
||||
categories.append(self.extract_category(category))
|
||||
return categories
|
||||
|
||||
@staticmethod
|
||||
def extract_category(category):
|
||||
'''
|
||||
|
||||
def extract_category(self, category):
|
||||
'''
|
||||
construct an individual category
|
||||
{category: "Category 1 Name",
|
||||
options: [{text: "Option 1 text", points: 1},
|
||||
@@ -48,41 +55,32 @@ class CombinedOpenEndedRubric:
|
||||
|
||||
all sorting and auto-point generation occurs in this function
|
||||
'''
|
||||
|
||||
has_score=False
|
||||
descriptionxml = category[0]
|
||||
optionsxml = category[1:]
|
||||
scorexml = category[1]
|
||||
if scorexml.tag == "option":
|
||||
optionsxml = category[1:]
|
||||
else:
|
||||
score = None
|
||||
if scorexml.tag == 'score':
|
||||
score_text = scorexml.text
|
||||
optionsxml = category[2:]
|
||||
has_score=True
|
||||
score = int(score_text)
|
||||
self.has_score = True
|
||||
# if we are missing the score tag and we are expecting one
|
||||
elif self.has_score:
|
||||
raise Exception("[extract_category] Category {0} is missing a score".format(descriptionxml.text))
|
||||
|
||||
|
||||
# parse description
|
||||
if descriptionxml.tag != 'description':
|
||||
raise Exception("[extract_category]: expected description tag, got {0} instead".format(descriptionxml.tag))
|
||||
|
||||
if has_score:
|
||||
if scorexml.tag != 'score':
|
||||
raise Exception("[extract_category]: expected score tag, got {0} instead".format(scorexml.tag))
|
||||
|
||||
for option in optionsxml:
|
||||
if option.tag != "option":
|
||||
raise Exception("[extract_category]: expected option tag, got {0} instead".format(option.tag))
|
||||
|
||||
description = descriptionxml.text
|
||||
|
||||
if has_score:
|
||||
score = int(scorexml.text)
|
||||
else:
|
||||
score = 0
|
||||
|
||||
cur_points = 0
|
||||
options = []
|
||||
autonumbering = True
|
||||
# parse options
|
||||
for option in optionsxml:
|
||||
if option.tag != 'option':
|
||||
if option.tag != 'option':
|
||||
raise Exception("[extract_category]: expected option tag, got {0} instead".format(option.tag))
|
||||
else:
|
||||
pointstr = option.get("points")
|
||||
@@ -99,18 +97,17 @@ class CombinedOpenEndedRubric:
|
||||
cur_points = cur_points + 1
|
||||
else:
|
||||
raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly dfined.")
|
||||
|
||||
selected = score == points
|
||||
optiontext = option.text
|
||||
selected = False
|
||||
if has_score:
|
||||
if points == score:
|
||||
selected = True
|
||||
options.append({'text': option.text, 'points': points, 'selected' : selected})
|
||||
options.append({'text': option.text, 'points': points, 'selected': selected})
|
||||
|
||||
# sort and check for duplicates
|
||||
options = sorted(options, key=lambda option: option['points'])
|
||||
CombinedOpenEndedRubric.validate_options(options)
|
||||
|
||||
return {'description': description, 'options': options, 'score' : score, 'has_score' : has_score}
|
||||
return {'description': description, 'options': options}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def validate_options(options):
|
||||
@@ -126,4 +123,4 @@ class CombinedOpenEndedRubric:
|
||||
if prev == option['points']:
|
||||
raise Exception("[extract_category]: found duplicate point values between two different options")
|
||||
else:
|
||||
prev = option['points']
|
||||
prev = option['points']
|
||||
|
||||
@@ -19,7 +19,7 @@ from xmodule.course_module import CourseDescriptor
|
||||
from student.models import unique_id_for_user
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from capa import inputtypes
|
||||
from xmodule.combined_open_ended_rubric import CombinedOpenEndedRubric
|
||||
from lxml import etree
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -263,8 +263,9 @@ def _get_next(course_id, grader_id, location):
|
||||
response = staff_grading_service().get_next(course_id, location, grader_id)
|
||||
response_json = json.loads(response)
|
||||
rubric = response_json['rubric']
|
||||
rubric_input = inputtypes.RubricInput(module_system, etree.XML(rubric), {'id': location})
|
||||
rubric_html = etree.tostring(rubric_input.get_html())
|
||||
rubric_renderer = CombinedOpenEndedRubric(False)
|
||||
rubric_xml = etree.XML(rubric)
|
||||
rubric_html = rubric_renderer.render_rubric(rubric_xml)
|
||||
response_json['rubric'] = rubric_html
|
||||
return json.dumps(response_json)
|
||||
except GradingServiceError:
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
<table class="rubric">
|
||||
% for i in range(len(rubric_categories)):
|
||||
<% category = rubric_categories[i] %>
|
||||
<tr>
|
||||
<th>
|
||||
${category['description']}
|
||||
% if category['has_score'] == True:
|
||||
(Your score: ${category['score']})
|
||||
% endif
|
||||
</th>
|
||||
% for j in range(len(category['options'])):
|
||||
<% option = category['options'][j] %>
|
||||
<td>
|
||||
<div class="view-only">
|
||||
${option['text']}
|
||||
% if option.has_key('selected'):
|
||||
% if option['selected'] == True:
|
||||
<div class="selected-grade">[${option['points']} points]</div>
|
||||
%else:
|
||||
<div class="grade">[${option['points']} points]</div>
|
||||
<form class="rubric-template" id="inputtype_${id}">
|
||||
<h3>Rubric</h3>
|
||||
% if view_only and has_score:
|
||||
<p>This is the rubric that was used to grade your submission.The highlighted selection matches how the grader feels you performed in each category.</p>
|
||||
% elif view_only:
|
||||
<p>Use the below rubric to rate this submission.</p>
|
||||
% else:
|
||||
<p>Select the criteria you feel best represents this submission in each category.</p>
|
||||
% endif
|
||||
<table class="rubric">
|
||||
% for i in range(len(categories)):
|
||||
<% category = categories[i] %>
|
||||
<tr>
|
||||
<th>${category['description']}</th>
|
||||
% for j in range(len(category['options'])):
|
||||
<% option = category['options'][j] %>
|
||||
<td>
|
||||
% if view_only:
|
||||
## if this is the selected rubric block, show it
|
||||
% if option['selected']:
|
||||
<div class="view-only selected-grade">
|
||||
% else:
|
||||
<div class="view-only">
|
||||
% endif
|
||||
${option['text']}
|
||||
<div class="grade">[${option['points']} points]</div>
|
||||
</div>
|
||||
% else:
|
||||
<div class="grade">[${option['points']} points]</div>
|
||||
%endif
|
||||
</div>
|
||||
</td>
|
||||
<input type="radio" class="score-selection" name="score-selection-${i}" id="score-${i}-${j}" value="${option['points']}"/>
|
||||
<label for="score-${i}-${j}">${option['text']}</label>
|
||||
% endif
|
||||
</td>
|
||||
% endfor
|
||||
</tr>
|
||||
% endfor
|
||||
</tr>
|
||||
% endfor
|
||||
</table>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user