edx-enterprise-data 8.7.0__tar.gz → 8.8.1__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.1}/CHANGELOG.rst +9 -0
  2. {edx_enterprise_data-8.7.0/edx_enterprise_data.egg-info → edx_enterprise_data-8.8.1}/PKG-INFO +1 -1
  3. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1/edx_enterprise_data.egg-info}/PKG-INFO +1 -1
  4. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/edx_enterprise_data.egg-info/SOURCES.txt +3 -0
  5. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/__init__.py +1 -1
  6. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/admin_analytics/constants.py +9 -3
  7. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/admin_analytics/data_loaders.py +7 -3
  8. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/admin_analytics/utils.py +52 -10
  9. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/paginators.py +1 -1
  10. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/serializers.py +39 -30
  11. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/urls.py +12 -6
  12. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/views/analytics_enrollments.py +85 -73
  13. edx_enterprise_data-8.8.1/enterprise_data/api/v1/views/analytics_leaderboard.py +141 -0
  14. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/views/enterprise_admin.py +20 -19
  15. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/views/enterprise_completions.py +49 -28
  16. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/renderers.py +14 -0
  17. edx_enterprise_data-8.8.1/enterprise_data/tests/admin_analytics/mock_analytics_data.py +511 -0
  18. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/admin_analytics/mock_enrollments.py +4 -4
  19. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +23 -22
  20. edx_enterprise_data-8.8.1/enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py +163 -0
  21. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/admin_analytics/test_enterprise_completions.py +1 -1
  22. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/utils.py +16 -0
  23. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/LICENSE +0 -0
  24. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/MANIFEST.in +0 -0
  25. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/README.md +0 -0
  26. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/edx_enterprise_data.egg-info/dependency_links.txt +0 -0
  27. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/edx_enterprise_data.egg-info/not-zip-safe +0 -0
  28. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/edx_enterprise_data.egg-info/requires.txt +0 -0
  29. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/edx_enterprise_data.egg-info/top_level.txt +0 -0
  30. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/admin_analytics/__init__.py +0 -0
  31. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/admin_analytics/completions_utils.py +0 -0
  32. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/admin_analytics/database.py +0 -0
  33. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/__init__.py +0 -0
  34. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/urls.py +0 -0
  35. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v0/__init__.py +0 -0
  36. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v0/serializers.py +0 -0
  37. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v0/urls.py +0 -0
  38. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v0/views.py +0 -0
  39. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/__init__.py +0 -0
  40. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/views/__init__.py +0 -0
  41. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/views/base.py +0 -0
  42. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/views/enterprise_learner.py +0 -0
  43. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/views/enterprise_offers.py +0 -0
  44. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/apps.py +0 -0
  45. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/clients.py +0 -0
  46. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/constants.py +0 -0
  47. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/filters.py +0 -0
  48. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/fixtures/enterprise_enrollment.json +0 -0
  49. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/fixtures/enterprise_user.json +0 -0
  50. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/__init__.py +0 -0
  51. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/__init__.py +0 -0
  52. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/create_dummy_data.py +0 -0
  53. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/create_dummy_data_lpr_v1.py +0 -0
  54. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/create_enterprise_enrollment.py +0 -0
  55. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/create_enterprise_learner_enrollment_lpr_v1.py +0 -0
  56. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/create_enterprise_learner_lpr_v1.py +0 -0
  57. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/create_enterprise_offer.py +0 -0
  58. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/create_enterprise_user.py +0 -0
  59. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/tests/__init__.py +0 -0
  60. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/tests/test_create_dummy_data_lpr_v1.py +0 -0
  61. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/tests/test_create_enterprise_enrollment.py +0 -0
  62. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/tests/test_create_enterprise_learner_enrollment_lpr_v1.py +0 -0
  63. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/tests/test_create_enterprise_learner_lpr_v1.py +0 -0
  64. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/tests/test_create_enterprise_user.py +0 -0
  65. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0001_initial.py +0 -0
  66. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0002_auto_20180430_1358.py +0 -0
  67. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0003_auto_20180501_0603.py +0 -0
  68. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0004_auto_20180501_0928.py +0 -0
  69. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0004_auto_20180508_1652.py +0 -0
  70. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0005_auto_20180524_2204.py +0 -0
  71. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0006_auto_20180612_0336.py +0 -0
  72. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0007_auto_20180612_0534.py +0 -0
  73. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0008_auto_20180614_0108.py +0 -0
  74. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0009_auto_20180628_1152.py +0 -0
  75. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0010_enterpriseenrollment_created.py +0 -0
  76. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0011_enterpriseuser.py +0 -0
  77. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0012_auto_20180831_1930.py +0 -0
  78. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0013_auto_20180831_1931.py +0 -0
  79. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0014_enterpriseuser_created.py +0 -0
  80. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0015_auto_20180907_1757.py +0 -0
  81. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0016_auto_20180924_2138.py +0 -0
  82. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0017_enterpriseenrollment_unenrollment_timestamp.py +0 -0
  83. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0018_enterprisedatafeaturerole_enterprisedataroleassignment.py +0 -0
  84. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0019_add_enterprise_data_feature_roles.py +0 -0
  85. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0020_add_role_based_access_control_switch.py +0 -0
  86. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0021_auto_20190329_1241.py +0 -0
  87. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0022_remove_role_based_access_control_switch.py +0 -0
  88. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0023_enterpriselearner_enterpriselearnerenrollment.py +0 -0
  89. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0024_auto_20210602_1811.py +0 -0
  90. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0025_auto_20210703_1854.py +0 -0
  91. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0026_auto_20210916_0414.py +0 -0
  92. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0027_enterpriselearnerenrollment_total_learning_time_seconds.py +0 -0
  93. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0028_enterpriselearnerenrollment_offer_id.py +0 -0
  94. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0029_enterpriseoffer.py +0 -0
  95. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0030_auto_20230609_1353.py +0 -0
  96. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0031_auto_20230615_0705.py +0 -0
  97. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0032_auto_20230704_0818.py +0 -0
  98. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0033_enterpriseadminlearnerprogress_enterpriseadminsummarizeinsights.py +0 -0
  99. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0034_auto_20230907_0834.py +0 -0
  100. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0035_auto_20230907_1154.py +0 -0
  101. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0036_enterprisesubsidybudget_subsidy_access_policy_display_name.py +0 -0
  102. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0037_alter_enterpriseenrollment_consent_granted.py +0 -0
  103. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0038_enterpriseoffer_export_timestamp.py +0 -0
  104. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0039_auto_20240212_1403.py +0 -0
  105. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0040_auto_20240718_0536_squashed_0043_alter_enterpriselearnerenrollment_enterprise_enrollment_id.py +0 -0
  106. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0044_enterpriseexecedlcmoduleperformance.py +0 -0
  107. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/__init__.py +0 -0
  108. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/models.py +0 -0
  109. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/paginators.py +0 -0
  110. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/settings/__init__.py +0 -0
  111. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/settings/test.py +0 -0
  112. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/signals.py +0 -0
  113. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/__init__.py +0 -0
  114. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/admin_analytics/__init__.py +0 -0
  115. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/admin_analytics/test_data_loaders.py +0 -0
  116. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/admin_analytics/test_utils.py +0 -0
  117. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/__init__.py +0 -0
  118. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/v0/__init__.py +0 -0
  119. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/v0/test_serializers.py +0 -0
  120. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/v1/__init__.py +0 -0
  121. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/v1/test_serializers.py +0 -0
  122. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/v1/test_views.py +0 -0
  123. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/v1/views/__init__.py +0 -0
  124. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/v1/views/test_enterprise_admin.py +0 -0
  125. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/factories.py +0 -0
  126. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/mixins.py +0 -0
  127. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/test_clients.py +0 -0
  128. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/test_filters.py +0 -0
  129. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/test_models.py +0 -0
  130. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/test_utils.py +0 -0
  131. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/test_views.py +0 -0
  132. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/urls.py +0 -0
  133. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/__init__.py +0 -0
  134. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/admin.py +0 -0
  135. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/apps.py +0 -0
  136. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/constants.py +0 -0
  137. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/migrations/0001_initial.py +0 -0
  138. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/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.1}/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.1}/enterprise_data_roles/migrations/0004_enterprisedataroleassignment_enterprise_id.py +0 -0
  141. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/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.1}/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.1}/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.1}/enterprise_data_roles/migrations/__init__.py +0 -0
  145. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/models.py +0 -0
  146. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/rules.py +0 -0
  147. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/tests/__init__.py +0 -0
  148. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/tests/factories.py +0 -0
  149. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/tests/test_models.py +0 -0
  150. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/__init__.py +0 -0
  151. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/clients/__init__.py +0 -0
  152. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/clients/enterprise.py +0 -0
  153. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/clients/s3.py +0 -0
  154. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/clients/snowflake.py +0 -0
  155. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/clients/vertica.py +0 -0
  156. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/delivery_method.py +0 -0
  157. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/external_resource_link_report.py +0 -0
  158. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/fixtures/__init__.py +0 -0
  159. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/fixtures/enterprise_customer_reporting.json +0 -0
  160. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/reporter.py +0 -0
  161. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/send_enterprise_reports.py +0 -0
  162. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/__init__.py +0 -0
  163. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_clients.py +0 -0
  164. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_delivery_method.py +0 -0
  165. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_enterprise_client.py +0 -0
  166. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_external_link_report.py +0 -0
  167. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_reporter.py +0 -0
  168. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_send_enterprise_reports.py +0 -0
  169. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_utils.py +0 -0
  170. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_vertica_client.py +0 -0
  171. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/utils.py +0 -0
  172. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/utils.py +0 -0
  173. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/base.in +0 -0
  174. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/base.txt +0 -0
  175. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/ci.txt +0 -0
  176. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/common_constraints.txt +0 -0
  177. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/constraints.txt +0 -0
  178. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/dev.txt +0 -0
  179. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/django.txt +0 -0
  180. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/pip.txt +0 -0
  181. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/pip_tools.txt +0 -0
  182. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/quality.txt +0 -0
  183. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/reporting.in +0 -0
  184. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/test-master.txt +0 -0
  185. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/test-reporting.txt +0 -0
  186. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/test.txt +0 -0
  187. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/setup.cfg +0 -0
  188. {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/setup.py +0 -0
@@ -16,6 +16,15 @@ Unreleased
16
16
 
17
17
  =========================
18
18
 
19
+ [8.8.1] - 2024-08-16
20
+ ---------------------
21
+ * refactor: Add logs and time measurements for different code blocks
22
+
23
+ [8.8.0] - 2024-08-15
24
+ ---------------------
25
+ * feat: Add API endpoints for advance analytics leaderboard data
26
+ * refactor: Use `response_type` and `chart_type` in advance analytics enrollments API endpoints
27
+
19
28
  [8.7.0] - 2024-08-13
20
29
  ---------------------
21
30
  * 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.1
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.1
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.1"
@@ -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'
@@ -7,6 +7,7 @@ import pandas
7
7
  from django.http import Http404
8
8
 
9
9
  from enterprise_data.admin_analytics.database import run_query
10
+ from enterprise_data.utils import timer
10
11
 
11
12
 
12
13
  def get_select_query(table: str, columns: list, enterprise_uuid: str) -> str:
@@ -65,7 +66,8 @@ def fetch_enrollment_data(enterprise_uuid: str):
65
66
  enterprise_uuid=enterprise_uuid,
66
67
  )
67
68
 
68
- results = run_query(query=query)
69
+ with timer('fetch_enrollment_data'):
70
+ results = run_query(query=query)
69
71
  if not results:
70
72
  raise Http404(f'No enrollment data found for enterprise {enterprise_uuid}')
71
73
 
@@ -113,7 +115,8 @@ def fetch_engagement_data(enterprise_uuid: str):
113
115
  table='fact_enrollment_engagement_day_admin_dash', columns=columns, enterprise_uuid=enterprise_uuid
114
116
  )
115
117
 
116
- results = run_query(query=query)
118
+ with timer('fetch_engagement_data'):
119
+ results = run_query(query=query)
117
120
  if not results:
118
121
  raise Http404(f'No engagement data found for enterprise {enterprise_uuid}')
119
122
 
@@ -171,7 +174,8 @@ def fetch_skills_data(enterprise_uuid: str):
171
174
  table='skills_daily_rollup_admin_dash', columns=cols, enterprise_uuid=enterprise_uuid
172
175
  )
173
176
 
174
- skills = run_query(query=query)
177
+ with timer('fetch_skills_data'):
178
+ skills = run_query(query=query)
175
179
 
176
180
  if not skills:
177
181
  raise Http404(f'No skills data found for enterprise {enterprise_uuid}')
