From 2c2cdc58221ef052bb83bfac9a544d8baa81c51b Mon Sep 17 00:00:00 2001 From: Giovanni Cimolin da Silva Date: Thu, 19 Aug 2021 15:07:45 -0300 Subject: [PATCH 01/29] test: Update CI runs to 3.8 --- circle.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/circle.yml b/circle.yml index b09601e4..79cdc521 100644 --- a/circle.yml +++ b/circle.yml @@ -131,42 +131,42 @@ workflows: build_and_deploy: jobs: - build: - name: py35-quality + name: py38-quality test_command: make quality - docker_image: circleci/python:3.5-buster-browsers + docker_image: circleci/python:3.8-browsers filters: tags: only: /.*/ - build: - name: py35-unit + name: py38-unit test_command: make test.unit - docker_image: circleci/python:3.5-buster-browsers + docker_image: circleci/python:3.8-browsers filters: tags: only: /.*/ - build: - name: py35-integration + name: py38-integration test_command: make test.integration - docker_image: circleci/python:3.5-buster-browsers + docker_image: circleci/python:3.8-browsers filters: tags: only: /.*/ - coverage: - name: py35-coverage - docker_image: circleci/python:3.5-buster + name: py38-coverage + docker_image: circleci/python:3.8 filters: tags: only: /.*/ requires: - - py35-quality - - py35-unit - - py35-integration + - py38-quality + - py38-unit + - py38-integration - deploy: - name: py35-deploy-bdist_wheel - docker_image: circleci/python:3.5-buster + name: py38-deploy-bdist_wheel + docker_image: circleci/python:3.8 dist_type: bdist_wheel requires: - - py35-coverage + - py38-coverage filters: tags: only: /v[0-9]+(\.[0-9]+)*/ From 48d306b5760974aa65c343544033ee01f15705bb Mon Sep 17 00:00:00 2001 From: Giovanni Cimolin da Silva Date: Thu, 19 Aug 2021 15:53:37 -0300 Subject: [PATCH 02/29] Upgrade requirements --- test_requirements.txt | 452 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 373 insertions(+), 79 deletions(-) diff --git a/test_requirements.txt b/test_requirements.txt index b1eada07..886fb168 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,87 +1,381 @@ # -# This file is autogenerated by pip-compile +# This file is autogenerated by pip-compile with python 3.8 # To update, run: # # pip-compile --output-file=test_requirements.txt test_requirements.in # --e git+https://github.com/edx/acid-block.git@98aecba94ecbfa934e2d00262741c0ea9f557fc9#egg=acid-xblock # via -r xblock-sdk/requirements/test.txt --e git+https://github.com/edx/django-pyfs.git@2.1#egg=django-pyfs==2.1 # via -r test_requirements.in --e xblock-sdk # via -r test_requirements.in --e git+https://github.com/edx/xblock-utils.git@2.0.0#egg=xblock-utils # via -r test_requirements.in -appdirs==1.4.3 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, fs, virtualenv -astroid==2.3.3 # via pylint -atomicwrites==1.3.0 # via -r xblock-sdk/requirements/test.txt, pytest -attrs==19.3.0 # via -r xblock-sdk/requirements/test.txt, pytest -binaryornot==0.4.4 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, cookiecutter -bok_choy==0.7.1 # via -r test_requirements.in, -r xblock-sdk/requirements/test.txt -boto3==1.12.9 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, fs-s3fs -boto==2.39.0 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt -botocore==1.15.9 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, boto3, s3transfer -certifi==2019.11.28 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, requests -chardet==3.0.4 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, binaryornot, requests -cookiecutter==0.9.0 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt -coverage==5.0.3 # via -r xblock-sdk/requirements/test.txt, pytest-cov -ddt==1.2.2 # via -r test_requirements.in, -r xblock-sdk/requirements/test.txt -distlib==0.3.0 # via -r xblock-sdk/requirements/test.txt, virtualenv -django-mysql==2.4.1 # via -r test_requirements.in -django-nose==1.4.6 # via -r test_requirements.in -django==2.2.10 # via -r test_requirements.in, -r xblock-sdk/requirements/base.txt, django-mysql, django-pyfs, xblock-sdk -docutils==0.15.2 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, botocore -filelock==3.0.12 # via -r xblock-sdk/requirements/test.txt, tox, virtualenv -fs-s3fs==0.1.8 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, django-pyfs -fs==2.0.27 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, django-pyfs, fs-s3fs, xblock -idna==2.8 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, requests -importlib-metadata==1.5.0 # via -r xblock-sdk/requirements/test.txt, pluggy, pytest, tox, virtualenv -importlib-resources==1.0.2 # via -r xblock-sdk/requirements/test.txt, virtualenv -isort==4.3.21 # via pylint -jinja2==2.11.1 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, cookiecutter -jmespath==0.9.5 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, boto3, botocore -lazy-object-proxy==1.4.3 # via astroid -lazy==1.1 # via -r test_requirements.in, -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, acid-xblock, bok-choy -lxml==3.8.0 # via -r test_requirements.in, -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, xblock -mako==1.1.1 # via -r xblock-sdk/requirements/test.txt, acid-xblock, xblock-utils -markupsafe==1.1.1 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, jinja2, mako, xblock -mccabe==0.6.1 # via pylint -mock==3.0.5 # via -r test_requirements.in, -r xblock-sdk/requirements/test.txt -more-itertools==5.0.0 # via -r xblock-sdk/requirements/test.txt, pytest -mysqlclient==1.4.6 # via -r test_requirements.in -needle==0.5.0 # via -r xblock-sdk/requirements/test.txt, bok-choy -nose==1.3.7 # via -r xblock-sdk/requirements/test.txt, django-nose, needle -packaging==20.1 # via -r xblock-sdk/requirements/test.txt, pytest, tox -pathlib2==2.3.5 # via -r xblock-sdk/requirements/test.txt, pytest -pillow==6.2.2 # via -r xblock-sdk/requirements/test.txt, needle -pluggy==0.13.1 # via -r xblock-sdk/requirements/test.txt, pytest, tox -py==1.8.1 # via -r xblock-sdk/requirements/test.txt, pytest, tox -pycodestyle==2.5.0 # via -r test_requirements.in -pylint==2.4.4 # via -r test_requirements.in -pyparsing==2.4.6 # via -r xblock-sdk/requirements/test.txt, packaging -pypng==0.0.20 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt -pytest-cov==2.8.1 # via -r xblock-sdk/requirements/test.txt -pytest-django==3.8.0 # via -r xblock-sdk/requirements/test.txt -pytest-rerunfailures==8.0 # via -r xblock-sdk/requirements/test.txt -pytest==4.6.9 # via -r test_requirements.in, -r xblock-sdk/requirements/test.txt, pytest-cov, pytest-django, pytest-rerunfailures -python-dateutil==2.8.1 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, botocore, xblock -pytz==2019.3 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, django, fs, xblock -pyyaml==5.3 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, cookiecutter, xblock -requests==2.22.0 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt -s3transfer==0.3.3 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, boto3 -selenium==3.4.1 # via -r test_requirements.in, -r xblock-sdk/requirements/test.txt, bok-choy, needle -simplejson==3.17.0 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, xblock-utils -six==1.14.0 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, astroid, bok-choy, django-pyfs, fs, fs-s3fs, mock, more-itertools, packaging, pathlib2, pytest, python-dateutil, tox, virtualenv, xblock -sqlparse==0.3.0 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, django -toml==0.10.0 # via -r xblock-sdk/requirements/test.txt, tox -tox-battery==0.5.2 # via -r xblock-sdk/requirements/test.txt -tox==3.14.5 # via -r xblock-sdk/requirements/test.txt, tox-battery -typed-ast==1.4.1 # via astroid -typing==3.7.4.1 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, fs -urllib3==1.25.8 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, botocore, requests -virtualenv==20.0.7 # via -r xblock-sdk/requirements/test.txt, tox -wcwidth==0.1.8 # via -r xblock-sdk/requirements/test.txt, pytest -web-fragments==0.3.1 # via -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, xblock, xblock-utils -webob==1.8.6 # via -r test_requirements.in, -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, xblock -wrapt==1.11.2 # via astroid -xblock==1.2.9 # via -r test_requirements.in, -r xblock-sdk/requirements/base.txt, -r xblock-sdk/requirements/test.txt, acid-xblock, xblock-utils -zipp==1.2.0 # via -r xblock-sdk/requirements/test.txt, importlib-metadata +-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock + # via + # -r test_requirements.in + # -r xblock-sdk/requirements/test.txt +-e git+https://github.com/edx/django-pyfs.git@2.1#egg=django-pyfs==2.1 + # via + # -r test_requirements.in + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt +-e file:///home/giovanni-opencraft/devstacks/master/src/problem-builder/xblock-sdk + # via -r test_requirements.in +-e git+https://github.com/edx/xblock-utils.git@2.0.0#egg=xblock-utils + # via -r test_requirements.in +appdirs==1.4.3 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # fs + # virtualenv +arrow==0.15.6 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # jinja2-time +astroid==2.6.6 + # via pylint +attrs==19.3.0 + # via + # -r xblock-sdk/requirements/test.txt + # pytest +binaryornot==0.4.4 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # cookiecutter +bok_choy==0.7.1 + # via + # -r test_requirements.in + # -r xblock-sdk/requirements/test.txt +boto==2.49.0 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt +boto3==1.13.2 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # fs-s3fs +botocore==1.16.2 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # boto3 + # s3transfer +certifi==2020.4.5.1 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # requests +chardet==3.0.4 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # binaryornot + # requests +click==7.1.2 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # cookiecutter +cookiecutter==1.7.2 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt +coverage==5.1 + # via + # -r xblock-sdk/requirements/test.txt + # pytest-cov +ddt==1.3.1 + # via + # -r test_requirements.in + # -r xblock-sdk/requirements/test.txt +distlib==0.3.0 + # via + # -r xblock-sdk/requirements/test.txt + # virtualenv +django==2.2.12 + # via + # -r test_requirements.in + # -r xblock-sdk/requirements/base.txt + # django-mysql + # django-pyfs + # xblock-sdk +django-mysql==3.12.0 + # via -r test_requirements.in +django-nose==1.4.7 + # via -r test_requirements.in +docutils==0.15.2 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # botocore +filelock==3.0.12 + # via + # -r xblock-sdk/requirements/test.txt + # tox + # virtualenv +fs==2.4.11 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # django-pyfs + # fs-s3fs + # xblock +fs-s3fs==1.1.1 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # django-pyfs +idna==2.9 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # requests +importlib-metadata==1.6.0 + # via -r xblock-sdk/requirements/test.txt +importlib-resources==1.5.0 + # via -r xblock-sdk/requirements/test.txt +isort==5.9.3 + # via pylint +jinja2==2.11.2 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # cookiecutter + # jinja2-time +jinja2-time==0.2.0 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # cookiecutter +jmespath==0.9.5 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # boto3 + # botocore +lazy==1.4 + # via + # -r test_requirements.in + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # acid-xblock + # bok-choy +lazy-object-proxy==1.6.0 + # via astroid +lxml==4.5.0 + # via + # -r test_requirements.in + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # xblock +mako==1.1.2 + # via + # -r xblock-sdk/requirements/test.txt + # acid-xblock + # xblock-utils +markupsafe==1.1.1 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # cookiecutter + # jinja2 + # mako + # xblock +mccabe==0.6.1 + # via pylint +mock==3.0.5 + # via + # -r test_requirements.in + # -r xblock-sdk/requirements/test.txt +more-itertools==8.2.0 + # via + # -r xblock-sdk/requirements/test.txt + # pytest +mysqlclient==2.0.3 + # via -r test_requirements.in +needle==0.5.0 + # via + # -r xblock-sdk/requirements/test.txt + # bok-choy +nose==1.3.7 + # via + # -r xblock-sdk/requirements/test.txt + # django-nose + # needle +packaging==20.3 + # via + # -r xblock-sdk/requirements/test.txt + # pytest + # tox +pathlib2==2.3.5 + # via -r xblock-sdk/requirements/test.txt +pillow==7.1.2 + # via + # -r xblock-sdk/requirements/test.txt + # needle +pluggy==0.13.1 + # via + # -r xblock-sdk/requirements/test.txt + # pytest + # tox +poyo==0.5.0 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # cookiecutter +py==1.8.1 + # via + # -r xblock-sdk/requirements/test.txt + # pytest + # tox +pycodestyle==2.7.0 + # via -r test_requirements.in +pylint==2.9.6 + # via -r test_requirements.in +pyparsing==2.4.7 + # via + # -r xblock-sdk/requirements/test.txt + # packaging +pypng==0.0.20 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt +pytest==5.4.1 + # via + # -r test_requirements.in + # -r xblock-sdk/requirements/test.txt + # pytest-cov + # pytest-django + # pytest-rerunfailures +pytest-cov==2.8.1 + # via -r xblock-sdk/requirements/test.txt +pytest-django==3.9.0 + # via -r xblock-sdk/requirements/test.txt +pytest-rerunfailures==9.0 + # via -r xblock-sdk/requirements/test.txt +python-dateutil==2.8.1 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # arrow + # botocore + # xblock +python-slugify==4.0.0 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # cookiecutter +pytz==2020.1 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # django + # fs + # xblock +pyyaml==5.3.1 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # xblock +requests==2.23.0 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # cookiecutter +s3transfer==0.3.3 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # boto3 +selenium==3.4.1 + # via + # -r test_requirements.in + # -r xblock-sdk/requirements/test.txt + # bok-choy + # needle +simplejson==3.17.0 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # xblock-utils +six==1.14.0 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # bok-choy + # cookiecutter + # django-pyfs + # fs + # fs-s3fs + # mock + # packaging + # pathlib2 + # python-dateutil + # tox + # virtualenv + # xblock +sqlparse==0.3.1 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # django +text-unidecode==1.3 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # python-slugify +toml==0.10.0 + # via + # -r xblock-sdk/requirements/test.txt + # pylint + # tox +tox==3.15.0 + # via + # -r xblock-sdk/requirements/test.txt + # tox-battery +tox-battery==0.5.2 + # via -r xblock-sdk/requirements/test.txt +typing==3.7.4.1 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt +urllib3==1.25.9 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # botocore + # requests +virtualenv==20.0.20 + # via + # -r xblock-sdk/requirements/test.txt + # tox +wcwidth==0.1.9 + # via + # -r xblock-sdk/requirements/test.txt + # pytest +web-fragments==0.3.1 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # xblock + # xblock-utils +webob==1.8.6 + # via + # -r test_requirements.in + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # xblock +wrapt==1.12.1 + # via astroid +xblock==1.3.0 + # via + # -r test_requirements.in + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt + # acid-xblock + # xblock-utils +zipp==1.2.0 + # via + # -r xblock-sdk/requirements/test.txt + # importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools From 58f7ea2724fc13fc9160d2b40e73832351b2fbc2 Mon Sep 17 00:00:00 2001 From: Giovanni Cimolin da Silva Date: Thu, 19 Aug 2021 16:01:44 -0300 Subject: [PATCH 03/29] tset --- test_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_requirements.txt b/test_requirements.txt index 886fb168..dd671d09 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -13,7 +13,7 @@ # -r test_requirements.in # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt --e file:///home/giovanni-opencraft/devstacks/master/src/problem-builder/xblock-sdk +-e xblock-sdk # via -r test_requirements.in -e git+https://github.com/edx/xblock-utils.git@2.0.0#egg=xblock-utils # via -r test_requirements.in From eb1034f4f5c3f92b726d0c89fa300bc836899176 Mon Sep 17 00:00:00 2001 From: Kshitij Sobti Date: Fri, 20 Aug 2021 09:31:09 +0530 Subject: [PATCH 04/29] Fix quality issues --- problem_builder/answer.py | 6 +++--- problem_builder/choice.py | 4 ++-- problem_builder/completion.py | 15 ++++++++++----- problem_builder/dashboard.py | 12 ++++++------ problem_builder/mcq.py | 11 ++++++----- problem_builder/mentoring.py | 13 ++++++------- problem_builder/models.py | 2 +- problem_builder/mrq.py | 2 +- problem_builder/plot.py | 6 +++--- problem_builder/questionnaire.py | 6 +++--- problem_builder/slider.py | 4 ++-- problem_builder/step.py | 4 ++-- problem_builder/step_review.py | 2 +- problem_builder/table.py | 12 ++++++------ problem_builder/tasks.py | 4 ++-- problem_builder/tests/integration/base_test.py | 4 ++-- .../tests/integration/test_author_changes.py | 2 +- .../tests/integration/test_dashboard.py | 2 +- .../tests/integration/test_instructor_tool.py | 2 +- .../tests/integration/test_step_builder.py | 2 +- problem_builder/tests/integration/test_titles.py | 2 +- problem_builder/tests/unit/test_models.py | 2 +- problem_builder/tip.py | 4 ++-- problem_builder/v1/studio_xml_utils.py | 2 +- 24 files changed, 65 insertions(+), 60 deletions(-) diff --git a/problem_builder/answer.py b/problem_builder/answer.py index faaadcd4..0e72f2b2 100644 --- a/problem_builder/answer.py +++ b/problem_builder/answer.py @@ -60,7 +60,7 @@ class AnswerMixin(XBlockWithPreviewMixin, XBlockWithTranslationServiceMixin, Stu Mixin to give an XBlock the ability to read/write data to the Answers DB table. """ def build_user_state_data(self, context=None): - result = super(AnswerMixin, self).build_user_state_data() + result = super().build_user_state_data() result['student_input'] = self.student_input return result @@ -131,7 +131,7 @@ def validate_field_data(self, validation, data): """ Validate this block's field data. """ - super(AnswerMixin, self).validate_field_data(validation, data) + super().validate_field_data(validation, data) def add_error(msg): validation.add(ValidationMessage(ValidationMessage.ERROR, msg)) @@ -264,7 +264,7 @@ def save(self): """ Replicate data changes on the related Django model used for sharing of data accross XBlocks """ - super(AnswerBlock, self).save() + super().save() student_id = self._get_student_id() if not student_id: diff --git a/problem_builder/choice.py b/problem_builder/choice.py index 0ef8fcdf..1538e472 100644 --- a/problem_builder/choice.py +++ b/problem_builder/choice.py @@ -98,7 +98,7 @@ def validate_field_data(self, validation, data): """ Validate this block's field data. """ - super(ChoiceBlock, self).validate_field_data(validation, data) + super().validate_field_data(validation, data) def add_error(msg): validation.add(ValidationMessage(ValidationMessage.ERROR, msg)) @@ -112,7 +112,7 @@ def validate(self): """ Validates the state of this XBlock. """ - validation = super(ChoiceBlock, self).validate() + validation = super().validate() if self.get_parent().all_choice_values.count(self.value) > 1: validation.add( ValidationMessage(ValidationMessage.ERROR, self._( diff --git a/problem_builder/completion.py b/problem_builder/completion.py index 3931b6d3..74f5ffe9 100644 --- a/problem_builder/completion.py +++ b/problem_builder/completion.py @@ -72,11 +72,16 @@ class NullableBoolean(JSONField): # We're OK redefining built-in `help` def __init__(self, help=None, default=UNSET, scope=Scope.content, display_name=None, **kwargs): # pylint: disable=redefined-builtin - super(NullableBoolean, self).__init__(help, default, scope, display_name, - values=({'display_name': "True", "value": True}, - {'display_name': "False", "value": False}, - {'display_name': "Null", "value": None}), - **kwargs) + super().__init__( + help, + default, + scope, + display_name, + values=({'display_name': "True", "value": True}, + {'display_name': "False", "value": False}, + {'display_name': "Null", "value": None}), + **kwargs + ) def from_json(self, value): if value is None: diff --git a/problem_builder/dashboard.py b/problem_builder/dashboard.py index 20f8fb13..89f40aa1 100644 --- a/problem_builder/dashboard.py +++ b/problem_builder/dashboard.py @@ -111,7 +111,7 @@ def __init__(self, rule_str, color_str): # Once it's been parsed, also try evaluating it with a test value: self._safe_eval_expression(self._rule_parsed, x=0) except (TypeError, SyntaxError) as e: - raise ValueError("Invalid Expression: {}".format(e)) + raise ValueError("Invalid Expression: {}".format(e)) from e except ZeroDivisionError: pass # This may depend on the value of 'x' which we set to zero but don't know yet. self.color_str = color_str @@ -312,11 +312,11 @@ def get_mentoring_blocks(self, mentoring_ids, ignore_errors=True): try: mentoring_id = self.scope_ids.usage_id.course_key.make_usage_key('mentoring', url_name) yield self.runtime.get_block(mentoring_id) - except Exception: + except Exception as err: if ignore_errors: yield None else: - raise InvalidUrlName(url_name) + raise InvalidUrlName(url_name) from err def parse_color_rules_str(self, color_rules_str, ignore_errors=True): """ @@ -338,12 +338,12 @@ def parse_color_rules_str(self, color_rules_str, ignore_errors=True): condition = "1" # Always true value = line rules.append(ColorRule(condition, value)) - except ValueError: + except ValueError as err: if ignore_errors: continue raise ValueError( _("Invalid color rule on line {line_number}").format(line_number=lineno + 1) - ) + ) from err return rules @lazy @@ -487,7 +487,7 @@ def validate_field_data(self, validation, data): """ Validate this block's field data. """ - super(DashboardBlock, self).validate_field_data(validation, data) + super().validate_field_data(validation, data) def add_error(msg): validation.add(ValidationMessage(ValidationMessage.ERROR, msg)) diff --git a/problem_builder/mcq.py b/problem_builder/mcq.py index 4ba60dd2..ee52e526 100644 --- a/problem_builder/mcq.py +++ b/problem_builder/mcq.py @@ -146,7 +146,7 @@ def validate_field_data(self, validation, data): """ Validate this block's field data. """ - super(MCQBlock, self).validate_field_data(validation, data) + super().validate_field_data(validation, data) def add_error(msg): validation.add(ValidationMessage(ValidationMessage.ERROR, msg)) @@ -230,8 +230,9 @@ def all_choice_values(self): def human_readable_choices(self): display_names = ["1 - {}".format(self.low), "2", "3", "4", "5 - {}".format(self.high)] return [ - {"display_name": dn, "value": val} for val, dn in zip(self.FIXED_VALUES, display_names) - ] + super(RatingBlock, self).human_readable_choices + {"display_name": dn, "value": val} + for val, dn in zip(self.FIXED_VALUES, display_names) + ] + super().human_readable_choices def get_author_edit_view_fragment(self, context): """ @@ -255,12 +256,12 @@ def url_name(self): defer to super(). In the workbench or any other platform, we use the name. """ try: - return super(RatingBlock, self).url_name + return super().url_name except AttributeError: return self.name def student_view(self, context): - fragment = super(RatingBlock, self).student_view(context) + fragment = super().student_view(context) rendering_for_studio = None if context: # Workbench does not provide context rendering_for_studio = context.get('author_edit_view') diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 39552330..fe374f57 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -142,7 +142,7 @@ def url_name(self): defer to super(). In the workbench or any other platform, we use the usage_id. """ try: - return super(BaseMentoringBlock, self).url_name + return super().url_name except AttributeError: return six.text_type(self.scope_ids.usage_id) @@ -446,8 +446,7 @@ def student_view(self, context): # Validate self.step: num_steps = len(self.steps) - if self.step > num_steps: - self.step = num_steps + self.step = min(num_steps, self.step) fragment = Fragment() child_content = u"" @@ -709,7 +708,7 @@ def validate(self): """ Validates the state of this XBlock except for individual field values. """ - validation = super(MentoringBlock, self).validate() + validation = super().validate() a_child_has_issues = False message_types_present = set() for child_id in self.children: @@ -739,7 +738,7 @@ def author_edit_view(self, context): """ local_context = context.copy() local_context['author_edit_view'] = True - fragment = super(MentoringBlock, self).author_edit_view(local_context) + fragment = super().author_edit_view(local_context) fragment.add_content(loader.render_django_template('templates/html/mentoring_url_name.html', { 'url_name': self.url_name })) @@ -822,7 +821,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes editable_fields = ('display_name', 'max_attempts', 'extended_feedback', 'weight') def build_user_state_data(self, context=None): - user_state_data = super(MentoringWithExplicitStepsBlock, self).build_user_state_data() + user_state_data = super().build_user_state_data() user_state_data['active_step'] = self.active_step_safe user_state_data['score_summary'] = self.get_score_summary() return user_state_data @@ -1106,7 +1105,7 @@ def author_edit_view(self, context): """ Add some HTML to the author view that allows authors to add child blocks. """ - fragment = super(MentoringWithExplicitStepsBlock, self).author_edit_view(context) + fragment = super().author_edit_view(context) fragment.add_content(loader.render_django_template('templates/html/mentoring_url_name.html', { "url_name": self.url_name })) diff --git a/problem_builder/models.py b/problem_builder/models.py index 0a153a3a..6ecfe4a6 100644 --- a/problem_builder/models.py +++ b/problem_builder/models.py @@ -55,7 +55,7 @@ class Meta: def save(self, *args, **kwargs): # Force validation of max_length self.full_clean() - super(Answer, self).save(*args, **kwargs) + super().save(*args, **kwargs) class Share(models.Model): diff --git a/problem_builder/mrq.py b/problem_builder/mrq.py index e487b9d4..b66cb9f4 100644 --- a/problem_builder/mrq.py +++ b/problem_builder/mrq.py @@ -183,7 +183,7 @@ def validate_field_data(self, validation, data): """ Validate this block's field data. """ - super(MRQBlock, self).validate_field_data(validation, data) + super().validate_field_data(validation, data) def add_error(msg): validation.add(ValidationMessage(ValidationMessage.ERROR, msg)) diff --git a/problem_builder/plot.py b/problem_builder/plot.py index 0aa8ac53..a97efe2b 100644 --- a/problem_builder/plot.py +++ b/problem_builder/plot.py @@ -244,7 +244,7 @@ def average_claims_json(self): return json.dumps(self.average_claims) def build_user_state_data(self, context=None): - user_state_data = super(PlotBlock, self).build_user_state_data() + user_state_data = super().build_user_state_data() user_state_data['default_claims'] = self.default_claims user_state_data['average_claims'] = self.average_claims return user_state_data @@ -381,7 +381,7 @@ def author_edit_view(self, context): """ Add some HTML to the author view that allows authors to add child blocks. """ - fragment = super(PlotBlock, self).author_edit_view(context) + fragment = super().author_edit_view(context) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/container_edit.js')) @@ -455,7 +455,7 @@ def validate_field_data(self, validation, data): """ Validate this block's field data. """ - super(PlotOverlayBlock, self).validate_field_data(validation, data) + super().validate_field_data(validation, data) def add_error(msg): validation.add(ValidationMessage(ValidationMessage.ERROR, msg)) diff --git a/problem_builder/questionnaire.py b/problem_builder/questionnaire.py index 41fbc210..cdacb0e9 100644 --- a/problem_builder/questionnaire.py +++ b/problem_builder/questionnaire.py @@ -172,7 +172,7 @@ def get_submission_display(self, submission): return submission def get_author_edit_view_fragment(self, context): - fragment = super(QuestionnaireAbstractBlock, self).author_edit_view(context) + fragment = super().author_edit_view(context) return fragment def author_edit_view(self, context): @@ -208,7 +208,7 @@ def validate_field_data(self, validation, data): All of this XBlock's fields should be found in "data", even if they aren't being changed or aren't even set (i.e. are defaults). """ - super(QuestionnaireAbstractBlock, self).validate_field_data(validation, data) + super().validate_field_data(validation, data) def add_error(msg): validation.add(ValidationMessage(ValidationMessage.ERROR, msg)) @@ -221,7 +221,7 @@ def validate(self): """ Validates the state of this XBlock. """ - validation = super(QuestionnaireAbstractBlock, self).validate() + validation = super().validate() def add_error(msg): validation.add(ValidationMessage(ValidationMessage.ERROR, msg)) diff --git a/problem_builder/slider.py b/problem_builder/slider.py index 0e88db78..321124c0 100644 --- a/problem_builder/slider.py +++ b/problem_builder/slider.py @@ -98,7 +98,7 @@ def url_name(self): defer to super(). In the workbench or any other platform, we use the name. """ try: - return super(SliderBlock, self).url_name + return super().url_name except AttributeError: return self.name @@ -188,4 +188,4 @@ def validate_field_data(self, validation, data): """ Validate this block's field data. """ - super(SliderBlock, self).validate_field_data(validation, data) + super().validate_field_data(validation, data) diff --git a/problem_builder/step.py b/problem_builder/step.py index eb4bfcb9..a78542f5 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -121,7 +121,7 @@ def siblings(self): @property def is_last_step(self): parent = self.get_parent() - return self.step_number == len(parent.step_ids) + return self.step_number == len(parent.step_ids) # pylint: disable=comparison-with-callable @property def allowed_nested_blocks(self): @@ -229,7 +229,7 @@ def author_edit_view(self, context): """ local_context = dict(context) local_context['author_edit_view'] = True - fragment = super(MentoringStepBlock, self).author_edit_view(local_context) + fragment = super().author_edit_view(local_context) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-tinymce-content.css')) diff --git a/problem_builder/step_review.py b/problem_builder/step_review.py index 915ab252..416b7ed7 100644 --- a/problem_builder/step_review.py +++ b/problem_builder/step_review.py @@ -334,7 +334,7 @@ def author_edit_view(self, context): """ Add some HTML to the author view that allows authors to add child blocks. """ - fragment = super(ReviewStepBlock, self).author_edit_view(context) + fragment = super().author_edit_view(context) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js')) diff --git a/problem_builder/table.py b/problem_builder/table.py index 2669465c..f2fb87c5 100644 --- a/problem_builder/table.py +++ b/problem_builder/table.py @@ -115,8 +115,8 @@ def table_render(self, data, suffix=''): block_id=self.block_id, ) context['student_submissions_key'] = share.submission_uid - except Share.DoesNotExist: - raise JsonHandlerError(403, _("You are not permitted to view this student's table.")) + except Share.DoesNotExist as err: + raise JsonHandlerError(403, _("You are not permitted to view this student's table.")) from err for child_id in self.children: child = self.runtime.get_block(child_id) @@ -162,8 +162,8 @@ def clear_notification(self, data, suffix=''): raise JsonHandlerError(400, "No usernames sent.") try: isinstance(usernames, list) - except ValueError: - raise JsonHandlerError(400, "Usernames must be a list.") + except ValueError as err: + raise JsonHandlerError(400, "Usernames must be a list.") from err Share.objects.filter( shared_with__username=self.current_user_key, shared_by__username__in=usernames, @@ -294,7 +294,7 @@ def author_edit_view(self, context): """ Add some HTML to the author view that allows authors to add choices and tips. """ - fragment = super(MentoringTableBlock, self).author_edit_view(context) + fragment = super().author_edit_view(context) fragment.add_content(loader.render_django_template('templates/html/mentoring-table-add-button.html', {})) # Share styles with the questionnaire edit CSS: fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/questionnaire-edit.css')) @@ -343,7 +343,7 @@ def author_edit_view(self, context): """ Add some HTML to the author view that allows authors to add choices and tips. """ - fragment = super(MentoringTableColumn, self).author_edit_view(context) + fragment = super().author_edit_view(context) fragment.content = u"
{}
".format(self.header) + fragment.content fragment.add_content(loader.render_django_template('templates/html/mentoring-column-add-button.html', {})) # Share styles with the questionnaire edit CSS: diff --git a/problem_builder/tasks.py b/problem_builder/tasks.py index ca28ea78..dd984654 100644 --- a/problem_builder/tasks.py +++ b/problem_builder/tasks.py @@ -35,8 +35,8 @@ def export_data(course_id, source_block_id_str, block_types, user_ids, match_str try: course_key = CourseKey.from_string(course_id) usage_key = UsageKey.from_string(source_block_id_str) - except InvalidKeyError: - raise ValueError("Could not find the specified Block ID.") + except InvalidKeyError as err: + raise ValueError("Could not find the specified Block ID.") from err src_block = modulestore().get_item(usage_key) course_key_str = six.text_type(course_key) diff --git a/problem_builder/tests/integration/base_test.py b/problem_builder/tests/integration/base_test.py index ca504ffc..41d10930 100644 --- a/problem_builder/tests/integration/base_test.py +++ b/problem_builder/tests/integration/base_test.py @@ -91,7 +91,7 @@ def load_scenario(self, xml_file, params=None, load_immediately=True): def go_to_view(self, view_name): """ Eliminate errors that come from the Workbench banner overlapping elements """ - element = super(ProblemBuilderBaseTest, self).go_to_view(view_name) + element = super().go_to_view(view_name) self.browser.execute_script('document.querySelectorAll("header.banner")[0].style.display="none";') return element @@ -152,7 +152,7 @@ class MentoringBaseTest(SeleniumBaseTest, PopupCheckMixin): def go_to_page(self, page_title, **kwargs): """ Eliminate errors that come from the Workbench banner overlapping elements """ - element = super(MentoringBaseTest, self).go_to_page(page_title, **kwargs) + element = super().go_to_page(page_title, **kwargs) self.browser.execute_script('document.querySelectorAll("header.banner")[0].style.display="none";') return element diff --git a/problem_builder/tests/integration/test_author_changes.py b/problem_builder/tests/integration/test_author_changes.py index 6bcb2196..a458f0ee 100644 --- a/problem_builder/tests/integration/test_author_changes.py +++ b/problem_builder/tests/integration/test_author_changes.py @@ -12,7 +12,7 @@ class AuthorChangesTest(ProblemBuilderBaseTest): Test various scenarios involving author changes made to a block already in use by students """ def setUp(self): - super(AuthorChangesTest, self).setUp() + super().setUp() self.load_scenario("author_changes.xml", {}, load_immediately=False) self.refresh_page() diff --git a/problem_builder/tests/integration/test_dashboard.py b/problem_builder/tests/integration/test_dashboard.py index 3533d710..c4a43081 100644 --- a/problem_builder/tests/integration/test_dashboard.py +++ b/problem_builder/tests/integration/test_dashboard.py @@ -117,7 +117,7 @@ class TestDashboardBlock(ProblemBuilderBaseTest): cleanup_on_success = True def setUp(self): - super(TestDashboardBlock, self).setUp() + super().setUp() # Apply a whole bunch of patches that are needed in lieu of the LMS/CMS runtime and edx-submissions: diff --git a/problem_builder/tests/integration/test_instructor_tool.py b/problem_builder/tests/integration/test_instructor_tool.py index 78225208..90fcf699 100644 --- a/problem_builder/tests/integration/test_instructor_tool.py +++ b/problem_builder/tests/integration/test_instructor_tool.py @@ -48,7 +48,7 @@ def __init__(self): class InstructorToolTest(SeleniumXBlockTest): def setUp(self): - super(InstructorToolTest, self).setUp() + super().setUp() self.set_scenario_xml(""" diff --git a/problem_builder/tests/integration/test_step_builder.py b/problem_builder/tests/integration/test_step_builder.py index 6e2d7b27..f311513e 100644 --- a/problem_builder/tests/integration/test_step_builder.py +++ b/problem_builder/tests/integration/test_step_builder.py @@ -52,7 +52,7 @@ def set_slider_value(self, slider_number, val): class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixins): def setUp(self): - super(StepBuilderTest, self).setUp() + super().setUp() mock_submissions_api = ExtendedMockSubmissionsAPI() patches = ( diff --git a/problem_builder/tests/integration/test_titles.py b/problem_builder/tests/integration/test_titles.py index 3156fdd1..80ef14ef 100644 --- a/problem_builder/tests/integration/test_titles.py +++ b/problem_builder/tests/integration/test_titles.py @@ -118,7 +118,7 @@ class StepTitlesTest(SeleniumXBlockTest): """ def setUp(self): - super(StepTitlesTest, self).setUp() + super().setUp() # Disable asides for this test since the acid aside seems to cause Database errors # When we test multiple scenarios in one test method. patcher = patch( diff --git a/problem_builder/tests/unit/test_models.py b/problem_builder/tests/unit/test_models.py index 90bcb5d8..f44468c3 100644 --- a/problem_builder/tests/unit/test_models.py +++ b/problem_builder/tests/unit/test_models.py @@ -11,7 +11,7 @@ class AnswerDeleteSignalTest(TestCase): """ Unit tests for pre_delete signal receiver. """ def setUp(self): - super(AnswerDeleteSignalTest, self).setUp() + super().setUp() self.course_id = 'course-v1:edX+DemoX+Demo_Course' self.anonymous_student_id = '123456789876543210' Answer.objects.create( diff --git a/problem_builder/tip.py b/problem_builder/tip.py index 858d44af..79c7edac 100644 --- a/problem_builder/tip.py +++ b/problem_builder/tip.py @@ -112,13 +112,13 @@ def clean_studio_edits(self, data): Clean up the edits during studio_view save """ if "values" in data: - data["values"] = list([six.text_type(v) for v in set(data["values"])]) + data["values"] = list(six.text_type(v) for v in set(data["values"])) def validate_field_data(self, validation, data): """ Validate this block's field data. """ - super(TipBlock, self).validate_field_data(validation, data) + super().validate_field_data(validation, data) def add_error(msg): validation.add(ValidationMessage(ValidationMessage.ERROR, msg)) diff --git a/problem_builder/v1/studio_xml_utils.py b/problem_builder/v1/studio_xml_utils.py index fc43a044..d312e44f 100644 --- a/problem_builder/v1/studio_xml_utils.py +++ b/problem_builder/v1/studio_xml_utils.py @@ -39,7 +39,7 @@ class TransientRuntime(Runtime): def __init__(self): id_manager = MemoryIdManager() field_data = KvsFieldData(DictKeyValueStore()) - super(TransientRuntime, self).__init__( + super().__init__( id_reader=id_manager, id_generator=id_manager, field_data=field_data, From af72be9f979c91401c860aa040ebe5151eb31eba Mon Sep 17 00:00:00 2001 From: Kshitij Sobti Date: Fri, 20 Aug 2021 09:58:06 +0530 Subject: [PATCH 05/29] remove python2 support, upgrade syntax --- problem_builder/answer.py | 20 +- problem_builder/choice.py | 20 +- problem_builder/completion.py | 12 +- problem_builder/dashboard.py | 40 ++-- problem_builder/dashboard_visual.py | 3 +- problem_builder/instructor_tool.py | 33 ++-- .../commands/copy_deprecated_course_id.py | 4 +- problem_builder/mcq.py | 30 ++- problem_builder/mentoring.py | 35 ++-- problem_builder/message.py | 51 +++--- problem_builder/migrations/0001_initial.py | 11 +- .../migrations/0002_auto_20160121_1525.py | 5 +- .../migrations/0003_auto_20161124_0755.py | 5 +- .../migrations/0004_copy_course_ids.py | 3 - .../migrations/0005_auto_20170112_1021.py | 3 - .../0006_remove_deprecated_course_id.py | 4 +- .../0007_lengthen_student_id_field.py | 2 - problem_builder/mixins.py | 23 +-- problem_builder/models.py | 9 +- problem_builder/mrq.py | 26 ++- problem_builder/platform_dependencies.py | 1 - problem_builder/plot.py | 18 +- problem_builder/questionnaire.py | 16 +- problem_builder/settings.py | 2 +- problem_builder/slider.py | 12 +- .../south_migrations/0001_initial.py | 1 - .../0002_copy_from_mentoring.py | 2 - ...ue_share_shared_by_shared_with_block_id.py | 1 - problem_builder/step.py | 18 +- problem_builder/step_review.py | 22 +-- problem_builder/sub_api.py | 7 +- problem_builder/swipe.py | 18 +- problem_builder/table.py | 18 +- problem_builder/tasks.py | 17 +- .../tests/integration/base_test.py | 20 +- .../tests/integration/test_answer.py | 1 - .../tests/integration/test_author_changes.py | 4 +- .../tests/integration/test_clarifications.py | 3 +- .../tests/integration/test_completion.py | 1 - .../tests/integration/test_dashboard.py | 9 +- .../tests/integration/test_instructor_tool.py | 6 +- .../tests/integration/test_mentoring.py | 14 +- .../tests/integration/test_messages.py | 7 +- .../tests/integration/test_multiple.py | 1 - .../tests/integration/test_progression.py | 1 - .../tests/integration/test_questionnaire.py | 11 +- .../tests/integration/test_slider.py | 1 - .../tests/integration/test_step_builder.py | 69 ++++--- .../tests/integration/test_table.py | 3 +- .../tests/integration/test_titles.py | 8 +- .../tests/unit/test_instructor_tool.py | 6 +- problem_builder/tests/unit/test_migration.py | 22 +-- problem_builder/tests/unit/test_mixins.py | 2 +- problem_builder/tests/unit/test_models.py | 3 +- .../tests/unit/test_problem_builder.py | 2 +- problem_builder/tests/unit/test_step.py | 10 +- .../tests/unit/test_step_builder.py | 28 +-- problem_builder/tests/unit/test_swipe.py | 4 +- problem_builder/tests/unit/utils.py | 2 +- problem_builder/tip.py | 13 +- problem_builder/utils.py | 1 - problem_builder/v1/studio_xml_utils.py | 5 +- problem_builder/v1/tests/test_upgrade.py | 11 +- problem_builder/v1/upgrade.py | 57 +++--- problem_builder/v1/xml_changes.py | 24 ++- run_tests.py | 5 +- setup.py | 5 +- test_requirements.in | 6 +- test_requirements.txt | 171 ++++++++---------- xblock-sdk | 2 +- 70 files changed, 465 insertions(+), 565 deletions(-) diff --git a/problem_builder/answer.py b/problem_builder/answer.py index 0e72f2b2..51b96ff7 100644 --- a/problem_builder/answer.py +++ b/problem_builder/answer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -23,9 +22,8 @@ import uuid import pkg_resources -import six -from lazy import lazy from django import utils +from lazy import lazy from xblock.core import XBlock from xblock.fields import Integer, Scope, String from xblock.fragment import Fragment @@ -137,7 +135,7 @@ def add_error(msg): validation.add(ValidationMessage(ValidationMessage.ERROR, msg)) if not data.name: - add_error(u"A Question ID is required.") + add_error("A Question ID is required.") @XBlock.needs("i18n") @@ -150,7 +148,7 @@ class AnswerBlock(SubmittingXBlockMixin, AnswerMixin, QuestionMixin, StudioEdita to make them searchable and referenceable across xblocks. """ CATEGORY = 'pb-answer' - STUDIO_LABEL = _(u"Long Answer") + STUDIO_LABEL = _("Long Answer") answerable = True name = String( @@ -192,7 +190,7 @@ def get_translation_content(self): return self.resource_string('public/js/translations/{lang}/textjs.js'.format( lang=utils.translation.to_locale(utils.translation.get_language()), )) - except IOError: + except OSError: return self.resource_string('public/js/translations/en/textjs.js') def mentoring_view(self, context=None): @@ -294,7 +292,7 @@ def student_view_data(self, context=None): """ return { 'id': self.name, - 'block_id': six.text_type(self.scope_ids.usage_id), + 'block_id': str(self.scope_ids.usage_id), 'display_name': self.display_name, 'type': self.CATEGORY, 'weight': self.weight, @@ -310,7 +308,7 @@ class AnswerRecapBlock(AnswerMixin, StudioEditableXBlockMixin, XBlock): """ CATEGORY = 'pb-answer-recap' - STUDIO_LABEL = _(u"Long Answer Recap") + STUDIO_LABEL = _("Long Answer Recap") name = String( display_name=_("Question ID"), @@ -343,9 +341,9 @@ def mentoring_view(self, context=None): location = self.location.replace(branch=None, version=None) # Standardize the key in case it isn't already target_key = { 'student_id': student_submissions_key, - 'course_id': six.text_type(location.course_key), + 'course_id': str(location.course_key), 'item_id': self.name, - 'item_type': u'pb-answer', + 'item_type': 'pb-answer', } submissions = sub_api.get_submissions(target_key, limit=1) try: @@ -381,6 +379,6 @@ def student_view_data(self, context=None): 'name': self.name, # For backwards compatibility; same as 'id' 'display_name': self.display_name, 'description': self.description, - 'block_id': six.text_type(self.scope_ids.usage_id), + 'block_id': str(self.scope_ids.usage_id), 'type': self.CATEGORY } diff --git a/problem_builder/choice.py b/problem_builder/choice.py index 1538e472..b296e1e0 100644 --- a/problem_builder/choice.py +++ b/problem_builder/choice.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -22,7 +21,6 @@ import uuid -import six from lxml import etree from xblock.core import XBlock from xblock.fields import Scope, String @@ -70,8 +68,8 @@ def display_name_with_default(self): try: status = self.get_parent().describe_choice_correctness(self.value) except Exception: - status = self._(u"Out of Context") # Parent block should implement describe_choice_correctness() - return self._(u"Choice ({status})").format(status=status) + status = self._("Out of Context") # Parent block should implement describe_choice_correctness() + return self._("Choice ({status})").format(status=status) def student_view_data(self, context=None): """ @@ -80,15 +78,15 @@ def student_view_data(self, context=None): """ # display_name_with_default gives out correctness - not adding it here return { - 'block_id': six.text_type(self.scope_ids.usage_id), - 'display_name': self.expand_static_url(self._(u"Choice ({content})").format(content=self.content)), + 'block_id': str(self.scope_ids.usage_id), + 'display_name': self.expand_static_url(self._("Choice ({content})").format(content=self.content)), 'value': self.value, 'content': self.expand_static_url(self.content), } def mentoring_view(self, context=None): """ Render this choice string within a mentoring block question. """ - return Fragment(u'{}'.format(self.content)) + return Fragment(f'{self.content}') def student_view(self, context=None): """ Normal view of this XBlock, identical to mentoring_view """ @@ -104,9 +102,9 @@ def add_error(msg): validation.add(ValidationMessage(ValidationMessage.ERROR, msg)) if not data.value.strip(): - add_error(self._(u"No value set. This choice will not work correctly.")) + add_error(self._("No value set. This choice will not work correctly.")) if not data.content.strip(): - add_error(self._(u"No choice text set yet.")) + add_error(self._("No choice text set yet.")) def validate(self): """ @@ -116,7 +114,7 @@ def validate(self): if self.get_parent().all_choice_values.count(self.value) > 1: validation.add( ValidationMessage(ValidationMessage.ERROR, self._( - u"This choice has a non-unique ID and won't work properly. " + "This choice has a non-unique ID and won't work properly. " "This can happen if you duplicate a choice rather than use the Add Choice button." )) ) @@ -145,7 +143,7 @@ def parse_xml(cls, node, runtime, keys, id_generator): setattr(block, field_name, node.attrib[field_name]) # HTML content: - block.content = six.text_type(node.text or u"") + block.content = str(node.text or "") for child in node: block.content += etree.tostring(child, encoding='unicode') diff --git a/problem_builder/completion.py b/problem_builder/completion.py index 74f5ffe9..43f9f5ce 100644 --- a/problem_builder/completion.py +++ b/problem_builder/completion.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -22,7 +21,6 @@ import logging -import six from xblock.core import XBlock from xblock.fields import UNSET, JSONField, Scope, String from xblock.fragment import Fragment @@ -88,7 +86,7 @@ def from_json(self, value): return None if isinstance(value, str): value = value.decode('ascii', errors='replace') - if isinstance(value, six.text_type): + if isinstance(value, str): return value.lower() == 'true' else: return bool(value) @@ -106,7 +104,7 @@ class CompletionBlock( The student's answer is always considered "correct". """ CATEGORY = 'pb-completion' - STUDIO_LABEL = _(u'Completion') + STUDIO_LABEL = _('Completion') USER_STATE_FIELDS = ['student_value'] answerable = True @@ -166,7 +164,7 @@ def student_view_data(self, context=None): """ return { 'id': self.name, - 'block_id': six.text_type(self.scope_ids.usage_id), + 'block_id': str(self.scope_ids.usage_id), 'display_name': self.display_name_with_default, 'type': self.CATEGORY, 'question': self.question, @@ -195,11 +193,11 @@ def submit(self, value): """ Persist answer submitted by student. """ - log.debug(u'Received Completion submission: "%s"', value) + log.debug('Received Completion submission: "%s"', value) self.student_value = value if sub_api: # Also send to the submissions API: sub_api.create_submission(self.student_item_key, value) result = self.get_last_result() - log.debug(u'Completion submission result: %s', result) + log.debug('Completion submission result: %s', result) return result diff --git a/problem_builder/dashboard.py b/problem_builder/dashboard.py index 89f40aa1..3b1bbae6 100644 --- a/problem_builder/dashboard.py +++ b/problem_builder/dashboard.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -31,7 +30,6 @@ import logging import operator as op -import six from django.template.defaultfilters import floatformat from lazy import lazy from xblock.core import XBlock @@ -111,7 +109,7 @@ def __init__(self, rule_str, color_str): # Once it's been parsed, also try evaluating it with a test value: self._safe_eval_expression(self._rule_parsed, x=0) except (TypeError, SyntaxError) as e: - raise ValueError("Invalid Expression: {}".format(e)) from e + raise ValueError(f"Invalid Expression: {e}") from e except ZeroDivisionError: pass # This may depend on the value of 'x' which we set to zero but don't know yet. self.color_str = color_str @@ -360,14 +358,14 @@ def _get_submission_key(self, usage_key): """ return dict( student_id=self.runtime.anonymous_student_id, - course_id=six.text_type(usage_key.course_key), - item_id=six.text_type(usage_key), + course_id=str(usage_key.course_key), + item_id=str(usage_key), item_type=usage_key.block_type, ) def color_for_value(self, value): """ Given a string value, get the color rule that matches, if any """ - if isinstance(value, six.string_types): + if isinstance(value, str): if value.isnumeric(): value = float(value) else: @@ -389,7 +387,7 @@ def student_view(self, context=None): # pylint: disable=unused-argument Standard view of this XBlock. """ if not self.mentoring_ids: - return Fragment(u"

