From 6219d0a90db49ee6764f5c3a84b9b7c1671a9652 Mon Sep 17 00:00:00 2001 From: Irtaza Akram Date: Fri, 19 Dec 2025 21:10:25 +0500 Subject: [PATCH] chore: move ShowCorrectness from xmodule.graders into XBlock --- CHANGELOG.rst | 8 +++ xblock/__init__.py | 2 +- xblock/scorable.py | 40 ++++++++++++++- xblock/test/test_scorable.py | 94 ++++++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 831846984..2b6a3b0f4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 ------------------ diff --git a/xblock/__init__.py b/xblock/__init__.py index f27076e42..779a96e05 100644 --- a/xblock/__init__.py +++ b/xblock/__init__.py @@ -2,4 +2,4 @@ XBlock Courseware Components """ -__version__ = '5.2.0' +__version__ = '5.3.0' diff --git a/xblock/scorable.py b/xblock/scorable.py index 4ddb34d72..db27602aa 100644 --- a/xblock/scorable.py +++ b/xblock/scorable.py @@ -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__) @@ -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 diff --git a/xblock/test/test_scorable.py b/xblock/test/test_scorable.py index bc3423805..c3803aef3 100644 --- a/xblock/test/test_scorable.py +++ b/xblock/test/test_scorable.py @@ -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 @@ -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 + )