edx-enterprise-data 9.8.1__py3-none-any.whl → 9.9.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edx-enterprise-data
3
- Version: 9.8.1
3
+ Version: 9.9.2
4
4
  Summary: Enterprise Reporting
5
5
  Home-page: https://github.com/openedx/edx-enterprise-data
6
6
  Author: edX
@@ -1,11 +1,11 @@
1
- enterprise_data/__init__.py,sha256=wpeo40dgq_B95YEkw7ewrj1yUNR51loTphUF54KRvrM,123
1
+ enterprise_data/__init__.py,sha256=x3gAs1jDZ1edFFuLMiYeJrlllW0xiAv3ykNfk-YHdE4,123
2
2
  enterprise_data/apps.py,sha256=aF6hZwDfI2oWj95tUTm_2ikHueQj-jLj-u0GrgzpsQI,414
3
- enterprise_data/clients.py,sha256=GvQupy5TVYfO_IKC3yzXSAgNP54r-PtIjidM5ws9Iks,3947
3
+ enterprise_data/clients.py,sha256=pBuCQOP7NYl264ZbMtZCRPCET1pg0by8w6PVlA-icKA,4995
4
4
  enterprise_data/constants.py,sha256=uCKjfpdlMYFZJsAj3n9RMw4Cmg5_6s3NuwocO-fch3s,238
5
5
  enterprise_data/filters.py,sha256=D2EiK12MMpBoz6eOUmTpoJEhj_sH7bA93NRRAdvkDVo,6163
6
- enterprise_data/models.py,sha256=6_rtrxVX-eW7IjJLLFDK712ih-tFqHWzlsPwwTk7XL0,26555
6
+ enterprise_data/models.py,sha256=gbZl-ei0qe_l-BFeE0gWeyfWqmgi9X9LAF8MYOEfk2s,26555
7
7
  enterprise_data/paginators.py,sha256=YPrC5TeXFt-ymenT2H8H2nCbDCnAzJQlH9kFPElRxWE,269
8
- enterprise_data/renderers.py,sha256=qggCLZklL9ohVcLHLO1pSecPJSDCCR7e_-PVobl9Lj8,3063
8
+ enterprise_data/renderers.py,sha256=56cbh7Af49VwHnbyanmtxHHoUc1IFn_U33gkA5V6_k8,3073
9
9
  enterprise_data/signals.py,sha256=8eqNPnlvmfsKf19lGWv5xTIuBgQIqR8EZSp9UYzC8Rc,1024
10
10
  enterprise_data/urls.py,sha256=bqtKF5OEWEwrNmHG3os-pZNuNsmjlhxEqp7yM4TbPf4,243
11
11
  enterprise_data/utils.py,sha256=djGuQUnvT8HJMMV60CzcD5BWerPIwiK9uG26bhkpWMg,3518
@@ -30,7 +30,7 @@ enterprise_data/api/v0/serializers.py,sha256=dngZTk6DhRxApchQKCMp1B_c8aVnQtH0NCq
30
30
  enterprise_data/api/v0/urls.py,sha256=vzJjqIo_S3AXWs9Us8XTaJc3FnxLbYzAkmLyuDQqum0,699
31
31
  enterprise_data/api/v0/views.py,sha256=4RslZ4NZOU-844bnebEQ71ji2utRY7jEijqC45oQQD0,14380
32
32
  enterprise_data/api/v1/__init__.py,sha256=1aAzAYU5hk-RW6cKUxa1645cbZMxn7GIZ7OMjWc9MKI,46
33
- enterprise_data/api/v1/serializers.py,sha256=DUhk8cP-JFQmbLR_JNGmmwis9Un5dyvd01kRdmxn8Lk,11866
33
+ enterprise_data/api/v1/serializers.py,sha256=27LcGcIJPPVm4PedHdgQclrPpdKhtd3E0OJYf4u5sBU,13788
34
34
  enterprise_data/api/v1/urls.py,sha256=Wl1xLboPg-Lq1ZvjAWf51JKYkHlmx02Kpq1nwfDyS8s,4372
