diff --git a/.github/workflows/build_pex.yml b/.github/workflows/build_pex.yml index 25b052d671a..f7b84a5a524 100644 --- a/.github/workflows/build_pex.yml +++ b/.github/workflows/build_pex.yml @@ -15,13 +15,15 @@ jobs: build_pex: name: Build PEX runs-on: ubuntu-20.04 - container: - image: python:2.7.18-buster outputs: pex-file-name: ${{ steps.get-pex-filename.outputs.pex-file-name }} steps: - uses: actions/checkout@v4 - - uses: actions/cache@v3 + - name: Set up Python 3.6 + uses: actions/setup-python@v5 + with: + python-version: 3.6 + - uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('requirements/*.txt') }} diff --git a/.github/workflows/build_whl.yml b/.github/workflows/build_whl.yml index c8182263389..24f0da163cf 100644 --- a/.github/workflows/build_whl.yml +++ b/.github/workflows/build_whl.yml @@ -14,35 +14,31 @@ jobs: build_whl: name: Build WHL runs-on: ubuntu-20.04 - container: - image: python:2.7.18-buster outputs: whl-file-name: ${{ steps.get-whl-filename.outputs.whl-file-name }} tar-file-name: ${{ steps.get-tar-filename.outputs.tar-file-name }} steps: - - name: Install Git LFS - run: | - curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash - apt-get install git-lfs - uses: actions/checkout@v4 with: fetch-depth: 0 lfs: true - name: Install Ubuntu dependencies run: | - apt-get -y -qq update - apt-get install -y gettext sudo + sudo apt-get -y -qq update + sudo apt-get install -y gettext + - name: Set up Python 3.6 + uses: actions/setup-python@v5 + with: + python-version: 3.6 - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '16.x' - - name: Install Yarn - run: npm install -g yarn + node-version: '18.x' - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Cache Node.js modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} @@ -53,14 +49,14 @@ jobs: yarn --frozen-lockfile npm rebuild node-sass - name: Cache Python dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('requirements/*.txt') }} restore-keys: | ${{ runner.os }}-pip- - name: Cache C extensions - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: cext_cache key: ${{ runner.os }}-cext-${{ hashFiles('requirements/cext*.txt') }} diff --git a/.github/workflows/c_extensions.yml b/.github/workflows/c_extensions.yml index 3becb0dbd71..13eff01917f 100644 --- a/.github/workflows/c_extensions.yml +++ b/.github/workflows/c_extensions.yml @@ -28,15 +28,13 @@ jobs: needs: pre_job if: ${{ needs.pre_job.outputs.should_skip != 'true' }} runs-on: ubuntu-20.04 - container: - image: python:2.7.18-buster - defaults: - run: - shell: bash steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.6 - name: pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-ext-${{ hashFiles('requirements/*.txt') }} @@ -54,13 +52,13 @@ jobs: # in the kolibri directory python -m compileall -q kolibri -x py2only # Until we have staged builds, we will be running this in each and every - # environment even though builds should be done in Py 2.7 + # environment even though builds should be done in Py 3.6 make staticdeps make staticdeps-cext pip install . - # Start and stop kolibri - disabled until we can move out of a container - # kolibri start --port=8081 - # kolibri stop + # Start and stop kolibri + coverage run -p kolibri start --port=8081 + coverage run -p kolibri stop # Run just tests in test/ py.test --cov=kolibri --cov-report= --cov-append --color=no test/ no_c_ext: @@ -68,15 +66,13 @@ jobs: needs: pre_job if: ${{ needs.pre_job.outputs.should_skip != 'true' }} runs-on: ubuntu-20.04 - container: - image: python:2.7.18-buster - defaults: - run: - shell: bash steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.6 - name: pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-no-ext-${{ hashFiles('requirements/*.txt') }} @@ -94,11 +90,11 @@ jobs: # in the kolibri directory python -m compileall -q kolibri -x py2only # Until we have staged builds, we will be running this in each and every - # environment even though builds should be done in Py 2.7 + # environment even though builds should be done in Py 3.6 make staticdeps pip install . - # Start and stop kolibri - disabled until we can move out of a container - # kolibri start --port=8081 - # kolibri stop + # Start and stop kolibri + coverage run -p kolibri start --port=8081 + coverage run -p kolibri stop # Run just tests in test/ py.test --cov=kolibri --cov-report= --cov-append --color=no test/ diff --git a/.github/workflows/check_docs.yml b/.github/workflows/check_docs.yml index 2237298374a..592c2673cac 100644 --- a/.github/workflows/check_docs.yml +++ b/.github/workflows/check_docs.yml @@ -35,7 +35,7 @@ jobs: with: python-version: 3.9 - name: pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-docs-${{ hashFiles('requirements/docs.txt') }} diff --git a/.github/workflows/check_licenses.yml b/.github/workflows/check_licenses.yml index 7f369f75e88..71f26a75058 100644 --- a/.github/workflows/check_licenses.yml +++ b/.github/workflows/check_licenses.yml @@ -33,9 +33,9 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '16.x' + node-version: '18.x' - name: Cache Node.js modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '**/node_modules' key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} diff --git a/.github/workflows/no_zombies.yml b/.github/workflows/no_zombies.yml index 78b1787b41c..ba260032fec 100644 --- a/.github/workflows/no_zombies.yml +++ b/.github/workflows/no_zombies.yml @@ -34,7 +34,7 @@ jobs: with: python-version: '3.11' - name: pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-base-${{ hashFiles('requirements/base.txt') }} diff --git a/.github/workflows/postgres.yml b/.github/workflows/postgres.yml index 846c455ec17..c0f7aa4eeda 100644 --- a/.github/workflows/postgres.yml +++ b/.github/workflows/postgres.yml @@ -42,7 +42,7 @@ jobs: python -m pip install --upgrade pip pip install tox - name: tox env cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ github.workspace }}/.tox/py3.9 key: ${{ runner.os }}-tox-py3.9-${{ hashFiles('requirements/*.txt') }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index f40296f5d3a..aa76529ba21 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -36,12 +36,12 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '16.x' + node-version: '18.x' - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Cache Node.js modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} diff --git a/.github/workflows/python2lint.yml b/.github/workflows/python2lint.yml deleted file mode 100644 index 3cb206ac9c9..00000000000 --- a/.github/workflows/python2lint.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Python 2 linting - -on: - push: - branches: - - develop - - 'release-v**' - pull_request: - branches: - - develop - - 'release-v**' - -jobs: - pre_job: - name: Path match check - runs-on: ubuntu-latest - # Map a step output to a job output - outputs: - should_skip: ${{ steps.skip_check.outputs.should_skip }} - steps: - - id: skip_check - uses: fkirc/skip-duplicate-actions@master - with: - github_token: ${{ github.token }} - paths: '["kolibri/**/*.py"]' - lint: - name: Python 2 syntax checking - needs: pre_job - if: ${{ needs.pre_job.outputs.should_skip != 'true' }} - runs-on: ubuntu-20.04 - container: - image: python:2.7.18-buster - steps: - - uses: actions/checkout@v4 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 - - name: Lint with flake8 - run: | - flake8 kolibri diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index e502bd5d682..bffc6a8a271 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -46,35 +46,13 @@ jobs: pip install "tox<4" - name: tox env cache if: ${{ needs.pre_job.outputs.should_skip != 'true' }} - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ github.workspace }}/.tox/py${{ matrix.python-version }} key: ${{ runner.os }}-tox-py${{ matrix.python-version }}-${{ hashFiles('requirements/*.txt') }} - name: Test with tox if: ${{ needs.pre_job.outputs.should_skip != 'true' }} run: tox -e py${{ matrix.python-version }} - unit_test27: - name: Python unit tests (2.7) - needs: pre_job - runs-on: ubuntu-20.04 - container: - image: python:2.7.18-buster - steps: - - uses: actions/checkout@v4 - - name: Install tox - if: ${{ needs.pre_job.outputs.should_skip != 'true' }} - run: | - python -m pip install --upgrade pip - pip install "tox<4" - - name: tox env cache - if: ${{ needs.pre_job.outputs.should_skip != 'true' }} - uses: actions/cache@v3 - with: - path: ${{ github.workspace }}/.tox/py2.7 - key: ${{ runner.os }}-tox-py2.7-${{ hashFiles('requirements/*.txt') }} - - name: Test with tox - if: ${{ needs.pre_job.outputs.should_skip != 'true' }} - run: tox -e py2.7 postgres: name: Python postgres unit tests needs: pre_job @@ -112,7 +90,7 @@ jobs: pip install "tox<4" - name: tox env cache if: ${{ needs.pre_job.outputs.should_skip != 'true' }} - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ github.workspace }}/.tox/py3.9 key: ${{ runner.os }}-tox-py3.9-${{ hashFiles('requirements/*.txt') }} @@ -142,7 +120,7 @@ jobs: pip install "tox<4" - name: tox env cache if: ${{ needs.pre_job.outputs.should_skip != 'true' }} - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ github.workspace }}/.tox/py${{ matrix.python-version }} key: ${{ runner.os }}-tox-py${{ matrix.python-version }}-${{ hashFiles('requirements/*.txt') }} diff --git a/.github/workflows/yarn.yml b/.github/workflows/yarn.yml index 20123f3823c..8ffcf7d55f0 100644 --- a/.github/workflows/yarn.yml +++ b/.github/workflows/yarn.yml @@ -33,12 +33,12 @@ jobs: - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '16.x' + node-version: '18.x' - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Cache Node.js modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} diff --git a/AUTHORS.md b/AUTHORS.md index b7383333720..bb1ac22f5a8 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -98,4 +98,7 @@ If you have contributed to Kolibri, feel free to add your name and Github accoun | Vikramaditya Singh | Ghat0tkach | | Kris Katkus | katkuskris | | Garvit Singhal | GarvitSinghal47 | +| Adars T S | a6ar55 | +| Shivang Rawat | ShivangRawat30 | +| Alex Vélez | AlexVelezLl | | Mazen Oweiss | moweiss | diff --git a/Makefile b/Makefile index df3aa492e5e..afc0b80b064 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ SHELL := /bin/bash # List most target names as 'PHONY' to prevent Make from thinking it will be creating a file of the same name -.PHONY: help clean clean-assets clean-build clean-pyc clean-docs lint test test-all assets coverage docs release test-namespaced-packages staticdeps staticdeps-cext writeversion setrequirements buildconfig pex i18n-extract-frontend i18n-extract-backend i18n-transfer-context i18n-extract i18n-django-compilemessages i18n-upload i18n-pretranslate i18n-pretranslate-approve-all i18n-download i18n-regenerate-fonts i18n-stats i18n-install-font i18n-download-translations i18n-download-glossary i18n-upload-glossary docker-whl docker-demoserver docker-devserver docker-envlist +.PHONY: help clean clean-assets clean-build clean-pyc clean-docs lint test test-all assets coverage docs release staticdeps staticdeps-cext writeversion setrequirements buildconfig pex i18n-extract-frontend i18n-extract-backend i18n-transfer-context i18n-extract i18n-django-compilemessages i18n-upload i18n-pretranslate i18n-pretranslate-approve-all i18n-download i18n-regenerate-fonts i18n-stats i18n-install-font i18n-download-translations i18n-download-glossary i18n-upload-glossary docker-whl docker-demoserver docker-devserver docker-envlist help: @@ -33,7 +33,6 @@ help: @echo "test: run tests quickly with the default Python" @echo "test-all: run tests on every Python version with Tox" @echo "test-with-postgres: run tests quickly with a temporary postgresql backend" - @echo "test-namespaced-packages: verify that we haven't fetched anything namespaced into kolibri/dist" @echo "coverage: run tests, recording and printing out Python code coverage" @echo "docs: generate developer documentation" @echo "start-foreground-with-postgres: run Kolibri in foreground mode with a temporary postgresql backend" @@ -152,30 +151,21 @@ release: @read __ twine upload -s dist/* -test-namespaced-packages: - # This expression checks that everything in kolibri/dist has an __init__.py - # To prevent namespaced packages from suddenly showing up - # https://github.com/learningequality/kolibri/pull/2972 - ! find kolibri/dist -mindepth 1 -maxdepth 1 -type d -not -name __pycache__ -not -name cext -not -name py2only -not -name *dist-info -exec ls {}/__init__.py \; 2>&1 | grep "No such file" - clean-staticdeps: rm -rf kolibri/dist/* || true # remove everything git checkout -- kolibri/dist # restore __init__.py staticdeps: clean-staticdeps - test "${SKIP_PY_CHECK}" = "1" || python2 --version 2>&1 | grep -q 2.7 || ( echo "Only intended to run on Python 2.7" && exit 1 ) - pip2 install -t kolibri/dist -r "requirements.txt" + test "${SKIP_PY_CHECK}" = "1" || python --version 2>&1 | grep -q 3.6 || ( echo "Only intended to run on Python 3.6" && exit 1 ) + pip install -t kolibri/dist -r "requirements.txt" rm -rf kolibri/dist/*.egg-info rm -r kolibri/dist/man kolibri/dist/bin || true # remove the two folders introduced by pip 10 - python2 build_tools/py2only.py # move `future` and `futures` packages to `kolibri/dist/py2only` - make test-namespaced-packages staticdeps-cext: rm -rf kolibri/dist/cext || true # remove everything python build_tools/install_cexts.py --file "requirements/cext.txt" # pip install c extensions pip install -t kolibri/dist/cext -r "requirements/cext_noarch.txt" --no-deps rm -rf kolibri/dist/*.egg-info - make test-namespaced-packages staticdeps-compileall: bash -c 'python --version' @@ -209,7 +199,7 @@ read-whl-file-version: python ./build_tools/read_whl_version.py ${whlfile} > kolibri/VERSION pex: - ls dist/*.whl | while read whlfile; do $(MAKE) read-whl-file-version whlfile=$$whlfile; pex $$whlfile --disable-cache -o dist/kolibri-`cat kolibri/VERSION | sed 's/+/_/g'`.pex -m kolibri --python-shebang=/usr/bin/python; done + ls dist/*.whl | while read whlfile; do $(MAKE) read-whl-file-version whlfile=$$whlfile; pex $$whlfile --disable-cache -o dist/kolibri-`cat kolibri/VERSION | sed 's/+/_/g'`.pex -m kolibri --python-shebang=/usr/bin/python3; done i18n-extract-backend: cd kolibri && python -m kolibri manage makemessages -- -l en --ignore 'node_modules/*' --ignore 'kolibri/dist/*' diff --git a/build_tools/install_cexts.py b/build_tools/install_cexts.py index 59aa3909b31..21ec8c4c5fc 100644 --- a/build_tools/install_cexts.py +++ b/build_tools/install_cexts.py @@ -2,7 +2,8 @@ This module defines functions to install c extensions for all the platforms into Kolibri. -It is required to have pip version greater than 19.3.1 to run this script. +See requirements/build.txt for the list of requirements that must be installed for this +script to run. Usage: > python build_tools/install_cexts.py --file "requirements/cext.txt" --cache-path "/cext_cache" @@ -170,8 +171,6 @@ def parse_package_page(files, pk_version, index_url, cache_path): * not the version specified in requirements.txt * not python versions that kolibri does not support * not macosx - * not win_x64 with python 3.8 - * not win32 with python 3.8 """ result = [] @@ -191,13 +190,10 @@ def parse_package_page(files, pk_version, index_url, cache_path): if package_version != pk_version: continue - if python_version != "27" and python_version not in supported_python3_versions: + if python_version not in supported_python3_versions: continue if "macosx" in platform: continue - if "win_amd64" in platform or "win32" in platform and python_version == "27": - # Don't install win_amd64 or win32 with python 2.7 - continue info = { "platform": platform, diff --git a/build_tools/py2only.py b/build_tools/py2only.py deleted file mode 100644 index 303fd994b18..00000000000 --- a/build_tools/py2only.py +++ /dev/null @@ -1,55 +0,0 @@ -import os -import shutil -import sys - -dest = "py2only" -futures_dirname = "concurrent" -scandir_module_name = "scandir.py" -typing_module_name = "typing.py" -DIST_DIR = os.path.join( - os.path.dirname(os.path.realpath(os.path.dirname(__file__))), "kolibri", "dist" -) - - -def hide_py2_modules(): - """ - Move the directory of 'futures' and python2-only modules of 'future' - inside the directory 'py2only' - """ - - # Move the directory of 'futures' inside the directory 'py2only' - _move_modules_to_py2only(futures_dirname) - _move_modules_to_py2only(scandir_module_name) - _move_modules_to_py2only(typing_module_name) - - # Future's submodules are not downloaded in Python 3 but only in Python 2 - if sys.version_info[0] == 2: - from future.standard_library import TOP_LEVEL_MODULES - - for module in TOP_LEVEL_MODULES: - if module == "test": - continue - - # Move the directory of submodules of 'future' inside 'py2only' - _move_modules_to_py2only(module) - - -def _move_modules_to_py2only(module_name): - module_src_path = os.path.join(DIST_DIR, module_name) - module_dst_path = os.path.join(DIST_DIR, dest, module_name) - shutil.move(module_src_path, module_dst_path) - - -if __name__ == "__main__": - # Temporarily add `kolibri/dist` to PYTHONPATH to import future - sys.path.append(DIST_DIR) - - try: - os.makedirs(os.path.join(DIST_DIR, dest)) - except OSError: - raise - - hide_py2_modules() - - # Remove `kolibri/dist` from PYTHONPATH - sys.path = sys.path[:-1] diff --git a/docker/base.dockerfile b/docker/base.dockerfile index 61a2ca8ce89..09ed0080274 100644 --- a/docker/base.dockerfile +++ b/docker/base.dockerfile @@ -1,6 +1,6 @@ FROM ubuntu:jammy -ENV NODE_VERSION=16.20.0 +ENV NODE_VERSION=18.19.0 # install required packages RUN apt-get update && \ @@ -22,7 +22,7 @@ RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources # install nodejs and yarn RUN apt-get update && \ ARCH=$(dpkg --print-architecture) && \ - curl -sSO https://deb.nodesource.com/node_16.x/pool/main/n/nodejs/nodejs_$NODE_VERSION-1nodesource1_$ARCH.deb && \ + curl -sSO https://deb.nodesource.com/node_18.x/pool/main/n/nodejs/nodejs_$NODE_VERSION-1nodesource1_$ARCH.deb && \ dpkg -i ./nodejs_$NODE_VERSION-1nodesource1_$ARCH.deb && \ rm nodejs_$NODE_VERSION-1nodesource1_$ARCH.deb && \ apt-get install yarn diff --git a/docker/build_whl.dockerfile b/docker/build_whl.dockerfile index deb7096324b..94c52b79ab0 100644 --- a/docker/build_whl.dockerfile +++ b/docker/build_whl.dockerfile @@ -1,6 +1,6 @@ FROM ubuntu:bionic -ENV NODE_VERSION=16.20.0 +ENV NODE_VERSION=18.19.0 # install required packages RUN apt-get update && \ @@ -10,7 +10,7 @@ RUN apt-get update && \ gettext \ git \ git-lfs \ - python2.7 \ + python3.6 \ python-pip \ python-sphinx @@ -24,7 +24,7 @@ RUN (curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -) && \ # install nodejs and yarn RUN apt-get update && \ - curl -sSO https://deb.nodesource.com/node_16.x/pool/main/n/nodejs/nodejs_$NODE_VERSION-1nodesource1_amd64.deb && \ + curl -sSO https://deb.nodesource.com/node_18.x/pool/main/n/nodejs/nodejs_$NODE_VERSION-1nodesource1_amd64.deb && \ dpkg -i ./nodejs_$NODE_VERSION-1nodesource1_amd64.deb && \ rm nodejs_$NODE_VERSION-1nodesource1_amd64.deb && \ apt-get install yarn diff --git a/docs/backend_architecture/content/index.rst b/docs/backend_architecture/content/index.rst index 8eb702455a0..dd21aae5548 100644 --- a/docs/backend_architecture/content/index.rst +++ b/docs/backend_architecture/content/index.rst @@ -6,7 +6,6 @@ This is a core module found in ``kolibri/core/content``. .. toctree:: :maxdepth: 1 - models concepts_and_definitions implementation api_methods diff --git a/docs/backend_architecture/content/models.rst b/docs/backend_architecture/content/models.rst deleted file mode 100644 index f3e260b9189..00000000000 --- a/docs/backend_architecture/content/models.rst +++ /dev/null @@ -1,5 +0,0 @@ -Models -====== - -.. automodule:: kolibri.core.content.models - :members: diff --git a/docs/conf.py b/docs/conf.py index 488a87e8d40..59259452e99 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -189,7 +189,7 @@ def setup(app): # Register the docstring processor with sphinx app.connect("autodoc-process-docstring", process_docstring) # Add our custom CSS overrides - app.add_stylesheet("theme_overrides.css") + app.add_css_file("theme_overrides.css") # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, diff --git a/docs/getting_started.rst b/docs/getting_started.rst index f464bd42d0c..58ccf67a53f 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -62,7 +62,7 @@ Python and Pip To develop on Kolibri, you'll need: -* Python 3.4+ or Python 2.7+ (Kolibri doesn't currently support Python 3.11.0 or higher) +* Python 3.6+ (Kolibri doesn't currently support Python 3.12.0 or higher) * `pip `__ Managing Python installations can be quite tricky. We *highly* recommend using `pyenv `__ or if you are more comfortable using a package manager, then package managers like `Homebrew `__ on Mac or ``apt`` on Debian for this. @@ -156,7 +156,7 @@ Note that the ``--upgrade`` flags above can usually be omitted to speed up the p Install Node.js, Yarn and other dependencies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -#. Install `Node.js `__ (version 16.x is required) +#. Install `Node.js `__ (version 18.x is required) #. Install `Yarn `__ #. Install non-python project-specific dependencies @@ -170,7 +170,7 @@ The Python project-specific dependencies installed above will install ``nodeenv` # If you are setting up the release-v0.15.x branch or earlier: nodeenv -p --node=10.17.0 # If you are setting up the develop branch: - nodeenv -p --node=16.20.0 + nodeenv -p --node=18.19.0 npm install -g yarn # other required project dependencies diff --git a/docs/howtos/another_kolibri_instance.md b/docs/howtos/another_kolibri_instance.md index ec703a0fda0..33d55019b2d 100644 --- a/docs/howtos/another_kolibri_instance.md +++ b/docs/howtos/another_kolibri_instance.md @@ -1,3 +1,5 @@ +# Running another Kolibri instance alongside the development server + This guide will walk you through the process of setting up and running another instance of Kolibri alongside your development server using the `pex` executable. ## Introduction diff --git a/docs/howtos/another_kolibri_instance.rst b/docs/howtos/another_kolibri_instance.rst deleted file mode 100644 index b011f4d6b83..00000000000 --- a/docs/howtos/another_kolibri_instance.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _another_kolibri_instance: - -Running another Kolibri instance alongside the development server -================================================================= - -.. mdinclude:: ./another_kolibri_instance.md diff --git a/docs/howtos/index.rst b/docs/howtos/index.rst index 4de513f7fd2..f83ecf881dc 100644 --- a/docs/howtos/index.rst +++ b/docs/howtos/index.rst @@ -13,3 +13,4 @@ These guides are step by step guides for common tasks in getting started and wor pyenv_virtualenv nodeenv another_kolibri_instance + rebasing_a_pull_request diff --git a/docs/howtos/installing_pyenv.md b/docs/howtos/installing_pyenv.md index 7319c02113f..c10292031ec 100644 --- a/docs/howtos/installing_pyenv.md +++ b/docs/howtos/installing_pyenv.md @@ -1,3 +1,5 @@ +## Installing pyenv + ### Prerequisites [Git](https://git-scm.com/) installed. diff --git a/docs/howtos/installing_pyenv.rst b/docs/howtos/installing_pyenv.rst deleted file mode 100644 index 98cfb59c102..00000000000 --- a/docs/howtos/installing_pyenv.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _installing_pyenv: - -Installing pyenv -================ - -.. mdinclude:: ./installing_pyenv.md diff --git a/docs/howtos/nodeenv.md b/docs/howtos/nodeenv.md index 3b2edbbf21f..d33a48b1731 100644 --- a/docs/howtos/nodeenv.md +++ b/docs/howtos/nodeenv.md @@ -1,3 +1,5 @@ +# Using nodeenv + ## Instructions Once you've created a python virtual environment, you can use `nodeenv` to install particular versions of node.js within the environment. This allows you to use a different node.js version in the virtual environment than what's available on your host, keep multiple virtual enviroments with different versions of node.js, and to install node.js "global" modules that are only available within the virtual environment. @@ -18,14 +20,14 @@ $ nodeenv -l but this lists out everything. Alternatively, here's a one line bash function that can be used to determine that version: ```bash $ function latest-node() { curl -s "https://nodejs.org/dist/latest-v$1.x/" | egrep -m 1 -o "$1\.[0-9]+\.[0-9]+" | head -1; } -$ latest-node 16 -16.16.0 +$ latest-node 18 +18.19.0 ``` Once you've determined the version, you can install it: ```bash -$ nodeenv --python-virtualenv --node 16.16.0 - * Install prebuilt node (16.16.0) ..... done. +$ nodeenv --python-virtualenv --node 18.19.0 + * Install prebuilt node (18.19.0) ..... done. * Appending data to /home/bjester/Projects/learningequality/kolibri/venv/bin/activate * Appending data to /home/bjester/Projects/learningequality/kolibri/venv/bin/activate.fish ``` diff --git a/docs/howtos/nodeenv.rst b/docs/howtos/nodeenv.rst deleted file mode 100644 index 67cc908dc2f..00000000000 --- a/docs/howtos/nodeenv.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _nodeenv: - -Using nodeenv -============= - -.. mdinclude:: ./nodeenv.md diff --git a/docs/howtos/pyenv_virtualenv.md b/docs/howtos/pyenv_virtualenv.md index acbbb94f260..918087b535b 100644 --- a/docs/howtos/pyenv_virtualenv.md +++ b/docs/howtos/pyenv_virtualenv.md @@ -1,3 +1,5 @@ +## Using pyenv-virtualenv + ### Virtual Environments Virtual environments allow a developer to have an encapsulated Python environment, using a specific version of Python, and with dependencies installed in a way that only affect the virtual environment. This is important as different projects or even different versions of the same project may have different dependencies, and virtual environments allow you to switch between them seamlessly and explicitly. diff --git a/docs/howtos/pyenv_virtualenv.rst b/docs/howtos/pyenv_virtualenv.rst deleted file mode 100644 index c98277a1afc..00000000000 --- a/docs/howtos/pyenv_virtualenv.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _pyenv_virtualenv: - -Using pyenv-virtualenv -====================== - -.. mdinclude:: ./pyenv_virtualenv.md diff --git a/docs/howtos/rebasing_a_pull_request.md b/docs/howtos/rebasing_a_pull_request.md new file mode 100644 index 00000000000..623e842099e --- /dev/null +++ b/docs/howtos/rebasing_a_pull_request.md @@ -0,0 +1,22 @@ +On certain occasions, it might be necessary to redirect a pull request from the develop branch to the latest release branch, such as `release-v*` (e.g., `release-v0.16.x` when working on version 0.16), or vice versa. This guide outlines the steps for rebasing a feature branch related to your pull request while maintaining a clean commit history. + +The demonstration centers on the process of rebasing a feature branch that is directed towards the `develop` branch in your pull request, transitioning it to the most recent release branch, identified as `release-v*`. If the need arises to rebase your pull request in the opposite direction—from `release-v*` to `develop` you can follow the same steps, just adjusting the branch names as indicated in the guide below. + + + - Make sure you have local versions of the `develop` branch and the `release-v*` branch. + - Ensure that both branches are up to date. For this guide, we'll assume they are named `develop` and `release-v*`, respectively. + +Locally, checkout your feature branch and run the following rebase command: + +``` +git rebase --onto release-v* develop +``` +This command will rebase your current feature branch onto `release-v*`, removing any commits that are already present in `develop`. + +After completing the rebase, you will need to force push to update your remote branch. Use the following command: + +``` +git push --force +``` + +**Caution:** Handle force-pushes with care. diff --git a/docs/howtos/rebasing_a_pull_request.rst b/docs/howtos/rebasing_a_pull_request.rst new file mode 100644 index 00000000000..ed587fabca9 --- /dev/null +++ b/docs/howtos/rebasing_a_pull_request.rst @@ -0,0 +1,6 @@ +.. _rebasing_a_pull_request: + +Rebasing a Pull Request +======================= + +.. mdinclude:: ./rebasing_a_pull_request.md diff --git a/docs/i18n.rst b/docs/i18n.rst index 9057d8c9e34..ee265b5343d 100644 --- a/docs/i18n.rst +++ b/docs/i18n.rst @@ -92,9 +92,35 @@ A pattern we use in order to avoid having to define the same string across multi In order to avoid bloating the common modules, we typically will not add a string we are duplicating to a common module unless it is being used across three or more files. -Common strings modules should typically have the following components: +Common strings modules should typically have a translator created using the ``createTranslator`` function in which strings are defined - these can then be used in the setup function of a component to expose specific strings as functions: + +.. code-block:: javascript + + import commonStringsModule from '../common/commonStringsModule'; + + + export default { + name: 'someComponent', + setup() { + const { myCoolString$, stringWithArgument$ } = commonStringsModule(); + return { + myCoolString$, stringWithArgument$ + }; + }, + }; + +.. code-block:: html + + + + +Previously, this has been handled via mixins, which has required this additional complexity in the modules. You may see modules that include the translator object and the following: -- A translator created using the ``createTranslator`` function in which strings are defined. - An exported function that accepts a ``string`` and an ``object`` - which it then passes to the ``$tr()`` function to get a string from the translator in the module. - An exported Vue mixin that exposes the exported function as a ``method``. This allows Vue components to use the mixin and have the exported function to get a translated string readily at hand easily. diff --git a/docs/stack.rst b/docs/stack.rst index 290cabc60be..4f69109c401 100644 --- a/docs/stack.rst +++ b/docs/stack.rst @@ -10,7 +10,7 @@ Note that since Kolibri is still in development, the APIs are subject to change, Server ------ -The server is a `Django `__ application, and contains only pure-Python (2.7+) dependencies at run-time. It is responsible for: +The server is a `Django `__ application, and contains only pure-Python (3.6+) dependencies at run-time. It is responsible for: - Interfacing with the database (either `SQLite `__ or `PostgreSQL `__) - Authentication and permission middleware diff --git a/integration_testing/016-features/admin/facility/facility-data/admin-export-data.feature b/integration_testing/016-features/admin/facility/facility-data/admin-export-data.feature index 10866ea3f92..c7b23eab82f 100644 --- a/integration_testing/016-features/admin/facility/facility-data/admin-export-data.feature +++ b/integration_testing/016-features/admin/facility/facility-data/admin-export-data.feature @@ -7,17 +7,19 @@ Feature: Admin export usage data And the learners have had interactions with the content on the device Scenario: Export session logs - When I click on "Generate log file" link under *Session logs* heading - Then I see the loading indicator - And the *Download* button is enabled - And the text change to "Generate a new log file" - When I click on *Download* button + When I click on the *Generate log* button under *Session logs* heading + Then I see the *Select a date range* modal + When I select a start and an end date + And I click *Generate* + Then I see a *Download* button displayed to the left of the *Generate log* button + When I click on the *Download* button Then I see the *Open/Save as* window, or the file 'content_session_logs.csv' is automatically saved on my local drive, depending on the browser defaults Scenario: Export summary logs - When I click on "Generate log file" link under *Summary logs* heading - Then I see the loading indicator - And the *Download* button is enabled - And the text change to "Generate a new log file" - When I click on *Download* button - Then I see the *Open/Save as* window, or the file 'content_summary_logs.csv' is automatically saved on my local drive, depending on the browser defaults + When I click on the *Generate log* button under *Summary logs* heading + Then I see the *Select a date range* modal + When I select a start and an end date + And I click *Generate* + Then I see a *Download* button displayed to the left of the *Generate log* button + When I click on the *Download* button + Then I see the *Open/Save as* window, or the file 'content_session_logs.csv' is automatically saved on my local drive, depending on the browser defaults diff --git a/integration_testing/016-features/common/app-bar/android-app-bar.feature b/integration_testing/016-features/common/app-bar/android-app-bar.feature index 7e105d7e21e..08eaea9eefb 100644 --- a/integration_testing/016-features/common/app-bar/android-app-bar.feature +++ b/integration_testing/016-features/common/app-bar/android-app-bar.feature @@ -16,7 +16,7 @@ Feature: Device setup And I see the star icon and the username which is truncated after XX characters When I click the *...* ellipsis button Then I see the *Info* and *Settings* tabs - When I click the star icon + When I click the star icon #NOT IMPLEMENTED Then I see the value of the earned points Scenario: New bottom bar on the native Android app @@ -33,10 +33,10 @@ Feature: Device setup Then I am at the selected page And the menu is hidden When I scroll down - Then both the top and the bottom bars hide + Then both the top and the bottom bars hide #NOT IMPLEMENTED When I scroll up - Then both bars appear again - When I am in a tab accessible through the menu such as *Coach*, *Facility*, *Device* + Then both bars appear again #NOT IMPLEMENTED + When I am in a tab not accessible through the menu such as *Coach*, *Facility*, *Device* Then nothing in the app bar is selected Scenario: Updates to side menu @@ -47,7 +47,7 @@ Feature: Device setup And I can see my points And I can see that all the other sections of the menu are collapsed And I can see the *My downloads*, *Profile*, *Change language* and *Sign out* options - And I can see the kolibri icon, version of Kolibri, and copyrigt text + And I can see the kolibri icon, version of Kolibri, and copyright text And I can see the *Usage and privacy* link Scenario: Updates to learn-only device side menu diff --git a/integration_testing/016-features/common/app-bar/appbar-scroll.feature b/integration_testing/016-features/common/app-bar/appbar-scroll.feature index e9704dde577..25ec96370c8 100644 --- a/integration_testing/016-features/common/app-bar/appbar-scroll.feature +++ b/integration_testing/016-features/common/app-bar/appbar-scroll.feature @@ -1,4 +1,4 @@ -Feature: Scrolling app bar +Feature: Scrolling app bar #NOT IMPLEMENTED Top app bar should become visible when scrolling up and hidden when scrolling down on mobile Background: diff --git a/integration_testing/016-features/learner/content-syncing/learner-automatic-content-syncing.feature b/integration_testing/016-features/learner/content-syncing/learner-automatic-content-syncing.feature index 12803e08181..587253b67fb 100644 --- a/integration_testing/016-features/learner/content-syncing/learner-automatic-content-syncing.feature +++ b/integration_testing/016-features/learner/content-syncing/learner-automatic-content-syncing.feature @@ -3,7 +3,7 @@ Feature: Learners automatic syncing Background: Given I am signed in as a learner user - Scenario: Learner on a learn-only device can see the device status in the side menu + Scenario: Learner on a learn-only device can see the device status in the side menu #NOT IMPLEMENTED Given I am on a learn-only device When I expand the side menu Then I see a *Device status* label diff --git a/integration_testing/016-features/learner/content-syncing/learner-explore-my-own-library.feature b/integration_testing/016-features/learner/content-syncing/learner-explore-my-own-library.feature index 5d55840b4e7..96a389d3677 100644 --- a/integration_testing/016-features/learner/content-syncing/learner-explore-my-own-library.feature +++ b/integration_testing/016-features/learner/content-syncing/learner-explore-my-own-library.feature @@ -45,6 +45,12 @@ Feature: Find new things in your library And I see the topic title of the first available folder And I see the available folders and resources cards in a single column view + Scenario: The *Show* dropdown is hidden if there are no resources downloaded by the user #NOT IMPLEMENTED + When I go to the *Library > Explore channel* modal + And there are available resources + And I have previously not downloaded any of the resources + Then the the *Show* dropdown is not being displayed + Scenario: All resources are displayed by default Given I am at the *Library > Explore channel* modal And I have previously downloaded resources @@ -54,12 +60,6 @@ Feature: Find new things in your library When I select the option *My downloads only* Then I see only the downloaded resources - Scenario: The *Show* dropdown is hidden if there are no resources downloaded by the user - When I go to the *Library > Explore channel* modal - And there are available resources - And I have previously not downloaded any of the resources - Then the the *Show* dropdown is not being displayed - Scenario: Add resource to My downloads from folder/resource browsing page Given I am at the *Library > Explore channel* modal And there are downloadable resources diff --git a/integration_testing/016-features/learner/content-syncing/learner-explore-other-libraries.feature b/integration_testing/016-features/learner/content-syncing/learner-explore-other-libraries.feature index 531d0d693ec..7b8b1d5123a 100644 --- a/integration_testing/016-features/learner/content-syncing/learner-explore-other-libraries.feature +++ b/integration_testing/016-features/learner/content-syncing/learner-explore-other-libraries.feature @@ -9,7 +9,7 @@ Feature: Learner explores other libraries Scenario: Explore someone else's library When I load the *Learn > Library* page And I look at a library in the *Other libraries* section of the page - And I click the *Explore this library* card + And I click the *Explore* card Then I see the *Explore libraries* page And I see the search by keyword and the search filters to the left And I see a *All libraries* link and a back arrow at the top @@ -25,13 +25,12 @@ Feature: Learner explores other libraries Then I see there only channels for pinned libraries And I see a *More libraries* section under the *Other libraries* section And I see some of the unpinned libraries - And I see a *See all libraries* card - When I click the *See all libraries* card - Then I see the *All libraries* page + And I see a *View all* card + When I click the *View all* card + Then I see the *Explore libraries* page And I see a *All libraries* label - And I see *Showing the libraries on other devices and networks around you* below the *All libraries* label + And I see *Showing libraries on other devices around you* below the *All libraries* label And I see the available full devices - And I see the *External storage* section Ans I see a pin icon next to each device name And I see an *Explore* button next to each section @@ -39,6 +38,7 @@ Feature: Learner explores other libraries Given I am at the *All libraries* page When I click the pin icon next to a unpinned library Then the color of the icon is changed to black + And I see a *Pinned to my library page* snackbar message When I close the *Explore library* page Then I am back at the *Library* page And I can see the newly pinned library @@ -47,11 +47,12 @@ Feature: Learner explores other libraries Given I am at the *All libraries* page When I click the pin icon next to a pinned library Then the color of the icon is changed to white + And I see a *Pin removed from my library page* snackbar message When I close the *Explore library* page Then I am back at the *Library* page And I can see the that the unpinned library is no longer displayed there - Scenario: Show other learn-only devices under "More libraries" + Scenario: Show other learn-only devices under *More libraries* #POSSIBLY NOT IMPLEMENTED Given I am at the *All libraries* page And there are available learn-only devices When I scroll down to the *More libraries* section @@ -85,21 +86,21 @@ Feature: Learner explores other libraries And there is a download icon on the resource card Then that resource is available When I click the download icon to download the resource - Then I see a confirmation *Started download Go to downloads* + Then I see a snackbar confirmation *Download requested Go to downloads* When I look at another resource card And there is no download icon And there is an elipsis button to the right of the info icon Then the resource has already been downloaded - Scenario: Add resource to My downloads from folder/resource browsing page + Scenario: Add resource to *My downloads* from folder/resource browsing page Given I am exploring a library with folders and resources available for download When I click the download icon of the resource - Then I see a confirmation *Started download Go to downloads* + Then I see a confirmation *Download requested Go to downloads* When the download has finished And I go to *My downloads* Then I see the resource in *My downloads* - Scenario: Add resource to My downloads from the information panel + Scenario: Add resource to *My downloads* from the information panel #NOT IMPLEMENTED Given I am exploring a library with folders and resources available for download When I click the *i* icon of the resource Then I see the info side panel for the resource diff --git a/integration_testing/016-features/learner/content-syncing/learner-library-page.feature b/integration_testing/016-features/learner/content-syncing/learner-library-page.feature index 21a56f530d9..fce1f77731b 100644 --- a/integration_testing/016-features/learner/content-syncing/learner-library-page.feature +++ b/integration_testing/016-features/learner/content-syncing/learner-library-page.feature @@ -9,10 +9,11 @@ Feature: My downloads - Library page Given I'm not connected to any device, network, or external storage When I load the *Learn > Library* page And I look at the *Other libraries* section of the page - Then I see the label *Other libraries* connection status + Then I see the label *Other libraries* + And I see *Searching for libraries around you.* and a spinner icon When the search is over Then I see *No other libraries around you right now* - And I see *Searching for new materials around you* #TO DO, this is not clear + And I see a *Refresh* link Scenario: Learner is connected to the Internet and there are up 1-3 libraries Given I'm connected to the Internet and there are devices with available libraries diff --git a/integration_testing/016-features/learner/content-syncing/misc-content-syncing.feature b/integration_testing/016-features/learner/content-syncing/misc-content-syncing.feature index 0e2736713ee..01b2d783468 100644 --- a/integration_testing/016-features/learner/content-syncing/misc-content-syncing.feature +++ b/integration_testing/016-features/learner/content-syncing/misc-content-syncing.feature @@ -3,7 +3,7 @@ Feature: Misc content syncing Background: Given I am signed in as a learner user - Scenario: Manage preferred language in user profile + Scenario: Manage preferred language in user profile #NOT IMPLEMENTED When I go to the *Profile* page Then I see the *Preferred language* field And I see the current preferred language @@ -15,7 +15,7 @@ Feature: Misc content syncing Then I am at the *Profile* page And I see that the preferred language is changed to the language I selected - Scenario: Changed position of the *Edit profile* form buttons + Scenario: Changed position of the *Edit profile* form buttons #NOT IMPLEMENTED When I go to the *Profile* page And I click *Edit* Then I see the *Save* button at the bottom right corner of the form @@ -30,4 +30,4 @@ Feature: Misc content syncing When I go to a channel resource And I click the *View information* button Then I see the info panel - And I see the button *Download resource* is changed to *Download* + And I see the button *Download resource* is changed to *Save to device* diff --git a/integration_testing/016-features/superadmin/sorting-user-tables.feature b/integration_testing/016-features/superadmin/common/sorting-user-tables.feature similarity index 81% rename from integration_testing/016-features/superadmin/sorting-user-tables.feature rename to integration_testing/016-features/superadmin/common/sorting-user-tables.feature index d3671fe591a..1d34aaaab79 100644 --- a/integration_testing/016-features/superadmin/sorting-user-tables.feature +++ b/integration_testing/016-features/superadmin/common/sorting-user-tables.feature @@ -4,41 +4,41 @@ Feature: Sorting user tables Given I am signed in as an admin And there are users who joined the facility using the *Change facility* option - Scenario: Default sorting at *Facility > Users* + Scenario: Default sorting at *Facility > Users* #NOT IMPLEMENTED When I go to *Facility > Users* Then I see that the default sorting on page load is by *Full name*, ascending - Scenario: Sort by *Date created* in *Facility > Users* + Scenario: Sort by *Date created* in *Facility > Users* #NOT IMPLEMENTED When I go to *Facility > Users* And I sort by *Date created* Then I see all users sorted by *Date created* ascending - Scenario: Sort by *Date added* in *Facility > Users* + Scenario: Sort by *Date added* in *Facility > Users* #NOT IMPLEMENTED When I go to *Facility > Users* And I sort by *Date added* Then I see all users sorted by *Date added* ascending - Scenario: Default sorting at *Device > Device permissions* + Scenario: Default sorting at *Device > Device permissions* #NOT IMPLEMENTED When I go to *Device > Device permissions* Then I see that the default sorting on page load is by *Full name*, ascending - Scenario: Sort by *Date created* in *Device > Device permissions* + Scenario: Sort by *Date created* in *Device > Device permissions* #NOT IMPLEMENTED When I go to *Device > Device permissions* And I sort by *Date created* Then I see all users sorted by *Date created* ascending - Scenario: Sort by *Date added* in *Device > Device permissions* + Scenario: Sort by *Date added* in *Device > Device permissions* #NOT IMPLEMENTED When I go to *Device > Device permissions* And I sort by *Date added* Then I see all users sorted by *Date added* ascending Scenario: Position of sort icon for text-only data fields - Given I am at *Facility > Users* or *Device > Device permissions* + Given I am at *Facility > Users* or *Device > Device permissions* #NOT IMPLEMENTED When I look at a column with text-only data fields Then I see that the column is left-aligned And I see that the icon is to the right of the column label - Scenario: Position of sort icon for numeric-only data fields + Scenario: Position of sort icon for numeric-only data fields #NOT IMPLEMENTED Given I am at *Facility > Users* or *Device > Device permissions* When I look at a column with numeric-only data fields Then I see that the column is right-aligned diff --git a/integration_testing/016-features/superadmin/device/device-settings/super-admin-change-device-settings.feature b/integration_testing/016-features/superadmin/device/device-settings/super-admin-change-device-settings.feature deleted file mode 100644 index 3b354b2dd62..00000000000 --- a/integration_testing/016-features/superadmin/device/device-settings/super-admin-change-device-settings.feature +++ /dev/null @@ -1,48 +0,0 @@ -Feature: Super admin changes device settings - Super admin needs to be able to change settings related to access to resources, unlisted channels and the default landing page - Background: - Given I am signed in to Kolibri as super admin user - And I am on *Device > Settings* page - And there are learner and coach user accounts created in the facility - And a channel has been imported from Kolibri Studio using a token - And there is another Kolibri instance on my network - - Scenario: Allow guest browsing - Given the *Allow users to access resources without signing in* checkbox is unchecked - And the *Landing page* option is set to the *Sign-in page* - And the *Learners should only see resources assigned to them in classes* is unchecked - When I check the *Allow users to access resources without signing in* checkbox - Then I see that the *Learners should only see resources assigned to them in classes* options is disabled (grayed out) - When I click the *Save* button - And I sign out - Then I see the *Explore without account* link on the sign-in page - When I click *Continues as a guest* - Then I see the *Learn > Channels* page - - Scenario: The landing page is the learn page - Given the *Allow users to access resources without signing in* checkbox is unchecked - And the *Landing page* option is set to the *Sign-in page* - And the *Learners should only see resources assigned to them in classes* is unchecked - When I select *Learn page* for the *Landing page* - Then I see that both the *Allow users to access resources without signing in* and *Learners should only see resources assigned to them in classes* options are disabled (grayed out) - When I click the *Save* button - And I sign out - Then I see immediately see the *Learn > Channels* page - When I sign-in as learner - Then I see *Learn > Channels* page again - - Scenario: Learners can only access assigned resources - Given the *Learners should only see resources assigned to them in classes* is unchecked - And the *Landing page* option is set to the *Sign-in page* - And the *Allow users to access resources without signing in* checkbox is unchecked - When I check the *Learners should only see resources assigned to them in classes* checkbox - And I click the *Save* button - And I sign out - Then I do not see the *Continues as a guest* link on the sign-in page - When I sign-in as learner - Then I see the *Learn > Classes* page - And I do not see the *Channels* or *Recommended* tabs - -Examples: -| full_name | username | password | -| John C. | learner | learner | diff --git a/integration_testing/016-features/superadmin/facility/facility-data/super-admin-export-data.feature b/integration_testing/016-features/superadmin/facility/facility-data/super-admin-export-data.feature index 86f69762524..ada7000a02a 100644 --- a/integration_testing/016-features/superadmin/facility/facility-data/super-admin-export-data.feature +++ b/integration_testing/016-features/superadmin/facility/facility-data/super-admin-export-data.feature @@ -2,7 +2,7 @@ Feature: Super admin exports usage data Super admin needs to be able to export session and summary logs for the facility Background: - Given I am signed in to Kolibri as super admin + Given I am signed in to Kolibri as a super admin And I am on *Facility > Data* page And the learners have had interactions with the content on the device diff --git a/integration_testing/016-features/superadmin/facility/facility-settings/facility-settings.feature b/integration_testing/016-features/superadmin/facility/facility-settings/facility-settings.feature index 75e0ec72100..8ac337dbb54 100644 --- a/integration_testing/016-features/superadmin/facility/facility-settings/facility-settings.feature +++ b/integration_testing/016-features/superadmin/facility/facility-settings/facility-settings.feature @@ -8,12 +8,12 @@ Feature: Facility settings And I am at *Facility > Settings* page Scenario: Allow learners to join the facility - When I select the option *Allow learners to join this facility* + When I select the option *Allow learners to join this facility* #NOT IMPLEMENTED And I click the *Save changes* button Then the option to allow learners to join the facility is enabled And I can join the facility as a learner - Scenario: Allow users to leave this facility and join a different facility + Scenario: Allow users to leave this facility and join a different facility #NOT IMPLEMENTED When I select the option *Allow users to leave this facility and join a different facility* And I click the *Save changes* button Then the option to allow users to leave this facility and join a different facility is enabled diff --git a/integration_testing/016-features/superadmin/device/device-settings/super-admin-rename-facility.feature b/integration_testing/016-features/superadmin/facility/facility-settings/super-admin-rename-facility.feature similarity index 100% rename from integration_testing/016-features/superadmin/device/device-settings/super-admin-rename-facility.feature rename to integration_testing/016-features/superadmin/facility/facility-settings/super-admin-rename-facility.feature diff --git a/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-creates-new-facility-in-device-setup.feature b/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-creates-new-facility-in-device-setup.feature deleted file mode 100644 index 62195d45a6a..00000000000 --- a/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-creates-new-facility-in-device-setup.feature +++ /dev/null @@ -1,76 +0,0 @@ -Feature: Super admin creates new facility during the device setup - Super admin needs to create their own account and perform the initial facility configuration of Kolibri through the setup wizard - - Background: - Given that the Kolibri installation was successful - And the server is running for the first time - And the browser is opened at the IP address 127.0.0.1:8080 - And I have selected a language in the device setup - And I have selected *Advanced setup* - And I have chosen a *Device name* - - Scenario: Create new facility - Given I see *Create or import facility* - When I click *New facility* - Then I see *What kind of learning environment is your facility?* - And I see *New facility - 1 of 6* in the app bar - - Scenario: Facility setup options - Given that I am on the *New facility - 1 of 6* of the setup wizard - And I see *What kind of learning environment is your facility?* - And I see *Non-formal* option - And I see *Formal* option - When I select *Non-formal* or *Formal* options - But I don't write anything in the *Facility name* field - And click *Continue* - Then I see the error notification - When I input something in the *Facility name* field - And I click *Continue* - Then I see the *New facility - 2 of 6* of the setup wizard - - Scenario: Guest access - Given that I am on the *New facility - 2 of 6* of the setup wizard - And I see *Enable guest access?* - When I select/change one of the options - And I click *Continue* - Then I see the *New facility - 3 of 6* of the setup wizard - - Scenario: Allow user account creation - Given that I am on the *New facility - 3 of 6* of the setup wizard - And I see *Allow anyone to create their own learner account?* - When I select/change one of the options - And I click *Continue* - Then I see the *New facility - 4 of 6* of the setup wizard - - Scenario: Enable passwords - Given that I am on the *New facility - 4 of 6* of the setup wizard - And I see *Enable passwords on learner accounts?* - When I select/change one of the options - And I click *Continue* - Then I see the *New facility - 5 of 6* of the setup wizard - - Scenario: Super admin account details for a new facility - Given that I am on the *New facility - 5 of 6* of the setup wizard - And I see *Create super admin account* - When I fill in the full name, username and password fields - And I click the *Usage and privacy* link - Then I see the *Usage and privacy* modal - And I see the text of the privacy statement with the first heading *Users* - When I click *Close* - Then the modal closes - When I click *Continue* - Then I see the *New facility - 6 of 6* of the setup wizard - - Scenario: Responsibility of the admin regarding privacy - Given that I am on the *New facility - 6 of 6* of the setup wizard - And I see *Responsibilities as an administrator* - When I click the *Usage and privacy* link - Then I see the *Usage and privacy* modal - And I see the text of the privacy statement with the first heading *Administrators* - When I click *Close* - Then the modal closes - When I click *Finish* - Then I see the *Setting up the facility...* page - And I see the *Welcome to Kolibri!* modal - When I click *Continue* - Then I see the *Device > Channels* page diff --git a/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-goes-through-setup-wizard-offline-mode.feature b/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-goes-through-setup-wizard-offline-mode.feature deleted file mode 100644 index a87657e07b5..00000000000 --- a/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-goes-through-setup-wizard-offline-mode.feature +++ /dev/null @@ -1,69 +0,0 @@ -Feature: Super admin goes through the setup wizard in offline mode - Super admin can go through the Setup Wizard in offline mode - - # In case of testing on virtual machine: - # Make sure that your testing environment has no Internet connection (before you start the VM, go to Properties > Network > Adapter 1, and uncheck the Enable Network Adapter checkbox; try to do a Google search to confirm that the VM has no Internet access). - # Download it on your host and unzip anywhere on your Windows 10 VM guest (Desktop should work fine). VM's Shared Folders setting should work even without the network adapter. - # Keep both folders (Kolibri and KOLIBRI_DATA) together in the same location. - # Open the Kolibri folder and double-click the Kolibri.exe file. - - Background: - Given The testing environment has no active Internet connection - And The Kolibri installation was successful - And the server is running for the first time - And the App is opened in the virtual machine - - Scenario: Select language - Given that I am at the beginning of the setup wizard - And I see *Please select the default language for Kolibri* - When I click the link *Español* - Then the wizard language changes to Spanish - When I click *More languages* - Then I see the *Change language* modal - When I select *Français* - And click *Confirmar* - Then the modal closes - And I see the wizard language changes to French - When I click the link *English* - Then the wizard language changes to English - When I click *Continue* button - Then I see *Getting started* - - Scenario: Select 'Quick start' - Given I see *How are you using Kolibri?* - When I select *Quick start* - And I click *Continue* - Then I see *Create super admin account* - - Scenario: Select 'Advanced setup' wizard - Given I am on the Language selection page - When I click the Continue button on that page - Then I see *Quick Start* and *Advanced setup* options - When I select *Advanced setup* option - And I click the Continue button - Then I see the Device name page - When I fill a device name and click Continue button - Then I see the Create or import facility page - When I click the New facility button - Then The *New facility - step 1 of 6* page loads - And I see *Non-formal* and *Formal* options for facility type - When I select *Formal* - And I give some name of my new facility - And I click the Continue button - Then The *New facility - step 2 of 6* page loads - And I see *Enable guest access?* options - When I select *No* and click Continue button - Then The "New facility - step 3 of 6" page loads - And I see the "Allow anyone to create their own learner account?" options - When I select *No* and click Continue button - Then The *New facility - step 4 of 6* page loads - And I see *Enable passwords on learner accounts?* options - When I select *No* and click Continue button - Then The *New facility - step 5 of 6* page loads - And I see *Create super admin account* form - When I fill the form with valid data and click Continue button - Then The *New facility - step 6 of 6* page loads - And I see *Responsibilities as an administrator* text - And I see the Finish button - When I click the Finish button - Then I am logged in to Kolibri diff --git a/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-imports-facility-in-device-setup.feature b/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-imports-facility-in-device-setup.feature deleted file mode 100644 index cbf3306feee..00000000000 --- a/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-imports-facility-in-device-setup.feature +++ /dev/null @@ -1,235 +0,0 @@ -Feature: Super admin imports facility in device setup - Super admin can import an existing facility from the local network - - Background: - Given that the Kolibri installation was successful - And the server is running for the first time - And the browser is opened at the IP address 127.0.0.1:8080 - And there is another auto-discoverable device in the local network that is running a server with a Kolibri facility - And I have selected a language in the device setup - And I have selected *Advanced setup* - And I have entered a *Device name* - - Scenario: View a list of devices in my network - Given I see *Select a facility setup for this device* - When I select *Import all data from an existing facility* - And I click *Continue* - Then I see *Select network address* - And I see a list of devices in my network - - Scenario: Import facility from a device with multiple facilities - Given I am at *Select network address* modal - And I select a device - And that device has more than one facility loaded - When I click *Continue* - Then I see *Import facility - 1 of 4* - And I see *Select facility* - And I see the name of the device from which I am importing - And I see the network address of that device - And I see a list of facilities on that device - When I select a facility - Then I see *Enter the username and password for a facility admin of or a super admin of - When I enter the username and password of a facility admin or super admin - And I click *Continue* - Then I see *Import facility - 2 of 4* - And I see *Loading ''* - And I see loading status bar - When the facility has finished loading - Then I see the status *Finished* - And I see a green check icon - And I see *'' successfully loaded to this device* - When I click *Continue* - Then I see *Import facility - 3 of 4* - - Scenario: Import facility from a device with only one facility - Given I am on *Select network address* modal - And I select a device - And that device has only one facility loaded - When I click *Continue* - Then I see *Import facility - 1 of 4* - And I see *Import facility* - And I see the name of the device from which I am importing - And I see the network address of that device - And I see *Enter the username and password for a facility admin or a super admin of ''* - When I enter the username and password of a facility admin or super admin - And I click *Continue* - Then I see *Import facility - 2 of 4* - And I see *Loading ''* - And I see loading status bar - When the facility has finished loading - Then I see the status *Finished* - And I see a green check icon - And I see *'' successfully loaded to this device* - When I click *Continue* - Then I see *Import facility - 3 of 4* - - Scenario: Import facility from a 0.14.x device with multiple facilities to a 0.15.x device - Given I am at *Select network address* modal - And I select a device - And that device has more than one facility loaded - When I click *Continue* - Then I see *Import facility - 1 of 4* - And I see *Select facility* - And I see the name of the device from which I am importing - And I see the network address of that device - And I see a list of facilities on that device - When I select a facility - Then I see *Enter the username and password for a facility admin of or a super admin of - When I enter the username and password of a facility admin or super admin - And I click *Continue* - Then I see *Import facility - 2 of 4* - And I see *Loading ''* - And I see loading status bar - When the facility has finished loading - Then I see the status *Finished* - And I see a green check icon - And I see *'' successfully loaded to this device* - When I click *Continue* - Then I see *Import facility - 3 of 4* - - Scenario: Import facility from a a 0.14.x device with only one facility - Given I am on *Select network address* modal - And I select a device - And that device has only one facility loaded - When I click *Continue* - Then I see *Import facility - 1 of 4* - And I see *Import facility* - And I see the name of the device from which I am importing - And I see the network address of that device - And I see *Enter the username and password for a facility admin or a super admin of ''* - When I enter the username and password of a facility admin or super admin - And I click *Continue* - Then I see *Import facility - 2 of 4* - And I see *Loading ''* - And I see loading status bar - When the facility has finished loading - Then I see the status *Finished* - And I see a green check icon - And I see *'' successfully loaded to this device* - When I click *Continue* - Then I see *Import facility - 3 of 4* - - Scenario: Import facility by manually adding the URL address of an existing Kolibri instance - Given I am on *Select network address* modal - When I click *Add new address* - Then I see the *New address* modal - When I enter the URL address of an existing Kolibri instance in the *Full network address* field - And I enter a name for this address in the *Name* field - And I click *Add* - Then I am back at the *Select network address* modal - And I see that the added network address is selected - When I click *Continue* - Then I see *Import facility - 1 of 4* - And I see *Import facility* - And I see the name of the device from which I am importing - And I see the network address of that device - And I see *Enter the username and password for a facility admin or a super admin of ''* - When I enter the username and password of a facility admin or super admin - And I click *Continue* - Then I see *Import facility - 2 of 4* - And I see *Loading ''* - And I see loading status bar - When the facility has finished loading - Then I see the status *Finished* - And I see a green check icon - And I see *'' successfully loaded to this device* - When I click *Continue* - Then I see *Import facility - 3 of 4* - - # to test this you will have to stop or disconnect the peer device - Scenario: Loading facility fails and user starts over - Given I am on *Import facility - 2 of 4* step - And I see *Loading ''* - And I see loading status messages - And I see an indeterminate loading spinner - And I don't see a back arrow in the app bar - When something happens which causes the load to fail - Then I see * of 7: Failed* - And I see *Could not load '' to this device* - And I see *Retry* button - And I see *Start over* button - When I click *Retry* - Then I see loading messages for the facility - When it fails again - And I click *Start over* - Then I see *Please select the default language for Kolibri* - - # to test this you will have to stop or disconnect the peer device - Scenario: Loading facility fails - Given I am on *Import facility - 2 of 4* - And I see *Loading ''* - And I see loading status messages - And I see an indeterminate loading spinner - And I don't see a back arrow in the app bar - And I wish to cancel the facility load - When I click *Cancel* - Then I see *Cancelled* - And I see *Could not load '' to this device* - And I see *Retry* button - And I see *Start over* button - - Scenario: Set super admin account to default admin - Given I am on *Import facility - 3 of 4* - And I see *Select super admin account* - And I see a dropdown for super admin - And I see the username of the admin that I used to load the facility - When I click *Continue* - Then I see *Import facility - 4 of 4* - - Scenario: Set super admin account to a different admin - Given I am on *Import facility - 3 of 4* - And I see *Set super admin account* - And I see a dropdown for super admin - And I see the username of the admin that I used to load the facility - When I click the super admin dropdown - Then I see a list of facility admins of the facility I loaded - And I see a list of super admins of the device I loaded from - When I select a different admin - Then I see the selected admin in the closed dropdown - When I click *Continue* - Then I see *Import facility - 4 of 4* - - Scenario: Set super admin account to create new admin - Given I am on *Import facility - 3 of 4* - And I see *Set super admin account* - And I see a dropdown for super admin - And I see the username of the admin that I used to load the facility - When I click the super admin dropdown - Then I see a list of facility admins of the facility I loaded - And I see a list of super admins of the device I loaded from - When I select *Create new super admin* - Then I see *This account will be associated with ''* - And I see form fields for *Full name*, *Username*, *Password*, and *Re-enter password* - When I fill in all form fields - And I click *Continue* - Then I see *Import facility - 4 of 4* - - Scenario: Responsibilities as an administrator - Given I am on *Import facility - 4 of 4* - And I see *Responsibilities as an administrator* - When I click *Usage and privacy* - Then I see *Usage and privacy* modal - When I click *Close* - Then I see *Responsibilities as an administrator - When I click *Finish* - Then I see *Welcome to Kolibri* - - Scenario: Streamlined content import after importing facility - Given I have successfully imported a facility during device setup - And I see *Welcome to Kolibri* - And I see a message that I should import channels to the device - And I see a message that reports will not display properly without resources - When I click *Continue* - Then I see *Select a source* - And I see the device that I imported from auto-selected - And I see *Choose another source* - When I click *Continue* - # In case the peer device has only the unlisted channels, make sure that the device setting to allow peers to see them is checked - Then I see *Select channels for import* - And I see that all channels are selected by default - When I click *Import* - Then I see the import task in the task manager - - Examples: - | username | password | device | facility | - | admin | admin | MyDevice | MyFacility | diff --git a/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-quick-start-setup-wizard.feature b/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-quick-start-setup-wizard.feature deleted file mode 100644 index e607ebc78e6..00000000000 --- a/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-quick-start-setup-wizard.feature +++ /dev/null @@ -1,46 +0,0 @@ -Feature: Super admin goes through the 'Quick start' setup wizard - Super admin can configure their device to Quick start setup - - Background: - Given that Kolibri installation was successful - And the server is running for the first time - And the browser is opened at the IP address 127.0.0.1:8080 - - Scenario: Select language - Given that I am at the beginning of the setup wizard - And I see *Please select the default language for Kolibri* - When I click the link *Español* - Then the wizard language changes to Spanish - When I click *More languages* - Then I see the *Change language* modal - When I select *Français* - And click *Confirmar* - Then the modal closes - And I see the wizard language changes to French - When I click the link *English* - Then the wizard language changes to English - When I click *Continue* button - Then I see *Getting started* - - Scenario: Select 'Quick start' - Given I see *How are you using Kolibri?* - When I select *Quick start* - And I click *Continue* - Then I see *Create super admin account* - - Scenario: Super admin account details for personal setup - Given I see *Create super admin account* - And I previously selected *Quick start* - And I don't see the *Usage and privacy* link - When I fill in the full name, username and password fields - And I click *Finish* - Then I see the *Setting up the facility...* page - And I see the *Welcome to Kolibri!* modal - When I click *OK* - Then I see the *Device > Channels* page - - Scenario: Super admin account created in *Setup Wizard* does not see a notification to update profile - Given I completed the Setup Wizard - And I created my super admin account - When I am redirected to *Device > Channels* page - Then I don't see the *Update your profile* modal diff --git a/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-setup-wizard-import-individual-users.feature b/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-setup-wizard-import-individual-users.feature deleted file mode 100644 index 8a581f83948..00000000000 --- a/integration_testing/016-features/superadmin/initial-setup/0.15 EDITS NEEDED/super-admin-setup-wizard-import-individual-users.feature +++ /dev/null @@ -1,152 +0,0 @@ -Feature: Import individual users - - Background: - Given Kolibri is not installed on my device - And the Kolibri installer is downloaded to my device - And Kolibri version 0.15 is installed on another device in my network - And I have a local wi-fi connection - When I install Kolibri - And I open Kolibri in my browser - Then I see *Please select the default language for Kolibri* - When I click *Continue* - Then I see *Getting started* - When I select *Advanced Setup* - And I click *Continue* - Then I see *Device name* - And I see - When I click *Continue* - Then I see *Select a facility setup for this device* - - Scenario: See available *Select a facility setup for this device* page features - Given I see *Select a facility setup for this device* - Then I see the *Full device* section - And I see the radio options *Create a new facility* and *Import all data from an existing facility* - And I see the *Learn-only device* section - And I see the radio option *Import one or more user accounts from an existing facility* - And I see the caption *This device supports auto-syncing with a full device that has the same facility* - When I hover my mouse over the tooltip next to *Full device* - Then I see *Features for learners, coaches, and admins will be available* - When I hover my mouse over the tooltip next to *Learn-only device* - Then I see *Only features for learners will be available. Features for coaches and admins will not be available.* - - Scenario: Import one user when there is only one facility in the network - Given I see *Select a facility setup for this device* - When I select *Import one or more user accounts from an existing facility* - And I click *Continue* - Then I see *Select network address* - And I see *Devices must be installed with Kolibri version 0.15.0* in the modal description - And I see radio options for devices in my network - And I see in the caption of each radio option - When I select - And I click *Continue* - Then I see *Import individual user accounts* - And I see *Import individual user accounts - 1 of 2* in the app bar - When I enter the and of in - And I click *Import* - Then I see *Loading user* - And I see *Import individual user accounts - 2 of 2* in the app bar - And I see a loading bar - When the user finishes importing - Then I see *Finished* - And I see * from successfully loaded to this device* - And I see the buttons *Finish* and *Import another user* - When I click *Finish* - Then I see the *Welcome to Kolibri!* modal on the *Channels* page - And I see that I am signed in as - - Scenario: Import a second user - Given I have finished importing the user to my *Learn-only device* - And I am viewing the *Loading user* page - When I click *Import another user* - Then I see the *Import individual user accounts* page - And I see a *Skip* button next to the *Import* button - When I enter and of - Then I see the *Loading user* page - When the user finishes importing to the device - Then I see *Finished* - And I see from successfully loaded to this device - And I see *On this device* - And I see a list with and - When I click *Finish* - Then I see the *Welcome to Kolibri* modal - And I see that I am signed in as the first user I imported - - Scenario: Change mind while importing a second user - Given I have finished importing the user to my *Learn-only device* - And I am viewing the *Loading user* page - When I click *Import another user* - Then I see the *Import individual user accounts* page - And I see a *Skip* button next to the *Import* button - When I click *Skip* - Then I see the *Welcome to Kolibri!* modal on the *Channels* page - - Scenario: Import coach or admin - Given I am on the *Import individual user accounts* page - When I enter and of a coach or admin user - And I click *Import* - Then I see the *Device limitations* modal - When I click *Import* - Then I see the *Loading user* page - - Scenario: Use an admin account - Given I am on the *Import individual user accounts* page - When I click *Use an admin account* - Then I see *Select a user* - And I see a user text box filter - And I see a user table - And I see the columns *Full name* and *Username* - And I see *Import* buttons on each row - And I see pagination at the bottom of the table - When I click *Import* on - Then I see the *Loading user* page - When finishes importing to the device - Then I see *Finished* - And I see from successfully loaded to this device - When I click *Import another user* - Then I see *Select a user* - And I see the user table - And I see that the row for is grayed out - And I see *Imported* instead of the *Import* button for - And I see a bottom bar with a secondary *Skip* button - When I click *Import* for - Then I see *Loading user* - When finishes importing to the device - Then I see *Finished* - And I see from successfully loaded to this device - And I see *On this device* - And I see a list with and - - Scenario: Change mind while importing a second user while using an admin account - Given I have finished importing the user to my *Learn-only device* - And I imported them using an admin account - When I click *Import another user* - Then I see *Select a user* - And I see a bottom bar with a secondary *Skip* button - When I click *Skip* - Then I see the *Welcome to Kolibri* modal on the *Channels* page - - Scenario: Import coach or admin while *Require password for learners* facility setting is disabled - Given I am viewing *Import individual user accounts* - And has disabled the facility setting *Require password for learners* - Then I see the *Username* text field - And I do not see the *Password* text field - When I enter the username of a coach or admin from - When I click *Import* - Then I see the modal *Enter password* - And I see *Please enter the password for - And I see * () is a on . This device is limited to features for learners only. Features for coaches and admins will not be available.* - And I see a *Password* text field - When I enter for - When I click *Import* - Then I see the *Loading user* page - And I see the user import is in progress - - Scenario: Import coach or admin after using an admin account - Given I am on the *Import individual user accounts* page - When I click *Use an admin account* - Then I see the *Select a user* page - When I click *Import* for - And is a coach or admin - Then I see the *Device limitations* modal - When I click *Import* - Then I see the *Loading user* page diff --git a/integration_testing/016-features/superadmin/initial-setup/device-setup.feature b/integration_testing/016-features/superadmin/initial-setup/device-setup.feature index e8a5c51623c..c60c93da22c 100644 --- a/integration_testing/016-features/superadmin/initial-setup/device-setup.feature +++ b/integration_testing/016-features/superadmin/initial-setup/device-setup.feature @@ -3,7 +3,7 @@ Feature: Device setup Background: Given that the Kolibri installation was successful - Scenario: Load the Kolibri app for the first time during device setup + Scenario: Load the Kolibri app for the first time during device setup #NOT IMPLEMENTED When I open the app for the first time Then I see a static image of the Kolibri logo And I see messages under the logo @@ -15,7 +15,7 @@ Feature: Device setup Scenario: *On my own* setup Given I am using a desktop browser And Kolibri has finished loading after opening it for the first time - When I click *Get started* + When I click *Get started* #this step is not implemented yet Then I see *How are you using Kolibri?* And I see that the checkbox for *On my own* is selected by default When I click *Continue* @@ -38,7 +38,7 @@ Feature: Device setup And I see *Setting up Kolibri* And I see *This may take several minutes* When Kolibri finishes loading - Then I see a modal *Add materials* + Then I see a modal *Welcome to Kolibri!* When I click *Continue* Then I am at *Learn > Library* page @@ -58,10 +58,10 @@ Feature: Device setup Then I am at the *What kind of learning environment is your facility?* page And I see that the *Non-formal* option is selected When I click *Continue* - Then I am at the *Enable guest access?* page + Then I am at the *Enable users to explore Kolibri without an account?* page And I see that the *Yes* option is selected When I click *Continue* - Then I am at the *Allow anyone to create their own learner account?* page + Then I am at the *Allow learners to join this facility?* page And I see that the *Yes* option is selected When I click *Continue* Then I am at the *Enable passwords on learner accounts?* page @@ -75,7 +75,7 @@ Feature: Device setup Then I see the *Setting up Kolibri* page When the setup has finished Then I am at the *Device > Channels* page - And I can see the *Add materials* modal + And I can see the *Welcome to Kolibri!* modal Scenario: Group learning - Full device - Create a formal facility Given I am at the *How are you using Kolibri?* page @@ -96,7 +96,7 @@ Feature: Device setup Then I am at the *Enable guest access?* page And I see that the *No. Users must have an account to view resources on Kolibri* option is selected When I click *Continue* - Then I am at the *Allow anyone to create their own learner account?* page + Then I am at the *Allow learners to join this facility?* page And I see that the *No. Admins must create all accounts* option is selected When I click *Continue* Then I am at the *Enable passwords on learner accounts?* page @@ -110,7 +110,7 @@ Feature: Device setup Then I see the *Setting up Kolibri* page When the setup has finished Then I am at the *Device > Channels* page - And I can see the *Add materials* modal + And I can see the *Welcome to Kolibri!* modal Scenario: Group learning - Full device - Import all data from an existing learning facility Given I am at the *Set up the learning facility for this full device* page @@ -124,7 +124,7 @@ Feature: Device setup When I select a facility And I click *Continue* Then I am at the *Import learning facility - 1 of 4* page - And I see *Import facility* + And I see *Import learning facility* And I see the name of the device from which I am importing And I see the network address of that device And I see *Enter the username and password for a facility admin of '' or a super admin of ''* @@ -136,7 +136,7 @@ Feature: Device setup When the facility has finished loading Then I see the status *Finished* And I see a green check icon - And I see *'' successfully loaded to this device* + And I see *The '' learning facility has been successfully loaded to this device* When I click *Continue* Then I am at the *Import learning facility - 3 of 4* page And I see *Select super admin* @@ -147,11 +147,39 @@ Feature: Device setup Then I am at the *Import facility - 4 of 4* page And I see *Responsibilities as an administrator* And a the *Usage and privacy* link - When I click *Finish* + When I click *Continue* Then I see the *Setting up Kolibri* page When the setup has finished Then I am at the *Device > Channels* page - And I can see the *Add materials* modal + And I can see the *Welcome to Kolibri!* modal + + Scenario: Group learning - Learn-only - Join a facility + Given I am at the *Select a facility setup for this device* page + When I select *Create a new user account for an existing facility* + And I click *Continue* + Then I am at the *Select facility* page #this page is shown only if there's more than 1 facility on the selected device + And I see a list of facilities in my network + And I see *Don't see your facility?* + And I see *Add new address* + When I click *Continue* + Then I am at the page *Create your account* + And I see text fields for *Full name*, *Username*, *Password* and *Re-enter password* + And I see the *Usage and privacy* link + When I fill in *Full name*, *Username*, *Password* and *Re-enter password* + And I click *Continue* + Then I am at the *Load user account* page + And I see a progress bar + When the process is complete + Then I see *'' from successfully loaded to this device* + And I see a green check icon + When I click *Finish* + Then I see *Setting up Kolibri* + And I see *This may take several minutes* + And I see the Kolibri loading icon + When the setup has finished + Then I can see the *Welcome to Kolibri!* modal + When I click *Continue* + Then I am at the *Learn > Library* page Scenario: Group learning - Learn-only - Import individual users Given I am at the *Select a facility setup for this learn-only device* page @@ -178,42 +206,14 @@ Feature: Device setup Then I am at the *Loading user* page And I see a green check icon And I see *'' from successfully loaded to this device* - And I see an *Import another user* link + And I see an *Import another user account* link When I click *Finish* Then I see the *Setting up Kolibri* page When the setup has finished - Then I can see the *Welcome* modal + Then I can see the *Welcome to Kolibri!* modal When I click *Continue* Then I am at the *Learn > Home* page - Scenario: Group learning - Learn-only - Join a facility - Given I am at the *Select a facility setup for this device* page - When I select *Create a new user account for an existing facility* - And I click *Continue* - Then I am at the *Select facility* page #this page is shown only if there's more than 1 facility on the selected device - And I see a list of facilities in my network - And I see *Don't see your facility?* - And I see *Add new address* - When I click *Continue* - Then I am at the page *Create your account* - And I see text fields for *Full name*, *Username*, *Password* and *Re-enter password* - And I see the *Usage and privacy* link - When I fill in *Full name*, *Username*, *Password* and *Re-enter password* - And I click *Continue* - Then I am at the *Load user account* page - And I see a progress bar - When the process is complete - Then I see *'' from successfully loaded to this device* - And I see a green check icon - When I click *Finish* - Then I see *Setting up Kolibri* - And I see *This may take several minutes* - And I see the Kolibri loading icon - When the setup has finished - Then I can see the *Add materials* modal - When I click *Continue* - Then I am at the *Learn > Library* page - Scenario: Group learning - Learn-only - Facility not available in *Join a facility* setup path Given I selected the *Group learning* setup option And I am on the *Select facility* page diff --git a/integration_testing/016-features/superadmin/initial-setup/post-setup.feature b/integration_testing/016-features/superadmin/initial-setup/post-setup.feature index 884b6efef1c..6d5ba750987 100644 --- a/integration_testing/016-features/superadmin/initial-setup/post-setup.feature +++ b/integration_testing/016-features/superadmin/initial-setup/post-setup.feature @@ -4,7 +4,7 @@ Feature: Post-setup onboarding Given that the Kolibri installation was successful And I have completed the device setup - Scenario: Finish *On my own* setup path - first user (super admin) + Scenario: Finish *On my own* setup path - first user (super admin) #NOT IMPLEMENTED Given I've finished the *On my own* setup path as a super admin And I'm connected to the Internet And there are no channels on the device @@ -24,7 +24,7 @@ Feature: Post-setup onboarding Then I am at the *Library* page And I no longer see any inline tips - Scenario: Finish *On my own* setup path - connected to other Kolibri server on local network + Scenario: Finish *On my own* setup path - connected to other Kolibri server on local network #NOT IMPLEMENTED Given I've finished the *On my own* setup path as a super admin And I'm connected to another Kolibri server on the local network When I click on a library card @@ -35,7 +35,7 @@ Feature: Post-setup onboarding When I click *Continue* Then I see the following inline tip for a highlighted card: *Learn more about this learning material and download it to use anytime.* - Scenario: Finish *Learn-only* setup path - Import individual user - connected to the Internet + Scenario: Finish *Learn-only* setup path - Import individual user - connected to the Internet #NOT IMPLEMENTED Given I've finished the *On my own* setup path as a learner And I'm connected to the Internet and another Kolibri server on the local network When I go to the *Library* page @@ -52,7 +52,7 @@ Feature: Post-setup onboarding When I click *Continue* Then I see the following inline tip for a highlighted card: *Learn more about this learning material and download it to use anytime.* - Scenario: Finish *Full device* setup path + Scenario: Finish *Full device* setup path #NOT IMPLEMENTED Given I've selected the *Full device* setup path at *What kind of device is this?* When I finish the setup Then I am redirected to *Device > Channels* @@ -68,11 +68,11 @@ Feature: Post-setup onboarding And I don't have content yet When I go to the *Library* page Then I see a *Your library* label - And I see the following text: *No channels yet. Start exploring libraries around you and find materials to add to your library.* - And I see the following text to the right: *No libraries found around you.* + And I see the following text: *There is nothing in your library yet. Explore libraries around you and start adding materials to your own.* + And I see the following text to the right: *No other libraries around you right now.* And I see a *Refresh* link next to the text - Scenario: First time use - content already on device + Scenario: First time use - content already on device #NOT IMPLEMENTED Given there's content already on device When I finish the setup up path Then I see the following message: *Welcome to [facility name]. Learning materials from your classes can be found on the home page.* diff --git a/kolibri/__init__.py b/kolibri/__init__.py index ba10d90a778..ce4b84a8534 100755 --- a/kolibri/__init__.py +++ b/kolibri/__init__.py @@ -3,10 +3,6 @@ This module is imported in setup.py, so you cannot for instance import a dependency. """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from kolibri.utils import env from kolibri.utils.version import get_version diff --git a/kolibri/__main__.py b/kolibri/__main__.py index 40500886e00..ec5f80a9d40 100644 --- a/kolibri/__main__.py +++ b/kolibri/__main__.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import sys if __name__ == "__main__": diff --git a/kolibri/core/__init__.py b/kolibri/core/__init__.py index 12d7cccb3a3..93bad367500 100644 --- a/kolibri/core/__init__.py +++ b/kolibri/core/__init__.py @@ -2,8 +2,4 @@ enters the docs) """ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - default_app_config = "kolibri.core.apps.KolibriCoreConfig" diff --git a/kolibri/core/analytics/test/test_ping.py b/kolibri/core/analytics/test/test_ping.py index e04a724da3c..1cd87f73ea5 100644 --- a/kolibri/core/analytics/test/test_ping.py +++ b/kolibri/core/analytics/test/test_ping.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import json import zlib diff --git a/kolibri/core/analytics/test/test_utils.py b/kolibri/core/analytics/test/test_utils.py index b9275ec15df..714def16fbc 100644 --- a/kolibri/core/analytics/test/test_utils.py +++ b/kolibri/core/analytics/test/test_utils.py @@ -1,7 +1,4 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - +import base64 import csv import datetime import hashlib @@ -18,7 +15,6 @@ from kolibri.core.analytics.models import PingbackNotification from kolibri.core.analytics.utils import calculate_list_stats from kolibri.core.analytics.utils import create_and_update_notifications -from kolibri.core.analytics.utils import encodestring from kolibri.core.analytics.utils import extract_channel_statistics from kolibri.core.analytics.utils import extract_facility_statistics from kolibri.core.auth.constants import demographics @@ -338,9 +334,9 @@ def test_extract_facility_statistics__soud_hash(self): actual = extract_facility_statistics(facility) users = sorted(self.users, key=lambda u: u.id) user_ids = ":".join([user.id for user in users]) - expected_soud_hash = encodestring(hashlib.md5(user_ids.encode()).digest())[ - :10 - ].decode() + expected_soud_hash = base64.encodebytes( + hashlib.md5(user_ids.encode()).digest() + )[:10].decode() self.assertEqual(expected_soud_hash, actual.pop("sh")) diff --git a/kolibri/core/analytics/utils.py b/kolibri/core/analytics/utils.py index 0d59d13c8c0..d08d5be7298 100644 --- a/kolibri/core/analytics/utils.py +++ b/kolibri/core/analytics/utils.py @@ -4,7 +4,6 @@ import json import logging import math -import sys import requests from dateutil import parser @@ -65,15 +64,6 @@ "registered", ] -if sys.version_info[0] >= 3: - # encodestring is a deprecated alias for - # encodebytes, which was finally removed - # in Python 3.9 - encodestring = base64.encodebytes -else: - # encodebytes does not exist in Python 2.7 - encodestring = base64.encodestring - def calculate_list_stats(data): if data: @@ -236,7 +226,7 @@ def extract_facility_statistics(facility): # fmt: off data = { # facility_id - "fi": encodestring(hashlib.md5(facility.id.encode()).digest())[:10].decode(), + "fi": base64.encodebytes(hashlib.md5(facility.id.encode()).digest())[:10].decode(), # settings "s": settings, # learners_count @@ -300,7 +290,9 @@ def extract_facility_statistics(facility): facility.facilityuser_set.order_by("id").values_list("id", flat=True) ) # soud_hash - data["sh"] = encodestring(hashlib.md5(user_ids.encode()).digest())[:10].decode() + data["sh"] = base64.encodebytes(hashlib.md5(user_ids.encode()).digest())[ + :10 + ].decode() return data diff --git a/kolibri/core/api.py b/kolibri/core/api.py index f169f66c93b..49af5b60eb4 100644 --- a/kolibri/core/api.py +++ b/kolibri/core/api.py @@ -17,7 +17,6 @@ from rest_framework.serializers import ValidationError from rest_framework.status import HTTP_201_CREATED from rest_framework.status import HTTP_503_SERVICE_UNAVAILABLE -from six import string_types from .utils.portal import registerfacility from kolibri.core.auth.models import Facility @@ -76,9 +75,7 @@ def get_default_valid_fields(self, queryset, view, context=None): # All the fields that we have field maps defined for - this only allows for simple mapped fields # where the field is essentially a rename, as we have no good way of doing ordering on a field that # that is doing more complex function based mapping. - mapped_fields = { - v: k for k, v in view.field_map.items() if isinstance(v, string_types) - } + mapped_fields = {v: k for k, v in view.field_map.items() if isinstance(v, str)} # All the fields of the model model_fields = {f.name for f in queryset.model._meta.get_fields()} # Loop through every value in the view's values tuple @@ -109,9 +106,7 @@ def remove_invalid_fields(self, queryset, fields, view, request): to do filtering based on valuesviewset setup """ # We filter the mapped fields to ones that do simple string mappings here, any functional maps are excluded. - mapped_fields = { - k: v for k, v in view.field_map.items() if isinstance(v, string_types) - } + mapped_fields = {k: v for k, v in view.field_map.items() if isinstance(v, str)} valid_fields = [ item[0] for item in self.get_valid_fields(queryset, view, {"request": request}) @@ -167,9 +162,7 @@ def generate_serializer(self): model = getattr(queryset, "model", None) if model is None: return Serializer - mapped_fields = { - v: k for k, v in self.field_map.items() if isinstance(v, string_types) - } + mapped_fields = {v: k for k, v in self.field_map.items() if isinstance(v, str)} fields = [] extra_kwargs = {} for value in self.values: diff --git a/kolibri/core/apps.py b/kolibri/core/apps.py index 4eb359dc8cf..387c2a2599c 100644 --- a/kolibri/core/apps.py +++ b/kolibri/core/apps.py @@ -11,6 +11,7 @@ from django_filters.filters import UUIDFilter from django_filters.rest_framework.filterset import FilterSet +from kolibri.core.errors import RedisConnectionError from kolibri.core.sqlite.pragmas import CONNECTION_PRAGMAS from kolibri.core.sqlite.pragmas import START_PRAGMAS from kolibri.core.sqlite.utils import repair_sqlite_db @@ -108,12 +109,15 @@ def activate_pragmas_on_start(): cursor.execute(START_PRAGMAS) connection.close() - @staticmethod - def check_redis_settings(): + @staticmethod # noqa C901 + def check_redis_settings(): # noqa C901 """ Check that Redis settings are sensible, and use the lower level Redis client to make updates if we are configured to do so, and if we should, otherwise make some logging noise. """ + + from redis.exceptions import ConnectionError + if OPTIONS["Cache"]["CACHE_BACKEND"] != "redis": return config_maxmemory = OPTIONS["Cache"]["CACHE_REDIS_MAXMEMORY"] @@ -161,6 +165,14 @@ def check_redis_settings(): "Problematic Redis settings detected, please see Redis configuration " "documentation for details: https://redis.io/topics/config" ) + + except ConnectionError as e: + logger.warning("Unable to connect to Redis: {}".format(str(e))) + + raise RedisConnectionError( + "Unable to connect to Redis: {}".format(str(e)) + ) from e + except Exception as e: logger.warning("Unable to check Redis settings") logger.warning(e) diff --git a/kolibri/core/assets/src/composables/useUser.js b/kolibri/core/assets/src/composables/useUser.js index 479f2ae81df..a61c4841b4b 100644 --- a/kolibri/core/assets/src/composables/useUser.js +++ b/kolibri/core/assets/src/composables/useUser.js @@ -9,6 +9,7 @@ export default function useUser() { const isAdmin = computed(() => store.getters.isAdmin); const isSuperuser = computed(() => store.getters.isSuperuser); const canManageContent = computed(() => store.getters.canManageContent); + const isAppContext = computed(() => store.getters.isAppContext); return { isLearnerOnlyImport, @@ -18,5 +19,6 @@ export default function useUser() { isAdmin, isSuperuser, canManageContent, + isAppContext, }; } diff --git a/kolibri/core/assets/src/constants.js b/kolibri/core/assets/src/constants.js index f6796739d4e..d6da17ff4d2 100644 --- a/kolibri/core/assets/src/constants.js +++ b/kolibri/core/assets/src/constants.js @@ -150,6 +150,7 @@ export const ERROR_CONSTANTS = { ALREADY_REGISTERED_FOR_COMMUNITY: 'ALREADY_REGISTERED_FOR_COMMUNITY', // 401 error constants INVALID_CREDENTIALS: 'INVALID_CREDENTIALS', + INVALID_USERNAME: 'INVALID_USERNAME', // 404 error constants NOT_FOUND: 'NOT_FOUND', INVALID_KDP_REGISTRATION_TOKEN: 'INVALID_KDP_REGISTRATION_TOKEN', @@ -186,3 +187,10 @@ export const ApplicationTypes = { KOLIBRI: 'kolibri', STUDIO: 'studio', }; + +// aliasing 'informal' to 'personal' since it's how we talk about it +export const Presets = Object.freeze({ + PERSONAL: 'informal', + FORMAL: 'formal', + NONFORMAL: 'nonformal', +}); diff --git a/kolibri/core/assets/src/core-app/client.js b/kolibri/core/assets/src/core-app/client.js index bb238426fc4..7515e6b8ca1 100644 --- a/kolibri/core/assets/src/core-app/client.js +++ b/kolibri/core/assets/src/core-app/client.js @@ -36,7 +36,7 @@ baseClient.interceptors.response.use( // client code is still trying to access data that they would be allowed to see // if they were logged in. if (error.response) { - if (error.response.status === 403) { + if (error.response.status === 403 || error.response.status === 401) { if (!store.state.core.session.id) { // Don't have any session information, so assume that this // page has just been reopened and the session has expired. diff --git a/kolibri/core/assets/src/core-app/index.js b/kolibri/core/assets/src/core-app/index.js index 96548e05ebb..10e451939c4 100644 --- a/kolibri/core/assets/src/core-app/index.js +++ b/kolibri/core/assets/src/core-app/index.js @@ -13,7 +13,7 @@ import Vuex from 'vuex'; import VueCompositionApi from '@vue/composition-api'; import KThemePlugin from 'kolibri-design-system/lib/KThemePlugin'; import heartbeat from 'kolibri.heartbeat'; -import KContentRenderer from '../views/ContentRenderer/KContentRenderer'; +import ContentRenderer from '../views/ContentRenderer'; import initializeTheme from '../styles/initializeTheme'; import { i18nSetup } from '../utils/i18n'; import setupPluginMediator from './pluginMediator'; @@ -64,7 +64,7 @@ Vue.use(VueCompositionApi); // - Register KDS components Vue.use(KThemePlugin); -Vue.component('KContentRenderer', KContentRenderer); +Vue.component('ContentRenderer', ContentRenderer); // Start the heartbeat polling here, as any URL needs should be set by now heartbeat.startPolling(); diff --git a/kolibri/core/assets/src/exams/utils.js b/kolibri/core/assets/src/exams/utils.js index b7eb26d0857..a59f1e571f2 100644 --- a/kolibri/core/assets/src/exams/utils.js +++ b/kolibri/core/assets/src/exams/utils.js @@ -1,5 +1,6 @@ import every from 'lodash/every'; import uniq from 'lodash/uniq'; +import { v4 as uuidv4 } from 'uuid'; import { assessmentMetaDataState } from 'kolibri.coreVue.vuex.mappers'; import { ExamResource, ContentNodeResource } from 'kolibri.resources'; @@ -82,6 +83,51 @@ function annotateQuestionsWithItem(questions) { }); } +/* Given a V2 question_sources, return V3 structure with those questions within one new section */ +/** + * @param {Array} questionSources - a V2 question_sources object + * @param {boolean} learners_see_fixed_order - whether the questions should be randomized or not + * - a V2 quiz will have this value on itself, but a V3 quiz will have it + * on each section, so it should be passed in here + * @returns V3 formatted question_sources + */ +export function convertV2toV3(questionSources, exam) { + questionSources = questionSources || []; // Default value while requiring all params + const questions = annotateQuestionsWithItem(questionSources); + return { + section_id: uuidv4(), + section_title: '', + description: '', + resource_pool: [], + questions, + learners_see_fixed_order: exam.learners_see_fixed_order, + question_count: exam.question_count, + }; +} + +export function revertV3toV2(questionSources) { + if (!questionSources.length) { + return []; + } + return questionSources[0].questions; +} + +/** + * @param {object} exam - an exam object of any question_sources version + * @returns V3 formatted question_sources + */ +export function convertExamQuestionSourcesToV3(exam, extraArgs = {}) { + if (exam.data_model_version !== 3) { + const V2_sources = convertExamQuestionSources(exam, extraArgs); + return [convertV2toV3(V2_sources, exam)]; + } + + return exam.question_sources; +} + +/** + * @returns V2 formatted question_sources + */ export function convertExamQuestionSources(exam, extraArgs = {}) { const { data_model_version } = exam; if (data_model_version === 0) { @@ -107,12 +153,23 @@ export function convertExamQuestionSources(exam, extraArgs = {}) { if (data_model_version === 1) { return annotateQuestionsWithItem(convertExamQuestionSourcesV1V2(exam.question_sources)); } + + // For backwards compatibility. If you are using V3, use the convertExamQuestionSourcesToV3 func + if (data_model_version === 3) { + return revertV3toV2(exam.question_sources); + } + return annotateQuestionsWithItem(exam.question_sources); } export function fetchNodeDataAndConvertExam(exam) { const { data_model_version } = exam; - if (data_model_version >= 2) { + if (data_model_version >= 3) { + /* For backwards compatibility, we need to convert V3 to V2 */ + exam.question_sources = revertV3toV2(exam.question_sources); + return Promise.resolve(exam); + } + if (data_model_version == 2) { exam.question_sources = annotateQuestionsWithItem(exam.question_sources); return Promise.resolve(exam); } diff --git a/kolibri/core/assets/src/mixins/__test__/notificationStrings.spec.js b/kolibri/core/assets/src/mixins/__test__/notificationStrings.spec.js index 0a24536b077..48b0c57e661 100644 --- a/kolibri/core/assets/src/mixins/__test__/notificationStrings.spec.js +++ b/kolibri/core/assets/src/mixins/__test__/notificationStrings.spec.js @@ -25,7 +25,7 @@ describe('Coach Notification Strings', () => { // Test that the rest of the messages don't need paramaters it('The other notification strings do not require params', () => { const paramMsgs = pluralTestCases.map(([key]) => key); - Object.keys(NotificationStrings.defaultMessages).forEach(key => { + Object.keys(NotificationStrings._defaultMessages).forEach(key => { if (paramMsgs.includes(key)) { expect(() => { NotificationStrings.$tr(key); diff --git a/kolibri/core/assets/src/mixins/commonCoreStrings.js b/kolibri/core/assets/src/mixins/commonCoreStrings.js index 1aa24d3812a..a13fff1b6e7 100644 --- a/kolibri/core/assets/src/mixins/commonCoreStrings.js +++ b/kolibri/core/assets/src/mixins/commonCoreStrings.js @@ -1289,28 +1289,17 @@ export const coreStrings = createTranslator('CommonCoreStrings', { }, // Device upgrades recommended - currentDeviceUsingIE11: { - message: 'You seem to be using Internet Explorer 11.', - context: - 'Displayed on a device that is using Internet Explorer 11, as part of a message encouraging the user to upgrade.', - }, userDevicesUsingIE11: { - message: 'Some users seem to be accessing Kolibri via Internet Explorer 11', + message: 'Some users seem to have accessed Kolibri via Internet Explorer 11', context: 'Displayed to an admin, where devices on their network are using Internet Explorer 11, as part of a message encouraging the user to upgrade.', }, - browserSupportWillBeDroppedIE11: { + browserSupportDroppedIE11: { message: - 'Please note that support for this browser will be dropped in the upcoming version, 0.17. We recommend installing other browsers, such as Mozilla Firefox or Google Chrome, in order to continue working with Kolibri.', + 'Please note that support for this browser has been dropped. We recommend installing other browsers, such as Mozilla Firefox or Google Chrome, in order to continue working with Kolibri.', context: 'Displayed to users of kolibri where one or more devices on the network are using Internet Explorer 11, as part of a message encouraging the user to upgrade.', }, - pythonSupportWillBeDropped: { - message: - 'Please note that support for Python 2.7 will be dropped in the upcoming version 0.17. Upgrade your Python version to Python 3.7+ to continue working with Kolibri. More recent versions of Python 3 are recommended.', - context: - 'Displayed to users of kolibri where one or more devices on the network are using Python 2.7, as part of a message encouraging the user to upgrade.', - }, // Content activity notStartedLabel: { diff --git a/kolibri/core/assets/src/mixins/notificationStrings.js b/kolibri/core/assets/src/mixins/notificationStrings.js index aa8c6420f3e..b6106798f15 100644 --- a/kolibri/core/assets/src/mixins/notificationStrings.js +++ b/kolibri/core/assets/src/mixins/notificationStrings.js @@ -139,6 +139,10 @@ export default createTranslator('NotificationStrings', { message: 'Device not removed', context: 'Snackbar message when a device fails to be removed from he sync schedule', }, + newLearningFacilityCreated: { + message: 'New learning facility created', + context: 'Snackbar message when a new facility created', + }, // TODO move more messages into this namespace: // - "Quiz started" // - "Quiz Ended" diff --git a/kolibri/core/assets/src/utils/browserInfo.js b/kolibri/core/assets/src/utils/browserInfo.js index 412b0d92498..f78e728bc71 100644 --- a/kolibri/core/assets/src/utils/browserInfo.js +++ b/kolibri/core/assets/src/utils/browserInfo.js @@ -64,30 +64,6 @@ export const os = { patch: osVersion[2], }; -/** - * Detection of whether an Android device is using WebView based on - * https://developer.chrome.com/multidevice/user-agent#webview_user_agent - * First checks for 'wv' (Lolipop+), then for 'Version/x.x' - */ -const isAndroid = os.name === 'Android'; -export const isAndroidWebView = - isAndroid && - (browser.name === 'Chrome Webview' || - (browser.name === 'Chrome' && /Version\/\d+\.\d+/.test(userAgent))); - -/** - * Embedded WebViews on Mac have no app identifier, while all the major browsers do, so check - * for browser app strings and mark as embedded if none are found. - */ -const isMac = os.name === 'Mac OS'; -export const isMacWebView = - isMac && !(/Safari/.test(userAgent) || /Chrome/.test(userAgent) || /Firefox/.test(userAgent)); - -/** - * All web views - */ -export const isEmbeddedWebView = isAndroidWebView || isMacWebView; - // Check for presence of the touch event in DOM or multi-touch capabilities export const isTouchDevice = 'ontouchstart' in window || diff --git a/kolibri/core/assets/src/utils/i18n.js b/kolibri/core/assets/src/utils/i18n.js index 989875440ce..025ea0bed97 100644 --- a/kolibri/core/assets/src/utils/i18n.js +++ b/kolibri/core/assets/src/utils/i18n.js @@ -132,13 +132,16 @@ class Translator { * @param {object} defaultMessages - an object mapping message ids to default messages. */ constructor(nameSpace, defaultMessages) { - this.nameSpace = nameSpace; - this.defaultMessages = defaultMessages; + this._nameSpace = nameSpace; + this._defaultMessages = defaultMessages; + for (const key in defaultMessages) { + this[`${key}$`] = this.$tr.bind(this, key); + } } $tr(messageId, args) { return $trWrapper( - this.nameSpace, - this.defaultMessages, + this._nameSpace, + this._defaultMessages, Vue.prototype.$formatMessage, messageId, args diff --git a/kolibri/core/assets/src/utils/minimumBrowserRequirements.js b/kolibri/core/assets/src/utils/minimumBrowserRequirements.js index b5f470d8822..7592c3e7cba 100644 --- a/kolibri/core/assets/src/utils/minimumBrowserRequirements.js +++ b/kolibri/core/assets/src/utils/minimumBrowserRequirements.js @@ -1,16 +1,41 @@ +import isUndefined from 'lodash/isUndefined'; +import browsers from 'browserslist-config-kolibri'; import { browser, passesRequirements } from './browserInfo'; import plugin_data from 'plugin_data'; -const minimumBrowserRequirements = { - IE: { - major: 11, - }, - Android: { - major: 4, - minor: 0, - patch: 2, - }, -}; +const minimumBrowserRequirements = {}; + +const browserRegex = /^([a-zA-Z]+) ([><=]+) (\d+)(?:\.(\d+))?(?:\.(\d+))?$/; + +for (const browser of browsers) { + const [name, sign, major, minor, patch] = browserRegex.exec(browser).slice(1); + if (sign !== '>' && sign !== '>=') { + throw new Error('Unsupported browser requirement'); + } + + // This only supports > and >=, but that's all we need. + // In the case that it is > then we will need to add one to the version number + // we will add one to the smallest defined version number out of major, minor, patch + const addOne = sign === '>'; + const entry = { + major: Number(major), + }; + let valueToIncrement = 'major'; + if (!isUndefined(minor)) { + entry.minor = Number(minor); + valueToIncrement = 'minor'; + // We only check for patch if we have a minor version number + // as it is not possible to be defined without a minor version number + if (!isUndefined(patch)) { + entry.patch = Number(patch); + valueToIncrement = 'patch'; + } + } + if (addOne) { + entry[valueToIncrement] += 1; + } + minimumBrowserRequirements[name] = entry; +} if (!passesRequirements(browser, minimumBrowserRequirements)) { window.location.href = plugin_data.unsupportedUrl; diff --git a/kolibri/core/assets/src/views/AttemptLogList.vue b/kolibri/core/assets/src/views/AttemptLogList.vue index 0a9941aa3a0..132cfa60772 100644 --- a/kolibri/core/assets/src/views/AttemptLogList.vue +++ b/kolibri/core/assets/src/views/AttemptLogList.vue @@ -76,7 +76,6 @@ diff --git a/kolibri/core/assets/src/views/ContentRenderer/DownloadButton.vue b/kolibri/core/assets/src/views/ContentRenderer/DownloadButton.vue index 7429efb3e5a..7a7283e83eb 100644 --- a/kolibri/core/assets/src/views/ContentRenderer/DownloadButton.vue +++ b/kolibri/core/assets/src/views/ContentRenderer/DownloadButton.vue @@ -20,12 +20,19 @@ + + + diff --git a/kolibri/core/assets/src/views/sync/FacilityTaskPanelDetails.vue b/kolibri/core/assets/src/views/sync/FacilityTaskPanelDetails.vue index 8edfc825169..04d0b7f067e 100644 --- a/kolibri/core/assets/src/views/sync/FacilityTaskPanelDetails.vue +++ b/kolibri/core/assets/src/views/sync/FacilityTaskPanelDetails.vue @@ -105,14 +105,20 @@ diff --git a/kolibri/plugins/coach/assets/src/views/home/HomePage/index.vue b/kolibri/plugins/coach/assets/src/views/home/HomePage/index.vue index 2eadf0d6f88..68d0bea907d 100644 --- a/kolibri/plugins/coach/assets/src/views/home/HomePage/index.vue +++ b/kolibri/plugins/coach/assets/src/views/home/HomePage/index.vue @@ -3,7 +3,6 @@ diff --git a/kolibri/plugins/coach/assets/src/views/plan/CoachExamsPage/index.vue b/kolibri/plugins/coach/assets/src/views/plan/CoachExamsPage/index.vue index 300c607ce8e..02b1ad638e4 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/CoachExamsPage/index.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/CoachExamsPage/index.vue @@ -3,7 +3,6 @@ diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/AccordionContainer.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/AccordionContainer.vue new file mode 100644 index 00000000000..847ad06075d --- /dev/null +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/AccordionContainer.vue @@ -0,0 +1,148 @@ + + + + + + + diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/AccordionItem.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/AccordionItem.vue new file mode 100644 index 00000000000..4758f2f2076 --- /dev/null +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/AccordionItem.vue @@ -0,0 +1,48 @@ + + + + + + + diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreateExamPreview.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreateExamPreview.vue index 7a35e7128c8..be1d0f70071 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreateExamPreview.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreateExamPreview.vue @@ -125,7 +125,6 @@ import { mapState } from 'vuex'; import BottomAppBar from 'kolibri.coreVue.components.BottomAppBar'; - import responsiveWindowMixin from 'kolibri.coreVue.mixins.responsiveWindowMixin'; import { ERROR_CONSTANTS } from 'kolibri.coreVue.vuex.constants'; import CatchErrors from 'kolibri.utils.CatchErrors'; import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; @@ -141,7 +140,7 @@ CoachImmersivePage, QuestionListPreview, }, - mixins: [responsiveWindowMixin, commonCoach, commonCoreStrings], + mixins: [commonCoach, commonCoreStrings], data() { return { showError: false, diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreatePracticeQuizPage.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreatePracticeQuizPage.vue index 1dafc0d0864..cae42e01ac6 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreatePracticeQuizPage.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreatePracticeQuizPage.vue @@ -41,7 +41,6 @@ import { mapState } from 'vuex'; import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants'; - import responsiveWindowMixin from 'kolibri.coreVue.mixins.responsiveWindowMixin'; import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; import ResourceSelectionBreadcrumbs from '../../plan/LessonResourceSelectionPage/SearchTools/ResourceSelectionBreadcrumbs'; import { PageNames } from '../../../constants'; @@ -56,7 +55,7 @@ ContentCardList, ResourceSelectionBreadcrumbs, }, - mixins: [commonCoreStrings, commonCoach, responsiveWindowMixin], + mixins: [commonCoreStrings, commonCoach], data() { return { viewMoreButtonState: 'no_more_results', diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreateQuizSection.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreateQuizSection.vue new file mode 100644 index 00000000000..f00f37f897f --- /dev/null +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/CreateQuizSection.vue @@ -0,0 +1,785 @@ + + + + + + + diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/QuestionListPreview.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/QuestionListPreview.vue index 724beb9ec6f..42b4155425a 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/QuestionListPreview.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/QuestionListPreview.vue @@ -70,7 +70,7 @@

