edx-enterprise-data 8.3.1__py3-none-any.whl → 8.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edx-enterprise-data
3
- Version: 8.3.1
3
+ Version: 8.5.0
4
4
  Summary: Enterprise Reporting
5
5
  Home-page: https://github.com/openedx/edx-enterprise-data
6
6
  Author: edX
@@ -1,18 +1,18 @@
1
- enterprise_data/__init__.py,sha256=9P5euSdqQ9R848n450LMmcWN8bT4_wUPcxDHek9waKk,123
1
+ enterprise_data/__init__.py,sha256=93nSn1NykQTFtU7czMTFJab_r7CaAtL_iv-q3peH5ME,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
5
5
  enterprise_data/filters.py,sha256=D2EiK12MMpBoz6eOUmTpoJEhj_sH7bA93NRRAdvkDVo,6163
6
- enterprise_data/models.py,sha256=3VHJ5eWfI0KZxh5u969E7uA2nE5VZqyUnk_VV--UTI4,19154
6
+ enterprise_data/models.py,sha256=khGcOh7NWP8KGu84t78Y2zAu3knREeXA_prApmU2NX8,24428
7
7
  enterprise_data/paginators.py,sha256=YPrC5TeXFt-ymenT2H8H2nCbDCnAzJQlH9kFPElRxWE,269
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=UruTQLJcEl-q5UWGPnJZrnBnV1OvzBvhuoB_JkRY4Is,2328
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=jP9CgUt_93Bl_ak1_6dBPN4PAEdDT1da3sEMjJ45NLg,4281
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=Cock_rTtHkBEKcG3cq7VcYRPEuxC_SyJBlZn9KdVeCA,2465
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
@@ -20,11 +20,11 @@ enterprise_data/api/v0/serializers.py,sha256=dngZTk6DhRxApchQKCMp1B_c8aVnQtH0NCq
20
20
  enterprise_data/api/v0/urls.py,sha256=vzJjqIo_S3AXWs9Us8XTaJc3FnxLbYzAkmLyuDQqum0,699
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
- enterprise_data/api/v1/serializers.py,sha256=AOHumabnzwK59nytoOZEWhA3f9EL_CV-bXPsYSH3LI8,8263
24
- enterprise_data/api/v1/urls.py,sha256=IQMAD9sZp0QeJa2cUd-WxwugWaW4hHZetQGulTjnrGc,1633
23
+ enterprise_data/api/v1/serializers.py,sha256=5C7IYNUuW7HvFVqtbsLIBE6A2Ywzmy6SFzPc4FjtclY,8562
24
+ enterprise_data/api/v1/urls.py,sha256=OlchBMQmyvhM2HpVbbSZCMpUD_y8tP9-vsorGtBvxVo,2048
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=mvpufYoqyaGbHuclRpFj-cQxeME1j4L6S6qQ72Rxem0,4727
27
+ enterprise_data/api/v1/views/enterprise_admin.py,sha256=RTSRyPfHvbzV_ihSbGjYi0VuE6AjaYZIyqpAKTMYa5Q,8980
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
@@ -85,6 +85,7 @@ enterprise_data/migrations/0037_alter_enterpriseenrollment_consent_granted.py,sh
85
85
  enterprise_data/migrations/0038_enterpriseoffer_export_timestamp.py,sha256=8St3DsKb-VI3LGGC-Mc7OvmFn67kVob0HKCPGKcjvu4,436
86
86
  enterprise_data/migrations/0039_auto_20240212_1403.py,sha256=rMiJcYx26ZGqPdQzgiWkUgbcRhD-tp9BgXdgt67vC2Q,1002
87
87
  enterprise_data/migrations/0040_auto_20240718_0536_squashed_0043_alter_enterpriselearnerenrollment_enterprise_enrollment_id.py,sha256=Kq_ResqoAsJjtyIysHjs4VaDxNh6p4fPMMyh93GIJxA,1693
88
+ enterprise_data/migrations/0044_enterpriseexecedlcmoduleperformance.py,sha256=1aJ0wIu9K2cT3AJdBIHuNCUpJysPWmq_37Ucp5ntEO8,6122
88
89
  enterprise_data/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
90
  enterprise_data/settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
90
91
  enterprise_data/settings/test.py,sha256=4-flfrlf81AthGx9wTaT5PscyoOWyhsDDqbzBl-z7Eg,4191
