edx-enterprise-data 8.5.0__py3-none-any.whl → 8.6.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.
@@ -0,0 +1,393 @@
1
+ """Unittests for analytics_enrollments.py"""
2
+
3
+ from datetime import datetime
4
+
5
+ import ddt
6
+ from mock import patch
7
+ from rest_framework import status
8
+ from rest_framework.reverse import reverse
9
+ from rest_framework.test import APITransactionTestCase
10
+
11
+ from enterprise_data.admin_analytics.constants import ENROLLMENT_CSV
12
+ from enterprise_data.api.v1.serializers import AdvanceAnalyticsEnrollmentSerializer as EnrollmentSerializer
13
+ from enterprise_data.api.v1.serializers import AdvanceAnalyticsEnrollmentStatsSerializer as EnrollmentStatsSerializer
14
+ from enterprise_data.tests.admin_analytics.mock_enrollments import (
15
+ ENROLLMENT_STATS_CSVS,
16
+ ENROLLMENTS,
17
+ enrollments_csv_content,
18
+ enrollments_dataframe,
19
+ )
20
+ from enterprise_data.tests.mixins import JWTTestMixin
21
+ from enterprise_data.tests.test_utils import UserFactory
22
+ from enterprise_data_roles.constants import ENTERPRISE_DATA_ADMIN_ROLE
23
+ from enterprise_data_roles.models import EnterpriseDataFeatureRole, EnterpriseDataRoleAssignment
24
+
25
+ INVALID_CALCULATION_ERROR = (
26
+ f"Calculation must be one of {EnrollmentSerializer.CALCULATION_CHOICES}"
27
+ )
28
+ INVALID_GRANULARITY_ERROR = (
29
+ f"Granularity must be one of {EnrollmentSerializer.GRANULARITY_CHOICES}"
30
+ )
31
+ INVALID_CSV_ERROR1 = f"csv_type must be one of {EnrollmentSerializer.CSV_TYPES}"
32
+ INVALID_CSV_ERROR2 = f"csv_type must be one of {EnrollmentStatsSerializer.CSV_TYPES}"
33
+
34
+
35
+ @ddt.ddt
36
+ class TestIndividualEnrollmentsAPI(JWTTestMixin, APITransactionTestCase):
37
+ """Tests for AdvanceAnalyticsIndividualEnrollmentsView."""
38
+
39
+ def setUp(self):
40
+ """
41
+ Setup method.
42
+ """
43
+ super().setUp()
44
+ self.user = UserFactory(is_staff=True)
45
+ role, __ = EnterpriseDataFeatureRole.objects.get_or_create(
46
+ name=ENTERPRISE_DATA_ADMIN_ROLE
47
+ )
48
+ self.role_assignment = EnterpriseDataRoleAssignment.objects.create(
49
+ role=role, user=self.user
50
+ )
51
+ self.client.force_authenticate(user=self.user)
52
+
53
+ self.enterprise_uuid = "ee5e6b3a-069a-4947-bb8d-d2dbc323396c"
54
+ self.set_jwt_cookie()
55
+
56
+ self.url = reverse(
57
+ "v1:enterprise-admin-analytics-enrollments",
58
+ kwargs={"enterprise_uuid": self.enterprise_uuid},
59
+ )
60
+
61
+ fetch_max_enrollment_datetime_patcher = patch(
62
+ 'enterprise_data.api.v1.views.analytics_enrollments.fetch_max_enrollment_datetime',
63
+ return_value=datetime.now()
64
+ )
65
+
66
+ fetch_max_enrollment_datetime_patcher.start()
67
+ self.addCleanup(fetch_max_enrollment_datetime_patcher.stop)
68
+
69
+ def verify_enrollment_data(self, results, results_count):
70
+ """Verify the received enrollment data."""
71
+ attrs = [
72
+ "email",
73
+ "course_title",
74
+ "course_subject",
75
+ "enroll_type",
76
+ "enterprise_enrollment_date",
77
+ ]
78
+
79
+ assert len(results) == results_count
80
+
81
+ filtered_data = []
82
+ for enrollment in ENROLLMENTS:
83
+ for result in results:
84
+ if enrollment["email"] == result["email"]:
85
+ filtered_data.append({attr: enrollment[attr] for attr in attrs})
86
+ break
87
+
88
+ received_data = sorted(results, key=lambda x: x["email"])
89
+ expected_data = sorted(filtered_data, key=lambda x: x["email"])
90
+ assert received_data == expected_data
91
+
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):
96
+ """
97
+ Test the GET method for the AdvanceAnalyticsIndividualEnrollmentsView works.
98
+ """
99
+ mock_fetch_and_cache_enrollments_data.return_value = enrollments_dataframe()
100
+
101
+ response = self.client.get(self.url, {"page_size": 2})
102
+ assert response.status_code == status.HTTP_200_OK
103
+ data = response.json()
104
+ assert data["next"] == f"http://testserver{self.url}?page=2&page_size=2"
105
+ assert data["previous"] is None
106
+ assert data["current_page"] == 1
107
+ assert data["num_pages"] == 3
108
+ assert data["count"] == 5
109
+ self.verify_enrollment_data(data["results"], 2)
110
+
111
+ response = self.client.get(self.url, {"page_size": 2, "page": 2})
112
+ assert response.status_code == status.HTTP_200_OK
113
+ data = response.json()
114
+ assert data["next"] == f"http://testserver{self.url}?page=3&page_size=2"
115
+ assert data["previous"] == f"http://testserver{self.url}?page_size=2"
116
+ assert data["current_page"] == 2
117
+ assert data["num_pages"] == 3
118
+ assert data["count"] == 5
119
+ self.verify_enrollment_data(data["results"], 2)
120
+
121
+ response = self.client.get(self.url, {"page_size": 2, "page": 3})
122
+ assert response.status_code == status.HTTP_200_OK
123
+ data = response.json()
124
+ assert data["next"] is None
125
+ assert data["previous"] == f"http://testserver{self.url}?page=2&page_size=2"
126
+ assert data["current_page"] == 3
127
+ assert data["num_pages"] == 3
128
+ assert data["count"] == 5
129
+ self.verify_enrollment_data(data["results"], 1)
130
+
131
+ response = self.client.get(self.url, {"page_size": 5})
132
+ assert response.status_code == status.HTTP_200_OK
133
+ data = response.json()
134
+ assert data["next"] is None
135
+ assert data["previous"] is None
136
+ assert data["current_page"] == 1
137
+ assert data["num_pages"] == 1
138
+ assert data["count"] == 5
139
+ self.verify_enrollment_data(data["results"], 5)
140
+
141
+ @patch(
142
+ "enterprise_data.api.v1.views.analytics_enrollments.fetch_and_cache_enrollments_data"
143
+ )
144
+ def test_get_csv(self, mock_fetch_and_cache_enrollments_data):
145
+ """
146
+ Test the GET method for the AdvanceAnalyticsIndividualEnrollmentsView return correct CSV data.
147
+ """
148
+ mock_fetch_and_cache_enrollments_data.return_value = enrollments_dataframe()
149
+ response = self.client.get(self.url, {"csv_type": ENROLLMENT_CSV.INDIVIDUAL_ENROLLMENTS.value})
150
+ assert response.status_code == status.HTTP_200_OK
151
+
152
+ # verify the response headers
153
+ assert response["Content-Type"] == "text/csv"
154
+ assert (
155
+ response["Content-Disposition"]
156
+ == 'attachment; filename="individual_enrollments.csv"'
157
+ )
158
+
159
+ # verify the response content
160
+ content = b"".join(response.streaming_content)
161
+ assert content == enrollments_csv_content()
162
+
163
+ @ddt.data(
164
+ {
165
+ "params": {"start_date": 1},
166
+ "error": {
167
+ "start_date": [
168
+ "Date has wrong format. Use one of these formats instead: YYYY-MM-DD."
169
+ ]
170
+ },
171
+ },
172
+ {
173
+ "params": {"end_date": 2},
174
+ "error": {
175
+ "end_date": [
176
+ "Date has wrong format. Use one of these formats instead: YYYY-MM-DD."
177
+ ]
178
+ },
179
+ },
180
+ {
181
+ "params": {"start_date": "2024-01-01", "end_date": "2023-01-01"},
182
+ "error": {
183
+ "non_field_errors": [
184
+ "start_date should be less than or equal to end_date."
185
+ ]
186
+ },
187
+ },
188
+ {
189
+ "params": {"calculation": "invalid"},
190
+ "error": {"calculation": [INVALID_CALCULATION_ERROR]},
191
+ },
192
+ {
193
+ "params": {"granularity": "invalid"},
194
+ "error": {"granularity": [INVALID_GRANULARITY_ERROR]},
195
+ },
196
+ {"params": {"csv_type": "invalid"}, "error": {"csv_type": [INVALID_CSV_ERROR1]}},
197
+ )
198
+ @ddt.unpack
199
+ def test_get_invalid_query_params(self, params, error):
200
+ """
201
+ Test the GET method return correct error if any query param value is incorrect.
202
+ """
203
+ response = self.client.get(self.url, params)
204
+ assert response.status_code == status.HTTP_400_BAD_REQUEST
205
+ assert response.json() == error
206
+
207
+
208
+ @ddt.ddt
209
+ class TestEnrollmentStatsAPI(JWTTestMixin, APITransactionTestCase):
210
+ """Tests for AdvanceAnalyticsEnrollmentStatsView."""
211
+
212
+ def setUp(self):
213
+ """
214
+ Setup method.
215
+ """
216
+ super().setUp()
217
+ self.user = UserFactory(is_staff=True)
218
+ role, __ = EnterpriseDataFeatureRole.objects.get_or_create(
219
+ name=ENTERPRISE_DATA_ADMIN_ROLE
220
+ )
221
+ self.role_assignment = EnterpriseDataRoleAssignment.objects.create(
222
+ role=role, user=self.user
223
+ )
224
+ self.client.force_authenticate(user=self.user)
225
+
226
+ self.enterprise_uuid = "ee5e6b3a-069a-4947-bb8d-d2dbc323396c"
227
+ self.set_jwt_cookie()
228
+
229
+ self.url = reverse(
230
+ "v1:enterprise-admin-analytics-enrollments-stats",
231
+ kwargs={"enterprise_uuid": self.enterprise_uuid},
232
+ )
233
+
234
+ fetch_max_enrollment_datetime_patcher = patch(
235
+ 'enterprise_data.api.v1.views.analytics_enrollments.fetch_max_enrollment_datetime',
236
+ return_value=datetime.now()
237
+ )
238
+
239
+ fetch_max_enrollment_datetime_patcher.start()
240
+ self.addCleanup(fetch_max_enrollment_datetime_patcher.stop)
241
+
242
+ def verify_enrollment_data(self, results):
243
+ """Verify the received enrollment data."""
244
+ attrs = [
245
+ "email",
246
+ "course_title",
247
+ "course_subject",
248
+ "enroll_type",
249
+ "enterprise_enrollment_date",
250
+ ]
251
+
252
+ filtered_data = []
253
+ for enrollment in ENROLLMENTS:
254
+ for result in results:
255
+ if enrollment["email"] == result["email"]:
256
+ filtered_data.append({attr: enrollment[attr] for attr in attrs})
257
+ break
258
+
259
+ received_data = sorted(results, key=lambda x: x["email"])
260
+ expected_data = sorted(filtered_data, key=lambda x: x["email"])
261
+ assert received_data == expected_data
262
+
263
+ @patch(
264
+ "enterprise_data.api.v1.views.analytics_enrollments.fetch_and_cache_enrollments_data"
265
+ )
266
+ def test_get(self, mock_fetch_and_cache_enrollments_data):
267
+ """
268
+ Test the GET method for the AdvanceAnalyticsEnrollmentStatsView works.
269
+ """
270
+ mock_fetch_and_cache_enrollments_data.return_value = enrollments_dataframe()
271
+
272
+ response = self.client.get(self.url)
273
+ assert response.status_code == status.HTTP_200_OK
274
+ data = response.json()
275
+ assert data == {
276
+ "enrollments_over_time": [
277
+ {
278
+ "enterprise_enrollment_date": "2020-04-03T00:00:00",
279
+ "enroll_type": "certificate",
280
+ "count": 1,
281
+ },
282
+ {
283
+ "enterprise_enrollment_date": "2020-04-08T00:00:00",
284
+ "enroll_type": "certificate",
285
+ "count": 1,
286
+ },
287
+ {
288
+ "enterprise_enrollment_date": "2021-05-11T00:00:00",
289
+ "enroll_type": "certificate",
290
+ "count": 1,
291
+ },
292
+ {
293
+ "enterprise_enrollment_date": "2021-07-03T00:00:00",
294
+ "enroll_type": "certificate",
295
+ "count": 1,
296
+ },
297
+ {
298
+ "enterprise_enrollment_date": "2021-07-04T00:00:00",
299
+ "enroll_type": "certificate",
300
+ "count": 1,
301
+ },
302
+ ],
303
+ "top_courses_by_enrollments": [
304
+ {"course_key": "NOGk+UVD31", "enroll_type": "certificate", "count": 1},
305
+ {"course_key": "QWXx+Jqz64", "enroll_type": "certificate", "count": 1},
306
+ {"course_key": "hEmW+tvk03", "enroll_type": "certificate", "count": 2},
307
+ {"course_key": "qZJC+KFX86", "enroll_type": "certificate", "count": 1},
308
+ ],
309
+ "top_subjects_by_enrollments": [
310
+ {
311
+ "course_subject": "business-management",
312
+ "enroll_type": "certificate",
313
+ "count": 2,
314
+ },
315
+ {
316
+ "course_subject": "communication",
317
+ "enroll_type": "certificate",
318
+ "count": 1,
319
+ },
320
+ {
321
+ "course_subject": "medicine",
322
+ "enroll_type": "certificate",
323
+ "count": 1,
324
+ },
325
+ {
326
+ "course_subject": "social-sciences",
327
+ "enroll_type": "certificate",
328
+ "count": 1,
329
+ },
330
+ ],
331
+ }
332
+
333
+ @patch("enterprise_data.api.v1.views.analytics_enrollments.fetch_and_cache_enrollments_data")
334
+ @ddt.data(
335
+ ENROLLMENT_CSV.ENROLLMENTS_OVER_TIME.value,
336
+ ENROLLMENT_CSV.TOP_COURSES_BY_ENROLLMENTS.value,
337
+ ENROLLMENT_CSV.TOP_SUBJECTS_BY_ENROLLMENTS.value,
338
+ )
339
+ def test_get_csv(self, csv_type, mock_fetch_and_cache_enrollments_data):
340
+ """
341
+ Test that AdvanceAnalyticsEnrollmentStatsView return correct CSV data.
342
+ """
343
+ mock_fetch_and_cache_enrollments_data.return_value = enrollments_dataframe()
344
+
345
+ response = self.client.get(self.url, {"csv_type": csv_type})
346
+ assert response.status_code == status.HTTP_200_OK
347
+ assert response["Content-Type"] == "text/csv"
348
+ # verify the response content
349
+ assert response.content == ENROLLMENT_STATS_CSVS[csv_type]
350
+
351
+ @ddt.data(
352
+ {
353
+ "params": {"start_date": 1},
354
+ "error": {
355
+ "start_date": [
356
+ "Date has wrong format. Use one of these formats instead: YYYY-MM-DD."
357
+ ]
358
+ },
359
+ },
360
+ {
361
+ "params": {"end_date": 2},
362
+ "error": {
363
+ "end_date": [
364
+ "Date has wrong format. Use one of these formats instead: YYYY-MM-DD."
365
+ ]
366
+ },
367
+ },
368
+ {
369
+ "params": {"start_date": "2024-01-01", "end_date": "2023-01-01"},
370
+ "error": {
371
+ "non_field_errors": [
372
+ "start_date should be less than or equal to end_date."
373
+ ]
374
+ },
375
+ },
376
+ {
377
+ "params": {"calculation": "invalid"},
378
+ "error": {"calculation": [INVALID_CALCULATION_ERROR]},
379
+ },
380
+ {
381
+ "params": {"granularity": "invalid"},
382
+ "error": {"granularity": [INVALID_GRANULARITY_ERROR]},
383
+ },
384
+ {"params": {"csv_type": "invalid"}, "error": {"csv_type": [INVALID_CSV_ERROR2]}},
385
+ )
386
+ @ddt.unpack
387
+ def test_get_invalid_query_params(self, params, error):
388
+ """
389
+ Test the GET method return correct error if any query param value is incorrect.
390
+ """
391
+ response = self.client.get(self.url, params)
392
+ assert response.status_code == status.HTTP_400_BAD_REQUEST
393
+ assert response.json() == error