edx-enterprise-data 10.9.8__tar.gz → 10.10.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 (207) hide show
  1. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/CHANGELOG.rst +47 -0
  2. {edx_enterprise_data-10.9.8/edx_enterprise_data.egg-info → edx_enterprise_data-10.10.1}/PKG-INFO +13 -5
  3. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1/edx_enterprise_data.egg-info}/PKG-INFO +13 -5
  4. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/edx_enterprise_data.egg-info/SOURCES.txt +3 -0
  5. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/edx_enterprise_data.egg-info/requires.txt +2 -2
  6. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/__init__.py +1 -1
  7. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/admin_analytics/data_loaders.py +1 -1
  8. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v1/serializers.py +65 -2
  9. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v1/views/enterprise_admin.py +1 -1
  10. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v1/views/enterprise_learner.py +47 -6
  11. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/clients.py +27 -0
  12. edx_enterprise_data-10.10.1/enterprise_data/migrations/0047_enterpriseexecedlcmoduleperformance_avg_after_lo_score_and_more.py +28 -0
  13. edx_enterprise_data-10.10.1/enterprise_data/migrations/0048_alter_enterpriseexecedlcmoduleperformance_avg_after_lo_score_and_more.py +23 -0
  14. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/models.py +3 -0
  15. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/renderers.py +1 -1
  16. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/utils.py +19 -0
  17. edx_enterprise_data-10.10.1/enterprise_reporting/constants.py +7 -0
  18. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/delivery_method.py +36 -7
  19. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/send_enterprise_reports.py +13 -3
  20. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/tests/test_delivery_method.py +46 -3
  21. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/tests/test_send_enterprise_reports.py +1 -1
  22. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/tests/test_utils.py +1 -0
  23. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/utils.py +2 -5
  24. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/requirements/base.in +2 -2
  25. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/requirements/base.txt +49 -50
  26. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/requirements/ci.txt +9 -9
  27. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/requirements/common_constraints.txt +7 -7
  28. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/requirements/constraints.txt +0 -15
  29. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/requirements/dev.txt +77 -86
  30. edx_enterprise_data-10.10.1/requirements/django.txt +1 -0
  31. edx_enterprise_data-10.10.1/requirements/pip.txt +16 -0
  32. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/requirements/pip_tools.txt +5 -5
  33. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/requirements/quality.txt +83 -92
  34. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/requirements/test-master.txt +54 -55
  35. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/requirements/test-reporting.txt +52 -46
  36. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/requirements/test.txt +55 -56
  37. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/setup.py +0 -1
  38. edx_enterprise_data-10.9.8/requirements/django.txt +0 -1
  39. edx_enterprise_data-10.9.8/requirements/pip.txt +0 -14
  40. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/LICENSE +0 -0
  41. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/MANIFEST.in +0 -0
  42. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/README.md +0 -0
  43. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/edx_enterprise_data.egg-info/dependency_links.txt +0 -0
  44. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/edx_enterprise_data.egg-info/not-zip-safe +0 -0
  45. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/edx_enterprise_data.egg-info/top_level.txt +0 -0
  46. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/admin_analytics/__init__.py +0 -0
  47. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/admin_analytics/constants.py +0 -0
  48. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/admin_analytics/database/__init__.py +0 -0
  49. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/admin_analytics/database/queries/__init__.py +0 -0
  50. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/admin_analytics/database/queries/fact_engagement_admin_dash.py +0 -0
  51. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py +0 -0
  52. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/admin_analytics/database/queries/skills_daily_rollup_admin_dash.py +0 -0
  53. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/admin_analytics/database/tables/__init__.py +0 -0
  54. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/admin_analytics/database/tables/base.py +0 -0
  55. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/admin_analytics/database/tables/fact_engagement_admin_dash.py +0 -0
  56. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py +0 -0
  57. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/admin_analytics/database/tables/skills_daily_rollup_admin_dash.py +0 -0
  58. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/admin_analytics/database/utils.py +0 -0
  59. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/__init__.py +0 -0
  60. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/urls.py +0 -0
  61. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v0/__init__.py +0 -0
  62. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v0/serializers.py +0 -0
  63. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v0/urls.py +0 -0
  64. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v0/views.py +0 -0
  65. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v1/__init__.py +0 -0
  66. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v1/urls.py +0 -0
  67. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v1/views/__init__.py +0 -0
  68. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v1/views/analytics_completions.py +0 -0
  69. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v1/views/analytics_engagements.py +0 -0
  70. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v1/views/analytics_enrollments.py +0 -0
  71. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v1/views/analytics_leaderboard.py +0 -0
  72. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v1/views/base.py +0 -0
  73. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/api/v1/views/enterprise_offers.py +0 -0
  74. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/apps.py +0 -0
  75. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/cache/__init__.py +0 -0
  76. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/cache/decorators.py +0 -0
  77. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/constants.py +0 -0
  78. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/filters.py +0 -0
  79. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/fixtures/enterprise_enrollment.json +0 -0
  80. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/fixtures/enterprise_user.json +0 -0
  81. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/__init__.py +0 -0
  82. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/__init__.py +0 -0
  83. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/create_dummy_data.py +0 -0
  84. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/create_dummy_data_lpr_v1.py +0 -0
  85. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/create_enterprise_enrollment.py +0 -0
  86. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/create_enterprise_learner_enrollment_lpr_v1.py +0 -0
  87. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/create_enterprise_learner_lpr_v1.py +0 -0
  88. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/create_enterprise_offer.py +0 -0
  89. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/create_enterprise_user.py +0 -0
  90. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/pre_warm_analytics_cache.py +0 -0
  91. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/tests/__init__.py +0 -0
  92. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/tests/test_create_dummy_data_lpr_v1.py +0 -0
  93. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/tests/test_create_enterprise_enrollment.py +0 -0
  94. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/tests/test_create_enterprise_learner_enrollment_lpr_v1.py +0 -0
  95. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/tests/test_create_enterprise_learner_lpr_v1.py +0 -0
  96. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/tests/test_create_enterprise_user.py +0 -0
  97. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/management/commands/tests/test_pre_warm_analytics_cache.py +0 -0
  98. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0001_initial.py +0 -0
  99. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0002_auto_20180430_1358.py +0 -0
  100. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0003_auto_20180501_0603.py +0 -0
  101. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0004_auto_20180501_0928.py +0 -0
  102. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0004_auto_20180508_1652.py +0 -0
  103. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0005_auto_20180524_2204.py +0 -0
  104. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0006_auto_20180612_0336.py +0 -0
  105. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0007_auto_20180612_0534.py +0 -0
  106. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0008_auto_20180614_0108.py +0 -0
  107. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0009_auto_20180628_1152.py +0 -0
  108. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0010_enterpriseenrollment_created.py +0 -0
  109. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0011_enterpriseuser.py +0 -0
  110. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0012_auto_20180831_1930.py +0 -0
  111. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0013_auto_20180831_1931.py +0 -0
  112. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0014_enterpriseuser_created.py +0 -0
  113. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0015_auto_20180907_1757.py +0 -0
  114. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0016_auto_20180924_2138.py +0 -0
  115. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0017_enterpriseenrollment_unenrollment_timestamp.py +0 -0
  116. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0018_enterprisedatafeaturerole_enterprisedataroleassignment.py +0 -0
  117. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0019_add_enterprise_data_feature_roles.py +0 -0
  118. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0020_add_role_based_access_control_switch.py +0 -0
  119. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0021_auto_20190329_1241.py +0 -0
  120. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0022_remove_role_based_access_control_switch.py +0 -0
  121. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0023_enterpriselearner_enterpriselearnerenrollment.py +0 -0
  122. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0024_auto_20210602_1811.py +0 -0
  123. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0025_auto_20210703_1854.py +0 -0
  124. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0026_auto_20210916_0414.py +0 -0
  125. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0027_enterpriselearnerenrollment_total_learning_time_seconds.py +0 -0
  126. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0028_enterpriselearnerenrollment_offer_id.py +0 -0
  127. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0029_enterpriseoffer.py +0 -0
  128. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0030_auto_20230609_1353.py +0 -0
  129. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0031_auto_20230615_0705.py +0 -0
  130. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0032_auto_20230704_0818.py +0 -0
  131. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0033_enterpriseadminlearnerprogress_enterpriseadminsummarizeinsights.py +0 -0
  132. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0034_auto_20230907_0834.py +0 -0
  133. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0035_auto_20230907_1154.py +0 -0
  134. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0036_enterprisesubsidybudget_subsidy_access_policy_display_name.py +0 -0
  135. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0037_alter_enterpriseenrollment_consent_granted.py +0 -0
  136. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0038_enterpriseoffer_export_timestamp.py +0 -0
  137. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0039_auto_20240212_1403.py +0 -0
  138. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0040_auto_20240718_0536_squashed_0043_alter_enterpriselearnerenrollment_enterprise_enrollment_id.py +0 -0
  139. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0044_enterpriseexecedlcmoduleperformance.py +0 -0
  140. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0045_alter_enterpriseexecedlcmoduleperformance_options_and_more.py +0 -0
  141. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/0046_enterprisegroupmembership.py +0 -0
  142. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/migrations/__init__.py +0 -0
  143. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/paginators.py +0 -0
  144. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/settings/__init__.py +0 -0
  145. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/settings/test.py +0 -0
  146. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/signals.py +0 -0
  147. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/__init__.py +0 -0
  148. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/admin_analytics/__init__.py +0 -0
  149. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/admin_analytics/mock_analytics_data.py +0 -0
  150. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/admin_analytics/mock_enrollments.py +0 -0
  151. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/admin_analytics/test_analytics_engagements.py +0 -0
  152. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +0 -0
  153. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py +0 -0
  154. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/admin_analytics/test_data_loaders.py +0 -0
  155. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/admin_analytics/test_enterprise_completions.py +0 -0
  156. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/api/__init__.py +0 -0
  157. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/api/v0/__init__.py +0 -0
  158. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/api/v0/test_serializers.py +0 -0
  159. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/api/v1/__init__.py +0 -0
  160. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/api/v1/test_serializers.py +0 -0
  161. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/api/v1/test_views.py +0 -0
  162. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/api/v1/views/__init__.py +0 -0
  163. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/api/v1/views/test_enterprise_admin.py +0 -0
  164. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/factories.py +0 -0
  165. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/mixins.py +0 -0
  166. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/test_clients.py +0 -0
  167. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/test_filters.py +0 -0
  168. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/test_models.py +0 -0
  169. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/test_utils.py +0 -0
  170. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/tests/test_views.py +0 -0
  171. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data/urls.py +0 -0
  172. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/__init__.py +0 -0
  173. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/admin.py +0 -0
  174. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/apps.py +0 -0
  175. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/constants.py +0 -0
  176. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/migrations/0001_initial.py +0 -0
  177. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/migrations/0002_add_enterprise_data_feature_roles.py +0 -0
  178. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/migrations/0003_add_role_based_access_control_switch.py +0 -0
  179. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/migrations/0004_enterprisedataroleassignment_enterprise_id.py +0 -0
  180. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/migrations/0005_turn_on_role_based_access_control_switch.py +0 -0
  181. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/migrations/0006_remove_role_based_access_control_switch.py +0 -0
  182. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/migrations/0007_enterprisedataroleassignment_applies_to_all_contexts.py +0 -0
  183. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/migrations/__init__.py +0 -0
  184. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/models.py +0 -0
  185. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/rules.py +0 -0
  186. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/tests/__init__.py +0 -0
  187. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/tests/factories.py +0 -0
  188. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_data_roles/tests/test_models.py +0 -0
  189. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/__init__.py +0 -0
  190. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/clients/__init__.py +0 -0
  191. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/clients/enterprise.py +0 -0
  192. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/clients/s3.py +0 -0
  193. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/clients/snowflake.py +0 -0
  194. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/clients/vertica.py +0 -0
  195. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/external_resource_link_report.py +0 -0
  196. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/fixtures/__init__.py +0 -0
  197. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/fixtures/enterprise_customer_reporting.json +0 -0
  198. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/reporter.py +0 -0
  199. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/tests/__init__.py +0 -0
  200. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/tests/test_clients.py +0 -0
  201. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/tests/test_enterprise_client.py +0 -0
  202. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/tests/test_external_link_report.py +0 -0
  203. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/tests/test_reporter.py +0 -0
  204. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/tests/test_vertica_client.py +0 -0
  205. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/enterprise_reporting/tests/utils.py +0 -0
  206. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/requirements/reporting.in +0 -0
  207. {edx_enterprise_data-10.9.8 → edx_enterprise_data-10.10.1}/setup.cfg +0 -0
@@ -15,6 +15,53 @@ Unreleased
15
15
  ----------
16
16
 
17
17
  =========================
18
+ [10.10.1] - 2025-03-18
19
+ ---------------------
20
+ * fix: Updated FROM email address to a provisioned email address.
21
+
22
+ [10.10.0] - 2025-02-24
23
+ ---------------------
24
+ * feat: Added separate handling for of SFTP transmission failures.
25
+
26
+ [10.9.2] - 2025-03-11
27
+ ---------------------
28
+ * feat: support flex groups in csv report on LPR.
29
+
30
+ [10.9.0] - 2025-02-24
31
+ ---------------------
32
+ * feat: get groups data from membership endpoint using enterprise client.
33
+
34
+ [10.8.1] - 2025-02-20
35
+ ---------------------
36
+ * fix: Limited accuracy of floating pointing numbers to 2 places.
37
+
38
+ [10.8.1] - 2025-02-20
39
+ ---------------------
40
+ * feat: Added 2 new columns in module performance report model and exposed them via associated REST API.
41
+
42
+ [10.7.8] - 2025-02-18
43
+ ---------------------
44
+ * chore: bump version from 10.7.7 to 10.7.8 for dependency upgrades
45
+
46
+ [10.7.7] - 2025-02-11
47
+ ---------------------
48
+ * chore: upgrade python requirements
49
+
50
+ [10.7.6] - 2025-02-03
51
+ ---------------------
52
+ * chore: upgrade python requirements
53
+
54
+ [10.7.5] - 2025-01-28
55
+ ---------------------
56
+ * chore: upgrade python requirements
57
+
58
+ [10.7.4] - 2025-01-27
59
+ ---------------------
60
+ * Fix: added UTC timezone in last_updated_date in enterprise enrollments API
61
+
62
+ [10.7.3] - 2025-01-21
63
+ ---------------------
64
+ * Fix: added timestampt in last_updated_date in enterprise enrollments API
18
65
 
19
66
  [10.7.2] - 2025-01-16
20
67
  ---------------------
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: edx-enterprise-data
3
- Version: 10.9.8
3
+ Version: 10.10.1
4
4
  Summary: Enterprise Reporting
5
5
  Home-page: https://github.com/openedx/edx-enterprise-data
6
6
  Author: edX
@@ -9,7 +9,6 @@ License: AGPL 3.0
9
9
  Classifier: Framework :: Django
10
10
  Classifier: Framework :: Django :: 4.2
11
11
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.8
13
12
  Classifier: Programming Language :: Python :: 3.12
14
13
  License-File: LICENSE
15
14
  Requires-Dist: Django
@@ -24,8 +23,8 @@ Requires-Dist: edx-rbac
24
23
  Requires-Dist: edx-rest-api-client
25
24
  Requires-Dist: factory_boy
26
25
  Requires-Dist: mysql-connector-python
27
- Requires-Dist: numpy<=1.24.4
28
- Requires-Dist: pandas<=2.0.3
26
+ Requires-Dist: numpy
27
+ Requires-Dist: pandas
29
28
  Requires-Dist: requests
30
29
  Requires-Dist: rules
31
30
  Provides-Extra: reporting
@@ -40,5 +39,14 @@ Requires-Dist: pyminizip; extra == "reporting"
40
39
  Requires-Dist: snowflake-connector-python; extra == "reporting"
