diff --git a/.annotaterb.yml b/.annotaterb.yml
new file mode 100644
index 0000000000..2c665f77be
--- /dev/null
+++ b/.annotaterb.yml
@@ -0,0 +1,65 @@
+---
+:position: before
+:position_in_additional_file_patterns: before
+:position_in_class: before
+:position_in_factory: before
+:position_in_fixture: before
+:position_in_routes: before
+:position_in_serializer: before
+:position_in_test: before
+:classified_sort: true
+:exclude_controllers: true
+:exclude_factories: true
+:exclude_fixtures: true
+:exclude_helpers: true
+:exclude_scaffolds: true
+:exclude_serializers: true
+:exclude_sti_subclasses: false
+:exclude_tests: true
+:force: false
+:format_markdown: false
+:format_rdoc: false
+:format_yard: false
+:frozen: false
+:grouped_polymorphic: false
+:ignore_model_sub_dir: false
+:ignore_unknown_models: false
+:include_version: false
+:show_check_constraints: false
+:show_complete_foreign_keys: false
+:show_foreign_keys: true
+:show_indexes: true
+:show_indexes_include: false
+:simple_indexes: false
+:sort: false
+:timestamp: false
+:trace: false
+:with_comment: true
+:with_column_comments: true
+:with_table_comments: true
+:position_of_column_comment: :with_name
+:active_admin: false
+:command:
+:debug: false
+:hide_default_column_types: ''
+:hide_limit_column_types: ''
+:timestamp_columns:
+- created_at
+- updated_at
+:ignore_columns:
+:ignore_routes:
+:ignore_multi_database_name: false
+:models: true
+:routes: false
+:skip_on_db_migrate: false
+:target_action: :do_annotations
+:wrapper:
+:wrapper_close: 'rubocop:enable Layout/LineLength, Lint/RedundantCopDisableDirective'
+:wrapper_open: 'rubocop:disable Layout/LineLength, Lint/RedundantCopDisableDirective'
+:classes_default_to_s: []
+:additional_file_patterns: []
+:model_dir:
+- app/models
+:require: []
+:root_dir:
+- ''
diff --git a/.dockerfiles/Dockerfile b/.dockerfiles/Dockerfile
index c86f0fba6b..16beb04f2f 100644
--- a/.dockerfiles/Dockerfile
+++ b/.dockerfiles/Dockerfile
@@ -1,8 +1,8 @@
-FROM ubuntu:jammy AS base
+FROM ubuntu:24.04 AS base
-ARG NODE_MAJOR=20
-ARG BUNDLER_VERSION='2.4.13'
-ARG RUBY_VERSION='3.3.4'
+ARG NODE_MAJOR=24
+ARG BUNDLER_VERSION='4.0.10'
+ARG RUBY_VERSION='3.4.9'
ARG USER=markus
# Required in order to ensure bind-mounts are owned by the correct user inside the container
@@ -10,29 +10,38 @@ ARG USER=markus
# Set this to the same UID as the user that owns the Markus files on the host machine.
ARG UID=1001
+# Remove ubuntu user, added in the 23.04 image
+RUN userdel -r ubuntu
+
# Create the user that runs the app
RUN useradd -m -u $UID -s /bin/bash $USER
-# Set up the correct node version for later installation
-ADD https://deb.nodesource.com/setup_$NODE_MAJOR.x /tmp/setup_node.sh
-RUN sh /tmp/setup_node.sh
+# Set up the correct node version AND PostgreSQL repo for later installation
+RUN apt-get update && \
+ DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends wget ca-certificates sudo gnupg lsb-release && \
+ install -m 0755 -d /etc/apt/keyrings && \
+ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg && \
+ echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
+ wget -O /tmp/setup_node.sh https://deb.nodesource.com/setup_$NODE_MAJOR.x && \
+ sh /tmp/setup_node.sh && \
+ rm /tmp/setup_node.sh
# Copy the debian package containing system dependencies
COPY markus_1.0_all.deb /
-# Install basic system dependencies
+# Install basic system dependencies and the specific PG client version
RUN apt-get update && \
- DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends /markus_1.0_all.deb && \
- rm /tmp/setup_node.sh /markus_1.0_all.deb
-
-# Install Ruby (we use ruby-install to configure the installed ruby version).
-RUN DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends wget ca-certificates sudo && \
- wget https://github.com/postmodern/ruby-install/releases/download/v0.9.3/ruby-install-0.9.3.tar.gz && \
- tar -xzvf ruby-install-0.9.3.tar.gz && \
- cd ruby-install-0.9.3/ && \
- make install && \
- ruby-install --update && \
- ruby-install --system ruby $RUBY_VERSION
+ DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
+ /markus_1.0_all.deb \
+ postgresql-client-17 && \
+ rm /markus_1.0_all.deb
+
+# Install Ruby from precompiled binaries.
+# See https://github.com/jdx/ruby
+RUN ARCH=$(uname -m | sed 's/x86_64/x86_64_linux/;s/aarch64/arm64_linux/') && \
+ wget -O /tmp/ruby.tar.gz https://github.com/jdx/ruby/releases/download/$RUBY_VERSION/ruby-$RUBY_VERSION.$ARCH.tar.gz && \
+ tar --strip-components=1 -C /usr/local -xzf /tmp/ruby.tar.gz && \
+ rm /tmp/ruby.tar.gz
# Enable reading of PDF files with imagemagick
RUN sed -ri 's/(rights=")none("\s+pattern="PDF")/\1read\2/' /etc/ImageMagick-6/policy.xml
@@ -81,6 +90,7 @@ RUN apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive add-apt-repository -y ppa:deadsnakes/ppa && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends openssh-server \
python3.13 \
+ python3.13-dev \
python3.13-venv \
equivs \
libjemalloc2
@@ -133,7 +143,7 @@ ENV RAILS_ENV=production
ENV NODE_ENV=production
# Install gems
-RUN SECRET_KEY_BASE=1 bundle install --deployment
+RUN SECRET_KEY_BASE=1 BUNDLE_DEPLOYMENT=1 bundle install
# Precompile assets
RUN SECRET_KEY_BASE=1 NO_SCHEMA_VALIDATE=true NO_INIT_SCHEDULER=true PGDATABASE=dummy bundle exec rails assets:precompile
diff --git a/.dockerfiles/entrypoint-dev-rails.sh b/.dockerfiles/entrypoint-dev-rails.sh
index bd450fe9f7..2091b51fbc 100755
--- a/.dockerfiles/entrypoint-dev-rails.sh
+++ b/.dockerfiles/entrypoint-dev-rails.sh
@@ -20,8 +20,5 @@ sed -i -e :a -e '/^\n*$/{$d;N;};/\n$/ba' /app/db/structure.sql
rm -f ./tmp/pids/server.pid
-# cssbundling-rails development command
-npm run build-dev:css &
-
# Then exec the container's main process (what's set as CMD in the Dockerfile or compose.yaml).
exec "$@"
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 2af4851e83..32405a2022 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,5 +1,9 @@
version: 2
updates:
+- package-ecosystem: github-actions
+ directory: "/"
+ schedule:
+ interval: quarterly
- package-ecosystem: bundler
directory: "/"
schedule:
diff --git a/.github/workflows/test_ci.yml b/.github/workflows/test_ci.yml
index 0465d5e993..5f828167a8 100644
--- a/.github/workflows/test_ci.yml
+++ b/.github/workflows/test_ci.yml
@@ -9,10 +9,10 @@ on:
jobs:
test_rspec:
if: github.event.pull_request.draft == false
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
services:
postgres:
- image: postgres:14
+ image: postgres:17
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@@ -42,13 +42,20 @@ jobs:
contents: read
steps:
- name: Checkout repo
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
+ - name: Add PostgreSQL apt repository
+ run: |
+ sudo apt-get install -y curl ca-certificates
+ sudo install -d /usr/share/postgresql-common/pgdg
+ sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
+ sudo sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
- name: Install and cache system dependencies
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: |
libpq-dev
cmake
+ pkg-config
ghostscript
pandoc
imagemagick
@@ -59,7 +66,7 @@ jobs:
pandoc
poppler-utils
fonts-liberation
- libasound2
+ libasound2t64
libatk-bridge2.0-0
libatk1.0-0
libatspi2.0-0
@@ -83,23 +90,25 @@ jobs:
libxfixes3
libxrandr2
libxshmfence1
- version: 1.0
+ postgresql-client-17
+ version: 1.0.2
# Packages 'fonts-liberation' and onward are needed for playwright's chromium installation.
# The list was taken from: https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/registry/nativeDeps.ts#L37-L63
+ # libasound2 renamed to libasound2t64 in Ubuntu 24.04
- name: Set up ruby and cache gems
uses: ruby/setup-ruby@v1
with:
- ruby-version: ruby-3.3
+ ruby-version: ruby-3.4
bundler-cache: true
- name: Set up node and cache packages
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
- node-version: 18
+ node-version: 24
cache: npm
- name: Install npm packages
run: npm ci
- name: Install python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Get pip cache dir
@@ -107,14 +116,14 @@ jobs:
run: |
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache pip
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: ${{ runner.os }}-pip-${{ hashFiles('requirements-jupyter.txt') }}-${{ hashFiles('requirements-scanner.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Cache playwright's installation of Chromium
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('requirements-jupyter.txt') }}
@@ -132,9 +141,7 @@ jobs:
sudo sed -ri 's/(rights=")none("\s+pattern="PDF")/\1read\2/' /etc/ImageMagick-6/policy.xml
cp config/database.yml.ci config/database.yml
- name: Build assets
- run: |
- bundle exec rake javascript:build
- bundle exec rake css:build
+ run: bundle exec rake javascript:build
- name: Set up database
run: bundle exec rails db:migrate
- name: Install chromedriver
@@ -159,7 +166,7 @@ jobs:
test_jest:
if: github.event.pull_request.draft == false
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
services:
redis:
image: redis
@@ -174,15 +181,16 @@ jobs:
permissions:
contents: read
env:
+ BUNDLE_WITHOUT: development:production:console:unicorn
RAILS_ENV: test
NODE_ENV: test
steps:
- name: Checkout repo
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Set up ruby and cache gems
uses: ruby/setup-ruby@v1
with:
- ruby-version: ruby-3.3
+ ruby-version: ruby-3.4
bundler-cache: true
- name: Configure server
run: |
@@ -196,9 +204,9 @@ jobs:
run: |
bundle exec rake javascript:build
- name: Set up node and cache packages
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
- node-version: 18
+ node-version: 24
cache: npm
- name: Install npm packages
run: npm ci
@@ -215,7 +223,7 @@ jobs:
finish:
needs: [test_rspec, test_jest]
if: github.event.pull_request.draft == false
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
permissions:
pull-requests: write
steps:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 19f0d6c978..c846fb1021 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -17,12 +17,12 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/rbubley/mirrors-prettier
- rev: v3.7.3
+ rev: v3.8.3
hooks:
- id: prettier
types_or: [javascript, jsx, css, scss, html]
- repo: https://github.com/thibaudcolas/pre-commit-stylelint
- rev: v16.26.1
+ rev: v17.10.0
hooks:
- id: stylelint
additional_dependencies: [
@@ -39,7 +39,7 @@ repos:
app/assets/stylesheets/common/_reset.scss
)$
- repo: https://github.com/rubocop/rubocop
- rev: v1.81.7
+ rev: v1.86.1
hooks:
- id: rubocop
args: ["--autocorrect"]
@@ -53,11 +53,11 @@ repos:
)$
additional_dependencies:
- rubocop-rails:2.33.4
- - rubocop-performance:1.23.0
- - rubocop-factory_bot:2.26.1
+ - rubocop-performance:1.26.1
+ - rubocop-factory_bot:2.28.0
- rubocop-rspec:3.2.0
- - rubocop-rspec_rails:2.30.0
- - rubocop-capybara:2.21.0
+ - rubocop-rspec_rails:2.32.0
+ - rubocop-capybara:2.22.1
- repo: local
hooks:
- id: erb_lint
@@ -73,6 +73,14 @@ repos:
)$
additional_dependencies:
- erb_lint:0.9.0
+ - id: i18n-tasks-health
+ name: i18n-tasks health
+ entry: i18n-tasks health
+ language: ruby
+ files: config/locales/
+ pass_filenames: false
+ additional_dependencies:
+ - i18n-tasks
exclude: vendor
diff --git a/.rubocop.yml b/.rubocop.yml
index 68812dd020..6104fdcbc7 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,11 +1,10 @@
-require:
- - rubocop-performance
- - rubocop-factory_bot
- - rubocop-rspec_rails
- - rubocop-capybara
plugins:
+ - rubocop-capybara
+ - rubocop-performance
- rubocop-rails
- rubocop-rspec
+ - rubocop-rspec_rails
+ - rubocop-factory_bot
AllCops:
Exclude:
@@ -29,6 +28,9 @@ FactoryBot/ConsistentParenthesesStyle:
Layout/EmptyLineAfterGuardClause:
Enabled: false
+Layout/IndentationWidth:
+ Enabled: false
+
Layout/LineEndStringConcatenationIndentation:
Enabled: true
@@ -45,6 +47,9 @@ Layout/FirstHashElementIndentation:
Layout/LineLength:
Max: 120
+Layout/MultilineMethodCallIndentation:
+ Enabled: false
+
# Disable to allow method chaining aligned at the '.'
Layout/MultilineOperationIndentation:
Enabled: false
@@ -348,6 +353,10 @@ Style/NumberedParametersLimit:
Style/NumericPredicate:
Enabled: false
+# TODO: enable this check
+Style/OneClassPerFile:
+ Enabled: false
+
Style/OpenStructUse:
Enabled: false
diff --git a/Changelog.md b/Changelog.md
index 68fa6292ca..ef7b8d1868 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,16 +1,62 @@
# Changelog
-## [unreleased]
-
-### 🛡️ Security
-
-### 🚨 Breaking changes
+## [v2.10.0]
### ✨ New features and improvements
+- Improve admin user list loading time by replacing ActiveRecord instantiation with direct column extraction (#7897)
+- Provide suggestions for partial student matching scans (#7760)
+- Allow inactive students to join groups (#7757)
+- Update autotest settings form UI (#7777)
+- Split courses into Current and Past sections for all users (#7801)
+- Add an administrator action on the course settings page to refresh autotest schema (#7828)
+- Add JS autotester example (#7866)
+- Return structured JSON from grade entry forms API show endpoint with optional student filter and CSV export (#7886)
+- Added term-based suffixes to course names created via LTI to ensure uniqueness across academic years (#7881)
+- Added `db:populate_course_dates` rake task to backfill `start_at`/`end_at` for existing courses, and permit those fields when creating courses through the admin UI (#7925)
+- Sync due date when creating or updating LTI gradebook line items, and re-sync automatically when an assessment is edited (#7872)
### 🐛 Bug fixes
+- Prevent "No rows found" message from displaying in tables when data is loading (#7790)
+- Fixed course card link behavior on the dashboard (#7887)
+- Fixed reserved interpolation key `%{format}` in `download_errors.unrecognized_format` locale string, renamed to `%{file_format}` (#7894)
+- Fix version mismatch between container client and database server (#7916)
+- Fixed filter Canvas Test Student from roster sync (#7926)
+- Fix: include original total mark in JSON response for remark requests (#7945)
+- Fixed `(hidden)` assignment labeling for assignments with `visible_on` and/or `visible_until` set (#7944)
### 🔧 Internal changes
+- Fixed flaky test `can bulk assign duplicated TAs to grade entry students` in `/spec/models/grade_entry_student_spec.rb` (#7958)
+- Added tests for `GroupsController` to fully cover `global_actions` (#7955)
+- Added tests for `graders_controller` to fully cover `grader_criteria_mapping` function (#7949)
+- Added tests for `GradersController` to fully cover `grader_groupers_mapping` (#7946)
+- Added seed task to assign TAs to A1 groupings and criteria (#7867)
+- Updated autotest seed files to ensure settings follow tester JSON schema (#7775)
+- Refactored grade entry form helper logic into `GradeEntryFormsController` and removed the newly-unused helper file. (#7789)
+- Added tests for `GradeEntryFormsController` to fully cover `update_grade_entry_form_params` (#7789)
+- Updated the grade breakdown summary table to use `@tanstack/react-table` v8 (#7800)
+- Internationalized custom model validation error messages by replacing hardcoded English strings with i18n symbol keys (#7805)
+- Changed model validation errors to use built-in error key resolution instead of inline `I18n.t` calls (#7806)
+- Upgraded to Rails v8.1 (#7815)
+- Updated the student table to use `@tanstack/react-table` v8 (#7826)
+- Added `active_record_doctor` development gem (#7861)
+- Added `annotaterb` development gem and added model and route annotations (#7861)
+- Added `pghero` gem and added PgHero admin dashboard (#7861)
+- Add nullable last_updated_by foreign key (to roles) to marks and grades tables to track who assigned which grade (#7878)
+- Updates ResultsController#update_mark to set the last_updated_by field when marks are modified (#7885)
+- Added pre-commit hook to run `i18n-tasks health` when locale files are changed (#7894)
+- Refactored tables to avoid recreating column definitions on every render (#7910)
+- Updated Docker configuration to use Ubuntu 24.04, Ruby 3.4.9, Bundler 4.0.10, Node 24, and Postgres 17 (#7911)
+- Update Dockerfile to include the python3.13-dev package which allows building c++ extensions from source (#7912)
+- Improved performance of `Table` component (#7919)
+- Updated GitHub Actions dependencies and added Dependabot config for quarterly GitHub Actions updates (#7920)
+- Updated `pdfjs-dist` to v5.6.205 (#7942)
+- Switched SCSS files to use `@use` instead of `@import` to reduce bundle size (#7943)
+- Fixed flaky git-hooks tests (#7950)
+- Replaced custom `HTMLElement` class methods with native versions and removed `application.js` (#7951)
+- Moved annotation-related Javascript to be bundled with webpack (#7953)
+- Upgraded `react-jsonschema-form` to v6.5.2 (#7954)
+- Fixed CSP warnings and updated CSP config. Switched webpack development source map to `eval-source-map`. (#7956)
+- Move all CSS builds into webpack pipeline (#7957)
## [v2.9.6]
diff --git a/Gemfile b/Gemfile
index 6a066f2b82..59d004b3f5 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,7 +8,7 @@ source 'https://rubygems.org'
# Bundler requires these gems in all environments
gem 'puma'
-gem 'rails', '~> 8.0.3'
+gem 'rails', '~> 8.1.2'
gem 'sprockets'
gem 'sprockets-rails'
@@ -37,7 +37,7 @@ gem 'histogram'
# Internationalization
gem 'i18n'
gem 'i18n-js'
-gem 'rails-i18n', '~> 8.0.2'
+gem 'rails-i18n', '~> 8.1.0'
# Redis
gem 'redis', '~> 5.4.1'
@@ -46,7 +46,7 @@ gem 'redis', '~> 5.4.1'
gem 'combine_pdf'
gem 'prawn'
gem 'prawn-qrcode'
-gem 'rmagick', '~> 6.1.4'
+gem 'rmagick', '~> 6.2.0'
gem 'rtesseract'
# Ruby miscellany
@@ -64,6 +64,7 @@ gem 'cookies_eu'
gem 'dry-validation' # For settings schema validation
gem 'exception_notification'
gem 'marcel'
+gem 'pghero'
gem 'rails-html-sanitizer'
gem 'rails_performance'
gem 'responders'
@@ -77,6 +78,7 @@ gem 'pg'
# Gems only used for development should be listed here so that they
# are not loaded in other environments.
group :development do
+ gem 'annotaterb', require: false
gem 'awesome_print'
gem 'better_errors'
gem 'binding_of_caller' # supplement for better_errors
@@ -96,7 +98,7 @@ group :test do
gem 'rails-controller-testing'
gem 'shoulda-callback-matchers', '~> 1.1.1'
gem 'shoulda-context', '~> 3.0.0.rc1'
- gem 'shoulda-matchers', '~> 6.5'
+ gem 'shoulda-matchers', '~> 7.0'
gem 'simplecov', require: false
gem 'simplecov-lcov', require: false
gem 'timecop'
@@ -106,11 +108,12 @@ end
# Gems needed (wanted) for both development and test can be
# listed here
group :development, :test do
+ gem 'active_record_doctor', require: false
gem 'bullet'
gem 'capybara'
gem 'debug', '>= 1.0.0'
gem 'i18n-tasks', require: false
- gem 'rspec-rails', '~> 8.0.2'
+ gem 'rspec-rails', '~> 8.0.4'
gem 'selenium-webdriver'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 41a49404b3..5586ac0305 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -3,29 +3,31 @@ GEM
specs:
action_policy (0.7.5)
ruby-next-core (>= 1.0)
- actioncable (8.0.3)
- actionpack (= 8.0.3)
- activesupport (= 8.0.3)
+ action_text-trix (2.1.18)
+ railties
+ actioncable (8.1.2.1)
+ actionpack (= 8.1.2.1)
+ activesupport (= 8.1.2.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
- actionmailbox (8.0.3)
- actionpack (= 8.0.3)
- activejob (= 8.0.3)
- activerecord (= 8.0.3)
- activestorage (= 8.0.3)
- activesupport (= 8.0.3)
+ actionmailbox (8.1.2.1)
+ actionpack (= 8.1.2.1)
+ activejob (= 8.1.2.1)
+ activerecord (= 8.1.2.1)
+ activestorage (= 8.1.2.1)
+ activesupport (= 8.1.2.1)
mail (>= 2.8.0)
- actionmailer (8.0.3)
- actionpack (= 8.0.3)
- actionview (= 8.0.3)
- activejob (= 8.0.3)
- activesupport (= 8.0.3)
+ actionmailer (8.1.2.1)
+ actionpack (= 8.1.2.1)
+ actionview (= 8.1.2.1)
+ activejob (= 8.1.2.1)
+ activesupport (= 8.1.2.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
- actionpack (8.0.3)
- actionview (= 8.0.3)
- activesupport (= 8.0.3)
+ actionpack (8.1.2.1)
+ actionview (= 8.1.2.1)
+ activesupport (= 8.1.2.1)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@@ -33,72 +35,77 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
- actiontext (8.0.3)
- actionpack (= 8.0.3)
- activerecord (= 8.0.3)
- activestorage (= 8.0.3)
- activesupport (= 8.0.3)
+ actiontext (8.1.2.1)
+ action_text-trix (~> 2.1.15)
+ actionpack (= 8.1.2.1)
+ activerecord (= 8.1.2.1)
+ activestorage (= 8.1.2.1)
+ activesupport (= 8.1.2.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (8.0.3)
- activesupport (= 8.0.3)
+ actionview (8.1.2.1)
+ activesupport (= 8.1.2.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
- activejob (8.0.3)
- activesupport (= 8.0.3)
+ active_record_doctor (2.0.1)
+ activerecord (>= 7.0.0)
+ activejob (8.1.2.1)
+ activesupport (= 8.1.2.1)
globalid (>= 0.3.6)
activejob-status (1.0.1)
activejob (>= 6.0)
activesupport (>= 6.0)
- activemodel (8.0.3)
- activesupport (= 8.0.3)
+ activemodel (8.1.2.1)
+ activesupport (= 8.1.2.1)
activemodel-serializers-xml (1.0.3)
activemodel (>= 5.0.0.a)
activesupport (>= 5.0.0.a)
builder (~> 3.1)
- activerecord (8.0.3)
- activemodel (= 8.0.3)
- activesupport (= 8.0.3)
+ activerecord (8.1.2.1)
+ activemodel (= 8.1.2.1)
+ activesupport (= 8.1.2.1)
timeout (>= 0.4.0)
- activestorage (8.0.3)
- actionpack (= 8.0.3)
- activejob (= 8.0.3)
- activerecord (= 8.0.3)
- activesupport (= 8.0.3)
+ activestorage (8.1.2.1)
+ actionpack (= 8.1.2.1)
+ activejob (= 8.1.2.1)
+ activerecord (= 8.1.2.1)
+ activesupport (= 8.1.2.1)
marcel (~> 1.0)
- activesupport (8.0.3)
+ activesupport (8.1.2.1)
base64
- benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
+ json
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
- addressable (2.8.7)
- public_suffix (>= 2.0.2, < 7.0)
+ addressable (2.9.0)
+ public_suffix (>= 2.0.2, < 8.0)
+ annotaterb (4.22.0)
+ activerecord (>= 6.0.0)
+ activesupport (>= 6.0.0)
ast (2.4.3)
autoprefixer-rails (10.4.21.0)
execjs (~> 2)
awesome_print (1.9.2)
base64 (0.3.0)
- benchmark (0.5.0)
better_errors (2.10.1)
erubi (>= 1.0.0)
rack (>= 0.9.0)
rouge (>= 1.0.0)
bigdecimal (3.3.1)
- binding_of_caller (1.0.1)
+ binding_of_caller (2.0.0)
debug_inspector (>= 1.2.0)
- bootsnap (1.18.6)
+ bootsnap (1.24.0)
msgpack (~> 1.2)
- brakeman (7.1.0)
+ brakeman (8.0.4)
racc
browser (6.2.0)
builder (3.3.0)
@@ -118,11 +125,11 @@ GEM
combine_pdf (1.0.31)
matrix
ruby-rc4 (>= 0.1.5)
- concurrent-ruby (1.3.5)
+ concurrent-ruby (1.3.6)
config (5.6.1)
deep_merge (~> 1.2, >= 1.2.1)
ostruct
- connection_pool (2.5.4)
+ connection_pool (3.0.2)
cookies_eu (1.7.8)
js_cookie_rails (~> 2.2.0)
crack (1.0.1)
@@ -132,7 +139,7 @@ GEM
cssbundling-rails (1.4.3)
railties (>= 6.0.0)
csv (3.3.5)
- date (3.4.1)
+ date (3.5.1)
debug (1.11.0)
irb (~> 1.10)
reline (>= 0.3.8)
@@ -177,7 +184,7 @@ GEM
dry-initializer (~> 3.2)
dry-schema (~> 1.14)
zeitwerk (~> 2.6)
- erb (5.0.2)
+ erb (6.0.4)
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
@@ -185,12 +192,12 @@ GEM
actionmailer (>= 7.1, < 9)
activesupport (>= 7.1, < 9)
execjs (2.10.0)
- factory_bot (6.5.4)
+ factory_bot (6.5.6)
activesupport (>= 6.1.0)
- factory_bot_rails (6.5.0)
+ factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 6.1.0)
- faker (3.5.2)
+ faker (3.8.0)
i18n (>= 1.8.11, < 2)
ffi (1.16.3)
fugit (1.11.1)
@@ -206,7 +213,7 @@ GEM
highline (3.1.2)
reline
histogram (0.2.4.1)
- i18n (1.14.7)
+ i18n (1.14.8)
concurrent-ruby (~> 1.0)
i18n-js (4.2.3)
glob (>= 0.4.0)
@@ -222,12 +229,13 @@ GEM
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.8, >= 1.8.1)
terminal-table (>= 1.5.1)
- io-console (0.8.1)
- irb (1.15.2)
+ io-console (0.8.2)
+ irb (1.17.0)
pp (>= 0.6.0)
+ prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
- js-routes (2.3.5)
+ js-routes (2.3.6)
railties (>= 5)
sorbet-runtime
js_cookie_rails (2.2.0)
@@ -247,7 +255,8 @@ GEM
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
machinist (2.0)
- mail (2.8.1)
+ mail (2.9.0)
+ logger
mini_mime (>= 0.1.1)
net-imap
net-pop
@@ -256,13 +265,15 @@ GEM
matrix (0.4.2)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
- minitest (5.26.0)
+ minitest (6.0.6)
+ drb (~> 2.0)
+ prism (~> 1.5)
mono_logger (1.1.2)
msgpack (1.8.0)
multi_json (1.15.0)
mustermann (3.0.4)
ruby2_keywords (~> 0.0.1)
- net-imap (0.5.11)
+ net-imap (0.6.4)
date
net-protocol
net-pop (0.1.2)
@@ -272,7 +283,7 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.5)
- nokogiri (1.19.1)
+ nokogiri (1.19.3)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
observer (0.1.2)
@@ -281,12 +292,14 @@ GEM
ast (~> 2.4.1)
racc
pdf-core (0.10.0)
- pg (1.6.2)
+ pg (1.6.3)
+ pghero (3.8.0)
+ activerecord (>= 7.2)
pkg-config (1.6.5)
pluck_to_hash (1.0.2)
activerecord (>= 4.0.2)
activesupport (>= 4.0.2)
- pp (0.6.2)
+ pp (0.6.3)
prettyprint
prawn (2.5.0)
matrix (~> 0.4)
@@ -296,16 +309,16 @@ GEM
prawn (>= 1)
rqrcode (>= 1.0.0)
prettyprint (0.2.0)
- prism (1.6.0)
- psych (5.2.6)
+ prism (1.9.0)
+ psych (5.3.1)
date
stringio
- public_suffix (6.0.2)
- puma (7.1.0)
+ public_suffix (7.0.5)
+ puma (7.2.0)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
- rack (3.2.5)
+ rack (3.2.6)
rack-cors (3.0.0)
logger
rack (>= 3.0.14)
@@ -313,27 +326,27 @@ GEM
base64 (>= 0.1.0)
logger (>= 1.6.0)
rack (>= 3.0.0, < 4)
- rack-session (2.1.1)
+ rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
- rackup (2.2.1)
+ rackup (2.3.1)
rack (>= 3)
- rails (8.0.3)
- actioncable (= 8.0.3)
- actionmailbox (= 8.0.3)
- actionmailer (= 8.0.3)
- actionpack (= 8.0.3)
- actiontext (= 8.0.3)
- actionview (= 8.0.3)
- activejob (= 8.0.3)
- activemodel (= 8.0.3)
- activerecord (= 8.0.3)
- activestorage (= 8.0.3)
- activesupport (= 8.0.3)
+ rails (8.1.2.1)
+ actioncable (= 8.1.2.1)
+ actionmailbox (= 8.1.2.1)
+ actionmailer (= 8.1.2.1)
+ actionpack (= 8.1.2.1)
+ actiontext (= 8.1.2.1)
+ actionview (= 8.1.2.1)
+ activejob (= 8.1.2.1)
+ activemodel (= 8.1.2.1)
+ activerecord (= 8.1.2.1)
+ activestorage (= 8.1.2.1)
+ activesupport (= 8.1.2.1)
bundler (>= 1.15.0)
- railties (= 8.0.3)
+ railties (= 8.1.2.1)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -345,16 +358,16 @@ GEM
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
- rails-i18n (8.0.2)
+ rails-i18n (8.1.0)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
- rails_performance (1.4.2)
+ rails_performance (1.6.0)
browser
railties
redis
- railties (8.0.3)
- actionpack (= 8.0.3)
- activesupport (= 8.0.3)
+ railties (8.1.2.1)
+ actionpack (= 8.1.2.1)
+ activesupport (= 8.1.2.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -363,24 +376,27 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
raindrops (0.20.0)
- rake (13.3.0)
+ rake (13.3.1)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
- rbs (3.9.5)
+ rbs (4.0.2)
logger
- rdoc (6.14.2)
+ prism (>= 1.6.0)
+ tsort
+ rdoc (7.2.0)
erb
psych (>= 4.0.0)
+ tsort
redcarpet (3.6.1)
redis (5.4.1)
redis-client (>= 0.22.0)
- redis-client (0.25.1)
+ redis-client (0.28.0)
connection_pool
redis-namespace (1.11.0)
redis (>= 4)
regexp_parser (2.9.0)
- reline (0.6.2)
+ reline (0.6.3)
io-console (~> 0.5)
responders (3.1.1)
actionpack (>= 5.2)
@@ -396,7 +412,7 @@ GEM
resque (>= 1.27)
rufus-scheduler (~> 3.2, != 3.3)
rexml (3.4.4)
- rmagick (6.1.4)
+ rmagick (6.2.0)
observer (~> 0.1)
pkg-config (~> 1.4)
rouge (4.1.3)
@@ -404,25 +420,25 @@ GEM
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
- rspec-core (3.13.5)
+ rspec-core (3.13.6)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
- rspec-mocks (3.13.5)
+ rspec-mocks (3.13.8)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
- rspec-rails (8.0.2)
+ rspec-rails (8.0.4)
actionpack (>= 7.2)
activesupport (>= 7.2)
railties (>= 7.2)
- rspec-core (~> 3.13)
- rspec-expectations (~> 3.13)
- rspec-mocks (~> 3.13)
- rspec-support (~> 3.13)
- rspec-support (3.13.5)
+ rspec-core (>= 3.13.0, < 5.0.0)
+ rspec-expectations (>= 3.13.0, < 5.0.0)
+ rspec-mocks (>= 3.13.0, < 5.0.0)
+ rspec-support (>= 3.13.0, < 5.0.0)
+ rspec-support (3.13.7)
rtesseract (3.1.4)
- ruby-lsp (0.26.2)
+ ruby-lsp (0.26.9)
language_server-protocol (~> 3.17.0)
prism (>= 1.2, < 2.0)
rbs (>= 3, < 5)
@@ -430,7 +446,7 @@ GEM
ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5)
ruby2_keywords (0.0.5)
- rubyzip (3.0.2)
+ rubyzip (3.2.2)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
rugged (1.9.0)
@@ -444,8 +460,8 @@ GEM
shoulda-callback-matchers (1.1.4)
activesupport (>= 3)
shoulda-context (3.0.0.rc1)
- shoulda-matchers (6.5.0)
- activesupport (>= 5.2.0)
+ shoulda-matchers (7.0.1)
+ activesupport (>= 7.1)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
@@ -460,7 +476,7 @@ GEM
rack-protection (= 4.2.0)
rack-session (>= 2.0.0, < 3)
tilt (~> 2.0)
- sorbet-runtime (0.5.12059)
+ sorbet-runtime (0.6.12935)
sprockets (4.2.2)
concurrent-ruby (~> 1.0)
logger
@@ -469,15 +485,15 @@ GEM
actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets (>= 3.0.0)
- stringio (3.1.7)
+ stringio (3.2.0)
terminal-table (4.0.0)
unicode-display_width (>= 1.1.1, < 4)
terser (1.2.6)
execjs (>= 0.3.0, < 3)
- thor (1.4.0)
+ thor (1.5.0)
tilt (2.6.1)
timecop (0.9.10)
- timeout (0.4.3)
+ timeout (0.6.1)
tsort (0.2.0)
ttfunk (1.8.0)
bigdecimal (~> 3.1)
@@ -490,7 +506,7 @@ GEM
kgio (~> 2.6)
raindrops (~> 0.7)
uniform_notifier (1.18.0)
- uri (1.1.0)
+ uri (1.1.1)
useragent (0.16.11)
webmock (3.26.1)
addressable (>= 2.8.0)
@@ -503,15 +519,17 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (2.7.3)
+ zeitwerk (2.7.5)
PLATFORMS
ruby
DEPENDENCIES
action_policy
+ active_record_doctor
activejob-status
activemodel-serializers-xml
+ annotaterb
autoprefixer-rails
awesome_print
better_errors
@@ -545,23 +563,24 @@ DEPENDENCIES
marcel
mini_mime
pg
+ pghero
pluck_to_hash
prawn
prawn-qrcode
puma
rack-cors
- rails (~> 8.0.3)
+ rails (~> 8.1.2)
rails-controller-testing
rails-html-sanitizer
- rails-i18n (~> 8.0.2)
+ rails-i18n (~> 8.1.0)
rails_performance
redcarpet
redis (~> 5.4.1)
responders
resque
resque-scheduler
- rmagick (~> 6.1.4)
- rspec-rails (~> 8.0.2)
+ rmagick (~> 6.2.0)
+ rspec-rails (~> 8.0.4)
rtesseract
ruby-lsp
rubyzip
@@ -569,7 +588,7 @@ DEPENDENCIES
selenium-webdriver
shoulda-callback-matchers (~> 1.1.1)
shoulda-context (~> 3.0.0.rc1)
- shoulda-matchers (~> 6.5)
+ shoulda-matchers (~> 7.0)
simplecov
simplecov-lcov
sprockets
diff --git a/app/MARKUS_VERSION b/app/MARKUS_VERSION
index 516ff4ebb3..0b16d2484d 100644
--- a/app/MARKUS_VERSION
+++ b/app/MARKUS_VERSION
@@ -1 +1 @@
-VERSION=v2.9.6,PATCH_LEVEL=DEV
+VERSION=v2.10.0,PATCH_LEVEL=DEV
diff --git a/app/assets/javascripts/Annotations/pdf_annotation_manager.js b/app/assets/javascripts/Annotations/pdf_annotation_manager.js
deleted file mode 100644
index e3334b727e..0000000000
--- a/app/assets/javascripts/Annotations/pdf_annotation_manager.js
+++ /dev/null
@@ -1,429 +0,0 @@
-(function (window) {
- "use strict";
-
- // CONSTANTS
- const MOUSE_OFFSET = 10; // The offset from the mouse cursor point
- const HIDE_BOX_THRESHOLD = 10; // Threshold for not displaying selection box in pixels
- const COORDINATE_PRECISION = 5; // Keep 5 decimal places (used when converting back from ints)
- const COORDINATE_MULTIPLIER = Math.pow(10, COORDINATE_PRECISION);
-
- /**
- * AnnotationManager subclass for PDF files.
- *
- * @param {boolean} enable_annotations Whether annotations can be modified
- */
- class PdfAnnotationManager extends AnnotationManager {
- constructor(enable_annotations) {
- // Members
- super();
- this.angle = 0; // current orientation of the PDF
-
- this.annotationControls = {}; // DOM elements added for annotations
-
- /** @type {{page: int, $control: HTMLElement}} */
- this.selectionBox = {};
-
- // The current selection area. The x, y are stored as percentages
- // for device independent resolution
- /** @type {{x: number, y: number, width: number, height: number}} */
- this.currentSelection = {};
-
- // Event Handlers
- if (enable_annotations) {
- this.bindPageEvents();
- }
- }
-
- /**
- * Get a selection for a specific page.
- * @return {{page: {int}, $control: {HTMLElement}}}
- */
- getSelectionBox($page) {
- let pageNumber = $page.data("page-number");
-
- if (
- this.selectionBox.page === pageNumber &&
- this.selectionBox.$control !== null &&
- $.contains(document, this.selectionBox.$control)
- ) {
- return this.selectionBox;
- } else if (this.selectionBox.$control != null) {
- this.selectionBox.$control.remove(); // Remove old control
- }
-
- let $control = document.createElement("div");
- $control.id = "sel_box";
- $control.addClass("annotation-holder-active");
- $control.style.display = "none";
-
- // append $control before the first annotation_holder but after the annotationLayer
- // or else you will be prevented from deleting/editing old annotations
- let $first_anno = $page.find(".annotation_holder:first")[0];
-
- if (!!$first_anno) {
- $first_anno.parentNode.insertBefore($control, $first_anno);
- } else {
- $page.append($control);
- }
-
- this.selectionBox = {
- page: pageNumber,
- $control: $control,
- };
-
- return this.selectionBox;
- }
-
- /**
- * Update and redraw the selection box.
- *
- * @param {jQuery} $page The page the control is for.
- * @param {{x, y, width, height, visible}} params Values to update
- */
- setSelectionBox($page, params) {
- if (params) {
- Object.assign(this.currentSelection, params);
-
- let sel_box = this.getSelectionBox($page).$control;
- sel_box.style.top = this.currentSelection.y * 100 + "%";
- sel_box.style.left = this.currentSelection.x * 100 + "%";
- sel_box.style.width = this.currentSelection.width * 100 + "%";
- sel_box.style.height = this.currentSelection.height * 100 + "%";
-
- sel_box.style.display = this.currentSelection.visible ? "block" : "none";
- }
- }
-
- hide_selection_box() {
- this.currentSelection.visible = false;
- if (this.selectionBox.$control) {
- this.selectionBox.$control.style.display = "none";
- }
- }
-
- /**
- * Returns the selection box coordinates (used when creating an annotation).
- * @returns {{x1, y1, x2, y2, page}}
- */
- getSelection(warn_no_selection = true) {
- let box = this.get_pdf_box_attrs();
- if (!box) {
- if (warn_no_selection) {
- alert(I18n.t("results.annotation.select_an_area_pdf"));
- }
- return false;
- }
- let page = box.page;
- box = getRotatedCoords(box, 360 - annotation_manager.angle);
-
- return {...box, page: page};
- }
-
- get_pdf_box_attrs() {
- let box = this.selectionRectangleAsInts();
- let boxSize = this.selectionBoxSize();
-
- if (!box || boxSize.width < HIDE_BOX_THRESHOLD || boxSize.height < HIDE_BOX_THRESHOLD) {
- return false;
- } else {
- return box;
- }
- }
-
- /**
- * Return the current selection bounding rectangle if there is one, and
- * converts the floating point percentages to integers.
- *
- * @return {{x1, y1, x2, y2, page}} Bounding box (points are in percentages)
- */
- selectionRectangleAsInts() {
- if (!this.currentSelection.visible) {
- return null;
- }
-
- return {
- x1: parseInt(this.currentSelection.x * COORDINATE_MULTIPLIER, 10),
- y1: parseInt(this.currentSelection.y * COORDINATE_MULTIPLIER, 10),
- x2: parseInt(
- (this.currentSelection.x + this.currentSelection.width) * COORDINATE_MULTIPLIER,
- 10
- ),
- y2: parseInt(
- (this.currentSelection.y + this.currentSelection.height) * COORDINATE_MULTIPLIER,
- 10
- ),
- page: this.selectionBox.page,
- };
- }
-
- /**
- * Get the selection box size in pixels. If there is no selection box
- * return {width: 0, height: 0}.
- *
- * @return {{width: int, height: int}} The selection box size in pixels
- */
- selectionBoxSize() {
- let $box = this.selectionBox.$control;
-
- return $box ? {width: $box.offsetWidth, height: $box.offsetHeight} : {width: 0, height: 0};
- }
-
- /**
- * Add an annotation to the PDF.
- *
- * @param {string} annotation_text_id [description]
- * @param {string} content [description]
- * @param {{x1: int, y1: int, x2: int, y2: int, page: int}} range
- * @param {string} annotation_id the id of the annotation
- * @param {boolean} is_remark
- */
- addAnnotation(annotation_text_id, content, range, annotation_id, is_remark) {
- let annotation = super.addAnnotation(
- annotation_text_id,
- content,
- range,
- annotation_id,
- is_remark
- );
-
- // Update display
- this.renderAnnotation(annotation);
- this.hide_selection_box();
- }
-
- /**
- * Remove an annotation
- * @param {string} annotation_id Annotation ID
- */
- removeAnnotation(annotation_id) {
- let annotation = super.removeAnnotation(annotation_id);
- if (annotation === undefined) {
- return;
- }
- this.annotationControls[annotation_id].remove(); // Delete DOM node
- }
-
- /**
- * Draw the annotation on the screen.
- *
- * @param {{annotation_id: string, annotation_text: Object, range: Object}} annotation
- */
- renderAnnotation = function (annotation) {
- let {annotation_id, annotation_text, range} = annotation;
- if (this.annotationControls[annotation_id]) {
- this.annotationControls[annotation_id].remove(); // Remove old controls
- }
-
- // The coords are in the unrotated form, but the PDF may be in a different orientation.
- let newCoords = getRotatedCoords(range, this.angle);
-
- let $control = document.createElement("div");
- $control.id = "annotation_holder_" + annotation_id;
- $control.addClass("annotation_holder");
- if (this.annotations[annotation_id].is_remark) {
- $control.addClass("remark");
- }
- $control.style.top = (newCoords.y1 / COORDINATE_MULTIPLIER) * 100 + "%";
- $control.style.left = (newCoords.x1 / COORDINATE_MULTIPLIER) * 100 + "%";
- $control.style.width = ((newCoords.x2 - newCoords.x1) / COORDINATE_MULTIPLIER) * 100 + "%";
- $control.style.height = ((newCoords.y2 - newCoords.y1) / COORDINATE_MULTIPLIER) * 100 + "%";
-
- let annotation_text_displayer = this.annotation_text_displayer;
- $control.onmousemove = function (ev) {
- let point = getRelativePointForEvent(ev, $page, -1);
-
- annotation_text_displayer.setDisplayNodeParent($page[0]);
- annotation_text_displayer.displayCollection(
- [annotation_text],
- point.x * 100,
- point.y * 100,
- "%"
- );
- };
-
- $control.onmouseleave = function () {
- annotation_text_displayer.hide();
- };
-
- this.annotationControls[annotation_id] = $control;
-
- let $page = $(`.page[data-page-number=${range.page}]`);
- $page.append($control);
- };
-
- /**
- * The following two functions are used to keep track of the orientation of
- * the PDF so we know how to render the annotations.
- */
- rotateClockwise90() {
- this.hide_selection_box();
- this.angle += 90;
- if (this.angle === 360) this.angle = 0;
- }
-
- resetAngle() {
- this.angle = 0;
- }
-
- /**
- * Set event handlers for PDF annotations.
- */
- bindPageEvents() {
- let $pages = $(".page");
- let selectionBoxActive = false; // Is the selection box in use
-
- // Start of click
- let start = {
- x: 0,
- y: 0,
- };
-
- // Helper: Start selection box
- const startSelection = (point, $target) => {
- this.setSelectionBox($target, {
- x: point.x,
- y: point.y,
- width: 0,
- height: 0,
- visible: true,
- });
-
- start = point;
- selectionBoxActive = true;
- };
-
- // Helper: Update selection box dimensions
- const updateSelection = (point, $target) => {
- if (!selectionBoxActive) {
- return;
- }
-
- this.setSelectionBox($target, {
- x: Math.min(start.x, point.x),
- y: Math.min(start.y, point.y),
- width: Math.abs(start.x - point.x),
- height: Math.abs(start.y - point.y),
- });
- };
-
- // Helper: End selection box
- const endSelection = () => {
- let size = this.selectionBoxSize();
-
- // If the box is REALLY small then hide it
- if (size.width < HIDE_BOX_THRESHOLD && size.height < HIDE_BOX_THRESHOLD) {
- this.hide_selection_box();
- }
-
- selectionBoxActive = false;
- };
-
- // Mouse event handlers
- $pages.mousedown(ev => {
- if (ev.which !== 1 && ev.target.id === "sel_box") {
- return;
- }
-
- let point = getRelativePointForEvent(ev);
- startSelection(point, $(ev.delegateTarget));
- });
-
- $pages.mousemove(ev => {
- let point = getRelativePointForEvent(ev);
- updateSelection(point, $(ev.delegateTarget));
- });
-
- $pages.mouseup(() => {
- endSelection();
- });
-
- // Touch event handlers
- $pages.on("touchstart", ev => {
- // Prevent default to avoid scrolling while annotating
- ev.preventDefault();
-
- let touch = ev.originalEvent.touches[0];
- let point = getRelativePointForEvent(touch, ev.delegateTarget, undefined);
- startSelection(point, $(ev.delegateTarget));
- });
-
- $pages.on("touchmove", ev => {
- // Prevent default to avoid scrolling while annotating
- ev.preventDefault();
-
- let touch = ev.originalEvent.touches[0];
- let point = getRelativePointForEvent(touch, ev.delegateTarget, undefined);
- updateSelection(point, $(ev.delegateTarget));
- });
-
- $pages.on("touchend", () => {
- endSelection();
- });
- }
- }
-
- /**
- * Returns the selection point in percentage units for a mouse or touch event.
- *
- * @param {Event|Touch} eventOrTouch The event or touch object.
- * @param {String|DOMNode|jQuery} relativeTo The element to calculate the offset for.
- * @param {number} mouseOffset Custom mouse offset value.
- * @return {{x: number, y:number}} The relative point in the element the event occurred in.
- */
- function getRelativePointForEvent(eventOrTouch, relativeTo, mouseOffset) {
- let $elem = relativeTo ? $(relativeTo) : $(eventOrTouch.delegateTarget);
- let offset = $elem.offset();
-
- let width = $elem.width();
- let height = $elem.height();
-
- let x = eventOrTouch.pageX - offset.left - (mouseOffset || MOUSE_OFFSET);
- let y = eventOrTouch.pageY - offset.top - (mouseOffset || MOUSE_OFFSET);
-
- return {
- x: 1 - (width - x) / width,
- y: 1 - (height - y) / height,
- };
- }
-
- /**
- * Returns the rotated coordinates of the annotation after applying
- * the rotation specified by angle.
- *
- * @param {{x1: int, y1: int, x2: int, y2: int, page: int}} coords
- * @param {number} angle
- */
- function getRotatedCoords(coords, angle) {
- let newCoords = {
- x1: coords.x1,
- x2: coords.x2,
- y1: coords.y1,
- y2: coords.y2,
- };
-
- switch (angle) {
- case 90:
- newCoords.x1 = COORDINATE_MULTIPLIER - coords.y2;
- newCoords.x2 = COORDINATE_MULTIPLIER - coords.y1;
- newCoords.y1 = coords.x1;
- newCoords.y2 = coords.x2;
- break;
- case 180:
- newCoords.x1 = COORDINATE_MULTIPLIER - coords.x2;
- newCoords.x2 = COORDINATE_MULTIPLIER - coords.x1;
- newCoords.y1 = COORDINATE_MULTIPLIER - coords.y2;
- newCoords.y2 = COORDINATE_MULTIPLIER - coords.y1;
- break;
- case 270:
- newCoords.x1 = coords.y1;
- newCoords.x2 = coords.y2;
- newCoords.y1 = COORDINATE_MULTIPLIER - coords.x2;
- newCoords.y2 = COORDINATE_MULTIPLIER - coords.x1;
- break;
- }
- return newCoords;
- }
-
- // Exports
- window.PdfAnnotationManager = PdfAnnotationManager;
-})(window);
diff --git a/app/assets/javascripts/Results/main.js b/app/assets/javascripts/Results/main.js
index e843d7bd39..fad1d3c2b5 100644
--- a/app/assets/javascripts/Results/main.js
+++ b/app/assets/javascripts/Results/main.js
@@ -1,15 +1,3 @@
-// Source code highlighting
-//= require Annotations/globals
-//= require Annotations/annotation_manager
-//= require Annotations/annotation_text
-//= require Annotations/annotation_text_displayer
-//= require Annotations/annotation_text_manager
-//= require Annotations/source_code_line
-//= require Annotations/text_annotation_manager
-//= require Annotations/image_annotation_manager
-//= require Annotations/pdf_annotation_manager
-//= require Annotations/html_annotations
-
// Page-specific Javascript
//= require Grader/marking
//= require panes
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
deleted file mode 100644
index 0f65548139..0000000000
--- a/app/assets/javascripts/application.js
+++ /dev/null
@@ -1,35 +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.
-//
-
-/** Helper functions for managing DOM elements' classes via pure JavaScript. */
-
-Element.prototype.addClass = function (className) {
- if (this.classList) {
- this.classList.add(className);
- } else {
- this.className += " " + className;
- }
-};
-
-Element.prototype.removeClass = function (className) {
- if (this.classList) {
- this.classList.remove(className);
- } else {
- this.className = this.className.replace(
- new RegExp("(^|\\b)" + className.split(" ").join("|") + "(\\b|$)", "gi"),
- " "
- );
- }
-};
-
-Element.prototype.hasClass = function (className) {
- if (this.classList) {
- return this.classList.contains(className);
- } else {
- return new RegExp("(^| )" + className + "( |$)", "gi").test(this.className);
- }
-};
diff --git a/app/assets/stylesheets/clickable.scss b/app/assets/stylesheets/clickable.scss
index f3c1be9b0f..0ad75d4824 100644
--- a/app/assets/stylesheets/clickable.scss
+++ b/app/assets/stylesheets/clickable.scss
@@ -1,4 +1,4 @@
-@import 'common/constants';
+@use 'common/constants' as *;
.clickable_links {
margin-left: 10px;
diff --git a/app/assets/stylesheets/common/_courses.scss b/app/assets/stylesheets/common/_courses.scss
index 5016b60171..b790758bbb 100644
--- a/app/assets/stylesheets/common/_courses.scss
+++ b/app/assets/stylesheets/common/_courses.scss
@@ -1,3 +1,6 @@
+@use 'constants' as *;
+@use 'mixins';
+
.course-list {
display: flex;
flex-flow: row wrap;
@@ -5,6 +8,10 @@
width: 100%;
}
+.course-list > h2 {
+ flex-basis: 100%;
+}
+
.course-card {
border: 2px solid $sub-menu;
border-radius: 5px;
@@ -18,7 +25,7 @@
max-width: 300px;
text-align: center;
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
max-width: 100%;
min-width: 300px;
}
diff --git a/app/assets/stylesheets/common/_criteria_filter.scss b/app/assets/stylesheets/common/_criteria_filter.scss
index 3c20280aca..274498abe8 100644
--- a/app/assets/stylesheets/common/_criteria_filter.scss
+++ b/app/assets/stylesheets/common/_criteria_filter.scss
@@ -1,4 +1,4 @@
-@import 'constants';
+@use 'constants' as *;
.criteria-filter {
border-left: $disabled-area solid 1.5px;
diff --git a/app/assets/stylesheets/common/_filter_modal.scss b/app/assets/stylesheets/common/_filter_modal.scss
index 29d6902a84..baa15195f2 100644
--- a/app/assets/stylesheets/common/_filter_modal.scss
+++ b/app/assets/stylesheets/common/_filter_modal.scss
@@ -1,4 +1,4 @@
-@import 'constants';
+@use 'constants' as *;
.filter-modal {
.filter-modal-title {
diff --git a/app/assets/stylesheets/common/_filter_modal_dropdown.scss b/app/assets/stylesheets/common/_filter_modal_dropdown.scss
index 8e5672d6ac..13ce02194c 100644
--- a/app/assets/stylesheets/common/_filter_modal_dropdown.scss
+++ b/app/assets/stylesheets/common/_filter_modal_dropdown.scss
@@ -1,6 +1,6 @@
-@import 'constants';
+@use 'constants' as *;
-@mixin filter-modal-dropdown {
+@mixin styles {
height: 30px;
width: $dropdown-horizontal;
diff --git a/app/assets/stylesheets/common/_icons.scss b/app/assets/stylesheets/common/_icons.scss
index 71e4b02637..1233bc81c0 100644
--- a/app/assets/stylesheets/common/_icons.scss
+++ b/app/assets/stylesheets/common/_icons.scss
@@ -1,3 +1,6 @@
+@use 'constants' as *;
+@use 'mixins';
+
.icon-left {
margin-right: 5px;
}
@@ -12,7 +15,7 @@ a[role='button'] > .svg-inline--fa {
padding-right: 5px;
vertical-align: baseline;
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
padding-right: 0;
}
@@ -27,7 +30,7 @@ a[role='button'] > .fa-layers {
margin-right: 5px;
vertical-align: baseline;
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
margin-right: 0;
}
diff --git a/app/assets/stylesheets/common/_login.scss b/app/assets/stylesheets/common/_login.scss
index fc6d94eabf..9f0bd36f7d 100644
--- a/app/assets/stylesheets/common/_login.scss
+++ b/app/assets/stylesheets/common/_login.scss
@@ -1,5 +1,5 @@
-@import 'mixins';
-@import 'constants';
+@use 'mixins';
+@use 'constants' as *;
/**
* Login screen
*/
@@ -17,7 +17,7 @@
width: 500px;
/* Mobile friendly! */
- @include breakpoint(tiny) {
+ @include mixins.breakpoint(tiny) {
border-radius: 0;
box-shadow: none;
left: 0;
diff --git a/app/assets/stylesheets/common/_markus.scss b/app/assets/stylesheets/common/_markus.scss
index 5e1bea2043..f3403cf831 100644
--- a/app/assets/stylesheets/common/_markus.scss
+++ b/app/assets/stylesheets/common/_markus.scss
@@ -1,25 +1,25 @@
@charset "UTF-8";
-@import 'reset';
-@import 'constants';
-@import 'columns';
-@import 'file_viewer';
-@import 'login';
-@import 'mixins';
-@import 'modals';
-@import 'notes_dialog';
-@import 'annotations_dialog';
-@import 'icons';
-@import 'react_json_schema_form';
-@import 'react_tabs';
-@import 'courses';
-@import 'url_viewer';
-@import 'statistics';
-@import 'criteria_filter';
-@import 'multi_select_dropdown';
-@import 'single_select_dropdown';
-@import 'filter_modal';
-@import 'table';
+@use 'reset';
+@use 'constants' as *;
+@use 'columns';
+@use 'file_viewer';
+@use 'login';
+@use 'mixins';
+@use 'modals';
+@use 'notes_dialog';
+@use 'annotations_dialog';
+@use 'icons';
+@use 'react_json_schema_form';
+@use 'react_tabs';
+@use 'courses';
+@use 'url_viewer';
+@use 'statistics';
+@use 'criteria_filter';
+@use 'multi_select_dropdown';
+@use 'single_select_dropdown';
+@use 'filter_modal';
+@use 'table';
/** Main */
@@ -29,7 +29,7 @@ body {
font: 400 0.8em/1.2em $fonts;
transition: background-color $time-quick;
- @include breakpoint(tiny) {
+ @include mixins.breakpoint(tiny) {
background-color: $background-main;
}
}
@@ -227,6 +227,15 @@ textarea {
}
}
+input[type='checkbox'] {
+ cursor: pointer;
+
+ &[disabled],
+ &[readonly] {
+ cursor: default;
+ }
+}
+
input {
padding: 0.25em 0.75em;
}
@@ -1290,7 +1299,7 @@ nav {
min-width: 100px;
padding: 0 0.4em;
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
min-width: 3em;
> .svg-inline--fa:only-child {
diff --git a/app/assets/stylesheets/common/_mixins.scss b/app/assets/stylesheets/common/_mixins.scss
index 12de1800f9..d1af8a93ac 100644
--- a/app/assets/stylesheets/common/_mixins.scss
+++ b/app/assets/stylesheets/common/_mixins.scss
@@ -1,4 +1,6 @@
/** For easier size breakpoints. */
+@use 'sass:map';
+@use 'sass:meta';
$breakpoints: (
'tiny': (
@@ -19,8 +21,8 @@ $breakpoints: (
);
@mixin breakpoint($name) {
- @if map-has-key($breakpoints, $name) {
- @media #{inspect(map-get($breakpoints, $name))} {
+ @if map.has-key($breakpoints, $name) {
+ @media #{meta.inspect(map.get($breakpoints, $name))} {
@content;
}
} @else {
diff --git a/app/assets/stylesheets/common/_modals.scss b/app/assets/stylesheets/common/_modals.scss
index f731afe8a3..16664118ee 100644
--- a/app/assets/stylesheets/common/_modals.scss
+++ b/app/assets/stylesheets/common/_modals.scss
@@ -1,4 +1,4 @@
-@import 'constants';
+@use 'constants' as *;
.react-modal {
background: $background-main;
diff --git a/app/assets/stylesheets/common/_multi_select_dropdown.scss b/app/assets/stylesheets/common/_multi_select_dropdown.scss
index 9f8a78f199..773cbb207d 100644
--- a/app/assets/stylesheets/common/_multi_select_dropdown.scss
+++ b/app/assets/stylesheets/common/_multi_select_dropdown.scss
@@ -1,8 +1,8 @@
-@import 'constants';
-@import 'filter_modal_dropdown';
+@use 'constants' as *;
+@use 'filter_modal_dropdown';
.dropdown.multi-select-dropdown {
- @include filter-modal-dropdown;
+ @include filter_modal_dropdown.styles;
padding: 0.25em;
.tags-box {
diff --git a/app/assets/stylesheets/common/_navigation.scss b/app/assets/stylesheets/common/_navigation.scss
index d82f875e74..60b4dfa815 100644
--- a/app/assets/stylesheets/common/_navigation.scss
+++ b/app/assets/stylesheets/common/_navigation.scss
@@ -1,7 +1,8 @@
/**
* Header, main menu, sub menu, and sub_sub menu.
*/
-@import 'constants';
+@use 'constants' as *;
+@use 'mixins';
/* Header */
@@ -12,14 +13,14 @@ header#header {
padding: 0.25em $dimen-horizontal-nav;
width: 100%;
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
border-bottom: 0;
display: block;
}
#course,
#user-info {
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
display: block;
}
}
@@ -63,7 +64,7 @@ nav {
min-height: 38px;
width: 90px;
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
display: none;
}
}
@@ -140,7 +141,7 @@ nav {
width: 100%;
z-index: 100000;
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
display: block;
}
@@ -178,7 +179,7 @@ nav {
#menus {
background: $background-support;
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
background: $sharp-line;
height: 100%;
overflow: hidden;
@@ -193,7 +194,7 @@ nav {
}
#menus_child {
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
background: $background-support;
border-right: 1px solid $primary-three;
height: 100%;
@@ -236,7 +237,7 @@ nav {
.main,
.sub,
.sub_sub {
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
background: $background-main;
border-bottom: 1px solid $primary-three;
border-top: 1px solid $primary-three;
@@ -246,7 +247,7 @@ nav {
}
li {
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
border-radius: 0;
text-align: left;
width: 100%;
@@ -257,7 +258,7 @@ nav {
.main,
.sub,
.sub_sub {
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
background: $background-main !important;
border-bottom: 1px solid $primary-three !important;
@@ -286,13 +287,13 @@ nav {
/* Header */
#header {
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
border: 0;
text-align: left !important;
}
#course {
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
display: block;
float: none !important;
padding: 1em;
@@ -300,7 +301,7 @@ nav {
}
#course {
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
border-bottom: 1px solid $primary-three;
}
}
@@ -319,7 +320,7 @@ nav {
}
li#logo {
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
display: none;
}
}
@@ -327,13 +328,13 @@ li#logo {
li#dropdown {
margin: 0.4em 0.5em 0.5em 0;
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
margin: 0 !important;
}
}
.dropdown {
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
border: 0;
border-radius: 0;
display: block;
diff --git a/app/assets/stylesheets/common/_notes_dialog.scss b/app/assets/stylesheets/common/_notes_dialog.scss
index 0966c0cea1..909348ea95 100644
--- a/app/assets/stylesheets/common/_notes_dialog.scss
+++ b/app/assets/stylesheets/common/_notes_dialog.scss
@@ -1,3 +1,5 @@
+@use 'constants' as *;
+
#notes_dialog {
max-height: 75%;
diff --git a/app/assets/stylesheets/common/_ocr_suggestions.scss b/app/assets/stylesheets/common/_ocr_suggestions.scss
new file mode 100644
index 0000000000..f9ea2dc672
--- /dev/null
+++ b/app/assets/stylesheets/common/_ocr_suggestions.scss
@@ -0,0 +1,42 @@
+// OCR Suggestions Styles
+// Used in assign_scans view for displaying OCR match data and student suggestions
+
+@use 'constants' as *;
+
+.ocr-suggestions-container {
+ margin: 1em 0;
+ padding: 1em;
+ background-color: $background-support;
+ border: 1px solid $gridline;
+ border-radius: var(--radius);
+ max-height: 400px;
+ overflow-y: auto;
+ overflow-x: hidden;
+
+ code {
+ background-color: $disabled-area;
+ padding: 0.2em 0.4em;
+ border-radius: 3px;
+ }
+
+ .no-match {
+ color: $disabled-text;
+ font-style: italic;
+ }
+}
+
+.ocr-suggestions-list {
+ margin-top: 0.5em;
+ position: static;
+
+ .ui-menu-item div:hover {
+ background-color: $primary-three;
+ color: $sharp-line;
+ }
+
+ .student-info {
+ font-size: 1.1em;
+ color: $sharp-line;
+ font-weight: 500;
+ }
+}
diff --git a/app/assets/stylesheets/common/_react_json_schema_form.scss b/app/assets/stylesheets/common/_react_json_schema_form.scss
index fda0b8d00e..810b968afa 100644
--- a/app/assets/stylesheets/common/_react_json_schema_form.scss
+++ b/app/assets/stylesheets/common/_react_json_schema_form.scss
@@ -1,6 +1,8 @@
// Custom styling for forms generated by react-json-schema-form.
// Currently only used in the automated_tests/manage view.
+@use 'constants' as *;
+
.rjsf {
// Fieldsets and legends
fieldset {
@@ -39,7 +41,7 @@
}
}
- .array-item-add > .svg-inline--fa {
+ .rjsf-array-item-add > .svg-inline--fa {
padding-right: 0;
}
@@ -47,7 +49,7 @@
min-width: 150px;
}
- .array-item {
+ .rjsf-array-item {
border: 1px solid $primary-two;
border-radius: $radius;
display: flex;
@@ -161,7 +163,7 @@
width: initial;
}
- .array-item {
+ .rjsf-array-item {
border: 0;
}
}
diff --git a/app/assets/stylesheets/common/_react_tabs.scss b/app/assets/stylesheets/common/_react_tabs.scss
index fde07fb696..39986a824b 100644
--- a/app/assets/stylesheets/common/_react_tabs.scss
+++ b/app/assets/stylesheets/common/_react_tabs.scss
@@ -1,4 +1,4 @@
-@import 'constants';
+@use 'constants' as *;
.react-tabs__tab.react-tabs__tab--disabled {
display: none;
diff --git a/app/assets/stylesheets/common/_reset.scss b/app/assets/stylesheets/common/_reset.scss
index 4c41bcf4a2..91d936925c 100644
--- a/app/assets/stylesheets/common/_reset.scss
+++ b/app/assets/stylesheets/common/_reset.scss
@@ -3,7 +3,7 @@
/* ==========================================================================
HTML5 display definitions
========================================================================== */
-@import 'constants';
+@use 'constants' as *;
/*
* Corrects `block` display not defined in IE 6/7/8/9 and Firefox 3.
*/
diff --git a/app/assets/stylesheets/common/_single_select_dropdown.scss b/app/assets/stylesheets/common/_single_select_dropdown.scss
index 90422e23d4..d00364a362 100644
--- a/app/assets/stylesheets/common/_single_select_dropdown.scss
+++ b/app/assets/stylesheets/common/_single_select_dropdown.scss
@@ -1,8 +1,8 @@
-@import 'constants';
-@import 'filter_modal_dropdown';
+@use 'constants' as *;
+@use 'filter_modal_dropdown';
.dropdown.single-select-dropdown {
- @include filter-modal-dropdown;
+ @include filter_modal_dropdown.styles;
a {
display: inline-block;
diff --git a/app/assets/stylesheets/common/_table.scss b/app/assets/stylesheets/common/_table.scss
index 624f208e45..d5624d48c8 100644
--- a/app/assets/stylesheets/common/_table.scss
+++ b/app/assets/stylesheets/common/_table.scss
@@ -1,4 +1,5 @@
-@import 'constants';
+@use 'constants' as *;
+@use 'mixins';
// These styles are partially derived from React Table v6.
// https://github.com/TanStack/table/blob/v6/src/index.styl
@@ -616,7 +617,7 @@
min-width: 100px;
padding: 0 0.4em;
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
min-width: 3em;
}
}
@@ -650,3 +651,7 @@
padding: 10px 0;
text-align: center;
}
+
+.grid-wrapper {
+ justify-content: center;
+}
diff --git a/app/assets/stylesheets/common/_url_viewer.scss b/app/assets/stylesheets/common/_url_viewer.scss
index ae39226ce7..eea217d513 100644
--- a/app/assets/stylesheets/common/_url_viewer.scss
+++ b/app/assets/stylesheets/common/_url_viewer.scss
@@ -1,3 +1,5 @@
+@use 'constants' as *;
+
.url-container {
display: flex;
flex-flow: column wrap;
diff --git a/app/assets/stylesheets/common/codeviewer.scss b/app/assets/stylesheets/common/codeviewer.scss
index 59924c6216..4519d7ce3a 100644
--- a/app/assets/stylesheets/common/codeviewer.scss
+++ b/app/assets/stylesheets/common/codeviewer.scss
@@ -1,5 +1,5 @@
-@import 'mixins';
-@import 'constants';
+@use 'mixins';
+@use 'constants' as *;
.text-viewer-container {
.toolbar {
diff --git a/app/assets/stylesheets/common/core.scss b/app/assets/stylesheets/common/core.scss
index e5d864ff6b..bcecb9b2ce 100644
--- a/app/assets/stylesheets/common/core.scss
+++ b/app/assets/stylesheets/common/core.scss
@@ -1,7 +1,9 @@
-@import 'markus';
-@import 'constants';
-@import 'navigation';
-@import '../../../../node_modules/@fortawesome/fontawesome-svg-core/styles';
+@use 'markus';
+@use 'constants' as *;
+@use 'navigation';
+@use 'ocr_suggestions';
+@use 'mixins';
+@use '../../../../node_modules/@fortawesome/fontawesome-svg-core/styles';
#about_dialog {
max-height: 75%;
@@ -92,7 +94,7 @@ th.required::after {
background: $background-main;
padding: $dimen-vertical $dimen-horizontal;
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
margin-top: 3.5em;
padding: $dimen-vertical $dimen-hor-mobile;
}
@@ -139,7 +141,7 @@ th.required::after {
.heading_buttons {
display: table-cell;
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
display: block;
}
}
@@ -148,7 +150,7 @@ th.required::after {
text-align: right;
vertical-align: middle;
- @include breakpoint(mobile) {
+ @include mixins.breakpoint(mobile) {
text-align: left;
}
}
@@ -268,7 +270,7 @@ th.required::after {
word-break: break-all;
}
-@include breakpoint(mobile) {
+@include mixins.breakpoint(mobile) {
button,
.button,
.form-control {
diff --git a/app/assets/stylesheets/common/jupyterlab-markus-custom.scss b/app/assets/stylesheets/common/jupyterlab-markus-custom.scss
index 5a9320cc6b..4f7dacc625 100644
--- a/app/assets/stylesheets/common/jupyterlab-markus-custom.scss
+++ b/app/assets/stylesheets/common/jupyterlab-markus-custom.scss
@@ -1,5 +1,5 @@
/* MarkUs custom styling */
-@import 'constants';
+@use 'constants' as *;
.markus-annotation {
background-color: $light-alert;
diff --git a/app/assets/stylesheets/common/pdfjs_custom.scss b/app/assets/stylesheets/common/pdfjs_custom.scss
index 40b4a39c89..5bab50f247 100644
--- a/app/assets/stylesheets/common/pdfjs_custom.scss
+++ b/app/assets/stylesheets/common/pdfjs_custom.scss
@@ -1,3 +1,10 @@
+// pdfjs-dist v5 sets color-scheme: light dark on :root, which causes browsers
+// to render native form controls (checkboxes, radios) in dark mode system styling.
+// MarkUs manages its own dark theme manually, so we lock the scheme to light.
+:root {
+ color-scheme: only light !important;
+}
+
.pdfContainerParent {
position: relative;
overflow: auto;
diff --git a/app/assets/stylesheets/context_menu.scss b/app/assets/stylesheets/context_menu.scss
index cb43b8d62f..37fe971e36 100644
--- a/app/assets/stylesheets/context_menu.scss
+++ b/app/assets/stylesheets/context_menu.scss
@@ -1,4 +1,4 @@
-@import 'common/core';
+@use 'common/constants' as *;
.ui-menu {
background: $background-main;
diff --git a/app/assets/stylesheets/entrypoints/rmd.scss b/app/assets/stylesheets/entrypoints/rmd.scss
index e6fd46b24f..ecca8121c2 100644
--- a/app/assets/stylesheets/entrypoints/rmd.scss
+++ b/app/assets/stylesheets/entrypoints/rmd.scss
@@ -1,6 +1,6 @@
/* MarkUs custom styling */
@use 'fonts';
-@import '../common/constants';
+@use 'common/constants' as *;
.markus-annotation {
background-color: $light-alert;
diff --git a/app/assets/stylesheets/grader.scss b/app/assets/stylesheets/grader.scss
index 2a61dd3caa..1068376199 100644
--- a/app/assets/stylesheets/grader.scss
+++ b/app/assets/stylesheets/grader.scss
@@ -1,4 +1,5 @@
-@import 'common/core';
+@use 'common/constants' as *;
+@use 'common/mixins';
/* Peer Review Styles */
@@ -207,7 +208,7 @@
display: flex;
width: 100%;
- @include breakpoint(small) {
+ @include mixins.breakpoint(small) {
display: block;
}
}
@@ -227,7 +228,7 @@
#right-pane {
overflow: auto;
- @include breakpoint(small) {
+ @include mixins.breakpoint(small) {
display: block;
float: none;
width: 100% !important;
@@ -238,7 +239,7 @@
margin-right: 3px;
width: 70%;
- @include breakpoint(small) {
+ @include mixins.breakpoint(small) {
margin-bottom: 1em;
padding-right: 0;
}
@@ -248,7 +249,7 @@
margin-left: 3px;
width: 29.5%;
- @include breakpoint(small) {
+ @include mixins.breakpoint(small) {
padding-left: 0;
}
}
@@ -259,7 +260,7 @@
position: inherit;
width: 4px;
- @include breakpoint(small) {
+ @include mixins.breakpoint(small) {
display: none;
}
}
diff --git a/app/controllers/admin/courses_controller.rb b/app/controllers/admin/courses_controller.rb
index 4ded4cb437..00f6810b4c 100644
--- a/app/controllers/admin/courses_controller.rb
+++ b/app/controllers/admin/courses_controller.rb
@@ -60,6 +60,23 @@ def reset_autotest_connection
respond_with current_course, location: -> { edit_admin_course_path(current_course) }
end
+ def refresh_autotest_schema
+ settings = current_course.autotest_setting
+ if settings.nil?
+ flash_message(:error, I18n.t('automated_tests.no_autotest_settings'))
+ return respond_with current_course, location: -> { edit_admin_course_path(current_course) }
+ end
+
+ begin
+ schema_json = get_schema(settings)
+ settings.update!(schema: schema_json)
+ flash_message(:success, I18n.t('automated_tests.manage_connection.refresh_schema_success'))
+ rescue StandardError => e
+ flash_message(:error, I18n.t('automated_tests.manage_connection.refresh_schema_failure', error: e.message))
+ end
+ respond_with current_course, location: -> { edit_admin_course_path(current_course) }
+ end
+
def destroy_lti_deployment
deployment = LtiDeployment.find(params[:lti_deployment_id])
deployment.destroy!
@@ -69,7 +86,7 @@ def destroy_lti_deployment
private
def course_create_params
- params.require(:course).permit(:name, :is_hidden, :display_name, :max_file_size)
+ params.require(:course).permit(:name, :is_hidden, :display_name, :max_file_size, :start_at, :end_at)
end
def course_update_params
diff --git a/app/controllers/admin/main_admin_controller.rb b/app/controllers/admin/main_admin_controller.rb
index 8ce2bf63ba..228a3e01fd 100644
--- a/app/controllers/admin/main_admin_controller.rb
+++ b/app/controllers/admin/main_admin_controller.rb
@@ -6,7 +6,10 @@ class MainAdminController < ApplicationController
layout 'assignment_content'
def index
- @dashboards = [{ name: t('resque.dashboard'), path: admin_resque_path }]
+ @dashboards = [
+ { name: t('resque.dashboard'), path: admin_resque_path },
+ { name: t('pghero.dashboard'), path: admin_pghero_path }
+ ]
if Settings.rails_performance.enabled
@dashboards << { name: t('rails_performance.dashboard'), path: admin_performance_path }
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 1d47b5a736..7ca3263070 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -9,7 +9,9 @@ class UsersController < ApplicationController
def index
respond_to do |format|
format.html
- format.json { render json: visible_users.order(:created_at).to_json(only: DEFAULT_FIELDS) }
+ format.json do
+ render json: visible_users.order(:created_at).pluck_to_hash(*DEFAULT_FIELDS)
+ end
end
end
diff --git a/app/controllers/annotation_categories_controller.rb b/app/controllers/annotation_categories_controller.rb
index d9ccb45739..77a8916bdf 100644
--- a/app/controllers/annotation_categories_controller.rb
+++ b/app/controllers/annotation_categories_controller.rb
@@ -170,7 +170,7 @@ def download
disposition: 'attachment'
else
flash[:error] = t('download_errors.unrecognized_format',
- format: params[:format])
+ file_format: params[:format])
redirect_to action: 'index',
id: params[:id]
end
diff --git a/app/controllers/api/grade_entry_forms_controller.rb b/app/controllers/api/grade_entry_forms_controller.rb
index f4250ce488..dbc687864d 100644
--- a/app/controllers/api/grade_entry_forms_controller.rb
+++ b/app/controllers/api/grade_entry_forms_controller.rb
@@ -3,17 +3,32 @@ class GradeEntryFormsController < MainApiController
DEFAULT_FIELDS = [:id, :short_identifier, :description, :due_date, :is_hidden, :visible_on, :visible_until,
:show_total].freeze
- # Sends the contents of the specified grade entry form
+ # Returns the specified grade entry form
# Requires: id
+ # Default: returns CSV export (backward compatible)
+ # Optional: .json extension or Accept: application/json header for structured JSON with student grades
+ # user_names[] (filter to specific students)
def show
grade_entry_form = record
- send_data grade_entry_form.export_as_csv(current_role),
- type: 'text/csv',
- filename: "#{grade_entry_form.short_identifier}_grades_report.csv",
- disposition: 'inline'
- rescue ActiveRecord::RecordNotFound => e
- # could not find grade entry form
- render 'shared/http_status', locals: { code: '404', message: e }, status: :not_found
+ if grade_entry_form.nil?
+ render 'shared/http_status', locals: { code: '404', message:
+ 'Grade Entry Form not found' }, status: :not_found
+ return
+ end
+
+ user_names = params[:user_names].presence
+
+ respond_to do |format|
+ format.json do
+ render json: grade_entry_form.export_as_json(current_role, user_names: user_names)
+ end
+ format.any do
+ send_data grade_entry_form.export_as_csv(current_role, user_names: user_names),
+ type: 'text/csv',
+ filename: "#{grade_entry_form.short_identifier}_grades_report.csv",
+ disposition: 'inline'
+ end
+ end
end
def index
diff --git a/app/controllers/api/groups_controller.rb b/app/controllers/api/groups_controller.rb
index 60a0a194ac..538f5e444d 100644
--- a/app/controllers/api/groups_controller.rb
+++ b/app/controllers/api/groups_controller.rb
@@ -187,9 +187,9 @@ def annotations
'annotations.y2 as y2']
annotations = grouping_relation.left_joins(current_submission_used:
- [submission_files:
- [annotations:
- [annotation_text: :annotation_category]]])
+ [{ submission_files:
+ [{ annotations:
+ [{ annotation_text: :annotation_category }] }] }])
.where(assessment_id: params[:assignment_id])
.where.not('annotations.id': nil)
.pluck_to_hash(*pluck_keys)
diff --git a/app/controllers/api/roles_controller.rb b/app/controllers/api/roles_controller.rb
index db725c9da6..2d9f6cd2ee 100644
--- a/app/controllers/api/roles_controller.rb
+++ b/app/controllers/api/roles_controller.rb
@@ -137,15 +137,15 @@ def create_role
def update_role(role)
ApplicationRecord.transaction do
- if params[:section_name]
- if params[:section_name].empty?
+ if params.key?(:section_name)
+ if params[:section_name].blank?
role.section = nil
else
role.section = @current_course.sections.find_by!(name: params[:section_name])
end
end
- role.grace_credits = params[:grace_credits] if params[:grace_credits]
- role.hidden = params[:hidden].to_s.casecmp('true').zero? if params[:hidden]
+ role.grace_credits = params[:grace_credits] if params.key?(:grace_credits)
+ role.hidden = params[:hidden].to_s.casecmp('true').zero? if params.key?(:hidden)
role.save!
end
render 'shared/http_status', locals: { code: '200', message:
diff --git a/app/controllers/api/users_controller.rb b/app/controllers/api/users_controller.rb
index 8cb6de3f7f..9e11effc58 100644
--- a/app/controllers/api/users_controller.rb
+++ b/app/controllers/api/users_controller.rb
@@ -12,7 +12,9 @@ def index
respond_to do |format|
format.xml { render xml: users.to_xml(only: DEFAULT_FIELDS, root: :users, skip_types: true) }
- format.json { render json: users.to_json(only: DEFAULT_FIELDS) }
+ format.json do
+ render json: users.pluck_to_hash(*DEFAULT_FIELDS)
+ end
end
end
diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb
index 29812c121d..9029566850 100644
--- a/app/controllers/assignments_controller.rb
+++ b/app/controllers/assignments_controller.rb
@@ -1074,10 +1074,12 @@ def assignment_params
def duration_params
params.require(:assignment).permit(
assignment_properties_attributes: [
- duration: [
- :hours,
- :minutes
- ]
+ {
+ duration: [
+ :hours,
+ :minutes
+ ]
+ }
]
)
end
diff --git a/app/controllers/automated_tests_controller.rb b/app/controllers/automated_tests_controller.rb
index 5eaa251d4c..bf0870057c 100644
--- a/app/controllers/automated_tests_controller.rb
+++ b/app/controllers/automated_tests_controller.rb
@@ -155,8 +155,10 @@ def upload_files
end
begin
- upload_files_helper(new_folders, new_files, unzip: unzip,
- max_file_size: assignment.course.max_file_size) do |f|
+ upload_files_helper(new_folders,
+ new_files,
+ unzip: unzip,
+ max_file_size: assignment.course.max_file_size) do |f|
if f.is_a?(String) # is a directory
folder_path = FileHelper.checked_join(autotest_files_path, f)
if folder_path.nil?
diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb
index 857cf8704d..02bc4b98eb 100644
--- a/app/controllers/courses_controller.rb
+++ b/app/controllers/courses_controller.rb
@@ -128,8 +128,8 @@ def download_assignments
type: 'text/yml',
disposition: 'attachment')
else
- flash[:error] = t('download_errors.unrecognized_format', format: format)
- redirect_back_or_to(course_assignments_path(current_course))
+ flash[:error] = t('download_errors.unrecognized_format', file_format: format)
+ redirect_back(fallback_location: course_assignments_path(current_course))
end
end
diff --git a/app/controllers/grade_entry_forms_controller.rb b/app/controllers/grade_entry_forms_controller.rb
index a5f4ab4ae5..3783127bda 100644
--- a/app/controllers/grade_entry_forms_controller.rb
+++ b/app/controllers/grade_entry_forms_controller.rb
@@ -1,7 +1,6 @@
# The actions necessary for managing grade entry forms.
class GradeEntryFormsController < ApplicationController
- include GradeEntryFormsHelper
include RoutingHelper
before_action { authorize! }
@@ -307,4 +306,45 @@ def destroy
flash_message(:error, t('grade_entry_forms.failed_deletion'))
end
end
+
+ # Helper functions
+ private
+
+ # Removes items that have empty names (so they don't get updated)
+ def update_grade_entry_form_params(attributes)
+ grade_entry_items =
+ params[:grade_entry_form][:grade_entry_items_attributes]
+
+ unless grade_entry_items.nil?
+ # Delete items with empty name and out_of
+ grade_entry_items.delete_if { |_, item| item[:name].empty? && item[:out_of].empty? }
+ # Update the attributes hash
+ max_position = 1
+ grade_entry_items.each_value do |item|
+ # Some items are being deleted so don't update those
+ unless item[:_destroy] == 1
+ item[:position] = max_position
+ max_position += 1
+ end
+ end
+ end
+ attributes[:grade_entry_items_attributes] = grade_entry_items
+ grade_entry_form_params(attributes)
+ end
+
+ def grade_entry_form_params(attributes)
+ attributes.require(:grade_entry_form)
+ .permit(:description,
+ :message,
+ :due_date,
+ :show_total,
+ :short_identifier,
+ :is_hidden,
+ grade_entry_items_attributes: [:name,
+ :out_of,
+ :position,
+ :bonus,
+ :_destroy,
+ :id])
+ end
end
diff --git a/app/controllers/graders_controller.rb b/app/controllers/graders_controller.rb
index 2080faabce..6abbe339af 100644
--- a/app/controllers/graders_controller.rb
+++ b/app/controllers/graders_controller.rb
@@ -6,7 +6,7 @@ class GradersController < ApplicationController
{ ta_memberships: :role, inviter: :section }].freeze
# The names of the associations of criteria required by the view, which
# should be eagerly loaded.
- CRITERION_ASSOC = [criterion_ta_associations: :ta].freeze
+ CRITERION_ASSOC = [{ criterion_ta_associations: :ta }].freeze
before_action { authorize! }
@@ -136,7 +136,6 @@ def global_actions
if found_empty_submission
assign_all_graders(filtered_grouping_ids, grader_ids)
flash_now(:info, I18n.t('graders.group_submission_no_files'))
- head :ok
else
assign_all_graders(grouping_ids, grader_ids)
end
@@ -192,6 +191,7 @@ def global_actions
rescue StandardError => e
head :bad_request
flash_now(:error, e.message)
+ return
end
end
when 'criteria_table'
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 12cb41fd2a..b73b3165a2 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -147,12 +147,18 @@ def assign_scans
if num_valid == num_total
flash_message(:success, t('exam_templates.assign_scans.done'))
end
+ # Get OCR match data and suggestions if available
+ ocr_match = OcrMatchService.get_match(next_grouping.id)
+ ocr_suggestions = ocr_match ? OcrMatchService.get_suggestions(next_grouping.id, current_course.id) : []
+
@data = {
group_name: next_grouping.group.group_name,
grouping_id: next_grouping.id,
students: names,
num_total: num_total,
- num_valid: num_valid
+ num_valid: num_valid,
+ ocr_match: ocr_match,
+ ocr_suggestions: format_ocr_suggestions(ocr_suggestions)
}
next_file = next_grouping.current_submission_used.submission_files.find_by(filename: 'COVER.pdf')
if next_file.nil?
@@ -221,6 +227,8 @@ def assign_student_and_next
end
StudentMembership
.find_or_create_by(role: student, grouping: @grouping, membership_status: StudentMembership::STATUSES[:inviter])
+ # Clear OCR match data after successful assignment
+ OcrMatchService.clear_match(@grouping.id)
end
next_grouping
end
@@ -243,12 +251,18 @@ def next_grouping
if num_valid == num_total
flash_message(:success, t('exam_templates.assign_scans.done'))
end
+ # Get OCR match data and suggestions if available
+ ocr_match = OcrMatchService.get_match(next_grouping.id)
+ ocr_suggestions = ocr_match ? OcrMatchService.get_suggestions(next_grouping.id, current_course.id) : []
+
if !@grouping.nil? && next_grouping.id == @grouping.id
render json: {
grouping_id: next_grouping.id,
students: names,
num_total: num_total,
- num_valid: num_valid
+ num_valid: num_valid,
+ ocr_match: ocr_match,
+ ocr_suggestions: format_ocr_suggestions(ocr_suggestions)
}
else
data = {
@@ -256,7 +270,9 @@ def next_grouping
grouping_id: next_grouping.id,
students: names,
num_total: num_total,
- num_valid: num_valid
+ num_valid: num_valid,
+ ocr_match: ocr_match,
+ ocr_suggestions: format_ocr_suggestions(ocr_suggestions)
}
next_file = next_grouping.current_submission_used.submission_files.find_by(filename: 'COVER.pdf')
unless next_file.nil?
@@ -290,6 +306,7 @@ def upload
group_rows << row.compact_blank
end
+
if result[:invalid_lines].empty?
@current_job = CreateGroupsJob.perform_later assignment, group_rows
session[:job_id] = @current_job.job_id
@@ -473,8 +490,8 @@ def invite_member
if errors.blank?
to_invite.each do |i|
i = i.strip
- invited_user = current_course.students.joins(:user).where(hidden: false).find_by('users.user_name': i)
- if invited_user.receives_invite_emails?
+ invited_user = current_course.students.joins(:user).find_by('users.user_name': i)
+ if invited_user&.receives_invite_emails?
NotificationMailer.with(inviter: current_role,
invited: invited_user,
grouping: @grouping).grouping_invite_email.deliver_later
@@ -683,9 +700,6 @@ def add_member(student, grouping, assignment)
end
@bad_user_names = []
- if student.hidden
- raise I18n.t('groups.invite_member.errors.not_found', user_name: student.user_name)
- end
if student.has_accepted_grouping_for?(assignment.id)
raise I18n.t('groups.invite_member.errors.already_grouped', user_name: student.user_name)
end
@@ -699,7 +713,7 @@ def add_member(student, grouping, assignment)
# Generate a warning if a member is added to a group and they
# have fewer grace days credits than already used by that group
if student.remaining_grace_credits < grouping.grace_period_deduction_single
- @warning_grace_day = I18n.t('groups.grace_day_over_limit', group: grouping.group.group_name)
+ flash_message(:warning, I18n.t('groups.grace_day_over_limit', group: grouping.group.group_name))
end
grouping.reload
@@ -728,6 +742,19 @@ def remove_member(membership, grouping)
grouping.reload
end
+ # Format OCR suggestions for JSON response
+ def format_ocr_suggestions(ocr_suggestions)
+ ocr_suggestions.map do |s|
+ {
+ id: s[:student].id,
+ user_name: s[:student].user.user_name,
+ id_number: s[:student].user.id_number,
+ display_name: s[:student].user.display_name,
+ similarity: (s[:similarity] * 100).round(1)
+ }
+ end
+ end
+
# This override is necessary because this controller is acting as a controller
# for both groups and groupings.
#
diff --git a/app/controllers/lti_deployments_controller.rb b/app/controllers/lti_deployments_controller.rb
index 94f9ed7c8d..aca2a6576c 100644
--- a/app/controllers/lti_deployments_controller.rb
+++ b/app/controllers/lti_deployments_controller.rb
@@ -90,8 +90,10 @@ def redirect_login
client_id: lti_launch_data[:client_id],
deployment_id: lti_params[LtiDeployment::LTI_CLAIMS[:deployment_id]],
lms_course_name: lti_params[LtiDeployment::LTI_CLAIMS[:context]]['title'],
+ lms_term_name: lti_params[LtiDeployment::LTI_CLAIMS[:custom]]['term_name'],
lms_course_label: lti_params[LtiDeployment::LTI_CLAIMS[:context]]['label'],
lms_course_id: lti_params[LtiDeployment::LTI_CLAIMS[:custom]]['course_id'],
+ lms_course_sourcedid: lti_params[LtiDeployment::LTI_CLAIMS[:lis]]&.[]('course_offering_sourcedid'),
user_roles: lti_params[LtiDeployment::LTI_CLAIMS[:roles]] }
if lti_params.key?(LtiDeployment::LTI_CLAIMS[:rlid])
rlid = lti_params[LtiDeployment::LTI_CLAIMS[:rlid]]['id']
@@ -129,6 +131,8 @@ def redirect_login
lms_course_id: lti_data[:lms_course_id])
lti_deployment.update!(
lms_course_name: lti_data[:lms_course_name],
+ lms_course_sourcedid: lti_data[:lms_course_sourcedid],
+ lms_term_name: lti_data[:lms_term_name],
resource_link_id: lti_data[:resource_link_id]
)
session[:lti_course_label] = lti_data[:lms_course_label]
@@ -213,8 +217,9 @@ def create_course
return
end
- name = params['name'].gsub(/[^a-zA-Z0-9\-_]/, '-') # Sanitize name to comply with Course name validation
- new_course = Course.find_or_initialize_by(name: name)
+ course_code = params['name'].gsub(/[^a-zA-Z0-9\-_]/, '-') # Sanitize name to comply with Course name validation
+ full_name = LtiConfig.get_course_name(record, course_code)
+ new_course = Course.find_or_initialize_by(name: full_name)
unless new_course.new_record?
flash_message(:error, I18n.t('lti.course_exists'))
redirect_to choose_course_lti_deployment_path
@@ -235,6 +240,13 @@ def get_config
raise NotImplementedError
end
+ # Define default URL options to not include locale
+ def default_url_options(_options = {})
+ {}
+ end
+
+ private
+
# Takes a string and returns a URI corresponding to the redirect
# endpoint for the lms
def construct_redirect_with_port(url, endpoint: nil)
@@ -244,9 +256,4 @@ def construct_redirect_with_port(url, endpoint: nil)
referer_host = referer_host_with_port if referer.to_s.start_with?(referer_host_with_port)
URI("#{referer_host}#{endpoint}")
end
-
- # Define default URL options to not include locale
- def default_url_options(_options = {})
- {}
- end
end
diff --git a/app/controllers/marks_graders_controller.rb b/app/controllers/marks_graders_controller.rb
index edd11e5eac..2a150ecb00 100644
--- a/app/controllers/marks_graders_controller.rb
+++ b/app/controllers/marks_graders_controller.rb
@@ -86,7 +86,7 @@ def upload
def grader_mapping
grade_entry_form = GradeEntryForm.find(params[:grade_entry_form_id])
- students = Student.left_outer_joins(:user, grade_entry_students: [tas: :user])
+ students = Student.left_outer_joins(:user, grade_entry_students: [{ tas: :user }])
.where('grade_entry_students.assessment_id': grade_entry_form.id)
.order('users.user_name', 'users_roles.user_name')
.pluck('users.user_name', 'users_roles.user_name')
diff --git a/app/controllers/results_controller.rb b/app/controllers/results_controller.rb
index f428298a5f..e79829db7a 100644
--- a/app/controllers/results_controller.rb
+++ b/app/controllers/results_controller.rb
@@ -239,7 +239,7 @@ def show
data[:assignment_max_mark] = assignment.max_mark
end
data[:total] = marks_map.pluck('mark')
- data[:old_total] = old_marks.values_at(:mark).compact.sum
+ data[:old_total] = original_result&.get_total_mark || 0
# Tags
all_tags = assignment.tags.pluck_to_hash(:id, :name)
@@ -454,7 +454,8 @@ def update_mark
m_logger = MarkusLogger.instance
- if result_mark.update(mark: mark_value, override: !(mark_value.nil? && result_mark.deductive_annotations_absent?))
+ if result_mark.update(mark: mark_value, override: !(mark_value.nil? && result_mark.deductive_annotations_absent?),
+ last_updated_by: current_role)
m_logger.log("User '#{current_role.user_name}' updated mark for " \
"submission (id: #{submission.id}) of " \
diff --git a/app/controllers/starter_file_groups_controller.rb b/app/controllers/starter_file_groups_controller.rb
index d5b1e6c6bd..15eca5ac2e 100644
--- a/app/controllers/starter_file_groups_controller.rb
+++ b/app/controllers/starter_file_groups_controller.rb
@@ -50,8 +50,10 @@ def update_files
end
begin
- upload_files_helper(new_folders, new_files, unzip: unzip,
- max_file_size: assignment.course.max_file_size) do |f|
+ upload_files_helper(new_folders,
+ new_files,
+ unzip: unzip,
+ max_file_size: assignment.course.max_file_size) do |f|
if f.is_a?(String) # is a directory
folder_path = FileHelper.checked_join(target_path, f)
if folder_path.nil?
diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb
index f779bb43b1..8ee53a7e0e 100644
--- a/app/controllers/submissions_controller.rb
+++ b/app/controllers/submissions_controller.rb
@@ -441,8 +441,10 @@ def update_files
messages.concat msgs
end
- upload_files_helper(new_folders, new_files, unzip: unzip,
- max_file_size: @assignment.course.max_file_size) do |f|
+ upload_files_helper(new_folders,
+ new_files,
+ unzip: unzip,
+ max_file_size: @assignment.course.max_file_size) do |f|
if f.is_a?(String) # is a directory
authorize! to: :manage_subdirectories? # ensure user is authorized for directories in zip files
success, msgs = add_folder(f, current_role, repo, path: path, txn: txn, required_files: required_files)
diff --git a/app/helpers/assessments_helper.rb b/app/helpers/assessments_helper.rb
new file mode 100644
index 0000000000..234a831e0c
--- /dev/null
+++ b/app/helpers/assessments_helper.rb
@@ -0,0 +1,7 @@
+module AssessmentsHelper
+ def formatted_assessment_visibility_label(assessment, base_text)
+ return t('assignments.hidden', assignment_text: base_text) if assessment.currently_hidden?
+
+ base_text
+ end
+end
diff --git a/app/helpers/grade_entry_forms_helper.rb b/app/helpers/grade_entry_forms_helper.rb
deleted file mode 100644
index 4278f903ef..0000000000
--- a/app/helpers/grade_entry_forms_helper.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# Helper methods for grade entry forms
-
-module GradeEntryFormsHelper
- # Removes items that have empty names (so they don't get updated)
- def update_grade_entry_form_params(attributes)
- grade_entry_items =
- params[:grade_entry_form][:grade_entry_items_attributes]
-
- unless grade_entry_items.nil?
- # Delete items with empty name and out_of
- grade_entry_items.delete_if { |_, item| item[:name].empty? && item[:out_of].empty? }
- # Update the attributes hash
- max_position = 1
- grade_entry_items.each_value do |item|
- # Some items are being deleted so don't update those
- unless item[:_destroy] == 1
- item[:position] = max_position
- max_position += 1
- end
- end
- end
- attributes[:grade_entry_items_attributes] = grade_entry_items
- grade_entry_form_params(attributes)
- end
-
- private
-
- def grade_entry_form_params(attributes)
- attributes.require(:grade_entry_form)
- .permit(:description,
- :message,
- :due_date,
- :show_total,
- :short_identifier,
- :is_hidden,
- grade_entry_items_attributes: [:name,
- :out_of,
- :position,
- :bonus,
- :_destroy,
- :id])
- end
-end
diff --git a/app/helpers/lti_helper.rb b/app/helpers/lti_helper.rb
index 8a434f1987..92ce231787 100644
--- a/app/helpers/lti_helper.rb
+++ b/app/helpers/lti_helper.rb
@@ -34,7 +34,7 @@ def roster_sync(lti_deployment, role_types, can_create_users: false, can_create_
lms_active_data = []
lms_inactive_ids = []
member_info.each do |user|
- next if user['roles'].include?(LtiDeployment::LTI_ROLES['test_user'])
+ next if user['roles'].include?(LtiDeployment::LTI_ROLES[:test_user])
next if role_types.none? { |role| user['roles'].include?(role) }
if user['status'] == LtiDeployment::LTI_STATUSES[:inactive]
lms_inactive_ids << user['user_id']
@@ -190,6 +190,7 @@ def create_or_update_lti_assessment(lti_deployment, assessment)
resourceId: assessment.short_identifier,
scoreMaximum: assessment.max_mark.to_f
}
+ payload[:endDateTime] = assessment.due_date.iso8601 if assessment.due_date.present?
auth_data = lti_deployment.lti_client.get_oauth_token([LtiDeployment::LTI_SCOPES[:ags_lineitem]])
lineitem_service = lti_deployment.lti_services.find_by!(service_type: 'agslineitem')
lineitem_uri = URI(lineitem_service.url)
@@ -199,7 +200,8 @@ def create_or_update_lti_assessment(lti_deployment, assessment)
else
req = Net::HTTP::Post.new(lineitem_uri)
end
- req.set_form_data(payload)
+ req.content_type = 'application/vnd.ims.lis.v2.lineitem+json'
+ req.body = payload.to_json
res = lti_deployment.send_lti_request!(req, lineitem_uri, auth_data, [LtiDeployment::LTI_SCOPES[:ags_lineitem]])
line_item_data = JSON.parse(res.body)
line_item.update!(lti_line_item_id: line_item_data['id'])
diff --git a/app/helpers/upload_helper.rb b/app/helpers/upload_helper.rb
index 75e954c3dd..9d4dabab09 100644
--- a/app/helpers/upload_helper.rb
+++ b/app/helpers/upload_helper.rb
@@ -61,8 +61,11 @@ def upload_files_helper(new_folders, new_files, unzip: false,
entry_size = zf.size
if entry_size && max_file_size && entry_size > max_file_size
max_mb = (max_file_size / 1_000_000.0).round(2)
- raise StandardError, I18n.t('upload_errors.zip_entry_too_large',
- file_name: zf.name, max_size: max_mb)
+ raise StandardError, I18n.t(
+ 'upload_errors.zip_entry_too_large',
+ file_name: zf.name,
+ max_size: max_mb
+ )
end
if entry_size && max_zip_total_size
total_size += entry_size
@@ -84,8 +87,11 @@ def upload_files_helper(new_folders, new_files, unzip: false,
streamed_size += chunk.bytesize
if max_file_size && streamed_size > max_file_size
max_mb = (max_file_size / 1_000_000.0).round(2)
- raise StandardError, I18n.t('upload_errors.zip_entry_too_large',
- file_name: zf.name, max_size: max_mb)
+ raise StandardError, I18n.t(
+ 'upload_errors.zip_entry_too_large',
+ file_name: zf.name,
+ max_size: max_mb
+ )
end
if entry_size.nil? && max_zip_total_size && total_size + streamed_size > max_zip_total_size
max_mb = (max_zip_total_size / 1_000_000.0).round(2)
diff --git a/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx b/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx
index f56f6eaf9a..3c72450b9a 100644
--- a/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx
+++ b/app/javascript/Components/Assessment_Chart/grade_breakdown_chart.jsx
@@ -1,41 +1,50 @@
import React from "react";
import {Bar} from "react-chartjs-2";
import {chartScales} from "../Helpers/chart_helpers";
-import ReactTable from "react-table";
+import Table from "../table/table";
+import {createColumnHelper} from "@tanstack/react-table";
import PropTypes from "prop-types";
import {CoreStatistics} from "./core_statistics";
import {FractionStat} from "./fraction_stat";
+const columnHelper = createColumnHelper();
+
export class GradeBreakdownChart extends React.Component {
render() {
+ const columns = [
+ columnHelper.accessor("position", {
+ id: "position",
+ enableColumnFilter: false,
+ }),
+ columnHelper.accessor("name", {
+ header: this.props.item_name,
+ minSize: 150,
+ enableColumnFilter: false,
+ }),
+ columnHelper.accessor("average", {
+ header: I18n.t("average"),
+ enableColumnFilter: false,
+ cell: info => (
+