diff --git a/.circleci/config.yml b/.circleci/config.yml index 5f6a0f2d2cc..6b4b6d113fc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,315 +1,367 @@ -# Ruby CircleCI 2.1 configuration file -# -# Check https://circleci.com/docs/2.0/language-ruby/ for more details -# version: 2.1 +orbs: + codecov: codecov/codecov@3.2.5 + +executors: + node: + docker: + - image: cimg/node:18.17.0 + + working_directory: ~/repo + resource_class: large + + ruby_with_postgres: + parameters: + collects_rails_coverage: + type: boolean + default: false + + docker: + - image: cimg/ruby:3.0.5-browsers + environment: + PG_HOST: localhost + PG_USER: ubuntu + RAILS_ENV: test + BUNDLE_APP_CONFIG: ~/repo/.bundle + DATABASE_URL: "postgres://ubuntu@localhost:5432/coursemology_test" + COLLECT_COVERAGE: << parameters.collects_rails_coverage >> + + - image: cimg/postgres:14.5 + environment: + POSTGRES_USER: ubuntu + POSTGRES_DB: coursemology_test + + working_directory: ~/repo + resource_class: large + commands: checkout_with_submodules: steps: - checkout - # Checkout submodules - run: name: Checkout submodules command: git submodule update --init --recursive - restore_ruby_cache: + rehydrate_ruby_deps: steps: - # Install bundler version - - run: - name: Install bundler version 2.2.33 - command: gem install bundler:2.2.33 - - # Restore cached Ruby dependencies - restore_cache: + name: Restore Ruby dependencies cache keys: - v3.0.5-ruby-{{ checksum "Gemfile.lock" }} - # Fallback to using the latest cache if no exact match is found - v3.0.5-ruby- - # Update Ruby dependencies - run: - name: Install dependencies - command: | - bundle install --jobs=4 --retry=3 --path vendor/bundle --without development:production --deployment + name: Install Bundler + command: gem install bundler:2.2.33 + + - run: + name: Install Ruby dependencies + command: bundle install --jobs=4 --retry=3 --path vendor/bundle --without development:production --deployment - # Recache the updated Ruby dependencies - save_cache: paths: - ./vendor/bundle - ./.bundle key: v3.0.5-ruby-{{ checksum "Gemfile.lock" }} - build_and_restore_node_cache: + rehydrate_node_deps: steps: - # Use the desired Node version - - run: - name: Swap Node versions - command: | - set +e - wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.1/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" - nvm install v18.12.1 - nvm alias default 18.12.1 - - echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV - echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV - - # Restore cached Node dependencies - restore_cache: + name: Restore client Yarn dependencies cache keys: - - v18.12.1-node-{{ checksum "client/yarn.lock" }} - # Fallback to using the latest cache if no exact match is found - - v18.12.1-node- + - v18.17.0-node-{{ checksum "client/yarn.lock" }}-{{ checksum "client/vendor/recorderjs/package.json" }} + - v18.17.0-node- - # Update Node dependencies - - run: cd client && yarn install && cd - + - run: + name: Install client Yarn dependencies + working_directory: client + command: yarn install - # Recache the updated Node dependencies - save_cache: paths: - ./client/node_modules - key: v18.12.1-node-{{ checksum "client/yarn.lock" }} + - ./client/vendor/recorderjs/node_modules + key: v18.17.0-node-{{ checksum "client/yarn.lock" }}-{{ checksum "client/vendor/recorderjs/package.json" }} - restore_node_cache: + restore_client_cache: steps: - restore_cache: - name: Restore Node cache + name: Restore client cache keys: - - v18.12.1-node-{{ checksum "client/yarn.lock" }} - # Fallback to using the latest cache if no exact match is found - - v18.12.1-node- + - v1-yarn-build-{{ .Revision }} build_and_cache_client: steps: + - restore_client_cache + - run: name: Build client - command: cd client && yarn build:production && cd - + working_directory: client + command: yarn build:test + - save_cache: paths: - ./client/build - - ./public/webpack key: v1-yarn-build-{{ .Revision }} - restore_client_cache: + setup_db: steps: - - restore_cache: - name: Restore client cache - keys: - - v1-yarn-build-{{ .Revision }} + - run: + name: Set up test database + command: bundle exec rake db:setup + environment: + COLLECT_COVERAGE: false - setup_db: + serve_static_site: steps: - run: - name: Setup DB - command: | - bundle exec rake db:setup + name: Download dirt-cheap-rocket + command: curl https://github.com/Coursemology/dirt-cheap-rocket/releases/latest/download/dirt-cheap-rocket.cjs -o dirt-cheap-rocket.cjs -L + - run: + name: Serve static site + command: node dirt-cheap-rocket.cjs + background: true environment: - DATABASE_URL: "postgres://ubuntu@localhost:5432/coursemology_test" + DCR_CLIENT_PORT: 3200 + DCR_SERVER_PORT: 7979 + DCR_PUBLIC_PATH: /static + DCR_ASSETS_DIR: client/build -jobs: - test: - docker: - # specify the version you desire here - - image: cimg/ruby:3.0.5-browsers - environment: - PG_HOST: localhost - PG_USER: ubuntu - RAILS_ENV: test - BUNDLE_APP_CONFIG: ~/repo/.bundle + serve_rails_server: + steps: + - run: + name: Serve Rails server + command: bundle exec rails s -p 7979 + background: true - # Specify service dependencies here if necessary - # CircleCI maintains a library of pre-built images - # documented at https://circleci.com/docs/2.0/circleci-images/ - - image: cimg/postgres:14.5 - environment: - POSTGRES_USER: ubuntu - POSTGRES_DB: coursemology_test + - run: + name: Wait for Rails server + command: curl -s --retry 1000 --retry-delay 1 --retry-connrefused http://lvh.me:7979 - parallelism: 30 + terminate_rails_and_wait_for_coverage_results: + steps: + - run: + name: Terminate Rails server + command: pkill -SIGTERM -f puma - working_directory: ~/repo + - run: + name: Wait for Rails coverage results + no_output_timeout: 5m + command: until [ -f coverage/codecov-result.json ]; do sleep 1; done + + setup_docker_layer_cache: + steps: + - setup_remote_docker: + version: 20.10.18 + docker_layer_caching: true + + # Install Ghostscript so `identify` in ImageMagick works with PDF files. + # To remove PDF security policy for ImageMagick (Ubuntu 20.04), see https://stackoverflow.com/questions/52998331/imagemagick-security-policy-pdf-blocking-conversion + # This is currently not used as CircleCI would fail to install occasionally. + install_ghostscript_and_imagemagick: + steps: + - run: + name: Install Ghostscript and ImageMagick + command: | + sudo apt update + sudo apt install imagemagick + sudo apt install ghostscript + sudo sed -i '/disable ghostscript format types/,+6d' /etc/ImageMagick-6/policy.xml + +jobs: + build_client: + executor: node steps: - checkout_with_submodules + - rehydrate_node_deps + - build_and_cache_client + + test_playwright: + executor: + name: ruby_with_postgres + collects_rails_coverage: true + + parallelism: 10 - - restore_ruby_cache - - restore_node_cache + steps: + - checkout_with_submodules + - setup_docker_layer_cache + - rehydrate_ruby_deps - restore_client_cache - # Install ghostscript so `identify` in ImageMagick works with PDF files. - # Remove pdf security policy for imagemagick (ubuntu 20.04) - # https://stackoverflow.com/questions/52998331/imagemagick-security-policy-pdf-blocking-conversion - # Installations below are currently disabled as CircleCI would fail to install occasionally - # - run: - # name: install ghostscript and imagemagick - # command: | - # sudo apt update - # sudo apt install imagemagick - # sudo apt install ghostscript - # sudo sed -i '/disable ghostscript format types/,+6d' /etc/ImageMagick-6/policy.xml + - setup_db + - serve_static_site + - serve_rails_server - - setup_remote_docker: - version: 20.10.18 - docker_layer_caching: true + - run: + name: Install Playwright dependencies + working_directory: tests + command: yarn install; yarn prepare + + - run: + name: Run Playwright tests + working_directory: tests + command: | + SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npx playwright test --shard=${SHARD}/${CIRCLE_NODE_TOTAL} --reporter=junit + zip -r test-results.zip playright-report test-results + environment: + PLAYWRIGHT_JUNIT_OUTPUT_NAME: results.xml + + - run: + name: Generate code coverage + working_directory: tests + command: yarn coverage + + - terminate_rails_and_wait_for_coverage_results + + - codecov/upload: + upload_name: playwright-client + file: tests/coverage/lcov.info + + - codecov/upload: + upload_name: playwright-rails + file: coverage/codecov-result.json + + - store_test_results: + path: ~/repo/tests/results.xml + + - store_artifacts: + path: ~/repo/tests/results.xml + + - store_artifacts: + path: ~/repo/tests/test-results.zip + + test_rspec: + executor: + name: ruby_with_postgres + collects_rails_coverage: true + + parallelism: 30 + + steps: + - checkout_with_submodules + - setup_docker_layer_cache + + - rehydrate_ruby_deps + - restore_client_cache - setup_db + - serve_static_site - # Run tests! - run: - name: run + name: Run RSpec tests no_output_timeout: 10m command: | mkdir /tmp/test-results - TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \ - circleci tests split --split-by=timings)" + TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)" bundle exec rspec \ --format progress \ --format RspecJunitFormatter \ --out /tmp/test-results/rspec.xml \ - --format progress \ - $TEST_FILES - environment: - DATABASE_URL: "postgres://ubuntu@localhost:5432/coursemology_test" + -- ${TEST_FILES} - # Collect reports - store_test_results: path: /tmp/test-results + - store_artifacts: path: /tmp/test-results destination: test-results - # The resource_class feature allows configuring CPU and RAM resources for each job. Different resource classes are available for different executors. https://circleci.com/docs/2.0/configuration-reference/#resourceclass - resource_class: large - lint: - docker: - - image: cimg/ruby:3.0.5-browsers - environment: - PG_HOST: localhost - PG_USER: ubuntu - RAILS_ENV: test - BUNDLE_APP_CONFIG: ~/repo/.bundle - - - image: cimg/postgres:14.5 - environment: - POSTGRES_USER: ubuntu - POSTGRES_DB: coursemology_test - - working_directory: ~/repo + factorybot_lint: + executor: ruby_with_postgres steps: - checkout - - - restore_ruby_cache - + - rehydrate_ruby_deps - setup_db - run: name: Run FactoryBot lint - command: | - bundle exec rake factory_bot:lint - environment: - DATABASE_URL: "postgres://ubuntu@localhost:5432/coursemology_test" + command: bundle exec rake factory_bot:lint jslint: - docker: - # specify the version you desire here - - image: cimg/ruby:3.0.5-browsers - environment: - RAILS_ENV: test - - working_directory: ~/repo + executor: node steps: - checkout_with_submodules - - - build_and_restore_node_cache - - - build_and_cache_client + - rehydrate_node_deps - run: - name: Run yarn checks - command: | - cd client - yarn lint - resource_class: large + name: Run ESLint and Prettier checks + working_directory: client + command: yarn lint jstest: - docker: - # specify the version you desire here - - image: cimg/ruby:3.0.5-browsers - environment: - RAILS_ENV: test - - working_directory: ~/repo + executor: node steps: - checkout_with_submodules + - rehydrate_node_deps - - build_and_restore_node_cache - - - build_and_cache_client - - # Build frontend JS - run: - name: Run yarn checks - command: | - cd client && yarn - yarn testci - resource_class: large + name: Build translations + working_directory: client + command: yarn run build:translations - i18n: - docker: - # specify the version you desire here - - image: cimg/ruby:3.0.5-browsers - environment: - PG_HOST: localhost - PG_USER: ubuntu - RAILS_ENV: test - BUNDLE_APP_CONFIG: ~/repo/.bundle + - run: + name: Run Jest tests + working_directory: client + command: yarn testci - # Specify service dependencies here if necessary - # CircleCI maintains a library of pre-built images - # documented at https://circleci.com/docs/2.0/circleci-images/ - - image: cimg/postgres:14.5 - environment: - POSTGRES_USER: ubuntu - POSTGRES_DB: coursemology_test + - codecov/upload: + upload_name: jest + file: client/coverage/lcov.info - working_directory: ~/repo + i18n_en: + executor: ruby_with_postgres steps: - checkout + - rehydrate_ruby_deps + - setup_db - - restore_ruby_cache + - run: + name: Check for unused translations (English) + command: bundle exec i18n-tasks unused --locales en + - run: + name: Check for missing translations (English) + command: bundle exec i18n-tasks missing --locales en + + i18n_zh: + executor: ruby_with_postgres + + steps: + - checkout + - rehydrate_ruby_deps - setup_db - # Run i18n checks! - run: - name: Run i18n checks - command: | - bundle exec i18n-tasks unused - bundle exec i18n-tasks missing - environment: - DATABASE_URL: "postgres://ubuntu@localhost:5432/coursemology_test" - resource_class: large + name: Check for unused translations (Mandarin) + command: bundle exec i18n-tasks unused --locales zh + + - run: + name: Check for missing translations (Mandarin) + command: bundle exec i18n-tasks missing --locales zh workflows: - version: 2 build_and_test_and_lint: jobs: - - lint - jslint - jstest - - i18n - - test: + - build_client + - i18n_en + - i18n_zh + - factorybot_lint + - test_rspec: + requires: + - build_client + - factorybot_lint + - test_playwright: requires: - - lint - - jslint - - jstest + - build_client diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 91600595a1b..00000000000 --- a/.coveralls.yml +++ /dev/null @@ -1 +0,0 @@ -service_name: travis-ci diff --git a/.foreman b/.foreman deleted file mode 100644 index 2abfe5b3e9a..00000000000 --- a/.foreman +++ /dev/null @@ -1,2 +0,0 @@ -# Defaults for foreman -procfile: Procfile.dev diff --git a/.gitignore b/.gitignore index 6859e686c65..34b5ab93fd8 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,10 @@ node_modules /.env.* /client/.env /client/.env.* + +# Ignore Playwright results +test-results/ +playwright-report/ +playwright/.cache/ +.nyc_output +coverage/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index db92c1cd5ec..6a4f29fbf16 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "vendor/assets/stylesheets/pygments-css"] - path = vendor/assets/stylesheets/pygments-css - url = https://github.com/richleland/pygments-css.git [submodule "vendor/assets/javascripts/recorderjs"] path = client/vendor/recorderjs url = https://github.com/mattdiamond/Recorderjs.git diff --git a/.hound.yml b/.hound.yml index eef73806924..5b9d8769909 100644 --- a/.hound.yml +++ b/.hound.yml @@ -4,9 +4,5 @@ rubocop: config_file: .rubocop.yml version: 1.22.1 -jshint: - config_file: .jshintrc - ignore_file: .jshintignore - -scss: - config_file: .scss-lint.yml +javascript: + enabled: false diff --git a/.jshintignore b/.jshintignore deleted file mode 100644 index 1a3430af1dc..00000000000 --- a/.jshintignore +++ /dev/null @@ -1 +0,0 @@ -client/** diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index ffd89d07e06..00000000000 --- a/.jshintrc +++ /dev/null @@ -1,27 +0,0 @@ -{ - "boss": false, - "browser": true, - "camelcase": true, - "curly": true, - "eqeqeq": true, - "eqnull": true, - "expr": true, - "forin": true, - "globals": { - "jQuery": true, - "$": true, - "ace": true - }, - "immed": true, - "indent": 4, - "latedef": "nofunc", - "maxlen": 100, - "newcap": true, - "noarg": true, - "quotmark": "single", - "strict": true, - "sub": true, - "trailing": true, - "undef": true, - "unused": true -} diff --git a/.scss-lint.yml b/.scss-lint.yml deleted file mode 100644 index f5ffd4a7ac2..00000000000 --- a/.scss-lint.yml +++ /dev/null @@ -1,40 +0,0 @@ -scss_files: 'app/**/*.scss' - -linters: - # Unhounding. - HexLength: - enabled: true - - StringQuotes: - style: single_quotes - - # Project-specific options - HexLength: - style: long - - IdSelector: - enabled: false - - LeadingZero: - style: include_zero - - PlaceholderInExtend: - enabled: false - - QualifyingElement: - allow_element_with_class: true - - NestingDepth: - max_depth: 5 - - SelectorDepth: - max_depth: 5 - - # CSS Modules requires class selectors to be in camelCase - SelectorFormat: - ignored_types: [class] - - # Allow CSS Modules composition - PropertySpelling: - extra_properties: - - composes diff --git a/Gemfile b/Gemfile index e888fe982b7..ace0d1f2d5a 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,9 @@ gem 'rails', '~> 6.0.6.1' # Use PostgreSQL for the backend gem 'pg' +# Enables CORS configuration to allow sharing resources with client on another domain +gem 'rack-cors' + # Instance/Course settings gem 'settings_on_rails' # Manage read/unread status @@ -35,25 +38,6 @@ gem 'active_record_upsert', '0.11.1' # Create pretty URLs and work with human-friendly strings gem 'friendly_id' -# Use SCSS for stylesheets -gem 'sass-rails' -# Use Uglifier as compressor for JavaScript assets -gem 'uglifier', '>= 1.3.0' - -# TODO: Check compatibility with webpacker 3.2.0 when it is released. -# https://github.com/rails/webpacker/blob/4f65c5ee58666bbe58b234c48d47ec7d48fab4d8/CHANGELOG.md -gem 'webpacker', '<= 5.4.4' -# Internationalisation for JavaScript. -gem 'i18n-js', '<= 3.10.0' - -# Use jQuery as the JavaScript library -gem 'jquery-rails' -# Our Coursemology will be themed using Bootstrap -gem 'bootstrap-sass' -gem 'bootstrap-sass-extras', '>= 0.1.0' -gem 'autoprefixer-rails' -# Use font-awesome for icons -gem 'font-awesome-rails' # HTML Pipeline and dependencies gem 'html-pipeline' gem 'sanitize', '>= 4.6.3' @@ -63,10 +47,6 @@ gem 'html-pipeline-rouge_filter', git: 'https://github.com/ekowidianto/html-pipe gem 'jbuilder' # Slim as the templating language gem 'slim-rails' -# ejs for client-side templates -gem 'ejs' -# High Voltage for static pages -gem 'high_voltage' # Paginator for Rails gem 'kaminari' # Work with Docker @@ -84,10 +64,6 @@ group :development do gem 'spring', platforms: [:ruby] gem 'listen' - # Gems to make development mode faster and less painful - - gem 'wdm', '>= 0.0.3', platforms: [:mswin, :mswin64] - # Helps to prevent database slowdowns gem 'lol_dba', require: false @@ -97,16 +73,12 @@ group :development do # bundle exec yardoc generates the API under doc/. # Use yard stats --list-undoc to find what needs documenting. gem 'yard', group: :doc - - # Gem to generate favicon - gem 'rails_real_favicon' end group :test do gem 'email_spec' gem 'rspec-html-matchers' gem 'should_not' - gem 'simplecov' gem 'shoulda-matchers' # Capybara for feature testing @@ -139,11 +111,13 @@ group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platform: :mri -end -group :ci do # Code Coverage reporters + gem 'simplecov' gem 'codecov', require: false +end + +group :ci do gem 'rspec-retry' gem 'rspec_junit_formatter' gem 'rubocop-rails' @@ -199,18 +173,6 @@ gem 'cancancan' # We also want stricter sanitization. gem 'rails_utils', git: 'https://github.com/raymondtangsc/rails_utils.git', branch: 'full-sanitize-flash' -# Themes for instances -gem 'themes_on_rails', '>= 0.3.1', git: 'https://github.com/raymondtangsc/themes_on_rails', - branch: 'xtang/rails_6' - -# Forms made easy for Rails -gem 'simple_form' -gem 'simple_form-bootstrap', git: 'https://github.com/purfectliterature/simple_form-bootstrap' -# Dynamic nested forms -gem 'cocoon' -gem 'bootstrap_tokenfield_rails' -gem 'twitter-typeahead-rails' - # Using CarrierWave for file uploads gem 'carrierwave' # Generate sequential filenames @@ -236,6 +198,5 @@ gem 'rwordnet', git: 'https://github.com/makqien/rwordnet' gem 'loofah', '>= 2.2.1' gem 'rails-html-sanitizer', '>= 1.0.4' -gem 'sprockets', '< 4.0.0' gem 'mimemagic', '0.4.3' gem 'ffi', '>= 1.14.2' diff --git a/Gemfile.lock b/Gemfile.lock index f57ffe3da69..4de339a3e00 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -36,17 +36,6 @@ GIT specs: rwordnet (2.0.0) -GIT - remote: https://github.com/purfectliterature/simple_form-bootstrap - revision: a7b5759e3a569a65d0e2b575c9ae38a7d5c9e5fc - specs: - simple_form-bootstrap (1.6.0) - actionpack (>= 4.1) - activemodel (>= 4.1) - bootstrap-sass (~> 3) - railties (>= 4.1) - simple_form (>= 3.1.0) - GIT remote: https://github.com/raymondtangsc/rails_utils.git revision: 5c8c0caacf08985ae14c5d007529a09d5f284da0 @@ -55,14 +44,6 @@ GIT rails_utils (4.0.0) rails (>= 3.2) -GIT - remote: https://github.com/raymondtangsc/themes_on_rails - revision: 4446d795aad4eaf730f7250646f25bfdb09de799 - branch: xtang/rails_6 - specs: - themes_on_rails (0.4.0) - rails (>= 3.2) - GEM remote: https://rubygems.org/ specs: @@ -135,8 +116,6 @@ GEM activerecord (>= 3.0.0) activesupport (>= 3.0.0) ast (2.4.2) - autoprefixer-rails (10.4.13.0) - execjs (~> 2) aws-eventstream (1.2.0) aws-partitions (1.792.0) aws-sdk-core (3.179.0) @@ -161,12 +140,6 @@ GEM erubi (~> 1.4) parser (>= 2.4) smart_properties - bootstrap-sass (3.4.1) - autoprefixer-rails (>= 5.2.1) - sassc (>= 2.0.0) - bootstrap-sass-extras (0.1.0) - rails (>= 3.1.0) - bootstrap_tokenfield_rails (0.12.1) builder (3.2.4) bullet (7.0.7) activesupport (>= 3.0.0) @@ -199,7 +172,6 @@ GEM mini_mime (>= 0.1.3) ssrf_filter (~> 1.0) childprocess (4.1.0) - cocoon (1.2.15) codecov (0.6.0) simplecov (>= 0.15, < 0.22) concurrent-ruby (1.2.2) @@ -226,7 +198,6 @@ GEM multi_json edge (0.6.1) activerecord (>= 5.0.0) - ejs (1.1.1) email_spec (2.2.2) htmlentities (~> 4.3.3) launchy (~> 2.1) @@ -235,7 +206,6 @@ GEM et-orbi (1.2.7) tzinfo excon (0.92.3) - execjs (2.8.1) exifr (1.3.9) factory_bot (6.2.1) activesupport (>= 5.0.0) @@ -261,8 +231,6 @@ GEM fog-xml (0.1.4) fog-core nokogiri (>= 1.5.11, < 2.0.0) - font-awesome-rails (4.7.0.8) - railties (>= 3.2, < 8.0) formatador (1.1.0) friendly_id (5.5.0) activerecord (>= 4.0.0) @@ -272,7 +240,6 @@ GEM raabro (~> 1.4) globalid (1.1.0) activesupport (>= 5.0) - high_voltage (3.1.2) highline (2.0.3) html-pipeline (2.14.3) activesupport (>= 2) @@ -281,8 +248,6 @@ GEM http_accept_language (2.1.1) i18n (1.14.1) concurrent-ruby (~> 1.0) - i18n-js (3.9.2) - i18n (>= 0.6.6) i18n-tasks (1.0.12) activesupport (>= 4.0.2) ast (>= 2.1.0) @@ -314,10 +279,6 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.2) - jquery-rails (4.5.1) - rails-dom-testing (>= 1, < 3) - railties (>= 4.2.0) - thor (>= 0.14, < 2.0) json (2.6.3) kaminari (1.2.2) activesupport (>= 4.1.0) @@ -400,12 +361,12 @@ GEM raabro (1.4.0) racc (1.7.1) rack (2.2.7) + rack-cors (2.0.1) + rack (>= 2.0.0) rack-mini-profiler (3.1.0) rack (>= 1.2.0) rack-protection (2.2.3) rack - rack-proxy (0.7.6) - rack rack-test (2.1.0) rack (>= 1.3) rails (6.0.6.1) @@ -437,10 +398,6 @@ GEM rails-i18n (7.0.5) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - rails_real_favicon (0.1.1) - json (>= 1.7, < 3) - rails - rubyzip (~> 2) railties (6.0.6.1) actionpack (= 6.0.6.1) activesupport (= 6.0.6.1) @@ -534,22 +491,11 @@ GEM sanitize (6.0.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) - sass-rails (6.0.0) - sassc-rails (~> 2.1, >= 2.1.1) - sassc (2.4.0) - ffi (~> 1.9) - sassc-rails (2.1.2) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt selenium-webdriver (4.5.0) childprocess (>= 0.5, < 5.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - semantic_range (3.0.0) settings_on_rails (0.3.1) activerecord (>= 3.1) should_not (1.1.0) @@ -563,9 +509,6 @@ GEM fugit (~> 1.8) globalid (>= 1.0.1) sidekiq (>= 6) - simple_form (5.1.0) - actionpack (>= 5.2) - activemodel (>= 5.2) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) @@ -604,14 +547,8 @@ GEM timeout (0.4.0) traceroute (0.8.1) rails (>= 3.0.0) - twitter-typeahead-rails (0.11.1) - actionpack (>= 3.1) - jquery-rails - railties (>= 3.1) tzinfo (1.2.11) thread_safe (~> 0.1) - uglifier (4.2.0) - execjs (>= 0.3.0, < 3) unicode-display_width (2.4.2) uniform_notifier (1.16.0) unread (0.12.0) @@ -625,11 +562,6 @@ GEM nokogiri (~> 1.6) rubyzip (>= 1.3.0) selenium-webdriver (~> 4.0) - webpacker (5.4.4) - activesupport (>= 5.2) - rack-proxy (>= 0.6.1) - railties (>= 5.2) - semantic_range (>= 2.3.0) websocket (1.2.9) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) @@ -653,11 +585,7 @@ DEPENDENCIES activerecord-userstamp! acts_as_tenant after_commit_action - autoprefixer-rails aws-sdk-s3 - bootstrap-sass - bootstrap-sass-extras (>= 0.1.0) - bootstrap_tokenfield_rails bullet (>= 4.14.9) byebug calculated_attributes @@ -666,7 +594,6 @@ DEPENDENCIES capybara-screenshot capybara-selenium carrierwave - cocoon codecov consistency_fail coursemology-polyglot! @@ -675,24 +602,19 @@ DEPENDENCIES devise_masquerade docker-api edge - ejs email_spec factory_bot_rails ffi (>= 1.14.2) filename flamegraph fog-aws (= 3.8.0) - font-awesome-rails friendly_id - high_voltage html-pipeline html-pipeline-rouge_filter! http_accept_language - i18n-js (<= 3.10.0) i18n-tasks image_optim_rails jbuilder - jquery-rails kaminari listen lograge @@ -705,11 +627,11 @@ DEPENDENCIES parallel_tests pg puma + rack-cors rack-mini-profiler rails (~> 6.0.6.1) rails-controller-testing rails-html-sanitizer (>= 1.0.4) - rails_real_favicon rails_utils! recaptcha record_tag_helper @@ -725,30 +647,21 @@ DEPENDENCIES rubyzip rwordnet! sanitize (>= 4.6.3) - sass-rails settings_on_rails should_not shoulda-matchers sidekiq sidekiq-cron - simple_form - simple_form-bootstrap! simplecov sinatra slim-rails spring - sprockets (< 4.0.0) stackprof - themes_on_rails (>= 0.3.1)! traceroute - twitter-typeahead-rails tzinfo-data - uglifier (>= 1.3.0) unread validates_hostname - wdm (>= 0.0.3) webdrivers - webpacker (<= 5.4.4) workflow workflow-activerecord (>= 4.1, < 7.0) yard diff --git a/Procfile.dev b/Procfile.dev deleted file mode 100644 index a5b163de63d..00000000000 --- a/Procfile.dev +++ /dev/null @@ -1,2 +0,0 @@ -web: bundle exec rails server -client: cd client && yarn build:development diff --git a/Procfile.profile b/Procfile.profile deleted file mode 100644 index 561a0a80794..00000000000 --- a/Procfile.profile +++ /dev/null @@ -1,2 +0,0 @@ -web: bundle exec rails server -client: cd client && yarn build:profile diff --git a/app/assets/images/favicon/android-chrome-192x192.png b/app/assets/images/favicon/android-chrome-192x192.png deleted file mode 100644 index cd6431744c6..00000000000 Binary files a/app/assets/images/favicon/android-chrome-192x192.png and /dev/null differ diff --git a/app/assets/images/favicon/android-chrome-512x512.png b/app/assets/images/favicon/android-chrome-512x512.png deleted file mode 100644 index 62ed6f7688d..00000000000 Binary files a/app/assets/images/favicon/android-chrome-512x512.png and /dev/null differ diff --git a/app/assets/images/favicon/apple-touch-icon.png b/app/assets/images/favicon/apple-touch-icon.png deleted file mode 100644 index ac27b587e24..00000000000 Binary files a/app/assets/images/favicon/apple-touch-icon.png and /dev/null differ diff --git a/app/assets/images/favicon/browserconfig.xml.erb b/app/assets/images/favicon/browserconfig.xml.erb deleted file mode 100644 index 0cd24fdec19..00000000000 --- a/app/assets/images/favicon/browserconfig.xml.erb +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #ffc40d - - - diff --git a/app/assets/images/favicon/favicon-16x16.png b/app/assets/images/favicon/favicon-16x16.png deleted file mode 100644 index 0c07f0f490b..00000000000 Binary files a/app/assets/images/favicon/favicon-16x16.png and /dev/null differ diff --git a/app/assets/images/favicon/favicon-32x32.png b/app/assets/images/favicon/favicon-32x32.png deleted file mode 100644 index 4f85df338c3..00000000000 Binary files a/app/assets/images/favicon/favicon-32x32.png and /dev/null differ diff --git a/app/assets/images/favicon/favicon.ico b/app/assets/images/favicon/favicon.ico deleted file mode 100644 index 53a745d7246..00000000000 Binary files a/app/assets/images/favicon/favicon.ico and /dev/null differ diff --git a/app/assets/images/favicon/manifest.json.erb b/app/assets/images/favicon/manifest.json.erb deleted file mode 100644 index 37ed60b2382..00000000000 --- a/app/assets/images/favicon/manifest.json.erb +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "Coursemology", - "icons": [ - { - "src": "<%= asset_path 'favicon/android-chrome-192x192.png' %>", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "<%= asset_path 'favicon/android-chrome-512x512.png' %>", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} \ No newline at end of file diff --git a/app/assets/images/favicon/mstile-144x144.png b/app/assets/images/favicon/mstile-144x144.png deleted file mode 100644 index d8f54a4ca97..00000000000 Binary files a/app/assets/images/favicon/mstile-144x144.png and /dev/null differ diff --git a/app/assets/images/favicon/mstile-150x150.png b/app/assets/images/favicon/mstile-150x150.png deleted file mode 100644 index 116cdb7b33a..00000000000 Binary files a/app/assets/images/favicon/mstile-150x150.png and /dev/null differ diff --git a/app/assets/images/favicon/mstile-310x150.png b/app/assets/images/favicon/mstile-310x150.png deleted file mode 100644 index 6d367cb1b89..00000000000 Binary files a/app/assets/images/favicon/mstile-310x150.png and /dev/null differ diff --git a/app/assets/images/favicon/mstile-310x310.png b/app/assets/images/favicon/mstile-310x310.png deleted file mode 100644 index 1975c66f787..00000000000 Binary files a/app/assets/images/favicon/mstile-310x310.png and /dev/null differ diff --git a/app/assets/images/favicon/mstile-70x70.png b/app/assets/images/favicon/mstile-70x70.png deleted file mode 100644 index 6efc88f1158..00000000000 Binary files a/app/assets/images/favicon/mstile-70x70.png and /dev/null differ diff --git a/app/assets/images/favicon/safari-pinned-tab.svg b/app/assets/images/favicon/safari-pinned-tab.svg deleted file mode 100644 index e8824c3ee02..00000000000 --- a/app/assets/images/favicon/safari-pinned-tab.svg +++ /dev/null @@ -1,123 +0,0 @@ - - - - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - - - - - - - - - - - - diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js deleted file mode 100644 index c805df19169..00000000000 --- a/app/assets/javascripts/application.js +++ /dev/null @@ -1,22 +0,0 @@ -// This is a manifest file that'll be compiled into application.js, which will include all the files -// listed below. -// -// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, -// vendor/assets/javascripts, or vendor/assets/javascripts of plugins, if any, can be referenced -// here using a relative path. -// -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. -// -// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details -// about supported directives. -// -//= require jquery_ujs -//= require i18n/translations -//= require bootstrap-sprockets -//= require twitter/typeahead -//= require bootstrap-tokenfield -//= require simple_form-bootstrap -//= require layout -//= require cocoon -//= require_tree . diff --git a/app/assets/javascripts/layout.js b/app/assets/javascripts/layout.js deleted file mode 100644 index 5b3cc2c468b..00000000000 --- a/app/assets/javascripts/layout.js +++ /dev/null @@ -1,22 +0,0 @@ -(function ($) { - 'use strict'; - - function initializeComponents(element) { - $('[data-toggle="popover"]', element).popover(); - // Tooltips are attached to elements with a title attribute, except for the Facebook button. - // See https://github.com/Coursemology/coursemology-theme/pull/5 - $('[title]', element).not('.fb-like *').tooltip(); - } - - // Queue component initialisation until the script has completely loaded. - // - // This prevents missing definitions for things like Ace themes, which are loaded after the - // application script. - $(function () { - initializeComponents(document); - - $(document).on('nested:fieldAdded', function (e) { - initializeComponents(e.field); - }); - }); -})(jQuery); diff --git a/app/assets/javascripts/patches/dropdown_mobile_fix.js b/app/assets/javascripts/patches/dropdown_mobile_fix.js deleted file mode 100644 index 9ccd566d0af..00000000000 --- a/app/assets/javascripts/patches/dropdown_mobile_fix.js +++ /dev/null @@ -1,17 +0,0 @@ -// Dropdown does not work on iOS devices as only click delegation for a and input are supported -// Event delegation for iOS http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html -// Proposed solution is modified from http://stackoverflow.com/a/22318440 - -(function ($) { - 'use strict'; - - function initializeDropdownEventListener() { - $('[data-toggle=dropdown]').each(function () { - this.addEventListener('click', function () {}, false); - }); - } - - $(function () { - initializeDropdownEventListener(); - }); -})(jQuery); diff --git a/app/assets/stylesheets/_variables.scss b/app/assets/stylesheets/_variables.scss deleted file mode 100644 index 3c020366c5b..00000000000 --- a/app/assets/stylesheets/_variables.scss +++ /dev/null @@ -1,35 +0,0 @@ -// -// Variables -// -------------------------------------------------- - -//== Colors -// -$grey: #808080 !default; -$red: #ff0000 !default; -$white: #ffffff !default; - -//## Variables for colors used in Coursemology. - -//== Fonts -// -//## Code editor typography. -$code-font-family: $font-family-monospace !default; -$code-font-size: $font-size-base !default; - -//== Icons and Logos -// - -//## Common sizes to be used across Coursemology. -$picture-thumb: 25px !default; -$picture-small: 75px !default; -$picture-medium: 100px !default; -$picture-large: 140px !default; - -//## Variables for icons or logos used in Coursemology. -$course-user-badge-achievement-badge: $picture-thumb !default; - -$course-layout-sidebar-logo: $picture-medium !default; - -$user-profile-picture: $picture-large !default; - -//== Others diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb deleted file mode 100644 index e3a35dba596..00000000000 --- a/app/assets/stylesheets/application.scss.erb +++ /dev/null @@ -1,25 +0,0 @@ -@import 'layout'; -@import 'mixins/*'; - -<% -# Import the rest of the files; @import '**/*' will include application.scss multiple times. -# This is not perfect because every time a new file is added all assets need to be cleaned for the -# new set of assets to be generated. -# -# TODO: Use compass-import-once after Compass/compass#1951 is fixed -# TODO: Revert to @import '**/*' after sass/sass#139 is fixed in sass-4.0. -exclude_imports = ['layout', 'mixins', 'application.scss'] -imports = Dir["#{__dir__}/*"] -imports.reject! do |path| - basename = File.basename(path, '.*') - basename.start_with?('_') || exclude_imports.include?(basename) -end -imports.map! do |path| - file_path = File.file?(path) - path = path[(__dir__.length + 1)..] - file_path ? path : "#{path}/**/*" -end - -imports.each do |file| %> -@import '<%= file %>'; -<% end %> diff --git a/app/assets/stylesheets/attachments.scss b/app/assets/stylesheets/attachments.scss deleted file mode 100644 index 74f0c63785d..00000000000 --- a/app/assets/stylesheets/attachments.scss +++ /dev/null @@ -1,8 +0,0 @@ -.attachment { - margin-bottom: 0.5em; - - .delete-attachment, - .uploaded-by { - padding-left: 1em; - } -} diff --git a/app/assets/stylesheets/course/assessment/question_bundle_assignments.scss b/app/assets/stylesheets/course/assessment/question_bundle_assignments.scss deleted file mode 100644 index 70f6f0cf067..00000000000 --- a/app/assets/stylesheets/course/assessment/question_bundle_assignments.scss +++ /dev/null @@ -1,15 +0,0 @@ -.course-assessment-question-bundle-assignments { - .validation-desc { - margin-top: 5px; - } - - .question-group-select { - display: inline-block; - width: 70%; - margin-right: 5px; - } - - .question-group-errors { - display: inline-block; - } -} diff --git a/app/assets/stylesheets/course/layout.scss b/app/assets/stylesheets/course/layout.scss deleted file mode 100644 index 741ddc97a49..00000000000 --- a/app/assets/stylesheets/course/layout.scss +++ /dev/null @@ -1,163 +0,0 @@ -$sidebar-width: 21rem; -$sidebar-margin-side: 1.5rem; - -.course-layout { - display: flex; - - @media (max-width: $screen-sm-min) { - flex-direction: column; - } - - #course-badge-achievement { - margin-bottom: 1em; - margin-left: 0.5em; - - .image > img { - height: $course-user-badge-achievement-badge; - width: $course-user-badge-achievement-badge; - } - - .achievement > a:hover { - text-decoration: none; - } - - .achievement-difference { - margin-left: 6px; - } - } - - #course-badge-level { - margin-left: 0.5em; - - .experience-points { - font-weight: 300; - } - - .level { - font-size: 150%; - margin-right: 0.5em; - } - - .next-level { - font-size: 85%; - margin-bottom: 1em; - } - - .progress { - height: 12px; - margin-bottom: 3px; - margin-top: 6px; - } - } - - #course-sidebar-logo { - .image > img { - height: $course-layout-sidebar-logo; - width: $course-layout-sidebar-logo; - } - } - - #course-navigation-sidebar { - .nav-icons > .fa { - font-size: 1.5em; - height: 0; - line-height: 0; - margin-right: 0.2em; - position: relative; - text-align: center; - top: 0.1em; - width: 1.5em; - } - - .unread { - margin-left: 0.4em; - } - - #admin-header-sidebar { - font-weight: bold; - margin: 30px 15px 10px; - } - } - - #users-container { - display: flex; - flex-wrap: wrap; - } - - .user { - align-items: center; - display: flex; - padding: 1rem $sidebar-margin-side; - - #user-sidebar { - display: flex; - flex-direction: column; - flex-grow: 1; - overflow: hidden; - text-overflow: ellipsis; - } - - #user-link-sidebar { - @include text-overflow; - font-size: 18px; - font-weight: bold; - flex: 1; - } - - #manage-email-subscription-link-sidebar { - font-size: 12px; - } - - .image img { - max-width: inherit; - } - - .image, - #user-link-sidebar { - display: table-cell; - vertical-align: middle; - } - } - - .notification + .notification { - margin-top: 10px; - } - - #hide-sidebar { - border: 0; - } - - #show-sidebar { - border: 0; - position: absolute; - z-index: 999; - } - - #full-sidebar { - margin-right: $sidebar-margin-side; - width: $sidebar-width; - - @media (max-width: $screen-sm-min) { - margin-bottom: $sidebar-margin-side; - margin-right: 0; - width: 100%; - } - } - - // TODO: Revisit this once SPA and see if we can deal with just `width: 100%;` - // For some reasons in /courses/:id/assessments/:id/submissions/:id/edit page, - // `width: 100%` causes `.page-content` to take the width of `.course-layout`. - // This 'patch' calculates the true width of `.page-content` and ensure the - // content never bulges out of `.course-layout`. I am not proud of this patch. - .page-content { - width: calc(100% - #{$sidebar-width} - #{$sidebar-margin-side}); - - @media (max-width: $screen-sm-min) { - width: 100%; - } - } - - .collapse:not(.in) + .page-content { - width: 100%; - } -} diff --git a/app/assets/stylesheets/course/statistics.scss b/app/assets/stylesheets/course/statistics.scss deleted file mode 100644 index 9a8b288ae85..00000000000 --- a/app/assets/stylesheets/course/statistics.scss +++ /dev/null @@ -1,3 +0,0 @@ -.progress { - margin: 0; -} diff --git a/app/assets/stylesheets/formatters.scss b/app/assets/stylesheets/formatters.scss deleted file mode 100644 index a50dd0499c0..00000000000 --- a/app/assets/stylesheets/formatters.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Styles for formatting resources. -.user { - > .image > img { - @include fit-round-img(50px); - margin-right: 0.5em; - } -} diff --git a/app/assets/stylesheets/jobs.scss b/app/assets/stylesheets/jobs.scss deleted file mode 100644 index 726c4349aa6..00000000000 --- a/app/assets/stylesheets/jobs.scss +++ /dev/null @@ -1,6 +0,0 @@ -.jobs.show { - .spinner { - font-size: 500%; - text-align: center; - } -} diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss deleted file mode 100644 index a4b5c02deb7..00000000000 --- a/app/assets/stylesheets/layout.scss +++ /dev/null @@ -1,65 +0,0 @@ -@import 'bootstrap-sprockets'; -@import 'bootstrap'; - -@import 'variables'; -@import 'tokenfield-typeahead'; -@import 'bootstrap-tokenfield'; - -// Fixes for compatibility with Typeahead v0.11 -.tt-menu { - @extend .tt-dropdown-menu; -} - -@import 'font-awesome'; - -// scss-lint:disable SelectorFormat -div.ace_editor, // We disable the linter because this is generated by script. -// scss-lint:enable SelectorFormat -textarea.code { - font-family: $code-font-family; - font-size: $code-font-size; -} - -table.codehilite { - tr { - td { - border: 0; - padding: 0; - - &.line-number { - font-family: $font-family-monospace; - font-size: ($font-size-base - 1); - width: 1%; // Force the line numbers to occupy as little space as possible. - - // Inspired by GitHub to ensure copying the content does not copy line numbers - &::before { - content: attr(data-line-number); - padding-right: $line-height-computed; - } - } - } - } - - pre { - background-color: transparent; - border: 0; - margin: 0; - padding: 0; - } -} - -.sidebar { - padding: 0; -} - -.nav-tabs { - margin-bottom: 1em; -} - -.attachment-uploader { - margin-bottom: 1em; -} - -img { - max-width: 100%; -} diff --git a/app/assets/stylesheets/mixins/_dim.scss b/app/assets/stylesheets/mixins/_dim.scss deleted file mode 100644 index a0f1479271e..00000000000 --- a/app/assets/stylesheets/mixins/_dim.scss +++ /dev/null @@ -1,6 +0,0 @@ -// Dimming style mixin. Indicates the item is currently disabled. -@mixin dim($bg-color) { - background-color: $bg-color; - opacity: 0.9; - padding: 0.8em; -} diff --git a/app/assets/stylesheets/mixins/_image.scss b/app/assets/stylesheets/mixins/_image.scss deleted file mode 100644 index 44d97e5593d..00000000000 --- a/app/assets/stylesheets/mixins/_image.scss +++ /dev/null @@ -1,12 +0,0 @@ -// Keep the aspect ratio of the image. -@mixin no-stretch { - object-fit: cover; - object-position: center; -} - -@mixin fit-round-img($width, $height: $width) { - @include no-stretch; - width: $width; - height: $height; - border-radius: 50%; -} diff --git a/app/assets/stylesheets/mixins/_text_overflow.scss b/app/assets/stylesheets/mixins/_text_overflow.scss deleted file mode 100644 index 62d8cf5f27f..00000000000 --- a/app/assets/stylesheets/mixins/_text_overflow.scss +++ /dev/null @@ -1,5 +0,0 @@ -@mixin text-overflow() { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} diff --git a/app/assets/stylesheets/users.scss b/app/assets/stylesheets/users.scss deleted file mode 100644 index e4068771a0e..00000000000 --- a/app/assets/stylesheets/users.scss +++ /dev/null @@ -1,7 +0,0 @@ -.users { - &.show { - .image > img { - @include fit-round-img($user-profile-picture); - } - } -} diff --git a/app/controllers/announcements_controller.rb b/app/controllers/announcements_controller.rb index 0eec13d17e7..77e6a3cdf2d 100644 --- a/app/controllers/announcements_controller.rb +++ b/app/controllers/announcements_controller.rb @@ -4,7 +4,6 @@ class AnnouncementsController < ApplicationController def index respond_to do |format| - format.html format.json do announcements = requesting_unread? ? unread_global_announcements : global_announcements @announcements = announcements.includes(:creator) @@ -12,17 +11,19 @@ def index end end - # Marks the current GenericAnnouncement as read by the current user and responds without a body. - # This is meant to be called via javascript. def mark_as_read - @announcement.mark_as_read! for: current_user - head :ok + if current_user + @announcement.mark_as_read! for: current_user + head :ok + else + head :no_content + end end protected def publicly_accessible? - requesting_unread? + requesting_unread? || action_name.to_sym == :mark_as_read end private diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6761178fee9..d05b7e44577 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,7 +10,6 @@ class ApplicationController < ActionController::Base include ApplicationControllerMultitenancyConcern include ApplicationComponentsConcern include ApplicationInternationalizationConcern - include ApplicationThemingConcern include ApplicationUserConcern include ApplicationUserTimeZoneConcern include ApplicationInstanceUserConcern @@ -21,6 +20,9 @@ class ApplicationController < ActionController::Base rescue_from IllegalStateError, with: :handle_illegal_state_error rescue_from ActionController::InvalidAuthenticityToken, with: :handle_csrf_error + def index + end + protected # Runs the provided block with Bullet disabled. @@ -38,21 +40,14 @@ def without_bullet private - # Handles +IllegalStateError+s with a HTTP 422. def handle_illegal_state_error(exception) @exception = exception - respond_to do |format| - format.html { render file: 'public/422', layout: false, status: 422 } - format.json { render file: 'public/422.json', layout: false, status: 422 } - end + render json: { error: exception.message }, status: :unprocessable_entity end def handle_csrf_error(exception) @exception = exception - respond_to do |format| - format.html { render file: 'public/403', layout: false, status: 403 } - format.json { render file: 'public/403.json', layout: false, status: 403 } - end + render json: { error: "Can't verify CSRF token authenticity" }, status: :forbidden end # lograge diff --git a/app/controllers/attachment_references_controller.rb b/app/controllers/attachment_references_controller.rb index bd33997443e..75dfc77dee4 100644 --- a/app/controllers/attachment_references_controller.rb +++ b/app/controllers/attachment_references_controller.rb @@ -19,22 +19,12 @@ def destroy success = @attachment_reference.destroy respond_to do |format| - format.html { render_html_response(success) } format.json { render_json_response(success) } end end private - def render_html_response(success) - if success - flash.now[:success] = t('.success') - else - flash.now[:danger] = t('.failure', - error: @attachment_reference.errors.full_messsages.to_sentence) - end - end - def render_json_response(success) if success head :ok diff --git a/app/controllers/concerns/application_components_concern.rb b/app/controllers/concerns/application_components_concern.rb index 8731a39bbbf..58e6dbcdef6 100644 --- a/app/controllers/concerns/application_components_concern.rb +++ b/app/controllers/concerns/application_components_concern.rb @@ -10,6 +10,6 @@ module ApplicationComponentsConcern def handle_component_not_found(exception) @exception = exception - render file: 'public/404', layout: false, status: :not_found + render json: { error: 'Component not found' }, status: :not_found end end diff --git a/app/controllers/concerns/application_multitenancy.rb b/app/controllers/concerns/application_multitenancy.rb index f9714dd8668..6e9c1fb4a8b 100644 --- a/app/controllers/concerns/application_multitenancy.rb +++ b/app/controllers/concerns/application_multitenancy.rb @@ -13,8 +13,7 @@ def deduce_tenant tenant_host = deduce_tenant_host instance = Instance.find_tenant_by_host_or_default(tenant_host) - if Rails.env.production? && instance && instance.default? && - instance.host.casecmp(tenant_host) != 0 + if Rails.env.production? && instance.default? && instance.host.casecmp(tenant_host) != 0 raise ActionController::RoutingError, 'Instance Not Found' end @@ -25,7 +24,13 @@ def deduce_tenant # @return [String] The host, with www removed. def deduce_tenant_host if Rails.env.development? - 'coursemology.org' + default_app_host = Application::Application.config.x.default_app_host + + if request.host.downcase.ends_with?(default_app_host) + request.host.sub(default_app_host, 'coursemology.org') + else + 'coursemology.org' + end elsif request.host.downcase.start_with?('www.') request.host[4..] else diff --git a/app/controllers/concerns/application_theming_concern.rb b/app/controllers/concerns/application_theming_concern.rb deleted file mode 100644 index d675e06dc40..00000000000 --- a/app/controllers/concerns/application_theming_concern.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true -module ApplicationThemingConcern - extend ActiveSupport::Concern - - included do - theme :deduce_theme - end - - private - - def deduce_theme - priorities = [] - priorities << current_tenant.host if current_tenant - priorities.find(&method(:theme_exists?)) || 'default' - end - - # Checks if the given theme exists. - # - # @param [String] theme_name The name of the theme to check. - # @return [Boolean] True if the theme exists. - def theme_exists?(theme_name) - File.exist?("#{themes_path}/#{theme_name}") - end - - # Gets the path to the themes directory. - # - # @return [String] The path to the themes directory - def themes_path - "#{Rails.root}/app/themes" - end -end diff --git a/app/controllers/concerns/application_user_concern.rb b/app/controllers/concerns/application_user_concern.rb index 51c0789ae2f..d6c3a50589b 100644 --- a/app/controllers/concerns/application_user_concern.rb +++ b/app/controllers/concerns/application_user_concern.rb @@ -21,11 +21,10 @@ def url_to_user_or_course_user(course, user) protected def publicly_accessible? - is_a?(HighVoltage::PagesController) + action_name.to_sym == :index end def handle_access_denied(exception) - @exception = exception - render 'pages/403', status: :forbidden + render json: { errors: exception.message }, status: :forbidden end end diff --git a/app/controllers/concerns/course/lesson_plan/strategies/base_personalization_strategy.rb b/app/controllers/concerns/course/lesson_plan/strategies/base_personalization_strategy.rb index 1bffa34ea3f..c391c7e0326 100644 --- a/app/controllers/concerns/course/lesson_plan/strategies/base_personalization_strategy.rb +++ b/app/controllers/concerns/course/lesson_plan/strategies/base_personalization_strategy.rb @@ -63,10 +63,10 @@ def execute(_course_user, _precomputed_data, _items_to_shift = nil) protected - # Round to "nearest" date in course's timezone, NOT user's timezone. + # Round to "nearest" date in course's time zone, NOT user's time zone. # # @param [ActiveSupport::TimeWithZone] datetime The datetime object to round. - # @param [String] course_tz The timezone of the course. + # @param [String] course_tz The time zone of the course. # @param [Boolean] to_2359 Whether to round off to 2359. This will set the datetime to be 2359 of the date before the # rounded date. def round_to_date(datetime, course_tz, to_2359: false) diff --git a/app/controllers/concerns/course/users_controller_management_concern.rb b/app/controllers/concerns/course/users_controller_management_concern.rb index 968b7dff50b..42ec15a39aa 100644 --- a/app/controllers/concerns/course/users_controller_management_concern.rb +++ b/app/controllers/concerns/course/users_controller_management_concern.rb @@ -31,7 +31,6 @@ def destroy def students respond_to do |format| - format.html format.json do @course_users = @course_users.students.includes(:groups, user: :emails).order_alphabetically end @@ -40,7 +39,6 @@ def students def staff respond_to do |format| - format.html format.json do @student_options = @course_users.students.order_alphabetically.pluck(:id, :name, :role) @course_users = @course_users.staff.includes(user: :emails).order_alphabetically diff --git a/app/controllers/course/achievement/achievements_controller.rb b/app/controllers/course/achievement/achievements_controller.rb index d10a942a890..881014fc0eb 100644 --- a/app/controllers/course/achievement/achievements_controller.rb +++ b/app/controllers/course/achievement/achievements_controller.rb @@ -9,7 +9,6 @@ def index def show @achievement_users = @achievement.course_users.without_phantom_users.students.includes([:user, :course]) respond_to do |format| - format.html { render 'index' } format.json { render 'show' } end end diff --git a/app/controllers/course/admin/admin_controller.rb b/app/controllers/course/admin/admin_controller.rb index 1c47317138d..be1606a5099 100644 --- a/app/controllers/course/admin/admin_controller.rb +++ b/app/controllers/course/admin/admin_controller.rb @@ -2,7 +2,6 @@ class Course::Admin::AdminController < Course::Admin::Controller def index respond_to do |format| - format.html { render 'course/admin/index' } format.json end end diff --git a/app/controllers/course/admin/announcement_settings_controller.rb b/app/controllers/course/admin/announcement_settings_controller.rb index 1840514ad1e..1b23c66d479 100644 --- a/app/controllers/course/admin/announcement_settings_controller.rb +++ b/app/controllers/course/admin/announcement_settings_controller.rb @@ -2,7 +2,6 @@ class Course::Admin::AnnouncementSettingsController < Course::Admin::Controller def edit respond_to do |format| - format.html { render 'course/admin/index' } format.json end end diff --git a/app/controllers/course/admin/assessment_settings_controller.rb b/app/controllers/course/admin/assessment_settings_controller.rb index 4faa659de46..2b9a8947188 100644 --- a/app/controllers/course/admin/assessment_settings_controller.rb +++ b/app/controllers/course/admin/assessment_settings_controller.rb @@ -2,7 +2,6 @@ class Course::Admin::AssessmentSettingsController < Course::Admin::Controller def edit respond_to do |format| - format.html { render 'course/admin/index' } format.json end end diff --git a/app/controllers/course/admin/codaveri_settings_controller.rb b/app/controllers/course/admin/codaveri_settings_controller.rb index 3c93747eeee..2c59c545a59 100644 --- a/app/controllers/course/admin/codaveri_settings_controller.rb +++ b/app/controllers/course/admin/codaveri_settings_controller.rb @@ -2,7 +2,6 @@ class Course::Admin::CodaveriSettingsController < Course::Admin::Controller def edit respond_to do |format| - format.html { render 'course/admin/index' } format.json end end diff --git a/app/controllers/course/admin/component_settings_controller.rb b/app/controllers/course/admin/component_settings_controller.rb index d62c79f2c02..cbe3cfe1e31 100644 --- a/app/controllers/course/admin/component_settings_controller.rb +++ b/app/controllers/course/admin/component_settings_controller.rb @@ -4,7 +4,6 @@ class Course::Admin::ComponentSettingsController < Course::Admin::Controller def edit respond_to do |format| - format.html { render 'course/admin/index' } format.json end end diff --git a/app/controllers/course/admin/discussion/topic_settings_controller.rb b/app/controllers/course/admin/discussion/topic_settings_controller.rb index d0cecb1511d..90849991f20 100644 --- a/app/controllers/course/admin/discussion/topic_settings_controller.rb +++ b/app/controllers/course/admin/discussion/topic_settings_controller.rb @@ -2,7 +2,6 @@ class Course::Admin::Discussion::TopicSettingsController < Course::Admin::Controller def edit respond_to do |format| - format.html { render 'course/admin/index' } format.json end end diff --git a/app/controllers/course/admin/forum_settings_controller.rb b/app/controllers/course/admin/forum_settings_controller.rb index 222deea6d13..fe27772091a 100644 --- a/app/controllers/course/admin/forum_settings_controller.rb +++ b/app/controllers/course/admin/forum_settings_controller.rb @@ -2,7 +2,6 @@ class Course::Admin::ForumSettingsController < Course::Admin::Controller def edit respond_to do |format| - format.html { render 'course/admin/index' } format.json end end diff --git a/app/controllers/course/admin/leaderboard_settings_controller.rb b/app/controllers/course/admin/leaderboard_settings_controller.rb index f075af169d1..f8e8d788885 100644 --- a/app/controllers/course/admin/leaderboard_settings_controller.rb +++ b/app/controllers/course/admin/leaderboard_settings_controller.rb @@ -2,7 +2,6 @@ class Course::Admin::LeaderboardSettingsController < Course::Admin::Controller def edit respond_to do |format| - format.html { render 'course/admin/index' } format.json end end diff --git a/app/controllers/course/admin/lesson_plan_settings_controller.rb b/app/controllers/course/admin/lesson_plan_settings_controller.rb index 8df7618cfde..bd26e1ac3f0 100644 --- a/app/controllers/course/admin/lesson_plan_settings_controller.rb +++ b/app/controllers/course/admin/lesson_plan_settings_controller.rb @@ -4,7 +4,6 @@ class Course::Admin::LessonPlanSettingsController < Course::Admin::Controller def edit respond_to do |format| - format.html { render 'course/admin/index' } format.json { @page_data = page_data } end end diff --git a/app/controllers/course/admin/material_settings_controller.rb b/app/controllers/course/admin/material_settings_controller.rb index 0072946f2d5..15f3c3dfe7b 100644 --- a/app/controllers/course/admin/material_settings_controller.rb +++ b/app/controllers/course/admin/material_settings_controller.rb @@ -2,7 +2,6 @@ class Course::Admin::MaterialSettingsController < Course::Admin::Controller def edit respond_to do |format| - format.html { render 'course/admin/index' } format.json end end diff --git a/app/controllers/course/admin/notification_settings_controller.rb b/app/controllers/course/admin/notification_settings_controller.rb index f6471212917..1c90258f5fe 100644 --- a/app/controllers/course/admin/notification_settings_controller.rb +++ b/app/controllers/course/admin/notification_settings_controller.rb @@ -2,7 +2,6 @@ class Course::Admin::NotificationSettingsController < Course::Admin::Controller def edit respond_to do |format| - format.html { render 'course/admin/index' } format.json { @page_data = page_data } end end diff --git a/app/controllers/course/admin/sidebar_settings_controller.rb b/app/controllers/course/admin/sidebar_settings_controller.rb index 0477ed500ce..b597527a07e 100644 --- a/app/controllers/course/admin/sidebar_settings_controller.rb +++ b/app/controllers/course/admin/sidebar_settings_controller.rb @@ -4,7 +4,6 @@ class Course::Admin::SidebarSettingsController < Course::Admin::Controller def edit respond_to do |format| - format.html { render 'course/admin/index' } format.json end end diff --git a/app/controllers/course/admin/video_settings_controller.rb b/app/controllers/course/admin/video_settings_controller.rb index 000eb91f411..5f520025421 100644 --- a/app/controllers/course/admin/video_settings_controller.rb +++ b/app/controllers/course/admin/video_settings_controller.rb @@ -2,7 +2,6 @@ class Course::Admin::VideoSettingsController < Course::Admin::Controller def edit respond_to do |format| - format.html { render 'course/admin/index' } format.json end end diff --git a/app/controllers/course/announcements_controller.rb b/app/controllers/course/announcements_controller.rb index c833809490d..d38a5894da4 100644 --- a/app/controllers/course/announcements_controller.rb +++ b/app/controllers/course/announcements_controller.rb @@ -8,7 +8,6 @@ class Course::AnnouncementsController < Course::ComponentController def index respond_to do |format| - format.html format.json do @course_users_hash = preload_course_users_hash(current_course) @announcements = @announcements.includes(:creator).with_read_marks_for(current_user) diff --git a/app/controllers/course/assessment/assessments_controller.rb b/app/controllers/course/assessment/assessments_controller.rb index 5e6b929e623..85cbebf6059 100644 --- a/app/controllers/course/assessment/assessments_controller.rb +++ b/app/controllers/course/assessment/assessments_controller.rb @@ -13,7 +13,6 @@ class Course::Assessment::AssessmentsController < Course::Assessment::Controller def index respond_to do |format| - format.html format.json do @assessments = @assessments.ordered_by_date_and_title.with_submissions_by(current_user) @@ -33,7 +32,6 @@ def index def show respond_to do |format| - format.html format.json do @assessment_time = @assessment.time_for(current_course_user) return render 'authenticate' unless can_access_assessment? @@ -152,10 +150,7 @@ def statistics end def monitoring - if monitor.nil? - render file: 'public/404', layout: false, status: :not_found - return - end + raise ComponentNotFoundError if monitor.nil? authorize! :read, @monitor diff --git a/app/controllers/course/assessment/question/programming_controller.rb b/app/controllers/course/assessment/question/programming_controller.rb index f75229f430b..66d8c617262 100644 --- a/app/controllers/course/assessment/question/programming_controller.rb +++ b/app/controllers/course/assessment/question/programming_controller.rb @@ -10,7 +10,6 @@ class Course::Assessment::Question::ProgrammingController < Course::Assessment:: def new respond_to do |format| - format.html format.json { format_test_cases } end end @@ -29,8 +28,6 @@ def create def edit respond_to do |format| - format.html - format.json do @meta = programming_package_service.extract_meta if @programming_question.edit_online? format_test_cases diff --git a/app/controllers/course/assessment/question/scribing_controller.rb b/app/controllers/course/assessment/question/scribing_controller.rb index cea55d886c1..2daa6d7070d 100644 --- a/app/controllers/course/assessment/question/scribing_controller.rb +++ b/app/controllers/course/assessment/question/scribing_controller.rb @@ -9,7 +9,6 @@ class Course::Assessment::Question::ScribingController < Course::Assessment::Que def new respond_to do |format| - format.html { render 'new' } format.json { render_scribing_question_json } end end @@ -42,7 +41,6 @@ def create # rubocop:disable Metrics/MethodLength def edit respond_to do |format| - format.html { render 'edit' } format.json { render_scribing_question_json } end end diff --git a/app/controllers/course/assessment/skills_controller.rb b/app/controllers/course/assessment/skills_controller.rb index ec777344389..482ee4ee13d 100644 --- a/app/controllers/course/assessment/skills_controller.rb +++ b/app/controllers/course/assessment/skills_controller.rb @@ -7,7 +7,6 @@ class Course::Assessment::SkillsController < Course::ComponentController def index @skills = @skills.includes(:skill_branch).group_by(&:skill_branch) respond_to do |format| - format.html { render 'index' } format.json { render 'index' } end end diff --git a/app/controllers/course/assessment/submission/submissions_controller.rb b/app/controllers/course/assessment/submission/submissions_controller.rb index 735036fc0a5..1517d83cd2a 100644 --- a/app/controllers/course/assessment/submission/submissions_controller.rb +++ b/app/controllers/course/assessment/submission/submissions_controller.rb @@ -32,7 +32,6 @@ def index authorize!(:view_all_submissions, @assessment) respond_to do |format| - format.html {} # rubocop:disable Lint/EmptyBlock format.json do @assessment = @assessment.calculated(:maximum_grade) @submissions = @submissions.calculated(:log_count, :graded_at, :grade, :grader_ids) @@ -68,7 +67,6 @@ def edit @monitoring_session_id = monitoring_service&.session&.id if should_monitor? respond_to do |format| - format.html format.json do return render json: { isSubmissionBlocked: true } if @submission.submission_view_blocked?(current_course_user) @@ -159,7 +157,6 @@ def download_all else job = download_job respond_to do |format| - format.html { redirect_to(job_path(job)) } format.json { render partial: 'jobs/submitted', locals: { job: job } } end end @@ -176,7 +173,6 @@ def download_statistics job = Course::Assessment::Submission::StatisticsDownloadJob. perform_later(current_course, current_user, submission_ids).job respond_to do |format| - format.html { redirect_to(job_path(job)) } format.json { render partial: 'jobs/submitted', locals: { job: job } } end end @@ -240,7 +236,6 @@ def delete_all job = Course::Assessment::Submission::DeletingJob. perform_later(current_user, submission_ids, @assessment).job respond_to do |format| - format.html { redirect_to(job_path(job)) } format.json { render partial: 'jobs/submitted', locals: { job: job } } end end @@ -286,7 +281,6 @@ def check_password log_service.log_submission_access(request) respond_to do |format| - format.html { render 'edit' } format.json { render json: { newSessionUrl: new_session_path } } end end diff --git a/app/controllers/course/assessment/submissions_controller.rb b/app/controllers/course/assessment/submissions_controller.rb index 3e5356120ad..9229eea439d 100644 --- a/app/controllers/course/assessment/submissions_controller.rb +++ b/app/controllers/course/assessment/submissions_controller.rb @@ -6,7 +6,6 @@ class Course::Assessment::SubmissionsController < Course::ComponentController def index respond_to do |format| - format.html format.json do @submissions = @submissions.from_category(category).confirmed @submissions = @submissions.filter_by_params(filter_params) unless filter_params.blank? @@ -19,7 +18,6 @@ def index def pending respond_to do |format| - format.html format.json do @submissions = pending_submissions.from_course(current_course) @submission_count = @submissions.count diff --git a/app/controllers/course/component_controller.rb b/app/controllers/course/component_controller.rb index 97186790af5..df78741d1da 100644 --- a/app/controllers/course/component_controller.rb +++ b/app/controllers/course/component_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true class Course::ComponentController < Course::Controller - layout 'course' - before_action :load_current_component_host before_action :check_component before_action :load_settings diff --git a/app/controllers/course/courses_controller.rb b/app/controllers/course/courses_controller.rb index f6bea291e2e..f82b0fc11ad 100644 --- a/app/controllers/course/courses_controller.rb +++ b/app/controllers/course/courses_controller.rb @@ -9,7 +9,6 @@ def index def show respond_to do |format| - format.html { render layout: 'course' } format.json do @currently_active_announcements = current_course.announcements. currently_active.includes(:creator) @@ -39,7 +38,7 @@ def sidebar protected def publicly_accessible? - params[:action] == 'index' + Set[:index, :sidebar].include?(action_name.to_sym) end private diff --git a/app/controllers/course/experience_points/disbursement_controller.rb b/app/controllers/course/experience_points/disbursement_controller.rb index 6e496961f0c..5abfce7cf0b 100644 --- a/app/controllers/course/experience_points/disbursement_controller.rb +++ b/app/controllers/course/experience_points/disbursement_controller.rb @@ -5,7 +5,6 @@ class Course::ExperiencePoints::DisbursementController < Course::ComponentContro def new respond_to do |format| - format.html { render 'new' } format.json { render 'new' } end end diff --git a/app/controllers/course/experience_points_records_controller.rb b/app/controllers/course/experience_points_records_controller.rb index 396d75e8c5d..d0c168cde37 100644 --- a/app/controllers/course/experience_points_records_controller.rb +++ b/app/controllers/course/experience_points_records_controller.rb @@ -6,7 +6,6 @@ class Course::ExperiencePointsRecordsController < Course::ComponentController def index respond_to do |format| - format.html format.json do updater_ids = @experience_points_records.active.pluck(:updater_id) @course_user_preload_service = diff --git a/app/controllers/course/forum/forums_controller.rb b/app/controllers/course/forum/forums_controller.rb index 3755117052b..bfcb4a888b7 100644 --- a/app/controllers/course/forum/forums_controller.rb +++ b/app/controllers/course/forum/forums_controller.rb @@ -5,7 +5,6 @@ class Course::Forum::ForumsController < Course::Forum::Controller def index respond_to do |format| - format.html format.json do @forums = @forums.with_forum_statistics(current_user) @unresolved_forums_ids = Course::Forum::Topic.filter_unresolved_forum(@forums.map(&:id)) @@ -15,7 +14,6 @@ def index def show respond_to do |format| - format.html { render 'index' } format.json do @topics = @forum.topics.accessible_by(current_ability).order_by_latest_post.with_topic_statistics. with_read_marks_for(current_user).includes(:creator).with_earliest_and_latest_post diff --git a/app/controllers/course/forum/topics_controller.rb b/app/controllers/course/forum/topics_controller.rb index 904298079ab..006f7e0ed46 100644 --- a/app/controllers/course/forum/topics_controller.rb +++ b/app/controllers/course/forum/topics_controller.rb @@ -12,7 +12,6 @@ class Course::Forum::TopicsController < Course::Forum::ComponentController def show respond_to do |format| - format.html { render 'course/forum/forums/index' } format.json do @topic.viewed_by(current_user) @topic.mark_as_read!(for: current_user) diff --git a/app/controllers/course/group/group_categories_controller.rb b/app/controllers/course/group/group_categories_controller.rb index e3eba1118a7..d6f99d6a1c7 100644 --- a/app/controllers/course/group/group_categories_controller.rb +++ b/app/controllers/course/group/group_categories_controller.rb @@ -6,7 +6,6 @@ class Course::Group::GroupCategoriesController < Course::ComponentController def index respond_to do |format| - format.html format.json end end diff --git a/app/controllers/course/learning_map_controller.rb b/app/controllers/course/learning_map_controller.rb index a0752ebc407..3e9dbad11b6 100644 --- a/app/controllers/course/learning_map_controller.rb +++ b/app/controllers/course/learning_map_controller.rb @@ -8,7 +8,6 @@ class Course::LearningMapController < Course::ComponentController def index respond_to do |format| - format.html format.json do prepare_response_data end @@ -74,7 +73,6 @@ def component def error_response(errors) respond_to do |format| - format.html format.json do render json: { errors: errors }, status: :bad_request end diff --git a/app/controllers/course/lesson_plan/items_controller.rb b/app/controllers/course/lesson_plan/items_controller.rb index 6076a14ac83..625e236fcc4 100644 --- a/app/controllers/course/lesson_plan/items_controller.rb +++ b/app/controllers/course/lesson_plan/items_controller.rb @@ -12,7 +12,6 @@ class Course::LessonPlan::ItemsController < Course::LessonPlan::Controller def index respond_to do |format| - format.html format.json { render_json_response } end end diff --git a/app/controllers/course/material/folders_controller.rb b/app/controllers/course/material/folders_controller.rb index 68d0547dcbc..a61c1d1bd75 100644 --- a/app/controllers/course/material/folders_controller.rb +++ b/app/controllers/course/material/folders_controller.rb @@ -4,7 +4,6 @@ class Course::Material::FoldersController < Course::Material::Controller def show respond_to do |format| - format.html format.json do @subfolders = @folder.children.with_content_statistics.accessible_by(current_ability). order(:name).includes(:owner).without_empty_linked_folder diff --git a/app/controllers/course/material/materials_controller.rb b/app/controllers/course/material/materials_controller.rb index b653c15041c..76405a55613 100644 --- a/app/controllers/course/material/materials_controller.rb +++ b/app/controllers/course/material/materials_controller.rb @@ -5,7 +5,7 @@ class Course::Material::MaterialsController < Course::Material::Controller def show authorize!(:read_owner, @material.folder) create_submission if @folder.owner_type == 'Course::Assessment' - redirect_to @material.attachment.url(filename: @material.name) + render json: { redirectUrl: @material.attachment.url(filename: @material.name) } end def update diff --git a/app/controllers/course/personal_times_controller.rb b/app/controllers/course/personal_times_controller.rb index 2ed1cc1d7f9..31408c6a0e6 100644 --- a/app/controllers/course/personal_times_controller.rb +++ b/app/controllers/course/personal_times_controller.rb @@ -7,7 +7,6 @@ class Course::PersonalTimesController < Course::ComponentController def index respond_to do |format| - format.html format.json do return unless params[:user_id].present? diff --git a/app/controllers/course/survey/responses_controller.rb b/app/controllers/course/survey/responses_controller.rb index dcc785b5a1c..5b0520e8f03 100644 --- a/app/controllers/course/survey/responses_controller.rb +++ b/app/controllers/course/survey/responses_controller.rb @@ -5,7 +5,6 @@ class Course::Survey::ResponsesController < Course::Survey::Controller def index authorize!(:manage, @survey) respond_to do |format| - format.html { render 'course/survey/surveys/index' } format.json do @course_students = current_course.course_users.students.order_alphabetically end @@ -27,7 +26,6 @@ def create def show authorize!(:read_answers, @response) respond_to do |format| - format.html { render 'course/survey/surveys/index' } format.json { render_response_json } end end @@ -36,7 +34,6 @@ def edit raise CanCan::AccessDenied if cannot?(:submit, @response) && cannot?(:modify, @response) respond_to do |format| - format.html { render 'course/survey/surveys/index' } format.json do @response.build_missing_answers if @response.save diff --git a/app/controllers/course/survey/surveys_controller.rb b/app/controllers/course/survey/surveys_controller.rb index 0291cf913d4..eb2c6b8d70e 100644 --- a/app/controllers/course/survey/surveys_controller.rb +++ b/app/controllers/course/survey/surveys_controller.rb @@ -7,7 +7,6 @@ class Course::Survey::SurveysController < Course::Survey::Controller def index respond_to do |format| - format.html format.json do @surveys = @surveys.includes(responses: { experience_points_record: :course_user }) end @@ -24,7 +23,6 @@ def create def show respond_to do |format| - format.html { render 'index' } format.json { render_survey_with_questions_json } end end @@ -47,7 +45,6 @@ def destroy def results respond_to do |format| - format.html { render 'index' } format.json { preload_questions_results } end end @@ -64,7 +61,6 @@ def download job = Course::Survey::SurveyDownloadJob. perform_later(@survey).job respond_to do |format| - format.html { redirect_to(job_path(job)) } format.json { render partial: 'jobs/submitted', locals: { job: job } } end end diff --git a/app/controllers/course/user_email_subscriptions_controller.rb b/app/controllers/course/user_email_subscriptions_controller.rb index 2a2fadb66c7..b24f09b64d4 100644 --- a/app/controllers/course/user_email_subscriptions_controller.rb +++ b/app/controllers/course/user_email_subscriptions_controller.rb @@ -6,7 +6,6 @@ def edit authorize!(:manage, Course::UserEmailUnsubscription.new(course_user: @course_user)) load_subscription_settings respond_to do |format| - format.html { render 'edit' } format.json { render partial: 'course/user_email_subscriptions/subscription_setting' } end end diff --git a/app/controllers/course/user_invitations_controller.rb b/app/controllers/course/user_invitations_controller.rb index 9228b3c6a54..feef66e188c 100644 --- a/app/controllers/course/user_invitations_controller.rb +++ b/app/controllers/course/user_invitations_controller.rb @@ -6,7 +6,6 @@ class Course::UserInvitationsController < Course::ComponentController def index respond_to do |format| - format.html format.json do @invitations = current_course.invitations.order(name: :asc) @without_invitations = params[:without_invitations] diff --git a/app/controllers/course/user_notifications_controller.rb b/app/controllers/course/user_notifications_controller.rb index 3cd802f206e..944b6223633 100644 --- a/app/controllers/course/user_notifications_controller.rb +++ b/app/controllers/course/user_notifications_controller.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true class Course::UserNotificationsController < Course::Controller + skip_authorize_resource :course, only: [:fetch] load_and_authorize_resource :user_notification, class: UserNotification.name, only: :mark_as_read def fetch diff --git a/app/controllers/course/user_registrations_controller.rb b/app/controllers/course/user_registrations_controller.rb index 82f51e4459d..3bd6b52a8e5 100644 --- a/app/controllers/course/user_registrations_controller.rb +++ b/app/controllers/course/user_registrations_controller.rb @@ -25,7 +25,8 @@ def ensure_unregistered_user role = t("course.users.role.#{current_course_user.role}") message = t('course.users.new.already_registered', role: role) - redirect_to course_path(current_course), info: message + + render json: { errors: message }, status: :conflict end def load_registration @@ -39,18 +40,6 @@ def registration_service @registration_service ||= Course::UserRegistrationService.new end - def create_success - success = - if @registration.course_user.present? - role = t("course.users.role.#{@registration.course_user.role}") - t('course.user_registrations.create.registered', role: role) - else - t('course.user_registrations.create.requested') - end - - redirect_to course_path(current_course), success: success - end - # @return [Course::UsersComponent] # @return [nil] If component is disabled. def component diff --git a/app/controllers/course/users_controller.rb b/app/controllers/course/users_controller.rb index 32b8bd22740..197638cc455 100644 --- a/app/controllers/course/users_controller.rb +++ b/app/controllers/course/users_controller.rb @@ -12,7 +12,6 @@ def show @skills_service = Course::SkillsMasteryPreloadService.new(current_course, @course_user) respond_to do |format| - format.html { render 'index' } format.json { render 'show' } end end diff --git a/app/controllers/course/video/submission/submissions_controller.rb b/app/controllers/course/video/submission/submissions_controller.rb index 1c1798c48dc..448a49a151c 100644 --- a/app/controllers/course/video/submission/submissions_controller.rb +++ b/app/controllers/course/video/submission/submissions_controller.rb @@ -6,7 +6,6 @@ class Course::Video::Submission::SubmissionsController < Course::Video::Submissi def index respond_to do |format| - format.html format.json do @submissions = @submissions.includes([{ experience_points_record: :course_user }, :statistic]) @my_students = current_course_user.try(:my_students) || [] @@ -17,7 +16,6 @@ def index def show respond_to do |format| - format.html { render 'index' } format.json do @sessions = @submission.sessions.with_events_present end @@ -26,18 +24,9 @@ def show def create if @submission.save - respond_to do |format| - format.json do - render json: { submissionId: @submission.id, - submissionUrl: edit_course_video_submission_path(current_course, @submission.video, - @submission) }, - status: :ok - end - end + render json: { submissionId: @submission.id } elsif @submission.existing_submission.present? - respond_to do |format| - format.json { render json: { submissionId: @submission.existing_submission.id }, status: :ok } - end + render json: { submissionId: @submission.existing_submission.id } else render json: { errors: @submission.errors.full_messages.to_sentence }, status: :bad_request end @@ -49,7 +38,6 @@ def edit authorize!(:edit, @submission) respond_to do |format| - format.html { render 'index' } format.json do @topics = @video.topics.includes(posts: :children).order(:timestamp) @topics = @topics.reject { |topic| topic.posts.empty? } diff --git a/app/controllers/course/video/videos_controller.rb b/app/controllers/course/video/videos_controller.rb index 83a77598111..998df29df1a 100644 --- a/app/controllers/course/video/videos_controller.rb +++ b/app/controllers/course/video/videos_controller.rb @@ -6,7 +6,6 @@ class Course::Video::VideosController < Course::Video::Controller def index respond_to do |format| - format.html format.json do @can_analyze = can_for_videos_in_current_course? :analyze @can_manage = can_for_videos_in_current_course? :manage @@ -26,7 +25,6 @@ def index def show respond_to do |format| - format.html { render 'index' } format.json { render 'show' } end end diff --git a/app/controllers/csrf_token_controller.rb b/app/controllers/csrf_token_controller.rb new file mode 100644 index 00000000000..58026dd4ba6 --- /dev/null +++ b/app/controllers/csrf_token_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class CsrfTokenController < ApplicationController + def csrf_token + render json: { csrfToken: form_authenticity_token } + end + + protected + + def publicly_accessible? + true + end +end diff --git a/app/controllers/instance_user_role_requests_controller.rb b/app/controllers/instance_user_role_requests_controller.rb index 12ed48e9ec3..474e11ee1a7 100644 --- a/app/controllers/instance_user_role_requests_controller.rb +++ b/app/controllers/instance_user_role_requests_controller.rb @@ -6,7 +6,6 @@ def index @user_role_requests = @user_role_requests.includes(:confirmer, :user) respond_to do |format| - format.html { render 'system/admin/instance/admin/index' } format.json end end diff --git a/app/controllers/system/admin/announcements_controller.rb b/app/controllers/system/admin/announcements_controller.rb index 078a37a92a0..27f8a6d044a 100644 --- a/app/controllers/system/admin/announcements_controller.rb +++ b/app/controllers/system/admin/announcements_controller.rb @@ -4,7 +4,6 @@ class System::Admin::AnnouncementsController < System::Admin::Controller def index respond_to do |format| - format.html { render 'system/admin/admin/index' } format.json do @announcements = @announcements.includes(:creator) end diff --git a/app/controllers/system/admin/courses_controller.rb b/app/controllers/system/admin/courses_controller.rb index 8686d87a424..97a47464915 100644 --- a/app/controllers/system/admin/courses_controller.rb +++ b/app/controllers/system/admin/courses_controller.rb @@ -4,7 +4,6 @@ class System::Admin::CoursesController < System::Admin::Controller def index respond_to do |format| - format.html { render 'system/admin/admin/index' } format.json do preload_courses end diff --git a/app/controllers/system/admin/instance/announcements_controller.rb b/app/controllers/system/admin/instance/announcements_controller.rb index f1451240c6a..04ad3a8a891 100644 --- a/app/controllers/system/admin/instance/announcements_controller.rb +++ b/app/controllers/system/admin/instance/announcements_controller.rb @@ -5,7 +5,6 @@ class System::Admin::Instance::AnnouncementsController < System::Admin::Instance def index respond_to do |format| - format.html { render 'system/admin/instance/admin/index' } format.json do @announcements = @announcements.includes(:creator) end diff --git a/app/controllers/system/admin/instance/components_controller.rb b/app/controllers/system/admin/instance/components_controller.rb index e74373cc11d..172ad63e8ca 100644 --- a/app/controllers/system/admin/instance/components_controller.rb +++ b/app/controllers/system/admin/instance/components_controller.rb @@ -4,7 +4,6 @@ class System::Admin::Instance::ComponentsController < System::Admin::Instance::C def index respond_to do |format| - format.html { render 'system/admin/instance/admin/index' } format.json end end diff --git a/app/controllers/system/admin/instance/courses_controller.rb b/app/controllers/system/admin/instance/courses_controller.rb index e395a57e646..7483866a00c 100644 --- a/app/controllers/system/admin/instance/courses_controller.rb +++ b/app/controllers/system/admin/instance/courses_controller.rb @@ -4,7 +4,6 @@ class System::Admin::Instance::CoursesController < System::Admin::Instance::Cont def index respond_to do |format| - format.html { render 'system/admin/instance/admin/index' } format.json do preload_courses end diff --git a/app/controllers/system/admin/instance/user_invitations_controller.rb b/app/controllers/system/admin/instance/user_invitations_controller.rb index 61c67acf60b..6a006696f96 100644 --- a/app/controllers/system/admin/instance/user_invitations_controller.rb +++ b/app/controllers/system/admin/instance/user_invitations_controller.rb @@ -7,7 +7,6 @@ class System::Admin::Instance::UserInvitationsController < System::Admin::Instan def index @invitations = @instance.invitations.order(name: :asc) respond_to do |format| - format.html { render 'system/admin/instance/admin/index' } format.json end end diff --git a/app/controllers/system/admin/instance/users_controller.rb b/app/controllers/system/admin/instance/users_controller.rb index 82700ea812c..2f1407b606d 100644 --- a/app/controllers/system/admin/instance/users_controller.rb +++ b/app/controllers/system/admin/instance/users_controller.rb @@ -5,7 +5,6 @@ class System::Admin::Instance::UsersController < System::Admin::Instance::Contro def index respond_to do |format| - format.html { render 'system/admin/instance/admin/index' } format.json do load_instance_users load_counts diff --git a/app/controllers/system/admin/instances_controller.rb b/app/controllers/system/admin/instances_controller.rb index 6541a0d355e..764a4ed347d 100644 --- a/app/controllers/system/admin/instances_controller.rb +++ b/app/controllers/system/admin/instances_controller.rb @@ -4,7 +4,6 @@ class System::Admin::InstancesController < System::Admin::Controller def index respond_to do |format| - format.html { render 'system/admin/admin/index' } format.json do preload_instances end diff --git a/app/controllers/system/admin/users_controller.rb b/app/controllers/system/admin/users_controller.rb index 13eec4aaa22..c4b9d07ee6b 100644 --- a/app/controllers/system/admin/users_controller.rb +++ b/app/controllers/system/admin/users_controller.rb @@ -4,7 +4,6 @@ class System::Admin::UsersController < System::Admin::Controller def index respond_to do |format| - format.html { render 'system/admin/admin/index' } format.json do load_users load_counts diff --git a/app/controllers/test/controller.rb b/app/controllers/test/controller.rb new file mode 100644 index 00000000000..1c50dff49c1 --- /dev/null +++ b/app/controllers/test/controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +class Test::Controller < ActionController::Base + before_action :restrict_to_test + + private + + def restrict_to_test + head :not_found unless Rails.env.test? + end +end diff --git a/app/controllers/test/factories_controller.rb b/app/controllers/test/factories_controller.rb new file mode 100644 index 00000000000..11eee7e2ef4 --- /dev/null +++ b/app/controllers/test/factories_controller.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +class Test::FactoriesController < Test::Controller + before_action :set_user_stamper, only: [:create] + + def create + models = {} + + ActsAsTenant.with_tenant(Instance.default) do + create_params.each do |factory_name, attributes| + traits = traits_from(attributes) + model = FactoryBot.create(factory_name, *traits, attributes) + models[factory_name] = model.as_json + rescue SystemStackError + models[factory_name] = { id: model.id } + end + end + + result = (models.size <= 1) ? models.values.first : models + render json: result, status: :created + end + + private + + def create_params + params.permit(factory: {}).to_h['factory'] + end + + def set_user_stamper + User.stamper = User.human_users.first + end + + def traits_from(attributes) + attributes.extract!('traits')[:traits]&.map(&:to_sym) + end +end diff --git a/app/controllers/test/mailer_controller.rb b/app/controllers/test/mailer_controller.rb new file mode 100644 index 00000000000..352eb45b494 --- /dev/null +++ b/app/controllers/test/mailer_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class Test::MailerController < Test::Controller + def last_sent + render json: ActionMailer::Base.deliveries.last + end + + def clear + ActionMailer::Base.deliveries.clear + + head :ok + end +end diff --git a/app/controllers/user/confirmations_controller.rb b/app/controllers/user/confirmations_controller.rb new file mode 100644 index 00000000000..83d62d851b7 --- /dev/null +++ b/app/controllers/user/confirmations_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +class User::ConfirmationsController < Devise::ConfirmationsController + respond_to :json + + def show + super do |email| + if email.persisted? && email.confirmed? + render json: { email: email.email } + else + render json: { error: 'Invalid token' }, status: :bad_request + end + + return + end + end +end diff --git a/app/controllers/user/passwords_controller.rb b/app/controllers/user/passwords_controller.rb new file mode 100644 index 00000000000..9f0eb5e2347 --- /dev/null +++ b/app/controllers/user/passwords_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +class User::PasswordsController < Devise::PasswordsController + respond_to :json + + def edit + super + + if (user = User.find_by(reset_password_token: hash_reset_password_token(params[:reset_password_token]))) + render json: { email: user.email } + else + render json: { error: 'Invalid token' }, status: :bad_request + end + end + + private + + def hash_reset_password_token(token) + Devise.token_generator.digest(self, :reset_password_token, token) + end +end diff --git a/app/controllers/user/registrations_controller.rb b/app/controllers/user/registrations_controller.rb index c9e850daaa4..f2bf00e6840 100644 --- a/app/controllers/user/registrations_controller.rb +++ b/app/controllers/user/registrations_controller.rb @@ -8,9 +8,18 @@ class User::RegistrationsController < Devise::RegistrationsController def new if @invitation&.confirmed? message = @invitation.confirmer ? t('.used_with_email', email: @invitation.confirmer.email) : t('.used') - redirect_to root_path, danger: message + render json: { message: message }, status: :conflict and return + elsif @invitation + course = @invitation.course + + render json: { + name: @invitation.name, + email: @invitation.email, + courseTitle: course.title, + courseId: course.id + } else - super + head :no_content end end @@ -18,13 +27,16 @@ def new def create unless verify_recaptcha build_resource(sign_up_params) - flash.now[:alert] = t('user.registrations.create.verify_recaptcha_alert') - flash.delete :recaptcha_error - return render :new + render json: { errors: { recaptcha: t('user.registrations.create.verify_recaptcha_alert') } }, + status: :unprocessable_entity + return end + User.transaction do super + @invitation.confirm!(confirmer: resource) if @invitation && !@invitation.confirmed? && resource.persisted? + @user = resource end end diff --git a/app/controllers/user/sessions_controller.rb b/app/controllers/user/sessions_controller.rb index f4763dac4fe..c776cdcf50b 100644 --- a/app/controllers/user/sessions_controller.rb +++ b/app/controllers/user/sessions_controller.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true class User::SessionsController < Devise::SessionsController + respond_to :json + # before_filter :configure_sign_in_params, only: [:create] # GET /resource/sign_in diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 1b6598e4708..c51acb3b839 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -4,21 +4,12 @@ class UsersController < ApplicationController def show if @user.built_in? - respond_to do |format| - format.html { render file: 'public/404', layout: false, status: 404 } - format.json { render file: 'public/404.json', layout: false, status: 404 } - end + head :not_found else - respond_to do |format| - format.html - format.json do - course_users = - @user.course_users.with_course_statistics.from_instance(current_tenant).includes(:course) - @current_courses = course_users.merge(Course.current).order(created_at: :desc) - @completed_courses = course_users.merge(Course.completed).order(created_at: :desc) - @instances = other_instances - end - end + course_users = @user.course_users.with_course_statistics.from_instance(current_tenant).includes(:course) + @current_courses = course_users.merge(Course.current).order(created_at: :desc) + @completed_courses = course_users.merge(Course.completed).order(created_at: :desc) + @instances = other_instances end end diff --git a/app/helpers/application_announcements_helper.rb b/app/helpers/application_announcements_helper.rb deleted file mode 100644 index 1b7103466aa..00000000000 --- a/app/helpers/application_announcements_helper.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true -module ApplicationAnnouncementsHelper - def global_announcements - render partial: 'announcements/global_announcements' - end -end diff --git a/app/helpers/application_cocoon_helper.rb b/app/helpers/application_cocoon_helper.rb deleted file mode 100644 index 2a070152800..00000000000 --- a/app/helpers/application_cocoon_helper.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true -module ApplicationCocoonHelper - # Shows a link that will allow to dynamically add a new associated object. This is a wrapper of - # #link_to_add_association - # - # @overload link_to_add_association(name, form, options, association, html_options = nil) - # Shows a link that will allow to dynamically add a new associated object. - # - # @param [String] name The text to show in the link. - # @param [ActionView::Helpers::FormBuilder] form The form builder. - # @param [Symbol] association The associated objects, this should be the name of the has_many - # relation. - # @param [Hash] html_options The html options to be passed to {#link_to}. - # @option html_options [String] :find_using The jQuery traversal method to allow node selection - # relative to the link. Example values: +this+, +closest+, +next+, +children+, etc. - # Default: absolute selection. - # @option html_options [String] :selector The jQuery selector to be used with +:find_using+ to - # determine where to insert the new node. Default: the parent node. - # @option html_options [String] :find_selector The alias of +:selector+, you should use this - # when +:find_using+ is not specified. Default: parent node. - # @option html_options [String] :insert_using ('before') The jQuery method that inserts the - # new data. Example values: +before+, +after+, +append+, +prepend+, etc. - # @return [String] - # @example Adding items after the link - # link_to_add_association(t('.add_option'), f, :options, find_selector: 'this', - # insert_using: 'after') - # @overload link_to_add_association(form, options, association, html_options = nil, &block) - # Shows a link that will allow to dynamically add a new associated object. The display of the - # link can be changed in the block. - # - # @param [String] name The text to show in the link. - # @param [ActionView::Helpers::FormBuilder] form The form builder. - # @param [Symbol] association The associated objects, this should be the name of the has_many - # relation. - # @param [Hash] html_options The html options to be passed to {#link_to}. - # @option html_options [String] :find_using The jQuery traversal method to allow node selection - # relative to the link. Example values: +this+, +closest+, +next+, +children+, etc. - # Default: absolute selection. - # @option html_options [String] :selector The jQuery selector to be used with +:find_using+ to - # determine where to insert the new node. Default: the parent node. - # @option html_options [String] :find_selector The alias of +:selector+, you should use this - # when +:find_using+ is not specified. - # @option html_options [String] :insert_using ('before') The jQuery method that inserts the - # new data. Example values: +before+, +after+, +append+, +prepend+, etc. - # @param [Proc] block The block to use for displaying the link. - # @return [String] - def link_to_add_association(name, form, association = nil, html_options = nil, &block) - name, form, association, html_options = nil, name, form, association if block_given? - html_options = html_options&.dup || {} - replace_cocoon_keys(html_options) - super(*[name, form, association, html_options].compact, &block) - end - - private - - def replace_cocoon_keys(html_options) - find_using = html_options.delete(:find_using) - selector = html_options.delete(:selector) || html_options.delete(:find_selector) - insert_using = html_options.delete(:insert_using) - - html_options['data-association-insertion-traversal'] = find_using.to_s if find_using - html_options['data-association-insertion-node'] = selector.to_s if selector - html_options['data-association-insertion-method'] = insert_using.to_s if insert_using - end -end diff --git a/app/helpers/application_formatters_helper.rb b/app/helpers/application_formatters_helper.rb index 6302921f310..e3a1d76590f 100644 --- a/app/helpers/application_formatters_helper.rb +++ b/app/helpers/application_formatters_helper.rb @@ -99,43 +99,4 @@ def format_duration(total_seconds) hours = total_seconds / (60 * 60) format('%02dH%02dM%02dS', hours: hours, minutes: minutes, seconds: seconds) end - - # A helper for generating CSS classes, based on the time-bounded status of the item. - # - # @param [ActiveRecord::Base] item An ActiveRecord object which has time-bounded fields. - # @return [Array] An array of CSS classes applicable for the provided item. - def time_period_class(item) - if !item.started? - ['not-started'] - elsif item.ended? - ['ended'] - else # Started, but not yet ended. - ['currently-active'] - end - end - - # A helper for retrieving the title for a time-bounded item's status. - # - # @param [ActiveRecord::Base] item An ActiveRecord object which has time-bounded fields. - # @return [String] A translated string representing the status of the item. - # @return [nil] If the item is valid. - def time_period_message(item) - if !item.started? - t('common.not_started') - elsif item.ended? - t('common.ended') - end - end - - # A helper for generating CSS classes, based on the unread status of the item. - # - # @param [ActiveRecord::Base] item An ActiveRecord object which acts as readable. - # @return [Array] An array of CSS classes applicable for the provided item. - def unread_class(item) - if item.unread?(current_user) - ['unread'] - else - [] - end - end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index ccd79bdb51f..16730032aa0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,88 +1,23 @@ # frozen_string_literal: true module ApplicationHelper - include FontAwesome::Rails::IconHelper - - include ApplicationThemingHelper - include ApplicationAnnouncementsHelper include ApplicationJobsHelper - include ApplicationWidgetsHelper - include ApplicationCocoonHelper include ApplicationNotificationsHelper include ApplicationFormattersHelper include RouteOverridesHelper - include FormForWithResourceHelper - include RenderWithinLayoutHelper - - # Accesses the header tags specified for the current page - def header_tags(*args, &proc) - content_for(:header_tags, *args, &proc) - end - - # @!method within_head_tag(&proc) - # Adds the given block to the header tags which will be added to the rendered page. - alias_method :within_head_tag, :header_tags - - # Generates a page header. The title shown will be the +.header+ key in the page - # that calls this helper. - # - # @param [String] header The custom page header string. - # @yield A block in which other helper methods may be called, to place child elements - # on the far right of the header. - # @return [String] - def page_header(header = nil) - content_tag(:div, class: 'page-header') do - content_tag(:h1) do - content_tag(:span, header || t('.header')) + - content_tag(:div, class: 'pull-right') do - yield if block_given? - end - end - end - end - # Generates all page titles, from the reverse breadcrumb if it's available, - # otherwise checks the +content_for?+ Rails helper. Appends the default - # title to everything. - # @return [String] - def page_title - if content_for?(:page_title) - "#{content_for(:page_title)} - " - elsif !breadcrumb_names.empty? - "#{breadcrumb_names.reverse.join(' - ')} - " - else - '' - end + - t('layout.coursemology') + def user_time_zone + user_signed_in? ? current_user.time_zone : nil end - # Returns a meta tag that has the server side context. Now the context contains following info: - # :controller-name The name of the current controller. - # e.g. 'Course::LessonPlanController' will return 'course/lesson_plan' - # :i18n-locale The locale on the server side. - # - # @return [String] The html meta tag. - def server_context_meta_tag - data = { - name: 'server-context', - 'data-controller-name': controller.class.name.sub(/Controller$/, '').underscore, - 'data-i18n-locale': I18n.locale, - 'data-time-zone': ActiveSupport::TimeZone::MAPPING[user_time_zone] - } - - tag(:meta, data) + def url_to_course_logo(course) + asset_url(course_logo_local_url(course)) end - def user_time_zone - user_signed_in? ? current_user.time_zone : nil - end + private - # This helper will includes all webpack assets - def webpack_assets_tag - capture do - concat javascript_pack_tag('vendors~coursemology') - concat javascript_pack_tag('coursemology') - end + def course_logo_local_url(course) + course.logo.medium.url || 'course_default_logo.svg' end end diff --git a/app/helpers/application_theming_helper.rb b/app/helpers/application_theming_helper.rb deleted file mode 100644 index 80f0e1b9c21..00000000000 --- a/app/helpers/application_theming_helper.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true -module ApplicationThemingHelper - def application_resources - # Used to include external javascript/css files, Currently it's empty - end - - # TODO: Remove this once fully SPA - def page_class - return nil if content_for?(:page_class_specified) - - page_class = super - content_for(:page_class_specified) { page_class } - @page_class = page_class - end - - # TODO: Remove this once fully SPA - def rails_page? - class_identifier = page_class.split.first - - [ - 'high-voltage-pages', - 'user-sessions', - 'user-registrations', - 'devise-passwords', - 'devise-confirmations' - ].include? class_identifier - end -end diff --git a/app/helpers/application_widgets_helper.rb b/app/helpers/application_widgets_helper.rb deleted file mode 100644 index 0de91c03bab..00000000000 --- a/app/helpers/application_widgets_helper.rb +++ /dev/null @@ -1,174 +0,0 @@ -# frozen_string_literal: true -module ApplicationWidgetsHelper - # Create a +edit+ button. - # - # @return [String] The HTML for the button. - # @overload edit_button(path, html_options = nil, &block) - # Creates a +edit+ button, pointing to the given path and HTML options. This would yield a - # button with an icon, unless a block is provided. - # @param [String] path The path to link to. - # @param [Hash] html_options The HTML options for the button. - # @param [Proc] block The block to use for displaying the link. - # @overload edit_button(resource, html_options = nil, &block) - # Creates a +edit+ button, pointing to the given resource. The URL is resolved using +url_for+. - # This would yield a button with an icon, unless a block is provided. - # @param [Array|Object] path The resource to link to. - # @param [Hash] html_options The HTML options for the button. - # @param [Proc] block The block to use for displaying the link. - # @overload edit_button(body, path, html_options = nil) - # Creates a +edit+ button, pointing to the given path and HTML options. This would create a - # button with the given body. - # @overload edit_button(body, resource, html_options = nil) - # Creates a +edit+ button, pointing to the given resource. The URL is resolved using +url_for+. - # This would create a button with the given body. - def edit_button(name, options = nil, html_options = nil, &block) - name, options, html_options = [nil, name, options] unless html_options - options = [:edit] + Array(options) unless options.is_a?(String) - block ||= proc { fa_icon 'edit' } - resource_button(:edit, 'btn-default', name || block, options, html_options&.dup) - end - - # Create a +delete+ button. - # - # @return [String] The HTML for the button. - # @overload delete_button(path, html_options = nil, &block) - # Creates a +delete+ button, pointing to the given path and HTML options. This would yield a - # button with an icon, unless a block is provided. - # @param [String] path The path to link to. - # @param [Hash] html_options The HTML options for the button. - # @param [Proc] block The block to use for displaying the link. - # @overload delete_button(resource, html_options = nil, &block) - # Creates a +delete+ button, pointing to the given resource. The URL is resolved using - # +url_for+. This would yield a button with an icon, unless a block is provided. - # @param [Array|Object] path The resource to link to. - # @param [Hash] html_options The HTML options for the button. - # @param [Proc] block The block to use for displaying the link. - # @overload delete_button(body, path, html_options = nil) - # Creates a +delete+ button, pointing to the given path and HTML options. This would create a - # button with the given body. - # @overload delete_button(body, resource, html_options = nil) - # Creates a +delete+ button, pointing to the given resource. The URL is resolved using - # +url_for+. This would create a button with the given body. - def delete_button(name, options = nil, html_options = nil, &block) - name, options, html_options = [nil, name, options] unless html_options - block ||= proc { fa_icon 'trash' } - - html_options = html_options&.dup || {} - html_options.reverse_merge!(method: :delete, - data: { confirm: t('helpers.buttons.delete_confirm_message') }) - resource_button(:delete, 'btn-danger', name || block, options, html_options) - end - - # Display a progress_bar with the given percentage and styling. The percentage is assumed to - # be a number ranging from 0-100. In addition, a block can be passed to add custom text. - # - # ActionView::Helpers::CaptureHelper#capture is used to ensure the block is rendered in the - # original view_context, rather than within the view_context of the progress bar layout. - # - # @param [Integer] percentage The percentage to be displayed on the progress bar. - # @param [Hash] opts Options to apply on the progress bar. Supports the following: - # class: css classes of progress bar (defaults to `progress-bar-info`), - # tooltip_text: text to be included in tooltip, - # tooltip_placement: 'left', 'top', 'bottom', or 'right'. - # @yield The HTML text which will be passed to the partial as text to be shown in the bar. - # @return [String] HTML string to render the progress bar. - def display_progress_bar(percentage, opts = {}, &block) - opts[:class] = ['progress-bar-info'] unless opts[:class] - text_in_block = capture(&block) if block_given? - render partial: 'layouts/progress_bar', - locals: { percentage: percentage, opts: opts, progress_bar_text: text_in_block } - end - - private - - # Creates a button for creating, editing, or deleting resources. - # - # @param [Symbol] key The key of the button. This can be +:new+, +:edit+, or +:delete+, and is - # used to look up an appropriate translation. - # @param [String] default_class The default CSS class to be applied to the button. - # @param [String|Proc] body The string to use as the body of the button, or a block which would - # be evaluated to give the body of the button. - # @param [Hash|nil] url_options The options to pass to +url_for+. - # @param [Hash|nil] html_options A hash of mutable options to pass to +link_to+. - def resource_button(key, default_class, body, url_options, html_options) - html_options ||= {} - html_options[:class] = deduce_resource_button_class(key, html_options[:class], default_class) - if !url_options.nil? && !url_options.is_a?(String) - html_options[:title] ||= deduce_resource_button_title(key, url_options) - end - - if body.is_a?(Proc) - link_to(url_options, html_options, &body) - else - link_to(body, url_options, html_options) - end - end - - # Deduce the CSS classes to be applied to the button from the user-specified classes and default - # class. - # - # @param [String|Symbol] key The key of the button, as passed to +resource_button+. - # @param [Array] custom_classes The CSS classes specified by the user. - # @param [String] default_type The default class to use if there is no explicit button class. - # @return [Array] The deduced set of CSS classes to apply. - def deduce_resource_button_class(key, custom_classes, default_type) - custom_classes = Set[*custom_classes] - custom_classes |= ['btn'].freeze - custom_classes << default_type unless resource_button_type_specified?(custom_classes) - custom_classes << key - custom_classes.to_a - end - - # Checks whether the given CSS classes have an explicit button type specified. - # - # @param [Set] css_classes The list of CSS classes specified. - # @return [Boolean] +true+ if the button type is specified. - def resource_button_type_specified?(css_classes) - available_button_types = Set['btn-default', 'btn-primary', 'btn-success', - 'btn-info', 'btn-warning', 'btn-danger'].freeze - css_classes.intersect?(available_button_types) - end - - # Deduces the title to be given to the button given the button type and the URL arguments. - # - # @param [Symbol] button_type The type of the button to generate a title for. - # @param [Symbol|Array|ActiveRecord::Base] resource The resource to deduce the title for. - def deduce_resource_button_title(button_type, resource) - resource = deduce_resource_button_resource(resource) - object_name = deduce_resource_object_name(resource) - resource_name = resource.try(:model_name).try(:human) || object_name.humanize - - keys = [] - keys << :"helpers.buttons.#{object_name}.#{button_type}" if object_name - keys << :"helpers.buttons.#{button_type}" - keys << "#{button_type.to_s.humanize} #{resource_name}" if resource_name - t(keys.shift, model: resource_name, default: keys) - end - - # Given a parameter set for +url_for+, deduce the resource that the parameter references. - # - # @param [Array|String] resource The resource to deduce. This handles arrays, which are - # interpreted as +url_for+ parameters. This also supports hash options to be provided to - # +url_for+. - # @return [Symbol] When an array is given. - # @return [String] When a string is given. - def deduce_resource_button_resource(resource) - return resource unless resource.is_a?(Array) - return resource[-2] if resource.last.is_a?(Hash) - - resource.last - end - - # Deduces the object name of the resource. This is the name used to construct the translation - # string and is also used as a name for sending form data. - # - # @param [Symbol|ActiveRecord::Base] resource The resource to deduce the title for. - # @return [String] The object name of the resource. - def deduce_resource_object_name(resource) - if resource.is_a?(Symbol) - resource.to_s - else - model_name_from_record_or_class(resource).param_key - end - end -end diff --git a/app/helpers/course/achievement/achievements_helper.rb b/app/helpers/course/achievement/achievements_helper.rb index 062e920f4a7..852a77ec57b 100644 --- a/app/helpers/course/achievement/achievements_helper.rb +++ b/app/helpers/course/achievement/achievements_helper.rb @@ -1,17 +1,5 @@ # frozen_string_literal: true module Course::Achievement::AchievementsHelper - # Returns the HTML code to display the achievement badge. If badge is present, return - # medium version of the badge (see ImageUploader for more versions). Otherwise, return - # default achievement badge. - # - # @param [Course::Achievement] achievement The achievement for which to display the badge. - # @return [String] A HTML fragment containing the image to display for the achievement. - def display_achievement_badge(achievement) - content_tag(:span, class: ['image']) do - image_tag(achievement_badge_path(achievement)) - end - end - # Returns the path of achievement badge, if badge is present. Otherwise, return # default achievement badge. # diff --git a/app/helpers/course/assessment/submission/submissions_autograded_helper.rb b/app/helpers/course/assessment/submission/submissions_autograded_helper.rb deleted file mode 100644 index 35af2c9de4e..00000000000 --- a/app/helpers/course/assessment/submission/submissions_autograded_helper.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true -module Course::Assessment::Submission::SubmissionsAutogradedHelper - # The maximum step that current user can attempt. - def max_step - @max_step ||= begin - question = next_unanswered_question - if question && !@assessment.skippable && cannot?(:manage, @assessment) - @submission.questions.index(question) + 1 - else - # All questions have been answered or assessment is skippable or user is a staff. - @submission.questions.length - end - end - end - - def next_unanswered_question - @next_unanswered_question ||= @submission.questions.next_unanswered(@submission) - end - - # The step that current user is on. - def current_step - @current_step ||= @current_question ? @submission.questions.index(@current_question) + 1 : nil - end - - # Highlight current step and grey out un-accessible steps. - def nav_class(step) - return 'active' if step == current_step - return 'disabled' if step > max_step - return 'completed' if step <= max_step - end -end diff --git a/app/helpers/course/assessment/submission/submissions_helper.rb b/app/helpers/course/assessment/submission/submissions_helper.rb index 8f8b81f50c6..4d4410bc6ee 100644 --- a/app/helpers/course/assessment/submission/submissions_helper.rb +++ b/app/helpers/course/assessment/submission/submissions_helper.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true module Course::Assessment::Submission::SubmissionsHelper - include Course::Assessment::Submission::SubmissionsAutogradedHelper include Course::Assessment::Answer::ProgrammingTestCaseHelper # Return the last non-current attempt if the submission is being attempted, diff --git a/app/helpers/course/controller_helper.rb b/app/helpers/course/controller_helper.rb index 007bc6341b2..395298295b1 100644 --- a/app/helpers/course/controller_helper.rb +++ b/app/helpers/course/controller_helper.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true module Course::ControllerHelper - include Course::LessonPlan::TodosHelper include Course::LeaderboardsHelper # Formats the given +CourseUser+ as a user-visible string. @@ -64,17 +63,7 @@ def link_to_user(user, options = {}, &block) end end - def url_to_course_logo(course) - asset_url(course_logo_local_url(course)) - end - def url_to_material(course, folder, material) course_material_folder_material_path(course, folder, material) end - - private - - def course_logo_local_url(course) - course.logo.medium.url || 'course_default_logo.svg' - end end diff --git a/app/helpers/course/lesson_plan/todos_helper.rb b/app/helpers/course/lesson_plan/todos_helper.rb deleted file mode 100644 index ada577a3c75..00000000000 --- a/app/helpers/course/lesson_plan/todos_helper.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true -module Course::LessonPlan::TodosHelper - # A helper to add a CSS class for each todo, based on the workflow state. - # - # @param [Course::ReferenceTime] timeline_for_todo The reference timeline for the actual todo. - # @return [Array] CSS class to be added to the todo tag. - def todo_status_class(timeline_for_todo) - if timeline_for_todo.end_at && timeline_for_todo.end_at < Time.zone.now - ['danger'] - else - [] - end - end -end diff --git a/app/helpers/form_for_with_resource_helper.rb b/app/helpers/form_for_with_resource_helper.rb deleted file mode 100644 index 1c032408bc5..00000000000 --- a/app/helpers/form_for_with_resource_helper.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true -module FormForWithResourceHelper - class << self - # Handles the +:resource+ option in +form_for+. - def form_for_with_resource_option(form_helper, record, options) - case options[:resource] - when Symbol - raise ArgumentError, ':resource and :url cannot both be specified' if options[:url] - - helper = url_helper_for_resource(record, options.delete(:resource)) - options[:url] = form_helper.public_send(helper, *record) - when nil - # noop - else - raise ArgumentError, 'Resource must be a symbol with the stem of route helper' - end - end - - private - - # Gets the URL for the record. This follows the pluralisation rules for Rails routes, where the - # CREATE action is plural, and the PUT action is singular. - # - # @param [Array|ActionRecord::Base] record The record to build the route - # from. - # @param [Symbol] helper The symbol with the stem of the URL helper. - # @return [Symbol] The appropriate URL helper to call. - def url_helper_for_resource(record, helper) - resource = record - resource = resource.last if resource.is_a?(Array) - - inflect_path(helper, resource.new_record?) - end - - # Inflects the path according to the plurality specified. - # - # @param [Symbol] stem The path to inflect. - # @param [Boolean] plural Whether to make the path plural. - # @return [String] The stem, in the given plurality. - def inflect_path(stem, plural) - components = stem.to_s.underscore.split('_') - components, suffix = parse_path_components(components) - - name = components.pop - name = plural ? name.pluralize : name.singularize - - components.push(name, suffix) - components.join('_').to_sym - end - - # Splits the path helper into the suffix (_path, or _url), and the resource involved. - # - # @param [String] components The components of the path helper. - # @return [Array<[String], String>] The list of components, with the suffix removed, followed by - # the suffix. - def parse_path_components(components) - if ['path', 'url'].include?(components.last) - [components, components.pop] - else - [components, 'path'] - end - end - end - - def form_for(record, options, &proc) - FormForWithResourceHelper.form_for_with_resource_option(self, record, options) - - super(record, options, &proc) - end -end diff --git a/app/helpers/render_within_layout_helper.rb b/app/helpers/render_within_layout_helper.rb deleted file mode 100644 index 831efec223e..00000000000 --- a/app/helpers/render_within_layout_helper.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true -module RenderWithinLayoutHelper - def render(*args, &proc) - arg = args.shift - case arg - when Hash - within_layout = arg.delete(:within_layout) - return view_renderer.render_within_layout(self, within_layout, *args, &proc) if within_layout - end - - args.unshift(arg) - super - end -end diff --git a/app/inputs/array_input.rb b/app/inputs/array_input.rb deleted file mode 100644 index 364ed6c7548..00000000000 --- a/app/inputs/array_input.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true -# https://railsguides.net/simple-form-array-text-input/ -# https://tenforward.consulting/blog/integrating-an-array-column-in-rails-with-simple-form -class ArrayInput < SimpleForm::Inputs::StringInput - def input(_wrapper_options) - input_html_options[:type] ||= input_type - existing_values = Array(object.public_send(attribute_name)).map do |array_el| - @builder.text_field(nil, input_html_options.merge(value: array_el, name: "#{object_name}[#{attribute_name}][]")) - end - if existing_values.empty? - existing_values.push @builder.text_field(nil, - input_html_options.merge(value: nil, - name: "#{object_name}[#{attribute_name}][]")) - end - existing_values.join.html_safe - end - - def input_type - :text - end -end diff --git a/app/inputs/code_input.rb b/app/inputs/code_input.rb deleted file mode 100644 index 74fa4f07968..00000000000 --- a/app/inputs/code_input.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true -class CodeInput < SimpleForm::Inputs::TextInput - private - - def html_options_for(namespace, css_classes) - return super unless namespace == :input - - super.merge(lang: options[:language]) - end -end diff --git a/app/themes/default/assets/images/default/.keep b/app/themes/default/assets/images/default/.keep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/app/themes/default/assets/javascripts/default/all.js b/app/themes/default/assets/javascripts/default/all.js deleted file mode 100644 index 87af27d8d68..00000000000 --- a/app/themes/default/assets/javascripts/default/all.js +++ /dev/null @@ -1,8 +0,0 @@ -// This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically -// be included in the compiled file accessible from http://example.com/assets/application.js -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// the compiled file. -// -//= require application -//= require_tree . diff --git a/app/themes/default/assets/stylesheets/default/all.scss.erb b/app/themes/default/assets/stylesheets/default/all.scss.erb deleted file mode 100644 index 36b330cdb5e..00000000000 --- a/app/themes/default/assets/stylesheets/default/all.scss.erb +++ /dev/null @@ -1,30 +0,0 @@ -// This is a manifest file that'll automatically include all the stylesheets available in this -// directory and any sub-directories. You're free to add application-wide styles to this file and -// they'll appear at the top of the compiled file, but it's generally better to create a new file -// per style scope. -// -@import 'application'; -@import 'pygments-css/github'; -<% -# Import the rest of the files; @import '**/*' will include application.scss multiple times. -# This is not perfect because every time a new file is added all assets need to be cleaned for the -# new set of assets to be generated. -# This is taken from app/assets/stylesheets/application.css.erb -# -# TODO: Use compass-import-once after Compass/compass#1951 is fixed -# TODO: Revert to @import '**/*' after sass/sass#139 is fixed in sass-4.0. -exclude_imports = ['layout', 'all.scss'] -imports = Dir["#{__dir__}/*"] -imports.reject! do |path| - basename = File.basename(path, '.*') - basename.start_with?('_') || exclude_imports.include?(basename) -end -imports.map! do |path| - file_path = File.file?(path) - path = path[(__dir__.length + 1)..-1] - file_path ? path : "#{path}/**/*" -end - -imports.each do |file| %> -@import '<%= file %>'; -<% end %> diff --git a/app/themes/default/locales/.keep b/app/themes/default/locales/.keep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/app/themes/default/views/layouts/_topbar.html.slim b/app/themes/default/views/layouts/_topbar.html.slim deleted file mode 100644 index f22ea549f4d..00000000000 --- a/app/themes/default/views/layouts/_topbar.html.slim +++ /dev/null @@ -1,51 +0,0 @@ -nav.navbar.navbar-inverse.navbar-fixed-top role="navigation" - div.container-fluid - div.navbar-header - button.navbar-toggle.collapsed type="button" data-toggle="collapse" data-target="#site-navigation-navbar" aria-expanded="false" aria-controls="navbar" - span.sr-only - = t('layout.navbar.toggle_navigation') - span.icon-bar - span.icon-bar - span.icon-bar - a.navbar-brand href=root_path - = t('layout.coursemology') - div.collapse.navbar-collapse#site-navigation-navbar - ul.nav.navbar-nav.pull-right - li - - my_courses = user_signed_in? && Course.containing_user(current_user).ordered_by_start_at - - if my_courses.present? - a.dropdown-toggle data-toggle="dropdown" - => t('layout.navbar.courses') - span.caret - ul.dropdown-menu.pull-right.courses-menu - - my_courses.each do |course| - li = link_to(format_inline_text(course.title), course_path(course)) - li.divider role='separator' - li = link_to(t('layout.navbar.all_courses'), courses_path) - - else - = link_to(t('layout.navbar.courses'), courses_path) - li - = link_to(t('layout.navbar.help'), '#') - - if user_signed_in? - li - a.dropdown-toggle data-toggle="dropdown" - => current_user.name - span.caret - ul.dropdown-menu - li - = link_to(t('user.admin.navbar.account_settings'), edit_user_profile_path) - - if can?(:manage, :all) - li - = link_to(t('layout.navbar.admin_panel'), admin_path) - - if can?(:manage, current_tenant) - li - = link_to(t('layout.navbar.instance_admin_panel'), admin_instance_admin_path) - li - = link_to(t('layout.navbar.sign_out'), destroy_user_session_path, method: :delete) - - if user_masquerade? - li = link_to t('layout.navbar.stop_masquerading'), back_masquerade_path(current_user) - - else - li - = link_to(t('layout.navbar.register'), new_user_registration_path) - li - = link_to(t('layout.navbar.sign_in'), new_user_session_path) diff --git a/app/themes/default/views/layouts/default.html.slim b/app/themes/default/views/layouts/default.html.slim deleted file mode 100644 index de90559f80e..00000000000 --- a/app/themes/default/views/layouts/default.html.slim +++ /dev/null @@ -1,28 +0,0 @@ -doctype html -html - head - title - = page_title - meta http-equiv="X-UA-Compatible" content="IE=edge" - meta name="status" content=response.status - = server_context_meta_tag - = viewport_meta_tag - = application_resources - = stylesheet_link_tag 'default/all', media: 'all' - = csrf_meta_tags - = webpack_assets_tag - = javascript_include_tag 'default/all' - = header_tags - = render 'layouts/favicon' - - body - - if rails_page? - = render 'layouts/topbar' - div#root.container-fluid - = global_announcements - = flash_messages - div class=@page_class - = yield - - else - div#root - = yield diff --git a/app/views/announcements/_global_announcement.html.slim b/app/views/announcements/_global_announcement.html.slim deleted file mode 100644 index 6b334cd442c..00000000000 --- a/app/views/announcements/_global_announcement.html.slim +++ /dev/null @@ -1,11 +0,0 @@ -div.panel.panel-primary.global-announcement id="global_announcement_#{global_announcement.id}" - div.panel-heading - div.pull-right - = link_to announcement_mark_as_read_path(global_announcement), remote: true, method: :post do - button.close (type='button' data-dismiss='alert' aria-label=t('.close') - data-target='#global_announcement_#{global_announcement.id}') - span aria-hidden='true' - | × - = format_inline_text(global_announcement.title) - div.panel-body - = format_html(global_announcement.content) diff --git a/app/views/announcements/_global_announcements.html.slim b/app/views/announcements/_global_announcements.html.slim deleted file mode 100644 index 24592772f6b..00000000000 --- a/app/views/announcements/_global_announcements.html.slim +++ /dev/null @@ -1,2 +0,0 @@ -- announcements = controller.unread_global_announcements -= render partial: 'announcements/global_announcement', collection: announcements diff --git a/app/views/announcements/index.html.slim b/app/views/announcements/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/announcements/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/pages/home.json.jbuilder b/app/views/application/index.json.jbuilder similarity index 58% rename from app/views/pages/home.json.jbuilder rename to app/views/application/index.json.jbuilder index 7edba331f2c..8c16b817077 100644 --- a/app/views/pages/home.json.jbuilder +++ b/app/views/application/index.json.jbuilder @@ -1,23 +1,32 @@ # frozen_string_literal: true +json.locale I18n.locale +json.timeZone ActiveSupport::TimeZone::MAPPING[user_time_zone] + if user_signed_in? my_courses = Course.containing_user(current_user).ordered_by_start_at + course_last_active_times_hash = CourseUser.for_user(current_user).pluck(:course_id, :last_active_at).to_h + if my_courses.present? json.courses my_courses do |course| + json.id course.id json.title course.title json.url course_path(course) + json.logoUrl url_to_course_logo(course) + json.lastActiveAt course_last_active_times_hash[course.id] end end json.user do + json.id current_user.id json.name current_user.name + json.primaryEmail current_user.email json.url user_path(current_user) json.avatarUrl user_image(current_user) json.role current_user.role json.instanceRole controller.current_instance_user&.role + json.canCreateNewCourse can?(:create, Course.new) end - json.signOutUrl destroy_user_session_path - if user_masquerade? json.masqueradeUserName current_user.name json.stopMasqueradingUrl back_masquerade_path(current_user) diff --git a/app/views/attachment_references/destroy.js.erb b/app/views/attachment_references/destroy.js.erb deleted file mode 100644 index 0f7be431fe3..00000000000 --- a/app/views/attachment_references/destroy.js.erb +++ /dev/null @@ -1,8 +0,0 @@ -<%- if @attachment_reference.destroyed? %> - $('#<%= dom_id(@attachment_reference) %>').remove(); -<% end %> - -// Remove older flash messages, and show users the new flash message -$('.course-layout').parents('.container-fluid:first').find('.alert').remove(); -$('.course-layout').parents('.container-fluid:first').prepend('<%= j(flash_messages) %>'); -window.scrollTo(0, 0); diff --git a/app/views/attachments/_attachment.html.slim b/app/views/attachments/_attachment.html.slim deleted file mode 100644 index 82229bbddbb..00000000000 --- a/app/views/attachments/_attachment.html.slim +++ /dev/null @@ -1,10 +0,0 @@ -- delete = local_assigns[:delete] ? local_assigns[:delete] : false -- if attachment - = content_tag_for(:div, attachment, class: ['attachment']) - span = link_to format_inline_text(attachment.name), attachment_reference_path(attachment), - target: "_blank" - span.uploaded-by = t('.uploaded_by', name: attachment.creator.name) - - if delete - span.delete-attachment - = link_to t('.delete_attachment'), attachment_reference_path(attachment), - data: { confirm: t('.confirm_delete_attachment') }, method: :delete, remote: true diff --git a/app/views/course/achievement/achievements/index.html.slim b/app/views/course/achievement/achievements/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/achievement/achievements/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/admin/index.html.slim b/app/views/course/admin/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/admin/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/announcements/index.html.slim b/app/views/course/announcements/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/announcements/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/assessments/edit.html.slim b/app/views/course/assessment/assessments/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/assessments/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/assessments/index.html.slim b/app/views/course/assessment/assessments/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/assessments/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/assessments/monitoring.html.slim b/app/views/course/assessment/assessments/monitoring.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/assessments/monitoring.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/assessments/show.html.slim b/app/views/course/assessment/assessments/show.html.slim deleted file mode 100644 index 72c4657f683..00000000000 --- a/app/views/course/assessment/assessments/show.html.slim +++ /dev/null @@ -1,4 +0,0 @@ -/ Randomized Assessment is temporarily hidden (PR#5406) -/ - if @assessment.randomization.present? -/ = render 'assessment_question_bundle_buttons', assessment: @assessment -div#app-root diff --git a/app/views/course/assessment/assessments/statistics.html.slim b/app/views/course/assessment/assessments/statistics.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/assessments/statistics.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/forum_post_responses/edit.html.slim b/app/views/course/assessment/question/forum_post_responses/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/forum_post_responses/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/forum_post_responses/new.html.slim b/app/views/course/assessment/question/forum_post_responses/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/forum_post_responses/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/multiple_responses/edit.html.slim b/app/views/course/assessment/question/multiple_responses/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/multiple_responses/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/multiple_responses/new.html.slim b/app/views/course/assessment/question/multiple_responses/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/multiple_responses/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/programming/edit.html.slim b/app/views/course/assessment/question/programming/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/programming/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/programming/new.html.slim b/app/views/course/assessment/question/programming/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/programming/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/scribing/edit.html.slim b/app/views/course/assessment/question/scribing/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/scribing/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/scribing/new.html.slim b/app/views/course/assessment/question/scribing/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/scribing/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/text_responses/_comprehension_group_fields.html.slim b/app/views/course/assessment/question/text_responses/_comprehension_group_fields.html.slim deleted file mode 100644 index 749e9ad852c..00000000000 --- a/app/views/course/assessment/question/text_responses/_comprehension_group_fields.html.slim +++ /dev/null @@ -1,23 +0,0 @@ -= content_tag_for(:tr, f.object, class: 'nested-fields') do - td - b = t('.group') - br - = link_to_remove_association t('.remove_group'), f - br - br - = f.input :maximum_group_grade, label: t('.maximum_group_grade'), - input_html: { class: ['text-response-group-maximum-group-grade'] } - - - group_id = f.object_name - - table.table.table-hover - thead - tr - th = link_to_add_association t('.add_point'), f, :points, - partial: 'comprehension_point_fields', - find_selector: 'tbody.tbody-groups.'+group_id, - insert_using: 'append' - / group_id must not be the last class so that it will be correctly substituted by cocoon - tbody class=[group_id, 'tbody-groups'] - = f.simple_fields_for :points do |comprehension_points_form| - = render 'comprehension_point_fields', f: comprehension_points_form diff --git a/app/views/course/assessment/question/text_responses/_comprehension_point_fields.html.slim b/app/views/course/assessment/question/text_responses/_comprehension_point_fields.html.slim deleted file mode 100644 index c0adec30cf0..00000000000 --- a/app/views/course/assessment/question/text_responses/_comprehension_point_fields.html.slim +++ /dev/null @@ -1,32 +0,0 @@ -= content_tag_for(:tr, f.object.group, class: 'nested-fields') do - td - b = t('.point') - br - = link_to_remove_association t('.remove_point'), f - br - br - = f.input :point_grade, label: t('.point_grade'), - input_html: { class: ['text-response-group-point-grade'] } - - - point_id = f.object_name - - .has-error - = f.error :solutions - - table.table.table-striped.table-hover.table-points - thead - tr - th = t('.solution_type') - th = t('.solution') - th - th = t('.information') - th - div.pull-right - = link_to_add_association t('.add_solution'), f, :solutions, - partial: 'comprehension_solution_fields', - find_selector: 'tbody.tbody-points.'+point_id, - insert_using: 'append' - / point_id must not be the last class so that it will be correctly substituted by cocoon - tbody class=[point_id, 'tbody-points'] - = f.simple_fields_for :solutions do |comprehension_solutions_form| - = render 'comprehension_solution_fields', f: comprehension_solutions_form diff --git a/app/views/course/assessment/question/text_responses/_comprehension_solution_fields.html.slim b/app/views/course/assessment/question/text_responses/_comprehension_solution_fields.html.slim deleted file mode 100644 index 3fd0cc77923..00000000000 --- a/app/views/course/assessment/question/text_responses/_comprehension_solution_fields.html.slim +++ /dev/null @@ -1,20 +0,0 @@ -= content_tag_for(:tr, f.object, class: 'nested-fields') do - td = f.input :solution_type, - collection: Course::Assessment::Question::TextResponseComprehensionSolution.solution_types.keys, - label_method: lambda { |key| t(".#{key}") }, - input_html: { class: ['text-response-solution-type'] }, - label: false - / TODO: Fix text to array. - / f.object_name must not be the last class so that it will be correctly substituted by cocoon - td class=[f.object_name, 'td-solution'] - = f.input :solution, as: :array, label: false, required: false, - input_html: { class: ['text-response-solution'] } - .has-error - = f.error :solution_lemma - td.td-solution-button - = link_to 'javascript:void(0)', - class: ['btn', 'btn-default', 'solution-button', f.object_name], - title: t('.add_solution_word') do - = fa_icon 'plus'.freeze - td = f.input :information, label: false, placeholder: t('.information_hint') - td = link_to_remove_association t('.remove'), f diff --git a/app/views/course/assessment/question/text_responses/_form_comprehension.html.slim b/app/views/course/assessment/question/text_responses/_form_comprehension.html.slim deleted file mode 100644 index da5b7846bea..00000000000 --- a/app/views/course/assessment/question/text_responses/_form_comprehension.html.slim +++ /dev/null @@ -1,29 +0,0 @@ -= simple_form_for [current_course, @assessment, @text_response_question] do |f| - = f.error_notification - = render partial: 'course/assessment/questions/form', locals: { f: f, question_assessment: @question_assessment } - = f.hidden_field :hide_text - = f.hidden_field :is_comprehension - = f.hidden_field :allow_attachment - - b = t('.multiline_explanation_comprehension_html') - table.table.table-hover.table-comprehension - thead - tr - th = link_to_add_association t('.add_group'), f, :groups, - partial: 'comprehension_group_fields', - find_selector: 'tbody.tbody-form', insert_using: 'append' - tbody.tbody-form - = f.simple_fields_for :groups do |comprehension_groups_form| - = render 'comprehension_group_fields', f: comprehension_groups_form - - - if @assessment.autograded? - p - b = t('.text_response_autograde') - - - name = t('.comprehension') - - - if f.object.persisted? - - button_text = t('helpers.buttons.update', model: name) - - else - - button_text = t('helpers.buttons.create', model: name) - = f.button :submit, button_text diff --git a/app/views/course/assessment/question/text_responses/edit.html.slim b/app/views/course/assessment/question/text_responses/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/text_responses/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/text_responses/new.html.slim b/app/views/course/assessment/question/text_responses/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/text_responses/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/voice_responses/edit.html.slim b/app/views/course/assessment/question/voice_responses/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/voice_responses/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/voice_responses/new.html.slim b/app/views/course/assessment/question/voice_responses/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/voice_responses/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question_bundle_assignments/edit.html.slim b/app/views/course/assessment/question_bundle_assignments/edit.html.slim index 1664dfc9a42..db223c4f3ed 100644 --- a/app/views/course/assessment/question_bundle_assignments/edit.html.slim +++ b/app/views/course/assessment/question_bundle_assignments/edit.html.slim @@ -1,4 +1,4 @@ -= page_header 'Edit Question Bundle Assignment' +/ = page_header 'Edit Question Bundle Assignment' - url = course_assessment_question_bundle_assignment_path(current_course, @assessment, @question_bundle_assignment) = simple_form_for @question_bundle_assignment, url: url do |f| diff --git a/app/views/course/assessment/question_bundle_assignments/index.html.slim b/app/views/course/assessment/question_bundle_assignments/index.html.slim index 9461133abf2..b1e8baa7fdd 100644 --- a/app/views/course/assessment/question_bundle_assignments/index.html.slim +++ b/app/views/course/assessment/question_bundle_assignments/index.html.slim @@ -1,4 +1,4 @@ -= page_header +/ = page_header h2 = t('.prepared_bundle_assignments') diff --git a/app/views/course/assessment/question_bundle_questions/edit.html.slim b/app/views/course/assessment/question_bundle_questions/edit.html.slim index df420915434..85b1f77a10e 100644 --- a/app/views/course/assessment/question_bundle_questions/edit.html.slim +++ b/app/views/course/assessment/question_bundle_questions/edit.html.slim @@ -1,4 +1,4 @@ -= page_header 'Edit Question Bundle Question' +/ = page_header 'Edit Question Bundle Question' = simple_form_for @question_bundle_question, url: course_assessment_question_bundle_question_path(current_course, @assessment, @question_bundle_question) do |f| diff --git a/app/views/course/assessment/question_bundle_questions/index.html.slim b/app/views/course/assessment/question_bundle_questions/index.html.slim index dc78efb3db6..cbffb4ea6c9 100644 --- a/app/views/course/assessment/question_bundle_questions/index.html.slim +++ b/app/views/course/assessment/question_bundle_questions/index.html.slim @@ -1,4 +1,4 @@ -= page_header 'Question Bundle Questions' +/ = page_header 'Question Bundle Questions' = link_to 'New Question Bundle Question', new_course_assessment_question_bundle_question_path(current_course, @assessment), class: %w(btn btn-primary) diff --git a/app/views/course/assessment/question_bundle_questions/new.html.slim b/app/views/course/assessment/question_bundle_questions/new.html.slim index 690f550fc64..f0ce85270c9 100644 --- a/app/views/course/assessment/question_bundle_questions/new.html.slim +++ b/app/views/course/assessment/question_bundle_questions/new.html.slim @@ -1,4 +1,4 @@ -= page_header 'New Question Bundle Question' +/ = page_header 'New Question Bundle Question' = simple_form_for @question_bundle_question, url: course_assessment_question_bundle_questions_path(current_course, @assessment) do |f| = render partial: 'form', locals: { f: f } diff --git a/app/views/course/assessment/question_bundles/edit.html.slim b/app/views/course/assessment/question_bundles/edit.html.slim index 0be90c31b66..5ae8b6ce8bb 100644 --- a/app/views/course/assessment/question_bundles/edit.html.slim +++ b/app/views/course/assessment/question_bundles/edit.html.slim @@ -1,4 +1,4 @@ -= page_header 'Edit Question Bundle' +/ = page_header 'Edit Question Bundle' = simple_form_for @question_bundle, url: course_assessment_question_bundle_path(current_course, @assessment, @question_bundle) do |f| diff --git a/app/views/course/assessment/question_bundles/index.html.slim b/app/views/course/assessment/question_bundles/index.html.slim index eeabdb26673..e99f335c1fd 100644 --- a/app/views/course/assessment/question_bundles/index.html.slim +++ b/app/views/course/assessment/question_bundles/index.html.slim @@ -1,4 +1,4 @@ -= page_header 'Question Bundles' +/ = page_header 'Question Bundles' = link_to 'New Question Bundle', new_course_assessment_question_bundle_path(current_course, @assessment), class: %w(btn btn-primary) diff --git a/app/views/course/assessment/question_bundles/new.html.slim b/app/views/course/assessment/question_bundles/new.html.slim index f9928623c0f..1b7fa123062 100644 --- a/app/views/course/assessment/question_bundles/new.html.slim +++ b/app/views/course/assessment/question_bundles/new.html.slim @@ -1,4 +1,4 @@ -= page_header 'New Question Bundle' +/ = page_header 'New Question Bundle' = simple_form_for @question_bundle, url: course_assessment_question_bundles_path(current_course, @assessment) do |f| = render partial: 'form', locals: { f: f } diff --git a/app/views/course/assessment/question_groups/edit.html.slim b/app/views/course/assessment/question_groups/edit.html.slim index b354f188834..28951179082 100644 --- a/app/views/course/assessment/question_groups/edit.html.slim +++ b/app/views/course/assessment/question_groups/edit.html.slim @@ -1,4 +1,4 @@ -= page_header 'Edit Question Group' +/ = page_header 'Edit Question Group' = simple_form_for @question_group, url: course_assessment_question_group_path(current_course, @assessment, @question_group) do |f| diff --git a/app/views/course/assessment/question_groups/index.html.slim b/app/views/course/assessment/question_groups/index.html.slim index 785acc3eed0..34df376ec86 100644 --- a/app/views/course/assessment/question_groups/index.html.slim +++ b/app/views/course/assessment/question_groups/index.html.slim @@ -1,4 +1,4 @@ -= page_header 'Question Groups' +/ = page_header 'Question Groups' = link_to 'New Question Group', new_course_assessment_question_group_path(current_course, @assessment), class: %w(btn btn-primary) diff --git a/app/views/course/assessment/question_groups/new.html.slim b/app/views/course/assessment/question_groups/new.html.slim index 57e486fb60b..06ba422f502 100644 --- a/app/views/course/assessment/question_groups/new.html.slim +++ b/app/views/course/assessment/question_groups/new.html.slim @@ -1,4 +1,4 @@ -= page_header 'New Question Group' +/ = page_header 'New Question Group' = simple_form_for @question_group, url: course_assessment_question_groups_path(current_course, @assessment) do |f| = render partial: 'form', locals: { f: f } diff --git a/app/views/course/assessment/sessions/new.html.slim b/app/views/course/assessment/sessions/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/sessions/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/skills/index.html.slim b/app/views/course/assessment/skills/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/skills/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/submission/logs/index.html.slim b/app/views/course/assessment/submission/logs/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/submission/logs/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/submission/submissions/edit.html.slim b/app/views/course/assessment/submission/submissions/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/submission/submissions/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/submission/submissions/index.html.slim b/app/views/course/assessment/submission/submissions/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/submission/submissions/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/submissions/index.html.slim b/app/views/course/assessment/submissions/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/submissions/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/courses/index.html.slim b/app/views/course/courses/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/courses/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/courses/show.html.slim b/app/views/course/courses/show.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/courses/show.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/discussion/topics/index.html.slim b/app/views/course/discussion/topics/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/discussion/topics/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/duplications/show.html.slim b/app/views/course/duplications/show.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/duplications/show.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/enrol_requests/index.html.slim b/app/views/course/enrol_requests/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/enrol_requests/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/experience_points/disbursement/new.html.slim b/app/views/course/experience_points/disbursement/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/experience_points/disbursement/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/experience_points_records/index.html.slim b/app/views/course/experience_points_records/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/experience_points_records/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/forum/forums/index.html.slim b/app/views/course/forum/forums/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/forum/forums/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/group/group_categories/index.html.slim b/app/views/course/group/group_categories/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/group/group_categories/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/group/group_categories/show.html.slim b/app/views/course/group/group_categories/show.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/group/group_categories/show.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/leaderboards/index.html.slim b/app/views/course/leaderboards/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/leaderboards/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/learning_map/index.html.slim b/app/views/course/learning_map/index.html.slim deleted file mode 100644 index 9b17f9c0201..00000000000 --- a/app/views/course/learning_map/index.html.slim +++ /dev/null @@ -1,3 +0,0 @@ -= page_header format_inline_text(@settings.title || t('.header')) - -div#app-root diff --git a/app/views/course/lesson_plan/items/_personal_or_ref_time.html.slim b/app/views/course/lesson_plan/items/_personal_or_ref_time.html.slim deleted file mode 100644 index 52218be18ea..00000000000 --- a/app/views/course/lesson_plan/items/_personal_or_ref_time.html.slim +++ /dev/null @@ -1,11 +0,0 @@ -- effective_time = item.time_for(course_user) -- reference_time = item.reference_time_for(course_user) -- if effective_time.is_a? Course::PersonalTime and effective_time.fixed? - span title=t('course.lesson_plan.items.fixed_desc') - = fa_icon 'lock' -=< format_datetime(effective_time[attribute], datetime_format) if effective_time[attribute] -- if (effective_time[attribute] != reference_time[attribute]) && (effective_time[attribute] != nil) && (reference_time[attribute] != nil) - br - strike - = t('course.lesson_plan.items.ref') - = format_datetime(reference_time[attribute], datetime_format) diff --git a/app/views/course/lesson_plan/items/index.html.slim b/app/views/course/lesson_plan/items/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/lesson_plan/items/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/levels/index.html.slim b/app/views/course/levels/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/levels/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/material/folders/show.html.slim b/app/views/course/material/folders/show.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/material/folders/show.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/personal_times/index.html.slim b/app/views/course/personal_times/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/personal_times/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/reference_timelines/index.html.slim b/app/views/course/reference_timelines/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/reference_timelines/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/statistics/index.html.slim b/app/views/course/statistics/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/statistics/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/survey/surveys/index.html.slim b/app/views/course/survey/surveys/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/survey/surveys/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/user_email_subscriptions/edit.html.slim b/app/views/course/user_email_subscriptions/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/user_email_subscriptions/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/user_invitations/index.html.slim b/app/views/course/user_invitations/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/user_invitations/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/user_invitations/new.html.slim b/app/views/course/user_invitations/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/user_invitations/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/user_registrations/create.html.slim b/app/views/course/user_registrations/create.html.slim deleted file mode 100644 index 5ac0dff5816..00000000000 --- a/app/views/course/user_registrations/create.html.slim +++ /dev/null @@ -1 +0,0 @@ -= render 'course/courses/show' diff --git a/app/views/course/users/index.html.slim b/app/views/course/users/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/users/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/users/staff.html.slim b/app/views/course/users/staff.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/users/staff.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/users/students.html.slim b/app/views/course/users/students.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/users/students.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/video/submission/submissions/index.html.slim b/app/views/course/video/submission/submissions/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/video/submission/submissions/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/video/videos/index.html.slim b/app/views/course/video/videos/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/video/videos/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/video_submissions/index.html.slim b/app/views/course/video_submissions/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/video_submissions/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/devise/confirmations/new.html.slim b/app/views/devise/confirmations/new.html.slim deleted file mode 100644 index 64c7702a32e..00000000000 --- a/app/views/devise/confirmations/new.html.slim +++ /dev/null @@ -1,13 +0,0 @@ -= page_header t('.resend') - -= simple_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| - = f.error_notification - = f.full_error :confirmation_token - - div.form-inputs - = f.input :email, required: true, autofocus: true, value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email), input_html: { autocomplete: "email" } - - div.form-actions - = f.button :submit, t('.resend') - -= render 'devise/shared/links' diff --git a/app/views/devise/passwords/new.html.slim b/app/views/devise/passwords/new.html.slim deleted file mode 100644 index 69b36a375cf..00000000000 --- a/app/views/devise/passwords/new.html.slim +++ /dev/null @@ -1,12 +0,0 @@ -= page_header t('.title') - -= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| - = f.error_notification - - div.form-inputs - = f.input :email, required: true, autofocus: true, input_html: { autocomplete: "email" } - - div.form-actions - = f.button :submit, t('.button') - -= render "devise/shared/links" diff --git a/app/views/layouts/_attachment_uploader.html.slim b/app/views/layouts/_attachment_uploader.html.slim deleted file mode 100644 index a89383d93bf..00000000000 --- a/app/views/layouts/_attachment_uploader.html.slim +++ /dev/null @@ -1,17 +0,0 @@ -div.attachment-uploader - - if multiple - - unless f.object.attachments.empty? - strong => t('.uploaded_files') - - f.object.attachments.each do |attachment| - = render partial: 'attachments/attachment', - locals: { attachment: attachment, delete: allow_delete } - div - strong = t('.new_files') - = f.file_field :files, multiple: true - - else - - if f.object.attachment.present? && f.object.attachment.persisted? - strong => t('.uploaded_file') - = render partial: 'attachments/attachment', - locals: { attachment: f.object.attachment, delete: allow_delete } - div - = f.file_field :file diff --git a/app/views/layouts/_favicon.html.erb b/app/views/layouts/_favicon.html.erb deleted file mode 100644 index 36f21cc7587..00000000000 --- a/app/views/layouts/_favicon.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<%# Layout used to add favicon. %> - - - - - - - - diff --git a/app/views/layouts/_progress_bar.html.slim b/app/views/layouts/_progress_bar.html.slim deleted file mode 100644 index 2bace588be5..00000000000 --- a/app/views/layouts/_progress_bar.html.slim +++ /dev/null @@ -1,6 +0,0 @@ -div.progress - div.progress-bar [ class=opts[:class] role='progressbar' aria-valuenow="#{percentage}" - aria-valuemin='0' aria-valuemax='100' style="width: #{percentage}%" - title=opts[:tooltip_text] data-placement=opts[:tooltip_placement]] - span.sr-only #{percentage}% Complete - = progress_bar_text diff --git a/app/views/layouts/_user.html.slim b/app/views/layouts/_user.html.slim deleted file mode 100644 index ca55adc8bb4..00000000000 --- a/app/views/layouts/_user.html.slim +++ /dev/null @@ -1,4 +0,0 @@ -= div_for(user) do - = display_user_image(user) - span.name - = display_user(user) diff --git a/app/views/layouts/course.html.slim b/app/views/layouts/course.html.slim deleted file mode 100644 index db67704c57f..00000000000 --- a/app/views/layouts/course.html.slim +++ /dev/null @@ -1,3 +0,0 @@ -- layout = controller.parent_layout(of_layout: 'course') || controller.current_layout -= render within_layout: layout do - div#app-root diff --git a/app/views/pages/403.html.slim b/app/views/pages/403.html.slim deleted file mode 100644 index b72b926f8ba..00000000000 --- a/app/views/pages/403.html.slim +++ /dev/null @@ -1,3 +0,0 @@ -div.page-header - h1 = t('pages.403.header') -p = simple_format(@exception.message) diff --git a/app/views/pages/home.html.slim b/app/views/pages/home.html.slim deleted file mode 100644 index d90da6f586b..00000000000 --- a/app/views/pages/home.html.slim +++ /dev/null @@ -1 +0,0 @@ -p Home Page TBD diff --git a/app/views/system/admin/admin/index.html.slim b/app/views/system/admin/admin/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/system/admin/admin/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/system/admin/instance/admin/index.html.slim b/app/views/system/admin/instance/admin/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/system/admin/instance/admin/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/user/profiles/edit.html.slim b/app/views/user/profiles/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/user/profiles/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/user/profiles/edit.json.jbuilder b/app/views/user/profiles/edit.json.jbuilder index 8c2400f5bc2..7154f0567e1 100644 --- a/app/views/user/profiles/edit.json.jbuilder +++ b/app/views/user/profiles/edit.json.jbuilder @@ -2,7 +2,7 @@ json.id current_user.id json.name current_user.name -json.timezone current_user.time_zone +json.timeZone user_time_zone json.locale I18n.locale json.imageUrl user_image(current_user) json.availableLocales I18n.available_locales diff --git a/app/views/user/registrations/create.json.jbuilder b/app/views/user/registrations/create.json.jbuilder new file mode 100644 index 00000000000..51f3133daef --- /dev/null +++ b/app/views/user/registrations/create.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true +json.id @user.id +json.confirmed @user.confirmed? diff --git a/app/views/user/registrations/new.html.slim b/app/views/user/registrations/new.html.slim deleted file mode 100644 index 5145789cff3..00000000000 --- a/app/views/user/registrations/new.html.slim +++ /dev/null @@ -1,26 +0,0 @@ -= page_header - -= simple_format(t('.already_registered_html', - sign_in: link_to(t('layout.navbar.sign_in'), new_user_session_path))) - -= simple_form_for resource, as: resource_name, url: registration_path(resource_name) do |f| - = f.error_notification - .form-inputs - - if @invitation - = hidden_field_tag :invitation, @invitation.invitation_key - = f.input :name, required: true, input_html: { value: @invitation.name }, disabled: true - = f.input :email, required: true, input_html: { value: @invitation.email }, disabled: true - - else - = f.input :name, required: true, autofocus: true - = f.input :email, required: true - = f.input :password, required: true, - hint: (t('.password_hint', length: @minimum_password_length) if @validatable) - = f.input :password_confirmation, required: true - - = recaptcha_tags - br - - .form-actions - = f.button :submit, t('.sign_up') - -= render 'devise/shared/links' diff --git a/app/views/user/sessions/new.html.slim b/app/views/user/sessions/new.html.slim deleted file mode 100644 index d89ea5d3348..00000000000 --- a/app/views/user/sessions/new.html.slim +++ /dev/null @@ -1,12 +0,0 @@ -= page_header - -= simple_form_for resource, as: resource_name, url: session_path(resource_name) do |f| - div.form-inputs - = f.input :email, required: false, autofocus: true - = f.input :password, required: false - = f.input :remember_me, as: :boolean if devise_mapping.rememberable? - - div.form-actions - = f.button :submit, t('.sign_in') - -= render 'devise/shared/links' diff --git a/app/views/users/show.html.slim b/app/views/users/show.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/users/show.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/bin/webpack b/bin/webpack deleted file mode 100755 index ae33ffaa0bd..00000000000 --- a/bin/webpack +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env ruby -$stdout.sync = true - -require 'shellwords' - -ENV['RAILS_ENV'] ||= 'development' -RAILS_ENV = ENV['RAILS_ENV'] - -ENV['NODE_ENV'] ||= RAILS_ENV -NODE_ENV = ENV['NODE_ENV'] - -APP_PATH = File.expand_path('../client', __dir__) -NODE_MODULES_PATH = File.join(APP_PATH, 'node_modules') -WEBPACK_CONFIG = File.join(APP_PATH, RAILS_ENV == 'development' ? 'webpack.dev.js' : 'webpack.prod.js') - -unless File.exist?(WEBPACK_CONFIG) - puts 'Webpack configuration not found.' - puts 'Please run bundle exec rails webpacker:install to install webpacker' - exit! -end - -env = { 'NODE_PATH' => NODE_MODULES_PATH.shellescape } -cmd = RAILS_ENV == 'development' ? [ 'yarn build:development' ] : [ 'yarn build:production' ] - -Dir.chdir(APP_PATH) do - exec env, *cmd -end diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server deleted file mode 100755 index 3ed8e0d01b4..00000000000 --- a/bin/webpack-dev-server +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env ruby -# TODO: This is the default template and needs to be configured to work with our -# client/ setup. File is retained as webpacker checks the presence of this file. -$stdout.sync = true - -require "shellwords" -require "yaml" -require "socket" - -ENV["RAILS_ENV"] ||= "development" -RAILS_ENV = ENV["RAILS_ENV"] - -ENV["NODE_ENV"] ||= RAILS_ENV -NODE_ENV = ENV["NODE_ENV"] - -APP_PATH = File.expand_path("../client", __dir__) -CONFIG_FILE = File.join(APP_PATH, "../config/webpacker.yml") -NODE_MODULES_PATH = File.join(APP_PATH, "node_modules") -WEBPACK_CONFIG = File.join(APP_PATH, RAILS_ENV == 'development' ? 'webpack.dev.js' : 'webpack.prod.js') - -DEFAULT_LISTEN_HOST_ADDR = NODE_ENV == 'development' ? 'localhost' : '0.0.0.0' - -def args(key) - index = ARGV.index(key) - index ? ARGV[index + 1] : nil -end - -begin - dev_server = YAML.load_file(CONFIG_FILE)[RAILS_ENV]["dev_server"] - - HOSTNAME = args('--host') || dev_server["host"] - PORT = args('--port') || dev_server["port"] - HTTPS = ARGV.include?('--https') || dev_server["https"] - DEV_SERVER_ADDR = "http#{"s" if HTTPS}://#{HOSTNAME}:#{PORT}" - LISTEN_HOST_ADDR = args('--listen-host') || DEFAULT_LISTEN_HOST_ADDR - -rescue Errno::ENOENT, NoMethodError - $stdout.puts "Webpack dev_server configuration not found in #{CONFIG_FILE}." - $stdout.puts "Please run bundle exec rails webpacker:install to install webpacker" - exit! -end - -begin - server = TCPServer.new(LISTEN_HOST_ADDR, PORT) - server.close - -rescue Errno::EADDRINUSE - $stdout.puts "Another program is running on port #{PORT}. Set a new port in #{CONFIG_FILE} for dev_server" - exit! -end - -# Delete supplied host, port and listen-host CLI arguments -["--host", "--port", "--listen-host"].each do |arg| - ARGV.delete(args(arg)) - ARGV.delete(arg) -end - -env = { "NODE_PATH" => NODE_MODULES_PATH.shellescape } - -cmd = [ - "#{NODE_MODULES_PATH}/.bin/webpack-dev-server", "--progress", "--color", - "--config", WEBPACK_CONFIG, - "--host", LISTEN_HOST_ADDR, - "--public", "#{HOSTNAME}:#{PORT}", - "--port", PORT.to_s -] + ARGV - -Dir.chdir(APP_PATH) do - exec env, *cmd -end diff --git a/client/.babelrc b/client/.babelrc index 4052e320149..2e91825d49d 100644 --- a/client/.babelrc +++ b/client/.babelrc @@ -42,6 +42,9 @@ }, "test": { "plugins": ["babel-plugin-transform-import-meta"] + }, + "e2e-test": { + "plugins": ["istanbul"] } } } diff --git a/client/.eslintignore b/client/.eslintignore index 1f63a19713e..15b05b64b92 100644 --- a/client/.eslintignore +++ b/client/.eslintignore @@ -1,2 +1,4 @@ vendor/**/* node_modules/**/* +build/**/* +coverage/** diff --git a/client/.eslintrc.js b/client/.eslintrc.js index f86ad82293f..d0a5c8e5da2 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -158,6 +158,7 @@ module.exports = { ignorePropertyModificationsFor: ['draft', 'reducerObject'], }, ], + 'no-console': ['error', { allow: ['warn', 'error', 'info'] }], }, globals: { window: true, @@ -193,7 +194,6 @@ module.exports = { rules: { '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], 'no-unused-vars': 'off', - 'no-console': ['error', { allow: ['warn', 'error'] }], 'react-hooks/rules-of-hooks': 'warn', 'react/react-in-jsx-scope': 'off', 'no-param-reassign': 'off', diff --git a/client/.prettierignore b/client/.prettierignore index 394f1261cb8..d8e6628c1cb 100644 --- a/client/.prettierignore +++ b/client/.prettierignore @@ -3,3 +3,4 @@ build coverage vendor *.yml +public/* \ No newline at end of file diff --git a/client/app/App.tsx b/client/app/App.tsx index 4539ec6e7ab..efafd1321b9 100644 --- a/client/app/App.tsx +++ b/client/app/App.tsx @@ -1,13 +1,11 @@ -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; - import Providers from 'lib/components/wrappers/Providers'; -import router from './router'; +import AuthenticatableApp from './routers/AuthenticatableApp'; import { store } from './store'; const App = (): JSX.Element => ( - + ); diff --git a/client/app/__test__/mocks/ResizeObserver.js b/client/app/__test__/mocks/ResizeObserver.js index d28d97506c9..02b3e8d25ab 100644 --- a/client/app/__test__/mocks/ResizeObserver.js +++ b/client/app/__test__/mocks/ResizeObserver.js @@ -1,6 +1,8 @@ class ResizeObserver { observe() {} + unobserve() {} + disconnect() {} } diff --git a/client/app/__test__/mocks/axiosMock.js b/client/app/__test__/mocks/axiosMock.js new file mode 100644 index 00000000000..321d17169fe --- /dev/null +++ b/client/app/__test__/mocks/axiosMock.js @@ -0,0 +1,18 @@ +import MockAdapter from 'axios-mock-adapter'; + +const registerCSRFTokenMockHandler = (mock) => { + mock.onGet('/csrf_token').reply(200, { csrfToken: 'mock_csrf_token' }); +}; + +export const createMockAdapter = (instance) => { + const mock = new MockAdapter(instance); + registerCSRFTokenMockHandler(mock); + + return Object.assign(mock, { + reset: () => { + mock.resetHandlers(); + mock.resetHistory(); + registerCSRFTokenMockHandler(mock); + }, + }); +}; diff --git a/client/app/__test__/setup.js b/client/app/__test__/setup.js index 641d603e766..f34b92f8144 100644 --- a/client/app/__test__/setup.js +++ b/client/app/__test__/setup.js @@ -51,9 +51,6 @@ global.$ = jQuery; global.jQuery = jQuery; global.buildContextOptions = buildContextOptions; -// Global mocks -document.head.innerHTML = ``; - window.history.pushState({}, '', `/courses/${courseId}`); // Global helper functions diff --git a/client/app/api/Attachments.js b/client/app/api/Attachments.js deleted file mode 100644 index cc2eb06c0d0..00000000000 --- a/client/app/api/Attachments.js +++ /dev/null @@ -1,15 +0,0 @@ -import BaseAPI from './Base'; - -class AttachmentsAPI extends BaseAPI { - delete(attachmentId) { - return this.client.delete(`${AttachmentsAPI.#urlPrefix}/${attachmentId}`); - } - - static get #urlPrefix() { - return '/attachments'; - } -} - -const attachmentsAPI = new AttachmentsAPI(); - -export default attachmentsAPI; diff --git a/client/app/api/Attachments.ts b/client/app/api/Attachments.ts new file mode 100644 index 00000000000..2da70835bba --- /dev/null +++ b/client/app/api/Attachments.ts @@ -0,0 +1,23 @@ +import BaseAPI from './Base'; +import { APIResponse } from './types'; + +class AttachmentsAPI extends BaseAPI { + #urlPrefix = '/attachments'; + + create(file: File): APIResponse<{ success: boolean; id?: number }> { + const formData = new FormData(); + + formData.append('file', file); + formData.append('name', file.name); + + return this.client.post(this.#urlPrefix, formData); + } + + delete(attachmentId: number): APIResponse { + return this.client.delete(`${this.#urlPrefix}/${attachmentId}`); + } +} + +const attachmentsAPI = new AttachmentsAPI(); + +export default attachmentsAPI; diff --git a/client/app/api/Base.js b/client/app/api/Base.js deleted file mode 100644 index ec18fc7240b..00000000000 --- a/client/app/api/Base.js +++ /dev/null @@ -1,22 +0,0 @@ -import axios from 'axios'; - -import { csrfToken } from 'lib/helpers/server-context'; - -export default class BaseAPI { - #client; - - constructor() { - this.#client = null; - } - - /** Returns the API client */ - get client() { - if (this.#client) return this.#client; - - const headers = { Accept: 'application/json', 'X-CSRF-Token': csrfToken }; - const params = { format: 'json' }; - - this.#client = axios.create({ headers, params }); - return this.#client; - } -} diff --git a/client/app/api/Base.ts b/client/app/api/Base.ts new file mode 100644 index 00000000000..afd46af6db3 --- /dev/null +++ b/client/app/api/Base.ts @@ -0,0 +1,96 @@ +import axios, { AxiosInstance, AxiosResponse } from 'axios'; + +import { + redirectToForbidden, + redirectToNotFound, + redirectToSignIn, +} from 'lib/hooks/router/redirect'; + +const isInvalidCSRFTokenResponse = (response?: AxiosResponse): boolean => + response?.status === 403 && + response.data?.error?.title?.toLowerCase().includes('csrf token'); + +const isUnauthenticatedResponse = (response?: AxiosResponse): boolean => + response?.status === 401 && + response.data?.error?.toLowerCase().includes('sign in or sign up'); + +const isUnauthorizedResponse = (response?: AxiosResponse): boolean => + response?.status === 403 && + response.data?.errors?.toLowerCase().includes('not authorized'); + +const isComponentNotFoundResponse = (response?: AxiosResponse): boolean => + response?.status === 404 && + response.data?.error?.toLowerCase().includes('component not found'); + +const redirectIfMatchesErrorIn = (response?: AxiosResponse): void => { + if (isUnauthenticatedResponse(response)) redirectToSignIn(true); + if (isUnauthorizedResponse(response)) redirectToForbidden(); + if (isComponentNotFoundResponse(response)) redirectToNotFound(); +}; + +const MAX_CSRF_RETRIES = 3; + +export default class BaseAPI { + #client: AxiosInstance | null = null; + + #retries = 0; + + /** Returns the API client */ + get client(): AxiosInstance { + this.#client ??= this.#createAxiosInstance(); + return this.#client; + } + + #createAxiosInstance(): AxiosInstance { + const client = axios.create({ + headers: { Accept: 'application/json' }, + params: { format: 'json' }, + }); + + client.interceptors.request.use(async (config) => { + config.withCredentials = true; + if (config.method === 'get') return config; + + config.headers['X-CSRF-Token'] = await this.#getAndSaveCSRFToken(); + return config; + }); + + client.interceptors.response.use( + (response) => { + if (response.config.method !== 'get') this.#retries = 0; + + return response; + }, + async (error) => { + if ( + isInvalidCSRFTokenResponse(error.response) && + this.#retries < MAX_CSRF_RETRIES + ) { + BaseAPI.#clearCSRFToken(); + this.#retries += 1; + return client.request(error.config); + } + + redirectIfMatchesErrorIn(error.response); + + return Promise.reject(error); + }, + ); + + return client; + } + + static #clearCSRFToken(): void { + window._CSRF_TOKEN = undefined; + } + + async #getAndSaveCSRFToken(): Promise { + window._CSRF_TOKEN ??= await this.#getCSRFToken(); + return window._CSRF_TOKEN; + } + + async #getCSRFToken(): Promise { + const response = await this.#client?.get('/csrf_token'); + return response?.data.csrfToken; + } +} diff --git a/client/app/api/Users.ts b/client/app/api/Users.ts index c12ee2c36a8..39bde343183 100644 --- a/client/app/api/Users.ts +++ b/client/app/api/Users.ts @@ -4,6 +4,7 @@ import { EmailData, EmailPostData, EmailsData, + InvitedSignUpData, PasswordPostData, ProfileData, ProfilePostData, @@ -72,13 +73,94 @@ export default class UsersAPI extends BaseAPI { return this.client.post(url); } - resendConfirmationEmail( + resendConfirmationEmailByURL( url: NonNullable, ): APIResponse { return this.client.post(url); } - signOut(url: string): APIResponse { - return this.client.delete(url); + masquerade(url: string): APIResponse { + return this.client.get(url); + } + + stopMasquerade(url: string): APIResponse { + return this.client.get(url); + } + + signOut(): APIResponse { + return this.client.delete(`${this.#urlPrefix}/sign_out`); + } + + signIn(email: string, password: string, rememberMe: boolean): APIResponse { + const formData = new FormData(); + + formData.append('user[email]', email); + formData.append('user[password]', password); + formData.append('user[remember_me]', rememberMe ? '1' : '0'); + + return this.client.post(`${this.#urlPrefix}/sign_in`, formData); + } + + signUp( + name: string, + email: string, + password: string, + captchaResponse: string, + invitation?: string, + ): APIResponse<{ id: number | null; confirmed: boolean }> { + const formData = new FormData(); + + formData.append('user[name]', name); + formData.append('user[email]', email); + formData.append('user[password]', password); + formData.append('user[password_confirmation]', password); + formData.append('g-recaptcha-response', captchaResponse); + if (invitation) formData.append('invitation', invitation); + + return this.client.post(this.#urlPrefix, formData); + } + + verifyInvitationToken(token: string): APIResponse { + return this.client.get(`${this.#urlPrefix}/sign_up`, { + params: { invitation: token }, + }); + } + + requestResetPassword(email: string): APIResponse { + const formData = new FormData(); + + formData.append('user[email]', email); + + return this.client.post(`${this.#urlPrefix}/password`, formData); + } + + resendConfirmationEmail(email: string): APIResponse { + const formData = new FormData(); + + formData.append('user[email]', email); + + return this.client.post(`${this.#urlPrefix}/confirmation`, formData); + } + + verifyResetPasswordToken(token: string): APIResponse<{ email: string }> { + return this.client.get(`${this.#urlPrefix}/password/edit`, { + params: { reset_password_token: token }, + }); + } + + resetPassword(token: string, password: string): APIResponse { + const formData = new FormData(); + + formData.append('user[reset_password_token]', token); + formData.append('user[password]', password); + formData.append('user[password_confirmation]', password); + + return this.client.patch(`${this.#urlPrefix}/password`, formData); + } + + confirmEmail(token: string): APIResponse<{ email: string }> { + return this.client.get(`${this.#urlPrefix}/confirmation`, { + params: { confirmation_token: token }, + }); } } diff --git a/client/app/api/course/Achievements.ts b/client/app/api/course/Achievements.ts index e459966dab6..843755306d0 100644 --- a/client/app/api/course/Achievements.ts +++ b/client/app/api/course/Achievements.ts @@ -6,6 +6,8 @@ import { AchievementPermissions, } from 'types/course/achievements'; +import { APIResponse } from 'api/types'; + import BaseCourseAPI from './Base'; export default class AchievementsAPI extends BaseCourseAPI { @@ -16,40 +18,27 @@ export default class AchievementsAPI extends BaseCourseAPI { /** * Fetches a list of achievements in a course. */ - index(): Promise< - AxiosResponse<{ - achievements: AchievementListData[]; - permissions: AchievementPermissions; - }> - > { + index(): APIResponse<{ + achievements: AchievementListData[]; + permissions: AchievementPermissions; + }> { return this.client.get(this.#urlPrefix); } /** * Fetches an achievement. */ - fetch(achievementId: number): Promise< - AxiosResponse<{ - achievement: AchievementData; - }> - > { - return this.client.get(`${this.#urlPrefix}/${achievementId}`); + fetch(id: number): APIResponse<{ achievement: AchievementData }> { + return this.client.get(`${this.#urlPrefix}/${id}`); } /** * Fetches course users related to an achievement. - * - * @param {number} achievementId - * @return {Promise} */ - fetchAchievementCourseUsers(achievementId: number): Promise< - AxiosResponse<{ - achievementCourseUsers: AchievementCourseUserData[]; - }> - > { - return this.client.get( - `${this.#urlPrefix}/${achievementId}/achievement_course_users`, - ); + fetchAchievementCourseUsers(id: number): APIResponse<{ + achievementCourseUsers: AchievementCourseUserData[]; + }> { + return this.client.get(`${this.#urlPrefix}/${id}/achievement_course_users`); } /** @@ -59,43 +48,34 @@ export default class AchievementsAPI extends BaseCourseAPI { * { * achievement: { :title, :description, etc } * } - * @return {Promise} - * success response: { :id } - ID of created achievement. - * error response: { errors: [] } - An array of errors will be returned upon validation error. */ - create(params: FormData): Promise< - AxiosResponse<{ - id: number; - }> - > { + create(params: FormData): APIResponse<{ id: number }> { return this.client.post(this.#urlPrefix, params); } /** * Updates the achievement. * - * @param {number} achievementId + * @param {number} id * @param {object} params - params in the format of { achievement: { :title, :description, etc } } - * @return {Promise} - * success response: {} - * error response: { errors: [] } - An array of errors will be returned upon validation error. */ update( - achievementId: number, + id: number, params: FormData | object, - ): Promise { - return this.client.patch(`${this.#urlPrefix}/${achievementId}`, params); + ): APIResponse<{ achievement: AchievementData }> { + return this.client.patch(`${this.#urlPrefix}/${id}`, params); } /** * Deletes an achievement. * * @param {number} achievementId - * @return {Promise} - * success response: {} - * error response: {} */ delete(achievementId: number): Promise { return this.client.delete(`${this.#urlPrefix}/${achievementId}`); } + + reorder(ordering: string): APIResponse { + return this.client.post(`${this.#urlPrefix}/reorder`, ordering); + } } diff --git a/client/app/api/course/Assessment/Assessments.js b/client/app/api/course/Assessment/Assessments.js index 72651c9f30d..11cd56516c5 100644 --- a/client/app/api/course/Assessment/Assessments.js +++ b/client/app/api/course/Assessment/Assessments.js @@ -79,12 +79,10 @@ export default class AssessmentsAPI extends BaseCourseAPI { } /** - * Create an assessment attempt. + * Creates an assessment attempt. * * @param {number} assessmentId - * @return {Promise} - * success response: { redirectUrl: string } - * error response: { error: string } + * @returns {import('api/types').APIResponse} */ attempt(assessmentId) { return this.client.get(`${this.#urlPrefix}/${assessmentId}/attempt`); diff --git a/client/app/api/course/Materials.js b/client/app/api/course/Materials.js index 537d67bce04..755bcbbc9b7 100644 --- a/client/app/api/course/Materials.js +++ b/client/app/api/course/Materials.js @@ -1,6 +1,21 @@ import BaseCourseAPI from './Base'; export default class MaterialsAPI extends BaseCourseAPI { + get #urlPrefix() { + return `/courses/${this.courseId}/materials/folders`; + } + + /** + * @param {number} folderId + * @param {number} materialId + * @returns {import('api/types').APIResponse} + */ + fetch(folderId, materialId) { + return this.client.get( + `${this.#urlPrefix}/${folderId}/files/${materialId}`, + ); + } + /** * Destroy the material. * @@ -15,8 +30,4 @@ export default class MaterialsAPI extends BaseCourseAPI { `${this.#urlPrefix}/${folderId}/files/${materialId}`, ); } - - get #urlPrefix() { - return `/courses/${this.courseId}/materials/folders`; - } } diff --git a/client/app/api/course/UserNotifications.ts b/client/app/api/course/UserNotifications.ts index 2b88f36f79a..21729341b19 100644 --- a/client/app/api/course/UserNotifications.ts +++ b/client/app/api/course/UserNotifications.ts @@ -9,7 +9,7 @@ export default class UserNotificationsAPI extends BaseCourseAPI { return `/courses/${this.courseId}/user_notifications`; } - fetch(): APIResponse { + fetch(): APIResponse { return this.client.get(`${this.#urlPrefix}/fetch`); } diff --git a/client/app/api/course/Video/Submissions.ts b/client/app/api/course/Video/Submissions.ts index 714c4026936..522109d670a 100644 --- a/client/app/api/course/Video/Submissions.ts +++ b/client/app/api/course/Video/Submissions.ts @@ -1,10 +1,12 @@ -import { AxiosResponse } from 'axios'; import { VideoEditSubmissionData, VideoSubmission, + VideoSubmissionAttemptData, VideoSubmissionData, } from 'types/course/video/submissions'; +import { APIResponse } from 'api/types'; + import BaseVideoAPI from './Base'; export default class SubmissionsAPI extends BaseVideoAPI { @@ -16,28 +18,39 @@ export default class SubmissionsAPI extends BaseVideoAPI { /** * Fetches a list of video submissions for a video in a course. */ - index(): Promise> { + index(): APIResponse { return this.client.get(this.#getUrlPrefix()); } /** * Fetch video submission in a course. */ - fetch(submissionId): Promise> { + fetch(submissionId: number): APIResponse { return this.client.get(`${this.#getUrlPrefix()}/${submissionId}`); } /** * Create a video submission in a course. */ - create(videoId: number): Promise> { + create(videoId: number): APIResponse { return this.client.post(`${this.#getUrlPrefix(videoId)}`); } /** * Fetch edit video submission in a course. */ - edit(submissionId): Promise> { + edit(submissionId: number): APIResponse { return this.client.get(`${this.#getUrlPrefix()}/${submissionId}/edit`); } + + /** + * Programmatically attempts to watch a video and get the submission URL. + * Created as a compatibility method for `NextVideoButton`. + * + * @param url URL in the form of `courses/:id/videos/:id/attempt` + * @returns + */ + attempt(url: string): APIResponse { + return this.client.get(url); + } } diff --git a/client/app/assets/error-illustration.svg b/client/app/assets/error-illustration.svg new file mode 100644 index 00000000000..4de41f744fa --- /dev/null +++ b/client/app/assets/error-illustration.svg @@ -0,0 +1,297 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/app/assets/forbidden-illustration.svg b/client/app/assets/forbidden-illustration.svg new file mode 100644 index 00000000000..7f44b780b73 --- /dev/null +++ b/client/app/assets/forbidden-illustration.svg @@ -0,0 +1,943 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/app/assets/not-found-illustration.svg b/client/app/assets/not-found-illustration.svg new file mode 100644 index 00000000000..ac77e60572b --- /dev/null +++ b/client/app/assets/not-found-illustration.svg @@ -0,0 +1,453 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/app/bundles/announcements/GlobalAnnouncementIndex.tsx b/client/app/bundles/announcements/GlobalAnnouncementIndex.tsx index 0006d00221c..7cca36a8b31 100644 --- a/client/app/bundles/announcements/GlobalAnnouncementIndex.tsx +++ b/client/app/bundles/announcements/GlobalAnnouncementIndex.tsx @@ -1,11 +1,11 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import AnnouncementsDisplay from 'bundles/course/announcements/components/misc/AnnouncementsDisplay'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { indexAnnouncements } from './operations'; import { getAllAnnouncementMiniEntities } from './selectors'; diff --git a/client/app/bundles/common/DashboardPage.tsx b/client/app/bundles/common/DashboardPage.tsx new file mode 100644 index 00000000000..a4598acdf12 --- /dev/null +++ b/client/app/bundles/common/DashboardPage.tsx @@ -0,0 +1,130 @@ +import { defineMessages } from 'react-intl'; +import { Navigate } from 'react-router-dom'; +import { ArrowForward } from '@mui/icons-material'; +import { Avatar, Typography } from '@mui/material'; +import moment from 'moment'; +import { HomeLayoutCourseData } from 'types/home'; + +import SearchField from 'lib/components/core/fields/SearchField'; +import Page from 'lib/components/core/layouts/Page'; +import Link from 'lib/components/core/Link'; +import { useAppContext } from 'lib/containers/AppContainer'; +import useItems from 'lib/hooks/items/useItems'; +import useTranslation from 'lib/hooks/useTranslation'; + +const translations = defineMessages({ + searchCourses: { + id: 'lib.components.navigation.CourseSwitcherPopupMenu.searchCourses', + defaultMessage: 'Search your courses', + }, + jumpBackIn: { + id: 'lib.components.navigation.CourseSwitcherPopupMenu.jumpBackIn', + defaultMessage: 'Jump back in', + }, + lastAccessed: { + id: 'lib.components.navigation.CourseSwitcherPopupMenu.lastAccessed', + defaultMessage: 'Last accessed {at}', + }, + noCoursesMatch: { + id: 'lib.components.navigation.CourseSwitcherPopupMenu.noCoursesMatch', + defaultMessage: 'Oops, no courses matched your search keyword.', + }, +}); + +interface CourseListItemProps { + course: HomeLayoutCourseData; +} + +const CourseListItem = (props: CourseListItemProps): JSX.Element => { + const { course } = props; + + const { t } = useTranslation(); + + return ( + +
+ + +
+ {course.title} + + {course.lastActiveAt && ( + + {t(translations.lastAccessed, { + at: moment(course.lastActiveAt).fromNow(), + })} + + )} +
+
+ + + + ); +}; + +const DashboardPage = (): JSX.Element => { + const { courses } = useAppContext(); + + const { t } = useTranslation(); + + const { processedItems: filteredCourses, handleSearch } = useItems( + courses ?? [], + ['title'], + (rawCourses) => + rawCourses?.slice().sort((a, b) => { + return moment(b.lastActiveAt).diff(moment(a.lastActiveAt)); + }), + courses?.length ?? 0, + ); + + return ( + + + {t(translations.jumpBackIn)} + + + + + {Boolean(courses?.length) && ( +
+ {filteredCourses?.map((course) => ( + + ))} + + {!filteredCourses?.length && ( + + {t(translations.noCoursesMatch)} + + )} +
+ )} +
+ ); +}; + +const DashboardPageRedirects = (): JSX.Element => { + const { courses } = useAppContext(); + + if (!courses?.length) return ; + + if (courses?.length === 1) return ; + + return ; +}; + +export default DashboardPageRedirects; diff --git a/client/app/bundles/common/ErrorPage.tsx b/client/app/bundles/common/ErrorPage.tsx new file mode 100644 index 00000000000..c1deb69761b --- /dev/null +++ b/client/app/bundles/common/ErrorPage.tsx @@ -0,0 +1,216 @@ +import { ReactNode } from 'react'; +import { defineMessages } from 'react-intl'; +import { LoaderFunction, redirect, useLoaderData } from 'react-router-dom'; +import { Typography } from '@mui/material'; +import forbiddenIllustration from 'assets/forbidden-illustration.svg?url'; +import notFoundIllustration from 'assets/not-found-illustration.svg?url'; + +import Page from 'lib/components/core/layouts/Page'; +import Link from 'lib/components/core/Link'; +import { + Attributions, + useSetAttributions, +} from 'lib/components/wrappers/AttributionsProvider'; +import { getForbiddenSourceURL } from 'lib/hooks/router/redirect'; +import useEffectOnce from 'lib/hooks/useEffectOnce'; +import useTranslation from 'lib/hooks/useTranslation'; + +const translations = defineMessages({ + notFound: { + id: 'app.ErrorPage.notFound', + defaultMessage: "That location doesn't exist in this universe...", + }, + notFoundSubtitle: { + id: 'app.ErrorPage.notFoundSubtitle', + defaultMessage: + "Check if you've typed the correct address, try again later, or go back home.", + }, + notFoundIllustrationAttribution: { + id: 'app.ErrorPage.notFoundIllustrationAttribution', + defaultMessage: + 'Graphic of a dog floating in space is created by Storyset from ' + + 'www.storyset.com, with modifications.', + }, + forbidden: { + id: 'app.ErrorPage.forbidden', + defaultMessage: 'Hold up, this galaxy is off-limits to you!', + }, + forbiddenSubtitle: { + id: 'app.ErrorPage.forbiddenSubtitle', + defaultMessage: + "You don't have permission to access the information behind this page. If you believe this is a mistake, " + + 'contact your administrator.', + }, + forbiddenIllustrationAttribution: { + id: 'app.ErrorPage.forbiddenIllustrationAttribution', + defaultMessage: + 'Graphic of an astronaut floating in space is created by Storyset from ' + + 'www.storyset.com, with modifications. Otherwise, go back.', + }, + error: { + id: 'app.ErrorPage.error', + defaultMessage: 'KABOOM, a meteor has just crashed.', + }, + errorSubtitle: { + id: 'app.ErrorPage.errorSubtitle', + defaultMessage: + 'A fatal error has occurred. You may try again later. If the problem persists, contact us.', + }, + errorIllustrationAttribution1: { + id: 'app.ErrorPage.errorIllustrationAttribution1', + defaultMessage: + 'Graphic of a planet earth in space is created by Storyset from ' + + 'www.storyset.com, with modifications. Otherwise, go back.', + }, + errorIllustrationAttribution2: { + id: 'app.ErrorPage.errorIllustrationAttribution2', + defaultMessage: + 'Graphic of a fire ball is created by Storyset from ' + + 'www.storyset.com, with modifications. Otherwise, go back.', + }, +}); + +interface ErrorPageProps { + illustrationSrc: string; + illustrationAlt: string; + title: ReactNode; + subtitle: ReactNode; + attributions?: Attributions; + tip?: ReactNode | false; + children?: ReactNode; +} + +const ErrorPage = (props: ErrorPageProps): JSX.Element => { + useSetAttributions(props.attributions); + + return ( + + {props.illustrationAlt} + + {props.tip !== false && ( + + {props.tip ?? window.location.pathname} + + )} + + + {props.title} + + + + {props.subtitle} + + + {props.children} + + ); +}; + +const NotFoundPage = (): JSX.Element => { + const { t } = useTranslation(); + + return ( + ( + + {chunk} + + ), + source: (chunk) => ( + + {chunk} + + ), + }), + }, + ]} + illustrationAlt="Not found illustration" + illustrationSrc={notFoundIllustration} + subtitle={t(translations.notFoundSubtitle, { + home: (chunk) => ( + + {chunk} + + ), + })} + title={t(translations.notFound)} + /> + ); +}; + +const ForbiddenPage = (): JSX.Element => { + const { t } = useTranslation(); + + const sourceURL = useLoaderData() as string | null; + + useEffectOnce(() => { + if (sourceURL) window.history.replaceState(null, '', sourceURL); + }); + + return ( + ( + + {chunk} + + ), + source: (chunk) => ( + + {chunk} + + ), + }), + }, + ]} + illustrationAlt="Forbidden illustration" + illustrationSrc={forbiddenIllustration} + subtitle={t(translations.forbiddenSubtitle)} + tip={sourceURL} + title={t(translations.forbidden)} + /> + ); +}; + +const forbiddenPageLoader: LoaderFunction = async ({ request }) => { + const sourceURL = getForbiddenSourceURL(request.url); + if (!sourceURL) return redirect('/'); + + return sourceURL; +}; + +export default { + NotFound: NotFoundPage, + Forbidden: Object.assign(ForbiddenPage, { loader: forbiddenPageLoader }), +}; diff --git a/client/app/bundles/common/LandingPage.tsx b/client/app/bundles/common/LandingPage.tsx new file mode 100644 index 00000000000..3e664d724e3 --- /dev/null +++ b/client/app/bundles/common/LandingPage.tsx @@ -0,0 +1,67 @@ +import { defineMessages } from 'react-intl'; +import { Button, Typography } from '@mui/material'; + +import Page from 'lib/components/core/layouts/Page'; +import Link from 'lib/components/core/Link'; +import useTranslation from 'lib/hooks/useTranslation'; + +const translations = defineMessages({ + signInToCoursemology: { + id: 'landing_page.sign_in_to_coursemology', + defaultMessage: 'Sign in to Coursemology', + }, + createAnAccount: { + id: 'landing_page.create_an_account', + defaultMessage: 'Create an account', + }, + newToCoursemology: { + id: 'landing_page.new_to_coursemology', + defaultMessage: 'New to Coursemology?', + }, + title: { + id: 'landing_page.title', + defaultMessage: 'Making your class a world of games in a universe of fun.', + }, + subtitle: { + id: 'landing_page.subtitle', + defaultMessage: + 'Coursemology adds fun elements, such as experience points, levels, and achievements to your classroom. ' + + 'These gamification elements motivate students to power through lessons and their assignments.', + }, +}); + +const LandingPage = (): JSX.Element => { + const { t } = useTranslation(); + + return ( + + + {t(translations.title)} + + + + {t(translations.subtitle)} + + + + + + + + {t(translations.newToCoursemology)} + + + + + + + ); +}; + +export default LandingPage; diff --git a/client/app/bundles/common/PrivacyPolicyPage/PrivacyPolicyPage.tsx b/client/app/bundles/common/PrivacyPolicyPage/PrivacyPolicyPage.tsx new file mode 100644 index 00000000000..4d655be2330 --- /dev/null +++ b/client/app/bundles/common/PrivacyPolicyPage/PrivacyPolicyPage.tsx @@ -0,0 +1,9 @@ +import MarkdownPage from 'lib/components/core/layouts/MarkdownPage'; + +import privacyPolicy from './privacy-policy.md'; + +const PrivacyPolicyPage = (): JSX.Element => ( + +); + +export default PrivacyPolicyPage; diff --git a/client/app/bundles/common/PrivacyPolicyPage/index.tsx b/client/app/bundles/common/PrivacyPolicyPage/index.tsx new file mode 100644 index 00000000000..58ce9822021 --- /dev/null +++ b/client/app/bundles/common/PrivacyPolicyPage/index.tsx @@ -0,0 +1,24 @@ +import { lazy, Suspense } from 'react'; +import { defineMessages } from 'react-intl'; + +const PrivacyPolicyPage = lazy( + () => + import(/* webpackChunkName: "PrivacyPolicyPage" */ './PrivacyPolicyPage'), +); + +const translations = defineMessages({ + privacyPolicy: { + id: 'app.PrivacyPolicyPage.privacyPolicy', + defaultMessage: 'Privacy Policy', + }, +}); + +const SuspensedPrivacyPolicyPage = (): JSX.Element => ( + + + +); + +const handle = translations.privacyPolicy; + +export default Object.assign(SuspensedPrivacyPolicyPage, { handle }); diff --git a/client/app/bundles/common/PrivacyPolicyPage/privacy-policy.md b/client/app/bundles/common/PrivacyPolicyPage/privacy-policy.md new file mode 100644 index 00000000000..9ea3de0ad66 --- /dev/null +++ b/client/app/bundles/common/PrivacyPolicyPage/privacy-policy.md @@ -0,0 +1,38 @@ +## Privacy Policy + +Effective 24 May 2022. + +This privacy policy sets out how Coursemology uses and protects any information that you give Coursemology when you use this website. Coursemology is committed to ensuring that your privacy is protected. Should we ask you to provide certain information by which you can be identified when using this website, then you can be assured that it will only be used in accordance with this privacy statement. Coursemology may change this policy from time to time by updating this page. You should check this page from time to time to ensure that you are happy with any changes. + +### What we collect + +We may collect the following information: + +- Name +- Contact information including email address +- IP address + +### What we do with the information we gather + +We require this information to understand your needs and provide you with a better service, and in particular for the following reasons: + +- Internal record keeping. +- We may use the information to improve our services. +- We may send emails about new courses, notification of your enrolled courses. +- From time to time, we may also use your information to contact you for research purposes. We may contact you by email. We may use the information to customise the website according to your interests. + +### Security + +We are committed to ensuring that your information is secure. In order to prevent unauthorised access or disclosure we have put in place suitable physical, electronic and managerial procedures to safeguard and secure the information we collect online. + +### How we use cookies + +A cookie is a small file which asks permission to be placed on your computer’s hard drive. Once you agree, the file is added and the cookie helps analyse web traffic or lets you know when you visit a particular site. + +Cookies allow web applications to respond to you as an individual. The web application can tailor its operations to your needs, likes and dislikes by gathering and remembering information about your preferences. We use traffic log cookies to identify which pages are being used. This helps us analyse data about webpage traffic and improve our website in order to tailor it to customer needs. + +We only use this information for statistical analysis purposes and then the data is removed from the system. Overall, cookies help us provide you with a better website, by enabling us to monitor which pages you find useful and which you do not. A cookie in no way gives us access to your computer or any information about you, other than the data you choose to share with us. You can choose to accept or decline cookies. Most web browsers automatically accept cookies, but you can usually modify your browser setting to decline cookies if you prefer. This may prevent you from taking full advantage of the website. + +### Controlling your personal information + +We will not sell, distribute or lease your personal information to third parties. diff --git a/client/app/bundles/common/TermsOfServicePage/TermsOfServicePage.tsx b/client/app/bundles/common/TermsOfServicePage/TermsOfServicePage.tsx new file mode 100644 index 00000000000..938510a6efb --- /dev/null +++ b/client/app/bundles/common/TermsOfServicePage/TermsOfServicePage.tsx @@ -0,0 +1,9 @@ +import MarkdownPage from 'lib/components/core/layouts/MarkdownPage'; + +import termsOfService from './terms-of-service.md'; + +const TermsOfServicePage = (): JSX.Element => ( + +); + +export default TermsOfServicePage; diff --git a/client/app/bundles/common/TermsOfServicePage/index.tsx b/client/app/bundles/common/TermsOfServicePage/index.tsx new file mode 100644 index 00000000000..594e2c2c111 --- /dev/null +++ b/client/app/bundles/common/TermsOfServicePage/index.tsx @@ -0,0 +1,24 @@ +import { lazy, Suspense } from 'react'; +import { defineMessages } from 'react-intl'; + +const TermsOfServicePage = lazy( + () => + import(/* webpackChunkName: "TermsOfServicePage" */ './TermsOfServicePage'), +); + +const translations = defineMessages({ + termsOfService: { + id: 'app.TermsOfServicePage.termsOfService', + defaultMessage: 'Terms of Service', + }, +}); + +const SuspensedTermsOfServicePage = (): JSX.Element => ( + + + +); + +const handle = translations.termsOfService; + +export default Object.assign(SuspensedTermsOfServicePage, { handle }); diff --git a/client/app/bundles/common/TermsOfServicePage/terms-of-service.md b/client/app/bundles/common/TermsOfServicePage/terms-of-service.md new file mode 100644 index 00000000000..213fefa886e --- /dev/null +++ b/client/app/bundles/common/TermsOfServicePage/terms-of-service.md @@ -0,0 +1,114 @@ +## Terms of Service + +Effective 12 July 2023. + +**PLEASE READ THIS TERMS OF SERVICE AGREEMENT (THE "TERMS OF SERVICE") CAREFULLY BEFORE ACCESSING OR PARTICIPATING IN ANY CHATROOM, NEWSGROUP, BULLETIN BOARD, MAILING LIST, WEBSITE, TRANSACTION OR OTHER ON-LINE FORUM, COURSE, OR SERVICE MADE AVAILABLE BY Coursemology.org. (“Coursemology") AT ENTRY-POINT URL (http://www.coursemology.org) AND ITS RELATED WEBSITES ("SITE" OR "SITES"). BY USING AND PARTICIPATING IN THE SITES, YOU SIGNIFY AND ACKNOWLEDGE THAT YOU HAVE READ THE TERMS OF SERVICE AND AGREE THAT THE TERMS OF SERVICE CONSTITUTES A BINDING LEGAL AGREEMENT BETWEEN YOU AND Coursemology, AND THAT YOU AGREE TO BE BOUND BY AND COMPLY WITH THE TERMS OF SERVICE. IF YOU DO NOT AGREE TO BE BOUND BY THE TERMS OF SERVICE, PLEASE DO NOT ACCESS THE SITES. THE PARTICIPATING INSTITUTIONS ARE THIRD PARTY BENEFICIARIES OF THE AGREEMENT AND MAY ENFORCE THOSE PROVISIONS BELOW THAT RELATE TO THE PARTICIPATING INSTITUTIONS.** + +### Age Restrictions + +Registration and participation on the Sites is restricted to those individuals over 18 years of age, emancipated minors, or those who possess legal parental or guardian consent, and are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties herein. By registering or participating in services or functions on the Sites, you hereby represent that you are over 18 years of age, an emancipated minor or in possession of consent by a legal parent or guardian and have the authority to enter into the terms herein. In any case, you affirm that you are over the age of 12 as the Site is not intended for children under 12. If you are under 12 years of age, do not use this site. In addition, those who wish to register and participate must meet the minimum requirements laid out in the Terms of Service (this document) and abide by the Honor Code herein. In addition, certain Courses may have additional eligibility requirements, as specified on the Course website. If you do not qualify or do not agree to these terms, you may not use the Site. + +### Right of Modification + +We reserve the right to change or modify the Terms of Service at our sole discretion at any time. Any change or modification to the Terms of Service will be effective immediately upon posting by us. For any material changes to the Terms, we will take reasonable steps to notify you of such changes. In all cases, your continued use of the Sites after publication of such modifications, with or without notification, constitutes binding acceptance of these modified Terms of Service. + +### Disclaimer + +Sites may include forums containing the personal opinions and other expressions of the persons who post entries on a wide range of topics. Neither the User Content (as defined below) on these Sites, nor any links to other websites, are screened, moderated, approved, reviewed or endorsed by Coursemology or its participating institutions. By posting to or viewing such forums, you agree that Coursemology and any of its participating institutions are not responsible or liable for the content of any postings therein. Coursemology reserves the right (but not the obligation) to remove any content from such forums in its discretion. + +### Rules for Online Conduct + +You agree to use the Sites in accordance with all applicable laws. Further, you agree that you will not use the Site for organized partisan political activities. You further agree that you will not e-mail or post any of the following content (“Prohibited Content”) anywhere on the Site, or on any other Coursemology computing resources: + +- Content that defames, harasses or threatens others +- Content that discusses illegal activities with the intent to commit such activities, or encourages others to commit such activities +- Content that infringes or misappropriates another's intellectual property rights, including, but not limited to, copyrights, trademarks or trade secrets +- Content that you do not have the right to disclose under contractual confidentiality obligations or fiduciary duties +- Material that contains obscene (i.e., pornographic) language or images +- Advertising, promotional materials, or any form of commercial solicitation +- Content that otherwise harms other users or visitors to the Sites +- Content that is otherwise unlawful or that violates any applicable local, state, national or international law. +- Content that probes, scans, or tests the vulnerability of any system or network +- Content that breaches or otherwise circumvents any security measures +- Content that interferes with or disrupts any user, host, or network, for example by sending a virus, overloading, flooding, spamming, or mail-bombing any other user or part of the Sites +- Content that plants malware or otherwise uses the Sites to distribute malware + +Although Coursemology does not routinely screen or monitor content posted by users to the Site, Coursemology reserves the right to remove Prohibited Content of which it becomes aware, but is under no obligation to do so. + +Copyrighted material, including without limitation software, graphics, text, photographs, sound, video and musical recordings, may not be placed on the Site without the express permission of the owner of the copyright in the material, or other legal entitlement to use the material. + +In addition, as a condition of accessing the Sites, you agree not to (a) reproduce, duplicate, copy, sell, resell or exploit any portion of the Sites other than as expressly allowed under these Terms of Service; (b) use Coursemology’s or any Participating Institution's name, trademarks, server or other materials in connection with, or to transmit, any unsolicited communications or emails; (c) use any high-volume, automated or electronic means to access the Sites (including without limitation, robots, spiders, scripts or web-scraping tools); (d) frame the Sites, place pop-up windows over its pages or otherwise affect the display of its page; or (e) interfere with or disrupt the Sites or servers or networks connected to the Sites, or disobey any requirements, procedures, policies or regulations of networks connected to the Sites. + +Finally, you agree that you will not access or attempt to access any other user's account, or misrepresent or attempt to misrepresent your identity while using the Sites. + +### User Accounts + +In order to fully participate in all Site activities, you must register for a personal account on the Site (a “User Account”) by providing an email address and a password for your User Account. You agree that you will never divulge or share access or access information to your User Account with any third party for any reason. You also agree to that you will create, use, and access only one User Account, and that you will not access the Site using multiple User Accounts. + +In setting up your User Account, you may be prompted or required to enter additional information, including but not limited to your name and location. Additional information may be required to confirm your identity. You represent that all information provided by you is accurate, current and complete and you agree that you will maintain and update your information to keep it accurate, current and complete. You acknowledge that if any information provided by you is untrue, inaccurate, not current or incomplete, we reserve the right to terminate your use of the Sites. + +### Privacy Policy + +You understand that any personal information you submit to Coursemology on the Sites will be treated by Coursemology in the manner described in the Privacy Policy. + +### Online Course and Certifications + +The Sites will, from time to time, offer online courses in a specific area of study or on a particular topic (an “Online Course”). Coursemology and the instructors of the Online Courses reserve the right to cancel, interrupt or reschedule any Online Course or modify its content as well as the point value or weight of any assignment, exam or other evaluation of progress. Online Courses offered are subject to the Disclaimer of Warranties / Limitation of Liabilities section below. + +For some courses, subject to your satisfactory performance in the Online Course as determined in the sole discretion of the instructors and the Participating Institutions, you may be awarded experience points acknowledging your completion of class components ("EXP"). This EXP, if provided to you, would be from Coursemology and/or from the instructors. You acknowledge that this EXP, if provided to you, may not be affiliated with Coursemology or any college or university. Further, Coursemology offers the right to offer or not offer any such EXP for a class or course component. You acknowledge that EXP, and Coursemology’s Online Courses, will not stand in the place of a course taken at an accredited institution, and do not convey academic credit. You acknowledge that neither the instructors of any Online Course nor the associated Participating Institutions will be involved in any attempts to get the course recognized by any educational or accredited institution, unless explicitly stated otherwise by Coursemology. The format of awarding EXP will be determined at the discretion of Coursemology and the instructors, and may vary by class in terms of formatting, e.g., whether or not it reports your detailed levels or EXP in the class, and in other ways. + +You may not take any Online Course offered by Coursemology or use any EXP as part of any tuition-based or for-credit certification or program for any college, university, or other academic institution without the express written permission from Coursemology. Such use of an Online Course or EXP is a violation of these Terms of Service. + +### Permission to Use Materials + +All content or other materials available on the Sites, including but not limited to code, images, text, layouts, arrangements, displays, illustrations, audio and video clips, HTML files and other content are the property of Coursemology and/or its affiliates or licensors and are protected by copyright, patent and/or other proprietary intellectual property rights under the Singapore and foreign laws. In consideration for your agreement to the terms and conditions contained here, Coursemology grants you a personal, non-exclusive, non-transferable license to access and use the Sites. You may download material from the Sites only for your own personal, non-commercial use. You may not otherwise copy, reproduce, retransmit, distribute, publish, commercially exploit or otherwise transfer any material, nor may you modify or create derivatives works of the material. The burden of determining that your use of any information, software or any other content on the Site is permissible rests with you. + +In connection with your participation in an Online Course, you will have the ability to access or download content or other course-related materials provided by other Users taking the course. While Coursemology requires Users to comply with the Coursemology Terms of Service in providing User Content, Coursemology cannot guarantee that any such User Content will be free of viruses, worms, back doors, Trojan horses or other contaminants which may harm your computer, tablet, hand-held device or any programs or files therein. Coursemology disclaims any responsibility or liability relating to your access or download of such User Content. Accordingly, Coursemology recommends that you only download or access files from a trusted source and implement security measures to scan downloaded files for contaminants. + +### User Material Submission + +The Sites may provide you with the ability to upload certain information, text, or materials, including without limitation, any information, text or materials you post on the Sites’ public forums such as the wiki or the discussion forums (“User Content”). With respect to User Content you submit or otherwise make available in connection with your use of the Site, and subject to the Privacy Policy, you grant Coursemology and the Participating Institutions a fully transferable, worldwide, perpetual, royalty-free and non-exclusive license to use, distribute, sublicense, reproduce, modify, adapt, publicly perform and publicly display such User Content. To the extent that you provide User Content, you represent and warrant to Coursemology and the Participating Institutions that (a) you have all necessary rights, licenses and/or clearances to provide and use User Content and permit Coursemology and the Participating Institutions to use such User Content as provided above; (b) such User Content is accurate and reasonably complete; (c) as between you and Coursemology, you shall be responsible for the payment of any third party fees related to the provision and use of such User Content and (d) such User Content does not and will not infringe or misappropriate any third party rights (including without limitation privacy, publicity, intellectual property and any other proprietary rights, such as copyright, trademark and patent rights) or constitute a fraudulent statement or misrepresentation or unfair business practice. + +The Sites may also provide you with ability to upload or send information to Coursemology regarding the Sites or related services (“Feedback”). By submitting the Feedback, you hereby grant Coursemology and the Participating Institutions an irrevocable license to use, disclose, reproduce, distribute, sublicense, prepare derivative works of, publicly perform and publicly display any such submission. + +### Links to Other Sites + +The Sites may include hyperlinks to sites maintained or controlled by others. Neither Coursemology nor the Participating Institutions are responsible for nor do they routinely screen, approve, review or endorse the contents of or use of any of the products or services that may be offered at these sites. + +### Online Education and Gamification Research + +Records of your participation in Online Courses may be used for researching online education and/or gamification. In the interests of this research, you may be exposed to slight variations in the course materials that will not substantially alter your learning experience. All research findings will be reported at the aggregate level and will not expose your personal identity. + +### Choice of Law/Forum Selection + +Sites are managed by Coursemology, located in Singapore. You agree that any dispute arising out of or relating to these Terms of Service or any content posted to a Site, including copies and republication thereof, whether based in contract, tort, statutory or other law, will be governed by the constitution of the Republic of Singapore. You further consent to the personal jurisdiction of and exclusive venue in the supreme and high courts located in and serving the Republic of Singapore as the legal forum for any such dispute. +Excluding claims for injunctive or other equitable relief, for claims related to the Coursemology Sites where the total amount sought is less than ten thousand Singapore Dollars ($10,000.00 SGD), either Coursemology or You may elect at any point during the dispute to resolve the claim through binding, non-appearance-based arbitration. The dispute will then be resolved using an established alternative dispute resolution ("ADR") provider, mutually agreed upon by You and Coursemology. The parties and the selected ADR provider shall not involve any personal appearance by the parties or witnesses, unless otherwise mutually agreed by the parties; rather, the arbitration shall be conducted, at the option of the party seeking relief, online, by telephone, online, or via written submissions alone. Any judgment rendered by the arbitrator may be entered in any court of competent jurisdiction. + +### Disclaimer of Warranty / Limitation of Liabilities + +**THE SITES AND ANY INFORMATION, PRODUCTS OR SERVICES THEREIN ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. COURSEMOLOGY AND ITS PARTICIPATING INSTITUTIONS, THEIR INSTRUCTORS AND THEIR STAFF (THE “COURSEMOLOGY PARTIES”) DO NOT WARRANT, AND HEREBY DISCLAIM ANY WARRANTIES, EITHER EXPRESS OR IMPLIED, WITH RESPECT TO THE ACCURACY, ADEQUACY OR COMPLETENESS OF ANY ONLINE COURSE, SITE, INFORMATION OBTAINED FROM A SITE, OR LINK TO A SITE. THE COURSEMOLOGY PARTIES DO NOT WARRANT THAT SITES WILL OPERATE IN AN UNINTERRUPTED OR ERROR-FREE MANNER OR THAT SITES ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS. WITHOUT LIMITING THE FOREGOING, THE COURSEMOLOGY PARTIES DO NOT WARRANT THAT (A) THE ONLINE COURSES OR SITES WILL MEET YOUR REQUIREMENTS OR EXPECTATIONS OR ACHIEVE THE INTENDED PURPOSES, (B) THE ONLINE COURSES OR SITES WILL NOT EXPERIENCE OUTAGES OR OTHERWISE BE UNINTERRUPTED, TIMELY, SECURE OR ERROR-FREE, (C) THE INFORMATION OR SERVICES OBTAINED THROUGH OR FROM THE ONLINE COURSES OR SITES WILL BE ACCURATE, COMPLETE, CURRENT, ERROR-FREE, COMPLETELY SECURE OR RELIABLE, OR (D) THAT DEFECTS IN OR ON THE ONLINE COURSES OR SITES WILL BE CORRECTED. NONE OF THE COURSEMOLOGY PARTIES MAKE ANY REPRESENTATION REGARDING YOUR ABILITY TO TRANSMIT AND RECEIVE INFORMATION FROM OR THROUGH THE SITES, AND YOU AGREE AND ACKNOWLEDGE THAT YOUR ABILITY TO ACCESS THE ONLINE COURSES AND SITES MAY BE IMPAIRED. THE COURSEMOLOGY PARTIES DISCLAIM ANY AND ALL LIABILITY RESULTING FROM OR RELATED TO SUCH EVENTS OR THE ACCESS OR USE OF THE ONLINE COURSES OR SITES OR ANY INFORMATION OR SERVICES RELATED TO THEM. YOU ACKNOWLEDGE AND AGREE THAT ANY ACCESS TO OR USE OF THE ONLINE COURSES AND SITES OR SUCH INFORMATION OR SERVICES IS AT YOUR OWN RISK. EXCEPT AS PROHIBITED BY LAW, YOU AGREE THAT THE COURSEMOLOGY PARTIES WILL NOT BE LIABLE TO YOU FOR ANY LOSS OR DAMAGES ARISING OUT OF OR RELATING TO THESE TERMS OF SERVICE OR YOUR (OR ANY THIRD PARTY'S) USE OR INABILITY TO USE AN ONLINE COURSE, SITE, DATA LOSS, YOUR PLACEMENT OF CONTENT ON A SITE, YOUR RELIANCE UPON INFORMATION OBTAINED FROM OR THROUGH AN ONLINE COURSE OR SITE, OR ANY OTHER POTENTIAL CLAIMS RELATED TO THE ONLINE COURSES OR SITES. EXCEPT AS PROHIBITED BY LAW, THE COURSEMOLOGY PARTIES WILL NOT HAVE LIABILITY FOR ANY CONSEQUENTIAL, INDIRECT, PUNITIVE, SPECIAL OR INCIDENTAL DAMAGES, WHETHER FORESEEABLE OR UNFORESEEABLE, (INCLUDING, BUT NOT LIMITED TO, CLAIMS FOR DEFAMATION, ERRORS, LOSS OF DATA, OR INTERRUPTION IN AVAILABILITY OF DATA), ARISING OUT OF OR RELATING TO THESE TERMS OF SERVICE, YOUR USE OR INABILITY TO USE ANY ONLINE COURSE OR SITE, DATA LOSS, ANY PURCHASES ON THIS SITE, YOUR PLACEMENT OF CONTENT ON A SITE, OR YOUR RELIANCE UPON INFORMATION OBTAINED FROM OR THROUGH ANY ONLINE COURSE OR SITE, WHETHER BASED IN CONTRACT, TORT, STATUTORY OR OTHER LAW, EXCEPT ONLY IN THE CASE OF DEATH OR PERSONAL INJURY WHERE AND ONLY TO THE EXTENT THAT APPLICABLE LAW REQUIRES SUCH LIABILITY. COURSEMOLOGY'S TOTAL CUMULATIVE LIABILITY ARISING OUT OF OR RELATED TO THE USER'S USE OF THE COURSEMOLOGY SITES WILL NOT EXCEED TWENTY U.S. DOLLARS ($20) OR THE TOTAL AMOUNT OF FEES RECEIVED BY COURSEMOLOGY FROM THE USER FOR THE USE OF THE COURSEMOLOGY SITES DURING THE PAST 12 MONTHS OF USE, WHICHEVER IS GREATER. YOU ACKNOWLEDGE AND AGREE THAT THE WARRANTY DISCLAIMERS AND THE LIMITATIONS OF LIABILITY SET FORTH IN THIS TERMS OF SERVICE REFLECT A REASONABLE AND FAIR ALLOCATION OF RISK BETWEEN YOU AND THE COURSEMOLOGY PARTIES, AND THAT THESE LIMITATIONS ARE AN ESSENTIAL BASIS TO COURSEMOLOGY'S ABILITY TO MAKE THE COURSEMOLOGY SITES AVAILABLE TO YOU ON AN ECONOMICALLY FEASIBLE BASIS. YOU AGREE THAT ANY CAUSE OF ACTION RELATED TO THE COURSEMOLOGY SITES MUST COMMENCE WITHIN ONE (1) YEAR AFTER THE CAUSE OF ACTION ACCRUES. OTHERWISE, SUCH CAUSE OF ACTION IS PERMANENTLY BARRED.** + +### Copyright Policy + +The Copyright Act (the “CA”) provides recourse for copyright owners who believe that material appearing on the Internet infringes their rights under Singapore copyright law. +If you believe in good faith that materials on the Coursemology Sites infringe your copyright, you (or your agent) may send us a notice requesting that the material be removed, or access to it blocked. +The notice must include the following information: (a) a physical or electronic signature of a person authorized to act on behalf of the owner of an exclusive right that is allegedly infringed; (b) identification of the copyrighted work claimed to have been infringed (or if multiple copyrighted works located on the Site are covered by a single notification, a representative list of such works); (c) identification of the material that is claimed to be infringing or the subject of infringing activity, and information reasonably sufficient to allow Coursemology to locate the material on the Site; (d) the name, address, telephone number, and email address (if available) of the complaining party; (e) a statement that the complaining party has a good faith belief that use of the material in the manner complained of is not authorized by the copyright owner, its agent, or the law; and (f) a statement that the information in the notification is accurate and, under penalty of perjury, that the complaining party is authorized to act on behalf of the owner of an exclusive right that is allegedly infringed. +Notices must meet the then-current statutory requirements imposed by the DMCA; see [http://www.loc.gov/copyright](http://www.loc.gov/copyright) for details. Notices and counter-notices with respect to the Site should be sent to [coursemology@gmail.com](mailto:coursemology@gmail.com). +We suggest that you consult your legal advisor before filing a notice. Also, be aware that there can be penalties for false claims under the CA. + +### Indemnification + +You agree to indemnify, defend and hold harmless Coursemology and the Participating Institutions, their respective subsidiaries and affiliates, and each of their respective officers, directors, agents, employees, and assignees, including the instructors of the Participating Institutions, from any and all claims, liabilities, expenses and damages, including reasonable attorneys’ fees and costs, made by any third party relating to or arising out of (a) your use or attempted use of the Sites or Online Course in violation of the Terms of Service; (b) your violation of any law or rights of any third party, or (c) information that you post or otherwise make available on the Sites or through the Online Course, including without limitation any claim of infringement or misappropriation of intellectual property or other proprietary rights. + +### Termination Rights + +You agree that each of Coursemology and the Participating Institutions, in their sole discretion, may terminate your use of the Site or your participation in it thereof, for any reason or no reason and that none of the Coursemology Parties shall have any liability to you for any such action. You further acknowledge that for the purpose of any Coursemology course your sole relationship with Coursemology and the Participating Institution is as defined in these Terms of Service; for the avoidance of doubt, you do not have student status at any Participating Institution through a Coursemology course and you are not entitled to any grievance or other resolution process for student disputes at any Participating Institution. You further agree that Coursemology has the right to cancel, delay, reschedule or alter the format of any Online Course at any time, and that none of the Coursemology Parties shall have any liability to you for any such action. If you no longer desire to participate in the Site, you may terminate your participation therein upon notice to Coursemology. + +### Honor Code + +All students participating in the class must agree to abide by the following code of conduct: + +- I will register for only one account. +- My answers to homework, quizzes and exams will be my own work (except for assignments that explicitly permit collaboration). +- I will not make solutions to homework, quizzes or exams available to anyone else. This includes both solutions written by me, as well as any official solutions provided by the course staff. +- I will not engage in any other activities that will dishonestly improve my results or dishonestly improve/hurt the results of others. diff --git a/client/app/bundles/common/store.ts b/client/app/bundles/common/store.ts new file mode 100644 index 00000000000..09a33d89881 --- /dev/null +++ b/client/app/bundles/common/store.ts @@ -0,0 +1,50 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { + DEFAULT_LOCALE, + DEFAULT_TIME_ZONE, +} from 'lib/constants/sharedConstants'; + +/** + * For now, we store a boolean instead of `userId?: number` because there + * isn't a need to store the `userId` at time of writing. Storing `userId` + * would mean that we have to also update it when the user masquerades. + * + * A boolean is kept here to prevent future developers from trying to use + * `userId` when its state update isn't fully thought out. If we ever + * decide to use `userId` more than just an indicator of authentication + * state, we can `SessionState` and `useAuthState`. These abstractions + * were made to make it easier to change the authentication implementations. + */ +export interface SessionState { + authenticated: boolean; + locale: string; + timeZone: string; +} + +const initialState: SessionState = { + authenticated: false, + locale: DEFAULT_LOCALE, + timeZone: DEFAULT_TIME_ZONE, +}; + +export const sessionStore = createSlice({ + name: 'session', + initialState, + reducers: { + setAuthenticated: (state, action: PayloadAction) => { + state.authenticated = action.payload; + }, + setI18nConfig: ( + state, + action: PayloadAction<{ locale?: string; timeZone?: string }>, + ) => { + state.locale = action.payload.locale ?? DEFAULT_LOCALE; + state.timeZone = action.payload.timeZone ?? DEFAULT_TIME_ZONE; + }, + }, +}); + +export const actions = sessionStore.actions; + +export default sessionStore.reducer; diff --git a/client/app/bundles/course/achievement/components/buttons/AchievementManagementButtons.tsx b/client/app/bundles/course/achievement/components/buttons/AchievementManagementButtons.tsx index 0a205b64f8f..8ff9e70acef 100644 --- a/client/app/bundles/course/achievement/components/buttons/AchievementManagementButtons.tsx +++ b/client/app/bundles/course/achievement/components/buttons/AchievementManagementButtons.tsx @@ -1,13 +1,13 @@ import { FC, useState } from 'react'; import { defineMessages } from 'react-intl'; import { useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { AchievementMiniEntity } from 'types/course/achievements'; import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import EditButton from 'lib/components/core/buttons/EditButton'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { deleteAchievement } from '../../operations'; diff --git a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx index 4e63f3697b8..4413c1ec4e0 100644 --- a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx +++ b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx @@ -1,13 +1,13 @@ -import { FC } from 'react'; -import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; +import { defineMessages } from 'react-intl'; import { Button } from '@mui/material'; -import axios from 'lib/axios'; +import CourseAPI from 'api/course'; +import toast from 'lib/hooks/toast'; +import useTranslation from 'lib/hooks/useTranslation'; require('jquery-ui/ui/widgets/sortable'); -interface Props extends WrappedComponentProps { +interface AchievementReorderingProps { handleReordering: (state: boolean) => void; isReordering: boolean; } @@ -45,21 +45,21 @@ function serializedOrdering(): string { return $('tbody').first().sortable('serialize', options); } -function submitReordering(ordering: string): Promise { - const action = `${window.location.pathname}/reorder`; +const AchievementReordering = ( + props: AchievementReorderingProps, +): JSX.Element => { + const { handleReordering, isReordering } = props; - return axios - .post(action, ordering) - .then(() => { - toast.success(translations.updateSuccess.defaultMessage); - }) - .catch(() => { - toast.error(translations.updateFailed.defaultMessage); - }); -} + const { t } = useTranslation(); -const AchievementReordering: FC = (props: Props) => { - const { intl, handleReordering, isReordering } = props; + async function submitReordering(ordering: string): Promise { + try { + await CourseAPI.achievements.reorder(ordering); + toast.success(t(translations.updateSuccess)); + } catch { + toast.error(t(translations.updateFailed)); + } + } return ( ); }; -export default injectIntl(AchievementReordering); +export default AchievementReordering; diff --git a/client/app/bundles/course/achievement/pages/AchievementAward/AchievementAwardManager.tsx b/client/app/bundles/course/achievement/pages/AchievementAward/AchievementAwardManager.tsx index a9dd87cbf7e..0b8b4fd23f7 100644 --- a/client/app/bundles/course/achievement/pages/AchievementAward/AchievementAwardManager.tsx +++ b/client/app/bundles/course/achievement/pages/AchievementAward/AchievementAwardManager.tsx @@ -6,7 +6,6 @@ import { WrappedComponentProps, } from 'react-intl'; import { useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { Button, Checkbox, Grid, Tooltip } from '@mui/material'; import { blue, green, red } from '@mui/material/colors'; import equal from 'fast-deep-equal'; @@ -23,6 +22,7 @@ import Note from 'lib/components/core/Note'; import { getAchievementURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { formatShortDateTime } from 'lib/moment'; import { awardAchievement } from '../../operations'; diff --git a/client/app/bundles/course/achievement/pages/AchievementEdit/index.tsx b/client/app/bundles/course/achievement/pages/AchievementEdit/index.tsx index 23d0ca6cd21..4ec02530441 100644 --- a/client/app/bundles/course/achievement/pages/AchievementEdit/index.tsx +++ b/client/app/bundles/course/achievement/pages/AchievementEdit/index.tsx @@ -1,10 +1,10 @@ import { FC, useEffect } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import AchievementForm from '../../components/forms/AchievementForm'; diff --git a/client/app/bundles/course/achievement/pages/AchievementNew/index.tsx b/client/app/bundles/course/achievement/pages/AchievementNew/index.tsx index e966867690c..e1d568ebc49 100644 --- a/client/app/bundles/course/achievement/pages/AchievementNew/index.tsx +++ b/client/app/bundles/course/achievement/pages/AchievementNew/index.tsx @@ -1,12 +1,12 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; import { useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { getAchievementURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import AchievementForm from '../../components/forms/AchievementForm'; diff --git a/client/app/bundles/course/achievement/pages/AchievementsIndex/index.tsx b/client/app/bundles/course/achievement/pages/AchievementsIndex/index.tsx index b29d02b5dc0..fe23364c08a 100644 --- a/client/app/bundles/course/achievement/pages/AchievementsIndex/index.tsx +++ b/client/app/bundles/course/achievement/pages/AchievementsIndex/index.tsx @@ -1,11 +1,11 @@ import { FC, ReactElement, useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import AddButton from 'lib/components/core/buttons/AddButton'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import AchievementReordering from '../../components/misc/AchievementReordering'; diff --git a/client/app/bundles/course/admin/pages/AnnouncementsSettings/index.tsx b/client/app/bundles/course/admin/pages/AnnouncementsSettings/index.tsx index 3d69c117770..9df8f61ea84 100644 --- a/client/app/bundles/course/admin/pages/AnnouncementsSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/AnnouncementsSettings/index.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { AnnouncementsSettingsData } from 'types/course/admin/announcements'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { FormEmitter } from 'lib/components/form/Form'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from 'lib/translations/form'; diff --git a/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Category.tsx b/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Category.tsx index 27401555a8d..fa104690755 100644 --- a/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Category.tsx +++ b/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Category.tsx @@ -177,7 +177,7 @@ const Category = (props: CategoryProps): JSX.Element => { {!renaming && ( setRenaming(true)} size="small" diff --git a/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Tab.tsx b/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Tab.tsx index f468e31f34f..5e42c1ecb98 100644 --- a/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Tab.tsx +++ b/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Tab.tsx @@ -139,7 +139,7 @@ const Tab = (props: TabProps): JSX.Element => { {!renaming && ( setRenaming(true)} size="small" @@ -151,7 +151,7 @@ const Tab = (props: TabProps): JSX.Element => { {tab.canDeleteTab && !stationary && ( ', () => { it('allow lesson plan item settings to be set', async () => { diff --git a/client/app/bundles/course/admin/pages/LessonPlanSettings/operations.ts b/client/app/bundles/course/admin/pages/LessonPlanSettings/operations.ts index 502af5638be..52376628ca8 100644 --- a/client/app/bundles/course/admin/pages/LessonPlanSettings/operations.ts +++ b/client/app/bundles/course/admin/pages/LessonPlanSettings/operations.ts @@ -1,7 +1,7 @@ -import { toast } from 'react-toastify'; import { AxiosError } from 'axios'; import CourseAPI from 'api/course'; +import toast from 'lib/hooks/toast'; import { update } from '../../reducers/lessonPlanSettings'; diff --git a/client/app/bundles/course/admin/pages/MaterialsSettings/index.tsx b/client/app/bundles/course/admin/pages/MaterialsSettings/index.tsx index 7b8c0274ff1..d43bd1f2e7f 100644 --- a/client/app/bundles/course/admin/pages/MaterialsSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/MaterialsSettings/index.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { MaterialsSettingsData } from 'types/course/admin/materials'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { FormEmitter } from 'lib/components/form/Form'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from 'lib/translations/form'; diff --git a/client/app/bundles/course/admin/pages/NotificationSettings/__test__/index.test.tsx b/client/app/bundles/course/admin/pages/NotificationSettings/__test__/index.test.tsx index 5d092e044de..26aabff3dea 100644 --- a/client/app/bundles/course/admin/pages/NotificationSettings/__test__/index.test.tsx +++ b/client/app/bundles/course/admin/pages/NotificationSettings/__test__/index.test.tsx @@ -1,4 +1,4 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { fireEvent, render, waitFor } from 'test-utils'; import CourseAPI from 'api/course'; @@ -24,7 +24,7 @@ const expectedPayload = { }, }; -const mock = new MockAdapter(CourseAPI.admin.notifications.client); +const mock = createMockAdapter(CourseAPI.admin.notifications.client); describe('', () => { it('allow emails notification settings to be set', async () => { diff --git a/client/app/bundles/course/admin/pages/NotificationSettings/operations.ts b/client/app/bundles/course/admin/pages/NotificationSettings/operations.ts index b03c73eda8c..4eb024eb4fa 100644 --- a/client/app/bundles/course/admin/pages/NotificationSettings/operations.ts +++ b/client/app/bundles/course/admin/pages/NotificationSettings/operations.ts @@ -1,7 +1,7 @@ -import { toast } from 'react-toastify'; import { AxiosError } from 'axios'; import CourseAPI from 'api/course'; +import toast from 'lib/hooks/toast'; import { update } from '../../reducers/notificationSettings'; diff --git a/client/app/bundles/course/admin/pages/SidebarSettings/index.tsx b/client/app/bundles/course/admin/pages/SidebarSettings/index.tsx index a8e6e695e5b..39ead2cc02b 100644 --- a/client/app/bundles/course/admin/pages/SidebarSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/SidebarSettings/index.tsx @@ -1,9 +1,9 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { SidebarItems } from 'types/course/admin/sidebar'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { fetchSidebarItems, updateSidebarItems } from './operations'; diff --git a/client/app/bundles/course/admin/pages/VideosSettings/VideosTabsManager/Tab.tsx b/client/app/bundles/course/admin/pages/VideosSettings/VideosTabsManager/Tab.tsx index 2d56712546c..5dedb856844 100644 --- a/client/app/bundles/course/admin/pages/VideosSettings/VideosTabsManager/Tab.tsx +++ b/client/app/bundles/course/admin/pages/VideosSettings/VideosTabsManager/Tab.tsx @@ -93,7 +93,7 @@ const Tab = (props: TabProps): JSX.Element => { {!renaming && ( setRenaming(true)} size="small" @@ -105,7 +105,7 @@ const Tab = (props: TabProps): JSX.Element => { {tab.canDeleteTab && ( setDeleting(true)} diff --git a/client/app/bundles/course/admin/pages/VideosSettings/index.tsx b/client/app/bundles/course/admin/pages/VideosSettings/index.tsx index a8005662dee..c02a6b1418b 100644 --- a/client/app/bundles/course/admin/pages/VideosSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/VideosSettings/index.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { VideosSettingsData, VideosTab } from 'types/course/admin/videos'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { FormEmitter } from 'lib/components/form/Form'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/bundles/course/announcements/components/misc/AnnouncementCard.tsx b/client/app/bundles/course/announcements/components/misc/AnnouncementCard.tsx index a767768e579..5ded70c5303 100644 --- a/client/app/bundles/course/announcements/components/misc/AnnouncementCard.tsx +++ b/client/app/bundles/course/announcements/components/misc/AnnouncementCard.tsx @@ -1,6 +1,5 @@ import { FC, memo, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { DateRange, PushPin } from '@mui/icons-material'; import { Paper, Typography } from '@mui/material'; import equal from 'fast-deep-equal'; @@ -15,6 +14,7 @@ import EditButton from 'lib/components/core/buttons/EditButton'; import CustomTooltip from 'lib/components/core/CustomTooltip'; import Link from 'lib/components/core/Link'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { formatFullDateTime } from 'lib/moment'; import AnnouncementEdit from '../../pages/AnnouncementEdit'; diff --git a/client/app/bundles/course/announcements/pages/AnnouncementEdit/index.tsx b/client/app/bundles/course/announcements/pages/AnnouncementEdit/index.tsx index a5e17947795..1716049bea7 100644 --- a/client/app/bundles/course/announcements/pages/AnnouncementEdit/index.tsx +++ b/client/app/bundles/course/announcements/pages/AnnouncementEdit/index.tsx @@ -1,11 +1,11 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { Operation } from 'store'; import { AnnouncementFormData } from 'types/course/announcements'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import AnnouncementForm from '../../components/forms/AnnouncementForm'; diff --git a/client/app/bundles/course/announcements/pages/AnnouncementNew/index.tsx b/client/app/bundles/course/announcements/pages/AnnouncementNew/index.tsx index 099a429f38c..eaeb048d958 100644 --- a/client/app/bundles/course/announcements/pages/AnnouncementNew/index.tsx +++ b/client/app/bundles/course/announcements/pages/AnnouncementNew/index.tsx @@ -1,11 +1,11 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { Operation } from 'store'; import { AnnouncementFormData } from 'types/course/announcements'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import AnnouncementForm, { diff --git a/client/app/bundles/course/announcements/pages/AnnouncementsIndex/index.tsx b/client/app/bundles/course/announcements/pages/AnnouncementsIndex/index.tsx index e3844a88e55..4e97afd98c0 100644 --- a/client/app/bundles/course/announcements/pages/AnnouncementsIndex/index.tsx +++ b/client/app/bundles/course/announcements/pages/AnnouncementsIndex/index.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Note from 'lib/components/core/Note'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import NewAnnouncementButton from '../../components/buttons/NewAnnouncementButton'; diff --git a/client/app/bundles/course/assessment/attemptLoader.ts b/client/app/bundles/course/assessment/attemptLoader.ts new file mode 100644 index 00000000000..75670b99a69 --- /dev/null +++ b/client/app/bundles/course/assessment/attemptLoader.ts @@ -0,0 +1,39 @@ +import { defineMessages } from 'react-intl'; +import { LoaderFunction, redirect } from 'react-router-dom'; +import { getIdFromUnknown } from 'utilities'; + +import CourseAPI from 'api/course'; +import toast from 'lib/hooks/toast'; +import { Translated } from 'lib/hooks/useTranslation'; + +const translations = defineMessages({ + errorAttemptingAssessment: { + id: 'assessment.attemptLoader.errorAttemptingAssessment', + defaultMessage: + 'An error occurred while attempting this assessment. Try again later.', + }, +}); + +const assessmentAttemptLoader: Translated = + (t) => + async ({ params }) => { + try { + const assessmentId = getIdFromUnknown(params?.assessmentId); + if (!assessmentId) return redirect('/'); + + const { data } = await CourseAPI.assessment.assessments.attempt( + assessmentId, + ); + + return redirect(data.redirectUrl); + } catch { + toast.error(t(translations.errorAttemptingAssessment)); + + const { courseId } = params; + if (!courseId) return redirect('/'); + + return redirect(`/courses/${courseId}/assessments`); + } + }; + +export default assessmentAttemptLoader; diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx b/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx index 758c6226421..27cbab0dd4b 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx +++ b/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx @@ -684,7 +684,7 @@ const AssessmentForm = (props: AssessmentFormProps): JSX.Element => { ( - + {chunk} ), @@ -729,7 +729,7 @@ const AssessmentForm = (props: AssessmentFormProps): JSX.Element => { > {t(translations.secretHint, { pulsegrid: (chunk) => ( - + {chunk} ), diff --git a/client/app/bundles/course/assessment/components/ConvertMcqMrqButton/ConvertMcqMrqPrompt.tsx b/client/app/bundles/course/assessment/components/ConvertMcqMrqButton/ConvertMcqMrqPrompt.tsx index 2ad9eb8b067..2d40cf115e1 100644 --- a/client/app/bundles/course/assessment/components/ConvertMcqMrqButton/ConvertMcqMrqPrompt.tsx +++ b/client/app/bundles/course/assessment/components/ConvertMcqMrqButton/ConvertMcqMrqPrompt.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { East } from '@mui/icons-material'; import { Alert, Chip, Typography } from '@mui/material'; import { McqMrqListData } from 'types/course/assessment/question/multiple-responses'; import Prompt, { PromptText } from 'lib/components/core/dialogs/Prompt'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { convertMcqMrq } from '../../operations'; @@ -50,17 +50,15 @@ const ConvertMcqMrqPrompt = (props: ConvertMcqMrqPromptProps): JSX.Element => { success: unsubmit ? t(translations.questionTypeChangedUnsubmitted) : t(translations.questionTypeChanged), - error: { - render: ({ data }) => { - const error = (data as Error)?.message; - return error || t(translations.errorChangingQuestionType); - }, - }, }) .then((data) => { props.onConvertComplete({ ...question, ...data }); props.onClose(); }) + .catch((error) => { + const message = (error as Error)?.message; + toast.error(message || t(translations.errorChangingQuestionType)); + }) .finally(() => setConverting(false)); }; diff --git a/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx b/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx index 52a572dce35..813ee464d05 100644 --- a/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx +++ b/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx @@ -1,4 +1,4 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { act, fireEvent, render, RenderResult, waitFor } from 'test-utils'; import CourseAPI from 'api/course'; @@ -29,8 +29,7 @@ const NEW_MATERIAL = { deleting: false, }; -const client = CourseAPI.materialFolders.client; -const mock = new MockAdapter(client); +const mock = createMockAdapter(CourseAPI.materialFolders.client); let fileManager: RenderResult; beforeEach(() => { diff --git a/client/app/bundles/course/assessment/components/FileManager/index.tsx b/client/app/bundles/course/assessment/components/FileManager/index.tsx index 0d6ce8495c9..76b9fccd2b0 100644 --- a/client/app/bundles/course/assessment/components/FileManager/index.tsx +++ b/client/app/bundles/course/assessment/components/FileManager/index.tsx @@ -1,6 +1,5 @@ import { CSSProperties, useState } from 'react'; import { injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Checkbox, CircularProgress } from '@mui/material'; import { AxiosError } from 'axios'; @@ -10,6 +9,7 @@ import DataTable from 'lib/components/core/layouts/DataTable'; import Link from 'lib/components/core/Link'; import { getWorkbinFileURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; +import toast from 'lib/hooks/toast'; import { formatLongDateTime } from 'lib/moment'; import Toolbar from './Toolbar'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowHeader.tsx b/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowHeader.tsx index c944a2668f8..f0ac60b7564 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowHeader.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowHeader.tsx @@ -1,6 +1,5 @@ -import { MouseEventHandler, useState } from 'react'; +import { useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { Assessment, Create, @@ -15,9 +14,10 @@ import { import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import { PromptText } from 'lib/components/core/dialogs/Prompt'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; -import { attemptAssessment, deleteAssessment } from '../../operations'; +import { deleteAssessment } from '../../operations'; import translations from '../../translations'; import { ACTION_LABELS } from '../AssessmentsIndex/ActionButtons'; @@ -31,32 +31,8 @@ const AssessmentShowHeader = ( const { with: assessment } = props; const { t } = useTranslation(); const [deleting, setDeleting] = useState(false); - const [attempting, setAttempting] = useState(false); const navigate = useNavigate(); - const actionButtonUrl = - assessment.status === 'open' ? '#' : assessment.actionButtonUrl; - - const handleActionButton: MouseEventHandler = (e) => { - if (assessment.status !== 'open') return; - setAttempting(true); - e.preventDefault(); - e.stopPropagation(); - toast - .promise(attemptAssessment(assessment.id), { - pending: t(translations.attemptingAssessment), - success: t(translations.createSubmissionSuccessful), - error: { - render: ({ data }) => { - const error = (data as Error)?.message; - return t(translations.createSubmissionFailed, { error }); - }, - }, - }) - .then((data) => navigate(data.redirectUrl)) - .catch(() => setAttempting(false)); - }; - const handleDelete = (): Promise => { const deleteUrl = assessment.deleteUrl; if (!deleteUrl) @@ -70,15 +46,14 @@ const AssessmentShowHeader = ( .promise(deleteAssessment(deleteUrl), { pending: t(translations.deletingAssessment), success: t(translations.assessmentDeleted), - error: { - render: ({ data }) => { - const error = (data as Error)?.message; - return error || t(translations.errorDeletingAssessment); - }, - }, }) .then((data: AssessmentDeleteResult) => navigate(data.redirect)) - .catch(() => setDeleting(false)); + .catch((error) => { + const message = (error as Error)?.message; + toast.error(message || t(translations.errorDeletingAssessment)); + + setDeleting(false); + }); }; return ( @@ -140,13 +115,11 @@ const AssessmentShowHeader = ( )} - {actionButtonUrl && ( - + {assessment.actionButtonUrl && ( +