Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add course overview field to RTEN model #193

Merged
merged 1 commit into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions eox_nelp/migrations/0011_pearsonrtenevent_course.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 3.2.21 on 2024-07-08 17:29

from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings


class Migration(migrations.Migration):

dependencies = [
('course_overviews', '0027_auto_20221102_1109'),
('eox_nelp', '0010_pearsonrtenevent_candidate'),
]

if getattr(settings, 'TESTING_MIGRATIONS', False):
dependencies = [
('eox_nelp', '0010_pearsonrtenevent_candidate'),
]
course_overview_model = 'eox_nelp.courseoverview'
else:
dependencies = [
('eox_nelp', '0010_pearsonrtenevent_candidate'),
('course_overviews', '0025_auto_20210702_1602'),
]
course_overview_model = 'course_overviews.courseoverview'

operations = [
migrations.AddField(
model_name='pearsonrtenevent',
name='course',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=course_overview_model),
),
]
4 changes: 2 additions & 2 deletions eox_nelp/pearson_vue/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ class PearsonRTENEventAdmin(admin.ModelAdmin):
list_display (list): List of fields to display in the admin list view.
readonly_fields (tuple): Tuple of fields that are read-only in the admin interface.
"""
list_display = ("event_type", "created_at", "candidate")
readonly_fields = ("created_at", "candidate", "event_type")
list_display = ("event_type", "candidate", "course", "created_at")
readonly_fields = ("created_at", "candidate", "course", "event_type")
list_filter = ["event_type"]


Expand Down
20 changes: 18 additions & 2 deletions eox_nelp/pearson_vue/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ class PearsonRTENSerializer(serializers.ModelSerializer):
Attributes:
content (serializers.JSONField): A field to handle JSON content.
"""

course = serializers.SerializerMethodField()
content = serializers.JSONField()

class Meta:
"""Meta class"""
model = PearsonRTENEvent
fields = ["event_type", "content", "candidate", "created_at"]
fields = ["event_type", "content", "candidate", "course", "created_at"]
read_only_fields = ["event_type", "created_at"]

def to_internal_value(self, data):
Expand All @@ -48,3 +48,19 @@ def to_internal_value(self, data):
dict: A dictionary containing the serialized data.
"""
return {"content": data}

def get_course(self, obj):
"""
Retrieves the course associated with the given object as a string.

This method checks if the `course` attribute of the provided object exists.
If it does, it returns the string representation of the course.
Otherwise, it returns `None`.

Args:
obj (object): The object containing the `course` attribute.

Returns:
str or None: The string representation of the course if it exists, otherwise `None`.
"""
return str(obj.course) if obj.course else None
30 changes: 29 additions & 1 deletion eox_nelp/pearson_vue/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
UNREVOKE_RESULT,
)
from eox_nelp.pearson_vue.models import PearsonRTENEvent
from eox_nelp.pearson_vue.pipeline import get_enrollment_from_id
from eox_nelp.pearson_vue.rti_backend import ResultNotificationBackend


Expand Down Expand Up @@ -103,7 +104,12 @@ def perform_create(self, serializer):
serializer (Serializer): The serializer instance used for data validation and saving.
"""
content_data = self.request.data.copy()
serializer.save(event_type=self.event_type, candidate=self.get_candidate(), content=content_data)
serializer.save(
event_type=self.event_type,
candidate=self.get_candidate(),
content=content_data,
course=self.get_course(),
)

def create(self, request, *args, **kwargs):
"""
Expand Down Expand Up @@ -142,6 +148,28 @@ def get_candidate(self):
except AnonymousUserId.DoesNotExist:
return None

def get_course(self):
"""
Retrieves the course associated with the enrollment ID from the request data.

This method extracts the `clientAuthorizationID` from the request data. If the ID is present,
it splits the ID to obtain the `enrollment_id` and then retrieves the enrollment object using
the `get_enrollment_from_id` function. If the enrollment object exists, it returns the associated course.
Otherwise, it returns `None`.

Returns:
Course or None: The course associated with the enrollment if it exists, otherwise `None`.
"""
client_authorization_id = self.request.data.get("authorization", {}).get("clientAuthorizationID")

if not client_authorization_id:
return None

enrollment_id = client_authorization_id.split("-")[0]
enrollment = get_enrollment_from_id(enrollment_id).get("enrollment")

return enrollment.course if enrollment else None


class ResultNotificationView(PearsonRTENBaseView):
"""
Expand Down
2 changes: 2 additions & 0 deletions eox_nelp/pearson_vue/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.contrib.auth import get_user_model
from django.db import models