@@ -1,15 +1,23 @@
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
+ from logging import getLogger
6
7
 
7
8
  from edx_django_utils.cache import TieredCache, get_cache_key
8
9
 
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
10
+ from enterprise_data.admin_analytics.constants import Calculation, Granularity
11
+ from enterprise_data.admin_analytics.data_loaders import (
12
+ fetch_engagement_data,
13
+ fetch_enrollment_data,
14
+ fetch_max_enrollment_datetime,
15
+ fetch_skills_data,
16
+ )
11
17
  from enterprise_data.utils import date_filter, primary_subject_truncate
12
18
 
19
+ LOGGER = getLogger(__name__)
20
+
13
21
 
14
22
  class ChartType(Enum):
15
23
  """
@@ -23,14 +31,45 @@ class ChartType(Enum):
23
31
  TOP_SUBJECTS_BY_COMPLETIONS = 'top_subjects_by_completions'
24
32
 
25
33
 
34
+ def fetch_enrollments_cache_expiry_timestamp():
35
+ """Calculate cache expiry timestamp"""
36
+ # TODO: Implement correct cache expiry logic for `enrollments` data.
37
+ # Current cache expiry logic is based on `enterprise_learner_enrollment` table,
38
+ # Which has nothing to do with the `enrollments` data. Instead cache expiry should
39
+ # be based on `fact_enrollment_admin_dash` table. Currently we have no timestamp in
40
+ # `fact_enrollment_admin_dash` table that can be used for cache expiry. Add a new
41
+ # column in the table for this purpose and then use that column for cache expiry.
42
+ last_updated_at = fetch_max_enrollment_datetime()
43
+ cache_expiry = (
44
+ last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
45
+ )
46
+ return cache_expiry
47
+
48
+
49
+ def fetch_engagements_cache_expiry_timestamp():
50
+ """Calculate cache expiry timestamp"""
51
+ # TODO: Implement correct cache expiry logic for `engagements` data.
52
+ # Current cache expiry logic is based on `enterprise_learner_enrollment` table,
53
+ # Which has nothing to do with the `engagements` data. Instead cache expiry should
54
+ # be based on `fact_enrollment_engagement_day_admin_dash` table. Currently we have
55
+ # no timestamp in `fact_enrollment_engagement_day_admin_dash` table that can be used
56
+ # for cache expiry. Add a new column in the table for this purpose and then use that
57
+ # column for cache expiry.
58
+ last_updated_at = fetch_max_enrollment_datetime()
59
+ cache_expiry = (
60
+ last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
61
+ )
62
+ return cache_expiry
63
+
64
+
26
65
  def granularity_aggregation(level, group, date, data_frame, aggregation_type="count"):
27
66
  """Aggregate data based on granularity"""
28
67
  df = data_frame
29
68
 
30
69
  period_mapping = {
31
- GRANULARITY.WEEKLY.value: "W",
32
- GRANULARITY.MONTHLY.value: "M",
33
- GRANULARITY.QUARTERLY.value: "Q"
70
+ Granularity.WEEKLY.value: "W",
71
+ Granularity.MONTHLY.value: "M",
72
+ Granularity.QUARTERLY.value: "Q"
34
73
  }
35
74
 
36
75
  if level in period_mapping:
@@ -52,15 +91,15 @@ def calculation_aggregation(calc, data_frame, aggregation_type="count"):
52
91
  df = data_frame
53
92
 
54
93
  window_mapping = {
55
- CALCULATION.MOVING_AVERAGE_3_PERIOD.value: 3,
56
- CALCULATION.MOVING_AVERAGE_7_PERIOD.value: 7,
94
+ Calculation.MOVING_AVERAGE_3_PERIOD.value: 3,
95
+ Calculation.MOVING_AVERAGE_7_PERIOD.value: 7,
57
96
  }
58
97
 
59
98
  aggregation_column = "count" if aggregation_type == "count" else "sum"
60
99
 
61
- if calc == CALCULATION.RUNNING_TOTAL.value:
100
+ if calc == Calculation.RUNNING_TOTAL.value:
62
101
  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]:
102
+ elif calc in [Calculation.MOVING_AVERAGE_3_PERIOD.value, Calculation.MOVING_AVERAGE_7_PERIOD.value]:
64
103
  df[aggregation_column] = (
65
104
  df.groupby("enroll_type")[aggregation_column]
66
105
  .rolling(window_mapping[calc])
@@ -108,6 +147,7 @@ def fetch_and_cache_enrollments_data(enterprise_id, cache_expiry):
108
147
  cached_response = TieredCache.get_cached_response(cache_key)
109
148
 
110
149
  if cached_response.is_found:
150
+ LOGGER.info(f"Enrollments data found in cache for Enterprise [{enterprise_id}]")
111
151
  return cached_response.value
112
152
  else:
113
153
  enrollments = fetch_enrollment_data(enterprise_id)
@@ -135,6 +175,7 @@ def fetch_and_cache_engagements_data(enterprise_id, cache_expiry):
135
175
  cached_response = TieredCache.get_cached_response(cache_key)
136
176
 
137
177
  if cached_response.is_found:
178
+ LOGGER.info(f"Engagements data found in cache for Enterprise [{enterprise_id}]")
138
179
  return cached_response.value
139
180
  else:
140
181
  engagements = fetch_engagement_data(enterprise_id)
@@ -162,6 +203,7 @@ def fetch_and_cache_skills_data(enterprise_id, cache_expiry):
162
203
  cached_response = TieredCache.get_cached_response(cache_key)
163
204
 
164
205
  if cached_response.is_found:
206
+ LOGGER.info(f"Skills data found in cache for Enterprise [{enterprise_id}]")
165
207
  return cached_response.value
166
208
  else:
167
209
  skills = fetch_skills_data(enterprise_id)
@@ -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,6 @@
1
1
  """Advance Analytics for Enrollments"""
2
- from datetime import datetime, timedelta
2
+ from datetime import datetime
3
+ from logging import getLogger
3
4
 
4
5
  from edx_rbac.decorators import permission_required
5
6
  from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
@@ -8,35 +9,22 @@ from rest_framework.views import APIView
8
9
 
9
10
  from django.http import HttpResponse, StreamingHttpResponse
10
11
 
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
12
+ from enterprise_data.admin_analytics.constants import Calculation, EnrollmentChart, Granularity, ResponseType
13
13
  from enterprise_data.admin_analytics.utils import (
14
14
  calculation_aggregation,
15
15
  fetch_and_cache_enrollments_data,
16
+ fetch_enrollments_cache_expiry_timestamp,
16
17
  granularity_aggregation,
17
18
  )
18
19
  from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
19
20
  from enterprise_data.api.v1.serializers import (
20
- AdvanceAnalyticsEnrollmentSerializer,
21
21
  AdvanceAnalyticsEnrollmentStatsSerializer,
22
+ AdvanceAnalyticsQueryParamSerializer,
22
23
  )
23
24
  from enterprise_data.renderers import IndividualEnrollmentsCSVRenderer
24
- from enterprise_data.utils import date_filter
25
+ from enterprise_data.utils import date_filter, timer
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
27
+ LOGGER = getLogger(__name__)
40
28
 
41
29
 
42
30
  class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
@@ -50,7 +38,7 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
50
38
  @permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
51
39
  def get(self, request, enterprise_uuid):
52
40
  """Get individual enrollments data"""
53
- serializer = AdvanceAnalyticsEnrollmentSerializer(data=request.GET)
41
+ serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
54
42
  serializer.is_valid(raise_exception=True)
55
43
 
56
44
  cache_expiry = fetch_enrollments_cache_expiry_timestamp()
@@ -59,7 +47,14 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
59
47
  # get values from query params or use default values
60
48
  start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
61
49
  end_date = serializer.data.get('end_date', datetime.now())
62
- csv_type = request.query_params.get('csv_type')
50
+ response_type = request.query_params.get('response_type', ResponseType.JSON.value)
51
+
52
+ LOGGER.info(
53
+ "Individual enrollments data requested for enterprise [%s] from [%s] to [%s]",
54
+ enterprise_uuid,
55
+ start_date,
56
+ end_date,
57
+ )
63
58
 
64
59
  # filter enrollments by date
65
60
  enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
@@ -77,11 +72,19 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
77
72
  enrollments["enterprise_enrollment_date"] = enrollments["enterprise_enrollment_date"].dt.date
78
73
  enrollments = enrollments.sort_values(by="enterprise_enrollment_date", ascending=False).reset_index(drop=True)
79
74
 
80
- if csv_type == ENROLLMENT_CSV.INDIVIDUAL_ENROLLMENTS.value:
75
+ LOGGER.info(
76
+ "Individual enrollments data prepared for enterprise [%s] from [%s] to [%s]",
77
+ enterprise_uuid,
78
+ start_date,
79
+ end_date,
80
+ )
81
+
82
+ if response_type == ResponseType.CSV.value:
83
+ filename = f"""individual_enrollments, {start_date} - {end_date}.csv"""
81
84
  return StreamingHttpResponse(
82
85
  IndividualEnrollmentsCSVRenderer().render(self._stream_serialized_data(enrollments)),
83
86
  content_type="text/csv",
84
- headers={"Content-Disposition": 'attachment; filename="individual_enrollments.csv"'},
87
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'},
85
88
  )
86
89
 
87
90
  paginator = self.pagination_class()
@@ -121,51 +124,60 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
121
124
  # get values from query params or use default
122
125
  start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
123
126
  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')
127
-
128
- if csv_type is None:
129
- data = {
130
- "enrollments_over_time": self.construct_enrollments_over_time(
131
- enrollments_df.copy(),
132
- start_date,
133
- end_date,
134
- granularity,
135
- calculation,
136
- ),
137
- "top_courses_by_enrollments": self.construct_top_courses_by_enrollments(
138
- enrollments_df.copy(),
139
- start_date,
140
- end_date,
141
- ),
142
- "top_subjects_by_enrollments": self.construct_top_subjects_by_enrollments(
143
- enrollments_df.copy(),
144
- start_date,
145
- end_date,
146
- ),
147
- }
127
+ granularity = serializer.data.get('granularity', Granularity.DAILY.value)
128
+ calculation = serializer.data.get('calculation', Calculation.TOTAL.value)
129
+ response_type = serializer.data.get('response_type', ResponseType.JSON.value)
130
+ chart_type = serializer.data.get('chart_type')
131
+
132
+ # TODO: Add validation that if response_type is CSV then chart_type must be provided
133
+
134
+ if response_type == ResponseType.JSON.value:
135
+ with timer('construct_enrollment_all_stats'):
136
+ data = {
137
+ "enrollments_over_time": self.construct_enrollments_over_time(
138
+ enrollments_df.copy(),
139
+ start_date,
140
+ end_date,
141
+ granularity,
142
+ calculation,
143
+ ),
144
+ "top_courses_by_enrollments": self.construct_top_courses_by_enrollments(
145
+ enrollments_df.copy(),
146
+ start_date,
147
+ end_date,
148
+ ),
149
+ "top_subjects_by_enrollments": self.construct_top_subjects_by_enrollments(
150
+ enrollments_df.copy(),
151
+ start_date,
152
+ end_date,
153
+ ),
154
+ }
148
155
  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
- )
156
+
157
+ if response_type == ResponseType.CSV.value:
158
+ if chart_type == EnrollmentChart.ENROLLMENTS_OVER_TIME.value:
159
+ with timer('construct_enrollments_over_time_csv'):
160
+ return self.construct_enrollments_over_time_csv(
161
+ enrollments_df.copy(),
162
+ start_date,
163
+ end_date,
164
+ granularity,
165
+ calculation,
166
+ )
167
+ elif chart_type == EnrollmentChart.TOP_COURSES_BY_ENROLLMENTS.value:
168
+ with timer('construct_top_courses_by_enrollments_csv'):
169
+ return self.construct_top_courses_by_enrollments_csv(
170
+ enrollments_df.copy(),
171
+ start_date,
172
+ end_date,
173
+ )
174
+ elif chart_type == EnrollmentChart.TOP_SUBJECTS_BY_ENROLLMENTS.value:
175
+ with timer('construct_top_subjects_by_enrollments_csv'):
176
+ return self.construct_top_subjects_by_enrollments_csv(
177
+ enrollments_df.copy(),
178
+ start_date,
179
+ end_date,
180
+ )
169
181
 
170
182
  def enrollments_over_time_common(self, enrollments_df, start_date, end_date, granularity, calculation):
171
183
  """
@@ -175,8 +187,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
175
187
  enrollments_df {DataFrame} -- DataFrame of enrollments
176
188
  start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
177
189
  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
190
+ granularity {str} -- Granularity of the data. One of Granularity choices
191
+ calculation {str} -- Calculation of the data. One of Calculation choices
180
192
  """
181
193
  # filter enrollments by date
182
194
  enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
@@ -202,8 +214,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
202
214
  enrollments_df {DataFrame} -- DataFrame of enrollments
203
215
  start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
204
216
  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
217
+ granularity {str} -- Granularity of the data. One of Granularity choices
218
+ calculation {str} -- Calculation of the data. One of Calculation choices
207
219
  """
208
220
  enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
209
221
 
@@ -218,8 +230,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
218
230
  enrollments_df {DataFrame} -- DataFrame of enrollments
219
231
  start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
220
232
  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
233
+ granularity {str} -- Granularity of the data. One of Granularity choices
234
+ calculation {str} -- Calculation of the data. One of Calculation choices
223
235
  """
224
236
  enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
225
237