Merge pull request #22911 from edx/ddumesnil/relevant-dates-AA-4
Show relevant dates in course dates sidebar
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
'<a href="{assignment_link}">{assignment_title}</a>'
|
||||
).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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ['<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,
|
||||
)
|
||||
|
||||
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 = [
|
||||
'<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 +337,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 +422,32 @@ 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)
|
||||
)
|
||||
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'
|
||||
|
||||
56
lms/djangoapps/courseware/utils.py
Normal file
56
lms/djangoapps/courseware/utils.py
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,26 +2,24 @@
|
||||
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:
|
||||
<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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user