maiha-active_record_view 0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+