diff --git a/lms/djangoapps/certificates/api.py b/lms/djangoapps/certificates/api.py index 9e2511a0a2..c88c656391 100644 --- a/lms/djangoapps/certificates/api.py +++ b/lms/djangoapps/certificates/api.py @@ -852,8 +852,15 @@ def display_date_for_certificate(course, certificate): Returns: datetime.date """ + try: + if certificate.date_override: + return certificate.date_override.date + except ObjectDoesNotExist: + pass + if _course_uses_available_date(course) and course.certificate_available_date < datetime.now(UTC): return course.certificate_available_date + return certificate.modified_date diff --git a/lms/djangoapps/certificates/tests/factories.py b/lms/djangoapps/certificates/tests/factories.py index c0c0e92fb7..10e6d747db 100644 --- a/lms/djangoapps/certificates/tests/factories.py +++ b/lms/djangoapps/certificates/tests/factories.py @@ -3,6 +3,7 @@ Certificates factories """ +import datetime from uuid import uuid4 from factory.django import DjangoModelFactory @@ -10,6 +11,7 @@ from factory.django import DjangoModelFactory from common.djangoapps.student.models import LinkedInAddToProfileConfiguration from lms.djangoapps.certificates.models import ( CertificateAllowlist, + CertificateDateOverride, CertificateHtmlViewConfiguration, CertificateInvalidation, CertificateStatuses, @@ -104,3 +106,14 @@ class LinkedInAddToProfileConfigurationFactory(DjangoModelFactory): enabled = True company_identifier = "1337" + + +class CertificateDateOverrideFactory(DjangoModelFactory): + """ + CertificateDateOverride factory + """ + class Meta: + model = CertificateDateOverride + + date = datetime.datetime(2021, 5, 11) + reason = "Learner really wanted this on their birthday" diff --git a/lms/djangoapps/certificates/tests/test_api.py b/lms/djangoapps/certificates/tests/test_api.py index 3f44192918..fadf0c89ee 100644 --- a/lms/djangoapps/certificates/tests/test_api.py +++ b/lms/djangoapps/certificates/tests/test_api.py @@ -1131,6 +1131,7 @@ class MockGeneratedCertificate: self.status = status self.created_date = datetime.now(pytz.UTC) self.modified_date = datetime.now(pytz.UTC) + self.date_override = None def is_valid(self): """ @@ -1139,6 +1140,11 @@ class MockGeneratedCertificate: return self.status == CertificateStatuses.downloadable +class MockCertificateDateOverride: + def __init__(self, date=None): + self.date = date or datetime.now(pytz.UTC) + + @contextmanager def configure_waffle_namespace(feature_enabled): """ @@ -1221,6 +1227,12 @@ class CertificatesApiTestCase(TestCase): assert maybe_avail == available_date_for_certificate(self.course, self.certificate) assert self.certificate.modified_date == display_date_for_certificate(self.course, self.certificate) + # With a certificate date override, display date returns the override, available date ignores it + self.certificate.date_override = MockCertificateDateOverride() + date = self.certificate.date_override.date + assert date == display_date_for_certificate(self.course, self.certificate) + assert maybe_avail == available_date_for_certificate(self.course, self.certificate) + @ddt.ddt class CertificatesMessagingTestCase(ModuleStoreTestCase): diff --git a/lms/djangoapps/certificates/tests/test_webview_views.py b/lms/djangoapps/certificates/tests/test_webview_views.py index 07a993d22a..1797384fd3 100644 --- a/lms/djangoapps/certificates/tests/test_webview_views.py +++ b/lms/djangoapps/certificates/tests/test_webview_views.py @@ -38,6 +38,7 @@ from lms.djangoapps.certificates.models import ( GeneratedCertificate ) from lms.djangoapps.certificates.tests.factories import ( + CertificateDateOverrideFactory, CertificateHtmlViewConfigurationFactory, GeneratedCertificateFactory, LinkedInAddToProfileConfigurationFactory @@ -237,6 +238,15 @@ class CommonCertificatesTestCase(ModuleStoreTestCase): ) template.save() + def _add_certificate_date_override(self): + """ + Creates a mock CertificateDateOverride and adds it to the certificate + """ + self.cert.date_override = CertificateDateOverrideFactory.create( + generated_certificate=self.cert, + overridden_by=self.user, + ) + @ddt.ddt class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase): @@ -646,10 +656,13 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) response = self.client.get(test_url) self.assertContains(response, '') + @ddt.data(False, True) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - def test_html_view_for_non_viewable_certificate_and_for_student_user(self): + def test_html_view_for_non_viewable_certificate_and_for_student_user(self, date_override): """ - Tests that Certificate HTML Web View returns "Cannot Find Certificate" if certificate is not viewable yet. + Tests that Certificate HTML Web View returns "Cannot Find Certificate" + if certificate is not viewable yet, regardless of certificate date + override """ test_certificates = [ { @@ -660,6 +673,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) 'is_active': True } ] + + # A certificate with an available date in the future should not be + # viewable, regardless of the date override. + if date_override: + self._add_certificate_date_override() + self.course.certificates = {'certificates': test_certificates} self.course.cert_html_view_enabled = True self.course.certificate_available_date = datetime.datetime.today() + datetime.timedelta(days=1) @@ -945,6 +964,54 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) ) self.assertContains(response, date) + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + @ddt.data( + (True, False), + (False, False), + (True, True), + (False, True) + ) + @ddt.unpack + def test_html_view_certificate_display_date(self, self_paced, date_override): + """ + Test certificate web view should display the correct date on the + certificate in all cases: + * self-paced, no date override + * instructor-paced with certificate_available_date + * self-paced with date override + * instructor-paced with date override + """ + self.course.self_paced = self_paced + if date_override: + self._add_certificate_date_override() + today = datetime.datetime.utcnow() + self.course.certificate_available_date = today + datetime.timedelta(-2) + self.store.update_item(self.course, self.user.id) + self._add_course_certificates(count=1, signatory_count=1, is_active=True) + test_url = get_certificate_url( + user_id=self.user.id, + course_id=str(self.course.id), + uuid=self.cert.verify_uuid + ) + + with override_waffle_switch(AUTO_CERTIFICATE_GENERATION, active=True): + response = self.client.get(test_url) + + if date_override: + expected_date = self.cert.date_override.date + elif self_paced or self.course.certificate_available_date > today: + expected_date = today + else: + expected_date = self.course.certificate_available_date + + date = '{month} {day}, {year}'.format( + month=strftime_localized(expected_date, "%B"), + day=expected_date.day, + year=expected_date.year + ) + + self.assertContains(response, date) + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_render_html_view_invalid_certificate_configuration(self): self.course.cert_html_view_enabled = True