edx-enterprise-data 8.7.0__tar.gz → 8.8.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 (188) hide show
  1. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/CHANGELOG.rst +5 -0
  2. {edx_enterprise_data-8.7.0/edx_enterprise_data.egg-info → edx_enterprise_data-8.8.0}/PKG-INFO +1 -1
  3. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0/edx_enterprise_data.egg-info}/PKG-INFO +1 -1
  4. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/edx_enterprise_data.egg-info/SOURCES.txt +3 -0
  5. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/__init__.py +1 -1
  6. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/admin_analytics/constants.py +9 -3
  7. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/admin_analytics/utils.py +46 -10
  8. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v1/paginators.py +1 -1
  9. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v1/serializers.py +39 -30
  10. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v1/urls.py +12 -6
  11. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v1/views/analytics_enrollments.py +42 -53
  12. edx_enterprise_data-8.8.0/enterprise_data/api/v1/views/analytics_leaderboard.py +120 -0
  13. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v1/views/enterprise_completions.py +5 -5
  14. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/renderers.py +14 -0
  15. edx_enterprise_data-8.8.0/enterprise_data/tests/admin_analytics/mock_analytics_data.py +501 -0
  16. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/admin_analytics/mock_enrollments.py +4 -4
  17. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +23 -22
  18. edx_enterprise_data-8.8.0/enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py +163 -0
  19. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/admin_analytics/test_enterprise_completions.py +1 -1
  20. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/LICENSE +0 -0
  21. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/MANIFEST.in +0 -0
  22. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/README.md +0 -0
  23. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/edx_enterprise_data.egg-info/dependency_links.txt +0 -0
  24. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/edx_enterprise_data.egg-info/not-zip-safe +0 -0
  25. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/edx_enterprise_data.egg-info/requires.txt +0 -0
  26. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/edx_enterprise_data.egg-info/top_level.txt +0 -0
  27. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/admin_analytics/__init__.py +0 -0
  28. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/admin_analytics/completions_utils.py +0 -0
  29. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/admin_analytics/data_loaders.py +0 -0
  30. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/admin_analytics/database.py +0 -0
  31. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/__init__.py +0 -0
  32. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/urls.py +0 -0
  33. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v0/__init__.py +0 -0
  34. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v0/serializers.py +0 -0
  35. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v0/urls.py +0 -0
  36. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v0/views.py +0 -0
  37. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v1/__init__.py +0 -0
  38. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v1/views/__init__.py +0 -0
  39. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v1/views/base.py +0 -0
  40. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v1/views/enterprise_admin.py +0 -0
  41. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v1/views/enterprise_learner.py +0 -0
  42. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/api/v1/views/enterprise_offers.py +0 -0
  43. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/apps.py +0 -0
  44. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/clients.py +0 -0
  45. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/constants.py +0 -0
  46. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/filters.py +0 -0
  47. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/fixtures/enterprise_enrollment.json +0 -0
  48. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/fixtures/enterprise_user.json +0 -0
  49. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/__init__.py +0 -0
  50. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/commands/__init__.py +0 -0
  51. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/commands/create_dummy_data.py +0 -0
  52. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/commands/create_dummy_data_lpr_v1.py +0 -0
  53. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/commands/create_enterprise_enrollment.py +0 -0
  54. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/commands/create_enterprise_learner_enrollment_lpr_v1.py +0 -0
  55. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/commands/create_enterprise_learner_lpr_v1.py +0 -0
  56. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/commands/create_enterprise_offer.py +0 -0
  57. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/commands/create_enterprise_user.py +0 -0
  58. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/commands/tests/__init__.py +0 -0
  59. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/commands/tests/test_create_dummy_data_lpr_v1.py +0 -0
  60. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/commands/tests/test_create_enterprise_enrollment.py +0 -0
  61. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/commands/tests/test_create_enterprise_learner_enrollment_lpr_v1.py +0 -0
  62. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/commands/tests/test_create_enterprise_learner_lpr_v1.py +0 -0
  63. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/management/commands/tests/test_create_enterprise_user.py +0 -0
  64. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0001_initial.py +0 -0
  65. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0002_auto_20180430_1358.py +0 -0
  66. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0003_auto_20180501_0603.py +0 -0
  67. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0004_auto_20180501_0928.py +0 -0
  68. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0004_auto_20180508_1652.py +0 -0
  69. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0005_auto_20180524_2204.py +0 -0
  70. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0006_auto_20180612_0336.py +0 -0
  71. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0007_auto_20180612_0534.py +0 -0
  72. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0008_auto_20180614_0108.py +0 -0
  73. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0009_auto_20180628_1152.py +0 -0
  74. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0010_enterpriseenrollment_created.py +0 -0
  75. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0011_enterpriseuser.py +0 -0
  76. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0012_auto_20180831_1930.py +0 -0
  77. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0013_auto_20180831_1931.py +0 -0
  78. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0014_enterpriseuser_created.py +0 -0
  79. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0015_auto_20180907_1757.py +0 -0
  80. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0016_auto_20180924_2138.py +0 -0
  81. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0017_enterpriseenrollment_unenrollment_timestamp.py +0 -0
  82. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0018_enterprisedatafeaturerole_enterprisedataroleassignment.py +0 -0
  83. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0019_add_enterprise_data_feature_roles.py +0 -0
  84. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0020_add_role_based_access_control_switch.py +0 -0
  85. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0021_auto_20190329_1241.py +0 -0
  86. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0022_remove_role_based_access_control_switch.py +0 -0
  87. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0023_enterpriselearner_enterpriselearnerenrollment.py +0 -0
  88. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0024_auto_20210602_1811.py +0 -0
  89. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0025_auto_20210703_1854.py +0 -0
  90. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0026_auto_20210916_0414.py +0 -0
  91. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0027_enterpriselearnerenrollment_total_learning_time_seconds.py +0 -0
  92. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0028_enterpriselearnerenrollment_offer_id.py +0 -0
  93. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0029_enterpriseoffer.py +0 -0
  94. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0030_auto_20230609_1353.py +0 -0
  95. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0031_auto_20230615_0705.py +0 -0
  96. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0032_auto_20230704_0818.py +0 -0
  97. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0033_enterpriseadminlearnerprogress_enterpriseadminsummarizeinsights.py +0 -0
  98. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0034_auto_20230907_0834.py +0 -0
  99. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0035_auto_20230907_1154.py +0 -0
  100. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0036_enterprisesubsidybudget_subsidy_access_policy_display_name.py +0 -0
  101. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0037_alter_enterpriseenrollment_consent_granted.py +0 -0
  102. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0038_enterpriseoffer_export_timestamp.py +0 -0
  103. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0039_auto_20240212_1403.py +0 -0
  104. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0040_auto_20240718_0536_squashed_0043_alter_enterpriselearnerenrollment_enterprise_enrollment_id.py +0 -0
  105. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/0044_enterpriseexecedlcmoduleperformance.py +0 -0
  106. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/migrations/__init__.py +0 -0
  107. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/models.py +0 -0
  108. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/paginators.py +0 -0
  109. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/settings/__init__.py +0 -0
  110. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/settings/test.py +0 -0
  111. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/signals.py +0 -0
  112. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/__init__.py +0 -0
  113. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/admin_analytics/__init__.py +0 -0
  114. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/admin_analytics/test_data_loaders.py +0 -0
  115. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/admin_analytics/test_utils.py +0 -0
  116. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/api/__init__.py +0 -0
  117. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/api/v0/__init__.py +0 -0
  118. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/api/v0/test_serializers.py +0 -0
  119. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/api/v1/__init__.py +0 -0
  120. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/api/v1/test_serializers.py +0 -0
  121. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/api/v1/test_views.py +0 -0
  122. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/api/v1/views/__init__.py +0 -0
  123. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/api/v1/views/test_enterprise_admin.py +0 -0
  124. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/factories.py +0 -0
  125. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/mixins.py +0 -0
  126. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/test_clients.py +0 -0
  127. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/test_filters.py +0 -0
  128. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/test_models.py +0 -0
  129. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/test_utils.py +0 -0
  130. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/tests/test_views.py +0 -0
  131. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/urls.py +0 -0
  132. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data/utils.py +0 -0
  133. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/__init__.py +0 -0
  134. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/admin.py +0 -0
  135. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/apps.py +0 -0
  136. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/constants.py +0 -0
  137. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/migrations/0001_initial.py +0 -0
  138. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/migrations/0002_add_enterprise_data_feature_roles.py +0 -0
  139. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/migrations/0003_add_role_based_access_control_switch.py +0 -0
  140. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/migrations/0004_enterprisedataroleassignment_enterprise_id.py +0 -0
  141. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/migrations/0005_turn_on_role_based_access_control_switch.py +0 -0
  142. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/migrations/0006_remove_role_based_access_control_switch.py +0 -0
  143. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/migrations/0007_enterprisedataroleassignment_applies_to_all_contexts.py +0 -0
  144. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/migrations/__init__.py +0 -0
  145. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/models.py +0 -0
  146. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/rules.py +0 -0
  147. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/tests/__init__.py +0 -0
  148. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/tests/factories.py +0 -0
  149. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_data_roles/tests/test_models.py +0 -0
  150. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/__init__.py +0 -0
  151. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/clients/__init__.py +0 -0
  152. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/clients/enterprise.py +0 -0
  153. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/clients/s3.py +0 -0
  154. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/clients/snowflake.py +0 -0
  155. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/clients/vertica.py +0 -0
  156. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/delivery_method.py +0 -0
  157. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/external_resource_link_report.py +0 -0
  158. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/fixtures/__init__.py +0 -0
  159. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/fixtures/enterprise_customer_reporting.json +0 -0
  160. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/reporter.py +0 -0
  161. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/send_enterprise_reports.py +0 -0
  162. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/tests/__init__.py +0 -0
  163. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/tests/test_clients.py +0 -0
  164. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/tests/test_delivery_method.py +0 -0
  165. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/tests/test_enterprise_client.py +0 -0
  166. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/tests/test_external_link_report.py +0 -0
  167. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/tests/test_reporter.py +0 -0
  168. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/tests/test_send_enterprise_reports.py +0 -0
  169. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/tests/test_utils.py +0 -0
  170. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/tests/test_vertica_client.py +0 -0
  171. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/tests/utils.py +0 -0
  172. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/enterprise_reporting/utils.py +0 -0
  173. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/requirements/base.in +0 -0
  174. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/requirements/base.txt +0 -0
  175. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/requirements/ci.txt +0 -0
  176. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/requirements/common_constraints.txt +0 -0
  177. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/requirements/constraints.txt +0 -0
  178. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/requirements/dev.txt +0 -0
  179. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/requirements/django.txt +0 -0
  180. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/requirements/pip.txt +0 -0
  181. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/requirements/pip_tools.txt +0 -0
  182. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/requirements/quality.txt +0 -0
  183. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/requirements/reporting.in +0 -0
  184. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/requirements/test-master.txt +0 -0
  185. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/requirements/test-reporting.txt +0 -0
  186. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/requirements/test.txt +0 -0
  187. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/setup.cfg +0 -0
  188. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.0}/setup.py +0 -0
@@ -16,6 +16,11 @@ Unreleased
16
16
 
17
17
  =========================
18
18
 
19
+ [8.8.0] - 2024-08-15
20
+ ---------------------
21
+ * feat: Add API endpoints for advance analytics leaderboard data
22
+ * refactor: Use `response_type` and `chart_type` in advance analytics enrollments API endpoints
23
+
19
24
  [8.7.0] - 2024-08-13
20
25
  ---------------------
21
26
  * feat: add endpoints to get completion data for an enterprise customer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edx-enterprise-data
3
- Version: 8.7.0
3
+ Version: 8.8.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.7.0
3
+ Version: 8.8.0
4
4
  Summary: Enterprise Reporting
5
5
  Home-page: https://github.com/openedx/edx-enterprise-data
6
6
  Author: edX
@@ -38,6 +38,7 @@ enterprise_data/api/v1/serializers.py
38
38
  enterprise_data/api/v1/urls.py
39
39
  enterprise_data/api/v1/views/__init__.py
40
40
  enterprise_data/api/v1/views/analytics_enrollments.py
41
+ enterprise_data/api/v1/views/analytics_leaderboard.py
41
42
  enterprise_data/api/v1/views/base.py
42
43
  enterprise_data/api/v1/views/enterprise_admin.py
