edx-enterprise-data 8.8.2__tar.gz → 8.10.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 (190) hide show
  1. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/CHANGELOG.rst +7 -0
  2. {edx_enterprise_data-8.8.2/edx_enterprise_data.egg-info → edx_enterprise_data-8.10.0}/PKG-INFO +1 -1
  3. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0/edx_enterprise_data.egg-info}/PKG-INFO +1 -1
  4. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/edx_enterprise_data.egg-info/SOURCES.txt +2 -0
  5. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/__init__.py +1 -1
  6. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/admin_analytics/constants.py +8 -0
  7. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/admin_analytics/data_loaders.py +11 -0
  8. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v1/serializers.py +31 -1
  9. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v1/urls.py +14 -0
  10. edx_enterprise_data-8.10.0/enterprise_data/api/v1/views/analytics_engagements.py +395 -0
  11. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v1/views/analytics_enrollments.py +1 -0
  12. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v1/views/analytics_leaderboard.py +4 -1
  13. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v1/views/enterprise_admin.py +1 -0
  14. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v1/views/enterprise_completions.py +2 -0
  15. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/renderers.py +14 -0
  16. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/admin_analytics/mock_analytics_data.py +41 -1
  17. edx_enterprise_data-8.10.0/enterprise_data/tests/admin_analytics/test_analytics_engagements.py +390 -0
  18. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/LICENSE +0 -0
  19. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/MANIFEST.in +0 -0
  20. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/README.md +0 -0
  21. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/edx_enterprise_data.egg-info/dependency_links.txt +0 -0
  22. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/edx_enterprise_data.egg-info/not-zip-safe +0 -0
  23. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/edx_enterprise_data.egg-info/requires.txt +0 -0
  24. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/edx_enterprise_data.egg-info/top_level.txt +0 -0
  25. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/admin_analytics/__init__.py +0 -0
  26. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/admin_analytics/completions_utils.py +0 -0
  27. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/admin_analytics/database.py +0 -0
  28. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/admin_analytics/utils.py +0 -0
  29. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/__init__.py +0 -0
  30. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/urls.py +0 -0
  31. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v0/__init__.py +0 -0
  32. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v0/serializers.py +0 -0
  33. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v0/urls.py +0 -0
  34. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v0/views.py +0 -0
  35. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v1/__init__.py +0 -0
  36. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v1/paginators.py +0 -0
  37. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v1/views/__init__.py +0 -0
  38. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v1/views/base.py +0 -0
  39. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v1/views/enterprise_learner.py +0 -0
  40. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/api/v1/views/enterprise_offers.py +0 -0
  41. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/apps.py +0 -0
  42. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/clients.py +0 -0
  43. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/constants.py +0 -0
  44. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/filters.py +0 -0
  45. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/fixtures/enterprise_enrollment.json +0 -0
  46. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/fixtures/enterprise_user.json +0 -0
  47. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/__init__.py +0 -0
  48. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/commands/__init__.py +0 -0
  49. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/commands/create_dummy_data.py +0 -0
  50. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/commands/create_dummy_data_lpr_v1.py +0 -0
  51. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/commands/create_enterprise_enrollment.py +0 -0
  52. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/commands/create_enterprise_learner_enrollment_lpr_v1.py +0 -0
  53. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/commands/create_enterprise_learner_lpr_v1.py +0 -0
  54. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/commands/create_enterprise_offer.py +0 -0
  55. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/commands/create_enterprise_user.py +0 -0
  56. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/commands/tests/__init__.py +0 -0
  57. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/commands/tests/test_create_dummy_data_lpr_v1.py +0 -0
  58. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/commands/tests/test_create_enterprise_enrollment.py +0 -0
  59. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/commands/tests/test_create_enterprise_learner_enrollment_lpr_v1.py +0 -0
  60. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/commands/tests/test_create_enterprise_learner_lpr_v1.py +0 -0
  61. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/management/commands/tests/test_create_enterprise_user.py +0 -0
  62. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0001_initial.py +0 -0
  63. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0002_auto_20180430_1358.py +0 -0
  64. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0003_auto_20180501_0603.py +0 -0
  65. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0004_auto_20180501_0928.py +0 -0
  66. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0004_auto_20180508_1652.py +0 -0
  67. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0005_auto_20180524_2204.py +0 -0
  68. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0006_auto_20180612_0336.py +0 -0
  69. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0007_auto_20180612_0534.py +0 -0
  70. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0008_auto_20180614_0108.py +0 -0
  71. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0009_auto_20180628_1152.py +0 -0
  72. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0010_enterpriseenrollment_created.py +0 -0
  73. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0011_enterpriseuser.py +0 -0
  74. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0012_auto_20180831_1930.py +0 -0
  75. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0013_auto_20180831_1931.py +0 -0
  76. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0014_enterpriseuser_created.py +0 -0
  77. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0015_auto_20180907_1757.py +0 -0
  78. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0016_auto_20180924_2138.py +0 -0
  79. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0017_enterpriseenrollment_unenrollment_timestamp.py +0 -0
  80. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0018_enterprisedatafeaturerole_enterprisedataroleassignment.py +0 -0
  81. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0019_add_enterprise_data_feature_roles.py +0 -0
  82. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0020_add_role_based_access_control_switch.py +0 -0
  83. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0021_auto_20190329_1241.py +0 -0
  84. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0022_remove_role_based_access_control_switch.py +0 -0
  85. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0023_enterpriselearner_enterpriselearnerenrollment.py +0 -0
  86. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0024_auto_20210602_1811.py +0 -0
  87. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0025_auto_20210703_1854.py +0 -0
  88. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0026_auto_20210916_0414.py +0 -0
  89. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0027_enterpriselearnerenrollment_total_learning_time_seconds.py +0 -0
  90. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0028_enterpriselearnerenrollment_offer_id.py +0 -0
  91. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0029_enterpriseoffer.py +0 -0
  92. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0030_auto_20230609_1353.py +0 -0
  93. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0031_auto_20230615_0705.py +0 -0
  94. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0032_auto_20230704_0818.py +0 -0
  95. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0033_enterpriseadminlearnerprogress_enterpriseadminsummarizeinsights.py +0 -0
  96. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0034_auto_20230907_0834.py +0 -0
  97. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0035_auto_20230907_1154.py +0 -0
  98. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0036_enterprisesubsidybudget_subsidy_access_policy_display_name.py +0 -0
  99. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0037_alter_enterpriseenrollment_consent_granted.py +0 -0
  100. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0038_enterpriseoffer_export_timestamp.py +0 -0
  101. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0039_auto_20240212_1403.py +0 -0
  102. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0040_auto_20240718_0536_squashed_0043_alter_enterpriselearnerenrollment_enterprise_enrollment_id.py +0 -0
  103. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/0044_enterpriseexecedlcmoduleperformance.py +0 -0
  104. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/migrations/__init__.py +0 -0
  105. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/models.py +0 -0
  106. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/paginators.py +0 -0
  107. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/settings/__init__.py +0 -0
  108. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/settings/test.py +0 -0
  109. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/signals.py +0 -0
  110. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/__init__.py +0 -0
  111. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/admin_analytics/__init__.py +0 -0
  112. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/admin_analytics/mock_enrollments.py +0 -0
  113. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +0 -0
  114. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py +0 -0
  115. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/admin_analytics/test_data_loaders.py +0 -0
  116. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/admin_analytics/test_enterprise_completions.py +0 -0
  117. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/admin_analytics/test_utils.py +0 -0
  118. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/api/__init__.py +0 -0
  119. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/api/v0/__init__.py +0 -0
  120. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/api/v0/test_serializers.py +0 -0
  121. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/api/v1/__init__.py +0 -0
  122. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/api/v1/test_serializers.py +0 -0
  123. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/api/v1/test_views.py +0 -0
  124. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/api/v1/views/__init__.py +0 -0
  125. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/api/v1/views/test_enterprise_admin.py +0 -0
  126. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/factories.py +0 -0
  127. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/mixins.py +0 -0
  128. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/test_clients.py +0 -0
  129. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/test_filters.py +0 -0
  130. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/test_models.py +0 -0
  131. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/test_utils.py +0 -0
  132. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/tests/test_views.py +0 -0
  133. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/urls.py +0 -0
  134. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data/utils.py +0 -0
  135. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/__init__.py +0 -0
  136. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/admin.py +0 -0
  137. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/apps.py +0 -0
  138. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/constants.py +0 -0
  139. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/migrations/0001_initial.py +0 -0
  140. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/migrations/0002_add_enterprise_data_feature_roles.py +0 -0
  141. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/migrations/0003_add_role_based_access_control_switch.py +0 -0
  142. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/migrations/0004_enterprisedataroleassignment_enterprise_id.py +0 -0
  143. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/migrations/0005_turn_on_role_based_access_control_switch.py +0 -0
  144. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/migrations/0006_remove_role_based_access_control_switch.py +0 -0
  145. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/migrations/0007_enterprisedataroleassignment_applies_to_all_contexts.py +0 -0
  146. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/migrations/__init__.py +0 -0
  147. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/models.py +0 -0
  148. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/rules.py +0 -0
  149. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/tests/__init__.py +0 -0
  150. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/tests/factories.py +0 -0
  151. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_data_roles/tests/test_models.py +0 -0
  152. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/__init__.py +0 -0
  153. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/clients/__init__.py +0 -0
  154. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/clients/enterprise.py +0 -0
  155. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/clients/s3.py +0 -0
  156. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/clients/snowflake.py +0 -0
  157. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/clients/vertica.py +0 -0
  158. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/delivery_method.py +0 -0
  159. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/external_resource_link_report.py +0 -0
  160. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/fixtures/__init__.py +0 -0
  161. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/fixtures/enterprise_customer_reporting.json +0 -0
  162. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/reporter.py +0 -0
  163. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/send_enterprise_reports.py +0 -0
  164. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/tests/__init__.py +0 -0
  165. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/tests/test_clients.py +0 -0
  166. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/tests/test_delivery_method.py +0 -0
  167. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/tests/test_enterprise_client.py +0 -0
  168. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/tests/test_external_link_report.py +0 -0
  169. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/tests/test_reporter.py +0 -0
  170. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/tests/test_send_enterprise_reports.py +0 -0
  171. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/tests/test_utils.py +0 -0
  172. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/tests/test_vertica_client.py +0 -0
  173. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/tests/utils.py +0 -0
  174. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/enterprise_reporting/utils.py +0 -0
  175. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/requirements/base.in +0 -0
  176. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/requirements/base.txt +0 -0
  177. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/requirements/ci.txt +0 -0
  178. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/requirements/common_constraints.txt +0 -0
  179. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/requirements/constraints.txt +0 -0
  180. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/requirements/dev.txt +0 -0
  181. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/requirements/django.txt +0 -0
  182. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/requirements/pip.txt +0 -0
  183. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/requirements/pip_tools.txt +0 -0
  184. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/requirements/quality.txt +0 -0
  185. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/requirements/reporting.in +0 -0
  186. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/requirements/test-master.txt +0 -0
  187. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/requirements/test-reporting.txt +0 -0
  188. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/requirements/test.txt +0 -0
  189. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/setup.cfg +0 -0
  190. {edx_enterprise_data-8.8.2 → edx_enterprise_data-8.10.0}/setup.py +0 -0
