edx-enterprise-data 8.6.1__py3-none-any.whl → 8.8.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 (23) hide show
  1. {edx_enterprise_data-8.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/METADATA +1 -1
  2. {edx_enterprise_data-8.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/RECORD +23 -17
  3. enterprise_data/__init__.py +1 -1
  4. enterprise_data/admin_analytics/completions_utils.py +261 -0
  5. enterprise_data/admin_analytics/constants.py +9 -3
  6. enterprise_data/admin_analytics/utils.py +50 -11
  7. enterprise_data/api/v1/paginators.py +1 -1
  8. enterprise_data/api/v1/serializers.py +44 -30
  9. enterprise_data/api/v1/urls.py +21 -4
  10. enterprise_data/api/v1/views/analytics_enrollments.py +42 -53
  11. enterprise_data/api/v1/views/analytics_leaderboard.py +120 -0
  12. enterprise_data/api/v1/views/enterprise_admin.py +9 -5
  13. enterprise_data/api/v1/views/enterprise_completions.py +177 -0
  14. enterprise_data/renderers.py +14 -0
  15. enterprise_data/tests/admin_analytics/mock_analytics_data.py +501 -0
  16. enterprise_data/tests/admin_analytics/mock_enrollments.py +23 -7
  17. enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +23 -22
  18. enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py +163 -0
  19. enterprise_data/tests/admin_analytics/test_enterprise_completions.py +202 -0
  20. enterprise_data/tests/api/v1/views/test_enterprise_admin.py +4 -0
  21. {edx_enterprise_data-8.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/LICENSE +0 -0
  22. {edx_enterprise_data-8.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/WHEEL +0 -0
  23. {edx_enterprise_data-8.6.1.dist-info → edx_enterprise_data-8.8.0.dist-info}/top_level.txt +0 -0
@@ -8,12 +8,14 @@ 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
11
12
  from enterprise_data.api.v1.views import enterprise_learner as enterprise_learner_views
12
13
  from enterprise_data.api.v1.views import enterprise_offers as enterprise_offers_views
13
14
  from enterprise_data.api.v1.views.analytics_enrollments import (
14
15
  AdvanceAnalyticsEnrollmentStatsView,
15
16
  AdvanceAnalyticsIndividualEnrollmentsView,
16
17
  )
18
+ from enterprise_data.api.v1.views.analytics_leaderboard import AdvanceAnalyticsLeaderboardView
17
19
  from enterprise_data.constants import UUID4_REGEX
18
20
 
19
21
  app_name = 'enterprise_data_api_v1'
@@ -52,25 +54,40 @@ urlpatterns = [
52
54
  name='enterprise-admin-insights'
53
55
  ),
54
56
  re_path(
55
- fr'^admin/anlaytics/(?P<enterprise_id>{UUID4_REGEX})$',
57
+ fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})$',
56
58
  enterprise_admin_views.EnterpriseAdminAnalyticsAggregatesView.as_view(),
57
59
  name='enterprise-admin-analytics-aggregates'
58
60
  ),
59
61
  re_path(
60
- fr'^admin/anlaytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments/stats$',
62
+ fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/leaderboard$',
63
+ AdvanceAnalyticsLeaderboardView.as_view(),
64
+ name='enterprise-admin-analytics-leaderboard'
65
+ ),
66
+ re_path(
67
+ fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments/stats$',
61
68
  AdvanceAnalyticsEnrollmentStatsView.as_view(),
62
69
  name='enterprise-admin-analytics-enrollments-stats'
63
70
  ),
64
71
  re_path(
65
- fr'^admin/anlaytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments$',
72
+ fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments$',
66
73
  AdvanceAnalyticsIndividualEnrollmentsView.as_view(),
67
74
  name='enterprise-admin-analytics-enrollments'
68
75
  ),
69
76
  re_path(
70
- fr'^admin/anlaytics/(?P<enterprise_id>{UUID4_REGEX})/skills/stats',
77
+ fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})/skills/stats',
71
78
  enterprise_admin_views.EnterpriseAdminAnalyticsSkillsView.as_view(),
72
79
  name='enterprise-admin-analytics-skills'
73
80
  ),
