From c3e36e11d392e1ad86d01d8a08e2eb0cd7ae3f7e Mon Sep 17 00:00:00 2001 From: donny-wong <141858744+donny-wong@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:33:47 -0400 Subject: [PATCH 01/86] update changelog with new release v2.8.0 [ci skip] (#7637) Co-authored-by: Donny Wong --- Changelog.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Changelog.md b/Changelog.md index b1bf4eb389..73d88d6a5e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,16 @@ ### 🚨 Breaking changes +### ✨ New features and improvements + +### 🐛 Bug fixes + +### 🔧 Internal changes + +## [v2.8.0] + +### 🚨 Breaking changes + ### ✨ New features and improvements - Improved layout and labeling in the assignment settings form for both standard and timed assessments. (#7531) - Design improvement of tables when the data is empty. (#7557) From 785f735a0f25a3acc685388d99c28da7a5c3ce43 Mon Sep 17 00:00:00 2001 From: David Liu Date: Fri, 15 Aug 2025 01:02:50 +0000 Subject: [PATCH 02/86] Upgraded to Rails v8.0.2.1 (#7640) --- Gemfile | 2 +- Gemfile.lock | 110 +++++++++++++++++++++++++-------------------------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/Gemfile b/Gemfile index 26cf1147fb..508913d260 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.2' +gem 'rails', '~> 8.0.2.1' gem 'sprockets' gem 'sprockets-rails' diff --git a/Gemfile.lock b/Gemfile.lock index cf5b6c3f48..51efc574eb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,29 +3,29 @@ GEM specs: action_policy (0.7.5) ruby-next-core (>= 1.0) - actioncable (8.0.2) - actionpack (= 8.0.2) - activesupport (= 8.0.2) + actioncable (8.0.2.1) + actionpack (= 8.0.2.1) + activesupport (= 8.0.2.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.2) - actionpack (= 8.0.2) - activejob (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) + actionmailbox (8.0.2.1) + actionpack (= 8.0.2.1) + activejob (= 8.0.2.1) + activerecord (= 8.0.2.1) + activestorage (= 8.0.2.1) + activesupport (= 8.0.2.1) mail (>= 2.8.0) - actionmailer (8.0.2) - actionpack (= 8.0.2) - actionview (= 8.0.2) - activejob (= 8.0.2) - activesupport (= 8.0.2) + actionmailer (8.0.2.1) + actionpack (= 8.0.2.1) + actionview (= 8.0.2.1) + activejob (= 8.0.2.1) + activesupport (= 8.0.2.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.2) - actionview (= 8.0.2) - activesupport (= 8.0.2) + actionpack (8.0.2.1) + actionview (= 8.0.2.1) + activesupport (= 8.0.2.1) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -33,42 +33,42 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.2) - actionpack (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) + actiontext (8.0.2.1) + actionpack (= 8.0.2.1) + activerecord (= 8.0.2.1) + activestorage (= 8.0.2.1) + activesupport (= 8.0.2.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.2) - activesupport (= 8.0.2) + actionview (8.0.2.1) + activesupport (= 8.0.2.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.0.2) - activesupport (= 8.0.2) + activejob (8.0.2.1) + activesupport (= 8.0.2.1) globalid (>= 0.3.6) activejob-status (1.0.1) activejob (>= 6.0) activesupport (>= 6.0) - activemodel (8.0.2) - activesupport (= 8.0.2) + activemodel (8.0.2.1) + activesupport (= 8.0.2.1) activemodel-serializers-xml (1.0.3) activemodel (>= 5.0.0.a) activesupport (>= 5.0.0.a) builder (~> 3.1) - activerecord (8.0.2) - activemodel (= 8.0.2) - activesupport (= 8.0.2) + activerecord (8.0.2.1) + activemodel (= 8.0.2.1) + activesupport (= 8.0.2.1) timeout (>= 0.4.0) - activestorage (8.0.2) - actionpack (= 8.0.2) - activejob (= 8.0.2) - activerecord (= 8.0.2) - activesupport (= 8.0.2) + activestorage (8.0.2.1) + actionpack (= 8.0.2.1) + activejob (= 8.0.2.1) + activerecord (= 8.0.2.1) + activesupport (= 8.0.2.1) marcel (~> 1.0) - activesupport (8.0.2) + activesupport (8.0.2.1) base64 benchmark (>= 0.3) bigdecimal @@ -318,20 +318,20 @@ GEM rack (>= 1.3) rackup (2.2.1) rack (>= 3) - rails (8.0.2) - actioncable (= 8.0.2) - actionmailbox (= 8.0.2) - actionmailer (= 8.0.2) - actionpack (= 8.0.2) - actiontext (= 8.0.2) - actionview (= 8.0.2) - activejob (= 8.0.2) - activemodel (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) + rails (8.0.2.1) + actioncable (= 8.0.2.1) + actionmailbox (= 8.0.2.1) + actionmailer (= 8.0.2.1) + actionpack (= 8.0.2.1) + actiontext (= 8.0.2.1) + actionview (= 8.0.2.1) + activejob (= 8.0.2.1) + activemodel (= 8.0.2.1) + activerecord (= 8.0.2.1) + activestorage (= 8.0.2.1) + activesupport (= 8.0.2.1) bundler (>= 1.15.0) - railties (= 8.0.2) + railties (= 8.0.2.1) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -350,9 +350,9 @@ GEM browser railties redis - railties (8.0.2) - actionpack (= 8.0.2) - activesupport (= 8.0.2) + railties (8.0.2.1) + actionpack (= 8.0.2.1) + activesupport (= 8.0.2.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -487,7 +487,7 @@ GEM crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) websocket (1.2.11) - websocket-driver (0.7.7) + websocket-driver (0.8.0) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -540,7 +540,7 @@ DEPENDENCIES prawn-qrcode puma rack-cors - rails (~> 8.0.2) + rails (~> 8.0.2.1) rails-controller-testing rails-html-sanitizer rails-i18n (~> 8.0.1) From d041d39f8d0e9f5ec9a4bde65df19ac21fa3067d Mon Sep 17 00:00:00 2001 From: ch-iv <108201575+ch-iv@users.noreply.github.com> Date: Sun, 17 Aug 2025 22:15:59 -0400 Subject: [PATCH 03/86] Upgraded Rubocop to target Ruby version 3.1 (#7643) --- .rubocop.yml | 2 +- app/controllers/api/main_api_controller.rb | 2 +- app/controllers/application_controller.rb | 4 ++-- app/models/grade_entry_student.rb | 4 ++-- app/models/group.rb | 4 ++-- config/initializers/version.rb | 2 +- spec/routing/routes_spec.rb | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 3241e00a86..61bea3be0b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -15,7 +15,7 @@ AllCops: - 'Vagrantfile' NewCops: enable SuggestExtensions: false - TargetRubyVersion: 2.7 + TargetRubyVersion: 3.1 Capybara/ClickLinkOrButtonStyle: # TODO: Enable and fix errors after reviewing system tests Enabled: false diff --git a/app/controllers/api/main_api_controller.rb b/app/controllers/api/main_api_controller.rb index b2d2d3c426..bd0cc634a4 100644 --- a/app/controllers/api/main_api_controller.rb +++ b/app/controllers/api/main_api_controller.rb @@ -17,7 +17,7 @@ class MainApiController < ActionController::Base # rubocop:disable Rails/Applica before_action { authorize! } AUTHTYPE = 'MarkUsAuth'.freeze - AUTH_TOKEN_REGEX = /#{AUTHTYPE} ([^\s,]+)/.freeze + AUTH_TOKEN_REGEX = /#{AUTHTYPE} ([^\s,]+)/ def page_not_found(message = HttpStatusHelper::ERROR_CODE['message']['404']) render 'shared/http_status', diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d1b3575409..98b82b522f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -56,8 +56,8 @@ def page_not_found protected - def use_time_zone(&block) - Time.use_zone(current_user.time_zone, &block) + def use_time_zone(&) + Time.use_zone(current_user.time_zone, &) end # Set locale according to URL parameter. If unknown parameter is diff --git a/app/models/grade_entry_student.rb b/app/models/grade_entry_student.rb index bba43a95ec..1a1504bdaf 100644 --- a/app/models/grade_entry_student.rb +++ b/app/models/grade_entry_student.rb @@ -82,7 +82,7 @@ def self.assign_all_tas(student_ids, ta_ids, form) # # Instances of the join model GradeEntryStudent are created if they do not # exist. - def self.assign_tas(student_ids, ta_ids, form, &block) + def self.assign_tas(student_ids, ta_ids, form, &) # Create non-existing grade entry students. merge_non_existing(student_ids, form.id) do |sids, form_ids| # Pair a single form ID with each student ID. @@ -91,7 +91,7 @@ def self.assign_tas(student_ids, ta_ids, form, &block) # Create non-existing grade entry student TA associations. ges_ids = form.grade_entry_students.where(role_id: student_ids).ids - GradeEntryStudentTa.merge_non_existing(ges_ids, ta_ids, &block) + GradeEntryStudentTa.merge_non_existing(ges_ids, ta_ids, &) end def self.unassign_tas(student_ids, grader_ids, form) diff --git a/app/models/group.rb b/app/models/group.rb index 586bc99213..396489d075 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -79,8 +79,8 @@ def repo_path end # Yields a repository object, if possible, and closes it after it is finished - def access_repo(&block) - Repository.get_class.access(repo_path, &block) + def access_repo(&) + Repository.get_class.access(repo_path, &) end private diff --git a/config/initializers/version.rb b/config/initializers/version.rb index 2466a9096d..00d785e039 100644 --- a/config/initializers/version.rb +++ b/config/initializers/version.rb @@ -1,7 +1,7 @@ # read MarkUs version from app/MARKUS_VERSION and set it as a configuration variable class VersionReader - VERSION_REGEX = /master|v\d+\.\d+\.\d+/.freeze + VERSION_REGEX = /master|v\d+\.\d+\.\d+/ def self.read_version version_file = Rails.root.join('app/MARKUS_VERSION').expand_path unless File.exist?(version_file) diff --git a/spec/routing/routes_spec.rb b/spec/routing/routes_spec.rb index f9f655bcb4..4ac12671e8 100644 --- a/spec/routing/routes_spec.rb +++ b/spec/routing/routes_spec.rb @@ -1,7 +1,7 @@ describe 'routing' do Rails.application.routes.routes.map do |r| spec = r.path.spec.to_s - next if spec.match %r{^/rails|^/assets|^/cable|.*\*path\(.:format\)}.freeze + next if %r{^/rails|^/assets|^/cable|.*\*path\(.:format\)}.match?(spec) r.verb.split('|').each do |verb| it "#{verb}: #{r.path.spec}" do parts = r.required_parts.index_with { |_part| '1' } From 7398e680dc2d248ee41c3ae30079746023715dc1 Mon Sep 17 00:00:00 2001 From: Freya Zhang <149322974+freyazjiner@users.noreply.github.com> Date: Mon, 18 Aug 2025 08:27:43 -0400 Subject: [PATCH 04/86] Migrated assignment summary table to react-table v8 (#7630) --- Changelog.md | 1 + .../__tests__/assignment_summary.test.jsx | 95 +++- .../Components/assignment_summary_table.jsx | 417 +++++++++++------- app/javascript/Components/table/table.jsx | 56 ++- 4 files changed, 412 insertions(+), 157 deletions(-) diff --git a/Changelog.md b/Changelog.md index 73d88d6a5e..424707692d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -9,6 +9,7 @@ ### 🐛 Bug fixes ### 🔧 Internal changes +- Updated the assignment summary table to use `@tanstack/react-table` v8 (#7630) ## [v2.8.0] diff --git a/app/javascript/Components/__tests__/assignment_summary.test.jsx b/app/javascript/Components/__tests__/assignment_summary.test.jsx index 6a5ada78b4..7bef195139 100644 --- a/app/javascript/Components/__tests__/assignment_summary.test.jsx +++ b/app/javascript/Components/__tests__/assignment_summary.test.jsx @@ -3,7 +3,8 @@ */ import {AssignmentSummaryTable} from "../assignment_summary_table"; -import {render, screen, fireEvent, waitFor} from "@testing-library/react"; +import {render, screen, fireEvent, waitFor, act} from "@testing-library/react"; +import {expect} from "@jest/globals"; describe("For the AssignmentSummaryTable's display of inactive groups", () => { let groups_sample; @@ -171,3 +172,95 @@ describe("For the AssignmentSummaryTable's display of an assignment without auto }); }); }); + +describe("For the AssignmentSummaryTable's subcomponent behavior", () => { + let groups_sample; + beforeEach(async () => { + groups_sample = [ + { + group_name: "group_0001", + section: null, + members: [ + ["c9nielse", "Nielsen", "Carl", true], + ["c8szyman", "Szymanowski", "Karol", true], + ], + tags: [], + graders: [["c9varoqu", "Nelle", "Varoquaux"]], + marking_state: "released", + final_grade: 9.0, + criteria: {1: 0.0}, + max_mark: "21.0", + result_id: 15, + submission_id: 15, + total_extra_marks: null, + }, + { + group_name: "group_0002", + section: "LEC0101", + members: [ + ["c8debuss", "Debussy", "Claude", false], + ["c8holstg", "Holst", "Gustav", false], + ], + tags: [], + graders: [["c9varoqu", "Nelle", "Varoquaux"]], + marking_state: "released", + final_grade: 6.0, + criteria: {1: 2.0}, + max_mark: "21.0", + result_id: 5, + submission_id: 5, + total_extra_marks: null, + }, + ]; + fetch.mockReset(); + fetch.mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ + data: groups_sample, + criteriaColumns: [ + { + Header: "dolores", + accessor: "criteria.1", + className: "number", + headerClassName: "", + }, + ], + numAssigned: 2, + numMarked: 2, + ltiDeployments: [], + }), + }); + + await act(async () => { + render( + + ); + }); + }); + + it("should initially hide grader subcomponent content", () => { + expect( + screen.queryByText(I18n.t("activerecord.models.ta", {count: groups_sample[0].graders.length})) + ).not.toBeInTheDocument(); + expect( + screen.queryByText(I18n.t("activerecord.models.ta", {count: groups_sample[1].graders.length})) + ).not.toBeInTheDocument(); + }); + + it("should show grader subcomponent when expanded", async () => { + const expanders = await screen.findAllByTestId("expander-button"); + + fireEvent.click(expanders[0]); + + await waitFor(() => { + expect( + screen.getByText(I18n.t("activerecord.models.ta", {count: groups_sample[0].graders.length})) + ).toBeInTheDocument(); + }); + }); +}); diff --git a/app/javascript/Components/assignment_summary_table.jsx b/app/javascript/Components/assignment_summary_table.jsx index e822162aba..73cd584156 100644 --- a/app/javascript/Components/assignment_summary_table.jsx +++ b/app/javascript/Components/assignment_summary_table.jsx @@ -1,13 +1,14 @@ import React from "react"; -import {markingStateColumn, getMarkingStates} from "./Helpers/table_helpers"; +import {getMarkingStates, selectFilter} from "./Helpers/table_helpers"; -import ReactTable from "react-table"; import DownloadTestResultsModal from "./Modals/download_test_results_modal"; import LtiGradeModal from "./Modals/send_lti_grades_modal"; +import {createColumnHelper} from "@tanstack/react-table"; +import Table from "./table/table"; export class AssignmentSummaryTable extends React.Component { - constructor() { - super(); + constructor(props) { + super(props); const markingStates = getMarkingStates([]); this.state = { data: [], @@ -21,7 +22,7 @@ export class AssignmentSummaryTable extends React.Component { showDownloadTestsModal: false, showLtiGradeModal: false, lti_deployments: [], - filtered: [], + columnFilters: [{id: "inactive", value: false}], inactiveGroupsCount: 0, }; } @@ -30,16 +31,205 @@ export class AssignmentSummaryTable extends React.Component { this.fetchData(); } - toggleShowInactiveGroups = showInactiveGroups => { - let filtered = this.state.filtered.filter(group => { - group.id !== "inactive"; + getColumns = () => { + const columnHelper = createColumnHelper(); + + const fixedColumns = [ + columnHelper.accessor("inactive", { + id: "inactive", + }), + columnHelper.accessor("group_name", { + id: "group_name", + header: () => I18n.t("activerecord.models.group.one"), + size: 100, + enableResizing: true, + cell: props => { + if (props.row.original.result_id) { + const path = Routes.edit_course_result_path( + this.props.course_id, + props.row.original.result_id + ); + return ( + + {props.getValue()} + {this.memberDisplay(props.getValue(), props.row.original.members)} + + ); + } else { + return ( + + {props.row.original.group_name} + {this.memberDisplay(props.row.original.group_name, props.row.original.members)} + + ); + } + }, + filterFn: (row, columnId, filterValue) => { + if (filterValue) { + // Check group name + if (row.original.group_name.includes(filterValue)) { + return true; + } + + // Check member names + const member_matches = row.original.members.some(member => + member.some(name => name.includes(filterValue)) + ); + + if (member_matches) { + return true; + } + + // Check grader user names + return row.original.graders.some(grader => grader.includes(filterValue)); + } else { + return true; + } + }, + }), + columnHelper.accessor("marking_state", { + header: () => I18n.t("activerecord.attributes.result.marking_state"), + accessorKey: "marking_state", + size: 100, + enableResizing: true, + cell: props => props.getValue(), + filterFn: "equalsString", + meta: { + filterVariant: "select", + }, + filterAllOptionText: + I18n.t("all") + + (this.state.markingStateFilter === "all" + ? ` (${Object.values(this.state.marking_states).reduce((a, b) => a + b)})` + : ""), + filterOptions: [ + { + value: "before_due_date", + text: + I18n.t("submissions.state.before_due_date") + + (["before_due_date", "all"].includes(this.state.markingStateFilter) + ? ` (${this.state.marking_states["before_due_date"]})` + : ""), + }, + { + value: "not_collected", + text: + I18n.t("submissions.state.not_collected") + + (["not_collected", "all"].includes(this.state.markingStateFilter) + ? ` (${this.state.marking_states["not_collected"]})` + : ""), + }, + { + value: "incomplete", + text: + I18n.t("submissions.state.in_progress") + + (["incomplete", "all"].includes(this.state.markingStateFilter) + ? ` (${this.state.marking_states["incomplete"]})` + : ""), + }, + { + value: "complete", + text: + I18n.t("submissions.state.complete") + + (["complete", "all"].includes(this.state.markingStateFilter) + ? ` (${this.state.marking_states["complete"]})` + : ""), + }, + { + value: "released", + text: + I18n.t("submissions.state.released") + + (["released", "all"].includes(this.state.markingStateFilter) + ? ` (${this.state.marking_states["released"]})` + : ""), + }, + { + value: "remark", + text: + I18n.t("submissions.state.remark_requested") + + (["remark", "all"].includes(this.state.markingStateFilter) + ? ` (${this.state.marking_states["remark"]})` + : ""), + }, + ], + Filter: selectFilter, + }), + columnHelper.accessor("tags", { + header: () => I18n.t("activerecord.models.tag.other"), + size: 90, + enableResizing: true, + cell: props => ( +
    + {props.row.original.tags.map(tag => ( +
  • + {tag} +
  • + ))} +
+ ), + minWidth: 80, + enableSorting: false, + filterFn: (row, columnId, filterValue) => { + if (filterValue) { + // Check tag names + return row.original.tags.some(tag => tag.includes(filterValue)); + } else { + return true; + } + }, + }), + columnHelper.accessor("final_grade", { + header: () => I18n.t("results.total_mark"), + size: 100, + enableResizing: true, + cell: props => { + if (props.row.original.final_grade || props.row.original.final_grade === 0) { + const max_mark = Math.round(props.row.original.max_mark * 100) / 100; + return props.row.original.final_grade + " / " + max_mark; + } else { + return ""; + } + }, + meta: {className: "number"}, + enableColumnFilter: false, + sortDescFirst: true, + }), + ]; + + const criteriaColumns = this.state.criteriaColumns.map(col => + columnHelper.accessor(col.accessor, { + id: col.id, + header: () => col.Header, + size: col.size || 100, + meta: { + className: col.className, + headerClassName: col.headerClassName, + }, + enableColumnFilter: col.enableColumnFilter, + sortDescFirst: col.sortDescFirst, + }) + ); + + const bonusColumn = columnHelper.accessor("total_extra_marks", { + header: () => I18n.t("activerecord.models.extra_mark.other"), + size: 100, + enableResizing: true, + meta: {className: "number"}, + enableColumnFilter: false, + sortDescFirst: true, }); + return [...fixedColumns, ...criteriaColumns, bonusColumn]; + }; + + toggleShowInactiveGroups = showInactiveGroups => { + let columnFilters = this.state.columnFilters.filter(group => group.id !== "inactive"); + if (!showInactiveGroups) { - filtered.push({id: "inactive", value: false}); + columnFilters.push({id: "inactive", value: false}); } - this.setState({filtered}); + this.setState({columnFilters}); }; memberDisplay = (group_name, members) => { @@ -57,11 +247,14 @@ export class AssignmentSummaryTable extends React.Component { }; fetchData = () => { - fetch(Routes.summary_course_assignment_path(this.props.course_id, this.props.assignment_id), { - headers: { - Accept: "application/json", - }, - }) + return fetch( + Routes.summary_course_assignment_path(this.props.course_id, this.props.assignment_id), + { + headers: { + Accept: "application/json", + }, + } + ) .then(response => { if (response.ok) { return response.json(); @@ -69,8 +262,8 @@ export class AssignmentSummaryTable extends React.Component { }) .then(res => { res.criteriaColumns.forEach(col => { - col["filterable"] = false; - col["defaultSortDesc"] = true; + col.enableColumnFilter = false; + col.sortDescFirst = true; }); let inactive_groups_count = 0; @@ -83,11 +276,10 @@ export class AssignmentSummaryTable extends React.Component { } }); - this.toggleShowInactiveGroups(false); - - const markingStates = getMarkingStates(res.data); + const processedData = this.processData(res.data); + const markingStates = getMarkingStates(processedData); this.setState({ - data: res.data, + data: processedData, criteriaColumns: res.criteriaColumns, num_assigned: res.numAssigned, num_marked: res.numMarked, @@ -100,6 +292,35 @@ export class AssignmentSummaryTable extends React.Component { }); }; + processData(data) { + data.forEach(row => { + switch (row.marking_state) { + case "not_collected": + row.marking_state = I18n.t("submissions.state.not_collected"); + break; + case "incomplete": + row.marking_state = I18n.t("submissions.state.in_progress"); + break; + case "complete": + row.marking_state = I18n.t("submissions.state.complete"); + break; + case "released": + row.marking_state = I18n.t("submissions.state.released"); + break; + case "remark": + row.marking_state = I18n.t("submissions.state.remark_requested"); + break; + case "before_due_date": + row.marking_state = I18n.t("submissions.state.before_due_date"); + break; + default: + // should not get here + row.marking_state = row.original.marking_state; + } + }); + return data; + } + onFilteredChange = (filtered, column) => { const summaryTable = this.wrappedInstance; if (column.id != "marking_state") { @@ -111,112 +332,6 @@ export class AssignmentSummaryTable extends React.Component { } }; - fixedColumns = () => { - return [ - { - show: false, - accessor: "inactive", - id: "inactive", - }, - { - Header: I18n.t("activerecord.models.group.one"), - id: "group_name", - accessor: "group_name", - Cell: row => { - if (row.original.result_id) { - const path = Routes.edit_course_result_path( - this.props.course_id, - row.original.result_id - ); - return ( - - {row.original.group_name} - {this.memberDisplay(row.original.group_name, row.original.members)} - - ); - } else { - return ( - - {row.original.group_name} - {this.memberDisplay(row.original.group_name, row.original.members)} - - ); - } - }, - filterMethod: (filter, row) => { - if (filter.value) { - // Check group name - if (row._original.group_name.includes(filter.value)) { - return true; - } - - // Check member names - const member_matches = row._original.members.some(member => - member.some(name => name.includes(filter.value)) - ); - - if (member_matches) { - return true; - } - - // Check grader user names - return row._original.graders.some(grader => grader.includes(filter.value)); - } else { - return true; - } - }, - }, - markingStateColumn(this.state.marking_states, this.state.markingStateFilter), - { - Header: I18n.t("activerecord.models.tag.other"), - accessor: "tags", - Cell: row => ( -
    - {row.original.tags.map(tag => ( -
  • - {tag} -
  • - ))} -
- ), - minWidth: 80, - sortable: false, - filterMethod: (filter, row) => { - if (filter.value) { - // Check tag names - return row._original.tags.some(tag => tag.includes(filter.value)); - } else { - return true; - } - }, - }, - { - Header: I18n.t("results.total_mark"), - accessor: "final_grade", - Cell: row => { - if (row.original.final_grade || row.original.final_grade === 0) { - const max_mark = Math.round(row.original.max_mark * 100) / 100; - return row.original.final_grade + " / " + max_mark; - } else { - return ""; - } - }, - className: "number", - filterable: false, - defaultSortDesc: true, - }, - ]; - }; - - bonusColumn = { - Header: I18n.t("activerecord.models.extra_mark.other"), - accessor: "total_extra_marks", - Cell: ({value}) => value, - className: "number", - filterable: false, - defaultSortDesc: true, - }; - onDownloadTestsModal = () => { this.setState({showDownloadTestsModal: true}); }; @@ -226,7 +341,7 @@ export class AssignmentSummaryTable extends React.Component { }; render() { - const {data, criteriaColumns} = this.state; + const {data} = this.state; let ltiButton; if (this.state.lti_deployments.length > 0) { ltiButton = ( @@ -304,30 +419,15 @@ export class AssignmentSummaryTable extends React.Component { )} - (this.wrappedInstance = r)} - SubComponent={row => { - return ( -
-

{I18n.t("activerecord.models.ta", {count: 2})}

-
    - {row.original.graders.map(grader => { - return ( -
  • - ({grader[0]}) {grader[1]} {grader[2]} -
  • - ); - })} -
-
- ); + columns={this.getColumns()} + initialState={{ + sorting: [{id: "group_name"}], }} + columnFilters={this.state.columnFilters} + getRowCanExpand={() => true} + renderSubComponent={renderSubComponent} loading={this.state.loading} /> { + return ( +
+

{I18n.t("activerecord.models.ta", {count: row.original.graders.length})}

+
    + {row.original.graders.map(grader => { + return ( +
  • + ({grader[0]}) {grader[1]} {grader[2]} +
  • + ); + })} +
+
+ ); +}; diff --git a/app/javascript/Components/table/table.jsx b/app/javascript/Components/table/table.jsx index 24febbc53b..e0bc2b1328 100644 --- a/app/javascript/Components/table/table.jsx +++ b/app/javascript/Components/table/table.jsx @@ -1,8 +1,10 @@ import React from "react"; import { + createColumnHelper, flexRender, getCoreRowModel, + getExpandedRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, @@ -13,28 +15,68 @@ import Filter from "./filter"; export const defaultNoDataText = () => I18n.t("table.no_data"); -export default function Table({columns, data, noDataText, initialState}) { +const columnHelper = createColumnHelper(); +export const expanderColumn = columnHelper.display({ + id: "expander", + header: () => null, + size: 32, + cell: ({row}) => { + return row.getCanExpand() ? ( +
+ ) : null; + }, +}); + +export default function Table({ + columns, + data, + noDataText, + initialState, + renderSubComponent, + getRowCanExpand, + columnFilters: externalColumnFilters, +}) { const [columnFilters, setColumnFilters] = React.useState([]); const [columnSizing, setColumnSizing] = React.useState({}); + const [columnVisibility, setColumnVisibility] = React.useState({inactive: false}); + const [expanded, setExpanded] = React.useState({}); + + React.useEffect(() => { + if (externalColumnFilters !== undefined) { + setColumnFilters(externalColumnFilters); + } + }, [externalColumnFilters]); + + const finalColumns = renderSubComponent ? [expanderColumn, ...columns] : columns; const table = useReactTable({ data, - columns, + columns: finalColumns, state: { columnFilters, columnSizing, + columnVisibility, + expanded, }, initialState: initialState, onColumnFiltersChange: setColumnFilters, + onColumnSizingChange: setColumnSizing, + onColumnVisibilityChange: setColumnVisibility, + onExpandedChange: setExpanded, getCoreRowModel: getCoreRowModel(), + getExpandedRowModel: getExpandedRowModel(), getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), getFacetedRowModel: getFacetedRowModel(), + getRowCanExpand, enableSortingRemoval: false, enableColumnResizing: true, columnResizeMode: "onChange", - onColumnSizingChange: setColumnSizing, }); return ( @@ -53,7 +95,7 @@ export default function Table({columns, data, noDataText, initialState}) { }[header.column.getIsSorted()]; return (
{ return (
{row.getVisibleCells().map(cell => { + const metaClass = cell.column.columnDef.meta?.className || ""; return (
+ {row.getIsExpanded() &&
{renderSubComponent({row})}
}
); })} From 899c1431b3b2c388c681765651dd61c4ad4d915a Mon Sep 17 00:00:00 2001 From: David Liu Date: Wed, 20 Aug 2025 02:08:45 +0000 Subject: [PATCH 05/86] Fixed group member filtering in assignment summary table (#7644) --- Changelog.md | 1 + app/javascript/Components/assignment_summary_table.jsx | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Changelog.md b/Changelog.md index 424707692d..0e7b0e62f7 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,7 @@ ### ✨ New features and improvements ### 🐛 Bug fixes +- Fixed group member filtering in assignment summary table (#7644) ### 🔧 Internal changes - Updated the assignment summary table to use `@tanstack/react-table` v8 (#7630) diff --git a/app/javascript/Components/assignment_summary_table.jsx b/app/javascript/Components/assignment_summary_table.jsx index 73cd584156..9769f32806 100644 --- a/app/javascript/Components/assignment_summary_table.jsx +++ b/app/javascript/Components/assignment_summary_table.jsx @@ -66,14 +66,15 @@ export class AssignmentSummaryTable extends React.Component { }, filterFn: (row, columnId, filterValue) => { if (filterValue) { + filterValue = filterValue.toLowerCase(); // Check group name - if (row.original.group_name.includes(filterValue)) { + if (row.original.group_name.toLowerCase().includes(filterValue)) { return true; } - // Check member names + // Check member names (first three values of each "member" array) const member_matches = row.original.members.some(member => - member.some(name => name.includes(filterValue)) + member.slice(0, 3).some(name => name.toLowerCase().includes(filterValue)) ); if (member_matches) { @@ -81,7 +82,7 @@ export class AssignmentSummaryTable extends React.Component { } // Check grader user names - return row.original.graders.some(grader => grader.includes(filterValue)); + return row.original.graders.some(grader => grader.toLowerCase().includes(filterValue)); } else { return true; } From 3d2b5b1d1891bbff2b3ac5dc1128a7fb22ad29e8 Mon Sep 17 00:00:00 2001 From: Omid Hemmati <129822623+hemmatio@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:54:45 -0400 Subject: [PATCH 06/86] Used cache-apt-pkgs-action to cache CI system dependencies (#7645) --- .github/workflows/test_ci.yml | 48 +++++++++++++++++++++++++++++++---- Changelog.md | 1 + 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test_ci.yml b/.github/workflows/test_ci.yml index 3a425ce1e6..f302b922e8 100644 --- a/.github/workflows/test_ci.yml +++ b/.github/workflows/test_ci.yml @@ -43,10 +43,49 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@v4 - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get -yqq install libpq-dev cmake ghostscript pandoc imagemagick libmagickwand-dev git libgl1 tesseract-ocr pandoc poppler-utils + - name: Install and cache system dependencies + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: | + libpq-dev + cmake + ghostscript + pandoc + imagemagick + libmagickwand-dev + git + libgl1 + tesseract-ocr + pandoc + poppler-utils + fonts-liberation + libasound2 + libatk-bridge2.0-0 + libatk1.0-0 + libatspi2.0-0 + libcairo2 + libcups2 + libdbus-1-3 + libdrm2 + libegl1 + libgbm1 + libglib2.0-0 + libgtk-3-0 + libnspr4 + libnss3 + libpango-1.0-0 + libx11-6 + libx11-xcb1 + libxcb1 + libxcomposite1 + libxdamage1 + libxext6 + libxfixes3 + libxrandr2 + libxshmfence1 + version: 1.0 + # 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 - name: Set up ruby and cache gems uses: ruby/setup-ruby@v1 with: @@ -86,7 +125,6 @@ jobs: python3.10 -m venv venv ./venv/bin/pip install -r requirements-jupyter.txt -r requirements-scanner.txt ./venv/bin/playwright install chromium - ./venv/bin/playwright install-deps chromium - name: Configure server run: | sudo rm -f /etc/localtime diff --git a/Changelog.md b/Changelog.md index 0e7b0e62f7..4e6be0b715 100644 --- a/Changelog.md +++ b/Changelog.md @@ -11,6 +11,7 @@ ### 🔧 Internal changes - Updated the assignment summary table to use `@tanstack/react-table` v8 (#7630) +- Updated Github Actions CI to use cache-apt-pkgs to speed up workflow runs (#7645) ## [v2.8.0] From 1a494af300426778481133506baf9e9f4b588984 Mon Sep 17 00:00:00 2001 From: David Liu Date: Fri, 29 Aug 2025 16:31:14 +0000 Subject: [PATCH 07/86] Fixed spacing issue for the remote authentication login button (#7646) --- Changelog.md | 1 + app/views/main/login.html.erb | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Changelog.md b/Changelog.md index 4e6be0b715..221b71c14c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,6 +8,7 @@ ### 🐛 Bug fixes - Fixed group member filtering in assignment summary table (#7644) +- Fixed spacing issue for the remote authentication login button (#7646) ### 🔧 Internal changes - Updated the assignment summary table to use `@tanstack/react-table` v8 (#7630) diff --git a/app/views/main/login.html.erb b/app/views/main/login.html.erb index a6610132c1..89c589d190 100644 --- a/app/views/main/login.html.erb +++ b/app/views/main/login.html.erb @@ -62,10 +62,8 @@ <% if Settings.remote_auth_login_url %> <% end %>
From 2c3cb36e0260c2d6ed7955980163d265881c3ad2 Mon Sep 17 00:00:00 2001 From: David Liu Date: Fri, 29 Aug 2025 20:25:50 +0000 Subject: [PATCH 08/86] Fixed API bug when creating binary submission files (#7647) --- Changelog.md | 1 + app/helpers/submissions_helper.rb | 2 +- .../api/submission_files_controller_spec.rb | 40 ++++++++++++++----- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/Changelog.md b/Changelog.md index 221b71c14c..1f8d44e3d9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -9,6 +9,7 @@ ### 🐛 Bug fixes - Fixed group member filtering in assignment summary table (#7644) - Fixed spacing issue for the remote authentication login button (#7646) +- Fixed API bug when creating binary submission files (#7647) ### 🔧 Internal changes - Updated the assignment summary table to use `@tanstack/react-table` v8 (#7630) diff --git a/app/helpers/submissions_helper.rb b/app/helpers/submissions_helper.rb index 665de26003..125fc5a49a 100644 --- a/app/helpers/submissions_helper.rb +++ b/app/helpers/submissions_helper.rb @@ -85,7 +85,7 @@ def upload_file(grouping, only_required_files: false) content = params[:file_content] end - tmpfile = Tempfile.new + tmpfile = Tempfile.new(binmode: true) begin tmpfile.write(content) tmpfile.rewind diff --git a/spec/controllers/api/submission_files_controller_spec.rb b/spec/controllers/api/submission_files_controller_spec.rb index 88035ac9a2..b19fbea441 100644 --- a/spec/controllers/api/submission_files_controller_spec.rb +++ b/spec/controllers/api/submission_files_controller_spec.rb @@ -57,19 +57,41 @@ context 'POST create' do context 'when the file does not exist yet' do + let(:filename) { 'v1/x/y/test.txt' } + let(:content) { 'This is a test file' } + let(:mime_type) { 'text/plain' } + before do - post :create, params: { assignment_id: assignment.id, group_id: group.id, filename: 'v1/x/y/test.txt', - mime_type: 'text', file_content: 'This is a test file', course_id: course.id } + post :create, params: { assignment_id: assignment.id, group_id: group.id, filename: filename, + mime_type: mime_type, file_content: content, course_id: course.id } end - it 'should create a file in the corresponding directory' do - path = Pathname.new('v1/x/y') - success, _messages = group.access_repo do |repo| - file_path = Pathname.new(assignment.repository_folder).join path - files = repo.get_latest_revision.files_at_path(file_path.to_s) - files.key? 'test.txt' + context 'when the file is plaintext' do + it 'should create the file in the corresponding directory' do + path = Pathname.new('v1/x/y') + success, _messages = group.access_repo do |repo| + file_path = Pathname.new(assignment.repository_folder).join path + files = repo.get_latest_revision.files_at_path(file_path.to_s) + files.key? File.basename(filename) + end + expect(success).to be_truthy + end + end + + context 'when the file is binary' do + let(:filename) { 'v1/x/y/test.pdf' } + let(:content) { file_fixture('submission_files/pdf.pdf') } + let(:mime_type) { 'application/pdf' } + + it 'should create the file in the corresponding directory' do + path = Pathname.new('v1/x/y') + success, _messages = group.access_repo do |repo| + file_path = Pathname.new(assignment.repository_folder).join path + files = repo.get_latest_revision.files_at_path(file_path.to_s) + files.key? File.basename(filename) + end + expect(success).to be_truthy end - expect(success).to be_truthy end it_behaves_like 'for a different course' From 6f91071331cb9090238ab18c479dfa2977590b63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 20:23:02 -0400 Subject: [PATCH 09/86] build(deps-dev): bump rspec-rails from 8.0.1 to 8.0.2 (#7649) Bumps [rspec-rails](https://github.com/rspec/rspec-rails) from 8.0.1 to 8.0.2. - [Changelog](https://github.com/rspec/rspec-rails/blob/main/Changelog.md) - [Commits](https://github.com/rspec/rspec-rails/compare/v8.0.1...v8.0.2) --- updated-dependencies: - dependency-name: rspec-rails dependency-version: 8.0.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile | 2 +- Gemfile.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 508913d260..264d103670 100644 --- a/Gemfile +++ b/Gemfile @@ -106,7 +106,7 @@ group :development, :test do gem 'capybara' gem 'debug', '>= 1.0.0' gem 'i18n-tasks', require: false - gem 'rspec-rails', '~> 8.0.1' + gem 'rspec-rails', '~> 8.0.2' gem 'selenium-webdriver' end diff --git a/Gemfile.lock b/Gemfile.lock index 51efc574eb..ce5a64a811 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -177,7 +177,7 @@ GEM dry-initializer (~> 3.2) dry-schema (~> 1.14) zeitwerk (~> 2.6) - erb (5.0.1) + erb (5.0.2) erubi (1.13.1) et-orbi (1.2.11) tzinfo @@ -222,7 +222,7 @@ GEM rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.8, >= 1.8.1) terminal-table (>= 1.5.1) - io-console (0.8.0) + io-console (0.8.1) irb (1.15.2) pp (>= 0.6.0) rdoc (>= 4.0.0) @@ -303,7 +303,7 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.1.16) + rack (3.2.0) rack-cors (3.0.0) logger rack (>= 3.0.14) @@ -375,7 +375,7 @@ GEM redis-namespace (1.11.0) redis (>= 4) regexp_parser (2.9.0) - reline (0.6.1) + reline (0.6.2) io-console (~> 0.5) responders (3.1.1) actionpack (>= 5.2) @@ -407,7 +407,7 @@ GEM rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (8.0.1) + rspec-rails (8.0.2) actionpack (>= 7.2) activesupport (>= 7.2) railties (>= 7.2) @@ -415,7 +415,7 @@ GEM rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-support (3.13.4) + rspec-support (3.13.5) rtesseract (3.1.4) ruby-next-core (1.1.1) ruby-progressbar (1.13.0) @@ -551,7 +551,7 @@ DEPENDENCIES resque resque-scheduler rmagick (~> 6.1.2) - rspec-rails (~> 8.0.1) + rspec-rails (~> 8.0.2) rtesseract rubyzip rugged From ae4a3aca1fccd574bd8298096190fbc268f070cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 20:23:50 -0400 Subject: [PATCH 10/86] build(deps-dev): bump selenium-webdriver from 4.34.0 to 4.35.0 (#7651) Bumps [selenium-webdriver](https://github.com/SeleniumHQ/selenium) from 4.34.0 to 4.35.0. - [Release notes](https://github.com/SeleniumHQ/selenium/releases) - [Changelog](https://github.com/SeleniumHQ/selenium/blob/trunk/rb/CHANGES) - [Commits](https://github.com/SeleniumHQ/selenium/compare/selenium-4.34.0...selenium-4.35.0) --- updated-dependencies: - dependency-name: selenium-webdriver dependency-version: 4.35.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ce5a64a811..77235e1a16 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -390,7 +390,7 @@ GEM redis (>= 3.3) resque (>= 1.27) rufus-scheduler (~> 3.2, != 3.3) - rexml (3.4.1) + rexml (3.4.2) rmagick (6.1.2) observer (~> 0.1) pkg-config (~> 1.4) @@ -426,11 +426,11 @@ GEM fugit (~> 1.1, >= 1.11.1) rugged (1.9.0) securerandom (0.4.1) - selenium-webdriver (4.34.0) + selenium-webdriver (4.35.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) + rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) shoulda-callback-matchers (1.1.4) activesupport (>= 3) From 5b60ed849d9ca25be34d93e66e2282f3751648bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 20:25:27 -0400 Subject: [PATCH 11/86] build(deps): bump the babel group with 4 updates (#7654) Bumps the babel group with 4 updates: [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime), [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core), [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) and [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env). Updates `@babel/runtime` from 7.28.2 to 7.28.3 - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.28.3/packages/babel-runtime) Updates `@babel/core` from 7.28.0 to 7.28.3 - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.28.3/packages/babel-core) Updates `@babel/plugin-transform-runtime` from 7.28.0 to 7.28.3 - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.28.3/packages/babel-plugin-transform-runtime) Updates `@babel/preset-env` from 7.28.0 to 7.28.3 - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.28.3/packages/babel-preset-env) --- updated-dependencies: - dependency-name: "@babel/runtime" dependency-version: 7.28.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: babel - dependency-name: "@babel/core" dependency-version: 7.28.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: babel - dependency-name: "@babel/plugin-transform-runtime" dependency-version: 7.28.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: babel - dependency-name: "@babel/preset-env" dependency-version: 7.28.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: babel ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 154 ++++++++++++++++++++++++---------------------- package.json | 8 +-- 2 files changed, 86 insertions(+), 76 deletions(-) diff --git a/package-lock.json b/package-lock.json index 65cd8c1953..fa280bbaa5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "MarkUs", "dependencies": { - "@babel/runtime": "^7.28.2", + "@babel/runtime": "^7.28.3", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.7.2", @@ -46,9 +46,9 @@ "ui-contextmenu": "^1.18.1" }, "devDependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-runtime": "^7.28.0", - "@babel/preset-env": "^7.28.0", + "@babel/core": "^7.28.3", + "@babel/plugin-transform-runtime": "^7.28.3", + "@babel/preset-env": "^7.28.3", "@babel/preset-react": "^7.27.1", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", @@ -129,21 +129,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -159,13 +160,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -229,18 +231,18 @@ "dev": true }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", - "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.27.1", + "@babel/traverse": "^7.28.3", "semver": "^6.3.1" }, "engines": { @@ -322,15 +324,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -458,25 +460,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -553,14 +557,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", - "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -938,13 +942,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", - "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { @@ -955,17 +959,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz", - "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz", + "integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -1560,10 +1565,11 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.0.tgz", - "integrity": "sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz", + "integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -1608,10 +1614,11 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.0.tgz", - "integrity": "sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.3.tgz", + "integrity": "sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", @@ -1776,10 +1783,11 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.0.tgz", - "integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", + "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", @@ -1789,7 +1797,7 @@ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", @@ -1800,8 +1808,8 @@ "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.0", "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.28.0", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.3", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-dotall-regex": "^7.27.1", @@ -1833,7 +1841,7 @@ "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.0", + "@babel/plugin-transform-regenerator": "^7.28.3", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -1895,9 +1903,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1919,17 +1927,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.2", "debug": "^4.3.1" }, "engines": { @@ -1937,10 +1946,11 @@ } }, "node_modules/@babel/types": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", - "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" diff --git a/package.json b/package.json index 7b661255d8..6f05bd4f33 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "MarkUs", "dependencies": { - "@babel/runtime": "^7.28.2", + "@babel/runtime": "^7.28.3", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.7.2", @@ -41,9 +41,9 @@ "ui-contextmenu": "^1.18.1" }, "devDependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-runtime": "^7.28.0", - "@babel/preset-env": "^7.28.0", + "@babel/core": "^7.28.3", + "@babel/plugin-transform-runtime": "^7.28.3", + "@babel/preset-env": "^7.28.3", "@babel/preset-react": "^7.27.1", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.6.3", From 9890b8d19bdd7fb4975de371b67b320abdda0bec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 16:06:00 -0400 Subject: [PATCH 12/86] build(deps-dev): bump simplecov-lcov from 0.8.0 to 0.9.0 (#7659) Bumps [simplecov-lcov](https://github.com/fortissimo1997/simplecov-lcov) from 0.8.0 to 0.9.0. - [Release notes](https://github.com/fortissimo1997/simplecov-lcov/releases) - [Changelog](https://github.com/fortissimo1997/simplecov-lcov/blob/master/CHANGELOG.md) - [Commits](https://github.com/fortissimo1997/simplecov-lcov/compare/v0.8.0...v0.9.0) --- updated-dependencies: - dependency-name: simplecov-lcov dependency-version: 0.9.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 77235e1a16..31ceefb6c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -442,7 +442,7 @@ GEM simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) - simplecov-lcov (0.8.0) + simplecov-lcov (0.9.0) simplecov_json_formatter (0.1.4) sinatra (4.1.1) logger (>= 1.6.0) From 66e9d7217370e20c17f1c5a796e2137588b2c738 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 16:08:45 -0400 Subject: [PATCH 13/86] build(deps): bump exception_notification from 5.0.0 to 5.0.1 (#7657) Bumps [exception_notification](https://github.com/kmcphillips/exception_notification) from 5.0.0 to 5.0.1. - [Release notes](https://github.com/kmcphillips/exception_notification/releases) - [Changelog](https://github.com/kmcphillips/exception_notification/blob/main/CHANGELOG.rdoc) - [Commits](https://github.com/kmcphillips/exception_notification/compare/v5.0.0...v5.0.1) --- updated-dependencies: - dependency-name: exception_notification dependency-version: 5.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 31ceefb6c7..e45800a3bb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -181,7 +181,7 @@ GEM erubi (1.13.1) et-orbi (1.2.11) tzinfo - exception_notification (5.0.0) + exception_notification (5.0.1) actionmailer (>= 7.1, < 9) activesupport (>= 7.1, < 9) execjs (2.10.0) @@ -261,7 +261,7 @@ GEM multi_json (1.15.0) mustermann (3.0.3) ruby2_keywords (~> 0.0.1) - net-imap (0.5.8) + net-imap (0.5.10) date net-protocol net-pop (0.1.2) From 8289b7a602e1a4f8058efa1e8b38b24ea8ee476d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 16:13:58 -0400 Subject: [PATCH 14/86] build(deps): bump pg from 1.6.0 to 1.6.1 (#7655) Bumps [pg](https://github.com/ged/ruby-pg) from 1.6.0 to 1.6.1. - [Changelog](https://github.com/ged/ruby-pg/blob/master/CHANGELOG.md) - [Commits](https://github.com/ged/ruby-pg/compare/v1.6.0...v1.6.1) --- updated-dependencies: - dependency-name: pg dependency-version: 1.6.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e45800a3bb..fa143b3b61 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -280,7 +280,7 @@ GEM ast (~> 2.4.1) racc pdf-core (0.10.0) - pg (1.6.0) + pg (1.6.1) pkg-config (1.6.2) pluck_to_hash (1.0.2) activerecord (>= 4.0.2) From df2b40f21cfaa71f6ffa0fc8d94507c503a27b3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 16:36:37 -0400 Subject: [PATCH 15/86] build(deps): bump @rails/actioncable in the rails group (#7656) Bumps the rails group with 1 update: [@rails/actioncable](https://github.com/rails/rails). Updates `@rails/actioncable` from 8.0.200 to 8.0.201 - [Release notes](https://github.com/rails/rails/releases) - [Commits](https://github.com/rails/rails/commits) --- updated-dependencies: - dependency-name: "@rails/actioncable" dependency-version: 8.0.201 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rails ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa280bbaa5..0cbe2b1bd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@fortawesome/free-regular-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", - "@rails/actioncable": "^8.0.200", + "@rails/actioncable": "^8.0.201", "@rails/ujs": "^7.1.3-4", "@rjsf/core": "^5.24.12", "@rjsf/validator-ajv8": "^5.24.12", @@ -3354,9 +3354,9 @@ } }, "node_modules/@rails/actioncable": { - "version": "8.0.200", - "resolved": "https://registry.npmjs.org/@rails/actioncable/-/actioncable-8.0.200.tgz", - "integrity": "sha512-EDqWyxck22BHmv1e+mD8Kl6GmtNkhEPdRfGFT7kvsv1yoXd9iYrqHDVAaR8bKmU/syC5eEZ2I5aWWxtB73ukMw==", + "version": "8.0.201", + "resolved": "https://registry.npmjs.org/@rails/actioncable/-/actioncable-8.0.201.tgz", + "integrity": "sha512-WiXZodvnK7u+wlu72DZydfV75x14HhzXI84sto9xcdsW1DMOHK+jYwQuuE/Wh/hKH5yajFIw/3DUP6MHDeGrbA==", "license": "MIT" }, "node_modules/@rails/ujs": { diff --git a/package.json b/package.json index 6f05bd4f33..ab3503e35d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@fortawesome/free-regular-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", - "@rails/actioncable": "^8.0.200", + "@rails/actioncable": "^8.0.201", "@rails/ujs": "^7.1.3-4", "@rjsf/core": "^5.24.12", "@rjsf/validator-ajv8": "^5.24.12", From bd307134c0ec99b2a5d50479da1e6dea82d86c83 Mon Sep 17 00:00:00 2001 From: David Liu Date: Tue, 9 Sep 2025 13:17:58 +0000 Subject: [PATCH 16/86] Fixed preview of URL submissions and generation of URL display names (#7661) * Fixed preview of URL submissions * Fixed autogeneration of URL alias in URL submission form --- Changelog.md | 2 + .../Modals/submission_url_submit_modal.jsx | 8 ++-- .../Components/Result/file_viewer.jsx | 4 +- .../Components/Result/url_viewer.jsx | 41 ++++++++++++++----- config/locales/common/en.yml | 1 - 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/Changelog.md b/Changelog.md index 1f8d44e3d9..8b187aa4e1 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,8 @@ - Fixed group member filtering in assignment summary table (#7644) - Fixed spacing issue for the remote authentication login button (#7646) - Fixed API bug when creating binary submission files (#7647) +- Fixed preview of URL submissions (#7661) +- Fixed autogeneration of URL alias in URL submission form (#7661) ### 🔧 Internal changes - Updated the assignment summary table to use `@tanstack/react-table` v8 (#7630) diff --git a/app/javascript/Components/Modals/submission_url_submit_modal.jsx b/app/javascript/Components/Modals/submission_url_submit_modal.jsx index ec6f48d982..ceb87057ba 100644 --- a/app/javascript/Components/Modals/submission_url_submit_modal.jsx +++ b/app/javascript/Components/Modals/submission_url_submit_modal.jsx @@ -7,6 +7,7 @@ class SubmitUrlUploadModal extends React.Component { this.state = { newUrl: "", newUrlText: "", + manualUrlAlias: false, }; } @@ -26,8 +27,8 @@ class SubmitUrlUploadModal extends React.Component { handleUrlChange = event => { const urlInput = event.target.value; try { - const validatedURL = new URL(urlInput); - if (this.state.newUrlText === "") { + if (!this.state.manualUrlAlias) { + const validatedURL = new URL(urlInput); const suggestedText = validatedURL.hostname; this.setState({newUrlText: suggestedText}); } @@ -39,13 +40,14 @@ class SubmitUrlUploadModal extends React.Component { }; handleUrlAliasChange = event => { - this.setState({newUrlText: event.target.value}); + this.setState({newUrlText: event.target.value, manualUrlAlias: true}); }; handleModalClose = () => { this.setState({ newUrl: "", newUrlText: "", + manualUrlAlias: false, }); this.props.onRequestClose(); }; diff --git a/app/javascript/Components/Result/file_viewer.jsx b/app/javascript/Components/Result/file_viewer.jsx index 91fbe6b356..d7eda16779 100644 --- a/app/javascript/Components/Result/file_viewer.jsx +++ b/app/javascript/Components/Result/file_viewer.jsx @@ -67,9 +67,9 @@ export class FileViewer extends React.Component { ) { return ; } else if (this.props.selectedFileType === "binary") { - return ; + return ; } else if (this.props.selectedFileType === "markusurl") { - return ; + return ; } else if (this.props.selectedFileType !== "") { return ( { + if (!this.props.url) { + return; + } + this.props.setLoadingCallback(true); + fetch(this.props.url) + .then(response => response.text()) + .then(content => + this.setState({externalURL: content}, () => this.props.setLoadingCallback(false)) + ) + .catch(error => { + this.props.setLoadingCallback(false); + if (error instanceof DOMException) return; + console.error(error); + }); + }; + configDisplay = () => { - const url = this.isValidURL(this.props.externalUrl); + const url = this.isValidURL(this.state.externalURL); if (!url) { return; } @@ -35,7 +55,6 @@ export class URLViewer extends React.Component { default: this.setState({embeddedURL: ""}); } - this.setState({isInvalidUrl: false}); }; /* @@ -117,8 +136,8 @@ export class URLViewer extends React.Component { {errorMessage(I18n.t("submissions.url_preview_error"))} ); - } else if (!this.isValidURL(this.props.externalUrl)) { - return errorMessage(I18n.t("submissions.invalid_url", {item: I18n.t("this")})); + } else if (!this.isValidURL(this.state.externalURL)) { + return errorMessage(I18n.t("submissions.invalid_url", {item: `"${this.state.externalURL}"`})); } else { return errorMessage(I18n.t("submissions.url_preview_error")); } @@ -129,11 +148,11 @@ export class URLViewer extends React.Component {
{/* Make Invalid URLs unclickable */} - {!this.isValidURL(this.props.externalUrl) ? ( - this.props.externalUrl + {!this.isValidURL(this.state.externalURL) ? ( + this.state.externalURL ) : ( - - {this.props.externalUrl} + + {this.state.externalURL} )}
diff --git a/config/locales/common/en.yml b/config/locales/common/en.yml index 8eb23a2fb9..f400a39427 100644 --- a/config/locales/common/en.yml +++ b/config/locales/common/en.yml @@ -76,7 +76,6 @@ en: table: no_data: No rows found search: Search - this: This to: to working: Loading write: Write From 17e87a40b969f0ce9984a2239a349f23efad99e6 Mon Sep 17 00:00:00 2001 From: donny-wong <141858744+donny-wong@users.noreply.github.com> Date: Wed, 10 Sep 2025 12:23:05 -0400 Subject: [PATCH 17/86] update changelog with new release v2.8.1 [ci skip] (#7665) Co-authored-by: Donny Wong --- Changelog.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 8b187aa4e1..1893baf6b5 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,13 @@ ### ✨ New features and improvements +### 🐛 Bug fixes + +### 🔧 Internal changes +- Updated Github Actions CI to use cache-apt-pkgs to speed up workflow runs (#7645) + +## [v2.8.1] + ### 🐛 Bug fixes - Fixed group member filtering in assignment summary table (#7644) - Fixed spacing issue for the remote authentication login button (#7646) @@ -15,7 +22,6 @@ ### 🔧 Internal changes - Updated the assignment summary table to use `@tanstack/react-table` v8 (#7630) -- Updated Github Actions CI to use cache-apt-pkgs to speed up workflow runs (#7645) ## [v2.8.0] From bda12c0617ac1a1b6a12f26cccc233c4dee84a4b Mon Sep 17 00:00:00 2001 From: Elizabeth Liu <157079783+lizzie-liu@users.noreply.github.com> Date: Tue, 16 Sep 2025 20:22:51 -0400 Subject: [PATCH 18/86] Added tests for AnnotationCategory.to_json method (#7666) --- Changelog.md | 1 + doc/markus-contributors.txt | 1 + spec/models/annotation_category_spec.rb | 73 +++++++++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/Changelog.md b/Changelog.md index 1893baf6b5..1861da659a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,7 @@ ### 🔧 Internal changes - Updated Github Actions CI to use cache-apt-pkgs to speed up workflow runs (#7645) +- Added tests to improve coverage for `AnnotationCategory`'s `self.to_json` method ## [v2.8.1] diff --git a/doc/markus-contributors.txt b/doc/markus-contributors.txt index ff85bd1d77..8e9982595c 100644 --- a/doc/markus-contributors.txt +++ b/doc/markus-contributors.txt @@ -70,6 +70,7 @@ Donny Wong Dylan Runkel Ealona Shmoel Egor Philippov +Elizabeth Liu Emerik Morency Erik Traikov Eugene Cheung diff --git a/spec/models/annotation_category_spec.rb b/spec/models/annotation_category_spec.rb index f1662fe327..1c4143d59e 100644 --- a/spec/models/annotation_category_spec.rb +++ b/spec/models/annotation_category_spec.rb @@ -353,4 +353,77 @@ end end end + + describe '.to_json' do + let(:assignment) { create(:assignment) } + + context 'when there is a single category and single text' do + let!(:category) { create(:annotation_category, assignment: assignment, annotation_category_name: 'Test A') } + let!(:text) { create(:annotation_text, annotation_category: category, content: 'Test A text') } + + it 'returns a JSON for the category with its text' do + category_json = AnnotationCategory.to_json([category]).first + expect(category_json[:id]).to eq(category.id) + expect(category_json[:annotation_category_name]).to eq('Test A') + expect(category_json[:texts].size).to eq(1) + + text_json = category_json[:texts].first + expect(text_json[:id]).to eq(text.id) + expect(text_json[:content]).to eq('Test A text') + expect(text_json[:deduction]).to be_nil + end + end + + context 'when there is a category with multiple texts' do + let!(:category) { create(:annotation_category, assignment: assignment, annotation_category_name: 'Test B') } + + it 'returns a JSON including all the texts for the category' do + create(:annotation_text, annotation_category: category, content: 'Test B text 1') + create(:annotation_text, annotation_category: category, content: 'Test B text 2') + + category_json = AnnotationCategory.to_json([category]).first + expect(category_json[:id]).to eq(category.id) + expect(category_json[:annotation_category_name]).to eq('Test B') + expect(category_json[:texts].size).to eq(2) + + texts_json = category_json[:texts] + expect(texts_json.pluck(:content)).to match_array(['Test B text 1', 'Test B text 2']) + expect(texts_json.pluck(:deduction)).to all(be_nil) + end + end + + context 'when there are multiple categories with text(s)' do + let!(:category1) do + create(:annotation_category, assignment: assignment, annotation_category_name: 'Category A') + end + let!(:category2) do + create(:annotation_category, assignment: assignment, annotation_category_name: 'Category B') + end + + it 'returns JSON for all categories with their texts' do + text1 = create(:annotation_text, annotation_category: category1, content: 'Category A text 1') + text2 = create(:annotation_text, annotation_category: category1, content: 'Category A text 2') + text3 = create(:annotation_text, annotation_category: category2, content: 'Category B text 3') + + categories_json = AnnotationCategory.to_json([category1, category2]) + category_ids = categories_json.pluck(:id) # rubocop:disable Rails/PluckId + + expect(category_ids).to match_array([category1.id, category2.id]) + + all_texts = categories_json.flat_map { |c| c[:texts] } + expect(all_texts).to include({ id: text1.id, content: 'Category A text 1', deduction: nil }) + expect(all_texts).to include({ id: text2.id, content: 'Category A text 2', deduction: nil }) + expect(all_texts).to include({ id: text3.id, content: 'Category B text 3', deduction: nil }) + end + end + + context 'when a category has no texts' do + let!(:category) { create(:annotation_category, assignment: assignment, annotation_category_name: 'Empty A') } + + it 'returns an empty texts array' do + category_json = AnnotationCategory.to_json([category]).first + expect(category_json[:texts]).to eq([]) + end + end + end end From f61daaafaf3a3a3184ecc5d596e48907fde9cced Mon Sep 17 00:00:00 2001 From: steven Date: Fri, 19 Sep 2025 12:44:43 -0400 Subject: [PATCH 19/86] Added loading icon for react tables (#7602) --------- Co-authored-by: david-yz-liu --- Changelog.md | 2 + app/assets/stylesheets/common/_table.scss | 6 + .../Components/Helpers/table_helpers.jsx | 31 ++- .../__tests__/course_summaries.test.jsx | 21 ++ .../__tests__/instructor_table.test.jsx | 19 ++ .../__tests__/student_table.test.jsx | 20 ++ .../Components/__tests__/ta_table.test.jsx | 23 ++- .../Components/course_summaries_table.jsx | 4 +- .../Components/instructor_table.jsx | 4 +- app/javascript/Components/notes_table.jsx | 3 + app/javascript/Components/student_table.jsx | 6 +- app/javascript/Components/ta_table.jsx | 9 +- app/javascript/Components/table/table.jsx | 22 ++- app/javascript/common/react_config.jsx | 6 +- jest_after_env_setup.js | 13 ++ package-lock.json | 179 +++++++++++++++--- package.json | 1 + 17 files changed, 328 insertions(+), 41 deletions(-) create mode 100644 app/javascript/Components/__tests__/course_summaries.test.jsx diff --git a/Changelog.md b/Changelog.md index 1861da659a..79a448cf55 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,7 @@ ### 🚨 Breaking changes ### ✨ New features and improvements +- Added new loading spinner icon for tables (#7602) ### 🐛 Bug fixes @@ -23,6 +24,7 @@ ### 🔧 Internal changes - Updated the assignment summary table to use `@tanstack/react-table` v8 (#7630) +- Updated Github Actions CI to use cache-apt-pkgs to speed up workflow runs (#7645) ## [v2.8.0] diff --git a/app/assets/stylesheets/common/_table.scss b/app/assets/stylesheets/common/_table.scss index 17aff5af9e..8377d6bd2e 100644 --- a/app/assets/stylesheets/common/_table.scss +++ b/app/assets/stylesheets/common/_table.scss @@ -658,3 +658,9 @@ margin-bottom: 1em; width: 100%; } + +// Additional custom styles +.loading-spinner { + padding: 10px 0; + text-align: center; +} diff --git a/app/javascript/Components/Helpers/table_helpers.jsx b/app/javascript/Components/Helpers/table_helpers.jsx index 978facb04c..d34fc688b1 100644 --- a/app/javascript/Components/Helpers/table_helpers.jsx +++ b/app/javascript/Components/Helpers/table_helpers.jsx @@ -1,10 +1,32 @@ import React from "react"; +import {Grid} from "react-loader-spinner"; /** * @file * Provides generic helper functions and components for react-table tables. */ +export function customLoadingProp({loading}) { + if (loading) { + return ( +
+ +
+ ); + } + + return null; +} + export function defaultSort(a, b) { // Sort values, putting undefined/nulls below all other values. // Based on react-table v6 defaultSortMethod (https://github.com/tannerlinsley/react-table/tree/v6/), @@ -230,6 +252,13 @@ export function getMarkingStates(data) { return markingStates; } -export function customNoDataComponent({children}) { +export function customNoDataComponent({children, loading}) { + if (loading) { + return null; + } return

{children}

; } + +export function customNoDataProps({state}) { + return {loading: state.loading, data: state.data}; +} diff --git a/app/javascript/Components/__tests__/course_summaries.test.jsx b/app/javascript/Components/__tests__/course_summaries.test.jsx new file mode 100644 index 0000000000..e0638741ad --- /dev/null +++ b/app/javascript/Components/__tests__/course_summaries.test.jsx @@ -0,0 +1,21 @@ +import {CourseSummaryTable} from "../course_summaries_table"; +import {render, screen, within, fireEvent, waitFor} from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import {customLoadingProp} from "../Helpers/table_helpers"; + +describe("For each CourseSummaries' loading status", () => { + beforeEach(() => { + jest.spyOn(global, "fetch").mockImplementation(() => new Promise(() => {})); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("shows loading spinner when data is being fetched", async () => { + render(); + + const spinner = await screen.findByLabelText("grid-loading"); + expect(spinner).toBeInTheDocument(); + }); +}); diff --git a/app/javascript/Components/__tests__/instructor_table.test.jsx b/app/javascript/Components/__tests__/instructor_table.test.jsx index 91b7b13012..b30eb60048 100644 --- a/app/javascript/Components/__tests__/instructor_table.test.jsx +++ b/app/javascript/Components/__tests__/instructor_table.test.jsx @@ -91,3 +91,22 @@ describe("For the InstructorTable's display of instructors", () => { }); }); }); + +describe("For each InstructorTable's loading status", () => { + beforeEach(() => { + jest.spyOn(global, "fetch").mockImplementation(() => new Promise(() => {})); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("InstructorTable Spinner", () => { + it("shows loading spinner when data is being fetched", async () => { + render(); + + const spinner = await screen.findByLabelText("grid-loading", {}, {timeout: 3000}); + expect(spinner).toBeInTheDocument(); + }); + }); +}); diff --git a/app/javascript/Components/__tests__/student_table.test.jsx b/app/javascript/Components/__tests__/student_table.test.jsx index 0878de7fbd..2584ee865a 100644 --- a/app/javascript/Components/__tests__/student_table.test.jsx +++ b/app/javascript/Components/__tests__/student_table.test.jsx @@ -5,6 +5,7 @@ import {StudentTable} from "../student_table"; import {render, screen, within, fireEvent, waitFor} from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import {CourseSummaryTable} from "../course_summaries_table"; describe("For the StudentTable component's states and props", () => { describe("submitting the child StudentsActionBox component", () => { @@ -319,3 +320,22 @@ describe("For the StudentTable's display of students", () => { }); }); }); + +describe("For each StudentTable's loading status", () => { + let mock_course_id = 1; + + beforeEach(() => { + jest.spyOn(global, "fetch").mockImplementation(() => new Promise(() => {})); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("shows loading spinner when data is being fetched", async () => { + render(); + + const spinner = await screen.findByLabelText("grid-loading"); + expect(spinner).toBeInTheDocument(); + }); +}); diff --git a/app/javascript/Components/__tests__/ta_table.test.jsx b/app/javascript/Components/__tests__/ta_table.test.jsx index 5f87db4bab..33005e78e2 100644 --- a/app/javascript/Components/__tests__/ta_table.test.jsx +++ b/app/javascript/Components/__tests__/ta_table.test.jsx @@ -99,7 +99,7 @@ describe("For the TATable's display of TAs", () => { }); it("No rows found is shown", async () => { - await screen.findByText(I18n.t("tas.empty_table")); + await screen.findByText(I18n.t("tas.empty_table"), {}, {timeout: 3000}); }); }); @@ -153,3 +153,24 @@ describe("For the TATable's display of TAs", () => { }); }); }); + +describe("For each TATable's loading status", () => { + let mock_course_id = 1; + + beforeEach(() => { + jest.spyOn(global, "fetch").mockImplementation(() => new Promise(() => {})); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("TATable Spinner", () => { + it("shows loading spinner when data is being fetched", async () => { + render(); + + const spinner = await screen.findByLabelText("grid-loading", {}, {timeout: 3000}); + expect(spinner).toBeInTheDocument(); + }); + }); +}); diff --git a/app/javascript/Components/course_summaries_table.jsx b/app/javascript/Components/course_summaries_table.jsx index 2276c98076..dcaf3d1920 100644 --- a/app/javascript/Components/course_summaries_table.jsx +++ b/app/javascript/Components/course_summaries_table.jsx @@ -1,5 +1,4 @@ import React from "react"; - import ReactTable from "react-table"; export class CourseSummaryTable extends React.Component { @@ -113,6 +112,9 @@ export class CourseSummaryTable extends React.Component { filtered={this.state.filtered} onFilteredChange={filtered => this.setState({filtered})} className={"auto-overflow"} + getNoDataProps={() => ({ + loading: this.props.loading, + })} />, ]; } diff --git a/app/javascript/Components/instructor_table.jsx b/app/javascript/Components/instructor_table.jsx index 4b87a79a56..e610ac6eb9 100644 --- a/app/javascript/Components/instructor_table.jsx +++ b/app/javascript/Components/instructor_table.jsx @@ -56,7 +56,8 @@ class InstructorTable extends React.Component { } componentDidMount() { - this.fetchData().then(data => this.setState({data: this.processData(data)})); + this.setState({loading: true}); + this.fetchData().then(data => this.setState({data: this.processData(data), loading: false})); } fetchData() { @@ -84,6 +85,7 @@ class InstructorTable extends React.Component { data={this.state.data} columns={this.columns} noDataText={I18n.t("instructors.empty_table")} + loading={this.state.loading} /> ); } diff --git a/app/javascript/Components/notes_table.jsx b/app/javascript/Components/notes_table.jsx index cd8b234178..8dbee3b525 100644 --- a/app/javascript/Components/notes_table.jsx +++ b/app/javascript/Components/notes_table.jsx @@ -117,6 +117,9 @@ class NotesTable extends React.Component { columns={this.columns} sortable={false} loading={this.state.loading} + getNoDataProps={() => ({ + loading: this.state.loading, + })} /> ); } diff --git a/app/javascript/Components/student_table.jsx b/app/javascript/Components/student_table.jsx index c8fdeb7152..96bd2355b7 100644 --- a/app/javascript/Components/student_table.jsx +++ b/app/javascript/Components/student_table.jsx @@ -95,6 +95,7 @@ class RawStudentTable extends React.Component { authenticity_token={this.props.authenticity_token} /> (this.checkboxTable = r)} data={data.students} columns={[ @@ -225,13 +226,16 @@ class RawStudentTable extends React.Component { filterable: false, }, ]} + getNoDataProps={() => ({ + loading, + data, + })} defaultSorted={[ { id: "user_name", }, ]} filterable - loading={loading} noDataText={I18n.t("students.empty_table")} {...this.props.getCheckboxProps()} /> diff --git a/app/javascript/Components/ta_table.jsx b/app/javascript/Components/ta_table.jsx index bb10887ba5..ac252e9616 100644 --- a/app/javascript/Components/ta_table.jsx +++ b/app/javascript/Components/ta_table.jsx @@ -68,7 +68,7 @@ class TATable extends React.Component { } componentDidMount() { - this.fetchData().then(data => this.setState({data: this.processData(data)})); + this.fetchData().then(data => this.setState({data: this.processData(data), loading: false})); } fetchData() { @@ -110,7 +110,12 @@ class TATable extends React.Component { render() { return ( - +
); } } diff --git a/app/javascript/Components/table/table.jsx b/app/javascript/Components/table/table.jsx index e0bc2b1328..f9226898bc 100644 --- a/app/javascript/Components/table/table.jsx +++ b/app/javascript/Components/table/table.jsx @@ -1,4 +1,5 @@ import React from "react"; +import {Grid} from "react-loader-spinner"; import { createColumnHelper, @@ -36,6 +37,7 @@ export default function Table({ data, noDataText, initialState, + loading, renderSubComponent, getRowCanExpand, columnFilters: externalColumnFilters, @@ -166,9 +168,23 @@ export default function Table({ ); })} - {!table.getRowModel().rows.length && ( -

{noDataText || defaultNoDataText()}

- )} + {!table.getRowModel().rows.length && + (loading ? ( +
+ +
+ ) : ( +

{noDataText || defaultNoDataText()}

+ ))} diff --git a/app/javascript/common/react_config.jsx b/app/javascript/common/react_config.jsx index 5eb84d9973..260588fd6d 100644 --- a/app/javascript/common/react_config.jsx +++ b/app/javascript/common/react_config.jsx @@ -1,11 +1,13 @@ -import {ReactTableDefaults} from "react-table"; import {I18n} from "i18n-js"; import translations from "translations.json"; +import {ReactTableDefaults} from "react-table"; import { defaultSort, stringFilterMethod, textFilter, customNoDataComponent, + customLoadingProp, + customNoDataProps, } from "../Components/Helpers/table_helpers"; const i18n = new I18n(translations); @@ -21,6 +23,8 @@ Object.assign(ReactTableDefaults, { defaultFilterMethod: stringFilterMethod, FilterComponent: textFilter, NoDataComponent: customNoDataComponent, + noDataProps: customNoDataProps, + LoadingComponent: customLoadingProp, }); Object.assign(ReactTableDefaults.column, { diff --git a/jest_after_env_setup.js b/jest_after_env_setup.js index 1c20ff02ee..435d78a7f0 100644 --- a/jest_after_env_setup.js +++ b/jest_after_env_setup.js @@ -79,3 +79,16 @@ global.activeCriterion = jest.fn(); // Ensure @testing-library/react cleanup function is called after every test import {cleanup} from "@testing-library/react"; afterEach(cleanup); + +import { + customLoadingProp, + customNoDataComponent, + customNoDataProps, +} from "/app/javascript/Components/Helpers/table_helpers"; +import {ReactTableDefaults} from "react-table"; + +Object.assign(ReactTableDefaults, { + NoDataComponent: customNoDataComponent, + noDataProps: customNoDataProps, + LoadingComponent: customLoadingProp, +}); diff --git a/package-lock.json b/package-lock.json index 0cbe2b1bd4..a607af4caf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "react-dom": "^18.3.1", "react-flatpickr": "^4.0.11", "react-keyed-file-browser": "^1.14.0", + "react-loader-spinner": "6.1.6", "react-modal": "^3.16.3", "react-table": "^6.11.5", "react-tabs": "^6.1.0", @@ -2124,6 +2125,27 @@ "tslib": "^2.4.0" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "6.7.2", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", @@ -3770,31 +3792,25 @@ "integrity": "sha512-GKX1Qnqxo4S+Z/+Z8KKPLpH282LD7jLHWJcVryOflnsnH+BtSDfieR6ObwBMwpnNws0bUK8GI7z0unQf9bARNQ==", "devOptional": true }, - "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" - }, "node_modules/@types/react": { - "version": "18.3.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", - "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", + "version": "19.1.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", + "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.5", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", - "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", + "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", "dev": true, "license": "MIT", "optional": true, "peer": true, "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.0.0" } }, "node_modules/@types/react-table": { @@ -3812,6 +3828,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT" + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -4787,6 +4809,15 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001726", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", @@ -5069,6 +5100,15 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, "node_modules/css-loader": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", @@ -5117,6 +5157,17 @@ "node": ">=10" } }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -5156,9 +5207,10 @@ } }, "node_modules/csstype": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", - "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" }, "node_modules/data-urls": { "version": "5.0.0", @@ -7709,8 +7761,7 @@ "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -7746,10 +7797,9 @@ } }, "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", - "dev": true, + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -7764,10 +7814,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -7848,8 +7899,7 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/pretty-format": { "version": "30.0.5", @@ -8082,6 +8132,29 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-loader-spinner": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz", + "integrity": "sha512-x5h1Jcit7Qn03MuKlrWcMG9o12cp9SNDVHVJTNRi9TgtGPKcjKiXkou4NRfLAtXaFB3+Z8yZsVzONmPzhv2ErA==", + "license": "MIT", + "dependencies": { + "react-is": "^18.2.0", + "styled-components": "^6.1.2" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-loader-spinner/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/react-modal": { "version": "3.16.3", "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.3.tgz", @@ -8470,6 +8543,12 @@ "node": ">=8" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8517,10 +8596,10 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -8680,6 +8759,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-components": { + "version": "6.1.19", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz", + "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index ab3503e35d..320bd0d555 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react-dom": "^18.3.1", "react-flatpickr": "^4.0.11", "react-keyed-file-browser": "^1.14.0", + "react-loader-spinner": "6.1.6", "react-modal": "^3.16.3", "react-table": "^6.11.5", "react-tabs": "^6.1.0", From 857462fab6ded729b3a0269b6e3711ab1b041314 Mon Sep 17 00:00:00 2001 From: James Han Date: Fri, 19 Sep 2025 12:47:33 -0400 Subject: [PATCH 20/86] Added extra tests for CriteriaController (#7668) --- Changelog.md | 1 + doc/markus-contributors.txt | 1 + spec/controllers/criteria_controller_spec.rb | 180 ++++++++++++++++++- spec/factories/assignments.rb | 7 + 4 files changed, 188 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 79a448cf55..37ed5e3b50 100644 --- a/Changelog.md +++ b/Changelog.md @@ -12,6 +12,7 @@ ### 🔧 Internal changes - Updated Github Actions CI to use cache-apt-pkgs to speed up workflow runs (#7645) - Added tests to improve coverage for `AnnotationCategory`'s `self.to_json` method +- Added tests to the Criteria Controller class to achieve full test coverage ## [v2.8.1] diff --git a/doc/markus-contributors.txt b/doc/markus-contributors.txt index 8e9982595c..ce97d8b8b7 100644 --- a/doc/markus-contributors.txt +++ b/doc/markus-contributors.txt @@ -101,6 +101,7 @@ Ishan Thukral Ivan Chepelev Jackson Lee Jakub Subczynski +James Han Jason Mai Jay Parekh Jeffrey Ling diff --git a/spec/controllers/criteria_controller_spec.rb b/spec/controllers/criteria_controller_spec.rb index 3968a8d5c1..a8c5f951d2 100644 --- a/spec/controllers/criteria_controller_spec.rb +++ b/spec/controllers/criteria_controller_spec.rb @@ -58,8 +58,43 @@ describe 'Using Checkbox Criterion' do let(:criterion) { :checkbox_criterion } + let(:checkbox_criterion) do + create(:checkbox_criterion, + assignment: assignment) + end it_behaves_like 'callbacks' + + describe 'An authenticated and authorized instructor doing a PUT' do + describe '#update' do + context 'when updating with assignment files' do + let!(:assignment_file1) { create(:assignment_file, assignment: assignment) } + let!(:assignment_file2) { create(:assignment_file, assignment: assignment) } + + before do + put_as instructor, + :update, + params: { + course_id: course.id, + id: checkbox_criterion.id, + checkbox_criterion: { + name: 'Updated Checkbox Criterion', + assignment_files: [assignment_file1.id, assignment_file2.id] + } + }, + format: :js + end + + it 'should associate assignment files with criterion' do + expect(checkbox_criterion.reload.assignment_files).to include(assignment_file1, assignment_file2) + end + + it 'should respond with success' do + expect(subject).to respond_with(:success) + end + end + end + end end describe 'Using Flexible Criteria' do @@ -196,6 +231,22 @@ it 'should respond with success' do expect(subject).to respond_with(:success) end + + context 'when marking has started' do + before do + # Create a grouping, submission, and result with marks to simulate marking having started + grouping = create(:grouping, assignment: assignment) + submission = create(:submission, grouping: grouping, submission_version_used: true) + result = create(:result, submission: submission, marking_state: Result::MARKING_STATES[:complete]) + criterion = create(:flexible_criterion, assignment: assignment, max_mark: 10) + create(:mark, result: result, criterion: criterion, mark: 5) + get_as instructor, :index, params: { course_id: course.id, assignment_id: assignment.id } + end + + it 'should display marking started warning' do + expect(flash[:notice]).to have_message(I18n.t('assignments.due_date.marking_started_warning')) + end + end end describe '#new' do @@ -368,7 +419,9 @@ context 'with save error' do before do allow_any_instance_of(FlexibleCriterion).to receive(:save).and_return(false) - allow_any_instance_of(FlexibleCriterion).to receive(:errors).and_return(ActiveModel::Errors.new(self)) + error_object = instance_double(ActiveModel::Errors) + allow(error_object).to receive(:full_messages).and_return(['Test error message']) + allow_any_instance_of(FlexibleCriterion).to receive(:errors).and_return(error_object) post_as instructor, :create, params: { course_id: course.id, assignment_id: assignment.id, @@ -385,6 +438,10 @@ it 'should respond with unprocessable entity' do expect(subject).to respond_with(:unprocessable_entity) end + + it 'should display error messages' do + expect(flash[:error]).to have_message('

Test error message

') + end end context 'without error on an assignment as the first criterion' do @@ -493,6 +550,56 @@ expect(response).to have_http_status(:bad_request) end end + + context 'when updating with assignment files' do + let!(:assignment_file1) { create(:assignment_file, assignment: assignment) } + let!(:assignment_file2) { create(:assignment_file, assignment: assignment) } + + before do + put_as instructor, + :update, + params: { + course_id: course.id, + id: flexible_criterion.id, + flexible_criterion: { + name: 'Updated Criterion', + assignment_files: [assignment_file1.id, assignment_file2.id] + } + }, + format: :js + end + + it 'should associate assignment files with criterion' do + expect(flexible_criterion.reload.assignment_files).to include(assignment_file1, assignment_file2) + end + + it 'should respond with success' do + expect(subject).to respond_with(:success) + end + end + + context 'when update fails with validation errors' do + before do + put_as instructor, + :update, + params: { + course_id: course.id, + id: flexible_criterion.id, + flexible_criterion: { + max_mark: -1 # Invalid max_mark to trigger validation error + } + }, + format: :js + end + + it 'should display error messages' do + expect(flash[:error]).not_to be_empty + end + + it 'should respond with unprocessable entity' do + expect(subject).to respond_with(:unprocessable_entity) + end + end end describe '#edit' do @@ -544,6 +651,28 @@ expect { FlexibleCriterion.find(flexible_criterion.id) }.to raise_error(ActiveRecord::RecordNotFound) end + + context 'when assignment marks are released' do + let!(:released_assignment) { create(:assignment_with_criteria_and_released_results) } + let!(:criterion_with_released_marks) do + released_assignment.criteria.find_by(type: 'FlexibleCriterion') + end + + before do + delete_as instructor, + :destroy, + params: { course_id: released_assignment.course_id, id: criterion_with_released_marks.id }, + format: :js + end + + it 'should flash error message' do + expect(flash[:error]).to have_message(I18n.t('criteria.errors.released_marks')) + end + + it 'should not delete the criterion' do + expect(FlexibleCriterion.find(criterion_with_released_marks.id)).to be_present + end + end end end @@ -765,6 +894,33 @@ expect(subject).to render_template(:update) end end + + context 'when updating with assignment files' do + let!(:assignment_file1) { create(:assignment_file, assignment: assignment) } + let!(:assignment_file2) { create(:assignment_file, assignment: assignment) } + + before do + put_as instructor, + :update, + params: { + course_id: course.id, + id: rubric_criterion.id, + rubric_criterion: { + name: 'Updated Rubric Criterion', + assignment_files: [assignment_file1.id, assignment_file2.id] + } + }, + format: :js + end + + it 'should associate assignment files with criterion' do + expect(rubric_criterion.reload.assignment_files).to include(assignment_file1, assignment_file2) + end + + it 'should respond with success' do + expect(subject).to respond_with(:success) + end + end end end @@ -1245,6 +1401,28 @@ end describe '#upload' do + context 'when marks are released' do + let!(:released_assignment) { create(:assignment_with_criteria_and_released_results) } + let(:test_file) { fixture_file_upload('criteria/upload_yml_mixed.yaml', 'text/yaml') } + + before do + post_as instructor, :upload, + params: { + course_id: released_assignment.course_id, + assignment_id: released_assignment.id, + upload_file: test_file + } + end + + it 'should flash error message' do + expect(flash[:error]).to have_message(I18n.t('criteria.errors.released_marks')) + end + + it 'should redirect to index' do + expect(response).to redirect_to(action: 'index', id: released_assignment.id) + end + end + it_behaves_like 'a controller supporting upload', formats: [:yml] do let(:params) { { course_id: course.id, assignment_id: assignment.id } } end diff --git a/spec/factories/assignments.rb b/spec/factories/assignments.rb index 9f65cc7bda..12b5b38507 100644 --- a/spec/factories/assignments.rb +++ b/spec/factories/assignments.rb @@ -42,6 +42,13 @@ end end + factory :assignment_with_criteria_and_released_results, parent: :assignment_with_criteria_and_results do + after(:create) do |a| + # Release marks by setting released_to_students to true on all current results + a.current_results.update_all(released_to_students: true) + end + end + factory :assignment_with_criteria_and_test_results, parent: :assignment do after(:create) do |a| create_list(:flexible_criterion, 3, assignment: a) From 9c91f608377464232013dc40007a1b7c0a69db61 Mon Sep 17 00:00:00 2001 From: donny-wong <141858744+donny-wong@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:51:34 -0400 Subject: [PATCH 21/86] Removed env check for resque host authorization (#7671) --- Changelog.md | 1 + config/initializers/resque.rb | 4 +--- config/settings.yml | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Changelog.md b/Changelog.md index 37ed5e3b50..14c4daec3a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,6 +8,7 @@ - Added new loading spinner icon for tables (#7602) ### 🐛 Bug fixes +- Resque Host Authorization, removing env condition as this is for all environments (#7671) ### 🔧 Internal changes - Updated Github Actions CI to use cache-apt-pkgs to speed up workflow runs (#7645) diff --git a/config/initializers/resque.rb b/config/initializers/resque.rb index 1276805d66..d43a338166 100644 --- a/config/initializers/resque.rb +++ b/config/initializers/resque.rb @@ -10,9 +10,7 @@ Resque::Server.class_eval do include SessionHandler - configure :production do - set :host_authorization, { permitted_hosts: Settings.resque.permitted_hosts } - end + set :host_authorization, { permitted_hosts: Settings.resque.permitted_hosts } before do unless real_user&.admin_user? diff --git a/config/settings.yml b/config/settings.yml index 5821bb4ace..a5323677ba 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -120,4 +120,4 @@ file_storage: rmd_convert_enabled: false resque: - permitted_hosts: ["localhost"] + permitted_hosts: [".localhost", ".internal"] From 5cfb63e2d5dda1b0d88b9d969c36a1a2b42e464f Mon Sep 17 00:00:00 2001 From: Freya Zhang <149322974+freyazjiner@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:49:58 -0400 Subject: [PATCH 22/86] Converted Create Group modal to React component (#7663) --- Changelog.md | 1 + app/assets/javascripts/Groups/index.js | 1 - app/assets/stylesheets/common/_markus.scss | 5 ++ .../Components/Modals/create_group_modal.jsx | 59 ++++++++++++++++++ .../__tests__/create_group_modal.test.jsx | 60 +++++++++++++++++++ app/javascript/Components/groups_manager.jsx | 43 +++++++------ app/views/groups/_create_group_modal.html.erb | 17 ------ app/views/groups/index.html.erb | 1 - 8 files changed, 149 insertions(+), 38 deletions(-) create mode 100644 app/javascript/Components/Modals/create_group_modal.jsx create mode 100644 app/javascript/Components/__tests__/create_group_modal.test.jsx delete mode 100644 app/views/groups/_create_group_modal.html.erb diff --git a/Changelog.md b/Changelog.md index 14c4daec3a..cea931207c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -12,6 +12,7 @@ ### 🔧 Internal changes - Updated Github Actions CI to use cache-apt-pkgs to speed up workflow runs (#7645) +- Converted "Create Group" functionality to React modal (#7663) - Added tests to improve coverage for `AnnotationCategory`'s `self.to_json` method - Added tests to the Criteria Controller class to achieve full test coverage diff --git a/app/assets/javascripts/Groups/index.js b/app/assets/javascripts/Groups/index.js index 2a518483f0..aae50ca6f7 100644 --- a/app/assets/javascripts/Groups/index.js +++ b/app/assets/javascripts/Groups/index.js @@ -5,7 +5,6 @@ var modalCreate, (function () { const domContentLoadedCB = function () { window.modal_rename = new ModalMarkus("#rename_group_dialog"); - modalCreate = new ModalMarkus("#create_group_dialog"); modalNotesGroup = new ModalMarkus("#notes_dialog"); modalAssignmentGroupReUse = new ModalMarkus("#assignment_group_use_dialog"); }; diff --git a/app/assets/stylesheets/common/_markus.scss b/app/assets/stylesheets/common/_markus.scss index 7867e82139..6fbaf4bdd5 100644 --- a/app/assets/stylesheets/common/_markus.scss +++ b/app/assets/stylesheets/common/_markus.scss @@ -878,6 +878,7 @@ ul.tags { } .ReactModal__Overlay--after-open { + background-color: rgba(0, 0, 0, 0.75) !important; z-index: 100; } @@ -923,6 +924,10 @@ ul.tags { height: 10em; max-height: 10em; } + + form label { + margin-right: 0.3em; + } } /** Menus */ diff --git a/app/javascript/Components/Modals/create_group_modal.jsx b/app/javascript/Components/Modals/create_group_modal.jsx new file mode 100644 index 0000000000..3ecc57ecde --- /dev/null +++ b/app/javascript/Components/Modals/create_group_modal.jsx @@ -0,0 +1,59 @@ +import React from "react"; +import Modal from "react-modal"; + +export default class CreateGroupModal extends React.Component { + constructor(props) { + super(props); + this.state = { + groupName: "", + }; + } + + componentDidMount() { + Modal.setAppElement("body"); + } + + handleChange = event => { + this.setState({groupName: event.target.value}); + }; + + handleSubmit = event => { + event.preventDefault(); + if (!this.state.groupName) { + return; + } + this.props.onSubmit(this.state.groupName); + this.setState({groupName: ""}); + }; + + render() { + return ( + +

{I18n.t("helpers.submit.create", {model: I18n.t("activerecord.models.group.one")})}

+
+ + this.handleChange(event)} + autoFocus + /> +
+ + +
+ +
+ ); + } +} diff --git a/app/javascript/Components/__tests__/create_group_modal.test.jsx b/app/javascript/Components/__tests__/create_group_modal.test.jsx new file mode 100644 index 0000000000..870abf6665 --- /dev/null +++ b/app/javascript/Components/__tests__/create_group_modal.test.jsx @@ -0,0 +1,60 @@ +import React from "react"; +import {render, screen, fireEvent, waitFor} from "@testing-library/react"; +import CreateGroupModal from "../Modals/create_group_modal"; +import Modal from "react-modal"; +import {expect} from "@jest/globals"; + +describe("CreateGroupModal", () => { + let props; + + beforeEach(() => { + props = { + isOpen: true, + onRequestClose: jest.fn().mockImplementation(() => (props.isOpen = false)), + onSubmit: jest.fn(), + }; + + Modal.setAppElement("body"); + render(); + }); + + it("should add the correct group name on successful submit", async () => { + const groupName = "Test Group"; + fireEvent.change(screen.getByLabelText(I18n.t("activerecord.models.group.one")), { + target: {value: groupName}, + }); + + const createGroupButton = screen + .getAllByText( + I18n.t("helpers.submit.create", {model: I18n.t("activerecord.models.group.one")}) + ) + .find(el => el.tagName.toLowerCase() === "button"); + + fireEvent.click(createGroupButton); + + await waitFor(() => { + expect(props.onSubmit).toHaveBeenCalledTimes(1); + expect(props.onSubmit).toHaveBeenCalledWith(groupName); + }); + }); + + it("should call onRequestClose on successful submit", async () => { + fireEvent.click(screen.getByText(I18n.t("cancel"))); + await waitFor(() => { + expect(props.onRequestClose).toHaveBeenCalledTimes(1); + }); + }); + + it("should not add the new group when the inputted group name is empty", async () => { + const createGroupButton = screen + .getAllByText( + I18n.t("helpers.submit.create", {model: I18n.t("activerecord.models.group.one")}) + ) + .find(el => el.tagName.toLowerCase() === "button"); + fireEvent.click(createGroupButton); + + await waitFor(() => { + expect(props.onSubmit).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/app/javascript/Components/groups_manager.jsx b/app/javascript/Components/groups_manager.jsx index a8ac25abe4..752c102dbe 100644 --- a/app/javascript/Components/groups_manager.jsx +++ b/app/javascript/Components/groups_manager.jsx @@ -6,6 +6,7 @@ import {withSelection, CheckboxTable} from "./markus_with_selection_hoc"; import ExtensionModal from "./Modals/extension_modal"; import {durationSort, selectFilter} from "./Helpers/table_helpers"; import AutoMatchModal from "./Modals/auto_match_modal"; +import CreateGroupModal from "./Modals/create_group_modal"; class GroupsManager extends React.Component { constructor(props) { @@ -20,6 +21,7 @@ class GroupsManager extends React.Component { selected_extension_data: {}, updating_extension: false, isAutoMatchModalOpen: false, + isCreateGroupModalOpen: false, examTemplates: [], loading: true, }; @@ -36,11 +38,6 @@ class GroupsManager extends React.Component { } componentDidMountCB = () => { - $("#create_group_dialog form").on("ajax:success", () => { - modalCreate.close(); - this.fetchData(); - }); - $("#rename_group_dialog form").on("ajax:success", () => { modal_rename.close(); this.fetchData(); @@ -97,23 +94,10 @@ class GroupsManager extends React.Component { Routes.new_course_assignment_group_path(this.props.course_id, this.props.assignment_id) ).then(this.fetchData); } else { - modalCreate.open(); - $("#new_group_name").val(""); - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", this.createGroupCB); - } else { - this.createGroupCB(); - } + this.setState({isCreateGroupModalOpen: true}); } }; - createGroupCB = () => { - $("#modal-create-close").click(function () { - modalCreate.close(); - }); - }; - createAllGroups = () => { $.get({ url: Routes.create_groups_when_students_work_alone_course_assignment_groups_path( @@ -199,6 +183,22 @@ class GroupsManager extends React.Component { }).then(this.fetchData); }; + handleCloseCreateGroupModal = () => { + this.setState({ + isCreateGroupModalOpen: false, + }); + }; + + handleSubmitCreateGroup = groupName => { + $.get({ + url: Routes.new_course_assignment_group_path(this.props.course_id, this.props.assignment_id), + data: {new_group_name: groupName}, + }).then(() => { + this.setState({isCreateGroupModalOpen: false}); + this.fetchData(); + }); + }; + handleShowAutoMatchModal = () => { if (this.groupsTable.state.selection.length === 0) { alert(I18n.t("groups.select_a_group")); @@ -354,6 +354,11 @@ class GroupsManager extends React.Component { examTemplates={this.state.examTemplates} onSubmit={this.autoMatch} /> + ); } diff --git a/app/views/groups/_create_group_modal.html.erb b/app/views/groups/_create_group_modal.html.erb deleted file mode 100644 index 280b900e6b..0000000000 --- a/app/views/groups/_create_group_modal.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -<%= content_for :modal_id, 'create_group_dialog' %> -<%= content_for :modal_title, t('helpers.submit.create', model: Group.model_name.human) %> -<%= content_for :modal_content do %> - <%= form_tag new_course_assignment_group_path(@current_course, @assignment), - method: :get, - remote: true do %> - <%= label_tag :new_group_name, Group.human_attribute_name(:group) %> - <%= text_field_tag :new_group_name, - '', - autocomplete: 'off', - maxlength: 30 %> -
- <%= submit_tag t('helpers.submit.create', model: Group.model_name.human) %> - <%= button_tag t(:cancel), id: 'modal-create-close' %> -
- <% end %> -<% end %> diff --git a/app/views/groups/index.html.erb b/app/views/groups/index.html.erb index ba92a1146a..7771013d15 100644 --- a/app/views/groups/index.html.erb +++ b/app/views/groups/index.html.erb @@ -64,6 +64,5 @@ <%= render partial: 'download_modal', layout: 'layouts/modal_dialog' %> <%= render partial: 'upload_modal', layout: 'layouts/modal_dialog' %> <%= render partial: 'assignment_group_use_modal', layout: 'layouts/modal_dialog' %> -<%= render partial: 'create_group_modal', layout: 'layouts/modal_dialog' %> <%= render partial: 'rename_group_modal', layout: 'layouts/modal_dialog' %> From b9d27d5cb6849c4a09770f1a26819522476d015e Mon Sep 17 00:00:00 2001 From: Elizabeth Liu <157079783+lizzie-liu@users.noreply.github.com> Date: Tue, 23 Sep 2025 21:02:05 -0400 Subject: [PATCH 23/86] Refactored Criterion subclasses to remove update_assigned_groups_count and weight methods (#7672) --- Changelog.md | 1 + app/models/checkbox_criterion.rb | 12 ------------ app/models/flexible_criterion.rb | 12 ------------ app/models/rubric_criterion.rb | 4 ---- spec/models/mark_spec.rb | 4 ++-- 5 files changed, 3 insertions(+), 30 deletions(-) diff --git a/Changelog.md b/Changelog.md index cea931207c..e8c424410b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -15,6 +15,7 @@ - Converted "Create Group" functionality to React modal (#7663) - Added tests to improve coverage for `AnnotationCategory`'s `self.to_json` method - Added tests to the Criteria Controller class to achieve full test coverage +- Refactored Criterion subclasses to remove redundant code ## [v2.8.1] diff --git a/app/models/checkbox_criterion.rb b/app/models/checkbox_criterion.rb index efca35fc59..770dfe866a 100644 --- a/app/models/checkbox_criterion.rb +++ b/app/models/checkbox_criterion.rb @@ -1,18 +1,6 @@ class CheckboxCriterion < Criterion DEFAULT_MAX_MARK = 1 - def update_assigned_groups_count - result = [] - tas.each do |ta| - result.concat(ta.get_groupings_by_assignment(assignment)) - end - self.assigned_groups_count = result.uniq.length - end - - def weight - max_mark - end - # Instantiate a CheckboxCriterion from a CSV row and attach it to the supplied # assignment. # row: An array representing one CSV file row. Should be in the following diff --git a/app/models/flexible_criterion.rb b/app/models/flexible_criterion.rb index 5c61ee22ba..4e0177ed9f 100644 --- a/app/models/flexible_criterion.rb +++ b/app/models/flexible_criterion.rb @@ -12,14 +12,6 @@ def reassign_annotation_category end end - def update_assigned_groups_count - result = [] - tas.each do |ta| - result.concat(ta.get_groupings_by_assignment(assignment)) - end - self.assigned_groups_count = result.uniq.length - end - # Instantiate a FlexibleCriterion from a CSV row and attach it to the supplied # assignment. # @@ -104,10 +96,6 @@ def to_yml 'bonus' => self.bonus } } end - def weight - 1 - end - def scale_marks super return if self.annotation_categories.nil? diff --git a/app/models/rubric_criterion.rb b/app/models/rubric_criterion.rb index 059fc86ddb..2a96449a1a 100644 --- a/app/models/rubric_criterion.rb +++ b/app/models/rubric_criterion.rb @@ -152,10 +152,6 @@ def to_yml levels_to_yml end - def weight - self.max_mark - end - def round_max_mark # (this was being done in a weird way, leaving the original in case there are problems) # factor = 10.0 ** 3 diff --git a/spec/models/mark_spec.rb b/spec/models/mark_spec.rb index e45f0c488c..56981e756e 100644 --- a/spec/models/mark_spec.rb +++ b/spec/models/mark_spec.rb @@ -58,9 +58,9 @@ create(:rubric_mark, mark: 4) end - it 'equals to mark times weight' do + it 'equals to mark times max mark' do related_rubric = rubric_mark.criterion - expect(rubric_mark.mark).to eq(related_rubric.weight) + expect(rubric_mark.mark).to eq(related_rubric.max_mark) end end From 62c0607200a234f0748bc2a11fd8a4981178c211 Mon Sep 17 00:00:00 2001 From: donny-wong <141858744+donny-wong@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:10:50 -0400 Subject: [PATCH 24/86] Updated view when a course cannot be created via an LTI external tool (#7669) --- Changelog.md | 1 + app/controllers/lti_deployments_controller.rb | 11 ++++++----- app/views/lti_deployments/message.html.erb | 4 ++++ config/locales/common/en.yml | 1 + spec/controllers/lti_deployments_controller_spec.rb | 2 +- 5 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 app/views/lti_deployments/message.html.erb diff --git a/Changelog.md b/Changelog.md index e8c424410b..ffb51aab3a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ ### ✨ New features and improvements - Added new loading spinner icon for tables (#7602) +- Update message and page displaying cannot create new course via external LTI tool (#7669) ### 🐛 Bug fixes - Resque Host Authorization, removing env condition as this is for all environments (#7671) diff --git a/app/controllers/lti_deployments_controller.rb b/app/controllers/lti_deployments_controller.rb index 22dfe1fea2..56ed3f2f6d 100644 --- a/app/controllers/lti_deployments_controller.rb +++ b/app/controllers/lti_deployments_controller.rb @@ -183,11 +183,12 @@ def check_host def create_course if LtiConfig.respond_to?(:allowed_to_create_course?) && !LtiConfig.allowed_to_create_course?(record) - render 'shared/http_status', - locals: { code: '422', - message: format(Settings.lti.unpermitted_new_course_message, - course_name: record.lms_course_name) }, - status: :unprocessable_entity, layout: false + @title = I18n.t('lti.course_creation_denied') + @message = format( + Settings.lti.unpermitted_new_course_message, + course_name: record.lms_course_name + ) + render 'message', status: :forbidden return end diff --git a/app/views/lti_deployments/message.html.erb b/app/views/lti_deployments/message.html.erb new file mode 100644 index 0000000000..1922919f06 --- /dev/null +++ b/app/views/lti_deployments/message.html.erb @@ -0,0 +1,4 @@ +
+

<%= @title %>

+

<%= @message %>

+
diff --git a/config/locales/common/en.yml b/config/locales/common/en.yml index f400a39427..762f70df51 100644 --- a/config/locales/common/en.yml +++ b/config/locales/common/en.yml @@ -22,6 +22,7 @@ en: help: Help lti: config_error: Error configuring LTI. + course_creation_denied: Course Creation Not Allowed course_exists: A course with this name already exists on MarkUs. Please select a course to link to. course_link_error: Unsuccessful. Please relaunch MarkUs from your LMS. course_link_success: Success. %{markus_course_name} is now linked with your LMS. diff --git a/spec/controllers/lti_deployments_controller_spec.rb b/spec/controllers/lti_deployments_controller_spec.rb index f0a9496fe2..a5d7ff9949 100644 --- a/spec/controllers/lti_deployments_controller_spec.rb +++ b/spec/controllers/lti_deployments_controller_spec.rb @@ -151,7 +151,7 @@ it 'responds with an error message' do post_as instructor, :create_course, params: course_params - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:forbidden) end end end From 2bdce841b02ef3ab7b35585fe13ba6d7da707c40 Mon Sep 17 00:00:00 2001 From: James Han Date: Thu, 25 Sep 2025 16:01:05 -0400 Subject: [PATCH 25/86] Fixed Rack HTTP status code deprecation warnings (#7675) --- Changelog.md | 1 + app/controllers/admin/courses_controller.rb | 4 ++-- app/controllers/api/assignments_controller.rb | 6 ++--- app/controllers/api/courses_controller.rb | 10 ++++----- .../api/feedback_files_controller.rb | 4 ++-- .../api/grade_entry_forms_controller.rb | 12 +++++----- app/controllers/api/groups_controller.rb | 16 +++++++------- app/controllers/api/main_api_controller.rb | 2 +- app/controllers/api/roles_controller.rb | 8 +++---- app/controllers/api/sections_controller.rb | 2 +- .../api/starter_file_groups_controller.rb | 16 +++++++------- .../api/submission_files_controller.rb | 8 +++---- app/controllers/api/tags_controller.rb | 4 ++-- app/controllers/api/users_controller.rb | 12 +++++----- app/controllers/assignments_controller.rb | 2 +- app/controllers/automated_tests_controller.rb | 6 ++--- app/controllers/courses_controller.rb | 4 ++-- app/controllers/criteria_controller.rb | 4 ++-- app/controllers/exam_templates_controller.rb | 4 ++-- app/controllers/groups_controller.rb | 6 ++--- app/controllers/lti_deployments_controller.rb | 4 ++-- app/controllers/submissions_controller.rb | 4 ++-- app/helpers/submissions_helper.rb | 6 ++--- .../admin/courses_controller_spec.rb | 4 ++-- .../api/assignments_controller_spec.rb | 12 +++++----- .../api/courses_controller_spec.rb | 4 ++-- .../api/feedback_files_controller_spec.rb | 2 +- .../api/grade_entry_forms_controller_spec.rb | 6 ++--- .../controllers/api/groups_controller_spec.rb | 18 +++++++-------- spec/controllers/api/roles_controller_spec.rb | 12 +++++----- .../api/sections_controller_spec.rb | 2 +- .../starter_file_groups_controller_spec.rb | 8 +++---- .../api/submission_files_controller_spec.rb | 2 +- spec/controllers/api/tags_controller_spec.rb | 4 ++-- spec/controllers/api/users_controller_spec.rb | 4 ++-- .../automated_tests_controller_spec.rb | 4 ++-- spec/controllers/courses_controller_spec.rb | 10 ++++----- spec/controllers/criteria_controller_spec.rb | 10 ++++----- .../exam_templates_controller_spec.rb | 4 ++-- spec/controllers/groups_controller_spec.rb | 10 ++++----- .../submissions_controller_spec.rb | 22 +++++++++---------- spec/support/lti_controller_examples.rb | 12 +++++----- 42 files changed, 148 insertions(+), 147 deletions(-) diff --git a/Changelog.md b/Changelog.md index ffb51aab3a..e494cca7c0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -17,6 +17,7 @@ - Added tests to improve coverage for `AnnotationCategory`'s `self.to_json` method - Added tests to the Criteria Controller class to achieve full test coverage - Refactored Criterion subclasses to remove redundant code +- Fixed Rack deprecation warnings by updating HTTP status code symbols (#7675) ## [v2.8.1] diff --git a/app/controllers/admin/courses_controller.rb b/app/controllers/admin/courses_controller.rb index 8572b74428..e9333cf173 100644 --- a/app/controllers/admin/courses_controller.rb +++ b/app/controllers/admin/courses_controller.rb @@ -36,7 +36,7 @@ def update def test_autotest_connection settings = current_course.autotest_setting - return head :unprocessable_entity unless settings&.url + return head :unprocessable_content unless settings&.url begin get_schema(current_course.autotest_setting) flash_now(:success, I18n.t('automated_tests.manage_connection.test_success', url: settings.url)) @@ -50,7 +50,7 @@ def test_autotest_connection def reset_autotest_connection settings = current_course.autotest_setting - return head :unprocessable_entity unless settings&.url + return head :unprocessable_content unless settings&.url @current_job = AutotestResetUrlJob.perform_later(current_course, settings.url, request.protocol + request.host_with_port, diff --git a/app/controllers/api/assignments_controller.rb b/app/controllers/api/assignments_controller.rb index d4b25704e3..4ee921fa11 100644 --- a/app/controllers/api/assignments_controller.rb +++ b/app/controllers/api/assignments_controller.rb @@ -57,7 +57,7 @@ def create if has_missing_params?([:short_identifier, :due_date, :description]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end @@ -205,7 +205,7 @@ def update_test_specs begin content = JSON.parse params[:specs] rescue JSON::ParserError => e - render 'shared/http_status', locals: { code: '422', message: e.message }, status: :unprocessable_entity + render 'shared/http_status', locals: { code: '422', message: e.message }, status: :unprocessable_content return end end @@ -213,7 +213,7 @@ def update_test_specs render 'shared/http_status', locals: { code: '422', message: HttpStatusHelper::ERROR_CODE['message']['422'] }, - status: :unprocessable_entity + status: :unprocessable_content else AutotestSpecsJob.perform_now(request.protocol + request.host_with_port, assignment, content) end diff --git a/app/controllers/api/courses_controller.rb b/app/controllers/api/courses_controller.rb index 0bf8635d4f..fe801da3cb 100644 --- a/app/controllers/api/courses_controller.rb +++ b/app/controllers/api/courses_controller.rb @@ -27,7 +27,7 @@ def show def create Course.create!(params.permit(:name, :is_hidden, :display_name)) rescue ActiveRecord::SubclassNotFound, ActiveRecord::RecordInvalid => e - render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_entity + render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_content rescue StandardError render 'shared/http_status', locals: { code: '500', message: HttpStatusHelper::ERROR_CODE['message']['500'] }, status: :internal_server_error @@ -39,7 +39,7 @@ def create def update current_course.update!(params.permit(:name, :is_hidden, :display_name)) rescue ActiveRecord::SubclassNotFound, ActiveRecord::RecordInvalid => e - render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_entity + render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_content rescue StandardError render 'shared/http_status', locals: { code: '500', message: HttpStatusHelper::ERROR_CODE['message']['500'] }, status: :internal_server_error @@ -51,7 +51,7 @@ def update def update_autotest_url AutotestResetUrlJob.perform_now(current_course, params[:url], request.protocol + request.host_with_port) rescue ActiveRecord::SubclassNotFound, ActiveRecord::RecordInvalid => e - render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_entity + render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_content rescue StandardError render 'shared/http_status', locals: { code: '500', message: HttpStatusHelper::ERROR_CODE['message']['500'] }, status: :internal_server_error @@ -83,7 +83,7 @@ def test_autotest_connection end else render 'shared/http_status', locals: { code: '422', message: - I18n.t('automated_tests.no_autotest_settings') }, status: :unprocessable_entity + I18n.t('automated_tests.no_autotest_settings') }, status: :unprocessable_content end end @@ -100,7 +100,7 @@ def reset_autotest_connection end else render 'shared/http_status', locals: { code: '422', message: - I18n.t('automated_tests.no_autotest_settings') }, status: :unprocessable_entity + I18n.t('automated_tests.no_autotest_settings') }, status: :unprocessable_content end end diff --git a/app/controllers/api/feedback_files_controller.rb b/app/controllers/api/feedback_files_controller.rb index c7489ba816..8a8422c96c 100644 --- a/app/controllers/api/feedback_files_controller.rb +++ b/app/controllers/api/feedback_files_controller.rb @@ -56,7 +56,7 @@ def create if has_missing_params?([:filename, :mime_type, :file_content]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end @@ -85,7 +85,7 @@ def create message: I18n.t('oversize_feedback_file', file_size: ActiveSupport::NumberHelper.number_to_human_size(size_diff), max_file_size: submission.course.max_file_size / 1_000_000) }, - status: :payload_too_large + status: :content_too_large return end if submission.feedback_files.create(filename: params[:filename], diff --git a/app/controllers/api/grade_entry_forms_controller.rb b/app/controllers/api/grade_entry_forms_controller.rb index efa47903e2..11127bdd0d 100644 --- a/app/controllers/api/grade_entry_forms_controller.rb +++ b/app/controllers/api/grade_entry_forms_controller.rb @@ -40,7 +40,7 @@ def create if has_missing_params?([:short_identifier]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end @@ -60,7 +60,7 @@ def create new_form = GradeEntryForm.new(create_params) unless new_form.save render 'shared/http_status', locals: { code: '422', message: - new_form.errors.full_messages.first }, status: :unprocessable_entity + new_form.errors.full_messages.first }, status: :unprocessable_content raise ActiveRecord::Rollback end @@ -69,7 +69,7 @@ def create grade_item = new_form.grade_entry_items.build(**column_params, position: i + 1) unless grade_item.save render 'shared/http_status', locals: { code: '422', message: - grade_item.errors.full_messages.first }, status: :unprocessable_entity + grade_item.errors.full_messages.first }, status: :unprocessable_content raise ActiveRecord::Rollback end end @@ -137,7 +137,7 @@ def update_grades if has_missing_params?([:user_name, :grade_entry_items]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end @@ -151,7 +151,7 @@ def update_grades if grade_entry_student.nil? # There is no student with that user_name render 'shared/http_status', locals: { code: '422', message: - 'There is no student with that user_name' }, status: :unprocessable_entity + 'There is no student with that user_name' }, status: :unprocessable_content return end @@ -162,7 +162,7 @@ def update_grades if grade_entry_item.nil? # There is no such grade entry item render 'shared/http_status', locals: { code: '422', message: - "There is no grade entry item named #{item}" }, status: :unprocessable_entity + "There is no grade entry item named #{item}" }, status: :unprocessable_content raise ActiveRecord::Rollback end grade = grade_entry_student.grades.find_or_create_by(grade_entry_item_id: grade_entry_item.id) diff --git a/app/controllers/api/groups_controller.rb b/app/controllers/api/groups_controller.rb index b284d0c4bc..bd4dbdc42b 100644 --- a/app/controllers/api/groups_controller.rb +++ b/app/controllers/api/groups_controller.rb @@ -20,7 +20,7 @@ def create end rescue StandardError => e render 'shared/http_status', locals: { code: '422', message: - e.message }, status: :unprocessable_entity + e.message }, status: :unprocessable_content end end @@ -68,7 +68,7 @@ def add_members if self.grouping.nil? # The group doesn't have a grouping associated with that assignment render 'shared/http_status', locals: { code: '422', message: - 'The group is not involved with that assignment' }, status: :unprocessable_entity + 'The group is not involved with that assignment' }, status: :unprocessable_content return end @@ -281,7 +281,7 @@ def update_marking_state if has_missing_params?([:marking_state]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end result = self.grouping&.current_submission_used&.get_latest_result @@ -336,7 +336,7 @@ def extension else # cannot delete a non existent extension; render failure render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content end when 'POST' if grouping.extension.nil? @@ -349,7 +349,7 @@ def extension else # cannot create extension as it already exists; render failure render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content end when 'PATCH' if grouping.extension.present? @@ -362,11 +362,11 @@ def extension else # cannot update extension as it does not exists; render failure render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content end end rescue ActiveRecord::RecordInvalid => e - render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_entity + render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_content end def collect_submission @@ -375,7 +375,7 @@ def collect_submission released = @grouping.current_submission_used.results.exists?(released_to_students: true) if released render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end end diff --git a/app/controllers/api/main_api_controller.rb b/app/controllers/api/main_api_controller.rb index bd0cc634a4..352f455d6a 100644 --- a/app/controllers/api/main_api_controller.rb +++ b/app/controllers/api/main_api_controller.rb @@ -70,7 +70,7 @@ def get_collection(collection) filter_params = params[:filter] ? params[:filter].permit(self.class::DEFAULT_FIELDS) : {} if params[:filter].present? && filter_params.empty? render 'shared/http_status', locals: { code: '422', message: - 'Invalid or malformed parameter values' }, status: :unprocessable_entity + 'Invalid or malformed parameter values' }, status: :unprocessable_content false elsif filter_params.empty? collection.order('id') diff --git a/app/controllers/api/roles_controller.rb b/app/controllers/api/roles_controller.rb index 4463b907be..db725c9da6 100644 --- a/app/controllers/api/roles_controller.rb +++ b/app/controllers/api/roles_controller.rb @@ -129,7 +129,7 @@ def create_role HttpStatusHelper::ERROR_CODE['message']['201'] }, status: :created end rescue ActiveRecord::SubclassNotFound, ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound => e - render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_entity + render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_content rescue StandardError render 'shared/http_status', locals: { code: '500', message: HttpStatusHelper::ERROR_CODE['message']['500'] }, status: :internal_server_error @@ -151,7 +151,7 @@ def update_role(role) render 'shared/http_status', locals: { code: '200', message: HttpStatusHelper::ERROR_CODE['message']['200'] }, status: :ok rescue ActiveRecord::SubclassNotFound, ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound => e - render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_entity + render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_content rescue StandardError render 'shared/http_status', locals: { code: '500', message: HttpStatusHelper::ERROR_CODE['message']['500'] }, status: :internal_server_error @@ -161,7 +161,7 @@ def find_role_by_username if has_missing_params?([:user_name]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end @@ -181,7 +181,7 @@ def filtered_roles if filter_params.empty? render 'shared/http_status', locals: { code: '422', message: 'Invalid or malformed parameter values' }, - status: :unprocessable_entity + status: :unprocessable_content return false else return collection.where(**filter_params) diff --git a/app/controllers/api/sections_controller.rb b/app/controllers/api/sections_controller.rb index ef9e2391e2..a4e5a57eb1 100644 --- a/app/controllers/api/sections_controller.rb +++ b/app/controllers/api/sections_controller.rb @@ -35,7 +35,7 @@ def create locals: { code: '201', message: HttpStatusHelper::ERROR_CODE['message']['201'] }, status: :created else render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content end end diff --git a/app/controllers/api/starter_file_groups_controller.rb b/app/controllers/api/starter_file_groups_controller.rb index 80675f58f8..11772f7e3d 100644 --- a/app/controllers/api/starter_file_groups_controller.rb +++ b/app/controllers/api/starter_file_groups_controller.rb @@ -61,7 +61,7 @@ def create_file if has_missing_params?([:filename, :file_content]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end @@ -73,7 +73,7 @@ def create_file file_path = FileHelper.checked_join(starter_file_group.path, params[:filename]) if file_path.nil? render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content else File.write(file_path, content, mode: 'wb') update_entries_and_warn(starter_file_group) @@ -91,14 +91,14 @@ def create_folder if has_missing_params?([:folder_path]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end folder_path = FileHelper.checked_join(starter_file_group.path, params[:folder_path]) if folder_path.nil? render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content else FileUtils.mkdir_p(folder_path) update_entries_and_warn(starter_file_group) @@ -116,13 +116,13 @@ def remove_file if has_missing_params?([:filename]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end file_path = FileHelper.checked_join(starter_file_group.path, params[:filename]) if file_path.nil? render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content else File.delete(file_path) update_entries_and_warn(starter_file_group) @@ -140,14 +140,14 @@ def remove_folder if has_missing_params?([:folder_path]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end folder_path = FileHelper.checked_join(starter_file_group.path, params[:folder_path]) if folder_path.nil? render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content else FileUtils.rm_rf(folder_path) update_entries_and_warn(starter_file_group) diff --git a/app/controllers/api/submission_files_controller.rb b/app/controllers/api/submission_files_controller.rb index 60d94ace0b..59160daedb 100644 --- a/app/controllers/api/submission_files_controller.rb +++ b/app/controllers/api/submission_files_controller.rb @@ -40,7 +40,7 @@ def index file = revision.files_at_path(File.join(assignment.repository_folder, path))[file_name] if file.nil? render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end file_contents = repo.download_as_string(file) @@ -88,7 +88,7 @@ def create_folders if has_missing_params?([:folder_path]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end success, messages = grouping.access_repo do |repo| @@ -115,7 +115,7 @@ def remove_file if has_missing_params?([:filename]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end @@ -143,7 +143,7 @@ def remove_folder if has_missing_params?([:folder_path]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end success, messages = grouping.access_repo do |repo| diff --git a/app/controllers/api/tags_controller.rb b/app/controllers/api/tags_controller.rb index 932cf24d84..f76e7c97dc 100644 --- a/app/controllers/api/tags_controller.rb +++ b/app/controllers/api/tags_controller.rb @@ -36,7 +36,7 @@ def create grouping.tags << new_tag end rescue StandardError => e - render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_entity + render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_content else render 'shared/http_status', locals: { code: '201', message: HttpStatusHelper::ERROR_CODE['message']['201'] }, status: :created @@ -50,7 +50,7 @@ def update begin tag.update!(**self.tag_params) rescue StandardError => e - render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_entity + render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_content else render 'shared/http_status', locals: { code: '200', message: HttpStatusHelper::ERROR_CODE['message']['200'] }, status: :ok diff --git a/app/controllers/api/users_controller.rb b/app/controllers/api/users_controller.rb index c0bc586cb6..8cb6de3f7f 100644 --- a/app/controllers/api/users_controller.rb +++ b/app/controllers/api/users_controller.rb @@ -23,7 +23,7 @@ def create if has_missing_params?([:user_name, :type, :first_name, :last_name]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end @@ -47,11 +47,11 @@ def create AdminUser.create!(params.permit(*DEFAULT_FIELDS)) else render 'shared/http_status', locals: { code: '422', message: 'Unknown user type' }, - status: :unprocessable_entity + status: :unprocessable_content return end rescue ActiveRecord::SubclassNotFound, ActiveRecord::RecordInvalid => e - render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_entity + render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_content else render 'shared/http_status', locals: { code: '201', message: HttpStatusHelper::ERROR_CODE['message']['201'] }, status: :created @@ -85,7 +85,7 @@ def update end user.update!(user_params) rescue ActiveRecord::SubclassNotFound, ActiveRecord::RecordInvalid => e - render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_entity + render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_content rescue StandardError render 'shared/http_status', locals: { code: '500', message: HttpStatusHelper::ERROR_CODE['message']['500'] }, status: :internal_server_error @@ -102,7 +102,7 @@ def update_by_username # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: HttpStatusHelper::ERROR_CODE['message']['422'] }, - status: :unprocessable_entity + status: :unprocessable_content return end @@ -113,7 +113,7 @@ def update_by_username end user.update!(user_params) rescue ActiveRecord::SubclassNotFound, ActiveRecord::RecordInvalid => e - render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_entity + render 'shared/http_status', locals: { code: '422', message: e.to_s }, status: :unprocessable_content rescue StandardError render 'shared/http_status', locals: { code: '500', message: HttpStatusHelper::ERROR_CODE['message']['500'] }, status: :internal_server_error diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index 42cf411688..afb335921c 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -536,7 +536,7 @@ def set_boolean_graders_options unless assignment.update(attributes) flash_now(:error, assignment.errors.full_messages.join(' ')) - head :unprocessable_entity + head :unprocessable_content return end head :ok diff --git a/app/controllers/automated_tests_controller.rb b/app/controllers/automated_tests_controller.rb index cf5642e704..8b7b61532d 100644 --- a/app/controllers/automated_tests_controller.rb +++ b/app/controllers/automated_tests_controller.rb @@ -219,17 +219,17 @@ def upload_specs test_specs = JSON.parse file_content rescue JSON::ParserError flash_now(:error, I18n.t('automated_tests.invalid_specs_file')) - head :unprocessable_entity + head :unprocessable_content rescue StandardError => e flash_now(:error, e.message) - head :unprocessable_entity + head :unprocessable_content else @current_job = AutotestSpecsJob.perform_later(request.protocol + request.host_with_port, assignment, test_specs) session[:job_id] = @current_job.job_id render 'shared/_poll_job' end else - head :unprocessable_entity + head :unprocessable_content end end diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index ba6a4dcafd..5e24656da5 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -79,7 +79,7 @@ def switch_role render partial: 'role_switch_handler', formats: [:js], handlers: [:erb], locals: { error: I18n.t('main.cannot_role_switch_to_self') }, - status: :unprocessable_entity + status: :unprocessable_content return end @@ -88,7 +88,7 @@ def switch_role render partial: 'role_switch_handler', formats: [:js], handlers: [:erb], locals: { error: I18n.t('main.cannot_role_switch') }, - status: :unprocessable_entity + status: :unprocessable_content return end diff --git a/app/controllers/criteria_controller.rb b/app/controllers/criteria_controller.rb index 1b911c6bb0..5a6ece2dfc 100644 --- a/app/controllers/criteria_controller.rb +++ b/app/controllers/criteria_controller.rb @@ -42,7 +42,7 @@ def create @criterion.errors.full_messages.each do |message| flash_message(:error, message) end - head :unprocessable_entity + head :unprocessable_content end end @@ -107,7 +107,7 @@ def update @criterion.errors.full_messages.each do |message| flash_message(:error, message) end - head :unprocessable_entity + head :unprocessable_content end end diff --git a/app/controllers/exam_templates_controller.rb b/app/controllers/exam_templates_controller.rb index 56303e611c..3a90083db4 100644 --- a/app/controllers/exam_templates_controller.rb +++ b/app/controllers/exam_templates_controller.rb @@ -102,7 +102,7 @@ def download_generate exam_template = record path = FileHelper.checked_join(exam_template.tmp_path, params[:file_name]) if path.nil? - head :unprocessable_entity + head :unprocessable_content else send_file(path, filename: params[:file_name], @@ -264,7 +264,7 @@ def download_error_file @assignment = record.assignment path = FileHelper.checked_join(exam_template.base_path, 'error', params[:file_name]) if path.nil? - head :unprocessable_entity + head :unprocessable_content else send_file(path, filename: params[:file_name], diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b737bed253..965ac9c06e 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -346,7 +346,7 @@ def use_another_assignment_groups target_assignment = Assignment.find(params[:assignment_id]) source_assignment = Assignment.find(params[:clone_assignment_id]) - return head :unprocessable_entity if target_assignment.course != source_assignment.course + return head :unprocessable_content if target_assignment.course != source_assignment.course if source_assignment.nil? flash_message(:warning, t('groups.clone_warning.could_not_find_source')) @@ -371,7 +371,7 @@ def accept_invitation current_role.join(@grouping) rescue ActiveRecord::RecordInvalid, RuntimeError => e flash_message(:error, e.message) - status = :unprocessable_entity + status = :unprocessable_content else m_logger = MarkusLogger.instance m_logger.log("Student '#{current_role.user_name}' joined group " \ @@ -389,7 +389,7 @@ def decline_invitation @grouping.decline_invitation(current_role) rescue RuntimeError => e flash_message(:error, e.message) - status = :unprocessable_entity + status = :unprocessable_content else m_logger = MarkusLogger.instance m_logger.log("Student '#{current_role.user_name}' declined invitation for group '#{@grouping.group.group_name}'.") diff --git a/app/controllers/lti_deployments_controller.rb b/app/controllers/lti_deployments_controller.rb index 56ed3f2f6d..90cfddc847 100644 --- a/app/controllers/lti_deployments_controller.rb +++ b/app/controllers/lti_deployments_controller.rb @@ -12,7 +12,7 @@ class LtiDeploymentsController < ApplicationController def launch if params[:client_id].blank? || params[:login_hint].blank? || params[:target_link_uri].blank? || params[:lti_message_hint].blank? - head :unprocessable_entity + head :unprocessable_content return end nonce = rand(10 ** 30).to_s.rjust(30, '0') @@ -176,7 +176,7 @@ def check_host known_lti_hosts << URI(root_url).host if known_lti_hosts.exclude?(URI(request.referer).host) render 'shared/http_status', locals: { code: '422', message: I18n.t('lti.config_error') }, - status: :unprocessable_entity, layout: false + status: :unprocessable_content, layout: false nil end end diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 1530b53ef1..b1c132bf3e 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -461,7 +461,7 @@ def update_files messages << commit_msg head :ok else - head :unprocessable_entity + head :unprocessable_content end end flash_repository_messages messages, @grouping.course @@ -507,7 +507,7 @@ def download_file max_content_size = params[:max_content_size].blank? ? -1 : params[:max_content_size].to_i if max_content_size != -1 && file_contents.size > max_content_size - head :payload_too_large + head :content_too_large return end diff --git a/app/helpers/submissions_helper.rb b/app/helpers/submissions_helper.rb index 125fc5a49a..15434b5746 100644 --- a/app/helpers/submissions_helper.rb +++ b/app/helpers/submissions_helper.rb @@ -57,7 +57,7 @@ def upload_file(grouping, only_required_files: false) if has_missing_params?([:filename, :mime_type, :file_content]) # incomplete/invalid HTTP params render 'shared/http_status', locals: { code: '422', message: - HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_entity + HttpStatusHelper::ERROR_CODE['message']['422'] }, status: :unprocessable_content return end @@ -66,7 +66,7 @@ def upload_file(grouping, only_required_files: false) if FileHelper.checked_join(path.to_s, filename).nil? message = I18n.t('errors.invalid_path') - render 'shared/http_status', locals: { code: '422', message: message }, status: :unprocessable_entity + render 'shared/http_status', locals: { code: '422', message: message }, status: :unprocessable_content return end @@ -75,7 +75,7 @@ def upload_file(grouping, only_required_files: false) if only_required_files && required_files.exclude?(filename) message = t('assignments.upload_file_requirement', file_name: params[:filename]) + "\n#{Assignment.human_attribute_name(:assignment_files)}: #{required_files.join(', ')}" - render 'shared/http_status', locals: { code: '422', message: message }, status: :unprocessable_entity + render 'shared/http_status', locals: { code: '422', message: message }, status: :unprocessable_content return end diff --git a/spec/controllers/admin/courses_controller_spec.rb b/spec/controllers/admin/courses_controller_spec.rb index 6d8b572ca3..b813d2900a 100644 --- a/spec/controllers/admin/courses_controller_spec.rb +++ b/spec/controllers/admin/courses_controller_spec.rb @@ -285,7 +285,7 @@ context 'there is no autotest_setting set' do it 'should return unprocessable_entity' do subject - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end @@ -331,7 +331,7 @@ context 'there is no autotest_setting set' do it 'should return unprocessable_entity' do subject - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end diff --git a/spec/controllers/api/assignments_controller_spec.rb b/spec/controllers/api/assignments_controller_spec.rb index 40dac04557..63e7689ff0 100644 --- a/spec/controllers/api/assignments_controller_spec.rb +++ b/spec/controllers/api/assignments_controller_spec.rb @@ -347,7 +347,7 @@ context 'missing short_id' do it 'should respond with 422' do post :create, params: params.slice(:description, :due_date, :course_id) - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'should not create an assignment' do @@ -359,7 +359,7 @@ context 'missing description' do it 'should respond with 404' do post :create, params: params.slice(:short_identifier, :due_date, :course_id) - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'should not create an assignment' do @@ -371,7 +371,7 @@ context 'missing due_date' do it 'should respond with 404' do post :create, params: params.slice(:short_identifier, :description, :course_id) - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'should not create an assignment' do @@ -545,7 +545,7 @@ expect(autotest_settings_for(assignment)).to eq({}) end - it('should not be successful') { expect(response).to have_http_status :unprocessable_entity } + it('should not be successful') { expect(response).to have_http_status :unprocessable_content } end it 'should fail if the assignment does not exist' do @@ -757,7 +757,7 @@ it 'responds with 422' do subject - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it_behaves_like 'does not submit' @@ -775,7 +775,7 @@ it 'responds with 422' do subject - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'does not create a temporary file' do diff --git a/spec/controllers/api/courses_controller_spec.rb b/spec/controllers/api/courses_controller_spec.rb index 6d7b76f40a..05fcf8064a 100644 --- a/spec/controllers/api/courses_controller_spec.rb +++ b/spec/controllers/api/courses_controller_spec.rb @@ -386,7 +386,7 @@ context 'there is no autotest_setting set' do it 'should return unprocessable_entity' do subject - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end @@ -432,7 +432,7 @@ context 'there is no autotest_setting set' do it 'should return unprocessable_entity' do subject - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end diff --git a/spec/controllers/api/feedback_files_controller_spec.rb b/spec/controllers/api/feedback_files_controller_spec.rb index 40d1452032..841603b998 100644 --- a/spec/controllers/api/feedback_files_controller_spec.rb +++ b/spec/controllers/api/feedback_files_controller_spec.rb @@ -192,7 +192,7 @@ post :create, params: { group_id: grouping.group.id, assignment_id: grouping.assignment.id, filename: filename, mime_type: 'text/plain', file_content: file_content, course_id: course.id } - expect(response).to have_http_status :payload_too_large + expect(response).to have_http_status :content_too_large end end end diff --git a/spec/controllers/api/grade_entry_forms_controller_spec.rb b/spec/controllers/api/grade_entry_forms_controller_spec.rb index 14b58df55f..69f57d0b0f 100644 --- a/spec/controllers/api/grade_entry_forms_controller_spec.rb +++ b/spec/controllers/api/grade_entry_forms_controller_spec.rb @@ -268,7 +268,7 @@ context 'missing short_id' do it 'should respond with 422' do post :create, params: params.slice(:description, :due_date, :course_id) - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'should not create an assignment' do @@ -280,7 +280,7 @@ context 'missing description' do it 'should respond with 422' do post :create, params: params.slice(:short_identifier, :due_date, :course_id) - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'should not create an assignment' do @@ -301,7 +301,7 @@ context 'where due_date is invalid' do it 'should respond with 422' do post :create, params: { **params, due_date: 'not a real date' } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end end diff --git a/spec/controllers/api/groups_controller_spec.rb b/spec/controllers/api/groups_controller_spec.rb index bd6cbd48ce..7ced0df90c 100644 --- a/spec/controllers/api/groups_controller_spec.rb +++ b/spec/controllers/api/groups_controller_spec.rb @@ -104,7 +104,7 @@ it 'raises an error - response status check' do assignment.group_name_autogenerated = false post_as instructor, :create, params: { course_id: course.id, assignment_id: assignment } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end end @@ -133,7 +133,7 @@ it 'raises an error - response status check' do post_as instructor, :create, params: { course_id: course.id, assignment_id: assignment, new_group_name: group_name } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end end @@ -260,7 +260,7 @@ it 'should reject invalid filters' do get :index, params: { assignment_id: groupings.first.assignment.id, course_id: course.id, filter: { bad_filter: 'something' } } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end end @@ -388,7 +388,7 @@ it_behaves_like 'for a different course' it 'should respond with 422' do - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'should not add the student to the group' do @@ -955,14 +955,14 @@ patch :extension, params: { assignment_id: assignment.id, course_id: course.id, id: group.id, extension: extension_params } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end context 'DELETE' do it 'should not work for a non existent extension' do delete :extension, params: { assignment_id: assignment.id, course_id: course.id, id: group.id } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end @@ -975,7 +975,7 @@ } post :extension, params: { assignment_id: assignment.id, course_id: course.id, id: group.id, extension: extension_params } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'should not allow time delta to be empty' do @@ -987,7 +987,7 @@ } post :extension, params: { assignment_id: assignment.id, course_id: course.id, id: group.id, extension: extension_params } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end end @@ -1055,7 +1055,7 @@ collect_current: true, apply_late_penalty: true, retain_existing_grading: true } - expect(response).to have_http_status :unprocessable_entity + expect(response).to have_http_status :unprocessable_content end end end diff --git a/spec/controllers/api/roles_controller_spec.rb b/spec/controllers/api/roles_controller_spec.rb index 6045688d35..815a097722 100644 --- a/spec/controllers/api/roles_controller_spec.rb +++ b/spec/controllers/api/roles_controller_spec.rb @@ -199,7 +199,7 @@ let(:user_name) { 'a!!' } it 'should raise a 422 error' do - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end @@ -207,7 +207,7 @@ let(:type) { 'Dragon' } it 'should raise a 422 error' do - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end @@ -215,7 +215,7 @@ let(:other_params) { { section_name: 'section.name' } } it 'should raise a 422 error' do - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end @@ -295,7 +295,7 @@ context 'with an invalid section name' do it 'should raise a 422 error' do put :update, params: { id: student.id, course_id: course.id, section_name: 'section.name' } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end @@ -450,7 +450,7 @@ it 'should raise a 422 error' do student = create(:student, course: course) post :create, params: { user_name: student.user_name, type: :student, course_id: course.id } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end @@ -593,7 +593,7 @@ it 'should raise a 422 error' do student = create(:student, course: course) post :create, params: { user_name: student.user_name, type: :student, course_id: course.id } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end diff --git a/spec/controllers/api/sections_controller_spec.rb b/spec/controllers/api/sections_controller_spec.rb index 4be6b30572..f27158c5ff 100644 --- a/spec/controllers/api/sections_controller_spec.rb +++ b/spec/controllers/api/sections_controller_spec.rb @@ -49,7 +49,7 @@ it 'should throw a 422 error and not create a section with when given an invalid param' do post :create, params: { course_id: course.id, section: { name: '' } } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(course.sections.find_by(name: '')).to be_nil end end diff --git a/spec/controllers/api/starter_file_groups_controller_spec.rb b/spec/controllers/api/starter_file_groups_controller_spec.rb index 887ffd6801..85f402ab72 100644 --- a/spec/controllers/api/starter_file_groups_controller_spec.rb +++ b/spec/controllers/api/starter_file_groups_controller_spec.rb @@ -348,7 +348,7 @@ end it 'returns a 422 status code' do - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'does not create the file' do @@ -411,7 +411,7 @@ end it 'returns a 422 status code' do - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'does not create the folder' do @@ -474,7 +474,7 @@ end it 'returns a 422 status code' do - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'does not delete the file' do @@ -540,7 +540,7 @@ end it 'returns a 422 status code' do - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'does not delete the folder' do diff --git a/spec/controllers/api/submission_files_controller_spec.rb b/spec/controllers/api/submission_files_controller_spec.rb index b19fbea441..728ced7028 100644 --- a/spec/controllers/api/submission_files_controller_spec.rb +++ b/spec/controllers/api/submission_files_controller_spec.rb @@ -288,7 +288,7 @@ let(:file_name) { file_names.map { |f| File.basename f }.join } it 'should return a 422 error' do - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end diff --git a/spec/controllers/api/tags_controller_spec.rb b/spec/controllers/api/tags_controller_spec.rb index 4d69e4f85b..7013fbb371 100644 --- a/spec/controllers/api/tags_controller_spec.rb +++ b/spec/controllers/api/tags_controller_spec.rb @@ -96,13 +96,13 @@ it 'should throw a 422 error if the grouping id is not valid' do post :create, params: { course_id: course.id, name: 'new_tag', assignment_id: assignment.id, grouping_id: grouping.id + 1 } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'should throw a 422 error if the assignment id is not valid' do post :create, params: { course_id: course.id, name: 'new_tag', assignment_id: assignment.id + 1, grouping_id: grouping.id } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end diff --git a/spec/controllers/api/users_controller_spec.rb b/spec/controllers/api/users_controller_spec.rb index d5a1a6a706..484e714574 100644 --- a/spec/controllers/api/users_controller_spec.rb +++ b/spec/controllers/api/users_controller_spec.rb @@ -212,7 +212,7 @@ let(:new_user) { build(:end_user, user_name: ' dragon ..') } it 'should raise a 422 error' do - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end @@ -220,7 +220,7 @@ let(:new_user) { build(:end_user, type: 'Dragon') } it 'should raise a 422 error' do - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end end diff --git a/spec/controllers/automated_tests_controller_spec.rb b/spec/controllers/automated_tests_controller_spec.rb index 4c154b4956..015509b103 100644 --- a/spec/controllers/automated_tests_controller_spec.rb +++ b/spec/controllers/automated_tests_controller_spec.rb @@ -449,7 +449,7 @@ end it 'should return a not_modified http status' do - expect(response).to have_http_status :unprocessable_entity + expect(response).to have_http_status :unprocessable_content end end @@ -465,7 +465,7 @@ end it 'should return a not_modified http status' do - expect(response).to have_http_status :unprocessable_entity + expect(response).to have_http_status :unprocessable_content end end end diff --git a/spec/controllers/courses_controller_spec.rb b/spec/controllers/courses_controller_spec.rb index d5840ebbc2..631132c25d 100644 --- a/spec/controllers/courses_controller_spec.rb +++ b/spec/controllers/courses_controller_spec.rb @@ -51,18 +51,18 @@ it 'fails the switch to the current instructor' do post_as instructor, :switch_role, params: { id: course.id, effective_user_login: instructor.user_name } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'fails the switch to another instructor' do post_as instructor, :switch_role, params: { id: course.id, effective_user_login: second_instructor.user_name } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'fails to switch to an admin' do admin = create(:admin_role, course: course) post_as instructor, :switch_role, params: { id: course.id, effective_user_login: admin.user_name } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end context 'when the current user is an admin' do @@ -70,7 +70,7 @@ it 'fails the switch to the current admin' do post_as admin_role, :switch_role, params: { id: course.id, effective_user_login: admin_role.user_name } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'switches to another instructor' do @@ -81,7 +81,7 @@ it 'fails to switch to another admin' do admin = create(:admin_role, course: course) post_as admin_role, :switch_role, params: { id: course.id, effective_user_login: admin.user_name } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end end diff --git a/spec/controllers/criteria_controller_spec.rb b/spec/controllers/criteria_controller_spec.rb index a8c5f951d2..d87df0d785 100644 --- a/spec/controllers/criteria_controller_spec.rb +++ b/spec/controllers/criteria_controller_spec.rb @@ -371,7 +371,7 @@ end it 'should respond with unprocessable entity' do - expect(subject).to respond_with(:unprocessable_entity) + expect(subject).to respond_with(:unprocessable_content) end end @@ -436,7 +436,7 @@ end it 'should respond with unprocessable entity' do - expect(subject).to respond_with(:unprocessable_entity) + expect(subject).to respond_with(:unprocessable_content) end it 'should display error messages' do @@ -597,7 +597,7 @@ end it 'should respond with unprocessable entity' do - expect(subject).to respond_with(:unprocessable_entity) + expect(subject).to respond_with(:unprocessable_content) end end end @@ -873,7 +873,7 @@ end it 'should respond with unprocessable entity' do - expect(subject).to respond_with(:unprocessable_entity) + expect(subject).to respond_with(:unprocessable_content) end end @@ -962,7 +962,7 @@ end it 'should respond with unprocessable entity' do - expect(subject).to respond_with(:unprocessable_entity) + expect(subject).to respond_with(:unprocessable_content) end end diff --git a/spec/controllers/exam_templates_controller_spec.rb b/spec/controllers/exam_templates_controller_spec.rb index 0243cd92ea..756713f4b4 100644 --- a/spec/controllers/exam_templates_controller_spec.rb +++ b/spec/controllers/exam_templates_controller_spec.rb @@ -318,7 +318,7 @@ end it 'responds with an error status code' do - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end end @@ -332,7 +332,7 @@ end it 'responds with an error status code' do - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index c439fa2d67..69c8f59107 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -1052,14 +1052,14 @@ it 'fails to accept when there is no invitation' do post_as @current_student, :accept_invitation, params: { course_id: course.id, assignment_id: grouping.assessment_id, grouping_id: grouping.id } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'fails to accept when the invitation has already been accepted' do create(:accepted_student_membership, role: @current_student, grouping: grouping) post_as @current_student, :accept_invitation, params: { course_id: course.id, assignment_id: grouping.assessment_id, grouping_id: grouping.id } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'fails to accept when another invitation has already been accepted' do @@ -1068,7 +1068,7 @@ create(:accepted_student_membership, role: @current_student, grouping: grouping2) post_as @current_student, :accept_invitation, params: { course_id: course.id, assignment_id: grouping.assessment_id, grouping_id: grouping.id } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'rejects other pending invitations' do @@ -1104,13 +1104,13 @@ create(:accepted_student_membership, role: @current_student, grouping: grouping) post_as @current_student, :decline_invitation, params: { course_id: course.id, assignment_id: grouping.assessment_id, grouping_id: grouping.id } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end it 'fails to reject when there is no invitation' do post_as @current_student, :decline_invitation, params: { course_id: course.id, assignment_id: grouping.assessment_id, grouping_id: grouping.id } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end diff --git a/spec/controllers/submissions_controller_spec.rb b/spec/controllers/submissions_controller_spec.rb index 5ede5aef7e..2c34369b38 100644 --- a/spec/controllers/submissions_controller_spec.rb +++ b/spec/controllers/submissions_controller_spec.rb @@ -194,7 +194,7 @@ post_as @student, :update_files, params: { course_id: course.id, assignment_id: @assignment.id, new_folders: [dir], path: '/' } - expect(response).to have_http_status :unprocessable_entity + expect(response).to have_http_status :unprocessable_content expect(Dir).not_to exist(File.join(@grouping.group.repo_path, @grouping.assignment.repository_folder, dir)) end @@ -204,7 +204,7 @@ params: { course_id: course.id, assignment_id: @assignment.id, delete_files: ['../../../../../LICENSE'], path: '/' } - expect(response).to have_http_status :unprocessable_entity + expect(response).to have_http_status :unprocessable_content expect(File).to exist( File.expand_path(File.join(@grouping.group.repo_path, '../../../../../LICENSE')) ) @@ -215,7 +215,7 @@ params: { course_id: course.id, assignment_id: @assignment.id, delete_folders: ['../../../../../doc'], path: '/' } - expect(response).to have_http_status :unprocessable_entity + expect(response).to have_http_status :unprocessable_content expect(Dir).to exist( File.expand_path(File.join(@grouping.group.repo_path, '../../../../../doc')) ) @@ -380,7 +380,7 @@ post_as @student, :update_files, params: { course_id: course.id, assignment_id: @assignment.id, new_files: [file1, file2] } - expect(response).to have_http_status :unprocessable_entity + expect(response).to have_http_status :unprocessable_content # Check to see if the file was added @grouping.group.access_repo do |repo| @@ -456,7 +456,7 @@ post_as @student, :update_files, params: { course_id: course.id, assignment_id: @assignment.id, new_folders: ['bad_folder'] } - expect(response).to have_http_status :unprocessable_entity + expect(response).to have_http_status :unprocessable_content end it 'does not commit the non required directory' do @@ -470,7 +470,7 @@ post_as @student, :update_files, params: { course_id: course.id, assignment_id: @assignment.id, new_folders: ['bad_folder/bad_subdirectory'] } - expect(response).to have_http_status :unprocessable_entity + expect(response).to have_http_status :unprocessable_content end it 'does not commit a non required subdirectory' do @@ -532,14 +532,14 @@ post_as @student, :update_files, params: { course_id: course.id, assignment_id: @assignment.id, new_files: ['.git'] } - expect(response).to have_http_status :unprocessable_entity + expect(response).to have_http_status :unprocessable_content end it 'should not be allowed in a zip file' do zip_file = fixture_file_upload('test_zip_git_file.zip', 'application/zip') post_as @student, :update_files, params: { course_id: course.id, assignment_id: @assignment.id, new_files: [zip_file], unzip: unzip } - expect(response).to have_http_status :unprocessable_entity + expect(response).to have_http_status :unprocessable_content end it 'should not create a .git file in the repo' do @@ -561,14 +561,14 @@ post_as @student, :update_files, params: { course_id: course.id, assignment_id: @assignment.id, new_folders: ['.git'] } - expect(response).to have_http_status :unprocessable_entity + expect(response).to have_http_status :unprocessable_content end it 'should not be allowed in a zip file' do zip_file = fixture_file_upload('test_zip_git_folder.zip', 'application/zip') post_as @student, :update_files, params: { course_id: course.id, assignment_id: @assignment.id, new_files: [zip_file], unzip: unzip } - expect(response).to have_http_status :unprocessable_entity + expect(response).to have_http_status :unprocessable_content end it 'should not create a .git directory in the repo' do @@ -2066,7 +2066,7 @@ def self.test_no_flash max_content_size: SAMPLE_FILE_CONTENT.size - 1 } end - it { expect(response).to have_http_status(:payload_too_large) } + it { expect(response).to have_http_status(:content_too_large) } it 'should have an empty response body' do expect(response.body).to be_empty diff --git a/spec/support/lti_controller_examples.rb b/spec/support/lti_controller_examples.rb index f98a709d1d..688bedd222 100644 --- a/spec/support/lti_controller_examples.rb +++ b/spec/support/lti_controller_examples.rb @@ -27,33 +27,33 @@ it 'responds with unprocessable_entity if no parameters are passed' do request.headers['Referer'] = host post :launch, params: {} - expect(subject).to respond_with(:unprocessable_entity) + expect(subject).to respond_with(:unprocessable_content) end it 'responds with unprocessable_entity if lti_message_hint is not passed' do request.headers['Referer'] = host post :launch, params: { client_id: client_id, target_link_uri: target_link_uri, login_hint: login_hint } - expect(subject).to respond_with(:unprocessable_entity) + expect(subject).to respond_with(:unprocessable_content) end it 'responds with unprocessable_entity if client_id is not passed' do request.headers['Referer'] = host post :launch, params: { lti_message_hint: lti_message_hint, target_link_uri: target_link_uri, login_hint: login_hint } - expect(subject).to respond_with(:unprocessable_entity) + expect(subject).to respond_with(:unprocessable_content) end it 'responds with unprocessable_entity if target_link_uri is not passed' do request.headers['Referer'] = host post :launch, params: { lti_message_hint: lti_message_hint, client_id: client_id, login_hint: login_hint } - expect(subject).to respond_with(:unprocessable_entity) + expect(subject).to respond_with(:unprocessable_content) end it 'responds with unprocessable_entity if login_hint is not passed' do request.headers['Referer'] = host post :launch, params: { lti_message_hint: lti_message_hint, client_id: client_id, target_link_uri: target_link_uri } - expect(subject).to respond_with(:unprocessable_entity) + expect(subject).to respond_with(:unprocessable_content) end context 'when all required params exist' do @@ -294,7 +294,7 @@ post_as instructor, :launch, params: { lti_message_hint: 'hint', login_hint: 'hint', client_id: 'LMS defined ID', target_link_uri: 'test.com' } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end end From 0ad15d39663862ed0a6cc36db5559be217b59797 Mon Sep 17 00:00:00 2001 From: donny-wong <141858744+donny-wong@users.noreply.github.com> Date: Fri, 26 Sep 2025 11:17:52 -0400 Subject: [PATCH 26/86] Provided file viewer the option to render Microsoft files (#7676) --- Changelog.md | 1 + app/lib/file_helper.rb | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index e494cca7c0..3f09601131 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,7 @@ ### ✨ New features and improvements - Added new loading spinner icon for tables (#7602) - Update message and page displaying cannot create new course via external LTI tool (#7669) +- Provide file viewer the option to render Microsoft files (#7676) ### 🐛 Bug fixes - Resque Host Authorization, removing env condition as this is for all environments (#7671) diff --git a/app/lib/file_helper.rb b/app/lib/file_helper.rb index 628e455858..fbd650611c 100644 --- a/app/lib/file_helper.rb +++ b/app/lib/file_helper.rb @@ -43,7 +43,13 @@ module FileHelper '.make' => 'makefile', '.markusurl' => 'markusurl', '.bin' => 'binary', - '.dat' => 'binary' }.freeze + '.dat' => 'binary', + '.doc' => 'binary', + '.docx' => 'binary', + '.xls' => 'binary', + '.xlsx' => 'binary', + '.ppt' => 'binary', + '.pptx' => 'binary' }.freeze COMMENT_TO_SYNTAX = { '.java' => %w[/* */], '.js' => %w[/* */], From 071c4262cab6f17eafe4044bbfab0bafa95f9f23 Mon Sep 17 00:00:00 2001 From: James Han Date: Fri, 26 Sep 2025 11:20:10 -0400 Subject: [PATCH 27/86] Fixed N+1 query in StudentsController#index by eager loading user association (#7678) --- Changelog.md | 1 + app/controllers/students_controller.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 3f09601131..5fadfcae8f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,7 @@ - Provide file viewer the option to render Microsoft files (#7676) ### 🐛 Bug fixes +- Fixed N+1 query problem in StudentsController by eager loading user association (#7678) - Resque Host Authorization, removing env condition as this is for all environments (#7671) ### 🔧 Internal changes diff --git a/app/controllers/students_controller.rb b/app/controllers/students_controller.rb index a1b23d4057..8279eba498 100644 --- a/app/controllers/students_controller.rb +++ b/app/controllers/students_controller.rb @@ -9,7 +9,7 @@ def index respond_to do |format| format.html format.json do - student_data = current_course.students.includes(:grace_period_deductions, :section).map do |s| + student_data = current_course.students.includes(:grace_period_deductions, :section, :user).map do |s| { _id: s.id, user_name: s.user_name, From f09ff463e54561434f7297c03ac6088b7df9bcc0 Mon Sep 17 00:00:00 2001 From: Elizabeth Liu <157079783+lizzie-liu@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:50:42 -0400 Subject: [PATCH 28/86] Ordered assignments by due date on index page and quick links dropdown (#7679) --- Changelog.md | 1 + app/views/assignments/_list_manage.html.erb | 2 +- app/views/shared/_assignments_dropdown_menu.html.erb | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index 5fadfcae8f..b72f9a01a9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -12,6 +12,7 @@ ### 🐛 Bug fixes - Fixed N+1 query problem in StudentsController by eager loading user association (#7678) - Resque Host Authorization, removing env condition as this is for all environments (#7671) +- Fixed ordering of assignments in the Assignment dropdown menu and Assignment index page (#7642) ### 🔧 Internal changes - Updated Github Actions CI to use cache-apt-pkgs to speed up workflow runs (#7645) diff --git a/app/views/assignments/_list_manage.html.erb b/app/views/assignments/_list_manage.html.erb index 88f13e458e..b3d49ad16a 100644 --- a/app/views/assignments/_list_manage.html.erb +++ b/app/views/assignments/_list_manage.html.erb @@ -1,6 +1,6 @@ <% assignments = @current_course.assignments .includes(:submission_rule, :assessment_section_properties, :pr_assignment) - .order(:id) %> + .order(:due_date, :short_identifier) %> <% action = @current_role.instructor? ? 'edit' : 'summary' %> <% if assignments.empty? %> diff --git a/app/views/shared/_assignments_dropdown_menu.html.erb b/app/views/shared/_assignments_dropdown_menu.html.erb index 28e3823f39..07f5e85c6d 100644 --- a/app/views/shared/_assignments_dropdown_menu.html.erb +++ b/app/views/shared/_assignments_dropdown_menu.html.erb @@ -28,7 +28,7 @@
  • <%= Assignment.model_name.human.pluralize %>
  • -<% assignments = @current_role.visible_assessments(assessment_type: 'Assignment') %> +<% assignments = @current_role.visible_assessments(assessment_type: 'Assignment').order(:due_date, :short_identifier) %> <% grade_entry_forms = @current_role.visible_assessments(assessment_type: 'GradeEntryForm') %> <%= render partial: 'shared/assignment_dropdown_link', From 62d1155acc1097170ac217778a4601921e47fe78 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:48:39 -0400 Subject: [PATCH 29/86] [pre-commit.ci] pre-commit autoupdate (#7631) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0) - [github.com/thibaudcolas/pre-commit-stylelint: v16.21.1 → v16.23.1](https://github.com/thibaudcolas/pre-commit-stylelint/compare/v16.21.1...v16.23.1) - [github.com/rubocop/rubocop: v1.77.0 → v1.80.1](https://github.com/rubocop/rubocop/compare/v1.77.0...v1.80.1) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- app/controllers/admin/courses_controller.rb | 1 + app/controllers/api/courses_controller.rb | 1 + app/controllers/api/submission_files_controller.rb | 1 + app/controllers/assignments_controller.rb | 1 + app/controllers/grade_entry_forms_controller.rb | 1 + app/controllers/submissions_controller.rb | 1 + app/helpers/automated_tests_helper.rb | 1 + app/jobs/autotest_job.rb | 1 + app/models/autotest_user.rb | 1 + app/models/marking_scheme.rb | 1 + config/initializers/rails_performance.rb | 1 + lib/tasks/autotest.rake | 1 + spec/controllers/course_summaries_controller_spec.rb | 1 + spec/controllers/main_controller_spec.rb | 1 + spec/helpers/automated_tests_helper_spec.rb | 1 + spec/jobs/autotest_reset_url_job_spec.rb | 1 + spec/mailers/notification_mailer_spec.rb | 1 + spec/support/course_association_shared_examples.rb | 1 + 19 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7de877eb60..b5b27bd8f9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-illegal-windows-names - id: check-json @@ -22,7 +22,7 @@ repos: - id: prettier types_or: [javascript, jsx, css, scss, html] - repo: https://github.com/thibaudcolas/pre-commit-stylelint - rev: v16.21.1 + rev: v16.23.1 hooks: - id: stylelint additional_dependencies: [ @@ -39,7 +39,7 @@ repos: app/assets/stylesheets/common/_reset.scss )$ - repo: https://github.com/rubocop/rubocop - rev: v1.77.0 + rev: v1.80.1 hooks: - id: rubocop args: ["--autocorrect"] diff --git a/app/controllers/admin/courses_controller.rb b/app/controllers/admin/courses_controller.rb index e9333cf173..901a81a902 100644 --- a/app/controllers/admin/courses_controller.rb +++ b/app/controllers/admin/courses_controller.rb @@ -1,6 +1,7 @@ module Admin class CoursesController < ApplicationController include AutomatedTestsHelper::AutotestApi + DEFAULT_FIELDS = [:id, :name, :is_hidden, :display_name].freeze before_action { authorize! } diff --git a/app/controllers/api/courses_controller.rb b/app/controllers/api/courses_controller.rb index fe801da3cb..3abd94843d 100644 --- a/app/controllers/api/courses_controller.rb +++ b/app/controllers/api/courses_controller.rb @@ -2,6 +2,7 @@ module Api # API controller for Courses class CoursesController < MainApiController include AutomatedTestsHelper::AutotestApi + DEFAULT_FIELDS = [:id, :name, :is_hidden, :display_name].freeze def index diff --git a/app/controllers/api/submission_files_controller.rb b/app/controllers/api/submission_files_controller.rb index 59160daedb..feda235d17 100644 --- a/app/controllers/api/submission_files_controller.rb +++ b/app/controllers/api/submission_files_controller.rb @@ -3,6 +3,7 @@ module Api # Uses Rails' RESTful routes (check 'rake routes' for the configured routes) class SubmissionFilesController < MainApiController include SubmissionsHelper + # Returns the requested submission file, or a zip containing all submission # files, including all annotations if requested # Requires: assignment_id, group_id diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index afb335921c..1624040503 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -2,6 +2,7 @@ class AssignmentsController < ApplicationController include RepositoryHelper include RoutingHelper include AutomatedTestsHelper + responders :flash before_action { authorize! } diff --git a/app/controllers/grade_entry_forms_controller.rb b/app/controllers/grade_entry_forms_controller.rb index fd1e206b6f..a5f4ab4ae5 100644 --- a/app/controllers/grade_entry_forms_controller.rb +++ b/app/controllers/grade_entry_forms_controller.rb @@ -3,6 +3,7 @@ class GradeEntryFormsController < ApplicationController include GradeEntryFormsHelper include RoutingHelper + before_action { authorize! } layout 'assignment_content' diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index b1c132bf3e..3f3d9e6cf5 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -3,6 +3,7 @@ class SubmissionsController < ApplicationController include SubmissionsHelper include RepositoryHelper + before_action { authorize! } authorize :from_codeviewer, through: :from_codeviewer_param diff --git a/app/helpers/automated_tests_helper.rb b/app/helpers/automated_tests_helper.rb index fd61f88c0f..5aa6a1696c 100644 --- a/app/helpers/automated_tests_helper.rb +++ b/app/helpers/automated_tests_helper.rb @@ -121,6 +121,7 @@ def get_markus_address(host_with_port) # Sends RESTful api requests to the autotester module AutotestApi include AutomatedTestsHelper + AUTOTEST_USERNAME = "markus_#{Rails.application.config.relative_url_root}".freeze class LimitExceededException < StandardError; end diff --git a/app/jobs/autotest_job.rb b/app/jobs/autotest_job.rb index 846ea0f751..0c936925a8 100644 --- a/app/jobs/autotest_job.rb +++ b/app/jobs/autotest_job.rb @@ -2,6 +2,7 @@ class AutotestJob < ApplicationJob include AutomatedTestsHelper include AutomatedTestsHelper::AutotestApi + around_perform do |job, block| block.call rescue LimitExceededException diff --git a/app/models/autotest_user.rb b/app/models/autotest_user.rb index 09bb8f37c8..790024c88d 100644 --- a/app/models/autotest_user.rb +++ b/app/models/autotest_user.rb @@ -1,6 +1,7 @@ # Model for Autotest user class AutotestUser < User include AutomatedTestsHelper::AutotestApi + USERNAME = '.autotestuser'.freeze def self.find_or_create diff --git a/app/models/marking_scheme.rb b/app/models/marking_scheme.rb index 2422ebea6a..9fb684ec38 100644 --- a/app/models/marking_scheme.rb +++ b/app/models/marking_scheme.rb @@ -1,5 +1,6 @@ class MarkingScheme < ApplicationRecord include CourseSummariesHelper + has_many :marking_weights, dependent: :destroy accepts_nested_attributes_for :marking_weights validates :name, uniqueness: { scope: :course_id } diff --git a/config/initializers/rails_performance.rb b/config/initializers/rails_performance.rb index 6490700f54..03a3b6f071 100644 --- a/config/initializers/rails_performance.rb +++ b/config/initializers/rails_performance.rb @@ -4,6 +4,7 @@ if Settings.rails_performance.enabled RailsPerformance::RailsPerformanceController.class_eval do include SessionHandler + before_action :check_user_not_authorized # Modify CSP for Performance Gem diff --git a/lib/tasks/autotest.rake b/lib/tasks/autotest.rake index c60a4f45f7..53cc2f1e23 100644 --- a/lib/tasks/autotest.rake +++ b/lib/tasks/autotest.rake @@ -2,6 +2,7 @@ namespace :db do desc 'Sets up environment to test the autotester' task autotest: :environment do include AutomatedTestsHelper + puts 'Set up testing environment for autotest' markus_url = ENV.fetch('MARKUS_URL', nil) || raise('no MARKUS_URL environment variable is set') autotest_url = ENV.fetch('AUTOTEST_URL', nil) diff --git a/spec/controllers/course_summaries_controller_spec.rb b/spec/controllers/course_summaries_controller_spec.rb index c359944505..34e8c0b1bc 100644 --- a/spec/controllers/course_summaries_controller_spec.rb +++ b/spec/controllers/course_summaries_controller_spec.rb @@ -1,6 +1,7 @@ describe CourseSummariesController do # TODO: add 'role is from a different course' shared tests to each route test below include CourseSummariesHelper + context 'An instructor' do let(:instructor) { create(:instructor) } let(:course) { instructor.course } diff --git a/spec/controllers/main_controller_spec.rb b/spec/controllers/main_controller_spec.rb index a9ecbd49a5..645893e427 100644 --- a/spec/controllers/main_controller_spec.rb +++ b/spec/controllers/main_controller_spec.rb @@ -1,5 +1,6 @@ describe MainController do include SessionHandler + let(:student) { create(:student) } let(:ta) { create(:ta) } let(:instructor) { create(:instructor) } diff --git a/spec/helpers/automated_tests_helper_spec.rb b/spec/helpers/automated_tests_helper_spec.rb index ccefc1d671..1eb40287a7 100644 --- a/spec/helpers/automated_tests_helper_spec.rb +++ b/spec/helpers/automated_tests_helper_spec.rb @@ -1,5 +1,6 @@ describe AutomatedTestsHelper do include ApplicationHelper + describe '.update_test_groups_from_specs' do subject { update_test_groups_from_specs assignment, specs } diff --git a/spec/jobs/autotest_reset_url_job_spec.rb b/spec/jobs/autotest_reset_url_job_spec.rb index 389f7cd368..80a52959ae 100644 --- a/spec/jobs/autotest_reset_url_job_spec.rb +++ b/spec/jobs/autotest_reset_url_job_spec.rb @@ -1,5 +1,6 @@ describe AutotestResetUrlJob do include AutomatedTestsHelper + let(:host_with_port) { 'http://localhost:3000' } let(:url) { 'http://example.com' } let(:course) { create(:course) } diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb index d2dfc94021..567cba3eef 100644 --- a/spec/mailers/notification_mailer_spec.rb +++ b/spec/mailers/notification_mailer_spec.rb @@ -2,6 +2,7 @@ RSpec.describe NotificationMailer do include ERB::Util + RSpec.shared_examples 'an email' do it 'renders the disclaimer in the body of the email.' do expect(mail.body.to_s).to include('This is an automated email. Please do not reply.') diff --git a/spec/support/course_association_shared_examples.rb b/spec/support/course_association_shared_examples.rb index 8d36edffb4..5a24dd5973 100644 --- a/spec/support/course_association_shared_examples.rb +++ b/spec/support/course_association_shared_examples.rb @@ -1,5 +1,6 @@ shared_examples 'course associations' do include CourseAssociationHelper + it 'should be valid when all belongs_to associations belong to the same course' do expect(subject).to be_valid end From 3a3170e68a8ee274225431a589f95a8e7789ab10 Mon Sep 17 00:00:00 2001 From: Freya Zhang <149322974+freyazjiner@users.noreply.github.com> Date: Tue, 30 Sep 2025 09:49:35 -0400 Subject: [PATCH 30/86] Converted "Rename Group" modal to React component (#7673) --- Changelog.md | 1 + app/assets/javascripts/Groups/index.js | 1 - .../Components/Modals/rename_group_modal.jsx | 70 +++++++++++++++++ .../__tests__/rename_group_modal.test.jsx | 76 +++++++++++++++++++ app/javascript/Components/groups_manager.jsx | 58 +++++++++----- app/views/groups/_rename_group_modal.html.erb | 12 --- app/views/groups/index.html.erb | 1 - 7 files changed, 184 insertions(+), 35 deletions(-) create mode 100644 app/javascript/Components/Modals/rename_group_modal.jsx create mode 100644 app/javascript/Components/__tests__/rename_group_modal.test.jsx delete mode 100644 app/views/groups/_rename_group_modal.html.erb diff --git a/Changelog.md b/Changelog.md index b72f9a01a9..056b73eed5 100644 --- a/Changelog.md +++ b/Changelog.md @@ -20,6 +20,7 @@ - Added tests to improve coverage for `AnnotationCategory`'s `self.to_json` method - Added tests to the Criteria Controller class to achieve full test coverage - Refactored Criterion subclasses to remove redundant code +- Converted "Rename Group" functionality to React modal (#7673) - Fixed Rack deprecation warnings by updating HTTP status code symbols (#7675) ## [v2.8.1] diff --git a/app/assets/javascripts/Groups/index.js b/app/assets/javascripts/Groups/index.js index aae50ca6f7..af855cd707 100644 --- a/app/assets/javascripts/Groups/index.js +++ b/app/assets/javascripts/Groups/index.js @@ -4,7 +4,6 @@ var modalCreate, (function () { const domContentLoadedCB = function () { - window.modal_rename = new ModalMarkus("#rename_group_dialog"); modalNotesGroup = new ModalMarkus("#notes_dialog"); modalAssignmentGroupReUse = new ModalMarkus("#assignment_group_use_dialog"); }; diff --git a/app/javascript/Components/Modals/rename_group_modal.jsx b/app/javascript/Components/Modals/rename_group_modal.jsx new file mode 100644 index 0000000000..51873ff8ae --- /dev/null +++ b/app/javascript/Components/Modals/rename_group_modal.jsx @@ -0,0 +1,70 @@ +import React from "react"; +import Modal from "react-modal"; + +export default class RenameGroupModal extends React.Component { + constructor(props) { + super(props); + this.state = { + groupName: props.initialGroupName || "", + }; + } + + componentDidMount() { + Modal.setAppElement("body"); + } + + componentDidUpdate(prevProps) { + if (this.props.isOpen && !prevProps.isOpen && this.props.initialGroupName) { + this.setState({groupName: this.props.initialGroupName}); + } + } + + handleChange = event => { + this.setState({groupName: event.target.value}); + }; + + handleSubmit = event => { + event.preventDefault(); + if (!this.state.groupName) { + return; + } + this.props.onSubmit(this.state.groupName); + this.setState({groupName: ""}); + }; + + render() { + return ( + +

    {I18n.t("groups.rename_group")}

    +
    + + this.handleChange(event)} + autoFocus + /> +
    + + +
    + +
    + ); + } +} diff --git a/app/javascript/Components/__tests__/rename_group_modal.test.jsx b/app/javascript/Components/__tests__/rename_group_modal.test.jsx new file mode 100644 index 0000000000..24e87f4fad --- /dev/null +++ b/app/javascript/Components/__tests__/rename_group_modal.test.jsx @@ -0,0 +1,76 @@ +import React from "react"; +import {render, screen, fireEvent, waitFor} from "@testing-library/react"; +import RenameGroupModal from "../Modals/rename_group_modal"; +import Modal from "react-modal"; +import {expect} from "@jest/globals"; + +describe("RenameGroupModal", () => { + let props; + + beforeEach(() => { + props = { + isOpen: true, + onRequestClose: jest.fn(), + onSubmit: jest.fn(), + }; + + Modal.setAppElement("body"); + render(); + }); + + it("should rename the group on successful submit", async () => { + const groupName = "new_group"; + fireEvent.change(screen.getByLabelText(I18n.t("activerecord.attributes.group.group_name")), { + target: {value: groupName}, + }); + + const renameGroupButton = screen.getByTestId("rename-submit-button"); + fireEvent.click(renameGroupButton); + + await waitFor(() => { + expect(props.onSubmit).toHaveBeenCalledTimes(1); + expect(props.onSubmit).toHaveBeenCalledWith(groupName); + }); + }); + + it("should call onRequestClose on successful call", async () => { + fireEvent.click(screen.getByText(I18n.t("cancel"))); + await waitFor(() => { + expect(props.onRequestClose).toHaveBeenCalledTimes(1); + }); + }); + + it("should not add the new group when the inputted group name is empty", async () => { + const renameGroupButton = screen.getByTestId("rename-submit-button"); + fireEvent.click(renameGroupButton); + + await waitFor(() => { + expect(props.onSubmit).not.toHaveBeenCalled(); + }); + }); + + it("should populate input with initialGroupName when modal opens", async () => { + const propsWithInitialName = { + isOpen: true, + onRequestClose: jest.fn(), + onSubmit: jest.fn(), + initialGroupName: "Original Group Name", + }; + + const {rerender} = render(); + rerender(); + + expect(screen.getByDisplayValue("Original Group Name")).toBeInTheDocument(); + }); + + it("should not submit when group name is empty", async () => { + fireEvent.change(screen.getByLabelText(I18n.t("activerecord.attributes.group.group_name")), { + target: {value: ""}, + }); + + const form = document.querySelector("form"); + fireEvent.submit(form); + + expect(props.onSubmit).not.toHaveBeenCalled(); + }); +}); diff --git a/app/javascript/Components/groups_manager.jsx b/app/javascript/Components/groups_manager.jsx index 752c102dbe..1694b2322e 100644 --- a/app/javascript/Components/groups_manager.jsx +++ b/app/javascript/Components/groups_manager.jsx @@ -7,6 +7,7 @@ import ExtensionModal from "./Modals/extension_modal"; import {durationSort, selectFilter} from "./Helpers/table_helpers"; import AutoMatchModal from "./Modals/auto_match_modal"; import CreateGroupModal from "./Modals/create_group_modal"; +import RenameGroupModal from "./Modals/rename_group_modal"; class GroupsManager extends React.Component { constructor(props) { @@ -17,11 +18,14 @@ class GroupsManager extends React.Component { show_hidden: false, hidden_students_count: 0, inactive_groups_count: 0, + renameGroupingId: null, + renameGroupName: "", show_modal: false, selected_extension_data: {}, updating_extension: false, isAutoMatchModalOpen: false, isCreateGroupModalOpen: false, + isRenameGroupDialogOpen: false, examTemplates: [], loading: true, }; @@ -29,21 +33,8 @@ class GroupsManager extends React.Component { componentDidMount() { this.fetchData(); - // TODO: Remove reliance on global modal - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", this.componentDidMountCB); - } else { - this.componentDidMountCB(); - } } - componentDidMountCB = () => { - $("#rename_group_dialog form").on("ajax:success", () => { - modal_rename.close(); - this.fetchData(); - }); - }; - fetchData = () => { fetch(Routes.course_assignment_groups_path(this.props.course_id, this.props.assignment_id), { headers: { @@ -131,13 +122,32 @@ class GroupsManager extends React.Component { ).then(this.fetchData); }; - renameGroup = grouping_id => { - $("#new_groupname").val(""); - $("#rename_group_dialog form").attr( - "action", - Routes.rename_group_course_group_path(this.props.course_id, grouping_id) - ); - modal_rename.open(); + renameGroup = (grouping_id, group_name) => { + this.setState({ + isRenameGroupDialogOpen: true, + renameGroupingId: grouping_id, + renameGroupName: group_name, + }); + }; + + handleRenameGroupDialog = newGroupName => { + $.post({ + url: Routes.rename_group_course_group_path(this.props.course_id, this.state.renameGroupingId), + data: { + new_groupname: newGroupName, + }, + }).then(() => { + this.setState({isRenameGroupDialogOpen: false}); + this.fetchData(); + }); + }; + + handleCloseRenameGroupDialog = () => { + this.setState({ + isRenameGroupDialogOpen: false, + renameGroupingId: null, + renameGroupName: "", + }); }; unassign = (grouping_id, student_user_name) => { @@ -359,6 +369,12 @@ class GroupsManager extends React.Component { onRequestClose={this.handleCloseCreateGroupModal} onSubmit={this.handleSubmitCreateGroup} /> + ); } @@ -396,7 +412,7 @@ class RawGroupsTable extends React.Component { {row.value} this.props.renameGroup(row.original._id)} + onClick={() => this.props.renameGroup(row.original._id, row.value)} title={I18n.t("groups.rename_group")} > diff --git a/app/views/groups/_rename_group_modal.html.erb b/app/views/groups/_rename_group_modal.html.erb deleted file mode 100644 index 92ce4621fc..0000000000 --- a/app/views/groups/_rename_group_modal.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -<%= content_for :modal_id, 'rename_group_dialog' %> -<%= content_for :modal_title, t('groups.rename_group') %> -<%= content_for :modal_content do %> - <%= form_tag '', remote: true do %> - <%= label_tag :new_groupname, Group.human_attribute_name(:group_name) %> - <%= text_field_tag :new_groupname, '', maxlength: 30, autocomplete: 'off' %> -
    - <%= submit_tag t('groups.rename_group') %> - -
    - <% end %> -<% end %> diff --git a/app/views/groups/index.html.erb b/app/views/groups/index.html.erb index 7771013d15..fa533f8a28 100644 --- a/app/views/groups/index.html.erb +++ b/app/views/groups/index.html.erb @@ -64,5 +64,4 @@ <%= render partial: 'download_modal', layout: 'layouts/modal_dialog' %> <%= render partial: 'upload_modal', layout: 'layouts/modal_dialog' %> <%= render partial: 'assignment_group_use_modal', layout: 'layouts/modal_dialog' %> -<%= render partial: 'rename_group_modal', layout: 'layouts/modal_dialog' %> From 84cadacc271c8a6bb5987b4ab5efa70882f85063 Mon Sep 17 00:00:00 2001 From: donny-wong <141858744+donny-wong@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:22:46 -0400 Subject: [PATCH 31/86] Prevented marks spreadsheet grade change when scrolling or using arrow keys (#7680) --- Changelog.md | 1 + app/javascript/Components/marks_spreadsheet.jsx | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/Changelog.md b/Changelog.md index 056b73eed5..c42bf3268d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -13,6 +13,7 @@ - Fixed N+1 query problem in StudentsController by eager loading user association (#7678) - Resque Host Authorization, removing env condition as this is for all environments (#7671) - Fixed ordering of assignments in the Assignment dropdown menu and Assignment index page (#7642) +- Prevent grade change in the grades table for a Marks Spreadsheet, when scrolling up or down with mouse or keys (#7680) ### 🔧 Internal changes - Updated Github Actions CI to use cache-apt-pkgs to speed up workflow runs (#7645) diff --git a/app/javascript/Components/marks_spreadsheet.jsx b/app/javascript/Components/marks_spreadsheet.jsx index 71a7fc337d..0fb5d4074f 100644 --- a/app/javascript/Components/marks_spreadsheet.jsx +++ b/app/javascript/Components/marks_spreadsheet.jsx @@ -409,6 +409,12 @@ class GradeEntryCell extends React.Component { value={this.state.value} min={this.props.bonus ? "" : 0} onChange={this.handleChange} + onWheel={e => e.currentTarget.blur()} // prevent scroll changing value + onKeyDown={e => { + if (e.key === "ArrowUp" || e.key === "ArrowDown") { + e.preventDefault(); // block arrow keys changing value + } + }} /> ); } From 31db8c349449fe283d5725cc5b6277af6e9f9e0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:30:28 -0400 Subject: [PATCH 32/86] build(deps): bump the babel group with 2 updates (#7684) Bumps the babel group with 2 updates: [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) and [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core). Updates `@babel/runtime` from 7.28.3 to 7.28.4 - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.28.4/packages/babel-runtime) Updates `@babel/core` from 7.28.3 to 7.28.4 - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.28.4/packages/babel-core) --- updated-dependencies: - dependency-name: "@babel/runtime" dependency-version: 7.28.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: babel - dependency-name: "@babel/core" dependency-version: 7.28.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: babel ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 130 ++++++++++++++++------------------------------ package.json | 4 +- 2 files changed, 48 insertions(+), 86 deletions(-) diff --git a/package-lock.json b/package-lock.json index a607af4caf..a1679efdd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "MarkUs", "dependencies": { - "@babel/runtime": "^7.28.3", + "@babel/runtime": "^7.28.4", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.7.2", @@ -47,7 +47,7 @@ "ui-contextmenu": "^1.18.1" }, "devDependencies": { - "@babel/core": "^7.28.3", + "@babel/core": "^7.28.4", "@babel/plugin-transform-runtime": "^7.28.3", "@babel/preset-env": "^7.28.3", "@babel/preset-react": "^7.27.1", @@ -78,19 +78,6 @@ "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", "dev": true }, - "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -130,22 +117,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.3", - "@babel/parser": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -177,16 +164,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", @@ -461,27 +438,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -1904,9 +1881,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1928,18 +1905,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.3", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", + "@babel/types": "^7.28.4", "debug": "^4.3.1" }, "engines": { @@ -1947,9 +1924,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2755,16 +2732,25 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - }, - "engines": { - "node": ">=6.0.0" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/resolve-uri": { @@ -2776,15 +2762,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/source-map": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", @@ -2796,21 +2773,6 @@ "@jridgewell/trace-mapping": "^0.3.25" } }, - "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", diff --git a/package.json b/package.json index 320bd0d555..44eed77646 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "MarkUs", "dependencies": { - "@babel/runtime": "^7.28.3", + "@babel/runtime": "^7.28.4", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.7.2", @@ -42,7 +42,7 @@ "ui-contextmenu": "^1.18.1" }, "devDependencies": { - "@babel/core": "^7.28.3", + "@babel/core": "^7.28.4", "@babel/plugin-transform-runtime": "^7.28.3", "@babel/preset-env": "^7.28.3", "@babel/preset-react": "^7.27.1", From df767f2cfee3739462d6f45807120d29a94a1079 Mon Sep 17 00:00:00 2001 From: James Han Date: Fri, 3 Oct 2025 14:29:24 -0400 Subject: [PATCH 33/86] Fixed foreign key violation when deleting sections with starter file groups (#7681) Fixes #7530. --- Changelog.md | 1 + app/controllers/sections_controller.rb | 1 - app/models/section.rb | 6 ++-- spec/controllers/sections_controller_spec.rb | 34 ++++++++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/Changelog.md b/Changelog.md index c42bf3268d..cdcd13a63d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -13,6 +13,7 @@ - Fixed N+1 query problem in StudentsController by eager loading user association (#7678) - Resque Host Authorization, removing env condition as this is for all environments (#7671) - Fixed ordering of assignments in the Assignment dropdown menu and Assignment index page (#7642) +- Updated Section model associations with appropriate dependent options to handle cascade deletion while preventing deletion when students exist (#7681) - Prevent grade change in the grades table for a Marks Spreadsheet, when scrolling up or down with mouse or keys (#7680) ### 🔧 Internal changes diff --git a/app/controllers/sections_controller.rb b/app/controllers/sections_controller.rb index 6429b1ccb6..29838b1d4c 100644 --- a/app/controllers/sections_controller.rb +++ b/app/controllers/sections_controller.rb @@ -59,7 +59,6 @@ def destroy if @section.has_students? flash_message(:error, t('.not_empty')) else - @section.assessment_section_properties.each(&:destroy) @section.destroy flash_message(:success, t('.success')) end diff --git a/app/models/section.rb b/app/models/section.rb index 751d246176..5885cd460b 100644 --- a/app/models/section.rb +++ b/app/models/section.rb @@ -2,9 +2,9 @@ class Section < ApplicationRecord validates :name, presence: true, allow_blank: false, format: { with: /\A[a-zA-Z0-9\-_ ]+\z/ } validates :name, uniqueness: { scope: :course_id } - has_many :students - has_many :assessment_section_properties, class_name: 'AssessmentSectionProperties' - has_many :section_starter_file_groups + has_many :students, dependent: :restrict_with_error + has_many :assessment_section_properties, class_name: 'AssessmentSectionProperties', dependent: :destroy + has_many :section_starter_file_groups, dependent: :destroy has_many :starter_file_groups, through: :section_starter_file_groups belongs_to :course, inverse_of: :sections diff --git a/spec/controllers/sections_controller_spec.rb b/spec/controllers/sections_controller_spec.rb index ffaaf679ea..7768eaad71 100644 --- a/spec/controllers/sections_controller_spec.rb +++ b/spec/controllers/sections_controller_spec.rb @@ -106,6 +106,40 @@ expect(flash[:error]).to have_message(I18n.t('sections.destroy.not_empty')) expect(Section.find(section.id)).to be_truthy end + + it 'deletes associated section_starter_file_groups when section is destroyed' do + assignment = create(:assignment, course: course) + starter_file_group = create(:starter_file_group, assignment: assignment) + section.section_starter_file_groups.create(starter_file_group: starter_file_group) + + expect do + delete_as @instructor, :destroy, params: { course_id: course.id, id: section.id } + end.to change { SectionStarterFileGroup.count }.by(-1) + + expect(flash[:success]).to have_message(I18n.t('sections.destroy.success')) + expect { Section.find(section.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'deletes associated assessment_section_properties when section is destroyed' do + assessment = create(:assignment, course: course) + section.assessment_section_properties.create(assessment: assessment) + + expect do + delete_as @instructor, :destroy, params: { course_id: course.id, id: section.id } + end.to change { AssessmentSectionProperties.count }.by(-1) + + expect(flash[:success]).to have_message(I18n.t('sections.destroy.success')) + expect { Section.find(section.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'prevents deletion via dependent: :restrict_with_error when students exist' do + student = create(:student) + section.students << student + + expect(section.destroy).to be false + expect(section.errors[:base]).to include('Cannot delete record because dependent students exist') + expect(Section.find(section.id)).to be_truthy + end end end end From 9738da229f463c6def7439b082e9273b9a6cd910 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:30:03 -0400 Subject: [PATCH 34/86] build(deps): bump rails-i18n from 8.0.1 to 8.0.2 (#7682) Bumps [rails-i18n](https://github.com/svenfuchs/rails-i18n) from 8.0.1 to 8.0.2. - [Changelog](https://github.com/svenfuchs/rails-i18n/blob/master/CHANGELOG.md) - [Commits](https://github.com/svenfuchs/rails-i18n/compare/v8.0.1...v8.0.2) --- updated-dependencies: - dependency-name: rails-i18n dependency-version: 8.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile | 2 +- Gemfile.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 264d103670..645e813479 100644 --- a/Gemfile +++ b/Gemfile @@ -37,7 +37,7 @@ gem 'histogram' # Internationalization gem 'i18n' gem 'i18n-js' -gem 'rails-i18n', '~> 8.0.1' +gem 'rails-i18n', '~> 8.0.2' # Redis gem 'redis', '~> 5.4.1' diff --git a/Gemfile.lock b/Gemfile.lock index fa143b3b61..cae50eeecf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -93,7 +93,7 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.2.2) + bigdecimal (3.2.3) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) bootsnap (1.18.6) @@ -122,7 +122,7 @@ GEM config (5.6.1) deep_merge (~> 1.2, >= 1.2.1) ostruct - connection_pool (2.5.3) + connection_pool (2.5.4) cookies_eu (1.7.8) js_cookie_rails (~> 2.2.0) crack (1.0.0) @@ -271,7 +271,7 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.9) + nokogiri (1.18.10) mini_portile2 (~> 2.8.2) racc (~> 1.4) observer (0.1.2) @@ -303,7 +303,7 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.2.0) + rack (3.2.1) rack-cors (3.0.0) logger rack (>= 3.0.14) @@ -343,7 +343,7 @@ 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.1) + rails-i18n (8.0.2) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) rails_performance (1.4.2) @@ -543,7 +543,7 @@ DEPENDENCIES rails (~> 8.0.2.1) rails-controller-testing rails-html-sanitizer - rails-i18n (~> 8.0.1) + rails-i18n (~> 8.0.2) rails_performance redcarpet redis (~> 5.4.1) From e76cbc79d81a23f593ace21e953b0bd9810cdecd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:18:24 -0400 Subject: [PATCH 35/86] build(deps): bump marcel from 1.0.4 to 1.1.0 (#7685) Bumps [marcel](https://github.com/rails/marcel) from 1.0.4 to 1.1.0. - [Release notes](https://github.com/rails/marcel/releases) - [Commits](https://github.com/rails/marcel/compare/v1.0.4...v1.1.0) --- updated-dependencies: - dependency-name: marcel dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index cae50eeecf..86ac88e1d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -251,7 +251,7 @@ GEM net-imap net-pop net-smtp - marcel (1.0.4) + marcel (1.1.0) matrix (0.4.2) mini_mime (1.1.5) mini_portile2 (2.8.9) From 61bfbd8c7fb987c51f09ab194c38482a3cc06f1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 11:21:15 -0400 Subject: [PATCH 36/86] build(deps): bump rails from 8.0.2.1 to 8.0.3 (#7683) Bumps [rails](https://github.com/rails/rails) from 8.0.2.1 to 8.0.3. - [Release notes](https://github.com/rails/rails/releases) - [Commits](https://github.com/rails/rails/compare/v8.0.2.1...v8.0.3) --- updated-dependencies: - dependency-name: rails dependency-version: 8.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile | 2 +- Gemfile.lock | 114 ++++++++++++++++++++++++++------------------------- 2 files changed, 59 insertions(+), 57 deletions(-) diff --git a/Gemfile b/Gemfile index 645e813479..86ee65d56e 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.2.1' +gem 'rails', '~> 8.0.3' gem 'sprockets' gem 'sprockets-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 86ac88e1d2..609a085a86 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,29 +3,29 @@ GEM specs: action_policy (0.7.5) ruby-next-core (>= 1.0) - actioncable (8.0.2.1) - actionpack (= 8.0.2.1) - activesupport (= 8.0.2.1) + actioncable (8.0.3) + actionpack (= 8.0.3) + activesupport (= 8.0.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.2.1) - actionpack (= 8.0.2.1) - activejob (= 8.0.2.1) - activerecord (= 8.0.2.1) - activestorage (= 8.0.2.1) - activesupport (= 8.0.2.1) + actionmailbox (8.0.3) + actionpack (= 8.0.3) + activejob (= 8.0.3) + activerecord (= 8.0.3) + activestorage (= 8.0.3) + activesupport (= 8.0.3) mail (>= 2.8.0) - actionmailer (8.0.2.1) - actionpack (= 8.0.2.1) - actionview (= 8.0.2.1) - activejob (= 8.0.2.1) - activesupport (= 8.0.2.1) + actionmailer (8.0.3) + actionpack (= 8.0.3) + actionview (= 8.0.3) + activejob (= 8.0.3) + activesupport (= 8.0.3) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.2.1) - actionview (= 8.0.2.1) - activesupport (= 8.0.2.1) + actionpack (8.0.3) + actionview (= 8.0.3) + activesupport (= 8.0.3) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -33,42 +33,42 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.2.1) - actionpack (= 8.0.2.1) - activerecord (= 8.0.2.1) - activestorage (= 8.0.2.1) - activesupport (= 8.0.2.1) + actiontext (8.0.3) + actionpack (= 8.0.3) + activerecord (= 8.0.3) + activestorage (= 8.0.3) + activesupport (= 8.0.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.2.1) - activesupport (= 8.0.2.1) + actionview (8.0.3) + activesupport (= 8.0.3) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.0.2.1) - activesupport (= 8.0.2.1) + activejob (8.0.3) + activesupport (= 8.0.3) globalid (>= 0.3.6) activejob-status (1.0.1) activejob (>= 6.0) activesupport (>= 6.0) - activemodel (8.0.2.1) - activesupport (= 8.0.2.1) + activemodel (8.0.3) + activesupport (= 8.0.3) activemodel-serializers-xml (1.0.3) activemodel (>= 5.0.0.a) activesupport (>= 5.0.0.a) builder (~> 3.1) - activerecord (8.0.2.1) - activemodel (= 8.0.2.1) - activesupport (= 8.0.2.1) + activerecord (8.0.3) + activemodel (= 8.0.3) + activesupport (= 8.0.3) timeout (>= 0.4.0) - activestorage (8.0.2.1) - actionpack (= 8.0.2.1) - activejob (= 8.0.2.1) - activerecord (= 8.0.2.1) - activesupport (= 8.0.2.1) + activestorage (8.0.3) + actionpack (= 8.0.3) + activejob (= 8.0.3) + activerecord (= 8.0.3) + activesupport (= 8.0.3) marcel (~> 1.0) - activesupport (8.0.2.1) + activesupport (8.0.3) base64 benchmark (>= 0.3) bigdecimal @@ -200,7 +200,7 @@ GEM rspec-core (~> 3.0) ruby-progressbar (~> 1.4) glob (0.4.0) - globalid (1.2.1) + globalid (1.3.0) activesupport (>= 6.1) hashdiff (1.2.0) highline (3.1.2) @@ -261,7 +261,7 @@ GEM multi_json (1.15.0) mustermann (3.0.3) ruby2_keywords (~> 0.0.1) - net-imap (0.5.10) + net-imap (0.5.11) date net-protocol net-pop (0.1.2) @@ -318,20 +318,20 @@ GEM rack (>= 1.3) rackup (2.2.1) rack (>= 3) - rails (8.0.2.1) - actioncable (= 8.0.2.1) - actionmailbox (= 8.0.2.1) - actionmailer (= 8.0.2.1) - actionpack (= 8.0.2.1) - actiontext (= 8.0.2.1) - actionview (= 8.0.2.1) - activejob (= 8.0.2.1) - activemodel (= 8.0.2.1) - activerecord (= 8.0.2.1) - activestorage (= 8.0.2.1) - activesupport (= 8.0.2.1) + 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) bundler (>= 1.15.0) - railties (= 8.0.2.1) + railties (= 8.0.3) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -350,13 +350,14 @@ GEM browser railties redis - railties (8.0.2.1) - actionpack (= 8.0.2.1) - activesupport (= 8.0.2.1) + railties (8.0.3) + actionpack (= 8.0.3) + activesupport (= 8.0.3) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) raindrops (0.20.0) @@ -469,6 +470,7 @@ GEM tilt (2.5.0) timecop (0.9.10) timeout (0.4.3) + tsort (0.2.0) ttfunk (1.8.0) bigdecimal (~> 3.1) tzinfo (2.0.6) @@ -540,7 +542,7 @@ DEPENDENCIES prawn-qrcode puma rack-cors - rails (~> 8.0.2.1) + rails (~> 8.0.3) rails-controller-testing rails-html-sanitizer rails-i18n (~> 8.0.2) From 4abd638dae642fe480678ad52208e7f1f73055e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 11:35:47 -0400 Subject: [PATCH 37/86] build(deps): bump rubyzip from 2.4.1 to 3.0.2 (#7648) * build(deps): bump rubyzip from 2.4.1 to 3.0.2 Bumps [rubyzip](https://github.com/rubyzip/rubyzip) from 2.4.1 to 3.0.2. - [Release notes](https://github.com/rubyzip/rubyzip/releases) - [Changelog](https://github.com/rubyzip/rubyzip/blob/main/Changelog.md) - [Commits](https://github.com/rubyzip/rubyzip/compare/v2.4.1...v3.0.2) --- updated-dependencies: - dependency-name: rubyzip dependency-version: 3.0.2 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Fixed failing test --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: david-yz-liu --- Gemfile.lock | 2 +- spec/models/starter_file_entry_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 609a085a86..c69e64361f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -422,7 +422,7 @@ GEM ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) ruby2_keywords (0.0.5) - rubyzip (2.4.1) + rubyzip (3.0.2) rufus-scheduler (3.9.2) fugit (~> 1.1, >= 1.11.1) rugged (1.9.0) diff --git a/spec/models/starter_file_entry_spec.rb b/spec/models/starter_file_entry_spec.rb index 73fd91c4a5..429dba0d35 100644 --- a/spec/models/starter_file_entry_spec.rb +++ b/spec/models/starter_file_entry_spec.rb @@ -49,7 +49,7 @@ it 'should add files to an open zip file' do FileUtils.rm_f(zip_path) - Zip::File.open(zip_path, Zip::File::CREATE) do |zip_file| + Zip::File.open(zip_path, create: true) do |zip_file| starter_file_entry.add_files_to_zip_file(zip_file) end Zip::File.open(zip_path) do |zip_file| From b9ce0968c2392d56879d9e475ec00f5897092380 Mon Sep 17 00:00:00 2001 From: David Liu Date: Sun, 5 Oct 2025 16:50:23 -0400 Subject: [PATCH 38/86] Updated pre-commit rubocop-rails to v2.33.4 (#7691) --- .pre-commit-config.yaml | 2 +- .rubocop.yml | 5 ++++- Changelog.md | 1 + app/controllers/api/groups_controller.rb | 4 +++- app/controllers/api/main_api_controller.rb | 4 ++-- app/controllers/main_controller.rb | 2 +- app/models/grade_entry_form.rb | 2 +- app/models/grouping.rb | 2 +- app/models/marking_scheme.rb | 2 +- app/models/submission.rb | 2 +- spec/models/grouping_spec.rb | 24 +++++++++++----------- 11 files changed, 28 insertions(+), 22 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5b27bd8f9..242612c676 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -52,7 +52,7 @@ repos: Vagrantfile )$ additional_dependencies: - - rubocop-rails:2.27.0 + - rubocop-rails:2.33.4 - rubocop-performance:1.23.0 - rubocop-factory_bot:2.26.1 - rubocop-rspec:3.2.0 diff --git a/.rubocop.yml b/.rubocop.yml index 61bea3be0b..68812dd020 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,10 +1,10 @@ require: - - rubocop-rails - rubocop-performance - rubocop-factory_bot - rubocop-rspec_rails - rubocop-capybara plugins: + - rubocop-rails - rubocop-rspec AllCops: @@ -187,6 +187,9 @@ Rails/RequireDependency: Rails/SkipsModelValidations: Enabled: false +Rails/StrongParametersExpect: # TODO: Enable this one and fix all issues + Enabled: false + Rails/UniqueValidationWithoutIndex: Enabled: false diff --git a/Changelog.md b/Changelog.md index cdcd13a63d..41e425dd37 100644 --- a/Changelog.md +++ b/Changelog.md @@ -24,6 +24,7 @@ - Refactored Criterion subclasses to remove redundant code - Converted "Rename Group" functionality to React modal (#7673) - Fixed Rack deprecation warnings by updating HTTP status code symbols (#7675) +- Updated pre-commit `rubocop-rails` version to 2.33.4 (#7691) ## [v2.8.1] diff --git a/app/controllers/api/groups_controller.rb b/app/controllers/api/groups_controller.rb index bd4dbdc42b..01c840405a 100644 --- a/app/controllers/api/groups_controller.rb +++ b/app/controllers/api/groups_controller.rb @@ -404,7 +404,9 @@ def collect_submission private def assignment - @assignment ||= Assignment.find_by(id: params[:assignment_id]) + return @assignment if defined?(@assignment) + + @assignment = Assignment.find_by(id: params[:assignment_id]) end def grouping diff --git a/app/controllers/api/main_api_controller.rb b/app/controllers/api/main_api_controller.rb index 352f455d6a..076628d17f 100644 --- a/app/controllers/api/main_api_controller.rb +++ b/app/controllers/api/main_api_controller.rb @@ -73,9 +73,9 @@ def get_collection(collection) 'Invalid or malformed parameter values' }, status: :unprocessable_content false elsif filter_params.empty? - collection.order('id') + collection.order(:id) else - collection.order('id').where(**filter_params) + collection.order(:id).where(**filter_params) end end diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index cd2ce16f7d..2a5f879294 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -118,7 +118,7 @@ def about # Render 404 error (page not found) if no other route matches. # See config/routes.rb - def page_not_found # rubocop:disable Lint/UselessMethodDefinition + def page_not_found super end diff --git a/app/models/grade_entry_form.rb b/app/models/grade_entry_form.rb index 6c54c82187..be1a230287 100644 --- a/app/models/grade_entry_form.rb +++ b/app/models/grade_entry_form.rb @@ -23,7 +23,7 @@ class GradeEntryForm < Assessment before_destroy -> { throw(:abort) if self.grades.where.not(grade: nil).exists? }, prepend: true # Set the default order of spreadsheets: in ascending order of id - default_scope { order('id ASC') } + default_scope { order(:id) } # Constants BLANK_MARK = ''.freeze diff --git a/app/models/grouping.rb b/app/models/grouping.rb index bb8ab44807..106665adef 100644 --- a/app/models/grouping.rb +++ b/app/models/grouping.rb @@ -11,7 +11,7 @@ class Grouping < ApplicationRecord after_commit :update_repo_permissions_after_save, on: [:create, :update] has_many :memberships, dependent: :destroy - has_many :student_memberships, -> { order('id') }, inverse_of: :grouping + has_many :student_memberships, -> { order(:id) }, inverse_of: :grouping has_many :non_rejected_student_memberships, -> { where.not(memberships: { membership_status: StudentMembership::STATUSES[:rejected] }) }, class_name: 'StudentMembership', diff --git a/app/models/marking_scheme.rb b/app/models/marking_scheme.rb index 9fb684ec38..03e4864ab1 100644 --- a/app/models/marking_scheme.rb +++ b/app/models/marking_scheme.rb @@ -7,7 +7,7 @@ class MarkingScheme < ApplicationRecord belongs_to :course - default_scope { order('id ASC') } + default_scope { order(:id) } # Returns an array of all students' weighted grades that are not nil def students_weighted_grades_array(current_role) diff --git a/app/models/submission.rb b/app/models/submission.rb index 475c74b113..4a6ffcbc04 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -32,7 +32,7 @@ class Submission < ApplicationRecord has_many :submission_files, dependent: :destroy has_many :annotations, through: :submission_files - has_many :test_runs, -> { order 'created_at DESC' }, dependent: :nullify, inverse_of: :submission + has_many :test_runs, -> { order(created_at: :desc) }, dependent: :nullify, inverse_of: :submission has_many :test_group_results, through: :test_runs has_many :feedback_files, dependent: :destroy diff --git a/spec/models/grouping_spec.rb b/spec/models/grouping_spec.rb index 5619d50d39..64c41bac91 100644 --- a/spec/models/grouping_spec.rb +++ b/spec/models/grouping_spec.rb @@ -1641,25 +1641,25 @@ def expect_updated_criteria_coverage_count_eq(expected_count) end it 'should let one navigate right if there is a result directly to the right' do - groupings = assignment.groupings.joins(:group).order('group_name') + groupings = assignment.groupings.joins(:group).order(:group_name) new_grouping = groupings.first.get_next_grouping(role, false) expect(new_grouping.group_id).to eq(groupings.last.group_id) end it 'should let one navigate left if there is a result directly to the left' do - groupings = assignment.groupings.joins(:group).order('group_name') + groupings = assignment.groupings.joins(:group).order(:group_name) new_grouping = groupings.last.get_next_grouping(role, true) expect(new_grouping.group_id).to eq(groupings.first.group_id) end it 'should not one navigate right if there is no result directly to the right' do - groupings = assignment.groupings.joins(:group).order('group_name') + groupings = assignment.groupings.joins(:group).order(:group_name) new_grouping = groupings.last.get_next_grouping(role, false) expect(new_grouping).to be_nil end it 'should not let one navigate left if there is no result directly to the left' do - groupings = assignment.groupings.joins(:group).order('group_name') + groupings = assignment.groupings.joins(:group).order(:group_name) new_grouping = groupings.first.get_next_grouping(role, true) expect(new_grouping).to be_nil end @@ -1668,13 +1668,13 @@ def expect_updated_criteria_coverage_count_eq(expected_count) let(:groupings_collected) { [false, true] } it 'should let me navigate to the right if any result exists towards the right' do - groupings = assignment.groupings.joins(:group).order('group_name') + groupings = assignment.groupings.joins(:group).order(:group_name) new_grouping = groupings.first.get_next_grouping(role, false) expect(new_grouping.group_id).to eq(groupings.last.group_id) end it 'should let one navigate left if there is a result directly to the left' do - groupings = assignment.groupings.joins(:group).order('group_name') + groupings = assignment.groupings.joins(:group).order(:group_name) new_grouping = groupings.last.get_next_grouping(role, true) expect(new_grouping.group_id).to eq(groupings.first.group_id) end @@ -1695,25 +1695,25 @@ def expect_updated_criteria_coverage_count_eq(expected_count) end it 'should let one navigate right if there is a result directly to the right' do - groupings = assignment.groupings.joins(:group).order('group_name') + groupings = assignment.groupings.joins(:group).order(:group_name) new_grouping = groupings.first.get_next_grouping(role, false) expect(new_grouping.group_id).to be(groupings.last.group_id) end it 'should let one navigate left if there is a result directly to the left' do - groupings = assignment.groupings.joins(:group).order('group_name') + groupings = assignment.groupings.joins(:group).order(:group_name) new_grouping = groupings.last.get_next_grouping(role, true) expect(new_grouping.group_id).to be(groupings.first.group_id) end it 'should not one navigate right if there is no result directly to the right' do - groupings = assignment.groupings.joins(:group).order('group_name') + groupings = assignment.groupings.joins(:group).order(:group_name) new_grouping = groupings.last.get_next_grouping(role, false) expect(new_grouping).to be_nil end it 'should not let one navigate left if there is no result directly to the left' do - groupings = assignment.groupings.joins(:group).order('group_name') + groupings = assignment.groupings.joins(:group).order(:group_name) new_grouping = groupings.first.get_next_grouping(role, true) expect(new_grouping).to be_nil end @@ -1727,13 +1727,13 @@ def expect_updated_criteria_coverage_count_eq(expected_count) end it 'should let me navigate to the right if any result exists towards the right' do - groupings = assignment.groupings.joins(:group).order('group_name') + groupings = assignment.groupings.joins(:group).order(:group_name) new_grouping = groupings.first.get_next_grouping(role, false) expect(new_grouping.group_id).to eq(groupings.last.group_id) end it 'should let one navigate left if there is a result directly to the left' do - groupings = assignment.groupings.joins(:group).order('group_name') + groupings = assignment.groupings.joins(:group).order(:group_name) new_grouping = groupings.last.get_next_grouping(role, true) expect(new_grouping.group_id).to eq(groupings.first.group_id) end From 68750f549fa958501fb7f4cff0978e64c2267b99 Mon Sep 17 00:00:00 2001 From: Freya Zhang <149322974+freyazjiner@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:01:59 -0400 Subject: [PATCH 39/86] Refactored assignment group reuse functionality to React modal (#7688) --- Changelog.md | 1 + app/assets/javascripts/Groups/index.js | 5 +- app/controllers/groups_controller.rb | 4 +- .../Modals/assignment_group_use_modal.jsx | 90 +++++++++++++ .../assignment_group_use_modal.test.jsx | 121 ++++++++++++++++++ app/javascript/Components/groups_manager.jsx | 45 +++++++ app/javascript/common/fontawesome_config.js | 2 + .../_assignment_group_use_modal.html.erb | 16 --- app/views/groups/index.html.erb | 33 ++--- config/locales/views/groups/en.yml | 1 - 10 files changed, 272 insertions(+), 46 deletions(-) create mode 100644 app/javascript/Components/Modals/assignment_group_use_modal.jsx create mode 100644 app/javascript/Components/__tests__/assignment_group_use_modal.test.jsx delete mode 100644 app/views/groups/_assignment_group_use_modal.html.erb diff --git a/Changelog.md b/Changelog.md index 41e425dd37..94a4f1ffef 100644 --- a/Changelog.md +++ b/Changelog.md @@ -24,6 +24,7 @@ - Refactored Criterion subclasses to remove redundant code - Converted "Rename Group" functionality to React modal (#7673) - Fixed Rack deprecation warnings by updating HTTP status code symbols (#7675) +- Refactored "Reuse Groups" functionality to use React modal and relocated button to action box row (#7688) - Updated pre-commit `rubocop-rails` version to 2.33.4 (#7691) ## [v2.8.1] diff --git a/app/assets/javascripts/Groups/index.js b/app/assets/javascripts/Groups/index.js index af855cd707..7c9e34a462 100644 --- a/app/assets/javascripts/Groups/index.js +++ b/app/assets/javascripts/Groups/index.js @@ -1,11 +1,8 @@ -var modalCreate, - modalNotesGroup, - modalAssignmentGroupReUse = null; +var modalCreate, modalNotesGroup; (function () { const domContentLoadedCB = function () { modalNotesGroup = new ModalMarkus("#notes_dialog"); - modalAssignmentGroupReUse = new ModalMarkus("#assignment_group_use_dialog"); }; if (document.readyState === "loading") { diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 965ac9c06e..c7a3c3d930 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -117,7 +117,9 @@ def index respond_to do |format| format.html format.json do - render json: @assignment.all_grouping_data + render json: @assignment.all_grouping_data.merge( + clone_assignments: @clone_assignments.as_json(only: [:id, :short_identifier]) + ) end end end diff --git a/app/javascript/Components/Modals/assignment_group_use_modal.jsx b/app/javascript/Components/Modals/assignment_group_use_modal.jsx new file mode 100644 index 0000000000..4854204d21 --- /dev/null +++ b/app/javascript/Components/Modals/assignment_group_use_modal.jsx @@ -0,0 +1,90 @@ +import React from "react"; +import Modal from "react-modal"; + +export default class AssignmentGroupUseModal extends React.Component { + constructor(props) { + super(props); + this.state = { + assignmentId: "", + isLoading: false, + }; + } + + componentDidMount() { + Modal.setAppElement("body"); + } + + componentDidUpdate(prevProps) { + if (this.props.isOpen && !prevProps.isOpen) { + if (this.props.cloneAssignments.length > 0) { + this.setState({ + assignmentId: this.props.cloneAssignments[0].id, + }); + } else { + this.setState({ + assignmentId: "", + }); + } + } + } + + handleChange = event => { + this.setState({assignmentId: event.target.value}); + }; + + handleSubmit = event => { + event.preventDefault(); + if (window.confirm(I18n.t("groups.delete_groups_linked"))) { + this.setState({isLoading: true}); + this.props.onSubmit(this.state.assignmentId); + } + }; + + render() { + return ( + +

    {I18n.t("groups.reuse_groups")}

    +
    + {I18n.t("groups.assignment_to_use")} + +
    + + +
    + +
    + ); + } +} diff --git a/app/javascript/Components/__tests__/assignment_group_use_modal.test.jsx b/app/javascript/Components/__tests__/assignment_group_use_modal.test.jsx new file mode 100644 index 0000000000..e0183b0aef --- /dev/null +++ b/app/javascript/Components/__tests__/assignment_group_use_modal.test.jsx @@ -0,0 +1,121 @@ +import React from "react"; +import {render, screen, fireEvent, waitFor, cleanup} from "@testing-library/react"; +import AssignmentGroupUseModal from "../Modals/assignment_group_use_modal"; +import Modal from "react-modal"; + +describe("AssignmentGroupUseModal", () => { + let props; + let mockCloneAssignments; + + beforeEach(() => { + mockCloneAssignments = [ + {id: 1, short_identifier: "Assignment 1"}, + {id: 2, short_identifier: "Assignment 2"}, + {id: 3, short_identifier: "Assignment 3"}, + ]; + props = { + isOpen: true, + onRequestClose: jest.fn(), + onSubmit: jest.fn(), + cloneAssignments: mockCloneAssignments, + }; + + Modal.setAppElement("body"); + global.confirm = jest.fn(() => true); + }); + + afterEach(() => { + cleanup(); + }); + + it("should display all clonable assignments in the dropdown", () => { + render(); + + mockCloneAssignments.forEach(assignment => { + expect(screen.getByText(assignment.short_identifier)).toBeInTheDocument(); + }); + }); + + it("should set assignmentId to first assignment when modal opens with assignments", async () => { + const closedProps = {...props, isOpen: false}; + const {rerender} = render(); + + rerender(); + + await waitFor(() => { + const select = document.getElementById("assignment-group-select"); + expect(select.value).toBe("1"); + }); + }); + + it("should call onSubmit with selected assignment ID on successful submit", async () => { + render(); + + const select = document.getElementById("assignment-group-select"); + fireEvent.change(select, {target: {value: "2"}}); + + fireEvent.click(screen.getByText(I18n.t("save"))); + + await waitFor(() => { + expect(global.confirm).toHaveBeenCalledWith(I18n.t("groups.delete_groups_linked")); + expect(props.onSubmit).toHaveBeenCalledWith("2"); + }); + + expect(screen.getByText(I18n.t("working"))).toBeInTheDocument(); + expect(screen.getByText(I18n.t("working"))).toBeDisabled(); + }); + + it("should call onRequestClose when cancel button is clicked", () => { + render(); + + fireEvent.click(screen.getByText(I18n.t("cancel"))); + expect(props.onRequestClose).toHaveBeenCalledTimes(1); + }); + + it("calls confirm and does not submit when user cancels", () => { + global.confirm = jest.fn(() => false); + render(); + + const select = document.getElementById("assignment-group-select"); + fireEvent.change(select, {target: {value: "3"}}); + + fireEvent.click(screen.getByText(I18n.t("save"))); + + expect(global.confirm).toHaveBeenCalledWith(I18n.t("groups.delete_groups_linked")); + expect(props.onSubmit).not.toHaveBeenCalled(); + }); + + it("calls confirm and submits when user confirms", async () => { + global.confirm = jest.fn(() => true); + render(); + + const select = document.getElementById("assignment-group-select"); + fireEvent.change(select, {target: {value: "3"}}); + + fireEvent.click(screen.getByText(I18n.t("save"))); + + await waitFor(() => { + expect(global.confirm).toHaveBeenCalledWith(I18n.t("groups.delete_groups_linked")); + expect(props.onSubmit).toHaveBeenCalledWith("3"); + }); + }); + + it("should disable save button when no assignments are available", () => { + const emptyProps = {...props, cloneAssignments: []}; + render(); + + expect(screen.getByText(I18n.t("save"))).toBeDisabled(); + }); + + it("should set assignmentId to empty string when modal opens with no assignments", async () => { + const emptyProps = {...props, isOpen: false, cloneAssignments: []}; + const {rerender} = render(); + + rerender(); + + await waitFor(() => { + const select = document.getElementById("assignment-group-select"); + expect(select.length).toBe(0); + }); + }); +}); diff --git a/app/javascript/Components/groups_manager.jsx b/app/javascript/Components/groups_manager.jsx index 1694b2322e..bbede40b3a 100644 --- a/app/javascript/Components/groups_manager.jsx +++ b/app/javascript/Components/groups_manager.jsx @@ -8,6 +8,7 @@ import {durationSort, selectFilter} from "./Helpers/table_helpers"; import AutoMatchModal from "./Modals/auto_match_modal"; import CreateGroupModal from "./Modals/create_group_modal"; import RenameGroupModal from "./Modals/rename_group_modal"; +import AssignmentGroupUseModal from "./Modals/assignment_group_use_modal"; class GroupsManager extends React.Component { constructor(props) { @@ -24,10 +25,12 @@ class GroupsManager extends React.Component { selected_extension_data: {}, updating_extension: false, isAutoMatchModalOpen: false, + isAssignmentGroupUseModalOpen: false, isCreateGroupModalOpen: false, isRenameGroupDialogOpen: false, examTemplates: [], loading: true, + cloneAssignments: [], }; } @@ -70,6 +73,7 @@ class GroupsManager extends React.Component { hidden_students_count: res.students.filter(student => student.hidden).length, inactive_groups_count: inactive_groups_count, examTemplates: res.exam_templates, + cloneAssignments: res.clone_assignments || [], }); }); }; @@ -209,6 +213,33 @@ class GroupsManager extends React.Component { }); }; + handleShowAssignmentGroupUseModal = () => { + this.setState({ + isAssignmentGroupUseModalOpen: true, + }); + }; + + handleCloseAssignmentGroupUseModal = () => { + this.setState({ + isAssignmentGroupUseModalOpen: false, + }); + }; + + handleSubmitAssignmentGroupUseModal = selectedAssignmentId => { + $.post({ + url: Routes.use_another_assignment_groups_course_assignment_groups_path( + this.props.course_id, + this.props.assignment_id + ), + data: { + clone_assignment_id: selectedAssignmentId, + }, + }).then(() => { + this.setState({isAssignmentGroupUseModalOpen: false}); + this.fetchData(); + }); + }; + handleShowAutoMatchModal = () => { if (this.groupsTable.state.selection.length === 0) { alert(I18n.t("groups.select_a_group")); @@ -305,11 +336,13 @@ class GroupsManager extends React.Component { createGroup={this.createGroup} deleteGroups={this.deleteGroups} handleShowAutoMatchModal={this.handleShowAutoMatchModal} + handleShowAssignmentGroupUseModal={this.handleShowAssignmentGroupUseModal} hiddenStudentsCount={this.state.loading ? null : this.state.hidden_students_count} hiddenGroupsCount={this.state.loading ? null : this.state.inactive_groups_count} scanned_exam={this.props.scanned_exam} showHidden={this.state.show_hidden} updateShowHidden={this.updateShowHidden} + vcs_submit={this.props.vcs_submit} />
    @@ -375,6 +408,12 @@ class GroupsManager extends React.Component { onSubmit={this.handleRenameGroupDialog} initialGroupName={this.state.renameGroupName} /> +
    ); } @@ -743,6 +782,12 @@ class GroupsActionBox extends React.Component { {I18n.t("students.display_inactive")} + {this.props.vcs_submit && ( + + )}
    this.handleChange(level)} - key={`${this.props.id}-${levelMark}`} - className={`rubric-level ${selectedClass} ${oldMarkClass}`} - > - - - - ); - }; - - render() { - const levels = this.props.levels.map(this.renderRubricLevel); - const expandedClass = this.props.expanded ? "expanded" : "collapsed"; - const unassignedClass = this.props.unassigned ? "unassigned" : ""; - return ( -
  • -
    -
    -
    - {this.props.name} - {this.props.bonus && ` (${I18n.t("activerecord.attributes.criterion.bonus")})`} - {!this.props.released_to_students && - !this.props.unassigned && - this.props.mark !== null && ( - this.props.destroyMark(e, this.props.id)} - style={{float: "right"}} - > - {I18n.t("helpers.submit.delete", { - model: I18n.t("activerecord.models.mark.one"), - })} - - )} -
    -
  • - {level.name} - - - {levelMark} -  /  - {this.props.max_mark} -
    - {levels} -
    -
    - - ); - } -} - -RubricCriterionInput.propTypes = { - bonus: PropTypes.bool, - destroyMark: PropTypes.func.isRequired, - expanded: PropTypes.bool.isRequired, - id: PropTypes.number.isRequired, - levels: PropTypes.array, - mark: PropTypes.number, - max_mark: PropTypes.number, - oldMark: PropTypes.object, - released_to_students: PropTypes.bool.isRequired, - toggleExpanded: PropTypes.func.isRequired, - unassigned: PropTypes.bool.isRequired, - updateMark: PropTypes.func.isRequired, -}; diff --git a/app/javascript/Components/Result/rubric_criterion_input.jsx b/app/javascript/Components/Result/rubric_criterion_input.jsx new file mode 100644 index 0000000000..5c8c07f67a --- /dev/null +++ b/app/javascript/Components/Result/rubric_criterion_input.jsx @@ -0,0 +1,104 @@ +import React from "react"; +import PropTypes from "prop-types"; + +import safe_marked from "../../common/safe_marked"; + +export default function RubricCriterionInput({ + bonus, + destroyMark, + expanded, + id, + levels, + mark, + max_mark, + name, + oldMark, + released_to_students, + toggleExpanded, + unassigned, + updateMark, +}) { + // The parameter `level` is the level object selected + const handleChange = level => { + updateMark(id, level.mark); + }; + + // The parameter `level` is the level object selected + const renderRubricLevel = level => { + const levelMark = level.mark.toFixed(2); + let selectedClass = ""; + let oldMarkClass = ""; + if (mark !== undefined && mark !== null && levelMark === mark.toFixed(2)) { + selectedClass = "selected"; + } + if ( + oldMark !== undefined && + oldMark.mark !== undefined && + levelMark === oldMark.mark.toFixed(2) + ) { + oldMarkClass = "old-mark"; + } + + return ( + handleChange(level)} + key={`${id}-${levelMark}`} + className={`rubric-level ${selectedClass} ${oldMarkClass}`} + > + + {level.name} + + + + {levelMark} +  /  + {max_mark} + + + ); + }; + + const rubricLevels = levels.map(renderRubricLevel); + const expandedClass = expanded ? "expanded" : "collapsed"; + const unassignedClass = unassigned ? "unassigned" : ""; + + return ( +
  • +
    +
    +
    + {name} + {bonus && ` (${I18n.t("activerecord.attributes.criterion.bonus")})`} + {!released_to_students && !unassigned && mark !== null && ( + destroyMark(e, id)} style={{float: "right"}}> + {I18n.t("helpers.submit.delete", { + model: I18n.t("activerecord.models.mark.one"), + })} + + )} +
    + + {rubricLevels} +
    +
    +
  • + ); +} + +RubricCriterionInput.propTypes = { + bonus: PropTypes.bool, + destroyMark: PropTypes.func.isRequired, + expanded: PropTypes.bool.isRequired, + id: PropTypes.number.isRequired, + levels: PropTypes.array, + mark: PropTypes.number, + max_mark: PropTypes.number, + oldMark: PropTypes.object, + released_to_students: PropTypes.bool.isRequired, + toggleExpanded: PropTypes.func.isRequired, + unassigned: PropTypes.bool.isRequired, + updateMark: PropTypes.func.isRequired, +}; diff --git a/app/javascript/Components/__tests__/marks_panel.test.jsx b/app/javascript/Components/__tests__/marks_panel.test.jsx index 631c9d89db..f902f5e40d 100644 --- a/app/javascript/Components/__tests__/marks_panel.test.jsx +++ b/app/javascript/Components/__tests__/marks_panel.test.jsx @@ -1,12 +1,11 @@ -import {render, screen} from "@testing-library/react"; +import {render, screen, waitFor} from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { - MarksPanel, - CheckboxCriterionInput, - FlexibleCriterionInput, - RubricCriterionInput, -} from "../Result/marks_panel"; +import {MarksPanel} from "../Result/marks_panel"; + +import CheckboxCriterionInput from "../Result/checkbox_criterion_input"; +import FlexibleCriterionInput from "../Result/flexible_criterion_input"; +import RubricCriterionInput from "../Result/rubric_criterion_input"; const convertToKebabCase = { CheckboxCriterion: "checkbox_criterion", @@ -153,6 +152,25 @@ describe("CheckboxCriterionInput", () => { expect(screen.queryByText(`(${I18n.t("results.remark.old_mark")}: 1)`)).toBeTruthy(); }); + + it("renders CheckboxCriterionInput with radio buttons", () => { + render(); + + // Check at least 1 criterion label renders + expect(screen.getAllByText(/criterion/i).length).toBeGreaterThan(0); + + // Check at least 1 radio button pair render (yes/no) + const radios = screen.getAllByRole("radio"); + expect(radios.length).toBeGreaterThanOrEqual(2); + }); + + it("shows Delete Mark link when mark exists and not released", () => { + render(); + + // Check at least one delete link renders + const deleteLinks = screen.getAllByText(/delete mark/i); + expect(deleteLinks.length).toBeGreaterThan(0); + }); }); describe("FlexibleCriterionInput", () => { @@ -303,7 +321,9 @@ describe("FlexibleCriterionInput", () => { await userEvent.clear(input); await userEvent.type(input, "Hi Prof Liu"); expect(input.value).toEqual("Hi Prof Liu"); - expect(input.classList.contains("invalid")).toBeTruthy(); + await waitFor(() => { + expect(input.classList.contains("invalid")).toBeTruthy(); + }); }); it("should set the mark as valid if it has a decimal", async () => { @@ -355,6 +375,29 @@ describe("FlexibleCriterionInput", () => { render(); expect(screen.queryAllByRole("textbox")).toEqual([]); }); + + it("renders FlexibleCriterionInput with input field", () => { + render(); + + // Check input box for marks renders + const input = screen.getByRole("textbox"); + expect(input).toBeInTheDocument(); + }); + + it("updates input value when mark prop changes", () => { + const {rerender} = render(); + + const input = screen.getByRole("textbox"); + expect(input.value).toBe("2"); + + // Re-render with new mark + rerender(); + expect(input.value).toBe("5"); + + // Re-render with null mark (should clear it) + rerender(); + expect(input.value).toBe(""); + }); }); describe("RubricCriterionInput", () => { @@ -470,4 +513,19 @@ describe("RubricCriterionInput", () => { expect(screen.queryAllByRole("link")).toEqual([]); }); + + it("renders RubricCriterionInput with rubric levels", () => { + render(); + + // Check criterion label renders + expect(screen.getByText(/criterion/i)).toBeInTheDocument(); + + // Check rubric levels render + expect(screen.getByText(/level 1/i)).toBeInTheDocument(); + expect(screen.getByText(/level 2/i)).toBeInTheDocument(); + + // Check rubric description renders + const descriptions = screen.getAllByText(/description/i); + expect(descriptions).toHaveLength(2); + }); }); From 2ca6a7b39b7ba80c1fb32d81b18f6aee664467e5 Mon Sep 17 00:00:00 2001 From: James Han Date: Fri, 24 Oct 2025 18:12:04 -0400 Subject: [PATCH 54/86] Added scheduled visibility fields for assessments (#7697) Add visible_on/visible_until columns to schedule assessment visibility automatically. Datetime columns override is_hidden when set. Section-specific datetime overrides global datetime. Updated models, policies, SQL function, API controllers, and added comprehensive tests. --- Changelog.md | 1 + app/controllers/api/assignments_controller.rb | 2 +- .../api/grade_entry_forms_controller.rb | 3 +- app/lib/repository.rb | 37 +++- app/models/assessment.rb | 10 ++ app/models/assessment_section_properties.rb | 11 ++ app/models/assignment.rb | 6 +- app/models/student.rb | 78 +++++++-- ...000_add_visibility_dates_to_assessments.rb | 8 + ...epo_permissions_for_datetime_visibility.rb | 159 ++++++++++++++++++ db/structure.sql | 150 ++++++++++------- spec/db/check_repo_permissions_spec.rb | 79 +++++++++ spec/models/assessment_spec.rb | 37 ++++ spec/models/course_spec.rb | 11 +- spec/models/student_spec.rb | 115 +++++++++++++ spec/policies/assignment_policy_spec.rb | 52 ++++++ 16 files changed, 678 insertions(+), 81 deletions(-) create mode 100644 db/migrate/20251010150000_add_visibility_dates_to_assessments.rb create mode 100644 db/migrate/20251010150001_update_check_repo_permissions_for_datetime_visibility.rb diff --git a/Changelog.md b/Changelog.md index a211c71fee..5f1824819d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,7 @@ ### 🚨 Breaking changes ### ✨ New features and improvements +- Added datetime-based visibility scheduling for assessments with `visible_on` and `visible_until` columns (#7697) - Added new loading spinner icon for tables (#7602) - Added functionality to apply bonuses and penalties as a percentage of the student's earned marks to ExtraMark model (#7702) diff --git a/app/controllers/api/assignments_controller.rb b/app/controllers/api/assignments_controller.rb index 4ee921fa11..b464e1fb0a 100644 --- a/app/controllers/api/assignments_controller.rb +++ b/app/controllers/api/assignments_controller.rb @@ -11,7 +11,7 @@ class AssignmentsController < MainApiController :student_form_groups, :remark_due_date, :remark_message, :assign_graders_to_criteria, :enable_test, :enable_student_tests, :allow_remarks, :display_grader_names_to_students, :group_name_autogenerated, - :repository_folder, :is_hidden, :vcs_submit, :token_period, + :repository_folder, :is_hidden, :visible_on, :visible_until, :vcs_submit, :token_period, :non_regenerating_tokens, :unlimited_tokens, :token_start_date, :token_end_date, :has_peer_review, :starter_file_type, :default_starter_file_group_id].freeze diff --git a/app/controllers/api/grade_entry_forms_controller.rb b/app/controllers/api/grade_entry_forms_controller.rb index 11127bdd0d..f4250ce488 100644 --- a/app/controllers/api/grade_entry_forms_controller.rb +++ b/app/controllers/api/grade_entry_forms_controller.rb @@ -1,6 +1,7 @@ module Api class GradeEntryFormsController < MainApiController - DEFAULT_FIELDS = [:id, :short_identifier, :description, :due_date, :is_hidden, :show_total].freeze + 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 # Requires: id diff --git a/app/lib/repository.rb b/app/lib/repository.rb index 7bc7d7af1f..018e606025 100644 --- a/app/lib/repository.rb +++ b/app/lib/repository.rb @@ -249,17 +249,46 @@ def self.get_repo_auth_records # Return a nested hash of the form { assignment_id => { section_id => visibility } } where visibility # is a boolean indicating whether the given assignment is visible to the given section. def self.visibility_hash + current_time = Time.current records = Assignment.left_outer_joins(:assessment_section_properties) .pluck_to_hash('assessments.id', 'section_id', 'assessments.is_hidden', - 'assessment_section_properties.is_hidden') + 'assessments.visible_on', + 'assessments.visible_until', + 'assessment_section_properties.is_hidden', + 'assessment_section_properties.visible_on', + 'assessment_section_properties.visible_until') + visibilities = records.uniq { |r| r['assessments.id'] } - .map { |r| [r['assessments.id'], Hash.new { !r['assessments.is_hidden'] }] } + .map do |r| + # Check if datetime-based visibility is set + visible_on = r['assessments.visible_on'] + visible_until = r['assessments.visible_until'] + default_visible = if visible_on || visible_until + (visible_on.nil? || visible_on <= current_time) && + (visible_until.nil? || visible_until >= current_time) + else + !r['assessments.is_hidden'] + end + [r['assessments.id'], Hash.new { default_visible }] + end .to_h + records.each do |r| - unless r['assessment_section_properties.is_hidden'].nil? - visibilities[r['assessments.id']][r['section_id']] = !r['assessment_section_properties.is_hidden'] + section_visible_on = r['assessment_section_properties.visible_on'] + section_visible_until = r['assessment_section_properties.visible_until'] + section_is_hidden = r['assessment_section_properties.is_hidden'] + + unless section_is_hidden.nil? && section_visible_on.nil? && section_visible_until.nil? + # Section-specific settings exist + section_visible = if section_visible_on || section_visible_until + (section_visible_on.nil? || section_visible_on <= current_time) && + (section_visible_until.nil? || section_visible_until >= current_time) + else + !section_is_hidden + end + visibilities[r['assessments.id']][r['section_id']] = section_visible end end visibilities diff --git a/app/models/assessment.rb b/app/models/assessment.rb index 56c6396ce3..35f3c87c1d 100644 --- a/app/models/assessment.rb +++ b/app/models/assessment.rb @@ -20,10 +20,13 @@ class Assessment < ApplicationRecord # date: true maps to DateValidator (custom_name: true maps to CustomNameValidator) # Look in lib/validators/* for more info validates :due_date, date: true + validates :visible_on, date: true, allow_nil: true + validates :visible_until, date: true, allow_nil: true validates :short_identifier, uniqueness: { scope: :course_id } validates :short_identifier, presence: true validate :short_identifier_unchanged, on: :update + validate :visible_dates_are_valid validates :description, presence: true validates :is_hidden, inclusion: { in: [true, false] } validates :short_identifier, format: { with: /\A[a-zA-Z0-9\-_]+\z/ } @@ -38,6 +41,13 @@ def short_identifier_unchanged false end + def visible_dates_are_valid + return if visible_on.nil? || visible_until.nil? + if visible_on >= visible_until + errors.add(:visible_until, 'must be after visible_on') + end + end + def upcoming(*) return true if self.due_date.nil? self.due_date > Time.current diff --git a/app/models/assessment_section_properties.rb b/app/models/assessment_section_properties.rb index 99c6da6fc8..6006ea7f86 100644 --- a/app/models/assessment_section_properties.rb +++ b/app/models/assessment_section_properties.rb @@ -5,6 +5,10 @@ class AssessmentSectionProperties < ApplicationRecord has_one :course, through: :assessment validate :courses_should_match + validates :visible_on, date: true, allow_nil: true + validates :visible_until, date: true, allow_nil: true + validate :visible_dates_are_valid + # Returns the dute date for a section of an assignment. Defaults to the global # due date of the assignment. def self.due_date_for(section, assignment) @@ -14,4 +18,11 @@ def self.due_date_for(section, assignment) where(section_id: section.id, assessment_id: assignment.id).first section_due_date.try(:due_date) || assignment.due_date end + + def visible_dates_are_valid + return if visible_on.nil? || visible_until.nil? + if visible_on >= visible_until + errors.add(:visible_until, 'must be after visible_on') + end + end end diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 6f0ecbe1f8..70e28fde19 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -120,7 +120,7 @@ class Assignment < Assessment :enable_student_tests, :allow_remarks, :display_grader_names_to_students, :display_median_to_students, :group_name_autogenerated, - :is_hidden, :vcs_submit, :has_peer_review].freeze + :is_hidden, :visible_on, :visible_until, :vcs_submit, :has_peer_review].freeze STARTER_FILES_DIR = ( Settings.file_storage.starter_files || File.join(Settings.file_storage.default_root_path, 'starter_files') @@ -1518,7 +1518,11 @@ def update_repo_required_files # Returns whether the visibility for this assignment changed after a save. def visibility_changed? saved_change_to_is_hidden? || + saved_change_to_visible_on? || + saved_change_to_visible_until? || assessment_section_properties.any?(&:is_hidden_previously_changed?) || + assessment_section_properties.any?(&:visible_on_previously_changed?) || + assessment_section_properties.any?(&:visible_until_previously_changed?) || @prev_assessment_section_property_ids != self.reload.assessment_section_properties.ids end end diff --git a/app/models/student.rb b/app/models/student.rb index 8cd6db7416..de9fbb35f5 100644 --- a/app/models/student.rb +++ b/app/models/student.rb @@ -256,26 +256,80 @@ def grace_credits_used_for(assessment) grouping.grace_period_deduction_single end - # Determine what assessments are visible to the role. - # By default, returns all assessments visible to the role for the current course. - # Optional parameter assessment_type takes values "Assignment" or "GradeEntryForm". If passed one of these options, - # only returns assessments of that type. Otherwise returns all assessment types. - # Optional parameter assessment_id: if passed an assessment id, returns a collection containing - # only the assessment with the given id, if it is visible to the current user. - # If it is not visible, returns an empty collection. + # Returns assessments visible to this student. + # Can filter by assessment_type ("Assignment" or "GradeEntryForm") and/or assessment_id. + # + # Visibility logic: + # - visible_on/visible_until datetime columns override is_hidden when set + # - Section-specific settings override global settings + # - Assessment is visible if current time is within the datetime range def visible_assessments(assessment_type: nil, assessment_id: nil) visible = self.assessments.where(type: assessment_type || Assessment.type) visible = visible.where(id: assessment_id) if assessment_id + current_time = Time.current + if self.section_id visible = visible.left_outer_joins(:assessment_section_properties) .where('assessment_section_properties.section_id': [self.section_id, nil]) - visible = visible.where('assessment_section_properties.is_hidden': false) - .or(visible.where('assessment_section_properties.is_hidden': nil, - 'assessments.is_hidden': false)) + + # Check section-specific visibility first (takes precedence when any section property is set) + section_visible = visible.where('assessment_section_properties.section_id': self.section_id) + .where.not(assessment_section_properties: { is_hidden: nil }) + .or(visible.where('assessment_section_properties.section_id': self.section_id) + .where.not(assessment_section_properties: { visible_on: nil })) + .or(visible.where('assessment_section_properties.section_id': self.section_id) + .where.not(assessment_section_properties: { visible_until: nil })) + + # Make sure current time is within the datetime range + section_visible = section_visible + .where(assessment_section_properties: { visible_on: nil }) + .or(section_visible.where(assessment_section_properties: { visible_on: ..current_time })) + section_visible = section_visible + .where(assessment_section_properties: { visible_until: nil }) + .or(section_visible.where(assessment_section_properties: { visible_until: current_time.. })) + + # Use datetime if set, otherwise fall back to is_hidden + section_visible = section_visible + .where.not(assessment_section_properties: { visible_on: nil }) + .or(section_visible.where.not(assessment_section_properties: { visible_until: nil })) + .or(section_visible.where(assessment_section_properties: { is_hidden: false })) + + # Check global visibility (when no section-specific settings exist) + global_visible = visible.where('assessment_section_properties.section_id': nil) + .or(visible.where(assessment_section_properties: { is_hidden: nil, + visible_on: nil, + visible_until: nil })) + + # Same datetime range check for global settings + global_visible = global_visible + .where(assessments: { visible_on: nil }) + .or(global_visible.where(assessments: { visible_on: ..current_time })) + global_visible = global_visible + .where(assessments: { visible_until: nil }) + .or(global_visible.where(assessments: { visible_until: current_time.. })) + + # Use datetime if set, otherwise fall back to is_hidden + global_visible = global_visible + .where.not(assessments: { visible_on: nil }) + .or(global_visible.where.not(assessments: { visible_until: nil })) + .or(global_visible.where(assessments: { is_hidden: false })) + + section_visible.or(global_visible) else - visible = visible.where(is_hidden: false) + # No section assigned - just check global visibility + visible = visible + .where(assessments: { visible_on: nil }) + .or(visible.where(assessments: { visible_on: ..current_time })) + visible = visible + .where(assessments: { visible_until: nil }) + .or(visible.where(assessments: { visible_until: current_time.. })) + + # Use datetime if set, otherwise fall back to is_hidden + visible + .where.not(assessments: { visible_on: nil }) + .or(visible.where.not(assessments: { visible_until: nil })) + .or(visible.where(assessments: { is_hidden: false })) end - visible end def section_name diff --git a/db/migrate/20251010150000_add_visibility_dates_to_assessments.rb b/db/migrate/20251010150000_add_visibility_dates_to_assessments.rb new file mode 100644 index 0000000000..75e9b9add8 --- /dev/null +++ b/db/migrate/20251010150000_add_visibility_dates_to_assessments.rb @@ -0,0 +1,8 @@ +class AddVisibilityDatesToAssessments < ActiveRecord::Migration[7.1] + def change + add_column :assessments, :visible_on, :datetime + add_column :assessments, :visible_until, :datetime + add_column :assessment_section_properties, :visible_on, :datetime + add_column :assessment_section_properties, :visible_until, :datetime + end +end diff --git a/db/migrate/20251010150001_update_check_repo_permissions_for_datetime_visibility.rb b/db/migrate/20251010150001_update_check_repo_permissions_for_datetime_visibility.rb new file mode 100644 index 0000000000..f5e96cc076 --- /dev/null +++ b/db/migrate/20251010150001_update_check_repo_permissions_for_datetime_visibility.rb @@ -0,0 +1,159 @@ +class UpdateCheckRepoPermissionsForDatetimeVisibility < ActiveRecord::Migration[7.1] + def up + # Drop and recreate the function with datetime visibility logic + execute <<-SQL + CREATE OR REPLACE FUNCTION public.check_repo_permissions(user_name_ character varying, course_name character varying, repo_name_ character varying) RETURNS boolean + LANGUAGE plpgsql + AS $$ + DECLARE + role_type varchar; + role_id_ integer; + BEGIN + SELECT roles.id, roles.type + INTO role_id_, role_type + FROM users + JOIN roles ON roles.user_id=users.id + JOIN courses ON roles.course_id=courses.id + WHERE courses.name=course_name AND users.user_name=user_name_ AND roles.hidden=false + FETCH FIRST ROW ONLY; + + IF role_type IN ('Instructor', 'AdminRole') THEN + RETURN true; + END IF; + IF role_type = 'Ta' THEN + RETURN EXISTS( + SELECT 1 + FROM memberships + JOIN roles ON roles.id = memberships.role_id + JOIN groupings ON memberships.grouping_id = groupings.id + JOIN groups ON groupings.group_id = groups.id + JOIN assignment_properties ON assignment_properties.assessment_id = groupings.assessment_id + WHERE memberships.type = 'TaMembership' + AND assignment_properties.anonymize_groups = false + AND roles.id = role_id_ + AND groups.repo_name = repo_name_ + ); + END IF; + IF role_type = 'Student' THEN + RETURN EXISTS( + SELECT roles.id + FROM memberships + JOIN roles ON roles.id=memberships.role_id + JOIN groupings ON memberships.grouping_id=groupings.id + JOIN groups ON groupings.group_id=groups.id + JOIN assignment_properties ON assignment_properties.assessment_id=groupings.assessment_id + JOIN assessments ON groupings.assessment_id=assessments.id + JOIN courses ON assessments.course_id=courses.id + LEFT OUTER JOIN assessment_section_properties ON assessment_section_properties.assessment_id=assessments.id + WHERE memberships.type='StudentMembership' + AND memberships.membership_status IN ('inviter','accepted') + AND assignment_properties.vcs_submit=true + AND roles.id=role_id_ + AND courses.is_hidden=false + AND groups.repo_name=repo_name_ + -- Datetime visibility logic: section-specific overrides global + AND ( + -- Section-specific visibility (when any section property is set) + ((assessment_section_properties.is_hidden IS NOT NULL OR + assessment_section_properties.visible_on IS NOT NULL OR + assessment_section_properties.visible_until IS NOT NULL) AND ( + -- If datetime columns are set, check datetime range + ((assessment_section_properties.visible_on IS NOT NULL OR assessment_section_properties.visible_until IS NOT NULL) AND + (assessment_section_properties.visible_on IS NULL OR assessment_section_properties.visible_on <= NOW()) AND + (assessment_section_properties.visible_until IS NULL OR assessment_section_properties.visible_until >= NOW())) + OR + -- Otherwise use is_hidden + ((assessment_section_properties.visible_on IS NULL AND assessment_section_properties.visible_until IS NULL) AND + assessment_section_properties.is_hidden=false) + )) + OR + -- Global visibility (no section properties set) + (assessment_section_properties.is_hidden IS NULL AND + assessment_section_properties.visible_on IS NULL AND + assessment_section_properties.visible_until IS NULL AND ( + -- If datetime columns are set, check datetime range + ((assessments.visible_on IS NOT NULL OR assessments.visible_until IS NOT NULL) AND + (assessments.visible_on IS NULL OR assessments.visible_on <= NOW()) AND + (assessments.visible_until IS NULL OR assessments.visible_until >= NOW())) + OR + -- Otherwise use is_hidden + ((assessments.visible_on IS NULL AND assessments.visible_until IS NULL) AND + assessments.is_hidden=false) + )) + ) + AND (assignment_properties.is_timed=false + OR groupings.start_time IS NOT NULL + OR (groupings.start_time IS NULL AND assessments.due_date= NOW())) + OR + -- Otherwise use is_hidden + ((assessment_section_properties.visible_on IS NULL AND assessment_section_properties.visible_until IS NULL) AND + assessment_section_properties.is_hidden=false) + )) + OR + -- Global visibility (no section properties set) + (assessment_section_properties.is_hidden IS NULL AND + assessment_section_properties.visible_on IS NULL AND + assessment_section_properties.visible_until IS NULL AND ( + -- If datetime columns are set, check datetime range + ((assessments.visible_on IS NOT NULL OR assessments.visible_until IS NOT NULL) AND + (assessments.visible_on IS NULL OR assessments.visible_on <= NOW()) AND + (assessments.visible_until IS NULL OR assessments.visible_until >= NOW())) + OR + -- Otherwise use is_hidden + ((assessments.visible_on IS NULL AND assessments.visible_until IS NULL) AND + assessments.is_hidden=false) + )) + ) + AND (assignment_properties.is_timed=false + OR groupings.start_time IS NOT NULL + OR (groupings.start_time IS NULL AND assessments.due_date