Skip to content

Commit 7378335

Browse files
committed
add remaining recommendation serializers, serializers and tests
1 parent 308d4f2 commit 7378335

File tree

4 files changed

+191
-1
lines changed

4 files changed

+191
-1
lines changed

contentcuration/contentcuration/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2708,7 +2708,7 @@ class BaseFeedback(models.Model):
27082708
# time_shown: timestamp of when the recommendations are first shown
27092709
created_at = models.DateTimeField(auto_now_add=True)
27102710

2711-
# for RecommendationsEvent class conntentnode_id represents:
2711+
# for RecommendationsEvent class contentnode_id represents:
27122712
# target_topic_id that the ID of the topic the user
27132713
# initiated the import from (where the imported content will go)
27142714
#

contentcuration/contentcuration/tests/test_serializers.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import uuid
2+
13
from django.db.models.query import QuerySet
4+
from django.utils import timezone
25
from le_utils.constants import content_kinds
36
from mock import Mock
47
from rest_framework import serializers
@@ -7,11 +10,14 @@
710
from contentcuration.models import Channel
811
from contentcuration.models import ContentNode
912
from contentcuration.models import DEFAULT_CONTENT_DEFAULTS
13+
from contentcuration.models import RecommendationsEvent
1014
from contentcuration.tests import testdata
1115
from contentcuration.viewsets.channel import ChannelSerializer as BaseChannelSerializer
1216
from contentcuration.viewsets.common import ContentDefaultsSerializer
1317
from contentcuration.viewsets.contentnode import ContentNodeSerializer
1418
from contentcuration.viewsets.feedback import FlagFeedbackEventSerializer
19+
from contentcuration.viewsets.feedback import RecommendationsEventSerializer
20+
from contentcuration.viewsets.feedback import RecommendationsInteractionEventSerializer
1521

1622