41
40
  Requires-Dist: unicodecsv==0.14.1; extra == "reporting"
42
41
  Requires-Dist: vertica-python; extra == "reporting"
42
+ Dynamic: author
43
+ Dynamic: author-email
44
+ Dynamic: classifier
45
+ Dynamic: description
46
+ Dynamic: home-page
47
+ Dynamic: license
48
+ Dynamic: provides-extra
49
+ Dynamic: requires-dist
50
+ Dynamic: summary
43
51
 
44
52
  Tools and products related to providing access to Enterprise data.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: edx-enterprise-data
3
- Version: 10.9.8
3
+ Version: 10.10.1
4
4
  Summary: Enterprise Reporting
5
5
  Home-page: https://github.com/openedx/edx-enterprise-data
6
6
  Author: edX
@@ -9,7 +9,6 @@ License: AGPL 3.0
9
9
  Classifier: Framework :: Django
10
10
  Classifier: Framework :: Django :: 4.2
11
11
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.8
13
12
  Classifier: Programming Language :: Python :: 3.12
14
13
  License-File: LICENSE
15
14
  Requires-Dist: Django
@@ -24,8 +23,8 @@ Requires-Dist: edx-rbac
24
23
  Requires-Dist: edx-rest-api-client
25
24
  Requires-Dist: factory_boy
26
25
  Requires-Dist: mysql-connector-python
27
- Requires-Dist: numpy<=1.24.4
28
- Requires-Dist: pandas<=2.0.3
26
+ Requires-Dist: numpy
27
+ Requires-Dist: pandas
29
28
  Requires-Dist: requests
30
29
  Requires-Dist: rules
31
30
  Provides-Extra: reporting
@@ -40,5 +39,14 @@ Requires-Dist: pyminizip; extra == "reporting"
40
39
  Requires-Dist: snowflake-connector-python; extra == "reporting"
41
40
  Requires-Dist: unicodecsv==0.14.1; extra == "reporting"
42
41
  Requires-Dist: vertica-python; extra == "reporting"
42
+ Dynamic: author
43
+ Dynamic: author-email
44
+ Dynamic: classifier
45
+ Dynamic: description
46
+ Dynamic: home-page
47
+ Dynamic: license
48
+ Dynamic: provides-extra
49
+ Dynamic: requires-dist
50
+ Dynamic: summary
43
51
 
44
52
  Tools and products related to providing access to Enterprise data.
@@ -117,6 +117,8 @@ enterprise_data/migrations/0040_auto_20240718_0536_squashed_0043_alter_enterpris
117
117
  enterprise_data/migrations/0044_enterpriseexecedlcmoduleperformance.py
118
118
  enterprise_data/migrations/0045_alter_enterpriseexecedlcmoduleperformance_options_and_more.py
119
119
  enterprise_data/migrations/0046_enterprisegroupmembership.py
120
+ enterprise_data/migrations/0047_enterpriseexecedlcmoduleperformance_avg_after_lo_score_and_more.py
121
+ enterprise_data/migrations/0048_alter_enterpriseexecedlcmoduleperformance_avg_after_lo_score_and_more.py
120
122
  enterprise_data/migrations/__init__.py
121
123
  enterprise_data/settings/__init__.py
122
124
  enterprise_data/settings/test.py
@@ -162,6 +164,7 @@ enterprise_data_roles/tests/__init__.py
162
164
  enterprise_data_roles/tests/factories.py
163
165
  enterprise_data_roles/tests/test_models.py
164
166
  enterprise_reporting/__init__.py
167
+ enterprise_reporting/constants.py
165
168
  enterprise_reporting/delivery_method.py
166
169
  enterprise_reporting/external_resource_link_report.py
167
170
  enterprise_reporting/reporter.py
@@ -10,8 +10,8 @@ edx-rbac
10
10
  edx-rest-api-client
11
11
  factory_boy
12
12
  mysql-connector-python
13
- numpy<=1.24.4
14
- pandas<=2.0.3
13
+ numpy
14
+ pandas
15
15
  requests
16
16
  rules
17
17
 
@@ -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__ = "10.9.8"
5
+ __version__ = "10.10.1"
@@ -21,4 +21,4 @@ def fetch_max_enrollment_datetime():
21
21
  results = run_query(query)
22
22
  if not results:
23
23
  return None
24
- return pandas.to_datetime(results[0][0])
24
+ return pandas.to_datetime(results[0][0], utc=True)
@@ -6,6 +6,7 @@ from uuid import UUID
6
6
  from rest_framework import serializers
7
7
 
8
8
  from enterprise_data.admin_analytics.constants import ResponseType
