edx-enterprise-data 10.0.1__py3-none-any.whl → 10.2.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: 10.0.1
3
+ Version: 10.2.0
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=u05ZrvDRwvKfKZqkpoRsMDTpW_O_IUEgUe9Z89mjgPU,124
1
+ enterprise_data/__init__.py,sha256=msobjDbSxGnYjz9FZlLzQBbpW1Y81HKRCZo4uSEsQic,124
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,20 +8,20 @@ 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=brRgmgtzPLsOdt1vYrk_EpoQLm2izmOdBapxAxNkXzA,8420
18
+ enterprise_data/admin_analytics/database/queries/fact_engagement_admin_dash.py,sha256=ms7wFVSC53VD0IHH9sXmfx7Eh0BoEDDtE0gdTRddKzg,9274
19
+ enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py,sha256=6EX0W3RWRzSrpIbqFJvStnj5Rkfzi9KyFP0UNmb75pU,8814
20
20
  enterprise_data/admin_analytics/database/queries/skills_daily_rollup_admin_dash.py,sha256=PgWwvtVCK5lbiq6z44lH0fwbkdWYukhyXZL9X8lNWCY,4099
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
24
- enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py,sha256=FFpGtv2PUIDSdTqSZV4F3tVP9uDvS64ZqQXMGygCGok,11294
23
+ enterprise_data/admin_analytics/database/tables/fact_engagement_admin_dash.py,sha256=675LqIERXTbQ3m2deoj10Xsm88nnEzepIRvJMYKHbl8,12840
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
27
27
  enterprise_data/api/urls.py,sha256=POqc_KATHdnpMf9zHtpO46pKD5KAlAExtx7G6iylLcU,273
@@ -33,12 +33,12 @@ 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=4jg2I50TNn2Q6InLCI9eS1MBiFo4SMRsTx4kZJtNmpI,6268
37
- enterprise_data/api/v1/views/analytics_engagements.py,sha256=W9DNalWIgoeo_KXqapDj2_ireRyXU47nKHw95sAEtxM,6306
38
- enterprise_data/api/v1/views/analytics_enrollments.py,sha256=HXgoFwvXk-CZcf6qbVOfYSCnkfDxGvfHxl0j3RcKz7A,6290
39
- enterprise_data/api/v1/views/analytics_leaderboard.py,sha256=zwd-2A2sctjAdx5BUjCqiwcZ5aNvM8wfm386pR-RwGQ,4114
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=3dyo7_OhyGEEeibemBrRsUOo0jbM4LbDgV5gw3YnVig,4186
40
40
  enterprise_data/api/v1/views/base.py,sha256=Kkmd5zgEBAhvwS_GoGXSK6lgbDNwSPioYn-QbnizI3w,3416
41
- enterprise_data/api/v1/views/enterprise_admin.py,sha256=hAZR7dh8MIFMibs1rw0mPcdj_DyTjTpa0OqmrGPNdoM,8205
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
43
43
  enterprise_data/api/v1/views/enterprise_offers.py,sha256=VifxgqTLFLVw4extYPlHcN1N_yjXcsYsAlYEnAbpb10,1266
44
44
  enterprise_data/cache/__init__.py,sha256=fiBUploll1kmDy2vCmnNpeZVTD4ewsgtRF14vVs0Rb4,1850
@@ -54,12 +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=3MCThZiZgfveD3NqFt95WTJ9LFTYxN8ZYbqWF1JinI8,8327
57
58
  enterprise_data/management/commands/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
59
  enterprise_data/management/commands/tests/test_create_dummy_data_lpr_v1.py,sha256=wt9fqAFKQVTqllpZ42dch-n31JavUifUIB9CKNYcnYM,1086
59
60
  enterprise_data/management/commands/tests/test_create_enterprise_enrollment.py,sha256=5CABLk8qAx8RP8mrFtnbJ4-xVkf9-5Mq6iQcx8jBfFc,1742
60
61
  enterprise_data/management/commands/tests/test_create_enterprise_learner_enrollment_lpr_v1.py,sha256=FzOBHcd1FfSVN5AI8oAKvmqj-W8Y-V7x9yJx9R1WrLQ,3845
61
62
  enterprise_data/management/commands/tests/test_create_enterprise_learner_lpr_v1.py,sha256=P7wyyaZ4LMWv1umpCThp4By5DWjtI7kYAmY0AeuY2XI,1637
