netzke-basepack 0.7.4 → 0.7.5

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 (74) hide show
  1. data/.travis.yml +11 -0
  2. data/CHANGELOG.rdoc +10 -0
  3. data/README.md +36 -2
  4. data/Rakefile +1 -3
  5. data/config/ci/before-travis.sh +28 -0
  6. data/lib/netzke/active_record.rb +10 -8
  7. data/lib/netzke/active_record/attributes.rb +28 -17
  8. data/lib/netzke/active_record/relation_extensions.rb +3 -1
  9. data/lib/netzke/basepack.rb +10 -2
  10. data/lib/netzke/basepack/action_column.rb +6 -8
  11. data/lib/netzke/basepack/data_accessor.rb +11 -174
  12. data/lib/netzke/basepack/data_adapters/abstract_adapter.rb +164 -0
  13. data/lib/netzke/basepack/data_adapters/active_record_adapter.rb +279 -0
  14. data/lib/netzke/basepack/data_adapters/data_mapper_adapter.rb +264 -0
  15. data/lib/netzke/basepack/data_adapters/sequel_adapter.rb +260 -0
  16. data/lib/netzke/basepack/form_panel.rb +3 -3
  17. data/lib/netzke/basepack/form_panel/fields.rb +6 -10
  18. data/lib/netzke/basepack/form_panel/javascripts/form_panel.js +1 -0
  19. data/lib/netzke/basepack/form_panel/services.rb +15 -16
  20. data/lib/netzke/basepack/grid_panel.rb +16 -10
  21. data/lib/netzke/basepack/grid_panel/columns.rb +6 -7
  22. data/lib/netzke/basepack/grid_panel/javascripts/event_handling.js +29 -27
  23. data/lib/netzke/basepack/grid_panel/services.rb +13 -90
  24. data/lib/netzke/basepack/paging_form_panel.rb +3 -3
  25. data/lib/netzke/basepack/query_builder.rb +2 -0
  26. data/lib/netzke/basepack/query_builder/javascripts/query_builder.js +29 -19
  27. data/lib/netzke/basepack/search_panel.rb +6 -3
  28. data/lib/netzke/basepack/search_panel/javascripts/search_panel.js +2 -1
  29. data/lib/netzke/basepack/search_window.rb +2 -1
  30. data/lib/netzke/basepack/version.rb +1 -1
  31. data/lib/netzke/data_mapper.rb +18 -0
  32. data/lib/netzke/data_mapper/attributes.rb +273 -0
  33. data/lib/netzke/data_mapper/combobox_options.rb +11 -0
  34. data/lib/netzke/data_mapper/relation_extensions.rb +38 -0
  35. data/lib/netzke/sequel.rb +18 -0
  36. data/lib/netzke/sequel/attributes.rb +274 -0
  37. data/lib/netzke/sequel/combobox_options.rb +10 -0
  38. data/lib/netzke/sequel/relation_extensions.rb +40 -0
  39. data/netzke-basepack.gemspec +24 -13
  40. data/test/basepack_test_app/Gemfile +33 -8
  41. data/test/basepack_test_app/Gemfile.lock +98 -79
  42. data/test/basepack_test_app/Guardfile +46 -0
  43. data/test/basepack_test_app/app/components/book_grid_with_persistence.rb +3 -0
  44. data/test/basepack_test_app/app/components/extras/book_presentation.rb +10 -3
  45. data/test/basepack_test_app/app/models/address.rb +27 -1
  46. data/test/basepack_test_app/app/models/author.rb +28 -0
  47. data/test/basepack_test_app/app/models/book.rb +43 -0
  48. data/test/basepack_test_app/app/models/book_with_custom_primary_key.rb +22 -0
  49. data/test/basepack_test_app/app/models/role.rb +21 -0
  50. data/test/basepack_test_app/app/models/user.rb +24 -0
  51. data/test/basepack_test_app/config/database.yml.sample +11 -10
  52. data/test/basepack_test_app/config/database.yml.travis +15 -0
  53. data/test/basepack_test_app/config/initializers/data_mapper_logging.rb +3 -0
  54. data/test/basepack_test_app/config/initializers/sequel.rb +26 -0
  55. data/test/basepack_test_app/db/schema.rb +0 -3
  56. data/test/basepack_test_app/features/grid_panel.feature +28 -8
  57. data/test/basepack_test_app/features/grid_sorting.feature +6 -6
  58. data/test/basepack_test_app/features/paging_form_panel.feature +13 -13
  59. data/test/basepack_test_app/features/search_in_grid.feature +31 -31
  60. data/test/basepack_test_app/features/step_definitions/generic_steps.rb +3 -1
  61. data/test/basepack_test_app/features/support/env.rb +17 -4
  62. data/test/basepack_test_app/lib/tasks/travis.rake +7 -0
  63. data/test/basepack_test_app/spec/components/form_panel_spec.rb +2 -2
  64. data/test/basepack_test_app/spec/data_adapter/adapter_spec.rb +68 -0
  65. data/test/basepack_test_app/spec/{active_record → data_adapter}/attributes_spec.rb +12 -4
  66. data/test/basepack_test_app/spec/data_adapter/relation_extensions_spec.rb +125 -0
  67. data/test/basepack_test_app/spec/spec_helper.rb +9 -0
  68. data/test/unit/active_record_basepack_test.rb +1 -1
  69. data/test/unit/grid_panel_test.rb +1 -1
  70. metadata +26 -31
  71. data/app/models/netzke_field_list.rb +0 -261
  72. data/app/models/netzke_model_attr_list.rb +0 -21
  73. data/app/models/netzke_persistent_array_auto_model.rb +0 -57
  74. data/test/basepack_test_app/spec/active_record/relation_extensions_spec.rb +0 -44
@@ -11,7 +11,8 @@
11
11
  buildFormFromQuery: function(query) {
12
12
  this.onClearAll();
13
13
  Ext.each(query, function(f){
14
- this.insert(this.items.length - 1, Ext.apply(f, {xtype: 'netzkebasepacksearchpanelconditionfield'}));
14
+ f.ownerCt = this;
15
+ this.insert(this.items.length - 1, Ext.createByAlias('widget.netzkebasepacksearchpanelconditionfield', f));
15
16
  }, this);
16
17
  this.doLayout();
17
18
  },
@@ -27,7 +27,8 @@ module Netzke
27
27
  component :search_panel do
28
28
  {
29
29
  :class_name => "Netzke::Basepack::QueryBuilder",
30
- :model => config[:model]
30
+ :model => config[:model],
31
+ :fields => config[:fields]
31
32
  }
32
33
  end
33
34
 
@@ -3,7 +3,7 @@ module Netzke
3
3
  module Version
4
4
  MAJOR = 0
5
5
  MINOR = 7
6
- PATCH = 4
6
+ PATCH = 5
7
7
 
8
8
  STRING = [MAJOR, MINOR, PATCH].compact.join('.')
9
9
  end
@@ -0,0 +1,18 @@
1
+ require 'netzke/data_mapper/attributes'
2
+ require 'netzke/data_mapper/combobox_options'
3
+ require 'netzke/data_mapper/relation_extensions'
4
+
5
+ module Netzke
6
+ module DataMapper
7
+ end
8
+ end
9
+
10
+ if defined? DataMapper
11
+ # Extend DataMapper
12
+
13
+ DataMapper::Model.append_extensions(Netzke::DataMapper::Attributes::ClassMethods)
14
+ DataMapper::Model.append_inclusions(Netzke::DataMapper::Attributes)
15
+ DataMapper::Model.append_extensions(Netzke::DataMapper::ComboboxOptions)
16
+ DataMapper::Model.append_extensions(Netzke::DataMapper::RelationExtensions)
17
+ end
18
+
@@ -0,0 +1,273 @@
1
+ module Netzke
2
+ module DataMapper
3
+ module Attributes
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :netzke_declared_attr
8
+ self.netzke_declared_attr = []
9
+
10
+ class_attribute :netzke_excluded_attr
11
+ self.netzke_excluded_attr = []
12
+
13
+ class_attribute :netzke_exposed_attr
14
+ end
15
+
16
+ module ClassMethods
17
+ def data_adapter
18
+ Netzke::Basepack::DataAdapters::AbstractAdapter.adapter_class(self).new(self)
19
+ end
20
+
21
+ # Define or configure an attribute.
22
+ # Example:
23
+ # netzke_attribute :recent, :type => :boolean, :read_only => true
24
+ def netzke_attribute(name, options = {})
25
+ name = name.to_s
26
+ options[:attr_type] = options.delete(:type) || options.delete(:attr_type) || :string
27
+ declared_attrs = self.netzke_declared_attr.dup
28
+ # if the attr was declared already, simply merge it with the new options
29
+ existing = declared_attrs.detect{ |va| va[:name] == name }
30
+ if existing
31
+ existing.merge!(options)
32
+ else
33
+ attr_config = {:name => name}.merge(options)
34
+ # if primary_key, insert in front, otherwise append
35
+ if name == self.primary_key
36
+ declared_attrs.insert(0, attr_config)
37
+ else
38
+ declared_attrs << {:name => name}.merge(options)
39
+ end
40
+ end
41
+ self.netzke_declared_attr = declared_attrs
42
+ end
43
+
44
+ # Exclude attributes from being picked up by grids and forms.
45
+ # Accepts an array of attribute names (as symbols).
46
+ # Example:
47
+ # netzke_expose_attributes :created_at, :updated_at, :crypted_password
48
+ def netzke_exclude_attributes(*args)
49
+ self.netzke_excluded_attr = args.map(&:to_s)
50
+ end
51
+
52
+ # Explicitly expose attributes that should be picked up by grids and forms.
53
+ # Accepts an array of attribute names (as symbols).
54
+ # Takes precedence over <tt>netzke_exclude_attributes</tt>.
55
+ # Example:
56
+ # netzke_expose_attributes :name, :role__name
57
+ def netzke_expose_attributes(*args)
58
+ self.netzke_exposed_attr = args.map(&:to_s)
59
+ end
60
+
61
+ # Returns the attributes that will be picked up by grids and forms.
62
+ def netzke_attributes
63
+ exposed = netzke_exposed_attributes
64
+ exposed ? netzke_attrs_in_forced_order(exposed) : netzke_attrs_in_natural_order
65
+ end
66
+
67
+ def netzke_attribute_hash
68
+ netzke_attributes.inject({}){ |r,a| r.merge(a[:name].to_sym => a) }
69
+ end
70
+
71
+ def netzke_exposed_attributes
72
+ exposed = self.netzke_exposed_attr
73
+ if exposed && !exposed.include?(self.primary_key)
74
+ # automatically declare primary key as a netzke attribute
75
+ netzke_attribute(self.primary_key)
76
+ exposed.insert(0, self.primary_key)
77
+ end
78
+ exposed
79
+ end
80
+
81
+ def primary_key
82
+ key.first.name.to_s
83
+ end
84
+
85
+ def column_names
86
+ properties.map(&:name).map(&:to_s)
87
+ end
88
+
89
+ def columns
90
+ properties
91
+ end
92
+
93
+ def columns_hash
94
+ properties.inject({}) { |hsh, prop|
95
+ hsh[prop.name.to_s] = prop
96
+ hsh
97
+ }
98
+ end
99
+
100
+ def property_with(name)
101
+ properties.find{|p| p.name == name.to_sym}
102
+ end
103
+
104
+ private
105
+ def netzke_attrs_in_forced_order(attrs)
106
+ attrs.collect do |attr_name|
107
+ declared = self.netzke_declared_attr.detect { |va| va[:name] == attr_name } || {}
108
+ in_columns_hash = columns_hash[attr_name] && {:name => attr_name, :attr_type => data_adapter.map_type(columns_hash[attr_name].class), :default_value => columns_hash[attr_name].default} || {} # {:virtual => true} # if nothing found in columns, mark it as "virtual" or not?
109
+ if in_columns_hash.empty?
110
+ # If not among the model columns, it's either virtual, or an association
111
+ merged = association_attr?(attr_name) ? declared.merge!(:name => attr_name) : declared.merge(:virtual => true)
112
+ else
113
+ # .. otherwise merge with what's declared
114
+ merged = in_columns_hash.merge(declared)
115
+ end
116
+
117
+ # We didn't find it among declared, nor among the model columns, nor does it seem association attribute
118
+ merged[:name].nil? && raise(ArgumentError, "Unknown attribute '#{attr_name}' for model #{self.name}", caller)
119
+
120
+ merged
121
+ end
122
+ end
123
+
124
+ # Returns netzke attributes in the order of columns in the table, followed by extra declared attributes
125
+ # Detects one-to-many association columns and replaces the name of the column with association column name (Netzke style), e.g.:
126
+ #
127
+ # role_id => role__name
128
+ def netzke_attrs_in_natural_order
129
+ (
130
+ declared_attrs = self.netzke_declared_attr
131
+
132
+ column_names.map do |name|
133
+ c = {:name => name, :attr_type => data_adapter.map_type(property_with(name).class)}
134
+
135
+ # If it's named as foreign key of some association, then it's an association column
136
+ # assoc = reflect_on_all_associations.detect { |a| foreign_key_for_assoc(a) == c[:name] }
137
+ assoc = relationships.detect { |r| r.child_key.first.name.to_s == c[:name] }
138
+ if assoc
139
+ assoc_class = assoc.parent_model
140
+ candidates = %w{name title label} << c[:name]
141
+ assoc_method = candidates.detect{|m| (assoc_class.instance_methods.map(&:to_s) + assoc_class.column_names).include?(m) }
142
+ c[:name] = "#{assoc.name}__#{assoc_method}"
143
+ c[:attr_type] = data_adapter.map_type(columns_hash[assoc_method]).try(:class) || :string # when it's an instance method rather than a column, fall back to :string
144
+ end
145
+
146
+ # auto set up the default value from the column settings
147
+ c.merge!(:default_value => property_with(name).default) if property_with(name).default
148
+
149
+ # if there's a declared attr with the same name, simply merge it with what's taken from the model's columns
150
+ if declared = declared_attrs.detect{ |va| va[:name] == c[:name] }
151
+ c.merge!(declared)
152
+ declared_attrs.delete(declared)
153
+ end
154
+ c
155
+ end +
156
+ declared_attrs
157
+ ).reject { |attr| self.netzke_excluded_attr.include?(attr[:name]) }
158
+ end
159
+
160
+ def association_attr?(attr_name)
161
+ !!attr_name.index("__") # probably we can't do much better than this, as we don't know at this moment if the associated model has a specific attribute, and we don't really want to find it out
162
+ end
163
+ end
164
+
165
+ # Transforms a record to array of values according to the passed attributes
166
+ def netzke_array(attributes = self.class.netzke_attributes)
167
+ res = []
168
+ for a in attributes
169
+ next if a[:included] == false
170
+ res << value_for_attribute(a, a[:nested_attribute])
171
+ end
172
+ res
173
+ end
174
+
175
+ def netzke_json
176
+ netzke_hash.to_nifty_json
177
+ end
178
+
179
+ # Accepts both hash and array of attributes
180
+ def netzke_hash(attributes = self.class.netzke_attributes)
181
+ res = {}
182
+ for a in (attributes.is_a?(Hash) ? attributes.values : attributes)
183
+ next if a[:included] == false
184
+ res[a[:name].to_sym] = self.value_for_attribute(a, a[:nested_attribute])
185
+ end
186
+ res
187
+ end
188
+
189
+ # Fetches the value specified by an (association) attribute
190
+ # If +through_association+ is true, get the value of the association by provided method, *not* the associated record's id
191
+ # E.g., author__name with through_association set to true may return "Vladimir Nabokov", while with through_association set to false, it'll return author_id for the current record
192
+ def value_for_attribute(a, through_association = false)
193
+ v = if a[:getter]
194
+ a[:getter].call(self)
195
+ elsif respond_to?("#{a[:name]}")
196
+ send("#{a[:name]}")
197
+ elsif is_association_attr?(a)
198
+ split = a[:name].to_s.split(/\.|__/)
199
+ assoc = self.class.relationships[split.first.to_sym]
200
+
201
+ if through_association
202
+ split.inject(self) do |r,m| # TODO: do we really need to descend deeper than 1 level?
203
+ if r.respond_to?(m)
204
+ r.send(m)
205
+ else
206
+ ::Rails.logger.debug "Netzke::Basepack: Wrong attribute name: #{a[:name]}" unless r.nil?
207
+ nil
208
+ end
209
+ end
210
+ else
211
+ self.send assoc.child_key.first.name
212
+ end
213
+ end
214
+
215
+ # a work-around for to_json not taking the current timezone into account when serializing ActiveSupport::TimeWithZone
216
+ v = v.to_datetime.to_s(:db) if [ActiveSupport::TimeWithZone].include?(v.class)
217
+ v = v.to_s(:db) if [DateTime, Date].include?(v.class)
218
+ v
219
+ end
220
+
221
+ # Assigns new value to an (association) attribute
222
+ def set_value_for_attribute(a, v)
223
+ v = v.to_time_in_current_zone if v.is_a?(Date) # convert Date to Time
224
+
225
+ if a[:setter]
226
+ a[:setter].call(self, v)
227
+ elsif respond_to?("#{a[:name]}=")
228
+ send("#{a[:name]}=", v)
229
+ elsif is_association_attr?(a)
230
+ split = a[:name].to_s.split(/\.|__/)
231
+ if a[:nested_attribute]
232
+ # We want:
233
+ # set_value_for_attribute({:name => :assoc_1__assoc_2__method, :nested_attribute => true}, 100)
234
+ # =>
235
+ # self.assoc_1.assoc_2.method = 100
236
+ split.inject(self) { |r,m| m == split.last ? (r && r.send("#{m}=", v) && r.save) : r.send(m) }
237
+ else
238
+ if split.size == 2
239
+ # search for association and assign it to self
240
+ assoc_name, assoc_method = split
241
+ relationship=self.class.relationships[assoc_name]
242
+
243
+ if relationship
244
+ if relationship.kind_of? ::DataMapper::Associations::OneToOne::Relationship
245
+ assoc_instance=self.send(assoc_name)
246
+ if assoc_instance
247
+ assoc_instance.send("#{assoc_method}=", v)
248
+ assoc_instance.save # what should we do when this fails?..
249
+ else
250
+ # what should we do in this case?
251
+ end
252
+ else
253
+ self.send("#{self.class.data_adapter.foreign_key_for assoc_name}=", v)
254
+ end
255
+ else
256
+ ::Rails.logger.debug "Netzke::Basepack: Association #{assoc} is not known for class #{self.class.name}"
257
+ end
258
+ else
259
+ ::Rails.logger.debug "Netzke::Basepack: Wrong attribute name: #{a[:name]}"
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ protected
266
+ # Returns true if passed attribute is an "association attribute"
267
+ def is_association_attr?(a)
268
+ # maybe the check is too simplistic, but will do for now
269
+ !!a[:name].to_s.index("__")
270
+ end
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,11 @@
1
+ module Netzke
2
+ module DataMapper
3
+ module ComboboxOptions
4
+ def netzke_combo_options_for(column, query = "")
5
+ # NOTE: :order=>[column.to_sym.asc] is necessary as per http://datamapper.org/docs/find.html, Version 1.2.0
6
+ values=all(:fields=>[column], :unique=>true, :order=>[column.to_sym.asc])
7
+ (query.blank? ? values : values.all(column.to_sym.like => "#{query}%")).map &column.to_sym
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,38 @@
1
+ module Netzke
2
+ module DataMapper
3
+ module RelationExtensions
4
+
5
+ def extend_with(*params)
6
+ scope = params.shift
7
+ case scope.class.name
8
+ when "Symbol" # model's scope
9
+ # In DataMapper case this is just a method
10
+ self.send(scope, *params)
11
+ when "String" # SQL query or SQL query with params (e.g. ["created_at < ?", 1.day.ago])
12
+ raise NotImplementedError.new("This method is unsupported, as DM doen't allow to extend relations with SQL")
13
+ when "Array"
14
+ self.extend_with(*scope)
15
+ when "Hash" # conditions hash
16
+ self.all(scope)
17
+ when "ActiveSupport::HashWithIndifferentAccess" # conditions hash
18
+ self.all(scope)
19
+ when "Proc" # receives a relation, must return a relation
20
+ scope.call(self)
21
+ else
22
+ raise ArgumentError, "Wrong parameter type for ActiveRecord::Relation#extend_with"
23
+ end
24
+ end
25
+
26
+ # Non-destructively extends itself whith a hash of double-underscore'd conditions,
27
+ # where the last part "__" is MetaWhere operator (which is required), e.g.:
28
+ # {:role__name__like => "%admin"}
29
+ def extend_with_netzke_conditions(cond)
30
+ cond.each_pair.inject(self) do |r, (k,v)|
31
+ assoc, method, *operator = k.to_s.split("__")
32
+ operator.empty? ? r.where(assoc.to_sym.send(method) => v) : r.where(assoc.to_sym => {method.to_sym.send(operator.last) => v}).joins(assoc.to_sym)
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ require 'netzke/sequel/attributes'
2
+ require 'netzke/sequel/combobox_options'
3
+ #require 'netzke/sequel/relation_extensions'
4
+
5
+ module Netzke
6
+ module Sequel
7
+ end
8
+ end
9
+
10
+ if defined? Sequel
11
+ # Extend Sequel
12
+ Sequel::Model.class_eval do
13
+ include ::Netzke::Sequel::Attributes
14
+ include ::Netzke::Sequel::ComboboxOptions
15
+ include ::Netzke::Sequel::RelationExtensions
16
+ end
17
+ end
18
+
@@ -0,0 +1,274 @@
1
+ module Netzke
2
+ module Sequel
3
+ module Attributes
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :netzke_declared_attr
8
+ self.netzke_declared_attr = []
9
+
10
+ class_attribute :netzke_excluded_attr
11
+ self.netzke_excluded_attr = []
12
+
13
+ class_attribute :netzke_exposed_attr
14
+ self.netzke_exposed_attr = []
15
+ end
16
+
17
+ def self.included receiver
18
+ receiver.extend ClassMethods
19
+ end
20
+
21
+ module ClassMethods
22
+ def data_adapter
23
+ @data_adapter = Netzke::Basepack::DataAdapters::AbstractAdapter.adapter_class(self).new(self)
24
+ end
25
+
26
+ # mostly AR compatible for our purposes ;-)
27
+ def columns_hash
28
+ db_schema.inject({}){|memo,(k,v)| memo[k.to_s] = v; memo}
29
+ end
30
+
31
+ def column_names
32
+ columns.map &:to_s
33
+ end
34
+
35
+ # human attribute name
36
+ def human_attribute_name attr
37
+ I18n.translate(attr, :scope => [:activerecord, :attributes, model_name.downcase.to_sym], :default => attr.to_s.humanize)
38
+ end
39
+
40
+ # Example:
41
+ # netzke_attribute :recent, :type => :boolean, :read_only => true
42
+ def netzke_attribute(name, options = {})
43
+ name = name.to_s
44
+ options[:attr_type] = options.delete(:type) || options.delete(:attr_type) || :string
45
+ declared_attrs = self.netzke_declared_attr.dup
46
+ # if the attr was declared already, simply merge it with the new options
47
+ existing = declared_attrs.detect{ |va| va[:name] == name }
48
+ if existing
49
+ existing.merge!(options)
50
+ else
51
+ attr_config = {:name => name}.merge(options)
52
+ # if primary_key, insert in front, otherwise append
53
+ if name == self.primary_key.to_s
54
+ declared_attrs.insert(0, attr_config)
55
+ else
56
+ declared_attrs << {:name => name}.merge(options)
57
+ end
58
+ end
59
+ self.netzke_declared_attr = declared_attrs
60
+ end
61
+
62
+ # Exclude attributes from being picked up by grids and forms.
63
+ # Accepts an array of attribute names (as symbols).
64
+ # Example:
65
+ # netzke_expose_attributes :created_at, :updated_at, :crypted_password
66
+ def netzke_exclude_attributes(*args)
67
+ self.netzke_excluded_attr = args.map(&:to_s)
68
+ end
69
+
70
+ # Explicitly expose attributes that should be picked up by grids and forms.
71
+ # Accepts an array of attribute names (as symbols).
72
+ # Takes precedence over <tt>netzke_exclude_attributes</tt>.
73
+ # Example:
74
+ # netzke_expose_attributes :name, :role__name
75
+ def netzke_expose_attributes(*args)
76
+ self.netzke_exposed_attr = args.map(&:to_s)
77
+ end
78
+
79
+ # Returns the attributes that will be picked up by grids and forms.
80
+ def netzke_attributes
81
+ exposed = netzke_exposed_attributes
82
+ exposed ? netzke_attrs_in_forced_order(exposed) : netzke_attrs_in_natural_order
83
+ end
84
+
85
+ def netzke_attribute_hash
86
+ netzke_attributes.inject({}){ |r,a| r.merge(a[:name].to_sym => a) }
87
+ end
88
+
89
+ def netzke_exposed_attributes
90
+ exposed = self.netzke_exposed_attr
91
+ if exposed && !exposed.include?(self.primary_key.to_s)
92
+ # automatically declare primary key as a netzke attribute
93
+ netzke_attribute(self.primary_key.to_s)
94
+ exposed.insert(0, self.primary_key.to_s)
95
+ end
96
+ exposed
97
+ end
98
+
99
+ private
100
+ def netzke_attrs_in_forced_order(attrs)
101
+ attrs.collect do |attr_name|
102
+ declared = self.netzke_declared_attr.detect { |va| va[:name] == attr_name } || {}
103
+ in_columns_hash = columns_hash[attr_name] && {:name => attr_name, :attr_type => columns_hash[attr_name][:type], :default_value => columns_hash[attr_name][:default]} || {} # {:virtual => true} # if nothing found in columns, mark it as "virtual" or not?
104
+ if in_columns_hash.empty?
105
+ # If not among the model columns, it's either virtual, or an association
106
+ merged = association_attr?(attr_name) ? declared.merge!(:name => attr_name) : declared.merge(:virtual => true)
107
+ else
108
+ # .. otherwise merge with what's declared
109
+ merged = in_columns_hash.merge(declared)
110
+ end
111
+
112
+ # We didn't find it among declared, nor among the model columns, nor does it seem association attribute
113
+ merged[:name].nil? && raise(ArgumentError, "Unknown attribute '#{attr_name}' for model #{self.name}", caller)
114
+
115
+ merged
116
+ end
117
+ end
118
+
119
+ # Returns netzke attributes in the order of columns in the table, followed by extra declared attributes
120
+ # Detects many-to-one association columns and replaces the name of the column with association column name (Netzke style), e.g.:
121
+ #
122
+ # role_id => role__name
123
+ def netzke_attrs_in_natural_order
124
+ (
125
+ declared_attrs = self.netzke_declared_attr
126
+
127
+ column_names.map do |name|
128
+ c = {:name => name, :attr_type => columns_hash[name][:type]}
129
+
130
+ # If it's named as foreign key of some association, then it's an association column
131
+ assoc = all_association_reflections.detect { |a| a[:key].to_s == c[:name] && a[:type] == :many_to_one }
132
+ if assoc
133
+ candidates = %w{name title label} << assoc[:key].to_s
134
+ assoc_class = assoc[:class_name].constantize
135
+ assoc_method = candidates.detect{|m| ( assoc_class.instance_methods.map(&:to_s) + column_names).include?(m) }
136
+ c[:name] = "#{assoc[:name].to_s}__#{assoc_method}"
137
+ c[:attr_type] = assoc_class.columns_hash[assoc_method].try(:[], :type) || :string # when it's an instance method rather than a column, fall back to :string
138
+ end
139
+
140
+ # auto set up the default value from the column settings
141
+ c.merge!(:default_value => columns_hash[name][:default]) if columns_hash[name][:default]
142
+
143
+ # if there's a declared attr with the same name, simply merge it with what's taken from the model's columns
144
+ if declared = declared_attrs.detect{ |va| va[:name] == c[:name] }
145
+ c.merge!(declared)
146
+ declared_attrs.delete(declared)
147
+ end
148
+ c
149
+ end +
150
+ declared_attrs
151
+ ).reject { |attr| self.netzke_excluded_attr.include?(attr[:name]) }
152
+ end
153
+
154
+ def association_attr?(attr_name)
155
+ !!attr_name.index("__") # probably we can't do much better than this, as we don't know at this moment if the associated model has a specific attribute, and we don't really want to find it out
156
+ end
157
+
158
+ end
159
+
160
+ # AR compatibility
161
+ def attributes
162
+ values
163
+ end
164
+
165
+ # Transforms a record to array of values according to the passed attributes
166
+ def netzke_array(attributes = self.class.netzke_attributes)
167
+ res = []
168
+ for a in attributes
169
+ next if a[:included] == false
170
+ res << value_for_attribute(a, a[:nested_attribute])
171
+ end
172
+ res
173
+ end
174
+
175
+ # convenience method to convert all netzke attributes of a model to nifty json
176
+ def netzke_json
177
+ netzke_hash.to_nifty_json
178
+ end
179
+
180
+ # Accepts both hash and array of attributes
181
+ def netzke_hash(attributes = self.class.netzke_attributes)
182
+ res = {}
183
+ for a in (attributes.is_a?(Hash) ? attributes.values : attributes)
184
+ next if a[:included] == false
185
+ res[a[:name].to_sym] = self.value_for_attribute(a, a[:nested_attribute])
186
+ end
187
+ res
188
+ end
189
+
190
+ # Fetches the value specified by an (association) attribute
191
+ # If +through_association+ is true, get the value of the association by provided method, *not* the associated record's id
192
+ # E.g., author__name with through_association set to true may return "Vladimir Nabokov", while with through_association set to false, it'll return author_id for the current record
193
+ def value_for_attribute(a, through_association = false)
194
+ v = if a[:getter]
195
+ a[:getter].call(self)
196
+ elsif respond_to?("#{a[:name]}")
197
+ send("#{a[:name]}")
198
+ elsif is_association_attr?(a)
199
+ split = a[:name].to_s.split(/\.|__/)
200
+ assoc = self.class.association_reflection(split.first.to_sym)
201
+ if through_association
202
+ split.inject(self) do |r,m| # TODO: do we really need to descend deeper than 1 level?
203
+ if r.respond_to?(m)
204
+ r.send(m)
205
+ else
206
+ logger.debug "Netzke::Basepack: Wrong attribute name: #{a[:name]}" unless r.nil?
207
+ nil
208
+ end
209
+ end
210
+ else
211
+ self.send("#{assoc[:key].to_s}")
212
+ end
213
+ end
214
+
215
+ # need to serialize Date and Time objects with to_s :db for compatibility with client side
216
+ # DATETIME fields in database are given as Time by Sequel
217
+ v = v.to_s(:db) if [Date, Time].include?(v.class)
218
+ v
219
+ end
220
+
221
+ # Assigns new value to an (association) attribute
222
+ def set_value_for_attribute(a, v)
223
+ v = v.to_time_in_current_zone if v.is_a?(Date) # convert Date to Time
224
+
225
+ if a[:setter]
226
+ a[:setter].call(self, v)
227
+ elsif respond_to?("#{a[:name]}=")
228
+ unless primary_key.to_s == a[:name] && v.blank? # In contrast to ActiveRecord, Sequel doesn't allow setting nil/NULL primary keys
229
+ send("#{a[:name]}=", v)
230
+ end
231
+ elsif is_association_attr?(a)
232
+ split = a[:name].to_s.split(/\.|__/)
233
+ if a[:nested_attribute]
234
+ # We want:
235
+ # set_value_for_attribute({:name => :assoc_1__assoc_2__method, :nested_attribute => true}, 100)
236
+ # =>
237
+ # self.assoc_1.assoc_2.method = 100
238
+ split.inject(self) { |r,m| m == split.last ? (r && r.send("#{m}=", v) && r.save) : r.send(m) }
239
+ else
240
+ if split.size == 2
241
+ # search for association and assign it to self
242
+ assoc = self.class.association_reflection(split.first.to_sym)
243
+ assoc_method = split.last
244
+ if assoc
245
+ if assoc[:type] == :one_to_one
246
+ assoc_instance = self.send(assoc[:name])
247
+ if assoc_instance
248
+ assoc_instance.send("#{assoc_method}=", v)
249
+ assoc_instance.save # what should we do when this fails?..
250
+ else
251
+ # what should we do in this case?
252
+ end
253
+ else
254
+ self.send("#{assoc[:key]}=", v)
255
+ end
256
+ else
257
+ logger.debug "Netzke::Basepack: Association #{assoc} is not known for class #{self.class.name}"
258
+ end
259
+ else
260
+ logger.debug "Netzke::Basepack: Wrong attribute name: #{a[:name]}"
261
+ end
262
+ end
263
+ end
264
+ end
265
+
266
+ protected
267
+ # Returns true if passed attribute is an "association attribute"
268
+ def is_association_attr?(a)
269
+ # maybe the check is too simplistic, but will do for now
270
+ !!a[:name].to_s.index("__")
271
+ end
272
+ end
273
+ end
274
+ end