Commit c0ca3fbb authored by Honcoop, T.'s avatar Honcoop, T.
Browse files

Feature/changed list analytics

parent a7d15311
......@@ -17,7 +17,9 @@ from coursera.models import (
__all__ = [
"ItemSerializer",
"VideoAnalyticsSerializer",
"VideoSerializer",
"AssignmentAnalyticsSerializer",
"AssignmentSerializer",
]
......@@ -45,14 +47,9 @@ class ItemSerializer(serializers.ModelSerializer):
def get_lesson_name(self, obj):
return obj.lesson.name
class VideoSerializer(ItemSerializer):
class VideoAnalyticsSerializer(ItemSerializer):
"""
Serializer an Item of type Video with its basic properties
and calculated analytics.
Calculates the following analytics:
watched_video:
Number of people who started watching the video.
finished_video:
......@@ -63,12 +60,6 @@ class VideoAnalyticsSerializer(ItemSerializer):
Number of likes on the video.
video_dislikes:
Number of dislikes on the video.
next_item:
Next item in the lesson.
next_video:
Next item of type Video in the lesson.
views_over_runtime
Number of views per 5-second interval in the video.
"""
class Meta(ItemSerializer.Meta):
......@@ -78,19 +69,13 @@ class VideoAnalyticsSerializer(ItemSerializer):
"video_comments",
"video_likes",
"video_dislikes",
"next_item",
"next_video",
"views_over_runtime",
]
watched_video = serializers.SerializerMethodField()
finished_video = serializers.SerializerMethodField()
video_comments = serializers.SerializerMethodField()
video_likes = serializers.SerializerMethodField()
video_dislikes = serializers.SerializerMethodField()
next_item = serializers.SerializerMethodField()
next_video = serializers.SerializerMethodField()
views_over_runtime = serializers.SerializerMethodField()
@cached_property
def filter(self):
......@@ -211,6 +196,74 @@ class VideoAnalyticsSerializer(ItemSerializer):
"video_likes"
]
class VideoAnalyticsSerializer(VideoSerializer):
"""
Serializer an Item of type Video with its basic properties
and calculated analytics.
Calculates the following analytics:
next_item:
Next item in the lesson.
next_video:
Next item of type Video in the lesson.
views_over_runtime
Number of views per 5-second interval in the video.
"""
class Meta(VideoSerializer.Meta):
fields = VideoSerializer.Meta.fields + [
"next_item",
"next_video",
"views_over_runtime",
]
next_item = serializers.SerializerMethodField()
next_video = serializers.SerializerMethodField()
views_over_runtime = serializers.SerializerMethodField()
@cached_property
def filter(self):
"""
Return a partial that applies the GenericFilterSet to the passed
queryset.
Requires the request object to be in the context.
"""
def get_filterset(data=None, queryset=None, *, request=None, prefix=None):
"""
Apply the GenericFilterSet to the queryset, and return the
filtered queryset.
"""
return GenericFilterSet(data, queryset, request=request, prefix=prefix).qs
return partial(
get_filterset, self.context["request"].GET, request=self.context["request"]
)
@cached_property
def clickstream_filter(self):
"""
Return a partial that applies the GenericFilterSet to the passed
queryset.
Requires the request object to be in the context.
"""
def get_filterset(data=None, queryset=None, *, request=None, prefix=None):
"""
Apply the ClickstreamEventFilterSet to the queryset, and return
the filtered queryset.
"""
return ClickstreamEventFilterSet(
data, queryset, request=request, prefix=prefix
).qs
return partial(
get_filterset, self.context["request"].GET, request=self.context["request"]
)
def get_next_item(self, obj):
"""
Return the next item in the lesson, if any.
......@@ -300,8 +353,7 @@ class VideoAnalyticsSerializer(ItemSerializer):
)
)
class AssignmentAnalyticsSerializer(ItemSerializer):
class AssignmentSerializer(ItemSerializer):
"""
Serialize an Item of type Assignment with its basic properties
and calculated analytics.
......@@ -314,24 +366,40 @@ class AssignmentAnalyticsSerializer(ItemSerializer):
Number of submissions divided by the number of enrolled students.
average_grade:
The average grade of all students who completed the assignment.
next_item:
The next item in the lesson.
next_assignment:
The next item of type Assignment in the lesson.
"""
class Meta(ItemSerializer.Meta):
fields = ItemSerializer.Meta.fields + [
"submissions",
"submission_ratio",
"average_grade",
"next_item",
"next_assignment",
]
submissions = serializers.IntegerField()
submission_ratio = serializers.FloatField()
average_grade = serializers.FloatField()
class AssignmentAnalyticsSerializer(AssignmentSerializer):
"""
Serialize an Item of type Assignment with its basic properties
and calculated analytics.
Calculates the following analytics:
next_item:
The next item in the lesson.
next_assignment:
The next item of type Assignment in the lesson.
"""
class Meta(AssignmentSerializer.Meta):
fields = AssignmentSerializer.Meta.fields + [
"next_item",
"next_assignment",
]
next_item = serializers.SerializerMethodField()
next_assignment = serializers.SerializerMethodField()
......
......@@ -21,7 +21,15 @@ __all__ = ["QuizSerializer", "QuizAnalyticsSerializer"]
class QuizSerializer(serializers.ModelSerializer):
"""
Serialize a Quiz with its basic properties.
Serialize a Quiz with its basic properties and
some basic statistics.
Calculates the following statistics:
average_grade:
The average grade of all students who completed the quiz.
average_attempts:
The average number of attempts per student.
"""
class Meta:
......@@ -38,6 +46,8 @@ class QuizSerializer(serializers.ModelSerializer):
"lesson",
"lesson_name",
"item_id",
"average_grade",
"average_attempts",
]
name = serializers.CharField()
......@@ -45,6 +55,28 @@ class QuizSerializer(serializers.ModelSerializer):
lesson = serializers.SerializerMethodField()
lesson_name = serializers.SerializerMethodField()
item_id = serializers.SerializerMethodField()
average_grade = serializers.FloatField()
average_attempts = serializers.SerializerMethodField()
@cached_property
def filter(self):
"""
Return a partial that applies the GenericFilterSet to the passed
queryset.
Requires the request object to be in the context.
"""
def get_filterset(data=None, queryset=None, *, request=None, prefix=None):
"""
Apply the GenericFilterSet to the queryset, and return the filtered
queryset.
"""
return GenericFilterSet(data, queryset, request=request, prefix=prefix).qs
return partial(
get_filterset, self.context["request"].GET, request=self.context["request"]
)
def get_lesson(self, obj):
return obj.items.all()[0].lesson.lesson_id
......@@ -55,6 +87,18 @@ class QuizSerializer(serializers.ModelSerializer):
def get_item_id(self, obj):
return obj.items.all()[0].item_id
def get_average_attempts(self, obj):
"""
Return the average number of attempts for this quiz within the given
timespan.
"""
return (
self.filter(Attempt.objects.filter(quiz=obj))
.values("eitdigital_user_id")
.annotate(number_of_attempts=Count("timestamp"))
.aggregate(average=Coalesce(Avg("number_of_attempts"), 0))["average"]
)
class QuizAnalyticsSerializer(QuizSerializer):
"""
Serialize a Quiz with its basic properties
......@@ -62,12 +106,8 @@ class QuizAnalyticsSerializer(QuizSerializer):
Calculates the following statistics:
average_grade:
The average grade of all students who completed the quiz.
grade_distribution:
The distribution of grades of all students who completed the quiz.
average_attempts:
The average number of attempts per student.
number_of_attempts:
The distribution of number of attempts per student.
correct_ratio_per_question:
......@@ -91,9 +131,7 @@ class QuizAnalyticsSerializer(QuizSerializer):
class Meta(QuizSerializer.Meta):
fields = QuizSerializer.Meta.fields + [
"average_grade",
"grade_distribution",
"average_attempts",
"number_of_attempts",
"correct_ratio_per_question",
"quiz_comments",
......@@ -105,10 +143,8 @@ class QuizAnalyticsSerializer(QuizSerializer):
"next_quiz",
]
average_grade = serializers.FloatField()
grade_distribution = serializers.SerializerMethodField()
number_of_attempts = serializers.SerializerMethodField()
average_attempts = serializers.SerializerMethodField()
correct_ratio_per_question = serializers.SerializerMethodField()
quiz_comments = serializers.SerializerMethodField()
quiz_likes = serializers.SerializerMethodField()
......@@ -157,18 +193,6 @@ class QuizAnalyticsSerializer(QuizSerializer):
.annotate(num_grades=Count("eitdigital_user"))
)
def get_average_attempts(self, obj):
"""
Return the average number of attempts for this quiz within the given
timespan.
"""
return (
self.filter(Attempt.objects.filter(quiz=obj))
.values("eitdigital_user_id")
.annotate(number_of_attempts=Count("timestamp"))
.aggregate(average=Coalesce(Avg("number_of_attempts"), 0))["average"]
)
def get_number_of_attempts(self, obj):
"""
Return the number of users that have used a specific number of attempts
......
......@@ -11,12 +11,14 @@ from coursera.filters import GenericFilterSet
from coursera.models import Branch, ClickstreamEvent, Course, Item, ItemType, Quiz
from coursera.serializers import (
AssignmentAnalyticsSerializer,
AssignmentSerializer,
CourseAnalyticsSerializer,
CourseSerializer,
ItemSerializer,
QuizAnalyticsSerializer,
QuizSerializer,
VideoAnalyticsSerializer,
VideoSerializer,
)
......@@ -92,7 +94,7 @@ class VideoAnalyticsViewSet(ReadOnlyModelViewSet):
.values("pk")[:1]
)
)
serializer_class = ItemSerializer
serializer_class = VideoSerializer
permission_classes = [IsAuthenticated]
lookup_field = "item_id"
......@@ -193,8 +195,7 @@ class QuizAnalyticsViewSet(ReadOnlyModelViewSet):
.annotate(name=F("items__name"))
.annotate(graded=F("items__type__graded"))
)
if self.action == "retrieve":
queryset = queryset.with_average_grade(self.generic_filterset)
queryset = queryset.with_average_grade(self.generic_filterset)
if "base_id" in self.kwargs:
queryset = queryset.filter(base_id=self.kwargs["base_id"]).order_by(
......@@ -221,7 +222,7 @@ class AssignmentAnalyticsViewSet(ReadOnlyModelViewSet):
.values("pk")[:1]
)
)
serializer_class = ItemSerializer
serializer_class = AssignmentSerializer
permission_classes = [IsAuthenticated]
lookup_field = "item_id"
......@@ -266,12 +267,11 @@ class AssignmentAnalyticsViewSet(ReadOnlyModelViewSet):
.filter(branch__course__in=self.request.user.courses)
.filter(branch__course=self.kwargs["course_id"])
)
if self.action == "retrieve":
queryset = (
queryset.with_submissions(self.generic_filterset)
.with_submission_ratio(self.generic_filterset)
.with_average_grade(self.generic_filterset)
)
queryset = (
queryset.with_submissions(self.generic_filterset)
.with_submission_ratio(self.generic_filterset)
.with_average_grade(self.generic_filterset)
)
if self.action == "list":
queryset = queryset.order_by(
"lesson__module__order", "lesson__order", "order"
......
......@@ -524,11 +524,31 @@ def test_video_list_view(teacher_api_client, coursera_course_id):
- type
- name
- optional
- watched_video
- finished_video
- video_comments
- video_likes
- video_dislikes
"""
response = teacher_api_client.get(
reverse("coursera-api:video-list", kwargs={"course_id": coursera_course_id})
)
keys = ["id", "branch", "item_id", "lesson", "lesson_name", "order", "type", "name", "optional"]
keys = [
"id",
"branch",
"item_id",
"lesson",
"lesson_name",
"order",
"type",
"name",
"optional",
"watched_video",
"finished_video",
"video_comments",
"video_likes",
"video_dislikes",
]
assert response.status_code == 200, str(response.content)
assert len(response.data) > 0, "no videos returned"
for item in response.data:
......@@ -559,8 +579,8 @@ def test_quiz_analytics_view(
- lesson_name
- item_id
- average_grade
- grade_distribution
- average_attempts
- grade_distribution
- number_of_attempts
- correct_ratio_per_question
- quiz_comments
......@@ -594,8 +614,8 @@ def test_quiz_analytics_view(
"lesson_name",
"item_id",
"average_grade",
"grade_distribution",
"average_attempts",
"grade_distribution",
"number_of_attempts",
"correct_ratio_per_question",
"quiz_comments",
......@@ -737,8 +757,8 @@ def test_quiz_analytics_view_invalid_date_filter(
"lesson_name",
"item_id",
"average_grade",
"grade_distribution",
"average_attempts",
"grade_distribution",
"number_of_attempts",
"correct_ratio_per_question",
"quiz_comments",
......@@ -786,8 +806,8 @@ def test_quiz_analytics_next_quiz(
"lesson_name",
"item_id",
"average_grade",
"grade_distribution",
"average_attempts",
"grade_distribution",
"number_of_attempts",
"correct_ratio_per_question",
"quiz_comments",
......@@ -845,6 +865,8 @@ def test_quiz_version_list_view(
- lesson
- lesson_name
- item_id
- average_grade
- average_attempts
"""
response = teacher_api_client.get(
reverse(
......@@ -867,6 +889,8 @@ def test_quiz_version_list_view(
"lesson",
"lesson_name",
"item_id",
"average_grade",
"average_attempts",
]
assert response.status_code == 200, str(response.content)
assert len(response.data) > 0, "no quizzes returned"
......@@ -893,6 +917,8 @@ def test_quiz_list_view(teacher_api_client, coursera_course_id):
- lesson
- lesson_name
- item_id
- average_grade
- average_attempts
"""
response = teacher_api_client.get(
reverse("coursera-api:quiz-list", kwargs={"course_id": coursera_course_id})
......@@ -909,6 +935,8 @@ def test_quiz_list_view(teacher_api_client, coursera_course_id):
"lesson",
"lesson_name",
"item_id",
"average_grade",
"average_attempts",
]
assert response.status_code == 200, str(response.content)
assert len(response.data) > 0, "no quizzes returned"
......@@ -1098,13 +1126,16 @@ def test_assignment_list_view(teacher_api_client, coursera_course_id):
- type
- name
- optional
- submissions
- submission_ratio
- average_grade
"""
response = teacher_api_client.get(
reverse(
"coursera-api:assignment-list", kwargs={"course_id": coursera_course_id}
)
)
keys = ["id", "branch", "item_id", "lesson", "lesson_name", "order", "type", "name", "optional"]
keys = ["id", "branch", "item_id", "lesson", "lesson_name", "order", "type", "name", "optional", "submissions", "submission_ratio", "average_grade",]
assert response.status_code == 200, str(response.content)
assert len(response.data) > 0, "no assignments returned"
for item in response.data:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment