feature: Updated the text when a learner earns a cert on the Dashboard

[MICROBA-678]

This patch will update the text on the course dashboard when a learner
successfully earns a certificate and that certificate is available. It
also adds to the Outline API for the Outline in the Learning MFE so that
the same changes can be made there.
This commit is contained in:
Albert (AJ) St. Aubin
2021-06-04 11:44:30 -04:00
parent 8c205e5c0e
commit eb346f1ab2
7 changed files with 85 additions and 86 deletions

View File

@@ -275,5 +275,5 @@ class CertificateDisplayTestLinkedHtmlView(CertificateDisplayTestBase):
response = self.client.get(reverse('dashboard'))
self.assertContains(response, 'View Test_Certificate')
self.assertContains(response, 'View my Test_Certificate')
self.assertContains(response, test_url)

View File

@@ -33,6 +33,7 @@ from common.djangoapps.util.milestones_helpers import (
)
from common.djangoapps.util.testing import UrlResetMixin # lint-amnesty, pylint: disable=unused-import
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
from lms.djangoapps.certificates.data import CertificateStatuses
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
@@ -44,6 +45,10 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
PASSWORD = 'test'
TOMORROW = now() + timedelta(days=1)
ONE_WEEK_AGO = now() - timedelta(weeks=1)
THREE_YEARS_FROM_NOW = now() + timedelta(days=(365 * 3))
THREE_YEARS_AGO = now() - timedelta(days=(365 * 3))
@ddt.ddt
@@ -167,9 +172,6 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
EMAIL_SETTINGS_ELEMENT_ID = "#actions-item-email-settings-0"
ENABLED_SIGNALS = ['course_published']
TOMORROW = now() + timedelta(days=1)
THREE_YEARS_FROM_NOW = now() + timedelta(days=(365 * 3))
THREE_YEARS_AGO = now() - timedelta(days=(365 * 3))
MOCK_SETTINGS = {
'FEATURES': {
'DISABLE_START_DATES': False,
@@ -219,38 +221,50 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
response = self.client.get(self.path)
self.assertRedirects(response, reverse('account_settings'))
def test_grade_appears_before_course_end_date(self):
"""
Verify that learners are not able to see their final grade before the end
of course in the learner dashboard
"""
self.course_key = CourseKey.from_string('course-v1:edX+DemoX+Demo_Course') # lint-amnesty, pylint: disable=attribute-defined-outside-init
self.course = CourseOverviewFactory.create(id=self.course_key, end_date=self.TOMORROW, # lint-amnesty, pylint: disable=attribute-defined-outside-init
certificate_available_date=self.THREE_YEARS_AGO,
lowest_passing_grade=0.3)
self.course_enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user) # lint-amnesty, pylint: disable=attribute-defined-outside-init
GeneratedCertificateFactory(status='notpassing', course_id=self.course.id, user=self.user, grade=0.45)
def test_course_cert_available_message_after_course_end(self):
course_key = CourseKey.from_string('course-v1:edX+DemoX+Demo_Course')
course = CourseOverviewFactory.create(
id=course_key,
end_date=THREE_YEARS_AGO,
certificate_available_date=TOMORROW,
lowest_passing_grade=0.3
)
CourseEnrollmentFactory(course_id=course.id, user=self.user)
GeneratedCertificateFactory(
status=CertificateStatuses.downloadable, course_id=course.id, user=self.user, grade=0.45
)
response = self.client.get(reverse('dashboard'))
# The final grade does not appear before the course has ended
self.assertContains(response, 'Your final grade:')
self.assertContains(response, '<span class="grade-value">45%</span>')
def test_grade_not_appears_before_cert_available_date(self):
"""
Verify that learners are able to see their final grade of the course in
the learner dashboard after the course had ended
"""
self.course_key = CourseKey.from_string('course-v1:edX+DemoX+Demo_Course') # lint-amnesty, pylint: disable=attribute-defined-outside-init
self.course = CourseOverviewFactory.create(id=self.course_key, end_date=self.THREE_YEARS_AGO, # lint-amnesty, pylint: disable=attribute-defined-outside-init
certificate_available_date=self.TOMORROW,
lowest_passing_grade=0.3)
self.course_enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user) # lint-amnesty, pylint: disable=attribute-defined-outside-init
GeneratedCertificateFactory(status='notpassing', course_id=self.course.id, user=self.user, grade=0.45)
self.assertContains(response, 'Your grade and certificate will be ready after')
def test_course_cert_available_message_same_day_as_course_end(self):
course_key = CourseKey.from_string('course-v1:edX+DemoX+Demo_Course')
course = CourseOverviewFactory.create(
id=course_key,
end_date=TOMORROW,
certificate_available_date=TOMORROW,
lowest_passing_grade=0.3
)
CourseEnrollmentFactory(course_id=course.id, user=self.user)
GeneratedCertificateFactory(
status=CertificateStatuses.downloadable, course_id=course.id, user=self.user, grade=0.45
)
response = self.client.get(reverse('dashboard'))
self.assertNotContains(response, 'Your final grade:')
self.assertNotContains(response, '<span class="grade-value">45%</span>')
self.assertContains(response, 'Your grade and certificate will be ready after')
def test_cert_available_message_after_course_end(self):
course_key = CourseKey.from_string('course-v1:edX+DemoX+Demo_Course')
course = CourseOverviewFactory.create(
id=course_key,
end_date=ONE_WEEK_AGO,
certificate_available_date=now(),
lowest_passing_grade=0.3
)
CourseEnrollmentFactory(course_id=course.id, user=self.user)
GeneratedCertificateFactory(
status=CertificateStatuses.downloadable, course_id=course.id, user=self.user, grade=0.45
)
response = self.client.get(reverse('dashboard'))
self.assertContains(response, 'Congratulations! Your certificate is ready.')
@patch.multiple('django.conf.settings', **MOCK_SETTINGS)
@ddt.data(
@@ -266,7 +280,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
Verify that the course sharing icons show up if course is starting in future and
any of marketing or social sharing urls are set.
"""
self.course = CourseFactory.create(start=self.TOMORROW, emit_signals=True, default_store=modulestore_type) # lint-amnesty, pylint: disable=attribute-defined-outside-init
self.course = CourseFactory.create(start=TOMORROW, emit_signals=True, default_store=modulestore_type) # lint-amnesty, pylint: disable=attribute-defined-outside-init
self.course_enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user) # lint-amnesty, pylint: disable=attribute-defined-outside-init
self.set_course_sharing_urls(set_marketing, set_social_sharing)
@@ -316,11 +330,11 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
CourseEntitlementFactory.create(user=self.user, course_uuid=program['courses'][0]['uuid'])
mock_get_programs.return_value = [program]
course_key = CourseKey.from_string('course-v1:FAKE+FA1-MA1.X+3T2017')
mock_course_overview.return_value = CourseOverviewFactory.create(start=self.TOMORROW, id=course_key)
mock_course_overview.return_value = CourseOverviewFactory.create(start=TOMORROW, id=course_key)
mock_course_runs.return_value = [
{
'key': str(course_key),
'enrollment_end': str(self.TOMORROW),
'enrollment_end': str(TOMORROW),
'pacing_type': 'instructor_paced',
'type': 'verified',
'status': 'published'
@@ -347,7 +361,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
mock_course_runs.return_value = [
{
'key': 'course-v1:edX+toy+2012_Fall',
'enrollment_end': str(self.TOMORROW),
'enrollment_end': str(TOMORROW),
'pacing_type': 'instructor_paced',
'type': 'verified',
'status': 'published'
@@ -370,14 +384,14 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
"""
CourseEntitlementFactory(
user=self.user,
created=self.THREE_YEARS_AGO,
created=THREE_YEARS_AGO,
expired_at=now()
)
mock_course_overview.return_value = CourseOverviewFactory(start=self.TOMORROW)
mock_course_overview.return_value = CourseOverviewFactory(start=TOMORROW)
mock_course_runs.return_value = [
{
'key': 'course-v1:FAKE+FA1-MA1.X+3T2017',
'enrollment_end': str(self.TOMORROW),
'enrollment_end': str(TOMORROW),
'pacing_type': 'instructor_paced',
'type': 'verified',
'status': 'published'
@@ -401,7 +415,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
# Test an enrollment end in the past
mocked_course_overview = CourseOverviewFactory.create(
start=self.TOMORROW, end=self.THREE_YEARS_FROM_NOW, self_paced=True, enrollment_end=self.THREE_YEARS_AGO
start=TOMORROW, end=THREE_YEARS_FROM_NOW, self_paced=True, enrollment_end=THREE_YEARS_AGO
)
mock_course_overview.return_value = mocked_course_overview
course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=str(mocked_course_overview.id))
@@ -419,7 +433,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
# self.assertIn(noAvailableSessions, response.content)
# Test an enrollment end in the future sets an availableSession
mocked_course_overview.enrollment_end = self.TOMORROW
mocked_course_overview.enrollment_end = TOMORROW
mocked_course_overview.save()
mock_course_overview.return_value = mocked_course_overview
@@ -465,7 +479,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
- a related programs message
"""
mocked_course_overview = CourseOverviewFactory(
start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW
start=TOMORROW, self_paced=True, enrollment_end=TOMORROW
)
mock_course_overview.return_value = mocked_course_overview
course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=str(mocked_course_overview.id))
@@ -500,10 +514,10 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
- a related programs message
"""
mocked_course_overview = CourseOverviewFactory(
start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW
start=TOMORROW, self_paced=True, enrollment_end=TOMORROW
)
mock_course_overview.return_value = mocked_course_overview
course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=str(mocked_course_overview.id), created=self.THREE_YEARS_AGO) # lint-amnesty, pylint: disable=line-too-long
course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=str(mocked_course_overview.id), created=THREE_YEARS_AGO) # lint-amnesty, pylint: disable=line-too-long
mock_course_runs.return_value = [
{
'key': str(mocked_course_overview.id),
@@ -513,7 +527,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
'status': 'published'
}
]
entitlement = CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment, created=self.THREE_YEARS_AGO) # lint-amnesty, pylint: disable=line-too-long
entitlement = CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment, created=THREE_YEARS_AGO) # lint-amnesty, pylint: disable=line-too-long
program = ProgramFactory()
program['courses'][0]['course_runs'] = [{'key': str(mocked_course_overview.id)}]
program['courses'][0]['uuid'] = entitlement.course_uuid
@@ -531,7 +545,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
"""
mock_email_feature.return_value = True
course_overview = CourseOverviewFactory(
start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW
start=TOMORROW, self_paced=True, enrollment_end=TOMORROW
)
course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=course_overview.id)
entitlement = CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment)
@@ -551,7 +565,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
Assert that the Email Settings action is not shown when the entitlement is not fulfilled.
"""
mock_email_feature.return_value = True
mock_course_overview.return_value = CourseOverviewFactory(start=self.TOMORROW)
mock_course_overview.return_value = CourseOverviewFactory(start=TOMORROW)
CourseEntitlementFactory(user=self.user)
response = self.client.get(self.path)
assert pq(response.content)(self.EMAIL_SETTINGS_ELEMENT_ID).length == 0
@@ -758,17 +772,17 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
Links will be removed from the course title, course image and button (View Course/Resume Course).
The course card should have an access expired message.
"""
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=self.THREE_YEARS_AGO - timedelta(days=30))
CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=THREE_YEARS_AGO - timedelta(days=30))
self.override_waffle_switch(True)
course = CourseFactory.create(start=self.THREE_YEARS_AGO)
course = CourseFactory.create(start=THREE_YEARS_AGO)
add_course_mode(course, mode_slug=CourseMode.AUDIT)
add_course_mode(course)
enrollment = CourseEnrollmentFactory.create(
user=self.user,
course_id=course.id
)
enrollment.created = self.THREE_YEARS_AGO + timedelta(days=1)
enrollment.created = THREE_YEARS_AGO + timedelta(days=1)
enrollment.save()
response = self.client.get(reverse('dashboard'))

View File

@@ -7,7 +7,6 @@ import logging
from urllib.parse import urlencode
from django.conf import settings
from django.utils.timezone import now
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
@@ -71,16 +70,3 @@ def has_certificates_enabled(course):
if not settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
return False
return course.cert_html_view_enabled
def should_display_grade(course_overview):
"""
Returns True or False depending upon either certificate available date
or course end date
"""
cert_available_date = course_overview.certificate_available_date
current_date = now().replace(hour=0, minute=0, second=0, microsecond=0)
if cert_available_date:
return cert_available_date < current_date
return course_overview.end and course_overview.end < current_date

View File

@@ -6,6 +6,7 @@ from django.utils.translation import ngettext
from rest_framework import serializers
from lms.djangoapps.course_home_api.dates.v1.serializers import DateSummarySerializer
from lms.djangoapps.course_home_api.progress.v1.serializers import CertificateDataSerializer
from lms.djangoapps.course_home_api.mixins import DatesBannerSerializerMixin, VerifiedModeSerializerMixin
@@ -113,6 +114,7 @@ class OutlineTabSerializer(DatesBannerSerializerMixin, VerifiedModeSerializerMix
Serializer for the Outline Tab
"""
access_expiration = serializers.DictField()
cert_data = CertificateDataSerializer()
course_blocks = CourseBlockSerializer()
course_goals = CourseGoalsSerializer()
course_tools = CourseToolSerializer(many=True)