{{ currentQuestion.title }}

- + +
+

Replace questions

+

+ {{ JSON.stringify(question) }} +

+

+ + Select resources + +

+
+ + + + + diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ResourceSelection.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ResourceSelection.vue new file mode 100644 index 00000000000..5d67bbe716a --- /dev/null +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/ResourceSelection.vue @@ -0,0 +1,425 @@ + + + + + + + diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/SectionEditor.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/SectionEditor.vue new file mode 100644 index 00000000000..378ae423dc6 --- /dev/null +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/SectionEditor.vue @@ -0,0 +1,437 @@ + + + + + + + diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/SectionSidePanel.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/SectionSidePanel.vue new file mode 100644 index 00000000000..20b31d9f655 --- /dev/null +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/SectionSidePanel.vue @@ -0,0 +1,96 @@ + + + + diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/TabsWithOverflow.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/TabsWithOverflow.vue new file mode 100644 index 00000000000..8375fa0dbc8 --- /dev/null +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/TabsWithOverflow.vue @@ -0,0 +1,134 @@ + + + + + + + diff --git a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/index.vue b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/index.vue index f8b1f697457..5a8b7549d08 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/index.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/CreateExamPage/index.vue @@ -6,176 +6,34 @@ authorizedRole="adminOrCoach" icon="close" :pageTitle="$tr('createNewExamLabel')" - :route="toolbarRoute" + :route="backRoute" > + + {{ selectionIsInvalidText }} + - + -

