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

Merge branch '11-leaving-paying-learners-per-module' into 'master'

Resolve "Leaving paying learners per module"

Closes #11

See merge request !10
parents 3d7e318c bdf17294
Pipeline #2270 passed with stage
in 3 minutes and 22 seconds
......@@ -19,6 +19,7 @@ from rest_framework import serializers
from coursera.filters import GenericFilterSet
from coursera.models import (
Branch,
CertificatePayment,
Course,
CourseMembership,
CourseProgress,
......@@ -215,6 +216,7 @@ class CourseAnalyticsSerializer(CourseSerializer):
cohorts: The number of cohorts in the course.
finished_learners_over_time: The cumulative number of students that finished the course per month.
leaving_learners_per_module: The number of students per module that had their last activity in that module.
leaving_paying_learners_per_module: The number of paying students per module that had their last activity in that module.
average_time: The average time between a student's first and last activity in the course.
average_time_per_module: The average time between a student's first and last activity in a module.
geo_data: The number of enrolled students per country.
......@@ -230,6 +232,7 @@ class CourseAnalyticsSerializer(CourseSerializer):
"cohorts",
"finished_learners_over_time",
"leaving_learners_per_module",
"leaving_paying_learners_per_module",
"average_time",
"average_time_per_module",
"geo_data",
......@@ -243,6 +246,7 @@ class CourseAnalyticsSerializer(CourseSerializer):
cohorts = serializers.IntegerField()
finished_learners_over_time = serializers.SerializerMethodField()
leaving_learners_per_module = serializers.SerializerMethodField()
leaving_paying_learners_per_module = serializers.SerializerMethodField()
average_time = serializers.SerializerMethodField()
average_time_per_module = serializers.SerializerMethodField()
geo_data = serializers.SerializerMethodField()
......@@ -316,6 +320,51 @@ class CourseAnalyticsSerializer(CourseSerializer):
.order_by("order")
)
def get_leaving_paying_learners_per_module(self, obj):
"""
For each module in course `obj`, get the number of learners who
haven't passed the course and whose activity furtherst in the course
was in that module.
"""
form = GenericFilterSet(
self.context["request"].GET,
CourseMembership.objects.none(),
request=self.context["request"],
).form
form.errors
from_date = form.cleaned_data.get("from_date")
to_date = form.cleaned_data.get("to_date")
paying_learners_list = CertificatePayment.objects.filter(
course=obj
).values_list("eitdigital_user_id")
if not to_date:
to_date = now()
if from_date and from_date > to_date:
to_date = from_date
subquery = CountSubquery(
LastActivityPerModule.objects.filter(module_id=OuterRef("pk"))
.filter(timestamp__lt=to_date - timedelta(weeks=6))
.filter(eitdigital_user__in=paying_learners_list)
)
if from_date:
subquery -= CountSubquery(
LastActivityPerModule.objects.filter(module_id=OuterRef("pk")).filter(
timestamp__lt=from_date - timedelta(weeks=6)
)
)
return list(
self._filter_current_branch(obj.pk)
.get()
.modules.values_list("module_id")
.annotate(user_count=subquery)
.order_by("order")
)
def get_average_time(self, obj):
"""
Return the average duration between each learners first and last activity
......
......@@ -35,6 +35,7 @@ def test_course_analytics_view(
- cohorts
- finished_learners_over_time
- leaving_learners_per_module
- leaving_paying_learners_per_module
- average_time
- average_time_per_module
- geo_data
......@@ -43,7 +44,7 @@ def test_course_analytics_view(
Also asserts that the number of database queries does not exceed the
predetermined number of queries required for this endpoint.
"""
with django_assert_max_num_queries(10) as captured:
with django_assert_max_num_queries(12) as captured:
response = teacher_api_client.get(
reverse("coursera-api:course-detail", kwargs={"pk": coursera_course_id})
)
......@@ -67,6 +68,7 @@ def test_course_analytics_view(
"cohorts",
"finished_learners_over_time",
"leaving_learners_per_module",
"leaving_paying_learners_per_module",
"average_time",
"average_time_per_module",
"geo_data",
......@@ -93,15 +95,8 @@ def test_course_analytics_date_filter(
reverse("coursera-api:course-detail", kwargs={"pk": coursera_course_id})
)
assert filtered_response.status_code == 200, str(filtered_response.content)
simple_keys = [
"enrolled_learners",
"leaving_learners",
"finished_learners",
]
list_keys = [
"ratings",
"leaving_learners_per_module",
]
simple_keys = ["enrolled_learners", "leaving_learners", "finished_learners"]
list_keys = ["ratings", "leaving_learners_per_module"]
for key in simple_keys:
assert filtered_response.data[key] <= response.data[key], key
......@@ -134,10 +129,7 @@ def test_course_analytics_date_filter_in_future(teacher_api_client, coursera_cou
"finished_learners",
"cohorts",
]
list_keys = [
"ratings",
"leaving_learners_per_module",
]
list_keys = ["ratings", "leaving_learners_per_module"]
for key in simple_keys:
assert filtered_response.data[key] == 0, key
......@@ -186,6 +178,7 @@ def test_course_analytics_invalid_date_filter(
"cohorts",
"finished_learners_over_time",
"leaving_learners_per_module",
"leaving_paying_learners_per_module",
"average_time",
"average_time_per_module",
"geo_data",
......@@ -490,7 +483,7 @@ def test_video_analytics_view_next_item_quiz(
Test that "next_item" returns the correct item id, type and passing
fraction when the lesson contains a next item of type quiz.
"""
response = teacher_api_client.get(
reverse(
"coursera-api:video-detail",
......@@ -534,18 +527,18 @@ def test_video_list_view(teacher_api_client, coursera_course_id):
reverse("coursera-api:video-list", kwargs={"course_id": coursera_course_id})
)
keys = [
"id",
"branch",
"item_id",
"lesson",
"lesson_name",
"order",
"type",
"name",
"optional",
"watched_video",
"finished_video",
"video_comments",
"id",
"branch",
"item_id",
"lesson",
"lesson_name",
"order",
"type",
"name",
"optional",
"watched_video",
"finished_video",
"video_comments",
"video_likes",
"video_dislikes",
]
......@@ -634,7 +627,7 @@ def test_quiz_analytics_view(
"unique_leaners",
"geo_data",
"geo_data_not_passed",
"score_per_attempt"
"score_per_attempt",
]
assert response.status_code == 200, str(response.content)
assert list(response.data.keys()) == keys
......@@ -782,18 +775,14 @@ def test_quiz_analytics_view_invalid_date_filter(
"unique_leaners",
"geo_data",
"geo_data_not_passed",
"score_per_attempt"
"score_per_attempt",
]
assert response.status_code == 200, str(response.content)
assert list(response.data.keys()) == keys
@pytest.mark.django_db
def test_quiz_analytics_next_quiz(
teacher_api_client,
coursera_alt_course_id,
):
def test_quiz_analytics_next_quiz(teacher_api_client, coursera_alt_course_id):
"""
Test that the quiz detail view returns a correct response when there is a
next quiz.
......@@ -836,11 +825,12 @@ def test_quiz_analytics_next_quiz(
"unique_leaners",
"geo_data",
"geo_data_not_passed",
"score_per_attempt"
"score_per_attempt",
]
assert response.status_code == 200, str(response.content)
assert list(response.data.keys()) == keys
@pytest.mark.django_db
def test_quiz_analytics_no_permissions(
teacher_api_client, coursera_assessment_base_id, coursera_assessment_version
......@@ -1002,10 +992,27 @@ def test_assignment_analytics_view(
kwargs={"course_id": coursera_course_id, "item_id": coursera_assignment_id},
)
)
keys = ["id", "branch", "item_id", "lesson", "lesson_name", "order", "type", "name", "optional", "submissions", "submission_ratio", "average_grade", "next_item", "next_assignment", "submission_ratio_paying_learners"]
keys = [
"id",
"branch",
"item_id",
"lesson",
"lesson_name",
"order",
"type",
"name",
"optional",
"submissions",
"submission_ratio",
"average_grade",
"next_item",
"next_assignment",
"submission_ratio_paying_learners",
]
assert response.status_code == 200, str(response.content)
assert list(response.data.keys()) == keys
@pytest.mark.django_db
def test_assignment_analytics_view_next_assignment(
teacher_api_client, coursera_course_id, coursera_assignment_id
......@@ -1020,7 +1027,23 @@ def test_assignment_analytics_view_next_assignment(
kwargs={"course_id": "V4m7Xf5qEeS9ISIACxWDhA", "item_id": "PoQSi"},
)
)
keys = ["id", "branch", "item_id", "lesson", "lesson_name", "order", "type", "name", "optional", "submissions", "submission_ratio", "average_grade", "next_item", "next_assignment", "submission_ratio_paying_learners"]
keys = [
"id",
"branch",
"item_id",
"lesson",
"lesson_name",
"order",
"type",
"name",
"optional",
"submissions",
"submission_ratio",
"average_grade",
"next_item",
"next_assignment",
"submission_ratio_paying_learners",
]
assert response.status_code == 200, str(response.content)
assert list(response.data.keys()) == keys
......@@ -1107,7 +1130,7 @@ def test_assignment_analytics_view_invalid_date_filter(
"average_grade",
"next_item",
"next_assignment",
"submission_ratio_paying_learners"
"submission_ratio_paying_learners",
]
assert response.status_code == 200, str(response.content)
assert list(response.data.keys()) == keys
......@@ -1161,7 +1184,20 @@ def test_assignment_list_view(teacher_api_client, coursera_course_id):
"coursera-api:assignment-list", kwargs={"course_id": coursera_course_id}
)
)
keys = ["id", "branch", "item_id", "lesson", "lesson_name", "order", "type", "name", "optional", "submissions", "submission_ratio", "average_grade",]
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