From eb346f1ab24eb03132a60530f95fcc27c99e5fe3 Mon Sep 17 00:00:00 2001
From: "Albert (AJ) St. Aubin"
Date: Fri, 4 Jun 2021 11:44:30 -0400
Subject: [PATCH] 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.
---
.../student/tests/test_certificates.py | 2 +-
common/djangoapps/student/tests/test_views.py | 114 ++++++++++--------
common/djangoapps/util/course.py | 14 ---
.../course_home_api/outline/v1/serializers.py | 2 +
.../course_home_api/outline/v1/views.py | 4 +
lms/static/sass/multicourse/_dashboard.scss | 9 +-
.../_dashboard_certificate_information.html | 26 ++--
7 files changed, 85 insertions(+), 86 deletions(-)
diff --git a/common/djangoapps/student/tests/test_certificates.py b/common/djangoapps/student/tests/test_certificates.py
index ba71a96a85..68935bd93d 100644
--- a/common/djangoapps/student/tests/test_certificates.py
+++ b/common/djangoapps/student/tests/test_certificates.py
@@ -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)
diff --git a/common/djangoapps/student/tests/test_views.py b/common/djangoapps/student/tests/test_views.py
index ffd0cdaf2a..3ee0dbd7c7 100644
--- a/common/djangoapps/student/tests/test_views.py
+++ b/common/djangoapps/student/tests/test_views.py
@@ -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, '45%')
-
- 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, '45%')
+ 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'))
diff --git a/common/djangoapps/util/course.py b/common/djangoapps/util/course.py
index fe6721499a..1a894003e0 100644
--- a/common/djangoapps/util/course.py
+++ b/common/djangoapps/util/course.py
@@ -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
diff --git a/lms/djangoapps/course_home_api/outline/v1/serializers.py b/lms/djangoapps/course_home_api/outline/v1/serializers.py
index 56991e2aee..1f16d83566 100644
--- a/lms/djangoapps/course_home_api/outline/v1/serializers.py
+++ b/lms/djangoapps/course_home_api/outline/v1/serializers.py
@@ -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)
diff --git a/lms/djangoapps/course_home_api/outline/v1/views.py b/lms/djangoapps/course_home_api/outline/v1/views.py
index 5f90f99b67..0b81d36af3 100644
--- a/lms/djangoapps/course_home_api/outline/v1/views.py
+++ b/lms/djangoapps/course_home_api/outline/v1/views.py
@@ -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,
diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss
index 6f5f6fa4f6..c5ab1c49b3 100644
--- a/lms/static/sass/multicourse/_dashboard.scss
+++ b/lms/static/sass/multicourse/_dashboard.scss
@@ -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);
diff --git a/lms/templates/dashboard/_dashboard_certificate_information.html b/lms/templates/dashboard/_dashboard_certificate_information.html
index 6b5856f3ed..3c2f988355 100644
--- a/lms/templates/dashboard/_dashboard_certificate_information.html
+++ b/lms/templates/dashboard/_dashboard_certificate_information.html
@@ -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:
% else:
-
-
- % if should_display_grade(course_overview):
- ${_("Your final grade:")}
- ${"{0:.0f}%".format(float(cert_status['grade'])*100)}.
- % 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':
+
+
+ % 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:
${Text(_("Verify your identity now."))}
% endif
-
-
+
% if cert_status['status'] == 'generating' or cert_status['status'] == 'downloadable' or cert_status['show_survey_button']: