diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 9867ac72f273..26cb40380972 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -17,6 +17,7 @@ jobs: name: ${{ matrix.shard_name }}(py=${{ matrix.python-version }},dj=${{ matrix.django-version }},mongo=${{ matrix.mongo-version }}) runs-on: ${{ matrix.os-version }} strategy: + fail-fast: false matrix: python-version: - "3.11" diff --git a/lms/djangoapps/courseware/tests/test_word_cloud.py b/lms/djangoapps/courseware/tests/test_word_cloud.py index 06217628cbca..6b444ef38f6a 100644 --- a/lms/djangoapps/courseware/tests/test_word_cloud.py +++ b/lms/djangoapps/courseware/tests/test_word_cloud.py @@ -1,15 +1,19 @@ """Word cloud integration tests using mongo modulestore.""" - - -import pytest - import json +import re +from uuid import UUID from operator import itemgetter +from unittest.mock import patch +import pytest +from django.conf import settings + +from common.djangoapps.student.tests.factories import RequestFactoryNoCsrf +from lms.djangoapps.courseware import block_render as render +from openedx.core.lib.url_utils import quote_slashes # noinspection PyUnresolvedReferences -from xmodule.tests.helpers import override_descriptor_system # pylint: disable=unused-import +from xmodule.tests.helpers import override_descriptor_system, mock_render_template # pylint: disable=unused-import from xmodule.x_module import STUDENT_VIEW - from .helpers import BaseTestXmodule @@ -18,6 +22,10 @@ class TestWordCloud(BaseTestXmodule): """Integration test for Word Cloud Block.""" CATEGORY = "word_cloud" + def setUp(self): + super().setUp() + self.request_factory = RequestFactoryNoCsrf() + def _get_users_state(self): """Return current state for each user: @@ -27,7 +35,23 @@ def _get_users_state(self): users_state = {} for user in self.users: - response = self.clients[user.username].post(self.get_url('get_state')) + if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK: + request = self.request_factory.post( + '/', + content_type='application/json', + HTTP_X_REQUESTED_WITH='XMLHttpRequest' + ) + request.user = user + request.session = {} + response = render.handle_xblock_callback( + request, + str(self.course.id), + quote_slashes(self.item_url), + 'handle_get_state', + '', + ) + else: + response = self.clients[user.username].post(self.get_url('get_state')) users_state[user.username] = json.loads(response.content.decode('utf-8')) return users_state @@ -40,11 +64,28 @@ def _post_words(self, words): users_state = {} for user in self.users: - response = self.clients[user.username].post( - self.get_url('submit'), - {'student_words[]': words}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest' - ) + if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK: + request = self.request_factory.post( + '/', + data={'student_words': words}, + content_type='application/json', + HTTP_X_REQUESTED_WITH='XMLHttpRequest' + ) + request.user = user + request.session = {} + response = render.handle_xblock_callback( + request, + str(self.course.id), + quote_slashes(self.item_url), + 'handle_submit_state', + '', + ) + else: + response = self.clients[user.username].post( + self.get_url('submit'), + {'student_words[]': words}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest' + ) users_state[user.username] = json.loads(response.content.decode('utf-8')) return users_state @@ -202,31 +243,43 @@ def test_handle_ajax_incorrect_dispatch(self): for user in self.users } - status_codes = {response.status_code for response in responses.values()} - assert status_codes.pop() == 200 + if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK: + for username, response in responses.items(): + self.assertEqual(response.status_code, 404) + else: + status_codes = {response.status_code for response in responses.values()} + assert status_codes.pop() == 200 - for user in self.users: - self.assertDictEqual( - json.loads(responses[user.username].content.decode('utf-8')), - { - 'status': 'fail', - 'error': 'Unknown Command!' - } - ) + for user in self.users: + self.assertDictEqual( + json.loads(responses[user.username].content.decode('utf-8')), + { + 'status': 'fail', + 'error': 'Unknown Command!' + } + ) - def test_word_cloud_constructor(self): + @patch('xblock.utils.resources.ResourceLoader.render_django_template', side_effect=mock_render_template) + def test_word_cloud_constructor(self, mock_render_django_template): """ Make sure that all parameters extracted correctly from xml. """ fragment = self.runtime.render(self.block, STUDENT_VIEW) expected_context = { - 'ajax_url': self.block.ajax_url, 'display_name': self.block.display_name, 'instructions': self.block.instructions, - 'element_class': self.block.location.block_type, - 'element_id': self.block.location.html_id(), + 'element_class': self.block.scope_ids.block_type, 'num_inputs': 5, # default value 'submitted': False, # default value, } - assert fragment.content == self.runtime.render_template('word_cloud.html', expected_context) + if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK: + expected_context['range_num_inputs'] = range(5) + uuid_str = re.search(r"UUID\('([a-f0-9\-]+)'\)", fragment.content).group(1) + expected_context['element_id'] = UUID(uuid_str) + mock_render_django_template.assert_called_once() + assert fragment.content == self.runtime.render_template('templates/word_cloud.html', expected_context) + else: + expected_context['ajax_url'] = self.block.ajax_url + expected_context['element_id'] = self.block.location.html_id() + assert fragment.content == self.runtime.render_template('word_cloud.html', expected_context) diff --git a/lms/envs/common.py b/lms/envs/common.py index 76127d062d81..d485abd29f85 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -5573,7 +5573,7 @@ def _should_send_learning_badge_events(settings): # .. toggle_warning: Not production-ready until https://github.com/openedx/edx-platform/issues/34840 is done. # .. toggle_creation_date: 2024-11-10 # .. toggle_target_removal_date: 2025-06-01 -USE_EXTRACTED_WORD_CLOUD_BLOCK = False +USE_EXTRACTED_WORD_CLOUD_BLOCK = True # .. toggle_name: USE_EXTRACTED_ANNOTATABLE_BLOCK # .. toggle_default: False diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index c075f9f2d40a..b870877d2b1a 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -1278,7 +1278,7 @@ xblock-utils==4.0.0 # via # edx-sga # xblock-poll -xblocks-contrib==0.1.0 +git+https://github.com/openedx/xblocks-contrib.git@farhan/word-cloud-xblock # via -r requirements/edx/bundled.in xmlsec==1.3.14 # via python3-saml diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 412b8c6147bf..bdbda597d063 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -2282,7 +2282,7 @@ xblock-utils==4.0.0 # -r requirements/edx/testing.txt # edx-sga # xblock-poll -xblocks-contrib==0.1.0 +git+https://github.com/openedx/xblocks-contrib.git@farhan/word-cloud-xblock # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 06436873c4cd..a29cfbeba698 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -1606,7 +1606,7 @@ xblock-utils==4.0.0 # -r requirements/edx/base.txt # edx-sga # xblock-poll -xblocks-contrib==0.1.0 +git+https://github.com/openedx/xblocks-contrib.git@farhan/word-cloud-xblock # via -r requirements/edx/base.txt xmlsec==1.3.14 # via diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 4fd65de9cfa6..dee385b5404b 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1696,7 +1696,7 @@ xblock-utils==4.0.0 # -r requirements/edx/base.txt # edx-sga # xblock-poll -xblocks-contrib==0.1.0 +git+https://github.com/openedx/xblocks-contrib.git@farhan/word-cloud-xblock # via -r requirements/edx/base.txt xmlsec==1.3.14 # via diff --git a/xmodule/tests/test_word_cloud.py b/xmodule/tests/test_word_cloud.py index 6c928465262b..82f8b196ba64 100644 --- a/xmodule/tests/test_word_cloud.py +++ b/xmodule/tests/test_word_cloud.py @@ -1,14 +1,17 @@ """Test for Word Cloud Block functional logic.""" import json +import os from unittest.mock import Mock +from django.conf import settings from django.test import TestCase from fs.memoryfs import MemoryFS from lxml import etree from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator from webob.multidict import MultiDict from xblock.field_data import DictFieldData +from xblock.fields import ScopeIds from xmodule.word_cloud_block import WordCloudBlock from . import get_test_descriptor_system, get_test_system @@ -42,7 +45,11 @@ def test_xml_import_export_cycle(self): olx_element = etree.fromstring(original_xml) runtime.id_generator = Mock() - block = WordCloudBlock.parse_xml(olx_element, runtime, None) + + def_id = runtime.id_generator.create_definition(olx_element.tag, olx_element.get('url_name')) + keys = ScopeIds(None, olx_element.tag, def_id, runtime.id_generator.create_usage(def_id)) + block = WordCloudBlock.parse_xml(olx_element, runtime, keys) + block.location = BlockUsageLocator( CourseLocator('org', 'course', 'run', branch='revision'), 'word_cloud', 'block_id' ) @@ -53,18 +60,34 @@ def test_xml_import_export_cycle(self): assert block.num_inputs == 3 assert block.num_top_words == 100 - node = etree.Element("unknown_root") - # This will export the olx to a separate file. - block.add_xml_to_node(node) + if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK: + filepath = 'word_cloud/block_id.xml' + runtime.export_fs.makedirs(os.path.dirname(filepath), recreate=True) + with runtime.export_fs.open(filepath, 'wb') as fileObj: + runtime.export_to_xml(block, fileObj) + else: + node = etree.Element("unknown_root") + # This will export the olx to a separate file. + block.add_xml_to_node(node) + with runtime.export_fs.open('word_cloud/block_id.xml') as f: exported_xml = f.read() + if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK: + exported_xml_tree = etree.fromstring(exported_xml.encode('utf-8')) + etree.cleanup_namespaces(exported_xml_tree) + if 'xblock-family' in exported_xml_tree.attrib: + del exported_xml_tree.attrib['xblock-family'] + exported_xml = etree.tostring(exported_xml_tree, encoding='unicode', pretty_print=True) + assert exported_xml == original_xml def test_bad_ajax_request(self): """ Make sure that answer for incorrect request is error json. """ + if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK: + return module_system = get_test_system() block = WordCloudBlock(module_system, DictFieldData(self.raw_field_data), Mock()) @@ -83,8 +106,12 @@ def test_good_ajax_request(self): module_system = get_test_system() block = WordCloudBlock(module_system, DictFieldData(self.raw_field_data), Mock()) - post_data = MultiDict(('student_words[]', word) for word in ['cat', 'cat', 'dog', 'sun']) - response = json.loads(block.handle_ajax('submit', post_data)) + if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK: + post_data = {'student_words': ['cat', 'cat', 'dog', 'sun']} + response = block.submit_state(post_data) + else: + post_data = MultiDict(('student_words[]', word) for word in ['cat', 'cat', 'dog', 'sun']) + response = json.loads(block.handle_ajax('submit', post_data)) assert response['status'] == 'success' assert response['submitted'] is True assert response['total_count'] == 22