81
+ re_path(
82
+ fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})/completions/stats$',
83
+ enterprise_completions_views.EnterrpiseAdminCompletionsStatsView.as_view(),
84
+ name='enterprise-admin-analytics-completions-stats'
85
+ ),
86
+ re_path(
87
+ fr'^admin/analytics/(?P<enterprise_id>{UUID4_REGEX})/completions$',
88
+ enterprise_completions_views.EnterrpiseAdminCompletionsView.as_view(),
89
+ name='enterprise-admin-analytics-completions'
90
+ ),
74
91
  ]
75
92
 
76
93
  urlpatterns += router.urls
@@ -1,5 +1,5 @@
1
1
  """Advance Analytics for Enrollments"""
2
- from datetime import datetime, timedelta
2
+ from datetime import datetime
3
3
 
4
4
  from edx_rbac.decorators import permission_required
5
5
  from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
@@ -8,37 +8,22 @@ from rest_framework.views import APIView
8
8
 
9
9
  from django.http import HttpResponse, StreamingHttpResponse
10
10
 
11
- from enterprise_data.admin_analytics.constants import CALCULATION, ENROLLMENT_CSV, GRANULARITY
12
- from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_datetime
11
+ from enterprise_data.admin_analytics.constants import Calculation, EnrollmentChart, Granularity, ResponseType
13
12
  from enterprise_data.admin_analytics.utils import (
14
13
  calculation_aggregation,
15
14
  fetch_and_cache_enrollments_data,
15
+ fetch_enrollments_cache_expiry_timestamp,
16
16
  granularity_aggregation,
17
17
  )
18
18
  from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
19
19
  from enterprise_data.api.v1.serializers import (
20
- AdvanceAnalyticsEnrollmentSerializer,
21
20
  AdvanceAnalyticsEnrollmentStatsSerializer,
21
+ AdvanceAnalyticsQueryParamSerializer,
22
22
  )
23
23
  from enterprise_data.renderers import IndividualEnrollmentsCSVRenderer
24
24
  from enterprise_data.utils import date_filter
25
25
 
26
26
 
27
- def fetch_enrollments_cache_expiry_timestamp():
28
- """Calculate cache expiry timestamp"""
29
- # TODO: Implement correct cache expiry logic for `enrollments` data.
30
- # Current cache expiry logic is based on `enterprise_learner_enrollment` table,
31
- # Which has nothing to do with the `enrollments` data. Instead cache expiry should
32
- # be based on `fact_enrollment_admin_dash` table. Currently we have no timestamp in
33
- # `fact_enrollment_admin_dash` table that can be used for cache expiry. Add a new
34
- # column in the table for this purpose and then use that column for cache expiry.
35
- last_updated_at = fetch_max_enrollment_datetime()
36
- cache_expiry = (
37
- last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
38
- )
39
- return cache_expiry
40
-
41
-
42
27
  class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