62
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=Nbwsl5CYMbi5cN2IwmUtTs5b_MeU6T0lquk3x8Tuh_4,2108
63
65
  enterprise_data/migrations/0001_initial.py,sha256=g2fNJGLxELthmYrJlPB7cBxbsLWB6u-MotQvE3t95ww,2358
64
66
  enterprise_data/migrations/0002_auto_20180430_1358.py,sha256=roRSgWy2p4hFefNFaajg-q_e8_5FHZih_ou1dLTwWQk,600
65
67
  enterprise_data/migrations/0003_auto_20180501_0603.py,sha256=N9C5QcdRMfzTycO6xqdi7WPkwrktn3y3NEsm30L4pS8,415
@@ -134,7 +136,7 @@ enterprise_data_roles/admin.py,sha256=QNP0VeWE092vZzpyxOA5UJK1nNGl5e71B1J0RCwo_n
134
136
  enterprise_data_roles/apps.py,sha256=nKi8TyuQ5Q6WGtKs5QeXvUTc3N-YQjKhyBnm2EM3Bng,260
135
137
  enterprise_data_roles/constants.py,sha256=7yHmbAyqeXNX-lg3AC1caFsv97mouAS4NFIrHhwGgQ0,476
136
138
  enterprise_data_roles/models.py,sha256=b4weAWOfQ1sMnODXQBcq4IsclR2NYWWxuOZ95fKnEfs,1679
137
- enterprise_data_roles/rules.py,sha256=AaONKA91ge-AZ1DsljekobouuCkepDzlfH4O1ece3KM,1948
139
+ enterprise_data_roles/rules.py,sha256=4W_qmVb3lQzTJqKqNXSRhW6cjKoZTygnj_4rXB_zljM,1900
138
140
  enterprise_data_roles/migrations/0001_initial.py,sha256=rXIP0mgd5w71bCvGEG2wCaHwFkCekM8nGLyUt9-3gaI,2083
139
141
  enterprise_data_roles/migrations/0002_add_enterprise_data_feature_roles.py,sha256=aDGjqYznGT9DV-dAVVIem9kNPNmcUmafinJIyd6FcJw,947
140
142
  enterprise_data_roles/migrations/0003_add_role_based_access_control_switch.py,sha256=7UAMcY6270OO05V9hDfM25lRFHp5v5DRYXdZpiS9rh4,955
@@ -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-10.0.1.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
173
- edx_enterprise_data-10.0.1.dist-info/METADATA,sha256=l9oTpFgoHhPD87131aEzIRKaxtSl3N2Ilbtq_CnOikU,1505
174
- edx_enterprise_data-10.0.1.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
175
- edx_enterprise_data-10.0.1.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
176
- edx_enterprise_data-10.0.1.dist-info/RECORD,,
174
+ edx_enterprise_data-10.2.0.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
175
+ edx_enterprise_data-10.2.0.dist-info/METADATA,sha256=ZaH_jYk1WE6bFmmmA3vCkGzWgs_q7lZgMtUnbjk52jU,1505
176
+ edx_enterprise_data-10.2.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
177
+ edx_enterprise_data-10.2.0.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
178
+ edx_enterprise_data-10.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.2.0)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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__ = "10.0.1"
5
+ __version__ = "10.2.0"
@@ -163,6 +163,55 @@ class FactEngagementAdminDashQueries:
163
163
  GROUP BY email;
