edx-enterprise-data 10.9.8__py3-none-any.whl → 10.10.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.
- {edx_enterprise_data-10.9.8.dist-info → edx_enterprise_data-10.10.1.dist-info}/METADATA +14 -6
- {edx_enterprise_data-10.9.8.dist-info → edx_enterprise_data-10.10.1.dist-info}/RECORD +23 -20
- {edx_enterprise_data-10.9.8.dist-info → edx_enterprise_data-10.10.1.dist-info}/WHEEL +1 -1
- enterprise_data/__init__.py +1 -1
- enterprise_data/admin_analytics/data_loaders.py +1 -1
- enterprise_data/api/v1/serializers.py +65 -2
- enterprise_data/api/v1/views/enterprise_admin.py +1 -1
- enterprise_data/api/v1/views/enterprise_learner.py +47 -6
- enterprise_data/clients.py +27 -0
- enterprise_data/migrations/0047_enterpriseexecedlcmoduleperformance_avg_after_lo_score_and_more.py +28 -0
- enterprise_data/migrations/0048_alter_enterpriseexecedlcmoduleperformance_avg_after_lo_score_and_more.py +23 -0
- enterprise_data/models.py +3 -0
- enterprise_data/renderers.py +1 -1
- enterprise_data/utils.py +19 -0
- enterprise_reporting/constants.py +7 -0
- enterprise_reporting/delivery_method.py +36 -7
- enterprise_reporting/send_enterprise_reports.py +13 -3
- enterprise_reporting/tests/test_delivery_method.py +46 -3
- enterprise_reporting/tests/test_send_enterprise_reports.py +1 -1
- enterprise_reporting/tests/test_utils.py +1 -0
- enterprise_reporting/utils.py +2 -5
- {edx_enterprise_data-10.9.8.dist-info → edx_enterprise_data-10.10.1.dist-info}/LICENSE +0 -0
- {edx_enterprise_data-10.9.8.dist-info → edx_enterprise_data-10.10.1.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.2
|
2
2
|
Name: edx-enterprise-data
|
3
|
-
Version: 10.
|
3
|
+
Version: 10.10.1
|
4
4
|
Summary: Enterprise Reporting
|
5
5
|
Home-page: https://github.com/openedx/edx-enterprise-data
|
6
6
|
Author: edX
|
@@ -9,7 +9,6 @@ License: AGPL 3.0
|
|
9
9
|
Classifier: Framework :: Django
|
10
10
|
Classifier: Framework :: Django :: 4.2
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
12
|
-
Classifier: Programming Language :: Python :: 3.8
|
13
12
|
Classifier: Programming Language :: Python :: 3.12
|
14
13
|
License-File: LICENSE
|
15
14
|
Requires-Dist: Django
|
@@ -22,10 +21,10 @@ Requires-Dist: edx-drf-extensions
|
|
22
21
|
Requires-Dist: edx-opaque-keys
|
23
22
|
Requires-Dist: edx-rbac
|
24
23
|
Requires-Dist: edx-rest-api-client
|
25
|
-
Requires-Dist:
|
24
|
+
Requires-Dist: factory_boy
|
26
25
|
Requires-Dist: mysql-connector-python
|
27
|
-
Requires-Dist: numpy
|
28
|
-
Requires-Dist: pandas
|
26
|
+
Requires-Dist: numpy
|
27
|
+
Requires-Dist: pandas
|
29
28
|
Requires-Dist: requests
|
30
29
|
Requires-Dist: rules
|
31
30
|
Provides-Extra: reporting
|
@@ -40,5 +39,14 @@ Requires-Dist: pyminizip; extra == "reporting"
|
|
40
39
|
Requires-Dist: snowflake-connector-python; extra == "reporting"
|
41
40
|
Requires-Dist: unicodecsv==0.14.1; extra == "reporting"
|
42
41
|
Requires-Dist: vertica-python; extra == "reporting"
|
42
|
+
Dynamic: author
|
43
|
+
Dynamic: author-email
|
44
|
+
Dynamic: classifier
|
45
|
+
Dynamic: description
|
46
|
+
Dynamic: home-page
|
47
|
+
Dynamic: license
|
48
|
+
Dynamic: provides-extra
|
49
|
+
Dynamic: requires-dist
|
50
|
+
Dynamic: summary
|
43
51
|
|
44
52
|
Tools and products related to providing access to Enterprise data.
|
@@ -1,17 +1,17 @@
|
|
1
|
-
enterprise_data/__init__.py,sha256=
|
1
|
+
enterprise_data/__init__.py,sha256=ZNUDCqsK0PsxTU8WCuIxtRJVuai59_dk0bJuCzGqMgY,125
|
2
2
|
enterprise_data/apps.py,sha256=aF6hZwDfI2oWj95tUTm_2ikHueQj-jLj-u0GrgzpsQI,414
|
3
|
-
enterprise_data/clients.py,sha256=
|
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
|
+
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=
|
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
|
-
enterprise_data/utils.py,sha256=
|
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
|
-
enterprise_data/admin_analytics/data_loaders.py,sha256=
|
14
|
+
enterprise_data/admin_analytics/data_loaders.py,sha256=b6RjEIxCol8ETQMY7QfwhqN9eEAvrUN_UldIG7rgsSY,736
|
15
15
|
enterprise_data/admin_analytics/database/__init__.py,sha256=vNSWKf2VV5xMegN7htJJtxtQEb0ASLC6frE2w0ZpYpE,104
|
16
16
|
enterprise_data/admin_analytics/database/utils.py,sha256=5u-d6ZQW95mF_r4bH8Xdi7DgpYAuDFOG_q0P-bjKXHU,1712
|
17
17
|
enterprise_data/admin_analytics/database/queries/__init__.py,sha256=IC5TLOr_GnydbrVbl2mWhwO3aUbYeHuDmfPTLmwGhZA,218
|
@@ -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=
|
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
|
@@ -38,8 +38,8 @@ enterprise_data/api/v1/views/analytics_engagements.py,sha256=Yo-bpA-0xOQHUPTFF0j
|
|
38
38
|
enterprise_data/api/v1/views/analytics_enrollments.py,sha256=hw87VZ0hFWpwf3QEHFn9cUgDy2s7SzsXt6Rf9TaNsS0,6282
|
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
|
-
enterprise_data/api/v1/views/enterprise_admin.py,sha256=
|
42
|
-
enterprise_data/api/v1/views/enterprise_learner.py,sha256=
|
41
|
+
enterprise_data/api/v1/views/enterprise_admin.py,sha256=xxpb48cjuHkFJH-IQG0DwOvdH8RgyYD-aQjyfMMJNFg,9030
|
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
|
@@ -151,11 +153,12 @@ enterprise_data_roles/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
|
|
151
153
|
enterprise_data_roles/tests/factories.py,sha256=VOm0NyOhiKkxl1MRIriRkKSpKypr9-4lG2FrsE2YfKo,1153
|
152
154
|
enterprise_data_roles/tests/test_models.py,sha256=wJv7ywk0BSbjlW_U142h0aFxZleAHyT92nIiPy_ECW4,1476
|
153
155
|
enterprise_reporting/__init__.py,sha256=yQO9ureIxFnl-1a_34H53elDwuAzXrSmhLlzqqD2SJ0,112
|
154
|
-
enterprise_reporting/
|
156
|
+
enterprise_reporting/constants.py,sha256=LAi9HPXDF5I8xT4E65YTO6FhnlK_6MdNpBeBVZ9Ckdk,211
|
157
|
+
enterprise_reporting/delivery_method.py,sha256=JpNEDT3yk4IV27OsqIq0iLv3xcTkW9B285P91YBUrsA,6109
|
155
158
|
enterprise_reporting/external_resource_link_report.py,sha256=jQ6RS0yec0IhAz4wErQ3q8Yn206R7aQbgcR2c803BLA,8066
|
156
159
|
enterprise_reporting/reporter.py,sha256=3wI46qH-CNCUC5r9-Eme1mQdMjwEsFk9myRb-ajzJkM,13807
|
157
|
-
enterprise_reporting/send_enterprise_reports.py,sha256=
|
158
|
-
enterprise_reporting/utils.py,sha256=
|
160
|
+
enterprise_reporting/send_enterprise_reports.py,sha256=W9xc7hu3ZP4zmoIndITc3hdXDbd4A3QEWQN0_ZO1E1A,5270
|
161
|
+
enterprise_reporting/utils.py,sha256=L_ENZYE-UgTy3sHRJSXjN-Hb3qkT-VLAIIqzIu9fpDQ,13745
|
159
162
|
enterprise_reporting/clients/__init__.py,sha256=9xW-Nj1A3JWb9rOWVFdFaDzcyremAS-whVB8DRW_wCY,5121
|
160
163
|
enterprise_reporting/clients/enterprise.py,sha256=-ZKoQTyDLYPLDfC7hWKhJZxVOorWt0kYmFAiOUJkyNM,9853
|
161
164
|
enterprise_reporting/clients/s3.py,sha256=CZ9FgwOGKo-lQmZ4cw8oIqoVmhEtwBb6jfhFqoNHgh4,559
|
@@ -165,16 +168,16 @@ enterprise_reporting/fixtures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
|
|
165
168
|
enterprise_reporting/fixtures/enterprise_customer_reporting.json,sha256=nS6E9KHW0Iqk7ZHtTyyVyrztIXxjn9OtBvMJkn7owxc,3959
|
166
169
|
enterprise_reporting/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
167
170
|
enterprise_reporting/tests/test_clients.py,sha256=h-h7xBJ6wIBKP-QqRxcJJGiQxLfTOLYByCuWfcCeCy0,8474
|
168
|
-
enterprise_reporting/tests/test_delivery_method.py,sha256=
|
171
|
+
enterprise_reporting/tests/test_delivery_method.py,sha256=_nvpxPjYb59ix0nsgX4Ani0gJ0mgcBCgfHpw0rS0scc,4561
|
169
172
|
enterprise_reporting/tests/test_enterprise_client.py,sha256=lpWm0muvA3alRjmlRAezE5901C9DU3WiySH4D5-U3qE,1058
|
170
173
|
enterprise_reporting/tests/test_external_link_report.py,sha256=zdnVOD1qtAp9c5EbIPnD9jcoLtW4iKs7gSVklgBK328,7029
|
171
174
|
enterprise_reporting/tests/test_reporter.py,sha256=PTmkGvPjGEjxiyizL88LAKnaWdvZDgOBjL4QStfOdyw,4057
|
172
|
-
enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=
|
173
|
-
enterprise_reporting/tests/test_utils.py,sha256=
|
175
|
+
enterprise_reporting/tests/test_send_enterprise_reports.py,sha256=zBj7sDvRLJQbRsOHwYeOzTGrKx7t7JUq5dLNGASiq7o,1052
|
176
|
+
enterprise_reporting/tests/test_utils.py,sha256=y4t6w9aKra-ftqtUeHvPwOhje-1npz7auV5o74ya8fE,9523
|
174
177
|
enterprise_reporting/tests/test_vertica_client.py,sha256=-R2yNCGUjRtoXwLMBloVFQkFYrJoo613VCr61gwI3kQ,140
|
175
178
|
enterprise_reporting/tests/utils.py,sha256=xms2LM7DV3wczXEfctOK1ddel1EE0J_YSr17UzbCDy4,1401
|
176
|
-
edx_enterprise_data-10.
|
177
|
-
edx_enterprise_data-10.
|
178
|
-
edx_enterprise_data-10.
|
179
|
-
edx_enterprise_data-10.
|
180
|
-
edx_enterprise_data-10.
|
179
|
+
edx_enterprise_data-10.10.1.dist-info/LICENSE,sha256=dql8h4yceoMhuzlcK0TT_i-NgTFNIZsgE47Q4t3dUYI,34520
|
180
|
+
edx_enterprise_data-10.10.1.dist-info/METADATA,sha256=hDRA9ivxmE2KXvocGs8ttz8nEI_sKRVhNdUAHa5wN6w,1685
|
181
|
+
edx_enterprise_data-10.10.1.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
182
|
+
edx_enterprise_data-10.10.1.dist-info/top_level.txt,sha256=f5F2kU-dob6MqiHJpgZkFzoCD5VMhsdpkTV5n9Tvq3I,59
|
183
|
+
edx_enterprise_data-10.10.1.dist-info/RECORD,,
|
enterprise_data/__init__.py
CHANGED
@@ -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,
|
@@ -16,6 +17,7 @@ from enterprise_data.models import (
|
|
16
17
|
EnterpriseOffer,
|
17
18
|
EnterpriseSubsidyBudget,
|
18
19
|
)
|
20
|
+
from enterprise_data.utils import calculate_percentage_difference
|
19
21
|
|
20
22
|
|
21
23
|
class EnterpriseLearnerEnrollmentSerializer(serializers.ModelSerializer):
|
@@ -25,6 +27,8 @@ class EnterpriseLearnerEnrollmentSerializer(serializers.ModelSerializer):
|
|
25
27
|
course_api_url = serializers.SerializerMethodField()
|
26
28
|
enterprise_user_id = serializers.SerializerMethodField()
|
27
29
|
total_learning_time_hours = serializers.SerializerMethodField()
|
30
|
+
enterprise_flex_group_name = serializers.SerializerMethodField()
|
31
|
+
enterprise_flex_group_uuid = serializers.SerializerMethodField()
|
28
32
|
|
29
33
|
class Meta:
|
30
34
|
model = EnterpriseLearnerEnrollment
|
@@ -43,8 +47,9 @@ class EnterpriseLearnerEnrollmentSerializer(serializers.ModelSerializer):
|
|
43
47
|
'last_activity_date', 'progress_status', 'passed_date', 'current_grade',
|
44
48
|
'letter_grade', 'enterprise_user_id', 'user_email', 'user_account_creation_date',
|
45
49
|
'user_country_code', 'user_username', 'user_first_name', 'user_last_name', 'enterprise_name',
|
46
|
-
'enterprise_customer_uuid', 'enterprise_sso_uid', 'created', 'course_api_url',
|
47
|
-
'
|
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',
|
48
53
|
)
|
49
54
|
|
50
55
|
def get_course_api_url(self, obj):
|
@@ -61,6 +66,51 @@ class EnterpriseLearnerEnrollmentSerializer(serializers.ModelSerializer):
|
|
61
66
|
"""Returns the learners total learning time in hours"""
|
62
67
|
return round((obj.total_learning_time_seconds or 0.0)/3600.0, 2)
|
63
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
|
+
|
64
114
|
|
65
115
|
class EnterpriseSubsidyBudgetSerializer(serializers.ModelSerializer):
|
66
116
|
"""
|
@@ -231,6 +281,7 @@ class EnterpriseExecEdLCModulePerformanceSerializer(serializers.ModelSerializer)
|
|
231
281
|
Serializer for EnterpriseExecEdLCModulePerformance model.
|
232
282
|
"""
|
233
283
|
extensions_requested = serializers.SerializerMethodField()
|
284
|
+
avg_lo_percentage_difference = serializers.SerializerMethodField()
|
234
285
|
|
235
286
|
class Meta:
|
236
287
|
model = EnterpriseExecEdLCModulePerformance
|
@@ -240,6 +291,18 @@ class EnterpriseExecEdLCModulePerformanceSerializer(serializers.ModelSerializer)
|
|
240
291
|
"""Return extensions_requested if not None, otherwise return 0"""
|
241
292
|
return obj.extensions_requested if obj.extensions_requested is not None else 0
|
242
293
|
|
294
|
+
def get_avg_lo_percentage_difference(self, obj):
|
295
|
+
"""
|
296
|
+
Return percentage difference between `avg_before_lo_score` and `avg_after_lo_score` if not None,
|
297
|
+
otherwise return None
|
298
|
+
"""
|
299
|
+
if obj.avg_before_lo_score is None or obj.avg_after_lo_score is None:
|
300
|
+
return None
|
301
|
+
return round(
|
302
|
+
calculate_percentage_difference(obj.avg_before_lo_score, obj.avg_after_lo_score),
|
303
|
+
2
|
304
|
+
)
|
305
|
+
|
243
306
|
|
244
307
|
class EnterpriseBudgetSerializer(serializers.ModelSerializer):
|
245
308
|
"""
|
@@ -126,7 +126,7 @@ class EnterpriseAdminAnalyticsAggregatesView(APIView):
|
|
126
126
|
'completions': completions,
|
127
127
|
'hours': hours,
|
128
128
|
'sessions': sessions,
|
129
|
-
'last_updated_at': last_updated_at
|
129
|
+
'last_updated_at': last_updated_at if last_updated_at else None,
|
130
130
|
'min_enrollment_date': min_enrollment_date,
|
131
131
|
'max_enrollment_date': max_enrollment_date,
|
132
132
|
},
|
@@ -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
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
|
enterprise_data/clients.py
CHANGED
@@ -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
|
enterprise_data/migrations/0047_enterpriseexecedlcmoduleperformance_avg_after_lo_score_and_more.py
ADDED
@@ -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/renderers.py
CHANGED
@@ -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', '
|
30
|
+
'is_subsidy', 'course_product_line', 'budget_id', 'enterprise_flex_group_name', 'enterprise_flex_group_uuid',
|
31
31
|
]
|
32
32
|
|
33
33
|
|
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
|
@@ -2,15 +2,19 @@
|
|
2
2
|
Classes that handle sending reports for enterprise customers with specific delivery methods.
|
3
3
|
"""
|
4
4
|
|
5
|
-
|
6
|
-
|
7
5
|
import logging
|
8
6
|
import os
|
9
7
|
from smtplib import SMTPException
|
10
8
|
|
11
9
|
import paramiko
|
12
10
|
|
13
|
-
from enterprise_reporting.
|
11
|
+
from enterprise_reporting.constants import SFTP_OPS_GENIE_EMAIL_ALERT_EMAILS, SFTP_OPS_GENIE_EMAIL_ALERT_FROM_EMAIL
|
12
|
+
from enterprise_reporting.utils import (
|
13
|
+
compress_and_encrypt,
|
14
|
+
decrypt_string,
|
15
|
+
retry_on_exception,
|
16
|
+
send_email_with_attachment,
|
17
|
+
)
|
14
18
|
|
15
19
|
LOGGER = logging.getLogger(__name__)
|
16
20
|
|
@@ -97,6 +101,8 @@ class SFTPDeliveryMethod(DeliveryMethod):
|
|
97
101
|
"""
|
98
102
|
Class that handles sending an enterprise report file via SFTP.
|
99
103
|
"""
|
104
|
+
sender_email = SFTP_OPS_GENIE_EMAIL_ALERT_FROM_EMAIL
|
105
|
+
receiver_emails = SFTP_OPS_GENIE_EMAIL_ALERT_EMAILS
|
100
106
|
|
101
107
|
def __init__(self, reporting_config, password):
|
102
108
|
"""Initialize the SFTP Delivery Method."""
|
@@ -106,9 +112,11 @@ class SFTPDeliveryMethod(DeliveryMethod):
|
|
106
112
|
self.username = reporting_config['sftp_username']
|
107
113
|
self.file_path = reporting_config['sftp_file_path']
|
108
114
|
|
109
|
-
|
110
|
-
|
111
|
-
|
115
|
+
@retry_on_exception(max_retries=3, delay=2, backoff=2)
|
116
|
+
def send_over_sftp(self, data_reports):
|
117
|
+
"""
|
118
|
+
Send the reports via SFTP, retry on exception.
|
119
|
+
"""
|
112
120
|
LOGGER.info('Connecting via SFTP to remote host {} for {}'.format(
|
113
121
|
self.hostname,
|
114
122
|
self.enterprise_customer_name
|
@@ -129,4 +137,25 @@ class SFTPDeliveryMethod(DeliveryMethod):
|
|
129
137
|
)
|
130
138
|
sftp.close()
|
131
139
|
ssh.close()
|
132
|
-
|
140
|
+
|
141
|
+
def send(self, files):
|
142
|
+
"""Send the given files through SFTP."""
|
143
|
+
try:
|
144
|
+
data_reports = super().send(files)
|
145
|
+
self.send_over_sftp(data_reports)
|
146
|
+
except Exception: # pylint: disable=broad-except
|
147
|
+
email_subject = f'SFTP transmission failed for {self.enterprise_customer_name}'
|
148
|
+
email_body = f'Failed to send {self.data_type} report for {self.enterprise_customer_name}'
|
149
|
+
LOGGER.exception(f'SFTP transmission failed for {self.enterprise_customer_name}')
|
150
|
+
else:
|
151
|
+
LOGGER.info(f'Successfully sent report via sftp for {self.enterprise_customer_name}')
|
152
|
+
email_subject = f'SFTP transmission successful for {self.enterprise_customer_name}'
|
153
|
+
email_body = f'SFTP transmission successful for {self.enterprise_customer_name}'
|
154
|
+
|
155
|
+
send_email_with_attachment(
|
156
|
+
subject=email_subject,
|
157
|
+
body=email_body,
|
158
|
+
from_email=self.sender_email,
|
159
|
+
to_email=self.receiver_emails,
|
160
|
+
attachment_data={},
|
161
|
+
)
|
@@ -4,13 +4,15 @@ Sends an Enterprise Customer's data file to a configured destination.
|
|
4
4
|
"""
|
5
5
|
|
6
6
|
|
7
|
-
|
8
7
|
import argparse
|
8
|
+
import datetime
|
9
9
|
import logging
|
10
10
|
import os
|
11
11
|
import re
|
12
12
|
import sys
|
13
13
|
|
14
|
+
import pytz
|
15
|
+
|
14
16
|
from enterprise_reporting.clients.enterprise import EnterpriseAPIClient
|
15
17
|
from enterprise_reporting.reporter import EnterpriseReportSender
|
16
18
|
from enterprise_reporting.utils import is_current_time_in_schedule
|
@@ -55,11 +57,13 @@ def cleanup_files(enterprise_id):
|
|
55
57
|
os.remove(os.path.join(directory, f))
|
56
58
|
|
57
59
|
|
58
|
-
def should_deliver_report(args, reporting_config):
|
60
|
+
def should_deliver_report(args, reporting_config, current_est_time):
|
59
61
|
"""Given CLI arguments and the reporting configuration, determine if delivery should happen."""
|
60
62
|
valid_data_type = reporting_config['data_type'] in (args.data_type or DATA_TYPES)
|
61
63
|
enterprise_customer_specified = bool(args.enterprise_customer)
|
64
|
+
|
62
65
|
meets_schedule_requirement = is_current_time_in_schedule(
|
66
|
+
current_est_time,
|
63
67
|
reporting_config['frequency'],
|
64
68
|
reporting_config['hour_of_day'],
|
65
69
|
reporting_config['day_of_month'],
|
@@ -101,6 +105,12 @@ def process_reports():
|
|
101
105
|
LOGGER.error(f'The enterprise {args.enterprise_customer} does not have a reporting configuration.')
|
102
106
|
sys.exit(1)
|
103
107
|
|
108
|
+
# We are defining the current est time globally because we want the current time for a job
|
109
|
+
# to remain same thoughout the job. This ensures that a single report is not processed multiple times.
|
110
|
+
# See this comment for more details: https://2u-internal.atlassian.net/browse/ENT-9954?focusedCommentId=5356815
|
111
|
+
est_timezone = pytz.timezone('US/Eastern')
|
112
|
+
current_est_time = datetime.datetime.now(est_timezone)
|
113
|
+
|
104
114
|
error_raised = False
|
105
115
|
for reporting_config in reporting_configs['results']:
|
106
116
|
LOGGER.info('Checking if {}\'s reporting config for {} data in {} format is ready for processing'.format(
|
@@ -109,7 +119,7 @@ def process_reports():
|
|
109
119
|
reporting_config['report_type'],
|
110
120
|
))
|
111
121
|
|
112
|
-
if should_deliver_report(args, reporting_config):
|
122
|
+
if should_deliver_report(args, reporting_config, current_est_time):
|
113
123
|
if send_data(reporting_config):
|
114
124
|
error_raised = True
|
115
125
|
else:
|
@@ -2,13 +2,12 @@
|
|
2
2
|
Test delivery methods.
|
3
3
|
"""
|
4
4
|
|
5
|
-
import sys
|
6
5
|
import unittest
|
7
6
|
|
8
7
|
import ddt
|
9
|
-
import pytest
|
10
8
|
|
11
|
-
from
|
9
|
+
from mock import patch, MagicMock
|
10
|
+
from enterprise_reporting.delivery_method import SFTPDeliveryMethod, SMTPDeliveryMethod
|
12
11
|
from enterprise_reporting.utils import encrypt_string
|
13
12
|
|
14
13
|
from .utils import create_files, verify_compressed
|
@@ -73,3 +72,47 @@ class TestDeliveryMethod(unittest.TestCase):
|
|
73
72
|
else:
|
74
73
|
assert len(delivery_files) == len(delivery_files)
|
75
74
|
assert delivery_files == [file['file'].name for file in files]
|
75
|
+
|
76
|
+
@patch('enterprise_reporting.delivery_method.send_email_with_attachment')
|
77
|
+
def test_verify_sftp_exception_handling(self, mock_send_email_with_attachment):
|
78
|
+
"""
|
79
|
+
Verify that SFTP transmission related exceptions are being handled correctly.
|
80
|
+
"""
|
81
|
+
file_data = [
|
82
|
+
{
|
83
|
+
'name': 'A History of Magic.txt',
|
84
|
+
'size': 1000
|
85
|
+
},
|
86
|
+
{
|
87
|
+
'name': 'Quidditch Through the Ages.txt',
|
88
|
+
'size': 500
|
89
|
+
},
|
90
|
+
{
|
91
|
+
'name': 'Quidditch Through the Ages.txt',
|
92
|
+
'size': 500
|
93
|
+
},
|
94
|
+
]
|
95
|
+
files, total_original_size = create_files(file_data)
|
96
|
+
sftp_delivery_method = SFTPDeliveryMethod(self.reporting_config, self.password)
|
97
|
+
|
98
|
+
# Verify failed SFTP transmission.
|
99
|
+
with patch('paramiko.SSHClient.connect', side_effect=Exception('SFTP transmission failed')):
|
100
|
+
sftp_delivery_method.send([file['file'] for file in files])
|
101
|
+
mock_send_email_with_attachment.assert_called_with(
|
102
|
+
subject='SFTP transmission failed for bleh-bleh',
|
103
|
+
body='Failed to send progress report for bleh-bleh',
|
104
|
+
from_email='enterprise-integrations@edx.org',
|
105
|
+
to_email=['enterprise-reporting-sftp@2u-internal.opsgenie.net'],
|
106
|
+
attachment_data={},
|
107
|
+
)
|
108
|
+
|
109
|
+
# Verify successful SFTP transmission.
|
110
|
+
with patch('paramiko.SSHClient', MagicMock()):
|
111
|
+
sftp_delivery_method.send([file['file'] for file in files])
|
112
|
+
mock_send_email_with_attachment.assert_called_with(
|
113
|
+
subject='SFTP transmission successful for bleh-bleh',
|
114
|
+
body='SFTP transmission successful for bleh-bleh',
|
115
|
+
from_email='enterprise-integrations@edx.org',
|
116
|
+
to_email=['enterprise-reporting-sftp@2u-internal.opsgenie.net'],
|
117
|
+
attachment_data={},
|
118
|
+
)
|
@@ -33,4 +33,4 @@ class TestSendEnterpriseReports(unittest.TestCase):
|
|
33
33
|
Command = namedtuple("Command", "data_type enterprise_customer")
|
34
34
|
args = Command('', '')
|
35
35
|
|
36
|
-
assert should_deliver_report(args, reporting_config)
|
36
|
+
assert should_deliver_report(args, reporting_config, current_est_time)
|
@@ -165,6 +165,7 @@ class TestUtilities(unittest.TestCase):
|
|
165
165
|
est_timezone = pytz.timezone('US/Eastern')
|
166
166
|
current_est_time = datetime.datetime.now(est_timezone)
|
167
167
|
assert utils.is_current_time_in_schedule(
|
168
|
+
current_est_time,
|
168
169
|
utils.FREQUENCY_TYPE_DAILY,
|
169
170
|
current_est_time.hour,
|
170
171
|
current_est_time.day,
|
enterprise_reporting/utils.py
CHANGED
@@ -3,7 +3,6 @@ Utility functions for Enterprise Reporting.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
|
6
|
-
import datetime
|
7
6
|
import logging
|
8
7
|
import os
|
9
8
|
import re
|
@@ -16,7 +15,6 @@ from urllib.parse import parse_qs, urlparse
|
|
16
15
|
import boto3
|
17
16
|
import pgpy
|
18
17
|
import pyminizip
|
19
|
-
import pytz
|
20
18
|
from cryptography.fernet import Fernet
|
21
19
|
from fernet_fields.hkdf import derive_fernet_key
|
22
20
|
|
@@ -149,12 +147,11 @@ def send_email_with_attachment(subject, body, from_email, to_email, attachment_d
|
|
149
147
|
LOGGER.debug(result)
|
150
148
|
|
151
149
|
|
152
|
-
def is_current_time_in_schedule(frequency, hour_of_day, day_of_month=None, day_of_week=None):
|
150
|
+
def is_current_time_in_schedule(current_est_time, frequency, hour_of_day, day_of_month=None, day_of_week=None):
|
153
151
|
"""
|
154
152
|
Determine if the current time is in the range specified by this configuration's schedule.
|
155
153
|
"""
|
156
|
-
|
157
|
-
current_est_time = datetime.datetime.now(est_timezone)
|
154
|
+
|
158
155
|
current_hour_of_day = current_est_time.hour
|
159
156
|
current_day_of_week = current_est_time.weekday()
|
160
157
|
current_day_of_month = current_est_time.day
|
File without changes
|
{edx_enterprise_data-10.9.8.dist-info → edx_enterprise_data-10.10.1.dist-info}/top_level.txt
RENAMED
File without changes
|