edx-enterprise-data 8.3.1__py3-none-any.whl → 8.4.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.3.1.dist-info → edx_enterprise_data-8.4.0.dist-info}/METADATA +1 -1
- {edx_enterprise_data-8.3.1.dist-info → edx_enterprise_data-8.4.0.dist-info}/RECORD +15 -15
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/data_loaders.py +45 -0
- enterprise_data/admin_analytics/utils.py +177 -1
- enterprise_data/api/v1/urls.py +5 -0
- enterprise_data/api/v1/views/enterprise_admin.py +154 -30
- enterprise_data/tests/admin_analytics/test_data_loaders.py +30 -1
- enterprise_data/tests/admin_analytics/test_utils.py +139 -0
- enterprise_data/tests/api/v1/views/test_enterprise_admin.py +49 -0
- enterprise_data/tests/test_utils.py +28 -0
- enterprise_data/utils.py +16 -0
- {edx_enterprise_data-8.3.1.dist-info → edx_enterprise_data-8.4.0.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-8.3.1.dist-info → edx_enterprise_data-8.4.0.dist-info}/WHEEL +0 -0
- {edx_enterprise_data-8.3.1.dist-info → edx_enterprise_data-8.4.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
enterprise_data/__init__.py,sha256=
|
1
|
+
enterprise_data/__init__.py,sha256=V3f8VWSTHKMmk7VgKNYsFMadFosPMVNgWxHub9YqP9c,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,11 +8,11 @@ enterprise_data/paginators.py,sha256=YPrC5TeXFt-ymenT2H8H2nCbDCnAzJQlH9kFPElRxWE
|
|
8
8
|
enterprise_data/renderers.py,sha256=WVt0qy9Ippdnl404Zsq-MruB9oQfY6h87ZzpScYBeaw,1770
|
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=kNO4nW_GBpBiIBlVUkCb4Xo0k1oVshT8nDOBP5eWoV8,2643
|
12
12
|
enterprise_data/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
-
enterprise_data/admin_analytics/data_loaders.py,sha256=
|
13
|
+
enterprise_data/admin_analytics/data_loaders.py,sha256=x1XNYdtJV1G9cv0SeBZqYitRV8-GlJXtEZ2cc2OJU7M,5415
|
14
14
|
enterprise_data/admin_analytics/database.py,sha256=mNS_9xE5h6O7oMMzr6kr6LDTTSNvKzo8vaM-YG8tOd8,1312
|
15
|
-
enterprise_data/admin_analytics/utils.py,sha256=
|
15
|
+
enterprise_data/admin_analytics/utils.py,sha256=J6E9JhZedWkJQ9TJJF_z95sL5Oka1_x7gs_st4Z35Yo,8420
|
16
16
|
enterprise_data/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
17
|
enterprise_data/api/urls.py,sha256=POqc_KATHdnpMf9zHtpO46pKD5KAlAExtx7G6iylLcU,273
|
18
18
|
enterprise_data/api/v0/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
|
@@ -21,10 +21,10 @@ enterprise_data/api/v0/urls.py,sha256=vzJjqIo_S3AXWs9Us8XTaJc3FnxLbYzAkmLyuDQqum
|
|
21
21
|
enterprise_data/api/v0/views.py,sha256=4RslZ4NZOU-844bnebEQ71ji2utRY7jEijqC45oQQD0,14380
|
22
22
|
enterprise_data/api/v1/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
|
23
23
|
enterprise_data/api/v1/serializers.py,sha256=AOHumabnzwK59nytoOZEWhA3f9EL_CV-bXPsYSH3LI8,8263
|
24
|
-
enterprise_data/api/v1/urls.py,sha256=
|
24
|
+
enterprise_data/api/v1/urls.py,sha256=m5B01cZuInVsJkRdUloHC45fHxSJKQMSzBSCgU2GpkA,1855
|
25
25
|
enterprise_data/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
26
26
|
enterprise_data/api/v1/views/base.py,sha256=FTAxlz5EzvAY657wzVgzhJPFSCHHzct7IDcvm71Smt8,866
|
27
|
-
enterprise_data/api/v1/views/enterprise_admin.py,sha256=
|
27
|
+
enterprise_data/api/v1/views/enterprise_admin.py,sha256=hBUj5XPyyBwpyNED62lLlV1etRK9vHRS6aYnZLTF2w0,8081
|
28
28
|
enterprise_data/api/v1/views/enterprise_learner.py,sha256=yABjJje3CT8I8YOhWr1_tTkdKtnGJom8eu3EFz_-0BU,18517
|
29
29
|
enterprise_data/api/v1/views/enterprise_offers.py,sha256=VifxgqTLFLVw4extYPlHcN1N_yjXcsYsAlYEnAbpb10,1266
|
30
30
|
enterprise_data/fixtures/enterprise_enrollment.json,sha256=6onPXXR29pMdTdbl_mn81sDi3Re5jkLUZz2TPMB_1IY,5786
|
@@ -94,11 +94,11 @@ enterprise_data/tests/mixins.py,sha256=YifptI9mtOhAWnBGyPUy4kX5OJNSDP3DvW2vb1E2t
|
|
94
94
|
enterprise_data/tests/test_clients.py,sha256=xBPHF9cgEFqNJoL4klOoYh_sVS3scZGcX0Ltc9Ghp7A,6336
|
95
95
|
enterprise_data/tests/test_filters.py,sha256=ZBbLl9Sgj5mJ7lTWoaFcEPwuxPDpIbMo2n_Fhurc0T8,7263
|
96
96
|
enterprise_data/tests/test_models.py,sha256=MWBY-LY5TPBjZ4GlvpM-h4W-BvRKr2Rml8Bzg1NPZ9M,3234
|
97
|
-
enterprise_data/tests/test_utils.py,sha256=
|
97
|
+
enterprise_data/tests/test_utils.py,sha256=vbmYM7DMN-lHS2p4yaa0Yd6uSGXd2qoZRDE9X3J4Sec,18385
|
98
98
|
enterprise_data/tests/test_views.py,sha256=UvDRNTxruy5zBK_KgUy2cBMbwlaTW_vkM0-TCXbQZiY,69667
|
99
99
|
enterprise_data/tests/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
100
|
-
enterprise_data/tests/admin_analytics/test_data_loaders.py,sha256=
|
101
|
-
enterprise_data/tests/admin_analytics/test_utils.py,sha256=
|
100
|
+
enterprise_data/tests/admin_analytics/test_data_loaders.py,sha256=o3denJ4aUS1pI5Crksl4C6m-NtCBm8ynoHBnLkf-v2U,4641
|
101
|
+
enterprise_data/tests/admin_analytics/test_utils.py,sha256=y33HXy6BDOoftdcz3qYlOYhgx7JSXDki-OLzBdTpiwA,11449
|
102
102
|
enterprise_data/tests/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
103
103
|
enterprise_data/tests/api/v0/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
104
104
|
enterprise_data/tests/api/v0/test_serializers.py,sha256=Gfty6gy6OQLN318uL1OCPhAZOqSUL50FWc0nC23VMnc,6257
|
@@ -106,7 +106,7 @@ enterprise_data/tests/api/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
|
|
106
106
|
enterprise_data/tests/api/v1/test_serializers.py,sha256=DwgEHcyOP3oqNUPB2O-NkJGeO_cYs9XJiq7791vJLZE,3682
|
107
107
|
enterprise_data/tests/api/v1/test_views.py,sha256=rLqUHfar0HdBNtz33hQxd_0qUUgr7Ku3KwQSQ1B4Ypg,15213
|
108
108
|
enterprise_data/tests/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
109
|
-
enterprise_data/tests/api/v1/views/test_enterprise_admin.py,sha256=
|
109
|
+
enterprise_data/tests/api/v1/views/test_enterprise_admin.py,sha256=7FCPpfFrv-pisJ9cxt3B-KIf-KM3a7XzQ8MweLp23wI,4783
|
110
110
|
enterprise_data_roles/__init__.py,sha256=toCpbypm2uDoWVw29_em9gPFNly8vNUS__C0b4TCqEg,112
|
111
111
|
enterprise_data_roles/admin.py,sha256=QNP0VeWE092vZzpyxOA5UJK1nNGl5e71B1J0RCwo_nU,998
|
112
112
|
enterprise_data_roles/apps.py,sha256=nKi8TyuQ5Q6WGtKs5QeXvUTc3N-YQjKhyBnm2EM3Bng,260
|
@@ -147,8 +147,8 @@ enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=WtL-RqGgu2x5PP
|
|
147
147
|
enterprise_reporting/tests/test_utils.py,sha256=Zt_TA0LVb-B6fQGkUkAKKVlUKKnQh8jnw1US1jKe7g8,9493
|
148
148
|
enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
|
149
149
|
enterprise_reporting/tests/utils.py,sha256=xms2LM7DV3wczXEfctOK1ddel1EE0J_YSr17UzbCDy4,1401
|
150
|
-
edx_enterprise_data-8.
|
151
|
-
edx_enterprise_data-8.
|
152
|
-
edx_enterprise_data-8.
|
153
|
-
edx_enterprise_data-8.
|
154
|
-
edx_enterprise_data-8.
|
150
|
+
edx_enterprise_data-8.4.0.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
|
151
|
+
edx_enterprise_data-8.4.0.dist-info/METADATA,sha256=o0b8zqSTviYu9WVM9aDEK6ozyC_PBJlKxUQJC3fNZic,1569
|
152
|
+
edx_enterprise_data-8.4.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
|
153
|
+
edx_enterprise_data-8.4.0.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
|
154
|
+
edx_enterprise_data-8.4.0.dist-info/RECORD,,
|
enterprise_data/__init__.py
CHANGED
@@ -135,3 +135,48 @@ def fetch_max_enrollment_datetime():
|
|
135
135
|
if not results:
|
136
136
|
return None
|
137
137
|
return pandas.to_datetime(results[0][0])
|
138
|
+
|
139
|
+
|
140
|
+
def fetch_skills_data(enterprise_uuid: str):
|
141
|
+
"""
|
142
|
+
Fetch skills data from the database for the given enterprise customer.
|
143
|
+
|
144
|
+
Arguments:
|
145
|
+
enterprise_uuid (str): The UUID of the enterprise customer.
|
146
|
+
|
147
|
+
Returns:
|
148
|
+
(pandas.DataFrame): The skills data.
|
149
|
+
"""
|
150
|
+
|
151
|
+
enterprise_uuid = enterprise_uuid.replace('-', '')
|
152
|
+
|
153
|
+
cols = [
|
154
|
+
'course_number',
|
155
|
+
'skill_type',
|
156
|
+
'skill_name',
|
157
|
+
'skill_url',
|
158
|
+
'confidence',
|
159
|
+
'skill_rank',
|
160
|
+
'course_title',
|
161
|
+
'course_key',
|
162
|
+
'level_type',
|
163
|
+
'primary_subject_name',
|
164
|
+
'date',
|
165
|
+
'enterprise_customer_uuid',
|
166
|
+
'enterprise_customer_name',
|
167
|
+
'enrolls',
|
168
|
+
'completions',
|
169
|
+
]
|
170
|
+
query = get_select_query(
|
171
|
+
table='skills_daily_rollup_admin_dash', columns=cols, enterprise_uuid=enterprise_uuid
|
172
|
+
)
|
173
|
+
|
174
|
+
skills = run_query(query=query)
|
175
|
+
|
176
|
+
if not skills:
|
177
|
+
raise Http404(f'No skills data found for enterprise {enterprise_uuid}')
|
178
|
+
|
179
|
+
skills = pandas.DataFrame(numpy.array(skills), columns=cols)
|
180
|
+
skills['date'] = skills['date'].astype('datetime64[ns]')
|
181
|
+
|
182
|
+
return skills
|
@@ -2,10 +2,21 @@
|
|
2
2
|
Utility functions for fetching data from the database.
|
3
3
|
"""
|
4
4
|
from datetime import datetime
|
5
|
+
from enum import Enum
|
5
6
|
|
6
7
|
from edx_django_utils.cache import TieredCache, get_cache_key
|
7
8
|
|
8
|
-
from enterprise_data.admin_analytics.data_loaders import fetch_engagement_data, fetch_enrollment_data
|
9
|
+
from enterprise_data.admin_analytics.data_loaders import fetch_engagement_data, fetch_enrollment_data, fetch_skills_data
|
10
|
+
from enterprise_data.utils import date_filter, primary_subject_truncate
|
11
|
+
|
12
|
+
|
13
|
+
class ChartType(Enum):
|
14
|
+
"""
|
15
|
+
Chart types.
|
16
|
+
"""
|
17
|
+
BUBBLE = 'bubble'
|
18
|
+
TOP_SKILLS_ENROLLMENT = 'top_skills_enrollment'
|
19
|
+
TOP_SKILLS_COMPLETION = 'top_skills_completion'
|
9
20
|
|
10
21
|
|
11
22
|
def get_cache_timeout(cache_expiry):
|
@@ -79,3 +90,168 @@ def fetch_and_cache_engagements_data(enterprise_id, cache_expiry):
|
|
79
90
|
cache_key, engagements, get_cache_timeout(cache_expiry)
|
80
91
|
)
|
81
92
|
return engagements
|
93
|
+
|
94
|
+
|
95
|
+
def fetch_and_cache_skills_data(enterprise_id, cache_expiry):
|
96
|
+
"""
|
97
|
+
Helper method to fetch and cache skills data.
|
98
|
+
|
99
|
+
Arguments:
|
100
|
+
enterprise_id (str): UUID of the enterprise customer in string format.
|
101
|
+
cache_expiry (datetime): Datetime object denoting the cache expiry.
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
(pandas.DataFrame): The skills data.
|
105
|
+
"""
|
106
|
+
cache_key = get_cache_key(
|
107
|
+
resource='enterprise-admin-analytics-aggregate-skills',
|
108
|
+
enterprise_customer=enterprise_id,
|
109
|
+
)
|
110
|
+
cached_response = TieredCache.get_cached_response(cache_key)
|
111
|
+
|
112
|
+
if cached_response.is_found:
|
113
|
+
return cached_response.value
|
114
|
+
else:
|
115
|
+
skills = fetch_skills_data(enterprise_id)
|
116
|
+
TieredCache.set_all_tiers(
|
117
|
+
cache_key, skills, get_cache_timeout(cache_expiry)
|
118
|
+
)
|
119
|
+
return skills
|
120
|
+
|
121
|
+
|
122
|
+
def get_skills_bubble_chart_df(skills_filtered):
|
123
|
+
""" Get the skills data for the bubble chart.
|
124
|
+
|
125
|
+
Args:
|
126
|
+
skills_filtered (list): The skills data.
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
(pandas.DataFrame): The skills data for the bubble chart.
|
130
|
+
"""
|
131
|
+
|
132
|
+
# Group by skill_name and skill_type, and aggregate enrolls and completions
|
133
|
+
skills_aggregated = (
|
134
|
+
skills_filtered.groupby(['skill_name', 'skill_type'], as_index=False)
|
135
|
+
.agg(enrolls=('enrolls', 'sum'), completions=('completions', 'sum'))
|
136
|
+
)
|
137
|
+
|
138
|
+
# Convert enrolls and completions to integers
|
139
|
+
skills_aggregated['enrolls'] = skills_aggregated['enrolls'].astype(int)
|
140
|
+
skills_aggregated['completions'] = skills_aggregated['completions'].astype(int)
|
141
|
+
|
142
|
+
# Sort the dataframe by enrolls and completions in descending order
|
143
|
+
skills_aggregated = skills_aggregated.sort_values(by=['enrolls', 'completions'], ascending=False)
|
144
|
+
|
145
|
+
return skills_aggregated
|
146
|
+
|
147
|
+
|
148
|
+
def get_top_skills_enrollment(skills_filtered):
|
149
|
+
""" Get the top skills by enrolls.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
skills_filtered (pandas.DataFrame): The skills data.
|
153
|
+
|
154
|
+
Returns:
|
155
|
+
(pandas.DataFrame): The top skills by enrolls data
|
156
|
+
"""
|
157
|
+
|
158
|
+
# Get the top 10 skills by enrolls
|
159
|
+
top_skills = (
|
160
|
+
skills_filtered.groupby('skill_name')
|
161
|
+
.enrolls.sum()
|
162
|
+
.sort_values(ascending=False)
|
163
|
+
.head(10)
|
164
|
+
.index
|
165
|
+
)
|
166
|
+
|
167
|
+
# Apply primary_subject_truncate to the primary_subject_name column
|
168
|
+
skills_filtered['primary_subject_name'] = skills_filtered['primary_subject_name'].apply(primary_subject_truncate)
|
169
|
+
|
170
|
+
# Filter data for the top skills and aggregate enrolls by skill_name and primary_subject_name
|
171
|
+
top_skills_enrollment_data = (
|
172
|
+
skills_filtered[skills_filtered.skill_name.isin(top_skills)]
|
173
|
+
.groupby(['skill_name', 'primary_subject_name'], as_index=False)
|
174
|
+
.agg(count=('enrolls', 'sum'))
|
175
|
+
)
|
176
|
+
|
177
|
+
# Sort the dataframe by primary_subject_name
|
178
|
+
top_skills_enrollment_data = top_skills_enrollment_data.sort_values(by="primary_subject_name")
|
179
|
+
|
180
|
+
return top_skills_enrollment_data
|
181
|
+
|
182
|
+
|
183
|
+
def get_top_skills_completion(skills_filtered):
|
184
|
+
""" Get the top skills by completions.
|
185
|
+
|
186
|
+
Args:
|
187
|
+
skills_filtered (pandas.DataFrame): The skills data.
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
(pandas.DataFrame): The top skills by completions
|
191
|
+
"""
|
192
|
+
|
193
|
+
# Get the top 10 skills by completions
|
194
|
+
top_skills = (
|
195
|
+
skills_filtered.groupby('skill_name')
|
196
|
+
.completions.sum()
|
197
|
+
.sort_values(ascending=False)
|
198
|
+
.head(10)
|
199
|
+
.index
|
200
|
+
)
|
201
|
+
|
202
|
+
# Apply primary_subject_truncate to the primary_subject_name column
|
203
|
+
skills_filtered['primary_subject_name'] = skills_filtered['primary_subject_name'].apply(primary_subject_truncate)
|
204
|
+
|
205
|
+
# Filter data for the top skills and aggregate completions by skill_name and primary_subject_name
|
206
|
+
top_skills_completion_data = (
|
207
|
+
skills_filtered[skills_filtered.skill_name.isin(top_skills)]
|
208
|
+
.groupby(['skill_name', 'primary_subject_name'], as_index=False)
|
209
|
+
.agg(count=('completions', 'sum'))
|
210
|
+
)
|
211
|
+
|
212
|
+
# Sort the dataframe by primary_subject_name
|
213
|
+
top_skills_completion_data = top_skills_completion_data.sort_values(by='primary_subject_name')
|
214
|
+
|
215
|
+
return top_skills_completion_data
|
216
|
+
|
217
|
+
|
218
|
+
def get_skills_chart_data(chart_type, start_date, end_date, skills):
|
219
|
+
"""
|
220
|
+
Get chart data for skill charts.
|
221
|
+
|
222
|
+
Arguments:
|
223
|
+
chart_type (ChartType): The type of chart to generate.
|
224
|
+
start_date (datetime): The start date for the date filter.
|
225
|
+
end_date (datetime): The end date for the date filter.
|
226
|
+
skills (pandas.DataFrame): The skills data.
|
227
|
+
"""
|
228
|
+
skills_filtered = date_filter(start=start_date, end=end_date, data_frame=skills.copy(), date_column='date')
|
229
|
+
if chart_type == ChartType.BUBBLE:
|
230
|
+
return get_skills_bubble_chart_df(skills_filtered=skills_filtered.copy())
|
231
|
+
elif chart_type == ChartType.TOP_SKILLS_ENROLLMENT:
|
232
|
+
return get_top_skills_enrollment(skills_filtered=skills_filtered.copy())
|
233
|
+
elif chart_type == ChartType.TOP_SKILLS_COMPLETION:
|
234
|
+
return get_top_skills_completion(skills_filtered=skills_filtered.copy())
|
235
|
+
else:
|
236
|
+
raise ValueError(f"Invalid chart type: {chart_type}")
|
237
|
+
|
238
|
+
|
239
|
+
def get_top_skills_csv_data(skills, start_date, end_date):
|
240
|
+
""" Get the top skills data for CSV download.
|
241
|
+
|
242
|
+
Args:
|
243
|
+
skills (pandas.DataFrame): The skills data.
|
244
|
+
start_date (str): The start date for the date filter.
|
245
|
+
end_date (str): The end date for the date filter.
|
246
|
+
|
247
|
+
Returns:
|
248
|
+
(pandas.DataFrame): The top skills data for CSV download.
|
249
|
+
"""
|
250
|
+
dff = get_skills_chart_data(
|
251
|
+
chart_type=ChartType.BUBBLE,
|
252
|
+
start_date=start_date,
|
253
|
+
end_date=end_date,
|
254
|
+
skills=skills.copy(),
|
255
|
+
)
|
256
|
+
dff = dff.sort_values(by='enrolls', ascending=False)
|
257
|
+
return dff
|
enterprise_data/api/v1/urls.py
CHANGED
@@ -47,6 +47,11 @@ urlpatterns = [
|
|
47
47
|
enterprise_admin_views.EnterpriseAdminAnalyticsAggregatesView.as_view(),
|
48
48
|
name='enterprise-admin-analytics-aggregates'
|
49
49
|
),
|
50
|
+
re_path(
|
51
|
+
fr'^admin/anlaytics/(?P<enterprise_id>{UUID4_REGEX})/skills/stats',
|
52
|
+
enterprise_admin_views.EnterpriseAdminAnalyticsSkillsView.as_view(),
|
53
|
+
name='enterprise-admin-analytics-skills'
|
54
|
+
),
|
50
55
|
]
|
51
56
|
|
52
57
|
urlpatterns += router.urls
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
Views for enterprise admin api v1.
|
3
3
|
"""
|
4
|
+
|
4
5
|
from datetime import datetime, timedelta
|
5
6
|
|
6
7
|
from edx_rbac.decorators import permission_required
|
@@ -9,8 +10,17 @@ from rest_framework.response import Response
|
|
9
10
|
from rest_framework.status import HTTP_200_OK, HTTP_404_NOT_FOUND
|
10
11
|
from rest_framework.views import APIView
|
11
12
|
|
13
|
+
from django.http import HttpResponse
|
14
|
+
|
12
15
|
from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_datetime
|
13
|
-
from enterprise_data.admin_analytics.utils import
|
16
|
+
from enterprise_data.admin_analytics.utils import (
|
17
|
+
ChartType,
|
18
|
+
fetch_and_cache_engagements_data,
|
19
|
+
fetch_and_cache_enrollments_data,
|
20
|
+
fetch_and_cache_skills_data,
|
21
|
+
get_skills_chart_data,
|
22
|
+
get_top_skills_csv_data,
|
23
|
+
)
|
14
24
|
from enterprise_data.api.v1 import serializers
|
15
25
|
from enterprise_data.models import EnterpriseAdminLearnerProgress, EnterpriseAdminSummarizeInsights
|
16
26
|
from enterprise_data.utils import date_filter
|
@@ -20,10 +30,13 @@ class EnterpriseAdminInsightsView(APIView):
|
|
20
30
|
"""
|
21
31
|
API for getting the enterprise admin insights.
|
22
32
|
"""
|
33
|
+
|
23
34
|
authentication_classes = (JwtAuthentication,)
|
24
|
-
http_method_names = [
|
35
|
+
http_method_names = ["get"]
|
25
36
|
|
26
|
-
@permission_required(
|
37
|
+
@permission_required(
|
38
|
+
"can_access_enterprise", fn=lambda request, enterprise_id: enterprise_id
|
39
|
+
)
|
27
40
|
def get(self, request, enterprise_id):
|
28
41
|
"""
|
29
42
|
HTTP GET endpoint to retrieve the enterprise admin insights
|
@@ -33,16 +46,24 @@ class EnterpriseAdminInsightsView(APIView):
|
|
33
46
|
learner_engagement = {}
|
34
47
|
|
35
48
|
try:
|
36
|
-
learner_progress = EnterpriseAdminLearnerProgress.objects.get(
|
37
|
-
|
38
|
-
|
49
|
+
learner_progress = EnterpriseAdminLearnerProgress.objects.get(
|
50
|
+
enterprise_customer_uuid=enterprise_id
|
51
|
+
)
|
52
|
+
learner_progress = serializers.EnterpriseAdminLearnerProgressSerializer(
|
53
|
+
learner_progress
|
54
|
+
).data
|
55
|
+
response_data["learner_progress"] = learner_progress
|
39
56
|
except EnterpriseAdminLearnerProgress.DoesNotExist:
|
40
57
|
pass
|
41
58
|
|
42
59
|
try:
|
43
|
-
learner_engagement = EnterpriseAdminSummarizeInsights.objects.get(
|
44
|
-
|
45
|
-
|
60
|
+
learner_engagement = EnterpriseAdminSummarizeInsights.objects.get(
|
61
|
+
enterprise_customer_uuid=enterprise_id
|
62
|
+
)
|
63
|
+
learner_engagement = serializers.EnterpriseAdminSummarizeInsightsSerializer(
|
64
|
+
learner_engagement
|
65
|
+
).data
|
66
|
+
response_data["learner_engagement"] = learner_engagement
|
46
67
|
except EnterpriseAdminSummarizeInsights.DoesNotExist:
|
47
68
|
pass
|
48
69
|
|
@@ -57,53 +78,156 @@ class EnterpriseAdminAnalyticsAggregatesView(APIView):
|
|
57
78
|
"""
|
58
79
|
API for getting the enterprise admin analytics aggregates.
|
59
80
|
"""
|
81
|
+
|
60
82
|
authentication_classes = (JwtAuthentication,)
|
61
|
-
http_method_names = [
|
83
|
+
http_method_names = ["get"]
|
62
84
|
|
63
|
-
@permission_required(
|
85
|
+
@permission_required(
|
86
|
+
"can_access_enterprise", fn=lambda request, enterprise_id: enterprise_id
|
87
|
+
)
|
64
88
|
def get(self, request, enterprise_id):
|
65
89
|
"""
|
66
90
|
HTTP GET endpoint to retrieve the enterprise admin aggregate data.
|
67
91
|
"""
|
68
|
-
serializer = serializers.AdminAnalyticsAggregatesQueryParamsSerializer(
|
92
|
+
serializer = serializers.AdminAnalyticsAggregatesQueryParamsSerializer(
|
93
|
+
data=request.GET
|
94
|
+
)
|
69
95
|
serializer.is_valid(raise_exception=True)
|
70
96
|
|
71
97
|
last_updated_at = fetch_max_enrollment_datetime()
|
72
|
-
cache_expiry =
|
98
|
+
cache_expiry = (
|
99
|
+
last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
|
100
|
+
)
|
73
101
|
|
74
|
-
enrollment = fetch_and_cache_enrollments_data(
|
75
|
-
|
102
|
+
enrollment = fetch_and_cache_enrollments_data(
|
103
|
+
enterprise_id, cache_expiry
|
104
|
+
).copy()
|
105
|
+
engagement = fetch_and_cache_engagements_data(
|
106
|
+
enterprise_id, cache_expiry
|
107
|
+
).copy()
|
76
108
|
# Use start and end date if provided by the client, if client has not provided then use
|
77
109
|
# 1. minimum enrollment date from the data as the start_date
|
78
110
|
# 2. today's date as the end_date
|
79
|
-
start_date = serializer.data.get(
|
80
|
-
|
111
|
+
start_date = serializer.data.get(
|
112
|
+
"start_date", enrollment.enterprise_enrollment_date.min()
|
113
|
+
)
|
114
|
+
end_date = serializer.data.get("end_date", datetime.now())
|
81
115
|
|
82
116
|
# Date filtering.
|
83
117
|
dff = date_filter(
|
84
|
-
start=start_date,
|
118
|
+
start=start_date,
|
119
|
+
end=end_date,
|
120
|
+
data_frame=enrollment.copy(),
|
121
|
+
date_column="enterprise_enrollment_date",
|
85
122
|
)
|
86
123
|
|
87
124
|
enrolls = len(dff)
|
88
125
|
courses = len(dff.course_key.unique())
|
89
126
|
|
90
|
-
dff = date_filter(
|
127
|
+
dff = date_filter(
|
128
|
+
start=start_date,
|
129
|
+
end=end_date,
|
130
|
+
data_frame=enrollment.copy(),
|
131
|
+
date_column="passed_date",
|
132
|
+
)
|
91
133
|
|
92
134
|
completions = dff.has_passed.sum()
|
93
135
|
|
94
136
|
# Date filtering.
|
95
|
-
dff = date_filter(
|
137
|
+
dff = date_filter(
|
138
|
+
start=start_date,
|
139
|
+
end=end_date,
|
140
|
+
data_frame=engagement.copy(),
|
141
|
+
date_column="activity_date",
|
142
|
+
)
|
96
143
|
|
97
144
|
hours = round(dff.learning_time_seconds.sum() / 60 / 60, 1)
|
98
145
|
sessions = dff.is_engaged.sum()
|
99
146
|
|
100
|
-
return Response(
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
147
|
+
return Response(
|
148
|
+
data={
|
149
|
+
"enrolls": enrolls,
|
150
|
+
"courses": courses,
|
151
|
+
"completions": completions,
|
152
|
+
"hours": hours,
|
153
|
+
"sessions": sessions,
|
154
|
+
"last_updated_at": last_updated_at.date() if last_updated_at else None,
|
155
|
+
"min_enrollment_date": enrollment.enterprise_enrollment_date.min().date(),
|
156
|
+
"max_enrollment_date": enrollment.enterprise_enrollment_date.max().date(),
|
157
|
+
},
|
158
|
+
status=HTTP_200_OK,
|
159
|
+
)
|
160
|
+
|
161
|
+
|
162
|
+
class EnterpriseAdminAnalyticsSkillsView(APIView):
|
163
|
+
"""
|
164
|
+
API for getting the enterprise admin analytics skills data.
|
165
|
+
"""
|
166
|
+
authentication_classes = (JwtAuthentication,)
|
167
|
+
http_method_names = ['get']
|
168
|
+
|
169
|
+
@permission_required(
|
170
|
+
'can_access_enterprise', fn=lambda request, enterprise_id: enterprise_id
|
171
|
+
)
|
172
|
+
def get(self, request, enterprise_id):
|
173
|
+
"""HTTP GET endpoint to retrieve the enterprise admin skills aggregated data.
|
174
|
+
|
175
|
+
Args:
|
176
|
+
request (HttpRequest): request object
|
177
|
+
enterprise_id (str): UUID of the enterprise customer
|
178
|
+
|
179
|
+
Returns:
|
180
|
+
response(HttpResponse): response object
|
181
|
+
"""
|
182
|
+
serializer = serializers.AdminAnalyticsAggregatesQueryParamsSerializer(
|
183
|
+
data=request.GET
|
184
|
+
)
|
185
|
+
serializer.is_valid(raise_exception=True)
|
186
|
+
|
187
|
+
start_date = serializer.data.get("start_date")
|
188
|
+
end_date = serializer.data.get("end_date", datetime.now())
|
189
|
+
|
190
|
+
last_updated_at = fetch_max_enrollment_datetime()
|
191
|
+
cache_expiry = (
|
192
|
+
last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
|
193
|
+
)
|
194
|
+
skills = fetch_and_cache_skills_data(enterprise_id, cache_expiry).copy()
|
195
|
+
|
196
|
+
if request.GET.get("format") == "csv":
|
197
|
+
csv_data = get_top_skills_csv_data(skills, start_date, end_date)
|
198
|
+
response = HttpResponse(content_type='text/csv')
|
199
|
+
filename = f"Skills by Enrollment and Completion, {start_date} - {end_date}.csv"
|
200
|
+
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
201
|
+
csv_data.to_csv(path_or_buf=response, index=False)
|
202
|
+
return response
|
203
|
+
|
204
|
+
top_skills = get_skills_chart_data(
|
205
|
+
chart_type=ChartType.BUBBLE,
|
206
|
+
start_date=start_date,
|
207
|
+
end_date=end_date,
|
208
|
+
skills=skills,
|
209
|
+
)
|
210
|
+
top_skills_enrollments = get_skills_chart_data(
|
211
|
+
chart_type=ChartType.TOP_SKILLS_ENROLLMENT,
|
212
|
+
start_date=start_date,
|
213
|
+
end_date=end_date,
|
214
|
+
skills=skills,
|
215
|
+
)
|
216
|
+
top_skills_by_completions = get_skills_chart_data(
|
217
|
+
chart_type=ChartType.TOP_SKILLS_COMPLETION,
|
218
|
+
start_date=start_date,
|
219
|
+
end_date=end_date,
|
220
|
+
skills=skills,
|
221
|
+
)
|
222
|
+
|
223
|
+
response_data = {
|
224
|
+
"top_skills": top_skills.to_dict(orient="records"),
|
225
|
+
"top_skills_by_enrollments": top_skills_enrollments.to_dict(
|
226
|
+
orient="records"
|
227
|
+
),
|
228
|
+
"top_skills_by_completions": top_skills_by_completions.to_dict(
|
229
|
+
orient="records"
|
230
|
+
),
|
231
|
+
}
|
232
|
+
|
233
|
+
return Response(data=response_data, status=HTTP_200_OK)
|
@@ -13,8 +13,13 @@ from enterprise_data.admin_analytics.data_loaders import (
|
|
13
13
|
fetch_engagement_data,
|
14
14
|
fetch_enrollment_data,
|
15
15
|
fetch_max_enrollment_datetime,
|
16
|
+
fetch_skills_data,
|
17
|
+
)
|
18
|
+
from enterprise_data.tests.test_utils import (
|
19
|
+
get_dummy_engagements_data,
|
20
|
+
get_dummy_enrollments_data,
|
21
|
+
get_dummy_skills_data,
|
16
22
|
)
|
17
|
-
from enterprise_data.tests.test_utils import get_dummy_engagements_data, get_dummy_enrollments_data
|
18
23
|
|
19
24
|
|
20
25
|
class TestDataLoaders(TestCase):
|
@@ -84,3 +89,27 @@ class TestDataLoaders(TestCase):
|
|
84
89
|
with pytest.raises(Http404) as error:
|
85
90
|
fetch_enrollment_data(enterprise_uuid)
|
86
91
|
error.value.message = f'No enrollment data found for enterprise {enterprise_uuid}'
|
92
|
+
|
93
|
+
def test_fetch_skills_data(self):
|
94
|
+
"""
|
95
|
+
Validate the fetch_skills_data function.
|
96
|
+
"""
|
97
|
+
with patch('enterprise_data.admin_analytics.data_loaders.run_query') as mock_run_query:
|
98
|
+
enterprise_uuid = str(uuid4())
|
99
|
+
mock_run_query.return_value = [
|
100
|
+
list(item.values()) for item in get_dummy_skills_data(enterprise_uuid)
|
101
|
+
]
|
102
|
+
|
103
|
+
skills_data = fetch_skills_data(enterprise_uuid)
|
104
|
+
self.assertEqual(skills_data.shape, (10, 15))
|
105
|
+
|
106
|
+
def test_fetch_skills_data_empty_data(self):
|
107
|
+
"""
|
108
|
+
Validate the fetch_skills_data function behavior when no data is returned from the query.
|
109
|
+
"""
|
110
|
+
with patch('enterprise_data.admin_analytics.data_loaders.run_query') as mock_run_query:
|
111
|
+
mock_run_query.return_value = []
|
112
|
+
enterprise_uuid = str(uuid4())
|
113
|
+
with pytest.raises(Http404) as error:
|
114
|
+
fetch_skills_data(enterprise_uuid)
|
115
|
+
error.value.message = f'No skills data found for enterprise {enterprise_uuid}'
|
@@ -3,6 +3,7 @@ Test the utility functions in the admin_analytics app.
|
|
3
3
|
"""
|
4
4
|
from datetime import datetime, timedelta
|
5
5
|
|
6
|
+
import pandas
|
6
7
|
from mock import patch
|
7
8
|
|
8
9
|
from django.test import TestCase
|
@@ -10,8 +11,13 @@ from django.test import TestCase
|
|
10
11
|
from enterprise_data.admin_analytics.utils import (
|
11
12
|
fetch_and_cache_engagements_data,
|
12
13
|
fetch_and_cache_enrollments_data,
|
14
|
+
fetch_and_cache_skills_data,
|
13
15
|
get_cache_timeout,
|
16
|
+
get_skills_bubble_chart_df,
|
17
|
+
get_top_skills_completion,
|
18
|
+
get_top_skills_enrollment,
|
14
19
|
)
|
20
|
+
from enterprise_data.utils import date_filter
|
15
21
|
|
16
22
|
|
17
23
|
class TestUtils(TestCase):
|
@@ -100,3 +106,136 @@ class TestUtils(TestCase):
|
|
100
106
|
self.assertEqual(enrollments, 'cached-engagements')
|
101
107
|
self.assertEqual(mock_tiered_cache.get_cached_response.call_count, 1)
|
102
108
|
self.assertEqual(mock_tiered_cache.set_all_tiers.call_count, 0)
|
109
|
+
|
110
|
+
def test_fetch_and_cache_skills_data(self):
|
111
|
+
"""
|
112
|
+
Validate the fetch_and_cache_skills_data function.
|
113
|
+
"""
|
114
|
+
with patch('enterprise_data.admin_analytics.utils.fetch_skills_data') as mock_fetch_skills_data:
|
115
|
+
with patch('enterprise_data.admin_analytics.utils.TieredCache') as mock_tiered_cache:
|
116
|
+
# Simulate the scenario where the data is not found in the cache.
|
117
|
+
mock_tiered_cache.get_cached_response.return_value.is_found = False
|
118
|
+
mock_fetch_skills_data.return_value = 'skills'
|
119
|
+
skills = fetch_and_cache_skills_data('enterprise_id', datetime.now() + timedelta(seconds=10))
|
120
|
+
self.assertEqual(skills, 'skills')
|
121
|
+
self.assertEqual(mock_tiered_cache.get_cached_response.call_count, 1)
|
122
|
+
self.assertEqual(mock_tiered_cache.set_all_tiers.call_count, 1)
|
123
|
+
|
124
|
+
def test_fetch_and_cache_skills_data_with_data_cache_found(self):
|
125
|
+
"""
|
126
|
+
Validate the fetch_and_cache_skills_data function.
|
127
|
+
"""
|
128
|
+
with patch('enterprise_data.admin_analytics.utils.fetch_skills_data') as mock_fetch_skills_data:
|
129
|
+
with patch('enterprise_data.admin_analytics.utils.TieredCache') as mock_tiered_cache:
|
130
|
+
# Simulate the scenario where the data is found in the cache.
|
131
|
+
mock_tiered_cache.get_cached_response.return_value.is_found = True
|
132
|
+
mock_tiered_cache.get_cached_response.return_value.value = 'cached-skills'
|
133
|
+
mock_fetch_skills_data.return_value = 'skills'
|
134
|
+
|
135
|
+
skills = fetch_and_cache_skills_data('enterprise_id', datetime.now() + timedelta(seconds=10))
|
136
|
+
self.assertEqual(skills, 'cached-skills')
|
137
|
+
self.assertEqual(mock_tiered_cache.get_cached_response.call_count, 1)
|
138
|
+
self.assertEqual(mock_tiered_cache.set_all_tiers.call_count, 0)
|
139
|
+
|
140
|
+
def test_get_skills_bubble_chart_df(self):
|
141
|
+
"""
|
142
|
+
Validate the get_skills_bubble_chart_df function.
|
143
|
+
"""
|
144
|
+
# Mock skills data
|
145
|
+
skills = pandas.DataFrame({
|
146
|
+
'skill_name': ['Skill A', 'Skill B', 'Skill C'],
|
147
|
+
'skill_type': ['Type 1', 'Type 2', 'Type 1'],
|
148
|
+
'enrolls': [100, 200, 150],
|
149
|
+
'completions': [50, 100, 75],
|
150
|
+
'date': [datetime.now(), datetime.now(), datetime.now()],
|
151
|
+
})
|
152
|
+
|
153
|
+
# Define the expected result
|
154
|
+
expected_result = pandas.DataFrame({
|
155
|
+
'skill_name': ['Skill B', 'Skill C', 'Skill A'],
|
156
|
+
'skill_type': ['Type 2', 'Type 1', 'Type 1'],
|
157
|
+
'enrolls': [200, 150, 100],
|
158
|
+
'completions': [100, 75, 50]
|
159
|
+
})
|
160
|
+
# Reset the index for the expected result
|
161
|
+
expected_result = expected_result.reset_index(drop=True)
|
162
|
+
|
163
|
+
start_date = datetime.now() - timedelta(days=10)
|
164
|
+
end_date = datetime.now()
|
165
|
+
filtered_skills = date_filter(start_date, end_date, skills, 'date')
|
166
|
+
# Call the function
|
167
|
+
result = get_skills_bubble_chart_df(filtered_skills)
|
168
|
+
|
169
|
+
# Reset the index for the result
|
170
|
+
result = result.reset_index(drop=True)
|
171
|
+
|
172
|
+
# Assert the result
|
173
|
+
pandas.testing.assert_frame_equal(result, expected_result)
|
174
|
+
|
175
|
+
def test_get_top_skills_enrollment(self):
|
176
|
+
"""
|
177
|
+
Validate the get_top_skills_enrollment function.
|
178
|
+
"""
|
179
|
+
# Mock skills data
|
180
|
+
skills = pandas.DataFrame({
|
181
|
+
'primary_subject_name': ['engineering', 'communication', 'engineering', 'other'],
|
182
|
+
'skill_name': ['Skill A', 'Skill B', 'Skill A', 'Skill D'],
|
183
|
+
'enrolls': [100, 200, 150, 300],
|
184
|
+
'completions': [50, 100, 75, 150],
|
185
|
+
'date': [datetime.now(), datetime.now(), datetime.now(), datetime.now()],
|
186
|
+
})
|
187
|
+
|
188
|
+
# Define the expected result
|
189
|
+
expected_result = pandas.DataFrame({
|
190
|
+
'skill_name': ['Skill B', 'Skill A', 'Skill D'],
|
191
|
+
'primary_subject_name': ['communication', 'engineering', 'other'],
|
192
|
+
'count': [200, 250, 300]
|
193
|
+
})
|
194
|
+
# Reset the index for the expected result
|
195
|
+
expected_result = expected_result.reset_index(drop=True)
|
196
|
+
start_date = datetime.now() - timedelta(days=10)
|
197
|
+
end_date = datetime.now()
|
198
|
+
filtered_skills = date_filter(start_date, end_date, skills, 'date')
|
199
|
+
|
200
|
+
# Call the function
|
201
|
+
result = get_top_skills_enrollment(filtered_skills)
|
202
|
+
|
203
|
+
# Reset the index for the result
|
204
|
+
result = result.reset_index(drop=True)
|
205
|
+
|
206
|
+
# Assert the result
|
207
|
+
pandas.testing.assert_frame_equal(result, expected_result)
|
208
|
+
|
209
|
+
def test_get_top_skills_completion(self):
|
210
|
+
"""
|
211
|
+
Validate the get_top_skills_completion function.
|
212
|
+
"""
|
213
|
+
# Mock skills data
|
214
|
+
skills = pandas.DataFrame({
|
215
|
+
'skill_name': ['Skill A', 'Skill B', 'Skill C', 'Skill D'],
|
216
|
+
'primary_subject_name': ['communication', 'engineering', 'communication', 'other'],
|
217
|
+
'enrolls': [100, 200, 150, 300],
|
218
|
+
'completions': [50, 100, 75, 150],
|
219
|
+
'date': [datetime.now(), datetime.now(), datetime.now(), datetime.now()]
|
220
|
+
})
|
221
|
+
|
222
|
+
# Define the expected result
|
223
|
+
expected_result = pandas.DataFrame({
|
224
|
+
'skill_name': ['Skill A', 'Skill C', 'Skill B', 'Skill D'],
|
225
|
+
'primary_subject_name': ['communication', 'communication', 'engineering', 'other'],
|
226
|
+
'count': [50, 75, 100, 150]
|
227
|
+
})
|
228
|
+
# Reset the index for the expected result
|
229
|
+
expected_result = expected_result.reset_index(drop=True)
|
230
|
+
start_date = datetime.now() - timedelta(days=10)
|
231
|
+
end_date = datetime.now()
|
232
|
+
filtered_skills = date_filter(start_date, end_date, skills, 'date')
|
233
|
+
|
234
|
+
# Call the function
|
235
|
+
result = get_top_skills_completion(filtered_skills)
|
236
|
+
|
237
|
+
# Reset the index for the result
|
238
|
+
result = result.reset_index(drop=True)
|
239
|
+
|
240
|
+
# Assert the result
|
241
|
+
pandas.testing.assert_frame_equal(result, expected_result)
|
@@ -2,6 +2,7 @@
|
|
2
2
|
Test cases for enterprise_admin views
|
3
3
|
"""
|
4
4
|
from unittest import mock
|
5
|
+
from uuid import uuid4
|
5
6
|
|
6
7
|
import ddt
|
7
8
|
from mock import patch
|
@@ -16,6 +17,7 @@ from enterprise_data.tests.test_utils import (
|
|
16
17
|
get_dummy_engagements_data,
|
17
18
|
get_dummy_enrollments_data,
|
18
19
|
get_dummy_enterprise_api_data,
|
20
|
+
get_dummy_skills_data,
|
19
21
|
)
|
20
22
|
from enterprise_data_roles.constants import ENTERPRISE_DATA_ADMIN_ROLE
|
21
23
|
from enterprise_data_roles.models import EnterpriseDataFeatureRole, EnterpriseDataRoleAssignment
|
@@ -80,3 +82,50 @@ class TestEnterpriseAdminAnalyticsAggregatesView(JWTTestMixin, APITransactionTes
|
|
80
82
|
assert 'last_updated_at' in response.json()
|
81
83
|
assert 'min_enrollment_date' in response.json()
|
82
84
|
assert 'max_enrollment_date' in response.json()
|
85
|
+
|
86
|
+
|
87
|
+
@ddt.ddt
|
88
|
+
@mark.django_db
|
89
|
+
class TestEnterpriseAdminAnalyticsSkillsView(JWTTestMixin, APITransactionTestCase):
|
90
|
+
"""
|
91
|
+
Tests for EnterpriseAdminAnalyticsSkillsView.
|
92
|
+
"""
|
93
|
+
|
94
|
+
def setUp(self):
|
95
|
+
"""
|
96
|
+
Setup method.
|
97
|
+
"""
|
98
|
+
super().setUp()
|
99
|
+
self.user = UserFactory()
|
100
|
+
self.enterprise_id = uuid4()
|
101
|
+
self.set_jwt_cookie(context=f'{self.enterprise_id}')
|
102
|
+
|
103
|
+
def _mock_run_query(self, query):
|
104
|
+
"""
|
105
|
+
mock implementation of run_query.
|
106
|
+
"""
|
107
|
+
if 'skills_daily_rollup_admin_dash' in query:
|
108
|
+
return [
|
109
|
+
list(item.values()) for item in get_dummy_skills_data(self.enterprise_id)
|
110
|
+
]
|
111
|
+
else:
|
112
|
+
return [
|
113
|
+
list(item.values()) for item in get_dummy_skills_data(self.enterprise_id)
|
114
|
+
]
|
115
|
+
|
116
|
+
def test_get_admin_analytics_skills(self):
|
117
|
+
"""
|
118
|
+
Test to get admin analytics skills.
|
119
|
+
"""
|
120
|
+
params = {'start_date': '2021-01-01', 'end_date': '2025-12-31'}
|
121
|
+
url = reverse('v1:enterprise-admin-analytics-skills', kwargs={'enterprise_id': self.enterprise_id})
|
122
|
+
with patch('enterprise_data.admin_analytics.data_loaders.run_query', side_effect=self._mock_run_query):
|
123
|
+
response = self.client.get(url, params)
|
124
|
+
assert response.status_code == status.HTTP_200_OK
|
125
|
+
data = response.json()
|
126
|
+
assert 'top_skills' in data
|
127
|
+
assert 'top_skills_by_enrollments' in data
|
128
|
+
assert 'top_skills_by_completions' in data
|
129
|
+
assert len(data['top_skills']) == 10
|
130
|
+
assert len(data['top_skills_by_enrollments']) == 10
|
131
|
+
assert len(data['top_skills_by_completions']) == 10
|
@@ -418,3 +418,31 @@ def get_dummy_enrollments_data(enterprise_uuid: str, count=10):
|
|
418
418
|
'has_passed': FAKER.boolean(),
|
419
419
|
} for _ in range(count)
|
420
420
|
]
|
421
|
+
|
422
|
+
|
423
|
+
def get_dummy_skills_data(enterprise_uuid: str, count=10):
|
424
|
+
"""
|
425
|
+
Utility method to get dummy skills data.
|
426
|
+
"""
|
427
|
+
return [
|
428
|
+
{
|
429
|
+
'course_number': FAKER.random_int(min=1),
|
430
|
+
'skill_type': 'skill_type',
|
431
|
+
'skill_name': ' '.join(FAKER.words(nb=2)).title(),
|
432
|
+
'skill_url': FAKER.url(),
|
433
|
+
'confidence': FAKER.random_int(min=1),
|
434
|
+
'skill_rank': FAKER.random_int(min=1),
|
435
|
+
'course_title': ' '.join(FAKER.words(nb=5)).title(),
|
436
|
+
'course_key': FAKER.slug(),
|
437
|
+
'level_type': 'level_type',
|
438
|
+
'primary_subject_name': ' '.join(FAKER.words(nb=2)).title(),
|
439
|
+
'date': FAKER.date_time_between(
|
440
|
+
start_date='-2M',
|
441
|
+
end_date='+2M',
|
442
|
+
),
|
443
|
+
'enterprise_customer_uuid': enterprise_uuid,
|
444
|
+
'enterprise_customer_name': ' '.join(FAKER.words(nb=2)).title(),
|
445
|
+
'enrolls': FAKER.random_int(min=1),
|
446
|
+
'completions': FAKER.random_int(min=1),
|
447
|
+
} for _ in range(count)
|
448
|
+
]
|
enterprise_data/utils.py
CHANGED
@@ -82,3 +82,19 @@ def date_filter(start, end, data_frame, date_column):
|
|
82
82
|
(pandas.DataFrame): The filtered DataFrame.
|
83
83
|
"""
|
84
84
|
return data_frame[(start <= data_frame[date_column]) & (data_frame[date_column] <= end)]
|
85
|
+
|
86
|
+
|
87
|
+
def primary_subject_truncate(x):
|
88
|
+
"""
|
89
|
+
Truncate primary subject to a few categories.
|
90
|
+
"""
|
91
|
+
if x in [
|
92
|
+
"business-management",
|
93
|
+
"computer-science",
|
94
|
+
"data-analysis-statistics",
|
95
|
+
"engineering",
|
96
|
+
"communication",
|
97
|
+
]:
|
98
|
+
return x
|
99
|
+
else:
|
100
|
+
return "other"
|
File without changes
|
File without changes
|
File without changes
|