-
Notifications
You must be signed in to change notification settings - Fork 4.3k
feat: Allow specifying a version when loading a v2 XBlock #35626
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
d0dce85
feat: Allow specifying a version when loading a v2 XBlock
bradenmacdonald a2dd25c
fix: 500 error if a v2 library xblock checks a course waffle flag
bradenmacdonald ec197b7
fix: v2 xblock "fields" API can now change display_name of any block
bradenmacdonald b4dd818
feat: Allow specifying a version when loading a v2 XBlock
bradenmacdonald badefc3
test: add tests for retrieving a particular XBlock version
bradenmacdonald 89f369c
feat: call XBlock handlers using the same version of the block
bradenmacdonald 4949d2d
chore: address mypy/pylint issues
bradenmacdonald 4c0ab81
test: check that handlers can't overwrite draft data with old data
bradenmacdonald d79d43f
docs: address review comments
bradenmacdonald File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
openedx/core/djangoapps/content_libraries/tests/fields_test_block.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| """ | ||
| Block for testing variously scoped XBlock fields. | ||
| """ | ||
| import json | ||
|
|
||
| from webob import Response | ||
| from web_fragments.fragment import Fragment | ||
| from xblock.core import XBlock, Scope | ||
| from xblock import fields | ||
|
|
||
|
|
||
| class FieldsTestBlock(XBlock): | ||
| """ | ||
| Block for testing variously scoped XBlock fields and XBlock handlers. | ||
|
|
||
| This has only authored fields. See also UserStateTestBlock which has user fields. | ||
| """ | ||
| BLOCK_TYPE = "fields-test" | ||
| has_score = False | ||
|
|
||
| display_name = fields.String(scope=Scope.settings, name='User State Test Block') | ||
| setting_field = fields.String(scope=Scope.settings, name='A setting') | ||
| content_field = fields.String(scope=Scope.content, name='A setting') | ||
|
|
||
| @XBlock.json_handler | ||
| def update_fields(self, data, suffix=None): # pylint: disable=unused-argument | ||
| """ | ||
| Update the authored fields of this block | ||
| """ | ||
| self.display_name = data["display_name"] | ||
| self.setting_field = data["setting_field"] | ||
| self.content_field = data["content_field"] | ||
| return {} | ||
|
|
||
| @XBlock.handler | ||
| def get_fields(self, request, suffix=None): # pylint: disable=unused-argument | ||
| """ | ||
| Get the various fields of this XBlock. | ||
| """ | ||
| return Response( | ||
| json.dumps({ | ||
| "display_name": self.display_name, | ||
| "setting_field": self.setting_field, | ||
| "content_field": self.content_field, | ||
| }), | ||
| content_type='application/json', | ||
| charset='UTF-8', | ||
| ) | ||
|
|
||
| def student_view(self, _context): | ||
| """ | ||
| Return the student view. | ||
| """ | ||
| fragment = Fragment() | ||
| fragment.add_content(f'<h1>{self.display_name}</h1>\n') | ||
| fragment.add_content(f'<p>SF: {self.setting_field}</p>\n') | ||
| fragment.add_content(f'<p>CF: {self.content_field}</p>\n') | ||
| handler_url = self.runtime.handler_url(self, 'get_fields') | ||
| fragment.add_content(f'<p>handler URL: {handler_url}</p>\n') | ||
| return fragment |
184 changes: 184 additions & 0 deletions
184
openedx/core/djangoapps/content_libraries/tests/test_embed_block.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| """ | ||
| Tests for the XBlock v2 runtime's "embed" view, using Content Libraries | ||
|
|
||
| This view is used in the MFE to preview XBlocks that are in the library. | ||
| """ | ||
| import re | ||
|
|
||
| import ddt | ||
| from django.core.exceptions import ValidationError | ||
| from django.test.utils import override_settings | ||
| from openedx_events.tests.utils import OpenEdxEventsTestMixin | ||
| import pytest | ||
| from xblock.core import XBlock | ||
|
|
||
| from openedx.core.djangoapps.content_libraries.tests.base import ( | ||
| ContentLibrariesRestApiTest | ||
| ) | ||
| from openedx.core.djangolib.testing.utils import skip_unless_cms | ||
| from .fields_test_block import FieldsTestBlock | ||
|
|
||
|
|
||
| @skip_unless_cms | ||
| @ddt.ddt | ||
| @override_settings(CORS_ORIGIN_WHITELIST=[]) # For some reason, this setting isn't defined in our test environment? | ||
| class LibrariesEmbedViewTestCase(ContentLibrariesRestApiTest, OpenEdxEventsTestMixin): | ||
| """ | ||
| Tests for embed_view and interacting with draft/published/past versions of | ||
| Learning-Core-based XBlocks (in Content Libraries). | ||
|
|
||
| These tests use the REST API, which in turn relies on the Python API. | ||
| Some tests may use the python API directly if necessary to provide | ||
| coverage of any code paths not accessible via the REST API. | ||
|
|
||
| In general, these tests should | ||
| (1) Use public APIs only - don't directly create data using other methods, | ||
| which results in a less realistic test and ties the test suite too | ||
| closely to specific implementation details. | ||
| (Exception: users can be provisioned using a user factory) | ||
| (2) Assert that fields are present in responses, but don't assert that the | ||
| entire response has some specific shape. That way, things like adding | ||
| new fields to an API response, which are backwards compatible, won't | ||
| break any tests, but backwards-incompatible API changes will. | ||
|
|
||
| WARNING: every test should have a unique library slug, because even though | ||
| the django/mysql database gets reset for each test case, the lookup between | ||
| library slug and bundle UUID does not because it's assumed to be immutable | ||
| and cached forever. | ||
| """ | ||
|
|
||
| @XBlock.register_temp_plugin(FieldsTestBlock, FieldsTestBlock.BLOCK_TYPE) | ||
| def test_embed_vew_versions(self): | ||
| """ | ||
| Test that the embed_view renders a block and can render different versions of it. | ||
| """ | ||
| # Create a library: | ||
| lib = self._create_library(slug="test-eb-1", title="Test Library", description="") | ||
| lib_id = lib["id"] | ||
| # Create an XBlock. This will be the empty version 1: | ||
| create_response = self._add_block_to_library(lib_id, FieldsTestBlock.BLOCK_TYPE, "block1") | ||
| block_id = create_response["id"] | ||
| # Create version 2 of the block by setting its OLX: | ||
| olx_response = self._set_library_block_olx(block_id, """ | ||
| <fields-test | ||
| display_name="Field Test Block (Old, v2)" | ||
| setting_field="Old setting value 2." | ||
| content_field="Old content value 2." | ||
| /> | ||
| """) | ||
| assert olx_response["version_num"] == 2 | ||
| # Create version 3 of the block by setting its OLX again: | ||
| olx_response = self._set_library_block_olx(block_id, """ | ||
| <fields-test | ||
| display_name="Field Test Block (Published, v3)" | ||
| setting_field="Published setting value 3." | ||
| content_field="Published content value 3." | ||
| /> | ||
| """) | ||
| assert olx_response["version_num"] == 3 | ||
| # Publish the library: | ||
| self._commit_library_changes(lib_id) | ||
|
|
||
| # Create the draft (version 4) of the block: | ||
| olx_response = self._set_library_block_olx(block_id, """ | ||
| <fields-test | ||
| display_name="Field Test Block (Draft, v4)" | ||
| setting_field="Draft setting value 4." | ||
| content_field="Draft content value 4." | ||
| /> | ||
| """) | ||
|
|
||
| # Now render the "embed block" view. This test only runs in CMS so it should default to the draft: | ||
| html = self._embed_block(block_id) | ||
|
|
||
| def check_fields(display_name, setting_value, content_value): | ||
| assert f'<h1>{display_name}</h1>' in html | ||
| assert f'<p>SF: {setting_value}</p>' in html | ||
| assert f'<p>CF: {content_value}</p>' in html | ||
| handler_url = re.search(r'<p>handler URL: ([^<]+)</p>', html).group(1) | ||
| assert handler_url.startswith('http') | ||
| handler_result = self.client.get(handler_url).json() | ||
| assert handler_result == { | ||
| "display_name": display_name, | ||
| "setting_field": setting_value, | ||
| "content_field": content_value, | ||
| } | ||
| check_fields('Field Test Block (Draft, v4)', 'Draft setting value 4.', 'Draft content value 4.') | ||
|
|
||
| # But if we request the published version, we get that: | ||
| html = self._embed_block(block_id, version="published") | ||
| check_fields('Field Test Block (Published, v3)', 'Published setting value 3.', 'Published content value 3.') | ||
|
|
||
| # And if we request a specific version, we get that: | ||
| html = self._embed_block(block_id, version=3) | ||
| check_fields('Field Test Block (Published, v3)', 'Published setting value 3.', 'Published content value 3.') | ||
|
|
||
| # And if we request a specific version, we get that: | ||
| html = self._embed_block(block_id, version=2) | ||
| check_fields('Field Test Block (Old, v2)', 'Old setting value 2.', 'Old content value 2.') | ||
|
|
||
| html = self._embed_block(block_id, version=4) | ||
| check_fields('Field Test Block (Draft, v4)', 'Draft setting value 4.', 'Draft content value 4.') | ||
|
|
||
| @XBlock.register_temp_plugin(FieldsTestBlock, FieldsTestBlock.BLOCK_TYPE) | ||
| def test_handlers_modifying_published_data(self): | ||
| """ | ||
| Test that if we requested any version other than "draft", the handlers should not allow _writing_ to authored | ||
| field data (because you'd be overwriting the latest draft version with changes based on an old version). | ||
|
|
||
| We may decide to relax this restriction in the future. Not sure how important it is. | ||
|
|
||
| Writing to student state is OK. | ||
| """ | ||
| # Create a library: | ||
| lib = self._create_library(slug="test-eb-2", title="Test Library", description="") | ||
| lib_id = lib["id"] | ||
| # Create an XBlock. This will be the empty version 1: | ||
| create_response = self._add_block_to_library(lib_id, FieldsTestBlock.BLOCK_TYPE, "block1") | ||
| block_id = create_response["id"] | ||
|
|
||
| # Now render the "embed block" view. This test only runs in CMS so it should default to the draft: | ||
| html = self._embed_block(block_id) | ||
|
|
||
| def call_update_handler(**kwargs): | ||
| handler_url = re.search(r'<p>handler URL: ([^<]+)</p>', html).group(1) | ||
| assert handler_url.startswith('http') | ||
| handler_url = handler_url.replace('get_fields', 'update_fields') | ||
| response = self.client.post(handler_url, kwargs, format='json') | ||
| assert response.status_code == 200 | ||
|
|
||
| def check_fields(display_name, setting_field, content_field): | ||
| assert f'<h1>{display_name}</h1>' in html | ||
| assert f'<p>SF: {setting_field}</p>' in html | ||
| assert f'<p>CF: {content_field}</p>' in html | ||
|
|
||
| # Call the update handler to change the fields on the draft: | ||
| call_update_handler(display_name="DN-01", setting_field="SV-01", content_field="CV-01") | ||
|
|
||
| # Render the block again and check that the handler was able to update the fields: | ||
| html = self._embed_block(block_id) | ||
| check_fields(display_name="DN-01", setting_field="SV-01", content_field="CV-01") | ||
|
|
||
| # Publish the library: | ||
| self._commit_library_changes(lib_id) | ||
|
|
||
| # Now try changing the authored fields of the published version using a handler: | ||
| html = self._embed_block(block_id, version="published") | ||
| expected_msg = "Do not make changes to a component starting from the published or past versions." | ||
| with pytest.raises(ValidationError, match=expected_msg) as err: | ||
| call_update_handler(display_name="DN-X", setting_field="SV-X", content_field="CV-X") | ||
|
|
||
| # Now try changing the authored fields of a specific past version using a handler: | ||
| html = self._embed_block(block_id, version=2) | ||
| with pytest.raises(ValidationError, match=expected_msg) as err: | ||
| call_update_handler(display_name="DN-X", setting_field="SV-X", content_field="CV-X") | ||
|
|
||
| # Make sure the fields were not updated: | ||
| html = self._embed_block(block_id) | ||
| check_fields(display_name="DN-01", setting_field="SV-01", content_field="CV-01") | ||
|
|
||
| # TODO: test that any static assets referenced in the student_view html are loaded as the correct version, and not | ||
| # always loaded as "latest draft". | ||
|
|
||
| # TODO: if we are ever able to run these tests in the LMS, test that the LMS only allows accessing the published | ||
| # version. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.