edx-enterprise-data 9.7.1__py3-none-any.whl → 9.7.5__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: 9.7.1
3
+ Version: 9.7.5
4
4
  Summary: Enterprise Reporting
5
5
  Home-page: https://github.com/openedx/edx-enterprise-data
6
6
  Author: edX
@@ -1,4 +1,4 @@
1
- enterprise_data/__init__.py,sha256=beJW1KjpyzsHXlYoxVmfUymgU7McJaqZjGVzmukVbWY,123
1
+ enterprise_data/__init__.py,sha256=QmGefZCq3lk2PDE_6qDozrF3mVOtsbiF55rRwfrJG0E,123
2
2
  enterprise_data/apps.py,sha256=aF6hZwDfI2oWj95tUTm_2ikHueQj-jLj-u0GrgzpsQI,414
3
3
  enterprise_data/clients.py,sha256=GvQupy5TVYfO_IKC3yzXSAgNP54r-PtIjidM5ws9Iks,3947
4
4
  enterprise_data/constants.py,sha256=uCKjfpdlMYFZJsAj3n9RMw4Cmg5_6s3NuwocO-fch3s,238
@@ -8,19 +8,19 @@ enterprise_data/paginators.py,sha256=YPrC5TeXFt-ymenT2H8H2nCbDCnAzJQlH9kFPElRxWE
8
8
  enterprise_data/renderers.py,sha256=d_bJZjeUTyHRBBtpCcslrTyldv6IMYQ_QW-GWijwGHU,3026
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=Hrmz6uB8LsPSrau-lK0vvBWzRKTMl_Tygxv8qWG2NNw,2471
11
+ enterprise_data/utils.py,sha256=sDrpBd62DpybCV41QCxRUaCuvch3qKjEhfUp9cA_GV0,2952
12
12
  enterprise_data/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  enterprise_data/admin_analytics/constants.py,sha256=-6uLAq5DUeA_rv5eUb9SeqlG3iVWV30qUS8asbK4430,160
14
14
  enterprise_data/admin_analytics/data_loaders.py,sha256=NixI-4M3D4MnI279x5hqqTw84uKpQy0TRib_g-0Bt5Q,726
15
15
  enterprise_data/admin_analytics/database/__init__.py,sha256=vNSWKf2VV5xMegN7htJJtxtQEb0ASLC6frE2w0ZpYpE,104
16
16
  enterprise_data/admin_analytics/database/utils.py,sha256=5u-d6ZQW95mF_r4bH8Xdi7DgpYAuDFOG_q0P-bjKXHU,1712
17
17
  enterprise_data/admin_analytics/database/queries/__init__.py,sha256=IC5TLOr_GnydbrVbl2mWhwO3aUbYeHuDmfPTLmwGhZA,218
18
- enterprise_data/admin_analytics/database/queries/fact_engagement_admin_dash.py,sha256=r62hjL680PEC-qKE7gty5oHAtluQdLsg1QoSuabhKOc,7289
19
- enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py,sha256=6EX0W3RWRzSrpIbqFJvStnj5Rkfzi9KyFP0UNmb75pU,8814
20
- enterprise_data/admin_analytics/database/queries/skills_daily_rollup_admin_dash.py,sha256=PgWwvtVCK5lbiq6z44lH0fwbkdWYukhyXZL9X8lNWCY,4099
18
+ enterprise_data/admin_analytics/database/queries/fact_engagement_admin_dash.py,sha256=eLy9GT0gFGLbw8yZZdEaeCAw0pTEUsy0-Ud5g2T9T80,10836
19
+ enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py,sha256=w30BiDOcKzV_FCdtxIG-_7zSCu5f3x5pgBRFCGjHu04,11011
20
+ enterprise_data/admin_analytics/database/queries/skills_daily_rollup_admin_dash.py,sha256=ZaJPghTrQvBGsFxfpeR8EE45ujtaYI9R_xkoDDqD2So,4269
21
21
  enterprise_data/admin_analytics/database/tables/__init__.py,sha256=Z-c3P9hqR-dC9uYKe63qHkQG9Nms8cLE2jRN-4jeMM0,289
22
22
  enterprise_data/admin_analytics/database/tables/base.py,sha256=1KyKsC18pW3m-5U-T6pdt5rIwsz6Wp3QFFbD3r6L6YQ,395
23
- enterprise_data/admin_analytics/database/tables/fact_engagement_admin_dash.py,sha256=8aJsMsnMw67M6NGeUtWbQ-2g-OwN69XSAlD7KiMfOoA,10523
23
+ enterprise_data/admin_analytics/database/tables/fact_engagement_admin_dash.py,sha256=VMUGwfrUsYd_NnxEDNBovAmHwhdTCSImGA6JjHzknhk,12959
24
24
  enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py,sha256=cRRBFRc2p54BkW_h7GuUud-gWzJrqKep9dYgxOf7tIY,11741
25
25
  enterprise_data/admin_analytics/database/tables/skills_daily_rollup_admin_dash.py,sha256=3xNwSi0wfCyBHcXPd6-9Ujs1NUm8kmZRg_gPrZzp9nQ,3233
26
26
  enterprise_data/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -33,10 +33,10 @@ enterprise_data/api/v1/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjW
33
33
  enterprise_data/api/v1/serializers.py,sha256=oS09fMJyb3DkSc2y5TH5Yknd9NjYrlmWMRPNqBZ741U,10902
