edx-enterprise-data 8.0.0__py3-none-any.whl → 8.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.2.0.dist-info}/METADATA +4 -1
- {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.2.0.dist-info}/RECORD +33 -19
- {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.2.0.dist-info}/WHEEL +1 -1
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/__init__.py +0 -0
- enterprise_data/admin_analytics/constants.py +15 -0
- enterprise_data/admin_analytics/data_loaders.py +137 -0
- enterprise_data/admin_analytics/database.py +31 -0
- enterprise_data/admin_analytics/utils.py +81 -0
- enterprise_data/api/v1/serializers.py +20 -0
- enterprise_data/api/v1/urls.py +13 -6
- enterprise_data/api/v1/views/__init__.py +0 -0
- enterprise_data/api/v1/views/base.py +26 -0
- enterprise_data/api/v1/views/enterprise_admin.py +109 -0
- enterprise_data/api/v1/{views.py → views/enterprise_learner.py} +5 -114
- enterprise_data/api/v1/views/enterprise_offers.py +41 -0
- enterprise_data/tests/admin_analytics/__init__.py +0 -0
- enterprise_data/tests/admin_analytics/test_data_loaders.py +86 -0
- enterprise_data/tests/admin_analytics/test_utils.py +102 -0
- enterprise_data/tests/api/v1/views/__init__.py +0 -0
- enterprise_data/tests/api/v1/views/test_enterprise_admin.py +82 -0
- enterprise_data/tests/test_filters.py +1 -1
- enterprise_data/tests/test_utils.py +73 -0
- enterprise_data/utils.py +48 -1
- enterprise_reporting/clients/__init__.py +2 -3
- enterprise_reporting/external_resource_link_report.py +3 -3
- enterprise_reporting/tests/test_clients.py +1 -1
- enterprise_reporting/tests/test_enterprise_client.py +2 -5
- enterprise_reporting/tests/test_external_link_report.py +2 -2
- enterprise_reporting/tests/test_utils.py +3 -3
- enterprise_reporting/utils.py +1 -1
- {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.2.0.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.2.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: edx-enterprise-data
|
3
|
-
Version: 8.
|
3
|
+
Version: 8.2.0
|
4
4
|
Summary: Enterprise Reporting
|
5
5
|
Home-page: https://github.com/openedx/edx-enterprise-data
|
6
6
|
Author: edX
|
@@ -23,6 +23,9 @@ Requires-Dist: edx-opaque-keys
|
|
23
23
|
Requires-Dist: edx-rbac
|
24
24
|
Requires-Dist: edx-rest-api-client
|
25
25
|
Requires-Dist: factory-boy
|
26
|
+
Requires-Dist: mysql-connector-python
|
27
|
+
Requires-Dist: numpy <=1.24.4
|
28
|
+
Requires-Dist: pandas <=2.0.3
|
26
29
|
Requires-Dist: requests
|
27
30
|
Requires-Dist: rules
|
28
31
|
Provides-Extra: reporting
|
@@ -1,4 +1,4 @@
|
|
1
|
-
enterprise_data/__init__.py,sha256=
|
1
|
+
enterprise_data/__init__.py,sha256=m1B80oppzBzILf9k9JMTLdt0jtuzOm21d91sVIZvtmM,123
|
2
2
|
enterprise_data/apps.py,sha256=aF6hZwDfI2oWj95tUTm_2ikHueQj-jLj-u0GrgzpsQI,414
|
3
3
|
enterprise_data/clients.py,sha256=GvQupy5TVYfO_IKC3yzXSAgNP54r-PtIjidM5ws9Iks,3947
|
4
4
|
enterprise_data/constants.py,sha256=uCKjfpdlMYFZJsAj3n9RMw4Cmg5_6s3NuwocO-fch3s,238
|
@@ -8,7 +8,12 @@ enterprise_data/paginators.py,sha256=YPrC5TeXFt-ymenT2H8H2nCbDCnAzJQlH9kFPElRxWE
|
|
8
8
|
enterprise_data/renderers.py,sha256=WVt0qy9Ippdnl404Zsq-MruB9oQfY6h87ZzpScYBeaw,1770
|
9
9
|
enterprise_data/signals.py,sha256=8eqNPnlvmfsKf19lGWv5xTIuBgQIqR8EZSp9UYzC8Rc,1024
|
10
10
|
enterprise_data/urls.py,sha256=bqtKF5OEWEwrNmHG3os-pZNuNsmjlhxEqp7yM4TbPf4,243
|
11
|
-
enterprise_data/utils.py,sha256=
|
11
|
+
enterprise_data/utils.py,sha256=UruTQLJcEl-q5UWGPnJZrnBnV1OvzBvhuoB_JkRY4Is,2328
|
12
|
+
enterprise_data/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
+
enterprise_data/admin_analytics/constants.py,sha256=m7mK2PxkwnuzRAWlcPfMRdjY1r8D3dpA_i6PD0_aqBM,587
|
14
|
+
enterprise_data/admin_analytics/data_loaders.py,sha256=jP9CgUt_93Bl_ak1_6dBPN4PAEdDT1da3sEMjJ45NLg,4281
|
15
|
+
enterprise_data/admin_analytics/database.py,sha256=PZRJ-URmZKo7ib88iEZbN61Nhp6uwc5NpzYrrff-gTg,857
|
16
|
+
enterprise_data/admin_analytics/utils.py,sha256=Cock_rTtHkBEKcG3cq7VcYRPEuxC_SyJBlZn9KdVeCA,2465
|
12
17
|
enterprise_data/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
18
|
enterprise_data/api/urls.py,sha256=POqc_KATHdnpMf9zHtpO46pKD5KAlAExtx7G6iylLcU,273
|
14
19
|
enterprise_data/api/v0/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
|
@@ -16,9 +21,13 @@ enterprise_data/api/v0/serializers.py,sha256=dngZTk6DhRxApchQKCMp1B_c8aVnQtH0NCq
|
|
16
21
|
enterprise_data/api/v0/urls.py,sha256=vzJjqIo_S3AXWs9Us8XTaJc3FnxLbYzAkmLyuDQqum0,699
|
17
22
|
enterprise_data/api/v0/views.py,sha256=4RslZ4NZOU-844bnebEQ71ji2utRY7jEijqC45oQQD0,14380
|
18
23
|
enterprise_data/api/v1/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
|
19
|
-
enterprise_data/api/v1/serializers.py,sha256=
|
20
|
-
enterprise_data/api/v1/urls.py,sha256=
|
21
|
-
enterprise_data/api/v1/views.py,sha256=
|
24
|
+
enterprise_data/api/v1/serializers.py,sha256=AOHumabnzwK59nytoOZEWhA3f9EL_CV-bXPsYSH3LI8,8263
|
25
|
+
enterprise_data/api/v1/urls.py,sha256=IQMAD9sZp0QeJa2cUd-WxwugWaW4hHZetQGulTjnrGc,1633
|
26
|
+
enterprise_data/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
27
|
+
enterprise_data/api/v1/views/base.py,sha256=FTAxlz5EzvAY657wzVgzhJPFSCHHzct7IDcvm71Smt8,866
|
28
|
+
enterprise_data/api/v1/views/enterprise_admin.py,sha256=mvpufYoqyaGbHuclRpFj-cQxeME1j4L6S6qQ72Rxem0,4727
|
29
|
+
enterprise_data/api/v1/views/enterprise_learner.py,sha256=yABjJje3CT8I8YOhWr1_tTkdKtnGJom8eu3EFz_-0BU,18517
|
30
|
+
enterprise_data/api/v1/views/enterprise_offers.py,sha256=VifxgqTLFLVw4extYPlHcN1N_yjXcsYsAlYEnAbpb10,1266
|
22
31
|
enterprise_data/fixtures/enterprise_enrollment.json,sha256=6onPXXR29pMdTdbl_mn81sDi3Re5jkLUZz2TPMB_1IY,5786
|
23
32
|
enterprise_data/fixtures/enterprise_user.json,sha256=6g8GvNY9j_fh1dvAU80bTAMI2F5vXCkb8a4UjsftMvQ,1970
|
24
33
|
enterprise_data/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -84,16 +93,21 @@ enterprise_data/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
84
93
|
enterprise_data/tests/factories.py,sha256=EqiB8xDVN9n6ZrFV0d4h6hEiRF57dpbmOjHezU7GHME,1964
|
85
94
|
enterprise_data/tests/mixins.py,sha256=YifptI9mtOhAWnBGyPUy4kX5OJNSDP3DvW2vb1E2tvw,805
|
86
95
|
enterprise_data/tests/test_clients.py,sha256=xBPHF9cgEFqNJoL4klOoYh_sVS3scZGcX0Ltc9Ghp7A,6336
|
87
|
-
enterprise_data/tests/test_filters.py,sha256=
|
96
|
+
enterprise_data/tests/test_filters.py,sha256=ZBbLl9Sgj5mJ7lTWoaFcEPwuxPDpIbMo2n_Fhurc0T8,7263
|
88
97
|
enterprise_data/tests/test_models.py,sha256=MWBY-LY5TPBjZ4GlvpM-h4W-BvRKr2Rml8Bzg1NPZ9M,3234
|
89
|
-
enterprise_data/tests/test_utils.py,sha256=
|
98
|
+
enterprise_data/tests/test_utils.py,sha256=itT-LvZwgJbLCSpqVCYA2TpCV9uKGi4TeVoelRfx-48,17327
|
90
99
|
enterprise_data/tests/test_views.py,sha256=UvDRNTxruy5zBK_KgUy2cBMbwlaTW_vkM0-TCXbQZiY,69667
|
100
|
+
enterprise_data/tests/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
101
|
+
enterprise_data/tests/admin_analytics/test_data_loaders.py,sha256=b4BjN88FX9WjE6XJjkJZnoEvWVB_DovBGJ_wh-HgT9I,3514
|
102
|
+
enterprise_data/tests/admin_analytics/test_utils.py,sha256=4qL_ZK-sGzbMMqiOrBrPmzdIPno7KohiaIfd7FMehic,5260
|
91
103
|
enterprise_data/tests/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
92
104
|
enterprise_data/tests/api/v0/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
93
105
|
enterprise_data/tests/api/v0/test_serializers.py,sha256=Gfty6gy6OQLN318uL1OCPhAZOqSUL50FWc0nC23VMnc,6257
|
94
106
|
enterprise_data/tests/api/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
95
107
|
enterprise_data/tests/api/v1/test_serializers.py,sha256=DwgEHcyOP3oqNUPB2O-NkJGeO_cYs9XJiq7791vJLZE,3682
|
96
108
|
enterprise_data/tests/api/v1/test_views.py,sha256=rLqUHfar0HdBNtz33hQxd_0qUUgr7Ku3KwQSQ1B4Ypg,15213
|
109
|
+
enterprise_data/tests/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
110
|
+
enterprise_data/tests/api/v1/views/test_enterprise_admin.py,sha256=A7QY4RR6BwzxSsC9equXau8_PER25-KbufCNZyGdYBI,3025
|
97
111
|
enterprise_data_roles/__init__.py,sha256=toCpbypm2uDoWVw29_em9gPFNly8vNUS__C0b4TCqEg,112
|
98
112
|
enterprise_data_roles/admin.py,sha256=QNP0VeWE092vZzpyxOA5UJK1nNGl5e71B1J0RCwo_nU,998
|
99
113
|
enterprise_data_roles/apps.py,sha256=nKi8TyuQ5Q6WGtKs5QeXvUTc3N-YQjKhyBnm2EM3Bng,260
|
@@ -113,11 +127,11 @@ enterprise_data_roles/tests/factories.py,sha256=VOm0NyOhiKkxl1MRIriRkKSpKypr9-4l
|
|
113
127
|
enterprise_data_roles/tests/test_models.py,sha256=wJv7ywk0BSbjlW_U142h0aFxZleAHyT92nIiPy_ECW4,1476
|
114
128
|
enterprise_reporting/__init__.py,sha256=yQO9ureIxFnl-1a_34H53elDwuAzXrSmhLlzqqD2SJ0,112
|
115
129
|
enterprise_reporting/delivery_method.py,sha256=bG-JCGhrK3nuC3P6D88zBRSwDJCbaDxN35nNlXzvoRM,4813
|
116
|
-
enterprise_reporting/external_resource_link_report.py,sha256
|
130
|
+
enterprise_reporting/external_resource_link_report.py,sha256=jQ6RS0yec0IhAz4wErQ3q8Yn206R7aQbgcR2c803BLA,8066
|
117
131
|
enterprise_reporting/reporter.py,sha256=3wI46qH-CNCUC5r9-Eme1mQdMjwEsFk9myRb-ajzJkM,13807
|
118
132
|
enterprise_reporting/send_enterprise_reports.py,sha256=usseqP7tG0oyG7goaBA-kz2nQnU6wGLOIv0jlfq_4Lg,4753
|
119
|
-
enterprise_reporting/utils.py,sha256=
|
120
|
-
enterprise_reporting/clients/__init__.py,sha256=
|
133
|
+
enterprise_reporting/utils.py,sha256=5T2G04Re8tMQ8fAjyy_TdDlq9ZZjm7Yrq_dRQjgTAn4,13860
|
134
|
+
enterprise_reporting/clients/__init__.py,sha256=9xW-Nj1A3JWb9rOWVFdFaDzcyremAS-whVB8DRW_wCY,5121
|
121
135
|
enterprise_reporting/clients/enterprise.py,sha256=-ZKoQTyDLYPLDfC7hWKhJZxVOorWt0kYmFAiOUJkyNM,9853
|
122
136
|
enterprise_reporting/clients/s3.py,sha256=CZ9FgwOGKo-lQmZ4cw8oIqoVmhEtwBb6jfhFqoNHgh4,559
|
123
137
|
enterprise_reporting/clients/snowflake.py,sha256=h6lezl8bpWDwV35TktyUn3YFbBOhPQZOoxKEt3JQhKg,1792
|
@@ -125,17 +139,17 @@ enterprise_reporting/clients/vertica.py,sha256=bSxL4NqFPrpWGTQdhzBM3jkxwVQoKOgTd
|
|
125
139
|
enterprise_reporting/fixtures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
126
140
|
enterprise_reporting/fixtures/enterprise_customer_reporting.json,sha256=nS6E9KHW0Iqk7ZHtTyyVyrztIXxjn9OtBvMJkn7owxc,3959
|
127
141
|
enterprise_reporting/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
128
|
-
enterprise_reporting/tests/test_clients.py,sha256=
|
142
|
+
enterprise_reporting/tests/test_clients.py,sha256=h-h7xBJ6wIBKP-QqRxcJJGiQxLfTOLYByCuWfcCeCy0,8474
|
129
143
|
enterprise_reporting/tests/test_delivery_method.py,sha256=Zy169SrKz5zWjysI_RhGujuPZWivDR3arm3kxAUBPF8,2598
|
130
|
-
enterprise_reporting/tests/test_enterprise_client.py,sha256=
|
131
|
-
enterprise_reporting/tests/test_external_link_report.py,sha256=
|
144
|
+
enterprise_reporting/tests/test_enterprise_client.py,sha256=lpWm0muvA3alRjmlRAezE5901C9DU3WiySH4D5-U3qE,1058
|
145
|
+
enterprise_reporting/tests/test_external_link_report.py,sha256=zdnVOD1qtAp9c5EbIPnD9jcoLtW4iKs7gSVklgBK328,7029
|
132
146
|
enterprise_reporting/tests/test_reporter.py,sha256=PTmkGvPjGEjxiyizL88LAKnaWdvZDgOBjL4QStfOdyw,4057
|
133
147
|
enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=WtL-RqGgu2x5PPqmD8ot8Uiqhlu9w8frat7CbW9RnFk,1034
|
134
|
-
enterprise_reporting/tests/test_utils.py,sha256=
|
148
|
+
enterprise_reporting/tests/test_utils.py,sha256=Zt_TA0LVb-B6fQGkUkAKKVlUKKnQh8jnw1US1jKe7g8,9493
|
135
149
|
enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
|
136
150
|
enterprise_reporting/tests/utils.py,sha256=xms2LM7DV3wczXEfctOK1ddel1EE0J_YSr17UzbCDy4,1401
|
137
|
-
edx_enterprise_data-8.
|
138
|
-
edx_enterprise_data-8.
|
139
|
-
edx_enterprise_data-8.
|
140
|
-
edx_enterprise_data-8.
|
141
|
-
edx_enterprise_data-8.
|
151
|
+
edx_enterprise_data-8.2.0.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
|
152
|
+
edx_enterprise_data-8.2.0.dist-info/METADATA,sha256=81erBLhfqTyzo75DfCtXfMt-qYPFnfrBSITELJCJCDw,1584
|
153
|
+
edx_enterprise_data-8.2.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
|
154
|
+
edx_enterprise_data-8.2.0.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
|
155
|
+
edx_enterprise_data-8.2.0.dist-info/RECORD,,
|
enterprise_data/__init__.py
CHANGED
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"""
|
2
|
+
Constants for admin analytics.
|
3
|
+
"""
|
4
|
+
import mysql.connector
|
5
|
+
|
6
|
+
from django.conf import settings
|
7
|
+
|
8
|
+
DATABASE_CONNECTION_CONFIG = {
|
9
|
+
'host': settings.DATABASES[settings.ENTERPRISE_REPORTING_DB_ALIAS]['HOST'],
|
10
|
+
'port': settings.DATABASES[settings.ENTERPRISE_REPORTING_DB_ALIAS]['PORT'],
|
11
|
+
'database': settings.DATABASES[settings.ENTERPRISE_REPORTING_DB_ALIAS]['NAME'],
|
12
|
+
'user': settings.DATABASES[settings.ENTERPRISE_REPORTING_DB_ALIAS]['USER'],
|
13
|
+
'password': settings.DATABASES[settings.ENTERPRISE_REPORTING_DB_ALIAS]['PASSWORD'],
|
14
|
+
}
|
15
|
+
DATABASE_CONNECTOR = mysql.connector.connect
|
@@ -0,0 +1,137 @@
|
|
1
|
+
"""
|
2
|
+
Utility functions for fetching data from the database.
|
3
|
+
"""
|
4
|
+
import numpy
|
5
|
+
import pandas
|
6
|
+
|
7
|
+
from django.http import Http404
|
8
|
+
|
9
|
+
from enterprise_data.admin_analytics.database import run_query
|
10
|
+
|
11
|
+
|
12
|
+
def get_select_query(table: str, columns: list, enterprise_uuid: str) -> str:
|
13
|
+
"""
|
14
|
+
Generate a SELECT query for the given table and columns.
|
15
|
+
|
16
|
+
Arguments:
|
17
|
+
table (str): The table to query.
|
18
|
+
columns (list): The columns to select.
|
19
|
+
enterprise_uuid (str): The UUID of the enterprise customer.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
(str): The SELECT query.
|
23
|
+
"""
|
24
|
+
return f'SELECT {", ".join(columns)} FROM {table} WHERE enterprise_customer_uuid = "{enterprise_uuid}"'
|
25
|
+
|
26
|
+
|
27
|
+
def fetch_enrollment_data(enterprise_uuid: str):
|
28
|
+
"""
|
29
|
+
Fetch enrollment data from the database for the given enterprise customer.
|
30
|
+
|
31
|
+
Arguments:
|
32
|
+
enterprise_uuid (str): The UUID of the enterprise customer.
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
(pandas.DataFrame): The enrollment data.
|
36
|
+
"""
|
37
|
+
enterprise_uuid = enterprise_uuid.replace('-', '')
|
38
|
+
|
39
|
+
columns = [
|
40
|
+
'enterprise_customer_name',
|
41
|
+
'enterprise_customer_uuid',
|
42
|
+
'lms_enrollment_id',
|
43
|
+
'user_id',
|
44
|
+
'email',
|
45
|
+
'course_key',
|
46
|
+
'courserun_key',
|
47
|
+
'course_id',
|
48
|
+
'course_subject',
|
49
|
+
'course_title',
|
50
|
+
'enterprise_enrollment_date',
|
51
|
+
'lms_enrollment_mode',
|
52
|
+
'enroll_type',
|
53
|
+
'program_title',
|
54
|
+
'date_certificate_awarded',
|
55
|
+
'grade_percent',
|
56
|
+
'cert_awarded',
|
57
|
+
'date_certificate_created_raw',
|
58
|
+
'passed_date_raw',
|
59
|
+
'passed_date',
|
60
|
+
'has_passed',
|
61
|
+
]
|
62
|
+
query = get_select_query(
|
63
|
+
table='fact_enrollment_admin_dash',
|
64
|
+
columns=columns,
|
65
|
+
enterprise_uuid=enterprise_uuid,
|
66
|
+
)
|
67
|
+
|
68
|
+
results = run_query(query=query)
|
69
|
+
if not results:
|
70
|
+
raise Http404(f'No enrollment data found for enterprise {enterprise_uuid}')
|
71
|
+
|
72
|
+
enrollments = pandas.DataFrame(numpy.array(results), columns=columns)
|
73
|
+
|
74
|
+
# Convert date columns to datetime.
|
75
|
+
enrollments['enterprise_enrollment_date'] = enrollments['enterprise_enrollment_date'].astype('datetime64[ns]')
|
76
|
+
enrollments['date_certificate_awarded'] = enrollments['date_certificate_awarded'].astype('datetime64[ns]')
|
77
|
+
enrollments['date_certificate_created_raw'] = enrollments['date_certificate_created_raw'].astype('datetime64[ns]')
|
78
|
+
enrollments['passed_date_raw'] = enrollments['passed_date_raw'].astype('datetime64[ns]')
|
79
|
+
enrollments['passed_date'] = enrollments['passed_date'].astype('datetime64[ns]')
|
80
|
+
|
81
|
+
return enrollments
|
82
|
+
|
83
|
+
|
84
|
+
def fetch_engagement_data(enterprise_uuid: str):
|
85
|
+
"""
|
86
|
+
Fetch engagement data from the database for the given enterprise customer.
|
87
|
+
|
88
|
+
Arguments:
|
89
|
+
enterprise_uuid (str): The UUID of the enterprise customer.
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
(pandas.DataFrame): The engagement data.
|
93
|
+
"""
|
94
|
+
enterprise_uuid = enterprise_uuid.replace('-', '')
|
95
|
+
|
96
|
+
columns = [
|
97
|
+
'user_id',
|
98
|
+
'email',
|
99
|
+
'enterprise_customer_uuid',
|
100
|
+
'course_key',
|
101
|
+
'enroll_type',
|
102
|
+
'activity_date',
|
103
|
+
'course_title',
|
104
|
+
'course_subject',
|
105
|
+
'is_engaged',
|
106
|
+
'is_engaged_video',
|
107
|
+
'is_engaged_forum',
|
108
|
+
'is_engaged_problem',
|
109
|
+
'is_active',
|
110
|
+
'learning_time_seconds',
|
111
|
+
]
|
112
|
+
query = get_select_query(
|
113
|
+
table='fact_enrollment_engagement_day_admin_dash', columns=columns, enterprise_uuid=enterprise_uuid
|
114
|
+
)
|
115
|
+
|
116
|
+
results = run_query(query=query)
|
117
|
+
if not results:
|
118
|
+
raise Http404(f'No engagement data found for enterprise {enterprise_uuid}')
|
119
|
+
|
120
|
+
engagement = pandas.DataFrame(numpy.array(results), columns=columns)
|
121
|
+
engagement['activity_date'] = engagement['activity_date'].astype('datetime64[ns]')
|
122
|
+
|
123
|
+
return engagement
|
124
|
+
|
125
|
+
|
126
|
+
def fetch_max_enrollment_datetime():
|
127
|
+
"""
|
128
|
+
Fetch the latest created date from the enterprise_learner_enrollment table.
|
129
|
+
|
130
|
+
created will be same for all records as this is added at the time of data load. Which is when the async process
|
131
|
+
populates the data in the table. We can use this to get the latest data load time.
|
132
|
+
"""
|
133
|
+
query = "SELECT MAX(created) FROM enterprise_learner_enrollment"
|
134
|
+
results = run_query(query)
|
135
|
+
if not results:
|
136
|
+
return None
|
137
|
+
return pandas.to_datetime(results[0][0])
|
@@ -0,0 +1,31 @@
|
|
1
|
+
"""
|
2
|
+
Utility functions for interacting with the database.
|
3
|
+
"""
|
4
|
+
from contextlib import closing
|
5
|
+
from logging import getLogger
|
6
|
+
|
7
|
+
from enterprise_data.admin_analytics.constants import DATABASE_CONNECTION_CONFIG, DATABASE_CONNECTOR
|
8
|
+
from enterprise_data.utils import timeit
|
9
|
+
|
10
|
+
LOGGER = getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
@timeit
|
14
|
+
def run_query(query):
|
15
|
+
"""
|
16
|
+
Run a query on the database and return the results.
|
17
|
+
|
18
|
+
Arguments:
|
19
|
+
query (str): The query to run.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
(list): The results of the query.
|
23
|
+
"""
|
24
|
+
try:
|
25
|
+
with closing(DATABASE_CONNECTOR(**DATABASE_CONNECTION_CONFIG)) as connection:
|
26
|
+
with closing(connection.cursor()) as cursor:
|
27
|
+
cursor.execute(query)
|
28
|
+
return cursor.fetchall()
|
29
|
+
except Exception:
|
30
|
+
LOGGER.exception(f'[run_query]: run_query failed for query "{query}".')
|
31
|
+
raise
|
@@ -0,0 +1,81 @@
|
|
1
|
+
"""
|
2
|
+
Utility functions for fetching data from the database.
|
3
|
+
"""
|
4
|
+
from datetime import datetime
|
5
|
+
|
6
|
+
from edx_django_utils.cache import TieredCache, get_cache_key
|
7
|
+
|
8
|
+
from enterprise_data.admin_analytics.data_loaders import fetch_engagement_data, fetch_enrollment_data
|
9
|
+
|
10
|
+
|
11
|
+
def get_cache_timeout(cache_expiry):
|
12
|
+
"""
|
13
|
+
Helper method to calculate cache timeout in seconds.
|
14
|
+
|
15
|
+
Arguments:
|
16
|
+
cache_expiry (datetime): Datetime object denoting the cache expiry.
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
(int): Cache timeout in seconds.
|
20
|
+
"""
|
21
|
+
now = datetime.now()
|
22
|
+
cache_timeout = 0
|
23
|
+
if cache_expiry > now:
|
24
|
+
# Calculate cache expiry in seconds from now.
|
25
|
+
cache_timeout = (cache_expiry - now).seconds
|
26
|
+
|
27
|
+
return cache_timeout
|
28
|
+
|
29
|
+
|
30
|
+
def fetch_and_cache_enrollments_data(enterprise_id, cache_expiry):
|
31
|
+
"""
|
32
|
+
Helper method to fetch and cache enrollments data.
|
33
|
+
|
34
|
+
Arguments:
|
35
|
+
enterprise_id (str): UUID of the enterprise customer in string format.
|
36
|
+
cache_expiry (datetime): Datetime object denoting the cache expiry.
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
(pandas.DataFrame): The enrollments data.
|
40
|
+
"""
|
41
|
+
cache_key = get_cache_key(
|
42
|
+
resource='enterprise-admin-analytics-aggregates-enrollments',
|
43
|
+
enterprise_customer=enterprise_id,
|
44
|
+
)
|
45
|
+
cached_response = TieredCache.get_cached_response(cache_key)
|
46
|
+
|
47
|
+
if cached_response.is_found:
|
48
|
+
return cached_response.value
|
49
|
+
else:
|
50
|
+
enrollments = fetch_enrollment_data(enterprise_id)
|
51
|
+
TieredCache.set_all_tiers(
|
52
|
+
cache_key, enrollments, get_cache_timeout(cache_expiry)
|
53
|
+
)
|
54
|
+
return enrollments
|
55
|
+
|
56
|
+
|
57
|
+
def fetch_and_cache_engagements_data(enterprise_id, cache_expiry):
|
58
|
+
"""
|
59
|
+
Helper method to fetch and cache engagements data.
|
60
|
+
|
61
|
+
Arguments:
|
62
|
+
enterprise_id (str): UUID of the enterprise customer in string format.
|
63
|
+
cache_expiry (datetime): Datetime object denoting the cache expiry.
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
(pandas.DataFrame): The engagements data.
|
67
|
+
"""
|
68
|
+
cache_key = get_cache_key(
|
69
|
+
resource='enterprise-admin-analytics-aggregates-engagements',
|
70
|
+
enterprise_customer=enterprise_id,
|
71
|
+
)
|
72
|
+
cached_response = TieredCache.get_cached_response(cache_key)
|
73
|
+
|
74
|
+
if cached_response.is_found:
|
75
|
+
return cached_response.value
|
76
|
+
else:
|
77
|
+
engagements = fetch_engagement_data(enterprise_id)
|
78
|
+
TieredCache.set_all_tiers(
|
79
|
+
cache_key, engagements, get_cache_timeout(cache_expiry)
|
80
|
+
)
|
81
|
+
return engagements
|
@@ -196,3 +196,23 @@ class EnterpriseAdminSummarizeInsightsSerializer(serializers.ModelSerializer):
|
|
196
196
|
class Meta:
|
197
197
|
model = EnterpriseAdminSummarizeInsights
|
198
198
|
fields = '__all__'
|
199
|
+
|
200
|
+
|
201
|
+
class AdminAnalyticsAggregatesQueryParamsSerializer(serializers.Serializer): # pylint: disable=abstract-method
|
202
|
+
"""
|
203
|
+
Serializer for validating admin analytics query params.
|
204
|
+
"""
|
205
|
+
start_date = serializers.DateField(required=False)
|
206
|
+
end_date = serializers.DateField(required=False)
|
207
|
+
|
208
|
+
def validate(self, attrs):
|
209
|
+
"""
|
210
|
+
Validate the query params.
|
211
|
+
|
212
|
+
Raises:
|
213
|
+
serializers.ValidationError: If start_date is greater than end_date.
|
214
|
+
"""
|
215
|
+
if 'start_date' in attrs and 'end_date' in attrs:
|
216
|
+
if attrs['start_date'] > attrs['end_date']:
|
217
|
+
raise serializers.ValidationError("start_date should be less than or equal to end_date.")
|
218
|
+
return attrs
|
enterprise_data/api/v1/urls.py
CHANGED
@@ -7,7 +7,9 @@ from rest_framework.routers import DefaultRouter
|
|
7
7
|
|
8
8
|
from django.urls import re_path
|
9
9
|
|
10
|
-
from enterprise_data.api.v1 import
|
10
|
+
from enterprise_data.api.v1.views import enterprise_admin as enterprise_admin_views
|
11
|
+
from enterprise_data.api.v1.views import enterprise_learner as enterprise_learner_views
|
12
|
+
from enterprise_data.api.v1.views import enterprise_offers as enterprise_offers_views
|
11
13
|
from enterprise_data.constants import UUID4_REGEX
|
12
14
|
|
13
15
|
app_name = 'enterprise_data_api_v1'
|
@@ -15,31 +17,36 @@ app_name = 'enterprise_data_api_v1'
|
|
15
17
|
router = DefaultRouter()
|
16
18
|
router.register(
|
17
19
|
r'enterprise/(?P<enterprise_id>.+)/enrollments',
|
18
|
-
|
20
|
+
enterprise_learner_views.EnterpriseLearnerEnrollmentViewSet,
|
19
21
|
'enterprise-learner-enrollment',
|
20
22
|
)
|
21
23
|
router.register(
|
22
24
|
r'enterprise/(?P<enterprise_id>.+)/offers',
|
23
|
-
|
25
|
+
enterprise_offers_views.EnterpriseOfferViewSet,
|
24
26
|
'enterprise-offers',
|
25
27
|
)
|
26
28
|
router.register(
|
27
29
|
r'enterprise/(?P<enterprise_id>.+)/users',
|
28
|
-
|
30
|
+
enterprise_learner_views.EnterpriseLearnerViewSet,
|
29
31
|
'enterprise-learner',
|
30
32
|
)
|
31
33
|
router.register(
|
32
34
|
r'enterprise/(?P<enterprise_id>.+)/learner_completed_courses',
|
33
|
-
|
35
|
+
enterprise_learner_views.EnterpriseLearnerCompletedCoursesViewSet,
|
34
36
|
'enterprise-learner-completed-courses',
|
35
37
|
)
|
36
38
|
|
37
39
|
urlpatterns = [
|
38
40
|
re_path(
|
39
41
|
fr'^admin/insights/(?P<enterprise_id>{UUID4_REGEX})$',
|
40
|
-
|
42
|
+
enterprise_admin_views.EnterpriseAdminInsightsView.as_view(),
|
41
43
|
name='enterprise-admin-insights'
|
42
44
|
),
|
45
|
+
re_path(
|
46
|
+
fr'^admin/anlaytics/(?P<enterprise_id>{UUID4_REGEX})$',
|
47
|
+
enterprise_admin_views.EnterpriseAdminAnalyticsAggregatesView.as_view(),
|
48
|
+
name='enterprise-admin-analytics-aggregates'
|
49
|
+
),
|
43
50
|
]
|
44
51
|
|
45
52
|
urlpatterns += router.urls
|
File without changes
|
@@ -0,0 +1,26 @@
|
|
1
|
+
"""
|
2
|
+
Base views for enterprise data api v1.
|
3
|
+
"""
|
4
|
+
from edx_rbac.mixins import PermissionRequiredMixin
|
5
|
+
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
6
|
+
from edx_rest_framework_extensions.paginators import DefaultPagination
|
7
|
+
|
8
|
+
from enterprise_data.constants import ANALYTICS_API_VERSION_1
|
9
|
+
|
10
|
+
|
11
|
+
class EnterpriseViewSetMixin(PermissionRequiredMixin):
|
12
|
+
"""
|
13
|
+
Base class for all Enterprise view sets.
|
14
|
+
"""
|
15
|
+
authentication_classes = (JwtAuthentication,)
|
16
|
+
pagination_class = DefaultPagination
|
17
|
+
permission_required = 'can_access_enterprise'
|
18
|
+
API_VERSION = ANALYTICS_API_VERSION_1
|
19
|
+
|
20
|
+
def paginate_queryset(self, queryset):
|
21
|
+
"""
|
22
|
+
Allows no_page query param to skip pagination
|
23
|
+
"""
|
24
|
+
if 'no_page' in self.request.query_params:
|
25
|
+
return None
|
26
|
+
return super().paginate_queryset(queryset)
|
@@ -0,0 +1,109 @@
|
|
1
|
+
"""
|
2
|
+
Views for enterprise admin api v1.
|
3
|
+
"""
|
4
|
+
from datetime import datetime, timedelta
|
5
|
+
|
6
|
+
from edx_rbac.decorators import permission_required
|
7
|
+
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
8
|
+
from rest_framework.response import Response
|
9
|
+
from rest_framework.status import HTTP_200_OK, HTTP_404_NOT_FOUND
|
10
|
+
from rest_framework.views import APIView
|
11
|
+
|
12
|
+
from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_datetime
|
13
|
+
from enterprise_data.admin_analytics.utils import fetch_and_cache_engagements_data, fetch_and_cache_enrollments_data
|
14
|
+
from enterprise_data.api.v1 import serializers
|
15
|
+
from enterprise_data.models import EnterpriseAdminLearnerProgress, EnterpriseAdminSummarizeInsights
|
16
|
+
from enterprise_data.utils import date_filter
|
17
|
+
|
18
|
+
|
19
|
+
class EnterpriseAdminInsightsView(APIView):
|
20
|
+
"""
|
21
|
+
API for getting the enterprise admin insights.
|
22
|
+
"""
|
23
|
+
authentication_classes = (JwtAuthentication,)
|
24
|
+
http_method_names = ['get']
|
25
|
+
|
26
|
+
@permission_required('can_access_enterprise', fn=lambda request, enterprise_id: enterprise_id)
|
27
|
+
def get(self, request, enterprise_id):
|
28
|
+
"""
|
29
|
+
HTTP GET endpoint to retrieve the enterprise admin insights
|
30
|
+
"""
|
31
|
+
response_data = {}
|
32
|
+
learner_progress = {}
|
33
|
+
learner_engagement = {}
|
34
|
+
|
35
|
+
try:
|
36
|
+
learner_progress = EnterpriseAdminLearnerProgress.objects.get(enterprise_customer_uuid=enterprise_id)
|
37
|
+
learner_progress = serializers.EnterpriseAdminLearnerProgressSerializer(learner_progress).data
|
38
|
+
response_data['learner_progress'] = learner_progress
|
39
|
+
except EnterpriseAdminLearnerProgress.DoesNotExist:
|
40
|
+
pass
|
41
|
+
|
42
|
+
try:
|
43
|
+
learner_engagement = EnterpriseAdminSummarizeInsights.objects.get(enterprise_customer_uuid=enterprise_id)
|
44
|
+
learner_engagement = serializers.EnterpriseAdminSummarizeInsightsSerializer(learner_engagement).data
|
45
|
+
response_data['learner_engagement'] = learner_engagement
|
46
|
+
except EnterpriseAdminSummarizeInsights.DoesNotExist:
|
47
|
+
pass
|
48
|
+
|
49
|
+
status = HTTP_200_OK
|
50
|
+
if learner_progress == {} and learner_engagement == {}:
|
51
|
+
status = HTTP_404_NOT_FOUND
|
52
|
+
|
53
|
+
return Response(data=response_data, status=status)
|
54
|
+
|
55
|
+
|
56
|
+
class EnterpriseAdminAnalyticsAggregatesView(APIView):
|
57
|
+
"""
|
58
|
+
API for getting the enterprise admin analytics aggregates.
|
59
|
+
"""
|
60
|
+
authentication_classes = (JwtAuthentication,)
|
61
|
+
http_method_names = ['get']
|
62
|
+
|
63
|
+
@permission_required('can_access_enterprise', fn=lambda request, enterprise_id: enterprise_id)
|
64
|
+
def get(self, request, enterprise_id):
|
65
|
+
"""
|
66
|
+
HTTP GET endpoint to retrieve the enterprise admin aggregate data.
|
67
|
+
"""
|
68
|
+
serializer = serializers.AdminAnalyticsAggregatesQueryParamsSerializer(data=request.GET)
|
69
|
+
serializer.is_valid(raise_exception=True)
|
70
|
+
|
71
|
+
last_updated_at = fetch_max_enrollment_datetime()
|
72
|
+
cache_expiry = last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
|
73
|
+
|
74
|
+
enrollment = fetch_and_cache_enrollments_data(enterprise_id, cache_expiry).copy()
|
75
|
+
engagement = fetch_and_cache_engagements_data(enterprise_id, cache_expiry).copy()
|
76
|
+
# Use start and end date if provided by the client, if client has not provided then use
|
77
|
+
# 1. minimum enrollment date from the data as the start_date
|
78
|
+
# 2. today's date as the end_date
|
79
|
+
start_date = serializer.data.get('start_date', enrollment.enterprise_enrollment_date.min())
|
80
|
+
end_date = serializer.data.get('end_date', datetime.now())
|
81
|
+
|
82
|
+
# Date filtering.
|
83
|
+
dff = date_filter(
|
84
|
+
start=start_date, end=end_date, data_frame=enrollment.copy(), date_column='enterprise_enrollment_date'
|
85
|
+
)
|
86
|
+
|
87
|
+
enrolls = len(dff)
|
88
|
+
courses = len(dff.course_key.unique())
|
89
|
+
|
90
|
+
dff = date_filter(start=start_date, end=end_date, data_frame=enrollment.copy(), date_column='passed_date')
|
91
|
+
|
92
|
+
completions = dff.has_passed.sum()
|
93
|
+
|
94
|
+
# Date filtering.
|
95
|
+
dff = date_filter(start=start_date, end=end_date, data_frame=engagement.copy(), date_column='activity_date')
|
96
|
+
|
97
|
+
hours = round(dff.learning_time_seconds.sum() / 60 / 60, 1)
|
98
|
+
sessions = dff.is_engaged.sum()
|
99
|
+
|
100
|
+
return Response(data={
|
101
|
+
'enrolls': enrolls,
|
102
|
+
'courses': courses,
|
103
|
+
'completions': completions,
|
104
|
+
'hours': hours,
|
105
|
+
'sessions': sessions,
|
106
|
+
'last_updated_at': last_updated_at.date() if last_updated_at else None,
|
107
|
+
'min_enrollment_date': enrollment.enterprise_enrollment_date.min().date(),
|
108
|
+
'max_enrollment_date': enrollment.enterprise_enrollment_date.max().date(),
|
109
|
+
}, status=HTTP_200_OK)
|