@@ -15,6 +15,13 @@ Unreleased
15
15
  ----------
16
16
 
17
17
  =========================
18
+ [8.10.0] - 2024-08-27
19
+ ---------------------
20
+ * feat: Added API endpoints for advance analytics engagements data.
21
+
22
+ [8.9.0] - 2024-08-23
23
+ ---------------------
24
+ * chore: Added logging to measure time taken for different code blocks.
18
25
 
19
26
  [8.8.2] - 2024-08-16
20
27
  ---------------------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edx-enterprise-data
3
- Version: 8.8.2
3
+ Version: 8.10.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.8.2
3
+ Version: 8.10.0
4
4
  Summary: Enterprise Reporting
5
5
  Home-page: https://github.com/openedx/edx-enterprise-data
6
6
  Author: edX
@@ -37,6 +37,7 @@ enterprise_data/api/v1/paginators.py
37
37
  enterprise_data/api/v1/serializers.py
38
38
  enterprise_data/api/v1/urls.py
39
39
  enterprise_data/api/v1/views/__init__.py
40
+ enterprise_data/api/v1/views/analytics_engagements.py
40
41
  enterprise_data/api/v1/views/analytics_enrollments.py
41
42
  enterprise_data/api/v1/views/analytics_leaderboard.py
42
43
  enterprise_data/api/v1/views/base.py