35
35
  enterprise_data/api/v1/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  enterprise_data/api/v1/views/analytics_completions.py,sha256=esFbJ5q8ssnm2Mfbc3rZXtiGHF-MeM4KQ4Ft3N7wwHU,6260
@@ -39,7 +39,7 @@ enterprise_data/api/v1/views/analytics_enrollments.py,sha256=hw87VZ0hFWpwf3QEHFn
39
39
  enterprise_data/api/v1/views/analytics_leaderboard.py,sha256=3dyo7_OhyGEEeibemBrRsUOo0jbM4LbDgV5gw3YnVig,4186
40
40
  enterprise_data/api/v1/views/base.py,sha256=Kkmd5zgEBAhvwS_GoGXSK6lgbDNwSPioYn-QbnizI3w,3416
41
41
  enterprise_data/api/v1/views/enterprise_admin.py,sha256=xxpb48cjuHkFJH-IQG0DwOvdH8RgyYD-aQjyfMMJNFg,9030
42
- enterprise_data/api/v1/views/enterprise_learner.py,sha256=lCB2guZeIDf-CbcdDzT2K5K-OW1Vt5i4R8K9aT1ZzuM,18627
42
+ enterprise_data/api/v1/views/enterprise_learner.py,sha256=noFAKFe1T8CXRSFt8aMajF-utPO4x2WK-kxdIYw5qE4,20601
43
43
  enterprise_data/api/v1/views/enterprise_offers.py,sha256=VifxgqTLFLVw4extYPlHcN1N_yjXcsYsAlYEnAbpb10,1266
44
44
  enterprise_data/cache/__init__.py,sha256=fiBUploll1kmDy2vCmnNpeZVTD4ewsgtRF14vVs0Rb4,1850
45
45
  enterprise_data/cache/decorators.py,sha256=vLbXK9VSv-HzVkkXS1-TkuSMxudwyxz04WFsAXqmjuM,1273
@@ -107,6 +107,7 @@ enterprise_data/migrations/0044_enterpriseexecedlcmoduleperformance.py,sha256=1a
107
107
  enterprise_data/migrations/0045_alter_enterpriseexecedlcmoduleperformance_options_and_more.py,sha256=HJkY2OvuE39ciWgaEescd--ixQhTVrwzbL0_bvNofSo,876
108
108
  enterprise_data/migrations/0046_enterprisegroupmembership.py,sha256=38qfi3EySDLrpP3gDuO_oUl_7rphGPZIrPsCuBhQ_dk,1766
109
109
  enterprise_data/migrations/0047_enterpriseexecedlcmoduleperformance_avg_after_lo_score_and_more.py,sha256=_lHmsJozjV_JEWyUfHDKleOhd53594fOaRhz7M0z-nY,897
110
+ enterprise_data/migrations/0048_alter_enterpriseexecedlcmoduleperformance_avg_after_lo_score_and_more.py,sha256=Hjgjj1LwNSCno45EHECCBy5jKKb51Me61xo8ukQLIEI,740
110
111
  enterprise_data/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
111
112
  enterprise_data/settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
112
113
  enterprise_data/settings/test.py,sha256=4-flfrlf81AthGx9wTaT5PscyoOWyhsDDqbzBl-z7Eg,4191
@@ -174,8 +175,8 @@ enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=WtL-RqGgu2x5PP
174
175
  enterprise_reporting/tests/test_utils.py,sha256=Zt_TA0LVb-B6fQGkUkAKKVlUKKnQh8jnw1US1jKe7g8,9493
175
176
  enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
176
177
  enterprise_reporting/tests/utils.py,sha256=xms2LM7DV3wczXEfctOK1ddel1EE0J_YSr17UzbCDy4,1401
