Skip to content

Commit

Permalink
feat: implement new celery task for pearson engine integration (#211)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrey-canon authored Jul 30, 2024
1 parent 2a32318 commit bcb9668
Show file tree
Hide file tree
Showing 7 changed files with 368 additions and 68 deletions.
84 changes: 61 additions & 23 deletions eox_nelp/admin/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
for selected course enrollments.
"""

from django.conf import settings
from django.contrib import admin

from eox_nelp.admin.register_admin_model import register_admin_model as register
from eox_nelp.edxapp_wrapper.student import CourseEnrollment, CourseEnrollmentAdmin
from eox_nelp.pearson_vue.tasks import cdd_task, ead_task, real_time_import_task
from eox_nelp.pearson_vue.tasks import cdd_task, ead_task, real_time_import_task, real_time_import_task_v2


@admin.action(description="Execute Pearson RTI request")
Expand All @@ -30,10 +31,17 @@ def pearson_real_time_action(modeladmin, request, queryset): # pylint: disable=
queryset: The QuerySet of selected CourseEnrollment instances.
"""
for course_enrollment in queryset:
real_time_import_task.delay(
course_id=str(course_enrollment.course_id),
user_id=course_enrollment.user.id,
)
if getattr(settings, "USE_PEARSON_ENGINE_SERVICE", False):
real_time_import_task_v2.delay(
user_id=course_enrollment.user.id,
exam_id=str(course_enrollment.course_id),
action_name="rti",
)
else:
real_time_import_task.delay(
course_id=str(course_enrollment.course_id),
user_id=course_enrollment.user.id,
)


@admin.action(description="Execute Pearson EAD Add request")
Expand All @@ -50,11 +58,19 @@ def pearson_add_ead_action(modeladmin, request, queryset): # pylint: disable=un
queryset: The QuerySet of selected CourseEnrollment instances.
"""
for course_enrollment in queryset:
ead_task.delay(
course_id=str(course_enrollment.course_id),
user_id=course_enrollment.user.id,
transaction_type="Add",
)
if getattr(settings, "USE_PEARSON_ENGINE_SERVICE", False):
real_time_import_task_v2.delay(
user_id=course_enrollment.user.id,
exam_id=str(course_enrollment.course_id),
action_name="ead",
transaction_type="Add",
)
else:
ead_task.delay(
course_id=str(course_enrollment.course_id),
user_id=course_enrollment.user.id,
transaction_type="Add",
)


@admin.action(description="Execute Pearson EAD Update request")
Expand All @@ -71,11 +87,19 @@ def pearson_update_ead_action(modeladmin, request, queryset): # pylint: disable
queryset: The QuerySet of selected CourseEnrollment instances.
"""
for course_enrollment in queryset:
ead_task.delay(
course_id=str(course_enrollment.course_id),
user_id=course_enrollment.user.id,
transaction_type="Update",
)
if getattr(settings, "USE_PEARSON_ENGINE_SERVICE", False):
real_time_import_task_v2.delay(
user_id=course_enrollment.user.id,
exam_id=str(course_enrollment.course_id),
action_name="ead",
transaction_type="Update",
)
else:
ead_task.delay(
course_id=str(course_enrollment.course_id),
user_id=course_enrollment.user.id,
transaction_type="Update",
)


@admin.action(description="Execute Pearson EAD Delete request")
Expand All @@ -92,11 +116,19 @@ def pearson_delete_ead_action(modeladmin, request, queryset): # pylint: disable
queryset: The QuerySet of selected CourseEnrollment instances.
"""
for course_enrollment in queryset:
ead_task.delay(
course_id=str(course_enrollment.course_id),
user_id=course_enrollment.user.id,
transaction_type="Delete",
)
if getattr(settings, "USE_PEARSON_ENGINE_SERVICE", False):
real_time_import_task_v2.delay(
user_id=course_enrollment.user.id,
exam_id=str(course_enrollment.course_id),
action_name="ead",
transaction_type="Delete",
)
else:
ead_task.delay(
course_id=str(course_enrollment.course_id),
user_id=course_enrollment.user.id,
transaction_type="Delete",
)


@admin.action(description="Execute Pearson CDD request for student.")
Expand All @@ -113,9 +145,15 @@ def pearson_cdd_action(modeladmin, request, queryset): # pylint: disable=unused
queryset: The QuerySet of selected CourseEnrollment instances.
"""
for course_enrollment in queryset:
cdd_task.delay(
user_id=course_enrollment.user.id,
)
if getattr(settings, "USE_PEARSON_ENGINE_SERVICE", False):
real_time_import_task_v2.delay(
user_id=course_enrollment.user.id,
action_name="cdd",
)
else:
cdd_task.delay(
user_id=course_enrollment.user.id,
)


class NelpCourseEnrollmentAdmin(CourseEnrollmentAdmin):
Expand Down
133 changes: 101 additions & 32 deletions eox_nelp/admin/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from unittest.mock import MagicMock, patch

from ddt import data, ddt, unpack
from django.test import RequestFactory, TestCase
from django.test import RequestFactory, TestCase, override_settings

from eox_nelp.admin import (
NelpCourseEnrollmentAdmin,
Expand All @@ -26,6 +26,42 @@ class TestPearsonAction(TestCase):
Unit tests for the pearson actions functions.
"""

def setUp(self):
"""Setup common conditions for every test case"""
self.request = RequestFactory().get("/admin")

def _create_mock_enrollment(self, course_id):
"""Create a mock course enrollment."""
user = MagicMock()
user.id = 1
enrollment = MagicMock()
enrollment.course_id = course_id
enrollment.user = user

return enrollment

def _prepare_call_kwargs(self, enrollments, call_args, extra_call_kwargs):
"""Prepare the call arguments for the mocked task."""
mocks_call_kwargs = []
for enrollment in enrollments:
call_kwargs = {
"user_id": enrollment.user.id,
"course_id": enrollment.course_id,
"exam_id": enrollment.course_id,
}
# Retain only required arguments and update with extra kwargs
call_kwargs = {key: call_kwargs[key] for key in call_args if key in call_kwargs}
call_kwargs.update(extra_call_kwargs)
mocks_call_kwargs.append(call_kwargs)

return mocks_call_kwargs

def _assert_mocked_task_calls(self, mocked_task, mocks_call_kwargs):
"""Assert that the mocked task was called with the correct parameters."""
for mock_call_kwargs in mocks_call_kwargs:
mocked_task.assert_any_call(**mock_call_kwargs)
self.assertEqual(mocked_task.call_count, len(mocks_call_kwargs))

@data(
{
"mock_task": "eox_nelp.pearson_vue.tasks.real_time_import_task.delay",
Expand Down Expand Up @@ -70,43 +106,76 @@ class TestPearsonAction(TestCase):
@unpack
def test_pearson_course_enrollment_action(self, mock_task, admin_action, call_args, extra_call_kwargs):
"""
Test that a pearson_action function calls the a task delay with correct parameters.
Test that a pearson_action function calls a task delay with correct parameters.
"""
user = MagicMock()
user.id = 1
course_enrollment_1 = MagicMock()
course_enrollment_1.course_id = "course-v1:TestX+T101+2024_T1"
course_enrollment_1.user = user
course_enrollment_2 = MagicMock()
course_enrollment_2.course_id = "course-v1:FutureX+T102+2025_T1"
course_enrollment_2.user = user
mocks_call_kwargs = [
{
"course_id": course_enrollment_1.course_id,
"user_id": user.id,
},
{
"course_id": course_enrollment_2.course_id,
"user_id": user.id,
}
queryset = [
self._create_mock_enrollment("course-v1:TestX+T101+2024_T1"),
self._create_mock_enrollment("course-v1:FutureX+T102+2025_T1"),
]
for mock_call_kwargs in mocks_call_kwargs:
for key in set(mock_call_kwargs.keys()).difference(call_args): # set main call args
del mock_call_kwargs[key]
mock_call_kwargs.update(extra_call_kwargs)
mocks_call_kwargs = self._prepare_call_kwargs(queryset, call_args, extra_call_kwargs)

queryset = [course_enrollment_1, course_enrollment_2]
modeladmin = MagicMock()
# Call the admin action
with patch(mock_task) as mocked_task:
admin_action(MagicMock(), self.request, queryset)
self._assert_mocked_task_calls(mocked_task, mocks_call_kwargs)

request = RequestFactory().get("/admin")
@data(
{
"admin_action": pearson_real_time_action,
"call_args": ["user_id", "exam_id"],
"extra_call_kwargs": {
"action_name": "rti",
},
},
{
"admin_action": pearson_add_ead_action,
"call_args": ["user_id", "exam_id"],
"extra_call_kwargs": {
"transaction_type": "Add",
"action_name": "ead",
},
},
{
"admin_action": pearson_update_ead_action,
"call_args": ["user_id", "exam_id"],
"extra_call_kwargs": {
"transaction_type": "Update",
"action_name": "ead",
},
},
{
"admin_action": pearson_delete_ead_action,
"call_args": ["user_id", "exam_id"],
"extra_call_kwargs": {
"transaction_type": "Delete",
"action_name": "ead",
},
},
{
"admin_action": pearson_cdd_action,
"call_args": ["user_id"],
"extra_call_kwargs": {
"action_name": "cdd",
},
},
)
@unpack
@override_settings(USE_PEARSON_ENGINE_SERVICE=True)
def test_pearson_course_enrollment_action_v2(self, admin_action, call_args, extra_call_kwargs):
"""
Test that a pearson_action function calls a task delay with correct parameters.
"""
queryset = [
self._create_mock_enrollment("course-v1:TestX+T101+2024_T1"),
self._create_mock_enrollment("course-v1:FutureX+T102+2025_T1"),
]
mocks_call_kwargs = self._prepare_call_kwargs(queryset, call_args, extra_call_kwargs)

# Call the admin action
with patch(mock_task) as mocked_task:
admin_action(modeladmin, request, queryset)
for mock_call_kwargs in mocks_call_kwargs:
mocked_task.assert_any_call(**mock_call_kwargs)
mocked_task.assert_any_call(**mock_call_kwargs)
self.assertEqual(mocked_task.call_count, len(mocks_call_kwargs))
with patch("eox_nelp.pearson_vue.tasks.real_time_import_task_v2.delay") as mocked_task:
admin_action(MagicMock(), self.request, queryset)
self._assert_mocked_task_calls(mocked_task, mocks_call_kwargs)


@ddt
Expand Down
6 changes: 6 additions & 0 deletions eox_nelp/pearson_vue/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,9 @@
</soapenv:Body>
</soapenv:Envelope>
"""

ALLOWED_RTI_ACTIONS = {
"rti": "real_time_import",
"cdd": "import_candidate_demographics",
"ead": "import_exam_authorization",
}
45 changes: 45 additions & 0 deletions eox_nelp/pearson_vue/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
"""

from celery import shared_task
from django.contrib.auth import get_user_model

from eox_nelp.api_clients.pearson_engine import PearsonEngineApiClient
from eox_nelp.pearson_vue.constants import ALLOWED_RTI_ACTIONS
from eox_nelp.pearson_vue.pipeline import audit_method, rename_function
from eox_nelp.pearson_vue.rti_backend import (
CandidateDemographicsDataImport,
ErrorRealTimeImportHandler,
ExamAuthorizationDataImport,
RealTimeImport,
)

User = get_user_model()


@shared_task(bind=True)
def real_time_import_task(self, pipeline_index=0, **kwargs):
Expand Down Expand Up @@ -93,3 +99,42 @@ def rti_error_handler_task(self, pipeline_index=0, **kwargs):
error_rti.run_pipeline()
except Exception as exc: # pylint: disable=broad-exception-caught
self.retry(exc=exc, kwargs=error_rti.backend_data)


@shared_task
def real_time_import_task_v2(user_id, exam_id=None, action_name="rti", **kwargs):
"""
Asynchronous task to perform a real-time import action using the Pearson Engine API.
This task handles different types of import actions, such as real-time import,
importing candidate demographics, and importing exam authorizations, by calling
the appropriate method on the PearsonEngineApiClient.
Args:
user_id (int): The ID of the user to be processed.
exam_id (str, optional): The ID of the exam for authorization. Default is None.
action_name (str, optional): The action to perform. Default is "rti".
Supported actions are:
- "rti" for real_time_import
- "cdd" for import_candidate_demographics
- "ead" for import_exam_authorization
**kwargs: Additional keyword arguments to pass to the API client method.
Raises:
KeyError: If action_name is not found in ALLOWED_RTI_ACTIONS.
User.DoesNotExist: If the user with the given user_id does not exist.
"""
action_key = ALLOWED_RTI_ACTIONS[action_name]

@audit_method(action="Pearson Engine Action")
@rename_function(name=action_key)
def audit_pearson_engine_action(user_id, exam_id, action_key, **kwargs):
action = getattr(PearsonEngineApiClient(), action_key)

action(
user=User.objects.get(id=user_id),
exam_id=exam_id,
**kwargs
)

audit_pearson_engine_action(user_id, exam_id, action_key, **kwargs)
Loading

0 comments on commit bcb9668

Please sign in to comment.