Skip to content

Commit

Permalink
create feedback models and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ozer550 committed Mar 27, 2024
1 parent 6e3be57 commit 7a3ed60
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 0 deletions.
8 changes: 8 additions & 0 deletions contentcuration/contentcuration/constants/feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FEEDBACK_TYPE_CHOICES = (
('IMPORTED', 'Imported'),
('REJECTED', 'Rejected'),
('PREVIEWED', 'Previewed'),
('SHOWMORE', 'Show More'),
('IGNORED', 'Ignored'),
('FLAGGED', 'Flagged'),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Generated by Django 3.2.24 on 2024-03-27 15:46
import uuid

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


class Migration(migrations.Migration):

dependencies = [
('contentcuration', '0147_alter_formatpreset_id'),
]

operations = [
migrations.CreateModel(
name='RecommendationsInteractionEvent',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('context', models.JSONField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('contentnode_id', models.UUIDField()),
('content_id', models.UUIDField()),
('feedback_type', models.CharField(choices=[('IMPORTED', 'Imported'), ('REJECTED', 'Rejected'), ('PREVIEWED', 'Previewed'), ('SHOWMORE', 'Show More'), ('IGNORED', 'Ignored'), ('FLAGGED', 'Flagged')], max_length=50)),
('feedback_reason', models.TextField()),
('recommendation_event_id', models.UUIDField()),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='RecommendationsEvent',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('context', models.JSONField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('contentnode_id', models.UUIDField()),
('content_id', models.UUIDField()),
('target_channel_id', models.UUIDField()),
('time_hidden', models.DateTimeField()),
('content', models.JSONField(default=list)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='FlagFeedbackEvent',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('context', models.JSONField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('contentnode_id', models.UUIDField()),
('content_id', models.UUIDField()),
('target_channel_id', models.UUIDField()),
('feedback_type', models.CharField(choices=[('IMPORTED', 'Imported'), ('REJECTED', 'Rejected'), ('PREVIEWED', 'Previewed'), ('SHOWMORE', 'Show More'), ('IGNORED', 'Ignored'), ('FLAGGED', 'Flagged')], max_length=50)),
('feedback_reason', models.TextField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
]
66 changes: 66 additions & 0 deletions contentcuration/contentcuration/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@

from contentcuration.constants import channel_history
from contentcuration.constants import completion_criteria
from contentcuration.constants import feedback
from contentcuration.constants import user_history
from contentcuration.constants.contentnode import kind_activity_map
from contentcuration.db.models.expressions import Array
Expand Down Expand Up @@ -2590,3 +2591,68 @@ class Meta:
name='task_result_signature',
),
]


class BaseFeedback(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
context = models.JSONField()

# for RecommendationInteractionEvent class contentnode_id represents:
# the date/time this interaction happened
#
# for RecommendationEvent class contentnode_id represents:
# time_shown: timestamp of when the recommendations are first shown
created_at = models.DateTimeField(auto_now_add=True)

# for RecommendationsEvent class conntentnode_id represents:
# target_topic_id that the ID of the topic the user
# initiated the import from (where the imported content will go)
#
# for ReccomendationsInteractionEvent class contentnode_id represents:
# contentNode_id of one of the item being interacted with
# (this must correspond to one of the items in the “content” array on the RecommendationEvent)
#
# for RecommendationsFlaggedEvent class contentnode_id represents:
# contentnode_id of the content that is being flagged.
contentnode_id = models.UUIDField()

# These are corresponding values of content_id to given contentNode_id for a ContentNode.
content_id = models.UUIDField()

class Meta:
abstract = True


class BaseFeedbackEvent(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
# The ID of the channel being worked on (where the content is being imported into)
# or the Channel Id where the flagged content exists.
target_channel_id = models.UUIDField()

class Meta:
abstract = True


class BaseFeedbackInteractionEvent(models.Model):
feedback_type = models.CharField(max_length=50, choices=feedback.FEEDBACK_TYPE_CHOICES)
feedback_reason = models.TextField()

class Meta:
abstract = True


class FlagFeedbackEvent(BaseFeedback, BaseFeedbackEvent, BaseFeedbackInteractionEvent):
pass


class RecommendationsInteractionEvent(BaseFeedback, BaseFeedbackInteractionEvent):
recommendation_event_id = models.UUIDField()
pass


class RecommendationsEvent(BaseFeedback, BaseFeedbackEvent):
# timestamp of when the user navigated away from the recommendation list
time_hidden = models.DateTimeField()
# A list of JSON blobs, representing the content items in the list of recommendations.
content = models.JSONField(default=list)
pass
90 changes: 90 additions & 0 deletions contentcuration/contentcuration/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import uuid
from uuid import uuid4

import mock
import pytest
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.management import call_command
from django.db.models import Q
from django.db.utils import IntegrityError
from django.test import TestCase
from django.utils import timezone
from le_utils.constants import content_kinds
from le_utils.constants import format_presets
Expand All @@ -21,9 +24,12 @@
from contentcuration.models import CONTENTNODE_TREE_ID_CACHE_KEY
from contentcuration.models import File
from contentcuration.models import FILE_DURATION_CONSTRAINT
from contentcuration.models import FlagFeedbackEvent
from contentcuration.models import generate_object_storage_name
from contentcuration.models import Invitation
from contentcuration.models import object_storage_name
from contentcuration.models import RecommendationsEvent
from contentcuration.models import RecommendationsInteractionEvent
from contentcuration.models import User
from contentcuration.models import UserHistory
from contentcuration.tests import testdata
Expand Down Expand Up @@ -981,3 +987,87 @@ def test_prune(self):
ChannelHistory.prune()
self.assertEqual(2, ChannelHistory.objects.count())
self.assertEqual(2, ChannelHistory.objects.filter(id__in=last_history_ids).count())


class FeedbackModelTests(TestCase):
def setUp(self):
call_command("loadconstants")
self.user = testdata.user()

def _create_base_feedback_data(self, context, contentnode_id, content_id):
base_feedback_data = {
'context': context,
'contentnode_id': contentnode_id,
'content_id': content_id,
}
return base_feedback_data

def _create_recommendation_event(self):
channel = testdata.channel()
node_where_import_was_initiated = testdata.node({"kind_id": content_kinds.TOPIC, "title": "recomendations provided here"})
base_feedback_data = self._create_base_feedback_data(
{'model_version': 1, 'breadcrums': "#Title#->Random"},
node_where_import_was_initiated.id,
node_where_import_was_initiated.content_id
)
recommendations_event = RecommendationsEvent.objects.create(
user=self.user,
target_channel_id=channel.id,
time_hidden=timezone.now(),
content=[{'content_id': str(uuid4()), 'node_id': str(uuid4()), 'channel_id': str(uuid4()), 'score': 4}],
**base_feedback_data
)

return recommendations_event

def test_create_flag_feedback_event(self):
channel = testdata.channel("testchannel")
flagged_node = testdata.node({"kind_id": content_kinds.TOPIC, "title": "SuS ContentNode"})
base_feedback_data = self._create_base_feedback_data(
{'spam': 'Spam or misleading'},
flagged_node.id,
flagged_node.content_id
)
flag_feedback_event = FlagFeedbackEvent.objects.create(
user=self.user,
target_channel_id=channel.id,
**base_feedback_data
)
self.assertEqual(flag_feedback_event.user, self.user)
self.assertEqual(flag_feedback_event.context['spam'], 'Spam or misleading')

def test_create_recommendations_interaction_event(self):
# This represents a node that was recommended by the model and was interacted by user!
recommended_node = testdata.node({"kind_id": content_kinds.TOPIC, "title": "This node was recommended by the model"})
base_feedback_data = self._create_base_feedback_data(
{"comment": "explicit reason given by user why he rejected this node!"},
recommended_node.id,
recommended_node.content_id
)
fk = self._create_recommendation_event().id
rec_interaction_event = RecommendationsInteractionEvent.objects.create(
feedback_type='rejected',
feedback_reason='some predefined reasons like (not related)',
recommendation_event_id=fk,
**base_feedback_data
)
self.assertEqual(rec_interaction_event.feedback_type, 'rejected')
self.assertEqual(rec_interaction_event.feedback_reason, 'some predefined reasons like (not related)')

def test_create_recommendations_event(self):
channel = testdata.channel()
node_where_import_was_initiated = testdata.node({"kind_id": content_kinds.TOPIC, "title": "recomendations provided here"})
base_feedback_data = self._create_base_feedback_data(
{'model_version': 1, 'breadcrums': "#Title#->Random"},
node_where_import_was_initiated.id,
node_where_import_was_initiated.content_id
)
recommendations_event = RecommendationsEvent.objects.create(
user=self.user,
target_channel_id=channel.id,
time_hidden=timezone.now(),
content=[{'content_id': str(uuid4()), 'node_id': str(uuid4()), 'channel_id': str(uuid4()), 'score': 4}],
**base_feedback_data
)
self.assertEqual(len(recommendations_event.content), 1)
self.assertEqual(recommendations_event.content[0]['score'], 4)

0 comments on commit 7a3ed60

Please sign in to comment.