EDUCATOR-165 instructor task and UI for overriding learner grades.
This commit is contained in:
@@ -28,7 +28,7 @@ from xblock.scorable import ScorableXBlockMixin, Score
|
||||
from xmodule.capa_base_constants import RANDOMIZATION, SHOWANSWER
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.graders import ShowCorrectness
|
||||
from .fields import Date, Timedelta
|
||||
from .fields import Date, Timedelta, ScoreField
|
||||
from .progress import Progress
|
||||
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
@@ -104,7 +104,8 @@ class CapaFields(object):
|
||||
attempts = Integer(
|
||||
help=_("Number of attempts taken by the student on this problem"),
|
||||
default=0,
|
||||
scope=Scope.user_state)
|
||||
scope=Scope.user_state
|
||||
)
|
||||
max_attempts = Integer(
|
||||
display_name=_("Maximum Attempts"),
|
||||
help=_("Defines the number of times a student can try to answer this problem. "
|
||||
@@ -183,6 +184,9 @@ class CapaFields(object):
|
||||
scope=Scope.user_state, default={})
|
||||
input_state = Dict(help=_("Dictionary for maintaining the state of inputtypes"), scope=Scope.user_state)
|
||||
student_answers = Dict(help=_("Dictionary with the current student responses"), scope=Scope.user_state)
|
||||
|
||||
# enforce_type is set to False here because this field is saved as a dict in the database.
|
||||
score = ScoreField(help=_("Dictionary with the current student score"), scope=Scope.user_state, enforce_type=False)
|
||||
has_saved_answers = Boolean(help=_("Whether or not the answers have been saved since last submit"),
|
||||
scope=Scope.user_state)
|
||||
done = Boolean(help=_("Whether the student has answered the problem"), scope=Scope.user_state)
|
||||
@@ -292,7 +296,8 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
|
||||
|
||||
self.set_state_from_lcp()
|
||||
|
||||
self.set_score(self.score_from_lcp())
|
||||
if self.score is None:
|
||||
self.set_score(self.score_from_lcp())
|
||||
|
||||
assert self.seed is not None
|
||||
|
||||
@@ -380,9 +385,8 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
|
||||
"""
|
||||
For now, just return weighted earned / weighted possible
|
||||
"""
|
||||
score = self.get_score()
|
||||
raw_earned = score.raw_earned
|
||||
raw_possible = score.raw_possible
|
||||
raw_earned = self.score.raw_earned
|
||||
raw_possible = self.score.raw_possible
|
||||
|
||||
if raw_possible > 0:
|
||||
if self.weight is not None:
|
||||
|
||||
@@ -331,6 +331,7 @@ class CapaDescriptor(CapaFields, RawDescriptor):
|
||||
rescore = module_attr('rescore')
|
||||
reset_problem = module_attr('reset_problem')
|
||||
save_problem = module_attr('save_problem')
|
||||
set_score = module_attr('set_score')
|
||||
set_state_from_lcp = module_attr('set_state_from_lcp')
|
||||
should_show_submit_button = module_attr('should_show_submit_button')
|
||||
should_show_reset_button = module_attr('should_show_reset_button')
|
||||
|
||||
@@ -6,6 +6,7 @@ import time
|
||||
import dateutil.parser
|
||||
from pytz import UTC
|
||||
from xblock.fields import JSONField
|
||||
from xblock.scorable import Score
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -252,3 +253,48 @@ class RelativeTime(JSONField):
|
||||
return value
|
||||
|
||||
return self.from_json(value)
|
||||
|
||||
|
||||
class ScoreField(JSONField):
|
||||
"""
|
||||
Field for blocks that need to store a Score. XBlocks that implement
|
||||
the ScorableXBlockMixin may need to store their score separately
|
||||
from their problem state, specifically for use in staff override
|
||||
of problem scores.
|
||||
"""
|
||||
MUTABLE = False
|
||||
|
||||
def from_json(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
if isinstance(value, Score):
|
||||
return value
|
||||
|
||||
if set(value) != {'raw_earned', 'raw_possible'}:
|
||||
raise TypeError('Scores must contain only a raw earned and raw possible value. Got {}'.format(
|
||||
set(value)
|
||||
))
|
||||
|
||||
raw_earned = value['raw_earned']
|
||||
raw_possible = value['raw_possible']
|
||||
|
||||
if raw_possible < 0:
|
||||
raise ValueError(
|
||||
'Error deserializing field of type {0}: Expected a positive number for raw_possible, got {1}.'.format(
|
||||
self.display_name,
|
||||
raw_possible,
|
||||
)
|
||||
)
|
||||
|
||||
if not (0 <= raw_earned <= raw_possible):
|
||||
raise ValueError(
|
||||
'Error deserializing field of type {0}: Expected raw_earned between 0 and {1}, got {2}.'.format(
|
||||
self.display_name,
|
||||
raw_possible,
|
||||
raw_earned
|
||||
)
|
||||
)
|
||||
|
||||
return Score(raw_earned, raw_possible)
|
||||
|
||||
enforce_type = from_json
|
||||
|
||||
@@ -241,8 +241,9 @@
|
||||
totalScore
|
||||
);
|
||||
}
|
||||
} else if (attemptsUsed === 0 || totalScore === 0) {
|
||||
} else if ((attemptsUsed === 0 || totalScore === 0) && curScore === 0) {
|
||||
// Render 'x point(s) possible' if student has not yet attempted question
|
||||
// But if staff has overridden score to a non-zero number, show it
|
||||
if (graded) {
|
||||
progressTemplate = ngettext(
|
||||
// Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).;
|
||||
|
||||
Reference in New Issue
Block a user