@@ -94,11 +95,11 @@ enterprise_data/tests/mixins.py,sha256=YifptI9mtOhAWnBGyPUy4kX5OJNSDP3DvW2vb1E2t
94
95
  enterprise_data/tests/test_clients.py,sha256=xBPHF9cgEFqNJoL4klOoYh_sVS3scZGcX0Ltc9Ghp7A,6336
95
96
  enterprise_data/tests/test_filters.py,sha256=ZBbLl9Sgj5mJ7lTWoaFcEPwuxPDpIbMo2n_Fhurc0T8,7263
96
97
  enterprise_data/tests/test_models.py,sha256=MWBY-LY5TPBjZ4GlvpM-h4W-BvRKr2Rml8Bzg1NPZ9M,3234
97
- enterprise_data/tests/test_utils.py,sha256=itT-LvZwgJbLCSpqVCYA2TpCV9uKGi4TeVoelRfx-48,17327
98
+ enterprise_data/tests/test_utils.py,sha256=vbmYM7DMN-lHS2p4yaa0Yd6uSGXd2qoZRDE9X3J4Sec,18385
98
99
  enterprise_data/tests/test_views.py,sha256=UvDRNTxruy5zBK_KgUy2cBMbwlaTW_vkM0-TCXbQZiY,69667
99
100
  enterprise_data/tests/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
100
- enterprise_data/tests/admin_analytics/test_data_loaders.py,sha256=b4BjN88FX9WjE6XJjkJZnoEvWVB_DovBGJ_wh-HgT9I,3514
101
- enterprise_data/tests/admin_analytics/test_utils.py,sha256=4qL_ZK-sGzbMMqiOrBrPmzdIPno7KohiaIfd7FMehic,5260
101
+ enterprise_data/tests/admin_analytics/test_data_loaders.py,sha256=o3denJ4aUS1pI5Crksl4C6m-NtCBm8ynoHBnLkf-v2U,4641
102
+ enterprise_data/tests/admin_analytics/test_utils.py,sha256=y33HXy6BDOoftdcz3qYlOYhgx7JSXDki-OLzBdTpiwA,11449
102
103
  enterprise_data/tests/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
103
104
  enterprise_data/tests/api/v0/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
105
  enterprise_data/tests/api/v0/test_serializers.py,sha256=Gfty6gy6OQLN318uL1OCPhAZOqSUL50FWc0nC23VMnc,6257
@@ -106,7 +107,7 @@ enterprise_data/tests/api/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
106
107
  enterprise_data/tests/api/v1/test_serializers.py,sha256=DwgEHcyOP3oqNUPB2O-NkJGeO_cYs9XJiq7791vJLZE,3682
107
108
  enterprise_data/tests/api/v1/test_views.py,sha256=rLqUHfar0HdBNtz33hQxd_0qUUgr7Ku3KwQSQ1B4Ypg,15213
108
109
  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=A7QY4RR6BwzxSsC9equXau8_PER25-KbufCNZyGdYBI,3025
110
+ enterprise_data/tests/api/v1/views/test_enterprise_admin.py,sha256=7FCPpfFrv-pisJ9cxt3B-KIf-KM3a7XzQ8MweLp23wI,4783
110
111
  enterprise_data_roles/__init__.py,sha256=toCpbypm2uDoWVw29_em9gPFNly8vNUS__C0b4TCqEg,112
111
112
  enterprise_data_roles/admin.py,sha256=QNP0VeWE092vZzpyxOA5UJK1nNGl5e71B1J0RCwo_nU,998
112
113
  enterprise_data_roles/apps.py,sha256=nKi8TyuQ5Q6WGtKs5QeXvUTc3N-YQjKhyBnm2EM3Bng,260
@@ -147,8 +148,8 @@ enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=WtL-RqGgu2x5PP
147
148
  enterprise_reporting/tests/test_utils.py,sha256=Zt_TA0LVb-B6fQGkUkAKKVlUKKnQh8jnw1US1jKe7g8,9493
148
149
  enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
149
150
  enterprise_reporting/tests/utils.py,sha256=xms2LM7DV3wczXEfctOK1ddel1EE0J_YSr17UzbCDy4,1401
