edx-enterprise-data 8.11.1__py3-none-any.whl → 8.12.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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edx-enterprise-data
3
- Version: 8.11.1
3
+ Version: 8.12.1
4
4
  Summary: Enterprise Reporting
5
5
  Home-page: https://github.com/openedx/edx-enterprise-data
6
6
  Author: edX
@@ -1,4 +1,4 @@
1
- enterprise_data/__init__.py,sha256=XT15XKKqKMAKtb0mKrGHjwWzDy3Y_AMCd_U5UKSh5Rc,124
1
+ enterprise_data/__init__.py,sha256=8TjCXGqGwt4y6eG7nZC6OYCp9Dut66Hr_TTySgVt3PE,124
2
2
  enterprise_data/apps.py,sha256=aF6hZwDfI2oWj95tUTm_2ikHueQj-jLj-u0GrgzpsQI,414
3
3
  enterprise_data/clients.py,sha256=GvQupy5TVYfO_IKC3yzXSAgNP54r-PtIjidM5ws9Iks,3947
4
4
  enterprise_data/constants.py,sha256=uCKjfpdlMYFZJsAj3n9RMw4Cmg5_6s3NuwocO-fch3s,238
@@ -15,14 +15,14 @@ enterprise_data/admin_analytics/constants.py,sha256=7WturLuMISekgcHHlgj45PPdPrDT
15
15
  enterprise_data/admin_analytics/data_loaders.py,sha256=4PSsIveNBtpFqwrLUjTl5oywHVYFAMdZrNRHRGMk4XY,6116
16
16
  enterprise_data/admin_analytics/utils.py,sha256=CQuTlg36AALJiopp4us-JN8oTXsw-jDXSJenbphLDME,12270
17
17
  enterprise_data/admin_analytics/database/__init__.py,sha256=vNSWKf2VV5xMegN7htJJtxtQEb0ASLC6frE2w0ZpYpE,104
18
- enterprise_data/admin_analytics/database/utils.py,sha256=Zl-W_w5SVb-QpIwWxbHr0w7z4L2ZFeruJSygWh8g7II,1408
18
+ enterprise_data/admin_analytics/database/utils.py,sha256=5u-d6ZQW95mF_r4bH8Xdi7DgpYAuDFOG_q0P-bjKXHU,1712
19
19
  enterprise_data/admin_analytics/database/queries/__init__.py,sha256=IC5TLOr_GnydbrVbl2mWhwO3aUbYeHuDmfPTLmwGhZA,218
20
20
  enterprise_data/admin_analytics/database/queries/fact_engagement_admin_dash.py,sha256=fq01Ni_sKnvSRoiPQfTnJ8TtRePQe5MBLmI5CpVy36o,747
21
- enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py,sha256=X1muOEhEvqD-tyhrVEQkPrf8_7g7SPaJuMwriUPo3jw,2062
21
+ enterprise_data/admin_analytics/database/queries/fact_enrollment_admin_dash.py,sha256=uuhX3OIB5Cp0-5uxN604HNEUuzb3s9nH3VR4CiEXs18,5388
22
22
  enterprise_data/admin_analytics/database/tables/__init__.py,sha256=3ECKlKlz1wuTER5M57CMFvmLFp0oCnVQXAUeTN96Bs0,213
23
23
  enterprise_data/admin_analytics/database/tables/base.py,sha256=1KyKsC18pW3m-5U-T6pdt5rIwsz6Wp3QFFbD3r6L6YQ,395
24
24
  enterprise_data/admin_analytics/database/tables/fact_engagement_admin_dash.py,sha256=EsG8KgRW84wtA_nJuUknjLYlDtaPSJf_9mWdkO2Bj2I,1293
25
- enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py,sha256=WiFWVF4TsDhLlDQr-3FuqTrzFy7Qv1Y5EnwZXYhdTrM,2962
25
+ enterprise_data/admin_analytics/database/tables/fact_enrollment_admin_dash.py,sha256=ix5QvPnrUZMVs_Fdt742i9PAmrQTXuqHlfW3PJhSQWo,7282
26
26
  enterprise_data/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  enterprise_data/api/urls.py,sha256=POqc_KATHdnpMf9zHtpO46pKD5KAlAExtx7G6iylLcU,273
28
28
  enterprise_data/api/v0/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
@@ -31,13 +31,13 @@ enterprise_data/api/v0/urls.py,sha256=vzJjqIo_S3AXWs9Us8XTaJc3FnxLbYzAkmLyuDQqum
31
31
  enterprise_data/api/v0/views.py,sha256=4RslZ4NZOU-844bnebEQ71ji2utRY7jEijqC45oQQD0,14380
32
32
  enterprise_data/api/v1/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
33
33
  enterprise_data/api/v1/paginators.py,sha256=f0xsilLaU94jSBltJk46tR1rLEIt7YrqSzMAAVtPXjA,3592
34
- enterprise_data/api/v1/serializers.py,sha256=Kk4zuRNcr4bfMYIwi2iXQAr_NM3OXuXNfxfrz7xO2iE,13194
35
- enterprise_data/api/v1/urls.py,sha256=nFcbPSfAIKW6Eiwd1vN0WMqdtBfh9_bBqAFCVrzRm5I,4091
34
+ enterprise_data/api/v1/serializers.py,sha256=FEuH-_iTlKFJoxM8ABzEYHDAWHsgSYfdQd2WSPdXoeE,13329
35
+ enterprise_data/api/v1/urls.py,sha256=JzaaVsHEAELMLFdhR60_8LRLb9klDeA4zdH1dLxnQEo,4048
36
36
  enterprise_data/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  enterprise_data/api/v1/views/analytics_engagements.py,sha256=8H3Fk-hTqJaU3H5Lpu1kFWq7p7xDp1pjge-sK7osDbg,17553
38
- enterprise_data/api/v1/views/analytics_enrollments.py,sha256=trC92GOej6vUoju53KT3dPEDnoPHdukfXEIXPThz76A,16886
38
+ enterprise_data/api/v1/views/analytics_enrollments.py,sha256=uVc36C0s9y1dPBfbYwFhXLqPlExvQvZeiJ4C45lb4ZQ,6447
39
39
  enterprise_data/api/v1/views/analytics_leaderboard.py,sha256=2DALqzUIbe4-ZGgHHIkYAKJ5L1ik2ruPtQNYtTdPba4,5974
40
- enterprise_data/api/v1/views/base.py,sha256=FTAxlz5EzvAY657wzVgzhJPFSCHHzct7IDcvm71Smt8,866
40
+ enterprise_data/api/v1/views/base.py,sha256=Kkmd5zgEBAhvwS_GoGXSK6lgbDNwSPioYn-QbnizI3w,3416
41
41
  enterprise_data/api/v1/views/enterprise_admin.py,sha256=d79WkyBbqLk6pKGWWrnbYdrbOgQztP03Q7NKRA5tZB4,8639
42
42
  enterprise_data/api/v1/views/enterprise_completions.py,sha256=bJG2ZtTbLyiBrj64iJHQNHEKLrJCzl9OuJ7nDtw-9aY,8377
43
43
  enterprise_data/api/v1/views/enterprise_learner.py,sha256=yABjJje3CT8I8YOhWr1_tTkdKtnGJom8eu3EFz_-0BU,18517
@@ -113,10 +113,10 @@ enterprise_data/tests/test_models.py,sha256=MWBY-LY5TPBjZ4GlvpM-h4W-BvRKr2Rml8Bz
113
113
  enterprise_data/tests/test_utils.py,sha256=vbmYM7DMN-lHS2p4yaa0Yd6uSGXd2qoZRDE9X3J4Sec,18385