34
34
  enterprise_data/api/v1/urls.py,sha256=IpOyS9UWuyip6fw6gtrxBw9SZLOdxh3sQ2j0Gdk4eOw,4180
35
35
  enterprise_data/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
- enterprise_data/api/v1/views/analytics_completions.py,sha256=xT9ywSRNjbQNLJjsm2N4cCyAXeibXY1800uirDn1prg,6260
37
- enterprise_data/api/v1/views/analytics_engagements.py,sha256=TK6i0tSk0QObda0ysunEFzYrlsvLvSEypJRLJEi7JCk,6298
38
- enterprise_data/api/v1/views/analytics_enrollments.py,sha256=6QRjzWapvc6jP3rv2QkGzfZMR2kRcsKqY3xRkvpIyRQ,6282
39
- enterprise_data/api/v1/views/analytics_leaderboard.py,sha256=zOkvoxX5YlBZ0aLrvpWrdXJmQuAIWKb9Q5DReBbRT8k,4108
36
+ enterprise_data/api/v1/views/analytics_completions.py,sha256=esFbJ5q8ssnm2Mfbc3rZXtiGHF-MeM4KQ4Ft3N7wwHU,6260
37
+ enterprise_data/api/v1/views/analytics_engagements.py,sha256=Yo-bpA-0xOQHUPTFF0jHWxjm6KpSC-l2nGxhYX0ajBk,6298
38
+ enterprise_data/api/v1/views/analytics_enrollments.py,sha256=hw87VZ0hFWpwf3QEHFn9cUgDy2s7SzsXt6Rf9TaNsS0,6282
39
+ enterprise_data/api/v1/views/analytics_leaderboard.py,sha256=3dyo7_OhyGEEeibemBrRsUOo0jbM4LbDgV5gw3YnVig,4186
40
40
  enterprise_data/api/v1/views/base.py,sha256=Kkmd5zgEBAhvwS_GoGXSK6lgbDNwSPioYn-QbnizI3w,3416
41
41
  enterprise_data/api/v1/views/enterprise_admin.py,sha256=DyN6RS4qY8sgKNIvcx1nRmSGuPm2pLYHZeh5YHXlH2w,8209
42
42
  enterprise_data/api/v1/views/enterprise_learner.py,sha256=NqI_Tlz5v3p4fYZe2RRCg54AizslZdHB3Ckh8YQrhIM,18163
@@ -54,14 +54,14 @@ enterprise_data/management/commands/create_enterprise_learner_enrollment_lpr_v1.
54
54
  enterprise_data/management/commands/create_enterprise_learner_lpr_v1.py,sha256=bUYmZHA3yK3ZBPhV0wkpRDgH_Q2b5rVQnwSp2hRmh28,1799
55
55
  enterprise_data/management/commands/create_enterprise_offer.py,sha256=0R1eEKWTCGjod4I8qBH2UBD-erqj5mFtM_DG5Vxet_0,1150
56
56
  enterprise_data/management/commands/create_enterprise_user.py,sha256=V_kvOSPZ1DXfAdF1W3AwAtavEYjdYaHBjjfzndZP8lk,1498
57
- enterprise_data/management/commands/pre_warm_analytics_cache.py,sha256=5AznzVrU_tGxqtVLreOhnJ7iJUKc388YSyctTowWWCs,8276
57
+ enterprise_data/management/commands/pre_warm_analytics_cache.py,sha256=3MCThZiZgfveD3NqFt95WTJ9LFTYxN8ZYbqWF1JinI8,8327
58
58
  enterprise_data/management/commands/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
59
  enterprise_data/management/commands/tests/test_create_dummy_data_lpr_v1.py,sha256=wt9fqAFKQVTqllpZ42dch-n31JavUifUIB9CKNYcnYM,1086
60
60
  enterprise_data/management/commands/tests/test_create_enterprise_enrollment.py,sha256=5CABLk8qAx8RP8mrFtnbJ4-xVkf9-5Mq6iQcx8jBfFc,1742
61
61
  enterprise_data/management/commands/tests/test_create_enterprise_learner_enrollment_lpr_v1.py,sha256=FzOBHcd1FfSVN5AI8oAKvmqj-W8Y-V7x9yJx9R1WrLQ,3845
62
62
  enterprise_data/management/commands/tests/test_create_enterprise_learner_lpr_v1.py,sha256=P7wyyaZ4LMWv1umpCThp4By5DWjtI7kYAmY0AeuY2XI,1637
63
63
  enterprise_data/management/commands/tests/test_create_enterprise_user.py,sha256=0uxG332-jYpaxUIrMtIVRTwvRfxTWEQBIwZlIQO7f2g,1469
64
- enterprise_data/management/commands/tests/test_pre_warm_analytics_cache.py,sha256=zjib-l9kNJiRG8EHxdQa66C8S0qaC5f4qb03-ddA7Rg,1997
64
+ enterprise_data/management/commands/tests/test_pre_warm_analytics_cache.py,sha256=Nbwsl5CYMbi5cN2IwmUtTs5b_MeU6T0lquk3x8Tuh_4,2108
65
65
  enterprise_data/migrations/0001_initial.py,sha256=g2fNJGLxELthmYrJlPB7cBxbsLWB6u-MotQvE3t95ww,2358
66
66
  enterprise_data/migrations/0002_auto_20180430_1358.py,sha256=roRSgWy2p4hFefNFaajg-q_e8_5FHZih_ou1dLTwWQk,600
67
67
  enterprise_data/migrations/0003_auto_20180501_0603.py,sha256=N9C5QcdRMfzTycO6xqdi7WPkwrktn3y3NEsm30L4pS8,415
