edx-enterprise-data 10.10.1__py3-none-any.whl → 10.11.1__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.
Files changed (24) hide show
  1. {edx_enterprise_data-10.10.1.dist-info → edx_enterprise_data-10.11.1.dist-info}/METADATA +3 -2
  2. {edx_enterprise_data-10.10.1.dist-info → edx_enterprise_data-10.11.1.dist-info}/RECORD +24 -15
  3. {edx_enterprise_data-10.10.1.dist-info → edx_enterprise_data-10.11.1.dist-info}/WHEEL +1 -1
  4. enterprise_data/__init__.py +1 -1
  5. enterprise_data/admin_analytics/database/filters/__init__.py +5 -0
  6. enterprise_data/admin_analytics/database/filters/base.py +7 -0
  7. enterprise_data/admin_analytics/database/filters/fact_enrollment_admin_dash.py +51 -0
  8. enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py +19 -18
  9. enterprise_data/admin_analytics/database/query_filters/__init__.py +7 -0
  10. enterprise_data/admin_analytics/database/query_filters/base.py +68 -0
  11. enterprise_data/admin_analytics/database/query_filters/between.py +30 -0
  12. enterprise_data/admin_analytics/database/query_filters/equal.py +27 -0
  13. enterprise_data/admin_analytics/database/query_filters/in_.py +28 -0
  14. enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py +144 -34
  15. enterprise_data/api/v1/serializers.py +1 -0
  16. enterprise_data/api/v1/views/analytics_enrollments.py +12 -5
  17. enterprise_data/api/v1/views/enterprise_learner.py +13 -22
  18. enterprise_data/clients.py +39 -7
  19. enterprise_data/exceptions.py +21 -0
  20. enterprise_data/management/commands/pre_warm_analytics_cache.py +14 -8
  21. enterprise_data/tests/admin_analytics/mock_enrollments.py +5 -0
  22. enterprise_data_roles/tests/test_models.py +0 -1
  23. {edx_enterprise_data-10.10.1.dist-info → edx_enterprise_data-10.11.1.dist-info/licenses}/LICENSE +0 -0
  24. {edx_enterprise_data-10.10.1.dist-info → edx_enterprise_data-10.11.1.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: edx-enterprise-data
3
- Version: 10.10.1
3
+ Version: 10.11.1
4
4
  Summary: Enterprise Reporting
5
5
  Home-page: https://github.com/openedx/edx-enterprise-data
6
6
  Author: edX
@@ -45,6 +45,7 @@ Dynamic: classifier
45
45
  Dynamic: description
46
46
  Dynamic: home-page
47
47
  Dynamic: license
48
+ Dynamic: license-file
48
49
  Dynamic: provides-extra
49
50
  Dynamic: requires-dist
50
51
  Dynamic: summary
@@ -1,7 +1,9 @@
1
- enterprise_data/__init__.py,sha256=ZNUDCqsK0PsxTU8WCuIxtRJVuai59_dk0bJuCzGqMgY,125
1
+ edx_enterprise_data-10.11.1.dist-info/licenses/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
2
+ enterprise_data/__init__.py,sha256=j6aLs1tTZrhX0hqVukHt4A3LxK06eXBgt0DlKcYKKK4,125
2
3
  enterprise_data/apps.py,sha256=aF6hZwDfI2oWj95tUTm_2ikHueQj-jLj-u0GrgzpsQI,414
3
- enterprise_data/clients.py,sha256=pBuCQOP7NYl264ZbMtZCRPCET1pg0by8w6PVlA-icKA,4995
4
+ enterprise_data/clients.py,sha256=glw-fXu3V7rKEpiUBF056gZEdNK4KnWcP9tNYELimRQ,6566
4
5
  enterprise_data/constants.py,sha256=uCKjfpdlMYFZJsAj3n9RMw4Cmg5_6s3NuwocO-fch3s,238
6
+ enterprise_data/exceptions.py,sha256=4AI6mdxgH3aNYJm5RCfxsf19MFSFIXc9xTWwPeERgHc,420
5
7
  enterprise_data/filters.py,sha256=D2EiK12MMpBoz6eOUmTpoJEhj_sH7bA93NRRAdvkDVo,6163
6
8
  enterprise_data/models.py,sha256=gbZl-ei0qe_l-BFeE0gWeyfWqmgi9X9LAF8MYOEfk2s,26555
7
9
  enterprise_data/paginators.py,sha256=YPrC5TeXFt-ymenT2H8H2nCbDCnAzJQlH9kFPElRxWE,269
@@ -14,14 +16,22 @@ enterprise_data/admin_analytics/constants.py,sha256=-6uLAq5DUeA_rv5eUb9SeqlG3iVW
14
16
  enterprise_data/admin_analytics/data_loaders.py,sha256=b6RjEIxCol8ETQMY7QfwhqN9eEAvrUN_UldIG7rgsSY,736
15
17
  enterprise_data/admin_analytics/database/__init__.py,sha256=vNSWKf2VV5xMegN7htJJtxtQEb0ASLC6frE2w0ZpYpE,104
16
18
  enterprise_data/admin_analytics/database/utils.py,sha256=5u-d6ZQW95mF_r4bH8Xdi7DgpYAuDFOG_q0P-bjKXHU,1712
19
+ enterprise_data/admin_analytics/database/filters/__init__.py,sha256=ZteBm55429cn8zaUVYHRFA7InQPWP0b4XiZeq24HQT4,115
20
+ enterprise_data/admin_analytics/database/filters/base.py,sha256=nOek6XgHEVBKUTGk-K0PZpy0RSZB3D7kc8OD5nlqXcE,71
21
+ enterprise_data/admin_analytics/database/filters/fact_enrollment_admin_dash.py,sha256=WKRBWRCujiMay2ePdl9vnNN1ux-LO0edyb79W4dm-S4,1808
17
22
  enterprise_data/admin_analytics/database/queries/__init__.py,sha256=IC5TLOr_GnydbrVbl2mWhwO3aUbYeHuDmfPTLmwGhZA,218
18
23
  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=ikTly1_fb-xzrXUTpA7B2dRRYpcuDhbpxMBkxbs-DP4,11064
24
+ enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py,sha256=0YbIQvNL8pgi-apsoYzBSnDeouiqZIUlYzIH4xkbY_w,10942
20
25
  enterprise_data/admin_analytics/database/queries/skills_daily_rollup_admin_dash.py,sha256=ZaJPghTrQvBGsFxfpeR8EE45ujtaYI9R_xkoDDqD2So,4269
26
+ enterprise_data/admin_analytics/database/query_filters/__init__.py,sha256=xW9cf5KGpMs33tTlil5gzKq4RxcZVCJZESsrHo2X0E4,182
27
+ enterprise_data/admin_analytics/database/query_filters/base.py,sha256=XZUC1AIaDPBq0cwh2Xn5wp_DZfsD3HnMMvqFUt7iM2E,2205
28
+ enterprise_data/admin_analytics/database/query_filters/between.py,sha256=YvAM4WSJSCHb1nCzeJLwku4zkJKHrqnSrEnl7-xMOoA,924
29
+ enterprise_data/admin_analytics/database/query_filters/equal.py,sha256=Z4OPVSZmoC52HElwladgydSx2VHLJmptke0QS2mNbz8,744
30
+ enterprise_data/admin_analytics/database/query_filters/in_.py,sha256=RsqDVTgzJ0vvEMmtyefPNZh8U3eShLp7ZqY_J5oC45E,802
21
31
  enterprise_data/admin_analytics/database/tables/__init__.py,sha256=Z-c3P9hqR-dC9uYKe63qHkQG9Nms8cLE2jRN-4jeMM0,289
22
32
  enterprise_data/admin_analytics/database/tables/base.py,sha256=1KyKsC18pW3m-5U-T6pdt5rIwsz6Wp3QFFbD3r6L6YQ,395
23
33
  enterprise_data/admin_analytics/database/tables/fact_engagement_admin_dash.py,sha256=VMUGwfrUsYd_NnxEDNBovAmHwhdTCSImGA6JjHzknhk,12959
24
- enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py,sha256=cRRBFRc2p54BkW_h7GuUud-gWzJrqKep9dYgxOf7tIY,11741
34
+ enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py,sha256=okRav232A2TD2WzVsOq_HHOm3itp_-uJB6uUxypykLI,15864
25
35
  enterprise_data/admin_analytics/database/tables/skills_daily_rollup_admin_dash.py,sha256=3xNwSi0wfCyBHcXPd6-9Ujs1NUm8kmZRg_gPrZzp9nQ,3233
26
36
  enterprise_data/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
37
  enterprise_data/api/urls.py,sha256=POqc_KATHdnpMf9zHtpO46pKD5KAlAExtx7G6iylLcU,273
@@ -30,16 +40,16 @@ enterprise_data/api/v0/serializers.py,sha256=dngZTk6DhRxApchQKCMp1B_c8aVnQtH0NCq
30
40
  enterprise_data/api/v0/urls.py,sha256=vzJjqIo_S3AXWs9Us8XTaJc3FnxLbYzAkmLyuDQqum0,699
31
41
  enterprise_data/api/v0/views.py,sha256=4RslZ4NZOU-844bnebEQ71ji2utRY7jEijqC45oQQD0,14380
32
42
  enterprise_data/api/v1/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
33
- enterprise_data/api/v1/serializers.py,sha256=27LcGcIJPPVm4PedHdgQclrPpdKhtd3E0OJYf4u5sBU,13788
43
+ enterprise_data/api/v1/serializers.py,sha256=U3zxiideqwpoJuofI0dkx0AwbN2GUiELxqJIXIVwI9s,13843
34
44
  enterprise_data/api/v1/urls.py,sha256=Wl1xLboPg-Lq1ZvjAWf51JKYkHlmx02Kpq1nwfDyS8s,4372
35
45
  enterprise_data/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
46
  enterprise_data/api/v1/views/analytics_completions.py,sha256=esFbJ5q8ssnm2Mfbc3rZXtiGHF-MeM4KQ4Ft3N7wwHU,6260
37
47
  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
48
+ enterprise_data/api/v1/views/analytics_enrollments.py,sha256=_KBo4RhEmfQXcCOgvowD28WCVJwlc4APuCnRDbAYK9E,6563
39
49
  enterprise_data/api/v1/views/analytics_leaderboard.py,sha256=3dyo7_OhyGEEeibemBrRsUOo0jbM4LbDgV5gw3YnVig,4186
40
50
  enterprise_data/api/v1/views/base.py,sha256=Kkmd5zgEBAhvwS_GoGXSK6lgbDNwSPioYn-QbnizI3w,3416
41
51
  enterprise_data/api/v1/views/enterprise_admin.py,sha256=xxpb48cjuHkFJH-IQG0DwOvdH8RgyYD-aQjyfMMJNFg,9030
42
- enterprise_data/api/v1/views/enterprise_learner.py,sha256=noFAKFe1T8CXRSFt8aMajF-utPO4x2WK-kxdIYw5qE4,20601
52
+ enterprise_data/api/v1/views/enterprise_learner.py,sha256=vRzfaQCEoeq_tEY7KsNZJVzgn7XEWZ1l-n0oUoSVGyU,20093
43
53
  enterprise_data/api/v1/views/enterprise_offers.py,sha256=VifxgqTLFLVw4extYPlHcN1N_yjXcsYsAlYEnAbpb10,1266
44
54
  enterprise_data/cache/__init__.py,sha256=fiBUploll1kmDy2vCmnNpeZVTD4ewsgtRF14vVs0Rb4,1850
45
55
  enterprise_data/cache/decorators.py,sha256=vLbXK9VSv-HzVkkXS1-TkuSMxudwyxz04WFsAXqmjuM,1273
@@ -54,7 +64,7 @@ enterprise_data/management/commands/create_enterprise_learner_enrollment_lpr_v1.
54
64
  enterprise_data/management/commands/create_enterprise_learner_lpr_v1.py,sha256=bUYmZHA3yK3ZBPhV0wkpRDgH_Q2b5rVQnwSp2hRmh28,1799
55
65
  enterprise_data/management/commands/create_enterprise_offer.py,sha256=0R1eEKWTCGjod4I8qBH2UBD-erqj5mFtM_DG5Vxet_0,1150
56
66
  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
67
+ enterprise_data/management/commands/pre_warm_analytics_cache.py,sha256=i64hcbvqGW0UWYbx046iEETzCOcva1cdYumdb5RByGU,8522
58
68
  enterprise_data/management/commands/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
69
  enterprise_data/management/commands/tests/test_create_dummy_data_lpr_v1.py,sha256=wt9fqAFKQVTqllpZ42dch-n31JavUifUIB9CKNYcnYM,1086
60
70
  enterprise_data/management/commands/tests/test_create_enterprise_enrollment.py,sha256=5CABLk8qAx8RP8mrFtnbJ4-xVkf9-5Mq6iQcx8jBfFc,1742
@@ -121,7 +131,7 @@ enterprise_data/tests/test_utils.py,sha256=3sM3YWEoihHupgwhh0NiI66CbcQPLY4I_W0xj
121
131
  enterprise_data/tests/test_views.py,sha256=UvDRNTxruy5zBK_KgUy2cBMbwlaTW_vkM0-TCXbQZiY,69667
122
132
  enterprise_data/tests/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
123
133
  enterprise_data/tests/admin_analytics/mock_analytics_data.py,sha256=qN73YxoD1kAtKsQo94avR0DGMCsJClrpqFxxYC6reqE,15248
124
- enterprise_data/tests/admin_analytics/mock_enrollments.py,sha256=u61tkqSdVKExURJF7arvBoXvbdkF39CkjxQiTvVMXIE,4467
134
+ enterprise_data/tests/admin_analytics/mock_enrollments.py,sha256=n67vb0dQVbNaI1PvEPlo8ljkrlQ5RvnHpR2kVuOk1go,4632
125
135
  enterprise_data/tests/admin_analytics/test_analytics_engagements.py,sha256=gfO6RI2gbDMhOD4p4h7l6sZpisglBreTPtJRqNxIgo8,10440
126
136
  enterprise_data/tests/admin_analytics/test_analytics_enrollments.py,sha256=uFa6gnlf-CthctdYTWsw-pLLOxkELGiq8YFq5CyWFbs,10826
127
137
  enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py,sha256=iFT47BVlgf93u1K2l3_8HDcXY-9vWKp3iUaWM7V0C2U,5476
@@ -151,7 +161,7 @@ enterprise_data_roles/migrations/0007_enterprisedataroleassignment_applies_to_al
151
161
  enterprise_data_roles/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
152
162
  enterprise_data_roles/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
153
163
  enterprise_data_roles/tests/factories.py,sha256=VOm0NyOhiKkxl1MRIriRkKSpKypr9-4lG2FrsE2YfKo,1153
154
- enterprise_data_roles/tests/test_models.py,sha256=wJv7ywk0BSbjlW_U142h0aFxZleAHyT92nIiPy_ECW4,1476
164
+ enterprise_data_roles/tests/test_models.py,sha256=r7fcYXnzWjlAHUxvvC8y7taUQWYPhyXuCU-q1KHWeO4,1475
155
165
  enterprise_reporting/__init__.py,sha256=yQO9ureIxFnl-1a_34H53elDwuAzXrSmhLlzqqD2SJ0,112
156
166
  enterprise_reporting/constants.py,sha256=LAi9HPXDF5I8xT4E65YTO6FhnlK_6MdNpBeBVZ9Ckdk,211
157
167
  enterprise_reporting/delivery_method.py,sha256=JpNEDT3yk4IV27OsqIq0iLv3xcTkW9B285P91YBUrsA,6109
@@ -176,8 +186,7 @@ enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=zBj7sDvRLJQbRs
176
186
  enterprise_reporting/tests/test_utils.py,sha256=y4t6w9aKra-ftqtUeHvPwOhje-1npz7auV5o74ya8fE,9523
177
187
  enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
178
188
  enterprise_reporting/tests/utils.py,sha256=xms2LM7DV3wczXEfctOK1ddel1EE0J_YSr17UzbCDy4,1401
179
- edx_enterprise_data-10.10.1.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
180
- edx_enterprise_data-10.10.1.dist-info/METADATA,sha256=hDRA9ivxmE2KXvocGs8ttz8nEI_sKRVhNdUAHa5wN6w,1685
181
- edx_enterprise_data-10.10.1.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
182
- edx_enterprise_data-10.10.1.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
183
- edx_enterprise_data-10.10.1.dist-info/RECORD,,
189
+ edx_enterprise_data-10.11.1.dist-info/METADATA,sha256=qGem3YioOvmd2BpaHm7xLHVjzDkgWIHl-eW5jn3r_o0,1707
190
+ edx_enterprise_data-10.11.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
191
+ edx_enterprise_data-10.11.1.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
192
+ edx_enterprise_data-10.11.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.0.0)
2
+ Generator: setuptools (78.1.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.10.1"
5
+ __version__ = "10.11.1"
@@ -0,0 +1,5 @@
1
+ """
2
+ Query Filters for database tables.
3
+ """
4
+
5
+ from .fact_enrollment_admin_dash import FactEnrollmentAdminDashFilters
@@ -0,0 +1,7 @@
1
+ """
2
+ Base filters for database tables.
3
+ """
4
+
5
+
6
+ class BaseFilter:
7
+ pass
@@ -0,0 +1,51 @@
1
+ """
2
+ Query filters for enrollments table.
3
+ """
4
+ from enterprise_data.admin_analytics.database.filters.base import BaseFilter
5
+ from enterprise_data.admin_analytics.database.query_filters import BetweenQueryFilter, EqualQueryFilter, INQueryFilter
6
+
7
+
8
+ class FactEnrollmentAdminDashFilters(BaseFilter):
9
+ """
10
+ Query filters for enrollments table.
11
+ """
12
+ @staticmethod
13
+ def enterprise_customer_uuid_filter(enterprise_customer_uuid_params_key: str) -> EqualQueryFilter:
14
+ """
15
+ Filter by enterprise customer uuid.
16
+
17
+ Arguments:
18
+ enterprise_customer_uuid_params_key: The key against which value will be passed in the query.
19
+ """
20
+ return EqualQueryFilter(
21
+ column='enterprise_customer_uuid',
22
+ value_placeholder=enterprise_customer_uuid_params_key,
23
+ )
24
+
25
+ @staticmethod
26
+ def enterprise_enrollment_date_range_filter(
27
+ start_date_params_key: str, end_date_params_key: str
28
+ ) -> BetweenQueryFilter:
29
+ """
30
+ Filter by enrollment date to be in the given range.
31
+
32
+ Arguments:
33
+ start_date_params_key (str): The start date key against which value will be passed in the query.
34
+ end_date_params_key (str): The end date key against which value will be passed in the query.
35
+ """
36
+ return BetweenQueryFilter(
37
+ column='enterprise_enrollment_date',
38
+ range_placeholders=(start_date_params_key, end_date_params_key),
39
+ )
40
+
41
+ @staticmethod
42
+ def enterprise_user_id_in_filter(
43
+ enterprise_user_id_param_keys: list,
44
+ ) -> INQueryFilter:
45
+ """
46
+ Filter by enterprise user id.
47
+ """
48
+ return INQueryFilter(
49
+ column='enterprise_user_id',
50
+ values_placeholders=enterprise_user_id_param_keys
51
+ )
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Module containing queries for the fact_enrollment_admin_dash table.
3
3
  """
4
+ from ..query_filters import QueryFilters
4
5
 
5
6
 
6
7
  class FactEnrollmentAdminDashQueries:
@@ -20,27 +21,25 @@ class FactEnrollmentAdminDashQueries:
20
21
  """
21
22
 
22
23
  @staticmethod
23
- def get_enrollment_count_query():
24
+ def get_enrollment_count_query(query_filters: QueryFilters) -> str:
24
25
  """
25
26
  Get the query to fetch the total number of enrollments for an enterprise customer.
26
27
  """
27
- return """
28
+ return f"""
28
29
  SELECT count(*)
29
30
  FROM fact_enrollment_admin_dash
30
- WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
31
- enterprise_enrollment_date BETWEEN %(start_date)s AND %(end_date)s;
31
+ WHERE {query_filters.to_sql()};
32
32
  """
33
33
 
34
34
  @staticmethod
35
- def get_all_enrollments_query():
35
+ def get_all_enrollments_query(query_filters: QueryFilters) -> str:
36
36
  """
37
37
  Get the query to fetch all enrollments.
38
38
  """
39
- return """
39
+ return f"""
40
40
  SELECT email, course_title, course_subject, enroll_type, enterprise_enrollment_date
41
41
  FROM fact_enrollment_admin_dash
42
- WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
43
- enterprise_enrollment_date BETWEEN %(start_date)s AND %(end_date)s
42
+ WHERE {query_filters.to_sql()}
44
43
  ORDER BY ENTERPRISE_ENROLLMENT_DATE DESC LIMIT %(limit)s OFFSET %(offset)s
45
44
  """
46
45
 
@@ -98,21 +97,21 @@ class FactEnrollmentAdminDashQueries:
98
97
  """
99
98
 
100
99
  @staticmethod
101
- def get_top_courses_by_enrollments_query(record_count=10):
100
+ def get_top_courses_by_enrollments_query(query_filters: QueryFilters, record_count: int = 10) -> str:
102
101
  """
103
102
  Get the query to fetch the enrollment count by courses.
104
103
 
105
104
  Query will fetch the top N courses by enrollment count. Where N is the value of record_count.
106
105
 
107
106
  Arguments:
107
+ query_filters (QueryFilters): List of query filters.
108
108
  record_count (int): Number of records to fetch.
109
109
  """
110
110
  return f"""
111
111
  WITH filtered_data AS (
112
112
  SELECT *
113
113
  FROM fact_enrollment_admin_dash
114
- WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
115
- enterprise_enrollment_date BETWEEN %(start_date)s AND %(end_date)s
114
+ WHERE {query_filters.to_sql()}
116
115
  ),
117
116
  top_10_courses AS (
118
117
  SELECT course_key
@@ -134,7 +133,7 @@ class FactEnrollmentAdminDashQueries:
134
133
  """
135
134
 
136
135
  @staticmethod
137
- def get_top_subjects_by_enrollments_query(record_count=10):
136
+ def get_top_subjects_by_enrollments_query(query_filters: QueryFilters, record_count: int = 10) -> str:
138
137
  """
139
138
  Get the query to fetch the enrollment count by subjects.
140
139
 
@@ -142,13 +141,13 @@ class FactEnrollmentAdminDashQueries:
142
141
 
143
142
  Arguments:
144
143
  record_count (int): Number of records to fetch.
144
+ query_filters (QueryFilters): List of query filters.
145
145
  """
146
146
  return f"""
147
147
  WITH filtered_data AS (
148
148
  SELECT *
149
149
  FROM fact_enrollment_admin_dash
150
- WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
151
- enterprise_enrollment_date BETWEEN %(start_date)s AND %(end_date)s
150
+ WHERE {query_filters.to_sql()}
152
151
  ),
153
152
  top_10_subjects AS (
154
153
  SELECT course_subject
@@ -168,15 +167,17 @@ class FactEnrollmentAdminDashQueries:
168
167
  """
169
168
 
170
169
  @staticmethod
171
- def get_enrolment_time_series_data_query():
170
+ def get_enrolment_time_series_data_query(query_filters: QueryFilters) -> str:
172
171
  """
173
172
  Get the query to fetch the enrollment time series data with daily aggregation.
173
+
174
+ Arguments:
175
+ query_filters (QueryFilters): A list of filters for this query.
174
176
  """
175
- return """
177
+ return f"""
176
178
  SELECT enterprise_enrollment_date, enroll_type, COUNT(*) as enrollment_count
177
179
  FROM fact_enrollment_admin_dash
178
- WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
179
- enterprise_enrollment_date BETWEEN %(start_date)s AND %(end_date)s
180
+ WHERE {query_filters.to_sql()}
180
181
  GROUP BY enterprise_enrollment_date, enroll_type
181
182
  ORDER BY enterprise_enrollment_date;
182
183
  """
@@ -0,0 +1,7 @@
1
+ """
2
+ Query Filters to filter query data.
3
+ """
4
+ from .base import QueryFilters
5
+ from .between import BetweenQueryFilter
6
+ from .equal import EqualQueryFilter
7
+ from .in_ import INQueryFilter
@@ -0,0 +1,68 @@
1
+ """
2
+ Base Query Filter.
3
+ """
4
+ from abc import ABC, abstractmethod
5
+
6
+
7
+ class QueryFilter(ABC):
8
+ """
9
+ Base Query filter class for functionality common to all filters.
10
+ """
11
+ @abstractmethod
12
+ def to_sql(self) -> str:
13
+ """
14
+ Convert the filter to a SQL string.
15
+ """
16
+ raise NotImplementedError
17
+
18
+ @staticmethod
19
+ def validate_argument_exclusivity(value, value_placeholder):
20
+ """
21
+ Validate that that arguments are mutually exclusive.
22
+
23
+ This method will make sure that at-least one of the value or value_placeholder is provided. it will also
24
+ make sure that both value and value_placeholder are not provided at the same time.
25
+
26
+ Arguments:
27
+ value: The value of the filter.
28
+ value_placeholder: The placeholder for the value.
29
+ """
30
+ if value is not None and value_placeholder is not None:
31
+ raise ValueError('Both value and value_placeholder cannot be provided at the same time.')
32
+ if value is None and value_placeholder is None:
33
+ raise ValueError('Either value or value_placeholder must be provided.')
34
+ return True
35
+
36
+ def value_to_sql(self, value):
37
+ """
38
+ Convert the given value to a string in a way that it can be used inside WHERE clause of SQL query.
39
+
40
+ Note: This method is meant to evolve according to user needs. currently it handles the following data types.
41
+ More types can be added as needed.
42
+
43
+ 1. str: Encloses the string in single quotes.
44
+ 2. list<str>: Encloses the list of strings in a tuple after calling this method recursively on each item.
45
+
46
+ Arguments:
47
+ value (Any): The value to format.
48
+
49
+ Returns:
50
+ (Any): The formatted value.
51
+ """
52
+ if isinstance(value, str):
53
+ return f"'{value}'"
54
+ if isinstance(value, list):
55
+ return [self.value_to_sql(item) for item in value]
56
+
57
+ return value
58
+
59
+
60
+ class QueryFilters(list):
61
+ """
62
+ A list of QueryFilter objects.
63
+ """
64
+ def to_sql(self) -> str:
65
+ """
66
+ Convert the filters to a SQL string.
67
+ """
68
+ return ' AND '.join([_filter.to_sql() for _filter in self])
@@ -0,0 +1,30 @@
1
+ """
2
+ Query filter for between operation.
3
+ """
4
+ from .base import QueryFilter
5
+
6
+
7
+ class BetweenQueryFilter(QueryFilter):
8
+ """
9
+ Query filter for between operation.
10
+ """
11
+
12
+ def __init__(self, column: str, _range: tuple = None, range_placeholders: tuple = None):
13
+ """
14
+ Initialize the filter.
15
+
16
+ This will also validate arguments.
17
+ """
18
+ self.validate_argument_exclusivity(_range, range_placeholders)
19
+
20
+ self.column = column
21
+ self.range = _range
22
+ self.range_placeholders = range_placeholders
23
+
24
+ def to_sql(self) -> str:
25
+ if self.range:
26
+ lower, upper = self.range
27
+ return f'{self.column} BETWEEN {self.value_to_sql(lower)} AND {self.value_to_sql(upper)}'
28
+ else:
29
+ lower_placeholder, upper_placeholder = self.range_placeholders
30
+ return f'{self.column} BETWEEN %({lower_placeholder})s AND %({upper_placeholder})s'
@@ -0,0 +1,27 @@
1
+ """
2
+ Query filter for equal operation.
3
+ """
4
+ from .base import QueryFilter
5
+
6
+
7
+ class EqualQueryFilter(QueryFilter):
8
+ """
9
+ Query filter for equal operation.
10
+ """
11
+ def __init__(self, column: str, value: str = None, value_placeholder: str = None):
12
+ """
13
+ Initialize the filter.
14
+
15
+ This will also validate arguments.
16
+ """
17
+ self.validate_argument_exclusivity(value, value_placeholder)
18
+
19
+ self.column = column
20
+ self.value = value
21
+ self.value_placeholder = value_placeholder
22
+
23
+ def to_sql(self) -> str:
24
+ if self.value is not None:
25
+ return f'{self.column} = {self.value_to_sql(self.value)}'
26
+ else:
27
+ return f'{self.column} = %({self.value_placeholder})s'
@@ -0,0 +1,28 @@
1
+ """
2
+ Query filter for between operation.
3
+ """
4
+ from .base import QueryFilter
5
+
6
+
7
+ class INQueryFilter(QueryFilter):
8
+ """
9
+ Query filter for IN operation.
10
+ """
11
+
12
+ def __init__(self, column: str, values: list = None, values_placeholders: list = None):
13
+ """
14
+ Initialize the filter.
15
+
16
+ This will also validate arguments.
17
+ """
18
+ self.validate_argument_exclusivity(values, values_placeholders)
19
+
20
+ self.column = column
21
+ self.values = values
22
+ self.values_placeholders = values_placeholders
23
+
24
+ def to_sql(self) -> str:
25
+ if self.values is not None:
26
+ return f'{self.column} IN {str(tuple(self.value_to_sql(self.values)))}'
27
+ else:
28
+ return f'{self.column} IN {tuple(f"%({item})s" for item in self.values_placeholders)}'
@@ -2,20 +2,95 @@
2
2
  Module for interacting with the fact_enrollment_admin_dash table.
3
3
  """
4
4
  from datetime import date, datetime
5
+ from logging import getLogger
6
+ from typing import Optional, Tuple
5
7
  from uuid import UUID
6
8
 
7
9
  from enterprise_data.cache.decorators import cache_it
10
+ from enterprise_data.clients import EnterpriseApiClient
11
+ from enterprise_data.exceptions import EnterpriseApiClientException
8
12
 
13
+ from ..filters import FactEnrollmentAdminDashFilters
9
14
  from ..queries import FactEnrollmentAdminDashQueries
15
+ from ..query_filters import INQueryFilter, QueryFilters
10
16
  from ..utils import run_query
11
17
  from .base import BaseTable
12
18
 
19
+ LOGGER = getLogger(__name__)
20
+
13
21
 
14
22
  class FactEnrollmentAdminDashTable(BaseTable):
15
23
  """
16
24
  Class for communicating with the fact_enrollment_admin_dash table.
17
25
  """
18
26
  queries = FactEnrollmentAdminDashQueries()
27
+ filters = FactEnrollmentAdminDashFilters()
28
+
29
+ def __get_enterprise_user_query_filter( # pylint: disable=inconsistent-return-statements
30
+ self, group_uuid: Optional[UUID],
31
+ enterprise_customer_uuid: UUID
32
+ ) -> Optional[Tuple[INQueryFilter, dict]]:
33
+ """
34
+ Get the query filter to filter enrollments for enterprise users in the given group.
35
+
36
+ Arguments:
37
+ group_uuid (UUID): The UUID of the group.
38
+ enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
39
+
40
+ Returns:
41
+ (INQueryFilter | None): The query filter to filter enrollments for enterprise users in the given group.
42
+ """
43
+ if not group_uuid:
44
+ return None
45
+
46
+ try:
47
+ learners_in_group = EnterpriseApiClient.get_enterprise_user_ids_in_group(group_uuid)
48
+ except EnterpriseApiClientException:
49
+ LOGGER.exception(
50
+ "Failed to get learners in group [%s] for enterprise [%s]",
51
+ group_uuid,
52
+ enterprise_customer_uuid,
53
+ )
54
+ else:
55
+ params = {f'eu_{i}': enterprise_user_id for i, enterprise_user_id in enumerate(learners_in_group)}
56
+ return self.filters.enterprise_user_id_in_filter(list(params.keys())), params
57
+
58
+ def __get_common_query_filters(
59
+ self, enterprise_customer_uuid: UUID,
60
+ group_uuid: Optional[UUID],
61
+ start_date: date,
62
+ end_date: date,
63
+ ) -> (QueryFilters, dict):
64
+ """
65
+ Utility method to get query filters common in most usages below.
66
+
67
+ This will return a tuple containing the query filters list and the dictionary of query parameters that
68
+ will be used in the query.
69
+
70
+ It will contain the following query filters.
71
+ 1. enterprise_customer_uuid filter to filter records for an enterprise customer.
72
+ 2. enrollment_date range filter to filter records by enrollment date.
73
+ 3. group_uuid filter to filter records for learners who belong to the given group.
74
+ """
75
+ query_filters = QueryFilters([
76
+ self.filters.enterprise_customer_uuid_filter('enterprise_customer_uuid'),
77
+ self.filters.enterprise_enrollment_date_range_filter('start_date', 'end_date'),
78
+ ])
79
+ params = {
80
+ 'enterprise_customer_uuid': enterprise_customer_uuid,
81
+ 'start_date': start_date,
82
+ 'end_date': end_date,
83
+ }
84
+
85
+ response = self.__get_enterprise_user_query_filter(
86
+ group_uuid, enterprise_customer_uuid
87
+ )
88
+ if response is not None:
89
+ enterprise_user_id_in_filter, enterprise_user_id_params = response
90
+ query_filters.append(enterprise_user_id_in_filter)
91
+ params.update(enterprise_user_id_params)
92
+
93
+ return query_filters, params
19
94
 
20
95
  def get_top_enterprises(self, count=10):
21
96
  """
@@ -34,25 +109,32 @@ class FactEnrollmentAdminDashTable(BaseTable):
34
109
  return [row[0] for row in result]
35
110
 
36
111
  @cache_it()
37
- def get_enrollment_count(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
112
+ def get_enrollment_count(
113
+ self,
114
+ enterprise_customer_uuid: UUID,
115
+ group_uuid: Optional[UUID],
116
+ start_date: date,
117
+ end_date: date,
118
+ ) -> int:
38
119
  """
39
120
  Get the total number of enrollments for the given enterprise customer.
40
121
 
41
122
  Arguments:
42
123
  enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
124
+ group_uuid (UUID): The UUID of the group.
43
125
  start_date (date): The start date.
44
126
  end_date (date): The end date.
45
127
 
46
128
  Returns:
47
129
  (int): The total number of enrollments.
48
130
  """
131
+ query_filters, query_filter_params = self.__get_common_query_filters(
132
+ enterprise_customer_uuid, group_uuid, start_date, end_date
133
+ )
134
+
49
135
  results = run_query(
50
- query=self.queries.get_enrollment_count_query(),
51
- params={
52
- 'enterprise_customer_uuid': enterprise_customer_uuid,
53
- 'start_date': start_date,
54
- 'end_date': end_date,
55
- }
136
+ query=self.queries.get_enrollment_count_query(query_filters),
137
+ params=query_filter_params
56
138
  )
57
139
  if not results:
58
140
  return 0
@@ -60,13 +142,20 @@ class FactEnrollmentAdminDashTable(BaseTable):
60
142
 
61
143
  @cache_it()
62
144
  def get_all_enrollments(
63
- self, enterprise_customer_uuid: UUID, start_date: date, end_date: date, limit: int, offset: int
64
- ):
145
+ self,
146
+ enterprise_customer_uuid: UUID,
147
+ group_uuid: Optional[UUID],
148
+ start_date: date,
149
+ end_date: date,
150
+ limit: int,
151
+ offset: int,
152
+ ) -> list:
65
153
  """
66
154
  Get all enrollments for the given enterprise customer.
67
155
 
68
156
  Arguments:
69
157
  enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
158
+ group_uuid (UUID): The UUID of the group.
70
159
  start_date (date): The start date.
71
160
  end_date (date): The end date.
72
161
  limit (int): The maximum number of records to return.
@@ -75,12 +164,14 @@ class FactEnrollmentAdminDashTable(BaseTable):
75
164
  Returns:
76
165
  list<dict>: A list of dictionaries containing the enrollment data.
77
166
  """
167
+ query_filters, query_filter_params = self.__get_common_query_filters(
168
+ enterprise_customer_uuid, group_uuid, start_date, end_date
169
+ )
170
+
78
171
  return run_query(
79
- query=self.queries.get_all_enrollments_query(),
172
+ query=self.queries.get_all_enrollments_query(query_filters),
80
173
  params={
81
- 'enterprise_customer_uuid': enterprise_customer_uuid,
82
- 'start_date': start_date,
83
- 'end_date': end_date,
174
+ **query_filter_params,
84
175
  'limit': limit,
85
176
  'offset': offset,
86
177
  },
@@ -167,71 +258,90 @@ class FactEnrollmentAdminDashTable(BaseTable):
167
258
  return int(results[0][0] or 0)
168
259
 
169
260
  @cache_it()
170
- def get_top_courses_by_enrollments(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
261
+ def get_top_courses_by_enrollments(
262
+ self, enterprise_customer_uuid: UUID,
263
+ group_uuid: Optional[UUID],
264
+ start_date: date,
265
+ end_date: date
266
+ ) -> list:
171
267
  """
172
268
  Get the top courses enrollments for the given enterprise customer.
173
269
 
174
270
  Arguments:
175
271
  enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
272
+ group_uuid (UUID): The UUID of the group.
176
273
  start_date (date): The start date.
177
274
  end_date (date): The end date.
178
275
 
179
276
  Returns:
180
277
  list<dict>: A list of dictionaries containing the course key, course_title and enrollment count.
181
278
  """
279
+ query_filters, query_filter_params = self.__get_common_query_filters(
280
+ enterprise_customer_uuid, group_uuid, start_date, end_date
281
+ )
282
+
182
283
  return run_query(
183
- query=self.queries.get_top_courses_by_enrollments_query(),
184
- params={
185
- 'enterprise_customer_uuid': enterprise_customer_uuid,
186
- 'start_date': start_date,
187
- 'end_date': end_date,
188
- },
284
+ query=self.queries.get_top_courses_by_enrollments_query(query_filters),
285
+ params=query_filter_params,
189
286
  as_dict=True,
190
287
  )
191
288
 
192
289
  @cache_it()
193
- def get_top_subjects_by_enrollments(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
290
+ def get_top_subjects_by_enrollments(
291
+ self,
292
+ enterprise_customer_uuid: UUID,
293
+ group_uuid: Optional[UUID],
294
+ start_date: date,
295
+ end_date: date,
296
+ ) -> list:
194
297
  """
195
298
  Get the top subjects by enrollments for the given enterprise customer.
196
299
 
197
300
  Arguments:
198
301
  enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
302
+ group_uuid (UUID): The UUID of the group.
199
303
  start_date (date): The start date.
200
304
  end_date (date): The end date.
201
305
 
202
306
  Returns:
203
307
  list<dict>: A list of dictionaries containing the subject and enrollment count.
204
308
  """
309
+ query_filters, query_filter_params = self.__get_common_query_filters(
310
+ enterprise_customer_uuid, group_uuid, start_date, end_date
311
+ )
312
+
205
313
  return run_query(
206
- query=self.queries.get_top_subjects_by_enrollments_query(),
207
- params={
208
- 'enterprise_customer_uuid': enterprise_customer_uuid,
209
- 'start_date': start_date,
210
- 'end_date': end_date,
211
- },
314
+ query=self.queries.get_top_subjects_by_enrollments_query(query_filters),
315
+ params=query_filter_params,
212
316
  as_dict=True,
213
317
  )
214
318
 
215
319
  @cache_it()
216
- def get_enrolment_time_series_data(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
320
+ def get_enrolment_time_series_data(
321
+ self, enterprise_customer_uuid: UUID,
322
+ group_uuid: Optional[UUID],
323
+ start_date: date,
324
+ end_date: date
325
+ ) -> list:
217
326
  """
218
327
  Get the enrollment time series data for the given enterprise customer.
219
328
 
220
329
  Arguments:
221
330
  enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
331
+ group_uuid (UUID): The UUID of the group.
222
332
  start_date (date): The start date.
223
333
  end_date (date): The end date.
224
334
 
225
335
  Returns:
226
336
  list<dict>: A list of dictionaries containing the date and enrollment count.
227
337
  """
338
+ query_filters, query_filter_params = self.__get_common_query_filters(
339
+ enterprise_customer_uuid, group_uuid, start_date, end_date
340
+ )
341
+
228
342
  return run_query(
229
- query=self.queries.get_enrolment_time_series_data_query(),
230
- params={
231
- 'enterprise_customer_uuid': enterprise_customer_uuid,
232
- 'start_date': start_date,
233
- 'end_date': end_date,
234
- },
343
+ query=self.queries.get_enrolment_time_series_data_query(query_filters),
344
+ params=query_filter_params,
235
345
  as_dict=True,
236
346
  )
237
347
 
@@ -341,6 +341,7 @@ class AdvanceAnalyticsQueryParamSerializer(serializers.Serializer): # pylint: d
341
341
  response_type = serializers.CharField(required=False)
342
342
  page = serializers.IntegerField(required=False, min_value=1)
343
343
  page_size = serializers.IntegerField(required=False, min_value=2)
344
+ group_uuid = serializers.UUIDField(required=False)
344
345
 
345
346
  def validate(self, attrs):
346
347
  """
@@ -52,8 +52,11 @@ class AdvanceAnalyticsEnrollmentsView(AnalyticsPaginationMixin, ViewSet):
52
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
+ group_uuid = serializer.data.get('group_uuid')
56
+
55
57
  enrollments = FactEnrollmentAdminDashTable().get_all_enrollments(
56
58
  enterprise_customer_uuid=enterprise_uuid,
59
+ group_uuid=group_uuid,
57
60
  start_date=start_date,
58
61
  end_date=end_date,
59
62
  limit=page_size,
@@ -61,6 +64,7 @@ class AdvanceAnalyticsEnrollmentsView(AnalyticsPaginationMixin, ViewSet):
61
64
  )
62
65
  total_count = FactEnrollmentAdminDashTable().get_enrollment_count(
63
66
  enterprise_customer_uuid=enterprise_uuid,
67
+ group_uuid=group_uuid,
64
68
  start_date=start_date,
65
69
  end_date=end_date,
66
70
  )
@@ -78,7 +82,7 @@ class AdvanceAnalyticsEnrollmentsView(AnalyticsPaginationMixin, ViewSet):
78
82
 
79
83
  return StreamingHttpResponse(
80
84
  IndividualEnrollmentsCSVRenderer().render(self._stream_serialized_data(
81
- enterprise_uuid, start_date, end_date, total_count
85
+ enterprise_uuid, group_uuid, start_date, end_date, total_count
82
86
  )),
83
87
  content_type="text/csv",
84
88
  headers={"Content-Disposition": f'attachment; filename="{filename}"'},
@@ -93,7 +97,7 @@ class AdvanceAnalyticsEnrollmentsView(AnalyticsPaginationMixin, ViewSet):
93
97
  )
94
98
 
95
99
  @staticmethod
96
- def _stream_serialized_data(enterprise_uuid, start_date, end_date, total_count, page_size=50000):
100
+ def _stream_serialized_data(enterprise_uuid, group_uuid, start_date, end_date, total_count, page_size=50000):
97
101
  """
98
102
  Stream the serialized data.
99
103
  """
@@ -101,6 +105,7 @@ class AdvanceAnalyticsEnrollmentsView(AnalyticsPaginationMixin, ViewSet):
101
105
  while offset < total_count:
102
106
  enrollments = FactEnrollmentAdminDashTable().get_all_enrollments(
103
107
  enterprise_customer_uuid=enterprise_uuid,
108
+ group_uuid=group_uuid,
104
109
  start_date=start_date,
105
110
  end_date=end_date,
106
111
  limit=page_size,
@@ -132,16 +137,18 @@ class AdvanceAnalyticsEnrollmentsView(AnalyticsPaginationMixin, ViewSet):
132
137
  # get values from query params or use default
133
138
  start_date = serializer.data.get('start_date', min_enrollment_date)
134
139
  end_date = serializer.data.get('end_date', date.today())
140
+ group_uuid = serializer.data.get('group_uuid')
141
+
135
142
  with timer('construct_enrollment_all_stats'):
136
143
  data = {
137
144
  'enrollments_over_time': FactEnrollmentAdminDashTable().get_enrolment_time_series_data(
138
- enterprise_uuid, start_date, end_date
145
+ enterprise_uuid, group_uuid, start_date, end_date
139
146
  ),
140
147
  'top_courses_by_enrollments': FactEnrollmentAdminDashTable().get_top_courses_by_enrollments(
141
- enterprise_uuid, start_date, end_date,
148
+ enterprise_uuid, group_uuid, start_date, end_date,
142
149
  ),
143
150
  'top_subjects_by_enrollments': FactEnrollmentAdminDashTable().get_top_subjects_by_enrollments(
144
- enterprise_uuid, start_date, end_date,
151
+ enterprise_uuid, group_uuid, start_date, end_date,
145
152
  ),
146
153
  }
147
154
  return Response(data)
@@ -22,6 +22,7 @@ from django.utils import timezone
22
22
  from enterprise_data.admin_analytics.database.utils import LOGGER
23
23
  from enterprise_data.api.v1 import serializers
24
24
  from enterprise_data.clients import EnterpriseApiClient
25
+ from enterprise_data.exceptions import EnterpriseApiClientException
25
26
  from enterprise_data.models import EnterpriseGroupMembership, EnterpriseLearner, EnterpriseLearnerEnrollment
26
27
  from enterprise_data.paginators import EnterpriseEnrollmentsPagination
27
28
  from enterprise_data.renderers import EnrollmentsCSVRenderer
@@ -202,29 +203,19 @@ class EnterpriseLearnerEnrollmentViewSet(EnterpriseViewSetMixin, viewsets.ReadOn
202
203
 
203
204
  # First, filter the queryset using flex_group_exists
204
205
  filtered_queryset = queryset.filter(Exists(flex_group_exists))
206
+ if filtered_queryset.exists():
207
+ return filtered_queryset
205
208
 
206
- # If no records are found, attempt to fetch group_learners from the API
207
- if not filtered_queryset.exists():
208
- try:
209
- enterprise_api_client = EnterpriseApiClient(
210
- settings.BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL,
211
- settings.BACKEND_SERVICE_EDX_OAUTH2_KEY,
212
- settings.BACKEND_SERVICE_EDX_OAUTH2_SECRET,
213
- )
214
- group_learners = enterprise_api_client.get_enterprise_group_learners(group_uuid)
215
- if group_learners:
216
- group_learners_ids = [learner['enterprise_customer_user_id'] for learner in group_learners]
217
- queryset = queryset.filter(enterprise_user_id__in=group_learners_ids)
218
- else:
219
- LOGGER.warning(f"No group learners found for group: {group_uuid}")
220
- raise NotFound(f"No learners found for group: {group_uuid}")
221
- except (Exception) as e: # pylint: disable=broad-exception-caught
222
- LOGGER.error("Failed to fetch group learners from API: %s", e)
223
- queryset = queryset.none() # API call failed or unexpected error, return an empty queryset
224
- else:
225
- queryset = filtered_queryset
226
-
227
- return queryset
209
+ try:
210
+ return queryset.filter(
211
+ enterprise_user_id__in=EnterpriseApiClient.get_enterprise_user_ids_in_group(group_uuid)
212
+ )
213
+ except EnterpriseApiClientException as error:
214
+ LOGGER.warning(f'No group learners found for group: {group_uuid}')
215
+ raise NotFound(f'No learners found for group: {group_uuid}') from error
216
+ except Exception as e: # pylint: disable=broad-exception-caught
217
+ LOGGER.error('Failed to fetch group learners from API: %s', e)
218
+ return queryset.none() # API call failed or unexpected error, return an empty queryset
228
219
 
229
220
  def filter_by_offer_id(self, queryset, offer_id):
230
221
  """
@@ -1,10 +1,10 @@
1
1
  """
2
2
  Clients used to connect to other systems.
3
3
  """
4
-
5
-
6
4
  import logging
5
+ from typing import Any, List
7
6
  from urllib.parse import urljoin
7
+ from uuid import UUID
8
8
 
9
9
  from edx_django_utils.cache import TieredCache
10
10
  from edx_rest_api_client.client import OAuthAPIClient
@@ -13,9 +13,11 @@ from rest_framework.exceptions import NotFound, ParseError
13
13
 
14
14
  from django.conf import settings
15
15
 
16
+ from enterprise_data.exceptions import EnterpriseApiClientException
16
17
  from enterprise_data.utils import get_cache_key
17
18
 
18
19
  DEFAULT_REPORTING_CACHE_TIMEOUT = 60 * 60 * 6 # 6 hours (Value is in seconds)
20
+ GROUP_DATA_CACHE_TIMEOUT = 60 * 60 # 1 hour (Value is in seconds)
19
21
  LOGGER = logging.getLogger('enterprise_data')
20
22
 
21
23
 
@@ -102,16 +104,28 @@ class EnterpriseApiClient(OAuthAPIClient):
102
104
 
103
105
  return data
104
106
 
105
- def get_enterprise_group_learners(self, group_uuid):
107
+ def get_enterprise_group_learners(self, group_uuid: UUID) -> List[Any]:
106
108
  """
107
109
  Get the learners associated with a given enterprise group.
108
110
 
109
111
  Returns: list of learners or None if unable to retrieve or no learners exist
110
112
  """
111
- LOGGER.info(f'[EnterpriseApiClient] getting learners for enterprise group:{group_uuid}')
113
+ LOGGER.info(f'[EnterpriseApiClient] getting learners for enterprise group: {group_uuid}')
114
+
115
+ cache_key = get_cache_key(
116
+ resource='enterprise-group-learners',
117
+ group_uuid=group_uuid,
118
+ )
119
+ cached_response = TieredCache.get_cached_response(cache_key)
120
+ if cached_response.is_found:
121
+ LOGGER.info(
122
+ f'[EnterpriseApiClient] cache info found for enterprise group: {group_uuid}'
123
+ f' with {cached_response.value}'
124
+ )
125
+ return cached_response.value
126
+
112
127
  url = urljoin(self.API_BASE_URL, f'enterprise-group/{group_uuid}/learners/')
113
128
  all_learners = []
114
-
115
129
  try:
116
130
  while url:
117
131
  response = self.get(url)
@@ -119,12 +133,30 @@ class EnterpriseApiClient(OAuthAPIClient):
119
133
  data = response.json()
120
134
  all_learners.extend(data.get('results', []))
121
135
  url = data.get('next') # Get the URL for the next page, if any
122
-
123
136
  except (HTTPError, RequestException) as exc:
124
137
  LOGGER.warning(
125
138
  "[Data Overview Failure] Unable to retrieve Enterprise Group Learners details. "
126
139
  f"Group: {group_uuid}, Exception: {exc}"
127
140
  )
128
- return None
141
+ raise EnterpriseApiClientException(
142
+ f'Unable to process Enterprise Group Learners details for group {group_uuid}'
143
+ ) from exc
129
144
 
145
+ TieredCache.set_all_tiers(cache_key, all_learners, GROUP_DATA_CACHE_TIMEOUT)
130
146
  return all_learners
147
+
148
+ @staticmethod
149
+ def get_enterprise_user_ids_in_group(group_uuid: UUID) -> List[int]:
150
+ """
151
+ Get a list of all enterprise customer user ids that belong to the given group.
152
+
153
+ Arguments:
154
+ group_uuid (str|UUID): The group uuid.
155
+ """
156
+ enterprise_api_client = EnterpriseApiClient(
157
+ settings.BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL,
158
+ settings.BACKEND_SERVICE_EDX_OAUTH2_KEY,
159
+ settings.BACKEND_SERVICE_EDX_OAUTH2_SECRET,
160
+ )
161
+ group_learners = enterprise_api_client.get_enterprise_group_learners(group_uuid)
162
+ return [learner['enterprise_customer_user_id'] for learner in group_learners]
@@ -0,0 +1,21 @@
1
+ """
2
+ Custom Exceptions related to enterprise data.
3
+ """
4
+
5
+
6
+ class EnterpriseDataException(Exception):
7
+ """
8
+ Base exception for enterprise data.
9
+ """
10
+
11
+ def __init__(self, message: str):
12
+ """
13
+ Initialize the exception with a message.
14
+ """
15
+ super().__init__(message)
16
+
17
+
18
+ class EnterpriseApiClientException(EnterpriseDataException):
19
+ """
20
+ Exception for enterprise API client.
21
+ """
@@ -2,6 +2,7 @@
2
2
  Management command for pre-warming the analytics cache for large enterprises.
3
3
  """
4
4
  from datetime import date
5
+ from uuid import UUID
5
6
 
6
7
  from django.core.management.base import BaseCommand, CommandError
7
8
 
@@ -21,12 +22,12 @@ class Command(BaseCommand):
21
22
  help = 'Pre-warm the analytics cache for a large enterprises.'
22
23
 
23
24
  @staticmethod
24
- def __cache_enrollment_data(enterprise_customer_uuid):
25
+ def __cache_enrollment_data(enterprise_customer_uuid: UUID):
25
26
  """
26
27
  Helper method to cache all the enrollment related data for the given enterprise.
27
28
 
28
29
  Arguments:
29
- enterprise_customer_uuid (str): The UUID of the enterprise customer.
30
+ enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
30
31
  """
31
32
  enterprise_enrollment_table = FactEnrollmentAdminDashTable()
32
33
  start_date, _ = enterprise_enrollment_table.get_enrollment_date_range(
@@ -35,12 +36,14 @@ class Command(BaseCommand):
35
36
  end_date = date.today()
36
37
  enterprise_enrollment_table.get_enrollment_count(
37
38
  enterprise_customer_uuid=enterprise_customer_uuid,
39
+ group_uuid=None,
38
40
  start_date=start_date,
39
41
  end_date=end_date,
40
42
  )
41
43
  page_size = 100
42
44
  enterprise_enrollment_table.get_all_enrollments(
43
45
  enterprise_customer_uuid=enterprise_customer_uuid,
46
+ group_uuid=None,
44
47
  start_date=start_date,
45
48
  end_date=end_date,
46
49
  limit=page_size,
@@ -58,27 +61,30 @@ class Command(BaseCommand):
58
61
  )
59
62
  enterprise_enrollment_table.get_top_courses_by_enrollments(
60
63
  enterprise_customer_uuid=enterprise_customer_uuid,
64
+ group_uuid=None,
61
65
  start_date=start_date,
62
66
  end_date=end_date,
63
67
  )
64
68
  enterprise_enrollment_table.get_top_subjects_by_enrollments(
65
69
  enterprise_customer_uuid=enterprise_customer_uuid,
70
+ group_uuid=None,
66
71
  start_date=start_date,
67
72
  end_date=end_date,
68
73
  )
69
74
  enterprise_enrollment_table.get_enrolment_time_series_data(
70
75
  enterprise_customer_uuid=enterprise_customer_uuid,
76
+ group_uuid=None,
71
77
  start_date=start_date,
72
78
  end_date=end_date,
73
79
  )
74
80
 
75
81
  @staticmethod
76
- def __cache_completions_data(enterprise_customer_uuid):
82
+ def __cache_completions_data(enterprise_customer_uuid: UUID):
77
83
  """
78
84
  Helper method to cache all the completions related data for the given enterprise.
79
85
 
80
86
  Arguments:
81
- enterprise_customer_uuid (str): The UUID of the enterprise customer.
87
+ enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
82
88
  """
83
89
  enterprise_enrollment_table = FactEnrollmentAdminDashTable()
84
90
  start_date, _ = enterprise_enrollment_table.get_enrollment_date_range(
@@ -116,12 +122,12 @@ class Command(BaseCommand):
116
122
  )
117
123
 
118
124
  @staticmethod
119
- def __cache_engagement_data(enterprise_customer_uuid):
125
+ def __cache_engagement_data(enterprise_customer_uuid: UUID):
120
126
  """
121
127
  Helper method to cache all the engagement related data for the given enterprise.
122
128
 
123
129
  Arguments:
124
- enterprise_customer_uuid (str): The UUID of the enterprise customer.
130
+ enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
125
131
  """
126
132
  start_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(
127
133
  enterprise_customer_uuid,
@@ -176,12 +182,12 @@ class Command(BaseCommand):
176
182
  )
177
183
 
178
184
  @staticmethod
179
- def __cache_skills_data(enterprise_customer_uuid):
185
+ def __cache_skills_data(enterprise_customer_uuid: UUID):
180
186
  """
181
187
  Helper method to cache all the skills related data for the given enterprise.
182
188
 
183
189
  Arguments:
184
- enterprise_customer_uuid (str): The UUID of the enterprise customer.
190
+ enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
185
191
  """
186
192
  start_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(
187
193
  enterprise_customer_uuid,
@@ -23,6 +23,7 @@ ENROLLMENTS = [
23
23
  "passed_date_raw": "2021-08-25",
24
24
  "passed_date": "2021-08-25",
25
25
  "has_passed": 1,
26
+ "enterprise_user_id": 1,
26
27
  },
27
28
  {
28
29
  "enterprise_customer_name": "Hill Ltd",
@@ -46,6 +47,7 @@ ENROLLMENTS = [
46
47
  "passed_date_raw": "2021-09-01",
47
48
  "passed_date": "2021-09-01",
48
49
  "has_passed": 1,
50
+ "enterprise_user_id": 1,
49
51
  },
50
52
  {
51
53
  "enterprise_customer_name": "Hill Ltd",
@@ -69,6 +71,7 @@ ENROLLMENTS = [
69
71
  "passed_date_raw": None,
70
72
  "passed_date": '2022-08-24',
71
73
  "has_passed": 0,
74
+ "enterprise_user_id": 1,
72
75
  },
73
76
  {
74
77
  "enterprise_customer_name": "Hill Ltd",
@@ -92,6 +95,7 @@ ENROLLMENTS = [
92
95
  "passed_date_raw": None,
93
96
  "passed_date": "2022-08-24",
94
97
  "has_passed": 0,
98
+ "enterprise_user_id": 1,
95
99
  },
96
100
  {
97
101
  "enterprise_customer_name": "Hill Ltd",
@@ -115,5 +119,6 @@ ENROLLMENTS = [
115
119
  "passed_date_raw": None,
116
120
  "passed_date": "2022-08-20",
117
121
  "has_passed": 0,
122
+ "enterprise_user_id": 1,
118
123
  },
119
124
  ]
@@ -2,7 +2,6 @@
2
2
  Tests for the `enterprise-data` models module.
3
3
  """
4
4
 
5
-
6
5
  import unittest
7
6
 
8
7
  import ddt