Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9a3bbb7
Updated aria-label to match previous implementations of search bars
mrafie1 Jun 13, 2026
5a4d69f
Added new search component to include filtering by case sensitivity. …
mrafie1 Jun 13, 2026
f60d621
Migrated grader and group tables to react table v8
mrafie1 Jun 13, 2026
996a8e7
Merge branch 'master' of https://github.com/MarkUsProject/Markus into…
mrafie1 Jun 13, 2026
2f7acc3
Updated columnVisibility for group table. Awaiting feedback
mrafie1 Jun 13, 2026
1d6e1d7
update changelog
mrafie1 Jun 13, 2026
a0abe67
Update Changelog.md
mrafie1 Jun 13, 2026
78ba3e2
Merge branch 'master' into migrate-GradersManager
mrafie1 Jun 15, 2026
03dbbc7
implemented minor feedback
mrafie1 Jun 20, 2026
4cabefa
build(deps): bump dayjs from 1.11.13 to 1.11.21 (#7982)
dependabot[bot] Jun 17, 2026
76679d7
build(deps): bump action_policy from 0.7.5 to 0.7.6 (#7976)
dependabot[bot] Jun 17, 2026
df4a04d
build(deps): bump dompurify from 3.4.0 to 3.4.9 (#8005)
dependabot[bot] Jun 17, 2026
42ad043
build(deps): bump playwright from 1.59.0 to 1.60.0 (#7980)
dependabot[bot] Jun 17, 2026
61d8a10
Fixed SVG rendering by converting base64 SVG data URIs to inline <svg…
donny-wong Jun 17, 2026
e5763e1
[pre-commit.ci] pre-commit autoupdate (#7974)
pre-commit-ci[bot] Jun 17, 2026
ae298bc
build(deps): bump the rjsf group with 2 updates (#7981)
dependabot[bot] Jun 17, 2026
f275cd3
Added missing foreight keys in seed data (#8006)
david-yz-liu Jun 17, 2026
482a036
build(deps): bump dompurify from 3.4.9 to 3.4.11 (#8009)
dependabot[bot] Jun 19, 2026
781eaa3
Added pagination to Admin Users table for performance (#7997)
donny-wong Jun 19, 2026
167b196
build(deps): bump nokogiri from 1.19.3 to 1.19.4 (#8010)
dependabot[bot] Jun 19, 2026
5633e94
Switched using react-resizable-panels in grading view (#8000)
YheChen Jun 19, 2026
7398d0f
fixed sorting issue
mrafie1 Jun 20, 2026
e629b8d
Updated grouptable showSection and showCoverage logic
mrafie1 Jun 20, 2026
aae0b5e
build(deps): bump concurrent-ruby from 1.3.6 to 1.3.7 (#8011)
dependabot[bot] Jun 20, 2026
62e02ba
Added confirm dialog when a student submits during late period (#8003)
danielrafailov1 Jun 20, 2026
ca05dfb
fix changelog
mrafie1 Jun 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ repos:
- id: prettier
types_or: [javascript, jsx, css, scss, html]
- repo: https://github.com/thibaudcolas/pre-commit-stylelint
rev: v17.10.0
rev: v17.12.0
hooks:
- id: stylelint
additional_dependencies: [
Expand All @@ -39,7 +39,7 @@ repos:
app/assets/stylesheets/common/_reset.scss
)$
- repo: https://github.com/rubocop/rubocop
rev: v1.86.1
rev: v1.87.0
hooks:
- id: rubocop
args: ["--autocorrect"]
Expand Down
6 changes: 6 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@
### 🚨 Breaking changes

### ✨ New features and improvements
- Migrated `GradersManager`'s `GradersTable` and `GroupsTable` components to React Table V8 (#8002)
- Added a confirm dialog when a student tries to submit work after the deadline has passed (#8003)
- Added a confirm dialog to the Upload Scans form that appears when no template divisions are assigned to the selected exam template (#7993)
- Migrated `MarkingSchemesTable` component to React Table V8 (#7985)
- Removed Graders Subcomponent and added a Graders column in the Assignment Grades tab (#7967)
- Added GET and PATCH /overall_comment API routes (#7963)
- Add case-sensitive search toggle to group name filters in graders, groups, submissions, and annotation usage tables (#7938)
- Add pagination to Admin Users table for performance (#7997)

### 🐛 Bug fixes
- Fixed bug where clicking MarkUs logo in navbar on mobile would open the sidebar instead of redirecting to courses page (#7990)
- Fixed bug where merge commits were incorrectly flagged as making a new assignment submission when no assignment files were changed (#7988)
- Fixed shift+up/shift+down keybinding being suppressed when a criterion input had focus; active criterion now scrolls into view when navigated to via keyboard (#7989)
- Fixed autotester spec upload when spec contains non-existent criterion (#7998)
- Fix SVG rendering by converting base64 SVG data URIs to inline <svg> (#8001)

### 🔧 Internal changes
- Replaced the grading view's custom jQuery pane resizing logic with `react-resizable-panels` (#8000)
- Added release automation scripts (#7914)
- Refactored the `SummaryPanel` marks chart modal to use `react-modal` instead of `ModalMarkus`, with test coverage for opening and closing the modal (#7996)
- Moved rubric criterion keyboard navigation (up/down/enter) from a global jQuery-based keybinding into `RubricCriterionInput`, replacing DOM class mutation with React state (`hoveredLevelIndex`); moved criterion navigation (shift+up/shift+down) into `MarksPanel`, eliminating the `window.marksPanel` global (#7989)
Expand All @@ -34,6 +39,7 @@
- Refactored `SubmissionFilePanel` subcomponents to React functional components (#7969)
- Migrated asset pipeline from Sprockets to Propshaft (#7970)
- Simplified Chart.js usage: removed the `DataChart` wrapper component, converted `chart_config.js` to an ES module, and replaced `registerables` with a minimal set of Chart.js components (#7987)
- Added missing foreign keys in seed data (#8006)

## [v2.10.0]

Expand Down
8 changes: 4 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
action_policy (0.7.5)
action_policy (0.7.6)
ruby-next-core (>= 1.0)
action_text-trix (2.1.18)
railties
Expand Down Expand Up @@ -123,7 +123,7 @@ GEM
combine_pdf (1.0.31)
matrix
ruby-rc4 (>= 0.1.5)
concurrent-ruby (1.3.6)
concurrent-ruby (1.3.7)
config (5.6.1)
deep_merge (~> 1.2, >= 1.2.1)
ostruct
Expand Down Expand Up @@ -278,7 +278,7 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.5)
nokogiri (1.19.3)
nokogiri (1.19.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
observer (0.1.2)
Expand Down Expand Up @@ -444,7 +444,7 @@ GEM
language_server-protocol (~> 3.17.0)
prism (>= 1.2, < 2.0)
rbs (>= 3, < 5)
ruby-next-core (1.1.1)
ruby-next-core (1.2.0)
ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5)
ruby2_keywords (0.0.5)
Expand Down
106 changes: 0 additions & 106 deletions app/assets/javascripts/Results/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,109 +25,3 @@
domContentLoadedCB();
}
})();

/* Constants: change them to customize the columns */
// Initial width percentage of left pane (e.g. 0.4 for 40%/60%)
var offset = 0.7;

// Limit from left/right that you can drag to. Must be smaller than offset
var limit = 0.25;

/* Global variables, manipulated later */
var left, right, panes_width, panes_offset, bounds;
var $panes, $drag;

/* Resizes the columns and handles column widths limits */
function resize_col() {
if (offset >= limit && offset <= 1 - limit) {
$drag.draggable("option", "revert", false);
left.style.width = offset * panes_width + "px";
var drag_width = $drag.width() + 2 * parseInt($drag.css("margin-left"));
right.style.width = (1 - offset) * panes_width - drag_width + "px";
} else {
// Just in case we somehow go past the limit
$drag.draggable("option", "revert", true);
offset = offset < limit ? limit : 1 - limit;
}

if (window.pdfViewer) {
window.pdfViewer.refresh_annotations();
}
}

/* Makes the bar draggable only along x-axis, containing to the panes box,
and handles the actual dragging event */
function make_draggable() {
$drag.draggable({
axis: "x",
containment: bounds,
revertDuration: 250,
drag: function (event, ui) {
// Update values in case they changed
panes_width = $panes.width();
panes_offset = $panes.offset();

// Calculate offset and resize
offset = (ui.offset.left - panes_offset.left) / panes_width;
resize_col();
},
});
}

/* Calculates the bounds for the drag bar */
function calculate_bounds() {
var drag_width = $drag.width() + 2 * parseInt($drag.css("margin-left"));
bounds = [
panes_offset.left + limit * panes_width + drag_width,
panes_offset.top,
panes_offset.left + panes_width - limit * panes_width - drag_width,
panes_offset.top + $panes.height(),
];
}

/* On page load: get DOM elements, calculate some stuff,
and initialize the drag bar/columns. */
function initializePanes() {
left = document.getElementById("left-pane");
right = document.getElementById("right-pane");
$panes = $("#panes");
$drag = $("#drag");
panes_width = $panes.width();
panes_offset = $panes.offset();

// Bounding box, taking the limit into account
calculate_bounds();

// Make sure the constants given are valid/positive
offset = Math.abs(offset);
limit = Math.abs(limit);

// Initialize the drag bar and resize the columns
make_draggable();
$drag.css("left", panes_offset.left + offset * panes_width + "px");
resize_col();
window.addEventListener("resize", fix_panes);
}

/* Handle window resizing */
// NOTE: Don't manually override window.onresize. This will conflict with
// other such uses, as we do in menu.js (TODO: change that one, too).
function fix_panes() {
panes_width = $panes.width();
panes_offset = $panes.offset();
resize_col();

// Update bounds
calculate_bounds();
$drag.draggable("destroy");
make_draggable();

// Make sure the drag bar stays in the right place
$drag.css("left", panes_offset.left + offset * panes_width + "px");
$drag.css("position", "inherit");

// Fix pdfViewer, if it exists.
if (window.pdfViewer) {
window.pdfViewer.refresh_annotations();
}
}
23 changes: 10 additions & 13 deletions app/assets/stylesheets/grader.scss
Original file line number Diff line number Diff line change
Expand Up @@ -202,31 +202,29 @@
display: flex;
flex-grow: 1;
margin-top: 0.5em;
min-height: 0;
width: 100%;

#panes {
display: flex;
width: 100%;

@include mixins.breakpoint(small) {
display: block;
display: block !important;
overflow: auto !important;
}
}

#left-pane,
#drag,
#right-pane {
.result-pane {
display: flex;
flex-direction: column;
flex-grow: 1;
margin: 0;
padding: 0;
vertical-align: top;
}

#left-pane,
#right-pane {
overflow: auto;
min-width: 0;

@include mixins.breakpoint(small) {
display: block;
Expand All @@ -235,33 +233,32 @@
}
}

#left-pane {
#left-pane .result-pane {
margin-right: 3px;
width: 70%;

@include mixins.breakpoint(small) {
margin-bottom: 1em;
margin-right: 0;
padding-right: 0;
}
}

#right-pane {
#right-pane .result-pane {
margin-left: 3px;
width: 29.5%;

@include mixins.breakpoint(small) {
margin-left: 0;
padding-left: 0;
}
}

#drag {
background: #ccc;
cursor: col-resize;
position: inherit;
width: 4px;

@include mixins.breakpoint(small) {
display: none;
display: none !important;
}
}
}
52 changes: 51 additions & 1 deletion app/controllers/admin/users_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module Admin
class UsersController < ApplicationController
DEFAULT_FIELDS = [:id, :user_name, :email, :id_number, :type, :first_name, :last_name].freeze
SEARCHABLE_FIELDS = %w[user_name first_name last_name email id_number].freeze
SORTABLE_FIELDS = %w[user_name first_name last_name email id_number type].freeze
before_action { authorize! }

respond_to :html
Expand All @@ -10,7 +12,19 @@ def index
respond_to do |format|
format.html
format.json do
render json: visible_users.order(:created_at).pluck_to_hash(*DEFAULT_FIELDS)
users_scope = sorted_users(filtered_users(visible_users))

per_page = (params[:per_page] || 100).to_i
current_page = (params[:page] || 1).to_i
total_count = users_scope.count
total_pages = [(total_count.to_f / per_page).ceil, 1].max
offset_value = (current_page - 1) * per_page
records = users_scope.limit(per_page).offset(offset_value)

render json: {
users: records.pluck_to_hash(*DEFAULT_FIELDS),
total_pages: total_pages
}
end
end
end
Expand Down Expand Up @@ -64,6 +78,42 @@ def visible_users
User.where.not(type: :AutotestUser)
end

# Apply column/type filters from the `filtered` param to the given scope.
def filtered_users(scope)
return scope if params[:filtered].blank?

JSON.parse(params[:filtered]).each do |f|
next if f['value'].blank?

if SEARCHABLE_FIELDS.include?(f['id'])
term = "%#{User.sanitize_sql_like(f['value'].strip)}%"
scope = scope.where("#{f['id']} ILIKE ?", term)
elsif f['id'] == 'type' && f['value'] != 'all'
scope = scope.where(type: f['value'])
end
end

scope
end

# Apply ordering from the `sorted` param to the given scope.
# Falls back to ordering by user_name when no valid sort is provided.
def sorted_users(scope)
return scope.order(:user_name) if params[:sorted].blank?

sort_configs = JSON.parse(params[:sorted])
order_clauses = sort_configs.filter_map do |sort_config|
next unless SORTABLE_FIELDS.include?(sort_config['id'])

direction = sort_config['desc'] ? 'DESC' : 'ASC'
"#{sort_config['id']} #{direction}"
end

return scope.order(:user_name) if order_clauses.empty?

scope.order(Arel.sql(order_clauses.join(', ')))
end

def flash_interpolation_options
{ resource_name: @user.user_name.presence || @user.model_name.human,
errors: @user.errors.full_messages.join('; ') }
Expand Down
4 changes: 4 additions & 0 deletions app/controllers/submissions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ def file_manager
set_filebrowser_vars(@grouping)
flash_file_manager_messages

past_due_date = @assignment.grouping_past_due_date?(@grouping)
past_collection_date = @grouping.past_collection_date?
@show_late_submit_confirmation = past_due_date && !past_collection_date

render 'file_manager', layout: 'assignment_content', locals: {}
end

Expand Down
7 changes: 7 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,11 @@ def markdown(text)
def yield_content!(content_key)
view_flow.content.delete(content_key)
end

def inline_svg_data_uris(html)
html.to_s.gsub(%r{<img\b[^>]*?\bsrc=(["'])data:image/svg\+xml;base64,([^"']+)\1[^>]*?>}i) do
svg = Base64.decode64(Regexp.last_match(2)).force_encoding('UTF-8')
svg.sub(/\A.*?(?=<svg)/m, '') # drop the <?xml?> prolog / DOCTYPE / comments
end
end
end
Loading