{}

{}

".format(self.display_name, _("Not configured."))) + return Fragment("

{}

{}

".format(self.display_name, _("Not configured."))) blocks = [] for mentoring_block in self.get_mentoring_blocks(self.mentoring_ids): @@ -495,36 +493,36 @@ def add_error(msg): try: list(self.get_mentoring_blocks(data.mentoring_ids, ignore_errors=False)) except InvalidUrlName as e: - add_error(_(u'Invalid block url_name given: "{bad_url_name}"').format(bad_url_name=six.text_type(e))) + add_error(_('Invalid block url_name given: "{bad_url_name}"').format(bad_url_name=str(e))) if data.exclude_questions: - for key, value in six.iteritems(data.exclude_questions): + for key, value in data.exclude_questions.items(): if not isinstance(value, list): add_error( - _(u"'Questions to be hidden' is malformed: value for key {key} is {value}, " - u"expected list of integers") + _("'Questions to be hidden' is malformed: value for key {key} is {value}, " + "expected list of integers") .format(key=key, value=value) ) if key not in data.mentoring_ids: add_error( - _(u"'Questions to be hidden' is malformed: mentoring url_name {url_name} " - u"is not added to Dashboard") + _("'Questions to be hidden' is malformed: mentoring url_name {url_name} " + "is not added to Dashboard") .format(url_name=key) ) if data.average_labels: - for key, value in six.iteritems(data.average_labels): - if not isinstance(value, six.string_types): + for key, value in data.average_labels.items(): + if not isinstance(value, str): add_error( - _(u"'Label for average value' is malformed: value for key {key} is {value}, expected string") + _("'Label for average value' is malformed: value for key {key} is {value}, expected string") .format(key=key, value=value) ) if key not in data.mentoring_ids: add_error( - _(u"'Label for average value' is malformed: mentoring url_name {url_name} " - u"is not added to Dashboard") + _("'Label for average value' is malformed: mentoring url_name {url_name} " + "is not added to Dashboard") .format(url_name=key) ) @@ -532,13 +530,13 @@ def add_error(msg): try: self.parse_color_rules_str(data.color_rules, ignore_errors=False) except ValueError as e: - add_error(six.text_type(e)) + add_error(str(e)) if data.visual_rules: try: rules = json.loads(data.visual_rules) except ValueError as e: - add_error(_(u"Visual rules contains an error: {error}").format(error=e)) + add_error(_("Visual rules contains an error: {error}").format(error=e)) else: if not isinstance(rules, dict): - add_error(_(u"Visual rules should be a JSON dictionary/object: {...}")) + add_error(_("Visual rules should be a JSON dictionary/object: {...}")) diff --git a/problem_builder/dashboard_visual.py b/problem_builder/dashboard_visual.py index dc3887dc..b814d636 100644 --- a/problem_builder/dashboard_visual.py +++ b/problem_builder/dashboard_visual.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -81,7 +80,7 @@ def __init__(self, blocks, rules, color_for_value, title, desc): continue # We only use blocks with numeric averages for the visual representation # Now we build the 'layer_data' information to pass on to the template: try: - layer_data = {"url": images[idx], "id": "layer{}".format(idx)} + layer_data = {"url": images[idx], "id": f"layer{idx}"} except IndexError: break diff --git a/problem_builder/instructor_tool.py b/problem_builder/instructor_tool.py index 7fb87731..e53c3ccd 100644 --- a/problem_builder/instructor_tool.py +++ b/problem_builder/instructor_tool.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -24,13 +23,13 @@ """ import json -import six from django.core.paginator import Paginator from xblock.core import XBlock from xblock.exceptions import JsonHandlerError from xblock.fields import Dict, List, Scope, String from xblock.fragment import Fragment from xblockutils.resources import ResourceLoader + from .mixins import TranslationContentMixin, XBlockWithTranslationServiceMixin from .utils import I18NService @@ -90,18 +89,20 @@ def author_view(self, context=None): """ Studio View """ # Warn the user that this block will only work from the LMS. (Since the CMS uses # different celery queues; our task listener is waiting for tasks on the LMS queue) - return Fragment(u'

Instructor Tool Block

This block only works from the LMS.

') + return Fragment('

Instructor Tool Block

This block only works from the LMS.

') def studio_view(self, context=None): """ View for editing Instructor Tool block in Studio. """ # Display friendly message explaining that the block is not editable. - return Fragment(u'

This is a preconfigured block. It is not editable.

') + return Fragment('

This is a preconfigured block. It is not editable.

