edx-enterprise-data 8.7.0__py3-none-any.whl → 8.8.1__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.
@@ -1,5 +1,6 @@
1
1
  """Advance Analytics for Enrollments"""
2
- from datetime import datetime, timedelta
2
+ from datetime import datetime
3
+ from logging import getLogger
3
4
 
4
5
  from edx_rbac.decorators import permission_required
5
6
  from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
@@ -8,35 +9,22 @@ from rest_framework.views import APIView
8
9
 
9
10
  from django.http import HttpResponse, StreamingHttpResponse
10
11
 
11
- from enterprise_data.admin_analytics.constants import CALCULATION, ENROLLMENT_CSV, GRANULARITY
12
- from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_datetime
12
+ from enterprise_data.admin_analytics.constants import Calculation, EnrollmentChart, Granularity, ResponseType
13
13
  from enterprise_data.admin_analytics.utils import (
14
14
  calculation_aggregation,
15
15
  fetch_and_cache_enrollments_data,
16
+ fetch_enrollments_cache_expiry_timestamp,
16
17
  granularity_aggregation,
17
18
  )
18
19
  from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
19
20
  from enterprise_data.api.v1.serializers import (
20
- AdvanceAnalyticsEnrollmentSerializer,
21
21
  AdvanceAnalyticsEnrollmentStatsSerializer,
22
+ AdvanceAnalyticsQueryParamSerializer,
22
23
  )
23
24
  from enterprise_data.renderers import IndividualEnrollmentsCSVRenderer
24
- from enterprise_data.utils import date_filter
25
+ from enterprise_data.utils import date_filter, timer
25
26
 
26
-
27
- def fetch_enrollments_cache_expiry_timestamp():
28
- """Calculate cache expiry timestamp"""
29
- # TODO: Implement correct cache expiry logic for `enrollments` data.
30
- # Current cache expiry logic is based on `enterprise_learner_enrollment` table,
31
- # Which has nothing to do with the `enrollments` data. Instead cache expiry should
32
- # be based on `fact_enrollment_admin_dash` table. Currently we have no timestamp in
33
- # `fact_enrollment_admin_dash` table that can be used for cache expiry. Add a new
34
- # column in the table for this purpose and then use that column for cache expiry.
35
- last_updated_at = fetch_max_enrollment_datetime()
36
- cache_expiry = (
37
- last_updated_at + timedelta(days=1) if last_updated_at else datetime.now()
38
- )
39
- return cache_expiry
27
+ LOGGER = getLogger(__name__)
40
28
 
41
29
 
42
30
  class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
@@ -50,7 +38,7 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
50
38
  @permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
51
39
  def get(self, request, enterprise_uuid):
52
40
  """Get individual enrollments data"""
53
- serializer = AdvanceAnalyticsEnrollmentSerializer(data=request.GET)
41
+ serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
54
42
  serializer.is_valid(raise_exception=True)
55
43
 
56
44
  cache_expiry = fetch_enrollments_cache_expiry_timestamp()
@@ -59,7 +47,14 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
59
47
  # get values from query params or use default values
60
48
  start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
61
49
  end_date = serializer.data.get('end_date', datetime.now())
62
- csv_type = request.query_params.get('csv_type')
50
+ response_type = request.query_params.get('response_type', ResponseType.JSON.value)
51
+
52
+ LOGGER.info(
53
+ "Individual enrollments data requested for enterprise [%s] from [%s] to [%s]",
54
+ enterprise_uuid,
55
+ start_date,
56
+ end_date,
57
+ )
63
58
 
64
59
  # filter enrollments by date
65
60
  enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
@@ -77,11 +72,19 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
77
72
  enrollments["enterprise_enrollment_date"] = enrollments["enterprise_enrollment_date"].dt.date
78
73
  enrollments = enrollments.sort_values(by="enterprise_enrollment_date", ascending=False).reset_index(drop=True)
79
74
 
