edx-enterprise-data 8.7.0__py3-none-any.whl → 8.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {edx_enterprise_data-8.7.0.dist-info → edx_enterprise_data-8.8.1.dist-info}/METADATA +1 -1
- {edx_enterprise_data-8.7.0.dist-info → edx_enterprise_data-8.8.1.dist-info}/RECORD +23 -20
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/constants.py +9 -3
- enterprise_data/admin_analytics/data_loaders.py +7 -3
- enterprise_data/admin_analytics/utils.py +52 -10
- enterprise_data/api/v1/paginators.py +1 -1
- enterprise_data/api/v1/serializers.py +39 -30
- enterprise_data/api/v1/urls.py +12 -6
- enterprise_data/api/v1/views/analytics_enrollments.py +85 -73
- enterprise_data/api/v1/views/analytics_leaderboard.py +141 -0
- enterprise_data/api/v1/views/enterprise_admin.py +20 -19
- enterprise_data/api/v1/views/enterprise_completions.py +49 -28
- enterprise_data/renderers.py +14 -0
- enterprise_data/tests/admin_analytics/mock_analytics_data.py +511 -0
- enterprise_data/tests/admin_analytics/mock_enrollments.py +4 -4
- enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +23 -22
- enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py +163 -0
- enterprise_data/tests/admin_analytics/test_enterprise_completions.py +1 -1
- enterprise_data/utils.py +16 -0
- {edx_enterprise_data-8.7.0.dist-info → edx_enterprise_data-8.8.1.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-8.7.0.dist-info → edx_enterprise_data-8.8.1.dist-info}/WHEEL +0 -0
- {edx_enterprise_data-8.7.0.dist-info → edx_enterprise_data-8.8.1.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,6 @@
|
|
1
1
|
"""Advance Analytics for Enrollments"""
|
2
|
-
from datetime import datetime
|
2
|
+
from datetime import datetime
|
3
|
+
from logging import getLogger
|
3
4
|
|
4
5
|
from edx_rbac.decorators import permission_required
|
5
6
|
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
@@ -8,35 +9,22 @@ from rest_framework.views import APIView
|
|
8
9
|
|
9
10
|
from django.http import HttpResponse, StreamingHttpResponse
|
10
11
|
|
11
|
-
from enterprise_data.admin_analytics.constants import
|
12
|
-
from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_datetime
|
12
|
+
from enterprise_data.admin_analytics.constants import Calculation, EnrollmentChart, Granularity, ResponseType
|
13
13
|
from enterprise_data.admin_analytics.utils import (
|
14
14
|
calculation_aggregation,
|
15
15
|
fetch_and_cache_enrollments_data,
|
16
|
+
fetch_enrollments_cache_expiry_timestamp,
|
16
17
|
granularity_aggregation,
|
17
18
|
)
|
18
19
|
from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
|
19
20
|
from enterprise_data.api.v1.serializers import (
|
20
|
-
AdvanceAnalyticsEnrollmentSerializer,
|
21
21
|
AdvanceAnalyticsEnrollmentStatsSerializer,
|
22
|
+
AdvanceAnalyticsQueryParamSerializer,
|
22
23
|
)
|
23
24
|
from enterprise_data.renderers import IndividualEnrollmentsCSVRenderer
|
24
|
-
from enterprise_data.utils import date_filter
|
25
|
+
from enterprise_data.utils import date_filter, timer
|
25
26
|
|
26
|
-
|
27
|
-
def fetch_enrollments_cache_expiry_timestamp():
|
28
|
-
"""Calculate cache expiry timestamp"""
|
29
|
-
# TODO: Implement correct cache expiry logic for `enrollments` data.
|
30
|
-
# Current cache expiry logic is based on `enterprise_learner_enrollment` table,
|
31
|
-
# Which has nothing to do with the `enrollments` data. Instead cache expiry should
|
32
|
-
# be based on `fact_enrollment_admin_dash` table. Currently we have no timestamp in
|
33
|
-
# `fact_enrollment_admin_dash` table that can be used for cache expiry. Add a new
|
34
|
-
# column in the table for this purpose and then use that column for cache expiry.
|
35
|
-
last_updated_at = fetch_max_enrollment_datetime()
|
36
|
-
cache_expiry = (
|
37
|
-
last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
|
38
|
-
)
|
39
|
-
return cache_expiry
|
27
|
+
LOGGER = getLogger(__name__)
|
40
28
|
|
41
29
|
|
42
30
|
class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
|
@@ -50,7 +38,7 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
|
|
50
38
|
@permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
|
51
39
|
def get(self, request, enterprise_uuid):
|
52
40
|
"""Get individual enrollments data"""
|
53
|
-
serializer =
|
41
|
+
serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
|
54
42
|
serializer.is_valid(raise_exception=True)
|
55
43
|
|
56
44
|
cache_expiry = fetch_enrollments_cache_expiry_timestamp()
|
@@ -59,7 +47,14 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
|
|
59
47
|
# get values from query params or use default values
|
60
48
|
start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
|
61
49
|
end_date = serializer.data.get('end_date', datetime.now())
|
62
|
-
|
50
|
+
response_type = request.query_params.get('response_type', ResponseType.JSON.value)
|
51
|
+
|
52
|
+
LOGGER.info(
|
53
|
+
"Individual enrollments data requested for enterprise [%s] from [%s] to [%s]",
|
54
|
+
enterprise_uuid,
|
55
|
+
start_date,
|
56
|
+
end_date,
|
57
|
+
)
|
63
58
|
|
64
59
|
# filter enrollments by date
|
65
60
|
enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
|
@@ -77,11 +72,19 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
|
|
77
72
|
enrollments["enterprise_enrollment_date"] = enrollments["enterprise_enrollment_date"].dt.date
|
78
73
|
enrollments = enrollments.sort_values(by="enterprise_enrollment_date", ascending=False).reset_index(drop=True)
|
79
74
|
|
80
|
-
|
75
|
+
LOGGER.info(
|
76
|
+
"Individual enrollments data prepared for enterprise [%s] from [%s] to [%s]",
|
77
|
+
enterprise_uuid,
|
78
|
+
start_date,
|
79
|
+
end_date,
|
80
|
+
)
|
81
|
+
|
82
|
+
if response_type == ResponseType.CSV.value:
|
83
|
+
filename = f"""individual_enrollments, {start_date} - {end_date}.csv"""
|
81
84
|
return StreamingHttpResponse(
|
82
85
|
IndividualEnrollmentsCSVRenderer().render(self._stream_serialized_data(enrollments)),
|
83
86
|
content_type="text/csv",
|
84
|
-
headers={"Content-Disposition": 'attachment; filename="
|
87
|
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
85
88
|
)
|
86
89
|
|
87
90
|
paginator = self.pagination_class()
|
@@ -121,51 +124,60 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
|
|
121
124
|
# get values from query params or use default
|
122
125
|
start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
|
123
126
|
end_date = serializer.data.get('end_date', datetime.now())
|
124
|
-
granularity = serializer.data.get('granularity',
|
125
|
-
calculation = serializer.data.get('calculation',
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
127
|
+
granularity = serializer.data.get('granularity', Granularity.DAILY.value)
|
128
|
+
calculation = serializer.data.get('calculation', Calculation.TOTAL.value)
|
129
|
+
response_type = serializer.data.get('response_type', ResponseType.JSON.value)
|
130
|
+
chart_type = serializer.data.get('chart_type')
|
131
|
+
|
132
|
+
# TODO: Add validation that if response_type is CSV then chart_type must be provided
|
133
|
+
|
134
|
+
if response_type == ResponseType.JSON.value:
|
135
|
+
with timer('construct_enrollment_all_stats'):
|
136
|
+
data = {
|
137
|
+
"enrollments_over_time": self.construct_enrollments_over_time(
|
138
|
+
enrollments_df.copy(),
|
139
|
+
start_date,
|
140
|
+
end_date,
|
141
|
+
granularity,
|
142
|
+
calculation,
|
143
|
+
),
|
144
|
+
"top_courses_by_enrollments": self.construct_top_courses_by_enrollments(
|
145
|
+
enrollments_df.copy(),
|
146
|
+
start_date,
|
147
|
+
end_date,
|
148
|
+
),
|
149
|
+
"top_subjects_by_enrollments": self.construct_top_subjects_by_enrollments(
|
150
|
+
enrollments_df.copy(),
|
151
|
+
start_date,
|
152
|
+
end_date,
|
153
|
+
),
|
154
|
+
}
|
148
155
|
return Response(data)
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
156
|
+
|
157
|
+
if response_type == ResponseType.CSV.value:
|
158
|
+
if chart_type == EnrollmentChart.ENROLLMENTS_OVER_TIME.value:
|
159
|
+
with timer('construct_enrollments_over_time_csv'):
|
160
|
+
return self.construct_enrollments_over_time_csv(
|
161
|
+
enrollments_df.copy(),
|
162
|
+
start_date,
|
163
|
+
end_date,
|
164
|
+
granularity,
|
165
|
+
calculation,
|
166
|
+
)
|
167
|
+
elif chart_type == EnrollmentChart.TOP_COURSES_BY_ENROLLMENTS.value:
|
168
|
+
with timer('construct_top_courses_by_enrollments_csv'):
|
169
|
+
return self.construct_top_courses_by_enrollments_csv(
|
170
|
+
enrollments_df.copy(),
|
171
|
+
start_date,
|
172
|
+
end_date,
|
173
|
+
)
|
174
|
+
elif chart_type == EnrollmentChart.TOP_SUBJECTS_BY_ENROLLMENTS.value:
|
175
|
+
with timer('construct_top_subjects_by_enrollments_csv'):
|
176
|
+
return self.construct_top_subjects_by_enrollments_csv(
|
177
|
+
enrollments_df.copy(),
|
178
|
+
start_date,
|
179
|
+
end_date,
|
180
|
+
)
|
169
181
|
|
170
182
|
def enrollments_over_time_common(self, enrollments_df, start_date, end_date, granularity, calculation):
|
171
183
|
"""
|
@@ -175,8 +187,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
|
|
175
187
|
enrollments_df {DataFrame} -- DataFrame of enrollments
|
176
188
|
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
177
189
|
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
178
|
-
granularity {str} -- Granularity of the data. One of
|
179
|
-
calculation {str} -- Calculation of the data. One of
|
190
|
+
granularity {str} -- Granularity of the data. One of Granularity choices
|
191
|
+
calculation {str} -- Calculation of the data. One of Calculation choices
|
180
192
|
"""
|
181
193
|
# filter enrollments by date
|
182
194
|
enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
|
@@ -202,8 +214,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
|
|
202
214
|
enrollments_df {DataFrame} -- DataFrame of enrollments
|
203
215
|
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
204
216
|
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
205
|
-
granularity {str} -- Granularity of the data. One of
|
206
|
-
calculation {str} -- Calculation of the data. One of
|
217
|
+
granularity {str} -- Granularity of the data. One of Granularity choices
|
218
|
+
calculation {str} -- Calculation of the data. One of Calculation choices
|
207
219
|
"""
|
208
220
|
enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
|
209
221
|
|
@@ -218,8 +230,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
|
|
218
230
|
enrollments_df {DataFrame} -- DataFrame of enrollments
|
219
231
|
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
220
232
|
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
221
|
-
granularity {str} -- Granularity of the data. One of
|
222
|
-
calculation {str} -- Calculation of the data. One of
|
233
|
+
granularity {str} -- Granularity of the data. One of Granularity choices
|
234
|
+
calculation {str} -- Calculation of the data. One of Calculation choices
|
223
235
|
"""
|
224
236
|
enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
|
225
237
|
|
@@ -0,0 +1,141 @@
|
|
1
|
+
"""Advance Analytics for Leaderboard"""
|
2
|
+
from datetime import datetime
|
3
|
+
from logging import getLogger
|
4
|
+
|
5
|
+
import numpy as np
|
6
|
+
import pandas as pd
|
7
|
+
from edx_rbac.decorators import permission_required
|
8
|
+
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
9
|
+
from rest_framework.views import APIView
|
10
|
+
|
11
|
+
from django.http import StreamingHttpResponse
|
12
|
+
|
13
|
+
from enterprise_data.admin_analytics.constants import ResponseType
|
14
|
+
from enterprise_data.admin_analytics.utils import (
|
15
|
+
fetch_and_cache_engagements_data,
|
16
|
+
fetch_and_cache_enrollments_data,
|
17
|
+
fetch_engagements_cache_expiry_timestamp,
|
18
|
+
fetch_enrollments_cache_expiry_timestamp,
|
19
|
+
)
|
20
|
+
from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
|
21
|
+
from enterprise_data.api.v1.serializers import AdvanceAnalyticsQueryParamSerializer
|
22
|
+
from enterprise_data.renderers import LeaderboardCSVRenderer
|
23
|
+
from enterprise_data.utils import date_filter
|
24
|
+
|
25
|
+
LOGGER = getLogger(__name__)
|
26
|
+
|
27
|
+
|
28
|
+
class AdvanceAnalyticsLeaderboardView(APIView):
|
29
|
+
"""
|
30
|
+
API for getting the advance analytics leaderboard data.
|
31
|
+
"""
|
32
|
+
authentication_classes = (JwtAuthentication,)
|
33
|
+
pagination_class = AdvanceAnalyticsPagination
|
34
|
+
http_method_names = ['get']
|
35
|
+
|
36
|
+
@permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
|
37
|
+
def get(self, request, enterprise_uuid):
|
38
|
+
"""Get leaderboard data"""
|
39
|
+
serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
|
40
|
+
serializer.is_valid(raise_exception=True)
|
41
|
+
|
42
|
+
enrollments_cache_expiry = fetch_enrollments_cache_expiry_timestamp()
|
43
|
+
enrollments_df = fetch_and_cache_enrollments_data(enterprise_uuid, enrollments_cache_expiry)
|
44
|
+
|
45
|
+
engagements_cache_expiry = fetch_engagements_cache_expiry_timestamp()
|
46
|
+
engagements_df = fetch_and_cache_engagements_data(enterprise_uuid, engagements_cache_expiry)
|
47
|
+
|
48
|
+
start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
|
49
|
+
end_date = serializer.data.get('end_date', datetime.now())
|
50
|
+
response_type = serializer.data.get('response_type', ResponseType.JSON.value)
|
51
|
+
|
52
|
+
LOGGER.info(
|
53
|
+
"Leaderboard data requested for enterprise [%s] from [%s] to [%s]",
|
54
|
+
enterprise_uuid,
|
55
|
+
start_date,
|
56
|
+
end_date,
|
57
|
+
)
|
58
|
+
|
59
|
+
# only include learners who have passed the course
|
60
|
+
enrollments_df = enrollments_df[enrollments_df["has_passed"] == 1]
|
61
|
+
|
62
|
+
# filter enrollments by date
|
63
|
+
enrollments_df = date_filter(start_date, end_date, enrollments_df, "passed_date")
|
64
|
+
|
65
|
+
completions = enrollments_df.groupby(["email"]).size().reset_index()
|
66
|
+
completions.columns = ["email", "course_completions"]
|
67
|
+
|
68
|
+
# filter engagements by date
|
69
|
+
engagements_df = date_filter(start_date, end_date, engagements_df, "activity_date")
|
70
|
+
|
71
|
+
engage = (
|
72
|
+
engagements_df.groupby(["email"])
|
73
|
+
.agg({"is_engaged": ["sum"], "learning_time_seconds": ["sum"]})
|
74
|
+
.reset_index()
|
75
|
+
)
|
76
|
+
engage.columns = ["email", "daily_sessions", "learning_time_seconds"]
|
77
|
+
engage["learning_time_hours"] = round(
|
78
|
+
engage["learning_time_seconds"].astype("float") / 60 / 60, 1
|
79
|
+
)
|
80
|
+
|
81
|
+
# if daily_sessions is 0, set average_session_length to 0 becuase otherwise it will be `inf`
|
82
|
+
engage["average_session_length"] = np.where(
|
83
|
+
engage["daily_sessions"] == 0,
|
84
|
+
0,
|
85
|
+
round(engage["learning_time_hours"] / engage["daily_sessions"].astype("float"), 1)
|
86
|
+
)
|
87
|
+
|
88
|
+
leaderboard_df = engage.merge(completions, on="email", how="left")
|
89
|
+
leaderboard_df = leaderboard_df.sort_values(
|
90
|
+
by=["learning_time_hours", "daily_sessions", "course_completions"],
|
91
|
+
ascending=[False, False, False],
|
92
|
+
)
|
93
|
+
|
94
|
+
# move the aggregated row with email 'null' to the end of the table
|
95
|
+
idx = leaderboard_df.index[leaderboard_df['email'] == 'null']
|
96
|
+
leaderboard_df.loc[idx, 'email'] = 'learners who have not shared consent'
|
97
|
+
leaderboard_df = pd.concat([leaderboard_df.drop(idx), leaderboard_df.loc[idx]])
|
98
|
+
|
99
|
+
# convert `nan` values to `None` because `nan` is not JSON serializable
|
100
|
+
leaderboard_df = leaderboard_df.replace(np.nan, None)
|
101
|
+
|
102
|
+
LOGGER.info(
|
103
|
+
"Leaderboard data prepared for enterprise [%s] from [%s] to [%s]",
|
104
|
+
enterprise_uuid,
|
105
|
+
start_date,
|
106
|
+
end_date,
|
107
|
+
)
|
108
|
+
|
109
|
+
if response_type == ResponseType.CSV.value:
|
110
|
+
filename = f"""Leaderboard, {start_date} - {end_date}.csv"""
|
111
|
+
leaderboard_df = leaderboard_df[
|
112
|
+
[
|
113
|
+
"email",
|
114
|
+
"learning_time_hours",
|
115
|
+
"daily_sessions",
|
116
|
+
"average_session_length",
|
117
|
+
"course_completions",
|
118
|
+
]
|
119
|
+
]
|
120
|
+
return StreamingHttpResponse(
|
121
|
+
LeaderboardCSVRenderer().render(self._stream_serialized_data(leaderboard_df)),
|
122
|
+
content_type="text/csv",
|
123
|
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
124
|
+
)
|
125
|
+
|
126
|
+
paginator = self.pagination_class()
|
127
|
+
page = paginator.paginate_queryset(leaderboard_df, request)
|
128
|
+
serialized_data = page.data.to_dict(orient='records')
|
129
|
+
response = paginator.get_paginated_response(serialized_data)
|
130
|
+
|
131
|
+
return response
|
132
|
+
|
133
|
+
def _stream_serialized_data(self, leaderboard_df, chunk_size=50000):
|
134
|
+
"""
|
135
|
+
Stream the serialized data.
|
136
|
+
"""
|
137
|
+
total_rows = leaderboard_df.shape[0]
|
138
|
+
for start_index in range(0, total_rows, chunk_size):
|
139
|
+
end_index = min(start_index + chunk_size, total_rows)
|
140
|
+
chunk = leaderboard_df.iloc[start_index:end_index]
|
141
|
+
yield from chunk.to_dict(orient='records')
|
@@ -27,7 +27,7 @@ from enterprise_data.models import (
|
|
27
27
|
EnterpriseAdminSummarizeInsights,
|
28
28
|
EnterpriseExecEdLCModulePerformance,
|
29
29
|
)
|
30
|
-
from enterprise_data.utils import date_filter
|
30
|
+
from enterprise_data.utils import date_filter, timer
|
31
31
|
|
32
32
|
from .base import EnterpriseViewSetMixin
|
33
33
|
|
@@ -211,24 +211,25 @@ class EnterpriseAdminAnalyticsSkillsView(APIView):
|
|
211
211
|
csv_data.to_csv(path_or_buf=response, index=False)
|
212
212
|
return response
|
213
213
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
214
|
+
with timer('skills_all_charts_data'):
|
215
|
+
top_skills = get_skills_chart_data(
|
216
|
+
chart_type=ChartType.BUBBLE,
|
217
|
+
start_date=start_date,
|
218
|
+
end_date=end_date,
|
219
|
+
skills=skills,
|
220
|
+
)
|
221
|
+
top_skills_enrollments = get_skills_chart_data(
|
222
|
+
chart_type=ChartType.TOP_SKILLS_ENROLLMENT,
|
223
|
+
start_date=start_date,
|
224
|
+
end_date=end_date,
|
225
|
+
skills=skills,
|
226
|
+
)
|
227
|
+
top_skills_by_completions = get_skills_chart_data(
|
228
|
+
chart_type=ChartType.TOP_SKILLS_COMPLETION,
|
229
|
+
start_date=start_date,
|
230
|
+
end_date=end_date,
|
231
|
+
skills=skills,
|
232
|
+
)
|
232
233
|
|
233
234
|
response_data = {
|
234
235
|
"top_skills": top_skills.to_dict(orient="records"),
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""Views for enterprise admin completions analytics."""
|
2
2
|
import datetime
|
3
3
|
from datetime import datetime, timedelta
|
4
|
+
from logging import getLogger
|
4
5
|
|
5
6
|
from edx_rbac.decorators import permission_required
|
6
7
|
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
@@ -18,12 +19,14 @@ from enterprise_data.admin_analytics.completions_utils import (
|
|
18
19
|
get_top_courses_by_completions,
|
19
20
|
get_top_subjects_by_completions,
|
20
21
|
)
|
21
|
-
from enterprise_data.admin_analytics.constants import
|
22
|
+
from enterprise_data.admin_analytics.constants import Calculation, Granularity
|
22
23
|
from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_datetime
|
23
24
|
from enterprise_data.admin_analytics.utils import ChartType, fetch_and_cache_enrollments_data
|
24
25
|
from enterprise_data.api.v1 import serializers
|
25
26
|
from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
|
26
|
-
from enterprise_data.utils import date_filter
|
27
|
+
from enterprise_data.utils import date_filter, timer
|
28
|
+
|
29
|
+
LOGGER = getLogger(__name__)
|
27
30
|
|
28
31
|
|
29
32
|
class EnterrpiseAdminCompletionsStatsView(APIView):
|
@@ -67,39 +70,43 @@ class EnterrpiseAdminCompletionsStatsView(APIView):
|
|
67
70
|
csv_data = {}
|
68
71
|
|
69
72
|
if chart_type == ChartType.COMPLETIONS_OVER_TIME.value:
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
73
|
+
with timer('completions_over_time_csv_data'):
|
74
|
+
csv_data = get_csv_data_for_completions_over_time(
|
75
|
+
start_date=start_date,
|
76
|
+
end_date=end_date,
|
77
|
+
enrollments=enrollments.copy(),
|
78
|
+
date_agg=serializer.data.get('granularity', Granularity.DAILY.value),
|
79
|
+
calc=serializer.data.get('calculation', Calculation.TOTAL.value),
|
80
|
+
)
|
77
81
|
elif chart_type == ChartType.TOP_COURSES_BY_COMPLETIONS.value:
|
78
|
-
|
79
|
-
|
80
|
-
|
82
|
+
with timer('top_courses_by_completions_csv_data'):
|
83
|
+
csv_data = get_csv_data_for_top_courses_by_completions(
|
84
|
+
start_date=start_date, end_date=end_date, enrollments=enrollments.copy()
|
85
|
+
)
|
81
86
|
elif chart_type == ChartType.TOP_SUBJECTS_BY_COMPLETIONS.value:
|
82
|
-
|
83
|
-
|
84
|
-
|
87
|
+
with timer('top_subjects_by_completions_csv_data'):
|
88
|
+
csv_data = get_csv_data_for_top_subjects_by_completions(
|
89
|
+
start_date=start_date, end_date=end_date, enrollments=enrollments.copy()
|
90
|
+
)
|
85
91
|
filename = csv_data['filename']
|
86
92
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
87
93
|
csv_data['data'].to_csv(path_or_buf=response)
|
88
94
|
return response
|
89
95
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
96
|
+
with timer('completions_all_charts_data'):
|
97
|
+
completions_over_time = get_completions_over_time(
|
98
|
+
start_date=start_date,
|
99
|
+
end_date=end_date,
|
100
|
+
dff=enrollments.copy(),
|
101
|
+
date_agg=serializer.data.get('granularity', Granularity.DAILY.value),
|
102
|
+
calc=serializer.data.get('calculation', Calculation.TOTAL.value),
|
103
|
+
)
|
104
|
+
top_courses_by_completions = get_top_courses_by_completions(
|
105
|
+
start_date=start_date, end_date=end_date, dff=enrollments.copy()
|
106
|
+
)
|
107
|
+
top_subjects_by_completions = get_top_subjects_by_completions(
|
108
|
+
start_date=start_date, end_date=end_date, dff=enrollments.copy()
|
109
|
+
)
|
103
110
|
|
104
111
|
return Response(
|
105
112
|
data={
|
@@ -153,6 +160,13 @@ class EnterrpiseAdminCompletionsView(APIView):
|
|
153
160
|
)
|
154
161
|
end_date = serializer.data.get('end_date', datetime.now())
|
155
162
|
|
163
|
+
LOGGER.info(
|
164
|
+
"Completions data requested for enterprise [%s] from [%s] to [%s]",
|
165
|
+
enterprise_id,
|
166
|
+
start_date,
|
167
|
+
end_date,
|
168
|
+
)
|
169
|
+
|
156
170
|
dff = enrollments[enrollments['has_passed'] == 1]
|
157
171
|
|
158
172
|
# Date filtering
|
@@ -162,6 +176,13 @@ class EnterrpiseAdminCompletionsView(APIView):
|
|
162
176
|
dff['passed_date'] = dff['passed_date'].dt.date
|
163
177
|
dff = dff.sort_values(by="passed_date", ascending=False).reset_index(drop=True)
|
164
178
|
|
179
|
+
LOGGER.info(
|
180
|
+
"Completions data prepared for enterprise [%s] from [%s] to [%s]",
|
181
|
+
enterprise_id,
|
182
|
+
start_date,
|
183
|
+
end_date,
|
184
|
+
)
|
185
|
+
|
165
186
|
if serializer.data.get('response_type') == 'csv':
|
166
187
|
response = HttpResponse(content_type='text/csv')
|
167
188
|
filename = f"Individual Completions, {start_date} - {end_date}.csv"
|
enterprise_data/renderers.py
CHANGED
@@ -43,3 +43,17 @@ class IndividualEnrollmentsCSVRenderer(CSVStreamingRenderer):
|
|
43
43
|
'enroll_type',
|
44
44
|
'enterprise_enrollment_date',
|
45
45
|
]
|
46
|
+
|
47
|
+
|
48
|
+
class LeaderboardCSVRenderer(CSVStreamingRenderer):
|
49
|
+
"""
|
50
|
+
Custom streaming csv renderer for advance analytics leaderboard data.
|
51
|
+
"""
|
52
|
+
|
53
|
+
header = [
|
54
|
+
'email',
|
55
|
+
'learning_time_hours',
|
56
|
+
'daily_sessions',
|
57
|
+
'average_session_length',
|
58
|
+
'course_completions',
|
59
|
+
]
|