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.
Files changed (33) hide show
  1. {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.2.0.dist-info}/METADATA +4 -1
  2. {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.2.0.dist-info}/RECORD +33 -19
  3. {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.2.0.dist-info}/WHEEL +1 -1
  4. enterprise_data/__init__.py +1 -1
  5. enterprise_data/admin_analytics/__init__.py +0 -0
  6. enterprise_data/admin_analytics/constants.py +15 -0
  7. enterprise_data/admin_analytics/data_loaders.py +137 -0
  8. enterprise_data/admin_analytics/database.py +31 -0
  9. enterprise_data/admin_analytics/utils.py +81 -0
  10. enterprise_data/api/v1/serializers.py +20 -0
  11. enterprise_data/api/v1/urls.py +13 -6
  12. enterprise_data/api/v1/views/__init__.py +0 -0
  13. enterprise_data/api/v1/views/base.py +26 -0
  14. enterprise_data/api/v1/views/enterprise_admin.py +109 -0
  15. enterprise_data/api/v1/{views.py → views/enterprise_learner.py} +5 -114
  16. enterprise_data/api/v1/views/enterprise_offers.py +41 -0
  17. enterprise_data/tests/admin_analytics/__init__.py +0 -0
  18. enterprise_data/tests/admin_analytics/test_data_loaders.py +86 -0
  19. enterprise_data/tests/admin_analytics/test_utils.py +102 -0
  20. enterprise_data/tests/api/v1/views/__init__.py +0 -0
  21. enterprise_data/tests/api/v1/views/test_enterprise_admin.py +82 -0
  22. enterprise_data/tests/test_filters.py +1 -1
  23. enterprise_data/tests/test_utils.py +73 -0
  24. enterprise_data/utils.py +48 -1
  25. enterprise_reporting/clients/__init__.py +2 -3
  26. enterprise_reporting/external_resource_link_report.py +3 -3
  27. enterprise_reporting/tests/test_clients.py +1 -1
  28. enterprise_reporting/tests/test_enterprise_client.py +2 -5
  29. enterprise_reporting/tests/test_external_link_report.py +2 -2
  30. enterprise_reporting/tests/test_utils.py +3 -3
  31. enterprise_reporting/utils.py +1 -1
  32. {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.2.0.dist-info}/LICENSE +0 -0
  33. {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.0.0
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=IAJ4_2bZu0JD8iXQoT4zzRiMRCLh1m5dwDvWkFW6KVA,123
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=JdJDCzpbaQgdOrdMR8RqwveIUlB3bN5uZTn1ez9dcqs,952
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=iRPdUnkAVcoJQVWdxQDtIFQJdLftnpjGNJlavMExWmg,7535
20
- enterprise_data/api/v1/urls.py,sha256=Vev34sBjvDSqGUuQQGTu-1ft-ISYUPO_GTOpx0RTiEA,1106
21
- enterprise_data/api/v1/views.py,sha256=gEPEkWFmS8n_nN5gk_MBRO9pkc2QwhS3OxPAVKxt08Q,22434
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=luCgJd-gnVCWLCBWG1Cd0zN3WutiYm8IvqduP0nL5oM,7244
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=BTXrxtX5zW7r1c9gXZEFFX2rxHt-bh1HDxJ-0y05KtE,14586
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=-KEB8jLkx4KFTTcoB1eznKte38XoAkya_VoFGUORz6o,8066
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=7t0IUeaLVxHmunLhYU6EVKrmizY_fquwUf3xdHWlkAE,13860
120
- enterprise_reporting/clients/__init__.py,sha256=Y2XqHWyEfgHBWnfwdO1P-MWLVqCivzWIJFm_lQFUMyI,5122
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=RTG9SFnScupe83Mm4_i6X1Kfvufbx_ofVnVcm08-a10,8474
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=nFgQ9Qo7TJQMBzKM-rTvCdOdoBp_UGrzA-vq0uy7R4s,1068
131
- enterprise_reporting/tests/test_external_link_report.py,sha256=yDCWCcVfKNjLMncCU0cEW78snDmI7Vc0lHIpcmTiMBA,7029
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=7Y5GmeibiC8Q5kIpYuIAkFpeyIDRs2U3ItxsabhR1zM,9493
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.0.0.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
138
- edx_enterprise_data-8.0.0.dist-info/METADATA,sha256=o5PAAEvr5HfEqPaIUI8FjVeNEaafrnrCXSfw7gh_fKo,1486
139
- edx_enterprise_data-8.0.0.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
140
- edx_enterprise_data-8.0.0.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
141
- edx_enterprise_data-8.0.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.1.1)
2
+ Generator: setuptools (72.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__ = "8.0.0"
5
+ __version__ = "8.2.0"
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
@@ -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 views
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
- views.EnterpriseLearnerEnrollmentViewSet,
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
- views.EnterpriseOfferViewSet,
25
+ enterprise_offers_views.EnterpriseOfferViewSet,
24
26
  'enterprise-offers',
25
27
  )
26
28
  router.register(
27
29
  r'enterprise/(?P<enterprise_id>.+)/users',
28
- views.EnterpriseLearnerViewSet,
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
- views.EnterpriseLearnerCompletedCoursesViewSet,
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
- views.EnterpriseAdminInsightsView.as_view(),
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)