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

Merge branch '7-new-quiz-analytics' into 'master'

Resolve "New quiz analytics"

Closes #7

See merge request !5
parents 969b200f f09da91b
Pipeline #2215 passed with stage
in 3 minutes and 27 seconds
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