150
- edx_enterprise_data-8.3.1.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
151
- edx_enterprise_data-8.3.1.dist-info/METADATA,sha256=Wb7AA7AgVPyoHB7_BzDnbnjcZ_-xx1nXtUCf0vkrAs8,1569
152
- edx_enterprise_data-8.3.1.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
153
- edx_enterprise_data-8.3.1.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
154
- edx_enterprise_data-8.3.1.dist-info/RECORD,,
151
+ edx_enterprise_data-8.5.0.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
152
+ edx_enterprise_data-8.5.0.dist-info/METADATA,sha256=oabxLQt9xaTLpOgQX1KdSryfXehxEHBMEroSMHcU6-I,1569
153
+ edx_enterprise_data-8.5.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
154
+ edx_enterprise_data-8.5.0.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
155
+ edx_enterprise_data-8.5.0.dist-info/RECORD,,
@@ -2,4 +2,4 @@
2
2
  Enterprise data api application. This Django app exposes API endpoints used by enterprises.
3
3
  """
4
4
 
5
- __version__ = "8.3.1"
5
+ __version__ = "8.5.0"
@@ -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
@@ -8,6 +8,7 @@ from rest_framework import serializers
8
8
  from enterprise_data.models import (
9
9
  EnterpriseAdminLearnerProgress,
10
10
  EnterpriseAdminSummarizeInsights,
11
+ EnterpriseExecEdLCModulePerformance,
11
12
  EnterpriseLearner,
12
13
  EnterpriseLearnerEnrollment,
13
14
  EnterpriseOffer,
@@ -216,3 +217,13 @@ class AdminAnalyticsAggregatesQueryParamsSerializer(serializers.Serializer): #
216
217
  if attrs['start_date'] > attrs['end_date']:
217
218
  raise serializers.ValidationError("start_date should be less than or equal to end_date.")
218
219
  return attrs
220
+
221
+
222
+ class EnterpriseExecEdLCModulePerformanceSerializer(serializers.ModelSerializer):
223
+ """
224
+ Serializer for EnterpriseExecEdLCModulePerformance model.
225
+ """
226
+
227
+ class Meta:
228
+ model = EnterpriseExecEdLCModulePerformance
229
+ fields = '__all__'
@@ -35,6 +35,11 @@ router.register(
35
35
  enterprise_learner_views.EnterpriseLearnerCompletedCoursesViewSet,
36
36
  'enterprise-learner-completed-courses',
37
37
  )
38
+ router.register(
39
+ r'enterprise/(?P<enterprise_id>.+)/module-performance',
40
+ enterprise_admin_views.EnterpriseExecEdLCModulePerformanceViewSet,
41
+ 'enterprise-admin-module-performance',
42
+ )
38
43
 
39
44
  urlpatterns = [
40
45
  re_path(
@@ -47,6 +52,11 @@ urlpatterns = [
47
52
  enterprise_admin_views.EnterpriseAdminAnalyticsAggregatesView.as_view(),
48
53
  name='enterprise-admin-analytics-aggregates'
49
54
  ),
55
+ re_path(
56
+ fr'^admin/anlaytics/(?P<enterprise_id>{UUID4_REGEX})/skills/stats',
57
+ enterprise_admin_views.EnterpriseAdminAnalyticsSkillsView.as_view(),
58
+ name='enterprise-admin-analytics-skills'
59
+ ),
50
60
  ]
51
61
 
52
62
  urlpatterns += router.urls
@@ -5,25 +5,44 @@ from datetime import datetime, timedelta
5
5
 
6
6
  from edx_rbac.decorators import permission_required
7
7
  from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
8
+ from rest_framework import filters, viewsets
8
9
  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 fetch_and_cache_engagements_data, fetch_and_cache_enrollments_data
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
- from enterprise_data.models import EnterpriseAdminLearnerProgress, EnterpriseAdminSummarizeInsights
25
+ from enterprise_data.models import (
26
+ EnterpriseAdminLearnerProgress,
27
+ EnterpriseAdminSummarizeInsights,
28
+ EnterpriseExecEdLCModulePerformance,
29
+ )
16
30
  from enterprise_data.utils import date_filter
17
31
 
32
+ from .base import EnterpriseViewSetMixin
33
+
18
34
 
19
35
  class EnterpriseAdminInsightsView(APIView):
20
36
  """
21
37
  API for getting the enterprise admin insights.
22
38
  """
39
+
23
40
  authentication_classes = (JwtAuthentication,)
24
- http_method_names = ['get']
41
+ http_method_names = ["get"]
25
42
 
26
- @permission_required('can_access_enterprise', fn=lambda request, enterprise_id: enterprise_id)
43
+ @permission_required(
44
+ "can_access_enterprise", fn=lambda request, enterprise_id: enterprise_id
45
+ )
27
46
  def get(self, request, enterprise_id):
28
47
  """
