edx-enterprise-data 9.7.11__py3-none-any.whl → 9.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edx-enterprise-data
3
- Version: 9.7.11
3
+ Version: 9.9.1
4
4
  Summary: Enterprise Reporting
5
5
  Home-page: https://github.com/openedx/edx-enterprise-data
6
6
  Author: edX
@@ -1,14 +1,14 @@
1
- enterprise_data/__init__.py,sha256=ueETKv3rC8y23OfdEXsWBU9_G2MqZGuKLQQ905LzAZE,124
1
+ enterprise_data/__init__.py,sha256=C2aHImiEECgYsjhXVl_9wAosZE3Qd9wtT1_s7eAwDU0,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=INIETN4B6G1EtxVzgBQr6-JMy3ZsHj9K_FCyrS5OFAk,26312
6
+ enterprise_data/models.py,sha256=gbZl-ei0qe_l-BFeE0gWeyfWqmgi9X9LAF8MYOEfk2s,26555
7
7
  enterprise_data/paginators.py,sha256=YPrC5TeXFt-ymenT2H8H2nCbDCnAzJQlH9kFPElRxWE,269
8
8
  enterprise_data/renderers.py,sha256=qggCLZklL9ohVcLHLO1pSecPJSDCCR7e_-PVobl9Lj8,3063
9
9
  enterprise_data/signals.py,sha256=8eqNPnlvmfsKf19lGWv5xTIuBgQIqR8EZSp9UYzC8Rc,1024
10
10
  enterprise_data/urls.py,sha256=bqtKF5OEWEwrNmHG3os-pZNuNsmjlhxEqp7yM4TbPf4,243
11
- enterprise_data/utils.py,sha256=sDrpBd62DpybCV41QCxRUaCuvch3qKjEhfUp9cA_GV0,2952
11
+ enterprise_data/utils.py,sha256=djGuQUnvT8HJMMV60CzcD5BWerPIwiK9uG26bhkpWMg,3518
12
12
  enterprise_data/admin_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  enterprise_data/admin_analytics/constants.py,sha256=-6uLAq5DUeA_rv5eUb9SeqlG3iVWV30qUS8asbK4430,160
14
14
  enterprise_data/admin_analytics/data_loaders.py,sha256=b6RjEIxCol8ETQMY7QfwhqN9eEAvrUN_UldIG7rgsSY,736
@@ -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=mya_ZvP7CTvUTF_ccYkMJZFuzOKIU-aM3eSCQmFazWY,11274
33
+ enterprise_data/api/v1/serializers.py,sha256=DUhk8cP-JFQmbLR_JNGmmwis9Un5dyvd01kRdmxn8Lk,11866
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
@@ -106,6 +106,8 @@ enterprise_data/migrations/0040_auto_20240718_0536_squashed_0043_alter_enterpris
106
106
  enterprise_data/migrations/0044_enterpriseexecedlcmoduleperformance.py,sha256=1aJ0wIu9K2cT3AJdBIHuNCUpJysPWmq_37Ucp5ntEO8,6122
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
+ 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
109
111
  enterprise_data/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
110
112
  enterprise_data/settings/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
111
113
  enterprise_data/settings/test.py,sha256=4-flfrlf81AthGx9wTaT5PscyoOWyhsDDqbzBl-z7Eg,4191
@@ -173,8 +175,8 @@ enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=WtL-RqGgu2x5PP
173
175
  enterprise_reporting/tests/test_utils.py,sha256=Zt_TA0LVb-B6fQGkUkAKKVlUKKnQh8jnw1US1jKe7g8,9493
174
176
  enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
175
177
  enterprise_reporting/tests/utils.py,sha256=xms2LM7DV3wczXEfctOK1ddel1EE0J_YSr17UzbCDy4,1401
