edx-enterprise-data 8.6.1__tar.gz → 8.7.0__tar.gz

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 (185) hide show
  1. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/CHANGELOG.rst +4 -0
  2. {edx_enterprise_data-8.6.1/edx_enterprise_data.egg-info → edx_enterprise_data-8.7.0}/PKG-INFO +1 -1
  3. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0/edx_enterprise_data.egg-info}/PKG-INFO +1 -1
  4. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/edx_enterprise_data.egg-info/SOURCES.txt +3 -0
  5. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/__init__.py +1 -1
  6. edx_enterprise_data-8.7.0/enterprise_data/admin_analytics/completions_utils.py +261 -0
  7. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/admin_analytics/utils.py +4 -1
  8. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/v1/serializers.py +5 -0
  9. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/v1/urls.py +11 -0
  10. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/v1/views/enterprise_admin.py +9 -5
  11. edx_enterprise_data-8.7.0/enterprise_data/api/v1/views/enterprise_completions.py +177 -0
  12. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/admin_analytics/mock_enrollments.py +19 -3
  13. edx_enterprise_data-8.7.0/enterprise_data/tests/admin_analytics/test_enterprise_completions.py +202 -0
  14. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/api/v1/views/test_enterprise_admin.py +4 -0
  15. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/LICENSE +0 -0
  16. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/MANIFEST.in +0 -0
  17. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/README.md +0 -0
  18. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/edx_enterprise_data.egg-info/dependency_links.txt +0 -0
  19. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/edx_enterprise_data.egg-info/not-zip-safe +0 -0
  20. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/edx_enterprise_data.egg-info/requires.txt +0 -0
  21. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/edx_enterprise_data.egg-info/top_level.txt +0 -0
  22. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/admin_analytics/__init__.py +0 -0
  23. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/admin_analytics/constants.py +0 -0
  24. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/admin_analytics/data_loaders.py +0 -0
  25. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/admin_analytics/database.py +0 -0
  26. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/__init__.py +0 -0
  27. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/urls.py +0 -0
  28. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/v0/__init__.py +0 -0
  29. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/v0/serializers.py +0 -0
  30. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/v0/urls.py +0 -0
  31. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/v0/views.py +0 -0
  32. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/v1/__init__.py +0 -0
  33. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/v1/paginators.py +0 -0
  34. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/v1/views/__init__.py +0 -0
  35. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/v1/views/analytics_enrollments.py +0 -0
  36. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/v1/views/base.py +0 -0
  37. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/v1/views/enterprise_learner.py +0 -0
  38. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/api/v1/views/enterprise_offers.py +0 -0
  39. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/apps.py +0 -0
  40. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/clients.py +0 -0
  41. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/constants.py +0 -0
  42. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/filters.py +0 -0
  43. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/fixtures/enterprise_enrollment.json +0 -0
  44. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/fixtures/enterprise_user.json +0 -0
  45. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/__init__.py +0 -0
  46. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/commands/__init__.py +0 -0
  47. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/commands/create_dummy_data.py +0 -0
  48. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/commands/create_dummy_data_lpr_v1.py +0 -0
  49. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/commands/create_enterprise_enrollment.py +0 -0
  50. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/commands/create_enterprise_learner_enrollment_lpr_v1.py +0 -0
  51. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/commands/create_enterprise_learner_lpr_v1.py +0 -0
  52. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/commands/create_enterprise_offer.py +0 -0
  53. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/commands/create_enterprise_user.py +0 -0
  54. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/commands/tests/__init__.py +0 -0
  55. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/commands/tests/test_create_dummy_data_lpr_v1.py +0 -0
  56. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/commands/tests/test_create_enterprise_enrollment.py +0 -0
  57. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/commands/tests/test_create_enterprise_learner_enrollment_lpr_v1.py +0 -0
  58. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/commands/tests/test_create_enterprise_learner_lpr_v1.py +0 -0
  59. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/management/commands/tests/test_create_enterprise_user.py +0 -0
  60. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0001_initial.py +0 -0
  61. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0002_auto_20180430_1358.py +0 -0
  62. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0003_auto_20180501_0603.py +0 -0
  63. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0004_auto_20180501_0928.py +0 -0
  64. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0004_auto_20180508_1652.py +0 -0
  65. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0005_auto_20180524_2204.py +0 -0
  66. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0006_auto_20180612_0336.py +0 -0
  67. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0007_auto_20180612_0534.py +0 -0
  68. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0008_auto_20180614_0108.py +0 -0
  69. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0009_auto_20180628_1152.py +0 -0
  70. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0010_enterpriseenrollment_created.py +0 -0
  71. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0011_enterpriseuser.py +0 -0
  72. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0012_auto_20180831_1930.py +0 -0
  73. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0013_auto_20180831_1931.py +0 -0
  74. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0014_enterpriseuser_created.py +0 -0
  75. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0015_auto_20180907_1757.py +0 -0
  76. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0016_auto_20180924_2138.py +0 -0
  77. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0017_enterpriseenrollment_unenrollment_timestamp.py +0 -0
  78. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0018_enterprisedatafeaturerole_enterprisedataroleassignment.py +0 -0
  79. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0019_add_enterprise_data_feature_roles.py +0 -0
  80. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0020_add_role_based_access_control_switch.py +0 -0
  81. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0021_auto_20190329_1241.py +0 -0
  82. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0022_remove_role_based_access_control_switch.py +0 -0
  83. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0023_enterpriselearner_enterpriselearnerenrollment.py +0 -0
  84. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0024_auto_20210602_1811.py +0 -0
  85. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0025_auto_20210703_1854.py +0 -0
  86. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0026_auto_20210916_0414.py +0 -0
  87. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0027_enterpriselearnerenrollment_total_learning_time_seconds.py +0 -0
  88. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0028_enterpriselearnerenrollment_offer_id.py +0 -0
  89. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0029_enterpriseoffer.py +0 -0
  90. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0030_auto_20230609_1353.py +0 -0
  91. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0031_auto_20230615_0705.py +0 -0
  92. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0032_auto_20230704_0818.py +0 -0
  93. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0033_enterpriseadminlearnerprogress_enterpriseadminsummarizeinsights.py +0 -0
  94. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0034_auto_20230907_0834.py +0 -0
  95. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0035_auto_20230907_1154.py +0 -0
  96. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0036_enterprisesubsidybudget_subsidy_access_policy_display_name.py +0 -0
  97. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0037_alter_enterpriseenrollment_consent_granted.py +0 -0
  98. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0038_enterpriseoffer_export_timestamp.py +0 -0
  99. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0039_auto_20240212_1403.py +0 -0
  100. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0040_auto_20240718_0536_squashed_0043_alter_enterpriselearnerenrollment_enterprise_enrollment_id.py +0 -0
  101. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/0044_enterpriseexecedlcmoduleperformance.py +0 -0
  102. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/migrations/__init__.py +0 -0
  103. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/models.py +0 -0
  104. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/paginators.py +0 -0
  105. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/renderers.py +0 -0
  106. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/settings/__init__.py +0 -0
  107. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/settings/test.py +0 -0
  108. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/signals.py +0 -0
  109. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/__init__.py +0 -0
  110. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/admin_analytics/__init__.py +0 -0
  111. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +0 -0
  112. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/admin_analytics/test_data_loaders.py +0 -0
  113. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/admin_analytics/test_utils.py +0 -0
  114. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/api/__init__.py +0 -0
  115. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/api/v0/__init__.py +0 -0
  116. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/api/v0/test_serializers.py +0 -0
  117. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/api/v1/__init__.py +0 -0
  118. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/api/v1/test_serializers.py +0 -0
  119. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/api/v1/test_views.py +0 -0
  120. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/api/v1/views/__init__.py +0 -0
  121. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/factories.py +0 -0
  122. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/mixins.py +0 -0
  123. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/test_clients.py +0 -0
  124. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/test_filters.py +0 -0
  125. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/test_models.py +0 -0
  126. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/test_utils.py +0 -0
  127. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/tests/test_views.py +0 -0
  128. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/urls.py +0 -0
  129. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data/utils.py +0 -0
  130. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/__init__.py +0 -0
  131. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/admin.py +0 -0
  132. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/apps.py +0 -0
  133. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/constants.py +0 -0
  134. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/migrations/0001_initial.py +0 -0
  135. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/migrations/0002_add_enterprise_data_feature_roles.py +0 -0
  136. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/migrations/0003_add_role_based_access_control_switch.py +0 -0
  137. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/migrations/0004_enterprisedataroleassignment_enterprise_id.py +0 -0
  138. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/migrations/0005_turn_on_role_based_access_control_switch.py +0 -0
  139. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/migrations/0006_remove_role_based_access_control_switch.py +0 -0
  140. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/migrations/0007_enterprisedataroleassignment_applies_to_all_contexts.py +0 -0
  141. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/migrations/__init__.py +0 -0
  142. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/models.py +0 -0
  143. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/rules.py +0 -0
  144. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/tests/__init__.py +0 -0
  145. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/tests/factories.py +0 -0
  146. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_data_roles/tests/test_models.py +0 -0
  147. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/__init__.py +0 -0
  148. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/clients/__init__.py +0 -0
  149. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/clients/enterprise.py +0 -0
  150. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/clients/s3.py +0 -0
  151. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/clients/snowflake.py +0 -0
  152. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/clients/vertica.py +0 -0
  153. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/delivery_method.py +0 -0
  154. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/external_resource_link_report.py +0 -0
  155. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/fixtures/__init__.py +0 -0
  156. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/fixtures/enterprise_customer_reporting.json +0 -0
  157. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/reporter.py +0 -0
  158. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/send_enterprise_reports.py +0 -0
  159. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/tests/__init__.py +0 -0
  160. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/tests/test_clients.py +0 -0
  161. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/tests/test_delivery_method.py +0 -0
  162. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/tests/test_enterprise_client.py +0 -0
  163. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/tests/test_external_link_report.py +0 -0
  164. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/tests/test_reporter.py +0 -0
  165. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/tests/test_send_enterprise_reports.py +0 -0
  166. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/tests/test_utils.py +0 -0
  167. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/tests/test_vertica_client.py +0 -0
  168. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/tests/utils.py +0 -0
  169. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/enterprise_reporting/utils.py +0 -0
  170. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/requirements/base.in +0 -0
  171. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/requirements/base.txt +0 -0
  172. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/requirements/ci.txt +0 -0
  173. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/requirements/common_constraints.txt +0 -0
  174. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/requirements/constraints.txt +0 -0
  175. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/requirements/dev.txt +0 -0
  176. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/requirements/django.txt +0 -0
  177. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/requirements/pip.txt +0 -0
  178. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/requirements/pip_tools.txt +0 -0
  179. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/requirements/quality.txt +0 -0
  180. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/requirements/reporting.in +0 -0
  181. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/requirements/test-master.txt +0 -0
  182. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/requirements/test-reporting.txt +0 -0
  183. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/requirements/test.txt +0 -0
  184. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/setup.cfg +0 -0
  185. {edx_enterprise_data-8.6.1 → edx_enterprise_data-8.7.0}/setup.py +0 -0
@@ -16,6 +16,10 @@ Unreleased
16
16
 
17
17
  =========================
18
18
 
19
+ [8.7.0] - 2024-08-13
20
+ ---------------------
21
+ * feat: add endpoints to get completion data for an enterprise customer
22
+
19
23
  [8.6.1] - 2024-08-12
20
24
  ---------------------
21
25
  * Dependency updates
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edx-enterprise-data
3
- Version: 8.6.1
3
+ Version: 8.7.0
4
4
  Summary: Enterprise Reporting
5
5
  Home-page: https://github.com/openedx/edx-enterprise-data
6
6
  Author: edX
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edx-enterprise-data
3
- Version: 8.6.1
3
+ Version: 8.7.0
4
4
  Summary: Enterprise Reporting
5
5
  Home-page: https://github.com/openedx/edx-enterprise-data
6
6
  Author: edX
@@ -21,6 +21,7 @@ enterprise_data/signals.py
21
21
  enterprise_data/urls.py
22
22
  enterprise_data/utils.py
23
23
  enterprise_data/admin_analytics/__init__.py
24
+ enterprise_data/admin_analytics/completions_utils.py
24
25
  enterprise_data/admin_analytics/constants.py
25
26
  enterprise_data/admin_analytics/data_loaders.py
26
27
  enterprise_data/admin_analytics/database.py
