diff --git a/lms/djangoapps/course_home_api/dates/v1/serializers.py b/lms/djangoapps/course_home_api/dates/v1/serializers.py index d8162527f9..0f805d87bf 100644 --- a/lms/djangoapps/course_home_api/dates/v1/serializers.py +++ b/lms/djangoapps/course_home_api/dates/v1/serializers.py @@ -22,6 +22,7 @@ class DateSummarySerializer(serializers.Serializer): link = serializers.SerializerMethodField() link_text = serializers.CharField() title = serializers.CharField() + extra_info = serializers.CharField() def get_learner_has_access(self, block): learner_is_full_access = self.context.get('learner_is_full_access', False) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 91eea2ba15..b1493f0842 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -10,6 +10,7 @@ from datetime import datetime import pytz import six from crum import get_current_request +from dateutil.parser import parse as parse_date from django.conf import settings from django.http import Http404, QueryDict from django.urls import reverse @@ -71,7 +72,7 @@ log = logging.getLogger(__name__) # Used by get_course_assignments below. You shouldn't need to use this type directly. _Assignment = namedtuple( 'Assignment', ['block_key', 'title', 'url', 'date', 'contains_gated_content', 'complete', 'past_due', - 'assignment_type'] + 'assignment_type', 'extra_info'] ) @@ -508,6 +509,7 @@ def get_course_assignment_date_blocks(course, user, request, num_return=None, date_block.past_due = assignment.past_due date_block.link = request.build_absolute_uri(assignment.url) if assignment.url else '' date_block.set_title(assignment.title, link=assignment.url) + date_block._extra_info = assignment.extra_info # pylint: disable=protected-access date_blocks.append(date_block) date_blocks = sorted((b for b in date_blocks if b.is_enabled or include_past_dates), key=date_block_key_fn) if num_return: @@ -533,26 +535,92 @@ def get_course_assignments(course_key, user, include_access=False): for subsection_key in block_data.get_children(section_key): due = block_data.get_xblock_field(subsection_key, 'due') graded = block_data.get_xblock_field(subsection_key, 'graded', False) - if not due or not graded: - continue + if due and graded: + contains_gated_content = include_access and block_data.get_xblock_field( + subsection_key, 'contains_gated_content', False) + title = block_data.get_xblock_field(subsection_key, 'display_name', _('Assignment')) - contains_gated_content = include_access and block_data.get_xblock_field( - subsection_key, 'contains_gated_content', False) - title = block_data.get_xblock_field(subsection_key, 'display_name', _('Assignment')) + assignment_type = block_data.get_xblock_field(subsection_key, 'format', None) - assignment_type = block_data.get_xblock_field(subsection_key, 'format', None) + url = None + start = block_data.get_xblock_field(subsection_key, 'start') + assignment_released = not start or start < now + if assignment_released: + url = reverse('jump_to', args=[course_key, subsection_key]) - url = None - start = block_data.get_xblock_field(subsection_key, 'start') - assignment_released = not start or start < now - if assignment_released: - url = reverse('jump_to', args=[course_key, subsection_key]) + 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, None + )) - 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 - )) + # Load all dates for ORA blocks as separate assignments + descendents = block_data.get_children(subsection_key) + while descendents: + descendent = descendents.pop() + descendents.extend(block_data.get_children(descendent)) + if block_data.get_xblock_field(descendent, 'category', None) == 'openassessment': + graded = block_data.get_xblock_field(descendent, 'graded', False) + has_score = block_data.get_xblock_field(descendent, 'has_score', False) + weight = block_data.get_xblock_field(descendent, 'weight', 1) + if not (graded and has_score and (weight is None or weight > 0)): + continue + + all_assessments = [{ + 'name': 'submission', + 'due': block_data.get_xblock_field(descendent, 'submission_due'), + 'start': block_data.get_xblock_field(descendent, 'submission_start'), + 'required': True + }] + valid_assessments = block_data.get_xblock_field(descendent, 'valid_assessments') + print(valid_assessments) + + if valid_assessments: + all_assessments.extend(valid_assessments) + + assignment_type = block_data.get_xblock_field(descendent, 'format', None) + complete = is_block_structure_complete_for_assignments(block_data, descendent) + + block_title = block_data.get_xblock_field(descendent, 'title', _('Open Response Assessment')) + + for assessment in all_assessments: + due = parse_date(assessment.get('due')).replace(tzinfo=pytz.UTC) if assessment.get('due') else None + if due is None: + continue + + assessment_name = assessment.get('name') + if assessment_name is None: + continue + + if assessment_name == 'self-assessment': + assessment_type = _("Self Assessment") + elif assessment_name == 'peer-assessment': + assessment_type = _("Peer Assessment") + elif assessment_name == 'staff-assessment': + assessment_type = _("Staff Assessment") + elif assessment_name == 'submission': + assessment_type = _("Submission") + else: + assessment_type = assessment_name + title = "{} ({})".format(block_title, assessment_type) + url = '' + start = parse_date(assessment.get('start')).replace(tzinfo=pytz.UTC) if assessment.get('start') else None + assignment_released = not start or start < now + if assignment_released: + url = reverse('jump_to', args=[course_key, descendent]) + + past_due = not complete and due and due < now + assignments.append(_Assignment( + descendent, + title, + url, + due, + False, + complete, + past_due, + assignment_type, + _("Open Response Assessment due dates are set by your instructor and can't be shifted.") + )) return assignments diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index c63a457c51..6d058a3bc9 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -77,6 +77,11 @@ class DateSummary(object): """The detail text displayed by this summary.""" return '' + @property + def extra_info(self): + """Extra detail to display as a tooltip.""" + return None + def register_alerts(self, request, course): """ Registers any relevant course alerts given the current request. @@ -388,6 +393,7 @@ class CourseAssignmentDate(DateSummary): self.contains_gated_content = False self.complete = None self.past_due = None + self._extra_info = None @property def date(self): @@ -405,6 +411,10 @@ class CourseAssignmentDate(DateSummary): def link(self): return self.assignment_link + @property + def extra_info(self): + return self._extra_info + @link.setter def link(self, link): self.assignment_link = link diff --git a/lms/djangoapps/courseware/tests/test_date_summary.py b/lms/djangoapps/courseware/tests/test_date_summary.py index 36e821a739..0e7a3b2db4 100644 --- a/lms/djangoapps/courseware/tests/test_date_summary.py +++ b/lms/djangoapps/courseware/tests/test_date_summary.py @@ -298,6 +298,72 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): for html_tag in assignment_title_html: self.assertIn(html_tag, assignment_title) + @RELATIVE_DATES_FLAG.override(active=True) + @ddt.data( + ([], 3), + ([{ + 'due': None, + 'start': None, + 'name': 'student-training', + 'examples': [ + { + 'answer': ['Replace this text with your own sample response...'], + 'options_selected': [ + {'option': 'Fair', 'criterion': 'Ideas'}, + {'option': 'Good', 'criterion': 'Content'} + ] + }, { + 'answer': ['Replace this text with another sample response...'], + 'options_selected': [ + {'option': 'Poor', 'criterion': 'Ideas'}, + {'option': 'Good', 'criterion': 'Content'} + ] + } + ] + }, { + 'due': '2029-01-01T00:00:00+00:00', + 'start': '2001-01-01T00:00:00+00:00', + 'must_be_graded_by': 3, + 'name': 'peer-assessment', + 'must_grade': 5 + }, { + 'due': '2029-01-01T00:00:00+00:00', + 'start': '2001-01-01T00:00:00+00:00', + 'name': 'self-assessment' + }], 5) + ) + @ddt.unpack + def test_dates_with_openassessments(self, rubric_assessments, date_block_count): + course = create_self_paced_course_run(days_till_start=-1, org_id='TestOrg') + + user = create_user() + request = self.make_request(user) + CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) + now = datetime.now(utc) + + chapter = ItemFactory.create( + parent=course, + category="chapter", + graded=True, + ) + section = ItemFactory.create( + parent=chapter, + category="sequential", + ) + vertical = ItemFactory.create( + parent=section, + category="vertical", + ) + ItemFactory.create( + parent=vertical, + category="openassessment", + rubric_assessments=rubric_assessments, + submission_start=(now + timedelta(days=1)).isoformat(), + submission_end=(now + timedelta(days=7)).isoformat(), + ) + blocks = get_course_date_blocks(course, user, request, include_past_dates=True) + self.assertEqual(len(blocks), date_block_count) + @RELATIVE_DATES_FLAG.override(active=True) def test_enabled_block_types_with_expired_course(self): course = create_course_run(days_till_start=-100) diff --git a/lms/djangoapps/courseware/transformers.py b/lms/djangoapps/courseware/transformers.py new file mode 100644 index 0000000000..d93c2a00fc --- /dev/null +++ b/lms/djangoapps/courseware/transformers.py @@ -0,0 +1,45 @@ +""" +Courseware BlockTransformer implementations +""" + +from openedx.core.djangoapps.content.block_structure.transformer import ( + BlockStructureTransformer, + FilteringTransformerMixin, +) + + +class OpenAssessmentDateTransformer(FilteringTransformerMixin, BlockStructureTransformer): + """ + BlockTransformer to collect all fields related to dates for openassessment problems. + """ + WRITE_VERSION = 1 + READ_VERSION = 1 + + @classmethod + def name(cls): + """ + Unique identifier for the transformer's class; + same identifier used in setup.py. + """ + return "content_type_gate" + + @classmethod + def collect(cls, block_structure): + """ + Collects any information that's necessary to execute this + transformer's transform method. + """ + block_structure.request_xblock_fields( + 'valid_assessments', + 'submission_start', + 'submission_due', + 'title', + 'graded', + 'format', + 'has_score', + ) + + def transform_block_filters(self, usage_info, block_structure): + # This Transformer exists only to collect fields needed by other code, so it + # doesn't transform the tree. + return block_structure.create_universal_filter() diff --git a/lms/static/sass/course/_dates.scss b/lms/static/sass/course/_dates.scss index 0a9de0ab27..5a69f91acc 100644 --- a/lms/static/sass/course/_dates.scss +++ b/lms/static/sass/course/_dates.scss @@ -99,10 +99,7 @@ } .no-access { - // This is too low-contrast for a11y purposes. But since it only applies to pieces of the page that are - // inaccessible to users, and we have a banner explaining the parts that are inaccessible at the top, - // we're OK from an accessibility point of view. - color: #d1d2d4; + color: #767676; } .timeline-date-content { @@ -115,10 +112,14 @@ align-items: center; &.not-released { - color: #767676; + color: #b1a3a3; } } + .timeline-date { + color: #2d323e; + } + .timeline-title { @include font-size(14); @@ -134,7 +135,7 @@ } &.not-released { - color: #d1d2d4; + color: #767676; } } @@ -149,8 +150,30 @@ color: #2d323e; text-decoration: underline; } + + &.not-released { + color: #767676; + } } + .timeline-extra-info { + @include font-size(14); + + display: flex; + flex: 100%; + line-height: 1.25; + + a { + color: #2d323e; + text-decoration: underline; + } + + &.not-released { + color: #767676; + } + } + + .pill { @include font-size(12); diff --git a/lms/templates/courseware/dates.html b/lms/templates/courseware/dates.html index 37fd5d78ea..ff76a25197 100644 --- a/lms/templates/courseware/dates.html +++ b/lms/templates/courseware/dates.html @@ -82,6 +82,11 @@ from openedx.core.djangolib.markup import HTML, Text
${course_date.description}
% endif + % if course_date.extra_info: +