80
- if csv_type == ENROLLMENT_CSV.INDIVIDUAL_ENROLLMENTS.value:
75
+ LOGGER.info(
76
+ "Individual enrollments data prepared for enterprise [%s] from [%s] to [%s]",
77
+ enterprise_uuid,
78
+ start_date,
79
+ end_date,
80
+ )
81
+
82
+ if response_type == ResponseType.CSV.value:
83
+ filename = f"""individual_enrollments, {start_date} - {end_date}.csv"""
81
84
  return StreamingHttpResponse(
82
85
  IndividualEnrollmentsCSVRenderer().render(self._stream_serialized_data(enrollments)),
83
86
  content_type="text/csv",
84
- headers={"Content-Disposition": 'attachment; filename="individual_enrollments.csv"'},
87
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'},
85
88
  )
86
89
 
87
90
  paginator = self.pagination_class()
@@ -121,51 +124,60 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
121
124
  # get values from query params or use default
122
125
  start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
123
126
  end_date = serializer.data.get('end_date', datetime.now())
124
- granularity = serializer.data.get('granularity', GRANULARITY.DAILY.value)
125
- calculation = serializer.data.get('calculation', CALCULATION.TOTAL.value)
126
- csv_type = serializer.data.get('csv_type')
127
-
128
- if csv_type is None:
129
- data = {
130
- "enrollments_over_time": self.construct_enrollments_over_time(
131
- enrollments_df.copy(),
132
- start_date,
133
- end_date,
134
- granularity,
135
- calculation,
136
- ),
137
- "top_courses_by_enrollments": self.construct_top_courses_by_enrollments(
138
- enrollments_df.copy(),
139
- start_date,
140
- end_date,
141
- ),
142
- "top_subjects_by_enrollments": self.construct_top_subjects_by_enrollments(
143
- enrollments_df.copy(),
144
- start_date,
145
- end_date,
146
- ),
147
- }
127
+ granularity = serializer.data.get('granularity', Granularity.DAILY.value)
128
+ calculation = serializer.data.get('calculation', Calculation.TOTAL.value)
129
+ response_type = serializer.data.get('response_type', ResponseType.JSON.value)
130
+ chart_type = serializer.data.get('chart_type')
131
+
132
+ # TODO: Add validation that if response_type is CSV then chart_type must be provided
133
+
134
+ if response_type == ResponseType.JSON.value:
135
+ with timer('construct_enrollment_all_stats'):
136
+ data = {
137
+ "enrollments_over_time": self.construct_enrollments_over_time(
138
+ enrollments_df.copy(),
139
+ start_date,
140
+ end_date,
141
+ granularity,
142
+ calculation,
143
+ ),
144
+ "top_courses_by_enrollments": self.construct_top_courses_by_enrollments(
145
+ enrollments_df.copy(),
146
+ start_date,
147
+ end_date,
148
+ ),
149
+ "top_subjects_by_enrollments": self.construct_top_subjects_by_enrollments(
150
+ enrollments_df.copy(),
151
+ start_date,
152
+ end_date,
153
+ ),
154
+ }
148
155
  return Response(data)
149
- elif csv_type == ENROLLMENT_CSV.ENROLLMENTS_OVER_TIME.value:
150
- return self.construct_enrollments_over_time_csv(
151
- enrollments_df.copy(),
152
- start_date,
153
- end_date,
154
- granularity,
155
- calculation,
156
- )
157
- elif csv_type == ENROLLMENT_CSV.TOP_COURSES_BY_ENROLLMENTS.value:
158
- return self.construct_top_courses_by_enrollments_csv(
159
- enrollments_df.copy(),
160
- start_date,
161
- end_date,
162
- )
163
- elif csv_type == ENROLLMENT_CSV.TOP_SUBJECTS_BY_ENROLLMENTS.value:
164
- return self.construct_top_subjects_by_enrollments_csv(
165
- enrollments_df.copy(),
166
- start_date,
167
- end_date,
168
- )
156
+
157
+ if response_type == ResponseType.CSV.value:
158
+ if chart_type == EnrollmentChart.ENROLLMENTS_OVER_TIME.value:
159
+ with timer('construct_enrollments_over_time_csv'):
160
+ return self.construct_enrollments_over_time_csv(
161
+ enrollments_df.copy(),
162
+ start_date,
163
+ end_date,
164
+ granularity,
165
+ calculation,
166
+ )
167
+ elif chart_type == EnrollmentChart.TOP_COURSES_BY_ENROLLMENTS.value:
168
+ with timer('construct_top_courses_by_enrollments_csv'):
169
+ return self.construct_top_courses_by_enrollments_csv(
170
+ enrollments_df.copy(),
171
+ start_date,
172
+ end_date,
173
+ )
174
+ elif chart_type == EnrollmentChart.TOP_SUBJECTS_BY_ENROLLMENTS.value:
175
+ with timer('construct_top_subjects_by_enrollments_csv'):
176
+ return self.construct_top_subjects_by_enrollments_csv(
177
+ enrollments_df.copy(),
178
+ start_date,
179
+ end_date,
180
+ )
169
181
 
170
182
  def enrollments_over_time_common(self, enrollments_df, start_date, end_date, granularity, calculation):
171
183
  """
@@ -175,8 +187,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
175
187
  enrollments_df {DataFrame} -- DataFrame of enrollments
176
188
  start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
177
189
  end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
178
- granularity {str} -- Granularity of the data. One of GRANULARITY choices
179
- calculation {str} -- Calculation of the data. One of CALCULATION choices
190
+ granularity {str} -- Granularity of the data. One of Granularity choices
191
+ calculation {str} -- Calculation of the data. One of Calculation choices
180
192
  """
181
193
  # filter enrollments by date
182
194
  enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
@@ -202,8 +214,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
202
214
  enrollments_df {DataFrame} -- DataFrame of enrollments
203
215
  start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
204
216
  end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
205
- granularity {str} -- Granularity of the data. One of GRANULARITY choices
206
- calculation {str} -- Calculation of the data. One of CALCULATION choices
217
+ granularity {str} -- Granularity of the data. One of Granularity choices
218
+ calculation {str} -- Calculation of the data. One of Calculation choices
207
219
  """
208
220
  enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
209
221
 
@@ -218,8 +230,8 @@ class AdvanceAnalyticsEnrollmentStatsView(APIView):
218
230
  enrollments_df {DataFrame} -- DataFrame of enrollments
219
231
  start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
220
232
  end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
221
- granularity {str} -- Granularity of the data. One of GRANULARITY choices
222
- calculation {str} -- Calculation of the data. One of CALCULATION choices
233
+ granularity {str} -- Granularity of the data. One of Granularity choices
234
+ calculation {str} -- Calculation of the data. One of Calculation choices
223
235
  """
224
236
  enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
225
237
 
