From d6bf54b576f53f44d2d253ae4539fac7422e2748 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 8 Jul 2020 11:55:51 -0400 Subject: [PATCH] Add ORA2 dates to the dates page ORA2 (openassessment) problems have multiple dates associated with are not bound to the `due` date that is modified by Personalized Learner Schedules. We expose the ORA2 dates separately in the dates page so that learners aren't surprised by the differing deadlines. [AA-223] --- .../course_home_api/dates/v1/serializers.py | 1 + lms/djangoapps/courseware/courses.py | 66 ++++++++++++++++++- lms/djangoapps/courseware/date_summary.py | 10 +++ lms/static/sass/course/_dates.scss | 13 ++++ lms/templates/courseware/dates.html | 5 ++ .../features/calendar_sync/tests/test_ics.py | 14 +++- .../content_type_gating/block_transformers.py | 7 +- .../course_experience/dates-summary.html | 3 + setup.py | 1 + 9 files changed, 113 insertions(+), 7 deletions(-) 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..23b8cfbe05 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 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: @@ -551,9 +553,69 @@ def get_course_assignments(course_key, user, include_access=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 + subsection_key, title, url, due, contains_gated_content, complete, past_due, assignment_type, None )) + # 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 + + valid_assessments = [{ + 'name': 'submission', + 'due': block_data.get_xblock_field(descendent, 'submission_due'), + 'start': block_data.get_xblock_field(descendent, 'submission_start'), + 'required': True + }] + block_data.get_xblock_field(descendent, 'valid_assessments') + gated = include_access and block_data.get_xblock_field(descendent, 'gated', False) + 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 valid_assessments: + due = parse_date(assessment['due']).replace(tzinfo=pytz.UTC) if assessment['due'] else None + if due 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['start']).replace(tzinfo=pytz.UTC) if assessment['start'] else None + assignment_released = not start or start < now + if assignment_released: + url = reverse('jump_to', args=[course_key, descendent]) + url = request and request.build_absolute_uri(url) + + past_due = not complete and due and due < now + assignments.append(_Assignment( + descendent, + title, + url, + due, + contains_gated_content, + 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/static/sass/course/_dates.scss b/lms/static/sass/course/_dates.scss index 0a9de0ab27..d4d5c895e1 100644 --- a/lms/static/sass/course/_dates.scss +++ b/lms/static/sass/course/_dates.scss @@ -151,6 +151,19 @@ } } + .timeline-extra-info { + @include font-size(14); + + display: flex; + flex: 100%; + line-height: 1.25; + + &.not-released { + color: #d1d2d4; + } + } + + .pill { @include font-size(12); diff --git a/lms/templates/courseware/dates.html b/lms/templates/courseware/dates.html index f140089522..78edf84a4a 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
${block.description}
+ % if block.extra_info: +
+ ${block.extra_info} +
+ % endif % endif diff --git a/openedx/features/calendar_sync/tests/test_ics.py b/openedx/features/calendar_sync/tests/test_ics.py index 8e053134d1..fd4ff6c1e2 100644 --- a/openedx/features/calendar_sync/tests/test_ics.py +++ b/openedx/features/calendar_sync/tests/test_ics.py @@ -41,10 +41,20 @@ class TestIcsGeneration(TestCase): def make_assigment( self, block_key=None, title=None, url=None, date=None, contains_gated_content=False, complete=False, - past_due=False, assignment_type=None + past_due=False, assignment_type=None, extra_info=None ): """ Bundles given info into a namedtupled like get_course_assignments returns """ - return _Assignment(block_key, title, url, date, contains_gated_content, complete, past_due, assignment_type) + return _Assignment( + block_key, + title, + url, + date, + contains_gated_content, + complete, + past_due, + assignment_type, + extra_info + ) def expected_ics(self, *assignments): """ Returns hardcoded expected ics strings for given assignments """ diff --git a/openedx/features/content_type_gating/block_transformers.py b/openedx/features/content_type_gating/block_transformers.py index db88164f75..a09f47385b 100644 --- a/openedx/features/content_type_gating/block_transformers.py +++ b/openedx/features/content_type_gating/block_transformers.py @@ -41,10 +41,11 @@ class ContentTypeGateTransformer(BlockStructureTransformer): inside of it is content gated. `contains_gated_content` can then be used to indicate something in the blocks subtree is gated. """ + if block_structure.get_xblock_field(block_key, 'contains_gated_content'): + return + block_structure.override_xblock_field(block_key, 'contains_gated_content', True) + for parent_block_key in block_structure.get_parents(block_key): - if block_structure.get_xblock_field(parent_block_key, 'contains_gated_content'): - continue - block_structure.override_xblock_field(parent_block_key, 'contains_gated_content', True) self._set_contains_gated_content_on_parents(block_structure, parent_block_key) def transform(self, usage_info, block_structure): diff --git a/openedx/features/course_experience/templates/course_experience/dates-summary.html b/openedx/features/course_experience/templates/course_experience/dates-summary.html index 2e9ab91422..08d29000bf 100644 --- a/openedx/features/course_experience/templates/course_experience/dates-summary.html +++ b/openedx/features/course_experience/templates/course_experience/dates-summary.html @@ -21,6 +21,9 @@ from django.utils.translation import ugettext as _ % if course_date.description:

${course_date.description}

% endif + % if course_date.extra_info: +
${course_date.extra_info}
+ % endif % if course_date.link and course_date.link_text: