Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ Change history for XBlock
Unreleased
----------

5.3.0 - 2025-12-19
------------------

* Add exceptions NotFoundError and ProcessingError
* Adds fields from xmodule into XBlock
* Adds Progress from xmodule into XBlock
* Adds ShowCorrectness from xmodule.graders into XBlock

5.2.0 - 2025-04-08
------------------

Expand Down
2 changes: 1 addition & 1 deletion xblock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
XBlock Courseware Components
"""

__version__ = '5.2.0'
__version__ = '5.3.0'
40 changes: 39 additions & 1 deletion xblock/scorable.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""
Scorable.
"""
from collections import namedtuple

import logging
from collections import namedtuple
from datetime import datetime

from pytz import UTC

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -112,3 +115,38 @@ def _publish_grade(self, score, only_if_higher=None):
'only_if_higher': only_if_higher,
}
self.runtime.publish(self, 'grade', grade_dict)


class ShowCorrectness:
"""
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"
NEVER_BUT_INCLUDE_GRADE = "never_but_include_grade"

@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 in (cls.NEVER, cls.NEVER_BUT_INCLUDE_GRADE):
return False

if has_staff_access:
# This is after the 'never' check because course staff can see correctness
# unless the sequence/problem explicitly prevents it
return True

if 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
94 changes: 94 additions & 0 deletions xblock/test/test_scorable.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
"""

# pylint: disable=protected-access
from datetime import datetime, timedelta
from unittest import TestCase
from unittest.mock import Mock

import ddt
from pytz import UTC

from xblock import scorable

Expand Down Expand Up @@ -84,3 +86,95 @@ def test_scoring_error(self):
block._scoring_error = True
with self.assertRaises(RuntimeError):
block.rescore(only_if_higher=False)


@ddt.ddt
class ShowCorrectnessTest(TestCase):
"""
Tests the correctness_available method
"""

def setUp(self):
super().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.
"""
assert scorable.ShowCorrectness.correctness_available()

@ddt.data(
(scorable.ShowCorrectness.ALWAYS, True),
(scorable.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.
"""
assert scorable.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.
"""
assert not scorable.ShowCorrectness.correctness_available(
show_correctness=scorable.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)
assert (
scorable.ShowCorrectness.correctness_available(
scorable.ShowCorrectness.PAST_DUE, due_date, has_staff_access
) == expected_result
)

@ddt.data(True, False)
def test_show_correctness_never_but_include_grade(self, has_staff_access):
"""
Test that show_correctness="never_but_include_grade" hides correctness from learners and course staff.
"""
assert not scorable.ShowCorrectness.correctness_available(
show_correctness=scorable.ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, has_staff_access=has_staff_access
)