- % if use_v2_cert_display_settings:
-
- % else:
-
- % endif
+
${_("Certificates are awarded at the end of a course run")}
- % if use_v2_cert_display_settings:
-
-
-
-
-
- ${_("Read more about this setting")}
-
+
+
+
+
+
+ ${_("Read more about this setting")}
+
+
+
+
${_("In all configurations of this setting, certificates are generated for learners as soon as they achieve the passing threshold in the course (which can occur before a final assignment based on course design)")}
+
+
${_("Immediately upon passing")}
+
${_("Learners can access their certificate as soon as they achieve a passing grade above the course grade threshold. Note: learners can achieve a passing grade before encountering all assignments in some course configurations.")}
-
-
${_("In all configurations of this setting, certificates are generated for learners as soon as they achieve the passing threshold in the course (which can occur before a final assignment based on course design)")}
-
-
${_("Immediately upon passing")}
-
${_("Learners can access their certificate as soon as they achieve a passing grade above the course grade threshold. Note: learners can achieve a passing grade before encountering all assignments in some course configurations.")}
-
-
-
${_("On course end date")}
-
${_("Learners with passing grades can access their certificate once the end date of the course has elapsed.")}
-
-
-
${_("A date after the course end date")}
-
${_("Learners with passing grades can access their certificate after the date that you set has elapsed.")}
-
+
+
${_("On course end date")}
+
${_("Learners with passing grades can access their certificate once the end date of the course has elapsed.")}
+
+
+
${_("A date after the course end date")}
+
${_("Learners with passing grades can access their certificate after the date that you set has elapsed.")}
- % endif
+
- % if use_v2_cert_display_settings:
-
- % else:
-
- % endif
+
diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py
index fb0a7236c0b5..b5d46949b506 100644
--- a/common/djangoapps/student/helpers.py
+++ b/common/djangoapps/student/helpers.py
@@ -646,21 +646,14 @@ def _is_certificate_earned_but_not_available(course_overview, status):
(bool): True if the user earned the certificate but it's hidden due to display behavior, else False
"""
- if settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS"):
- return (
- not certificates_viewable_for_course(course_overview)
- and CertificateStatuses.is_passing_status(status)
- and course_overview.certificates_display_behavior in (
- CertificatesDisplayBehaviors.END_WITH_DATE,
- CertificatesDisplayBehaviors.END
- )
- )
- else:
- return (
- not certificates_viewable_for_course(course_overview) and
- CertificateStatuses.is_passing_status(status) and
- course_overview.certificate_available_date
+ return (
+ not certificates_viewable_for_course(course_overview)
+ and CertificateStatuses.is_passing_status(status)
+ and course_overview.certificates_display_behavior in (
+ CertificatesDisplayBehaviors.END_WITH_DATE,
+ CertificatesDisplayBehaviors.END
)
+ )
def process_survey_link(survey_link, user):
diff --git a/common/djangoapps/third_party_auth/tests/test_pipeline_integration.py b/common/djangoapps/third_party_auth/tests/test_pipeline_integration.py
index 4bfc710fe901..c19b0b8d96aa 100644
--- a/common/djangoapps/third_party_auth/tests/test_pipeline_integration.py
+++ b/common/djangoapps/third_party_auth/tests/test_pipeline_integration.py
@@ -583,7 +583,7 @@ def test_verification_signal(self):
"""
Verification signal is sent upon approval.
"""
- with mock.patch('openedx_events.learning.signals.IDV_ATTEMPT_APPROVED.send_event') as mock_signal:
+ with mock.patch('openedx.core.djangoapps.signals.signals.LEARNER_SSO_VERIFIED.send_robust') as mock_signal:
# Begin the pipeline.
pipeline.set_id_verification_status(
auth_entry=pipeline.AUTH_ENTRY_LOGIN,
diff --git a/common/static/data/geoip/GeoLite2-Country.mmdb b/common/static/data/geoip/GeoLite2-Country.mmdb
index 2e9d48109648..479048791da5 100644
Binary files a/common/static/data/geoip/GeoLite2-Country.mmdb and b/common/static/data/geoip/GeoLite2-Country.mmdb differ
diff --git a/lms/djangoapps/bulk_email/message_types.py b/lms/djangoapps/bulk_email/message_types.py
index 033f5423e1f7..53c7e92029a5 100644
--- a/lms/djangoapps/bulk_email/message_types.py
+++ b/lms/djangoapps/bulk_email/message_types.py
@@ -12,3 +12,4 @@ class BulkEmail(BaseMessageType):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.options['from_address'] = kwargs['context']['from_address']
+ self.options['transactional'] = True
diff --git a/lms/djangoapps/certificates/api.py b/lms/djangoapps/certificates/api.py
index 4439eeb5f220..bd7db8662e70 100644
--- a/lms/djangoapps/certificates/api.py
+++ b/lms/djangoapps/certificates/api.py
@@ -10,7 +10,6 @@
import logging
from datetime import datetime
-from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
@@ -286,12 +285,9 @@ def certificate_downloadable_status(student, course_key):
course_overview = get_course_overview_or_none(course_key)
- if settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS"):
- display_behavior_is_valid = (
- course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
- )
- else:
- display_behavior_is_valid = True
+ display_behavior_is_valid = (
+ course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
+ )
if (
not certificates_viewable_for_course(course_overview)
@@ -837,10 +833,7 @@ def can_show_certificate_message(course, student, course_grade, certificates_ena
def _course_uses_available_date(course):
"""Returns if the course has an certificate_available_date set and that it should be used"""
- if settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS"):
- display_behavior_is_valid = course.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
- else:
- display_behavior_is_valid = True
+ display_behavior_is_valid = course.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
return (
can_show_certificate_available_date_field(course)
diff --git a/lms/djangoapps/certificates/docs/diagrams/certificate_generation.dsl b/lms/djangoapps/certificates/docs/diagrams/certificate_generation.dsl
index d7ca8fd9a400..e5b66bf3b2ab 100644
--- a/lms/djangoapps/certificates/docs/diagrams/certificate_generation.dsl
+++ b/lms/djangoapps/certificates/docs/diagrams/certificate_generation.dsl
@@ -32,6 +32,8 @@ workspace {
grades_app -> signal_handlers "Emits COURSE_GRADE_NOW_PASSED signal"
verify_student_app -> signal_handlers "Emits IDV_ATTEMPT_APPROVED signal"
+ verify_student_app -> signal_handlers "Emits LEARNER_SSO_VERIFIED signal"
+ verify_student_app -> signal_handlers "Emits PHOTO_VERIFICATION_APPROVED signal"
student_app -> signal_handlers "Emits ENROLLMENT_TRACK_UPDATED signal"
allowlist -> signal_handlers "Emits APPEND_CERTIFICATE_ALLOWLIST signal"
signal_handlers -> generation_handler "Invokes generate_allowlist_certificate()"
diff --git a/lms/djangoapps/certificates/signals.py b/lms/djangoapps/certificates/signals.py
index 53055bf9c86e..39042ff34164 100644
--- a/lms/djangoapps/certificates/signals.py
+++ b/lms/djangoapps/certificates/signals.py
@@ -32,6 +32,8 @@
from openedx.core.djangoapps.signals.signals import (
COURSE_GRADE_NOW_FAILED,
COURSE_GRADE_NOW_PASSED,
+ LEARNER_SSO_VERIFIED,
+ PHOTO_VERIFICATION_APPROVED,
)
from openedx_events.learning.signals import EXAM_ATTEMPT_REJECTED, IDV_ATTEMPT_APPROVED
@@ -117,17 +119,13 @@ def _listen_for_failing_grade(sender, user, course_id, grade, **kwargs): # pyli
log.info(f'Certificate marked not passing for {user.id} : {course_id} via failing grade')
-@receiver(IDV_ATTEMPT_APPROVED, dispatch_uid="learner_track_changed")
-def _listen_for_id_verification_status_changed(sender, signal, **kwargs): # pylint: disable=unused-argument
+def _handle_id_verification_approved(user):
"""
- Listen for a signal indicating that the user's id verification status has changed.
+ Generate a certificate for the user if they are now verified
"""
if not auto_certificate_generation_enabled():
return
- event_data = kwargs.get('idv_attempt')
- user = User.objects.get(id=event_data.user.id)
-
user_enrollments = CourseEnrollment.enrollments_for_user(user=user)
expected_verification_status = IDVerificationService.user_status(user)
expected_verification_status = expected_verification_status['status']
@@ -145,6 +143,25 @@ def _listen_for_id_verification_status_changed(sender, signal, **kwargs): # pyl
)
+@receiver(LEARNER_SSO_VERIFIED, dispatch_uid="sso_learner_verified")
+@receiver(PHOTO_VERIFICATION_APPROVED, dispatch_uid="photo_verification_approved")
+def _listen_for_sso_verification_approved(sender, user, **kwargs): # pylint: disable=unused-argument
+ """
+ Listen for a signal on SSOVerification indicating that the user has been verified.
+ """
+ _handle_id_verification_approved(user)
+
+
+@receiver(IDV_ATTEMPT_APPROVED, dispatch_uid="openedx_idv_attempt_approved")
+def _listen_for_id_verification_approved_event(sender, signal, **kwargs): # pylint: disable=unused-argument
+ """
+ Listen for an openedx event indicating that the user's id verification status has changed.
+ """
+ event_data = kwargs.get('idv_attempt')
+ user = User.objects.get(id=event_data.user.id)
+ _handle_id_verification_approved(user)
+
+
@receiver(ENROLLMENT_TRACK_UPDATED)
def _listen_for_enrollment_mode_change(sender, user, course_key, mode, **kwargs): # pylint: disable=unused-argument
"""
diff --git a/lms/djangoapps/certificates/tests/test_api.py b/lms/djangoapps/certificates/tests/test_api.py
index 21d04a7e3da6..cb11b9e00bd1 100644
--- a/lms/djangoapps/certificates/tests/test_api.py
+++ b/lms/djangoapps/certificates/tests/test_api.py
@@ -209,29 +209,6 @@ def test_with_downloadable_web_cert(self):
"uuid": cert_status["uuid"],
}
- @ddt.data(
- (False, timedelta(days=2), False, True),
- (False, -timedelta(days=2), True, None),
- (True, timedelta(days=2), True, None),
- )
- @ddt.unpack
- @patch.dict(settings.FEATURES, {"CERTIFICATES_HTML_VIEW": True})
- @patch.dict(settings.FEATURES, {"ENABLE_V2_CERT_DISPLAY_SETTINGS": False})
- def test_cert_api_return_v1(self, self_paced, cert_avail_delta, cert_downloadable_status, earned_but_not_available):
- """
- Test 'downloadable status'
- """
- cert_avail_date = datetime.now(pytz.UTC) + cert_avail_delta
- self.course.self_paced = self_paced
- self.course.certificate_available_date = cert_avail_date
- self.course.save()
-
- self._setup_course_certificate()
-
- downloadable_status = certificate_downloadable_status(self.student, self.course.id)
- assert downloadable_status["is_downloadable"] == cert_downloadable_status
- assert downloadable_status.get("earned_but_not_available") == earned_but_not_available
-
@ddt.data(
(True, timedelta(days=2), CertificatesDisplayBehaviors.END_WITH_DATE, True, None),
(False, -timedelta(days=2), CertificatesDisplayBehaviors.EARLY_NO_INFO, True, None),
@@ -243,8 +220,7 @@ def test_cert_api_return_v1(self, self_paced, cert_avail_delta, cert_downloadabl
)
@ddt.unpack
@patch.dict(settings.FEATURES, {"CERTIFICATES_HTML_VIEW": True})
- @patch.dict(settings.FEATURES, {"ENABLE_V2_CERT_DISPLAY_SETTINGS": True})
- def test_cert_api_return_v2(
+ def test_cert_api_return(
self,
self_paced,
cert_avail_delta,
diff --git a/lms/djangoapps/certificates/tests/test_filters.py b/lms/djangoapps/certificates/tests/test_filters.py
index 781b77461f41..a131a1d52582 100644
--- a/lms/djangoapps/certificates/tests/test_filters.py
+++ b/lms/djangoapps/certificates/tests/test_filters.py
@@ -23,7 +23,7 @@
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.certificates.signals import (
_listen_for_enrollment_mode_change,
- _listen_for_id_verification_status_changed,
+ _handle_id_verification_approved,
listen_for_passing_grade
)
from lms.djangoapps.certificates.tests.factories import CertificateAllowlistFactory
@@ -272,7 +272,7 @@ def test_listen_for_passing_grade(self):
mock.Mock(return_value={"status": "approved"})
)
@mock.patch("lms.djangoapps.certificates.api.auto_certificate_generation_enabled", mock.Mock(return_value=True))
- def test_listen_for_id_verification_status_changed(self):
+ def test_handle_id_verification_approved(self):
"""
Test stop certificate generation process after the verification status changed by raising a filters exception.
@@ -280,7 +280,7 @@ def test_listen_for_id_verification_status_changed(self):
- CertificateCreationRequested is triggered and executes TestStopCertificateGenerationStep.
- The certificate is not generated.
"""
- _listen_for_id_verification_status_changed(None, self.user)
+ _handle_id_verification_approved(self.user)
self.assertFalse(
GeneratedCertificate.objects.filter(
diff --git a/lms/djangoapps/certificates/tests/test_utils.py b/lms/djangoapps/certificates/tests/test_utils.py
index 00bb0bbe3ed0..298e624fdd8e 100644
--- a/lms/djangoapps/certificates/tests/test_utils.py
+++ b/lms/djangoapps/certificates/tests/test_utils.py
@@ -5,7 +5,6 @@
from unittest.mock import patch
import ddt
-from django.conf import settings
from django.test import TestCase
from pytz import utc
@@ -80,40 +79,7 @@ def test_has_html_certificates_enabled_from_course_overview_disabled(self):
(CertificatesDisplayBehaviors.END, False, False, _LAST_MONTH, True, True),
)
@ddt.unpack
- @patch.dict(settings.FEATURES, ENABLE_V2_CERT_DISPLAY_SETTINGS=True)
- def test_should_certificate_be_visible_v2(
- self,
- certificates_display_behavior,
- certificates_show_before_end,
- has_ended,
- certificate_available_date,
- self_paced,
- expected_value
- ):
- """Test whether the certificate should be visible to user given multiple usecases"""
- assert should_certificate_be_visible(
- certificates_display_behavior,
- certificates_show_before_end,
- has_ended,
- certificate_available_date,
- self_paced
- ) == expected_value
-
- @ddt.data(
- ('early_with_info', True, True, _LAST_MONTH, False, True),
- ('early_no_info', False, False, _LAST_MONTH, False, True),
- ('end', True, False, _LAST_MONTH, False, True),
- ('end', False, True, _LAST_MONTH, False, True),
- ('end', False, False, _NEXT_WEEK, False, False),
- ('end', False, False, _LAST_WEEK, False, True),
- ('end', False, False, None, False, False),
- ('early_with_info', False, False, None, False, True),
- ('end', False, False, _NEXT_WEEK, False, False),
- ('end', False, False, _NEXT_WEEK, True, True),
- )
- @ddt.unpack
- @patch.dict(settings.FEATURES, ENABLE_V2_CERT_DISPLAY_SETTINGS=False)
- def test_should_certificate_be_visible_v1(
+ def test_should_certificate_be_visible(
self,
certificates_display_behavior,
certificates_show_before_end,
diff --git a/lms/djangoapps/certificates/utils.py b/lms/djangoapps/certificates/utils.py
index 725c54ac09bc..7ff2a4c97b27 100644
--- a/lms/djangoapps/certificates/utils.py
+++ b/lms/djangoapps/certificates/utils.py
@@ -153,30 +153,19 @@ def should_certificate_be_visible(
certificate_available_date (datetime): the date the certificate is available on for the course.
self_paced (bool): Whether the course is self-paced.
"""
- if settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS"):
- show_early = (
- certificates_display_behavior == CertificatesDisplayBehaviors.EARLY_NO_INFO
- or certificates_show_before_end
- )
- past_available_date = (
- certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
- and certificate_available_date
- and certificate_available_date < datetime.now(utc)
- )
- ended_without_available_date = (
- certificates_display_behavior == CertificatesDisplayBehaviors.END
- and has_ended
- )
- else:
- show_early = (
- certificates_display_behavior in ('early_with_info', 'early_no_info')
- or certificates_show_before_end
- )
- past_available_date = (
- certificate_available_date
- and certificate_available_date < datetime.now(utc)
- )
- ended_without_available_date = (certificate_available_date is None) and has_ended
+ show_early = (
+ certificates_display_behavior == CertificatesDisplayBehaviors.EARLY_NO_INFO
+ or certificates_show_before_end
+ )
+ past_available_date = (
+ certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
+ and certificate_available_date
+ and certificate_available_date < datetime.now(utc)
+ )
+ ended_without_available_date = (
+ certificates_display_behavior == CertificatesDisplayBehaviors.END
+ and has_ended
+ )
return any((self_paced, show_early, past_available_date, ended_without_available_date))
diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py
index 40a1f4499305..06e4e8a55337 100644
--- a/lms/djangoapps/certificates/views/webview.py
+++ b/lms/djangoapps/certificates/views/webview.py
@@ -353,28 +353,22 @@ def _get_user_certificate(request, user, course_key, course_overview, preview_mo
if preview_mode:
# certificate is being previewed from studio
if request.user.has_perm(PREVIEW_CERTIFICATES, course_overview):
- if not settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS"):
- if course_overview.certificate_available_date and not course_overview.self_paced:
- modified_date = course_overview.certificate_available_date
- else:
- modified_date = datetime.now().date()
+ if (
+ course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
+ and course_overview.certificate_available_date
+ and not course_overview.self_paced
+ ):
+ modified_date = course_overview.certificate_available_date
+ elif course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END:
+ modified_date = course_overview.end
else:
- if (
- course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE
- and course_overview.certificate_available_date
- and not course_overview.self_paced
- ):
- modified_date = course_overview.certificate_available_date
- elif course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END:
- modified_date = course_overview.end
- else:
- modified_date = datetime.now().date()
- user_certificate = GeneratedCertificate(
- mode=preview_mode,
- verify_uuid=str(uuid4().hex),
- modified_date=modified_date,
- created_date=datetime.now().date(),
- )
+ modified_date = datetime.now().date()
+ user_certificate = GeneratedCertificate(
+ mode=preview_mode,
+ verify_uuid=str(uuid4().hex),
+ modified_date=modified_date,
+ created_date=datetime.now().date(),
+ )
elif certificates_viewable_for_course(course_overview):
# certificate is being viewed by learner or public
try:
diff --git a/lms/djangoapps/commerce/utils.py b/lms/djangoapps/commerce/utils.py
index 82ed8c483024..617852b4f662 100644
--- a/lms/djangoapps/commerce/utils.py
+++ b/lms/djangoapps/commerce/utils.py
@@ -238,6 +238,7 @@ def refund_entitlement(course_entitlement):
return False
+@pluggable_override('OVERRIDE_REFUND_SEAT')
def refund_seat(course_enrollment, change_mode=False):
"""
Attempt to initiate a refund for any orders associated with the seat being unenrolled,
@@ -287,7 +288,7 @@ def refund_seat(course_enrollment, change_mode=False):
user=enrollee,
)
if change_mode:
- _auto_enroll(course_enrollment)
+ auto_enroll(course_enrollment)
else:
log.info('No refund opened for user [%s], course [%s]', enrollee.id, course_key_str)
@@ -354,7 +355,7 @@ def _refund_in_commerce_coordinator(course_enrollment, change_mode):
log.info('Refund successfully sent to Commerce Coordinator for user [%s], course [%s].',
course_enrollment.user_id, course_key_str)
if change_mode:
- _auto_enroll(course_enrollment)
+ auto_enroll(course_enrollment)
return True
else:
# Refund was not meant to be sent to Commerce Coordinator
@@ -363,7 +364,7 @@ def _refund_in_commerce_coordinator(course_enrollment, change_mode):
return False
-def _auto_enroll(course_enrollment):
+def auto_enroll(course_enrollment):
"""
Helper method to update an enrollment to a default course mode.
diff --git a/lms/djangoapps/courseware/toggles.py b/lms/djangoapps/courseware/toggles.py
index 43fb40436a5e..e6070a2e3bd1 100644
--- a/lms/djangoapps/courseware/toggles.py
+++ b/lms/djangoapps/courseware/toggles.py
@@ -148,7 +148,6 @@
# .. toggle_creation_date: 2019-05-16
# .. toggle_expiration_date: None
# .. toggle_tickets: https://github.com/mitodl/edx-platform/issues/123
-# .. toggle_status: unsupported
COURSES_INVITE_ONLY = SettingToggle('COURSES_INVITE_ONLY', default=False)
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index 7adc1ee4cd02..d285b4a63f05 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -3239,26 +3239,31 @@ def post(self, request, course_id):
return JsonResponse(response_payload)
-@transaction.non_atomic_requests
-@ensure_csrf_cookie
-@cache_control(no_cache=True, no_store=True, must_revalidate=True)
-@require_course_permission(permissions.START_CERTIFICATE_GENERATION)
-@require_POST
-@common_exceptions_400
-def start_certificate_generation(request, course_id):
+@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
+@method_decorator(transaction.non_atomic_requests, name='dispatch')
+class StartCertificateGeneration(DeveloperErrorViewMixin, APIView):
"""
Start generating certificates for all students enrolled in given course.
"""
- course_key = CourseKey.from_string(course_id)
- task = task_api.generate_certificates_for_students(request, course_key)
- message = _('Certificate generation task for all students of this course has been started. '
- 'You can view the status of the generation task in the "Pending Tasks" section.')
- response_payload = {
- 'message': message,
- 'task_id': task.task_id
- }
+ permission_classes = (IsAuthenticated, permissions.InstructorPermission)
+ permission_name = permissions.START_CERTIFICATE_GENERATION
- return JsonResponse(response_payload)
+ @method_decorator(ensure_csrf_cookie)
+ @method_decorator(transaction.non_atomic_requests)
+ def post(self, request, course_id):
+ """
+ Generating certificates for all students enrolled in given course.
+ """
+ course_key = CourseKey.from_string(course_id)
+ task = task_api.generate_certificates_for_students(request, course_key)
+ message = _('Certificate generation task for all students of this course has been started. '
+ 'You can view the status of the generation task in the "Pending Tasks" section.')
+ response_payload = {
+ 'message': message,
+ 'task_id': task.task_id
+ }
+
+ return JsonResponse(response_payload)
@transaction.non_atomic_requests
diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py
index 9c01aaa1b596..de9086db53d4 100644
--- a/lms/djangoapps/instructor/views/api_urls.py
+++ b/lms/djangoapps/instructor/views/api_urls.py
@@ -82,7 +82,7 @@
# Certificates
path('enable_certificate_generation', api.enable_certificate_generation, name='enable_certificate_generation'),
- path('start_certificate_generation', api.start_certificate_generation, name='start_certificate_generation'),
+ path('start_certificate_generation', api.StartCertificateGeneration.as_view(), name='start_certificate_generation'),
path('start_certificate_regeneration', api.start_certificate_regeneration, name='start_certificate_regeneration'),
path('certificate_exception_view/', api.certificate_exception_view, name='certificate_exception_view'),
re_path(r'^generate_certificate_exceptions/(?P[^/]*)', api.GenerateCertificateExceptions.as_view(),
diff --git a/lms/djangoapps/learner_home/serializers.py b/lms/djangoapps/learner_home/serializers.py
index 3d156f3640ca..c464e0c6a443 100644
--- a/lms/djangoapps/learner_home/serializers.py
+++ b/lms/djangoapps/learner_home/serializers.py
@@ -299,23 +299,20 @@ def get_cert_info(self, enrollment):
def get_availableDate(self, enrollment):
"""Available date changes based off of Certificate display behavior"""
course_overview = enrollment.course_overview
- available_date = course_overview.certificate_available_date
-
- if settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS", False):
- if (
- course_overview.certificates_display_behavior
- == CertificatesDisplayBehaviors.END_WITH_DATE
- and course_overview.certificate_available_date
- ):
- available_date = course_overview.certificate_available_date
- elif (
- course_overview.certificates_display_behavior
- == CertificatesDisplayBehaviors.END
- and course_overview.end
- ):
- available_date = course_overview.end
- else:
+ available_date = None
+
+ if (
+ course_overview.certificates_display_behavior
+ == CertificatesDisplayBehaviors.END_WITH_DATE
+ and course_overview.certificate_available_date
+ ):
available_date = course_overview.certificate_available_date
+ elif (
+ course_overview.certificates_display_behavior
+ == CertificatesDisplayBehaviors.END
+ and course_overview.end
+ ):
+ available_date = course_overview.end
return serializers.DateTimeField().to_representation(available_date)
diff --git a/lms/djangoapps/learner_home/test_serializers.py b/lms/djangoapps/learner_home/test_serializers.py
index ac11a8b2990d..e7d22d5e71e2 100644
--- a/lms/djangoapps/learner_home/test_serializers.py
+++ b/lms/djangoapps/learner_home/test_serializers.py
@@ -522,9 +522,12 @@ def test_with_data(self):
# ... with a certificate
input_context = self.create_test_context(input_data.course)
- # ... and some data preemptively gathered
+ # ... and some data preemptively gathered, including a certificate display behavior
available_date = random_date()
input_data.course.certificate_available_date = available_date
+ input_data.course.certificates_display_behavior = (
+ CertificatesDisplayBehaviors.END_WITH_DATE
+ )
cert_url = input_context["cert_statuses"][input_data.course.id][
"cert_web_view_url"
]
@@ -544,23 +547,6 @@ def test_with_data(self):
},
)
- @mock.patch.dict(settings.FEATURES, ENABLE_V2_CERT_DISPLAY_SETTINGS=False)
- def test_available_date_old_format(self):
- # Given new cert display settings are not enabled
- input_data = self.create_test_enrollment(course_mode=CourseMode.VERIFIED)
- input_data.course.certificate_available_date = random_date()
- input_context = self.create_test_context(input_data.course)
-
- # When I get certificate info
- output_data = CertificateSerializer(input_data, context=input_context).data
-
- # Then the available date is defaulted to the certificate available date
- expected_available_date = datetime_to_django_format(
- input_data.course.certificate_available_date
- )
- self.assertEqual(output_data["availableDate"], expected_available_date)
-
- @mock.patch.dict(settings.FEATURES, ENABLE_V2_CERT_DISPLAY_SETTINGS=True)
def test_available_date_course_end(self):
# Given new cert display settings are enabled
input_data = self.create_test_enrollment(course_mode=CourseMode.VERIFIED)
@@ -578,7 +564,6 @@ def test_available_date_course_end(self):
expected_available_date = datetime_to_django_format(input_data.course.end)
self.assertEqual(output_data["availableDate"], expected_available_date)
- @mock.patch.dict(settings.FEATURES, ENABLE_V2_CERT_DISPLAY_SETTINGS=True)
def test_available_date_specific_end(self):
# Given new cert display settings are enabled
input_data = self.create_test_enrollment(course_mode=CourseMode.VERIFIED)
diff --git a/lms/djangoapps/verify_student/management/commands/tests/test_backfill_sso_verifications_for_old_account_links.py b/lms/djangoapps/verify_student/management/commands/tests/test_backfill_sso_verifications_for_old_account_links.py
index 891ff9fda5d8..d9f356758dd8 100644
--- a/lms/djangoapps/verify_student/management/commands/tests/test_backfill_sso_verifications_for_old_account_links.py
+++ b/lms/djangoapps/verify_student/management/commands/tests/test_backfill_sso_verifications_for_old_account_links.py
@@ -54,7 +54,7 @@ def test_performance(self):
#self.assertNumQueries(100)
def test_signal_called(self):
- with patch('openedx_events.learning.signals.IDV_ATTEMPT_APPROVED.send_event') as mock_signal:
+ with patch('openedx.core.djangoapps.signals.signals.LEARNER_SSO_VERIFIED.send_robust') as mock_signal:
call_command('backfill_sso_verifications_for_old_account_links', '--provider-slug', self.provider.provider_id) # lint-amnesty, pylint: disable=line-too-long
assert mock_signal.call_count == 1
diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py
index 9a0ac369640a..ae00ca8a1c57 100644
--- a/lms/djangoapps/verify_student/models.py
+++ b/lms/djangoapps/verify_student/models.py
@@ -44,9 +44,8 @@
rsa_decrypt,
rsa_encrypt
)
+from openedx.core.djangoapps.signals.signals import LEARNER_SSO_VERIFIED, PHOTO_VERIFICATION_APPROVED
from openedx.core.storage import get_storage
-from openedx_events.learning.signals import IDV_ATTEMPT_APPROVED
-from openedx_events.learning.data import UserData, VerificationAttemptData
from .utils import auto_verify_for_testing_enabled, earliest_allowed_verification_date, submit_request_to_ss
@@ -251,23 +250,13 @@ def send_approval_signal(self, approved_by='None'):
user_id=self.user, reviewer=approved_by
))
- # Emit event to find and generate eligible certificates
- verification_data = VerificationAttemptData(
- attempt_id=self.id,
- user=UserData(
- pii=None,
- id=self.user.id,
- is_active=self.user.is_active,
- ),
- status=self.status,
- name=self.name,
- expiration_date=self.expiration_datetime,
- )
- IDV_ATTEMPT_APPROVED.send_event(
- idv_attempt=verification_data,
+ # Emit signal to find and generate eligible certificates
+ LEARNER_SSO_VERIFIED.send_robust(
+ sender=PhotoVerification,
+ user=self.user,
)
- message = 'IDV_ATTEMPT_APPROVED signal fired for {user} from SSOVerification'
+ message = 'LEARNER_SSO_VERIFIED signal fired for {user} from SSOVerification'
log.info(message.format(user=self.user.username))
@@ -465,23 +454,13 @@ def approve(self, user_id=None, service=""):
)
self.save()
- # Emit event to find and generate eligible certificates
- verification_data = VerificationAttemptData(
- attempt_id=self.id,
- user=UserData(
- pii=None,
- id=self.user.id,
- is_active=self.user.is_active,
- ),
- status=self.status,
- name=self.name,
- expiration_date=self.expiration_datetime,
- )
- IDV_ATTEMPT_APPROVED.send_event(
- idv_attempt=verification_data,
+ # Emit signal to find and generate eligible certificates
+ PHOTO_VERIFICATION_APPROVED.send_robust(
+ sender=PhotoVerification,
+ user=self.user,
)
- message = 'IDV_ATTEMPT_APPROVED signal fired for {user} from PhotoVerification'
+ message = 'PHOTO_VERIFICATION_APPROVED signal fired for {user} from PhotoVerification'
log.info(message.format(user=self.user.username))
@status_before_must_be("ready", "must_retry")
diff --git a/lms/envs/common.py b/lms/envs/common.py
index f2bcfa822b6e..df8878f26914 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -949,17 +949,6 @@
# .. toggle_tickets: 'https://openedx.atlassian.net/browse/OSPR-5290'
'ENABLE_BULK_USER_RETIREMENT': False,
- # .. toggle_name: FEATURES['ENABLE_V2_CERT_DISPLAY_SETTINGS']
- # .. toggle_implementation: DjangoSetting
- # .. toggle_default: False
- # .. toggle_description: Whether to use the reimagined certificates_display_behavior and certificate_available_date
- # .. settings. Will eventually become the default.
- # .. toggle_use_cases: temporary
- # .. toggle_creation_date: 2021-07-26
- # .. toggle_target_removal_date: 2021-10-01
- # .. toggle_tickets: 'https://openedx.atlassian.net/browse/MICROBA-1405'
- 'ENABLE_V2_CERT_DISPLAY_SETTINGS': False,
-
# .. toggle_name: FEATURES['ENABLE_INTEGRITY_SIGNATURE']
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
diff --git a/lms/templates/dashboard/_dashboard_certificate_information.html b/lms/templates/dashboard/_dashboard_certificate_information.html
index 5f3436a7f718..5e8225bdcb8b 100644
--- a/lms/templates/dashboard/_dashboard_certificate_information.html
+++ b/lms/templates/dashboard/_dashboard_certificate_information.html
@@ -44,14 +44,11 @@
<%
- if settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS", False):
- certificate_available_date_string = ""
- if course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE and course_overview.certificate_available_date:
- certificate_available_date_string = course_overview.certificate_available_date.strftime('%Y-%m-%dT%H:%M:%S%z')
- elif course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END and course_overview.end:
- certificate_available_date_string = course_overview.end.strftime('%Y-%m-%dT%H:%M:%S%z')
- else:
+ certificate_available_date_string = ""
+ if course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END_WITH_DATE and course_overview.certificate_available_date:
certificate_available_date_string = course_overview.certificate_available_date.strftime('%Y-%m-%dT%H:%M:%S%z')
+ elif course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.END and course_overview.end:
+ certificate_available_date_string = course_overview.end.strftime('%Y-%m-%dT%H:%M:%S%z')
container_string = _("Your grade and certificate will be ready after {date}.")
format = 'shortDate'
%>
diff --git a/openedx/core/djangoapps/models/course_details.py b/openedx/core/djangoapps/models/course_details.py
index d8d5deae0110..c00d7d0b8816 100644
--- a/openedx/core/djangoapps/models/course_details.py
+++ b/openedx/core/djangoapps/models/course_details.py
@@ -371,10 +371,6 @@ def validate_certificate_settings(cls, certificate_available_date, certificates_
tuple[str, str]: updated certificate_available_date, updated certificates_display_behavior
None
"""
- # If V2 is not enable, return original values
- if not settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS", False):
- return (certificate_available_date, certificates_display_behavior)
-
# "early_no_info" will always show regardless of settings
if certificates_display_behavior == CertificatesDisplayBehaviors.EARLY_NO_INFO:
return (None, CertificatesDisplayBehaviors.EARLY_NO_INFO)
diff --git a/openedx/core/djangoapps/models/tests/test_course_details.py b/openedx/core/djangoapps/models/tests/test_course_details.py
index 86301740c7f1..41e739ecb4c8 100644
--- a/openedx/core/djangoapps/models/tests/test_course_details.py
+++ b/openedx/core/djangoapps/models/tests/test_course_details.py
@@ -8,7 +8,6 @@
import pytest
import ddt
from pytz import UTC
-from unittest.mock import patch # lint-amnesty, pylint: disable=wrong-import-order
from django.conf import settings
from xmodule.modulestore import ModuleStoreEnum
@@ -212,30 +211,7 @@ def test_fetch_video(self):
),
)
@ddt.unpack
- @patch.dict(settings.FEATURES, ENABLE_V2_CERT_DISPLAY_SETTINGS=True)
- def test_validate_certificate_settings_v2(self, stored_date, stored_behavior, expected_date, expected_behavior):
- assert CourseDetails.validate_certificate_settings(
- stored_date, stored_behavior
- ) == (expected_date, expected_behavior)
-
- @ddt.data(
- (
- EXAMPLE_CERTIFICATE_AVAILABLE_DATE,
- CertificatesDisplayBehaviors.END_WITH_DATE,
- EXAMPLE_CERTIFICATE_AVAILABLE_DATE,
- CertificatesDisplayBehaviors.END_WITH_DATE
- ),
- (
- None,
- "invalid_option",
- None,
- "invalid_option"
- ),
- )
- @ddt.unpack
- @patch.dict(settings.FEATURES, ENABLE_V2_CERT_DISPLAY_SETTINGS=False)
- def test_validate_certificate_settings_v1(self, stored_date, stored_behavior, expected_date, expected_behavior):
- """Test that method just returns passed in arguments if v2 is off"""
+ def test_validate_certificate_settings(self, stored_date, stored_behavior, expected_date, expected_behavior):
assert CourseDetails.validate_certificate_settings(
stored_date, stored_behavior
) == (expected_date, expected_behavior)
diff --git a/openedx/core/djangoapps/signals/signals.py b/openedx/core/djangoapps/signals/signals.py
index 495389152f7a..24624464ab91 100644
--- a/openedx/core/djangoapps/signals/signals.py
+++ b/openedx/core/djangoapps/signals/signals.py
@@ -36,5 +36,16 @@
# ]
COURSE_GRADE_NOW_FAILED = Signal()
+# Signal that indicates that a user has become verified via SSO for certificate purposes
+# providing_args=['user']
+LEARNER_SSO_VERIFIED = Signal()
+
+# Signal that indicates a user has been verified via verify_studnet.PhotoVerification for certificate purposes
+# Please note that this signal and the corresponding PhotoVerification model are planned for deprecation.
+# Future implementations of IDV will use the verify_student.VerificationAttempt model and corresponding
+# openedx events.
+# DEPR: https://github.com/openedx/edx-platform/issues/35128
+PHOTO_VERIFICATION_APPROVED = Signal()
+
# providing_args=['user']
USER_ACCOUNT_ACTIVATED = Signal() # Signal indicating email verification
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 5a6074093eeb..46d8fb4192e7 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -26,7 +26,7 @@ celery>=5.2.2,<6.0.0
# The team that owns this package will manually bump this package rather than having it pulled in automatically.
# This is to allow them to better control its deployment and to do it in a process that works better
# for them.
-edx-enterprise==4.26.0
+edx-enterprise==4.26.1
# Stay on LTS version, remove once this is added to common constraint
Django<5.0
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 72333a45814f..693d7bc73216 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -467,7 +467,7 @@ edx-drf-extensions==10.4.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.26.0
+edx-enterprise==4.26.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
@@ -502,7 +502,7 @@ edx-opaque-keys[django]==2.11.0
# ora2
edx-organizations==6.13.0
# via -r requirements/edx/kernel.in
-edx-proctoring==4.18.1
+edx-proctoring==4.18.2
# via
# -r requirements/edx/kernel.in
# edx-proctoring-proctortrack
diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt
index 53004d094820..bb2e335a9f93 100644
--- a/requirements/edx/development.txt
+++ b/requirements/edx/development.txt
@@ -741,7 +741,7 @@ edx-drf-extensions==10.4.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.26.0
+edx-enterprise==4.26.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
@@ -790,7 +790,7 @@ edx-organizations==6.13.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-edx-proctoring==4.18.1
+edx-proctoring==4.18.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt
index dcef8359eccd..74453bc3659e 100644
--- a/requirements/edx/doc.txt
+++ b/requirements/edx/doc.txt
@@ -547,7 +547,7 @@ edx-drf-extensions==10.4.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.26.0
+edx-enterprise==4.26.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -581,7 +581,7 @@ edx-opaque-keys[django]==2.11.0
# ora2
edx-organizations==6.13.0
# via -r requirements/edx/base.txt
-edx-proctoring==4.18.1
+edx-proctoring==4.18.2
# via
# -r requirements/edx/base.txt
# edx-proctoring-proctortrack
diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt
index 05c761338b45..2a9a03160379 100644
--- a/requirements/edx/testing.txt
+++ b/requirements/edx/testing.txt
@@ -571,7 +571,7 @@ edx-drf-extensions==10.4.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.26.0
+edx-enterprise==4.26.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -607,7 +607,7 @@ edx-opaque-keys[django]==2.11.0
# ora2
edx-organizations==6.13.0
# via -r requirements/edx/base.txt
-edx-proctoring==4.18.1
+edx-proctoring==4.18.2
# via
# -r requirements/edx/base.txt
# edx-proctoring-proctortrack
diff --git a/xmodule/course_block.py b/xmodule/course_block.py
index 46ad7476f6d1..5b1f92d77734 100644
--- a/xmodule/course_block.py
+++ b/xmodule/course_block.py
@@ -1115,9 +1115,6 @@ def __init__(self, *args, **kwargs):
except InvalidTabsException as err:
raise type(err)(f'{str(err)} For course: {str(self.id)}') # lint-amnesty, pylint: disable=line-too-long
- if not settings.FEATURES.get("ENABLE_V2_CERT_DISPLAY_SETTINGS"):
- self.set_default_certificate_available_date()
-
def set_grading_policy(self, course_policy):
"""
The JSON object can have the keys GRADER and GRADE_CUTOFFS. If either is