Skip to content

Commit

Permalink
Merge branch 'master' into hamza/ENT-9440-samlproviderconfig-history-…
Browse files Browse the repository at this point in the history
…tracking
  • Loading branch information
hamzawaleed01 authored Oct 7, 2024
2 parents 294dcbb + c34ccff commit 36a413a
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()"
Expand Down
29 changes: 23 additions & 6 deletions lms/djangoapps/certificates/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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']
Expand All @@ -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
"""
Expand Down
6 changes: 3 additions & 3 deletions lms/djangoapps/certificates/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -272,15 +272,15 @@ 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.
Expected result:
- 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(
Expand Down
37 changes: 21 additions & 16 deletions lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lms/djangoapps/instructor/views/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<generate_for>[^/]*)', api.generate_certificate_exceptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
43 changes: 11 additions & 32 deletions lms/djangoapps/verify_student/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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))


Expand Down Expand Up @@ -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")
Expand Down
11 changes: 11 additions & 0 deletions openedx/core/djangoapps/signals/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 36a413a

Please sign in to comment.