@@ -39,6 +40,7 @@ enterprise_data/api/v1/views/__init__.py
39
40
  enterprise_data/api/v1/views/analytics_enrollments.py
40
41
  enterprise_data/api/v1/views/base.py
41
42
  enterprise_data/api/v1/views/enterprise_admin.py
43
+ enterprise_data/api/v1/views/enterprise_completions.py
42
44
  enterprise_data/api/v1/views/enterprise_learner.py
43
45
  enterprise_data/api/v1/views/enterprise_offers.py
44
46
  enterprise_data/fixtures/enterprise_enrollment.json
@@ -115,6 +117,7 @@ enterprise_data/tests/admin_analytics/__init__.py
115
117
  enterprise_data/tests/admin_analytics/mock_enrollments.py
116
118
  enterprise_data/tests/admin_analytics/test_analytics_enrollments.py
117
119
  enterprise_data/tests/admin_analytics/test_data_loaders.py
120
+ enterprise_data/tests/admin_analytics/test_enterprise_completions.py
118
121
  enterprise_data/tests/admin_analytics/test_utils.py
119
122
  enterprise_data/tests/api/__init__.py
120
123
  enterprise_data/tests/api/v0/__init__.py
@@ -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.6.1"
5
+ __version__ = "8.7.0"
@@ -0,0 +1,261 @@
1
+ """This module contains utility functions for completions analytics."""
2
+ from enterprise_data.utils import date_filter
3
+
4
+
5
+ def date_aggregation(level, group, date, df, type_="count"):
6
+ """Perform date aggregation on a DataFrame.
7
+
8
+ This function aggregates data based on the specified level of aggregation (e.g., daily, weekly, monthly, quarterly)
9
+ and returns the aggregated data.
10
+
11
+ Args:
12
+ level (str): The level of aggregation. Possible values are "Daily", "Weekly", "Monthly", and "Quarterly".
13
+ group (list): A list of column names to group the data by.
14
+ date (str): The name of the date column in the DataFrame.
15
+ df (pandas.DataFrame): The DataFrame containing the data to be aggregated.
16
+ type_ (str, optional): The type of aggregation to perform. Possible values
17
+ are "count" and "sum". Defaults to "count".
18
+
19
+ Returns:
20
+ pandas.DataFrame: The aggregated data.
21
+
22
+ """
23
+ if type_ == "count":
24
+ if level == "Daily":
25
+ df = df.groupby(group).size().reset_index()
26
+ group.append("count")
27
+ df.columns = group
28
+ elif level == "Weekly":
29
+ df[date] = df[date].dt.to_period("W").dt.start_time
30
+ df = df.groupby(group).size().reset_index()
31
+ group.append("count")
32
+ df.columns = group
33
+ elif level == "Monthly":
34
+ df[date] = df[date].dt.to_period("M").dt.start_time
35
+ df = df.groupby(group).size().reset_index()
36
+ group.append("count")
37
+ df.columns = group
38
+ elif level == "Quarterly":
39
+ df[date] = df[date].dt.to_period("Q").dt.start_time
40
+ df = df.groupby(group).size().reset_index()
41
+ group.append("count")
42
+ df.columns = group
43
+ elif type_ == "sum":
44
+ if level == "Daily":
45
+ df = df.groupby(group).sum().reset_index()
46
+ group.append("sum")
47
+ df.columns = group
48
+ elif level == "Weekly":
49
+ df[date] = df[date].dt.to_period("W").dt.start_time
50
+ df = df.groupby(group).sum().reset_index()
51
+ group.append("sum")
52
+ df.columns = group
53
+ elif level == "Monthly":
54
+ df[date] = df[date].dt.to_period("M").dt.start_time
55
+ df = df.groupby(group).sum().reset_index()
56
+ group.append("sum")
57
+ df.columns = group
58
+ elif level == "Quarterly":
59
+ df[date] = df[date].dt.to_period("Q").dt.start_time
60
+ df = df.groupby(group).sum().reset_index()
61
+ group.append("sum")
62
+ df.columns = group
63
+
64
+ return df
65
+
66
+
67
+ def calculation(calc, df, type_="count"):
68
+ """Perform a calculation on the given DataFrame based on the specified calculation type.
69
+
70
+ Args:
71
+ calc (str): The calculation type. Possible values are "Total", "Running Total",
72
+ "Moving Average (3 Period)", and "Moving Average (7 Period)".
73
+ df (pandas.DataFrame): The filtered enrollments data.
74
+ type_ (str, optional): The type of calculation to perform. Default is "count".
75
+
76
+ Returns:
77
+ pandas.DataFrame: The aggregated data after performing the calculation.
78
+ """
79
+ if type_ == "count":
80
+ if calc == "Total":
81
+ pass
82
+ elif calc == "Running Total":
83
+ df["count"] = df.groupby("enroll_type")["count"].cumsum()
84
+ elif calc == "Moving Average (3 Period)":
85
+ df["count"] = (
86
+ df.groupby("enroll_type")["count"]
87
+ .rolling(3)
88
+ .mean()
89
+ .droplevel(level=[0])
90
+ )
91
+ elif calc == "Moving Average (7 Period)":
92
+ df["count"] = (
93
+ df.groupby("enroll_type")["count"]
94
+ .rolling(7)
95
+ .mean()
96
+ .droplevel(level=[0])
97
+ )
98
+ elif type_ == "sum":
99
+ if calc == "Total":
100
+ pass
101
+ elif calc == "Running Total":
102
+ df["sum"] = df.groupby("enroll_type")["sum"].cumsum()
103
+ elif calc == "Moving Average (3 Period)":
104
+ df["sum"] = (
105
+ df.groupby("enroll_type")["sum"].rolling(3).mean().droplevel(level=[0])
106
+ )
107
+ elif calc == "Moving Average (7 Period)":
108
+ df["sum"] = (
109
+ df.groupby("enroll_type")["sum"].rolling(7).mean().droplevel(level=[0])
110
+ )
111
+
112
+ return df
113
+
114
+
115
+ def get_completions_over_time(start_date, end_date, dff, date_agg, calc):
116
+ """Get agreggated data for completions over time graph.
117
+
118
+ Args:
119
+ start_date (datetime): The start date for the date filter.
120
+ end_date (datetime): The end date for the date filter.
121
+ dff (pandas.DataFrame): enrollments data
122
+ date_agg (str): It denotes the granularity of the aggregated date which can be Daily, Weekly, Monthly, Quarterly
123
+ calc (str): Calculations denoiated the period for the running averages. It can be Total, Running Total, Moving
124
+ Average (3 Period), Moving Average (7 Period)
125
+ """
126
+
127
+ dff = dff[dff["has_passed"] == 1]
128
+
129
+ # Date filtering.
130
+ dff = date_filter(start=start_date, end=end_date, data_frame=dff, date_column="passed_date")
131
+
132
+ # Date aggregation.
133
+ dff = date_aggregation(
134
+ level=date_agg, group=["passed_date", "enroll_type"], date="passed_date", df=dff
135
+ )
136
+
137
+ # Calculating metric.
138
+ dff = calculation(calc=calc, df=dff)
139
+
140
+ return dff
141
+
142
+
143
+ def get_top_courses_by_completions(start_date, end_date, dff):
144
+ """Get top 10 courses by completions.
145
+
146
+ Args:
147
+ start_date (datetime): The start date for the date filter.
148
+ end_date (datetime): The end date for the date filter.
149
+ dff (pandas.DataFrame): Enrollments data
150
+ """
151
+
152
+ dff = dff[dff["has_passed"] == 1]
153
+
154
+ # Date filtering.
155
+ dff = date_filter(start=start_date, end=end_date, data_frame=dff, date_column="passed_date")
156
+
157
+ courses = list(
158
+ dff.groupby(["course_key"]).size().sort_values(ascending=False)[:10].index
159
+ )
160
+
161
+ dff = (
162
+ dff[dff.course_key.isin(courses)]
163
+ .groupby(["course_key", "course_title", "enroll_type"])
164
+ .size()
165
+ .reset_index()
166
+ )
167
+ dff.columns = ["course_key", "course_title", "enroll_type", "count"]
168
+
169
+ return dff
170
+
171
+
172
+ def get_top_subjects_by_completions(start_date, end_date, dff):
173
+ """Get top 10 subjects by completions.
174
+
175
+ Args:
176
+ start_date (datetime): The start date for the date filter.
177
+ end_date (datetime): The end date for the date filter.
178
+ dff (pandas.DataFrame): Enrollments data
179
+ """
180
+
181
+ dff = dff[dff["has_passed"] == 1]
182
+
183
+ # Date filtering.
184
+ dff = date_filter(start=start_date, end=end_date, data_frame=dff, date_column="passed_date")
185
+
186
+ subjects = list(
187
+ dff.groupby(["course_subject"]).size().sort_values(ascending=False)[:10].index
188
+ )
189
+
190
+ dff = (
191
+ dff[dff.course_subject.isin(subjects)]
192
+ .groupby(["course_subject", "enroll_type"])
193
+ .size()
194
+ .reset_index()
195
+ )
196
+ dff.columns = ["course_subject", "enroll_type", "count"]
197
+
198
+ return dff
199
+
200
+
201
+ def get_csv_data_for_completions_over_time(
202
+ start_date, end_date, enrollments, date_agg, calc
203
+ ):
204
+ """Get csv data for completions over time graph.
205
+
206
+ Args:
207
+ start_date (datetime): The start date for the date filter.
208
+ end_date (datetime): The end date for the date filter.
209
+ enrollments (pandas.DataFrame): Filtered enrollments data
210
+ date_agg (str): it denotes the granularity of the aggregated date which can be Daily, Weekly, Monthly, Quarterly
211
+ calc (str): calculations denoiated the period for the running averages. It can be Total, Running Total, Moving
212
+ Average (3 Period), Moving Average (7 Period)
213
+
214
+ Returns:
215
+ dict: csv data
216
+ """
217
+
218
+ dff = get_completions_over_time(start_date, end_date, enrollments, date_agg, calc)
219
+ dff = dff.pivot(index="passed_date", columns="enroll_type", values="count")
220
+ filename = (
221
+ f"Completions Timeseries, {start_date} - {end_date} ({date_agg} {calc}).csv"
222
+ )
223
+ return {"filename": filename, "data": dff}
224
+
225
+
226
+ def get_csv_data_for_top_courses_by_completions(start_date, end_date, enrollments):
227
+ """Get csv data for top 10 courses by completions.
228
+
229
+ Args:
230
+ start_date (datetime): The start date for the date filter.
231
+ end_date (datetime): The end date for the date filter.
232
+ enrollments (pandas.DataFrame): Filtered enrollments data
233
+
234
+ Returns:
235
+ dict: csv data
236
+ """
237
+
238
+ dff = get_top_courses_by_completions(start_date, end_date, enrollments)
239
+ dff = dff.pivot(
240
+ index=["course_key", "course_title"], columns="enroll_type", values="count"
241
+ )
242
+ filename = f"Top 10 Courses by Completions, {start_date} - {end_date}.csv"
243
+ return {"filename": filename, "data": dff}
244
+
245
+
246
+ def get_csv_data_for_top_subjects_by_completions(start_date, end_date, enrollments):
247
+ """Get csv data for top 10 subjects by completions.
248
+
249
+ Args:
250
+ start_date (datetime): The start date for the date filter.
251
+ end_date (datetime): The end date for the date filter.
252
+ enrollments (pandas.DataFrame): Filtered enrollments data
253
+
254
+ Returns:
255
+ dict: csv data
256
+ """
257
+
258
+ dff = get_top_subjects_by_completions(start_date, end_date, enrollments)
259
+ dff = dff.pivot(index="course_subject", columns="enroll_type", values="count")
260
+ filename = f"Top 10 Subjects by Completions, {start_date} - {end_date}.csv"
261
+ return {"filename": filename, "data": dff}
@@ -18,6 +18,9 @@ class ChartType(Enum):
18
18
  BUBBLE = 'bubble'