1723
def ensure_no_querysets_in_serializer(object):
@@ -225,3 +231,111 @@ def test_invalid_data(self):
225231
data = {'context': 'invalid'}
226232
serializer = FlagFeedbackEventSerializer(data=data)
227233
self.assertFalse(serializer.is_valid())
234+
235+
236+
class RecommendationsInteractionEventSerializerTestCase(BaseAPITestCase):
237+
def setUp(self):
238+
super(RecommendationsInteractionEventSerializerTestCase, self).setUp()
239+
self.channel = testdata.channel("testchannel")
240+
self.interaction_node = testdata.node(
241+
{
242+
"kind_id": content_kinds.VIDEO,
243+
"title": "Recommended Video content",
244+
},
245+
)
246+
self.node_where_import_is_initiated = testdata.node(
247+
{
248+
"kind_id": content_kinds.TOPIC,
249+
"title": "Node where content is imported",
250+
},
251+
)
252+
self.recommendation_event = RecommendationsEvent.objects.create(
253+
user=self.user,
254+
target_channel_id=self.channel.id,
255+
content_id=self.node_where_import_is_initiated.content_id,
256+
contentnode_id=self.node_where_import_is_initiated.id,
257+
context={'model_version': 1, 'breadcrumbs': "#Title#->Random"},
258+
time_hidden=timezone.now(),
259+
content=[{'content_id': str(uuid.uuid4()), 'node_id': str(uuid.uuid4()), 'channel_id': str(uuid.uuid4()), 'score': 4}]
260+
)
261+
262+
def test_deserialization_and_validation(self):
263+
data = {
264+
'context': {'test_key': 'test_value'},
265+
'contentnode_id': str(self.interaction_node.id),
266+
'content_id': str(self.interaction_node.content_id),
267+
'feedback_type': 'IGNORED',
268+
'feedback_reason': '----',
269+
'recommendation_event_id': str(self.recommendation_event.id)
270+
}
271+
serializer = RecommendationsInteractionEventSerializer(data=data)
272+
self.assertTrue(serializer.is_valid(), serializer.errors)
273+
instance = serializer.save()
274+
self.assertEqual(instance.context, data['context'])
275+
self.assertEqual(instance.feedback_type, data['feedback_type'])
276+
self.assertEqual(str(instance.recommendation_event_id), data['recommendation_event_id'])
277+
278+
def test_invalid_data(self):
279+
data = {'context': 'invalid'}
280+
serializer = RecommendationsInteractionEventSerializer(data=data)
281+
self.assertFalse(serializer.is_valid())
282+
283+
data = {
284+
'context': {'test_key': 'test_value'},
285+
'contentnode_id': str(self.interaction_node.id),
286+
'content_id': str(self.interaction_node.content_id),
287+
'feedback_type': 'INVALID_TYPE',
288+
'feedback_reason': '-----',
289+
'recommendation_event_id': 'invalid-uuid'
290+
}
291+
serializer = RecommendationsInteractionEventSerializer(data=data)
292+
self.assertFalse(serializer.is_valid())
293+
294+
295+
class RecommendationsEventSerializerTestCase(BaseAPITestCase):
296+
def setUp(self):
297+
super(RecommendationsEventSerializerTestCase, self).setUp()
298+
self.channel = testdata.channel("testchannel")
299+
self.node_where_import_is_initiated = testdata.node(
300+
{
301+
"kind_id": content_kinds.TOPIC,
302+
"title": "Title of the topic",
303+
},
304+
)
305+
306+
def test_deserialization_and_validation(self):
307+
data = {
308+
'user': self.user.id,
309+
'target_channel_id': str(self.channel.id),
310+
'context': {'model_version': 1, 'breadcrumbs': "#Title#->Random"},
311+
'contentnode_id': str(self.node_where_import_is_initiated.id),
312+
'content_id': str(self.node_where_import_is_initiated.content_id),
313+
'time_hidden': timezone.now().isoformat(),
314+
'content': [{'content_id': str(uuid.uuid4()), 'node_id': str(uuid.uuid4()), 'channel_id': str(uuid.uuid4()), 'score': 4}]
315+
}
316+
serializer = RecommendationsEventSerializer(data=data)
317+
self.assertTrue(serializer.is_valid(), serializer.errors)
318+
instance = serializer.save()
319+
self.assertEqual(instance.context, data['context'])
320+
self.assertEqual(instance.user.id, data['user'])
321+
self.assertEqual(str(instance.contentnode_id).replace('-', ''), data['contentnode_id'].replace('-', ''))
322+
self.assertEqual(instance.content, data['content'])
323+
324+
def test_invalid_data(self):
325+
# Test with missing required fields
326+
data = {'context': 'invalid'}
327+
serializer = RecommendationsEventSerializer(data=data)
328+
self.assertFalse(serializer.is_valid())
329+
330+
# Test with invalid contentnode_id
331+
data = {
332+
'user': self.user.id,
333+
'target_channel_id': str(self.channel.id),
334+
'context': {'model_version': 1, 'breadcrumbs': "#Title#->Random"},
335+
'contentnode_id': 'invalid-uuid',
336+
'content_id': str(self.node_where_import_is_initiated.content_id),
337+
'time_hidden': timezone.now().isoformat(),
338+
'content': [{'content_id': str(uuid.uuid4()), 'node_id': str(uuid.uuid4()), 'channel_id': str(uuid.uuid4()), 'score': 4}]
339+
}
340+
serializer = RecommendationsEventSerializer(data=data)
341+
self.assertFalse(serializer.is_valid())

