diff --git a/cms/djangoapps/contentstore/tests/test_request_event.py b/cms/djangoapps/contentstore/tests/test_request_event.py index 7ac72f25a06971b3a4f7a519043bc0a3bac04f3f..9c5bebe58d42319998e14314b2e7b33129fd95c1 100644 --- a/cms/djangoapps/contentstore/tests/test_request_event.py +++ b/cms/djangoapps/contentstore/tests/test_request_event.py @@ -3,7 +3,7 @@ from django.test import TestCase from django.urls import reverse -from six import unichr # pylint: disable=W0622 +from six import unichr from contentstore.views.helpers import event as cms_user_track @@ -20,7 +20,7 @@ class CMSLogTest(TestCase): """ requests = [ {"event": "my_event", "event_type": "my_event_type", "page": "my_page"}, - {"event": "{'json': 'object'}", "event_type": unichr(512), "page": "my_page"} # pylint: disable=unicode-format-string + {"event": "{'json': 'object'}", "event_type": unichr(512), "page": "my_page"} ] for request_params in requests: response = self.client.post(reverse(cms_user_track), request_params) diff --git a/lms/djangoapps/courseware/course_tools.py b/lms/djangoapps/courseware/course_tools.py index ecad90aee08efe580b33dbc8665b091260e8ff38..b13116cd6166e8f6552864c7f944189dd04a3088 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 a7477fe27986310f5cf8df24d87e8aefb14a00be..5fa7d181d13b19609200ac6d8afef84728cc05c7 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -15,7 +15,9 @@ 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 _ 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 +29,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, @@ -45,7 +49,7 @@ from openedx.core.djangoapps.enrollments.api import get_course_enrollment_detail from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.lib.api.view_utils import LazySequence from openedx.features.course_duration_limits.access import AuditExpiredError -from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG +from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, DATE_WIDGET_V2_FLAG from static_replace import replace_static_urls from student.models import CourseEnrollment from survey.utils import is_survey_required_and_unanswered @@ -390,7 +394,7 @@ 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. @@ -405,17 +409,53 @@ 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] + if DATE_WIDGET_V2_FLAG.is_enabled(course.id): + blocks.append(CourseExpiredDate(course, user)) + 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 item.start else None + 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 + assignment_title = item.display_name if item.display_name else _('Assignment') + date_block.set_title(assignment_title, link=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 ffcb774209a38db6626058478a14a46eff1e86ef..5746495de7eea273fcaf1e0812d42b8a4dcdcd6a 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -20,12 +20,15 @@ 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_experience import UPGRADE_DEADLINE_MESSAGE, CourseHomeMessages +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 DATE_WIDGET_V2_FLAG, UPGRADE_DEADLINE_MESSAGE, CourseHomeMessages from student.models import CourseEnrollment from .context_processor import user_timezone_locale_prefs @@ -59,6 +62,11 @@ class DateSummary(object): """The title of this summary.""" return '' + @property + def title_html(self): + """The title as html for this summary.""" + return '' + @property def description(self): """The detail text displayed by this summary.""" @@ -287,6 +295,14 @@ class CourseEndDate(DateSummary): @property def date(self): + if DATE_WIDGET_V2_FLAG.is_enabled(self.course_id) and 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 +334,65 @@ 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 + self.assignment_title_html = 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 + + @property + def title_html(self): + return self.assignment_title_html + + def set_title(self, title, link=None): + """ Used to set the title_html and title properties for the assignment date block """ + if link: + self.assignment_title_html = HTML( + '<a href="{assignment_link}">{assignment_title}</a>' + ).format(assignment_link=link, assignment_title=title) + 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 +459,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 3918d305765089884a0cbe56f99f527abd2c1314..4a7d86e86587eaf54226e543fc5a56d90322caa3 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 7f39f211dcc667bf6941a5ccc63c33010d24595e..48774aac3c3e3d34db496c91db66f6611f36c399 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,13 @@ 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_experience import UNIFIED_COURSE_TAB_FLAG, UPGRADE_DEADLINE_MESSAGE, CourseHomeMessages +from openedx.features.course_duration_limits.models import CourseDurationLimitConfig +from openedx.features.course_experience import ( + DATE_WIDGET_V2_FLAG, 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 +134,174 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): CourseEnrollmentFactory(course_id=course.id, user=user, mode=CourseMode.VERIFIED) self.assert_block_types(course, user, expected_blocks) + @override_waffle_flag(DATE_WIDGET_V2_FLAG, active=True) + def test_enabled_block_types_with_assignments(self): # pylint: disable=too-many-statements + """ + 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 = ['<a href=', '</a>'] + 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, + ) + subsection_6 = ItemFactory.create( + category='sequential', + display_name='No start date', + parent_location=section.location, + start=None, + due=now + timedelta(days=9), + graded=True, + ) + subsection_7 = ItemFactory.create( + category='sequential', + # Setting display name to None should set the assignment title to 'Assignment' + display_name=None, + parent_location=section.location, + start=now - timedelta(days=14), + due=now + timedelta(days=10), + graded=True, + ) + dummy_subsection = ItemFactory.create(category='sequential') + + 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, + (subsection_6.location, 'due'): subsection_6.due, + (subsection_7.location, 'due'): subsection_7.due, + (subsection_7.location, 'start'): subsection_7.start, + # Adding this in for the case where we return a block that + # doesn't actually exist as part of the course. Should just be ignored. + (dummy_subsection.location, 'due'): dummy_subsection.due, + } + # 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_html) or 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, 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_html) or 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) + elif 'No start date' == assignment_title: + # Can't determine if it is released so it does not get a link + for html_tag in assignment_title_html: + self.assertNotIn(html_tag, assignment_title) + # This is the item with no display name where we set one ourselves. + elif 'Assignment' in assignment_title: + # Can't determine if it is released so it does not get a link + for html_tag in assignment_title_html: + self.assertIn(html_tag, assignment_title) + + @override_waffle_flag(DATE_WIDGET_V2_FLAG, active=True) + 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 +350,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): html_elements = [ '<h3 class="hd hd-6 handouts-header">Upcoming Dates</h3>', - '<div class="date-summary-container">', + '<div class="date-summary', '<p class="hd hd-6 date localized-datetime"', 'data-timezone="None"' ] @@ -202,7 +375,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): html_elements = [ '<h3 class="hd hd-6 handouts-header">Upcoming Dates</h3>', - '<div class="date-summary-container">', + '<div class="date-summary', '<p class="hd hd-6 date localized-datetime"', 'data-timezone="America/Los_Angeles"' ] @@ -287,6 +460,33 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ) self.assertEqual(block.title, 'Course End') + @ddt.data( + {'weeks_to_complete': 7}, # Weeks to complete > time til end (end date shown) + {'weeks_to_complete': 4}, # Weeks to complete < time til end (end date not shown) + ) + @override_waffle_flag(DATE_WIDGET_V2_FLAG, active=True) + 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 0000000000000000000000000000000000000000..a378ace954fb2af9f46e4ce168ebeb672802d4e7 --- /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 c0a524f75fae09d06bedba9a3b70ec461123773f..3e4420e33fd89917a7c893650243b03190ea6de3 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 99d20d8fac89576456fe66f9c541af3e2479ff6b..9e10d9d9cdbdc2b5e19bfc561946c6e318705361 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 3f91244b4f951c95492a41c4c63cb7c9ff04b7a7..b1fe70780ba39768e3f1d6ec1cfe79a2f090ce26 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,69 @@ } // 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; - - .left-column { - flex: 5%; - - .calendar-icon { - margin-top: 3px; - height: 1em; - width: auto; - background: url('#{$static-path}/images/calendar-alt-regular.svg'); - background-repeat: no-repeat; - } + display: flex; + justify-content: space-between; + padding: 12px 0; + &:last-of-type { + padding-bottom: 0; + } + + .left-column { + flex: 0 0 24px; + + .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: 85%; + .right-column { + flex: auto; - .localized-datetime { - font-weight: $font-weight-bold; - margin-bottom: 8px; - } + .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"); - } + .heading { + font: -apple-system-body; + line-height: 1.5; + font-weight: $font-bold; + color: theme-color("dark"); - .description { - margin-bottom: $baseline/2; - display: inline-block; + a { + font-weight: $font-semibold; } + } - .heading, .description { - font-size: 0.9rem; - } + .description { + margin-bottom: 0; + display: inline-block; + } - .date-summary-link { - font-weight: $font-semibold; + .heading, .description { + font-size: 0.9rem; + } - a { - color: $link-color; - font-weight: $font-regular; - } + .date-summary-link { + font-weight: $font-semibold; + + a { + color: $link-color; + font-size: 0.9rem; } } + } - .date { - color: theme-color("dark"); - font: -apple-system-body; - } + .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 c7b22a07d0eb208215978ba70e363908784e8859..6437e282385703f3abfd6d11c8a743f368b0b04b 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 a528416fb4a6a49a1ed9dd83e353f6d7123c2972..8f69656b5a1c30e4e94276d1a6a0cab9fe4fc96d 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/__init__.py b/openedx/features/course_experience/__init__.py index a419ca520e3748dfce6865ff1b679c9a2797cb55..034541e53e092c4ebf8292b484941a385b8f51e8 100644 --- a/openedx/features/course_experience/__init__.py +++ b/openedx/features/course_experience/__init__.py @@ -81,6 +81,10 @@ COURSE_ENABLE_UNENROLLED_ACCESS_FLAG = CourseWaffleFlag(SEO_WAFFLE_FLAG_NAMESPAC # Waffle flag to enable relative dates for course content RELATIVE_DATES_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'relative_dates') +# Waffle flag to enable showing FBE messaging, assignment due dates, and modified +# end date logic (for self-paced courses) in the date widget +DATE_WIDGET_V2_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'date_widget_v2') + def course_home_page_title(course): # pylint: disable=unused-argument """ 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 36952923c8186fec08f888a0f41923dfcd1b1574..0cfa204bccdda6c9de2654eba248e97e7c7007b1 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,26 @@ from django.utils.translation import ugettext as _ %> <%page args="course_date" expression_filter="h"/> -<div class="date-summary-container"> - <div class="date-summary date-summary-${course_date.css_class}"> - <div class="left-column"> - <div class="calendar-icon"></div> - </div> - <div class="right-column"> - % if course_date.date: - <p class="hd hd-6 date localized-datetime" data-format="shortDate" data-datetime="${course_date.date}" data-timezone="${user_timezone}" data-language="${user_language}"></p> - % endif - % if course_date.title: - <span class="hd hd-6 heading">${course_date.title}</span> - % endif - % if course_date.description: - <p class="description">${course_date.description}</p> - % endif - % if course_date.link and course_date.link_text: - <span class="date-summary-link"> - <a href="${course_date.link}">${course_date.link_text}</a> - </span> - % endif - </div> +<div class="date-summary date-summary-${course_date.css_class}"> + <div class="left-column"> + <div class="calendar-icon"></div> + </div> + <div class="right-column"> + % if course_date.date: + <p class="hd hd-6 date localized-datetime" data-format="shortDate" data-datetime="${course_date.date}" data-timezone="${user_timezone}" data-language="${user_language}"></p> + % endif + % if course_date.title_html: + <div class="hd hd-6 heading">${course_date.title_html}</div> + % elif course_date.title: + <div class="hd hd-6 heading">${course_date.title}</div> + % endif + % if course_date.description: + <p class="description">${course_date.description}</p> + % endif + % if course_date.link and course_date.link_text: + <div class="date-summary-link"> + <a href="${course_date.link}">${course_date.link_text}</a> + </div> + % endif </div> </div> 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 ea0e6e7a4bb4b636ccf4e0a2efcb334f585b4ba3..8b88c99381e2f2bcffa8d166b695aedef8d3ab66 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, diff --git a/openedx/features/course_experience/views/course_dates.py b/openedx/features/course_experience/views/course_dates.py index 204a273494579137079fcbfe6a1c37d938213e41..91c2bbfd21a8660c66cf60988e2c2b0b2111f98b 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 695aef426b896e5d6e3f1ac0f67c80771f88a96f..a41d9b8f819e59b4100a96c9ce6a841f66e6a6bd 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 8b9840e00e935a8caf4bb804db1b53bfa31f4529..53ac8a898e7f70d317c4db19b3d1891c116793d7 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 diff --git a/scripts/thresholds.sh b/scripts/thresholds.sh index 6f8bfb89d4b2d18c9173b041c3e9946bdaf54b70..31d9d2fb008cb6a2ab84c6747b250c61cbeb101a 100755 --- a/scripts/thresholds.sh +++ b/scripts/thresholds.sh @@ -2,6 +2,6 @@ set -e export LOWER_PYLINT_THRESHOLD=1000 -export UPPER_PYLINT_THRESHOLD=2005 +export UPPER_PYLINT_THRESHOLD=1985 export ESLINT_THRESHOLD=5530 export STYLELINT_THRESHOLD=880