114
114
  enterprise_data/tests/test_views.py,sha256=UvDRNTxruy5zBK_KgUy2cBMbwlaTW_vkM0-TCXbQZiY,69667
115
115
  enterprise_data/tests/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
- enterprise_data/tests/admin_analytics/mock_analytics_data.py,sha256=LoYAh5cn_4rv2stGcMP5PELv2l7Eu2DTKG-v7viNTCs,20266
116
+ enterprise_data/tests/admin_analytics/mock_analytics_data.py,sha256=VZHhMKSQn5S-o1wNReSVyEnI_-6EkW-TUV5EvXCl5is,19536
117
117
  enterprise_data/tests/admin_analytics/mock_enrollments.py,sha256=LfuMo9Kn-OQD4z42G3BRuM5MXUUXXlaAMhTqfJf46XE,7266
118
118
  enterprise_data/tests/admin_analytics/test_analytics_engagements.py,sha256=KPXtBPaAOrzfff7W-xERSGx9KtZAJndLbIJx3gopSnE,15689
119
- enterprise_data/tests/admin_analytics/test_analytics_enrollments.py,sha256=UdKRkP6BNbsSo-gm0YCoddT-ReUMI1x9E6HNLSHT7pY,15177
119
+ enterprise_data/tests/admin_analytics/test_analytics_enrollments.py,sha256=GePv5E8BRGRWUHUwGaXvYIsN3dtDpNXUh-yfW5iBTi4,13781
120
120
  enterprise_data/tests/admin_analytics/test_analytics_leaderboard.py,sha256=VSEyDAHfWBJvqmx9yzd4NnPAqK3TqaKrMBWswMAdzfU,6206
121
121
  enterprise_data/tests/admin_analytics/test_data_loaders.py,sha256=o3denJ4aUS1pI5Crksl4C6m-NtCBm8ynoHBnLkf-v2U,4641
122
122
  enterprise_data/tests/admin_analytics/test_enterprise_completions.py,sha256=afkHQFy4bvqZ0pq5Drl1t2nv8zxbgca2jzOQbihlPG0,7359
@@ -169,8 +169,8 @@ enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=WtL-RqGgu2x5PP
169
169
  enterprise_reporting/tests/test_utils.py,sha256=Zt_TA0LVb-B6fQGkUkAKKVlUKKnQh8jnw1US1jKe7g8,9493
170
170
  enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
171
171
  enterprise_reporting/tests/utils.py,sha256=xms2LM7DV3wczXEfctOK1ddel1EE0J_YSr17UzbCDy4,1401
172
- edx_enterprise_data-8.11.1.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
173
- edx_enterprise_data-8.11.1.dist-info/METADATA,sha256=27UVJwZuQylIJzEZ-R-VWbzisk0jOxS04oHJNrjLQTw,1570
174
- edx_enterprise_data-8.11.1.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
175
- edx_enterprise_data-8.11.1.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
176
- edx_enterprise_data-8.11.1.dist-info/RECORD,,
172
+ edx_enterprise_data-8.12.1.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
173
+ edx_enterprise_data-8.12.1.dist-info/METADATA,sha256=zAJ9YvmaCGHDChLF25Cf1zmK3Okmbq3qIG7UJ6E7xuw,1570
174
+ edx_enterprise_data-8.12.1.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
175
+ edx_enterprise_data-8.12.1.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
176
+ edx_enterprise_data-8.12.1.dist-info/RECORD,,
@@ -2,4 +2,4 @@
2
2
  Enterprise data api application. This Django app exposes API endpoints used by enterprises.
3
3
  """
4
4
 
5
- __version__ = "8.11.1"
5
+ __version__ = "8.12.1"
@@ -7,6 +7,30 @@ class FactEnrollmentAdminDashQueries:
7
7
  """
8
8
  Queries related to the fact_enrollment_admin_dash table.
9
9
  """
10
+ @staticmethod
11
+ def get_enrollment_count_query():
12
+ """
13
+ Get the query to fetch the total number of enrollments for an enterprise customer.
14
+ """
15
+ return """
16
+ SELECT count(*)
17
+ FROM fact_enrollment_admin_dash
18
+ WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
19
+ enterprise_enrollment_date BETWEEN %(start_date)s AND %(end_date)s;
20
+ """
21
+
22
+ @staticmethod
23
+ def get_all_enrollments_query():
24
+ """
25
+ Get the query to fetch all enrollments.
26
+ """
27
+ return """
28
+ SELECT email, course_title, course_subject, enroll_type, enterprise_enrollment_date
29
+ FROM fact_enrollment_admin_dash
30
+ WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
31
+ enterprise_enrollment_date BETWEEN %(start_date)s AND %(end_date)s
32
+ ORDER BY ENTERPRISE_ENROLLMENT_DATE DESC LIMIT %(limit)s OFFSET %(offset)s
33
+ """
10
34
 
11
35
  @staticmethod
12
36
  def get_enrollment_date_range_query():
@@ -59,3 +83,55 @@ class FactEnrollmentAdminDashQueries:
59
83
  WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
60
84
  activity_date BETWEEN %(start_date)s AND %(end_date)s;
61
85
  """
86
+
87
+ @staticmethod
88
+ def get_top_courses_enrollments_query(record_count=20):
89
+ """
90
+ Get the query to fetch the enrollment count by courses.
91
+
92
+ Query will fetch the top N courses by enrollment count. Where N is the value of record_count.
93
+
94
+ Arguments:
95
+ record_count (int): Number of records to fetch.
96
+ """
97
+ # Some local environments raise error when course_title is added in SELECT without GROUP BY.
98
+ # If you face this issue, you can remove course_title from SELECT.
99
+ return f"""
100
+ SELECT course_key, course_title , enroll_type, count(course_key) as enrollment_count
101
+ FROM fact_enrollment_admin_dash
102
+ WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
103
+ enterprise_enrollment_date BETWEEN %(start_date)s AND %(end_date)s
104
+ GROUP BY course_key, enroll_type ORDER BY enrollment_count DESC LIMIT {record_count};
105
+ """
106
+
107
+ @staticmethod
108
+ def get_top_subjects_by_enrollments_query(record_count=20):
109
+ """
110
+ Get the query to fetch the enrollment count by subjects.
111
+
112
+ Query will fetch the top N subjects by enrollment count. Where N is the value of record_count.
113
+
114
+ Arguments:
115
+ record_count (int): Number of records to fetch.
116
+ """
117
+ return f"""
118
+ SELECT course_subject, enroll_type, count(course_subject) enrollment_count
119
+ FROM fact_enrollment_admin_dash
120
+ WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
121
+ enterprise_enrollment_date BETWEEN %(start_date)s AND %(end_date)s
122
+ GROUP BY course_subject, enroll_type ORDER BY enrollment_count DESC LIMIT {record_count};
123
+ """
124
+
125
+ @staticmethod
126
+ def get_enrolment_time_series_data_query():
127
+ """
128
+ Get the query to fetch the enrollment time series data with daily aggregation.
129
+ """
130
+ return """
131
+ SELECT enterprise_enrollment_date, enroll_type, COUNT(*) as enrollment_count
132
+ FROM fact_enrollment_admin_dash
133
+ WHERE enterprise_customer_uuid=%(enterprise_customer_uuid)s AND
134
+ enterprise_enrollment_date BETWEEN %(start_date)s AND %(end_date)s
135
+ GROUP BY enterprise_enrollment_date, enroll_type
136
+ ORDER BY enterprise_enrollment_date;
137
+ """
@@ -15,6 +15,56 @@ class FactEnrollmentAdminDashTable(BaseTable):
15
15
  """
16
16
  queries = FactEnrollmentAdminDashQueries()
17
17
 
18
+ def get_enrollment_count(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
19
+ """
20
+ Get the total number of enrollments for the given enterprise customer.
21
+
22
+ Arguments:
23
+ enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
24
+ start_date (date): The start date.
25
+ end_date (date): The end date.
26
+
27
+ Returns:
28
+ (int): The total number of enrollments.
29
+ """
30
+ results = run_query(
31
+ query=self.queries.get_enrollment_count_query(),
32
+ params={
33
+ 'enterprise_customer_uuid': enterprise_customer_uuid,
34
+ 'start_date': start_date,
35
+ 'end_date': end_date,
36
+ }
37
+ )
38
+ return results[0][0]
39
+
40
+ def get_all_enrollments(
41
+ self, enterprise_customer_uuid: UUID, start_date: date, end_date: date, limit: int, offset: int
42
+ ):
43
+ """
44
+ Get all enrollments for the given enterprise customer.
45
+
46
+ Arguments:
47
+ enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
48
+ start_date (date): The start date.
49
+ end_date (date): The end date.
50
+ limit (int): The maximum number of records to return.
51
+ offset (int): The number of records to skip.
52
+
53
+ Returns:
54
+ list<dict>: A list of dictionaries containing the enrollment data.
55
+ """
56
+ return run_query(
57
+ query=self.queries.get_all_enrollments_query(),
58
+ params={
59
+ 'enterprise_customer_uuid': enterprise_customer_uuid,
60
+ 'start_date': start_date,
61
+ 'end_date': end_date,
62
+ 'limit': limit,
63
+ 'offset': offset,
64
+ },
65
+ as_dict=True,
66
+ )
67
+
18
68
  def get_enrollment_date_range(self, enterprise_customer_uuid: UUID):
19
69
  """