@@ -136,7 +136,7 @@ enterprise_data_roles/admin.py,sha256=QNP0VeWE092vZzpyxOA5UJK1nNGl5e71B1J0RCwo_n
136
136
  enterprise_data_roles/apps.py,sha256=nKi8TyuQ5Q6WGtKs5QeXvUTc3N-YQjKhyBnm2EM3Bng,260
137
137
  enterprise_data_roles/constants.py,sha256=7yHmbAyqeXNX-lg3AC1caFsv97mouAS4NFIrHhwGgQ0,476
138
138
  enterprise_data_roles/models.py,sha256=b4weAWOfQ1sMnODXQBcq4IsclR2NYWWxuOZ95fKnEfs,1679
139
- enterprise_data_roles/rules.py,sha256=AaONKA91ge-AZ1DsljekobouuCkepDzlfH4O1ece3KM,1948
139
+ enterprise_data_roles/rules.py,sha256=4W_qmVb3lQzTJqKqNXSRhW6cjKoZTygnj_4rXB_zljM,1900
140
140
  enterprise_data_roles/migrations/0001_initial.py,sha256=rXIP0mgd5w71bCvGEG2wCaHwFkCekM8nGLyUt9-3gaI,2083
141
141
  enterprise_data_roles/migrations/0002_add_enterprise_data_feature_roles.py,sha256=aDGjqYznGT9DV-dAVVIem9kNPNmcUmafinJIyd6FcJw,947
142
142
  enterprise_data_roles/migrations/0003_add_role_based_access_control_switch.py,sha256=7UAMcY6270OO05V9hDfM25lRFHp5v5DRYXdZpiS9rh4,955
@@ -171,8 +171,8 @@ enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=WtL-RqGgu2x5PP
171
171
  enterprise_reporting/tests/test_utils.py,sha256=Zt_TA0LVb-B6fQGkUkAKKVlUKKnQh8jnw1US1jKe7g8,9493
172
172
  enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
173
173
  enterprise_reporting/tests/utils.py,sha256=xms2LM7DV3wczXEfctOK1ddel1EE0J_YSr17UzbCDy4,1401
174
- edx_enterprise_data-9.7.1.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
175
- edx_enterprise_data-9.7.1.dist-info/METADATA,sha256=sHPXXn_ikyBqlaGAx2XbPGCFGqbk-ggqan5fQn_uCtk,1569
176
- edx_enterprise_data-9.7.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
177
- edx_enterprise_data-9.7.1.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
178
- edx_enterprise_data-9.7.1.dist-info/RECORD,,
174
+ edx_enterprise_data-9.7.5.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
175
+ edx_enterprise_data-9.7.5.dist-info/METADATA,sha256=G13Kc9ak6jRBohemgmSPxyQJDL_GSNtHXGJCR3Eo4xY,1569
176
+ edx_enterprise_data-9.7.5.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
177
+ edx_enterprise_data-9.7.5.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
178
+ edx_enterprise_data-9.7.5.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__ = "9.7.1"
5
+ __version__ = "9.7.5"
@@ -48,7 +48,7 @@ class FactEngagementAdminDashQueries:
48
48
  """
49
49
 
50
50
  @staticmethod
51
- def get_top_courses_by_engagement_query(record_count=20):
51
+ def get_top_courses_by_engagement_query(record_count=10):
52
52
  """
53
53
  Get the query to fetch the learning time in hours by courses.
54
54
 
@@ -61,16 +61,40 @@ class FactEngagementAdminDashQueries:
61
61
  (str): Query to fetch the learning time in hours by courses for the top courses by user engagement.
62
62
  """
63
63
  return f"""
64
- SELECT course_key, course_title, enroll_type, SUM(learning_time_seconds)/3600 as learning_time_hours
65
- FROM fact_enrollment_engagement_day_admin_dash
66
- WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
64
+ WITH filtered_data AS (
65
+ SELECT
66
+ course_key,
67
+ course_title,
68
+ enroll_type,
69
+ (learning_time_seconds / 60.0 / 60.0) AS learning_time_hours,
70
+ activity_date
71
+ FROM fact_enrollment_engagement_day_admin_dash
72
+ WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
67
73
  activity_date BETWEEN %(start_date)s AND %(end_date)s
68
- GROUP BY course_key, course_title, enroll_type
69
- ORDER BY learning_time_hours DESC LIMIT {record_count};
74
+ ),
75
+ top_10_courses AS (
76
+ SELECT
77
+ course_key,
78
+ SUM(learning_time_hours) as total_learning_time
79
+ FROM filtered_data
80
+ GROUP BY course_key
81
+ ORDER BY total_learning_time DESC
82
+ LIMIT {record_count}
83
+ )
84
+ SELECT
85
+ d.course_key,
86
+ d.course_title,
87
+ d.enroll_type,
88
+ ROUND(SUM(d.learning_time_hours)) AS learning_time_hours
89
+ FROM filtered_data d
90
+ JOIN top_10_courses tc
91
+ ON d.course_key = tc.course_key
92
+ GROUP BY d.course_key, d.course_title, d.enroll_type
93
+ ORDER BY total_learning_time DESC;
70
94
  """
71
95
 
72
96
  @staticmethod
73
- def get_top_subjects_by_engagement_query(record_count=20):
97
+ def get_top_subjects_by_engagement_query(record_count=10):
74
98
  """
75
99
  Get the query to fetch the learning time in hours by subjects.
