edx-enterprise-data 9.0.0__py3-none-any.whl → 9.1.0__py3-none-any.whl

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 (24) hide show
  1. {edx_enterprise_data-9.0.0.dist-info → edx_enterprise_data-9.1.0.dist-info}/METADATA +4 -3
  2. {edx_enterprise_data-9.0.0.dist-info → edx_enterprise_data-9.1.0.dist-info}/RECORD +22 -23
  3. {edx_enterprise_data-9.0.0.dist-info → edx_enterprise_data-9.1.0.dist-info}/WHEEL +1 -1
  4. enterprise_data/__init__.py +1 -1
  5. enterprise_data/admin_analytics/constants.py +0 -16
  6. enterprise_data/admin_analytics/database/queries/fact_engagement_admin_dash.py +90 -0
  7. enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py +83 -4
  8. enterprise_data/admin_analytics/database/tables/fact_engagement_admin_dash.py +116 -0
  9. enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py +97 -3
  10. enterprise_data/api/v1/serializers.py +1 -55
  11. enterprise_data/api/v1/urls.py +14 -17
  12. enterprise_data/api/v1/views/analytics_completions.py +150 -0
  13. enterprise_data/api/v1/views/analytics_engagements.py +106 -351
  14. enterprise_data/api/v1/views/analytics_enrollments.py +3 -6
  15. enterprise_data/renderers.py +22 -7
  16. enterprise_data/tests/admin_analytics/mock_analytics_data.py +12 -90
  17. enterprise_data/tests/admin_analytics/mock_enrollments.py +0 -66
  18. enterprise_data/tests/admin_analytics/test_analytics_engagements.py +120 -240
  19. enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +12 -81
  20. enterprise_data/tests/admin_analytics/test_enterprise_completions.py +105 -120
  21. enterprise_data/admin_analytics/completions_utils.py +0 -261
  22. enterprise_data/api/v1/views/enterprise_completions.py +0 -200
  23. {edx_enterprise_data-9.0.0.dist-info → edx_enterprise_data-9.1.0.dist-info}/LICENSE +0 -0
  24. {edx_enterprise_data-9.0.0.dist-info → edx_enterprise_data-9.1.0.dist-info}/top_level.txt +0 -0