{{ $tr('createNewExamLabel') }}

+ - - {{ selectionIsInvalidText }} - - -

{{ coachString('detailsLabel') }}

- - - - - - - - - - - - - - - - -

{{ $tr('chooseExercises') }}

-
- - - - -
-
-

- {{ coreString('selectFromBookmarks') }} -

- -
- -
-

{{ coreString('bookmarksLabel') }}

-

{{ $tr('resources', { count: bookmarksCount }) }}

-
-
-
-
- -
- - - - -

{{ topicTitle }}

-

{{ topicDescription }}

- -
- - - - +
+ @@ -183,42 +41,34 @@ - + diff --git a/kolibri/plugins/coach/assets/src/views/plan/GroupEnrollPage/index.vue b/kolibri/plugins/coach/assets/src/views/plan/GroupEnrollPage/index.vue index 3df95c218f5..ae514a3bafb 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/GroupEnrollPage/index.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/GroupEnrollPage/index.vue @@ -79,7 +79,6 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import differenceWith from 'lodash/differenceWith'; - import responsiveWindowMixin from 'kolibri.coreVue.mixins.responsiveWindowMixin'; import FilterTextbox from 'kolibri.coreVue.components.FilterTextbox'; import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; import filterUsersByNames from 'kolibri.utils.filterUsersByNames'; @@ -94,7 +93,7 @@ FilterTextbox, UserTable, }, - mixins: [responsiveWindowMixin, commonCoach, commonCoreStrings], + mixins: [commonCoach, commonCoreStrings], data() { return { filterInput: '', diff --git a/kolibri/plugins/coach/assets/src/views/plan/GroupMembersPage/index.vue b/kolibri/plugins/coach/assets/src/views/plan/GroupMembersPage/index.vue index 7a0430145c0..57791b56621 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/GroupMembersPage/index.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/GroupMembersPage/index.vue @@ -3,7 +3,6 @@

diff --git a/kolibri/plugins/coach/assets/src/views/plan/GroupsPage/GroupRow.vue b/kolibri/plugins/coach/assets/src/views/plan/GroupsPage/GroupRow.vue index 7f751e52da2..d94499ebfce 100644 --- a/kolibri/plugins/coach/assets/src/views/plan/GroupsPage/GroupRow.vue +++ b/kolibri/plugins/coach/assets/src/views/plan/GroupsPage/GroupRow.vue @@ -32,13 +32,12 @@ diff --git a/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerHeader.vue b/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerHeader.vue index 1c26730f0d1..14799c46980 100644 --- a/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerHeader.vue +++ b/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerHeader.vue @@ -53,13 +53,13 @@ - - @@ -71,16 +71,34 @@ import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; import commonCoach from '../common'; + import { LEARNERS_TABS_ID, LearnersTabs } from '../../constants/tabsConstants'; + import { useCoachTabs } from '../../composables/useCoachTabs'; export default { name: 'ReportsLearnerHeader', mixins: [commonCoach, commonCoreStrings], + setup() { + const { saveTabsClick, wereTabsClickedRecently } = useCoachTabs(); + return { + saveTabsClick, + wereTabsClickedRecently, + }; + }, props: { enablePrint: { type: Boolean, required: false, default: false, }, + activeTabId: { + type: String, + required: true, + }, + }, + data() { + return { + LEARNERS_TABS_ID, + }; }, computed: { learner() { @@ -115,6 +133,32 @@ ); return statuses.length; }, + tabs() { + return [ + { + id: LearnersTabs.REPORTS, + label: this.coachString('reportsLabel'), + to: this.classRoute('ReportsLearnerReportPage', {}), + }, + { + id: LearnersTabs.ACTIVITY, + label: this.coachString('activityLabel'), + to: this.classRoute('ReportsLearnerActivityPage', {}), + }, + ]; + }, + }, + mounted() { + // focus the active tab but only when it's likely + // that this header was re-mounted as a result + // of navigation after clicking a tab (focus shouldn't + // be manipulated programatically in other cases, e.g. + // when visiting the Plan page for the first time) + if (this.wereTabsClickedRecently(this.LEARNERS_TABS_ID)) { + this.$nextTick(() => { + this.$refs.tabList.focusActiveTab(); + }); + } }, $trs: { back: { @@ -122,6 +166,10 @@ context: "Link that takes user back to the list of learners on the 'Reports' tab, from the individual learner's information page.", }, + reportLearners: { + message: 'Report learners', + context: 'Labels the Reports > Learners tab for screen reander users', + }, }, }; diff --git a/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerListPage.vue b/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerListPage.vue index 11df96898b5..80d99b5d53e 100644 --- a/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerListPage.vue +++ b/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerListPage.vue @@ -3,7 +3,6 @@ diff --git a/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerReportLessonPage.vue b/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerReportLessonPage.vue index 05679417d85..0e74b56ceb7 100644 --- a/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerReportLessonPage.vue +++ b/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerReportLessonPage.vue @@ -3,7 +3,6 @@ diff --git a/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerReportPage.vue b/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerReportPage.vue index 6384283dafe..9923de4cfc4 100644 --- a/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerReportPage.vue +++ b/kolibri/plugins/coach/assets/src/views/reports/ReportsLearnerReportPage.vue @@ -3,89 +3,92 @@ - - - - - - -

{{ coachString('lessonsAssignedLabel') }}

- - - + + +
+ +
@@ -98,6 +101,7 @@ import commonCoach from '../common'; import CoachAppBarPage from '../CoachAppBarPage'; import { PageNames } from '../../constants'; + import { LEARNERS_TABS_ID, LearnersTabs } from '../../constants/tabsConstants'; import ReportsLearnerHeader from './ReportsLearnerHeader'; import ReportsControls from './ReportsControls'; @@ -109,6 +113,12 @@ ReportsControls, }, mixins: [commonCoach, commonCoreStrings], + data() { + return { + LEARNERS_TABS_ID, + LearnersTabs, + }; + }, computed: { learner() { return this.learnerMap[this.$route.params.learnerId]; diff --git a/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonBase.vue b/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonBase.vue index 39517b91105..e97599cbedb 100644 --- a/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonBase.vue +++ b/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonBase.vue @@ -3,7 +3,6 @@ diff --git a/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonExerciseLearnerListPage.vue b/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonExerciseLearnerListPage.vue index 4c9be7835ed..a9455fc0868 100644 --- a/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonExerciseLearnerListPage.vue +++ b/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonExerciseLearnerListPage.vue @@ -3,7 +3,6 @@ diff --git a/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonLearnerBase.vue b/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonLearnerBase.vue index bc71d28ea53..550a2d625b8 100644 --- a/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonLearnerBase.vue +++ b/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonLearnerBase.vue @@ -3,7 +3,6 @@ diff --git a/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonListPage.vue b/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonListPage.vue index 4e2f5fc5ef3..bbc6f5728ac 100644 --- a/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonListPage.vue +++ b/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonListPage.vue @@ -3,7 +3,6 @@ diff --git a/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonManagerPage.vue b/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonManagerPage.vue index f2631b7e190..b91b68d98e7 100644 --- a/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonManagerPage.vue +++ b/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonManagerPage.vue @@ -3,7 +3,6 @@ diff --git a/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonResourceLearnerListPage.vue b/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonResourceLearnerListPage.vue index b020a2f5cd6..b68d6228d92 100644 --- a/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonResourceLearnerListPage.vue +++ b/kolibri/plugins/coach/assets/src/views/reports/ReportsLessonResourceLearnerListPage.vue @@ -3,7 +3,6 @@ diff --git a/kolibri/plugins/coach/assets/src/views/reports/ReportsQuizBaseListPage.vue b/kolibri/plugins/coach/assets/src/views/reports/ReportsQuizBaseListPage.vue index 40284ec53a4..4984824c25c 100644 --- a/kolibri/plugins/coach/assets/src/views/reports/ReportsQuizBaseListPage.vue +++ b/kolibri/plugins/coach/assets/src/views/reports/ReportsQuizBaseListPage.vue @@ -3,7 +3,6 @@ diff --git a/kolibri/plugins/coach/assets/src/views/reports/ReportsQuizListPage.vue b/kolibri/plugins/coach/assets/src/views/reports/ReportsQuizListPage.vue index e82436c1335..7edcb25ac1f 100644 --- a/kolibri/plugins/coach/assets/src/views/reports/ReportsQuizListPage.vue +++ b/kolibri/plugins/coach/assets/src/views/reports/ReportsQuizListPage.vue @@ -3,7 +3,6 @@ diff --git a/kolibri/plugins/coach/assets/test/showPageActions.spec.js b/kolibri/plugins/coach/assets/test/showPageActions.spec.js index a8aa3ad9c98..f1a2617cd96 100644 --- a/kolibri/plugins/coach/assets/test/showPageActions.spec.js +++ b/kolibri/plugins/coach/assets/test/showPageActions.spec.js @@ -327,6 +327,7 @@ fakeExamState.forEach(fakeExam => { }); }); +// Tests are disabled. See https://github.com/learningequality/kolibri/issues/11615 describe('showPage actions for coach exams section', () => { let store; @@ -338,7 +339,7 @@ describe('showPage actions for coach exams section', () => { }); describe('showExamsPage', () => { - it('store is properly set up when there are no problems', async () => { + xit('store is properly set up when there are no problems', async () => { ClassroomResource.fetchCollection.mockResolvedValue(fakeItems); ExamResource.fetchCollection.mockResolvedValue(fakeExams); @@ -356,7 +357,7 @@ describe('showPage actions for coach exams section', () => { }); }); - it('store is properly set up when there are errors', async () => { + xit('store is properly set up when there are errors', async () => { ClassroomResource.fetchCollection.mockResolvedValue(fakeItems); ExamResource.fetchCollection.mockRejectedValue('channel error'); try { diff --git a/kolibri/plugins/coach/assets/test/useFetchTree.fixtures.js b/kolibri/plugins/coach/assets/test/useFetchTree.fixtures.js new file mode 100644 index 00000000000..15e6aea4423 --- /dev/null +++ b/kolibri/plugins/coach/assets/test/useFetchTree.fixtures.js @@ -0,0 +1,4719 @@ +/** + * Generated by copying the response from the fetchTree API endpoint by way of Quiz Creation's + * resource selection workflow. + * + * For a convenient way to generate a response including "more" - use this channel token: + * tigil-fajod + * + * Note that all fixtures here are referring that channel. The "without more" fixture is the first + * page of results, which only includes a single topic, and therefore no "more" object. + */ + +export const fetchTreeTopicResponseWithMore = { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + author: '', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '80423ac7fd9d4dd2987214fbd49e68ea', + description: '', + kind: 'topic', + license_description: null, + license_name: null, + license_owner: '', + num_coach_contents: 0, + options: {}, + parent: '40581e004acf482d86042f852adb1985', + sort_order: 1.0, + title: 'One Topic', + lft: 2, + rght: 57, + tree_id: 4, + learning_activities: [], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + ], + admin_imported: false, + assessmentmetadata: null, + tags: [], + files: [], + thumbnail: null, + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: false, + children: { + results: [ + { + id: 'c6516394603a49f9bf35eedc2e9f586a', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: 'fc3968b0c38f54a4b8b8b42ba6b44730', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Selecting the Incorrect Statement Practice', + lft: 3, + rght: 4, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'aea2eb60d34d58e6b752cbe6aa547679', + '1c068d60ac705be8919e5c987ecf1a8d', + '1e5f07a4d8aa5105990a3de45eb8e957', + 'bb2cc6a375ee5be285ea2105dee60ab1', + '30bd631c3705558e8838edb7645c4b36', + 'b69a76cb42865db1bb13c9be255a4472', + '18eec6deed2252d4b890c1e414cf0697', + 'e2a478a9f8635fe785b4d163954a9570', + '3c07be3012fb5167aee524b8a3f29d1c', + '127d6916d0535b3999d761b057993c26', + '505af15982525524ba0c78b1f0351d6f', + ], + number_of_assessments: 11, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'c6516394603a49f9bf35eedc2e9f586a', + }, + tags: [], + files: [ + { + id: 'a418d01678024981aa8bf3891ebfddc0', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '01143cf2bcbfcfc91cbb7b1c63583873', + available: true, + file_size: 24422, + extension: 'perseus', + storage_url: '/content/storage/0/1/01143cf2bcbfcfc91cbb7b1c63583873.perseus', + }, + { + id: '5356eff16b7d41f1948fff165b715c73', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '9af49c7fb61c4401a780618d39cbad1b', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: 'b5e61e4231f55c29982a1813ee8c817c', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Factors - World Problems Practice', + lft: 5, + rght: 6, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'd07ddfaa68c65a1e8d2e1b1058fcb7e3', + 'f6df88f00fd158f582701f941ead799e', + '488d0510fcf150dfb9d1d55156712303', + 'bf0552397e585ecca5734e308a579a55', + '92eb15278664568690c0e200061ba99f', + '3f43bc5b8acd5124aed0be08048aa721', + '2fb047e1486e5e788fe8d713860c5b10', + 'a5e562871d965a38aaa8e3758e1f211c', + 'ed4947a44faa5a258b5f49b2a56bd9f1', + 'ec5b2b02be0557f99e104d4225db32d3', + '551bfe97f82e5ede9cc82c75ec62fd4f', + '4945bd5d15ec5dda8b8dafb492de4e4a', + '8b07b21cd3b75919a4167b1c3b8ed44c', + '818b2fe981d25cd3a0b2ae4282211e0d', + '0eed52d0b7dc5732bb6b0a6cc018c8bd', + '8e8855776cd45fab9cd726f451356d35', + '3355cf7568ed55de961eb4d5ea026bbe', + '506163732f5453f2b32f5c2cd5fd0ce5', + 'bc11fad066155864ae4eaa2f665be78b', + 'ade8dabaafe355e3912aec26e0599850', + '3d0b6b64ef515a81a8b71ce5827d846d', + '08a82e402e2053b5a779b9970e2531f9', + 'c99049ade75d558fb9761c30fbcd8699', + '1dc561bfc0fe5ceeb4bc3d412a9f46e3', + '2a04d838ff3b5f3bbc5f42f1c452d4cd', + '82f6f70c4a5b553bbfb159fba2bd6063', + '25769b2201a35d91a843c3e3aa114f4c', + '617afb3f92195a03ae59773e8d2ec4ba', + '45f8d940e13a57f79d65320d2fea622c', + '4a63a3932a395cf5a049f997fcae0660', + '39a61c0b0caf52ec86fb0bc9d7d950b7', + 'c2abe2296b445fb5a644fee9d0b0fc7e', + '1938b598130f59c398d7f4a0cdf06fe1', + 'b76765d464035bd795cec91347b5d171', + '69271599d6945156bd865a227d2f4153', + 'd9a1602115c75ffd95471dbee7b46498', + ], + number_of_assessments: 36, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '9af49c7fb61c4401a780618d39cbad1b', + }, + tags: [], + files: [ + { + id: 'ef8dd48f45e1452c9953977415ab3e48', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '6e211de9f83d9164b671d63c0a6798b1', + available: true, + file_size: 70372, + extension: 'perseus', + storage_url: '/content/storage/6/e/6e211de9f83d9164b671d63c0a6798b1.perseus', + }, + { + id: '11e043db50744708b3875bcbb4776b88', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: 'c8e249a7530c4e41996331e77b9ae80c', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: 'fe38b6886cc057f3a2046188518171ff', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Making an Even Group - Word Problems Practice', + lft: 7, + rght: 8, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '932d0d9c67165330a6ea7d6f5f9af056', + '3a8ac435fde5589fb52dd7b2126bb474', + '4b6fcfbaa5a053c695837143fbc371ff', + 'e29413708add5729906ae41346597c39', + '909a0170a54c5943852f773f729cc5e9', + 'ae759bfd92e95599bc1e16b69f942656', + 'e30eacdca24e520fb39584a3b66fd164', + '92a562a20ebf57c6878a42e6343d50ed', + 'a976809128a05a19a7c40411fce9d2bb', + '04e5c83219a651f88393d8524393f20c', + ], + number_of_assessments: 10, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'c8e249a7530c4e41996331e77b9ae80c', + }, + tags: [], + files: [ + { + id: '8e3e4d121eaa4f2e86fe3fdb2964b174', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'f39a618516d35e5ff134ed40b2e5e7e1', + available: true, + file_size: 20477, + extension: 'perseus', + storage_url: '/content/storage/f/3/f39a618516d35e5ff134ed40b2e5e7e1.perseus', + }, + { + id: 'f826a3492bd44e19b8316fb8195ad6d0', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: 'ce4697e5aba744b28dfcaae738855533', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: 'fe38b6886cc057f3a2046188518171ff', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Making an Even Group - Word Problems Practice', + lft: 9, + rght: 10, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '932d0d9c67165330a6ea7d6f5f9af056', + '3a8ac435fde5589fb52dd7b2126bb474', + '4b6fcfbaa5a053c695837143fbc371ff', + 'e29413708add5729906ae41346597c39', + '909a0170a54c5943852f773f729cc5e9', + 'ae759bfd92e95599bc1e16b69f942656', + 'e30eacdca24e520fb39584a3b66fd164', + '92a562a20ebf57c6878a42e6343d50ed', + 'a976809128a05a19a7c40411fce9d2bb', + '04e5c83219a651f88393d8524393f20c', + ], + number_of_assessments: 10, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'ce4697e5aba744b28dfcaae738855533', + }, + tags: [], + files: [ + { + id: '23f5d85a00c24ef49e23ab8b8ca6f0ce', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'f39a618516d35e5ff134ed40b2e5e7e1', + available: true, + file_size: 20477, + extension: 'perseus', + storage_url: '/content/storage/f/3/f39a618516d35e5ff134ed40b2e5e7e1.perseus', + }, + { + id: '05f9c892f8c5443685c12fcc9365ea56', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '55af18c1f4aa43bd82be805388b01401', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '86fd9fd03feb5f2f803b4d8739e790ea', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Finding the Total Amount - Word Problems Practice', + lft: 11, + rght: 12, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'ea7eb8786b91557c94ed4a42d1adbd36', + 'f7f0d2a78fd659569fac5e7caab6544f', + '12f0f5f2c4285987944452426a6cd711', + '77491566c0c85a14b480e768fcd6a3cb', + 'f67266af98395de68f5dbff1c3964baf', + 'fa4ae6d46c285036943b4dfb6956ec05', + 'a86db5371d745c58954b146cc7219101', + '5c22c382ae9a5a01afb7539764015992', + '2d9432c9235458a4b58fd77610a967bf', + '01d4b7ee9a5c5828b3ef04675a6d489d', + ], + number_of_assessments: 10, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '55af18c1f4aa43bd82be805388b01401', + }, + tags: [], + files: [ + { + id: '513ec1ca134c4c85a1a09e54144d17c7', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'd557f4df1d938c076a84aa54a33a9e52', + available: true, + file_size: 21893, + extension: 'perseus', + storage_url: '/content/storage/d/5/d557f4df1d938c076a84aa54a33a9e52.perseus', + }, + { + id: 'cf1b8cd9d05a4b46967900909f68746e', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '0d1a02a783574673b33e080a228953f7', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '10ac3735b3c75f2c9fb67e9f6c29c7ba', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Multiple Units and Multiplication - Word Problems Practice', + lft: 13, + rght: 14, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'e0f76575028054d2bb6cc4165e470446', + '7a1329a137eb5293b00c315587de6f3c', + '02197bea43555ce496c285ad2c581e55', + '2da55cc88e3f5c80aca867aa203f8ea1', + '6256d4ee28b6509dbabaf91849f542b5', + '879e5535fbb45d39bc66bde76fbe40db', + 'c34f1b4ef4df547691cea110c9f76478', + '8ac2bfa7976f55f4b96a9b7b26f9279f', + 'f1344e86d72e5d978ce827721d94ee1f', + '5b17ff9f9de45771a2665de7f30f38da', + '5414db78b11a57f0aecc53eb7acbf36d', + 'b81607aac8d85bbeb17b9d297b569c4b', + '234a9a9dc6d55047b52c6dce58218b4f', + 'e3667031820d5b83a75fb3719d59251f', + 'edbca2581f0a5aceb9b18c6a6f4159a2', + ], + number_of_assessments: 15, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '0d1a02a783574673b33e080a228953f7', + }, + tags: [], + files: [ + { + id: 'd3a62806e1d1425c971f1d5fc41d931e', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '08f26d4fd47ef64d71268bd0a7caca05', + available: true, + file_size: 31318, + extension: 'perseus', + storage_url: '/content/storage/0/8/08f26d4fd47ef64d71268bd0a7caca05.perseus', + }, + { + id: '2f6d9b1e1c2c4ddbb808f37457fa62f9', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: 'a2da2013ade04d11bd281d6cb428d953', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '303834931d7a5db58adc3fce6168b5e3', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Dividing then Multiply to Find the New Amount - Word Problems Practice', + lft: 15, + rght: 16, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '291775e8acbf5be389c3e0a81a0b7e5a', + '13500ec108935269b130cf477e4dd9f3', + 'caad7e4a35d4541db5acaac69510ebde', + '7d4033651cca5d18a53e727d74394391', + 'ec5ac189b47750348505a2483f6c81f2', + 'cb3c3f6c920857359525231e46c7ba18', + 'd38e19f652fd5601b3f3aad4271d053e', + '63f4cd6296985364b6e2eed6dcb8d1ee', + 'a34c500386f4586cb621a81ee4277280', + '1b9c1e26c85e5e4c984085be03832956', + ], + number_of_assessments: 10, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'a2da2013ade04d11bd281d6cb428d953', + }, + tags: [], + files: [ + { + id: '335c4296be0f4e77a5223984ff9bed69', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '271b2c845745550e67527dd3f99379a4', + available: true, + file_size: 20036, + extension: 'perseus', + storage_url: '/content/storage/2/7/271b2c845745550e67527dd3f99379a4.perseus', + }, + { + id: '2f0f304074344ea9a432f9884e72b856', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '1fef5a0d7ae34be0abf29c6fe6eeb367', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '303834931d7a5db58adc3fce6168b5e3', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Dividing then Multiply to Find the New Amount - Word Problems Practice', + lft: 17, + rght: 18, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '291775e8acbf5be389c3e0a81a0b7e5a', + '13500ec108935269b130cf477e4dd9f3', + 'caad7e4a35d4541db5acaac69510ebde', + '7d4033651cca5d18a53e727d74394391', + 'ec5ac189b47750348505a2483f6c81f2', + 'cb3c3f6c920857359525231e46c7ba18', + 'd38e19f652fd5601b3f3aad4271d053e', + '63f4cd6296985364b6e2eed6dcb8d1ee', + 'a34c500386f4586cb621a81ee4277280', + '1b9c1e26c85e5e4c984085be03832956', + ], + number_of_assessments: 10, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '1fef5a0d7ae34be0abf29c6fe6eeb367', + }, + tags: [], + files: [ + { + id: 'e34d1d4bcfbb4cec8a2db9d0eeb109db', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '271b2c845745550e67527dd3f99379a4', + available: true, + file_size: 20036, + extension: 'perseus', + storage_url: '/content/storage/2/7/271b2c845745550e67527dd3f99379a4.perseus', + }, + { + id: '579bc069ba024843b2f0e594364e784b', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '6f9920f490ea46d3b308770e85baadc0', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '6ac40373b299516097fc1bc079c2499a', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Express the Sentence and Calculate - Division Practice', + lft: 19, + rght: 20, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'e65f064085f65631b2382203abbdfc83', + 'cc8524ef4a755db98c4bd22dfce96e1c', + 'ebddc0b4829959cf8d7de14c3ae50419', + '54204518145151d3b80480e1f6f2a320', + '34a0113c8c6c54a5970d3ecd289ba6d0', + '0196bae85f475fbbb843d0249ad2662a', + 'ed452e28fb6b51d3ac16823cb125678a', + '08348d7093c154db89c20cd149d25d64', + '7db87538d54753e4ac73c80f46dbc75e', + '152210e5445359d7b0e97475056b0c22', + '87093e1fad895f889707022226323f06', + 'f8a00b80bed159f693aafd1d6c7a774d', + '77fe8d8e08645fa0a4cfb008a1e6f976', + '52dcc4f2c2be5d34af8cf3d5fa81d518', + 'b8c2d54be5ab5f00b86d2209a8a2d834', + '9688261ce05a54fd95373b311b6c38ae', + '20c4de38e58a5657bf1bfe5743e23524', + ], + number_of_assessments: 17, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '6f9920f490ea46d3b308770e85baadc0', + }, + tags: [], + files: [ + { + id: '7bdd86a88eaa40db8ec5fd89b53c82ca', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '5c6d08822d4c31ad16449dbee6faaa97', + available: true, + file_size: 32221, + extension: 'perseus', + storage_url: '/content/storage/5/c/5c6d08822d4c31ad16449dbee6faaa97.perseus', + }, + { + id: 'a49978522a0e480faa341aa0188a57fc', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '067bf202946b4405bff8ecc7dae0f480', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '6ac40373b299516097fc1bc079c2499a', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Express the Sentence and Calculate - Division Practice', + lft: 21, + rght: 22, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'e65f064085f65631b2382203abbdfc83', + 'cc8524ef4a755db98c4bd22dfce96e1c', + 'ebddc0b4829959cf8d7de14c3ae50419', + '54204518145151d3b80480e1f6f2a320', + '34a0113c8c6c54a5970d3ecd289ba6d0', + '0196bae85f475fbbb843d0249ad2662a', + 'ed452e28fb6b51d3ac16823cb125678a', + '08348d7093c154db89c20cd149d25d64', + '7db87538d54753e4ac73c80f46dbc75e', + '152210e5445359d7b0e97475056b0c22', + '87093e1fad895f889707022226323f06', + 'f8a00b80bed159f693aafd1d6c7a774d', + '77fe8d8e08645fa0a4cfb008a1e6f976', + '52dcc4f2c2be5d34af8cf3d5fa81d518', + 'b8c2d54be5ab5f00b86d2209a8a2d834', + '9688261ce05a54fd95373b311b6c38ae', + '20c4de38e58a5657bf1bfe5743e23524', + ], + number_of_assessments: 17, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '067bf202946b4405bff8ecc7dae0f480', + }, + tags: [], + files: [ + { + id: '21deefa5418844fdb8168e434e6f3f6b', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '5c6d08822d4c31ad16449dbee6faaa97', + available: true, + file_size: 32221, + extension: 'perseus', + storage_url: '/content/storage/5/c/5c6d08822d4c31ad16449dbee6faaa97.perseus', + }, + { + id: '309d077378fc4d96abf134c7b771b24b', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '1aeeaadc6529481eb7c4a36fd70c58f2', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: 'eaeca2dc1e275fe4a64071eb395a578e', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Using Multiplication Replacement to Find a New Amount Practice', + lft: 23, + rght: 24, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'd75f8c2b226f5044bf665c0c4ee12448', + '3458b1fc63b65ca6b11b4fc80fc2b2ac', + '4d49842969dc500dbb960ca8498221ff', + '85600f7d9b345f9ab0a80fc32f96e08b', + '743bd5ac01a55273bbcf754887c9103b', + 'bc2e920970f354e699cb26cba1589ceb', + '763a958a58745d90abbfdf59e92b8d9f', + 'd430cfca3514565f9d10a39d4c8ac29a', + '56877097fef85307862ba2caea2e589d', + '06629301bcff5e94b491fe7adea508f2', + '0f867c30e2b25c72967d11fb1e23146f', + 'de82270f541e547998b630d28f480d78', + ], + number_of_assessments: 12, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '1aeeaadc6529481eb7c4a36fd70c58f2', + }, + tags: [], + files: [ + { + id: '45f8b9443cd344f984e853f136eaa7b3', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'ac0d1c9d7316d55d9cf64abf3c041cb9', + available: true, + file_size: 25433, + extension: 'perseus', + storage_url: '/content/storage/a/c/ac0d1c9d7316d55d9cf64abf3c041cb9.perseus', + }, + { + id: 'fc44d79dc30d49168a07657d8829b74d', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: 'b5d973e9af5a444a86d4082c8a7c82c4', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: 'd0a7234627ef5ed196d3c1377f07c46e', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Dividing Multi-digit Numbers by 1 Digit Numbers Practice', + lft: 25, + rght: 26, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '4eca128fee48584180cd2ea1ddcb62cb', + 'e728eaf004b25a0aa43b76c78e91877b', + '779e0c7f6a425bd4be46608e1f42fa50', + '148911cb06355b15a02d976e7cf4260b', + '27c3c60dc2df58b99f049ab55555a74f', + '9a2ab24dc1a65f27a896bbd326797fc7', + '33e9e3ab14f95215bf5e8f82053cca99', + 'efa3a3dfdd9e5ea5a2406b9e464df15f', + 'e016a8cdd0035516a314c202531c7af9', + '31a0232c94a452fc92251bd1edd5d3f4', + ], + number_of_assessments: 10, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'b5d973e9af5a444a86d4082c8a7c82c4', + }, + tags: [], + files: [ + { + id: 'c71f8fdae6b24540b95300e871a36fe4', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '8255566ad2efb95a0d9d15cfc0647f93', + available: true, + file_size: 20807, + extension: 'perseus', + storage_url: '/content/storage/8/2/8255566ad2efb95a0d9d15cfc0647f93.perseus', + }, + { + id: 'ad38b5e5abab4dec81a4e50ac58ffe19', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + ], + more: { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + params: { + next__gt: 26, + depth: 2, + }, + }, + }, +}; + +export const fetchMoreTopicResponse = { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + author: '', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '80423ac7fd9d4dd2987214fbd49e68ea', + description: '', + kind: 'topic', + license_description: null, + license_name: null, + license_owner: '', + num_coach_contents: 0, + options: {}, + parent: '40581e004acf482d86042f852adb1985', + sort_order: 1.0, + title: 'One Topic', + lft: 2, + rght: 57, + tree_id: 4, + learning_activities: [], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + ], + admin_imported: false, + assessmentmetadata: null, + tags: [], + files: [], + thumbnail: null, + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: false, + children: { + results: [ + { + id: 'fdf27ea549604e95addc68409e03dadc', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '9f04806a4ede5ba7b3797cf3ef2c8bc5', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Letter Replacement (Division) Practice', + lft: 27, + rght: 28, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'd86713a6bfc055ee951e9edd614da80b', + 'bf3f5605f1905399ab3724cfafcd19e9', + '984fcb3bbee4508f9a3b50d60143e985', + '5019cd3cd8b15a70b0616f65d6a1fc1d', + '4d2be4b36f6a5cb795f2097271e17f33', + '2765fe4c8bed546c9a404d97bd8a804d', + 'a1eaa75c822b5b818ee60c34f13a6b2b', + '5ba2a9d2538554b98abdcfc2ede9019d', + '528e018a4b8e5adaa18554eda0a2927c', + '797f75ccec0a5090826cc4abfbca26b4', + 'de6236fcdafe55bc8c38cde8bc901a6a', + '3fab0b33a1475fb3980e576c703fd836', + 'f4ae257fe7a95d8fa2a7b790cba7b9bd', + 'a8f9915d92c65e4a8e823f2c7c4b49b7', + 'd14a02d9c7905f5c8ea88b56dad57b64', + 'e83454de5c205b54a60b384950c3eadd', + ], + number_of_assessments: 16, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'fdf27ea549604e95addc68409e03dadc', + }, + tags: [], + files: [ + { + id: '8fa771da04f245d69fade574f81e763a', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '8b0dd8158d5145fa3338a899b122a48b', + available: true, + file_size: 30943, + extension: 'perseus', + storage_url: '/content/storage/8/b/8b0dd8158d5145fa3338a899b122a48b.perseus', + }, + { + id: '69f39841a94d47bb8184f72d554b547a', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: 'ae127709a6db4e01a75a18dd637b0abe', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: 'd942237d4a4556468db50059fa9d3594', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Dividing 2-digit Numbers by 2-digit Numbers Practice', + lft: 29, + rght: 30, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '83069057d4cb590eb3c638a044ab033a', + '0ad49ec85e10533fbe6329956705976b', + '17c6d64ea80b58c7bbd4470eec8145de', + 'c7020877cc6d5e53a94b42a1eec7db07', + '709ceea6367f5510b6f736f7c470fbea', + '60cf32362a895845b19b7ce9feaa6731', + 'ef2639ea65c557c2a9811928cb81da50', + '0b806b21d6a5568f8248b9660ecf7eca', + 'c423621c6de85181872e6bf210586119', + 'c0707050d403503a9c541fa004d8ba31', + '9ae5347ec1cc55ed88036756c4ae5410', + 'dafe430929de5c8c9c6269915532c1b4', + ], + number_of_assessments: 12, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'ae127709a6db4e01a75a18dd637b0abe', + }, + tags: [], + files: [ + { + id: '5fb5b70b1c8b4fef82c44af6405316cc', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '9936e7fbe1ffc2a6810f4a26b607666b', + available: true, + file_size: 22616, + extension: 'perseus', + storage_url: '/content/storage/9/9/9936e7fbe1ffc2a6810f4a26b607666b.perseus', + }, + { + id: 'e1eee58075f341f986936b2dda3f385e', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '8133977edfda4716a072b62b91e061eb', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '873cf74e413d5da19769edbf03fbfe45', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Single Object Length (English Units) Practice', + lft: 31, + rght: 32, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '0892022efaa35a91bfcef14031b6d11e', + '8f818a61e7e65a2ebf704e974a36c51c', + 'c018de5892b65112861ce23ca63e10c4', + 'a91a5a7d71f251da8b8476b10d692be6', + '247a78da731d530c97a9b5ce7278ce99', + '14bae03412a350ab910c701775e295f1', + '0d2cf29c3bb957e682d9aea90fb122d3', + '19cbfa3c32385ca684b1ade1352ec1be', + '413d928c1ffb5acd8ce4d014ad40c4fe', + 'd6a6c1d28da15d3ca5eb6bce83ce8158', + 'b314505a20f0540793a9a79c33e80c6e', + '99c088c7c14d5a19b51caa3e1c56bef3', + 'c9b8f2022a3a598bb2341b3260a6dadd', + 'a77b329cbf0850ab9523171012f17662', + '449be11f9ed85be0bc06af83173fb4f0', + '4d7f8a8b7775542ca59c4d823d218987', + '731a6c7406b15f9697a2b10eb02fa975', + '060ffcf4a06a57a5a98fb32ae9c59d38', + '42ce678d0e725c25b5d8e02a2775fb31', + '2e5b30b428a7586f9328ad9be926f342', + 'b049469701cc511b95fdce6eb59df7e3', + '87331d55ac1c5bdc9211bff3b59b33c5', + '2ed34c6bd03f50b69082a7d89ece4d60', + '56d74f6f9706568e9224448076e946bf', + '3c707415819358e38887aea26cb8e8db', + '7ce4e944b36c5b778f6fe4a6209bde2e', + ], + number_of_assessments: 26, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '8133977edfda4716a072b62b91e061eb', + }, + tags: [], + files: [ + { + id: '9e2d6f9807d443e2a5b3674ea82d905f', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'd19c2746dc3ade0a195307f455bc910b', + available: true, + file_size: 346951, + extension: 'perseus', + storage_url: '/content/storage/d/1/d19c2746dc3ade0a195307f455bc910b.perseus', + }, + { + id: '800acd2bc02649c1b01aa8c53b103194', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: 'd170c9f6efa24d6298ecf4b831b7da1b', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '3ee0da8ea44e5da3a1f5d131dd7086ea', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Objects Longer than a Foot or a Yard Practice', + lft: 33, + rght: 34, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '9f3ecb4cf4445edf87bf16ff8c7483ce', + 'b23f7b51e4545eb2bc20122f1eb1668c', + 'a83c1fa1d20953e7aca338751f37339e', + 'a111ea47004459c4a177e6f25467bacb', + '4950e4f0a2f95218ad7ca55c4f8afa3f', + '7dccb0d27b1950c98762650127b5d5e3', + 'dd235edb06e750318d0f328d9fce18b4', + 'fbadb8f912515838a6d8b22b9fe13fc7', + '4d9e90c7b6095ab488c32ab6872afa70', + '805a4c46acf65ec48107215b18df5a45', + 'e8f9ec9e35da513cbed891ad87ae09ed', + 'c6aff17ba99955c58659e1731601353c', + '4033cac83cfe583cb9de6270e47129c2', + 'd16846ed3a0550da982714aa27f2aa15', + '244b9f3282635fea8e0dabc817db247a', + ], + number_of_assessments: 15, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'd170c9f6efa24d6298ecf4b831b7da1b', + }, + tags: [], + files: [ + { + id: 'ca1cd223e2f44c338efe489e3e5b4b6d', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '86a5f5144fcaf7b426863c7b8f0a0505', + available: true, + file_size: 32980, + extension: 'perseus', + storage_url: '/content/storage/8/6/86a5f5144fcaf7b426863c7b8f0a0505.perseus', + }, + { + id: '96b73278a883405885ec5538d9716a03', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '7f5299ed83c34012a5d6477b1c740796', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '7c274209d8b858cf941352bda5656e71', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Comparing Meters, Centimeters, and Millimeters Practice', + lft: 35, + rght: 36, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '9dfbec73801e52f6bdf2edf528b4d60b', + 'c8246464b2985044986d4fb022fd2458', + '60557a259838567cabe8b327dceacc4b', + '0dd5842985dc532db6d0af8cd3db13b8', + '4112a155a0a456c5b9c1709a4c956fbb', + '4151da55d28e5dd590e1cbe960b663bb', + '4eaa46c7dc435cbeb121b396838cf59c', + 'e6964a82251d5c18abd96b0b2a509eec', + 'd8b8fe600edd5592a8b965e275f3e1f2', + 'af03897616de5f68a8997b67373f40e7', + '912449c63fc6544e97ec0b20ae828fe0', + 'e11a2c98e51d5967a320858c22927468', + ], + number_of_assessments: 12, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '7f5299ed83c34012a5d6477b1c740796', + }, + tags: [], + files: [ + { + id: 'eeb4071a6af3443daf23134068799fc7', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'baf60f9fda80c6fce9d8381d51d9c80d', + available: true, + file_size: 23854, + extension: 'perseus', + storage_url: '/content/storage/b/a/baf60f9fda80c6fce9d8381d51d9c80d.perseus', + }, + { + id: '7c8d8232a0414f1f95d4e40f11ef12ca', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '0b3124f27b51479bb63b41c8fb35f630', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '68b7563cb2dc547f9dd4c8a087068462', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Ordering Meters, Centimeters, and Millimeters Practice', + lft: 37, + rght: 38, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '94e676faaffa5eb2a05e88cc8e35c541', + '182f684bce3d5d04995e491658c0cb84', + 'eb66ea91aa2b537e9e20ce75684284fe', + '795a0fef28b450e39bd0750142204215', + 'cb32b49758fc550c98121f43ffc947f9', + '121d9da8978d5e88a5b187beb53b667b', + 'e556a5d509075b459fbd140188920e5e', + '89047c219a9057e79b99d7fc9b04c2dc', + '6298eb980a64527ea87a0c5389e6df04', + '2615a656591b5752aacf144fb1eba4b7', + ], + number_of_assessments: 10, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '0b3124f27b51479bb63b41c8fb35f630', + }, + tags: [], + files: [ + { + id: '394a0327d6ff42caa2c82e04d32f2450', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'ac0c15b2859368d6c8da0e6b6f2174c0', + available: true, + file_size: 20351, + extension: 'perseus', + storage_url: '/content/storage/a/c/ac0c15b2859368d6c8da0e6b6f2174c0.perseus', + }, + { + id: 'ec5156ca538542c6967ab8b178b3ea96', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '966680e1fcda4db682a9a5b981767699', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '13e3017d434851f392c09b6c0ec8e08d', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Converting Millimeters to Meters Practice', + lft: 39, + rght: 40, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '2193ee9ab15d5adcaed4ddac9b673a87', + '8bebd861b0d257c09925662d0dfd9a6c', + '6d971c88a649572bae1ab077a53d6517', + '3194487c9a5157cf80149f57cb8e1976', + '762b80ebe8495d4b93b1e3804a754a0b', + '7105a3547398573f8385aa6e51fdf89d', + '9f176bad886b55a187c7379fb3fcdf88', + '043a404838b45f72b0cd280023a4ce23', + '80274551446b517fa27373f05882c860', + '091ddbf3127f52d38ca8ddc2f9d2327c', + 'd48fca5ad13a5467bfda425c1f93736a', + 'cb7da8d1c7585fb6ba01ded2a3f722f4', + '6b07d6d50cf85582a678ade9323a8a45', + '6ea80ba1151a539dba7d570b3abdca2b', + '9c761719bd8d54eca77af423409033e2', + '40b429e6e0ac587093af00471ca12324', + 'a81bf7db965854b49118e4e01435e2ab', + '09149a57f13753f8b3ffd9cdfdfc6de6', + 'a4efb485f21054e6b5c59ae8075ca6aa', + '98b37c4e67bd5ef886c074748bedd0ee', + 'c2aab7cbe3a15b91823d8eb5909c8603', + '8702f2b5e82f58f78358cd875c6d8da1', + 'fe6a3807b1d65f1e9dbbb5a15412f7a9', + '96948c3567a55aa2b210c4783462074c', + '854574f699ca5f83ada45fb41f4c5f4c', + '0fcf9f637a2a523b86b7d9222c8458cf', + '5af6ed93e7565725802a17d75bc87400', + 'f49cb4dbd3e9532b8943e0833a57ff6b', + '2acaf78475445d5ba1c38ccbc57700d4', + '77d0651b4b055217b5bfaae514ae4560', + '9aab4ecf93145398bd9542126217cd5d', + 'ad8ba8ae155e546b84fe2c3e7f3eddc4', + '043ca47b1afc58ad9079aa1f7cdf234e', + '8c2f042e20e155e1a77846c289a049de', + '308d62b123485d9fb69db0304e664315', + 'dce8bea023985575b5793ab2d4172d25', + '52a3d8eb03555c97b07f62d3a85146af', + 'dcc66a9c23105c6888ec389208207d74', + '487a49a98eff5ba18cf8e4e435a5a6da', + '02f1424fd5f854ab940130e5bd668b53', + 'c53269f2ecc05cf093e5722a4dee2faf', + 'a4241a935c9e5f90ab77645b0d1a9bb6', + 'a0aa753e00095769a173d77b275d1096', + 'ab71460bb74256de82c89e93a84c07f8', + '79a838e175a3555e929218a7d222c825', + 'cf226cc8ab5d520ba52198b637077fff', + '3174664cf3ac5c84bc259081290041ce', + '34ac6a212dac5ca788e8c4a84f77bbd3', + '423fabe047eb56e5ac4d40df0a7c0366', + 'af80c08d022c573aa7625938810144ce', + 'ca53bde5d0265934841c6fa651c5e856', + '4e46e0105f9f5f168b9407ea45af3da8', + '66b4e378bad85df6ba479011081dbdcd', + 'adfd6462b6c359e38c1c106bc3970a6c', + 'db068bfe63185ae6a01df86198d2bcd3', + '50306dfcfcc95051bc9b25f8bf01e4af', + '7649236f33e755069876b8e7d7f921fe', + 'd0f032a56e6853bfba76ceb4d7995004', + '77d2c91907ed50f28d4a92c7ed7ee9ca', + '3073e8a204965a7cb945bcde97c8d7b9', + '55fcdfe4df905411a6737007c659cd60', + '6bfeaec9a6bd58fb9ece9d2d8e81fb2e', + 'c8279eb6145e5a388ce653767876d860', + '23dc96811b0353acb619ba340b329809', + '5ca01b09a1235fc0859e56f212facc81', + 'e9932ce4d07f522caafa4fb90888960f', + 'bcf847b170b15ba7821fe1ca05dd04e2', + '76cd8c88ff1b52859daff877acbca51b', + '26877fb03e3057bba7a0b5288f46ebcf', + 'b65c6ab235c85a55aab687007d899da4', + '404b7a08045a50219f4f3bed3789aab4', + '13ca795d0b3f54e78a3d467ded6526be', + '974c64d2b9035859ab50d839397fa38a', + '3a13fd12ef0c5697aa6f96a665998641', + 'fb07bc3df66a5d0e90f63597e90ee1f8', + '8921e9bef6825b31849e231c371d1612', + '50a1e308127251e881662fa863bf43bb', + '59cf42e1a3895d76a89e88cdc5954086', + 'd187f83029525a41b0299c2e520c1c8c', + 'f933f81749705d74851272cafbd24c48', + '54e2bd98d00751a58cc91080576d6512', + '2a5cfa5f8cf25cfab5af9fdafaa8cd65', + '18a1a7c822235eda8b0425bd082d516d', + '63b870bbef96581fac4921763ed41862', + 'fa9519dd33ac5ab09f153e420aea9188', + '8717c09858d35c869e23f52b24a41f94', + '1a11378047625e528d07bf518595bd7e', + '4f9b6f5989a950d9a4f3956f2da79b1d', + 'dbdf027958e35d2a8a4dfe8194a94936', + 'b69f055cc315577bb67e0256ca390e79', + '4a48faf9ea945cc1b2b28625670d1a96', + '56ab5aacaa8a5364ac9293e08261f91c', + '14313af8c053584a8ce3a377c35d915e', + '6126c2e16722592ba6bc5b946ab68d99', + '6776926fcd1c554ea7c72205a5c50f68', + '47070fd1e5e65cb687080f4aad0e9abc', + 'bdeb1f5d1ad251a0a0102730f0901f8c', + '3689e167b8895c17923085bf0804a0df', + '01dddc3f8d9952fd9648492b10ff1185', + 'dfca0f3149cf5ddfabe8a3ccb0ae351e', + 'd8658ac5979d547c93fc4b03a54ca856', + '08ee9b8ddf6b56a6892470c2fedd293b', + 'ed9a28b38d5051db95ef02e2c6db7f9d', + '613641c684ff5d1f879f00085a7051e1', + '77b70bb0f40b52f187cb67f626f2b1f0', + '537cb586551c545a89e3a24f18c9f3f3', + 'cedff264036f512788691ee84d985c43', + '3864c8c43b6f522a856995e7ca183b96', + '63fd805fab9b5a25b1686672ea8f2e98', + 'ac034014f50b56ec8f0668e809593934', + '3aa3c4d1083252dcb553ffe8cbe98e40', + '48876dc6210d54db829777c5cbe6b072', + 'a492dc00850f567b92814cb21d33d47d', + 'dcd9d95f1ef45d32aeaabc60cb213702', + 'd805034942e758c0910292045dcc3dfb', + 'b181c70982515bfc844eff0a2f8cc841', + '2697146f47a7539b9e96e02d22d2529b', + '81a7058dbfcd5302bfb253e748b333fa', + 'cc88576b20515bd888b010f07d9f5c7f', + '687f657ab7c55c83b502d71e9cb29451', + '4b90ae5c389f595dad339f6c170aad00', + '6bdf359ab043578daccde1c2662b17f8', + '2140319fd43b5cf4a3ebb65009ed7c79', + '1bd9156cf2655d1aa231a4c531f1157e', + 'b9c0dff88bc353dc9173b07e7dbf5190', + '8a5d982fa38156caa379c43bc650d592', + '87a14f67b0d759128809f0475492b056', + '0bdd63654aab57b4bbeb03e8bc0a9f96', + '13e505f7bde85e60a7b6b6986f8512b7', + '58fa647a14235c2095fa09a6f3e00840', + '2e852c63d3905fd6b419ed87568168aa', + 'cdc051c060675359bacdfa92818cfc73', + '4032e06da1c1561188ce5401af56e1ef', + '54570cc36c7e5337a28089e679aa61b9', + '843401184f5e56308b240753375e7402', + '29a820ea73b654799d34681e37dfc881', + 'ab24bef96b255879a55f986f6533f562', + '20bed8185a775514b3e47f401617733d', + '64e9054291535971ad67670cee365d77', + '48f6586e8c995c5c8d98e5a853682de6', + '827ebc90cc225a0c9d1054531ed7249c', + 'ec954f9ae8d456f9ba2944adc34e007d', + '27ce59fa969d5745b3a6f54e39e2b196', + 'ca71c69caf9a57fb90d11cb9095cab04', + 'd97afae7079d5c59af759d8006cbc148', + '9a9ab3649677508f9eef033ddd2e5ff0', + 'ce689a07d9995476944c501db0db1070', + '5310c1a1a1a854db9fa0bf306d35e929', + 'd2ac6d620f5e50889cb9f76e994d6a89', + '3082652a80755ff782e47b693840fd08', + '1588363ebc50552981388df4655254ea', + 'e9d80b6104f251359797d4e2f7b05cad', + '48f4721eec3254639c525883ea7bbe73', + '41f8226484055f5b913f9c05c5fe1ff0', + '9a542c37f2435b13bfb2d32c72830c75', + '7d7b2a4676525a9bb098f65da02b8243', + 'd07c18bbd7a0595a98d916c959a3d398', + '9de52eeed3e553da89245e1d77747d66', + '04d3ee413362589199d614c499a7cf67', + '1674cd3c5cb65935b492a3178d6150d2', + ], + number_of_assessments: 160, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '966680e1fcda4db682a9a5b981767699', + }, + tags: [], + files: [ + { + id: 'b4456a80717f4ebdb34d6f1f06ac9129', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '5eed19a3ad8091e3cbf24379d9e4386a', + available: true, + file_size: 297882, + extension: 'perseus', + storage_url: '/content/storage/5/e/5eed19a3ad8091e3cbf24379d9e4386a.perseus', + }, + { + id: 'cf3c3a73dcbc42a1872db5e41edafcca', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '3fe5af34ae874f33bcf6076ba852e7fc', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '6dc3a599fcc35e409f1bed0bbea1a8ae', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Mixed Units: Which is Shortest? Practice', + lft: 41, + rght: 42, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '3875b392c798561b8cb927b85e02690f', + '8674616d5f775f04ae141cce2c969371', + '7fa8c15c85585c8cb85bfdb3cba08d77', + '74bb97e49fe9580daf8d8e368aa2c275', + 'ed37865f4632564d9ce6b95a17094cda', + 'b1e2af71af4d5cda8b50cb2bc375a6a3', + '4c252770619b5782b9ddd0bb787d831a', + 'df62a4c630ab579083f5bfe147f45689', + 'e863fcfdcd205b8498942a412f958a36', + '741dab7fc4dd566e956ca98cd4836986', + '8316f4e954c857f48f43d30e8ac97ced', + 'c81b88d575e1578fa797096fac19b7e6', + 'e3cd477885a256b5ae3689508418811f', + '0404f971366b5265a1cac45fcecdf057', + '44282b5e35d45d0687ebcab980361dd3', + 'f0828dce778b55aba47106fff903be71', + '58861252e5785dcb9f30493ca991e943', + '3b6cea55d1755d38842e56fa7da9b05c', + '30991126c0115cb9b182e0c2489d3162', + '5aad680e1a825946a41b9c4717d67b3e', + ], + number_of_assessments: 20, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '3fe5af34ae874f33bcf6076ba852e7fc', + }, + tags: [], + files: [ + { + id: '4f51996ebd264c819b7cf24d4ab90b32', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'b4e3b362b1d7f7d46b6f0028a8cc4a01', + available: true, + file_size: 43716, + extension: 'perseus', + storage_url: '/content/storage/b/4/b4e3b362b1d7f7d46b6f0028a8cc4a01.perseus', + }, + { + id: '7815cb2af00a4eba87830ae2e5bd79ba', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: 'e68551ac88684c7e8d15a9665ffcc7a7', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: 'ac1b81e938d85b8e90362a60a93f5d01', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Converting Meters to Millimeters Practice', + lft: 43, + rght: 44, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '1245fd023a1059a9b03843521d5ce4bb', + 'f5160e5b272c52bfb8e041e8af4020ae', + '47df8af6ddf85193a23b7e5df66074f9', + 'a70e705ff25c5cbab103a971fdf2895a', + '34230253ff2b574b8718e733507faf01', + '1952504947c9541384f9cc4382351231', + '73497f93926c50849070f3a2fd661975', + '50a999b24a715fa18f1f032d4c102266', + 'c7c2f5d150b05373901ae13b735c01b9', + '4e179a2a58bc565e8dcf6df9c431ff2d', + '282032380f495f0889a5fb7f1f439bb9', + '8e0a2980772a542bb060339f220cff11', + '1114cc9411145db5be5e09b7b329ceb0', + '06ff8a02965952ef9c4fa9bbd5e62c6a', + 'b1c21e88a8625fcaa41b4968ae8fedb1', + '31d4c8c1121f5ebb94079d46512570ec', + '419361b2d1f759e680d8311365e26f83', + '6fb72f7ac1cc59ddb69e48c5add69203', + '407580a6c3fc5aba80111a2b63ffbc9c', + 'f3693f4c1e25586ab6dd278de3b2def8', + '2b321c452708513aaf46d174a4cd7b77', + '045be868a658531e9894482ec4958101', + '13c35cc85e195f80922940aa4c8099ae', + '18fd20e6d79e5c36b77badf35f8c2225', + '997587c50b06515c9d9572e9cef4512b', + '1e4c20c669975123b84f157a33c9c2eb', + '90a22e0002ea5a9784da25d3c8c2d6dc', + '0bae5a6c182d561f8a20a6b00d95e637', + 'e7a0666f2f115d98bd9998c763c26dd9', + '2092b07dda745abb8e5ab72ae319b3de', + 'e4df28c5011b5f3eb23fe83a5f32d340', + 'b022c755a3ce582b98f79afb95d64614', + 'e7d8d2b9b8165ca18d1ecb1ae56562fb', + 'd42d41923890569ebd8494b3eb8a23e8', + '3dd4dab3594e53c882fa824448f2c0ea', + '4ac100ae7f5651c6a47db530ab9b077c', + '140d0346125c511991e994b4447c011b', + 'a9c7067da7cb50129301ec2893a85739', + '7e5896187f2a58c5ba981fcb359577dd', + '93be409ac3be55c0a02286f0909318c2', + 'f497475543d35cd4985900381add7172', + '83de2bcba4eb550cb55f663c7a281573', + 'd2fb6b642dc4571d9e8923b802312a12', + 'cee07ad4407459c4ac895f6277328f1c', + 'f7ad633cb70759d7907ed0150ef888ab', + 'def92fe1d6785afa8cb5fa8637edc29d', + '9d0573773fad5929991636f73448e0df', + 'd337e1be002b585187f9d57aed6bca36', + '0f8b655ece525943a2ea1268fc6c8e6d', + '7393616a7c885026b77aa4fefb60f4a5', + 'e3ec7168950f5c1ab0d71ffb1e23fcf2', + 'd2ae90ee2f75551ea905eba81bc12812', + 'd3a9829274925f0884b72c26bfde3e53', + '3b51a281a4ed5ff09105e29b9f3db0bf', + '4ea3b468cee55669b59845bd1fc620dd', + '363e98844a435f1bbcb7b1c5b978652f', + 'fe3ed79d66da521c80d088b48b3aa46c', + '51daceb5e455543292b1d49c159cd730', + 'f6a1d298da7a5cff93c22a42ffb3b6e2', + 'f6748f6348a0520da81b01ab57159222', + 'ba5ed5f55b4952808c8e7004738e3433', + '11f91acb6a62572d9411eb485c026825', + '2de265d4b7fd5f2db98887ad489d30d6', + '50fbe69d644150209a5ef6b28d5c8c55', + 'a704524b7bbf5a76ad7a1c960a1d5ae5', + '896c3f58cd7a587e94755d0a266b9466', + 'f568d6fcfcbd51ee95a123c76eaa364a', + '45203040065f50019adaf7672233078c', + 'a25c8eb2feab57578288adbf773d8b18', + 'be03f4f141615bea807e309afea86963', + 'd1998e55c02a55dd8d61f32776faccc6', + 'e10d77a7684759c7aab4fd4cae83e135', + '4d072dd9b8af554c8dc08682b35d2446', + '48ee2eb21ee258aca19aa62d90342890', + 'b598b5a5a5985ec1ab1051a99b9632d3', + 'a5e6fc8afa4756108708bea94a134f9c', + '5f9338c83e70529592dbba0d93603a75', + '2329bd91bfb956f4a5199edacf28e3bc', + 'ce0e1c2f4b68531595ce94b7c22d0418', + '8654b7eb74935c57aa9c97b90934e474', + '77c7c3186aa35f6bbb5bea9ebb6f4b50', + 'cf62e639c48150f3b325adf08869689d', + '05de7e0f5b0652fd889d677a8913ddee', + 'f5434b2a57cd5eda870131e57cbeddf7', + '55349159715c5e759f876e79b24954fe', + '9a240f6b0841535d8fc67cdc8ac6013c', + '032c6d3e789a5950891055123a2046cc', + '19bc3d4d8d845f54a0c677acedba2369', + '979bd99c27365b69b8975838d0230b5d', + 'ebb92b5760155f67ad5692f5f6852888', + 'fc2ed0e554e05fc0ad799f78afe6d58e', + 'aefdd8d86a505a128da80867a9db3a47', + '77b766840fa953c8b70208c39db31ae2', + 'e324c69bd6275010977d1c90fb3412b3', + '2c39a9173b5f51738718aa6e564702b9', + '9761d9aa5a6d592289e52d46df727175', + '50fb93eb45535c74874957fe10c8d89e', + 'ce6d3a58351258b0822f22e711c41c38', + '92a052ef710d558b8493d51004daf71c', + '9a23e91e9a415f1a8891932899752c6e', + 'b4608a03bd175c049e06d567488e9373', + '8ab049034717508ca8709fbb057ebfbe', + '6ad3372d0cab57f2827fc998de7ecb83', + 'bb23fffe262c5abcb9713ab00d209027', + '9bbd737c0fcc5415bdde70ba007c4418', + 'f241070eca1956f5a11caece45f69439', + 'ceb215f26d5953889d15d33882891b48', + '4f089e8aeefa5b3993d27eb63e0f792c', + '7a98243fba87574c82515c8d81b06be6', + '142cbe5880615fe6bda50bc212896565', + '40ab73f573485318ae06cdda544da96e', + 'c440c06895ba55839148f64d6adf5625', + 'b85e322118e15032bbde8d5be835c855', + 'f842a060b6b35db19f51811d05a68a2e', + '49a9e9b8bf125838a46d949a7e2e96d7', + '282ae2b1951054319c8be41ea1366f7b', + '7be181d58724534b8bc9070855f7719e', + '98277f5fc03a5ffd84025503e6285a2f', + '4f1d457c90b65cc1b971bdd9b5cadfe4', + '683c2e07c32a55c7b56703c5b873a106', + '313f38ad353055749eac21ff98c4cce2', + 'da66aafd6abb573b9ff8de2bde6a6be7', + '5ef4d97ab0715f4892724703ffd6964d', + '925e5ede5ce950f09d31668a712f0de8', + '0145b6d1f97057f7b0e687c79e7120ec', + '3828828d1c9c5726a6088489e8448231', + '901db9bdb86f5f8091d639c977a8674d', + 'ef61e92318e65381b555b414f9e861cd', + 'b97148fa182f54be98b74df9e943389d', + '0548a662c338564a8502966e0f503af6', + '0f2d15460f515e9a936a63e9e9d4e36c', + '999603a8967d590db7b9c902d32fe6ed', + '277fd1c9d50d5681b3f1666f1e2e9b90', + 'c8641681673c5bfcb0d6cf1c575d9279', + '11c29c705a3156b9b722090a534dc6e2', + '872547240e245e1eb13f160025ea8cef', + '0af4a3434d005029b6fbb37520b8934a', + '30b5a12063bf5af987c06b5f4acfcc2f', + 'a56904e46f345afe9bbf3a8274592a43', + '006095f19016565cb985661e96a963f5', + 'fd20d446449653d2b26f404dfc08e4f1', + '3037ea367327596f9a6c6962b99b3d17', + '4715d6683ced5685ad388c30c686df29', + '4b0d5c5854d354b1a6b1eb5fdcc1e046', + '7f9da0c3e2c0529cba13c6605854ffe0', + '14f06e4e364a5ccd8b8b0f6e3a7b8956', + 'c66f8aff2195520fb103dc917701fde6', + '24837fda75795098b9cb697302e00dbf', + '2e7db3d2c4185bc69bc41362197c7cb8', + '50bac2a869ef55fbbdda82cc324ba1aa', + 'e66e848524ce5065aac640d26a78c521', + 'c9809992857057009f8f3e803e8d920e', + '8855d3790ae2574999b7a86b45802a86', + 'dffc77efff825e62b56292c46d83004b', + 'daa7d225578a57c5ba735202c2d292f9', + 'e4766da74d4352b19abdbdacf8d9230c', + '64d7de6df74a51bfafad79bb102e7b84', + 'd3ee2785dcc658bcb51eb3d2f218ab34', + '222248bfe24150429c3d731bbdc0b0e1', + 'ea862acc73e05c63afd01fb1c91a1147', + '22dd2fa9ebc65792acfa4ab0416eb708', + ], + number_of_assessments: 161, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'e68551ac88684c7e8d15a9665ffcc7a7', + }, + tags: [], + files: [ + { + id: 'e2a0bd407d954767a7abeff0ff8a286d', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'f8e61d8a75ba09d445bcc8491672a3ef', + available: true, + file_size: 299964, + extension: 'perseus', + storage_url: '/content/storage/f/8/f8e61d8a75ba09d445bcc8491672a3ef.perseus', + }, + { + id: '069a05259fa240eeaaf6f8431c3071f7', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: 'c8d94683db4a46fcba6515d12493deee', + author: 'CK-12 Editor', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '768b6057edeb5050b3da7a653b30e3b5', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Addition of Integers Practice', + lft: 45, + rght: 46, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'bc55618c1b355b58b1e389cf303a138e', + '21d28b9d3d1a53b08112d613720b3505', + 'f92a72a5badc52909410dd4b6c0b4ab3', + '3d36416a2b2354f19a8a6f6e71851fdf', + 'f3ecfa7afa165a74a80a306c0d13b9df', + '381ad5d3cbdd5398905f970fca6538be', + '650e80bf9568514fa6ae57c4995b4b12', + '788a2c6b741c556dbfeb799c1b6c2582', + 'c23f9e7d77ed55beb93e71c8a3e9a40f', + '06bf8f45cc99510584e5e3361b51ce9c', + '45570264e84354f280d2fa888be110ed', + '82448dad6ade568ea8cdd44bf2c94061', + '46a2d14253ea5be9a2b55972bbb5a6c5', + 'ad6ca686197855c0963c6e7fd1b73713', + '966d8530a29851b2b95880475fc64225', + '6010dd37b2415620b734e0469da33cf6', + '94c67195ad515b1ca39d45af0837607e', + '2ad8cf7b537a5f3a8bba8e5bc86a8c6d', + '4033c0cc434e5f28b36c9241b8507169', + '5faa50e38e06508aaa2312365573b047', + 'bffaae5ee98957a69348a50f50cedf6d', + 'd66036c8fe35522a88f9d5c755e8c5ff', + '569e989bc82e53fbb997a8ee310d1ab5', + '2f583cc1b59c5346ab3f502f7a648e8a', + '267f143f6af159718c360318df4191b1', + '301250077ae858d0aa16e32635365d40', + 'ed86d10a0923572ab8bef458b879c39c', + 'acd9ad92704f59a5b3d9fc85a058976f', + '64fb9eba0fc05a56a39c84858832930a', + '7458121455dc5296a39b3ae768c37f0e', + '2dd5058a85d15ea3bc9f3401e36eea41', + '92b5ff5766935226b8b2b916ceb6d953', + '26fb0c287b7f5fcab8164a63786b6818', + '39d0a2a4374e5d40ab6196910d766e91', + '0c82561c0da65d7ea498222611ff9e17', + '3f9a7fbbb77c5aaf885676501ae8edb9', + '84e65fd3b61a5491a12806a9f57f7fae', + 'd2bb428e25e45622958ed9b102bf740e', + ], + number_of_assessments: 38, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'c8d94683db4a46fcba6515d12493deee', + }, + tags: [], + files: [ + { + id: 'a268f0f8a22048c3ac3099be4657d5a9', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'ec4057b58353e8169c1a56fc83658c9d', + available: true, + file_size: 93388, + extension: 'perseus', + storage_url: '/content/storage/e/c/ec4057b58353e8169c1a56fc83658c9d.perseus', + }, + { + id: '88410459466a4f1fbf8dd5b38759fb69', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'e25fc9da81cf1c450f18dd79c23d85f9', + available: true, + file_size: 3412, + extension: 'jpg', + storage_url: '/content/storage/e/2/e25fc9da81cf1c450f18dd79c23d85f9.jpg', + }, + ], + thumbnail: '/content/storage/e/2/e25fc9da81cf1c450f18dd79c23d85f9.jpg', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '6611fad3b8394fc1a9043e2209cc6421', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '02d28dab145758708f21bb6122feb1d4', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Whole Number Multiplication Practice', + lft: 47, + rght: 48, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'fdc69a0aa85b5525a4910ee0509bc44b', + '499d0fea47d555d89fa48f0342e0b722', + '2bd106de5d1150bd8d8ec5c08699f543', + '27ca4e2aa54a54d09b97ca60ac6c183e', + 'e6b299582e0456c185f89584dd85f4d5', + '7c7b8237ece0561ba2373f03e6ffda2e', + '67f65efd41645bad96aa0957f99539a1', + '283d2e7ad25a5123af023f449f18e651', + 'b8dcf245e1b859658e5a352389444d5d', + 'a40cb29a22c55d1fbd4564cc2b44ac78', + 'ab6f0f7f85a05c1abbb36279c65eeff7', + 'f86eac8ac3f6587399ebab0b0f72a98f', + '5ef0c48afdda5505972a604fea2cae25', + 'fa7dc3d0b5a552bcbf6ef0f5f4d4c08d', + '7657cad0f4b959b98fb21a9c9ef77db7', + 'f24a9eb0a68d5224aa5edc8884ea0f82', + '9caa2b3cb69a5c9593a0141e6b541955', + '402a52ba005259d18c58e9993661814c', + '1d1d630feb4e5bf090881885d3b68158', + 'c63151bad3db5e8ea980716295cad478', + 'e836619e50085d62823cfc1183836236', + '1c3a7015764a5cfd89878a47508e7c9f', + '4fa9036de0d5547ba775d75d7410f34d', + 'f31125d428125757abfa870153e852d2', + 'ca39773f388351338a6fb2cba4294407', + '9614dc7253785597a04d7998676dcd56', + '96d405a3bd1b5f6da5dd6fda1795e91f', + '5b12a66ee4235d63a6528455361a2ac4', + '9bc941e07b3055ae8ba3f5169a9dbace', + 'c1c8feea0a3d5008adf1ab92b8e6f1cf', + '17cafe8114d15123981af585cafb89bf', + 'b13202c41e2f5893a9aa6f18aad2b959', + '150a12482e445c3c82b1af92477505e4', + 'fc428b9292705709a9bfc26081aa91b0', + '1f6249ea5d165aadab8b7bb98abb6504', + 'b750beda2e1e56f28349d17986d78fe6', + '7466caae383c594c941c04c2646221a1', + 'e53523eece225096b005eea7c0c1f850', + 'b12d963773775a30bd067102ceb05783', + 'deef9046205e54a5be87b4d737ffa7c2', + 'daf95c96c0b057ab8a70241ddbb2d9f2', + 'd3fa9e14e66953389dd06d17ded0f38d', + '0e8b718eefea51f3b55f92ccf80ac621', + '6ef658dc92c450d1a2ad101b7aa5fbc2', + '67bb5d03c98b5f10adc1e162269c4f30', + '4447d236be585c3ab597cf97fd32bb14', + ], + number_of_assessments: 46, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '6611fad3b8394fc1a9043e2209cc6421', + }, + tags: [], + files: [ + { + id: '7dc18c759968402493dc2a552a0132c8', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'c13e34f112400f1741e6e15524e5e4ad', + available: true, + file_size: 130992, + extension: 'perseus', + storage_url: '/content/storage/c/1/c13e34f112400f1741e6e15524e5e4ad.perseus', + }, + { + id: '7caead19594a46c0a0ac3535c7c28b59', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: '143a0f0bf287f78d6b9c676de3b03796', + available: true, + file_size: 7993, + extension: 'jpg', + storage_url: '/content/storage/1/4/143a0f0bf287f78d6b9c676de3b03796.jpg', + }, + ], + thumbnail: '/content/storage/1/4/143a0f0bf287f78d6b9c676de3b03796.jpg', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '178115046c3b4bf78679d637e9d0464e', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '87684226d2c4545fbe692989681d189a', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Whole Number Division Practice', + lft: 49, + rght: 50, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '6a8d708be300573c8b7551746b0eebe5', + '157ec5aa7bc7549bad2e991e9b621c33', + '0f360f09aa1c5ca6b27c354dc07fab23', + '08db066a8f7d5728aec2bab6c2f2c69a', + 'c379ef50e2d65647af58dac4de1f45a8', + '9af17213ae395ba4a85314aaeebef8a5', + '1665293b099551898df05d569ea38f28', + '2accc0d7b0e95ef989a665fc432ab4fb', + 'a004520b77ff5cc8baf827a6a4cb7aac', + '1887dadfdde4530f9333b60e9f075fb4', + 'e62dd243209b5c65ab8b6b928d5869f6', + '556f4cd0829552559d7d02c279102410', + '2f9dadc0bf8c5e0fb35c8ad2804b5ad4', + 'bef8454dd6675d5ab48def7c9526525a', + '5ee548bb85905ce891be09aa91aaf72f', + '48f3641d323d50f2a508f12731b91c7a', + '88d6991036365663823186f7e7b69083', + '4e7da2b4accb5e26b4ea6135e4bb75b4', + 'b14f2b42d42750fba833193059b75048', + '066caf96a5485d61bcfdc04b628f5959', + 'eaf18163b85b552894fb2df14cd4feb4', + 'e46c044063c75d6893e6c144b7ed7c14', + 'a34c636092b05d36a4e49fe3e89b3b67', + '5c56e9df1a4a5d3ebdc4dd660cf76bde', + '3517847629e150328a2e0fa90b249df5', + '589a82722fe05fdba1047aa1ba70dc8b', + 'a3853af3de985914827e5dfb0d7e0547', + '3eda6e36c05a5caca2805de763083534', + '002d4fda184c5b6ab15da4e005314757', + '64523d3bf65e5b7eb1dbd0bfd1bd3964', + '087b2df1051856128240ae7708420b73', + 'ef60918812715d87a29a677b5d03e24d', + '2e8c1bb4e6615acea02652e297b5b53a', + 'ae2f65987c10541684a5c5ea1ff3a46c', + '4299a22ca01a576798bfb41606040c18', + 'bcc882b1341f5b99a4699e6719a26615', + '97af06f0acd65b9789a68b359b7e95a2', + '1a1b8172d81151da83b8412bf31de4e2', + '2163c8ec957b584ebd791be7fed9fbaa', + '89ea04c4c58e5754a34667216914c9d8', + '2bdd672a87295bcb8ae9eacd2ed93c4a', + 'fabdcac7f9ad5980a3d4a3ef54e7d388', + '96fd9e74b3715a9ba445d61d51d02965', + '17440a7df847548594248d96d467a937', + '055ce3af7d975103a8894a257a169e67', + '8e7f0d0d679a58f0831682de415da55d', + 'ba4e241f99f05632820a8408a731234d', + '1fdd92a3a8fd54e9b192bdf1ab715503', + '74bbf77a4d0051c8af4b91f5ea9a5935', + '166251c774fd5325a917f6a033c06b0e', + 'db704a6f90a05d0ba3743bfc48111ada', + 'd356b1b766c955e19ab68faf45493b35', + 'c7015b67bdf05aca8b6c1c68a0c9ed1d', + '3d72c7eec756543dbcdc4e20d9eaec03', + 'ddf9f5cf5d86584a9088caeccd2b5dc3', + 'ca9b352729405d6cbaf56476d1bbc391', + '3d739c31f8e9521f88430ec83bd6d7a5', + '5a23b3fc907c51ec99547243b78434dd', + '5c9dd569a31b5ce28129ee5fa196d3d6', + '9832f5d993e7578a95301ccce0d749cd', + 'be967b3f2cbc5edea1e7a1117e66a826', + 'a1e61377df2057b5adb7ea3196b57623', + 'fbeb548f414c5ffcae3cf1849a683557', + 'f49357361ac45411844245581526b037', + '23c123e946f65c35ac5bcf7e1adc3fd9', + '58dc803c83965d25b8e3961e28d6b9f8', + '849ee2248a365b4083f5e704dbd3c9d4', + '71a4256ffcc25781b4b9eb4700ab73e1', + '424bc78fab34521e886f45a6107bc1ad', + 'd6979e4334d9541ea7763ff3cabde4b2', + 'f02c6a0c15a652b8bb211bbfc78c3196', + 'e4ba04950ac65091a4dece23e4fc2562', + 'b013a5373bfe542d979ec6ef528b1950', + 'f23cefd5bf9a526eb6d3d91c1989d302', + 'ab1dce9701095a68a8fe90d70198c3dc', + '22076e6898ad50f3ac97691c90d0c9c9', + '56a9eeb2b8495f4d840845b61f6bafed', + '2cd274f525c75903a13c3f0f2a149d11', + 'e6cb8d71090f56cb9b2d3189701aee57', + '788be300ffd456309fba9eca94a08880', + 'fde15467b6f75577a341aeb024ec5801', + 'c77226c6beed5da5a50c3c555624f877', + 'b43206e7f18352df8d2148a99d65d165', + 'e33c42c42d075601948a4fdf5b941834', + '1ff4724cfdaf5b9aaf367aa9de413b23', + 'db0848b9867d5d7588e40dcd11d517ad', + '059ebd5e75f35348a0ded9cef71017eb', + '89cd07395d5650eb952a146e8bb611a0', + 'eadfcee57be956ff98d92d4869b44f19', + '8182a62a96cf50368fa4d41de9c13c11', + '6214a65ce0325d7088c4e1b825f3bbe1', + '744ff25740cb5dcfbb4f5d508b8fddbf', + 'b8ec711faf48516d89e4918cb7df6b09', + '9634c3569ae156bea215420c50675f2f', + '8a81fcd71c2456c69a3481f496a6d764', + '4977edbd8ec25d75bc745d7f0c04ff78', + '82fc3ca959715723a6e597f1af410284', + 'c822e29a97b559448e6dbf1cc90d6afc', + '3a7afc019f325dc0bf12b45b672de433', + '9c379e4a5bf1586eb468524e9291c3ea', + 'b7140fd49dac5835b2ee4ec9037ddc9e', + '9e93eba06d3956439ba89bcdbda5cecc', + '2be18fc10fff50b9933de7a5b951817f', + '2144d4fea4395920b276d82441119ebf', + '56549061005b59dd9c7377dd7b2881c4', + 'aac8b114bc42593aae3071bc9cec1551', + '05f78976e99c5b2481d3676bf85a6ff9', + 'c5c103cfce0455aebe9092d2442a8a1d', + '48a60f51c6f75292a783523930775a8e', + ], + number_of_assessments: 109, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '178115046c3b4bf78679d637e9d0464e', + }, + tags: [], + files: [ + { + id: '78c39ab9ca9243b5a57cdebff1161189', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '2ca20bd79fb6c04428f78a9a1509da6d', + available: true, + file_size: 282766, + extension: 'perseus', + storage_url: '/content/storage/2/c/2ca20bd79fb6c04428f78a9a1509da6d.perseus', + }, + { + id: '7edb06f7fc544b259a003d91ce5e64d9', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: '8832cd40d43e97c96ef3c8456e40c4a7', + available: true, + file_size: 3217, + extension: 'jpg', + storage_url: '/content/storage/8/8/8832cd40d43e97c96ef3c8456e40c4a7.jpg', + }, + ], + thumbnail: '/content/storage/8/8/8832cd40d43e97c96ef3c8456e40c4a7.jpg', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + ], + more: { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + params: { + next__gt: 50, + depth: 2, + }, + }, + }, +}; + +export const fetchTreeTopicWithoutMore = { + id: '40581e004acf482d86042f852adb1985', + author: '', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '40581e004acf482d86042f852adb1985', + description: '', + kind: 'topic', + license_description: null, + license_name: null, + license_owner: '', + num_coach_contents: 0, + options: {}, + parent: null, + sort_order: 1.0, + title: 'One Topic, Lots of Exercises', + lft: 1, + rght: 58, + tree_id: 4, + learning_activities: [], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [], + admin_imported: false, + assessmentmetadata: null, + tags: [], + files: [], + thumbnail: null, + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: false, + children: { + results: [ + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + author: '', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '80423ac7fd9d4dd2987214fbd49e68ea', + description: '', + kind: 'topic', + license_description: null, + license_name: null, + license_owner: '', + num_coach_contents: 0, + options: {}, + parent: '40581e004acf482d86042f852adb1985', + sort_order: 1.0, + title: 'One Topic', + lft: 2, + rght: 57, + tree_id: 4, + learning_activities: [], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + ], + admin_imported: false, + assessmentmetadata: null, + tags: [], + files: [], + thumbnail: null, + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: false, + children: { + results: [ + { + id: 'c6516394603a49f9bf35eedc2e9f586a', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: 'fc3968b0c38f54a4b8b8b42ba6b44730', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Selecting the Incorrect Statement Practice', + lft: 3, + rght: 4, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'aea2eb60d34d58e6b752cbe6aa547679', + '1c068d60ac705be8919e5c987ecf1a8d', + '1e5f07a4d8aa5105990a3de45eb8e957', + 'bb2cc6a375ee5be285ea2105dee60ab1', + '30bd631c3705558e8838edb7645c4b36', + 'b69a76cb42865db1bb13c9be255a4472', + '18eec6deed2252d4b890c1e414cf0697', + 'e2a478a9f8635fe785b4d163954a9570', + '3c07be3012fb5167aee524b8a3f29d1c', + '127d6916d0535b3999d761b057993c26', + '505af15982525524ba0c78b1f0351d6f', + ], + number_of_assessments: 11, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'c6516394603a49f9bf35eedc2e9f586a', + }, + tags: [], + files: [ + { + id: 'a418d01678024981aa8bf3891ebfddc0', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '01143cf2bcbfcfc91cbb7b1c63583873', + available: true, + file_size: 24422, + extension: 'perseus', + storage_url: '/content/storage/0/1/01143cf2bcbfcfc91cbb7b1c63583873.perseus', + }, + { + id: '5356eff16b7d41f1948fff165b715c73', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '9af49c7fb61c4401a780618d39cbad1b', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: 'b5e61e4231f55c29982a1813ee8c817c', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Factors - World Problems Practice', + lft: 5, + rght: 6, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'd07ddfaa68c65a1e8d2e1b1058fcb7e3', + 'f6df88f00fd158f582701f941ead799e', + '488d0510fcf150dfb9d1d55156712303', + 'bf0552397e585ecca5734e308a579a55', + '92eb15278664568690c0e200061ba99f', + '3f43bc5b8acd5124aed0be08048aa721', + '2fb047e1486e5e788fe8d713860c5b10', + 'a5e562871d965a38aaa8e3758e1f211c', + 'ed4947a44faa5a258b5f49b2a56bd9f1', + 'ec5b2b02be0557f99e104d4225db32d3', + '551bfe97f82e5ede9cc82c75ec62fd4f', + '4945bd5d15ec5dda8b8dafb492de4e4a', + '8b07b21cd3b75919a4167b1c3b8ed44c', + '818b2fe981d25cd3a0b2ae4282211e0d', + '0eed52d0b7dc5732bb6b0a6cc018c8bd', + '8e8855776cd45fab9cd726f451356d35', + '3355cf7568ed55de961eb4d5ea026bbe', + '506163732f5453f2b32f5c2cd5fd0ce5', + 'bc11fad066155864ae4eaa2f665be78b', + 'ade8dabaafe355e3912aec26e0599850', + '3d0b6b64ef515a81a8b71ce5827d846d', + '08a82e402e2053b5a779b9970e2531f9', + 'c99049ade75d558fb9761c30fbcd8699', + '1dc561bfc0fe5ceeb4bc3d412a9f46e3', + '2a04d838ff3b5f3bbc5f42f1c452d4cd', + '82f6f70c4a5b553bbfb159fba2bd6063', + '25769b2201a35d91a843c3e3aa114f4c', + '617afb3f92195a03ae59773e8d2ec4ba', + '45f8d940e13a57f79d65320d2fea622c', + '4a63a3932a395cf5a049f997fcae0660', + '39a61c0b0caf52ec86fb0bc9d7d950b7', + 'c2abe2296b445fb5a644fee9d0b0fc7e', + '1938b598130f59c398d7f4a0cdf06fe1', + 'b76765d464035bd795cec91347b5d171', + '69271599d6945156bd865a227d2f4153', + 'd9a1602115c75ffd95471dbee7b46498', + ], + number_of_assessments: 36, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '9af49c7fb61c4401a780618d39cbad1b', + }, + tags: [], + files: [ + { + id: 'ef8dd48f45e1452c9953977415ab3e48', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '6e211de9f83d9164b671d63c0a6798b1', + available: true, + file_size: 70372, + extension: 'perseus', + storage_url: '/content/storage/6/e/6e211de9f83d9164b671d63c0a6798b1.perseus', + }, + { + id: '11e043db50744708b3875bcbb4776b88', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: 'c8e249a7530c4e41996331e77b9ae80c', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: 'fe38b6886cc057f3a2046188518171ff', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Making an Even Group - Word Problems Practice', + lft: 7, + rght: 8, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '932d0d9c67165330a6ea7d6f5f9af056', + '3a8ac435fde5589fb52dd7b2126bb474', + '4b6fcfbaa5a053c695837143fbc371ff', + 'e29413708add5729906ae41346597c39', + '909a0170a54c5943852f773f729cc5e9', + 'ae759bfd92e95599bc1e16b69f942656', + 'e30eacdca24e520fb39584a3b66fd164', + '92a562a20ebf57c6878a42e6343d50ed', + 'a976809128a05a19a7c40411fce9d2bb', + '04e5c83219a651f88393d8524393f20c', + ], + number_of_assessments: 10, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'c8e249a7530c4e41996331e77b9ae80c', + }, + tags: [], + files: [ + { + id: '8e3e4d121eaa4f2e86fe3fdb2964b174', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'f39a618516d35e5ff134ed40b2e5e7e1', + available: true, + file_size: 20477, + extension: 'perseus', + storage_url: '/content/storage/f/3/f39a618516d35e5ff134ed40b2e5e7e1.perseus', + }, + { + id: 'f826a3492bd44e19b8316fb8195ad6d0', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: 'ce4697e5aba744b28dfcaae738855533', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: 'fe38b6886cc057f3a2046188518171ff', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Making an Even Group - Word Problems Practice', + lft: 9, + rght: 10, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '932d0d9c67165330a6ea7d6f5f9af056', + '3a8ac435fde5589fb52dd7b2126bb474', + '4b6fcfbaa5a053c695837143fbc371ff', + 'e29413708add5729906ae41346597c39', + '909a0170a54c5943852f773f729cc5e9', + 'ae759bfd92e95599bc1e16b69f942656', + 'e30eacdca24e520fb39584a3b66fd164', + '92a562a20ebf57c6878a42e6343d50ed', + 'a976809128a05a19a7c40411fce9d2bb', + '04e5c83219a651f88393d8524393f20c', + ], + number_of_assessments: 10, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'ce4697e5aba744b28dfcaae738855533', + }, + tags: [], + files: [ + { + id: '23f5d85a00c24ef49e23ab8b8ca6f0ce', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'f39a618516d35e5ff134ed40b2e5e7e1', + available: true, + file_size: 20477, + extension: 'perseus', + storage_url: '/content/storage/f/3/f39a618516d35e5ff134ed40b2e5e7e1.perseus', + }, + { + id: '05f9c892f8c5443685c12fcc9365ea56', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '55af18c1f4aa43bd82be805388b01401', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '86fd9fd03feb5f2f803b4d8739e790ea', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Finding the Total Amount - Word Problems Practice', + lft: 11, + rght: 12, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'ea7eb8786b91557c94ed4a42d1adbd36', + 'f7f0d2a78fd659569fac5e7caab6544f', + '12f0f5f2c4285987944452426a6cd711', + '77491566c0c85a14b480e768fcd6a3cb', + 'f67266af98395de68f5dbff1c3964baf', + 'fa4ae6d46c285036943b4dfb6956ec05', + 'a86db5371d745c58954b146cc7219101', + '5c22c382ae9a5a01afb7539764015992', + '2d9432c9235458a4b58fd77610a967bf', + '01d4b7ee9a5c5828b3ef04675a6d489d', + ], + number_of_assessments: 10, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '55af18c1f4aa43bd82be805388b01401', + }, + tags: [], + files: [ + { + id: '513ec1ca134c4c85a1a09e54144d17c7', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'd557f4df1d938c076a84aa54a33a9e52', + available: true, + file_size: 21893, + extension: 'perseus', + storage_url: '/content/storage/d/5/d557f4df1d938c076a84aa54a33a9e52.perseus', + }, + { + id: 'cf1b8cd9d05a4b46967900909f68746e', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '0d1a02a783574673b33e080a228953f7', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '10ac3735b3c75f2c9fb67e9f6c29c7ba', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Multiple Units and Multiplication - Word Problems Practice', + lft: 13, + rght: 14, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'e0f76575028054d2bb6cc4165e470446', + '7a1329a137eb5293b00c315587de6f3c', + '02197bea43555ce496c285ad2c581e55', + '2da55cc88e3f5c80aca867aa203f8ea1', + '6256d4ee28b6509dbabaf91849f542b5', + '879e5535fbb45d39bc66bde76fbe40db', + 'c34f1b4ef4df547691cea110c9f76478', + '8ac2bfa7976f55f4b96a9b7b26f9279f', + 'f1344e86d72e5d978ce827721d94ee1f', + '5b17ff9f9de45771a2665de7f30f38da', + '5414db78b11a57f0aecc53eb7acbf36d', + 'b81607aac8d85bbeb17b9d297b569c4b', + '234a9a9dc6d55047b52c6dce58218b4f', + 'e3667031820d5b83a75fb3719d59251f', + 'edbca2581f0a5aceb9b18c6a6f4159a2', + ], + number_of_assessments: 15, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '0d1a02a783574673b33e080a228953f7', + }, + tags: [], + files: [ + { + id: 'd3a62806e1d1425c971f1d5fc41d931e', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '08f26d4fd47ef64d71268bd0a7caca05', + available: true, + file_size: 31318, + extension: 'perseus', + storage_url: '/content/storage/0/8/08f26d4fd47ef64d71268bd0a7caca05.perseus', + }, + { + id: '2f6d9b1e1c2c4ddbb808f37457fa62f9', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: 'a2da2013ade04d11bd281d6cb428d953', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '303834931d7a5db58adc3fce6168b5e3', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Dividing then Multiply to Find the New Amount - Word Problems Practice', + lft: 15, + rght: 16, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '291775e8acbf5be389c3e0a81a0b7e5a', + '13500ec108935269b130cf477e4dd9f3', + 'caad7e4a35d4541db5acaac69510ebde', + '7d4033651cca5d18a53e727d74394391', + 'ec5ac189b47750348505a2483f6c81f2', + 'cb3c3f6c920857359525231e46c7ba18', + 'd38e19f652fd5601b3f3aad4271d053e', + '63f4cd6296985364b6e2eed6dcb8d1ee', + 'a34c500386f4586cb621a81ee4277280', + '1b9c1e26c85e5e4c984085be03832956', + ], + number_of_assessments: 10, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'a2da2013ade04d11bd281d6cb428d953', + }, + tags: [], + files: [ + { + id: '335c4296be0f4e77a5223984ff9bed69', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '271b2c845745550e67527dd3f99379a4', + available: true, + file_size: 20036, + extension: 'perseus', + storage_url: '/content/storage/2/7/271b2c845745550e67527dd3f99379a4.perseus', + }, + { + id: '2f0f304074344ea9a432f9884e72b856', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '1fef5a0d7ae34be0abf29c6fe6eeb367', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '303834931d7a5db58adc3fce6168b5e3', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Dividing then Multiply to Find the New Amount - Word Problems Practice', + lft: 17, + rght: 18, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '291775e8acbf5be389c3e0a81a0b7e5a', + '13500ec108935269b130cf477e4dd9f3', + 'caad7e4a35d4541db5acaac69510ebde', + '7d4033651cca5d18a53e727d74394391', + 'ec5ac189b47750348505a2483f6c81f2', + 'cb3c3f6c920857359525231e46c7ba18', + 'd38e19f652fd5601b3f3aad4271d053e', + '63f4cd6296985364b6e2eed6dcb8d1ee', + 'a34c500386f4586cb621a81ee4277280', + '1b9c1e26c85e5e4c984085be03832956', + ], + number_of_assessments: 10, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '1fef5a0d7ae34be0abf29c6fe6eeb367', + }, + tags: [], + files: [ + { + id: 'e34d1d4bcfbb4cec8a2db9d0eeb109db', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '271b2c845745550e67527dd3f99379a4', + available: true, + file_size: 20036, + extension: 'perseus', + storage_url: '/content/storage/2/7/271b2c845745550e67527dd3f99379a4.perseus', + }, + { + id: '579bc069ba024843b2f0e594364e784b', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '6f9920f490ea46d3b308770e85baadc0', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '6ac40373b299516097fc1bc079c2499a', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Express the Sentence and Calculate - Division Practice', + lft: 19, + rght: 20, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'e65f064085f65631b2382203abbdfc83', + 'cc8524ef4a755db98c4bd22dfce96e1c', + 'ebddc0b4829959cf8d7de14c3ae50419', + '54204518145151d3b80480e1f6f2a320', + '34a0113c8c6c54a5970d3ecd289ba6d0', + '0196bae85f475fbbb843d0249ad2662a', + 'ed452e28fb6b51d3ac16823cb125678a', + '08348d7093c154db89c20cd149d25d64', + '7db87538d54753e4ac73c80f46dbc75e', + '152210e5445359d7b0e97475056b0c22', + '87093e1fad895f889707022226323f06', + 'f8a00b80bed159f693aafd1d6c7a774d', + '77fe8d8e08645fa0a4cfb008a1e6f976', + '52dcc4f2c2be5d34af8cf3d5fa81d518', + 'b8c2d54be5ab5f00b86d2209a8a2d834', + '9688261ce05a54fd95373b311b6c38ae', + '20c4de38e58a5657bf1bfe5743e23524', + ], + number_of_assessments: 17, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '6f9920f490ea46d3b308770e85baadc0', + }, + tags: [], + files: [ + { + id: '7bdd86a88eaa40db8ec5fd89b53c82ca', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '5c6d08822d4c31ad16449dbee6faaa97', + available: true, + file_size: 32221, + extension: 'perseus', + storage_url: '/content/storage/5/c/5c6d08822d4c31ad16449dbee6faaa97.perseus', + }, + { + id: 'a49978522a0e480faa341aa0188a57fc', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '067bf202946b4405bff8ecc7dae0f480', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: '6ac40373b299516097fc1bc079c2499a', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Express the Sentence and Calculate - Division Practice', + lft: 21, + rght: 22, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'e65f064085f65631b2382203abbdfc83', + 'cc8524ef4a755db98c4bd22dfce96e1c', + 'ebddc0b4829959cf8d7de14c3ae50419', + '54204518145151d3b80480e1f6f2a320', + '34a0113c8c6c54a5970d3ecd289ba6d0', + '0196bae85f475fbbb843d0249ad2662a', + 'ed452e28fb6b51d3ac16823cb125678a', + '08348d7093c154db89c20cd149d25d64', + '7db87538d54753e4ac73c80f46dbc75e', + '152210e5445359d7b0e97475056b0c22', + '87093e1fad895f889707022226323f06', + 'f8a00b80bed159f693aafd1d6c7a774d', + '77fe8d8e08645fa0a4cfb008a1e6f976', + '52dcc4f2c2be5d34af8cf3d5fa81d518', + 'b8c2d54be5ab5f00b86d2209a8a2d834', + '9688261ce05a54fd95373b311b6c38ae', + '20c4de38e58a5657bf1bfe5743e23524', + ], + number_of_assessments: 17, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '067bf202946b4405bff8ecc7dae0f480', + }, + tags: [], + files: [ + { + id: '21deefa5418844fdb8168e434e6f3f6b', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '5c6d08822d4c31ad16449dbee6faaa97', + available: true, + file_size: 32221, + extension: 'perseus', + storage_url: '/content/storage/5/c/5c6d08822d4c31ad16449dbee6faaa97.perseus', + }, + { + id: '309d077378fc4d96abf134c7b771b24b', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: '1aeeaadc6529481eb7c4a36fd70c58f2', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: 'eaeca2dc1e275fe4a64071eb395a578e', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Using Multiplication Replacement to Find a New Amount Practice', + lft: 23, + rght: 24, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + 'd75f8c2b226f5044bf665c0c4ee12448', + '3458b1fc63b65ca6b11b4fc80fc2b2ac', + '4d49842969dc500dbb960ca8498221ff', + '85600f7d9b345f9ab0a80fc32f96e08b', + '743bd5ac01a55273bbcf754887c9103b', + 'bc2e920970f354e699cb26cba1589ceb', + '763a958a58745d90abbfdf59e92b8d9f', + 'd430cfca3514565f9d10a39d4c8ac29a', + '56877097fef85307862ba2caea2e589d', + '06629301bcff5e94b491fe7adea508f2', + '0f867c30e2b25c72967d11fb1e23146f', + 'de82270f541e547998b630d28f480d78', + ], + number_of_assessments: 12, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: '1aeeaadc6529481eb7c4a36fd70c58f2', + }, + tags: [], + files: [ + { + id: '45f8b9443cd344f984e853f136eaa7b3', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: 'ac0d1c9d7316d55d9cf64abf3c041cb9', + available: true, + file_size: 25433, + extension: 'perseus', + storage_url: '/content/storage/a/c/ac0d1c9d7316d55d9cf64abf3c041cb9.perseus', + }, + { + id: 'fc44d79dc30d49168a07657d8829b74d', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + { + id: 'b5d973e9af5a444a86d4082c8a7c82c4', + author: 'CK-12', + available: true, + channel_id: '40581e004acf482d86042f852adb1985', + coach_content: false, + content_id: 'd0a7234627ef5ed196d3c1377f07c46e', + description: '', + kind: 'exercise', + license_description: '', + license_name: 'CC BY-NC', + license_owner: 'CK-12', + num_coach_contents: 0, + options: { + completion_criteria: { + threshold: { + m: 5, + n: 5, + mastery_model: 'm_of_n', + }, + model: 'mastery', + }, + }, + parent: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + sort_order: 1.0, + title: 'Dividing Multi-digit Numbers by 1 Digit Numbers Practice', + lft: 25, + rght: 26, + tree_id: 4, + learning_activities: ['VwRCom7G'], + grade_levels: [], + resource_types: [], + accessibility_labels: [], + categories: [], + duration: null, + ancestors: [ + { + id: '40581e004acf482d86042f852adb1985', + title: 'One Topic, Lots of Exercises', + }, + { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + title: 'One Topic', + }, + ], + admin_imported: true, + assessmentmetadata: { + assessment_item_ids: [ + '4eca128fee48584180cd2ea1ddcb62cb', + 'e728eaf004b25a0aa43b76c78e91877b', + '779e0c7f6a425bd4be46608e1f42fa50', + '148911cb06355b15a02d976e7cf4260b', + '27c3c60dc2df58b99f049ab55555a74f', + '9a2ab24dc1a65f27a896bbd326797fc7', + '33e9e3ab14f95215bf5e8f82053cca99', + 'efa3a3dfdd9e5ea5a2406b9e464df15f', + 'e016a8cdd0035516a314c202531c7af9', + '31a0232c94a452fc92251bd1edd5d3f4', + ], + number_of_assessments: 10, + mastery_model: { + type: 'm_of_n', + n: 5, + m: 5, + }, + randomize: true, + is_manipulable: true, + contentnode: 'b5d973e9af5a444a86d4082c8a7c82c4', + }, + tags: [], + files: [ + { + id: 'c71f8fdae6b24540b95300e871a36fe4', + priority: 1, + preset: 'exercise', + supplementary: false, + thumbnail: false, + lang: null, + checksum: '8255566ad2efb95a0d9d15cfc0647f93', + available: true, + file_size: 20807, + extension: 'perseus', + storage_url: '/content/storage/8/2/8255566ad2efb95a0d9d15cfc0647f93.perseus', + }, + { + id: 'ad38b5e5abab4dec81a4e50ac58ffe19', + priority: 2, + preset: 'exercise_thumbnail', + supplementary: true, + thumbnail: true, + lang: null, + checksum: 'a7899bc76099a7622a37d70a09817d83', + available: true, + file_size: 8753, + extension: 'png', + storage_url: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + }, + ], + thumbnail: '/content/storage/a/7/a7899bc76099a7622a37d70a09817d83.png', + lang: { + id: 'en', + lang_code: 'en', + lang_subcode: null, + lang_name: 'English', + lang_direction: 'ltr', + }, + is_leaf: true, + }, + ], + more: { + id: 'a02f76983d6b42bf9148dbdc5c1cbb5d', + params: { + next__gt: 26, + depth: 1, + }, + }, + }, + }, + ], + more: null, + }, +}; diff --git a/kolibri/plugins/coach/assets/test/useFetchTree.spec.js b/kolibri/plugins/coach/assets/test/useFetchTree.spec.js new file mode 100644 index 00000000000..2ec64906092 --- /dev/null +++ b/kolibri/plugins/coach/assets/test/useFetchTree.spec.js @@ -0,0 +1,84 @@ +import { get } from '@vueuse/core'; +import { ContentNodeResource } from 'kolibri.resources'; +import useFetchTree from '../src/composables/useFetchTree.js'; +import { + fetchTreeTopicResponseWithMore, + fetchMoreTopicResponse, + fetchTreeTopicWithoutMore, +} from './useFetchTree.fixtures.js'; + +// The properties that useFetchTree should expose, aka. the public API +const publicApi = ['topic', 'resources', 'loading', 'fetchTree', 'fetchMore', 'hasMore']; +var resources, topic, fetchTree, fetchMore, hasMore; + +jest.mock('kolibri.resources'); + +describe('useFetchTree', () => { + describe('fetching data with ContentNode.fetchTree when there is more', () => { + beforeAll(async () => { + ContentNodeResource.fetchTree.mockResolvedValue(fetchTreeTopicResponseWithMore); + ({ resources, topic, fetchTree, fetchMore, hasMore } = useFetchTree({ + topicId: '1', + })); + await fetchTree(); + }); + it('saves locally the topic', async () => { + expect(get(topic)).toEqual(fetchTreeTopicResponseWithMore); + }); + + it('saves the children of the topic', async () => { + expect(get(resources)).toEqual(fetchTreeTopicResponseWithMore.children.results); + }); + + it('exposes a computed property to determine if there is more to fetch', async () => { + expect(get(hasMore)).toBeTruthy(); + }); + + it('fetches the next page of data, resulting in the results being appended to the existing results', async () => { + const resourcesBeforeFetchingMore = get(resources); + // Need to update the mock to be sure it's retruning the correct data + ContentNodeResource.fetchTree.mockResolvedValue(fetchMoreTopicResponse); + await fetchMore(); + expect(get(resources)).toEqual([ + ...resourcesBeforeFetchingMore, + ...fetchMoreTopicResponse.children.results, + ]); + }); + + describe('fetching more data', () => { + beforeAll(async () => { + ContentNodeResource.fetchTree.mockResolvedValue(fetchTreeTopicWithoutMore); + ({ resources, topic, fetchTree, fetchMore, hasMore } = useFetchTree({ + topicId: '1', + })); + await fetchTree(); + }); + it('saves locally the topic', async () => { + expect(get(topic)).toEqual(fetchTreeTopicWithoutMore); + }); + + it('saves the children of the topic', async () => { + expect(get(resources)).toEqual(fetchTreeTopicWithoutMore.children.results); + }); + it('does not result in "having more"', async () => { + expect(get(hasMore)).toBeFalsy(); + }); + it('rejects the promise if there is nothing more to fetch', async () => { + expect(fetchMore()).rejects.toBeTruthy(); + }); + }); + }); + + describe('API', () => { + it.each(Object.keys(useFetchTree({ topicId: '1' })))('exposes a %s property', property => { + expect(publicApi.includes(property)); + }); + + it.each(Object.keys(useFetchTree({ topicId: '1' })))( + 'exposes no properties prefixed with _', + property => { + expect(property[0]).not.toBe('_'); + } + ); + }); +}); diff --git a/kolibri/plugins/coach/assets/test/useQuizCreation.spec.js b/kolibri/plugins/coach/assets/test/useQuizCreation.spec.js new file mode 100644 index 00000000000..31defb1526b --- /dev/null +++ b/kolibri/plugins/coach/assets/test/useQuizCreation.spec.js @@ -0,0 +1,217 @@ +import { get } from '@vueuse/core'; +import { ChannelResource, ExamResource } from 'kolibri.resources'; +import { objectWithDefaults } from 'kolibri.utils.objectSpecs'; +import { QuizExercise, QuizQuestion } from '../src/composables/quizCreationSpecs.js'; +import useQuizCreation from '../src/composables/useQuizCreation.js'; + +const { + // Methods + updateSection, + // replaceSelectedQuestions, + addSection, + removeSection, + setActiveSection, + initializeQuiz, + updateQuiz, + addQuestionToSelection, + removeQuestionFromSelection, + saveQuiz, + + // Computed + channels, + quiz, + allSections, + activeSection, + // activeExercisePool, + activeQuestions, + selectedActiveQuestions, + // replacementQuestionPool, +} = useQuizCreation(); + +const _channel = { root: 'channel_1', name: 'Channel 1', kind: 'channel', is_leaf: false }; +ChannelResource.fetchCollection = jest.fn(() => Promise.resolve([_channel])); +ExamResource.saveModel = jest.fn(() => Promise.resolve()); + +/** + * @param num {number} - The number of questions to create + * @param overrides {object} - Any overrides to apply to the default question + */ +function generateQuestions(num = 0) { + const qs = []; + for (let i = 0; i < num; i++) { + const question = objectWithDefaults({ question_id: i, counter_in_exercise: i }, QuizQuestion); + qs.push(question); + } + return qs; +} + +/** @param numQuestions {number} - The number of questions to create within the exercise + * @returns {Exercise} - An exercise with the given number of questions + * A helper function to mock an exercise with a given number of questions (for `resource_pool`) + */ +function generateExercise(numQuestions) { + const exercise = objectWithDefaults({ resource_id: 'exercise_1' }, QuizExercise); + exercise.questions = generateQuestions(numQuestions); + return exercise; +} + +describe('useQuizCreation', () => { + describe('Quiz initialization', () => { + beforeAll(() => { + // Only need this called once in this scope + initializeQuiz(); + }); + + it('Should create the first section and add it to the quiz', () => { + expect(get(allSections)).toHaveLength(1); + }); + + it('Should set the active section to the first section', () => { + expect(get(activeSection).section_id).toEqual(get(allSections)[0].section_id); + }); + + it('Should reset the quiz altogether if initializeQuiz is called again', () => { + addSection(); + expect(get(allSections)).toHaveLength(2); + initializeQuiz(); + expect(get(allSections)).toHaveLength(1); + }); + + it('Populates the channels list', () => { + expect(get(channels)).toHaveLength(1); + }); + }); + + describe('Quiz management', () => { + beforeEach(() => { + // Let's get a fresh quiz for each test + initializeQuiz(); + }); + + describe('Quiz CRUD', () => { + it('Can save the quiz', () => { + expect(() => saveQuiz()).not.toThrow(); + expect(ExamResource.saveModel).toHaveBeenCalled(); + }); + + it('Can update the quiz given a subset of valid properties', () => { + const newTitle = 'New Title'; + updateQuiz({ title: newTitle }); + expect(get(quiz).title).toEqual(newTitle); + }); + + it('Throws a TypeError if the given updates are not a valid Quiz object', () => { + expect(() => updateQuiz({ title: 1, question_sources: 'hi' })).toThrow(TypeError); + }); + + it('Can add a new section to the quiz', () => { + expect(get(allSections)).toHaveLength(1); + addSection(); + expect(get(allSections)).toHaveLength(2); + }); + + it('Can remove a section from the quiz', () => { + const addedSection = addSection(); + expect(get(allSections)).toHaveLength(2); + removeSection(addedSection.section_id); + expect(get(allSections)).toHaveLength(1); + expect( + get(allSections).find(s => s.section_id === addedSection.section_id) + ).toBeUndefined(); + }); + + it('Can change the activeSection', () => { + const addedSection = addSection(); + addSection(); // This automatically sets the added section as active, but we won't use it + expect(get(activeSection).section_id).not.toEqual(addedSection.section_id); + setActiveSection(addedSection.section_id); // Now we set the first added section as active + expect(get(activeSection).section_id).toEqual(addedSection.section_id); + }); + + it('Can update any section', () => { + const addedSection = addSection(); + const newTitle = 'New Title'; + updateSection({ section_id: addedSection.section_id, section_title: newTitle }); + expect( + get(allSections).find(s => s.section_id === addedSection.section_id).section_title + ).toEqual(newTitle); + }); + + it('Will update `questions` to match `question_count` property when it is changed', () => { + // Setup a mock exercise w/ some questions; update the activeSection with their values + const exercise = generateExercise(10); + const questions = exercise.questions; + updateSection({ + section_id: get(activeSection).section_id, + questions, + resource_pool: [exercise], + }); + expect(get(activeQuestions)).toHaveLength(questions.length); + expect(get(activeQuestions).length).not.toEqual(0); + expect(get(activeSection).resource_pool).toHaveLength(1); + + // Now let's change the question count and see if the questions array is updated + const newQuestionCount = 5; + updateSection({ + section_id: get(activeSection).section_id, + question_count: newQuestionCount, + }); + // Now questions should only be as long as newQuestionCount + expect(get(activeQuestions)).toHaveLength(newQuestionCount); + // And it should have split it into head & tail and kept the head so indexes 0 and 4 ought + // to be the same as the first and last questions in the updated questions array + expect(get(activeQuestions)[0].question_id).toEqual(questions[0].question_id); + expect(get(activeQuestions)[4].question_id).toEqual(questions[4].question_id); + + const newQuestionCount2 = 10; + updateSection({ + section_id: get(activeSection).section_id, + question_count: newQuestionCount2, + }); + expect(get(activeQuestions)).toHaveLength(newQuestionCount2); + }); + + it('Throws a TypeError if trying to update a section with a bad section shape', () => { + expect(() => updateSection({ section_id: null, title: 1 })).toThrow(TypeError); + }); + }); + + describe('Question list (de)selection', () => { + beforeEach(() => { + initializeQuiz(); + const questions = [1, 2, 3].map(i => objectWithDefaults({ question_id: i }, QuizQuestion)); + const { section_id } = get(activeSection); + updateSection({ section_id, questions }); + }); + it('Can add a question to the selected questions', () => { + const { question_id } = get(activeQuestions)[0]; + addQuestionToSelection(question_id); + expect(get(selectedActiveQuestions)).toHaveLength(1); + }); + it("Can remove a question from the active section's selected questions", () => { + const { question_id } = get(activeQuestions)[0]; + addQuestionToSelection(question_id); + expect(get(selectedActiveQuestions)).toHaveLength(1); + removeQuestionFromSelection(question_id); + expect(get(selectedActiveQuestions)).toHaveLength(0); + }); + it('Does not hold duplicates, so adding an existing question does nothing', () => { + const { question_id } = get(activeQuestions)[0]; + addQuestionToSelection(question_id); + expect(get(selectedActiveQuestions)).toHaveLength(1); + addQuestionToSelection(question_id); + expect(get(selectedActiveQuestions)).toHaveLength(1); + }); + }); + + describe('Question replacement', () => { + beforeEach(() => { + initializeQuiz(); + const questions = [1, 2, 3].map(i => objectWithDefaults({ question_id: i }, QuizQuestion)); + const { section_id } = get(activeSection); + updateSection({ section_id, questions }); + }); + it('Can give a list of questions in the exercise pool but not in the selected questions', () => {}); + }); + }); +}); diff --git a/kolibri/plugins/coach/class_summary_api.py b/kolibri/plugins/coach/class_summary_api.py index 62bf778a4e0..7e281b13bc7 100644 --- a/kolibri/plugins/coach/class_summary_api.py +++ b/kolibri/plugins/coach/class_summary_api.py @@ -274,9 +274,20 @@ def serialize_lessons(queryset): def _map_exam(item): item["assignments"] = item.pop("exam_assignments") - item["node_ids"] = list( - {question["exercise_id"] for question in item.get("question_sources")} - ) + data_model_version = item.pop("data_model_version") + if data_model_version == 3: + item["node_ids"] = [ + question["exercise_id"] + for question_source in item.get("question_sources", []) + for question in question_source.get("questions", []) + if question.get("exercise_id") is not None + ] + else: + item["node_ids"] = [ + question["exercise_id"] + for question in item.get("question_sources", []) + if question.get("exercise_id") is not None + ] return item diff --git a/kolibri/plugins/coach/kolibri_plugin.py b/kolibri/plugins/coach/kolibri_plugin.py index 1ffa55e3606..9e854c47827 100644 --- a/kolibri/plugins/coach/kolibri_plugin.py +++ b/kolibri/plugins/coach/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import logging from kolibri.core.auth.constants.user_kinds import COACH diff --git a/kolibri/plugins/coach/test/test_class_summary.py b/kolibri/plugins/coach/test/test_class_summary.py index aa65a72991a..462738ad344 100644 --- a/kolibri/plugins/coach/test/test_class_summary.py +++ b/kolibri/plugins/coach/test/test_class_summary.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import uuid from django.urls import reverse diff --git a/kolibri/plugins/coach/test/test_classroom_notifications.py b/kolibri/plugins/coach/test/test_classroom_notifications.py index ab744e4db74..665e32c02af 100644 --- a/kolibri/plugins/coach/test/test_classroom_notifications.py +++ b/kolibri/plugins/coach/test/test_classroom_notifications.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.urls import reverse from rest_framework.test import APITestCase diff --git a/kolibri/plugins/coach/test/test_difficult_questions.py b/kolibri/plugins/coach/test/test_difficult_questions.py index 93175c8f0fa..6106b83e819 100644 --- a/kolibri/plugins/coach/test/test_difficult_questions.py +++ b/kolibri/plugins/coach/test/test_difficult_questions.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import json from datetime import timedelta diff --git a/kolibri/plugins/coach/test/test_lesson_report.py b/kolibri/plugins/coach/test/test_lesson_report.py index 4aa7cb44f53..8c5982d2fdb 100644 --- a/kolibri/plugins/coach/test/test_lesson_report.py +++ b/kolibri/plugins/coach/test/test_lesson_report.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import datetime import json diff --git a/kolibri/plugins/coach/views.py b/kolibri/plugins/coach/views.py index 935b7075229..dd904872adc 100644 --- a/kolibri/plugins/coach/views.py +++ b/kolibri/plugins/coach/views.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.utils.decorators import method_decorator from django.views.generic.base import TemplateView diff --git a/kolibri/plugins/default_theme/kolibri_plugin.py b/kolibri/plugins/default_theme/kolibri_plugin.py index c4703336d3e..8d8f8318bf2 100644 --- a/kolibri/plugins/default_theme/kolibri_plugin.py +++ b/kolibri/plugins/default_theme/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from django.templatetags.static import static from kolibri.core import theme_hook diff --git a/kolibri/plugins/demo_server/kolibri_plugin.py b/kolibri/plugins/demo_server/kolibri_plugin.py index 8d0578009d3..f553be30243 100644 --- a/kolibri/plugins/demo_server/kolibri_plugin.py +++ b/kolibri/plugins/demo_server/kolibri_plugin.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - from kolibri.core.webpack import hooks as webpack_hooks from kolibri.plugins import KolibriPluginBase from kolibri.plugins.hooks import register_hook diff --git a/kolibri/plugins/device/assets/src/constants.js b/kolibri/plugins/device/assets/src/constants.js index fd49e1efe3c..bd9057991a1 100644 --- a/kolibri/plugins/device/assets/src/constants.js +++ b/kolibri/plugins/device/assets/src/constants.js @@ -71,3 +71,7 @@ export const MeteredConnectionDownloadOptions = { DISALLOW_DOWNLOAD_ON_METERED_CONNECTION: 'DISALLOW_DOWNLOAD_ON_METERED_CONNECTION', ALLOW_DOWNLOAD_ON_METERED_CONNECTION: 'ALLOW_DOWNLOAD_ON_METERED_CONNECTION', }; + +export const ImportFacility = 'import_facility'; + +export const CreateNewFacility = 'create_new_facility'; diff --git a/kolibri/plugins/device/assets/src/modules/deviceInfo/handlers.js b/kolibri/plugins/device/assets/src/modules/deviceInfo/handlers.js index c446ccc35c5..1c272fdd100 100644 --- a/kolibri/plugins/device/assets/src/modules/deviceInfo/handlers.js +++ b/kolibri/plugins/device/assets/src/modules/deviceInfo/handlers.js @@ -2,7 +2,8 @@ import client from 'kolibri.client'; import urls from 'kolibri.urls'; import samePageCheckGenerator from 'kolibri.utils.samePageCheckGenerator'; import bytesForHumans from 'kolibri.utils.bytesForHumans'; -import { isEmbeddedWebView } from 'kolibri.utils.browserInfo'; +import { get } from '@vueuse/core'; +import useUser from 'kolibri.coreVue.composables.useUser'; /* Function to fetch device info from the backend * and resolve validated data @@ -20,9 +21,10 @@ export function getDeviceInfo() { data.device_name = nameResponse.data.name; const { server } = infoResponse.headers; + const { isAppContext } = useUser(); if (server.includes('0.0.0.0')) { - if (isEmbeddedWebView) { + if (get(isAppContext)) { data.server_type = 'Kolibri app server'; } else { data.server_type = 'Kolibri internal server'; diff --git a/kolibri/plugins/device/assets/src/modules/manageContent/index.js b/kolibri/plugins/device/assets/src/modules/manageContent/index.js index e9dc3b15cea..00da52d4ad6 100644 --- a/kolibri/plugins/device/assets/src/modules/manageContent/index.js +++ b/kolibri/plugins/device/assets/src/modules/manageContent/index.js @@ -49,12 +49,24 @@ export default { return channels.map(channel => { const taskIndex = findLastIndex(getters.managedTasks, task => { + const isLatest = task => { + const tasksWithSameChannelId = getters.managedTasks.filter( + t => t.extra_metadata.channel_id === channel.id && t.status === TaskStatuses.COMPLETED + ); + const maxScheduledDatetime = tasksWithSameChannelId.reduce( + (max, current) => + current.scheduled_datetime > max ? current.scheduled_datetime : max, + tasksWithSameChannelId[0].scheduled_datetime + ); + return task.scheduled_datetime === maxScheduledDatetime; + }; return ( ![TaskTypes.DISKCONTENTEXPORT, TaskTypes.DISKEXPORT, TaskTypes.DELETECHANNEL].includes( task.type ) && task.extra_metadata.channel_id === channel.id && - task.status === TaskStatuses.COMPLETED + task.status === TaskStatuses.COMPLETED && + isLatest(task) // corresponds to latest changes on channel ); }); return { diff --git a/kolibri/plugins/device/assets/src/modules/wizard/handlers.js b/kolibri/plugins/device/assets/src/modules/wizard/handlers.js index b6d1a0e8952..2c55f3719a2 100644 --- a/kolibri/plugins/device/assets/src/modules/wizard/handlers.js +++ b/kolibri/plugins/device/assets/src/modules/wizard/handlers.js @@ -1,5 +1,6 @@ import find from 'lodash/find'; import router from 'kolibri.coreVue.router'; +import logger from 'kolibri.lib.logging'; import samePageCheckGenerator from 'kolibri.utils.samePageCheckGenerator'; import { TransferTypes } from 'kolibri.utils.syncTaskUtils'; import { ContentNodeGranularResource, RemoteChannelResource } from 'kolibri.resources'; @@ -12,6 +13,8 @@ import { } from './apiPeerImport'; import { getChannelWithContentSizes } from './apiChannelMetadata'; +const logging = logger.getLogger(__filename); + // Utilities for the show*Page actions function getSelectedDrive(store, driveId) { return new Promise((resolve, reject) => { @@ -168,7 +171,7 @@ export function showSelectContentPage(store, params) { // are no data for this channel on a device yet (download channel // metadata task will be triggered later for this situation) if (error.response && error.response.status === 404) { - console.log( + logging.error( `^^^ 404 (Not Found) error returned while requesting "${error.response.config.url}..." is an expected response.` ); } diff --git a/kolibri/plugins/device/assets/src/views/AvailableChannelsPage/index.vue b/kolibri/plugins/device/assets/src/views/AvailableChannelsPage/index.vue index 8dcf45af1b0..74590f7c33b 100644 --- a/kolibri/plugins/device/assets/src/views/AvailableChannelsPage/index.vue +++ b/kolibri/plugins/device/assets/src/views/AvailableChannelsPage/index.vue @@ -139,7 +139,6 @@ import omit from 'lodash/omit'; import some from 'lodash/some'; import uniqBy from 'lodash/uniqBy'; - import responsiveWindowMixin from 'kolibri.coreVue.mixins.responsiveWindowMixin'; import ImmersivePage from 'kolibri.coreVue.components.ImmersivePage'; import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; import { TaskResource } from 'kolibri.resources'; @@ -176,7 +175,7 @@ SelectionBottomBar, UiAlert, }, - mixins: [commonCoreStrings, commonDeviceStrings, responsiveWindowMixin, taskNotificationMixin], + mixins: [commonCoreStrings, commonDeviceStrings, taskNotificationMixin], setup() { useContentTasks(); }, @@ -331,9 +330,9 @@ return this.$router.push({ query: newQuery }); }, handleSubmitToken({ token, channels }) { + this.showTokenModal = false; if (channels.length > 1) { if (this.$route.query.token !== token) { - this.disableModal = true; this.$router.push({ ...this.$route, query: { @@ -341,8 +340,6 @@ token, }, }); - } else { - this.showTokenModal = false; } } else { this.goToSelectContentPageForChannel(channels[0]); diff --git a/kolibri/plugins/device/assets/src/views/DeprecationWarningBanner.vue b/kolibri/plugins/device/assets/src/views/DeprecationWarningBanner.vue index ed88a3f993d..d6dcc2cdff9 100644 --- a/kolibri/plugins/device/assets/src/views/DeprecationWarningBanner.vue +++ b/kolibri/plugins/device/assets/src/views/DeprecationWarningBanner.vue @@ -1,7 +1,7 @@ @@ -143,6 +150,11 @@ @success="handleStartImportSuccess" @cancel="showImportModal = false" /> +