Skip to content

Commit ac8f8a0

Browse files
authored
Merge branch 'master' into jci/issue35245
2 parents 34b532e + 1fe67d3 commit ac8f8a0

File tree

22 files changed

+410
-209
lines changed

22 files changed

+410
-209
lines changed

cms/envs/common.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2520,6 +2520,8 @@
25202520
CREDENTIALS_INTERNAL_SERVICE_URL = 'http://localhost:8005'
25212521
CREDENTIALS_PUBLIC_SERVICE_URL = 'http://localhost:8005'
25222522
CREDENTIALS_SERVICE_USERNAME = 'credentials_service_user'
2523+
# time between scheduled runs, in seconds
2524+
NOTIFY_CREDENTIALS_FREQUENCY = 14400
25232525

25242526
ANALYTICS_DASHBOARD_URL = 'http://localhost:18110/courses'
25252527
ANALYTICS_DASHBOARD_NAME = 'Your Platform Name Here Insights'

lms/djangoapps/courseware/access_utils.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
It allows us to share code between access.py and block transformers.
44
"""
55

6-
76
from datetime import datetime, timedelta
87
from logging import getLogger
98

@@ -21,7 +20,7 @@
2120
EnrollmentRequiredAccessError,
2221
IncorrectActiveEnterpriseAccessError,
2322
StartDateEnterpriseLearnerError,
24-
StartDateError
23+
StartDateError,
2524
)
2625
from lms.djangoapps.courseware.masquerade import get_course_masquerade, is_masquerading_as_student
2726
from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, COURSE_PRE_START_ACCESS_FLAG
@@ -58,9 +57,12 @@ def adjust_start_date(user, days_early_for_beta, start, course_key):
5857

5958
if CourseBetaTesterRole(course_key).has_user(user):
6059
debug("Adjust start time: user in beta role for %s", course_key)
61-
delta = timedelta(days_early_for_beta)
62-
effective = start - delta
63-
return effective
60+
# timedelta.max days from now is in the year 2739931, so that's probably pretty safe
61+
delta = timedelta(min(days_early_for_beta, timedelta.max.days))
62+
try:
63+
return start - delta
64+
except OverflowError:
65+
return start
6466

6567
return start
6668

@@ -93,7 +95,7 @@ def enterprise_learner_enrolled(request, user, course_key):
9395
# enterprise_customer_data is either None (if learner is not linked to any customer) or a serialized
9496
# EnterpriseCustomer representing the learner's active linked customer.
9597
enterprise_customer_data = enterprise_customer_from_session_or_learner_data(request)
96-
learner_portal_enabled = enterprise_customer_data and enterprise_customer_data['enable_learner_portal']
98+
learner_portal_enabled = enterprise_customer_data and enterprise_customer_data["enable_learner_portal"]
9799
if not learner_portal_enabled:
98100
return False
99101

@@ -102,18 +104,18 @@ def enterprise_learner_enrolled(request, user, course_key):
102104
enterprise_enrollments = EnterpriseCourseEnrollment.objects.filter(
103105
course_id=course_key,
104106
enterprise_customer_user__user_id=user.id,
105-
enterprise_customer_user__enterprise_customer__uuid=enterprise_customer_data['uuid'],
107+
enterprise_customer_user__enterprise_customer__uuid=enterprise_customer_data["uuid"],
106108
)
107109
enterprise_enrollment_exists = enterprise_enrollments.exists()
108110
log.info(
109111
(
110-
'[enterprise_learner_enrolled] Checking for an enterprise enrollment for '
111-
'lms_user_id=%s in course_key=%s via enterprise_customer_uuid=%s. '
112-
'Exists: %s'
112+
"[enterprise_learner_enrolled] Checking for an enterprise enrollment for "
113+
"lms_user_id=%s in course_key=%s via enterprise_customer_uuid=%s. "
114+
"Exists: %s"
113115
),
114116
user.id,
115117
course_key,
116-
enterprise_customer_data['uuid'],
118+
enterprise_customer_data["uuid"],
117119
enterprise_enrollment_exists,
118120
)
119121
return enterprise_enrollment_exists
@@ -130,7 +132,7 @@ def check_start_date(user, days_early_for_beta, start, course_key, display_error
130132
Returns:
131133
AccessResponse: Either ACCESS_GRANTED or StartDateError.
132134
"""
133-
start_dates_disabled = settings.FEATURES['DISABLE_START_DATES']
135+
start_dates_disabled = settings.FEATURES["DISABLE_START_DATES"]
134136
masquerading_as_student = is_masquerading_as_student(user, course_key)
135137

