Commit 463f3161 by Vik Paruchuri

Merge remote-tracking branch 'origin/master' into fix/vik/peer-grading-images

parents f565d17b c68b0d3c
...@@ -23,6 +23,10 @@ import self_assessment_module ...@@ -23,6 +23,10 @@ import self_assessment_module
import open_ended_module import open_ended_module
from combined_open_ended_rubric import CombinedOpenEndedRubric, RubricParsingError from combined_open_ended_rubric import CombinedOpenEndedRubric, RubricParsingError
from .stringify import stringify_children from .stringify import stringify_children
import dateutil
import dateutil.parser
import datetime
from timeparse import parse_timedelta
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -54,10 +58,6 @@ HUMAN_TASK_TYPE = { ...@@ -54,10 +58,6 @@ HUMAN_TASK_TYPE = {
'openended' : "External Grader", 'openended' : "External Grader",
} }
class IncorrectMaxScoreError(Exception):
def __init__(self, msg):
self.msg = msg
class CombinedOpenEndedV1Module(): class CombinedOpenEndedV1Module():
""" """
This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc). This is a module that encapsulates all open ended grading (self assessment, peer assessment, etc).
...@@ -165,19 +165,35 @@ class CombinedOpenEndedV1Module(): ...@@ -165,19 +165,35 @@ class CombinedOpenEndedV1Module():
self.is_scored = self.metadata.get('is_graded', IS_SCORED) in TRUE_DICT self.is_scored = self.metadata.get('is_graded', IS_SCORED) in TRUE_DICT
self.accept_file_upload = self.metadata.get('accept_file_upload', ACCEPT_FILE_UPLOAD) in TRUE_DICT self.accept_file_upload = self.metadata.get('accept_file_upload', ACCEPT_FILE_UPLOAD) in TRUE_DICT
display_due_date_string = self.metadata.get('due', None)
if display_due_date_string is not None:
try:
self.display_due_date = dateutil.parser.parse(display_due_date_string)
except ValueError:
log.error("Could not parse due date {0} for location {1}".format(display_due_date_string, location))
raise
else:
self.display_due_date = None
grace_period_string = self.metadata.get('graceperiod', None)
if grace_period_string is not None and self.display_due_date:
try:
self.grace_period = parse_timedelta(grace_period_string)
self.close_date = self.display_due_date + self.grace_period
except:
log.error("Error parsing the grace period {0} for location {1}".format(grace_period_string, location))
raise
else:
self.grace_period = None
self.close_date = self.display_due_date
# Used for progress / grading. Currently get credit just for # Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect). # completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) self._max_score = int(self.metadata.get('max_score', MAX_SCORE))
if self._max_score > MAX_SCORE_ALLOWED:
error_message = "Max score {0} is higher than max score allowed {1} for location {2}".format(self._max_score,
MAX_SCORE_ALLOWED, location)
log.error(error_message)
raise IncorrectMaxScoreError(error_message)
rubric_renderer = CombinedOpenEndedRubric(system, True) rubric_renderer = CombinedOpenEndedRubric(system, True)
rubric_string = stringify_children(definition['rubric']) rubric_string = stringify_children(definition['rubric'])
rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED) rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED, self._max_score)
#Static data is passed to the child modules to render #Static data is passed to the child modules to render
self.static_data = { self.static_data = {
...@@ -187,6 +203,7 @@ class CombinedOpenEndedV1Module(): ...@@ -187,6 +203,7 @@ class CombinedOpenEndedV1Module():
'rubric': definition['rubric'], 'rubric': definition['rubric'],
'display_name': self.display_name, 'display_name': self.display_name,
'accept_file_upload': self.accept_file_upload, 'accept_file_upload': self.accept_file_upload,
'close_date' : self.close_date,
} }
self.task_xml = definition['task_xml'] self.task_xml = definition['task_xml']
......
...@@ -29,10 +29,13 @@ class CombinedOpenEndedRubric(object): ...@@ -29,10 +29,13 @@ class CombinedOpenEndedRubric(object):
success = False success = False
try: try:
rubric_categories = self.extract_categories(rubric_xml) rubric_categories = self.extract_categories(rubric_xml)
html = self.system.render_template('open_ended_rubric.html', max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories)
max_score = max(max_scores)
html = self.system.render_template('open_ended_rubric.html',
{'categories': rubric_categories, {'categories': rubric_categories,
'has_score': self.has_score, 'has_score': self.has_score,
'view_only': self.view_only}) 'view_only': self.view_only,
'max_score': max_score})
success = True success = True
except: except:
error_message = "[render_rubric] Could not parse the rubric with xml: {0}".format(rubric_xml) error_message = "[render_rubric] Could not parse the rubric with xml: {0}".format(rubric_xml)
...@@ -40,7 +43,7 @@ class CombinedOpenEndedRubric(object): ...@@ -40,7 +43,7 @@ class CombinedOpenEndedRubric(object):
raise RubricParsingError(error_message) raise RubricParsingError(error_message)
return success, html return success, html
def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed): def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed, max_score):
success, rubric_feedback = self.render_rubric(rubric_string) success, rubric_feedback = self.render_rubric(rubric_string)
if not success: if not success:
error_message = "Could not parse rubric : {0} for location {1}".format(rubric_string, location.url()) error_message = "Could not parse rubric : {0} for location {1}".format(rubric_string, location.url())
...@@ -48,13 +51,21 @@ class CombinedOpenEndedRubric(object): ...@@ -48,13 +51,21 @@ class CombinedOpenEndedRubric(object):
raise RubricParsingError(error_message) raise RubricParsingError(error_message)
rubric_categories = self.extract_categories(rubric_string) rubric_categories = self.extract_categories(rubric_string)
total = 0
for category in rubric_categories: for category in rubric_categories:
total = total + len(category['options']) - 1
if len(category['options']) > (max_score_allowed + 1): if len(category['options']) > (max_score_allowed + 1):
error_message = "Number of score points in rubric {0} higher than the max allowed, which is {1}".format( error_message = "Number of score points in rubric {0} higher than the max allowed, which is {1}".format(
len(category['options']), max_score_allowed) len(category['options']), max_score_allowed)
log.error(error_message) log.error(error_message)
raise RubricParsingError(error_message) raise RubricParsingError(error_message)
if total != max_score:
error_msg = "The max score {0} for problem {1} does not match the total number of points in the rubric {2}".format(
max_score, location, total)
log.error(error_msg)
raise RubricParsingError(error_msg)
def extract_categories(self, element): def extract_categories(self, element):
''' '''
Contstruct a list of categories such that the structure looks like: Contstruct a list of categories such that the structure looks like:
......
...@@ -231,47 +231,6 @@ div.result-container { ...@@ -231,47 +231,6 @@ div.result-container {
} }
} }
div.result-container, section.open-ended-child {
.rubric {
margin-bottom:25px;
tr {
margin:10px 0px;
height: 100%;
}
td {
padding: 20px 0px 25px 0px;
margin: 10px 0px;
height: 100%;
}
th {
padding: 5px;
margin: 5px;
}
label,
.view-only {
margin:2px;
position: relative;
padding: 10px 15px 25px 15px;
width: 145px;
height:100%;
display: inline-block;
min-height: 50px;
min-width: 50px;
background-color: #CCC;
font-size: .85em;
}
.grade {
position: absolute;
bottom:0px;
right:0px;
margin:10px;
}
.selected-grade {
background: #666;
color: white;
}
}
}
section.open-ended-child { section.open-ended-child {
@media print { @media print {
...@@ -445,7 +404,6 @@ section.open-ended-child { ...@@ -445,7 +404,6 @@ section.open-ended-child {
div.short-form-response { div.short-form-response {
background: #F6F6F6; background: #F6F6F6;
border: 1px solid #ddd; border: 1px solid #ddd;
border-top: 0;
margin-bottom: 20px; margin-bottom: 20px;
overflow-y: auto; overflow-y: auto;
height: 200px; height: 200px;
...@@ -586,11 +544,6 @@ section.open-ended-child { ...@@ -586,11 +544,6 @@ section.open-ended-child {
} }
.submission_feedback { .submission_feedback {
// background: #F3F3F3;
// border: 1px solid #ddd;
// @include border-radius(3px);
// padding: 8px 12px;
// margin-top: 10px;
@include inline-block; @include inline-block;
font-style: italic; font-style: italic;
margin: 8px 0 0 10px; margin: 8px 0 0 10px;
......
...@@ -113,7 +113,7 @@ class GradingService(object): ...@@ -113,7 +113,7 @@ class GradingService(object):
try: try:
if 'rubric' in response_json: if 'rubric' in response_json:
rubric = response_json['rubric'] rubric = response_json['rubric']
rubric_renderer = CombinedOpenEndedRubric(self.system, False) rubric_renderer = CombinedOpenEndedRubric(self.system, view_only)
success, rubric_html = rubric_renderer.render_rubric(rubric) success, rubric_html = rubric_renderer.render_rubric(rubric)
response_json['rubric'] = rubric_html response_json['rubric'] = rubric_html
return response_json return response_json
......
class @Rubric
constructor: () ->
# finds the scores for each rubric category
@get_score_list: () =>
# find the number of categories:
num_categories = $('table.rubric tr').length
score_lst = []
# get the score for each one
for i in [0..(num_categories-2)]
score = $("input[name='score-selection-#{i}']:checked").val()
score_lst.push(score)
return score_lst
@get_total_score: () ->
score_lst = @get_score_list()
tot = 0
for score in score_lst
tot += parseInt(score)
return tot
@check_complete: () ->
# check to see whether or not any categories have not been scored
num_categories = $('table.rubric tr').length
# -2 because we want to skip the header
for i in [0..(num_categories-2)]
score = $("input[name='score-selection-#{i}']:checked").val()
if score == undefined
return false
return true
class @CombinedOpenEnded class @CombinedOpenEnded
constructor: (element) -> constructor: (element) ->
@element=element @element=element
...@@ -222,9 +255,9 @@ class @CombinedOpenEnded ...@@ -222,9 +255,9 @@ class @CombinedOpenEnded
save_assessment: (event) => save_assessment: (event) =>
event.preventDefault() event.preventDefault()
if @child_state == 'assessing' if @child_state == 'assessing' && Rubric.check_complete()
checked_assessment = @$('input[name="grade-selection"]:checked') checked_assessment = Rubric.get_total_score()
data = {'assessment' : checked_assessment.val()} data = {'assessment' : checked_assessment}
$.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) => $.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) =>
if response.success if response.success
@child_state = response.state @child_state = response.state
......
...@@ -233,23 +233,11 @@ class @PeerGradingProblem ...@@ -233,23 +233,11 @@ class @PeerGradingProblem
fetch_submission_essay: () => fetch_submission_essay: () =>
@backend.post('get_next_submission', {location: @location}, @render_submission) @backend.post('get_next_submission', {location: @location}, @render_submission)
# finds the scores for each rubric category
get_score_list: () =>
# find the number of categories:
num_categories = $('table.rubric tr').length
score_lst = []
# get the score for each one
for i in [0..(num_categories-1)]
score = $("input[name='score-selection-#{i}']:checked").val()
score_lst.push(score)
return score_lst
construct_data: () -> construct_data: () ->
data = data =
rubric_scores: @get_score_list() rubric_scores: Rubric.get_score_list()
score: @grade score: Rubric.get_total_score()
location: @location location: @location
submission_id: @essay_id_input.val() submission_id: @essay_id_input.val()
submission_key: @submission_key_input.val() submission_key: @submission_key_input.val()
...@@ -317,17 +305,11 @@ class @PeerGradingProblem ...@@ -317,17 +305,11 @@ class @PeerGradingProblem
# called after a grade is selected on the interface # called after a grade is selected on the interface
graded_callback: (event) => graded_callback: (event) =>
@grade = $("input[name='grade-selection']:checked").val()
if @grade == undefined
return
# check to see whether or not any categories have not been scored # check to see whether or not any categories have not been scored
num_categories = $('table.rubric tr').length if Rubric.check_complete()
for i in [0..(num_categories-1)] # show button if we have scores for all categories
score = $("input[name='score-selection-#{i}']:checked").val() @show_submit_button()
if score == undefined @grade = Rubric.get_total_score()
return
# show button if we have scores for all categories
@show_submit_button()
...@@ -401,6 +383,7 @@ class @PeerGradingProblem ...@@ -401,6 +383,7 @@ class @PeerGradingProblem
# render common information between calibration and grading # render common information between calibration and grading
render_submission_data: (response) => render_submission_data: (response) =>
@content_panel.show() @content_panel.show()
@error_container.hide()
@submission_container.append(@make_paragraphs(response.student_response)) @submission_container.append(@make_paragraphs(response.student_response))
@prompt_container.html(response.prompt) @prompt_container.html(response.prompt)
...@@ -448,28 +431,5 @@ class @PeerGradingProblem ...@@ -448,28 +431,5 @@ class @PeerGradingProblem
@submit_button.show() @submit_button.show()
setup_score_selection: (max_score) => setup_score_selection: (max_score) =>
# first, get rid of all the old inputs, if any.
@score_selection_container.html("""
<h3>Overall Score</h3>
<p>Choose an overall score for this submission.</p>
""")
# Now create new labels and inputs for each possible score.
for score in [0..max_score]
id = 'score-' + score
label = """<label for="#{id}">#{score}</label>"""
input = """
<input type="radio" name="grade-selection" id="#{id}" value="#{score}"/>
""" # " fix broken parsing in emacs
@score_selection_container.append(input + label)
# And now hook up an event handler again # And now hook up an event handler again
$("input[name='score-selection']").change @graded_callback $("input[class='score-selection']").change @graded_callback
$("input[name='grade-selection']").change @graded_callback
#mock_backend = false
#ajax_url = $('.peer-grading').data('ajax_url')
#backend = new PeerGradingProblemBackend(ajax_url, mock_backend)
#$(document).ready(() -> new PeerGradingProblem(backend))
...@@ -549,14 +549,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -549,14 +549,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
@param system: modulesystem @param system: modulesystem
@return: Success indicator @return: Success indicator
""" """
if self.attempts > self.max_attempts: # Once we close the problem, we should not allow students
# If too many attempts, prevent student from saving answer and # to save answers
# seeing rubric. In normal use, students shouldn't see this because closed, msg = self.check_if_closed()
# they won't see the reset button once they're out of attempts. if closed:
return { return msg
'success': False,
'error': 'Too many attempts.'
}
if self.state != self.INITIAL: if self.state != self.INITIAL:
return self.out_of_sync_error(get) return self.out_of_sync_error(get)
......
...@@ -74,7 +74,7 @@ class OpenEndedChild(object): ...@@ -74,7 +74,7 @@ class OpenEndedChild(object):
'done': 'Problem complete', 'done': 'Problem complete',
} }
def __init__(self, system, location, definition, descriptor, static_data, def __init__(self, system, location, definition, descriptor, static_data,
instance_state=None, shared_state=None, **kwargs): instance_state=None, shared_state=None, **kwargs):
# Load instance state # Load instance state
if instance_state is not None: if instance_state is not None:
...@@ -99,6 +99,7 @@ class OpenEndedChild(object): ...@@ -99,6 +99,7 @@ class OpenEndedChild(object):
self.rubric = static_data['rubric'] self.rubric = static_data['rubric']
self.display_name = static_data['display_name'] self.display_name = static_data['display_name']
self.accept_file_upload = static_data['accept_file_upload'] self.accept_file_upload = static_data['accept_file_upload']
self.close_date = static_data['close_date']
# Used for progress / grading. Currently get credit just for # Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect). # completion (doesn't matter if you self-assessed correct/incorrect).
...@@ -117,6 +118,27 @@ class OpenEndedChild(object): ...@@ -117,6 +118,27 @@ class OpenEndedChild(object):
""" """
pass pass
def closed(self):
if self.close_date is not None and datetime.utcnow() > self.close_date:
return True
return False
def check_if_closed(self):
if self.closed():
return True, {
'success': False,
'error': 'This problem is now closed.'
}
elif self.attempts > self.max_attempts:
return True, {
'success': False,
'error': 'Too many attempts.'
}
else:
return False, {}
def latest_answer(self): def latest_answer(self):
"""Empty string if not available""" """Empty string if not available"""
if not self.history: if not self.history:
......
...@@ -125,7 +125,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -125,7 +125,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
if self.state == self.INITIAL: if self.state == self.INITIAL:
return '' return ''
rubric_renderer = CombinedOpenEndedRubric(system, True) rubric_renderer = CombinedOpenEndedRubric(system, False)
success, rubric_html = rubric_renderer.render_rubric(self.rubric) success, rubric_html = rubric_renderer.render_rubric(self.rubric)
# we'll render it # we'll render it
...@@ -190,15 +190,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -190,15 +190,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
Dictionary with keys 'success' and either 'error' (if not success), Dictionary with keys 'success' and either 'error' (if not success),
or 'rubric_html' (if success). or 'rubric_html' (if success).
""" """
# Check to see if attempts are less than max # Check to see if this problem is closed
if self.attempts > self.max_attempts: closed, msg = self.check_if_closed()
# If too many attempts, prevent student from saving answer and if closed:
# seeing rubric. In normal use, students shouldn't see this because return msg
# they won't see the reset button once they're out of attempts.
return {
'success': False,
'error': 'Too many attempts.'
}
if self.state != self.INITIAL: if self.state != self.INITIAL:
return self.out_of_sync_error(get) return self.out_of_sync_error(get)
......
...@@ -31,9 +31,10 @@ class OpenEndedChildTest(unittest.TestCase): ...@@ -31,9 +31,10 @@ class OpenEndedChildTest(unittest.TestCase):
<category> <category>
<description>Response Quality</description> <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> <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>
<option>Second option</option>
</category> </category>
</rubric></rubric>''' </rubric></rubric>'''
max_score = 4 max_score = 1
static_data = { static_data = {
'max_attempts': 20, 'max_attempts': 20,
...@@ -42,6 +43,7 @@ class OpenEndedChildTest(unittest.TestCase): ...@@ -42,6 +43,7 @@ class OpenEndedChildTest(unittest.TestCase):
'max_score': max_score, 'max_score': max_score,
'display_name': 'Name', 'display_name': 'Name',
'accept_file_upload': False, 'accept_file_upload': False,
'close_date': None
} }
definition = Mock() definition = Mock()
descriptor = Mock() descriptor = Mock()
...@@ -158,6 +160,7 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -158,6 +160,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'display_name': 'Name', 'display_name': 'Name',
'accept_file_upload': False, 'accept_file_upload': False,
'rewrite_content_links' : "", 'rewrite_content_links' : "",
'close_date': None,
} }
oeparam = etree.XML(''' oeparam = etree.XML('''
...@@ -274,9 +277,10 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -274,9 +277,10 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
<category> <category>
<description>Response Quality</description> <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> <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>
<option>Second option</option>
</category> </category>
</rubric></rubric>''' </rubric></rubric>'''
max_score = 3 max_score = 1
metadata = {'attempts': '10', 'max_score': max_score} metadata = {'attempts': '10', 'max_score': max_score}
...@@ -288,6 +292,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -288,6 +292,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
'display_name': 'Name', 'display_name': 'Name',
'accept_file_upload' : False, 'accept_file_upload' : False,
'rewrite_content_links' : "", 'rewrite_content_links' : "",
'close_date' : "",
} }
oeparam = etree.XML(''' oeparam = etree.XML('''
...@@ -320,7 +325,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -320,7 +325,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
descriptor = Mock() descriptor = Mock()
def setUp(self): def setUp(self):
self.combinedoe = CombinedOpenEndedV1Module(test_system, self.location, self.definition, self.descriptor, self.static_data, metadata=self.metadata) self.combinedoe = CombinedOpenEndedV1Module(test_system, self.location, self.definition, self.descriptor, static_data = self.static_data, metadata=self.metadata)
def test_get_tag_name(self): def test_get_tag_name(self):
name = self.combinedoe.get_tag_name("<t>Tag</t>") name = self.combinedoe.get_tag_name("<t>Tag</t>")
......
...@@ -46,11 +46,13 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -46,11 +46,13 @@ class SelfAssessmentTest(unittest.TestCase):
'max_score': 1, 'max_score': 1,
'display_name': "Name", 'display_name': "Name",
'accept_file_upload': False, 'accept_file_upload': False,
'close_date': None
} }
self.module = SelfAssessmentModule(test_system, self.location, self.module = SelfAssessmentModule(test_system, self.location,
self.definition, self.descriptor, self.definition, self.descriptor,
static_data, state, metadata=self.metadata) static_data,
state, metadata=self.metadata)
def test_get_html(self): def test_get_html(self):
html = self.module.get_html(test_system) html = self.module.get_html(test_system)
......
...@@ -2,9 +2,12 @@ ...@@ -2,9 +2,12 @@
Helper functions for handling time in the format we like. Helper functions for handling time in the format we like.
""" """
import time import time
import re
from datetime import timedelta
TIME_FORMAT = "%Y-%m-%dT%H:%M" TIME_FORMAT = "%Y-%m-%dT%H:%M"
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
def parse_time(time_str): def parse_time(time_str):
""" """
...@@ -22,3 +25,23 @@ def stringify_time(time_struct): ...@@ -22,3 +25,23 @@ def stringify_time(time_struct):
Convert a time struct to a string Convert a time struct to a string
""" """
return time.strftime(TIME_FORMAT, time_struct) return time.strftime(TIME_FORMAT, time_struct)
def parse_timedelta(time_str):
"""
time_str: A string with the following components:
<D> day[s] (optional)
<H> hour[s] (optional)
<M> minute[s] (optional)
<S> second[s] (optional)
Returns a datetime.timedelta parsed from the string
"""
parts = TIMEDELTA_REGEX.match(time_str)
if not parts:
return
parts = parts.groupdict()
time_params = {}
for (name, param) in parts.iteritems():
if param:
time_params[name] = int(param)
return timedelta(**time_params)
...@@ -212,55 +212,19 @@ class @StaffGrading ...@@ -212,55 +212,19 @@ class @StaffGrading
setup_score_selection: => setup_score_selection: =>
# first, get rid of all the old inputs, if any.
@grade_selection_container.html("""
<h3>Overall Score</h3>
<p>Choose an overall score for this submission.</p>
""")
# Now create new labels and inputs for each possible score.
for score in [0..@max_score]
id = 'score-' + score
label = """<label for="#{id}">#{score}</label>"""
input = """
<input type="radio" class="grade-selection" name="grade-selection" id="#{id}" value="#{score}"/>
""" # " fix broken parsing in emacs
@grade_selection_container.append(input + label)
$('.grade-selection').click => @graded_callback()
@score_selection_container.html(@rubric) @score_selection_container.html(@rubric)
$('.score-selection').click => @graded_callback() $('.score-selection').click => @graded_callback()
graded_callback: () => graded_callback: () =>
@grade = $("input[name='grade-selection']:checked").val() # show button if we have scores for all categories
if @grade == undefined if Rubric.check_complete()
return @state = state_graded
# check to see whether or not any categories have not been scored @submit_button.show()
num_categories = $('table.rubric tr').length
for i in [0..(num_categories-1)]
score = $("input[name='score-selection-#{i}']:checked").val()
if score == undefined
return
# show button if we have scores for all categories
@state = state_graded
@submit_button.show()
set_button_text: (text) => set_button_text: (text) =>
@action_button.attr('value', text) @action_button.attr('value', text)
# finds the scores for each rubric category
get_score_list: () =>
# find the number of categories:
num_categories = $('table.rubric tr').length
score_lst = []
# get the score for each one
for i in [0..(num_categories-1)]
score = $("input[name='score-selection-#{i}']:checked").val()
score_lst.push(score)
return score_lst
ajax_callback: (response) => ajax_callback: (response) =>
# always clear out errors and messages on transition. # always clear out errors and messages on transition.
@error_msg = '' @error_msg = ''
...@@ -285,8 +249,8 @@ class @StaffGrading ...@@ -285,8 +249,8 @@ class @StaffGrading
skip_and_get_next: () => skip_and_get_next: () =>
data = data =
score: @grade score: Rubric.get_total_score()
rubric_scores: @get_score_list() rubric_scores: Rubric.get_score_list()
feedback: @feedback_area.val() feedback: @feedback_area.val()
submission_id: @submission_id submission_id: @submission_id
location: @location location: @location
...@@ -299,8 +263,8 @@ class @StaffGrading ...@@ -299,8 +263,8 @@ class @StaffGrading
submit_and_get_next: () -> submit_and_get_next: () ->
data = data =
score: @grade score: Rubric.get_total_score()
rubric_scores: @get_score_list() rubric_scores: Rubric.get_score_list()
feedback: @feedback_area.val() feedback: @feedback_area.val()
submission_id: @submission_id submission_id: @submission_id
location: @location location: @location
......
...@@ -44,9 +44,9 @@ ...@@ -44,9 +44,9 @@
@import "course/gradebook"; @import "course/gradebook";
@import "course/tabs"; @import "course/tabs";
@import "course/staff_grading"; @import "course/staff_grading";
@import "course/rubric";
@import "course/open_ended_grading"; @import "course/open_ended_grading";
// instructor // instructor
@import "course/instructor/instructor"; @import "course/instructor/instructor";
......
.rubric {
margin: 40px 0px;
tr {
margin:10px 0px;
height: 100%;
}
td {
padding: 20px 0px 25px 0px;
height: 100%;
border: 1px black solid;
text-align: center;
}
th {
padding: 5px;
margin: 5px;
text-align: center;
}
.points-header th {
padding: 0px;
}
.rubric-label
{
position: relative;
padding: 0px 15px 15px 15px;
width: 130px;
min-height: 50px;
min-width: 50px;
font-size: .9em;
background-color: white;
display: block;
}
.grade {
position: absolute;
bottom:0px;
right:0px;
margin:10px;
}
.selected-grade,
.selected-grade .rubric-label {
background: #666;
color: white;
}
input[type=radio]:checked + .rubric-label {
background: white;
color: $base-font-color; }
input[class='score-selection'] {
position: relative;
margin-left: 10px;
font-size: 16px;
}
}
...@@ -12,7 +12,7 @@ div.peer-grading{ ...@@ -12,7 +12,7 @@ div.peer-grading{
label { label {
margin: 10px; margin: 10px;
padding: 5px; padding: 5px;
display: inline-block; @include inline-block;
min-width: 50px; min-width: 50px;
background-color: #CCC; background-color: #CCC;
text-size: 1.5em; text-size: 1.5em;
...@@ -176,49 +176,4 @@ div.peer-grading{ ...@@ -176,49 +176,4 @@ div.peer-grading{
} }
} }
padding: 40px; padding: 40px;
.rubric {
tr {
margin:10px 0px;
height: 100%;
}
td {
padding: 20px 0px 25px 0px;
height: 100%;
}
th {
padding: 5px;
margin: 5px;
}
label,
.view-only {
margin:2px;
position: relative;
padding: 15px 15px 25px 15px;
width: 150px;
height:100%;
display: inline-block;
min-height: 50px;
min-width: 50px;
background-color: #CCC;
font-size: .9em;
}
.grade {
position: absolute;
bottom:0px;
right:0px;
margin:10px;
}
.selected-grade {
background: #666;
color: white;
}
input[type=radio]:checked + label {
background: #666;
color: white; }
input[class='score-selection'] {
display: none;
}
}
} }
...@@ -75,6 +75,7 @@ ...@@ -75,6 +75,7 @@
</p> </p>
<p class="grade-selection-container"> <p class="grade-selection-container">
</p> </p>
<h3>Written Feedback</h3>
<textarea name="feedback" placeholder="Feedback for student (optional)" <textarea name="feedback" placeholder="Feedback for student (optional)"
class="feedback-area" cols="70" ></textarea> class="feedback-area" cols="70" ></textarea>
</div> </div>
......
...@@ -8,26 +8,33 @@ ...@@ -8,26 +8,33 @@
<p>Select the criteria you feel best represents this submission in each category.</p> <p>Select the criteria you feel best represents this submission in each category.</p>
% endif % endif
<table class="rubric"> <table class="rubric">
<tr class="points-header">
<th></th>
% for i in range(max_score + 1):
<th>
${i} points
</th>
% endfor
</tr>
% for i in range(len(categories)): % for i in range(len(categories)):
<% category = categories[i] %> <% category = categories[i] %>
<tr> <tr>
<th>${category['description']}</th> <th>${category['description']}</th>
% for j in range(len(category['options'])): % for j in range(len(category['options'])):
<% option = category['options'][j] %> <% option = category['options'][j] %>
%if option['selected']:
<td class="selected-grade">
%else:
<td> <td>
% endif
% if view_only: % if view_only:
## if this is the selected rubric block, show it highlighted ## if this is the selected rubric block, show it highlighted
% if option['selected']: <div class="rubric-label">
<div class="view-only selected-grade">
% else:
<div class="view-only">
% endif
${option['text']} ${option['text']}
<div class="grade">[${option['points']} points]</div>
</div> </div>
% else: % else:
<input type="radio" class="score-selection" name="score-selection-${i}" id="score-${i}-${j}" value="${option['points']}"/> <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> <label class="rubric-label" for="score-${i}-${j}">${option['text']}</label>
% endif % endif
</td> </td>
% endfor % endfor
......
...@@ -52,7 +52,9 @@ ...@@ -52,7 +52,9 @@
<p class="rubric-selection-container"></p> <p class="rubric-selection-container"></p>
<p class="score-selection-container"> <p class="score-selection-container">
</p> </p>
<textarea name="feedback" placeholder="Feedback for student (optional)" <h3>Written Feedback</h3>
<p>Please include some written feedback as well.</p>
<textarea name="feedback" placeholder="Feedback for student"
class="feedback-area" cols="70" ></textarea> class="feedback-area" cols="70" ></textarea>
<p class="flag-student-container">Flag this submission for review by course staff (use if the submission contains inappropriate content): <input type="checkbox" class="flag-checkbox" value="student_is_flagged"></p> <p class="flag-student-container">Flag this submission for review by course staff (use if the submission contains inappropriate content): <input type="checkbox" class="flag-checkbox" value="student_is_flagged"></p>
</div> </div>
......
...@@ -2,20 +2,4 @@ ...@@ -2,20 +2,4 @@
<div class="rubric"> <div class="rubric">
${rubric | n } ${rubric | n }
</div> </div>
% if not read_only:
<div class="scoring-container">
<h3>Scoring</h3>
<p>Please select a score below:</p>
<div class="grade-selection">
%for i in xrange(0,max_score+1):
<% id = "score-{0}".format(i) %>
<input type="radio" class="grade-selection" name="grade-selection" value="${i}" id="${id}">
<label for="${id}">${i}</label>
%endfor
</div>
</div>
% endif
</div> </div>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment