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:
Cody Cutrer 2013-09-26 15:15:55 -06:00
parent df0098e54d
commit 266a89e7db
37 changed files with 249 additions and 93 deletions

View File

@ -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)

View File

@ -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}

View File

@ -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')

View File

@ -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

View File

@ -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}

View File

@ -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]
}

View File

@ -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)

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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]
]
}

View File

@ -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] = []

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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] || [])

View File

@ -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'")

View File

@ -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

View File

@ -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)

View File

@ -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'")

View File

@ -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,

View File

@ -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] + " " %>

View File

@ -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 %>

View File

@ -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 %>

View File

@ -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>

View File

@ -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 %>

View File

@ -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>

View File

@ -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;

129
lib/canvas/icu.rb Normal file
View File

@ -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

View File

@ -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)

View File

@ -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