177
- edx_enterprise_data-9.8.1.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
178
- edx_enterprise_data-9.8.1.dist-info/METADATA,sha256=jOs9Mr25DN3cB7ruL2kpzdTocOVqwtlozgrwXcxlCL8,1569
179
- edx_enterprise_data-9.8.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
180
- edx_enterprise_data-9.8.1.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
181
- edx_enterprise_data-9.8.1.dist-info/RECORD,,
178
+ edx_enterprise_data-9.9.2.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
179
+ edx_enterprise_data-9.9.2.dist-info/METADATA,sha256=m_Qv6vPHZd8XUx4cmIc-GQo3uJYh35tFLt4YqQ7iGY8,1569
180
+ edx_enterprise_data-9.9.2.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
181
+ edx_enterprise_data-9.9.2.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
182
+ edx_enterprise_data-9.9.2.dist-info/RECORD,,
@@ -2,4 +2,4 @@
2
2
  Enterprise data api application. This Django app exposes API endpoints used by enterprises.
3
3
  """
4
4
 
5
- __version__ = "9.8.1"
5
+ __version__ = "9.9.2"
@@ -6,6 +6,7 @@ from uuid import UUID
6
6
  from rest_framework import serializers
7
7
 
8
8
  from enterprise_data.admin_analytics.constants import ResponseType
9
+ from enterprise_data.cache.decorators import cache_it
9
10
  from enterprise_data.models import (
10
11
  EnterpriseAdminLearnerProgress,
11
12
  EnterpriseAdminSummarizeInsights,
@@ -26,6 +27,8 @@ class EnterpriseLearnerEnrollmentSerializer(serializers.ModelSerializer):
26
27
  course_api_url = serializers.SerializerMethodField()
27
28
  enterprise_user_id = serializers.SerializerMethodField()
28
29
  total_learning_time_hours = serializers.SerializerMethodField()
30
+ enterprise_flex_group_name = serializers.SerializerMethodField()
31
+ enterprise_flex_group_uuid = serializers.SerializerMethodField()
29
32
 
30
33
  class Meta:
31
34
  model = EnterpriseLearnerEnrollment
@@ -44,8 +47,9 @@ class EnterpriseLearnerEnrollmentSerializer(serializers.ModelSerializer):
44
47
  'last_activity_date', 'progress_status', 'passed_date', 'current_grade',
45
48
  'letter_grade', 'enterprise_user_id', 'user_email', 'user_account_creation_date',
46
49
  'user_country_code', 'user_username', 'user_first_name', 'user_last_name', 'enterprise_name',
47
- 'enterprise_customer_uuid', 'enterprise_sso_uid', 'created', 'course_api_url', 'total_learning_time_hours',
48
- 'is_subsidy', 'course_product_line', 'budget_id', 'enterprise_group_name', 'enterprise_group_uuid',
50
+ 'enterprise_customer_uuid', 'enterprise_sso_uid', 'created', 'course_api_url',
51
+ 'total_learning_time_hours', 'is_subsidy', 'course_product_line', 'budget_id',
52
+ 'enterprise_flex_group_name', 'enterprise_flex_group_uuid',
49
53
  )
50
54
 
51
55
  def get_course_api_url(self, obj):
@@ -62,6 +66,51 @@ class EnterpriseLearnerEnrollmentSerializer(serializers.ModelSerializer):
62
66
  """Returns the learners total learning time in hours"""
63
67
  return round((obj.total_learning_time_seconds or 0.0)/3600.0, 2)
64
68
 
69
+ @cache_it()
70
+ def _get_flex_groups(self, obj):
71
+ """
72
+ Returns list of tuples containing group (name, uuid) pairs for the learner.
73
+ This is cached to prevent duplicate database queries.
74
+ """
75
+ enterprise_user_id = obj.enterprise_user_id
76
+
77
+ if not enterprise_user_id:
78
+ return []
79
+
80
+ # Get all group memberships for this user in a single query
81
+ # Order by name for consistent ordering
82
+ return list(
83
+ EnterpriseGroupMembership.objects.filter(
84
+ enterprise_customer_user_id=enterprise_user_id,
85
+ membership_is_removed=False,
86
+ group_is_removed=False,
87
+ group_type="flex",
88
+ )
89
+ .order_by("enterprise_group_name")
90
+ .values_list("enterprise_group_name", "enterprise_group_uuid")
91
+ .distinct()
92
+ )
93
+
94
+ def get_enterprise_flex_group_name(self, obj):
95
+ """Returns a comma-separated list of enterprise group names that the learner is associated with"""
96
+ groups = self._get_flex_groups(obj)
97
+
98
+ if not groups:
99
+ return obj.enterprise_group_name
100
+
101
+ # Return comma-separated list of group names (first element of each tuple)
102
+ return ', '.join(group[0] for group in groups)
103
+
104
+ def get_enterprise_flex_group_uuid(self, obj):
105
+ """Returns a comma-separated list of enterprise group UUIDs that the learner is associated with"""
106
+ groups = self._get_flex_groups(obj)
107
+
108
+ if not groups:
109
+ return obj.enterprise_group_uuid
110
+
111
+ # Return comma-separated list of group UUIDs (second element of each tuple)
112
+ return ', '.join(str(group[1]) for group in groups)
113
+
65
114
 
66
115
  class EnterpriseSubsidyBudgetSerializer(serializers.ModelSerializer):
67
116
  """
