From ecba21dcc0823b716fa6d6696419d3897ddc2035 Mon Sep 17 00:00:00 2001 From: oliviaruizknott Date: Wed, 11 Aug 2021 15:13:13 -0600 Subject: [PATCH] feat: Display date override on certificate If the certificate has an associated certificate date override, display that date on the certificate instead of any other date. The date override should not affect whether or not the certificate is visible / available; only the date displayed on the certificate. --- lms/djangoapps/certificates/api.py | 7 ++ .../certificates/tests/factories.py | 13 ++++ lms/djangoapps/certificates/tests/test_api.py | 12 ++++ .../certificates/tests/test_webview_views.py | 71 ++++++++++++++++++- 4 files changed, 101 insertions(+), 2 deletions(-) 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