Skip to content
Open
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
7 changes: 7 additions & 0 deletions cms/templates/js/show-correctness-editor.underscore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
<% } %>
<%- gettext('If the subsection does not have a due date, learners always see their scores when they submit answers to assessments.') %>
</p>
<label class="label">
<input class="input input-radio" name="show-correctness" type="radio" value="never_but_include_grade" aria-describedby="never_show_correctness_but_include_grade_description" />
<%- gettext('Never show individual assessment results, but show overall assessment results after due date') %>
</label>
<p class='field-message' id='never_show_correctness_description'>
<%- gettext('Learners do not see question-level correctness or scores before or after the due date. However, once the due date passes, they can see their overall score for the subsection on the Progress page.') %>
</p>
</div>
</div>
</form>
1 change: 1 addition & 0 deletions lms/djangoapps/course_home_api/progress/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class SubsectionScoresSerializer(ReadOnlySerializer):
assignment_type = serializers.CharField(source='format')
block_key = serializers.SerializerMethodField()
display_name = serializers.CharField()
due = serializers.DateTimeField(allow_null=True)
has_graded_assignment = serializers.BooleanField(source='graded')
override = serializers.SerializerMethodField()
learner_has_access = serializers.SerializerMethodField()
Expand Down
10 changes: 7 additions & 3 deletions lms/djangoapps/course_home_api/progress/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,8 @@ def test_url_hidden_if_subsection_hide_after_due(self):
assert hide_after_due_subsection['url'] is None

@ddt.data(
(True, 0.7), # midterm and final are visible to staff
(False, 0.3), # just the midterm is visible to learners
(True, 0.72), # lab, midterm and final are visible to staff
(False, 0.32), # Only lab and midterm is visible to learners
)
@ddt.unpack
def test_course_grade_considers_subsection_grade_visibility(self, is_staff, expected_percent):
Expand All @@ -301,14 +301,18 @@ def test_course_grade_considers_subsection_grade_visibility(self, is_staff, expe
never = self.add_subsection_with_problem(format='Homework', show_correctness='never')
always = self.add_subsection_with_problem(format='Midterm Exam', show_correctness='always')
past_due = self.add_subsection_with_problem(format='Final Exam', show_correctness='past_due', due=tomorrow)
never_but_show_grade = self.add_subsection_with_problem(
format='Lab', show_correctness='never_but_include_grade'
)

answer_problem(self.course, get_mock_request(self.user), never)
answer_problem(self.course, get_mock_request(self.user), always)
answer_problem(self.course, get_mock_request(self.user), past_due)
answer_problem(self.course, get_mock_request(self.user), never_but_show_grade)

# First, confirm the grade in the database - it should never change based on user state.
# This is midterm and final and a single problem added together.
assert CourseGradeFactory().read(self.user, self.course).percent == 0.72
assert CourseGradeFactory().read(self.user, self.course).percent == 0.73

response = self.client.get(self.url)
assert response.status_code == 200
Expand Down
1 change: 1 addition & 0 deletions lms/djangoapps/course_home_api/progress/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class ProgressTabView(RetrieveAPIView):
assignment_type: (str) the format, if any, of the Subsection (Homework, Exam, etc)
block_key: (str) the key of the given subsection block
display_name: (str) a str of what the name of the Subsection is for displaying on the site
due: (str or None) the due date of the subsection in ISO 8601 format, or None if no due date is set
has_graded_assignment: (bool) whether or not the Subsection is a graded assignment
learner_has_access: (bool) whether the learner has access to the subsection (could be FBE gated)
num_points_earned: (int) the amount of points the user has earned for the given subsection
Expand Down
27 changes: 26 additions & 1 deletion lms/djangoapps/courseware/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1781,6 +1781,14 @@ def assert_progress_page_show_grades(self, response, show_correctness, due_date,
(ShowCorrectness.PAST_DUE, TODAY, True),
(ShowCorrectness.PAST_DUE, TOMORROW, False),
(ShowCorrectness.PAST_DUE, TOMORROW, True),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, None, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, None, True),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, YESTERDAY, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, YESTERDAY, True),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, TODAY, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, TODAY, True),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, TOMORROW, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, TOMORROW, True),
)
@ddt.unpack
def test_progress_page_no_problem_scores(self, show_correctness, due_date_name, graded):
Expand Down Expand Up @@ -1821,6 +1829,14 @@ def test_progress_page_no_problem_scores(self, show_correctness, due_date_name,
(ShowCorrectness.PAST_DUE, TODAY, True, True),
(ShowCorrectness.PAST_DUE, TOMORROW, False, False),
(ShowCorrectness.PAST_DUE, TOMORROW, True, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, None, False, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, None, True, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, YESTERDAY, False, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, YESTERDAY, True, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, TODAY, False, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, TODAY, True, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, TOMORROW, False, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, TOMORROW, True, False),
)
@ddt.unpack
def test_progress_page_hide_scores_from_learner(self, show_correctness, due_date_name, graded, show_grades):
Expand Down Expand Up @@ -1873,11 +1889,20 @@ def test_progress_page_hide_scores_from_learner(self, show_correctness, due_date
(ShowCorrectness.PAST_DUE, TODAY, True, True),
(ShowCorrectness.PAST_DUE, TOMORROW, False, True),
(ShowCorrectness.PAST_DUE, TOMORROW, True, True),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, None, False, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, None, True, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, YESTERDAY, False, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, YESTERDAY, True, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, TODAY, False, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, TODAY, True, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, TOMORROW, False, False),
(ShowCorrectness.NEVER_BUT_INCLUDE_GRADE, TOMORROW, True, False),
)
@ddt.unpack
def test_progress_page_hide_scores_from_staff(self, show_correctness, due_date_name, graded, show_grades):
"""
Test that problem scores are hidden from staff viewing a learner's progress page only if show_correctness=never.
Test that problem scores are hidden from staff viewing a learner's progress page only if show_correctness is
never or never_but_include_grade.
"""
due_date = self.DATES[due_date_name]
self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded)
Expand Down
8 changes: 7 additions & 1 deletion lms/djangoapps/grades/subsection_grade.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