43
44
  enterprise_data/api/v1/views/enterprise_completions.py
@@ -114,8 +115,10 @@ enterprise_data/tests/test_models.py
114
115
  enterprise_data/tests/test_utils.py
115
116
  enterprise_data/tests/test_views.py
116
117
  enterprise_data/tests/admin_analytics/__init__.py
118
+ enterprise_data/tests/admin_analytics/mock_analytics_data.py
117
119
  enterprise_data/tests/admin_analytics/mock_enrollments.py
118
120
  enterprise_data/tests/admin_analytics/test_analytics_enrollments.py
121
+ enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py
119
122
  enterprise_data/tests/admin_analytics/test_data_loaders.py
120
123
  enterprise_data/tests/admin_analytics/test_enterprise_completions.py
121
124
  enterprise_data/tests/admin_analytics/test_utils.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.7.0"
5
+ __version__ = "8.8.0"
@@ -3,7 +3,7 @@
3
3
  from enum import Enum
4
4
 
5
5
 
6
- class GRANULARITY(Enum):
6
+ class Granularity(Enum):
7
7
  """Granularity choices"""
8
8
  DAILY = 'Daily'
9
9
  WEEKLY = 'Weekly'
@@ -11,7 +11,7 @@ class GRANULARITY(Enum):
11
11
  QUARTERLY = 'Quarterly'
12
12
 
13
13
 
14
- class CALCULATION(Enum):
14
+ class Calculation(Enum):
15
15
  """Calculation choices"""
16
16
  TOTAL = 'Total'
17
17
  RUNNING_TOTAL = 'Running Total'
@@ -19,9 +19,15 @@ class CALCULATION(Enum):
19
19
  MOVING_AVERAGE_7_PERIOD = 'Moving Average (7 Period)'
20
20
 
21
21
 
22
- class ENROLLMENT_CSV(Enum):
22
+ class EnrollmentChart(Enum):
23
23
  """CSV choices"""
24
24
  ENROLLMENTS_OVER_TIME = 'enrollments_over_time'
25
25
  TOP_COURSES_BY_ENROLLMENTS = 'top_courses_by_enrollments'
26
26
  TOP_SUBJECTS_BY_ENROLLMENTS = 'top_subjects_by_enrollments'
27
27
  INDIVIDUAL_ENROLLMENTS = 'individual_enrollments'
28
+
29
+
30
+ class ResponseType(Enum):
31
+ """Response type choices"""
32
+ JSON = 'json'
33
+ CSV = 'csv'
@@ -1,13 +1,18 @@
1
1
  """
2
2
  Utility functions for fetching data from the database.
3
3
  """
4
- from datetime import datetime
4
+ from datetime import datetime, timedelta
5
5
  from enum import Enum
6
6
 
7
7
  from edx_django_utils.cache import TieredCache, get_cache_key
8
8
 
9
- from enterprise_data.admin_analytics.constants import CALCULATION, GRANULARITY
10
- from enterprise_data.admin_analytics.data_loaders import fetch_engagement_data, fetch_enrollment_data, fetch_skills_data
9
+ from enterprise_data.admin_analytics.constants import Calculation, Granularity
10
+ from enterprise_data.admin_analytics.data_loaders import (
11
+ fetch_engagement_data,
12
+ fetch_enrollment_data,
13
+ fetch_max_enrollment_datetime,
14
+ fetch_skills_data,
15
+ )
11
16
  from enterprise_data.utils import date_filter, primary_subject_truncate
12
17
 
13
18
 
@@ -23,14 +28,45 @@ class ChartType(Enum):
23
28
  TOP_SUBJECTS_BY_COMPLETIONS = 'top_subjects_by_completions'
24
29
 
25
30
 
31
+ def fetch_enrollments_cache_expiry_timestamp():
32
+ """Calculate cache expiry timestamp"""
33
+ # TODO: Implement correct cache expiry logic for `enrollments` data.
34
+ # Current cache expiry logic is based on `enterprise_learner_enrollment` table,
35
+ # Which has nothing to do with the `enrollments` data. Instead cache expiry should
36
+ # be based on `fact_enrollment_admin_dash` table. Currently we have no timestamp in
37
+ # `fact_enrollment_admin_dash` table that can be used for cache expiry. Add a new
38
+ # column in the table for this purpose and then use that column for cache expiry.
39
+ last_updated_at = fetch_max_enrollment_datetime()
40
+ cache_expiry = (
41
+ last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
42
+ )
43
+ return cache_expiry
44
+
45
+
46
+ def fetch_engagements_cache_expiry_timestamp():
47
+ """Calculate cache expiry timestamp"""
48
+ # TODO: Implement correct cache expiry logic for `engagements` data.
49
+ # Current cache expiry logic is based on `enterprise_learner_enrollment` table,
50
+ # Which has nothing to do with the `engagements` data. Instead cache expiry should
51
+ # be based on `fact_enrollment_engagement_day_admin_dash` table. Currently we have
52
+ # no timestamp in `fact_enrollment_engagement_day_admin_dash` table that can be used
53
+ # for cache expiry. Add a new column in the table for this purpose and then use that
54
+ # column for cache expiry.
55
+ last_updated_at = fetch_max_enrollment_datetime()
56
+ cache_expiry = (
57
+ last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
58
+ )
59
+ return cache_expiry
60
+
61
+
26
62
  def granularity_aggregation(level, group, date, data_frame, aggregation_type="count"):
27
63
  """Aggregate data based on granularity"""
28
64
  df = data_frame
29
65
 
30
66
  period_mapping = {
31
- GRANULARITY.WEEKLY.value: "W",
32
- GRANULARITY.MONTHLY.value: "M",
33
- GRANULARITY.QUARTERLY.value: "Q"
67
+ Granularity.WEEKLY.value: "W",
68
+ Granularity.MONTHLY.value: "M",
69
+ Granularity.QUARTERLY.value: "Q"
34
70
  }
35
71
 
36
72
  if level in period_mapping:
@@ -52,15 +88,15 @@ def calculation_aggregation(calc, data_frame, aggregation_type="count"):
52
88
  df = data_frame
53
89
 
54
90
  window_mapping = {
55
- CALCULATION.MOVING_AVERAGE_3_PERIOD.value: 3,
56
- CALCULATION.MOVING_AVERAGE_7_PERIOD.value: 7,
91
+ Calculation.MOVING_AVERAGE_3_PERIOD.value: 3,
92
+ Calculation.MOVING_AVERAGE_7_PERIOD.value: 7,
57
93
  }
58
94
 
59
95
  aggregation_column = "count" if aggregation_type == "count" else "sum"
60
96
 
61
- if calc == CALCULATION.RUNNING_TOTAL.value:
97
+ if calc == Calculation.RUNNING_TOTAL.value:
62
98
  df[aggregation_column] = df.groupby("enroll_type")[aggregation_column].cumsum()