20
70
  Get the enrollment date range for the given enterprise customer.
@@ -83,3 +133,69 @@ class FactEnrollmentAdminDashTable(BaseTable):
83
133
  }
84
134
  )
85
135
  return results[0][0]
136
+
137
+ def get_top_courses_by_enrollments(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
138
+ """
139
+ Get the top courses enrollments for the given enterprise customer.
140
+
141
+ Arguments:
142
+ enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
143
+ start_date (date): The start date.
144
+ end_date (date): The end date.
145
+
146
+ Returns:
147
+ list<dict>: A list of dictionaries containing the course key, course_title and enrollment count.
148
+ """
149
+ return run_query(
150
+ query=self.queries.get_top_courses_enrollments_query(),
151
+ params={
152
+ 'enterprise_customer_uuid': enterprise_customer_uuid,
153
+ 'start_date': start_date,
154
+ 'end_date': end_date,
155
+ },
156
+ as_dict=True,
157
+ )
158
+
159
+ def get_top_subjects_by_enrollments(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
160
+ """
161
+ Get the top subjects by enrollments for the given enterprise customer.
162
+
163
+ Arguments:
164
+ enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
165
+ start_date (date): The start date.
166
+ end_date (date): The end date.
167
+
168
+ Returns:
169
+ list<dict>: A list of dictionaries containing the subject and enrollment count.
170
+ """
171
+ return run_query(
172
+ query=self.queries.get_top_subjects_by_enrollments_query(),
173
+ params={
174
+ 'enterprise_customer_uuid': enterprise_customer_uuid,
175
+ 'start_date': start_date,
176
+ 'end_date': end_date,
177
+ },
178
+ as_dict=True,
179
+ )
180
+
181
+ def get_enrolment_time_series_data(self, enterprise_customer_uuid: UUID, start_date: date, end_date: date):
182
+ """
183
+ Get the enrollment time series data for the given enterprise customer.
184
+
185
+ Arguments:
186
+ enterprise_customer_uuid (UUID): The UUID of the enterprise customer.
187
+ start_date (date): The start date.
188
+ end_date (date): The end date.
189
+
190
+ Returns:
191
+ list<dict>: A list of dictionaries containing the date and enrollment count.
192
+ """
193
+ return run_query(
194
+ query=self.queries.get_enrolment_time_series_data_query(),
195
+ params={
196
+ 'enterprise_customer_uuid': enterprise_customer_uuid,
197
+ 'start_date': start_date,
198
+ 'end_date': end_date,
199
+ },
200
+ as_dict=True,
201
+ )
@@ -30,22 +30,27 @@ def get_db_connection(database=settings.ENTERPRISE_REPORTING_DB_ALIAS):
30
30
 
31
31
 
32
32
  @timeit
33
- def run_query(query, params: dict = None):
33
+ def run_query(query, params: dict = None, as_dict=False):
34
34
  """
35
35
  Run a query on the database and return the results.
36
36
 
37
37
  Arguments:
38
38
  query (str): The query to run.
39
39
  params (dict): The parameters to pass to the query.
40
+ as_dict (bool): When True, return the results as a dictionary.
40
41
 
41
42
  Returns:
42
- (list): The results of the query.
43
+ (list | dict): The results of the query.
43
44
  """
44
45
  try:
45
46
  with closing(get_db_connection()) as connection:
46
47
  with closing(connection.cursor()) as cursor:
47
48
  cursor.execute(query, params=params)
48
- return cursor.fetchall()
49
+ if as_dict:
50
+ columns = [column[0] for column in cursor.description]
51
+ return [dict(zip(columns, row)) for row in cursor.fetchall()]
52
+ else:
53
+ return cursor.fetchall()
49
54
  except Exception:
50
55
  LOGGER.exception(f'[run_query]: run_query failed for query "{query}".')
51
56
  raise
@@ -265,6 +265,8 @@ class AdvanceAnalyticsQueryParamSerializer(serializers.Serializer): # pylint: d
265
265
  granularity = serializers.CharField(required=False)
266
266
  calculation = serializers.CharField(required=False)
267
267
  response_type = serializers.CharField(required=False)
268
+ page = serializers.IntegerField(required=False, min_value=1)
269
+ page_size = serializers.IntegerField(required=False, min_value=2)
268
270
 
269
271
  def validate(self, attrs):
270
272
  """
@@ -15,10 +15,7 @@ from enterprise_data.api.v1.views.analytics_engagements import (
15
15
  AdvanceAnalyticsEngagementStatsView,
16
16
  AdvanceAnalyticsIndividualEngagementsView,
17
17
  )
18
- from enterprise_data.api.v1.views.analytics_enrollments import (
19
- AdvanceAnalyticsEnrollmentStatsView,
20
- AdvanceAnalyticsIndividualEnrollmentsView,
21
- )
18
+ from enterprise_data.api.v1.views.analytics_enrollments import AdvanceAnalyticsEnrollmentsView
22
19
  from enterprise_data.api.v1.views.analytics_leaderboard import AdvanceAnalyticsLeaderboardView
23
20
  from enterprise_data.constants import UUID4_REGEX
24
21
 
@@ -69,12 +66,12 @@ urlpatterns = [
69
66
  ),
70
67
  re_path(
71
68
  fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments/stats$',
72
- AdvanceAnalyticsEnrollmentStatsView.as_view(),
69
+ AdvanceAnalyticsEnrollmentsView.as_view({'get': 'stats'}),
73
70
  name='enterprise-admin-analytics-enrollments-stats'
74
71
  ),
75
72
  re_path(
76
73
  fr'^admin/analytics/(?P<enterprise_uuid>{UUID4_REGEX})/enrollments$',
77
- AdvanceAnalyticsIndividualEnrollmentsView.as_view(),
74
+ AdvanceAnalyticsEnrollmentsView.as_view({'get': 'list'}),
78
75
  name='enterprise-admin-analytics-enrollments'
79
76
  ),
80
77
  re_path(
@@ -1,52 +1,74 @@
1
- """Advance Analytics for Enrollments"""
1
+ """
2
+ Advance Analytics for API endpoints to fetch enterprise enrollments data.
3
+ """
2
4
  from datetime import datetime
3
5
  from logging import getLogger
4
6
 
5
7
  from edx_rbac.decorators import permission_required
6
8
  from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
9
+ from rest_framework.decorators import action
7
10
  from rest_framework.response import Response
8
- from rest_framework.views import APIView
11
+ from rest_framework.viewsets import ViewSet
9
12
 
10
- from django.http import HttpResponse, StreamingHttpResponse
13
+ from django.http import StreamingHttpResponse
11
14
 
12
- from enterprise_data.admin_analytics.constants import Calculation, EnrollmentChart, Granularity, ResponseType
13
- from enterprise_data.admin_analytics.utils import (
14
- calculation_aggregation,
15
- fetch_and_cache_enrollments_data,
16
- fetch_enrollments_cache_expiry_timestamp,
17
- granularity_aggregation,
18
- )
15
+ from enterprise_data.admin_analytics.constants import ResponseType
16
+ from enterprise_data.admin_analytics.database.tables import FactEnrollmentAdminDashTable
19
17
  from enterprise_data.api.v1.paginators import AdvanceAnalyticsPagination
20
18
  from enterprise_data.api.v1.serializers import (
21
19
  AdvanceAnalyticsEnrollmentStatsSerializer,
22
20
  AdvanceAnalyticsQueryParamSerializer,
23
21
  )
22
+ from enterprise_data.api.v1.views.base import AnalyticsPaginationMixin
24
23
  from enterprise_data.renderers import IndividualEnrollmentsCSVRenderer
25
- from enterprise_data.utils import date_filter, timer
24
+ from enterprise_data.utils import timer
26
25
 
27
26
  LOGGER = getLogger(__name__)
28
27
 
29
28
 
30
- class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
29
+ class AdvanceAnalyticsEnrollmentsView(AnalyticsPaginationMixin, ViewSet):
31
30
  """
32
- API for getting the advance analytics individual enrollments data.
31
+ View to handle requests for enterprise enrollments data.
32
+
33
+ Here is the list of URLs that are handled by this view:
34
+ 1. `enterprise_data_api_v1.enterprise-learner-enrollment-list`: Get individual enrollment data.
35
+ 2. `enterprise_data_api_v1.enterprise-learner-enrollment-stats`: Get enrollment stats data.
33
36
  """
34
37
  authentication_classes = (JwtAuthentication,)
35
38
  pagination_class = AdvanceAnalyticsPagination
36
- http_method_names = ['get']
39
+ http_method_names = ('get', )
37
40
 
38
41
  @permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
39
- def get(self, request, enterprise_uuid):
40
- """Get individual enrollments data"""
42
+ def list(self, request, enterprise_uuid):
43
+ """
44
+ Get individual enrollments data for the enterprise.
45
+ """
46
+ # Remove hyphens from the UUID
47
+ enterprise_uuid = enterprise_uuid.replace('-', '')
48
+
41
49
  serializer = AdvanceAnalyticsQueryParamSerializer(data=request.GET)
42
50
  serializer.is_valid(raise_exception=True)
43
-
44
- cache_expiry = fetch_enrollments_cache_expiry_timestamp()
45
- enrollments_df = fetch_and_cache_enrollments_data(enterprise_uuid, cache_expiry)
51
+ min_enrollment_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(
52
+ enterprise_uuid,
53
+ )
46
54
 
47
55
  # get values from query params or use default values
48
- start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
56
+ start_date = serializer.data.get('start_date', min_enrollment_date)
49
57
  end_date = serializer.data.get('end_date', datetime.now())
58
+ page = serializer.data.get('page', 1)
59
+ page_size = serializer.data.get('page_size', 100)
60
+ enrollments = FactEnrollmentAdminDashTable().get_all_enrollments(
61
+ enterprise_customer_uuid=enterprise_uuid,
62
+ start_date=start_date,
63
+ end_date=end_date,
64
+ limit=page_size,
65
+ offset=(page - 1) * page_size,
66
+ )
67
+ total_count = FactEnrollmentAdminDashTable().get_enrollment_count(
68
+ enterprise_customer_uuid=enterprise_uuid,
69
+ start_date=start_date,
70
+ end_date=end_date,
71
+ )
50
72
  response_type = request.query_params.get('response_type', ResponseType.JSON.value)
51
73
 
52
74
  LOGGER.info(
@@ -56,333 +78,75 @@ class AdvanceAnalyticsIndividualEnrollmentsView(APIView):
56
78
  end_date,
57
79
  )
58
80
 
59
- # filter enrollments by date
60
- enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
61
-
62
- # select only the columns that will be in the table.
63
- enrollments = enrollments[
64
- [
65
- "email",
66
- "course_title",
67
- "course_subject",
68
- "enroll_type",
69
- "enterprise_enrollment_date",
70
- ]
71
- ]
72
- enrollments["enterprise_enrollment_date"] = enrollments["enterprise_enrollment_date"].dt.date
73
- enrollments = enrollments.sort_values(by="enterprise_enrollment_date", ascending=False).reset_index(drop=True)
74
-
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
81
  if response_type == ResponseType.CSV.value:
83
82
  filename = f"""individual_enrollments, {start_date} - {end_date}.csv"""
83
+
84
84
  return StreamingHttpResponse(
85
- IndividualEnrollmentsCSVRenderer().render(self._stream_serialized_data(enrollments)),
85
+ IndividualEnrollmentsCSVRenderer().render(self._stream_serialized_data(
86
+ enterprise_uuid, start_date, end_date, total_count
87
+ )),
86
88
  content_type="text/csv",
87
89
  headers={"Content-Disposition": f'attachment; filename="{filename}"'},
88
90
  )
89
91
 
90
- paginator = self.pagination_class()
91
- page = paginator.paginate_queryset(enrollments, request)
92
- serialized_data = page.data.to_dict(orient='records')
93
- response = paginator.get_paginated_response(serialized_data)
94
-
95
- return response
92
+ return self.get_paginated_response(
93
+ request=request,
94
+ records=enrollments,
95
+ page=page,
96
+ page_size=page_size,
97
+ total_count=total_count,
98
+ )
96
99
 
97
- def _stream_serialized_data(self, enrollments, chunk_size=50000):
100
+ @staticmethod
101
+ def _stream_serialized_data(enterprise_uuid, start_date, end_date, total_count, page_size=50000):
98
102
  """
99
103
  Stream the serialized data.
100
104
  """
101
- total_rows = enrollments.shape[0]
102
- for start_index in range(0, total_rows, chunk_size):
103
- end_index = min(start_index + chunk_size, total_rows)
104
- chunk = enrollments.iloc[start_index:end_index]
105
- yield from chunk.to_dict(orient='records')
106
-
107
-
108
- class AdvanceAnalyticsEnrollmentStatsView(APIView):
109
- """
110
- API for getting the advance analytics enrollment chart stats.
111
- """
112
- authentication_classes = (JwtAuthentication,)
113
- http_method_names = ['get']
105
+ offset = 0
106
+ while offset < total_count:
107
+ enrollments = FactEnrollmentAdminDashTable().get_all_enrollments(
108
+ enterprise_customer_uuid=enterprise_uuid,
109
+ start_date=start_date,
110
+ end_date=end_date,
111
+ limit=page_size,
112
+ offset=offset,
113
+ )
114
+ yield from enrollments
115
+ offset += page_size
114
116
 
115
117
  @permission_required('can_access_enterprise', fn=lambda request, enterprise_uuid: enterprise_uuid)
116
- def get(self, request, enterprise_uuid): # lint-amnesty, pylint: disable=inconsistent-return-statements
117
- """Get enrollment chart stats"""
118
- serializer = AdvanceAnalyticsEnrollmentStatsSerializer(data=request.GET)
119
- serializer.is_valid(raise_exception=True)
120
-
121
- cache_expiry = fetch_enrollments_cache_expiry_timestamp()
122
- enrollments_df = fetch_and_cache_enrollments_data(enterprise_uuid, cache_expiry)
123
-
124
- # get values from query params or use default
125
- start_date = serializer.data.get('start_date', enrollments_df.enterprise_enrollment_date.min())
126
- end_date = serializer.data.get('end_date', datetime.now())
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
- }
155
- return Response(data)
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
- )
181
-
182
- def enrollments_over_time_common(self, enrollments_df, start_date, end_date, granularity, calculation):
183
- """
184
- Common method for constructing enrollments over time data.
185
-
186
- Arguments:
187
- enrollments_df {DataFrame} -- DataFrame of enrollments
188
- start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
189
- end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
190
- granularity {str} -- Granularity of the data. One of Granularity choices
191
- calculation {str} -- Calculation of the data. One of Calculation choices
192
- """
193
- # filter enrollments by date
194
- enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
195
-
196
- # aggregate enrollments by granularity
197
- enrollments = granularity_aggregation(
198
- level=granularity,
199
- group=["enterprise_enrollment_date", "enroll_type"],
200
- date="enterprise_enrollment_date",
201
- data_frame=enrollments,
202
- )
203
-
204
- # aggregate enrollments by calculation
205
- enrollments = calculation_aggregation(calc=calculation, data_frame=enrollments)
206
-
207
- return enrollments
208
-
209
- def construct_enrollments_over_time(self, enrollments_df, start_date, end_date, granularity, calculation):
210
- """
211
- Construct enrollments over time data.
212
-
213
- Arguments:
214
- enrollments_df {DataFrame} -- DataFrame of enrollments
215
- start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
216
- end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
217
- granularity {str} -- Granularity of the data. One of Granularity choices
218
- calculation {str} -- Calculation of the data. One of Calculation choices
219
- """
220
- enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
221
-
222
- # convert dataframe to a list of records
223
- return enrollments.to_dict(orient='records')
224
-
225
- def construct_enrollments_over_time_csv(self, enrollments_df, start_date, end_date, granularity, calculation):
226
- """
227
- Construct enrollments over time CSV.
228
-
229
- Arguments:
230
- enrollments_df {DataFrame} -- DataFrame of enrollments
231
- start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
232
- end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
233
- granularity {str} -- Granularity of the data. One of Granularity choices
234
- calculation {str} -- Calculation of the data. One of Calculation choices
118
+ @action(detail=False, methods=['get'], name='Charts Data', url_path='stats')
119
+ def stats(self, request, enterprise_uuid):
235
120
  """
236
- enrollments = self.enrollments_over_time_common(enrollments_df, start_date, end_date, granularity, calculation)
121
+ Get data to populate enterprise enrollment charts.
237
122
 
238
- enrollments = enrollments.pivot(
239
- index="enterprise_enrollment_date", columns="enroll_type", values="count"
240
- )
241
-
242
- filename = f"Enrollment Timeseries, {start_date} - {end_date} ({granularity} {calculation}).csv"
243
- return self.construct_csv_response(enrollments, filename)
244
-
245
- def top_courses_by_enrollments_common(self, enrollments_df, start_date, end_date, group_by_columns, columns):
246
- """
247
- Common method for constructing top courses by enrollments data.
248
-
249
- Arguments:
250
- enrollments_df {DataFrame} -- DataFrame of enrollments
251
- start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
252
- end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
253
- group_by_columns {list} -- List of columns to group by
254
- columns {list} -- List of column for the final result
255
- """
256
- # filter enrollments by date
257
- enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
258
-
259
- courses = list(
260
- enrollments.groupby(["course_key"]).size().sort_values(ascending=False)[:10].index
261
- )
262
-
263
- enrollments = (
264
- enrollments[enrollments.course_key.isin(courses)]
265
- .groupby(group_by_columns)
266
- .size()
267
- .reset_index()
268
- )
269
- enrollments.columns = columns
270
-
271
- return enrollments
272
-
273
- def construct_top_courses_by_enrollments(self, enrollments_df, start_date, end_date):
274
- """
275
- Construct top courses by enrollments data.
276
-
277
- Arguments:
278
- enrollments_df {DataFrame} -- DataFrame of enrollments
279
- start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
280
- end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
123
+ Here is the list of the charts and their corresponding data:
124
+ 1. `enrollments_over_time`: This will show time series data of enrollments over time.
125
+ 2. `top_courses_by_enrollments`: This will show the top courses by enrollments.
126
+ 3. `top_subjects_by_enrollments`: This will show the top subjects by enrollments.
281
127
  """
282
- group_by_columns = ["course_key", "enroll_type"]
283
- columns = ["course_key", "enroll_type", "count"]
284
- enrollments = self.top_courses_by_enrollments_common(
285
- enrollments_df,
286
- start_date,
287
- end_date,
288
- group_by_columns,
289
- columns
290
- )
291
-
292
- # convert dataframe to a list of records
293
- return enrollments.to_dict(orient='records')
294
-
295
- def construct_top_courses_by_enrollments_csv(self, enrollments_df, start_date, end_date):
296
- """
297
- Construct top courses by enrollments CSV.
298
-
299
- Arguments:
300
- enrollments_df {DataFrame} -- DataFrame of enrollments
301
- start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
302
- end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
303
- """
304
- group_by_columns = ["course_key", "course_title", "enroll_type"]
305
- columns = ["course_key", "course_title", "enroll_type", "count"]
306
- enrollments = self.top_courses_by_enrollments_common(
307
- enrollments_df,
308
- start_date,
309
- end_date,
310
- group_by_columns,
311
- columns
312
- )
313
-
314
- enrollments = enrollments.pivot(
315
- index=["course_key", "course_title"], columns="enroll_type", values="count"
316
- )
128
+ # Remove hyphens from the UUID
129
+ enterprise_uuid = enterprise_uuid.replace('-', '')
317
130
 
318
- filename = f"Top 10 Courses, {start_date} - {end_date}.csv"
319
- return self.construct_csv_response(enrollments, filename)
320
-
321
- def top_subjects_by_enrollments_common(self, enrollments_df, start_date, end_date):
322
- """
323
- Common method for constructing top subjects by enrollments data.
324
-
325
- Arguments:
326
- enrollments_df {DataFrame} -- DataFrame of enrollments
327
- start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
328
- end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
329
- """
330
- # filter enrollments by date
331
- enrollments = date_filter(start_date, end_date, enrollments_df, "enterprise_enrollment_date")
332
-
333
- subjects = list(
334
- enrollments.groupby(["course_subject"]).size().sort_values(ascending=False)[:10].index
335
- )
131
+ serializer = AdvanceAnalyticsEnrollmentStatsSerializer(data=request.GET)
132
+ serializer.is_valid(raise_exception=True)
336
133
 
337
- enrollments = (
338
- enrollments[enrollments.course_subject.isin(subjects)]
339
- .groupby(["course_subject", "enroll_type"])
340
- .size()
341
- .reset_index()
134
+ min_enrollment_date, _ = FactEnrollmentAdminDashTable().get_enrollment_date_range(
135
+ enterprise_uuid,
342
136
  )
343
- enrollments.columns = ["course_subject", "enroll_type", "count"]
344
-
345
- return enrollments
346
-
347
- def construct_top_subjects_by_enrollments(self, enrollments_df, start_date, end_date):
348
- """
349
- Construct top subjects by enrollments data.
350
-
351
- Arguments:
352
- enrollments_df {DataFrame} -- DataFrame of enrollments
353
- start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
354
- end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
355
- """
356
- enrollments = self.top_subjects_by_enrollments_common(enrollments_df, start_date, end_date)
357
- # convert dataframe to a list of records
358
- return enrollments.to_dict(orient='records')
359
-
360
- def construct_top_subjects_by_enrollments_csv(self, enrollments_df, start_date, end_date):
361
- """
362
- Construct top subjects by enrollments CSV.
363
-
364
- Arguments:
365
- enrollments_df {DataFrame} -- DataFrame of enrollments
366
- start_date {datetime} -- Enrollment start date in the format 'YYYY-MM-DD'
367
- end_date {datetime} -- Enrollment end date in the format 'YYYY-MM-DD'
368
- """
369
- enrollments = self.top_subjects_by_enrollments_common(enrollments_df, start_date, end_date)
370
- enrollments = enrollments.pivot(index="course_subject", columns="enroll_type", values="count")
371
-
372
- filename = f"Top 10 Subjects by Enrollment, {start_date} - {end_date}.csv"
373
- return self.construct_csv_response(enrollments, filename)
374
-
375
- def construct_csv_response(self, enrollments, filename):
376
- """
377
- Construct CSV response.
378
-
379
- Arguments:
380
- enrollments {DataFrame} -- DataFrame of enrollments
381
- filename {str} -- Filename for the CSV
382
- """
383
- response = HttpResponse(content_type='text/csv')
384
- response['Content-Disposition'] = f'attachment; filename="{filename}"'
385
- response['Access-Control-Expose-Headers'] = 'Content-Disposition'
386
- enrollments.to_csv(path_or_buf=response)
387
-
388
- return response
137
+ # get values from query params or use default
138
+ start_date = serializer.data.get('start_date', min_enrollment_date)
139
+ end_date = serializer.data.get('end_date', datetime.now())
140
+ with timer('construct_enrollment_all_stats'):
141
+ data = {
142
+ 'enrollments_over_time': FactEnrollmentAdminDashTable().get_enrolment_time_series_data(
143
+ enterprise_uuid, start_date, end_date
144
+ ),
145
+ 'top_courses_by_enrollments': FactEnrollmentAdminDashTable().get_top_courses_by_enrollments(
146
+ enterprise_uuid, start_date, end_date,
147
+ ),
148
+ 'top_subjects_by_enrollments': FactEnrollmentAdminDashTable().get_top_subjects_by_enrollments(
149
+ enterprise_uuid, start_date, end_date,
150
+ ),
151
+ }
152
+ return Response(data)
@@ -1,9 +1,14 @@
1
1
  """
2
2
  Base views for enterprise data api v1.
3
3
  """
4
+ import math
5
+
4
6
  from edx_rbac.mixins import PermissionRequiredMixin
5
7
  from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
6
8
  from edx_rest_framework_extensions.paginators import DefaultPagination
9
+ from rest_framework.exceptions import NotFound
10
+ from rest_framework.response import Response
11
+ from rest_framework.utils.urls import remove_query_param, replace_query_param
7
12
 
8
13
  from enterprise_data.constants import ANALYTICS_API_VERSION_1
9
14
 
@@ -24,3 +29,77 @@ class EnterpriseViewSetMixin(PermissionRequiredMixin):
24
29
  if 'no_page' in self.request.query_params:
25
30
  return None
26
31
  return super().paginate_queryset(queryset)
32
+
33
+
34
+ class AnalyticsPaginationMixin:
35
+ """
36
+ Mixin that provides utility methods to allow pagination on views.
37
+ """
38
+ page_query_param = 'page'
39
+ page_size_query_param = 'page_size'
40
+
41
+ def get_next_link(self, request, page_number, page_count):
42
+ """
43
+ Get the link to the next page.
44
+
45
+ Arguments:
46
+ request (Request): The request object.
47
+ page_number (int): The current page number.
48
+ page_count (int): The total number of pages.
49
+
50
+ Returns:
51
+ str: The link to the next page.
52
+ """
53
+ if page_number >= page_count:
54
+ return None
55
+ return replace_query_param(
56
+ url=request.build_absolute_uri(),
57
+ key=self.page_query_param,
58
+ val=page_number + 1,
59
+ )
60
+
61
+ def get_previous_link(self, request, page_number):
62
+ """
63
+ Get the link to the previous page.
64
+
65
+ Arguments:
66
+ request (Request): The request object.
67
+ page_number (int): The current page number.
68
+
69
+ Returns:
70
+ str: The link to the previous page.
71
+ """
72
+ if page_number <= 1:
73
+ return None
74
+ url = request.build_absolute_uri()
75
+ next_page = page_number - 1
76
+ if next_page == 1:
77
+ return remove_query_param(url, self.page_query_param)
78
+ return replace_query_param(url, self.page_query_param, next_page)
79
+
80
+ def get_paginated_response(self, request, records, page, page_size, total_count):
81
+ """
82
+ Get pagination data.
83
+
84
+ Arguments:
85
+ request (Request): The request object.
86
+ records (list): The records to return.
87
+ page (int): The current page number.
88
+ page_size (int): The number of records per page.
89
+ total_count (int): The total number of records
90
+
91
+ Returns:
92
+ (Response): The pagination data.
93
+ """
94
+ page_count = math.ceil(total_count / page_size)
95
+ if page_count > 0 and (page <= 0 or page > page_count):
96
+ raise NotFound('Invalid page.')
97
+
98
+ return Response({
99
+ 'next': self.get_next_link(request, page, page_count),
100
+ 'previous': self.get_previous_link(request, page),
101
+ 'count': total_count,
102
+ 'num_pages': page_count,
103
+ 'current_page': page,
104
+ 'results': records,
105
+ })
@@ -384,18 +384,6 @@ def engagements_dataframe():
384
384
  return engagements
385
385
 
386
386
 
387
- def enrollments_csv_content():
388
- """Return the CSV content of enrollments."""
389
- return (
390
- b'email,course_title,course_subject,enroll_type,enterprise_enrollment_date\r\n'
391
- b'rebeccanelson@example.com,Re-engineered tangible approach,business-management,certificate,2021-07-04\r\n'
392
- b'taylorjames@example.com,Re-engineered tangible approach,business-management,certificate,2021-07-03\r\n'
393
- b'ssmith@example.com,Secured static capability,medicine,certificate,2021-05-11\r\n'
394
- b'amber79@example.com,Streamlined zero-defect attitude,communication,certificate,2020-04-08\r\n'
395
- b'kathleenmartin@example.com,Horizontal solution-oriented hub,social-sciences,certificate,2020-04-03\r\n'
396
- )
397
-
398
-
399
387
  def leaderboard_csv_content():
400
388
  """Return the CSV content of leaderboard."""
401
389
  # return (
@@ -1,5 +1,4 @@
1
1
  """Unittests for analytics_enrollments.py"""
2
-
3
2
  from datetime import datetime
4
3
 
5
4
  import ddt
@@ -8,15 +7,10 @@ from rest_framework import status
8
7
  from rest_framework.reverse import reverse
9
8
  from rest_framework.test import APITransactionTestCase
10
9
 
11
- from enterprise_data.admin_analytics.constants import EnrollmentChart, ResponseType
10
+ from enterprise_data.admin_analytics.constants import ResponseType
12
11
  from enterprise_data.api.v1.serializers import AdvanceAnalyticsEnrollmentStatsSerializer as EnrollmentStatsSerializer
13
12
  from enterprise_data.api.v1.serializers import AdvanceAnalyticsQueryParamSerializer
14
- from enterprise_data.tests.admin_analytics.mock_analytics_data import (
15
- ENROLLMENT_STATS_CSVS,
16
- ENROLLMENTS,
17
- enrollments_csv_content,
18
- enrollments_dataframe,
19
- )
13
+ from enterprise_data.tests.admin_analytics.mock_analytics_data import ENROLLMENTS
20
14
  from enterprise_data.tests.mixins import JWTTestMixin
21
15
  from enterprise_data.tests.test_utils import UserFactory
22
16
  from enterprise_data_roles.constants import ENTERPRISE_DATA_ADMIN_ROLE
@@ -59,8 +53,8 @@ class TestIndividualEnrollmentsAPI(JWTTestMixin, APITransactionTestCase):
59
53
  )
60
54
 
61
55
  fetch_max_enrollment_datetime_patcher = patch(
62
- 'enterprise_data.api.v1.views.analytics_enrollments.fetch_enrollments_cache_expiry_timestamp',
63
- return_value=datetime.now()
56
+ 'enterprise_data.api.v1.views.analytics_enrollments.FactEnrollmentAdminDashTable.get_enrollment_date_range',
57
+ return_value=(datetime.now(), datetime.now())
64
58
  )
65
59
 
66
60
  fetch_max_enrollment_datetime_patcher.start()
@@ -69,11 +63,11 @@ class TestIndividualEnrollmentsAPI(JWTTestMixin, APITransactionTestCase):
69
63
  def verify_enrollment_data(self, results, results_count):
70
64
  """Verify the received enrollment data."""
71
65
  attrs = [
72
- "email",
73
- "course_title",
74
- "course_subject",
75
- "enroll_type",
76
- "enterprise_enrollment_date",
66
+ 'email',
67
+ 'course_title',
68
+ 'course_subject',
69
+ 'enroll_type',
70
+ 'enterprise_enrollment_date',
77
71
  ]
78
72
 
79
73
  assert len(results) == results_count
@@ -89,16 +83,16 @@ class TestIndividualEnrollmentsAPI(JWTTestMixin, APITransactionTestCase):
89
83
  expected_data = sorted(filtered_data, key=lambda x: x["email"])
90
84
  assert received_data == expected_data
91
85
 
92
- @patch(
93
- "enterprise_data.api.v1.views.analytics_enrollments.fetch_and_cache_enrollments_data"
94
- )
95
- def test_get(self, mock_fetch_and_cache_enrollments_data):
86
+ @patch('enterprise_data.api.v1.views.analytics_enrollments.FactEnrollmentAdminDashTable.get_enrollment_count')
87
+ @patch('enterprise_data.api.v1.views.analytics_enrollments.FactEnrollmentAdminDashTable.get_all_enrollments')
88
+ def test_get(self, mock_get_all_enrollments, mock_get_enrollment_count):
96
89
  """
97
90
  Test the GET method for the AdvanceAnalyticsIndividualEnrollmentsView works.
98
91
  """
99
- mock_fetch_and_cache_enrollments_data.return_value = enrollments_dataframe()
92
+ mock_get_all_enrollments.return_value = ENROLLMENTS
93
+ mock_get_enrollment_count.return_value = len(ENROLLMENTS)
100
94
 
101
- response = self.client.get(self.url, {"page_size": 2})
95
+ response = self.client.get(self.url + '?page_size=2')
102
96
  assert response.status_code == status.HTTP_200_OK
103
97
  assert response["Content-Type"] == "application/json"
104
98
  data = response.json()
@@ -107,9 +101,8 @@ class TestIndividualEnrollmentsAPI(JWTTestMixin, APITransactionTestCase):
107
101
  assert data["current_page"] == 1
108
102
  assert data["num_pages"] == 3
109
103
  assert data["count"] == 5
110
- self.verify_enrollment_data(data["results"], 2)
111
104
 
112
- response = self.client.get(self.url, {"page_size": 2, "page": 2})
105
+ response = self.client.get(self.url + '?page_size=2&page=2')
113
106
  assert response.status_code == status.HTTP_200_OK
114
107
  assert response["Content-Type"] == "application/json"
115
108
  data = response.json()
@@ -118,9 +111,8 @@ class TestIndividualEnrollmentsAPI(JWTTestMixin, APITransactionTestCase):
118
111
  assert data["current_page"] == 2
119
112
  assert data["num_pages"] == 3
120
113
  assert data["count"] == 5
121
- self.verify_enrollment_data(data["results"], 2)
122
114
 
123
- response = self.client.get(self.url, {"page_size": 2, "page": 3})
115
+ response = self.client.get(self.url + '?page_size=2&page=3')
124
116
  assert response.status_code == status.HTTP_200_OK
125
117
  assert response["Content-Type"] == "application/json"
126
118
  data = response.json()
@@ -129,9 +121,8 @@ class TestIndividualEnrollmentsAPI(JWTTestMixin, APITransactionTestCase):
129
121
  assert data["current_page"] == 3
130
122
  assert data["num_pages"] == 3
131
123
  assert data["count"] == 5
132
- self.verify_enrollment_data(data["results"], 1)
133
124
 
134
- response = self.client.get(self.url, {"page_size": 5})
125
+ response = self.client.get(self.url + '?page_size=5')
135
126
  assert response.status_code == status.HTTP_200_OK
136
127
  assert response["Content-Type"] == "application/json"
137
128
  data = response.json()
@@ -140,16 +131,15 @@ class TestIndividualEnrollmentsAPI(JWTTestMixin, APITransactionTestCase):
140
131
  assert data["current_page"] == 1
141
132
  assert data["num_pages"] == 1
142
133
  assert data["count"] == 5
143
- self.verify_enrollment_data(data["results"], 5)
144
134
 
145
- @patch(
146
- "enterprise_data.api.v1.views.analytics_enrollments.fetch_and_cache_enrollments_data"
147
- )
148
- def test_get_csv(self, mock_fetch_and_cache_enrollments_data):
135
+ @patch('enterprise_data.api.v1.views.analytics_enrollments.FactEnrollmentAdminDashTable.get_enrollment_count')
136
+ @patch('enterprise_data.api.v1.views.analytics_enrollments.FactEnrollmentAdminDashTable.get_all_enrollments')
137
+ def test_get_csv(self, mock_get_all_enrollments, mock_get_enrollment_count):
149
138
  """
150
139
  Test the GET method for the AdvanceAnalyticsIndividualEnrollmentsView return correct CSV data.
151
140
  """
152
- mock_fetch_and_cache_enrollments_data.return_value = enrollments_dataframe()
141
+ mock_get_all_enrollments.return_value = ENROLLMENTS
142
+ mock_get_enrollment_count.return_value = len(ENROLLMENTS)
153
143
  response = self.client.get(self.url, {"response_type": ResponseType.CSV.value})
154
144
  assert response.status_code == status.HTTP_200_OK
155
145
 
@@ -157,8 +147,33 @@ class TestIndividualEnrollmentsAPI(JWTTestMixin, APITransactionTestCase):
157
147
  assert response["Content-Type"] == "text/csv"
158
148
 
159
149
  # verify the response content
160
- content = b"".join(response.streaming_content)
161
- assert content == enrollments_csv_content()
150
+ content = b"".join(response.streaming_content).decode().splitlines()
151
+ assert len(content) == 6
152
+
153
+ # Verify CSV header.
154
+ assert 'email,course_title,course_subject,enroll_type,enterprise_enrollment_date' == content[0]
155
+
156
+ # verify the content
157
+ assert (
158
+ 'rebeccanelson@example.com,Re-engineered tangible approach,business-management,certificate,2021-07-04'
159
+ in content
160
+ )
161
+ assert (
162
+ 'taylorjames@example.com,Re-engineered tangible approach,business-management,certificate,2021-07-03'
163
+ in content
164
+ )
165
+ assert (
166
+ 'ssmith@example.com,Secured static capability,medicine,certificate,2021-05-11'
167
+ in content
168
+ )
169
+ assert (
170
+ 'amber79@example.com,Streamlined zero-defect attitude,communication,certificate,2020-04-08'
171
+ in content
172
+ )
173
+ assert (
174
+ 'kathleenmartin@example.com,Horizontal solution-oriented hub,social-sciences,certificate,2020-04-03'
175
+ in content
176
+ )
162
177
 
163
178
  @ddt.data(
164
179
  {
@@ -232,8 +247,8 @@ class TestEnrollmentStatsAPI(JWTTestMixin, APITransactionTestCase):
232
247
  )
233
248
 
234
249
  fetch_max_enrollment_datetime_patcher = patch(
235
- 'enterprise_data.api.v1.views.analytics_enrollments.fetch_enrollments_cache_expiry_timestamp',
236
- return_value=datetime.now()
250
+ 'enterprise_data.api.v1.views.analytics_enrollments.FactEnrollmentAdminDashTable.get_enrollment_date_range',
251
+ return_value=(datetime.now(), datetime.now())
237
252
  )
238
253
 
239
254
  fetch_max_enrollment_datetime_patcher.start()
@@ -261,93 +276,35 @@ class TestEnrollmentStatsAPI(JWTTestMixin, APITransactionTestCase):
261
276
  assert received_data == expected_data
262
277
 
263
278
  @patch(
264
- "enterprise_data.api.v1.views.analytics_enrollments.fetch_and_cache_enrollments_data"
279
+ 'enterprise_data.api.v1.views.analytics_enrollments.FactEnrollmentAdminDashTable.'
280
+ 'get_top_subjects_by_enrollments'
281
+ )
282
+ @patch(
283
+ 'enterprise_data.api.v1.views.analytics_enrollments.FactEnrollmentAdminDashTable.get_top_courses_by_enrollments'
284
+ )
285
+ @patch(
286
+ 'enterprise_data.api.v1.views.analytics_enrollments.FactEnrollmentAdminDashTable.get_enrolment_time_series_data'
265
287
  )
266
- def test_get(self, mock_fetch_and_cache_enrollments_data):
288
+ def test_get(
289
+ self,
290
+ mock_get_enrolment_time_series_data,
291
+ mock_get_top_courses_by_enrollments,
292
+ mock_get_top_subjects_by_enrollments,
293
+ ):
267
294
  """
268
295
  Test the GET method for the AdvanceAnalyticsEnrollmentStatsView works.
269
296
  """
270
- mock_fetch_and_cache_enrollments_data.return_value = enrollments_dataframe()
297
+ mock_get_enrolment_time_series_data.return_value = []
298
+ mock_get_top_courses_by_enrollments.return_value = []
299
+ mock_get_top_subjects_by_enrollments.return_value = []
271
300
 
272
301
  response = self.client.get(self.url)
273
302
  assert response.status_code == status.HTTP_200_OK
274
303
  assert response["Content-Type"] == "application/json"
275
304
  data = response.json()
276
- assert data == {
277
- "enrollments_over_time": [
278
- {
279
- "enterprise_enrollment_date": "2020-04-03T00:00:00",
280
- "enroll_type": "certificate",
281
- "count": 1,
282
- },
283
- {
284
- "enterprise_enrollment_date": "2020-04-08T00:00:00",
285
- "enroll_type": "certificate",
286
- "count": 1,
287
- },
288
- {
289
- "enterprise_enrollment_date": "2021-05-11T00:00:00",
290
- "enroll_type": "certificate",
291
- "count": 1,
292
- },
293
- {
294
- "enterprise_enrollment_date": "2021-07-03T00:00:00",
295
- "enroll_type": "certificate",
296
- "count": 1,
297
- },
298
- {
299
- "enterprise_enrollment_date": "2021-07-04T00:00:00",
300
- "enroll_type": "certificate",
301
- "count": 1,
302
- },
303
- ],
304
- "top_courses_by_enrollments": [
305
- {"course_key": "NOGk+UVD31", "enroll_type": "certificate", "count": 1},
306
- {"course_key": "QWXx+Jqz64", "enroll_type": "certificate", "count": 1},
307
- {"course_key": "hEmW+tvk03", "enroll_type": "certificate", "count": 2},
308
- {"course_key": "qZJC+KFX86", "enroll_type": "certificate", "count": 1},
309
- ],
310
- "top_subjects_by_enrollments": [
311
- {
312
- "course_subject": "business-management",
313
- "enroll_type": "certificate",
314
- "count": 2,
315
- },
316
- {
317
- "course_subject": "communication",
318
- "enroll_type": "certificate",
319
- "count": 1,
320
- },
321
- {
322
- "course_subject": "medicine",
323
- "enroll_type": "certificate",
324
- "count": 1,
325
- },
326
- {
327
- "course_subject": "social-sciences",
328
- "enroll_type": "certificate",
329
- "count": 1,
330
- },
331
- ],
332
- }
333
-
334
- @patch("enterprise_data.api.v1.views.analytics_enrollments.fetch_and_cache_enrollments_data")
335
- @ddt.data(
336
- EnrollmentChart.ENROLLMENTS_OVER_TIME.value,
337
- EnrollmentChart.TOP_COURSES_BY_ENROLLMENTS.value,
338
- EnrollmentChart.TOP_SUBJECTS_BY_ENROLLMENTS.value,
339
- )
340
- def test_get_csv(self, chart_type, mock_fetch_and_cache_enrollments_data):
341
- """
342
- Test that AdvanceAnalyticsEnrollmentStatsView return correct CSV data.
343
- """
344
- mock_fetch_and_cache_enrollments_data.return_value = enrollments_dataframe()
345
-
346
- response = self.client.get(self.url, {"response_type": ResponseType.CSV.value, "chart_type": chart_type})
347
- assert response.status_code == status.HTTP_200_OK
348
- assert response["Content-Type"] == "text/csv"
349
- # verify the response content
350
- assert response.content == ENROLLMENT_STATS_CSVS[chart_type]
305
+ assert 'enrollments_over_time' in data
306
+ assert 'top_courses_by_enrollments' in data
307
+ assert 'top_subjects_by_enrollments' in data
351
308
 
352
309
  @ddt.data(
353
310
  {