29
48
  HTTP GET endpoint to retrieve the enterprise admin insights
@@ -33,16 +52,24 @@ class EnterpriseAdminInsightsView(APIView):
33
52
  learner_engagement = {}
34
53
 
35
54
  try:
36
- learner_progress = EnterpriseAdminLearnerProgress.objects.get(enterprise_customer_uuid=enterprise_id)
37
- learner_progress = serializers.EnterpriseAdminLearnerProgressSerializer(learner_progress).data
38
- response_data['learner_progress'] = learner_progress
55
+ learner_progress = EnterpriseAdminLearnerProgress.objects.get(
56
+ enterprise_customer_uuid=enterprise_id
57
+ )
58
+ learner_progress = serializers.EnterpriseAdminLearnerProgressSerializer(
59
+ learner_progress
60
+ ).data
61
+ response_data["learner_progress"] = learner_progress
39
62
  except EnterpriseAdminLearnerProgress.DoesNotExist:
40
63
  pass
41
64
 
42
65
  try:
43
- learner_engagement = EnterpriseAdminSummarizeInsights.objects.get(enterprise_customer_uuid=enterprise_id)
44
- learner_engagement = serializers.EnterpriseAdminSummarizeInsightsSerializer(learner_engagement).data
45
- response_data['learner_engagement'] = learner_engagement
66
+ learner_engagement = EnterpriseAdminSummarizeInsights.objects.get(
67
+ enterprise_customer_uuid=enterprise_id
68
+ )
69
+ learner_engagement = serializers.EnterpriseAdminSummarizeInsightsSerializer(
70
+ learner_engagement
71
+ ).data
72
+ response_data["learner_engagement"] = learner_engagement
46
73
  except EnterpriseAdminSummarizeInsights.DoesNotExist:
47
74
  pass
48
75
 
@@ -57,53 +84,178 @@ class EnterpriseAdminAnalyticsAggregatesView(APIView):
57
84
  """
58
85
  API for getting the enterprise admin analytics aggregates.
59
86
  """
87
+
60
88
  authentication_classes = (JwtAuthentication,)
61
- http_method_names = ['get']
89
+ http_method_names = ["get"]
62
90
 
63
- @permission_required('can_access_enterprise', fn=lambda request, enterprise_id: enterprise_id)
91
+ @permission_required(
92
+ "can_access_enterprise", fn=lambda request, enterprise_id: enterprise_id
93
+ )
64
94
  def get(self, request, enterprise_id):
65
95
  """
66
96
  HTTP GET endpoint to retrieve the enterprise admin aggregate data.
67
97
  """
68
- serializer = serializers.AdminAnalyticsAggregatesQueryParamsSerializer(data=request.GET)
98
+ serializer = serializers.AdminAnalyticsAggregatesQueryParamsSerializer(
99
+ data=request.GET
100
+ )
69
101
  serializer.is_valid(raise_exception=True)
70
102
 
71
103
  last_updated_at = fetch_max_enrollment_datetime()
72
- cache_expiry = last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
104
+ cache_expiry = (
105
+ last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
106
+ )
73
107
 
74
- enrollment = fetch_and_cache_enrollments_data(enterprise_id, cache_expiry).copy()
75
- engagement = fetch_and_cache_engagements_data(enterprise_id, cache_expiry).copy()
108
+ enrollment = fetch_and_cache_enrollments_data(
109
+ enterprise_id, cache_expiry
110
+ ).copy()
111
+ engagement = fetch_and_cache_engagements_data(
112
+ enterprise_id, cache_expiry
113
+ ).copy()
76
114
  # Use start and end date if provided by the client, if client has not provided then use
77
115
  # 1. minimum enrollment date from the data as the start_date
78
116
  # 2. today's date as the end_date
79
- start_date = serializer.data.get('start_date', enrollment.enterprise_enrollment_date.min())
117
+ start_date = serializer.data.get(
118
+ 'start_date', enrollment.enterprise_enrollment_date.min()
119
+ )
80
120
  end_date = serializer.data.get('end_date', datetime.now())
81
121
 
82
122
  # Date filtering.
83
123
  dff = date_filter(
84
- start=start_date, end=end_date, data_frame=enrollment.copy(), date_column='enterprise_enrollment_date'
124
+ start=start_date,
125
+ end=end_date,
126
+ data_frame=enrollment.copy(),
127
+ date_column='enterprise_enrollment_date',
85
128
  )