@@ -117,6 +118,7 @@ enterprise_data/tests/test_views.py
117
118
  enterprise_data/tests/admin_analytics/__init__.py
118
119
  enterprise_data/tests/admin_analytics/mock_analytics_data.py
119
120
  enterprise_data/tests/admin_analytics/mock_enrollments.py
121
+ enterprise_data/tests/admin_analytics/test_analytics_engagements.py
120
122
  enterprise_data/tests/admin_analytics/test_analytics_enrollments.py
121
123
  enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py
122
124
  enterprise_data/tests/admin_analytics/test_data_loaders.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.8.2"
5
+ __version__ = "8.10.0"
@@ -31,3 +31,11 @@ class ResponseType(Enum):
31
31
  """Response type choices"""
32
32
  JSON = 'json'
33
33
  CSV = 'csv'
34
+
35
+
36
+ class EngagementChart(Enum):
37
+ """Response Choices"""
38
+ ENGAGEMENTS_OVER_TIME = 'engagements_over_time'
39
+ TOP_COURSES_BY_ENGAGEMENTS = 'top_courses_by_engagements'
40
+ TOP_SUBJECTS_BY_ENGAGEMENTS = 'top_subjects_by_engagements'
41
+ INDIVIDUAL_ENGAGEMENTS = 'individual_engagements'
@@ -1,6 +1,8 @@
1
1
  """
2
2
  Utility functions for fetching data from the database.
3
3
  """
4
+ from logging import getLogger
5
+
4
6
  import numpy
5
7
  import pandas
6
8
 
@@ -9,6 +11,8 @@ from django.http import Http404
9
11
  from enterprise_data.admin_analytics.database import run_query
10
12
  from enterprise_data.utils import timer
11
13
 
14
+ LOGGER = getLogger(__name__)
15
+
12
16
 
13
17
  def get_select_query(table: str, columns: list, enterprise_uuid: str) -> str:
14
18
  """
@@ -68,10 +72,13 @@ def fetch_enrollment_data(enterprise_uuid: str):
68
72
 
69
73
  with timer('fetch_enrollment_data'):
70
74
  results = run_query(query=query)
75
+
71
76
  if not results:
72
77
  raise Http404(f'No enrollment data found for enterprise {enterprise_uuid}')
73
78
 
79
+ LOGGER.info(f'[PLOTLY] Enrollment data fetched successfully. Records: {len(results)}')
74
80
  enrollments = pandas.DataFrame(numpy.array(results), columns=columns)
81
+ LOGGER.info('[PLOTLY] Enrollment data converted to DataFrame.')
75
82
 
76
83
  # Convert date columns to datetime.
77
84
  enrollments['enterprise_enrollment_date'] = enrollments['enterprise_enrollment_date'].astype('datetime64[ns]')
@@ -120,7 +127,9 @@ def fetch_engagement_data(enterprise_uuid: str):
120
127
  if not results:
121
128
  raise Http404(f'No engagement data found for enterprise {enterprise_uuid}')
122
129
 
130
+ LOGGER.info(f'[PLOTLY] Engagement data fetched successfully. Records: {len(results)}')
123
131
  engagement = pandas.DataFrame(numpy.array(results), columns=columns)
132
+ LOGGER.info('[PLOTLY] Engagement data converted to DataFrame.')
124
133
  engagement['activity_date'] = engagement['activity_date'].astype('datetime64[ns]')
125
134
 
126
135
  return engagement
@@ -180,7 +189,9 @@ def fetch_skills_data(enterprise_uuid: str):
180
189
  if not skills:
181
190
  raise Http404(f'No skills data found for enterprise {enterprise_uuid}')
182
191
 
192
+ LOGGER.info(f'[PLOTLY] Skills data fetched successfully. Records: {len(skills)}')
183
193
  skills = pandas.DataFrame(numpy.array(skills), columns=cols)
194
+ LOGGER.info('[PLOTLY] Skills data converted to DataFrame.')
184
195
  skills['date'] = skills['date'].astype('datetime64[ns]')
185
196
 
186
197
  return skills
@@ -5,7 +5,13 @@ from uuid import UUID
5
5
 
6
6
  from rest_framework import serializers
7
7
 
8
- from enterprise_data.admin_analytics.constants import Calculation, EnrollmentChart, Granularity, ResponseType
8
+ from enterprise_data.admin_analytics.constants import (
9
+ Calculation,
10
+ EngagementChart,
11
+ EnrollmentChart,
12
+ Granularity,
13
+ ResponseType,
14
+ )
9
15
  from enterprise_data.models import (
10
16
  EnterpriseAdminLearnerProgress,
11
17
  EnterpriseAdminSummarizeInsights,
@@ -331,3 +337,27 @@ class AdvanceAnalyticsEnrollmentStatsSerializer(
331
337
  if value not in self.CHART_TYPES:
332
338
  raise serializers.ValidationError(f"chart_type must be one of {self.CHART_TYPES}")
333
339
  return value
340
+
341
+
342
+ class AdvanceAnalyticsEngagementStatsSerializer(
343
+ AdvanceAnalyticsQueryParamSerializer
344
+ ): # pylint: disable=abstract-method
345
+ """Serializer for validating Advance Analytics Engagements Stats API"""
346
+ CHART_TYPES = [
347
+ EngagementChart.ENGAGEMENTS_OVER_TIME.value,
348
+ EngagementChart.TOP_COURSES_BY_ENGAGEMENTS.value,
349
+ EngagementChart.TOP_SUBJECTS_BY_ENGAGEMENTS.value
350
+ ]
351
+
352
+ chart_type = serializers.CharField(required=False)
353
+
354
+ def validate_chart_type(self, value):
355
+ """
356
+ Validate the chart_type value.
357
+
358
+ Raises:
359
+ serializers.ValidationError: If chart_type is not one of the valid choices
360
+ """
361
+ if value not in self.CHART_TYPES:
362
+ raise serializers.ValidationError(f"chart_type must be one of {self.CHART_TYPES}")
363
+ return value
@@ -11,6 +11,10 @@ from enterprise_data.api.v1.views import enterprise_admin as enterprise_admin_vi
11
11
  from enterprise_data.api.v1.views import enterprise_completions as enterprise_completions_views
12
12
  from enterprise_data.api.v1.views import enterprise_learner as enterprise_learner_views
13
13
  from enterprise_data.api.v1.views import enterprise_offers as enterprise_offers_views
14
+ from enterprise_data.api.v1.views.analytics_engagements import (
15
+ AdvanceAnalyticsEngagementStatsView,
16
+ AdvanceAnalyticsIndividualEngagementsView,
17
+ )
14
18
  from enterprise_data.api.v1.views.analytics_enrollments import (
15
19
  AdvanceAnalyticsEnrollmentStatsView,
16
20
  AdvanceAnalyticsIndividualEnrollmentsView,
@@ -73,6 +77,16 @@ urlpatterns = [
73
77
  AdvanceAnalyticsIndividualEnrollmentsView.as_view(),
74
78
  name='enterprise-admin-analytics-enrollments'
75
79
  ),
80
+ re_path(
81
+ fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/engagements/stats$',
82
+ AdvanceAnalyticsEngagementStatsView.as_view(),
83
+ name='enterprise-admin-analytics-engagements-stats'
84
+ ),
85
+ re_path(
86
+ fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/engagements$',
87
+ AdvanceAnalyticsIndividualEngagementsView.as_view(),
88
+ name='enterprise-admin-analytics-engagements'
89
+ ),
76
90
  re_path(
77
91
  fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})/skills/stats',
78
92
  enterprise_admin_views.EnterpriseAdminAnalyticsSkillsView.as_view(),
@@ -0,0 +1,395 @@
1
+ """Advance Analytics for Engagements"""
2
+ from datetime import datetime
3
+
4
+ from edx_rbac.decorators import permission_required
5
+ from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
6
+ from rest_framework import status as rest_status
7
+ from rest_framework.response import Response
8
+ from rest_framework.views import APIView
9
+
10
+ from django.http import HttpResponse, StreamingHttpResponse
11
+
12
+ from enterprise_data.admin_analytics.constants import Calculation, EngagementChart, Granularity, ResponseType
13
+ from enterprise_data.admin_analytics.utils import (
14
+ calculation_aggregation,
15
+ fetch_and_cache_engagements_data,
16
+ fetch_and_cache_enrollments_data,
17
+ fetch_enrollments_cache_expiry_timestamp,
18
+ granularity_aggregation,
19
+ )
20
+ from enterprise_data.api.v1 import serializers
21
+ from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
22
+ from enterprise_data.renderers import IndividualEngagementsCSVRenderer
23
+ from enterprise_data.utils import date_filter
24
+
25
+
26
+ class AdvanceAnalyticsIndividualEngagementsView(APIView):
27
+ """
28
+ API for getting the advance analytics individual engagements data.
29
+ """
30
+
31
+ authentication_classes = (JwtAuthentication,)
32
+ pagination_class = AdvanceAnalyticsPagination
33
+ http_method_names = ["get"]
34
+
35
+ @permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
36
+ def get(self, request, enterprise_uuid):
37
+ """
38
+ HTTP GET endpoint to retrieve the enterprise engagements data.
39
+ """
40
+ serializer = serializers.AdvanceAnalyticsEngagementStatsSerializer(data=request.GET)
41
+ serializer.is_valid(raise_exception=True)
42
+ cache_expiry = fetch_enrollments_cache_expiry_timestamp()
43
+
44
+ enrollment_df = fetch_and_cache_enrollments_data(enterprise_uuid, cache_expiry).copy()
45
+ engagement_df = fetch_and_cache_engagements_data(enterprise_uuid, cache_expiry).copy()
46
+ # Use start and end date if provided by the client, if client has not provided then use
47
+ # 1. minimum enrollment date from the data as the start_date
48
+ # 2. today's date as the end_date
49
+ start_date = serializer.data.get('start_date', enrollment_df.enterprise_enrollment_date.min())
50
+ end_date = serializer.data.get('end_date', datetime.now())
51
+ response_type = request.query_params.get('response_type', ResponseType.JSON.value)
52
+ # Date filtering.
53
+ engagements = date_filter(
54
+ start=start_date, end=end_date, data_frame=engagement_df.copy(), date_column='activity_date'
55
+ )
56
+ engagements["learning_time_hours"] = engagements["learning_time_seconds"] / 60 / 60
57
+ engagements = engagements[engagements["learning_time_hours"] > 0]
58
+ engagements["learning_time_hours"] = round(engagements["learning_time_hours"].astype(float), 1)
59
+
60
+ # Select only the columns that will be in the table.
61
+ engagements = engagements[
62
+ [
63
+ "email",
64
+ "course_title",
65
+ "activity_date",
66
+ "course_subject",
67
+ "learning_time_hours",
68
+ ]
69
+ ]
70
+ engagements["activity_date"] = engagements["activity_date"].dt.date
71
+ engagements = engagements.sort_values(by="activity_date", ascending=False).reset_index(drop=True)
72
+ if response_type == ResponseType.CSV.value:
73
+ response = StreamingHttpResponse(
74
+ IndividualEngagementsCSVRenderer().render(self._stream_serialized_data(engagements)),
75
+ content_type="text/csv"
76
+ )
77
+ start_date = start_date.strftime('%Y/%m/%d')
78
+ end_date = end_date.strftime('%Y/%m/%d')
79
+ filename = f"""Individual Engagements, {start_date} - {end_date}.csv"""
80
+ response['Content-Disposition'] = f'attachment; filename="{filename}"'
81
+ response['Access-Control-Expose-Headers'] = 'Content-Disposition'
82
+ return response
83
+
84
+ paginator = self.pagination_class()
85
+ page = paginator.paginate_queryset(engagements, request)
86
+ serialized_data = page.data.to_dict(orient='records')
87
+ response = paginator.get_paginated_response(serialized_data)
88
+
89
+ return response
90
+
91
+ def _stream_serialized_data(self, engagements, chunk_size=50000):
92
+ """
93
+ Stream the serialized data.
94
+ """
95
+ total_rows = engagements.shape[0]
96
+ for start_index in range(0, total_rows, chunk_size):
97
+ end_index = min(start_index + chunk_size, total_rows)
98
+ chunk = engagements.iloc[start_index:end_index]
99
+ yield from chunk.to_dict(orient='records')
100
+
101
+
102
+ class AdvanceAnalyticsEngagementStatsView(APIView):
103
+ """
104
+ API for getting the advance analytics engagements statistics data.
105
+ """
106
+
107
+ authentication_classes = (JwtAuthentication,)
108
+ http_method_names = ["get"]
109
+
110
+ @permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
111
+ def get(self, request, enterprise_uuid):
112
+ """
113
+ HTTP GET endpoint to retrieve the enterprise engagements statistics data.
114
+ """
115
+ serializer = serializers.AdvanceAnalyticsEngagementStatsSerializer(data=request.GET)
116
+ serializer.is_valid(raise_exception=True)
117
+
118
+ cache_expiry = fetch_enrollments_cache_expiry_timestamp()
119
+
120
+ enrollment_df = fetch_and_cache_enrollments_data(enterprise_uuid, cache_expiry).copy()
121
+ engagement_df = fetch_and_cache_engagements_data(enterprise_uuid, cache_expiry).copy()
122
+ # Use start and end date if provided by the client, if client has not provided then use
123
+ # 1. minimum enrollment date from the data as the start_date
124
+ # 2. today's date as the end_date
125
+ start_date = serializer.data.get('start_date', enrollment_df.enterprise_enrollment_date.min())
126
+ end_date = serializer.data.get('end_date', datetime.now())
127
+ granularity = serializer.data.get('granularity', Granularity.DAILY.value)
128
+ calculation = serializer.data.get('calculation', Calculation.TOTAL.value)
129
+ chart_type = serializer.data.get('chart_type')
130
+
131
+ if chart_type is None:
132
+ data = {
133
+ "engagements_over_time": self.construct_engagements_over_time(
134
+ engagement_df.copy(),
135
+ start_date,
136
+ end_date,
137
+ granularity,
138
+ calculation,
139
+ ),
140
+ "top_courses_by_engagement": self.construct_top_courses_by_engagements(
141
+ engagement_df.copy(),
142
+ start_date,
143
+ end_date,
144
+ ),
145
+ "top_subjects_by_engagement": self.construct_top_subjects_by_engagements(
146
+ engagement_df.copy(),
147
+ start_date,
148
+ end_date,
149
+ ),
150
+ }
151
+ return Response(data)
152
+ elif chart_type == EngagementChart.ENGAGEMENTS_OVER_TIME.value:
153
+ return self.construct_engagements_over_time_csv(
154
+ engagement_df.copy(),
155
+ start_date,
156
+ end_date,
157
+ granularity,
158
+ calculation,
159
+ )
160
+ elif chart_type == EngagementChart.TOP_COURSES_BY_ENGAGEMENTS.value:
161
+ return self.construct_top_courses_by_engagements_csv(
162
+ engagement_df.copy(),
163
+ start_date,
164
+ end_date,
165
+ )
166
+ elif chart_type == EngagementChart.TOP_SUBJECTS_BY_ENGAGEMENTS.value:
167
+ return self.construct_top_subjects_by_engagements_csv(
168
+ engagement_df.copy(),
169
+ start_date,
170
+ end_date,
171
+ )
172
+ return Response(data='Not Found', status=rest_status.HTTP_400_BAD_REQUEST)
173
+
174
+ def engagements_over_time_common(self, engagements_df, start_date, end_date, granularity, calculation):
175
+ """
176
+ Common method for constructing engagements over time data.
177
+
178
+ Arguments:
179
+ engagements_df {DataFrame} -- DataFrame of engagements
180
+ start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
181
+ end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
182
+ granularity {str} -- Granularity of the data. One of Granularity choices
183
+ calculation {str} -- Calculation of the data. One of Calculation choices
184
+ """
185
+ engagements_df["learning_time_hours"] = engagements_df["learning_time_seconds"] / 60 / 60
186
+ engagements_df = engagements_df[engagements_df["learning_time_hours"] > 0]
187
+ engagements_df["learning_time_hours"] = round(engagements_df["learning_time_hours"].astype(float), 1)
188
+
189
+ engagements_df = engagements_df[["activity_date", "enroll_type", "learning_time_hours"]]
190
+
191
+ # Date filtering.
192
+ engagements_df = date_filter(
193
+ start=start_date, end=end_date, data_frame=engagements_df, date_column="activity_date"
194
+ )
195
+
196
+ # Date aggregation.
197
+ engagements_df = granularity_aggregation(
198
+ level=granularity,
199
+ group=["activity_date", "enroll_type"],
200
+ date="activity_date",
201
+ data_frame=engagements_df,
202
+ aggregation_type="sum",
203
+ )
204
+
205
+ # Calculating metric.
206
+ engagements = calculation_aggregation(calc=calculation, aggregation_type="sum", data_frame=engagements_df)
207
+ return engagements
208
+
209
+ def construct_engagements_over_time(self, engagements_df, start_date, end_date, granularity, calculation):
210
+ """
211
+ Construct engagements over time data.
212
+
213
+ Arguments:
214
+ engagements_df {DataFrame} -- DataFrame of engagements
215
+ start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
216
+ end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
217
+ granularity {str} -- Granularity of the data. One of Granularity choices
218
+ calculation {str} -- Calculation of the data. One of Calculation choices
219
+ """
220
+ engagements = self.engagements_over_time_common(engagements_df, start_date, end_date, granularity, calculation)
221
+ # convert dataframe to a list of records
222
+ return engagements.to_dict(orient='records')
223
+
224
+ def construct_engagements_over_time_csv(self, engagements_df, start_date, end_date, granularity, calculation):
225
+ """
226
+ Construct engagements over time CSV.
227
+
228
+ Arguments:
229
+ engagements_df {DataFrame} -- DataFrame of engagements
230
+ start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
231
+ end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
232
+ granularity {str} -- Granularity of the data. One of Granularity choices
233
+ calculation {str} -- Calculation of the data. One of Calculation choices
234
+ """
235
+ engagements = self.engagements_over_time_common(engagements_df, start_date, end_date, granularity, calculation)
236
+
237
+ engagements = engagements.pivot(
238
+ index="activity_date", columns="enroll_type", values="sum"
239
+ )
240
+
241
+ filename = f"Engagement Timeseries, {start_date} - {end_date} ({granularity} {calculation}).csv"
242
+ return self.construct_csv_response(engagements, filename)
243
+
244
+ def top_courses_by_engagements_common(self, engagements_df, start_date, end_date):
245
+ """
246
+ Common method for constructing top courses by engagements data.
247
+
248
+ Arguments:
249
+ engagements_df {DataFrame} -- DataFrame of engagements
250
+ start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
251
+ end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
252
+ group_by_columns {list} -- List of columns to group by
253
+ columns {list} -- List of column for the final result
254
+ """
255
+ engagements_df["learning_time_hours"] = engagements_df["learning_time_seconds"] / 60 / 60
256
+ engagements_df["learning_time_hours"] = engagements_df["learning_time_hours"].astype("float")
257
+
258
+ # Date filtering.
259
+ engagements = date_filter(
260
+ start=start_date, end=end_date, data_frame=engagements_df, date_column="activity_date"
261
+ )
262
+
263
+ courses = list(
264
+ engagements.groupby(["course_key"])
265
+ .learning_time_hours.sum()
266
+ .sort_values(ascending=False)[:10]
267
+ .index
268
+ )
269
+
270
+ engagements = (
271
+ engagements_df[engagements_df.course_key.isin(courses)]
272
+ .groupby(["course_key", "course_title", "enroll_type"])
273
+ .learning_time_hours.sum()
274
+ .reset_index()
275
+ )
276
+
277
+ engagements.columns = ["course_key", "course_title", "enroll_type", "count"]
278
+
279
+ return engagements
280
+
281
+ def construct_top_courses_by_engagements(self, engagements_df, start_date, end_date):
282
+ """
283
+ Construct top courses by engagements data.
284
+
285
+ Arguments:
286
+ engagements_df {DataFrame} -- DataFrame of engagements
287
+ start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
288
+ end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
289
+ """
290
+ engagements = self.top_courses_by_engagements_common(
291
+ engagements_df,
292
+ start_date,
293
+ end_date
294
+ )
295
+
296
+ # convert dataframe to a list of records
297
+ return engagements.to_dict(orient='records')
298
+
299
+ def construct_top_courses_by_engagements_csv(self, engagements_df, start_date, end_date):
300
+ """
301
+ Construct top courses by engagements CSV.
302
+
303
+ Arguments:
304
+ engagements_df {DataFrame} -- DataFrame of engagements
305
+ start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
306
+ end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
307
+ """
308
+ engagements = self.top_courses_by_engagements_common(
309
+ engagements_df,
310
+ start_date,
311
+ end_date
312
+ )
313
+
314
+ engagements = engagements.pivot(
315
+ index=["course_key", "course_title"], columns="enroll_type", values="count"
316
+ )
317
+
318
+ filename = f"Top 10 Courses by Learning Hours, {start_date} - {end_date}.csv"
319
+ return self.construct_csv_response(engagements, filename)
320
+
321
+ def top_subjects_by_engagements_common(self, engagements_df, start_date, end_date):
322
+ """
323
+ Common method for constructing top subjects by engagements data.
324
+
325
+ Arguments:
326
+ engagements_df {DataFrame} -- DataFrame of engagements
327
+ start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
328
+ end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
329
+ """
330
+ engagements_df["learning_time_hours"] = engagements_df["learning_time_seconds"] / 60 / 60
331
+ engagements_df["learning_time_hours"] = engagements_df["learning_time_hours"].astype("float")
332
+
333
+ # Date filtering.
334
+ engagements = date_filter(
335
+ start=start_date, end=end_date, data_frame=engagements_df, date_column="activity_date"
336
+ )
337
+
338
+ subjects = list(
339
+ engagements.groupby(["course_subject"])
340
+ .learning_time_hours.sum()
341
+ .sort_values(ascending=False)[:10]
342
+ .index
343
+ )
344
+
345
+ engagements = (
346
+ engagements[engagements.course_subject.isin(subjects)]
347
+ .groupby(["course_subject", "enroll_type"])
348
+ .learning_time_hours.sum()
349
+ .reset_index()
350
+ )
351
+ engagements.columns = ["course_subject", "enroll_type", "count"]
352
+
353
+ return engagements
354
+
355
+ def construct_top_subjects_by_engagements(self, engagements_df, start_date, end_date):
356
+ """
357
+ Construct top subjects by engagements data.
358
+
359
+ Arguments:
360
+ engagements_df {DataFrame} -- DataFrame of engagements
361
+ start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
362
+ end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
363
+ """
364
+ engagements = self.top_subjects_by_engagements_common(engagements_df, start_date, end_date)
365
+ # convert dataframe to a list of records
366
+ return engagements.to_dict(orient='records')
367
+
368
+ def construct_top_subjects_by_engagements_csv(self, engagements_df, start_date, end_date):
369
+ """
370
+ Construct top subjects by engagements CSV.
371
+
372
+ Arguments:
373
+ engagements_df {DataFrame} -- DataFrame of engagements
374
+ start_date {datetime} -- Engagement start date in the format 'YYYY-MM-DD'
375
+ end_date {datetime} -- Engagement end date in the format 'YYYY-MM-DD'
376
+ """
377
+ engagements = self.top_subjects_by_engagements_common(engagements_df, start_date, end_date)
378
+ engagements = engagements.pivot(index="course_subject", columns="enroll_type", values="count")
379
+ filename = f"Top 10 Subjects by Learning Hours, {start_date} - {end_date}.csv"
380
+ return self.construct_csv_response(engagements, filename)
381
+
382
+ def construct_csv_response(self, engagements, filename):
383
+ """
384
+ Construct CSV response.
385
+
386
+ Arguments:
387
+ engagements {DataFrame} -- DataFrame of engagements
388
+ filename {str} -- Filename for the CSV
389
+ """
390
+ response = HttpResponse(content_type='text/csv')
391
+ response['Content-Disposition'] = f'attachment; filename="{filename}"'
392
+ response['Access-Control-Expose-Headers'] = 'Content-Disposition'
393
+ engagements.to_csv(path_or_buf=response)
394
+
395
+ return response
@@ -382,6 +382,7 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
382
382
  """