43
28
  """
44
29
  API for getting the advance analytics individual enrollments data.
@@ -50,7 +35,7 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
50
35
  @permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
51
36
  def get(self, request, enterprise_uuid):
52
37
  """Get individual enrollments data"""
53
- serializer = AdvanceAnalyticsEnrollmentSerializer(data=request.GET)
38
+ serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
54
39
  serializer.is_valid(raise_exception=True)
55
40
 
56
41
  cache_expiry = fetch_enrollments_cache_expiry_timestamp()
@@ -59,7 +44,7 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
59
44
  # get values from query params or use default values
60
45
  start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
61
46
  end_date = serializer.data.get('end_date', datetime.now())
62
- csv_type = request.query_params.get('csv_type')
47
+ response_type = request.query_params.get('response_type', ResponseType.JSON.value)
63
48
 
64
49
  # filter enrollments by date
65
50
  enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
@@ -77,11 +62,12 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
77
62
  enrollments["enterprise_enrollment_date"] = enrollments["enterprise_enrollment_date"].dt.date
78
63
  enrollments = enrollments.sort_values(by="enterprise_enrollment_date", ascending=False).reset_index(drop=True)
79
64
 
80
- if csv_type == ENROLLMENT_CSV.INDIVIDUAL_ENROLLMENTS.value:
65
+ if response_type == ResponseType.CSV.value:
66
+ filename = f"""individual_enrollments, {start_date} - {end_date}.csv"""
81
67
  return StreamingHttpResponse(
82
68
  IndividualEnrollmentsCSVRenderer().render(self._stream_serialized_data(enrollments)),
83
69
  content_type="text/csv",
84
- headers={"Content-Disposition": 'attachment; filename="individual_enrollments.csv"'},
70
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'},
85
71
  )
86
72
 
87
73
  paginator = self.pagination_class()
@@ -121,11 +107,12 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
121
107
  # get values from query params or use default
122
108
  start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
123
109
  end_date = serializer.data.get('end_date', datetime.now())
124
- granularity = serializer.data.get('granularity', GRANULARITY.DAILY.value)
125
- calculation = serializer.data.get('calculation', CALCULATION.TOTAL.value)
126
- csv_type = serializer.data.get('csv_type')
110
+ granularity = serializer.data.get('granularity', Granularity.DAILY.value)
111
+ calculation = serializer.data.get('calculation', Calculation.TOTAL.value)
112
+ response_type = serializer.data.get('response_type', ResponseType.JSON.value)
113
+ chart_type = serializer.data.get('chart_type')
127
114
 
128
- if csv_type is None:
115
+ if response_type == ResponseType.JSON.value:
129
116
  data = {
130
117
  "enrollments_over_time": self.construct_enrollments_over_time(
131
118
  enrollments_df.copy(),
@@ -146,26 +133,28 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
146
133
  ),
147
134
  }
148
135
  return Response(data)
149
- elif csv_type == ENROLLMENT_CSV.ENROLLMENTS_OVER_TIME.value:
150
- return self.construct_enrollments_over_time_csv(
151
- enrollments_df.copy(),
152
- start_date,
153
- end_date,
154
- granularity,
155
- calculation,
156
- )
157
- elif csv_type == ENROLLMENT_CSV.TOP_COURSES_BY_ENROLLMENTS.value:
158
- return self.construct_top_courses_by_enrollments_csv(
159
- enrollments_df.copy(),
160
- start_date,
161
- end_date,
162
- )
163
- elif csv_type == ENROLLMENT_CSV.TOP_SUBJECTS_BY_ENROLLMENTS.value:
164
- return self.construct_top_subjects_by_enrollments_csv(
165
- enrollments_df.copy(),
166
- start_date,
167
- end_date,
168
- )
136
+
137
+ if response_type == ResponseType.CSV.value:
138
+ if chart_type == EnrollmentChart.ENROLLMENTS_OVER_TIME.value:
139
+ return self.construct_enrollments_over_time_csv(
140
+ enrollments_df.copy(),
141
+ start_date,
142
+ end_date,
143
+ granularity,
144
+ calculation,
145
+ )
146
+ elif chart_type == EnrollmentChart.TOP_COURSES_BY_ENROLLMENTS.value:
147
+ return self.construct_top_courses_by_enrollments_csv(
148
+ enrollments_df.copy(),
149
+ start_date,
150
+ end_date,
151
+ )
152
+ elif chart_type == EnrollmentChart.TOP_SUBJECTS_BY_ENROLLMENTS.value:
153
+ return self.construct_top_subjects_by_enrollments_csv(
154
+ enrollments_df.copy(),
155
+ start_date,
156
+ end_date,
157
+ )
169
158
 
170
159
  def enrollments_over_time_common(self, enrollments_df, start_date, end_date, granularity, calculation):
171
160
  """
@@ -175,8 +164,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
175
164
  enrollments_df {DataFrame} -- DataFrame of enrollments
176
165
  start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
177
166
  end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