9
+ from enterprise_data.cache.decorators import cache_it
9
10
  from enterprise_data.models import (
10
11
  EnterpriseAdminLearnerProgress,
11
12
  EnterpriseAdminSummarizeInsights,
@@ -16,6 +17,7 @@ from enterprise_data.models import (
16
17
  EnterpriseOffer,
17
18
  EnterpriseSubsidyBudget,
18
19
  )
20
+ from enterprise_data.utils import calculate_percentage_difference
19
21
 
20
22
 
21
23
  class EnterpriseLearnerEnrollmentSerializer(serializers.ModelSerializer):
@@ -25,6 +27,8 @@ class EnterpriseLearnerEnrollmentSerializer(serializers.ModelSerializer):
25
27
  course_api_url = serializers.SerializerMethodField()
26
28
  enterprise_user_id = serializers.SerializerMethodField()
27
29
  total_learning_time_hours = serializers.SerializerMethodField()
30
+ enterprise_flex_group_name = serializers.SerializerMethodField()
31
+ enterprise_flex_group_uuid = serializers.SerializerMethodField()
28
32
 
29
33
  class Meta:
30
34
  model = EnterpriseLearnerEnrollment
@@ -43,8 +47,9 @@ class EnterpriseLearnerEnrollmentSerializer(serializers.ModelSerializer):
43
47
  'last_activity_date', 'progress_status', 'passed_date', 'current_grade',
44
48
  'letter_grade', 'enterprise_user_id', 'user_email', 'user_account_creation_date',
45
49
  'user_country_code', 'user_username', 'user_first_name', 'user_last_name', 'enterprise_name',
46
- 'enterprise_customer_uuid', 'enterprise_sso_uid', 'created', 'course_api_url', 'total_learning_time_hours',
47
- 'is_subsidy', 'course_product_line', 'budget_id', 'enterprise_group_name', 'enterprise_group_uuid',
50
+ 'enterprise_customer_uuid', 'enterprise_sso_uid', 'created', 'course_api_url',
51
+ 'total_learning_time_hours', 'is_subsidy', 'course_product_line', 'budget_id',
52
+ 'enterprise_flex_group_name', 'enterprise_flex_group_uuid',
48
53
  )
49
54
 
50
55
  def get_course_api_url(self, obj):
@@ -61,6 +66,51 @@ class EnterpriseLearnerEnrollmentSerializer(serializers.ModelSerializer):
61
66
  """Returns the learners total learning time in hours"""
62
67
  return round((obj.total_learning_time_seconds or 0.0)/3600.0, 2)
63
68
 
69
+ @cache_it()
70
+ def _get_flex_groups(self, obj):
71
+ """
72
+ Returns list of tuples containing group (name, uuid) pairs for the learner.
73
+ This is cached to prevent duplicate database queries.
74
+ """
75
+ enterprise_user_id = obj.enterprise_user_id
76
+
77
+ if not enterprise_user_id:
78
+ return []
79
+
80
+ # Get all group memberships for this user in a single query
81
+ # Order by name for consistent ordering
82
+ return list(
83
+ EnterpriseGroupMembership.objects.filter(
84
+ enterprise_customer_user_id=enterprise_user_id,
85
+ membership_is_removed=False,
86
+ group_is_removed=False,
87
+ group_type="flex",
88
+ )
89
+ .order_by("enterprise_group_name")
90
+ .values_list("enterprise_group_name", "enterprise_group_uuid")
91
+ .distinct()
92
+ )
93
+
94
+ def get_enterprise_flex_group_name(self, obj):
95
+ """Returns a comma-separated list of enterprise group names that the learner is associated with"""
96
+ groups = self._get_flex_groups(obj)
97
+
98
+ if not groups:
99
+ return obj.enterprise_group_name
100
+
101
+ # Return comma-separated list of group names (first element of each tuple)
102
+ return ', '.join(group[0] for group in groups)
103
+
104
+ def get_enterprise_flex_group_uuid(self, obj):
105
+ """Returns a comma-separated list of enterprise group UUIDs that the learner is associated with"""
106
+ groups = self._get_flex_groups(obj)
107
+
108
+ if not groups:
109
+ return obj.enterprise_group_uuid
110
+
111
+ # Return comma-separated list of group UUIDs (second element of each tuple)
112
+ return ', '.join(str(group[1]) for group in groups)
113
+
64
114
 
65
115
  class EnterpriseSubsidyBudgetSerializer(serializers.ModelSerializer):
66
116
  """
@@ -231,6 +281,7 @@ class EnterpriseExecEdLCModulePerformanceSerializer(serializers.ModelSerializer)
231
281
  Serializer for EnterpriseExecEdLCModulePerformance model.
232
282
  """
233
283
  extensions_requested = serializers.SerializerMethodField()
284
+ avg_lo_percentage_difference = serializers.SerializerMethodField()
234
285
 
235
286
  class Meta:
236
287
  model = EnterpriseExecEdLCModulePerformance
@@ -240,6 +291,18 @@ class EnterpriseExecEdLCModulePerformanceSerializer(serializers.ModelSerializer)
240
291
  """Return extensions_requested if not None, otherwise return 0"""
241
292
  return obj.extensions_requested if obj.extensions_requested is not None else 0
242
293
 
294
+ def get_avg_lo_percentage_difference(self, obj):
295
+ """
296
+ Return percentage difference between `avg_before_lo_score` and `avg_after_lo_score` if not None,
297
+ otherwise return None
298
+ """
299
+ if obj.avg_before_lo_score is None or obj.avg_after_lo_score is None:
300
+ return None
301
+ return round(
302
+ calculate_percentage_difference(obj.avg_before_lo_score, obj.avg_after_lo_score),
303
+ 2
304
+ )
305
+
243
306
 
244
307
  class EnterpriseBudgetSerializer(serializers.ModelSerializer):
