diff --git a/app/helpers/alphabetical_paginator_helper.rb b/app/helpers/alphabetical_paginator_helper.rb new file mode 100644 index 00000000..e8f758bf --- /dev/null +++ b/app/helpers/alphabetical_paginator_helper.rb @@ -0,0 +1,69 @@ +module AlphabeticalPaginatorHelper + + # make our custom url generation methods available to views + include Api::CustomUrlHelpers + + def alphabetical_paginator(current_page = 'a', window_size = 1, index_size = 1) + fail if window_size < 1 + fail if index_size < 1 + + other = AlphabeticalPaginatorQuery::OTHER #"\u{1F30F}" + numbers = AlphabeticalPaginatorQuery::NUMBERS + start_char = 'a' + end_char = 'z' + + pages = [ + make_page(other, other, current_page), + make_page(numbers, numbers, current_page) + ] + + + current = 0 + min = 0 + max = ((end_char.ord + 1 - start_char.ord) ** index_size) - 1 + while current <= max do + next_number = current + window_size + + left = get_chars(current, 26, index_size, start_char.ord) + right = get_chars([max, next_number - 1].min, 26, index_size, start_char.ord) + + page = left + '-' + right + title = left + if window_size != 1 || index_size != 1 + title = page + end + + pages << make_page(title, page, current_page) + + current = next_number + end + + + render partial: 'shared/alphabetical_paginator', locals: { + paginator: { + pages: pages + } + } + end + + private + + def get_chars(index, delta, index_size, offset) + result = '' + (1..index_size).each do |i| + + char = (index / (delta**(index_size - i))) % delta + result += (offset + char).to_i.chr + end + + result + end + + def make_page(title, page, current_page) + { + title: title, + page: page, + current?: page == current_page + } + end +end \ No newline at end of file diff --git a/app/views/shared/_alphabetical_paginator.haml b/app/views/shared/_alphabetical_paginator.haml new file mode 100644 index 00000000..765a252e --- /dev/null +++ b/app/views/shared/_alphabetical_paginator.haml @@ -0,0 +1,11 @@ +-# locals paginator +%ul.pagination + - paginator[:pages].each do |page| + - if page[:current?] + %li.active + %a + = page[:title] + - else + %li + = link_to page[:title], page: page[:page] + diff --git a/config/initializers/active_record_extensions.rb b/config/initializers/active_record_extensions.rb new file mode 100644 index 00000000..8e8c19a8 --- /dev/null +++ b/config/initializers/active_record_extensions.rb @@ -0,0 +1 @@ +require 'alphabetical_paginator_query' \ No newline at end of file diff --git a/lib/modules/alphabetical_paginator_query.rb b/lib/modules/alphabetical_paginator_query.rb new file mode 100644 index 00000000..b7914dff --- /dev/null +++ b/lib/modules/alphabetical_paginator_query.rb @@ -0,0 +1,54 @@ +module AlphabeticalPaginatorQuery + OTHER = "\u{1F30F}" + NUMBERS = '0-9' + + extend ActiveSupport::Concern + + class_methods do + + def alphabetical_page(field, range) + range = 'a-b' if range.blank? + left, right = range.split('-') + query_type = validate_range(left, right) + + if query_type == :normal + if left == right then + q = where(["LOWER(LEFT(\"#{field.to_s}\", ?)) = ?", + left.length, + left + ]) + else + q = where(["LOWER(LEFT(\"#{field.to_s}\", ?)) >= ? AND LOWER(LEFT(\"#{field.to_s}\", ?)) <= ?", + left.length, + left, + right.length, + right + ]) + end + elsif query_type == :numbers + q = where(["\"#{field.to_s}\" ~ '^\\d+'", field]) + else + q = where(["\"#{field.to_s}\" !~ '^[a-zA-Z0-9]'", field]) + end + + + q.order(field) + end + + private + + def validate_range(left, right) + return :other if OTHER == left && right.blank? + return :numbers if left == '0' && right == '9' + + return :normal if /^[a-z]+$/ =~ left && /^[a-z]+$/ =~ right + + fail ArgumentError, 'Alphabetical paginator range invalid' + end + + end + +end + + +ActiveRecord::Base.send(:include, AlphabeticalPaginatorQuery) \ No newline at end of file diff --git a/spec/lib/modules/alphabetical_paginator_spec.rb b/spec/lib/modules/alphabetical_paginator_spec.rb new file mode 100644 index 00000000..be9c6796 --- /dev/null +++ b/spec/lib/modules/alphabetical_paginator_spec.rb @@ -0,0 +1,99 @@ +require 'rails_helper' + +describe 'alphabetical paginator activerecord extension' do + before :each do + FactoryGirl.build(:user, user_name: '汉字 user').save(validate: false) + FactoryGirl.create(:user, user_name: 'aauser') + FactoryGirl.create(:user, user_name: 'anuser') + FactoryGirl.create(:user, user_name: 'amuser') + FactoryGirl.create(:user, user_name: 'azuser') + FactoryGirl.create(:user, user_name: 'buser') + FactoryGirl.create(:user, user_name: 'zzzzzuser') + FactoryGirl.create(:user, user_name: '_user') + FactoryGirl.create(:user, user_name: '123user') + end + + it 'returns users in the other range' do + users = User.alphabetical_page(:user_name, "\u{1F30F}") + expect(users.count).to eq(2) + expect(users[0].user_name).to eq('_user') + expect(users[1].user_name).to eq('汉字 user') + end + + it 'returns users in the number range' do + users = User.alphabetical_page(:user_name, '0-9') + expect(users.count).to eq(1) + expect(users[0].user_name).to eq('123user') + end + + it 'returns users in the a-a range' do + users = User.alphabetical_page(:user_name, 'a-a') + expect(users.count).to eq(5) + expect(users[0].user_name).to eq('aauser') + expect(users[1].user_name).to eq('Admin') + expect(users[2].user_name).to eq('amuser') + expect(users[3].user_name).to eq('anuser') + expect(users[4].user_name).to eq('azuser') + end + + it 'returns users in the a-b range' do + users = User.alphabetical_page(:user_name, 'a-b') + expect(users.count).to eq(6) + expect(users[0].user_name).to eq('aauser') + expect(users[1].user_name).to eq('Admin') + expect(users[2].user_name).to eq('amuser') + expect(users[3].user_name).to eq('anuser') + expect(users[4].user_name).to eq('azuser') + expect(users[5].user_name).to eq('buser') + end + + it 'returns users in the z-z range' do + users = User.alphabetical_page(:user_name, 'z-z') + expect(users.count).to eq(1) + expect(users[0].user_name).to eq('zzzzzuser') + end + + it 'returns users in the yzz-zzz range' do + users = User.alphabetical_page(:user_name, 'yzz-zzz') + expect(users.count).to eq(1) + expect(users[0].user_name).to eq('zzzzzuser') + end + + it 'returns users in the an-am range' do + users = User.alphabetical_page(:user_name, 'am-an') + expect(users.count).to eq(2) + expect(users[0].user_name).to eq('amuser') + expect(users[1].user_name).to eq('anuser') + end + + context 'optimization for matching arguments' do + it 'uses a simpler query format for ranges with indentical left and rights' do + query = User.alphabetical_page(:user_name, 'aaa-aaa').to_sql + expect(query).to include('LOWER(LEFT("user_name", 3)) = \'aaa\'') + expect(query).to match(/WHERE..LOWER(?!.*LOWER)/) + end + end + + context 'validating arguments' do + cases = [ + '1-2', + "\u{1F30D}", + "\u{1F30F}-\u{1F30F}", + '汉-字', + 'a1-a2', + '---', + '\';-- SELECT * FROM users', + 'A-Z', + 'aA-ab', + 'aa-zZ' + ] + + cases.each do |bad_case| + it "fails to process an invalid case ('#{bad_case}')" do + expect { + User.alphabetical_page(:user_name, bad_case) + }.to raise_error(ArgumentError, 'Alphabetical paginator range invalid') + end + end + end +end