Skip to content

Commit eb15b12

Browse files
committed
Merge branch 'master' into chris/FAL-4266-add-title-to-course-details
2 parents 1b5205a + 6c6fc5d commit eb15b12

27 files changed

Lines changed: 980 additions & 162 deletions

File tree

cms/djangoapps/contentstore/views/preview.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from xblock.exceptions import NoSuchHandlerError
1919
from xblock.runtime import KvsFieldData
2020

21+
from openedx.core.djangoapps.video_config.services import VideoConfigService
2122
from xmodule.contentstore.django import contentstore
2223
from xmodule.exceptions import NotFoundError, ProcessingError
2324
from xmodule.modulestore.django import XBlockI18nService, modulestore
@@ -214,7 +215,8 @@ def _prepare_runtime_for_preview(request, block):
214215
"teams_configuration": TeamsConfigurationService(),
215216
"sandbox": SandboxService(contentstore=contentstore, course_id=course_id),
216217
"cache": CacheService(cache),
217-
'replace_urls': ReplaceURLService
218+
'replace_urls': ReplaceURLService,
219+
'video_config': VideoConfigService(),
218220
}
219221

220222
block.runtime.get_block_for_descriptor = partial(_load_preview_block, request)

common/djangoapps/third_party_auth/api/serializers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,7 @@ def get_username(self, social_user):
2020

2121
def get_remote_id(self, social_user):
2222
""" Gets remote id from social user based on provider """
23+
remote_id_field_name = self.context.get('remote_id_field_name', None)
24+
if remote_id_field_name:
25+
return self.provider.get_remote_id_from_field_name(social_user, remote_id_field_name)
2326
return self.provider.get_remote_id_from_social_auth(social_user)

common/djangoapps/third_party_auth/api/tests/test_views.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@
3838
PASSWORD = "edx"
3939

4040

41-
def get_mapping_data_by_usernames(usernames):
41+
def get_mapping_data_by_usernames(usernames, remote_id_field_name=False):
4242
""" Generate mapping data used in response """
43+
if remote_id_field_name:
44+
return [{'username': username, 'remote_id': 'external_' + username} for username in usernames]
4345
return [{'username': username, 'remote_id': 'remote_' + username} for username in usernames]
4446

4547

