diff --git a/eox_nelp/migrations/0010_pearsonrtenevent_candidate.py b/eox_nelp/migrations/0010_pearsonrtenevent_candidate.py new file mode 100644 index 00000000..2994d899 --- /dev/null +++ b/eox_nelp/migrations/0010_pearsonrtenevent_candidate.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.21 on 2024-07-05 21:33 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('eox_nelp', '0009_pearsonrtenevent_appointments'), + ] + + operations = [ + migrations.AddField( + model_name='pearsonrtenevent', + name='candidate', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/eox_nelp/pearson_vue/admin.py b/eox_nelp/pearson_vue/admin.py index 9091421e..24c45b0a 100644 --- a/eox_nelp/pearson_vue/admin.py +++ b/eox_nelp/pearson_vue/admin.py @@ -22,9 +22,9 @@ 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") - readonly_fields = ("created_at",) - list_filter = ["event_type",] + list_display = ("event_type", "created_at", "candidate") + readonly_fields = ("created_at", "candidate", "event_type") + list_filter = ["event_type"] admin.site.register(PearsonRTENEvent, PearsonRTENEventAdmin) diff --git a/eox_nelp/pearson_vue/api/v1/serializers.py b/eox_nelp/pearson_vue/api/v1/serializers.py index ead7db6e..ebc6f490 100644 --- a/eox_nelp/pearson_vue/api/v1/serializers.py +++ b/eox_nelp/pearson_vue/api/v1/serializers.py @@ -34,7 +34,7 @@ class PearsonRTENSerializer(serializers.ModelSerializer): class Meta: """Meta class""" model = PearsonRTENEvent - fields = ["event_type", "content", "created_at"] + fields = ["event_type", "content", "candidate", "created_at"] read_only_fields = ["event_type", "created_at"] def to_internal_value(self, data): diff --git a/eox_nelp/pearson_vue/api/v1/views.py b/eox_nelp/pearson_vue/api/v1/views.py index 670776f6..481b513f 100644 --- a/eox_nelp/pearson_vue/api/v1/views.py +++ b/eox_nelp/pearson_vue/api/v1/views.py @@ -22,6 +22,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from eox_nelp.edxapp_wrapper.student import AnonymousUserId from eox_nelp.pearson_vue.api.v1.serializers import PearsonRTENSerializer from eox_nelp.pearson_vue.constants import ( CANCEL_APPOINTMENT, @@ -102,7 +103,7 @@ 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, content=content_data) + serializer.save(event_type=self.event_type, candidate=self.get_candidate(), content=content_data) def create(self, request, *args, **kwargs): """ @@ -123,6 +124,24 @@ def create(self, request, *args, **kwargs): headers = self.get_success_headers(serializer.data) return Response({}, status=status.HTTP_200_OK, headers=headers) + def get_candidate(self): + """ + Retrieves the candidate user based on the client candidate ID provided in the request data. + + This method extracts the `clientCandidateID` from the request data, removes the "NELC" prefix, + and attempts to retrieve the corresponding user from the `AnonymousUserId` model. If the candidate + ID does not exist, it returns `None`. + + Returns: + user (object or None): The user object associated with the anonymous user ID, or `None` if not found. + """ + anonymous_user_id = self.request.data.get("clientCandidateID", "").replace("NELC", "") + + try: + return AnonymousUserId.objects.get(anonymous_user_id=anonymous_user_id).user + except AnonymousUserId.DoesNotExist: + return None + class ResultNotificationView(PearsonRTENBaseView): """ diff --git a/eox_nelp/pearson_vue/models.py b/eox_nelp/pearson_vue/models.py index aa3037a7..118bd3eb 100644 --- a/eox_nelp/pearson_vue/models.py +++ b/eox_nelp/pearson_vue/models.py @@ -10,6 +10,7 @@ EVENT_TYPE_CHOICES: A list of tuples representing the possible choices for an event type. Each choice represents a different type of event. """ +from django.contrib.auth import get_user_model from django.db import models from eox_nelp.pearson_vue.constants import ( @@ -33,6 +34,7 @@ (MODIFY_APPOINTMENT, "Modify Appointment"), (CANCEL_APPOINTMENT, "Cancel Appointment"), ] +User = get_user_model() class PearsonRTENEvent(models.Model): @@ -47,3 +49,4 @@ class PearsonRTENEvent(models.Model): content = models.JSONField() 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) diff --git a/eox_nelp/pearson_vue/tests/api/v1/test_views.py b/eox_nelp/pearson_vue/tests/api/v1/test_views.py index 6c2be5fd..cd41d6c2 100644 --- a/eox_nelp/pearson_vue/tests/api/v1/test_views.py +++ b/eox_nelp/pearson_vue/tests/api/v1/test_views.py @@ -14,11 +14,13 @@ from unittest.mock import 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 rest_framework import status from rest_framework.test import APIClient +from eox_nelp.edxapp_wrapper.student import AnonymousUserId from eox_nelp.pearson_vue.constants import ( CANCEL_APPOINTMENT, MODIFY_APPOINTMENT, @@ -48,6 +50,13 @@ def setUp(self): # pylint: disable=invalid-name self.user, _ = User.objects.get_or_create(username='testuser', password='12345') self.client.force_authenticate(user=self.user) + def tearDown(self): # pylint: disable=invalid-name + """ + Reset the mocked objects after each test. + """ + AnonymousUserId.reset_mock() + AnonymousUserId.objects.get.side_effect = None + @override_settings(ENABLE_CERTIFICATE_PUBLISHER=False) def test_create_result_notification_event(self): """ @@ -57,18 +66,48 @@ def test_create_result_notification_event(self): - The number of records has increased in 1. - Response returns a 200 status code. - Response data is empty. + - AnonymousUserId.objects.get 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() + AnonymousUserId.objects.get.return_value.user = self.user + response = self.client.post( + reverse(f"pearson-vue-api:v1:{self.event_type}"), + {"clientCandidateID": "NELC123456"}, + format="json", + ) + + final_count = PearsonRTENEvent.objects.filter(event_type=self.event_type, candidate=self.user).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") + + def test_create_result_notification_event_without_user(self): + """ + Test creating an event without clienCandidateID. + + Expected behavior: + - The number of records has increased in 1. + - Response returns a 200 status code. + - Response data is empty. + - AnonymousUserId.objects.get has been called with the expected data. """ # pylint: disable=no-member - initial_count = PearsonRTENEvent.objects.filter(event_type=self.event_type).count() + initial_count = PearsonRTENEvent.objects.filter(event_type=self.event_type, candidate=None).count() + AnonymousUserId.DoesNotExist = ObjectDoesNotExist + AnonymousUserId.objects.get.side_effect = AnonymousUserId.DoesNotExist response = self.client.post(reverse(f"pearson-vue-api:v1:{self.event_type}"), {}, format="json") - final_count = PearsonRTENEvent.objects.filter(event_type=self.event_type).count() + final_count = PearsonRTENEvent.objects.filter(event_type=self.event_type, candidate=None).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="") def test_get_event(self): """