86
129
 
87
130
  enrolls = len(dff)
88
131
  courses = len(dff.course_key.unique())
89
132
 
90
- dff = date_filter(start=start_date, end=end_date, data_frame=enrollment.copy(), date_column='passed_date')
133
+ dff = date_filter(
134
+ start=start_date,
135
+ end=end_date,
136
+ data_frame=enrollment.copy(),
137
+ date_column='passed_date',
138
+ )
91
139
 
92
140
  completions = dff.has_passed.sum()
93
141
 
94
142
  # Date filtering.
95
- dff = date_filter(start=start_date, end=end_date, data_frame=engagement.copy(), date_column='activity_date')
143
+ dff = date_filter(
144
+ start=start_date,
145
+ end=end_date,
146
+ data_frame=engagement.copy(),
147
+ date_column='activity_date',
148
+ )
96
149
 
97
150
  hours = round(dff.learning_time_seconds.sum() / 60 / 60, 1)
98
151
  sessions = dff.is_engaged.sum()
99
152
 
100
- return Response(data={
101
- 'enrolls': enrolls,
102
- 'courses': courses,
103
- 'completions': completions,
104
- 'hours': hours,
105
- 'sessions': sessions,
106
- 'last_updated_at': last_updated_at.date() if last_updated_at else None,
107
- 'min_enrollment_date': enrollment.enterprise_enrollment_date.min().date(),
108
- 'max_enrollment_date': enrollment.enterprise_enrollment_date.max().date(),
109
- }, status=HTTP_200_OK)
153
+ return Response(
154
+ data={
155
+ 'enrolls': enrolls,
156
+ 'courses': courses,
157
+ 'completions': completions,
158
+ 'hours': hours,
159
+ 'sessions': sessions,
160
+ 'last_updated_at': last_updated_at.date() if last_updated_at else None,
161
+ 'min_enrollment_date': enrollment.enterprise_enrollment_date.min().date(),
162
+ 'max_enrollment_date': enrollment.enterprise_enrollment_date.max().date(),
163
+ },
164
+ status=HTTP_200_OK,
165
+ )
166
+
167
+
168
+ class EnterpriseAdminAnalyticsSkillsView(APIView):
169
+ """
170
+ API for getting the enterprise admin analytics skills data.
171
+ """
172
+ authentication_classes = (JwtAuthentication,)
173
+ http_method_names = ['get']
174
+
175
+ @permission_required(
176
+ 'can_access_enterprise', fn=lambda request, enterprise_id: enterprise_id
177
+ )
178
+ def get(self, request, enterprise_id):
179
+ """HTTP GET endpoint to retrieve the enterprise admin skills aggregated data.
180
+
181
+ Args:
182
+ request (HttpRequest): request object
183
+ enterprise_id (str): UUID of the enterprise customer
184
+
185
+ Returns:
186
+ response(HttpResponse): response object
187
+ """
188
+ serializer = serializers.AdminAnalyticsAggregatesQueryParamsSerializer(
189
+ data=request.GET
190
+ )
191
+ serializer.is_valid(raise_exception=True)
192
+
193
+ start_date = serializer.data.get("start_date")
194
+ end_date = serializer.data.get("end_date", datetime.now())
195
+
196
+ last_updated_at = fetch_max_enrollment_datetime()
197
+ cache_expiry = (
198
+ last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
199
+ )
200
+ skills = fetch_and_cache_skills_data(enterprise_id, cache_expiry).copy()
201
+
202
+ if request.GET.get("format") == "csv":
203
+ csv_data = get_top_skills_csv_data(skills, start_date, end_date)
204
+ response = HttpResponse(content_type='text/csv')
205
+ filename = f"Skills by Enrollment and Completion, {start_date} - {end_date}.csv"
206
+ response['Content-Disposition'] = f'attachment; filename="{filename}"'
207
+ csv_data.to_csv(path_or_buf=response, index=False)
208
+ return response
209
+
210
+ top_skills = get_skills_chart_data(
211
+ chart_type=ChartType.BUBBLE,
212
+ start_date=start_date,
213
+ end_date=end_date,
214
+ skills=skills,
215
+ )
216
+ top_skills_enrollments = get_skills_chart_data(
217
+ chart_type=ChartType.TOP_SKILLS_ENROLLMENT,
218
+ start_date=start_date,
219
+ end_date=end_date,
220
+ skills=skills,
221
+ )
222
+ top_skills_by_completions = get_skills_chart_data(
223
+ chart_type=ChartType.TOP_SKILLS_COMPLETION,
224
+ start_date=start_date,
225
+ end_date=end_date,
226
+ skills=skills,
227
+ )
228
+
229
+ response_data = {
230
+ "top_skills": top_skills.to_dict(orient="records"),
231
+ "top_skills_by_enrollments": top_skills_enrollments.to_dict(
232
+ orient="records"
233
+ ),
234
+ "top_skills_by_completions": top_skills_by_completions.to_dict(
235
+ orient="records"
236
+ ),
237
+ }
238
+
239
+ return Response(data=response_data, status=HTTP_200_OK)
240
+
241
+
242
+ class EnterpriseExecEdLCModulePerformanceViewSet(EnterpriseViewSetMixin, viewsets.ReadOnlyModelViewSet):
243
+ """
244
+ View to for getting enterprise exec ed learner module performance records.
245
+ """
246
+ serializer_class = serializers.EnterpriseExecEdLCModulePerformanceSerializer
247
+ filter_backends = (filters.OrderingFilter, filters.SearchFilter)
248
+ ordering_fields = '__all__'
249
+ ordering = ('last_access',)
250
+ search_fields = (
251
+ 'username',
252
+ 'course_name'
253
+ )
254
+
255
+ def get_queryset(self):
256
+ """
257
+ Return the queryset of EnterpriseExecEdLCModulePerformance objects.
258
+ """
259
+ return EnterpriseExecEdLCModulePerformance.objects.filter(
260
+ enterprise_customer_uuid=self.kwargs['enterprise_id'],
261
+ )
@@ -0,0 +1,87 @@
1
+ # Generated by Django 4.2.14 on 2024-08-08 07:43
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('enterprise_data', '0040_auto_20240718_0536_squashed_0043_alter_enterpriselearnerenrollment_enterprise_enrollment_id'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.CreateModel(
14
+ name='EnterpriseExecEdLCModulePerformance',
15
+ fields=[
16
+ ('module_performance_unique_id', models.CharField(max_length=350, primary_key=True, serialize=False)),
17
+ ('registration_id', models.PositiveIntegerField(null=True)),
18
+ ('subsidy_transaction_id', models.UUIDField(null=True)),
19
+ ('enterprise_customer_uuid', models.UUIDField(db_index=True, null=True)),
20
+ ('ocm_lms_user_id', models.PositiveIntegerField(null=True)),
21
+ ('is_internal_subsidy', models.BooleanField(null=True)),
22
+ ('ocm_enrollment_id', models.PositiveIntegerField(null=True)),
23
+ ('ocm_courserun_key', models.CharField(max_length=255, null=True)),
24
+ ('university_name', models.CharField(max_length=500, null=True)),
25
+ ('university_abbreviation', models.CharField(max_length=255, null=True)),
26
+ ('partner_short_name', models.CharField(max_length=255, null=True)),
27
+ ('university_country', models.CharField(max_length=255, null=True)),
28
+ ('school', models.CharField(max_length=500, null=True)),
29
+ ('faculty', models.CharField(max_length=500, null=True)),
30
+ ('department', models.CharField(max_length=500, null=True)),
31
+ ('course_code', models.PositiveIntegerField(null=True)),
32
+ ('course_name', models.CharField(db_index=True, max_length=500)),
33
+ ('course_abbreviation', models.CharField(max_length=128, null=True)),
34
+ ('course_abbreviation_short', models.CharField(max_length=64, null=True)),
35
+ ('course_type', models.CharField(max_length=128, null=True)),
36
+ ('subject_vertical', models.CharField(max_length=128, null=True)),
37
+ ('presentation_abbreviation', models.CharField(max_length=128, null=True)),
38
+ ('presentation_name', models.CharField(max_length=500, null=True)),
39
+ ('presentation_code', models.PositiveIntegerField(null=True)),
40
+ ('presentation_close_date', models.DateField(null=True)),
41
+ ('presentation_start_date', models.DateField(null=True)),
42
+ ('promotion_code', models.CharField(max_length=255, null=True)),
43
+ ('promotion_category_name', models.CharField(max_length=255, null=True)),
44
+ ('product_type', models.CharField(max_length=128, null=True)),
45
+ ('product_life_cycle_status', models.CharField(max_length=128, null=True)),
46
+ ('enrolment_id', models.PositiveIntegerField(null=True)),
47
+ ('olc_user_id', models.PositiveIntegerField(null=True)),
48
+ ('first_name', models.CharField(max_length=255, null=True)),
49
+ ('last_name', models.CharField(max_length=255, null=True)),
50
+ ('username', models.CharField(db_index=True, max_length=255)),
51
+ ('status', models.CharField(max_length=128, null=True)),
52
+ ('company_name', models.CharField(max_length=255, null=True)),
53
+ ('module_number', models.PositiveIntegerField(null=True)),
54
+ ('module_name', models.CharField(max_length=255, null=True)),
55
+ ('module_1_release_date', models.DateField(null=True)),
56
+ ('last_module_release_date', models.DateField(null=True)),
57
+ ('last_module_end_date', models.DateField(null=True)),
58
+ ('final_mark', models.DecimalField(decimal_places=2, max_digits=38, null=True)),
59
+ ('assign_grade', models.DecimalField(decimal_places=2, max_digits=38, null=True)),
60
+ ('last_access', models.DateField(null=True)),
61
+ ('all_activities_completed_count', models.PositiveIntegerField(null=True)),
62
+ ('all_activities_total_count', models.PositiveIntegerField(null=True)),
63
+ ('percentage_completed_activities', models.DecimalField(decimal_places=2, max_digits=38, null=True)),
64
+ ('extensions_requested', models.PositiveIntegerField(null=True)),
65
+ ('module_grade', models.DecimalField(decimal_places=2, max_digits=38, null=True)),
66
+ ('log_viewed', models.PositiveIntegerField(null=True)),
67
+ ('hours_online', models.DecimalField(decimal_places=2, max_digits=38, null=True)),
68
+ ('orientation_module_accessed', models.CharField(max_length=128, null=True)),
69
+ ('graded_activities_completed_count', models.PositiveIntegerField(null=True)),
70
+ ('graded_activities_total_count', models.PositiveIntegerField(null=True)),
71
+ ('percentage_completed_graded_activities', models.DecimalField(decimal_places=2, max_digits=38, null=True)),
72
+ ('assessment_activities_completed_count', models.PositiveIntegerField(null=True)),
73
+ ('assessment_activities_total_count', models.PositiveIntegerField(null=True)),
74
+ ('course_material_activities_completed_count', models.PositiveIntegerField(null=True)),
75
+ ('course_material_activities_total_count', models.PositiveIntegerField(null=True)),
76
+ ('discussion_forum_activities_completed_count', models.PositiveIntegerField(null=True)),
77
+ ('discussion_forum_activities_total_count', models.PositiveIntegerField(null=True)),
78
+ ('pass_grade', models.DecimalField(decimal_places=2, max_digits=38, null=True)),
79
+ ],
80
+ options={
81
+ 'verbose_name': 'Exec Ed LC Module Performance',
82
+ 'verbose_name_plural': 'Exec Ed LC Module Performance',
83
+ 'db_table': 'exec_ed_lc_module_performance',
84
+ 'ordering': ['last_access'],
85
+ },
86
+ ),
87
+ ]
enterprise_data/models.py CHANGED
@@ -434,3 +434,97 @@ class EnterpriseUser(models.Model):
434
434
  Return uniquely identifying string representation.
