├── .gitignore ├── .rspec ├── .rvmrc ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.rdoc ├── Rakefile ├── TODO ├── data_table.gemspec ├── lib ├── data_table.rb ├── data_table │ ├── active_record.rb │ ├── base.rb │ ├── mongoid.rb │ ├── rails.rb │ └── version.rb └── mongoid │ └── data_table.rb └── spec ├── active_record_data_table_spec.rb ├── data_table_spec.rb ├── mongoid_data_table_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | *.gem 3 | .bundle 4 | *.swp 5 | bin 6 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use 1.9.3@data_table 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | data_table (0.4.15) 5 | rails (>= 3.0) 6 | 7 | GEM 8 | remote: http://rubygems.org/ 9 | specs: 10 | actioncable (5.2.1) 11 | actionpack (= 5.2.1) 12 | nio4r (~> 2.0) 13 | websocket-driver (>= 0.6.1) 14 | actionmailer (5.2.1) 15 | actionpack (= 5.2.1) 16 | actionview (= 5.2.1) 17 | activejob (= 5.2.1) 18 | mail (~> 2.5, >= 2.5.4) 19 | rails-dom-testing (~> 2.0) 20 | actionpack (5.2.1) 21 | actionview (= 5.2.1) 22 | activesupport (= 5.2.1) 23 | rack (~> 2.0) 24 | rack-test (>= 0.6.3) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 27 | actionview (5.2.1) 28 | activesupport (= 5.2.1) 29 | builder (~> 3.1) 30 | erubi (~> 1.4) 31 | rails-dom-testing (~> 2.0) 32 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 33 | activejob (5.2.1) 34 | activesupport (= 5.2.1) 35 | globalid (>= 0.3.6) 36 | activemodel (5.2.1) 37 | activesupport (= 5.2.1) 38 | activerecord (5.2.1) 39 | activemodel (= 5.2.1) 40 | activesupport (= 5.2.1) 41 | arel (>= 9.0) 42 | activestorage (5.2.1) 43 | actionpack (= 5.2.1) 44 | activerecord (= 5.2.1) 45 | marcel (~> 0.3.1) 46 | activesupport (5.2.1) 47 | concurrent-ruby (~> 1.0, >= 1.0.2) 48 | i18n (>= 0.7, < 2) 49 | minitest (~> 5.1) 50 | tzinfo (~> 1.1) 51 | arel (9.0.0) 52 | builder (3.2.3) 53 | concurrent-ruby (1.0.5) 54 | crass (1.0.4) 55 | diff-lcs (1.2.4) 56 | erubi (1.7.1) 57 | globalid (0.4.1) 58 | activesupport (>= 4.2.0) 59 | i18n (1.1.1) 60 | concurrent-ruby (~> 1.0) 61 | loofah (2.2.3) 62 | crass (~> 1.0.2) 63 | nokogiri (>= 1.5.9) 64 | mail (2.7.1) 65 | mini_mime (>= 0.1.1) 66 | marcel (0.3.3) 67 | mimemagic (~> 0.3.2) 68 | method_source (0.9.1) 69 | mimemagic (0.3.2) 70 | mini_mime (1.0.1) 71 | mini_portile2 (2.3.0) 72 | minitest (5.11.3) 73 | nio4r (2.3.1) 74 | nokogiri (1.8.5) 75 | mini_portile2 (~> 2.3.0) 76 | rack (2.0.5) 77 | rack-test (1.1.0) 78 | rack (>= 1.0, < 3) 79 | rails (5.2.1) 80 | actioncable (= 5.2.1) 81 | actionmailer (= 5.2.1) 82 | actionpack (= 5.2.1) 83 | actionview (= 5.2.1) 84 | activejob (= 5.2.1) 85 | activemodel (= 5.2.1) 86 | activerecord (= 5.2.1) 87 | activestorage (= 5.2.1) 88 | activesupport (= 5.2.1) 89 | bundler (>= 1.3.0) 90 | railties (= 5.2.1) 91 | sprockets-rails (>= 2.0.0) 92 | rails-dom-testing (2.0.3) 93 | activesupport (>= 4.2.0) 94 | nokogiri (>= 1.6) 95 | rails-html-sanitizer (1.0.4) 96 | loofah (~> 2.2, >= 2.2.2) 97 | railties (5.2.1) 98 | actionpack (= 5.2.1) 99 | activesupport (= 5.2.1) 100 | method_source 101 | rake (>= 0.8.7) 102 | thor (>= 0.19.0, < 2.0) 103 | rake (12.3.1) 104 | rr (1.1.2) 105 | rspec (2.14.1) 106 | rspec-core (~> 2.14.0) 107 | rspec-expectations (~> 2.14.0) 108 | rspec-mocks (~> 2.14.0) 109 | rspec-core (2.14.5) 110 | rspec-expectations (2.14.2) 111 | diff-lcs (>= 1.1.3, < 2.0) 112 | rspec-mocks (2.14.3) 113 | shoulda (2.11.3) 114 | sprockets (3.7.2) 115 | concurrent-ruby (~> 1.0) 116 | rack (> 1, < 3) 117 | sprockets-rails (3.2.1) 118 | actionpack (>= 4.0) 119 | activesupport (>= 4.0) 120 | sprockets (>= 3.0.0) 121 | thor (0.20.0) 122 | thread_safe (0.3.6) 123 | tzinfo (1.2.5) 124 | thread_safe (~> 0.1) 125 | websocket-driver (0.7.0) 126 | websocket-extensions (>= 0.1.0) 127 | websocket-extensions (0.1.3) 128 | 129 | PLATFORMS 130 | ruby 131 | 132 | DEPENDENCIES 133 | data_table! 134 | rr (~> 1.0) 135 | rspec (~> 2.0) 136 | shoulda (~> 2.11) 137 | 138 | BUNDLED WITH 139 | 1.16.1 140 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Jason Dew 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Data Table 2 | 3 | Makes it easy to ship data to a jQuery DataTable from ActiveRecord or Mongoid. 4 | 5 | == Quick example: 6 | 7 | in your javascript: 8 | 9 | $(".providers-data-table").dataTable({"bJQueryUI" : true, 10 | "bProcessing" : true, 11 | "bAutoWidth" : false, 12 | "sPaginationType" : "full_numbers", 13 | "aoColumns" : [{"sType" : "html"}, null, null, null, null], 14 | "aaSorting" : [[0, 'asc'], [1, 'asc']], 15 | "bServerSide" : true, 16 | "sAjaxSource" : "/providers.json" }).fnSetFilteringDelay(); 17 | 18 | Note: the fnSetFilteringDelay() call isn't required but highly recommended: http://datatables.net/plug-ins/api#fnSetFilteringDelay 19 | 20 | in your controller: 21 | 22 | class ProvidersController < ApplicationController 23 | 24 | def index 25 | respond_to do |wants| 26 | wants.html 27 | wants.json do 28 | render(:json => Provider.for_data_table(self, %w(name fein categories.name county state), %w(name fein)) do |provider| 29 | ["<%= link_to(provider, provider) %>", provider.fein, provider.category.name, provider.county, provider.state] 30 | end) 31 | end 32 | end 33 | end 34 | 35 | end 36 | 37 | in your view (assuming HAML): 38 | 39 | %table.providers-data-table 40 | %thead 41 | %tr 42 | %th Name 43 | %th FEIN 44 | %th Category 45 | %th County 46 | %th State 47 | 48 | %tbody 49 | 50 | and in your Gemfile 51 | 52 | # for ActiveRecord 53 | gem "will_paginate" 54 | 55 | # for Mongoid 56 | gem "kaminari" 57 | 58 | If using *Mongoid* add this to your model 59 | 60 | include Mongoid::DataTable 61 | 62 | 63 | == Advanced Features 64 | 65 | === Date fields 66 | 67 | In order to handle date fields properly, enclose the field name in an array along with a hash like so: 68 | 69 | Provider.for_data_table(self, %w(name fein opened_on), ["name", "fein", ["opened_on", {:date => true}]]) { ... } 70 | 71 | === Split fields 72 | 73 | To handle split fields, that is to handle searching for "x-y", add a hash with a split key: 74 | 75 | Provider.for_data_table(self, %w(name fein suffix), ["name", ["fein", "suffix", {:split => "-"}]]) { ... } 76 | 77 | == Copyright 78 | 79 | Copyright (c) 2010-2011 Jason Dew. See LICENSE for details. 80 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler" 2 | 3 | Bundler::GemHelper.install_tasks 4 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - repackage as a Rails 3 responder 2 | -------------------------------------------------------------------------------- /data_table.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | require "data_table/version" 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "data_table" 6 | s.version = DataTable::VERSION 7 | s.platform = Gem::Platform::RUBY 8 | s.authors = ["Jason Dew"] 9 | s.email = ["jason.dew@gmail.com"] 10 | s.homepage = "https://github.com/jasondew/data_table" 11 | s.summary = %q{Simple data preparation from AR/Mongoid to the jQuery DataTables plugin} 12 | s.description = %q{Simple data preparation from AR/Mongoid to the jQuery DataTables plugin} 13 | s.license = "MIT" 14 | 15 | s.rubyforge_project = "data_table" 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 20 | s.require_paths = ["lib"] 21 | 22 | s.add_dependency "rails", ">=3.0" 23 | 24 | s.add_development_dependency "rspec", "~>2.0" 25 | s.add_development_dependency "shoulda", "~>2.11" 26 | s.add_development_dependency "rr", "~>1.0" 27 | end 28 | -------------------------------------------------------------------------------- /lib/data_table.rb: -------------------------------------------------------------------------------- 1 | require "rails" 2 | require "active_support/core_ext/object/blank" 3 | 4 | begin 5 | require "active_support/core_ext/object/json" 6 | rescue LoadError 7 | require "active_support/core_ext/object/to_json" rescue LoadError 8 | end 9 | 10 | require "active_support/json/encoding" 11 | require "active_support/core_ext/string/output_safety" 12 | require "active_support/core_ext/string/inflections" 13 | 14 | require "data_table/base" 15 | require "data_table/active_record" 16 | require "data_table/mongoid" 17 | require "data_table/rails" 18 | require "mongoid/data_table" 19 | -------------------------------------------------------------------------------- /lib/data_table/active_record.rb: -------------------------------------------------------------------------------- 1 | module DataTable 2 | module ActiveRecord 3 | module ClassMethods 4 | 5 | def _find_objects params, fields, search_fields 6 | self.where(_where_conditions params[:ssearch], search_fields). 7 | includes(_discover_joins fields).tap do |query| 8 | if query.respond_to?(:references!) 9 | query.references!(_discover_joins fields) 10 | end 11 | end. 12 | order(_order_fields params, fields). 13 | paginate :page => _page(params), :per_page => _per_page(params) 14 | end 15 | 16 | def _discover_joins fields 17 | joins = Set.new 18 | object = self.new 19 | 20 | fields.each { |it| 21 | field = it.split('.') 22 | 23 | if (field.size == 2) then 24 | next if object.class.name.downcase == field[0].singularize 25 | 26 | if object.respond_to?(field[0].to_sym) 27 | joins.add field[0].to_sym 28 | elsif object.respond_to?(field[0].singularize.to_sym) 29 | joins.add field[0].singularize.to_sym 30 | end 31 | end 32 | } 33 | 34 | joins.to_a 35 | end 36 | 37 | def _where_conditions query, search_fields, join_operator = "OR" 38 | return if query.blank? 39 | 40 | all_conditions = [] 41 | all_parameters = [] 42 | 43 | query.split.each do |term| 44 | conditions = [] 45 | parameters = [] 46 | 47 | search_fields.each do |field| 48 | next if (clause = _where_condition(term, field.dup)).empty? 49 | conditions << clause.shift 50 | parameters += clause 51 | end 52 | 53 | all_conditions << conditions 54 | all_parameters << parameters 55 | end 56 | 57 | [all_conditions.map {|conditions| "(" + conditions.join(" #{join_operator} ") + ")" }.join(" AND "), *all_parameters.flatten] 58 | end 59 | 60 | def _where_condition query, field 61 | return [] if query.blank? 62 | 63 | if field.is_a? Array 64 | options = field.extract_options! 65 | 66 | if options[:split] 67 | _split_where_condition query, field, options 68 | elsif options[:date] 69 | _date_where_condition query, field.first 70 | else 71 | _where_conditions(query, field, "AND") 72 | end 73 | else 74 | ["UPPER(#{field}) LIKE ?", "%#{query.upcase}%"] 75 | end 76 | end 77 | 78 | def _date_where_condition query, field 79 | begin 80 | ["#{field} = ?", Date.parse(query)] 81 | rescue ArgumentError 82 | [] 83 | end 84 | end 85 | 86 | def _split_where_condition query, fields, options 87 | conditions = [] 88 | parameters = [] 89 | split_query = query.split options[:split] 90 | types = options[:types] || ([:string] * fields.size) 91 | 92 | if split_query.size == fields.size 93 | fields.zip(split_query).zip(types).each do |((field, query), type)| 94 | if type == :numeric 95 | conditions << "#{field} = ?" 96 | parameters << query.to_i 97 | else 98 | conditions << "UPPER(#{field}) LIKE ?" 99 | parameters << "%#{query.upcase}%" 100 | end 101 | end 102 | 103 | ["(" + conditions.join(" AND ") + ")", *parameters] 104 | else 105 | [] 106 | end 107 | end 108 | 109 | def _order_fields params, fields 110 | direction = params[:ssortdir_0] == "asc" ? "ASC" : "DESC" 111 | %{#{fields[params[:isortcol_0].to_i]} #{direction}} 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/data_table/base.rb: -------------------------------------------------------------------------------- 1 | module DataTable 2 | 3 | def self.included base 4 | base.send :extend, ClassMethods 5 | base.send :extend, Mongoid::ClassMethods 6 | end 7 | 8 | module ClassMethods 9 | def for_data_table controller, fields, search_fields=nil, explicit_block=nil, &implicit_block 10 | incoming_params = if controller.params.respond_to?(:permit) then 11 | controller.params.permit(:sSearch, :sEcho, :iDisplayStart, :iDisplayLength, :iSortCol_0, :sSortDir_0) 12 | else 13 | controller.params 14 | end 15 | params = Hash[*incoming_params.to_h.map {|key, value| [key.to_s.downcase.to_sym, value] }.flatten] 16 | search_fields ||= fields 17 | block = (explicit_block or implicit_block) 18 | 19 | objects = _find_objects params, fields, search_fields 20 | matching_count = objects.respond_to?(:total_entries) ? objects.total_entries : _matching_count(params, search_fields) 21 | 22 | {:sEcho => params[:secho].to_i, 23 | :iTotalRecords => self.count, 24 | :iTotalDisplayRecords => matching_count, 25 | :aaData => _yield_and_render_array(controller, objects, block) 26 | }.to_json.html_safe 27 | end 28 | 29 | private 30 | 31 | def _yield_and_render_array controller, objects, block 32 | objects.map do |object| 33 | block[object].map do |string| 34 | safe_string = string.to_s.gsub("|", " ") 35 | controller.instance_eval %{ 36 | log_level, Rails.logger.level = Rails.logger.level, Logger::ERROR 37 | render_to_string(:inline => %q|#{safe_string}|, :locals => {:#{self.name.underscore} => object}).tap do 38 | Rails.logger.level = log_level 39 | end 40 | } 41 | end 42 | end 43 | end 44 | 45 | def _page params 46 | return 1 if params[:idisplaystart].blank? or params[:idisplaylength].blank? 47 | 48 | params[:idisplaystart].to_i / params[:idisplaylength].to_i + 1 49 | end 50 | 51 | def _per_page params 52 | case (display_length = params[:idisplaylength].to_i) 53 | when -1 then self.count 54 | when 0 then 25 55 | else display_length 56 | end 57 | end 58 | end 59 | 60 | end 61 | -------------------------------------------------------------------------------- /lib/data_table/mongoid.rb: -------------------------------------------------------------------------------- 1 | module DataTable 2 | module Mongoid 3 | module ClassMethods 4 | def _find_objects params, fields, search_fields 5 | self.where(_where_conditions params[:ssearch], search_fields). 6 | order_by(_order_by_fields params, fields). 7 | page(_page params). 8 | per(_per_page params) 9 | end 10 | 11 | def _matching_count params, search_fields 12 | self.where(_where_conditions params[:ssearch], search_fields).count 13 | end 14 | 15 | def _where_conditions raw_query, search_fields 16 | query = _sanitize raw_query 17 | return if (query = _sanitize raw_query).blank? 18 | 19 | if search_fields.size == 1 20 | terms = query.split(/\s+/) 21 | 22 | if terms.size == 1 23 | {search_fields.first => /#{terms.first}/i} 24 | else 25 | {search_fields.first => {"$all" => terms.map {|term| /#{term}/i }}} 26 | end 27 | else 28 | terms = query.split(/\s+/) 29 | terms_and_fields = terms.map do |term| 30 | {"$or" => search_fields.map {|field| {field => /#{term}/i} }} 31 | end 32 | 33 | {"$and" => terms_and_fields} 34 | end 35 | end 36 | 37 | def _order_by_fields params, fields 38 | [fields[params[:isortcol_0].to_i], params[:ssortdir_0] || "asc"] 39 | end 40 | 41 | def _sanitize string 42 | string.to_s.strip.gsub(/([\^\\\/\.\+\*\?\|\[\]\(\)\{\}\$])/) { "\\#{$1}" } 43 | end 44 | end 45 | end 46 | end 47 | 48 | -------------------------------------------------------------------------------- /lib/data_table/rails.rb: -------------------------------------------------------------------------------- 1 | module DataTable 2 | class Railtie < Rails::Railtie 3 | initializer 'data_table.initialize' do 4 | ActiveSupport.on_load(:active_record) do 5 | ::ActiveRecord::Base.send :extend, DataTable::ClassMethods 6 | ::ActiveRecord::Base.send :extend, DataTable::ActiveRecord::ClassMethods 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/data_table/version.rb: -------------------------------------------------------------------------------- 1 | module DataTable 2 | VERSION = "0.4.15" 3 | end 4 | -------------------------------------------------------------------------------- /lib/mongoid/data_table.rb: -------------------------------------------------------------------------------- 1 | if defined?(Mongoid) 2 | module Mongoid 3 | module DataTable 4 | def self.included base 5 | base.send :extend, ::DataTable::ClassMethods 6 | base.send :extend, ::DataTable::Mongoid::ClassMethods 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/active_record_data_table_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe DataTable do 4 | 5 | include DataTable::ActiveRecord::ClassMethods 6 | 7 | context "#_find_objects" do 8 | 9 | it "should find the objects required based on the params" do 10 | params = {:ssearch => "answer", :isortcol_0 => "0", :ssortdir_0 => "desc", :idisplaylength => 10, :secho => 1} 11 | 12 | mock(self)._discover_joins(%w(foo bar baz)) { [] } 13 | mock(self)._where_conditions("answer", %w(foo bar)) { "where clause" } 14 | mock(self)._order_fields(params, %w(foo bar baz)) { "order" } 15 | 16 | mock(self).where("where clause") { mock!.includes([]) { mock!.order("order") { mock!.paginate({:page => :page, :per_page => :per_page}) { :answer } } } } 17 | mock(self)._page(params) { :page } 18 | mock(self)._per_page(params) { :per_page } 19 | 20 | _find_objects(params, %w(foo bar baz), %w(foo bar)).should == :answer 21 | end 22 | 23 | end 24 | 25 | context "#_where_conditions" do 26 | 27 | it "should return nil if the query is blank" do 28 | send(:_where_conditions, "", %w(foo bar baz)).should == nil 29 | end 30 | 31 | it "should return an AR array with an entry for each search field" do 32 | send(:_where_conditions, "query", %w(foo bar)).should == ["(UPPER(foo) LIKE ? OR UPPER(bar) LIKE ?)", "%QUERY%", "%QUERY%"] 33 | end 34 | 35 | context "with multiple terms" do 36 | it "should return an AR array with conditions for all combinations of terms and fields" do 37 | send(:_where_conditions, "q1 q2", %w(f1 f2)).should == ["(UPPER(f1) LIKE ? OR UPPER(f2) LIKE ?) AND (UPPER(f1) LIKE ? OR UPPER(f2) LIKE ?)", "%Q1%", "%Q1%", "%Q2%", "%Q2%"] 38 | end 39 | end 40 | 41 | context "with a date field" do 42 | it "should return an AR array using equality and converting to a date" do 43 | send(:_where_conditions, "2011/09/03", [["f1", {:date => true}]]).should == ["(f1 = ?)", Date.new(2011, 9, 3)] 44 | end 45 | 46 | it "should return an AR array properly not search date fields with non-dates" do 47 | send(:_where_conditions, "foo", ["f1", ["f2", {:date => true}]]).should == ["(UPPER(f1) LIKE ?)", "%FOO%"] 48 | end 49 | end 50 | 51 | context "with complex conditions" do 52 | it "should return an AR array with an entry for each search field" do 53 | send(:_where_conditions, "query", [%w(foo bar)]).should == ["((UPPER(foo) LIKE ? AND UPPER(bar) LIKE ?))", "%QUERY%", "%QUERY%"] 54 | end 55 | 56 | it "should return an AR array with an entry for each search field with a split query" do 57 | send(:_where_conditions, "query-two", [['foo', 'bar', {:split => '-'}]]).should == ["((UPPER(foo) LIKE ? AND UPPER(bar) LIKE ?))", "%QUERY%", "%TWO%"] 58 | end 59 | 60 | it "should return an AR array with an entry for each search field with ands and ors" do 61 | send(:_where_conditions, "query", ['foz', ['foo', 'bar']]).should == ["(UPPER(foz) LIKE ? OR (UPPER(foo) LIKE ? AND UPPER(bar) LIKE ?))", "%QUERY%", "%QUERY%", "%QUERY%"] 62 | end 63 | 64 | it "should return an AR array with an entry for each search field with ands and ors with a split query" do 65 | send(:_where_conditions, "query-two", ['foz', ['foo', 'bar', {:split => '-'}]]).should == ["(UPPER(foz) LIKE ? OR (UPPER(foo) LIKE ? AND UPPER(bar) LIKE ?))", "%QUERY-TWO%", "%QUERY%", "%TWO%"] 66 | end 67 | 68 | it "should ignore a split query if the query isn't the size of the split fields" do 69 | send(:_where_conditions, "query", ['foz', ['foo', 'bar', {:split => '-'}]]).should == ["(UPPER(foz) LIKE ?)", "%QUERY%"] 70 | end 71 | 72 | it "should still work with multiple terms" do 73 | send(:_where_conditions, "q1 q-2", ['F1', ['P1', 'P2', {:split => '-'}]]).should == 74 | ["(UPPER(F1) LIKE ?) AND (UPPER(F1) LIKE ? OR (UPPER(P1) LIKE ? AND UPPER(P2) LIKE ?))", "%Q1%", "%Q-2%", "%Q%", "%2%"] 75 | end 76 | 77 | it "should not use like with a numeric split query" do 78 | send(:_where_conditions, "101-04", [['foo', 'bar', {:split => '-', :types => [:numeric, :numeric]}]]).should == 79 | ["((foo = ? AND bar = ?))", 101, 4] 80 | end 81 | end 82 | end 83 | 84 | context "#_discover_joins" do 85 | 86 | it "should return the joins on the fields" do 87 | mock(self).new {self} 88 | stub(self).foo {true} 89 | stub(self).foz {true} 90 | stub(self).furs {true} 91 | 92 | joins = _discover_joins(%w(foo.bar foz.ber furs.bib nones.zip baz)) 93 | joins.should include :foo 94 | joins.should include :foz 95 | joins.should include :furs 96 | joins.should_not include :nones 97 | end 98 | 99 | end 100 | 101 | context "#_order_fields" do 102 | 103 | it "should find the field name and pass the sort direction" do 104 | send(:_order_fields, {:isortcol_0 => "1", :ssortdir_0 => "asc"}, %w(foo bar baz)).should == "bar ASC" 105 | end 106 | 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/data_table_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "action_controller/metal/strong_parameters" 3 | 4 | describe DataTable do 5 | 6 | include DataTable::ClassMethods 7 | 8 | context "on being included" do 9 | it "should extend ClassMethods" do 10 | klass = Class.new 11 | mock(klass).send(:extend, DataTable::ClassMethods) 12 | mock(klass).send(:extend, DataTable::Mongoid::ClassMethods) 13 | klass.instance_eval %{include DataTable} 14 | end 15 | end 16 | 17 | context "#for_data_table" do 18 | 19 | it "should produce JSON for the datatables plugin to consume" do 20 | params = ActionController::Parameters.new(sSearch: "answer", iSortCol_0: "0", sSortDir_0: "desc", iDisplayLength: "10", sEcho: "1") 21 | normalized_params = {ssearch: "answer", isortcol_0: "0", ssortdir_0: "desc", idisplaylength: "10", secho: "1"} 22 | controller = stub!.params { params }.subject 23 | 24 | fields = %w(foo bar baz) 25 | search_fields = %w(foo bar) 26 | block = :block 27 | 28 | mock(self).count { 42 } 29 | objects = mock!.total_entries { 10 }.subject 30 | mock(self)._find_objects(normalized_params, fields, search_fields) { objects } 31 | mock(self)._yield_and_render_array(controller, objects, block) { :results } 32 | 33 | result = for_data_table(controller, fields, search_fields, block) 34 | result.should == {:sEcho => 1, :iTotalRecords => 42, :iTotalDisplayRecords => 10, :aaData => :results}.to_json.html_safe 35 | end 36 | 37 | it "should work with a pagination library that doesn't respond to #total_entries" do 38 | params = {:ssearch => "answer", :isortcol_0 => "0", :ssortdir_0 => "desc", :idisplaylength => "10", :secho => "1"} 39 | controller = stub!.params { params }.subject 40 | 41 | fields = %w(foo bar baz) 42 | search_fields = %w(foo bar) 43 | 44 | mock(self).count { 42 } 45 | mock(self)._matching_count(params, search_fields) { 10 } 46 | mock(self)._find_objects(params, fields, search_fields) { :objects } 47 | mock(self)._yield_and_render_array(controller, :objects, :block) { :results } 48 | 49 | result = for_data_table(controller, fields, search_fields, :block) 50 | result.should == {:sEcho => 1, :iTotalRecords => 42, :iTotalDisplayRecords => 10, :aaData => :results}.to_json.html_safe 51 | end 52 | 53 | end 54 | 55 | context "#_yield_and_render_array" do 56 | 57 | it "should walk through the array and render it, passing in the appropriate local name" do 58 | block = lambda {|x| mock!.map { [42] }.subject } 59 | 60 | result = _yield_and_render_array Object.new, [:foo], block 61 | result.should == [[42]] 62 | end 63 | 64 | end 65 | 66 | context "#_page" do 67 | 68 | context "with a display length of 10" do 69 | it "should return 1 when start is 0" do 70 | send(:_page, {idisplaystart: "0", idisplaylength: "10"}).should == 1 71 | end 72 | 73 | it "should return 2 when start is 10" do 74 | send(:_page, {idisplaystart: "10", idisplaylength: "10"}).should == 2 75 | end 76 | 77 | it "should default to 1 when idisplaystart or idisplaylength are missing" do 78 | send(:_page, {}).should == 1 79 | end 80 | 81 | end 82 | 83 | end 84 | 85 | context "#_per_page" do 86 | 87 | it "should return 10 given an iDisplayLength of 10" do 88 | send(:_per_page, {idisplaylength: "10"}).should == 10 89 | end 90 | 91 | it "should return a default of 25 given an invalid iDisplayLength" do 92 | send(:_per_page, {idisplaylength: "foobar"}).should == 25 93 | end 94 | 95 | it "should return self.count given an iDisplayLength of -1" do 96 | mock(self).count { :all } 97 | send(:_per_page, {idisplaylength: "-1"}).should == :all 98 | end 99 | 100 | it "should default to 10 if idisplaylength is missing" do 101 | send(:_per_page, {}).should == 25 102 | end 103 | 104 | end 105 | 106 | end 107 | -------------------------------------------------------------------------------- /spec/mongoid_data_table_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe DataTable do 4 | 5 | include DataTable::Mongoid::ClassMethods 6 | 7 | context "#_find_objects" do 8 | it "should find the objects required based on the params" do 9 | params = {:ssearch => "answer", :isortcol_0 => "0", :ssortdir_0 => "desc", :idisplaylength => 10, :secho => 1} 10 | 11 | mock(self)._where_conditions("answer", %w(foo bar)) { "where clause" } 12 | mock(self)._order_by_fields(params, %w(foo bar baz)) { "order by" } 13 | 14 | mock(self)._page(params) { :page } 15 | mock(self)._per_page(params) { :per_page } 16 | mock(self).where("where clause") { mock!.order_by("order by") { mock!.page(:page) { mock!.per(:per_page) { :answer } } } } 17 | 18 | _find_objects(params, %w(foo bar baz), %w(foo bar)).should == :answer 19 | end 20 | end 21 | 22 | context "#_where_conditions" do 23 | 24 | it "should return nil if the query is blank" do 25 | send(:_where_conditions, "", %w(foo bar baz)).should == nil 26 | end 27 | 28 | it "should return a mongoid $and wrapping an $or for each search field" do 29 | send(:_where_conditions, "q", %w(foo bar)).should == {"$and" => [{"$or"=>[{"foo"=>/q/i}, {"bar"=>/q/i}]}]} 30 | end 31 | 32 | it "should not use $or if there is only one search field" do 33 | send(:_where_conditions, "q", %w(f)).should == {"f" => /q/i} 34 | end 35 | 36 | context "given multiple search terms" do 37 | it "should require a match for each term when there is a single search field" do 38 | send(:_where_conditions, "q1 q2", %w(f)).should == {"f" => {"$all" => [/q1/i, /q2/i]}} 39 | end 40 | 41 | it "should require a match for each term when there is a single search field with spaces at the end" do 42 | send(:_where_conditions, "q1 ", %w(f)).should == {"f" => /q1/i} 43 | end 44 | end 45 | 46 | end 47 | 48 | context "#_order_by_fields" do 49 | 50 | it "should find the field name and pass the sort direction" do 51 | send(:_order_by_fields, {:isortcol_0 => "1", :ssortdir_0 => "asc"}, %w(foo bar baz)).should == ["bar", "asc"] 52 | end 53 | 54 | it "should use defaults if none are given" do 55 | send(:_order_by_fields, {}, %w(foo bar baz)).should == ["foo", "asc"] 56 | end 57 | 58 | end 59 | 60 | context "#_sanitize" do 61 | 62 | it "should work for nil" do 63 | send(:_sanitize, nil).should == "" 64 | end 65 | 66 | it "should escape characters for the regex" do 67 | send(:_sanitize, " ^\\/.+*?|[](){}$ ").should == "\\^\\\\\\/\\.\\+\\*\\?\\|\\[\\]\\(\\)\\{\\}\\$" 68 | end 69 | 70 | end 71 | 72 | end 73 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "data_table" 2 | 3 | RSpec.configure do |config| 4 | config.treat_symbols_as_metadata_keys_with_true_values = true 5 | config.run_all_when_everything_filtered = true 6 | config.filter_run :focus 7 | 8 | config.mock_with :rr 9 | config.order = "random" 10 | end 11 | --------------------------------------------------------------------------------