maiha-active_record_view 0.1

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.
Files changed (42) hide show
  1. data/README +25 -0
  2. data/Rakefile +22 -0
  3. data/arv/properties/boolean +4 -0
  4. data/arv/properties/date +5 -0
  5. data/arv/properties/datetime +5 -0
  6. data/arv/properties/default +3 -0
  7. data/arv/properties/integer +6 -0
  8. data/arv/properties/string +4 -0
  9. data/arv/properties/text +7 -0
  10. data/arv/properties/time +5 -0
  11. data/arv/properties/timestamp +5 -0
  12. data/arv/property.erb +19 -0
  13. data/init.rb +0 -0
  14. data/install.rb +1 -0
  15. data/lib/active_record_view.rb +2 -0
  16. data/lib/active_record_view/controller.rb +9 -0
  17. data/lib/active_record_view/default_actions.rb +29 -0
  18. data/lib/active_record_view/engines.rb +68 -0
  19. data/lib/active_record_view/helper.rb +152 -0
  20. data/lib/active_record_view/record_identifier.rb +42 -0
  21. data/lib/active_record_view/render_arv.rb +51 -0
  22. data/lib/active_record_view/write_exception.rb +11 -0
  23. data/lib/localized/model.rb +103 -0
  24. data/lib/localized/view_property.rb +337 -0
  25. data/spec/fixtures/card.rb +12 -0
  26. data/spec/fixtures/card/equip.rb +13 -0
  27. data/spec/fixtures/card/magic.rb +13 -0
  28. data/spec/fixtures/card/member.rb +13 -0
  29. data/spec/fixtures/card/unit.rb +13 -0
  30. data/spec/fixtures/deck.rb +2 -0
  31. data/spec/localized/decks.yml +39 -0
  32. data/spec/migrate/001_create_cards.rb +16 -0
  33. data/spec/migrate/002_create_decks.rb +5 -0
  34. data/spec/models/active_record_view_helper_spec.rb +393 -0
  35. data/spec/models/render_spec.rb +220 -0
  36. data/spec/schema.rb +30 -0
  37. data/spec/spec.opts +6 -0
  38. data/spec/spec_helper.rb +27 -0
  39. data/tasks/active_record_view_tasks.rake +37 -0
  40. data/test/active_record_view_test.rb +8 -0
  41. data/uninstall.rb +1 -0
  42. metadata +93 -0