245
308
  """
@@ -126,7 +126,7 @@ class EnterpriseAdminAnalyticsAggregatesView(APIView):
126
126
  'completions': completions,
127
127
  'hours': hours,
128
128
  'sessions': sessions,
129
- 'last_updated_at': last_updated_at.date() if last_updated_at else None,
129
+ 'last_updated_at': last_updated_at if last_updated_at else None,
130
130
  'min_enrollment_date': min_enrollment_date,
131
131
  'max_enrollment_date': max_enrollment_date,
132
132
  },
@@ -8,6 +8,7 @@ from uuid import UUID
8
8
 
9
9
  from rest_framework import filters, viewsets
10
10
  from rest_framework.decorators import action
11
+ from rest_framework.exceptions import NotFound
11
12
  from rest_framework.response import Response
12
13
 
13
14
  from django.conf import settings
@@ -20,6 +21,7 @@ from django.utils import timezone
20
21
 
21
22
  from enterprise_data.admin_analytics.database.utils import LOGGER
22
23
  from enterprise_data.api.v1 import serializers
24
+ from enterprise_data.clients import EnterpriseApiClient
23
25
  from enterprise_data.models import EnterpriseGroupMembership, EnterpriseLearner, EnterpriseLearnerEnrollment
24
26
  from enterprise_data.paginators import EnterpriseEnrollmentsPagination
25
27
  from enterprise_data.renderers import EnrollmentsCSVRenderer
@@ -176,12 +178,51 @@ class EnterpriseLearnerEnrollmentViewSet(EnterpriseViewSetMixin, viewsets.ReadOn
176
178
 
177
179
  group_uuid = query_filters.get('group_uuid')
178
180
  if group_uuid:
179
- flex_group_exists = EnterpriseGroupMembership.objects.filter(
180
- enterprise_customer_user_id=OuterRef('enterprise_user_id'),
181
- enterprise_group_uuid=group_uuid,
182
- group_type='flex'
183
- )
184
- queryset = queryset.filter(Exists(flex_group_exists))
181
+ queryset = self.filter_by_group_uuid(queryset, group_uuid)
182
+
183
+ return queryset
184
+
185
+ def filter_by_group_uuid(self, queryset, group_uuid):
186
+ """
187
+ Filters the queryset based on the provided group_uuid. If no records are found,
188
+ it attempts to fetch group learners from the API and filter the queryset again.
189
+
190
+ Args:
191
+ queryset (QuerySet): The initial queryset to filter.
192
+ group_uuid (str): The UUID of the group to filter by.
193
+
194
+ Returns:
195
+ QuerySet: The filtered queryset.
196
+ """
197
+ flex_group_exists = EnterpriseGroupMembership.objects.filter(
198
+ enterprise_customer_user_id=OuterRef('enterprise_user_id'),
199
+ enterprise_group_uuid=group_uuid,
200
+ group_type='flex'
201
+ )
202
+
203
+ # First, filter the queryset using flex_group_exists
204
+ filtered_queryset = queryset.filter(Exists(flex_group_exists))
205
+
206
+ # If no records are found, attempt to fetch group_learners from the API
207
+ if not filtered_queryset.exists():
208
+ try:
209
+ enterprise_api_client = EnterpriseApiClient(
210
+ settings.BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL,
211
+ settings.BACKEND_SERVICE_EDX_OAUTH2_KEY,
212
+ settings.BACKEND_SERVICE_EDX_OAUTH2_SECRET,
213
+ )
214
+ group_learners = enterprise_api_client.get_enterprise_group_learners(group_uuid)
215
+ if group_learners:
216
+ group_learners_ids = [learner['enterprise_customer_user_id'] for learner in group_learners]
217
+ queryset = queryset.filter(enterprise_user_id__in=group_learners_ids)
218
+ else:
219
+ LOGGER.warning(f"No group learners found for group: {group_uuid}")
220
+ raise NotFound(f"No learners found for group: {group_uuid}")
221
+ except (Exception) as e: # pylint: disable=broad-exception-caught
222
+ LOGGER.error("Failed to fetch group learners from API: %s", e)
223
+ queryset = queryset.none() # API call failed or unexpected error, return an empty queryset
224
+ else:
225
+ queryset = filtered_queryset
185
226
 
186
227
  return queryset
187
228
 
@@ -101,3 +101,30 @@ class EnterpriseApiClient(OAuthAPIClient):
101
101
  TieredCache.set_all_tiers(cache_key, data, DEFAULT_REPORTING_CACHE_TIMEOUT)
102
102
 
103
103
  return data
104
+
105
+ def get_enterprise_group_learners(self, group_uuid):
106
+ """
107
+ Get the learners associated with a given enterprise group.
108
+
109
+ Returns: list of learners or None if unable to retrieve or no learners exist
110
+ """
111
+ LOGGER.info(f'[EnterpriseApiClient] getting learners for enterprise group:{group_uuid}')
112
+ url = urljoin(self.API_BASE_URL, f'enterprise-group/{group_uuid}/learners/')
113
+ all_learners = []
114
+
115
+ try:
116
+ while url:
117
+ response = self.get(url)
118
+ response.raise_for_status()
119
+ data = response.json()
120
+ all_learners.extend(data.get('results', []))
121
+ url = data.get('next') # Get the URL for the next page, if any
122
+
123
+ except (HTTPError, RequestException) as exc:
124
+ LOGGER.warning(
125
+ "[Data Overview Failure] Unable to retrieve Enterprise Group Learners details. "
126
+ f"Group: {group_uuid}, Exception: {exc}"
127
+ )
128
+ return None
129
+
130
+ return all_learners
@@ -0,0 +1,28 @@
1
+ # Generated by Django 4.2.15 on 2025-02-20 08:08
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('enterprise_data', '0046_enterprisegroupmembership'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='enterpriseexecedlcmoduleperformance',
15
+ name='avg_after_lo_score',
16
+ field=models.DecimalField(decimal_places=6, max_digits=38, null=True),
17
+ ),
18
+ migrations.AddField(
19
+ model_name='enterpriseexecedlcmoduleperformance',
20
+ name='avg_before_lo_score',
21
+ field=models.DecimalField(decimal_places=6, max_digits=38, null=True),
22
+ ),
23
+ migrations.AddField(
24
+ model_name='enterpriseexecedlcmoduleperformance',
25
+ name='question_name',
26
+ field=models.CharField(max_length=500, null=True),
27
+ ),
28
+ ]
@@ -0,0 +1,23 @@
1
+ # Generated by Django 4.2.15 on 2025-02-25 08:17
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('enterprise_data', '0047_enterpriseexecedlcmoduleperformance_avg_after_lo_score_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='enterpriseexecedlcmoduleperformance',
15
+ name='avg_after_lo_score',
16
+ field=models.DecimalField(decimal_places=2, max_digits=38, null=True),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='enterpriseexecedlcmoduleperformance',
20
+ name='avg_before_lo_score',
21
+ field=models.DecimalField(decimal_places=2, max_digits=38, null=True),
22
+ ),
23
+ ]
@@ -558,6 +558,9 @@ class EnterpriseExecEdLCModulePerformance(models.Model):
558
558
  discussion_forum_activities_completed_count = models.PositiveIntegerField(null=True)
559
559
  discussion_forum_activities_total_count = models.PositiveIntegerField(null=True)
560
560
  pass_grade = models.DecimalField(max_digits=38, decimal_places=2, null=True)
561
+ question_name = models.CharField(max_length=500, null=True)
562
+ avg_before_lo_score = models.DecimalField(max_digits=38, decimal_places=2, null=True)
563
+ avg_after_lo_score = models.DecimalField(max_digits=38, decimal_places=2, null=True)
561
564
 
562
565
  def __str__(self):
563
566
  """