contentcuration/contentcuration/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
from contentcuration.viewsets.clipboard import ClipboardViewSet
4141
from contentcuration.viewsets.contentnode import ContentNodeViewSet
4242
from contentcuration.viewsets.feedback import FlagFeedbackEventViewSet
43+
from contentcuration.viewsets.feedback import RecommendationsEventViewSet
44+
from contentcuration.viewsets.feedback import RecommendationsInteractionEventViewSet
4345
from contentcuration.viewsets.file import FileViewSet
4446
from contentcuration.viewsets.invitation import InvitationViewSet
4547
from contentcuration.viewsets.recommendation import RecommendationView
@@ -70,6 +72,8 @@ def get_redirect_url(self, *args, **kwargs):
7072
router.register(r'admin-users', AdminUserViewSet, basename='admin-users')
7173
router.register(r'clipboard', ClipboardViewSet, basename='clipboard')
7274
router.register(r'flagged', FlagFeedbackEventViewSet, basename='flagged')
75+
router.register(r'recommendations', RecommendationsEventViewSet, basename='recommendations')
76+
router.register(r'recommendationsinteraction', RecommendationsInteractionEventViewSet, basename='recommendations-interaction')
7377

7478
urlpatterns = [
7579
re_path(r'^api/', include(router.urls)),

contentcuration/contentcuration/viewsets/feedback.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from rest_framework.permissions import IsAuthenticated
55

66
from contentcuration.models import FlagFeedbackEvent
7+
from contentcuration.models import RecommendationsEvent
8+
from contentcuration.models import RecommendationsInteractionEvent
79

810

911
class IsAdminForListAndDestroy(permissions.BasePermission):
@@ -48,6 +50,76 @@ class Meta:
4850
fields = BaseFeedbackSerializer.Meta.fields + BaseFeedbackEventSerializer.Meta.fields + BaseFeedbackInteractionEventSerializer.Meta.fields
4951

5052

53+
class RecommendationsInteractionEventSerializer(BaseFeedbackSerializer, BaseFeedbackInteractionEventSerializer):
54+
recommendation_event_id = serializers.UUIDField()
55+
56+
class Meta:
57+
model = RecommendationsInteractionEvent
58+
fields = BaseFeedbackSerializer.Meta.fields + BaseFeedbackInteractionEventSerializer.Meta.fields + ['recommendation_event_id']
59+
60+
def create(self, validated_data):
61+
return RecommendationsInteractionEvent(**validated_data)
62+
63+
def update(self, instance, validated_data):
64+
instance.save()
65+
66+
67+
class RecommendationsEventSerializer(BaseFeedbackSerializer, BaseFeedbackEventSerializer):
68+
content = serializers.JSONField(default=list)
69+
70+
class Meta:
71+
model = RecommendationsEvent
72+
fields = BaseFeedbackSerializer.Meta.fields + BaseFeedbackEventSerializer.Meta.fields + ['content', 'time_hidden']
73+
74+
def create(self, validated_data):
75+
return RecommendationsEvent(**validated_data)
76+
77+
def update(self, instance, validated_data):
78+
instance.save()
79+
80+
81+
class RecommendationsInteractionEventViewSet(
82+
viewsets.ViewSet,
83+
):
84+
queryset = RecommendationsInteractionEvent.objects.all()
85+
serializer_class = RecommendationsInteractionEventSerializer
86+
87+
# TODO: decide export mechansim to make use of gathered data
88+
89+
def create(self, request):
90+
pass
91+
92+
def update(self, request, pk=None):
93+
pass
94+
95+
def partial_update(self, request, pk=None):
96+
pass
97+
98+
def destroy(self, request, pk=None):
99+
pass
100+
101+
102+
class RecommendationsEventViewSet(
103+
viewsets.ViewSet
104+
):
105+
queryset = RecommendationsEvent.objects.all()
106+
serializer_class = RecommendationsEventSerializer
107+
108+
# TODO: decide export mechansim to make use of gathered data
109+
110+
def create(self, request):
111+
pass
112+
113+
def update(self, request, pk=None):
114+
pass
115+
116+
def partial_update(self, request, pk=None):
117+
pass
118+
119+
def destroy(self, request, pk=None):
120+
pass
121+
122+
51123
class FlagFeedbackEventViewSet(viewsets.ModelViewSet):
52124
queryset = FlagFeedbackEvent.objects.all()
53125
serializer_class = FlagFeedbackEventSerializer

0 commit comments

Comments
 (0)