19
19
  TOP_SKILLS_ENROLLMENT = 'top_skills_enrollment'
20
20
  TOP_SKILLS_COMPLETION = 'top_skills_completion'
21
+ COMPLETIONS_OVER_TIME = 'completions_over_time'
22
+ TOP_COURSES_BY_COMPLETIONS = 'top_courses_by_completions'
23
+ TOP_SUBJECTS_BY_COMPLETIONS = 'top_subjects_by_completions'
21
24
 
22
25
 
23
26
  def granularity_aggregation(level, group, date, data_frame, aggregation_type="count"):
@@ -172,7 +175,7 @@ def get_skills_bubble_chart_df(skills_filtered):
172
175
  """ Get the skills data for the bubble chart.
173
176
 
174
177
  Args:
175
- skills_filtered (list): The skills data.
178
+ skills_filtered (pandas.DataFrame): The skills data.
176
179
 
177
180
  Returns:
178
181
  (pandas.DataFrame): The skills data for the bubble chart.
@@ -206,6 +206,11 @@ class AdminAnalyticsAggregatesQueryParamsSerializer(serializers.Serializer): #
206
206
  """
207
207
  start_date = serializers.DateField(required=False)
208
208
  end_date = serializers.DateField(required=False)
209
+ granularity = serializers.CharField(required=False)
210
+ calculation = serializers.CharField(required=False)
211
+ response_type = serializers.CharField(required=False)
212
+ page = serializers.IntegerField(required=False)
213
+ chart_type = serializers.CharField(required=False)
209
214
 