@@ -27,7 +27,7 @@ class EnrollmentsCSVRenderer(CSVStreamingRenderer):
27
27
  'letter_grade', 'enterprise_user_id', 'user_email', 'user_account_creation_date',
28
28
  'user_country_code', 'user_username', 'user_first_name', 'user_last_name', 'enterprise_name',
29
29
  'enterprise_customer_uuid', 'enterprise_sso_uid', 'created', 'course_api_url', 'total_learning_time_hours',
30
- 'is_subsidy', 'course_product_line', 'budget_id', 'enterprise_group_name', 'enterprise_group_uuid',
30
+ 'is_subsidy', 'course_product_line', 'budget_id', 'enterprise_flex_group_name', 'enterprise_flex_group_uuid',
31
31
  ]
32
32
 
33
33
 
@@ -114,3 +114,22 @@ def find_first(iterable, condition):
114
114
  return next(item for item in iterable if condition(item))
115
115
  except StopIteration:
116
116
  return None
117
+
118
+
119
+ def calculate_percentage_difference(first, second):
120
+ """
121
+ Calculate the percentage difference between two numbers.
122
+
123
+ It will calculate the percentage difference between the two numbers using the formula:
124
+ ((second - first) / (first + second)/2) * 100
125
+
126
+ Arguments:
127
+ first (float): The first number.
128
+ second (float): The second number.
129
+
130
+ Returns:
131
+ float: The percentage difference between the two numbers.
132
+ """
133
+ if first == 0 and second == 0:
134
+ return 0
135
+ return ((second - first) / ((first + second) / 2)) * 100
@@ -0,0 +1,7 @@
1
+ """
2
+ Constants for enterprise_reporting.
3
+ """
4
+
5
+
6
+ SFTP_OPS_GENIE_EMAIL_ALERT_FROM_EMAIL = "enterprise-integrations@edx.org"
7
+ SFTP_OPS_GENIE_EMAIL_ALERT_EMAILS = ['enterprise-reporting-sftp@2u-internal.opsgenie.net']
@@ -2,15 +2,19 @@
2
2
  Classes that handle sending reports for enterprise customers with specific delivery methods.
3
3
  """
4
4
 
5
-
6
-
7
5
  import logging
8
6
  import os
9
7
  from smtplib import SMTPException
10
8
 
11
9
  import paramiko
12
10
 
13
- from enterprise_reporting.utils import compress_and_encrypt, decrypt_string, send_email_with_attachment
11
+ from enterprise_reporting.constants import SFTP_OPS_GENIE_EMAIL_ALERT_EMAILS, SFTP_OPS_GENIE_EMAIL_ALERT_FROM_EMAIL
12
+ from enterprise_reporting.utils import (
13
+ compress_and_encrypt,
14
+ decrypt_string,
15
+ retry_on_exception,
16
+ send_email_with_attachment,
17
+ )
14
18
 
15
19
  LOGGER = logging.getLogger(__name__)
16
20
 
@@ -97,6 +101,8 @@ class SFTPDeliveryMethod(DeliveryMethod):
97
101
  """