@@ -8,6 +8,7 @@ from uuid import UUID
8
8
 
9
9
  from rest_framework import filters, viewsets
10
10
  from rest_framework.decorators import action
11
+ from rest_framework.exceptions import NotFound
11
12
  from rest_framework.response import Response
12
13
 
13
14
  from django.conf import settings
@@ -20,6 +21,7 @@ from django.utils import timezone
20
21
 
21
22
  from enterprise_data.admin_analytics.database.utils import LOGGER
22
23
  from enterprise_data.api.v1 import serializers
24
+ from enterprise_data.clients import EnterpriseApiClient
23
25
  from enterprise_data.models import EnterpriseGroupMembership, EnterpriseLearner, EnterpriseLearnerEnrollment
24
26
  from enterprise_data.paginators import EnterpriseEnrollmentsPagination
25
27
  from enterprise_data.renderers import EnrollmentsCSVRenderer
@@ -176,12 +178,51 @@ class EnterpriseLearnerEnrollmentViewSet(EnterpriseViewSetMixin, viewsets.ReadOn
176
178
 
177
179
  group_uuid = query_filters.get('group_uuid')
178
180
  if group_uuid:
179
- flex_group_exists = EnterpriseGroupMembership.objects.filter(
180
- enterprise_customer_user_id=OuterRef('enterprise_user_id'),
181
- enterprise_group_uuid=group_uuid,
182
- group_type='flex'
183
- )
184
- queryset = queryset.filter(Exists(flex_group_exists))
181
+ queryset = self.filter_by_group_uuid(queryset, group_uuid)
182
+
183
+ return queryset
184
+
185
+ def filter_by_group_uuid(self, queryset, group_uuid):
186
+ """
187
+ Filters the queryset based on the provided group_uuid. If no records are found,
188
+ it attempts to fetch group learners from the API and filter the queryset again.
189
+
190
+ Args:
191
+ queryset (QuerySet): The initial queryset to filter.
192
+ group_uuid (str): The UUID of the group to filter by.
193
+
194
+ Returns:
195
+ QuerySet: The filtered queryset.
196
+ """
197
+ flex_group_exists = EnterpriseGroupMembership.objects.filter(
198
+ enterprise_customer_user_id=OuterRef('enterprise_user_id'),
199
+ enterprise_group_uuid=group_uuid,
200
+ group_type='flex'
201
+ )
202
+
203
+ # First, filter the queryset using flex_group_exists
204
+ filtered_queryset = queryset.filter(Exists(flex_group_exists))
205
+
206
+ # If no records are found, attempt to fetch group_learners from the API
207
+ if not filtered_queryset.exists():
208
+ try:
209
+ enterprise_api_client = EnterpriseApiClient(
210
+ settings.BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL,
211
+ settings.BACKEND_SERVICE_EDX_OAUTH2_KEY,
212
+ settings.BACKEND_SERVICE_EDX_OAUTH2_SECRET,
213
+ )
214
+ group_learners = enterprise_api_client.get_enterprise_group_learners(group_uuid)
215
+ if group_learners:
216
+ group_learners_ids = [learner['enterprise_customer_user_id'] for learner in group_learners]
217
+ queryset = queryset.filter(enterprise_user_id__in=group_learners_ids)
218
+ else:
219
+ LOGGER.warning(f"No group learners found for group: {group_uuid}")
220
+ raise NotFound(f"No learners found for group: {group_uuid}")
221
+ except (Exception) as e: # pylint: disable=broad-exception-caught
222
+ LOGGER.error("Failed to fetch group learners from API: %s", e)
223
+ queryset = queryset.none() # API call failed or unexpected error, return an empty queryset
224
+ else:
225
+ queryset = filtered_queryset
185
226
 
186
227
  return queryset
187
228
 
@@ -101,3 +101,30 @@ class EnterpriseApiClient(OAuthAPIClient):
101
101
  TieredCache.set_all_tiers(cache_key, data, DEFAULT_REPORTING_CACHE_TIMEOUT)
102
102
 
103
103
  return data
104
+
105
+ def get_enterprise_group_learners(self, group_uuid):
106
+ """
107
+ Get the learners associated with a given enterprise group.
108
+
109
+ Returns: list of learners or None if unable to retrieve or no learners exist
110
+ """
111
+ LOGGER.info(f'[EnterpriseApiClient] getting learners for enterprise group:{group_uuid}')
112
+ url = urljoin(self.API_BASE_URL, f'enterprise-group/{group_uuid}/learners/')
113
+ all_learners = []
114
+
115
+ try:
116
+ while url:
117
+ response = self.get(url)
118
+ response.raise_for_status()
119
+ data = response.json()
120
+ all_learners.extend(data.get('results', []))
121
+ url = data.get('next') # Get the URL for the next page, if any
122
+
123
+ except (HTTPError, RequestException) as exc:
124
+ LOGGER.warning(
125
+ "[Data Overview Failure] Unable to retrieve Enterprise Group Learners details. "
126
+ f"Group: {group_uuid}, Exception: {exc}"
127
+ )
128
+ return None
129
+
130
+ return all_learners
@@ -0,0 +1,23 @@
1
+ # Generated by Django 4.2.15 on 2025-02-25 08:17
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('enterprise_data', '0047_enterpriseexecedlcmoduleperformance_avg_after_lo_score_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='enterpriseexecedlcmoduleperformance',
15
+ name='avg_after_lo_score',
16
+ field=models.DecimalField(decimal_places=2, max_digits=38, null=True),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='enterpriseexecedlcmoduleperformance',
20
+ name='avg_before_lo_score',
21
+ field=models.DecimalField(decimal_places=2, max_digits=38, null=True),
22
+ ),
23
+ ]
enterprise_data/models.py CHANGED
@@ -559,8 +559,8 @@ class EnterpriseExecEdLCModulePerformance(models.Model):
559
559
  discussion_forum_activities_total_count = models.PositiveIntegerField(null=True)
