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

Resolve "New quiz analytics"

parent 969b200f
from functools import partial
from django.db.models import Avg, Count, DecimalField, FloatField, Max, Min, OuterRef, Q
from django.db.models import Avg, Count, DecimalField, FloatField, Max, Min, OuterRef, Q, F, Subquery
from django.db.models.functions import Cast, Coalesce
from django.utils.functional import cached_property
from rest_framework import serializers
......@@ -13,6 +13,7 @@ from coursera.models import (
ItemGrade,
ItemRating,
Quiz,
EITDigitalUser,
)
from coursera.utils import CountSubquery, NullIf
......@@ -127,6 +128,8 @@ class QuizAnalyticsSerializer(QuizSerializer):
The next item in the lesson.
next_quiz:
The next item of type Quiz in the lesson.
unique_learners:
Number of unique learners taking the quiz.
"""
class Meta(QuizSerializer.Meta):
......@@ -141,6 +144,10 @@ class QuizAnalyticsSerializer(QuizSerializer):
"last_attempt_grade_distribution",
"next_item",
"next_quiz",
"unique_leaners",
"geo_data",
"geo_data_not_passed",
"score_per_attempt"
]
grade_distribution = serializers.SerializerMethodField()
......@@ -153,6 +160,10 @@ class QuizAnalyticsSerializer(QuizSerializer):
last_attempt_grade_distribution = serializers.SerializerMethodField()
next_item = serializers.SerializerMethodField()
next_quiz = serializers.SerializerMethodField()
unique_leaners = serializers.SerializerMethodField()
geo_data = serializers.SerializerMethodField()
geo_data_not_passed = serializers.SerializerMethodField()
score_per_attempt = serializers.SerializerMethodField()
@cached_property
def filter(self):
......@@ -357,3 +368,80 @@ class QuizAnalyticsSerializer(QuizSerializer):
}
except IndexError:
return {"assessment_id": "", "assessment_version": 0}
def get_unique_leaners(self, obj):
"""
Return the number of unique learners that took a quiz.
"""
return Attempt.objects.filter(quiz=obj).values("eitdigital_user").distinct().count()
def get_geo_data(self, obj):
"""
Return the count of countries of users of the quiz.
"""
try:
return obj.geo_data
except AttributeError:
return list(
EITDigitalUser.objects.filter(
eitdigital_user_id__in=Attempt.objects.filter(quiz=obj).values("eitdigital_user_id")
)
.annotate(three_let=F("country_cd__three_let"))
.annotate(country_name=F("country_cd__country"))
.values_list("three_let", "country_name")
.annotate(country_count=Count("eitdigital_user_id"))
)
def get_geo_data_not_passed(self, obj):
"""
Return the count of countries of users of the quiz that have not passed it.
"""
try:
return obj.geo_data
except AttributeError:
return list(
EITDigitalUser.objects.filter(
eitdigital_user_id__in=ItemGrade.objects.filter(
item__quizzes=obj,
course=self.context["course_id"],
passing_state=ItemGrade.NOT_PASSED
).values("eitdigital_user_id"),
).filter(
eitdigital_user_id__in=Attempt.objects.filter(quiz=obj).values("eitdigital_user_id")
)
.annotate(three_let=F("country_cd__three_let"))
.annotate(country_name=F("country_cd__country"))
.values_list("three_let", "country_name")
.annotate(country_count=Count("eitdigital_user_id"))
)
def get_score_per_attempt(self, obj):
"""
Return the average score for each attempt count.
"""
return list(
self.filter(Attempt.objects.filter(quiz=obj))
.annotate(
number_of_attempts=CountSubquery(
GenericFilterSet(
self.context["request"].GET,
Attempt.objects.filter(
quiz_id=OuterRef("quiz_id"),
eitdigital_user_id=OuterRef("eitdigital_user_id"),
),
).qs
)
)
.annotate(
grade=Subquery(self.filter(
ItemGrade.objects.filter(
item__quizzes=obj,
course=self.context["course_id"],
eitdigital_user_id=OuterRef("eitdigital_user_id"),
)
).values("overall"))
)
.values_list("number_of_attempts")
.order_by("number_of_attempts")
.annotate(avg=Avg("grade"))
)
......@@ -590,6 +590,10 @@ def test_quiz_analytics_view(
- last_attempt_grade_distribution
- next_item
- next_quiz
- unique_leaners
- geo_data
- geo_data_not_passed
- score_per_attempt
"""
response = teacher_api_client.get(
reverse(
......@@ -625,6 +629,10 @@ def test_quiz_analytics_view(
"last_attempt_grade_distribution",
"next_item",
"next_quiz",
"unique_leaners",
"geo_data",
"geo_data_not_passed",
"score_per_attempt"
]
assert response.status_code == 200, str(response.content)
assert list(response.data.keys()) == keys
......@@ -768,6 +776,10 @@ def test_quiz_analytics_view_invalid_date_filter(
"last_attempt_grade_distribution",
"next_item",
"next_quiz",
"unique_leaners",
"geo_data",
"geo_data_not_passed",
"score_per_attempt"
]
assert response.status_code == 200, str(response.content)
assert list(response.data.keys()) == keys
......@@ -817,6 +829,10 @@ def test_quiz_analytics_next_quiz(
"last_attempt_grade_distribution",
"next_item",
"next_quiz",
"unique_leaners",
"geo_data",
"geo_data_not_passed",
"score_per_attempt"
]
assert response.status_code == 200, str(response.content)
assert list(response.data.keys()) == keys
......
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