Skip to content

Commit

Permalink
feat: add course overview field to RTEN model (#193)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrey-canon committed Jul 10, 2024
1 parent 115e025 commit 531c386
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 10 deletions.
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

0 comments on commit 531c386

Please sign in to comment.