178
- granularity {str} -- Granularity of the data. One of GRANULARITY choices
179
- calculation {str} -- Calculation of the data. One of CALCULATION choices
167
+ granularity {str} -- Granularity of the data. One of Granularity choices
168
+ calculation {str} -- Calculation of the data. One of Calculation choices
180
169
  """
181
170
  # filter enrollments by date
182
171
  enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
@@ -202,8 +191,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
202
191
  enrollments_df {DataFrame} -- DataFrame of enrollments
203
192
  start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
204
193
  end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
205
- granularity {str} -- Granularity of the data. One of GRANULARITY choices
206
- calculation {str} -- Calculation of the data. One of CALCULATION choices
194
+ granularity {str} -- Granularity of the data. One of Granularity choices
195
+ calculation {str} -- Calculation of the data. One of Calculation choices
207
196
  """
208
197
  enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
209
198
 
@@ -218,8 +207,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
218
207
  enrollments_df {DataFrame} -- DataFrame of enrollments
219
208
  start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
220
209
  end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
221
- granularity {str} -- Granularity of the data. One of GRANULARITY choices
222
- calculation {str} -- Calculation of the data. One of CALCULATION choices
210
+ granularity {str} -- Granularity of the data. One of Granularity choices
211
+ calculation {str} -- Calculation of the data. One of Calculation choices
223
212
  """
224
213
  enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
225
214
 
@@ -0,0 +1,120 @@
1
+ """Advance Analytics for Leaderboard"""
2
+ from datetime import datetime
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from edx_rbac.decorators import permission_required
7
+ from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
8
+ from rest_framework.views import APIView
9
+
10
+ from django.http import StreamingHttpResponse
11
+
12
+ from enterprise_data.admin_analytics.constants import ResponseType
13
+ from enterprise_data.admin_analytics.utils import (
14
+ fetch_and_cache_engagements_data,
15
+ fetch_and_cache_enrollments_data,
16
+ fetch_engagements_cache_expiry_timestamp,
17
+ fetch_enrollments_cache_expiry_timestamp,
18
+ )
19
+ from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
20
+ from enterprise_data.api.v1.serializers import AdvanceAnalyticsQueryParamSerializer
21
+ from enterprise_data.renderers import LeaderboardCSVRenderer
22
+ from enterprise_data.utils import date_filter
23
+
24
+
25
+ class AdvanceAnalyticsLeaderboardView(APIView):
26
+ """
27
+ API for getting the advance analytics leaderboard data.
28
+ """
29
+ authentication_classes = (JwtAuthentication,)
30
+ pagination_class = AdvanceAnalyticsPagination
31
+ http_method_names = ['get']
32
+
33
+ @permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
34
+ def get(self, request, enterprise_uuid):
35
+ """Get leaderboard data"""
36
+ serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
37
+ serializer.is_valid(raise_exception=True)
38
+
39
+ enrollments_cache_expiry = fetch_enrollments_cache_expiry_timestamp()
40
+ enrollments_df = fetch_and_cache_enrollments_data(enterprise_uuid, enrollments_cache_expiry)
41
+
42
+ engagements_cache_expiry = fetch_engagements_cache_expiry_timestamp()
43
+ engagements_df = fetch_and_cache_engagements_data(enterprise_uuid, engagements_cache_expiry)
44
+
45
+ start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
46
+ end_date = serializer.data.get('end_date', datetime.now())
47
+ response_type = serializer.data.get('response_type', ResponseType.JSON.value)
48
+
49
+ # only include learners who have passed the course
50
+ enrollments_df = enrollments_df[enrollments_df["has_passed"] == 1]
51
+
52
+ # filter enrollments by date
53
+ enrollments_df = date_filter(start_date, end_date, enrollments_df, "passed_date")
54
+
55
+ completions = enrollments_df.groupby(["email"]).size().reset_index()
56
+ completions.columns = ["email", "course_completions"]
57
+
58
+ # filter engagements by date
59
+ engagements_df = date_filter(start_date, end_date, engagements_df, "activity_date")
60
+
61
+ engage = (
62
+ engagements_df.groupby(["email"])
63
+ .agg({"is_engaged": ["sum"], "learning_time_seconds": ["sum"]})
64
+ .reset_index()
65
+ )
66
+ engage.columns = ["email", "daily_sessions", "learning_time_seconds"]
67
+ engage["learning_time_hours"] = round(
68
+ engage["learning_time_seconds"].astype("float") / 60 / 60, 1
69
+ )
70
+ engage["average_session_length"] = round(
71
+ engage["learning_time_hours"] / engage["daily_sessions"].astype("float"), 1
72
+ )
73
+
74
+ leaderboard_df = engage.merge(completions, on="email", how="left")
75
+ leaderboard_df = leaderboard_df.sort_values(
76
+ by=["learning_time_hours", "daily_sessions", "course_completions"],
77
+ ascending=[False, False, False],
78
+ )
79
+
80
+ # move the aggregated row with email 'null' to the end of the table
81
+ idx = leaderboard_df.index[leaderboard_df['email'] == 'null']
82
+ leaderboard_df.loc[idx, 'email'] = 'learners who have not shared consent'
83
+ leaderboard_df = pd.concat([leaderboard_df.drop(idx), leaderboard_df.loc[idx]])
84
+
85
+ # convert `nan` values to `None` because `nan` is not JSON serializable
86
+ leaderboard_df = leaderboard_df.replace(np.nan, None)
87
+
88
+ if response_type == ResponseType.CSV.value:
89
+ filename = f"""Leaderboard, {start_date} - {end_date}.csv"""
90
+ leaderboard_df = leaderboard_df[
91
+ [
92
+ "email",
93
+ "learning_time_hours",
94
+ "daily_sessions",
95
+ "average_session_length",
96
+ "course_completions",
97
+ ]
98
+ ]
99
+ return StreamingHttpResponse(
100
+ LeaderboardCSVRenderer().render(self._stream_serialized_data(leaderboard_df)),
101
+ content_type="text/csv",
102
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'},
103
+ )
104
+
105
+ paginator = self.pagination_class()
106
+ page = paginator.paginate_queryset(leaderboard_df, request)
107
+ serialized_data = page.data.to_dict(orient='records')
108
+ response = paginator.get_paginated_response(serialized_data)
109
+
110
+ return response
111
+
112
+ def _stream_serialized_data(self, leaderboard_df, chunk_size=50000):
113
+ """
114
+ Stream the serialized data.
115
+ """
116
+ total_rows = leaderboard_df.shape[0]
117
+ for start_index in range(0, total_rows, chunk_size):
118
+ end_index = min(start_index + chunk_size, total_rows)
119
+ chunk = leaderboard_df.iloc[start_index:end_index]
120
+ yield from chunk.to_dict(orient='records')
@@ -189,17 +189,21 @@ class EnterpriseAdminAnalyticsSkillsView(APIView):
189
189
  data=request.GET
190
190
  )
191
191
  serializer.is_valid(raise_exception=True)
192
-
193
- start_date = serializer.data.get("start_date")
194
- end_date = serializer.data.get("end_date", datetime.now())
195
-
196
192
  last_updated_at = fetch_max_enrollment_datetime()
197
193
  cache_expiry = (
198
194
  last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
199
195
  )
196
+
197
+ enrollment = fetch_and_cache_enrollments_data(
198
+ enterprise_id, cache_expiry
199
+ ).copy()
200
+
201
+ start_date = serializer.data.get('start_date', enrollment.enterprise_enrollment_date.min())
202
+ end_date = serializer.data.get('end_date', datetime.now())
203
+
200
204
  skills = fetch_and_cache_skills_data(enterprise_id, cache_expiry).copy()
201
205
 
202
- if request.GET.get("format") == "csv":
206
+ if serializer.data.get('response_type') == 'csv':
203
207
  csv_data = get_top_skills_csv_data(skills, start_date, end_date)
204
208
  response = HttpResponse(content_type='text/csv')
205
209
  filename = f"Skills by Enrollment and Completion, {start_date} - {end_date}.csv"
@@ -0,0 +1,177 @@
1
+ """Views for enterprise admin completions analytics."""
2
+ import datetime
3
+ from datetime import datetime, timedelta
4
+
5
+ from edx_rbac.decorators import permission_required
6
+ from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
7
+ from rest_framework.response import Response
8
+ from rest_framework.status import HTTP_200_OK
9
+ from rest_framework.views import APIView
10
+
11
+ from django.http import HttpResponse
12
+
13
+ from enterprise_data.admin_analytics.completions_utils import (
14
+ get_completions_over_time,
15
+ get_csv_data_for_completions_over_time,
16
+ get_csv_data_for_top_courses_by_completions,
17
+ get_csv_data_for_top_subjects_by_completions,
18
+ get_top_courses_by_completions,
19
+ get_top_subjects_by_completions,
20
+ )
21
+ from enterprise_data.admin_analytics.constants import Calculation, Granularity
22
+ from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_datetime
23
+ from enterprise_data.admin_analytics.utils import ChartType, fetch_and_cache_enrollments_data
24
+ from enterprise_data.api.v1 import serializers
25
+ from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
26
+ from enterprise_data.utils import date_filter
27
+
28
+
29
+ class EnterrpiseAdminCompletionsStatsView(APIView):
30
+ """
31
+ API for getting the enterprise admin completions.
32
+ """
33
+ authentication_classes = (JwtAuthentication,)
34
+ http_method_names = ['get']
35
+
36
+ @permission_required(
37
+ "can_access_enterprise", fn=lambda request, enterprise_id: enterprise_id
38
+ )
39
+ def get(self, request, enterprise_id):
40
+ """
41
+ HTTP GET endpoint to retrieve the enterprise admin completions
42
+ """
43
+ serializer = serializers.AdminAnalyticsAggregatesQueryParamsSerializer(
44
+ data=request.GET
45
+ )
46
+ serializer.is_valid(raise_exception=True)
47
+
48
+ last_updated_at = fetch_max_enrollment_datetime()
49
+ cache_expiry = (
50
+ last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
51
+ )
52
+
53
+ enrollments = fetch_and_cache_enrollments_data(
54
+ enterprise_id, cache_expiry
55
+ ).copy()
56
+ # Use start and end date if provided by the client, if client has not provided then use
57
+ # 1. minimum enrollment date from the data as the start_date
58
+ # 2. today's date as the end_date
59
+ start_date = serializer.data.get(
60
+ "start_date", enrollments.enterprise_enrollment_date.min()
61
+ )
62
+ end_date = serializer.data.get("end_date", datetime.now())
63
+
64
+ if serializer.data.get('response_type') == 'csv':
65
+ chart_type = serializer.data.get('chart_type')
66
+ response = HttpResponse(content_type='text/csv')
67
+ csv_data = {}
68
+
69
+ if chart_type == ChartType.COMPLETIONS_OVER_TIME.value:
70
+ csv_data = get_csv_data_for_completions_over_time(
71
+ start_date=start_date,
72
+ end_date=end_date,
73
+ enrollments=enrollments.copy(),
74
+ date_agg=serializer.data.get('granularity', Granularity.DAILY.value),
75
+ calc=serializer.data.get('calculation', Calculation.TOTAL.value),
76
+ )
77
+ elif chart_type == ChartType.TOP_COURSES_BY_COMPLETIONS.value:
78
+ csv_data = get_csv_data_for_top_courses_by_completions(
79
+ start_date=start_date, end_date=end_date, enrollments=enrollments.copy()
80
+ )
81
+ elif chart_type == ChartType.TOP_SUBJECTS_BY_COMPLETIONS.value:
82
+ csv_data = get_csv_data_for_top_subjects_by_completions(
83
+ start_date=start_date, end_date=end_date, enrollments=enrollments.copy()
84
+ )
85
+ filename = csv_data['filename']
86
+ response['Content-Disposition'] = f'attachment; filename="{filename}"'
87
+ csv_data['data'].to_csv(path_or_buf=response)
88
+ return response
89
+
90
+ completions_over_time = get_completions_over_time(
91
+ start_date=start_date,
92
+ end_date=end_date,
93
+ dff=enrollments.copy(),
94
+ date_agg=serializer.data.get('granularity', Granularity.DAILY.value),
95
+ calc=serializer.data.get('calculation', Calculation.TOTAL.value),
96
+ )
97
+ top_courses_by_completions = get_top_courses_by_completions(
98
+ start_date=start_date, end_date=end_date, dff=enrollments.copy()
99
+ )
100
+ top_subjects_by_completions = get_top_subjects_by_completions(
101
+ start_date=start_date, end_date=end_date, dff=enrollments.copy()
102
+ )
103
+
104
+ return Response(
105
+ data={
106
+ 'completions_over_time': completions_over_time.to_dict(
107
+ orient="records"
108
+ ),
109
+ 'top_courses_by_completions': top_courses_by_completions.to_dict(
110
+ orient="records"
111
+ ),
112
+ 'top_subjects_by_completions': top_subjects_by_completions.to_dict(
113
+ orient="records"
114
+ ),
115
+ },
116
+ status=HTTP_200_OK,
117
+ )
118
+
119
+
120
+ class EnterrpiseAdminCompletionsView(APIView):
121
+ """
122
+ API for getting the enterprise admin completions.
123
+ """
124
+ authentication_classes = (JwtAuthentication,)
125
+ http_method_names = ['get']
126
+ pagination_class = AdvanceAnalyticsPagination
127
+
128
+ @permission_required(
129
+ "can_access_enterprise", fn=lambda request, enterprise_id: enterprise_id
130
+ )
131
+ def get(self, request, enterprise_id):
132
+ """
133
+ HTTP GET endpoint to retrieve the enterprise admin completions
134
+ """
135
+ serializer = serializers.AdminAnalyticsAggregatesQueryParamsSerializer(
136
+ data=request.GET
137
+ )
138
+ serializer.is_valid(raise_exception=True)
139
+
140
+ last_updated_at = fetch_max_enrollment_datetime()
141
+ cache_expiry = (
142
+ last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
143
+ )
144
+
145
+ enrollments = fetch_and_cache_enrollments_data(
146
+ enterprise_id, cache_expiry
147
+ ).copy()
148
+ # Use start and end date if provided by the client, if client has not provided then use
149
+ # 1. minimum enrollment date from the data as the start_date
150
+ # 2. today's date as the end_date
151
+ start_date = serializer.data.get(
152
+ 'start_date', enrollments.enterprise_enrollment_date.min()
153
+ )
154
+ end_date = serializer.data.get('end_date', datetime.now())
155
+
156
+ dff = enrollments[enrollments['has_passed'] == 1]
157
+
158
+ # Date filtering
159
+ dff = date_filter(start=start_date, end=end_date, data_frame=dff, date_column='passed_date')
160
+
161
+ dff = dff[['email', 'course_title', 'course_subject', 'passed_date']]
162
+ dff['passed_date'] = dff['passed_date'].dt.date
163
+ dff = dff.sort_values(by="passed_date", ascending=False).reset_index(drop=True)
164
+
165
+ if serializer.data.get('response_type') == 'csv':
166
+ response = HttpResponse(content_type='text/csv')
167
+ filename = f"Individual Completions, {start_date} - {end_date}.csv"
168
+ response['Content-Disposition'] = f'attachment; filename="{filename}"'
169
+ dff.to_csv(path_or_buf=response, index=False)
170
+ return response
171
+
172
+ paginator = self.pagination_class()
173
+ page = paginator.paginate_queryset(dff, request)
174
+ serialized_data = page.data.to_dict(orient='records')
175
+ response = paginator.get_paginated_response(serialized_data)
176
+
177
+ return response
@@ -43,3 +43,17 @@ class IndividualEnrollmentsCSVRenderer(CSVStreamingRenderer):
43
43
  'enroll_type',
44
44
  'enterprise_enrollment_date',
45
45
  ]
46
+
47
+
48
+ class LeaderboardCSVRenderer(CSVStreamingRenderer):
49
+ """
50
+ Custom streaming csv renderer for advance analytics leaderboard data.
51
+ """
52
+
53
+ header = [
54
+ 'email',
55
+ 'learning_time_hours',
56
+ 'daily_sessions',
57
+ 'average_session_length',
58
+ 'course_completions',
59
+ ]