98
102
  Class that handles sending an enterprise report file via SFTP.
99
103
  """
104
+ sender_email = SFTP_OPS_GENIE_EMAIL_ALERT_FROM_EMAIL
105
+ receiver_emails = SFTP_OPS_GENIE_EMAIL_ALERT_EMAILS
100
106
 
101
107
  def __init__(self, reporting_config, password):
102
108
  """Initialize the SFTP Delivery Method."""
@@ -106,9 +112,11 @@ class SFTPDeliveryMethod(DeliveryMethod):
106
112
  self.username = reporting_config['sftp_username']
107
113
  self.file_path = reporting_config['sftp_file_path']
108
114
 
109
- def send(self, files):
110
- """Send the given files through SFTP."""
111
- data_reports = super().send(files)
115
+ @retry_on_exception(max_retries=3, delay=2, backoff=2)
116
+ def send_over_sftp(self, data_reports):
117
+ """
118
+ Send the reports via SFTP, retry on exception.
119
+ """
112
120
  LOGGER.info('Connecting via SFTP to remote host {} for {}'.format(
113
121
  self.hostname,
114
122
  self.enterprise_customer_name
@@ -129,4 +137,25 @@ class SFTPDeliveryMethod(DeliveryMethod):
129
137
  )
130
138
  sftp.close()
131
139
  ssh.close()
132
- LOGGER.info(f'Successfully sent report via sftp for {self.enterprise_customer_name}')
140
+
141
+ def send(self, files):
142
+ """Send the given files through SFTP."""
143
+ try:
144
+ data_reports = super().send(files)
145
+ self.send_over_sftp(data_reports)
146
+ except Exception: # pylint: disable=broad-except
147
+ email_subject = f'SFTP transmission failed for {self.enterprise_customer_name}'
148
+ email_body = f'Failed to send {self.data_type} report for {self.enterprise_customer_name}'
149
+ LOGGER.exception(f'SFTP transmission failed for {self.enterprise_customer_name}')
150
+ else:
151
+ LOGGER.info(f'Successfully sent report via sftp for {self.enterprise_customer_name}')
152
+ email_subject = f'SFTP transmission successful for {self.enterprise_customer_name}'
153
+ email_body = f'SFTP transmission successful for {self.enterprise_customer_name}'
154
+
155
+ send_email_with_attachment(
156
+ subject=email_subject,
157
+ body=email_body,
158
+ from_email=self.sender_email,
159
+ to_email=self.receiver_emails,
160
+ attachment_data={},
161
+ )
@@ -4,13 +4,15 @@ Sends an Enterprise Customer's data file to a configured destination.
4
4
  """
5
5
 
6
6
 
7
-
8
7
  import argparse
8
+ import datetime
9
9
  import logging
10
10
  import os
11
11
  import re
12
12
  import sys
13
13
 
14
+ import pytz
15
+
14
16
  from enterprise_reporting.clients.enterprise import EnterpriseAPIClient
15
17
  from enterprise_reporting.reporter import EnterpriseReportSender
16
18
  from enterprise_reporting.utils import is_current_time_in_schedule
@@ -55,11 +57,13 @@ def cleanup_files(enterprise_id):
55
57
  os.remove(os.path.join(directory, f))
56
58
 
57
59
 
58
- def should_deliver_report(args, reporting_config):
60
+ def should_deliver_report(args, reporting_config, current_est_time):
59
61
  """Given CLI arguments and the reporting configuration, determine if delivery should happen."""
60
62
  valid_data_type = reporting_config['data_type'] in (args.data_type or DATA_TYPES)
61
63
  enterprise_customer_specified = bool(args.enterprise_customer)
64
+
62
65
  meets_schedule_requirement = is_current_time_in_schedule(
66
+ current_est_time,
63
67
  reporting_config['frequency'],
64
68
  reporting_config['hour_of_day'],
65
69
  reporting_config['day_of_month'],
@@ -101,6 +105,12 @@ def process_reports():
101
105
  LOGGER.error(f'The enterprise {args.enterprise_customer} does not have a reporting configuration.')
102
106
  sys.exit(1)
103
107
 
108
+ # We are defining the current est time globally because we want the current time for a job
109
+ # to remain same thoughout the job. This ensures that a single report is not processed multiple times.
110
+ # See this comment for more details: https://2u-internal.atlassian.net/browse/ENT-9954?focusedCommentId=5356815
111
+ est_timezone = pytz.timezone('US/Eastern')
112
+ current_est_time = datetime.datetime.now(est_timezone)
113
+
104
114
  error_raised = False
105
115
  for reporting_config in reporting_configs['results']:
106
116
  LOGGER.info('Checking if {}\'s reporting config for {} data in {} format is ready for processing'.format(
@@ -109,7 +119,7 @@ def process_reports():
109
119
  reporting_config['report_type'],
110
120
  ))
111
121
 
112
- if should_deliver_report(args, reporting_config):
122
+ if should_deliver_report(args, reporting_config, current_est_time):
113
123
  if send_data(reporting_config):
114
124
  error_raised = True
115
125
  else: