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.
Files changed (32) hide show
  1. {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.3.0.dist-info}/METADATA +4 -1
  2. {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.3.0.dist-info}/RECORD +32 -19
  3. {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.3.0.dist-info}/WHEEL +1 -1
  4. enterprise_data/__init__.py +1 -1
  5. enterprise_data/admin_analytics/__init__.py +0 -0
  6. enterprise_data/admin_analytics/data_loaders.py +137 -0
  7. enterprise_data/admin_analytics/database.py +50 -0
  8. enterprise_data/admin_analytics/utils.py +81 -0
  9. enterprise_data/api/v1/serializers.py +20 -0
  10. enterprise_data/api/v1/urls.py +13 -6
  11. enterprise_data/api/v1/views/__init__.py +0 -0
  12. enterprise_data/api/v1/views/base.py +26 -0
  13. enterprise_data/api/v1/views/enterprise_admin.py +109 -0
  14. enterprise_data/api/v1/{views.py → views/enterprise_learner.py} +5 -114
  15. enterprise_data/api/v1/views/enterprise_offers.py +41 -0
  16. enterprise_data/tests/admin_analytics/__init__.py +0 -0
  17. enterprise_data/tests/admin_analytics/test_data_loaders.py +86 -0
  18. enterprise_data/tests/admin_analytics/test_utils.py +102 -0
  19. enterprise_data/tests/api/v1/views/__init__.py +0 -0
  20. enterprise_data/tests/api/v1/views/test_enterprise_admin.py +82 -0
  21. enterprise_data/tests/test_filters.py +1 -1
  22. enterprise_data/tests/test_utils.py +73 -0
  23. enterprise_data/utils.py +48 -1
  24. enterprise_reporting/clients/__init__.py +2 -3
  25. enterprise_reporting/external_resource_link_report.py +3 -3
  26. enterprise_reporting/tests/test_clients.py +1 -1
  27. enterprise_reporting/tests/test_enterprise_client.py +2 -5
  28. enterprise_reporting/tests/test_external_link_report.py +2 -2
  29. enterprise_reporting/tests/test_utils.py +3 -3
  30. enterprise_reporting/utils.py +1 -1
  31. {edx_enterprise_data-8.0.0.dist-info → edx_enterprise_data-8.3.0.dist-info}/LICENSE +0 -0
  32. {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 api v1.
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
@@ -1,9 +1,9 @@
1
1
  """
2
2
  Tests for clients in enterprise_reporting.
3
3
  """
4
+ from datetime import datetime, timedelta
4
5
  from unittest.mock import Mock, patch
5
6
  from urllib.parse import urljoin
6
- from datetime import datetime, timedelta
7
7
 
8
8
  import responses
9
9