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.
Files changed (27) hide show
  1. {edx_enterprise_data-8.9.0.dist-info → edx_enterprise_data-8.11.0.dist-info}/METADATA +1 -1
  2. {edx_enterprise_data-8.9.0.dist-info → edx_enterprise_data-8.11.0.dist-info}/RECORD +27 -17
  3. enterprise_data/__init__.py +1 -1
  4. enterprise_data/admin_analytics/constants.py +8 -0
  5. enterprise_data/admin_analytics/database/__init__.py +4 -0
  6. enterprise_data/admin_analytics/database/queries/__init__.py +5 -0
  7. enterprise_data/admin_analytics/database/queries/fact_engagement_admin_dash.py +21 -0
  8. enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py +61 -0
  9. enterprise_data/admin_analytics/database/tables/__init__.py +5 -0
  10. enterprise_data/admin_analytics/database/tables/base.py +18 -0
  11. enterprise_data/admin_analytics/database/tables/fact_engagement_admin_dash.py +38 -0
  12. enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py +76 -0
  13. enterprise_data/admin_analytics/{database.py → database/utils.py} +3 -2
  14. enterprise_data/api/v1/serializers.py +31 -1
  15. enterprise_data/api/v1/urls.py +14 -0
  16. enterprise_data/api/v1/views/analytics_engagements.py +395 -0
  17. enterprise_data/api/v1/views/analytics_enrollments.py +1 -0
  18. enterprise_data/api/v1/views/analytics_leaderboard.py +4 -1
  19. enterprise_data/api/v1/views/enterprise_admin.py +15 -44
  20. enterprise_data/api/v1/views/enterprise_completions.py +2 -0
  21. enterprise_data/renderers.py +14 -0
  22. enterprise_data/tests/admin_analytics/mock_analytics_data.py +41 -1
  23. enterprise_data/tests/admin_analytics/test_analytics_engagements.py +390 -0
  24. enterprise_data/tests/api/v1/views/test_enterprise_admin.py +43 -20
  25. {edx_enterprise_data-8.9.0.dist-info → edx_enterprise_data-8.11.0.dist-info}/LICENSE +0 -0
  26. {edx_enterprise_data-8.9.0.dist-info → edx_enterprise_data-8.11.0.dist-info}/WHEEL +0 -0
  27. {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={"Content-Disposition": f'attachment; filename="{filename}"'},
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 date_filter, timer
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
- cache_expiry = (
105
- last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
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', enrollment.enterprise_enrollment_date.min()
109
+ 'start_date', min_enrollment_date.date()
119
110
  )
120
- end_date = serializer.data.get('end_date', datetime.now())
111
+ end_date = serializer.data.get('end_date', datetime.today())
121
112
 
122
- # Date filtering.
123
- dff = date_filter(
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
- enrolls = len(dff)
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
- completions = dff.has_passed.sum()
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': enrollment.enterprise_enrollment_date.min().date(),
162
- 'max_enrollment_date': enrollment.enterprise_enrollment_date.max().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
 
@@ -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
+ )