76
100
 
@@ -83,12 +107,34 @@ class FactEngagementAdminDashQueries:
83
107
  (str): Query to fetch the learning time in hours by subjects for the top subjects by user engagement.
84
108
  """
85
109
  return f"""
86
- SELECT course_subject, enroll_type, SUM(learning_time_seconds)/3600 as learning_time_hours
87
- FROM fact_enrollment_engagement_day_admin_dash
88
- WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
110
+ WITH filtered_data AS (
111
+ SELECT
112
+ course_subject,
113
+ enroll_type,
114
+ (learning_time_seconds / 60.0 / 60.0) AS learning_time_hours,
115
+ activity_date
116
+ FROM fact_enrollment_engagement_day_admin_dash
117
+ WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
89
118
  activity_date BETWEEN %(start_date)s AND %(end_date)s
90
- GROUP BY course_subject, enroll_type
91
- ORDER BY learning_time_hours DESC LIMIT {record_count};
119
+ ),
120
+ top_10_subjects AS (
121
+ SELECT
122
+ course_subject,
123
+ SUM(learning_time_hours) as total_learning_time
124
+ FROM filtered_data
125
+ GROUP BY course_subject
126
+ ORDER BY total_learning_time DESC
127
+ LIMIT {record_count}
128
+ )
129
+ SELECT
130
+ d.course_subject,
131
+ d.enroll_type,
132
+ ROUND(SUM(d.learning_time_hours)) AS learning_time_hours
133
+ FROM filtered_data d
134
+ JOIN top_10_subjects ts
135
+ ON d.course_subject = ts.course_subject
136
+ GROUP BY d.course_subject, d.enroll_type
137
+ ORDER BY total_learning_time DESC;
92
138
  """
93
139
 
94
140
  @staticmethod
@@ -163,6 +209,55 @@ class FactEngagementAdminDashQueries:
163
209
  GROUP BY email;
164
210
  """
165
211
 
212
+ @staticmethod
213
+ def get_engagement_data_for_leaderboard_null_email_only_query():
214
+ """
215
+ Get the query to fetch the engagement data for leaderboard for NULL emails only.
216
+
217
+ Query should fetch the engagement data for like learning time, session length of
218
+ the enterprise learners to show in the leaderboard.
219
+
220
+ Returns:
221
+ (str): Query to fetch the engagement data for leaderboard.
222
+ """
223
+ return """
224
+ SELECT
225
+ email,
226
+ ROUND(SUM(learning_time_seconds) / 3600, 1) as learning_time_hours,
227
+ SUM(is_engaged) as session_count,
228
+ CASE
229
+ WHEN SUM(is_engaged) = 0 THEN 0.0
230
+ ELSE ROUND(SUM(learning_time_seconds) / 3600 / SUM(is_engaged), 1)
231
+ END AS average_session_length
232
+ FROM fact_enrollment_engagement_day_admin_dash
233
+ WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
234
+ (activity_date BETWEEN %(start_date)s AND %(end_date)s) AND
235
+ is_engaged = 1 AND
236
+ email is NULL
237
+ GROUP BY email;
238
+ """
239
+
240
+ @staticmethod
241
+ def get_completion_data_for_leaderboard_null_email_only_query():
242
+ """
243
+ Get the query to fetch the completions data for leaderboard for NULL emails.
244
+
245
+ Query should fetch the completion data for like course completion count of
246
+ the enterprise learners to show in the leaderboard.
247
+
248
+ Returns:
249
+ (list<str>): Query to fetch the completions data for leaderboard.
250
+ """
251
+ return """
252
+ SELECT email, count(course_key) as course_completion_count
253
+ FROM fact_enrollment_admin_dash
254
+ WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
255
+ (passed_date BETWEEN %(start_date)s AND %(end_date)s) AND
256
+ has_passed = 1 AND
257
+ email is NULL
258
+ GROUP BY email;
259
+ """
260
+
166
261
  @staticmethod
167
262
  def get_leaderboard_data_count_query():
168
263
  """
@@ -98,7 +98,7 @@ class FactEnrollmentAdminDashQueries:
98
98
  """
99
99
 
100
100
  @staticmethod
101
- def get_top_courses_by_enrollments_query(record_count=20):
101
+ def get_top_courses_by_enrollments_query(record_count=10):
102
102
  """
103
103
  Get the query to fetch the enrollment count by courses.
104
104
 
@@ -108,15 +108,32 @@ class FactEnrollmentAdminDashQueries:
108
108
  record_count (int): Number of records to fetch.
109
109
  """
110
110
  return f"""
111
- SELECT course_key, course_title , enroll_type, count(course_key) as enrollment_count
112
- FROM fact_enrollment_admin_dash
113
- WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
111
+ WITH filtered_data AS (
112
+ SELECT *
113
+ FROM fact_enrollment_admin_dash
114
+ WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
114
115
  enterprise_enrollment_date BETWEEN %(start_date)s AND %(end_date)s
115
- GROUP BY course_key, course_title, enroll_type ORDER BY enrollment_count DESC LIMIT {record_count};
116
+ ),
117
+ top_10_courses AS (
118
+ SELECT course_key
119
+ FROM filtered_data
120
+ GROUP BY course_key
121
+ ORDER BY COUNT(*) DESC
122
+ LIMIT {record_count}
123
+ )
124
+
125
+ SELECT
126
+ d.course_key,
127
+ d.enroll_type,
128
+ COUNT(*) AS enrollment_count
129
+ FROM filtered_data d
130
+ JOIN top_10_courses tc
131
+ ON d.course_key = tc.course_key
132
+ GROUP BY d.course_key, d.enroll_type;
116
133
  """