@@ -76,11 +78,13 @@ def setUp(self): # pylint: disable=arguments-differ
7678
provider=google.backend_name,
7779
uid=f'{username}@gmail.com',
7880
)
79-
UserSocialAuth.objects.create(
81+
usa = UserSocialAuth.objects.create(
8082
user=user,
8183
provider=testshib.backend_name,
8284
uid=f'{testshib.slug}:remote_{username}',
8385
)
86+
usa.set_extra_data({'external_user_id': f'external_{username}'})
87+
usa.refresh_from_db()
8488
# Create another user not linked to any providers:
8589
UserFactory.create(username=CARL_USERNAME, email=f'{CARL_USERNAME}@example.com', password=PASSWORD)
8690

@@ -304,12 +308,20 @@ def test_list_all_user_mappings_oauth2(self, valid_call, expect_code, expect_dat
304308
@ddt.data(
305309
({'username': [ALICE_USERNAME, STAFF_USERNAME]}, 200,
306310
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
311+
({'username': [ALICE_USERNAME, STAFF_USERNAME], 'remote_id_field_name': 'external_user_id'}, 200,
312+
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
307313
({'remote_id': ['remote_' + ALICE_USERNAME, 'remote_' + STAFF_USERNAME, 'remote_' + CARL_USERNAME]}, 200,
308314
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
315+
({'remote_id': ['remote_' + ALICE_USERNAME, 'remote_' + STAFF_USERNAME, 'remote_' + CARL_USERNAME],
316+
'remote_id_field_name': 'external_user_id'}, 200,
317+
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
309318
({'username': [ALICE_USERNAME, CARL_USERNAME, STAFF_USERNAME]}, 200,
310319
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
311320
({'username': [ALICE_USERNAME], 'remote_id': ['remote_' + STAFF_USERNAME]}, 200,
312321
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
322+
({'username': [ALICE_USERNAME], 'remote_id': ['remote_' + STAFF_USERNAME],
323+
'remote_id_field_name': 'external_user_id'}, 200,
324+
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
313325
)
314326
@ddt.unpack
315327
def test_user_mappings_with_query_params_comma_separated(self, query_params, expect_code, expect_data):
@@ -321,19 +333,29 @@ def test_user_mappings_with_query_params_comma_separated(self, query_params, exp
321333
for attr in ['username', 'remote_id']:
322334
if attr in query_params:
323335
params.append('{}={}'.format(attr, ','.join(query_params[attr])))
336+
if 'remote_id_field_name' in query_params:
337+
params.append('remote_id_field_name={}'.format(query_params['remote_id_field_name']))
324338
url = "{}?{}".format(base_url, '&'.join(params))
325339
response = self.client.get(url, HTTP_X_EDX_API_KEY=VALID_API_KEY)
326340
self._verify_response(response, expect_code, expect_data)
327341

328342
@ddt.data(
329343
({'username': [ALICE_USERNAME, STAFF_USERNAME]}, 200,
330344
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
345+
({'username': [ALICE_USERNAME, STAFF_USERNAME], 'remote_id_field_name': 'external_user_id'}, 200,
346+
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
331347
({'remote_id': ['remote_' + ALICE_USERNAME, 'remote_' + STAFF_USERNAME, 'remote_' + CARL_USERNAME]}, 200,
332348
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
349+
({'remote_id': ['remote_' + ALICE_USERNAME, 'remote_' + STAFF_USERNAME, 'remote_' + CARL_USERNAME],
350+
'remote_id_field_name': 'external_user_id'}, 200,
351+
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
333352
({'username': [ALICE_USERNAME, CARL_USERNAME, STAFF_USERNAME]}, 200,
334353
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
335354
({'username': [ALICE_USERNAME], 'remote_id': ['remote_' + STAFF_USERNAME]}, 200,
336355
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME])),
356+
({'username': [ALICE_USERNAME], 'remote_id': ['remote_' + STAFF_USERNAME],
357+
'remote_id_field_name': 'external_user_id'}, 200,
358+
get_mapping_data_by_usernames([ALICE_USERNAME, STAFF_USERNAME], remote_id_field_name=True)),
337359
)
338360
@ddt.unpack
339361
def test_user_mappings_with_query_params_multi_value_key(self, query_params, expect_code, expect_data):
@@ -345,6 +367,8 @@ def test_user_mappings_with_query_params_multi_value_key(self, query_params, exp
345367
for attr in ['username', 'remote_id']:
346368
if attr in query_params:
347369
params.setlist(attr, query_params[attr])
370+
if 'remote_id_field_name' in query_params:
371+
params['remote_id_field_name'] = query_params['remote_id_field_name']
348372
url = f"{base_url}?{params.urlencode()}"
349373
response = self.client.get(url, HTTP_X_EDX_API_KEY=VALID_API_KEY)
350374
self._verify_response(response, expect_code, expect_data)

common/djangoapps/third_party_auth/api/views.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,9 @@ class UserMappingView(ListAPIView):
323323
324324
GET /api/third_party_auth/v0/providers/{provider_id}/users?username={username1},{username2}
325325
326+
GET /api/third_party_auth/v0/providers/{provider_id}/users?username={username1}&
327+
remote_id_field_name={external_id_field_name}
328+
326329
GET /api/third_party_auth/v0/providers/{provider_id}/users?username={username1}&usernames={username2}
327330
328331
GET /api/third_party_auth/v0/providers/{provider_id}/users?remote_id={remote_id1},{remote_id2}
@@ -346,6 +349,9 @@ class UserMappingView(ListAPIView):
346349
* usernames: Optional. List of comma separated edX usernames to filter the result set.
347350
e.g. ?usernames=bob123,jane456
348351
352+
* remote_id_field_name: Optional. The field name to use for the remote id lookup.
353+
Useful when learners are coming from external LMS. e.g. ?remote_id_field_name=ext_userid_sf
354+
349355
* page, page_size: Optional. Used for paging the result set, especially when getting
350356
an unfiltered list.
351357
@@ -415,6 +421,7 @@ def get_serializer_context(self):
415421
remove idp_slug from the remote_id if there is any
416422
"""
417423
context = super().get_serializer_context()
424+
context['remote_id_field_name'] = self.request.query_params.get('remote_id_field_name', None)
418425
context['provider'] = self.provider
419426

420427
return context

common/djangoapps/third_party_auth/models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,17 @@ def match_social_auth(self, social_auth):
810810
prefix = self.slug + ":"
811811
return self.backend_name == social_auth.provider and social_auth.uid.startswith(prefix)
812812

813+
def get_remote_id_from_field_name(self, social_auth, field_name):
814+
""" Given a UserSocialAuth object, return the user remote ID against the field name provided. """
815+
if not self.match_social_auth(social_auth):
816+
raise ValueError(
817+
f"UserSocialAuth record does not match given provider {self.provider_id}"
818+
)
819+
field_value = social_auth.extra_data.get(field_name, None)
820+
if field_value and isinstance(field_value, list):
821+
return field_value[0]
822+
return field_value
823+
813824
def get_remote_id_from_social_auth(self, social_auth):
814825
""" Given a UserSocialAuth object, return the remote ID used by this provider. """
815826
assert self.match_social_auth(social_auth)

lms/djangoapps/courseware/block_render.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from xblock.runtime import KvsFieldData
4343

4444
from lms.djangoapps.teams.services import TeamsService
45+
from openedx.core.djangoapps.video_config.services import VideoConfigService
4546
from openedx.core.lib.xblock_services.call_to_action import CallToActionService
4647
from xmodule.contentstore.django import contentstore
4748
from xmodule.exceptions import NotFoundError, ProcessingError
@@ -635,6 +636,7 @@ def inner_get_block(block: XBlock) -> XBlock | None:
635636
'call_to_action': CallToActionService(),
636637
'publish': EventPublishingService(user, course_id, track_function),
637638
'enrollments': EnrollmentsService(),
639+
'video_config': VideoConfigService(),
638640
}
639641

640642
runtime.get_block_for_descriptor = inner_get_block

lms/djangoapps/courseware/tests/test_video_mongo.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from lxml import etree
3838
from path import Path as path
3939
from xmodule.contentstore.content import StaticContent
40-
from xmodule.course_block import (
40+
from openedx.core.djangoapps.video_config.sharing import (
4141
COURSE_VIDEO_SHARING_ALL_VIDEOS,
4242
COURSE_VIDEO_SHARING_NONE,
4343
COURSE_VIDEO_SHARING_PER_VIDEO
@@ -57,6 +57,7 @@
5757
from common.djangoapps.xblock_django.constants import ATTR_KEY_REQUEST_COUNTRY_CODE
5858
from lms.djangoapps.courseware.tests.helpers import get_context_dict_from_string
5959
from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE
60+
from openedx.core.djangoapps.video_config import sharing
6061
from openedx.core.djangoapps.video_pipeline.config.waffle import DEPRECATE_YOUTUBE
6162
from openedx.core.djangoapps.waffle_utils.models import WaffleFlagCourseOverrideModel
6263
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
@@ -260,14 +261,14 @@ def test_is_public_sharing_enabled(self, feature_enabled):
260261
"""Test public video url."""
261262
assert self.block.public_access is True
262263
with self.mock_feature_toggle(enabled=feature_enabled):
263-
assert self.block.is_public_sharing_enabled() == feature_enabled
264+
assert sharing.is_public_sharing_enabled(self.block.location, self.block.public_access) == feature_enabled
264265

265266
def test_is_public_sharing_enabled__not_public(self):
266267
self.block.public_access = False
267268
with self.mock_feature_toggle():
268-
assert not self.block.is_public_sharing_enabled()
269+
assert not sharing.is_public_sharing_enabled(self.block.location, self.block.public_access)
269270

270-
@patch('xmodule.video_block.video_block.VideoBlock.get_course_video_sharing_override')
271+
@patch('openedx.core.djangoapps.video_config.sharing.get_course_video_sharing_override')
271272
def test_is_public_sharing_enabled_by_course_override(self, mock_course_sharing_override):
272273

273274
# Given a course overrides all videos to be shared
@@ -276,47 +277,47 @@ def test_is_public_sharing_enabled_by_course_override(self, mock_course_sharing_
276277

277278
# When I try to determine if public sharing is enabled
278279
with self.mock_feature_toggle():
279-
is_public_sharing_enabled = self.block.is_public_sharing_enabled()
280+
is_public_sharing_enabled = sharing.is_public_sharing_enabled(self.block.location, self.block.public_access)
280281

281282
# Then I will get that course value
282283
self.assertTrue(is_public_sharing_enabled)
283284

284-
@patch('xmodule.video_block.video_block.VideoBlock.get_course_video_sharing_override')
285+
@patch('openedx.core.djangoapps.video_config.sharing.get_course_video_sharing_override')
285286
def test_is_public_sharing_disabled_by_course_override(self, mock_course_sharing_override):
286287
# Given a course overrides no videos to be shared
287288
mock_course_sharing_override.return_value = COURSE_VIDEO_SHARING_NONE
288289
self.block.public_access = 'some-arbitrary-value'
289290

290291
# When I try to determine if public sharing is enabled
291292
with self.mock_feature_toggle():
292-
is_public_sharing_enabled = self.block.is_public_sharing_enabled()
293+
is_public_sharing_enabled = sharing.is_public_sharing_enabled(self.block.location, self.block.public_access)
293294

294295
# Then I will get that course value
295296
self.assertFalse(is_public_sharing_enabled)
296297

297298
@ddt.data(COURSE_VIDEO_SHARING_PER_VIDEO, None)
298-
@patch('xmodule.video_block.video_block.VideoBlock.get_course_video_sharing_override')
299+
@patch('openedx.core.djangoapps.video_config.sharing.get_course_video_sharing_override')
299300
def test_is_public_sharing_enabled_per_video(self, mock_override_value, mock_course_sharing_override):
300301
# Given a course does not override per-video settings
301302
mock_course_sharing_override.return_value = mock_override_value
302303
self.block.public_access = 'some-arbitrary-value'
303304

304305
# When I try to determine if public sharing is enabled
305306
with self.mock_feature_toggle():
306-
is_public_sharing_enabled = self.block.is_public_sharing_enabled()
307+
is_public_sharing_enabled = sharing.is_public_sharing_enabled(self.block.location, self.block.public_access)
307308

308309
# I will get the per-video value
309310
self.assertEqual(self.block.public_access, is_public_sharing_enabled)
310311

311-
@patch('xmodule.video_block.video_block.get_course_by_id')
312+
@patch('openedx.core.lib.courses.get_course_by_id')
312313
def test_is_public_sharing_course_not_found(self, mock_get_course):
313314
# Given a course does not override per-video settings
314315
mock_get_course.side_effect = Http404()
315316
self.block.public_access = 'some-arbitrary-value'
316317

317318
# When I try to determine if public sharing is enabled
318319
with self.mock_feature_toggle():
319-
is_public_sharing_enabled = self.block.is_public_sharing_enabled()
320+
is_public_sharing_enabled = sharing.is_public_sharing_enabled(self.block.location, self.block.public_access)
320321

321322
# I will fall-back to per-video values
322323
self.assertEqual(self.block.public_access, is_public_sharing_enabled)
@@ -325,7 +326,7 @@ def test_is_public_sharing_course_not_found(self, mock_get_course):
325326
def test_context(self, is_public_sharing_enabled):
326327
with self.mock_feature_toggle():
327328
with patch.object(
328-
self.block,
329+
sharing,
329330
'is_public_sharing_enabled',
330331
return_value=is_public_sharing_enabled
331332
):

lms/djangoapps/courseware/views/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
139139
from openedx.core.djangoapps.util.user_messages import PageLevelMessages
140140
from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE
141+
from openedx.core.djangoapps.video_config.sharing import is_public_sharing_enabled
141142
from openedx.core.djangoapps.zendesk_proxy.utils import create_zendesk_ticket
142143
from openedx.core.djangolib.markup import HTML, Text
143144
from openedx.core.lib.courses import get_course_by_id
@@ -1869,7 +1870,7 @@ def get_course_and_video_block(self, usage_key_string):
18691870
)
18701871

18711872
# Block must be marked as public to be viewed
1872-
if not video_block.is_public_sharing_enabled():
1873+
if not is_public_sharing_enabled(video_block.location, video_block.public_access):
18731874
raise Http404("Video not found.")
18741875

18751876
return course, video_block

openedx/core/djangoapps/content_libraries/api/libraries.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,11 @@ def get_libraries_for_user(user, org=None, text_search=None, order=None) -> Quer
267267
Q(learning_package__description__icontains=text_search)
268268
)
269269

270-
filtered = permissions.perms[permissions.CAN_VIEW_THIS_CONTENT_LIBRARY].filter(user, qs)
270+
# Using distinct() temporarily to avoid duplicate results caused by overlapping permission checks
271+
# between Bridgekeeper and the new authorization framework. This ensures correct results for now,
272+
# but it should be removed once Bridgekeeper support is fully dropped and all permission logic
273+
# is handled through openedx-authz.
274+
filtered = permissions.perms[permissions.CAN_VIEW_THIS_CONTENT_LIBRARY].filter(user, qs).distinct()
271275

272276
if order:
273277
order_query = 'learning_package__'

0 commit comments

Comments
 (0)