@@ -0,0 +1,141 @@
1
+ """Advance Analytics for Leaderboard"""
2
+ from datetime import datetime
3
+ from logging import getLogger
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from edx_rbac.decorators import permission_required
8
+ from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
9
+ from rest_framework.views import APIView
10
+
11
+ from django.http import StreamingHttpResponse
12
+
13
+ from enterprise_data.admin_analytics.constants import ResponseType
14
+ from enterprise_data.admin_analytics.utils import (
15
+ fetch_and_cache_engagements_data,
16
+ fetch_and_cache_enrollments_data,
17
+ fetch_engagements_cache_expiry_timestamp,
18
+ fetch_enrollments_cache_expiry_timestamp,
19
+ )
20
+ from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
21
+ from enterprise_data.api.v1.serializers import AdvanceAnalyticsQueryParamSerializer
22
+ from enterprise_data.renderers import LeaderboardCSVRenderer
23
+ from enterprise_data.utils import date_filter
24
+
25
+ LOGGER = getLogger(__name__)
26
+
27
+
28
+ class AdvanceAnalyticsLeaderboardView(APIView):
29
+ """
30
+ API for getting the advance analytics leaderboard data.
31
+ """
32
+ authentication_classes = (JwtAuthentication,)
33
+ pagination_class = AdvanceAnalyticsPagination
34
+ http_method_names = ['get']
35
+
36
+ @permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
37
+ def get(self, request, enterprise_uuid):
38
+ """Get leaderboard data"""
39
+ serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
40
+ serializer.is_valid(raise_exception=True)
41
+
42
+ enrollments_cache_expiry = fetch_enrollments_cache_expiry_timestamp()
43
+ enrollments_df = fetch_and_cache_enrollments_data(enterprise_uuid, enrollments_cache_expiry)
44
+
45
+ engagements_cache_expiry = fetch_engagements_cache_expiry_timestamp()
46
+ engagements_df = fetch_and_cache_engagements_data(enterprise_uuid, engagements_cache_expiry)
47
+
48
+ start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
49
+ end_date = serializer.data.get('end_date', datetime.now())
50
+ response_type = serializer.data.get('response_type', ResponseType.JSON.value)
51
+
52
+ LOGGER.info(
53
+ "Leaderboard data requested for enterprise [%s] from [%s] to [%s]",
54
+ enterprise_uuid,
55
+ start_date,
56
+ end_date,
57
+ )
58
+
59
+ # only include learners who have passed the course
60
+ enrollments_df = enrollments_df[enrollments_df["has_passed"] == 1]
61
+
62
+ # filter enrollments by date
63
+ enrollments_df = date_filter(start_date, end_date, enrollments_df, "passed_date")
64
+
65
+ completions = enrollments_df.groupby(["email"]).size().reset_index()
66
+ completions.columns = ["email", "course_completions"]
67
+
68
+ # filter engagements by date
69
+ engagements_df = date_filter(start_date, end_date, engagements_df, "activity_date")
70
+
71
+ engage = (
72
+ engagements_df.groupby(["email"])
73
+ .agg({"is_engaged": ["sum"], "learning_time_seconds": ["sum"]})
74
+ .reset_index()
75
+ )
76
+ engage.columns = ["email", "daily_sessions", "learning_time_seconds"]
77
+ engage["learning_time_hours"] = round(
78
+ engage["learning_time_seconds"].astype("float") / 60 / 60, 1
79
+ )
80
+
81
+ # if daily_sessions is 0, set average_session_length to 0 becuase otherwise it will be `inf`
82
+ engage["average_session_length"] = np.where(
83
+ engage["daily_sessions"] == 0,
84
+ 0,
85
+ round(engage["learning_time_hours"] / engage["daily_sessions"].astype("float"), 1)
86
+ )
87
+
88
+ leaderboard_df = engage.merge(completions, on="email", how="left")
89
+ leaderboard_df = leaderboard_df.sort_values(
90
+ by=["learning_time_hours", "daily_sessions", "course_completions"],
91
+ ascending=[False, False, False],
92
+ )
93
+
94
+ # move the aggregated row with email 'null' to the end of the table
95
+ idx = leaderboard_df.index[leaderboard_df['email'] == 'null']
96
+ leaderboard_df.loc[idx, 'email'] = 'learners who have not shared consent'
97
+ leaderboard_df = pd.concat([leaderboard_df.drop(idx), leaderboard_df.loc[idx]])
98
+
99
+ # convert `nan` values to `None` because `nan` is not JSON serializable
100
+ leaderboard_df = leaderboard_df.replace(np.nan, None)
101
+
102
+ LOGGER.info(
103
+ "Leaderboard data prepared for enterprise [%s] from [%s] to [%s]",
104
+ enterprise_uuid,
105
+ start_date,
106
+ end_date,
107
+ )
108
+
109
+ if response_type == ResponseType.CSV.value:
110
+ filename = f"""Leaderboard, {start_date} - {end_date}.csv"""
111
+ leaderboard_df = leaderboard_df[
112
+ [
113
+ "email",
114
+ "learning_time_hours",
115
+ "daily_sessions",
116
+ "average_session_length",
117
+ "course_completions",
118
+ ]
119
+ ]
120
+ return StreamingHttpResponse(
121
+ LeaderboardCSVRenderer().render(self._stream_serialized_data(leaderboard_df)),
122
+ content_type="text/csv",
123
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'},
124
+ )
125
+
126
+ paginator = self.pagination_class()
127
+ page = paginator.paginate_queryset(leaderboard_df, request)
128
+ serialized_data = page.data.to_dict(orient='records')
129
+ response = paginator.get_paginated_response(serialized_data)
130
+
131
+ return response
132
+
133
+ def _stream_serialized_data(self, leaderboard_df, chunk_size=50000):
134
+ """
135
+ Stream the serialized data.
136
+ """
137
+ total_rows = leaderboard_df.shape[0]
138
+ for start_index in range(0, total_rows, chunk_size):
139
+ end_index = min(start_index + chunk_size, total_rows)
140
+ chunk = leaderboard_df.iloc[start_index:end_index]
141
+ yield from chunk.to_dict(orient='records')
@@ -27,7 +27,7 @@ from enterprise_data.models import (
27
27
  EnterpriseAdminSummarizeInsights,
28
28
  EnterpriseExecEdLCModulePerformance,
29
29
  )
