AA-225: Only consider scored items for past due assignments
When considering if an assignment is past due for the dates tab, only look at the scored and graded units in the subsection (i.e. ignore reading and video units). This still leaves the "complete" field alone -- i.e. those subsections will still be left incomplete generally. But for assignment-focused tasks, they will instead be considered complete.
This commit is contained in:
@@ -60,6 +60,9 @@ class StubCompletionService(object):
|
||||
"""
|
||||
return {candidate: self._completion_value for candidate in candidates}
|
||||
|
||||
def get_completable_children(self, node):
|
||||
return node.get_children()
|
||||
|
||||
def get_complete_on_view_delay_ms(self):
|
||||
"""
|
||||
Return the completion-by-viewing delay in milliseconds.
|
||||
@@ -72,15 +75,15 @@ class StubCompletionService(object):
|
||||
def vertical_is_complete(self, item):
|
||||
if item.scope_ids.block_type != 'vertical':
|
||||
raise ValueError('The passed in xblock is not a vertical type!')
|
||||
return self._completion_value
|
||||
return self._completion_value == 1 if self._enabled else None
|
||||
|
||||
|
||||
class BaseVerticalBlockTest(XModuleXmlImportTest):
|
||||
"""
|
||||
Tests for the BaseVerticalBlock.
|
||||
"""
|
||||
test_html_1 = 'Test HTML 1'
|
||||
test_html_2 = 'Test HTML 2'
|
||||
test_html = 'Test HTML'
|
||||
test_problem = 'Test Problem'
|
||||
|
||||
def setUp(self):
|
||||
super(BaseVerticalBlockTest, self).setUp()
|
||||
@@ -90,8 +93,8 @@ class BaseVerticalBlockTest(XModuleXmlImportTest):
|
||||
vertical = xml.VerticalFactory.build(parent=sequence)
|
||||
|
||||
self.course = self.process_xml(course)
|
||||
xml.HtmlFactory(parent=vertical, url_name='test-html-1', text=self.test_html_1)
|
||||
xml.HtmlFactory(parent=vertical, url_name='test-html-2', text=self.test_html_2)
|
||||
xml.HtmlFactory(parent=vertical, url_name='test-html', text=self.test_html)
|
||||
xml.ProblemFactory(parent=vertical, url_name='test-problem', text=self.test_problem)
|
||||
|
||||
self.course = self.process_xml(course)
|
||||
course_seq = self.course.get_children()[0]
|
||||
@@ -103,8 +106,10 @@ class BaseVerticalBlockTest(XModuleXmlImportTest):
|
||||
self.vertical = course_seq.get_children()[0]
|
||||
self.vertical.xmodule_runtime = self.module_system
|
||||
|
||||
self.html1block = self.vertical.get_children()[0]
|
||||
self.html2block = self.vertical.get_children()[1]
|
||||
self.html_block = self.vertical.get_children()[0]
|
||||
self.problem_block = self.vertical.get_children()[1]
|
||||
self.problem_block.has_score = True
|
||||
self.problem_block.graded = True
|
||||
|
||||
self.username = "bilbo"
|
||||
self.default_context = {"bookmarked": False, "username": self.username}
|
||||
@@ -151,8 +156,11 @@ class VerticalBlockTestCase(BaseVerticalBlockTest):
|
||||
html = self.module_system.render(
|
||||
self.vertical, view, self.default_context if context is None else context
|
||||
).content
|
||||
self.assertIn(self.test_html_1, html)
|
||||
self.assertIn(self.test_html_2, html)
|
||||
self.assertIn(self.test_html, html)
|
||||
if view == STUDENT_VIEW:
|
||||
self.assertIn(self.test_problem, html)
|
||||
else:
|
||||
self.assertNotIn(self.test_problem, html)
|
||||
self.assertIn("'due': datetime.datetime({year}, {month}, {day}".format(
|
||||
year=self.vertical.due.year, month=self.vertical.due.month, day=self.vertical.due.day), html)
|
||||
if view == STUDENT_VIEW:
|
||||
@@ -161,9 +169,30 @@ class VerticalBlockTestCase(BaseVerticalBlockTest):
|
||||
self.assert_bookmark_info(self.assertNotIn, html)
|
||||
if context:
|
||||
self.assertIn("'subsection_format': '{}'".format(context['format']), html)
|
||||
self.assertIn("'completed': {}".format(completion_value), html)
|
||||
self.assertIn("'completed': {}".format(completion_value == 1), html)
|
||||
self.assertIn("'past_due': {}".format(self.vertical.due < now), html)
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_render_problem_without_score(self, has_score):
|
||||
"""
|
||||
Test the rendering of the student and public view.
|
||||
"""
|
||||
self.module_system._services['bookmarks'] = Mock()
|
||||
self.module_system._services['user'] = StubUserService()
|
||||
self.module_system._services['completion'] = StubCompletionService(enabled=True, completion_value=0)
|
||||
|
||||
now = datetime.now(pytz.UTC)
|
||||
self.vertical.due = now + timedelta(days=-1)
|
||||
self.problem_block.has_score = has_score
|
||||
|
||||
html = self.module_system.render(self.vertical, STUDENT_VIEW, self.default_context).content
|
||||
if has_score:
|
||||
self.assertIn("'completed': False", html)
|
||||
self.assertIn("'past_due': True", html)
|
||||
else:
|
||||
self.assertIn("'completed': None", html)
|
||||
self.assertIn("'past_due': False", html)
|
||||
|
||||
@ddt.unpack
|
||||
@ddt.data(
|
||||
(True, 0.9, True),
|
||||
@@ -176,7 +205,7 @@ class VerticalBlockTestCase(BaseVerticalBlockTest):
|
||||
"""
|
||||
Test that mark-completed-on-view-after-delay is only set for relevant child Xblocks.
|
||||
"""
|
||||
with patch.object(self.html1block, 'render') as mock_student_view:
|
||||
with patch.object(self.html_block, 'render') as mock_student_view:
|
||||
self.module_system._services['completion'] = StubCompletionService(
|
||||
enabled=completion_enabled,
|
||||
completion_value=completion_value,
|
||||
@@ -198,8 +227,8 @@ class VerticalBlockTestCase(BaseVerticalBlockTest):
|
||||
'is_unit_page': True
|
||||
}
|
||||
html = self.module_system.render(self.vertical, AUTHOR_VIEW, context).content
|
||||
self.assertNotIn(self.test_html_1, html)
|
||||
self.assertNotIn(self.test_html_2, html)
|
||||
self.assertNotIn(self.test_html, html)
|
||||
self.assertNotIn(self.test_problem, html)
|
||||
|
||||
# Vertical should render reorderable children on the container page
|
||||
reorderable_items = set()
|
||||
@@ -208,5 +237,5 @@ class VerticalBlockTestCase(BaseVerticalBlockTest):
|
||||
'reorderable_items': reorderable_items,
|
||||
}
|
||||
html = self.module_system.render(self.vertical, AUTHOR_VIEW, context).content
|
||||
self.assertIn(self.test_html_1, html)
|
||||
self.assertIn(self.test_html_2, html)
|
||||
self.assertIn(self.test_html, html)
|
||||
self.assertIn(self.test_problem, html)
|
||||
|
||||
@@ -91,8 +91,8 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse
|
||||
'content': rendered_child.content
|
||||
})
|
||||
|
||||
completed = completion_service and completion_service.vertical_is_complete(self)
|
||||
past_due = not completed and self.due and self.due < datetime.now(pytz.UTC)
|
||||
completed = self.is_block_complete_for_assignments(completion_service)
|
||||
past_due = completed is False and self.due and self.due < datetime.now(pytz.UTC)
|
||||
fragment_context = {
|
||||
'items': contents,
|
||||
'xblock_context': context,
|
||||
@@ -224,3 +224,40 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse
|
||||
xblock_body["content_type"] = "Sequence"
|
||||
|
||||
return xblock_body
|
||||
|
||||
# So far, we only need this here. Move it somewhere more sensible if other bits of code want it too.
|
||||
def is_block_complete_for_assignments(self, completion_service):
|
||||
"""
|
||||
Considers a block complete only if all scored & graded leaf blocks are complete.
|
||||
|
||||
This is different from the normal `complete` flag because children of the block that are informative (like
|
||||
readings or videos) do not count. We only care about actual homework content.
|
||||
|
||||
Compare with is_block_structure_complete_for_assignments in course_experience/utils.py, which does the same
|
||||
calculation, but for a BlockStructure node and its children.
|
||||
|
||||
Returns:
|
||||
True if complete
|
||||
False if not
|
||||
None if no assignments present or no completion info present (don't show any past-due or complete info)
|
||||
"""
|
||||
if not completion_service or not completion_service.completion_tracking_enabled():
|
||||
return None
|
||||
|
||||
children = completion_service.get_completable_children(self)
|
||||
children_locations = [child.scope_ids.usage_id for child in children]
|
||||
completions = completion_service.get_completions(children_locations)
|
||||
|
||||
all_complete = None
|
||||
for child in children:
|
||||
complete = completions[child.scope_ids.usage_id] == 1
|
||||
graded = getattr(child, 'graded', False)
|
||||
has_score = getattr(child, 'has_score', False)
|
||||
weight = getattr(child, 'weight', 1)
|
||||
scored = has_score and (weight is None or weight > 0)
|
||||
if graded and scored:
|
||||
if not complete:
|
||||
return False
|
||||
all_complete = True
|
||||
|
||||
return all_complete
|
||||
|
||||
@@ -3,7 +3,6 @@ Functions for accessing and displaying courses within the
|
||||
courseware.
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
from collections import defaultdict, namedtuple
|
||||
from datetime import datetime
|
||||
@@ -12,7 +11,6 @@ import pytz
|
||||
import six
|
||||
from crum import get_current_request
|
||||
from django.conf import settings
|
||||
from django.db.models import Prefetch
|
||||
from django.http import Http404, QueryDict
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
@@ -25,7 +23,6 @@ from six import text_type
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
|
||||
import branding
|
||||
from course_modes.models import CourseMode
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
from lms.djangoapps.courseware.access_response import (
|
||||
AuthenticationRequiredAccessError,
|
||||
@@ -60,6 +57,7 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_
|
||||
from openedx.core.lib.api.view_utils import LazySequence
|
||||
from openedx.features.course_duration_limits.access import AuditExpiredError
|
||||
from openedx.features.course_experience import RELATIVE_DATES_FLAG
|
||||
from openedx.features.course_experience.utils import is_block_structure_complete_for_assignments
|
||||
from static_replace import replace_static_urls
|
||||
from survey.utils import SurveyRequiredAccessError, check_survey_required_and_unanswered
|
||||
from util.date_utils import strftime_localized
|
||||
@@ -550,7 +548,7 @@ def get_course_assignments(course_key, user, include_access=False):
|
||||
if assignment_released:
|
||||
url = reverse('jump_to', args=[course_key, subsection_key])
|
||||
|
||||
complete = block_data.get_xblock_field(subsection_key, 'complete', False)
|
||||
complete = is_block_structure_complete_for_assignments(block_data, subsection_key)
|
||||
past_due = not complete and due < now
|
||||
assignments.append(_Assignment(
|
||||
subsection_key, title, url, due, contains_gated_content, complete, past_due, assignment_type
|
||||
|
||||
@@ -11,6 +11,8 @@ import ddt
|
||||
import mock
|
||||
import pytz
|
||||
import six
|
||||
from completion.models import BlockCompletion
|
||||
from completion.test_utils import CompletionWaffleTestMixin
|
||||
from crum import set_current_request
|
||||
from django.conf import settings
|
||||
from django.test.client import RequestFactory
|
||||
@@ -25,6 +27,7 @@ from lms.djangoapps.courseware.courses import (
|
||||
get_cms_block_link,
|
||||
get_cms_course_link,
|
||||
get_course_about_section,
|
||||
get_course_assignments,
|
||||
get_course_by_id,
|
||||
get_course_chapter_ids,
|
||||
get_course_info_section,
|
||||
@@ -454,3 +457,26 @@ class TestGetCourseChapters(ModuleStoreTestCase):
|
||||
course_chapter_ids,
|
||||
[six.text_type(child) for child in course.children]
|
||||
)
|
||||
|
||||
|
||||
class TestGetCourseAssignments(CompletionWaffleTestMixin, ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the `get_course_assignments` function.
|
||||
"""
|
||||
|
||||
def test_completion_ignores_non_scored_items(self):
|
||||
"""
|
||||
Test that we treat a sequential with incomplete (but not scored) items (like a video maybe) as complete.
|
||||
"""
|
||||
course = CourseFactory()
|
||||
chapter = ItemFactory(parent=course, category='chapter', graded=True, due=datetime.datetime.now())
|
||||
sequential = ItemFactory(parent=chapter, category='sequential')
|
||||
problem = ItemFactory(parent=sequential, category='problem', has_score=True)
|
||||
ItemFactory(parent=sequential, category='video', has_score=False)
|
||||
|
||||
self.override_waffle_switch(True)
|
||||
BlockCompletion.objects.submit_completion(self.user, problem.location, 1)
|
||||
|
||||
assignments = get_course_assignments(course.location.context_key, self.user, None)
|
||||
self.assertEqual(len(assignments), 1)
|
||||
self.assertTrue(assignments[0].complete)
|
||||
|
||||
@@ -3146,7 +3146,7 @@ class DatesTabTestCase(ModuleStoreTestCase):
|
||||
format='Homework',
|
||||
)
|
||||
vertical = ItemFactory.create(category='vertical', parent_location=subsection.location)
|
||||
ItemFactory.create(category='problem', parent_location=vertical.location)
|
||||
ItemFactory.create(category='problem', parent_location=vertical.location, has_score=True)
|
||||
|
||||
with patch('lms.djangoapps.courseware.views.views.get_enrollment') as mock_get_enrollment:
|
||||
mock_get_enrollment.return_value = {
|
||||
|
||||
@@ -299,14 +299,30 @@ def dates_banner_should_display(course_key, user):
|
||||
for section_key in block_data.get_children(course_usage_key):
|
||||
for subsection_key in block_data.get_children(section_key):
|
||||
subsection_due_date = block_data.get_xblock_field(subsection_key, 'due', None)
|
||||
if subsection_due_date and (
|
||||
not block_data.get_xblock_field(subsection_key, 'complete', False)
|
||||
and block_data.get_xblock_field(subsection_key, 'graded', False)
|
||||
and subsection_due_date < timezone.now()
|
||||
):
|
||||
# Display the banner if the due date for an incomplete graded subsection
|
||||
# has passed
|
||||
if (subsection_due_date and subsection_due_date < timezone.now() and
|
||||
not is_block_structure_complete_for_assignments(block_data, subsection_key)):
|
||||
# Display the banner if the due date for an incomplete graded subsection has passed
|
||||
return True, block_data.get_xblock_field(subsection_key, 'contains_gated_content', False)
|
||||
|
||||
# Don't display the banner if there were no missed deadlines
|
||||
return False, False
|
||||
|
||||
|
||||
def is_block_structure_complete_for_assignments(block_data, block_key):
|
||||
"""
|
||||
Considers a block complete only if all scored & graded leaf blocks are complete.
|
||||
|
||||
This is different from the normal `complete` flag because children of the block that are informative (like
|
||||
readings or videos) do not count. We only care about actual homework content.
|
||||
"""
|
||||
children = block_data.get_children(block_key)
|
||||
if children:
|
||||
return all(is_block_structure_complete_for_assignments(block_data, child_key) for child_key in children)
|
||||
|
||||
complete = block_data.get_xblock_field(block_key, 'complete', False)
|
||||
graded = block_data.get_xblock_field(block_key, 'graded', False)
|
||||
has_score = block_data.get_xblock_field(block_key, 'has_score', False)
|
||||
weight = block_data.get_xblock_field(block_key, 'weight', 1)
|
||||
scored = has_score and (weight is None or weight > 0)
|
||||
|
||||
return complete or not graded or not scored
|
||||
|
||||
Reference in New Issue
Block a user