560
560
  pass_grade = models.DecimalField(max_digits=38, decimal_places=2, null=True)
561
561
  question_name = models.CharField(max_length=500, null=True)
562
- avg_before_lo_score = models.DecimalField(max_digits=38, decimal_places=6, null=True)
563
- avg_after_lo_score = models.DecimalField(max_digits=38, decimal_places=6, null=True)
562
+ avg_before_lo_score = models.DecimalField(max_digits=38, decimal_places=2, null=True)
563
+ avg_after_lo_score = models.DecimalField(max_digits=38, decimal_places=2, null=True)
564
564
 
565
565
  def __str__(self):
566
566
  """
@@ -27,7 +27,7 @@ class EnrollmentsCSVRenderer(CSVStreamingRenderer):
27
27
  'letter_grade', 'enterprise_user_id', 'user_email', 'user_account_creation_date',
28
28
  'user_country_code', 'user_username', 'user_first_name', 'user_last_name', 'enterprise_name',
29
29
  'enterprise_customer_uuid', 'enterprise_sso_uid', 'created', 'course_api_url', 'total_learning_time_hours',
30
- 'is_subsidy', 'course_product_line', 'budget_id', 'enterprise_group_name', 'enterprise_group_uuid',
30
+ 'is_subsidy', 'course_product_line', 'budget_id', 'enterprise_flex_group_name', 'enterprise_flex_group_uuid',
31
31
  ]
32
32
 
33
33