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.
- {edx_enterprise_data-8.5.0.dist-info → edx_enterprise_data-8.6.1.dist-info}/METADATA +1 -1
- {edx_enterprise_data-8.5.0.dist-info → edx_enterprise_data-8.6.1.dist-info}/RECORD +15 -10
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/constants.py +27 -0
- enterprise_data/admin_analytics/utils.py +49 -0
- enterprise_data/api/v1/paginators.py +121 -0
- enterprise_data/api/v1/serializers.py +90 -0
- enterprise_data/api/v1/urls.py +14 -0
- enterprise_data/api/v1/views/analytics_enrollments.py +375 -0
- enterprise_data/renderers.py +14 -0
- enterprise_data/tests/admin_analytics/mock_enrollments.py +169 -0
- enterprise_data/tests/admin_analytics/test_analytics_enrollments.py +393 -0
- {edx_enterprise_data-8.5.0.dist-info → edx_enterprise_data-8.6.1.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-8.5.0.dist-info → edx_enterprise_data-8.6.1.dist-info}/WHEEL +0 -0
- {edx_enterprise_data-8.5.0.dist-info → edx_enterprise_data-8.6.1.dist-info}/top_level.txt +0 -0
@@ -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
|
File without changes
|
File without changes
|
File without changes
|