From 23486a560d8c8d65b6dbddcb3a95a3245c1c2395 Mon Sep 17 00:00:00 2001 From: Dillon Dumesnil Date: Mon, 13 Jan 2020 16:00:41 -0500 Subject: [PATCH] Show relevant dates in course dates sidebar This includes (at least) upcoming assignments, FBE access expiration, and course end date. AA-4 --- lms/djangoapps/courseware/course_tools.py | 2 +- lms/djangoapps/courseware/courses.py | 59 +++++-- lms/djangoapps/courseware/date_summary.py | 114 +++++++----- lms/djangoapps/courseware/tests/helpers.py | 2 +- .../courseware/tests/test_date_summary.py | 167 +++++++++++++++++- lms/djangoapps/courseware/utils.py | 56 ++++++ lms/djangoapps/experiments/utils.py | 2 +- lms/djangoapps/experiments/views_custom.py | 2 +- .../sass/features/_course-experience.scss | 108 +++++------ .../core/djangoapps/schedules/resolvers.py | 2 +- .../features/course_duration_limits/access.py | 2 +- .../course_experience/dates-summary.html | 40 ++--- .../tests/views/test_course_home.py | 4 +- .../course_experience/views/course_dates.py | 2 +- .../course_experience/views/course_sock.py | 2 +- openedx/features/discounts/utils.py | 2 +- 16 files changed, 419 insertions(+), 147 deletions(-) create mode 100644 lms/djangoapps/courseware/utils.py diff --git a/lms/djangoapps/courseware/course_tools.py b/lms/djangoapps/courseware/course_tools.py index ecad90aee0..b13116cd61 100644 --- a/lms/djangoapps/courseware/course_tools.py +++ b/lms/djangoapps/courseware/course_tools.py @@ -10,7 +10,7 @@ from crum import get_current_request from django.utils.translation import ugettext as _ from course_modes.models import CourseMode -from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link +from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link from openedx.features.course_experience.course_tools import CourseTool from student.models import CourseEnrollment diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index a7477fe279..bdeb662269 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -16,6 +16,7 @@ from django.db.models import Prefetch from django.http import Http404, QueryDict from django.urls import reverse from edx_django_utils.monitoring import function_trace +from edx_when.api import get_dates_for_course from fs.errors import ResourceNotFound from opaque_keys.edx.keys import UsageKey from path import Path as path @@ -27,7 +28,9 @@ from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.access_response import MilestoneAccessError, StartDateError from lms.djangoapps.courseware.date_summary import ( CertificateAvailableDate, + CourseAssignmentDate, CourseEndDate, + CourseExpiredDate, CourseStartDate, TodaysDate, VerificationDeadlineDate, @@ -390,13 +393,14 @@ def get_course_info_section(request, user, course, section_key): return html -def get_course_date_blocks(course, user): +def get_course_date_blocks(course, user, request=None, include_past_dates=False, num_assignments=None): """ Return the list of blocks to display on the course info page, sorted by date. """ block_classes = [ CourseEndDate, + CourseExpiredDate, CourseStartDate, TodaysDate, VerificationDeadlineDate, @@ -405,17 +409,50 @@ def get_course_date_blocks(course, user): if certs_api.get_active_web_certificate(course): block_classes.insert(0, CertificateAvailableDate) - blocks = (cls(course, user) for cls in block_classes) + blocks = [cls(course, user) for cls in block_classes] + blocks.extend(get_course_assignment_due_dates( + course, user, request, num_return=num_assignments, include_past_dates=include_past_dates)) - def block_key_fn(block): - """ - If the block's date is None, return the maximum datetime in order - to force it to the end of the list of displayed blocks. - """ - if block.date is None: - return datetime.max.replace(tzinfo=pytz.UTC) - return block.date - return sorted((b for b in blocks if b.is_enabled), key=block_key_fn) + return sorted((b for b in blocks if b.date and (b.is_enabled or include_past_dates)), key=date_block_key_fn) + + +def date_block_key_fn(block): + """ + If the block's date is None, return the maximum datetime in order + to force it to the end of the list of displayed blocks. + """ + return block.date or datetime.max.replace(tzinfo=pytz.UTC) + + +def get_course_assignment_due_dates(course, user, request, num_return=None, include_past_dates=False): + """ + Returns a list of assignment (at the subsection/sequential level) due date + blocks for the given course. Will return num_return results or all results + if num_return is None in date increasing order. + """ + store = modulestore() + all_course_dates = get_dates_for_course(course.id, user) + date_blocks = [] + for (block_key, date_type), date in all_course_dates.items(): + if date_type == 'due' and block_key.block_type == 'sequential': + item = store.get_item(block_key) + if item.graded: + date_block = CourseAssignmentDate(course, user) + date_block.date = date + + block_url = None + now = datetime.now().replace(tzinfo=pytz.UTC) + assignment_released = item.start < now + if assignment_released: + block_url = reverse('jump_to', args=[course.id, block_key]) + block_url = request.build_absolute_uri(block_url) if request else None + date_block.set_title(item.display_name, block_url) + + 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: + return date_blocks[:num_return] + return date_blocks # TODO: Fix this such that these are pulled in as extra course-specific tabs. diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index ffcb774209..a431983b0c 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -20,11 +20,14 @@ from lazy import lazy from pytz import utc from course_modes.models import CourseMode, get_cosmetic_verified_display_price -from lms.djangoapps.commerce.utils import EcommerceService +from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link, verified_upgrade_link_is_valid from lms.djangoapps.verify_student.models import VerificationDeadline from lms.djangoapps.verify_student.services import IDVerificationService +from openedx.core.djangoapps.catalog.utils import get_course_run_details from openedx.core.djangoapps.certificates.api import can_show_certificate_available_date_field from openedx.core.djangolib.markup import HTML, Text +from openedx.features.course_duration_limits.access import get_user_course_expiration_date +from openedx.features.course_duration_limits.models import CourseDurationLimitConfig from openedx.features.course_experience import UPGRADE_DEADLINE_MESSAGE, CourseHomeMessages from student.models import CourseEnrollment @@ -287,6 +290,14 @@ class CourseEndDate(DateSummary): @property def date(self): + if self.course.self_paced: + weeks_to_complete = get_course_run_details(self.course.id, ['weeks_to_complete']).get('weeks_to_complete') + if weeks_to_complete: + course_duration = datetime.timedelta(weeks=weeks_to_complete) + if self.course.end < (self.current_time + course_duration): + return self.course.end + return None + return self.course.end def register_alerts(self, request, course): @@ -318,6 +329,60 @@ class CourseEndDate(DateSummary): ) +class CourseAssignmentDate(DateSummary): + """ + Displays due dates for homework assignments with a link to the homework + assignment if the link is provided. + """ + css_class = 'assignment' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.assignment_date = None + self.assignment_title = None + + @property + def date(self): + return self.assignment_date + + @date.setter + def date(self, date): + self.assignment_date = date + + @property + def title(self): + return self.assignment_title + + def set_title(self, title, link=None): + if link: + self.assignment_title = HTML( + '{assignment_title}' + ).format(assignment_link=link, assignment_title=title) + else: + self.assignment_title = title + + +class CourseExpiredDate(DateSummary): + """ + Displays the course expiration date for Audit learners (if enabled) + """ + css_class = 'course-expired' + + @property + def date(self): + if not CourseDurationLimitConfig.enabled_for_enrollment(user=self.user, course_key=self.course_id): + return + return get_user_course_expiration_date(self.user, self.course) + + @property + def description(self): + return _('You lose all access to this course, including your progress.') + + @property + def title(self): + return _('Audit Access Expires') + + class CertificateAvailableDate(DateSummary): """ Displays the certificate available date of the course. @@ -384,53 +449,6 @@ class CertificateAvailableDate(DateSummary): ) -def verified_upgrade_deadline_link(user, course=None, course_id=None): - """ - Format the correct verified upgrade link for the specified ``user`` - in a course. - - One of ``course`` or ``course_id`` must be supplied. If both are specified, - ``course`` will take priority. - - Arguments: - user (:class:`~django.contrib.auth.models.User`): The user to display - the link for. - course (:class:`.CourseOverview`): The course to render a link for. - course_id (:class:`.CourseKey`): The course_id of the course to render for. - - Returns: - The formatted link that will allow the user to upgrade to verified - in this course. - """ - if course is not None: - course_id = course.id - return EcommerceService().upgrade_url(user, course_id) - - -def verified_upgrade_link_is_valid(enrollment=None): - """ - Return whether this enrollment can be upgraded. - - Arguments: - enrollment (:class:`.CourseEnrollment`): The enrollment under consideration. - If None, then the enrollment is considered to be upgradeable. - """ - # Return `true` if user is not enrolled in course - if enrollment is None: - return False - - upgrade_deadline = enrollment.upgrade_deadline - - if upgrade_deadline is None: - return False - - if datetime.datetime.now(utc).date() > upgrade_deadline.date(): - return False - - # Show the summary if user enrollment is in which allow user to upsell - return enrollment.is_active and enrollment.mode in CourseMode.UPSELL_TO_VERIFIED_MODES - - class VerifiedUpgradeDeadlineDate(DateSummary): """ Displays the date before which learners must upgrade to the diff --git a/lms/djangoapps/courseware/tests/helpers.py b/lms/djangoapps/courseware/tests/helpers.py index 3918d30576..4a7d86e865 100644 --- a/lms/djangoapps/courseware/tests/helpers.py +++ b/lms/djangoapps/courseware/tests/helpers.py @@ -21,7 +21,7 @@ from xblock.field_data import DictFieldData from edxmako.shortcuts import render_to_string from lms.djangoapps.courseware.access import has_access -from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link +from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link from lms.djangoapps.courseware.masquerade import handle_ajax, setup_masquerade from lms.djangoapps.lms_xblock.field_data import LmsFieldData from openedx.core.djangoapps.content.course_overviews.models import CourseOverview diff --git a/lms/djangoapps/courseware/tests/test_date_summary.py b/lms/djangoapps/courseware/tests/test_date_summary.py index 7f39f211dc..49908e8697 100644 --- a/lms/djangoapps/courseware/tests/test_date_summary.py +++ b/lms/djangoapps/courseware/tests/test_date_summary.py @@ -18,7 +18,9 @@ from course_modes.tests.factories import CourseModeFactory from lms.djangoapps.courseware.courses import get_course_date_blocks from lms.djangoapps.courseware.date_summary import ( CertificateAvailableDate, + CourseAssignmentDate, CourseEndDate, + CourseExpiredDate, CourseStartDate, TodaysDate, VerificationDeadlineDate, @@ -38,10 +40,11 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory from openedx.core.djangoapps.user_api.preferences.api import set_user_preference from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag +from openedx.features.course_duration_limits.models import CourseDurationLimitConfig from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, UPGRADE_DEADLINE_MESSAGE, CourseHomeMessages from student.tests.factories import TEST_PASSWORD, CourseEnrollmentFactory, UserFactory from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @ddt.ddt @@ -129,6 +132,138 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) self.assert_block_types(course, user, expected_blocks) + def test_enabled_block_types_with_assignments(self): + """ + Creates a course with multiple subsections to test all of the different + cases for assignment dates showing up. Mocks out calling the edx-when + service and then validates the correct data is set and returned. + """ + course = create_course_run(days_till_start=-100) + user = create_user() + request = RequestFactory().request() + request.user = user + CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) + now = datetime.now(utc) + assignment_title_html = [''] + with self.store.bulk_operations(course.id): + section = ItemFactory.create(category='chapter', parent_location=course.location) + subsection_1 = ItemFactory.create( + category='sequential', + display_name='Released', + parent_location=section.location, + start=now - timedelta(days=1), + due=now + timedelta(days=6), + graded=True, + ) + subsection_2 = ItemFactory.create( + category='sequential', + display_name='Not released', + parent_location=section.location, + start=now + timedelta(days=1), + due=now + timedelta(days=7), + graded=True, + ) + subsection_3 = ItemFactory.create( + category='sequential', + display_name='Third nearest assignment', + parent_location=section.location, + start=now + timedelta(days=1), + due=now + timedelta(days=8), + graded=True, + ) + subsection_4 = ItemFactory.create( + category='sequential', + display_name='Past due date', + parent_location=section.location, + start=now - timedelta(days=14), + due=now - timedelta(days=7), + graded=True, + ) + subsection_5 = ItemFactory.create( + category='sequential', + display_name='Not returned since we do not get non-graded subsections', + parent_location=section.location, + start=now + timedelta(days=1), + due=now - timedelta(days=7), + graded=False, + ) + + with patch('lms.djangoapps.courseware.courses.get_dates_for_course') as mock_get_dates: + mock_get_dates.return_value = { + (subsection_1.location, 'due'): subsection_1.due, + (subsection_1.location, 'start'): subsection_1.start, + (subsection_2.location, 'due'): subsection_2.due, + (subsection_2.location, 'start'): subsection_2.start, + (subsection_3.location, 'due'): subsection_3.due, + (subsection_3.location, 'start'): subsection_3.start, + (subsection_4.location, 'due'): subsection_4.due, + (subsection_4.location, 'start'): subsection_4.start, + (subsection_5.location, 'due'): subsection_5.due, + (subsection_5.location, 'start'): subsection_5.start, + } + # Standard widget case where we restrict the number of assignments. + expected_blocks = ( + TodaysDate, CourseAssignmentDate, CourseAssignmentDate, CourseEndDate, VerificationDeadlineDate + ) + blocks = get_course_date_blocks(course, user, request, num_assignments=2) + self.assertEqual(len(blocks), len(expected_blocks)) + self.assertEqual(set(type(b) for b in blocks), set(expected_blocks)) + assignment_blocks = filter(lambda b: isinstance(b, CourseAssignmentDate), blocks) + for assignment in assignment_blocks: + assignment_title = str(assignment.title) + self.assertNotEqual(assignment_title, 'Third nearest assignment') + self.assertNotEqual(assignment_title, 'Past due date') + self.assertNotEqual(assignment_title, 'Not returned since we do not get non-graded subsections') + # checking if it is _in_ the title instead of being the title since released assignments + # are actually links. Unreleased assignments are just the string of the title. + if 'Released' in assignment_title: + for html_tag in assignment_title_html: + self.assertIn(html_tag, assignment_title) + elif assignment_title == 'Not released': + for html_tag in assignment_title_html: + self.assertNotIn(html_tag, assignment_title) + + # No restrictions on number of assignments to return + expected_blocks = ( + CourseStartDate, TodaysDate, CourseAssignmentDate, CourseAssignmentDate, CourseAssignmentDate, + CourseAssignmentDate, CourseEndDate, VerificationDeadlineDate + ) + blocks = get_course_date_blocks(course, user, request, include_past_dates=True) + self.assertEqual(len(blocks), len(expected_blocks)) + self.assertEqual(set(type(b) for b in blocks), set(expected_blocks)) + assignment_blocks = filter(lambda b: isinstance(b, CourseAssignmentDate), blocks) + for assignment in assignment_blocks: + assignment_title = str(assignment.title) + self.assertNotEqual(assignment_title, 'Not returned since we do not get non-graded subsections') + # checking if it is _in_ the title instead of being the title since released assignments + # are actually links. Unreleased assignments are just the string of the title. + if 'Released' in assignment_title: + for html_tag in assignment_title_html: + self.assertIn(html_tag, assignment_title) + elif assignment_title == 'Not released': + for html_tag in assignment_title_html: + self.assertNotIn(html_tag, assignment_title) + elif assignment_title == 'Third nearest assignment': + # It's still not released + for html_tag in assignment_title_html: + self.assertNotIn(html_tag, assignment_title) + elif 'Past due date' in assignment_title: + self.assertGreater(now, assignment.date) + for html_tag in assignment_title_html: + self.assertIn(html_tag, assignment_title) + + def test_enabled_block_types_with_expired_course(self): + course = create_course_run(days_till_start=-100) + user = create_user() + # These two lines are to trigger the course expired block to be rendered + CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.AUDIT) + CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=utc)) + + expected_blocks = ( + TodaysDate, CourseEndDate, CourseExpiredDate, VerifiedUpgradeDeadlineDate + ) + self.assert_block_types(course, user, expected_blocks) + @ddt.data( # Course not started ({}, (CourseStartDate, TodaysDate, CourseEndDate)), @@ -177,7 +312,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): html_elements = [ '

Upcoming Dates

', - '
', + '
Upcoming Dates', - '
', + '
time til end (end date shown) + {'weeks_to_complete': 4}, # Weeks to complete < time til end (end date not shown) + ) + def test_course_end_date_self_paced(self, cr_details): + """ + In self-paced courses, the end date will now only show up if the learner + views the course within the course's weeks to complete (as defined in + the course-discovery service). E.g. if the weeks to complete is 5 weeks + and the course doesn't end for 10 weeks, there will be no end date, but + if the course ends in 3 weeks, the end date will appear. + """ + now = datetime.now(utc) + end_timedelta_number = 5 + course = CourseFactory.create( + start=now + timedelta(days=-7), end=now + timedelta(weeks=end_timedelta_number), self_paced=True) + user = create_user() + with patch('lms.djangoapps.courseware.date_summary.get_course_run_details') as mock_get_cr_details: + mock_get_cr_details.return_value = cr_details + block = CourseEndDate(course, user) + self.assertEqual(block.title, 'Course End') + if cr_details['weeks_to_complete'] > end_timedelta_number: + self.assertEqual(block.date, course.end) + else: + self.assertIsNone(block.date) + def test_ecommerce_checkout_redirect(self): """Verify the block link redirects to ecommerce checkout if it's enabled.""" sku = 'TESTSKU' diff --git a/lms/djangoapps/courseware/utils.py b/lms/djangoapps/courseware/utils.py new file mode 100644 index 0000000000..a378ace954 --- /dev/null +++ b/lms/djangoapps/courseware/utils.py @@ -0,0 +1,56 @@ +"""Utility functions that have to do with the courseware.""" + + +import datetime + +from lms.djangoapps.commerce.utils import EcommerceService +from pytz import utc + +from course_modes.models import CourseMode + + +def verified_upgrade_deadline_link(user, course=None, course_id=None): + """ + Format the correct verified upgrade link for the specified ``user`` + in a course. + + One of ``course`` or ``course_id`` must be supplied. If both are specified, + ``course`` will take priority. + + Arguments: + user (:class:`~django.contrib.auth.models.User`): The user to display + the link for. + course (:class:`.CourseOverview`): The course to render a link for. + course_id (:class:`.CourseKey`): The course_id of the course to render for. + + Returns: + The formatted link that will allow the user to upgrade to verified + in this course. + """ + if course is not None: + course_id = course.id + return EcommerceService().upgrade_url(user, course_id) + + +def verified_upgrade_link_is_valid(enrollment=None): + """ + Return whether this enrollment can be upgraded. + + Arguments: + enrollment (:class:`.CourseEnrollment`): The enrollment under consideration. + If None, then the enrollment is considered to be upgradeable. + """ + # Return `true` if user is not enrolled in course + if enrollment is None: + return False + + upgrade_deadline = enrollment.upgrade_deadline + + if upgrade_deadline is None: + return False + + if datetime.datetime.now(utc).date() > upgrade_deadline.date(): + return False + + # Show the summary if user enrollment is in which allow user to upsell + return enrollment.is_active and enrollment.mode in CourseMode.UPSELL_TO_VERIFIED_MODES diff --git a/lms/djangoapps/experiments/utils.py b/lms/djangoapps/experiments/utils.py index c0a524f75f..3e4420e33f 100644 --- a/lms/djangoapps/experiments/utils.py +++ b/lms/djangoapps/experiments/utils.py @@ -13,7 +13,7 @@ from opaque_keys.edx.keys import CourseKey from course_modes.models import format_course_price, get_cosmetic_verified_display_price, CourseMode from lms.djangoapps.courseware.access import has_staff_access_to_preview_mode -from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link, verified_upgrade_link_is_valid +from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link, verified_upgrade_link_is_valid from entitlements.models import CourseEntitlement from lms.djangoapps.commerce.utils import EcommerceService from openedx.core.djangoapps.catalog.utils import get_programs diff --git a/lms/djangoapps/experiments/views_custom.py b/lms/djangoapps/experiments/views_custom.py index 99d20d8fac..9e10d9d9cd 100644 --- a/lms/djangoapps/experiments/views_custom.py +++ b/lms/djangoapps/experiments/views_custom.py @@ -23,7 +23,7 @@ from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiv from openedx.core.lib.api.permissions import ApiKeyHeaderPermissionIsAuthenticated from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin -from lms.djangoapps.courseware.date_summary import verified_upgrade_link_is_valid +from lms.djangoapps.courseware.utils import verified_upgrade_link_is_valid from course_modes.models import get_cosmetic_verified_display_price from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.experiments.stable_bucketing import stable_bucketing_hash_group diff --git a/lms/static/sass/features/_course-experience.scss b/lms/static/sass/features/_course-experience.scss index 3f91244b4f..b0483ee3a3 100644 --- a/lms/static/sass/features/_course-experience.scss +++ b/lms/static/sass/features/_course-experience.scss @@ -14,8 +14,8 @@ text-align: center; } - &:not(:first-child) { - margin-top: $baseline; + &:not(:last-child) { + margin-bottom: 32px; } } } @@ -412,64 +412,66 @@ } // date summary -.date-summary-container { - .date-summary { - @include clearfix; +.date-summary { + @include clearfix; - display: flex; - justify-content: space-between; - padding: $baseline/2 $baseline/2 $baseline/2 0; + display: flex; + justify-content: space-between; + padding: 12px 0; + &:last-of-type { + padding-bottom: 0; + } - .left-column { - flex: 5%; + .left-column { + flex: 0 0 24px; - .calendar-icon { - margin-top: 3px; - height: 1em; - width: auto; - background: url('#{$static-path}/images/calendar-alt-regular.svg'); - background-repeat: no-repeat; - } + .calendar-icon { + margin-top: 4px; + height: 1em; + width: 16px; + background: url('#{$static-path}/images/calendar-alt-regular.svg'); + background-repeat: no-repeat; + } + } + + .right-column { + flex: auto; + + .localized-datetime { + font-weight: $font-weight-bold; + margin-bottom: 8px; } - .right-column { - flex: 85%; - - .localized-datetime { - font-weight: $font-weight-bold; - margin-bottom: 8px; - } - - .heading { - font: -apple-system-body; - line-height: 1; - font-weight: $font-bold; - color: theme-color("dark"); - } - - .description { - margin-bottom: $baseline/2; - display: inline-block; - } - - .heading, .description { - font-size: 0.9rem; - } - - .date-summary-link { - font-weight: $font-semibold; - - a { - color: $link-color; - font-weight: $font-regular; - } - } - } - - .date { - color: theme-color("dark"); + .heading { font: -apple-system-body; + line-height: 1; + font-weight: $font-bold; + color: theme-color("dark"); } + + .description { + margin-bottom: 0; + display: inline-block; + } + + .heading, .description { + font-size: 0.9rem; + } + + .date-summary-link { + margin-top: 8px; + font-weight: $font-semibold; + + a { + color: $link-color; + font-size: 0.9rem; + } + } + } + + .date { + color: theme-color("dark"); + font: -apple-system-body; } } diff --git a/openedx/core/djangoapps/schedules/resolvers.py b/openedx/core/djangoapps/schedules/resolvers.py index c7b22a07d0..6437e28238 100644 --- a/openedx/core/djangoapps/schedules/resolvers.py +++ b/openedx/core/djangoapps/schedules/resolvers.py @@ -14,7 +14,7 @@ from edx_ace.recipient import Recipient from edx_ace.recipient_resolver import RecipientResolver from edx_django_utils.monitoring import function_trace, set_custom_metric -from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link, verified_upgrade_link_is_valid +from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link, verified_upgrade_link_is_valid from lms.djangoapps.discussion.notification_prefs.views import UsernameCipher from openedx.core.djangoapps.ace_common.template_context import get_base_template_context from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_SHOW_UNSUBSCRIBE_WAFFLE_SWITCH diff --git a/openedx/features/course_duration_limits/access.py b/openedx/features/course_duration_limits/access.py index 1b5ed1323b..6dfd476157 100644 --- a/openedx/features/course_duration_limits/access.py +++ b/openedx/features/course_duration_limits/access.py @@ -17,7 +17,7 @@ from web_fragments.fragment import Fragment from course_modes.models import CourseMode from lms.djangoapps.courseware.access_response import AccessError from lms.djangoapps.courseware.access_utils import ACCESS_GRANTED -from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link +from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link from lms.djangoapps.courseware.masquerade import get_course_masquerade, is_masquerading_as_specific_student from openedx.core.djangoapps.catalog.utils import get_course_run_details from openedx.core.djangoapps.content.course_overviews.models import CourseOverview 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 36952923c8..e8a66c083b 100644 --- a/openedx/features/course_experience/templates/course_experience/dates-summary.html +++ b/openedx/features/course_experience/templates/course_experience/dates-summary.html @@ -2,26 +2,24 @@ from django.utils.translation import ugettext as _ %> <%page args="course_date" expression_filter="h"/> -
-
-
-
-
-
+
+
+
+
+
+ % if course_date.date: +

+ % endif + % if course_date.title: +
${course_date.title}
+ % endif + % if course_date.description: +

${course_date.description}

+ % endif + % if course_date.link and course_date.link_text: + + % endif
diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py index ea0e6e7a4b..74562c19b3 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -25,7 +25,7 @@ from experiments.models import ExperimentData from lms.djangoapps.commerce.models import CommerceConfiguration from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.course_goals.api import add_course_goal, remove_course_goal -from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link +from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link from lms.djangoapps.courseware.tests.factories import ( BetaTesterFactory, GlobalStaffFactory, @@ -218,7 +218,7 @@ class TestCourseHomePage(CourseHomePageTestCase): # Fetch the view and verify the query counts # TODO: decrease query count as part of REVO-28 - with self.assertNumQueries(74, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): + with self.assertNumQueries(76, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): with check_mongo_calls(4): url = course_home_url(self.course) self.client.get(url) diff --git a/openedx/features/course_experience/views/course_dates.py b/openedx/features/course_experience/views/course_dates.py index 204a273494..91c2bbfd21 100644 --- a/openedx/features/course_experience/views/course_dates.py +++ b/openedx/features/course_experience/views/course_dates.py @@ -25,7 +25,7 @@ class CourseDatesFragmentView(EdxFragmentView): """ course_key = CourseKey.from_string(course_id) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False) - course_date_blocks = get_course_date_blocks(course, request.user) + course_date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=2) context = { 'course_date_blocks': [block for block in course_date_blocks if block.title != 'current_datetime'] diff --git a/openedx/features/course_experience/views/course_sock.py b/openedx/features/course_experience/views/course_sock.py index 695aef426b..a41d9b8f81 100644 --- a/openedx/features/course_experience/views/course_sock.py +++ b/openedx/features/course_experience/views/course_sock.py @@ -6,7 +6,7 @@ Fragment for rendering the course's sock and associated toggle button. from django.template.loader import render_to_string from web_fragments.fragment import Fragment -from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link, verified_upgrade_link_is_valid +from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link, verified_upgrade_link_is_valid from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.features.discounts.utils import format_strikeout_price from student.models import CourseEnrollment diff --git a/openedx/features/discounts/utils.py b/openedx/features/discounts/utils.py index 8b9840e00e..53ac8a898e 100644 --- a/openedx/features/discounts/utils.py +++ b/openedx/features/discounts/utils.py @@ -11,7 +11,7 @@ from edx_django_utils.cache import RequestCache import pytz from course_modes.models import get_course_prices, format_course_price -from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link +from lms.djangoapps.courseware.utils import verified_upgrade_deadline_link from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from experiments.models import ExperimentData