edx-enterprise-data 8.11.0__py3-none-any.whl → 8.12.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.11.0.dist-info → edx_enterprise_data-8.12.0.dist-info}/METADATA +1 -1
- {edx_enterprise_data-8.11.0.dist-info → edx_enterprise_data-8.12.0.dist-info}/RECORD +16 -16
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py +78 -0
- enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py +128 -3
- enterprise_data/admin_analytics/database/utils.py +8 -3
- enterprise_data/api/v1/serializers.py +2 -0
- enterprise_data/api/v1/urls.py +3 -6
- enterprise_data/api/v1/views/analytics_enrollments.py +90 -332
- enterprise_data/api/v1/views/base.py +79 -0
- enterprise_data/api/v1/views/enterprise_admin.py +5 -3
- enterprise_data/tests/admin_analytics/mock_analytics_data.py +0 -12
- enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +72 -115
- {edx_enterprise_data-8.11.0.dist-info → edx_enterprise_data-8.12.0.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-8.11.0.dist-info → edx_enterprise_data-8.12.0.dist-info}/WHEEL +0 -0
- {edx_enterprise_data-8.11.0.dist-info → edx_enterprise_data-8.12.0.dist-info}/top_level.txt +0 -0
@@ -1,52 +1,71 @@
|
|
1
|
-
"""
|
1
|
+
"""
|
2
|
+
Advance Analytics for API endpoints to fetch enterprise enrollments data.
|
3
|
+
"""
|
2
4
|
from datetime import datetime
|
3
5
|
from logging import getLogger
|
4
6
|
|
5
7
|
from edx_rbac.decorators import permission_required
|
6
8
|
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
9
|
+
from rest_framework.decorators import action
|
7
10
|
from rest_framework.response import Response
|
8
|
-
from rest_framework.
|
11
|
+
from rest_framework.viewsets import ViewSet
|
9
12
|
|
10
|
-
from django.http import
|
13
|
+
from django.http import StreamingHttpResponse
|
11
14
|
|
12
|
-
from enterprise_data.admin_analytics.constants import
|
13
|
-
from enterprise_data.admin_analytics.
|
14
|
-
calculation_aggregation,
|
15
|
-
fetch_and_cache_enrollments_data,
|
16
|
-
fetch_enrollments_cache_expiry_timestamp,
|
17
|
-
granularity_aggregation,
|
18
|
-
)
|
15
|
+
from enterprise_data.admin_analytics.constants import ResponseType
|
16
|
+
from enterprise_data.admin_analytics.database.tables import FactEnrollmentAdminDashTable
|
19
17
|
from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
|
20
18
|
from enterprise_data.api.v1.serializers import (
|
21
19
|
AdvanceAnalyticsEnrollmentStatsSerializer,
|
22
20
|
AdvanceAnalyticsQueryParamSerializer,
|
23
21
|
)
|
22
|
+
from enterprise_data.api.v1.views.base import AnalyticsPaginationMixin
|
24
23
|
from enterprise_data.renderers import IndividualEnrollmentsCSVRenderer
|
25
|
-
from enterprise_data.utils import
|
24
|
+
from enterprise_data.utils import timer
|
26
25
|
|
27
26
|
LOGGER = getLogger(__name__)
|
28
27
|
|
29
28
|
|
30
|
-
class
|
29
|
+
class AdvanceAnalyticsEnrollmentsView(AnalyticsPaginationMixin, ViewSet):
|
31
30
|
"""
|
32
|
-
|
31
|
+
View to handle requests for enterprise enrollments data.
|
32
|
+
|
33
|
+
Here is the list of URLs that are handled by this view:
|
34
|
+
1. `enterprise_data_api_v1.enterprise-learner-enrollment-list`: Get individual enrollment data.
|
35
|
+
2. `enterprise_data_api_v1.enterprise-learner-enrollment-stats`: Get enrollment stats data.
|
33
36
|
"""
|
34
37
|
authentication_classes = (JwtAuthentication,)
|
35
38
|
pagination_class = AdvanceAnalyticsPagination
|
36
|
-
http_method_names =
|
39
|
+
http_method_names = ('get', )
|
37
40
|
|
38
41
|
@permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
|
39
|
-
def
|
40
|
-
"""
|
42
|
+
def list(self, request, enterprise_uuid):
|
43
|
+
"""
|
44
|
+
Get individual enrollments data for the enterprise.
|
45
|
+
"""
|
41
46
|
serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
|
42
47
|
serializer.is_valid(raise_exception=True)
|
43
|
-
|
44
|
-
|
45
|
-
|
48
|
+
min_enrollment_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(
|
49
|
+
enterprise_uuid,
|
50
|
+
)
|
46
51
|
|
47
52
|
# get values from query params or use default values
|
48
|
-
start_date = serializer.data.get('start_date',
|
53
|
+
start_date = serializer.data.get('start_date', min_enrollment_date)
|
49
54
|
end_date = serializer.data.get('end_date', datetime.now())
|
55
|
+
page = serializer.data.get('page', 1)
|
56
|
+
page_size = serializer.data.get('page_size', 100)
|
57
|
+
enrollments = FactEnrollmentAdminDashTable().get_all_enrollments(
|
58
|
+
enterprise_customer_uuid=enterprise_uuid,
|
59
|
+
start_date=start_date,
|
60
|
+
end_date=end_date,
|
61
|
+
limit=page_size,
|
62
|
+
offset=(page - 1) * page_size,
|
63
|
+
)
|
64
|
+
total_count = FactEnrollmentAdminDashTable().get_enrollment_count(
|
65
|
+
enterprise_customer_uuid=enterprise_uuid,
|
66
|
+
start_date=start_date,
|
67
|
+
end_date=end_date,
|
68
|
+
)
|
50
69
|
response_type = request.query_params.get('response_type', ResponseType.JSON.value)
|
51
70
|
|
52
71
|
LOGGER.info(
|
@@ -56,333 +75,72 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
|
|
56
75
|
end_date,
|
57
76
|
)
|
58
77
|
|
59
|
-
# filter enrollments by date
|
60
|
-
enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
|
61
|
-
|
62
|
-
# select only the columns that will be in the table.
|
63
|
-
enrollments = enrollments[
|
64
|
-
[
|
65
|
-
"email",
|
66
|
-
"course_title",
|
67
|
-
"course_subject",
|
68
|
-
"enroll_type",
|
69
|
-
"enterprise_enrollment_date",
|
70
|
-
]
|
71
|
-
]
|
72
|
-
enrollments["enterprise_enrollment_date"] = enrollments["enterprise_enrollment_date"].dt.date
|
73
|
-
enrollments = enrollments.sort_values(by="enterprise_enrollment_date", ascending=False).reset_index(drop=True)
|
74
|
-
|
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
78
|
if response_type == ResponseType.CSV.value:
|
83
79
|
filename = f"""individual_enrollments, {start_date} - {end_date}.csv"""
|
80
|
+
|
84
81
|
return StreamingHttpResponse(
|
85
|
-
IndividualEnrollmentsCSVRenderer().render(self._stream_serialized_data(
|
82
|
+
IndividualEnrollmentsCSVRenderer().render(self._stream_serialized_data(
|
83
|
+
enterprise_uuid, start_date, end_date, total_count
|
84
|
+
)),
|
86
85
|
content_type="text/csv",
|
87
86
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
88
87
|
)
|
89
88
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
89
|
+
return self.get_paginated_response(
|
90
|
+
request=request,
|
91
|
+
records=enrollments,
|
92
|
+
page=page,
|
93
|
+
page_size=page_size,
|
94
|
+
total_count=total_count,
|
95
|
+
)
|
96
96
|
|
97
|
-
|
97
|
+
@staticmethod
|
98
|
+
def _stream_serialized_data(enterprise_uuid, start_date, end_date, total_count, page_size=50000):
|
98
99
|
"""
|
99
100
|
Stream the serialized data.
|
100
101
|
"""
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
authentication_classes = (JwtAuthentication,)
|
113
|
-
http_method_names = ['get']
|
102
|
+
offset = 0
|
103
|
+
while offset < total_count:
|
104
|
+
enrollments = FactEnrollmentAdminDashTable().get_all_enrollments(
|
105
|
+
enterprise_customer_uuid=enterprise_uuid,
|
106
|
+
start_date=start_date,
|
107
|
+
end_date=end_date,
|
108
|
+
limit=page_size,
|
109
|
+
offset=offset,
|
110
|
+
)
|
111
|
+
yield from enrollments
|
112
|
+
offset += page_size
|
114
113
|
|
115
114
|
@permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
|
116
|
-
|
117
|
-
|
118
|
-
serializer = AdvanceAnalyticsEnrollmentStatsSerializer(data=request.GET)
|
119
|
-
serializer.is_valid(raise_exception=True)
|
120
|
-
|
121
|
-
cache_expiry = fetch_enrollments_cache_expiry_timestamp()
|
122
|
-
enrollments_df = fetch_and_cache_enrollments_data(enterprise_uuid, cache_expiry)
|
123
|
-
|
124
|
-
# get values from query params or use default
|
125
|
-
start_date = serializer.data.get('start_date', enrollments_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
|
-
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
|
-
}
|
155
|
-
return Response(data)
|
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
|
-
)
|
181
|
-
|
182
|
-
def enrollments_over_time_common(self, enrollments_df, start_date, end_date, granularity, calculation):
|
183
|
-
"""
|
184
|
-
Common method for constructing enrollments over time data.
|
185
|
-
|
186
|
-
Arguments:
|
187
|
-
enrollments_df {DataFrame} -- DataFrame of enrollments
|
188
|
-
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
189
|
-
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
190
|
-
granularity {str} -- Granularity of the data. One of Granularity choices
|
191
|
-
calculation {str} -- Calculation of the data. One of Calculation choices
|
192
|
-
"""
|
193
|
-
# filter enrollments by date
|
194
|
-
enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
|
195
|
-
|
196
|
-
# aggregate enrollments by granularity
|
197
|
-
enrollments = granularity_aggregation(
|
198
|
-
level=granularity,
|
199
|
-
group=["enterprise_enrollment_date", "enroll_type"],
|
200
|
-
date="enterprise_enrollment_date",
|
201
|
-
data_frame=enrollments,
|
202
|
-
)
|
203
|
-
|
204
|
-
# aggregate enrollments by calculation
|
205
|
-
enrollments = calculation_aggregation(calc=calculation, data_frame=enrollments)
|
206
|
-
|
207
|
-
return enrollments
|
208
|
-
|
209
|
-
def construct_enrollments_over_time(self, enrollments_df, start_date, end_date, granularity, calculation):
|
210
|
-
"""
|
211
|
-
Construct enrollments over time data.
|
212
|
-
|
213
|
-
Arguments:
|
214
|
-
enrollments_df {DataFrame} -- DataFrame of enrollments
|
215
|
-
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
216
|
-
end_date {datetime} -- Enrollment 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
|
-
enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
|
221
|
-
|
222
|
-
# convert dataframe to a list of records
|
223
|
-
return enrollments.to_dict(orient='records')
|
224
|
-
|
225
|
-
def construct_enrollments_over_time_csv(self, enrollments_df, start_date, end_date, granularity, calculation):
|
226
|
-
"""
|
227
|
-
Construct enrollments over time CSV.
|
228
|
-
|
229
|
-
Arguments:
|
230
|
-
enrollments_df {DataFrame} -- DataFrame of enrollments
|
231
|
-
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
232
|
-
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
233
|
-
granularity {str} -- Granularity of the data. One of Granularity choices
|
234
|
-
calculation {str} -- Calculation of the data. One of Calculation choices
|
235
|
-
"""
|
236
|
-
enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
|
237
|
-
|
238
|
-
enrollments = enrollments.pivot(
|
239
|
-
index="enterprise_enrollment_date", columns="enroll_type", values="count"
|
240
|
-
)
|
241
|
-
|
242
|
-
filename = f"Enrollment Timeseries, {start_date} - {end_date} ({granularity} {calculation}).csv"
|
243
|
-
return self.construct_csv_response(enrollments, filename)
|
244
|
-
|
245
|
-
def top_courses_by_enrollments_common(self, enrollments_df, start_date, end_date, group_by_columns, columns):
|
246
|
-
"""
|
247
|
-
Common method for constructing top courses by enrollments data.
|
248
|
-
|
249
|
-
Arguments:
|
250
|
-
enrollments_df {DataFrame} -- DataFrame of enrollments
|
251
|
-
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
252
|
-
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
253
|
-
group_by_columns {list} -- List of columns to group by
|
254
|
-
columns {list} -- List of column for the final result
|
255
|
-
"""
|
256
|
-
# filter enrollments by date
|
257
|
-
enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
|
258
|
-
|
259
|
-
courses = list(
|
260
|
-
enrollments.groupby(["course_key"]).size().sort_values(ascending=False)[:10].index
|
261
|
-
)
|
262
|
-
|
263
|
-
enrollments = (
|
264
|
-
enrollments[enrollments.course_key.isin(courses)]
|
265
|
-
.groupby(group_by_columns)
|
266
|
-
.size()
|
267
|
-
.reset_index()
|
268
|
-
)
|
269
|
-
enrollments.columns = columns
|
270
|
-
|
271
|
-
return enrollments
|
272
|
-
|
273
|
-
def construct_top_courses_by_enrollments(self, enrollments_df, start_date, end_date):
|
115
|
+
@action(detail=False, methods=['get'], name='Charts Data', url_path='stats')
|
116
|
+
def stats(self, request, enterprise_uuid):
|
274
117
|
"""
|
275
|
-
|
118
|
+
Get data to populate enterprise enrollment charts.
|
276
119
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
120
|
+
Here is the list of the charts and their corresponding data:
|
121
|
+
1. `enrollments_over_time`: This will show time series data of enrollments over time.
|
122
|
+
2. `top_courses_by_enrollments`: This will show the top courses by enrollments.
|
123
|
+
3. `top_subjects_by_enrollments`: This will show the top subjects by enrollments.
|
281
124
|
"""
|
282
|
-
|
283
|
-
|
284
|
-
enrollments = self.top_courses_by_enrollments_common(
|
285
|
-
enrollments_df,
|
286
|
-
start_date,
|
287
|
-
end_date,
|
288
|
-
group_by_columns,
|
289
|
-
columns
|
290
|
-
)
|
291
|
-
|
292
|
-
# convert dataframe to a list of records
|
293
|
-
return enrollments.to_dict(orient='records')
|
294
|
-
|
295
|
-
def construct_top_courses_by_enrollments_csv(self, enrollments_df, start_date, end_date):
|
296
|
-
"""
|
297
|
-
Construct top courses by enrollments CSV.
|
298
|
-
|
299
|
-
Arguments:
|
300
|
-
enrollments_df {DataFrame} -- DataFrame of enrollments
|
301
|
-
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
302
|
-
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
303
|
-
"""
|
304
|
-
group_by_columns = ["course_key", "course_title", "enroll_type"]
|
305
|
-
columns = ["course_key", "course_title", "enroll_type", "count"]
|
306
|
-
enrollments = self.top_courses_by_enrollments_common(
|
307
|
-
enrollments_df,
|
308
|
-
start_date,
|
309
|
-
end_date,
|
310
|
-
group_by_columns,
|
311
|
-
columns
|
312
|
-
)
|
313
|
-
|
314
|
-
enrollments = enrollments.pivot(
|
315
|
-
index=["course_key", "course_title"], columns="enroll_type", values="count"
|
316
|
-
)
|
317
|
-
|
318
|
-
filename = f"Top 10 Courses, {start_date} - {end_date}.csv"
|
319
|
-
return self.construct_csv_response(enrollments, filename)
|
320
|
-
|
321
|
-
def top_subjects_by_enrollments_common(self, enrollments_df, start_date, end_date):
|
322
|
-
"""
|
323
|
-
Common method for constructing top subjects by enrollments data.
|
324
|
-
|
325
|
-
Arguments:
|
326
|
-
enrollments_df {DataFrame} -- DataFrame of enrollments
|
327
|
-
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
328
|
-
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
329
|
-
"""
|
330
|
-
# filter enrollments by date
|
331
|
-
enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
|
332
|
-
|
333
|
-
subjects = list(
|
334
|
-
enrollments.groupby(["course_subject"]).size().sort_values(ascending=False)[:10].index
|
335
|
-
)
|
125
|
+
serializer = AdvanceAnalyticsEnrollmentStatsSerializer(data=request.GET)
|
126
|
+
serializer.is_valid(raise_exception=True)
|
336
127
|
|
337
|
-
|
338
|
-
|
339
|
-
.groupby(["course_subject", "enroll_type"])
|
340
|
-
.size()
|
341
|
-
.reset_index()
|
128
|
+
min_enrollment_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(
|
129
|
+
enterprise_uuid,
|
342
130
|
)
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
return
|
359
|
-
|
360
|
-
def construct_top_subjects_by_enrollments_csv(self, enrollments_df, start_date, end_date):
|
361
|
-
"""
|
362
|
-
Construct top subjects by enrollments CSV.
|
363
|
-
|
364
|
-
Arguments:
|
365
|
-
enrollments_df {DataFrame} -- DataFrame of enrollments
|
366
|
-
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
367
|
-
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
368
|
-
"""
|
369
|
-
enrollments = self.top_subjects_by_enrollments_common(enrollments_df, start_date, end_date)
|
370
|
-
enrollments = enrollments.pivot(index="course_subject", columns="enroll_type", values="count")
|
371
|
-
|
372
|
-
filename = f"Top 10 Subjects by Enrollment, {start_date} - {end_date}.csv"
|
373
|
-
return self.construct_csv_response(enrollments, filename)
|
374
|
-
|
375
|
-
def construct_csv_response(self, enrollments, filename):
|
376
|
-
"""
|
377
|
-
Construct CSV response.
|
378
|
-
|
379
|
-
Arguments:
|
380
|
-
enrollments {DataFrame} -- DataFrame of enrollments
|
381
|
-
filename {str} -- Filename for the CSV
|
382
|
-
"""
|
383
|
-
response = HttpResponse(content_type='text/csv')
|
384
|
-
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
385
|
-
response['Access-Control-Expose-Headers'] = 'Content-Disposition'
|
386
|
-
enrollments.to_csv(path_or_buf=response)
|
387
|
-
|
388
|
-
return response
|
131
|
+
# get values from query params or use default
|
132
|
+
start_date = serializer.data.get('start_date', min_enrollment_date)
|
133
|
+
end_date = serializer.data.get('end_date', datetime.now())
|
134
|
+
with timer('construct_enrollment_all_stats'):
|
135
|
+
data = {
|
136
|
+
'enrollments_over_time': FactEnrollmentAdminDashTable().get_enrolment_time_series_data(
|
137
|
+
enterprise_uuid, start_date, end_date
|
138
|
+
),
|
139
|
+
'top_courses_by_enrollments': FactEnrollmentAdminDashTable().get_top_courses_by_enrollments(
|
140
|
+
enterprise_uuid, start_date, end_date,
|
141
|
+
),
|
142
|
+
'top_subjects_by_enrollments': FactEnrollmentAdminDashTable().get_top_subjects_by_enrollments(
|
143
|
+
enterprise_uuid, start_date, end_date,
|
144
|
+
),
|
145
|
+
}
|
146
|
+
return Response(data)
|
@@ -1,9 +1,14 @@
|
|
1
1
|
"""
|
2
2
|
Base views for enterprise data api v1.
|
3
3
|
"""
|
4
|
+
import math
|
5
|
+
|
4
6
|
from edx_rbac.mixins import PermissionRequiredMixin
|
5
7
|
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
6
8
|
from edx_rest_framework_extensions.paginators import DefaultPagination
|
9
|
+
from rest_framework.exceptions import NotFound
|
10
|
+
from rest_framework.response import Response
|
11
|
+
from rest_framework.utils.urls import remove_query_param, replace_query_param
|
7
12
|
|
8
13
|
from enterprise_data.constants import ANALYTICS_API_VERSION_1
|
9
14
|
|
@@ -24,3 +29,77 @@ class EnterpriseViewSetMixin(PermissionRequiredMixin):
|
|
24
29
|
if 'no_page' in self.request.query_params:
|
25
30
|
return None
|
26
31
|
return super().paginate_queryset(queryset)
|
32
|
+
|
33
|
+
|
34
|
+
class AnalyticsPaginationMixin:
|
35
|
+
"""
|
36
|
+
Mixin that provides utility methods to allow pagination on views.
|
37
|
+
"""
|
38
|
+
page_query_param = 'page'
|
39
|
+
page_size_query_param = 'page_size'
|
40
|
+
|
41
|
+
def get_next_link(self, request, page_number, page_count):
|
42
|
+
"""
|
43
|
+
Get the link to the next page.
|
44
|
+
|
45
|
+
Arguments:
|
46
|
+
request (Request): The request object.
|
47
|
+
page_number (int): The current page number.
|
48
|
+
page_count (int): The total number of pages.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
str: The link to the next page.
|
52
|
+
"""
|
53
|
+
if page_number >= page_count:
|
54
|
+
return None
|
55
|
+
return replace_query_param(
|
56
|
+
url=request.build_absolute_uri(),
|
57
|
+
key=self.page_query_param,
|
58
|
+
val=page_number + 1,
|
59
|
+
)
|
60
|
+
|
61
|
+
def get_previous_link(self, request, page_number):
|
62
|
+
"""
|
63
|
+
Get the link to the previous page.
|
64
|
+
|
65
|
+
Arguments:
|
66
|
+
request (Request): The request object.
|
67
|
+
page_number (int): The current page number.
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
str: The link to the previous page.
|
71
|
+
"""
|
72
|
+
if page_number <= 1:
|
73
|
+
return None
|
74
|
+
url = request.build_absolute_uri()
|
75
|
+
next_page = page_number - 1
|
76
|
+
if next_page == 1:
|
77
|
+
return remove_query_param(url, self.page_query_param)
|
78
|
+
return replace_query_param(url, self.page_query_param, next_page)
|
79
|
+
|
80
|
+
def get_paginated_response(self, request, records, page, page_size, total_count):
|
81
|
+
"""
|
82
|
+
Get pagination data.
|
83
|
+
|
84
|
+
Arguments:
|
85
|
+
request (Request): The request object.
|
86
|
+
records (list): The records to return.
|
87
|
+
page (int): The current page number.
|
88
|
+
page_size (int): The number of records per page.
|
89
|
+
total_count (int): The total number of records
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
(Response): The pagination data.
|
93
|
+
"""
|
94
|
+
page_count = math.ceil(total_count / page_size)
|
95
|
+
if page <= 0 or page > page_count:
|
96
|
+
raise NotFound('Invalid page.')
|
97
|
+
|
98
|
+
return Response({
|
99
|
+
'next': self.get_next_link(request, page, page_count),
|
100
|
+
'previous': self.get_previous_link(request, page),
|
101
|
+
'count': total_count,
|
102
|
+
'num_pages': page_count,
|
103
|
+
'current_page': page,
|
104
|
+
'results': records,
|
105
|
+
})
|
@@ -95,6 +95,8 @@ class EnterpriseAdminAnalyticsAggregatesView(APIView):
|
|
95
95
|
"""
|
96
96
|
HTTP GET endpoint to retrieve the enterprise admin aggregate data.
|
97
97
|
"""
|
98
|
+
# Validate the enterprise_id
|
99
|
+
enterprise_id = enterprise_id.replace('-', '')
|
98
100
|
serializer = serializers.AdminAnalyticsAggregatesQueryParamsSerializer(
|
99
101
|
data=request.GET
|
100
102
|
)
|
@@ -106,7 +108,7 @@ class EnterpriseAdminAnalyticsAggregatesView(APIView):
|
|
106
108
|
)
|
107
109
|
|
108
110
|
start_date = serializer.data.get(
|
109
|
-
'start_date', min_enrollment_date
|
111
|
+
'start_date', min_enrollment_date
|
110
112
|
)
|
111
113
|
end_date = serializer.data.get('end_date', datetime.today())
|
112
114
|
|
@@ -128,8 +130,8 @@ class EnterpriseAdminAnalyticsAggregatesView(APIView):
|
|
128
130
|
'hours': hours,
|
129
131
|
'sessions': sessions,
|
130
132
|
'last_updated_at': last_updated_at.date() if last_updated_at else None,
|
131
|
-
'min_enrollment_date': min_enrollment_date
|
132
|
-
'max_enrollment_date': max_enrollment_date
|
133
|
+
'min_enrollment_date': min_enrollment_date,
|
134
|
+
'max_enrollment_date': max_enrollment_date,
|
133
135
|
},
|
134
136
|
status=HTTP_200_OK,
|
135
137
|
)
|
@@ -384,18 +384,6 @@ def engagements_dataframe():
|
|
384
384
|
return engagements
|
385
385
|
|
386
386
|
|
387
|
-
def enrollments_csv_content():
|
388
|
-
"""Return the CSV content of enrollments."""
|
389
|
-
return (
|
390
|
-
b'email,course_title,course_subject,enroll_type,enterprise_enrollment_date\r\n'
|
391
|
-
b'rebeccanelson@example.com,Re-engineered tangible approach,business-management,certificate,2021-07-04\r\n'
|
392
|
-
b'taylorjames@example.com,Re-engineered tangible approach,business-management,certificate,2021-07-03\r\n'
|
393
|
-
b'ssmith@example.com,Secured static capability,medicine,certificate,2021-05-11\r\n'
|
394
|
-
b'amber79@example.com,Streamlined zero-defect attitude,communication,certificate,2020-04-08\r\n'
|
395
|
-
b'kathleenmartin@example.com,Horizontal solution-oriented hub,social-sciences,certificate,2020-04-03\r\n'
|
396
|
-
)
|
397
|
-
|
398
|
-
|
399
387
|
def leaderboard_csv_content():
|
400
388
|
"""Return the CSV content of leaderboard."""
|
401
389
|
# return (
|