Skip to content

Commit

Permalink
Merge branch 'master' into sameeramin/upgrade-edx-enterprise-1103544
Browse files Browse the repository at this point in the history
  • Loading branch information
sameeramin committed Sep 19, 2024
2 parents 448a213 + ca11c14 commit 0f64e16
Show file tree
Hide file tree
Showing 42 changed files with 1,052 additions and 128 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.core.djangoapps.signals.signals.LEARNER_NOW_VERIFIED.send_robust') as mock_signal:
with mock.patch('openedx_events.learning.signals.IDV_ATTEMPT_APPROVED.send_event') as mock_signal:
# Begin the pipeline.
pipeline.set_id_verification_status(
auth_entry=pipeline.AUTH_ENTRY_LOGIN,
Expand Down
402 changes: 402 additions & 0 deletions docs/decisions/0020-upstream-downstream.rst

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ workspace {
}

grades_app -> signal_handlers "Emits COURSE_GRADE_NOW_PASSED signal"
verify_student_app -> signal_handlers "Emits LEARNER_NOW_VERIFIED signal"
verify_student_app -> signal_handlers "Emits IDV_ATTEMPT_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
10 changes: 6 additions & 4 deletions lms/djangoapps/certificates/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@
from openedx.core.djangoapps.signals.signals import (
COURSE_GRADE_NOW_FAILED,
COURSE_GRADE_NOW_PASSED,
LEARNER_NOW_VERIFIED
)
from openedx_events.learning.signals import EXAM_ATTEMPT_REJECTED
from openedx_events.learning.signals import EXAM_ATTEMPT_REJECTED, IDV_ATTEMPT_APPROVED

User = get_user_model()

Expand Down Expand Up @@ -118,14 +117,17 @@ 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(LEARNER_NOW_VERIFIED, dispatch_uid="learner_track_changed")
def _listen_for_id_verification_status_changed(sender, user, **kwargs): # pylint: disable=unused-argument
@receiver(IDV_ATTEMPT_APPROVED, dispatch_uid="learner_track_changed")
def _listen_for_id_verification_status_changed(sender, signal, **kwargs): # pylint: disable=unused-argument
"""
Listen for a signal indicating that the user's id verification status has changed.
"""
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 Down
19 changes: 12 additions & 7 deletions lms/djangoapps/certificates/tests/test_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,20 @@
from openedx_events.data import EventsMetadata
from openedx_events.learning.data import ExamAttemptData, UserData, UserPersonalData
from openedx_events.learning.signals import EXAM_ATTEMPT_REJECTED
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from openedx_events.tests.utils import OpenEdxEventsTestMixin

from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from lms.djangoapps.certificates.api import has_self_generated_certificates_enabled
from lms.djangoapps.certificates.config import AUTO_CERTIFICATE_GENERATION
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.models import (
CertificateGenerationConfiguration,
GeneratedCertificate
)
from lms.djangoapps.certificates.models import CertificateGenerationConfiguration, GeneratedCertificate
from lms.djangoapps.certificates.signals import handle_exam_attempt_rejected_event
from lms.djangoapps.certificates.tests.factories import CertificateAllowlistFactory, GeneratedCertificateFactory
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from lms.djangoapps.grades.tests.utils import mock_passing_grade
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory


class SelfGeneratedCertsSignalTest(ModuleStoreTestCase):
Expand Down Expand Up @@ -302,10 +300,17 @@ def test_failing_grade_allowlist(self):
assert cert.status == CertificateStatuses.downloadable


class LearnerIdVerificationTest(ModuleStoreTestCase):
class LearnerIdVerificationTest(ModuleStoreTestCase, OpenEdxEventsTestMixin):
"""
Tests for certificate generation task firing on learner id verification
"""
ENABLED_OPENEDX_EVENTS = ['org.openedx.learning.idv_attempt.approved.v1']

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.start_events_isolation()

def setUp(self):
super().setUp()
self.course_one = CourseFactory.create(self_paced=True)
Expand Down
27 changes: 19 additions & 8 deletions lms/djangoapps/instructor/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4704,15 +4704,19 @@ class TestOauthInstructorAPILevelsAccess(SharedModuleStoreTestCase, LoginEnrollm
Test endpoints using Oauth2 authentication.
"""

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course = CourseFactory.create(
entrance_exam_id='i4x://{}/{}/chapter/Entrance_exam'.format('test_org', 'test_course')
)

def setUp(self):
super().setUp()
self.course = CourseFactory.create(
org='test_org',
course='test_course',
run='test_run',
entrance_exam_id='i4x://{}/{}/chapter/Entrance_exam'.format('test_org', 'test_course')
)
self.problem_location = msk_from_problem_urlname(
self.course.id,
'robot-some-problem-urlname'
)
self.problem_urlname = str(self.problem_location)

self.other_user = UserFactory()
dot_application = ApplicationFactory(user=self.other_user, authorization_grant_type='password')
Expand Down Expand Up @@ -4744,7 +4748,14 @@ def setUp(self):
"send-to": ["myself"],
"subject": "This is subject",
"message": "message"
}, 'data_researcher')
}, 'data_researcher'),
('list_instructor_tasks',
{
'problem_location_str': self.problem_urlname,
'unique_student_identifier': self.other_user.email
},
'data_researcher'),
('list_instructor_tasks', {}, 'data_researcher')
]

self.fake_jwt = ('wyJUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjaGFuZ2UtbWUiLCJleHAiOjE3MjU4OTA2NzIsImdyY'
Expand Down
40 changes: 31 additions & 9 deletions lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
from lms.djangoapps.instructor_task.models import ReportStore
from lms.djangoapps.instructor.views.serializer import (
AccessSerializer, BlockDueDateSerializer, RoleNameSerializer, ShowStudentExtensionSerializer, UserSerializer,
SendEmailSerializer, StudentAttemptsSerializer
SendEmailSerializer, StudentAttemptsSerializer, ListInstructorTaskInputSerializer
)
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, is_course_cohorted
Expand Down Expand Up @@ -2373,9 +2373,8 @@ def get(self, request, course_id):
return _list_instructor_tasks(request=request, course_id=course_id)


@require_POST
@ensure_csrf_cookie
def list_instructor_tasks(request, course_id):
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
class ListInstructorTasks(APIView):
"""
List instructor tasks.
Expand All @@ -2385,21 +2384,44 @@ def list_instructor_tasks(request, course_id):
- `problem_location_str` and `unique_student_identifier` lists task
history for problem AND student (intersection)
"""
return _list_instructor_tasks(request=request, course_id=course_id)
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
permission_name = permissions.SHOW_TASKS
serializer_class = ListInstructorTaskInputSerializer

@method_decorator(ensure_csrf_cookie)
def post(self, request, course_id):
"""
List instructor tasks.
"""
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)

return _list_instructor_tasks(
request=request, course_id=course_id, serialize_data=serializer.validated_data
)


@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_course_permission(permissions.SHOW_TASKS)
def _list_instructor_tasks(request, course_id):
def _list_instructor_tasks(request, course_id, serialize_data=None):
"""
List instructor tasks.
Internal function with common code for both DRF and and tradition views.
"""
# This method is also used by other APIs with the GET method.
# The query_params attribute is utilized for GET requests,
# where parameters are passed as query strings.

course_id = CourseKey.from_string(course_id)
params = getattr(request, 'query_params', request.POST)
problem_location_str = strip_if_string(params.get('problem_location_str', False))
student = params.get('unique_student_identifier', None)
if serialize_data is not None:
problem_location_str = strip_if_string(serialize_data.get('problem_location_str', False))
student = serialize_data.get('unique_student_identifier', None)
else:
params = getattr(request, 'query_params', request.POST)
problem_location_str = strip_if_string(params.get('problem_location_str', False))
student = params.get('unique_student_identifier', None)

if student is not None:
student = get_student_from_identifier(student)

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 @@ -44,7 +44,7 @@
name='list_entrance_exam_instructor_tasks'),
path('mark_student_can_skip_entrance_exam', api.mark_student_can_skip_entrance_exam,
name='mark_student_can_skip_entrance_exam'),
path('list_instructor_tasks', api.list_instructor_tasks, name='list_instructor_tasks'),
path('list_instructor_tasks', api.ListInstructorTasks.as_view(), name='list_instructor_tasks'),
path('list_background_email_tasks', api.list_background_email_tasks, name='list_background_email_tasks'),
path('list_email_content', api.ListEmailContent.as_view(), name='list_email_content'),
path('list_forum_members', api.list_forum_members, name='list_forum_members'),
Expand Down
37 changes: 37 additions & 0 deletions lms/djangoapps/instructor/views/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,43 @@ def validate_unique_student_identifier(self, value):
return user


class ListInstructorTaskInputSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer for handling the input data for the problem response report generation API.
Attributes:
unique_student_identifier (str): The email or username of the student.
This field is optional, but if provided, the `problem_location_str`
must also be provided.
problem_location_str (str): The string representing the location of the problem within the course.
This field is optional, unless `unique_student_identifier` is provided.
"""
unique_student_identifier = serializers.CharField(
max_length=255,
help_text="Email or username of student",
required=False
)
problem_location_str = serializers.CharField(
help_text="Problem location",
required=False
)

def validate(self, data):
"""
Validate the data to ensure that if unique_student_identifier is provided,
problem_location_str must also be provided.
"""
unique_student_identifier = data.get('unique_student_identifier')
problem_location_str = data.get('problem_location_str')

if unique_student_identifier and not problem_location_str:
raise serializers.ValidationError(
"unique_student_identifier must accompany problem_location_str"
)

return data


class ShowStudentExtensionSerializer(serializers.Serializer):
"""
Serializer for validating and processing the student identifier.
Expand Down
42 changes: 41 additions & 1 deletion lms/djangoapps/verify_student/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
from lms.djangoapps.verify_student.emails import send_verification_approved_email
from lms.djangoapps.verify_student.exceptions import VerificationAttemptInvalidStatus
from lms.djangoapps.verify_student.models import VerificationAttempt
from lms.djangoapps.verify_student.signals.signals import (
emit_idv_attempt_approved_event,
emit_idv_attempt_created_event,
emit_idv_attempt_denied_event,
emit_idv_attempt_pending_event,
)
from lms.djangoapps.verify_student.statuses import VerificationAttemptStatus
from lms.djangoapps.verify_student.tasks import send_verification_status_email

Expand Down Expand Up @@ -70,14 +76,22 @@ def create_verification_attempt(user: User, name: str, status: str, expiration_d
expiration_datetime=expiration_datetime,
)

emit_idv_attempt_created_event(
attempt_id=verification_attempt.id,
user=user,
status=status,
name=name,
expiration_date=expiration_datetime,
)

return verification_attempt.id


def update_verification_attempt(
attempt_id: int,
name: Optional[str] = None,
status: Optional[str] = None,
expiration_datetime: Optional[datetime] = None
expiration_datetime: Optional[datetime] = None,
):
"""
Update a verification attempt.
Expand Down Expand Up @@ -125,3 +139,29 @@ def update_verification_attempt(
attempt.expiration_datetime = expiration_datetime

attempt.save()

user = attempt.user
if status == VerificationAttemptStatus.PENDING:
emit_idv_attempt_pending_event(
attempt_id=attempt_id,
user=user,
status=status,
name=name,
expiration_date=expiration_datetime,
)
elif status == VerificationAttemptStatus.APPROVED:
emit_idv_attempt_approved_event(
attempt_id=attempt_id,
user=user,
status=status,
name=name,
expiration_date=expiration_datetime,
)
elif status == VerificationAttemptStatus.DENIED:
emit_idv_attempt_denied_event(
attempt_id=attempt_id,
user=user,
status=status,
name=name,
expiration_date=expiration_datetime,
)
2 changes: 1 addition & 1 deletion lms/djangoapps/verify_student/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ def ready(self):
"""
Connect signal handlers.
"""
from lms.djangoapps.verify_student import signals # pylint: disable=unused-import
from lms.djangoapps.verify_student.signals import signals # pylint: disable=unused-import
from lms.djangoapps.verify_student import tasks # pylint: disable=unused-import
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.core.djangoapps.signals.signals.LEARNER_NOW_VERIFIED.send_robust') as mock_signal:
with patch('openedx_events.learning.signals.IDV_ATTEMPT_APPROVED.send_event') 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
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def _create_attempts(self, num_attempts):
for _ in range(num_attempts):
self.create_upload_and_submit_attempt_for_user()

@patch('lms.djangoapps.verify_student.signals.idv_update_signal.send')
@patch('lms.djangoapps.verify_student.signals.signals.idv_update_signal.send')
def test_resubmit_in_date_range(self, send_idv_update_mock):
call_command('retry_failed_photo_verifications',
status="submitted",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def _create_attempts(self, num_attempts):
for _ in range(num_attempts):
self.create_and_submit_attempt_for_user()

@patch('lms.djangoapps.verify_student.signals.idv_update_signal.send')
@patch('lms.djangoapps.verify_student.signals.signals.idv_update_signal.send')
def test_command(self, send_idv_update_mock):
call_command('trigger_softwaresecurephotoverifications_post_save_signal', start_date_time='2021-10-31 06:00:00')

Expand Down
Loading

0 comments on commit 0f64e16

Please sign in to comment.