tabulatr 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore CHANGED
@@ -2,3 +2,4 @@ pkg
2
2
  tabulatr-*.gem
3
3
  Gemfile.lock
4
4
  .bundle
5
+ spec/dummy_app/tmp/*
data/Changelog.textile CHANGED
@@ -1,5 +1,12 @@
1
1
  h1. Changelog for tabulatr
2
2
 
3
+ h2. 0.2.0
4
+
5
+ * Mongoid can also be used in place of ActiveRecord
6
+ * fixed handling of the primary_key column and name
7
+ * Added dom_id to tables tr. Thanks to <a href="https://github.com/sterrym">sterrym (Tim Glen)</a>!
8
+ * Added note on "Request-URI Too Large error" to README
9
+
3
10
  h2. 0.1.3
4
11
 
5
12
  * nicer table headers. Thanks to <a href="https://github.com/troya2" target="_blank">troya2 (Troy Anderson)</a>!
data/Gemfile CHANGED
@@ -14,4 +14,5 @@ group :development, :test do
14
14
  gem 'capybara'
15
15
  gem 'launchy'
16
16
  gem 'database_cleaner'
17
+ gem 'mongoid'
17
18
  end
data/README.textile CHANGED
@@ -4,7 +4,7 @@ h1. Tabulatr - Index Tables made easy, finally
4
4
 
5
5
  h2. Mission Objective
6
6
 
7
- Tabulatr aims at making the ever-recurring task of creating listings of ActiveRecord models simple and uniform.
7
+ Tabulatr aims at making the ever-recurring task of creating listings of ActiveRecord or Mongoid models simple and uniform.
8
8
 
9
9
  We found ourselves reinventing the wheel in every project we made, by using
10
10
 
@@ -313,16 +313,22 @@ To override these, you should, in an initializer, call
313
313
  h2. Dependencies
314
314
 
315
315
  We use
316
- * <a href="http://github.com/provideal/whiny_hash">WhinyHash</a> to handle the options in a fail-early-manner,
317
- * <a href="http://github.com/provideal/id_stuffer">IdStuffer</a> to compress the _remembered_ ids.
316
+ * <a href="http://github.com/provideal/whiny_hash">whiny_hash</a> to handle the options in a fail-early-manner,
317
+ * <a href="http://github.com/provideal/id_stuffer">id_stuffer</a> to compress the _remembered_ ids.
318
318
  * And... eh... It's an extension for Rails 3, so it might be handy to have a version of Rails 3 and Ruby 1.8.? or 1.9.?.
319
319
 
320
320
  h2. Bugs
321
321
 
322
- There are, definitely, roughly 999 bugs in Tabulatr, although we do some testing (see <tt>spec/</tt>). So if you hunt them, please let me know using the <a href="https://github.com/provideal/tabulatr/issues">GitHub Bugtracker</a>.
322
+ h3. Request-URI Too Large error
323
+
324
+ Since everyhing about the state of the table is intentionally encoded in the URI, the URI may become somewhat bloated if fancy selecting is used. By using <a href="http://github.com/provideal/id_stuffer">id_stuffer</a>, we drastically reduced the size of the URI (that's the reason we actually made it), but it may still become rather large. This is a problem particulary when using WEBrick, because WEBricks URIs must not exceed 2048 characters. And this limit is hard-coded IIRC. So – If you run into this limitation – please consider using another server. (Thanks to <a href="https://github.com/stepheneb">stepheneb</a> for calling my attention back to this.)
325
+
326
+ h3. Other, new bugs
327
+
328
+ There are roughly another 997 bugs in Tabulatr, although we do some testing (see <tt>spec/</tt>). So if you hunt them, please let me know using the <a href="https://github.com/provideal/tabulatr/issues">GitHub Bugtracker</a>.
323
329
 
324
330
  h2. Contributing to tabulatr
325
-
331
+
326
332
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
327
333
  * Check out the <a href="https://github.com/provideal/tabulatr/issues">issue tracker</a> to make sure someone already hasn't requested it and/or contributed it
328
334
  * Fork the project
@@ -26,7 +26,7 @@
26
26
  if Object.const_defined? "ActiveRecord"
27
27
  class ActiveRecord::Base
28
28
  def self.find_for_table(params, opts={}, &block)
29
- Tabulatr::Finder.find_for_active_record_table(self, params, opts, &block)
29
+ Tabulatr::Finder.find_for_table(self, params, opts, &block)
30
30
  end
31
31
  end
32
32
  end
@@ -21,13 +21,14 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
- # buggy like hell
25
- if Object.const_defined? "Mongoid"
26
- raise "Mongoid support is buggy like hell as of now"
27
- module Mongoid::Document
24
+ # This is somewhat ugly, for it creates the Mongoid module, no matter
25
+ # whether it's about to be used. So you will find an empty Mongoid
26
+ # module in every Tabulatr project.
27
+ module Mongoid
28
+ module Document
28
29
  module ClassMethods
29
30
  def find_for_table(params, opts={}, &block)
30
- Tabulatr.find_for_mongoid_table(self, params, opts, &block)
31
+ Tabulatr::Finder.find_for_table(self, params, opts, &block)
31
32
  end
32
33
  end
33
34
  end
@@ -80,8 +80,12 @@ class Tabulatr
80
80
  assoc = @record.class.reflect_on_association(relation)
81
81
  make_tag(:td, opts[:td_html]) do
82
82
  format = opts[:format]
83
- concat(if assoc.collection? and opts[:map]
84
- @record.send(relation.to_sym).map do |r|
83
+ ass = @record.send(relation.to_sym)
84
+ if opts[:sort_by]
85
+ # TODO: SORTING specified by opts[:sort_by]
86
+ end
87
+ concat(if (ass.is_a?(Array) or assoc.collection?) and opts[:map]
88
+ ass.map do |r|
85
89
  val = h(r.send(opts[:method] || name))
86
90
  if format.is_a?(Proc) then format.call(val)
87
91
  elsif format.is_a?(String) then h(format % val)
@@ -90,8 +94,8 @@ class Tabulatr
90
94
  end
91
95
  end.join(opts[:join_symbol])
92
96
  else
93
- ass = @record.send(relation.to_sym)
94
97
  return '' unless ass
98
+ #puts ass.to_s
95
99
  val = h(ass.send(opts[:method] || name))
96
100
  val = if format.is_a?(Proc) then format.call(val)
97
101
  elsif format.is_a?(String) then h(format % val)
@@ -106,10 +110,10 @@ class Tabulatr
106
110
  raise "Whatever that's for!" if block_given?
107
111
  iname = "#{@classname}#{@table_form_options[:checked_postfix]}[current_page][]"
108
112
  make_tag(:td, opts[:td_html]) do
109
- checked = @checked[:selected].member?(@record.id.to_s) ? :checked : nil
113
+ checked = @checked[:selected].member?(@record.send(@id)) ? :checked : nil
110
114
  make_tag(:input, :type => 'checkbox', :name => iname,
111
- :id => "#{@classname}#{@table_form_options[:checked_postfix]}_#{@record.id.to_s}",
112
- :value => @record.id, :checked => checked)
115
+ :id => "#{@classname}#{@table_form_options[:checked_postfix]}_#{@record.send(@id).to_s}",
116
+ :value => @record.send(@id), :checked => checked)
113
117
  end
114
118
  end
115
119
 
@@ -28,9 +28,15 @@ module Tabulatr::Finder
28
28
  # -------------------------------------------------------------------
29
29
  # Called if SomeActveRecordSubclass::find_for_table(params) is called
30
30
  #
31
- def self.find_for_active_record_table(klaz, params, o={}, &block)
31
+ def self.find_for_table(klaz, params, o={}, &block)
32
+ rel = klaz
33
+ typ = if klaz.respond_to?(:descends_from_active_record?) and klaz.descends_from_active_record? then :ar
34
+ elsif klaz.include?(Mongoid::Document) then :mongoid
35
+ else raise("Don't know how to deal with class '#{klaz}'")
36
+ end
37
+
32
38
  # on the first run, get the correct like db-operator, can still be ovrrridden
33
- unless Tabulatr::SQL_OPTIONS[:like]
39
+ unless typ != :ar || Tabulatr::SQL_OPTIONS[:like]
34
40
  case ActiveRecord::Base.connection.class.to_s
35
41
  when "ActiveRecord::ConnectionAdapters::MysqlAdapter" then Tabulatr.sql_options(:like => 'LIKE')
36
42
  when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter" then Tabulatr.sql_options(:like => 'ILIKE')
@@ -56,8 +62,11 @@ module Tabulatr::Finder
56
62
  # before we do anything else, we find whether there's something to do for batch actions
57
63
  checked_param = ActiveSupport::HashWithIndifferentAccess.new({:checked_ids => '', :current_page => []}).
58
64
  merge(params[check_name] || {})
65
+ id = (typ==:ar ? klaz.primary_key.to_sym : :id)
66
+ id_type = (typ==:ar ? klaz.columns_hash[id.to_s].type : :string)
59
67
  checked_ids = uncompress_id_list(checked_param[:checked_ids])
60
68
  new_ids = checked_param[:current_page]
69
+ new_ids.map!(&:to_i) if id_type==:integer
61
70
  selected_ids = checked_ids + new_ids
62
71
  batch_param = params[batch_name]
63
72
  if batch_param.present? and block_given?
@@ -66,10 +75,10 @@ module Tabulatr::Finder
66
75
  end
67
76
 
68
77
  # then, we obey any "select" buttons if pushed
69
- precondition = opts[:precondition] || "(1=1)"
78
+ precon = rel
79
+ precon = precon.where(opts[:precondition]) if opts[:precondition].present?
70
80
  if checked_param[:select_all]
71
- all = klaz.find :all, :conditions => precondition, :select => :id
72
- selected_ids = all.map { |r| r.id.to_s }
81
+ selected_ids = (typ==:ar ? precon.select(:id) : precon.only(:id) ).to_a.map { |r| r.send(id) }
73
82
  elsif checked_param[:select_none]
74
83
  selected_ids = []
75
84
  elsif checked_param[:select_visible]
@@ -118,33 +127,34 @@ module Tabulatr::Finder
118
127
 
119
128
  # firstly, get the conditions from the filters
120
129
  includes = []
121
- precondition = opts[:precondition] || "(1=1)"
122
- conditions = filter_param.inject([precondition.dup, []]) do |c, t|
130
+ rel = rel.where(opts[:precondition]) if opts[:precondition]
131
+ conditions = filter_param.each do |t|
123
132
  n, v = t
133
+ next unless v.present?
124
134
  # FIXME n = name_escaping(n)
125
135
  if (n != form_options[:associations_filter])
126
- condition_from("#{klaz.table_name}.#{n}",v,c)
136
+ table_name = (typ==:ar ? klaz.table_name : klaz.to_s.tableize.gsub('/','_'))
137
+ rel = condition_from(rel, typ, "#{table_name}.#{n}", v)
127
138
  else
128
- v.inject(c) do |c,t|
139
+ v.each do |t|
129
140
  n,v = t
130
141
  assoc, att = n.split(".").map(&:to_sym)
131
142
  r = klaz.reflect_on_association(assoc)
132
143
  includes << assoc
133
- tname = r.table_name
134
- nn = "#{tname}.#{att}"
135
- condition_from(nn,v,c)
144
+ table_name = (typ==:ar ? klaz.table_name : klaz.to_s.tableize)
145
+ nn = "#{table_name}.#{att}"
146
+ rel = condition_from(rel, typ, nn, v)
136
147
  end
137
148
  end
138
149
  end
139
- conditions = [conditions.first] + conditions.last
140
150
 
141
151
  # more button handling
142
152
  if checked_param[:select_filtered]
143
- all = klaz.find :all, :conditions => conditions, :select => :id, :include => includes
144
- selected_ids = (selected_ids + all.map { |r| r.id.to_s }).sort.uniq
153
+ all = rel.all
154
+ selected_ids = (selected_ids + all.map { |r| i=r.send(id); i.is_a?(Fixnum) ? i : i.to_s }).sort.uniq
145
155
  elsif checked_param[:unselect_filtered]
146
- all = klaz.find :all, :conditions => conditions, :select => :id, :include => includes
147
- selected_ids = (selected_ids - all.map { |r| r.id.to_s }).sort.uniq
156
+ all = rel.dup.all
157
+ selected_ids = (selected_ids - all.map { |r| i=r.send(id); i.is_a?(Fixnum) ? i : i.to_s }).sort.uniq
148
158
  end
149
159
 
150
160
  # secondly, find the order_by stuff
@@ -158,7 +168,6 @@ module Tabulatr::Finder
158
168
  end
159
169
  raise "SECURITY violation, sort field name is '#{n}'" unless /^[\w]+$/.match order_direction
160
170
  raise "SECURITY violation, sort field name is '#{n}'" unless /^[\d\w]+$/.match order_by
161
- order = "#{order_by} #{order_direction}"
162
171
  else
163
172
  if opts[:default_order]
164
173
  l = opts[:default_order].split(" ")
@@ -166,11 +175,11 @@ module Tabulatr::Finder
166
175
  if l.length == 0 or l.length > 2
167
176
  order_by = l[0]
168
177
  order_direction = l[1] || 'asc'
169
- order = "#{order_by} #{order_direction}"
170
178
  else
171
179
  order = order_by = order_direction = nil
172
180
  end
173
181
  end
182
+ order = (typ==:ar ? "#{order_by} #{order_direction}" : [order_by.to_s, order_direction.to_s]) if order_by
174
183
 
175
184
  # thirdly, get the pagination data
176
185
  paginate_options = Tabulatr.paginate_options.merge(opts).merge(pops)
@@ -178,40 +187,41 @@ module Tabulatr::Finder
178
187
  page = paginate_options[:page].to_i
179
188
  page += 1 if paginate_options[:page_right]
180
189
  page -= 1 if paginate_options[:page_left]
181
- c = klaz.count :conditions => conditions, :include => includes
190
+ c = rel.count
182
191
  pages = (c/pagesize).ceil
183
192
  page = [1, [page, pages].min].max
184
-
193
+ total = klaz
194
+ total = total.where(opts[:precondition]) if opts[:precondition]
195
+ total = total.count
196
+
185
197
  # Now, actually find the stuff
186
- found = klaz.find :all, :conditions => conditions,
187
- :limit => pagesize.to_i, :offset => ((page-1)*pagesize).to_i,
188
- :order => order, :include => includes
198
+ found = rel.limit(pagesize.to_i).offset(((page-1)*pagesize).to_i)
199
+ .order(order).to_a #, :include => includes
189
200
 
190
201
  # finally, inject methods to retrieve the current 'settings'
191
- fio = Tabulatr.finder_inject_options
192
- found.define_singleton_method(fio[:filters]) do filter_param end
193
- found.define_singleton_method(fio[:classname]) do cname end
194
- found.define_singleton_method(fio[:pagination]) do
202
+ found.define_singleton_method(:__filters) do filter_param end
203
+ found.define_singleton_method(:__classinfo) do [cname, id, id_type] end
204
+ found.define_singleton_method(:__pagination) do
195
205
  { :page => page, :pagesize => pagesize, :count => c, :pages => pages,
196
206
  :pagesizes => paginate_options[:pagesizes],
197
- :total => klaz.count(:conditions => precondition) }
207
+ :total => total }
198
208
  end
199
- found.define_singleton_method(fio[:sorting]) do
209
+ found.define_singleton_method(:__sorting) do
200
210
  order ? { :by => order_by, :direction => order_direction } : nil
201
211
  end
202
- visible_ids = (found.map { |r| r.id.to_s })
212
+ visible_ids = (found.map { |r| r.send(id) })
203
213
  checked_ids = compress_id_list(selected_ids - visible_ids)
204
214
  visible_ids = compress_id_list(visible_ids)
205
- found.define_singleton_method(fio[:checked]) do
215
+ found.define_singleton_method(:__checked) do
206
216
  { :selected => selected_ids,
207
217
  :checked_ids => checked_ids,
208
218
  :visible => visible_ids
209
219
  }
210
220
  end
211
- found.define_singleton_method(fio[:stateful]) do
221
+ found.define_singleton_method(:__stateful) do
212
222
  (opts[:stateful] ? true : false)
213
223
  end
214
- found.define_singleton_method(fio[:store_data]) do
224
+ found.define_singleton_method(:__store_data) do
215
225
  opts[:store_data] || {}
216
226
  end
217
227
 
@@ -24,18 +24,32 @@
24
24
  # These are extensions for use from ActionController instances
25
25
  module Tabulatr::Finder
26
26
 
27
- require File.join(File.dirname(__FILE__), 'finder', 'find_for_active_record_table')
28
- require File.join(File.dirname(__FILE__), 'finder', 'find_for_mongoid_table')
27
+ require File.join(File.dirname(__FILE__), 'finder', 'find_for_table')
29
28
 
30
29
  # compress the list of ids as good as I could imagine ;)
31
30
  # uses fancy base twisting
32
31
  def self.compress_id_list(list)
33
- IdStuffer.stuff(list)
32
+ if list.length == 0
33
+ ""
34
+ elsif list.first.is_a?(Fixnum)
35
+ IdStuffer.stuff(list)
36
+ else
37
+ "GzB" + Base64.encode64s(
38
+ Zlib::Deflate.deflate(
39
+ list.join(Tabulatr.table_form_options[:checked_separator])))
40
+ end
34
41
  end
35
42
 
36
43
  # inverse of compress_id_list
37
44
  def self.uncompress_id_list(str)
38
- IdStuffer.unstuff(str).map &:to_s
45
+ if !str.present?
46
+ []
47
+ elsif str.starts_with?("GzB")
48
+ Zlib::Inflate.inflate(Base64.decode64(str[3..-1]))
49
+ .split(Tabulatr.table_form_options[:checked_separator])
50
+ else
51
+ IdStuffer.unstuff(str)
52
+ end
39
53
  end
40
54
 
41
55
  class Invoker
@@ -57,26 +71,46 @@ private
57
71
  klaz.to_s.downcase.gsub("/","_")
58
72
  end
59
73
 
60
- def self.condition_from(n,v,c)
74
+ def self.condition_from(rel, typ, n, v)
61
75
  raise "SECURITY violation, field name is '#{n}'" unless /^[\d\w]+(\.[\d\w]+)?$/.match n
62
76
  @like ||= Tabulatr.sql_options[:like]
63
- nc = c
64
77
  if v.is_a?(String)
65
78
  if v.present?
66
- nc = [c[0] << " AND (#{n} = ?) ", c[1] << v]
79
+ if typ == :ar
80
+ rel = rel.where(n => v)
81
+ elsif typ == :mongoid
82
+ nn = n.split('.').last
83
+ rel = rel.where(nn => v)
84
+ else raise "Unknown db type '#{typ}'"
85
+ end
67
86
  end
68
87
  elsif v.is_a?(Hash)
69
88
  if v[:like]
70
89
  if v[:like].present?
71
- nc = [c[0] << " AND (#{n} #{@like} ?) ", c[1] << "%#{v[:like]}%"]
90
+ if typ==:ar
91
+ rel = rel.where("#{n} #{@like} ?", "%#{v[:like]}%")
92
+ elsif typ==:mongoid
93
+ nn = n.split('.').last
94
+ rel = rel.where(nn => Regexp.new(v[:like]))
95
+ else
96
+ raise "Unknown db type '#{typ}'"
97
+ end
72
98
  end
73
99
  else
74
- nc = [c[0] << " AND (#{n} >= ?) ", c[1] << "#{v[:from]}"] if v[:from].present?
75
- nc = [nc[0] << " AND (#{n} <= ?) ", nc[1] << "#{v[:to]}"] if v[:to].present?
100
+ if typ==:ar
101
+ rel = rel.where("#{n} >= ?", "#{v[:from]}") if v[:from].present?
102
+ rel = rel.where("#{n} <= ?", "#{v[:to]}") if v[:to].present?
103
+ elsif typ==:mongoid
104
+ nn = n.split('.').last.to_sym
105
+ rel = rel.where(nn.gte => v[:from]) if v[:from].present?
106
+ rel = rel.where(nn.lte => v[:to]) if v[:to].present?
107
+ else
108
+ raise "Unknown db type '#{typ}'"
109
+ end
76
110
  end
77
111
  else
78
112
  raise "Wrong filter type: #{v.class}"
79
113
  end
80
- nc
114
+ rel
81
115
  end
82
116
  end
@@ -27,7 +27,7 @@ class Tabulatr
27
27
  def render_paginator
28
28
  # get the current pagination state
29
29
  pagination_name = "#{@classname}#{TABLE_FORM_OPTIONS[:pagination_postfix]}"
30
- pparams = @records.send(FINDER_INJECT_OPTIONS[:pagination])
30
+ pparams = @records.__pagination
31
31
  page = pparams[:page].to_i
32
32
  pages = pparams[:pages].to_i
33
33
  pagesize = pparams[:pagesize].to_i
@@ -153,22 +153,10 @@ class Tabulatr
153
153
  :link => false, # proc or symbol to make the content a link
154
154
  :join_symbol => ', ', # symbol used to join the elements of 'many' associations
155
155
  :map => true, # whether to map the call on individual records (true) or call on the list (false)
156
+ :sort_by => false, # sort the elements of an association
156
157
  :sortable => true # if set, sorting-stuff is added to the header cell
157
158
  })
158
159
 
159
- # these settings are considered constant for the whole application, can not be overridden
160
- # on a per-table basis.
161
- # That's necessary to allow find_for_table to work properly
162
- FINDER_INJECT_OPTIONS = WhinyHash.new({
163
- :pagination => :__pagination,
164
- :filters => :__filters,
165
- :classname => :__classname,
166
- :sorting => :__sorting,
167
- :checked => :__checked,
168
- :store_data => :__store_data,
169
- :stateful => :__stateful
170
- })
171
-
172
160
  # defaults for the find_for_table
173
161
  FINDER_OPTIONS = WhinyHash.new({
174
162
  :default_order => false,
@@ -183,11 +171,6 @@ class Tabulatr
183
171
  :like => nil
184
172
  })
185
173
 
186
- def self.finder_inject_options(n=nil)
187
- FINDER_INJECT_OPTIONS.merge!(n) if n
188
- FINDER_INJECT_OPTIONS
189
- end
190
-
191
174
  def self.finder_options(n=nil)
192
175
  FINDER_OPTIONS.merge!(n) if n
193
176
  FINDER_OPTIONS
@@ -41,6 +41,7 @@ class Tabulatr
41
41
  include ActionView::Helpers::FormTagHelper
42
42
  include ActionView::Helpers::FormOptionsHelper
43
43
  include ActionView::Helpers::TranslationHelper
44
+ include ActionView::Helpers::RecordTagHelper
44
45
 
45
46
  # Constructor of Tabulatr
46
47
  #
@@ -56,13 +57,13 @@ class Tabulatr
56
57
  @val = []
57
58
  @record = nil
58
59
  @row_mode = false
59
- @classname = @records.send(FINDER_INJECT_OPTIONS[:classname])
60
- @pagination = @records.send(FINDER_INJECT_OPTIONS[:pagination])
61
- @filters = @records.send(FINDER_INJECT_OPTIONS[:filters])
62
- @sorting = @records.send(FINDER_INJECT_OPTIONS[:sorting])
63
- @checked = @records.send(FINDER_INJECT_OPTIONS[:checked])
64
- @store_data = @records.send(FINDER_INJECT_OPTIONS[:store_data])
65
- @stateful = @records.send(FINDER_INJECT_OPTIONS[:stateful])
60
+ @classname, @id, @id_type = @records.__classinfo
61
+ @pagination = @records.__pagination
62
+ @filters = @records.__filters
63
+ @sorting = @records.__sorting
64
+ @checked = @records.__checked
65
+ @store_data = @records.__store_data
66
+ @stateful = @records.__stateful
66
67
  @should_translate = @table_options[:translate]
67
68
  end
68
69
 
@@ -214,7 +215,7 @@ private
214
215
  else
215
216
  rc = nil
216
217
  end
217
- make_tag(:tr, row_html.merge(:class => rc)) do
218
+ make_tag(:tr, row_html.merge(:class => rc, :id => dom_id(record))) do
218
219
  yield(data_row_builder(record))
219
220
  end # </tr>
220
221
  end
@@ -1,3 +1,3 @@
1
1
  class Tabulatr
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -1,6 +1,23 @@
1
- class Product < ActiveRecord::Base
1
+ if USE_MONGOID
2
+ class Product
3
+ include Mongoid::Document
2
4
 
3
- has_and_belongs_to_many :tags
4
- belongs_to :vendor
5
-
6
- end
5
+ has_and_belongs_to_many :tags
6
+ belongs_to :vendor
7
+
8
+ field :name, :type => String
9
+ field :url, :type => String
10
+ field :active, :type => Boolean
11
+ field :description, :type => String
12
+ field :price, :type => Fixnum
13
+ field :created_at, :type => Time
14
+ field :updated_at, :type => Time
15
+ end
16
+ else
17
+ class Product < ActiveRecord::Base
18
+
19
+ has_and_belongs_to_many :tags
20
+ belongs_to :vendor
21
+
22
+ end
23
+ end
@@ -1,3 +1,13 @@
1
- class Tag < ActiveRecord::Base
2
- has_and_belongs_to_many :products
3
- end
1
+ if USE_MONGOID
2
+ class Tag
3
+ include Mongoid::Document
4
+ has_and_belongs_to_many :products
5
+ field :title, :type => String
6
+ field :created_at, :type => Time
7
+ field :updated_at, :type => Time
8
+ end
9
+ else
10
+ class Tag < ActiveRecord::Base
11
+ has_and_belongs_to_many :products
12
+ end
13
+ end
@@ -1,5 +1,17 @@
1
- class Vendor < ActiveRecord::Base
2
-
3
- has_many :products
4
-
1
+ if USE_MONGOID
2
+ class Vendor
3
+ include Mongoid::Document
4
+
5
+ has_many :products
6
+ field :name, :type => String
7
+ field :url, :type => String
8
+ field :active, :type => Boolean
9
+ field :description, :type => String
10
+ field :created_at, :type => Time
11
+ field :updated_at, :type => Time
12
+ end
13
+ else
14
+ class Vendor < ActiveRecord::Base
15
+ has_many :products
16
+ end
5
17
  end
@@ -4,6 +4,7 @@
4
4
  t.column :price
5
5
  t.column :active
6
6
  t.column :created_at
7
+ t.association :vendor, :created_at
7
8
  t.association :vendor, :name
8
9
  t.association :tags, :title
9
10
  t.association :tags, :count, :map => false, :format => '--%d--'
@@ -40,3 +40,6 @@ module DummyApp
40
40
  config.filter_parameters += [:password]
41
41
  end
42
42
  end
43
+
44
+ USE_MONGOID = File.exists?(File.join(Rails::root, 'tmp', 'use_mongoid.txt'))
45
+ puts("Using #{USE_MONGOID ? 'Mongoid' : 'ActiveRecord'}")
@@ -0,0 +1,17 @@
1
+ defaults: &defaults
2
+ host: localhost
3
+ # slaves:
4
+ # - host: slave1.local
5
+ # port: 27018
6
+ # - host: slave2.local
7
+ # port: 27019
8
+ persist_in_safe_mode: true
9
+
10
+ development:
11
+ <<: *defaults
12
+ database: tabulatr_development
13
+
14
+ test:
15
+ <<: *defaults
16
+ database: tabulatr_test
17
+
@@ -1,6 +1,10 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe "Tabulatrs" do
4
+
5
+ Mongoid.master.collections.select do |collection|
6
+ collection.name !~ /system/
7
+ end.each(&:drop)
4
8
 
5
9
  names = ["lorem", "ipsum", "dolor", "sit", "amet", "consectetur",
6
10
  "adipisicing", "elit", "sed", "eiusmod", "tempor", "incididunt", "labore",
@@ -16,7 +20,7 @@ describe "Tabulatrs" do
16
20
  # General stuf
17
21
  WORKS_IN_GENERAL = CONTAINS_BUTTONS = CONTAINS_COLUMN_HEADERS = CONTAINS_OTHER_CONTROLS = tralse
18
22
  # This fills in the data, so rather not cmment this out
19
- CONTAINS_ACTUAL_DATA = CONTAINS_ASSOC_DATA = CONTAINS_ACTUAL_DATA_MULTIPLE = CONTAINS_DATA_ON_FURTHER_PAGES = true
23
+ CONTAINS_ACTUAL_DATA = CONTAINS_ASSOC_DATA = CONTAINS_ACTUAL_DATA_MULTIPLE = CONTAINS_DATA_ON_FURTHER_PAGES = tralse
20
24
  # Paginatione
21
25
  PAGES_UP_AND_DOWN = JUMPS_TO_CORRECT_PAGE = CHANGES_PAGE_SIZE = tralse
22
26
  # Filters
@@ -24,7 +28,7 @@ describe "Tabulatrs" do
24
28
  # Sorting
25
29
  KNOWS_HOW_TO_SORT = tralse
26
30
  # Statful
27
- SORTS_STATEFULLY = FILTERS_STATEFULLY = SELECTS_STATEFULLY = true
31
+ SORTS_STATEFULLY = FILTERS_STATEFULLY = SELECTS_STATEFULLY = tralse
28
32
  # selecting and batch actions
29
33
  SELECT_BUTTONS_WORK = KNOWS_HOW_TO_SELECT_AND_APPLY_BATCH_ACTIONS = tralse
30
34
 
@@ -33,6 +37,7 @@ describe "Tabulatrs" do
33
37
  tag1 = Tag.create!(:title => 'foo')
34
38
  tag2 = Tag.create!(:title => 'bar')
35
39
  tag3 = Tag.create!(:title => 'fubar')
40
+ ids = []
36
41
 
37
42
  describe "General data" do
38
43
  it "works in general" do
@@ -52,7 +57,7 @@ describe "Tabulatrs" do
52
57
 
53
58
  it "contains column headers" do
54
59
  visit index_simple_products_path
55
- ['Id','Title','Price','Active','Created At','Vendor Name','Tags Title','Tags Count'].each do |n|
60
+ ['Id','Title','Price','Active','Created At','Vendor Created At','Vendor Name','Tags Title','Tags Count'].each do |n|
56
61
  page.should have_content(n)
57
62
  end
58
63
  end if CONTAINS_COLUMN_HEADERS
@@ -69,9 +74,11 @@ describe "Tabulatrs" do
69
74
  page.should have_content("true")
70
75
  page.should have_content("10.0")
71
76
  page.should have_content("--0--")
77
+ #save_and_open_page
72
78
  page.should have_content(sprintf(Tabulatr::TABLE_OPTIONS[:info_text], 1, 1, 0, 1))
73
79
  product.vendor = vendor1
74
80
  product.save!
81
+ ids << product.id
75
82
  visit index_simple_products_path
76
83
  page.should have_content("ven d'or")
77
84
  end if CONTAINS_ACTUAL_DATA
@@ -90,6 +97,7 @@ describe "Tabulatrs" do
90
97
  9.times do |i|
91
98
  product = Product.create!(:title => names[i+1], :active => i.even?, :price => 11.0+i,
92
99
  :description => "blah blah #{i}", :vendor => i.even? ? vendor1 : vendor2)
100
+ ids << product.id
93
101
  visit index_simple_products_path
94
102
  page.should have_content(names[i])
95
103
  page.should have_content((11.0+i).to_s)
@@ -97,10 +105,18 @@ describe "Tabulatrs" do
97
105
  end
98
106
  end if CONTAINS_ACTUAL_DATA_MULTIPLE
99
107
 
108
+ it "contains row identifiers" do
109
+ visit index_simple_products_path
110
+ Product.all.each do |product|
111
+ page.should have_css("#product_#{product.id}")
112
+ end
113
+ end if CONTAINS_ACTUAL_DATA
114
+
100
115
  it "contains the further data on the further pages" do
101
116
  names[10..-1].each_with_index do |n,i|
102
117
  product = Product.create!(:title => n, :active => i.even?, :price => 20.0+i,
103
118
  :description => "blah blah #{i}", :vendor => i.even? ? vendor1 : vendor2)
119
+ ids << product.id
104
120
  visit index_simple_products_path
105
121
  page.should_not have_content(n)
106
122
  page.should_not have_content((30.0+i).to_s)
@@ -207,6 +223,7 @@ describe "Tabulatrs" do
207
223
  click_button("Apply")
208
224
  page.should have_content(str)
209
225
  tot = (names.select do |s| s.match Regexp.new(str) end).length
226
+ #save_and_open_page
210
227
  page.should have_content(sprintf(Tabulatr::TABLE_OPTIONS[:info_text], [10,tot].min, names.length, 0, tot))
211
228
  end
212
229
  end if FILTERS_WITH_LIKE
@@ -239,6 +256,7 @@ describe "Tabulatrs" do
239
256
  describe "Sorting" do
240
257
  it "knows how to sort" do
241
258
  visit index_sort_products_path
259
+ # save_and_open_page
242
260
  (1..10).each do |i|
243
261
  page.should have_content names[-i]
244
262
  end
@@ -273,21 +291,25 @@ describe "Tabulatrs" do
273
291
  end if SORTS_STATEFULLY
274
292
 
275
293
  it "filters statefully" do
294
+ Capybara.reset_sessions!
276
295
  visit index_stateful_products_path
277
296
  fill_in("product_filter[title]", :with => "lorem")
278
297
  click_button("Apply")
279
298
  visit index_stateful_products_path
299
+ #save_and_open_page
280
300
  page.should have_content(sprintf(Tabulatr::TABLE_OPTIONS[:info_text], 1, names.length, 0, 1))
281
301
  click_button("Reset")
282
302
  page.should have_content(sprintf(Tabulatr::TABLE_OPTIONS[:info_text], 10, names.length, 0, names.length))
283
303
  end if FILTERS_STATEFULLY
284
-
304
+
285
305
  it "selects statefully" do
286
306
  visit index_stateful_products_path
307
+ fill_in("product_filter[title]", :with => "")
308
+ click_button("Apply")
287
309
  n = names.length
288
310
  (n/10).times do |i|
289
311
  (1..3).each do |j|
290
- check("product_checked_#{10*i+j}")
312
+ check("product_checked_#{ids[10*i+j]}")
291
313
  end
292
314
  click_button("Apply")
293
315
  tot = 3*(i+1)
@@ -342,13 +364,13 @@ describe "Tabulatrs" do
342
364
  click_button("Apply")
343
365
  page.should have_content(sprintf(Tabulatr::TABLE_OPTIONS[:info_text], 10, n, n-tot, n))
344
366
  end if SELECT_BUTTONS_WORK
345
-
367
+
346
368
  it "knows how to select and apply batch actions" do
347
369
  visit index_select_products_path
348
370
  n = names.length
349
371
  (n/10).times do |i|
350
372
  (1..3).each do |j|
351
- check("product_checked_#{10*i+j}")
373
+ check("product_checked_#{ids[10*i+j]}")
352
374
  end
353
375
  click_button("Apply")
354
376
  tot = 3*(i+1)
data/tabulatr.gemspec CHANGED
@@ -6,10 +6,10 @@ Gem::Specification.new do |s|
6
6
  s.name = "tabulatr"
7
7
  s.version = Tabulatr::VERSION.dup
8
8
  s.platform = Gem::Platform::RUBY
9
- s.summary = "A tight DSL to build tables of ActiveRecord models with sorting, pagination, finding/filtering, selecting and batch actions."
9
+ s.summary = "A tight DSL to build tables of ActiveRecord or Mongoid models with sorting, pagination, finding/filtering, selecting and batch actions."
10
10
  s.email = "info@provideal.net"
11
11
  s.homepage = "http://github.com/provideal/tabulatr"
12
- s.description = "A tight DSL to build tables of ActiveRecord models with sorting, pagination, finding/filtering, selecting and batch actions. " +
12
+ s.description = "A tight DSL to build tables of ActiveRecord or Mongoid models with sorting, pagination, finding/filtering, selecting and batch actions. " +
13
13
  "Tries to do for tables what formtastic and simple_form did for forms."
14
14
  s.authors = ['Peter Horn', 'René Sprotte']
15
15
 
metadata CHANGED
@@ -1,8 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tabulatr
3
3
  version: !ruby/object:Gem::Version
4
- prerelease:
5
- version: 0.1.3
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 2
8
+ - 0
9
+ version: 0.2.0
6
10
  platform: ruby
7
11
  authors:
8
12
  - Peter Horn
@@ -11,7 +15,7 @@ autorequire:
11
15
  bindir: bin
12
16
  cert_chain: []
13
17
 
14
- date: 2011-04-11 00:00:00 +02:00
18
+ date: 2011-05-26 00:00:00 +02:00
15
19
  default_executable:
16
20
  dependencies:
17
21
  - !ruby/object:Gem::Dependency
@@ -22,6 +26,9 @@ dependencies:
22
26
  requirements:
23
27
  - - ~>
24
28
  - !ruby/object:Gem::Version
29
+ segments:
30
+ - 3
31
+ - 0
25
32
  version: "3.0"
26
33
  type: :runtime
27
34
  version_requirements: *id001
@@ -33,6 +40,10 @@ dependencies:
33
40
  requirements:
34
41
  - - ">="
35
42
  - !ruby/object:Gem::Version
43
+ segments:
44
+ - 0
45
+ - 0
46
+ - 2
36
47
  version: 0.0.2
37
48
  type: :runtime
38
49
  version_requirements: *id002
@@ -44,10 +55,14 @@ dependencies:
44
55
  requirements:
45
56
  - - ">="
46
57
  - !ruby/object:Gem::Version
58
+ segments:
59
+ - 0
60
+ - 0
61
+ - 1
47
62
  version: 0.0.1
48
63
  type: :runtime
49
64
  version_requirements: *id003
50
- description: A tight DSL to build tables of ActiveRecord models with sorting, pagination, finding/filtering, selecting and batch actions. Tries to do for tables what formtastic and simple_form did for forms.
65
+ description: A tight DSL to build tables of ActiveRecord or Mongoid models with sorting, pagination, finding/filtering, selecting and batch actions. Tries to do for tables what formtastic and simple_form did for forms.
51
66
  email: info@provideal.net
52
67
  executables: []
53
68
 
@@ -88,8 +103,7 @@ files:
88
103
  - lib/tabulatr/tabulatr/data_cell.rb
89
104
  - lib/tabulatr/tabulatr/filter_cell.rb
90
105
  - lib/tabulatr/tabulatr/finder.rb
91
- - lib/tabulatr/tabulatr/finder/find_for_active_record_table.rb
92
- - lib/tabulatr/tabulatr/finder/find_for_mongoid_table.rb
106
+ - lib/tabulatr/tabulatr/finder/find_for_table.rb
93
107
  - lib/tabulatr/tabulatr/formattr.rb
94
108
  - lib/tabulatr/tabulatr/header_cell.rb
95
109
  - lib/tabulatr/tabulatr/paginator.rb
@@ -136,6 +150,7 @@ files:
136
150
  - spec/dummy_app/config/initializers/session_store.rb
137
151
  - spec/dummy_app/config/initializers/tabulatr_settings.rb
138
152
  - spec/dummy_app/config/locales/en.yml
153
+ - spec/dummy_app/config/mongoid.yml
139
154
  - spec/dummy_app/config/routes.rb
140
155
  - spec/dummy_app/db/migrate/20110112151112_create_vendors.rb
141
156
  - spec/dummy_app/db/migrate/20110112151200_create_products.rb
@@ -184,20 +199,24 @@ required_ruby_version: !ruby/object:Gem::Requirement
184
199
  requirements:
185
200
  - - ">="
186
201
  - !ruby/object:Gem::Version
202
+ segments:
203
+ - 0
187
204
  version: "0"
188
205
  required_rubygems_version: !ruby/object:Gem::Requirement
189
206
  none: false
190
207
  requirements:
191
208
  - - ">="
192
209
  - !ruby/object:Gem::Version
210
+ segments:
211
+ - 0
193
212
  version: "0"
194
213
  requirements: []
195
214
 
196
215
  rubyforge_project: tabulatr
197
- rubygems_version: 1.5.0
216
+ rubygems_version: 1.3.7
198
217
  signing_key:
199
218
  specification_version: 3
200
- summary: A tight DSL to build tables of ActiveRecord models with sorting, pagination, finding/filtering, selecting and batch actions.
219
+ summary: A tight DSL to build tables of ActiveRecord or Mongoid models with sorting, pagination, finding/filtering, selecting and batch actions.
201
220
  test_files:
202
221
  - spec/dummy_app/.gitignore
203
222
  - spec/dummy_app/app/controllers/application_controller.rb
@@ -239,6 +258,7 @@ test_files:
239
258
  - spec/dummy_app/config/initializers/session_store.rb
240
259
  - spec/dummy_app/config/initializers/tabulatr_settings.rb
241
260
  - spec/dummy_app/config/locales/en.yml
261
+ - spec/dummy_app/config/mongoid.yml
242
262
  - spec/dummy_app/config/routes.rb
243
263
  - spec/dummy_app/db/migrate/20110112151112_create_vendors.rb
244
264
  - spec/dummy_app/db/migrate/20110112151200_create_products.rb
@@ -1,116 +0,0 @@
1
- #--
2
- # Copyright (c) 2010-2011 Peter Horn, Provideal GmbH
3
- #
4
- # Permission is hereby granted, free of charge, to any person obtaining
5
- # a copy of this software and associated documentation files (the
6
- # "Software"), to deal in the Software without restriction, including
7
- # without limitation the rights to use, copy, modify, merge, publish,
8
- # distribute, sublicense, and/or sell copies of the Software, and to
9
- # permit persons to whom the Software is furnished to do so, subject to
10
- # the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be
13
- # included in all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
- # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
- # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
- # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
- # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
- # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
- #++
23
-
24
- # These are extensions for use from ActionController instances
25
- # In a seperate class call only for clearity
26
- module Tabulatr::Finder
27
-
28
- # ----------------------------------------------------------------
29
- # Called if SomeMongoidDocument::find_for_table(params) is called
30
- #
31
- def self.find_for_mongoid_table(klaz, params, opts={})
32
- # firstly, get the conditions from the filters
33
- cname = class_to_param(klaz)
34
- filter_param = (params["#{cname}#{TABLE_FORM_OPTIONS[:filter_postfix]}"] || {})
35
- conditions = filter_param.inject({}) do |c, t|
36
- n, v = t
37
- nc = c
38
- # FIXME n = name_escaping(n)
39
- raise "SECURITY violation, field name is '#{n}'" unless /^[\d\w]+$/.match n
40
- if v.class == String
41
- if v.present?
42
- nc[n.to_sym] = v
43
- end
44
- elsif v.is_a?(Hash)
45
- if v[:like]
46
- if v[:like].present?
47
- nc[n.to_sym] = Regexp.new("#{v[:like]}")
48
- end
49
- else
50
- nc[n.to_sym.gte] = "#{v[:from]}" if v[:from].present?
51
- nc[n.to_sym.lte] = "#{v[:to]}" if v[:to].present?
52
- end
53
- else
54
- raise "Wrong filter type: #{v.class}"
55
- end
56
- nc
57
- end
58
-
59
- # secondly, find the order_by stuff
60
- # FIXME: Implement me! PLEEEZE!
61
- sortparam = params["#{cname}#{TABLE_FORM_OPTIONS[:sort_postfix]}"]
62
- if sortparam
63
- if sortparam[:_resort]
64
- order_by = sortparam[:_resort].first.first
65
- order_direction = sortparam[:_resort].first.last.first.first
66
- else
67
- order_by = sortparam.first.first
68
- order_direction = sortparam.first.last.first.first
69
- end
70
- raise "SECURITY violation, sort field name is '#{n}'" unless /^[\w]+$/.match order_direction
71
- raise "SECURITY violation, sort field name is '#{n}'" unless /^[\d\w]+$/.match order_by
72
- order = [order_by, order_direction]
73
- else
74
- order = nil
75
- end
76
-
77
- # thirdly, get the pagination data
78
- paginate_options = PAGINATE_OPTIONS.merge(opts).
79
- merge(params["#{cname}#{TABLE_FORM_OPTIONS[:pagination_postfix]}"] || {})
80
- page = paginate_options[:page].to_i
81
- page += 1 if paginate_options[:page_right]
82
- page -= 1 if paginate_options[:page_left]
83
- pagesize = paginate_options[:pagesize].to_f
84
- c = klaz.count :conditions => conditions
85
- pages = (c/pagesize).ceil
86
- page = [1, [page, pages].min].max
87
-
88
- # Now, actually find the stuff
89
- found = klaz.find(:conditions => conditions)
90
- found = found.order_by([order]) if order
91
- found = found.paginate(:page => page, :per_page => pagesize)
92
-
93
- # finally, inject methods to retrieve the current 'settings'
94
- found.define_singleton_method(fio[:filters]) do filter_param end
95
- found.define_singleton_method(fio[:classname]) do cname end
96
- found.define_singleton_method(fio[:pagination]) do
97
- {:page => page, :pagesize => pagesize, :count => c, :pages => pages,
98
- :pagesizes => paginate_options[:pagesizes]}
99
- end
100
- found.define_singleton_method(fio[:sorting]) do
101
- order ? { :by => order_by, :direction => order_direction } : nil
102
- end
103
- checked_param = params["#{cname}#{TABLE_FORM_OPTIONS[:checked_postfix]}"]
104
- checked_ids = checked_param[:checked].split(TABLE_FORM_OPTIONS[:checked_separator])
105
- new_ids = checked_param[:current_page] || []
106
- selected_ids = checked_ids + new_ids
107
- ids = found.map { |r| r.id.to_s }
108
- checked_ids = selected_ids - ids
109
- found.define_singleton_method(fio[:checked]) do
110
- { :selected => selected_ids,
111
- :checked_ids => checked_ids.join(TABLE_FORM_OPTIONS[:checked_separator]) }
112
- end
113
- found
114
- end
115
-
116
- end