210
215
  def validate(self, attrs):
211
216
  """
@@ -8,6 +8,7 @@ from rest_framework.routers import DefaultRouter
8
8
  from django.urls import re_path
9
9
 
10
10
  from enterprise_data.api.v1.views import enterprise_admin as enterprise_admin_views
11
+ from enterprise_data.api.v1.views import enterprise_completions as enterprise_completions_views
11
12
  from enterprise_data.api.v1.views import enterprise_learner as enterprise_learner_views
12
13
  from enterprise_data.api.v1.views import enterprise_offers as enterprise_offers_views
13
14
  from enterprise_data.api.v1.views.analytics_enrollments import (
@@ -71,6 +72,16 @@ urlpatterns = [
71
72
  enterprise_admin_views.EnterpriseAdminAnalyticsSkillsView.as_view(),
72
73
  name='enterprise-admin-analytics-skills'
73
74
  ),
75
+ re_path(
76
+ fr'^admin/anlaytics/(?P<enterprise_id>{UUID4_REGEX})/completions/stats$',
77
+ enterprise_completions_views.EnterrpiseAdminCompletionsStatsView.as_view(),
78
+ name='enterprise-admin-analytics-completions-stats'
79
+ ),
80
+ re_path(
81
+ fr'^admin/anlaytics/(?P<enterprise_id>{UUID4_REGEX})/completions$',
82
+ enterprise_completions_views.EnterrpiseAdminCompletionsView.as_view(),
83
+ name='enterprise-admin-analytics-completions'
84
+ ),
74
85
  ]
75
86
 
76
87
  urlpatterns += router.urls
@@ -189,17 +189,21 @@ class EnterpriseAdminAnalyticsSkillsView(APIView):
189
189
  data=request.GET
190
190
  )
191
191
  serializer.is_valid(raise_exception=True)
192
-
193
- start_date = serializer.data.get("start_date")
194
- end_date = serializer.data.get("end_date", datetime.now())
195
-
196
192
  last_updated_at = fetch_max_enrollment_datetime()
197
193
  cache_expiry = (
198
194
  last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
199
195
  )
196
+
197
+ enrollment = fetch_and_cache_enrollments_data(
198
+ enterprise_id, cache_expiry
199
+ ).copy()
200
+
201
+ start_date = serializer.data.get('start_date', enrollment.enterprise_enrollment_date.min())
202
+ end_date = serializer.data.get('end_date', datetime.now())
203
+
200
204
  skills = fetch_and_cache_skills_data(enterprise_id, cache_expiry).copy()
201
205
 
202
- if request.GET.get("format") == "csv":
206
+ if serializer.data.get('response_type') == 'csv':
203
207
  csv_data = get_top_skills_csv_data(skills, start_date, end_date)
204
208
  response = HttpResponse(content_type='text/csv')
205
209
  filename = f"Skills by Enrollment and Completion, {start_date} - {end_date}.csv"
@@ -0,0 +1,177 @@
1
+ """Views for enterprise admin completions analytics."""
2
+ import datetime
3
+ from datetime import datetime, timedelta
4
+
5
+ from edx_rbac.decorators import permission_required
6
+ from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
7
+ from rest_framework.response import Response
8
+ from rest_framework.status import HTTP_200_OK
9
+ from rest_framework.views import APIView
10
+
11
+ from django.http import HttpResponse
12
+
13
+ from enterprise_data.admin_analytics.completions_utils import (
14
+ get_completions_over_time,
15
+ get_csv_data_for_completions_over_time,
16
+ get_csv_data_for_top_courses_by_completions,
17
+ get_csv_data_for_top_subjects_by_completions,
18
+ get_top_courses_by_completions,
19
+ get_top_subjects_by_completions,
20
+ )
21
+ from enterprise_data.admin_analytics.constants import CALCULATION, GRANULARITY
22
+ from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_datetime
23
+ from enterprise_data.admin_analytics.utils import ChartType, fetch_and_cache_enrollments_data
24
+ from enterprise_data.api.v1 import serializers
25
+ from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
26
+ from enterprise_data.utils import date_filter
27
+
28
+
29
+ class EnterrpiseAdminCompletionsStatsView(APIView):
30
+ """
31
+ API for getting the enterprise admin completions.
32
+ """
33
+ authentication_classes = (JwtAuthentication,)
34
+ http_method_names = ['get']
35
+
36
+ @permission_required(
37
+ "can_access_enterprise", fn=lambda request, enterprise_id: enterprise_id
38
+ )
39
+ def get(self, request, enterprise_id):
40
+ """
41
+ HTTP GET endpoint to retrieve the enterprise admin completions
42
+ """
43
+ serializer = serializers.AdminAnalyticsAggregatesQueryParamsSerializer(
44
+ data=request.GET
45
+ )
46
+ serializer.is_valid(raise_exception=True)
47
+
48
+ last_updated_at = fetch_max_enrollment_datetime()
49
+ cache_expiry = (
50
+ last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
51
+ )
52
+
53
+ enrollments = fetch_and_cache_enrollments_data(
54
+ enterprise_id, cache_expiry
55
+ ).copy()
56
+ # Use start and end date if provided by the client, if client has not provided then use
57
+ # 1. minimum enrollment date from the data as the start_date
58
+ # 2. today's date as the end_date
59
+ start_date = serializer.data.get(
60
+ "start_date", enrollments.enterprise_enrollment_date.min()
61
+ )
62
+ end_date = serializer.data.get("end_date", datetime.now())
63
+
64
+ if serializer.data.get('response_type') == 'csv':
65
+ chart_type = serializer.data.get('chart_type')
66
+ response = HttpResponse(content_type='text/csv')
67
+ csv_data = {}
68
+
69
+ if chart_type == ChartType.COMPLETIONS_OVER_TIME.value:
70
+ csv_data = get_csv_data_for_completions_over_time(
71
+ start_date=start_date,
72
+ end_date=end_date,
73
+ enrollments=enrollments.copy(),
74
+ date_agg=serializer.data.get('granularity', GRANULARITY.DAILY.value),
75
+ calc=serializer.data.get('calculation', CALCULATION.TOTAL.value),
76
+ )
77
+ elif chart_type == ChartType.TOP_COURSES_BY_COMPLETIONS.value:
78
+ csv_data = get_csv_data_for_top_courses_by_completions(
79
+ start_date=start_date, end_date=end_date, enrollments=enrollments.copy()
80
+ )
81
+ elif chart_type == ChartType.TOP_SUBJECTS_BY_COMPLETIONS.value:
82
+ csv_data = get_csv_data_for_top_subjects_by_completions(
83
+ start_date=start_date, end_date=end_date, enrollments=enrollments.copy()
84
+ )
85
+ filename = csv_data['filename']
86
+ response['Content-Disposition'] = f'attachment; filename="{filename}"'
87
+ csv_data['data'].to_csv(path_or_buf=response)
88
+ return response
89
+
90
+ completions_over_time = get_completions_over_time(
91
+ start_date=start_date,
92
+ end_date=end_date,
93
+ dff=enrollments.copy(),
94
+ date_agg=serializer.data.get('granularity', GRANULARITY.DAILY.value),
95
+ calc=serializer.data.get('calculation', CALCULATION.TOTAL.value),
96
+ )
97
+ top_courses_by_completions = get_top_courses_by_completions(
98
+ start_date=start_date, end_date=end_date, dff=enrollments.copy()
99
+ )
100
+ top_subjects_by_completions = get_top_subjects_by_completions(
101
+ start_date=start_date, end_date=end_date, dff=enrollments.copy()
102
+ )
103
+
104
+ return Response(
105
+ data={
106
+ 'completions_over_time': completions_over_time.to_dict(
107
+ orient="records"
108
+ ),
109
+ 'top_courses_by_completions': top_courses_by_completions.to_dict(
110
+ orient="records"
111
+ ),
112
+ 'top_subjects_by_completions': top_subjects_by_completions.to_dict(
113
+ orient="records"
114
+ ),
115
+ },
116
+ status=HTTP_200_OK,
117
+ )
118
+
119
+
120
+ class EnterrpiseAdminCompletionsView(APIView):
121
+ """
122
+ API for getting the enterprise admin completions.
123
+ """
124
+ authentication_classes = (JwtAuthentication,)
125
+ http_method_names = ['get']
126
+ pagination_class = AdvanceAnalyticsPagination
127
+
128
+ @permission_required(
129
+ "can_access_enterprise", fn=lambda request, enterprise_id: enterprise_id
130
+ )
131
+ def get(self, request, enterprise_id):
132
+ """
133
+ HTTP GET endpoint to retrieve the enterprise admin completions
134
+ """
135
+ serializer = serializers.AdminAnalyticsAggregatesQueryParamsSerializer(
136
+ data=request.GET
137
+ )
138
+ serializer.is_valid(raise_exception=True)
139
+
140
+ last_updated_at = fetch_max_enrollment_datetime()
141
+ cache_expiry = (
142
+ last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
143
+ )
144
+
145
+ enrollments = fetch_and_cache_enrollments_data(
146
+ enterprise_id, cache_expiry
147
+ ).copy()
148
+ # Use start and end date if provided by the client, if client has not provided then use
149
+ # 1. minimum enrollment date from the data as the start_date
150
+ # 2. today's date as the end_date
151
+ start_date = serializer.data.get(
152
+ 'start_date', enrollments.enterprise_enrollment_date.min()
153
+ )
154
+ end_date = serializer.data.get('end_date', datetime.now())
155
+
156
+ dff = enrollments[enrollments['has_passed'] == 1]
157
+
158
+ # Date filtering
159
+ dff = date_filter(start=start_date, end=end_date, data_frame=dff, date_column='passed_date')
160
+
161
+ dff = dff[['email', 'course_title', 'course_subject', 'passed_date']]
162
+ dff['passed_date'] = dff['passed_date'].dt.date
163
+ dff = dff.sort_values(by="passed_date", ascending=False).reset_index(drop=True)
164
+
165
+ if serializer.data.get('response_type') == 'csv':
166
+ response = HttpResponse(content_type='text/csv')
167
+ filename = f"Individual Completions, {start_date} - {end_date}.csv"
168
+ response['Content-Disposition'] = f'attachment; filename="{filename}"'
169
+ dff.to_csv(path_or_buf=response, index=False)
170
+ return response
171
+
172
+ paginator = self.pagination_class()
173
+ page = paginator.paginate_queryset(dff, request)
174
+ serialized_data = page.data.to_dict(orient='records')
175
+ response = paginator.get_paginated_response(serialized_data)
176
+
177
+ return response
@@ -3,6 +3,7 @@
3
3
  import pandas as pd
4
4
 
5
5
  from enterprise_data.admin_analytics.constants import ENROLLMENT_CSV
6
+ from enterprise_data.admin_analytics.utils import ChartType
6
7
 
7
8
  ENROLLMENTS = [
8
9
  {
@@ -71,7 +72,7 @@ ENROLLMENTS = [
71
72
  "cert_awarded": 0,
72
73
  "date_certificate_created_raw": None,
73
74
  "passed_date_raw": None,
74
- "passed_date": None,
75
+ "passed_date": '2022-08-24',
75
76
  "has_passed": 0,
76
77
  },
77
78
  {
@@ -94,7 +95,7 @@ ENROLLMENTS = [
94
95
  "cert_awarded": 0,
95
96
  "date_certificate_created_raw": None,
96
97
  "passed_date_raw": None,
97
- "passed_date": None,
98
+ "passed_date": "2022-08-24",
98
99
  "has_passed": 0,
99
100
  },
100
101
  {
@@ -117,7 +118,7 @@ ENROLLMENTS = [
117
118
  "cert_awarded": 0,
118
119
  "date_certificate_created_raw": None,
119
120
  "passed_date_raw": None,
120
- "passed_date": None,
121
+ "passed_date": "2022-08-20",
121
122
  "has_passed": 0,
122
123
  },
123
124
  ]
@@ -142,6 +143,21 @@ ENROLLMENT_STATS_CSVS = {
142
143
  b'course_subject,certificate\nbusiness-management,2\ncommunication,1\nmedicine,1\nsocial-sciences,1\n'
143
144
  )
144
145
  }
146
+ COMPLETIONS_STATS_CSVS = {
147
+ ChartType.COMPLETIONS_OVER_TIME.value: (
148
+ b'passed_date,certificate\n'
149
+ b'2021-08-25,1\n'
150
+ b'2021-09-01,2\n'
151
+ ),
152
+ ChartType.TOP_COURSES_BY_COMPLETIONS.value: (
153
+ b'course_key,course_title,certificate\n'
154
+ b'hEmW+tvk03,Re-engineered tangible approach,2\n'
155
+ ),
156
+ ChartType.TOP_SUBJECTS_BY_COMPLETIONS.value: (
157
+ b'course_subject,certificate\n'
158
+ b'business-management,2\n'
159
+ )
160
+ }
145
161
 
146
162
 
147
163
  def enrollments_dataframe():