@@ -5,13 +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 (
9
- Calculation,
10
- EngagementChart,
11
- EnrollmentChart,
12
- Granularity,
13
- ResponseType,
14
- )
8
+ from enterprise_data.admin_analytics.constants import Calculation, Granularity, ResponseType
15
9
  from enterprise_data.models import (
16
10
  EnterpriseAdminLearnerProgress,
17
11
  EnterpriseAdminSummarizeInsights,
@@ -315,51 +309,3 @@ class AdvanceAnalyticsQueryParamSerializer(serializers.Serializer): # pylint: d
315
309
  if value not in self.CALCULATION_CHOICES:
316
310
  raise serializers.ValidationError(f"Calculation must be one of {self.CALCULATION_CHOICES}")
317
311
  return value
318
-
319
-
320
- class AdvanceAnalyticsEnrollmentStatsSerializer(
321
- AdvanceAnalyticsQueryParamSerializer
322
- ): # pylint: disable=abstract-method
323
- """Serializer for validating Advance Analytics Enrollments Stats API"""
324
- CHART_TYPES = [
325
- EnrollmentChart.ENROLLMENTS_OVER_TIME.value,
326
- EnrollmentChart.TOP_COURSES_BY_ENROLLMENTS.value,
327
- EnrollmentChart.TOP_SUBJECTS_BY_ENROLLMENTS.value
328
- ]
329
-
330
- chart_type = serializers.CharField(required=False)
331
-
332
- def validate_chart_type(self, value):
333
- """
334
- Validate the chart_type value.
335
-
336
- Raises:
337
- serializers.ValidationError: If chart_type is not one of the valid choices
338
- """
339
- if value not in self.CHART_TYPES:
340
- raise serializers.ValidationError(f"chart_type must be one of {self.CHART_TYPES}")
341
- return value
342
-
343
-
344
- class AdvanceAnalyticsEngagementStatsSerializer(
345
- AdvanceAnalyticsQueryParamSerializer
346
- ): # pylint: disable=abstract-method
347
- """Serializer for validating Advance Analytics Engagements Stats API"""
348
- CHART_TYPES = [
349
- EngagementChart.ENGAGEMENTS_OVER_TIME.value,
350
- EngagementChart.TOP_COURSES_BY_ENGAGEMENTS.value,
351
- EngagementChart.TOP_SUBJECTS_BY_ENGAGEMENTS.value
352
- ]
353
-
354
- chart_type = serializers.CharField(required=False)
355
-
356
- def validate_chart_type(self, value):
357
- """
358
- Validate the chart_type value.
359
-
360
- Raises:
361
- serializers.ValidationError: If chart_type is not one of the valid choices
362
- """
363
- if value not in self.CHART_TYPES:
364
- raise serializers.ValidationError(f"chart_type must be one of {self.CHART_TYPES}")
365
- return value
@@ -8,13 +8,10 @@ from rest_framework.routers import DefaultRouter
8
8
  from django.urls import re_path
9
9
 
10
10
  from enterprise_data.api.v1.views import enterprise_admin as enterprise_admin_views
11
- from enterprise_data.api.v1.views import enterprise_completions as enterprise_completions_views
12
11
  from enterprise_data.api.v1.views import enterprise_learner as enterprise_learner_views
13
12
  from enterprise_data.api.v1.views import enterprise_offers as enterprise_offers_views
14
- from enterprise_data.api.v1.views.analytics_engagements import (
15
- AdvanceAnalyticsEngagementStatsView,
16
- AdvanceAnalyticsIndividualEngagementsView,
17
- )
13
+ from enterprise_data.api.v1.views.analytics_completions import AdvanceAnalyticsCompletionsView
14
+ from enterprise_data.api.v1.views.analytics_engagements import AdvanceAnalyticsEngagementView
18
15
  from enterprise_data.api.v1.views.analytics_enrollments import AdvanceAnalyticsEnrollmentsView
19
16
  from enterprise_data.api.v1.views.analytics_leaderboard import AdvanceAnalyticsLeaderboardView
20
17
  from enterprise_data.constants import UUID4_REGEX
@@ -74,14 +71,24 @@ urlpatterns = [
74
71
  AdvanceAnalyticsEnrollmentsView.as_view({'get': 'list'}),
75
72
  name='enterprise-admin-analytics-enrollments'
76
73
  ),
74
+ re_path(
75
+ fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/completions/stats$',
76
+ AdvanceAnalyticsCompletionsView.as_view({'get': 'stats'}),
77
+ name='enterprise-admin-analytics-completions-stats'
78
+ ),
79
+ re_path(
80
+ fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/completions$',
81
+ AdvanceAnalyticsCompletionsView.as_view({'get': 'list'}),
82
+ name='enterprise-admin-analytics-completions'
83
+ ),
77
84
  re_path(
78
85
  fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/engagements/stats$',
79
- AdvanceAnalyticsEngagementStatsView.as_view(),
86
+ AdvanceAnalyticsEngagementView.as_view({'get': 'stats'}),
80
87
  name='enterprise-admin-analytics-engagements-stats'
81
88
  ),
82
89
  re_path(
83
90
  fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/engagements$',
84
- AdvanceAnalyticsIndividualEngagementsView.as_view(),
91
+ AdvanceAnalyticsEngagementView.as_view({'get': 'list'}),
85
92
  name='enterprise-admin-analytics-engagements'
86
93
  ),
87
94
  re_path(
@@ -89,16 +96,6 @@ urlpatterns = [
89
96
  enterprise_admin_views.EnterpriseAdminAnalyticsSkillsView.as_view(),
90
97
  name='enterprise-admin-analytics-skills'
91
98
  ),
92
- re_path(
93
- fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})/completions/stats$',
94
- enterprise_completions_views.EnterrpiseAdminCompletionsStatsView.as_view(),
95
- name='enterprise-admin-analytics-completions-stats'
96
- ),
97
- re_path(
98
- fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})/completions$',
99
- enterprise_completions_views.EnterrpiseAdminCompletionsView.as_view(),
100
- name='enterprise-admin-analytics-completions'
101
- ),
102
99
  ]
103
100
 
104
101
  urlpatterns += router.urls