117
134
 
118
135
  @staticmethod
119
- def get_top_subjects_by_enrollments_query(record_count=20):
136
+ def get_top_subjects_by_enrollments_query(record_count=10):
120
137
  """
121
138
  Get the query to fetch the enrollment count by subjects.
122
139
 
@@ -126,11 +143,27 @@ class FactEnrollmentAdminDashQueries:
126
143
  record_count (int): Number of records to fetch.
127
144
  """
128
145
  return f"""
129
- SELECT course_subject, enroll_type, count(course_subject) enrollment_count
130
- FROM fact_enrollment_admin_dash
131
- WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
146
+ WITH filtered_data AS (
147
+ SELECT *
148
+ FROM fact_enrollment_admin_dash
149
+ WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
132
150
  enterprise_enrollment_date BETWEEN %(start_date)s AND %(end_date)s
133
- GROUP BY course_subject, enroll_type ORDER BY enrollment_count DESC LIMIT {record_count};
151
+ ),
152
+ top_10_subjects AS (
153
+ SELECT course_subject
154
+ FROM filtered_data
155
+ GROUP BY course_subject
156
+ ORDER BY COUNT(*) DESC
157
+ LIMIT {record_count}
158
+ )
159
+ SELECT
160
+ d.course_subject,
161
+ d.enroll_type,
162
+ COUNT(*) AS enrollment_count
163
+ FROM filtered_data d
164
+ JOIN top_10_subjects ts
165
+ ON d.course_subject = ts.course_subject
166
+ GROUP BY d.course_subject, d.enroll_type;
134
167
  """
135
168
 
136
169
  @staticmethod
@@ -162,7 +195,7 @@ class FactEnrollmentAdminDashQueries:
162
195
  """
163
196
 
164
197
  @staticmethod
165
- def get_top_courses_by_completions_query(record_count=20):
198
+ def get_top_courses_by_completions_query(record_count=10):
166
199
  """
167
200
  Get the query to fetch the completion count by courses.
168
201
 
@@ -175,17 +208,40 @@ class FactEnrollmentAdminDashQueries:
175
208
  (str): Query to fetch the enrollment count by courses for the top courses by enrollment count.
176
209
  """
177
210
  return f"""
178
- SELECT course_key, course_title, enroll_type, count(course_key) as completion_count
179
- FROM fact_enrollment_admin_dash
180
- WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
181
- has_passed=1 AND
211
+ WITH filtered_data AS (
212
+ SELECT
213
+ course_key,
214
+ course_title,
215
+ enroll_type,
216
+ passed_date
217
+ FROM fact_enrollment_admin_dash
218
+ WHERE has_passed = 1 AND
219
+ enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
182
220
  passed_date BETWEEN %(start_date)s AND %(end_date)s
183
- GROUP BY course_key, course_title, enroll_type
184
- ORDER BY completion_count DESC LIMIT {record_count};
221
+ ),
222
+ top_10_courses AS (
223
+ SELECT
224
+ course_key,
225
+ COUNT(*) AS total_completion_count
226
+ FROM filtered_data
227
+ GROUP BY course_key
228
+ ORDER BY total_completion_count DESC
229
+ LIMIT {record_count}
230
+ )
231
+ SELECT
232
+ d.course_key,
233
+ d.course_title,
234
+ d.enroll_type,
235
+ COUNT(*) AS completion_count
236
+ FROM filtered_data d
237
+ JOIN top_10_courses tc
238
+ ON d.course_key = tc.course_key
239
+ GROUP BY d.course_key, d.course_title, d.enroll_type
240
+ ORDER BY total_completion_count DESC;
185
241
  """
186
242
 
187
243
  @staticmethod
188
- def get_top_subjects_by_completions_query(record_count=20):
244
+ def get_top_subjects_by_completions_query(record_count=10):
189
245
  """
190
246
  Get the query to fetch the completion count by subjects.
191
247
 
@@ -198,13 +254,34 @@ class FactEnrollmentAdminDashQueries:
198
254
  (str): Query to fetch the completion count by subjects for the top subjects by completion count.
199
255
  """
200
256
  return f"""
201
- SELECT course_subject, enroll_type, count(course_subject) as completion_count
202
- FROM fact_enrollment_admin_dash
203
- WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
204
- has_passed=1 AND
257
+ WITH filtered_data AS (
258
+ SELECT
259
+ course_subject,
260
+ enroll_type,
261
+ passed_date
262
+ FROM fact_enrollment_admin_dash
263
+ WHERE has_passed = 1 AND
264
+ enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
205
265
  passed_date BETWEEN %(start_date)s AND %(end_date)s
206
- GROUP BY course_subject, enroll_type
207
- ORDER BY completion_count DESC LIMIT {record_count};
266
+ ),
267
+ top_10_subjects AS (
268
+ SELECT
269
+ course_subject,
270
+ COUNT(*) AS total_completion_count
271
+ FROM filtered_data
272
+ GROUP BY course_subject
273
+ ORDER BY total_completion_count DESC
274
+ LIMIT {record_count}
275
+ )
276
+ SELECT
277
+ d.course_subject,
278
+ d.enroll_type,
279
+ COUNT(*) AS completion_count
280
+ FROM filtered_data d
281
+ JOIN top_10_subjects ts
282
+ ON d.course_subject = ts.course_subject
283
+ GROUP BY d.course_subject, d.enroll_type
284
+ ORDER BY total_completion_count DESC;
208
285
  """
209
286
 
210
287
  @staticmethod
@@ -38,7 +38,8 @@ class SkillsDailyRollupAdminDashQueries:
38
38
  WITH TopSkills AS (
39
39
  -- Get top 10 skills by total enrollments
40
40
  SELECT
41
- skill_name
41
+ skill_name,
42
+ SUM(enrolls) AS total_enrollment_count
42
43
  FROM
43
44
  skills_daily_rollup_admin_dash
44
45
  WHERE
@@ -47,7 +48,7 @@ class SkillsDailyRollupAdminDashQueries:
47
48
  GROUP BY
48
49
  skill_name
49
50
  ORDER BY
50
- SUM(enrolls) DESC
51
+ total_enrollment_count DESC
51
52
  LIMIT 10
52
53
  )
53
54
  SELECT
@@ -70,7 +71,7 @@ class SkillsDailyRollupAdminDashQueries:
70
71
  GROUP BY
71
72
  sd.skill_name, subject_name
72
73
  ORDER BY
73
- subject_name;
74
+ total_enrollment_count DESC;
74
75
  """
