From 8412a0f5b18c14f4a3a8493e19fc3debbca1d82c Mon Sep 17 00:00:00 2001 From: stv Date: Wed, 17 Jul 2019 10:54:46 -0700 Subject: [PATCH] Modernize XBlock infrastructure --- .coveragerc | 23 + .dockerignore | 1 + .eslintrc.yml | 290 ++++++++ .github/pull_request_template.md | 18 + .gitignore | 7 +- .travis.yml | 54 +- .tx/config | 8 + Dockerfile | 8 + Makefile | 107 +++ README.markdown | 29 - README.rst | 48 ++ conf/locale | 1 - freetextresponse/__init__.py | 1 - freetextresponse/freetextresponse.py | 692 ------------------ .../locale/vi/LC_MESSAGES/django.mo | Bin 1062 -> 0 bytes .../locale/vi/LC_MESSAGES/django.po | 48 -- freetextresponse/mixins/__init__.py | 3 + .../{mixins.py => mixins/dates.py} | 21 +- freetextresponse/mixins/fragment.py | 91 +++ freetextresponse/mixins/i18n.py | 32 + freetextresponse/mixins/scenario.py | 72 ++ freetextresponse/mixins/user.py | 22 + freetextresponse/models.py | 242 ++++++ freetextresponse/public/view.js | 99 ++- .../scenarios/free-text-response-many.xml | 40 + .../scenarios/free-text-response-single.xml | 5 + freetextresponse/settings.py | 10 +- .../{freetextresponse_view.html => view.html} | 0 .../{tests.py => tests/test_all.py} | 5 +- freetextresponse/translations/README.txt | 4 - .../translations/ar/LC_MESSAGES/text.mo | Bin 0 -> 1669 bytes .../translations/ar/LC_MESSAGES/text.po | 231 ++++++ .../translations/en/LC_MESSAGES/text.mo | Bin 0 -> 429 bytes .../translations/en/LC_MESSAGES/text.po | 204 ++++++ .../translations/eo/LC_MESSAGES/text.mo | Bin 0 -> 421 bytes .../translations/eo/LC_MESSAGES/text.po | 204 ++++++ .../translations/fr_CA/LC_MESSAGES/text.mo | Bin 0 -> 5261 bytes .../translations/fr_CA/LC_MESSAGES/text.po | 228 ++++++ .../translations/vi/LC_MESSAGES/text.mo | Bin 0 -> 5223 bytes .../translations/vi/LC_MESSAGES/text.po | 219 ++++++ freetextresponse/views.py | 364 +++++++++ freetextresponse/xblocks.py | 23 + i18n.mk | 31 + manage.py | 0 package.json | 24 +- pylintrc | 32 +- reports/.gitignore | 1 + setup.cfg | 6 - setup.py | 59 +- tox.ini | 106 ++- 50 files changed, 2767 insertions(+), 946 deletions(-) create mode 100644 .coveragerc create mode 120000 .dockerignore create mode 100644 .eslintrc.yml create mode 100644 .github/pull_request_template.md create mode 100644 .tx/config create mode 100644 Dockerfile create mode 100755 Makefile delete mode 100644 README.markdown create mode 100644 README.rst delete mode 120000 conf/locale delete mode 100644 freetextresponse/freetextresponse.py delete mode 100644 freetextresponse/locale/vi/LC_MESSAGES/django.mo delete mode 100644 freetextresponse/locale/vi/LC_MESSAGES/django.po create mode 100644 freetextresponse/mixins/__init__.py rename freetextresponse/{mixins.py => mixins/dates.py} (68%) create mode 100644 freetextresponse/mixins/fragment.py create mode 100644 freetextresponse/mixins/i18n.py create mode 100644 freetextresponse/mixins/scenario.py create mode 100644 freetextresponse/mixins/user.py create mode 100644 freetextresponse/models.py create mode 100644 freetextresponse/scenarios/free-text-response-many.xml create mode 100644 freetextresponse/scenarios/free-text-response-single.xml rename freetextresponse/templates/{freetextresponse_view.html => view.html} (100%) rename freetextresponse/{tests.py => tests/test_all.py} (99%) delete mode 100644 freetextresponse/translations/README.txt create mode 100644 freetextresponse/translations/ar/LC_MESSAGES/text.mo create mode 100644 freetextresponse/translations/ar/LC_MESSAGES/text.po create mode 100644 freetextresponse/translations/en/LC_MESSAGES/text.mo create mode 100644 freetextresponse/translations/en/LC_MESSAGES/text.po create mode 100644 freetextresponse/translations/eo/LC_MESSAGES/text.mo create mode 100644 freetextresponse/translations/eo/LC_MESSAGES/text.po create mode 100644 freetextresponse/translations/fr_CA/LC_MESSAGES/text.mo create mode 100644 freetextresponse/translations/fr_CA/LC_MESSAGES/text.po create mode 100644 freetextresponse/translations/vi/LC_MESSAGES/text.mo create mode 100644 freetextresponse/translations/vi/LC_MESSAGES/text.po create mode 100644 freetextresponse/views.py create mode 100644 freetextresponse/xblocks.py create mode 100644 i18n.mk mode change 100644 => 100755 manage.py create mode 100644 reports/.gitignore delete mode 100644 setup.cfg mode change 100644 => 100755 setup.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..1da856bf --- /dev/null +++ b/.coveragerc @@ -0,0 +1,23 @@ +[run] +data_file = reports/.coverage +source = freetextresponse +branch = true + +[report] +ignore_errors = True +exclude_lines = + pragma: no cover + def __repr__ + if self.debug: + if settings.DEBUG + raise AssertionError + raise NotImplementedError + if 0: + if __name__ == .__main__.: + +[html] +title = FreeTextResponse Python Test Coverage Report +directory = reports/cover + +[xml] +output = reports/coverage.xml diff --git a/.dockerignore b/.dockerignore new file mode 120000 index 00000000..3e4e48b0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.gitignore \ No newline at end of file diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 00000000..08aaf748 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,290 @@ +env: + browser: true +extends: 'eslint:recommended' +parserOptions: + ecmaVersion: 5 +rules: + array-bracket-spacing: error + array-callback-return: error + arrow-body-style: 0 + arrow-parens: + - error + - 'as-needed' + arrow-spacing: error + indent: + - error + - 4 + - SwitchCase: 1 + block-spacing: error + brace-style: + - error + - 1tbs + camelcase: error + callback-return: + - error + - + - cb + - callback + - next + class-methods-use-this: error + comma-dangle: + - error + - always + comma-spacing: error + comma-style: + - error + - last + computed-property-spacing: error + consistent-return: error + curly: + - error + - all + default-case: error + dot-location: + - error + - property + dot-notation: + - error + - allowKeywords: true + eol-last: error + eqeqeq: error + func-call-spacing: error + func-style: + - error + - declaration + function-paren-newline: + - error + - consistent + generator-star-spacing: error + guard-for-in: error + handle-callback-err: + - error + - err + key-spacing: + - error + - beforeColon: false + afterColon: true + keyword-spacing: error + linebreak-style: + - error + - unix + lines-around-comment: + - error + - beforeBlockComment: true + afterBlockComment: false + beforeLineComment: true + afterLineComment: false + max-len: + - error + - 160 + - ignoreComments: true + ignoreUrls: true + ignoreStrings: true + ignoreTemplateLiterals: true + ignoreRegExpLiterals: true + max-statements-per-line: error + new-cap: error + new-parens: error + no-alert: error + no-array-constructor: error + no-async-promise-executor: error + no-buffer-constructor: error + no-caller: error + no-confusing-arrow: error + no-console: error + no-delete-var: error + no-else-return: + - error + - allowElseIf: false + no-eval: error + no-extend-native: error + no-extra-bind: error + no-extra-parens: + - error + - all + no-fallthrough: error + no-floating-decimal: error + no-global-assign: error + no-implied-eval: error + no-invalid-this: error + no-iterator: error + no-label-var: error + no-labels: error + no-lone-blocks: error + no-loop-func: error + no-mixed-requires: error + no-mixed-spaces-and-tabs: + - error + - false + no-multi-spaces: error + no-multi-str: error + no-multiple-empty-lines: + - error + - max: 2 + maxBOF: 0 + maxEOF: 0 + no-nested-ternary: error + no-new: error + no-new-func: error + no-new-object: error + no-new-require: error + no-new-wrappers: error + no-octal: error + no-octal-escape: error + no-param-reassign: error + no-path-concat: error + no-process-exit: error + no-proto: error + no-prototype-builtins: error + no-redeclare: error + no-restricted-properties: + - error + - property: substring + message: 'Use .slice instead of .substring.' + - property: substr + message: 'Use .slice instead of .substr.' + - object: assert + property: equal + message: 'Use assert.strictEqual instead of assert.equal.' + - object: assert + property: notEqual + message: 'Use assert.notStrictEqual instead of assert.notEqual.' + - object: assert + property: deepEqual + message: 'Use assert.deepStrictEqual instead of assert.deepEqual.' + - object: assert + property: notDeepEqual + message: 'Use assert.notDeepStrictEqual instead of assert.notDeepEqual.' + no-return-assign: error + no-script-url: error + no-self-assign: error + no-self-compare: error + no-sequences: error + no-shadow: error + no-shadow-restricted-names: error + no-tabs: error + no-throw-literal: error + no-trailing-spaces: error + no-undef: + - error + - typeof: true + no-undef-init: error + no-undefined: error + no-underscore-dangle: + - error + - allowAfterThis: true + no-unmodified-loop-condition: error + no-unneeded-ternary: error + no-unused-expressions: error + no-unused-vars: + - error + - vars: all + args: 'after-used' + no-use-before-define: error + no-useless-call: error + no-useless-catch: error + no-useless-computed-key: error + no-useless-concat: error + no-useless-constructor: error + no-useless-escape: error + no-useless-rename: error + no-useless-return: error + no-whitespace-before-property: error + no-with: error + no-var: 0 + object-curly-newline: + - error + - consistent: true + multiline: true + object-curly-spacing: + - error + - always + object-property-newline: + - error + - allowAllPropertiesOnSameLine: true + object-shorthand: error + one-var-declaration-per-line: error + operator-assignment: error + operator-linebreak: error + padding-line-between-statements: + - error + - blankLine: always + prev: + - const + - let + next: '*' + - blankLine: any + prev: + - const + - let + - var + next: + - const + - let + - var + prefer-arrow-callback: 0 + prefer-const: error + prefer-numeric-literals: error + prefer-promise-reject-errors: error + prefer-rest-params: error + prefer-spread: error + prefer-template: off + quotes: + - error + - single + - avoidEscape: true + quote-props: + - error + - 'as-needed' + radix: error + require-atomic-updates: error + require-jsdoc: error + rest-spread-spacing: error + semi: error + semi-spacing: + - error + - before: false + after: true + semi-style: error + space-before-blocks: error + space-before-function-paren: + - error + - anonymous: always + named: never + space-in-parens: error + space-infix-ops: error + space-unary-ops: + - error + - words: true + nonwords: false + spaced-comment: + - error + - always + - exceptions: + - '-' + strict: + - error + - function + switch-colon-spacing: error + symbol-description: error + template-curly-spacing: + - error + - never + template-tag-spacing: error + unicode-bom: error + valid-jsdoc: + - error + - prefer: + return: returns + preferType: + String: string + Number: number + Boolean: boolean + array: Array + object: Object + function: Function + wrap-iife: error + yield-star-spacing: error + yoda: + - error + - never diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..d4be3429 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ +# Overview +What do we need to know about this change? + +# Test Instructions +- Checkout the branch +- Update settings? +- Anything else? + +# TODO +- [ ] Compile static assets +- [ ] Lint all files +- [ ] Pass all tests +- [ ] Bump the version number in `setup.py` +- [ ] Attach screenshots? +- [ ] Code Reviewer 1: +- [ ] Code Reviewer 2: +- [ ] Submit PR against `edx-platform` to bump the version +- [ ] Upload to PyPi diff --git a/.gitignore b/.gitignore index 96d74d46..7bbf64cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ *.egg-info/ node_modules/ +*.log +package-lock.json *.pyc *.sw[op] -.coverage -coverage.xml .tox/ -.DS_STORE +sdk/ +venv/ diff --git a/.travis.yml b/.travis.yml index b6510718..f18bfafa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,42 @@ -sudo: false +addons: + apt: + packages: + - nodejs language: python -cache: pip -python: - - '2.7' +sudo: false +matrix: + include: + - python: 3.6 + env: TOXENV=csslint + - python: 3.6 + env: TOXENV=eslint + - python: 3.6 + env: TOXENV=pycodestyle + - python: 3.6 + env: TOXENV=pylint + - python: 3.6 + env: TOXENV=py36 + - python: 2.7 + env: TOXENV=pycodestyle + - python: 2.7 + env: TOXENV=pylint + - python: 2.7 + env: TOXENV=py27 + - python: 2.7 + env: TOXENV=translations_validate before_install: - - 'uname -a' - - 'python --version' + - "pip install -U pip" + - export BOTO_CONFIG=/dev/null +cache: + directories: + - $HOME/.cache/pip install: - - 'pip install tox' - - 'virtualenv --version' - - 'easy_install --version' - - 'pip --version' - - 'tox --version' + - "make requirements" + - "pip install coveralls" script: - - 'tox -v' + - make test branches: only: - - 'master' -env: - - TOXENV=py27-dj18 - - TOXENV=coveralls - - TOXENV=pep8 - - TOXENV=pylint + - master +after_success: + coveralls diff --git a/.tx/config b/.tx/config new file mode 100644 index 00000000..c0f85a52 --- /dev/null +++ b/.tx/config @@ -0,0 +1,8 @@ +[main] +host = https://www.transifex.com + +[stanford-openedx-xblocks.free-text-response] +file_filter = freetextresponse/translations//LC_MESSAGES/text.po +source_file = freetextresponse/translations/en/LC_MESSAGES/text.po +source_lang = en +type = PO diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..6397d91c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM stvstnfrd/workbench:latest +MAINTAINER stv +ADD package.json /root/xblock/ +RUN cd /root/xblock && npm install +ADD . /root/xblock +RUN make -C /root/xblock requirements +RUN pip install -e /root/xblock +ENV HOME /root diff --git a/Makefile b/Makefile new file mode 100755 index 00000000..59fad4f0 --- /dev/null +++ b/Makefile @@ -0,0 +1,107 @@ +#!/usr/bin/make -f +module_root := freetextresponse +css_files := $(patsubst %.less, %.css, $(wildcard ./$(module_root)/public/*.less)) +html_files := $(wildcard $(module_root)/templates/*.html) +js_files := $(wildcard $(module_root)/public/*.js) +py_files := $(wildcard $(module_root)/**/*.py) +files_with_translations := $(js_files) $(html_files) $(py_files) +translation_root := $(module_root)/translations +po_files := $(wildcard $(translation_root)/*/LC_MESSAGES/*.po) +ifneq ($(strip $(language)),) + po_files := $(po_files) $(translation_root)/$(language)/LC_MESSAGES/text.po +endif +ifeq ($(strip $(po_files)),) + po_files = $(translation_root)/en/LC_MESSAGES/text.po +endif +mo_files := $(patsubst %.po,%.mo,$(po_files)) + +.PHONY: help +help: ## This. + @perl -ne 'print if /^[a-zA-Z_-]+:.*## .*$$/' $(MAKEFILE_LIST) \ + | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +.PHONY: runserver +runserver: build_docker ## Run server inside XBlock Workbench container + $(docker_run) $(_NAME) + +.PHONY: clean +clean: ## Remove build artifacts + tox -e clean + rm -rf reports/cover + rm -rf .tox/ + rm -rf *.egg-info/ + rm -rf .eggs/ + rm -rf package-lock.json + rm -rf node_modules/ + find . -name '*.pyc' -delete + find . -name __pycache__ -delete + +.PHONY: quality +quality: requirements # Run all quality checks + tox -e csslint,eslint,pycodestyle,pylint + +.PHONY: requirements +requirements: requirements_js requirements_py ## Install all required packages + +.PHONY: requirements_py +requirements_py: # Install required python packages + pip install tox==3.7.0 + +.PHONY: requirements_js +requirements_js: # Install required javascript packages + npm install + +.PHONY: static +static: requirements_js $(css_files) ## Compile the less->css +$(module_root)/public/%.css: $(module_root)/public/%.less + @echo "$< -> $@" + node_modules/less/bin/lessc $< $@ + +.PHONY: test +test: requirements ## Run all quality checks and unit tests + tox -p all + +# extract +%.po: $(files_with_translations) + mkdir -p $(@D) + ./manage.py makemessages -l "$(patsubst $(translation_root)/%/LC_MESSAGES,%,$(@D))" + mv "$(patsubst %/text.po,%/django.po,$(@))" "$(@)" + +# compile +%.mo: %.po + msgfmt -o "$(@)" "$(<)" + +.PHONY: translations +translations: ## Update translation files + make $(mo_files) + @echo + @echo 'Translations up-to-date.' + @echo "You can add a new language like this:" + @echo ' make $(@) language=fr' + @echo 'where `fr` is the language code.' + @echo + +include *.mk + +_NAME=free-text-response:latest +_VOLUME=-v '$(PWD):/root/xblock' +_PORT= +runserver: _PORT = -p 8000:8000 +docker_test: _VOLUME = -v '$(PWD)/reports:/root/xblock/reports' +docker_run=docker run $(_PORT) $(_VOLUME) --rm -it +docker_make=$(docker_run) --entrypoint make $(_NAME) +docker_make_args=language=$(language) -C /root/xblock +docker_make_more=$(docker_make) $(docker_make_args) + +.PHONY: build_docker +build_docker: + docker build -t $(_NAME) . +.PHONY: docker_static docker_test docker_translations +define run-in-docker +$(docker_make_more) $(patsubst docker_%, %, $@) +endef +docker_shell: + $(docker_run) --entrypoint /bin/bash $(_NAME) +docker_static: ; make build_docker; $(run-in-docker) ## Compile static assets in docker container +docker_translations: ; make build_docker; $(run-in-docker) ## Update translation files in docker container +docker_test: ; make build_docker; $(run-in-docker) ## Run tests in docker container diff --git a/README.markdown b/README.markdown deleted file mode 100644 index ade9618d..00000000 --- a/README.markdown +++ /dev/null @@ -1,29 +0,0 @@ -# Free-text Response XBlock -XBlock to capture a free-text response. - -This package provides an XBlock for use with the EdX Platform and makes -it possible for instructors to create questions that expect a -free-text response. - -Instructors define the following paramters in Studio: -- display name -- display correctness (True/False) -- Full-Credit Phrases (a list of phrases, of which, one must be present in the student's response in order to receive full-credit) -- Half-Credit Phrases (a list of phrases, of which, one must be present in the student's response in order to receive at least half-credit) -- Maximum Number of Attempts -- Maximum Word Count -- Minimum Word Count -- Prompt -- Question Weight - -Students enter and submit their free-text responses, which instantly gets evaluated -according to the parameters above. - -# Installation -- Add the xblock to your requirements/edx/github.text file - e.g. -e git+https://github.com/Stanford-Online/xblock-free-text-response@cfb793db182b60281875b83b53a98640d740ebcf#egg=xblock-free-text-response - -- In Studio Settings/Advanced Settings add the xblock to the Advanced Module List. - e.g. "freetextresponse" - -Now, when you create a component "Free-text Response" should appear in the Advanced Component List. diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..fba0e5b1 --- /dev/null +++ b/README.rst @@ -0,0 +1,48 @@ +Free Text Response XBlock +================================ + +XBlock to capture a free-text response. + +|badge-travis| +|badge-coveralls| + +This package provides an XBlock for use with the EdX Platform and makes +it possible for instructors to create questions that expect a +free-text response. + + +Installation +------------ + + +System Administrator +~~~~~~~~~~~~~~~~~~~~ + +To install the XBlock on your platform, +add the following to your `requirements.txt` file: + + xblock-free-text-response + +You'll also need to add this to your `INSTALLED_APPS`: + + freetextresponse + + +Course Staff +~~~~~~~~~~~~ + +To install the XBlock in your course, +access your `Advanced Module List`: + + Settings -> Advanced Settings -> Advanced Module List + +and add the following: + + freetextresponse + + + +.. |badge-coveralls| image:: https://coveralls.io/repos/github/Stanford-Online/xblock-free-text-response/badge.svg?branch=master + :target: https://coveralls.io/github/Stanford-Online/xblock-free-text-response?branch=master +.. |badge-travis| image:: https://travis-ci.org/Stanford-Online/xblock-free-text-response.svg?branch=master + :target: https://travis-ci.org/Stanford-Online/xblock-free-text-response diff --git a/conf/locale b/conf/locale deleted file mode 120000 index 1b356a91..00000000 --- a/conf/locale +++ /dev/null @@ -1 +0,0 @@ -../freetextresponse/locale/ \ No newline at end of file diff --git a/freetextresponse/__init__.py b/freetextresponse/__init__.py index cc7cb91d..b5c3628c 100644 --- a/freetextresponse/__init__.py +++ b/freetextresponse/__init__.py @@ -3,4 +3,3 @@ Instructors can specify a list of phrases, of which one must be present in order for the student to receive credit. """ -from .freetextresponse import FreeTextResponse diff --git a/freetextresponse/freetextresponse.py b/freetextresponse/freetextresponse.py deleted file mode 100644 index 497bebbc..00000000 --- a/freetextresponse/freetextresponse.py +++ /dev/null @@ -1,692 +0,0 @@ -""" -This is the core logic for the Free-text Response XBlock -""" -from enum import Enum -from django.db import IntegrityError -from django.template.context import Context -from django.utils.translation import ungettext -from django.utils.translation import ugettext_lazy as _ -from django.utils.translation import ugettext -from xblock.core import XBlock -from xblock.fields import Boolean -from xblock.fields import Float -from xblock.fields import Integer -from xblock.fields import List -from xblock.fields import Scope -from xblock.fields import String -from xblock.fragment import Fragment -from xblock.validation import ValidationMessage -from xblockutils.resources import ResourceLoader -from xblockutils.studio_editable import StudioEditableXBlockMixin -from .mixins import EnforceDueDates, MissingDataFetcherMixin - - -MAX_RESPONSES = 3 - - -@XBlock.needs("i18n") -class FreeTextResponse( - EnforceDueDates, - MissingDataFetcherMixin, - StudioEditableXBlockMixin, - XBlock, -): - # pylint: disable=too-many-ancestors, too-many-instance-attributes - """ - Enables instructors to create questions with free-text responses. - """ - - loader = ResourceLoader(__name__) - - @staticmethod - def workbench_scenarios(): - """ - Gather scenarios to be displayed in the workbench - """ - scenarios = [ - ('Free-text Response XBlock', - ''' - - - - - - - - - - - '''), - ] - return scenarios - - display_correctness = Boolean( - display_name=_('Display Correctness?'), - help=_( - 'This is a flag that indicates if the indicator ' - 'icon should be displayed after a student enters ' - 'their response' - ), - default=True, - scope=Scope.settings, - ) - display_other_student_responses = Boolean( - display_name=_('Display Other Student Responses'), - help=_( - 'This will display other student responses to the ' - 'student after they submit their response.' - ), - default=False, - scope=Scope.settings, - ) - displayable_answers = List( - default=[], - scope=Scope.user_state_summary, - help=_('System selected answers to give to students'), - ) - display_name = String( - display_name=_('Display Name'), - help=_( - 'This is the title for this question type' - ), - default='Free-text Response', - scope=Scope.settings, - ) - fullcredit_keyphrases = List( - display_name=_('Full-Credit Key Phrases'), - help=_( - 'This is a list of words or phrases, one of ' - 'which must be present in order for the student\'s answer ' - 'to receive full credit' - ), - default=[], - scope=Scope.settings, - ) - halfcredit_keyphrases = List( - display_name=_('Half-Credit Key Phrases'), - help=_( - 'This is a list of words or phrases, one of ' - 'which must be present in order for the student\'s answer ' - 'to receive half credit' - ), - default=[], - scope=Scope.settings, - ) - max_attempts = Integer( - display_name=_('Maximum Number of Attempts'), - help=_( - 'This is the maximum number of times a ' - 'student is allowed to attempt the problem' - ), - default=0, - values={'min': 1}, - scope=Scope.settings, - ) - max_word_count = Integer( - display_name=_('Maximum Word Count'), - help=_( - 'This is the maximum number of words allowed for this ' - 'question' - ), - default=10000, - values={'min': 1}, - scope=Scope.settings, - ) - min_word_count = Integer( - display_name=_('Minimum Word Count'), - help=_( - 'This is the minimum number of words required ' - 'for this question' - ), - default=1, - values={'min': 1}, - scope=Scope.settings, - ) - prompt = String( - display_name=_('Prompt'), - help=_( - 'This is the prompt students will see when ' - 'asked to enter their response' - ), - default='Please enter your response within this text area', - scope=Scope.settings, - multiline_editor=True, - ) - submitted_message = String( - display_name=_('Submission Received Message'), - help=_( - 'This is the message students will see upon ' - 'submitting their response' - ), - default='Your submission has been received', - scope=Scope.settings, - ) - weight = Integer( - display_name=_('Weight'), - help=_( - 'This assigns an integer value representing ' - 'the weight of this problem' - ), - default=0, - values={'min': 1}, - scope=Scope.settings, - ) - saved_message = String( - display_name=_('Draft Received Message'), - help=_( - 'This is the message students will see upon ' - 'submitting a draft response' - ), - default=( - 'Your answers have been saved but not graded. ' - 'Click "Submit" to grade them.' - ), - scope=Scope.settings, - ) - - count_attempts = Integer( - default=0, - scope=Scope.user_state, - ) - score = Float( - default=0.0, - scope=Scope.user_state, - ) - student_answer = String( - default='', - scope=Scope.user_state, - ) - - has_score = True - - editable_fields = ( - 'display_name', - 'prompt', - 'weight', - 'max_attempts', - 'display_correctness', - 'min_word_count', - 'max_word_count', - 'fullcredit_keyphrases', - 'halfcredit_keyphrases', - 'submitted_message', - 'display_other_student_responses', - 'saved_message', - ) - - def build_fragment( - self, - rendered_template, - initialize_js_func, - additional_css=[], - additional_js=[], - ): - # pylint: disable=dangerous-default-value, too-many-arguments - """ - Creates a fragment for display. - """ - fragment = Fragment(rendered_template) - for item in additional_css: - url = self.runtime.local_resource_url(self, item) - fragment.add_css_url(url) - for item in additional_js: - url = self.runtime.local_resource_url(self, item) - fragment.add_javascript_url(url) - fragment.initialize_js(initialize_js_func) - return fragment - - # Decorate the view in order to support multiple devices e.g. mobile - # See: https://openedx.atlassian.net/wiki/display/MA/Course+Blocks+API - # section 'View @supports(multi_device) decorator' - @XBlock.supports('multi_device') - def student_view(self, context={}): - # pylint: disable=dangerous-default-value - """The main view of FreeTextResponse, displayed when viewing courses. - - The main view which displays the general layout for FreeTextResponse - - Args: - context: Not used for this view. - - Returns: - (Fragment): The HTML Fragment for this XBlock, which determines the - general frame of the FreeTextResponse Question. - """ - display_other_responses = self.display_other_student_responses - self.runtime.service(self, 'i18n') - context.update( - { - 'display_name': self.display_name, - 'indicator_class': self._get_indicator_class(), - 'nodisplay_class': self._get_nodisplay_class(), - 'problem_progress': self._get_problem_progress(), - 'prompt': self.prompt, - 'student_answer': self.student_answer, - 'is_past_due': self.is_past_due(), - 'used_attempts_feedback': self._get_used_attempts_feedback(), - 'visibility_class': self._get_indicator_visibility_class(), - 'word_count_message': self._get_word_count_message(), - 'display_other_responses': display_other_responses, - 'other_responses': self.get_other_answers(), - } - ) - template = self.loader.render_django_template( - 'templates/freetextresponse_view.html', - context=Context(context), - i18n_service=self.runtime.service(self, 'i18n'), - ) - fragment = self.build_fragment( - template, - initialize_js_func='FreeTextResponseView', - additional_css=[ - 'public/view.css', - ], - additional_js=[ - 'public/view.js', - ], - ) - return fragment - - def max_score(self): - """ - Returns the configured number of possible points for this component. - Arguments: - None - Returns: - float: The number of possible points for this component - """ - return self.weight - - @classmethod - def _generate_validation_message(cls, msg): - """ - Helper method to generate a ValidationMessage from - the supplied string - """ - result = ValidationMessage( - ValidationMessage.ERROR, - ugettext(unicode(msg)) - ) - return result - - def validate_field_data(self, validation, data): - """ - Validates settings entered by the instructor. - """ - if data.weight < 0: - msg = FreeTextResponse._generate_validation_message( - 'Weight Attempts cannot be negative' - ) - validation.add(msg) - if data.max_attempts < 0: - msg = FreeTextResponse._generate_validation_message( - 'Maximum Attempts cannot be negative' - ) - validation.add(msg) - if data.min_word_count < 1: - msg = FreeTextResponse._generate_validation_message( - 'Minimum Word Count cannot be less than 1' - ) - validation.add(msg) - if data.min_word_count > data.max_word_count: - msg = FreeTextResponse._generate_validation_message( - 'Minimum Word Count cannot be greater than Max Word Count' - ) - validation.add(msg) - if not data.submitted_message: - msg = FreeTextResponse._generate_validation_message( - 'Submission Received Message cannot be blank' - ) - validation.add(msg) - - def _get_indicator_visibility_class(self): - """ - Returns the visibility class for the correctness indicator html element - """ - if self.display_correctness: - result = '' - else: - result = 'hidden' - return result - - def _get_word_count_message(self): - """ - Returns the word count message - """ - result = ungettext( - "Your response must be " - "between {min} and {max} word.", - "Your response must be " - "between {min} and {max} words.", - self.max_word_count, - ).format( - min=self.min_word_count, - max=self.max_word_count, - ) - return result - - def _get_invalid_word_count_message(self, ignore_attempts=False): - """ - Returns the invalid word count message - """ - result = '' - if ( - (ignore_attempts or self.count_attempts > 0) and - (not self._word_count_valid()) - ): - word_count_message = self._get_word_count_message() - result = ugettext( - "Invalid Word Count. {word_count_message}" - ).format( - word_count_message=word_count_message, - ) - return result - - def _get_indicator_class(self): - """ - Returns the class of the correctness indicator element - """ - result = 'unanswered' - if self.display_correctness and self._word_count_valid(): - if self._determine_credit() == Credit.zero: - result = 'incorrect' - else: - result = 'correct' - return result - - def _word_count_valid(self): - """ - Returns a boolean value indicating whether the current - word count of the user's answer is valid - """ - word_count = len(self.student_answer.split()) - result = ( - word_count <= self.max_word_count and - word_count >= self.min_word_count - ) - return result - - @classmethod - def _is_at_least_one_phrase_present(cls, phrases, answer): - """ - Determines if at least one of the supplied phrases is - present in the given answer - """ - answer = answer.lower() - matches = [ - phrase.lower() in answer - for phrase in phrases - ] - return any(matches) - - def _get_problem_progress(self): - """ - Returns a statement of progress for the XBlock, which depends - on the user's current score - """ - if self.weight == 0: - result = '' - elif self.score == 0.0: - result = "({})".format( - ungettext( - "{weight} point possible", - "{weight} points possible", - self.weight, - ).format( - weight=self.weight, - ) - ) - else: - scaled_score = self.score * self.weight - # No trailing zero and no scientific notation - score_string = ('%.15f' % scaled_score).rstrip('0').rstrip('.') - result = "({})".format( - ungettext( - "{score_string}/{weight} point", - "{score_string}/{weight} points", - self.weight, - ).format( - score_string=score_string, - weight=self.weight, - ) - ) - return result - - def _compute_score(self): - """ - Computes and publishes the user's core for the XBlock - based on their answer - """ - credit = self._determine_credit() - self.score = credit.value - try: - self.runtime.publish( - self, - 'grade', - { - 'value': self.score, - 'max_value': Credit.full.value - } - ) - except IntegrityError: - pass - - def _determine_credit(self): - # Not a standard xlbock pylint disable. - # This is a problem with pylint 'enums and R0204 in general' - """ - Helper Method that determines the level of credit that - the user should earn based on their answer - """ - result = None - if self.student_answer == '' or not self._word_count_valid(): - result = Credit.zero - elif not self.fullcredit_keyphrases \ - and not self.halfcredit_keyphrases: - result = Credit.full - elif FreeTextResponse._is_at_least_one_phrase_present( - self.fullcredit_keyphrases, - self.student_answer - ): - result = Credit.full - elif FreeTextResponse._is_at_least_one_phrase_present( - self.halfcredit_keyphrases, - self.student_answer - ): - result = Credit.half - else: - result = Credit.zero - return result - - def _get_used_attempts_feedback(self): - """ - Returns the text with feedback to the user about the number of attempts - they have used if applicable - """ - result = '' - if self.max_attempts > 0: - result = ungettext( - 'You have used {count_attempts} of {max_attempts} submission', - 'You have used {count_attempts} of {max_attempts} submissions', - self.max_attempts, - ).format( - count_attempts=self.count_attempts, - max_attempts=self.max_attempts, - ) - return result - - def _get_nodisplay_class(self): - """ - Returns the css class for the submit button - """ - result = '' - if self.max_attempts > 0 and self.count_attempts >= self.max_attempts: - result = 'nodisplay' - return result - - def _get_submitted_message(self): - """ - Returns the message to display in the submission-received div - """ - result = '' - if self._word_count_valid(): - result = self.submitted_message - return result - - def _get_user_alert(self, ignore_attempts=False): - """ - Returns the message to display in the user_alert div - depending on the student answer - """ - result = '' - if not self._word_count_valid(): - result = self._get_invalid_word_count_message(ignore_attempts) - return result - - def _can_submit(self): - if self.is_past_due(): - return False - if self.max_attempts == 0: - return True - if self.count_attempts < self.max_attempts: - return True - return False - - @XBlock.json_handler - def submit(self, data, suffix=''): - # pylint: disable=unused-argument - """ - Processes the user's submission - """ - # Fails if the UI submit/save buttons were shut - # down on the previous sumbisson - if self._can_submit(): - self.student_answer = data['student_answer'] - # Counting the attempts and publishing a score - # even if word count is invalid. - self.count_attempts += 1 - self._compute_score() - display_other_responses = self.display_other_student_responses - if display_other_responses and data.get('can_record_response'): - self.store_student_response() - result = { - 'status': 'success', - 'problem_progress': self._get_problem_progress(), - 'indicator_class': self._get_indicator_class(), - 'used_attempts_feedback': self._get_used_attempts_feedback(), - 'nodisplay_class': self._get_nodisplay_class(), - 'submitted_message': self._get_submitted_message(), - 'user_alert': self._get_user_alert( - ignore_attempts=True, - ), - 'other_responses': self.get_other_answers(), - 'display_other_responses': self.display_other_student_responses, - 'visibility_class': self._get_indicator_visibility_class(), - } - return result - - @XBlock.json_handler - def save_reponse(self, data, suffix=''): - # pylint: disable=unused-argument - """ - Processes the user's save - """ - # Fails if the UI submit/save buttons were shut - # down on the previous sumbisson - if self.max_attempts == 0 or self.count_attempts < self.max_attempts: - self.student_answer = data['student_answer'] - result = { - 'status': 'success', - 'problem_progress': self._get_problem_progress(), - 'used_attempts_feedback': self._get_used_attempts_feedback(), - 'nodisplay_class': self._get_nodisplay_class(), - 'submitted_message': '', - 'user_alert': self.saved_message, - 'visibility_class': self._get_indicator_visibility_class(), - } - return result - - def store_student_response(self): - """ - Submit a student answer to the answer pool by appending the given - answer to the end of the list. - """ - # if the answer is wrong, do not display it - if self.score != Credit.full.value: - return - - student_id = self.get_student_id() - # remove any previous answers the student submitted - for index, response in enumerate(self.displayable_answers): - if response['student_id'] == student_id: - del self.displayable_answers[index] - break - - self.displayable_answers.append({ - 'student_id': student_id, - 'answer': self.student_answer, - }) - - # Want to store extra response so student can still see - # MAX_RESPONSES answers if their answer is in the pool. - response_index = -(MAX_RESPONSES+1) - self.displayable_answers = self.displayable_answers[response_index:] - - def get_other_answers(self): - """ - Returns at most MAX_RESPONSES answers from the pool. - - Does not return answers the student had submitted. - """ - student_id = self.get_student_id() - display_other_responses = self.display_other_student_responses - shouldnt_show_other_responses = not display_other_responses - student_answer_incorrect = self._determine_credit() == Credit.zero - if student_answer_incorrect or shouldnt_show_other_responses: - return [] - return_list = [ - response - for response in self.displayable_answers - if response['student_id'] != student_id - ] - - return_list = return_list[-(MAX_RESPONSES):] - return return_list - - -class Credit(Enum): - # pylint: disable=too-few-public-methods - """ - An enumeration of the different types of credit a submission can be - awareded: Zero Credit, Half Credit, and Full Credit - """ - zero = 0.0 - half = 0.5 - full = 1.0 diff --git a/freetextresponse/locale/vi/LC_MESSAGES/django.mo b/freetextresponse/locale/vi/LC_MESSAGES/django.mo deleted file mode 100644 index 83d921952fd5b09b26246772138e3a6e20b54bde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1062 zcmbu7zi-n(6vqve#Urc?kF6D)9lQJ}PJ>WGiz>CEDxnAoDZ0cj$$@=m?#@X{$jsb< z1t!#?17c%pB$o1bAbH~7;5h+`iclr`}mc=WJ>99NU(y$7~06DLjIT7kHt& zjhWJxZJMr#*-G9utf3-Em5c>{1IbyceXf6fhlOHXGkkzvrB? G?fwA6MQx}6 diff --git a/freetextresponse/locale/vi/LC_MESSAGES/django.po b/freetextresponse/locale/vi/LC_MESSAGES/django.po deleted file mode 100644 index b814bfb6..00000000 --- a/freetextresponse/locale/vi/LC_MESSAGES/django.po +++ /dev/null @@ -1,48 +0,0 @@ -# Vietnamese Translations for Free Text Response Xblock -# plural translations are copies of the singular version - -# Copyright (C) 2017 -# This file is distributed under the same license as the free-text-response package. -# Hang Pham , 2017 -# -msgid "" -msgstr "" -"Project-Id-Version: 0.1.2\n" -"Report-Msgid-Bugs-To: mondiaz \n" -"POT-Creation-Date: 2017-06-13 22:13+0000\n" -"PO-Revision-Date: 2017-06-14 22:13+0000\n" -"Last-Translator: mondiaz , 2017\n" -"Language-Team: Vietnamese (https://www.transifex.com/stanford-online/teams/22259/vi/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: vi\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: freetextresponse/freetextresponse.py:427 -msgid "Your response must be between {min} and {max} word." -msgid_plural "Your response must be between {min} and {max} words." -msgstr[0] "Giới hạn {min}-{max} từ." -msgstr[1] "Giới hạn {min}-{max} từ." - -#: freetextresponse/freetextresponse.py:449 -msgid "Invalid Word Count. {word_count_message}" -msgstr "Số từ không hợp lệ. {word_count_message}" - -#: freetextresponse/freetextresponse.py:502 -msgid "{weight} point possible" -msgid_plural "{weight} points possible" -msgstr[0] "" -msgstr[1] "" - -#: freetextresponse/freetextresponse.py:515 -msgid "{score_string}/{weight} point" -msgid_plural "{score_string}/{weight} points" -msgstr[0] "" -msgstr[1] "" - -#: freetextresponse/freetextresponse.py:577 -msgid "You have used {count_attempts} of {max_attempts} submission" -msgid_plural "You have used {count_attempts} of {max_attempts} submissions" -msgstr[0] "Bạn đã thử {count_attempts} của {max_attempts} lần" -msgstr[1] "Bạn đã thử {count_attempts} của {max_attempts} lần" diff --git a/freetextresponse/mixins/__init__.py b/freetextresponse/mixins/__init__.py new file mode 100644 index 00000000..ccb716ce --- /dev/null +++ b/freetextresponse/mixins/__init__.py @@ -0,0 +1,3 @@ +""" +Mixin behavior to XBlocks +""" diff --git a/freetextresponse/mixins.py b/freetextresponse/mixins/dates.py similarity index 68% rename from freetextresponse/mixins.py rename to freetextresponse/mixins/dates.py index 73c1a3dc..6fe36d1a 100644 --- a/freetextresponse/mixins.py +++ b/freetextresponse/mixins/dates.py @@ -1,10 +1,10 @@ """ -Mixins for the Free Text Response XBlock +Extend XBlocks with datetime helpers """ -# pylint: disable=too-few-public-methods import datetime +# pylint: disable=too-few-public-methods class EnforceDueDates(object): """ xBlock Mixin to allow xblocks to check the due date @@ -32,20 +32,3 @@ def is_past_due(self): due = due + graceperiod return now > due return False - - -class MissingDataFetcherMixin(object): - """ - The mixin used for getting the student_id of the current user. - """ - def get_student_id(self): - """ - Get the student id. - """ - if hasattr(self, 'xmodule_runtime'): - student_id = self.xmodule_runtime.anonymous_student_id - # pylint:disable=E1101 - else: - student_id = self.scope_ids.user_id or '' - student_id = unicode(student_id) - return student_id diff --git a/freetextresponse/mixins/fragment.py b/freetextresponse/mixins/fragment.py new file mode 100644 index 00000000..6407bfa2 --- /dev/null +++ b/freetextresponse/mixins/fragment.py @@ -0,0 +1,91 @@ +""" +Mixin fragment/html behavior into XBlocks + +Note: We should resume test coverage for all lines in this file once +split into its own library. +""" +from __future__ import absolute_import + +from xblock.core import XBlock +from xblock.fragment import Fragment + + +class XBlockFragmentBuilderMixin(object): + """ + Create a default XBlock fragment builder + """ + static_css = [ + 'view.css', + ] + static_js = [ + 'view.js', + ] + static_js_init = None + template = 'view.html' + + def provide_context(self, context): # pragma: no cover + """ + Build a context dictionary to render the student view + + This should generally be overriden by child classes. + """ + context = context or {} + context = dict(context) + return context + + @XBlock.supports('multi_device') + def student_view(self, context=None): + """ + Build the fragment for the default student view + """ + template = self.template + context = self.provide_context(context) + static_css = self.static_css or [] + static_js = self.static_js or [] + js_init = self.static_js_init + fragment = self.build_fragment( + template=template, + context=context, + css=static_css, + js=static_js, + js_init=js_init, + ) + return fragment + + def build_fragment( + self, + template='', + context=None, + css=None, + js=None, + js_init=None, + ): + """ + Creates a fragment for display. + """ + context = context or {} + css = css or [] + js = js or [] + rendered_template = '' + if template: # pragma: no cover + template = 'templates/' + template + rendered_template = self.loader.render_django_template( + template, + context=context, + i18n_service=self.runtime.service(self, 'i18n'), + ) + fragment = Fragment(rendered_template) + for item in css: + if item.startswith('/'): + url = item + else: + item = 'public/' + item + url = self.runtime.local_resource_url(self, item) + fragment.add_css_url(url) + for item in js: + item = 'public/' + item + url = self.runtime.local_resource_url(self, item) + fragment.add_javascript_url(url) + if js_init: # pragma: no cover + fragment.initialize_js(js_init) + return fragment diff --git a/freetextresponse/mixins/i18n.py b/freetextresponse/mixins/i18n.py new file mode 100644 index 00000000..cbb7265d --- /dev/null +++ b/freetextresponse/mixins/i18n.py @@ -0,0 +1,32 @@ +""" +Mixin i18n logic +""" +from xblock.core import XBlock + + +@XBlock.needs('i18n') +class I18nXBlockMixin(XBlock): + """ + Make an XBlock translation-aware + """ + + def _i18n_service(self): + """ + Provide the XBlock runtime's i18n service + """ + service = self.runtime.service(self, 'i18n') + return service + + def ugettext(self, text): + """ + Call ugettext from the XBlock i18n service + """ + text = self._i18n_service().ugettext(text) + return text + + def ungettext(self, *args, **kwargs): + """ + Call ungettext from the XBlock i18n service + """ + text = self._i18n_service().ungettext(*args, **kwargs) + return text diff --git a/freetextresponse/mixins/scenario.py b/freetextresponse/mixins/scenario.py new file mode 100644 index 00000000..c5886cdc --- /dev/null +++ b/freetextresponse/mixins/scenario.py @@ -0,0 +1,72 @@ +""" +Mixin workbench behavior into XBlocks +""" +from glob import glob +import pkg_resources + + +def _read_file(file_path): + """ + Read in a file's contents + """ + with open(file_path) as file_input: + file_contents = file_input.read() + return file_contents + + +def _parse_title(file_path): + """ + Parse a title from a file name + """ + title = file_path + title = title.split('/')[-1] + title = '.'.join(title.split('.')[:-1]) + title = ' '.join(title.split('-')) + title = ' '.join([ + word.capitalize() + for word in title.split(' ') + ]) + return title + + +def _read_files(files): + """ + Read the contents of a list of files + """ + file_contents = [ + ( + _parse_title(file_path), + _read_file(file_path), + ) + for file_path in files + ] + return file_contents + + +def _find_files(directory): + """ + Find XML files in the directory + """ + pattern = "{directory}/*.xml".format( + directory=directory, + ) + files = glob(pattern) + return files + + +class XBlockWorkbenchMixin(object): + """ + Provide a default test workbench for the XBlock + """ + + @classmethod + def workbench_scenarios(cls): + """ + Gather scenarios to be displayed in the workbench + """ + module = cls.__module__ + module = module.split('.')[0] + directory = pkg_resources.resource_filename(module, 'scenarios') + files = _find_files(directory) + scenarios = _read_files(files) + return scenarios diff --git a/freetextresponse/mixins/user.py b/freetextresponse/mixins/user.py new file mode 100644 index 00000000..853dc8c5 --- /dev/null +++ b/freetextresponse/mixins/user.py @@ -0,0 +1,22 @@ +""" +Extend XBlock with additional user functionality +""" +from six import text_type + + +# pylint: disable=too-few-public-methods +class MissingDataFetcherMixin(object): + """ + The mixin used for getting the student_id of the current user. + """ + def get_student_id(self): + """ + Get the student id. + """ + if hasattr(self, 'xmodule_runtime'): + student_id = self.xmodule_runtime.anonymous_student_id + # pylint:disable=E1101 + else: + student_id = self.scope_ids.user_id or '' + student_id = text_type(student_id) + return student_id diff --git a/freetextresponse/models.py b/freetextresponse/models.py new file mode 100644 index 00000000..ccb50570 --- /dev/null +++ b/freetextresponse/models.py @@ -0,0 +1,242 @@ +""" +Handle data access logic for the XBlock +""" +from __future__ import absolute_import + +from enum import Enum +from django.db import IntegrityError +from django.utils.translation import ugettext_lazy as _ +from xblock.fields import Boolean +from xblock.fields import Float +from xblock.fields import Integer +from xblock.fields import List +from xblock.fields import Scope +from xblock.fields import String + + +MAX_RESPONSES = 3 + + +class FreeTextResponseModelMixin(object): + """ + Handle data access for Image Modal XBlock instances + """ + + editable_fields = [ + 'display_name', + 'prompt', + 'weight', + 'max_attempts', + 'display_correctness', + 'min_word_count', + 'max_word_count', + 'fullcredit_keyphrases', + 'halfcredit_keyphrases', + 'submitted_message', + 'display_other_student_responses', + 'saved_message', + ] + + display_correctness = Boolean( + display_name=_('Display Correctness?'), + help=_( + 'This is a flag that indicates if the indicator ' + 'icon should be displayed after a student enters ' + 'their response' + ), + default=True, + scope=Scope.settings, + ) + display_other_student_responses = Boolean( + display_name=_('Display Other Student Responses'), + help=_( + 'This will display other student responses to the ' + 'student after they submit their response.' + ), + default=False, + scope=Scope.settings, + ) + displayable_answers = List( + default=[], + scope=Scope.user_state_summary, + help=_('System selected answers to give to students'), + ) + display_name = String( + display_name=_('Display Name'), + help=_( + 'This is the title for this question type' + ), + default='Free-text Response', + scope=Scope.settings, + ) + fullcredit_keyphrases = List( + display_name=_('Full-Credit Key Phrases'), + help=_( + 'This is a list of words or phrases, one of ' + 'which must be present in order for the student\'s answer ' + 'to receive full credit' + ), + default=[], + scope=Scope.settings, + ) + halfcredit_keyphrases = List( + display_name=_('Half-Credit Key Phrases'), + help=_( + 'This is a list of words or phrases, one of ' + 'which must be present in order for the student\'s answer ' + 'to receive half credit' + ), + default=[], + scope=Scope.settings, + ) + max_attempts = Integer( + display_name=_('Maximum Number of Attempts'), + help=_( + 'This is the maximum number of times a ' + 'student is allowed to attempt the problem' + ), + default=0, + values={'min': 1}, + scope=Scope.settings, + ) + max_word_count = Integer( + display_name=_('Maximum Word Count'), + help=_( + 'This is the maximum number of words allowed for this ' + 'question' + ), + default=10000, + values={'min': 1}, + scope=Scope.settings, + ) + min_word_count = Integer( + display_name=_('Minimum Word Count'), + help=_( + 'This is the minimum number of words required ' + 'for this question' + ), + default=1, + values={'min': 1}, + scope=Scope.settings, + ) + prompt = String( + display_name=_('Prompt'), + help=_( + 'This is the prompt students will see when ' + 'asked to enter their response' + ), + default='Please enter your response within this text area', + scope=Scope.settings, + multiline_editor=True, + ) + submitted_message = String( + display_name=_('Submission Received Message'), + help=_( + 'This is the message students will see upon ' + 'submitting their response' + ), + default='Your submission has been received', + scope=Scope.settings, + ) + weight = Integer( + display_name=_('Weight'), + help=_( + 'This assigns an integer value representing ' + 'the weight of this problem' + ), + default=0, + values={'min': 1}, + scope=Scope.settings, + ) + saved_message = String( + display_name=_('Draft Received Message'), + help=_( + 'This is the message students will see upon ' + 'submitting a draft response' + ), + default=( + 'Your answers have been saved but not graded. ' + 'Click "Submit" to grade them.' + ), + scope=Scope.settings, + ) + count_attempts = Integer( + default=0, + scope=Scope.user_state, + ) + score = Float( + default=0.0, + scope=Scope.user_state, + ) + student_answer = String( + default='', + scope=Scope.user_state, + ) + has_score = True + show_in_read_only_mode = True + + def store_student_response(self): + """ + Submit a student answer to the answer pool by appending the given + answer to the end of the list. + """ + # if the answer is wrong, do not display it + if self.score != Credit.full.value: + return + + student_id = self.get_student_id() + # remove any previous answers the student submitted + for index, response in enumerate(self.displayable_answers): + if response['student_id'] == student_id: + del self.displayable_answers[index] + break + + self.displayable_answers.append({ + 'student_id': student_id, + 'answer': self.student_answer, + }) + + # Want to store extra response so student can still see + # MAX_RESPONSES answers if their answer is in the pool. + response_index = -(MAX_RESPONSES+1) + self.displayable_answers = self.displayable_answers[response_index:] + + def max_score(self): + """ + Returns the configured number of possible points for this component. + Arguments: + None + Returns: + float: The number of possible points for this component + """ + return self.weight + + def _compute_score(self): + """ + Computes and publishes the user's core for the XBlock + based on their answer + """ + credit = self._determine_credit() + self.score = credit.value + try: + self.runtime.publish( + self, + 'grade', + { + 'value': self.score, + 'max_value': Credit.full.value + } + ) + except IntegrityError: + pass + + +class Credit(Enum): + # pylint: disable=too-few-public-methods + """ + An enumeration of the different types of credit a submission can be + awareded: Zero Credit, Half Credit, and Full Credit + """ + zero = 0.0 + half = 0.5 + full = 1.0 diff --git a/freetextresponse/public/view.js b/freetextresponse/public/view.js index 54c1d299..0fa0d3e7 100644 --- a/freetextresponse/public/view.js +++ b/freetextresponse/public/view.js @@ -1,4 +1,12 @@ +/* eslint-disable no-unused-vars */ +/** + * Initialize the FreeTextResponse student view + * @param {Object} runtime - The XBlock JS Runtime + * @param {Object} element - The containing DOM element for this instance of the XBlock + * @returns {undefined} nothing + */ function FreeTextResponseView(runtime, element) { + /* eslint-enable no-unused-vars */ 'use strict'; var $ = window.jQuery; @@ -18,12 +26,12 @@ function FreeTextResponseView(runtime, element) { var responseList = $element.find('.response-list'); var url = runtime.handlerUrl(element, 'submit'); var urlSave = runtime.handlerUrl(element, 'save_reponse'); - var xblockId = $element.attr('data-usage-id'); var cachedAnswerId = xblockId + '_cached_answer'; var problemProgressId = xblockId + '_problem_progress'; var usedAttemptsFeedbackId = xblockId + '_used_attempts_feedback'; - if ($xblocksContainer.data(cachedAnswerId) !== undefined) { + + if (typeof $xblocksContainer.data(cachedAnswerId) !== 'undefined') { textareaStudentAnswer.text($xblocksContainer.data(cachedAnswerId)); problemProgress.text($xblocksContainer.data(problemProgressId)); usedAttemptsFeedback.text($xblocksContainer.data(usedAttemptsFeedbackId)); @@ -31,14 +39,50 @@ function FreeTextResponseView(runtime, element) { // POLYFILL notify if it does not exist. Like in the xblock workbench. runtime.notify = runtime.notify || function () { + // eslint-disable-next-line prefer-rest-params, no-console console.log('POLYFILL runtime.notify', arguments); }; - function setClassForTextAreaParent(new_class) { + /** + * Update CSS classes + * @param {string} newClass - a CSS class name to be used + * @returns {undefined} nothing + */ + function setClassForTextAreaParent(newClass) { textareaParent.removeClass('correct'); textareaParent.removeClass('incorrect'); textareaParent.removeClass('unanswered'); - textareaParent.addClass(new_class); + textareaParent.addClass(newClass); + } + + /** + * Convert list of responses to an html string + * @param {Array} responses - a list of Responses + * @returns {string} a string of HTML to add to the page + */ + function getStudentResponsesHtml(responses) { + var html = ''; + var noResponsesText = responseList.data('noresponse'); + responses.forEach(function (item) { + html += '
  • ' + item.answer + '
  • '; + }); + html = html || '
  • ' + noResponsesText + '
  • '; + return html; + } + + /** + * Display responses, if applicable + * @param {Object} response - a jQuery HTTP response + * @returns {undefined} nothing + */ + function displayResponsesIfAnswered(response) { + if (!response.display_other_responses) { + $element.find('.responses-box').addClass('hidden'); + return; + } + var responseHTML = getStudentResponsesHtml(response.other_responses); + responseList.html(responseHTML); + $element.find('.responses-box').removeClass('hidden'); } buttonHide.on('click', function () { @@ -51,13 +95,15 @@ function FreeTextResponseView(runtime, element) { buttonSubmit.text(buttonSubmit[0].dataset.checking); runtime.notify('submit', { message: 'Submitting...', - state: 'start' + state: 'start', }); $.ajax(url, { type: 'POST', data: JSON.stringify({ - 'student_answer': $element.find('.student_answer').val(), - 'can_record_response': $element.find('.messageCheckbox').prop('checked') + // eslint-disable-next-line camelcase + student_answer: $element.find('.student_answer').val(), + // eslint-disable-next-line camelcase + can_record_response: $element.find('.messageCheckbox').prop('checked'), }), success: function buttonSubmitOnSuccess(response) { usedAttemptsFeedback.text(response.used_attempts_feedback); @@ -75,49 +121,27 @@ function FreeTextResponseView(runtime, element) { $xblocksContainer.data(usedAttemptsFeedbackId, response.used_attempts_feedback); runtime.notify('submit', { - state: 'end' + state: 'end', }); }, error: function buttonSubmitOnError() { runtime.notify('error', {}); - } + }, }); return false; }); - function getStudentResponsesHtml(responses) { - /* - Convert list of responses to a html string to add to the page - */ - var html = ''; - var noResponsesText = responseList.data('noresponse'); - responses.forEach(function(item) { - html += '
  • ' + item.answer + '
  • '; - }); - html = html || '
  • ' + noResponsesText + '
  • '; - return html; - } - - function displayResponsesIfAnswered(response) { - if (!response.display_other_responses) { - $element.find('.responses-box').addClass('hidden'); - return; - } - var responseHTML = getStudentResponsesHtml(response.other_responses); - responseList.html(responseHTML); - $element.find('.responses-box').removeClass('hidden'); - } - buttonSave.on('click', function () { buttonSave.text(buttonSave[0].dataset.checking); runtime.notify('save', { message: 'Saving...', - state: 'start' + state: 'start', }); $.ajax(urlSave, { type: 'POST', data: JSON.stringify({ - 'student_answer': $element.find('.student_answer').val() + // eslint-disable-next-line camelcase + student_answer: $element.find('.student_answer').val(), }), success: function buttonSaveOnSuccess(response) { buttonSubmit.addClass(response.nodisplay_class); @@ -133,17 +157,18 @@ function FreeTextResponseView(runtime, element) { $xblocksContainer.data(usedAttemptsFeedbackId, response.used_attempts_feedback); runtime.notify('save', { - state: 'end' + state: 'end', }); }, error: function buttonSaveOnError() { runtime.notify('error', {}); - } + }, }); return false; }); - textareaStudentAnswer.on('keydown', function() { + textareaStudentAnswer.on('keydown', function () { + // Reset Messages submissionReceivedMessage.text(''); userAlertMessage.text(''); diff --git a/freetextresponse/scenarios/free-text-response-many.xml b/freetextresponse/scenarios/free-text-response-many.xml new file mode 100644 index 00000000..cad93883 --- /dev/null +++ b/freetextresponse/scenarios/free-text-response-many.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + diff --git a/freetextresponse/scenarios/free-text-response-single.xml b/freetextresponse/scenarios/free-text-response-single.xml new file mode 100644 index 00000000..1891f46a --- /dev/null +++ b/freetextresponse/scenarios/free-text-response-single.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/freetextresponse/settings.py b/freetextresponse/settings.py index 665e598e..3e6060f0 100644 --- a/freetextresponse/settings.py +++ b/freetextresponse/settings.py @@ -8,12 +8,10 @@ # 'NAME': 'intentionally-omitted', }, } - -TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' - INSTALLED_APPS = ( - 'django_nose', 'freetextresponse', ) - -SECRET_KEY = 'freetextresponse_SECRET_KEY' +LOCALE_PATHS = [ + 'freetextresponse/translations', +] +SECRET_KEY = 'SECRET_KEY' diff --git a/freetextresponse/templates/freetextresponse_view.html b/freetextresponse/templates/view.html similarity index 100% rename from freetextresponse/templates/freetextresponse_view.html rename to freetextresponse/templates/view.html diff --git a/freetextresponse/tests.py b/freetextresponse/tests/test_all.py similarity index 99% rename from freetextresponse/tests.py rename to freetextresponse/tests/test_all.py index b2c7a839..1286cf03 100644 --- a/freetextresponse/tests.py +++ b/freetextresponse/tests/test_all.py @@ -14,7 +14,6 @@ from xblockutils.resources import ResourceLoader from django.db import IntegrityError -from django.template.context import Context from .freetextresponse import Credit from .freetextresponse import FreeTextResponse @@ -96,7 +95,7 @@ def test_generate_validation_message(self): ValidationMessage.ERROR, _(msg) ) - test_result = FreeTextResponse._generate_validation_message(msg) + test_result = self.xblock._generate_validation_message(msg) self.assertEqual( type(result), type(test_result), @@ -192,7 +191,7 @@ def test_build_fragment_prompt_html(self): loader = ResourceLoader('freetextresponse') template = loader.render_django_template( 'templates/freetextresponse_view.html', - context=Context(context), + context=context, ) fragment = self.xblock.build_fragment( template, diff --git a/freetextresponse/translations/README.txt b/freetextresponse/translations/README.txt deleted file mode 100644 index 0493bcc4..00000000 --- a/freetextresponse/translations/README.txt +++ /dev/null @@ -1,4 +0,0 @@ -Use this translations directory to provide internationalized strings for your XBlock project. - -For more information on how to enable translations, visit the Open edX XBlock tutorial on Internationalization: -http://edx.readthedocs.org/projects/xblock-tutorial/en/latest/edx_platform/edx_lms.html diff --git a/freetextresponse/translations/ar/LC_MESSAGES/text.mo b/freetextresponse/translations/ar/LC_MESSAGES/text.mo new file mode 100644 index 0000000000000000000000000000000000000000..779618a9ec12c5d44e037fb57f196d9b07351450 GIT binary patch literal 1669 zcmdUu&2Jk;7{&)EUoITz1qlIqFGNMK>s_}^noYM5oD>Xo9VMm}r)uJz+6&%YYj+$w zs&Yu;(hyD@xFR7`+(rpjgj%(gIPp)+T#&eN=mDvB#51-@3Q{jz5F@|-y)*B}JTsd8 z<=Bz08ODpSS7ANaORxtpGn^+FI|{ns3!o1k0Y3zfgB9=?*d6mr@EOd%0ZrbGF?Yu6 zgE8-b4a9!}e}_#x$=EBHzlo0*#2r(mWRqzv!lS#dn-VLL6^hhErhw(p` z7SbeYNw&mS1Y2q(9k$e7jzY;UiLl;~?31LOQYx}m5@&*_m_U++rx$``(w{h`S zRq)88*_1DbH9FpqvXy!He5cc~rD+*n5nWT4Pg)|jM9|Gk6lD3yf^#a*)BKwUojl@c zu3Re5AKr*-JGo*KOA$lhY6~ZoqAT-wB4Ip3wFXZ!A^nf5=d9C*d1luuBDLn@S`y%0 zd31U?l(~gwJLOI5T#`naM{#R7&HTw38o7S#`wqQB4k7CME?jry7T^|)`-bZ{XZ;g2 zF){Mb_^zoqaTs&mM}pJS2%bdn)J%@4UDa3ndYe>V_tcKs(;NCLy~Px}kp1>nQlv1P5wBdQsZ-mY6p7_^tMrs)1fn5b&EmguDVIs t>vO`=rrv_1cJ&sjZt6|K-!@i8pOxMkeN<`>*Mzg%|J(8{z5ZW|{}XG~d5r)7 literal 0 HcmV?d00001 diff --git a/freetextresponse/translations/ar/LC_MESSAGES/text.po b/freetextresponse/translations/ar/LC_MESSAGES/text.po new file mode 100644 index 00000000..2bfdecf5 --- /dev/null +++ b/freetextresponse/translations/ar/LC_MESSAGES/text.po @@ -0,0 +1,231 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +# Translators: +# Ahmad AbdArrahman , 2019 +# Nabeel El-Dughailib , 2019 +# may , 2019 +# abdallah.nassif , 2019 +# Natalia Berdnikov , 2019 +# e2f_ar r3 , 2019 +# Roy Zakka, 2019 +# Omar Al-Ithawi , 2019 +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-09-26 07:22+0000\n" +"PO-Revision-Date: 2019-07-04 00:16+0000\n" +"Last-Translator: Omar Al-Ithawi , 2019\n" +"Language-Team: Arabic (https://www.transifex.com/open-edx/teams/6205/ar/)\n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +#: freetextresponse/models.py:41 +msgid "Display Correctness?" +msgstr "" + +#: freetextresponse/models.py:43 +msgid "" +"This is a flag that indicates if the indicator icon should be displayed " +"after a student enters their response" +msgstr "" + +#: freetextresponse/models.py:51 +msgid "Display Other Student Responses" +msgstr "" + +#: freetextresponse/models.py:53 +msgid "" +"This will display other student responses to the student after they submit " +"their response." +msgstr "" + +#: freetextresponse/models.py:62 +msgid "System selected answers to give to students" +msgstr "" + +#: freetextresponse/models.py:65 +msgid "Display Name" +msgstr "عرض الاسم" + +#: freetextresponse/models.py:67 +msgid "This is the title for this question type" +msgstr "" + +#: freetextresponse/models.py:73 +msgid "Full-Credit Key Phrases" +msgstr "" + +#: freetextresponse/models.py:75 +msgid "" +"This is a list of words or phrases, one of which must be present in order " +"for the student's answer to receive full credit" +msgstr "" + +#: freetextresponse/models.py:83 +msgid "Half-Credit Key Phrases" +msgstr "" + +#: freetextresponse/models.py:85 +msgid "" +"This is a list of words or phrases, one of which must be present in order " +"for the student's answer to receive half credit" +msgstr "" + +#: freetextresponse/models.py:93 +msgid "Maximum Number of Attempts" +msgstr "" + +#: freetextresponse/models.py:95 +msgid "" +"This is the maximum number of times a student is allowed to attempt the " +"problem" +msgstr "" + +#: freetextresponse/models.py:103 +msgid "Maximum Word Count" +msgstr "أقصى عدد للكلمات" + +#: freetextresponse/models.py:105 +msgid "This is the maximum number of words allowed for this question" +msgstr "" + +#: freetextresponse/models.py:113 +msgid "Minimum Word Count" +msgstr "أقل عدد للكلمات" + +#: freetextresponse/models.py:115 +msgid "This is the minimum number of words required for this question" +msgstr "" + +#: freetextresponse/models.py:123 +msgid "Prompt" +msgstr "علامة الاستعداد" + +#: freetextresponse/models.py:125 +msgid "This is the prompt students will see when asked to enter their response" +msgstr "" + +#: freetextresponse/models.py:133 +msgid "Submission Received Message" +msgstr "" + +#: freetextresponse/models.py:135 +msgid "This is the message students will see upon submitting their response" +msgstr "" + +#: freetextresponse/models.py:142 +msgid "Weight" +msgstr "القيمة" + +#: freetextresponse/models.py:144 +msgid "This assigns an integer value representing the weight of this problem" +msgstr "" + +#: freetextresponse/models.py:152 +msgid "Draft Received Message" +msgstr "" + +#: freetextresponse/models.py:154 +msgid "This is the message students will see upon submitting a draft response" +msgstr "" + +#: freetextresponse/templates/view.html:15 +msgid "" +"Allow my response to possibly be visible by other learners after submitting " +"their response" +msgstr "" + +#: freetextresponse/templates/view.html:21 +#: freetextresponse/templates/view.html:24 +msgid "Checking..." +msgstr "جاري التحقّق..." + +#: freetextresponse/templates/view.html:21 +#: freetextresponse/templates/view.html:22 +msgid "Submit" +msgstr "تقديم" + +#: freetextresponse/templates/view.html:24 +#: freetextresponse/templates/view.html:25 +msgid "Save" +msgstr "حفظ" + +#: freetextresponse/templates/view.html:34 +msgid "Hide" +msgstr "إخفاء" + +#: freetextresponse/templates/view.html:35 +msgid "Show" +msgstr "إظهار" + +#: freetextresponse/templates/view.html:36 +msgid "peer responses" +msgstr "" + +#: freetextresponse/templates/view.html:38 +msgid "Submissions by others" +msgstr "" + +#: freetextresponse/templates/view.html:39 +#: freetextresponse/templates/view.html:43 +msgid "No responses to show at this time" +msgstr "" + +#: freetextresponse/views.py:126 +#, python-brace-format +msgid "{weight} point possible" +msgid_plural "{weight} points possible" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" +msgstr[5] "" + +#: freetextresponse/views.py:139 +#, python-brace-format +msgid "{score_string}/{weight} point" +msgid_plural "{score_string}/{weight} points" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" +msgstr[5] "" + +#: freetextresponse/views.py:157 +#, python-brace-format +msgid "You have used {count_attempts} of {max_attempts} submission" +msgid_plural "You have used {count_attempts} of {max_attempts} submissions" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" +msgstr[5] "" + +#: freetextresponse/views.py:181 +#, python-brace-format +msgid "Your response must be between {min} and {max} word." +msgid_plural "Your response must be between {min} and {max} words." +msgstr[0] "يجب أن يكون جوابك بين {min} و{max} حرفا." +msgstr[1] "يجب أن يكون جوابك بين {min} و{max} حرفا." +msgstr[2] "يجب أن يكون جوابك بين {min} و{max} حرفا." +msgstr[3] "يجب أن يكون جوابك بين {min} و{max} حروف." +msgstr[4] "يجب أن يكون جوابك بين {min} و{max} حرفا." +msgstr[5] "يجب أن يكون جوابك بين {min} و{max} حرفا." + +#: freetextresponse/views.py:277 +#, python-brace-format +msgid "Invalid Word Count. {word_count_message}" +msgstr "عدد كلمات غير صالح. {word_count_message}" diff --git a/freetextresponse/translations/en/LC_MESSAGES/text.mo b/freetextresponse/translations/en/LC_MESSAGES/text.mo new file mode 100644 index 0000000000000000000000000000000000000000..4bc92a5b3d4c33df66284940e5cc34db749e3739 GIT binary patch literal 429 zcmYL_Pfx-y7>6->+R?Lz9=zd;0~3V;hRF&p?q9|r>a9YZGoxMFV)TRf_53WpQzO6R z$B(l!RV9UpAFTVK}MEj?%^6!KzUZvHktJ9oLyEX@$h&K5Fc3#k)PPD_+VW zS-)^?gdH#Q8T9vFQ|no1U~;WCr7S4vKb6)=tDWW*%#_|5N@V>rG~*!7*_>rV@;fnR N_i0lcUvucRz5)5rceVfk literal 0 HcmV?d00001 diff --git a/freetextresponse/translations/en/LC_MESSAGES/text.po b/freetextresponse/translations/en/LC_MESSAGES/text.po new file mode 100644 index 00000000..de714157 --- /dev/null +++ b/freetextresponse/translations/en/LC_MESSAGES/text.po @@ -0,0 +1,204 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-09-26 07:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: freetextresponse/models.py:41 +msgid "Display Correctness?" +msgstr "" + +#: freetextresponse/models.py:43 +msgid "" +"This is a flag that indicates if the indicator icon should be displayed " +"after a student enters their response" +msgstr "" + +#: freetextresponse/models.py:51 +msgid "Display Other Student Responses" +msgstr "" + +#: freetextresponse/models.py:53 +msgid "" +"This will display other student responses to the student after they submit " +"their response." +msgstr "" + +#: freetextresponse/models.py:62 +msgid "System selected answers to give to students" +msgstr "" + +#: freetextresponse/models.py:65 +msgid "Display Name" +msgstr "" + +#: freetextresponse/models.py:67 +msgid "This is the title for this question type" +msgstr "" + +#: freetextresponse/models.py:73 +msgid "Full-Credit Key Phrases" +msgstr "" + +#: freetextresponse/models.py:75 +msgid "" +"This is a list of words or phrases, one of which must be present in order " +"for the student's answer to receive full credit" +msgstr "" + +#: freetextresponse/models.py:83 +msgid "Half-Credit Key Phrases" +msgstr "" + +#: freetextresponse/models.py:85 +msgid "" +"This is a list of words or phrases, one of which must be present in order " +"for the student's answer to receive half credit" +msgstr "" + +#: freetextresponse/models.py:93 +msgid "Maximum Number of Attempts" +msgstr "" + +#: freetextresponse/models.py:95 +msgid "" +"This is the maximum number of times a student is allowed to attempt the " +"problem" +msgstr "" + +#: freetextresponse/models.py:103 +msgid "Maximum Word Count" +msgstr "" + +#: freetextresponse/models.py:105 +msgid "This is the maximum number of words allowed for this question" +msgstr "" + +#: freetextresponse/models.py:113 +msgid "Minimum Word Count" +msgstr "" + +#: freetextresponse/models.py:115 +msgid "This is the minimum number of words required for this question" +msgstr "" + +#: freetextresponse/models.py:123 +msgid "Prompt" +msgstr "" + +#: freetextresponse/models.py:125 +msgid "This is the prompt students will see when asked to enter their response" +msgstr "" + +#: freetextresponse/models.py:133 +msgid "Submission Received Message" +msgstr "" + +#: freetextresponse/models.py:135 +msgid "This is the message students will see upon submitting their response" +msgstr "" + +#: freetextresponse/models.py:142 +msgid "Weight" +msgstr "" + +#: freetextresponse/models.py:144 +msgid "This assigns an integer value representing the weight of this problem" +msgstr "" + +#: freetextresponse/models.py:152 +msgid "Draft Received Message" +msgstr "" + +#: freetextresponse/models.py:154 +msgid "This is the message students will see upon submitting a draft response" +msgstr "" + +#: freetextresponse/templates/view.html:15 +msgid "" +"Allow my response to possibly be visible by other learners after submitting " +"their response" +msgstr "" + +#: freetextresponse/templates/view.html:21 +#: freetextresponse/templates/view.html:24 +msgid "Checking..." +msgstr "" + +#: freetextresponse/templates/view.html:21 +#: freetextresponse/templates/view.html:22 +msgid "Submit" +msgstr "" + +#: freetextresponse/templates/view.html:24 +#: freetextresponse/templates/view.html:25 +msgid "Save" +msgstr "" + +#: freetextresponse/templates/view.html:34 +msgid "Hide" +msgstr "" + +#: freetextresponse/templates/view.html:35 +msgid "Show" +msgstr "" + +#: freetextresponse/templates/view.html:36 +msgid "peer responses" +msgstr "" + +#: freetextresponse/templates/view.html:38 +msgid "Submissions by others" +msgstr "" + +#: freetextresponse/templates/view.html:39 +#: freetextresponse/templates/view.html:43 +msgid "No responses to show at this time" +msgstr "" + +#: freetextresponse/views.py:126 +#, python-brace-format +msgid "{weight} point possible" +msgid_plural "{weight} points possible" +msgstr[0] "" +msgstr[1] "" + +#: freetextresponse/views.py:139 +#, python-brace-format +msgid "{score_string}/{weight} point" +msgid_plural "{score_string}/{weight} points" +msgstr[0] "" +msgstr[1] "" + +#: freetextresponse/views.py:157 +#, python-brace-format +msgid "You have used {count_attempts} of {max_attempts} submission" +msgid_plural "You have used {count_attempts} of {max_attempts} submissions" +msgstr[0] "" +msgstr[1] "" + +#: freetextresponse/views.py:181 +#, python-brace-format +msgid "Your response must be between {min} and {max} word." +msgid_plural "Your response must be between {min} and {max} words." +msgstr[0] "" +msgstr[1] "" + +#: freetextresponse/views.py:277 +#, python-brace-format +msgid "Invalid Word Count. {word_count_message}" +msgstr "" diff --git a/freetextresponse/translations/eo/LC_MESSAGES/text.mo b/freetextresponse/translations/eo/LC_MESSAGES/text.mo new file mode 100644 index 0000000000000000000000000000000000000000..2e4346eb610994eb7076e4327fbc6f6d99ffde39 GIT binary patch literal 421 zcmYL^Pfx-y9ELG^+R?Lznt0IQ564Ut3K%OZxVV2AgG6r?>YR>tX^YVh;@9)D*r|~> zdD1q0+P?kT-}`LY>{t#gN0wd7sijHJ@@hBxc5Iygq)NWBz7`hOJyL^MY)2TO`#!|7&&Q@!&q@<>>==(;TU;tw-0U~ybI)BV(+}|+E@`X z{#G`Z|9SV6WDq6b6Lg#4C}=+)5JM5Yg@bx*gXUOJTUM&wbRkx8d*Rtgcuo z259-rl;&nM&nM8^dQGh-u7HZAT$Q50sP|YJC#^P$RWOlituYey&r*-P0s5Vejs-d# Kq3+4RX?+7O#dJ^r literal 0 HcmV?d00001 diff --git a/freetextresponse/translations/eo/LC_MESSAGES/text.po b/freetextresponse/translations/eo/LC_MESSAGES/text.po new file mode 100644 index 00000000..af087c30 --- /dev/null +++ b/freetextresponse/translations/eo/LC_MESSAGES/text.po @@ -0,0 +1,204 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-09-26 07:23+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: freetextresponse/models.py:41 +msgid "Display Correctness?" +msgstr "" + +#: freetextresponse/models.py:43 +msgid "" +"This is a flag that indicates if the indicator icon should be displayed " +"after a student enters their response" +msgstr "" + +#: freetextresponse/models.py:51 +msgid "Display Other Student Responses" +msgstr "" + +#: freetextresponse/models.py:53 +msgid "" +"This will display other student responses to the student after they submit " +"their response." +msgstr "" + +#: freetextresponse/models.py:62 +msgid "System selected answers to give to students" +msgstr "" + +#: freetextresponse/models.py:65 +msgid "Display Name" +msgstr "" + +#: freetextresponse/models.py:67 +msgid "This is the title for this question type" +msgstr "" + +#: freetextresponse/models.py:73 +msgid "Full-Credit Key Phrases" +msgstr "" + +#: freetextresponse/models.py:75 +msgid "" +"This is a list of words or phrases, one of which must be present in order " +"for the student's answer to receive full credit" +msgstr "" + +#: freetextresponse/models.py:83 +msgid "Half-Credit Key Phrases" +msgstr "" + +#: freetextresponse/models.py:85 +msgid "" +"This is a list of words or phrases, one of which must be present in order " +"for the student's answer to receive half credit" +msgstr "" + +#: freetextresponse/models.py:93 +msgid "Maximum Number of Attempts" +msgstr "" + +#: freetextresponse/models.py:95 +msgid "" +"This is the maximum number of times a student is allowed to attempt the " +"problem" +msgstr "" + +#: freetextresponse/models.py:103 +msgid "Maximum Word Count" +msgstr "" + +#: freetextresponse/models.py:105 +msgid "This is the maximum number of words allowed for this question" +msgstr "" + +#: freetextresponse/models.py:113 +msgid "Minimum Word Count" +msgstr "" + +#: freetextresponse/models.py:115 +msgid "This is the minimum number of words required for this question" +msgstr "" + +#: freetextresponse/models.py:123 +msgid "Prompt" +msgstr "" + +#: freetextresponse/models.py:125 +msgid "This is the prompt students will see when asked to enter their response" +msgstr "" + +#: freetextresponse/models.py:133 +msgid "Submission Received Message" +msgstr "" + +#: freetextresponse/models.py:135 +msgid "This is the message students will see upon submitting their response" +msgstr "" + +#: freetextresponse/models.py:142 +msgid "Weight" +msgstr "" + +#: freetextresponse/models.py:144 +msgid "This assigns an integer value representing the weight of this problem" +msgstr "" + +#: freetextresponse/models.py:152 +msgid "Draft Received Message" +msgstr "" + +#: freetextresponse/models.py:154 +msgid "This is the message students will see upon submitting a draft response" +msgstr "" + +#: freetextresponse/templates/view.html:15 +msgid "" +"Allow my response to possibly be visible by other learners after submitting " +"their response" +msgstr "" + +#: freetextresponse/templates/view.html:21 +#: freetextresponse/templates/view.html:24 +msgid "Checking..." +msgstr "" + +#: freetextresponse/templates/view.html:21 +#: freetextresponse/templates/view.html:22 +msgid "Submit" +msgstr "" + +#: freetextresponse/templates/view.html:24 +#: freetextresponse/templates/view.html:25 +msgid "Save" +msgstr "" + +#: freetextresponse/templates/view.html:34 +msgid "Hide" +msgstr "" + +#: freetextresponse/templates/view.html:35 +msgid "Show" +msgstr "" + +#: freetextresponse/templates/view.html:36 +msgid "peer responses" +msgstr "" + +#: freetextresponse/templates/view.html:38 +msgid "Submissions by others" +msgstr "" + +#: freetextresponse/templates/view.html:39 +#: freetextresponse/templates/view.html:43 +msgid "No responses to show at this time" +msgstr "" + +#: freetextresponse/views.py:126 +#, python-brace-format +msgid "{weight} point possible" +msgid_plural "{weight} points possible" +msgstr[0] "" +msgstr[1] "" + +#: freetextresponse/views.py:139 +#, python-brace-format +msgid "{score_string}/{weight} point" +msgid_plural "{score_string}/{weight} points" +msgstr[0] "" +msgstr[1] "" + +#: freetextresponse/views.py:157 +#, python-brace-format +msgid "You have used {count_attempts} of {max_attempts} submission" +msgid_plural "You have used {count_attempts} of {max_attempts} submissions" +msgstr[0] "" +msgstr[1] "" + +#: freetextresponse/views.py:181 +#, python-brace-format +msgid "Your response must be between {min} and {max} word." +msgid_plural "Your response must be between {min} and {max} words." +msgstr[0] "" +msgstr[1] "" + +#: freetextresponse/views.py:277 +#, python-brace-format +msgid "Invalid Word Count. {word_count_message}" +msgstr "" diff --git a/freetextresponse/translations/fr_CA/LC_MESSAGES/text.mo b/freetextresponse/translations/fr_CA/LC_MESSAGES/text.mo new file mode 100644 index 0000000000000000000000000000000000000000..932c6ac074c5773d0fc3cd2b240ac9e42fc07175 GIT binary patch literal 5261 zcmc(iO^h5z6~_w_2y93QA$%kRy9gg^C!X20Wjprxi_O{&D`(fM-Ng~e$n;Lv%v9VT zvt8XgyR25?fRJ)P9Ds-jgb)|JIUpf{IB{VlP8>KP#33Lgj$Al^;1Kb9)!j3bwGAki zLTlUqsp_g%@BQCLJ-`0w{oheMhj^dlz5WiRPJrKkC;#yD?^EiF;0E|0_#5ys@b}>R zz`ueA!GD1t2A{ZJsgHn1D_j8iQvv_T86EHm@SEVH;A<8B68r??--7Q3{|54>{#oH0 z;Kvx>_b#PA1Rexs-3jm+@GN)v8bc zpz!o3@crOFz$4%T50&4KgPQRY_;D}=<=k(84e;Bb_~}=m$oXT1e+Qpt{1D2>`ct6z z>jH>r>Ix|PR>6;geGt{v%iwe1w?MJ`ccAcf0Oei=zYBgHeCiRU*1$cm1-=1Z2Ty;X z-1iD7^RI(n0w2e@qR#-l3H}JgCFnKX735DnNKgrX2SMTQItYE9gHM5k zmya_*P4kNFqCd<0Be5e7Zt@Ri_(x(!ctBl$j!a7Y)erQ0z9gn3Zc$%BrH@VFNgiD2 zeMJbW&+vYdS2&dv5t|P1){k)f6tAu)z)$mv?+?k`Tok1P9dGN*xPF>Aqw`evQ|Ih@ zw5`{T-n8=5==E)#<~@_?$OKtpGN*%1&d?R>vCVUvbTyxCHWH-fd!~Jp`DU}JE?U=* zf^9vYW|?W{iE-}2XmTlt&1i7dAF`Skp-FPRQfcMX#fznEqrDiulI)H?$PH`+3|5tqVu)nc$^ z<095eMZAu!X-CiHxrzI^8+{tfP>VM4t0$(G(h(m{m~}na4sxinj4gIAXDRa3YOrb4 zDqq#A5DNcka=W$TZ*JreSMET>wnI1VOa#MF6eMmSe&X0JocZHRTAf;x^8-%lCSam% zlAA6T!b@RLxlcI2f8|v@Ft*#v#eT1NKT8RTxRfG-bSDbL|5#>|(6)ozIBjLMsVCA* z+cq}AKoNx!Sz#FyqN$IhK)cH8oa_~Ub3A#S{b`YPUKw$j(@5@@UO%GK#E6!Io^AJZ zT(FFTDr3-uwINn^*dnS`=S>R*!j(`&cuIun4uPfHKCmY2d^=9=5xVh9L#K2r0!Fpsb2KislJdF zxnqRfn|I?66zZ2K ztc``RagFLkKD6}m-29cf=P&7NmsVC6t}acjn0}h&jYZeBVdJ@?>l$lmOHVCdU6Z>g zkR)ncq&T+pvDu@i8?&by$4=_mXIsaPKQqh2!p4e`8+YHjXB)G}_3Uix=*e>33xUfU zYZ+B53i32V5o@?u^_Ng5{Hn#tMeW=OxDrn?9_ZpX;09dlxXSPY~?fb1vWXO+r1!jCO`N2wablD<8^X& z$7GF5NjnX3b4#CExB1j^RAfQaxSVFOYw4u#hwj|5GrIhF?ogu7>!XLyOsQp)#U{^T zXZS;EsLqCaWup^L4`1`sdz+|Kn20NlAES<^A7nb54hq&#SNq%|Ns#37GaKFlHd6|L zOABsmnj%SoO0v2(+{D)U??O+EfhsoC|6xjpZo+QM#J( zGLEjrIvt84r9Q(wc3RO#Rjm*LhI{Klfp~Ic_~U}!qP!M$I~wlEZf=#~o^(n$vLBiJ z_64DdZM{m(2coJdw+?%3k5kOIW%Fe2(lS-6>&uE=;&7PQG;Y7BYSC@eWjR-KMZ4fW zuk9EAl@w70`ciYKws9&xla9{jG*CgYX}W=2@@kP#%LJ>73QbC9`PQmpXw|zWG&@)nebBbxSW91q+& zA;Bz7mgn&Zr->0Fb>iIGz|wP~a5R=*DqQOzzDcDAL&Cm@ZT|l>nM@7T@UFm9NtqoJ z@!m;&qfxS2WvLeKNZmBEM3F?YWCf8TzTW5GI|}uw*Qb;r+sA&J=rrTjY@|>1e;4_J za5oW#dXqcCyC&s-1rj23j-&X;i4vKk62kX7@y5pf55lQ-A{6b)P@b%dBPDCns=kf0 zynVi3yR&@@^p=1A>Z?JQ_!QOoO(7!Z|C^y31xu;IKH=~ojflx;No(`M%uECo+*6au z$ckg%yUF5F4?J(6tu8}>r?}?nE(Kb=E&7fM!Z`E9iio&Cq0$qRN!L`%QU%ntw2<4z zd_@, YEAR. +# +# Translators: +# Omar Al-Ithawi , 2019 +# Pierre Mailhot , 2019 +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-09-26 07:24+0000\n" +"PO-Revision-Date: 2019-07-04 00:16+0000\n" +"Last-Translator: Pierre Mailhot , 2019\n" +"Language-Team: French (Canada) (https://www.transifex.com/open-edx/" +"teams/6205/fr_CA/)\n" +"Language: fr_CA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: freetextresponse/models.py:41 +msgid "Display Correctness?" +msgstr "Afficher l'exactitude?" + +#: freetextresponse/models.py:43 +msgid "" +"This is a flag that indicates if the indicator icon should be displayed " +"after a student enters their response" +msgstr "" +"C'est un drapeau qui indique si l'icône de l'indicateur doit être affichée " +"après qu'un élève ait entré sa réponse" + +#: freetextresponse/models.py:51 +msgid "Display Other Student Responses" +msgstr "Afficher les réponses des autres étudiants" + +#: freetextresponse/models.py:53 +msgid "" +"This will display other student responses to the student after they submit " +"their response." +msgstr "" +"Cela affichera les réponses des autres étudiants à l’étudiant après qu’il " +"aura soumis sa réponse." + +#: freetextresponse/models.py:62 +msgid "System selected answers to give to students" +msgstr "Réponses sélectionnées par le système à donner aux étudiants" + +#: freetextresponse/models.py:65 +msgid "Display Name" +msgstr "Nom d'affichage" + +#: freetextresponse/models.py:67 +msgid "This is the title for this question type" +msgstr "Ceci est le titre pour ce type de question" + +#: freetextresponse/models.py:73 +msgid "Full-Credit Key Phrases" +msgstr "Phrases clés de crédit complet" + +#: freetextresponse/models.py:75 +msgid "" +"This is a list of words or phrases, one of which must be present in order " +"for the student's answer to receive full credit" +msgstr "" +"Ceci est une liste de mots ou d'expressions, dont l'un doit être présent " +"pour que la réponse de l'élève reçoive un crédit complet." + +#: freetextresponse/models.py:83 +msgid "Half-Credit Key Phrases" +msgstr "Phrases clés de demi crédit" + +#: freetextresponse/models.py:85 +msgid "" +"This is a list of words or phrases, one of which must be present in order " +"for the student's answer to receive half credit" +msgstr "" +"Ceci est une liste de mots ou d'expressions, dont l'un doit être présent " +"pour que la réponse de l'élève reçoive un demi crédit." + +#: freetextresponse/models.py:93 +msgid "Maximum Number of Attempts" +msgstr "Nombre maximum de tentatives" + +#: freetextresponse/models.py:95 +msgid "" +"This is the maximum number of times a student is allowed to attempt the " +"problem" +msgstr "" +"C'est le nombre maximum de fois qu'un étudiant est autorisé à tenter le " +"problème" + +#: freetextresponse/models.py:103 +msgid "Maximum Word Count" +msgstr "Nombre de mots maximum" + +#: freetextresponse/models.py:105 +msgid "This is the maximum number of words allowed for this question" +msgstr "C'est le nombre maximum de mots permis pour cette question" + +#: freetextresponse/models.py:113 +msgid "Minimum Word Count" +msgstr "Nombre de mots minimum" + +#: freetextresponse/models.py:115 +msgid "This is the minimum number of words required for this question" +msgstr "C'est le nombre minimum de mots requis pour cette question" + +#: freetextresponse/models.py:123 +msgid "Prompt" +msgstr "Invite" + +#: freetextresponse/models.py:125 +msgid "This is the prompt students will see when asked to enter their response" +msgstr "" +"Il s’agit de l’invite que les étudiants verront lorsqu’on leur demandera de " +"répondre." + +#: freetextresponse/models.py:133 +msgid "Submission Received Message" +msgstr "Message de soumission reçue" + +#: freetextresponse/models.py:135 +msgid "This is the message students will see upon submitting their response" +msgstr "C'est le message que les étudiants verront en soumettant leur réponse" + +#: freetextresponse/models.py:142 +msgid "Weight" +msgstr "Poids" + +#: freetextresponse/models.py:144 +msgid "This assigns an integer value representing the weight of this problem" +msgstr "Ceci assigne une valeur entière représentant le poids de ce problème" + +#: freetextresponse/models.py:152 +msgid "Draft Received Message" +msgstr "Message d'ébauche reçue" + +#: freetextresponse/models.py:154 +msgid "This is the message students will see upon submitting a draft response" +msgstr "" +"C'est le message que les étudiants verront lors de la soumission d'une " +"ébauche de réponse" + +#: freetextresponse/templates/view.html:15 +msgid "" +"Allow my response to possibly be visible by other learners after submitting " +"their response" +msgstr "" +"Permettre à ma réponse d'être éventuellement visible par d'autres apprenants " +"après avoir soumis leur réponse" + +#: freetextresponse/templates/view.html:21 +#: freetextresponse/templates/view.html:24 +msgid "Checking..." +msgstr "Vérification en cours..." + +#: freetextresponse/templates/view.html:21 +#: freetextresponse/templates/view.html:22 +msgid "Submit" +msgstr "Soumettre" + +#: freetextresponse/templates/view.html:24 +#: freetextresponse/templates/view.html:25 +msgid "Save" +msgstr "Sauvegarder" + +#: freetextresponse/templates/view.html:34 +msgid "Hide" +msgstr "Cacher" + +#: freetextresponse/templates/view.html:35 +msgid "Show" +msgstr "Montrer" + +#: freetextresponse/templates/view.html:36 +msgid "peer responses" +msgstr "réponses des pairs" + +#: freetextresponse/templates/view.html:38 +msgid "Submissions by others" +msgstr "Soumissions par d'autres" + +#: freetextresponse/templates/view.html:39 +#: freetextresponse/templates/view.html:43 +msgid "No responses to show at this time" +msgstr "Aucune réponse à afficher pour le moment" + +#: freetextresponse/views.py:126 +#, python-brace-format +msgid "{weight} point possible" +msgid_plural "{weight} points possible" +msgstr[0] "{weight} point possible" +msgstr[1] "{weight} points possible" + +#: freetextresponse/views.py:139 +#, python-brace-format +msgid "{score_string}/{weight} point" +msgid_plural "{score_string}/{weight} points" +msgstr[0] "{score_string}/{weight} point" +msgstr[1] "{score_string}/{weight} points" + +#: freetextresponse/views.py:157 +#, python-brace-format +msgid "You have used {count_attempts} of {max_attempts} submission" +msgid_plural "You have used {count_attempts} of {max_attempts} submissions" +msgstr[0] "Vous avez utilisé {count_attempts} de {max_attempts} soumission." +msgstr[1] "Vous avez utilisé {count_attempts} de {max_attempts} soumissions." + +#: freetextresponse/views.py:181 +#, python-brace-format +msgid "Your response must be between {min} and {max} word." +msgid_plural "Your response must be between {min} and {max} words." +msgstr[0] "Votre réponse doit comporter entre {min} et {max} mots." +msgstr[1] "Votre réponse doit comporter entre {min} et {max} mots." + +#: freetextresponse/views.py:277 +#, python-brace-format +msgid "Invalid Word Count. {word_count_message}" +msgstr "Nombre de mots invalide. {word_count_message}" + +#~ msgid "Please enter your response within this text area" +#~ msgstr "Veuillez entrer votre réponse dans cette zone de texte" diff --git a/freetextresponse/translations/vi/LC_MESSAGES/text.mo b/freetextresponse/translations/vi/LC_MESSAGES/text.mo new file mode 100644 index 0000000000000000000000000000000000000000..be0010d021c51101b1d51eda12d8512e553abf71 GIT binary patch literal 5223 zcmc&%TWlOx89vaKi_4`H%B2+g(*g}icD;_8wAnONv12EeFR|h!5eO8=eJ3s>z9A-)AX1H2n} z3-~GE?|>fx{t5UX@Lk}if%o4j#Akp*DUJj2Cp>)d9`nHaf#-qu0d06u_0jL!p)0q^;s5C?(Z z1&#q<1Re&y1N=I0=ZAzi0#rce`*R@2|9ha_@54eYq5T9f0R9sAG2j;m&L9{2^|8t@aqUjv7LzXg67c-Kd<7H}ueTmrrd zyapV*2i^ew6*vr>1$oxxdEiOlpMk9BIFhP>7l5q8yTE1OE(Fi|T>&!B*MS`8A5**= z!4TRzft=?FAmp=Ju`aNe^%+1sf)9HQ(pwDx);o+GJ6{OHUc45^3EEbF?qvUE7i71* z*i-CVgdrfI^(O1e7g8u7gVpn^K-Q5pCmCv#P0j0zVBw^iI( zNQ?y1KaKu;J}(X{U8{Hv8j0dq`X-cGKiTP=_NubeIA%AQHAz{9hK{GKv^X4NKeTfg!2T$OS|QT@ne(HP`|Ar&*=CR11OO_$~2T9KjM*w+b&r^A(Tt zkH}P5R=#JXCdH#=))B>|e8dC=l1iDKRd$|Wrq)Rw=``!u*%IIEC(GIHR#DoL;go5@ z$~Bku-4unAS=IyP2UJZk3>&4vfCClFtv2QM_2;ZiY0AEDqN*Bt;~iv#O1BYsR);&*_;h;Z(w!XA-Fm{MgaK z&UQ7D?X9V2aFnE>p+WD+iU`aK(Y1 zMk?wS3VCtTR;T!8lu&?Ugc1$H%kC&kO`un}ES6!RQ{5hWbla2WznQCfa`)KpRNb63 zbqW2Jt14VUfq@dXUm;FS{yogmc~O(7(T-4JS^H5ePiYh5oLU)NcBj+|9$z@l{`p-k zHjmC8d2&m??%u#b{S*$?+;};60;dz|p-3}_MkWp&8Ko1W$7jcnO%EKGwJ0{ZDZQY| zx#46%=SoqL24;?x_*vv}5#YuOH<@u9`3R$RHKfx)>#|SUOYqCx!PM3oh3)2# zYBbkcRU~d+fRc1twVU6o66E4nnWNkasf4wjr4f8TgH31K&2{fdB2nU{12+U)H=o@+ zz7&HsO}3kt6irwHj`~qc?1UvIN_bFBgZ8zMWf$Mo0)|Y?7125inKM zx*;atej~}&`WKoF+icozm)>60d+i{c#%?^4a+dwmya<7;H+(eCh5Q>SXkF%COpq<- z!R}G7!NkoA?Tzb(s>qAM8(8Zy9K@c{tbMOVI1$(F&2!kuQS)0b<1}wK&s!I^UT#pO zwVJL~!BQA?$?iC(+8b{fxOU^MgwSAxjjaIAYL5}2D6F}Zpd5_7ffQu!cN{smtU5yY zZ@tE(;8)lJes#onP#&>ml*%6LuTe??3a;I}hFuZPYZOF?-sefLpmfl)JLR~Ufr%q0 zZYpSZlZ=T$8CzW;+{O*7bz0tF$Qf{s#};hIi(a0~_W#9>4f0GMM|chuX!E`e-Cn+S zOvP4NCm@__tG&kRF7Hl4cIY}ftpJPd<_kzw$34BNa*Jo)uvzMEQf~c<%?A&6Cvy=t zHtC7~2sq7rG_et~9NEl(2cNW=r(B`U!9P5O3nn|Vdz{phs{eRt%X52!_Di}=)c9A% zR?EK^xvK#iyWf|Q-uQc-Ch3C+N}-J1QIXbFANNf{w5}$0_|rBYWt@|onodq_T5!Cg z8a5X=Hsbp0TiiYBwka)ZmQ9K5dmJvnT&j~ge%iti=^N)TGXqmRo16*zUOL^LTi!dZ uzaJ7uAV0hc*I*I*mY8#Ii0sU~=+7*?gpn?o+rHjq1=f9$%X+$A7ykfOBz8dn literal 0 HcmV?d00001 diff --git a/freetextresponse/translations/vi/LC_MESSAGES/text.po b/freetextresponse/translations/vi/LC_MESSAGES/text.po new file mode 100644 index 00000000..73850e98 --- /dev/null +++ b/freetextresponse/translations/vi/LC_MESSAGES/text.po @@ -0,0 +1,219 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +# Translators: +# Omar Al-Ithawi , 2019 +# Vu Bach, 2019 +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-09-26 07:24+0000\n" +"PO-Revision-Date: 2019-07-04 00:16+0000\n" +"Last-Translator: Vu Bach, 2019\n" +"Language-Team: Vietnamese (https://www.transifex.com/open-edx/teams/6205/" +"vi/)\n" +"Language: vi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: freetextresponse/models.py:41 +msgid "Display Correctness?" +msgstr "Hiển Thị Chỉnh Sửa? " + +#: freetextresponse/models.py:43 +msgid "" +"This is a flag that indicates if the indicator icon should be displayed " +"after a student enters their response" +msgstr "" +"Đây là một lá cờ cho biết nếu biểu tượng hiển thị nên xuất hiện sau khi một " +"học viên nhập trả lời của họ." + +#: freetextresponse/models.py:51 +msgid "Display Other Student Responses" +msgstr "Hiển Thị Trả Lời Khác của Học Viên " + +#: freetextresponse/models.py:53 +msgid "" +"This will display other student responses to the student after they submit " +"their response." +msgstr "" +"Sẽ hiển thị những câu trả lời khác của học viên tới học viên sau khi họ đã " +"gửi phản hổi. " + +#: freetextresponse/models.py:62 +msgid "System selected answers to give to students" +msgstr "Hệ thống đã chọn đáp án để đưa cho học viên" + +#: freetextresponse/models.py:65 +msgid "Display Name" +msgstr "Tên hiển thị" + +#: freetextresponse/models.py:67 +msgid "This is the title for this question type" +msgstr "Đây là tiêu đề cho dạng câu hỏi này" + +#: freetextresponse/models.py:73 +msgid "Full-Credit Key Phrases" +msgstr "Từ Khóa Toàn Bộ Điểm " + +#: freetextresponse/models.py:75 +msgid "" +"This is a list of words or phrases, one of which must be present in order " +"for the student's answer to receive full credit" +msgstr "" +"Đây là danh sách những từ hoặc cụm từ, nếu xuất hiện trong đáp án của học " +"viên thì họ sẽ được nhận điểm tối đa. " + +#: freetextresponse/models.py:83 +msgid "Half-Credit Key Phrases" +msgstr "Từ Khóa Nửa Điểm " + +#: freetextresponse/models.py:85 +msgid "" +"This is a list of words or phrases, one of which must be present in order " +"for the student's answer to receive half credit" +msgstr "" +"Đây là danh sách những từ hoặc cụm từ, nếu xuất hiện trong đáp án của học " +"viên thì họ sẽ chỉ được nhận nửa số điểm tối đa. " + +#: freetextresponse/models.py:93 +msgid "Maximum Number of Attempts" +msgstr "Số Lần Thử Tối Đa " + +#: freetextresponse/models.py:95 +msgid "" +"This is the maximum number of times a student is allowed to attempt the " +"problem" +msgstr "Đây là số lần tối đa mà học viên được phép thử đối với câu hỏi." + +#: freetextresponse/models.py:103 +msgid "Maximum Word Count" +msgstr "Số Từ Tối Đa " + +#: freetextresponse/models.py:105 +msgid "This is the maximum number of words allowed for this question" +msgstr "Đây là số lượng từ tối đa dùng cho câu hỏi này. " + +#: freetextresponse/models.py:113 +msgid "Minimum Word Count" +msgstr "Số Từ Tối Thiểu " + +#: freetextresponse/models.py:115 +msgid "This is the minimum number of words required for this question" +msgstr "Đây là số lượng từ tối thiểu dùng cho câu hỏi này. " + +#: freetextresponse/models.py:123 +msgid "Prompt" +msgstr "Gợi ý" + +#: freetextresponse/models.py:125 +msgid "This is the prompt students will see when asked to enter their response" +msgstr "" +"Đây là lời nhắc mà học viên sẽ thấy khi họ được hỏi để nhập câu trả lời " + +#: freetextresponse/models.py:133 +msgid "Submission Received Message" +msgstr "Tin Nhắn Sau Khi Đã Nộp Bài " + +#: freetextresponse/models.py:135 +msgid "This is the message students will see upon submitting their response" +msgstr "Đây là tin nhắn học viên sẽ thấy sau khi nộp câu trả lời của họ " + +#: freetextresponse/models.py:142 +msgid "Weight" +msgstr "Weight" + +#: freetextresponse/models.py:144 +msgid "This assigns an integer value representing the weight of this problem" +msgstr "Đặt một giá trị số hiển thị tỷ trọng của câu hỏi " + +#: freetextresponse/models.py:152 +msgid "Draft Received Message" +msgstr "Tin Nhắn Sau Khi Đã Nộp Nháp " + +#: freetextresponse/models.py:154 +msgid "This is the message students will see upon submitting a draft response" +msgstr "Đây là tin nhắn học viên sẽ thấy sau khi nộp bản nháp phản h của họ" + +#: freetextresponse/templates/view.html:15 +msgid "" +"Allow my response to possibly be visible by other learners after submitting " +"their response" +msgstr "" +"Cho phép phản hổi của mình được nhìn thấy bởi học viên khác sau khi họ đã " +"nộp bài " + +#: freetextresponse/templates/view.html:21 +#: freetextresponse/templates/view.html:24 +msgid "Checking..." +msgstr "Đang kiểm tra..." + +#: freetextresponse/templates/view.html:21 +#: freetextresponse/templates/view.html:22 +msgid "Submit" +msgstr "Nộp Bài " + +#: freetextresponse/templates/view.html:24 +#: freetextresponse/templates/view.html:25 +msgid "Save" +msgstr "Lưu" + +#: freetextresponse/templates/view.html:34 +msgid "Hide" +msgstr "Ẩn" + +#: freetextresponse/templates/view.html:35 +msgid "Show" +msgstr "Hiện " + +#: freetextresponse/templates/view.html:36 +msgid "peer responses" +msgstr "trả lời của bạn khác " + +#: freetextresponse/templates/view.html:38 +msgid "Submissions by others" +msgstr "Bài nộp của Học viên khác " + +#: freetextresponse/templates/view.html:39 +#: freetextresponse/templates/view.html:43 +msgid "No responses to show at this time" +msgstr "Hiện tại không có phản hồi nào để hiển thị " + +#: freetextresponse/views.py:126 +#, python-brace-format +msgid "{weight} point possible" +msgid_plural "{weight} points possible" +msgstr[0] "{weight} điểm tối đa " + +#: freetextresponse/views.py:139 +#, python-brace-format +msgid "{score_string}/{weight} point" +msgid_plural "{score_string}/{weight} points" +msgstr[0] "{score_string}/{weight} điểm " + +#: freetextresponse/views.py:157 +#, python-brace-format +msgid "You have used {count_attempts} of {max_attempts} submission" +msgid_plural "You have used {count_attempts} of {max_attempts} submissions" +msgstr[0] "Bạn đã thử {count_attempts} của {max_attempts} lần\"" + +#: freetextresponse/views.py:181 +#, python-brace-format +msgid "Your response must be between {min} and {max} word." +msgid_plural "Your response must be between {min} and {max} words." +msgstr[0] "Giới hạn {min}-{max} từ." + +#: freetextresponse/views.py:277 +#, python-brace-format +msgid "Invalid Word Count. {word_count_message}" +msgstr "Số từ không hợp lệ. {word_count_message}" + +#~ msgid "Please enter your response within this text area" +#~ msgstr "Hãy nhập câu trả lời của bạn tạị khung chữ này " diff --git a/freetextresponse/views.py b/freetextresponse/views.py new file mode 100644 index 00000000..07069fc6 --- /dev/null +++ b/freetextresponse/views.py @@ -0,0 +1,364 @@ +""" +Handle view logic for the XBlock +""" +from __future__ import absolute_import + +from six import text_type +from xblock.core import XBlock +from xblock.validation import ValidationMessage +from xblockutils.resources import ResourceLoader +from xblockutils.studio_editable import StudioEditableXBlockMixin + +from .mixins.dates import EnforceDueDates +from .mixins.fragment import XBlockFragmentBuilderMixin +from .mixins.i18n import I18nXBlockMixin +from .models import Credit +from .models import MAX_RESPONSES + + +# pylint: disable=no-member +class FreeTextResponseViewMixin( + I18nXBlockMixin, + EnforceDueDates, + XBlockFragmentBuilderMixin, + StudioEditableXBlockMixin, +): + """ + Handle view logic for FreeTextResponse XBlock instances + """ + + loader = ResourceLoader(__name__) + static_js_init = 'FreeTextResponseView' + + def provide_context(self, context=None): + """ + Build a context dictionary to render the student view + """ + context = context or {} + context = dict(context) + context.update({ + 'display_name': self.display_name, + 'indicator_class': self._get_indicator_class(), + 'nodisplay_class': self._get_nodisplay_class(), + 'problem_progress': self._get_problem_progress(), + 'prompt': self.prompt, + 'student_answer': self.student_answer, + 'is_past_due': self.is_past_due(), + 'used_attempts_feedback': self._get_used_attempts_feedback(), + 'visibility_class': self._get_indicator_visibility_class(), + 'word_count_message': self._get_word_count_message(), + 'display_other_responses': self.display_other_student_responses, + 'other_responses': self.get_other_answers(), + 'user_alert': '', + 'submitted_message': '', + }) + return context + + def _get_indicator_class(self): + """ + Returns the class of the correctness indicator element + """ + result = 'unanswered' + if self.display_correctness and self._word_count_valid(): + if self._determine_credit() == Credit.zero: + result = 'incorrect' + else: + result = 'correct' + return result + + def _get_nodisplay_class(self): + """ + Returns the css class for the submit button + """ + result = '' + if self.max_attempts > 0 and self.count_attempts >= self.max_attempts: + result = 'nodisplay' + return result + + def _word_count_valid(self): + """ + Returns a boolean value indicating whether the current + word count of the user's answer is valid + """ + word_count = len(self.student_answer.split()) + result = self.max_word_count >= word_count >= self.min_word_count + return result + + def _determine_credit(self): + # Not a standard xlbock pylint disable. + # This is a problem with pylint 'enums and R0204 in general' + """ + Helper Method that determines the level of credit that + the user should earn based on their answer + """ + result = None + if self.student_answer == '' or not self._word_count_valid(): + result = Credit.zero + elif not self.fullcredit_keyphrases \ + and not self.halfcredit_keyphrases: + result = Credit.full + elif _is_at_least_one_phrase_present( + self.fullcredit_keyphrases, + self.student_answer + ): + result = Credit.full + elif _is_at_least_one_phrase_present( + self.halfcredit_keyphrases, + self.student_answer + ): + result = Credit.half + else: + result = Credit.zero + return result + + def _get_problem_progress(self): + """ + Returns a statement of progress for the XBlock, which depends + on the user's current score + """ + if self.weight == 0: + result = '' + elif self.score == 0.0: + result = "({})".format( + self.ungettext( + "{weight} point possible", + "{weight} points possible", + self.weight, + ).format( + weight=self.weight, + ) + ) + else: + scaled_score = self.score * self.weight + # No trailing zero and no scientific notation + score_string = ('%.15f' % scaled_score).rstrip('0').rstrip('.') + result = "({})".format( + self.ungettext( + "{score_string}/{weight} point", + "{score_string}/{weight} points", + self.weight, + ).format( + score_string=score_string, + weight=self.weight, + ) + ) + return result + + def _get_used_attempts_feedback(self): + """ + Returns the text with feedback to the user about the number of attempts + they have used if applicable + """ + result = '' + if self.max_attempts > 0: + result = self.ungettext( + 'You have used {count_attempts} of {max_attempts} submission', + 'You have used {count_attempts} of {max_attempts} submissions', + self.max_attempts, + ).format( + count_attempts=self.count_attempts, + max_attempts=self.max_attempts, + ) + return result + + def _get_indicator_visibility_class(self): + """ + Returns the visibility class for the correctness indicator html element + """ + if self.display_correctness: + result = '' + else: + result = 'hidden' + return result + + def _get_word_count_message(self): + """ + Returns the word count message + """ + result = self.ungettext( + "Your response must be " + "between {min} and {max} word.", + "Your response must be " + "between {min} and {max} words.", + self.max_word_count, + ).format( + min=self.min_word_count, + max=self.max_word_count, + ) + return result + + def get_other_answers(self): + """ + Returns at most MAX_RESPONSES answers from the pool. + + Does not return answers the student had submitted. + """ + student_id = self.get_student_id() + display_other_responses = self.display_other_student_responses + shouldnt_show_other_responses = not display_other_responses + student_answer_incorrect = self._determine_credit() == Credit.zero + if student_answer_incorrect or shouldnt_show_other_responses: + return [] + return_list = [ + response + for response in self.displayable_answers + if response['student_id'] != student_id + ] + return_list = return_list[-(MAX_RESPONSES):] + return return_list + + @XBlock.json_handler + def submit(self, data, suffix=''): + # pylint: disable=unused-argument + """ + Processes the user's submission + """ + # Fails if the UI submit/save buttons were shut + # down on the previous sumbisson + if self._can_submit(): + self.student_answer = data['student_answer'] + # Counting the attempts and publishing a score + # even if word count is invalid. + self.count_attempts += 1 + self._compute_score() + display_other_responses = self.display_other_student_responses + if display_other_responses and data.get('can_record_response'): + self.store_student_response() + result = { + 'status': 'success', + 'problem_progress': self._get_problem_progress(), + 'indicator_class': self._get_indicator_class(), + 'used_attempts_feedback': self._get_used_attempts_feedback(), + 'nodisplay_class': self._get_nodisplay_class(), + 'submitted_message': self._get_submitted_message(), + 'user_alert': self._get_user_alert( + ignore_attempts=True, + ), + 'other_responses': self.get_other_answers(), + 'display_other_responses': self.display_other_student_responses, + 'visibility_class': self._get_indicator_visibility_class(), + } + return result + + @XBlock.json_handler + def save_reponse(self, data, suffix=''): + # pylint: disable=unused-argument + """ + Processes the user's save + """ + # Fails if the UI submit/save buttons were shut + # down on the previous sumbisson + if self.max_attempts == 0 or self.count_attempts < self.max_attempts: + self.student_answer = data['student_answer'] + result = { + 'status': 'success', + 'problem_progress': self._get_problem_progress(), + 'used_attempts_feedback': self._get_used_attempts_feedback(), + 'nodisplay_class': self._get_nodisplay_class(), + 'submitted_message': '', + 'user_alert': self.saved_message, + 'visibility_class': self._get_indicator_visibility_class(), + } + return result + + def _get_invalid_word_count_message(self, ignore_attempts=False): + """ + Returns the invalid word count message + """ + result = '' + if ( + (ignore_attempts or self.count_attempts > 0) and + (not self._word_count_valid()) + ): + word_count_message = self._get_word_count_message() + result = self.ugettext( + "Invalid Word Count. {word_count_message}" + ).format( + word_count_message=word_count_message, + ) + return result + + def _get_submitted_message(self): + """ + Returns the message to display in the submission-received div + """ + result = '' + if self._word_count_valid(): + result = self.submitted_message + return result + + def _get_user_alert(self, ignore_attempts=False): + """ + Returns the message to display in the user_alert div + depending on the student answer + """ + result = '' + if not self._word_count_valid(): + result = self._get_invalid_word_count_message(ignore_attempts) + return result + + def _can_submit(self): + """ + Determine if a user may submit a response + """ + if self.is_past_due(): + return False + if self.max_attempts == 0: + return True + if self.count_attempts < self.max_attempts: + return True + return False + + def _generate_validation_message(self, text): + """ + Helper method to generate a ValidationMessage from + the supplied string + """ + result = ValidationMessage( + ValidationMessage.ERROR, + self.ugettext(text_type(text)) + ) + return result + + def validate_field_data(self, validation, data): + """ + Validates settings entered by the instructor. + """ + if data.weight < 0: + msg = self._generate_validation_message( + 'Weight Attempts cannot be negative' + ) + validation.add(msg) + if data.max_attempts < 0: + msg = self._generate_validation_message( + 'Maximum Attempts cannot be negative' + ) + validation.add(msg) + if data.min_word_count < 1: + msg = self._generate_validation_message( + 'Minimum Word Count cannot be less than 1' + ) + validation.add(msg) + if data.min_word_count > data.max_word_count: + msg = self._generate_validation_message( + 'Minimum Word Count cannot be greater than Max Word Count' + ) + validation.add(msg) + if not data.submitted_message: + msg = self._generate_validation_message( + 'Submission Received Message cannot be blank' + ) + validation.add(msg) + + +def _is_at_least_one_phrase_present(phrases, answer): + """ + Determines if at least one of the supplied phrases is + present in the given answer + """ + answer = answer.lower() + matches = [ + phrase.lower() in answer + for phrase in phrases + ] + return any(matches) diff --git a/freetextresponse/xblocks.py b/freetextresponse/xblocks.py new file mode 100644 index 00000000..fb847aad --- /dev/null +++ b/freetextresponse/xblocks.py @@ -0,0 +1,23 @@ +""" +This is the core logic for the XBlock +""" +from __future__ import absolute_import +from xblock.core import XBlock + +from .mixins.scenario import XBlockWorkbenchMixin +from .mixins.user import MissingDataFetcherMixin +from .models import FreeTextResponseModelMixin +from .views import FreeTextResponseViewMixin + + +@XBlock.needs('i18n') +class FreeTextResponse( + FreeTextResponseModelMixin, + FreeTextResponseViewMixin, + MissingDataFetcherMixin, + XBlockWorkbenchMixin, + XBlock, +): + """ + A fullscreen image modal XBlock. + """ diff --git a/i18n.mk b/i18n.mk new file mode 100644 index 00000000..f0a2fead --- /dev/null +++ b/i18n.mk @@ -0,0 +1,31 @@ +## Localization targets + +WORKDIR := $(module_root) + +translations_extract: ## extract strings to be translated, outputting .po files + # Extract Python and Django template strings + mkdir -p $(WORKDIR)/translations/en/LC_MESSAGES/ + tox -e translations_extract + # cat $(WORKDIR)/translations/en/LC_MESSAGES/django-partial.po | \ + # grep -v 'Plural-Forms: nplurals' > $(WORKDIR)/translations/en/LC_MESSAGES/text.po + # rm -f $(WORKDIR)/translations/en/LC_MESSAGES/django-partial.po + +translations_compile: ## compile translation files, outputting .mo files for each supported language + tox -e translations_compile + +translations_detect_changed: ## Determines if the source translation files are up-to-date, otherwise exit with a non-zero code. + tox -e translations_detect_changed + +translations_pull: ## pull translations from Transifex + tox -e translations_pull + make translations_compile + +translations_push: translations_extract ## push source translation files (.po) to Transifex + tox -e translations_push + +translations_dummy: ## generate dummy translation (.po) files + tox -e translations_dummy + +translations_build_dummy: translations_extract translations_dummy translations_compile ## generate and compile dummy translation files + +translations_validate: translations_build_dummy translations_detect_changed ## validate translations diff --git a/manage.py b/manage.py old mode 100644 new mode 100755 diff --git a/package.json b/package.json index 34336be7..1c7cce6a 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,10 @@ { - "name": "xblock-free-text-response", - "title": "FreeTextResponse XBlock", - "description": "Enables instructors to create questions with free-text responses.", - "version": "0.4.0", - "homepage": "https://github.com/Stanford-Online/xblock-free-text-response", - "author": { - "name": "Azim Pradhan", - "email": "azim.pradhan@gmail.com" + "scripts": { + "test": "tox" }, - "repository": { - "type": "git", - "url": "https://github.com/Stanford-Online/xblock-free-text-response.git" - }, - "bugs": { - "url": "https://github.com/Stanford-Online/xblock-free-text-response/issues" - }, - "keywords": [ - "openedx", - "xblock" + "devDependencies": [ + "eslint", + "less", + "csslint" ] } diff --git a/pylintrc b/pylintrc index 9256b318..184ab2b2 100644 --- a/pylintrc +++ b/pylintrc @@ -1,8 +1,30 @@ -[VARIABLES] -dummy-variables-rgx=_|dummy +[MESSAGES CONTROL] +disable = + locally-disabled, + locally-enabled, + no-init, + no-self-use, + too-few-public-methods, + too-many-ancestors, + too-many-arguments, + too-many-instance-attributes, + too-many-public-methods, + useless-object-inheritance, [REPORTS] -reports=no +output-format = text -[MESSAGES CONTROL] -disable=locally-disabled +[BASIC] +good-names = f,i,j,k,db,ex,Run,_,__,js +no-docstring-rgx = __.*__$|test_.+|setUp$|setUpClass$|tearDown$|tearDownClass$|Meta$ + +[FORMAT] +ignore-long-lines = ^.*https?://.*$ + +[VARIABLES] +dummy-variables-rgx = _|dummy|unused|.*_unused + +[CLASSES] +defining-attr-methods = __init__,__new__,setUp +valid-classmethod-first-arg = cls +valid-metaclass-classmethod-first-arg = mcs diff --git a/reports/.gitignore b/reports/.gitignore new file mode 100644 index 00000000..72e8ffc0 --- /dev/null +++ b/reports/.gitignore @@ -0,0 +1 @@ +* diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 69a7e07a..00000000 --- a/setup.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[metadata] -description-file = README.markdown - -[nosetests] -cover-package=freetextresponse -cover-tests=1 diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 346807bf..fd2a9650 --- a/setup.py +++ b/setup.py @@ -1,53 +1,39 @@ -import json +""" +A configurable, open text-type response +""" +from os import path from setuptools import setup -from setuptools.command.test import test as TestCommand -class Tox(TestCommand): - user_options = [('tox-args=', 'a', 'Arguments to pass to tox')] +version = '0.5.0' +description = __doc__.strip().split('\n')[0] +this_directory = path.abspath(path.dirname(__file__)) +with open(path.join(this_directory, 'README.rst')) as file_in: + long_description = file_in.read() - def initialize_options(self): - TestCommand.initialize_options(self) - self.tox_args = None - - def finalize_options(self): - TestCommand.finalize_options(self) - self.test_args = [] - self.test_suite = True - - def run_tests(self): - # import here, cause outside the eggs aren't loaded - import tox - import shlex - args = self.tox_args - if args: - args = shlex.split(self.tox_args) - errno = tox.cmdline(args=args) - sys.exit(errno) setup( - name="xblock-free-text-response", - version="0.4.0", - description="Enables instructors to create questions with free-text responses.", + name='xblock-free-text-response', + version=version, + description=description, + long_description=long_description, + author='stv', + author_email='stv@stanford.edu', + url='https://github.com/Stanford-Online/xblock-free-text-response', license='AGPL-3.0', packages=[ 'freetextresponse', ], install_requires=[ - 'coverage', - 'ddt', - 'django<2.0', - 'django_nose', - 'edx-opaque-keys', + 'Django<2.0.0', 'enum34', - 'mock', - 'mako', + 'six', 'XBlock', 'xblock-utils', ], entry_points={ 'xblock.v1': [ - 'freetextresponse = freetextresponse:FreeTextResponse', + 'freetextresponse = freetextresponse.xblocks:FreeTextResponse', ], }, package_dir={ @@ -56,6 +42,7 @@ def run_tests(self): package_data={ "freetextresponse": [ 'public/*', + 'scenarios/*.xml', 'templates/*', ], }, @@ -70,4 +57,10 @@ def run_tests(self): 'Topic :: Education', 'Topic :: Internet :: WWW/HTTP', ], + test_suite='freetextresponse.tests', + tests_require=[ + 'ddt', + 'edx-opaque-keys', + 'mock', + ], ) diff --git a/tox.ini b/tox.ini index a53d9c24..c143acfa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,38 +1,98 @@ [tox] -downloadcache = {toxworkdir}/_download/ -envlist = py27-dj18,coverage,pep8,pylint +envlist = + csslint + eslint + py27 + py36 + pycodestyle + pylint [testenv] -commands = {envpython} manage.py test +deps = + coverage +commands = + coverage run manage.py test + coverage report + coverage html + +[testenv:clean] +commands = + coverage erase +skip_install = True + +[testenv:csslint] +whitelist_externals = {toxinidir}/node_modules/csslint/dist/cli.js +passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH +commands = + {toxinidir}/node_modules/csslint/dist/cli.js freetextresponse/public/ +deps = +skip_install = True + +[testenv:eslint] +whitelist_externals = {toxinidir}/node_modules/eslint/bin/eslint.js +passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH +commands = + {toxinidir}/node_modules/eslint/bin/eslint.js --fix freetextresponse/public/view.js +deps = +skip_install = True -[testenv:pep8] +[testenv:pycodestyle] +passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH deps = - pep8 -commands = {envbindir}/pep8 freetextresponse/ + pycodestyle +commands = + pycodestyle freetextresponse/ [testenv:pylint] +passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH deps = pylint -commands = {envbindir}/pylint freetextresponse/ +commands = + pylint freetextresponse/ -[testenv:coverage] +[testenv:translations_push] deps = - coverage -setenv = - NOSE_COVER_TESTS=1 - NOSE_WITH_COVERAGE=1 + transifex-client commands = - # Added so next command covers declarations properly - {envbindir}/coverage run --source=freetextresponse manage.py test - {envpython} manage.py test --cover-xml + tx push -s -[testenv:coveralls] +[testenv:translations_pull] deps = - coveralls -setenv = - NOSE_COVER_TESTS=1 - NOSE_WITH_COVERAGE=1 -passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH + edx-i18n-tools==0.4.8 + transifex-client +commands = + cd freetextresponse && i18n_tool transifex pull +whitelist_externals = + cd + +[testenv:translations_compile] +deps = + edx-i18n-tools==0.4.8 +commands = + cd freetextresponse && i18n_tool generate +whitelist_externals = + cd + +[testenv:translations_dummy] +deps = + edx-i18n-tools==0.4.8 +commands = + cd freetextresponse && i18n_tool dummy +whitelist_externals = + cd + +[testenv:translations_detect_changed] +deps = + edx-i18n-tools==0.4.8 +commands = + cd freetextresponse && i18n_tool changed +whitelist_externals = + cd + +[testenv:translations_extract] +deps = + edx-i18n-tools==0.4.8 commands = - {envbindir}/coverage run --source=freetextresponse manage.py test - {envbindir}/coveralls + cd freetextresponse && i18n_tool extract +whitelist_externals = + cd