edx-enterprise-data 8.4.0__py3-none-any.whl → 8.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {edx_enterprise_data-8.4.0.dist-info → edx_enterprise_data-8.6.0.dist-info}/METADATA +1 -1
- {edx_enterprise_data-8.4.0.dist-info → edx_enterprise_data-8.6.0.dist-info}/RECORD +18 -12
- 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 +101 -0
- enterprise_data/api/v1/urls.py +19 -0
- enterprise_data/api/v1/views/analytics_enrollments.py +375 -0
- enterprise_data/api/v1/views/enterprise_admin.py +43 -15
- enterprise_data/migrations/0044_enterpriseexecedlcmoduleperformance.py +87 -0
- enterprise_data/models.py +94 -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.4.0.dist-info → edx_enterprise_data-8.6.0.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-8.4.0.dist-info → edx_enterprise_data-8.6.0.dist-info}/WHEEL +0 -0
- {edx_enterprise_data-8.4.0.dist-info → edx_enterprise_data-8.6.0.dist-info}/top_level.txt +0 -0
enterprise_data/renderers.py
CHANGED
@@ -29,3 +29,17 @@ class EnrollmentsCSVRenderer(CSVStreamingRenderer):
|
|
29
29
|
'enterprise_sso_uid', 'created', 'course_api_url', 'total_learning_time_hours', 'is_subsidy',
|
30
30
|
'course_product_line', 'budget_id', 'enterprise_group_name', 'enterprise_group_uuid',
|
31
31
|
]
|
32
|
+
|
33
|
+
|
34
|
+
class IndividualEnrollmentsCSVRenderer(CSVStreamingRenderer):
|
35
|
+
"""
|
36
|
+
Custom streaming csv renderer for advance analytics individual enrollments data.
|
37
|
+
"""
|
38
|
+
|
39
|
+
header = [
|
40
|
+
'email',
|
41
|
+
'course_title',
|
42
|
+
'course_subject',
|
43
|
+
'enroll_type',
|
44
|
+
'enterprise_enrollment_date',
|
45
|
+
]
|
@@ -0,0 +1,169 @@
|
|
1
|
+
"""Mock data for enrollments"""
|
2
|
+
|
3
|
+
import pandas as pd
|
4
|
+
|
5
|
+
from enterprise_data.admin_analytics.constants import ENROLLMENT_CSV
|
6
|
+
|
7
|
+
ENROLLMENTS = [
|
8
|
+
{
|
9
|
+
"enterprise_customer_name": "Hill Ltd",
|
10
|
+
"enterprise_customer_uuid": "33ce656295e04ecfa2a77d407eb96f69",
|
11
|
+
"lms_enrollment_id": 1013,
|
12
|
+
"user_id": 8907,
|
13
|
+
"email": "rebeccanelson@example.com",
|
14
|
+
"course_key": "hEmW+tvk03",
|
15
|
+
"courserun_key": "course-v1:hEmW+tvk03+1T9889",
|
16
|
+
"course_id": "1681",
|
17
|
+
"course_subject": "business-management",
|
18
|
+
"course_title": "Re-engineered tangible approach",
|
19
|
+
"enterprise_enrollment_date": "2021-07-04",
|
20
|
+
"lms_enrollment_mode": "verified",
|
21
|
+
"enroll_type": "certificate",
|
22
|
+
"program_title": "Non-Program",
|
23
|
+
"date_certificate_awarded": "2021-08-25",
|
24
|
+
"grade_percent": 0.99,
|
25
|
+
"cert_awarded": 1,
|
26
|
+
"date_certificate_created_raw": "2021-08-25",
|
27
|
+
"passed_date_raw": "2021-08-25",
|
28
|
+
"passed_date": "2021-08-25",
|
29
|
+
"has_passed": 1,
|
30
|
+
},
|
31
|
+
{
|
32
|
+
"enterprise_customer_name": "Hill Ltd",
|
33
|
+
"enterprise_customer_uuid": "33ce656295e04ecfa2a77d407eb96f69",
|
34
|
+
"lms_enrollment_id": 9172,
|
35
|
+
"user_id": 8369,
|
36
|
+
"email": "taylorjames@example.com",
|
37
|
+
"course_key": "hEmW+tvk03",
|
38
|
+
"courserun_key": "course-v1:hEmW+tvk03+1T9889",
|
39
|
+
"course_id": "1681",
|
40
|
+
"course_subject": "business-management",
|
41
|
+
"course_title": "Re-engineered tangible approach",
|
42
|
+
"enterprise_enrollment_date": "2021-07-03",
|
43
|
+
"lms_enrollment_mode": "verified",
|
44
|
+
"enroll_type": "certificate",
|
45
|
+
"program_title": "Non-Program",
|
46
|
+
"date_certificate_awarded": "2021-09-01",
|
47
|
+
"grade_percent": 0.93,
|
48
|
+
"cert_awarded": 1,
|
49
|
+
"date_certificate_created_raw": "2021-09-01",
|
50
|
+
"passed_date_raw": "2021-09-01",
|
51
|
+
"passed_date": "2021-09-01",
|
52
|
+
"has_passed": 1,
|
53
|
+
},
|
54
|
+
{
|
55
|
+
"enterprise_customer_name": "Hill Ltd",
|
56
|
+
"enterprise_customer_uuid": "33ce656295e04ecfa2a77d407eb96f69",
|
57
|
+
"lms_enrollment_id": 9552,
|
58
|
+
"user_id": 8719,
|
59
|
+
"email": "ssmith@example.com",
|
60
|
+
"course_key": "qZJC+KFX86",
|
61
|
+
"courserun_key": "course-v1:qZJC+KFX86+1T8918",
|
62
|
+
"course_id": "1725",
|
63
|
+
"course_subject": "medicine",
|
64
|
+
"course_title": "Secured static capability",
|
65
|
+
"enterprise_enrollment_date": "2021-05-11",
|
66
|
+
"lms_enrollment_mode": "verified",
|
67
|
+
"enroll_type": "certificate",
|
68
|
+
"program_title": "Non-Program",
|
69
|
+
"date_certificate_awarded": None,
|
70
|
+
"grade_percent": 0.0,
|
71
|
+
"cert_awarded": 0,
|
72
|
+
"date_certificate_created_raw": None,
|
73
|
+
"passed_date_raw": None,
|
74
|
+
"passed_date": None,
|
75
|
+
"has_passed": 0,
|
76
|
+
},
|
77
|
+
{
|
78
|
+
"enterprise_customer_name": "Hill Ltd",
|
79
|
+
"enterprise_customer_uuid": "33ce656295e04ecfa2a77d407eb96f69",
|
80
|
+
"lms_enrollment_id": 3436,
|
81
|
+
"user_id": 3125,
|
82
|
+
"email": "kathleenmartin@example.com",
|
83
|
+
"course_key": "QWXx+Jqz64",
|
84
|
+
"courserun_key": "course-v1:QWXx+Jqz64+1T9449",
|
85
|
+
"course_id": "4878",
|
86
|
+
"course_subject": "social-sciences",
|
87
|
+
"course_title": "Horizontal solution-oriented hub",
|
88
|
+
"enterprise_enrollment_date": "2020-04-03",
|
89
|
+
"lms_enrollment_mode": "verified",
|
90
|
+
"enroll_type": "certificate",
|
91
|
+
"program_title": "Non-Program",
|
92
|
+
"date_certificate_awarded": None,
|
93
|
+
"grade_percent": 0.0,
|
94
|
+
"cert_awarded": 0,
|
95
|
+
"date_certificate_created_raw": None,
|
96
|
+
"passed_date_raw": None,
|
97
|
+
"passed_date": None,
|
98
|
+
"has_passed": 0,
|
99
|
+
},
|
100
|
+
{
|
101
|
+
"enterprise_customer_name": "Hill Ltd",
|
102
|
+
"enterprise_customer_uuid": "33ce656295e04ecfa2a77d407eb96f69",
|
103
|
+
"lms_enrollment_id": 5934,
|
104
|
+
"user_id": 4853,
|
105
|
+
"email": "amber79@example.com",
|
106
|
+
"course_key": "NOGk+UVD31",
|
107
|
+
"courserun_key": "course-v1:NOGk+UVD31+1T4956",
|
108
|
+
"course_id": "4141",
|
109
|
+
"course_subject": "communication",
|
110
|
+
"course_title": "Streamlined zero-defect attitude",
|
111
|
+
"enterprise_enrollment_date": "2020-04-08",
|
112
|
+
"lms_enrollment_mode": "verified",
|
113
|
+
"enroll_type": "certificate",
|
114
|
+
"program_title": "Non-Program",
|
115
|
+
"date_certificate_awarded": None,
|
116
|
+
"grade_percent": 0.0,
|
117
|
+
"cert_awarded": 0,
|
118
|
+
"date_certificate_created_raw": None,
|
119
|
+
"passed_date_raw": None,
|
120
|
+
"passed_date": None,
|
121
|
+
"has_passed": 0,
|
122
|
+
},
|
123
|
+
]
|
124
|
+
|
125
|
+
ENROLLMENT_STATS_CSVS = {
|
126
|
+
ENROLLMENT_CSV.ENROLLMENTS_OVER_TIME.value: (
|
127
|
+
b'enterprise_enrollment_date,certificate\n'
|
128
|
+
b'2020-04-03,1\n'
|
129
|
+
b'2020-04-08,1\n'
|
130
|
+
b'2021-05-11,1\n'
|
131
|
+
b'2021-07-03,1\n'
|
132
|
+
b'2021-07-04,1\n'
|
133
|
+
),
|
134
|
+
ENROLLMENT_CSV.TOP_COURSES_BY_ENROLLMENTS.value: (
|
135
|
+
b'course_key,course_title,certificate\n'
|
136
|
+
b'NOGk+UVD31,Streamlined zero-defect attitude,1\n'
|
137
|
+
b'QWXx+Jqz64,Horizontal solution-oriented hub,1\n'
|
138
|
+
b'hEmW+tvk03,Re-engineered tangible approach,2\n'
|
139
|
+
b'qZJC+KFX86,Secured static capability,1\n'
|
140
|
+
),
|
141
|
+
ENROLLMENT_CSV.TOP_SUBJECTS_BY_ENROLLMENTS.value: (
|
142
|
+
b'course_subject,certificate\nbusiness-management,2\ncommunication,1\nmedicine,1\nsocial-sciences,1\n'
|
143
|
+
)
|
144
|
+
}
|
145
|
+
|
146
|
+
|
147
|
+
def enrollments_dataframe():
|
148
|
+
"""Return a DataFrame of enrollments."""
|
149
|
+
enrollments = pd.DataFrame(ENROLLMENTS)
|
150
|
+
|
151
|
+
enrollments['enterprise_enrollment_date'] = enrollments['enterprise_enrollment_date'].astype('datetime64[ns]')
|
152
|
+
enrollments['date_certificate_awarded'] = enrollments['date_certificate_awarded'].astype('datetime64[ns]')
|
153
|
+
enrollments['date_certificate_created_raw'] = enrollments['date_certificate_created_raw'].astype('datetime64[ns]')
|
154
|
+
enrollments['passed_date_raw'] = enrollments['passed_date_raw'].astype('datetime64[ns]')
|
155
|
+
enrollments['passed_date'] = enrollments['passed_date'].astype('datetime64[ns]')
|
156
|
+
|
157
|
+
return enrollments
|
158
|
+
|
159
|
+
|
160
|
+
def enrollments_csv_content():
|
161
|
+
"""Return the CSV content of enrollments."""
|
162
|
+
return (
|
163
|
+
b'email,course_title,course_subject,enroll_type,enterprise_enrollment_date\r\n'
|
164
|
+
b'rebeccanelson@example.com,Re-engineered tangible approach,business-management,certificate,2021-07-04\r\n'
|
165
|
+
b'taylorjames@example.com,Re-engineered tangible approach,business-management,certificate,2021-07-03\r\n'
|
166
|
+
b'ssmith@example.com,Secured static capability,medicine,certificate,2021-05-11\r\n'
|
167
|
+
b'amber79@example.com,Streamlined zero-defect attitude,communication,certificate,2020-04-08\r\n'
|
168
|
+
b'kathleenmartin@example.com,Horizontal solution-oriented hub,social-sciences,certificate,2020-04-03\r\n'
|
169
|
+
)
|
@@ -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
|