├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── data │ │ ├── old │ │ │ ├── settings.json │ │ │ ├── data.json │ │ │ └── config_out.json │ │ ├── html │ │ │ ├── settings.json │ │ │ ├── data.json │ │ │ └── config_out.json │ │ ├── plain │ │ │ ├── settings.json │ │ │ ├── data.json │ │ │ └── config_out.json │ │ └── assessment │ │ │ ├── settings.json │ │ │ ├── data.json │ │ │ └── config_out.json │ ├── test_fixtures.py │ ├── test_indexibility.py │ └── test_standard_mode.py ├── pylintrc └── utils.py ├── requirements ├── workbench.txt ├── pip.in ├── ci.in ├── base.in ├── pip-tools.in ├── quality.in ├── dev.in ├── pip.txt ├── test.in ├── pip-tools.txt ├── constraints.txt ├── ci.txt ├── private.readme ├── base.txt ├── test.txt ├── quality.txt └── dev.txt ├── drag_and_drop_v2 ├── translations ├── conf │ └── locale │ │ ├── __init__.py │ │ ├── config.yaml │ │ └── settings.py ├── __init__.py ├── public │ ├── img │ │ └── triangle.png │ ├── themes │ │ └── apros.css │ ├── js │ │ ├── translations │ │ │ ├── en │ │ │ │ └── text.js │ │ │ ├── tr │ │ │ │ └── text.js │ │ │ ├── nl │ │ │ │ └── text.js │ │ │ ├── hi │ │ │ │ └── text.js │ │ │ └── it │ │ │ │ └── text.js │ │ └── vendor │ │ │ └── virtual-dom-1.3.0.min.js │ └── css │ │ └── drag_and_drop_edit.css ├── templates │ └── html │ │ ├── drag_and_drop.html │ │ └── js_templates.html ├── compat.py ├── default_data.py └── utils.py ├── doc └── img │ ├── edit-view.png │ ├── edit-view-items.png │ ├── edit-view-zones.png │ ├── student-view-start.png │ └── student-view-finish.png ├── MANIFEST.in ├── .github ├── dependabot.yml ├── workflows │ ├── commitlint.yml │ ├── self-assign-issue.yml │ ├── add-depr-ticket-to-depr-board.yml │ ├── pypi-publish.yml │ ├── add-remove-label-on-comment.yml │ ├── upgrade-python-requirements.yml │ └── ci.yml └── pull_request_template.md ├── pylintrc_tweaks ├── openedx.yaml ├── manage.py ├── .gitignore ├── tox.ini ├── catalog-info.yaml ├── Makefile ├── Native_API.md ├── setup.py └── pylintrc /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/workbench.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /drag_and_drop_v2/translations: -------------------------------------------------------------------------------- 1 | conf/locale/ -------------------------------------------------------------------------------- /tests/unit/data/old/settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /drag_and_drop_v2/conf/locale/__init__.py: -------------------------------------------------------------------------------- 1 | """ Drag and Drop v2 XBlock Translated PO and MO files""" 2 | -------------------------------------------------------------------------------- /doc/img/edit-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/xblock-drag-and-drop-v2/HEAD/doc/img/edit-view.png -------------------------------------------------------------------------------- /doc/img/edit-view-items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/xblock-drag-and-drop-v2/HEAD/doc/img/edit-view-items.png -------------------------------------------------------------------------------- /doc/img/edit-view-zones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/xblock-drag-and-drop-v2/HEAD/doc/img/edit-view-zones.png -------------------------------------------------------------------------------- /doc/img/student-view-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/xblock-drag-and-drop-v2/HEAD/doc/img/student-view-start.png -------------------------------------------------------------------------------- /doc/img/student-view-finish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/xblock-drag-and-drop-v2/HEAD/doc/img/student-view-finish.png -------------------------------------------------------------------------------- /requirements/pip.in: -------------------------------------------------------------------------------- 1 | # Core dependencies for installing other packages 2 | -c constraints.txt 3 | 4 | pip 5 | setuptools 6 | wheel 7 | -------------------------------------------------------------------------------- /drag_and_drop_v2/__init__.py: -------------------------------------------------------------------------------- 1 | """ Drag and Drop v2 XBlock """ 2 | from .drag_and_drop_v2 import DragAndDropBlock 3 | 4 | __version__ = "5.0.3" 5 | -------------------------------------------------------------------------------- /drag_and_drop_v2/public/img/triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/xblock-drag-and-drop-v2/HEAD/drag_and_drop_v2/public/img/triangle.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include Changelog.md 2 | include LICENSE 3 | include README.md 4 | include requirements/base.in 5 | include requirements/constraints.txt 6 | -------------------------------------------------------------------------------- /requirements/ci.in: -------------------------------------------------------------------------------- 1 | # Requirements for running tests in CI 2 | -c constraints.txt 3 | 4 | tox # Virtualenv management for tests 5 | -------------------------------------------------------------------------------- /requirements/base.in: -------------------------------------------------------------------------------- 1 | # Core requirements for using this application 2 | -c constraints.txt 3 | 4 | django-statici18n 5 | bleach[css] 6 | XBlock[django] 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Adding new check for github-actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /requirements/pip-tools.in: -------------------------------------------------------------------------------- 1 | # Just the dependencies to run pip-tools, mainly for the "upgrade" make target 2 | 3 | -c constraints.txt 4 | 5 | pip-tools # Contains pip-compile, used to generate pip requirements files 6 | -------------------------------------------------------------------------------- /drag_and_drop_v2/templates/html/drag_and_drop.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
3 | 4 | {% trans "Loading drag and drop problem." %} 5 |
6 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | # Run commitlint on the commit messages in a pull request. 2 | 3 | name: Lint Commit Messages 4 | 5 | on: 6 | - pull_request 7 | 8 | jobs: 9 | commitlint: 10 | uses: openedx/.github/.github/workflows/commitlint.yml@master 11 | -------------------------------------------------------------------------------- /pylintrc_tweaks: -------------------------------------------------------------------------------- 1 | # pylintrc tweaks for use with edx_lint. 2 | [MASTER] 3 | ignore = migrations 4 | load-plugins = edx_lint.pylint,pylint_django,pylint_celery 5 | 6 | [MESSAGES CONTROL] 7 | disable+= 8 | django-not-configured, 9 | unused-argument, 10 | unsubscriptable-object 11 | -------------------------------------------------------------------------------- /requirements/quality.in: -------------------------------------------------------------------------------- 1 | # Requirements for code quality checks 2 | -c constraints.txt 3 | 4 | -r test.txt # Core and testing dependencies for this package 5 | 6 | edx-lint # edX pylint rules and plugins 7 | pycodestyle # PEP 8 compliance validation 8 | -------------------------------------------------------------------------------- /openedx.yaml: -------------------------------------------------------------------------------- 1 | # This file describes this Open edX repo, as described in OEP-2: 2 | # http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification 3 | 4 | tags: 5 | - xblock-drag-and-drop-v2 6 | - library 7 | oeps: 8 | oep-2: false 9 | oep-7: true 10 | oep-18: true 11 | -------------------------------------------------------------------------------- /requirements/dev.in: -------------------------------------------------------------------------------- 1 | # Additional requirements for development of this application 2 | -c constraints.txt 3 | 4 | -r pip-tools.txt # pip-tools and its dependencies, for managing requirements files 5 | -r quality.txt # Core and quality check dependencies 6 | -r ci.txt # dependencies for setting up testing in CI 7 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from django.core.management import execute_from_command_line 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault( 8 | "DJANGO_SETTINGS_MODULE", 9 | "drag_and_drop_v2.conf.locale.settings" 10 | ) 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /requirements/pip.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | wheel==0.45.1 8 | # via -r requirements/pip.in 9 | 10 | # The following packages are considered to be unsafe in a requirements file: 11 | pip==25.3 12 | # via -r requirements/pip.in 13 | setuptools==80.9.0 14 | # via -r requirements/pip.in 15 | -------------------------------------------------------------------------------- /drag_and_drop_v2/conf/locale/config.yaml: -------------------------------------------------------------------------------- 1 | # Configuration for i18n workflow. 2 | 3 | locales: 4 | - en # English - Source Language 5 | 6 | # The locales used for fake-accented English, for testing. 7 | dummy_locales: 8 | - eo 9 | - rtl # Fake testing language for Arabic 10 | 11 | # Directories we don't search for strings. 12 | ignore_dirs: 13 | - '*/css' 14 | - 'public/js/translations' 15 | - 'public/js/vendor' 16 | -------------------------------------------------------------------------------- /.github/workflows/self-assign-issue.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs when a comment is made on the ticket 2 | # If the comment starts with "assign me" it assigns the author to the 3 | # ticket (case insensitive) 4 | 5 | name: Assign comment author to ticket if they say "assign me" 6 | on: 7 | issue_comment: 8 | types: [created] 9 | 10 | jobs: 11 | self_assign_by_comment: 12 | uses: openedx/.github/.github/workflows/self-assign-issue.yml@master 13 | -------------------------------------------------------------------------------- /tests/unit/data/html/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "display_name": "DnDv2 XBlock with HTML instructions", 3 | "max_attempts": 0, 4 | "show_title": false, 5 | "question_text": "Solve this drag-and-drop problem.", 6 | "show_question_header": false, 7 | "weight": 1, 8 | "item_background_color": "white", 9 | "item_text_color": "#000080", 10 | "url_name": "unique_name", 11 | "answer_available": false 12 | } 13 | -------------------------------------------------------------------------------- /tests/unit/data/plain/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "display_name": "DnDv2 XBlock with plain text instructions", 3 | "max_attempts": 0, 4 | "show_title": true, 5 | "question_text": "Can you solve this drag-and-drop problem?", 6 | "show_question_header": true, 7 | "weight": 1, 8 | "item_background_color": "", 9 | "item_text_color": "", 10 | "url_name": "test", 11 | "max_items_per_zone": 4, 12 | "answer_available": false 13 | } 14 | -------------------------------------------------------------------------------- /tests/unit/data/assessment/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "display_name": "DnDv2 XBlock with plain text instructions", 3 | "mode": "assessment", 4 | "max_attempts": 10, 5 | "show_title": true, 6 | "question_text": "Can you solve this drag-and-drop problem?", 7 | "show_question_header": true, 8 | "weight": 5, 9 | "item_background_color": "", 10 | "item_text_color": "", 11 | "url_name": "test", 12 | "answer_available": false 13 | } 14 | -------------------------------------------------------------------------------- /requirements/test.in: -------------------------------------------------------------------------------- 1 | # Requirements for test runs. 2 | -c constraints.txt 3 | 4 | -r base.txt # Core dependencies for this package 5 | 6 | pytest-cov # pytest extension for code coverage statistics 7 | pytest-django # pytest extension for better Django support 8 | ddt # data-driven tests 9 | 10 | mock # required by the workbench 11 | openedx-django-pyfs # required by the workbench 12 | 13 | edx-i18n-tools # For i18n_tool dummy 14 | 15 | xblock-sdk>0.7 # workbench 16 | -------------------------------------------------------------------------------- /requirements/pip-tools.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | build==1.3.0 8 | # via pip-tools 9 | click==8.3.1 10 | # via pip-tools 11 | packaging==25.0 12 | # via build 13 | pip-tools==7.5.2 14 | # via -r requirements/pip-tools.in 15 | pyproject-hooks==1.2.0 16 | # via 17 | # build 18 | # pip-tools 19 | wheel==0.45.1 20 | # via pip-tools 21 | 22 | # The following packages are considered to be unsafe in a requirements file: 23 | # pip 24 | # setuptools 25 | -------------------------------------------------------------------------------- /requirements/constraints.txt: -------------------------------------------------------------------------------- 1 | # Version constraints for pip-installation. 2 | # 3 | # This file doesn't install any packages. It specifies version constraints 4 | # that will be applied if a package is needed. 5 | # 6 | # When pinning something here, please provide an explanation of why. Ideally, 7 | # link to other information that will help people in the future to remove the 8 | # pin when possible. Writing an issue against the offending project and 9 | # linking to it here is good. 10 | 11 | # Common constraints for edx repos 12 | -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt 13 | -------------------------------------------------------------------------------- /requirements/ci.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | cachetools==6.2.2 8 | # via tox 9 | chardet==5.2.0 10 | # via tox 11 | colorama==0.4.6 12 | # via tox 13 | distlib==0.4.0 14 | # via virtualenv 15 | filelock==3.20.0 16 | # via 17 | # tox 18 | # virtualenv 19 | packaging==25.0 20 | # via 21 | # pyproject-api 22 | # tox 23 | platformdirs==4.5.1 24 | # via 25 | # tox 26 | # virtualenv 27 | pluggy==1.6.0 28 | # via tox 29 | pyproject-api==1.10.0 30 | # via tox 31 | tox==4.32.0 32 | # via -r requirements/ci.in 33 | virtualenv==20.35.4 34 | # via tox 35 | -------------------------------------------------------------------------------- /.github/workflows/add-depr-ticket-to-depr-board.yml: -------------------------------------------------------------------------------- 1 | # Run the workflow that adds new tickets that are either: 2 | # - labelled "DEPR" 3 | # - title starts with "[DEPR]" 4 | # - body starts with "Proposal Date" (this is the first template field) 5 | # to the org-wide DEPR project board 6 | 7 | name: Add newly created DEPR issues to the DEPR project board 8 | 9 | on: 10 | issues: 11 | types: [opened] 12 | 13 | jobs: 14 | routeissue: 15 | uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master 16 | secrets: 17 | GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }} 18 | GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }} 19 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }} 20 | -------------------------------------------------------------------------------- /requirements/private.readme: -------------------------------------------------------------------------------- 1 | # If there are any Python packages you want to keep in your virtualenv beyond 2 | # those listed in the official requirements files, create a "private.in" file 3 | # and list them there. Generate the corresponding "private.txt" file pinning 4 | # all of their indirect dependencies to specific versions as follows: 5 | 6 | # pip-compile private.in 7 | 8 | # This allows you to use "pip-sync" without removing these packages: 9 | 10 | # pip-sync requirements/*.txt 11 | 12 | # "private.in" and "private.txt" aren't checked into git to avoid merge 13 | # conflicts, and the presence of this file allows "private.*" to be 14 | # included in scripted pip-sync usage without requiring that those files be 15 | # created first. 16 | -------------------------------------------------------------------------------- /.github/workflows/pypi-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | push: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v6 14 | 15 | - name: setup python 16 | uses: actions/setup-python@v6 17 | with: 18 | python-version: 3.11 19 | 20 | - name: Install Dependencies 21 | run: pip install -r requirements/pip.txt 22 | 23 | - name: Build package 24 | run: python setup.py sdist bdist_wheel 25 | 26 | - name: Publish to PyPi 27 | uses: pypa/gh-action-pypi-publish@release/v1 28 | with: 29 | user: __token__ 30 | password: ${{ secrets.PYPI_UPLOAD_TOKEN }} 31 | -------------------------------------------------------------------------------- /tests/pylintrc: -------------------------------------------------------------------------------- 1 | [REPORTS] 2 | reports=no 3 | 4 | [FORMAT] 5 | max-line-length=120 6 | 7 | [MESSAGES CONTROL] 8 | disable= 9 | attribute-defined-outside-init, 10 | locally-disabled, 11 | missing-docstring, 12 | abstract-class-little-used, 13 | too-many-ancestors, 14 | too-few-public-methods, 15 | too-many-public-methods, 16 | invalid-name, 17 | no-member, 18 | star-args, 19 | no-else-return, 20 | useless-object-inheritance, 21 | unsubscriptable-object, 22 | bad-option-value, 23 | len-as-condition, 24 | useless-super-delegation, 25 | bad-option-value, 26 | missing-docstring, 27 | no-member, 28 | wrong-import-order, 29 | line-too-long 30 | 31 | [SIMILARITIES] 32 | min-similarity-lines=6 33 | 34 | [OPTIONS] 35 | max-args=6 36 | -------------------------------------------------------------------------------- /.github/workflows/add-remove-label-on-comment.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs when a comment is made on the ticket 2 | # If the comment starts with "label: " it tries to apply 3 | # the label indicated in rest of comment. 4 | # If the comment starts with "remove label: ", it tries 5 | # to remove the indicated label. 6 | # Note: Labels are allowed to have spaces and this script does 7 | # not parse spaces (as often a space is legitimate), so the command 8 | # "label: really long lots of words label" will apply the 9 | # label "really long lots of words label" 10 | 11 | name: Allows for the adding and removing of labels via comment 12 | 13 | on: 14 | issue_comment: 15 | types: [created] 16 | 17 | jobs: 18 | add_remove_labels: 19 | uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | venv/ 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | .noseids 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Translations 45 | *.pot 46 | 47 | # test output: 48 | /*.log 49 | /tests.*.png 50 | var/* 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # PyBuilder 56 | target/ 57 | 58 | # IDEs 59 | .idea 60 | .idea/* 61 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-python-requirements.yml: -------------------------------------------------------------------------------- 1 | name: Upgrade Python Requirements 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * 1" 6 | workflow_dispatch: 7 | inputs: 8 | branch: 9 | description: Target branch against which to create requirements PR 10 | required: true 11 | default: master 12 | 13 | jobs: 14 | call-upgrade-python-requirements-workflow: 15 | uses: openedx/.github/.github/workflows/upgrade-python-requirements.yml@master 16 | # Do not run on forks 17 | if: github.repository_owner == 'openedx' 18 | with: 19 | branch: ${{ github.event.inputs.branch || 'master' }} 20 | # optional parameters below; fill in if you'd like github or email notifications 21 | # user_reviewers: "" 22 | # team_reviewers: "" 23 | # email_address: "" 24 | # send_success_notification: false 25 | secrets: 26 | requirements_bot_github_token: ${{ secrets.REQUIREMENTS_BOT_GITHUB_TOKEN }} 27 | requirements_bot_github_email: ${{ secrets.REQUIREMENTS_BOT_GITHUB_EMAIL }} 28 | edx_smtp_username: ${{ secrets.EDX_SMTP_USERNAME }} 29 | edx_smtp_password: ${{ secrets.EDX_SMTP_PASSWORD }} 30 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{311,312}-django{42,52},quality,translations 3 | 4 | [pycodestyle] 5 | exclude = .git,.tox 6 | 7 | [pytest] 8 | # Use the workbench settings file. 9 | DJANGO_SETTINGS_MODULE = workbench.settings 10 | addopts = --cov-report term-missing --cov-report xml 11 | filterwarnings = 12 | ignore::DeprecationWarning 13 | ignore::FutureWarning 14 | 15 | [coverage:run] 16 | omit = drag_and_drop_v2/translations/settings.py 17 | 18 | [testenv] 19 | allowlist_externals = 20 | make 21 | mkdir 22 | deps = 23 | django42: Django>=4.2,<4.3 24 | django52: Django>=5.2,<5.3 25 | -r{toxinidir}/requirements/test.txt 26 | commands = 27 | mkdir -p var 28 | pytest {posargs:tests/unit/ --cov drag_and_drop_v2} 29 | 30 | [testenv:quality] 31 | deps = 32 | -r{toxinidir}/requirements/quality.txt 33 | commands = 34 | pycodestyle drag_and_drop_v2 tests manage.py setup.py --max-line-length=120 35 | pylint drag_and_drop_v2 36 | pylint tests --rcfile=tests/pylintrc 37 | 38 | [testenv:translations] 39 | allowlist_externals = 40 | make 41 | deps = 42 | Django>=4.2,<4.3 43 | -r{toxinidir}/requirements/test.txt 44 | commands = 45 | make check_translations_up_to_date 46 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: 8 | - '**' 9 | workflow_dispatch: 10 | 11 | concurrency: 12 | group: "${{ github.workflow }}-${{ github.ref }}" 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | tests: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: [ubuntu-latest] 22 | python-version: [3.11, 3.12] 23 | toxenv: [django42, django52, quality, translations] 24 | 25 | steps: 26 | - name: checkout repo 27 | uses: actions/checkout@v6 28 | with: 29 | submodules: recursive 30 | 31 | - name: setup python 32 | uses: actions/setup-python@v6 33 | with: 34 | python-version: ${{ matrix.python-version }} 35 | 36 | - name: Install translations dependencies 37 | if: ${{ startsWith(matrix.toxenv, 'translations') }} 38 | run: | 39 | sudo apt-get update 40 | sudo apt-get install -y gettext 41 | 42 | - name: Install Dependencies 43 | run: make requirements 44 | 45 | - name: Run Tests 46 | env: 47 | TOXENV: ${{ matrix.toxenv }} 48 | run: tox 49 | -------------------------------------------------------------------------------- /drag_and_drop_v2/compat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Compatibility layer to isolate core-platform waffle flags from implementation. 3 | """ 4 | 5 | # Waffle flags configuration 6 | 7 | # Namespace 8 | WAFFLE_NAMESPACE = "drag_and_drop_v2" 9 | 10 | # Course Waffle Flags 11 | # .. toggle_name: drag_and_drop_v2.grading_ignore_decoys 12 | # .. toggle_implementation: CourseWaffleFlag 13 | # .. toggle_default: False 14 | # .. toggle_description: Enables alternative grading for the xblock 15 | # that does not include decoy items in the score. 16 | # .. toggle_use_cases: open_edx 17 | # .. toggle_creation_date: 2022-11-10 18 | GRADING_IGNORE_DECOYS = 'grading_ignore_decoys' 19 | 20 | 21 | def get_grading_ignore_decoys_waffle_flag(): 22 | """ 23 | Import and return Waffle flag for enabling alternative grading for drag_and_drop_v2 Xblock. 24 | """ 25 | # pylint: disable=import-error,import-outside-toplevel 26 | from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag 27 | try: 28 | # HACK: The base class of the `CourseWaffleFlag` was changed in Olive. 29 | # Ref: https://github.com/openedx/public-engineering/issues/28 30 | return CourseWaffleFlag(WAFFLE_NAMESPACE, GRADING_IGNORE_DECOYS, __name__) 31 | except ValueError: 32 | # pylint: disable=toggle-missing-annotation 33 | return CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.{GRADING_IGNORE_DECOYS}', __name__) 34 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | # This file records information about this repo. Its use is described in OEP-55: 2 | # https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html 3 | 4 | apiVersion: backstage.io/v1alpha1 5 | # (Required) Acceptable Values: Component, Resource, System 6 | # A repo will almost certainly be a Component. 7 | kind: Component 8 | metadata: 9 | name: 'xblock-drag-and-drop-v2' 10 | description: "An XBlock implementing a friendly drag-and-drop style problem" 11 | annotations: 12 | # (Optional) Annotation keys and values can be whatever you want. 13 | # We use it in Open edX repos to have a comma-separated list of GitHub user 14 | # names that might be interested in changes to the architecture of this 15 | # component. 16 | openedx.org/component-type: "XBlock" 17 | openedx.org/arch-interest-groups: "feanil, e0d" 18 | spec: 19 | 20 | # (Required) This can be a group (`group:`) or a user (`user:`). 21 | # Don't forget the "user:" or "group:" prefix. Groups must be GitHub team 22 | # names in the openedx GitHub organization: https://github.com/orgs/openedx/teams 23 | 24 | owner: 'user:Agrendalath' 25 | 26 | # (Required) Acceptable Type Values: service, website, library 27 | type: 'library' 28 | 29 | # (Required) Acceptable Lifecycle Values: experimental, production, deprecated 30 | lifecycle: 'production' 31 | -------------------------------------------------------------------------------- /tests/unit/data/html/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "zones": [ 3 | { 4 | "index": 1, 5 | "width": 200, 6 | "title": "Zone 1", 7 | "height": 100, 8 | "y": 200, 9 | "x": 100, 10 | "id": "zone-1", 11 | "align": "right" 12 | }, 13 | { 14 | "index": 2, 15 | "width": 200, 16 | "title": "Zone 2", 17 | "height": 100, 18 | "y": 0, 19 | "x": 0, 20 | "id": "zone-2", 21 | "align": "center" 22 | } 23 | ], 24 | 25 | "items": [ 26 | { 27 | "displayName": "1", 28 | "feedback": { 29 | "incorrect": "No 1", 30 | "correct": "Yes 1" 31 | }, 32 | "zones": ["Zone 1"], 33 | "imageURL": "", 34 | "id": 0 35 | }, 36 | { 37 | "displayName": "2", 38 | "feedback": { 39 | "incorrect": "No 2", 40 | "correct": "Yes 2" 41 | }, 42 | "zones": [ 43 | "Zone 2", 44 | "Zone 1" 45 | ], 46 | "imageURL": "", 47 | "id": 1 48 | }, 49 | { 50 | "displayName": "X", 51 | "feedback": { 52 | "incorrect": "", 53 | "correct": "" 54 | }, 55 | "zones": [], 56 | "imageURL": "", 57 | "id": 2 58 | }, 59 | { 60 | "displayName": "", 61 | "feedback": { 62 | "incorrect": "", 63 | "correct": "" 64 | }, 65 | "zones": [], 66 | "imageURL": "http://placehold.it/100x300", 67 | "id": 3 68 | } 69 | ], 70 | "feedback": { 71 | "start": "HTML Intro Feed", 72 | "finish": "Final feedback!" 73 | }, 74 | "targetImgDescription": "This describes the target image" 75 | } 76 | -------------------------------------------------------------------------------- /tests/unit/data/plain/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "zones": [ 3 | { 4 | "title": "Zone 1", 5 | "y": 123, 6 | "x": 234, 7 | "width": 345, 8 | "height": 456, 9 | "uid": "zone-1", 10 | "align": "left" 11 | }, 12 | { 13 | "title": "Zone 2", 14 | "y": 20, 15 | "x": 10, 16 | "width": 30, 17 | "height": 40, 18 | "uid": "zone-2", 19 | "align": "center" 20 | } 21 | ], 22 | 23 | 24 | "items": [ 25 | { 26 | "displayName": "1", 27 | "feedback": { 28 | "incorrect": "No 1", 29 | "correct": "Yes 1" 30 | }, 31 | "zones": ["zone-1"], 32 | "imageURL": "", 33 | "id": 0 34 | }, 35 | { 36 | "displayName": "2", 37 | "feedback": { 38 | "incorrect": "No 2", 39 | "correct": "Yes 2" 40 | }, 41 | "zones": [ 42 | "zone-2", 43 | "zone-1" 44 | ], 45 | "imageURL": "", 46 | "id": 1 47 | }, 48 | { 49 | "displayName": "X", 50 | "feedback": { 51 | "incorrect": "", 52 | "correct": "" 53 | }, 54 | "zones": [], 55 | "imageURL": "/static/test_url_expansion", 56 | "id": 2 57 | }, 58 | { 59 | "displayName": "", 60 | "feedback": { 61 | "incorrect": "", 62 | "correct": "" 63 | }, 64 | "zones": [], 65 | "imageURL": "http://placehold.it/200x100", 66 | "id": 3 67 | } 68 | ], 69 | 70 | 71 | "feedback": { 72 | "start": "This is the initial feedback.", 73 | "finish": "This is the final feedback." 74 | }, 75 | 76 | 77 | "targetImg": "http://placehold.it/800x600", 78 | "targetImgDescription": "This describes the target image", 79 | "displayLabels": false 80 | } 81 | -------------------------------------------------------------------------------- /tests/unit/data/assessment/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "zones": [ 3 | { 4 | "title": "Zone 1", 5 | "y": 123, 6 | "x": 234, 7 | "width": 345, 8 | "height": 456, 9 | "uid": "zone-1", 10 | "align": "right" 11 | }, 12 | { 13 | "title": "Zone 2", 14 | "y": 20, 15 | "x": 10, 16 | "width": 30, 17 | "height": 40, 18 | "uid": "zone-2" 19 | } 20 | ], 21 | 22 | 23 | "items": [ 24 | { 25 | "displayName": "1", 26 | "feedback": { 27 | "incorrect": "No 1", 28 | "correct": "Yes 1" 29 | }, 30 | "zone": "zone-1", 31 | "imageURL": "", 32 | "id": 0 33 | }, 34 | { 35 | "displayName": "2", 36 | "feedback": { 37 | "incorrect": "No 2", 38 | "correct": "Yes 2" 39 | }, 40 | "zone": "zone-2", 41 | "imageURL": "", 42 | "id": 1 43 | }, 44 | { 45 | "displayName": "3", 46 | "feedback": { 47 | "incorrect": "No 3", 48 | "correct": "Yes 3" 49 | }, 50 | "zone": "zone-2", 51 | "imageURL": "", 52 | "id": 2 53 | }, 54 | { 55 | "displayName": "X", 56 | "feedback": { 57 | "incorrect": "", 58 | "correct": "" 59 | }, 60 | "zone": "none", 61 | "imageURL": "/static/test_url_expansion", 62 | "id": 3 63 | }, 64 | { 65 | "displayName": "", 66 | "feedback": { 67 | "incorrect": "", 68 | "correct": "" 69 | }, 70 | "zone": "none", 71 | "imageURL": "http://placehold.it/200x100", 72 | "id": 4 73 | } 74 | ], 75 | 76 | 77 | "feedback": { 78 | "start": "This is the initial feedback.", 79 | "finish": "This is the final feedback." 80 | }, 81 | 82 | 83 | "targetImg": "http://placehold.it/800x600", 84 | "targetImgDescription": "This describes the target image", 85 | "displayLabels": false 86 | } 87 | -------------------------------------------------------------------------------- /tests/unit/test_fixtures.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import json 4 | 5 | from xblock.utils.resources import ResourceLoader 6 | 7 | from tests.utils import TestCaseMixin, make_block 8 | 9 | loader = ResourceLoader(__name__) 10 | 11 | 12 | class BaseDragAndDropAjaxFixture(TestCaseMixin): 13 | ZONE_1 = None 14 | ZONE_2 = None 15 | 16 | OVERALL_FEEDBACK_KEY = 'overall_feedback' 17 | FEEDBACK_KEY = 'feedback' 18 | 19 | FEEDBACK = { 20 | 0: {"correct": None, "incorrect": None}, 21 | 1: {"correct": None, "incorrect": None}, 22 | 2: {"correct": None, "incorrect": None}, 23 | } 24 | 25 | START_FEEDBACK = None 26 | FINAL_FEEDBACK = None 27 | 28 | FOLDER = None 29 | 30 | def setUp(self): 31 | self.patch_workbench() 32 | self.block = make_block() 33 | initial_settings = self.initial_settings() 34 | for field in initial_settings: 35 | setattr(self.block, field, initial_settings[field]) 36 | self.block.data = self.initial_data() 37 | 38 | @staticmethod 39 | def _make_feedback_message(message=None, message_class=None): 40 | return {"message": message, "message_class": message_class} 41 | 42 | @classmethod 43 | def initial_data(cls): 44 | return json.loads(loader.load_unicode(f'data/{cls.FOLDER}/data.json')) 45 | 46 | @classmethod 47 | def initial_settings(cls): 48 | return json.loads(loader.load_unicode(f'data/{cls.FOLDER}/settings.json')) 49 | 50 | @classmethod 51 | def expected_student_data(cls): 52 | return json.loads(loader.load_unicode(f'data/{cls.FOLDER}/config_out.json')) 53 | 54 | def test_student_view_data(self): 55 | data = self.block.student_view_data() 56 | expected = self.expected_student_data() 57 | expected['block_id'] = data['block_id'] # Block ids aren't stable 58 | self.assertEqual(data, expected) 59 | -------------------------------------------------------------------------------- /drag_and_drop_v2/public/themes/apros.css: -------------------------------------------------------------------------------- 1 | .themed-xblock.xblock--drag-and-drop { 2 | background-color: #fff; 3 | } 4 | 5 | /* Shared styles used in header and footer */ 6 | 7 | .themed-xblock.xblock--drag-and-drop .title1 { 8 | color: #555555; 9 | text-transform: uppercase; 10 | font-weight: bold; 11 | font-style: normal; 12 | } 13 | 14 | /* drag-container holds the .item-bank and the .target */ 15 | 16 | .themed-xblock.xblock--drag-and-drop .drag-container { 17 | background-color: #ebf0f2; 18 | } 19 | 20 | .themed-xblock.xblock--drag-and-drop .item-bank { 21 | border-radius: 0px; 22 | } 23 | 24 | /*** DRAGGABLE ITEMS ***/ 25 | 26 | .themed-xblock.xblock--drag-and-drop .drag-container .option { 27 | border-radius: 0px; 28 | text-align: initial; 29 | font-size: 14px; 30 | background-color: #2e83cd; 31 | color: #fff; 32 | opacity: 1; 33 | } 34 | 35 | .themed-xblock.xblock--drag-and-drop .drag-container .option.fade { 36 | opacity: 0.5; 37 | } 38 | 39 | /*** DROP TARGET ***/ 40 | 41 | .themed-xblock.xblock--drag-and-drop .target { 42 | background-color: #fff; 43 | } 44 | 45 | .themed-xblock.xblock--drag-and-drop .drag-container .target .zone p { 46 | font-family: Arial; 47 | font-size: 16px; 48 | font-weight: bold; 49 | text-align: center; 50 | text-transform: uppercase; 51 | } 52 | 53 | /*** FEEDBACK ***/ 54 | 55 | .themed-xblock.xblock--drag-and-drop .feedback { 56 | border-top: solid 1px #bdbdbd; 57 | } 58 | 59 | .themed-xblock.xblock--drag-and-drop .popup { 60 | background-color: #66a5b5; 61 | } 62 | 63 | .themed-xblock.xblock--drag-and-drop .popup .popup-content { 64 | color: #ffffff; 65 | font-size: 14px; 66 | } 67 | 68 | .themed-xblock.xblock--drag-and-drop .popup .close { 69 | cursor: pointer; 70 | color: #ffffff; 71 | font-family: "fontawesome"; 72 | font-size: 18pt; 73 | } 74 | 75 | .themed-xblock.xblock--drag-and-drop .link-button { 76 | cursor: pointer; 77 | color: #3384ca; 78 | } 79 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | appdirs==1.4.4 8 | # via fs 9 | asgiref==3.11.0 10 | # via django 11 | bleach[css]==6.3.0 12 | # via -r requirements/base.in 13 | boto3==1.42.4 14 | # via fs-s3fs 15 | botocore==1.42.4 16 | # via 17 | # boto3 18 | # s3transfer 19 | django==5.2.9 20 | # via 21 | # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt 22 | # django-appconf 23 | # django-statici18n 24 | # openedx-django-pyfs 25 | django-appconf==1.2.0 26 | # via django-statici18n 27 | django-statici18n==2.6.0 28 | # via -r requirements/base.in 29 | fs==2.4.16 30 | # via 31 | # fs-s3fs 32 | # openedx-django-pyfs 33 | # xblock 34 | fs-s3fs==1.1.1 35 | # via openedx-django-pyfs 36 | jmespath==1.0.1 37 | # via 38 | # boto3 39 | # botocore 40 | lazy==1.6 41 | # via xblock 42 | lxml==6.0.2 43 | # via xblock 44 | mako==1.3.10 45 | # via xblock 46 | markupsafe==3.0.3 47 | # via 48 | # mako 49 | # xblock 50 | openedx-django-pyfs==3.8.0 51 | # via xblock 52 | python-dateutil==2.9.0.post0 53 | # via 54 | # botocore 55 | # xblock 56 | pytz==2025.2 57 | # via xblock 58 | pyyaml==6.0.3 59 | # via xblock 60 | s3transfer==0.16.0 61 | # via boto3 62 | simplejson==3.20.2 63 | # via xblock 64 | six==1.17.0 65 | # via 66 | # fs 67 | # fs-s3fs 68 | # python-dateutil 69 | sqlparse==0.5.4 70 | # via django 71 | tinycss2==1.4.0 72 | # via bleach 73 | urllib3==2.6.0 74 | # via botocore 75 | web-fragments==3.1.0 76 | # via xblock 77 | webencodings==0.5.1 78 | # via 79 | # bleach 80 | # tinycss2 81 | webob==1.8.9 82 | # via xblock 83 | xblock[django]==5.2.0 84 | # via -r requirements/base.in 85 | 86 | # The following packages are considered to be unsafe in a requirements file: 87 | # setuptools 88 | -------------------------------------------------------------------------------- /tests/unit/data/old/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "zones": [ 3 | { 4 | "index": 1, 5 | "width": 200, 6 | "title": "Zone 1", 7 | "height": 100, 8 | "y": "200", 9 | "x": "100", 10 | "id": "zone-1" 11 | }, 12 | { 13 | "index": 2, 14 | "width": 200, 15 | "title": "Zone 2", 16 | "height": 100, 17 | "y": 0, 18 | "x": 0, 19 | "id": "zone-2" 20 | } 21 | ], 22 | "items": [ 23 | { 24 | "displayName": "1", 25 | "feedback": { 26 | "incorrect": "No 1", 27 | "correct": "Yes 1" 28 | }, 29 | "zone": "Zone 1", 30 | "imageURL": "", 31 | "id": 0, 32 | "size": { 33 | "width": "190px", 34 | "height": "auto" 35 | } 36 | }, 37 | { 38 | "displayName": "2", 39 | "feedback": { 40 | "incorrect": "No 2", 41 | "correct": "Yes 2" 42 | }, 43 | "zone": "Zone 2", 44 | "imageURL": "", 45 | "id": 1, 46 | "size": { 47 | "width": "190px", 48 | "height": "auto" 49 | } 50 | }, 51 | { 52 | "displayName": "X", 53 | "feedback": { 54 | "incorrect": "", 55 | "correct": "" 56 | }, 57 | "zone": "none", 58 | "imageURL": "", 59 | "id": 2, 60 | "size": { 61 | "width": "100px", 62 | "height": "100px" 63 | } 64 | }, 65 | { 66 | "displayName": "", 67 | "feedback": { 68 | "incorrect": "", 69 | "correct": "" 70 | }, 71 | "zone": "none", 72 | "imageURL": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg", 73 | "id": 3, 74 | "size": { 75 | "width": "190px", 76 | "height": "auto" 77 | } 78 | } 79 | ], 80 | "state": { 81 | "items": {}, 82 | "finished": true 83 | }, 84 | "feedback": { 85 | "start": "Intro Feed", 86 | "finish": "Final Feed" 87 | }, 88 | "targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png", 89 | "targetImgDescription": "This describes the target image" 90 | } 91 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | ## Description 13 | 14 | Describe what this pull request changes, and why. Include implications for people using this change. 15 | Design decisions and their rationales should be documented in the repo (docstring / ADR), per 16 | [OEP-19](https://open-edx-proposals.readthedocs.io/en/latest/oep-0019-bp-developer-documentation.html), and can be 17 | linked here. 18 | 19 | Useful information to include: 20 | - Which edX user roles will this change impact? Common user roles are "Learner", "Course Author", 21 | "Developer", and "Operator". 22 | - Include screenshots for changes to the UI (ideally, both "before" and "after" screenshots, if applicable). 23 | - Provide links to the description of corresponding configuration changes. Remember to correctly annotate these 24 | changes. 25 | 26 | ## Supporting information 27 | 28 | Link to other information about the change, such as Jira issues, GitHub issues, or Discourse discussions. 29 | Be sure to check they are publicly readable, or if not, repeat the information here. 30 | 31 | ## Testing instructions 32 | 33 | Please provide detailed step-by-step instructions for testing this change. 34 | 35 | ## Deadline 36 | 37 | "None" if there's no rush, or provide a specific date or event (and reason) if there is one. 38 | 39 | ## Other information 40 | 41 | Include anything else that will help reviewers and consumers understand the change. 42 | - Does this change depend on other changes elsewhere? 43 | - Any special concerns or limitations? For example: deprecations, migrations, security, or accessibility. 44 | - If your [database migration](https://openedx.atlassian.net/wiki/spaces/AC/pages/23003228/Everything+About+Database+Migrations) can't be rolled back easily. 45 | -------------------------------------------------------------------------------- /tests/unit/data/plain/config_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "block_id": "test-show-title-parameter.drag_and_drop_v2.d183.u0", 3 | "display_name": "DnDv2 XBlock with plain text instructions", 4 | "type": "drag-and-drop-v2", 5 | "weight": 1, 6 | "title": "DnDv2 XBlock with plain text instructions", 7 | "mode": "standard", 8 | "max_attempts": 0, 9 | "graded": false, 10 | "weighted_max_score": 1, 11 | "show_title": true, 12 | "answer_available": false, 13 | "problem_text": "Can you solve this drag-and-drop problem?", 14 | "show_problem_header": true, 15 | "target_img_expanded_url": "http://placehold.it/800x600", 16 | "target_img_description": "This describes the target image", 17 | "item_background_color": null, 18 | "item_text_color": null, 19 | "display_zone_borders": false, 20 | "display_zone_borders_dragging": false, 21 | "display_zone_labels": false, 22 | "url_name": "test", 23 | "max_items_per_zone": 4, 24 | "has_deadline_passed": false, 25 | 26 | "zones": [ 27 | { 28 | "title": "Zone 1", 29 | "y": 123, 30 | "x": 234, 31 | "width": 345, 32 | "height": 456, 33 | "uid": "zone-1", 34 | "align": "left" 35 | }, 36 | { 37 | "title": "Zone 2", 38 | "y": 20, 39 | "x": 10, 40 | "width": 30, 41 | "height": 40, 42 | "uid": "zone-2", 43 | "align": "center" 44 | } 45 | ], 46 | 47 | "items": [ 48 | { 49 | "displayName": "1", 50 | "imageURL": "", 51 | "expandedImageURL": "", 52 | "id": 0 53 | }, 54 | { 55 | "displayName": "2", 56 | "imageURL": "", 57 | "expandedImageURL": "", 58 | "id": 1 59 | }, 60 | { 61 | "displayName": "X", 62 | "imageURL": "/static/test_url_expansion", 63 | "expandedImageURL": "/course/test-course/assets/test_url_expansion", 64 | "id": 2 65 | }, 66 | { 67 | "displayName": "", 68 | "imageURL": "http://placehold.it/200x100", 69 | "expandedImageURL": "http://placehold.it/200x100", 70 | "id": 3 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /tests/unit/data/html/config_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "block_id": "test-show-title-parameter.drag_and_drop_v2.d167.u0", 3 | "display_name": "DnDv2 XBlock with HTML instructions", 4 | "type": "drag-and-drop-v2", 5 | "weight": 1, 6 | "title": "DnDv2 XBlock with HTML instructions", 7 | "mode": "standard", 8 | "max_attempts": 0, 9 | "graded": false, 10 | "weighted_max_score": 1, 11 | "show_title": false, 12 | "answer_available": false, 13 | "problem_text": "Solve this drag-and-drop problem.", 14 | "show_problem_header": false, 15 | "target_img_expanded_url": "/expanded/url/to/drag_and_drop_v2/public/img/triangle.png", 16 | "target_img_description": "This describes the target image", 17 | "item_background_color": "white", 18 | "item_text_color": "#000080", 19 | "display_zone_borders": false, 20 | "display_zone_borders_dragging": false, 21 | "display_zone_labels": false, 22 | "url_name": "unique_name", 23 | "max_items_per_zone": null, 24 | "has_deadline_passed": false, 25 | "zones": [ 26 | { 27 | "title": "Zone 1", 28 | "x": 100, 29 | "y": 200, 30 | "width": 200, 31 | "height": 100, 32 | "uid": "Zone 1", 33 | "align": "right" 34 | }, 35 | { 36 | "title": "Zone 2", 37 | "x": 0, 38 | "y": 0, 39 | "width": 200, 40 | "height": 100, 41 | "uid": "Zone 2", 42 | "align": "center" 43 | } 44 | ], 45 | 46 | "items": [ 47 | { 48 | "displayName": "1", 49 | "imageURL": "", 50 | "expandedImageURL": "", 51 | "id": 0 52 | }, 53 | { 54 | "displayName": "2", 55 | "imageURL": "", 56 | "expandedImageURL": "", 57 | "id": 1 58 | }, 59 | { 60 | "displayName": "X", 61 | "imageURL": "", 62 | "expandedImageURL": "", 63 | "id": 2 64 | }, 65 | { 66 | "displayName": "", 67 | "imageURL": "http://placehold.it/100x300", 68 | "expandedImageURL": "http://placehold.it/100x300", 69 | "id": 3 70 | } 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /tests/unit/data/assessment/config_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "block_id": "test-show-title-parameter.drag_and_drop_v2.d151.u0", 3 | "display_name": "DnDv2 XBlock with plain text instructions", 4 | "type": "drag-and-drop-v2", 5 | "weight": 5, 6 | "title": "DnDv2 XBlock with plain text instructions", 7 | "mode": "assessment", 8 | "max_attempts": 10, 9 | "graded": false, 10 | "weighted_max_score": 5, 11 | "show_title": true, 12 | "answer_available": false, 13 | "problem_text": "Can you solve this drag-and-drop problem?", 14 | "show_problem_header": true, 15 | "target_img_expanded_url": "http://placehold.it/800x600", 16 | "target_img_description": "This describes the target image", 17 | "item_background_color": null, 18 | "item_text_color": null, 19 | "display_zone_borders": false, 20 | "display_zone_borders_dragging": false, 21 | "display_zone_labels": false, 22 | "url_name": "test", 23 | "max_items_per_zone": null, 24 | "has_deadline_passed": false, 25 | 26 | "zones": [ 27 | { 28 | "title": "Zone 1", 29 | "y": 123, 30 | "x": 234, 31 | "width": 345, 32 | "height": 456, 33 | "uid": "zone-1", 34 | "align": "right" 35 | }, 36 | { 37 | "title": "Zone 2", 38 | "y": 20, 39 | "x": 10, 40 | "width": 30, 41 | "height": 40, 42 | "uid": "zone-2", 43 | "align": "center" 44 | } 45 | ], 46 | 47 | "items": [ 48 | { 49 | "displayName": "1", 50 | "imageURL": "", 51 | "expandedImageURL": "", 52 | "id": 0 53 | }, 54 | { 55 | "displayName": "2", 56 | "imageURL": "", 57 | "expandedImageURL": "", 58 | "id": 1 59 | }, 60 | { 61 | "displayName": "3", 62 | "imageURL": "", 63 | "expandedImageURL": "", 64 | "id": 2 65 | }, 66 | { 67 | "displayName": "X", 68 | "imageURL": "/static/test_url_expansion", 69 | "expandedImageURL": "/course/test-course/assets/test_url_expansion", 70 | "id": 3 71 | }, 72 | { 73 | "displayName": "", 74 | "imageURL": "http://placehold.it/200x100", 75 | "expandedImageURL": "http://placehold.it/200x100", 76 | "id": 4 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /tests/unit/data/old/config_out.json: -------------------------------------------------------------------------------- 1 | { 2 | "block_id": "test-show-title-parameter.drag_and_drop_v2.d199.u0", 3 | "display_name": "Drag and Drop", 4 | "type": "drag-and-drop-v2", 5 | "weight": 1, 6 | "title": "Drag and Drop", 7 | "mode": "standard", 8 | "max_attempts": null, 9 | "graded": false, 10 | "weighted_max_score": 1, 11 | "show_title": true, 12 | "answer_available": false, 13 | "problem_text": "", 14 | "show_problem_header": true, 15 | "target_img_expanded_url": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png", 16 | "target_img_description": "This describes the target image", 17 | "item_background_color": null, 18 | "item_text_color": null, 19 | "display_zone_borders": false, 20 | "display_zone_borders_dragging": false, 21 | "display_zone_labels": false, 22 | "url_name": "", 23 | "max_items_per_zone": null, 24 | "has_deadline_passed": false, 25 | 26 | "zones": [ 27 | { 28 | "title": "Zone 1", 29 | "x": "100", 30 | "y": "200", 31 | "width": 200, 32 | "height": 100, 33 | "uid": "Zone 1", 34 | "align": "center" 35 | }, 36 | { 37 | "title": "Zone 2", 38 | "x": 0, 39 | "y": 0, 40 | "width": 200, 41 | "height": 100, 42 | "uid": "Zone 2", 43 | "align": "center" 44 | } 45 | ], 46 | 47 | "items": [ 48 | { 49 | "displayName": "1", 50 | "imageURL": "", 51 | "expandedImageURL": "", 52 | "id": 0, 53 | "size": {"height": "auto", "width": "190px"} 54 | }, 55 | { 56 | "displayName": "2", 57 | "imageURL": "", 58 | "expandedImageURL": "", 59 | "id": 1, 60 | "size": {"height": "auto", "width": "190px"} 61 | }, 62 | { 63 | "displayName": "X", 64 | "imageURL": "", 65 | "expandedImageURL": "", 66 | "id": 2, 67 | "size": {"height": "100px", "width": "100px"} 68 | }, 69 | { 70 | "displayName": "", 71 | "imageURL": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg", 72 | "expandedImageURL": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg", 73 | "id": 3, 74 | "size": {"height": "auto", "width": "190px"} 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /drag_and_drop_v2/conf/locale/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for xblock-drag-and-drop-v2 project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.11/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.11/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | from __future__ import absolute_import 13 | import os 14 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 15 | 16 | 17 | # Quick-start development settings - unsuitable for production 18 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 19 | 20 | # SECURITY WARNING: keep the secret key used in production secret! 21 | # This is just a container for running tests, it's okay to allow it to be 22 | # defaulted here if not present in environment settings 23 | SECRET_KEY = os.environ.get('SECRET_KEY', '",cB3Jr.?xu[x_Ci]!%HP>#^AVmWi@r/W3u,w?pY+~J!R>;WN+,3}Sb{K=Jp~;&k') 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | # This is just a container for running tests 27 | DEBUG = True 28 | 29 | TEMPLATE_DEBUG = True 30 | 31 | ALLOWED_HOSTS = [] 32 | 33 | 34 | # Application definition 35 | 36 | INSTALLED_APPS = ( 37 | 'statici18n', 38 | 'drag_and_drop_v2', 39 | ) 40 | 41 | # Internationalization 42 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 43 | 44 | LANGUAGE_CODE = 'en' 45 | 46 | TIME_ZONE = 'UTC' 47 | 48 | USE_I18N = True 49 | 50 | USE_L10N = True 51 | 52 | USE_TZ = True 53 | 54 | 55 | # Static files (CSS, JavaScript, Images) 56 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 57 | 58 | STATIC_URL = '/static/' 59 | 60 | # statici18n 61 | # http://django-statici18n.readthedocs.io/en/latest/settings.html 62 | 63 | LANGUAGES = [ 64 | ('ar', 'Arabic'), 65 | ('de', 'German'), 66 | ('en', 'English - Source Language'), 67 | ('eo', 'Esperanto'), 68 | ('es_419', 'Spanish (Latin America)'), 69 | ('fr', 'French'), 70 | ('he', 'Hebrew'), 71 | ('hi', 'Hindi'), 72 | ('it', 'Italian'), 73 | ('ja', 'Japanese'), 74 | ('ko', 'Korean (Korea)'), 75 | ('nl', 'Dutch'), 76 | ('pl', 'Polski'), 77 | ('pt_BR', 'Portuguese (Brazil)'), 78 | ('pt_PT', 'Portuguese (Portugal)'), 79 | ('ru', 'Russian'), 80 | ('tr', 'Turkish'), 81 | ('zh_CN', 'Chinese (China)'), 82 | ] 83 | 84 | LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")] 85 | 86 | STATICI18N_DOMAIN = 'text' 87 | STATICI18N_NAMESPACE = 'DragAndDropI18N' 88 | STATICI18N_PACKAGES = ( 89 | 'drag_and_drop_v2', 90 | ) 91 | STATICI18N_ROOT = 'drag_and_drop_v2/public/js' 92 | STATICI18N_OUTPUT_DIR = 'translations' 93 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import json 4 | import random 5 | import re 6 | 7 | from mock import Mock, patch 8 | from six.moves import range 9 | from webob import Request 10 | from workbench.runtime import WorkbenchRuntime 11 | from xblock.fields import ScopeIds 12 | from xblock.runtime import DictKeyValueStore, KvsFieldData 13 | 14 | import drag_and_drop_v2 15 | 16 | 17 | def make_request(data, method='POST'): 18 | """ Make a webob JSON Request """ 19 | request = Request.blank('/') 20 | request.method = 'POST' 21 | data = json.dumps(data).encode('utf-8') if data is not None else b'' 22 | request.body = data 23 | request.method = method 24 | return request 25 | 26 | 27 | def make_block(): 28 | """ Instantiate a DragAndDropBlock XBlock inside a WorkbenchRuntime """ 29 | block_type = 'drag_and_drop_v2' 30 | key_store = DictKeyValueStore() 31 | field_data = KvsFieldData(key_store) 32 | runtime = WorkbenchRuntime() 33 | runtime.course_id = "dummy_course_id" 34 | # noinspection PyProtectedMember 35 | runtime._services['replace_urls'] = Mock( # pylint: disable=protected-access 36 | replace_urls=lambda html, static_replace_only=False: re.sub( 37 | r'"/static/([^"]*)"', r'"/course/test-course/assets/\1"', html 38 | ) 39 | ) 40 | def_id = runtime.id_generator.create_definition(block_type) 41 | usage_id = runtime.id_generator.create_usage(def_id) 42 | scope_ids = ScopeIds('user', block_type, def_id, usage_id) 43 | return drag_and_drop_v2.DragAndDropBlock(runtime, field_data, scope_ids=scope_ids) 44 | 45 | 46 | def generate_max_and_attempts(count=100): 47 | for _ in range(count): 48 | max_attempts = random.randint(1, 100) 49 | attempts = random.randint(0, 100) 50 | expect_validation_error = max_attempts <= attempts 51 | yield max_attempts, attempts, expect_validation_error 52 | 53 | 54 | class TestCaseMixin(object): 55 | """ Helpful mixins for unittest TestCase subclasses """ 56 | maxDiff = None 57 | 58 | DROP_ITEM_HANDLER = 'drop_item' 59 | DO_ATTEMPT_HANDLER = 'do_attempt' 60 | RESET_HANDLER = 'reset' 61 | SHOW_ANSWER_HANDLER = 'show_answer' 62 | USER_STATE_HANDLER = 'student_view_user_state' 63 | 64 | def patch_workbench(self): 65 | self.apply_patch( 66 | 'workbench.runtime.WorkbenchRuntime.local_resource_url', 67 | lambda _, _block, path: '/expanded/url/to/drag_and_drop_v2/' + path 68 | ) 69 | self.apply_patch( 70 | 'drag_and_drop_v2.drag_and_drop_v2.get_grading_ignore_decoys_waffle_flag', 71 | lambda: Mock(is_enabled=lambda _: False), 72 | ) 73 | 74 | def apply_patch(self, *args, **kwargs): 75 | new_patch = patch(*args, **kwargs) 76 | mock = new_patch.start() 77 | self.addCleanup(new_patch.stop) 78 | return mock 79 | 80 | def call_handler(self, handler_name, data=None, expect_json=True, method='POST'): 81 | response = self.block.handle(handler_name, make_request(data, method=method)) 82 | if expect_json: 83 | self.assertEqual(response.status_code, 200) 84 | return json.loads(response.body.decode('utf-8')) 85 | return response 86 | -------------------------------------------------------------------------------- /drag_and_drop_v2/public/js/translations/en/text.js: -------------------------------------------------------------------------------- 1 | 2 | (function(global){ 3 | var DragAndDropI18N = { 4 | init: function() { 5 | 6 | 7 | 'use strict'; 8 | { 9 | const globals = this; 10 | const django = globals.django || (globals.django = {}); 11 | 12 | 13 | django.pluralidx = function(n) { 14 | const v = (n != 1); 15 | if (typeof v === 'boolean') { 16 | return v ? 1 : 0; 17 | } else { 18 | return v; 19 | } 20 | }; 21 | 22 | 23 | /* gettext library */ 24 | 25 | django.catalog = django.catalog || {}; 26 | 27 | 28 | if (!django.jsi18n_initialized) { 29 | django.gettext = function(msgid) { 30 | const value = django.catalog[msgid]; 31 | if (typeof value === 'undefined') { 32 | return msgid; 33 | } else { 34 | return (typeof value === 'string') ? value : value[0]; 35 | } 36 | }; 37 | 38 | django.ngettext = function(singular, plural, count) { 39 | const value = django.catalog[singular]; 40 | if (typeof value === 'undefined') { 41 | return (count == 1) ? singular : plural; 42 | } else { 43 | return value.constructor === Array ? value[django.pluralidx(count)] : value; 44 | } 45 | }; 46 | 47 | django.gettext_noop = function(msgid) { return msgid; }; 48 | 49 | django.pgettext = function(context, msgid) { 50 | let value = django.gettext(context + '\x04' + msgid); 51 | if (value.includes('\x04')) { 52 | value = msgid; 53 | } 54 | return value; 55 | }; 56 | 57 | django.npgettext = function(context, singular, plural, count) { 58 | let value = django.ngettext(context + '\x04' + singular, context + '\x04' + plural, count); 59 | if (value.includes('\x04')) { 60 | value = django.ngettext(singular, plural, count); 61 | } 62 | return value; 63 | }; 64 | 65 | django.interpolate = function(fmt, obj, named) { 66 | if (named) { 67 | return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])}); 68 | } else { 69 | return fmt.replace(/%s/g, function(match){return String(obj.shift())}); 70 | } 71 | }; 72 | 73 | 74 | /* formatting library */ 75 | 76 | django.formats = { 77 | "DATETIME_FORMAT": "N j, Y, P", 78 | "DATETIME_INPUT_FORMATS": [ 79 | "%Y-%m-%d %H:%M:%S", 80 | "%Y-%m-%d %H:%M:%S.%f", 81 | "%Y-%m-%d %H:%M", 82 | "%m/%d/%Y %H:%M:%S", 83 | "%m/%d/%Y %H:%M:%S.%f", 84 | "%m/%d/%Y %H:%M", 85 | "%m/%d/%y %H:%M:%S", 86 | "%m/%d/%y %H:%M:%S.%f", 87 | "%m/%d/%y %H:%M", 88 | "%Y-%m-%d" 89 | ], 90 | "DATE_FORMAT": "N j, Y", 91 | "DATE_INPUT_FORMATS": [ 92 | "%Y-%m-%d", 93 | "%m/%d/%Y", 94 | "%m/%d/%y" 95 | ], 96 | "DECIMAL_SEPARATOR": ".", 97 | "FIRST_DAY_OF_WEEK": 0, 98 | "MONTH_DAY_FORMAT": "F j", 99 | "NUMBER_GROUPING": 3, 100 | "SHORT_DATETIME_FORMAT": "m/d/Y P", 101 | "SHORT_DATE_FORMAT": "m/d/Y", 102 | "THOUSAND_SEPARATOR": ",", 103 | "TIME_FORMAT": "P", 104 | "TIME_INPUT_FORMATS": [ 105 | "%H:%M:%S", 106 | "%H:%M:%S.%f", 107 | "%H:%M" 108 | ], 109 | "YEAR_MONTH_FORMAT": "F Y" 110 | }; 111 | 112 | django.get_format = function(format_type) { 113 | const value = django.formats[format_type]; 114 | if (typeof value === 'undefined') { 115 | return format_type; 116 | } else { 117 | return value; 118 | } 119 | }; 120 | 121 | /* add to global namespace */ 122 | globals.pluralidx = django.pluralidx; 123 | globals.gettext = django.gettext; 124 | globals.ngettext = django.ngettext; 125 | globals.gettext_noop = django.gettext_noop; 126 | globals.pgettext = django.pgettext; 127 | globals.npgettext = django.npgettext; 128 | globals.interpolate = django.interpolate; 129 | globals.get_format = django.get_format; 130 | 131 | django.jsi18n_initialized = true; 132 | } 133 | }; 134 | 135 | 136 | } 137 | }; 138 | DragAndDropI18N.init(); 139 | global.DragAndDropI18N = DragAndDropI18N; 140 | }(this)); 141 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean help compile_translations dummy_translations extract_translations detect_changed_source_translations \ 2 | build_dummy_translations validate_translations check_translations_up_to_date \ 3 | requirements selfcheck test test.python test.unit test.quality upgrade 4 | 5 | .DEFAULT_GOAL := help 6 | 7 | WORKING_DIR := drag_and_drop_v2 8 | JS_TARGET := $(WORKING_DIR)/public/js/translations 9 | EXTRACT_DIR := $(WORKING_DIR)/conf/locale/en/LC_MESSAGES 10 | EXTRACTED_DJANGO_PARTIAL := $(EXTRACT_DIR)/django-partial.po 11 | EXTRACTED_DJANGOJS_PARTIAL := $(EXTRACT_DIR)/djangojs-partial.po 12 | EXTRACTED_DJANGO := $(EXTRACT_DIR)/django.po 13 | 14 | help: ## display this help message 15 | @echo "Please use \`make ' where is one of" 16 | @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' 17 | 18 | clean: ## remove generated byte code, coverage reports, and build artifacts 19 | find . -name '__pycache__' -exec rm -rf {} + 20 | find . -name '*.pyc' -exec rm -f {} + 21 | find . -name '*.pyo' -exec rm -f {} + 22 | find . -name '*~' -exec rm -f {} + 23 | rm -fr build/ 24 | rm -fr dist/ 25 | rm -fr *.egg-info 26 | 27 | ## Localization targets 28 | 29 | extract_translations: ## extract strings to be translated, outputting .po files 30 | cd $(WORKING_DIR) && i18n_tool extract 31 | mv $(EXTRACTED_DJANGO_PARTIAL) $(EXTRACTED_DJANGO) 32 | # Safely concatenate djangojs if it exists. The file will exist in this repo, but we're trying to follow a pattern 33 | # between all repositories that use i18n_tool 34 | if test -f $(EXTRACTED_DJANGOJS_PARTIAL); then \ 35 | msgcat $(EXTRACTED_DJANGO) $(EXTRACTED_DJANGOJS_PARTIAL) -o $(EXTRACTED_DJANGO) && \ 36 | rm $(EXTRACTED_DJANGOJS_PARTIAL); \ 37 | fi 38 | sed -i'' -e 's/nplurals=INTEGER/nplurals=2/' $(EXTRACTED_DJANGO) 39 | sed -i'' -e 's/plural=EXPRESSION/plural=\(n != 1\)/' $(EXTRACTED_DJANGO) 40 | 41 | compile_translations: ## compile translation files, outputting .mo files for each supported language 42 | cd $(WORKING_DIR) && i18n_tool generate -v 43 | python manage.py compilejsi18n --namespace DragAndDropI18N --output $(JS_TARGET) 44 | 45 | detect_changed_source_translations: 46 | cd $(WORKING_DIR) && i18n_tool changed 47 | 48 | dummy_translations: ## generate dummy translation (.po) files 49 | cd $(WORKING_DIR) && i18n_tool dummy 50 | 51 | build_dummy_translations: dummy_translations compile_translations ## generate and compile dummy translation files 52 | 53 | validate_translations: build_dummy_translations detect_changed_source_translations ## validate translations 54 | 55 | check_translations_up_to_date: extract_translations compile_translations dummy_translations detect_changed_source_translations ## extract, compile, and check if translation files are up-to-date 56 | 57 | piptools: ## install pinned version of pip-compile and pip-sync 58 | pip install -r requirements/pip.txt 59 | pip install -r requirements/pip-tools.txt 60 | 61 | requirements: piptools ## install test requirements locally 62 | pip-sync requirements/ci.txt 63 | 64 | requirements_python: piptools ## install all requirements locally 65 | pip-sync requirements/dev.txt requirements/private.* 66 | 67 | test.quality: selfcheck ## run quality checkers on the codebase 68 | tox -e quality 69 | 70 | test.python: ## run python unit tests in the local virtualenv 71 | pytest --cov drag_and_drop_v2 $(TEST) 72 | 73 | test.unit: ## run all unit tests 74 | tox $(TEST) 75 | 76 | test: test.unit test.quality ## Run all tests 77 | tox -e translations 78 | 79 | # Define PIP_COMPILE_OPTS=-v to get more information during make upgrade. 80 | PIP_COMPILE = pip-compile --upgrade $(PIP_COMPILE_OPTS) 81 | 82 | upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade 83 | upgrade: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in 84 | pip install -qr requirements/pip-tools.txt 85 | # Make sure to compile files after any other files they include! 86 | $(PIP_COMPILE) --allow-unsafe -o requirements/pip.txt requirements/pip.in 87 | $(PIP_COMPILE) -o requirements/pip-tools.txt requirements/pip-tools.in 88 | pip install -qr requirements/pip.txt 89 | pip install -qr requirements/pip-tools.txt 90 | $(PIP_COMPILE) -o requirements/base.txt requirements/base.in 91 | $(PIP_COMPILE) -o requirements/test.txt requirements/test.in 92 | $(PIP_COMPILE) -o requirements/quality.txt requirements/quality.in 93 | $(PIP_COMPILE) -o requirements/ci.txt requirements/ci.in 94 | $(PIP_COMPILE) -o requirements/dev.txt requirements/dev.in 95 | sed -i '/^[dD]jango==/d' requirements/test.txt 96 | 97 | selfcheck: ## check that the Makefile is well-formed 98 | @echo "The Makefile is well-formed." 99 | -------------------------------------------------------------------------------- /drag_and_drop_v2/default_data.py: -------------------------------------------------------------------------------- 1 | """ Default data for Drag and Drop v2 XBlock """ 2 | from .utils import _ 3 | 4 | TARGET_IMG_DESCRIPTION = _( 5 | "An isosceles triangle with three layers of similar height. " 6 | "It is shown upright, so the widest layer is located at the bottom, " 7 | "and the narrowest layer is located at the top." 8 | ) 9 | 10 | TOP_ZONE_ID = "top" 11 | MIDDLE_ZONE_ID = "middle" 12 | BOTTOM_ZONE_ID = "bottom" 13 | 14 | TOP_ZONE_TITLE = _("The Top Zone") 15 | MIDDLE_ZONE_TITLE = _("The Middle Zone") 16 | BOTTOM_ZONE_TITLE = _("The Bottom Zone") 17 | 18 | TOP_ZONE_DESCRIPTION = _("Use this zone to associate an item with the top layer of the triangle.") 19 | MIDDLE_ZONE_DESCRIPTION = _("Use this zone to associate an item with the middle layer of the triangle.") 20 | BOTTOM_ZONE_DESCRIPTION = _("Use this zone to associate an item with the bottom layer of the triangle.") 21 | 22 | ITEM_CORRECT_FEEDBACK_TOP = _("Correct! This one belongs to The Top Zone.") 23 | ITEM_CORRECT_FEEDBACK_MIDDLE = _("Correct! This one belongs to The Middle Zone.") 24 | ITEM_CORRECT_FEEDBACK_BOTTOM = _("Correct! This one belongs to The Bottom Zone.") 25 | ITEM_INCORRECT_FEEDBACK = _("No, this item does not belong here. Try again.") 26 | ITEM_NO_ZONE_FEEDBACK = _("You silly, there are no zones for this one.") 27 | ITEM_ANY_ZONE_FEEDBACK = _("Of course it goes here! It goes anywhere!") 28 | 29 | ITEM_TOP_ZONE_NAME = _("Goes to the top") 30 | ITEM_MIDDLE_ZONE_NAME = _("Goes to the middle") 31 | ITEM_BOTTOM_ZONE_NAME = _("Goes to the bottom") 32 | ITEM_ANY_ZONE_NAME = _("Goes anywhere") 33 | ITEM_NO_ZONE_NAME = _("I don't belong anywhere") 34 | 35 | START_FEEDBACK = _("Drag the items onto the image above.") 36 | FINISH_FEEDBACK = _("Good work! You have completed this drag and drop problem.") 37 | 38 | DEFAULT_DATA = { 39 | "targetImgDescription": TARGET_IMG_DESCRIPTION, 40 | "zones": [ 41 | { 42 | "uid": TOP_ZONE_ID, 43 | "title": TOP_ZONE_TITLE, 44 | "description": TOP_ZONE_DESCRIPTION, 45 | "x": 160, 46 | "y": 30, 47 | "width": 196, 48 | "height": 178, 49 | "align": "center" 50 | }, 51 | { 52 | "uid": MIDDLE_ZONE_ID, 53 | "title": MIDDLE_ZONE_TITLE, 54 | "description": MIDDLE_ZONE_DESCRIPTION, 55 | "x": 86, 56 | "y": 210, 57 | "width": 340, 58 | "height": 138, 59 | "align": "center" 60 | }, 61 | { 62 | "uid": BOTTOM_ZONE_ID, 63 | "title": BOTTOM_ZONE_TITLE, 64 | "description": BOTTOM_ZONE_DESCRIPTION, 65 | "x": 15, 66 | "y": 350, 67 | "width": 485, 68 | "height": 135, 69 | "align": "center" 70 | } 71 | ], 72 | "items": [ 73 | { 74 | "displayName": ITEM_TOP_ZONE_NAME, 75 | "feedback": { 76 | "incorrect": ITEM_INCORRECT_FEEDBACK, 77 | "correct": ITEM_CORRECT_FEEDBACK_TOP 78 | }, 79 | "zones": [ 80 | TOP_ZONE_ID 81 | ], 82 | "imageURL": "", 83 | "id": 0, 84 | }, 85 | { 86 | "displayName": ITEM_MIDDLE_ZONE_NAME, 87 | "feedback": { 88 | "incorrect": ITEM_INCORRECT_FEEDBACK, 89 | "correct": ITEM_CORRECT_FEEDBACK_MIDDLE 90 | }, 91 | "zones": [ 92 | MIDDLE_ZONE_ID 93 | ], 94 | "imageURL": "", 95 | "id": 1, 96 | }, 97 | { 98 | "displayName": ITEM_BOTTOM_ZONE_NAME, 99 | "feedback": { 100 | "incorrect": ITEM_INCORRECT_FEEDBACK, 101 | "correct": ITEM_CORRECT_FEEDBACK_BOTTOM 102 | }, 103 | "zones": [ 104 | BOTTOM_ZONE_ID 105 | ], 106 | "imageURL": "", 107 | "id": 2, 108 | }, 109 | { 110 | "displayName": ITEM_ANY_ZONE_NAME, 111 | "feedback": { 112 | "incorrect": "", 113 | "correct": ITEM_ANY_ZONE_FEEDBACK 114 | }, 115 | "zones": [ 116 | TOP_ZONE_ID, 117 | BOTTOM_ZONE_ID, 118 | MIDDLE_ZONE_ID 119 | ], 120 | "imageURL": "", 121 | "id": 3 122 | }, 123 | { 124 | "displayName": ITEM_NO_ZONE_NAME, 125 | "feedback": { 126 | "incorrect": ITEM_NO_ZONE_FEEDBACK, 127 | "correct": "" 128 | }, 129 | "zones": [], 130 | "imageURL": "", 131 | "id": 4, 132 | }, 133 | ], 134 | "feedback": { 135 | "start": START_FEEDBACK, 136 | "finish": FINISH_FEEDBACK, 137 | }, 138 | } 139 | -------------------------------------------------------------------------------- /tests/unit/test_indexibility.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from .test_fixtures import BaseDragAndDropAjaxFixture 4 | 5 | 6 | class TestPlainDragAndDropIndexibility(BaseDragAndDropAjaxFixture, unittest.TestCase): 7 | FOLDER = "plain" 8 | 9 | def test_indexibility(self): 10 | expected_indexing_result = { 11 | 'content': { 12 | 'question_text': 'Can you solve this drag-and-drop problem?', 13 | 'item_1_image_description': '', 14 | 'background_image_description': 'This describes the target image', 15 | 'item_2_display_name': 'X', 16 | 'item_0_image_description': '', 17 | 'zone_0_display_name': 'Zone 1', 18 | 'item_3_image_description': '', 19 | 'item_1_display_name': '2', 20 | 'zone_1_display_name': 'Zone 2', 21 | 'zone_1_description': '', 22 | 'item_0_display_name': '1', 23 | 'item_2_image_description': '', 24 | 'zone_0_description': '', 25 | 'item_3_display_name': '', 26 | 'display_name': 'DnDv2 XBlock with plain text instructions' 27 | }, 28 | 'content_type': 'Drag and Drop' 29 | } 30 | self.assertEqual(self.block.index_dictionary(), expected_indexing_result) 31 | 32 | 33 | class TestOldDragAndDropIndexibility(BaseDragAndDropAjaxFixture, unittest.TestCase): 34 | FOLDER = "old" 35 | 36 | def test_indexibility(self): 37 | expected_indexing_result = { 38 | 'content_type': 'Drag and Drop', 39 | 'content': { 40 | 'item_3_image_description': '', 41 | 'zone_1_description': '', 42 | 'item_1_display_name': '2', 43 | 'zone_1_display_name': 'Zone 2', 44 | 'item_1_image_description': '', 45 | 'item_0_display_name': '1', 46 | 'question_text': '', 47 | 'background_image_description': 'This describes the target image', 48 | 'zone_0_description': '', 49 | 'zone_0_display_name': 'Zone 1', 50 | 'item_0_image_description': '', 51 | 'item_3_display_name': '', 52 | 'display_name': 'Drag and Drop', 53 | 'item_2_image_description': '', 54 | 'item_2_display_name': 'X' 55 | } 56 | } 57 | self.assertEqual(self.block.index_dictionary(), expected_indexing_result) 58 | 59 | 60 | class TestHtmlDragAndDropIndexibility(BaseDragAndDropAjaxFixture, unittest.TestCase): 61 | FOLDER = "html" 62 | 63 | def test_indexibility(self): 64 | expected_indexing_result = { 65 | 'content_type': 'Drag and Drop', 66 | 'content': { 67 | 'item_2_display_name': 'X', 68 | 'zone_0_display_name': 'Zone 1', 69 | 'item_1_display_name': '2', 70 | 'item_3_image_description': '', 71 | 'zone_0_description': '', 72 | 'zone_1_display_name': 'Zone 2', 73 | 'background_image_description': 'This describes the target image', 74 | 'zone_1_description': '', 75 | 'item_1_image_description': '', 76 | 'item_0_image_description': '', 77 | 'item_2_image_description': '', 78 | 'question_text': 'Solve this drag-and-drop problem.', 79 | 'item_3_display_name': '', 80 | 'display_name': 'DnDv2 XBlock with HTML instructions', 81 | 'item_0_display_name': '1' 82 | } 83 | } 84 | self.assertEqual(self.block.index_dictionary(), expected_indexing_result) 85 | 86 | 87 | class TestAssessmentDragAndDropIndexibility(BaseDragAndDropAjaxFixture, unittest.TestCase): 88 | FOLDER = "assessment" 89 | 90 | def test_indexibility(self): 91 | expected_indexing_result = { 92 | 'content_type': 'Drag and Drop', 93 | 'content': { 94 | 'item_3_image_description': '', 95 | 'item_0_display_name': '1', 96 | 'zone_0_description': '', 97 | 'item_2_display_name': '3', 98 | 'item_2_image_description': '', 99 | 'background_image_description': 'This describes the target image', 100 | 'display_name': 'DnDv2 XBlock with plain text instructions', 101 | 'item_1_image_description': '', 102 | 'item_4_display_name': '', 103 | 'zone_1_description': '', 104 | 'item_1_display_name': '2', 105 | 'item_3_display_name': 'X', 106 | 'question_text': 'Can you solve this drag-and-drop problem?', 107 | 'zone_1_display_name': 'Zone 2', 108 | 'zone_0_display_name': 'Zone 1', 109 | 'item_4_image_description': '', 110 | 'item_0_image_description': '' 111 | } 112 | } 113 | self.assertEqual(self.block.index_dictionary(), expected_indexing_result) 114 | -------------------------------------------------------------------------------- /Native_API.md: -------------------------------------------------------------------------------- 1 | Native API Documentation 2 | ======================== 3 | 4 | This documents the APIs that can be used to implement native wrappers. There are 5 | three types of APIs: 6 | 7 | - `student_view_data`: Exposes block settings and content as JSON data. Can be 8 | retrieved via the edX 9 | [Course Blocks API](https://openedx.atlassian.net/wiki/spaces/EDUCATOR/pages/29688043/Course+Blocks+API). 10 | Does not include user-specific data, which is available from `student_view_user_state`. 11 | - `student_view_user_state`: XBlock handler which returns the currently logged 12 | in user's state data in JSON format. 13 | - Custom XBlock handlers used for submitting user input and recording user actions. 14 | 15 | Drag and Drop (`drag-and-drop-v2`) 16 | ----------------------------------- 17 | 18 | ### `student_view_data` 19 | 20 | - `block_id`: (string) The XBlock's usage ID. 21 | - `display_name`: (string) The XBlock's display name. 22 | - `mode`: (string) A choice of between \[standard, assessment\]. 23 | - `type`: (string) Always equal to `drag-and-drop-v2`. 24 | - `weight`: (float) The weight of the problem. 25 | - `zones`: (list) A list of `zone` objects. 26 | - `max_attempts`: (int) Maximum number of attempts in assessment mode. 27 | - `graded`: (boolean) `true` if grading is enabled. 28 | - `weighted_max_score`: (float) The maximum score multiplied by the weight. 29 | - `max_items_per_zone`: (int) Maximum number of items in a zone. 30 | - `url_name`: (string) Url path for the lesson. 31 | - `display_zone_labels`: (boolean) 32 | - `display_zone_borders`: (boolean) 33 | - `items`: (array) A list of draggable `item`. 34 | - `title`: (string) Title to be shown. 35 | - `show_title`: (boolean) `true` indicates the title should be shown. 36 | - `problem_text`: (string) Problem description. 37 | - `show_problem_header`: (boolean) `true` indicates the description should be shown. 38 | - `target_img_expanded_url`: (string) URL for the background image. 39 | - `target_img_description`: (string) Description of the background image. 40 | - `item_background_color`: (string) Background color for the draggable items. 41 | - `item_text_color`: (string) Text color for the draggable items. 42 | 43 | ### `student_view_user_state` 44 | 45 | Contains the current state of the user interaction with the block. 46 | 47 | - `attempts`: (integer) Number of attempts used so far. 48 | - `finished`: (boolean) `true` if the user successfully completed the problem at least once. 49 | - `grade`: (float) Current grade. 50 | - `items`: (object) Object indexing each draggable `item` 51 | - `overall_feedback`: (array) List of feedback `message` 52 | 53 | #### `zone` 54 | 55 | The zones are the target for dropping the draggable elements and contains information specific for each one: 56 | 57 | - `uid`: (string) Unique id. 58 | - `title`: (string) Description. 59 | - `align`: (string) Alignment of items. 60 | - `height`: (int) Vertical size. 61 | - `width`: (int) Horizontal size. 62 | - `x`: (int) Horizontal position. 63 | - `y`: (int) Vertical position. 64 | - `description`: (string) Long description. 65 | 66 | ### `item` 67 | 68 | Information about each draggable item and their placement. 69 | 70 | - `imageURL`: (string) Relative URL for image. 71 | - `displayName`: (string) Text description. 72 | - `expandedImageURL`: (string) Full URL to image. 73 | - `id`: (int) Unique id for item. 74 | - `correct`: (boolean) `true` if the item was dragged to the correct place. 75 | - `zone`: (string) Reference to the uid of the target zone. 76 | 77 | ### `message` 78 | 79 | Contains feedback shown to the user 80 | 81 | - `message`: (string) Content of the feedback. 82 | 83 | ### Custom Handlers 84 | 85 | #### `drop_item` 86 | 87 | This JSON handler is used to handle dropping an item in a zone. The arguments are 88 | - `val`: (string) The number of the item. 89 | - `zone`: (string) The uid of the zone. 90 | 91 | In assessment mode will return an empty object, while in standard mode it returns the following: 92 | - `correct`: (boolean) `true` if correct. 93 | - `grade`: (float) Current grade for the problem. 94 | - `finished`: (boolean) `true` if finished. 95 | - `overall_feedback`: (message) Feedback when finished. 96 | - `feedback`: (message) Feedback for the current try. 97 | 98 | #### `do_attempt` 99 | 100 | Check submitted solution and return feedback if in assessment mode. 101 | 102 | - `correct`: (boolean) `true` if solution is correct. 103 | - `attempts`: (int) Number of attempts remaining. 104 | - `grade`: (float) Final grade. 105 | - `misplaced_items`: (array) List of misplaced items ids. 106 | - `feedback`: (message) Feedback message for current try. 107 | - `overall_feedback`: (message) Feedback when finished. 108 | 109 | #### `publish_event` 110 | 111 | This endpoint is used to publish XBlock event from frontend 112 | 113 | ##### `event` 114 | 115 | - `event_type`: (string) Event identifier. 116 | 117 | #### `reset` 118 | 119 | This resets the current try in assessment mode and returns the previous state. 120 | 121 | #### `show_answer` 122 | 123 | Returns correct answer in assessment mode. 124 | 125 | #### `expand_static_url` 126 | 127 | AJAX-accessible handler for expanding URLs to static image files that can be used as background. -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | appdirs==1.4.4 8 | # via 9 | # -r requirements/base.txt 10 | # fs 11 | arrow==1.4.0 12 | # via cookiecutter 13 | asgiref==3.11.0 14 | # via 15 | # -r requirements/base.txt 16 | # django 17 | binaryornot==0.4.4 18 | # via cookiecutter 19 | bleach[css]==6.3.0 20 | # via -r requirements/base.txt 21 | boto3==1.42.4 22 | # via 23 | # -r requirements/base.txt 24 | # fs-s3fs 25 | botocore==1.42.4 26 | # via 27 | # -r requirements/base.txt 28 | # boto3 29 | # s3transfer 30 | certifi==2025.11.12 31 | # via requests 32 | chardet==5.2.0 33 | # via binaryornot 34 | charset-normalizer==3.4.4 35 | # via requests 36 | click==8.3.1 37 | # via cookiecutter 38 | cookiecutter==2.6.0 39 | # via xblock-sdk 40 | coverage[toml]==7.12.0 41 | # via pytest-cov 42 | ddt==1.7.2 43 | # via -r requirements/test.in 44 | # via 45 | # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt 46 | # -r requirements/base.txt 47 | # django-appconf 48 | # django-statici18n 49 | # edx-i18n-tools 50 | # openedx-django-pyfs 51 | # xblock-sdk 52 | django-appconf==1.2.0 53 | # via 54 | # -r requirements/base.txt 55 | # django-statici18n 56 | django-statici18n==2.6.0 57 | # via -r requirements/base.txt 58 | edx-i18n-tools==1.9.0 59 | # via -r requirements/test.in 60 | fs==2.4.16 61 | # via 62 | # -r requirements/base.txt 63 | # fs-s3fs 64 | # openedx-django-pyfs 65 | # xblock 66 | fs-s3fs==1.1.1 67 | # via 68 | # -r requirements/base.txt 69 | # openedx-django-pyfs 70 | # xblock-sdk 71 | idna==3.11 72 | # via requests 73 | iniconfig==2.3.0 74 | # via pytest 75 | jinja2==3.1.6 76 | # via cookiecutter 77 | jmespath==1.0.1 78 | # via 79 | # -r requirements/base.txt 80 | # boto3 81 | # botocore 82 | lazy==1.6 83 | # via 84 | # -r requirements/base.txt 85 | # xblock 86 | lxml[html-clean]==6.0.2 87 | # via 88 | # -r requirements/base.txt 89 | # edx-i18n-tools 90 | # lxml-html-clean 91 | # xblock 92 | # xblock-sdk 93 | lxml-html-clean==0.4.3 94 | # via lxml 95 | mako==1.3.10 96 | # via 97 | # -r requirements/base.txt 98 | # xblock 99 | markdown-it-py==4.0.0 100 | # via rich 101 | markupsafe==3.0.3 102 | # via 103 | # -r requirements/base.txt 104 | # jinja2 105 | # mako 106 | # xblock 107 | mdurl==0.1.2 108 | # via markdown-it-py 109 | mock==5.2.0 110 | # via -r requirements/test.in 111 | openedx-django-pyfs==3.8.0 112 | # via 113 | # -r requirements/base.txt 114 | # -r requirements/test.in 115 | # xblock 116 | packaging==25.0 117 | # via pytest 118 | path==16.16.0 119 | # via edx-i18n-tools 120 | pluggy==1.6.0 121 | # via 122 | # pytest 123 | # pytest-cov 124 | polib==1.2.0 125 | # via edx-i18n-tools 126 | pygments==2.19.2 127 | # via 128 | # pytest 129 | # rich 130 | pypng==0.20220715.0 131 | # via xblock-sdk 132 | pytest==9.0.2 133 | # via 134 | # pytest-cov 135 | # pytest-django 136 | pytest-cov==7.0.0 137 | # via -r requirements/test.in 138 | pytest-django==4.11.1 139 | # via -r requirements/test.in 140 | python-dateutil==2.9.0.post0 141 | # via 142 | # -r requirements/base.txt 143 | # arrow 144 | # botocore 145 | # xblock 146 | python-slugify==8.0.4 147 | # via cookiecutter 148 | pytz==2025.2 149 | # via 150 | # -r requirements/base.txt 151 | # xblock 152 | pyyaml==6.0.3 153 | # via 154 | # -r requirements/base.txt 155 | # cookiecutter 156 | # edx-i18n-tools 157 | # xblock 158 | requests==2.32.5 159 | # via 160 | # cookiecutter 161 | # xblock-sdk 162 | rich==14.2.0 163 | # via cookiecutter 164 | s3transfer==0.16.0 165 | # via 166 | # -r requirements/base.txt 167 | # boto3 168 | simplejson==3.20.2 169 | # via 170 | # -r requirements/base.txt 171 | # xblock 172 | # xblock-sdk 173 | six==1.17.0 174 | # via 175 | # -r requirements/base.txt 176 | # fs 177 | # fs-s3fs 178 | # python-dateutil 179 | sqlparse==0.5.4 180 | # via 181 | # -r requirements/base.txt 182 | # django 183 | text-unidecode==1.3 184 | # via python-slugify 185 | tinycss2==1.4.0 186 | # via 187 | # -r requirements/base.txt 188 | # bleach 189 | tzdata==2025.2 190 | # via arrow 191 | urllib3==2.6.0 192 | # via 193 | # -r requirements/base.txt 194 | # botocore 195 | # requests 196 | web-fragments==3.1.0 197 | # via 198 | # -r requirements/base.txt 199 | # xblock 200 | # xblock-sdk 201 | webencodings==0.5.1 202 | # via 203 | # -r requirements/base.txt 204 | # bleach 205 | # tinycss2 206 | webob==1.8.9 207 | # via 208 | # -r requirements/base.txt 209 | # xblock 210 | # xblock-sdk 211 | xblock[django]==5.2.0 212 | # via 213 | # -r requirements/base.txt 214 | # xblock-sdk 215 | xblock-sdk==0.13.0 216 | # via -r requirements/test.in 217 | 218 | # The following packages are considered to be unsafe in a requirements file: 219 | # setuptools 220 | -------------------------------------------------------------------------------- /drag_and_drop_v2/public/js/translations/tr/text.js: -------------------------------------------------------------------------------- 1 | 2 | (function(global){ 3 | var DragAndDropI18N = { 4 | init: function() { 5 | 6 | 7 | 'use strict'; 8 | { 9 | const globals = this; 10 | const django = globals.django || (globals.django = {}); 11 | 12 | 13 | django.pluralidx = function(n) { 14 | const v = (n > 1); 15 | if (typeof v === 'boolean') { 16 | return v ? 1 : 0; 17 | } else { 18 | return v; 19 | } 20 | }; 21 | 22 | 23 | /* gettext library */ 24 | 25 | django.catalog = django.catalog || {}; 26 | 27 | const newcatalog = { 28 | "Background description": "Arkaplan a\u00e7\u0131klamas\u0131", 29 | "Cancel": "\u0130ptal", 30 | "Change background": "Arkaplan\u0131 de\u011fi\u015ftir", 31 | "Correctly placed {correct_count} item": [ 32 | "Do\u011fru yerle\u015ftirilmi\u015f {correct_count} \u00f6\u011fe", 33 | "Do\u011fru yerle\u015ftirilmi\u015f {correct_count} \u00f6\u011fe" 34 | ], 35 | "Drag and Drop": "S\u00fcr\u00fckle ve B\u0131rak", 36 | "Feedback": "Geri Bildirim", 37 | "Hints:": "\u0130pu\u00e7lar\u0131:", 38 | "Misplaced {misplaced_count} item (misplaced item was returned to the item bank)": [ 39 | "{misplaced_count} \u00f6\u011fe yanl\u0131\u015f yerle\u015ftirildi. Yanl\u0131\u015f yerle\u015ftirilen \u00f6\u011feler, \u00f6\u011fe bankas\u0131na iade edildi.", 40 | "{misplaced_count} \u00f6\u011fe yanl\u0131\u015f yerle\u015ftirildi. Yanl\u0131\u015f yerle\u015ftirilen \u00f6\u011feler, \u00f6\u011fe bankas\u0131na iade edildi." 41 | ], 42 | "Mode": "Mod", 43 | "Problem": "Sorun", 44 | "Save": "Kaydet", 45 | "Saving": "Kaydediliyor", 46 | "Show title": "Ba\u015fl\u0131\u011f\u0131 g\u00f6ster", 47 | "Some of your answers were not correct.": "Cevaplar\u0131n\u0131zdan baz\u0131lar\u0131 do\u011fru de\u011fildi.", 48 | "The Bottom Zone": "Alt B\u00f6lge", 49 | "The Middle Zone": "Orta B\u00f6lge", 50 | "The Top Zone": "\u00dcst B\u00f6lge", 51 | "Title": "Ba\u015fl\u0131k", 52 | "Your highest score is {score}": "En y\u00fcksek puan\u0131n\u0131z {score}" 53 | }; 54 | for (const key in newcatalog) { 55 | django.catalog[key] = newcatalog[key]; 56 | } 57 | 58 | 59 | if (!django.jsi18n_initialized) { 60 | django.gettext = function(msgid) { 61 | const value = django.catalog[msgid]; 62 | if (typeof value === 'undefined') { 63 | return msgid; 64 | } else { 65 | return (typeof value === 'string') ? value : value[0]; 66 | } 67 | }; 68 | 69 | django.ngettext = function(singular, plural, count) { 70 | const value = django.catalog[singular]; 71 | if (typeof value === 'undefined') { 72 | return (count == 1) ? singular : plural; 73 | } else { 74 | return value.constructor === Array ? value[django.pluralidx(count)] : value; 75 | } 76 | }; 77 | 78 | django.gettext_noop = function(msgid) { return msgid; }; 79 | 80 | django.pgettext = function(context, msgid) { 81 | let value = django.gettext(context + '\x04' + msgid); 82 | if (value.includes('\x04')) { 83 | value = msgid; 84 | } 85 | return value; 86 | }; 87 | 88 | django.npgettext = function(context, singular, plural, count) { 89 | let value = django.ngettext(context + '\x04' + singular, context + '\x04' + plural, count); 90 | if (value.includes('\x04')) { 91 | value = django.ngettext(singular, plural, count); 92 | } 93 | return value; 94 | }; 95 | 96 | django.interpolate = function(fmt, obj, named) { 97 | if (named) { 98 | return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])}); 99 | } else { 100 | return fmt.replace(/%s/g, function(match){return String(obj.shift())}); 101 | } 102 | }; 103 | 104 | 105 | /* formatting library */ 106 | 107 | django.formats = { 108 | "DATETIME_FORMAT": "d F Y H:i", 109 | "DATETIME_INPUT_FORMATS": [ 110 | "%d/%m/%Y %H:%M:%S", 111 | "%d/%m/%Y %H:%M:%S.%f", 112 | "%d/%m/%Y %H:%M", 113 | "%Y-%m-%d %H:%M:%S", 114 | "%Y-%m-%d %H:%M:%S.%f", 115 | "%Y-%m-%d %H:%M", 116 | "%Y-%m-%d" 117 | ], 118 | "DATE_FORMAT": "d F Y", 119 | "DATE_INPUT_FORMATS": [ 120 | "%d/%m/%Y", 121 | "%d/%m/%y", 122 | "%y-%m-%d", 123 | "%Y-%m-%d" 124 | ], 125 | "DECIMAL_SEPARATOR": ",", 126 | "FIRST_DAY_OF_WEEK": 1, 127 | "MONTH_DAY_FORMAT": "d F", 128 | "NUMBER_GROUPING": 3, 129 | "SHORT_DATETIME_FORMAT": "d M Y H:i", 130 | "SHORT_DATE_FORMAT": "d M Y", 131 | "THOUSAND_SEPARATOR": ".", 132 | "TIME_FORMAT": "H:i", 133 | "TIME_INPUT_FORMATS": [ 134 | "%H:%M:%S", 135 | "%H:%M:%S.%f", 136 | "%H:%M" 137 | ], 138 | "YEAR_MONTH_FORMAT": "F Y" 139 | }; 140 | 141 | django.get_format = function(format_type) { 142 | const value = django.formats[format_type]; 143 | if (typeof value === 'undefined') { 144 | return format_type; 145 | } else { 146 | return value; 147 | } 148 | }; 149 | 150 | /* add to global namespace */ 151 | globals.pluralidx = django.pluralidx; 152 | globals.gettext = django.gettext; 153 | globals.ngettext = django.ngettext; 154 | globals.gettext_noop = django.gettext_noop; 155 | globals.pgettext = django.pgettext; 156 | globals.npgettext = django.npgettext; 157 | globals.interpolate = django.interpolate; 158 | globals.get_format = django.get_format; 159 | 160 | django.jsi18n_initialized = true; 161 | } 162 | }; 163 | 164 | 165 | } 166 | }; 167 | DragAndDropI18N.init(); 168 | global.DragAndDropI18N = DragAndDropI18N; 169 | }(this)); 170 | -------------------------------------------------------------------------------- /drag_and_drop_v2/public/js/translations/nl/text.js: -------------------------------------------------------------------------------- 1 | 2 | (function(global){ 3 | var DragAndDropI18N = { 4 | init: function() { 5 | 6 | 7 | 'use strict'; 8 | { 9 | const globals = this; 10 | const django = globals.django || (globals.django = {}); 11 | 12 | 13 | django.pluralidx = function(n) { 14 | const v = (n != 1); 15 | if (typeof v === 'boolean') { 16 | return v ? 1 : 0; 17 | } else { 18 | return v; 19 | } 20 | }; 21 | 22 | 23 | /* gettext library */ 24 | 25 | django.catalog = django.catalog || {}; 26 | 27 | const newcatalog = { 28 | "Correctly placed {correct_count} item": [ 29 | "Correct geplaatste {correct_count} item", 30 | "Correct geplaatste {correct_count} items" 31 | ], 32 | "Hints:": "Hints:", 33 | "Misplaced {misplaced_count} item (misplaced item was returned to the item bank)": [ 34 | "Misplaatste {misplaced_count} item. Misplaatste item is teruggebracht naar de itembank", 35 | "Misplaatste {misplaced_count} items. Misplaatste items zijn teruggestuurd naar de itembank" 36 | ], 37 | "Problem": "Probleem", 38 | "Some of your answers were not correct.": "Sommige van uw antwoorden waren niet correct.", 39 | "Your highest score is {score}": "Je hoogste score is {score}" 40 | }; 41 | for (const key in newcatalog) { 42 | django.catalog[key] = newcatalog[key]; 43 | } 44 | 45 | 46 | if (!django.jsi18n_initialized) { 47 | django.gettext = function(msgid) { 48 | const value = django.catalog[msgid]; 49 | if (typeof value === 'undefined') { 50 | return msgid; 51 | } else { 52 | return (typeof value === 'string') ? value : value[0]; 53 | } 54 | }; 55 | 56 | django.ngettext = function(singular, plural, count) { 57 | const value = django.catalog[singular]; 58 | if (typeof value === 'undefined') { 59 | return (count == 1) ? singular : plural; 60 | } else { 61 | return value.constructor === Array ? value[django.pluralidx(count)] : value; 62 | } 63 | }; 64 | 65 | django.gettext_noop = function(msgid) { return msgid; }; 66 | 67 | django.pgettext = function(context, msgid) { 68 | let value = django.gettext(context + '\x04' + msgid); 69 | if (value.includes('\x04')) { 70 | value = msgid; 71 | } 72 | return value; 73 | }; 74 | 75 | django.npgettext = function(context, singular, plural, count) { 76 | let value = django.ngettext(context + '\x04' + singular, context + '\x04' + plural, count); 77 | if (value.includes('\x04')) { 78 | value = django.ngettext(singular, plural, count); 79 | } 80 | return value; 81 | }; 82 | 83 | django.interpolate = function(fmt, obj, named) { 84 | if (named) { 85 | return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])}); 86 | } else { 87 | return fmt.replace(/%s/g, function(match){return String(obj.shift())}); 88 | } 89 | }; 90 | 91 | 92 | /* formatting library */ 93 | 94 | django.formats = { 95 | "DATETIME_FORMAT": "j F Y H:i", 96 | "DATETIME_INPUT_FORMATS": [ 97 | "%d-%m-%Y %H:%M:%S", 98 | "%d-%m-%y %H:%M:%S", 99 | "%Y-%m-%d %H:%M:%S", 100 | "%d/%m/%Y %H:%M:%S", 101 | "%d/%m/%y %H:%M:%S", 102 | "%Y/%m/%d %H:%M:%S", 103 | "%d-%m-%Y %H:%M:%S.%f", 104 | "%d-%m-%y %H:%M:%S.%f", 105 | "%Y-%m-%d %H:%M:%S.%f", 106 | "%d/%m/%Y %H:%M:%S.%f", 107 | "%d/%m/%y %H:%M:%S.%f", 108 | "%Y/%m/%d %H:%M:%S.%f", 109 | "%d-%m-%Y %H.%M:%S", 110 | "%d-%m-%y %H.%M:%S", 111 | "%d/%m/%Y %H.%M:%S", 112 | "%d/%m/%y %H.%M:%S", 113 | "%d-%m-%Y %H.%M:%S.%f", 114 | "%d-%m-%y %H.%M:%S.%f", 115 | "%d/%m/%Y %H.%M:%S.%f", 116 | "%d/%m/%y %H.%M:%S.%f", 117 | "%d-%m-%Y %H:%M", 118 | "%d-%m-%y %H:%M", 119 | "%Y-%m-%d %H:%M", 120 | "%d/%m/%Y %H:%M", 121 | "%d/%m/%y %H:%M", 122 | "%Y/%m/%d %H:%M", 123 | "%d-%m-%Y %H.%M", 124 | "%d-%m-%y %H.%M", 125 | "%d/%m/%Y %H.%M", 126 | "%d/%m/%y %H.%M", 127 | "%Y-%m-%d" 128 | ], 129 | "DATE_FORMAT": "j F Y", 130 | "DATE_INPUT_FORMATS": [ 131 | "%d-%m-%Y", 132 | "%d-%m-%y", 133 | "%d/%m/%Y", 134 | "%d/%m/%y", 135 | "%Y/%m/%d", 136 | "%Y-%m-%d" 137 | ], 138 | "DECIMAL_SEPARATOR": ",", 139 | "FIRST_DAY_OF_WEEK": 1, 140 | "MONTH_DAY_FORMAT": "j F", 141 | "NUMBER_GROUPING": 3, 142 | "SHORT_DATETIME_FORMAT": "j-n-Y H:i", 143 | "SHORT_DATE_FORMAT": "j-n-Y", 144 | "THOUSAND_SEPARATOR": ".", 145 | "TIME_FORMAT": "H:i", 146 | "TIME_INPUT_FORMATS": [ 147 | "%H:%M:%S", 148 | "%H:%M:%S.%f", 149 | "%H.%M:%S", 150 | "%H.%M:%S.%f", 151 | "%H.%M", 152 | "%H:%M" 153 | ], 154 | "YEAR_MONTH_FORMAT": "F Y" 155 | }; 156 | 157 | django.get_format = function(format_type) { 158 | const value = django.formats[format_type]; 159 | if (typeof value === 'undefined') { 160 | return format_type; 161 | } else { 162 | return value; 163 | } 164 | }; 165 | 166 | /* add to global namespace */ 167 | globals.pluralidx = django.pluralidx; 168 | globals.gettext = django.gettext; 169 | globals.ngettext = django.ngettext; 170 | globals.gettext_noop = django.gettext_noop; 171 | globals.pgettext = django.pgettext; 172 | globals.npgettext = django.npgettext; 173 | globals.interpolate = django.interpolate; 174 | globals.get_format = django.get_format; 175 | 176 | django.jsi18n_initialized = true; 177 | } 178 | }; 179 | 180 | 181 | } 182 | }; 183 | DragAndDropI18N.init(); 184 | global.DragAndDropI18N = DragAndDropI18N; 185 | }(this)); 186 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Imports ########################################################### 4 | 5 | import os 6 | import re 7 | import sys 8 | 9 | from setuptools import find_packages, setup 10 | 11 | 12 | def load_requirements(*requirements_paths): 13 | """ 14 | Load all requirements from the specified requirements files. 15 | 16 | Requirements will include any constraints from files specified 17 | with -c in the requirements files. 18 | Returns a list of requirement strings. 19 | """ 20 | # UPDATED VIA SEMGREP - if you need to remove/modify this method remove this line and add a comment specifying why. 21 | 22 | # e.g. {"django": "Django", "confluent-kafka": "confluent_kafka[avro]"} 23 | by_canonical_name = {} 24 | 25 | def check_name_consistent(package): 26 | """ 27 | Raise exception if package is named different ways. 28 | 29 | This ensures that packages are named consistently so we can match 30 | constraints to packages. It also ensures that if we require a package 31 | with extras we don't constrain it without mentioning the extras (since 32 | that too would interfere with matching constraints.) 33 | """ 34 | canonical = package.lower().replace('_', '-').split('[')[0] 35 | seen_spelling = by_canonical_name.get(canonical) 36 | if seen_spelling is None: 37 | by_canonical_name[canonical] = package 38 | elif seen_spelling != package: 39 | raise Exception( 40 | f'Encountered both "{seen_spelling}" and "{package}" in requirements ' 41 | 'and constraints files; please use just one or the other.' 42 | ) 43 | 44 | requirements = {} 45 | constraint_files = set() 46 | 47 | # groups "pkg<=x.y.z,..." into ("pkg", "<=x.y.z,...") 48 | re_package_name_base_chars = r"a-zA-Z0-9\-_." # chars allowed in base package name 49 | # Two groups: name[maybe,extras], and optionally a constraint 50 | requirement_line_regex = re.compile( 51 | r"([%s]+(?:\[[%s,\s]+\])?)([<>=][^#\s]+)?" 52 | % (re_package_name_base_chars, re_package_name_base_chars) 53 | ) 54 | 55 | def add_version_constraint_or_raise(current_line, current_requirements, add_if_not_present): 56 | regex_match = requirement_line_regex.match(current_line) 57 | if regex_match: 58 | package = regex_match.group(1) 59 | version_constraints = regex_match.group(2) 60 | check_name_consistent(package) 61 | existing_version_constraints = current_requirements.get(package, None) 62 | # It's fine to add constraints to an unconstrained package, 63 | # but raise an error if there are already constraints in place. 64 | if existing_version_constraints and existing_version_constraints != version_constraints: 65 | raise BaseException(f'Multiple constraint definitions found for {package}:' 66 | f' "{existing_version_constraints}" and "{version_constraints}".' 67 | f'Combine constraints into one location with {package}' 68 | f'{existing_version_constraints},{version_constraints}.') 69 | if add_if_not_present or package in current_requirements: 70 | current_requirements[package] = version_constraints 71 | 72 | # Read requirements from .in files and store the path to any 73 | # constraint files that are pulled in. 74 | for path in requirements_paths: 75 | with open(path) as reqs: 76 | for line in reqs: 77 | if is_requirement(line): 78 | add_version_constraint_or_raise(line, requirements, True) 79 | if line and line.startswith('-c') and not line.startswith('-c http'): 80 | constraint_files.add(os.path.dirname(path) + '/' + line.split('#')[0].replace('-c', '').strip()) 81 | 82 | # process constraint files: add constraints to existing requirements 83 | for constraint_file in constraint_files: 84 | with open(constraint_file) as reader: 85 | for line in reader: 86 | if is_requirement(line): 87 | add_version_constraint_or_raise(line, requirements, False) 88 | 89 | # process back into list of pkg><=constraints strings 90 | constrained_requirements = [f'{pkg}{version or ""}' for (pkg, version) in sorted(requirements.items())] 91 | return constrained_requirements 92 | 93 | 94 | def is_requirement(line): 95 | """ 96 | Return True if the requirement line is a package requirement. 97 | 98 | Returns: 99 | bool: True if the line is not blank, a comment, 100 | a URL, or an included file 101 | """ 102 | # UPDATED VIA SEMGREP - if you need to remove/modify this method remove this line and add a comment specifying why 103 | 104 | return line and line.strip() and not line.startswith(('-r', '#', '-e', 'git+', '-c')) 105 | 106 | 107 | def get_version(*file_paths): 108 | """ 109 | Extract the version string from the file. 110 | Input: 111 | - file_paths: relative path fragments to file with 112 | version string 113 | """ 114 | filename = os.path.join(os.path.dirname(__file__), *file_paths) 115 | version_file = open(filename, encoding="utf8").read() 116 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) 117 | if version_match: 118 | return version_match.group(1) 119 | raise RuntimeError('Unable to find version string.') 120 | 121 | 122 | def package_data(pkg, root_list): 123 | """Generic function to find package_data for `pkg` under `root`.""" 124 | data = [] 125 | for root in root_list: 126 | for dirname, _, files in os.walk(os.path.join(pkg, root)): 127 | for fname in files: 128 | data.append(os.path.relpath(os.path.join(dirname, fname), pkg)) 129 | 130 | return {pkg: data} 131 | 132 | 133 | VERSION = get_version('drag_and_drop_v2', '__init__.py') 134 | 135 | if sys.argv[-1] == 'tag': 136 | print("Tagging the version on GitHub:") 137 | os.system("git tag -a %s -m 'version %s'" % (VERSION, VERSION)) 138 | os.system("git push --tags") 139 | sys.exit() 140 | 141 | README = open(os.path.join(os.path.dirname(__file__), 'README.md'), encoding="utf8").read() 142 | CHANGELOG = open(os.path.join(os.path.dirname(__file__), 'Changelog.md'), encoding="utf8").read() 143 | 144 | setup( 145 | name='xblock-drag-and-drop-v2', 146 | version=VERSION, 147 | description='XBlock - Drag-and-Drop v2', 148 | long_description=README + '\n\n' + CHANGELOG, 149 | long_description_content_type='text/markdown', 150 | classifiers=[ 151 | 'Programming Language :: Python', 152 | 'Programming Language :: Python :: 3.11', 153 | 'Programming Language :: Python :: 3.12', 154 | 'Framework :: Django', 155 | 'Framework :: Django :: 4.2', 156 | 'Framework :: Django :: 5.2', 157 | ], 158 | url='https://github.com/openedx/xblock-drag-and-drop-v2', 159 | install_requires=load_requirements('requirements/base.in'), 160 | entry_points={ 161 | 'xblock.v1': 'drag-and-drop-v2 = drag_and_drop_v2:DragAndDropBlock', 162 | }, 163 | packages=['drag_and_drop_v2'], 164 | package_data=package_data("drag_and_drop_v2", ["static", "templates", "public", "translations"]), 165 | python_requires=">=3.11", 166 | ) 167 | -------------------------------------------------------------------------------- /drag_and_drop_v2/public/js/translations/hi/text.js: -------------------------------------------------------------------------------- 1 | 2 | (function(global){ 3 | var DragAndDropI18N = { 4 | init: function() { 5 | 6 | 7 | 'use strict'; 8 | { 9 | const globals = this; 10 | const django = globals.django || (globals.django = {}); 11 | 12 | 13 | django.pluralidx = function(n) { 14 | const v = (n != 1); 15 | if (typeof v === 'boolean') { 16 | return v ? 1 : 0; 17 | } else { 18 | return v; 19 | } 20 | }; 21 | 22 | 23 | /* gettext library */ 24 | 25 | django.catalog = django.catalog || {}; 26 | 27 | const newcatalog = { 28 | "Cancel": "\u0930\u0926\u094d\u0926 \u0915\u0930\u0947\u0902", 29 | "Close": "\u092c\u0902\u0926 \u0915\u0930\u0947", 30 | "Continue": "\u091c\u093e\u0930\u0940 \u0930\u0916\u0947\u0902", 31 | "Correct": "\u0938\u0939\u0940", 32 | "Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.": "\u0907\u0938 \u0938\u092e\u0938\u094d\u092f\u093e \u0915\u093e \u0909\u0924\u094d\u0924\u0930 \u0926\u0947\u0928\u0947 \u0915\u0947 \u0932\u093f\u090f \u090f\u0915 \u091b\u093e\u0924\u094d\u0930 \u0915\u093f\u0924\u0928\u0940 \u092c\u093e\u0930 \u0915\u094b\u0936\u093f\u0936 \u0915\u0930 \u0938\u0915\u0924\u093e \u0939\u0948, \u0907\u0938\u0947 \u092a\u0930\u093f\u092d\u093e\u0937\u093f\u0924 \u0915\u0930\u0924\u093e \u0939\u0948\u0964 \u092f\u0926\u093f \u092e\u093e\u0928 \u0938\u0947\u091f \u0928\u0939\u0940\u0902 \u0939\u0948, \u0924\u094b \u0905\u0928\u0902\u0924 \u092a\u094d\u0930\u092f\u093e\u0938\u094b\u0902 \u0915\u0940 \u0905\u0928\u0941\u092e\u0924\u093f \u0939\u0948\u0964", 33 | "Drag the items onto the image above.": "\u090a\u092a\u0930 \u0915\u0940 \u091b\u0935\u093f \u092a\u0930 \u0906\u0907\u091f\u092e \u0916\u0940\u0902\u091a\u0947\u0902\u0964", 34 | "Feedback": "\u092a\u094d\u0930\u0924\u093f\u0915\u094d\u0930\u093f\u092f\u093e", 35 | "Goes anywhere": "\u0915\u0939\u0940\u0902 \u092d\u0940 \u091a\u0932\u093e \u091c\u093e\u0924\u093e \u0939\u0948 |", 36 | "I don't belong anywhere": "\u092e\u0948\u0902 \u0915\u0939\u0940\u0902 \u092d\u0940 \u0928\u0939\u0940\u0902 \u0939\u0942\u0902", 37 | "Incorrect": "\u0917\u093c\u0932\u0924", 38 | "Items": "\u0907\u091f\u0947\u092e\u094d\u0938 ", 39 | "Mode": "\u092a\u094d\u0930\u0923\u093e\u0932\u0940", 40 | "No, this item does not belong here. Try again.": "\u0928\u0939\u0940\u0902, \u092f\u0939 \u0906\u0907\u091f\u092e \u092f\u0939\u093e\u0901 \u0928\u0939\u0940\u0902 \u0939\u0948\u0964 \u092a\u0941\u0928\u0903 \u092a\u094d\u0930\u092f\u093e\u0938 \u0915\u0930\u0947\u0902\u0964", 41 | "Of course it goes here! It goes anywhere!": "\u0915\u0939\u0940\u0902 \u092d\u0940 \u091a\u0932\u093e \u091c\u093e\u0924\u093e \u0939\u0948 |", 42 | "Problem": "\u0938\u092e\u0938\u094d\u092f\u093e", 43 | "Problem Weight": "\u0938\u092e\u0938\u094d\u092f\u093e \u0935\u091c\u0928", 44 | "Save": "\u0938\u0947\u0935 \u0915\u0930\u0947\u0902", 45 | "Saving": "\u0938\u0947\u0935 \u0939\u094b \u0930\u0939\u093e \u0939\u0948", 46 | "Submit": "\u092a\u094d\u0930\u0938\u094d\u0924\u0941\u0924", 47 | "Submitting": "\u092d\u0947\u091c\u0928\u0947 \u0938\u0947", 48 | "The Top Zone": "\u0936\u0940\u0930\u094d\u0937 \u0915\u094d\u0937\u0947\u0924\u094d\u0930", 49 | "Title": "\u0936\u0940\u0930\u094d\u0937\u0915", 50 | "You silly, there are no zones for this one.": "\u0906\u092a \u092e\u0942\u0930\u094d\u0916\u0924\u093e\u092a\u0942\u0930\u094d\u0923 \u0939\u0948\u0902, \u0907\u0938\u0915\u0947 \u0932\u093f\u090f \u0915\u094b\u0908 \u0915\u094d\u0937\u0947\u0924\u094d\u0930 \u0928\u0939\u0940\u0902 \u0939\u0948\u0964" 51 | }; 52 | for (const key in newcatalog) { 53 | django.catalog[key] = newcatalog[key]; 54 | } 55 | 56 | 57 | if (!django.jsi18n_initialized) { 58 | django.gettext = function(msgid) { 59 | const value = django.catalog[msgid]; 60 | if (typeof value === 'undefined') { 61 | return msgid; 62 | } else { 63 | return (typeof value === 'string') ? value : value[0]; 64 | } 65 | }; 66 | 67 | django.ngettext = function(singular, plural, count) { 68 | const value = django.catalog[singular]; 69 | if (typeof value === 'undefined') { 70 | return (count == 1) ? singular : plural; 71 | } else { 72 | return value.constructor === Array ? value[django.pluralidx(count)] : value; 73 | } 74 | }; 75 | 76 | django.gettext_noop = function(msgid) { return msgid; }; 77 | 78 | django.pgettext = function(context, msgid) { 79 | let value = django.gettext(context + '\x04' + msgid); 80 | if (value.includes('\x04')) { 81 | value = msgid; 82 | } 83 | return value; 84 | }; 85 | 86 | django.npgettext = function(context, singular, plural, count) { 87 | let value = django.ngettext(context + '\x04' + singular, context + '\x04' + plural, count); 88 | if (value.includes('\x04')) { 89 | value = django.ngettext(singular, plural, count); 90 | } 91 | return value; 92 | }; 93 | 94 | django.interpolate = function(fmt, obj, named) { 95 | if (named) { 96 | return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])}); 97 | } else { 98 | return fmt.replace(/%s/g, function(match){return String(obj.shift())}); 99 | } 100 | }; 101 | 102 | 103 | /* formatting library */ 104 | 105 | django.formats = { 106 | "DATETIME_FORMAT": "N j, Y, P", 107 | "DATETIME_INPUT_FORMATS": [ 108 | "%Y-%m-%d %H:%M:%S", 109 | "%Y-%m-%d %H:%M:%S.%f", 110 | "%Y-%m-%d %H:%M", 111 | "%m/%d/%Y %H:%M:%S", 112 | "%m/%d/%Y %H:%M:%S.%f", 113 | "%m/%d/%Y %H:%M", 114 | "%m/%d/%y %H:%M:%S", 115 | "%m/%d/%y %H:%M:%S.%f", 116 | "%m/%d/%y %H:%M" 117 | ], 118 | "DATE_FORMAT": "j F Y", 119 | "DATE_INPUT_FORMATS": [ 120 | "%Y-%m-%d", 121 | "%m/%d/%Y", 122 | "%m/%d/%y", 123 | "%b %d %Y", 124 | "%b %d, %Y", 125 | "%d %b %Y", 126 | "%d %b, %Y", 127 | "%B %d %Y", 128 | "%B %d, %Y", 129 | "%d %B %Y", 130 | "%d %B, %Y" 131 | ], 132 | "DECIMAL_SEPARATOR": ".", 133 | "FIRST_DAY_OF_WEEK": 0, 134 | "MONTH_DAY_FORMAT": "j F", 135 | "NUMBER_GROUPING": 0, 136 | "SHORT_DATETIME_FORMAT": "m/d/Y P", 137 | "SHORT_DATE_FORMAT": "d-m-Y", 138 | "THOUSAND_SEPARATOR": ",", 139 | "TIME_FORMAT": "g:i A", 140 | "TIME_INPUT_FORMATS": [ 141 | "%H:%M:%S", 142 | "%H:%M:%S.%f", 143 | "%H:%M" 144 | ], 145 | "YEAR_MONTH_FORMAT": "F Y" 146 | }; 147 | 148 | django.get_format = function(format_type) { 149 | const value = django.formats[format_type]; 150 | if (typeof value === 'undefined') { 151 | return format_type; 152 | } else { 153 | return value; 154 | } 155 | }; 156 | 157 | /* add to global namespace */ 158 | globals.pluralidx = django.pluralidx; 159 | globals.gettext = django.gettext; 160 | globals.ngettext = django.ngettext; 161 | globals.gettext_noop = django.gettext_noop; 162 | globals.pgettext = django.pgettext; 163 | globals.npgettext = django.npgettext; 164 | globals.interpolate = django.interpolate; 165 | globals.get_format = django.get_format; 166 | 167 | django.jsi18n_initialized = true; 168 | } 169 | }; 170 | 171 | 172 | } 173 | }; 174 | DragAndDropI18N.init(); 175 | global.DragAndDropI18N = DragAndDropI18N; 176 | }(this)); 177 | -------------------------------------------------------------------------------- /requirements/quality.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | appdirs==1.4.4 8 | # via 9 | # -r requirements/test.txt 10 | # fs 11 | arrow==1.4.0 12 | # via 13 | # -r requirements/test.txt 14 | # cookiecutter 15 | asgiref==3.11.0 16 | # via 17 | # -r requirements/test.txt 18 | # django 19 | astroid==3.3.11 20 | # via 21 | # pylint 22 | # pylint-celery 23 | binaryornot==0.4.4 24 | # via 25 | # -r requirements/test.txt 26 | # cookiecutter 27 | bleach[css]==6.3.0 28 | # via -r requirements/test.txt 29 | boto3==1.42.4 30 | # via 31 | # -r requirements/test.txt 32 | # fs-s3fs 33 | botocore==1.42.4 34 | # via 35 | # -r requirements/test.txt 36 | # boto3 37 | # s3transfer 38 | certifi==2025.11.12 39 | # via 40 | # -r requirements/test.txt 41 | # requests 42 | chardet==5.2.0 43 | # via 44 | # -r requirements/test.txt 45 | # binaryornot 46 | charset-normalizer==3.4.4 47 | # via 48 | # -r requirements/test.txt 49 | # requests 50 | click==8.3.1 51 | # via 52 | # -r requirements/test.txt 53 | # click-log 54 | # code-annotations 55 | # cookiecutter 56 | # edx-lint 57 | click-log==0.4.0 58 | # via edx-lint 59 | code-annotations==2.3.0 60 | # via edx-lint 61 | cookiecutter==2.6.0 62 | # via 63 | # -r requirements/test.txt 64 | # xblock-sdk 65 | coverage[toml]==7.12.0 66 | # via 67 | # -r requirements/test.txt 68 | # pytest-cov 69 | ddt==1.7.2 70 | # via -r requirements/test.txt 71 | dill==0.4.0 72 | # via pylint 73 | django==5.2.9 74 | # via 75 | # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt 76 | # -r requirements/test.txt 77 | # django-appconf 78 | # django-statici18n 79 | # edx-i18n-tools 80 | # openedx-django-pyfs 81 | # xblock-sdk 82 | django-appconf==1.2.0 83 | # via 84 | # -r requirements/test.txt 85 | # django-statici18n 86 | django-statici18n==2.6.0 87 | # via -r requirements/test.txt 88 | edx-i18n-tools==1.9.0 89 | # via -r requirements/test.txt 90 | edx-lint==5.6.0 91 | # via -r requirements/quality.in 92 | fs==2.4.16 93 | # via 94 | # -r requirements/test.txt 95 | # fs-s3fs 96 | # openedx-django-pyfs 97 | # xblock 98 | fs-s3fs==1.1.1 99 | # via 100 | # -r requirements/test.txt 101 | # openedx-django-pyfs 102 | # xblock-sdk 103 | idna==3.11 104 | # via 105 | # -r requirements/test.txt 106 | # requests 107 | iniconfig==2.3.0 108 | # via 109 | # -r requirements/test.txt 110 | # pytest 111 | isort==6.1.0 112 | # via pylint 113 | jinja2==3.1.6 114 | # via 115 | # -r requirements/test.txt 116 | # code-annotations 117 | # cookiecutter 118 | jmespath==1.0.1 119 | # via 120 | # -r requirements/test.txt 121 | # boto3 122 | # botocore 123 | lazy==1.6 124 | # via 125 | # -r requirements/test.txt 126 | # xblock 127 | lxml[html-clean]==6.0.2 128 | # via 129 | # -r requirements/test.txt 130 | # edx-i18n-tools 131 | # lxml-html-clean 132 | # xblock 133 | # xblock-sdk 134 | lxml-html-clean==0.4.3 135 | # via 136 | # -r requirements/test.txt 137 | # lxml 138 | mako==1.3.10 139 | # via 140 | # -r requirements/test.txt 141 | # xblock 142 | markdown-it-py==4.0.0 143 | # via 144 | # -r requirements/test.txt 145 | # rich 146 | markupsafe==3.0.3 147 | # via 148 | # -r requirements/test.txt 149 | # jinja2 150 | # mako 151 | # xblock 152 | mccabe==0.7.0 153 | # via pylint 154 | mdurl==0.1.2 155 | # via 156 | # -r requirements/test.txt 157 | # markdown-it-py 158 | mock==5.2.0 159 | # via -r requirements/test.txt 160 | openedx-django-pyfs==3.8.0 161 | # via 162 | # -r requirements/test.txt 163 | # xblock 164 | packaging==25.0 165 | # via 166 | # -r requirements/test.txt 167 | # pytest 168 | path==16.16.0 169 | # via 170 | # -r requirements/test.txt 171 | # edx-i18n-tools 172 | platformdirs==4.5.1 173 | # via pylint 174 | pluggy==1.6.0 175 | # via 176 | # -r requirements/test.txt 177 | # pytest 178 | # pytest-cov 179 | polib==1.2.0 180 | # via 181 | # -r requirements/test.txt 182 | # edx-i18n-tools 183 | pycodestyle==2.14.0 184 | # via -r requirements/quality.in 185 | pygments==2.19.2 186 | # via 187 | # -r requirements/test.txt 188 | # pytest 189 | # rich 190 | pylint==3.3.9 191 | # via 192 | # edx-lint 193 | # pylint-celery 194 | # pylint-django 195 | # pylint-plugin-utils 196 | pylint-celery==0.3 197 | # via edx-lint 198 | pylint-django==2.6.1 199 | # via edx-lint 200 | pylint-plugin-utils==0.9.0 201 | # via 202 | # pylint-celery 203 | # pylint-django 204 | pypng==0.20220715.0 205 | # via 206 | # -r requirements/test.txt 207 | # xblock-sdk 208 | pytest==9.0.2 209 | # via 210 | # -r requirements/test.txt 211 | # pytest-cov 212 | # pytest-django 213 | pytest-cov==7.0.0 214 | # via -r requirements/test.txt 215 | pytest-django==4.11.1 216 | # via -r requirements/test.txt 217 | python-dateutil==2.9.0.post0 218 | # via 219 | # -r requirements/test.txt 220 | # arrow 221 | # botocore 222 | # xblock 223 | python-slugify==8.0.4 224 | # via 225 | # -r requirements/test.txt 226 | # code-annotations 227 | # cookiecutter 228 | pytz==2025.2 229 | # via 230 | # -r requirements/test.txt 231 | # xblock 232 | pyyaml==6.0.3 233 | # via 234 | # -r requirements/test.txt 235 | # code-annotations 236 | # cookiecutter 237 | # edx-i18n-tools 238 | # xblock 239 | requests==2.32.5 240 | # via 241 | # -r requirements/test.txt 242 | # cookiecutter 243 | # xblock-sdk 244 | rich==14.2.0 245 | # via 246 | # -r requirements/test.txt 247 | # cookiecutter 248 | s3transfer==0.16.0 249 | # via 250 | # -r requirements/test.txt 251 | # boto3 252 | simplejson==3.20.2 253 | # via 254 | # -r requirements/test.txt 255 | # xblock 256 | # xblock-sdk 257 | six==1.17.0 258 | # via 259 | # -r requirements/test.txt 260 | # edx-lint 261 | # fs 262 | # fs-s3fs 263 | # python-dateutil 264 | sqlparse==0.5.4 265 | # via 266 | # -r requirements/test.txt 267 | # django 268 | stevedore==5.6.0 269 | # via code-annotations 270 | text-unidecode==1.3 271 | # via 272 | # -r requirements/test.txt 273 | # python-slugify 274 | tinycss2==1.4.0 275 | # via 276 | # -r requirements/test.txt 277 | # bleach 278 | tomlkit==0.13.3 279 | # via pylint 280 | tzdata==2025.2 281 | # via 282 | # -r requirements/test.txt 283 | # arrow 284 | urllib3==2.6.0 285 | # via 286 | # -r requirements/test.txt 287 | # botocore 288 | # requests 289 | web-fragments==3.1.0 290 | # via 291 | # -r requirements/test.txt 292 | # xblock 293 | # xblock-sdk 294 | webencodings==0.5.1 295 | # via 296 | # -r requirements/test.txt 297 | # bleach 298 | # tinycss2 299 | webob==1.8.9 300 | # via 301 | # -r requirements/test.txt 302 | # xblock 303 | # xblock-sdk 304 | xblock[django]==5.2.0 305 | # via 306 | # -r requirements/test.txt 307 | # xblock-sdk 308 | xblock-sdk==0.13.0 309 | # via -r requirements/test.txt 310 | 311 | # The following packages are considered to be unsafe in a requirements file: 312 | # setuptools 313 | -------------------------------------------------------------------------------- /drag_and_drop_v2/public/js/translations/it/text.js: -------------------------------------------------------------------------------- 1 | 2 | (function(global){ 3 | var DragAndDropI18N = { 4 | init: function() { 5 | 6 | 7 | 'use strict'; 8 | { 9 | const globals = this; 10 | const django = globals.django || (globals.django = {}); 11 | 12 | 13 | django.pluralidx = function(n) { 14 | const v = n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2; 15 | if (typeof v === 'boolean') { 16 | return v ? 1 : 0; 17 | } else { 18 | return v; 19 | } 20 | }; 21 | 22 | 23 | /* gettext library */ 24 | 25 | django.catalog = django.catalog || {}; 26 | 27 | const newcatalog = { 28 | "Add a zone": "Aggiungi una zona", 29 | "Add an item": "Aggiungi un oggetto", 30 | "Assessment": "Valutazione", 31 | "Background URL": "URL di sfondo", 32 | "Background description": "Descrizione sfondo", 33 | "Cancel": "Annulla", 34 | "Change background": "Cambia sfondo", 35 | "Continue": "Continua", 36 | "Correct": "Corretto", 37 | "Correctly placed {correct_count} item": [ 38 | "{correct_count} elementi posizionati correttamente", 39 | "{correct_count} elementi posizionati correttamente", 40 | "{correct_count} elementi posizionati correttamente" 41 | ], 42 | "Defines the number of points the problem is worth.": "Definisce il numero di punto che il problema vale.", 43 | "Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.": "Definisce il numero di volte in cui uno studente pu\u00f2 provare a rispondere a questo problema. Se il valore non \u00e8 impostato, vengono consentiti infiniti tentativi.", 44 | "Display label names on the image": "Visualizza i nomi delle etichette sull'immagine", 45 | "Display the title to the learner?": "Mostra il titolo al discente?", 46 | "Drag and Drop": "Trascina e Rilascia", 47 | "Explanation": "Spiegazione", 48 | "Final feedback": "Feedback finale", 49 | "Hints:": "Suggerimenti:", 50 | "Incorrect": "Errato", 51 | "Introductory feedback": "Feedback introduttivo", 52 | "Item background color": "Colore di sfondo dell'oggetto", 53 | "Item text color": "Coloro del testo dell'oggetto", 54 | "Items": "Oggetti", 55 | "Loading drag and drop problem.": "Caricamento del problema di drag and drop.", 56 | "Max number of attempts reached": "Numero massimo di tentativi raggiunto", 57 | "Maximum attempts": "Tentativi massimi", 58 | "Misplaced {misplaced_count} item (misplaced item was returned to the item bank)": [ 59 | "{misplaced_count} elementi fuori posto. Gli oggetti smarriti sono stati restituiti alla banca degli articoli.", 60 | "{misplaced_count} elementi fuori posto. Gli oggetti smarriti sono stati restituiti alla banca degli articoli.", 61 | "{misplaced_count} elementi fuori posto. Gli oggetti smarriti sono stati restituiti alla banca degli articoli." 62 | ], 63 | "Mode": "Modalit\u00e0", 64 | "Number of attempts learner used": "Numero di tentativi che il discente ha utilizzato", 65 | "Problem": "Problema", 66 | "Problem Weight": "Peso del problema", 67 | "Problem data": "Dato del problema", 68 | "Problem text": "Testo del problema", 69 | "Save": "Salva", 70 | "Saving": "Salvataggio in corso", 71 | "Show title": "Mostra titolo", 72 | "Some of your answers were not correct.": "Alcune delle tue risposte non erano corrette.", 73 | "Standard": "Standard", 74 | "Submitting": "In fase di invio", 75 | "The description of the problem or instructions shown to the learner.": "Descrizione del problema o istruzioni mostrate al discente.", 76 | "Title": "Titolo", 77 | "Your highest score is {score}": "Il tuo punteggio pi\u00f9 alto \u00e8 {score}", 78 | "Zone borders": "Bordi della zona", 79 | "Zone definitions": "Definizioni della zona", 80 | "Zone labels": "Etichette della zona", 81 | "Zones": "Zone" 82 | }; 83 | for (const key in newcatalog) { 84 | django.catalog[key] = newcatalog[key]; 85 | } 86 | 87 | 88 | if (!django.jsi18n_initialized) { 89 | django.gettext = function(msgid) { 90 | const value = django.catalog[msgid]; 91 | if (typeof value === 'undefined') { 92 | return msgid; 93 | } else { 94 | return (typeof value === 'string') ? value : value[0]; 95 | } 96 | }; 97 | 98 | django.ngettext = function(singular, plural, count) { 99 | const value = django.catalog[singular]; 100 | if (typeof value === 'undefined') { 101 | return (count == 1) ? singular : plural; 102 | } else { 103 | return value.constructor === Array ? value[django.pluralidx(count)] : value; 104 | } 105 | }; 106 | 107 | django.gettext_noop = function(msgid) { return msgid; }; 108 | 109 | django.pgettext = function(context, msgid) { 110 | let value = django.gettext(context + '\x04' + msgid); 111 | if (value.includes('\x04')) { 112 | value = msgid; 113 | } 114 | return value; 115 | }; 116 | 117 | django.npgettext = function(context, singular, plural, count) { 118 | let value = django.ngettext(context + '\x04' + singular, context + '\x04' + plural, count); 119 | if (value.includes('\x04')) { 120 | value = django.ngettext(singular, plural, count); 121 | } 122 | return value; 123 | }; 124 | 125 | django.interpolate = function(fmt, obj, named) { 126 | if (named) { 127 | return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])}); 128 | } else { 129 | return fmt.replace(/%s/g, function(match){return String(obj.shift())}); 130 | } 131 | }; 132 | 133 | 134 | /* formatting library */ 135 | 136 | django.formats = { 137 | "DATETIME_FORMAT": "l d F Y H:i", 138 | "DATETIME_INPUT_FORMATS": [ 139 | "%d/%m/%Y %H:%M:%S", 140 | "%d/%m/%Y %H:%M:%S.%f", 141 | "%d/%m/%Y %H:%M", 142 | "%d/%m/%y %H:%M:%S", 143 | "%d/%m/%y %H:%M:%S.%f", 144 | "%d/%m/%y %H:%M", 145 | "%Y-%m-%d %H:%M:%S", 146 | "%Y-%m-%d %H:%M:%S.%f", 147 | "%Y-%m-%d %H:%M", 148 | "%d-%m-%Y %H:%M:%S", 149 | "%d-%m-%Y %H:%M:%S.%f", 150 | "%d-%m-%Y %H:%M", 151 | "%d-%m-%y %H:%M:%S", 152 | "%d-%m-%y %H:%M:%S.%f", 153 | "%d-%m-%y %H:%M", 154 | "%Y-%m-%d" 155 | ], 156 | "DATE_FORMAT": "d F Y", 157 | "DATE_INPUT_FORMATS": [ 158 | "%d/%m/%Y", 159 | "%Y/%m/%d", 160 | "%d-%m-%Y", 161 | "%Y-%m-%d", 162 | "%d-%m-%y", 163 | "%d/%m/%y" 164 | ], 165 | "DECIMAL_SEPARATOR": ",", 166 | "FIRST_DAY_OF_WEEK": 1, 167 | "MONTH_DAY_FORMAT": "j F", 168 | "NUMBER_GROUPING": 3, 169 | "SHORT_DATETIME_FORMAT": "d/m/Y H:i", 170 | "SHORT_DATE_FORMAT": "d/m/Y", 171 | "THOUSAND_SEPARATOR": ".", 172 | "TIME_FORMAT": "H:i", 173 | "TIME_INPUT_FORMATS": [ 174 | "%H:%M:%S", 175 | "%H:%M:%S.%f", 176 | "%H:%M" 177 | ], 178 | "YEAR_MONTH_FORMAT": "F Y" 179 | }; 180 | 181 | django.get_format = function(format_type) { 182 | const value = django.formats[format_type]; 183 | if (typeof value === 'undefined') { 184 | return format_type; 185 | } else { 186 | return value; 187 | } 188 | }; 189 | 190 | /* add to global namespace */ 191 | globals.pluralidx = django.pluralidx; 192 | globals.gettext = django.gettext; 193 | globals.ngettext = django.ngettext; 194 | globals.gettext_noop = django.gettext_noop; 195 | globals.pgettext = django.pgettext; 196 | globals.npgettext = django.npgettext; 197 | globals.interpolate = django.interpolate; 198 | globals.get_format = django.get_format; 199 | 200 | django.jsi18n_initialized = true; 201 | } 202 | }; 203 | 204 | 205 | } 206 | }; 207 | DragAndDropI18N.init(); 208 | global.DragAndDropI18N = DragAndDropI18N; 209 | }(this)); 210 | -------------------------------------------------------------------------------- /drag_and_drop_v2/templates/html/js_templates.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 87 | 88 | 99 | 100 | 183 | 184 | 201 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | appdirs==1.4.4 8 | # via 9 | # -r requirements/quality.txt 10 | # fs 11 | arrow==1.4.0 12 | # via 13 | # -r requirements/quality.txt 14 | # cookiecutter 15 | asgiref==3.11.0 16 | # via 17 | # -r requirements/quality.txt 18 | # django 19 | astroid==3.3.11 20 | # via 21 | # -r requirements/quality.txt 22 | # pylint 23 | # pylint-celery 24 | binaryornot==0.4.4 25 | # via 26 | # -r requirements/quality.txt 27 | # cookiecutter 28 | bleach[css]==6.3.0 29 | # via -r requirements/quality.txt 30 | boto3==1.42.4 31 | # via 32 | # -r requirements/quality.txt 33 | # fs-s3fs 34 | botocore==1.42.4 35 | # via 36 | # -r requirements/quality.txt 37 | # boto3 38 | # s3transfer 39 | build==1.3.0 40 | # via 41 | # -r requirements/pip-tools.txt 42 | # pip-tools 43 | cachetools==6.2.2 44 | # via 45 | # -r requirements/ci.txt 46 | # tox 47 | certifi==2025.11.12 48 | # via 49 | # -r requirements/quality.txt 50 | # requests 51 | chardet==5.2.0 52 | # via 53 | # -r requirements/ci.txt 54 | # -r requirements/quality.txt 55 | # binaryornot 56 | # tox 57 | charset-normalizer==3.4.4 58 | # via 59 | # -r requirements/quality.txt 60 | # requests 61 | click==8.3.1 62 | # via 63 | # -r requirements/pip-tools.txt 64 | # -r requirements/quality.txt 65 | # click-log 66 | # code-annotations 67 | # cookiecutter 68 | # edx-lint 69 | # pip-tools 70 | click-log==0.4.0 71 | # via 72 | # -r requirements/quality.txt 73 | # edx-lint 74 | code-annotations==2.3.0 75 | # via 76 | # -r requirements/quality.txt 77 | # edx-lint 78 | colorama==0.4.6 79 | # via 80 | # -r requirements/ci.txt 81 | # tox 82 | cookiecutter==2.6.0 83 | # via 84 | # -r requirements/quality.txt 85 | # xblock-sdk 86 | coverage[toml]==7.12.0 87 | # via 88 | # -r requirements/quality.txt 89 | # pytest-cov 90 | ddt==1.7.2 91 | # via -r requirements/quality.txt 92 | dill==0.4.0 93 | # via 94 | # -r requirements/quality.txt 95 | # pylint 96 | distlib==0.4.0 97 | # via 98 | # -r requirements/ci.txt 99 | # virtualenv 100 | django==5.2.9 101 | # via 102 | # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt 103 | # -r requirements/quality.txt 104 | # django-appconf 105 | # django-statici18n 106 | # edx-i18n-tools 107 | # openedx-django-pyfs 108 | # xblock-sdk 109 | django-appconf==1.2.0 110 | # via 111 | # -r requirements/quality.txt 112 | # django-statici18n 113 | django-statici18n==2.6.0 114 | # via -r requirements/quality.txt 115 | edx-i18n-tools==1.9.0 116 | # via -r requirements/quality.txt 117 | edx-lint==5.6.0 118 | # via -r requirements/quality.txt 119 | filelock==3.20.0 120 | # via 121 | # -r requirements/ci.txt 122 | # tox 123 | # virtualenv 124 | fs==2.4.16 125 | # via 126 | # -r requirements/quality.txt 127 | # fs-s3fs 128 | # openedx-django-pyfs 129 | # xblock 130 | fs-s3fs==1.1.1 131 | # via 132 | # -r requirements/quality.txt 133 | # openedx-django-pyfs 134 | # xblock-sdk 135 | idna==3.11 136 | # via 137 | # -r requirements/quality.txt 138 | # requests 139 | iniconfig==2.3.0 140 | # via 141 | # -r requirements/quality.txt 142 | # pytest 143 | isort==6.1.0 144 | # via 145 | # -r requirements/quality.txt 146 | # pylint 147 | jinja2==3.1.6 148 | # via 149 | # -r requirements/quality.txt 150 | # code-annotations 151 | # cookiecutter 152 | jmespath==1.0.1 153 | # via 154 | # -r requirements/quality.txt 155 | # boto3 156 | # botocore 157 | lazy==1.6 158 | # via 159 | # -r requirements/quality.txt 160 | # xblock 161 | lxml[html-clean]==6.0.2 162 | # via 163 | # -r requirements/quality.txt 164 | # edx-i18n-tools 165 | # lxml-html-clean 166 | # xblock 167 | # xblock-sdk 168 | lxml-html-clean==0.4.3 169 | # via 170 | # -r requirements/quality.txt 171 | # lxml 172 | mako==1.3.10 173 | # via 174 | # -r requirements/quality.txt 175 | # xblock 176 | markdown-it-py==4.0.0 177 | # via 178 | # -r requirements/quality.txt 179 | # rich 180 | markupsafe==3.0.3 181 | # via 182 | # -r requirements/quality.txt 183 | # jinja2 184 | # mako 185 | # xblock 186 | mccabe==0.7.0 187 | # via 188 | # -r requirements/quality.txt 189 | # pylint 190 | mdurl==0.1.2 191 | # via 192 | # -r requirements/quality.txt 193 | # markdown-it-py 194 | mock==5.2.0 195 | # via -r requirements/quality.txt 196 | openedx-django-pyfs==3.8.0 197 | # via 198 | # -r requirements/quality.txt 199 | # xblock 200 | packaging==25.0 201 | # via 202 | # -r requirements/ci.txt 203 | # -r requirements/pip-tools.txt 204 | # -r requirements/quality.txt 205 | # build 206 | # pyproject-api 207 | # pytest 208 | # tox 209 | path==16.16.0 210 | # via 211 | # -r requirements/quality.txt 212 | # edx-i18n-tools 213 | pip-tools==7.5.2 214 | # via -r requirements/pip-tools.txt 215 | platformdirs==4.5.1 216 | # via 217 | # -r requirements/ci.txt 218 | # -r requirements/quality.txt 219 | # pylint 220 | # tox 221 | # virtualenv 222 | pluggy==1.6.0 223 | # via 224 | # -r requirements/ci.txt 225 | # -r requirements/quality.txt 226 | # pytest 227 | # pytest-cov 228 | # tox 229 | polib==1.2.0 230 | # via 231 | # -r requirements/quality.txt 232 | # edx-i18n-tools 233 | pycodestyle==2.14.0 234 | # via -r requirements/quality.txt 235 | pygments==2.19.2 236 | # via 237 | # -r requirements/quality.txt 238 | # pytest 239 | # rich 240 | pylint==3.3.9 241 | # via 242 | # -r requirements/quality.txt 243 | # edx-lint 244 | # pylint-celery 245 | # pylint-django 246 | # pylint-plugin-utils 247 | pylint-celery==0.3 248 | # via 249 | # -r requirements/quality.txt 250 | # edx-lint 251 | pylint-django==2.6.1 252 | # via 253 | # -r requirements/quality.txt 254 | # edx-lint 255 | pylint-plugin-utils==0.9.0 256 | # via 257 | # -r requirements/quality.txt 258 | # pylint-celery 259 | # pylint-django 260 | pypng==0.20220715.0 261 | # via 262 | # -r requirements/quality.txt 263 | # xblock-sdk 264 | pyproject-api==1.10.0 265 | # via 266 | # -r requirements/ci.txt 267 | # tox 268 | pyproject-hooks==1.2.0 269 | # via 270 | # -r requirements/pip-tools.txt 271 | # build 272 | # pip-tools 273 | pytest==9.0.2 274 | # via 275 | # -r requirements/quality.txt 276 | # pytest-cov 277 | # pytest-django 278 | pytest-cov==7.0.0 279 | # via -r requirements/quality.txt 280 | pytest-django==4.11.1 281 | # via -r requirements/quality.txt 282 | python-dateutil==2.9.0.post0 283 | # via 284 | # -r requirements/quality.txt 285 | # arrow 286 | # botocore 287 | # xblock 288 | python-slugify==8.0.4 289 | # via 290 | # -r requirements/quality.txt 291 | # code-annotations 292 | # cookiecutter 293 | pytz==2025.2 294 | # via 295 | # -r requirements/quality.txt 296 | # xblock 297 | pyyaml==6.0.3 298 | # via 299 | # -r requirements/quality.txt 300 | # code-annotations 301 | # cookiecutter 302 | # edx-i18n-tools 303 | # xblock 304 | requests==2.32.5 305 | # via 306 | # -r requirements/quality.txt 307 | # cookiecutter 308 | # xblock-sdk 309 | rich==14.2.0 310 | # via 311 | # -r requirements/quality.txt 312 | # cookiecutter 313 | s3transfer==0.16.0 314 | # via 315 | # -r requirements/quality.txt 316 | # boto3 317 | simplejson==3.20.2 318 | # via 319 | # -r requirements/quality.txt 320 | # xblock 321 | # xblock-sdk 322 | six==1.17.0 323 | # via 324 | # -r requirements/quality.txt 325 | # edx-lint 326 | # fs 327 | # fs-s3fs 328 | # python-dateutil 329 | sqlparse==0.5.4 330 | # via 331 | # -r requirements/quality.txt 332 | # django 333 | stevedore==5.6.0 334 | # via 335 | # -r requirements/quality.txt 336 | # code-annotations 337 | text-unidecode==1.3 338 | # via 339 | # -r requirements/quality.txt 340 | # python-slugify 341 | tinycss2==1.4.0 342 | # via 343 | # -r requirements/quality.txt 344 | # bleach 345 | tomlkit==0.13.3 346 | # via 347 | # -r requirements/quality.txt 348 | # pylint 349 | tox==4.32.0 350 | # via -r requirements/ci.txt 351 | tzdata==2025.2 352 | # via 353 | # -r requirements/quality.txt 354 | # arrow 355 | urllib3==2.6.0 356 | # via 357 | # -r requirements/quality.txt 358 | # botocore 359 | # requests 360 | virtualenv==20.35.4 361 | # via 362 | # -r requirements/ci.txt 363 | # tox 364 | web-fragments==3.1.0 365 | # via 366 | # -r requirements/quality.txt 367 | # xblock 368 | # xblock-sdk 369 | webencodings==0.5.1 370 | # via 371 | # -r requirements/quality.txt 372 | # bleach 373 | # tinycss2 374 | webob==1.8.9 375 | # via 376 | # -r requirements/quality.txt 377 | # xblock 378 | # xblock-sdk 379 | wheel==0.45.1 380 | # via 381 | # -r requirements/pip-tools.txt 382 | # pip-tools 383 | xblock[django]==5.2.0 384 | # via 385 | # -r requirements/quality.txt 386 | # xblock-sdk 387 | xblock-sdk==0.13.0 388 | # via -r requirements/quality.txt 389 | 390 | # The following packages are considered to be unsafe in a requirements file: 391 | # pip 392 | # setuptools 393 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | # *************************** 2 | # ** DO NOT EDIT THIS FILE ** 3 | # *************************** 4 | # 5 | # This file was generated by edx-lint: https://github.com/openedx/edx-lint 6 | # 7 | # If you want to change this file, you have two choices, depending on whether 8 | # you want to make a local change that applies only to this repo, or whether 9 | # you want to make a central change that applies to all repos using edx-lint. 10 | # 11 | # Note: If your pylintrc file is simply out-of-date relative to the latest 12 | # pylintrc in edx-lint, ensure you have the latest edx-lint installed 13 | # and then follow the steps for a "LOCAL CHANGE". 14 | # 15 | # LOCAL CHANGE: 16 | # 17 | # 1. Edit the local pylintrc_tweaks file to add changes just to this 18 | # repo's file. 19 | # 20 | # 2. Run: 21 | # 22 | # $ edx_lint write pylintrc 23 | # 24 | # 3. This will modify the local file. Submit a pull request to get it 25 | # checked in so that others will benefit. 26 | # 27 | # 28 | # CENTRAL CHANGE: 29 | # 30 | # 1. Edit the pylintrc file in the edx-lint repo at 31 | # https://github.com/openedx/edx-lint/blob/master/edx_lint/files/pylintrc 32 | # 33 | # 2. install the updated version of edx-lint (in edx-lint): 34 | # 35 | # $ pip install . 36 | # 37 | # 3. Run (in edx-lint): 38 | # 39 | # $ edx_lint write pylintrc 40 | # 41 | # 4. Make a new version of edx_lint, submit and review a pull request with the 42 | # pylintrc update, and after merging, update the edx-lint version and 43 | # publish the new version. 44 | # 45 | # 5. In your local repo, install the newer version of edx-lint. 46 | # 47 | # 6. Run: 48 | # 49 | # $ edx_lint write pylintrc 50 | # 51 | # 7. This will modify the local file. Submit a pull request to get it 52 | # checked in so that others will benefit. 53 | # 54 | # 55 | # 56 | # 57 | # 58 | # STAY AWAY FROM THIS FILE! 59 | # 60 | # 61 | # 62 | # 63 | # 64 | # SERIOUSLY. 65 | # 66 | # ------------------------------ 67 | # Generated by edx-lint version: 5.3.6 68 | # ------------------------------ 69 | [MASTER] 70 | ignore = migrations 71 | persistent = yes 72 | load-plugins = edx_lint.pylint,pylint_django,pylint_celery 73 | 74 | [MESSAGES CONTROL] 75 | enable = 76 | blacklisted-name, 77 | line-too-long, 78 | 79 | abstract-class-instantiated, 80 | abstract-method, 81 | access-member-before-definition, 82 | anomalous-backslash-in-string, 83 | anomalous-unicode-escape-in-string, 84 | arguments-differ, 85 | assert-on-tuple, 86 | assigning-non-slot, 87 | assignment-from-no-return, 88 | assignment-from-none, 89 | attribute-defined-outside-init, 90 | bad-except-order, 91 | bad-format-character, 92 | bad-format-string-key, 93 | bad-format-string, 94 | bad-open-mode, 95 | bad-reversed-sequence, 96 | bad-staticmethod-argument, 97 | bad-str-strip-call, 98 | bad-super-call, 99 | binary-op-exception, 100 | boolean-datetime, 101 | catching-non-exception, 102 | cell-var-from-loop, 103 | confusing-with-statement, 104 | continue-in-finally, 105 | dangerous-default-value, 106 | duplicate-argument-name, 107 | duplicate-bases, 108 | duplicate-except, 109 | duplicate-key, 110 | expression-not-assigned, 111 | format-combined-specification, 112 | format-needs-mapping, 113 | function-redefined, 114 | global-variable-undefined, 115 | import-error, 116 | import-self, 117 | inconsistent-mro, 118 | inherit-non-class, 119 | init-is-generator, 120 | invalid-all-object, 121 | invalid-format-index, 122 | invalid-length-returned, 123 | invalid-sequence-index, 124 | invalid-slice-index, 125 | invalid-slots-object, 126 | invalid-slots, 127 | invalid-unary-operand-type, 128 | logging-too-few-args, 129 | logging-too-many-args, 130 | logging-unsupported-format, 131 | lost-exception, 132 | method-hidden, 133 | misplaced-bare-raise, 134 | misplaced-future, 135 | missing-format-argument-key, 136 | missing-format-attribute, 137 | missing-format-string-key, 138 | no-member, 139 | no-method-argument, 140 | no-name-in-module, 141 | no-self-argument, 142 | no-value-for-parameter, 143 | non-iterator-returned, 144 | non-parent-method-called, 145 | nonexistent-operator, 146 | not-a-mapping, 147 | not-an-iterable, 148 | not-callable, 149 | not-context-manager, 150 | not-in-loop, 151 | pointless-statement, 152 | pointless-string-statement, 153 | raising-bad-type, 154 | raising-non-exception, 155 | redefined-builtin, 156 | redefined-outer-name, 157 | redundant-keyword-arg, 158 | repeated-keyword, 159 | return-arg-in-generator, 160 | return-in-init, 161 | return-outside-function, 162 | signature-differs, 163 | super-init-not-called, 164 | super-method-not-called, 165 | syntax-error, 166 | test-inherits-tests, 167 | too-few-format-args, 168 | too-many-format-args, 169 | too-many-function-args, 170 | translation-of-non-string, 171 | truncated-format-string, 172 | undefined-all-variable, 173 | undefined-loop-variable, 174 | undefined-variable, 175 | unexpected-keyword-arg, 176 | unexpected-special-method-signature, 177 | unpacking-non-sequence, 178 | unreachable, 179 | unsubscriptable-object, 180 | unsupported-binary-operation, 181 | unsupported-membership-test, 182 | unused-format-string-argument, 183 | unused-format-string-key, 184 | used-before-assignment, 185 | using-constant-test, 186 | yield-outside-function, 187 | 188 | astroid-error, 189 | fatal, 190 | method-check-failed, 191 | parse-error, 192 | raw-checker-failed, 193 | 194 | empty-docstring, 195 | invalid-characters-in-docstring, 196 | missing-docstring, 197 | wrong-spelling-in-comment, 198 | wrong-spelling-in-docstring, 199 | 200 | unused-argument, 201 | unused-import, 202 | unused-variable, 203 | 204 | eval-used, 205 | exec-used, 206 | 207 | bad-classmethod-argument, 208 | bad-mcs-classmethod-argument, 209 | bad-mcs-method-argument, 210 | bare-except, 211 | broad-except, 212 | consider-iterating-dictionary, 213 | consider-using-enumerate, 214 | global-at-module-level, 215 | global-variable-not-assigned, 216 | literal-used-as-attribute, 217 | logging-format-interpolation, 218 | logging-not-lazy, 219 | multiple-imports, 220 | multiple-statements, 221 | no-classmethod-decorator, 222 | no-staticmethod-decorator, 223 | protected-access, 224 | redundant-unittest-assert, 225 | reimported, 226 | simplifiable-if-statement, 227 | simplifiable-range, 228 | singleton-comparison, 229 | superfluous-parens, 230 | unidiomatic-typecheck, 231 | unnecessary-lambda, 232 | unnecessary-pass, 233 | unnecessary-semicolon, 234 | unneeded-not, 235 | useless-else-on-loop, 236 | wrong-assert-type, 237 | 238 | deprecated-method, 239 | deprecated-module, 240 | 241 | too-many-boolean-expressions, 242 | too-many-nested-blocks, 243 | too-many-statements, 244 | 245 | wildcard-import, 246 | wrong-import-order, 247 | wrong-import-position, 248 | 249 | missing-final-newline, 250 | mixed-line-endings, 251 | trailing-newlines, 252 | trailing-whitespace, 253 | unexpected-line-ending-format, 254 | 255 | bad-inline-option, 256 | bad-option-value, 257 | deprecated-pragma, 258 | unrecognized-inline-option, 259 | useless-suppression, 260 | disable = 261 | bad-indentation, 262 | broad-exception-raised, 263 | consider-using-f-string, 264 | duplicate-code, 265 | file-ignored, 266 | fixme, 267 | global-statement, 268 | invalid-name, 269 | locally-disabled, 270 | no-else-return, 271 | suppressed-message, 272 | too-few-public-methods, 273 | too-many-ancestors, 274 | too-many-arguments, 275 | too-many-branches, 276 | too-many-instance-attributes, 277 | too-many-lines, 278 | too-many-locals, 279 | too-many-public-methods, 280 | too-many-return-statements, 281 | ungrouped-imports, 282 | unspecified-encoding, 283 | unused-wildcard-import, 284 | use-maxsplit-arg, 285 | 286 | feature-toggle-needs-doc, 287 | illegal-waffle-usage, 288 | 289 | logging-fstring-interpolation, 290 | django-not-configured, 291 | unused-argument, 292 | unsubscriptable-object 293 | 294 | [REPORTS] 295 | output-format = text 296 | reports = no 297 | score = no 298 | 299 | [BASIC] 300 | module-rgx = (([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 301 | const-rgx = (([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$ 302 | class-rgx = [A-Z_][a-zA-Z0-9]+$ 303 | function-rgx = ([a-z_][a-z0-9_]{2,40}|test_[a-z0-9_]+)$ 304 | method-rgx = ([a-z_][a-z0-9_]{2,40}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*|maxDiff|test_[a-z0-9_]+)$ 305 | attr-rgx = [a-z_][a-z0-9_]{2,30}$ 306 | argument-rgx = [a-z_][a-z0-9_]{2,30}$ 307 | variable-rgx = [a-z_][a-z0-9_]{2,30}$ 308 | class-attribute-rgx = ([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 309 | inlinevar-rgx = [A-Za-z_][A-Za-z0-9_]*$ 310 | good-names = f,i,j,k,db,ex,Run,_,__ 311 | bad-names = foo,bar,baz,toto,tutu,tata 312 | no-docstring-rgx = __.*__$|test_.+|setUp$|setUpClass$|tearDown$|tearDownClass$|Meta$ 313 | docstring-min-length = 5 314 | 315 | [FORMAT] 316 | max-line-length = 120 317 | ignore-long-lines = ^\s*(# )?((?)|(\.\. \w+: .*))$ 318 | single-line-if-stmt = no 319 | max-module-lines = 1000 320 | indent-string = ' ' 321 | 322 | [MISCELLANEOUS] 323 | notes = FIXME,XXX,TODO 324 | 325 | [SIMILARITIES] 326 | min-similarity-lines = 4 327 | ignore-comments = yes 328 | ignore-docstrings = yes 329 | ignore-imports = no 330 | 331 | [TYPECHECK] 332 | ignore-mixin-members = yes 333 | ignored-classes = SQLObject 334 | unsafe-load-any-extension = yes 335 | generated-members = 336 | REQUEST, 337 | acl_users, 338 | aq_parent, 339 | objects, 340 | DoesNotExist, 341 | can_read, 342 | can_write, 343 | get_url, 344 | size, 345 | content, 346 | status_code, 347 | create, 348 | build, 349 | fields, 350 | tag, 351 | org, 352 | course, 353 | category, 354 | name, 355 | revision, 356 | _meta, 357 | 358 | [VARIABLES] 359 | init-import = no 360 | dummy-variables-rgx = _|dummy|unused|.*_unused 361 | additional-builtins = 362 | 363 | [CLASSES] 364 | defining-attr-methods = __init__,__new__,setUp 365 | valid-classmethod-first-arg = cls 366 | valid-metaclass-classmethod-first-arg = mcs 367 | 368 | [DESIGN] 369 | max-args = 5 370 | ignored-argument-names = _.* 371 | max-locals = 15 372 | max-returns = 6 373 | max-branches = 12 374 | max-statements = 50 375 | max-parents = 7 376 | max-attributes = 7 377 | min-public-methods = 2 378 | max-public-methods = 20 379 | 380 | [IMPORTS] 381 | deprecated-modules = regsub,TERMIOS,Bastion,rexec 382 | import-graph = 383 | ext-import-graph = 384 | int-import-graph = 385 | 386 | [EXCEPTIONS] 387 | overgeneral-exceptions = builtins.Exception 388 | 389 | # 6114ba904f03712e1def5d0f459a5ce5a0927223 390 | -------------------------------------------------------------------------------- /drag_and_drop_v2/public/css/drag_and_drop_edit.css: -------------------------------------------------------------------------------- 1 | .xblock--drag-and-drop--editor { 2 | width: 100%; 3 | height: 100%; 4 | margin-bottom: 0 !important; /* Remove an undesired whitespace from Studio */ 5 | } 6 | 7 | .modal-window .drag-builder { 8 | width: 100%; 9 | height: calc(100% - 55px); 10 | position: absolute; 11 | overflow-y: scroll; 12 | } 13 | 14 | /*** Drop Target ***/ 15 | .xblock--drag-and-drop--editor .zone { 16 | position: absolute; 17 | 18 | display: -webkit-box; 19 | display: -moz-box; 20 | display: -ms-flexbox; 21 | display: -webkit-flex; 22 | display: flex; 23 | 24 | /* Internet Explorer 10 */ 25 | -ms-flex-pack:center; 26 | -ms-flex-align:center; 27 | 28 | /* Firefox */ 29 | -moz-box-pack:center; 30 | -moz-box-align:center; 31 | 32 | /* Safari, Opera, and Chrome */ 33 | -webkit-box-pack:center; 34 | -webkit-box-align:center; 35 | 36 | /* W3C */ 37 | box-pack:center; 38 | box-align:center; 39 | 40 | border: 1px dotted #565656; 41 | box-sizing: border-box; 42 | } 43 | 44 | .xblock--drag-and-drop--editor .zone p { 45 | width: 100%; 46 | font-family: Arial; 47 | font-size: 16px; 48 | font-weight: bold; 49 | text-align: center; 50 | margin-top: auto; 51 | margin-bottom: auto; 52 | } 53 | 54 | /** Builder **/ 55 | .xblock--drag-and-drop--editor .hidden { 56 | display: none !important; 57 | } 58 | 59 | .xblock--drag-and-drop--editor .tab { 60 | width: 100%; 61 | background-color: #eee; 62 | padding: 3px 0; 63 | position: relative; 64 | } 65 | 66 | .xblock--drag-and-drop--editor .tab::after { 67 | content: ""; 68 | display: table; 69 | clear: both; 70 | } 71 | 72 | .xblock--drag-and-drop--editor .tab h3 { 73 | font-size: 18px; 74 | } 75 | 76 | .xblock--drag-and-drop--editor .tab h4, 77 | .xblock--drag-and-drop--editor .tab .h4 { 78 | display: block; 79 | font-size: 16px; 80 | margin: 20px 0 8px 0; 81 | } 82 | 83 | .xblock--drag-and-drop--editor .tab .items-form .item .row:first-of-type .h4 { 84 | margin-top: 0; 85 | } 86 | 87 | .xblock--drag-and-drop--editor .tab-header, 88 | .xblock--drag-and-drop--editor .tab-content { 89 | width: 96%; 90 | margin: 2%; 91 | } 92 | 93 | .xblock--drag-and-drop--editor .items { 94 | width: calc(100% - 515px); 95 | margin: 10px 0 0 0; 96 | } 97 | 98 | .xblock--drag-and-drop--editor .target-image-form .background-image-type { 99 | display: block; 100 | margin-bottom: 8px; 101 | } 102 | 103 | .xblock--drag-and-drop--editor .target-image-form .background-auto { 104 | margin-top: 20px; 105 | } 106 | 107 | .xblock--drag-and-drop--editor .target-image-form input[type="text"] { 108 | width: 50%; 109 | } 110 | .xblock--drag-and-drop--editor .target-image-form textarea { 111 | width: 97%; 112 | } 113 | 114 | .xblock--drag-and-drop--editor .target-image-form textarea { 115 | display: block; 116 | } 117 | 118 | .xblock--drag-and-drop--editor .target-image-form .background-auto .autozone-layout, 119 | .xblock--drag-and-drop--editor .target-image-form .background-auto .autozone-size { 120 | width: 4em; 121 | } 122 | 123 | .xblock--drag-and-drop--editor input, 124 | .xblock--drag-and-drop--editor textarea { 125 | box-sizing: border-box; 126 | font-size: 14px; 127 | background: #fff; 128 | box-shadow: none; 129 | padding: 6px 8px; 130 | border: 1px solid #b2b2b2; 131 | color: #4c4c4c; 132 | } 133 | 134 | .xblock--drag-and-drop--editor label > span { 135 | display: inline-block; 136 | margin-bottom: 0.25em; 137 | } 138 | 139 | .xblock--drag-and-drop--editor label.checkbox-label { 140 | font-size: 14px; 141 | } 142 | 143 | /* Main Tab */ 144 | .xblock--drag-and-drop--editor .feedback-tab input, 145 | .xblock--drag-and-drop--editor .feedback-tab select { 146 | display: block; 147 | } 148 | 149 | .xblock--drag-and-drop--editor .feedback-tab input[type=checkbox] { 150 | display: inline-block; 151 | } 152 | 153 | /* Zones Tab */ 154 | .xblock--drag-and-drop--editor .zones-tab .zone-editor { 155 | position: relative; 156 | display: flex; 157 | flex-direction: row; 158 | flex-wrap: nowrap; 159 | align-items: flex-start; 160 | justify-content: space-between; 161 | } 162 | 163 | .xblock--drag-and-drop--editor .zones-tab .tab-content .controls { 164 | width: 40%; 165 | max-width: 50%; 166 | min-width: 330px; 167 | } 168 | 169 | .xblock--drag-and-drop--editor .zones-tab .tab-content .target { 170 | position: relative; 171 | border: 1px solid #ccc; 172 | overflow: hidden; 173 | } 174 | .xblock--drag-and-drop--editor .zones-tab .tab-content .target-img { 175 | display: block; 176 | width: auto; 177 | height: auto; 178 | max-width: 100%; 179 | } 180 | 181 | .xblock--drag-and-drop--editor .zones-form .zone-row { 182 | background-color: #b1d9f1; 183 | padding: 1rem; 184 | margin-bottom: 2rem; 185 | } 186 | 187 | .xblock--drag-and-drop--editor .zones-form .zone-row label { 188 | display: block; 189 | } 190 | 191 | .xblock--drag-and-drop--editor .zones-form .zone-row label > span { 192 | display: inline-block; 193 | font-size: 14px; 194 | min-width: 8rem; 195 | } 196 | 197 | .xblock--drag-and-drop--editor .zones-form .zone-row label > input { 198 | width: 63%; 199 | margin: 0 0 5px; 200 | line-height: 2.664rem; /* .title gets line-height from a Studio rule that does not apply to .description; 201 | here we make sure that both input fields get the same value for line-height */ 202 | } 203 | 204 | .xblock--drag-and-drop--editor .zones-form .zone-row .layout { 205 | margin: 2rem 0; 206 | } 207 | 208 | .xblock--drag-and-drop--editor .zones-form .zone-row .layout label { 209 | display: inline-block; 210 | width: 45%; 211 | } 212 | 213 | .xblock--drag-and-drop--editor .zones-form .zone-row .layout .zone-size, 214 | .xblock--drag-and-drop--editor .zones-form .zone-row .layout .zone-coord { 215 | width: 35%; 216 | line-height: inherit; 217 | } 218 | 219 | .xblock--drag-and-drop--editor .zones-form .zone-row .alignment { 220 | margin-bottom: 15px; 221 | } 222 | 223 | 224 | .xblock--drag-and-drop--editor .feedback-form textarea { 225 | width: 99%; 226 | height: 128px; 227 | } 228 | 229 | .xblock--drag-and-drop--editor .form-help { 230 | margin: 0.5rem 0 1rem; 231 | font-size: 12px; 232 | } 233 | 234 | .xblock--drag-and-drop--editor .item-styles-form, 235 | .xblock--drag-and-drop--editor .items-form { 236 | margin-bottom: 30px; 237 | } 238 | 239 | .xblock--drag-and-drop--editor .items-form .item { 240 | background-color: #b1d9f1; 241 | padding: 2rem; 242 | margin-bottom: 2rem; 243 | } 244 | 245 | .xblock--drag-and-drop--editor .items-form select { 246 | width: 35%; 247 | } 248 | 249 | .xblock--drag-and-drop--editor .items-form .zone-checkbox { 250 | width: initial; 251 | } 252 | 253 | .xblock--drag-and-drop--editor .items-form .item-text, 254 | .xblock--drag-and-drop--editor .items-form .item-image-url { 255 | width: 50%; 256 | } 257 | 258 | .xblock--drag-and-drop--editor .items-form .item-width { 259 | width: 50px; 260 | } 261 | 262 | .xblock--drag-and-drop--editor .items-form textarea { 263 | width: 97%; 264 | } 265 | 266 | .xblock--drag-and-drop--editor .items-form .row.advanced { 267 | display: none; 268 | } 269 | .xblock--drag-and-drop--editor .items-form .row.advanced-link { 270 | font-size: 12px; 271 | margin-top: 2em; 272 | } 273 | .xblock--drag-and-drop--editor .items-form .row.advanced-link button { 274 | background: none; 275 | border: none; 276 | color: #4c4c4c; 277 | } 278 | .xblock--drag-and-drop--editor .items-form .row.advanced-link button:before { 279 | content: '\25B6'; 280 | margin-right: 0.5em; 281 | } 282 | .rtl .xblock--drag-and-drop--editor .items-form .row.advanced-link button:before { 283 | content: '\25C0'; 284 | margin-left: 0.5em; 285 | margin-right: 0; 286 | } 287 | 288 | .xblock--drag-and-drop-editor .items-form .zone-checkbox-row { 289 | margin-bottom: 0px; 290 | } 291 | 292 | /** Buttons **/ 293 | .xblock--drag-and-drop--editor .btn { 294 | background-color: #1d5280; 295 | color: #fff; 296 | border: 1px solid #156ab4; 297 | border-radius: 6px; 298 | padding: 5px 10px; 299 | } 300 | 301 | .xblock--drag-and-drop--editor .btn:hover, 302 | .xblock--drag-and-drop--editor .btn:focus { 303 | background-color: #296ba5; 304 | box-shadow: 0 1px 1px rgba(0,0,0,0.5); 305 | } 306 | 307 | .xblock--drag-and-drop--editor .remove-zone, 308 | .xblock--drag-and-drop--editor .remove-item { 309 | float: right; 310 | padding: 3px; 311 | border-radius: 12px; 312 | } 313 | .rtl .xblock--drag-and-drop--editor .remove-zone, 314 | .rtl .xblock--drag-and-drop--editor .remove-item { 315 | float: left; 316 | } 317 | 318 | .xblock--drag-and-drop--editor .icon { 319 | width: 14px; 320 | height: 14px; 321 | border-radius: 7px; 322 | display: inline-block; 323 | } 324 | 325 | .xblock--drag-and-drop--editor .remove-zone .icon, 326 | .xblock--drag-and-drop--editor .remove-item .icon { 327 | display: block; 328 | } 329 | 330 | .xblock--drag-and-drop--editor .tab .field-error { 331 | outline: none; 332 | box-shadow: 0 0 10px 0 darkred; 333 | /* Needed because Safari won't show the box-shadow for textareas otherwise. */ 334 | -webkit-appearance: none; 335 | } 336 | 337 | .xblock--drag-and-drop--editor .icon.add:before { 338 | content: ''; 339 | height: 10px; 340 | width: 2px; 341 | background-color: #fff; 342 | position: relative; 343 | display: inline; 344 | float: left; 345 | top: 2px; 346 | left: 6px; 347 | } 348 | 349 | .xblock--drag-and-drop--editor .icon.add:after { 350 | content: ''; 351 | height: 2px; 352 | width: 10px; 353 | background-color: #fff; 354 | position: relative; 355 | display: inline; 356 | float: left; 357 | top: 6px; 358 | left: 0; 359 | } 360 | 361 | .xblock--drag-and-drop--editor .icon.remove:before { 362 | content: ''; 363 | height: 10px; 364 | width: 2px; 365 | background-color: #fff; 366 | position: relative; 367 | display: inline; 368 | float: left; 369 | top: 2px; 370 | left: 6px; 371 | -webkit-transform: rotate(45deg); 372 | -ms-transform: rotate(45deg); 373 | transform: rotate(45deg); 374 | } 375 | 376 | .xblock--drag-and-drop--editor .icon.remove:after { 377 | content: ''; 378 | height: 2px; 379 | width: 10px; 380 | background-color: #fff; 381 | position: relative; 382 | display: inline; 383 | float: left; 384 | top: 6px; 385 | left: 0; 386 | -webkit-transform: rotate(45deg); 387 | -ms-transform: rotate(45deg); 388 | transform: rotate(45deg); 389 | } 390 | 391 | .modal-window .modal-content .editor-with-buttons.xblock--drag-and-drop--editor { 392 | margin-bottom: 0; 393 | } 394 | -------------------------------------------------------------------------------- /drag_and_drop_v2/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ Drag and Drop v2 XBlock - Utils """ 3 | from __future__ import absolute_import 4 | 5 | import copy 6 | import re 7 | from collections import namedtuple 8 | 9 | import bleach 10 | 11 | from bleach.css_sanitizer import CSSSanitizer 12 | 13 | 14 | def _(text): 15 | """ Dummy `gettext` replacement to make string extraction tools scrape strings marked for translation """ 16 | return text 17 | 18 | 19 | def ngettext_fallback(text_singular, text_plural, number): 20 | """ Dummy `ngettext` replacement to make string extraction tools scrape strings marked for translation """ 21 | if number == 1: 22 | return text_singular 23 | else: 24 | return text_plural 25 | 26 | 27 | def _clean_data(data): 28 | """ Remove html tags and extra white spaces e.g newline, tabs etc from provided data """ 29 | cleaner = bleach.Cleaner(tags=[], strip=True) 30 | cleaned_text = " ".join(re.split(r"\s+", cleaner.clean(data), flags=re.UNICODE)).strip() 31 | return cleaned_text 32 | 33 | 34 | # Convert `bleach.ALLOWED_TAGS` to a set because it is a list in `bleach<6.0.0`. 35 | ALLOWED_TAGS = set(bleach.ALLOWED_TAGS) | { 36 | 'br', 37 | 'caption', 38 | 'dd', 39 | 'del', 40 | 'div', 41 | 'dl', 42 | 'dt', 43 | 'h1', 44 | 'h2', 45 | 'h3', 46 | 'h4', 47 | 'h5', 48 | 'h6', 49 | 'hr', 50 | 'img', 51 | 'p', 52 | 'pre', 53 | 's', 54 | 'strike', 55 | 'span', 56 | 'sub', 57 | 'sup', 58 | 'table', 59 | 'tbody', 60 | 'td', 61 | 'tfoot', 62 | 'th', 63 | 'thead', 64 | 'tr', 65 | 'u', 66 | } 67 | ALLOWED_ATTRIBUTES = { 68 | '*': ['class', 'style', 'id'], 69 | 'a': ['href', 'title', 'target', 'rel'], 70 | 'abbr': ['title'], 71 | 'acronym': ['title'], 72 | 'audio': ['controls', 'autobuffer', 'autoplay', 'src'], 73 | 'img': ['src', 'alt', 'title', 'width', 'height'], 74 | 'table': ['border', 'cellspacing', 'cellpadding'], 75 | 'td': ['style', 'scope'], 76 | } 77 | 78 | 79 | def sanitize_html(raw_body: str) -> str: 80 | """ 81 | Remove not allowed HTML tags to mitigate XSS vulnerabilities. 82 | """ 83 | bleach_options = { 84 | "tags": ALLOWED_TAGS, 85 | "protocols": bleach.ALLOWED_PROTOCOLS, 86 | "strip": True, 87 | "attributes": ALLOWED_ATTRIBUTES, 88 | "css_sanitizer": CSSSanitizer() 89 | } 90 | 91 | return bleach.clean(raw_body, **bleach_options) 92 | 93 | 94 | class DummyTranslationService: 95 | """ 96 | Dummy drop-in replacement for i18n XBlock service 97 | """ 98 | gettext = _ 99 | ngettext = ngettext_fallback 100 | 101 | 102 | class FeedbackMessages: 103 | """ 104 | Feedback messages collection 105 | """ 106 | class MessageClasses: 107 | """ 108 | Namespace for message classes 109 | """ 110 | CORRECT_SOLUTION = "correct" 111 | PARTIAL_SOLUTION = "partial" 112 | INCORRECT_SOLUTION = "incorrect" 113 | 114 | CORRECTLY_PLACED = CORRECT_SOLUTION 115 | MISPLACED = INCORRECT_SOLUTION 116 | NOT_PLACED = INCORRECT_SOLUTION 117 | 118 | INITIAL_FEEDBACK = "initial" 119 | FINAL_FEEDBACK = "final" 120 | 121 | GRADE_FEEDBACK_TPL = _('Your highest score is {score}') 122 | FINAL_ATTEMPT_TPL = _('Final attempt was used, highest score is {score}') 123 | 124 | @staticmethod 125 | def correctly_placed(number, ngettext=ngettext_fallback): 126 | """ 127 | Formats "correctly placed items" message 128 | """ 129 | return ngettext( 130 | 'Correctly placed {correct_count} item', 131 | 'Correctly placed {correct_count} items', 132 | number 133 | ).format(correct_count=number) 134 | 135 | @staticmethod 136 | def misplaced(number, ngettext=ngettext_fallback): 137 | """ 138 | Formats "misplaced items" message 139 | """ 140 | return ngettext( 141 | 'Misplaced {misplaced_count} item', 142 | 'Misplaced {misplaced_count} items', 143 | number 144 | ).format(misplaced_count=number) 145 | 146 | @staticmethod 147 | def misplaced_returned(number, ngettext=ngettext_fallback): 148 | """ 149 | Formats "misplaced items returned to bank" message 150 | """ 151 | return ngettext( 152 | 'Misplaced {misplaced_count} item (misplaced item was returned to the item bank)', 153 | 'Misplaced {misplaced_count} items (misplaced items were returned to the item bank)', 154 | number 155 | ).format(misplaced_count=number) 156 | 157 | @staticmethod 158 | def not_placed(number, ngettext=ngettext_fallback): 159 | """ 160 | Formats "did not place required items" message 161 | """ 162 | return ngettext( 163 | 'Did not place {missing_count} required item', 164 | 'Did not place {missing_count} required items', 165 | number 166 | ).format(missing_count=number) 167 | 168 | 169 | FeedbackMessage = namedtuple("FeedbackMessage", ["message", "message_class"]) 170 | ItemStats = namedtuple( 171 | 'ItemStats', 172 | ["required", "placed", "correctly_placed", "decoy", "decoy_in_bank"] 173 | ) 174 | 175 | 176 | class Constants: 177 | """ 178 | Namespace class for various constants 179 | """ 180 | ALLOWED_ZONE_ALIGNMENTS = ['left', 'right', 'center'] 181 | DEFAULT_ZONE_ALIGNMENT = 'center' 182 | 183 | STANDARD_MODE = "standard" 184 | ASSESSMENT_MODE = "assessment" 185 | ATTR_KEY_USER_IS_STAFF = "edx-platform.user_is_staff" 186 | 187 | 188 | class SHOWANSWER: 189 | """ 190 | Constants for when to show answer 191 | """ 192 | AFTER_ALL_ATTEMPTS = "after_all_attempts" 193 | AFTER_ALL_ATTEMPTS_OR_CORRECT = "after_all_attempts_or_correct" 194 | ALWAYS = "always" 195 | ANSWERED = "answered" 196 | ATTEMPTED = "attempted" 197 | ATTEMPTED_NO_PAST_DUE = "attempted_no_past_due" 198 | CLOSED = "closed" 199 | CORRECT_OR_PAST_DUE = "correct_or_past_due" 200 | DEFAULT = "default" 201 | FINISHED = "finished" 202 | NEVER = "never" 203 | PAST_DUE = "past_due" 204 | 205 | 206 | class StateMigration: 207 | """ 208 | Helper class to apply zone data and item state migrations 209 | """ 210 | def __init__(self, block): 211 | self._block = block 212 | 213 | @staticmethod 214 | def _apply_migration(obj_id, obj, migrations): 215 | """ 216 | Applies migrations sequentially to a copy of an `obj`, to avoid updating actual data 217 | """ 218 | tmp = copy.deepcopy(obj) 219 | for method in migrations: 220 | tmp = method(obj_id, tmp) 221 | 222 | return tmp 223 | 224 | def apply_zone_migrations(self, zone): 225 | """ 226 | Applies zone migrations 227 | """ 228 | migrations = (self._zone_v1_to_v2, self._zone_v2_to_v2p1) 229 | zone_id = zone.get('uid', zone.get('id')) 230 | 231 | return self._apply_migration(zone_id, zone, migrations) 232 | 233 | def apply_item_state_migrations(self, item_id, item_state): 234 | """ 235 | Applies item_state migrations 236 | """ 237 | migrations = (self._item_state_v1_to_v1p5, self._item_state_v1p5_to_v2, self._item_state_v2_to_v2p1) 238 | 239 | return self._apply_migration(item_id, item_state, migrations) 240 | 241 | @classmethod 242 | def _zone_v1_to_v2(cls, unused_zone_id, zone): 243 | """ 244 | Migrates zone data from v1.0 format to v2.0 format. 245 | 246 | Changes: 247 | * v1 used zone "title" as UID, while v2 zone has dedicated "uid" property 248 | * "id" and "index" properties are no longer used 249 | 250 | In: {'id': 1, 'index': 2, 'title': "Zone", ...} 251 | Out: {'uid': "Zone", ...} 252 | """ 253 | if "uid" not in zone: 254 | zone["uid"] = zone.get("title") 255 | zone.pop("id", None) 256 | zone.pop("index", None) 257 | 258 | return zone 259 | 260 | @classmethod 261 | def _zone_v2_to_v2p1(cls, unused_zone_id, zone): 262 | """ 263 | Migrates zone data from v2.0 to v2.1 264 | 265 | Changes: 266 | * Removed "none" zone alignment; default align is "center" 267 | 268 | In: { 269 | 'uid': "Zone", "align": "none", 270 | "x_percent": "10%", "y_percent": "10%", "width_percent": "10%", "height_percent": "10%" 271 | } 272 | Out: { 273 | 'uid': "Zone", "align": "center", 274 | "x_percent": "10%", "y_percent": "10%", "width_percent": "10%", "height_percent": "10%" 275 | } 276 | """ 277 | if zone.get('align', None) not in Constants.ALLOWED_ZONE_ALIGNMENTS: 278 | zone['align'] = Constants.DEFAULT_ZONE_ALIGNMENT 279 | 280 | return zone 281 | 282 | @classmethod 283 | def _item_state_v1_to_v1p5(cls, unused_item_id, item): 284 | """ 285 | Migrates item_state from v1.0 to v1.5 286 | 287 | Changes: 288 | * Item state is now a dict instead of tuple 289 | 290 | In: ('100px', '120px') 291 | Out: {'top': '100px', 'left': '120px'} 292 | """ 293 | if isinstance(item, dict): 294 | return item 295 | else: 296 | return {'top': item[0], 'left': item[1]} 297 | 298 | @classmethod 299 | def _item_state_v1p5_to_v2(cls, unused_item_id, item): 300 | """ 301 | Migrates item_state from v1.5 to v2.0 302 | 303 | Changes: 304 | * Item placement attributes switched from absolute (left-top) to relative (x_percent-y_percent) units 305 | 306 | In: {'zone': 'Zone", 'correct': True, 'top': '100px', 'left': '120px'} 307 | Out: {'zone': 'Zone", 'correct': True, 'top': '100px', 'left': '120px'} 308 | """ 309 | # Conversion can't be made as parent dimensions are unknown to python - converted in JS 310 | # Since 2.1 JS this conversion became unnecesary, so it was removed from JS code 311 | return item 312 | 313 | def _item_state_v2_to_v2p1(self, item_id, item): 314 | """ 315 | Migrates item_state from v2.0 to v2.1 316 | 317 | * Single item can correspond to multiple zones - "zone" key is added to each item 318 | * Assessment mode - "correct" key is added to each item 319 | * Removed "no zone align" option; only automatic alignment is now allowed - removes attributes related to 320 | "absolute" placement of an item (relative to background image, as opposed to the zone) 321 | """ 322 | self._multiple_zones_migration(item_id, item) 323 | self._assessment_mode_migration(item) 324 | self._automatic_alignment_migration(item) 325 | 326 | return item 327 | 328 | def _multiple_zones_migration(self, item_id, item): 329 | """ 330 | Changes: 331 | * Adds "zone" attribute 332 | 333 | In: {'item_id': 0} 334 | Out: {'zone': 'Zone", 'item_id": 0} 335 | 336 | In: {'item_id': 1} 337 | Out: {'zone': 'unknown", 'item_id": 1} 338 | """ 339 | if item.get('zone') is None: 340 | valid_zones = self._block.get_item_zones(int(item_id)) 341 | if valid_zones: 342 | # If we get to this point, then the item was placed prior to support for 343 | # multiple correct zones being added. As a result, it can only be correct 344 | # on a single zone, and so we can trust that the item was placed on the 345 | # zone with index 0. 346 | item['zone'] = valid_zones[0] 347 | else: 348 | item['zone'] = 'unknown' 349 | 350 | @classmethod 351 | def _assessment_mode_migration(cls, item): 352 | """ 353 | Changes: 354 | * Adds "correct" attribute if missing 355 | 356 | In: {'item_id': 0} 357 | Out: {'item_id': 'correct': True} 358 | 359 | In: {'item_id': 0, 'correct': True} 360 | Out: {'item_id': 'correct': True} 361 | 362 | In: {'item_id': 0, 'correct': False} 363 | Out: {'item_id': 'correct': False} 364 | """ 365 | # If correctness information is missing 366 | # (because problem was completed before assessment mode was implemented), 367 | # assume the item is in correct zone (in standard mode, only items placed 368 | # into correct zone are stored in item state). 369 | if item.get('correct') is None: 370 | item['correct'] = True 371 | 372 | @classmethod 373 | def _automatic_alignment_migration(cls, item): 374 | """ 375 | Changes: 376 | * Removed old "absolute" placement attributes 377 | * Removed "none" zone alignment, making "x_percent" and "y_percent" attributes obsolete 378 | 379 | In: {'zone': 'Zone", 'correct': True, 'top': '100px', 'left': '120px', 'absolute': true} 380 | Out: {'zone': 'Zone", 'correct': True} 381 | 382 | In: {'zone': 'Zone", 'correct': True, 'x_percent': '90%', 'y_percent': '20%'} 383 | Out: {'zone': 'Zone", 'correct': True} 384 | """ 385 | attributes_to_remove = ['x_percent', 'y_percent', 'left', 'top', 'absolute'] 386 | for attribute in attributes_to_remove: 387 | item.pop(attribute, None) 388 | 389 | return item 390 | -------------------------------------------------------------------------------- /drag_and_drop_v2/public/js/vendor/virtual-dom-1.3.0.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define("virtual-dom-1.3.0",[],e);else{var n;"undefined"!=typeof window?n=window:"undefined"!=typeof global?n=global:"undefined"!=typeof self&&(n=self),n.virtualDom=e()}}(function(){return function e(n,t,r){function o(s,u){if(!t[s]){if(!n[s]){var a="function"==typeof require&&require;if(!u&&a)return a(s,!0);if(i)return i(s,!0);var f=new Error("Cannot find module '"+s+"'");throw f.code="MODULE_NOT_FOUND",f}var v=t[s]={exports:{}};n[s][0].call(v.exports,function(e){var t=n[s][1][e];return o(t?t:e)},v,v.exports,e,n,t,r)}return t[s].exports}for(var i="function"==typeof require&&require,s=0;s>>0:i>>>0;(u=o.exec(n))&&(a=u.index+u[0].length,!(a>c&&(v.push(n.slice(c,u.index)),!r&&u.length>1&&u[0].replace(s,function(){for(var n=1;n1&&u.index=i)));)o.lastIndex===u.index&&o.lastIndex++;return c===n.length?(f||!o.test(""))&&v.push(""):v.push(n.slice(c)),v.length>i?v.slice(0,i):v}}()},{}],6:[function(){},{}],7:[function(e,n){"use strict";function t(e){var n=e[i];return n||(n=e[i]={}),n}var r=e("individual/one-version"),o="7";r("ev-store",o);var i="__EV_STORE_KEY@"+o;n.exports=t},{"individual/one-version":9}],8:[function(e,n){(function(e){"use strict";function t(e,n){return e in r?r[e]:(r[e]=n,n)}var r="undefined"!=typeof window?window:"undefined"!=typeof e?e:{};n.exports=t}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],9:[function(e,n){"use strict";function t(e,n,t){var o="__INDIVIDUAL_ONE_VERSION_"+e,i=o+"_ENFORCE_SINGLETON",s=r(i,n);if(s!==n)throw new Error("Can only have one copy of "+e+".\nYou already have version "+s+" installed.\nThis means you cannot install version "+n);return r(o,t)}var r=e("./index.js");n.exports=t},{"./index.js":8}],10:[function(e,n){(function(t){var r="undefined"!=typeof t?t:"undefined"!=typeof window?window:{},o=e("min-document");if("undefined"!=typeof document)n.exports=document;else{var i=r["__GLOBAL_DOCUMENT_CACHE@4"];i||(i=r["__GLOBAL_DOCUMENT_CACHE@4"]=o),n.exports=i}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"min-document":6}],11:[function(e,n){"use strict";n.exports=function(e){return"object"==typeof e&&null!==e}},{}],12:[function(e,n){function t(e){return"[object Array]"===o.call(e)}var r=Array.isArray,o=Object.prototype.toString;n.exports=r||t},{}],13:[function(e,n){var t=e("./vdom/patch.js");n.exports=t},{"./vdom/patch.js":18}],14:[function(e,n){function t(e,n,t){for(var i in n){var a=n[i];void 0===a?r(e,i,a,t):u(a)?(r(e,i,a,t),a.hook&&a.hook(e,i,t?t[i]:void 0)):s(a)?o(e,n,t,i,a):e[i]=a}}function r(e,n,t,r){if(r){var o=r[n];if(u(o))o.unhook&&o.unhook(e,n,t);else if("attributes"===n)for(var i in o)e.removeAttribute(i);else if("style"===n)for(var s in o)e.style[s]="";else e[n]="string"==typeof o?"":null}}function o(e,n,t,r,o){var u=t?t[r]:void 0;if("attributes"!==r){if(u&&s(u)&&i(u)!==i(o))return void(e[r]=o);s(e[r])||(e[r]={});var a="style"===r?"":void 0;for(var f in o){var v=o[f];e[r][f]=void 0===v?a:v}}else for(var d in o){var c=o[d];void 0===c?e.removeAttribute(d):e.setAttribute(d,c)}}function i(e){return Object.getPrototypeOf?Object.getPrototypeOf(e):e.__proto__?e.__proto__:e.constructor?e.constructor.prototype:void 0}var s=e("is-object"),u=e("../vnode/is-vhook.js");n.exports=t},{"../vnode/is-vhook.js":26,"is-object":11}],15:[function(e,n){function t(e,n){var f=n?n.document||r:r,v=n?n.warn:null;if(e=a(e).a,u(e))return e.init();if(s(e))return f.createTextNode(e.text);if(!i(e))return v&&v("Item is not a valid virtual dom node",e),null;var d=null===e.namespace?f.createElement(e.tagName):f.createElementNS(e.namespace,e.tagName),c=e.properties;o(d,c);for(var p=e.children,l=0;l=i;){if(r=(s+i)/2>>0,o=e[r],i===s)return o>=n&&t>=o;if(n>o)i=r+1;else{if(!(o>t))return!0;s=r-1}}return!1}function i(e,n){return e>n?1:-1}var s={};n.exports=t},{}],17:[function(e,n){function t(e,n,t){var a=e.type,c=e.vNode,l=e.patch;switch(a){case p.REMOVE:return r(n,c);case p.INSERT:return o(n,l,t);case p.VTEXT:return i(n,c,l,t);case p.WIDGET:return s(n,c,l,t);case p.VNODE:return u(n,c,l,t);case p.ORDER:return f(n,l),n;case p.PROPS:return d(n,l,c.properties),n;case p.THUNK:return v(n,t.patch(n,l,t));default:return n}}function r(e,n){var t=e.parentNode;return t&&t.removeChild(e),a(e,n),null}function o(e,n,t){var r=l(n,t);return e&&e.appendChild(r),e}function i(e,n,t,r){var o;if(3===e.nodeType)e.replaceData(0,e.length,t.text),o=e;else{var i=e.parentNode;o=l(t,r),i&&i.replaceChild(o,e)}return o}function s(e,n,t,r){var o,i=h(n,t);o=i?t.update(n,e)||e:l(t,r);var s=e.parentNode;return s&&o!==e&&s.replaceChild(o,e),i||a(e,n),o}function u(e,n,t,r){var o=e.parentNode,i=l(t,r);return o&&o.replaceChild(i,e),i}function a(e,n){"function"==typeof n.destroy&&c(n)&&n.destroy(e)}function f(e,n){var t,r=[],o=e.childNodes,i=o.length,s=n.reverse;for(t=0;i>t;t++)r.push(e.childNodes[t]);var u,a,f,v,d,c=0;for(t=0;i>t;){if(u=n[t],v=1,void 0!==u&&u!==t){for(;n[t+v]===u+v;)v++;for(s[t]>t+v&&c++,a=r[u],f=o[t+c]||null,d=0;a!==f&&d++u+v&&c--}t in n.removes&&c++,t+=v}}function v(e,n){return e&&n&&e!==n&&e.parentNode&&(console.log(e),e.parentNode.replaceChild(n,e)),n}var d=e("./apply-properties"),c=e("../vnode/is-widget.js"),p=e("../vnode/vpatch.js"),l=e("./create-element"),h=e("./update-widget");n.exports=t},{"../vnode/is-widget.js":29,"../vnode/vpatch.js":32,"./apply-properties":14,"./create-element":15,"./update-widget":19}],18:[function(e,n){function t(e,n){return r(e,n)}function r(e,n,t){var u=i(n);if(0===u.length)return e;var f=a(e,n.a,u),v=e.ownerDocument;t||(t={patch:r},v!==s&&(t.document=v));for(var d=0;dw;w++){var m=t[w];o(m)?(p+=m.count||0,!l&&m.hasWidgets&&(l=!0),!h&&m.hasThunks&&(h=!0),y||!m.hooks&&!m.descendantHooks||(y=!0)):!l&&i(m)?"function"==typeof m.destroy&&(l=!0):!h&&s(m)&&(h=!0)}this.count=c+p,this.hasWidgets=l,this.hasThunks=h,this.hooks=d,this.descendantHooks=y}var r=e("./version"),o=e("./is-vnode"),i=e("./is-widget"),s=e("./is-thunk"),u=e("./is-vhook");n.exports=t;var a={},f=[];t.prototype.version=r,t.prototype.type="VirtualNode"},{"./is-thunk":25,"./is-vhook":26,"./is-vnode":27,"./is-widget":29,"./version":30}],32:[function(e,n){function t(e,n,t){this.type=Number(e),this.vNode=n,this.patch=t}var r=e("./version");t.NONE=0,t.VTEXT=1,t.VNODE=2,t.WIDGET=3,t.PROPS=4,t.ORDER=5,t.INSERT=6,t.REMOVE=7,t.THUNK=8,n.exports=t,t.prototype.version=r,t.prototype.type="VirtualPatch"},{"./version":30}],33:[function(e,n){function t(e){this.text=String(e)}var r=e("./version");n.exports=t,t.prototype.version=r,t.prototype.type="VirtualText"},{"./version":30}],34:[function(e,n){function t(e,n){var s;for(var u in e){u in n||(s=s||{},s[u]=void 0);var a=e[u],f=n[u];if(a!==f)if(o(a)&&o(f))if(r(f)!==r(a))s=s||{},s[u]=f;else if(i(f))s=s||{},s[u]=f;else{var v=t(a,f);v&&(s=s||{},s[u]=v)}else s=s||{},s[u]=f}for(var d in n)d in e||(s=s||{},s[d]=n[d]);return s}function r(e){return Object.getPrototypeOf?Object.getPrototypeOf(e):e.__proto__?e.__proto__:e.constructor?e.constructor.prototype:void 0}var o=e("is-object"),i=e("../vnode/is-vhook");n.exports=t},{"../vnode/is-vhook":26,"is-object":11}],35:[function(e,n){function t(e,n){var t={a:e};return r(e,n,t,0),t}function r(e,n,t,r){if(e!==n){var s=t[r],a=!1;if(w(e)||w(n))u(e,n,t,r);else if(null==n)x(e)||(i(e,t,r),s=t[r]),s=p(s,new h(h.REMOVE,e,n));else if(y(n))if(y(e))if(e.tagName===n.tagName&&e.namespace===n.namespace&&e.key===n.key){var f=j(e.properties,n.properties);f&&(s=p(s,new h(h.PROPS,e,f))),s=o(e,n,t,s,r)}else s=p(s,new h(h.VNODE,e,n)),a=!0;else s=p(s,new h(h.VNODE,e,n)),a=!0;else g(n)?g(e)?e.text!==n.text&&(s=p(s,new h(h.VTEXT,e,n))):(s=p(s,new h(h.VTEXT,e,n)),a=!0):x(n)&&(x(e)||(a=!0),s=p(s,new h(h.WIDGET,e,n)));s&&(t[r]=s),a&&i(e,t,r)}}function o(e,n,t,o,i){for(var s=e.children,u=d(s,n.children),a=s.length,f=u.length,v=a>f?a:f,c=0;v>c;c++){var l=s[c],g=u[c];i+=1,l?r(l,g,t,i):g&&(o=p(o,new h(h.INSERT,null,g))),y(l)&&l.count&&(i+=l.count)}return u.moves&&(o=p(o,new h(h.ORDER,e,u.moves))),o}function i(e,n,t){f(e,n,t),s(e,n,t)}function s(e,n,t){if(x(e))"function"==typeof e.destroy&&(n[t]=p(n[t],new h(h.REMOVE,e,null)));else if(y(e)&&(e.hasWidgets||e.hasThunks))for(var r=e.children,o=r.length,i=0;o>i;i++){var a=r[i];t+=1,s(a,n,t),y(a)&&a.count&&(t+=a.count)}else w(e)&&u(e,null,n,t)}function u(e,n,r,o){var i=m(e,n),s=t(i.a,i.b);a(s)&&(r[o]=new h(h.THUNK,null,s))}function a(e){for(var n in e)if("a"!==n)return!0;return!1}function f(e,n,t){if(y(e)){if(e.hooks&&(n[t]=p(n[t],new h(h.PROPS,e,v(e.hooks)))),e.descendantHooks||e.hasThunks)for(var r=e.children,o=r.length,i=0;o>i;i++){var s=r[i];t+=1,f(s,n,t),y(s)&&s.count&&(t+=s.count)}}else w(e)&&u(e,null,n,t)}function v(e){var n={};for(var t in e)n[t]=void 0;return n}function d(e,n){var t=c(n);if(!t)return n;var r=c(e);if(!r)return n;var o={},i={};for(var s in t)o[t[s]]=r[s];for(var u in r)i[r[u]]=t[u];for(var a=e.length,f=n.length,v=a>f?a:f,d=[],p=0,l=0,h=0,y={},g=y.removes={},x=y.reverse={},w=!1;v>p;){var m=i[l];if(void 0!==m)d[l]=n[m],m!==h&&(y[m]=h,x[h]=m,w=!0),h++;else if(l in i)d[l]=void 0,g[l]=h++,w=!0;else{for(;void 0!==o[p];)p++;if(v>p){var j=n[p];j&&(d[l]=j,p!==h&&(w=!0,y[p]=h,x[h]=p),h++),p++}}l++}return w&&(d.moves=y),d}function c(e){var n,t;for(n=0;n1", "incorrect": "No 1"}, 310 | 1: {"correct": "Yes 2", "incorrect": "No 2"}, 311 | 2: {"correct": "", "incorrect": ""} 312 | } 313 | 314 | INITIAL_FEEDBACK = "HTML Intro Feed" 315 | FINAL_FEEDBACK = "Final feedback!" 316 | 317 | 318 | class TestDragAndDropPlainData(StandardModeFixture, unittest.TestCase): 319 | FOLDER = "plain" 320 | 321 | ZONE_1 = "zone-1" 322 | ZONE_2 = "zone-2" 323 | 324 | FEEDBACK = { 325 | 0: {"correct": "Yes 1", "incorrect": "No 1"}, 326 | 1: {"correct": "Yes 2", "incorrect": "No 2"}, 327 | 2: {"correct": "", "incorrect": ""} 328 | } 329 | 330 | INITIAL_FEEDBACK = "This is the initial feedback." 331 | FINAL_FEEDBACK = "This is the final feedback." 332 | 333 | 334 | class TestOldDataFormat(TestDragAndDropPlainData): 335 | """ 336 | Make sure we can work with the slightly-older format for 'data' field values. 337 | """ 338 | FOLDER = "old" 339 | 340 | INITIAL_FEEDBACK = "Intro Feed" 341 | FINAL_FEEDBACK = "Final Feed" 342 | 343 | ZONE_1 = "Zone 1" 344 | ZONE_2 = "Zone 2" 345 | --------------------------------------------------------------------------------