edx-enterprise-data 8.0.0__py3-none-any.whl → 8.3.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.0.0.dist-info → edx_enterprise_data-8.3.0.dist-info}/METADATA +4 -1
- {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.3.0.dist-info}/RECORD +32 -19
- {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.3.0.dist-info}/WHEEL +1 -1
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/__init__.py +0 -0
- enterprise_data/admin_analytics/data_loaders.py +137 -0
- enterprise_data/admin_analytics/database.py +50 -0
- enterprise_data/admin_analytics/utils.py +81 -0
- enterprise_data/api/v1/serializers.py +20 -0
- enterprise_data/api/v1/urls.py +13 -6
- enterprise_data/api/v1/views/__init__.py +0 -0
- enterprise_data/api/v1/views/base.py +26 -0
- enterprise_data/api/v1/views/enterprise_admin.py +109 -0
- enterprise_data/api/v1/{views.py → views/enterprise_learner.py} +5 -114
- enterprise_data/api/v1/views/enterprise_offers.py +41 -0
- enterprise_data/tests/admin_analytics/__init__.py +0 -0
- enterprise_data/tests/admin_analytics/test_data_loaders.py +86 -0
- enterprise_data/tests/admin_analytics/test_utils.py +102 -0
- enterprise_data/tests/api/v1/views/__init__.py +0 -0
- enterprise_data/tests/api/v1/views/test_enterprise_admin.py +82 -0
- enterprise_data/tests/test_filters.py +1 -1
- enterprise_data/tests/test_utils.py +73 -0
- enterprise_data/utils.py +48 -1
- enterprise_reporting/clients/__init__.py +2 -3
- enterprise_reporting/external_resource_link_report.py +3 -3
- enterprise_reporting/tests/test_clients.py +1 -1
- enterprise_reporting/tests/test_enterprise_client.py +2 -5
- enterprise_reporting/tests/test_external_link_report.py +2 -2
- enterprise_reporting/tests/test_utils.py +3 -3
- enterprise_reporting/utils.py +1 -1
- {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.3.0.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.3.0.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,15 @@
|
|
1
1
|
"""
|
2
|
-
Views for enterprise
|
2
|
+
Views for the enterprise learner.
|
3
3
|
"""
|
4
4
|
|
5
5
|
from datetime import date, timedelta
|
6
6
|
from logging import getLogger
|
7
7
|
from uuid import UUID
|
8
8
|
|
9
|
-
from django_filters.rest_framework import DjangoFilterBackend
|
10
9
|
from edx_django_utils.cache import TieredCache
|
11
|
-
from edx_rbac.decorators import permission_required
|
12
|
-
from edx_rbac.mixins import PermissionRequiredMixin
|
13
|
-
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
14
|
-
from edx_rest_framework_extensions.paginators import DefaultPagination
|
15
10
|
from rest_framework import filters, viewsets
|
16
11
|
from rest_framework.decorators import action
|
17
12
|
from rest_framework.response import Response
|
18
|
-
from rest_framework.status import HTTP_200_OK, HTTP_404_NOT_FOUND
|
19
|
-
from rest_framework.views import APIView
|
20
13
|
|
21
14
|
from django.conf import settings
|
22
15
|
from django.core.paginator import Paginator
|
@@ -27,52 +20,18 @@ from django.http import StreamingHttpResponse
|
|
27
20
|
from django.utils import timezone
|
28
21
|
|
29
22
|
from enterprise_data.api.v1 import serializers
|
30
|
-
from enterprise_data.constants import ANALYTICS_API_VERSION_1
|
31
23
|
from enterprise_data.filters import AuditEnrollmentsFilterBackend, AuditUsersEnrollmentFilterBackend
|
32
|
-
from enterprise_data.models import
|
33
|
-
EnterpriseAdminLearnerProgress,
|
34
|
-
EnterpriseAdminSummarizeInsights,
|
35
|
-
EnterpriseLearner,
|
36
|
-
EnterpriseLearnerEnrollment,
|
37
|
-
EnterpriseOffer,
|
38
|
-
)
|
24
|
+
from enterprise_data.models import EnterpriseLearner, EnterpriseLearnerEnrollment
|
39
25
|
from enterprise_data.paginators import EnterpriseEnrollmentsPagination
|
40
26
|
from enterprise_data.renderers import EnrollmentsCSVRenderer
|
41
|
-
from enterprise_data.utils import get_cache_key
|
27
|
+
from enterprise_data.utils import get_cache_key, subtract_one_month
|
28
|
+
|
29
|
+
from .base import EnterpriseViewSetMixin
|
42
30
|
|
43
31
|
LOGGER = getLogger(__name__)
|
44
32
|
DEFAULT_LEARNER_CACHE_TIMEOUT = 60 * 10
|
45
33
|
|
46
34
|
|
47
|
-
def subtract_one_month(original_date):
|
48
|
-
"""
|
49
|
-
Returns a date exactly one month prior to the passed in date.
|
50
|
-
"""
|
51
|
-
one_day = timedelta(days=1)
|
52
|
-
one_month_earlier = original_date - one_day
|
53
|
-
while one_month_earlier.month == original_date.month or one_month_earlier.day > original_date.day:
|
54
|
-
one_month_earlier -= one_day
|
55
|
-
return one_month_earlier
|
56
|
-
|
57
|
-
|
58
|
-
class EnterpriseViewSetMixin(PermissionRequiredMixin):
|
59
|
-
"""
|
60
|
-
Base class for all Enterprise view sets.
|
61
|
-
"""
|
62
|
-
authentication_classes = (JwtAuthentication,)
|
63
|
-
pagination_class = DefaultPagination
|
64
|
-
permission_required = 'can_access_enterprise'
|
65
|
-
API_VERSION = ANALYTICS_API_VERSION_1
|
66
|
-
|
67
|
-
def paginate_queryset(self, queryset):
|
68
|
-
"""
|
69
|
-
Allows no_page query param to skip pagination
|
70
|
-
"""
|
71
|
-
if 'no_page' in self.request.query_params:
|
72
|
-
return None
|
73
|
-
return super().paginate_queryset(queryset)
|
74
|
-
|
75
|
-
|
76
35
|
class EnterpriseLearnerEnrollmentViewSet(EnterpriseViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
77
36
|
"""
|
78
37
|
Viewset for routes related to Enterprise course enrollments.
|
@@ -337,37 +296,6 @@ class EnterpriseLearnerEnrollmentViewSet(EnterpriseViewSetMixin, viewsets.ReadOn
|
|
337
296
|
return Response(content)
|
338
297
|
|
339
298
|
|
340
|
-
class EnterpriseOfferViewSet(EnterpriseViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
341
|
-
"""
|
342
|
-
Viewset for enterprise offers.
|
343
|
-
"""
|
344
|
-
serializer_class = serializers.EnterpriseOfferSerializer
|
345
|
-
filter_backends = (filters.OrderingFilter, DjangoFilterBackend,)
|
346
|
-
ordering_fields = '__all__'
|
347
|
-
|
348
|
-
lookup_field = 'offer_id'
|
349
|
-
|
350
|
-
filterset_fields = (
|
351
|
-
'offer_id',
|
352
|
-
'status'
|
353
|
-
)
|
354
|
-
|
355
|
-
def get_object(self):
|
356
|
-
"""
|
357
|
-
This ensures that UUIDs with dashes are properly handled when requesting info about offers.
|
358
|
-
|
359
|
-
Related to the work in EnterpriseOfferSerializer with `to_internal_value` and `to_representation`
|
360
|
-
"""
|
361
|
-
self.kwargs['offer_id'] = self.kwargs['offer_id'].replace('-', '')
|
362
|
-
return super().get_object()
|
363
|
-
|
364
|
-
def get_queryset(self):
|
365
|
-
enterprise_customer_uuid = self.kwargs['enterprise_id']
|
366
|
-
return EnterpriseOffer.objects.filter(
|
367
|
-
enterprise_customer_uuid=enterprise_customer_uuid,
|
368
|
-
)
|
369
|
-
|
370
|
-
|
371
299
|
class EnterpriseLearnerViewSet(EnterpriseViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
372
300
|
"""
|
373
301
|
Viewset for routes related to Enterprise Learners.
|
@@ -498,40 +426,3 @@ class EnterpriseLearnerCompletedCoursesViewSet(EnterpriseViewSetMixin, viewsets.
|
|
498
426
|
is_consent_granted=True, # DSC check required
|
499
427
|
).values('user_email').annotate(completed_courses=Count('courserun_key')).order_by('user_email')
|
500
428
|
return enrollments
|
501
|
-
|
502
|
-
|
503
|
-
class EnterpriseAdminInsightsView(APIView):
|
504
|
-
"""
|
505
|
-
API for getting the enterprise admin insights.
|
506
|
-
"""
|
507
|
-
authentication_classes = (JwtAuthentication,)
|
508
|
-
http_method_names = ['get']
|
509
|
-
|
510
|
-
@permission_required('can_access_enterprise', fn=lambda request, enterprise_id: enterprise_id)
|
511
|
-
def get(self, request, enterprise_id):
|
512
|
-
"""
|
513
|
-
HTTP GET endpoint to retrieve the enterprise admin insights
|
514
|
-
"""
|
515
|
-
response_data = {}
|
516
|
-
learner_progress = {}
|
517
|
-
learner_engagement = {}
|
518
|
-
|
519
|
-
try:
|
520
|
-
learner_progress = EnterpriseAdminLearnerProgress.objects.get(enterprise_customer_uuid=enterprise_id)
|
521
|
-
learner_progress = serializers.EnterpriseAdminLearnerProgressSerializer(learner_progress).data
|
522
|
-
response_data['learner_progress'] = learner_progress
|
523
|
-
except EnterpriseAdminLearnerProgress.DoesNotExist:
|
524
|
-
pass
|
525
|
-
|
526
|
-
try:
|
527
|
-
learner_engagement = EnterpriseAdminSummarizeInsights.objects.get(enterprise_customer_uuid=enterprise_id)
|
528
|
-
learner_engagement = serializers.EnterpriseAdminSummarizeInsightsSerializer(learner_engagement).data
|
529
|
-
response_data['learner_engagement'] = learner_engagement
|
530
|
-
except EnterpriseAdminSummarizeInsights.DoesNotExist:
|
531
|
-
pass
|
532
|
-
|
533
|
-
status = HTTP_200_OK
|
534
|
-
if learner_progress == {} and learner_engagement == {}:
|
535
|
-
status = HTTP_404_NOT_FOUND
|
536
|
-
|
537
|
-
return Response(data=response_data, status=status)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
"""
|
2
|
+
Views for enterprise offers
|
3
|
+
"""
|
4
|
+
from django_filters.rest_framework import DjangoFilterBackend
|
5
|
+
from rest_framework import filters, viewsets
|
6
|
+
|
7
|
+
from enterprise_data.api.v1 import serializers
|
8
|
+
from enterprise_data.models import EnterpriseOffer
|
9
|
+
|
10
|
+
from .base import EnterpriseViewSetMixin
|
11
|
+
|
12
|
+
|
13
|
+
class EnterpriseOfferViewSet(EnterpriseViewSetMixin, viewsets.ReadOnlyModelViewSet):
|
14
|
+
"""
|
15
|
+
Viewset for enterprise offers.
|
16
|
+
"""
|
17
|
+
serializer_class = serializers.EnterpriseOfferSerializer
|
18
|
+
filter_backends = (filters.OrderingFilter, DjangoFilterBackend,)
|
19
|
+
ordering_fields = '__all__'
|
20
|
+
|
21
|
+
lookup_field = 'offer_id'
|
22
|
+
|
23
|
+
filterset_fields = (
|
24
|
+
'offer_id',
|
25
|
+
'status'
|
26
|
+
)
|
27
|
+
|
28
|
+
def get_object(self):
|
29
|
+
"""
|
30
|
+
This ensures that UUIDs with dashes are properly handled when requesting info about offers.
|
31
|
+
|
32
|
+
Related to the work in EnterpriseOfferSerializer with `to_internal_value` and `to_representation`
|
33
|
+
"""
|
34
|
+
self.kwargs['offer_id'] = self.kwargs['offer_id'].replace('-', '')
|
35
|
+
return super().get_object()
|
36
|
+
|
37
|
+
def get_queryset(self):
|
38
|
+
enterprise_customer_uuid = self.kwargs['enterprise_id']
|
39
|
+
return EnterpriseOffer.objects.filter(
|
40
|
+
enterprise_customer_uuid=enterprise_customer_uuid,
|
41
|
+
)
|
File without changes
|
@@ -0,0 +1,86 @@
|
|
1
|
+
"""
|
2
|
+
Test the utility functions in the admin_analytics app for data loading operations.
|
3
|
+
"""
|
4
|
+
from uuid import uuid4
|
5
|
+
|
6
|
+
import pytest
|
7
|
+
from mock import patch
|
8
|
+
|
9
|
+
from django.http import Http404
|
10
|
+
from django.test import TestCase
|
11
|
+
|
12
|
+
from enterprise_data.admin_analytics.data_loaders import (
|
13
|
+
fetch_engagement_data,
|
14
|
+
fetch_enrollment_data,
|
15
|
+
fetch_max_enrollment_datetime,
|
16
|
+
)
|
17
|
+
from enterprise_data.tests.test_utils import get_dummy_engagements_data, get_dummy_enrollments_data
|
18
|
+
|
19
|
+
|
20
|
+
class TestDataLoaders(TestCase):
|
21
|
+
"""
|
22
|
+
Test suite for the utility functions in the admin_analytics package for data loading operations.
|
23
|
+
"""
|
24
|
+
|
25
|
+
def test_fetch_max_enrollment_datetime(self):
|
26
|
+
"""
|
27
|
+
Validate the fetch_max_enrollment_datetime function.
|
28
|
+
"""
|
29
|
+
with patch('enterprise_data.admin_analytics.data_loaders.run_query') as mock_run_query:
|
30
|
+
mock_run_query.return_value = [['2024-07-26']]
|
31
|
+
|
32
|
+
max_enrollment_date = fetch_max_enrollment_datetime()
|
33
|
+
self.assertEqual(max_enrollment_date.strftime('%Y-%m-%d'), '2024-07-26')
|
34
|
+
|
35
|
+
# Validate the case where the query returns an empty result.
|
36
|
+
mock_run_query.return_value = []
|
37
|
+
max_enrollment_date = fetch_max_enrollment_datetime()
|
38
|
+
self.assertIsNone(max_enrollment_date)
|
39
|
+
|
40
|
+
def test_fetch_engagement_data(self):
|
41
|
+
"""
|
42
|
+
Validate the fetch_engagement_data function.
|
43
|
+
"""
|
44
|
+
with patch('enterprise_data.admin_analytics.data_loaders.run_query') as mock_run_query:
|
45
|
+
enterprise_uuid = str(uuid4())
|
46
|
+
mock_run_query.return_value = [
|
47
|
+
list(item.values()) for item in get_dummy_engagements_data(enterprise_uuid, 10)
|
48
|
+
]
|
49
|
+
|
50
|
+
engagement_data = fetch_engagement_data(enterprise_uuid)
|
51
|
+
self.assertEqual(engagement_data.shape, (10, 14))
|
52
|
+
|
53
|
+
def test_fetch_engagement_data_empty_data(self):
|
54
|
+
"""
|
55
|
+
Validate the fetch_engagement_data function behavior when no data is returned from the query.
|
56
|
+
"""
|
57
|
+
with patch('enterprise_data.admin_analytics.data_loaders.run_query') as mock_run_query:
|
58
|
+
mock_run_query.return_value = []
|
59
|
+
enterprise_uuid = str(uuid4())
|
60
|
+
with pytest.raises(Http404) as error:
|
61
|
+
fetch_engagement_data(enterprise_uuid)
|
62
|
+
error.value.message = f'No engagement data found for enterprise {enterprise_uuid}'
|
63
|
+
|
64
|
+
def test_fetch_enrollment_data(self):
|
65
|
+
"""
|
66
|
+
Validate the fetch_enrollment_data function.
|
67
|
+
"""
|
68
|
+
with patch('enterprise_data.admin_analytics.data_loaders.run_query') as mock_run_query:
|
69
|
+
enterprise_uuid = str(uuid4())
|
70
|
+
mock_run_query.return_value = [
|
71
|
+
list(item.values()) for item in get_dummy_enrollments_data(enterprise_uuid)
|
72
|
+
]
|
73
|
+
|
74
|
+
enrollment_data = fetch_enrollment_data(enterprise_uuid)
|
75
|
+
self.assertEqual(enrollment_data.shape, (10, 21))
|
76
|
+
|
77
|
+
def test_fetch_enrollment_data_empty_data(self):
|
78
|
+
"""
|
79
|
+
Validate the fetch_enrollment_data function behavior when no data is returned from the query.
|
80
|
+
"""
|
81
|
+
with patch('enterprise_data.admin_analytics.data_loaders.run_query') as mock_run_query:
|
82
|
+
mock_run_query.return_value = []
|
83
|
+
enterprise_uuid = str(uuid4())
|
84
|
+
with pytest.raises(Http404) as error:
|
85
|
+
fetch_enrollment_data(enterprise_uuid)
|
86
|
+
error.value.message = f'No enrollment data found for enterprise {enterprise_uuid}'
|
@@ -0,0 +1,102 @@
|
|
1
|
+
"""
|
2
|
+
Test the utility functions in the admin_analytics app.
|
3
|
+
"""
|
4
|
+
from datetime import datetime, timedelta
|
5
|
+
|
6
|
+
from mock import patch
|
7
|
+
|
8
|
+
from django.test import TestCase
|
9
|
+
|
10
|
+
from enterprise_data.admin_analytics.utils import (
|
11
|
+
fetch_and_cache_engagements_data,
|
12
|
+
fetch_and_cache_enrollments_data,
|
13
|
+
get_cache_timeout,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class TestUtils(TestCase):
|
18
|
+
"""
|
19
|
+
Test suite for the utility functions in the admin_analytics package.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def test_get_cache_timeout(self):
|
23
|
+
"""
|
24
|
+
Validate the get_cache_timeout function.
|
25
|
+
"""
|
26
|
+
now = datetime.now().replace(microsecond=0)
|
27
|
+
with patch('enterprise_data.admin_analytics.utils.datetime') as mock_datetime:
|
28
|
+
mock_datetime.now.return_value = now
|
29
|
+
cache_expiry = now
|
30
|
+
self.assertEqual(get_cache_timeout(cache_expiry), 0)
|
31
|
+
|
32
|
+
cache_expiry = now + timedelta(seconds=10)
|
33
|
+
self.assertEqual(get_cache_timeout(cache_expiry), 10)
|
34
|
+
|
35
|
+
cache_expiry = now + timedelta(seconds=100)
|
36
|
+
self.assertEqual(get_cache_timeout(cache_expiry), 100)
|
37
|
+
|
38
|
+
# Validate the case where cache_expiry is in the past.
|
39
|
+
cache_expiry = now - timedelta(seconds=10)
|
40
|
+
self.assertEqual(get_cache_timeout(cache_expiry), 0)
|
41
|
+
|
42
|
+
def test_fetch_and_cache_enrollments_data(self):
|
43
|
+
"""
|
44
|
+
Validate the fetch_and_cache_enrollments_data function.
|
45
|
+
"""
|
46
|
+
with patch('enterprise_data.admin_analytics.utils.fetch_enrollment_data') as mock_fetch_enrollment_data:
|
47
|
+
with patch('enterprise_data.admin_analytics.utils.TieredCache') as mock_tiered_cache:
|
48
|
+
# Simulate the scenario where the data is not found in the cache.
|
49
|
+
mock_tiered_cache.get_cached_response.return_value.is_found = False
|
50
|
+
mock_fetch_enrollment_data.return_value = 'enrollments'
|
51
|
+
|
52
|
+
enrollments = fetch_and_cache_enrollments_data('enterprise_id', datetime.now() + timedelta(seconds=10))
|
53
|
+
self.assertEqual(enrollments, 'enrollments')
|
54
|
+
self.assertEqual(mock_tiered_cache.get_cached_response.call_count, 1)
|
55
|
+
self.assertEqual(mock_tiered_cache.set_all_tiers.call_count, 1)
|
56
|
+
|
57
|
+
def test_fetch_and_cache_enrollments_data_with_data_cache_found(self):
|
58
|
+
"""
|
59
|
+
Validate the fetch_and_cache_enrollments_data function.
|
60
|
+
"""
|
61
|
+
with patch('enterprise_data.admin_analytics.utils.fetch_enrollment_data') as mock_fetch_enrollment_data:
|
62
|
+
with patch('enterprise_data.admin_analytics.utils.TieredCache') as mock_tiered_cache:
|
63
|
+
# Simulate the scenario where the data is found in the cache.
|
64
|
+
mock_tiered_cache.get_cached_response.return_value.is_found = True
|
65
|
+
mock_tiered_cache.get_cached_response.return_value.value = 'cached-enrollments'
|
66
|
+
mock_fetch_enrollment_data.return_value = 'enrollments'
|
67
|
+
|
68
|
+
enrollments = fetch_and_cache_enrollments_data('enterprise_id', datetime.now() + timedelta(seconds=10))
|
69
|
+
self.assertEqual(enrollments, 'cached-enrollments')
|
70
|
+
self.assertEqual(mock_tiered_cache.get_cached_response.call_count, 1)
|
71
|
+
self.assertEqual(mock_tiered_cache.set_all_tiers.call_count, 0)
|
72
|
+
|
73
|
+
def test_fetch_and_cache_engagements_data(self):
|
74
|
+
"""
|
75
|
+
Validate the fetch_and_cache_engagements_data function.
|
76
|
+
"""
|
77
|
+
with patch('enterprise_data.admin_analytics.utils.fetch_engagement_data') as mock_fetch_engagement_data:
|
78
|
+
with patch('enterprise_data.admin_analytics.utils.TieredCache') as mock_tiered_cache:
|
79
|
+
# Simulate the scenario where the data is not found in the cache.
|
80
|
+
mock_tiered_cache.get_cached_response.return_value.is_found = False
|
81
|
+
mock_fetch_engagement_data.return_value = 'engagements'
|
82
|
+
|
83
|
+
enrollments = fetch_and_cache_engagements_data('enterprise_id', datetime.now() + timedelta(seconds=10))
|
84
|
+
self.assertEqual(enrollments, 'engagements')
|
85
|
+
self.assertEqual(mock_tiered_cache.get_cached_response.call_count, 1)
|
86
|
+
self.assertEqual(mock_tiered_cache.set_all_tiers.call_count, 1)
|
87
|
+
|
88
|
+
def test_fetch_and_cache_engagements_data_with_data_cache_found(self):
|
89
|
+
"""
|
90
|
+
Validate the fetch_and_cache_engagements_data function.
|
91
|
+
"""
|
92
|
+
with patch('enterprise_data.admin_analytics.utils.fetch_engagement_data') as mock_fetch_engagement_data:
|
93
|
+
with patch('enterprise_data.admin_analytics.utils.TieredCache') as mock_tiered_cache:
|
94
|
+
# Simulate the scenario where the data is found in the cache.
|
95
|
+
mock_tiered_cache.get_cached_response.return_value.is_found = True
|
96
|
+
mock_tiered_cache.get_cached_response.return_value.value = 'cached-engagements'
|
97
|
+
mock_fetch_engagement_data.return_value = 'engagements'
|
98
|
+
|
99
|
+
enrollments = fetch_and_cache_engagements_data('enterprise_id', datetime.now() + timedelta(seconds=10))
|
100
|
+
self.assertEqual(enrollments, 'cached-engagements')
|
101
|
+
self.assertEqual(mock_tiered_cache.get_cached_response.call_count, 1)
|
102
|
+
self.assertEqual(mock_tiered_cache.set_all_tiers.call_count, 0)
|
File without changes
|
@@ -0,0 +1,82 @@
|
|
1
|
+
"""
|
2
|
+
Test cases for enterprise_admin views
|
3
|
+
"""
|
4
|
+
from unittest import mock
|
5
|
+
|
6
|
+
import ddt
|
7
|
+
from mock import patch
|
8
|
+
from pytest import mark
|
9
|
+
from rest_framework import status
|
10
|
+
from rest_framework.reverse import reverse
|
11
|
+
from rest_framework.test import APITransactionTestCase
|
12
|
+
|
13
|
+
from enterprise_data.tests.mixins import JWTTestMixin
|
14
|
+
from enterprise_data.tests.test_utils import (
|
15
|
+
UserFactory,
|
16
|
+
get_dummy_engagements_data,
|
17
|
+
get_dummy_enrollments_data,
|
18
|
+
get_dummy_enterprise_api_data,
|
19
|
+
)
|
20
|
+
from enterprise_data_roles.constants import ENTERPRISE_DATA_ADMIN_ROLE
|
21
|
+
from enterprise_data_roles.models import EnterpriseDataFeatureRole, EnterpriseDataRoleAssignment
|
22
|
+
|
23
|
+
|
24
|
+
@ddt.ddt
|
25
|
+
@mark.django_db
|
26
|
+
class TestEnterpriseAdminAnalyticsAggregatesView(JWTTestMixin, APITransactionTestCase):
|
27
|
+
"""
|
28
|
+
Tests for EnterpriseAdminAnalyticsAggregatesView.
|
29
|
+
"""
|
30
|
+
|
31
|
+
def setUp(self):
|
32
|
+
"""
|
33
|
+
Setup method.
|
34
|
+
"""
|
35
|
+
super().setUp()
|
36
|
+
self.user = UserFactory(is_staff=True)
|
37
|
+
role, __ = EnterpriseDataFeatureRole.objects.get_or_create(name=ENTERPRISE_DATA_ADMIN_ROLE)
|
38
|
+
self.role_assignment = EnterpriseDataRoleAssignment.objects.create(
|
39
|
+
role=role,
|
40
|
+
user=self.user
|
41
|
+
)
|
42
|
+
self.client.force_authenticate(user=self.user)
|
43
|
+
|
44
|
+
mocked_get_enterprise_customer = mock.patch(
|
45
|
+
'enterprise_data.filters.EnterpriseApiClient.get_enterprise_customer',
|
46
|
+
return_value=get_dummy_enterprise_api_data()
|
47
|
+
)
|
48
|
+
|
49
|
+
self.mocked_get_enterprise_customer = mocked_get_enterprise_customer.start()
|
50
|
+
self.addCleanup(mocked_get_enterprise_customer.stop)
|
51
|
+
self.enterprise_id = 'ee5e6b3a-069a-4947-bb8d-d2dbc323396c'
|
52
|
+
self.set_jwt_cookie()
|
53
|
+
|
54
|
+
def _mock_run_query(self, query):
|
55
|
+
"""
|
56
|
+
mock implementation of run_query.
|
57
|
+
"""
|
58
|
+
if 'fact_enrollment_admin_dash' in query:
|
59
|
+
return [
|
60
|
+
list(item.values()) for item in get_dummy_enrollments_data(self.enterprise_id, 15)
|
61
|
+
]
|
62
|
+
else:
|
63
|
+
return [
|
64
|
+
list(item.values()) for item in get_dummy_engagements_data(self.enterprise_id, 15)
|
65
|
+
]
|
66
|
+
|
67
|
+
def test_get_admin_analytics_aggregates(self):
|
68
|
+
"""
|
69
|
+
Test to get admin analytics aggregates.
|
70
|
+
"""
|
71
|
+
url = reverse('v1:enterprise-admin-analytics-aggregates', kwargs={'enterprise_id': self.enterprise_id})
|
72
|
+
with patch('enterprise_data.admin_analytics.data_loaders.run_query', side_effect=self._mock_run_query):
|
73
|
+
response = self.client.get(url)
|
74
|
+
assert response.status_code == status.HTTP_200_OK
|
75
|
+
assert 'enrolls' in response.json()
|
76
|
+
assert 'courses' in response.json()
|
77
|
+
assert 'completions' in response.json()
|
78
|
+
assert 'hours' in response.json()
|
79
|
+
assert 'sessions' in response.json()
|
80
|
+
assert 'last_updated_at' in response.json()
|
81
|
+
assert 'min_enrollment_date' in response.json()
|
82
|
+
assert 'max_enrollment_date' in response.json()
|
@@ -12,7 +12,7 @@ from rest_framework.test import APIRequestFactory, APITestCase
|
|
12
12
|
|
13
13
|
from django.conf import settings
|
14
14
|
|
15
|
-
from enterprise_data.api.v1.views import EnterpriseLearnerViewSet
|
15
|
+
from enterprise_data.api.v1.views.enterprise_learner import EnterpriseLearnerViewSet
|
16
16
|
from enterprise_data.filters import AuditUsersEnrollmentFilterBackend
|
17
17
|
from enterprise_data.models import EnterpriseEnrollment, EnterpriseLearnerEnrollment
|
18
18
|
from enterprise_data.tests.mixins import JWTTestMixin
|
@@ -345,3 +345,76 @@ def get_dummy_enterprise_api_data(**kwargs):
|
|
345
345
|
'replace_sensitive_sso_username': False
|
346
346
|
}
|
347
347
|
return enterprise_api_dummy_data
|
348
|
+
|
349
|
+
|
350
|
+
def get_dummy_engagements_data(enterprise_uuid: str, count=10):
|
351
|
+
"""
|
352
|
+
Utility method to get dummy enrollment's data.
|
353
|
+
"""
|
354
|
+
return [
|
355
|
+
{
|
356
|
+
'user_id': FAKER.random_int(min=1),
|
357
|
+
'email': FAKER.email(),
|
358
|
+
'enterprise_customer_uuid': enterprise_uuid,
|
359
|
+
'course_key': FAKER.slug(),
|
360
|
+
'enroll_type': 'verified',
|
361
|
+
'activity_date': FAKER.date_time_between(
|
362
|
+
start_date='-2M',
|
363
|
+
end_date='+2M',
|
364
|
+
),
|
365
|
+
'course_title': ' '.join(FAKER.words(nb=5)).title(),
|
366
|
+
'course_subject': ' '.join(FAKER.words(nb=2)).title(),
|
367
|
+
'is_engaged': FAKER.boolean(),
|
368
|
+
'is_engaged_video': FAKER.boolean(),
|
369
|
+
'is_engaged_forum': FAKER.boolean(),
|
370
|
+
'is_engaged_problem': FAKER.boolean(),
|
371
|
+
'is_active': FAKER.boolean(),
|
372
|
+
'learning_time_seconds': FAKER.random_int(min=1),
|
373
|
+
} for _ in range(count)
|
374
|
+
]
|
375
|
+
|
376
|
+
|
377
|
+
def get_dummy_enrollments_data(enterprise_uuid: str, count=10):
|
378
|
+
"""
|
379
|
+
Utility method to get dummy enrollment's data.
|
380
|
+
"""
|
381
|
+
return [
|
382
|
+
{
|
383
|
+
'enterprise_customer_name': ' '.join(FAKER.words(nb=2)).title(),
|
384
|
+
'enterprise_customer_uuid': enterprise_uuid,
|
385
|
+
'lms_enrollment_id': FAKER.random_int(min=1),
|
386
|
+
'user_id': FAKER.random_int(min=1),
|
387
|
+
'email': FAKER.email(),
|
388
|
+
'course_key': FAKER.slug(),
|
389
|
+
'courserun_key': FAKER.slug(),
|
390
|
+
'course_id': FAKER.slug(),
|
391
|
+
'course_subject': ' '.join(FAKER.words(nb=2)).title(),
|
392
|
+
'course_title': ' '.join(FAKER.words(nb=5)).title(),
|
393
|
+
'enterprise_enrollment_date': FAKER.date_time_between(
|
394
|
+
start_date='-2M',
|
395
|
+
end_date='+2M',
|
396
|
+
),
|
397
|
+
'lms_enrollment_mode': 'verified',
|
398
|
+
'enroll_type': 'verified',
|
399
|
+
'program_title': ' '.join(FAKER.words(nb=2)).title(),
|
400
|
+
'date_certificate_awarded': FAKER.date_time_between(
|
401
|
+
start_date='-2M',
|
402
|
+
end_date='+2M',
|
403
|
+
),
|
404
|
+
'grade_percent': FAKER.pyfloat(right_digits=2, min_value=0, max_value=1),
|
405
|
+
'cert_awarded': FAKER.boolean(),
|
406
|
+
'date_certificate_created_raw': FAKER.date_time_between(
|
407
|
+
start_date='-2M',
|
408
|
+
end_date='+2M',
|
409
|
+
),
|
410
|
+
'passed_date_raw': FAKER.date_time_between(
|
411
|
+
start_date='-2M',
|
412
|
+
end_date='+2M',
|
413
|
+
),
|
414
|
+
'passed_date': FAKER.date_time_between(
|
415
|
+
start_date='-2M',
|
416
|
+
end_date='+2M',
|
417
|
+
),
|
418
|
+
'has_passed': FAKER.boolean(),
|
419
|
+
} for _ in range(count)
|
420
|
+
]
|
enterprise_data/utils.py
CHANGED
@@ -1,9 +1,14 @@
|
|
1
1
|
"""
|
2
2
|
Utility functions for Enterprise Data app.
|
3
3
|
"""
|
4
|
-
|
5
4
|
import hashlib
|
6
5
|
import random
|
6
|
+
import time
|
7
|
+
from datetime import timedelta
|
8
|
+
from functools import wraps
|
9
|
+
from logging import getLogger
|
10
|
+
|
11
|
+
LOGGER = getLogger(__name__)
|
7
12
|
|
8
13
|
|
9
14
|
def get_cache_key(**kwargs):
|
@@ -35,3 +40,45 @@ def get_unique_id():
|
|
35
40
|
Return a unique 32 bit integer.
|
36
41
|
"""
|
37
42
|
return random.getrandbits(32)
|
43
|
+
|
44
|
+
|
45
|
+
def subtract_one_month(original_date):
|
46
|
+
"""
|
47
|
+
Return a date exactly one month prior to the passed in date.
|
48
|
+
"""
|
49
|
+
one_day = timedelta(days=1)
|
50
|
+
one_month_earlier = original_date - one_day
|
51
|
+
while one_month_earlier.month == original_date.month or one_month_earlier.day > original_date.day:
|
52
|
+
one_month_earlier -= one_day
|
53
|
+
return one_month_earlier
|
54
|
+
|
55
|
+
|
56
|
+
def timeit(func):
|
57
|
+
"""
|
58
|
+
Measure time taken by a function.
|
59
|
+
"""
|
60
|
+
@wraps(func)
|
61
|
+
def wrapper(*args, **kwargs):
|
62
|
+
start = time.time()
|
63
|
+
result = func(*args, **kwargs)
|
64
|
+
end = time.time()
|
65
|
+
LOGGER.info(f'Time taken by {func.__name__}: {end - start} seconds')
|
66
|
+
return result
|
67
|
+
|
68
|
+
return wrapper
|
69
|
+
|
70
|
+
|
71
|
+
def date_filter(start, end, data_frame, date_column):
|
72
|
+
"""
|
73
|
+
Filter a pandas DataFrame by date range.
|
74
|
+
|
75
|
+
Arguments:
|
76
|
+
start (DatetimeScalar | NaTType | None): The start date.
|
77
|
+
end (DatetimeScalar | NaTType | None): The end date.
|
78
|
+
data_frame (pandas.DataFrame): The DataFrame to filter.
|
79
|
+
date_column (str): The name of the date column.
|
80
|
+
|
81
|
+
Returns:
|
82
|
+
(pandas.DataFrame): The filtered DataFrame.
|
83
|
+
"""
|
84
|
+
return data_frame[(start <= data_frame[date_column]) & (data_frame[date_column] <= end)]
|
@@ -2,18 +2,17 @@
|
|
2
2
|
Clients used to access third party systems.
|
3
3
|
"""
|
4
4
|
|
5
|
+
import logging
|
5
6
|
import os
|
6
7
|
from datetime import datetime, timedelta
|
7
8
|
from functools import wraps
|
8
9
|
from urllib.parse import parse_qs, urljoin, urlparse
|
9
|
-
from edx_rest_api_client.client import get_oauth_access_token
|
10
10
|
|
11
|
-
import logging
|
12
11
|
import requests
|
12
|
+
from edx_rest_api_client.client import get_oauth_access_token
|
13
13
|
|
14
14
|
from enterprise_reporting.utils import retry_on_exception
|
15
15
|
|
16
|
-
|
17
16
|
LOGGER = logging.getLogger(__name__)
|
18
17
|
|
19
18
|
|
@@ -3,13 +3,13 @@ External Resource Link Report Generation Code.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
|
6
|
-
from collections import Counter
|
7
|
-
from datetime import date
|
8
|
-
import operator
|
9
6
|
import logging
|
7
|
+
import operator
|
10
8
|
import os
|
11
9
|
import re
|
12
10
|
import sys
|
11
|
+
from collections import Counter
|
12
|
+
from datetime import date
|
13
13
|
from urllib.parse import urlparse
|
14
14
|
|
15
15
|
from py2neo import Graph
|