from abc import ABCMeta
from collections import OrderedDict
from datetime import datetime
from logging import getLogger

from pytz import UTC
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the standard library's datetime.timezone.utc instead of using the pytz package.

from lazy import lazy

from lms.djangoapps.grades.models import BlockRecord, PersistentSubsectionGrade
Expand Down Expand Up @@ -59,6 +60,11 @@ def show_grades(self, has_staff_access):
"""
Returns whether subsection scores are currently available to users with or without staff access.
"""
if self.show_correctness == ShowCorrectness.NEVER_BUT_INCLUDE_GRADE:
# For NEVER_BUT_INCLUDE_GRADE, show_grades returns True if the due date has passed,
# but correctness_available returns False.
return (self.due is None or
self.due < datetime.now(UTC))
return ShowCorrectness.correctness_available(self.show_correctness, self.due, has_staff_access)

@property
Expand Down
8 changes: 4 additions & 4 deletions lms/templates/courseware/progress.html
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ <h3 class="hd hd-3" id="chapter_${loop.index}">${ chapter['display_name']}</h3>
<h4 class="hd hd-4">
%if hide_url:
<p class="d-inline">${section.display_name}
%if (total > 0 or earned > 0) and section.show_grades(staff_access):
%if (total > 0 or earned > 0) and section.show_grades(staff_access) and not section.show_correctness == 'never_but_include_grade':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, we want to keep logic this specific out of the template. Is there a reason it can't be incorporated into the show_grades() logic?

<span class="sr">
${_("{earned} of {total} possible points").format(earned='{:.3n}'.format(float(earned)), total='{:.3n}'.format(float(total)))}
</span>
Expand All @@ -189,14 +189,14 @@ <h4 class="hd hd-4">
%else:
<a href="${reverse('courseware_subsection', kwargs=dict(course_id=str(course.id), section=chapter['url_name'], subsection=section.url_name))}">
${ section.display_name}
%if (total > 0 or earned > 0) and section.show_grades(staff_access):
%if (total > 0 or earned > 0) and section.show_grades(staff_access) and not section.show_correctness == 'never_but_include_grade':
<span class="sr">
${_("{earned} of {total} possible points").format(earned='{:.3n}'.format(float(earned)), total='{:.3n}'.format(float(total)))}
</span>
%endif
</a>
%endif
%if (total > 0 or earned > 0) and section.show_grades(staff_access):
%if (total > 0 or earned > 0) and section.show_grades(staff_access) and not section.show_correctness == 'never_but_include_grade':
<span> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</span>
%endif
</h4>
Expand All @@ -219,7 +219,7 @@ <h4 class="hd hd-4">
%endif
</p>
%if len(section.problem_scores.values()) > 0:
%if section.show_grades(staff_access):
%if section.show_grades(staff_access) and not section.show_correctness == 'never_but_include_grade':
<dl class="scores">
<dt class="hd hd-6">${ _("Problem Scores: ") if section.graded else _("Practice Scores: ")}</dt>
%for score in section.problem_scores.values():
Expand Down
3 changes: 2 additions & 1 deletion xmodule/graders.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,13 +485,14 @@ class ShowCorrectness:
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 == cls.NEVER:
if show_correctness in (cls.NEVER, cls.NEVER_BUT_INCLUDE_GRADE):
return False
elif has_staff_access:
# This is after the 'never' check because course staff can see correctness
Expand Down
8 changes: 8 additions & 0 deletions xmodule/tests/test_graders.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,3 +493,11 @@ def test_show_correctness_past_due(self, due_date_str, has_staff_access, expecte
due_date = getattr(self, due_date_str)
assert ShowCorrectness.correctness_available(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 ShowCorrectness.correctness_available(show_correctness=ShowCorrectness.NEVER_BUT_INCLUDE_GRADE,
has_staff_access=has_staff_access)
Loading