Sortable User Course List

closes OUT-6339 OUT-6346
flag=none

Test Plan:
- There are many cases that are handled by the automated and unit
  tests. The following steps are to ensure the page loads
  and behaves properly:
- Open the User Courses page (/courses)
- Observe that each column now has a sorting icon next to the
  column name
- Observe that the Published column is default sorted in
  ascending order
- Click through the different columns and observe that sorting
  by those columns and in descending order works as expected

Change-Id: Ie522e17f161f2a4d0ed8c35f62c5006162de9bd5
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/346737
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Martin Yosifov <martin.yosifov@instructure.com>
QA-Review: Martin Yosifov <martin.yosifov@instructure.com>
Product-Review: Kyle Rosenbaum <krosenbaum@instructure.com>
This commit is contained in:
Angela Gomba 2024-05-20 18:01:02 -04:00
parent e7faa44614
commit bd6db735b8
11 changed files with 655 additions and 53 deletions

View File

@ -567,15 +567,50 @@ class CoursesController < ApplicationController
end
end
@past_enrollments.sort_by! { |e| [e.course.published? ? 0 : 1, Canvas::ICU.collation_key(e.long_name)] }
[@current_enrollments, @future_enrollments].each do |list|
list.sort_by! do |e|
[e.course.published? ? 0 : 1, e.active? ? 1 : 0, Canvas::ICU.collation_key(e.long_name)]
end
end
@current_enrollments = sort_enrollments(@current_enrollments, "current")
@past_enrollments = sort_enrollments(@past_enrollments, "past")
@future_enrollments = sort_enrollments(@future_enrollments, "future")
end
helper_method :load_enrollments_for_index
def sort_enrollments(enrollments, type)
sort_column = nil
order = nil
case type
when "current"
sort_column = params[:cc_sort]
order = params[:cc_order]
when "past"
sort_column = params[:pc_sort]
order = params[:pc_order]
when "future"
sort_column = params[:fc_sort]
order = params[:fc_order]
end
sorted_enrollments = enrollments.sort_by! do |e|
case sort_column
when "favorite"
@current_user.courses_with_primary_enrollment(:favorite_courses).map(&:id).include?(e.course_id) ? 0 : 1
when "course"
e.course.name
when "nickname"
nickname = e.course.nickname_for(@current_user, nil)
[nickname.nil? ? 1 : 0, nickname]
when "term"
[e.course.enrollment_term.default_term? ? 1 : 0, e.course.enrollment_term.name]
when "enrolled_as"
e.readable_role_name
else
if type == "past"
[e.course.published? ? 0 : 1, Canvas::ICU.collation_key(e.long_name)]
else
[e.course.published? ? 0 : 1, e.active? ? 1 : 0, Canvas::ICU.collation_key(e.long_name)]
end
end
end
(order == "desc") ? sorted_enrollments.reverse : sorted_enrollments
end
def enrollments_for_index(type)
instance_variable_get(:"@#{type}_enrollments")
end

View File

@ -181,4 +181,24 @@ module CoursesHelper
end
end
end
def get_sorting_order(curr_col, sorted_col, order)
"desc" if (sorted_col.nil? && curr_col == "published") || (curr_col == sorted_col && order != "desc")
end
def get_sorting_icon(curr_col, sorted_col, order)
if (curr_col == sorted_col) || (curr_col == "published" && sorted_col.nil?)
"icon-mini-arrow-#{(order == "desc") ? "down" : "up"}"
else
"icon-mini-arrow-double"
end
end
def get_courses_params(table, col, params)
params.permit(:cc_sort, :cc_order, :pc_sort, :pc_order, :fc_sort, :fc_order).merge({
"#{table}_sort": col,
"#{table}_order": get_sorting_order(col, params[:cc_sort], params[:cc_order]),
focus: table
})
end
end

View File

@ -24,16 +24,28 @@
/* Column widths are based on these cells */
.course-list-star-column {
width: 3%;
width: 10%;
&.course-list-favorite-icon {
color: $ic-color-icon-disabled;
}
}
.course-list-sort-icon {
color: #C7CDD1;
&.sorted {
color: $ic-font-color-dark;
}
}
.course-list-column-header {
a {
color: $ic-font-color-dark;
text-decoration: none;
}
}
.course-list-term-column,
.course-list-enrolled-as-column {
width: 15%;
}
.course-list-published-column {
.course-list-published-column {
width: 7%;
}
.course-list-nickname-column {

View File

@ -16,6 +16,8 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
%>
<% js_bundle :courses %>
<%
if k5_user?
all_courses_title = t('All Subjects')
@ -71,12 +73,38 @@
<table id="my_courses_table" class="ic-Table ic-Table--bordered course-list-table">
<thead>
<tr>
<% if @show_star_column %><th scope="col" class="course-list-star-column"><span class="screenreader-only"><%= t ('Favorites') %></span></th><% end %>
<th scope="col" class="course-list-course-title-column course-list-no-left-border"><%= course_label %></th>
<th scope="col" class="course-list-nickname-column course-list-no-left-border"><%= t ('Nickname') %></th>
<th scope="col" class="course-list-term-column course-list-no-left-border"><%= t ('Term') %></th>
<th scope="col" class="course-list-enrolled-as-column course-list-no-left-border"><%= t ('Enrolled as') %></th>
<th scope="col" class="course-list-published-column course-list-no-left-border"><%= t ('Published') %></th>
<% if @show_star_column %>
<th scope="col" class="course-list-star-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("cc", "favorite", params)) %>" id="cc_favorite">
<%= t('Favorite') %> <i class="<%= "course-list-sort-icon #{params[:cc_sort] == "favorite" ? "sorted" : ""}" %> <%= get_sorting_icon("favorite", params[:cc_sort], params[:cc_order]) %>"></i>
</a>
</th>
<% end %>
<th scope="col" class="course-list-course-title-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("cc", "course", params)) %>" id="cc_course">
<%= course_label %> <i class="<%= "course-list-sort-icon #{params[:cc_sort] == "course" ? "sorted" : ""}" %> <%= get_sorting_icon("course", params[:cc_sort], params[:cc_order]) %>"></i>
</a>
</th>
<th scope="col" class="course-list-nickname-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("cc", "nickname", params)) %>" id="cc_nickname">
<%= t ('Nickname') %> <i class="<%= "course-list-sort-icon #{params[:cc_sort] == "nickname" ? "sorted" : ""}" %> <%= get_sorting_icon("nickname", params[:cc_sort], params[:cc_order]) %>"></i>
</a>
</th>
<th scope="col" class="course-list-term-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("cc", "term", params)) %>" id="cc_term">
<%= t ('Term') %> <i class="<%= "course-list-sort-icon #{params[:cc_sort] == "term" ? "sorted" : ""}" %> <%= get_sorting_icon("term", params[:cc_sort], params[:cc_order]) %>"></i>
</a>
</th>
<th scope="col" class="course-list-enrolled-as-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("cc", "enrolled_as", params)) %>" id="cc_enrolled_as">
<%= t ('Enrolled as') %> <i class="<%= "course-list-sort-icon #{params[:cc_sort] == "enrolled_as" ? "sorted" : ""}" %> <%= get_sorting_icon("enrolled_as", params[:cc_sort], params[:cc_order]) %>"></i>
</a>
</th>
<th scope="col" class="course-list-published-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("cc", "published", params)) %>" id="cc_published">
<%= t ('Published') %> <i class="<%= "course-list-sort-icon #{(params[:cc_sort] == "published" || params[:cc_sort].nil?) ? "sorted" : ""}" %> <%= get_sorting_icon("published", params[:cc_sort], params[:cc_order]) %>"></i>
</a>
</th>
</tr>
</thead>
<tbody>
@ -95,12 +123,38 @@
<table id="past_enrollments_table" class="ic-Table ic-Table--bordered course-list-table">
<thead>
<tr>
<% if @show_star_column %><th scope="col" class="course-list-star-column"><span class="screenreader-only"><%= t ('Favorites') %></span></th><% end %>
<th scope="col" class="course-list-course-title-column course-list-no-left-border"><%= course_label %></th>
<th scope="col" class="course-list-nickname-column course-list-no-left-border"><%= t ('Nickname') %></th>
<th scope="col" class="course-list-term-column course-list-no-left-border"><%= t ('Term') %></th>
<th scope="col" class="course-list-enrolled-as-column course-list-no-left-border"><%= t ('Enrolled as') %></th>
<th scope="col" class="course-list-published-column course-list-no-left-border"><%= t ('Published') %></th>
<% if @show_star_column %>
<th scope="col" class="course-list-star-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("pc", "favorite", params)) %>" id="pc_favorite">
<%= t('Favorite') %> <i class="<%= "course-list-sort-icon #{params[:pc_sort] == "favorite" ? "sorted" : ""}" %> <%= get_sorting_icon("favorite", params[:pc_sort], params[:pc_order]) %>"></i>
</a>
</th>
<% end %>
<th scope="col" class="course-list-course-title-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("pc", "course", params)) %>" id="pc_course">
<%= course_label %> <i class="<%= "course-list-sort-icon #{params[:pc_sort] == "course" ? "sorted" : ""}" %> <%= get_sorting_icon("course", params[:pc_sort], params[:pc_order]) %>"></i>
</a>
</th>
<th scope="col" class="course-list-nickname-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("pc", "nickname", params)) %>" id="pc_nickname">
<%= t ('Nickname') %> <i class="<%= "course-list-sort-icon #{params[:pc_sort] == "nickname" ? "sorted" : ""}" %> <%= get_sorting_icon("nickname", params[:pc_sort], params[:pc_order]) %>"></i>
</a>
</th>
<th scope="col" class="course-list-term-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("pc", "term", params)) %>" id="pc_term">
<%= t ('Term') %> <i class="<%= "course-list-sort-icon #{params[:pc_sort] == "term" ? "sorted" : ""}" %> <%= get_sorting_icon("term", params[:pc_sort], params[:pc_order]) %>"></i>
</a>
</th>
<th scope="col" class="course-list-enrolled-as-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("pc", "enrolled_as", params)) %>" id="pc_enrolled_as">
<%= t ('Enrolled as') %> <i class="<%= "course-list-sort-icon #{params[:pc_sort] == "enrolled_as" ? "sorted" : ""}" %> <%= get_sorting_icon("enrolled_as", params[:pc_sort], params[:pc_order]) %>"></i>
</a>
</th>
<th scope="col" class="course-list-published-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("pc", "published", params)) %>" id="pc_published">
<%= t ('Published') %> <i class="<%= "course-list-sort-icon #{(params[:pc_sort] == "published" || params[:pc_sort].nil?) ? "sorted" : ""}" %> <%= get_sorting_icon("published", params[:pc_sort], params[:pc_order]) %>"></i>
</a>
</th>
</tr>
</thead>
<tbody>
@ -119,12 +173,38 @@
<table id="future_enrollments_table" class="ic-Table ic-Table--bordered course-list-table">
<thead>
<tr>
<% if @show_star_column %><th scope="col" class="course-list-star-column"><span class="screenreader-only"><%= t ('Favorites') %></span></th><% end %>
<th scope="col" class="course-list-course-title-column course-list-no-left-border"><%= course_label %></th>
<th scope="col" class="course-list-nickname-column course-list-no-left-border"><%= t ('Nickname') %></th>
<th scope="col" class="course-list-term-column course-list-no-left-border"><%= t ('Term') %></th>
<th scope="col" class="course-list-enrolled-as-column course-list-no-left-border"><%= t ('Enrolled as') %></th>
<th scope="col" class="course-list-published-column course-list-no-left-border"><%= t ('Published') %></th>
<% if @show_star_column %>
<th scope="col" class="course-list-star-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("fc", "favorite", params)) %>" id="fc_favorite">
<%= t('Favorite') %> <i class="<%= "course-list-sort-icon #{params[:fc_sort] == "favorite" ? "sorted" : ""}" %> <%= get_sorting_icon("favorite", params[:fc_sort], params[:fc_order]) %>"></i>
</a>
</th>
<% end %>
<th scope="col" class="course-list-course-title-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("fc", "course", params)) %>" id="fc_course">
<%= course_label %> <i class="<%= "course-list-sort-icon #{params[:fc_sort] == "course" ? "sorted" : ""}" %> <%= get_sorting_icon("course", params[:fc_sort], params[:fc_order]) %>"></i>
</a>
</th>
<th scope="col" class="course-list-nickname-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("fc", "nickname", params)) %>" id="fc_nickname">
<%= t ('Nickname') %> <i class="<%= "course-list-sort-icon #{params[:fc_sort] == "nickname" ? "sorted" : ""}" %> <%= get_sorting_icon("nickname", params[:fc_sort], params[:fc_order]) %>"></i>
</a>
</th>
<th scope="col" class="course-list-term-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("fc", "term", params)) %>" id="fc_term">
<%= t ('Term') %> <i class="<%= "course-list-sort-icon #{params[:fc_sort] == "term" ? "sorted" : ""}" %> <%= get_sorting_icon("term", params[:fc_sort], params[:fc_order]) %>"></i>
</a>
</th>
<th scope="col" class="course-list-enrolled-as-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("fc", "enrolled_as", params)) %>" id="fc_enrolled_as">
<%= t ('Enrolled as') %> <i class="<%= "course-list-sort-icon #{params[:fc_sort] == "enrolled_as" ? "sorted" : ""}" %> <%= get_sorting_icon("enrolled_as", params[:fc_sort], params[:fc_order]) %>"></i>
</a>
</th>
<th scope="col" class="course-list-published-column course-list-column-header course-list-no-left-border">
<a href="<%= courses_path(get_courses_params("fc", "published", params)) %>" id="fc_published">
<%= t ('Published') %> <i class="<%= "course-list-sort-icon #{(params[:fc_sort] == "published" || params[:fc_sort].nil?) ? "sorted" : ""}" %> <%= get_sorting_icon("published", params[:fc_sort], params[:fc_order]) %>"></i>
</a>
</th>
</tr>
</thead>
<tbody>

View File

@ -29,12 +29,12 @@ describe CoursesController do
controller.instance_variable_set(:@domain_root_account, Account.default)
end
def get_index(user = nil)
def get_index(user: nil, index_params: {})
user_session(user) if user
user ||= @user
controller.instance_variable_set(:@current_user, user)
get "index", params: index_params
controller.load_enrollments_for_index
get "index"
end
it "forces login" do
@ -75,7 +75,7 @@ describe CoursesController do
course_with_student_logged_in
toggle_k5_setting(@course.account)
get_index @student
get_index(user: @student)
expect(assigns[:js_bundles].flatten).to include :k5_theme
expect(assigns[:css_bundles].flatten).to include :k5_theme, :k5_font
end
@ -83,7 +83,7 @@ describe CoursesController do
it "does not set k5_theme when k5 is off" do
course_with_student_logged_in
get_index @student
get_index(user: @student)
expect(assigns[:js_bundles].flatten).not_to include :k5_theme
expect(assigns[:css_bundles].flatten).not_to include :k5_theme, :k5_font
end
@ -93,7 +93,7 @@ describe CoursesController do
toggle_k5_setting(@course.account)
toggle_classic_font_setting(@course.account)
get_index @student
get_index(user: @student)
expect(assigns[:css_bundles].flatten).to include :k5_theme
expect(assigns[:css_bundles].flatten).not_to include :k5_font
end
@ -139,6 +139,133 @@ describe CoursesController do
end
end
shared_examples "sorting" do
before do
@course1 = (type == "future") ? Account.default.courses.create!(name: "A", start_at: 1.month.from_now, restrict_enrollments_to_course_dates: true) : Account.default.courses.create!(name: "A")
@course2 = (type == "future") ? Account.default.courses.create!(name: "Z", start_at: 1.month.from_now, restrict_enrollments_to_course_dates: true) : Account.default.courses.create!(name: "Z")
# user is enrolled as a student in course 1
enrollment1 = course_with_student user: @student, course: @course1
enrollment1.invite!
# publish course 2
@course2.offer!
# user is enrolled as a ta in course 2
enrollment2 = course_with_ta course: @course2, user: @student, active_all: true
term1 = @course1.root_account.enrollment_terms.create!(name: "Term 1")
@course1.enrollment_term = term1
@course1.save!
term2 = @course2.root_account.enrollment_terms.create!(name: "Term 2")
@course2.enrollment_term = term2
@course2.save!
@student.set_preference(:course_nicknames, @course1.id, "English")
@student.set_preference(:course_nicknames, @course2.id, "Math")
@student.favorites.create!(context: @course2)
if type == "past"
[enrollment1, enrollment2].each(&:complete!)
end
end
context "on published column" do
it "lists unpublished courses after published" do
user_session(@student)
get_index
expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course2.id, @course1.id]
end
it "lists unpublished courses after published when descending order" do
user_session(@student)
get_index(index_params: { sort_column => "published", order_column => "desc" })
expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course1.id, @course2.id]
end
end
context "on enrolled as column" do
it "lists enrollment type alphabetically" do
user_session(@student)
get_index(index_params: { sort_column => "enrolled_as" })
expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course1.id, @course2.id]
end
it "lists enrollment type reverse alphabetically when descending order" do
user_session(@student)
get_index(index_params: { sort_column => "enrolled_as", order_column => "desc" })
expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course2.id, @course1.id]
end
end
context "on term column" do
it "lists terms alphabetically" do
user_session(@student)
get_index(index_params: { sort_column => "term" })
expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course1.id, @course2.id]
end
it "lists terms reverse alphabetically when descending order" do
user_session(@student)
get_index(index_params: { sort_column => "term", order_column => "desc" })
expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course2.id, @course1.id]
end
end
context "on nickname column" do
it "lists course nicknames alphabetically" do
user_session(@student)
get_index(index_params: { sort_column => "nickname" })
expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course1.id, @course2.id]
end
it "lists course nicknames reverse alphabetically when descending order" do
user_session(@student)
get_index(index_params: { sort_column => "nickname", order_column => "desc" })
expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course2.id, @course1.id]
end
end
context "on course name column" do
it "lists course names alphabetically" do
user_session(@student)
get_index(index_params: { sort_column => "course" })
expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course1.id, @course2.id]
end
it "lists course names reverse alphabetically when descending order" do
user_session(@student)
get_index(index_params: { sort_column => "course", order_column => "desc" })
expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course2.id, @course1.id]
end
end
context "on favorites column" do
it "lists favorited courses first" do
user_session(@student)
get_index(index_params: { sort_column => "favorite" })
if type == "past"
# Only active courses can be favorited. Therefore, we don't expect the sorting to affect the order.
expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course1.id, @course2.id]
else
expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course2.id, @course1.id]
end
end
it "lists favorited courses last when descending order" do
user_session(@student)
get_index(index_params: { sort_column => "favorite", order_column => "desc" })
if type == "past"
# Only active courses can be favorited. Therefore, the list will just be reversed from its ascending order.
expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course2.id, @course1.id]
else
expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course1.id, @course2.id]
end
end
end
end
describe "current_enrollments" do
it "groups enrollments by course and type" do
# enrollments with multiple sections of the same type should be de-duped
@ -246,25 +373,6 @@ describe CoursesController do
expect(assigns[:future_enrollments]).to be_empty
end
describe "unpublished_courses" do
it "lists unpublished courses after published" do
# unpublished course
course1 = Account.default.courses.create! name: "A"
enrollment1 = course_with_student user: @student, course: course1
enrollment1.invite!
expect(course1).to be_unpublished
# published course
course2 = Account.default.courses.create! name: "Z"
course2.offer!
course_with_student course: course2, user: @student, active_all: true
user_session(@student)
get_index
expect(assigns[:current_enrollments].map(&:course_id)).to eq [course2.id, course1.id]
end
end
context "as enrollment admin" do
it "includes courses with no applicable start/end dates" do
# no dates at all
@ -293,6 +401,14 @@ describe CoursesController do
expect(assigns[:future_enrollments]).to be_empty
end
end
describe "sorting" do
include_examples "sorting" do
let(:type) { "current" }
let(:sort_column) { "cc_sort" }
let(:order_column) { "cc_order" }
end
end
end
describe "past_enrollments" do
@ -502,7 +618,7 @@ describe CoursesController do
course1.enrollment_term.update_attribute(:end_at, 1.month.ago)
get_index(@student)
get_index(user: @student)
expect(response).to be_successful
expect(assigns[:past_enrollments]).to be_empty
expect(assigns[:current_enrollments]).to be_empty
@ -510,13 +626,13 @@ describe CoursesController do
observer = user_with_pseudonym(active_all: true)
add_linked_observer(@student, observer)
get_index(observer)
get_index(user: observer)
expect(response).to be_successful
expect(assigns[:past_enrollments]).to be_empty
expect(assigns[:current_enrollments]).to be_empty
expect(assigns[:future_enrollments]).to be_empty
get_index(teacher)
get_index(user: teacher)
expect(response).to be_successful
expect(assigns[:past_enrollments]).to eq [teacher_enrollment]
expect(assigns[:current_enrollments]).to be_empty
@ -565,6 +681,14 @@ describe CoursesController do
expect(assigns[:past_enrollments].map(&:course_id)).to eq [course2.id, course1.id] # Z, then A
end
end
describe "sorting" do
include_examples "sorting" do
let(:type) { "past" }
let(:sort_column) { "pc_sort" }
let(:order_column) { "pc_order" }
end
end
end
describe "future_enrollments" do
@ -698,6 +822,14 @@ describe CoursesController do
expect(assigns[:future_enrollments].map(&:course_id)).to eq [course2.id, course1.id] # Z, then A
end
end
describe "sorting" do
include_examples "sorting" do
let(:type) { "future" }
let(:sort_column) { "fc_sort" }
let(:order_column) { "fc_order" }
end
end
end
describe "per-assignment permissions" do

View File

@ -270,4 +270,64 @@ describe CoursesHelper do
expect(format_course_section_date).to eq "(no date)"
end
end
describe "sortable user course list helpers" do
context "get_sorting_order" do
it "returns 'desc' if we are sorting on the current col that is in ascending order" do
expect(get_sorting_order("favorite", "favorite", nil)).to eq("desc")
end
it "returns 'desc' if the default col is being sorted on in ascending order" do
expect(get_sorting_order("published", nil, nil)).to eq("desc")
end
it "returns nil if we are not sorting on the current col" do
expect(get_sorting_order("enrolled_as", "favorite", nil)).to be_nil
end
it "returns nil if we are sorting on the current col that is in descending order" do
expect(get_sorting_order("favorite", "favorite", "desc")).to be_nil
end
end
context "get_sorting_icon" do
it "returns the double arrow icon if we are not sorting on the given column" do
expect(get_sorting_icon("favorite", "published", "desc")).to eq("icon-mini-arrow-double")
end
it "returns the upward arrow icon if we are sorting on the given column in ascending order" do
expect(get_sorting_icon("favorite", "favorite", nil)).to eq("icon-mini-arrow-up")
end
it "returns the downward arrow icon if we are sorting on the given column in descending order" do
expect(get_sorting_icon("favorite", "favorite", "desc")).to eq("icon-mini-arrow-down")
end
end
context "get_courses_params" do
it "returns the correct params for the given table" do
table = "cc"
column = "favorite"
old_params = ActionController::Parameters.new
new_params = ActionController::Parameters.new(cc_sort: column, cc_order: nil, focus: table)
expect(get_courses_params(table, column, old_params)).to eq(new_params.permit(:cc_sort, :cc_order, :focus))
end
it "returns the correct params for the given table and params for other tables" do
table = "cc"
column = "favorite"
old_params = ActionController::Parameters.new(pc_sort: "published")
new_params = ActionController::Parameters.new(cc_sort: column, cc_order: nil, focus: table, pc_sort: "published")
expect(get_courses_params(table, column, old_params)).to eq(new_params.permit(:cc_sort, :cc_order, :focus, :pc_sort))
end
it "only returns permitted params" do
table = "cc"
column = "favorite"
old_params = ActionController::Parameters.new(foo: "bar")
new_params = ActionController::Parameters.new(cc_sort: column, cc_order: nil, focus: table)
expect(get_courses_params(table, column, old_params)).to eq(new_params.permit(:cc_sort, :cc_order, :focus))
end
end
end
end

View File

@ -177,4 +177,200 @@ describe "course index" do
expect(fj('h2:contains("Create Course")')).to be_displayed
end
end
context "sorting" do
it "by favorite column lists favorited courses first when ascending and last when descending" do
favorite_course = @current_courses[0]
@user.favorites.create!(context: favorite_course)
get "/courses"
# ascending
favorites_column_header(current_enrollments_selector).click
wait_for_ajaximations
sorted_courses = table_rows(current_enrollments_selector)
# remove header row
sorted_courses.shift
expect(sorted_courses.first).to eq row_with_text(favorite_course.name)
# descending
favorites_column_header(current_enrollments_selector).click
wait_for_ajaximations
sorted_courses = table_rows(current_enrollments_selector)
# remove header row
sorted_courses.shift
expect(sorted_courses.last).to eq row_with_text(favorite_course.name)
end
it "by course column lists alphabetically when ascending and reverse when descending" do
course_name = "Classic Course (Current)"
# Selenium is having trouble clicking the nickname column header, even though it
# works fine in the UI. To test sorting, we just load the page with the
# url params to test nickname column sorting.
# Otherwise we would test it the same way as the other headers:
# get "/courses"
# title_column_header(current_enrollments_selector).click
# wait_for_ajaximations
get "/courses?cc_sort=course"
sorted_courses = table_rows(current_enrollments_selector)
# remove header row
sorted_courses.shift
expect(sorted_courses.first).to eq row_with_text(course_name)
# descending
get "/courses?cc_order=desc&cc_sort=course"
sorted_courses = table_rows(current_enrollments_selector)
# remove header row
sorted_courses.shift
expect(sorted_courses.last).to eq row_with_text(course_name)
end
it "by nickname column lists alphabetically when ascending and reverse when descending" do
course_nickname = "Classic Course Nickname (Current)"
course = @current_courses[0]
@user.set_preference(:course_nicknames, course.id, course_nickname)
# Selenium is having trouble clicking the term column header, even though it
# works fine in the UI. To test sorting, we just load the page with the
# url params to test term column sorting.
# Otherwise we would test it the same way as the other headers:
# get "/courses"
# nickname_column_header(current_enrollments_selector).click
# wait_for_ajaximations
get "/courses?cc_sort=nickname"
sorted_courses = table_rows(current_enrollments_selector)
# remove header row
sorted_courses.shift
expect(sorted_courses.first).to eq row_with_text(course_nickname)
# descending
get "/courses?cc_order=desc&cc_sort=nickname"
sorted_courses = table_rows(current_enrollments_selector)
# remove header row
sorted_courses.shift
expect(sorted_courses.last).to eq row_with_text(course_nickname)
end
it "by term column lists alphabetically when ascending and reverse when descending" do
course = @current_courses[0]
term = course.root_account.enrollment_terms.create!(name: "Term 1")
course.enrollment_term = term
course.save!
# Selenium is having trouble clicking the term column header, even though it
# works fine in the UI. To test sorting, we just load the page with the
# url params to test term column sorting.
# Otherwise we would test it the same way as the other headers:
# get "/courses"
# term_column_header(current_enrollments_selector).click
# wait_for_ajaximations
get "/courses?cc_sort=term"
sorted_courses = table_rows(current_enrollments_selector)
# remove header row
sorted_courses.shift
expect(sorted_courses.first).to eq row_with_text(course.name)
# descending
get "/courses?cc_order=desc&cc_sort=term"
sorted_courses = table_rows(current_enrollments_selector)
# remove header row
sorted_courses.shift
expect(sorted_courses.last).to eq row_with_text(course.name)
end
it "by enrolled as column lists alphabetically when ascending and reverse when descending" do
@current_courses << course_with_ta(course_name: "Classic Course 2 (Current)", account: @classic_account, user: @user, active_all: true).course
get "/courses"
# ascending
enrolled_as_column_header(current_enrollments_selector).click
wait_for_ajaximations
sorted_courses = table_rows(current_enrollments_selector)
# remove header row
sorted_courses.shift
expect(sorted_courses.first.text.include?("Student")).to be true
expect(sorted_courses.last.text.include?("TA")).to be true
# descending
enrolled_as_column_header(current_enrollments_selector).click
wait_for_ajaximations
sorted_courses = table_rows(current_enrollments_selector)
# remove header row
sorted_courses.shift
expect(sorted_courses.first.text.include?("TA")).to be true
expect(sorted_courses.last.text.include?("Student")).to be true
end
it "by published column lists published first when ascending and non-published first when descending" do
@current_courses << course_with_student(course_name: "Classic Course 2 (Current)", account: @classic_account, user: @user, active_all: true).course
unpublished_course = @current_courses[2]
unpublished_course.workflow_state = "created"
unpublished_course.save!
get "/courses"
# sorted by published (ascending) by default
sorted_courses = table_rows(current_enrollments_selector)
# remove header row
sorted_courses.shift
expect(sorted_courses.last).to eq row_with_text(unpublished_course.name)
# descending
published_column_header(current_enrollments_selector).click
wait_for_ajaximations
sorted_courses = table_rows(current_enrollments_selector)
# remove header row
sorted_courses.shift
expect(sorted_courses.first).to eq row_with_text(unpublished_course.name)
end
it "is independent across tables" do
@current_courses << course_with_student(course_name: "Classic Course 2 (Current)", account: @classic_account, user: @user, active_all: true).course
unpublished_course = @current_courses[2]
unpublished_course.workflow_state = "created"
unpublished_course.save!
@past_courses << course_with_ta(course_name: "Classic Course 3 (Current)", account: @classic_account, user: @user, active_all: true).course
course_as_ta = @past_courses[2]
course_as_ta.complete!
favorite_course = @future_courses[0]
@user.favorites.create!(context: favorite_course)
get "/courses"
# sort the current courses by published (desc) column
published_column_header(current_enrollments_selector).click
wait_for_ajaximations
# sort the past courses by enrolled as column
enrolled_as_column_header(past_enrollments_selector).click
wait_for_ajaximations
# sort the future courses by favorites column
favorites_column_header(future_enrollments_selector).click
wait_for_ajaximations
sorted_current_courses = table_rows(current_enrollments_selector)
sorted_past_courses = table_rows(past_enrollments_selector)
sorted_future_courses = table_rows(future_enrollments_selector)
# remove header rows
sorted_current_courses.shift
sorted_past_courses.shift
sorted_future_courses.shift
expect(sorted_current_courses.first).to eq row_with_text(unpublished_course.name)
expect(sorted_past_courses.last).to eq row_with_text(course_as_ta.name)
expect(sorted_future_courses.first).to eq row_with_text(favorite_course.name)
end
end
end

View File

@ -77,4 +77,36 @@ module CourseIndexPage
def favorite_icon(course_name)
f(favorite_icon_selector(course_name))
end
def favorites_column_header(table)
table_header_row(table)[0]
end
def title_column_header(table)
table_header_row(table)[1]
end
def nickname_column_header(table)
table_header_row(table)[2]
end
def term_column_header(table)
table_header_row(table)[3]
end
def enrolled_as_column_header(table)
table_header_row(table)[4]
end
def published_column_header(table)
table_header_row(table)[5]
end
def table_rows(table)
ff("#{table} tr")
end
def table_header_row(table)
ff("#{table} tr th")
end
end

View File

@ -82,6 +82,7 @@ const featureBundles: {
course_statistics: () => import('./features/course_statistics/index'),
course_wizard: () => import('./features/course_wizard/index'),
course: () => import('./features/course/index'),
courses: () => import('./features/courses/index'),
dashboard: () => import('./features/dashboard/index'),
deep_linking_response: () => import('./features/deep_linking_response/index'),
developer_keys_v2: () => import('./features/developer_keys_v2/index'),

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2024 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import ready from '@instructure/ready'
ready(() => {
const params = new URLSearchParams(window.location.search)
const sortingTable = params.get('focus')
if (sortingTable) {
const sortingCol = params.get(sortingTable + '_sort')
const focusedHeader = document.querySelector('a#' + sortingTable + '_' + sortingCol)
if (focusedHeader) focusedHeader.focus()
}
})

View File

@ -0,0 +1,5 @@
{
"name": "@canvas-features/courses",
"private": true,
"version": "1.0.0"
}