diff --git a/.travis.yml b/.travis.yml index 647cc1b59..7bc7087fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ services: - xvfb sudo: false python: - - "2.7" - "3.5" services: - xvfb diff --git a/Changelog.md b/Changelog.md index 78cbc1b55..fe82b9cc9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,12 @@ Drag and Drop XBlock changelog ============================== +Version 2.3 (2020-08-04) +--------------------------- + +* Make Drag and Drop Indexable. +* Drop Python 2 support. + Version 2.2.10 (2020-04-14) --------------------------- diff --git a/drag_and_drop_v2/drag_and_drop_v2.py b/drag_and_drop_v2/drag_and_drop_v2.py index 52429f628..75c744c07 100644 --- a/drag_and_drop_v2/drag_and_drop_v2.py +++ b/drag_and_drop_v2/drag_and_drop_v2.py @@ -16,6 +16,7 @@ import six.moves.urllib.parse # pylint: disable=import-error import six.moves.urllib.request # pylint: disable=import-error import webob + from django.utils import translation from xblock.core import XBlock from xblock.exceptions import JsonHandlerError @@ -28,7 +29,7 @@ from .default_data import DEFAULT_DATA from .utils import ( Constants, DummyTranslationService, FeedbackMessage, - FeedbackMessages, ItemStats, StateMigration, _ + FeedbackMessages, ItemStats, StateMigration, _clean_data, _ ) # Globals ########################################################### @@ -1119,3 +1120,56 @@ def workbench_scenarios(): "" ), ] + + def index_dictionary(self): + """ + Return dictionary prepared with module content and type for indexing. + """ + # return key/value fields in a Python dict object + # values may be numeric / string or dict + # default implementation is an empty dict + + xblock_body = super(DragAndDropBlock, self).index_dictionary() + + zones_display_names = { + "zone_{}_display_name".format(zone_i): + _clean_data(zone.get("title", "")) + for zone_i, zone in enumerate(self.data.get("zones", [])) + } + + zones_description = { + "zone_{}_description".format(zone_i): + _clean_data(zone.get("description", "")) + for zone_i, zone in enumerate(self.data.get("zones", [])) + } + + items_display_names = { + "item_{}_display_name".format(item_i): + _clean_data(item.get("displayName", "")) + for item_i, item in enumerate(self.data.get("items", [])) + } + + items_image_description = { + "item_{}_image_description".format(item_i): + _clean_data(item.get("imageDescription", "")) + for item_i, item in enumerate(self.data.get("items", [])) + } + + index_body = { + "display_name": self.display_name, + "question_text": _clean_data(self.question_text), + "background_image_description": self.data.get("targetImgDescription", ""), + } + index_body.update(items_display_names) + index_body.update(items_image_description) + index_body.update(zones_display_names) + index_body.update(zones_description) + + if "content" in xblock_body: + xblock_body["content"].update(index_body) + else: + xblock_body["content"] = index_body + + xblock_body["content_type"] = "Drag and Drop" + + return xblock_body diff --git a/drag_and_drop_v2/utils.py b/drag_and_drop_v2/utils.py index 613c454c1..b0efacad4 100644 --- a/drag_and_drop_v2/utils.py +++ b/drag_and_drop_v2/utils.py @@ -3,8 +3,11 @@ from __future__ import absolute_import import copy +import re from collections import namedtuple +from bleach.sanitizer import Cleaner + def _(text): """ Dummy `gettext` replacement to make string extraction tools scrape strings marked for translation """ @@ -19,6 +22,13 @@ def ngettext_fallback(text_singular, text_plural, number): return text_plural +def _clean_data(data): + """ Remove html tags and extra white spaces e.g newline, tabs etc from provided data """ + cleaner = Cleaner(tags=[], strip=True) + cleaned_text = " ".join(re.split(r"\s+", cleaner.clean(data), flags=re.UNICODE)).strip() + return cleaned_text + + class DummyTranslationService(object): """ Dummy drop-in replacement for i18n XBlock service diff --git a/requirements.txt b/requirements.txt index 3a7f31b57..89721c1d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ edx-i18n-tools==0.4.7 pycodestyle==2.5.0 django-statici18n==1.8.0 transifex-client==0.13.4 +bleach==3.1.5 git+https://github.com/edx/xblock-utils.git@v1.2.2#egg=xblock-utils==1.2.2 XBlock==1.2.6 -e . diff --git a/setup.py b/setup.py index f37dc8d09..f86ff1383 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def package_data(pkg, root_list): setup( name='xblock-drag-and-drop-v2', - version='2.2.10', + version='2.3', description='XBlock - Drag-and-Drop v2', packages=['drag_and_drop_v2'], install_requires=[ @@ -31,6 +31,7 @@ def package_data(pkg, root_list): 'xblock-utils', 'ddt', 'mock', + 'bleach', ], entry_points={ 'xblock.v1': 'drag-and-drop-v2 = drag_and_drop_v2:DragAndDropBlock', diff --git a/tests/unit/test_indexibility.py b/tests/unit/test_indexibility.py new file mode 100644 index 000000000..332f265dc --- /dev/null +++ b/tests/unit/test_indexibility.py @@ -0,0 +1,113 @@ +import unittest + +from .test_advanced import BaseDragAndDropAjaxFixture + + +class TestPlainDragAndDropIndexibility(BaseDragAndDropAjaxFixture, unittest.TestCase): + FOLDER = "plain" + + def test_indexibility(self): + expected_indexing_result = { + 'content': { + 'question_text': 'Can you solve this drag-and-drop problem?', + 'item_1_image_description': '', + 'background_image_description': 'This describes the target image', + 'item_2_display_name': 'X', + 'item_0_image_description': '', + 'zone_0_display_name': 'Zone 1', + 'item_3_image_description': '', + 'item_1_display_name': '2', + 'zone_1_display_name': 'Zone 2', + 'zone_1_description': '', + 'item_0_display_name': '1', + 'item_2_image_description': '', + 'zone_0_description': '', + 'item_3_display_name': '', + 'display_name': 'DnDv2 XBlock with plain text instructions' + }, + 'content_type': 'Drag and Drop' + } + self.assertEqual(self.block.index_dictionary(), expected_indexing_result) + + +class TestOldDragAndDropIndexibility(BaseDragAndDropAjaxFixture, unittest.TestCase): + FOLDER = "old" + + def test_indexibility(self): + expected_indexing_result = { + 'content_type': 'Drag and Drop', + 'content': { + 'item_3_image_description': '', + 'zone_1_description': '', + 'item_1_display_name': '2', + 'zone_1_display_name': 'Zone 2', + 'item_1_image_description': '', + 'item_0_display_name': '1', + 'question_text': '', + 'background_image_description': 'This describes the target image', + 'zone_0_description': '', + 'zone_0_display_name': 'Zone 1', + 'item_0_image_description': '', + 'item_3_display_name': '', + 'display_name': 'Drag and Drop', + 'item_2_image_description': '', + 'item_2_display_name': 'X' + } + } + self.assertEqual(self.block.index_dictionary(), expected_indexing_result) + + +class TestHtmlDragAndDropIndexibility(BaseDragAndDropAjaxFixture, unittest.TestCase): + FOLDER = "html" + + def test_indexibility(self): + expected_indexing_result = { + 'content_type': 'Drag and Drop', + 'content': { + 'item_2_display_name': 'X', + 'zone_0_display_name': 'Zone 1', + 'item_1_display_name': '2', + 'item_3_image_description': '', + 'zone_0_description': '', + 'zone_1_display_name': 'Zone 2', + 'background_image_description': 'This describes the target image', + 'zone_1_description': '', + 'item_1_image_description': '', + 'item_0_image_description': '', + 'item_2_image_description': '', + 'question_text': 'Solve this drag-and-drop problem.', + 'item_3_display_name': '', + 'display_name': 'DnDv2 XBlock with HTML instructions', + 'item_0_display_name': '1' + } + } + self.assertEqual(self.block.index_dictionary(), expected_indexing_result) + + +class TestAssessmentDragAndDropIndexibility(BaseDragAndDropAjaxFixture, unittest.TestCase): + FOLDER = "assessment" + + def test_indexibility(self): + expected_indexing_result = { + 'content_type': 'Drag and Drop', + 'content': { + 'item_3_image_description': '', + 'item_0_display_name': '1', + 'zone_0_description': '', + 'item_2_display_name': '3', + 'item_2_image_description': '', + 'background_image_description': 'This describes the target image', + 'display_name': 'DnDv2 XBlock with plain text instructions', + 'item_1_image_description': '', + 'item_4_display_name': '', + 'zone_1_description': '', + 'item_1_display_name': '2', + 'item_3_display_name': 'X', + 'question_text': 'Can you solve this drag-and-drop problem?', + 'zone_1_display_name': 'Zone 2', + 'zone_0_display_name': 'Zone 1', + 'item_4_image_description': '', + 'item_0_image_description': '' + } + } + self.assertEqual(self.block.index_dictionary(), expected_indexing_result)