edx-enterprise-data 8.9.0__py3-none-any.whl → 8.11.0__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.9.0.dist-info → edx_enterprise_data-8.11.0.dist-info}/METADATA +1 -1
- {edx_enterprise_data-8.9.0.dist-info → edx_enterprise_data-8.11.0.dist-info}/RECORD +27 -17
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/constants.py +8 -0
- enterprise_data/admin_analytics/database/__init__.py +4 -0
- enterprise_data/admin_analytics/database/queries/__init__.py +5 -0
- enterprise_data/admin_analytics/database/queries/fact_engagement_admin_dash.py +21 -0
- enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py +61 -0
- enterprise_data/admin_analytics/database/tables/__init__.py +5 -0
- enterprise_data/admin_analytics/database/tables/base.py +18 -0
- enterprise_data/admin_analytics/database/tables/fact_engagement_admin_dash.py +38 -0
- enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py +76 -0
- enterprise_data/admin_analytics/{database.py → database/utils.py} +3 -2
- enterprise_data/api/v1/serializers.py +31 -1
- enterprise_data/api/v1/urls.py +14 -0
- enterprise_data/api/v1/views/analytics_engagements.py +395 -0
- enterprise_data/api/v1/views/analytics_enrollments.py +1 -0
- enterprise_data/api/v1/views/analytics_leaderboard.py +4 -1
- enterprise_data/api/v1/views/enterprise_admin.py +15 -44
- enterprise_data/api/v1/views/enterprise_completions.py +2 -0
- enterprise_data/renderers.py +14 -0
- enterprise_data/tests/admin_analytics/mock_analytics_data.py +41 -1
- enterprise_data/tests/admin_analytics/test_analytics_engagements.py +390 -0
- enterprise_data/tests/api/v1/views/test_enterprise_admin.py +43 -20
- {edx_enterprise_data-8.9.0.dist-info → edx_enterprise_data-8.11.0.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-8.9.0.dist-info → edx_enterprise_data-8.11.0.dist-info}/WHEEL +0 -0
- {edx_enterprise_data-8.9.0.dist-info → edx_enterprise_data-8.11.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,395 @@
|
|
1
|
+
"""Advance Analytics for Engagements"""
|
2
|
+
from datetime import datetime
|
3
|
+
|
4
|
+
from edx_rbac.decorators import permission_required
|
5
|
+
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
6
|
+
from rest_framework import status as rest_status
|
7
|
+
from rest_framework.response import Response
|
8
|
+
from rest_framework.views import APIView
|
9
|
+
|
10
|
+
from django.http import HttpResponse, StreamingHttpResponse
|
11
|
+
|
12
|
+
from enterprise_data.admin_analytics.constants import Calculation, EngagementChart, Granularity, ResponseType
|
13
|
+
from enterprise_data.admin_analytics.utils import (
|
14
|
+
calculation_aggregation,
|
15
|
+
fetch_and_cache_engagements_data,
|
16
|
+
fetch_and_cache_enrollments_data,
|
17
|
+
fetch_enrollments_cache_expiry_timestamp,
|
18
|
+
granularity_aggregation,
|
19
|
+
)
|
20
|
+
from enterprise_data.api.v1 import serializers
|
21
|
+
from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
|
22
|
+
from enterprise_data.renderers import IndividualEngagementsCSVRenderer
|
23
|
+
from enterprise_data.utils import date_filter
|
24
|
+
|
25
|
+
|
26
|
+
class AdvanceAnalyticsIndividualEngagementsView(APIView):
|
27
|
+
"""
|
28
|
+
API for getting the advance analytics individual engagements data.
|
29
|
+
"""
|
30
|
+
|
31
|
+
authentication_classes = (JwtAuthentication,)
|
32
|
+
pagination_class = AdvanceAnalyticsPagination
|
33
|
+
http_method_names = ["get"]
|
34
|
+
|
35
|
+
@permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
|
36
|
+
def get(self, request, enterprise_uuid):
|
37
|
+
"""
|
38
|
+
HTTP GET endpoint to retrieve the enterprise engagements data.
|
39
|
+
"""
|
40
|
+
serializer = serializers.AdvanceAnalyticsEngagementStatsSerializer(data=request.GET)
|
41
|
+
serializer.is_valid(raise_exception=True)
|
42
|
+
cache_expiry = fetch_enrollments_cache_expiry_timestamp()
|
43
|
+
|
44
|
+
enrollment_df = fetch_and_cache_enrollments_data(enterprise_uuid, cache_expiry).copy()
|
45
|
+
engagement_df = fetch_and_cache_engagements_data(enterprise_uuid, cache_expiry).copy()
|
46
|
+
# Use start and end date if provided by the client, if client has not provided then use
|
47
|
+
# 1. minimum enrollment date from the data as the start_date
|
48
|
+
# 2. today's date as the end_date
|
49
|
+
start_date = serializer.data.get('start_date', enrollment_df.enterprise_enrollment_date.min())
|
50
|
+
end_date = serializer.data.get('end_date', datetime.now())
|
51
|
+
response_type = request.query_params.get('response_type', ResponseType.JSON.value)
|
52
|
+
# Date filtering.
|
53
|
+
engagements = date_filter(
|
54
|
+
start=start_date, end=end_date, data_frame=engagement_df.copy(), date_column='activity_date'
|
55
|
+
)
|
56
|
+
engagements["learning_time_hours"] = engagements["learning_time_seconds"] / 60 / 60
|
57
|
+
engagements = engagements[engagements["learning_time_hours"] > 0]
|
58
|
+
engagements["learning_time_hours"] = round(engagements["learning_time_hours"].astype(float), 1)
|
59
|
+
|
60
|
+
# Select only the columns that will be in the table.
|
61
|
+
engagements = engagements[
|
62
|
+
[
|
63
|
+
"email",
|
64
|
+
"course_title",
|
65
|
+
"activity_date",
|
66
|
+
"course_subject",
|
67
|
+
"learning_time_hours",
|
68
|
+
]
|
69
|
+
]
|
70
|
+
engagements["activity_date"] = engagements["activity_date"].dt.date
|
71
|
+
engagements = engagements.sort_values(by="activity_date", ascending=False).reset_index(drop=True)
|
72
|
+
if response_type == ResponseType.CSV.value:
|
73
|
+
response = StreamingHttpResponse(
|
74
|
+
IndividualEngagementsCSVRenderer().render(self._stream_serialized_data(engagements)),
|
75
|
+
content_type="text/csv"
|
76
|
+
)
|
77
|
+
start_date = start_date.strftime('%Y/%m/%d')
|
78
|
+
end_date = end_date.strftime('%Y/%m/%d')
|
79
|
+
filename = f"""Individual Engagements, {start_date} - {end_date}.csv"""
|
80
|
+
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
81
|
+
response['Access-Control-Expose-Headers'] = 'Content-Disposition'
|
82
|
+
return response
|
83
|
+
|
84
|
+
paginator = self.pagination_class()
|
85
|
+
page = paginator.paginate_queryset(engagements, request)
|
86
|
+
serialized_data = page.data.to_dict(orient='records')
|
87
|
+
response = paginator.get_paginated_response(serialized_data)
|
88
|
+
|
89
|
+
return response
|
90
|
+
|
91
|
+
def _stream_serialized_data(self, engagements, chunk_size=50000):
|
92
|
+
"""
|
93
|
+
Stream the serialized data.
|
94
|
+
"""
|
95
|
+
total_rows = engagements.shape[0]
|
96
|
+
for start_index in range(0, total_rows, chunk_size):
|
97
|
+
end_index = min(start_index + chunk_size, total_rows)
|
98
|
+
chunk = engagements.iloc[start_index:end_index]
|
99
|
+
yield from chunk.to_dict(orient='records')
|
100
|
+
|
101
|
+
|
102
|
+
class AdvanceAnalyticsEngagementStatsView(APIView):
|
103
|
+
"""
|
104
|
+
API for getting the advance analytics engagements statistics data.
|
105
|
+
"""
|
106
|
+
|
107
|
+
authentication_classes = (JwtAuthentication,)
|
108
|
+
http_method_names = ["get"]
|
109
|
+
|
110
|
+
@permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
|
111
|
+
def get(self, request, enterprise_uuid):
|
112
|
+
"""
|
113
|
+
HTTP GET endpoint to retrieve the enterprise engagements statistics data.
|
114
|
+
"""
|
115
|
+
serializer = serializers.AdvanceAnalyticsEngagementStatsSerializer(data=request.GET)
|
116
|
+
serializer.is_valid(raise_exception=True)
|
117
|
+
|
118
|
+
cache_expiry = fetch_enrollments_cache_expiry_timestamp()
|
119
|
+
|
120
|
+
enrollment_df = fetch_and_cache_enrollments_data(enterprise_uuid, cache_expiry).copy()
|
121
|
+
engagement_df = fetch_and_cache_engagements_data(enterprise_uuid, cache_expiry).copy()
|
122
|
+
# Use start and end date if provided by the client, if client has not provided then use
|
123
|
+
# 1. minimum enrollment date from the data as the start_date
|
124
|
+
# 2. today's date as the end_date
|
125
|
+
start_date = serializer.data.get('start_date', enrollment_df.enterprise_enrollment_date.min())
|
126
|
+
end_date = serializer.data.get('end_date', datetime.now())
|
127
|
+
granularity = serializer.data.get('granularity', Granularity.DAILY.value)
|
128
|
+
calculation = serializer.data.get('calculation', Calculation.TOTAL.value)
|
129
|
+
chart_type = serializer.data.get('chart_type')
|
130
|
+
|
131
|
+
if chart_type is None:
|
132
|
+
data = {
|
133
|
+
"engagements_over_time": self.construct_engagements_over_time(
|
134
|
+
engagement_df.copy(),
|
135
|
+
start_date,
|
136
|
+
end_date,
|
137
|
+
granularity,
|
138
|
+
calculation,
|
139
|
+
),
|
140
|
+
"top_courses_by_engagement": self.construct_top_courses_by_engagements(
|
141
|
+
engagement_df.copy(),
|
142
|
+
start_date,
|
143
|
+
end_date,
|
144
|
+
),
|
145
|
+
"top_subjects_by_engagement": self.construct_top_subjects_by_engagements(
|
146
|
+
engagement_df.copy(),
|
147
|
+
start_date,
|
148
|
+
end_date,
|
149
|
+
),
|
150
|
+
}
|
151
|
+
return Response(data)
|
152
|
+
elif chart_type == EngagementChart.ENGAGEMENTS_OVER_TIME.value:
|
153
|
+
return self.construct_engagements_over_time_csv(
|
154
|
+
engagement_df.copy(),
|
155
|
+
start_date,
|
156
|
+
end_date,
|
157
|
+
granularity,
|
158
|
+
calculation,
|
159
|
+
)
|
160
|
+
elif chart_type == EngagementChart.TOP_COURSES_BY_ENGAGEMENTS.value:
|
161
|
+
return self.construct_top_courses_by_engagements_csv(
|
162
|
+
engagement_df.copy(),
|
163
|
+
start_date,
|
164
|
+
end_date,
|
165
|
+
)
|
166
|
+
elif chart_type == EngagementChart.TOP_SUBJECTS_BY_ENGAGEMENTS.value:
|
167
|
+
return self.construct_top_subjects_by_engagements_csv(
|
168
|
+
engagement_df.copy(),
|
169
|
+
start_date,
|
170
|
+
end_date,
|
171
|
+
)
|
172
|
+
return Response(data='Not Found', status=rest_status.HTTP_400_BAD_REQUEST)
|
173
|
+
|
174
|
+
def engagements_over_time_common(self, engagements_df, start_date, end_date, granularity, calculation):
|
175
|
+
"""
|
176
|
+
Common method for constructing engagements over time data.
|
177
|
+
|
178
|
+
Arguments:
|
179
|
+
engagements_df {DataFrame} -- DataFrame of engagements
|
180
|
+
start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
|
181
|
+
end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
|
182
|
+
granularity {str} -- Granularity of the data. One of Granularity choices
|
183
|
+
calculation {str} -- Calculation of the data. One of Calculation choices
|
184
|
+
"""
|
185
|
+
engagements_df["learning_time_hours"] = engagements_df["learning_time_seconds"] / 60 / 60
|
186
|
+
engagements_df = engagements_df[engagements_df["learning_time_hours"] > 0]
|
187
|
+
engagements_df["learning_time_hours"] = round(engagements_df["learning_time_hours"].astype(float), 1)
|
188
|
+
|
189
|
+
engagements_df = engagements_df[["activity_date", "enroll_type", "learning_time_hours"]]
|
190
|
+
|
191
|
+
# Date filtering.
|
192
|
+
engagements_df = date_filter(
|
193
|
+
start=start_date, end=end_date, data_frame=engagements_df, date_column="activity_date"
|
194
|
+
)
|
195
|
+
|
196
|
+
# Date aggregation.
|
197
|
+
engagements_df = granularity_aggregation(
|
198
|
+
level=granularity,
|
199
|
+
group=["activity_date", "enroll_type"],
|
200
|
+
date="activity_date",
|
201
|
+
data_frame=engagements_df,
|
202
|
+
aggregation_type="sum",
|
203
|
+
)
|
204
|
+
|
205
|
+
# Calculating metric.
|
206
|
+
engagements = calculation_aggregation(calc=calculation, aggregation_type="sum", data_frame=engagements_df)
|
207
|
+
return engagements
|
208
|
+
|
209
|
+
def construct_engagements_over_time(self, engagements_df, start_date, end_date, granularity, calculation):
|
210
|
+
"""
|
211
|
+
Construct engagements over time data.
|
212
|
+
|
213
|
+
Arguments:
|
214
|
+
engagements_df {DataFrame} -- DataFrame of engagements
|
215
|
+
start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
|
216
|
+
end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
|
217
|
+
granularity {str} -- Granularity of the data. One of Granularity choices
|
218
|
+
calculation {str} -- Calculation of the data. One of Calculation choices
|
219
|
+
"""
|
220
|
+
engagements = self.engagements_over_time_common(engagements_df, start_date, end_date, granularity, calculation)
|
221
|
+
# convert dataframe to a list of records
|
222
|
+
return engagements.to_dict(orient='records')
|
223
|
+
|
224
|
+
def construct_engagements_over_time_csv(self, engagements_df, start_date, end_date, granularity, calculation):
|
225
|
+
"""
|
226
|
+
Construct engagements over time CSV.
|
227
|
+
|
228
|
+
Arguments:
|
229
|
+
engagements_df {DataFrame} -- DataFrame of engagements
|
230
|
+
start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
|
231
|
+
end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
|
232
|
+
granularity {str} -- Granularity of the data. One of Granularity choices
|
233
|
+
calculation {str} -- Calculation of the data. One of Calculation choices
|
234
|
+
"""
|
235
|
+
engagements = self.engagements_over_time_common(engagements_df, start_date, end_date, granularity, calculation)
|
236
|
+
|
237
|
+
engagements = engagements.pivot(
|
238
|
+
index="activity_date", columns="enroll_type", values="sum"
|
239
|
+
)
|
240
|
+
|
241
|
+
filename = f"Engagement Timeseries, {start_date} - {end_date} ({granularity} {calculation}).csv"
|
242
|
+
return self.construct_csv_response(engagements, filename)
|
243
|
+
|
244
|
+
def top_courses_by_engagements_common(self, engagements_df, start_date, end_date):
|
245
|
+
"""
|
246
|
+
Common method for constructing top courses by engagements data.
|
247
|
+
|
248
|
+
Arguments:
|
249
|
+
engagements_df {DataFrame} -- DataFrame of engagements
|
250
|
+
start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
|
251
|
+
end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
|
252
|
+
group_by_columns {list} -- List of columns to group by
|
253
|
+
columns {list} -- List of column for the final result
|
254
|
+
"""
|
255
|
+
engagements_df["learning_time_hours"] = engagements_df["learning_time_seconds"] / 60 / 60
|
256
|
+
engagements_df["learning_time_hours"] = engagements_df["learning_time_hours"].astype("float")
|
257
|
+
|
258
|
+
# Date filtering.
|
259
|
+
engagements = date_filter(
|
260
|
+
start=start_date, end=end_date, data_frame=engagements_df, date_column="activity_date"
|
261
|
+
)
|
262
|
+
|
263
|
+
courses = list(
|
264
|
+
engagements.groupby(["course_key"])
|
265
|
+
.learning_time_hours.sum()
|
266
|
+
.sort_values(ascending=False)[:10]
|
267
|
+
.index
|
268
|
+
)
|
269
|
+
|
270
|
+
engagements = (
|
271
|
+
engagements_df[engagements_df.course_key.isin(courses)]
|
272
|
+
.groupby(["course_key", "course_title", "enroll_type"])
|
273
|
+
.learning_time_hours.sum()
|
274
|
+
.reset_index()
|
275
|
+
)
|
276
|
+
|
277
|
+
engagements.columns = ["course_key", "course_title", "enroll_type", "count"]
|
278
|
+
|
279
|
+
return engagements
|
280
|
+
|
281
|
+
def construct_top_courses_by_engagements(self, engagements_df, start_date, end_date):
|
282
|
+
"""
|
283
|
+
Construct top courses by engagements data.
|
284
|
+
|
285
|
+
Arguments:
|
286
|
+
engagements_df {DataFrame} -- DataFrame of engagements
|
287
|
+
start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
|
288
|
+
end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
|
289
|
+
"""
|
290
|
+
engagements = self.top_courses_by_engagements_common(
|
291
|
+
engagements_df,
|
292
|
+
start_date,
|
293
|
+
end_date
|
294
|
+
)
|
295
|
+
|
296
|
+
# convert dataframe to a list of records
|
297
|
+
return engagements.to_dict(orient='records')
|
298
|
+
|
299
|
+
def construct_top_courses_by_engagements_csv(self, engagements_df, start_date, end_date):
|
300
|
+
"""
|
301
|
+
Construct top courses by engagements CSV.
|
302
|
+
|
303
|
+
Arguments:
|
304
|
+
engagements_df {DataFrame} -- DataFrame of engagements
|
305
|
+
start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
|
306
|
+
end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
|
307
|
+
"""
|
308
|
+
engagements = self.top_courses_by_engagements_common(
|
309
|
+
engagements_df,
|
310
|
+
start_date,
|
311
|
+
end_date
|
312
|
+
)
|
313
|
+
|
314
|
+
engagements = engagements.pivot(
|
315
|
+
index=["course_key", "course_title"], columns="enroll_type", values="count"
|
316
|
+
)
|
317
|
+
|
318
|
+
filename = f"Top 10 Courses by Learning Hours, {start_date} - {end_date}.csv"
|
319
|
+
return self.construct_csv_response(engagements, filename)
|
320
|
+
|
321
|
+
def top_subjects_by_engagements_common(self, engagements_df, start_date, end_date):
|
322
|
+
"""
|
323
|
+
Common method for constructing top subjects by engagements data.
|
324
|
+
|
325
|
+
Arguments:
|
326
|
+
engagements_df {DataFrame} -- DataFrame of engagements
|
327
|
+
start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
|
328
|
+
end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
|
329
|
+
"""
|
330
|
+
engagements_df["learning_time_hours"] = engagements_df["learning_time_seconds"] / 60 / 60
|
331
|
+
engagements_df["learning_time_hours"] = engagements_df["learning_time_hours"].astype("float")
|
332
|
+
|
333
|
+
# Date filtering.
|
334
|
+
engagements = date_filter(
|
335
|
+
start=start_date, end=end_date, data_frame=engagements_df, date_column="activity_date"
|
336
|
+
)
|
337
|
+
|
338
|
+
subjects = list(
|
339
|
+
engagements.groupby(["course_subject"])
|
340
|
+
.learning_time_hours.sum()
|
341
|
+
.sort_values(ascending=False)[:10]
|
342
|
+
.index
|
343
|
+
)
|
344
|
+
|
345
|
+
engagements = (
|
346
|
+
engagements[engagements.course_subject.isin(subjects)]
|
347
|
+
.groupby(["course_subject", "enroll_type"])
|
348
|
+
.learning_time_hours.sum()
|
349
|
+
.reset_index()
|
350
|
+
)
|
351
|
+
engagements.columns = ["course_subject", "enroll_type", "count"]
|
352
|
+
|
353
|
+
return engagements
|
354
|
+
|
355
|
+
def construct_top_subjects_by_engagements(self, engagements_df, start_date, end_date):
|
356
|
+
"""
|
357
|
+
Construct top subjects by engagements data.
|
358
|
+
|
359
|
+
Arguments:
|
360
|
+
engagements_df {DataFrame} -- DataFrame of engagements
|
361
|
+
start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
|
362
|
+
end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
|
363
|
+
"""
|
364
|
+
engagements = self.top_subjects_by_engagements_common(engagements_df, start_date, end_date)
|
365
|
+
# convert dataframe to a list of records
|
366
|
+
return engagements.to_dict(orient='records')
|
367
|
+
|
368
|
+
def construct_top_subjects_by_engagements_csv(self, engagements_df, start_date, end_date):
|
369
|
+
"""
|
370
|
+
Construct top subjects by engagements CSV.
|
371
|
+
|
372
|
+
Arguments:
|
373
|
+
engagements_df {DataFrame} -- DataFrame of engagements
|
374
|
+
start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
|
375
|
+
end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
|
376
|
+
"""
|
377
|
+
engagements = self.top_subjects_by_engagements_common(engagements_df, start_date, end_date)
|
378
|
+
engagements = engagements.pivot(index="course_subject", columns="enroll_type", values="count")
|
379
|
+
filename = f"Top 10 Subjects by Learning Hours, {start_date} - {end_date}.csv"
|
380
|
+
return self.construct_csv_response(engagements, filename)
|
381
|
+
|
382
|
+
def construct_csv_response(self, engagements, filename):
|
383
|
+
"""
|
384
|
+
Construct CSV response.
|
385
|
+
|
386
|
+
Arguments:
|
387
|
+
engagements {DataFrame} -- DataFrame of engagements
|
388
|
+
filename {str} -- Filename for the CSV
|
389
|
+
"""
|
390
|
+
response = HttpResponse(content_type='text/csv')
|
391
|
+
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
392
|
+
response['Access-Control-Expose-Headers'] = 'Content-Disposition'
|
393
|
+
engagements.to_csv(path_or_buf=response)
|
394
|
+
|
395
|
+
return response
|
@@ -382,6 +382,7 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
|
|
382
382
|
"""
|
383
383
|
response = HttpResponse(content_type='text/csv')
|
384
384
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
385
|
+
response['Access-Control-Expose-Headers'] = 'Content-Disposition'
|
385
386
|
enrollments.to_csv(path_or_buf=response)
|
386
387
|
|
387
388
|
return response
|
@@ -120,7 +120,10 @@ class AdvanceAnalyticsLeaderboardView(APIView):
|
|
120
120
|
return StreamingHttpResponse(
|
121
121
|
LeaderboardCSVRenderer().render(self._stream_serialized_data(leaderboard_df)),
|
122
122
|
content_type="text/csv",
|
123
|
-
headers={
|
123
|
+
headers={
|
124
|
+
"Content-Disposition": f'attachment; filename="{filename}"',
|
125
|
+
"Access-Control-Expose-Headers": "Content-Disposition"
|
126
|
+
},
|
124
127
|
)
|
125
128
|
|
126
129
|
paginator = self.pagination_class()
|
@@ -13,9 +13,9 @@ from rest_framework.views import APIView
|
|
13
13
|
from django.http import HttpResponse
|
14
14
|
|
15
15
|
from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_datetime
|
16
|
+
from enterprise_data.admin_analytics.database.tables import FactEngagementAdminDashTable, FactEnrollmentAdminDashTable
|
16
17
|
from enterprise_data.admin_analytics.utils import (
|
17
18
|
ChartType,
|
18
|
-
fetch_and_cache_engagements_data,
|
19
19
|
fetch_and_cache_enrollments_data,
|
20
20
|
fetch_and_cache_skills_data,
|
21
21
|
get_skills_chart_data,
|
@@ -27,7 +27,7 @@ from enterprise_data.models import (
|
|
27
27
|
EnterpriseAdminSummarizeInsights,
|
28
28
|
EnterpriseExecEdLCModulePerformance,
|
29
29
|
)
|
30
|
-
from enterprise_data.utils import
|
30
|
+
from enterprise_data.utils import timer
|
31
31
|
|
32
32
|
from .base import EnterpriseViewSetMixin
|
33
33
|
|
@@ -101,55 +101,25 @@ class EnterpriseAdminAnalyticsAggregatesView(APIView):
|
|
101
101
|
serializer.is_valid(raise_exception=True)
|
102
102
|
|
103
103
|
last_updated_at = fetch_max_enrollment_datetime()
|
104
|
-
|
105
|
-
|
104
|
+
min_enrollment_date, max_enrollment_date = FactEnrollmentAdminDashTable().get_enrollment_date_range(
|
105
|
+
enterprise_id,
|
106
106
|
)
|
107
107
|
|
108
|
-
enrollment = fetch_and_cache_enrollments_data(
|
109
|
-
enterprise_id, cache_expiry
|
110
|
-
).copy()
|
111
|
-
engagement = fetch_and_cache_engagements_data(
|
112
|
-
enterprise_id, cache_expiry
|
113
|
-
).copy()
|
114
|
-
# Use start and end date if provided by the client, if client has not provided then use
|
115
|
-
# 1. minimum enrollment date from the data as the start_date
|
116
|
-
# 2. today's date as the end_date
|
117
108
|
start_date = serializer.data.get(
|
118
|
-
'start_date',
|
109
|
+
'start_date', min_enrollment_date.date()
|
119
110
|
)
|
120
|
-
end_date = serializer.data.get('end_date', datetime.
|
111
|
+
end_date = serializer.data.get('end_date', datetime.today())
|
121
112
|
|
122
|
-
|
123
|
-
|
124
|
-
start=start_date,
|
125
|
-
end=end_date,
|
126
|
-
data_frame=enrollment.copy(),
|
127
|
-
date_column='enterprise_enrollment_date',
|
113
|
+
enrolls, courses = FactEnrollmentAdminDashTable().get_enrollment_and_course_count(
|
114
|
+
enterprise_id, start_date, end_date,
|
128
115
|
)
|
129
|
-
|
130
|
-
|
131
|
-
courses = len(dff.course_key.unique())
|
132
|
-
|
133
|
-
dff = date_filter(
|
134
|
-
start=start_date,
|
135
|
-
end=end_date,
|
136
|
-
data_frame=enrollment.copy(),
|
137
|
-
date_column='passed_date',
|
116
|
+
completions = FactEnrollmentAdminDashTable().get_completion_count(
|
117
|
+
enterprise_id, start_date, end_date,
|
138
118
|
)
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
# Date filtering.
|
143
|
-
dff = date_filter(
|
144
|
-
start=start_date,
|
145
|
-
end=end_date,
|
146
|
-
data_frame=engagement.copy(),
|
147
|
-
date_column='activity_date',
|
119
|
+
hours, sessions = FactEngagementAdminDashTable().get_learning_hours_and_daily_sessions(
|
120
|
+
enterprise_id, start_date, end_date,
|
148
121
|
)
|
149
122
|
|
150
|
-
hours = round(dff.learning_time_seconds.sum() / 60 / 60, 1)
|
151
|
-
sessions = dff.is_engaged.sum()
|
152
|
-
|
153
123
|
return Response(
|
154
124
|
data={
|
155
125
|
'enrolls': enrolls,
|
@@ -158,8 +128,8 @@ class EnterpriseAdminAnalyticsAggregatesView(APIView):
|
|
158
128
|
'hours': hours,
|
159
129
|
'sessions': sessions,
|
160
130
|
'last_updated_at': last_updated_at.date() if last_updated_at else None,
|
161
|
-
'min_enrollment_date':
|
162
|
-
'max_enrollment_date':
|
131
|
+
'min_enrollment_date': min_enrollment_date.date(),
|
132
|
+
'max_enrollment_date': max_enrollment_date.date(),
|
163
133
|
},
|
164
134
|
status=HTTP_200_OK,
|
165
135
|
)
|
@@ -208,6 +178,7 @@ class EnterpriseAdminAnalyticsSkillsView(APIView):
|
|
208
178
|
response = HttpResponse(content_type='text/csv')
|
209
179
|
filename = f"Skills by Enrollment and Completion, {start_date} - {end_date}.csv"
|
210
180
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
181
|
+
response['Access-Control-Expose-Headers'] = 'Content-Disposition'
|
211
182
|
csv_data.to_csv(path_or_buf=response, index=False)
|
212
183
|
return response
|
213
184
|
|
@@ -90,6 +90,7 @@ class EnterrpiseAdminCompletionsStatsView(APIView):
|
|
90
90
|
)
|
91
91
|
filename = csv_data['filename']
|
92
92
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
93
|
+
response['Access-Control-Expose-Headers'] = 'Content-Disposition'
|
93
94
|
csv_data['data'].to_csv(path_or_buf=response)
|
94
95
|
return response
|
95
96
|
|
@@ -187,6 +188,7 @@ class EnterrpiseAdminCompletionsView(APIView):
|
|
187
188
|
response = HttpResponse(content_type='text/csv')
|
188
189
|
filename = f"Individual Completions, {start_date} - {end_date}.csv"
|
189
190
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
191
|
+
response['Access-Control-Expose-Headers'] = 'Content-Disposition'
|
190
192
|
dff.to_csv(path_or_buf=response, index=False)
|
191
193
|
return response
|
192
194
|
|
enterprise_data/renderers.py
CHANGED
@@ -57,3 +57,17 @@ class LeaderboardCSVRenderer(CSVStreamingRenderer):
|
|
57
57
|
'average_session_length',
|
58
58
|
'course_completions',
|
59
59
|
]
|
60
|
+
|
61
|
+
|
62
|
+
class IndividualEngagementsCSVRenderer(CSVStreamingRenderer):
|
63
|
+
"""
|
64
|
+
Custom streaming csv renderer for advance analytics individual engagements data.
|
65
|
+
"""
|
66
|
+
|
67
|
+
header = [
|
68
|
+
'email',
|
69
|
+
'course_title',
|
70
|
+
'activity_date',
|
71
|
+
'course_subject',
|
72
|
+
'learning_time_hours',
|
73
|
+
]
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
import pandas as pd
|
4
4
|
|
5
|
-
from enterprise_data.admin_analytics.constants import EnrollmentChart
|
5
|
+
from enterprise_data.admin_analytics.constants import EngagementChart, EnrollmentChart
|
6
6
|
from enterprise_data.admin_analytics.utils import ChartType
|
7
7
|
|
8
8
|
ENROLLMENTS = [
|
@@ -339,6 +339,30 @@ ENROLLMENT_STATS_CSVS = {
|
|
339
339
|
)
|
340
340
|
}
|
341
341
|
|
342
|
+
ENGAGEMENT_STATS_CSVS = {
|
343
|
+
EngagementChart.ENGAGEMENTS_OVER_TIME.value: (
|
344
|
+
b'activity_date,certificate\n'
|
345
|
+
b'2021-07-19,0.0\n'
|
346
|
+
b'2021-07-26,4.4\n'
|
347
|
+
b'2021-07-27,1.2\n'
|
348
|
+
b'2021-08-05,3.6\n'
|
349
|
+
b'2021-08-21,2.7\n'
|
350
|
+
b'2021-09-02,1.3\n'
|
351
|
+
b'2021-09-21,1.5\n'
|
352
|
+
b'2022-05-17,0.0\n'
|
353
|
+
),
|
354
|
+
EngagementChart.TOP_COURSES_BY_ENGAGEMENTS.value: (
|
355
|
+
b'course_key,course_title,certificate\n'
|
356
|
+
b'Kcpr+XoR30,Assimilated even-keeled focus group,0.0\n'
|
357
|
+
b'luGg+KNt30,Synergized reciprocal encoding,14.786944444444444\n'
|
358
|
+
),
|
359
|
+
EngagementChart.TOP_SUBJECTS_BY_ENGAGEMENTS.value: (
|
360
|
+
b'course_subject,certificate\n'
|
361
|
+
b'business-management,14.786944444444444\n'
|
362
|
+
b'engineering,0.0\n'
|
363
|
+
)
|
364
|
+
}
|
365
|
+
|
342
366
|
|
343
367
|
def enrollments_dataframe():
|
344
368
|
"""Return a DataFrame of enrollments."""
|
@@ -509,3 +533,19 @@ COMPLETIONS_STATS_CSVS = {
|
|
509
533
|
b'business-management,2\n'
|
510
534
|
)
|
511
535
|
}
|
536
|
+
|
537
|
+
|
538
|
+
def engagements_csv_content():
|
539
|
+
"""Return the CSV content of engagements."""
|
540
|
+
return (
|
541
|
+
b'email,course_title,activity_date,course_subject,learning_time_hours\r\n'
|
542
|
+
b'graceperez@example.com,Synergized reciprocal encoding,2022-05-17,business-management,0.0\r\n'
|
543
|
+
b'webertodd@example.com,Synergized reciprocal encoding,2021-09-21,business-management,1.5\r\n'
|
544
|
+
b'yferguson@example.net,Synergized reciprocal encoding,2021-09-02,business-management,1.3\r\n'
|
545
|
+
b'seth57@example.org,Synergized reciprocal encoding,2021-08-21,business-management,2.7\r\n'
|
546
|
+
b'padillamichelle@example.org,Synergized reciprocal encoding,2021-08-05,business-management,1.0\r\n'
|
547
|
+
b'weaverpatricia@example.net,Synergized reciprocal encoding,2021-08-05,business-management,2.6\r\n'
|
548
|
+
b'yallison@example.org,Synergized reciprocal encoding,2021-07-27,business-management,1.2\r\n'
|
549
|
+
b'paul77@example.org,Synergized reciprocal encoding,2021-07-26,business-management,4.4\r\n'
|
550
|
+
b'samanthaclarke@example.org,Synergized reciprocal encoding,2021-07-19,business-management,0.0\r\n'
|
551
|
+
)
|