') def check_pending_export(self): """ If we're waiting for an export, see if it has finished, and if so, get the result. """ - from .tasks import export_data as export_data_task # Import here since this is edX LMS specific + from .tasks import \ + export_data as \ + export_data_task # Import here since this is edX LMS specific if self.active_export_task_id: async_result = export_data_task.AsyncResult(self.active_export_task_id) if async_result.ready(): @@ -116,10 +117,10 @@ def _save_result(self, task_result): del task_result.result['display_data'] self.last_export_result = task_result.result else: - self.last_export_result = {'error': u'Unexpected result: {}'.format(repr(task_result.result))} + self.last_export_result = {'error': f'Unexpected result: {repr(task_result.result)}'} self.display_data = None else: - self.last_export_result = {'error': six.text_type(task_result.result)} + self.last_export_result = {'error': str(task_result.result)} self.display_data = None @XBlock.json_handler @@ -136,7 +137,7 @@ def get_result_page(self, data, suffix=''): def student_view(self, context=None): """ Normal View """ if not self.user_is_staff(): - return Fragment(u'

This interface can only be used by course staff.

') + return Fragment('

This interface can only be used by course staff.

') block_choices = { self._('Multiple Choice Question'): 'MCQBlock', self._('Multiple Response Question'): 'MRQBlock', @@ -147,7 +148,7 @@ def student_view(self, context=None): html = loader.render_django_template('templates/html/instructor_tool.html', { 'block_choices': block_choices, 'course_blocks_api': COURSE_BLOCKS_API, - 'root_block_id': six.text_type(getattr(self.runtime, 'course_id', 'course_id')), + 'root_block_id': str(getattr(self.runtime, 'course_id', 'course_id')), }, i18n_service=self.i18n_service) fragment = Fragment(html) fragment.add_javascript(self.get_translation_content()) @@ -225,7 +226,7 @@ def start_export(self, data, suffix=''): user_ids = [] for username in usernames.split(','): username = username.strip() - user_id = user_service.get_anonymous_user_id(username, six.text_type(self.runtime.course_id)) + user_id = user_service.get_anonymous_user_id(username, str(self.runtime.course_id)) if user_id: user_ids.append(user_id) if not user_ids: @@ -234,16 +235,18 @@ def start_export(self, data, suffix=''): if not root_block_id: root_block_id = self.scope_ids.usage_id # Block ID not in workbench runtime. - root_block_id = six.text_type(getattr(root_block_id, 'block_id', root_block_id)) + root_block_id = str(getattr(root_block_id, 'block_id', root_block_id)) # Launch task - from .tasks import export_data as export_data_task # Import here since this is edX LMS specific + from .tasks import \ + export_data as \ + export_data_task # Import here since this is edX LMS specific self._delete_export() # Make sure we nail down our state before sending off an asynchronous task. self.save() async_result = export_data_task.delay( # course_id not available in workbench. - six.text_type(getattr(self.runtime, 'course_id', 'course_id')), + str(getattr(self.runtime, 'course_id', 'course_id')), root_block_id, block_types, user_ids, @@ -264,7 +267,9 @@ def start_export(self, data, suffix=''): @XBlock.json_handler def cancel_export(self, request, suffix=''): - from .tasks import export_data as export_data_task # Import here since this is edX LMS specific + from .tasks import \ + export_data as \ + export_data_task # Import here since this is edX LMS specific if self.active_export_task_id: async_result = export_data_task.AsyncResult(self.active_export_task_id) async_result.revoke() diff --git a/problem_builder/management/commands/copy_deprecated_course_id.py b/problem_builder/management/commands/copy_deprecated_course_id.py index 0543eec7..56beda39 100644 --- a/problem_builder/management/commands/copy_deprecated_course_id.py +++ b/problem_builder/management/commands/copy_deprecated_course_id.py @@ -32,7 +32,7 @@ def handle(self, *args, **options): queryset = Answer.objects.filter(course_key__isnull=True) self.stdout.write( - "Copying Answer.course_id field into Answer.course_key in batches of {}".format(batch_size) + f"Copying Answer.course_id field into Answer.course_key in batches of {batch_size}" ) idx = 0 @@ -44,7 +44,7 @@ def handle(self, *args, **options): for answer in batch: answer.course_key = answer.course_id answer.save() - self.stdout.write("Processed batch {}".format(idx)) + self.stdout.write(f"Processed batch {idx}") time.sleep(sleep_time) self.stdout.write("Successfully copied Answer.course_id into Answer.course_key") diff --git a/problem_builder/mcq.py b/problem_builder/mcq.py index ee52e526..ed83cccb 100644 --- a/problem_builder/mcq.py +++ b/problem_builder/mcq.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -22,7 +21,6 @@ import logging -import six from xblock.fields import List, Scope, String from xblock.fragment import Fragment from xblock.validation import ValidationMessage @@ -52,7 +50,7 @@ class MCQBlock(SubmittingXBlockMixin, StudentViewUserStateMixin, QuestionnaireAb An XBlock used to ask multiple-choice questions """ CATEGORY = 'pb-mcq' - STUDIO_LABEL = _(u"Multiple Choice Question") + STUDIO_LABEL = _("Multiple Choice Question") USER_STATE_FIELDS = ['num_attempts', 'student_choice'] message = String( @@ -84,12 +82,12 @@ def describe_choice_correctness(self, choice_value): if choice_value in self.correct_choices: if len(self.correct_choices) == 1: # Translators: This is an adjective, describing a choice as correct - return self._(u"Correct") - return self._(u"Acceptable") + return self._("Correct") + return self._("Acceptable") else: if len(self.correct_choices) == 1: - return self._(u"Wrong") - return self._(u"Not Acceptable") + return self._("Wrong") + return self._("Not Acceptable") def calculate_results(self, submission): correct = submission in self.correct_choices @@ -127,10 +125,10 @@ def get_last_result(self): return self.get_results({'submission': self.student_choice}) if self.student_choice else {} def submit(self, submission): - log.debug(u'Received MCQ submission: "%s"', submission) + log.debug('Received MCQ submission: "%s"', submission) result = self.calculate_results(submission['value']) self.student_choice = submission['value'] - log.debug(u'MCQ submission result: %s', result) + log.debug('MCQ submission result: %s', result) return result def get_author_edit_view_fragment(self, context): @@ -138,7 +136,7 @@ def get_author_edit_view_fragment(self, context): The options for the 1-5 values of the Likert scale aren't child blocks but we want to show them in the author edit view, for clarity. """ - fragment = Fragment(u"

{}

".format(self.question)) + fragment = Fragment(f"

{self.question}

") self.render_children(context, fragment, can_reorder=True, can_add=False) return fragment @@ -162,13 +160,13 @@ def choice_name(choice_value): if all_values and not correct: add_error( - self._(u"You must indicate the correct answer[s], or the student will always get this question wrong.") + self._("You must indicate the correct answer[s], or the student will always get this question wrong.") ) if len(correct) < len(data.correct_choices): - add_error(self._(u"Duplicate correct choices set")) + add_error(self._("Duplicate correct choices set")) for val in (correct - all_values): add_error( - self._(u"A choice value listed as correct does not exist: {choice}").format(choice=choice_name(val)) + self._("A choice value listed as correct does not exist: {choice}").format(choice=choice_name(val)) ) def student_view_data(self, context=None): @@ -178,7 +176,7 @@ def student_view_data(self, context=None): """ return { 'id': self.name, - 'block_id': six.text_type(self.scope_ids.usage_id), + 'block_id': str(self.scope_ids.usage_id), 'display_name': self.display_name_with_default, 'type': self.CATEGORY, 'question': self.expand_static_url(self.question), @@ -197,7 +195,7 @@ class RatingBlock(MCQBlock): An XBlock used to rate something on a five-point scale, e.g. Likert Scale """ CATEGORY = 'pb-rating' - STUDIO_LABEL = _(u"Rating Question") + STUDIO_LABEL = _("Rating Question") low = String( display_name=_("Low"), @@ -228,7 +226,7 @@ def all_choice_values(self): @property def human_readable_choices(self): - display_names = ["1 - {}".format(self.low), "2", "3", "4", "5 - {}".format(self.high)] + display_names = [f"1 - {self.low}", "2", "3", "4", f"5 - {self.high}"] return [ {"display_name": dn, "value": val} for val, dn in zip(self.FIXED_VALUES, display_names) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index fe374f57..012c3703 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -26,7 +25,6 @@ from decimal import ROUND_HALF_UP, Decimal from itertools import chain -import six from lazy.lazy import lazy from xblock.core import XBlock from xblock.exceptions import JsonHandlerError, NoSuchViewError @@ -52,7 +50,8 @@ from .message import MentoringMessageBlock, get_message_label from .mixins import (ExpandStaticURLMixin, MessageParentMixin, QuestionMixin, StepParentMixin, StudentViewUserStateMixin, - StudentViewUserStateResultsTransformerMixin, TranslationContentMixin, + StudentViewUserStateResultsTransformerMixin, + TranslationContentMixin, XBlockWithTranslationServiceMixin, _normalize_id) from .step_review import ReviewStepBlock from .utils import I18NService @@ -144,7 +143,7 @@ def url_name(self): try: return super().url_name except AttributeError: - return six.text_type(self.scope_ids.usage_id) + return str(self.scope_ids.usage_id) @property def review_tips_json(self): @@ -337,14 +336,14 @@ def allowed_nested_blocks(self): try: from xmodule.video_module.video_module import VideoBlock additional_blocks.append(NestedXBlockSpec( - VideoBlock, category='video', label=_(u"Video") + VideoBlock, category='video', label=_("Video") )) except ImportError: pass try: from imagemodal import ImageModal additional_blocks.append(NestedXBlockSpec( - ImageModal, category='imagemodal', label=_(u"Image Modal") + ImageModal, category='imagemodal', label=_("Image Modal") )) except ImportError: pass @@ -358,7 +357,7 @@ def allowed_nested_blocks(self): try: from ooyala_player.ooyala_player import OoyalaPlayerBlock additional_blocks.append(NestedXBlockSpec( - OoyalaPlayerBlock, category='ooyala-player', label=_(u"Ooyala Player") + OoyalaPlayerBlock, category='ooyala-player', label=_("Ooyala Player") )) except ImportError: pass @@ -439,7 +438,8 @@ def score(self): @XBlock.supports("multi_device") # Mark as mobile-friendly def student_view(self, context): - from .questionnaire import QuestionnaireAbstractBlock # Import here to avoid circular dependency + from .questionnaire import \ + QuestionnaireAbstractBlock # Import here to avoid circular dependency # Migrate stored data if necessary self.migrate_fields() @@ -449,14 +449,14 @@ def student_view(self, context): self.step = min(num_steps, self.step) fragment = Fragment() - child_content = u"" + child_content = "" mcq_hide_previous_answer = self.get_option('pb_mcq_hide_previous_answer') for child_id in self.children: child = self.runtime.get_block(child_id) if child is None: # child should not be None but it can happen due to bugs or permission issues - child_content += u"

[{}]

".format(self._(u"Error: Unable to load child component.")) + child_content += "

[{}]

".format(self._("Error: Unable to load child component.")) elif not isinstance(child, MentoringMessageBlock): try: if mcq_hide_previous_answer and isinstance(child, QuestionnaireAbstractBlock): @@ -531,7 +531,7 @@ def next_step_url(self): """ Returns the URL of the next step's page """ - return '/jump_to_id/{}'.format(self.next_step) + return f'/jump_to_id/{self.next_step}' @property def hide_feedback(self): @@ -722,13 +722,13 @@ def validate(self): if msg_type in message_types_present: validation.add(ValidationMessage( ValidationMessage.ERROR, - self._(u"There should only be one '{msg_type}' message component.").format(msg_type=msg_type) + self._("There should only be one '{msg_type}' message component.").format(msg_type=msg_type) )) message_types_present.add(msg_type) if a_child_has_issues: validation.add(ValidationMessage( ValidationMessage.ERROR, - self._(u"A component inside this mentoring block has issues.") + self._("A component inside this mentoring block has issues.") )) return validation @@ -770,7 +770,7 @@ def student_view_data(self, context=None): components.append(block.student_view_data()) return { - 'block_id': six.text_type(self.scope_ids.usage_id), + 'block_id': str(self.scope_ids.usage_id), 'display_name': self.display_name, 'max_attempts': self.max_attempts, 'extended_feedback': self.extended_feedback, @@ -866,7 +866,8 @@ def step_ids(self): """ Get the usage_ids of all of this XBlock's children that are steps. """ - from .step import MentoringStepBlock # Import here to avoid circular dependency + from .step import \ + MentoringStepBlock # Import here to avoid circular dependency return [ _normalize_id(child_id) for child_id in self.children if child_isinstance(self, child_id, MentoringStepBlock) @@ -978,7 +979,7 @@ def student_view(self, context): for child_id in self.children: child = self.runtime.get_block(child_id) if child is None: # child should not be None but it can happen due to bugs or permission issues - child_content = u"

[{}]

".format(self._(u"Error: Unable to load child component.")) + child_content = "

[{}]

".format(self._("Error: Unable to load child component.")) else: child_fragment = self._render_child_fragment(child, context, view='mentoring_view') fragment.add_frag_resources(child_fragment) @@ -1127,7 +1128,7 @@ def student_view_data(self, context=None): return { 'title': self.display_name, - 'block_id': six.text_type(self.scope_ids.usage_id), + 'block_id': str(self.scope_ids.usage_id), 'display_name': self.display_name, 'show_title': self.show_title, 'weight': self.weight, diff --git a/problem_builder/message.py b/problem_builder/message.py index 10896ddb..6578422d 100644 --- a/problem_builder/message.py +++ b/problem_builder/message.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -18,7 +17,7 @@ # "AGPLv3". If not, see . # -import six + # Imports ########################################################### from lxml import etree from xblock.core import XBlock @@ -44,42 +43,42 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin, XBlockWithTransla """ MESSAGE_TYPES = { "completed": { - "display_name": _(u"Completed"), - "studio_label": _(u'Message (Complete)'), - "long_display_name": _(u"Message shown when complete"), - "default": _(u"Great job!"), + "display_name": _("Completed"), + "studio_label": _('Message (Complete)'), + "long_display_name": _("Message shown when complete"), + "default": _("Great job!"), "description": _( - u"This message will be shown when the student achieves a perfect score. " + "This message will be shown when the student achieves a perfect score. " ), }, "incomplete": { - "display_name": _(u"Incomplete"), - "studio_label": _(u'Message (Incomplete)'), - "long_display_name": _(u"Message shown when incomplete"), - "default": _(u"Not quite! You can try again, though."), + "display_name": _("Incomplete"), + "studio_label": _('Message (Incomplete)'), + "long_display_name": _("Message shown when incomplete"), + "default": _("Not quite! You can try again, though."), "description": _( - u"This message will be shown when the student gets at least one question wrong, " + "This message will be shown when the student gets at least one question wrong, " "but is allowed to try again. " ), }, "max_attempts_reached": { - "display_name": _(u"Reached max. # of attempts"), - "studio_label": _(u'Message (Max # Attempts)'), - "long_display_name": _(u"Message shown when student reaches max. # of attempts"), - "default": _(u"Sorry, you have used up all of your allowed submissions."), + "display_name": _("Reached max. # of attempts"), + "studio_label": _('Message (Max # Attempts)'), + "long_display_name": _("Message shown when student reaches max. # of attempts"), + "default": _("Sorry, you have used up all of your allowed submissions."), "description": _( - u"This message will be shown when the student has used up " + "This message will be shown when the student has used up " "all of their allowed attempts without achieving a perfect score. " ), }, "on-assessment-review-question": { - "display_name": _(u"Study tips if this question was wrong"), - "long_display_name": _(u"Study tips shown if question was answered incorrectly"), + "display_name": _("Study tips if this question was wrong"), + "long_display_name": _("Study tips shown if question was answered incorrectly"), "default": _( - u"Review ____." + "Review ____." ), "description": _( - u"This message will be shown when the student is reviewing " + "This message will be shown when the student is reviewing " "their answers to the assessment, if the student got this specific question " "wrong and is allowed to try again." ), @@ -114,7 +113,7 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin, XBlockWithTransla def mentoring_view(self, context=None): """ Render this message for use by a mentoring block. """ - html = u'
{content}
'.format( + html = '
{content}
'.format( msg_type=self.type, content=self.content ) @@ -126,7 +125,7 @@ def student_view(self, context=None): def author_view(self, context=None): fragment = self.mentoring_view(context) - fragment.content += u'

{}

'.format(self.help_text) + fragment.content += f'

{self.help_text}

' return fragment @property @@ -134,14 +133,14 @@ def display_name_with_default(self): try: return self._(self.MESSAGE_TYPES[self.type]["long_display_name"]) except KeyError: - return u"INVALID MESSAGE" + return "INVALID MESSAGE" @property def help_text(self): try: return self._(self.MESSAGE_TYPES[self.type]["description"]) except KeyError: - return u"This message is not a valid message type!" + return "This message is not a valid message type!" @classmethod def get_template(cls, template_id): @@ -159,7 +158,7 @@ def parse_xml(cls, node, runtime, keys, id_generator): Construct this XBlock from the given XML node. """ block = runtime.construct_xblock_from_class(cls, keys) - block.content = six.text_type(node.text or u"") + block.content = str(node.text or "") if 'type' in node.attrib: # 'type' is optional - default is 'completed' block.type = node.attrib['type'] for child in node: diff --git a/problem_builder/migrations/0001_initial.py b/problem_builder/migrations/0001_initial.py index e8df77a8..860bc692 100644 --- a/problem_builder/migrations/0001_initial.py +++ b/problem_builder/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models @@ -17,13 +14,13 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=50, db_index=True)), ('student_id', models.CharField(max_length=32, db_index=True)), ('course_id', models.CharField(max_length=50, db_index=True)), - ('student_input', models.TextField(default=u'', blank=True)), - ('created_on', models.DateTimeField(auto_now_add=True, verbose_name=u'created on')), - ('modified_on', models.DateTimeField(auto_now=True, verbose_name=u'modified on')), + ('student_input', models.TextField(default='', blank=True)), + ('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')), + ('modified_on', models.DateTimeField(auto_now=True, verbose_name='modified on')), ], ), migrations.AlterUniqueTogether( name='answer', - unique_together=set([('student_id', 'course_id', 'name')]), + unique_together={('student_id', 'course_id', 'name')}, ), ] diff --git a/problem_builder/migrations/0002_auto_20160121_1525.py b/problem_builder/migrations/0002_auto_20160121_1525.py index b6f8be44..7cba9959 100644 --- a/problem_builder/migrations/0002_auto_20160121_1525.py +++ b/problem_builder/migrations/0002_auto_20160121_1525.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.conf import settings from django.db import migrations, models @@ -26,6 +23,6 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name='share', - unique_together=set([('shared_by', 'shared_with', 'block_id')]), + unique_together={('shared_by', 'shared_with', 'block_id')}, ), ] diff --git a/problem_builder/migrations/0003_auto_20161124_0755.py b/problem_builder/migrations/0003_auto_20161124_0755.py index 1da75161..7d5a1d06 100644 --- a/problem_builder/migrations/0003_auto_20161124_0755.py +++ b/problem_builder/migrations/0003_auto_20161124_0755.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models @@ -18,6 +15,6 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name='answer', - unique_together=set([('student_id', 'course_key', 'name'), ('student_id', 'course_id', 'name')]), + unique_together={('student_id', 'course_key', 'name'), ('student_id', 'course_id', 'name')}, ), ] diff --git a/problem_builder/migrations/0004_copy_course_ids.py b/problem_builder/migrations/0004_copy_course_ids.py index 99568587..7988acd6 100644 --- a/problem_builder/migrations/0004_copy_course_ids.py +++ b/problem_builder/migrations/0004_copy_course_ids.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations diff --git a/problem_builder/migrations/0005_auto_20170112_1021.py b/problem_builder/migrations/0005_auto_20170112_1021.py index d3fdc406..bb5e4ed7 100644 --- a/problem_builder/migrations/0005_auto_20170112_1021.py +++ b/problem_builder/migrations/0005_auto_20170112_1021.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/problem_builder/migrations/0006_remove_deprecated_course_id.py b/problem_builder/migrations/0006_remove_deprecated_course_id.py index 79e16995..6c6f11dd 100644 --- a/problem_builder/migrations/0006_remove_deprecated_course_id.py +++ b/problem_builder/migrations/0006_remove_deprecated_course_id.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.4 on 2017-09-19 13:57 -from __future__ import unicode_literals from django.db import migrations @@ -14,7 +12,7 @@ class Migration(migrations.Migration): operations = [ migrations.AlterUniqueTogether( name='answer', - unique_together=set([('student_id', 'course_key', 'name')]), + unique_together={('student_id', 'course_key', 'name')}, ), migrations.RemoveField( model_name='answer', diff --git a/problem_builder/migrations/0007_lengthen_student_id_field.py b/problem_builder/migrations/0007_lengthen_student_id_field.py index 5e667ea1..a4915718 100644 --- a/problem_builder/migrations/0007_lengthen_student_id_field.py +++ b/problem_builder/migrations/0007_lengthen_student_id_field.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.25 on 2019-11-28 23:25 -from __future__ import unicode_literals from django.db import migrations, models diff --git a/problem_builder/mixins.py b/problem_builder/mixins.py index 364fb880..bd38f784 100644 --- a/problem_builder/mixins.py +++ b/problem_builder/mixins.py @@ -1,11 +1,9 @@ import json import pkg_resources -import six import webob from django import utils from lazy import lazy -from .settings import PB_LANGUAGE_JS_DIRECTORY_MAP from xblock.core import XBlock from xblock.fields import UNIQUE_ID, Boolean, Float, Scope, String from xblock.fragment import Fragment @@ -14,6 +12,8 @@ from problem_builder.tests.unit.utils import DateTimeEncoder +from .settings import PB_LANGUAGE_JS_DIRECTORY_MAP + loader = ResourceLoader(__name__) @@ -44,7 +44,7 @@ def _(self, text): class EnumerableChildMixin(XBlockWithTranslationServiceMixin): - CAPTION = _(u"Child") + CAPTION = _("Child") show_title = Boolean( display_name=_("Show title"), @@ -67,7 +67,7 @@ def step_number(self): @lazy def lonely_child(self): if _normalize_id(self.scope_ids.usage_id) not in self.siblings: - message = u"{child_caption}'s parent should contain {child_caption}".format(child_caption=self.CAPTION) + message = "{child_caption}'s parent should contain {child_caption}".format(child_caption=self.CAPTION) raise ValueError(message, self, self.siblings) return len(self.siblings) == 1 @@ -77,7 +77,7 @@ def display_name_with_default(self): if self.display_name: return self.display_name if not self.lonely_child: - return self._(u"{child_caption} {number}").format( + return self._("{child_caption} {number}").format( child_caption=self.CAPTION, number=self.step_number ) return self._(self.CAPTION) @@ -109,7 +109,8 @@ class MessageParentMixin: """ def get_message_content(self, message_type, or_default=False): - from problem_builder.message import MentoringMessageBlock # Import here to avoid circular dependency + from problem_builder.message import \ + MentoringMessageBlock # Import here to avoid circular dependency for child_id in self.children: if child_isinstance(self, child_id, MentoringMessageBlock): child = self.runtime.get_block(child_id) @@ -130,7 +131,7 @@ class QuestionMixin(EnumerableChildMixin): A step is a question that the user can answer (as opposed to a read-only child). """ - CAPTION = _(u"Question") + CAPTION = _("Question") has_author_view = True @@ -175,7 +176,7 @@ class NoSettingsMixin: def studio_view(self, _context=None): """ Studio View """ - return Fragment(u'

{}

'.format(self._("This XBlock does not have any settings."))) + return Fragment('

{}

'.format(self._("This XBlock does not have any settings."))) class StudentViewUserStateMixin: @@ -206,7 +207,7 @@ def build_user_state_data(self, context=None): result = {} transforms = self.transforms() - for _, field in six.iteritems(self.fields): + for _, field in self.fields.items(): # Only insert fields if their scopes and field names match if field.scope in self.INCLUDE_SCOPES and field.name in self.USER_STATE_FIELDS: transformer = transforms.get(field.name, lambda value: value) @@ -311,6 +312,6 @@ def get_translation_content(self): # i.e for polish language code is pl but js directory is pl_PL # to coup this behaviour PB_LANGUAGES_JS_DIRECTORY_MAP is added to get the desired directory. js_directory = PB_LANGUAGE_JS_DIRECTORY_MAP.get(language, 'en') - return self.resource_string('public/js/translations/{lang}/textjs.js'.format(lang=js_directory)) - except IOError: + return self.resource_string(f'public/js/translations/{js_directory}/textjs.js') + except OSError: return self.resource_string('public/js/translations/en/textjs.js') diff --git a/problem_builder/models.py b/problem_builder/models.py index 6ecfe4a6..37dba986 100644 --- a/problem_builder/models.py +++ b/problem_builder/models.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -26,9 +25,9 @@ from .platform_dependencies import AnonymousUserId - # Classes ########################################################### + class Answer(models.Model): """ Django model used to store AnswerBlock data that need to be shared @@ -48,9 +47,9 @@ class Meta: name = models.CharField(max_length=50, db_index=True) student_id = models.CharField(max_length=50, db_index=True) course_key = models.CharField(max_length=255, db_index=True) - student_input = models.TextField(blank=True, default=u'') - created_on = models.DateTimeField(u'created on', auto_now_add=True) - modified_on = models.DateTimeField(u'modified on', auto_now=True) + student_input = models.TextField(blank=True, default='') + created_on = models.DateTimeField('created on', auto_now_add=True) + modified_on = models.DateTimeField('modified on', auto_now=True) def save(self, *args, **kwargs): # Force validation of max_length diff --git a/problem_builder/mrq.py b/problem_builder/mrq.py index b66cb9f4..94d4e540 100644 --- a/problem_builder/mrq.py +++ b/problem_builder/mrq.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -22,7 +21,6 @@ import logging -import six from xblock.fields import Boolean, List, Scope, String from xblock.validation import ValidationMessage from xblockutils.resources import ResourceLoader @@ -50,7 +48,7 @@ class MRQBlock(SubmittingXBlockMixin, StudentViewUserStateMixin, QuestionnaireAb An XBlock used to ask multiple-response questions """ CATEGORY = 'pb-mrq' - STUDIO_LABEL = _(u"Multiple Response Question") + STUDIO_LABEL = _("Multiple Response Question") USER_STATE_FIELDS = ['student_choices', ] student_choices = List( @@ -91,10 +89,10 @@ class MRQBlock(SubmittingXBlockMixin, StudentViewUserStateMixin, QuestionnaireAb def describe_choice_correctness(self, choice_value): if choice_value in self.required_choices: - return self._(u"Required") + return self._("Required") elif choice_value in self.ignored_choices: - return self._(u"Ignored") - return self._(u"Not Acceptable") + return self._("Ignored") + return self._("Not Acceptable") def get_results(self, previous_result): """ @@ -111,12 +109,12 @@ def get_last_result(self): return {} def submit(self, submissions): - log.debug(u'Received MRQ submissions: "%s"', submissions) + log.debug('Received MRQ submissions: "%s"', submissions) result = self.calculate_results(submissions) self.student_choices = submissions - log.debug(u'MRQ submissions result: %s', result) + log.debug('MRQ submissions result: %s', result) return result def calculate_results(self, submissions): @@ -199,15 +197,15 @@ def choice_name(choice_value): ignored = set(data.ignored_choices) if len(required) < len(data.required_choices): - add_error(self._(u"Duplicate required choices set")) + add_error(self._("Duplicate required choices set")) if len(ignored) < len(data.ignored_choices): - add_error(self._(u"Duplicate ignored choices set")) + add_error(self._("Duplicate ignored choices set")) for val in required.intersection(ignored): - add_error(self._(u"A choice is listed as both required and ignored: {}").format(choice_name(val))) + add_error(self._("A choice is listed as both required and ignored: {}").format(choice_name(val))) for val in (required - all_values): - add_error(self._(u"A choice value listed as required does not exist: {}").format(choice_name(val))) + add_error(self._("A choice value listed as required does not exist: {}").format(choice_name(val))) for val in (ignored - all_values): - add_error(self._(u"A choice value listed as ignored does not exist: {}").format(choice_name(val))) + add_error(self._("A choice value listed as ignored does not exist: {}").format(choice_name(val))) def student_view_data(self, context=None): """ @@ -216,7 +214,7 @@ def student_view_data(self, context=None): """ return { 'id': self.name, - 'block_id': six.text_type(self.scope_ids.usage_id), + 'block_id': str(self.scope_ids.usage_id), 'display_name': self.display_name, 'title': self.display_name, 'type': self.CATEGORY, diff --git a/problem_builder/platform_dependencies.py b/problem_builder/platform_dependencies.py index f1b05f21..6eaf3699 100644 --- a/problem_builder/platform_dependencies.py +++ b/problem_builder/platform_dependencies.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # diff --git a/problem_builder/plot.py b/problem_builder/plot.py index a97efe2b..f41c87cd 100644 --- a/problem_builder/plot.py +++ b/problem_builder/plot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -21,7 +20,6 @@ import json import logging -import six from lazy.lazy import lazy from xblock.core import XBlock from xblock.fields import Scope, String @@ -69,7 +67,7 @@ class PlotBlock( """ CATEGORY = 'sb-plot' - STUDIO_LABEL = _(u"Plot") + STUDIO_LABEL = _("Plot") # Settings display_name = String( @@ -160,7 +158,7 @@ class PlotBlock( @lazy def course_key_str(self): location = _normalize_id(self.location) - return six.text_type(location.course_key) + return str(location.course_key) @property def default_claims(self): @@ -331,8 +329,8 @@ def author_preview_view(self, context): fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/plot-preview.css')) if self.overlay_ids: fragment.add_content( - u"

{}

".format( - _(u"In addition to the default and average overlays the plot includes the following overlays:") + "

{}

".format( + _("In addition to the default and average overlays the plot includes the following overlays:") )) for overlay in self.overlays: overlay_fragment = self._render_child_fragment(overlay, context, view='mentoring_view') @@ -396,7 +394,7 @@ class PlotOverlayBlock(StudioEditableXBlockMixin, XBlockWithPreviewMixin, XBlock """ CATEGORY = 'sb-plot-overlay' - STUDIO_LABEL = _(u"Plot Overlay") + STUDIO_LABEL = _("Plot Overlay") # Settings display_name = String( @@ -461,14 +459,14 @@ def add_error(msg): validation.add(ValidationMessage(ValidationMessage.ERROR, msg)) if not data.plot_label.strip(): - add_error(_(u"No plot label set. Button for toggling visibility of this overlay will not have a label.")) + add_error(_("No plot label set. Button for toggling visibility of this overlay will not have a label.")) if not data.point_color.strip(): - add_error(_(u"No point color set. This overlay will not work correctly.")) + add_error(_("No point color set. This overlay will not work correctly.")) # If parent plot is associated with one or more claims, prompt user to add claim data parent = self.get_parent() if parent.claims.strip() and not data.claim_data.strip(): - add_error(_(u"No claim data provided. This overlay will not work correctly.")) + add_error(_("No claim data provided. This overlay will not work correctly.")) def author_preview_view(self, context): return self.student_view(context) diff --git a/problem_builder/questionnaire.py b/problem_builder/questionnaire.py index cdacb0e9..44286a9f 100644 --- a/problem_builder/questionnaire.py +++ b/problem_builder/questionnaire.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -20,9 +19,9 @@ # Imports ########################################################### -import pkg_resources import uuid +import pkg_resources from django import utils from django.utils.safestring import mark_safe from lazy import lazy @@ -98,13 +97,13 @@ def get_translation_content(self): return self.resource_string('public/js/translations/{lang}/textjs.js'.format( lang=utils.translation.to_locale(utils.translation.get_language()), )) - except IOError: + except OSError: return self.resource_string('public/js/translations/en/textjs.js') def student_view(self, context=None): name = getattr(self, "unmixed_class", self.__class__).__name__ - template_path = 'templates/html/{}.html'.format(name.lower()) + template_path = f'templates/html/{name.lower()}.html' context = context.copy() if context else {} context['self'] = self @@ -116,6 +115,7 @@ def student_view(self, context=None): # of questionnaire.[css/js] into the DOM. So we use the mentoring block here if possible. block_with_resources = self.get_parent() from .mentoring import MentoringBlock + # We use an inline import here to avoid a circular dependency with the .mentoring module. if not isinstance(block_with_resources, MentoringBlock): block_with_resources = self @@ -213,9 +213,9 @@ def validate_field_data(self, validation, data): def add_error(msg): validation.add(ValidationMessage(ValidationMessage.ERROR, msg)) if not data.name: - add_error(self._(u"A unique Question ID is required.")) + add_error(self._("A unique Question ID is required.")) elif ' ' in data.name: - add_error(self._(u"Question ID should not contain spaces.")) + add_error(self._("Question ID should not contain spaces.")) def validate(self): """ @@ -230,13 +230,13 @@ def add_error(msg): all_choice_values = self.all_choice_values all_choice_values_set = set(all_choice_values) if len(all_choice_values) != len(all_choice_values_set): - add_error(self._(u"Some choice values are not unique.")) + add_error(self._("Some choice values are not unique.")) # Validate the tips: values_with_tips = set() for tip in self.get_tips(): values = set(tip.values) if values & values_with_tips: - add_error(self._(u"Multiple tips configured for the same choice.")) + add_error(self._("Multiple tips configured for the same choice.")) break values_with_tips.update(values) return validation diff --git a/problem_builder/settings.py b/problem_builder/settings.py index e7adf5d0..9048f735 100644 --- a/problem_builder/settings.py +++ b/problem_builder/settings.py @@ -66,7 +66,7 @@ # statici18n # http://django-statici18n.readthedocs.io/en/latest/settings.html -with open(os.path.join(BASE_DIR, 'problem_builder/translations/config.yaml'), 'r') as locale_config_file: +with open(os.path.join(BASE_DIR, 'problem_builder/translations/config.yaml')) as locale_config_file: locale_config = yaml.load(locale_config_file) LANGUAGES = [ diff --git a/problem_builder/slider.py b/problem_builder/slider.py index 321124c0..031da11c 100644 --- a/problem_builder/slider.py +++ b/problem_builder/slider.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -23,7 +22,6 @@ import logging import uuid -import six from xblock.core import XBlock from xblock.fields import Float, Scope, String from xblock.fragment import Fragment @@ -57,7 +55,7 @@ class SliderBlock( The student's answer is always considered "correct". """ CATEGORY = 'pb-slider' - STUDIO_LABEL = _(u"Ranged Value Slider") + STUDIO_LABEL = _("Ranged Value Slider") USER_STATE_FIELDS = ['student_value'] answerable = True @@ -106,7 +104,7 @@ def mentoring_view(self, context): """ Main view of this block """ context = context.copy() if context else {} context['question'] = self.question - context['slider_id'] = 'pb-slider-{}'.format(uuid.uuid4().hex[:20]) + context['slider_id'] = f'pb-slider-{uuid.uuid4().hex[:20]}' context['initial_value'] = int(self.student_value*100) if self.student_value is not None else 50 context['min_label'] = self.min_label context['max_label'] = self.max_label @@ -128,7 +126,7 @@ def mentoring_view(self, context): def student_view_data(self, context=None): return { 'id': self.name, - 'block_id': six.text_type(self.scope_ids.usage_id), + 'block_id': str(self.scope_ids.usage_id), 'display_name': self.display_name_with_default, 'type': self.CATEGORY, 'question': self.question, @@ -167,7 +165,7 @@ def get_results(self, _previous_result_unused=None): return self.get_last_result() def submit(self, value): - log.debug(u'Received Slider submission: "%s"', value) + log.debug('Received Slider submission: "%s"', value) if value < 0 or value > 1: return {} # Invalid self.student_value = value @@ -175,7 +173,7 @@ def submit(self, value): # Also send to the submissions API: sub_api.create_submission(self.student_item_key, value) result = self.get_last_result() - log.debug(u'Slider submission result: %s', result) + log.debug('Slider submission result: %s', result) return result def get_submission_display(self, submission): diff --git a/problem_builder/south_migrations/0001_initial.py b/problem_builder/south_migrations/0001_initial.py index 579aae31..2994dfd8 100644 --- a/problem_builder/south_migrations/0001_initial.py +++ b/problem_builder/south_migrations/0001_initial.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from south.db import db from south.v2 import SchemaMigration diff --git a/problem_builder/south_migrations/0002_copy_from_mentoring.py b/problem_builder/south_migrations/0002_copy_from_mentoring.py index a87ea812..8948dbfe 100644 --- a/problem_builder/south_migrations/0002_copy_from_mentoring.py +++ b/problem_builder/south_migrations/0002_copy_from_mentoring.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- from django.db.utils import DatabaseError - from south.db import db from south.v2 import DataMigration diff --git a/problem_builder/south_migrations/0003_auto__add_share__add_unique_share_shared_by_shared_with_block_id.py b/problem_builder/south_migrations/0003_auto__add_share__add_unique_share_shared_by_shared_with_block_id.py index 19673fdd..08685f0c 100644 --- a/problem_builder/south_migrations/0003_auto__add_share__add_unique_share_shared_by_shared_with_block_id.py +++ b/problem_builder/south_migrations/0003_auto__add_share__add_unique_share_shared_by_shared_with_block_id.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from south.db import db from south.v2 import SchemaMigration diff --git a/problem_builder/step.py b/problem_builder/step.py index a78542f5..6d22ed4e 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -20,7 +19,6 @@ import logging -import six from lazy.lazy import lazy from xblock.core import XBlock from xblock.fields import List, Scope, String @@ -81,8 +79,8 @@ class MentoringStepBlock( """ An XBlock for a step. """ - CAPTION = _(u"Step") - STUDIO_LABEL = _(u"Mentoring Step") + CAPTION = _("Step") + STUDIO_LABEL = _("Mentoring Step") CATEGORY = 'sb-step' USER_STATE_FIELDS = ['student_results'] @@ -138,14 +136,14 @@ def allowed_nested_blocks(self): try: from xmodule.video_module.video_module import VideoBlock additional_blocks.append(NestedXBlockSpec( - VideoBlock, category='video', label=_(u"Video") + VideoBlock, category='video', label=_("Video") )) except ImportError: pass try: from imagemodal import ImageModal additional_blocks.append(NestedXBlockSpec( - ImageModal, category='imagemodal', label=_(u"Image Modal") + ImageModal, category='imagemodal', label=_("Image Modal") )) except ImportError: pass @@ -153,7 +151,7 @@ def allowed_nested_blocks(self): try: from ooyala_player.ooyala_player import OoyalaPlayerBlock additional_blocks.append(NestedXBlockSpec( - OoyalaPlayerBlock, category='ooyala-player', label=_(u"Ooyala Player") + OoyalaPlayerBlock, category='ooyala-player', label=_("Ooyala Player") )) except ImportError: pass @@ -254,7 +252,7 @@ def _render_view(self, context, view): for child_id in self.children: child = self.runtime.get_block(child_id) if child is None: # child should not be None but it can happen due to bugs or permission issues - child_contents.append(u"

[{}]

".format(self._(u"Error: Unable to load child component."))) + child_contents.append("

[{}]

".format(self._("Error: Unable to load child component."))) else: if rendering_for_studio and isinstance(child, PlotBlock): # Don't use view to render plot blocks in Studio. @@ -263,7 +261,7 @@ def _render_view(self, context, view): # which causes "SubmissionRequestError" in Studio. # - author_preview_view does not supply JS code for plot that JS code for step depends on # (step calls "update" on plot to get latest data during rendering). - child_contents.append(u"

{}

".format(child.display_name)) + child_contents.append(f"

{child.display_name}

") else: child_fragment = self._render_child_fragment(child, context, view) fragment.add_frag_resources(child_fragment) @@ -295,7 +293,7 @@ def student_view_data(self, context=None): components.append(child.student_view_data(context)) return { - 'block_id': six.text_type(self.scope_ids.usage_id), + 'block_id': str(self.scope_ids.usage_id), 'display_name': self.display_name_with_default, 'type': self.CATEGORY, 'title': self.display_name_with_default, diff --git a/problem_builder/step_review.py b/problem_builder/step_review.py index 416b7ed7..c46b3977 100644 --- a/problem_builder/step_review.py +++ b/problem_builder/step_review.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -20,7 +19,6 @@ import logging -import six from xblock.core import XBlock from xblock.fields import Scope, String from xblock.fragment import Fragment @@ -113,7 +111,7 @@ def is_applicable(self, context): def student_view_data(self, context=None): return { - 'block_id': six.text_type(self.scope_ids.usage_id), + 'block_id': str(self.scope_ids.usage_id), 'display_name': self.display_name_with_default, 'type': self.CATEGORY, 'content': self.content, @@ -123,7 +121,7 @@ def student_view_data(self, context=None): def student_view(self, _context=None): """ Render this message. """ - html = u'
{content}
'.format( + html = '
{content}
'.format( content=self.content ) return Fragment(html) @@ -140,7 +138,7 @@ def author_view(self, context=None): desc += self.SCORE_CONDITIONS_DESCRIPTIONS[self.score_condition] + "
" if self.num_attempts_condition != self.ATTEMPTS_ANY: desc += self.NUM_ATTEMPTS_COND_DESCRIPTIONS[self.num_attempts_condition] - fragment.content = u'

{}

'.format(desc) + fragment.content + fragment.content = f'

{desc}

' + fragment.content return fragment @@ -168,7 +166,7 @@ def student_view(self, context=None): def student_view_data(self, context=None): return { - 'block_id': six.text_type(self.scope_ids.usage_id), + 'block_id': str(self.scope_ids.usage_id), 'display_name': self.display_name_with_default, 'type': self.CATEGORY, } @@ -218,12 +216,12 @@ def student_view(self, context=None): 'tips': review_tips, }, i18n_service=self.i18n_service) else: - html = u"" + html = "" return Fragment(html) def student_view_data(self, context=None): return { - 'block_id': six.text_type(self.scope_ids.usage_id), + 'block_id': str(self.scope_ids.usage_id), 'display_name': self.display_name_with_default, 'type': self.CATEGORY, } @@ -286,15 +284,15 @@ def student_view(self, context=None): fragment = Fragment() if "score_summary" not in context: - fragment.add_content(u"Error: This block only works inside a Step Builder block.") + fragment.add_content("Error: This block only works inside a Step Builder block.") elif not context["score_summary"]: # Note: The following text should never be seen (in theory) so does not need to be translated. - fragment.add_content(u"Your score and review messages will appear here.") + fragment.add_content("Your score and review messages will appear here.") else: for child_id in self.children: child = self.runtime.get_block(child_id) if child is None: # child should not be None but it can happen due to bugs or permission issues - fragment.add_content(u"

[{}]

".format(u"Error: Unable to load child component.")) + fragment.add_content("

[{}]

".format("Error: Unable to load child component.")) else: if hasattr(child, 'is_applicable'): if not child.is_applicable(context): @@ -321,7 +319,7 @@ def student_view_data(self, context=None): components.append(child.student_view_data(context)) return { - 'block_id': six.text_type(self.scope_ids.usage_id), + 'block_id': str(self.scope_ids.usage_id), 'display_name': self.display_name, 'type': self.CATEGORY, 'title': self.display_name, diff --git a/problem_builder/sub_api.py b/problem_builder/sub_api.py index 529d15aa..8aaec3f6 100644 --- a/problem_builder/sub_api.py +++ b/problem_builder/sub_api.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -21,7 +20,7 @@ Integrations between these XBlocks and the edX Submissions API """ -import six + from xblock.completable import XBlockCompletionMode try: @@ -47,7 +46,7 @@ def student_item_key(self): location = self.location.replace(branch=None, version=None) # Standardize the key in case it isn't already return dict( student_id=self.runtime.anonymous_student_id, - course_id=six.text_type(location.course_key), - item_id=six.text_type(location), + course_id=str(location.course_key), + item_id=str(location), item_type=self.scope_ids.block_type, ) diff --git a/problem_builder/swipe.py b/problem_builder/swipe.py index 1e6d1177..d3d49b0e 100644 --- a/problem_builder/swipe.py +++ b/problem_builder/swipe.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -22,7 +21,6 @@ import logging -import six from xblock.core import XBlock from xblock.fields import Boolean, Scope, String from xblock.fragment import Fragment @@ -53,7 +51,7 @@ class SwipeBlock( An XBlock used to ask binary-choice questions with a swiping interface """ CATEGORY = 'pb-swipe' - STUDIO_LABEL = _(u"Swipeable Binary Choice Question") + STUDIO_LABEL = _("Swipeable Binary Choice Question") USER_STATE_FIELDS = ['student_choice'] text = String( @@ -113,11 +111,11 @@ def get_last_result(self): return self.get_results({'submission': self.student_choice}) if self.student_choice else {} def submit(self, submission): - log.debug(u'Received Swipe submission: "%s"', submission) + log.debug('Received Swipe submission: "%s"', submission) # We expect to receive a boolean indicating the swipe the student made (left -> false, right -> true) self.student_choice = submission['value'] result = self.calculate_results(self.student_choice) - log.debug(u'Swipe submission result: %s', result) + log.debug('Swipe submission result: %s', result) return result def student_view_data(self, context=None): @@ -127,7 +125,7 @@ def student_view_data(self, context=None): """ return { 'id': self.name, - 'block_id': six.text_type(self.scope_ids.usage_id), + 'block_id': str(self.scope_ids.usage_id), 'display_name': self.display_name_with_default, 'type': self.CATEGORY, 'text': self.text, @@ -147,21 +145,21 @@ def expand_static_url(self, url): for this. """ if hasattr(self.runtime, 'replace_urls'): - url = self.runtime.replace_urls('"{}"'.format(url))[1:-1] + url = self.runtime.replace_urls(f'"{url}"')[1:-1] elif hasattr(self.runtime, 'course_id'): # edX Studio uses a different runtime for 'studio_view' than 'student_view', # and the 'studio_view' runtime doesn't provide the replace_urls API. from .platform_dependencies import replace_static_urls if replace_static_urls: - url = replace_static_urls('"{}"'.format(url), None, course_id=self.runtime.course_id)[1:-1] + url = replace_static_urls(f'"{url}"', None, course_id=self.runtime.course_id)[1:-1] return url def mentoring_view(self, context=None): """ Render the swipe image, text & whether it's correct within a mentoring block question. """ return Fragment( ( - u'' - u'

"{text}"

' + '' + '

"{text}"

' ).format( img_url=self.expand_static_url(self.img_url), text=self.text, diff --git a/problem_builder/table.py b/problem_builder/table.py index f2fb87c5..958d0a2d 100644 --- a/problem_builder/table.py +++ b/problem_builder/table.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -23,7 +22,6 @@ import errno import json -import six from django.contrib.auth.models import User from xblock.core import XBlock from xblock.exceptions import JsonHandlerError @@ -65,7 +63,7 @@ class MentoringTableBlock( Supports different types of formatting through the `type` parameter. """ CATEGORY = 'pb-table' - STUDIO_LABEL = _(u"Answer Recap Table") + STUDIO_LABEL = _("Answer Recap Table") display_name = String( display_name=_("Display name"), @@ -175,13 +173,13 @@ def clear_notification(self, data, suffix=''): @property def block_id(self): usage_id = self.scope_ids.usage_id - if isinstance(usage_id, six.string_types): + if isinstance(usage_id, str): return usage_id try: - return six.text_type(usage_id.replace(branch=None, version_guid=None)) + return str(usage_id.replace(branch=None, version_guid=None)) except AttributeError: pass - return six.text_type(usage_id) + return str(usage_id) @XBlock.json_handler def share_results(self, data, suffix=''): @@ -256,11 +254,11 @@ def student_view(self, context): if self.type: # Load an optional background image: - context['bg_image_url'] = self.runtime.local_resource_url(self, 'public/img/{}-bg.png'.format(self.type)) + context['bg_image_url'] = self.runtime.local_resource_url(self, f'public/img/{self.type}-bg.png') # Load an optional description for the background image, for accessibility try: - context['bg_image_description'] = loader.load_unicode('static/text/table-{}.txt'.format(self.type)) - except IOError as e: + context['bg_image_description'] = loader.load_unicode(f'static/text/table-{self.type}.txt') + except OSError as e: if e.errno == errno.ENOENT: pass else: @@ -344,7 +342,7 @@ def author_edit_view(self, context): Add some HTML to the author view that allows authors to add choices and tips. """ fragment = super().author_edit_view(context) - fragment.content = u"
{}
".format(self.header) + fragment.content + fragment.content = f"
{self.header}
" + fragment.content fragment.add_content(loader.render_django_template('templates/html/mentoring-column-add-button.html', {})) # Share styles with the questionnaire edit CSS: fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/questionnaire-edit.css')) diff --git a/problem_builder/tasks.py b/problem_builder/tasks.py index dd984654..e703dc9d 100644 --- a/problem_builder/tasks.py +++ b/problem_builder/tasks.py @@ -3,19 +3,18 @@ """ import time -import six -from django.contrib.auth.models import User -from django.db.models import F - from celery.task import task from celery.utils.log import get_task_logger +from django.contrib.auth.models import User +from django.db.models import F from lms.djangoapps.instructor_task.models import ReportStore from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey, UsageKey -from problem_builder.answer import AnswerBlock from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError +from problem_builder.answer import AnswerBlock + from .mcq import MCQBlock, RatingBlock from .mrq import MRQBlock from .questionnaire import QuestionnaireAbstractBlock @@ -39,7 +38,7 @@ def export_data(course_id, source_block_id_str, block_types, user_ids, match_str raise ValueError("Could not find the specified Block ID.") from err src_block = modulestore().get_item(usage_key) - course_key_str = six.text_type(course_key) + course_key_str = str(course_key) type_map = {cls.__name__: cls for cls in [MCQBlock, MRQBlock, RatingBlock, AnswerBlock]} if not block_types: @@ -81,12 +80,12 @@ def scan_for_blocks(block): rows += results # Generate the CSV: - filename = u"pb-data-export-{}.csv".format(time.strftime("%Y-%m-%d-%H%M%S", time.gmtime(start_timestamp))) + filename = "pb-data-export-{}.csv".format(time.strftime("%Y-%m-%d-%H%M%S", time.gmtime(start_timestamp))) report_store = ReportStore.from_config(config_name='GRADES_DOWNLOAD') report_store.store_rows(course_key, filename, rows) generation_time_s = time.time() - start_timestamp - logger.debug("Done data export - took {} seconds".format(generation_time_s)) + logger.debug(f"Done data export - took {generation_time_s} seconds") return { "error": None, @@ -185,7 +184,7 @@ def _get_submissions(course_key_str, block, user_id): """ # Load the actual student submissions for `block`. # Note this requires one giant query that retrieves all student submissions for `block` at once. - block_id = six.text_type(block.scope_ids.usage_id.replace(branch=None, version_guid=None)) + block_id = str(block.scope_ids.usage_id.replace(branch=None, version_guid=None)) block_type = _get_type(block) if block_type == 'pb-answer': block_id = block.name # item_id of Long Answer submission matches question ID and not block ID diff --git a/problem_builder/tests/integration/base_test.py b/problem_builder/tests/integration/base_test.py index 41d10930..c6dfaa6b 100644 --- a/problem_builder/tests/integration/base_test.py +++ b/problem_builder/tests/integration/base_test.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -19,6 +18,7 @@ # from unittest import mock + from xblock.fields import String from xblockutils.base_test import SeleniumBaseTest, SeleniumXBlockTest from xblockutils.resources import ResourceLoader @@ -84,7 +84,7 @@ def load_scenario(self, xml_file, params=None, load_immediately=True): Given the name of an XML file in the xml_templates folder, load it into the workbench. """ params = params or {} - scenario = loader.render_django_template("xml_templates/{}".format(xml_file), params) + scenario = loader.render_django_template(f"xml_templates/{xml_file}", params) self.set_scenario_xml(scenario) if load_immediately: return self.go_to_view("student_view") @@ -158,7 +158,7 @@ def go_to_page(self, page_title, **kwargs): @classmethod def setUpClass(cls): - super(MentoringBaseTest, cls).setUpClass() + super().setUpClass() cls.__asides_patch = mock.patch( "workbench.runtime.WorkbenchRuntime.applicable_aside_types", mock.Mock(return_value=[]) @@ -168,7 +168,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): cls.__asides_patch.stop() - super(MentoringBaseTest, cls).tearDownClass() + super().tearDownClass() class MentoringAssessmentBaseTest(ProblemBuilderBaseTest): @@ -185,7 +185,7 @@ def question_text(number): def load_assessment_scenario(self, xml_file, params=None): """ Loads an assessment scenario from an XML template """ params = params or {} - scenario = loader.render_django_template("xml_templates/{}".format(xml_file), params) + scenario = loader.render_django_template(f"xml_templates/{xml_file}", params) self.set_scenario_xml(scenario) return self.go_to_assessment() @@ -276,13 +276,13 @@ def multiple_response_question(self, number, mentoring, controls, choice_names, "Its gracefulness": False, "Its bugs": False, } - self.assertEquals(choices.state, expected_choices) + self.assertEqual(choices.state, expected_choices) for name in choice_names: choices.select(name) expected_choices[name] = True - self.assertEquals(choices.state, expected_choices) + self.assertEqual(choices.state, expected_choices) self.selected_controls(controls, last) @@ -312,7 +312,7 @@ def expect_question_visible(self, number, mentoring, question_text=None): def answer_mcq(self, number, name, value, mentoring, controls, is_last=False): self.expect_question_visible(number, mentoring) - mentoring.find_element_by_css_selector('input[name={}][value={}]'.format(name, value)).click() + mentoring.find_element_by_css_selector(f'input[name={name}][value={value}]').click() controls.submit.click() if is_last: self.wait_until_clickable(controls.review) @@ -328,7 +328,7 @@ def _assert_checkmark(self, mentoring, result): states[result] += 1 for name, count in states.items(): - self.assertEqual(len(mentoring.find_elements_by_css_selector(".submit .checkmark-{}".format(name))), count) + self.assertEqual(len(mentoring.find_elements_by_css_selector(f".submit .checkmark-{name}")), count) class GetChoices: @@ -354,4 +354,4 @@ def get_option_element(self, text): for choice in self._mcq.find_elements_by_css_selector(".choice"): if choice.text == text: return choice - raise AssertionError("Expected selectable item present: {}".format(text)) + raise AssertionError(f"Expected selectable item present: {text}") diff --git a/problem_builder/tests/integration/test_answer.py b/problem_builder/tests/integration/test_answer.py index b8d0da5d..0f2020e1 100644 --- a/problem_builder/tests/integration/test_answer.py +++ b/problem_builder/tests/integration/test_answer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # diff --git a/problem_builder/tests/integration/test_author_changes.py b/problem_builder/tests/integration/test_author_changes.py index a458f0ee..38081742 100644 --- a/problem_builder/tests/integration/test_author_changes.py +++ b/problem_builder/tests/integration/test_author_changes.py @@ -38,8 +38,8 @@ def reload_pb_block(self): def submit_answers(self, q1_answer='yes', q2_answer='elegance', q3_answer="It's boring."): """ Answer all three questions in the 'author_changes.xml' scenario correctly """ - self.pb_block_dom.find_element_by_css_selector('input[name=q1][value={}]'.format(q1_answer)).click() - self.pb_block_dom.find_element_by_css_selector('input[name=q2][value={}]'.format(q2_answer)).click() + self.pb_block_dom.find_element_by_css_selector(f'input[name=q1][value={q1_answer}]').click() + self.pb_block_dom.find_element_by_css_selector(f'input[name=q2][value={q2_answer}]').click() self.pb_block_dom.find_element_by_css_selector('textarea').send_keys(q3_answer) self.click_submit(self.pb_block_dom) diff --git a/problem_builder/tests/integration/test_clarifications.py b/problem_builder/tests/integration/test_clarifications.py index d1f94a5a..d39b775f 100644 --- a/problem_builder/tests/integration/test_clarifications.py +++ b/problem_builder/tests/integration/test_clarifications.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -80,7 +79,7 @@ class ClarificationTest(SeleniumXBlockTest): """ def prepare_xml_scenario(self, xml_template): - span = '{}'.format(self.clarification_text) + span = f'{self.clarification_text}' escaped_span = escape(span, quote=True) return xml_template.format( clarify=span, diff --git a/problem_builder/tests/integration/test_completion.py b/problem_builder/tests/integration/test_completion.py index 6fb3922d..c0c30c7e 100644 --- a/problem_builder/tests/integration/test_completion.py +++ b/problem_builder/tests/integration/test_completion.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # diff --git a/problem_builder/tests/integration/test_dashboard.py b/problem_builder/tests/integration/test_dashboard.py index c4a43081..32ebb5b8 100644 --- a/problem_builder/tests/integration/test_dashboard.py +++ b/problem_builder/tests/integration/test_dashboard.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -20,8 +19,8 @@ import json from functools import wraps from textwrap import dedent - from unittest.mock import Mock, patch + from selenium.common.exceptions import NoSuchElementException from .base_test import ProblemBuilderBaseTest @@ -167,7 +166,7 @@ def _assert_cell_contents(self, cell, expected_visible_text, expected_screen_rea self.assertEqual(screen_reader_text, expected_screen_reader_text) def _format_sr_text(self, visible_text): - return "Score: {value}".format(value=visible_text) + return f"Score: {visible_text}" def _set_mentoring_values(self): pbs = self.browser.find_elements_by_css_selector('.mentoring') @@ -285,9 +284,9 @@ def test_dashboard_alternative(self): """ dashboard = self.browser.find_element_by_css_selector('.pb-dashboard') header_p = dashboard.find_element_by_id('header-paragraph') - self.assertEquals(header_p.text, 'Header') + self.assertEqual(header_p.text, 'Header') footer_p = dashboard.find_element_by_id('footer-paragraph') - self.assertEquals(footer_p.text, 'Footer') + self.assertEqual(footer_p.text, 'Footer') steps = dashboard.find_elements_by_css_selector('tbody') self.assertEqual(len(steps), 3) diff --git a/problem_builder/tests/integration/test_instructor_tool.py b/problem_builder/tests/integration/test_instructor_tool.py index 90fcf699..21dc8704 100644 --- a/problem_builder/tests/integration/test_instructor_tool.py +++ b/problem_builder/tests/integration/test_instructor_tool.py @@ -1,12 +1,8 @@ -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals - import math import re import time - from unittest.mock import Mock, patch + from selenium.common.exceptions import NoSuchElementException from xblockutils.base_test import SeleniumXBlockTest diff --git a/problem_builder/tests/integration/test_mentoring.py b/problem_builder/tests/integration/test_mentoring.py index d53730af..c2614ea3 100644 --- a/problem_builder/tests/integration/test_mentoring.py +++ b/problem_builder/tests/integration/test_mentoring.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -17,8 +16,9 @@ # along with this program in a file in the toplevel directory called # "AGPLv3". If not, see . # -import ddt from unittest import mock + +import ddt from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.support.ui import WebDriverWait @@ -35,14 +35,14 @@ def test_display_submit_false_does_not_display_submit(self): def _get_mentoring_theme_settings(theme): return { 'package': 'problem_builder', - 'locations': ['public/themes/{}.css'.format(theme)] + 'locations': [f'public/themes/{theme}.css'] } @ddt.ddt class ProblemBuilderQuestionnaireBlockTest(ProblemBuilderBaseTest): def _get_xblock(self, mentoring, name): - return mentoring.find_element_by_css_selector(".xblock-v1[data-name='{}']".format(name)) + return mentoring.find_element_by_css_selector(f".xblock-v1[data-name='{name}']") def _get_choice(self, questionnaire, choice_index): return questionnaire.find_elements_by_css_selector(".choices-list .choice")[choice_index] @@ -94,10 +94,10 @@ def _assert_checkmark(self, checkmark, correct=True, shown=True): self.assertTrue(checkmark.is_displayed()) self.assertIn(checkmark_class, result_classes) - self.assertEquals(checkmark_label, result_label) + self.assertEqual(checkmark_label, result_label) else: self.assertFalse(checkmark.is_displayed()) - self.assertEquals('', result_label) + self.assertEqual('', result_label) def _assert_mcq(self, mcq, previous_answer_shown=True): if previous_answer_shown: @@ -271,7 +271,7 @@ def did_load_homepage(driver): title = driver.find_element_by_css_selector('h1.title') return title and title.text == "XBlock scenarios" - wait.until(did_load_homepage, u"Workbench home page should have loaded") + wait.until(did_load_homepage, "Workbench home page should have loaded") mentoring = self.go_to_view("student_view") submit = self._get_submit(mentoring) self.wait_until_visible(submit) diff --git a/problem_builder/tests/integration/test_messages.py b/problem_builder/tests/integration/test_messages.py index c58baffc..757e837c 100644 --- a/problem_builder/tests/integration/test_messages.py +++ b/problem_builder/tests/integration/test_messages.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -23,10 +22,10 @@ COMPLETED, INCOMPLETE, MAX_REACHED = "completed", "incomplete", "max_attempts_reached" MESSAGES = { - COMPLETED: u"Great job! (completed message)", - INCOMPLETE: u"Not quite! You can try again, though. (incomplete message)", + COMPLETED: "Great job! (completed message)", + INCOMPLETE: "Not quite! You can try again, though. (incomplete message)", MAX_REACHED: ( - u"Sorry, you have used up all of your allowed submissions. (max_attempts_reached message)" + "Sorry, you have used up all of your allowed submissions. (max_attempts_reached message)" ), } diff --git a/problem_builder/tests/integration/test_multiple.py b/problem_builder/tests/integration/test_multiple.py index 5747cab9..45ec2fe7 100644 --- a/problem_builder/tests/integration/test_multiple.py +++ b/problem_builder/tests/integration/test_multiple.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # diff --git a/problem_builder/tests/integration/test_progression.py b/problem_builder/tests/integration/test_progression.py index 84ab41bc..2a6c9eff 100644 --- a/problem_builder/tests/integration/test_progression.py +++ b/problem_builder/tests/integration/test_progression.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # diff --git a/problem_builder/tests/integration/test_questionnaire.py b/problem_builder/tests/integration/test_questionnaire.py index e999077b..3a28898c 100644 --- a/problem_builder/tests/integration/test_questionnaire.py +++ b/problem_builder/tests/integration/test_questionnaire.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -237,9 +236,9 @@ def test_questionnaire_html_choices(self, page): # Ensure each questionnaire label contains the input item, and the expected option. labels = self._get_questionnaire_options(choices_list) - self.assertEquals(len(labels), len(expected_options)) + self.assertEqual(len(labels), len(expected_options)) for idx, label in enumerate(labels): - self.assertEquals(len(label.find_elements_by_tag_name('input')), 1) + self.assertEqual(len(label.find_elements_by_tag_name('input')), 1) self.assertIn(expected_options[idx], label.get_attribute('innerHTML').strip()) self.assert_messages_empty(messages) @@ -260,8 +259,8 @@ def test_questionnaire_html_choices(self, page): def _get_inner_height(self, elem): return elem.size['height'] - \ - int(elem.value_of_css_property("padding-top").replace(u'px', u'')) - \ - int(elem.value_of_css_property("padding-bottom").replace(u'px', u'')) + int(elem.value_of_css_property("padding-top").replace('px', '')) - \ + int(elem.value_of_css_property("padding-bottom").replace('px', '')) @ddt.unpack @ddt.data( @@ -274,7 +273,7 @@ def test_tip_height(self, choice_value, expected_height): choices_list = mentoring.find_element_by_css_selector(".choices-list") submit = mentoring.find_element_by_css_selector('.submit input.input-main') - choice_input_css_selector = ".choice input[value={}]".format(choice_value) + choice_input_css_selector = f".choice input[value={choice_value}]" choice_input = choices_list.find_element_by_css_selector(choice_input_css_selector) choice_wrapper = choice_input.find_element_by_xpath("./ancestor::div[@class='choice']") diff --git a/problem_builder/tests/integration/test_slider.py b/problem_builder/tests/integration/test_slider.py index 39ee167a..8ed289fe 100644 --- a/problem_builder/tests/integration/test_slider.py +++ b/problem_builder/tests/integration/test_slider.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # diff --git a/problem_builder/tests/integration/test_step_builder.py b/problem_builder/tests/integration/test_step_builder.py index f311513e..fd96a2aa 100644 --- a/problem_builder/tests/integration/test_step_builder.py +++ b/problem_builder/tests/integration/test_step_builder.py @@ -1,10 +1,9 @@ import time +from unittest.mock import patch from ddt import data, ddt, unpack -from unittest.mock import patch from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.support.ui import WebDriverWait - from workbench.runtime import WorkbenchRuntime from .base_test import (CORRECT, INCORRECT, PARTIAL, GetChoices, @@ -37,7 +36,7 @@ class MultipleSliderBlocksTestMixins(): """ Mixins for testing slider blocks. Allows multiple slider blocks on the page. """ def get_slider_value(self, slider_number): - print('SLIDER NUMBER: {}'.format(slider_number)) + print(f'SLIDER NUMBER: {slider_number}') return int( self.browser.execute_script("return $('.pb-slider-range').eq(arguments[0]).val()", slider_number-1) ) @@ -117,7 +116,7 @@ def freeform_answer( self.assertIn(self.question_text(number), step_builder.text) self.assertIn("What is your goal?", step_builder.text) - self.assertEquals(saved_value, answer.get_attribute("value")) + self.assertEqual(saved_value, answer.get_attribute("value")) if not saved_value: self.assert_disabled(controls.submit) @@ -128,7 +127,7 @@ def freeform_answer( answer.clear() answer.send_keys(text_input) - self.assertEquals(text_input, answer.get_attribute("value")) + self.assertEqual(text_input, answer.get_attribute("value")) self.assert_clickable(controls.submit) self.ending_controls(controls, last) @@ -155,11 +154,11 @@ def single_choice_question(self, number, step_builder, controls, choice_name, re choices = GetChoices(question) expected_state = {"Yes": False, "Maybe not": False, "I don't understand": False} - self.assertEquals(choices.state, expected_state) + self.assertEqual(choices.state, expected_state) choices.select(choice_name) expected_state[choice_name] = True - self.assertEquals(choices.state, expected_state) + self.assertEqual(choices.state, expected_state) self.selected_controls(controls, last) @@ -186,10 +185,10 @@ def rating_question(self, number, step_builder, controls, choice_name, result, l "5 - Extremely good": False, "I don't want to rate it": False, } - self.assertEquals(choices.state, expected_choices) + self.assertEqual(choices.state, expected_choices) choices.select(choice_name) expected_choices[choice_name] = True - self.assertEquals(choices.state, expected_choices) + self.assertEqual(choices.state, expected_choices) self.ending_controls(controls, last) @@ -287,7 +286,7 @@ def peek_at_review(self, step_builder, controls, expected, extended_feedback=Fal self.assert_hidden(controls.review) self.assert_hidden(controls.review_link) - def popup_check(self, step_builder, item_feedbacks, prefix='', do_submit=True): + def popup_check(self, step_builder, item_feedbacks, prefix='', do_submit=True): # pylint: disable=arguments-renamed for index, expected_feedback in enumerate(item_feedbacks): self.wait_until_exists(prefix + " .choice") @@ -416,22 +415,22 @@ def test_step_builder(self, params): # Note that we can't use patched_method.assert_called_once_with here # because there is no way to obtain a reference to the block instance generated from the XML scenario self.assertTrue(patched_method.called) - self.assertEquals(len(patched_method.call_args_list), 2) + self.assertEqual(len(patched_method.call_args_list), 2) block_object = self.load_root_xblock() positional_args = patched_method.call_args_list[0][0] block, event, data = positional_args - self.assertEquals(block.scope_ids.usage_id, block_object.scope_ids.usage_id) - self.assertEquals(event, 'grade') - self.assertEquals(data, {'value': 0.625, 'max_value': 1}) + self.assertEqual(block.scope_ids.usage_id, block_object.scope_ids.usage_id) + self.assertEqual(event, 'grade') + self.assertEqual(data, {'value': 0.625, 'max_value': 1}) # test xblock.interaction is submitted block, event, data = patched_method.call_args_list[1][0] - self.assertEquals(block.scope_ids.usage_id, block_object.scope_ids.usage_id) - self.assertEquals(event, 'xblock.interaction') - self.assertEquals(data, { + self.assertEqual(block.scope_ids.usage_id, block_object.scope_ids.usage_id) + self.assertEqual(event, 'xblock.interaction') + self.assertEqual(data, { 'attempts_max': max_attempts if max_attempts else 'unlimited', 'attempts_count': 1, 'score': 63 }) @@ -542,7 +541,7 @@ def test_mcq_feedback(self, choice_index, choice_text, expected_feedback, correc def review_mcq(self, step_builder, choice_index, expected_feedback, correct): correctness = 'correct' if correct else 'incorrect' - mcq_link = step_builder.find_elements_by_css_selector('.{}-list li a'.format(correctness))[0] + mcq_link = step_builder.find_elements_by_css_selector(f'.{correctness}-list li a')[0] mcq_link.click() mcq = step_builder.find_element_by_css_selector(".xblock-v1[data-name='mcq_1_1']") self.assert_choice_feedback(mcq, choice_index, expected_feedback, correct) @@ -571,7 +570,7 @@ def assert_choice_result(self, choice_result, correct): result_label = choice_result.get_attribute('aria-label').strip() self.wait_until_visible(choice_result) self.assertIn(checkmark_class, result_classes) - self.assertEquals(checkmark_label, result_label) + self.assertEqual(checkmark_label, result_label) def test_review_tips(self): params = { @@ -739,7 +738,7 @@ def answer_rating_question(self, step_number, question_number, step_builder, que self.wait_until_text_in(question_text, step_builder) self.assertIn(question, step_builder.text) choices = GetChoices( - step_builder, 'div[data-name="rating_{}_{}"] > .rating'.format(step_number, question_number) + step_builder, f'div[data-name="rating_{step_number}_{question_number}"] > .rating' ) choices.select(choice_name) @@ -790,7 +789,7 @@ class Namespace: def plot_empty(self, step_builder): points = step_builder.find_elements_by_css_selector("circle") - self.assertEquals(points, []) + self.assertEqual(points, []) def check_quadrant_labels(self, step_builder, plot_controls, hidden, labels=['Q1', 'Q2', 'Q3', 'Q4']): quadrant_labels = step_builder.find_elements_by_css_selector(".quadrant-label") @@ -801,11 +800,11 @@ def check_quadrant_labels(self, step_builder, plot_controls, hidden, labels=['Q1 plot_controls.quadrants_button.value_of_css_property('border-left-color'), ] if hidden: - self.assertEquals(quadrant_labels, []) + self.assertEqual(quadrant_labels, []) self.assertTrue(all(bc == HTMLColors.RED for bc in quadrants_button_border_colors)) else: - self.assertEquals(len(quadrant_labels), 4) - self.assertEquals(set(label.text for label in quadrant_labels), set(labels)) + self.assertEqual(len(quadrant_labels), 4) + self.assertEqual({label.text for label in quadrant_labels}, set(labels)) self.assertTrue(all(bc == HTMLColors.GREEN for bc in quadrants_button_border_colors)) def click_overlay_button(self, overlay_button, overlay_on, color_on=None, color_off=HTMLColors.GREY): @@ -831,7 +830,7 @@ def click_average_button(self, plot_controls, overlay_on, color_on=HTMLColors.BL self.click_overlay_button(plot_controls.average_button, overlay_on, color_on) def check_button_label(self, button, expected_value): - self.assertEquals(button.get_attribute('value'), expected_value) + self.assertEqual(button.get_attribute('value'), expected_value) def test_empty_plot(self): step_builder, controls = self.load_assessment_scenario("step_builder_plot_defaults.xml", {}) @@ -884,7 +883,7 @@ def check_overlays(self, step_builder, total_num_points, overlays): # Check if correct number of points is present selector = 'circle' + overlay['selector'] points = step_builder.find_elements_by_css_selector(selector) - self.assertEquals(len(points), overlay['num_points']) + self.assertEqual(len(points), overlay['num_points']) # Check point colors point_colors = [ point.value_of_css_property('fill') for point in points @@ -894,12 +893,12 @@ def check_overlays(self, step_builder, total_num_points, overlays): tooltips = { point.get_attribute('data-tooltip') for point in points } - self.assertEquals(tooltips, set(overlay['tooltips'])) + self.assertEqual(tooltips, set(overlay['tooltips'])) # Check positions point_positions = { (point.get_attribute('cx'), point.get_attribute('cy')) for point in points } - self.assertEquals(point_positions, set(overlay['positions'])) + self.assertEqual(point_positions, set(overlay['positions'])) def test_plot(self): step_builder, controls = self.load_assessment_scenario("step_builder_plot.xml", {}) @@ -1054,11 +1053,11 @@ def test_plot_with_scale_questions(self): def check_display_status(self, element, hidden): if hidden: display_status = element.value_of_css_property('display') - self.assertEquals(display_status, 'none') + self.assertEqual(display_status, 'none') else: # self.wait_until_visible(element) display_status = element.value_of_css_property('display') - self.assertEquals(display_status, 'block') + self.assertEqual(display_status, 'block') def check_plot_info(self, step_builder, hidden, visible_overlays=[], hidden_overlays=[]): # Check if plot info is present and visible @@ -1073,15 +1072,15 @@ def check_plot_info(self, step_builder, hidden, visible_overlays=[], hidden_over citation = overlay['citation'] if description is not None or citation is not None: overlay_plot_label = overlay_info.find_element_by_css_selector('.overlay-plot-label') - self.assertEquals(overlay_plot_label.text, overlay['plot_label']) + self.assertEqual(overlay_plot_label.text, overlay['plot_label']) text_color = overlay_plot_label.value_of_css_property('color') - self.assertEquals(text_color, overlay['plot_label_color']) + self.assertEqual(text_color, overlay['plot_label_color']) if description is not None: overlay_description = overlay_info.find_element_by_css_selector('.overlay-description') - self.assertEquals(overlay_description.text, 'Description: ' + description) + self.assertEqual(overlay_description.text, 'Description: ' + description) if citation is not None: overlay_citation = overlay_info.find_element_by_css_selector('.overlay-citation') - self.assertEquals(overlay_citation.text, 'Source: ' + citation) + self.assertEqual(overlay_citation.text, 'Source: ' + citation) # Check if info about hidden overlays is hidden for overlay in hidden_overlays: @@ -1401,7 +1400,7 @@ def provide_freeform_answer(self, step_number, question_number, step_builder, te textarea = current_question.find_element_by_css_selector("textarea") textarea.clear() textarea.send_keys(text_input) - self.assertEquals(textarea.get_attribute("value"), text_input) + self.assertEqual(textarea.get_attribute("value"), text_input) def submit_and_go_to_review_step(self, step_builder, controls, result): controls.submit.click() diff --git a/problem_builder/tests/integration/test_table.py b/problem_builder/tests/integration/test_table.py index 14131e8e..37ed47e1 100644 --- a/problem_builder/tests/integration/test_table.py +++ b/problem_builder/tests/integration/test_table.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -70,4 +69,4 @@ def test_mentoring_table(self): table = self.go_to_page('Table 3', css_selector='.mentoring-table') patched_method.assert_called_once_with(original_contents) link = table.find_element_by_css_selector('a') - self.assertEquals(link.text, 'Updated link') + self.assertEqual(link.text, 'Updated link') diff --git a/problem_builder/tests/integration/test_titles.py b/problem_builder/tests/integration/test_titles.py index 80ef14ef..87563021 100644 --- a/problem_builder/tests/integration/test_titles.py +++ b/problem_builder/tests/integration/test_titles.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -21,9 +20,10 @@ Test that the various title/display_name options for Answer and MCQ/MRQ/Ratings work. """ +from unittest.mock import patch + # Imports ########################################################### import ddt -from unittest.mock import patch from xblockutils.base_test import SeleniumXBlockTest # Classes ########################################################### @@ -136,8 +136,8 @@ def test_all_the_things(self): for qtype in ("mcq", "mrq", "rating", "long_answer"): template = getattr(self, qtype + "_template") xml = template.format( - display_name_attr='display_name="{}"'.format(display_name) if display_name is not None else "", - show_title_attr='show_title="{}"'.format(show_title) if show_title is not None else "", + display_name_attr=f'display_name="{display_name}"' if display_name is not None else "", + show_title_attr=f'show_title="{show_title}"' if show_title is not None else "", ) self.set_scenario_xml(xml) pb_element = self.go_to_view() diff --git a/problem_builder/tests/unit/test_instructor_tool.py b/problem_builder/tests/unit/test_instructor_tool.py index 67a4603d..d87f626e 100644 --- a/problem_builder/tests/unit/test_instructor_tool.py +++ b/problem_builder/tests/unit/test_instructor_tool.py @@ -2,9 +2,9 @@ Unit tests for Instructor Tool block """ import unittest +from unittest.mock import Mock, patch import ddt -from unittest.mock import Mock, patch from xblock.field_data import DictFieldData from problem_builder.instructor_tool import (COURSE_BLOCKS_API, @@ -41,7 +41,7 @@ def setUp(self): self.runtime_mock.get_block = self._get_block self.runtime_mock.course_id = self.course_id scope_ids_mock = Mock() - scope_ids_mock.usage_id = u'0' + scope_ids_mock.usage_id = '0' self.block = InstructorToolBlock( self.runtime_mock, field_data=DictFieldData({}), scope_ids=scope_ids_mock ) @@ -60,7 +60,7 @@ def test_student_view_template_args(self): """ with patch('problem_builder.instructor_tool.loader') as patched_loader: - patched_loader.render_django_template.return_value = u'' + patched_loader.render_django_template.return_value = '' self.block.student_view() self.service_mock.i18n_service = Mock(return_value=None) patched_loader.render_django_template.assert_called_once_with('templates/html/instructor_tool.html', { diff --git a/problem_builder/tests/unit/test_migration.py b/problem_builder/tests/unit/test_migration.py index cbabc56c..1013c07a 100644 --- a/problem_builder/tests/unit/test_migration.py +++ b/problem_builder/tests/unit/test_migration.py @@ -1,7 +1,7 @@ import copy import unittest - from unittest.mock import MagicMock, Mock + from xblock.field_data import DictFieldData from problem_builder.mentoring import MentoringBlock @@ -18,16 +18,16 @@ def test_partial_completion_status_migration(self): """ # Instantiate a mentoring block with the old format student_results = [ - [u'goal', - {u'completed': True, - u'score': 1, - u'student_input': u'test', - u'weight': 1}], - [u'mcq_1_1', - {u'completed': False, - u'score': 0, - u'submission': u'maybenot', - u'weight': 1}], + ['goal', + {'completed': True, + 'score': 1, + 'student_input': 'test', + 'weight': 1}], + ['mcq_1_1', + {'completed': False, + 'score': 0, + 'submission': 'maybenot', + 'weight': 1}], ] mentoring = MentoringBlock(MagicMock(), DictFieldData({'student_results': student_results}), Mock()) self.assertEqual(copy.deepcopy(student_results), mentoring.student_results) diff --git a/problem_builder/tests/unit/test_mixins.py b/problem_builder/tests/unit/test_mixins.py index 0d2d10af..595e9af0 100644 --- a/problem_builder/tests/unit/test_mixins.py +++ b/problem_builder/tests/unit/test_mixins.py @@ -1,9 +1,9 @@ import json import unittest from datetime import datetime +from unittest.mock import MagicMock, Mock import pytz -from unittest.mock import MagicMock, Mock from xblock.core import XBlock from xblock.field_data import DictFieldData from xblock.fields import Boolean, DateTime, Integer, Scope, String diff --git a/problem_builder/tests/unit/test_models.py b/problem_builder/tests/unit/test_models.py index f44468c3..4215cf56 100644 --- a/problem_builder/tests/unit/test_models.py +++ b/problem_builder/tests/unit/test_models.py @@ -1,9 +1,10 @@ """ Unit tests for models. """ -from django.test import TestCase from unittest.mock import MagicMock, PropertyMock +from django.test import TestCase + from problem_builder.models import Answer, delete_anonymous_user_answers diff --git a/problem_builder/tests/unit/test_problem_builder.py b/problem_builder/tests/unit/test_problem_builder.py index 5f38eb4c..740db845 100644 --- a/problem_builder/tests/unit/test_problem_builder.py +++ b/problem_builder/tests/unit/test_problem_builder.py @@ -1,8 +1,8 @@ import unittest from random import random +from unittest.mock import MagicMock, Mock, PropertyMock, patch import ddt -from unittest.mock import MagicMock, Mock, PropertyMock, patch from xblock.field_data import DictFieldData from xblock.runtime import NullI18nService diff --git a/problem_builder/tests/unit/test_step.py b/problem_builder/tests/unit/test_step.py index 0e86f4f8..569e5cc4 100644 --- a/problem_builder/tests/unit/test_step.py +++ b/problem_builder/tests/unit/test_step.py @@ -1,6 +1,6 @@ import unittest - from unittest.mock import Mock + from xblock.field_data import DictFieldData from problem_builder.mixins import QuestionMixin, StepParentMixin @@ -66,8 +66,8 @@ def test_proper_number_is_returned_for_step(self): step2 = Step() block._set_children_for_test(step1, 1, "2", "Step", NotAStep(), False, step2, NotAStep()) - self.assertEquals(step1.step_number, 1) - self.assertEquals(step2.step_number, 2) + self.assertEqual(step1.step_number, 1) + self.assertEqual(step2.step_number, 2) def test_the_number_does_not_represent_the_order_of_creation(self): block = Parent() @@ -75,8 +75,8 @@ def test_the_number_does_not_represent_the_order_of_creation(self): step2 = Step() block._set_children_for_test(step2, 1, "2", "Step", NotAStep(), False, step1, NotAStep()) - self.assertEquals(step1.step_number, 2) - self.assertEquals(step2.step_number, 1) + self.assertEqual(step1.step_number, 2) + self.assertEqual(step2.step_number, 1) def test_lonely_child_is_true_for_stand_alone_steps(self): block = Parent() diff --git a/problem_builder/tests/unit/test_step_builder.py b/problem_builder/tests/unit/test_step_builder.py index bb053bf0..236b6822 100644 --- a/problem_builder/tests/unit/test_step_builder.py +++ b/problem_builder/tests/unit/test_step_builder.py @@ -1,6 +1,6 @@ import unittest - from unittest.mock import Mock + from xblock.field_data import DictFieldData from problem_builder.mentoring import MentoringWithExplicitStepsBlock @@ -50,7 +50,7 @@ def make_block(block_type, data, **kwargs): # Create top-level Step Builder block. step_builder_data = { - 'display_name': u'My Step Builder', + 'display_name': 'My Step Builder', 'show_title': False, 'weight': 5.0, 'max_attempts': 3, @@ -72,17 +72,17 @@ def make_block(block_type, data, **kwargs): blocks_by_id['child_b'] = child_b step_data = { - 'display_name': u'First Step', + 'display_name': 'First Step', 'show_title': True, - 'next_button_label': u'Next Question', - 'message': u'This is the message.', + 'next_button_label': 'Next Question', + 'message': 'This is the message.', 'children': [child_a.scope_ids.usage_id, child_b.scope_ids.usage_id], } make_block(MentoringStepBlock, step_data, for_parent=step_builder) # Create a 'Step Review' block (as child of 'Step Builder'). review_step_data = { - 'display_name': u'My Review Step', + 'display_name': 'My Review Step', } review_step = make_block(ReviewStepBlock, review_step_data, for_parent=step_builder) @@ -91,14 +91,14 @@ def make_block(block_type, data, **kwargs): # Create 'Conditional Message' block as child of 'Step Review'. conditional_message_data = { - 'content': u'This message is conditional', - 'score_condition': u'perfect', - 'num_attempts_condition': u'can_try_again', + 'content': 'This message is conditional', + 'score_condition': 'perfect', + 'num_attempts_condition': 'can_try_again', } make_block(ConditionalMessageBlock, conditional_message_data, for_parent=review_step) expected = { - 'block_id': u'1', + 'block_id': '1', 'display_name': step_builder_data['display_name'], 'title': step_builder_data['display_name'], 'show_title': step_builder_data['show_title'], @@ -107,7 +107,7 @@ def make_block(block_type, data, **kwargs): 'extended_feedback': step_builder_data['extended_feedback'], 'components': [ { - 'block_id': u'2', + 'block_id': '2', 'type': 'sb-step', 'display_name': step_data['display_name'], 'title': step_data['display_name'], @@ -117,18 +117,18 @@ def make_block(block_type, data, **kwargs): 'components': ['child_a_json'], }, { - 'block_id': u'3', + 'block_id': '3', 'type': 'sb-review-step', 'display_name': review_step_data['display_name'], 'title': review_step_data['display_name'], 'components': [ { - 'block_id': u'4', + 'block_id': '4', 'display_name': "Score Summary", 'type': 'sb-review-score', }, { - 'block_id': u'5', + 'block_id': '5', 'display_name': "Conditional Message", 'type': 'sb-conditional-message', 'content': conditional_message_data['content'], diff --git a/problem_builder/tests/unit/test_swipe.py b/problem_builder/tests/unit/test_swipe.py index d48d5974..b0dc1fc3 100644 --- a/problem_builder/tests/unit/test_swipe.py +++ b/problem_builder/tests/unit/test_swipe.py @@ -1,7 +1,7 @@ import unittest +from unittest.mock import Mock import ddt -from unittest.mock import Mock from xblock.field_data import DictFieldData from problem_builder.swipe import SwipeBlock @@ -14,7 +14,7 @@ def test_student_view_data(self): Ensure that all expected fields are always returned with appropriate values. """ - runtime = Mock(replace_urls=lambda url: '"{}"'.format(url)) + runtime = Mock(replace_urls=lambda url: f'"{url}"') block = SwipeBlock(runtime, DictFieldData({"display_name": "Test"}), Mock()) self.assertEqual(block.student_view_data()['correct'], False) diff --git a/problem_builder/tests/unit/utils.py b/problem_builder/tests/unit/utils.py index f4bb7863..cf44c5ee 100644 --- a/problem_builder/tests/unit/utils.py +++ b/problem_builder/tests/unit/utils.py @@ -3,8 +3,8 @@ """ import json from datetime import date, datetime - from unittest.mock import MagicMock, Mock, patch + from xblock.field_data import DictFieldData diff --git a/problem_builder/tip.py b/problem_builder/tip.py index 79c7edac..f86545fa 100644 --- a/problem_builder/tip.py +++ b/problem_builder/tip.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -20,7 +19,7 @@ # Imports ########################################################### -import six + from django.utils.html import strip_tags from lxml import etree from xblock.core import XBlock @@ -83,9 +82,9 @@ def display_name_with_default(self): if entry["value"] in self.values: display_name = strip_tags(entry["display_name"]) # Studio studio_view can't handle html in display_name if len(display_name) > 20: - display_name = display_name[:20] + u'…' + display_name = display_name[:20] + '…' values_list.append(display_name) - return self._(u"Tip for {list_of_choices}").format(list_of_choices=u", ".join(values_list)) + return self._("Tip for {list_of_choices}").format(list_of_choices=", ".join(values_list)) def mentoring_view(self, context=None): """ Render this XBlock within a mentoring block. """ @@ -112,7 +111,7 @@ def clean_studio_edits(self, data): Clean up the edits during studio_view save """ if "values" in data: - data["values"] = list(six.text_type(v) for v in set(data["values"])) + data["values"] = list(str(v) for v in set(data["values"])) def validate_field_data(self, validation, data): """ @@ -129,7 +128,7 @@ def add_error(msg): pass else: for dummy in set(data.values) - valid_values: - add_error(self._(u"A choice selected for this tip does not exist.")) + add_error(self._("A choice selected for this tip does not exist.")) @classmethod def parse_xml(cls, node, runtime, keys, id_generator): @@ -142,7 +141,7 @@ def parse_xml(cls, node, runtime, keys, id_generator): block.width = node.get('width', '') block.height = node.get('height', '') - block.content = six.text_type(node.text or u"") + block.content = str(node.text or "") for child in node: block.content += etree.tostring(child, encoding='unicode') diff --git a/problem_builder/utils.py b/problem_builder/utils.py index cade2132..f7686c9c 100644 --- a/problem_builder/utils.py +++ b/problem_builder/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # diff --git a/problem_builder/v1/studio_xml_utils.py b/problem_builder/v1/studio_xml_utils.py index d312e44f..907e7f1d 100644 --- a/problem_builder/v1/studio_xml_utils.py +++ b/problem_builder/v1/studio_xml_utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -26,7 +25,7 @@ It works by parsing the XML and creating XBlocks in a temporary runtime environment, so that the blocks' fields can be read and copied into Studio. """ -import six + from xblock.fields import Scope from xblock.runtime import (DictKeyValueStore, KvsFieldData, MemoryIdManager, Runtime, ScopeIds) @@ -89,7 +88,7 @@ def update_from_temp_block(real_block, temp_block): Recursively copy all fields and children from temp_block to real_block. """ # Fields: - for field_name, field in six.iteritems(temp_block.fields): + for field_name, field in temp_block.fields.items(): if field.scope in (Scope.content, Scope.settings) and field.is_set_on(temp_block): setattr(real_block, field_name, getattr(temp_block, field_name)) # Children: diff --git a/problem_builder/v1/tests/test_upgrade.py b/problem_builder/v1/tests/test_upgrade.py index d5357874..b6ef7f5a 100644 --- a/problem_builder/v1/tests/test_upgrade.py +++ b/problem_builder/v1/tests/test_upgrade.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -25,6 +24,7 @@ import ddt from lxml import etree +from sample_xblocks.basic.content import HtmlBlock from six import StringIO from xblock.core import XBlock from xblock.fields import ScopeIds @@ -33,7 +33,6 @@ from problem_builder.mentoring import MentoringBlock from problem_builder.v1.xml_changes import convert_xml_to_v2 -from sample_xblocks.basic.content import HtmlBlock xml_path = os.path.join(os.path.dirname(__file__), "xml") @@ -60,7 +59,7 @@ def test_xml_upgrade(self, file_name): """ Convert a v1 mentoring block to v2 and then compare the resulting block to a pre-converted one. """ - with open("{}/{}_old.xml".format(xml_path, file_name)) as xmlfile: + with open(f"{xml_path}/{file_name}_old.xml") as xmlfile: temp_node = etree.parse(xmlfile).getroot() old_block = self.create_block_from_node(temp_node) @@ -69,7 +68,7 @@ def test_xml_upgrade(self, file_name): convert_xml_to_v2(xml_root) converted_block = self.create_block_from_node(xml_root) - with open("{}/{}_new.xml".format(xml_path, file_name)) as xmlfile: + with open(f"{xml_path}/{file_name}_new.xml") as xmlfile: temp_node = etree.parse(xmlfile).getroot() new_block = self.create_block_from_node(temp_node) @@ -77,7 +76,7 @@ def test_xml_upgrade(self, file_name): self.assertBlocksAreEquivalent(converted_block, new_block) except AssertionError: xml_result = etree.tostring(xml_root, pretty_print=True, encoding="UTF-8") - print("Converted XML:\n{}".format(xml_result)) + print(f"Converted XML:\n{xml_result}") raise def create_block_from_node(self, node): @@ -132,5 +131,5 @@ def clean_html(self, html_str): """ # We wrap it in so that the given HTML string doesn't need a single root element. parser = etree.XMLParser(remove_blank_text=True) - parsed = etree.parse(StringIO(u"{}".format(html_str)), parser=parser).getroot() + parsed = etree.parse(StringIO(f"{html_str}"), parser=parser).getroot() return etree.tostring(parsed, pretty_print=False, encoding="UTF-8")[3:-3] diff --git a/problem_builder/v1/upgrade.py b/problem_builder/v1/upgrade.py index 62ad1c38..d6eae86d 100644 --- a/problem_builder/v1/upgrade.py +++ b/problem_builder/v1/upgrade.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -33,18 +32,16 @@ import sys import warnings -import six from lxml import etree +from mentoring import MentoringBlock from six import StringIO -from mentoring import MentoringBlock from problem_builder.mentoring import MentoringBlock as NewMentoringBlock from .platform_dependencies import StudentModule from .studio_xml_utils import studio_update_from_node from .xml_changes import convert_xml_to_v2 - if not StudentModule: raise ImportError("Could not import StudentModule from edx-platform courseware app.") @@ -64,7 +61,7 @@ def upgrade_block(store, block, from_version="v1"): warnings.simplefilter("always") convert_xml_to_v2(root, from_version=from_version) for warning in warnings_caught: - print(u" ➔ {}".format(six.text_type(warning.message))) + print(f" ➔ {str(warning.message)}") # We need some special-case handling to deal with HTML being an XModule and not a pure XBlock: try: @@ -77,7 +74,7 @@ def parse_xml_for_HtmlDescriptor(cls, node, runtime, keys, id_generator): block = runtime.construct_xblock_from_class(cls, keys) block.data = node.text if node.text else "" for child in list(node): - if isinstance(child.tag, six.string_types): + if isinstance(child.tag, str): block.data += etree.tostring(child) return block HtmlDescriptor.parse_xml = parse_xml_for_HtmlDescriptor @@ -101,12 +98,12 @@ def parse_xml_for_HtmlDescriptor(cls, node, runtime, keys, id_generator): parent_children = parent.children index = parent_children.index(old_usage_id) - url_name = six.text_type(old_usage_id.block_id) + url_name = str(old_usage_id.block_id) if "url_name" in root.attrib: url_name_xml = root.attrib.pop("url_name") if url_name != url_name_xml: - print(u" ➔ Two conflicting url_name values! Using the 'real' one : {}".format(url_name)) - print(u" ➔ References to the old url_name ({}) need to be updated manually.".format(url_name_xml)) + print(f" ➔ Two conflicting url_name values! Using the 'real' one : {url_name}") + print(f" ➔ References to the old url_name ({url_name_xml}) need to be updated manually.") block = store.create_item( user_id=None, course_key=old_usage_id.course_key, @@ -117,14 +114,14 @@ def parse_xml_for_HtmlDescriptor(cls, node, runtime, keys, id_generator): parent_children[index] = block.location parent.save() store.update_item(parent, user_id=None) - print(u" ➔ problem-builder created: {}".format(url_name)) + print(f" ➔ problem-builder created: {url_name}") # Now we've changed the block's block_type but in doing so we've disrupted the student data. # Migrate it now: student_data = StudentModule.objects.filter(module_state_key=old_usage_id) num_entries = student_data.count() if num_entries > 0: - print(u" ➔ Migrating {} student records to new block".format(num_entries)) + print(f" ➔ Migrating {num_entries} student records to new block") student_data.update(module_state_key=block.location) # Replace block with the new version and the new children: @@ -142,9 +139,9 @@ def parse_xml_for_HtmlDescriptor(cls, node, runtime, keys, id_generator): from opaque_keys.edx.keys import CourseKey from xmodule.modulestore.django import modulestore - print(u"┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓") - print(u"┃ Mentoring Upgrade Script ┃") - print(u"┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛") + print("┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓") + print("┃ Mentoring Upgrade Script ┃") + print("┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛") try: course_id = CourseKey.from_string(sys.argv[1]) @@ -161,9 +158,9 @@ def parse_xml_for_HtmlDescriptor(cls, node, runtime, keys, id_generator): store = modulestore() course = store.get_course(course_id) if course is None: - sys.exit(u"Course '{}' not found.".format(six.text_type(course_id))) - print(u" ➔ Found course: {}".format(course.display_name)) - print(u" ➔ Searching for mentoring blocks") + sys.exit(f"Course '{str(course_id)}' not found.") + print(f" ➔ Found course: {course.display_name}") + print(" ➔ Searching for mentoring blocks") blocks_found = [] def find_mentoring_blocks(block): @@ -180,40 +177,40 @@ def find_mentoring_blocks(block): find_mentoring_blocks(course) total = len(blocks_found) - print(u" ➔ Found {} mentoring blocks".format(total)) + print(f" ➔ Found {total} mentoring blocks") - print(u" ➔ Doing a quick sanity check of the url_names") + print(" ➔ Doing a quick sanity check of the url_names") url_names = set() stop = False for block_id in blocks_found: url_name = block_id.block_id block = course.runtime.get_block(block_id) if url_name in url_names: - print(u" ➔ Mentoring block {} appears in the course in multiple places!".format(url_name)) - print(u' (display_name: "{}", parent {}: "{}")'.format( + print(f" ➔ Mentoring block {url_name} appears in the course in multiple places!") + print(' (display_name: "{}", parent {}: "{}")'.format( block.display_name, block.parent, block.get_parent().display_name )) - print(u' To fix, you must delete the extra occurences.') + print(' To fix, you must delete the extra occurences.') stop = True continue - if block.url_name and block.url_name != six.text_type(block_id.block_id): - print(u" ➔ Warning: Mentoring block {} has a different url_name set in the XML.".format(url_name)) - print(u" If other blocks reference this block using the XML url_name '{}',".format(block.url_name)) - print(u" those blocks will need to be updated.") + if block.url_name and block.url_name != str(block_id.block_id): + print(f" ➔ Warning: Mentoring block {url_name} has a different url_name set in the XML.") + print(f" If other blocks reference this block using the XML url_name '{block.url_name}',") + print(" those blocks will need to be updated.") if "--force" not in sys.argv: - print(u" In order to force this upgrade to continue, add --force to the end of the command.") + print(" In order to force this upgrade to continue, add --force to the end of the command.") stop = True url_names.add(url_name) if stop: - sys.exit(u" ➔ Exiting due to errors preventing the upgrade.") + sys.exit(" ➔ Exiting due to errors preventing the upgrade.") with store.bulk_operations(course.location.course_key): count = 1 for block_id in blocks_found: block = course.runtime.get_block(block_id) - print(u" ➔ Upgrading block {} of {} - \"{}\"".format(count, total, block.url_name)) + print(f" ➔ Upgrading block {count} of {total} - \"{block.url_name}\"") count += 1 upgrade_block(store, block, from_version) - print(u" ➔ Complete.") + print(" ➔ Complete.") diff --git a/problem_builder/v1/xml_changes.py b/problem_builder/v1/xml_changes.py index 5356483a..915fbe35 100644 --- a/problem_builder/v1/xml_changes.py +++ b/problem_builder/v1/xml_changes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -23,7 +22,6 @@ import json import warnings -import six from lxml import etree @@ -87,12 +85,12 @@ def applies_to(node): return node.tag == "title" and node.getparent().tag == "problem-builder" def apply(self): - title = self.node.text.strip() if self.node.text else u'' + title = self.node.text.strip() if self.node.text else '' p = self.node.getparent() old_display_name = p.get("display_name") if old_display_name and old_display_name != title: warnings.warn( - u'Replacing display_name="{}" with value "{}"'.format(p.attrib["display_name"], title) + 'Replacing display_name="{}" with <title> value "{}"'.format(p.attrib["display_name"], title) ) p.attrib["display_name"] = title p.remove(self.node) @@ -107,7 +105,7 @@ def applies_to(node): def apply(self): p = self.node.getparent() if self.node.text: - p.text = (p.text if p.text else u"") + self.node.text + p.text = (p.text if p.text else "") + self.node.text index = list(p).index(self.node) for child in list(self.node): index += 1 @@ -148,7 +146,7 @@ def applies_to(node): return node.tag == "pb-column" and node.getparent().tag == "pb-table" def apply(self): - header_html = u"" + header_html = "" to_remove = [] for child in list(self.node): if child.tag == "header": @@ -162,7 +160,7 @@ def apply(self): if "read_only" in child.attrib: del child.attrib["read_only"] elif child.tag != "html": - warnings.warn("Invalid <pb-column> element: Unexpected <{}>".format(child.tag)) + warnings.warn(f"Invalid <pb-column> element: Unexpected <{child.tag}>") return for child in to_remove: self.node.remove(child) @@ -204,7 +202,7 @@ def apply(self): self.node.attrib.pop("read_only") for name in self.node.attrib: if name != "name": - warnings.warn("Invalid attribute found on <answer>: {}".format(name)) + warnings.warn(f"Invalid attribute found on <answer>: {name}") class QuestionToField(Change): @@ -270,7 +268,7 @@ def apply(self): def add_to_list(list_name, value): if list_name in p.attrib: - p.attrib[list_name] += ",{}".format(value) + p.attrib[list_name] += f",{value}" else: p.attrib[list_name] = value @@ -284,7 +282,7 @@ def add_to_list(list_name, value): elif self.node.attrib.get("reject"): value = self.node.attrib.pop("reject") else: - warnings.warn(u"Invalid <tip> element found: {}".format(etree.tostring(self.node))) + warnings.warn(f"Invalid <tip> element found: {etree.tostring(self.node)}") return else: # This is an MCQ or Rating question: @@ -296,9 +294,9 @@ def add_to_list(list_name, value): elif self.node.attrib.get("require"): value = self.node.attrib.pop("require") add_to_list("correct_choices", value) - warnings.warn(u"<tip> element in an MCQ/Rating used 'require' rather than 'display'") + warnings.warn("<tip> element in an MCQ/Rating used 'require' rather than 'display'") else: - warnings.warn(u"Invalid <tip> element found: {}".format(etree.tostring(self.node))) + warnings.warn(f"Invalid <tip> element found: {etree.tostring(self.node)}") return self.node.attrib["values"] = value if (self.node.text is None or self.node.text.strip() == "") and not list(self.node): @@ -320,7 +318,7 @@ class CommaSeparatedListToJson(Change): APPLY_TO_ATTRIBUTES = ("values", "correct_choices", "required_choices", "ignored_choices") def _convert_value(self, raw_value): - return json.dumps([six.text_type(val).strip() for val in raw_value.split(',')]) + return json.dumps([str(val).strip() for val in raw_value.split(',')]) @staticmethod def applies_to(node): diff --git a/run_tests.py b/run_tests.py index 52d222b5..2c6eaeba 100755 --- a/run_tests.py +++ b/run_tests.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Run tests for the Problem Builder XBlock @@ -11,8 +10,6 @@ import sys import logging -import six - logging_level_overrides = { 'workbench.views': logging.ERROR, 'django.request': logging.ERROR, @@ -35,7 +32,7 @@ from django.conf import settings settings.INSTALLED_APPS += ("problem_builder", ) - for noisy_logger, log_level in six.iteritems(logging_level_overrides): + for noisy_logger, log_level in logging_level_overrides.items(): logging.getLogger(noisy_logger).setLevel(log_level) from django.core.management import execute_from_command_line diff --git a/setup.py b/setup.py index a0bbf9f0..3054a011 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (c) 2014-2015 Harvard, edX & OpenCraft # @@ -51,8 +50,8 @@ class VerifyTagCommand(install): def run(self): tag = os.getenv('CIRCLE_TAG') - if tag != 'v{}'.format(VERSION): - info = "Git tag: {0} does not match the version of this app: {1}".format( + if tag != f'v{VERSION}': + info = "Git tag: {} does not match the version of this app: {}".format( tag, VERSION ) sys.exit(info) diff --git a/test_requirements.in b/test_requirements.in index ab4750cd..e5a86e34 100644 --- a/test_requirements.in +++ b/test_requirements.in @@ -2,9 +2,9 @@ bok_choy==0.7.1 ddt django_nose>=1.4.6 --e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock --e git+https://github.com/edx/django-pyfs.git@2.1#egg=django-pyfs==2.1 --e git+https://github.com/edx/xblock-utils.git@2.0.0#egg=xblock-utils +-e git+https://github.com/edx/acid-block.git@af1e91745cc2a68e6ef1884242135f7be2f6e9ef#egg=acid-xblock +-e git+https://github.com/edx/django-pyfs.git@3.1.0#egg=django-pyfs==3.1 +-e git+https://github.com/edx/xblock-utils.git@2.2.0#egg=xblock-utils lazy lxml mock diff --git a/test_requirements.txt b/test_requirements.txt index dd671d09..9df82acf 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,36 +1,36 @@ # -# This file is autogenerated by pip-compile with python 3.8 +# This file is autogenerated by pip-compile # To update, run: # # pip-compile --output-file=test_requirements.txt test_requirements.in # --e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock +-e git+https://github.com/edx/acid-block.git@af1e91745cc2a68e6ef1884242135f7be2f6e9ef#egg=acid-xblock # via # -r test_requirements.in # -r xblock-sdk/requirements/test.txt --e git+https://github.com/edx/django-pyfs.git@2.1#egg=django-pyfs==2.1 +-e git+https://github.com/edx/django-pyfs.git@3.1.0#egg=django-pyfs==3.1 # via # -r test_requirements.in # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt --e xblock-sdk +-e file:///home/xitij/projects/edx/src/xblocks/problem-builder/xblock-sdk # via -r test_requirements.in --e git+https://github.com/edx/xblock-utils.git@2.0.0#egg=xblock-utils +-e git+https://github.com/edx/xblock-utils.git@2.2.0#egg=xblock-utils # via -r test_requirements.in -appdirs==1.4.3 +appdirs==1.4.4 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # fs # virtualenv -arrow==0.15.6 +arrow==1.1.0 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # jinja2-time astroid==2.6.6 # via pylint -attrs==19.3.0 +attrs==21.2.0 # via # -r xblock-sdk/requirements/test.txt # pytest @@ -43,114 +43,111 @@ bok_choy==0.7.1 # via # -r test_requirements.in # -r xblock-sdk/requirements/test.txt -boto==2.49.0 +boto3==1.17.78 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt -boto3==1.13.2 + # fs-s3fs +boto==2.49.0 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt - # fs-s3fs -botocore==1.16.2 +botocore==1.20.78 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # boto3 # s3transfer -certifi==2020.4.5.1 +certifi==2020.12.5 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # requests -chardet==3.0.4 +chardet==4.0.0 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # binaryornot # requests -click==7.1.2 +click==8.0.1 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # cookiecutter -cookiecutter==1.7.2 +cookiecutter==1.7.3 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt -coverage==5.1 +coverage[toml]==5.5 # via # -r xblock-sdk/requirements/test.txt # pytest-cov -ddt==1.3.1 +ddt==1.4.2 # via # -r test_requirements.in # -r xblock-sdk/requirements/test.txt -distlib==0.3.0 +distlib==0.3.1 # via # -r xblock-sdk/requirements/test.txt # virtualenv -django==2.2.12 - # via - # -r test_requirements.in - # -r xblock-sdk/requirements/base.txt - # django-mysql - # django-pyfs - # xblock-sdk django-mysql==3.12.0 # via -r test_requirements.in django-nose==1.4.7 # via -r test_requirements.in -docutils==0.15.2 +django==2.2.23 # via + # -r test_requirements.in # -r xblock-sdk/requirements/base.txt - # -r xblock-sdk/requirements/test.txt - # botocore + # django-mysql + # django-pyfs + # xblock-sdk filelock==3.0.12 # via # -r xblock-sdk/requirements/test.txt # tox # virtualenv -fs==2.4.11 +fs-s3fs==1.1.1 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # django-pyfs - # fs-s3fs - # xblock -fs-s3fs==1.1.1 +fs==2.4.13 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # django-pyfs -idna==2.9 + # fs-s3fs + # xblock +idna==2.10 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # requests -importlib-metadata==1.6.0 - # via -r xblock-sdk/requirements/test.txt -importlib-resources==1.5.0 - # via -r xblock-sdk/requirements/test.txt +iniconfig==1.1.1 + # via + # -r xblock-sdk/requirements/test.txt + # pytest isort==5.9.3 # via pylint -jinja2==2.11.2 +jinja2-time==0.2.0 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # cookiecutter - # jinja2-time -jinja2-time==0.2.0 +jinja2==3.0.1 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # cookiecutter -jmespath==0.9.5 + # jinja2-time +jmespath==0.10.0 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # boto3 # botocore +lazy-object-proxy==1.6.0 + # via astroid lazy==1.4 # via # -r test_requirements.in @@ -158,37 +155,30 @@ lazy==1.4 # -r xblock-sdk/requirements/test.txt # acid-xblock # bok-choy -lazy-object-proxy==1.6.0 - # via astroid -lxml==4.5.0 +lxml==4.6.3 # via # -r test_requirements.in # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # xblock -mako==1.1.2 +mako==1.1.4 # via # -r xblock-sdk/requirements/test.txt # acid-xblock # xblock-utils -markupsafe==1.1.1 +markupsafe==2.0.1 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt - # cookiecutter # jinja2 # mako # xblock mccabe==0.6.1 # via pylint -mock==3.0.5 +mock==4.0.3 # via # -r test_requirements.in # -r xblock-sdk/requirements/test.txt -more-itertools==8.2.0 - # via - # -r xblock-sdk/requirements/test.txt - # pytest mysqlclient==2.0.3 # via -r test_requirements.in needle==0.5.0 @@ -200,14 +190,12 @@ nose==1.3.7 # -r xblock-sdk/requirements/test.txt # django-nose # needle -packaging==20.3 +packaging==20.9 # via # -r xblock-sdk/requirements/test.txt # pytest # tox -pathlib2==2.3.5 - # via -r xblock-sdk/requirements/test.txt -pillow==7.1.2 +pillow==8.2.0 # via # -r xblock-sdk/requirements/test.txt # needle @@ -221,7 +209,7 @@ poyo==0.5.0 # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # cookiecutter -py==1.8.1 +py==1.10.0 # via # -r xblock-sdk/requirements/test.txt # pytest @@ -238,19 +226,19 @@ pypng==0.0.20 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt -pytest==5.4.1 +pytest-cov==2.12.0 + # via -r xblock-sdk/requirements/test.txt +pytest-django==4.3.0 + # via -r xblock-sdk/requirements/test.txt +pytest-rerunfailures==9.1.1 + # via -r xblock-sdk/requirements/test.txt +pytest==6.2.4 # via # -r test_requirements.in # -r xblock-sdk/requirements/test.txt # pytest-cov # pytest-django # pytest-rerunfailures -pytest-cov==2.8.1 - # via -r xblock-sdk/requirements/test.txt -pytest-django==3.9.0 - # via -r xblock-sdk/requirements/test.txt -pytest-rerunfailures==9.0 - # via -r xblock-sdk/requirements/test.txt python-dateutil==2.8.1 # via # -r xblock-sdk/requirements/base.txt @@ -258,29 +246,29 @@ python-dateutil==2.8.1 # arrow # botocore # xblock -python-slugify==4.0.0 +python-slugify==5.0.2 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # cookiecutter -pytz==2020.1 +pytz==2021.1 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # django # fs # xblock -pyyaml==5.3.1 +pyyaml==5.4.1 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # xblock -requests==2.23.0 +requests==2.25.1 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # cookiecutter -s3transfer==0.3.3 +s3transfer==0.4.2 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt @@ -291,28 +279,23 @@ selenium==3.4.1 # -r xblock-sdk/requirements/test.txt # bok-choy # needle -simplejson==3.17.0 +simplejson==3.17.2 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # xblock-utils -six==1.14.0 +six==1.16.0 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # bok-choy # cookiecutter - # django-pyfs # fs # fs-s3fs - # mock - # packaging - # pathlib2 # python-dateutil # tox # virtualenv - # xblock -sqlparse==0.3.1 +sqlparse==0.4.1 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt @@ -322,42 +305,36 @@ text-unidecode==1.3 # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # python-slugify -toml==0.10.0 +toml==0.10.2 # via # -r xblock-sdk/requirements/test.txt + # coverage # pylint + # pytest # tox -tox==3.15.0 - # via - # -r xblock-sdk/requirements/test.txt - # tox-battery -tox-battery==0.5.2 +tox-battery==0.6.1 # via -r xblock-sdk/requirements/test.txt -typing==3.7.4.1 +tox==3.23.1 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt -urllib3==1.25.9 + # tox-battery +urllib3==1.26.4 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # botocore # requests -virtualenv==20.0.20 +virtualenv==20.4.6 # via # -r xblock-sdk/requirements/test.txt # tox -wcwidth==0.1.9 - # via - # -r xblock-sdk/requirements/test.txt - # pytest -web-fragments==0.3.1 +web-fragments==1.0.0 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # xblock # xblock-utils -webob==1.8.6 +webob==1.8.7 # via # -r test_requirements.in # -r xblock-sdk/requirements/base.txt @@ -365,17 +342,13 @@ webob==1.8.6 # xblock wrapt==1.12.1 # via astroid -xblock==1.3.0 +xblock==1.4.1 # via # -r test_requirements.in # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # acid-xblock # xblock-utils -zipp==1.2.0 - # via - # -r xblock-sdk/requirements/test.txt - # importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/xblock-sdk b/xblock-sdk index 446e979b..c1384a5b 160000 --- a/xblock-sdk +++ b/xblock-sdk @@ -1 +1 @@ -Subproject commit 446e979bad1f3bf7f891787b32d5b2e7620b2f23 +Subproject commit c1384a5b6e058a5f15b231013414f859e9924008 From 82e8aa161be38f4102495269e11e7a6f084536cb Mon Sep 17 00:00:00 2001 From: Kshitij Sobti <kshitij@sobti.in> Date: Fri, 20 Aug 2021 10:45:30 +0530 Subject: [PATCH 06/29] Fix requirements --- .../translations/babel_underscore.cfg | 2 +- requirements-dev.txt | 16 ++++++++-------- test_requirements.txt | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/problem_builder/translations/babel_underscore.cfg b/problem_builder/translations/babel_underscore.cfg index 45747558..ed47846b 100644 --- a/problem_builder/translations/babel_underscore.cfg +++ b/problem_builder/translations/babel_underscore.cfg @@ -2,4 +2,4 @@ input_encoding = utf-8 [extractors] -underscore = django_babel_underscore:extract +underscore = enmerkar_underscore:extract diff --git a/requirements-dev.txt b/requirements-dev.txt index 656af858..29a8042b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,10 @@ # Internationalization and Localization requirements -e git://github.com/edx/xblock-sdk.git@0.1.9#egg=xblock-sdk==v0.1.9 -Django~=2.2.10 -django-statici18n==1.8.2 -transifex-client==0.12.1 -edx-i18n-tools==0.5.0 -pycodestyle==2.4.0 -pylint==2.4.4 -git+https://github.com/edx/django-babel-underscore.git@37705f7377a4d0a4e673f1431895ce28a8860cd7#egg=django-babel-underscore==0.6.0 -mysqlclient==1.4.6 +Django~=2.2.24 +django-statici18n==2.0.1 +transifex-client==0.14.2 +edx-i18n-tools==0.5.3 +pycodestyle==2.7.0 +pylint==2.7.4 +enmerkar-underscore==2.1.0 +mysqlclient==2.0.3 diff --git a/test_requirements.txt b/test_requirements.txt index 9df82acf..d2e7aba8 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -13,7 +13,7 @@ # -r test_requirements.in # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt --e file:///home/xitij/projects/edx/src/xblocks/problem-builder/xblock-sdk +-e xblock-sdk # via -r test_requirements.in -e git+https://github.com/edx/xblock-utils.git@2.2.0#egg=xblock-utils # via -r test_requirements.in From 2db75b2280616efd00704036305934f403fdef2e Mon Sep 17 00:00:00 2001 From: Kshitij Sobti <kshitij@sobti.in> Date: Fri, 20 Aug 2021 11:22:05 +0530 Subject: [PATCH 07/29] Fix tests --- .gitignore | 5 ++-- circle.yml | 27 +++++-------------- problem_builder/settings.py | 2 +- .../tests/integration/test_clarifications.py | 2 +- requirements-dev.txt | 2 +- 5 files changed, 13 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 84979430..ee591cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,12 @@ *~ *.pyc -/.coverage +/.coverage* /xblock_problem_builder.egg-info -/workbench.* +/workbench* /dist /templates /var +/src *.iml .idea/* dump.rdb diff --git a/circle.yml b/circle.yml index 79cdc521..3dccfa5b 100644 --- a/circle.yml +++ b/circle.yml @@ -3,7 +3,7 @@ jobs: build: docker: - image: <<parameters.docker_image>> - - image: circleci/mysql:5.6 + - image: circleci/mysql:5.7 command: mysqld --character-set-server=latin1 --collation-server=latin1_swedish_ci environment: MYSQL_ROOT_PASSWORD: rootpw @@ -21,20 +21,7 @@ jobs: name: Update system command: | sudo apt-get update - sudo apt-get install -y libgtk3.0-cil-dev libasound2 libasound2 libdbus-glib-1-2 libdbus-1-3 libgtk2.0-0 default-libmysqlclient-dev - - run: - name: Install geckodriver - command: | - wget https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz - tar xzf geckodriver-v0.26.0-linux64.tar.gz - sudo mv geckodriver /usr/local/bin/geckodriver - which geckodriver - - run: - name: Install new firefox - command: | - wget https://archive.mozilla.org/pub/firefox/releases/70.0.1/linux-x86_64/en-US/firefox-70.0.1.tar.bz2 - tar jxf firefox-70.0.1.tar.bz2 - export PATH="$(pwd)/firefox/:$PATH" + sudo apt-get install -y libgtk3.0-cil-dev libasound2 libgtk2.0-0 default-libmysqlclient-dev libmysqlclient-dev firefox firefox-geckodriver - run: name: Sync submodules command: git submodule sync @@ -133,27 +120,27 @@ workflows: - build: name: py38-quality test_command: make quality - docker_image: circleci/python:3.8-browsers + docker_image: cimg/python:3.8-browsers filters: tags: only: /.*/ - build: name: py38-unit test_command: make test.unit - docker_image: circleci/python:3.8-browsers + docker_image: cimg/python:3.8-browsers filters: tags: only: /.*/ - build: name: py38-integration test_command: make test.integration - docker_image: circleci/python:3.8-browsers + docker_image: cimg/python:3.8-browsers filters: tags: only: /.*/ - coverage: name: py38-coverage - docker_image: circleci/python:3.8 + docker_image: cimg/python:3.8 filters: tags: only: /.*/ @@ -163,7 +150,7 @@ workflows: - py38-integration - deploy: name: py38-deploy-bdist_wheel - docker_image: circleci/python:3.8 + docker_image: cimg/python:3.8 dist_type: bdist_wheel requires: - py38-coverage diff --git a/problem_builder/settings.py b/problem_builder/settings.py index 9048f735..0609773a 100644 --- a/problem_builder/settings.py +++ b/problem_builder/settings.py @@ -67,7 +67,7 @@ # http://django-statici18n.readthedocs.io/en/latest/settings.html with open(os.path.join(BASE_DIR, 'problem_builder/translations/config.yaml')) as locale_config_file: - locale_config = yaml.load(locale_config_file) + locale_config = yaml.safe_load(locale_config_file) LANGUAGES = [ (code, code,) diff --git a/problem_builder/tests/integration/test_clarifications.py b/problem_builder/tests/integration/test_clarifications.py index d39b775f..b80d7dbe 100644 --- a/problem_builder/tests/integration/test_clarifications.py +++ b/problem_builder/tests/integration/test_clarifications.py @@ -20,7 +20,7 @@ Test that <span class="pb-clarification"> elements are transformed into LMS-like tooltips. """ -from cgi import escape +from html import escape # Imports ########################################################### import ddt diff --git a/requirements-dev.txt b/requirements-dev.txt index 29a8042b..ac5c1b53 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ # Internationalization and Localization requirements --e git://github.com/edx/xblock-sdk.git@0.1.9#egg=xblock-sdk==v0.1.9 +-e xblock-sdk Django~=2.2.24 django-statici18n==2.0.1 transifex-client==0.14.2 From de235c61e5f54081fb5f5bec713809b485b05f1d Mon Sep 17 00:00:00 2001 From: Kshitij Sobti <kshitij@sobti.in> Date: Wed, 1 Sep 2021 13:54:05 +0530 Subject: [PATCH 08/29] pytest --- Makefile | 10 +++--- circle.yml | 7 ++-- problem_builder/answer.py | 2 +- problem_builder/mentoring.py | 4 +-- problem_builder/settings.py | 8 ++--- problem_builder/step.py | 2 +- .../tests/unit/test_answer_mixin.py | 2 ++ problem_builder/v1/tests/test_upgrade.py | 4 +-- pytest.ini | 2 ++ requirements-dev.txt | 12 +++---- requirements.txt | 4 +-- run_tests.py | 8 ++--- setup.cfg | 3 ++ setup.py | 2 +- test_requirements.in | 10 ++---- test_requirements.txt | 32 +++++++------------ 16 files changed, 54 insertions(+), 58 deletions(-) create mode 100644 pytest.ini diff --git a/Makefile b/Makefile index ad6f3564..d594cde9 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ help: ## display this help message @echo "Please use \`make <target>' where <target> is one of" @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' -upgrade: +upgrade: ## Upgrade test requirements pip-compile --upgrade --output-file test_requirements.txt test_requirements.in extract_translations: ## extract strings to be translated, outputting .po files @@ -24,7 +24,7 @@ compile_translations: ## compile translation files, outputting .mo files for eac cd $(WORKING_DIR) && i18n_tool generate python manage.py compilejsi18n --namespace ProblemBuilderXBlockI18N --output $(JS_TARGET) -detect_changed_source_translations: +detect_changed_source_translations: ## Detect changes in source code that affect translations cd $(WORKING_DIR) && i18n_tool changed dummy_translations: ## generate dummy translation (.po) files @@ -48,10 +48,10 @@ quality: ## run quality checkers on the codebase pylint problem_builder test.unit: ## run python unit tests with coverage - coverage run run_tests.py problem_builder/v1/tests - coverage run run_tests.py problem_builder/tests/unit + pytest problem_builder/v1/tests + pytest problem_builder/tests/unit test.integration: ## run python selenium tests with coverage - coverage run run_tests.py problem_builder/tests/integration + pytest problem_builder/tests/integration test: quality test.unit test.integration ## Run all tests diff --git a/circle.yml b/circle.yml index 3dccfa5b..7556f5ca 100644 --- a/circle.yml +++ b/circle.yml @@ -35,11 +35,14 @@ jobs: mkdir -p /tmp/coverage/$CIRCLE_SHA1 virtualenv venv source venv/bin/activate + mkdir var + pip -q install -r requirements.txt + pip -q install -r requirements-dev.txt pip -q install -r test_requirements.txt pip install -e . <<parameters.test_command>> - if [ -e .coverage.* ]; then - cp .coverage.* /tmp/coverage/$CIRCLE_SHA1/. + if [ -e .coverage* ]; then + cp .coverage* /tmp/coverage/$CIRCLE_SHA1/. fi - persist_to_workspace: root: /tmp diff --git a/problem_builder/answer.py b/problem_builder/answer.py index 51b96ff7..615ebafe 100644 --- a/problem_builder/answer.py +++ b/problem_builder/answer.py @@ -243,7 +243,7 @@ def submit(self, submission): item_key['item_id'] = self.name sub_api.create_submission(item_key, self.student_input) - log.info(u'Answer submitted for`{}`: "{}"'.format(self.name, self.student_input)) + log.info('Answer submitted for`{}`: "{}"'.format(self.name, self.student_input)) return self.get_results() @property diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 012c3703..0d3eabd0 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -602,7 +602,7 @@ def _get_standard_results(self): @XBlock.json_handler def submit(self, submissions, suffix=''): - log.info(u'Received submissions: {}'.format(submissions)) + log.info('Received submissions: {}'.format(submissions)) # server-side check that the user is allowed to submit: if self.max_attempts_reached: raise JsonHandlerError(403, "Maximum number of attempts already reached.") @@ -950,7 +950,7 @@ def review_tips(self): # Review tips are only shown if the student is allowed to try again. return [] review_tips = [] - status_cache = dict() + status_cache = {} steps = self.steps for step in steps: status_cache.update(dict(step.student_results)) diff --git a/problem_builder/settings.py b/problem_builder/settings.py index 0609773a..30330fcd 100644 --- a/problem_builder/settings.py +++ b/problem_builder/settings.py @@ -7,6 +7,8 @@ """ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) +from workbench.settings import * + import os import yaml @@ -33,11 +35,9 @@ # Application definition -INSTALLED_APPS = ( +INSTALLED_APPS += ( 'statici18n', 'problem_builder', - 'django.contrib.auth', - 'django.contrib.contenttypes', ) # Internationalization @@ -66,7 +66,7 @@ # statici18n # http://django-statici18n.readthedocs.io/en/latest/settings.html -with open(os.path.join(BASE_DIR, 'problem_builder/translations/config.yaml')) as locale_config_file: +with open(os.path.join(BASE_DIR, 'problem_builder/translations/config.yaml'), encoding='utf8') as locale_config_file: locale_config = yaml.safe_load(locale_config_file) LANGUAGES = [ diff --git a/problem_builder/step.py b/problem_builder/step.py index 6d22ed4e..4dde1c47 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -169,7 +169,7 @@ def has_question(self): def submit(self, submissions): """ Handle a student submission. This is called by the parent XBlock. """ - log.info(u'Received submissions: {}'.format(submissions)) + log.info('Received submissions: {}'.format(submissions)) # Submit child blocks (questions) and gather results submit_results = [] diff --git a/problem_builder/tests/unit/test_answer_mixin.py b/problem_builder/tests/unit/test_answer_mixin.py index c7e6857f..91c2042d 100644 --- a/problem_builder/tests/unit/test_answer_mixin.py +++ b/problem_builder/tests/unit/test_answer_mixin.py @@ -2,6 +2,7 @@ Unit tests for AnswerMixin. """ import json +import pytest import unittest from collections import namedtuple from datetime import datetime @@ -12,6 +13,7 @@ from problem_builder.models import Answer +@pytest.mark.django_db class TestAnswerMixin(unittest.TestCase): """ Unit tests for AnswerMixin. """ diff --git a/problem_builder/v1/tests/test_upgrade.py b/problem_builder/v1/tests/test_upgrade.py index b6ef7f5a..16357ec5 100644 --- a/problem_builder/v1/tests/test_upgrade.py +++ b/problem_builder/v1/tests/test_upgrade.py @@ -59,7 +59,7 @@ def test_xml_upgrade(self, file_name): """ Convert a v1 mentoring block to v2 and then compare the resulting block to a pre-converted one. """ - with open(f"{xml_path}/{file_name}_old.xml") as xmlfile: + with open(f"{xml_path}/{file_name}_old.xml", encoding='utf8') as xmlfile: temp_node = etree.parse(xmlfile).getroot() old_block = self.create_block_from_node(temp_node) @@ -68,7 +68,7 @@ def test_xml_upgrade(self, file_name): convert_xml_to_v2(xml_root) converted_block = self.create_block_from_node(xml_root) - with open(f"{xml_path}/{file_name}_new.xml") as xmlfile: + with open(f"{xml_path}/{file_name}_new.xml", encoding='utf8') as xmlfile: temp_node = etree.parse(xmlfile).getroot() new_block = self.create_block_from_node(temp_node) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..cac1ce11 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --cov . --ds=problem_builder.settings --cov-append \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index ac5c1b53..e621e3c4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,10 @@ # Internationalization and Localization requirements -e xblock-sdk Django~=2.2.24 -django-statici18n==2.0.1 -transifex-client==0.14.2 -edx-i18n-tools==0.5.3 -pycodestyle==2.7.0 -pylint==2.7.4 +django-statici18n +transifex-client +edx-i18n-tools +pycodestyle +pylint enmerkar-underscore==2.1.0 -mysqlclient==2.0.3 +mysqlclient diff --git a/requirements.txt b/requirements.txt index 89d4ce10..f5f1582c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ XBlock>=1.2 ddt -unicodecsv==0.11.0 +unicodecsv edx-opaque-keys>=0.4 --e git+https://github.com/edx/xblock-utils.git@2.0.0#egg=xblock-utils +xblock-utils -e . diff --git a/run_tests.py b/run_tests.py index 2c6eaeba..f4f8e5ea 100755 --- a/run_tests.py +++ b/run_tests.py @@ -19,7 +19,7 @@ if __name__ == "__main__": # Use the workbench settings file: - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "workbench.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "problem_builder.settings") # Configure a range of ports in case the default port of 8081 is in use os.environ.setdefault("DJANGO_LIVE_TEST_SERVER_ADDRESS", "localhost:8081-8099") @@ -28,9 +28,9 @@ except OSError: # May already exist. pass - - from django.conf import settings - settings.INSTALLED_APPS += ("problem_builder", ) +# + # from django.conf import settings + # settings.INSTALLED_APPS += ("problem_builder", ) for noisy_logger, log_level in logging_level_overrides.items(): logging.getLogger(noisy_logger).setLevel(log_level) diff --git a/setup.cfg b/setup.cfg index 855aa7c3..bc506d86 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,6 @@ [pycodestyle] max-line-length = 120 exclude = problem_builder/migrations + +; [tool:pytest] +; DJANGO_SETTINGS_MODULE = problem_builder.settings \ No newline at end of file diff --git a/setup.py b/setup.py index 3054a011..43055aa9 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ # Constants ######################################################### -VERSION = '4.1.13' +VERSION = '5.0.0' # Functions ######################################################### diff --git a/test_requirements.in b/test_requirements.in index e5a86e34..7be1d77a 100644 --- a/test_requirements.in +++ b/test_requirements.in @@ -1,16 +1,12 @@ -e xblock-sdk -bok_choy==0.7.1 +bok_choy ddt -django_nose>=1.4.6 --e git+https://github.com/edx/acid-block.git@af1e91745cc2a68e6ef1884242135f7be2f6e9ef#egg=acid-xblock --e git+https://github.com/edx/django-pyfs.git@3.1.0#egg=django-pyfs==3.1 --e git+https://github.com/edx/xblock-utils.git@2.2.0#egg=xblock-utils lazy lxml mock -selenium==3.4.1 +selenium<4 webob -XBlock>=1.2 +XBlock Django~=2.2.10 pytest pylint diff --git a/test_requirements.txt b/test_requirements.txt index d2e7aba8..182aed3f 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -4,19 +4,10 @@ # # pip-compile --output-file=test_requirements.txt test_requirements.in # --e git+https://github.com/edx/acid-block.git@af1e91745cc2a68e6ef1884242135f7be2f6e9ef#egg=acid-xblock - # via - # -r test_requirements.in - # -r xblock-sdk/requirements/test.txt --e git+https://github.com/edx/django-pyfs.git@3.1.0#egg=django-pyfs==3.1 - # via - # -r test_requirements.in - # -r xblock-sdk/requirements/base.txt - # -r xblock-sdk/requirements/test.txt -e xblock-sdk # via -r test_requirements.in --e git+https://github.com/edx/xblock-utils.git@2.2.0#egg=xblock-utils - # via -r test_requirements.in +acid-xblock==0.2.1 + # via -r xblock-sdk/requirements/test.txt appdirs==1.4.4 # via # -r xblock-sdk/requirements/base.txt @@ -28,7 +19,7 @@ arrow==1.1.0 # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # jinja2-time -astroid==2.6.6 +astroid==2.7.3 # via pylint attrs==21.2.0 # via @@ -90,10 +81,12 @@ distlib==0.3.1 # via # -r xblock-sdk/requirements/test.txt # virtualenv -django-mysql==3.12.0 - # via -r test_requirements.in -django-nose==1.4.7 +django-mysql==4.0.0 # via -r test_requirements.in +django-pyfs==3.0 + # via + # -r xblock-sdk/requirements/base.txt + # -r xblock-sdk/requirements/test.txt django==2.2.23 # via # -r test_requirements.in @@ -165,7 +158,6 @@ mako==1.1.4 # via # -r xblock-sdk/requirements/test.txt # acid-xblock - # xblock-utils markupsafe==2.0.1 # via # -r xblock-sdk/requirements/base.txt @@ -188,7 +180,6 @@ needle==0.5.0 nose==1.3.7 # via # -r xblock-sdk/requirements/test.txt - # django-nose # needle packaging==20.9 # via @@ -199,6 +190,8 @@ pillow==8.2.0 # via # -r xblock-sdk/requirements/test.txt # needle +platformdirs==2.3.0 + # via pylint pluggy==0.13.1 # via # -r xblock-sdk/requirements/test.txt @@ -216,7 +209,7 @@ py==1.10.0 # tox pycodestyle==2.7.0 # via -r test_requirements.in -pylint==2.9.6 +pylint==2.10.2 # via -r test_requirements.in pyparsing==2.4.7 # via @@ -283,7 +276,6 @@ simplejson==3.17.2 # via # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt - # xblock-utils six==1.16.0 # via # -r xblock-sdk/requirements/base.txt @@ -333,7 +325,6 @@ web-fragments==1.0.0 # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # xblock - # xblock-utils webob==1.8.7 # via # -r test_requirements.in @@ -348,7 +339,6 @@ xblock==1.4.1 # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # acid-xblock - # xblock-utils # The following packages are considered to be unsafe in a requirements file: # setuptools From 6fd01121c6b0f092f6b500edc00e6b6952a0771a Mon Sep 17 00:00:00 2001 From: Meysam Azad <meysam@opencraft.com> Date: Thu, 2 Sep 2021 16:01:34 +0300 Subject: [PATCH 09/29] fix: run tests via coverage to enable report --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index d594cde9..eee776d2 100644 --- a/Makefile +++ b/Makefile @@ -48,10 +48,10 @@ quality: ## run quality checkers on the codebase pylint problem_builder test.unit: ## run python unit tests with coverage - pytest problem_builder/v1/tests - pytest problem_builder/tests/unit + coverage run run_tests.py problem_builder/v1/tests + coverage run run_tests.py problem_builder/tests/unit test.integration: ## run python selenium tests with coverage - pytest problem_builder/tests/integration + coverage run run_tests.py problem_builder/tests/integration test: quality test.unit test.integration ## Run all tests From 87c5694c0c0882037e59cfa0fdfb82fb47e8e62b Mon Sep 17 00:00:00 2001 From: Meysam Azad <meysam@opencraft.com> Date: Thu, 2 Sep 2021 19:21:36 +0300 Subject: [PATCH 10/29] fix: change coverage directory --- circle.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/circle.yml b/circle.yml index 7556f5ca..d3c9ab4c 100644 --- a/circle.yml +++ b/circle.yml @@ -32,7 +32,7 @@ jobs: name: Run tests command: | export PATH="$(pwd)/firefox/:$PATH" - mkdir -p /tmp/coverage/$CIRCLE_SHA1 + mkdir -p /tmp/workspace/coverage/$CIRCLE_SHA1 virtualenv venv source venv/bin/activate mkdir var @@ -42,7 +42,7 @@ jobs: pip install -e . <<parameters.test_command>> if [ -e .coverage* ]; then - cp .coverage* /tmp/coverage/$CIRCLE_SHA1/. + cp .coverage* /tmp/workspace/coverage/$CIRCLE_SHA1/. fi - persist_to_workspace: root: /tmp From 094702ace3b0d9fa5453064ec1806f5de1ef69ff Mon Sep 17 00:00:00 2001 From: Meysam Azad <meysam@opencraft.com> Date: Thu, 2 Sep 2021 19:25:59 +0300 Subject: [PATCH 11/29] fix: point to coverage directory --- circle.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/circle.yml b/circle.yml index d3c9ab4c..8fd12407 100644 --- a/circle.yml +++ b/circle.yml @@ -32,7 +32,7 @@ jobs: name: Run tests command: | export PATH="$(pwd)/firefox/:$PATH" - mkdir -p /tmp/workspace/coverage/$CIRCLE_SHA1 + mkdir -p /tmp/coverage/$CIRCLE_SHA1 virtualenv venv source venv/bin/activate mkdir var @@ -42,7 +42,7 @@ jobs: pip install -e . <<parameters.test_command>> if [ -e .coverage* ]; then - cp .coverage* /tmp/workspace/coverage/$CIRCLE_SHA1/. + cp .coverage* /tmp/coverage/$CIRCLE_SHA1/. fi - persist_to_workspace: root: /tmp @@ -69,7 +69,7 @@ jobs: - run: command: | source venv/bin/activate - coverage combine /tmp/workspace/coverage/$CIRCLE_SHA1/ + coverage combine /tmp/coverage/$CIRCLE_SHA1/ coverage report deploy: From 48fe006cf852e68aa1d6c03c2706d424f1f9e70e Mon Sep 17 00:00:00 2001 From: Kshitij Sobti <kshitij@sobti.in> Date: Fri, 3 Sep 2021 04:27:20 +0530 Subject: [PATCH 12/29] Switch to orbs for circleci --- circle.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/circle.yml b/circle.yml index 8fd12407..8720d05e 100644 --- a/circle.yml +++ b/circle.yml @@ -1,4 +1,6 @@ version: 2.1 +orbs: + browser-tools: circleci/browser-tools@1.2.1 jobs: build: docker: @@ -12,26 +14,26 @@ jobs: type: string docker_image: type: string + parallel: + default: 1 + type: integer + parallelism: <<parameters.parallel>> environment: MOZ_HEADLESS: 1 WORKBENCH_DATABASES: '{"default": {"ENGINE": "django.db.backends.mysql", "NAME": "db", "USER": "root", "PASSWORD": "rootpw", "HOST": "127.0.0.1", "OPTIONS": {"charset": "utf8mb4"}}}' steps: - checkout - - run: - name: Update system - command: | - sudo apt-get update - sudo apt-get install -y libgtk3.0-cil-dev libasound2 libgtk2.0-0 default-libmysqlclient-dev libmysqlclient-dev firefox firefox-geckodriver + - browser-tools/install-firefox + - browser-tools/install-geckodriver - run: name: Sync submodules command: git submodule sync - run: name: Update submodules - command: git submodule update --init + command: git submodule update --init - run: name: Run tests command: | - export PATH="$(pwd)/firefox/:$PATH" mkdir -p /tmp/coverage/$CIRCLE_SHA1 virtualenv venv source venv/bin/activate @@ -136,7 +138,11 @@ workflows: only: /.*/ - build: name: py38-integration - test_command: make test.integration + parallel: 4 + test_command: | + set -e + TEST_FILES=$(circleci tests glob "problem_builder/tests/integration/test_*.py" | circleci tests split --split-by=timings) + pytest --verbose $TEST_FILES docker_image: cimg/python:3.8-browsers filters: tags: From bc561bccc89aaf60cfb9fb49927b09c714362b2c Mon Sep 17 00:00:00 2001 From: Kshitij Sobti <kshitij@sobti.in> Date: Fri, 3 Sep 2021 16:01:26 +0530 Subject: [PATCH 13/29] test --- circle.yml | 2 +- requirements-dev.txt | 2 +- requirements.txt | 1 - test_requirements.in | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/circle.yml b/circle.yml index 8720d05e..e240f330 100644 --- a/circle.yml +++ b/circle.yml @@ -20,7 +20,7 @@ jobs: parallelism: <<parameters.parallel>> environment: MOZ_HEADLESS: 1 - WORKBENCH_DATABASES: '{"default": {"ENGINE": "django.db.backends.mysql", "NAME": "db", "USER": "root", "PASSWORD": "rootpw", "HOST": "127.0.0.1", "OPTIONS": {"charset": "utf8mb4"}}}' + WORKBENCH_DATABASES: '{"default": {"ENGINE": "django.db.backends.mysql", "NAME": "db", "USER": "root", "PASSWORD": "rootpw", "HOST": "127.0.0.1", "OPTIONS": {"charset": "utf8mb4"}}}' steps: - checkout - browser-tools/install-firefox diff --git a/requirements-dev.txt b/requirements-dev.txt index e621e3c4..2381d4a5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ # Internationalization and Localization requirements -e xblock-sdk -Django~=2.2.24 +Django<3 django-statici18n transifex-client edx-i18n-tools diff --git a/requirements.txt b/requirements.txt index f5f1582c..c0dabdb1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ XBlock>=1.2 -ddt unicodecsv edx-opaque-keys>=0.4 xblock-utils diff --git a/test_requirements.in b/test_requirements.in index 7be1d77a..13fb0ca9 100644 --- a/test_requirements.in +++ b/test_requirements.in @@ -7,7 +7,7 @@ mock selenium<4 webob XBlock -Django~=2.2.10 +Django<3 pytest pylint pycodestyle From bdd205f9f4ec59c6cb904e802ff05693286948c4 Mon Sep 17 00:00:00 2001 From: Giovanni Cimolin da Silva <giovannicimolin@gmail.com> Date: Mon, 6 Sep 2021 11:40:05 -0300 Subject: [PATCH 14/29] Increase webdriver delay timeout --- problem_builder/tests/integration/test_step_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/problem_builder/tests/integration/test_step_builder.py b/problem_builder/tests/integration/test_step_builder.py index fd96a2aa..88905d29 100644 --- a/problem_builder/tests/integration/test_step_builder.py +++ b/problem_builder/tests/integration/test_step_builder.py @@ -1420,7 +1420,7 @@ def is_scrolled_to_top(driver): tolerance = 2 return abs(scroll_top - step_builder_offset) <= tolerance - wait = WebDriverWait(self.browser, 5) + wait = WebDriverWait(self.browser, 10) wait.until(is_scrolled_to_top) def scroll_down(self): From 0e68301f01e834ba28c042677578928e82585c62 Mon Sep 17 00:00:00 2001 From: Giovanni Cimolin da Silva <giovannicimolin@gmail.com> Date: Mon, 6 Sep 2021 11:50:33 -0300 Subject: [PATCH 15/29] Increase timeout again --- problem_builder/tests/integration/test_step_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/problem_builder/tests/integration/test_step_builder.py b/problem_builder/tests/integration/test_step_builder.py index 88905d29..e44b39fe 100644 --- a/problem_builder/tests/integration/test_step_builder.py +++ b/problem_builder/tests/integration/test_step_builder.py @@ -1420,7 +1420,7 @@ def is_scrolled_to_top(driver): tolerance = 2 return abs(scroll_top - step_builder_offset) <= tolerance - wait = WebDriverWait(self.browser, 10) + wait = WebDriverWait(self.browser, 20) wait.until(is_scrolled_to_top) def scroll_down(self): From 7ad0335c57eb8b2a1b17e04656d7e1110bf51a61 Mon Sep 17 00:00:00 2001 From: Giovanni Cimolin da Silva <giovannicimolin@gmail.com> Date: Mon, 6 Sep 2021 12:01:43 -0300 Subject: [PATCH 16/29] Test extra delay when clicking on link --- problem_builder/tests/integration/test_step_builder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/problem_builder/tests/integration/test_step_builder.py b/problem_builder/tests/integration/test_step_builder.py index e44b39fe..12ed568f 100644 --- a/problem_builder/tests/integration/test_step_builder.py +++ b/problem_builder/tests/integration/test_step_builder.py @@ -543,6 +543,7 @@ def review_mcq(self, step_builder, choice_index, expected_feedback, correct): correctness = 'correct' if correct else 'incorrect' mcq_link = step_builder.find_elements_by_css_selector(f'.{correctness}-list li a')[0] mcq_link.click() + time.sleep(3) # give some time for changes mcq = step_builder.find_element_by_css_selector(".xblock-v1[data-name='mcq_1_1']") self.assert_choice_feedback(mcq, choice_index, expected_feedback, correct) From f476d227f9301f847a1e29915b903fe55f22e931 Mon Sep 17 00:00:00 2001 From: Giovanni Cimolin da Silva <giovannicimolin@gmail.com> Date: Mon, 6 Sep 2021 14:06:33 -0300 Subject: [PATCH 17/29] Revert "Test extra delay when clicking on link" This reverts commit 7ad0335c57eb8b2a1b17e04656d7e1110bf51a61. --- problem_builder/tests/integration/test_step_builder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/problem_builder/tests/integration/test_step_builder.py b/problem_builder/tests/integration/test_step_builder.py index 12ed568f..e44b39fe 100644 --- a/problem_builder/tests/integration/test_step_builder.py +++ b/problem_builder/tests/integration/test_step_builder.py @@ -543,7 +543,6 @@ def review_mcq(self, step_builder, choice_index, expected_feedback, correct): correctness = 'correct' if correct else 'incorrect' mcq_link = step_builder.find_elements_by_css_selector(f'.{correctness}-list li a')[0] mcq_link.click() - time.sleep(3) # give some time for changes mcq = step_builder.find_element_by_css_selector(".xblock-v1[data-name='mcq_1_1']") self.assert_choice_feedback(mcq, choice_index, expected_feedback, correct) From 273cc1cb51aaf1a1f1397b87088578860a8e9c73 Mon Sep 17 00:00:00 2001 From: Giovanni Cimolin da Silva <giovannicimolin@gmail.com> Date: Mon, 6 Sep 2021 16:27:54 -0300 Subject: [PATCH 18/29] test delay in the right place --- problem_builder/tests/integration/test_step_builder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/problem_builder/tests/integration/test_step_builder.py b/problem_builder/tests/integration/test_step_builder.py index e44b39fe..b397d15b 100644 --- a/problem_builder/tests/integration/test_step_builder.py +++ b/problem_builder/tests/integration/test_step_builder.py @@ -542,6 +542,7 @@ def test_mcq_feedback(self, choice_index, choice_text, expected_feedback, correc def review_mcq(self, step_builder, choice_index, expected_feedback, correct): correctness = 'correct' if correct else 'incorrect' mcq_link = step_builder.find_elements_by_css_selector(f'.{correctness}-list li a')[0] + time.sleep(1) # give some time for changes mcq_link.click() mcq = step_builder.find_element_by_css_selector(".xblock-v1[data-name='mcq_1_1']") self.assert_choice_feedback(mcq, choice_index, expected_feedback, correct) From b1669505126fde741115fa84e102eecb79da839f Mon Sep 17 00:00:00 2001 From: Giovanni Cimolin da Silva <giovannicimolin@gmail.com> Date: Tue, 14 Sep 2021 14:13:53 -0300 Subject: [PATCH 19/29] Extra delay --- problem_builder/tests/integration/test_instructor_tool.py | 1 + 1 file changed, 1 insertion(+) diff --git a/problem_builder/tests/integration/test_instructor_tool.py b/problem_builder/tests/integration/test_instructor_tool.py index 21dc8704..31e5f93f 100644 --- a/problem_builder/tests/integration/test_instructor_tool.py +++ b/problem_builder/tests/integration/test_instructor_tool.py @@ -189,6 +189,7 @@ def test_data_export_error(self): delete_button = instructor_tool.find_element_by_class_name('data-export-delete') start_button.click() + time.sleep(1) self.wait_until_hidden(start_button) self.wait_until_hidden(result_block) From e8d453416c31e38cd348ea8861b4ef300be80120 Mon Sep 17 00:00:00 2001 From: Giovanni Cimolin da Silva <giovannicimolin@gmail.com> Date: Tue, 14 Sep 2021 14:16:43 -0300 Subject: [PATCH 20/29] Increase delay --- problem_builder/tests/integration/test_instructor_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/problem_builder/tests/integration/test_instructor_tool.py b/problem_builder/tests/integration/test_instructor_tool.py index 31e5f93f..db9b983c 100644 --- a/problem_builder/tests/integration/test_instructor_tool.py +++ b/problem_builder/tests/integration/test_instructor_tool.py @@ -189,7 +189,7 @@ def test_data_export_error(self): delete_button = instructor_tool.find_element_by_class_name('data-export-delete') start_button.click() - time.sleep(1) + time.sleep(5) self.wait_until_hidden(start_button) self.wait_until_hidden(result_block) From 817181050c5c96e113e16a5a2aabb80050be9bca Mon Sep 17 00:00:00 2001 From: Matjaz Gregoric <mtyaka@gmail.com> Date: Tue, 28 Sep 2021 15:14:16 +0200 Subject: [PATCH 21/29] Add instructions on how to re-run CI builds with VNC support. --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/README.md b/README.md index 2b0cea21..be1d3812 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,54 @@ make test.unit make test.integration ``` +Debugging CI Failures +--------------------- + +Sometimes it can be hard to figure out why some tests fail in the CI. +When Circle CI browser based tests fail for unknown reasons, it can be helpful to run them with VNC enabled +so that you can observe the browser (or even interact with it) while the tests are running. + +To enable VNC on Circle CI, first re-run the failing test with SSH enabled: in the Circle CI UI, +click the "Rerun" dropdown and select "Rerun Job with SSH". The job will be re-run with SSH enabled. +You can find the IP/port combination that lets you log into the VM with your github SSH key under the "Enable SSH" +step in the pipeline UI. + +SSH into the VM, forwarding the VNC port: + +```bash +ssh -p <port> <ip-address> -L 5900:localhost:5900 +``` + +Install the required packages: + +```bash +sudo apt-get install -yq xvfb x11vnc fluxbox +``` + +Start up xvfb and the VNC server: + +```bash +rm -f /tmp/.X$(echo ${DISPLAY:-:0} | cut -b2-)-lock +Xvfb ${DISPLAY:-:0} -ac -listen tcp -screen 0 1440x900x24 & +/usr/bin/fluxbox -display ${DISPLAY:-:0} -screen 0 & +x11vnc -display ${DISPLAY:-:0} -forever -noxdamage -rfbport 5900 -quiet -passwd pass & +``` + +You should now be able to connect to the server via VNC. On macOS, you can use the built-in VNC viewer +that you can launch by opening Finder and choosing the "Go -> Connect to Server.." from the menu. +Type in `localhost:5900` and enter `pass` when asked for the password. + +You are all set up to run integration tests with screen sharing enabled. +For some reason Firefox does not want to start in foreground mode when run as non-root, +so you'll have to run the tests as root. + +```bash +unset MOZ_HEADLESS +cd /home/circleci/project +source venv/bin/activate +make test +``` + Working with Translations ------------------------- From 91ca0ff6cb811f60b4feee966312c75bff1b48ff Mon Sep 17 00:00:00 2001 From: Matjaz Gregoric <mtyaka@gmail.com> Date: Tue, 28 Sep 2021 15:14:34 +0200 Subject: [PATCH 22/29] Adjust timeouts and delays for tests. --- .../tests/integration/test_instructor_tool.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/problem_builder/tests/integration/test_instructor_tool.py b/problem_builder/tests/integration/test_instructor_tool.py index db9b983c..e723affd 100644 --- a/problem_builder/tests/integration/test_instructor_tool.py +++ b/problem_builder/tests/integration/test_instructor_tool.py @@ -12,7 +12,7 @@ class MockTasksModule: """Mock for the tasks module, which can only be meaningfully import in the LMS.""" - def __init__(self, successful=True, display_data=[]): + def __init__(self, successful=True, display_data=[], delay=0): self.export_data = Mock() async_result = self.export_data.async_result async_result.ready.side_effect = [False, False, True, True] @@ -28,8 +28,13 @@ def __init__(self, successful=True, display_data=[]): ) else: async_result.result = 'error' - self.export_data.AsyncResult.return_value = async_result - self.export_data.delay.return_value = async_result + + def produce_result(*args): + time.sleep(delay) + return async_result + + self.export_data.AsyncResult.side_effect = produce_result + self.export_data.delay.side_effect = produce_result class MockInstructorTaskModelsModule: @@ -113,6 +118,7 @@ def test_data_export_delete(self): cancel_button = instructor_tool.find_element_by_class_name('data-export-cancel') delete_button = instructor_tool.find_element_by_class_name('data-export-delete') + self.wait_until_clickable(start_button) start_button.click() self.wait_until_visible(result_block) @@ -135,7 +141,7 @@ def test_data_export_delete(self): 'lms.djangoapps.instructor_task': True, 'lms.djangoapps.instructor_task.models': MockInstructorTaskModelsModule(), }) - @patch.object(InstructorToolBlock, 'user_is_staff', Mock(return_value=True)) + @patch.object(InstructorToolBlock, 'user_is_staff', Mock(return_value=True, delay=1)) def test_data_export_success(self): instructor_tool = self.go_to_view() start_button = instructor_tool.find_element_by_class_name('data-export-start') @@ -146,6 +152,7 @@ def test_data_export_success(self): cancel_button = instructor_tool.find_element_by_class_name('data-export-cancel') delete_button = instructor_tool.find_element_by_class_name('data-export-delete') + self.wait_until_clickable(start_button) start_button.click() self.wait_until_hidden(start_button) @@ -171,7 +178,7 @@ def test_data_export_success(self): self.assertEqual('', status_area.text) @patch.dict('sys.modules', { - 'problem_builder.tasks': MockTasksModule(successful=False), + 'problem_builder.tasks': MockTasksModule(successful=False, delay=1), 'lms': True, 'lms.djangoapps': True, 'lms.djangoapps.instructor_task': True, @@ -188,8 +195,8 @@ def test_data_export_error(self): cancel_button = instructor_tool.find_element_by_class_name('data-export-cancel') delete_button = instructor_tool.find_element_by_class_name('data-export-delete') + self.wait_until_clickable(start_button) start_button.click() - time.sleep(5) self.wait_until_hidden(start_button) self.wait_until_hidden(result_block) @@ -224,6 +231,7 @@ def test_pagination_no_results(self): current_page_info = instructor_tool.find_element_by_id('current-page') total_pages_info = instructor_tool.find_element_by_id('total-pages') + self.wait_until_clickable(start_button) start_button.click() self.wait_until_visible(result_block) @@ -260,6 +268,7 @@ def test_pagination_single_result(self): current_page_info = instructor_tool.find_element_by_id('current-page') total_pages_info = instructor_tool.find_element_by_id('total-pages') + self.wait_until_clickable(start_button) start_button.click() self.wait_until_visible(result_block) @@ -302,6 +311,7 @@ def test_pagination_multiple_results(self): current_page_info = instructor_tool.find_element_by_id('current-page') total_pages_info = instructor_tool.find_element_by_id('total-pages') + self.wait_until_clickable(start_button) start_button.click() self.wait_until_visible(result_block) From f87eaa79bcbb6e0591f658dd56519b0d5627f610 Mon Sep 17 00:00:00 2001 From: Matjaz Gregoric <mtyaka@gmail.com> Date: Wed, 29 Sep 2021 11:07:56 +0200 Subject: [PATCH 23/29] Fix workspace path. --- circle.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/circle.yml b/circle.yml index e240f330..4cddeafa 100644 --- a/circle.yml +++ b/circle.yml @@ -20,7 +20,7 @@ jobs: parallelism: <<parameters.parallel>> environment: MOZ_HEADLESS: 1 - WORKBENCH_DATABASES: '{"default": {"ENGINE": "django.db.backends.mysql", "NAME": "db", "USER": "root", "PASSWORD": "rootpw", "HOST": "127.0.0.1", "OPTIONS": {"charset": "utf8mb4"}}}' + WORKBENCH_DATABASES: '{"default": {"ENGINE": "django.db.backends.mysql", "NAME": "db", "USER": "root", "PASSWORD": "rootpw", "HOST": "127.0.0.1", "OPTIONS": {"charset": "utf8mb4"}}}' steps: - checkout - browser-tools/install-firefox @@ -30,7 +30,7 @@ jobs: command: git submodule sync - run: name: Update submodules - command: git submodule update --init + command: git submodule update --init - run: name: Run tests command: | @@ -71,7 +71,7 @@ jobs: - run: command: | source venv/bin/activate - coverage combine /tmp/coverage/$CIRCLE_SHA1/ + coverage combine /tmp/workspace/coverage/$CIRCLE_SHA1/ coverage report deploy: From 9dc0e6a37fcbcde66846ecf6cffceff9ecc9353d Mon Sep 17 00:00:00 2001 From: Matjaz Gregoric <mtyaka@gmail.com> Date: Wed, 29 Sep 2021 12:17:38 +0200 Subject: [PATCH 24/29] Remove coverage reports, they're not working. --- circle.yml | 45 +++------------------------------------------ 1 file changed, 3 insertions(+), 42 deletions(-) diff --git a/circle.yml b/circle.yml index 4cddeafa..7c490c47 100644 --- a/circle.yml +++ b/circle.yml @@ -34,7 +34,6 @@ jobs: - run: name: Run tests command: | - mkdir -p /tmp/coverage/$CIRCLE_SHA1 virtualenv venv source venv/bin/activate mkdir var @@ -43,36 +42,6 @@ jobs: pip -q install -r test_requirements.txt pip install -e . <<parameters.test_command>> - if [ -e .coverage* ]; then - cp .coverage* /tmp/coverage/$CIRCLE_SHA1/. - fi - - persist_to_workspace: - root: /tmp - paths: - - coverage - - coverage: - docker: - - image: <<parameters.docker_image>> - parameters: - docker_image: - type: string - steps: - - checkout - - run: - name: Update system and install deps - command: | - sudo apt-get update - virtualenv venv - source venv/bin/activate - pip install 'coverage==5.0.3' - - attach_workspace: - at: /tmp/workspace - - run: - command: | - source venv/bin/activate - coverage combine /tmp/workspace/coverage/$CIRCLE_SHA1/ - coverage report deploy: docker: @@ -147,22 +116,14 @@ workflows: filters: tags: only: /.*/ - - coverage: - name: py38-coverage - docker_image: cimg/python:3.8 - filters: - tags: - only: /.*/ - requires: - - py38-quality - - py38-unit - - py38-integration - deploy: name: py38-deploy-bdist_wheel docker_image: cimg/python:3.8 dist_type: bdist_wheel requires: - - py38-coverage + - py38-quality + - py38-unit + - py38-integration filters: tags: only: /v[0-9]+(\.[0-9]+)*/ From 95bead54534c4d68a4dcfc5c2620d452ee537332 Mon Sep 17 00:00:00 2001 From: Matjaz Gregoric <mtyaka@gmail.com> Date: Thu, 30 Sep 2021 08:19:50 +0200 Subject: [PATCH 25/29] Update requirements for django 3.2. --- requirements-dev.txt | 2 +- test_requirements.in | 3 +- test_requirements.txt | 108 ++++++++++++++---------------------------- 3 files changed, 38 insertions(+), 75 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 2381d4a5..caa416a1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ # Internationalization and Localization requirements -e xblock-sdk -Django<3 +Django<4 django-statici18n transifex-client edx-i18n-tools diff --git a/test_requirements.in b/test_requirements.in index 13fb0ca9..f292fb66 100644 --- a/test_requirements.in +++ b/test_requirements.in @@ -7,11 +7,10 @@ mock selenium<4 webob XBlock -Django<3 +Django<4 pytest pylint pycodestyle django-mysql mysqlclient --r xblock-sdk/requirements/base.txt -r xblock-sdk/requirements/test.txt diff --git a/test_requirements.txt b/test_requirements.txt index 182aed3f..4fbd9465 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile +# This file is autogenerated by pip-compile with python 3.8 # To update, run: # # pip-compile --output-file=test_requirements.txt test_requirements.in @@ -10,16 +10,16 @@ acid-xblock==0.2.1 # via -r xblock-sdk/requirements/test.txt appdirs==1.4.4 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # fs # virtualenv arrow==1.1.0 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # jinja2-time -astroid==2.7.3 +asgiref==3.4.1 + # via django +astroid==2.8.0 # via pylint attrs==21.2.0 # via @@ -27,48 +27,38 @@ attrs==21.2.0 # pytest binaryornot==0.4.4 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # cookiecutter bok_choy==0.7.1 # via # -r test_requirements.in # -r xblock-sdk/requirements/test.txt +boto==2.49.0 + # via -r xblock-sdk/requirements/test.txt boto3==1.17.78 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # fs-s3fs -boto==2.49.0 - # via - # -r xblock-sdk/requirements/base.txt - # -r xblock-sdk/requirements/test.txt botocore==1.20.78 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # boto3 # s3transfer certifi==2020.12.5 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # requests chardet==4.0.0 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # binaryornot # requests click==8.0.1 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # cookiecutter cookiecutter==1.7.3 - # via - # -r xblock-sdk/requirements/base.txt - # -r xblock-sdk/requirements/test.txt + # via -r xblock-sdk/requirements/test.txt coverage[toml]==5.5 # via # -r xblock-sdk/requirements/test.txt @@ -81,39 +71,33 @@ distlib==0.3.1 # via # -r xblock-sdk/requirements/test.txt # virtualenv -django-mysql==4.0.0 - # via -r test_requirements.in -django-pyfs==3.0 - # via - # -r xblock-sdk/requirements/base.txt - # -r xblock-sdk/requirements/test.txt -django==2.2.23 +django==3.2.7 # via # -r test_requirements.in - # -r xblock-sdk/requirements/base.txt # django-mysql # django-pyfs # xblock-sdk +django-mysql==4.1.0 + # via -r test_requirements.in +django-pyfs==3.0 + # via -r xblock-sdk/requirements/test.txt filelock==3.0.12 # via # -r xblock-sdk/requirements/test.txt # tox # virtualenv -fs-s3fs==1.1.1 - # via - # -r xblock-sdk/requirements/base.txt - # -r xblock-sdk/requirements/test.txt - # django-pyfs fs==2.4.13 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # django-pyfs # fs-s3fs # xblock +fs-s3fs==1.1.1 + # via + # -r xblock-sdk/requirements/test.txt + # django-pyfs idna==2.10 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # requests iniconfig==1.1.1 @@ -122,36 +106,31 @@ iniconfig==1.1.1 # pytest isort==5.9.3 # via pylint -jinja2-time==0.2.0 +jinja2==3.0.1 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # cookiecutter -jinja2==3.0.1 + # jinja2-time +jinja2-time==0.2.0 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # cookiecutter - # jinja2-time jmespath==0.10.0 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # boto3 # botocore -lazy-object-proxy==1.6.0 - # via astroid lazy==1.4 # via # -r test_requirements.in - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # acid-xblock # bok-choy +lazy-object-proxy==1.6.0 + # via astroid lxml==4.6.3 # via # -r test_requirements.in - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # xblock mako==1.1.4 @@ -160,7 +139,6 @@ mako==1.1.4 # acid-xblock markupsafe==2.0.1 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # jinja2 # mako @@ -190,7 +168,7 @@ pillow==8.2.0 # via # -r xblock-sdk/requirements/test.txt # needle -platformdirs==2.3.0 +platformdirs==2.4.0 # via pylint pluggy==0.13.1 # via @@ -199,7 +177,6 @@ pluggy==0.13.1 # tox poyo==0.5.0 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # cookiecutter py==1.10.0 @@ -209,21 +186,13 @@ py==1.10.0 # tox pycodestyle==2.7.0 # via -r test_requirements.in -pylint==2.10.2 +pylint==2.11.1 # via -r test_requirements.in pyparsing==2.4.7 # via # -r xblock-sdk/requirements/test.txt # packaging pypng==0.0.20 - # via - # -r xblock-sdk/requirements/base.txt - # -r xblock-sdk/requirements/test.txt -pytest-cov==2.12.0 - # via -r xblock-sdk/requirements/test.txt -pytest-django==4.3.0 - # via -r xblock-sdk/requirements/test.txt -pytest-rerunfailures==9.1.1 # via -r xblock-sdk/requirements/test.txt pytest==6.2.4 # via @@ -232,38 +201,38 @@ pytest==6.2.4 # pytest-cov # pytest-django # pytest-rerunfailures +pytest-cov==2.12.0 + # via -r xblock-sdk/requirements/test.txt +pytest-django==4.3.0 + # via -r xblock-sdk/requirements/test.txt +pytest-rerunfailures==9.1.1 + # via -r xblock-sdk/requirements/test.txt python-dateutil==2.8.1 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # arrow # botocore # xblock python-slugify==5.0.2 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # cookiecutter pytz==2021.1 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # django # fs # xblock pyyaml==5.4.1 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # xblock requests==2.25.1 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # cookiecutter s3transfer==0.4.2 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # boto3 selenium==3.4.1 @@ -273,12 +242,9 @@ selenium==3.4.1 # bok-choy # needle simplejson==3.17.2 - # via - # -r xblock-sdk/requirements/base.txt - # -r xblock-sdk/requirements/test.txt + # via -r xblock-sdk/requirements/test.txt six==1.16.0 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # bok-choy # cookiecutter @@ -289,12 +255,10 @@ six==1.16.0 # virtualenv sqlparse==0.4.1 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # django text-unidecode==1.3 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # python-slugify toml==0.10.2 @@ -304,15 +268,18 @@ toml==0.10.2 # pylint # pytest # tox -tox-battery==0.6.1 - # via -r xblock-sdk/requirements/test.txt tox==3.23.1 # via # -r xblock-sdk/requirements/test.txt # tox-battery +tox-battery==0.6.1 + # via -r xblock-sdk/requirements/test.txt +typing-extensions==3.10.0.2 + # via + # astroid + # pylint urllib3==1.26.4 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # botocore # requests @@ -322,13 +289,11 @@ virtualenv==20.4.6 # tox web-fragments==1.0.0 # via - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # xblock webob==1.8.7 # via # -r test_requirements.in - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # xblock wrapt==1.12.1 @@ -336,7 +301,6 @@ wrapt==1.12.1 xblock==1.4.1 # via # -r test_requirements.in - # -r xblock-sdk/requirements/base.txt # -r xblock-sdk/requirements/test.txt # acid-xblock From b8ddbed9034be11ca72b14e5d7285b566fb52d72 Mon Sep 17 00:00:00 2001 From: Matjaz Gregoric <mtyaka@gmail.com> Date: Thu, 30 Sep 2021 08:41:31 +0200 Subject: [PATCH 26/29] Update language codes. --- problem_builder/translations/config.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/problem_builder/translations/config.yaml b/problem_builder/translations/config.yaml index b6e373b9..2f1002ca 100644 --- a/problem_builder/translations/config.yaml +++ b/problem_builder/translations/config.yaml @@ -3,15 +3,15 @@ locales: - en # English - Source Language - ar # Arabic - - es_419 # Spanish - - pt_BR # Portuguese - - zh_CN # Chinese + - es-419 # Spanish + - pt-br # Portuguese + - zh-cn # Chinese - fr # French - - fr_CA # French (Canada) - - de_DE # German - - ja_JP # Japanese - - pl_PL # Polish - - ko_KR #Korean + - fr-ca # French (Canada) + - de-de # German + - ja-jp # Japanese + - pl-pl # Polish + - ko-kr #Korean # Add languages here as needed # The locales used for fake-accented English, for testing. From 301d75bad485de27619fdd310c1e06904ae76a48 Mon Sep 17 00:00:00 2001 From: Matjaz Gregoric <mtyaka@gmail.com> Date: Thu, 30 Sep 2021 08:50:43 +0200 Subject: [PATCH 27/29] Remove deprecated usage of xblock.fragments.Fragment. --- problem_builder/answer.py | 2 +- problem_builder/choice.py | 2 +- problem_builder/completion.py | 2 +- problem_builder/dashboard.py | 2 +- problem_builder/instructor_tool.py | 2 +- problem_builder/mcq.py | 2 +- problem_builder/mentoring.py | 6 +++--- problem_builder/message.py | 2 +- problem_builder/mixins.py | 2 +- problem_builder/plot.py | 4 ++-- problem_builder/questionnaire.py | 2 +- problem_builder/slider.py | 2 +- problem_builder/step.py | 4 ++-- problem_builder/step_review.py | 4 ++-- problem_builder/swipe.py | 2 +- problem_builder/table.py | 6 +++--- problem_builder/tip.py | 2 +- 17 files changed, 24 insertions(+), 24 deletions(-) diff --git a/problem_builder/answer.py b/problem_builder/answer.py index 615ebafe..8c697e3e 100644 --- a/problem_builder/answer.py +++ b/problem_builder/answer.py @@ -24,9 +24,9 @@ import pkg_resources from django import utils from lazy import lazy +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Integer, Scope, String -from xblock.fragment import Fragment from xblock.validation import ValidationMessage from xblockutils.resources import ResourceLoader from xblockutils.studio_editable import (StudioEditableXBlockMixin, diff --git a/problem_builder/choice.py b/problem_builder/choice.py index b296e1e0..b44fe6b4 100644 --- a/problem_builder/choice.py +++ b/problem_builder/choice.py @@ -22,9 +22,9 @@ import uuid from lxml import etree +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Scope, String -from xblock.fragment import Fragment from xblock.validation import ValidationMessage from xblockutils.studio_editable import (StudioEditableXBlockMixin, XBlockWithPreviewMixin) diff --git a/problem_builder/completion.py b/problem_builder/completion.py index 43f9f5ce..ea072324 100644 --- a/problem_builder/completion.py +++ b/problem_builder/completion.py @@ -21,9 +21,9 @@ import logging +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import UNSET, JSONField, Scope, String -from xblock.fragment import Fragment from xblockutils.resources import ResourceLoader from xblockutils.studio_editable import StudioEditableXBlockMixin diff --git a/problem_builder/dashboard.py b/problem_builder/dashboard.py index 3b1bbae6..4d865c95 100644 --- a/problem_builder/dashboard.py +++ b/problem_builder/dashboard.py @@ -32,9 +32,9 @@ from django.template.defaultfilters import floatformat from lazy import lazy +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Boolean, Dict, List, Scope, String -from xblock.fragment import Fragment from xblock.validation import ValidationMessage from xblockutils.helpers import child_isinstance from xblockutils.resources import ResourceLoader diff --git a/problem_builder/instructor_tool.py b/problem_builder/instructor_tool.py index e53c3ccd..fe239090 100644 --- a/problem_builder/instructor_tool.py +++ b/problem_builder/instructor_tool.py @@ -24,10 +24,10 @@ import json from django.core.paginator import Paginator +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.exceptions import JsonHandlerError from xblock.fields import Dict, List, Scope, String -from xblock.fragment import Fragment from xblockutils.resources import ResourceLoader from .mixins import TranslationContentMixin, XBlockWithTranslationServiceMixin diff --git a/problem_builder/mcq.py b/problem_builder/mcq.py index ed83cccb..4e782633 100644 --- a/problem_builder/mcq.py +++ b/problem_builder/mcq.py @@ -21,8 +21,8 @@ import logging +from web_fragments.fragment import Fragment from xblock.fields import List, Scope, String -from xblock.fragment import Fragment from xblock.validation import ValidationMessage from xblockutils.resources import ResourceLoader diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 0d3eabd0..ccfa9649 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -26,10 +26,10 @@ from itertools import chain from lazy.lazy import lazy +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.exceptions import JsonHandlerError, NoSuchViewError from xblock.fields import Boolean, Float, Integer, List, Scope, String -from xblock.fragment import Fragment from xblock.validation import ValidationMessage from xblockutils.helpers import child_isinstance from xblockutils.resources import ResourceLoader @@ -471,7 +471,7 @@ def student_view(self, context): child_fragment = Fragment(child.data) else: child_fragment = child.render('student_view', context) - fragment.add_frag_resources(child_fragment) + fragment.add_fragment_resources(child_fragment) child_content += child_fragment.content fragment.add_content(loader.render_django_template('templates/html/mentoring.html', { @@ -982,7 +982,7 @@ def student_view(self, context): child_content = "<p>[{}]</p>".format(self._("Error: Unable to load child component.")) else: child_fragment = self._render_child_fragment(child, context, view='mentoring_view') - fragment.add_frag_resources(child_fragment) + fragment.add_fragment_resources(child_fragment) child_content = child_fragment.content children_contents.append(child_content) diff --git a/problem_builder/message.py b/problem_builder/message.py index 6578422d..aebc8ffc 100644 --- a/problem_builder/message.py +++ b/problem_builder/message.py @@ -20,9 +20,9 @@ # Imports ########################################################### from lxml import etree +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Scope, String -from xblock.fragment import Fragment from xblockutils.studio_editable import StudioEditableXBlockMixin from problem_builder.mixins import XBlockWithTranslationServiceMixin diff --git a/problem_builder/mixins.py b/problem_builder/mixins.py index bd38f784..ebed43e0 100644 --- a/problem_builder/mixins.py +++ b/problem_builder/mixins.py @@ -4,9 +4,9 @@ import webob from django import utils from lazy import lazy +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import UNIQUE_ID, Boolean, Float, Scope, String -from xblock.fragment import Fragment from xblockutils.helpers import child_isinstance from xblockutils.resources import ResourceLoader diff --git a/problem_builder/plot.py b/problem_builder/plot.py index f41c87cd..e3ec354d 100644 --- a/problem_builder/plot.py +++ b/problem_builder/plot.py @@ -21,9 +21,9 @@ import logging from lazy.lazy import lazy +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Scope, String -from xblock.fragment import Fragment from xblock.validation import ValidationMessage from xblockutils.helpers import child_isinstance from xblockutils.resources import ResourceLoader @@ -334,7 +334,7 @@ def author_preview_view(self, context): )) for overlay in self.overlays: overlay_fragment = self._render_child_fragment(overlay, context, view='mentoring_view') - fragment.add_frag_resources(overlay_fragment) + fragment.add_fragment_resources(overlay_fragment) fragment.add_content(overlay_fragment.content) return fragment diff --git a/problem_builder/questionnaire.py b/problem_builder/questionnaire.py index 44286a9f..e5a441b2 100644 --- a/problem_builder/questionnaire.py +++ b/problem_builder/questionnaire.py @@ -25,9 +25,9 @@ from django import utils from django.utils.safestring import mark_safe from lazy import lazy +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Scope, String -from xblock.fragment import Fragment from xblock.validation import ValidationMessage from xblockutils.helpers import child_isinstance from xblockutils.resources import ResourceLoader diff --git a/problem_builder/slider.py b/problem_builder/slider.py index 031da11c..59f8d0f3 100644 --- a/problem_builder/slider.py +++ b/problem_builder/slider.py @@ -22,9 +22,9 @@ import logging import uuid +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Float, Scope, String -from xblock.fragment import Fragment from xblockutils.resources import ResourceLoader from xblockutils.studio_editable import StudioEditableXBlockMixin diff --git a/problem_builder/step.py b/problem_builder/step.py index 4dde1c47..693587d0 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -20,9 +20,9 @@ import logging from lazy.lazy import lazy +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import List, Scope, String -from xblock.fragment import Fragment from xblockutils.resources import ResourceLoader from xblockutils.studio_editable import (NestedXBlockSpec, StudioContainerWithNestedXBlocksMixin, @@ -264,7 +264,7 @@ def _render_view(self, context, view): child_contents.append(f"<p>{child.display_name}</p>") else: child_fragment = self._render_child_fragment(child, context, view) - fragment.add_frag_resources(child_fragment) + fragment.add_fragment_resources(child_fragment) child_contents.append(child_fragment.content) fragment.add_content(loader.render_django_template('templates/html/step.html', { diff --git a/problem_builder/step_review.py b/problem_builder/step_review.py index c46b3977..088e4b2b 100644 --- a/problem_builder/step_review.py +++ b/problem_builder/step_review.py @@ -19,9 +19,9 @@ import logging +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Scope, String -from xblock.fragment import Fragment from xblockutils.resources import ResourceLoader from xblockutils.studio_editable import (NestedXBlockSpec, StudioContainerWithNestedXBlocksMixin, @@ -301,7 +301,7 @@ def student_view(self, context=None): # that Studio doesn't wrap with with unwanted controls and the XBlock SDK # workbench doesn't add the acid-aside to the fragment. child_fragment = self._render_child_fragment(child, context, view="embedded_student_view") - fragment.add_frag_resources(child_fragment) + fragment.add_fragment_resources(child_fragment) fragment.add_content(child_fragment.content) return fragment diff --git a/problem_builder/swipe.py b/problem_builder/swipe.py index d3d49b0e..a905f079 100644 --- a/problem_builder/swipe.py +++ b/problem_builder/swipe.py @@ -21,9 +21,9 @@ import logging +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Boolean, Scope, String -from xblock.fragment import Fragment from xblockutils.resources import ResourceLoader from xblockutils.studio_editable import (StudioEditableXBlockMixin, XBlockWithPreviewMixin) diff --git a/problem_builder/table.py b/problem_builder/table.py index 958d0a2d..3343c183 100644 --- a/problem_builder/table.py +++ b/problem_builder/table.py @@ -23,10 +23,10 @@ import json from django.contrib.auth.models import User +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.exceptions import JsonHandlerError from xblock.fields import Boolean, Scope, String -from xblock.fragment import Fragment from xblockutils.resources import ResourceLoader from xblockutils.studio_editable import (StudioContainerXBlockMixin, StudioEditableXBlockMixin, @@ -235,7 +235,7 @@ def student_view(self, context): child = self.runtime.get_block(child_id) # Child should be an instance of MentoringTableColumn child_frag = child.render('mentoring_view', context) - fragment.add_frag_resources(child_frag) + fragment.add_fragment_resources(child_frag) context['allow_sharing'] = self.allow_sharing context['allow_download'] = self.allow_download @@ -327,7 +327,7 @@ def mentoring_view(self, context=None): else: child_frag = child.render('mentoring_view', context) fragment.add_content(child_frag.content) - fragment.add_frag_resources(child_frag) + fragment.add_fragment_resources(child_frag) return fragment def author_preview_view(self, context): diff --git a/problem_builder/tip.py b/problem_builder/tip.py index f86545fa..18abdecc 100644 --- a/problem_builder/tip.py +++ b/problem_builder/tip.py @@ -22,9 +22,9 @@ from django.utils.html import strip_tags from lxml import etree +from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import List, Scope, String -from xblock.fragment import Fragment from xblock.validation import ValidationMessage from xblockutils.resources import ResourceLoader from xblockutils.studio_editable import StudioEditableXBlockMixin From 59f5a1802234e5a33f7263ccb9b95d3c069d50b3 Mon Sep 17 00:00:00 2001 From: Matjaz Gregoric <mtyaka@gmail.com> Date: Thu, 30 Sep 2021 10:13:29 +0200 Subject: [PATCH 28/29] Fix quality violations. --- problem_builder/answer.py | 8 ++++---- problem_builder/dashboard.py | 2 +- problem_builder/mentoring.py | 7 ++++--- problem_builder/message.py | 5 +---- problem_builder/mixins.py | 4 ++-- problem_builder/plot.py | 5 ++--- problem_builder/questionnaire.py | 5 ++--- problem_builder/step.py | 5 +++-- problem_builder/step_review.py | 6 ++---- problem_builder/swipe.py | 9 ++------- problem_builder/tasks.py | 3 ++- problem_builder/tests/integration/base_test.py | 2 +- .../tests/integration/test_progression.py | 2 +- .../tests/integration/test_step_builder.py | 16 ++++++++-------- problem_builder/v1/upgrade.py | 9 +++++---- problem_builder/v1/xml_changes.py | 2 +- 16 files changed, 41 insertions(+), 49 deletions(-) diff --git a/problem_builder/answer.py b/problem_builder/answer.py index 8c697e3e..5e4e3eb8 100644 --- a/problem_builder/answer.py +++ b/problem_builder/answer.py @@ -186,10 +186,9 @@ def resource_string(path): return data.decode("utf8") def get_translation_content(self): + lang = utils.translation.to_locale(utils.translation.get_language()) try: - return self.resource_string('public/js/translations/{lang}/textjs.js'.format( - lang=utils.translation.to_locale(utils.translation.get_language()), - )) + return self.resource_string(f'public/js/translations/{lang}/textjs.js') except OSError: return self.resource_string('public/js/translations/en/textjs.js') @@ -243,7 +242,8 @@ def submit(self, submission): item_key['item_id'] = self.name sub_api.create_submission(item_key, self.student_input) - log.info('Answer submitted for`{}`: "{}"'.format(self.name, self.student_input)) + log_message = f'Answer submitted for`{self.name}`: "{self.student_input}"' + log.info(log_message) return self.get_results() @property diff --git a/problem_builder/dashboard.py b/problem_builder/dashboard.py index 4d865c95..0fd4fb60 100644 --- a/problem_builder/dashboard.py +++ b/problem_builder/dashboard.py @@ -387,7 +387,7 @@ def student_view(self, context=None): # pylint: disable=unused-argument Standard view of this XBlock. """ if not self.mentoring_ids: - return Fragment("<h1>{}</h1><p>{}</p>".format(self.display_name, _("Not configured."))) + return Fragment(f'<h1>{self.display_name}</h1><p>{_("Not configured.")}</p>') blocks = [] for mentoring_block in self.get_mentoring_blocks(self.mentoring_ids): diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index ccfa9649..9e5e3a88 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -456,7 +456,7 @@ def student_view(self, context): for child_id in self.children: child = self.runtime.get_block(child_id) if child is None: # child should not be None but it can happen due to bugs or permission issues - child_content += "<p>[{}]</p>".format(self._("Error: Unable to load child component.")) + child_content += f'<p>[{self._("Error: Unable to load child component.")}]</p>' elif not isinstance(child, MentoringMessageBlock): try: if mcq_hide_previous_answer and isinstance(child, QuestionnaireAbstractBlock): @@ -602,7 +602,8 @@ def _get_standard_results(self): @XBlock.json_handler def submit(self, submissions, suffix=''): - log.info('Received submissions: {}'.format(submissions)) + log_message = f'Received submissions: {submissions}' + log.info(log_message) # server-side check that the user is allowed to submit: if self.max_attempts_reached: raise JsonHandlerError(403, "Maximum number of attempts already reached.") @@ -979,7 +980,7 @@ def student_view(self, context): for child_id in self.children: child = self.runtime.get_block(child_id) if child is None: # child should not be None but it can happen due to bugs or permission issues - child_content = "<p>[{}]</p>".format(self._("Error: Unable to load child component.")) + child_content = f'<p>[{self._("Error: Unable to load child component.")}]</p>' else: child_fragment = self._render_child_fragment(child, context, view='mentoring_view') fragment.add_fragment_resources(child_fragment) diff --git a/problem_builder/message.py b/problem_builder/message.py index aebc8ffc..5f7dfcd6 100644 --- a/problem_builder/message.py +++ b/problem_builder/message.py @@ -113,10 +113,7 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin, XBlockWithTransla def mentoring_view(self, context=None): """ Render this message for use by a mentoring block. """ - html = '<div class="submission-message {msg_type}">{content}</div>'.format( - msg_type=self.type, - content=self.content - ) + html = f'<div class="submission-message {self.type}">{self.content}</div>' return Fragment(html) def student_view(self, context=None): diff --git a/problem_builder/mixins.py b/problem_builder/mixins.py index ebed43e0..935913fa 100644 --- a/problem_builder/mixins.py +++ b/problem_builder/mixins.py @@ -122,7 +122,7 @@ def get_message_content(self, message_type, or_default=False): if or_default: # Return the default value since no custom message is set. # Note the WYSIWYG editor usually wraps the .content HTML in a <p> tag so we do the same here. - return '<p>{}</p>'.format(MentoringMessageBlock.MESSAGE_TYPES[message_type]['default']) + return f'<p>{MentoringMessageBlock.MESSAGE_TYPES[message_type]["default"]}</p>' class QuestionMixin(EnumerableChildMixin): @@ -176,7 +176,7 @@ class NoSettingsMixin: def studio_view(self, _context=None): """ Studio View """ - return Fragment('<p>{}</p>'.format(self._("This XBlock does not have any settings."))) + return Fragment(f'<p>{self._("This XBlock does not have any settings.")}</p>') class StudentViewUserStateMixin: diff --git a/problem_builder/plot.py b/problem_builder/plot.py index e3ec354d..728b0a43 100644 --- a/problem_builder/plot.py +++ b/problem_builder/plot.py @@ -328,10 +328,9 @@ def author_preview_view(self, context): fragment.add_content(loader.render_django_template('templates/html/plot_preview.html', context)) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/plot-preview.css')) if self.overlay_ids: + message = _("In addition to the default and average overlays the plot includes the following overlays:") fragment.add_content( - "<p>{}</p>".format( - _("In addition to the default and average overlays the plot includes the following overlays:") - )) + f'<p>{message}</p>') for overlay in self.overlays: overlay_fragment = self._render_child_fragment(overlay, context, view='mentoring_view') fragment.add_fragment_resources(overlay_fragment) diff --git a/problem_builder/questionnaire.py b/problem_builder/questionnaire.py index e5a441b2..befd5bbb 100644 --- a/problem_builder/questionnaire.py +++ b/problem_builder/questionnaire.py @@ -93,10 +93,9 @@ def resource_string(path): return data.decode("utf8") def get_translation_content(self): + lang = utils.translation.to_locale(utils.translation.get_language()) try: - return self.resource_string('public/js/translations/{lang}/textjs.js'.format( - lang=utils.translation.to_locale(utils.translation.get_language()), - )) + return self.resource_string(f'public/js/translations/{lang}/textjs.js') except OSError: return self.resource_string('public/js/translations/en/textjs.js') diff --git a/problem_builder/step.py b/problem_builder/step.py index 693587d0..4ab493d4 100644 --- a/problem_builder/step.py +++ b/problem_builder/step.py @@ -169,7 +169,8 @@ def has_question(self): def submit(self, submissions): """ Handle a student submission. This is called by the parent XBlock. """ - log.info('Received submissions: {}'.format(submissions)) + log_message = f'Received submissions: {submissions}' + log.info(log_message) # Submit child blocks (questions) and gather results submit_results = [] @@ -252,7 +253,7 @@ def _render_view(self, context, view): for child_id in self.children: child = self.runtime.get_block(child_id) if child is None: # child should not be None but it can happen due to bugs or permission issues - child_contents.append("<p>[{}]</p>".format(self._("Error: Unable to load child component."))) + child_contents.append(f'<p>[{self._("Error: Unable to load child component.")}]</p>') else: if rendering_for_studio and isinstance(child, PlotBlock): # Don't use view to render plot blocks in Studio. diff --git a/problem_builder/step_review.py b/problem_builder/step_review.py index 088e4b2b..9bb1afc4 100644 --- a/problem_builder/step_review.py +++ b/problem_builder/step_review.py @@ -121,9 +121,7 @@ def student_view_data(self, context=None): def student_view(self, _context=None): """ Render this message. """ - html = '<div class="review-conditional-message">{content}</div>'.format( - content=self.content - ) + html = f'<div class="review-conditional-message">{self.content}</div>' return Fragment(html) embedded_student_view = student_view @@ -292,7 +290,7 @@ def student_view(self, context=None): for child_id in self.children: child = self.runtime.get_block(child_id) if child is None: # child should not be None but it can happen due to bugs or permission issues - fragment.add_content("<p>[{}]</p>".format("Error: Unable to load child component.")) + fragment.add_content('<p>[Error: Unable to load child component.]</p>') else: if hasattr(child, 'is_applicable'): if not child.is_applicable(context): diff --git a/problem_builder/swipe.py b/problem_builder/swipe.py index a905f079..a7bb785a 100644 --- a/problem_builder/swipe.py +++ b/problem_builder/swipe.py @@ -157,13 +157,8 @@ def expand_static_url(self, url): def mentoring_view(self, context=None): """ Render the swipe image, text & whether it's correct within a mentoring block question. """ return Fragment( - ( - '<img src="{img_url}" style="max-width: 100%;" />' - '<p class="swipe-text">"{text}"</p>' - ).format( - img_url=self.expand_static_url(self.img_url), - text=self.text, - ) + f'<img src="{self.expand_static_url(self.img_url)}" style="max-width: 100%;" />' + f'<p class="swipe-text">"{self.text}"</p>' ) def student_view(self, context=None): diff --git a/problem_builder/tasks.py b/problem_builder/tasks.py index e703dc9d..d2953cd8 100644 --- a/problem_builder/tasks.py +++ b/problem_builder/tasks.py @@ -80,7 +80,8 @@ def scan_for_blocks(block): rows += results # Generate the CSV: - filename = "pb-data-export-{}.csv".format(time.strftime("%Y-%m-%d-%H%M%S", time.gmtime(start_timestamp))) + timestamp = time.strftime("%Y-%m-%d-%H%M%S", time.gmtime(start_timestamp)) + filename = f"pb-data-export-{timestamp}.csv" report_store = ReportStore.from_config(config_name='GRADES_DOWNLOAD') report_store.store_rows(course_key, filename, rows) diff --git a/problem_builder/tests/integration/base_test.py b/problem_builder/tests/integration/base_test.py index c6dfaa6b..d4cccecd 100644 --- a/problem_builder/tests/integration/base_test.py +++ b/problem_builder/tests/integration/base_test.py @@ -178,7 +178,7 @@ class MentoringAssessmentBaseTest(ProblemBuilderBaseTest): @staticmethod def question_text(number): if number: - return "Question %s" % number + return f"Question {number}" else: return "Question" diff --git a/problem_builder/tests/integration/test_progression.py b/problem_builder/tests/integration/test_progression.py index 2a6c9eff..a653a859 100644 --- a/problem_builder/tests/integration/test_progression.py +++ b/problem_builder/tests/integration/test_progression.py @@ -34,7 +34,7 @@ def assert_warning(self, warning_dom, link_href): warning_link = warning_dom.find_element_by_xpath('./*') self.assertTrue( warning_link.get_attribute('href').endswith(link_href), - "expected link href '{}' to end with '{}'".format(warning_link.get_attribute('href'), link_href) + f"expected link href '{warning_link.get_attribute('href')}' to end with '{link_href}'" ) def assert_warning_is_hidden(self, mentoring): diff --git a/problem_builder/tests/integration/test_step_builder.py b/problem_builder/tests/integration/test_step_builder.py index b397d15b..63d3ebe1 100644 --- a/problem_builder/tests/integration/test_step_builder.py +++ b/problem_builder/tests/integration/test_step_builder.py @@ -218,23 +218,23 @@ def assert_review_conditional_messages_equal(self, step_builder, messages_expect self.assertListEqual([msg.text for msg in messages], messages_expected) def peek_at_review(self, step_builder, controls, expected, extended_feedback=False): - self.wait_until_text_in("You scored {percentage}% on this assessment.".format(**expected), step_builder) + self.wait_until_text_in(f"You scored {expected['percentage']}% on this assessment.", step_builder) # Check grade breakdown if expected["correct"] == 1: - self.assertIn("You answered 1 question correctly.".format(**expected), step_builder.text) + self.assertIn("You answered 1 question correctly.", step_builder.text) else: - self.assertIn("You answered {correct} questions correctly.".format(**expected), step_builder.text) + self.assertIn(f"You answered {expected['correct']} questions correctly.", step_builder.text) if expected["partial"] == 1: self.assertIn("You answered 1 question partially correctly.", step_builder.text) else: - self.assertIn("You answered {partial} questions partially correctly.".format(**expected), step_builder.text) + self.assertIn(f"You answered {expected['partial']} questions partially correctly.", step_builder.text) if expected["incorrect"] == 1: self.assertIn("You answered 1 question incorrectly.", step_builder.text) else: - self.assertIn("You answered {incorrect} questions incorrectly.".format(**expected), step_builder.text) + self.assertIn(f"You answered {expected['incorrect']} questions incorrectly.", step_builder.text) # Check presence of review links # - If unlimited attempts: no review links, no explanation @@ -262,7 +262,7 @@ def peek_at_review(self, step_builder, controls, expected, extended_feedback=Fal elif expected["num_attempts"] == expected["max_attempts"]: if extended_feedback: for correctness in ['correct', 'incorrect', 'partial']: - review_items = step_builder.find_elements_by_css_selector('.%s-list li' % correctness) + review_items = step_builder.find_elements_by_css_selector(f'.{correctness}-list li') self.assertEqual(len(review_items), expected[correctness]) self.assertTrue(review_links_explained) else: @@ -271,12 +271,12 @@ def peek_at_review(self, step_builder, controls, expected, extended_feedback=Fal # Check if info about number of attempts used is correct if expected["max_attempts"] == 1: - self.assertIn("You have used {num_attempts} of 1 submission.".format(**expected), step_builder.text) + self.assertIn(f"You have used {expected['num_attempts']} of 1 submission.", step_builder.text) elif expected["max_attempts"] == 0: self.assertNotIn("You have used", step_builder.text) else: self.assertIn( - "You have used {num_attempts} of {max_attempts} submissions.".format(**expected), + f"You have used {expected['num_attempts']} of {expected['max_attempts']} submissions.", step_builder.text ) diff --git a/problem_builder/v1/upgrade.py b/problem_builder/v1/upgrade.py index d6eae86d..f94a2ac5 100644 --- a/problem_builder/v1/upgrade.py +++ b/problem_builder/v1/upgrade.py @@ -153,7 +153,7 @@ def parse_xml_for_HtmlDescriptor(cls, node, runtime, keys, id_generator): if sys.argv[2] == "--version=v0": from_version = "v0" else: - sys.exit("invalid second argument: {}".format(' '.join(sys.argv[2:]))) + sys.exit(f"invalid second argument: {' '.join(sys.argv[2:])}") store = modulestore() course = store.get_course(course_id) @@ -187,9 +187,10 @@ def find_mentoring_blocks(block): block = course.runtime.get_block(block_id) if url_name in url_names: print(f" ➔ Mentoring block {url_name} appears in the course in multiple places!") - print(' (display_name: "{}", parent {}: "{}")'.format( - block.display_name, block.parent, block.get_parent().display_name - )) + print( + f' (display_name: "{block.display_name}", parent {block.parent}:' + f' "{block.get_parent().display_name)}")' + ) print(' To fix, you must delete the extra occurences.') stop = True continue diff --git a/problem_builder/v1/xml_changes.py b/problem_builder/v1/xml_changes.py index 915fbe35..71737f9a 100644 --- a/problem_builder/v1/xml_changes.py +++ b/problem_builder/v1/xml_changes.py @@ -90,7 +90,7 @@ def apply(self): old_display_name = p.get("display_name") if old_display_name and old_display_name != title: warnings.warn( - 'Replacing display_name="{}" with <title> value "{}"'.format(p.attrib["display_name"], title) + f'Replacing display_name="{p.attrib["display_name"]}" with <title> value "{title}"' ) p.attrib["display_name"] = title p.remove(self.node) From b755331c7511889430e52e812c7d0c9ea6c9059c Mon Sep 17 00:00:00 2001 From: Matjaz Gregoric <mtyaka@gmail.com> Date: Fri, 1 Oct 2021 11:21:11 +0200 Subject: [PATCH 29/29] Move PB_LANGUAGE_JS_DIRECTORY_MAP to mixins.py. It's not used anywhere else and doesn't have to be configurable. --- problem_builder/mixins.py | 16 ++++++++++++++-- problem_builder/settings.py | 14 -------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/problem_builder/mixins.py b/problem_builder/mixins.py index 935913fa..166b9d07 100644 --- a/problem_builder/mixins.py +++ b/problem_builder/mixins.py @@ -12,10 +12,22 @@ from problem_builder.tests.unit.utils import DateTimeEncoder -from .settings import PB_LANGUAGE_JS_DIRECTORY_MAP - loader = ResourceLoader(__name__) +PB_LANGUAGE_JS_DIRECTORY_MAP = { + 'ar': 'ar', + 'de-de': 'de_DE', + 'en': 'en', + 'es-419': 'es_419', + 'fr': 'fr', + 'fr-ca': 'fr_CA', + 'ja-jp': 'ja_JP', + 'pl': 'pl_PL', + 'pt-br': 'pt_BR', + 'zh-cn': 'zh_CN', + 'ko-kr': 'ko_KR' +} + # Make '_' a no-op so we can scrape strings def _(text): diff --git a/problem_builder/settings.py b/problem_builder/settings.py index 30330fcd..7e0b8407 100644 --- a/problem_builder/settings.py +++ b/problem_builder/settings.py @@ -83,17 +83,3 @@ ) STATICI18N_ROOT = 'problem_builder/public/js' STATICI18N_OUTPUT_DIR = 'translations' - -PB_LANGUAGE_JS_DIRECTORY_MAP = { - 'ar': 'ar', - 'de-de': 'de_DE', - 'en': 'en', - 'es-419': 'es_419', - 'fr': 'fr', - 'fr-ca': 'fr_CA', - 'ja-jp': 'ja_JP', - 'pl': 'pl_PL', - 'pt-br': 'pt_BR', - 'zh-cn': 'zh_CN', - 'ko-kr': 'ko_KR' -}