edx-enterprise-data 8.0.0__py3-none-any.whl → 8.3.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 (32) hide show
  1. {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.3.0.dist-info}/METADATA +4 -1
  2. {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.3.0.dist-info}/RECORD +32 -19
  3. {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.3.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/data_loaders.py +137 -0
  7. enterprise_data/admin_analytics/database.py +50 -0
  8. enterprise_data/admin_analytics/utils.py +81 -0
  9. enterprise_data/api/v1/serializers.py +20 -0
  10. enterprise_data/api/v1/urls.py +13 -6
  11. enterprise_data/api/v1/views/__init__.py +0 -0
  12. enterprise_data/api/v1/views/base.py +26 -0
  13. enterprise_data/api/v1/views/enterprise_admin.py +109 -0
  14. enterprise_data/api/v1/{views.py → views/enterprise_learner.py} +5 -114
  15. enterprise_data/api/v1/views/enterprise_offers.py +41 -0
  16. enterprise_data/tests/admin_analytics/__init__.py +0 -0
  17. enterprise_data/tests/admin_analytics/test_data_loaders.py +86 -0
  18. enterprise_data/tests/admin_analytics/test_utils.py +102 -0
  19. enterprise_data/tests/api/v1/views/__init__.py +0 -0
  20. enterprise_data/tests/api/v1/views/test_enterprise_admin.py +82 -0
  21. enterprise_data/tests/test_filters.py +1 -1
  22. enterprise_data/tests/test_utils.py +73 -0
  23. enterprise_data/utils.py +48 -1
  24. enterprise_reporting/clients/__init__.py +2 -3
  25. enterprise_reporting/external_resource_link_report.py +3 -3
  26. enterprise_reporting/tests/test_clients.py +1 -1
  27. enterprise_reporting/tests/test_enterprise_client.py +2 -5
  28. enterprise_reporting/tests/test_external_link_report.py +2 -2
  29. enterprise_reporting/tests/test_utils.py +3 -3
  30. enterprise_reporting/utils.py +1 -1
  31. {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.3.0.dist-info}/LICENSE +0 -0
  32. {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.3.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.3.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=JHlSJ-Qb3ZQAeXmu39_pHVKHsKMJhrBFC4PUa-oeVtU,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,11 @@ 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/data_loaders.py,sha256=jP9CgUt_93Bl_ak1_6dBPN4PAEdDT1da3sEMjJ45NLg,4281
14
+ enterprise_data/admin_analytics/database.py,sha256=mNS_9xE5h6O7oMMzr6kr6LDTTSNvKzo8vaM-YG8tOd8,1312
15
+ enterprise_data/admin_analytics/utils.py,sha256=Cock_rTtHkBEKcG3cq7VcYRPEuxC_SyJBlZn9KdVeCA,2465
12
16
  enterprise_data/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
17
  enterprise_data/api/urls.py,sha256=POqc_KATHdnpMf9zHtpO46pKD5KAlAExtx7G6iylLcU,273
14
18
  enterprise_data/api/v0/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
@@ -16,9 +20,13 @@ enterprise_data/api/v0/serializers.py,sha256=dngZTk6DhRxApchQKCMp1B_c8aVnQtH0NCq
16
20
  enterprise_data/api/v0/urls.py,sha256=vzJjqIo_S3AXWs9Us8XTaJc3FnxLbYzAkmLyuDQqum0,699
17
21
  enterprise_data/api/v0/views.py,sha256=4RslZ4NZOU-844bnebEQ71ji2utRY7jEijqC45oQQD0,14380
18
22
  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
23
+ enterprise_data/api/v1/serializers.py,sha256=AOHumabnzwK59nytoOZEWhA3f9EL_CV-bXPsYSH3LI8,8263
24
+ enterprise_data/api/v1/urls.py,sha256=IQMAD9sZp0QeJa2cUd-WxwugWaW4hHZetQGulTjnrGc,1633
25
+ enterprise_data/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ enterprise_data/api/v1/views/base.py,sha256=FTAxlz5EzvAY657wzVgzhJPFSCHHzct7IDcvm71Smt8,866
27
+ enterprise_data/api/v1/views/enterprise_admin.py,sha256=mvpufYoqyaGbHuclRpFj-cQxeME1j4L6S6qQ72Rxem0,4727
28
+ enterprise_data/api/v1/views/enterprise_learner.py,sha256=yABjJje3CT8I8YOhWr1_tTkdKtnGJom8eu3EFz_-0BU,18517
29
+ enterprise_data/api/v1/views/enterprise_offers.py,sha256=VifxgqTLFLVw4extYPlHcN1N_yjXcsYsAlYEnAbpb10,1266
22
30
  enterprise_data/fixtures/enterprise_enrollment.json,sha256=6onPXXR29pMdTdbl_mn81sDi3Re5jkLUZz2TPMB_1IY,5786
23
31
  enterprise_data/fixtures/enterprise_user.json,sha256=6g8GvNY9j_fh1dvAU80bTAMI2F5vXCkb8a4UjsftMvQ,1970
24
32
  enterprise_data/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -84,16 +92,21 @@ enterprise_data/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
84
92
  enterprise_data/tests/factories.py,sha256=EqiB8xDVN9n6ZrFV0d4h6hEiRF57dpbmOjHezU7GHME,1964
85
93
  enterprise_data/tests/mixins.py,sha256=YifptI9mtOhAWnBGyPUy4kX5OJNSDP3DvW2vb1E2tvw,805
86
94
  enterprise_data/tests/test_clients.py,sha256=xBPHF9cgEFqNJoL4klOoYh_sVS3scZGcX0Ltc9Ghp7A,6336
87
- enterprise_data/tests/test_filters.py,sha256=luCgJd-gnVCWLCBWG1Cd0zN3WutiYm8IvqduP0nL5oM,7244
95
+ enterprise_data/tests/test_filters.py,sha256=ZBbLl9Sgj5mJ7lTWoaFcEPwuxPDpIbMo2n_Fhurc0T8,7263
88
96
  enterprise_data/tests/test_models.py,sha256=MWBY-LY5TPBjZ4GlvpM-h4W-BvRKr2Rml8Bzg1NPZ9M,3234
89
- enterprise_data/tests/test_utils.py,sha256=BTXrxtX5zW7r1c9gXZEFFX2rxHt-bh1HDxJ-0y05KtE,14586
97
+ enterprise_data/tests/test_utils.py,sha256=itT-LvZwgJbLCSpqVCYA2TpCV9uKGi4TeVoelRfx-48,17327
90
98
  enterprise_data/tests/test_views.py,sha256=UvDRNTxruy5zBK_KgUy2cBMbwlaTW_vkM0-TCXbQZiY,69667
99
+ enterprise_data/tests/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
100
+ enterprise_data/tests/admin_analytics/test_data_loaders.py,sha256=b4BjN88FX9WjE6XJjkJZnoEvWVB_DovBGJ_wh-HgT9I,3514
101
+ enterprise_data/tests/admin_analytics/test_utils.py,sha256=4qL_ZK-sGzbMMqiOrBrPmzdIPno7KohiaIfd7FMehic,5260
91
102
  enterprise_data/tests/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
103
  enterprise_data/tests/api/v0/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
93
104
  enterprise_data/tests/api/v0/test_serializers.py,sha256=Gfty6gy6OQLN318uL1OCPhAZOqSUL50FWc0nC23VMnc,6257
94
105
  enterprise_data/tests/api/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
106
  enterprise_data/tests/api/v1/test_serializers.py,sha256=DwgEHcyOP3oqNUPB2O-NkJGeO_cYs9XJiq7791vJLZE,3682
96
107
  enterprise_data/tests/api/v1/test_views.py,sha256=rLqUHfar0HdBNtz33hQxd_0qUUgr7Ku3KwQSQ1B4Ypg,15213
108
+ enterprise_data/tests/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
109
+ enterprise_data/tests/api/v1/views/test_enterprise_admin.py,sha256=A7QY4RR6BwzxSsC9equXau8_PER25-KbufCNZyGdYBI,3025
97
110
  enterprise_data_roles/__init__.py,sha256=toCpbypm2uDoWVw29_em9gPFNly8vNUS__C0b4TCqEg,112
98
111
  enterprise_data_roles/admin.py,sha256=QNP0VeWE092vZzpyxOA5UJK1nNGl5e71B1J0RCwo_nU,998
99
112
  enterprise_data_roles/apps.py,sha256=nKi8TyuQ5Q6WGtKs5QeXvUTc3N-YQjKhyBnm2EM3Bng,260
@@ -113,11 +126,11 @@ enterprise_data_roles/tests/factories.py,sha256=VOm0NyOhiKkxl1MRIriRkKSpKypr9-4l
113
126
  enterprise_data_roles/tests/test_models.py,sha256=wJv7ywk0BSbjlW_U142h0aFxZleAHyT92nIiPy_ECW4,1476
114
127
  enterprise_reporting/__init__.py,sha256=yQO9ureIxFnl-1a_34H53elDwuAzXrSmhLlzqqD2SJ0,112
115
128
  enterprise_reporting/delivery_method.py,sha256=bG-JCGhrK3nuC3P6D88zBRSwDJCbaDxN35nNlXzvoRM,4813
116
- enterprise_reporting/external_resource_link_report.py,sha256=-KEB8jLkx4KFTTcoB1eznKte38XoAkya_VoFGUORz6o,8066
129
+ enterprise_reporting/external_resource_link_report.py,sha256=jQ6RS0yec0IhAz4wErQ3q8Yn206R7aQbgcR2c803BLA,8066
117
130
  enterprise_reporting/reporter.py,sha256=3wI46qH-CNCUC5r9-Eme1mQdMjwEsFk9myRb-ajzJkM,13807
118
131
  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
132
+ enterprise_reporting/utils.py,sha256=5T2G04Re8tMQ8fAjyy_TdDlq9ZZjm7Yrq_dRQjgTAn4,13860
133
+ enterprise_reporting/clients/__init__.py,sha256=9xW-Nj1A3JWb9rOWVFdFaDzcyremAS-whVB8DRW_wCY,5121
121
134
  enterprise_reporting/clients/enterprise.py,sha256=-ZKoQTyDLYPLDfC7hWKhJZxVOorWt0kYmFAiOUJkyNM,9853
122
135
  enterprise_reporting/clients/s3.py,sha256=CZ9FgwOGKo-lQmZ4cw8oIqoVmhEtwBb6jfhFqoNHgh4,559
123
136
  enterprise_reporting/clients/snowflake.py,sha256=h6lezl8bpWDwV35TktyUn3YFbBOhPQZOoxKEt3JQhKg,1792
@@ -125,17 +138,17 @@ enterprise_reporting/clients/vertica.py,sha256=bSxL4NqFPrpWGTQdhzBM3jkxwVQoKOgTd
125
138
  enterprise_reporting/fixtures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
126
139
  enterprise_reporting/fixtures/enterprise_customer_reporting.json,sha256=nS6E9KHW0Iqk7ZHtTyyVyrztIXxjn9OtBvMJkn7owxc,3959
127
140
  enterprise_reporting/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
128
- enterprise_reporting/tests/test_clients.py,sha256=RTG9SFnScupe83Mm4_i6X1Kfvufbx_ofVnVcm08-a10,8474
141
+ enterprise_reporting/tests/test_clients.py,sha256=h-h7xBJ6wIBKP-QqRxcJJGiQxLfTOLYByCuWfcCeCy0,8474
129
142
  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
143
+ enterprise_reporting/tests/test_enterprise_client.py,sha256=lpWm0muvA3alRjmlRAezE5901C9DU3WiySH4D5-U3qE,1058
144
+ enterprise_reporting/tests/test_external_link_report.py,sha256=zdnVOD1qtAp9c5EbIPnD9jcoLtW4iKs7gSVklgBK328,7029
132
145
  enterprise_reporting/tests/test_reporter.py,sha256=PTmkGvPjGEjxiyizL88LAKnaWdvZDgOBjL4QStfOdyw,4057
133
146
  enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=WtL-RqGgu2x5PPqmD8ot8Uiqhlu9w8frat7CbW9RnFk,1034
134
- enterprise_reporting/tests/test_utils.py,sha256=7Y5GmeibiC8Q5kIpYuIAkFpeyIDRs2U3ItxsabhR1zM,9493
147
+ enterprise_reporting/tests/test_utils.py,sha256=Zt_TA0LVb-B6fQGkUkAKKVlUKKnQh8jnw1US1jKe7g8,9493
135
148
  enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
136
149
  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,,
150
+ edx_enterprise_data-8.3.0.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
151
+ edx_enterprise_data-8.3.0.dist-info/METADATA,sha256=II7SpGZtFiTm5AZirTzKkugBrYoyJ1PhWk25pBs98MY,1584
152
+ edx_enterprise_data-8.3.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
153
+ edx_enterprise_data-8.3.0.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
154
+ edx_enterprise_data-8.3.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.3.0"
File without changes
@@ -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,50 @@
1
+ """
2
+ Utility functions for interacting with the database.
3
+ """
4
+ from contextlib import closing
5
+ from logging import getLogger
6
+
7
+ from mysql.connector import connect
8
+
9
+ from django.conf import settings
10
+
11
+ from enterprise_data.utils import timeit
12
+
13
+ LOGGER = getLogger(__name__)
14
+
15
+
16
+ def get_db_connection(database=settings.ENTERPRISE_REPORTING_DB_ALIAS):
17
+ """
18
+ Get a connection to the database.
19
+
20
+ Returns:
21
+ (mysql.connector.connection.MySQLConnection): The database connection.
22
+ """
23
+ return connect(
24
+ host=settings.DATABASES[database]['HOST'],
25
+ port=settings.DATABASES[database]['PORT'],
26
+ database=settings.DATABASES[database]['NAME'],
27
+ user=settings.DATABASES[database]['USER'],
28
+ password=settings.DATABASES[database]['PASSWORD'],
29
+ )
30
+
31
+
32
+ @timeit
33
+ def run_query(query):
34
+ """
35
+ Run a query on the database and return the results.
36
+
37
+ Arguments:
38
+ query (str): The query to run.
39
+
40
+ Returns:
41
+ (list): The results of the query.
42
+ """
43
+ try:
44
+ with closing(get_db_connection()) as connection:
45
+ with closing(connection.cursor()) as cursor:
46
+ cursor.execute(query)
47
+ return cursor.fetchall()
48
+ except Exception:
49
+ LOGGER.exception(f'[run_query]: run_query failed for query "{query}".')
50
+ 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)