from eox_nelp.edxapp_wrapper.course_overviews import CourseOverview
from eox_nelp.pearson_vue.constants import (
CANCEL_APPOINTMENT,
MODIFY_APPOINTMENT,
Expand Down Expand Up @@ -50,3 +51,4 @@ class PearsonRTENEvent(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
event_type = models.CharField(max_length=20, choices=EVENT_TYPE_CHOICES)
candidate = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
course = models.ForeignKey(CourseOverview, null=True, on_delete=models.DO_NOTHING)
64 changes: 59 additions & 5 deletions eox_nelp/pearson_vue/tests/api/v1/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@
TestUnrevokeResultView: Unit tests for the UnrevokeResultView.
"""
import unittest
from unittest.mock import patch
from unittest.mock import Mock, patch

from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django.test import override_settings
from django.urls import reverse
from opaque_keys.edx.keys import CourseKey
from rest_framework import status
from rest_framework.test import APIClient

from eox_nelp.edxapp_wrapper.course_overviews import CourseOverview
from eox_nelp.edxapp_wrapper.student import AnonymousUserId
from eox_nelp.pearson_vue.constants import (
CANCEL_APPOINTMENT,
Expand Down Expand Up @@ -49,6 +51,8 @@ def setUp(self): # pylint: disable=invalid-name
self.client = APIClient()
self.user, _ = User.objects.get_or_create(username='testuser', password='12345')
self.client.force_authenticate(user=self.user)
self.course_key = CourseKey.from_string("course-v1:test+CS501+2022_T4")
self.course, _ = CourseOverview.objects.get_or_create(id=self.course_key)

def tearDown(self): # pylint: disable=invalid-name
"""
Expand All @@ -57,8 +61,9 @@ def tearDown(self): # pylint: disable=invalid-name
AnonymousUserId.reset_mock()
AnonymousUserId.objects.get.side_effect = None

@patch("eox_nelp.pearson_vue.api.v1.views.get_enrollment_from_id")
@override_settings(ENABLE_CERTIFICATE_PUBLISHER=False)
def test_create_result_notification_event(self):
def test_create_result_notification_event(self, enrollment_from_id_mock):
"""
Test creating an event.

Expand All @@ -67,23 +72,39 @@ def test_create_result_notification_event(self):
- Response returns a 200 status code.
- Response data is empty.
- AnonymousUserId.objects.get has been called with the expected data.
- get_enrollment_from_id has been called with the expected data.
"""
# pylint: disable=no-member
initial_count = PearsonRTENEvent.objects.filter(event_type=self.event_type, candidate=self.user).count()
initial_count = PearsonRTENEvent.objects.filter(
event_type=self.event_type,
candidate=self.user,
course=self.course,
).count()
enrollment_from_id_mock.return_value = {"enrollment": Mock(course=self.course)}
AnonymousUserId.objects.get.return_value.user = self.user

response = self.client.post(
reverse(f"pearson-vue-api:v1:{self.event_type}"),
{"clientCandidateID": "NELC123456"},
{
"clientCandidateID": "NELC123456",
"authorization": {
"clientAuthorizationID": "1584-4785"
},
},
format="json",
)

final_count = PearsonRTENEvent.objects.filter(event_type=self.event_type, candidate=self.user).count()
final_count = PearsonRTENEvent.objects.filter(
event_type=self.event_type,
candidate=self.user,
course=self.course,
).count()

self.assertEqual(final_count, initial_count + 1)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {})
AnonymousUserId.objects.get.assert_called_once_with(anonymous_user_id="123456")
enrollment_from_id_mock.assert_called_once_with("1584")

@override_settings(ENABLE_CERTIFICATE_PUBLISHER=False)
def test_create_result_notification_event_without_user(self):
Expand All @@ -110,6 +131,39 @@ def test_create_result_notification_event_without_user(self):
self.assertEqual(response.data, {})
AnonymousUserId.objects.get.assert_called_once_with(anonymous_user_id="")

@patch("eox_nelp.pearson_vue.api.v1.views.get_enrollment_from_id")
@override_settings(ENABLE_CERTIFICATE_PUBLISHER=False)
def test_create_result_notification_event_with_invalid_authorization_id(self, enrollment_from_id_mock):
"""
Test creating an event with invalid clienAuthorizationID.

Expected behavior:
- The number of records has increased in 1.
- Response returns a 200 status code.
- Response data is empty.
- the course record is None
- get_enrollment_from_id has been called with the expected data.
"""
# pylint: disable=no-member
initial_record_ids = list(
PearsonRTENEvent.objects.filter(event_type=self.event_type).values_list('id', flat=True)
)
enrollment_from_id_mock.return_value = {}

response = self.client.post(
reverse(f"pearson-vue-api:v1:{self.event_type}"),
{"authorization": {"clientAuthorizationID": "1584-4785"}},
format="json",
)

new_records = PearsonRTENEvent.objects.filter(event_type=self.event_type).exclude(id__in=initial_record_ids)

self.assertEqual(1, new_records.count())
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {})
self.assertIsNone(new_records[0].course)
enrollment_from_id_mock.assert_called_once_with("1584")

def test_get_event(self):
"""
Test retrieving an event.
Expand Down
Loading