383
383
  response = HttpResponse(content_type='text/csv')
384
384
  response['Content-Disposition'] = f'attachment; filename="{filename}"'
385
+ response['Access-Control-Expose-Headers'] = 'Content-Disposition'
385
386
  enrollments.to_csv(path_or_buf=response)
386
387
 
387
388
  return response
@@ -120,7 +120,10 @@ class AdvanceAnalyticsLeaderboardView(APIView):
120
120
  return StreamingHttpResponse(
121
121
  LeaderboardCSVRenderer().render(self._stream_serialized_data(leaderboard_df)),
122
122
  content_type="text/csv",
123
- headers={"Content-Disposition": f'attachment; filename="{filename}"'},
123
+ headers={
124
+ "Content-Disposition": f'attachment; filename="{filename}"',
125
+ "Access-Control-Expose-Headers": "Content-Disposition"
126
+ },
124
127
  )
125
128
 
126
129
  paginator = self.pagination_class()
@@ -208,6 +208,7 @@ class EnterpriseAdminAnalyticsSkillsView(APIView):
208
208
  response = HttpResponse(content_type='text/csv')
209
209
  filename = f"Skills by Enrollment and Completion, {start_date} - {end_date}.csv"
210
210
  response['Content-Disposition'] = f'attachment; filename="{filename}"'
