-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This pager exposes a an application helper(which renders a UI partial) along with an active record extension that makes it easy to filter a textual field based on lexiographical categories. Note: it technically is not a pager because there are no fixed size pages of results (and commonly there will be no items on some pages). Its logic is limited for now because it currently fulfills all functionality required. However I see it being useful in the REST API as a filter function. I also expect it could be further expanded to get totals and to also dynamically remove irrelevant pages. This was done for #322
- Loading branch information
Showing
5 changed files
with
234 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
require 'alphabetical_paginator_query' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |