- ## When attempts are not 0, the CTA above will contain a message about the number of used attempts
- % if attempts_allowed and (not submit_disabled_cta or attempts_used == 0):
- ${ngettext("You have used {num_used} of {num_total} attempt", "You have used {num_used} of {num_total} attempts", attempts_allowed).format(num_used=attempts_used, num_total=attempts_allowed)}
- % endif
- ${_("Some problems have options such as save, reset, hints, or show answer. These options follow the Submit button.")}
-
-% if cdn_eval:
-
-% endif;
diff --git a/lms/envs/common.py b/lms/envs/common.py
index fd5d0d004897..74bfb6e979ec 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -1085,6 +1085,7 @@
DATABASE_ROUTERS = [
'openedx.core.lib.django_courseware_routers.StudentModuleHistoryExtendedRouter',
+ 'openedx.core.lib.blockstore_api.db_routers.BlockstoreRouter',
'edx_django_utils.db.read_replica.ReadReplicaRouter',
]
@@ -3243,6 +3244,9 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
# For save for later
'lms.djangoapps.save_for_later',
+
+ # Blockstore
+ 'blockstore.apps.bundles',
]
######################### CSRF #########################################
@@ -4970,6 +4974,10 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
BLOCKSTORE_PUBLIC_URL_ROOT = 'http://localhost:18250'
BLOCKSTORE_API_URL = 'http://localhost:18250/api/v1/'
+# Disable the Blockstore app API by default.
+# See openedx.core.lib.blockstore_api.config for details.
+BLOCKSTORE_USE_BLOCKSTORE_APP_API = False
+
# .. setting_name: XBLOCK_RUNTIME_V2_EPHEMERAL_DATA_CACHE
# .. setting_default: default
# .. setting_description: The django cache key of the cache to use for storing anonymous user state for XBlocks.
@@ -4985,6 +4993,40 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
# configured to expire after one hour.
BLOCKSTORE_BUNDLE_CACHE_TIMEOUT = 3000
+# .. setting_name: BUNDLE_ASSET_URL_STORAGE_KEY
+# .. setting_default: None
+# .. setting_description: When this is set, `BUNDLE_ASSET_URL_STORAGE_SECRET` is
+# set, and `boto3` is installed, this is used as an AWS IAM access key for
+# generating signed, read-only URLs for blockstore assets stored in S3.
+# Otherwise, URLs are generated based on the default storage configuration.
+# See `blockstore.apps.bundles.storage.LongLivedSignedUrlStorage` for details.
+BUNDLE_ASSET_URL_STORAGE_KEY = None
+
+# .. setting_name: BUNDLE_ASSET_URL_STORAGE_SECRET
+# .. setting_default: None
+# .. setting_description: When this is set, `BUNDLE_ASSET_URL_STORAGE_KEY` is
+# set, and `boto3` is installed, this is used as an AWS IAM secret key for
+# generating signed, read-only URLs for blockstore assets stored in S3.
+# Otherwise, URLs are generated based on the default storage configuration.
+# See `blockstore.apps.bundles.storage.LongLivedSignedUrlStorage` for details.
+BUNDLE_ASSET_URL_STORAGE_SECRET = None
+
+# .. setting_name: BUNDLE_ASSET_STORAGE_SETTINGS
+# .. setting_default: dict, appropriate for file system storage.
+# .. setting_description: When this is set, `BUNDLE_ASSET_URL_STORAGE_KEY` is
+# set, and `boto3` is installed, this provides the bucket name and location for blockstore assets stored in S3.
+# See `blockstore.apps.bundles.storage.LongLivedSignedUrlStorage` for details.
+BUNDLE_ASSET_STORAGE_SETTINGS = dict(
+ # Backend storage
+ # STORAGE_CLASS='storages.backends.s3boto.S3BotoStorage',
+ # STORAGE_KWARGS=dict(bucket='bundle-asset-bucket', location='/path-to-bundles/'),
+ STORAGE_CLASS='django.core.files.storage.FileSystemStorage',
+ STORAGE_KWARGS=dict(
+ location=MEDIA_ROOT,
+ base_url=MEDIA_URL,
+ ),
+)
+
######################### MICROSITE ###############################
MICROSITE_ROOT_DIR = '/edx/app/edxapp/edx-microsite'
MICROSITE_CONFIGURATION = {}
diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py
index e255327f89d2..133d3cf1e3ba 100644
--- a/lms/envs/devstack.py
+++ b/lms/envs/devstack.py
@@ -19,7 +19,6 @@
# Don't use S3 in devstack, fall back to filesystem
del DEFAULT_FILE_STORAGE
-MEDIA_ROOT = "/edx/var/edxapp/uploads"
ORA2_FILEUPLOAD_BACKEND = 'django'
diff --git a/lms/envs/test.py b/lms/envs/test.py
index 406b5156ac98..b50f3e5e8a05 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -223,16 +223,6 @@
},
}
-############################### BLOCKSTORE #####################################
-# Blockstore tests
-RUN_BLOCKSTORE_TESTS = os.environ.get('EDXAPP_RUN_BLOCKSTORE_TESTS', 'no').lower() in ('true', 'yes', '1')
-BLOCKSTORE_API_URL = os.environ.get('EDXAPP_BLOCKSTORE_API_URL', "http://edx.devstack.blockstore-test:18251/api/v1/")
-BLOCKSTORE_API_AUTH_TOKEN = os.environ.get('EDXAPP_BLOCKSTORE_API_AUTH_TOKEN', 'edxapp-test-key')
-XBLOCK_RUNTIME_V2_EPHEMERAL_DATA_CACHE = 'blockstore' # This must be set to a working cache for the tests to pass
-
-# Dummy secret key for dev
-SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
-
############################# SECURITY SETTINGS ################################
# Default to advanced security in common.py, so tests can reset here to use
# a simpler security model
@@ -314,7 +304,7 @@
############################ STATIC FILES #############################
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
MEDIA_ROOT = TEST_ROOT / "uploads"
-MEDIA_URL = "/static/uploads/"
+MEDIA_URL = "/uploads/"
STATICFILES_DIRS.append(("uploads", MEDIA_ROOT))
_NEW_STATICFILES_DIRS = []
@@ -553,6 +543,24 @@
derive_settings(__name__)
+############################### BLOCKSTORE #####################################
+# Blockstore tests
+RUN_BLOCKSTORE_TESTS = os.environ.get('EDXAPP_RUN_BLOCKSTORE_TESTS', 'no').lower() in ('true', 'yes', '1')
+BLOCKSTORE_USE_BLOCKSTORE_APP_API = not RUN_BLOCKSTORE_TESTS
+BLOCKSTORE_API_URL = os.environ.get('EDXAPP_BLOCKSTORE_API_URL', "http://edx.devstack.blockstore-test:18251/api/v1/")
+BLOCKSTORE_API_AUTH_TOKEN = os.environ.get('EDXAPP_BLOCKSTORE_API_AUTH_TOKEN', 'edxapp-test-key')
+XBLOCK_RUNTIME_V2_EPHEMERAL_DATA_CACHE = 'blockstore' # This must be set to a working cache for the tests to pass
+BUNDLE_ASSET_STORAGE_SETTINGS = dict(
+ STORAGE_CLASS='django.core.files.storage.FileSystemStorage',
+ STORAGE_KWARGS=dict(
+ location=MEDIA_ROOT,
+ base_url=MEDIA_URL,
+ ),
+)
+
+# Dummy secret key for dev
+SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
+
############### Settings for edx-rbac ###############
SYSTEM_WIDE_ROLE_CLASSES = os.environ.get("SYSTEM_WIDE_ROLE_CLASSES", [])
diff --git a/openedx/core/djangoapps/content_libraries/api.py b/openedx/core/djangoapps/content_libraries/api.py
index 9e22df4196bd..c1d95c9d6819 100644
--- a/openedx/core/djangoapps/content_libraries/api.py
+++ b/openedx/core/djangoapps/content_libraries/api.py
@@ -65,7 +65,7 @@
from django.contrib.auth.models import AbstractUser, Group
from django.core.exceptions import PermissionDenied
from django.core.validators import validate_unicode_slug
-from django.db import IntegrityError
+from django.db import IntegrityError, transaction
from django.utils.translation import gettext as _
from elasticsearch.exceptions import ConnectionError as ElasticConnectionError
from lxml import etree
@@ -436,15 +436,18 @@ def create_library(
)
# Now create the library reference in our database:
try:
- ref = ContentLibrary.objects.create(
- org=org,
- slug=slug,
- type=library_type,
- bundle_uuid=bundle.uuid,
- allow_public_learning=allow_public_learning,
- allow_public_read=allow_public_read,
- license=library_license,
- )
+ # Atomic transaction required because if this fails,
+ # we need to delete the bundle in the exception handler.
+ with transaction.atomic():
+ ref = ContentLibrary.objects.create(
+ org=org,
+ slug=slug,
+ type=library_type,
+ bundle_uuid=bundle.uuid,
+ allow_public_learning=allow_public_learning,
+ allow_public_read=allow_public_read,
+ license=library_license,
+ )
except IntegrityError:
delete_bundle(bundle.uuid)
raise LibraryAlreadyExists(slug) # lint-amnesty, pylint: disable=raise-missing-from
diff --git a/openedx/core/djangoapps/content_libraries/library_bundle.py b/openedx/core/djangoapps/content_libraries/library_bundle.py
index b2d3c34efa15..ed03b1801222 100644
--- a/openedx/core/djangoapps/content_libraries/library_bundle.py
+++ b/openedx/core/djangoapps/content_libraries/library_bundle.py
@@ -2,7 +2,6 @@
Helper code for working with Blockstore bundles that contain OLX
"""
-import dateutil.parser
import logging # lint-amnesty, pylint: disable=wrong-import-order
from functools import lru_cache # lint-amnesty, pylint: disable=wrong-import-order
@@ -347,12 +346,17 @@ def get_static_files_for_definition(self, definition_key):
problem/quiz1/definition.xml
problem/quiz1/static/image1.png
Then this will return
- [BundleFile(path="image1.png", size, url, hash_digest)]
+ [BundleFileData(path="image1.png", size, url, hash_digest)]
"""
path_prefix = self.get_static_prefix_for_definition(definition_key)
path_prefix_len = len(path_prefix)
return [
- blockstore_api.BundleFile(path=f.path[path_prefix_len:], size=f.size, url=f.url, hash_digest=f.hash_digest)
+ blockstore_api.BundleFileData(
+ path=f.path[path_prefix_len:],
+ size=f.size,
+ url=f.url,
+ hash_digest=f.hash_digest,
+ )
for f in get_bundle_files_cached(self.bundle_uuid, draft_name=self.draft_name)
if f.path.startswith(path_prefix)
]
@@ -369,8 +373,7 @@ def get_last_published_time(self):
version = get_bundle_version_number(self.bundle_uuid)
if version == 0:
return None
- created_at_str = blockstore_api.get_bundle_version(self.bundle_uuid, version)['snapshot']['created_at']
- last_published_time = dateutil.parser.parse(created_at_str)
+ last_published_time = blockstore_api.get_bundle_version(self.bundle_uuid, version).created_at
self.cache.set(cache_key, last_published_time)
return last_published_time
diff --git a/openedx/core/djangoapps/content_libraries/tests/base.py b/openedx/core/djangoapps/content_libraries/tests/base.py
index 38138bac2e95..34aee552ee66 100644
--- a/openedx/core/djangoapps/content_libraries/tests/base.py
+++ b/openedx/core/djangoapps/content_libraries/tests/base.py
@@ -4,10 +4,12 @@
from contextlib import contextmanager
from io import BytesIO
from urllib.parse import urlencode
-import unittest
-from unittest.mock import patch
+from unittest import mock, skipUnless
+from urllib.parse import urlparse
from django.conf import settings
+from django.test import LiveServerTestCase
+from django.test.client import RequestFactory
from django.test.utils import override_settings
from organizations.models import Organization
from rest_framework.test import APITestCase, APIClient
@@ -47,8 +49,39 @@
URL_BLOCK_XBLOCK_HANDLER = '/api/xblock/v2/xblocks/{block_key}/handler/{user_id}-{secure_token}/{handler_name}/'
-# Decorator for tests that require blockstore
-requires_blockstore = unittest.skipUnless(settings.RUN_BLOCKSTORE_TESTS, "Requires a running Blockstore server")
+# Decorators for tests that require the blockstore service/app
+requires_blockstore = skipUnless(settings.RUN_BLOCKSTORE_TESTS, "Requires a running Blockstore server")
+
+requires_blockstore_app = skipUnless(settings.BLOCKSTORE_USE_BLOCKSTORE_APP_API, "Requires blockstore app")
+
+
+class BlockstoreAppTestMixin:
+ """
+ Sets up the environment for tests to be run using the installed Blockstore app.
+ """
+ def setUp(self):
+ """
+ Ensure there's an active request, so that bundle file URLs can be made absolute.
+ """
+ super().setUp()
+
+ # Patch the blockstore get_current_request to use our live_server_url
+ mock.patch('blockstore.apps.api.methods.get_current_request',
+ mock.Mock(return_value=self._get_current_request())).start()
+ self.addCleanup(mock.patch.stopall)
+
+ def _get_current_request(self):
+ """
+ Returns a request object using the live_server_url, if available.
+ """
+ request_args = {}
+ if hasattr(self, 'live_server_url'):
+ live_server_url = urlparse(self.live_server_url)
+ name, port = live_server_url.netloc.split(':')
+ request_args['SERVER_NAME'] = name
+ request_args['SERVER_PORT'] = port or '80'
+ request_args['wsgi.url_scheme'] = live_server_url.scheme
+ return RequestFactory().request(**request_args)
def elasticsearch_test(func):
@@ -63,9 +96,11 @@ def elasticsearch_test(func):
'host': settings.TEST_ELASTICSEARCH_HOST,
'port': settings.TEST_ELASTICSEARCH_PORT,
}])(func)
- func = patch("openedx.core.djangoapps.content_libraries.libraries_index.SearchIndexerBase.SEARCH_KWARGS", new={
- 'refresh': 'wait_for'
- })(func)
+ func = mock.patch(
+ "openedx.core.djangoapps.content_libraries.libraries_index.SearchIndexerBase.SEARCH_KWARGS",
+ new={
+ 'refresh': 'wait_for'
+ })(func)
return func
else:
@classmethod
@@ -77,20 +112,19 @@ def mock_perform(cls, filter_terms, text_search):
size=MAX_SIZE
)
- func = patch(
+ func = mock.patch(
"openedx.core.djangoapps.content_libraries.libraries_index.SearchIndexerBase.SEARCH_KWARGS",
new={}
)(func)
- func = patch(
+ func = mock.patch(
"openedx.core.djangoapps.content_libraries.libraries_index.SearchIndexerBase._perform_elastic_search",
new=mock_perform
)(func)
return func
-@requires_blockstore
@skip_unless_cms # Content Libraries REST API is only available in Studio
-class ContentLibrariesRestApiTest(APITestCase):
+class _ContentLibrariesRestApiTestMixin:
"""
Base class for Blockstore-based Content Libraries test that use the REST API
@@ -350,3 +384,26 @@ def _get_block_handler_url(self, block_key, handler_name):
"""
url = URL_BLOCK_GET_HANDLER_URL.format(block_key=block_key, handler_name=handler_name)
return self._api('get', url, None, expect_response=200)["handler_url"]
+
+
+@requires_blockstore
+class ContentLibrariesRestApiBlockstoreServiceTest(_ContentLibrariesRestApiTestMixin, APITestCase):
+ """
+ Base class for Blockstore-based Content Libraries test that use the REST API
+ and the standalone Blockstore service.
+ """
+
+
+@requires_blockstore_app
+class ContentLibrariesRestApiTest(
+ _ContentLibrariesRestApiTestMixin,
+ BlockstoreAppTestMixin,
+ APITestCase,
+ LiveServerTestCase,
+):
+ """
+ Base class for Blockstore-based Content Libraries test that use the REST API
+ and the installed Blockstore app.
+
+ We run this test with a live server, so that the blockstore asset files can be served.
+ """
diff --git a/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py b/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py
index 7cdbe54eebe7..a39c5371e829 100644
--- a/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py
+++ b/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py
@@ -19,6 +19,7 @@
from openedx.core.djangoapps.content_libraries.libraries_index import LibraryBlockIndexer, ContentLibraryIndexer
from openedx.core.djangoapps.content_libraries.tests.base import (
+ ContentLibrariesRestApiBlockstoreServiceTest,
ContentLibrariesRestApiTest,
elasticsearch_test,
URL_BLOCK_METADATA_URL,
@@ -33,8 +34,7 @@
@ddt.ddt
-@elasticsearch_test
-class ContentLibrariesTest(ContentLibrariesRestApiTest):
+class ContentLibrariesTestMixin:
"""
General tests for Blockstore-based Content Libraries
@@ -369,6 +369,60 @@ def test_library_blocks(self):
# fin
+ def test_library_blocks_studio_view(self):
+ """
+ Test the happy path of working with an HTML XBlock in a the studio_view of a content library.
+ """
+ lib = self._create_library(slug="testlib2", title="A Test Library", description="Testing XBlocks")
+ lib_id = lib["id"]
+ assert lib['has_unpublished_changes'] is False
+
+ # A library starts out empty:
+ assert self._get_library_blocks(lib_id) == []
+
+ # Add a 'html' XBlock to the library:
+ block_data = self._add_block_to_library(lib_id, "html", "html1")
+ self.assertDictContainsEntries(block_data, {
+ "id": "lb:CL-TEST:testlib2:html:html1",
+ "display_name": "Text",
+ "block_type": "html",
+ "has_unpublished_changes": True,
+ })
+ block_id = block_data["id"]
+ # Confirm that the result contains a definition key, but don't check its value,
+ # which for the purposes of these tests is an implementation detail.
+ assert 'def_key' in block_data
+
+ # now the library should contain one block and have unpublished changes:
+ assert self._get_library_blocks(lib_id) == [block_data]
+ assert self._get_library(lib_id)['has_unpublished_changes'] is True
+
+ # Publish the changes:
+ self._commit_library_changes(lib_id)
+ assert self._get_library(lib_id)['has_unpublished_changes'] is False
+ # And now the block information should also show that block has no unpublished changes:
+ block_data["has_unpublished_changes"] = False
+ self.assertDictContainsEntries(self._get_library_block(block_id), block_data)
+ assert self._get_library_blocks(lib_id) == [block_data]
+
+ # Now update the block's OLX:
+ orig_olx = self._get_library_block_olx(block_id)
+ assert ' 1
+@elasticsearch_test
+class ContentLibrariesBlockstoreServiceTest(
+ ContentLibrariesTestMixin,
+ ContentLibrariesRestApiBlockstoreServiceTest,
+):
+ """
+ General tests for Blockstore-based Content Libraries, using the standalone Blockstore service.
+ """
+
+
+@elasticsearch_test
+class ContentLibrariesTest(
+ ContentLibrariesTestMixin,
+ ContentLibrariesRestApiTest,
+):
+ """
+ General tests for Blockstore-based Content Libraries, using the installed Blockstore app.
+ """
+
+
@ddt.ddt
class ContentLibraryXBlockValidationTest(APITestCase):
"""Tests only focused on service validation, no Blockstore needed."""
@@ -941,8 +1015,7 @@ def student_view(self, context=None):
@ddt.ddt
-@elasticsearch_test
-class ContentLibrariesXBlockTypeOverrideTest(ContentLibrariesRestApiTest):
+class ContentLibrariesXBlockTypeOverrideTestMixin:
"""
Tests for Blockstore-based Content Libraries XBlock API,
where the expected XBlock type returned is overridden in the request.
@@ -1073,3 +1146,23 @@ def test_block_type_handler(self, slug, api_args, expected_type):
assert f"lb:CL-TEST:handler-{slug}:video:handler-{slug}" in response['transcripts']['en']
del response['transcripts']['en']
assert response == expected_response
+
+
+@elasticsearch_test
+class ContentLibrariesXBlockTypeOverrideBlockstoreServiceTest(
+ ContentLibrariesXBlockTypeOverrideTestMixin,
+ ContentLibrariesRestApiBlockstoreServiceTest,
+):
+ """
+ Tests for the Content Libraries XBlock API type override using the standalone Blockstore service.
+ """
+
+
+@elasticsearch_test
+class ContentLibrariesXBlockTypeOverrideTest(
+ ContentLibrariesXBlockTypeOverrideTestMixin,
+ ContentLibrariesRestApiTest,
+):
+ """
+ Tests for the Content Libraries XBlock API type override using the installed Blockstore app.
+ """
diff --git a/openedx/core/djangoapps/content_libraries/tests/test_libraries_index.py b/openedx/core/djangoapps/content_libraries/tests/test_libraries_index.py
index 194786db51fe..fb6464c35c06 100644
--- a/openedx/core/djangoapps/content_libraries/tests/test_libraries_index.py
+++ b/openedx/core/djangoapps/content_libraries/tests/test_libraries_index.py
@@ -10,12 +10,14 @@
from search.search_engine_base import SearchEngine
from openedx.core.djangoapps.content_libraries.libraries_index import ContentLibraryIndexer, LibraryBlockIndexer
-from openedx.core.djangoapps.content_libraries.tests.base import ContentLibrariesRestApiTest, elasticsearch_test
+from openedx.core.djangoapps.content_libraries.tests.base import (
+ ContentLibrariesRestApiBlockstoreServiceTest,
+ ContentLibrariesRestApiTest,
+ elasticsearch_test,
+)
-@override_settings(FEATURES={**settings.FEATURES, 'ENABLE_CONTENT_LIBRARY_INDEX': True})
-@elasticsearch_test
-class ContentLibraryIndexerTest(ContentLibrariesRestApiTest):
+class ContentLibraryIndexerTestMixin:
"""
Tests the operation of ContentLibraryIndexer
"""
@@ -181,7 +183,27 @@ def verify_uncommitted_libraries(library_key, has_unpublished_changes, has_unpub
@override_settings(FEATURES={**settings.FEATURES, 'ENABLE_CONTENT_LIBRARY_INDEX': True})
@elasticsearch_test
-class LibraryBlockIndexerTest(ContentLibrariesRestApiTest):
+class ContentLibraryIndexerBlockstoreServiceTest(
+ ContentLibraryIndexerTestMixin,
+ ContentLibrariesRestApiBlockstoreServiceTest,
+):
+ """
+ Tests the operation of ContentLibraryIndexer using the standalone Blockstore service.
+ """
+
+
+@override_settings(FEATURES={**settings.FEATURES, 'ENABLE_CONTENT_LIBRARY_INDEX': True})
+@elasticsearch_test
+class ContentLibraryIndexerTest(
+ ContentLibraryIndexerTestMixin,
+ ContentLibrariesRestApiTest,
+):
+ """
+ Tests the operation of ContentLibraryIndexer using the installed Blockstore app.
+ """
+
+
+class LibraryBlockIndexerTestMixin:
"""
Tests the operation of LibraryBlockIndexer
"""
@@ -279,3 +301,25 @@ def test_crud_block(self):
LibraryBlockIndexer.get_items([block['id']])
self._delete_library(lib['id'])
assert LibraryBlockIndexer.get_items([block['id']]) == []
+
+
+@override_settings(FEATURES={**settings.FEATURES, 'ENABLE_CONTENT_LIBRARY_INDEX': True})
+@elasticsearch_test
+class LibraryBlockIndexerBlockstoreServiceTest(
+ LibraryBlockIndexerTestMixin,
+ ContentLibrariesRestApiBlockstoreServiceTest,
+):
+ """
+ Tests the operation of LibraryBlockIndexer using the standalone Blockstore service.
+ """
+
+
+@override_settings(FEATURES={**settings.FEATURES, 'ENABLE_CONTENT_LIBRARY_INDEX': True})
+@elasticsearch_test
+class LibraryBlockIndexerTest(
+ LibraryBlockIndexerTestMixin,
+ ContentLibrariesRestApiTest,
+):
+ """
+ Tests the operation of LibraryBlockIndexer using the installed Blockstore app.
+ """
diff --git a/openedx/core/djangoapps/content_libraries/tests/test_runtime.py b/openedx/core/djangoapps/content_libraries/tests/test_runtime.py
index 36e768bce1b4..cf9da3dff33c 100644
--- a/openedx/core/djangoapps/content_libraries/tests/test_runtime.py
+++ b/openedx/core/djangoapps/content_libraries/tests/test_runtime.py
@@ -6,7 +6,8 @@
from completion.test_utils import CompletionWaffleTestMixin
from django.db import connections
-from django.test import TestCase, override_settings
+from django.test import LiveServerTestCase, TestCase
+from django.utils.text import slugify
from organizations.models import Organization
from rest_framework.test import APIClient
from xblock.core import XBlock
@@ -14,7 +15,9 @@
from lms.djangoapps.courseware.model_data import get_score
from openedx.core.djangoapps.content_libraries import api as library_api
from openedx.core.djangoapps.content_libraries.tests.base import (
+ BlockstoreAppTestMixin,
requires_blockstore,
+ requires_blockstore_app,
URL_BLOCK_RENDER_VIEW,
URL_BLOCK_GET_HANDLER_URL,
URL_BLOCK_METADATA_URL,
@@ -33,26 +36,27 @@ class ContentLibraryContentTestMixin:
"""
Mixin for content library tests that creates two students and a library.
"""
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
+ def setUp(self):
+ super().setUp()
# Create a couple students that the tests can use
- cls.student_a = UserFactory.create(username="Alice", email="alice@example.com", password="edx")
- cls.student_b = UserFactory.create(username="Bob", email="bob@example.com", password="edx")
+ self.student_a = UserFactory.create(username="Alice", email="alice@example.com", password="edx")
+ self.student_b = UserFactory.create(username="Bob", email="bob@example.com", password="edx")
+
# Create a collection using Blockstore API directly only because there
# is not yet any Studio REST API for doing so:
- cls.collection = blockstore_api.create_collection("Content Library Test Collection")
+ self.collection = blockstore_api.create_collection("Content Library Test Collection")
# Create an organization
- cls.organization = Organization.objects.create(
+ self.organization = Organization.objects.create(
name="Content Libraries Tachyon Exploration & Survey Team",
short_name="CL-TEST",
)
- cls.library = library_api.create_library(
- collection_uuid=cls.collection.uuid,
+ _, slug = self.id().rsplit('.', 1)
+ self.library = library_api.create_library(
+ collection_uuid=self.collection.uuid,
library_type=COMPLEX,
- org=cls.organization,
- slug=cls.__name__,
- title=(cls.__name__ + " Test Lib"),
+ org=self.organization,
+ slug=slugify(slug),
+ title=(f"{slug} Test Lib"),
description="",
allow_public_learning=True,
allow_public_read=False,
@@ -60,10 +64,7 @@ def setUpClass(cls):
)
-@requires_blockstore
-# EphemeralKeyValueStore requires a working cache, and the default test cache doesn't work:
-@override_settings(XBLOCK_RUNTIME_V2_EPHEMERAL_DATA_CACHE='blockstore')
-class ContentLibraryRuntimeTest(ContentLibraryContentTestMixin, TestCase):
+class ContentLibraryRuntimeTestMixin(ContentLibraryContentTestMixin):
"""
Basic tests of the Blockstore-based XBlock runtime using XBlocks in a
content library.
@@ -181,10 +182,25 @@ def test_xblock_metadata(self):
@requires_blockstore
+class ContentLibraryRuntimeBServiceTest(ContentLibraryRuntimeTestMixin, TestCase):
+ """
+ Tests XBlock runtime using XBlocks in a content library using the standalone Blockstore service.
+ """
+
+
+@requires_blockstore_app
+class ContentLibraryRuntimeTest(ContentLibraryRuntimeTestMixin, BlockstoreAppTestMixin, LiveServerTestCase):
+ """
+ Tests XBlock runtime using XBlocks in a content library using the installed Blockstore app.
+
+ We run this test with a live server, so that the blockstore asset files can be served.
+ """
+
+
# We can remove the line below to enable this in Studio once we implement a session-backed
# field data store which we can use for both studio users and anonymous users
@skip_unless_lms
-class ContentLibraryXBlockUserStateTest(ContentLibraryContentTestMixin, TestCase):
+class ContentLibraryXBlockUserStateTestMixin(ContentLibraryContentTestMixin):
"""
Test that the Blockstore-based XBlock runtime can store and retrieve student
state for XBlocks when learners access blocks directly in a library context,
@@ -487,8 +503,27 @@ def test_i18n(self):
@requires_blockstore
+class ContentLibraryXBlockUserStateBServiceTest(ContentLibraryXBlockUserStateTestMixin, TestCase):
+ """
+ Tests XBlock user state for XBlocks in a content library using the standalone Blockstore service.
+ """
+
+
+@requires_blockstore_app
+class ContentLibraryXBlockUserStateTest(
+ ContentLibraryXBlockUserStateTestMixin,
+ BlockstoreAppTestMixin,
+ LiveServerTestCase,
+):
+ """
+ Tests XBlock user state for XBlocks in a content library using the installed Blockstore app.
+
+ We run this test with a live server, so that the blockstore asset files can be served.
+ """
+
+
@skip_unless_lms # No completion tracking in Studio
-class ContentLibraryXBlockCompletionTest(ContentLibraryContentTestMixin, CompletionWaffleTestMixin, TestCase):
+class ContentLibraryXBlockCompletionTestMixin(ContentLibraryContentTestMixin, CompletionWaffleTestMixin):
"""
Test that the Blockstore-based XBlocks can track their completion status
using the completion library.
@@ -539,3 +574,30 @@ def get_block_completion_status():
# Now the block is completed
assert get_block_completion_status() == 1
+
+
+@requires_blockstore
+class ContentLibraryXBlockCompletionBServiceTest(
+ ContentLibraryXBlockCompletionTestMixin,
+ CompletionWaffleTestMixin,
+ TestCase,
+):
+ """
+ Test that the Blockstore-based XBlocks can track their completion status
+ using the standalone Blockstore service.
+ """
+
+
+@requires_blockstore_app
+class ContentLibraryXBlockCompletionTest(
+ ContentLibraryXBlockCompletionTestMixin,
+ CompletionWaffleTestMixin,
+ BlockstoreAppTestMixin,
+ LiveServerTestCase,
+):
+ """
+ Test that the Blockstore-based XBlocks can track their completion status
+ using the installed Blockstore app.
+
+ We run this test with a live server, so that the blockstore asset files can be served.
+ """
diff --git a/openedx/core/djangoapps/content_libraries/tests/test_static_assets.py b/openedx/core/djangoapps/content_libraries/tests/test_static_assets.py
index 0342e27ebda4..a3ff52c789e9 100644
--- a/openedx/core/djangoapps/content_libraries/tests/test_static_assets.py
+++ b/openedx/core/djangoapps/content_libraries/tests/test_static_assets.py
@@ -5,7 +5,10 @@
import requests
-from openedx.core.djangoapps.content_libraries.tests.base import ContentLibrariesRestApiTest
+from openedx.core.djangoapps.content_libraries.tests.base import (
+ ContentLibrariesRestApiBlockstoreServiceTest,
+ ContentLibrariesRestApiTest,
+)
# Binary data representing an SVG image file
SVG_DATA = """