├── 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 |
--------------------------------------------------------------------------------