edx-enterprise-data 9.0.1__py3-none-any.whl → 9.1.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-9.0.1.dist-info → edx_enterprise_data-9.1.0.dist-info}/METADATA +1 -1
- {edx_enterprise_data-9.0.1.dist-info → edx_enterprise_data-9.1.0.dist-info}/RECORD +22 -23
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/constants.py +0 -16
- enterprise_data/admin_analytics/database/queries/fact_engagement_admin_dash.py +90 -0
- enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py +83 -4
- enterprise_data/admin_analytics/database/tables/fact_engagement_admin_dash.py +116 -0
- enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py +97 -3
- enterprise_data/api/v1/serializers.py +1 -55
- enterprise_data/api/v1/urls.py +14 -17
- enterprise_data/api/v1/views/analytics_completions.py +150 -0
- enterprise_data/api/v1/views/analytics_engagements.py +106 -351
- enterprise_data/api/v1/views/analytics_enrollments.py +3 -6
- enterprise_data/renderers.py +22 -7
- enterprise_data/tests/admin_analytics/mock_analytics_data.py +12 -90
- enterprise_data/tests/admin_analytics/mock_enrollments.py +0 -66
- enterprise_data/tests/admin_analytics/test_analytics_engagements.py +120 -240
- enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +12 -81
- enterprise_data/tests/admin_analytics/test_enterprise_completions.py +105 -120
- enterprise_data/admin_analytics/completions_utils.py +0 -261
- enterprise_data/api/v1/views/enterprise_completions.py +0 -200
- {edx_enterprise_data-9.0.1.dist-info → edx_enterprise_data-9.1.0.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-9.0.1.dist-info → edx_enterprise_data-9.1.0.dist-info}/WHEEL +0 -0
- {edx_enterprise_data-9.0.1.dist-info → edx_enterprise_data-9.1.0.dist-info}/top_level.txt +0 -0
@@ -1,395 +1,150 @@
|
|
1
|
-
"""
|
1
|
+
"""
|
2
|
+
Views for handling REST endpoints related to Engagements analytics.
|
3
|
+
"""
|
4
|
+
|
2
5
|
from datetime import datetime
|
6
|
+
from logging import getLogger
|
3
7
|
|
4
8
|
from edx_rbac.decorators import permission_required
|
5
9
|
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
6
|
-
from rest_framework import
|
10
|
+
from rest_framework.decorators import action
|
7
11
|
from rest_framework.response import Response
|
8
|
-
from rest_framework.
|
12
|
+
from rest_framework.viewsets import ViewSet
|
9
13
|
|
10
|
-
from django.http import
|
14
|
+
from django.http import StreamingHttpResponse
|
11
15
|
|
12
|
-
from enterprise_data.admin_analytics.constants import
|
13
|
-
from enterprise_data.admin_analytics.
|
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
|
16
|
+
from enterprise_data.admin_analytics.constants import ResponseType
|
17
|
+
from enterprise_data.admin_analytics.database.tables import FactEngagementAdminDashTable, FactEnrollmentAdminDashTable
|
21
18
|
from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
|
19
|
+
from enterprise_data.api.v1.serializers import AdvanceAnalyticsQueryParamSerializer
|
20
|
+
from enterprise_data.api.v1.views.base import AnalyticsPaginationMixin
|
22
21
|
from enterprise_data.renderers import IndividualEngagementsCSVRenderer
|
23
|
-
from enterprise_data.utils import
|
22
|
+
from enterprise_data.utils import timer
|
24
23
|
|
24
|
+
LOGGER = getLogger(__name__)
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
API for getting the advance analytics individual engagements data.
|
26
|
+
|
27
|
+
class AdvanceAnalyticsEngagementView(AnalyticsPaginationMixin, ViewSet):
|
29
28
|
"""
|
29
|
+
View to handle requests for enterprise engagement data.
|
30
30
|
|
31
|
+
Here is the list of URLs that are handled by this view:
|
32
|
+
1. `enterprise_data_api_v1.enterprise-learner-engagement-list`: Get individual engagement data.
|
33
|
+
2. `enterprise_data_api_v1.enterprise-learner-engagement-stats`: Get engagement stats data.
|
34
|
+
"""
|
31
35
|
authentication_classes = (JwtAuthentication,)
|
32
36
|
pagination_class = AdvanceAnalyticsPagination
|
33
|
-
http_method_names =
|
37
|
+
http_method_names = ('get', )
|
34
38
|
|
35
39
|
@permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
|
36
|
-
def
|
40
|
+
def list(self, request, enterprise_uuid):
|
37
41
|
"""
|
38
|
-
|
42
|
+
Get individual engagements data for the enterprise.
|
39
43
|
"""
|
40
|
-
|
44
|
+
# Remove hyphens from the UUID
|
45
|
+
enterprise_uuid = enterprise_uuid.replace('-', '')
|
46
|
+
|
47
|
+
serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
|
41
48
|
serializer.is_valid(raise_exception=True)
|
42
|
-
|
49
|
+
min_enrollment_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(
|
50
|
+
enterprise_uuid,
|
51
|
+
)
|
43
52
|
|
44
|
-
|
45
|
-
|
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())
|
53
|
+
# get values from query params or use default values
|
54
|
+
start_date = serializer.data.get('start_date', min_enrollment_date)
|
50
55
|
end_date = serializer.data.get('end_date', datetime.now())
|
56
|
+
page = serializer.data.get('page', 1)
|
57
|
+
page_size = serializer.data.get('page_size', 100)
|
58
|
+
engagements = FactEngagementAdminDashTable().get_all_engagements(
|
59
|
+
enterprise_customer_uuid=enterprise_uuid,
|
60
|
+
start_date=start_date,
|
61
|
+
end_date=end_date,
|
62
|
+
limit=page_size,
|
63
|
+
offset=(page - 1) * page_size,
|
64
|
+
)
|
65
|
+
total_count = FactEngagementAdminDashTable().get_engagement_count(
|
66
|
+
enterprise_customer_uuid=enterprise_uuid,
|
67
|
+
start_date=start_date,
|
68
|
+
end_date=end_date,
|
69
|
+
)
|
51
70
|
response_type = request.query_params.get('response_type', ResponseType.JSON.value)
|
52
|
-
|
53
|
-
|
54
|
-
|
71
|
+
|
72
|
+
LOGGER.info(
|
73
|
+
"Individual engagements data requested for enterprise [%s] from [%s] to [%s]",
|
74
|
+
enterprise_uuid,
|
75
|
+
start_date,
|
76
|
+
end_date,
|
55
77
|
)
|
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
78
|
|
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
79
|
if response_type == ResponseType.CSV.value:
|
73
|
-
|
74
|
-
|
75
|
-
|
80
|
+
filename = f"""individual_engagements, {start_date} - {end_date}.csv"""
|
81
|
+
|
82
|
+
return StreamingHttpResponse(
|
83
|
+
IndividualEngagementsCSVRenderer().render(self._stream_serialized_data(
|
84
|
+
enterprise_uuid, start_date, end_date, total_count
|
85
|
+
)),
|
86
|
+
content_type="text/csv",
|
87
|
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
76
88
|
)
|
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
89
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
+
return self.get_paginated_response(
|
91
|
+
request=request,
|
92
|
+
records=engagements,
|
93
|
+
page=page,
|
94
|
+
page_size=page_size,
|
95
|
+
total_count=total_count,
|
96
|
+
)
|
90
97
|
|
91
|
-
|
98
|
+
@staticmethod
|
99
|
+
def _stream_serialized_data(enterprise_uuid, start_date, end_date, total_count, page_size=50000):
|
92
100
|
"""
|
93
101
|
Stream the serialized data.
|
94
102
|
"""
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
authentication_classes = (JwtAuthentication,)
|
108
|
-
http_method_names = ["get"]
|
103
|
+
offset = 0
|
104
|
+
while offset < total_count:
|
105
|
+
engagements = FactEngagementAdminDashTable().get_all_engagements(
|
106
|
+
enterprise_customer_uuid=enterprise_uuid,
|
107
|
+
start_date=start_date,
|
108
|
+
end_date=end_date,
|
109
|
+
limit=page_size,
|
110
|
+
offset=offset,
|
111
|
+
)
|
112
|
+
yield from engagements
|
113
|
+
offset += page_size
|
109
114
|
|
110
115
|
@permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
|
111
|
-
|
116
|
+
@action(detail=False, methods=['get'], name='Enterprise engagements data for charts', url_path='stats')
|
117
|
+
def stats(self, request, enterprise_uuid):
|
112
118
|
"""
|
113
|
-
|
119
|
+
Get data to populate enterprise engagement charts.
|
120
|
+
|
121
|
+
Here is the list of the charts and their corresponding data:
|
122
|
+
1. `engagement_over_time`: This will show time series data of engagements over time.
|
123
|
+
2. `top_courses_by_engagement`: This will show the top courses by engagements.
|
124
|
+
3. `top_subjects_by_engagement`: This will show the top subjects by engagements.
|
114
125
|
"""
|
115
|
-
|
116
|
-
|
126
|
+
# Remove hyphens from the UUID
|
127
|
+
enterprise_uuid = enterprise_uuid.replace('-', '')
|
117
128
|
|
118
|
-
|
129
|
+
serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
|
130
|
+
serializer.is_valid(raise_exception=True)
|
119
131
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
#
|
124
|
-
|
125
|
-
start_date = serializer.data.get('start_date', enrollment_df.enterprise_enrollment_date.min())
|
132
|
+
min_enrollment_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(
|
133
|
+
enterprise_uuid,
|
134
|
+
)
|
135
|
+
# get values from query params or use default
|
136
|
+
start_date = serializer.data.get('start_date', min_enrollment_date)
|
126
137
|
end_date = serializer.data.get('end_date', datetime.now())
|
127
|
-
|
128
|
-
calculation = serializer.data.get('calculation', Calculation.TOTAL.value)
|
129
|
-
chart_type = serializer.data.get('chart_type')
|
130
|
-
|
131
|
-
if chart_type is None:
|
138
|
+
with timer('construct_engagement_all_stats'):
|
132
139
|
data = {
|
133
|
-
|
134
|
-
|
135
|
-
start_date,
|
136
|
-
end_date,
|
137
|
-
granularity,
|
138
|
-
calculation,
|
140
|
+
'engagement_over_time': FactEngagementAdminDashTable().get_engagement_time_series_data(
|
141
|
+
enterprise_uuid, start_date, end_date
|
139
142
|
),
|
140
|
-
|
141
|
-
|
142
|
-
start_date,
|
143
|
-
end_date,
|
143
|
+
'top_courses_by_engagement': FactEngagementAdminDashTable().get_top_courses_by_engagement(
|
144
|
+
enterprise_uuid, start_date, end_date,
|
144
145
|
),
|
145
|
-
|
146
|
-
|
147
|
-
start_date,
|
148
|
-
end_date,
|
146
|
+
'top_subjects_by_engagement': FactEngagementAdminDashTable().get_top_subjects_by_engagement(
|
147
|
+
enterprise_uuid, start_date, end_date,
|
149
148
|
),
|
150
149
|
}
|
151
|
-
|
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
|
150
|
+
return Response(data)
|
@@ -15,10 +15,7 @@ from django.http import StreamingHttpResponse
|
|
15
15
|
from enterprise_data.admin_analytics.constants import ResponseType
|
16
16
|
from enterprise_data.admin_analytics.database.tables import FactEnrollmentAdminDashTable
|
17
17
|
from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
|
18
|
-
from enterprise_data.api.v1.serializers import
|
19
|
-
AdvanceAnalyticsEnrollmentStatsSerializer,
|
20
|
-
AdvanceAnalyticsQueryParamSerializer,
|
21
|
-
)
|
18
|
+
from enterprise_data.api.v1.serializers import AdvanceAnalyticsQueryParamSerializer
|
22
19
|
from enterprise_data.api.v1.views.base import AnalyticsPaginationMixin
|
23
20
|
from enterprise_data.renderers import IndividualEnrollmentsCSVRenderer
|
24
21
|
from enterprise_data.utils import timer
|
@@ -115,7 +112,7 @@ class AdvanceAnalyticsEnrollmentsView(AnalyticsPaginationMixin, ViewSet):
|
|
115
112
|
offset += page_size
|
116
113
|
|
117
114
|
@permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
|
118
|
-
@action(detail=False, methods=['get'], name='
|
115
|
+
@action(detail=False, methods=['get'], name='Enterprise enrollments data for charts', url_path='stats')
|
119
116
|
def stats(self, request, enterprise_uuid):
|
120
117
|
"""
|
121
118
|
Get data to populate enterprise enrollment charts.
|
@@ -128,7 +125,7 @@ class AdvanceAnalyticsEnrollmentsView(AnalyticsPaginationMixin, ViewSet):
|
|
128
125
|
# Remove hyphens from the UUID
|
129
126
|
enterprise_uuid = enterprise_uuid.replace('-', '')
|
130
127
|
|
131
|
-
serializer =
|
128
|
+
serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
|
132
129
|
serializer.is_valid(raise_exception=True)
|
133
130
|
|
134
131
|
min_enrollment_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(
|
enterprise_data/renderers.py
CHANGED
@@ -45,17 +45,17 @@ class IndividualEnrollmentsCSVRenderer(CSVStreamingRenderer):
|
|
45
45
|
]
|
46
46
|
|
47
47
|
|
48
|
-
class
|
48
|
+
class IndividualCompletionsCSVRenderer(CSVStreamingRenderer):
|
49
49
|
"""
|
50
|
-
Custom streaming csv renderer for advance analytics
|
50
|
+
Custom streaming csv renderer for advance analytics individual completions data.
|
51
51
|
"""
|
52
52
|
|
53
53
|
header = [
|
54
54
|
'email',
|
55
|
-
'
|
56
|
-
'
|
57
|
-
'
|
58
|
-
'
|
55
|
+
'course_title',
|
56
|
+
'course_subject',
|
57
|
+
'enroll_type',
|
58
|
+
'passed_date',
|
59
59
|
]
|
60
60
|
|
61
61
|
|
@@ -67,7 +67,22 @@ class IndividualEngagementsCSVRenderer(CSVStreamingRenderer):
|
|
67
67
|
header = [
|
68
68
|
'email',
|
69
69
|
'course_title',
|
70
|
-
'activity_date',
|
71
70
|
'course_subject',
|
71
|
+
'enroll_type',
|
72
|
+
'activity_date',
|
73
|
+
'learning_time_hours',
|
74
|
+
]
|
75
|
+
|
76
|
+
|
77
|
+
class LeaderboardCSVRenderer(CSVStreamingRenderer):
|
78
|
+
"""
|
79
|
+
Custom streaming csv renderer for advance analytics leaderboard data.
|
80
|
+
"""
|
81
|
+
|
82
|
+
header = [
|
83
|
+
'email',
|
72
84
|
'learning_time_hours',
|
85
|
+
'daily_sessions',
|
86
|
+
'average_session_length',
|
87
|
+
'course_completions',
|
73
88
|
]
|