View File

@@ -37,6 +37,7 @@ from lms.djangoapps.courseware.context_processor import user_timezone_locale_pre
from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_info_section, get_course_with_access
from lms.djangoapps.courseware.date_summary import TodaysDate
from lms.djangoapps.courseware.masquerade import is_masquerading, setup_masquerade
from lms.djangoapps.courseware.views.views import get_cert_data
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
from openedx.features.course_duration_limits.access import get_access_expiration_data
@@ -196,6 +197,7 @@ class OutlineTabView(RetrieveAPIView):
# Set all of the defaults
access_expiration = None
cert_data = None
course_blocks = None
course_goals = {
'goal_options': [],
@@ -232,6 +234,7 @@ class OutlineTabView(RetrieveAPIView):
offer_data = generate_offer_data(request.user, course_overview)
access_expiration = get_access_expiration_data(request.user, course_overview)
cert_data = get_cert_data(request.user, course, enrollment.mode) if is_enrolled else None
# Only show the set course goal message for enrolled, unverified
# users in a course that allows for verified statuses.
@@ -277,6 +280,7 @@ class OutlineTabView(RetrieveAPIView):
data = {
'access_expiration': access_expiration,
'cert_data': cert_data,
'course_blocks': course_blocks,
'course_goals': course_goals,
'course_tools': course_tools,

View File

@@ -956,10 +956,6 @@
margin: 0;
}
&.course-status-processing {
color: #0d7d4d;
}
.credit-action {
.credit-btn {
@extend %btn-pl-yellow-base;
@@ -1071,6 +1067,11 @@
}
}
&.course-status-earned-not-available,
&.course-status-certavailable {
color: #0d7d4d;
}
&.course-status-certavailable {
.message-copy {
width: flex-grid(6, 12);

View File

@@ -4,7 +4,7 @@
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.util.course import should_display_grade
from lms.djangoapps.certificates.data import CertificateStatuses
%>
<%namespace name='static' file='../static_content.html'/>
@@ -20,7 +20,7 @@ from common.djangoapps.util.course import should_display_grade
<%
if cert_status['status'] == 'certificate_earned_but_not_available':
status_css_class = 'course-status-processing'
status_css_class = 'course-status-earned-not-available'
elif cert_status['status'] == 'generating':
status_css_class = 'course-status-certrendering'
elif cert_status['status'] == 'downloadable':
@@ -44,18 +44,11 @@ else:
</p>
</div>
% else:
<div class="message message-status ${status_css_class} is-shown">
<p class="message-copy">
% if should_display_grade(course_overview):
${_("Your final grade:")}
<span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>.
% elif course_overview.certificate_available_date and not course_overview.self_paced:
<%
cert_available_date = course_overview.certificate_available_date.strftime('%Y-%m-%d')
%>
${_("Grades will be finalized on {cert_available_date}".format(cert_available_date=cert_available_date))}
% endif
% if cert_status['status'] == 'notpassing':
<div class="message message-status ${status_css_class} d-flex justify-content-between align-items-center">
<div class="message-copy">
% if cert_status['status'] == CertificateStatuses.downloadable:
${_("Congratulations! Your certificate is ready.")}
% elif cert_status['status'] == 'notpassing':
% if enrollment.mode != 'audit':
${_("Grade required for a {cert_name_short}:").format(cert_name_short=cert_name_short)}
% else:
@@ -76,8 +69,7 @@ else:
<a href="${reverify_link}"> ${Text(_("Verify your identity now."))}</a>
</p>
% endif
</p>
</div>
% if cert_status['status'] == 'generating' or cert_status['status'] == 'downloadable' or cert_status['show_survey_button']:
<div class="wrapper-message-primary">
@@ -92,7 +84,7 @@ else:
<li>
<a class="btn btn-primary" href="${cert_status['cert_web_view_url']}" rel="noopener" target="_blank"
title="${_('This link will open the certificate web view')}">
${_("View {cert_name_short}").format(cert_name_short=cert_name_short,)}
${_("View my {cert_name_short}").format(cert_name_short=cert_name_short,)}
</a>
</li>
% elif cert_status['status'] == 'downloadable' and enrollment.mode in CourseMode.NON_VERIFIED_MODES: