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 candidate field to RTEN model #190

Merged
merged 1 commit into from
Jul 8, 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
21 changes: 21 additions & 0 deletions eox_nelp/migrations/0010_pearsonrtenevent_candidate.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
6 changes: 3 additions & 3 deletions eox_nelp/pearson_vue/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion eox_nelp/pearson_vue/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
21 changes: 20 additions & 1 deletion eox_nelp/pearson_vue/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
"""
Expand All @@ -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", "")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we'd avoid modifying clientCandidateId(product requirement?) due to its impact on product functionality and user experience. Any changes would necessitate significant code updates related to anonymous IDs.


try:
return AnonymousUserId.objects.get(anonymous_user_id=anonymous_user_id).user
except AnonymousUserId.DoesNotExist:
return None


class ResultNotificationView(PearsonRTENBaseView):
"""
Expand Down
3 changes: 3 additions & 0 deletions eox_nelp/pearson_vue/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -33,6 +34,7 @@
(MODIFY_APPOINTMENT, "Modify Appointment"),
(CANCEL_APPOINTMENT, "Cancel Appointment"),
]
User = get_user_model()


class PearsonRTENEvent(models.Model):
Expand All @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we could use blank=True, in order to don't be mandatory in forms or serializer.?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to do this as mandatory as possible since the idea is that every record has a user however we don't know if the client will send the whole data in the example format and secondly the migration process complains about previous records when the null=True field is not present, that's why I added the null field and I don't think we should include the blank field

44 changes: 42 additions & 2 deletions eox_nelp/pearson_vue/tests/api/v1/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
"""
Expand All @@ -57,18 +66,49 @@ 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")

@override_settings(ENABLE_CERTIFICATE_PUBLISHER=False)
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):
"""
Expand Down
Loading