edx-enterprise-data 8.5.0__py3-none-any.whl → 8.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {edx_enterprise_data-8.5.0.dist-info → edx_enterprise_data-8.6.1.dist-info}/METADATA +1 -1
- {edx_enterprise_data-8.5.0.dist-info → edx_enterprise_data-8.6.1.dist-info}/RECORD +15 -10
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/constants.py +27 -0
- enterprise_data/admin_analytics/utils.py +49 -0
- enterprise_data/api/v1/paginators.py +121 -0
- enterprise_data/api/v1/serializers.py +90 -0
- enterprise_data/api/v1/urls.py +14 -0
- enterprise_data/api/v1/views/analytics_enrollments.py +375 -0
- enterprise_data/renderers.py +14 -0
- enterprise_data/tests/admin_analytics/mock_enrollments.py +169 -0
- enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +393 -0
- {edx_enterprise_data-8.5.0.dist-info → edx_enterprise_data-8.6.1.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-8.5.0.dist-info → edx_enterprise_data-8.6.1.dist-info}/WHEEL +0 -0
- {edx_enterprise_data-8.5.0.dist-info → edx_enterprise_data-8.6.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,375 @@
|
|
1
|
+
"""Advance Analytics for Enrollments"""
|
2
|
+
from datetime import datetime, timedelta
|
3
|
+
|
4
|
+
from edx_rbac.decorators import permission_required
|
5
|
+
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
6
|
+
from rest_framework.response import Response
|
7
|
+
from rest_framework.views import APIView
|
8
|
+
|
9
|
+
from django.http import HttpResponse, StreamingHttpResponse
|
10
|
+
|
11
|
+
from enterprise_data.admin_analytics.constants import CALCULATION, ENROLLMENT_CSV, GRANULARITY
|
12
|
+
from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_datetime
|
13
|
+
from enterprise_data.admin_analytics.utils import (
|
14
|
+
calculation_aggregation,
|
15
|
+
fetch_and_cache_enrollments_data,
|
16
|
+
granularity_aggregation,
|
17
|
+
)
|
18
|
+
from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
|
19
|
+
from enterprise_data.api.v1.serializers import (
|
20
|
+
AdvanceAnalyticsEnrollmentSerializer,
|
21
|
+
AdvanceAnalyticsEnrollmentStatsSerializer,
|
22
|
+
)
|
23
|
+
from enterprise_data.renderers import IndividualEnrollmentsCSVRenderer
|
24
|
+
from enterprise_data.utils import date_filter
|
25
|
+
|
26
|
+
|
27
|
+
def fetch_enrollments_cache_expiry_timestamp():
|
28
|
+
"""Calculate cache expiry timestamp"""
|
29
|
+
# TODO: Implement correct cache expiry logic for `enrollments` data.
|
30
|
+
# Current cache expiry logic is based on `enterprise_learner_enrollment` table,
|
31
|
+
# Which has nothing to do with the `enrollments` data. Instead cache expiry should
|
32
|
+
# be based on `fact_enrollment_admin_dash` table. Currently we have no timestamp in
|
33
|
+
# `fact_enrollment_admin_dash` table that can be used for cache expiry. Add a new
|
34
|
+
# column in the table for this purpose and then use that column for cache expiry.
|
35
|
+
last_updated_at = fetch_max_enrollment_datetime()
|
36
|
+
cache_expiry = (
|
37
|
+
last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
|
38
|
+
)
|
39
|
+
return cache_expiry
|
40
|
+
|
41
|
+
|
42
|
+
class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
|
43
|
+
"""
|
44
|
+
API for getting the advance analytics individual enrollments data.
|
45
|
+
"""
|
46
|
+
authentication_classes = (JwtAuthentication,)
|
47
|
+
pagination_class = AdvanceAnalyticsPagination
|
48
|
+
http_method_names = ['get']
|
49
|
+
|
50
|
+
@permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
|
51
|
+
def get(self, request, enterprise_uuid):
|
52
|
+
"""Get individual enrollments data"""
|
53
|
+
serializer = AdvanceAnalyticsEnrollmentSerializer(data=request.GET)
|
54
|
+
serializer.is_valid(raise_exception=True)
|
55
|
+
|
56
|
+
cache_expiry = fetch_enrollments_cache_expiry_timestamp()
|
57
|
+
enrollments_df = fetch_and_cache_enrollments_data(enterprise_uuid, cache_expiry)
|
58
|
+
|
59
|
+
# get values from query params or use default values
|
60
|
+
start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
|
61
|
+
end_date = serializer.data.get('end_date', datetime.now())
|
62
|
+
csv_type = request.query_params.get('csv_type')
|
63
|
+
|
64
|
+
# filter enrollments by date
|
65
|
+
enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
|
66
|
+
|
67
|
+
# select only the columns that will be in the table.
|
68
|
+
enrollments = enrollments[
|
69
|
+
[
|
70
|
+
"email",
|
71
|
+
"course_title",
|
72
|
+
"course_subject",
|
73
|
+
"enroll_type",
|
74
|
+
"enterprise_enrollment_date",
|
75
|
+
]
|
76
|
+
]
|
77
|
+
enrollments["enterprise_enrollment_date"] = enrollments["enterprise_enrollment_date"].dt.date
|
78
|
+
enrollments = enrollments.sort_values(by="enterprise_enrollment_date", ascending=False).reset_index(drop=True)
|
79
|
+
|
80
|
+
if csv_type == ENROLLMENT_CSV.INDIVIDUAL_ENROLLMENTS.value:
|
81
|
+
return StreamingHttpResponse(
|
82
|
+
IndividualEnrollmentsCSVRenderer().render(self._stream_serialized_data(enrollments)),
|
83
|
+
content_type="text/csv",
|
84
|
+
headers={"Content-Disposition": 'attachment; filename="individual_enrollments.csv"'},
|
85
|
+
)
|
86
|
+
|
87
|
+
paginator = self.pagination_class()
|
88
|
+
page = paginator.paginate_queryset(enrollments, request)
|
89
|
+
serialized_data = page.data.to_dict(orient='records')
|
90
|
+
response = paginator.get_paginated_response(serialized_data)
|
91
|
+
|
92
|
+
return response
|
93
|
+
|
94
|
+
def _stream_serialized_data(self, enrollments, chunk_size=50000):
|
95
|
+
"""
|
96
|
+
Stream the serialized data.
|
97
|
+
"""
|
98
|
+
total_rows = enrollments.shape[0]
|
99
|
+
for start_index in range(0, total_rows, chunk_size):
|
100
|
+
end_index = min(start_index + chunk_size, total_rows)
|
101
|
+
chunk = enrollments.iloc[start_index:end_index]
|
102
|
+
yield from chunk.to_dict(orient='records')
|
103
|
+
|
104
|
+
|
105
|
+
class AdvanceAnalyticsEnrollmentStatsView(APIView):
|
106
|
+
"""
|
107
|
+
API for getting the advance analytics enrollment chart stats.
|
108
|
+
"""
|
109
|
+
authentication_classes = (JwtAuthentication,)
|
110
|
+
http_method_names = ['get']
|
111
|
+
|
112
|
+
@permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
|
113
|
+
def get(self, request, enterprise_uuid): # lint-amnesty, pylint: disable=inconsistent-return-statements
|
114
|
+
"""Get enrollment chart stats"""
|
115
|
+
serializer = AdvanceAnalyticsEnrollmentStatsSerializer(data=request.GET)
|
116
|
+
serializer.is_valid(raise_exception=True)
|
117
|
+
|
118
|
+
cache_expiry = fetch_enrollments_cache_expiry_timestamp()
|
119
|
+
enrollments_df = fetch_and_cache_enrollments_data(enterprise_uuid, cache_expiry)
|
120
|
+
|
121
|
+
# get values from query params or use default
|
122
|
+
start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
|
123
|
+
end_date = serializer.data.get('end_date', datetime.now())
|
124
|
+
granularity = serializer.data.get('granularity', GRANULARITY.DAILY.value)
|
125
|
+
calculation = serializer.data.get('calculation', CALCULATION.TOTAL.value)
|
126
|
+
csv_type = serializer.data.get('csv_type')
|
127
|
+
|
128
|
+
if csv_type is None:
|
129
|
+
data = {
|
130
|
+
"enrollments_over_time": self.construct_enrollments_over_time(
|
131
|
+
enrollments_df.copy(),
|
132
|
+
start_date,
|
133
|
+
end_date,
|
134
|
+
granularity,
|
135
|
+
calculation,
|
136
|
+
),
|
137
|
+
"top_courses_by_enrollments": self.construct_top_courses_by_enrollments(
|
138
|
+
enrollments_df.copy(),
|
139
|
+
start_date,
|
140
|
+
end_date,
|
141
|
+
),
|
142
|
+
"top_subjects_by_enrollments": self.construct_top_subjects_by_enrollments(
|
143
|
+
enrollments_df.copy(),
|
144
|
+
start_date,
|
145
|
+
end_date,
|
146
|
+
),
|
147
|
+
}
|
148
|
+
return Response(data)
|
149
|
+
elif csv_type == ENROLLMENT_CSV.ENROLLMENTS_OVER_TIME.value:
|
150
|
+
return self.construct_enrollments_over_time_csv(
|
151
|
+
enrollments_df.copy(),
|
152
|
+
start_date,
|
153
|
+
end_date,
|
154
|
+
granularity,
|
155
|
+
calculation,
|
156
|
+
)
|
157
|
+
elif csv_type == ENROLLMENT_CSV.TOP_COURSES_BY_ENROLLMENTS.value:
|
158
|
+
return self.construct_top_courses_by_enrollments_csv(
|
159
|
+
enrollments_df.copy(),
|
160
|
+
start_date,
|
161
|
+
end_date,
|
162
|
+
)
|
163
|
+
elif csv_type == ENROLLMENT_CSV.TOP_SUBJECTS_BY_ENROLLMENTS.value:
|
164
|
+
return self.construct_top_subjects_by_enrollments_csv(
|
165
|
+
enrollments_df.copy(),
|
166
|
+
start_date,
|
167
|
+
end_date,
|
168
|
+
)
|
169
|
+
|
170
|
+
def enrollments_over_time_common(self, enrollments_df, start_date, end_date, granularity, calculation):
|
171
|
+
"""
|
172
|
+
Common method for constructing enrollments over time data.
|
173
|
+
|
174
|
+
Arguments:
|
175
|
+
enrollments_df {DataFrame} -- DataFrame of enrollments
|
176
|
+
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
177
|
+
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
178
|
+
granularity {str} -- Granularity of the data. One of GRANULARITY choices
|
179
|
+
calculation {str} -- Calculation of the data. One of CALCULATION choices
|
180
|
+
"""
|
181
|
+
# filter enrollments by date
|
182
|
+
enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
|
183
|
+
|
184
|
+
# aggregate enrollments by granularity
|
185
|
+
enrollments = granularity_aggregation(
|
186
|
+
level=granularity,
|
187
|
+
group=["enterprise_enrollment_date", "enroll_type"],
|
188
|
+
date="enterprise_enrollment_date",
|
189
|
+
data_frame=enrollments,
|
190
|
+
)
|
191
|
+
|
192
|
+
# aggregate enrollments by calculation
|
193
|
+
enrollments = calculation_aggregation(calc=calculation, data_frame=enrollments)
|
194
|
+
|
195
|
+
return enrollments
|
196
|
+
|
197
|
+
def construct_enrollments_over_time(self, enrollments_df, start_date, end_date, granularity, calculation):
|
198
|
+
"""
|
199
|
+
Construct enrollments over time data.
|
200
|
+
|
201
|
+
Arguments:
|
202
|
+
enrollments_df {DataFrame} -- DataFrame of enrollments
|
203
|
+
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
204
|
+
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
205
|
+
granularity {str} -- Granularity of the data. One of GRANULARITY choices
|
206
|
+
calculation {str} -- Calculation of the data. One of CALCULATION choices
|
207
|
+
"""
|
208
|
+
enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
|
209
|
+
|
210
|
+
# convert dataframe to a list of records
|
211
|
+
return enrollments.to_dict(orient='records')
|
212
|
+
|
213
|
+
def construct_enrollments_over_time_csv(self, enrollments_df, start_date, end_date, granularity, calculation):
|
214
|
+
"""
|
215
|
+
Construct enrollments over time CSV.
|
216
|
+
|
217
|
+
Arguments:
|
218
|
+
enrollments_df {DataFrame} -- DataFrame of enrollments
|
219
|
+
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
220
|
+
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
221
|
+
granularity {str} -- Granularity of the data. One of GRANULARITY choices
|
222
|
+
calculation {str} -- Calculation of the data. One of CALCULATION choices
|
223
|
+
"""
|
224
|
+
enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
|
225
|
+
|
226
|
+
enrollments = enrollments.pivot(
|
227
|
+
index="enterprise_enrollment_date", columns="enroll_type", values="count"
|
228
|
+
)
|
229
|
+
|
230
|
+
filename = f"Enrollment Timeseries, {start_date} - {end_date} ({granularity} {calculation}).csv"
|
231
|
+
return self.construct_csv_response(enrollments, filename)
|
232
|
+
|
233
|
+
def top_courses_by_enrollments_common(self, enrollments_df, start_date, end_date, group_by_columns, columns):
|
234
|
+
"""
|
235
|
+
Common method for constructing top courses by enrollments data.
|
236
|
+
|
237
|
+
Arguments:
|
238
|
+
enrollments_df {DataFrame} -- DataFrame of enrollments
|
239
|
+
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
240
|
+
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
241
|
+
group_by_columns {list} -- List of columns to group by
|
242
|
+
columns {list} -- List of column for the final result
|
243
|
+
"""
|
244
|
+
# filter enrollments by date
|
245
|
+
enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
|
246
|
+
|
247
|
+
courses = list(
|
248
|
+
enrollments.groupby(["course_key"]).size().sort_values(ascending=False)[:10].index
|
249
|
+
)
|
250
|
+
|
251
|
+
enrollments = (
|
252
|
+
enrollments[enrollments.course_key.isin(courses)]
|
253
|
+
.groupby(group_by_columns)
|
254
|
+
.size()
|
255
|
+
.reset_index()
|
256
|
+
)
|
257
|
+
enrollments.columns = columns
|
258
|
+
|
259
|
+
return enrollments
|
260
|
+
|
261
|
+
def construct_top_courses_by_enrollments(self, enrollments_df, start_date, end_date):
|
262
|
+
"""
|
263
|
+
Construct top courses by enrollments data.
|
264
|
+
|
265
|
+
Arguments:
|
266
|
+
enrollments_df {DataFrame} -- DataFrame of enrollments
|
267
|
+
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
268
|
+
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
269
|
+
"""
|
270
|
+
group_by_columns = ["course_key", "enroll_type"]
|
271
|
+
columns = ["course_key", "enroll_type", "count"]
|
272
|
+
enrollments = self.top_courses_by_enrollments_common(
|
273
|
+
enrollments_df,
|
274
|
+
start_date,
|
275
|
+
end_date,
|
276
|
+
group_by_columns,
|
277
|
+
columns
|
278
|
+
)
|
279
|
+
|
280
|
+
# convert dataframe to a list of records
|
281
|
+
return enrollments.to_dict(orient='records')
|
282
|
+
|
283
|
+
def construct_top_courses_by_enrollments_csv(self, enrollments_df, start_date, end_date):
|
284
|
+
"""
|
285
|
+
Construct top courses by enrollments CSV.
|
286
|
+
|
287
|
+
Arguments:
|
288
|
+
enrollments_df {DataFrame} -- DataFrame of enrollments
|
289
|
+
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
290
|
+
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
291
|
+
"""
|
292
|
+
group_by_columns = ["course_key", "course_title", "enroll_type"]
|
293
|
+
columns = ["course_key", "course_title", "enroll_type", "count"]
|
294
|
+
enrollments = self.top_courses_by_enrollments_common(
|
295
|
+
enrollments_df,
|
296
|
+
start_date,
|
297
|
+
end_date,
|
298
|
+
group_by_columns,
|
299
|
+
columns
|
300
|
+
)
|
301
|
+
|
302
|
+
enrollments = enrollments.pivot(
|
303
|
+
index=["course_key", "course_title"], columns="enroll_type", values="count"
|
304
|
+
)
|
305
|
+
|
306
|
+
filename = f"Top 10 Courses, {start_date} - {end_date}.csv"
|
307
|
+
return self.construct_csv_response(enrollments, filename)
|
308
|
+
|
309
|
+
def top_subjects_by_enrollments_common(self, enrollments_df, start_date, end_date):
|
310
|
+
"""
|
311
|
+
Common method for constructing top subjects by enrollments data.
|
312
|
+
|
313
|
+
Arguments:
|
314
|
+
enrollments_df {DataFrame} -- DataFrame of enrollments
|
315
|
+
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
316
|
+
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
317
|
+
"""
|
318
|
+
# filter enrollments by date
|
319
|
+
enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
|
320
|
+
|
321
|
+
subjects = list(
|
322
|
+
enrollments.groupby(["course_subject"]).size().sort_values(ascending=False)[:10].index
|
323
|
+
)
|
324
|
+
|
325
|
+
enrollments = (
|
326
|
+
enrollments[enrollments.course_subject.isin(subjects)]
|
327
|
+
.groupby(["course_subject", "enroll_type"])
|
328
|
+
.size()
|
329
|
+
.reset_index()
|
330
|
+
)
|
331
|
+
enrollments.columns = ["course_subject", "enroll_type", "count"]
|
332
|
+
|
333
|
+
return enrollments
|
334
|
+
|
335
|
+
def construct_top_subjects_by_enrollments(self, enrollments_df, start_date, end_date):
|
336
|
+
"""
|
337
|
+
Construct top subjects by enrollments data.
|
338
|
+
|
339
|
+
Arguments:
|
340
|
+
enrollments_df {DataFrame} -- DataFrame of enrollments
|
341
|
+
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
342
|
+
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
343
|
+
"""
|
344
|
+
enrollments = self.top_subjects_by_enrollments_common(enrollments_df, start_date, end_date)
|
345
|
+
# convert dataframe to a list of records
|
346
|
+
return enrollments.to_dict(orient='records')
|
347
|
+
|
348
|
+
def construct_top_subjects_by_enrollments_csv(self, enrollments_df, start_date, end_date):
|
349
|
+
"""
|
350
|
+
Construct top subjects by enrollments CSV.
|
351
|
+
|
352
|
+
Arguments:
|
353
|
+
enrollments_df {DataFrame} -- DataFrame of enrollments
|
354
|
+
start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
|
355
|
+
end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
|
356
|
+
"""
|
357
|
+
enrollments = self.top_subjects_by_enrollments_common(enrollments_df, start_date, end_date)
|
358
|
+
enrollments = enrollments.pivot(index="course_subject", columns="enroll_type", values="count")
|
359
|
+
|
360
|
+
filename = f"Top 10 Subjects by Enrollment, {start_date} - {end_date}.csv"
|
361
|
+
return self.construct_csv_response(enrollments, filename)
|
362
|
+
|
363
|
+
def construct_csv_response(self, enrollments, filename):
|
364
|
+
"""
|
365
|
+
Construct CSV response.
|
366
|
+
|
367
|
+
Arguments:
|
368
|
+
enrollments {DataFrame} -- DataFrame of enrollments
|
369
|
+
filename {str} -- Filename for the CSV
|
370
|
+
"""
|
371
|
+
response = HttpResponse(content_type='text/csv')
|
372
|
+
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
373
|
+
enrollments.to_csv(path_or_buf=response)
|
374
|
+
|
375
|
+
return response
|
enterprise_data/renderers.py
CHANGED
@@ -29,3 +29,17 @@ class EnrollmentsCSVRenderer(CSVStreamingRenderer):
|
|
29
29
|
'enterprise_sso_uid', 'created', 'course_api_url', 'total_learning_time_hours', 'is_subsidy',
|
30
30
|
'course_product_line', 'budget_id', 'enterprise_group_name', 'enterprise_group_uuid',
|
31
31
|
]
|
32
|
+
|
33
|
+
|
34
|
+
class IndividualEnrollmentsCSVRenderer(CSVStreamingRenderer):
|
35
|
+
"""
|
36
|
+
Custom streaming csv renderer for advance analytics individual enrollments data.
|
37
|
+
"""
|
38
|
+
|
39
|
+
header = [
|
40
|
+
'email',
|
41
|
+
'course_title',
|
42
|
+
'course_subject',
|
43
|
+
'enroll_type',
|
44
|
+
'enterprise_enrollment_date',
|
45
|
+
]
|
@@ -0,0 +1,169 @@
|
|
1
|
+
"""Mock data for enrollments"""
|
2
|
+
|
3
|
+
import pandas as pd
|
4
|
+
|
5
|
+
from enterprise_data.admin_analytics.constants import ENROLLMENT_CSV
|
6
|
+
|
7
|
+
ENROLLMENTS = [
|
8
|
+
{
|
9
|
+
"enterprise_customer_name": "Hill Ltd",
|
10
|
+
"enterprise_customer_uuid": "33ce656295e04ecfa2a77d407eb96f69",
|
11
|
+
"lms_enrollment_id": 1013,
|
12
|
+
"user_id": 8907,
|
13
|
+
"email": "rebeccanelson@example.com",
|
14
|
+
"course_key": "hEmW+tvk03",
|
15
|
+
"courserun_key": "course-v1:hEmW+tvk03+1T9889",
|
16
|
+
"course_id": "1681",
|
17
|
+
"course_subject": "business-management",
|
18
|
+
"course_title": "Re-engineered tangible approach",
|
19
|
+
"enterprise_enrollment_date": "2021-07-04",
|
20
|
+
"lms_enrollment_mode": "verified",
|
21
|
+
"enroll_type": "certificate",
|
22
|
+
"program_title": "Non-Program",
|
23
|
+
"date_certificate_awarded": "2021-08-25",
|
24
|
+
"grade_percent": 0.99,
|
25
|
+
"cert_awarded": 1,
|
26
|
+
"date_certificate_created_raw": "2021-08-25",
|
27
|
+
"passed_date_raw": "2021-08-25",
|
28
|
+
"passed_date": "2021-08-25",
|
29
|
+
"has_passed": 1,
|
30
|
+
},
|
31
|
+
{
|
32
|
+
"enterprise_customer_name": "Hill Ltd",
|
33
|
+
"enterprise_customer_uuid": "33ce656295e04ecfa2a77d407eb96f69",
|
34
|
+
"lms_enrollment_id": 9172,
|
35
|
+
"user_id": 8369,
|
36
|
+
"email": "taylorjames@example.com",
|
37
|
+
"course_key": "hEmW+tvk03",
|
38
|
+
"courserun_key": "course-v1:hEmW+tvk03+1T9889",
|
39
|
+
"course_id": "1681",
|
40
|
+
"course_subject": "business-management",
|
41
|
+
"course_title": "Re-engineered tangible approach",
|
42
|
+
"enterprise_enrollment_date": "2021-07-03",
|
43
|
+
"lms_enrollment_mode": "verified",
|
44
|
+
"enroll_type": "certificate",
|
45
|
+
"program_title": "Non-Program",
|
46
|
+
"date_certificate_awarded": "2021-09-01",
|
47
|
+
"grade_percent": 0.93,
|
48
|
+
"cert_awarded": 1,
|
49
|
+
"date_certificate_created_raw": "2021-09-01",
|
50
|
+
"passed_date_raw": "2021-09-01",
|
51
|
+
"passed_date": "2021-09-01",
|
52
|
+
"has_passed": 1,
|
53
|
+
},
|
54
|
+
{
|
55
|
+
"enterprise_customer_name": "Hill Ltd",
|
56
|
+
"enterprise_customer_uuid": "33ce656295e04ecfa2a77d407eb96f69",
|
57
|
+
"lms_enrollment_id": 9552,
|
58
|
+
"user_id": 8719,
|
59
|
+
"email": "ssmith@example.com",
|
60
|
+
"course_key": "qZJC+KFX86",
|
61
|
+
"courserun_key": "course-v1:qZJC+KFX86+1T8918",
|
62
|
+
"course_id": "1725",
|
63
|
+
"course_subject": "medicine",
|
64
|
+
"course_title": "Secured static capability",
|
65
|
+
"enterprise_enrollment_date": "2021-05-11",
|
66
|
+
"lms_enrollment_mode": "verified",
|
67
|
+
"enroll_type": "certificate",
|
68
|
+
"program_title": "Non-Program",
|
69
|
+
"date_certificate_awarded": None,
|
70
|
+
"grade_percent": 0.0,
|
71
|
+
"cert_awarded": 0,
|
72
|
+
"date_certificate_created_raw": None,
|
73
|
+
"passed_date_raw": None,
|
74
|
+
"passed_date": None,
|
75
|
+
"has_passed": 0,
|
76
|
+
},
|
77
|
+
{
|
78
|
+
"enterprise_customer_name": "Hill Ltd",
|
79
|
+
"enterprise_customer_uuid": "33ce656295e04ecfa2a77d407eb96f69",
|
80
|
+
"lms_enrollment_id": 3436,
|
81
|
+
"user_id": 3125,
|
82
|
+
"email": "kathleenmartin@example.com",
|
83
|
+
"course_key": "QWXx+Jqz64",
|
84
|
+
"courserun_key": "course-v1:QWXx+Jqz64+1T9449",
|
85
|
+
"course_id": "4878",
|
86
|
+
"course_subject": "social-sciences",
|
87
|
+
"course_title": "Horizontal solution-oriented hub",
|
88
|
+
"enterprise_enrollment_date": "2020-04-03",
|
89
|
+
"lms_enrollment_mode": "verified",
|
90
|
+
"enroll_type": "certificate",
|
91
|
+
"program_title": "Non-Program",
|
92
|
+
"date_certificate_awarded": None,
|
93
|
+
"grade_percent": 0.0,
|
94
|
+
"cert_awarded": 0,
|
95
|
+
"date_certificate_created_raw": None,
|
96
|
+
"passed_date_raw": None,
|
97
|
+
"passed_date": None,
|
98
|
+
"has_passed": 0,
|
99
|
+
},
|
100
|
+
{
|
101
|
+
"enterprise_customer_name": "Hill Ltd",
|
102
|
+
"enterprise_customer_uuid": "33ce656295e04ecfa2a77d407eb96f69",
|
103
|
+
"lms_enrollment_id": 5934,
|
104
|
+
"user_id": 4853,
|
105
|
+
"email": "amber79@example.com",
|
106
|
+
"course_key": "NOGk+UVD31",
|
107
|
+
"courserun_key": "course-v1:NOGk+UVD31+1T4956",
|
108
|
+
"course_id": "4141",
|
109
|
+
"course_subject": "communication",
|
110
|
+
"course_title": "Streamlined zero-defect attitude",
|
111
|
+
"enterprise_enrollment_date": "2020-04-08",
|
112
|
+
"lms_enrollment_mode": "verified",
|
113
|
+
"enroll_type": "certificate",
|
114
|
+
"program_title": "Non-Program",
|
115
|
+
"date_certificate_awarded": None,
|
116
|
+
"grade_percent": 0.0,
|
117
|
+
"cert_awarded": 0,
|
118
|
+
"date_certificate_created_raw": None,
|
119
|
+
"passed_date_raw": None,
|
120
|
+
"passed_date": None,
|
121
|
+
"has_passed": 0,
|
122
|
+
},
|
123
|
+
]
|
124
|
+
|
125
|
+
ENROLLMENT_STATS_CSVS = {
|
126
|
+
ENROLLMENT_CSV.ENROLLMENTS_OVER_TIME.value: (
|
127
|
+
b'enterprise_enrollment_date,certificate\n'
|
128
|
+
b'2020-04-03,1\n'
|
129
|
+
b'2020-04-08,1\n'
|
130
|
+
b'2021-05-11,1\n'
|
131
|
+
b'2021-07-03,1\n'
|
132
|
+
b'2021-07-04,1\n'
|
133
|
+
),
|
134
|
+
ENROLLMENT_CSV.TOP_COURSES_BY_ENROLLMENTS.value: (
|
135
|
+
b'course_key,course_title,certificate\n'
|
136
|
+
b'NOGk+UVD31,Streamlined zero-defect attitude,1\n'
|
137
|
+
b'QWXx+Jqz64,Horizontal solution-oriented hub,1\n'
|
138
|
+
b'hEmW+tvk03,Re-engineered tangible approach,2\n'
|
139
|
+
b'qZJC+KFX86,Secured static capability,1\n'
|
140
|
+
),
|
141
|
+
ENROLLMENT_CSV.TOP_SUBJECTS_BY_ENROLLMENTS.value: (
|
142
|
+
b'course_subject,certificate\nbusiness-management,2\ncommunication,1\nmedicine,1\nsocial-sciences,1\n'
|
143
|
+
)
|
144
|
+
}
|
145
|
+
|
146
|
+
|
147
|
+
def enrollments_dataframe():
|
148
|
+
"""Return a DataFrame of enrollments."""
|
149
|
+
enrollments = pd.DataFrame(ENROLLMENTS)
|
150
|
+
|
151
|
+
enrollments['enterprise_enrollment_date'] = enrollments['enterprise_enrollment_date'].astype('datetime64[ns]')
|
152
|
+
enrollments['date_certificate_awarded'] = enrollments['date_certificate_awarded'].astype('datetime64[ns]')
|
153
|
+
enrollments['date_certificate_created_raw'] = enrollments['date_certificate_created_raw'].astype('datetime64[ns]')
|
154
|
+
enrollments['passed_date_raw'] = enrollments['passed_date_raw'].astype('datetime64[ns]')
|
155
|
+
enrollments['passed_date'] = enrollments['passed_date'].astype('datetime64[ns]')
|
156
|
+
|
157
|
+
return enrollments
|
158
|
+
|
159
|
+
|
160
|
+
def enrollments_csv_content():
|
161
|
+
"""Return the CSV content of enrollments."""
|
162
|
+
return (
|
163
|
+
b'email,course_title,course_subject,enroll_type,enterprise_enrollment_date\r\n'
|
164
|
+
b'rebeccanelson@example.com,Re-engineered tangible approach,business-management,certificate,2021-07-04\r\n'
|
165
|
+
b'taylorjames@example.com,Re-engineered tangible approach,business-management,certificate,2021-07-03\r\n'
|
166
|
+
b'ssmith@example.com,Secured static capability,medicine,certificate,2021-05-11\r\n'
|
167
|
+
b'amber79@example.com,Streamlined zero-defect attitude,communication,certificate,2020-04-08\r\n'
|
168
|
+
b'kathleenmartin@example.com,Horizontal solution-oriented hub,social-sciences,certificate,2020-04-03\r\n'
|
169
|
+
)
|