75
76
 
76
77
  @staticmethod
@@ -82,7 +83,8 @@ class SkillsDailyRollupAdminDashQueries:
82
83
  WITH TopSkills AS (
83
84
  -- Get top 10 skills by total completions
84
85
  SELECT
85
- skill_name
86
+ skill_name,
87
+ SUM(completions) AS total_completion_count
86
88
  FROM
87
89
  skills_daily_rollup_admin_dash
88
90
  WHERE
@@ -91,7 +93,7 @@ class SkillsDailyRollupAdminDashQueries:
91
93
  GROUP BY
92
94
  skill_name
93
95
  ORDER BY
94
- SUM(completions) DESC
96
+ total_completion_count DESC
95
97
  LIMIT 10
96
98
  )
97
99
  SELECT
@@ -114,5 +116,5 @@ class SkillsDailyRollupAdminDashQueries:
114
116
  GROUP BY
115
117
  sd.skill_name, subject_name
116
118
  ORDER BY
117
- subject_name;
119
+ total_completion_count DESC;
118
120
  """
@@ -5,11 +5,14 @@ from datetime import date
5
5
  from uuid import UUID
6
6
 
7
7
  from enterprise_data.cache.decorators import cache_it
8
+ from enterprise_data.utils import find_first
8
9
 
9
10
  from ..queries import FactEngagementAdminDashQueries
10
11
  from ..utils import run_query
11
12
  from .base import BaseTable
12
13
 
14
+ NULL_EMAIL_TEXT = 'learners who have not shared consent'
15
+
13
16
 
14
17
  class FactEngagementAdminDashTable(BaseTable):
15
18
  """
@@ -168,7 +171,14 @@ class FactEngagementAdminDashTable(BaseTable):
168
171
 
169
172
  @cache_it()
170
173
  def _get_engagement_data_for_leaderboard(
171
- self, enterprise_customer_uuid: UUID, start_date: date, end_date: date, limit: int, offset: int
174
+ self,
175
+ enterprise_customer_uuid: UUID,
176
+ start_date: date,
177
+ end_date: date,
178
+ limit: int,
179
+ offset: int,
180
+ include_null_email: bool,
181
+
172
182
  ):
173
183
  """
174
184
  Get the engagement data for leaderboard.
@@ -182,11 +192,12 @@ class FactEngagementAdminDashTable(BaseTable):
182
192
  end_date (date): The end date.
183
193
  limit (int): The maximum number of records to return.
184
194
  offset (int): The number of records to skip.
195
+ include_null_email (bool): If True, only fetch data for NULL emails.
185
196
 
186
197
  Returns:
187
198
  list[dict]: The engagement data for leaderboard.
188
199
  """