136138
if start_dates_disabled and not masquerading_as_student:
@@ -161,8 +163,8 @@ def in_preview_mode():
161163
Returns whether the user is in preview mode or not.
162164
"""
163165
hostname = get_current_request_hostname()
164-
preview_lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE', None)
165-
return bool(preview_lms_base and hostname and hostname.split(':')[0] == preview_lms_base.split(':')[0])
166+
preview_lms_base = settings.FEATURES.get("PREVIEW_LMS_BASE", None)
167+
return bool(preview_lms_base and hostname and hostname.split(":")[0] == preview_lms_base.split(":")[0])
166168

167169

168170
def check_course_open_for_learner(user, course):
@@ -233,18 +235,19 @@ def check_public_access(course, visibilities):
233235

234236
def check_data_sharing_consent(course_id):
235237
"""
236-
Grants access if the user is do not need DataSharing consent, otherwise returns data sharing link.
238+
Grants access if the user is do not need DataSharing consent, otherwise returns data sharing link.
237239
238-
Returns:
239-
AccessResponse: Either ACCESS_GRANTED or DataSharingConsentRequiredAccessError
240-
"""
240+
Returns:
241+
AccessResponse: Either ACCESS_GRANTED or DataSharingConsentRequiredAccessError
242+
"""
241243
from openedx.features.enterprise_support.api import get_enterprise_consent_url
244+
242245
consent_url = get_enterprise_consent_url(
243246
request=get_current_request(),
244247
course_id=str(course_id),
245-
return_to='courseware',
248+
return_to="courseware",
246249
enrollment_exists=True,
247-
source='CoursewareAccess'
250+
source="CoursewareAccess",
248251
)
249252
if consent_url:
250253
return DataSharingConsentRequiredAccessError(consent_url=consent_url)
@@ -274,7 +277,7 @@ def check_correct_active_enterprise_customer(user, course_id):
274277
except (EnterpriseCustomerUser.DoesNotExist, EnterpriseCustomerUser.MultipleObjectsReturned):
275278
# Ideally this should not happen. As there should be only 1 active enterprise customer in our system
276279
log.error("Multiple or No Active Enterprise found for the user %s.", user.id)
277-
active_enterprise_name = 'Incorrect'
280+
active_enterprise_name = "Incorrect"
278281

279282
enrollment_enterprise_name = enterprise_enrollments.first().enterprise_customer_user.enterprise_customer.name
280283
return IncorrectActiveEnterpriseAccessError(enrollment_enterprise_name, active_enterprise_name)
Lines changed: 64 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""
22
Unit test for various Utility functions
33
"""
4+
45
import json
6+
from datetime import date, timedelta
57
from unittest.mock import patch
68

79
import ddt
@@ -14,13 +16,44 @@
1416
from common.djangoapps.student.tests.factories import GlobalStaffFactory, UserFactory
1517
from lms.djangoapps.courseware.constants import UNEXPECTED_ERROR_IS_ELIGIBLE
1618
from lms.djangoapps.courseware.tests.factories import FinancialAssistanceConfigurationFactory
19+
from lms.djangoapps.courseware.access_utils import adjust_start_date
1720
from lms.djangoapps.courseware.utils import (
1821
create_financial_assistance_application,
1922
get_financial_assistance_application_status,
20-
is_eligible_for_financial_aid
23+
is_eligible_for_financial_aid,
2124
)
2225

2326

27+
@ddt.ddt
28+
class TestAccessUtils(TestCase):
29+
"""Tests for the access_utils functions."""
30+
31+
@ddt.data(
32+
# days_early_for_beta, is_beta_user, expected_date
33+
(None, True, "2025-01-03"),
34+
(2, True, "2025-01-01"),
35+
(timedelta.max.days + 10, True, "2025-01-03"),
36+
(None, False, "2025-01-03"),
37+
(2, False, "2025-01-03"),
38+
(timedelta.max.days + 10, False, "2025-01-03"),
39+
)
40+
@ddt.unpack
41+
def test_adjust_start_date(self, days_early_for_beta, is_beta_user, expected_date):
42+
"""Tests adjust_start_date
43+
44+
Should only modify the date if the user is beta for the course,
45+
and `days_early_for_beta` is sensible number."""
46+
start = date(2025, 1, 3)
47+
expected = date.fromisoformat(expected_date)
48+
user = "princessofpower"
49+
course_key = "edx1+8675"
50+
with patch("lms.djangoapps.courseware.access_utils.CourseBetaTesterRole") as role_mock:
51+
instance = role_mock.return_value
52+
instance.has_user.return_value = is_beta_user
53+
adjusted_date = adjust_start_date(user, days_early_for_beta, start, course_key)
54+
self.assertEqual(expected, adjusted_date)
55+
56+
2457
@ddt.ddt
2558
class TestFinancialAssistanceViews(TestCase):
2659
"""
@@ -29,17 +62,17 @@ class TestFinancialAssistanceViews(TestCase):
2962