30
- from enterprise_data.utils import date_filter
30
+ from enterprise_data.utils import date_filter, timer
31
31
 
32
32
  from .base import EnterpriseViewSetMixin
33
33
 
@@ -211,24 +211,25 @@ class EnterpriseAdminAnalyticsSkillsView(APIView):
211
211
  csv_data.to_csv(path_or_buf=response, index=False)
212
212
  return response
213
213
 
214
- top_skills = get_skills_chart_data(
215
- chart_type=ChartType.BUBBLE,
216
- start_date=start_date,
217
- end_date=end_date,
218
- skills=skills,
219
- )
220
- top_skills_enrollments = get_skills_chart_data(
221
- chart_type=ChartType.TOP_SKILLS_ENROLLMENT,
222
- start_date=start_date,
223
- end_date=end_date,
224
- skills=skills,
225
- )
226
- top_skills_by_completions = get_skills_chart_data(
227
- chart_type=ChartType.TOP_SKILLS_COMPLETION,
228
- start_date=start_date,
229
- end_date=end_date,
230
- skills=skills,
231
- )
214
+ with timer('skills_all_charts_data'):
215
+ top_skills = get_skills_chart_data(
216
+ chart_type=ChartType.BUBBLE,
217
+ start_date=start_date,
218
+ end_date=end_date,
219
+ skills=skills,
220
+ )
221
+ top_skills_enrollments = get_skills_chart_data(
222
+ chart_type=ChartType.TOP_SKILLS_ENROLLMENT,
223
+ start_date=start_date,
224
+ end_date=end_date,
225
+ skills=skills,
226
+ )
227
+ top_skills_by_completions = get_skills_chart_data(
228
+ chart_type=ChartType.TOP_SKILLS_COMPLETION,
229
+ start_date=start_date,
230
+ end_date=end_date,
231
+ skills=skills,
232
+ )
232
233
 