@@ -0,0 +1,150 @@
1
+ """
2
+ Views for enterprise admin completions analytics.
3
+ """
4
+
5
+ from datetime import datetime
6
+ from logging import getLogger
7
+
8
+ from edx_rbac.decorators import permission_required
9
+ from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
10
+ from rest_framework.decorators import action
11
+ from rest_framework.response import Response
12
+ from rest_framework.viewsets import ViewSet
13
+
14
+ from django.http import StreamingHttpResponse
15
+
16
+ from enterprise_data.admin_analytics.constants import ResponseType
17
+ from enterprise_data.admin_analytics.database.tables import FactEnrollmentAdminDashTable
18
+ from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
19
+ from enterprise_data.api.v1.serializers import AdvanceAnalyticsQueryParamSerializer
20
+ from enterprise_data.api.v1.views.base import AnalyticsPaginationMixin
21
+ from enterprise_data.renderers import IndividualCompletionsCSVRenderer
22
+ from enterprise_data.utils import timer
23
+
24
+ LOGGER = getLogger(__name__)
25
+
26
+
27
+ class AdvanceAnalyticsCompletionsView(AnalyticsPaginationMixin, ViewSet):
28
+ """
29
+ View to handle requests for enterprise completion data.
30
+
31
+ Here is the list of URLs that are handled by this view:
32
+ 1. `enterprise_data_api_v1.enterprise-learner-completion-list`: Get individual completion data.
33
+ 2. `enterprise_data_api_v1.enterprise-learner-completion-stats`: Get completion stats data.
34
+ """
35
+ authentication_classes = (JwtAuthentication,)
36
+ pagination_class = AdvanceAnalyticsPagination
37
+ http_method_names = ('get', )
38
+
39
+ @permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
40
+ def list(self, request, enterprise_uuid):
41
+ """
42
+ Get individual completions data for the enterprise.
43
+ """
44
+ # Remove hyphens from the UUID
45
+ enterprise_uuid = enterprise_uuid.replace('-', '')
46
+
47
+ serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
48
+ serializer.is_valid(raise_exception=True)
49
+ min_enrollment_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(
50
+ enterprise_uuid,
51
+ )
52
+
53
+ # get values from query params or use default values
54
+ start_date = serializer.data.get('start_date', min_enrollment_date)
55
+ end_date = serializer.data.get('end_date', datetime.now())
56
+ page = serializer.data.get('page', 1)
57
+ page_size = serializer.data.get('page_size', 100)
58
+ completions = FactEnrollmentAdminDashTable().get_all_completions(
59
+ enterprise_customer_uuid=enterprise_uuid,
60
+ start_date=start_date,
61
+ end_date=end_date,
62
+ limit=page_size,
63
+ offset=(page - 1) * page_size,
64
+ )
65
+ total_count = FactEnrollmentAdminDashTable().get_completion_count(
66
+ enterprise_customer_uuid=enterprise_uuid,
67
+ start_date=start_date,
68
+ end_date=end_date,
69
+ )
70
+ response_type = request.query_params.get('response_type', ResponseType.JSON.value)
71
+
72
+ LOGGER.info(
73
+ "Individual completions data requested for enterprise [%s] from [%s] to [%s]",
74
+ enterprise_uuid,
75
+ start_date,
76
+ end_date,
77
+ )
78
+
79
+ if response_type == ResponseType.CSV.value:
80
+ filename = f"""individual_completions, {start_date} - {end_date}.csv"""
81
+
82
+ return StreamingHttpResponse(
83
+ IndividualCompletionsCSVRenderer().render(self._stream_serialized_data(
84
+ enterprise_uuid, start_date, end_date, total_count
85
+ )),
86
+ content_type="text/csv",
87
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'},
88
+ )
89
+
90
+ return self.get_paginated_response(
91
+ request=request,
92
+ records=completions,
93
+ page=page,
94
+ page_size=page_size,
95
+ total_count=total_count,
96
+ )
97
+
98
+ @staticmethod
99
+ def _stream_serialized_data(enterprise_uuid, start_date, end_date, total_count, page_size=50000):
100
+ """
101
+ Stream the serialized data.
102
+ """
103
+ offset = 0
104
+ while offset < total_count:
105
+ completions = FactEnrollmentAdminDashTable().get_all_completions(
106
+ enterprise_customer_uuid=enterprise_uuid,
107
+ start_date=start_date,
108
+ end_date=end_date,
109
+ limit=page_size,
110
+ offset=offset,
111
+ )
112
+ yield from completions
113
+ offset += page_size
114
+
115
+ @permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
116
+ @action(detail=False, methods=['get'], name='Enterprise completions data for charts', url_path='stats')
117
+ def stats(self, request, enterprise_uuid):
118
+ """
119
+ Get data to populate enterprise completion charts.
120
+
121
+ Here is the list of the charts and their corresponding data:
122
+ 1. `completions_over_time`: This will show time series data of completions over time.
123
+ 2. `top_courses_by_completions`: This will show the top courses by completions.
124
+ 3. `top_subjects_by_completions`: This will show the top subjects by completions.
125
+ """
126
+ # Remove hyphens from the UUID
127
+ enterprise_uuid = enterprise_uuid.replace('-', '')
128
+
129
+ serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
130
+ serializer.is_valid(raise_exception=True)
131
+
132
+ min_enrollment_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(
133
+ enterprise_uuid,
134
+ )
135
+ # get values from query params or use default
136
+ start_date = serializer.data.get('start_date', min_enrollment_date)
137
+ end_date = serializer.data.get('end_date', datetime.now())
138
+ with timer('construct_completion_all_stats'):
139
+ data = {
140
+ 'completions_over_time': FactEnrollmentAdminDashTable().get_completions_time_series_data(
141
+ enterprise_uuid, start_date, end_date
142
+ ),
143
+ 'top_courses_by_completions': FactEnrollmentAdminDashTable().get_top_courses_by_completions(
144
+ enterprise_uuid, start_date, end_date,
145
+ ),
146
+ 'top_subjects_by_completions': FactEnrollmentAdminDashTable().get_top_subjects_by_completions(
147
+ enterprise_uuid, start_date, end_date,
148
+ ),
149
+ }
150
+ return Response(data)