3063
def setUp(self) -> None:
3164
super().setUp()
32-
self.test_course_id = 'course-v1:edX+Test+1'
65+
self.test_course_id = "course-v1:edX+Test+1"
3366
self.user = UserFactory()
3467
self.global_staff = GlobalStaffFactory.create()
3568
_ = FinancialAssistanceConfigurationFactory(
36-
api_base_url='http://financial.assistance.test:1234',
69+
api_base_url="http://financial.assistance.test:1234",
3770
service_username=self.global_staff.username,
3871
fa_backend_enabled_courses_percentage=100,
39-
enabled=True
72+
enabled=True,
4073
)
4174
_ = Application.objects.create(
42-
name='Test Application',
75+
name="Test Application",
4376
user=self.global_staff,
4477
client_type=Application.CLIENT_PUBLIC,
4578
authorization_grant_type=Application.GRANT_CLIENT_CREDENTIALS,
@@ -51,33 +84,31 @@ def _mock_response(self, status_code, content=None):
5184
"""
5285
mock_response = Response()
5386
mock_response.status_code = status_code
54-
mock_response._content = json.dumps(content).encode('utf-8') # pylint: disable=protected-access
87+
mock_response._content = json.dumps(content).encode("utf-8") # pylint: disable=protected-access
5588
return mock_response
5689

5790
@ddt.data(
58-
{'is_eligible': True, 'reason': None},
59-
{'is_eligible': False, 'reason': 'This course is not eligible for financial aid'}
91+
{"is_eligible": True, "reason": None},
92+
{"is_eligible": False, "reason": "This course is not eligible for financial aid"},
6093
)
6194
def test_is_eligible_for_financial_aid(self, response_data):
6295
"""
6396
Tests the functionality of is_eligible_for_financial_aid which calls edx-financial-assistance backend
6497
to return eligibility status for financial assistance for a given course.
6598
"""
66-
with patch.object(OAuthAPIClient, 'request') as oauth_mock:
99+
with patch.object(OAuthAPIClient, "request") as oauth_mock:
67100
oauth_mock.return_value = self._mock_response(status.HTTP_200_OK, response_data)
68101
is_eligible, reason = is_eligible_for_financial_aid(self.test_course_id)
69-
assert is_eligible is response_data.get('is_eligible')
70-
assert reason == response_data.get('reason')
102+
assert is_eligible is response_data.get("is_eligible")
103+
assert reason == response_data.get("reason")
71104

72105
def test_is_eligible_for_financial_aid_invalid_course_id(self):
73106
"""
74107
Tests the functionality of is_eligible_for_financial_aid for an invalid course id.
75108
"""
76109
error_message = f"Invalid course id {self.test_course_id} provided"
77-
with patch.object(OAuthAPIClient, 'request') as oauth_mock:
78-
oauth_mock.return_value = self._mock_response(
79-
status.HTTP_400_BAD_REQUEST, {"message": error_message}
80-
)
110+
with patch.object(OAuthAPIClient, "request") as oauth_mock:
111+
oauth_mock.return_value = self._mock_response(status.HTTP_400_BAD_REQUEST, {"message": error_message})
81112
is_eligible, reason = is_eligible_for_financial_aid(self.test_course_id)
82113
assert is_eligible is False
83114
assert reason == error_message
@@ -86,9 +117,9 @@ def test_is_eligible_for_financial_aid_invalid_unexpected_error(self):
86117
"""
87118
Tests the functionality of is_eligible_for_financial_aid for an unexpected error
88119
"""
89-
with patch.object(OAuthAPIClient, 'request') as oauth_mock:
120+
with patch.object(OAuthAPIClient, "request") as oauth_mock:
90121
oauth_mock.return_value = self._mock_response(
91-
status.HTTP_500_INTERNAL_SERVER_ERROR, {'error': 'unexpected error occurred'}
122+
status.HTTP_500_INTERNAL_SERVER_ERROR, {"error": "unexpected error occurred"}
92123
)
93124
is_eligible, reason = is_eligible_for_financial_aid(self.test_course_id)
94125
assert is_eligible is False
@@ -99,45 +130,39 @@ def test_get_financial_assistance_application_status(self):
99130
Tests the functionality of get_financial_assistance_application_status against a user id and a course id
100131
edx-financial-assistance backend to return status of a financial assistance application.
101132
"""
102-
test_response = {'id': 123, 'status': 'ACCEPTED', 'coupon_code': 'ABCD..'}
103-
with patch.object(OAuthAPIClient, 'request') as oauth_mock:
133+
test_response = {"id": 123, "status": "ACCEPTED", "coupon_code": "ABCD.."}
134+
with patch.object(OAuthAPIClient, "request") as oauth_mock:
104135
oauth_mock.return_value = self._mock_response(status.HTTP_200_OK, test_response)
105136
has_application, reason = get_financial_assistance_application_status(self.user.id, self.test_course_id)
106137
assert has_application is True
107138
assert reason == test_response
108139

109140
@ddt.data(
110-
{
111-
'status': status.HTTP_400_BAD_REQUEST,
112-
'content': {'message': 'Invalid course id provided'}
113-
},
114-
{
115-
'status': status.HTTP_404_NOT_FOUND,
116-
'content': {'message': 'Application details not found'}
117-
}
141+
{"status": status.HTTP_400_BAD_REQUEST, "content": {"message": "Invalid course id provided"}},
142+
{"status": status.HTTP_404_NOT_FOUND, "content": {"message": "Application details not found"}},
118143
)
119144
def test_get_financial_assistance_application_status_unsuccessful(self, response_data):
120145
"""
121146
Tests unsuccessful scenarios of get_financial_assistance_application_status
122147
against a user id and a course id edx-financial-assistance backend.
123148
"""
124-
with patch.object(OAuthAPIClient, 'request') as oauth_mock:
125-
oauth_mock.return_value = self._mock_response(response_data.get('status'), response_data.get('content'))
149+
with patch.object(OAuthAPIClient, "request") as oauth_mock:
150+
oauth_mock.return_value = self._mock_response(response_data.get("status"), response_data.get("content"))
126151
has_application, reason = get_financial_assistance_application_status(self.user.id, self.test_course_id)
127152
assert has_application is False
128-
assert reason == response_data.get('content').get('message')
153+
assert reason == response_data.get("content").get("message")
129154

130155
def test_create_financial_assistance_application(self):
131156
"""
132157
Tests the functionality of create_financial_assistance_application which calls edx-financial-assistance backend
133158
to create a new financial assistance application given a form data.
134159
"""
135160
test_form_data = {
136-
'lms_user_id': self.user.id,
137-
'course_id': self.test_course_id,
161+
"lms_user_id": self.user.id,
162+
"course_id": self.test_course_id,
138163
}
139-
with patch.object(OAuthAPIClient, 'request') as oauth_mock:
140-
oauth_mock.return_value = self._mock_response(status.HTTP_200_OK, {'success': True})
164+
with patch.object(OAuthAPIClient, "request") as oauth_mock:
165+
oauth_mock.return_value = self._mock_response(status.HTTP_200_OK, {"success": True})
141166
response = create_financial_assistance_application(form_data=test_form_data)
142167
assert response.status_code == status.HTTP_204_NO_CONTENT
143168

@@ -147,12 +172,12 @@ def test_create_financial_assistance_application_bad_request(self):
147172
to create a new financial assistance application given a form data.
148173
"""
149174
test_form_data = {
150-
'lms_user_id': self.user.id,
151-
'course_id': 'invalid_course_id',
175+
"lms_user_id": self.user.id,
176+
"course_id": "invalid_course_id",
152177
}
153-
error_response = {'message': 'Invalid course id provided'}
154-
with patch.object(OAuthAPIClient, 'request') as oauth_mock:
178+
error_response = {"message": "Invalid course id provided"}
179+
with patch.object(OAuthAPIClient, "request") as oauth_mock:
155180
oauth_mock.return_value = self._mock_response(status.HTTP_400_BAD_REQUEST, error_response)
156181
response = create_financial_assistance_application(form_data=test_form_data)
157182
assert response.status_code == status.HTTP_400_BAD_REQUEST
158-
assert json.loads(response.content.decode('utf-8')) == error_response
183+
assert json.loads(response.content.decode("utf-8")) == error_response

lms/envs/common.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4331,6 +4331,8 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
43314331

43324332
CREDENTIALS_INTERNAL_SERVICE_URL = 'http://localhost:8005'
43334333
CREDENTIALS_PUBLIC_SERVICE_URL = 'http://localhost:8005'
4334+
# time between scheduled runs, in seconds
4335+
NOTIFY_CREDENTIALS_FREQUENCY = 14400
43344336

43354337
COMMENTS_SERVICE_URL = 'http://localhost:18080'
43364338
COMMENTS_SERVICE_KEY = 'password'

openedx/core/djangoapps/content_staging/tests/test_clipboard.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# pylint: skip-file
12
"""
23
Tests for the clipboard functionality
34
"""

0 commit comments

Comments
 (0)