233
234
  response_data = {
234
235
  "top_skills": top_skills.to_dict(orient="records"),
@@ -1,6 +1,7 @@
1
1
  """Views for enterprise admin completions analytics."""
2
2
  import datetime
3
3
  from datetime import datetime, timedelta
4
+ from logging import getLogger
4
5
 
5
6
  from edx_rbac.decorators import permission_required
6
7
  from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
@@ -18,12 +19,14 @@ from enterprise_data.admin_analytics.completions_utils import (
18
19
  get_top_courses_by_completions,
19
20
  get_top_subjects_by_completions,
20
21
  )
21
- from enterprise_data.admin_analytics.constants import CALCULATION, GRANULARITY
22
+ from enterprise_data.admin_analytics.constants import Calculation, Granularity
22
23
  from enterprise_data.admin_analytics.data_loaders import fetch_max_enrollment_datetime
23
24
  from enterprise_data.admin_analytics.utils import ChartType, fetch_and_cache_enrollments_data
24
25
  from enterprise_data.api.v1 import serializers
25
26
  from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
26
- from enterprise_data.utils import date_filter
27
+ from enterprise_data.utils import date_filter, timer
28
+
29
+ LOGGER = getLogger(__name__)
27
30
 
28
31
 
29
32
  class EnterrpiseAdminCompletionsStatsView(APIView):
@@ -67,39 +70,43 @@ class EnterrpiseAdminCompletionsStatsView(APIView):
67
70
  csv_data = {}
68
71
 
69
72
  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
- )
73
+ with timer('completions_over_time_csv_data'):
74
+ csv_data = get_csv_data_for_completions_over_time(
75
+ start_date=start_date,
76
+ end_date=end_date,
77
+ enrollments=enrollments.copy(),
78
+ date_agg=serializer.data.get('granularity', Granularity.DAILY.value),
79
+ calc=serializer.data.get('calculation', Calculation.TOTAL.value),
80
+ )
77
81
  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
- )
82
+ with timer('top_courses_by_completions_csv_data'):
83
+ csv_data = get_csv_data_for_top_courses_by_completions(
84
+ start_date=start_date, end_date=end_date, enrollments=enrollments.copy()
85
+ )
81
86
  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
- )
87
+ with timer('top_subjects_by_completions_csv_data'):
88
+ csv_data = get_csv_data_for_top_subjects_by_completions(
89
+ start_date=start_date, end_date=end_date, enrollments=enrollments.copy()
90
+ )
85
91
  filename = csv_data['filename']
86
92
  response['Content-Disposition'] = f'attachment; filename="{filename}"'
87
93
  csv_data['data'].to_csv(path_or_buf=response)
88
94
  return response
89
95
 
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
- )
96
+ with timer('completions_all_charts_data'):
97
+ completions_over_time = get_completions_over_time(
98
+ start_date=start_date,
99
+ end_date=end_date,
100
+ dff=enrollments.copy(),
101
+ date_agg=serializer.data.get('granularity', Granularity.DAILY.value),
102
+ calc=serializer.data.get('calculation', Calculation.TOTAL.value),
103
+ )
104
+ top_courses_by_completions = get_top_courses_by_completions(
105
+ start_date=start_date, end_date=end_date, dff=enrollments.copy()
106
+ )
107
+ top_subjects_by_completions = get_top_subjects_by_completions(
108
+ start_date=start_date, end_date=end_date, dff=enrollments.copy()
109
+ )
103
110
 
104
111
  return Response(
105
112
  data={
@@ -153,6 +160,13 @@ class EnterrpiseAdminCompletionsView(APIView):
153
160
  )
154
161
  end_date = serializer.data.get('end_date', datetime.now())
155
162
 
163
+ LOGGER.info(
164
+ "Completions data requested for enterprise [%s] from [%s] to [%s]",
165
+ enterprise_id,
166
+ start_date,
167
+ end_date,
168
+ )
169
+
156
170
  dff = enrollments[enrollments['has_passed'] == 1]
157
171
 
158
172
  # Date filtering
@@ -162,6 +176,13 @@ class EnterrpiseAdminCompletionsView(APIView):
162
176
  dff['passed_date'] = dff['passed_date'].dt.date
163
177
  dff = dff.sort_values(by="passed_date", ascending=False).reset_index(drop=True)
164
178
 
179
+ LOGGER.info(
180
+ "Completions data prepared for enterprise [%s] from [%s] to [%s]",
181
+ enterprise_id,
182
+ start_date,
183
+ end_date,
184
+ )
185
+
165
186
  if serializer.data.get('response_type') == 'csv':
166
187
  response = HttpResponse(content_type='text/csv')
167
188
  filename = f"Individual Completions, {start_date} - {end_date}.csv"
@@ -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
+ ]