164
164
  """
165
165
 
166
+ @staticmethod
167
+ def get_engagement_data_for_leaderboard_null_email_only_query():
168
+ """
169
+ Get the query to fetch the engagement data for leaderboard for NULL emails only.
170
+
171
+ Query should fetch the engagement data for like learning time, session length of
172
+ the enterprise learners to show in the leaderboard.
173
+
174
+ Returns:
175
+ (str): Query to fetch the engagement data for leaderboard.
176
+ """
177
+ return """
178
+ SELECT
179
+ email,
180
+ ROUND(SUM(learning_time_seconds) / 3600, 1) as learning_time_hours,
181
+ SUM(is_engaged) as session_count,
182
+ CASE
183
+ WHEN SUM(is_engaged) = 0 THEN 0.0
184
+ ELSE ROUND(SUM(learning_time_seconds) / 3600 / SUM(is_engaged), 1)
185
+ END AS average_session_length
186
+ FROM fact_enrollment_engagement_day_admin_dash
187
+ WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
188
+ (activity_date BETWEEN %(start_date)s AND %(end_date)s) AND
189
+ is_engaged = 1 AND
190
+ email is NULL
191
+ GROUP BY email;
192
+ """
193
+
194
+ @staticmethod
195
+ def get_completion_data_for_leaderboard_null_email_only_query():
196
+ """
197
+ Get the query to fetch the completions data for leaderboard for NULL emails.
198
+
199
+ Query should fetch the completion data for like course completion count of
200
+ the enterprise learners to show in the leaderboard.
201
+
202
+ Returns:
203
+ (list<str>): Query to fetch the completions data for leaderboard.
204
+ """
205
+ return """
206
+ SELECT email, count(course_key) as course_completion_count
207
+ FROM fact_enrollment_admin_dash
208
+ WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
209
+ (passed_date BETWEEN %(start_date)s AND %(end_date)s) AND
210
+ has_passed = 1 AND
211
+ email is NULL
212
+ GROUP BY email;
213
+ """
214
+
166
215
  @staticmethod
167
216
  def get_leaderboard_data_count_query():
168
217
  """
@@ -7,6 +7,18 @@ class FactEnrollmentAdminDashQueries:
7
7
  """
8
8
  Queries related to the fact_enrollment_admin_dash table.
9
9
  """
10
+ @staticmethod
11
+ def get_top_enterprises_query(count=10):
12
+ """
13
+ Get the query to fetch the top enterprises by enrollments.
14
+ """
15
+ return f"""
16
+ SELECT enterprise_customer_uuid
17
+ FROM fact_enrollment_admin_dash
18
+ GROUP BY enterprise_customer_uuid
19
+ ORDER BY COUNT(enterprise_customer_uuid) DESC LIMIT {count};
20
+ """
21
+
10
22
  @staticmethod
11
23
  def get_enrollment_count_query():
12
24
  """
@@ -5,6 +5,7 @@ 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
@@ -168,7 +169,14 @@ class FactEngagementAdminDashTable(BaseTable):
168
169
 
169
170
  @cache_it()
170
171
  def _get_engagement_data_for_leaderboard(
171
- self, enterprise_customer_uuid: UUID, start_date: date, end_date: date, limit: int, offset: int
172
+ self,
173
+ enterprise_customer_uuid: UUID,
174
+ start_date: date,
175
+ end_date: date,
176
+ limit: int,
177
+ offset: int,
178
+ include_null_email: bool,
179
+
172
180
  ):
173
181
  """
174
182
  Get the engagement data for leaderboard.
@@ -182,11 +190,12 @@ class FactEngagementAdminDashTable(BaseTable):
182
190
  end_date (date): The end date.
183
191
  limit (int): The maximum number of records to return.
184
192
  offset (int): The number of records to skip.
193
+ include_null_email (bool): If True, only fetch data for NULL emails.
185
194
 
186
195
  Returns:
187
196
  list[dict]: The engagement data for leaderboard.
188
197
  """
189
- return run_query(
198
+ engagements = run_query(
190
199
  query=self.queries.get_engagement_data_for_leaderboard_query(),
191
200
  params={
192
201
  'enterprise_customer_uuid': enterprise_customer_uuid,
@@ -198,9 +207,27 @@ class FactEngagementAdminDashTable(BaseTable):
198
207
  as_dict=True,
199
208
  )
200
209
 
210
+ if include_null_email:
211
+ engagement_for_null_email = run_query(
212
+ query=self.queries.get_engagement_data_for_leaderboard_null_email_only_query(),
213
+ params={
214
+ 'enterprise_customer_uuid': enterprise_customer_uuid,
215
+ 'start_date': start_date,
216
+ 'end_date': end_date,
217
+ },
218
+ as_dict=True,
219
+ )
220
+ engagements += engagement_for_null_email
221
+ return engagements
222
+
201
223
  @cache_it()
202
224
  def _get_completion_data_for_leaderboard_query(
203
- self, enterprise_customer_uuid: UUID, start_date: date, end_date: date, email_list: list
225
+ self,
226
+ enterprise_customer_uuid: UUID,
227
+ start_date: date,
228
+ end_date: date,
229
+ email_list: list,
230
+ include_null_email: bool,
204
231
  ):
205
232
  """
206
233
  Get the completion data for leaderboard.
@@ -213,11 +240,13 @@ class FactEngagementAdminDashTable(BaseTable):
213
240
  start_date (date): The start date.