@@ -0,0 +1,51 @@
1
+ module ActiveRecordView
2
+ module RenderArv
3
+ def render(*args, &block)
4
+ options = args.first
5
+ if options.is_a?(Hash) and options[:arv]
6
+ arv = options.delete(:arv)
7
+ render_arv(arv, options)
8
+ else
9
+ super
10
+ end
11
+ end
12
+
13
+ def render_arv(arv, options = {})
14
+ path, arv_name = partial_pieces(arv.to_s)
15
+ root = Pathname(RAILS_ROOT).cleanpath.to_s + "/"
16
+ file = "app/views/%s/%s.arv" % [path, arv_name]
17
+ rhtml = arv_erb_code(File.read(root + file), arv_name, options)
18
+ ActiveRecord::Base.logger.debug "Rendering %s" % file.inspect
19
+ render :inline=>rhtml
20
+ end
21
+
22
+ private
23
+ def arv_erb_code(buffer, arv_name, options)
24
+ html = ''
25
+ array = buffer.scan(/^([^#]\S+?)\s*=(.*?)$/m)
26
+ names = array.map(&:first)
27
+ leads = array.map{|a| a.last.to_s.strip}
28
+
29
+ names.each_with_index do |name, i|
30
+ next if (lead = leads[i]).blank? and (i > 0)
31
+ colspan = 1 + (leads[i+1..-1].map(&:blank?)+[false]).index(false)
32
+ html << "<th class='%s' colspan=%d>%s</th>" % [name, colspan, lead]
33
+ end
34
+ options[:class] = "arv-list #{arv_name} #{options[:class]}".strip
35
+ content_tag(:table, <<-ERB, options)
36
+ <thead><tr>#{html}</tr></thead>
37
+ <%= collection_tbody_for(@#{arv_name}, %w( #{names.join(' ')} )) %>
38
+ ERB
39
+ end
40
+
41
+ # derived from Rails1.2 for Rails2.1
42
+ def partial_pieces(partial_path)
43
+ if partial_path.include?('/')
44
+ return File.dirname(partial_path), File.basename(partial_path)
45
+ else
46
+ return controller.class.controller_path, partial_path
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveRecordView::WriteException
2
+ def write_exception_logger
3
+ ActionController::Base.logger
4
+ end
5
+
6
+ def write_exception(error, size = 5)
7
+ user_backtrace = error.application_backtrace.reject{|i| i =~ %r{^\#\{RAILS_ROOT\}/vendor/}}
8
+ trace = user_backtrace[0,size - 1].join("\n")
9
+ write_exception_logger.error "%s: %s (%s)" % [error.class, error.message, trace]
10
+ end
11
+ end
@@ -0,0 +1,103 @@
1
+ class Localized::Model
2
+ attr_reader :yaml_path, :active_record
3
+ delegate :logger, :to=>"ActiveRecord::Base"
4
+ cattr_accessor :yaml_path
5
+ if defined?(RAILS_ROOT)
6
+ self.yaml_path = File.join(RAILS_ROOT, "db/localized")
7
+ end
8
+
9
+ class ConfigurationError < ActionView::ActionViewError; end
10
+ class << self
11
+ def [](model_name)
12
+ klass = active_record_class_for(model_name)
13
+ @localized_models ||= {}
14
+ @localized_models[klass.table_name] ||= new(klass)
15
+ end
16
+
17
+ def active_record_class_for(klass)
18
+ case klass
19
+ when Class
20
+ # nop
21
+ when ActiveRecord::Base
22
+ klass = klass.class
23
+ else
24
+ klass = klass.to_s.classify.constantize
25
+ end
26
+
27
+ klass.ancestors.include?(ActiveRecord::Base) or
28
+ raise ConfigurationError, "#{name}[] expects ActiveRecord class, but got #{klass.name}"
29
+
30
+ return klass
31
+ end
32
+
33
+ def human_value(record, column_name)
34
+ self[record.class].view_property(column_name).human_value(record[column_name])
35
+ end
36
+
37
+ def masters(model_name, column_name)
38
+ self[model_name].view_property(column_name).masters
39
+ end
40
+ end
41
+
42
+ def initialize(active_record)
43
+ @active_record = active_record
44
+ @table_name = @active_record.table_name
45
+ @view_properties = {}
46
+ @yaml = YAML::load_file(absolute_yaml_path)
47
+ logger.debug("Loaded localized setting from '#{absolute_yaml_path}'")
48
+ rescue Errno::ENOENT
49
+ raise ConfigurationError, "Cannot read YAML data from #{absolute_yaml_path}.\nRun 'rake arv:create:view %s'" % @active_record
50
+ rescue ArgumentError => err
51
+ logger.debug("Localize Error: YAML #{err} in #{yaml_path}")
52
+ @load_error = true
53
+ end
54
+
55
+ def [] (group_name, attr_name = nil)
56
+ return nil if load_error?
57
+ localized_name = @yaml[group_name.to_s]
58
+ localized_name = localized_name[attr_name] if attr_name
59
+ return localized_name
60
+ rescue => err
61
+ logger.debug("Localize Error: for (%s,%s). %s" % [group_name, attr_name, err])
62
+ return nil
63
+ end
64
+
65
+ def instance_name
66
+ self[:names][:instance]
67
+ end
68
+
69
+ def masters
70
+ pkey = active_record.primary_key
71
+ options = active_record.columns_hash[instance_name.to_s] && {:select=>"#{pkey},#{instance_name}"} || {}
72
+ active_record.find(:all, options.merge(:order=>pkey)).collect{|r| [r[pkey], localize_instance(r)]}
73
+ end
74
+
75
+ def localize_instance (record)
76
+ name = instance_name
77
+ case name
78
+ when NilClass ; return nil
79
+ when Symbol ; return record.send(name)
80
+ when String ; return name.gsub('%d', record.id.to_s)
81
+ else
82
+ raise TypeError, "got %s, expected Symbol or String. Check '%s'" % [name.class, yaml_path]
83
+ end
84
+ end
85
+
86
+ def load_error?
87
+ @load_error
88
+ end
89
+
90
+ def view_property(column_name)
91
+ @view_properties[column_name] ||= Localized::ViewProperty.new(self, column_name, self["property_#{column_name}"])
92
+ end
93
+
94
+ private
95
+ def absolute_yaml_path
96
+ @absolute_yaml_path ||= (Pathname(self.class.yaml_path) + yaml_path).cleanpath
97
+ end
98
+
99
+ def yaml_path
100
+ "#{@table_name}.yml"
101
+ end
102
+
103
+ end
@@ -0,0 +1,337 @@
1
+ class Localized::ViewProperty
2
+ attr_reader :options, :model, :master_class, :column_name, :master_hash
3
+
4
+ class << self
5
+ def [](model_name, column_name)
6
+ model = find_model(model_name)
7
+ if model
8
+ model.view_property(column_name)
9
+ else
10
+ nil
11
+ end
12
+ end
13
+
14
+ private
15
+ def find_model(name)
16
+ @cached_models ||= {}
17
+ case (model = @cached_models[name])
18
+ when :missing
19
+ return nil
20
+ when Localized::Model
21
+ return model
22
+ when NilClass
23
+ begin
24
+ return @cached_models[name] = Localized::Model[name]
25
+ rescue Localized::Model::ConfigurationError
26
+ @cached_models[name] = :missing
27
+ return nil
28
+ end
29
+ else
30
+ raise "[BUG] Localized::ViewProperty is broken! (cannot find model for #{name})"
31
+ end
32
+ end
33
+ end
34
+
35
+ def initialize(model, column_name, hash = nil)
36
+ hash ||= {}
37
+
38
+ @blank = true if hash.blank?
39
+ @model = model
40
+ @column_name = column_name
41
+ @yaml_data = hash
42
+ @options = hash[:options] || {}
43
+ @master_array = []
44
+ @master_hash = {}
45
+ @master_class = nil
46
+
47
+ parse_master
48
+ @include_blank = @master_array.first.first.to_s.empty? rescue false
49
+ end
50
+
51
+ def blank?
52
+ @blank
53
+ end
54
+
55
+ protected
56
+ def parse_master
57
+ master = @yaml_data[:masters]
58
+ @master_array = []
59
+ @master_hash = {}
60
+ @master_class = nil
61
+
62
+ case master
63
+ when NilClass
64
+ when String
65
+ model = Localized::Model[master]
66
+ @master_class = model.active_record
67
+ @master_array = model.masters
68
+ @master_array.each do |key, val|
69
+ @master_hash[key] = val
70
+ end
71
+ when Array
72
+ master.each do |hash|
73
+ key, val = hash.to_a.first
74
+ @master_array << [key, val]
75
+ @master_hash[key] = val
76
+ end
77
+ else
78
+ raise Localized::Model::ConfigurationError,
79
+ "Cannot accept '%s' as master. It should be an Array or :belongs_to or a String(ActiveRecord class name). Check %s." % [master.class, @model.yaml_path]
80
+ end
81
+ end
82
+
83
+ public
84
+ def [](key)
85
+ @yaml_data[key]
86
+ end
87
+
88
+ def masters
89
+ @master_array
90
+ end
91
+
92
+ def master(value)
93
+ @master_hash[value]
94
+ end
95
+
96
+ def reload_master
97
+ parse_master
98
+ return masters
99
+ end
100
+
101
+ def has_master?
102
+ not masters.empty?
103
+ end
104
+
105
+ def include_blank?
106
+ @include_blank
107
+ end
108
+
109
+ def has_time_format?
110
+ self[:time_format]
111
+ end
112
+
113
+ def has_format?(postfix = nil)
114
+ self["format#{postfix}".intern] || self[:format]
115
+ end
116
+
117
+ def has_column_type?
118
+ self[:column_type].is_a?(Symbol)
119
+ end
120
+
121
+ def column_type
122
+ (has_column_type? && self[:column_type]) || (has_master? && :master) || nil
123
+ end
124
+
125
+ def system_column_type
126
+ column = klass.columns_hash[column_name.to_s]
127
+ column ? column.type : nil
128
+ rescue
129
+ nil
130
+ end
131
+
132
+ def klass
133
+ model.active_record
134
+ end
135
+
136
+
137
+ # TODO: split to property model and rendering engine
138
+ include ActionView::Helpers::TagHelper # for content_tag
139
+ include ActionView::Helpers::FormHelper # for check_box
140
+ include ActionView::Helpers::FormTagHelper # for check_box_tag
141
+
142
+
143
+ ######################################################################
144
+ ### Rendering
145
+
146
+ def human_value (value, controller = nil)
147
+ controller &&= controller.is_a?(ActionController::Base) ? controller : controller.controller
148
+
149
+ case column_type
150
+ when :acts_as_bits
151
+ aab_names = klass.send("#{column_name.to_s.singularize}_names")
152
+ checkeds = Hash[*aab_names.zip(value.to_s.split(//)).flatten]
153
+
154
+ aab_masters = self.masters
155
+ aab_masters = klass.send("#{column_name.to_s.singularize}_names_with_labels") if aab_masters.blank?
156
+
157
+ type = :button
158
+ case type
159
+ when :button
160
+ lis = aab_masters.map{|(name, title)|
161
+ style = (checkeds[name].to_i == 1) ? "checked" : "unchecked"
162
+ span = content_tag(:span, title || name)
163
+ content_tag(:li, span, :class=>style)
164
+ }
165
+ return content_tag(:ul, lis.join(' '), :class=>"aab")
166
+ when :checkbox
167
+ options = (options||{}).merge(:disabled=>"disabled")
168
+ html = masters.map{|(name, title)|
169
+ check = check_box_tag("aab", 1, (checkeds[name].to_i==1), options)
170
+ label = h(title || name)
171
+ '<span style="white-space: nowrap;">%s %s</span>' % [check, label]
172
+ }.join(" &nbsp;&nbsp; ")
173
+ return html
174
+ end
175
+ end
176
+
177
+ if has_master?
178
+ html = master(value)
179
+ elsif has_time_format?
180
+ html = [Date, Time].include?(value.class) ? value.strftime(self[:time_format]) : ''
181
+ elsif controller && format = has_format?("_" + controller.action_name)
182
+ html = format % ERB::Util.html_escape(value)
183
+ elsif (column_type || system_column_type) == :text
184
+ html = ERB::Util.html_escape(value.strip.to_s).gsub(/\r?\n/,'<BR>')
185
+ else
186
+ html = ERB::Util.html_escape(value)
187
+ end
188
+ end
189
+
190
+
191
+ def human_edit(singular_name, view, opts = {})
192
+ record = opts.delete(:record) || view.instance_variable_get("@#{singular_name}")
193
+ options = self.options.merge(opts)
194
+ options[:class] = "#{column_name} #{options[:class]}".strip
195
+
196
+ if time_format = has_time_format?
197
+ value = record.send(column_name)
198
+ if edit_format = has_format?("_edit")
199
+ return human_edit_time_text_field_with_format(view, edit_format, value, singular_name)
200
+ else
201
+ return human_edit_time_with_format(view, time_format, value, singular_name)
202
+ end
203
+ end
204
+
205
+ case column_type
206
+ when :acts_as_bits
207
+ aab_masters = self.masters
208
+ aab_masters = klass.send("#{column_name.to_s.singularize}_names_with_labels") if aab_masters.blank?
209
+
210
+ html = aab_masters.map{|(name, title)|
211
+ check = view.check_box(singular_name, name, options) rescue "(#{name}?)"
212
+ label = view.send(:h, title || name)
213
+ label = view.send(:content_tag, :label, label, :for=>"#{singular_name}_#{name}")
214
+ '<span style="white-space: nowrap;">%s %s</span>' % [check, label]
215
+ }.join(" &nbsp;&nbsp; ")
216
+ when :acts_as_tree
217
+ value = record.send(column_name)
218
+ record = master_class.find(value) rescue nil
219
+ html = view.acts_as_tree_field(singular_name, column_name, master_class, record)
220
+ when :checkbox, :check_box
221
+ html = view.check_box(singular_name, column_name, options)
222
+ when :radio, :radio_button
223
+ separater = "&nbsp;"
224
+ delimiter = "&nbsp;&nbsp;"
225
+ html = masters.map{|key,val|
226
+ [
227
+ view.radio_button(singular_name, column_name, key, options).to_s,
228
+ # content_tag(:label, val.to_s, :for=>"#{singular_name}_#{column_name}_#{key}")
229
+ val.to_s
230
+ ].join(separater)
231
+ } * delimiter
232
+
233
+ # add following line to your css to avoid blocked radio button in AR error
234
+ # div.fieldWithErrors .radio-group {border:1px solid #FF0000;}
235
+ # .radio-group div.fieldWithErrors {display : inline;}
236
+
237
+ html = content_tag :div, html, :class=>"radio-group"
238
+ html = content_tag :div, html, :class=>"fieldWithErrors" if record.errors.on(column_name)
239
+
240
+ when :master
241
+ html = view.collection_select(singular_name, column_name, masters, :first, :last, options)
242
+ when :time
243
+ tag = ActionView::Helpers::InstanceTag.new(singular_name, column_name, view)
244
+ html = tag.to_time_select_tag(options)
245
+ when NilClass
246
+ if system_column_type
247
+ tag = ActionView::Helpers::InstanceTag.new(singular_name, column_name, view, view, record)
248
+ html = tag.to_tag(options)
249
+ else
250
+ if view.respond_to?(column_name)
251
+ view.send(column_name, record)
252
+ else
253
+ view.text_field_tag "#{singular_name}[#{column_name}]", record[column_name]
254
+ end
255
+ end
256
+ else
257
+ tag = ActionView::Helpers::InstanceTag.new(singular_name, column_name, view, view, record)
258
+ tag.instance_eval("def column_type; :%s; end" % self[:column_type])
259
+ html = tag.to_tag(options)
260
+ end
261
+
262
+ # if format = has_format?("_" + view.controller.action_name)
263
+ # html = format % html
264
+ # end
265
+
266
+ return html
267
+ end
268
+
269
+ protected
270
+ def human_edit_time_with_format(view, time_format, value, singular_name)
271
+ used = Set.new
272
+ opts = Proc.new { |position|
273
+ used << position
274
+ options.merge(:prefix => singular_name, :field_name=>"#{column_name}(#{position}i)")}
275
+
276
+ html = time_format.gsub(/%([YmdHMS])/) do
277
+ case $1
278
+ when 'Y'; view.select_year(value, opts.call(1))
279
+ when 'm'; view.select_month(value, opts.call(2))
280
+ when 'd'; view.select_day(value, opts.call(3))
281
+ when 'H'; view.select_hour(value, opts.call(4))
282
+ when 'M'; view.select_minute(value, opts.call(5))
283
+ when 'S'; view.select_second(value, opts.call(6))
284
+ end
285
+ end
286
+
287
+ hidden = (1...used.min).map{|i|
288
+ name = "%s[%s]" % opts.call(i).values_at(:prefix, :field_name)
289
+ view.hidden_field_tag(name, 1)}.join
290
+ return hidden + html
291
+ end
292
+
293
+ def human_edit_time_text_field_with_format(view, time_format, value, singular_name)
294
+ used = Set.new
295
+ name = Proc.new { |position|
296
+ used << position
297
+ "%s[%s(%di)]" % [singular_name, column_name, position]}
298
+ time = Proc.new { |time_object, method, zero|
299
+ begin
300
+ val = time_object.__send__(method).to_i
301
+ val = "%02d" % val if zero
302
+ val
303
+ rescue
304
+ ''
305
+ end
306
+ }
307
+ opts = proc {|*args| size, klass = args
308
+ hash = options.merge(:style=>"width:#{size}px;")
309
+ hash.merge!(:class=>"#{hash[:class]} #{klass}") if klass
310
+ hash
311
+ }
312
+
313
+ html = time_format.gsub(/%(0?)(\d*)([YmdHMS])/) do
314
+ zero = !$1.blank?
315
+ size = ($2.blank? ? 2 : $2).to_i*10+5
316
+ case $3
317
+ when 'Y'; view.text_field_tag(name.call(1), time.call(value,:year,zero), opts.call(size))
318
+ when 'm'; view.text_field_tag(name.call(2), time.call(value,:month,zero), opts.call(size))
319
+ when 'd'; view.text_field_tag(name.call(3), time.call(value,:day,zero), opts.call(size))
320
+ when 'H'; view.text_field_tag(name.call(4), time.call(value,:hour,zero), opts.call(size, "time-hour"))
321
+ when 'M'; view.text_field_tag(name.call(5), time.call(value,:min,zero), opts.call(size, "time-minute"))
322
+ when 'S'; view.text_field_tag(name.call(6), time.call(value,:sec,zero), opts.call(size))
323
+ end
324
+ end
325
+
326
+ hidden = (1...used.min).map{|i| view.hidden_field_tag(name.call(i), 1)}.join
327
+ return hidden + html
328
+ end
329
+
330
+ def configuration_error(message)
331
+ raise Localized::Model::ConfigurationError, "%s (check 'property_%s' in %s)" %
332
+ [message, column_name, model.yaml_path]
333
+ end
334
+ end
335
+
336
+
337
+