176
- edx_enterprise_data-9.7.11.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
177
- edx_enterprise_data-9.7.11.dist-info/METADATA,sha256=lQKZwcAoJZxQA-ec3JXt_jrmJAcsk0qKnnB47mywOwU,1570
178
- edx_enterprise_data-9.7.11.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
179
- edx_enterprise_data-9.7.11.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
180
- edx_enterprise_data-9.7.11.dist-info/RECORD,,
178
+ edx_enterprise_data-9.9.1.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
179
+ edx_enterprise_data-9.9.1.dist-info/METADATA,sha256=ncWbp84mP_4B5WW0va6GldAOyH5AxZ-3j6XacbwQjWY,1569
180
+ edx_enterprise_data-9.9.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
181
+ edx_enterprise_data-9.9.1.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
182
+ edx_enterprise_data-9.9.1.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.7.11"
5
+ __version__ = "9.9.1"
@@ -16,6 +16,7 @@ from enterprise_data.models import (
16
16
  EnterpriseOffer,
17
17
  EnterpriseSubsidyBudget,
18
18
  )
19
+ from enterprise_data.utils import calculate_percentage_difference
19
20
 
20
21
 
21
22
  class EnterpriseLearnerEnrollmentSerializer(serializers.ModelSerializer):
@@ -231,6 +232,7 @@ class EnterpriseExecEdLCModulePerformanceSerializer(serializers.ModelSerializer)
231
232
  Serializer for EnterpriseExecEdLCModulePerformance model.
232
233
  """
233
234
  extensions_requested = serializers.SerializerMethodField()
235
+ avg_lo_percentage_difference = serializers.SerializerMethodField()
234
236
 
235
237
  class Meta:
236
238
  model = EnterpriseExecEdLCModulePerformance
@@ -240,6 +242,18 @@ class EnterpriseExecEdLCModulePerformanceSerializer(serializers.ModelSerializer)
240
242
  """Return extensions_requested if not None, otherwise return 0"""
241
243
  return obj.extensions_requested if obj.extensions_requested is not None else 0
242
244
 
245
+ def get_avg_lo_percentage_difference(self, obj):
246
+ """
247
+ Return percentage difference between `avg_before_lo_score` and `avg_after_lo_score` if not None,
248
+ otherwise return None
249
+ """
250
+ if obj.avg_before_lo_score is None or obj.avg_after_lo_score is None:
251
+ return None
252
+ return round(
253
+ calculate_percentage_difference(obj.avg_before_lo_score, obj.avg_after_lo_score),
254
+ 2
255
+ )
256
+
243
257
 
244
258
  class EnterpriseBudgetSerializer(serializers.ModelSerializer):
245
259
  """
@@ -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,28 @@
1
+ # Generated by Django 4.2.15 on 2025-02-20 08:08
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('enterprise_data', '0046_enterprisegroupmembership'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='enterpriseexecedlcmoduleperformance',
15
+ name='avg_after_lo_score',
16
+ field=models.DecimalField(decimal_places=6, max_digits=38, null=True),
17
+ ),
18
+ migrations.AddField(
19
+ model_name='enterpriseexecedlcmoduleperformance',
20
+ name='avg_before_lo_score',
21
+ field=models.DecimalField(decimal_places=6, max_digits=38, null=True),
22
+ ),
23
+ migrations.AddField(
24
+ model_name='enterpriseexecedlcmoduleperformance',
25
+ name='question_name',
26
+ field=models.CharField(max_length=500, null=True),
27
+ ),
28
+ ]
@@ -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
@@ -558,6 +558,9 @@ class EnterpriseExecEdLCModulePerformance(models.Model):
558
558
  discussion_forum_activities_completed_count = models.PositiveIntegerField(null=True)
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
+ question_name = models.CharField(max_length=500, 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)
561
564
 
562
565
  def __str__(self):
563
566
  """
enterprise_data/utils.py CHANGED
@@ -114,3 +114,22 @@ def find_first(iterable, condition):
114
114
  return next(item for item in iterable if condition(item))
115
115
  except StopIteration:
116
116
  return None
117
+
118
+
119
+ def calculate_percentage_difference(first, second):
120
+ """
121
+ Calculate the percentage difference between two numbers.
122
+
123
+ It will calculate the percentage difference between the two numbers using the formula:
124
+ ((second - first) / (first + second)/2) * 100
125
+
126
+ Arguments:
127
+ first (float): The first number.
128
+ second (float): The second number.
129
+
130
+ Returns:
131
+ float: The percentage difference between the two numbers.
132
+ """
133
+ if first == 0 and second == 0:
134
+ return 0
135
+ return ((second - first) / ((first + second) / 2)) * 100