214
241
  end_date (date): The end date.
215
242
  email_list (list<str>): List of emails of the enterprise learners.
243
+ include_null_email (bool): If True, only fetch data for NULL emails.
216
244
 
217
245
  Returns:
218
246
  list[dict]: The engagement data for leaderboard.
219
247
  """
220
- return run_query(
248
+
249
+ completions = run_query(
221
250
  query=self.queries.get_completion_data_for_leaderboard_query(email_list),
222
251
  params={
223
252
  'enterprise_customer_uuid': enterprise_customer_uuid,
@@ -227,8 +256,28 @@ class FactEngagementAdminDashTable(BaseTable):
227
256
  as_dict=True,
228
257
  )
229
258
 
259
+ if include_null_email:
260
+ completions_for_null_email = run_query(
261
+ query=self.queries.get_completion_data_for_leaderboard_null_email_only_query(),
262
+ params={
263
+ 'enterprise_customer_uuid': enterprise_customer_uuid,
264
+ 'start_date': start_date,
265
+ 'end_date': end_date,
266
+ },
267
+ as_dict=True,
268
+ )
269
+ completions += completions_for_null_email
270
+
271
+ return completions
272
+
230
273
  def get_all_leaderboard_data(
231
- self, enterprise_customer_uuid: UUID, start_date: date, end_date: date, limit: int, offset: int
274
+ self,
275
+ enterprise_customer_uuid: UUID,
276
+ start_date: date,
277
+ end_date: date,
278
+ limit: int,
279
+ offset: int,
280
+ total_count: int,
232
281
  ):
233
282
  """
234
283
  Get the leaderboard data for the given enterprise customer.
@@ -239,32 +288,48 @@ class FactEngagementAdminDashTable(BaseTable):
239
288
  end_date (date): The end date.
240
289
  limit (int): The maximum number of records to return.
241
290
  offset (int): The number of records to skip.
291
+ total_count (int): The total number of records.
242
292
 
243
293
  Returns:
244
294
  list[dict]: The leaderboard data.
245
295
  """
296
+ include_null_email = False
297
+ # If this is the last or only page, we need to include NULL emails record.
298
+ if total_count <= offset + limit:
299
+ include_null_email = True
300
+
246
301
  engagement_data = self._get_engagement_data_for_leaderboard(
247
302
  enterprise_customer_uuid=enterprise_customer_uuid,
248
303
  start_date=start_date,
249
304
  end_date=end_date,
250
305
  limit=limit,
251
306
  offset=offset,
307
+ include_null_email=include_null_email,
252
308
  )
253
309
  # If there is no data, no need to proceed.
254
310
  if not engagement_data:
255
311
  return []
256
312
 
257
- engagement_data_dict = {engagement['email']: engagement for engagement in engagement_data}
313
+ engagement_data_dict = {
314
+ engagement['email']: engagement for engagement in engagement_data if engagement['email']
315
+ }
258
316
  completion_data = self._get_completion_data_for_leaderboard_query(
259
317
  enterprise_customer_uuid=enterprise_customer_uuid,
260
318
  start_date=start_date,
261
319
  end_date=end_date,
262
320
  email_list=list(engagement_data_dict.keys()),
321
+ include_null_email=include_null_email,
263
322
  )
264
323
  for completion in completion_data:
265
324
  email = completion['email']
266
325
  engagement_data_dict[email]['course_completion_count'] = completion['course_completion_count']
267
326
 
327
+ if include_null_email:
328
+ engagement_data_dict['None'] = find_first(engagement_data, lambda x: x['email'] is None) or {}
329
+ completion = find_first(completion_data, lambda x: x['email'] is None) or \
330
+ {'course_completion_count': 'Unknown'}
331
+ engagement_data_dict['None']['course_completion_count'] = completion['course_completion_count']
332
+
268
333
  return list(engagement_data_dict.values())
269
334
 
270
335
  @cache_it()
@@ -17,6 +17,22 @@ class FactEnrollmentAdminDashTable(BaseTable):
17
17
  """
18
18
  queries = FactEnrollmentAdminDashQueries()
19
19
 
20
+ def get_top_enterprises(self, count=10):
21
+ """
22
+ Get the top enterprises by enrollments.
23
+
24
+ Arguments:
25
+ count (int): The number of enterprises to return.
26
+
27
+ Returns:
28
+ list<str>: A list of enterprise UUIDs.
29
+ """
30
+ result = run_query(
31
+ query=self.queries.get_top_enterprises_query(count),
32
+ as_dict=False,
33
+ )
34
+ return [row[0] for row in result]
35
+
20
36
  @cache_it()