189
- return run_query(
200
+ engagements = run_query(
190
201
  query=self.queries.get_engagement_data_for_leaderboard_query(),
191
202
  params={
192
203
  'enterprise_customer_uuid': enterprise_customer_uuid,
@@ -198,9 +209,27 @@ class FactEngagementAdminDashTable(BaseTable):
198
209
  as_dict=True,
199
210
  )
200
211
 
212
+ if include_null_email:
213
+ engagement_for_null_email = run_query(
214
+ query=self.queries.get_engagement_data_for_leaderboard_null_email_only_query(),
215
+ params={
216
+ 'enterprise_customer_uuid': enterprise_customer_uuid,
217
+ 'start_date': start_date,
218
+ 'end_date': end_date,
219
+ },
220
+ as_dict=True,
221
+ )
222
+ engagements += engagement_for_null_email
223
+ return engagements
224
+
201
225
  @cache_it()
202
226
  def _get_completion_data_for_leaderboard_query(
203
- self, enterprise_customer_uuid: UUID, start_date: date, end_date: date, email_list: list
227
+ self,
228
+ enterprise_customer_uuid: UUID,
229
+ start_date: date,
230
+ end_date: date,
231
+ email_list: list,
232
+ include_null_email: bool,
204
233
  ):
205
234
  """
206
235
  Get the completion data for leaderboard.
@@ -213,11 +242,13 @@ class FactEngagementAdminDashTable(BaseTable):
213
242
  start_date (date): The start date.
214
243
  end_date (date): The end date.
215
244
  email_list (list<str>): List of emails of the enterprise learners.
245
+ include_null_email (bool): If True, only fetch data for NULL emails.
216
246
 
217
247
  Returns:
218
248
  list[dict]: The engagement data for leaderboard.
219
249
  """
220
- return run_query(
250
+
251
+ completions = run_query(
221
252
  query=self.queries.get_completion_data_for_leaderboard_query(email_list),
222
253
  params={
223
254
  'enterprise_customer_uuid': enterprise_customer_uuid,
@@ -227,8 +258,28 @@ class FactEngagementAdminDashTable(BaseTable):
227
258
  as_dict=True,
228
259
  )
229
260
 
261
+ if include_null_email:
262
+ completions_for_null_email = run_query(
263
+ query=self.queries.get_completion_data_for_leaderboard_null_email_only_query(),
264
+ params={
265
+ 'enterprise_customer_uuid': enterprise_customer_uuid,
266
+ 'start_date': start_date,
267
+ 'end_date': end_date,
268
+ },
269
+ as_dict=True,
270
+ )
271
+ completions += completions_for_null_email
272
+
273
+ return completions
274
+
230
275
  def get_all_leaderboard_data(
231
- self, enterprise_customer_uuid: UUID, start_date: date, end_date: date, limit: int, offset: int
276
+ self,
277
+ enterprise_customer_uuid: UUID,
278
+ start_date: date,
279
+ end_date: date,
280
+ limit: int,
281
+ offset: int,
282
+ total_count: int,
232
283
  ):
233
284
  """
234
285
  Get the leaderboard data for the given enterprise customer.
@@ -239,32 +290,49 @@ class FactEngagementAdminDashTable(BaseTable):
239
290
  end_date (date): The end date.
240
291
  limit (int): The maximum number of records to return.
241
292
  offset (int): The number of records to skip.
293
+ total_count (int): The total number of records.
242
294
 
243
295
  Returns:
244
296
  list[dict]: The leaderboard data.
245
297
  """
298
+ include_null_email = False
299
+ # If this is the last or only page, we need to include NULL emails record.
300
+ if total_count <= offset + limit:
301
+ include_null_email = True
302
+
246
303
  engagement_data = self._get_engagement_data_for_leaderboard(
247
304
  enterprise_customer_uuid=enterprise_customer_uuid,
248
305
  start_date=start_date,
249
306
  end_date=end_date,
250
307
  limit=limit,
251
308
  offset=offset,
309
+ include_null_email=include_null_email,
252
310
  )
253
311
  # If there is no data, no need to proceed.
254
312
  if not engagement_data:
255
313
  return []
256
314
 
257
- engagement_data_dict = {engagement['email']: engagement for engagement in engagement_data}
315
+ engagement_data_dict = {
316
+ engagement['email']: engagement for engagement in engagement_data if engagement['email']
317
+ }
258
318
  completion_data = self._get_completion_data_for_leaderboard_query(
259
319
  enterprise_customer_uuid=enterprise_customer_uuid,
260
320
  start_date=start_date,
261
321
  end_date=end_date,
262
322
  email_list=list(engagement_data_dict.keys()),
323
+ include_null_email=include_null_email,
263
324
  )
264
325
  for completion in completion_data:
265
326
  email = completion['email']
266
327
  engagement_data_dict[email]['course_completion_count'] = completion['course_completion_count']
267
328
 
329
+ if include_null_email:
330
+ engagement_data_dict['None'] = find_first(engagement_data, lambda x: x['email'] is None) or {}
331
+ completion = find_first(completion_data, lambda x: x['email'] is None) or \
332
+ {'course_completion_count': ''}
333
+ engagement_data_dict['None']['course_completion_count'] = completion['course_completion_count']
334
+ engagement_data_dict['None']['email'] = NULL_EMAIL_TEXT
335
+
268
336
  return list(engagement_data_dict.values())
269
337
 
270
338
  @cache_it()
@@ -75,7 +75,7 @@ class AdvanceAnalyticsCompletionsView(AnalyticsPaginationMixin, ViewSet):
75
75
  )
76
76
 
77
77
  if response_type == ResponseType.CSV.value:
78
- filename = f"""individual_completions, {start_date} - {end_date}.csv"""
78
+ filename = f"""Individual Completions, {start_date} - {end_date}.csv"""
79
79
 
80
80
  return StreamingHttpResponse(
81
81
  IndividualCompletionsCSVRenderer().render(self._stream_serialized_data(
@@ -75,7 +75,7 @@ class AdvanceAnalyticsEngagementView(AnalyticsPaginationMixin, ViewSet):
75
75
  )
76
76
 
77
77
  if response_type == ResponseType.CSV.value:
78
- filename = f"""individual_engagements, {start_date} - {end_date}.csv"""
78
+ filename = f"""Individual Engagements, {start_date} - {end_date}.csv"""
79
79
 
80
80
  return StreamingHttpResponse(
81
81
  IndividualEngagementsCSVRenderer().render(self._stream_serialized_data(
@@ -74,7 +74,7 @@ class AdvanceAnalyticsEnrollmentsView(AnalyticsPaginationMixin, ViewSet):
74
74
  )
75
75
 
76
76
  if response_type == ResponseType.CSV.value:
77
- filename = f"""individual_enrollments, {start_date} - {end_date}.csv"""
77
+ filename = f"""Individual Enrollments, {start_date} - {end_date}.csv"""
78
78
 
79
79
  return StreamingHttpResponse(
80
80
  IndividualEnrollmentsCSVRenderer().render(self._stream_serialized_data(
@@ -49,17 +49,18 @@ class AdvanceAnalyticsLeaderboardView(AnalyticsPaginationMixin, ViewSet):
49
49
  end_date = serializer.data.get('end_date', date.today())
50
50
  page = serializer.data.get('page', 1)
51
51
  page_size = serializer.data.get('page_size', 100)
52
- leaderboard = FactEngagementAdminDashTable().get_all_leaderboard_data(
52
+ total_count = FactEngagementAdminDashTable().get_leaderboard_data_count(
53
53
  enterprise_customer_uuid=enterprise_uuid,
54
54
  start_date=start_date,
55
55
  end_date=end_date,
56
- limit=page_size,
57
- offset=(page - 1) * page_size,
58
56
  )
59
- total_count = FactEngagementAdminDashTable().get_leaderboard_data_count(
57
+ leaderboard = FactEngagementAdminDashTable().get_all_leaderboard_data(
60
58
  enterprise_customer_uuid=enterprise_uuid,
61
59
  start_date=start_date,
62
60
  end_date=end_date,
61
+ limit=page_size,
62
+ offset=(page - 1) * page_size,
63
+ total_count=total_count,
63
64
  )
64
65
  response_type = request.query_params.get('response_type', ResponseType.JSON.value)
65
66
 
@@ -102,6 +103,7 @@ class AdvanceAnalyticsLeaderboardView(AnalyticsPaginationMixin, ViewSet):
102
103
  end_date=end_date,
103
104
  limit=page_size,
104
105
  offset=offset,
106
+ total_count=total_count,
105
107
  )
106
108
  yield from leaderboard
107
109
  offset += page_size
@@ -161,17 +161,18 @@ class Command(BaseCommand):
161
161
  start_date=start_date,
162
162
  end_date=end_date,
163
163
  )
164
- enterprise_engagement_table.get_all_leaderboard_data(
164
+ total_count = enterprise_engagement_table.get_leaderboard_data_count(
165
165
  enterprise_customer_uuid=enterprise_customer_uuid,
166
166
  start_date=start_date,
167
167
  end_date=end_date,
168
- limit=page_size,
169
- offset=0,
170
168
  )
171
- enterprise_engagement_table.get_leaderboard_data_count(
169
+ enterprise_engagement_table.get_all_leaderboard_data(
172
170
  enterprise_customer_uuid=enterprise_customer_uuid,
173
171
  start_date=start_date,
174
172
  end_date=end_date,
173
+ limit=page_size,
174
+ offset=0,
175
+ total_count=total_count,
175
176
  )
176
177
 
177
178
  @staticmethod
@@ -30,9 +30,18 @@ class Test(TestCase):
30
30
  get_enrollment_date_range_patcher.start()
31
31
  self.addCleanup(get_enrollment_date_range_patcher.stop)
32
32
 
33
- @patch('enterprise_data.admin_analytics.database.tables.fact_engagement_admin_dash.run_query', MagicMock())
34
- @patch('enterprise_data.admin_analytics.database.tables.fact_enrollment_admin_dash.run_query', MagicMock())
35
- @patch('enterprise_data.admin_analytics.database.tables.skills_daily_rollup_admin_dash.run_query', MagicMock())
33
+ @patch(
34
+ 'enterprise_data.admin_analytics.database.tables.fact_engagement_admin_dash.run_query',
35
+ MagicMock(return_value=[])
36
+ )
37
+ @patch(
38
+ 'enterprise_data.admin_analytics.database.tables.fact_enrollment_admin_dash.run_query',
39
+ MagicMock(return_value=[])
40
+ )
41
+ @patch(
42
+ 'enterprise_data.admin_analytics.database.tables.skills_daily_rollup_admin_dash.run_query',
43
+ MagicMock(return_value=[])
44
+ )
36
45
  @patch('enterprise_data.api.v1.views.analytics_enrollments.FactEnrollmentAdminDashTable.get_top_enterprises')
37
46
  @patch('enterprise_data.cache.decorators.cache.set')
38
47
  @patch('enterprise_data.cache.decorators.cache.get')
@@ -47,5 +56,5 @@ class Test(TestCase):
47
56
 
48
57
  call_command('pre_warm_analytics_cache')
49
58
 
50
- assert mock_get_cache.call_count == 24
51
- assert mock_set_cache.call_count == 24
59
+ assert mock_get_cache.call_count == 23
60
+ assert mock_set_cache.call_count == 23
enterprise_data/utils.py CHANGED
@@ -97,3 +97,20 @@ def primary_subject_truncate(x):
97
97
  return x
98
98
  else:
99
99
  return "other"
100
+
101
+
102
+ def find_first(iterable, condition):
103
+ """
104
+ Find the first item in an iterable that satisfies the condition.
105
+
106
+ Arguments:
107
+ iterable (iterable): The iterable to search.
108
+ condition (function): The condition to satisfy.
109
+
110
+ Returns:
111
+ The first item that satisfies the condition, or None if no item satisfies the condition.
112
+ """
113
+ try:
114
+ return next(item for item in iterable if condition(item))
115
+ except StopIteration:
116
+ return None
@@ -57,5 +57,5 @@ def request_user_has_explicit_access(*args, **kwargs):
57
57
 
58
58
  rules.add_perm(
59
59
  'can_access_enterprise',
60
- request_user_has_implicit_access | request_user_has_explicit_access # pylint: disable=unsupported-binary-operation
60
+ request_user_has_implicit_access | request_user_has_explicit_access
61
61
  )