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.
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/CHANGELOG.rst +9 -0
- {edx_enterprise_data-8.7.0/edx_enterprise_data.egg-info → edx_enterprise_data-8.8.1}/PKG-INFO +1 -1
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1/edx_enterprise_data.egg-info}/PKG-INFO +1 -1
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/edx_enterprise_data.egg-info/SOURCES.txt +3 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/__init__.py +1 -1
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/admin_analytics/constants.py +9 -3
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/admin_analytics/data_loaders.py +7 -3
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/admin_analytics/utils.py +52 -10
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/paginators.py +1 -1
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/serializers.py +39 -30
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/urls.py +12 -6
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/views/analytics_enrollments.py +85 -73
- edx_enterprise_data-8.8.1/enterprise_data/api/v1/views/analytics_leaderboard.py +141 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/views/enterprise_admin.py +20 -19
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/views/enterprise_completions.py +49 -28
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/renderers.py +14 -0
- edx_enterprise_data-8.8.1/enterprise_data/tests/admin_analytics/mock_analytics_data.py +511 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/admin_analytics/mock_enrollments.py +4 -4
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +23 -22
- edx_enterprise_data-8.8.1/enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py +163 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/admin_analytics/test_enterprise_completions.py +1 -1
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/utils.py +16 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/LICENSE +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/MANIFEST.in +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/README.md +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/edx_enterprise_data.egg-info/dependency_links.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/edx_enterprise_data.egg-info/not-zip-safe +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/edx_enterprise_data.egg-info/requires.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/edx_enterprise_data.egg-info/top_level.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/admin_analytics/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/admin_analytics/completions_utils.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/admin_analytics/database.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/urls.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v0/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v0/serializers.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v0/urls.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v0/views.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/views/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/views/base.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/views/enterprise_learner.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/views/enterprise_offers.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/apps.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/clients.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/constants.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/filters.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/fixtures/enterprise_enrollment.json +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/fixtures/enterprise_user.json +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/create_dummy_data.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/create_dummy_data_lpr_v1.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/create_enterprise_enrollment.py +0 -0
- {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
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/create_enterprise_learner_lpr_v1.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/create_enterprise_offer.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/create_enterprise_user.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/tests/__init__.py +0 -0
- {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
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/tests/test_create_enterprise_enrollment.py +0 -0
- {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
- {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
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/management/commands/tests/test_create_enterprise_user.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0001_initial.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0002_auto_20180430_1358.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0003_auto_20180501_0603.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0004_auto_20180501_0928.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0004_auto_20180508_1652.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0005_auto_20180524_2204.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0006_auto_20180612_0336.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0007_auto_20180612_0534.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0008_auto_20180614_0108.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0009_auto_20180628_1152.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0010_enterpriseenrollment_created.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0011_enterpriseuser.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0012_auto_20180831_1930.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0013_auto_20180831_1931.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0014_enterpriseuser_created.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0015_auto_20180907_1757.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0016_auto_20180924_2138.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0017_enterpriseenrollment_unenrollment_timestamp.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0018_enterprisedatafeaturerole_enterprisedataroleassignment.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0019_add_enterprise_data_feature_roles.py +0 -0
- {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
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0021_auto_20190329_1241.py +0 -0
- {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
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0023_enterpriselearner_enterpriselearnerenrollment.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0024_auto_20210602_1811.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0025_auto_20210703_1854.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0026_auto_20210916_0414.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0027_enterpriselearnerenrollment_total_learning_time_seconds.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0028_enterpriselearnerenrollment_offer_id.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0029_enterpriseoffer.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0030_auto_20230609_1353.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0031_auto_20230615_0705.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0032_auto_20230704_0818.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0033_enterpriseadminlearnerprogress_enterpriseadminsummarizeinsights.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0034_auto_20230907_0834.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0035_auto_20230907_1154.py +0 -0
- {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
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0037_alter_enterpriseenrollment_consent_granted.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0038_enterpriseoffer_export_timestamp.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0039_auto_20240212_1403.py +0 -0
- {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
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/0044_enterpriseexecedlcmoduleperformance.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/migrations/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/models.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/paginators.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/settings/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/settings/test.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/signals.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/admin_analytics/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/admin_analytics/test_data_loaders.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/admin_analytics/test_utils.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/v0/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/v0/test_serializers.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/v1/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/v1/test_serializers.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/v1/test_views.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/v1/views/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/api/v1/views/test_enterprise_admin.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/factories.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/mixins.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/test_clients.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/test_filters.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/test_models.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/test_utils.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/tests/test_views.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/urls.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/admin.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/apps.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/constants.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/migrations/0001_initial.py +0 -0
- {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
- {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
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/migrations/0004_enterprisedataroleassignment_enterprise_id.py +0 -0
- {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
- {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
- {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
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/migrations/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/models.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/rules.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/tests/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/tests/factories.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data_roles/tests/test_models.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/clients/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/clients/enterprise.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/clients/s3.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/clients/snowflake.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/clients/vertica.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/delivery_method.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/external_resource_link_report.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/fixtures/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/fixtures/enterprise_customer_reporting.json +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/reporter.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/send_enterprise_reports.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/__init__.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_clients.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_delivery_method.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_enterprise_client.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_external_link_report.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_reporter.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_send_enterprise_reports.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_utils.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/test_vertica_client.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/tests/utils.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_reporting/utils.py +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/base.in +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/base.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/ci.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/common_constraints.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/constraints.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/dev.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/django.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/pip.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/pip_tools.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/quality.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/reporting.in +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/test-master.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/test-reporting.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/requirements/test.txt +0 -0
- {edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/setup.cfg +0 -0
- {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
|
{edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/edx_enterprise_data.egg-info/SOURCES.txt
RENAMED
@@ -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
|
{edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/admin_analytics/constants.py
RENAMED
@@ -3,7 +3,7 @@
|
|
3
3
|
from enum import Enum
|
4
4
|
|
5
5
|
|
6
|
-
class
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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}')
|
{edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/admin_analytics/utils.py
RENAMED
@@ -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
|
10
|
-
from enterprise_data.admin_analytics.data_loaders import
|
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
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
56
|
-
|
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 ==
|
100
|
+
if calc == Calculation.RUNNING_TOTAL.value:
|
62
101
|
df[aggregation_column] = df.groupby("enroll_type")[aggregation_column].cumsum()
|
63
|
-
elif calc in [
|
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)
|
{edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/paginators.py
RENAMED
@@ -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 =
|
75
|
+
page_size = 50
|
76
76
|
max_page_size = 100
|
77
77
|
|
78
78
|
def paginate_queryset(self, queryset, request, view=None):
|
{edx_enterprise_data-8.7.0 → edx_enterprise_data-8.8.1}/enterprise_data/api/v1/serializers.py
RENAMED
@@ -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
|
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
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
+
Granularity.DAILY.value,
|
246
|
+
Granularity.WEEKLY.value,
|
247
|
+
Granularity.MONTHLY.value,
|
248
|
+
Granularity.QUARTERLY.value
|
245
249
|
]
|
246
250
|
CALCULATION_CHOICES = [
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
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
|
297
|
-
|
298
|
-
|
299
|
-
|
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
|
-
|
322
|
+
chart_type = serializers.CharField(required=False)
|
303
323
|
|
304
|
-
def
|
324
|
+
def validate_chart_type(self, value):
|
305
325
|
"""
|
306
|
-
Validate the
|
326
|
+
Validate the chart_type value.
|
307
327
|
|
308
328
|
Raises:
|
309
|
-
serializers.ValidationError: If
|
329
|
+
serializers.ValidationError: If chart_type is not one of the valid choices
|
310
330
|
"""
|
311
|
-
if value not in self.
|
312
|
-
raise serializers.ValidationError(f"
|
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/
|
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/
|
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/
|
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/
|
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/
|
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/
|
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
|
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
|
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 =
|
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
|
-
|
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
|
-
|
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="
|
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',
|
125
|
-
calculation = serializer.data.get('calculation',
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
179
|
-
calculation {str} -- Calculation of the data. One of
|
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
|
206
|
-
calculation {str} -- Calculation of the data. One of
|
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
|
222
|
-
calculation {str} -- Calculation of the data. One of
|
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
|
|