edx-enterprise-data 8.8.0__py3-none-any.whl → 8.8.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.8.0.dist-info → edx_enterprise_data-8.8.1.dist-info}/METADATA +1 -1
- {edx_enterprise_data-8.8.0.dist-info → edx_enterprise_data-8.8.1.dist-info}/RECORD +14 -14
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/data_loaders.py +7 -3
- enterprise_data/admin_analytics/utils.py +6 -0
- enterprise_data/api/v1/views/analytics_enrollments.py +60 -37
- enterprise_data/api/v1/views/analytics_leaderboard.py +23 -2
- enterprise_data/api/v1/views/enterprise_admin.py +20 -19
- enterprise_data/api/v1/views/enterprise_completions.py +48 -27
- enterprise_data/tests/admin_analytics/mock_analytics_data.py +18 -8
- enterprise_data/utils.py +16 -0
- {edx_enterprise_data-8.8.0.dist-info → edx_enterprise_data-8.8.1.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-8.8.0.dist-info → edx_enterprise_data-8.8.1.dist-info}/WHEEL +0 -0
- {edx_enterprise_data-8.8.0.dist-info → edx_enterprise_data-8.8.1.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
enterprise_data/__init__.py,sha256=
|
1
|
+
enterprise_data/__init__.py,sha256=SFfjo0qp4b4IjWzit7qM4Eib5E1or2vxpohALjmvTNM,123
|
2
2
|
enterprise_data/apps.py,sha256=aF6hZwDfI2oWj95tUTm_2ikHueQj-jLj-u0GrgzpsQI,414
|
3
3
|
enterprise_data/clients.py,sha256=GvQupy5TVYfO_IKC3yzXSAgNP54r-PtIjidM5ws9Iks,3947
|
4
4
|
enterprise_data/constants.py,sha256=uCKjfpdlMYFZJsAj3n9RMw4Cmg5_6s3NuwocO-fch3s,238
|
@@ -8,13 +8,13 @@ enterprise_data/paginators.py,sha256=YPrC5TeXFt-ymenT2H8H2nCbDCnAzJQlH9kFPElRxWE
|
|
8
8
|
enterprise_data/renderers.py,sha256=9gIzavWspZTk4vDfVKXJtdn0tSZ2xNgkF-Akf7AWIDM,2389
|
9
9
|
enterprise_data/signals.py,sha256=8eqNPnlvmfsKf19lGWv5xTIuBgQIqR8EZSp9UYzC8Rc,1024
|
10
10
|
enterprise_data/urls.py,sha256=bqtKF5OEWEwrNmHG3os-pZNuNsmjlhxEqp7yM4TbPf4,243
|
11
|
-
enterprise_data/utils.py,sha256=
|
11
|
+
enterprise_data/utils.py,sha256=KykylyK9KTauj239-_V5w5iQHQBhFF3iuY3Df7p6Q5E,3017
|
12
12
|
enterprise_data/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
13
|
enterprise_data/admin_analytics/completions_utils.py,sha256=kGmLy7x6aD0coNYgzLa5XzJypLkGTT5clDHLSH_QFDE,9442
|
14
14
|
enterprise_data/admin_analytics/constants.py,sha256=aHDgTHdsjbKNpgtNLDsl4giqhhrRkCGi72ysGIEk0Ao,817
|
15
|
-
enterprise_data/admin_analytics/data_loaders.py,sha256=
|
15
|
+
enterprise_data/admin_analytics/data_loaders.py,sha256=YpSyygATVFtItcWlkIHvjsX5Lh1qMw6onDK-ZHP_AUw,5586
|
16
16
|
enterprise_data/admin_analytics/database.py,sha256=mNS_9xE5h6O7oMMzr6kr6LDTTSNvKzo8vaM-YG8tOd8,1312
|
17
|
-
enterprise_data/admin_analytics/utils.py,sha256=
|
17
|
+
enterprise_data/admin_analytics/utils.py,sha256=CQuTlg36AALJiopp4us-JN8oTXsw-jDXSJenbphLDME,12270
|
18
18
|
enterprise_data/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
19
|
enterprise_data/api/urls.py,sha256=POqc_KATHdnpMf9zHtpO46pKD5KAlAExtx7G6iylLcU,273
|
20
20
|
enterprise_data/api/v0/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
|
@@ -26,11 +26,11 @@ enterprise_data/api/v1/paginators.py,sha256=f0xsilLaU94jSBltJk46tR1rLEIt7YrqSzMA
|
|
26
26
|
enterprise_data/api/v1/serializers.py,sha256=9F2LGa8IKvglgeYNHw3Q0eEZUWknwHZMNZOdpDviEo4,12327
|
27
27
|
enterprise_data/api/v1/urls.py,sha256=xFsBf3TTsdblFAiHq1Bj3h82Ye1PS3cgqLC0pIso2js,3504
|
28
28
|
enterprise_data/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
29
|
-
enterprise_data/api/v1/views/analytics_enrollments.py,sha256=
|
30
|
-
enterprise_data/api/v1/views/analytics_leaderboard.py,sha256=
|
29
|
+
enterprise_data/api/v1/views/analytics_enrollments.py,sha256=om8i5HTDxDMSny1fy49Wh38LdYLLz-k-oFQJqlQLOxg,16812
|
30
|
+
enterprise_data/api/v1/views/analytics_leaderboard.py,sha256=EE34fJWYp8OGbIj8oRLlgMhbSm37w98oPXn1aPIZWWE,5860
|
31
31
|
enterprise_data/api/v1/views/base.py,sha256=FTAxlz5EzvAY657wzVgzhJPFSCHHzct7IDcvm71Smt8,866
|
32
|
-
enterprise_data/api/v1/views/enterprise_admin.py,sha256=
|
33
|
-
enterprise_data/api/v1/views/enterprise_completions.py,sha256=
|
32
|
+
enterprise_data/api/v1/views/enterprise_admin.py,sha256=F34FbOz1gLwHPUAQOg3mOjZoKirbCRrjZS2xFJSUBpw,9274
|
33
|
+
enterprise_data/api/v1/views/enterprise_completions.py,sha256=OkH6eYNql6UGVU-Xjx7PzwzaHeUHfa6e4WJmkUoHGDM,8221
|
34
34
|
enterprise_data/api/v1/views/enterprise_learner.py,sha256=yABjJje3CT8I8YOhWr1_tTkdKtnGJom8eu3EFz_-0BU,18517
|
35
35
|
enterprise_data/api/v1/views/enterprise_offers.py,sha256=VifxgqTLFLVw4extYPlHcN1N_yjXcsYsAlYEnAbpb10,1266
|
36
36
|
enterprise_data/fixtures/enterprise_enrollment.json,sha256=6onPXXR29pMdTdbl_mn81sDi3Re5jkLUZz2TPMB_1IY,5786
|
@@ -104,7 +104,7 @@ enterprise_data/tests/test_models.py,sha256=MWBY-LY5TPBjZ4GlvpM-h4W-BvRKr2Rml8Bz
|
|
104
104
|
enterprise_data/tests/test_utils.py,sha256=vbmYM7DMN-lHS2p4yaa0Yd6uSGXd2qoZRDE9X3J4Sec,18385
|
105
105
|
enterprise_data/tests/test_views.py,sha256=UvDRNTxruy5zBK_KgUy2cBMbwlaTW_vkM0-TCXbQZiY,69667
|
106
106
|
enterprise_data/tests/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
107
|
-
enterprise_data/tests/admin_analytics/mock_analytics_data.py,sha256=
|
107
|
+
enterprise_data/tests/admin_analytics/mock_analytics_data.py,sha256=mLGTIP89VAQgqKtYlYnKUAKyZATzKnBtI_5aJwMmr7Q,18344
|
108
108
|
enterprise_data/tests/admin_analytics/mock_enrollments.py,sha256=LfuMo9Kn-OQD4z42G3BRuM5MXUUXXlaAMhTqfJf46XE,7266
|
109
109
|
enterprise_data/tests/admin_analytics/test_analytics_enrollments.py,sha256=UdKRkP6BNbsSo-gm0YCoddT-ReUMI1x9E6HNLSHT7pY,15177
|
110
110
|
enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py,sha256=VSEyDAHfWBJvqmx9yzd4NnPAqK3TqaKrMBWswMAdzfU,6206
|
@@ -159,8 +159,8 @@ enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=WtL-RqGgu2x5PP
|
|
159
159
|
enterprise_reporting/tests/test_utils.py,sha256=Zt_TA0LVb-B6fQGkUkAKKVlUKKnQh8jnw1US1jKe7g8,9493
|
160
160
|
enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
|
161
161
|
enterprise_reporting/tests/utils.py,sha256=xms2LM7DV3wczXEfctOK1ddel1EE0J_YSr17UzbCDy4,1401
|
162
|
-
edx_enterprise_data-8.8.
|
163
|
-
edx_enterprise_data-8.8.
|
164
|
-
edx_enterprise_data-8.8.
|
165
|
-
edx_enterprise_data-8.8.
|
166
|
-
edx_enterprise_data-8.8.
|
162
|
+
edx_enterprise_data-8.8.1.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
|
163
|
+
edx_enterprise_data-8.8.1.dist-info/METADATA,sha256=hQ_trgT7XN1RJtLpZUIF33MB0ycCnKRAf1aOKxatsWw,1569
|
164
|
+
edx_enterprise_data-8.8.1.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
|
165
|
+
edx_enterprise_data-8.8.1.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
|
166
|
+
edx_enterprise_data-8.8.1.dist-info/RECORD,,
|
enterprise_data/__init__.py
CHANGED
@@ -7,6 +7,7 @@ import pandas
|
|
7
7
|
from django.http import Http404
|
8
8
|
|
9
9
|
from enterprise_data.admin_analytics.database import run_query
|
10
|
+
from enterprise_data.utils import timer
|
10
11
|
|
11
12
|
|
12
13
|
def get_select_query(table: str, columns: list, enterprise_uuid: str) -> str:
|
@@ -65,7 +66,8 @@ def fetch_enrollment_data(enterprise_uuid: str):
|
|
65
66
|
enterprise_uuid=enterprise_uuid,
|
66
67
|
)
|
67
68
|
|
68
|
-
|
69
|
+
with timer('fetch_enrollment_data'):
|
70
|
+
results = run_query(query=query)
|
69
71
|
if not results:
|
70
72
|
raise Http404(f'No enrollment data found for enterprise {enterprise_uuid}')
|
71
73
|
|
@@ -113,7 +115,8 @@ def fetch_engagement_data(enterprise_uuid: str):
|
|
113
115
|
table='fact_enrollment_engagement_day_admin_dash', columns=columns, enterprise_uuid=enterprise_uuid
|
114
116
|
)
|
115
117
|
|
116
|
-
|
118
|
+
with timer('fetch_engagement_data'):
|
119
|
+
results = run_query(query=query)
|
117
120
|
if not results:
|
118
121
|
raise Http404(f'No engagement data found for enterprise {enterprise_uuid}')
|
119
122
|
|
@@ -171,7 +174,8 @@ def fetch_skills_data(enterprise_uuid: str):
|
|
171
174
|
table='skills_daily_rollup_admin_dash', columns=cols, enterprise_uuid=enterprise_uuid
|
172
175
|
)
|
173
176
|
|
174
|
-
|
177
|
+
with timer('fetch_skills_data'):
|
178
|
+
skills = run_query(query=query)
|
175
179
|
|
176
180
|
if not skills:
|
177
181
|
raise Http404(f'No skills data found for enterprise {enterprise_uuid}')
|
@@ -3,6 +3,7 @@ Utility functions for fetching data from the database.
|
|
3
3
|
"""
|
4
4
|
from datetime import datetime, timedelta
|
5
5
|
from enum import Enum
|
6
|
+
from logging import getLogger
|
6
7
|
|
7
8
|
from edx_django_utils.cache import TieredCache, get_cache_key
|
8
9
|
|
@@ -15,6 +16,8 @@ from enterprise_data.admin_analytics.data_loaders import (
|
|
15
16
|
)
|
16
17
|
from enterprise_data.utils import date_filter, primary_subject_truncate
|
17
18
|
|
19
|
+
LOGGER = getLogger(__name__)
|
20
|
+
|
18
21
|
|
19
22
|
class ChartType(Enum):
|
20
23
|
"""
|
@@ -144,6 +147,7 @@ def fetch_and_cache_enrollments_data(enterprise_id, cache_expiry):
|
|
144
147
|
cached_response = TieredCache.get_cached_response(cache_key)
|
145
148
|
|
146
149
|
if cached_response.is_found:
|
150
|
+
LOGGER.info(f"Enrollments data found in cache for Enterprise [{enterprise_id}]")
|
147
151
|
return cached_response.value
|
148
152
|
else:
|
149
153
|
enrollments = fetch_enrollment_data(enterprise_id)
|
@@ -171,6 +175,7 @@ def fetch_and_cache_engagements_data(enterprise_id, cache_expiry):
|
|
171
175
|
cached_response = TieredCache.get_cached_response(cache_key)
|
172
176
|
|
173
177
|
if cached_response.is_found:
|
178
|
+
LOGGER.info(f"Engagements data found in cache for Enterprise [{enterprise_id}]")
|
174
179
|
return cached_response.value
|
175
180
|
else:
|
176
181
|
engagements = fetch_engagement_data(enterprise_id)
|
@@ -198,6 +203,7 @@ def fetch_and_cache_skills_data(enterprise_id, cache_expiry):
|
|
198
203
|
cached_response = TieredCache.get_cached_response(cache_key)
|
199
204
|
|
200
205
|
if cached_response.is_found:
|
206
|
+
LOGGER.info(f"Skills data found in cache for Enterprise [{enterprise_id}]")
|
201
207
|
return cached_response.value
|
202
208
|
else:
|
203
209
|
skills = fetch_skills_data(enterprise_id)
|
@@ -1,5 +1,6 @@
|
|
1
1
|
"""Advance Analytics for Enrollments"""
|
2
2
|
from datetime import datetime
|
3
|
+
from logging import getLogger
|
3
4
|
|
4
5
|
from edx_rbac.decorators import permission_required
|
5
6
|
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
@@ -21,7 +22,9 @@ from enterprise_data.api.v1.serializers import (
|
|
21
22
|
AdvanceAnalyticsQueryParamSerializer,
|
22
23
|
)
|
23
24
|
from enterprise_data.renderers import IndividualEnrollmentsCSVRenderer
|
24
|
-
from enterprise_data.utils import date_filter
|
25
|
+
from enterprise_data.utils import date_filter, timer
|
26
|
+
|
27
|
+
LOGGER = getLogger(__name__)
|
25
28
|
|
26
29
|
|
27
30
|
class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
|
@@ -46,6 +49,13 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
|
|
46
49
|
end_date = serializer.data.get('end_date', datetime.now())
|
47
50
|
response_type = request.query_params.get('response_type', ResponseType.JSON.value)
|
48
51
|
|
52
|
+
LOGGER.info(
|
53
|
+
"Individual enrollments data requested for enterprise [%s] from [%s] to [%s]",
|
54
|
+
enterprise_uuid,
|
55
|
+
start_date,
|
56
|
+
end_date,
|
57
|
+
)
|
58
|
+
|
49
59
|
# filter enrollments by date
|
50
60
|
enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
|
51
61
|
|
@@ -62,6 +72,13 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
|
|
62
72
|
enrollments["enterprise_enrollment_date"] = enrollments["enterprise_enrollment_date"].dt.date
|
63
73
|
enrollments = enrollments.sort_values(by="enterprise_enrollment_date", ascending=False).reset_index(drop=True)
|
64
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
|
+
|
65
82
|
if response_type == ResponseType.CSV.value:
|
66
83
|
filename = f"""individual_enrollments, {start_date} - {end_date}.csv"""
|
67
84
|
return StreamingHttpResponse(
|
@@ -112,49 +129,55 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
|
|
112
129
|
response_type = serializer.data.get('response_type', ResponseType.JSON.value)
|
113
130
|
chart_type = serializer.data.get('chart_type')
|
114
131
|
|
132
|
+
# TODO: Add validation that if response_type is CSV then chart_type must be provided
|
133
|
+
|
115
134
|
if response_type == ResponseType.JSON.value:
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|
+
}
|
135
155
|
return Response(data)
|
136
156
|
|
137
157
|
if response_type == ResponseType.CSV.value:
|
138
158
|
if chart_type == EnrollmentChart.ENROLLMENTS_OVER_TIME.value:
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
+
)
|
146
167
|
elif chart_type == EnrollmentChart.TOP_COURSES_BY_ENROLLMENTS.value:
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
+
)
|
152
174
|
elif chart_type == EnrollmentChart.TOP_SUBJECTS_BY_ENROLLMENTS.value:
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
+
)
|
158
181
|
|
159
182
|
def enrollments_over_time_common(self, enrollments_df, start_date, end_date, granularity, calculation):
|
160
183
|
"""
|
@@ -1,5 +1,6 @@
|
|
1
1
|
"""Advance Analytics for Leaderboard"""
|
2
2
|
from datetime import datetime
|
3
|
+
from logging import getLogger
|
3
4
|
|
4
5
|
import numpy as np
|
5
6
|
import pandas as pd
|
@@ -21,6 +22,8 @@ from enterprise_data.api.v1.serializers import AdvanceAnalyticsQueryParamSeriali
|
|
21
22
|
from enterprise_data.renderers import LeaderboardCSVRenderer
|
22
23
|
from enterprise_data.utils import date_filter
|
23
24
|
|
25
|
+
LOGGER = getLogger(__name__)
|
26
|
+
|
24
27
|
|
25
28
|
class AdvanceAnalyticsLeaderboardView(APIView):
|
26
29
|
"""
|
@@ -46,6 +49,13 @@ class AdvanceAnalyticsLeaderboardView(APIView):
|
|
46
49
|
end_date = serializer.data.get('end_date', datetime.now())
|
47
50
|
response_type = serializer.data.get('response_type', ResponseType.JSON.value)
|
48
51
|
|
52
|
+
LOGGER.info(
|
53
|
+
"Leaderboard data requested for enterprise [%s] from [%s] to [%s]",
|
54
|
+
enterprise_uuid,
|
55
|
+
start_date,
|
56
|
+
end_date,
|
57
|
+
)
|
58
|
+
|
49
59
|
# only include learners who have passed the course
|
50
60
|
enrollments_df = enrollments_df[enrollments_df["has_passed"] == 1]
|
51
61
|
|
@@ -67,8 +77,12 @@ class AdvanceAnalyticsLeaderboardView(APIView):
|
|
67
77
|
engage["learning_time_hours"] = round(
|
68
78
|
engage["learning_time_seconds"].astype("float") / 60 / 60, 1
|
69
79
|
)
|
70
|
-
|
71
|
-
|
80
|
+
|
81
|
+
# if daily_sessions is 0, set average_session_length to 0 becuase otherwise it will be `inf`
|
82
|
+
engage["average_session_length"] = np.where(
|
83
|
+
engage["daily_sessions"] == 0,
|
84
|
+
0,
|
85
|
+
round(engage["learning_time_hours"] / engage["daily_sessions"].astype("float"), 1)
|
72
86
|
)
|
73
87
|
|
74
88
|
leaderboard_df = engage.merge(completions, on="email", how="left")
|
@@ -85,6 +99,13 @@ class AdvanceAnalyticsLeaderboardView(APIView):
|
|
85
99
|
# convert `nan` values to `None` because `nan` is not JSON serializable
|
86
100
|
leaderboard_df = leaderboard_df.replace(np.nan, None)
|
87
101
|
|
102
|
+
LOGGER.info(
|
103
|
+
"Leaderboard data prepared for enterprise [%s] from [%s] to [%s]",
|
104
|
+
enterprise_uuid,
|
105
|
+
start_date,
|
106
|
+
end_date,
|
107
|
+
)
|
108
|
+
|
88
109
|
if response_type == ResponseType.CSV.value:
|
89
110
|
filename = f"""Leaderboard, {start_date} - {end_date}.csv"""
|
90
111
|
leaderboard_df = leaderboard_df[
|
@@ -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
|
30
|
+
from enterprise_data.utils import date_filter, timer
|
31
31
|
|
32
32
|
from .base import EnterpriseViewSetMixin
|
33
33
|
|
@@ -211,24 +211,25 @@ class EnterpriseAdminAnalyticsSkillsView(APIView):
|
|
211
211
|
csv_data.to_csv(path_or_buf=response, index=False)
|
212
212
|
return response
|
213
213
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
214
|
+
with timer('skills_all_charts_data'):
|
215
|
+
top_skills = get_skills_chart_data(
|
216
|
+
chart_type=ChartType.BUBBLE,
|
217
|
+
start_date=start_date,
|
218
|
+
end_date=end_date,
|
219
|
+
skills=skills,
|
220
|
+
)
|
221
|
+
top_skills_enrollments = get_skills_chart_data(
|
222
|
+
chart_type=ChartType.TOP_SKILLS_ENROLLMENT,
|
223
|
+
start_date=start_date,
|
224
|
+
end_date=end_date,
|
225
|
+
skills=skills,
|
226
|
+
)
|
227
|
+
top_skills_by_completions = get_skills_chart_data(
|
228
|
+
chart_type=ChartType.TOP_SKILLS_COMPLETION,
|
229
|
+
start_date=start_date,
|
230
|
+
end_date=end_date,
|
231
|
+
skills=skills,
|
232
|
+
)
|
232
233
|
|
233
234
|
response_data = {
|
234
235
|
"top_skills": top_skills.to_dict(orient="records"),
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""Views for enterprise admin completions analytics."""
|
2
2
|
import datetime
|
3
3
|
from datetime import datetime, timedelta
|
4
|
+
from logging import getLogger
|
4
5
|
|
5
6
|
from edx_rbac.decorators import permission_required
|
6
7
|
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
@@ -23,7 +24,9 @@ from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_da
|
|
23
24
|
from enterprise_data.admin_analytics.utils import ChartType, fetch_and_cache_enrollments_data
|
24
25
|
from enterprise_data.api.v1 import serializers
|
25
26
|
from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
|
26
|
-
from enterprise_data.utils import date_filter
|
27
|
+
from enterprise_data.utils import date_filter, timer
|
28
|
+
|
29
|
+
LOGGER = getLogger(__name__)
|
27
30
|
|
28
31
|
|
29
32
|
class EnterrpiseAdminCompletionsStatsView(APIView):
|
@@ -67,39 +70,43 @@ class EnterrpiseAdminCompletionsStatsView(APIView):
|
|
67
70
|
csv_data = {}
|
68
71
|
|
69
72
|
if chart_type == ChartType.COMPLETIONS_OVER_TIME.value:
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
73
|
+
with timer('completions_over_time_csv_data'):
|
74
|
+
csv_data = get_csv_data_for_completions_over_time(
|
75
|
+
start_date=start_date,
|
76
|
+
end_date=end_date,
|
77
|
+
enrollments=enrollments.copy(),
|
78
|
+
date_agg=serializer.data.get('granularity', Granularity.DAILY.value),
|
79
|
+
calc=serializer.data.get('calculation', Calculation.TOTAL.value),
|
80
|
+
)
|
77
81
|
elif chart_type == ChartType.TOP_COURSES_BY_COMPLETIONS.value:
|
78
|
-
|
79
|
-
|
80
|
-
|
82
|
+
with timer('top_courses_by_completions_csv_data'):
|
83
|
+
csv_data = get_csv_data_for_top_courses_by_completions(
|
84
|
+
start_date=start_date, end_date=end_date, enrollments=enrollments.copy()
|
85
|
+
)
|
81
86
|
elif chart_type == ChartType.TOP_SUBJECTS_BY_COMPLETIONS.value:
|
82
|
-
|
83
|
-
|
84
|
-
|
87
|
+
with timer('top_subjects_by_completions_csv_data'):
|
88
|
+
csv_data = get_csv_data_for_top_subjects_by_completions(
|
89
|
+
start_date=start_date, end_date=end_date, enrollments=enrollments.copy()
|
90
|
+
)
|
85
91
|
filename = csv_data['filename']
|
86
92
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
87
93
|
csv_data['data'].to_csv(path_or_buf=response)
|
88
94
|
return response
|
89
95
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
96
|
+
with timer('completions_all_charts_data'):
|
97
|
+
completions_over_time = get_completions_over_time(
|
98
|
+
start_date=start_date,
|
99
|
+
end_date=end_date,
|
100
|
+
dff=enrollments.copy(),
|
101
|
+
date_agg=serializer.data.get('granularity', Granularity.DAILY.value),
|
102
|
+
calc=serializer.data.get('calculation', Calculation.TOTAL.value),
|
103
|
+
)
|
104
|
+
top_courses_by_completions = get_top_courses_by_completions(
|
105
|
+
start_date=start_date, end_date=end_date, dff=enrollments.copy()
|
106
|
+
)
|
107
|
+
top_subjects_by_completions = get_top_subjects_by_completions(
|
108
|
+
start_date=start_date, end_date=end_date, dff=enrollments.copy()
|
109
|
+
)
|
103
110
|
|
104
111
|
return Response(
|
105
112
|
data={
|
@@ -153,6 +160,13 @@ class EnterrpiseAdminCompletionsView(APIView):
|
|
153
160
|
)
|
154
161
|
end_date = serializer.data.get('end_date', datetime.now())
|
155
162
|
|
163
|
+
LOGGER.info(
|
164
|
+
"Completions data requested for enterprise [%s] from [%s] to [%s]",
|
165
|
+
enterprise_id,
|
166
|
+
start_date,
|
167
|
+
end_date,
|
168
|
+
)
|
169
|
+
|
156
170
|
dff = enrollments[enrollments['has_passed'] == 1]
|
157
171
|
|
158
172
|
# Date filtering
|
@@ -162,6 +176,13 @@ class EnterrpiseAdminCompletionsView(APIView):
|
|
162
176
|
dff['passed_date'] = dff['passed_date'].dt.date
|
163
177
|
dff = dff.sort_values(by="passed_date", ascending=False).reset_index(drop=True)
|
164
178
|
|
179
|
+
LOGGER.info(
|
180
|
+
"Completions data prepared for enterprise [%s] from [%s] to [%s]",
|
181
|
+
enterprise_id,
|
182
|
+
start_date,
|
183
|
+
end_date,
|
184
|
+
)
|
185
|
+
|
165
186
|
if serializer.data.get('response_type') == 'csv':
|
166
187
|
response = HttpResponse(content_type='text/csv')
|
167
188
|
filename = f"Individual Completions, {start_date} - {end_date}.csv"
|
@@ -374,14 +374,24 @@ def enrollments_csv_content():
|
|
374
374
|
|
375
375
|
def leaderboard_csv_content():
|
376
376
|
"""Return the CSV content of leaderboard."""
|
377
|
+
# return (
|
378
|
+
# b'email,learning_time_hours,daily_sessions,average_session_length,course_completions\r\n'
|
379
|
+
# b'paul77@example.org,4.4,1,4.4,\r\nseth57@example.org,2.7,1,2.7,\r\n'
|
380
|
+
# b'weaverpatricia@example.net,2.6,1,2.6,\r\nwebertodd@example.com,1.5,1,1.5,\r\n'
|
381
|
+
# b'yferguson@example.net,1.3,1,1.3,\r\nyallison@example.org,1.2,1,1.2,\r\n'
|
382
|
+
# b'padillamichelle@example.org,1.0,1,1.0,\r\ncaseyjohnny@example.com,0.0,0,,\r\n'
|
383
|
+
# b'crystal86@example.net,0.0,0,,\r\ngraceperez@example.com,0.0,0,,\r\n'
|
384
|
+
# b'mackwilliam@example.com,0.0,0,,\r\nsamanthaclarke@example.org,0.0,0,,\r\n'
|
385
|
+
# )
|
386
|
+
|
377
387
|
return (
|
378
388
|
b'email,learning_time_hours,daily_sessions,average_session_length,course_completions\r\n'
|
379
389
|
b'paul77@example.org,4.4,1,4.4,\r\nseth57@example.org,2.7,1,2.7,\r\n'
|
380
390
|
b'weaverpatricia@example.net,2.6,1,2.6,\r\nwebertodd@example.com,1.5,1,1.5,\r\n'
|
381
391
|
b'yferguson@example.net,1.3,1,1.3,\r\nyallison@example.org,1.2,1,1.2,\r\n'
|
382
|
-
b'padillamichelle@example.org,1.0,1,1.0,\r\ncaseyjohnny@example.com,0.0,0
|
383
|
-
b'crystal86@example.net,0.0,0
|
384
|
-
b'mackwilliam@example.com,0.0,0
|
392
|
+
b'padillamichelle@example.org,1.0,1,1.0,\r\ncaseyjohnny@example.com,0.0,0,0.0,\r\n'
|
393
|
+
b'crystal86@example.net,0.0,0,0.0,\r\ngraceperez@example.com,0.0,0,0.0,\r\n'
|
394
|
+
b'mackwilliam@example.com,0.0,0,0.0,\r\nsamanthaclarke@example.org,0.0,0,0.0,\r\n'
|
385
395
|
)
|
386
396
|
|
387
397
|
|
@@ -447,7 +457,7 @@ LEADERBOARD_RESPONSE = [
|
|
447
457
|
"daily_sessions": 0,
|
448
458
|
"learning_time_seconds": 0,
|
449
459
|
"learning_time_hours": 0.0,
|
450
|
-
"average_session_length":
|
460
|
+
"average_session_length": 0.0,
|
451
461
|
"course_completions": None,
|
452
462
|
},
|
453
463
|
{
|
@@ -455,7 +465,7 @@ LEADERBOARD_RESPONSE = [
|
|
455
465
|
"daily_sessions": 0,
|
456
466
|
"learning_time_seconds": 0,
|
457
467
|
"learning_time_hours": 0.0,
|
458
|
-
"average_session_length":
|
468
|
+
"average_session_length": 0.0,
|
459
469
|
"course_completions": None,
|
460
470
|
},
|
461
471
|
{
|
@@ -463,7 +473,7 @@ LEADERBOARD_RESPONSE = [
|
|
463
473
|
"daily_sessions": 0,
|
464
474
|
"learning_time_seconds": 21,
|
465
475
|
"learning_time_hours": 0.0,
|
466
|
-
"average_session_length":
|
476
|
+
"average_session_length": 0.0,
|
467
477
|
"course_completions": None,
|
468
478
|
},
|
469
479
|
{
|
@@ -471,7 +481,7 @@ LEADERBOARD_RESPONSE = [
|
|
471
481
|
"daily_sessions": 0,
|
472
482
|
"learning_time_seconds": 0,
|
473
483
|
"learning_time_hours": 0.0,
|
474
|
-
"average_session_length":
|
484
|
+
"average_session_length": 0.0,
|
475
485
|
"course_completions": None,
|
476
486
|
},
|
477
487
|
{
|
@@ -479,7 +489,7 @@ LEADERBOARD_RESPONSE = [
|
|
479
489
|
"daily_sessions": 0,
|
480
490
|
"learning_time_seconds": 29,
|
481
491
|
"learning_time_hours": 0.0,
|
482
|
-
"average_session_length":
|
492
|
+
"average_session_length": 0.0,
|
483
493
|
"course_completions": None,
|
484
494
|
},
|
485
495
|
]
|
enterprise_data/utils.py
CHANGED
@@ -4,6 +4,7 @@ Utility functions for Enterprise Data app.
|
|
4
4
|
import hashlib
|
5
5
|
import random
|
6
6
|
import time
|
7
|
+
from contextlib import contextmanager
|
7
8
|
from datetime import timedelta
|
8
9
|
from functools import wraps
|
9
10
|
from logging import getLogger
|
@@ -68,6 +69,21 @@ def timeit(func):
|
|
68
69
|
return wrapper
|
69
70
|
|
70
71
|
|
72
|
+
@contextmanager
|
73
|
+
def timer(prefix):
|
74
|
+
"""
|
75
|
+
Context manager to measure the time taken by a block of code.
|
76
|
+
|
77
|
+
Arguments:
|
78
|
+
prefix (str): The prefix to print in the log.
|
79
|
+
"""
|
80
|
+
start = time.time()
|
81
|
+
yield
|
82
|
+
end = time.time()
|
83
|
+
difference = end - start
|
84
|
+
print(f"TIMER:: {prefix} took {difference:.20f} seconds")
|
85
|
+
|
86
|
+
|
71
87
|
def date_filter(start, end, data_frame, date_column):
|
72
88
|
"""
|
73
89
|
Filter a pandas DataFrame by date range.
|
File without changes
|
File without changes
|
File without changes
|