use unicode sorting for ruby and db stuff
fixes CNVS-7199, CNVS-4414 abstract some ICU stuff out to Canvas::ICU, and add some missing functionality in FFI-ICU test plan: * create a bunch of groups in the same category, named 1-10. 10 should sort after 9, not 1 * repeat for creating and publishing quizzes *with the same due date*. go to the quiz index, and 10 should sort after 9, not 1 Change-Id: I323eb12dfb5bd23dbcbb3b543fa1b90a72f4341b Reviewed-on: https://gerrit.instructure.com/24732 Reviewed-by: Cody Cutrer <cody@instructure.com> Product-Review: Cody Cutrer <cody@instructure.com> QA-Review: Cody Cutrer <cody@instructure.com> Tested-by: Jenkins <jenkins@instructure.com>
This commit is contained in:
parent
df0098e54d
commit
266a89e7db
|
@ -378,7 +378,7 @@ class AccountsController < ApplicationController
|
|||
@account.available_account_roles.each_with_index do |type, idx|
|
||||
order_hash[type] = idx
|
||||
end
|
||||
@account_users = @account_users.select(&:user).sort_by{|au| [order_hash[au.membership_type] || 999, au.user.sortable_name.downcase] }
|
||||
@account_users = @account_users.select(&:user).sort_by{|au| [order_hash[au.membership_type] || 999, Canvas::ICU.collation_key(au.user.sortable_name)] }
|
||||
@announcements = @account.announcements
|
||||
@alerts = @account.alerts
|
||||
@role_types = RoleOverride.account_membership_types(@account)
|
||||
|
|
|
@ -255,7 +255,7 @@ class AssignmentsController < ApplicationController
|
|||
active_tab = "Syllabus"
|
||||
if authorized_action(@context, @current_user, [:read, :read_syllabus])
|
||||
return unless tab_enabled?(@context.class::TAB_SYLLABUS)
|
||||
@groups = @context.assignment_groups.active.order(:position, :name).all
|
||||
@groups = @context.assignment_groups.active.order(:position, AssignmentGroup.best_unicode_collation_key('name')).all
|
||||
@assignment_groups = @groups
|
||||
@events = @context.events_for(@current_user)
|
||||
@undated_events = @events.select {|e| e.start_at == nil}
|
||||
|
|
|
@ -480,7 +480,7 @@ class CalendarEventsApiController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
@events = @events.sort_by{ |e| [(e.start_at || Time.now), e.title] }
|
||||
@events = @events.sort_by{ |e| [(e.start_at || Time.now), Canvas::ICU.collation_key(e.title)] }
|
||||
|
||||
@contexts.each do |context|
|
||||
log_asset_access("calendar_feed:#{context.asset_string}", "calendar", 'other')
|
||||
|
|
|
@ -208,10 +208,10 @@ class CommunicationChannelsController < ApplicationController
|
|||
@merge_opportunities << [user, account_to_pseudonyms_hash.map do |(account, pseudonyms)|
|
||||
pseudonyms.detect { |p| p.sis_user_id } || pseudonyms.sort { |a, b| a.position <=> b.position }.first
|
||||
end]
|
||||
@merge_opportunities.last.last.sort! { |a, b| a.account.name <=> b.account.name }
|
||||
@merge_opportunities.last.last.sort! { |a, b| Canvas::ICU.compare(a.account.name, b.account.name) }
|
||||
end
|
||||
end
|
||||
@merge_opportunities.sort! { |a, b| [a.first == @current_user ? 0 : 1, a.first.name] <=> [b.first == @current_user ? 0 : 1, b.first.name] }
|
||||
@merge_opportunities.sort! { |a, b| [a.first == @current_user ? 0 : 1, Canvas::ICU.collation_key(a.first.name)] <=> [b.first == @current_user ? 0 : 1, Canvas::ICU.collation_key(b.first.name)] }
|
||||
|
||||
js_env :PASSWORD_POLICY => @domain_root_account.password_policy
|
||||
|
||||
|
|
|
@ -158,7 +158,7 @@ class CoursesController < ApplicationController
|
|||
def index
|
||||
respond_to do |format|
|
||||
format.html {
|
||||
@current_enrollments = @current_user.cached_current_enrollments(:include_enrollment_uuid => session[:enrollment_uuid]).sort_by{|e| [e.active? ? 1 : 0, e.long_name] }
|
||||
@current_enrollments = @current_user.cached_current_enrollments(:include_enrollment_uuid => session[:enrollment_uuid]).sort_by{|e| [e.active? ? 1 : 0, Canvas::ICU.collation_key(e.long_name)] }
|
||||
@past_enrollments = @current_user.enrollments.with_each_shard{|scope| scope.past }
|
||||
@future_enrollments = @current_user.enrollments.with_each_shard{|scope| scope.future.includes(:root_account)}.reject{|e| e.root_account.settings[:restrict_student_future_view]}
|
||||
|
||||
|
@ -1146,7 +1146,7 @@ class CoursesController < ApplicationController
|
|||
when 'syllabus'
|
||||
add_crumb(t('#crumbs.syllabus', "Syllabus"))
|
||||
@syllabus_body = api_user_content(@context.syllabus_body, @context)
|
||||
@groups = @context.assignment_groups.active.order(:position, :name).all
|
||||
@groups = @context.assignment_groups.active.order(:position, AssignmentGroup.best_unicode_collation_key('name')).all
|
||||
@events = @context.calendar_events.active.to_a
|
||||
@events.concat @context.assignments.active.to_a
|
||||
@undated_events = @events.select {|e| e.start_at == nil}
|
||||
|
|
|
@ -149,7 +149,7 @@ class EnrollmentsApiController < ApplicationController
|
|||
endpoint_scope = (@context.is_a?(Course) ? (@section.present? ? "section" : "course") : "user")
|
||||
scope_arguments = {
|
||||
:conditions => enrollment_index_conditions,
|
||||
:order => 'enrollments.type ASC, users.sortable_name ASC',
|
||||
:order => "enrollments.type, #{User.sortable_name_order_by_clause("users")}",
|
||||
:include => [:user, :course, :course_section]
|
||||
}
|
||||
|
||||
|
|
|
@ -154,7 +154,7 @@ class GradebooksController < ApplicationController
|
|||
@groups = @context.assignment_groups.active
|
||||
@groups_order = {}
|
||||
@groups.each_with_index{|group, idx| @groups_order[group.id] = idx }
|
||||
@just_assignments = @context.assignments.active.gradeable.order(:due_at, :title).select{|a| @groups_order[a.assignment_group_id] }
|
||||
@just_assignments = @context.assignments.active.gradeable.order(:due_at, Assignment.best_unicode_collation_key('title')).select{|a| @groups_order[a.assignment_group_id] }
|
||||
newest = Time.parse("Jan 1 2010")
|
||||
@just_assignments = @just_assignments.sort_by{|a| [a.due_at || newest, @groups_order[a.assignment_group_id] || 0, a.position || 0] }
|
||||
@assignments = @just_assignments.dup + groups_as_assignments(@groups)
|
||||
|
|
|
@ -198,7 +198,7 @@ class GroupsController < ApplicationController
|
|||
return unless authorized_action(@context, @current_user, :read_roster)
|
||||
|
||||
@groups = all_groups = @context.groups.active.by_name
|
||||
@categories = @context.group_categories.order("role <> 'student_organized'", :name)
|
||||
@categories = @context.group_categories.order("role <> 'student_organized'", GroupCategory.best_unicode_collation_key('name'))
|
||||
@user_groups = @current_user.group_memberships_for(@context) if @current_user
|
||||
|
||||
unless api_request?
|
||||
|
|
|
@ -117,7 +117,7 @@ class OutcomesController < ApplicationController
|
|||
if authorized_action(@context, @current_user, :manage_outcomes)
|
||||
@account_contexts = @context.associated_accounts rescue []
|
||||
@current_outcomes = @context.linked_learning_outcomes
|
||||
@outcomes = @context.available_outcomes.sort_by{ |o| o.title }
|
||||
@outcomes = Canvas::ICU.collate_by(@context.available_outcomes, &:title)
|
||||
if params[:unused]
|
||||
@outcomes -= @current_outcomes
|
||||
end
|
||||
|
|
|
@ -32,7 +32,7 @@ class QuestionBanksController < ApplicationController
|
|||
@question_banks += @context.inherited_assessment_question_banks.active
|
||||
end
|
||||
@question_banks = @question_banks.select{|b| b.grants_right?(@current_user, nil, :manage) } if params[:managed] == '1'
|
||||
@question_banks = @question_banks.uniq.sort_by{|b| b.title || "zzz" }
|
||||
@question_banks = Canvas::ICU.collate_by(@question_banks.uniq) { |b| b.title || "zzz" }
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json { render :json => @question_banks.map{ |b| b.as_json(methods: [:cached_context_short_name, :assessment_question_count]) }}
|
||||
|
@ -61,7 +61,7 @@ class QuestionBanksController < ApplicationController
|
|||
|
||||
add_crumb(@bank.title)
|
||||
if authorized_action(@bank, @current_user, :read)
|
||||
@alignments = @bank.learning_outcome_alignments.sort_by{|a| a.learning_outcome.short_description.downcase }
|
||||
@alignments = Canvas::ICU.collate_by(@bank.learning_outcome_alignments) { |a| a.learning_outcome.short_description }
|
||||
@questions = @bank.assessment_questions.active.paginate(:per_page => 50, :page => 1)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,7 +34,7 @@ class QuizzesController < ApplicationController
|
|||
def index
|
||||
if authorized_action(@context, @current_user, :read)
|
||||
return unless tab_enabled?(@context.class::TAB_QUIZZES)
|
||||
@quizzes = @context.quizzes.active.include_assignment.sort_by{|q| [(q.assignment ? q.assignment.due_at : q.lock_at) || Time.parse("Jan 1 2020"), q.title || ""]}
|
||||
@quizzes = @context.quizzes.active.include_assignment.sort_by{|q| [(q.assignment ? q.assignment.due_at : q.lock_at) || Time.parse("Jan 1 2020"), Canvas::ICU.collation_key(q.title || "")]}
|
||||
|
||||
# draft state - only filter by available? for students
|
||||
if @context.draft_state_enabled?
|
||||
|
|
|
@ -26,7 +26,7 @@ class RubricsController < ApplicationController
|
|||
return unless authorized_action(@context, @current_user, :manage)
|
||||
js_env :ROOT_OUTCOME_GROUP => get_root_outcome
|
||||
@rubric_associations = @context.rubric_associations.bookmarked.include_rubric.to_a
|
||||
@rubric_associations = @rubric_associations.select(&:rubric_id).once_per(&:rubric_id).sort_by{|a| a.rubric.title }
|
||||
@rubric_associations = Canvas::ICU.collate_by(@rubric_associations.select(&:rubric_id).once_per(&:rubric_id)) { |r| r.rubric.title }
|
||||
@rubrics = @rubric_associations.map(&:rubric)
|
||||
@context.is_a?(User) ? render(:action => 'user_index') : render
|
||||
end
|
||||
|
|
|
@ -243,14 +243,14 @@ class SearchController < ApplicationController
|
|||
[
|
||||
context_state_ranks[context[:state]],
|
||||
context_type_ranks[context[:type]],
|
||||
context[:name].downcase,
|
||||
Canvas::ICU.collation_key(context[:name]),
|
||||
context[:id]
|
||||
]
|
||||
}
|
||||
else
|
||||
result.sort_by{ |context|
|
||||
[
|
||||
context[:name].downcase,
|
||||
Canvas::ICU.collation_key(context[:name]),
|
||||
context[:id]
|
||||
]
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ class SubAccountsController < ApplicationController
|
|||
|
||||
def sub_accounts_of(account, current_depth = 0)
|
||||
account_data = @accounts[account.id] = { :account => account, :course_count => 0}
|
||||
sub_accounts = account.sub_accounts.active.order(:name).limit(101) unless current_depth == 2
|
||||
sub_accounts = account.sub_accounts.active.order(Account.best_unicode_collation_key('name')).limit(101) unless current_depth == 2
|
||||
sub_account_ids = (sub_accounts || []).map(&:id)
|
||||
if current_depth == 2 || sub_accounts.length > 100
|
||||
account_data[:sub_account_ids] = []
|
||||
|
|
|
@ -145,7 +145,7 @@ class SubmissionsController < ApplicationController
|
|||
if @assignment.muted? && !@submission.grants_right?(@current_user, :read_grade)
|
||||
@visible_rubric_assessments = []
|
||||
else
|
||||
@visible_rubric_assessments = @submission.rubric_assessments.select{|a| a.grants_rights?(@current_user, session, :read)[:read]}.sort_by{|a| [a.assessment_type == 'grading' ? '0' : '1', a.assessor_name] }
|
||||
@visible_rubric_assessments = @submission.rubric_assessments.select{|a| a.grants_rights?(@current_user, session, :read)[:read]}.sort_by{|a| [a.assessment_type == 'grading' ? '0' : '1', Canvas::ICU.collation_key(a.assessor_name)] }
|
||||
end
|
||||
|
||||
@assessment_request = @submission.assessment_requests.find_by_assessor_id(@current_user.id) rescue nil
|
||||
|
|
|
@ -1548,6 +1548,6 @@ class UsersController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
data.values.sort_by { |e| e[:enrollment].user.sortable_name.downcase }
|
||||
Canvas::ICU.collate_by(data.values) { |e| e[:enrollment].user.sortable_name }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -289,15 +289,10 @@ class AppointmentGroup < ActiveRecord::Base
|
|||
else participants
|
||||
end
|
||||
|
||||
two_tier_cmp = lambda do |a, b, attr1, attr2|
|
||||
cmp = a.send(attr1) <=> b.send(attr1)
|
||||
cmp == 0 ? a.send(attr2) <=> b.send(attr2) : cmp
|
||||
end
|
||||
|
||||
if participant_type == 'User'
|
||||
participants.sort { |a,b| two_tier_cmp.call(a, b, :sortable_name, :id) }
|
||||
participants.sort_by { |p| [Canvas::ICU.collation_key(p.sortable_name), p.id] }
|
||||
else
|
||||
participants.sort { |a,b| two_tier_cmp.call(a, b, :name, :id) }
|
||||
participants.sort_by { |p| [Canvas::ICU.collation_key(p.name), p.id] }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1225,7 +1225,7 @@ class Assignment < ActiveRecord::Base
|
|||
|
||||
def visible_rubric_assessments_for(user)
|
||||
if self.rubric_association
|
||||
self.rubric_association.rubric_assessments.select{|a| a.grants_rights?(user, :read)[:read]}.sort_by{|a| [a.assessment_type == 'grading' ? '0' : '1', a.assessor_name] }
|
||||
self.rubric_association.rubric_assessments.select{|a| a.grants_rights?(user, :read)[:read]}.sort_by{|a| [a.assessment_type == 'grading' ? '0' : '1', Canvas::ICU.collation_key(a.assessor_name)] }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1768,7 +1768,7 @@ class Assignment < ActiveRecord::Base
|
|||
|
||||
def sort_key
|
||||
# undated assignments go last
|
||||
[due_at ? 0 : 1, due_at || 0, title]
|
||||
[due_at ? 0 : 1, due_at || 0, Canvas::ICU.collation_key(title)]
|
||||
end
|
||||
|
||||
def special_class; nil; end
|
||||
|
|
|
@ -54,7 +54,7 @@ class CommunicationChannel < ActiveRecord::Base
|
|||
RETIRE_THRESHOLD = 5
|
||||
|
||||
def self.sms_carriers
|
||||
@sms_carriers ||= (Setting.from_config('sms', false) ||
|
||||
@sms_carriers ||= Canvas::ICU.collate_by((Setting.from_config('sms', false) ||
|
||||
{ 'AT&T' => 'txt.att.net',
|
||||
'Alltel' => 'message.alltel.com',
|
||||
'Boost' => 'myboostmobile.com',
|
||||
|
@ -66,7 +66,7 @@ class CommunicationChannel < ActiveRecord::Base
|
|||
'Sprint PCS' => 'messaging.sprintpcs.com',
|
||||
'T-Mobile' => 'tmomail.net',
|
||||
'Verizon' => 'vtext.com',
|
||||
'Virgin Mobile' => 'vmobl.com' }).map.sort
|
||||
'Virgin Mobile' => 'vmobl.com' }), &:first)
|
||||
end
|
||||
|
||||
def pseudonym
|
||||
|
|
|
@ -117,7 +117,7 @@ module Context
|
|||
|
||||
def sorted_rubrics(user, context)
|
||||
associations = RubricAssociation.bookmarked.for_context_codes(context.asset_string).include_rubric
|
||||
associations.to_a.once_per(&:rubric_id).select{|r| r.rubric }.sort_by{|r| r.rubric.title || "zzzz" }
|
||||
Canvas::ICU.collate_by(associations.to_a.once_per(&:rubric_id).select{|r| r.rubric }) { |r| r.rubric.title || "zzzz" }
|
||||
end
|
||||
|
||||
def rubric_contexts(user)
|
||||
|
|
|
@ -431,7 +431,7 @@ class ContextExternalTool < ActiveRecord::Base
|
|||
contexts.each do |context|
|
||||
tools += context.context_external_tools.active
|
||||
end
|
||||
tools.sort_by(&:name)
|
||||
Canvas::ICU.collate_by(tools, &:name)
|
||||
end
|
||||
|
||||
# Order of precedence: Basic LTI defines precedence as first
|
||||
|
|
|
@ -698,7 +698,7 @@ class Conversation < ActiveRecord::Base
|
|||
MessageableUser.select("#{MessageableUser.build_select}, last_authored_at, conversation_id").
|
||||
joins(:all_conversations).
|
||||
where(:conversation_participants => { :conversation_id => conversations }).
|
||||
order('last_authored_at IS NULL, last_authored_at DESC, LOWER(COALESCE(short_name, name))').group_by { |mu| mu.conversation_id.to_i }
|
||||
order("last_authored_at IS NULL, last_authored_at DESC, #{Conversation.best_unicode_collation_key("COALESCE(short_name, name)")}").group_by { |mu| mu.conversation_id.to_i }
|
||||
end
|
||||
conversations.each do |conversation|
|
||||
participants[conversation.global_id].concat(user_map[conversation.id] || [])
|
||||
|
|
|
@ -444,7 +444,7 @@ class Course < ActiveRecord::Base
|
|||
scope :recently_ended, lambda { where(:conclude_at => 1.month.ago..Time.zone.now).order("start_at DESC").limit(10) }
|
||||
scope :recently_created, lambda { where("created_at>?", 1.month.ago).order("created_at DESC").limit(50).includes(:teachers) }
|
||||
scope :for_term, lambda {|term| term ? where(:enrollment_term_id => term) : scoped }
|
||||
scope :active_first, order("CASE WHEN courses.workflow_state='available' THEN 0 ELSE 1 END, name")
|
||||
scope :active_first, lambda { order("CASE WHEN courses.workflow_state='available' THEN 0 ELSE 1 END, #{best_unicode_collation_key('name')}") }
|
||||
scope :name_like, lambda { |name| where(wildcard('courses.name', 'courses.sis_source_id', 'courses.course_code', name)) }
|
||||
scope :needs_account, lambda { |account, limit| where(:account_id => nil, :root_account_id => account).limit(limit) }
|
||||
scope :active, where("courses.workflow_state<>'deleted'")
|
||||
|
|
|
@ -54,7 +54,7 @@ class GradingStandard < ActiveRecord::Base
|
|||
end
|
||||
|
||||
scope :active, where("grading_standards.workflow_state<>'deleted'")
|
||||
scope :sorted, order("usage_count >= 3 DESC, title ASC")
|
||||
scope :sorted, lambda { order("usage_count >= 3 DESC, #{best_unicode_collation_key('title')}") }
|
||||
|
||||
VERSION = 2
|
||||
|
||||
|
|
|
@ -304,7 +304,7 @@ class LearningOutcome < ActiveRecord::Base
|
|||
scope :has_result_for, lambda { |user|
|
||||
joins(:learning_outcome_results).
|
||||
where("learning_outcomes.id=learning_outcome_results.learning_outcome_id AND learning_outcome_results.user_id=?", user).
|
||||
order(:short_description)
|
||||
order(best_unicode_collation_key('short_description'))
|
||||
}
|
||||
|
||||
scope :global, where(:context_id => nil)
|
||||
|
|
|
@ -39,7 +39,7 @@ class Rubric < ActiveRecord::Base
|
|||
serialize :data
|
||||
simply_versioned
|
||||
|
||||
scope :publicly_reusable, where(:reusable => true).order(:title)
|
||||
scope :publicly_reusable, lambda { where(:reusable => true).order(best_unicode_collation_key('title')) }
|
||||
scope :matching, lambda { |search| where(wildcard('rubrics.title', search)).order("rubrics.association_count DESC") }
|
||||
scope :before, lambda { |date| where("rubrics.created_at<?", date) }
|
||||
scope :active, where("workflow_state<>'deleted'")
|
||||
|
|
|
@ -1324,7 +1324,7 @@ class User < ActiveRecord::Base
|
|||
context_codes = ([self] + self.management_contexts).uniq.map(&:asset_string)
|
||||
rubrics = self.context_rubrics.active
|
||||
rubrics += Rubric.active.find_all_by_context_code(context_codes)
|
||||
rubrics.uniq.sort_by{|r| [(r.association_count || 0) > 3 ? 'a' : 'b', (r.title.downcase rescue 'zzzzz')]}
|
||||
rubrics.uniq.sort_by{|r| [(r.association_count || 0) > 3 ? 'a' : 'b', Canvas::ICU.collation_key(r.title || 'zzzzz')]}
|
||||
end
|
||||
|
||||
def assignments_recently_graded(opts={})
|
||||
|
@ -1616,7 +1616,7 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
res.sort_by{ |c| [c.primary_enrollment_rank, c.name.downcase] }
|
||||
res.sort_by{ |c| [c.primary_enrollment_rank, Canvas::ICU.collation_key(c.name)] }
|
||||
end
|
||||
memoize :courses_with_primary_enrollment
|
||||
|
||||
|
@ -1826,7 +1826,7 @@ class User < ActiveRecord::Base
|
|||
event_codes = context_codes + AppointmentGroup.manageable_by(self, context_codes).intersecting(opts[:start_at], opts[:end_at]).map(&:asset_string)
|
||||
events += ev.for_user_and_context_codes(self, event_codes, []).between(opts[:start_at], opts[:end_at]).updated_after(opts[:updated_at])
|
||||
events += Assignment.active.for_context_codes(context_codes).due_between(opts[:start_at], opts[:end_at]).updated_after(opts[:updated_at]).with_just_calendar_attributes
|
||||
events.sort_by{|e| [e.start_at, e.title || ""] }.uniq
|
||||
events.sort_by{|e| [e.start_at, Canvas::ICU.collation_key(e.title || "")] }.uniq
|
||||
end
|
||||
|
||||
def upcoming_events(opts={})
|
||||
|
@ -1846,7 +1846,7 @@ class User < ActiveRecord::Base
|
|||
include_submitted_count.
|
||||
map {|a| a.overridden_for(self)},opts.merge(:time => now)).
|
||||
first(opts[:limit])
|
||||
events.sort_by{|e| [e.start_at ? 0: 1,e.start_at || 0, e.title] }.uniq.first(opts[:limit])
|
||||
events.sort_by{|e| [e.start_at ? 0: 1,e.start_at || 0, Canvas::ICU.collation_key(e.title)] }.uniq.first(opts[:limit])
|
||||
end
|
||||
|
||||
def select_upcoming_assignments(assignments,opts)
|
||||
|
@ -1870,7 +1870,7 @@ class User < ActiveRecord::Base
|
|||
undated_events = []
|
||||
undated_events += CalendarEvent.active.for_user_and_context_codes(self, context_codes, []).undated.updated_after(opts[:updated_at])
|
||||
undated_events += Assignment.active.for_context_codes(context_codes).undated.updated_after(opts[:updated_at]).with_just_calendar_attributes
|
||||
undated_events.sort_by{|e| e.title }
|
||||
Canvas::ICU.collate_by(undated_events, &:title)
|
||||
end
|
||||
|
||||
def setup_context_lookups(contexts=nil)
|
||||
|
@ -2191,7 +2191,7 @@ class User < ActiveRecord::Base
|
|||
contexts += Group.where(:id => info.common_groups.keys).all if info.common_groups.present?
|
||||
end
|
||||
end
|
||||
contexts.map(&:name).sort_by{|c|c.downcase}
|
||||
Canvas::ICU.collate(contexts.map(&:name))
|
||||
end
|
||||
|
||||
def mark_all_conversations_as_read!
|
||||
|
@ -2227,7 +2227,7 @@ class User < ActiveRecord::Base
|
|||
if !e.course
|
||||
coalesced_enrollments << {
|
||||
:enrollment => e,
|
||||
:sortable => [e.rank_sortable, e.state_sortable, e.long_name],
|
||||
:sortable => [e.rank_sortable, e.state_sortable, Canvas::ICU.collation_key(e.long_name)],
|
||||
:types => [ e.readable_type ]
|
||||
}
|
||||
end
|
||||
|
@ -2248,9 +2248,8 @@ class User < ActiveRecord::Base
|
|||
active_enrollments = coalesced_enrollments.map{ |e| e[:enrollment] }
|
||||
|
||||
cached_group_memberships = self.cached_current_group_memberships
|
||||
coalesced_group_memberships = cached_group_memberships.
|
||||
select{ |gm| gm.active_given_enrollments?(active_enrollments) }.
|
||||
sort_by{ |gm| gm.group.name }
|
||||
coalesced_group_memberships = Canvas::ICU.collate_by(cached_group_memberships.
|
||||
select{ |gm| gm.active_given_enrollments?(active_enrollments) }) { |gm| gm.group.name }
|
||||
|
||||
@menu_data = {
|
||||
:group_memberships => coalesced_group_memberships,
|
||||
|
|
|
@ -302,7 +302,7 @@ Allowing Draft State here will allow teachers to enable it in their individual c
|
|||
<% end %>
|
||||
<% end %>
|
||||
<% f.fields_for :services do |services| %>
|
||||
<% Account.services_exposed_to_ui_hash(:setting, @current_user, @account).sort_by { |k,h| h[:name] }.each do |key, service| %>
|
||||
<% Account.services_exposed_to_ui_hash(:setting, @current_user, @account).sort_by { |k,h| Canvas::ICU.collation_key(h[:name]) }.each do |key, service| %>
|
||||
<div>
|
||||
<%= services.check_box key, :checked => @account.service_enabled?(key) %>
|
||||
<%= services.label key, service[:name] + " " %>
|
||||
|
@ -445,7 +445,7 @@ Allowing Draft State here will allow teachers to enable it in their individual c
|
|||
<fieldset>
|
||||
<legend><%= t(:enabled_web_serices_title, "Enabled Web Services") %></legend>
|
||||
<% f.fields_for :services do |services| %>
|
||||
<% exposed_services.sort_by { |k,h| h[:name] }.each do |key, service| %>
|
||||
<% exposed_services.sort_by { |k,h| Canvas::ICU.collation_key(h[:name]) }.each do |key, service| %>
|
||||
<div>
|
||||
<%= services.check_box key, :checked => @account.service_enabled?(key) %>
|
||||
<%= services.label key, service[:name] + " " %>
|
||||
|
|
|
@ -168,7 +168,7 @@ require([
|
|||
<ul class="unstyled_list peer_reviews" style="padding-left: 30px; font-size: 0.8em; width: 50%;">
|
||||
<li class="peer_review no_requests_message" style="<%= hidden unless !submission || submission.assigned_assessments.empty? %>"><%= t :none_assigned, "None Assigned" %></li>
|
||||
<% if submission %>
|
||||
<% submission.assigned_assessments.sort_by{|r| r.asset.user.last_name_first}.each do |request| %>
|
||||
<% Canvas::ICU.collate_by(submission.assigned_assessments) {|r| r.asset.user.sortable_name }.each do |request| %>
|
||||
<% if (request && request.asset && request.asset.user) %>
|
||||
<%= render :partial => 'peer_review_assignment', :object => request %>
|
||||
<% end %>
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
<span class="hidden_name nobr" style="display: none;"><%= t(:student, "Student") %></span>
|
||||
<%
|
||||
es = @enrollments_hash[student.id] if @context.course_sections.active.count > 1 && @enrollments_hash && @enrollments_hash[student.id]
|
||||
sections = es.map{ |e| e.course_section }.compact.sort_by{ |s| s.display_name } if es
|
||||
sections = Canvas::ICU.collate_by(es.map{ |e| e.course_section }.compact, &:display_name) if es
|
||||
%>
|
||||
<div class="secondary_identifier <%= 'with_section' if es %>" title="<%= student.secondary_identifier %>"><%= student.secondary_identifier %></div>
|
||||
<% if es %>
|
||||
<% section_names = sections.map{ |s| s.display_name}.to_sentence %>
|
||||
<% sections.each do |section| %>
|
||||
<div class="course_section" data-course_section_id="<%= section.id %>" title="<%= section.display_name %>"><%= section.display_name %></div>
|
||||
<% end %>
|
||||
|
|
|
@ -147,9 +147,9 @@
|
|||
<div style="margin-top: 5px;">
|
||||
<select class="module_item_select" multiple>
|
||||
<% includes = @context.grants_right?(@current_user, session, :manage_files) ? :active_file_attachments : :visible_file_attachments %>
|
||||
<% @context.folders.active.limit(200).includes(includes).sort_by{|f| f.full_name}.each do |folder| %>
|
||||
<% Canvas::ICU.collate_by(@context.folders.active.limit(200).includes(includes), &:full_name).each do |folder| %>
|
||||
<optgroup label="<%= folder.full_name %>">
|
||||
<% folder.send(includes).sort_by{|file| file.display_name }.each do |file| %>
|
||||
<% Canvas::ICU.collate_by(folder.send(includes), &:display_name).each do |file| %>
|
||||
<option value="<%= file.id %>"><%= file.display_name %></option>
|
||||
<% end %>
|
||||
</optgroup>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<th><%= t('emails', 'Emails') %></th>
|
||||
<td><%= t('no_emails', 'no emails') %></td>
|
||||
<td>
|
||||
<% emails = (user.communication_channels.unretired.email + other_user.communication_channels.unretired.email).map(&:path).uniq.sort %>
|
||||
<% emails = Canvas::ICU.collate((user.communication_channels.unretired.email + other_user.communication_channels.unretired.email).map(&:path).uniq) %>
|
||||
<% if emails.empty? %>
|
||||
<%= t('no_emails', 'no emails') %>
|
||||
<% else %>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<th><%= t('logins', 'Logins') %></th>
|
||||
<td><%= t('no_logins', 'no logins') %></td>
|
||||
<td>
|
||||
<% pseudonyms = (user.pseudonyms.active + other_user.pseudonyms.active).sort_by(&:unique_id) %>
|
||||
<% pseudonyms = Canvas::ICU.collate_by(user.pseudonyms.active + other_user.pseudonyms.active, &:unique_id) %>
|
||||
<% if pseudonyms.empty? %>
|
||||
<%= t('no_logins', 'no logins') %>
|
||||
<% else %>
|
||||
|
@ -51,7 +51,7 @@
|
|||
<th><%= t('enrollments', 'Enrollments') %></th>
|
||||
<td><%= t('no_enrollments', 'no enrollments') %></td>
|
||||
<td>
|
||||
<% enrollments = (user.current_enrollments + other_user.current_enrollments).sort_by{|e| [e.state_sortable, e.rank_sortable, e.course.name] } %>
|
||||
<% enrollments = (user.current_enrollments + other_user.current_enrollments).sort_by{|e| [e.state_sortable, e.rank_sortable, Canvas::ICU.collation_key(e.course.name)] } %>
|
||||
<% if enrollments.empty? %>
|
||||
<%= t('no_enrollments', 'no enrollments') %>
|
||||
<% else %>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
</div>
|
||||
<% end %>
|
||||
<div>
|
||||
<% pages = @context.wiki.wiki_pages.not_deleted.select{|p| !p.new_record? }.sort_by{|p| p.title || ""} %>
|
||||
<% pages = Canvas::ICU.collate_by(@context.wiki.wiki_pages.not_deleted.select{|p| !p.new_record? }){|p| p.title || ""} %>
|
||||
<% if pages.length > 10 %>
|
||||
<h2><%= t 'headers.common_pages', 'Common Pages' %></h2>
|
||||
<div class="rs-margin-lr rs-margin-bottom">
|
||||
|
@ -51,13 +51,13 @@
|
|||
<% if pages.length < 8 %>
|
||||
<ul class="item_list limit_height">
|
||||
<%= render :partial => 'wiki_pages/page_link' %>
|
||||
<%= render :partial => 'wiki_pages/page_link', :collection => pages.sort_by{|p| p.title }, :locals => {:skip_front_page => true} %>
|
||||
<%= render :partial => 'wiki_pages/page_link', :collection => Canvas::ICU.collate_by(pages, &:title), :locals => {:skip_front_page => true} %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<ul class="item_list limit_height">
|
||||
<li><a href="#" class="more_pages_link" style="padding-left: 20px; font-size: 0.9em;"><%= t 'links.show_all', 'show all...' %></a></li>
|
||||
<%= render :partial => 'wiki_pages/page_link', :locals => {:hidden => true} %>
|
||||
<%= render :partial => 'wiki_pages/page_link', :collection => pages.sort_by{|p| p.title }, :locals => {:skip_front_page => true, :hidden => true} %>
|
||||
<%= render :partial => 'wiki_pages/page_link', :collection => Canvas::ICU.collate_by(pages, &:title), :locals => {:skip_front_page => true, :hidden => true} %>
|
||||
</ul>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -341,28 +341,6 @@ class ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def self.init_icu
|
||||
return if defined?(@icu)
|
||||
begin
|
||||
Bundler.require 'icu'
|
||||
if !ICU::Lib.respond_to?(:ucol_getRules)
|
||||
suffix = ICU::Lib.figure_suffix(ICU::Lib.version.to_s)
|
||||
ICU::Lib.attach_function(:ucol_getRules, "ucol_getRules#{suffix}", [:pointer, :pointer], :pointer)
|
||||
ICU::Collation::Collator.class_eval do
|
||||
def rules
|
||||
length = FFI::MemoryPointer.new(:int)
|
||||
ptr = ICU::Lib.ucol_getRules(@c, length)
|
||||
ptr.read_array_of_uint16(length.read_int).pack("U*")
|
||||
end
|
||||
end
|
||||
end
|
||||
@icu = true
|
||||
@collation_local_map = {}
|
||||
rescue LoadError
|
||||
@icu = false
|
||||
end
|
||||
end
|
||||
|
||||
def self.best_unicode_collation_key(col)
|
||||
if ActiveRecord::Base.configurations[Rails.env]['adapter'] == 'postgresql'
|
||||
# For PostgreSQL, we can't trust a simple LOWER(column), with any collation, since
|
||||
|
@ -375,15 +353,7 @@ class ActiveRecord::Base
|
|||
if @collkey == 0
|
||||
"CAST(LOWER(replace(#{col}, '\\', '\\\\')) AS bytea)"
|
||||
else
|
||||
locale = 'root'
|
||||
init_icu
|
||||
if @icu
|
||||
# only use the actual locale if it differs from root; using a different locale means we
|
||||
# can't use our index, which usually doesn't matter, but sometimes is very important
|
||||
locale = @collation_local_map[I18n.locale] ||= ICU::Collation::Collator.new(I18n.locale.to_s).rules.empty? ? 'root' : I18n.locale
|
||||
end
|
||||
|
||||
"collkey(#{col}, '#{locale}', true, 2, true)"
|
||||
"collkey(#{col}, '#{Canvas::ICU.locale_for_collation}', true, 2, true)"
|
||||
end
|
||||
else
|
||||
# Not yet optimized for other dbs (MySQL's default collation is case insensitive;
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
module Canvas::ICU
|
||||
begin
|
||||
Bundler.require 'icu'
|
||||
if !ICU::Lib.respond_to?(:ucol_getRules)
|
||||
suffix = ICU::Lib.figure_suffix(ICU::Lib.version.to_s)
|
||||
|
||||
ICU::Lib.attach_function(:ucol_getRules, "ucol_getRules#{suffix}", [:pointer, :pointer], :pointer)
|
||||
ICU::Lib.attach_function(:ucol_getSortKey, "ucol_getSortKey#{suffix}", [:pointer, :pointer, :int, :pointer, :int], :int)
|
||||
ICU::Lib.attach_function(:ucol_getAttribute, "ucol_getAttribute#{suffix}", [:pointer, :int, :pointer], :int)
|
||||
ICU::Lib.attach_function(:ucol_setAttribute, "ucol_setAttribute#{suffix}", [:pointer, :int, :int, :pointer], :void)
|
||||
|
||||
ICU::Collation::Collator.class_eval do
|
||||
def rules
|
||||
@rules ||= begin
|
||||
length = FFI::MemoryPointer.new(:int)
|
||||
ptr = ICU::Lib.ucol_getRules(@c, length)
|
||||
ptr.read_array_of_uint16(length.read_int).pack("U*")
|
||||
end
|
||||
end
|
||||
|
||||
def collation_key(string)
|
||||
ptr = ICU::UCharPointer.from_string(string)
|
||||
size = ICU::Lib.ucol_getSortKey(@c, ptr, string.jlength, nil, 0)
|
||||
buffer = FFI::MemoryPointer.new(:char, size)
|
||||
ICU::Lib.ucol_getSortKey(@c, ptr, string.jlength, buffer, size)
|
||||
buffer.read_bytes(size - 1)
|
||||
end
|
||||
|
||||
def [](attribute)
|
||||
ATTRIBUTE_VALUES_INVERSE[ICU::Lib.check_error do |error|
|
||||
ICU::Lib.ucol_getAttribute(@c, ATTRIBUTES[attribute], error)
|
||||
end]
|
||||
end
|
||||
|
||||
def []=(attribute, value)
|
||||
ICU::Lib.check_error do |error|
|
||||
ICU::Lib.ucol_setAttribute(@c, ATTRIBUTES[attribute], ATTRIBUTE_VALUES[value], error)
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
ATTRIBUTES = {
|
||||
french_collation: 0,
|
||||
alternate_handling: 1,
|
||||
case_first: 2,
|
||||
case_level: 3,
|
||||
normalization_mode: 4,
|
||||
strength: 5,
|
||||
hiragana_quaternary_mode: 6,
|
||||
numeric_collation: 7,
|
||||
}.freeze
|
||||
|
||||
ATTRIBUTES.each_key do |attribute|
|
||||
class_eval <<-CODE
|
||||
def #{attribute}
|
||||
self[:#{attribute}]
|
||||
end
|
||||
|
||||
def #{attribute}=(value)
|
||||
self[:#{attribute}] = value
|
||||
end
|
||||
CODE
|
||||
end
|
||||
|
||||
ATTRIBUTE_VALUES = {
|
||||
nil => -1,
|
||||
primary: 0,
|
||||
secondary: 1,
|
||||
default_strength: 2,
|
||||
tertiary: 2,
|
||||
quaternary: 3,
|
||||
identical: 15,
|
||||
|
||||
false => 16,
|
||||
true => 17,
|
||||
|
||||
shifted: 20,
|
||||
non_ignorable: 21,
|
||||
|
||||
lower_first: 24,
|
||||
upper_first: 25,
|
||||
}.freeze
|
||||
ATTRIBUTE_VALUES_INVERSE = Hash[ATTRIBUTE_VALUES.map {|k,v| [v, k]}].freeze
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def self.collator
|
||||
@collations ||= {}
|
||||
@collations[I18n.locale] ||= begin
|
||||
collator = ICU::Collation::Collator.new(I18n.locale.to_s)
|
||||
collator.normalization_mode = true
|
||||
collator.numeric_collation = true
|
||||
collator.alternate_handling = :shifted
|
||||
collator
|
||||
end
|
||||
end
|
||||
|
||||
def self.locale_for_collation
|
||||
collator.rules.empty? ? 'root' : I18n.locale
|
||||
end
|
||||
|
||||
class << self
|
||||
delegate :collate, :compare, :collation_key, to: :collator
|
||||
end
|
||||
|
||||
rescue LoadError
|
||||
|
||||
def self.locale_for_collation
|
||||
'root'
|
||||
end
|
||||
|
||||
def self.collate(sortable)
|
||||
sortable.sort { |a, b| compare(a, b) }
|
||||
end
|
||||
|
||||
def self.compare(a, b)
|
||||
collation_key(a) <=> collation_key(b)
|
||||
end
|
||||
|
||||
def self.collation_key(string)
|
||||
string.downcase
|
||||
end
|
||||
end
|
||||
|
||||
def self.collate_by(sortable)
|
||||
sortable.sort { |a, b| compare(yield(a), yield(b)) }
|
||||
end
|
||||
end
|
|
@ -300,7 +300,7 @@ module Canvas::Migration::Helpers
|
|||
end
|
||||
|
||||
def course_attachments_data(content_list, source_course)
|
||||
source_course.folders.active.select('id, full_name, name').includes(:active_file_attachments).sort_by{|f| f.full_name}.each do |folder|
|
||||
Canvas::ICU.collate_by(source_course.folders.active.select('id, full_name, name').includes(:active_file_attachments), &:full_name).each do |folder|
|
||||
next if folder.active_file_attachments.length == 0
|
||||
|
||||
item = course_item_hash('folders', folder)
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
#
|
||||
# Copyright (C) 2011 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/>.
|
||||
#
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
|
||||
|
||||
describe Canvas::ICU do
|
||||
shared_examples_for "Collator" do
|
||||
describe ".collate_by" do
|
||||
it "should work" do
|
||||
array = [{id: 2, str: 'a'}, {id:1, str: 'b'}]
|
||||
result = Canvas::ICU.collate_by(array) { |x| x[:str] }
|
||||
result.first[:id].should == 2
|
||||
result.last[:id].should == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe ".collation_key" do
|
||||
it "should return something that's comparable" do
|
||||
a = "a"; b = "b"
|
||||
a_prime = Canvas::ICU.collation_key(a)
|
||||
a.object_id.should_not == a_prime.object_id
|
||||
b_prime = Canvas::ICU.collation_key(b)
|
||||
(a_prime <=> b_prime).should == -1
|
||||
end
|
||||
end
|
||||
|
||||
describe ".compare" do
|
||||
it "should work" do
|
||||
Canvas::ICU.compare("a", "b").should == -1
|
||||
end
|
||||
end
|
||||
|
||||
describe ".collate" do
|
||||
it "should work" do
|
||||
Canvas::ICU.collate(["b", "a"]).should == ["a", "b"]
|
||||
end
|
||||
|
||||
it "should at the least be case insensitive" do
|
||||
results = Canvas::ICU.collate(["b", "a", "A", "B"])
|
||||
results[0..1].sort.should == ["a", "A"].sort
|
||||
results[2..3].sort.should == ["b", "B"].sort
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "default" do
|
||||
it_should_behave_like "Collator"
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue