Commit 6ca8a702 by Jillian Vogel Committed by Tim Krones

Mask grades on progress page according to "Show Correctness" setting.

parent e8a36957
...@@ -25,8 +25,9 @@ from capa.responsetypes import StudentInputError, ResponseError, LoncapaProblemE ...@@ -25,8 +25,9 @@ from capa.responsetypes import StudentInputError, ResponseError, LoncapaProblemE
from capa.util import convert_files_to_filenames, get_inner_html_from_xpath from capa.util import convert_files_to_filenames, get_inner_html_from_xpath
from xblock.fields import Boolean, Dict, Float, Integer, Scope, String, XMLString from xblock.fields import Boolean, Dict, Float, Integer, Scope, String, XMLString
from xblock.scorable import ScorableXBlockMixin, Score from xblock.scorable import ScorableXBlockMixin, Score
from xmodule.capa_base_constants import RANDOMIZATION, SHOWANSWER, SHOW_CORRECTNESS from xmodule.capa_base_constants import RANDOMIZATION, SHOWANSWER
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from xmodule.graders import ShowCorrectness
from .fields import Date, Timedelta from .fields import Date, Timedelta
from .progress import Progress from .progress import Progress
...@@ -120,11 +121,11 @@ class CapaFields(object): ...@@ -120,11 +121,11 @@ class CapaFields(object):
help=_("Defines when to show whether a learner's answer to the problem is correct. " help=_("Defines when to show whether a learner's answer to the problem is correct. "
"Configured on the subsection."), "Configured on the subsection."),
scope=Scope.settings, scope=Scope.settings,
default=SHOW_CORRECTNESS.ALWAYS, default=ShowCorrectness.ALWAYS,
values=[ values=[
{"display_name": _("Always"), "value": SHOW_CORRECTNESS.ALWAYS}, {"display_name": _("Always"), "value": ShowCorrectness.ALWAYS},
{"display_name": _("Never"), "value": SHOW_CORRECTNESS.NEVER}, {"display_name": _("Never"), "value": ShowCorrectness.NEVER},
{"display_name": _("Past Due"), "value": SHOW_CORRECTNESS.PAST_DUE}, {"display_name": _("Past Due"), "value": ShowCorrectness.PAST_DUE},
], ],
) )
showanswer = String( showanswer = String(
...@@ -921,17 +922,11 @@ class CapaMixin(ScorableXBlockMixin, CapaFields): ...@@ -921,17 +922,11 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
Limits access to the correct/incorrect flags, messages, and problem score. Limits access to the correct/incorrect flags, messages, and problem score.
""" """
if self.show_correctness == SHOW_CORRECTNESS.NEVER: return ShowCorrectness.correctness_available(
return False show_correctness=self.show_correctness,
elif self.runtime.user_is_staff: due_date=self.close_date,
# This is after the 'never' check because admins can see correctness has_staff_access=self.runtime.user_is_staff,
# unless the problem explicitly prevents it )
return True
elif self.show_correctness == SHOW_CORRECTNESS.PAST_DUE:
return self.is_past_due()
# else: self.show_correctness == SHOW_CORRECTNESS.ALWAYS
return True
def update_score(self, data): def update_score(self, data):
""" """
......
...@@ -4,15 +4,6 @@ Constants for capa_base problems ...@@ -4,15 +4,6 @@ Constants for capa_base problems
""" """
class SHOW_CORRECTNESS(object): # pylint: disable=invalid-name
"""
Constants for when to show correctness
"""
ALWAYS = "always"
PAST_DUE = "past_due"
NEVER = "never"
class SHOWANSWER(object): class SHOWANSWER(object):
""" """
Constants for when to show answer Constants for when to show answer
......
...@@ -10,9 +10,10 @@ import logging ...@@ -10,9 +10,10 @@ import logging
import random import random
import sys import sys
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime # Used by pycontracts. pylint: disable=unused-import from datetime import datetime
from contracts import contract from contracts import contract
from pytz import UTC
log = logging.getLogger("edx.courseware") log = logging.getLogger("edx.courseware")
...@@ -462,3 +463,38 @@ def _min_or_none(itr): ...@@ -462,3 +463,38 @@ def _min_or_none(itr):
return min(itr) return min(itr)
except ValueError: except ValueError:
return None return None
class ShowCorrectness(object):
"""
Helper class for determining whether correctness is currently hidden for a block.
When correctness is hidden, this limits the user's access to the correct/incorrect flags, messages, problem scores,
and aggregate subsection and course grades.
"""
"""
Constants used to indicate when to show correctness
"""
ALWAYS = "always"
PAST_DUE = "past_due"
NEVER = "never"
@classmethod
def correctness_available(cls, show_correctness='', due_date=None, has_staff_access=False):
"""
Returns whether correctness is available now, for the given attributes.
"""
if show_correctness == cls.NEVER:
return False
elif has_staff_access:
# This is after the 'never' check because course staff can see correctness
# unless the sequence/problem explicitly prevents it
return True
elif show_correctness == cls.PAST_DUE:
# Is it now past the due date?
return (due_date is None or
due_date < datetime.now(UTC))
# else: show_correctness == cls.ALWAYS
return True
...@@ -2,13 +2,15 @@ ...@@ -2,13 +2,15 @@
Grading tests Grading tests
""" """
from datetime import datetime
import ddt
import unittest import unittest
from datetime import datetime, timedelta
import ddt
from pytz import UTC
from xmodule import graders from xmodule import graders
from xmodule.graders import ProblemScore, AggregatedScore, aggregate_scores from xmodule.graders import (
AggregatedScore, ProblemScore, ShowCorrectness, aggregate_scores
)
class GradesheetTest(unittest.TestCase): class GradesheetTest(unittest.TestCase):
...@@ -315,3 +317,86 @@ class GraderTest(unittest.TestCase): ...@@ -315,3 +317,86 @@ class GraderTest(unittest.TestCase):
with self.assertRaises(ValueError) as error: with self.assertRaises(ValueError) as error:
graders.grader_from_conf([invalid_conf]) graders.grader_from_conf([invalid_conf])
self.assertIn(expected_error_message, error.exception.message) self.assertIn(expected_error_message, error.exception.message)
@ddt.ddt
class ShowCorrectnessTest(unittest.TestCase):
"""
Tests the correctness_available method
"""
def setUp(self):
super(ShowCorrectnessTest, self).setUp()
now = datetime.now(UTC)
day_delta = timedelta(days=1)
self.yesterday = now - day_delta
self.today = now
self.tomorrow = now + day_delta
def test_show_correctness_default(self):
"""
Test that correctness is visible by default.
"""
self.assertTrue(ShowCorrectness.correctness_available())
@ddt.data(
(ShowCorrectness.ALWAYS, True),
(ShowCorrectness.ALWAYS, False),
# Any non-constant values behave like "always"
('', True),
('', False),
('other-value', True),
('other-value', False),
)
@ddt.unpack
def test_show_correctness_always(self, show_correctness, has_staff_access):
"""
Test that correctness is visible when show_correctness is turned on.
"""
self.assertTrue(ShowCorrectness.correctness_available(
show_correctness=show_correctness,
has_staff_access=has_staff_access
))
@ddt.data(True, False)
def test_show_correctness_never(self, has_staff_access):
"""
Test that show_correctness="never" hides correctness from learners and course staff.
"""
self.assertFalse(ShowCorrectness.correctness_available(
show_correctness=ShowCorrectness.NEVER,
has_staff_access=has_staff_access
))
@ddt.data(
# Correctness not visible to learners if due date in the future
('tomorrow', False, False),
# Correctness is visible to learners if due date in the past
('yesterday', False, True),
# Correctness is visible to learners if due date in the past (just)
('today', False, True),
# Correctness is visible to learners if there is no due date
(None, False, True),
# Correctness is visible to staff if due date in the future
('tomorrow', True, True),
# Correctness is visible to staff if due date in the past
('yesterday', True, True),
# Correctness is visible to staff if there is no due date
(None, True, True),
)
@ddt.unpack
def test_show_correctness_past_due(self, due_date_str, has_staff_access, expected_result):
"""
Test show_correctness="past_due" to ensure:
* correctness is always visible to course staff
* correctness is always visible to everyone if there is no due date
* correctness is visible to learners after the due date, when there is a due date.
"""
if due_date_str is None:
due_date = None
else:
due_date = getattr(self, due_date_str)
self.assertEquals(
ShowCorrectness.correctness_available(ShowCorrectness.PAST_DUE, due_date, has_staff_access),
expected_result
)
...@@ -40,6 +40,7 @@ SUPPORTED_FIELDS = [ ...@@ -40,6 +40,7 @@ SUPPORTED_FIELDS = [
SupportedFieldType('graded'), SupportedFieldType('graded'),
SupportedFieldType('format'), SupportedFieldType('format'),
SupportedFieldType('due'), SupportedFieldType('due'),
SupportedFieldType('show_correctness'),
# 'student_view_data' # 'student_view_data'
SupportedFieldType(StudentViewTransformer.STUDENT_VIEW_DATA, StudentViewTransformer), SupportedFieldType(StudentViewTransformer.STUDENT_VIEW_DATA, StudentViewTransformer),
# 'student_view_multi_device' # 'student_view_multi_device'
......
...@@ -44,7 +44,7 @@ class BlocksAPITransformer(BlockStructureTransformer): ...@@ -44,7 +44,7 @@ class BlocksAPITransformer(BlockStructureTransformer):
transform method. transform method.
""" """
# collect basic xblock fields # collect basic xblock fields
block_structure.request_xblock_fields('graded', 'format', 'display_name', 'category', 'due') block_structure.request_xblock_fields('graded', 'format', 'display_name', 'category', 'due', 'show_correctness')
# collect data from containing transformers # collect data from containing transformers
StudentViewTransformer.collect(block_structure) StudentViewTransformer.collect(block_structure)
......
...@@ -172,6 +172,9 @@ class BlocksView(DeveloperErrorViewMixin, ListAPIView): ...@@ -172,6 +172,9 @@ class BlocksView(DeveloperErrorViewMixin, ListAPIView):
* due: The due date of the block. Returned only if "due" is included * due: The due date of the block. Returned only if "due" is included
in the "requested_fields" parameter. in the "requested_fields" parameter.
* show_correctness: Whether to show scores/correctness to learners for the current sequence or problem.
Returned only if "show_correctness" is included in the "requested_fields" parameter.
""" """
def list(self, request, usage_key_string): # pylint: disable=arguments-differ def list(self, request, usage_key_string): # pylint: disable=arguments-differ
......
...@@ -7,7 +7,7 @@ from logging import getLogger ...@@ -7,7 +7,7 @@ from logging import getLogger
from lms.djangoapps.grades.scores import get_score, possibly_scored from lms.djangoapps.grades.scores import get_score, possibly_scored
from lms.djangoapps.grades.models import BlockRecord, PersistentSubsectionGrade from lms.djangoapps.grades.models import BlockRecord, PersistentSubsectionGrade
from xmodule import block_metadata_utils, graders from xmodule import block_metadata_utils, graders
from xmodule.graders import AggregatedScore from xmodule.graders import AggregatedScore, ShowCorrectness
from ..config.waffle import waffle, WRITE_ONLY_IF_ENGAGED from ..config.waffle import waffle, WRITE_ONLY_IF_ENGAGED
...@@ -27,6 +27,7 @@ class SubsectionGradeBase(object): ...@@ -27,6 +27,7 @@ class SubsectionGradeBase(object):
self.format = getattr(subsection, 'format', '') self.format = getattr(subsection, 'format', '')
self.due = getattr(subsection, 'due', None) self.due = getattr(subsection, 'due', None)
self.graded = getattr(subsection, 'graded', False) self.graded = getattr(subsection, 'graded', False)
self.show_correctness = getattr(subsection, 'show_correctness', '')
self.course_version = getattr(subsection, 'course_version', None) self.course_version = getattr(subsection, 'course_version', None)
self.subtree_edited_timestamp = getattr(subsection, 'subtree_edited_on', None) self.subtree_edited_timestamp = getattr(subsection, 'subtree_edited_on', None)
...@@ -47,6 +48,12 @@ class SubsectionGradeBase(object): ...@@ -47,6 +48,12 @@ class SubsectionGradeBase(object):
) )
return self.all_total.attempted return self.all_total.attempted
def show_grades(self, has_staff_access):
"""
Returns whether subsection scores are currently available to users with or without staff access.
"""
return ShowCorrectness.correctness_available(self.show_correctness, self.due, has_staff_access)
class ZeroSubsectionGrade(SubsectionGradeBase): class ZeroSubsectionGrade(SubsectionGradeBase):
""" """
...@@ -224,7 +231,7 @@ class SubsectionGrade(SubsectionGradeBase): ...@@ -224,7 +231,7 @@ class SubsectionGrade(SubsectionGradeBase):
log_func( log_func(
u"Grades: SG.{}, subsection: {}, course: {}, " u"Grades: SG.{}, subsection: {}, course: {}, "
u"version: {}, edit: {}, user: {}," u"version: {}, edit: {}, user: {},"
u"total: {}/{}, graded: {}/{}".format( u"total: {}/{}, graded: {}/{}, show_correctness: {}".format(
log_statement, log_statement,
self.location, self.location,
self.location.course_key, self.location.course_key,
...@@ -235,5 +242,6 @@ class SubsectionGrade(SubsectionGradeBase): ...@@ -235,5 +242,6 @@ class SubsectionGrade(SubsectionGradeBase):
self.all_total.possible, self.all_total.possible,
self.graded_total.earned, self.graded_total.earned,
self.graded_total.possible, self.graded_total.possible,
self.show_correctness,
) )
) )
""" """
Grades Transformer Grades Transformer
""" """
import json
from base64 import b64encode from base64 import b64encode
from functools import reduce as functools_reduce from functools import reduce as functools_reduce
from hashlib import sha1 from hashlib import sha1
from logging import getLogger from logging import getLogger
import json
from lms.djangoapps.course_blocks.transformers.utils import collect_unioned_set_field, get_field_on_block from lms.djangoapps.course_blocks.transformers.utils import collect_unioned_set_field, get_field_on_block
from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer
...@@ -29,6 +29,7 @@ class GradesTransformer(BlockStructureTransformer): ...@@ -29,6 +29,7 @@ class GradesTransformer(BlockStructureTransformer):
graded: (boolean) graded: (boolean)
has_score: (boolean) has_score: (boolean)
weight: (numeric) weight: (numeric)
show_correctness: (string) when to show grades (one of 'always', 'past_due', 'never')
Additionally, the following value is calculated and stored as a Additionally, the following value is calculated and stored as a
transformer_block_field for each block: transformer_block_field for each block:
...@@ -37,7 +38,16 @@ class GradesTransformer(BlockStructureTransformer): ...@@ -37,7 +38,16 @@ class GradesTransformer(BlockStructureTransformer):
""" """
WRITE_VERSION = 4 WRITE_VERSION = 4
READ_VERSION = 4 READ_VERSION = 4
FIELDS_TO_COLLECT = [u'due', u'format', u'graded', u'has_score', u'weight', u'course_version', u'subtree_edited_on'] FIELDS_TO_COLLECT = [
u'due',
u'format',
u'graded',
u'has_score',
u'weight',
u'course_version',
u'subtree_edited_on',
u'show_correctness',
]
EXPLICIT_GRADED_FIELD_NAME = 'explicit_graded' EXPLICIT_GRADED_FIELD_NAME = 'explicit_graded'
......
...@@ -180,12 +180,30 @@ from django.utils.http import urlquote_plus ...@@ -180,12 +180,30 @@ from django.utils.http import urlquote_plus
%endif %endif
</p> </p>
%if len(section.problem_scores.values()) > 0: %if len(section.problem_scores.values()) > 0:
<dl class="scores"> %if section.show_grades(staff_access):
<dt class="hd hd-6">${ _("Problem Scores: ") if section.graded else _("Practice Scores: ")}</dt> <dl class="scores">
%for score in section.problem_scores.values(): <dt class="hd hd-6">${ _("Problem Scores: ") if section.graded else _("Practice Scores: ")}</dt>
<dd>${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</dd> %for score in section.problem_scores.values():
%endfor <dd>${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</dd>
</dl> %endfor
</dl>
%else:
<p class="hide-scores">
%if section.show_correctness == 'past_due':
%if section.graded:
${_("Problem scores are hidden until the due date.")}
%else:
${_("Practice scores are hidden until the due date.")}
%endif
%else:
%if section.graded:
${_("Problem scores are hidden.")}
%else:
${_("Practice scores are hidden.")}
%endif
%endif
</p>
%endif
%else: %else:
<p class="no-scores">${_("No problem scores in this section")}</p> <p class="no-scores">${_("No problem scores in this section")}</p>
%endif %endif
......
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