Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- Fix version mismatch between container client and database server (#7916)
- Fixed filter Canvas Test Student from roster sync (#7926)
- Fix: include original total mark in JSON response for remark requests (#7945)
- Fixed `(hidden)` assignment labeling for assignments with `visible_on` and/or `visible_until` set (#7944)

### 🔧 Internal changes
- Fixed flaky test `can bulk assign duplicated TAs to grade entry students` in `/spec/models/grade_entry_student_spec.rb` (#7958)
Expand Down
7 changes: 7 additions & 0 deletions app/helpers/assessments_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module AssessmentsHelper
def formatted_assessment_visibility_label(assessment, base_text)
return t('assignments.hidden', assignment_text: base_text) if assessment.currently_hidden?

base_text
end
end
8 changes: 8 additions & 0 deletions app/models/assessment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ def upcoming(*)
self.due_date > Time.current
end

def currently_hidden?
return true if is_hidden
return true if visible_on.present? && Time.current < visible_on
return true if visible_until.present? && Time.current > visible_until

false
end

# Returns grade distribution histogram bins of the grades for this assessment, using the grades in
# self.completed_result_marks.
def grade_distribution_array(intervals = 20)
Expand Down
2 changes: 1 addition & 1 deletion app/views/assignments/_list_manage.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<% assignments.each do |assignment| %>
<% route = { controller: 'assignments', action: action, id: assignment.id } %>
<% assignment_text = "#{h(assignment.short_identifier)}: #{h(assignment.description)}" %>
<% assignment_text = t('assignments.hidden', assignment_text: assignment_text) if assignment.is_hidden %>
<% assignment_text = formatted_assessment_visibility_label(assignment, assignment_text) %>
<tr>
<td>
<%= link_to assignment_text, route %>
Expand Down
19 changes: 6 additions & 13 deletions app/views/courses/_dashboard_list.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,12 @@
<% @assignments.each do |assignment| -%>
<tr>
<td>
<% if assignment.is_hidden %>
<%= link_to truncate(t('assignments.hidden',
assignment_text:
"#{h(assignment.short_identifier)}: #{h(assignment.description)}")),
view_summary_course_assignment_path(@current_course, assignment.id),
data: { remote: true, id: assignment.short_identifier },
class: (assignment.id == @current_assignment.id ? "inactive" : "") %>
<% else %>
<%= link_to assignment.short_identifier + ': ' + assignment.description,
view_summary_course_assignment_path(@current_course, assignment.id),
data: { remote: true, id: assignment.short_identifier },
class: (assignment.id == @current_assignment.id ? "inactive" : "") %>
<% end %>
<% assignment_text = formatted_assessment_visibility_label(assignment,
"#{h(assignment.short_identifier)}: #{h(assignment.description)}") %>
<%= link_to truncate(assignment_text),
view_summary_course_assignment_path(@current_course, assignment.id),
data: { remote: true, id: assignment.short_identifier },
class: (assignment.id == @current_assignment.id ? "inactive" : "") %>
</td>

<td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
<% if assessment.is_a?(Assignment) && assessment.is_peer_review? %>
<% title = "#{assessment.parent_assignment.short_identifier} #{PeerReview.model_name.human}" %>
<% end %>
<% if assessment.is_hidden %>
<% title = t('assignments.hidden', assignment_text: title) %>
<% end %>
<% title = formatted_assessment_visibility_label(assessment, title) %>
<%= title %>
<% end %>
<ul>
Expand Down
4 changes: 1 addition & 3 deletions app/views/shared/_assignment_dropdown_link.html.erb
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
<li class="<%= active_id == assignment.id ? 'active' : '' %>">
<% link_text = assignment.short_identifier %>
<% if assignment.is_hidden %>
<% link_text = t('assignments.hidden', assignment_text: link_text) %>
<% end %>
<% link_text = formatted_assessment_visibility_label(assignment, link_text) %>

<%= link_to link_text,
switch_course_assignment_path(@current_course, assignment.id),
Expand Down
1 change: 1 addition & 0 deletions doc/markus-contributors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ Parker Hutcheson
Partoo Vafaeikia
Paymahn Moghadasian
Peter Guanjie Zhao
Philip Kukulak
Pranav Rao
Rafael Padilha
Raine Yang
Expand Down
78 changes: 78 additions & 0 deletions spec/controllers/assignments_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
describe AssignmentsController do
include AutomatedTestsHelper
include ActiveSupport::Testing::TimeHelpers

# TODO: add 'role is from a different course' shared tests to each route test below

Expand Down Expand Up @@ -410,6 +411,83 @@
end
end

describe '#index rendered visibility labels' do
render_views

let(:role) { create(:instructor) }
let(:course) { role.course }
let(:current_time) { Time.zone.local(2026, 5, 9, 12, 0, 0) }

visibility_cases = [
{ label: 'explicitly hidden', options: -> { { is_hidden: true } }, hidden: true },
{ label: 'future start', options: -> { { visible_on: 1.day.from_now } }, hidden: true },
{ label: 'past end', options: -> { { visible_until: 1.day.ago } }, hidden: true },
{ label: 'window future',
options: -> { { visible_on: 1.day.from_now, visible_until: 2.days.from_now } }, hidden: true },
{ label: 'window expired',
options: -> { { visible_on: 2.days.ago, visible_until: 1.day.ago } }, hidden: true },
{ label: 'plain visible', options: -> { {} }, hidden: false },
{ label: 'past start', options: -> { { visible_on: 1.day.ago } }, hidden: false },
{ label: 'future end', options: -> { { visible_until: 1.day.from_now } }, hidden: false },
{ label: 'window current',
options: -> { { visible_on: 1.day.ago, visible_until: 1.day.from_now } }, hidden: false },
{ label: 'starts now', options: -> { { visible_on: Time.current } }, hidden: false },
{ label: 'ends now', options: -> { { visible_until: Time.current } }, hidden: false }
]

before { travel_to current_time }

visibility_cases.each do |vc|
it "renders #{vc[:hidden] ? 'the hidden label' : 'no hidden label'} for #{vc[:label]}" do
assignment = create(:assignment, course: course, **vc[:options].call)
get_as role, :index, params: { course_id: course.id }

base_text = "#{assignment.short_identifier}: #{assignment.description}"
hidden_text = I18n.t('assignments.hidden', assignment_text: base_text)

if vc[:hidden]
expect(response.body).to include(hidden_text)
else
expect(response.body).to include(base_text)
expect(response.body).not_to include(hidden_text)
end
end
end
end

describe '#edit rendered visibility labels' do
render_views

let(:role) { create(:instructor) }
let(:course) { role.course }
let(:current_time) { Time.zone.local(2026, 5, 9, 12, 0, 0) }
let(:scheduled_target) { create(:assignment, course: course, visible_on: 1.day.from_now) }
let(:currently_visible) { create(:assignment, course: course, visible_until: 1.day.from_now) }

before do
travel_to current_time
scheduled_target
currently_visible
end

it 'renders the hidden label in both the current assignment submenu title and the dropdown list item' do
get_as role, :edit, params: { course_id: course.id, id: scheduled_target.id }

current_title = I18n.t('assignments.hidden', assignment_text: scheduled_target.short_identifier)
doc = response.parsed_body
dropdown = doc.at_css('li#dropdown > div.dropdown')
dropdown_text = dropdown.children.find { |node| node.text? && !node.text.strip.empty? }.text.strip
target_path = switch_course_assignment_path(course, scheduled_target.id)
visible_path = switch_course_assignment_path(course, currently_visible.id)
target_link = dropdown.at_css("ul a[href='#{target_path}'][title='#{scheduled_target.description}']")
visible_link = dropdown.at_css("ul a[href='#{visible_path}'][title='#{currently_visible.description}']")

expect(dropdown_text).to eq(current_title)
expect(target_link.text).to eq(current_title)
expect(visible_link.text).to eq(currently_visible.short_identifier)
end
end

describe '#set_boolean_graders_options' do
let!(:assignment) { create(:assignment) }

Expand Down
49 changes: 49 additions & 0 deletions spec/controllers/courses_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
describe CoursesController do
include ActiveSupport::Testing::TimeHelpers

let(:instructor) { create(:instructor) }
let(:course) { instructor.course }
let(:student) { create(:student, course: course) }
Expand Down Expand Up @@ -358,6 +360,53 @@
end
end

describe '#show rendered visibility labels' do
render_views

let(:current_time) { Time.zone.local(2026, 5, 9, 12, 0, 0) }

# short_identifier and description keep text short so (hidden) is never truncated in _dashboard_list
visibility_cases = [
{ label: 'future start',
options: -> { { short_identifier: 'A1', description: 'a', visible_on: 1.day.from_now } },
hidden: true },
{ label: 'past end',
options: -> { { short_identifier: 'A2', description: 'a', visible_until: 1.day.ago } },
hidden: true },
{ label: 'plain visible',
options: -> { { short_identifier: 'A3', description: 'a' } },
hidden: false },
{ label: 'future end',
options: -> { { short_identifier: 'A4', description: 'a', visible_until: 1.day.from_now } },
hidden: false },
{ label: 'starts now',
options: -> { { short_identifier: 'A5', description: 'a', visible_on: Time.current } },
hidden: false },
{ label: 'ends now',
options: -> { { short_identifier: 'A6', description: 'a', visible_until: Time.current } },
hidden: false }
]

before { travel_to current_time }

visibility_cases.each do |vc|
it "renders #{vc[:hidden] ? 'the hidden label' : 'no hidden label'} for #{vc[:label]}" do
assignment = create(:assignment, course: course, **vc[:options].call)
get_as instructor, :show, params: { id: course }

base_text = "#{assignment.short_identifier}: #{assignment.description}"
hidden_text = I18n.t('assignments.hidden', assignment_text: base_text)

if vc[:hidden]
expect(response.body).to include(hidden_text)
else
expect(response.body).to include(base_text)
expect(response.body).not_to include(hidden_text)
end
end
end
end

describe '#upload_assignments' do
it_behaves_like 'a controller supporting upload', route_name: :upload_assignments do
let(:params) { { id: course.id } }
Expand Down
Loading