edx-enterprise-data 8.12.1__py3-none-any.whl → 9.0.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.12.1.dist-info → edx_enterprise_data-9.0.0.dist-info}/METADATA +3 -4
- {edx_enterprise_data-8.12.1.dist-info → edx_enterprise_data-9.0.0.dist-info}/RECORD +16 -14
- {edx_enterprise_data-8.12.1.dist-info → edx_enterprise_data-9.0.0.dist-info}/WHEEL +1 -1
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/data_loaders.py +0 -48
- enterprise_data/admin_analytics/database/queries/skills_daily_rollup_admin_dash.py +118 -0
- enterprise_data/admin_analytics/database/tables/__init__.py +1 -0
- enterprise_data/admin_analytics/database/tables/skills_daily_rollup_admin_dash.py +94 -0
- enterprise_data/admin_analytics/utils.py +0 -171
- enterprise_data/api/v1/views/enterprise_admin.py +25 -56
- enterprise_data/tests/admin_analytics/mock_analytics_data.py +56 -0
- enterprise_data/tests/admin_analytics/test_data_loaders.py +1 -30
- enterprise_data/tests/admin_analytics/test_utils.py +0 -139
- enterprise_data/tests/api/v1/views/test_enterprise_admin.py +133 -44
- {edx_enterprise_data-8.12.1.dist-info → edx_enterprise_data-9.0.0.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-8.12.1.dist-info → edx_enterprise_data-9.0.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: edx-enterprise-data
|
3
|
-
Version:
|
3
|
+
Version: 9.0.0
|
4
4
|
Summary: Enterprise Reporting
|
5
5
|
Home-page: https://github.com/openedx/edx-enterprise-data
|
6
6
|
Author: edX
|
@@ -9,7 +9,6 @@ License: AGPL 3.0
|
|
9
9
|
Classifier: Framework :: Django
|
10
10
|
Classifier: Framework :: Django :: 4.2
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
12
|
-
Classifier: Programming Language :: Python :: 3.8
|
13
12
|
Classifier: Programming Language :: Python :: 3.12
|
14
13
|
License-File: LICENSE
|
15
14
|
Requires-Dist: Django
|
@@ -24,8 +23,8 @@ Requires-Dist: edx-rbac
|
|
24
23
|
Requires-Dist: edx-rest-api-client
|
25
24
|
Requires-Dist: factory-boy
|
26
25
|
Requires-Dist: mysql-connector-python
|
27
|
-
Requires-Dist: numpy
|
28
|
-
Requires-Dist: pandas
|
26
|
+
Requires-Dist: numpy
|
27
|
+
Requires-Dist: pandas
|
29
28
|
Requires-Dist: requests
|
30
29
|
Requires-Dist: rules
|
31
30
|
Provides-Extra: reporting
|
@@ -1,4 +1,4 @@
|
|
1
|
-
enterprise_data/__init__.py,sha256=
|
1
|
+
enterprise_data/__init__.py,sha256=vkhCefJkr927CkxZwyM-rQdSx9oK9C0-wIYclDJtrmg,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
|
@@ -12,17 +12,19 @@ enterprise_data/utils.py,sha256=_zK3BErjZSIkP_JNzK8m-DR5pRTnxKylP9I-vURaRcE,3009
|
|
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=7WturLuMISekgcHHlgj45PPdPrDTYM-l21lA8-Q_Tfc,1107
|
15
|
-
enterprise_data/admin_analytics/data_loaders.py,sha256=
|
16
|
-
enterprise_data/admin_analytics/utils.py,sha256=
|
15
|
+
enterprise_data/admin_analytics/data_loaders.py,sha256=z5OHCmsjQuu1lBw43mSSCbdSQ40fmL_WtodV_Tcnj5U,4791
|
16
|
+
enterprise_data/admin_analytics/utils.py,sha256=DftyBaSKnf0f6E3adH8yVAvGNeQGROywufqnJxZ0W84,6295
|
17
17
|
enterprise_data/admin_analytics/database/__init__.py,sha256=vNSWKf2VV5xMegN7htJJtxtQEb0ASLC6frE2w0ZpYpE,104
|
18
18
|
enterprise_data/admin_analytics/database/utils.py,sha256=5u-d6ZQW95mF_r4bH8Xdi7DgpYAuDFOG_q0P-bjKXHU,1712
|
19
19
|
enterprise_data/admin_analytics/database/queries/__init__.py,sha256=IC5TLOr_GnydbrVbl2mWhwO3aUbYeHuDmfPTLmwGhZA,218
|
20
20
|
enterprise_data/admin_analytics/database/queries/fact_engagement_admin_dash.py,sha256=fq01Ni_sKnvSRoiPQfTnJ8TtRePQe5MBLmI5CpVy36o,747
|
21
21
|
enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py,sha256=uuhX3OIB5Cp0-5uxN604HNEUuzb3s9nH3VR4CiEXs18,5388
|
22
|
-
enterprise_data/admin_analytics/database/
|
22
|
+
enterprise_data/admin_analytics/database/queries/skills_daily_rollup_admin_dash.py,sha256=PgWwvtVCK5lbiq6z44lH0fwbkdWYukhyXZL9X8lNWCY,4099
|
23
|
+
enterprise_data/admin_analytics/database/tables/__init__.py,sha256=Z-c3P9hqR-dC9uYKe63qHkQG9Nms8cLE2jRN-4jeMM0,289
|
23
24
|
enterprise_data/admin_analytics/database/tables/base.py,sha256=1KyKsC18pW3m-5U-T6pdt5rIwsz6Wp3QFFbD3r6L6YQ,395
|
24
25
|
enterprise_data/admin_analytics/database/tables/fact_engagement_admin_dash.py,sha256=EsG8KgRW84wtA_nJuUknjLYlDtaPSJf_9mWdkO2Bj2I,1293
|
25
26
|
enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py,sha256=ix5QvPnrUZMVs_Fdt742i9PAmrQTXuqHlfW3PJhSQWo,7282
|
27
|
+
enterprise_data/admin_analytics/database/tables/skills_daily_rollup_admin_dash.py,sha256=9PqLeVqByrz7R0qumRbwJlr5lzIWn7Fl7WEGM0aJVlw,3131
|
26
28
|
enterprise_data/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
27
29
|
enterprise_data/api/urls.py,sha256=POqc_KATHdnpMf9zHtpO46pKD5KAlAExtx7G6iylLcU,273
|
28
30
|
enterprise_data/api/v0/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
|
@@ -38,7 +40,7 @@ enterprise_data/api/v1/views/analytics_engagements.py,sha256=8H3Fk-hTqJaU3H5Lpu1
|
|
38
40
|
enterprise_data/api/v1/views/analytics_enrollments.py,sha256=uVc36C0s9y1dPBfbYwFhXLqPlExvQvZeiJ4C45lb4ZQ,6447
|
39
41
|
enterprise_data/api/v1/views/analytics_leaderboard.py,sha256=2DALqzUIbe4-ZGgHHIkYAKJ5L1ik2ruPtQNYtTdPba4,5974
|
40
42
|
enterprise_data/api/v1/views/base.py,sha256=Kkmd5zgEBAhvwS_GoGXSK6lgbDNwSPioYn-QbnizI3w,3416
|
41
|
-
enterprise_data/api/v1/views/enterprise_admin.py,sha256=
|
43
|
+
enterprise_data/api/v1/views/enterprise_admin.py,sha256=DsR1oHFhe6LCIFjIJ4YLLZ7PUChvNlFfdZD-sxHoijY,7388
|
42
44
|
enterprise_data/api/v1/views/enterprise_completions.py,sha256=bJG2ZtTbLyiBrj64iJHQNHEKLrJCzl9OuJ7nDtw-9aY,8377
|
43
45
|
enterprise_data/api/v1/views/enterprise_learner.py,sha256=yABjJje3CT8I8YOhWr1_tTkdKtnGJom8eu3EFz_-0BU,18517
|
44
46
|
enterprise_data/api/v1/views/enterprise_offers.py,sha256=VifxgqTLFLVw4extYPlHcN1N_yjXcsYsAlYEnAbpb10,1266
|
@@ -113,14 +115,14 @@ enterprise_data/tests/test_models.py,sha256=MWBY-LY5TPBjZ4GlvpM-h4W-BvRKr2Rml8Bz
|
|
113
115
|
enterprise_data/tests/test_utils.py,sha256=vbmYM7DMN-lHS2p4yaa0Yd6uSGXd2qoZRDE9X3J4Sec,18385
|
114
116
|
enterprise_data/tests/test_views.py,sha256=UvDRNTxruy5zBK_KgUy2cBMbwlaTW_vkM0-TCXbQZiY,69667
|
115
117
|
enterprise_data/tests/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
116
|
-
enterprise_data/tests/admin_analytics/mock_analytics_data.py,sha256=
|
118
|
+
enterprise_data/tests/admin_analytics/mock_analytics_data.py,sha256=LiuwYQXj44XW_mBIQz9gP3Izy8dwrkX-Yp9s8PA19e4,20866
|
117
119
|
enterprise_data/tests/admin_analytics/mock_enrollments.py,sha256=LfuMo9Kn-OQD4z42G3BRuM5MXUUXXlaAMhTqfJf46XE,7266
|
118
120
|
enterprise_data/tests/admin_analytics/test_analytics_engagements.py,sha256=KPXtBPaAOrzfff7W-xERSGx9KtZAJndLbIJx3gopSnE,15689
|
119
121
|
enterprise_data/tests/admin_analytics/test_analytics_enrollments.py,sha256=GePv5E8BRGRWUHUwGaXvYIsN3dtDpNXUh-yfW5iBTi4,13781
|
120
122
|
enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py,sha256=VSEyDAHfWBJvqmx9yzd4NnPAqK3TqaKrMBWswMAdzfU,6206
|
121
|
-
enterprise_data/tests/admin_analytics/test_data_loaders.py,sha256=
|
123
|
+
enterprise_data/tests/admin_analytics/test_data_loaders.py,sha256=b4BjN88FX9WjE6XJjkJZnoEvWVB_DovBGJ_wh-HgT9I,3514
|
122
124
|
enterprise_data/tests/admin_analytics/test_enterprise_completions.py,sha256=afkHQFy4bvqZ0pq5Drl1t2nv8zxbgca2jzOQbihlPG0,7359
|
123
|
-
enterprise_data/tests/admin_analytics/test_utils.py,sha256=
|
125
|
+
enterprise_data/tests/admin_analytics/test_utils.py,sha256=4qL_ZK-sGzbMMqiOrBrPmzdIPno7KohiaIfd7FMehic,5260
|
124
126
|
enterprise_data/tests/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
125
127
|
enterprise_data/tests/api/v0/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
126
128
|
enterprise_data/tests/api/v0/test_serializers.py,sha256=Gfty6gy6OQLN318uL1OCPhAZOqSUL50FWc0nC23VMnc,6257
|
@@ -128,7 +130,7 @@ enterprise_data/tests/api/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
|
|
128
130
|
enterprise_data/tests/api/v1/test_serializers.py,sha256=DwgEHcyOP3oqNUPB2O-NkJGeO_cYs9XJiq7791vJLZE,3682
|
129
131
|
enterprise_data/tests/api/v1/test_views.py,sha256=rLqUHfar0HdBNtz33hQxd_0qUUgr7Ku3KwQSQ1B4Ypg,15213
|
130
132
|
enterprise_data/tests/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
131
|
-
enterprise_data/tests/api/v1/views/test_enterprise_admin.py,sha256=
|
133
|
+
enterprise_data/tests/api/v1/views/test_enterprise_admin.py,sha256=ysdxVJU-beWQ7eCCTvnKR4br8VFz2lPg7w64z9mrado,9495
|
132
134
|
enterprise_data_roles/__init__.py,sha256=toCpbypm2uDoWVw29_em9gPFNly8vNUS__C0b4TCqEg,112
|
133
135
|
enterprise_data_roles/admin.py,sha256=QNP0VeWE092vZzpyxOA5UJK1nNGl5e71B1J0RCwo_nU,998
|
134
136
|
enterprise_data_roles/apps.py,sha256=nKi8TyuQ5Q6WGtKs5QeXvUTc3N-YQjKhyBnm2EM3Bng,260
|
@@ -169,8 +171,8 @@ enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=WtL-RqGgu2x5PP
|
|
169
171
|
enterprise_reporting/tests/test_utils.py,sha256=Zt_TA0LVb-B6fQGkUkAKKVlUKKnQh8jnw1US1jKe7g8,9493
|
170
172
|
enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
|
171
173
|
enterprise_reporting/tests/utils.py,sha256=xms2LM7DV3wczXEfctOK1ddel1EE0J_YSr17UzbCDy4,1401
|
172
|
-
edx_enterprise_data-
|
173
|
-
edx_enterprise_data-
|
174
|
-
edx_enterprise_data-
|
175
|
-
edx_enterprise_data-
|
176
|
-
edx_enterprise_data-
|
174
|
+
edx_enterprise_data-9.0.0.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
|
175
|
+
edx_enterprise_data-9.0.0.dist-info/METADATA,sha256=c0qWPvk7cM_zOA1XYs1-xxzL9Gw9_XbKlYL7u0tgVAQ,1504
|
176
|
+
edx_enterprise_data-9.0.0.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
|
177
|
+
edx_enterprise_data-9.0.0.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
|
178
|
+
edx_enterprise_data-9.0.0.dist-info/RECORD,,
|
enterprise_data/__init__.py
CHANGED
@@ -147,51 +147,3 @@ def fetch_max_enrollment_datetime():
|
|
147
147
|
if not results:
|
148
148
|
return None
|
149
149
|
return pandas.to_datetime(results[0][0])
|
150
|
-
|
151
|
-
|
152
|
-
def fetch_skills_data(enterprise_uuid: str):
|
153
|
-
"""
|
154
|
-
Fetch skills data from the database for the given enterprise customer.
|
155
|
-
|
156
|
-
Arguments:
|
157
|
-
enterprise_uuid (str): The UUID of the enterprise customer.
|
158
|
-
|
159
|
-
Returns:
|
160
|
-
(pandas.DataFrame): The skills data.
|
161
|
-
"""
|
162
|
-
|
163
|
-
enterprise_uuid = enterprise_uuid.replace('-', '')
|
164
|
-
|
165
|
-
cols = [
|
166
|
-
'course_number',
|
167
|
-
'skill_type',
|
168
|
-
'skill_name',
|
169
|
-
'skill_url',
|
170
|
-
'confidence',
|
171
|
-
'skill_rank',
|
172
|
-
'course_title',
|
173
|
-
'course_key',
|
174
|
-
'level_type',
|
175
|
-
'primary_subject_name',
|
176
|
-
'date',
|
177
|
-
'enterprise_customer_uuid',
|
178
|
-
'enterprise_customer_name',
|
179
|
-
'enrolls',
|
180
|
-
'completions',
|
181
|
-
]
|
182
|
-
query = get_select_query(
|
183
|
-
table='skills_daily_rollup_admin_dash', columns=cols, enterprise_uuid=enterprise_uuid
|
184
|
-
)
|
185
|
-
|
186
|
-
with timer('fetch_skills_data'):
|
187
|
-
skills = run_query(query=query)
|
188
|
-
|
189
|
-
if not skills:
|
190
|
-
raise Http404(f'No skills data found for enterprise {enterprise_uuid}')
|
191
|
-
|
192
|
-
LOGGER.info(f'[PLOTLY] Skills data fetched successfully. Records: {len(skills)}')
|
193
|
-
skills = pandas.DataFrame(numpy.array(skills), columns=cols)
|
194
|
-
LOGGER.info('[PLOTLY] Skills data converted to DataFrame.')
|
195
|
-
skills['date'] = skills['date'].astype('datetime64[ns]')
|
196
|
-
|
197
|
-
return skills
|
@@ -0,0 +1,118 @@
|
|
1
|
+
"""
|
2
|
+
Module containing queries for the skills_daily_rollup_admin_dash table.
|
3
|
+
"""
|
4
|
+
|
5
|
+
|
6
|
+
class SkillsDailyRollupAdminDashQueries:
|
7
|
+
"""
|
8
|
+
Queries related to the skills_daily_rollup_admin_dash table.
|
9
|
+
"""
|
10
|
+
@staticmethod
|
11
|
+
def get_top_skills():
|
12
|
+
"""
|
13
|
+
Get the query to fetch the top skills for an enterprise customer.
|
14
|
+
"""
|
15
|
+
return """
|
16
|
+
SELECT
|
17
|
+
skill_name,
|
18
|
+
skill_type,
|
19
|
+
SUM(enrolls) AS enrolls,
|
20
|
+
SUM(completions) AS completions
|
21
|
+
FROM
|
22
|
+
skills_daily_rollup_admin_dash
|
23
|
+
WHERE
|
24
|
+
enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
|
25
|
+
date BETWEEN %(start_date)s AND %(end_date)s
|
26
|
+
GROUP BY
|
27
|
+
skill_name, skill_type
|
28
|
+
ORDER BY
|
29
|
+
enrolls DESC, completions DESC;
|
30
|
+
"""
|
31
|
+
|
32
|
+
@staticmethod
|
33
|
+
def get_top_skills_by_enrollment():
|
34
|
+
"""
|
35
|
+
Get the query to fetch the top skills by enrollment for an enterprise customer.
|
36
|
+
"""
|
37
|
+
return """
|
38
|
+
WITH TopSkills AS (
|
39
|
+
-- Get top 10 skills by total enrollments
|
40
|
+
SELECT
|
41
|
+
skill_name
|
42
|
+
FROM
|
43
|
+
skills_daily_rollup_admin_dash
|
44
|
+
WHERE
|
45
|
+
enterprise_customer_uuid=%(enterprise_customer_uuid)s
|
46
|
+
AND date BETWEEN %(start_date)s AND %(end_date)s
|
47
|
+
GROUP BY
|
48
|
+
skill_name
|
49
|
+
ORDER BY
|
50
|
+
SUM(enrolls) DESC
|
51
|
+
LIMIT 10
|
52
|
+
)
|
53
|
+
SELECT
|
54
|
+
sd.skill_name,
|
55
|
+
CASE
|
56
|
+
WHEN sd.primary_subject_name IN (
|
57
|
+
'business-management', 'computer-science',
|
58
|
+
'data-analysis-statistics', 'engineering', 'communication'
|
59
|
+
) THEN sd.primary_subject_name
|
60
|
+
ELSE 'other'
|
61
|
+
END AS subject_name,
|
62
|
+
SUM(sd.enrolls) AS count
|
63
|
+
FROM
|
64
|
+
skills_daily_rollup_admin_dash AS sd
|
65
|
+
JOIN
|
66
|
+
TopSkills AS ts ON sd.skill_name = ts.skill_name
|
67
|
+
WHERE
|
68
|
+
sd.enterprise_customer_uuid=%(enterprise_customer_uuid)s
|
69
|
+
AND date BETWEEN %(start_date)s AND %(end_date)s
|
70
|
+
GROUP BY
|
71
|
+
sd.skill_name, subject_name
|
72
|
+
ORDER BY
|
73
|
+
subject_name;
|
74
|
+
"""
|
75
|
+
|
76
|
+
@staticmethod
|
77
|
+
def get_top_skills_by_completion():
|
78
|
+
"""
|
79
|
+
Get the query to fetch the top skills by completion for an enterprise customer.
|
80
|
+
"""
|
81
|
+
return """
|
82
|
+
WITH TopSkills AS (
|
83
|
+
-- Get top 10 skills by total completions
|
84
|
+
SELECT
|
85
|
+
skill_name
|
86
|
+
FROM
|
87
|
+
skills_daily_rollup_admin_dash
|
88
|
+
WHERE
|
89
|
+
enterprise_customer_uuid=%(enterprise_customer_uuid)s
|
90
|
+
AND date BETWEEN %(start_date)s AND %(end_date)s
|
91
|
+
GROUP BY
|
92
|
+
skill_name
|
93
|
+
ORDER BY
|
94
|
+
SUM(completions) DESC
|
95
|
+
LIMIT 10
|
96
|
+
)
|
97
|
+
SELECT
|
98
|
+
sd.skill_name,
|
99
|
+
CASE
|
100
|
+
WHEN sd.primary_subject_name IN (
|
101
|
+
'business-management', 'computer-science',
|
102
|
+
'data-analysis-statistics', 'engineering', 'communication'
|
103
|
+
) THEN sd.primary_subject_name
|
104
|
+
ELSE 'other'
|
105
|
+
END AS subject_name,
|
106
|
+
SUM(sd.completions) AS count
|
107
|
+
FROM
|
108
|
+
skills_daily_rollup_admin_dash AS sd
|
109
|
+
JOIN
|
110
|
+
TopSkills AS ts ON sd.skill_name = ts.skill_name
|
111
|
+
WHERE
|
112
|
+
sd.enterprise_customer_uuid=%(enterprise_customer_uuid)s
|
113
|
+
AND date BETWEEN %(start_date)s AND %(end_date)s
|
114
|
+
GROUP BY
|
115
|
+
sd.skill_name, subject_name
|
116
|
+
ORDER BY
|
117
|
+
subject_name;
|
118
|
+
"""
|
@@ -3,3 +3,4 @@ This module contains the database queries for the admin analytics.
|
|
3
3
|
"""
|
4
4
|
from .fact_engagement_admin_dash import FactEngagementAdminDashTable
|
5
5
|
from .fact_enrollment_admin_dash import FactEnrollmentAdminDashTable
|
6
|
+
from .skills_daily_rollup_admin_dash import SkillsDailyRollupAdminDashTable
|
@@ -0,0 +1,94 @@
|
|
1
|
+
"""
|
2
|
+
Module for interacting with the skills_daily_rollup_admin_dash table.
|
3
|
+
"""
|
4
|
+
from datetime import date
|
5
|
+
from uuid import UUID
|
6
|
+
|
7
|
+
from enterprise_data.admin_analytics.database.queries.skills_daily_rollup_admin_dash import (
|
8
|
+
SkillsDailyRollupAdminDashQueries,
|
9
|
+
)
|
10
|
+
from enterprise_data.admin_analytics.database.tables.base import BaseTable
|
11
|
+
from enterprise_data.admin_analytics.database.utils import run_query
|
12
|
+
|
13
|
+
|
14
|
+
class SkillsDailyRollupAdminDashTable(BaseTable):
|
15
|
+
"""
|
16
|
+
Class for communicating with the skills_daily_rollup_admin_dash table.
|
17
|
+
"""
|
18
|
+
queries = SkillsDailyRollupAdminDashQueries()
|
19
|
+
|
20
|
+
def get_top_skills(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
|
21
|
+
"""
|
22
|
+
Get the top skills for the given enterprise customer.
|
23
|
+
|
24
|
+
Arguments:
|
25
|
+
enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
|
26
|
+
start_date (date): The start date.
|
27
|
+
end_date (date): The end date.
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
list<dict>: A list of dictionaries containing the skill_name, skill_type, enrolls and completions.
|
31
|
+
"""
|
32
|
+
return run_query(
|
33
|
+
query=self.queries.get_top_skills(),
|
34
|
+
params={
|
35
|
+
'enterprise_customer_uuid': enterprise_customer_uuid,
|
36
|
+
'start_date': start_date,
|
37
|
+
'end_date': end_date,
|
38
|
+
},
|
39
|
+
as_dict=True,
|
40
|
+
)
|
41
|
+
|
42
|
+
def get_top_skills_by_enrollment(
|
43
|
+
self,
|
44
|
+
enterprise_customer_uuid: UUID,
|
45
|
+
start_date: date,
|
46
|
+
end_date: date
|
47
|
+
):
|
48
|
+
"""
|
49
|
+
Get the top skills by enrollments for the given enterprise customer.
|
50
|
+
|
51
|
+
Arguments:
|
52
|
+
enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
|
53
|
+
start_date (date): The start date.
|
54
|
+
end_date (date): The end date.
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
list<dict>: A list of dictionaries containing the skill_name, subject_name, count.
|
58
|
+
"""
|
59
|
+
return run_query(
|
60
|
+
query=self.queries.get_top_skills_by_enrollment(),
|
61
|
+
params={
|
62
|
+
'enterprise_customer_uuid': enterprise_customer_uuid,
|
63
|
+
'start_date': start_date,
|
64
|
+
'end_date': end_date,
|
65
|
+
},
|
66
|
+
as_dict=True,
|
67
|
+
)
|
68
|
+
|
69
|
+
def get_top_skills_by_completion(
|
70
|
+
self,
|
71
|
+
enterprise_customer_uuid: UUID,
|
72
|
+
start_date: date,
|
73
|
+
end_date: date
|
74
|
+
):
|
75
|
+
"""
|
76
|
+
Get the top skills by completion for the given enterprise customer.
|
77
|
+
|
78
|
+
Arguments:
|
79
|
+
enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
|
80
|
+
start_date (date): The start date.
|
81
|
+
end_date (date): The end date.
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
list<dict>: A list of dictionaries containing the skill_name, subject_name, count.
|
85
|
+
"""
|
86
|
+
return run_query(
|
87
|
+
query=self.queries.get_top_skills_by_completion(),
|
88
|
+
params={
|
89
|
+
'enterprise_customer_uuid': enterprise_customer_uuid,
|
90
|
+
'start_date': start_date,
|
91
|
+
'end_date': end_date,
|
92
|
+
},
|
93
|
+
as_dict=True,
|
94
|
+
)
|
@@ -12,9 +12,7 @@ from enterprise_data.admin_analytics.data_loaders import (
|
|
12
12
|
fetch_engagement_data,
|
13
13
|
fetch_enrollment_data,
|
14
14
|
fetch_max_enrollment_datetime,
|
15
|
-
fetch_skills_data,
|
16
15
|
)
|
17
|
-
from enterprise_data.utils import date_filter, primary_subject_truncate
|
18
16
|
|
19
17
|
LOGGER = getLogger(__name__)
|
20
18
|
|
@@ -23,9 +21,6 @@ class ChartType(Enum):
|
|
23
21
|
"""
|
24
22
|
Chart types.
|
25
23
|
"""
|
26
|
-
BUBBLE = 'bubble'
|
27
|
-
TOP_SKILLS_ENROLLMENT = 'top_skills_enrollment'
|
28
|
-
TOP_SKILLS_COMPLETION = 'top_skills_completion'
|
29
24
|
COMPLETIONS_OVER_TIME = 'completions_over_time'
|
30
25
|
TOP_COURSES_BY_COMPLETIONS = 'top_courses_by_completions'
|
31
26
|
TOP_SUBJECTS_BY_COMPLETIONS = 'top_subjects_by_completions'
|
@@ -183,169 +178,3 @@ def fetch_and_cache_engagements_data(enterprise_id, cache_expiry):
|
|
183
178
|
cache_key, engagements, get_cache_timeout(cache_expiry)
|
184
179
|
)
|
185
180
|
return engagements
|
186
|
-
|
187
|
-
|
188
|
-
def fetch_and_cache_skills_data(enterprise_id, cache_expiry):
|
189
|
-
"""
|
190
|
-
Helper method to fetch and cache skills data.
|
191
|
-
|
192
|
-
Arguments:
|
193
|
-
enterprise_id (str): UUID of the enterprise customer in string format.
|
194
|
-
cache_expiry (datetime): Datetime object denoting the cache expiry.
|
195
|
-
|
196
|
-
Returns:
|
197
|
-
(pandas.DataFrame): The skills data.
|
198
|
-
"""
|
199
|
-
cache_key = get_cache_key(
|
200
|
-
resource='enterprise-admin-analytics-aggregate-skills',
|
201
|
-
enterprise_customer=enterprise_id,
|
202
|
-
)
|
203
|
-
cached_response = TieredCache.get_cached_response(cache_key)
|
204
|
-
|
205
|
-
if cached_response.is_found:
|
206
|
-
LOGGER.info(f"Skills data found in cache for Enterprise [{enterprise_id}]")
|
207
|
-
return cached_response.value
|
208
|
-
else:
|
209
|
-
skills = fetch_skills_data(enterprise_id)
|
210
|
-
TieredCache.set_all_tiers(
|
211
|
-
cache_key, skills, get_cache_timeout(cache_expiry)
|
212
|
-
)
|
213
|
-
return skills
|
214
|
-
|
215
|
-
|
216
|
-
def get_skills_bubble_chart_df(skills_filtered):
|
217
|
-
""" Get the skills data for the bubble chart.
|
218
|
-
|
219
|
-
Args:
|
220
|
-
skills_filtered (pandas.DataFrame): The skills data.
|
221
|
-
|
222
|
-
Returns:
|
223
|
-
(pandas.DataFrame): The skills data for the bubble chart.
|
224
|
-
"""
|
225
|
-
|
226
|
-
# Group by skill_name and skill_type, and aggregate enrolls and completions
|
227
|
-
skills_aggregated = (
|
228
|
-
skills_filtered.groupby(['skill_name', 'skill_type'], as_index=False)
|
229
|
-
.agg(enrolls=('enrolls', 'sum'), completions=('completions', 'sum'))
|
230
|
-
)
|
231
|
-
|
232
|
-
# Convert enrolls and completions to integers
|
233
|
-
skills_aggregated['enrolls'] = skills_aggregated['enrolls'].astype(int)
|
234
|
-
skills_aggregated['completions'] = skills_aggregated['completions'].astype(int)
|
235
|
-
|
236
|
-
# Sort the dataframe by enrolls and completions in descending order
|
237
|
-
skills_aggregated = skills_aggregated.sort_values(by=['enrolls', 'completions'], ascending=False)
|
238
|
-
|
239
|
-
return skills_aggregated
|
240
|
-
|
241
|
-
|
242
|
-
def get_top_skills_enrollment(skills_filtered):
|
243
|
-
""" Get the top skills by enrolls.
|
244
|
-
|
245
|
-
Args:
|
246
|
-
skills_filtered (pandas.DataFrame): The skills data.
|
247
|
-
|
248
|
-
Returns:
|
249
|
-
(pandas.DataFrame): The top skills by enrolls data
|
250
|
-
"""
|
251
|
-
|
252
|
-
# Get the top 10 skills by enrolls
|
253
|
-
top_skills = (
|
254
|
-
skills_filtered.groupby('skill_name')
|
255
|
-
.enrolls.sum()
|
256
|
-
.sort_values(ascending=False)
|
257
|
-
.head(10)
|
258
|
-
.index
|
259
|
-
)
|
260
|
-
|
261
|
-
# Apply primary_subject_truncate to the primary_subject_name column
|
262
|
-
skills_filtered['primary_subject_name'] = skills_filtered['primary_subject_name'].apply(primary_subject_truncate)
|
263
|
-
|
264
|
-
# Filter data for the top skills and aggregate enrolls by skill_name and primary_subject_name
|
265
|
-
top_skills_enrollment_data = (
|
266
|
-
skills_filtered[skills_filtered.skill_name.isin(top_skills)]
|
267
|
-
.groupby(['skill_name', 'primary_subject_name'], as_index=False)
|
268
|
-
.agg(count=('enrolls', 'sum'))
|
269
|
-
)
|
270
|
-
|
271
|
-
# Sort the dataframe by primary_subject_name
|
272
|
-
top_skills_enrollment_data = top_skills_enrollment_data.sort_values(by="primary_subject_name")
|
273
|
-
|
274
|
-
return top_skills_enrollment_data
|
275
|
-
|
276
|
-
|
277
|
-
def get_top_skills_completion(skills_filtered):
|
278
|
-
""" Get the top skills by completions.
|
279
|
-
|
280
|
-
Args:
|
281
|
-
skills_filtered (pandas.DataFrame): The skills data.
|
282
|
-
|
283
|
-
Returns:
|
284
|
-
(pandas.DataFrame): The top skills by completions
|
285
|
-
"""
|
286
|
-
|
287
|
-
# Get the top 10 skills by completions
|
288
|
-
top_skills = (
|
289
|
-
skills_filtered.groupby('skill_name')
|
290
|
-
.completions.sum()
|
291
|
-
.sort_values(ascending=False)
|
292
|
-
.head(10)
|
293
|
-
.index
|
294
|
-
)
|
295
|
-
|
296
|
-
# Apply primary_subject_truncate to the primary_subject_name column
|
297
|
-
skills_filtered['primary_subject_name'] = skills_filtered['primary_subject_name'].apply(primary_subject_truncate)
|
298
|
-
|
299
|
-
# Filter data for the top skills and aggregate completions by skill_name and primary_subject_name
|
300
|
-
top_skills_completion_data = (
|
301
|
-
skills_filtered[skills_filtered.skill_name.isin(top_skills)]
|
302
|
-
.groupby(['skill_name', 'primary_subject_name'], as_index=False)
|
303
|
-
.agg(count=('completions', 'sum'))
|
304
|
-
)
|
305
|
-
|
306
|
-
# Sort the dataframe by primary_subject_name
|
307
|
-
top_skills_completion_data = top_skills_completion_data.sort_values(by='primary_subject_name')
|
308
|
-
|
309
|
-
return top_skills_completion_data
|
310
|
-
|
311
|
-
|
312
|
-
def get_skills_chart_data(chart_type, start_date, end_date, skills):
|
313
|
-
"""
|
314
|
-
Get chart data for skill charts.
|
315
|
-
|
316
|
-
Arguments:
|
317
|
-
chart_type (ChartType): The type of chart to generate.
|
318
|
-
start_date (datetime): The start date for the date filter.
|
319
|
-
end_date (datetime): The end date for the date filter.
|
320
|
-
skills (pandas.DataFrame): The skills data.
|
321
|
-
"""
|
322
|
-
skills_filtered = date_filter(start=start_date, end=end_date, data_frame=skills.copy(), date_column='date')
|
323
|
-
if chart_type == ChartType.BUBBLE:
|
324
|
-
return get_skills_bubble_chart_df(skills_filtered=skills_filtered.copy())
|
325
|
-
elif chart_type == ChartType.TOP_SKILLS_ENROLLMENT:
|
326
|
-
return get_top_skills_enrollment(skills_filtered=skills_filtered.copy())
|
327
|
-
elif chart_type == ChartType.TOP_SKILLS_COMPLETION:
|
328
|
-
return get_top_skills_completion(skills_filtered=skills_filtered.copy())
|
329
|
-
else:
|
330
|
-
raise ValueError(f"Invalid chart type: {chart_type}")
|
331
|
-
|
332
|
-
|
333
|
-
def get_top_skills_csv_data(skills, start_date, end_date):
|
334
|
-
""" Get the top skills data for CSV download.
|
335
|
-
|
336
|
-
Args:
|
337
|
-
skills (pandas.DataFrame): The skills data.
|
338
|
-
start_date (str): The start date for the date filter.
|
339
|
-
end_date (str): The end date for the date filter.
|
340
|
-
|
341
|
-
Returns:
|
342
|
-
(pandas.DataFrame): The top skills data for CSV download.
|
343
|
-
"""
|
344
|
-
dff = get_skills_chart_data(
|
345
|
-
chart_type=ChartType.BUBBLE,
|
346
|
-
start_date=start_date,
|
347
|
-
end_date=end_date,
|
348
|
-
skills=skills.copy(),
|
349
|
-
)
|
350
|
-
dff = dff.sort_values(by='enrolls', ascending=False)
|
351
|
-
return dff
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
Views for enterprise admin api v1.
|
3
3
|
"""
|
4
|
-
from datetime import datetime
|
4
|
+
from datetime import datetime
|
5
5
|
|
6
6
|
from edx_rbac.decorators import permission_required
|
7
7
|
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
@@ -10,16 +10,11 @@ from rest_framework.response import Response
|
|
10
10
|
from rest_framework.status import HTTP_200_OK, HTTP_404_NOT_FOUND
|
11
11
|
from rest_framework.views import APIView
|
12
12
|
|
13
|
-
from django.http import HttpResponse
|
14
|
-
|
15
13
|
from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_datetime
|
16
|
-
from enterprise_data.admin_analytics.database.tables import
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
fetch_and_cache_skills_data,
|
21
|
-
get_skills_chart_data,
|
22
|
-
get_top_skills_csv_data,
|
14
|
+
from enterprise_data.admin_analytics.database.tables import (
|
15
|
+
FactEngagementAdminDashTable,
|
16
|
+
FactEnrollmentAdminDashTable,
|
17
|
+
SkillsDailyRollupAdminDashTable,
|
23
18
|
)
|
24
19
|
from enterprise_data.api.v1 import serializers
|
25
20
|
from enterprise_data.models import (
|
@@ -157,61 +152,35 @@ class EnterpriseAdminAnalyticsSkillsView(APIView):
|
|
157
152
|
Returns:
|
158
153
|
response(HttpResponse): response object
|
159
154
|
"""
|
160
|
-
|
161
|
-
|
162
|
-
)
|
155
|
+
enterprise_id = enterprise_id.replace('-', '')
|
156
|
+
serializer = serializers.AdvanceAnalyticsQueryParamSerializer(data=request.GET)
|
163
157
|
serializer.is_valid(raise_exception=True)
|
164
|
-
last_updated_at = fetch_max_enrollment_datetime()
|
165
|
-
cache_expiry = (
|
166
|
-
last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
|
167
|
-
)
|
168
158
|
|
169
|
-
|
170
|
-
|
171
|
-
).copy()
|
159
|
+
if (start_date := serializer.data.get('start_date')) is None:
|
160
|
+
start_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(enterprise_id)
|
172
161
|
|
173
|
-
start_date = serializer.data.get('start_date', enrollment.enterprise_enrollment_date.min())
|
174
162
|
end_date = serializer.data.get('end_date', datetime.now())
|
175
163
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
csv_data.to_csv(path_or_buf=response, index=False)
|
185
|
-
return response
|
186
|
-
|
187
|
-
with timer('skills_all_charts_data'):
|
188
|
-
top_skills = get_skills_chart_data(
|
189
|
-
chart_type=ChartType.BUBBLE,
|
190
|
-
start_date=start_date,
|
191
|
-
end_date=end_date,
|
192
|
-
skills=skills,
|
193
|
-
)
|
194
|
-
top_skills_enrollments = get_skills_chart_data(
|
195
|
-
chart_type=ChartType.TOP_SKILLS_ENROLLMENT,
|
196
|
-
start_date=start_date,
|
197
|
-
end_date=end_date,
|
198
|
-
skills=skills,
|
164
|
+
with timer('top_skills'):
|
165
|
+
skills = SkillsDailyRollupAdminDashTable().get_top_skills(enterprise_id, start_date, end_date)
|
166
|
+
|
167
|
+
with timer('top_skills_by_enrollments'):
|
168
|
+
top_skills_by_enrollments = SkillsDailyRollupAdminDashTable().get_top_skills_by_enrollment(
|
169
|
+
enterprise_id,
|
170
|
+
start_date,
|
171
|
+
end_date
|
199
172
|
)
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
173
|
+
with timer('top_skills_by_completions'):
|
174
|
+
top_skills_by_completions = SkillsDailyRollupAdminDashTable().get_top_skills_by_completion(
|
175
|
+
enterprise_id,
|
176
|
+
start_date,
|
177
|
+
end_date
|
205
178
|
)
|
206
179
|
|
207
180
|
response_data = {
|
208
|
-
"top_skills":
|
209
|
-
"top_skills_by_enrollments":
|
210
|
-
|
211
|
-
),
|
212
|
-
"top_skills_by_completions": top_skills_by_completions.to_dict(
|
213
|
-
orient="records"
|
214
|
-
),
|
181
|
+
"top_skills": skills,
|
182
|
+
"top_skills_by_enrollments": top_skills_by_enrollments,
|
183
|
+
"top_skills_by_completions": top_skills_by_completions,
|
215
184
|
}
|
216
185
|
|
217
186
|
return Response(data=response_data, status=HTTP_200_OK)
|
@@ -537,3 +537,59 @@ def engagements_csv_content():
|
|
537
537
|
b'paul77@example.org,Synergized reciprocal encoding,2021-07-26,business-management,4.4\r\n'
|
538
538
|
b'samanthaclarke@example.org,Synergized reciprocal encoding,2021-07-19,business-management,0.0\r\n'
|
539
539
|
)
|
540
|
+
|
541
|
+
|
542
|
+
TOP_SKILLS = [
|
543
|
+
{
|
544
|
+
"skill_name": "Python (Programming Language)",
|
545
|
+
"skill_type": "Specialized Skill",
|
546
|
+
"enrolls": 19027.0,
|
547
|
+
"completions": 3004.0
|
548
|
+
},
|
549
|
+
{
|
550
|
+
"skill_name": "Data Science",
|
551
|
+
"skill_type": "Specialized Skill",
|
552
|
+
"enrolls": 13756.0,
|
553
|
+
"completions": 1517.0
|
554
|
+
},
|
555
|
+
{
|
556
|
+
"skill_name": "Algorithms",
|
557
|
+
"skill_type": "Specialized Skill",
|
558
|
+
"enrolls": 12776.0,
|
559
|
+
"completions": 1640.0
|
560
|
+
},
|
561
|
+
]
|
562
|
+
TOP_SKILLS_BY_ENROLLMENTS = [
|
563
|
+
{
|
564
|
+
"skill_name": "Python (Programming Language)",
|
565
|
+
"subject_name": "business-management",
|
566
|
+
"count": 313.0
|
567
|
+
},
|
568
|
+
{
|
569
|
+
"skill_name": "Machine Learning",
|
570
|
+
"subject_name": "business-management",
|
571
|
+
"count": 442.0
|
572
|
+
},
|
573
|
+
{
|
574
|
+
"skill_name": "Computer Science",
|
575
|
+
"subject_name": "business-management",
|
576
|
+
"count": 39.0
|
577
|
+
},
|
578
|
+
]
|
579
|
+
TOP_SKILLS_BY_COMPLETIONS = [
|
580
|
+
{
|
581
|
+
"skill_name": "Python (Programming Language)",
|
582
|
+
"subject_name": "business-management",
|
583
|
+
"count": 21.0
|
584
|
+
},
|
585
|
+
{
|
586
|
+
"skill_name": "SQL (Programming Language)",
|
587
|
+
"subject_name": "business-management",
|
588
|
+
"count": 11.0
|
589
|
+
},
|
590
|
+
{
|
591
|
+
"skill_name": "Algorithms",
|
592
|
+
"subject_name": "business-management",
|
593
|
+
"count": 15.0
|
594
|
+
},
|
595
|
+
]
|
@@ -13,13 +13,8 @@ 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,
|
22
16
|
)
|
17
|
+
from enterprise_data.tests.test_utils import get_dummy_engagements_data, get_dummy_enrollments_data
|
23
18
|
|
24
19
|
|
25
20
|
class TestDataLoaders(TestCase):
|
@@ -89,27 +84,3 @@ class TestDataLoaders(TestCase):
|
|
89
84
|
with pytest.raises(Http404) as error:
|
90
85
|
fetch_enrollment_data(enterprise_uuid)
|
91
86
|
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,7 +3,6 @@ Test the utility functions in the admin_analytics app.
|
|
3
3
|
"""
|
4
4
|
from datetime import datetime, timedelta
|
5
5
|
|
6
|
-
import pandas
|
7
6
|
from mock import patch
|
8
7
|
|
9
8
|
from django.test import TestCase
|
@@ -11,13 +10,8 @@ from django.test import TestCase
|
|
11
10
|
from enterprise_data.admin_analytics.utils import (
|
12
11
|
fetch_and_cache_engagements_data,
|
13
12
|
fetch_and_cache_enrollments_data,
|
14
|
-
fetch_and_cache_skills_data,
|
15
13
|
get_cache_timeout,
|
16
|
-
get_skills_bubble_chart_df,
|
17
|
-
get_top_skills_completion,
|
18
|
-
get_top_skills_enrollment,
|
19
14
|
)
|
20
|
-
from enterprise_data.utils import date_filter
|
21
15
|
|
22
16
|
|
23
17
|
class TestUtils(TestCase):
|
@@ -106,136 +100,3 @@ class TestUtils(TestCase):
|
|
106
100
|
self.assertEqual(enrollments, 'cached-engagements')
|
107
101
|
self.assertEqual(mock_tiered_cache.get_cached_response.call_count, 1)
|
108
102
|
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)
|
@@ -3,7 +3,6 @@ Test cases for enterprise_admin views
|
|
3
3
|
"""
|
4
4
|
from datetime import datetime
|
5
5
|
from unittest import mock
|
6
|
-
from uuid import uuid4
|
7
6
|
|
8
7
|
import ddt
|
9
8
|
from mock import patch
|
@@ -16,13 +15,13 @@ from enterprise_data.admin_analytics.database.queries import (
|
|
16
15
|
FactEngagementAdminDashQueries,
|
17
16
|
FactEnrollmentAdminDashQueries,
|
18
17
|
)
|
19
|
-
from enterprise_data.tests.
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
get_dummy_enterprise_api_data,
|
24
|
-
get_dummy_skills_data,
|
18
|
+
from enterprise_data.tests.admin_analytics.mock_analytics_data import (
|
19
|
+
TOP_SKILLS,
|
20
|
+
TOP_SKILLS_BY_COMPLETIONS,
|
21
|
+
TOP_SKILLS_BY_ENROLLMENTS,
|
25
22
|
)
|
23
|
+
from enterprise_data.tests.mixins import JWTTestMixin
|
24
|
+
from enterprise_data.tests.test_utils import UserFactory, get_dummy_enterprise_api_data
|
26
25
|
from enterprise_data_roles.constants import ENTERPRISE_DATA_ADMIN_ROLE
|
27
26
|
from enterprise_data_roles.models import EnterpriseDataFeatureRole, EnterpriseDataRoleAssignment
|
28
27
|
|
@@ -108,51 +107,141 @@ class TestEnterpriseAdminAnalyticsAggregatesView(JWTTestMixin, APITransactionTes
|
|
108
107
|
|
109
108
|
|
110
109
|
@ddt.ddt
|
111
|
-
|
112
|
-
|
113
|
-
"""
|
114
|
-
Tests for EnterpriseAdminAnalyticsSkillsView.
|
115
|
-
"""
|
110
|
+
class TestSkillsStatsAPI(JWTTestMixin, APITransactionTestCase):
|
111
|
+
"""Tests for EnterpriseAdminAnalyticsSkillsView."""
|
116
112
|
|
117
113
|
def setUp(self):
|
118
114
|
"""
|
119
115
|
Setup method.
|
120
116
|
"""
|
121
117
|
super().setUp()
|
122
|
-
self.user = UserFactory()
|
123
|
-
|
124
|
-
|
118
|
+
self.user = UserFactory(is_staff=True)
|
119
|
+
role, __ = EnterpriseDataFeatureRole.objects.get_or_create(
|
120
|
+
name=ENTERPRISE_DATA_ADMIN_ROLE
|
121
|
+
)
|
122
|
+
self.role_assignment = EnterpriseDataRoleAssignment.objects.create(
|
123
|
+
role=role, user=self.user
|
124
|
+
)
|
125
|
+
self.client.force_authenticate(user=self.user)
|
125
126
|
|
126
|
-
|
127
|
+
self.enterprise_uuid = "ee5e6b3a069a4947bb8dd2dbc323396c"
|
128
|
+
self.set_jwt_cookie()
|
129
|
+
|
130
|
+
self.url = reverse(
|
131
|
+
"v1:enterprise-admin-analytics-skills",
|
132
|
+
kwargs={"enterprise_id": self.enterprise_uuid},
|
133
|
+
)
|
134
|
+
|
135
|
+
@patch('enterprise_data.api.v1.views.enterprise_admin.FactEnrollmentAdminDashTable.get_enrollment_date_range')
|
136
|
+
@patch('enterprise_data.api.v1.views.enterprise_admin.SkillsDailyRollupAdminDashTable.get_top_skills')
|
137
|
+
@patch('enterprise_data.api.v1.views.enterprise_admin.SkillsDailyRollupAdminDashTable.get_top_skills_by_enrollment')
|
138
|
+
@patch('enterprise_data.api.v1.views.enterprise_admin.SkillsDailyRollupAdminDashTable.get_top_skills_by_completion')
|
139
|
+
def test_get(
|
140
|
+
self,
|
141
|
+
mock_get_top_skills_by_completion,
|
142
|
+
mock_get_top_skills_by_enrollment,
|
143
|
+
mock_get_top_skills,
|
144
|
+
mock_get_enrollment_date_range
|
145
|
+
):
|
127
146
|
"""
|
128
|
-
|
147
|
+
Test the GET method for the EnterpriseAdminAnalyticsSkillsView works.
|
129
148
|
"""
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
149
|
+
mock_get_enrollment_date_range.return_value = ("2020-04-03", "2024-07-04")
|
150
|
+
mock_get_top_skills.return_value = TOP_SKILLS
|
151
|
+
mock_get_top_skills_by_enrollment.return_value = TOP_SKILLS_BY_ENROLLMENTS
|
152
|
+
mock_get_top_skills_by_completion.return_value = TOP_SKILLS_BY_COMPLETIONS
|
153
|
+
|
154
|
+
response = self.client.get(self.url)
|
155
|
+
assert response.status_code == status.HTTP_200_OK
|
156
|
+
data = response.json()
|
157
|
+
assert data == {
|
158
|
+
"top_skills": [
|
159
|
+
{
|
160
|
+
"skill_name": "Python (Programming Language)",
|
161
|
+
"skill_type": "Specialized Skill",
|
162
|
+
"enrolls": 19027.0,
|
163
|
+
"completions": 3004.0,
|
164
|
+
},
|
165
|
+
{
|
166
|
+
"skill_name": "Data Science",
|
167
|
+
"skill_type": "Specialized Skill",
|
168
|
+
"enrolls": 13756.0,
|
169
|
+
"completions": 1517.0,
|
170
|
+
},
|
171
|
+
{
|
172
|
+
"skill_name": "Algorithms",
|
173
|
+
"skill_type": "Specialized Skill",
|
174
|
+
"enrolls": 12776.0,
|
175
|
+
"completions": 1640.0,
|
176
|
+
},
|
177
|
+
],
|
178
|
+
"top_skills_by_enrollments": [
|
179
|
+
{
|
180
|
+
"skill_name": "Python (Programming Language)",
|
181
|
+
"subject_name": "business-management",
|
182
|
+
"count": 313.0,
|
183
|
+
},
|
184
|
+
{
|
185
|
+
"skill_name": "Machine Learning",
|
186
|
+
"subject_name": "business-management",
|
187
|
+
"count": 442.0,
|
188
|
+
},
|
189
|
+
{
|
190
|
+
"skill_name": "Computer Science",
|
191
|
+
"subject_name": "business-management",
|
192
|
+
"count": 39.0,
|
193
|
+
},
|
194
|
+
],
|
195
|
+
"top_skills_by_completions": [
|
196
|
+
{
|
197
|
+
"skill_name": "Python (Programming Language)",
|
198
|
+
"subject_name": "business-management",
|
199
|
+
"count": 21.0,
|
200
|
+
},
|
201
|
+
{
|
202
|
+
"skill_name": "SQL (Programming Language)",
|
203
|
+
"subject_name": "business-management",
|
204
|
+
"count": 11.0,
|
205
|
+
},
|
206
|
+
{
|
207
|
+
"skill_name": "Algorithms",
|
208
|
+
"subject_name": "business-management",
|
209
|
+
"count": 15.0,
|
210
|
+
},
|
211
|
+
],
|
212
|
+
}
|
213
|
+
|
214
|
+
@ddt.data(
|
215
|
+
{
|
216
|
+
"params": {"start_date": 1},
|
217
|
+
"error": {
|
218
|
+
"start_date": [
|
219
|
+
"Date has wrong format. Use one of these formats instead: YYYY-MM-DD."
|
220
|
+
]
|
221
|
+
},
|
222
|
+
},
|
223
|
+
{
|
224
|
+
"params": {"end_date": 2},
|
225
|
+
"error": {
|
226
|
+
"end_date": [
|
227
|
+
"Date has wrong format. Use one of these formats instead: YYYY-MM-DD."
|
228
|
+
]
|
229
|
+
},
|
230
|
+
},
|
231
|
+
{
|
232
|
+
"params": {"start_date": "2024-01-01", "end_date": "2023-01-01"},
|
233
|
+
"error": {
|
234
|
+
"non_field_errors": [
|
235
|
+
"start_date should be less than or equal to end_date."
|
236
|
+
]
|
237
|
+
},
|
238
|
+
},
|
239
|
+
)
|
240
|
+
@ddt.unpack
|
241
|
+
def test_get_invalid_query_params(self, params, error):
|
144
242
|
"""
|
145
|
-
Test
|
243
|
+
Test the GET method return correct error if any query param value is incorrect.
|
146
244
|
"""
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
response = self.client.get(url, params)
|
151
|
-
assert response.status_code == status.HTTP_200_OK
|
152
|
-
data = response.json()
|
153
|
-
assert 'top_skills' in data
|
154
|
-
assert 'top_skills_by_enrollments' in data
|
155
|
-
assert 'top_skills_by_completions' in data
|
156
|
-
assert len(data['top_skills']) == 10
|
157
|
-
assert len(data['top_skills_by_enrollments']) == 10
|
158
|
-
assert len(data['top_skills_by_completions']) == 10
|
245
|
+
response = self.client.get(self.url, params)
|
246
|
+
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
247
|
+
assert response.json() == error
|
File without changes
|
File without changes
|