def delete(self, request, lib_key_str): # pylint: disable=unused-argument """ Revert the draft changes made to the specified block and its descendants. Restore it to the last published version """ key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key( key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY) api.revert_changes(key) return Response({})
def delete(self, request, lib_key_str, user_id): """ Remove the specified user's permission to access or edit this content library. """ key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key(key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY_TEAM) group = get_object_or_404(Group, pk=int(user_id)) api.set_library_group_permissions(key, group, access_level=None) return Response({})
def post(self, request, lib_key_str): """ Create a new link in this library. """ key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key( key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY) serializer = LibraryBundleLinkSerializer(data=request.data) serializer.is_valid(raise_exception=True) target_key = LibraryLocatorV2.from_string( serializer.validated_data['opaque_key']) api.create_bundle_link( library_key=key, link_id=serializer.validated_data['id'], target_opaque_key=target_key, version=serializer.validated_data[ 'version'], # a number, or None for "use latest version" ) return Response({})
def post(self, request, lib_key_str): """ Commit the draft changes made to the specified block and its descendants. """ key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key( key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY) api.publish_changes(key) return Response({})
def patch(self, request, lib_key_str): """ Update a content library """ key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key(key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY) serializer = ContentLibraryUpdateSerializer(data=request.data, partial=True) serializer.is_valid(raise_exception=True) api.update_library(key, **serializer.validated_data) result = api.get_library(key) return Response(ContentLibraryMetadataSerializer(result).data)
def get(self, request, lib_key_str, username): """ Gets the current permissions settings for a particular user. """ key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key(key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY_TEAM) user = get_object_or_404(User, username=username) grant = api.get_library_user_permissions(key, user) if not grant: raise NotFound return Response(ContentLibraryPermissionSerializer(grant).data)
def get(self, request, lib_key_str): """ Get the list of users and groups who have permissions to view and edit this library. """ key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key( key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY_TEAM) team = api.get_library_team(key) return Response( ContentLibraryPermissionSerializer(team, many=True).data)
def patch(self, request, lib_key_str): """ Update a content library """ key = LibraryLocatorV2.from_string(lib_key_str) serializer = ContentLibraryUpdateSerializer(data=request.data, partial=True) serializer.is_valid(raise_exception=True) api.update_library(key, **serializer.validated_data) result = api.get_library(key) return Response(ContentLibraryMetadataSerializer(result).data)
def put(self, request, lib_key_str, group_name): """ Add a group to this content library, with permissions specified in the request body. """ key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key(key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY_TEAM) serializer = ContentLibraryPermissionLevelSerializer(data=request.data) serializer.is_valid(raise_exception=True) group = get_object_or_404(Group, name=group_name) api.set_library_group_permissions(key, group, access_level=serializer.validated_data["access_level"]) return Response({})
def test_authorize_lti_launch_when_no_library(self): """ Given no library When authorize_lti_launch is called Then return False """ self.assertFalse(ContentLibrary.objects.exists()) authorized = ContentLibrary.authorize_lti_launch( LibraryLocatorV2(org='foo', slug='foobar'), issuer='http://a-fake-issuer', client_id='a-fake-client-id') self.assertFalse(authorized)
def delete(self, request, lib_key_str, username): """ Remove the specified user's permission to access or edit this content library. """ key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key(key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY_TEAM) user = get_object_or_404(User, username=username) try: api.set_library_user_permissions(key, user, access_level=None) except api.LibraryPermissionIntegrityError as err: raise ValidationError(detail=str(err)) return Response({})
def create_mock_library(self, *, course_id=None, course_key_str=None): """ Create a library mock. """ mock_library = mock.MagicMock() mock_library.library_key = LibraryLocatorV2.from_string( self.library_key_str) if course_key_str is None: course_key_str = self.course_key_str if course_id is None: course_id = CourseKey.from_string(course_key_str) type(mock_library).course_id = mock.PropertyMock( return_value=course_id) return mock_library
def patch(self, request, lib_key_str, link_id): """ Update the specified link to point to a different version of its target bundle. Pass e.g. {"version": 40} or pass {"version": None} to update to the latest published version. """ key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key(key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY) serializer = LibraryBundleLinkUpdateSerializer(data=request.data) serializer.is_valid(raise_exception=True) api.update_bundle_link(key, link_id, version=serializer.validated_data['version']) return Response({})
def retrieve(self, request, lib_key_str, pk=None): """ Retrieve a import task for inspection. """ library_key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key( library_key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY, ) import_task = api.ContentLibraryBlockImportTask.objects.get(pk=pk) return Response(ContentLibraryBlockImportTaskSerializer(import_task).data)
def list(self, request, lib_key_str): """ List all import tasks for this library. """ library_key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key( library_key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY) queryset = api.ContentLibrary.objects.get_by_key( library_key).import_tasks result = ContentLibraryBlockImportTaskSerializer(queryset, many=True).data paginator = LibraryApiPagination() return paginator.get_paginated_response( paginator.paginate_queryset(result, request))
def put(self, request, lib_key_str, username): """ Add a user to this content library, with permissions specified in the request body. """ key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key(key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY_TEAM) serializer = ContentLibraryPermissionLevelSerializer(data=request.data) serializer.is_valid(raise_exception=True) user = get_object_or_404(User, username=username) try: api.set_library_user_permissions(key, user, access_level=serializer.validated_data["access_level"]) except api.LibraryPermissionIntegrityError as err: raise ValidationError(detail=str(err)) grant = api.get_library_user_permissions(key, user) return Response(ContentLibraryPermissionSerializer(grant).data)
def test_schema_updates(self): """ Test that outdated indexes aren't retrieved """ with patch("openedx.core.djangoapps.content_libraries.libraries_index.ContentLibraryIndexer.SCHEMA_VERSION", new=0): result = self._create_library(slug="test-lib-schemaupdates-1", title="Title 1", description="Description") library_key = LibraryLocatorV2.from_string(result['id']) self.assertEqual(len(ContentLibraryIndexer.get_items([library_key])), 1) with patch("openedx.core.djangoapps.content_libraries.libraries_index.ContentLibraryIndexer.SCHEMA_VERSION", new=1): self.assertEqual(len(ContentLibraryIndexer.get_items([library_key])), 0) call_command("reindex_content_library", all=True, force=True) self.assertEqual(len(ContentLibraryIndexer.get_items([library_key])), 1)
def patch(self, request, lib_key_str): """ Update a content library """ key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key(key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY) serializer = ContentLibraryUpdateSerializer(data=request.data, partial=True) serializer.is_valid(raise_exception=True) data = dict(serializer.validated_data) if 'type' in data: data['library_type'] = data.pop('type') try: api.update_library(key, **data) except api.IncompatibleTypesError as err: raise ValidationError({'type': str(err)}) result = api.get_library(key) return Response(ContentLibraryMetadataSerializer(result).data)
def test_schema_updates(self): """ Test that outdated indexes aren't retrieved """ result = self._create_library(slug="test-lib-schemaupdates-1", title="Title 1", description="Description") library_key = LibraryLocatorV2.from_string(result['id']) ContentLibraryIndexer.get_libraries([library_key]) with patch("openedx.core.djangoapps.content_libraries.libraries_index.ContentLibraryIndexer.SCHEMA_VERSION", new=1): with self.assertRaises(LibraryNotIndexedException): ContentLibraryIndexer.get_libraries([library_key]) call_command("reindex_content_library", all=True, quiet=True) ContentLibraryIndexer.get_libraries([library_key])
def post(self, request, lib_key_str): """ Add a new XBlock to this content library """ library_key = LibraryLocatorV2.from_string(lib_key_str) serializer = LibraryXBlockCreationSerializer(data=request.data) serializer.is_valid(raise_exception=True) parent_block_usage_str = serializer.validated_data.pop("parent_block", None) if parent_block_usage_str: # Add this as a child of an existing block: parent_block_usage = LibraryUsageLocatorV2.from_string(parent_block_usage_str) if parent_block_usage.context_key != library_key: raise ValidationError(detail={"parent_block": "Usage ID doesn't match library ID in the URL."}) result = api.create_library_block_child(parent_block_usage, **serializer.validated_data) else: # Create a new regular top-level block: result = api.create_library_block(library_key, **serializer.validated_data) return Response(LibraryXBlockMetadataSerializer(result).data)
def create(self, request, lib_key_str): """ Create and queue an import tasks for this library. """ library_key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key( library_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY, ) serializer = ContentLibraryBlockImportTaskCreateSerializer(data=request.data) serializer.is_valid(raise_exception=True) course_key = serializer.validated_data['course_key'] import_task = api.import_blocks_create_task(library_key, course_key) return Response(ContentLibraryBlockImportTaskSerializer(import_task).data)
def get(self, request, lib_key_str): """ Get the list of all top-level blocks in this content library """ key = LibraryLocatorV2.from_string(lib_key_str) text_search = request.query_params.get('text_search', None) api.require_permission_for_library_key(key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY) result = api.get_library_blocks(key, text_search=text_search) # Verify `pagination` param to maintain compatibility with older # non pagination-aware clients if request.GET.get('pagination', 'false').lower() == 'true': paginator = LibraryApiPagination() result = paginator.paginate_queryset(result, request) serializer = LibraryXBlockMetadataSerializer(result, many=True) return paginator.get_paginated_response(serializer.data) return Response(LibraryXBlockMetadataSerializer(result, many=True).data)
def test_index_libraries(self): """ Test if libraries are being indexed correctly """ result1 = self._create_library(slug="test-lib-index-1", title="Title 1", description="Description") result2 = self._create_library(slug="test-lib-index-2", title="Title 2", description="Description") for result in [result1, result2]: library_key = LibraryLocatorV2.from_string(result['id']) response = ContentLibraryIndexer.get_items([library_key])[0] self.assertEqual(response['id'], result['id']) self.assertEqual(response['title'], result['title']) self.assertEqual(response['description'], result['description']) self.assertEqual(response['uuid'], result['bundle_uuid']) self.assertEqual(response['num_blocks'], 0) self.assertEqual(response['version'], result['version']) self.assertEqual(response['last_published'], None) self.assertEqual(response['has_unpublished_changes'], False) self.assertEqual(response['has_unpublished_deletes'], False)
def post(self, request, lib_key_str): """ Add a user to this content library via email, with permissions specified in the request body. """ key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key( key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY_TEAM) serializer = ContentLibraryAddPermissionByEmailSerializer( data=request.data) serializer.is_valid(raise_exception=True) try: user = User.objects.get( email=serializer.validated_data.get('email')) except User.DoesNotExist: raise ValidationError({ 'email': _('We could not find a user with that email address.') }) grant = api.get_library_user_permissions(key, user) if grant: return Response( { 'email': [_('This user already has access to this library.')] }, status=status.HTTP_400_BAD_REQUEST, ) try: api.set_library_user_permissions( key, user, access_level=serializer.validated_data["access_level"]) except api.LibraryPermissionIntegrityError as err: raise ValidationError(detail=str(err)) grant = api.get_library_user_permissions(key, user) return Response(ContentLibraryPermissionSerializer(grant).data)
def test_update_libraries(self): """ Test if indexes are updated when libraries are updated """ lib = self._create_library(slug="test-lib-update", title="Title", description="Description") library_key = LibraryLocatorV2.from_string(lib['id']) self._update_library(lib['id'], title="New Title", description="New Title") response = ContentLibraryIndexer.get_libraries([library_key])[0] self.assertEqual(response['id'], lib['id']) self.assertEqual(response['title'], "New Title") self.assertEqual(response['description'], "New Title") self.assertEqual(response['uuid'], lib['bundle_uuid']) self.assertEqual(response['num_blocks'], 0) self.assertEqual(response['version'], lib['version']) self.assertEqual(response['last_published'], None) self.assertEqual(response['has_unpublished_changes'], False) self.assertEqual(response['has_unpublished_deletes'], False) self._delete_library(lib['id']) with self.assertRaises(LibraryNotIndexedException): ContentLibraryIndexer.get_libraries([library_key])
def post(self, request, lib_key_str): """ Add a new XBlock to this content library """ library_key = LibraryLocatorV2.from_string(lib_key_str) api.require_permission_for_library_key(library_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY) serializer = LibraryXBlockCreationSerializer(data=request.data) serializer.is_valid(raise_exception=True) parent_block_usage_str = serializer.validated_data.pop("parent_block", None) if parent_block_usage_str: # Add this as a child of an existing block: parent_block_usage = LibraryUsageLocatorV2.from_string(parent_block_usage_str) if parent_block_usage.context_key != library_key: raise ValidationError(detail={"parent_block": "Usage ID doesn't match library ID in the URL."}) result = api.create_library_block_child(parent_block_usage, **serializer.validated_data) else: # Create a new regular top-level block: try: result = api.create_library_block(library_key, **serializer.validated_data) except api.IncompatibleTypesError as err: raise ValidationError( detail={'block_type': str(err)}, ) return Response(LibraryXBlockMetadataSerializer(result).data)
class LibraryUsageLocatorV2Tests(TestCase): """ Tests for :class:`.LibraryUsageLocatorV2` """ VALID_LIB_KEY = LibraryLocatorV2("SchoolX", "lib-slug") def test_inheritance(self): """ A LibraryUsageLocatorV2 is a usage key """ usage_key = LibraryUsageLocatorV2(self.VALID_LIB_KEY, "problem", "p1") self.assertIsInstance(usage_key, UsageKey) @ddt.data( 'lb:MITx:reallyhardproblems:problem:problem1', 'lb:edX:demo-lib.2019:html:introduction', 'lb:UnicodeX:i18n-lib:html:έψιλον', ) def test_roundtrip_from_string(self, key): usage_key = UsageKey.from_string(key) serialized = text_type(usage_key) self.assertEqual(key, serialized) @ddt.data( { "lib_key": VALID_LIB_KEY, "block_type": "video", "usage_id": "vid-a4" }, { "lib_key": VALID_LIB_KEY, "block_type": "problem", "usage_id": "p1" }, { "lib_key": VALID_LIB_KEY, "block_type": "problem", "usage_id": "1" }, ) def test_roundtrip_from_key(self, key_args): key = LibraryUsageLocatorV2(**key_args) serialized = text_type(key) deserialized = UsageKey.from_string(serialized) self.assertEqual(key, deserialized) @ddt.data( # Keys with invalid lib_key: { "lib_key": "lib:SchoolX:this-is-a-string", "block_type": "problem", "usage_id": "p1" }, { "lib_key": "foobar", "block_type": "problem", "usage_id": "p1" }, { "lib_key": None, "block_type": "problem", "usage_id": "p1" }, # Keys with invalid block_type: { "lib_key": VALID_LIB_KEY, "block_type": None, "usage_id": "vid-a4" }, { "lib_key": VALID_LIB_KEY, "block_type": "a b", "usage_id": "vid-a4" }, # Keys with invalid usage_id: { "lib_key": VALID_LIB_KEY, "block_type": "video", "usage_id": None }, { "lib_key": VALID_LIB_KEY, "block_type": "video", "usage_id": "" }, { "lib_key": VALID_LIB_KEY, "block_type": "video", "usage_id": "a b c" }, { "lib_key": VALID_LIB_KEY, "block_type": "video", "usage_id": "$!%^" }, { "lib_key": VALID_LIB_KEY, "block_type": "video", "usage_id": 1 }, ) def test_invalid_args(self, key_args): with self.assertRaises((InvalidKeyError, TypeError, ValueError)): LibraryUsageLocatorV2(**key_args) def test_map_into_course(self): """ Test that key.map_into_course(key.course_key) won't raise an error as this pattern is used in several places in the LMS that still support old mongo. """ key = LibraryUsageLocatorV2(self.VALID_LIB_KEY, block_type="problem", usage_id="p1") self.assertEqual(key.map_into_course(key.course_key), key)
def test_update_library_blocks(self): """ Test if indexes are updated when blocks in libraries are updated """ def commit_library_and_verify(library_key): """ Commit library changes, and verify that there are no uncommited changes anymore """ last_published = ContentLibraryIndexer.get_libraries([library_key])[0]['last_published'] self._commit_library_changes(str(library_key)) response = ContentLibraryIndexer.get_libraries([library_key])[0] self.assertEqual(response['has_unpublished_changes'], False) self.assertEqual(response['has_unpublished_deletes'], False) self.assertGreaterEqual(response['last_published'], last_published) return response def verify_uncommitted_libraries(library_key, has_unpublished_changes, has_unpublished_deletes): """ Verify uncommitted changes and deletes in the index """ response = ContentLibraryIndexer.get_libraries([library_key])[0] self.assertEqual(response['has_unpublished_changes'], has_unpublished_changes) self.assertEqual(response['has_unpublished_deletes'], has_unpublished_deletes) return response lib = self._create_library(slug="test-lib-update-block", title="Title", description="Description") library_key = LibraryLocatorV2.from_string(lib['id']) # Verify uncommitted new blocks block = self._add_block_to_library(lib['id'], "problem", "problem1") response = verify_uncommitted_libraries(library_key, True, False) self.assertEqual(response['last_published'], None) self.assertEqual(response['num_blocks'], 1) # Verify committed new blocks self._commit_library_changes(lib['id']) response = verify_uncommitted_libraries(library_key, False, False) self.assertEqual(response['num_blocks'], 1) # Verify uncommitted deleted blocks self._delete_library_block(block['id']) response = verify_uncommitted_libraries(library_key, True, True) self.assertEqual(response['num_blocks'], 0) # Verify committed deleted blocks self._commit_library_changes(lib['id']) response = verify_uncommitted_libraries(library_key, False, False) self.assertEqual(response['num_blocks'], 0) block = self._add_block_to_library(lib['id'], "problem", "problem1") self._commit_library_changes(lib['id']) # Verify changes to blocks # Verify OLX updates on blocks self._set_library_block_olx(block["id"], "<problem/>") verify_uncommitted_libraries(library_key, True, False) commit_library_and_verify(library_key) # Verify asset updates on blocks self._set_library_block_asset(block["id"], "whatever.png", b"data") verify_uncommitted_libraries(library_key, True, False) commit_library_and_verify(library_key) self._delete_library_block_asset(block["id"], "whatever.png", expect_response=204) verify_uncommitted_libraries(library_key, True, False) commit_library_and_verify(library_key) lib2 = self._create_library(slug="test-lib-update-block-2", title="Title 2", description="Description") self._add_block_to_library(lib2["id"], "problem", "problem1") self._commit_library_changes(lib2["id"]) #Verify new links on libraries self._link_to_library(lib["id"], "library_2", lib2["id"]) verify_uncommitted_libraries(library_key, True, False) #Verify reverting uncommitted changes self._revert_library_changes(lib["id"]) verify_uncommitted_libraries(library_key, False, False)
def library_key(self): """ Get the LibraryLocatorV2 opaque key for this library """ return LibraryLocatorV2(org=self.org.short_name, slug=self.slug)
def test_invalid_args(self, key_args): with self.assertRaises((InvalidKeyError, TypeError, ValueError)): LibraryLocatorV2(**key_args)