211
+ response['Access-Control-Expose-Headers'] = 'Content-Disposition'
211
212
  csv_data.to_csv(path_or_buf=response, index=False)
212
213
  return response
213
214
 
@@ -90,6 +90,7 @@ class EnterrpiseAdminCompletionsStatsView(APIView):
90
90
  )
91
91
  filename = csv_data['filename']
92
92
  response['Content-Disposition'] = f'attachment; filename="{filename}"'
93
+ response['Access-Control-Expose-Headers'] = 'Content-Disposition'
93
94
  csv_data['data'].to_csv(path_or_buf=response)
94
95
  return response
95
96
 
@@ -187,6 +188,7 @@ class EnterrpiseAdminCompletionsView(APIView):
187
188
  response = HttpResponse(content_type='text/csv')
188
189
  filename = f"Individual Completions, {start_date} - {end_date}.csv"
189
190
  response['Content-Disposition'] = f'attachment; filename="{filename}"'
191
+ response['Access-Control-Expose-Headers'] = 'Content-Disposition'
190
192
  dff.to_csv(path_or_buf=response, index=False)
191
193
  return response
192
194
 
@@ -57,3 +57,17 @@ class LeaderboardCSVRenderer(CSVStreamingRenderer):
57
57
  'average_session_length',
58
58
  'course_completions',
59
59
  ]
60
+
61
+
62
+ class IndividualEngagementsCSVRenderer(CSVStreamingRenderer):
63
+ """
64
+ Custom streaming csv renderer for advance analytics individual engagements data.
65
+ """
66
+
67
+ header = [
68
+ 'email',
69
+ 'course_title',
70
+ 'activity_date',
71
+ 'course_subject',
72
+ 'learning_time_hours',
73
+ ]