21
37
  def get_enrollment_count(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
22
38
  """
@@ -77,7 +93,7 @@ class FactEnrollmentAdminDashTable(BaseTable):
77
93
  Get the enrollment date range for the given enterprise customer.
78
94
 
79
95
  Arguments:
80
- enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
96
+ enterprise_customer_uuid (UUID | str): The UUID of the enterprise customer.
81
97
 
82
98
  Returns:
83
99
  (tuple<date, date>): The minimum and maximum enrollment dates.
@@ -2,7 +2,7 @@
2
2
  Views for enterprise admin completions analytics.
3
3
  """
4
4
 
5
- from datetime import datetime
5
+ from datetime import date
6
6
  from logging import getLogger
7
7
 
8
8
  from edx_rbac.decorators import permission_required
@@ -50,7 +50,7 @@ class AdvanceAnalyticsCompletionsView(AnalyticsPaginationMixin, ViewSet):
50
50
 
51
51
  # get values from query params or use default values
52
52
  start_date = serializer.data.get('start_date', min_enrollment_date)
53
- end_date = serializer.data.get('end_date', datetime.now())
53
+ end_date = serializer.data.get('end_date', date.today())
54
54
  page = serializer.data.get('page', 1)
55
55
  page_size = serializer.data.get('page_size', 100)
56
56
  completions = FactEnrollmentAdminDashTable().get_all_completions(
@@ -132,7 +132,7 @@ class AdvanceAnalyticsCompletionsView(AnalyticsPaginationMixin, ViewSet):
132
132
  )
133
133
  # get values from query params or use default
134
134
  start_date = serializer.data.get('start_date', min_enrollment_date)
135
- end_date = serializer.data.get('end_date', datetime.now())
135
+ end_date = serializer.data.get('end_date', date.today())
136
136
  with timer('construct_completion_all_stats'):
137
137
  data = {
138
138
  'completions_over_time': FactEnrollmentAdminDashTable().get_completions_time_series_data(
@@ -2,7 +2,7 @@
2
2
  Views for handling REST endpoints related to Engagements analytics.
3
3
  """
4
4
 
5
- from datetime import datetime
5
+ from datetime import date
6
6
  from logging import getLogger
7
7
 
8
8
  from edx_rbac.decorators import permission_required
@@ -50,7 +50,7 @@ class AdvanceAnalyticsEngagementView(AnalyticsPaginationMixin, ViewSet):
50
50
 
51
51
  # get values from query params or use default values
52
52
  start_date = serializer.data.get('start_date', min_enrollment_date)
53
- end_date = serializer.data.get('end_date', datetime.now())
53
+ end_date = serializer.data.get('end_date', date.today())
54
54
  page = serializer.data.get('page', 1)
55
55
  page_size = serializer.data.get('page_size', 100)
56
56
  engagements = FactEngagementAdminDashTable().get_all_engagements(
@@ -132,7 +132,7 @@ class AdvanceAnalyticsEngagementView(AnalyticsPaginationMixin, ViewSet):
132
132
  )
133
133
  # get values from query params or use default
134
134
  start_date = serializer.data.get('start_date', min_enrollment_date)
135
- end_date = serializer.data.get('end_date', datetime.now())
135
+ end_date = serializer.data.get('end_date', date.today())
136
136
  with timer('construct_engagement_all_stats'):
137
137
  data = {
138
138
  'engagement_over_time': FactEngagementAdminDashTable().get_engagement_time_series_data(
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Advance Analytics for API endpoints to fetch enterprise enrollments data.
3
3
  """
4
- from datetime import datetime
4
+ from datetime import date
5
5
  from logging import getLogger
6
6
 
7
7
  from edx_rbac.decorators import permission_required
@@ -49,7 +49,7 @@ class AdvanceAnalyticsEnrollmentsView(AnalyticsPaginationMixin, ViewSet):
49
49
 
50
50
  # get values from query params or use default values
51
51
  start_date = serializer.data.get('start_date', min_enrollment_date)
52
- end_date = serializer.data.get('end_date', datetime.now())
52
+ end_date = serializer.data.get('end_date', date.today())
53
53
  page = serializer.data.get('page', 1)
54
54
  page_size = serializer.data.get('page_size', 100)
55
55
  enrollments = FactEnrollmentAdminDashTable().get_all_enrollments(
@@ -131,7 +131,7 @@ class AdvanceAnalyticsEnrollmentsView(AnalyticsPaginationMixin, ViewSet):
131
131
  )
132
132
  # get values from query params or use default
133
133
  start_date = serializer.data.get('start_date', min_enrollment_date)
134
- end_date = serializer.data.get('end_date', datetime.now())
134
+ end_date = serializer.data.get('end_date', date.today())
135
135
  with timer('construct_enrollment_all_stats'):
136
136
  data = {
137
137
  'enrollments_over_time': FactEnrollmentAdminDashTable().get_enrolment_time_series_data(
@@ -2,7 +2,7 @@
2
2
  Views for fetching leaderboard data.
3
3
  """
4
4
 
5
- from datetime import datetime
5
+ from datetime import date
6
6
  from logging import getLogger
7
7
 
8
8
  from edx_rbac.decorators import permission_required
@@ -46,20 +46,21 @@ class AdvanceAnalyticsLeaderboardView(AnalyticsPaginationMixin, ViewSet):
46
46
 
47
47
  # get values from query params or use default values
48
48
  start_date = serializer.data.get('start_date', min_enrollment_date)
49
- end_date = serializer.data.get('end_date', datetime.now())
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
@@ -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 date, datetime
5
5
 
6
6
  from edx_rbac.decorators import permission_required
7
7
  from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
@@ -160,7 +160,7 @@ class EnterpriseAdminAnalyticsSkillsView(APIView):
160
160
  if (start_date := serializer.data.get('start_date')) is None:
161
161
  start_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(enterprise_id)
162
162
 
163
- end_date = serializer.data.get('end_date', datetime.now())
163
+ end_date = serializer.data.get('end_date', date.today())
164
164
 
165
165
  with timer('top_skills'):
166
166
  skills = SkillsDailyRollupAdminDashTable().get_top_skills(enterprise_id, start_date, end_date)
@@ -0,0 +1,219 @@
1
+ """
2
+ Management command for pre-warming the analytics cache for large enterprises.
3
+ """
4
+ from datetime import date
5
+
6
+ from django.core.management.base import BaseCommand, CommandError
7
+
8
+ from enterprise_data.admin_analytics.database.tables import (
9
+ FactEngagementAdminDashTable,
10
+ FactEnrollmentAdminDashTable,
11
+ SkillsDailyRollupAdminDashTable,
12
+ )
13
+
14
+
15
+ class Command(BaseCommand):
16
+ """
17
+ Add cache entries for analytics related data for a large enterprise.
18
+
19
+ The top enterprises will be the ones with the most enrollments.
20
+ """
21
+ help = 'Pre-warm the analytics cache for a large enterprises.'
22
+
23
+ @staticmethod
24
+ def __cache_enrollment_data(enterprise_customer_uuid):
25
+ """
26
+ Helper method to cache all the enrollment related data for the given enterprise.
27
+
28
+ Arguments:
29
+ enterprise_customer_uuid (str): The UUID of the enterprise customer.
30
+ """
31
+ enterprise_enrollment_table = FactEnrollmentAdminDashTable()
32
+ start_date, _ = enterprise_enrollment_table.get_enrollment_date_range(
33
+ enterprise_customer_uuid,
34
+ )
35
+ end_date = date.today()
36
+ enterprise_enrollment_table.get_enrollment_count(
37
+ enterprise_customer_uuid=enterprise_customer_uuid,
38
+ start_date=start_date,
39
+ end_date=end_date,
40
+ )
41
+ page_size = 100
42
+ enterprise_enrollment_table.get_all_enrollments(
43
+ enterprise_customer_uuid=enterprise_customer_uuid,
44
+ start_date=start_date,
45
+ end_date=end_date,
46
+ limit=page_size,
47
+ offset=0,
48
+ )
49
+ enterprise_enrollment_table.get_enrollment_and_course_count(
50
+ enterprise_customer_uuid=enterprise_customer_uuid,
51
+ start_date=start_date,
52
+ end_date=end_date,
53
+ )
54
+ enterprise_enrollment_table.get_completion_count(
55
+ enterprise_customer_uuid=enterprise_customer_uuid,
56
+ start_date=start_date,
57
+ end_date=end_date,
58
+ )
59
+ enterprise_enrollment_table.get_top_courses_by_enrollments(
60
+ enterprise_customer_uuid=enterprise_customer_uuid,
61
+ start_date=start_date,
62
+ end_date=end_date,
63
+ )
64
+ enterprise_enrollment_table.get_top_subjects_by_enrollments(
65
+ enterprise_customer_uuid=enterprise_customer_uuid,
66
+ start_date=start_date,
67
+ end_date=end_date,
68
+ )
69
+ enterprise_enrollment_table.get_enrolment_time_series_data(
70
+ enterprise_customer_uuid=enterprise_customer_uuid,
71
+ start_date=start_date,
72
+ end_date=end_date,
73
+ )
74
+
75
+ @staticmethod
76
+ def __cache_completions_data(enterprise_customer_uuid):
77
+ """
78
+ Helper method to cache all the completions related data for the given enterprise.
79
+
80
+ Arguments:
81
+ enterprise_customer_uuid (str): The UUID of the enterprise customer.
82
+ """
83
+ enterprise_enrollment_table = FactEnrollmentAdminDashTable()
84
+ start_date, _ = enterprise_enrollment_table.get_enrollment_date_range(
85
+ enterprise_customer_uuid,
86
+ )
87
+ end_date = date.today()
88
+
89
+ page_size = 100
90
+ enterprise_enrollment_table.get_all_completions(
91
+ enterprise_customer_uuid=enterprise_customer_uuid,
92
+ start_date=start_date,
93
+ end_date=end_date,
94
+ limit=page_size,
95
+ offset=0,
96
+ )
97
+ enterprise_enrollment_table.get_completion_count(
98
+ enterprise_customer_uuid=enterprise_customer_uuid,
99
+ start_date=start_date,
100
+ end_date=end_date,
101
+ )
102
+ enterprise_enrollment_table.get_top_courses_by_completions(
103
+ enterprise_customer_uuid=enterprise_customer_uuid,
104
+ start_date=start_date,
105
+ end_date=end_date,
106
+ )
107
+ enterprise_enrollment_table.get_top_subjects_by_completions(
108
+ enterprise_customer_uuid=enterprise_customer_uuid,
109
+ start_date=start_date,
110
+ end_date=end_date,
111
+ )
112
+ enterprise_enrollment_table.get_completions_time_series_data(
113
+ enterprise_customer_uuid=enterprise_customer_uuid,
114
+ start_date=start_date,
115
+ end_date=end_date,
116
+ )
117
+
118
+ @staticmethod
119
+ def __cache_engagement_data(enterprise_customer_uuid):
120
+ """
121
+ Helper method to cache all the engagement related data for the given enterprise.
122
+
123
+ Arguments:
124
+ enterprise_customer_uuid (str): The UUID of the enterprise customer.
125
+ """
126
+ start_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(
127
+ enterprise_customer_uuid,
128
+ )
129
+ end_date = date.today()
130
+ enterprise_engagement_table = FactEngagementAdminDashTable()
131
+ enterprise_engagement_table.get_learning_hours_and_daily_sessions(
132
+ enterprise_customer_uuid=enterprise_customer_uuid,
133
+ start_date=start_date,
134
+ end_date=end_date,
135
+ )
136
+ enterprise_engagement_table.get_engagement_count(
137
+ enterprise_customer_uuid=enterprise_customer_uuid,
138
+ start_date=start_date,
139
+ end_date=end_date,
140
+ )
141
+ page_size = 100
142
+ enterprise_engagement_table.get_all_engagements(
143
+ enterprise_customer_uuid=enterprise_customer_uuid,
144
+ start_date=start_date,
145
+ end_date=end_date,
146
+ limit=page_size,
147
+ offset=0,
148
+ )
149
+ enterprise_engagement_table.get_top_courses_by_engagement(
150
+ enterprise_customer_uuid=enterprise_customer_uuid,
151
+ start_date=start_date,
152
+ end_date=end_date,
153
+ )
154
+ enterprise_engagement_table.get_top_subjects_by_engagement(
155
+ enterprise_customer_uuid=enterprise_customer_uuid,
156
+ start_date=start_date,
157
+ end_date=end_date,
158
+ )
159
+ enterprise_engagement_table.get_engagement_time_series_data(
160
+ enterprise_customer_uuid=enterprise_customer_uuid,
161
+ start_date=start_date,
162
+ end_date=end_date,
163
+ )
164
+ total_count = enterprise_engagement_table.get_leaderboard_data_count(
165
+ enterprise_customer_uuid=enterprise_customer_uuid,
166
+ start_date=start_date,
167
+ end_date=end_date,
168
+ )
169
+ enterprise_engagement_table.get_all_leaderboard_data(
170
+ enterprise_customer_uuid=enterprise_customer_uuid,
171
+ start_date=start_date,
172
+ end_date=end_date,
173
+ limit=page_size,
174
+ offset=0,
175
+ total_count=total_count,
176
+ )
177
+
178
+ @staticmethod
179
+ def __cache_skills_data(enterprise_customer_uuid):
180
+ """
181
+ Helper method to cache all the skills related data for the given enterprise.
182
+
183
+ Arguments:
184
+ enterprise_customer_uuid (str): The UUID of the enterprise customer.
185
+ """
186
+ start_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(
187
+ enterprise_customer_uuid,
188
+ )
189
+ end_date = date.today()
190
+ skills_table = SkillsDailyRollupAdminDashTable()
191
+ skills_table.get_top_skills(
192
+ enterprise_customer_uuid=enterprise_customer_uuid,
193
+ start_date=start_date,
194
+ end_date=end_date,
195
+ )
196
+ skills_table.get_top_skills_by_enrollment(
197
+ enterprise_customer_uuid=enterprise_customer_uuid,
198
+ start_date=start_date,
199
+ end_date=end_date,
200
+ )
201
+ skills_table.get_top_skills_by_completion(
202
+ enterprise_customer_uuid=enterprise_customer_uuid,
203
+ start_date=start_date,
204
+ end_date=end_date,
205
+ )
206
+
207
+ def handle(self, *args, **options):
208
+ for enterprise_customer_uuid in FactEnrollmentAdminDashTable().get_top_enterprises():
209
+ try:
210
+ self.__cache_enrollment_data(enterprise_customer_uuid)
211
+ self.__cache_completions_data(enterprise_customer_uuid)
212
+ self.__cache_engagement_data(enterprise_customer_uuid)
213
+ self.__cache_skills_data(enterprise_customer_uuid)
214
+ except Exception as exc:
215
+ info = (
216
+ 'Error trying to add cache entries for enterprise '
217
+ '{}: {}'.format(enterprise_customer_uuid, exc)
218
+ )
219
+ raise CommandError(info) from exc
@@ -0,0 +1,60 @@
1
+ """
2
+ Tests for `./manage.py pre_warm_analytics_cache` management command.
3
+ """
4
+ from datetime import datetime
5
+ from unittest import TestCase
6
+
7
+ from mock import MagicMock, patch
8
+ from pytest import mark
9
+
10
+ from django.core.management import call_command
11
+
12
+
13
+ @mark.django_db
14
+ class Test(TestCase):
15
+ """
16
+ Tests to validate the behavior of `./manage.py pre_warm_analytics_cache` management command.
17
+ """
18
+ def setUp(self):
19
+ """
20
+ Setup method.
21
+ """
22
+ super().setUp()
23
+ self.enterprise_uuid = 'ee5e6b3a-069a-4947-bb8d-d2dbc323396c'
24
+
25
+ get_enrollment_date_range_patcher = patch(
26
+ 'enterprise_data.api.v1.views.analytics_enrollments.FactEnrollmentAdminDashTable.get_enrollment_date_range',
27
+ return_value=(datetime.now(), datetime.now())
28
+ )
29
+
30
+ get_enrollment_date_range_patcher.start()
31
+ self.addCleanup(get_enrollment_date_range_patcher.stop)
32
+
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
+ )
45
+ @patch('enterprise_data.api.v1.views.analytics_enrollments.FactEnrollmentAdminDashTable.get_top_enterprises')
46
+ @patch('enterprise_data.cache.decorators.cache.set')
47
+ @patch('enterprise_data.cache.decorators.cache.get')
48
+ def test_pre_warm_analytics_cache(self, mock_get_cache, mock_set_cache, mock_get_top_enterprises):
49
+ """
50
+ Validate that the command caches the analytics data for a large enterprise.
51
+ """
52
+ mock_get_top_enterprises.return_value = [
53
+ self.enterprise_uuid
54
+ ]
55
+ mock_get_cache.return_value = MagicMock(is_found=False)
56
+
57
+ call_command('pre_warm_analytics_cache')
58
+
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
  )