435
435
  """
436
436
  return self.__str__()
437
+
438
+
439
+ class EnterpriseExecEdLCModulePerformance(models.Model):
440
+ """
441
+ Model for Exec Ed LC Module Performance.
442
+ """
443
+
444
+ objects = EnterpriseReportingModelManager()
445
+
446
+ class Meta:
447
+ app_label = 'enterprise_data'
448
+ db_table = 'exec_ed_lc_module_performance'
449
+ verbose_name = _("Exec Ed LC Module Performance")
450
+ verbose_name_plural = _("Exec Ed LC Module Performance")
451
+
452
+ module_performance_unique_id = models.CharField(max_length=350, primary_key=True)
453
+ registration_id = models.PositiveIntegerField(null=True)
454
+ subsidy_transaction_id = models.UUIDField(null=True)
455
+ enterprise_customer_uuid = models.UUIDField(db_index=True, null=True)
456
+ ocm_lms_user_id = models.PositiveIntegerField(null=True)
457
+ is_internal_subsidy = models.BooleanField(null=True)
458
+ ocm_enrollment_id = models.PositiveIntegerField(null=True)
459
+ ocm_courserun_key = models.CharField(max_length=255, null=True)
460
+ university_name = models.CharField(max_length=500, null=True)
461
+ university_abbreviation = models.CharField(max_length=255, null=True)
462
+ partner_short_name = models.CharField(max_length=255, null=True)
463
+ university_country = models.CharField(max_length=255, null=True)
464
+ school = models.CharField(max_length=500, null=True)
465
+ faculty = models.CharField(max_length=500, null=True)
466
+ department = models.CharField(max_length=500, null=True)
467
+ course_code = models.PositiveIntegerField(null=True)
468
+ course_name = models.CharField(max_length=500, db_index=True)
469
+ course_abbreviation = models.CharField(max_length=128, null=True)
470
+ course_abbreviation_short = models.CharField(max_length=64, null=True)
471
+ course_type = models.CharField(max_length=128, null=True)
472
+ subject_vertical = models.CharField(max_length=128, null=True)
473
+ presentation_abbreviation = models.CharField(max_length=128, null=True)
474
+ presentation_name = models.CharField(max_length=500, null=True)
475
+ presentation_code = models.PositiveIntegerField(null=True)
476
+ presentation_close_date = models.DateField(null=True)
477
+ presentation_start_date = models.DateField(null=True)
478
+ promotion_code = models.CharField(max_length=255, null=True)
479
+ promotion_category_name = models.CharField(max_length=255, null=True)
480
+ product_type = models.CharField(max_length=128, null=True)
481
+ product_life_cycle_status = models.CharField(max_length=128, null=True)
482
+ enrolment_id = models.PositiveIntegerField(null=True)
483
+ olc_user_id = models.PositiveIntegerField(null=True)
484
+ first_name = models.CharField(max_length=255, null=True)
485
+ last_name = models.CharField(max_length=255, null=True)
486
+ username = models.CharField(max_length=255, db_index=True)
487
+ status = models.CharField(max_length=128, null=True)
488
+ company_name = models.CharField(max_length=255, null=True)
489
+ module_number = models.PositiveIntegerField(null=True)
490
+ module_name = models.CharField(max_length=255, null=True)
491
+ module_1_release_date = models.DateField(null=True)
492
+ last_module_release_date = models.DateField(null=True)
493
+ last_module_end_date = models.DateField(null=True)
494
+ final_mark = models.DecimalField(max_digits=38, decimal_places=2, null=True)
495
+ assign_grade = models.DecimalField(max_digits=38, decimal_places=2, null=True)
496
+ last_access = models.DateField(null=True)
497
+ all_activities_completed_count = models.PositiveIntegerField(null=True)
498
+ all_activities_total_count = models.PositiveIntegerField(null=True)
499
+ percentage_completed_activities = models.DecimalField(max_digits=38, decimal_places=2, null=True)
500
+ extensions_requested = models.PositiveIntegerField(null=True)
501
+ module_grade = models.DecimalField(max_digits=38, decimal_places=2, null=True)
502
+ log_viewed = models.PositiveIntegerField(null=True)
503
+ hours_online = models.DecimalField(max_digits=38, decimal_places=2, null=True)
504
+ orientation_module_accessed = models.CharField(max_length=128, null=True)
505
+ graded_activities_completed_count = models.PositiveIntegerField(null=True)
506
+ graded_activities_total_count = models.PositiveIntegerField(null=True)
507
+ percentage_completed_graded_activities = models.DecimalField(max_digits=38, decimal_places=2, null=True)
508
+ assessment_activities_completed_count = models.PositiveIntegerField(null=True)
509
+ assessment_activities_total_count = models.PositiveIntegerField(null=True)
510
+ course_material_activities_completed_count = models.PositiveIntegerField(null=True)
511
+ course_material_activities_total_count = models.PositiveIntegerField(null=True)
512
+ discussion_forum_activities_completed_count = models.PositiveIntegerField(null=True)
513
+ discussion_forum_activities_total_count = models.PositiveIntegerField(null=True)
514
+ pass_grade = models.DecimalField(max_digits=38, decimal_places=2, null=True)
515
+
516
+ def __str__(self):
517
+ """
518
+ Return a human-readable string representation of the object.
519
+ """
520
+ return "<EnterpriseExecEdLCModulePerformance User {user} of {enterprise} in module {module}>".format(
521
+ user=self.ocm_lms_user_id,
522
+ enterprise=self.enterprise_customer_uuid,
523
+ module=self.module_number,
524
+ )
525
+
526
+ def __repr__(self):
527
+ """
528
+ Return uniquely identifying string representation.
529
+ """
530
+ return self.__str__()
@@ -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"