63
- elif calc in [CALCULATION.MOVING_AVERAGE_3_PERIOD.value, CALCULATION.MOVING_AVERAGE_7_PERIOD.value]:
99
+ elif calc in [Calculation.MOVING_AVERAGE_3_PERIOD.value, Calculation.MOVING_AVERAGE_7_PERIOD.value]:
64
100
  df[aggregation_column] = (
65
101
  df.groupby("enroll_type")[aggregation_column]
66
102
  .rolling(window_mapping[calc])
@@ -72,7 +72,7 @@ class AdvanceAnalyticsPagination(PageNumberPagination):
72
72
  max_page_size (int): The maximum allowed page size.
73
73
  """
74
74
  page_size_query_param = "page_size"
75
- page_size = 10
75
+ page_size = 50
76
76
  max_page_size = 100
77
77
 
78
78
  def paginate_queryset(self, queryset, request, view=None):
@@ -5,7 +5,7 @@ from uuid import UUID
5
5
 
6
6
  from rest_framework import serializers
7
7
 
8
- from enterprise_data.admin_analytics.constants import CALCULATION, ENROLLMENT_CSV, GRANULARITY
8
+ from enterprise_data.admin_analytics.constants import Calculation, EnrollmentChart, Granularity, ResponseType
9
9
  from enterprise_data.models import (
10
10
  EnterpriseAdminLearnerProgress,
11
11
  EnterpriseAdminSummarizeInsights,
@@ -237,23 +237,28 @@ class EnterpriseExecEdLCModulePerformanceSerializer(serializers.ModelSerializer)
237
237
 
238
238
  class AdvanceAnalyticsQueryParamSerializer(serializers.Serializer): # pylint: disable=abstract-method
239
239
  """Serializer for validating query params"""
240
+ RESPONSE_TYPES = [
241
+ ResponseType.JSON.value,
242
+ ResponseType.CSV.value
243
+ ]
240
244
  GRANULARITY_CHOICES = [
241
- GRANULARITY.DAILY.value,
242
- GRANULARITY.WEEKLY.value,
243
- GRANULARITY.MONTHLY.value,
244
- GRANULARITY.QUARTERLY.value
245
+ Granularity.DAILY.value,
246
+ Granularity.WEEKLY.value,
247
+ Granularity.MONTHLY.value,
248
+ Granularity.QUARTERLY.value
245
249
  ]
246
250
  CALCULATION_CHOICES = [
247
- CALCULATION.TOTAL.value,
248
- CALCULATION.RUNNING_TOTAL.value,
249
- CALCULATION.MOVING_AVERAGE_3_PERIOD.value,
250
- CALCULATION.MOVING_AVERAGE_7_PERIOD.value
251
+ Calculation.TOTAL.value,
252
+ Calculation.RUNNING_TOTAL.value,
253
+ Calculation.MOVING_AVERAGE_3_PERIOD.value,
254
+ Calculation.MOVING_AVERAGE_7_PERIOD.value
251
255
  ]
252
256
 
253
257
  start_date = serializers.DateField(required=False)
254
258
  end_date = serializers.DateField(required=False)
255
259
  granularity = serializers.CharField(required=False)
256
260
  calculation = serializers.CharField(required=False)
261
+ response_type = serializers.CharField(required=False)
257
262
 
258
263
  def validate(self, attrs):
259
264
  """
@@ -270,6 +275,17 @@ class AdvanceAnalyticsQueryParamSerializer(serializers.Serializer): # pylint: d
270
275
 
271
276
  return attrs
272
277
 
278
+ def validate_response_type(self, value):
279
+ """
280
+ Validate the response_type value.
281
+
282
+ Raises:
283
+ serializers.ValidationError: If response_type is not one of the valid choices in `RESPONSE_TYPES`.
284
+ """
285
+ if value not in self.RESPONSE_TYPES:
286
+ raise serializers.ValidationError(f"response_type must be one of {self.RESPONSE_TYPES}")
287
+ return value
288
+
273
289
  def validate_granularity(self, value):
274
290
  """
275
291
  Validate the granularity value.
@@ -293,32 +309,25 @@ class AdvanceAnalyticsQueryParamSerializer(serializers.Serializer): # pylint: d
293
309
  return value
294
310
 
295
311
 
296
- class AdvanceAnalyticsEnrollmentSerializer(AdvanceAnalyticsQueryParamSerializer): # pylint: disable=abstract-method
297
- """Serializer for validating Advance Analytics Enrollments API"""
298
- CSV_TYPES = [
299
- ENROLLMENT_CSV.INDIVIDUAL_ENROLLMENTS.value
312
+ class AdvanceAnalyticsEnrollmentStatsSerializer(
313
+ AdvanceAnalyticsQueryParamSerializer
314
+ ): # pylint: disable=abstract-method
315
+ """Serializer for validating Advance Analytics Enrollments Stats API"""
316
+ CHART_TYPES = [
317
+ EnrollmentChart.ENROLLMENTS_OVER_TIME.value,
318
+ EnrollmentChart.TOP_COURSES_BY_ENROLLMENTS.value,
319
+ EnrollmentChart.TOP_SUBJECTS_BY_ENROLLMENTS.value
300
320
  ]
301
321
 
302
- csv_type = serializers.CharField(required=False)
322
+ chart_type = serializers.CharField(required=False)
303
323
 
304
- def validate_csv_type(self, value):
324
+ def validate_chart_type(self, value):
305
325
  """
306
- Validate the csv_type value.
326
+ Validate the chart_type value.
307
327
 
308
328
  Raises:
309
- serializers.ValidationError: If csv_type is not one of the valid choices
329
+ serializers.ValidationError: If chart_type is not one of the valid choices
310
330
  """
311
- if value not in self.CSV_TYPES:
312
- raise serializers.ValidationError(f"csv_type must be one of {self.CSV_TYPES}")
331
+ if value not in self.CHART_TYPES:
332
+ raise serializers.ValidationError(f"chart_type must be one of {self.CHART_TYPES}")
313
333
  return value
314
-
315
-
316
- class AdvanceAnalyticsEnrollmentStatsSerializer(
317
- AdvanceAnalyticsEnrollmentSerializer
318
- ): # pylint: disable=abstract-method
319
- """Serializer for validating Advance Analytics Enrollments Stats API"""
320
- CSV_TYPES = [
321
- ENROLLMENT_CSV.ENROLLMENTS_OVER_TIME.value,
322
- ENROLLMENT_CSV.TOP_COURSES_BY_ENROLLMENTS.value,
323
- ENROLLMENT_CSV.TOP_SUBJECTS_BY_ENROLLMENTS.value
324
- ]
@@ -15,6 +15,7 @@ from enterprise_data.api.v1.views.analytics_enrollments import (
15
15
  AdvanceAnalyticsEnrollmentStatsView,
16
16
  AdvanceAnalyticsIndividualEnrollmentsView,
17
17
  )
18
+ from enterprise_data.api.v1.views.analytics_leaderboard import AdvanceAnalyticsLeaderboardView
18
19
  from enterprise_data.constants import UUID4_REGEX
19
20
 
20
21
  app_name = 'enterprise_data_api_v1'
@@ -53,32 +54,37 @@ urlpatterns = [
53
54
  name='enterprise-admin-insights'
54
55
  ),
55
56
  re_path(
56
- fr'^admin/anlaytics/(?P<enterprise_id>{UUID4_REGEX})$',
57
+ fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})$',
57
58
  enterprise_admin_views.EnterpriseAdminAnalyticsAggregatesView.as_view(),
58
59
  name='enterprise-admin-analytics-aggregates'
59
60
  ),
60
61
  re_path(
61
- fr'^admin/anlaytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments/stats$',
62
+ fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/leaderboard$',
63
+ AdvanceAnalyticsLeaderboardView.as_view(),
64
+ name='enterprise-admin-analytics-leaderboard'
65
+ ),
66
+ re_path(
67
+ fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments/stats$',
62
68
  AdvanceAnalyticsEnrollmentStatsView.as_view(),
63
69
  name='enterprise-admin-analytics-enrollments-stats'
64
70
  ),
65
71
  re_path(
66
- fr'^admin/anlaytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments$',
72
+ fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments$',
67
73
  AdvanceAnalyticsIndividualEnrollmentsView.as_view(),
68
74
  name='enterprise-admin-analytics-enrollments'
69
75
  ),
70
76
  re_path(
71
- fr'^admin/anlaytics/(?P<enterprise_id>{UUID4_REGEX})/skills/stats',
77
+ fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})/skills/stats',
72
78
  enterprise_admin_views.EnterpriseAdminAnalyticsSkillsView.as_view(),
73
79
  name='enterprise-admin-analytics-skills'
74
80
  ),
75
81
  re_path(
76
- fr'^admin/anlaytics/(?P<enterprise_id>{UUID4_REGEX})/completions/stats$',
82
+ fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})/completions/stats$',
77
83
  enterprise_completions_views.EnterrpiseAdminCompletionsStatsView.as_view(),
78
84
  name='enterprise-admin-analytics-completions-stats'
79
85
  ),
80
86
  re_path(
81
- fr'^admin/anlaytics/(?P<enterprise_id>{UUID4_REGEX})/completions$',
87
+ fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})/completions$',
82
88
  enterprise_completions_views.EnterrpiseAdminCompletionsView.as_view(),
83
89
  name='enterprise-admin-analytics-completions'
84
90
  ),
@@ -1,5 +1,5 @@
1
1
  """Advance Analytics for Enrollments"""
2
- from datetime import datetime, timedelta
2
+ from datetime import datetime
3
3
 
4
4
  from edx_rbac.decorators import permission_required
5
5
  from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
@@ -8,37 +8,22 @@ from rest_framework.views import APIView
8
8
 
9
9
  from django.http import HttpResponse, StreamingHttpResponse
10
10
 
11
- from enterprise_data.admin_analytics.constants import CALCULATION, ENROLLMENT_CSV, GRANULARITY
12
- from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_datetime
11
+ from enterprise_data.admin_analytics.constants import Calculation, EnrollmentChart, Granularity, ResponseType
13
12
  from enterprise_data.admin_analytics.utils import (
14
13
  calculation_aggregation,
15
14
  fetch_and_cache_enrollments_data,
15
+ fetch_enrollments_cache_expiry_timestamp,
16
16
  granularity_aggregation,
17
17
  )
18
18
  from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
19
19
  from enterprise_data.api.v1.serializers import (
20
- AdvanceAnalyticsEnrollmentSerializer,
21
20
  AdvanceAnalyticsEnrollmentStatsSerializer,
21
+ AdvanceAnalyticsQueryParamSerializer,
22
22
  )
23
23
  from enterprise_data.renderers import IndividualEnrollmentsCSVRenderer
24
24
  from enterprise_data.utils import date_filter
25
25
 
26
26
 
27
- def fetch_enrollments_cache_expiry_timestamp():
28
- """Calculate cache expiry timestamp"""
29
- # TODO: Implement correct cache expiry logic for `enrollments` data.
30
- # Current cache expiry logic is based on `enterprise_learner_enrollment` table,
31
- # Which has nothing to do with the `enrollments` data. Instead cache expiry should
32
- # be based on `fact_enrollment_admin_dash` table. Currently we have no timestamp in
33
- # `fact_enrollment_admin_dash` table that can be used for cache expiry. Add a new
34
- # column in the table for this purpose and then use that column for cache expiry.
35
- last_updated_at = fetch_max_enrollment_datetime()
36
- cache_expiry = (
37
- last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
38
- )
39
- return cache_expiry
40
-
41
-
42
27
  class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
43
28
  """
44
29
  API for getting the advance analytics individual enrollments data.
@@ -50,7 +35,7 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
50
35
  @permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
51
36
  def get(self, request, enterprise_uuid):
52
37
  """Get individual enrollments data"""
53
- serializer = AdvanceAnalyticsEnrollmentSerializer(data=request.GET)
38
+ serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
54
39
  serializer.is_valid(raise_exception=True)
55
40
 
56
41
  cache_expiry = fetch_enrollments_cache_expiry_timestamp()
@@ -59,7 +44,7 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
59
44
  # get values from query params or use default values
60
45
  start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
61
46
  end_date = serializer.data.get('end_date', datetime.now())
62
- csv_type = request.query_params.get('csv_type')
47
+ response_type = request.query_params.get('response_type', ResponseType.JSON.value)
63
48
 
64
49
  # filter enrollments by date
65
50
  enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
@@ -77,11 +62,12 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
77
62
  enrollments["enterprise_enrollment_date"] = enrollments["enterprise_enrollment_date"].dt.date
78
63
  enrollments = enrollments.sort_values(by="enterprise_enrollment_date", ascending=False).reset_index(drop=True)
79
64
 
80
- if csv_type == ENROLLMENT_CSV.INDIVIDUAL_ENROLLMENTS.value:
65
+ if response_type == ResponseType.CSV.value:
66
+ filename = f"""individual_enrollments, {start_date} - {end_date}.csv"""
81
67
  return StreamingHttpResponse(
82
68
  IndividualEnrollmentsCSVRenderer().render(self._stream_serialized_data(enrollments)),
83
69
  content_type="text/csv",
84
- headers={"Content-Disposition": 'attachment; filename="individual_enrollments.csv"'},
70
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'},
85
71
  )
86
72
 
87
73
  paginator = self.pagination_class()
@@ -121,11 +107,12 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
121
107
  # get values from query params or use default
122
108
  start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
123
109
  end_date = serializer.data.get('end_date', datetime.now())
124
- granularity = serializer.data.get('granularity', GRANULARITY.DAILY.value)
125
- calculation = serializer.data.get('calculation', CALCULATION.TOTAL.value)
126
- csv_type = serializer.data.get('csv_type')
110
+ granularity = serializer.data.get('granularity', Granularity.DAILY.value)
111
+ calculation = serializer.data.get('calculation', Calculation.TOTAL.value)
112
+ response_type = serializer.data.get('response_type', ResponseType.JSON.value)
113
+ chart_type = serializer.data.get('chart_type')
127
114
 
128
- if csv_type is None:
115
+ if response_type == ResponseType.JSON.value:
129
116
  data = {
130
117
  "enrollments_over_time": self.construct_enrollments_over_time(
131
118
  enrollments_df.copy(),
@@ -146,26 +133,28 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
146
133
  ),
147
134
  }
148
135
  return Response(data)
149
- elif csv_type == ENROLLMENT_CSV.ENROLLMENTS_OVER_TIME.value:
150
- return self.construct_enrollments_over_time_csv(
151
- enrollments_df.copy(),
152
- start_date,
153
- end_date,
154
- granularity,
155
- calculation,
156
- )
157
- elif csv_type == ENROLLMENT_CSV.TOP_COURSES_BY_ENROLLMENTS.value:
158
- return self.construct_top_courses_by_enrollments_csv(
159
- enrollments_df.copy(),
160
- start_date,
161
- end_date,
162
- )
163
- elif csv_type == ENROLLMENT_CSV.TOP_SUBJECTS_BY_ENROLLMENTS.value:
164
- return self.construct_top_subjects_by_enrollments_csv(
165
- enrollments_df.copy(),
166
- start_date,
167
- end_date,
168
- )
136
+
137
+ if response_type == ResponseType.CSV.value:
138
+ if chart_type == EnrollmentChart.ENROLLMENTS_OVER_TIME.value:
139
+ return self.construct_enrollments_over_time_csv(
140
+ enrollments_df.copy(),
141
+ start_date,
142
+ end_date,
143
+ granularity,
144
+ calculation,
145
+ )
146
+ elif chart_type == EnrollmentChart.TOP_COURSES_BY_ENROLLMENTS.value:
147
+ return self.construct_top_courses_by_enrollments_csv(
148
+ enrollments_df.copy(),
149
+ start_date,
150
+ end_date,
151
+ )
152
+ elif chart_type == EnrollmentChart.TOP_SUBJECTS_BY_ENROLLMENTS.value:
153
+ return self.construct_top_subjects_by_enrollments_csv(
154
+ enrollments_df.copy(),
155
+ start_date,
156
+ end_date,
157
+ )
169
158
 
170
159
  def enrollments_over_time_common(self, enrollments_df, start_date, end_date, granularity, calculation):
171
160
  """
@@ -175,8 +164,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
175
164
  enrollments_df {DataFrame} -- DataFrame of enrollments
176
165
  start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
177
166
  end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
178
- granularity {str} -- Granularity of the data. One of GRANULARITY choices
179
- calculation {str} -- Calculation of the data. One of CALCULATION choices
167
+ granularity {str} -- Granularity of the data. One of Granularity choices
168
+ calculation {str} -- Calculation of the data. One of Calculation choices
180
169
  """
181
170
  # filter enrollments by date
182
171
  enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
@@ -202,8 +191,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
202
191
  enrollments_df {DataFrame} -- DataFrame of enrollments
203
192
  start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
204
193
  end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
205
- granularity {str} -- Granularity of the data. One of GRANULARITY choices
206
- calculation {str} -- Calculation of the data. One of CALCULATION choices
194
+ granularity {str} -- Granularity of the data. One of Granularity choices
195
+ calculation {str} -- Calculation of the data. One of Calculation choices
207
196
  """
208
197
  enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
209
198
 
@@ -218,8 +207,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
218
207
  enrollments_df {DataFrame} -- DataFrame of enrollments
219
208
  start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
220
209
  end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
221
- granularity {str} -- Granularity of the data. One of GRANULARITY choices
222
- calculation {str} -- Calculation of the data. One of CALCULATION choices
210
+ granularity {str} -- Granularity of the data. One of Granularity choices
211
+ calculation {str} -- Calculation of the data. One of Calculation choices
223
212
  """
224
213
  enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
225
214
 
@@ -0,0 +1,120 @@
1
+ """Advance Analytics for Leaderboard"""
2
+ from datetime import datetime
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from edx_rbac.decorators import permission_required
7
+ from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
8
+ from rest_framework.views import APIView
9
+
10
+ from django.http import StreamingHttpResponse
11
+
12
+ from enterprise_data.admin_analytics.constants import ResponseType
13
+ from enterprise_data.admin_analytics.utils import (
14
+ fetch_and_cache_engagements_data,
15
+ fetch_and_cache_enrollments_data,
16
+ fetch_engagements_cache_expiry_timestamp,
17
+ fetch_enrollments_cache_expiry_timestamp,
18
+ )
19
+ from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
20
+ from enterprise_data.api.v1.serializers import AdvanceAnalyticsQueryParamSerializer
21
+ from enterprise_data.renderers import LeaderboardCSVRenderer
22
+ from enterprise_data.utils import date_filter
23
+
24
+
25
+ class AdvanceAnalyticsLeaderboardView(APIView):
26
+ """
27
+ API for getting the advance analytics leaderboard data.
28
+ """
29
+ authentication_classes = (JwtAuthentication,)
30
+ pagination_class = AdvanceAnalyticsPagination
31
+ http_method_names = ['get']
32
+
33
+ @permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
34
+ def get(self, request, enterprise_uuid):
35
+ """Get leaderboard data"""
36
+ serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
37
+ serializer.is_valid(raise_exception=True)
38
+
39
+ enrollments_cache_expiry = fetch_enrollments_cache_expiry_timestamp()
40
+ enrollments_df = fetch_and_cache_enrollments_data(enterprise_uuid, enrollments_cache_expiry)
41
+
42
+ engagements_cache_expiry = fetch_engagements_cache_expiry_timestamp()
43
+ engagements_df = fetch_and_cache_engagements_data(enterprise_uuid, engagements_cache_expiry)
44
+
45
+ start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
46
+ end_date = serializer.data.get('end_date', datetime.now())
47
+ response_type = serializer.data.get('response_type', ResponseType.JSON.value)
48
+
49
+ # only include learners who have passed the course
50
+ enrollments_df = enrollments_df[enrollments_df["has_passed"] == 1]
51
+
52
+ # filter enrollments by date
53
+ enrollments_df = date_filter(start_date, end_date, enrollments_df, "passed_date")
54
+
55
+ completions = enrollments_df.groupby(["email"]).size().reset_index()
56
+ completions.columns = ["email", "course_completions"]
57
+
58
+ # filter engagements by date
59
+ engagements_df = date_filter(start_date, end_date, engagements_df, "activity_date")
60
+
61
+ engage = (
62
+ engagements_df.groupby(["email"])
63
+ .agg({"is_engaged": ["sum"], "learning_time_seconds": ["sum"]})
64
+ .reset_index()
65
+ )
66
+ engage.columns = ["email", "daily_sessions", "learning_time_seconds"]
67
+ engage["learning_time_hours"] = round(
68
+ engage["learning_time_seconds"].astype("float") / 60 / 60, 1
69
+ )
70
+ engage["average_session_length"] = round(
71
+ engage["learning_time_hours"] / engage["daily_sessions"].astype("float"), 1
72
+ )
73
+
74
+ leaderboard_df = engage.merge(completions, on="email", how="left")
75
+ leaderboard_df = leaderboard_df.sort_values(
76
+ by=["learning_time_hours", "daily_sessions", "course_completions"],
77
+ ascending=[False, False, False],
78
+ )
79
+
80
+ # move the aggregated row with email 'null' to the end of the table
81
+ idx = leaderboard_df.index[leaderboard_df['email'] == 'null']
82
+ leaderboard_df.loc[idx, 'email'] = 'learners who have not shared consent'
83
+ leaderboard_df = pd.concat([leaderboard_df.drop(idx), leaderboard_df.loc[idx]])
84
+
85
+ # convert `nan` values to `None` because `nan` is not JSON serializable
86
+ leaderboard_df = leaderboard_df.replace(np.nan, None)
87
+
88
+ if response_type == ResponseType.CSV.value:
89
+ filename = f"""Leaderboard, {start_date} - {end_date}.csv"""
90
+ leaderboard_df = leaderboard_df[
91
+ [
92
+ "email",
93
+ "learning_time_hours",
94
+ "daily_sessions",
95
+ "average_session_length",
96
+ "course_completions",
97
+ ]
98
+ ]
99
+ return StreamingHttpResponse(
100
+ LeaderboardCSVRenderer().render(self._stream_serialized_data(leaderboard_df)),
101
+ content_type="text/csv",
102
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'},
103
+ )
104
+
105
+ paginator = self.pagination_class()
106
+ page = paginator.paginate_queryset(leaderboard_df, request)
107
+ serialized_data = page.data.to_dict(orient='records')
108
+ response = paginator.get_paginated_response(serialized_data)
109
+
110
+ return response
111
+
112
+ def _stream_serialized_data(self, leaderboard_df, chunk_size=50000):
113
+ """
114
+ Stream the serialized data.
115
+ """
116
+ total_rows = leaderboard_df.shape[0]
117
+ for start_index in range(0, total_rows, chunk_size):
118
+ end_index = min(start_index + chunk_size, total_rows)
119
+ chunk = leaderboard_df.iloc[start_index:end_index]
120
+ yield from chunk.to_dict(orient='records')