edx-enterprise-data 10.10.1__py3-none-any.whl → 10.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {edx_enterprise_data-10.10.1.dist-info → edx_enterprise_data-10.11.0.dist-info}/METADATA +3 -2
- {edx_enterprise_data-10.10.1.dist-info → edx_enterprise_data-10.11.0.dist-info}/RECORD +24 -15
- {edx_enterprise_data-10.10.1.dist-info → edx_enterprise_data-10.11.0.dist-info}/WHEEL +1 -1
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/database/filters/__init__.py +5 -0
- enterprise_data/admin_analytics/database/filters/base.py +7 -0
- enterprise_data/admin_analytics/database/filters/fact_enrollment_admin_dash.py +51 -0
- enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py +19 -18
- enterprise_data/admin_analytics/database/query_filters/__init__.py +7 -0
- enterprise_data/admin_analytics/database/query_filters/base.py +68 -0
- enterprise_data/admin_analytics/database/query_filters/between.py +30 -0
- enterprise_data/admin_analytics/database/query_filters/equal.py +27 -0
- enterprise_data/admin_analytics/database/query_filters/in_.py +28 -0
- enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py +144 -34
- enterprise_data/api/v1/serializers.py +1 -0
- enterprise_data/api/v1/views/analytics_enrollments.py +12 -5
- enterprise_data/api/v1/views/enterprise_learner.py +13 -22
- enterprise_data/clients.py +39 -7
- enterprise_data/exceptions.py +21 -0
- enterprise_data/management/commands/pre_warm_analytics_cache.py +14 -8
- enterprise_data/tests/admin_analytics/mock_enrollments.py +5 -0
- enterprise_data_roles/tests/test_models.py +0 -1
- {edx_enterprise_data-10.10.1.dist-info → edx_enterprise_data-10.11.0.dist-info/licenses}/LICENSE +0 -0
- {edx_enterprise_data-10.10.1.dist-info → edx_enterprise_data-10.11.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: edx-enterprise-data
|
3
|
-
Version: 10.
|
3
|
+
Version: 10.11.0
|
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
|
-
|
1
|
+
edx_enterprise_data-10.11.0.dist-info/licenses/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
|
2
|
+
enterprise_data/__init__.py,sha256=nDaMECZ0E2QipjI60KC7fwBRg7oDJLQr1bfT6czggFQ,125
|
2
3
|
enterprise_data/apps.py,sha256=aF6hZwDfI2oWj95tUTm_2ikHueQj-jLj-u0GrgzpsQI,414
|
3
|
-
enterprise_data/clients.py,sha256=
|
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=
|
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=
|
34
|
+
enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py,sha256=9YPX9vdNGQD-bqpRd_voqH8lJ5mSakibc2FgFKfJPPk,15828
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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.
|
180
|
-
edx_enterprise_data-10.
|
181
|
-
edx_enterprise_data-10.
|
182
|
-
edx_enterprise_data-10.
|
183
|
-
edx_enterprise_data-10.10.1.dist-info/RECORD,,
|
189
|
+
edx_enterprise_data-10.11.0.dist-info/METADATA,sha256=BA0kvVq5-XmTgHE-p05B0oL10L49owYI6kl3_2RBvJY,1707
|
190
|
+
edx_enterprise_data-10.11.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
191
|
+
edx_enterprise_data-10.11.0.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
|
192
|
+
edx_enterprise_data-10.11.0.dist-info/RECORD,,
|
enterprise_data/__init__.py
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
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,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}': i for i in range(len(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(
|
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,
|
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
|
-
|
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(
|
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(
|
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(
|
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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
"""
|
enterprise_data/clients.py
CHANGED
@@ -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
|
-
|
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 (
|
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 (
|
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 (
|
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 (
|
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
|
]
|
{edx_enterprise_data-10.10.1.dist-info → edx_enterprise_data-10.11.0.dist-info/licenses}/LICENSE
RENAMED
File without changes
|
{edx_enterprise_data-10.10.1.dist-info → edx_enterprise_data-10.11.0.dist-info}/top_level.txt
RENAMED
File without changes
|