test: Allow for Extracted Word Cloud Block | Fix Test Cases (#35983)
Run tests for both the built-in and extracted WordCloud block. The tests are mostly compatible with both versions of the block, except for a few places where the XBlock framework and the built-in XModule system differ which we've had to handle using conditionals. This moves us closer to enabling the extracted WordCloud block by default and eventually removing the built-in block. Part of: https://github.com/openedx/edx-platform/issues/34840
This commit is contained in:
committed by
GitHub
parent
17f0e256bd
commit
4a9fc77ecb
@@ -138,11 +138,11 @@ class BaseTestXmodule(ModuleStoreTestCase):
|
||||
self.setup_course()
|
||||
self.initialize_module(metadata=self.METADATA, data=self.DATA)
|
||||
|
||||
def get_url(self, dispatch):
|
||||
def get_url(self, dispatch, handler_name='xmodule_handler'):
|
||||
"""Return item url with dispatch."""
|
||||
return reverse(
|
||||
'xblock_handler',
|
||||
args=(str(self.course.id), quote_slashes(self.item_url), 'xmodule_handler', dispatch)
|
||||
args=(str(self.course.id), quote_slashes(self.item_url), handler_name, dispatch)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
"""Word cloud integration tests using mongo modulestore."""
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import re
|
||||
from operator import itemgetter
|
||||
from unittest.mock import patch
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.test import override_settings
|
||||
from xblock import plugin
|
||||
|
||||
import json
|
||||
from operator import itemgetter
|
||||
|
||||
from common.djangoapps.student.tests.factories import RequestFactoryNoCsrf
|
||||
from xmodule import word_cloud_block
|
||||
# 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
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("override_descriptor_system")
|
||||
class TestWordCloud(BaseTestXmodule):
|
||||
class _TestWordCloudBase(BaseTestXmodule):
|
||||
"""Integration test for Word Cloud Block."""
|
||||
__test__ = False
|
||||
CATEGORY = "word_cloud"
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
plugin.PLUGIN_CACHE = {}
|
||||
importlib.reload(word_cloud_block)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.request_factory = RequestFactoryNoCsrf()
|
||||
|
||||
def _get_users_state(self):
|
||||
"""Return current state for each user:
|
||||
|
||||
@@ -27,7 +44,18 @@ class TestWordCloud(BaseTestXmodule):
|
||||
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:
|
||||
# The extracted Word Cloud XBlock uses @XBlock.json_handler, which expects a different
|
||||
# request format and url pattern
|
||||
handler_url = self.get_url('', handler_name='handle_get_state')
|
||||
response = self.clients[user.username].post(
|
||||
handler_url,
|
||||
data=json.dumps({}),
|
||||
content_type='application/json',
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
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 +68,22 @@ class TestWordCloud(BaseTestXmodule):
|
||||
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:
|
||||
# The extracted Word Cloud XBlock uses @XBlock.json_handler, which expects a different
|
||||
# request format and url pattern
|
||||
handler_url = self.get_url('', handler_name='handle_submit_state')
|
||||
response = self.clients[user.username].post(
|
||||
handler_url,
|
||||
data=json.dumps({'student_words': words}),
|
||||
content_type='application/json',
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
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
|
||||
@@ -52,7 +91,6 @@ class TestWordCloud(BaseTestXmodule):
|
||||
def _check_response(self, response_contents, correct_jsons):
|
||||
"""Utility function that compares correct and real responses."""
|
||||
for username, content in response_contents.items():
|
||||
|
||||
# Used in debugger for comparing objects.
|
||||
# self.maxDiff = None
|
||||
|
||||
@@ -120,7 +158,6 @@ class TestWordCloud(BaseTestXmodule):
|
||||
|
||||
correct_state = {}
|
||||
for index, user in enumerate(self.users):
|
||||
|
||||
correct_state[user.username] = {
|
||||
'status': 'success',
|
||||
'submitted': True,
|
||||
@@ -202,6 +239,14 @@ class TestWordCloud(BaseTestXmodule):
|
||||
for user in self.users
|
||||
}
|
||||
|
||||
if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK:
|
||||
# The extracted Word Cloud XBlock uses @XBlock.json_handler to handle AJAX requests,
|
||||
# which automatically returns a 404 for unknown requests, so there's no need to test
|
||||
# the incorrect dispatch case in this scenario.
|
||||
for username, response in responses.items():
|
||||
self.assertEqual(response.status_code, 404)
|
||||
return
|
||||
|
||||
status_codes = {response.status_code for response in responses.values()}
|
||||
assert status_codes.pop() == 200
|
||||
|
||||
@@ -214,19 +259,44 @@ class TestWordCloud(BaseTestXmodule):
|
||||
}
|
||||
)
|
||||
|
||||
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:
|
||||
# If `USE_EXTRACTED_WORD_CLOUD_BLOCK` is enabled, the `expected_context` will be different
|
||||
# because in the extracted Word Cloud XBlock, the expected context:
|
||||
# - contains `range_num_inputs`
|
||||
# - uses `UUID` for `element_id` instead of `html_id()`
|
||||
# - does not include `ajax_url` since it uses the `@XBlock.json_handler` decorator for AJAX requests
|
||||
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()
|
||||
# Remove i18n service
|
||||
fragment_content_clean = re.sub(r"\{.*?}", "{}", fragment.content)
|
||||
assert fragment_content_clean == 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)
|
||||
|
||||
|
||||
@override_settings(USE_EXTRACTED_WORD_CLOUD_BLOCK=True)
|
||||
class TestWordCloudExtracted(_TestWordCloudBase):
|
||||
__test__ = True
|
||||
|
||||
|
||||
@override_settings(USE_EXTRACTED_WORD_CLOUD_BLOCK=False)
|
||||
class TestWordCloudBuiltIn(_TestWordCloudBase):
|
||||
__test__ = True
|
||||
|
||||
@@ -1,38 +1,48 @@
|
||||
"""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 django.test import override_settings
|
||||
from fs.memoryfs import MemoryFS
|
||||
from lxml import etree
|
||||
from webob import Request
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
from webob import Request
|
||||
from webob.multidict import MultiDict
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds
|
||||
|
||||
from xmodule.word_cloud_block import WordCloudBlock
|
||||
from xmodule import word_cloud_block
|
||||
from . import get_test_descriptor_system, get_test_system
|
||||
|
||||
|
||||
class WordCloudBlockTest(TestCase):
|
||||
class _TestWordCloudBase(TestCase):
|
||||
"""
|
||||
Logic tests for Word Cloud Block.
|
||||
"""
|
||||
__test__ = False
|
||||
|
||||
raw_field_data = {
|
||||
'all_words': {'cat': 10, 'dog': 5, 'mom': 1, 'dad': 2},
|
||||
'top_words': {'cat': 10, 'dog': 5, 'dad': 2},
|
||||
'submitted': False,
|
||||
'display_name': 'Word Cloud Block',
|
||||
'instructions': 'Enter some random words that comes to your mind'
|
||||
}
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.word_cloud_class = word_cloud_block.reset_class()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.raw_field_data = {
|
||||
'all_words': {'cat': 10, 'dog': 5, 'mom': 1, 'dad': 2},
|
||||
'top_words': {'cat': 10, 'dog': 5, 'dad': 2},
|
||||
'submitted': False,
|
||||
'display_name': 'Word Cloud Block',
|
||||
'instructions': 'Enter some random words that comes to your mind'
|
||||
}
|
||||
|
||||
def test_xml_import_export_cycle(self):
|
||||
"""
|
||||
Test the import export cycle.
|
||||
"""
|
||||
|
||||
runtime = get_test_descriptor_system()
|
||||
runtime.export_fs = MemoryFS()
|
||||
|
||||
@@ -43,7 +53,11 @@ class WordCloudBlockTest(TestCase):
|
||||
|
||||
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 = self.word_cloud_class.parse_xml(olx_element, runtime, keys)
|
||||
|
||||
block.location = BlockUsageLocator(
|
||||
CourseLocator('org', 'course', 'run', branch='revision'), 'word_cloud', 'block_id'
|
||||
)
|
||||
@@ -54,38 +68,70 @@ class WordCloudBlockTest(TestCase):
|
||||
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:
|
||||
# For extracted XBlocks, we need to manually export the XML definition to a file to properly test the
|
||||
# import/export cycle. This is because extracted XBlocks use XBlock core's `add_xml_to_node` method,
|
||||
# which does not export the XML to a file like `XmlMixin.add_xml_to_node` does.
|
||||
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:
|
||||
# For extracted XBlocks, we need to remove the `xblock-family` attribute from the exported XML to ensure
|
||||
# consistency with the original XML.
|
||||
# This is because extracted XBlocks use the core XBlock's `add_xml_to_node` method, which includes this
|
||||
# attribute, whereas `XmlMixin.add_xml_to_node` does not.
|
||||
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.
|
||||
"""
|
||||
|
||||
module_system = get_test_system()
|
||||
block = WordCloudBlock(module_system, DictFieldData(self.raw_field_data), Mock())
|
||||
block = self.word_cloud_class(module_system, DictFieldData(self.raw_field_data), Mock())
|
||||
|
||||
response = json.loads(block.handle_ajax('bad_dispatch', {}))
|
||||
self.assertDictEqual(response, {
|
||||
'status': 'fail',
|
||||
'error': 'Unknown Command!'
|
||||
})
|
||||
if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK:
|
||||
# The extracted Word Cloud XBlock uses @XBlock.json_handler for handling AJAX requests,
|
||||
# which requires a different way of method invocation.
|
||||
with self.assertRaises(AttributeError) as context:
|
||||
json.loads(block.bad_dispatch('bad_dispatch', {}))
|
||||
self.assertIn("'WordCloudBlock' object has no attribute 'bad_dispatch'", str(context.exception))
|
||||
else:
|
||||
response = json.loads(block.handle_ajax('bad_dispatch', {}))
|
||||
self.assertDictEqual(response, {
|
||||
'status': 'fail',
|
||||
'error': 'Unknown Command!'
|
||||
})
|
||||
|
||||
def test_good_ajax_request(self):
|
||||
"""
|
||||
Make sure that ajax request works correctly.
|
||||
"""
|
||||
|
||||
module_system = get_test_system()
|
||||
block = WordCloudBlock(module_system, DictFieldData(self.raw_field_data), Mock())
|
||||
block = self.word_cloud_class(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:
|
||||
# The extracted Word Cloud XBlock uses @XBlock.json_handler for handling AJAX requests.
|
||||
# It expects a standard Python dictionary as POST data and returns a JSON object in response.
|
||||
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
|
||||
@@ -109,9 +155,8 @@ class WordCloudBlockTest(TestCase):
|
||||
"""
|
||||
Test indexibility of Word Cloud
|
||||
"""
|
||||
|
||||
module_system = get_test_system()
|
||||
block = WordCloudBlock(module_system, DictFieldData(self.raw_field_data), Mock())
|
||||
block = self.word_cloud_class(module_system, DictFieldData(self.raw_field_data), Mock())
|
||||
assert block.index_dictionary() ==\
|
||||
{'content_type': 'Word Cloud',
|
||||
'content': {'display_name': 'Word Cloud Block',
|
||||
@@ -128,13 +173,23 @@ class WordCloudBlockTest(TestCase):
|
||||
'num_top_words': 10,
|
||||
'display_student_percents': 'False',
|
||||
}
|
||||
if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK:
|
||||
# In the extracted Word Cloud XBlock, we use StudioEditableXBlockMixin.submit_studio_edits,
|
||||
# which expects a different handler name and request JSON format.
|
||||
handler_name = 'submit_studio_edits'
|
||||
TEST_REQUEST_JSON = {
|
||||
'values': TEST_SUBMIT_DATA,
|
||||
}
|
||||
else:
|
||||
handler_name = 'studio_submit'
|
||||
TEST_REQUEST_JSON = TEST_SUBMIT_DATA
|
||||
module_system = get_test_system()
|
||||
block = WordCloudBlock(module_system, DictFieldData(self.raw_field_data), Mock())
|
||||
body = json.dumps(TEST_SUBMIT_DATA)
|
||||
block = self.word_cloud_class(module_system, DictFieldData(self.raw_field_data), Mock())
|
||||
body = json.dumps(TEST_REQUEST_JSON)
|
||||
request = Request.blank('/')
|
||||
request.method = 'POST'
|
||||
request.body = body.encode('utf-8')
|
||||
res = block.handle('studio_submit', request)
|
||||
res = block.handle(handler_name, request)
|
||||
assert json.loads(res.body.decode('utf8')) == {'result': 'success'}
|
||||
|
||||
assert block.display_name == TEST_SUBMIT_DATA['display_name']
|
||||
@@ -142,3 +197,13 @@ class WordCloudBlockTest(TestCase):
|
||||
assert block.num_inputs == TEST_SUBMIT_DATA['num_inputs']
|
||||
assert block.num_top_words == TEST_SUBMIT_DATA['num_top_words']
|
||||
assert block.display_student_percents == (TEST_SUBMIT_DATA['display_student_percents'] == "True")
|
||||
|
||||
|
||||
@override_settings(USE_EXTRACTED_WORD_CLOUD_BLOCK=True)
|
||||
class TestWordCloudExtracted(_TestWordCloudBase):
|
||||
__test__ = True
|
||||
|
||||
|
||||
@override_settings(USE_EXTRACTED_WORD_CLOUD_BLOCK=False)
|
||||
class TestWordCloudBuiltIn(_TestWordCloudBase):
|
||||
__test__ = True
|
||||
|
||||
@@ -316,8 +316,17 @@ class _BuiltInWordCloudBlock( # pylint: disable=abstract-method
|
||||
return xblock_body
|
||||
|
||||
|
||||
WordCloudBlock = (
|
||||
_ExtractedWordCloudBlock if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK
|
||||
else _BuiltInWordCloudBlock
|
||||
)
|
||||
WordCloudBlock = None
|
||||
|
||||
|
||||
def reset_class():
|
||||
"""Reset class as per django settings flag"""
|
||||
global WordCloudBlock
|
||||
WordCloudBlock = (
|
||||
_ExtractedWordCloudBlock if settings.USE_EXTRACTED_WORD_CLOUD_BLOCK
|
||||
else _BuiltInWordCloudBlock
|
||||
)
|
||||
return WordCloudBlock
|
||||
|
||||
reset_class()
|
||||
WordCloudBlock.__name__ = "WordCloudBlock"
|
||||
|
||||
Reference in New Issue
Block a user